哈希表(HashTable)

本质

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

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 使用增量式扩容方法优化了这个缺点,同时还有拉链法的实现(放在链表头部头插,因为新插入的调用概率会高)。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 223,726评论 6 521
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 95,697评论 3 402
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 170,734评论 0 366
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 60,508评论 1 300
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 69,522评论 6 399
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 53,051评论 1 314
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 41,429评论 3 427
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 40,403评论 0 278
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 46,930评论 1 323
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,977评论 3 343
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 41,122评论 1 354
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 36,763评论 5 350
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 42,454评论 3 336
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,931评论 0 25
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 34,047评论 1 275
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 49,613评论 3 380
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 46,150评论 2 363

推荐阅读更多精彩内容

  • --- layout: post title: "如果有人问你关系型数据库的原理,叫他看这篇文章(转)" date...
    蓝坠星阅读 796评论 0 3
  • 关于Mongodb的全面总结 MongoDB的内部构造《MongoDB The Definitive Guide》...
    中v中阅读 31,959评论 2 89
  • 今天看到一位朋友写的mysql笔记总结,觉得写的很详细很用心,这里转载一下,供大家参考下,也希望大家能关注他原文地...
    信仰与初衷阅读 4,737评论 0 30
  • 我想要生孩子,那只是因为我自认长了幅还不错的皮囊,不生孩子年岁老去,可惜了,我的孩子讲不定还可以风华绝代。 我需要...
    闭上耳朵阅读 76评论 0 0
  • 夜风起,酒稍挥 何人不睡待愁催。 玉岸累时尘。借书还时,仙人踏云南去。 今屋人,言慎微 草树红碧满窗悲。 星辰扶月...
    博宝宝阅读 386评论 0 0