当前位置:   article > 正文

C++&&数据结构——哈希表_c++哈希表

c++哈希表

目录

一,unordered系列容器

1.1 关于unordered系列

1.2 unordered_set

1.4 unordered_map

1.5 性能对比

二,哈希

2.1 哈希概念

2.2 常用哈希函数

2.3 哈希冲突及解决

2.3.1 闭散列

2.3.2 开散列

2.4 哈希表扩容

2.4.1 闭散列扩容

2.4.2 开散列扩容

三,哈希表模拟实现

3.1 映射函数实现

3.2 闭散列哈希表

3.3 开散列哈希表


一,unordered系列容器

1.1 关于unordered系列

在C++98中,STL提供了以红黑树为底层的一系列关联式容器,查询时效率可达到logN,但是当树中节点非常多时,查询效率也不理想,所以在C++11中,STL提供了unordered系列的几个容器,使用哈希表作为底层,大大增加了查询效率

1.2 unordered_set

关于unordered_set的使用和之前介绍的set大体相同,如下代码:

  1. void test_unordered_set()
  2. {
  3. unordered_set<int> s;
  4. s.insert(2);
  5. s.insert(3);
  6. s.insert(1);
  7. s.insert(2);
  8. s.insert(5);
  9. unordered_set<int>::iterator it = s.begin();
  10. while (it != s.end())
  11. {
  12. cout << *it << " ";
  13. ++it;
  14. }
  15. cout << endl;
  16. }

 

1.4 unordered_map

  1. void test_unordered_map()
  2. {
  3. string arr[] = { "西瓜", "西瓜", "苹果", "西瓜", "苹果", "苹果", "西瓜", "苹果", "香蕉", "苹果", "香蕉", "梨" };
  4. map<string, int> countMap;
  5. for (auto& e : arr)
  6. {
  7. countMap[e]++;
  8. }
  9. for (auto& kv : countMap)
  10. {
  11. cout << kv.first << ":" << kv.second << endl;
  12. }
  13. }

 

1.5 性能对比

  1. void test_op() //测试性能
  2. {
  3. //产生n个随机数
  4. int n = 100000;
  5. vector<int> v;
  6. v.reserve(n);
  7. srand(time(0));
  8. for (int i = 0; i < n; ++i)
  9. {
  10. //把n个随机数放到vector里去
  11. //v.push_back(i); //有序插入
  12. // //v.push_back(rand()); // 重复多
  13. v.push_back(rand() + i); // 重复少
  14. }
  15. size_t begin1 = clock();
  16. set<int> s;
  17. for (auto e : v)
  18. {
  19. s.insert(e);//先往set插入
  20. }
  21. size_t end1 = clock();
  22. size_t begin2 = clock();
  23. unordered_set<int> us;
  24. for (auto e : v)
  25. {
  26. us.insert(e);//再往unordered_set插入
  27. }
  28. size_t end2 = clock();
  29. cout << "size:" << s.size() << endl;
  30. cout << "set insert:" << end1 - begin1 << endl; //算出set插入时间
  31. cout << "unordered_set insert:" << end2 - begin2 << endl; //算出unordered_set插入时间
  32. cout << endl;
  33. size_t begin3 = clock();
  34. for (auto e : v)
  35. {
  36. s.find(e);
  37. }
  38. size_t end3 = clock();
  39. size_t begin4 = clock();
  40. for (auto e : v)
  41. {
  42. us.find(e);
  43. }
  44. size_t end4 = clock();
  45. //对比查找效率
  46. cout << "set find:" << end3 - begin3 << endl;
  47. cout << "unordered_set find:" << end4 - begin4 << endl;
  48. cout << endl;
  49. size_t begin5 = clock();
  50. for (auto e : v)
  51. {
  52. s.erase(e);
  53. }
  54. size_t end5 = clock();
  55. size_t begin6 = clock();
  56. for (auto e : v)
  57. {
  58. us.erase(e);
  59. }
  60. size_t end6 = clock();
  61. //对比删除效率
  62. cout << "set erase:" << end5 - begin5 << endl;
  63. cout << "unordered_set erase:" << end6 - begin6 << endl;
  64. unordered_map<string, int> countMap;
  65. countMap.insert(make_pair("苹果", 1));
  66. //可以支持
  67. unordered_map<string, int> countmap;
  68. countmap.insert(make_pair("苹果", 1));
  69. //综合各种场景而言,unordered系列综合性能是更好的,尤其是find
  70. }

 

二,哈希

2.1 哈希概念

哈希本质是一种设计思路。

顺序结构以及平衡树中,元素关键码与其存储位置之间没有对应的关系,因此在查找一个元素时,必须经过多次关键码的比较。顺序查找时间复杂度为O(N),平衡树中为O(logN),查找效率取决于搜索过程中元素的比较次数。

所以,为了使查找效率更高,推出了一种理想的搜索方法,即不经过任何比较,一次直接从表中查到相关的数据。如果构造一种存储结构,通过某种函数(HashFunc)使元素的存储位置与它的关键码之间建立一一映射的关系,那么查找时能很快找到该数据。

向该结构中:

①插入元素:根据插入元素的key值,以此函数计算出该元素的存储位置进行存放

②查找元素:对要查找元素的key值进行相同的计算,得出存储位置,再对比关键码查看结构当中是否有该元素

该方法被称为哈希(散列)方法,哈希方法中使用的位置计算函数称为哈希(散列)函数,构造出来的结构称为哈希表

例如:数据集合{ 1,6,7,4,5,9 },哈希函数设置为hash(key) = key % capacity

用该方法进行搜索不必进行多次关键码比较,因此搜索速度比较快

2.2 常用哈希函数

1,直接定值法

取关键字的某个线性函数为散列地址:Hash(key) = A*key + B。

这种方法的优点是简单,缺点是需要提前直到关键字的分布情况,适合查找比较小且连续的数据

2,除留余数法

设哈希表允许的地址数为m,去一个不大于m,但最接近或者等于m的质数作为除数,按照哈希函数:Hash(key) = key%p (p<=m),将关键码转换成哈希地址

2.3 哈希冲突及解决

就上面的图而言,如果插入44时,会算出和4同样的位置。

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

2.3.1 闭散列

闭散列也叫开放定址法,当发生哈希冲突时,如果哈希表未被填满,说明哈必然还有其他空位置,那么可以把key存放到冲突位置的“下一个”空位置中去,这里寻找下一个位置的方法称为“线性探测”

就上面的图,我们要插入44,会发生哈希冲突,所以我们从发生冲突的位置开始依次向后探测,直到寻找到下一个空位置为止,该方法应用在插入函数中,如下图:

对于删除,采用闭散列处理哈希冲突时,不能直接删除表中的数据否则会影响其他数据的搜索,所以采用标记的伪删除法来删除,给要删除的位置打上delete的标记,具体实现请看后面的模拟实现部分

2.3.2 开散列

开散列又叫链地址法,首先对关键码集合用哈希函数计算地址,具有相同地址的关键码用一个单链表集合起来,称每个单链表为一个桶,每个链表的头结点存在哈希表中,如下图:

2.4 哈希表扩容

2.4.1 闭散列扩容

哈希表的负载因子定义为:i = 表中现有数据个数/表的总长度

由于表长是定值,i与表中现有数据个数成正比,所以,负载因子越大,表面填入表中的数据越多,产生冲突的可能性越大,负载因子越小,产生冲突的可能性越小。

对于开放定址法,负载因子必须严格限制在0.7 -- 0.8以下,超过0.8,查表时的CPU的计算效率成指数上升。因此,一些采用开放定址法的hash库,如Java的库限制了负载因子为0.75,超过将resize哈希表。扩容具体实现请看下面哈希表模拟实现部分

2.4.2 开散列扩容

桶的个数是一定的,随着数据的不断插入,每个桶中元素不断增多,极端情况下可能会导致一个桶中的链表节点非常多,影响哈希表的查找效率,所以需要对哈希表进行增容。

最好的情况是,每个桶中刚好有一个节点,再插入数据时,都会发生哈希冲突,所以在数据个数等于桶的个数时,也就是负载因子等于1的适合进行扩容

注:如果实在没办法扩容,但是又有很多值经过哈希函数运算后插入同一个地址,那么可以将桶挂单链表改为挂红黑树。

三,哈希表模拟实现

3.1 映射函数实现

  1. template<class K>
  2. struct HashFunc
  3. {
  4. size_t operator()(const K& key)
  5. {
  6. return (size_t)key;
  7. }
  8. };
  9. //特化 -- 如果是普通类型走上面的,如果是string类型走下面的
  10. template<>
  11. struct HashFunc<string>
  12. {
  13. //将字符串变成整数以后非常常见
  14. size_t operator()(const string& key)
  15. {
  16. //BKDR算法
  17. size_t val = 0;
  18. for (auto ch : key)
  19. {
  20. val *= 131;
  21. val += ch;
  22. }
  23. return val;
  24. }
  25. };

3.2 闭散列哈希表

  1. enum State//标志位,解决删除带来的老六问题
  2. {
  3. EMPTY,
  4. EXIST,
  5. DELETE
  6. };
  7. template<class K, class V>
  8. struct HashData
  9. {
  10. pair<K, V> _kv;
  11. State _state = EMPTY;
  12. };
  13. template<class K, class V, class Hash = HashFunc<K>>//仿函数
  14. class HashTable
  15. {
  16. public:
  17. bool Insert(const pair<K, V>& kv)
  18. {
  19. if (Find(kv.first))
  20. return false;
  21. if (_tables.size() == 0 || 10 * _size / _tables.size() >= 7)//使用负载因子控制扩容
  22. {
  23. //size_t newsize = _tables.size() == 0 ? 10 : _tables.size() * 2;
  24. //vector<HashData> newtables(newsize);
  25. 遍历旧表,重新映射到新表
  26. //for (auto& data : _tables)
  27. //{
  28. // if (data._state == EXIST)
  29. // {
  30. // // 重新算在新表的位置
  31. // size_t i = 1;
  32. // size_t index = hashi;
  33. // while (newtables[index]._state == EXIST)
  34. // {
  35. // index = hashi + i;
  36. // index %= newtables.size();
  37. // ++i;
  38. // }
  39. // newtables[index]._kv = data._kv;
  40. // newtables[index]._state = EXIST;
  41. // }
  42. //}
  43. //_tables.swap(newtables);
  44. size_t newSize = _tables.size() == 0 ? 10 : _tables.size() * 2;
  45. HashTable<K, V> newHT;
  46. newHT._tables.resize(newSize);//把空间开好
  47. for (auto& e : _tables)//遍历旧表
  48. {
  49. if (e._state == EXIST)
  50. {
  51. newHT.Insert(e._kv);//直接复用插入
  52. }
  53. }
  54. _tables.swap(newHT._tables);
  55. }
  56. //线性探测
  57. Hash hash;
  58. size_t hashi = hash(kv.first) % _tables.size();
  59. while (_tables[hashi]._state == EXIST)
  60. {
  61. //找空位置,如果走到结尾了,从头开始找,反正要找到一个空位置
  62. //注意不是从头开始找,从映射的值的那个位置开始找
  63. hashi++;
  64. hashi %= _tables.size();
  65. }
  66. _tables[hashi]._kv = kv;
  67. _tables[hashi]._state = EXIST;
  68. ++_size;
  69. 线性探测问题:某个位置冲突很多的情况下,互相占用,冲突一篇,效率变低
  70. 二次探测 -- 按2的i次方进行探测 hash+i^2 (i>=0)
  71. //while (_tables[hashi]._state == EXIST)
  72. //{
  73. // ++i;
  74. // hashi = start + i * i;
  75. // hashi %= _tables.size();
  76. //}
  77. //tables[hashi].kv = kv;
  78. //_tables[hashi].state = EXIST;
  79. //++_szie;
  80. return true;
  81. }
  82. HashData<K, V>* Find(const K& key)
  83. {
  84. if (_tables.size() == 0)//表为空,返回空
  85. {
  86. return nullptr;
  87. }
  88. Hash hash;
  89. size_t start = hash(key) % _tables.size();
  90. size_t hashi = start;
  91. while (_tables[hashi]._state != EMPTY)
  92. {
  93. if (_tables[hashi]._state != DELETE && _tables[hashi]._kv.first == key)//要同时判断状态和值
  94. {
  95. return &_tables[hashi];//找到了,返回该处的地址
  96. }
  97. else
  98. {
  99. hashi++;
  100. hashi %= _tables.size();
  101. if (hashi == start)//极端判断,删一点插入一点
  102. break;
  103. }
  104. }
  105. return nullptr;
  106. }
  107. bool Erase(const K& key)
  108. {
  109. HashData<K, V>* ret = Find(key);
  110. if (ret)
  111. {
  112. ret->_state = DELETE;
  113. --_size;
  114. return true;
  115. }
  116. else
  117. {
  118. return false;//要删除的值不存在
  119. }
  120. }
  121. void Print()
  122. {
  123. for (size_t i = 0; i < _tables.size(); i++)
  124. {
  125. if (_tables[i]._state == EXIST)
  126. {
  127. printf("[%d:%d]", i, _tables[i]._kv.first);
  128. }
  129. else
  130. {
  131. printf("[%d:*]", i);
  132. }
  133. }
  134. cout << endl;
  135. }
  136. private:
  137. vector<HashData<K, V>> _tables;
  138. size_t _size;//表示已经存储了多少个有效数据
  139. };

测试代码: 

  1. void TestHashTable1()
  2. {
  3. int a[] = { 1,11,4,15,26,7,44,9 };
  4. HashTable<int, int> ht;
  5. for (auto e : a)
  6. {
  7. ht.Insert(make_pair(e, e));
  8. }
  9. ht.Print();
  10. ht.Erase(4);
  11. cout << ht.Find(44)->_kv.first << endl;
  12. cout << ht.Find(4) << endl;
  13. ht.Print();
  14. }
  15. void TestHashTable2()
  16. {
  17. int a[] = { 3, 33, 2, 13, 5, 12, 1002 };
  18. HashTable<int, int> ht;
  19. for (auto e : a)
  20. {
  21. ht.Insert(make_pair(e, e));
  22. }
  23. ht.Insert(make_pair(15, 15));
  24. if (ht.Find(13))
  25. {
  26. cout << "13在" << endl;
  27. }
  28. else
  29. {
  30. cout << "13不在" << endl;
  31. }
  32. ht.Erase(13);
  33. if (ht.Find(13))
  34. {
  35. cout << "13在" << endl;
  36. }
  37. else
  38. {
  39. cout << "13不在" << endl;
  40. }
  41. }
  42. void TestHashTable3()
  43. {
  44. string arr[] = { "苹果", "西瓜", "苹果", "西瓜", "苹果", "苹果", "西瓜", "苹果", "香蕉", "苹果", "香蕉" };
  45. HashTable<string, int> countHT;
  46. for (auto& str : arr)
  47. {
  48. auto ptr = countHT.Find(str);
  49. if (ptr)
  50. {
  51. ptr->_kv.second++;
  52. }
  53. else
  54. {
  55. countHT.Insert(make_pair(str, 1));
  56. }
  57. }
  58. }
  59. void TestHashTable4()
  60. {
  61. HashFunc<string> hash;
  62. cout << hash("abcd") << endl;
  63. cout << hash("bcad") << endl;
  64. cout << hash("eat") << endl;
  65. cout << hash("ate") << endl;
  66. cout << hash("abcd") << endl;
  67. cout << hash("aadd") << endl << endl;
  68. cout << hash("abcd") << endl;
  69. cout << hash("bcad") << endl;
  70. cout << hash("eat") << endl;
  71. cout << hash("ate") << endl;
  72. cout << hash("abcd") << endl;
  73. cout << hash("aadd") << endl << endl;
  74. }

3.3 开散列哈希表

  1. template<class K, class V>
  2. struct HashNode
  3. {
  4. pair<K, V>_kv;
  5. HashNode<K, V>* _next;
  6. HashNode(const pair<K, V>& kv)
  7. :_kv(kv)
  8. , _next(nullptr)
  9. {}
  10. };
  11. template<class K, class V, class Hash = HashFunc<K>>
  12. class HashTable
  13. {
  14. typedef HashNode<K, V> Node;
  15. public:
  16. ~HashTable()
  17. {
  18. /*for (size_t i = 0; i < _tables.size(); i++)
  19. {
  20. Node* cur = _tables[i];
  21. while (cur)
  22. {
  23. Node* next = cur->_next;
  24. delete(cur);
  25. cur = next;
  26. }
  27. _tables[i] = nullptr;
  28. }*/
  29. for (auto& cur : _tables)
  30. {
  31. while (cur)
  32. {
  33. Node* next = cur->_next;
  34. delete cur;
  35. cur = next;
  36. }
  37. cur = nullptr;
  38. }
  39. }
  40. Node* Find(const K& key)
  41. {
  42. if (_tables.size() == 0)
  43. {
  44. return nullptr;
  45. }
  46. Hash hash;
  47. size_t hashi = hash(key) % _tables.size();
  48. Node* cur = _tables[hashi];
  49. while (cur)
  50. {
  51. if (cur->_kv.first == key)
  52. {
  53. return cur;
  54. }
  55. cur = cur->_next;
  56. }
  57. return nullptr;
  58. }
  59. bool Erase(const K& key)
  60. {
  61. //不能直接用Find找这个节点
  62. if (_tables.size() == 0)
  63. {
  64. return nullptr;
  65. }
  66. //现在这个数组是一个指针数组,里面存的都是指针,由hashi的数字代表位置,_tables[hashi]代表该位置的指针,该指针指向下面的单链表
  67. Hash hash;
  68. size_t hashi = hash(key) % _tables.size();
  69. Node* cur = _tables[hashi];
  70. Node* prev = nullptr;
  71. while (cur)
  72. {
  73. if (cur->_kv.first == key)//找到了
  74. {
  75. if (prev == nullptr)//头删
  76. {
  77. _tables[hashi] = cur->_next;
  78. }
  79. else//中间删
  80. {
  81. prev->_next = cur->_next;
  82. }
  83. delete cur;
  84. --_size;
  85. return true;
  86. }
  87. else//没找到
  88. {
  89. prev = cur;
  90. cur = cur->_next;
  91. }
  92. }
  93. return false;
  94. }
  95. inline size_t GetNextPrime(size_t prime)
  96. {
  97. //SGI
  98. static const size_t __stl_num_primes = 28;
  99. static const size_t __stl_prime_list[__stl_num_primes] =
  100. {
  101. 53, 97, 193, 389, 769,
  102. 1543, 3079, 6151, 12289, 24593,
  103. 49157, 98317, 196613, 393241, 786433,
  104. 1572869, 3145739, 6291469, 12582917, 25165843,
  105. 50331653, 100663319, 201326611, 402653189, 805306457,
  106. 1610612741, 3221225473, 4294967291
  107. };
  108. //去上面的一堆素数中找第一个大于n的值
  109. for (size_t i = 0; i < __stl_num_primes; ++i)
  110. {
  111. if (__stl_prime_list[i] > prime)
  112. {
  113. return __stl_prime_list[i];
  114. }
  115. }
  116. return -1;
  117. }
  118. bool Insert(const pair<K, V>& kv)
  119. {
  120. //去重
  121. if (Find(kv.first))
  122. {
  123. return false;
  124. }
  125. Hash hash;
  126. //负载因子到1就扩容
  127. if (_size == _tables.size())
  128. {
  129. //size_t newSize = _tables.size() == 0 ? 10 : _tables.size() * 2;
  130. vector<Node*> newTables;
  131. //newTables.resize(newSize, nullptr);
  132. newTables.resize(GetNextPrime(_tables.size()), nullptr);//利用素数表来扩容
  133. //旧表中节点映射移动到新表
  134. for (size_t i = 0; i < _tables.size(); i++)//如果复用insert,调用insert,会生成新节点,然后拷贝再释放旧节点,代价太大,所以我们直接把旧节点废物利用,直接把旧节点搞过来
  135. {
  136. Node* cur = _tables[i];
  137. while (cur)
  138. {
  139. Node* next = cur->_next;
  140. size_t hashi = hash(cur->_kv.first) % newTables.size();//通过映射找到新表中对应的位置
  141. cur->_next = newTables[hashi];
  142. newTables[hashi] = cur;
  143. cur = next;//往后走
  144. }
  145. _tables[i] = nullptr;
  146. }
  147. _tables.swap(newTables);
  148. }
  149. size_t hashi = hash(kv.first) % _tables.size();
  150. //头插
  151. Node* newnode = new Node(kv);
  152. newnode->_next = _tables[hashi];
  153. _tables[hashi] = newnode;
  154. ++_size;
  155. return true;
  156. }
  157. size_t Size()
  158. {
  159. return _size;
  160. }
  161. //表的长度 -- 有多少个桶的位置
  162. size_t TablesSize()
  163. {
  164. return _tables.size();
  165. }
  166. //表中已经有多少桶被使用了
  167. size_t BucketNum()
  168. {
  169. size_t num = 0;
  170. for (size_t i = 0; i < _tables.size(); ++i)
  171. {
  172. if (_tables[i])
  173. {
  174. ++num;
  175. }
  176. }
  177. return num;
  178. }
  179. size_t MaxBucketLenth()
  180. {
  181. size_t maxLen = 0;
  182. for (size_t i = 0; i < _tables.size(); ++i)
  183. {
  184. size_t len = 0;
  185. Node* cur = _tables[i];
  186. while (cur)
  187. {
  188. ++len;
  189. cur = cur->_next;
  190. }
  191. if (len > maxLen)
  192. {
  193. maxLen = len;
  194. }
  195. }
  196. return maxLen;
  197. }
  198. private:
  199. vector<Node*> _tables;
  200. size_t _size = 0;//存储的有效数据
  201. };

测试代码:

  1. void TestHashBucket1()
  2. {
  3. int a[] = { 3, 33, 2, 13, 5, 12, 1002 };
  4. HashTable<int, int> ht;
  5. for (auto e : a)
  6. {
  7. ht.Insert(make_pair(e, e));
  8. }
  9. ht.Insert(make_pair(15, 15));
  10. ht.Insert(make_pair(25, 25));
  11. ht.Insert(make_pair(35, 35));
  12. ht.Insert(make_pair(45, 45));
  13. ht.Erase(12);
  14. ht.Erase(3);
  15. ht.Erase(33);
  16. }
  17. void TestHashBucket2()
  18. {
  19. string arr[] = { "苹果", "西瓜", "苹果", "西瓜", "苹果", "苹果", "西瓜", "苹果", "香蕉", "苹果", "香蕉" };
  20. //HashTable<string, int, HashFuncString> countHT;
  21. HashTable<string, int> countHT;
  22. for (auto& str : arr)
  23. {
  24. auto ptr = countHT.Find(str);
  25. if (ptr)
  26. {
  27. ptr->_kv.second++;
  28. }
  29. else
  30. {
  31. countHT.Insert(make_pair(str, 1));
  32. }
  33. }
  34. }
  35. //字符串转整形算法测试函数
  36. void TestHashBucket3()
  37. {
  38. //HashTable<string, string, HashStr> ht;
  39. HashTable<string, string> ht;
  40. ht.Insert(make_pair("sort", "排序"));
  41. ht.Insert(make_pair("string", "字符串"));
  42. ht.Insert(make_pair("left", "左边"));
  43. ht.Insert(make_pair("right", "右边"));
  44. ht.Insert(make_pair("", "右边"));
  45. HashFunc<string> hashstr;
  46. cout << hashstr("abcd") << endl;
  47. cout << hashstr("bcda") << endl;
  48. cout << hashstr("aadd") << endl;
  49. cout << hashstr("eat") << endl;
  50. cout << hashstr("ate") << endl;
  51. }
  52. void TestHashBucket4()
  53. {
  54. int n = 1000000;
  55. vector<int> v;
  56. v.reserve(n);
  57. srand(time(0));
  58. for (int i = 0; i < n; ++i)
  59. {
  60. //v.push_back(i);
  61. v.push_back(rand() + i); // 重复少
  62. //v.push_back(rand()); // 重复多
  63. }
  64. size_t begin1 = clock();
  65. HashTable<int, int> ht;
  66. for (auto e : v)
  67. {
  68. ht.Insert(make_pair(e, e));
  69. }
  70. size_t end1 = clock();
  71. cout << "数据个数:" << ht.Size() << endl;
  72. cout << "表的长度:" << ht.TablesSize() << endl;
  73. cout << "桶的个数:" << ht.BucketNum() << endl;
  74. cout << "平均每个桶的长度:" << (double)ht.Size() / (double)ht.BucketNum() << endl;
  75. cout << "最长的桶的长度:" << ht.MaxBucketLenth() << endl;
  76. cout << "负载因子:" << (double)ht.Size() / (double)ht.TablesSize() << endl;
  77. }
声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/weixin_40725706/article/detail/325143
推荐阅读
相关标签
  

闽ICP备14008679号