redis-dict上

哈希表

花哨的算法比简单算法更容易出 bug、更难实现。尽量使用简单的算法配合简单的数据结构。
只要掌握了数据结构中的四大法宝,就可以包打天下,他们是:array 、linked list 、hash table、binary tree 。这四大法宝可不是各自为战的,灵活结合才能游刃有余。比如,一个用hash table组织的symbol table,其中是一个个由字符型array构成的linked list。

以数据为中心。如果已经选择了正确的数据结构并且把一切都组织得井井有条,正确的算法也就不言自明。编程的核心是数据结构,而不是算法。

  上面的两条语录是节选自golang的缔造者核心人物RobPike。以数据结构为中心而不是算法,这样的思想和Linus的一致,大神就是不一样。
  RobPike提到了数据结构的四大法宝,我们的sds可以说是array的应用,adlist是linklist的应用,今天要讲的字典就是hash table的应用,在redis中没有使用二叉树去做搜索,而是采用了跳跃表,我觉得这个是可取的,毕竟跳跃表相比红黑树来说更容易实现和掌握。虽然没有用到binary tree,不过redis数据结构部分也会涉及到三种了,如果各位同学想了解binary tree的常用知识点,可以在文章留言,三儿会在数据结构部分结束后追加一篇binary tree的文章。
  大神都说法宝了那就速速学习吧,我们都知道字典的底层实现是哈希表,所以我们接下来都以哈希表来描述了。哈希表的增删改查均摊时间复杂度都是O(1),非常的高效,这也是为什么它流行的原因了。

哈希函数

  哈希表直观上理解就是链表数组(拉链法),一个好的哈希表在于关键字能快速定位且占用内存大小适宜。那这其中的关键就是哈希函数了。

作用

  把任意长度的输入通过哈希算法变换成固定长度的输出。

特点

  1. 输入域是无穷大的,输出域是有限的
  2. 相同的输入必定对应相同的输出
  3. 不同的输入可能对应相同的输出
  4. 输出域中的每个元素哈希碰撞都是等概率的

  第三个特点其实就是哈希碰撞,一个哈希函数的好坏基本上取决于第四个特点,换句话说,如果我们有200个输入,哈希表的长度是20,那么每个链表的长度理想情况下都是10,直观上看就是元素分布的比较均匀。那么一个不太好的哈希函数可能导致只有一个链表,这个链表的长度是200。
  上面我我们已经说过哈希函数的特点了,但是比较抽象,那么直观的来说呢?结合刚才三儿说的那两个描述第四个特点的例子,那么就应该是用来打乱输入规律的。既然打乱了输入规律,那么元素就分布的比较均匀,虽然不太可能叫出现上述例子那么理想的情况,但也基本避免了那种极坏的场景。这样就可以保证内存空间不会浪费了。所以说哈希函数决定了定位的快慢和占用内存的是否合理。

结构

  先从宏观上认识一下哈希表的结构组成。数组+链表+哈希算法

节点

typedef struct dictEntry {
    void *key;
    union {
        void *val;
        uint64_t u64;
        int64_t s4;
        double d;
    }v;
    struct dictEntry *next
} dictEntry;

哈希表

typedef struct dictht {
    dictEntry **table;             
    unsigned long size;            
    unsigned long sizemask;        
    unsigned long used;
} dictht;
  • table:拉链法中的数组
  • size:数组容量
  • sizemask:用于计算索引
  • used:当前数组下的链表所有的节点个数

字典

typedef struct dict {
    dictType *type;
    void *privdata;
    dictht th[2];              
    long rehashidx;
    unsigned long iterators         
} dict;
  • th:哈希表使用了两个数组,至于为什么事两个,源码会解析
  • rehashidx:用于区分是否在进行rehash的过程,这是redis对rehash的优化手段

总结

  今天分享了哈希表的基本原理,主要是哈希函数和哈希表的组成,暂时还没有设计源码的解读,因为昨天有读者提意见文章太长,三儿觉得也是,所以分开写,容易消化。

问题

  • 哈希表节点值部分使用了共用体,你了解共用体的底层原理吗
  • dict中的type成员和private成员有什么作用呢
  • dict中为什么使用了两个哈希表呢
  • redis中对于哈希表的rehash过程做了什么优化呢

  如果你知道其中答案那么留言吧,不要掩饰你的才华。

dictIterator

typedef struct dictIterator {
    dict *d;
    long index;
    int table, safe;
    dictEntry *entry, *nextEntry;
    
    long long fingerprint;
} dictIterator

API

创建类函数

一个创建操作用了三个函数,尽可能的把可以重用的代码都分开,提高代码的可用性。

_dictReset
static void _dictReset(dictht *ht)
{
    ht->table = NULL;
    ht->size = 0;
    ht->sizemask = 0;
    ht->used = 0;
}
dictCreate
dict *dictCreate(dictType *type, void *privDataPtr)
{
    dict *d = zmalloc(sizeof(*d));
    
    _dictInit(d, type, privDataPtr);
    return d
}
dictInit
int _dictInit(dict *d, dictTyoe *type, void *privDataPtr)
{
    _dictReset(&d->ht[0]);
    _dictReset(&d->ht[1]);
    
    d->type = type;
    d->privdata = privDataPtr;
    d->iterators = 0;
    d->rehasidx = -1
    
    return DICT_OK
}

查找类操作

dictFind
dictEntry *dictFind(dict *d, const void *key)
{
    dictEntry *he;
    uint64_t h, idx, table;
    
    /*如果哈希表中没有任何entry,则直接返回*/
    if (d->ht[0].used + d->ht[1].used == 0)
        return NULL;
        
    /*如果哈希表rehash还没有结束,执行一次rehash*/
    if (dictIsRehashing(d))
        _dictRehashStep(d);
        
    /*的哦到键值对应哈希值*/
    h = dictHasKey(d, key);
    
    /*遍历两个table哈希值对应的链表,如果存在键值相等的即返回*/
    for (table = 0; table <=1; table++) {
        idx = h & d->ht[table].sizemak;
        he = d->ht[table].table[idx];
        while (he) {
            if (key == he->key || dictCompareKeys(d, key, he->key))
                return he;
            he = he->next;
        }
        
        //如果没有在rehash的过程,那么不需要查看table[1],因为此时所有的数据都在table[0]
        if (!dictIsRehashing(d))
            return NULL;
    }
}

增加类操作

#define dictIsRehashing(d) (d->rehashidx != -1)
dictAddRaw
dictEntry *dictAddRaw(dict *d, void *key, dictEntry **existing)
{
    long index;
    dictEntry *entry;
    dictht *ht;
    
    /*rehash是一个渐进的过程,所以先判断是否处在rehash的过程,如果在则执行一次rehash的操作*/
    if (dectIsRehashing(d)) 
        _dictRehashStep(d);
        
    /*查询一个键在hash表中位于的数组的下标,-1代表不存在*/
    if ((index = _dictKeyIndex(d, key, dictHashKey(d,key), existing)) == -1)
        return NULL;
        
    ht = dictIsRehashing(d) ? &d->ht[1] : &d->ht[0]         //rehash过程中直接添加到ht[1]中,避免了rehash的工作
    entry = zmalloc(sizeof(*entry));
    entry->next = ht->table[index];
    ht->table[index] = entry;
    ht->used++;
    
    /*设置entry的键值*/
    dictSetKey(d, entry, key);                          
    return entry
}
dictReplace
/*添加或者修改都是用过这个函数实现,通过键是否存在判断是添加还是修改*/

int dictReplace(dict *d, void *key, void* val)
{
    dictEntry *entry, *existing, auxentry;

    /*尝试添加一个entry到dict,如果添加失败证明dict中已经存在key*/        
    entry = dictAddRow(d, key, val);
    if (entry) {
        dictSetVal(d, entry, val)
        return 1
    }
    
    /*走到这里证明是修改操作,entry设置新值,释放原来值的空间*/
    auxentry = *existing;
    dictSetVal(d, existing, val);
    dictFreeVal(d, &auxentry);
    return 0
}

关于作者

大四学生一枚,分析数据结构,面试题,golang,C语言等知识。QQ交流群:521625004。微信公众号:后台技术栈。
image

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值