• C++:哈希,unordered_map和unordered_set


    目录

    一.unordered_map和unordered_set

    1.时间复杂度:它们查找的时间复杂度平均都是O(1)

    2.它们的底层结构相同,都使用哈希桶

    简单的使用代码:

    二.哈希

    1. 直接定址法--(数分布集中 常用)

    2. 除留余数法--(数分布不集中,均匀 常用)

    3.哈希冲突

    (2)闭散列

    线性探测:

    添加状态

    二次探测

    4.负载因子

    5.哈希冲突的处理方法和哈希函数 区分

    阶段一:只有插入的哈希

    阶段二:完善哈希表,雏形哈希桶

    1.当key是string或其他类型,如何映射?

    2.仿函数中的特化

    3.开散列(哈希桶)

    阶段三:完善哈希桶

    析构函数

    优化insert

    总代码:

    阶段四:模拟实现unorderedmap/set

    (1)template 第二个模板T

    (2)class KeyOfT模板参数:

    (3)HashFunc模板 要放到 UnorderedSet.h / UnorderedMap.h 中

    UnorderedSet.h

    UnorderedMap.h

    HashTable.h

    Test.cpp


    一.unordered_map和unordered_set

    unordered_map和unordered_set和map/set用法一样,不同就是unordered_map和unordered_set不排序,map/set自动排序

    1.时间复杂度:它们查找的时间复杂度平均都是O(1)

    哈希是通过哈希函数来计算元素的存储位置的,找的时候同样通过哈希函数找元素位 置,不需要循环遍历因此时间复杂度为O(1)

    2.它们的底层结构相同,都使用哈希桶

    3.它们在进行元素插入时,不需要比较key找待插入元素的位置,只需要通过哈希函数,就可以确认元素需要存储的位置。

    简单的使用代码:

    1. #include
    2. #include
    3. #include
    4. #include
    5. #include
    6. #include
    7. using namespace std;
    8. void test_set()
    9. {
    10. unordered_set<int> s;
    11. //set s;
    12. s.insert(2);
    13. s.insert(3);
    14. s.insert(1);
    15. s.insert(2);
    16. s.insert(5);
    17. //unordered_set::iterator it = s.begin();
    18. auto it = s.begin();
    19. while (it != s.end())
    20. {
    21. cout << *it << " ";
    22. ++it;
    23. }
    24. cout << endl;
    25. for (auto e : s)
    26. {
    27. cout << e << " ";
    28. }
    29. cout << endl;
    30. }
    31. void test_op() //用于记录插入1000w个数两个容器花费时间的函数
    32. {
    33. int n = 10000000;
    34. vector<int> v;
    35. v.reserve(n);
    36. srand(time(0));
    37. for (int i = 0; i < n; ++i)
    38. {
    39. //v.push_back(i);
    40. //v.push_back(rand()+i); // 重复少
    41. v.push_back(rand()); // 重复多
    42. }
    43. size_t begin1 = clock();
    44. set<int> s;
    45. for (auto e : v)
    46. {
    47. s.insert(e);
    48. }
    49. size_t end1 = clock();
    50. size_t begin2 = clock();
    51. unordered_set<int> us;
    52. for (auto e : v)
    53. {
    54. us.insert(e);
    55. }
    56. size_t end2 = clock();
    57. cout << s.size() << endl;
    58. cout << "set insert:" << end1 - begin1 << endl;
    59. cout << "unordered_set insert:" << end2 - begin2 << endl;
    60. size_t begin3 = clock();
    61. for (auto e : v)
    62. {
    63. s.find(e);
    64. }
    65. size_t end3 = clock();
    66. size_t begin4 = clock();
    67. for (auto e : v)
    68. {
    69. us.find(e);
    70. }
    71. size_t end4 = clock();
    72. cout << "set find:" << end3 - begin3 << endl;
    73. cout << "unordered_set find:" << end4 - begin4 << endl;
    74. size_t begin5 = clock();
    75. for (auto e : v)
    76. {
    77. s.erase(e);
    78. }
    79. size_t end5 = clock();
    80. size_t begin6 = clock();
    81. for (auto e : v)
    82. {
    83. us.erase(e);
    84. }
    85. size_t end6 = clock();
    86. cout << "set erase:" << end5 - begin5 << endl;
    87. cout << "unordered_set erase:" << end6 - begin6 << endl;
    88. }
    89. void test_map()
    90. {
    91. unordered_map dict;
    92. dict.insert(make_pair("sort", "排序"));
    93. dict.insert(make_pair("left", "左边"));
    94. dict.insert(make_pair("left", "剩余"));
    95. dict["string"];
    96. dict["left"] = "剩余";
    97. dict["string"] = "字符串";
    98. }
    99. int main()
    100. {
    101. //test_set();
    102. //test_op();
    103. test_map();
    104. return 0;
    105. }

    二.哈希

    哈希(散列)-映射:存储关键字跟存储位置建立关联关系

    值和存储位置——> 映射关系 ——> 查找按映射关系去找

    哈希是一种用来进行高效查找的数据结构,查找的时间复杂度平均为O(1)

    采用哈希方式解决问题时,必须使用哈希函数

    哈希查找的时间复杂度不一定是O(1)(因为存在哈希冲突,一般基本都是O(1))

    哈希是以牺牲空间为代价,提高查询的效率(采用哈希处理时,一般所需空间都会比元素个数多,否则产生冲突的概率就比较大,影 响哈希的性能)

    直接建立映射关系问题:
    1、数据范围分布很广、不集中(除留余数法解决
    2、key的数据不是整数,是字符串怎么办?是自定义类型对象怎么办?

    1. 直接定址法--(数分布集中 常用)

    取关键字的某个线性函数为散列地址:Hash(Key)= A*Key + B
    优点:简单、均匀
    缺点:需要事先知道关键字的分布情况
    使用场景:适合查找比较小且集中连续的情况

    2. 除留余数法--(数分布不集中,均匀 常用)

    key %表大小——>映射的位置
                             下面都假设这个表大小是10

     除留余数法会有个问题,就是哈希冲突

    3.哈希冲突

    不同关键字通过相同哈希哈数计算出相同的哈希地址,该种现象称为哈希冲突 或哈希碰撞

    例如:除留余数法中,20%表大小10=0,把20放在下标0处,如果有30,50呢,30,50%10也得放在下标0处,所以要解决哈希冲突,解决哈希冲突两种常见的方法是:闭散列开散列。

    (2)闭散列

    闭散列:也叫开放定址法,当发生哈希冲突时,如果哈希表未被装满,说明在哈希表中必然还有
    空位置,那么可以把key存放到冲突位置中的“下一个” 空位置中去。那如何寻找下一个空位置
    呢?闭散列分为线性探测二次探测

    线性探测:

    从发生冲突的位置开始,依次向后探测,直到寻找到下一个空位置为止。

    hash(key)%N + i(i= 0,1,2,3...):比如10%N(N是表大小10)=0,因为下标0已经放了20,则再加i=1,10%10+1=1,就把10放到了下标是1的地方。30:放30就是30%10+2=2,把30放到了下标是2的地方
     

     缺点:我占你的,你占他的,拥堵

    添加状态

    比如查找50时,一直找到空就停止,但是如果在50之前有删除的数据,就会在删除数据的位置停下,这样就找不到50了,所以添加个状态用于区分空和删除的位置。遇到空EMPTY就停,遇到删除DELETE和存在 EXITS就不停

    1. enum State
    2. {
    3. EMPTY,
    4. EXITS,
    5. DELETE
    6. };

    二次探测

    线性探测的缺陷是产生冲突的数据堆积在一块,这与其找下一个空位置有关系,因为找空位
    置的方式就是挨着往后逐个去找,因此二次探测为了避免该问题,改成每次跳

    hash(key)%N +  (i= 0,1,2,3...)

    比如10%N(N是表大小10)=0,因为下标0已经放了20,则再加 =1,10%10+1=1,就把10放到了下标是1的地方。30:放30就是30%10+2²=4,把30放到了下标是4的地方

    对上面方法的优化:不那么拥堵

    starti %= _tables.size();还是.capacity() ?

    _tables.size()是能存数据的位置个数,_tables.capacity()是总容量大小(包括没有初始化的空间)类似resize和reserve的区别,starti %= _tables.size(); 插入数据只能插在能插入的位置,因为_tables[hashi] 的operator[] 会自动检查这个位置是否有值还是没有值的随机数,有值就可以插入。

    4.负载因子

    负载因子(散列表的载荷因子α) = 填入表中的元素个数 / 哈希表的长度

    由于表长是定值,α与“填入表中的元素个数”成正比,所以,α越大,表明填入表中的元素越多,产生冲突的可能性就越大:反之,α越小,标明填入表中的元素越少,产生冲突的可能性就越小。

    哈希表什么情况下进行扩容?如何扩容?
    负载因子大于0.7就扩容。

    5.哈希冲突的处理方法和哈希函数 区分

     哈希函数作用是:建立元素与其存储位置之前的对应关系的,在存储元素时,先通过哈希函数计算元素在哈希表格中的存储位置,然后存储元素。好的哈希函数可以减少冲突的概率,但是不能绝对避免哈希函数,万一发生哈希冲突,得需要借助哈希冲突处理方法来 解决。

       常见的哈希函数有:直接定址法、除留余数法、平方取中法、随机数法、数字分析法、叠加法等

       常见哈希冲突处理:闭散列(线性探测、二次探测)、开散列(链地址法)、多次散列

    常见哈希函数
    1. 直接定址法--(常用)
    取关键字的某个线性函数为散列地址:Hash(Key)= A*Key + B
    优点:简单、均匀
    缺点:需要事先知道关键字的分布情况
    使用场景:适合查找比较小且连续的情况
    2. 除留余数法--(常用)
    设散列表中允许的地址数为m,取一个不大于m,但最接近或者等于m的质数p作为除数,
    按照哈希函数:Hash(key) = key% p(p<=m),将关键码转换成哈希地址
    3. 平方取中法--(了解)
    假设关键字为1234,对它平方就是1522756,抽取中间的3位227作为哈希地址;
    再比如关键字为4321,对它平方就是18671041,抽取中间的3位671(或710)作为哈希地址
    平方取中法比较适合:不知道关键字的分布,而位数又不是很大的情况
    4. 折叠法--(了解)
    折叠法是将关键字从左到右分割成位数相等的几部分(最后一部分位数可以短些),然后将这
    几部分叠加求和,并按散列表表长,取后几位作为散列地址。
    折叠法适合事先不需要知道关键字的分布,适合关键字位数比较多的情况
    5. 随机数法--(了解)
    选择一个随机函数,取关键字的随机函数值为它的哈希地址,即H(key) = random(key),其中
    random为随机数函数。
    通常应用于关键字长度不等时采用此法
    6. 数学分析法--(了解)
    设有n个d位数,每一位可能有r种不同的符号,这r种不同的符号在各位上出现的频率不一定
    相同,可能在某些位上分布比较均匀,每种符号出现的机会均等,在某些位上分布不均匀只
    有某几种符号经常出现。可根据散列表的大小,选择其中各种符号分布均匀的若干位作为散
    列地址。

    阶段一:只有插入的哈希

    1. #pragma once
    2. #include
    3. enum State
    4. {
    5. EMPTY,
    6. EXITS,
    7. DELETE
    8. };
    9. // 休息21:16继续
    10. template<class K, class V>
    11. struct HashData
    12. {
    13. pair _kv;
    14. State _state;
    15. };
    16. template<class K, class V>
    17. class HashTable
    18. {
    19. public:
    20. bool Insert(const pair& kv)
    21. {
    22. // 负载因子到0.7及以上,就扩容。这里算 负载因子>0.7,因为是小数,前面可能需要类型转换,否则除出来是0,我们不如把 负载因子*10>7
    23. if (_tables.size() == 0 || _n * 10 / _tables.size() >= 7)
    24. {
    25. size_t newSize = _tables.size() == 0 ? 10 : _tables.size() * 2;
    26. // 扩容以后,需要重新映射
    27. HashTable newHT;
    28. newHT._tables.resize(newSize);
    29. // 遍历旧表,插入newHT
    30. for (auto& e : _tables)
    31. {
    32. if (e._state == EXITS)
    33. {
    34. newHT.Insert(e._kv);
    35. }
    36. }
    37. newHT._tables.swap(_tables);
    38. }
    39. size_t starti = kv.first;
    40. starti %= _tables.size();
    41. size_t hashi = starti;
    42. size_t i = 1;
    43. // 线性探测/二次探测
    44. while (_tables[hashi]._state == EXITS)
    45. {
    46. hashi = starti + i;
    47. ++i;
    48. hashi %= _tables.size();
    49. }
    50. _tables[hashi]._kv = kv;
    51. _tables[hashi]._state = EXITS;
    52. _n++;
    53. }
    54. HashData* Find(const K& key);
    55. bool Erase(const K& key);
    56. private:
    57. vector _tables;
    58. size_t _n = 0; // 存储关键字个数
    59. };

    阶段二:完善哈希表,雏形哈希桶

    1.当key是string或其他类型,如何映射?

    key是string或者自定义类型时,需要转成整数再映射进哈希表,这个转成整数的方法有多样,比如:string类型转成整数方法是BKDR法,hash乘一个值131再加字符串的ASCII值,这样能减少冲突。如果仅仅是通过整个字符串的ASCII值来来映射,比如"abc"和"bac"就一样,一样就是哈希冲突,哈希冲突也没关系,我们有处理哈希冲突的机制:把映射到同一位置的值像下一个位置放(线性探测)。类似于先放20后再放10,映射在大小是10的哈希表中,都放在0处,哈希冲突了,就可以把10放在20后面的空位置上。

    1. template<>
    2. struct DefaultHash
    3. {
    4. size_t operator()(const string& key)
    5. { //key是string类型用仿函数DefaultHash转成整数
    6. //转换方法:BKDR ,数学研究出来的
    7. size_t hash = 0;
    8. for (auto ch : key)
    9. {
    10. hash = hash * 131 + ch; //hash乘一个值131再加字符串的ASCII
    11. }
    12. return hash;
    13. }
    14. };

    2.仿函数中的特化

    这样模拟实现unorodered_map时,直接定义 unorodered_map a; 时不用传仿函数是因为template> 仿函数给了缺省参数,不传默认是基础的类struct DefaultHash,直接定义 unorodered_map b; 时也可以不用传仿函数是因为类模板特化,key是string就会使用特化版本的struct DefaultHash

    1. template<class K>
    2. struct DefaultHash
    3. {
    4. size_t operator()(const K& key)
    5. {
    6. return (size_t)key;
    7. }
    8. };
    9. template<>
    10. struct DefaultHash
    11. {
    12. size_t operator()(const string& key)
    13. { //key是string类型用仿函数DefaultHash转成整数
    14. //转换方法:BKDR ,数学研究出来的
    15. size_t hash = 0;
    16. for (auto ch : key)
    17. {
    18. hash = hash * 131 + ch; //hash乘一个值131再加字符串的ASCII
    19. }
    20. return hash;
    21. }
    22. };
    23. template<class K, class V, class HashFunc = DefaultHash>
    24. class HashTable
    25. {
    26. typedef HashData Data;
    27. public:
    28. ……

    3.开散列(哈希桶)

    (1) 哈希桶/开散列/链地址法/开链法 概念
    开散列法又叫 哈希桶/链地址法/开链法,数据不存在表中,表里面存储一个链表指针,冲突的数据链表形式挂起来。 哈希桶用 单链表,不进行插入删除操作就不用双向链表,仅头插就可以,而且单链表比双向链表少存一个指针,能省则省。

    vector 数组每个位置存的是链表第一个值的指针

    1. #pragma once
    2. #include
    3. namespace CloseHash
    4. {
    5. enum State
    6. {
    7. EMPTY,
    8. EXITS,
    9. DELETE
    10. };
    11. template<class K, class V>
    12. struct HashData
    13. {
    14. pair _kv;
    15. State _state = EMPTY;
    16. };
    17. template<class K>
    18. struct DefaultHash
    19. {
    20. size_t operator()(const K& key)
    21. {
    22. return (size_t)key;
    23. }
    24. };
    25. template<>
    26. struct DefaultHash
    27. {
    28. size_t operator()(const string& key)
    29. { //key是string类型用仿函数DefaultHash转成整数
    30. //转换方法:BKDR ,数学研究出来的
    31. size_t hash = 0;
    32. for (auto ch : key)
    33. {
    34. hash = hash * 131 + ch; //hash乘一个值131再加字符串的ASCII
    35. }
    36. return hash;
    37. }
    38. };
    39. //struct StringHash
    40. //{
    41. // // "abcd"
    42. // // "aa"
    43. // size_t operator()(const string& key)
    44. // {
    45. // //return key[0]; 可以
    46. // //return (size_t)&key; 不行
    47. //
    48. // /*size_t hash = 0;
    49. // for (auto ch : key)
    50. // {
    51. // hash += ch;
    52. // }
    53. //
    54. // return hash;*/
    55. //
    56. // // BKDR
    57. // size_t hash = 0;
    58. // for (auto ch : key)
    59. // {
    60. // hash = hash* 131 + ch;
    61. // }
    62. //
    63. // return hash;
    64. // }
    65. //};
    66. template<class K, class V, class HashFunc = DefaultHash>
    67. class HashTable
    68. {
    69. typedef HashData Data;
    70. public:
    71. bool Insert(const pair& kv)
    72. {
    73. if (Find(kv.first)) //去冗余 易错点1,容易忘写
    74. {
    75. return false;
    76. }
    77. // 负载因子到0.7及以上,就扩容
    78. if (_tables.size() == 0 || _n * 10 / _tables.size() >= 7)
    79. {
    80. size_t newSize = _tables.size() == 0 ? 10 : _tables.size() * 2;
    81. // 扩容以后,需要重新映射
    82. HashTable newHT;
    83. newHT._tables.resize(newSize);
    84. // 遍历旧表,插入newHT
    85. for (auto& e : _tables)
    86. {
    87. if (e._state == EXITS) //易错点2,容易忘写
    88. {
    89. newHT.Insert(e._kv);
    90. }
    91. }
    92. newHT._tables.swap(_tables);
    93. }
    94. HashFunc hf;
    95. size_t starti = hf(kv.first);
    96. starti %= _tables.size();
    97. size_t hashi = starti;
    98. size_t i = 1;
    99. // 线性探测/二次探测
    100. while (_tables[hashi]._state == EXITS)
    101. {
    102. hashi = starti + i;
    103. ++i;
    104. hashi %= _tables.size();
    105. }
    106. _tables[hashi]._kv = kv;
    107. _tables[hashi]._state = EXITS;
    108. _n++;
    109. return true;
    110. }
    111. Data* Find(const K& key)
    112. {
    113. if (_tables.size() == 0) //哈希表还没数据时不能查找
    114. {
    115. return nullptr;
    116. }
    117. HashFunc hf;
    118. size_t starti = hf(key); //不同类型的key通过对应的仿函数转成整数-
    119. starti %= _tables.size();//-比如key是string类型,就-
    120. size_t hashi = starti; //-用仿函数DefaultHash转成整形
    121. size_t i = 1;
    122. while (_tables[hashi]._state != EMPTY)
    123. { //下面如果状态是DELETE就无法被查找到
    124. if (_tables[hashi]._state != DELETE && _tables[hashi]._kv.first == key)
    125. {
    126. return &_tables[hashi];
    127. }
    128. hashi = starti + i;
    129. ++i;
    130. hashi %= _tables.size();
    131. }
    132. return nullptr;
    133. }
    134. bool Erase(const K& key)
    135. {
    136. Data* ret = Find(key);
    137. if (ret)
    138. {
    139. ret->_state = DELETE;
    140. --_n;
    141. return true;
    142. }
    143. else
    144. {
    145. return false;
    146. }
    147. }
    148. private:
    149. vector _tables;
    150. size_t _n = 0; // 存储关键字个数
    151. };
    152. void TestHT1()
    153. {
    154. int a[] = { 20, 5, 8, 99999, 10, 30, 50 };
    155. //HashTable> ht;
    156. HashTable<int, int> ht;
    157. if (ht.Find(10))
    158. {
    159. cout << "找到了10" << endl;
    160. }
    161. for (auto e : a)
    162. {
    163. ht.Insert(make_pair(e, e));
    164. }
    165. // 测试扩容
    166. ht.Insert(make_pair(15, 15));
    167. ht.Insert(make_pair(5, 5));
    168. ht.Insert(make_pair(15, 15));
    169. if (ht.Find(50))
    170. {
    171. cout << "找到了50" << endl;
    172. }
    173. if (ht.Find(10))
    174. {
    175. cout << "找到了10" << endl;
    176. }
    177. ht.Erase(10);
    178. ht.Erase(10);
    179. if (ht.Find(50))
    180. {
    181. cout << "找到了50" << endl;
    182. }
    183. if (ht.Find(10))
    184. {
    185. cout << "找到了10" << endl;
    186. }
    187. }
    188. void TestHT2()
    189. {
    190. string arr[] = { "苹果", "西瓜", "苹果", "西瓜", "苹果", "苹果", "西瓜", "苹果", "香蕉", "苹果", "香蕉" };
    191. /*string s1("苹果");
    192. string s2("果苹");
    193. string s3("果果");
    194. string s4("萍果");
    195. string s5("abcd");
    196. string s6("bcad");
    197. string s7("aadd");
    198. StringHash hf;
    199. cout << hf(s1) << endl;
    200. cout << hf(s2) << endl;
    201. cout << hf(s3) << endl;
    202. cout << hf(s4) << endl << endl;
    203. cout << hf(s5) << endl;
    204. cout << hf(s6) << endl;
    205. cout << hf(s7) << endl;*/
    206. //HashTable countHT;
    207. HashTableint> countHT;
    208. for (auto& str : arr)
    209. {
    210. auto ret = countHT.Find(str);
    211. if (ret)
    212. {
    213. ret->_kv.second++;
    214. }
    215. else
    216. {
    217. countHT.Insert(make_pair(str, 1));
    218. }
    219. }
    220. // 对应类型配一个仿函数,仿函数对象实现把key对象转换成映射的整数
    221. //HashTable countHT;
    222. //HashTable countHT;
    223. HashTableint> copy(countHT);
    224. }
    225. }
    226. namespace Bucket
    227. {
    228. template<class K, class V>
    229. struct HashNode
    230. {
    231. pair _kv;
    232. HashNode* _next;
    233. HashNode(const pair& kv)
    234. :_kv(kv)
    235. , _next(nullptr)
    236. {}
    237. };
    238. template<class K, class V>
    239. class HashTable
    240. {
    241. typedef HashNode Node;
    242. public:
    243. bool Insert(const pair& kv)
    244. {
    245. if (Find(kv.first))
    246. {
    247. return false;
    248. }
    249. // 负载因子 == 1 扩容
    250. if (_tables.size() == _n)
    251. {
    252. // 扩容,有缺陷,可以再优化,大家下去可以思考一下
    253. size_t newSize = _tables.size() == 0 ? 10 : _tables.size() * 2;
    254. HashTable newHT;
    255. newHT._tables.resize(newSize, nullptr);
    256. for (size_t i = 0; i < _tables.size(); ++i)
    257. {
    258. Node* cur = _tables[i];
    259. while (cur)
    260. {
    261. newHT.Insert(cur->_kv);
    262. cur = cur->_next;
    263. }
    264. }
    265. newHT._tables.swap(_tables);
    266. }
    267. size_t hashi = kv.first;
    268. hashi %= _tables.size();
    269. // 头插到对应的桶即可
    270. Node* newnode = new Node(kv);
    271. newnode->_next = _tables[hashi];
    272. _tables[hashi] = newnode;
    273. ++_n;
    274. return true;
    275. }
    276. Node* Find(const K& key)
    277. {
    278. if (_tables.size() == 0)
    279. {
    280. return nullptr;
    281. }
    282. size_t hashi = key;
    283. hashi %= _tables.size();
    284. Node* cur = _tables[hashi];
    285. while (cur)
    286. {
    287. if (cur->_kv.first == key)
    288. {
    289. return cur;
    290. }
    291. cur = cur->_next;
    292. }
    293. return nullptr;
    294. }
    295. private:
    296. // 指针数组
    297. vector _tables;
    298. size_t _n = 0;
    299. };
    300. void TestHT1()
    301. {
    302. int a[] = { 20, 5, 8, 99999, 10, 30, 50 };
    303. //HashTable> ht;
    304. HashTable<int, int> ht;
    305. if (ht.Find(10))
    306. {
    307. cout << "找到了10" << endl;
    308. }
    309. for (auto e : a)
    310. {
    311. ht.Insert(make_pair(e, e));
    312. }
    313. // 测试扩容
    314. ht.Insert(make_pair(15, 15));
    315. ht.Insert(make_pair(5, 5));
    316. ht.Insert(make_pair(15, 15));
    317. ht.Insert(make_pair(25, 15));
    318. ht.Insert(make_pair(35, 15));
    319. ht.Insert(make_pair(45, 15));
    320. }
    321. }

    阶段三:完善哈希桶

    新增析构函数,优化版insert,删除Erase,拷贝构造,赋值(自己写),Hashfunc(string也可以%)

    析构函数

    vector是自定义类型会去调用自己的析构函数析构数组,但不会释放每个位置上的链表的节点,我们要一个一个释放每个链表的每个节点

    1. ~HashTable()
    2. {
    3. for (size_t i = 0; i < _tables.size(); ++i)
    4. {
    5. Node* cur = _tables[i];
    6. while (cur)
    7. {
    8. Node* next = cur->_next;
    9. delete cur;
    10. cur = next;
    11. }
    12. _tables[i] = nullptr;
    13. }
    14. }

    优化insert

    开一个新的扩容后大小的数组newTable,把原数组_table的节点一个一个头插到新数组中,这样就避免原方法中的双重消耗:新开节点(插入数组),释放旧数组节点

    1. bool Insert(const T& data)
    2. {
    3. HashFunc hf;
    4. KeyOfT kot;
    5. if (Find(kot(data)))
    6. {
    7. return false;
    8. }
    9. // 负载因子 == 1 扩容
    10. if (_tables.size() == _n)
    11. {
    12. size_t newSize = _tables.size() == 0 ? 10 : _tables.size() * 2;
    13. vector newTable; //开一个新的扩容后大小的数组
    14. newTable.resize(newSize, nullptr); //开一个新的扩容后大小的数组
    15. for (size_t i = 0; i < _tables.size(); ++i)
    16. {
    17. Node* cur = _tables[i];
    18. while (cur)
    19. {
    20. Node* next = cur->_next;
    21. size_t hashi = hf(kot(cur->_data)) % newSize;
    22. cur->_next = newTable[hashi];
    23. newTable[hashi] = cur;
    24. cur = next;
    25. }
    26. _tables[i] = nullptr;
    27. }
    28. newTable.swap(_tables);
    29. }
    30. size_t hashi = hf(kot(data));
    31. hashi %= _tables.size();
    32. // 头插到对应的桶即可
    33. Node* newnode = new Node(data);
    34. newnode->_next = _tables[hashi];
    35. _tables[hashi] = newnode;
    36. ++_n;
    37. return true;
    38. }

    总代码:

    1. #pragma once
    2. #include
    3. template<class K>
    4. struct DefaultHash
    5. {
    6. size_t operator()(const K& key)
    7. {
    8. return (size_t)key;
    9. }
    10. };
    11. template<>
    12. struct DefaultHash
    13. {
    14. size_t operator()(const string& key)
    15. {
    16. // BKDR
    17. size_t hash = 0;
    18. for (auto ch : key)
    19. {
    20. hash = hash * 131 + ch;
    21. }
    22. return hash;
    23. }
    24. };
    25. namespace Bucket
    26. {
    27. template<class K, class V>
    28. struct HashNode
    29. {
    30. pair _kv;
    31. HashNode* _next;
    32. HashNode(const pair& kv)
    33. :_kv(kv)
    34. , _next(nullptr)
    35. {}
    36. };
    37. template<class K, class V, class HashFunc = DefaultHash>
    38. class HashTable
    39. {
    40. typedef HashNode Node;
    41. public:
    42. ~HashTable()
    43. {
    44. for (size_t i = 0; i < _tables.size(); ++i)
    45. {
    46. Node* cur = _tables[i];
    47. while (cur)
    48. {
    49. Node* next = cur->_next;
    50. delete cur;
    51. cur = next;
    52. }
    53. _tables[i] = nullptr;
    54. }
    55. }
    56. bool Insert(const pair& kv)
    57. {
    58. if (Find(kv.first))
    59. {
    60. return false;
    61. }
    62. HashFunc hf;
    63. // 负载因子 == 1 扩容
    64. if (_tables.size() == _n)
    65. {
    66. // 扩容,有缺陷,可以再优化,大家下去可以思考一下
    67. /*size_t newSize = _tables.size() == 0 ? 10 : _tables.size() * 2;
    68. HashTable newHT;
    69. newHT._tables.resize(newSize, nullptr);
    70. for (size_t i = 0; i < _tables.size(); ++i)
    71. {
    72. Node* cur = _tables[i];
    73. while (cur)
    74. {
    75. newHT.Insert(cur->_kv);
    76. cur = cur->_next;
    77. }
    78. }
    79. newHT._tables.swap(_tables);*/
    80. size_t newSize = _tables.size() == 0 ? 10 : _tables.size() * 2;
    81. vector newTable;
    82. newTable.resize(newSize, nullptr);
    83. for (size_t i = 0; i < _tables.size(); ++i)
    84. {
    85. Node* cur = _tables[i];
    86. while (cur)
    87. {
    88. Node* next = cur->_next;
    89. size_t hashi = hf(cur->_kv.first) % newSize;
    90. cur->_next = newTable[hashi];
    91. newTable[hashi] = cur;
    92. cur = next;
    93. }
    94. _tables[i] = nullptr;
    95. }
    96. newTable.swap(_tables);
    97. }
    98. size_t hashi = hf(kv.first);
    99. hashi %= _tables.size();
    100. // 头插到对应的桶即可
    101. Node* newnode = new Node(kv);
    102. newnode->_next = _tables[hashi];
    103. _tables[hashi] = newnode;
    104. ++_n;
    105. return true;
    106. }
    107. Node* Find(const K& key)
    108. {
    109. if (_tables.size() == 0)
    110. {
    111. return nullptr;
    112. }
    113. HashFunc hf;
    114. size_t hashi = hf(key);
    115. //size_t hashi = HashFunc()(key);
    116. hashi %= _tables.size();
    117. Node* cur = _tables[hashi];
    118. while (cur)
    119. {
    120. if (cur->_kv.first == key)
    121. {
    122. return cur;
    123. }
    124. cur = cur->_next;
    125. }
    126. return nullptr;
    127. }
    128. bool Erase(const K& key)
    129. {
    130. if (_tables.size() == 0)
    131. {
    132. return false;
    133. }
    134. HashFunc hf;
    135. size_t hashi = hf(key);
    136. hashi %= _tables.size();
    137. Node* prev = nullptr;
    138. Node* cur = _tables[hashi];
    139. while (cur)
    140. {
    141. if (cur->_kv.first == key)
    142. {
    143. if (prev == nullptr)
    144. {
    145. _tables[hashi] = cur->_next;
    146. }
    147. else
    148. {
    149. prev->_next = cur->_next;
    150. }
    151. delete cur;
    152. return true;
    153. }
    154. prev = cur;
    155. cur = cur->_next;
    156. }
    157. return false;
    158. //size_t hashi = key;
    159. //hashi %= _tables.size();
    160. //Node* cur = _tables[hashi];
    161. //while (cur)
    162. //{
    163. // if (cur->_kv.first == key)
    164. // {
    165. // if (cur->_next == nullptr)
    166. // {
    167. // cur->_kv = _tables[hashi]->_kv;
    168. // Node* first = _tables[hashi];
    169. // _tables[hashi] = first->_next;
    170. // delete first;
    171. // }
    172. // else
    173. // {
    174. // //....
    175. // }
    176. // return true;
    177. // }
    178. // prev = cur;
    179. // cur = cur->_next;
    180. //}
    181. //return false;
    182. }
    183. private:
    184. // 指针数组
    185. vector _tables;
    186. size_t _n = 0;
    187. };
    188. void TestHT1()
    189. {
    190. int a[] = { 20, 5, 8, 99999, 10, 30, 50 };
    191. //HashTable> ht;
    192. HashTable<int, int> ht;
    193. if (ht.Find(10))
    194. {
    195. cout << "找到了10" << endl;
    196. }
    197. for (auto e : a)
    198. {
    199. ht.Insert(make_pair(e, e));
    200. }
    201. ht.Erase(20);
    202. ht.Erase(10);
    203. ht.Erase(30);
    204. ht.Erase(50);
    205. // 测试扩容
    206. ht.Insert(make_pair(15, 15));
    207. ht.Insert(make_pair(5, 5));
    208. ht.Insert(make_pair(15, 15));
    209. ht.Insert(make_pair(25, 15));
    210. ht.Insert(make_pair(35, 15));
    211. ht.Insert(make_pair(45, 15));
    212. }
    213. void TestHT2()
    214. {
    215. int a[] = { 20, 5, 8, 99999, 10, 30, 50 };
    216. HashTable<int, int> ht;
    217. for (auto e : a)
    218. {
    219. ht.Insert(make_pair(e, e));
    220. }
    221. // 需要自己实现拷贝构造,完成链表桶深拷贝
    222. //HashTable copy(ht);
    223. }
    224. void TestHT3()
    225. {
    226. string arr[] = { "苹果", "西瓜", "苹果", "西瓜", "苹果", "苹果", "西瓜", "苹果", "香蕉", "苹果", "香蕉" };
    227. //HashTable countHT;
    228. HashTableint> countHT;
    229. for (auto& str : arr)
    230. {
    231. auto ret = countHT.Find(str);
    232. if (ret)
    233. {
    234. ret->_kv.second++;
    235. }
    236. else
    237. {
    238. countHT.Insert(make_pair(str, 1));
    239. }
    240. }
    241. }
    242. }

    阶段四:模拟实现unorderedmap/set

    (1)template 第二个模板T

    作用是让同一份哈希桶的代码能区分unordered_set和unordered_map,如果是unordered_set模板T就传入K,unordered_map模板T就传入pair

    (2)class KeyOfT模板参数:

    用于取出map中的pair中的K,取set中的key

    1. KeyOfT kot;
    2. if (Find(kot(data)))
    3. {
    4. return false;
    5. }

    (3)HashFunc模板 要放到 UnorderedSet.h / UnorderedMap.h 中

    因为K可能是 Date 这些自定义类型,无法直接比较,要写个仿函数DateHash,test_set() 中定义对象时就传这个仿函数unordered_set sd;

    UnorderedSet.h

    1. pragma once
    2. #include "HashTable.h"
    3. namespace bit
    4. {
    5. // 21:06
    6. template<class K, class HashFunc = DefaultHash>
    7. class unordered_set
    8. {
    9. struct SetKeyOfT
    10. {
    11. const K& operator()(const K& key)
    12. {
    13. return key;
    14. }
    15. };
    16. public:
    17. typedef typename Bucket::HashTable::iterator iterator;
    18. iterator begin()
    19. {
    20. return _ht.begin();
    21. }
    22. iterator end()
    23. {
    24. return _ht.end();
    25. }
    26. pairbool> insert(const K& key)
    27. {
    28. return _ht.Insert(key);
    29. }
    30. iterator find(const K& key)
    31. {
    32. return _ht.Find(key);
    33. }
    34. bool erase(const K& key)
    35. {
    36. return _ht.Erase(key);
    37. }
    38. private:
    39. Bucket::HashTable _ht;
    40. };
    41. struct Date
    42. {
    43. Date(int year = 1, int month = 1, int day = 1)
    44. :_year(year)
    45. , _month(month)
    46. , _day(day)
    47. {}
    48. bool operator==(const Date& d) const
    49. {
    50. return _year == d._year
    51. && _month == d._month
    52. && _day == d._day;
    53. }
    54. int _year;
    55. int _month;
    56. int _day;
    57. };
    58. struct DateHash
    59. {
    60. size_t operator()(const Date& d)
    61. {
    62. //return d._year + d._month + d._day;
    63. size_t hash = 0;
    64. hash += d._year;
    65. hash *= 131;
    66. hash += d._month;
    67. hash *= 1313;
    68. hash += d._day;
    69. //cout << hash << endl;
    70. return hash;
    71. }
    72. };
    73. void test_set()
    74. {
    75. unordered_set<int> s;
    76. //set s;
    77. s.insert(2);
    78. s.insert(3);
    79. s.insert(1);
    80. s.insert(2);
    81. s.insert(5);
    82. s.insert(12);
    83. //unordered_set::iterator it = s.begin();
    84. unordered_set<int>::iterator it;
    85. it = s.begin();
    86. //auto it = s.begin();
    87. while (it != s.end())
    88. {
    89. cout << *it << " ";
    90. ++it;
    91. }
    92. cout << endl;
    93. for (auto e : s)
    94. {
    95. cout << e << " ";
    96. }
    97. cout << endl;
    98. unordered_set sd;
    99. sd.insert(Date(2022, 3, 4));
    100. sd.insert(Date(2022, 4, 3));
    101. }
    102. }

    UnorderedMap.h

    1. #pragma once
    2. #include "HashTable.h"
    3. namespace bit
    4. {
    5. template<class K, class V, class HashFunc = DefaultHash>
    6. class unordered_map
    7. {
    8. struct MapKeyOfT
    9. {
    10. const K& operator()(const pair& kv)
    11. {
    12. return kv.first;
    13. }
    14. };
    15. public:
    16. typedef typename Bucket::HashTable, MapKeyOfT, HashFunc>::iterator iterator;
    17. iterator begin()
    18. {
    19. return _ht.begin();
    20. }
    21. iterator end()
    22. {
    23. return _ht.end();
    24. }
    25. pairbool> insert(const pair& kv)
    26. {
    27. return _ht.Insert(kv);
    28. }
    29. iterator find(const K& key)
    30. {
    31. return _ht.Find(key);
    32. }
    33. bool erase(const K& key)
    34. {
    35. return _ht.Erase(key);
    36. }
    37. V& operator[](const K& key)
    38. {
    39. pairbool> ret = insert(make_pair(key, V()));
    40. return ret.first->second;
    41. }
    42. private:
    43. Bucket::HashTable, MapKeyOfT, HashFunc> _ht;
    44. };
    45. void test_map()
    46. {
    47. unordered_map dict;
    48. dict.insert(make_pair("sort", ""));
    49. dict.insert(make_pair("left", ""));
    50. dict.insert(make_pair("left", "ʣ"));
    51. dict["string"];
    52. dict["left"] = "ʣ";
    53. dict["string"] = "ַ";
    54. unordered_map::iterator it = dict.begin();
    55. while (it != dict.end())
    56. {
    57. cout << it->first << " " << it->second << endl;
    58. ++it;
    59. }
    60. cout << endl;
    61. for (auto& kv : dict)
    62. {
    63. cout << kv.first << " " << kv.second << endl;
    64. }
    65. }
    66. }

    HashTable.h

    1. #pragma once
    2. #include
    3. template<class K>
    4. struct DefaultHash
    5. {
    6. size_t operator()(const K& key)
    7. {
    8. return (size_t)key;
    9. }
    10. };
    11. template<>
    12. struct DefaultHash
    13. {
    14. size_t operator()(const string& key)
    15. {
    16. // BKDR
    17. size_t hash = 0;
    18. for (auto ch : key)
    19. {
    20. hash = hash * 131 + ch;
    21. }
    22. return hash;
    23. }
    24. };
    25. namespace Bucket
    26. {
    27. template<class T>
    28. struct HashNode
    29. {
    30. T _data;
    31. HashNode* _next;
    32. HashNode(const T& data)
    33. :_data(data)
    34. , _next(nullptr)
    35. {}
    36. };
    37. template<class K, class T, class KeyOfT, class HashFunc>
    38. class HashTable;
    39. template<class K, class T, class KeyOfT, class HashFunc>
    40. class __HTIterator
    41. {
    42. typedef HashNode Node;
    43. typedef __HTIterator Self;
    44. public:
    45. Node* _node;
    46. HashTable* _pht;
    47. __HTIterator() {} //默认构造函数和非默认构造函数重载
    48. __HTIterator(Node* node, HashTable* pht)
    49. :_node(node)
    50. , _pht(pht)
    51. {}
    52. Self& operator++()
    53. {
    54. if (_node->_next)
    55. {
    56. _node = _node->_next;
    57. }
    58. else
    59. {
    60. KeyOfT kot;
    61. HashFunc hf;
    62. size_t hashi = hf(kot(_node->_data)) % _pht->_tables.size();
    63. ++hashi;
    64. //找下一个不为空的桶
    65. for (; hashi < _pht->_tables.size(); ++hashi)
    66. {
    67. if (_pht->_tables[hashi])
    68. {
    69. _node = _pht->_tables[hashi];
    70. break;
    71. }
    72. }
    73. // 没有找到不为空的桶,用nullptr去做end标识
    74. if (hashi == _pht->_tables.size())
    75. {
    76. _node = nullptr;
    77. }
    78. }
    79. return *this;
    80. }
    81. T& operator*()
    82. {
    83. return _node->_data;
    84. }
    85. T* operator->()
    86. {
    87. return &_node->_data;
    88. }
    89. bool operator!=(const Self& s) const
    90. {
    91. return _node != s._node;
    92. }
    93. bool operator==(const Self& s) const
    94. {
    95. return _node == s._node;
    96. }
    97. };
    98. // unordered_map ->HashTable, MapKeyOfT> _ht;
    99. // unordered_set ->HashTable _ht;
    100. template<class K, class T, class KeyOfT, class HashFunc>
    101. class HashTable
    102. {
    103. template<class K, class T, class KeyOfT, class HashFunc>
    104. friend class __HTIterator;
    105. typedef HashNode Node;
    106. public:
    107. typedef __HTIterator iterator;
    108. iterator begin()
    109. {
    110. for (size_t i = 0; i < _tables.size(); ++i)
    111. {
    112. Node* cur = _tables[i];
    113. if (cur)
    114. {
    115. return iterator(cur, this);
    116. }
    117. }
    118. return end();
    119. }
    120. iterator end()
    121. {
    122. return iterator(nullptr, this);
    123. }
    124. ~HashTable()
    125. {
    126. for (size_t i = 0; i < _tables.size(); ++i)
    127. {
    128. Node* cur = _tables[i];
    129. while (cur)
    130. {
    131. Node* next = cur->_next;
    132. delete cur;
    133. cur = next;
    134. }
    135. _tables[i] = nullptr;
    136. }
    137. }
    138. size_t GetNextPrime(size_t prime)
    139. {
    140. const int PRIMECOUNT = 28;
    141. static const size_t primeList[PRIMECOUNT] =
    142. {
    143. 53ul, 97ul, 193ul, 389ul, 769ul,
    144. 1543ul, 3079ul, 6151ul, 12289ul, 24593ul,
    145. 49157ul, 98317ul, 196613ul, 393241ul, 786433ul,
    146. 1572869ul, 3145739ul, 6291469ul, 12582917ul, 25165843ul,
    147. 50331653ul, 100663319ul, 201326611ul, 402653189ul, 805306457ul,
    148. 1610612741ul, 3221225473ul, 4294967291ul
    149. };
    150. // 获取比prime大那一个素数
    151. size_t i = 0;
    152. for (; i < PRIMECOUNT; ++i)
    153. {
    154. if (primeList[i] > prime)
    155. return primeList[i];
    156. }
    157. return primeList[i];
    158. }
    159. pairbool> Insert(const T& data)
    160. {
    161. HashFunc hf;
    162. KeyOfT kot;
    163. iterator pos = Find(kot(data));
    164. if (pos != end())
    165. {
    166. return make_pair(pos, false);
    167. }
    168. // 负载因子 == 1 扩容
    169. if (_tables.size() == _n)
    170. {
    171. //size_t newSize = _tables.size() == 0 ? 11 : _tables.size() * 2;
    172. size_t newSize = GetNextPrime(_tables.size());
    173. if (newSize != _tables.size())
    174. {
    175. vector newTable;
    176. newTable.resize(newSize, nullptr);
    177. for (size_t i = 0; i < _tables.size(); ++i)
    178. {
    179. Node* cur = _tables[i];
    180. while (cur)
    181. {
    182. Node* next = cur->_next;
    183. size_t hashi = hf(kot(cur->_data)) % newSize;
    184. cur->_next = newTable[hashi];
    185. newTable[hashi] = cur;
    186. cur = next;
    187. }
    188. _tables[i] = nullptr;
    189. }
    190. newTable.swap(_tables);
    191. }
    192. }
    193. size_t hashi = hf(kot(data));
    194. hashi %= _tables.size();
    195. // 头插到对应的桶即可
    196. Node* newnode = new Node(data);
    197. newnode->_next = _tables[hashi];
    198. _tables[hashi] = newnode;
    199. ++_n;
    200. return make_pair(iterator(newnode, this), false);;
    201. }
    202. iterator Find(const K& key)
    203. {
    204. if (_tables.size() == 0)
    205. {
    206. return iterator(nullptr, this);
    207. }
    208. KeyOfT kot;
    209. HashFunc hf;
    210. size_t hashi = hf(key);
    211. //size_t hashi = HashFunc()(key);
    212. hashi %= _tables.size();
    213. Node* cur = _tables[hashi];
    214. while (cur)
    215. {
    216. if (kot(cur->_data) == key)
    217. {
    218. return iterator(cur, this);
    219. }
    220. cur = cur->_next;
    221. }
    222. return iterator(nullptr, this);
    223. }
    224. bool Erase(const K& key)
    225. {
    226. if (_tables.size() == 0)
    227. {
    228. return false;
    229. }
    230. HashFunc hf;
    231. KeyOfT kot;
    232. size_t hashi = hf(key);
    233. hashi %= _tables.size();
    234. Node* prev = nullptr;
    235. Node* cur = _tables[hashi];
    236. while (cur)
    237. {
    238. if (kot(cur->_data) == key)
    239. {
    240. if (prev == nullptr)
    241. {
    242. _tables[hashi] = cur->_next;
    243. }
    244. else
    245. {
    246. prev->_next = cur->_next;
    247. }
    248. delete cur;
    249. return true;
    250. }
    251. prev = cur;
    252. cur = cur->_next;
    253. }
    254. return false;
    255. }
    256. private:
    257. // 指针数组
    258. vector _tables;
    259. size_t _n = 0;
    260. };
    261. }

    Test.cpp

    1. #include
    2. #include
    3. #include
    4. #include
    5. #include
    6. #include
    7. using namespace std;
    8. #include "HashTable.h"
    9. #include "UnorderedMap.h"
    10. #include "UnorderedSet.h"
    11. void test_set()
    12. {
    13. unordered_set<int> s;
    14. //set s;
    15. s.insert(2);
    16. s.insert(3);
    17. s.insert(1);
    18. s.insert(2);
    19. s.insert(5);
    20. //unordered_set::iterator it = s.begin();
    21. auto it = s.begin();
    22. while (it != s.end())
    23. {
    24. cout << *it << " ";
    25. ++it;
    26. }
    27. cout << endl;
    28. for (auto e : s)
    29. {
    30. cout << e << " ";
    31. }
    32. cout << endl;
    33. }
    34. void test_op()
    35. {
    36. int n = 10000000;
    37. vector<int> v;
    38. v.reserve(n);
    39. srand(time(0));
    40. for (int i = 0; i < n; ++i)
    41. {
    42. //v.push_back(i);
    43. //v.push_back(rand()+i); // 重复少
    44. v.push_back(rand()); // 重复多
    45. }
    46. size_t begin1 = clock();
    47. set<int> s;
    48. for (auto e : v)
    49. {
    50. s.insert(e);
    51. }
    52. size_t end1 = clock();
    53. size_t begin2 = clock();
    54. unordered_set<int> us;
    55. for (auto e : v)
    56. {
    57. us.insert(e);
    58. }
    59. size_t end2 = clock();
    60. cout << s.size() << endl;
    61. cout << "set insert:" << end1 - begin1 << endl;
    62. cout << "unordered_set insert:" << end2 - begin2 << endl;
    63. size_t begin3 = clock();
    64. for (auto e : v)
    65. {
    66. s.find(e);
    67. }
    68. size_t end3 = clock();
    69. size_t begin4 = clock();
    70. for (auto e : v)
    71. {
    72. us.find(e);
    73. }
    74. size_t end4 = clock();
    75. cout << "set find:" << end3 - begin3 << endl;
    76. cout << "unordered_set find:" << end4 - begin4 << endl;
    77. size_t begin5 = clock();
    78. for (auto e : v)
    79. {
    80. s.erase(e);
    81. }
    82. size_t end5 = clock();
    83. size_t begin6 = clock();
    84. for (auto e : v)
    85. {
    86. us.erase(e);
    87. }
    88. size_t end6 = clock();
    89. cout << "set erase:" << end5 - begin5 << endl;
    90. cout << "unordered_set erase:" << end6 - begin6 << endl;
    91. }
    92. void test_map()
    93. {
    94. unordered_map dict;
    95. dict.insert(make_pair("sort", "排序"));
    96. dict.insert(make_pair("left", "左边"));
    97. dict.insert(make_pair("left", "剩余"));
    98. dict["string"];
    99. dict["left"] = "剩余";
    100. dict["string"] = "字符串";
    101. }
    102. int main()
    103. {
    104. bit::test_set();
    105. //test_op();
    106. bit::test_map();
    107. //Bucket::TestHT3();
    108. //TestHT2();
    109. return 0;
    110. }

  • 相关阅读:
    mdio bcm5482访问
    从入门开始手把手搭建千万级Java算法测试-计数排序与快速排序的比较(常规快排)比较
    visio导出SVG矢量图在wps中显示问题
    AMD有史以来最大的一笔交易,350亿美元收购赛灵思成功
    python高阶爬虫---视频类内容爬取(以BiliBili和城市影院为例说明)
    多语言的字符串处理记录
    SpringCloud Sleuth 分布式请求链路跟踪
    SQL二次注入详解
    create® 3入门教程-反应Reflexes
    索引数据结构选择
  • 原文地址:https://blog.csdn.net/zhang_si_hang/article/details/126739994