哈希
哈希本质上还是一个数组,只是数组的每一个位置要存储的值进行了映射;
哈希也可以叫做散列;
哈希比红黑树快是因为,不需要重复进行比较大小,直接用映射关系进行查找;
哈希函数的设计应该注意要减少哈希冲突;
1.unordered系列关联式容器的使用
Java设计的是HashMap与HashSet,TreeMap与TreeSet。
stl的map与set的底层是红黑树,unorderedmap与unorderedset的底层设计的是哈希表/桶;
与map/set的区别是,1.迭代器使用的都是单向迭代器;2.遍历出来不是有序的,只能去重;3.哈希表的性能比起红黑树更高一些;
注意插入重复数据时哈希更快,非重复时红黑树更快,其他如查找,删除都是哈希更快。
对于有序数据还是红黑树更快,包括插入,查找,删除;
1.1unordered_set
1.2unordered_map
2.不同查找方式的对比
1.暴力查找时间按复杂度O(N);
2.有序数组的二分查找O(lgN),存在排序O(N*logN)和增删改需要挪动数据O(N)的问题。
3.平衡搜索树O(lgN),天然有序,增删查改也比较方便;
4.哈希(散列)存储的值和存储的位置建立一个对应关系,根据对应关系进行快速查找;
3.哈希
1.计数排序使用的就是一种哈希;
2.图书馆找书,根据字母排序进行哈希查找相关书籍;
如果数据很分散,那就退化成了暴力查找;
3.1常见的哈希函数
1.直接定址法,使用直接映射查找,一对一,适合数据集中使用,问题:数据分散就会退化成暴力查找;
2.除留余数法,开辟capacity个位置的空间,int i = k % capacity,然后将key放到第i个位置,问题:会产生哈希冲突/碰撞,即不同的值可能会映射到同一个位置,值和位置的关系是多对一的关系;
如果是整型直接进行映射,如果是对象或者字符串就需要先与整数进行映射,然后在用整数和位置进行映射;
3.2解决哈希冲突
1.闭散列的开放定址法,缺陷是冲突会互相影响,如连续空间的值会互相影响;
思路是:位置被占了,就找下一个位置(本质上找的是别人的位置),会造成踩踏拥堵;
线性探测:从发生冲突的位置开始,依次向后探测,直到寻找到下一个空位置为止,空间不够会进行扩容;
问题是:1.会不断的抢占位置;2.哈希表不能太满,否则会导致下降,所以是不可能找不到空位置的;3.删除会导致查找出错,所以需要存在三个状态标记,1.exist,2.empty,3.delete ;
二次探测,与线性探测唯一的区别就是每次移动走的是平方,1 4 9 …,好处是每次查找的时候不是走连续的空间,若一段连续空间内存在多个值,就会降低哈希冲突的概率;
2.开散列的拉链法(哈希桶):
在原有顺序表的位置使用类似链表的方式,在顺序表外将多个值链接在此位置之后;
4.线性探测实现哈希表
1.使用枚举变量,来控制状态;2.对于哈希数据要存放kv和状态枚举变量;3.对于哈希表使用vector来开辟空间存放哈希数据,使用size记录有效数据;4.设计负载因子,负载因子为表中的元素个数除以长度;
对于插入函数的设计,1.要注意负载因子越大,产生哈希冲突的概率越大,但是空间利用率更高,所以综合一下控制负载因子到一定值就扩容,不能满了才扩容或空间利用率过低;2.扩容先计算新的空间大小,然后重新开辟一个哈希表,并且使用resize使得大小和空间相同,遍历旧表插入新表,然后交换新旧表;3.使用除留余数法来获取顺序表的下标;4…不断线性探测空位置;5.找到了空位置,插入哈希数据,设置kv并修改状态变量;
对于删除函数的设计,较为简单先find,然后置为delete并且size–。
由于映射使用的是下标位置是自然数,所以要注意对于非无符号整型要先建立无符号整型映射,推荐使用仿函数;对于常见的类型可以使用模板的特化;如果只是使用字符转整型相加还是右很多场景得到的值相同,难以避免哈希冲突,所以需要使用哈希算法BKDR算法,即每次加之前乘131,使用131是因冲突数相较于其他算法较少;
代码实现:
#pragma once#include <vector>namespace HashTable
{enum Status{EXIST = 0,EMPTY,DELETE,};template <class K, class V>struct HashData{std::pair<K, V> kv_;Status status_ = EMPTY;};template <class K>struct Default{size_t operator()(const K &key){return (size_t)key;}};template <>struct Default<std::string>{size_t operator()(const std::string &key){size_t i = 0;for (const auto &ch : key){i *= 131;i += ch;}return i;}};// struct stringfunc// {// size_t operator()(const std::string& key)// {// return key[0];// }// };template <class K, class V, class HashFunc = Default<K>>class HashTable{public:HashTable(){table_.resize(10); // 使用resize保证size和capacity的值是一样的}bool insert(const std::pair<K, V> &kv) // 要注意不可以使得这个表太满,否则会降低查找效率,甚至表满了会不断死循环下去{if (find(kv.first)){return false;}HashFunc func;// 扩容逻辑,要注意的是扩容完成之后映射关系会发生改变,因为扩容后除数变大了if ((double)size_ / (double)table_.size() >= 0.7){size_t newsize = table_.size() * 2;HashTable<K, V, HashFunc> hashtable;hashtable.table_.resize(newsize);// 遍历旧表重新插入到新表for (const auto &e : table_){if (e.status_ == EXIST){hashtable.insert(e.kv_);}}// 交换新旧表table_.swap(hashtable.table_);}// 线性探测size_t hashi = func(kv.first) % table_.size(); // 不能%capacity因为断言检查的是size,会有越界风险,最好是使得size与capacity相等// 如果这个位置存在值,就需要探测到空停止if (func(table_[hashi].kv_.first) != func(kv.first)){while (table_[hashi].status_ == EXIST){hashi++;hashi %= table_.size();}// 找到了空位置进行插入table_[hashi].kv_ = kv;table_[hashi].status_ = EXIST;size_++;return true;}else{return false;}}HashData<const K, V> *find(const K &key){Default<K> func;size_t hashi = func(key) % table_.size();while (table_[hashi].status_ != EMPTY) // 由于delete状态可能是数据再后面位置,不能停止{if (table_[hashi].status_ != DELETE && table_[hashi].kv_.first == key){return (HashData<const K, V> *)&table_[hashi]; // 会产生隐式类型转化,有的编译器不支持需要显式转换}hashi++;hashi %= table_.size();}return nullptr;}bool erase(const K &key){HashData<const K, V> *pos = find(key);if (pos){pos->status_ = DELETE;size_--;return true;}return false;}private:std::vector<HashData<K, V>> table_; // 物理空间上就是一个是顺序表,逻辑上是一个哈希size_t size_ = 0; // 存放有效数据的个数};
}