文章目录
哈希表
花哨的算法比简单算法更容易出 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),非常的高效,这也是为什么它流行的原因了。
哈希函数
哈希表直观上理解就是链表数组(拉链法),一个好的哈希表在于关键字能快速定位且占用内存大小适宜。那这其中的关键就是哈希函数了。
作用
把任意长度的输入通过哈希算法变换成固定长度的输出。
特点
- 输入域是无穷大的,输出域是有限的
- 相同的输入必定对应相同的输出
- 不同的输入可能对应相同的输出
- 输出域中的每个元素哈希碰撞都是等概率的
第三个特点其实就是哈希碰撞,一个哈希函数的好坏基本上取决于第四个特点,换句话说,如果我们有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。微信公众号:后台技术栈。