美文网首页
数据结构-散列表(HashTable)

数据结构-散列表(HashTable)

作者: GemShi | 来源:发表于2019-01-31 17:06 被阅读53次
    本质

    哈希表的底层数据结构是数组,数组中每一个元素存放的是键值对。

    structure of hashtable
    核心原理

    f(key) = index

    将key传入,通过某个算法f,计算出索引,如果索引冲突,再通过某个办法对索引进行+1或-1再算一遍,直到算到不冲突为止。

    负载因子 = 总键值对数 / 哈希表总长度

    负载因子用来衡量用来衡量哈希表的空满程度,一般,当负载因子为0.75~1的值就会进行自动扩容。

    存取过程(以iOS方法缓存列表底层实现为例)

    example:Class内部结构中有个方法缓存列表,当objc_msgSend进行到搜索方法缓存列表的步骤时,为使查找更高效,缓存列表的底层就是通过哈希表实现对调用过的方法的缓存。
    1.存
    (1)根据key计算出索引值
    (2)如果该索引中没有元素,就存入键值对
    (3)如果该索引中有元素,索引冲突,就对索引进行+1或-1进行存储
    (4)如果当前已存在的所有索引都有元素,就进行哈希表的扩容
    (5)设置新空间是旧空间的2倍
    (6)重新分配内存
    (7)重新设置掩码_mask = newCapacity-1
    (8)会将旧内存释放掉,清空缓存

    • objc_cache.mm源码解析
    static void cache_fill_nolock(Class cls, SEL sel, IMP imp, id receiver)
    {
        cacheUpdateLock.assertLocked();
        // Never cache before +initialize is done
        if (!cls->isInitialized()) return;
        // Make sure the entry wasn't added to the cache by some other thread 
        // before we grabbed the cacheUpdateLock.
        if (cache_getImp(cls, sel)) return;
        cache_t *cache = getCache(cls);
        cache_key_t key = getKey(sel);
        // Use the cache as-is if it is less than 3/4 full
        mask_t newOccupied = cache->occupied() + 1;
        mask_t capacity = cache->capacity();
        if (cache->isConstantEmptyCache()) {
            // Cache is read-only. Replace it.
            cache->reallocate(capacity, capacity ?: INIT_CACHE_SIZE);
        }
        else if (newOccupied <= capacity / 4 * 3) {
            // Cache is less than 3/4 full. Use it as-is.
        }
        else {
            // Cache is too full. Expand it.
            cache->expand();
        }
        // Scan for the first unused slot and insert there.
        // There is guaranteed to be an empty slot because the 
        // minimum size is 4 and we resized at 3/4 full.
        bucket_t *bucket = cache->find(key, receiver);
        if (bucket->key() == 0) cache->incrementOccupied();
        //设置bucket中的key和imp
        bucket->set(key, imp);
    }
    //扩容
    void cache_t::expand()
    {
        cacheUpdateLock.assertLocked();
        
        uint32_t oldCapacity = capacity();
        //新的空间是旧的空间的2倍
        uint32_t newCapacity = oldCapacity ? oldCapacity*2 : INIT_CACHE_SIZE;
        if ((uint32_t)(mask_t)newCapacity != newCapacity) {
            // mask overflow - can't grow further
            // fixme this wastes one bit of mask
            newCapacity = oldCapacity;
        }
        //重新分配内存
        reallocate(oldCapacity, newCapacity);
    }
    
    void cache_t::reallocate(mask_t oldCapacity, mask_t newCapacity)
    {
        bool freeOld = canBeFreed();
        bucket_t *oldBuckets = buckets();
        bucket_t *newBuckets = allocateBuckets(newCapacity);
        // Cache's old contents are not propagated. 
        // This is thought to save cache memory at the cost of extra cache fills.
        // fixme re-measure this
        assert(newCapacity > 0);
        assert((uintptr_t)(mask_t)(newCapacity-1) == newCapacity-1);
        //会重新设置最新的_mask = newCapacity - 1;
        setBucketsAndMask(newBuckets, newCapacity - 1);
        
        if (freeOld) {
            //会将旧的释放掉,清空缓存
            cache_collect_free(oldBuckets, oldCapacity);
            cache_collect(false);
        }
    }
    

    2.取
    (1)通过key得到索引
    (2)do-while循环,如果散列表元素的key恰好等于按位与掩码(&_mask)取出的索引,就直接返回
    (3)如果不是,就将索引-1继续查找

    • objc_cache.mm源码解析
    bucket_t * cache_t::find(cache_key_t k, id receiver)
    {
        assert(k != 0);
        //返回buckets散列表
        bucket_t *b = buckets();
        mask_t m = mask();
        //得到索引
        mask_t begin = cache_hash(k, m);
        mask_t i = begin;
        do {
            //如果buket_t的索引的恰好等于通过&_mask取出的索引,就直接返回
            if (b[i].key() == 0  ||  b[i].key() == k) {
                return &b[i];
            }
        } while ((i = cache_next(i, m)) != begin);
        //如果不是,直接i-1,如果减到0还不行,就直接是_mask(相当于再从最后一位继续减)
        
        // hack
        Class cls = (Class)((uintptr_t)this - offsetof(objc_class, cache));
        cache_t::bad_cache(receiver, (SEL)k, cls);
    }
    
    static inline mask_t cache_hash(cache_key_t key, mask_t mask) 
    {
        //按位与mask得到索引
        return (mask_t)(key & mask);
    }
    
    #if __arm__  ||  __x86_64__  ||  __i386__
    // objc_msgSend has few registers available.
    // Cache scan increments and wraps at special end-marking bucket.
    #define CACHE_END_MARKER 1
    static inline mask_t cache_next(mask_t i, mask_t mask) {
        return (i+1) & mask;
    }
    #elif __arm64__
    // objc_msgSend has lots of registers available.
    // Cache scan decrements. No end marker needed.
    #define CACHE_END_MARKER 0
    static inline mask_t cache_next(mask_t i, mask_t mask) {
        //arm64架构,索引-1
        return i ? i-1 : mask;
    }
    
    总结

    Objective-C中的实现,优缺点并存,但相对于实际情况而言,还是优点大于缺点。因为对于方法缓存列表,一般不会有大量的数据,扩容或许是少数情况。此时,你可能会有疑问,当数据量少的时候,岂不是牺牲了内存?然而,哈希表就是采用了空间换时间,牺牲内存空间,换取存取效率的方法。所以,整体符合需求即可。
    优点:
    整体而言,提升了存取效率,时间复杂度为O(1),无需遍历。
    缺点:
    当哈希表比较大时,如果扩容会导致瞬间效率降低。

    不同编程语言,哈希表的实现也各有特点,但是本质和原理不变。

    例如:
    对于大量的数据或者数据库中的存取,Java和Redis也有自己的优化方法。
    1.Java中,当哈希函数不合理导致链表过长时,会使用红黑树来保证插入和查找的效率。
    2.Redis 使用增量式扩容方法优化了这个缺点,同时还有拉链法的实现(放在链表头部头插,因为新插入的调用概率会高)。

    相关文章

      网友评论

          本文标题:数据结构-散列表(HashTable)

          本文链接:https://www.haomeiwen.com/subject/wvgksqtx.html