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

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

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

    也可以点击原文阅读。

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

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

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

    这里使用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. }

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

    查操作

    获取key对应的value。

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

    最后,上面有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. }
    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. template<typename K, typename V>
    2. return out;
    3. }

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

    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}

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

    在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++后添加下面代码:

    每次resize从capacity中取值。

    remove函数修改

    在size—后修改:

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