1. 哈希的概念
哈希(又称作散列)是一种通过哈希函数将关键字key映射到存储位置的思想,可以支持 o(1) 时间复杂度的快速查找。而哈希表是这种思想的具体实现,核心在于映射与冲突解决。
之前我们可能接触过类似的哈希思想,例如直接定址法:当key的范围比较集中时,我们可以简单高效地通过建立一个数组,每个key直接对应数组下标;在存储英文字母时,我们也可以通过ascii码-a的ascii码从而得到存储位置下标。本质就是通过key计算出一个绝对/相对位置,存储在数组中,实现快速查找和排序。
2. 哈希冲突和负载因子
2.1 哈希冲突
直接定址法虽然简单粗暴,但有时key的范围并没有预期的那样集中,此时如果建立过大的数组就会导致浪费内存甚至内存不足。因此,当我们开的数组范围并未完全包含key的映射范围时,就会产生多个key映射到数组中同一个位置的问题,这就是哈希冲突。哈希冲突不可避免,但我们可以通过优化哈希函数来减少哈希冲突,并设计解决冲突的方案。
2.2 负载因子
假设哈希表中存储了n个值,哈希表的大小为m,那么负载因子=n/m,可以理解为哈希表的存储率/空间利用率。负载因子越大,哈希冲突的概率越高。
3. 模拟实现哈希表
3.1 对key的要求
3.1.1 支持将key转为整数
实现哈希表首先要对key计算变换,使其能够存入数组,这一部分是哈希函数的工作。但在此之前,我们需要对key初次加工,使其能够作为参数传入哈希函数。也就是设计一个能将key转为整数的前置函数。这道工序就相当于图中的绿色方框,专门处理非size_t类型的key:

以string为例,这里我们使用bkdr哈希思路,将字符串转为size_t型存储。通过反复乘131并累加字符值,将字符串映射为size_t整数,在哈希表中用于确定存储位置。这种思路的优点在于能大大减少哈希冲突:
//key转成整型
template<class k>
struct hashfunc
{
size_t operator()(const k& key)
{
return (size_t)key;
}
};
//(全特化)针对string的版本
template<>
struct hashfunc<string>
{
size_t operator()(const string& key)
{
size_t hash = 0;
for (auto e : key) //bkdr哈希
{
hash *= 131;
hash += e;
}
return hash;
}
};3.1.2 支持等价比较
哈希表的插入,查找,删除功能都需要key支持等价比较的条件。以unordered_set为例,在查找key时,需要计算出对应的哈希值,并加以定位和遍历,直至遇到符合的key;插入和删除操作内部则嵌入了查找的逻辑,从而实现“key不重复”和“找到key并删除”的功能。
3.2 减少哈希冲突——扩容和哈希函数
减少哈希冲突有两种方式,即适当扩容和采用更优的哈希函数:
- 扩容:当哈希表的负载因子达到了
0.7(开放定址法)/1(链地址法),则说明需要扩容。 - 哈希函数:⼀个好的哈希函数应该让n个关键字被
等概率的均匀散列分布到哈希表的m个空间中,但是实际中却很难做到,但是我们要尽量往这个⽅向去考量设计。以下是关于哈希函数的介绍:
3.2.1 除法散列法 / 除留余数法(推荐)
顾名思义,除法散列法通过取余数得到哈希值。假设哈希表⼤⼩为m,那么通过key除以m的余数作为映射位置的下标,也就是哈希函数为:h(key) = key % m。
注意事项:
使⽤除法散列法时,应尽量避免m为某些值,如2的幂,10的幂等(本质上是保留末n位)。这里给出的建议是:m取不太接近2的整数次幂的质数。
在实践中,java和c++的处理风格迥异:
java保留了m为2的幂特点,但通过位运算使得key的所有位都参加了运算,使哈希值更具独特性:

c++则通过设置一个固定的质数表,使得m每次扩容的空间大小为近似原空间2倍的质数:
static const unsigned long __stl_prime_list[__stl_num_primes] =
{
53, 97, 193, 389, 769,
1543, 3079, 6151, 12289, 24593,
49157, 98317, 196613, 393241, 786433,
1572869, 3145739, 6291469, 12582917, 25165843,
50331653, 100663319, 201326611, 402653189, 805306457,
1610612741, 3221225473, 4294967291
};3.2.2 乘法散列法和全域散列法
- 乘法散列法:对哈希表大小
m没有要求,主要思路是floor(m * 小数部分(key * a)),其中a的取值范围在(0, 1),推荐采用黄金分割点(0.618033…)。 - 全域散列法:给散列函数增加
随机性,避免被恶意攻击。具体方式是每次初始化哈希表时,选定一个足够大的质数作为p,随后在特定范围内选取m,a,b作为参数,根据公式hab (key) = ((a × key + b)%p )%m生成一个具体的哈希函数并固定使用。
3.3 处理哈希冲突
哈希冲突不可避免,因此需要设计应对哈希冲突的解决方法:
3.3.1 开放定址法
开放定址法的规则有三种:线性探测,二次探测,双重探测。本质上都是在冲突发生时,通过某种规则找到一个空位置进行存储。这里详细介绍线性探测的方法:
线性探测:从发⽣冲突的位置开始,依次线性向后探测,直到寻找到下⼀个没有存储数据的位置为⽌,如果⾛到哈希表尾,则回绕到哈希表头的位置。

代码实现:
//状态栏
enum state
{
exist,
empty,
delete
};
//key转成整型
template<class k>
struct hashfunc
{
size_t operator()(const k& key)
{
return (size_t)key;
}
};
//(全特化)针对string的版本
template<>
struct hashfunc<string>
{
size_t operator()(const string& key)
{
size_t hash = 0;
for (auto e : key) //bkdr哈希
{
hash *= 131;
hash += e;
}
return hash;
}
};
//哈希扩容(接近2倍的素数)
inline unsigned long __stl_next_prime(unsigned long n)
{
// note: assumes long is at least 32 bits.
static const int __stl_num_primes = 28; //数组大小
static const unsigned long __stl_prime_list[__stl_num_primes] =
{
53, 97, 193, 389, 769,
1543, 3079, 6151, 12289, 24593,
49157, 98317, 196613, 393241, 786433,
1572869, 3145739, 6291469, 12582917, 25165843,
50331653, 100663319, 201326611, 402653189, 805306457,
1610612741, 3221225473, 4294967291
};
//查找区间(左闭右开)
const unsigned long* first = __stl_prime_list; //指针
const unsigned long* last = __stl_prime_list + __stl_num_primes; //指针
const unsigned long* pos = lower_bound(first, last, n); //区间内查找第一个>=n的数
return pos == last ? *(last - 1) : *pos; //注意返回时需要解引用
}
//1.开放定址法
namespace open_address
{
//哈希表中的数据
template<class k, class v>
struct hashdata
{
pair<k, v> _kv;
state _state = empty;
};
//哈希表
template<class k, class v, class hash = hashfunc<k>>
class hashtable
{
private:
vector<hashdata<k, v>> _tables;
size_t _n = 0;
public:
//构造函数
hashtable()
{
_tables.resize(__stl_next_prime(0));
_n = 0;
}
bool insert(const pair<k, v>& kv)
{
//key存在时插入失败
if (find(kv.first))
{
return false;
}
//负载因子为1时扩容
if (_n == _tables.size()) //注意不能是整数相除
{
//1. 扩容
hashtable<k, v, hash> newht;
newht._tables.resize(__stl_next_prime(_tables.size() + 1)); //注意一定要加1,否则存满后会导致死循环
//2. 旧表数据映射到新表
for (auto& e : _tables)
{
if (e._state == exist)
{
//再次走一遍插入(巧妙复用,现代写法)
newht.insert(e._kv);
}
}
//3. 旧表指针指向新表
_tables.swap(newht._tables); //效率高(交换指针)
}
hash hash; //仿函数实例化(或者用hashfunc<k>()(key))
size_t hash0 = hash(kv.first) % _tables.size(); //注意不能对capacity取模,可能导致越界访问
size_t hashi = hash0;
size_t i = 1;
while (_tables[hashi]._state == exist)
{
//线性探测
hashi = hash0 + i;
hashi %= _tables.size(); //防止越界,回绕
i++;
}
_tables[hashi]._kv = kv;
_tables[hashi]._state = exist;
_n++;
return true;
}
//查找
hashdata<k, v>* find(const k& key)
{
//找key(hashi)
hash hash;
size_t hash0 = hash(key) % _tables.size(); //注意不能对capacity取模,可能导致越界访问
size_t hashi = hash0;
size_t i = 1;
while (_tables[hashi]._state != empty)
{
if (_tables[hashi]._state == exist && _tables[hashi]._kv.first == key) //增加对exist的检测
{
return &_tables[hashi]; //返回指针的地址
}
//线性探测
hashi = hash0 + i;
hashi %= _tables.size(); //防止越界,回绕
i++;
}
return nullptr;
}
//删除
bool erase(const k& key)
{
hashdata <k, v>* ret = find(key);
if (!ret)
{
return false;
}
else
{
ret->_state = delete; //直接改状态即可
return true;
}
}
};
}开放定址法实现简单,但在冲突较多时会严重降低查找效率。因此它适用于冲突概率低、负载因子小的场景。
3.3.2 链地址法(推荐)
链地址法又名哈希桶,是最常用、最稳定的哈希冲突解决方式——每个桶不存元素,而是存一个链表头,所有冲突的元素都挂在链表上。链地址法的负载因子没有限制,可以大于1。stl中哈希表的最⼤负载因⼦基本控制在1,⼤于1就扩容,我们下⾯实现也使⽤这个⽅式。
极端场景:如果出现了某个桶特别长的情况,可以使用全域散列法 / 链表优化为红黑树(java8采用)等方式提高查找效率。
代码实现:
//状态栏
enum state
{
exist,
empty,
delete
};
//key转成整型
template<class k>
struct hashfunc
{
size_t operator()(const k& key)
{
return (size_t)key;
}
};
//(全特化)针对string的版本
template<>
struct hashfunc<string>
{
size_t operator()(const string& key)
{
size_t hash = 0;
for (auto e : key) //bkdr哈希
{
hash *= 131;
hash += e;
}
return hash;
}
};
//哈希扩容(接近2倍的素数)
inline unsigned long __stl_next_prime(unsigned long n)
{
// note: assumes long is at least 32 bits.
static const int __stl_num_primes = 28; //数组大小
static const unsigned long __stl_prime_list[__stl_num_primes] =
{
53, 97, 193, 389, 769,
1543, 3079, 6151, 12289, 24593,
49157, 98317, 196613, 393241, 786433,
1572869, 3145739, 6291469, 12582917, 25165843,
50331653, 100663319, 201326611, 402653189, 805306457,
1610612741, 3221225473, 4294967291
};
//查找区间(左闭右开)
const unsigned long* first = __stl_prime_list; //指针
const unsigned long* last = __stl_prime_list + __stl_num_primes; //指针
const unsigned long* pos = lower_bound(first, last, n); //区间内查找第一个>=n的数
return pos == last ? *(last - 1) : *pos; //注意返回时需要解引用
}
//2.链地址法
namespace hash_bucket
{
template<class k, class v>
struct hashnode
{
pair<k, v> _kv;
hashnode<k, v>* _next;
hashnode(const pair<k, v>& kv)
:_kv(kv)
,_next(nullptr)
{ }
};
template<class k, class v, class hash = hashfunc<k>>
class hashtable
{
private:
typedef hashnode<k, v> node;
vector<node*> _tables;
size_t _n;
public:
//构造函数
hashtable()
:_tables(__stl_next_prime(0))
,_n(0)
{}
//拷贝构造(复用insert,但是效率比手写深拷贝低)
hashtable(const hashtable& hst)
:_tables(__stl_next_prime(hst._tables.size() == 0 ? __stl_next_prime(0) : hst._tables.size())) //注意检验哈希表是否为空
,_n(0)
{
for (size_t i = 0; i < hst._tables.size(); i++)
{
node* cur = hst._tables[i];
while (cur)
{
this->insert(cur->_kv);
cur = cur->_next;
}
}
}
void swaphash(hashtable& hst) //传引用
{
_tables.swap(hst._tables);
swap(_n, hst._n);
}
//赋值重载(swaphash现代写法)
hashtable& operator=(hashtable hst)
{
swaphash(hst);
return *this;
}
//vector不会把桶释放,需要单独实现析构函数
~hashtable()
{
for (size_t i = 0; i < _tables.size(); i++)
{
node* cur = _tables[i];
while (cur)
{
node* del = cur;
cur = cur->_next;
delete del;
}
_tables[i] = nullptr;
}
}
//插入
bool insert(const pair<k, v>& kv)
{
hash hash;
//扩容
if (_n == _tables.size())
{
vector<node*> newtables(__stl_next_prime((unsigned long)_tables.size() + 1), nullptr);
for (size_t i = 0; i < _tables.size(); i++)
{
node* cur = _tables[i];
while (cur)
{
node* next = cur->_next;
size_t hashi = hash(cur->_kv.first) % newtables.size();
cur->_next = newtables[hashi];
newtables[hashi] = cur;
cur = next;
}
_tables[i] = nullptr;
}
_tables.swap(newtables);
}
if (find(kv.first)) return false; //不允许冗余
//头插
size_t hashi = hash(kv.first) % _tables.size();
node* newnode = new node(kv);
newnode->_next = _tables[hashi];
_tables[hashi] = newnode;
++_n;
return true;
}
//查找
node* find(const k& key)
{
hash hash;
size_t hashi = hash(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)
{
hash hash;
size_t hashi = hash(key) % _tables.size();
node* cur = _tables[hashi];
if (!cur) return false;
node* prev = nullptr;
while (cur)
{
if (cur->_kv.first == key)
{
//1.删除头节点
if (prev == nullptr)
{
_tables[hashi] = cur->_next;
}
else
{
prev->_next = cur->_next;
}
delete cur;
return true;
}
else
{
prev = cur;
cur = cur->_next;
}
}
return false;
}
};
}3.4 代码整合
hashtable.h
#pragma once
#include<vector>
using namespace std;
//状态栏
enum state
{
exist,
empty,
delete
};
//key转成整型
template<class k>
struct hashfunc
{
size_t operator()(const k& key)
{
return (size_t)key;
}
};
//(全特化)针对string的版本
template<>
struct hashfunc<string>
{
size_t operator()(const string& key)
{
size_t hash = 0;
for (auto e : key) //bkdr哈希
{
hash *= 131;
hash += e;
}
return hash;
}
};
//哈希扩容(接近2倍的素数)
inline unsigned long __stl_next_prime(unsigned long n)
{
// note: assumes long is at least 32 bits.
static const int __stl_num_primes = 28; //数组大小
static const unsigned long __stl_prime_list[__stl_num_primes] =
{
53, 97, 193, 389, 769,
1543, 3079, 6151, 12289, 24593,
49157, 98317, 196613, 393241, 786433,
1572869, 3145739, 6291469, 12582917, 25165843,
50331653, 100663319, 201326611, 402653189, 805306457,
1610612741, 3221225473, 4294967291
};
//查找区间(左闭右开)
const unsigned long* first = __stl_prime_list; //指针
const unsigned long* last = __stl_prime_list + __stl_num_primes; //指针
const unsigned long* pos = lower_bound(first, last, n); //区间内查找第一个>=n的数
return pos == last ? *(last - 1) : *pos; //注意返回时需要解引用
}
//1.开放定址法
namespace open_address
{
//哈希表中的数据
template<class k, class v>
struct hashdata
{
pair<k, v> _kv;
state _state = empty;
};
//哈希表
template<class k, class v, class hash = hashfunc<k>>
class hashtable
{
private:
vector<hashdata<k, v>> _tables;
size_t _n = 0;
public:
//构造函数
hashtable()
{
_tables.resize(__stl_next_prime(0));
_n = 0;
}
bool insert(const pair<k, v>& kv)
{
//key存在时插入失败
if (find(kv.first))
{
return false;
}
//负载因子为1时扩容
if (_n == _tables.size()) //注意不能是整数相除
{
//1. 扩容
hashtable<k, v, hash> newht;
newht._tables.resize(__stl_next_prime(_tables.size() + 1)); //注意一定要加1,否则存满后会导致死循环
//2. 旧表数据映射到新表
for (auto& e : _tables)
{
if (e._state == exist)
{
//再次走一遍插入(巧妙复用,现代写法)
newht.insert(e._kv);
}
}
//3. 旧表指针指向新表
_tables.swap(newht._tables); //效率高(交换指针)
}
hash hash; //仿函数实例化(或者用hashfunc<k>()(key))
size_t hash0 = hash(kv.first) % _tables.size(); //注意不能对capacity取模,可能导致越界访问
size_t hashi = hash0;
size_t i = 1;
while (_tables[hashi]._state == exist)
{
//线性探测
hashi = hash0 + i;
hashi %= _tables.size(); //防止越界,回绕
i++;
}
_tables[hashi]._kv = kv;
_tables[hashi]._state = exist;
_n++;
return true;
}
//查找
hashdata<k, v>* find(const k& key)
{
//找key(hashi)
hash hash;
size_t hash0 = hash(key) % _tables.size(); //注意不能对capacity取模,可能导致越界访问
size_t hashi = hash0;
size_t i = 1;
while (_tables[hashi]._state != empty)
{
if (_tables[hashi]._state == exist && _tables[hashi]._kv.first == key) //增加对exist的检测
{
return &_tables[hashi]; //返回指针的地址
}
//线性探测
hashi = hash0 + i;
hashi %= _tables.size(); //防止越界,回绕
i++;
}
return nullptr;
}
//删除
bool erase(const k& key)
{
hashdata <k, v>* ret = find(key);
if (!ret)
{
return false;
}
else
{
ret->_state = delete; //直接改状态即可
return true;
}
}
};
}
//2.链地址法
namespace hash_bucket
{
template<class k, class v>
struct hashnode
{
pair<k, v> _kv;
hashnode<k, v>* _next;
hashnode(const pair<k, v>& kv)
:_kv(kv)
,_next(nullptr)
{ }
};
template<class k, class v, class hash = hashfunc<k>>
class hashtable
{
private:
typedef hashnode<k, v> node;
vector<node*> _tables;
size_t _n;
public:
//构造函数
hashtable()
:_tables(__stl_next_prime(0))
,_n(0)
{}
//拷贝构造(复用insert,但是效率比手写深拷贝低)
hashtable(const hashtable& hst)
:_tables(__stl_next_prime(hst._tables.size() == 0 ? __stl_next_prime(0) : hst._tables.size())) //注意检验哈希表是否为空
,_n(0)
{
for (size_t i = 0; i < hst._tables.size(); i++)
{
node* cur = hst._tables[i];
while (cur)
{
this->insert(cur->_kv);
cur = cur->_next;
}
}
}
void swaphash(hashtable& hst) //传引用
{
_tables.swap(hst._tables);
swap(_n, hst._n);
}
//赋值重载(swaphash现代写法)
hashtable& operator=(hashtable hst)
{
swaphash(hst);
return *this;
}
//vector不会把桶释放,需要单独实现析构函数
~hashtable()
{
for (size_t i = 0; i < _tables.size(); i++)
{
node* cur = _tables[i];
while (cur)
{
node* del = cur;
cur = cur->_next;
delete del;
}
_tables[i] = nullptr;
}
}
//插入
bool insert(const pair<k, v>& kv)
{
hash hash;
//扩容
if (_n == _tables.size())
{
vector<node*> newtables(__stl_next_prime((unsigned long)_tables.size() + 1), nullptr);
for (size_t i = 0; i < _tables.size(); i++)
{
node* cur = _tables[i];
while (cur)
{
node* next = cur->_next;
size_t hashi = hash(cur->_kv.first) % newtables.size();
cur->_next = newtables[hashi];
newtables[hashi] = cur;
cur = next;
}
_tables[i] = nullptr;
}
_tables.swap(newtables);
}
if (find(kv.first)) return false; //不允许冗余
//头插
size_t hashi = hash(kv.first) % _tables.size();
node* newnode = new node(kv);
newnode->_next = _tables[hashi];
_tables[hashi] = newnode;
++_n;
return true;
}
//查找
node* find(const k& key)
{
hash hash;
size_t hashi = hash(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)
{
hash hash;
size_t hashi = hash(key) % _tables.size();
node* cur = _tables[hashi];
if (!cur) return false;
node* prev = nullptr;
while (cur)
{
if (cur->_kv.first == key)
{
//1.删除头节点
if (prev == nullptr)
{
_tables[hashi] = cur->_next;
}
else
{
prev->_next = cur->_next;
}
delete cur;
return true;
}
else
{
prev = cur;
cur = cur->_next;
}
}
return false;
}
};
}测试代码test_hash.cpp
#include<iostream>
#include "hashtable.h"
using namespace std;
//整型测试(开放定址)
void test_hash1()
{
int a[] = { 19, 100, 63, 56, 13, 80, 71, 12 };
open_address::hashtable<int, int> hash;
for (auto x : a)
{
pair<int, int> p1 = { x, x };
hash.insert(p1);
}
pair<int, int> p2 = { 15, 15 };
hash.insert(p2);
hash.erase(100);
if (hash.find(100))
{
cout << "找到了" << endl;
}
else
{
cout << "没找到" << endl;
}
if (hash.find(19))
{
cout << "找到了" << endl;
}
else
{
cout << "没找到" << endl;
}
}
//字符串测试(开放定址)
void test_hash2()
{
vector<string> v = { "hello", "world", "abcd", "adcb", "acdb", "bcad", "c++", "int", "string", "char", "c", "linux"};
open_address::hashtable<string, int, hashfunc<string>> hash;
for (auto& str : v)
{
hash.insert({ str, hashfunc<string>()(str) });
}
if (hash.find("abcd"))
{
cout << "找到了" << endl;
}
else
{
cout << "没找到" << endl;
}
hash.erase("abcd");
if (hash.find("abcd"))
{
cout << "找到了" << endl;
}
else
{
cout << "没找到" << endl;
}
if (hash.find("linux"))
{
cout << "找到了" << endl;
}
else
{
cout << "没找到" << endl;
}
}
//整型测试(哈希桶)
void test_hash3()
{
int a[] = {19, 30, 5, 36, 13, 20, 21, 12, 24, 96};
hash_bucket::hashtable<int, int> hash3;
for (auto x : a)
{
hash3.insert({ x, x });
}
hash3.insert({ 100, 100 });
hash3.insert({ 101, 101 });
if (hash3.find(19)) cout << "找到了19" << endl;
else cout << "没找到19" << endl;
hash3.erase(19);
if (hash3.find(19)) cout << "找到了19" << endl;
else cout << "没找到19" << endl;
if (hash3.find(100)) cout << "找到了100" << endl;
else cout << "没找到100" << endl;
}
//字符串测试(哈希桶)
void test_hash4()
{
vector<string> v = { "hello", "world", "abcd", "adcb", "acdb", "bcad", "c++", "int", "string", "char", "c", "linux" };
hash_bucket::hashtable<string, size_t, hashfunc<string>> hash4;
for (auto& str : v)
{
hash4.insert({ str, hashfunc<string>()(str) });
}
if (hash4.find("abcd"))
{
cout << "找到了" << endl;
}
else
{
cout << "没找到" << endl;
}
hash4.erase("abcd");
if (hash4.find("abcd"))
{
cout << "找到了" << endl;
}
else
{
cout << "没找到" << endl;
}
if (hash4.find("linux"))
{
cout << "找到了" << endl;
}
else
{
cout << "没找到" << endl;
}
}
//赋值重载和拷贝构造测试
void test_hash5()
{
int a[] = { 19, 30, 5, 36, 13, 20, 21, 12, 24, 96 };
hash_bucket::hashtable<int, int> hash5;
for (auto x : a)
{
hash5.insert({ x, x });
}
//拷贝构造
hash_bucket::hashtable<int, int> hash6 = hash5;
//赋值重载
hash_bucket::hashtable<int, int> hash7;
hash_bucket::hashtable<int, int> hash8;
hash7 = hash8 = hash5;
}
int main()
{
cout << "test_hash1" << endl;
test_hash1();
cout << endl;
cout << "test_hash2" << endl;
test_hash2();
cout << endl;
cout << "test_hash3" << endl;
test_hash3();
cout << endl;
cout << "test_hash4" << endl;
test_hash4();
cout << endl;
cout << "test_hash5" << endl;
test_hash5();
return 0;
}到此这篇关于c++ 哈希表从原理剖析到模拟实现方法的文章就介绍到这了,更多相关c++ 哈希表原理内容请搜索代码网以前的文章或继续浏览下面的相关文章希望大家以后多多支持代码网!
发表评论