从0到1打牢算法基础之手写一个哈希表

0.导语

目的:手写实现一个哈希表,采用拉链法构建,每个hash(key)对应的是一个红黑树。

看起来很简单,但可以学到很多东西。实现语言:C++。

为了打牢算法基础,github开了个仓库,来完成后面算法基础的应用与实现,地址:

https://github.com/Light-City/algPratice

也可以点击原文阅读。

1.简易版哈希表

我们将哈希表封装在一个类中,完成遍历的定义与声明以及构造、析构的实现:

  1. template<typename Key, typename Value>
  2. class HashTable {
  3. private:
  4. const static int upperTol = 3;
  5. const static int lowerTol = 1;
  6. const static int initCapacity = 1;
  7. map<Key, Value> **hashtable;
  8. int M;
  9. int size;
  10. public:
  11. /**
  12. * 传参构造
  13. * @param M
  14. */
  15. HashTable(int M) : M(M), size(0) {
  16. // 这里的括号是为了初始化为0,这就可以不用写下面的代码,当然在后面add之类的操作,就不需要动态分配内存.
  17. // this->hashtable = new map<Key, Value> *[M]();
  18. this->hashtable = new map<Key, Value> *[M];
  19. for (int i = 0; i < M; i++) {
  20. this->hashtable[i] = new map<Key, Value>;
  21. }
  22. }
  23. /**
  24. * 默认构造
  25. */
  26. HashTable() {
  27. HashTable(initCapacity);
  28. }
  29. /**
  30. * 析构函数,释放内存
  31. */
  32. ~HashTable() {
  33. free(M);
  34. }
  35. private:
  36. /**
  37. * 释放内存
  38. * @param M
  39. */
  40. void free(int M) {
  41. for (int i = 0; i < M; i++) {
  42. if (hashtable[i])
  43. delete hashtable[i];
  44. }
  45. delete[]hashtable;
  46. };

对于哈希表实现,里面有一个比较重要的哈希函数,这里我们先自己定义一个:

  1. /**
  2. * 哈希函数
  3. * @param key
  4. * @return
  5. */
  6. int hashFunc(Key key) {
  7. std::hash<Key> h;
  8. return (h(key) & 0x7fffffff) % M;
  9. }

这里使用std的hash得到值之后,将其&0x7fffffff,去掉高位的负号,转为正数,然后余上M。

现在有了这些我们来实现一下它的增删改查。

增操作

底层采用的是红黑树,插入是使用insert方法,通过构造一个pair来完成。而当key存在的时候,更新值即可,对于更新这一块,如果直接使用insert是不起作用的,比如下面测试:

  1. map<string,int> m{{"a",1},{"b",2}};
  2. for(auto i:m) cout<<i.first<<" "<<i.second<<" ";
  3. cout<<endl;
  4. m.insert({{"a",2}});
  5. for(auto i:m) cout<<i.first<<" "<<i.second<<" ";
  6. cout<<endl;
  7. m["a"]=2;
  8. for(auto i:m) cout<<i.first<<" "<<i.second<<" ";
  9. cout<<endl;

输出:

  1. a 1 b 2
  2. a 1 b 2
  3. a 2 b 2

因此,如果要修改key对应的value,可以通过[]来修改,还可以先删除,再插入,这里就用这个方法。

  1. /**
  2. * 添加新元素
  3. * @param key
  4. * @param value
  5. */
  6. void add(Key key, Value value) {
  7. // 拉链法出来的map如果为空,就动态分配一个map,然后进行插入
  8. // 如果key不存在就看内存是否存在,不存在,就分配,存在就插入
  9. if (hashtable[hashFunc(key)] == NULL || hashtable[hashFunc(key)]->count(key) == 0) {
  10. if (hashtable[hashFunc(key)] == NULL)
  11. hashtable[hashFunc(key)] = new map<Key, Value>;
  12. hashtable[hashFunc(key)]->insert(make_pair(key, value));
  13. size++;
  14. if (size >= maxCapacity())
  15. resize(2 * M);
  16. } else {
  17. // 否则,修改value.
  18. hashtable[hashFunc(key)]->erase(key);
  19. hashtable[hashFunc(key)]->insert(make_pair(key, value));
  20. }
  21. }

删操作

如果key存在,就删除,size—,否则返回失败标记。

  1. /**
  2. * 移除Key
  3. * @param key
  4. * @return 0 success -1 fail
  5. */
  6. Value remove(Key key) {
  7. Value ret = -1;
  8. // 是否包含key,若包含key,则直接删除
  9. if (contains(key)) {
  10. hashtable[hashFunc(key)]->erase(key);
  11. size--;
  12. // if (size == 0) delete hashtable[hashFunc(key)]; // 可以添加这行来动态减少内存
  13. ret = 0;
  14. // initCapacity 保证不会越界
  15. if (size < minCapacity() && M / 2 >= initCapacity) resize(M / 2);
  16. }
  17. return ret;
  18. }

改操作

前面提到过,这里就直接放代码。

  1. /**
  2. * 重设value
  3. * @param key
  4. * @param value
  5. */
  6. void set(Key key, Value value) {
  7. // key不存在
  8. if (!contains(key))
  9. hrow "key not exists!";
  10. // 修改value
  11. hashtable[hashFunc(key)]->erase(key);
  12. hashtable[hashFunc(key)]->insert(make_pair(key, value));
  13. }

查操作

获取key对应的value。

  1. /**
  2. * 获取key对应的value
  3. * @param key
  4. * @return
  5. */
  6. Value get(Key key) {
  7. if (contains(key))
  8. return hashtable[hashFunc(key)]->at(key);
  9. return 0;
  10. }

最后,上面有containsresize等函数未提。

key存在与否

首先contains函数实现,就是判断key存在与否:

  1. /**
  2. * 是否包含key
  3. * @param key
  4. * @return
  5. */
  6. bool contains(Key key) {
  7. return hashtable[hashFunc(key)] == NULL || this->hashtable[hashFunc(key)]->count(key) == 0 ? false : true;
  8. }

获取size

  1. /**
  2. * 获取哈希表元素个数
  3. * @return
  4. */
  5. int getSize() {
  6. return size;
  7. }

最大容量与最小容量

  1. /**
  2. * 最大容量
  3. * @return
  4. */
  5. Value maxCapacity() {
  6. return M * upperTol;
  7. }
  8. /**
  9. * 最小容量
  10. * @return
  11. */
  12. Value minCapacity() {
  13. return M * lowerTol;
  14. }

resize函数

完成动态调整内存,将原来内存中的内容拷贝到新分配的空间,释放原空间!

  1. /**
  2. * 动态调整内存,保证时间复杂度O(1)查找
  3. * 把扩容后的操作,平摊到前面每次操作,时间复杂度O(2),那就是O(1)了
  4. * @param newM
  5. */
  6. void resize(int newM) {
  7. cout << "resize " << newM << endl;
  8. map<Key, Value> **newHashTable = new map<Key, Value> *[newM];
  9. for (int i = 0; i < newM; i++) {
  10. newHashTable[i] = new map<Key, Value>;
  11. }
  12. int oldM = M;
  13. this->M = newM;
  14. for (int i = 0; i < oldM; i++) {
  15. map<Key, Value> m = *(hashtable[i]);
  16. for (auto p:m)
  17. newHashTable[hashFunc(p.first)]->insert(make_pair(p.first, p.second));
  18. }
  19. free(oldM);
  20. this->hashtable = newHashTable;
  21. }

重载<< 操作符

声明:

  1. private:
  2. template<typename K, typename V>
  3. // 重载<<操作符
  4. friend ostream &operator<<(ostream &out, HashTable<K, V> &hashTable);

定义:

  1. template<typename K, typename V>
  2. ostream &operator<<(ostream &out, HashTable<K, V> &hashTable) {
  3. hashTable.print();
  4. return out;
  5. }

至此,上述哈希表实现完毕,现在来测试:

  1. #include "hash.h"
  2. #include <vector>
  3. int main() {
  4. vector<string> words{"java", "c++", "c", "c++", "c#", "python", "ruby", "python",
  5. "c", "c", "c++", "java", "c++", "rust", "python"};
  6. HashTable<string, int> ht(1);
  7. for (string word : words) {
  8. if (ht.contains(word)) {
  9. ht.set(word, ht.get(word) + 1);
  10. } else {
  11. ht.add(word, 1);
  12. }
  13. }
  14. cout<<ht;
  15. cout<<"size="<<ht.getSize()<<",maxCapacity="<<ht.maxCapacity()<<",minCapacity="<<ht.minCapacity()<<endl;
  16. string w="c++";
  17. ht.remove(w);
  18. if (ht.contains(w))
  19. cout << "" << w << ": " << ht.get(w) << endl;
  20. else
  21. cout << "No word " << w << " in words" << endl;
  22. cout<<ht;
  23. ht.remove("c#");
  24. ht.remove("java");
  25. ht.remove("c");
  26. cout<<"size="<<ht.getSize()<<",maxCapacity="<<ht.maxCapacity()<<",minCapacity="<<ht.minCapacity()<<endl;
  27. cout<<ht;
  28. return 0;
  29. }

输出结果:

  1. resize 2
  2. resize 4
  3. {c#:1,java:2,ruby:1,c:3,rust:1,python:3,c++:4}
  4. size=7,maxCapacity=12,minCapacity=4
  5. No word c++ in words
  6. {c#:1,java:2,ruby:1,c:3,rust:1,python:3}
  7. resize 2
  8. size=3,maxCapacity=6,minCapacity=2
  9. {python:3,ruby:1,rust:1}

至此,完成了一个简单的哈希表。

1.优化哈希表

在gcc2.9版本中,底层的哈希表是以素数作为容量动态修改的,因此这里的优化从这里出发:

类内部开头添加下面数组:

  1. // 素数数组
  2. const vector<int> capacity = {53, 97, 193, 389, 769, 1543, 3079, 6151, 12289, 24593, 49157, 98317,
  3. 196613, 393241, 786433, 1572869, 3145739, 6291469, 12582917, 25165843,
  4. 50331653, 100663319, 201326611, 402653189, 805306457, 1610612741};

去掉带参数的构造函数,修改默认构造为:

  1. /**
  2. * 默认构造
  3. * @param M
  4. */
  5. HashTable() {
  6. M = capacity[capacityIndex], size = 0;
  7. // 这里的括号是为了初始化为0,这就可以不用写下面的代码,当然在后面add之类的操作,就不需要动态分配内存.
  8. // this->hashtable = new map<Key, Value> *[M]();
  9. this->hashtable = new map<Key, Value> *[M];
  10. for (int i = 0; i < M; i++) {
  11. this->hashtable[i] = new map<Key, Value>;
  12. }
  13. }

修改add函数:在size++后添加下面代码:

  1. if (size >= maxCapacity() && capacityIndex + 1 < capacity.size()) {
  2. capacityIndex++;
  3. resize(capacity[M]);
  4. }

每次resize从capacity中取值。

remove函数修改

在size—后修改:

  1. if (size < minCapacity() && capacityIndex - 1 >= 0) {
  2. capacityIndex--;
  3. resize(capacityIndex);
  4. }

至此,哈希表完成!测试代码同上。