优秀的个人网站案例分析,这么做3d展示网站,win7创建wordpress,wordpress 读书模板前言#xff1a; 前面我们一同学习了二叉搜索树#xff0c;以及特殊版本的平衡二叉搜索树#xff0c;这些容器让我们查找数据的效率提高到了O(log^2 N)。虽然效率提高了很多#xff0c;但是有没有一种理想的方法使得我们能提高到O(1)呢#xff1f;其实在C语言数据结构中 前面我们一同学习了二叉搜索树以及特殊版本的平衡二叉搜索树这些容器让我们查找数据的效率提高到了O(log^2 N)。虽然效率提高了很多但是有没有一种理想的方法使得我们能提高到O(1)呢其实在C语言数据结构中我们接触过哈希表他可以使效率提高到O(1)。 哈希表作为STL中我们所必须学习和了解的容器是一种一一映射的存储方式其次它在日常生活中的应用范围也是很广的例如位图海量数据筛选中用到的布隆过滤器等等…… 下面我们就来先学习一下STL中的应用哈希表的两个容器再了解一下底层结构 两个关联式容器unordered_map和unordered_setunordered系列的关联式容器之所以效率比较高是因为其底层使用了哈希结构最后再来模拟实现一下。
目录
一STL中底层应用哈希的两个容器
1、unordered_set
2、unordered_map
二常见的查找性能对比
三哈希表的概念及模拟实现
1、哈希的概念
2、哈希函数
3、哈希冲突
4、闭散列——开放定址法
5、开散列——链地址法(开链法)哈希桶
四详细代码 一STL中底层应用哈希的两个容器
在STL中对应的容器分别是unordered_map和unordered_set这两个关联式容器。、
我们会用map和set其实就会用unordered_map和unordered_set这两个容器但是这两类容器是有区别的
我们一一分析
1、unordered_set
文档链接-unordered_set文档 我们使用一下unordered_set的接口函数 void test_unordered_set1()
{unordered_setint s1;s1.insert(1);s1.insert(2);s1.insert(9);s1.insert(2);s1.insert(3);s1.insert(3);s1.insert(4);s1.insert(5);unordered_setint::iterator it s1.begin();while (it ! s1.end()){cout *it ;it;}cout endl;for (auto e : s1){cout e ;}
} 结果是实现了存储去重但是是无序的。
由上图和查阅资料得知
map和set 去重 排序unordered_map和unordered_set 只有去重
这里主要原因是底层实现不同map和set底层是红黑树unordered_set和unordered_map底层是红黑树。
其余函数接口和之前所学的容器使用起来大致相同不再一一赘述。
unordered_map和unordered_set都是单向迭代器 值得注意的是unordered_map和unordered_set的迭代器都是单向迭代器而我们之前学的map和set则是双向迭代器所以迭代器可以也可以--。 unordered_set和set的性能对比 int main()
{const size_t N 100000;unordered_setint us;setint s;vectorint v;v.reserve(N);srand(time(0));for (size_t i 0; i N; i){v.push_back(rand());//v.push_back(rand()i);//v.push_back(i);}size_t begin1 clock();for (auto e : v){s.insert(e);}size_t end1 clock();cout set insert: end1 - begin1 endl;size_t begin2 clock();for (auto e : v){us.insert(e);}size_t end2 clock();cout unordered_set insert: end2 - begin2 endl;size_t begin3 clock();for (auto e : v){s.find(e);}size_t end3 clock();cout set find: end3 - begin3 endl;size_t begin4 clock();for (auto e : v){us.find(e);}size_t end4 clock();cout unordered_set find: end4 - begin4 endl endl;size_t begin5 clock();for (auto e : v){s.erase(e);}size_t end5 clock();cout set erase: end5 - begin5 endl;size_t begin6 clock();for (auto e : v){us.erase(e);}size_t end6 clock();cout unordered_set erase: end6 - begin6 endl endl;return 0;
}
数据随机但有重复 数据随机但重复少 数据连续无重复 总结
总的来说unordered_map和unordered_set要比map和set的性能要好的但是也并不是一定的当数据量很大的时候扩容重新哈希是有消耗的。 2、unordered_map
文档链接-unordered_map文档 我们使用unordered1_map的接口函数
void test_unordered_map()
{string arr[] { 梨,梨,苹果,梨,西瓜,西瓜 };unordered_mapstring, int m;for (auto e : arr){m[e];}for (auto kv : m){cout kv.first : kv.second endl;}
}
int main()
{test_unordered_map();return 0;
}总体来说unordered_map和map的用法差不多但是他们的效率有所不同。
二常见的查找性能对比
暴力查找 时间复杂度〇(N)二分查找 时间复杂度〇(logN) 缺点 — 有序、数组结构搜索二叉树 时间复杂度〇(N)缺点 — 极端场景退化成单支平衡二叉搜索树 时间复杂度〇(logN)AVLTree: 左右子树高度差不超过1红黑树最长路径不超过最短路径的2倍哈希B树系列 多叉平衡搜索树 — 数据库原理跳表
ps 红黑树高度略高一些但是跟AVL树是同一数量级对于现代计算机没有差别但是红黑树相对而言近似平衡旋转少。 三哈希表的概念及模拟实现
1、哈希的概念
我们已学过的查找 顺序结构以及平衡树中元素关键码与其存储位置之间没有对应的关系因此在查找一个元素 时必须要经过关键码的多次比较。顺序查找时间复杂度为O(N)平衡树中为树的高度即 O(log_2 N)搜索的效率取决于搜索过程中元素的比较次数。 理想的查找方法 可以不经过任何比较一次直接从表中得到要搜索的元素。 如果构造一种存储结构通过某种函数(hashFunc)使元素的存储位置与它的关键码之间能够建立 一一映射的关系那么在查找时通过该函数可以很快找到该元素。 该中存储结构可以实现 插入元素时 根据待插入元素的关键码以此函数计算出该元素的存储位置并按此位置进行存放。 查找元素时 对元素的关键码进行同样的计算把求得的函数值当做元素的存储位置在结构中按此位置取元素比较若关键码相等则搜索成功。 该方式即为哈希(散列)方法哈希方法中使用的转换函数称为哈希(散列)函数构造出来的结构称 为哈希表(Hash Table)(或者称散列表 2、哈希函数 我们如何一一将键值转换为对应的关键码值并映射到对应序号的存储位置呢 直接建立映射关系问题: 1、数据范围分布很广、不集中可能存在空间浪费严重的问题2、key的数据不是整数是字符串怎么办是自定义类型对象怎么办 此时我们就需要一个函数对特殊非整数类型的数据进行处理使其返回一个特定的整数这个函数我们叫做 —— 哈希函数。 常见的哈希函数 1、直接定址法常用 取关键字的某个线性函数为散列地址HashKey A*Key B 优点简单、均匀 缺点需要事先知道关键字的分布情况 使用场景适合查找比较小且连续的情况 2、除留余数法常用 设散列表中允许的地址数为m取一个不大于m但最接近或者等于m的质数p作为除数 按照哈希函数Hash(key) key% p(pm),将关键码转换成哈希地址 3、其余常见但不常用的还有 平方取中法、折叠法、随机数法、数学分析法等。
字符串也有自己类型的哈希函数-----参考文献了解即可 3、哈希冲突 不同关键字通过相同哈希哈数计算出相同的哈希地址该种现象称为哈希冲突或哈希碰撞。 按照上述哈希函数计算出键值对应的关键码值但是算出来的这些码值当中有很大的可能会出现关键码值相同的情况这种情况就叫作哈希冲突。 哈希函数设计的越精妙产生哈希冲突的可能性就越低但是无法避免哈希冲突。 解决哈希冲突两种常见的方法是闭散列和开散列 4、闭散列——开放定址法 闭散列也叫开放定址法当发生哈希冲突时如果哈希表未被装满说明在哈希表中必然还有 空位置那么可以把key存放到冲突位置中的“下一个” 空位置中去。那如何寻找下一个空位置呢 这就用到了我们的—— 线性探测依次去找空位置 如下图中的场景现在需要插入元素44先通过哈希函数计算哈希地址hashAddr为4 因此44理论上应该插在该位置但是该位置已经放了值为4的元素即发生哈希冲突。 线性探测
从发生冲突的位置开始依次向后探测直到寻找到下一个空位置为止。
插入
通过哈希函数获取待插入元素在哈希表中的位置线性探测找到空位置将值插入
查找
挨个遍历哈希表直到找到空为止
删除
过关键码值再用线性探测找到该值直接删除注意删除要是直接删除的话有可能会影响查找的准确性如图删除了4要去找44就会找不到。
那怎么办呢
所以我们给每个键值提供一个状态采取伪删除的方法
即通过对每一个数据加上一个标识状态即可 线性探测的缺点
一旦发生哈希冲突所有的冲突连在一起容易产生数据“堆积”即不同关键码占据了可利用的空位置使得寻找某关键码的位置需要许多次比较导致搜索效率降低。
所以我们引入了二次探测跳跃着找空位置是相对上面方法的优化使得数据可能不那么拥堵。 所以经过上面的介绍我们想自己实现一个闭散列需要注意以下的几点
哈希函数的选择哈希冲突的应对如何应对“堆积”导致效率低下的情况如何扩容表
第一点和第二点我们已经在上文介绍过了这里我们应用的就是开放定址法和线性探测。 第三点如何应对“堆积”导致效率低下的情况
查询资料 首先根据上文我们知道闭散列哈希表并不能太满
太满就会导致线性探测时找不到位置更不能放满那样探测就会陷入死循环所以要控制一下存储的数据我们引入了一个变量n来记录存储数据的个数
散列表的载荷因子定义为: a 填入表中的元素个数 / 散列表的长度
所以我们要控制一下负载因子 第四点负载因子超了如何扩容
有人会说直接resize扩容就行了啊。但是你没注意到一个问题 我在刚刚那个表中又插入了13这时按理说应该扩容了防止效率变低。
假如我扩容到20格我想找13的时候根据哈希函数13不应该在编号是13的格子中吗但是我是存储在3中啊这就矛盾了...
所以扩容时我们不能直接将原来的数据拷贝过去
因为哈希是映射的关系关键码值是通过数据和表的大小计算出来的如果直接拷贝的话全都乱套了这时我们需要重新映射 如图所示也不是特别麻烦 直接建立一个新表然后遍历旧表一次映射到新表中 不过扩容时会有不少的消耗
补充
映射的时候取模应该是对表的size()取模而不是capacity()因为对capacity取模的话可能取到超出size的位置operator[]会对超出size的检查不过有的也不检查根据不同版本的库里定 5、开散列——链地址法(开链法)哈希桶 开散列法又叫链地址法(开链法)首先对关键码集合用散列函数计算散列地址具有相同地 址的关键码归于同一子集合每一个子集合称为一个桶各个桶中的元素通过一个单链表链 接起来各链表的头结点存储在哈希表中。 从上图可以看出开散列中每个桶中放的都是发生哈希冲突的元素。
很显然哈希桶中每个元素是个地址所以哈希桶的底层原理就是一个指针数组每个结点再挂着一个单链表这样冲突就很容易解决了。
还是老问题
如何应对“堆积”导致效率低下的情况如何扩容
如何应对“堆积”导致效率低下的情况
这里我们还是选择适时扩容那什么情况扩容呢 桶的个数是一定的随着元素的不断插入每个桶中元素的个数不断增多极端情况下可 能会导致一个桶中链表节点非常多会影响的哈希表的性能因此在一定条件下需要对哈希 表进行增容那该条件怎么确认呢开散列最好的情况是每个哈希桶中刚好挂一个节点 再继续插入元素时每一次都会发生哈希冲突因此在元素个数刚好等于桶的个数时可 以给哈希表增容。 如何扩容 方案一 方案二 很显然我们更倾向于方案二 方案一写法更简单但是不断递归开销更大。 注哈希桶结点插入我们一般采用头插的方法因为对于每一个链表如果尾插需要先找到尾增加了时间消耗头插的话消耗更低。 因为开散列是一个指针数组涉及到空间的开辟所以析构函数我们要自己完善 ~HashTable(){for (auto cur : _tables){while (cur){Node* next cur-_next;delete cur;cur next;}cur nullptr;}}四详细代码
namespace OpenAddress
{enum State{EMPTY,EXIST,DELETE};templateclass K, class Vstruct HashData{pairK, V _kv;State _state EMPTY;};templateclass K, class Vclass HashTable{public:HashDataK, V* Find(const K key){if (_tables.size() 0){return nullptr;}size_t hashi key % _tables.size();//线性探测size_t i 1;size_t index hashi;while (_tables[index]._state ! EMPTY){if (_tables[index]._state EXIST _tables[index]._kv.first key){return _tables[index];}index hashi i;index % _tables.size();i;if (index hashi){break;}}return nullptr;}bool Erase(const K key){HashDataK, V* ret Find(key);if (ret){ret-_state DELETE;--_n;return true;}else{return false;}}bool Insert(const pairK, V kv){if (Find(kv.first)){return false;}if (_tables.size() 0 || _n * 10 / _tables.size() 7){size_t newsize _tables.size() 0 ? 10 : _tables.size() * 2;HashTableK, V newht;newht._tables.resize(newsize);for (auto data : _tables){if (data._state EXIST){newht.Insert(data._kv);}}_tables.swap(newht._tables);}size_t hashi kv.first % _tables.size();//线性探测size_t i 1;size_t index hashi;while (_tables[index]._state EXIST){index hashi i;index % _tables.size();i;}_tables[index]._kv kv;_tables[index]._state EXIST;_n;return true;}private:vectorHashDataK, V _tables;size_t _n 0;//存储的数据个数};void TestHashTable2(){HashTableint, int ht;int arr[] { 1,2,2,3,3,3,4,4,4,4,5,9,2,3 };for (auto e : arr){ht.Insert(make_pair(e, e));}}void TestHashTable1(){int a[] { 3, 33, 2, 13, 5, 12, 1002 };HashTableint, int ht;for (auto e : a){ht.Insert(make_pair(e, e));}ht.Insert(make_pair(15, 15));if (ht.Find(13)){cout 13在 endl;}else{cout 13不在 endl;}ht.Erase(13);if (ht.Find(13)){cout 13在 endl;}else{cout 13不在 endl;}}
}namespace HashBacket
{templateclass K,class Vstruct HashNode{HashNodeK, V* _next;pairK, V _kv;HashNode(const pairK, V kv):_next(nullptr), _kv(kv){}};templateclass K,class Vclass HashTable{typedef HashNodeK, V Node;public:~HashTable(){for (auto cur : _tables){while (cur){Node* next cur-_next;delete cur;cur next;}cur nullptr;}}Node* Find(const K key){if (_tables.size() 0){return nullptr;}size_t hashi key % _tables.size();Node* cur _tables[hashi];while (cur){if (cur-_kv.first key){return cur;}cur cur-_next;}return nullptr;}bool Erase(const K key){size_t hashi key % _tables.size();Node* prev nullptr;Node* cur _tables[hashi];while (cur){if (cur-_kv.first key){if(prevnullptr){ _tables[hashi] cur-_next;}else{prev-_next cur-_next;}delete cur;return true;}else{prev cur;cur cur-_next;}}return false;}bool Insert(const pairK, V kv){if (Find(kv.first)){return false;}if (_n _tables.size()){size_t newsize _tables.size() 0 ? 10 : _tables.size() * 2;vectorNode* newtables(newsize, nullptr);for (auto cur : _tables){while (cur){Node* next cur-_next;size_t hashi cur-_kv.first % newtables.size();//头插到新表cur-_next newtables[hashi];newtables[hashi] cur;cur next;}}_tables.swap(newtables);}size_t hashi kv.first % _tables.size();// 头插Node* newnode new Node(kv);newnode-_next _tables[hashi];_tables[hashi] newnode;_n;return true;}private:vectorNode* _tables;size_t _n 0;};void TestHashTable1(){int a[] { 3, 33, 2, 13, 5, 12, 1002 };HashTableint, int ht;for (auto e : a){ht.Insert(make_pair(e, e));}ht.Insert(make_pair(15, 15));ht.Insert(make_pair(25, 25));ht.Insert(make_pair(35, 35));ht.Insert(make_pair(45, 45));}void TestHashTable2(){int a[] { 3, 33, 2, 13, 5, 12, 1002 };HashTableint, int ht;for (auto e : a){ht.Insert(make_pair(e, e));}ht.Erase(12);ht.Erase(3);ht.Erase(33);}
} 感谢你的阅读