Jamzy Wang

life is a struggle,be willing to do,be happy to bear~~~

Hash Table详解

2014-06-27 21:49

原创声明:本作品采用知识共享署名-非商业性使用 3.0 版本许可协议进行许可,欢迎转载,演绎,但是必须保留本文的署名(包含链接),且不得用于商业目的。

散列表(Hash table,也叫哈希表),是根据关键字(Key value)而直接访问在内存存储位置的数据结构。也就是说,它通过计算一个关于键值的函数,将所需查询的数据映射到表中一个位置来访问记录,这加快了查找速度。这个映射函数称做散列函数,存放记录的数组称做散列表。

应用场景

任何一个问题,当选择数据结构的时候一般只有有限的几个选择:数组、链表、二叉树(绝大多数的场景下都是使用二叉查找树或者其变体如平衡二叉树、红黑树等)、哈希表。二叉树上的一般操作(查找、插入、删除)的最优时间复杂度往往是O(log n),而hash表上的一般操作(查找、插入、删除)时间复杂度是O(1)。因此,当某个问题要求时间复杂度是O(1)时,一般只有使用哈希表这一个唯一的选择。当然,一个长度为n的哈希表的空间复杂度是O(n)。

小结:某个场景要求时间复杂度为O(1),考虑哈希表

In many situations, hash tables turn out to be more efficient than search trees or any other table lookup structure. For this reason, they are widely used in many kinds of computer software, particularly for associative arrays, database indexing, caches, and sets.

哈希映射

1) 根据key值计算hash值: hash = hashfunc(key)

1
2
3
hashfunc:散列函数
key:可以是任何类型的键值,最常见的是字符串、整型
hash:由于一般用数组来存放记录,因此hash值一般是整数

2) 将hash值映射到存放记录的数组中:index = hash % array_size

1
2
array_size:存放记录的数组的长度
index:记录在数组中的下标索引

注:hash是一种映射方式,指的是上面的(1)。hash的使用场景很多,比如加密哈希,局部敏感哈希,而hash table(上述的1,2)只是hash的一个应用场景。

当key是int类型的数据时,可以不用上述第(1)步处理,直接第(2)步计算index,即: 当key是其他类型的数据时,比如字符串,则必须先计算一个hash值才能计算index,即: A small phone book as a hash table:

哈希函数构造

一个性能良好的的哈希函数 hashfunc(key) 应该具备下面两个特点:

  1. the function should provide a uniform distribution of hash values
  2. Perfect hashing allows for constant time lookups in the worst case

常见散列函数

  • 直接寻址法:取关键字或关键字的某个线性函数值为散列地址。即 ,其中 为常数(这种散列函数叫做自身函数)

  • 数字分析法:假设关键字是以r为基的数,并且哈希表中可能出现的关键字都是事先知道的,则可取关键字的若干数位组成哈希地址。

  • 平方取中法:取关键字平方后的中间几位为哈希地址。通常在选定哈希函数时不一定能知道关键字的全部情况,取其中的哪几位也不一定合适,而一个数平方后的中间几位数和数的每一位都相关,由此使随机分布的关键字得到的哈希地址也是随机的。取的位数由表长决定。

  • 折叠法:将关键字分割成位数相同的几部分(最后一部分的位数可以不同),然后取这几部分的叠加和(舍去进位)作为哈希地址。

  • 随机数法

  • 除留余数法:取关键字被某个不大于散列表表长m的数p除后所得的余数为散列地址。即, 。不仅可以对关键字直接取模,也可在折叠法、平方取中法等运算之后取模。对p的选择很重要,一般取素数或m,若p选择不好,容易产生碰撞。

字符串哈希

在实际应用中最常遇到的问题是计算一个字符串的hash值,现有的用于计算字符串hash值的算法有很多,比如: DJB2 DJB2a (variant using xor rather than +) FNV-1 (32-bit) FNV-1a (32-bit) SDBM CRC32 Murmur2 (32-bit) SuperFastHash

注:这篇文章中详细介绍了常见的哈希算法:General Purpose Hash Function Algorithms

注:这篇文章中详细介绍了各种常见的字符串哈希算法的性能对比:Which hashing algorithm is best for uniqueness and speed

下面介绍两种使用最广泛的性能优良的字符串hash值计算方法:DJB Hash,Murmur Hash。Redis的字典实现中就使用了这两种哈希算法。 下面介绍两种使用最广泛的性能优良的字符串hash值计算方法:DJB Hash,Murmur Hash。Redis的字典实现中就使用了这两种哈希算法。

1. DJB Hash Function

An algorithm produced by Professor Daniel J. Bernstein and shown first to the world on the usenet newsgroup comp.lang.c.It is one of the most efficient hash functions ever published.

1
2
3
4
5
6
7
8
9
unsigned int DJBHash(const std::string& str)
{
   unsigned int hash = 5381;
   for(std::size_t i = 0; i < str.length(); i++)
   {
      hash = ((hash << 5) + hash) + str[i];
   }
   return hash;
}

2.Murmur Hash

MurmurHash 是一种非加密型哈希函数,适用于一般的哈希检索操作。 由Austin Appleby在2008年发明,并出现了多个变种,都已经发布到了公有领域。与其它流行的哈希函数相比,对于规律性较强的key,MurmurHash的随机分布特征表现更良好。当前的版本是MurmurHash3,能够产生出32-bit或128-bit哈希值。具体信息请参考 MurmurHash 的主页:http://code.google.com/p/smhasher/ 。

算法伪代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
Murmur3_32(key, len, seed)
    // Note: In this version, all integer arithmetic is performed with unsigned 32 bit integers.
    //       In the case of overflow, the result is constrained by the application of modulo  arithmetic.    
    c1  0xcc9e2d51    c2  0x1b873593    r1  15    r2  13    m  5    n  0xe6546b64
    hash  seed
    for each fourByteChunk of key
        k  fourByteChunk
        k  k * c1
        k  (k << r1) OR (k >> (32-r1))
        k  k * c2
        hash  hash XOR k
        hash  (hash << r2) OR (hash >> (32-r2))
        hash  hash * m + n
    with any remainingBytesInKey
        remainingBytes  SwapEndianOrderOf(remainingBytesInKey)
        // Note: Endian swapping is only necessary on big-endian machines.
        //       The purpose is to place the meaningful digits towards the low end of the value,
        //       so that these digits have the greatest potential to affect the low range digits
        //       in the subsequent multiplication.  Consider that locating the meaningful digits
        //       in the high range would produce a greater effect upon the high digits of the
        //       multiplication, and notably, that such high digits are likely to be discarded
        //       by the modulo arithmetic under overflow.  We don't want that.

        remainingBytes  remainingBytes * c1
        remainingBytes  (remainingBytes << r1) OR (remainingBytes >> (32 - r1))
        remainingBytes  remainingBytes * c2
        hash  hash XOR remainingBytes

    hash  hash XOR len
    hash  hash XOR (hash >> 16)
    hash  hash * 0x85ebca6b
    hash  hash XOR (hash >> 13)
    hash  hash * 0xc2b2ae35
    hash  hash XOR (hash >> 16)

算法C语言实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
uint32_t murmur3_32(const char *key, uint32_t len, uint32_t seed) {
        static const uint32_t c1 = 0xcc9e2d51;
        static const uint32_t c2 = 0x1b873593;
        static const uint32_t r1 = 15;
        static const uint32_t r2 = 13;
        static const uint32_t m = 5;
        static const uint32_t n = 0xe6546b64;

        uint32_t hash = seed;

        const int nblocks = len / 4;
        const uint32_t *blocks = (const uint32_t *) key;
        int i;
        for (i = 0; i < nblocks; i++) {
                uint32_t k = blocks[i];
                k *= c1;
                k = (k << r1) | (k >> (32 - r1));
                k *= c2;

                hash ^= k;
                hash = ((hash << r2) | (hash >> (32 - r2))) * m + n;
        }

        const uint8_t *tail = (const uint8_t *) (key + nblocks * 4);
        uint32_t k1 = 0;

        switch (len & 3) {
        case 3:
                k1 ^= tail[2] << 16;
        case 2:
                k1 ^= tail[1] << 8;
        case 1:
                k1 ^= tail[0];

                k1 *= c1;
                k1 = (k1 << r1) | (k1 >> (32 - r1));
                k1 *= c2;
                hash ^= k1;
        }

        hash ^= len;
        hash ^= (hash >> 16);
        hash *= 0x85ebca6b;
        hash ^= (hash >> 13);
        hash *= 0xc2b2ae35;
        hash ^= (hash >> 16);

        return hash;
}

# 碰撞处理 对不同的关键字可能得到同一散列地址,即 ,而 ,这种现象称为碰撞(Collision)。具有相同函数值的关键字对该散列函数来说称做同义词。处理碰撞的方法可以分为两大类:单独链表法、开发寻址法。

Separate chaining(单独链表法)

each bucket is independent, and has some sort of list of entries with the same index.

注:一个性能良好的哈希表,每个桶应该没有入口或者只有一个入口,有些时候可能会出现两个或者三个,极少有超过3个的。<

(图源)

时间复杂度分析

假设哈希函数产生的哈希值均匀分布在哈希表上,哈希表的长度可以动态增长,那么在哈希表上的插入、删除、查找操作的摊还时间复杂度是O(1),每次操作的实际消耗时间与哈希表的装载因子线性相关。 查找过程中,关键码的比较次数,取决于产生冲突的多少,产生的冲突少,查找效率就高,产生的冲突多,查找效率就低。因此,影响产生冲突多少的因素,也就是影响查找效率的因素。影响产生冲突多少有以下三个因素: 1. 散列函数是否均匀 2. 处理冲突的方 3. 散列表的载荷因子(load factor)

载荷因子

散列表的载荷因子定义为:

1
装载因子 = 填入表中的元素个数 / 散列表的长度

是散列表装满程度的标志因子。由于表长是定值,与“填入表中的元素个数”成正比,所以,越大,表明填入表中的元素越多,产生冲突的可能性就越大;反之,越小,标明填入表中的元素越少,产生冲突的可能性就越小。实际上,散列表的平均查找长度是载荷因子的函数,只是不同处理冲突的方法有不同的函数。

对于开放寻址法,荷载因子是特别重要因素,应严格限制在0.7-0.8以下。超过0.8,查表时的CPU缓存不命中(cache missing)按照指数曲线上升。因此,一些采用开放寻址法的hash库,如Java的系统库限制了荷载因子为0.75,超过此值将resize散列表。

C++实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
//链表节点定义
class LinkedHashEntry {
private:
      int key;
      int value;
      LinkedHashEntry *next;
public:
      LinkedHashEntry(int key, int value) {
            this->key = key;
            this->value = value;
            this->next = NULL;
      }

      int getKey() {
            return key;
      }

      int getValue() {
            return value;
      }

      void setValue(int value) {
            this->value = value;
      }

      LinkedHashEntry *getNext() {
            return next;
      }

      void setNext(LinkedHashEntry *next) {
            this->next = next;
      }
};
//哈希表长度
const int TABLE_SIZE = 128;

//哈希表定义
class HashMap {
private:
      LinkedHashEntry **table;
public:
      HashMap() {
            table = new LinkedHashEntry*[TABLE_SIZE];
            for (int i = 0; i < TABLE_SIZE; i++)
                  table[i] = NULL;
      }

      int get(int key) {
            int hash = (key % TABLE_SIZE);
            if (table[hash] == NULL)
                  return -1;
            else {
                  LinkedHashEntry *entry = table[hash];
                  while (entry != NULL && entry->getKey() != key)
                        entry = entry->getNext();
                  if (entry == NULL)
                        return -1;
                  else
                        return entry->getValue();
            }
      }

      void put(int key, int value) {
            int hash = (key % TABLE_SIZE);
            if (table[hash] == NULL)
                  table[hash] = new LinkedHashEntry(key, value);
            else {
                  LinkedHashEntry *entry = table[hash];
                  while (entry->getNext() != NULL)
                        entry = entry->getNext();
                  if (entry->getKey() == key)
                        entry->setValue(value);
                  else
                        entry->setNext(new LinkedHashEntry(key, value));
            }
      }

      void remove(int key) {
            int hash = (key % TABLE_SIZE);
            if (table[hash] != NULL) {
                  LinkedHashEntry *prevEntry = NULL;
                  LinkedHashEntry *entry = table[hash];
                  while (entry->getNext() != NULL && entry->getKey() != key) {
                        prevEntry = entry;
                        entry = entry->getNext();
                  }
                  if (entry->getKey() == key) {
                        if (prevEntry == NULL) {
                             LinkedHashEntry *nextEntry = entry->getNext();
                             delete entry;
                             table[hash] = nextEntry;
                        } else {
                             LinkedHashEntry *next = entry->getNext();
                              delete entry;
                             prevEntry->setNext(next);
                        }
                  }
            }
      }

      ~HashMap() {
            for (int i = 0; i < TABLE_SIZE; i++)
                  if (table[i] != NULL) {
                        LinkedHashEntry *prevEntry = NULL;
                        LinkedHashEntry *entry = table[i];
                        while (entry != NULL) {
                             prevEntry = entry;
                             entry = entry->getNext();
                             delete prevEntry;
                        }
                  }
            delete[] table;
      }
};

Open addressing(开放寻址法)

all entry records are stored in the bucket array itself.

(图源)

Hash collision resolved by open addressing with linear probing (interval=1). Note that “Ted Baker” has a unique hash, but nevertheless collided with “Sandra Dee”, that had previously collided with “John Smith”.

常用序列探测法

  • Linear probing(线性探测法), in which the interval between probes is fixed (usually 1)
  • Quadratic probing(二次探测法), in which the interval between probes is increased by adding the successive outputs of a quadratic polynomial to the starting value given by the original hash computation
  • Double hashing(双散列), in which the interval between probes is computed by another hash function

, ,其中为散列函数,m为散列表长, 为增量序列,为已发生碰撞的次数。增量序列可有下列取法: 称为 线性探测(Linear Probing);即,或者为其他线性函数。相当于逐个探测存放地址的表,直到查找到一个空单元,把散列地址存放在该空单元。 称为 平方探测(Quadratic Probing)。相对线性探测,相当于发生碰撞时探测间隔 个单元的位置是否为空,如果为空,将地址存放进去。 伪随机数序列,称为 伪随机探测。

聚集(Cluster)

在函数地址的表中,散列函数的结果不均匀地占据表的单元,形成区块,造成线性探测产生一次聚集(primary clustering)和平方探测的二次聚集(secondary clustering),散列到区块中的任何关键字需要查找多次试选单元才能插入表中,解决冲突,造成时间浪费。对于开放寻址法,聚集会造成性能的灾难性损失,是必须避免的。

线性探测法示例

插入元素发生碰撞:Andrew Wilson与JAck Williams冲突,Andrew Wilson按存放到哈希表的下一个空位中。 删除元素操作:现在要删除元素Sandra Miller: 如果简单的从哈希表中直接删除 “Sandra Miller” ,即将”Sandra Miller”处设置为空,如下图所示,那么哈希表将被破坏,通过查找算法将找不到 “Andrew Wilson” 。因为查找算法在遇到第一个为空的bucket时查找过程就会终止,也就是说查找到“Sandra Miller”的位置,查找过程就结束。 解决这个问题的方法是将删除元素的位置用一个特殊的值替代,这个特殊的值不能和任何key相等。现在查找算法就可以正常进行。若之后再次插入元素,那么被删除的位置将被重新使用。 ### 时间复杂度分析 假设哈希函数使得哈希值均匀分布,那么在插入、删除、查找的摊还时间复杂度是O(1)。 开发寻址法对哈希函数的选取非常敏感,哈希表的性能对哈希表的装载因子也很敏感。

Performance of the hash tables, based on open addressing scheme is very sensitive to the table’s load factor. If load factor exceeds 0.7 threshold, table’s speed drastically degrades. Indeed, length of probe sequence is proportional to (loadFactor) / (1 - loadFactor) value. In extreme case, when loadFactor approaches 1, length of the sequence approaches infinity. In practice it means, that there are no more free slots in the table and algorithm will never find place to insert a new element. Hence, this kind of hash tables should support dynamic resizing in order to be efficient. ### C++实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
class HashEntry {
private:
      int key;
      int value;
public:
      HashEntry(int key, int value) {
            this->key = key;
            this->value = value;
      }

      int getKey() {
            return key;
      }

      int getValue() {
            return value;
      }

      void setValue(int value) {
            this->value = value;
      }
};
class DeletedEntry: public HashEntry {
private:
      static DeletedEntry *entry;
      DeletedEntry() :
            HashEntry(-1, -1) {
      }
public:
      static DeletedEntry *getUniqueDeletedEntry() {
            if (entry == NULL)
                  entry = new DeletedEntry();
            return entry;
      }
};

DeletedEntry *DeletedEntry::entry = NULL;
const int TABLE_SIZE = 128;

class HashMap {
private:
      HashEntry **table;
public:
      HashMap() {
            table = new HashEntry*[TABLE_SIZE];
            for (int i = 0; i < TABLE_SIZE; i++)
                  table[i] = NULL;
      }

      int get(int key) {
            int hash = (key % TABLE_SIZE);
            int initialHash = -1;
            while (hash != initialHash && (table[hash]
                        == DeletedEntry::getUniqueDeletedEntry() || table[hash] != NULL
                        && table[hash]->getKey() != key)) {
                  if (initialHash == -1)
                        initialHash = hash;
                  hash = (hash + 1) % TABLE_SIZE;
            }
            if (table[hash] == NULL || hash == initialHash)
                  return -1;
            else
                  return table[hash]->getValue();
      }

      void put(int key, int value) {
            int hash = (key % TABLE_SIZE);
            int initialHash = -1;
            int indexOfDeletedEntry = -1;
            while (hash != initialHash && (table[hash]
                        == DeletedEntry::getUniqueDeletedEntry() || table[hash] != NULL
                        && table[hash]->getKey() != key)) {
                  if (initialHash == -1)
                        initialHash = hash;
                  if (table[hash] == DeletedEntry::getUniqueDeletedEntry())
                        indexOfDeletedEntry = hash;
                  hash = (hash + 1) % TABLE_SIZE;
            }
            if ((table[hash] == NULL || hash == initialHash) && indexOfDeletedEntry
                        != -1)
                  table[indexOfDeletedEntry] = new HashEntry(key, value);
            else if (initialHash != hash)
                  if (table[hash] != DeletedEntry::getUniqueDeletedEntry()
                             && table[hash] != NULL && table[hash]->getKey() == key)
                        table[hash]->setValue(value);
                  else
                        table[hash] = new HashEntry(key, value);
      }

      void remove(int key) {
            int hash = (key % TABLE_SIZE);
            int initialHash = -1;
            while (hash != initialHash && (table[hash]
                        == DeletedEntry::getUniqueDeletedEntry() || table[hash] != NULL
                        && table[hash]->getKey() != key)) {
                  if (initialHash == -1)
                        initialHash = hash;
                  hash = (hash + 1) % TABLE_SIZE;
            }
            if (hash != initialHash && table[hash] != NULL) {
                  delete table[hash];
                  table[hash] = DeletedEntry::getUniqueDeletedEntry();
            }
      }

      ~HashMap() {
            for (int i = 0; i < TABLE_SIZE; i++)
                  if (table[i] != NULL && table[i]
                             != DeletedEntry::getUniqueDeletedEntry())
                        delete table[i];
            delete[] table;
      }
};

### 开放寻址法 VS 链地址法

哈希表的动态增长

随着哈希表装载因子的变大,发生碰撞的次数变得越来也多,哈希表的性能变得越来越差。对于单独链表法实现的哈希表,尚可以容忍,但是对于开放寻址法,这种性能的下降是不能接受的,因此对于开放寻址法需要寻找一种方法解决这个问题。

在实际应用中,解决这个问题的办法是动态的增大哈希表的长度,当装载因子超过某个阈值时增加哈希表的长度。 ### 动态增长算法 当哈希表的长度发生变化之后,所有key在哈希表中对应的下标索引需要全部重新计算,不能直接从原来的哈希表中拷贝到新的哈希表中。必须一个一个计算原来哈希表中的key的哈希值并插入到新的哈希表中。 动态增长算法的时间复杂度分析: 动态增长不影响哈希表插入、删除、查找的摊还时间复杂度。但是当哈希表发生动态增长时,需要O(n)的时间完成操作,这个在实时应用场景中可能并不合适。

C++实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
const int DEFAULT_TABLE_SIZE = 128;

class HashMap {
private:
      float threshold;
      int maxSize;
      int tableSize;
      int size;
      LinkedHashEntry **table;

      void resize() {
            int oldTableSize = tableSize;
            tableSize *= 2;
            maxSize = (int) (tableSize * threshold);
            LinkedHashEntry **oldTable = table;
            table = new LinkedHashEntry*[tableSize];
            for (int i = 0; i < tableSize; i++)
                  table[i] = NULL;
            size = 0;
            for (int hash = 0; hash < oldTableSize; hash++)
                  if (oldTable[hash] != NULL) {
                        LinkedHashEntry *oldEntry;
                        LinkedHashEntry *entry = oldTable[hash];
                        while (entry != NULL) {
                             put(entry->getKey(), entry->getValue());
                             oldEntry = entry;
                             entry = entry->getNext();
                             delete oldEntry;
                        }
                  }
            delete[] oldTable;
      }

public:
      HashMap() {
            threshold = 0.75f;
            maxSize = 96;
            tableSize = DEFAULT_TABLE_SIZE;
            size = 0;
            table = new LinkedHashEntry*[tableSize];
            for (int i = 0; i < tableSize; i++)
                  table[i] = NULL;
      }

      void setThreshold(float threshold) {
            this->threshold = threshold;
            maxSize = (int) (tableSize * threshold);
      }

      int get(int key) {
            int hash = (key % tableSize);
            if (table[hash] == NULL)
                  return -1;
            else {
                  LinkedHashEntry *entry = table[hash];
                  while (entry != NULL && entry->getKey() != key)
                        entry = entry->getNext();
                  if (entry == NULL)
                        return -1;
                  else
                        return entry->getValue();
            }
      }

      void put(int key, int value) {
            int hash = (key % tableSize);
            if (table[hash] == NULL) {
                  table[hash] = new LinkedHashEntry(key, value);
                  size++;
            } else {
                  LinkedHashEntry *entry = table[hash];
                  while (entry->getNext() != NULL)
                        entry = entry->getNext();
                  if (entry->getKey() == key)
                        entry->setValue(value);
                  else {
                        entry->setNext(new LinkedHashEntry(key, value));
                        size++;
                  }
            }
            if (size >= maxSize)
                  resize();
      }

      void remove(int key) {
            int hash = (key % tableSize);
            if (table[hash] != NULL) {
                  LinkedHashEntry *prevEntry = NULL;
                  LinkedHashEntry *entry = table[hash];
                  while (entry->getNext() != NULL && entry->getKey() != key) {
                        prevEntry = entry;
                        entry = entry->getNext();
                  }
                  if (entry->getKey() == key) {
                        if (prevEntry == NULL) {
                             LinkedHashEntry *nextEntry = entry->getNext();
                             delete entry;
                             table[hash] = nextEntry;
                        } else {
                             LinkedHashEntry *next = entry->getNext();
                             delete entry;
                             prevEntry->setNext(next);
                        }
                        size--;
                  }
            }
      }

      ~HashMap() {
            for (int hash = 0; hash < tableSize; hash++)
                  if (table[hash] != NULL) {
                        LinkedHashEntry *prevEntry = NULL;
                        LinkedHashEntry *entry = table[hash];
                        while (entry != NULL) {
                             prevEntry = entry;
                             entry = entry->getNext();
                             delete prevEntry;
                        }
                  }
            delete[] table;
      }
};

### 渐进式动态增长算法 动态增长算法在触发增长操作时,需要耗费O(n)的时间,在实时应用场景中这是不能接受的,在redis字典的实现中,作者提出了一种性能良好的适合实时应用场景的resize算法,使得触发增长时也只需要常数时间完成插入操作。作者的解决办法是分多次、渐进式地完成的旧哈希表到新哈希表的拷贝而不是一次拷贝完成。

redis字典的 rehash本质上就是哈希表的resize过程, 实际上就是执行以下任务: 1. 创建一个比 ht[0]->table 更大的 ht[1]->table ; 2. 将 ht[0]->table 中的所有键值对迁移到 ht[1]->table ; 3. 将原有 ht[0] 的数据清空,并将 ht[1] 替换为新的 ht[0] ;

经过以上步骤之后, 程序就在不改变原有键值对数据的基础上, 增大了哈希表的大小。 作为例子, 以下四个小节展示了一次对哈希表进行 rehash 的完整过程。 1 . 开始 rehash

这个阶段有两个事情要做: 1. 设置字典的 rehashidx 为 0 ,标识着 rehash 的开始; 2. 为 ht[1]->table 分配空间,大小至少为 ht[0]->used 的两倍;

这时的字典是这个样子: (图源)

2 . Rehash 进行中

在这个阶段, ht[0]->table 的节点会被逐渐迁移到 ht[1]->table , 因为 rehash 是分多次进行的(细节在下一节解释), 字典的rehashidx 变量会记录 rehash 进行到 ht[0] 的哪个索引位置上。 以下是 rehashidx 值为 2 时,字典的样子: (图源) 注意除了节点的移动外, 字典的 rehashidx 、 ht[0]->used 和 ht[1]->used 三个属性也产生了变化。

3 . 节点迁移完毕到了这个阶段,所有的节点都已经从 ht[0] 迁移到 ht[1] 了: (图源)

4 . Rehash 完毕

在 rehash 的最后阶段,程序会执行以下工作: 释放 ht[0] 的空间; 用 ht[1] 来代替 ht[0] ,使原来的 ht[1] 成为新的 ht[0] ; 创建一个新的空哈希表,并将它设置为 ht[1] ; 将字典的 rehashidx 属性设置为 -1 ,标识 rehash 已停止; 以下是字典 rehash 完毕之后的样子: (图源) 对比字典 rehash 前后, 新的 ht[0] 空间更大, 并且字典原有的键值对也没有被修改或者删除。

参考文献

Comments