文章目录
- 一.开散列
- 1. 开散列的概念
- 2. 开散列结构
- 3. Insert 插入
- 4. Find 查找
- 5. Insert 扩容
- 6. Erase 删除
- 7. 析构函数
- 8. 其它函数接口
- 9. 性能测试
- 二.封装
- 1. 封装内部结构
- 2. 实现接口
- 三.代器器
- 1. 迭代器的定义
- 2. 常用接口
- 3. 迭代器++
- 4. begin()、end()
- 5. find的改动
- 6. 下标访问[ ]重载
- 四.源码与测试用例
- 1. 底层HashTable
- 2. unordered_set/map
- 3. 测试用例
前言: 上一篇博客我们使用闭散列的方式实现了Hash,其实在STL库unordered_set、unordered_map中底层是开散列的方式实现的Hash,所以,本篇博客就再使用开散列的方式实现Hash,并将unordered_set、unordered_map进行封装。
一.开散列
1. 开散列的概念
开散列法又称链地址法(拉链法、哈希桶),首先对关键码集合用散列函数计算散列地址,具有相同地址的关键码归于同一子集合,每一个字集合称为一个桶,各个桶中的元素通过一个单链表链接起来,各链表的头结点存储在哈希表中。
2. 开散列结构
首先我们要使用vector来存储每个链表的节点,然后每个节点中有数据域和指针next域。然后我们可以将HashNode的构造函数写一下,使用pair类型构造处一个HashNode。
template <class K, class V>
struct HashNode
{HashNode(const pair<K, V>& kv):_kv(kv), _next(nullptr){}pair<K, V> _kv;HashNode<K, V>* _next;
};template <class K, class V, class Hash = HashFunc<K>>
class HashTable
{
public:typedef HashNode<K, V> Node;
private:vector<Node*> _table;size_t _size = 0;
};
3. Insert 插入
首先我们实现插入的主逻辑,然后对其进行逐步优化。
我们根据 kv 创建一个节点,然后根据仿函数进行取模求出映射位置,然后进行链表的头插。
bool Insert(const pair<K, V>& kv)
{ Hash hash;size_t hashi = hash(kv.first) % _table.size();//头插Node* newNode = new Node(kv);newNode->_next = _table[hashi];_table[hashi] = newNode;++_size;return true;
}
像哈希表中插入数据首先要保证数据的唯一性,所以我们要先进行去重处理,此时我们顺带实现Find函数。
4. Find 查找
根据key值求出映射位置,如果该位置不为空,则进行链表的遍历,如果找到key值,则返回cur节点,如果找不到则向后遍历,直到cur为空。
Node* Find(const K& key)
{if (_table.size() == 0) return nullptr;Hash hash;size_t hashi = hash(key) % _table.size();//向桶中进行查找 Node* cur = _table[hashi];while (cur){if (cur->_kv.first == key){return cur;}cur = cur->_next;}return nullptr;
}
5. Insert 扩容
插入的主逻辑实现了,去重判断也实现了,接下来就是表的扩容。
如果哈希表的大小为0或达到了哈希的负载因子,则要进行扩容。
我们看一下STL库中负载因子控制的多少:
STL库中设计的负载因子为:当表中插入的元素个数>哈希表的大小,即负载因为为1的时候进行扩容,将表的大小扩容到 next_size.
扩容的挪动数据要注意,因为开散列的每个桶上的数据个数不同。进行扩容后,桶中每个元素都可能映射到不同的新位置处,所以我们不能像闭散列那样复用Insert,要重新将结点链接到新表中。
挪动时要让原表中的结点一个一个链接到新表中:
//扩容 --- 如果插入的数据等于表的大小
if (_size == _table.size())
{size_t newSize = _table.size() == 0 ? 10 : _table.size() * 2;vector<Node*> newTable;newTable.resize(newSize, nullptr);//将旧表中的节点移动映射到新表Hash hash;for (size_t i = 0; i < _table.size(); i++){Node* cur = _table[i];while (cur){Node* next = cur->_next;size_t hashi = hash(cur->_kv.first) % _table.size();cur->_next = newTable[hashi];newTable[hashi] = cur;cur = next;}//将旧表i位置处结点清空_table[i] = nullptr;}_table.swap(newTable);
}
发现源码中进行扩容时调用了next_size
函数,扩容直接将size乘以2不就行了吗,为什么要特殊计算 size ?
因为hash表的大小最好是素数,如果是素数,映射的结果冲突几率就小,因为非素数因子多,进行映射后相同位置冲突大。将hash表的大小设计为素数后,其实就可以做到hash表中个别桶的冲突次数过多而过分的大。
详细可以看这篇文章:算法分析:哈希表的大小为何是素数
现在我们也添加这个功能:
库中使用lower_bound(返回第一个大于等于n的下标)/upper_bound(返回第一个大于n的下标),其实直接使用for循环遍历就行了.
inline size_t __stl_next_prime(size_t n)
{static const size_t __stl_num_primes = 28;static const size_t __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};//取下一次扩容的大小:for (size_t i = 0; i < __stl_next_prime; i++){if (__stl_prime_list[i] > n)return __stl_prime_list[i];}return (size_t)-1;
}
6. Erase 删除
虽然我们实现了Find函数,但是单单使用Find是无法完成删除功能的。
例如下面这种情况,单链表删除中间结点我们还需要知道 prev 结点。
bool Erase(const K& key)
{if (_table.size() == 0) return false;Hash hash;size_t hashi = hash(key) % _table.size();Node* pre = nullptr;Node* cur = _table[hashi];while (cur){if (cur->_kv.first == hash(key)){//如果删除的是链中第一个元素 --- 即头删if (pre == nullptr){_table[hashi] = cur->_next;}//2.中间删除else{pre->_next = cur->_next;}delete cur;--_size;return true;}pre = cur;cur = cur->_next;}return false;
}
7. 析构函数
注意了,当哈希表生命周期结束后会调用析构函数,我们使用的vector会自动释放表中的内容,可是vector中存放的是链表,我们释放时还要对桶(链表)进行释放,所以我们要手动写一个析构函数。
~HashTable()
{for (size_t i = 0; i < _table.size(); ++i){Node* cur = _table[i];while (cur){Node* next = cur->_next;delete cur;cur = next;}_table[i] = nullptr;}
}
8. 其它函数接口
//表的长度
size_t BucketSize()
{return _table.size();
}
//数据个数
size_t Size()
{return _size;
}
//桶的数量
size_t BucketNum()
{size_t Num = 0;for (size_t i = 0; i < BucketSize(); i++){if (_table[i]) Num++;}return Num;
}
//最长的桶
size_t MaxBucketLenth()
{size_t Max_len = 0;size_t temp = 0;for (size_t i = 0; i < BucketSize(); i++){if (_table[i]){size_t len = 1;Node* cur = _table[i]->_next;while (cur){len++;cur = cur->_next;}if (len > Max_len){Max_len = len;temp = i;}}}printf("Max_len_i:[%u]\n", temp);return Max_len;
}
9. 性能测试
void TestHT()
{int n = 18000000;vector<int> v;v.reserve(n);srand((unsigned int)time(0));for (int i = 0; i < n; ++i){v.push_back(rand()+i); // 重复少//v.push_back(rand()); // 重复多}size_t begin1 = clock();HashTable<int, int> ht;for (auto e : v){ht.Insert(make_pair(e, e));}size_t end1 = clock();cout << "数据个数:" << ht.Size() << endl;cout << "表的长度:" << ht.BucketSize() << endl;cout << "桶的个数:" << ht.BucketNum() << endl;cout << "平均每个桶的长度:" << (double)ht.Size() / (double)ht.BucketNum() << endl;cout << "最长的桶的长度:" << ht.MaxBucketLenth() << endl;cout << "负载因子:" << (double)ht.Size() / (double)ht.BucketSize() << endl;
}
发现,将哈希表的大小设置为素数后,即使负载因子到了0.9,最长的桶也不过才是 2。所以hash表的查找效率为O(1)。
接下来我们对比红黑树和hash表其查找效率(查找1千万个数据)
哈希表插入效率较低,是因为扩容挪动数据非常消耗时间。
接下来我们使用set、onordered_set(底层对应的就是红黑树和hash表),向其中插入1千万的随机数,对比其性能,并对onordered_set进行直接插入和提前扩容再进行插入的效率对比。
测试代码如下:
void test_op()
{int n = 10000000; //1千万个数据vector<int> v;v.reserve(n);srand((unsigned int)time(0));for (int i = 0; i < n; ++i){//v.push_back(i);v.push_back(rand()^ 1311 * 144+i);}size_t begin1 = clock();set<int> s;for (auto e : v){s.insert(e);}size_t end1 = clock();size_t begin2 = clock();unordered_set<int> us;us.reserve(n);for (auto e : v){us.insert(e);}size_t end2 = clock();cout << "有效数据个数:" << s.size() << endl;cout << "\nInsert 插入:" << endl;cout << "set : " << end1 - begin1 << endl;cout << "unordered_set : " << end2 - begin2 << endl;size_t begin3 = clock();for (auto e : v){s.find(e);}size_t end3 = clock();size_t begin4 = clock();for (auto e : v){us.find(e);}size_t end4 = clock();cout << "\nFind 查找:" << endl;cout << "set :" << end3 - begin3 << endl;cout << "unordered_set :" << end4 - begin4 << endl;size_t begin5 = clock();for (auto e : v){s.erase(e);}size_t end5 = clock();size_t begin6 = clock();for (auto e : v){us.erase(e);}size_t end6 = clock();cout << "\nErase 删除:" << endl;cout << "set erase:" << end5 - begin5 << endl;cout << "unordered_set erase:" << end6 - begin6 << endl;
}
以上就是我们hash开散列的基本实现了,实现了以上功能我们就可以封装unordered_map/unordered_set了。
二.封装
1. 封装内部结构
首先是改变HashTable中每个结点存储的数据类型,如unordered_set中存放的是key,unordered_map中存放的是pair类型,所以我们将结点中存储的类型改为T,如果是set,T对应就是key,如果是map,那T就对应pair结构。
template <class T>
struct HashNode
{HashNode(const T& data):_data(data), _next(nullptr){}T _data;HashNode<T>* _next;
};
所以,Insert插入的类型也应改为T模板类型,在使用到类型中的值时,使用仿函数取出该比较的数据。
然后我们就来编写unordered_set(map)类
unordered中底层就是调用我们写的HashTable,所以直接使用HashTable定义成员变量,并传入模板参数。(以下简写的set、map都对应的Hash方法实现的unordered_set(map))
注意,因为set是Key模型,设置一个模板参数即可;而map是KV模型,需要设置两个模板参数对应pair的中的两个数据类型。所以,在底层我们统统传入HashTalbe两个模板参数,并以第二个模板参数为准决定底层存储什么类型,如果是set,就使用仿函数取出key,如果是map就使用仿函数取出pair.first。
所以,在传入参数前我们要先编写好仿函数set(map)KeyOfT,以便于底层取出数据。
//**** set *********
template<class K, class Hash = HashFunc<K>>
class unordered_set
{
public:
private:struct setKeyOfT{const K& operator()(const K& key){return key;}};//两个模板参数都传入KHashTable<K, K, Hash, setKeyOfT> _ht;
};
//**** map *********
template<class K, class V, class Hash = HashFunc<K>>
class unordered_map
{
public:
private://让HashTable取出pair中的K --- 内部类struct mapKeyOfT{const K& operator()(const pair<K, V>& kv){return kv.first;}};HashTable<K, pair<K, V>, Hash, mapKeyOfT> _ht;
};
2. 实现接口
接下来就是为我们封装的map、set 设计成员函数,其实我们只是封装了一层,本质还是调用HashTable中的Insert、Erase等函数。
// ****** set ********
bool insert(const K& kv)
{return _ht.Insert(kv);
}bool erase(const K& kv)
{return _ht.Erase(kv);
}// ****** map ********
bool insert(const pair<K, V>& kv)
{return _ht.Insert(kv);
}
bool erase(const K& k)
{return _ht.Erase(k);
}
注意,Insert、Erase的底层中,涉及到key值操作的,我们要进行使用两层仿函数进行取值。
三.代器器
1. 迭代器的定义
在HashTable中有迭代器的接口(begin()、end()),而迭代器中也会使用到HashTable的结构,所以,在实现迭代器之前我们要先进行HashTable的声明(注意:模板类的声明要加上模板参数一起声明)。
我们来看看源码中迭代器是如何定义的
接下来是我们的定义:
//前置声明
template <class K, class T, class Hash, class keyOfT>
class HashTable;template<class K, class T, class Hash, class keyOfT>
class __Hash_Iteartor
{
public:typedef HashNode<T> Node;typedef HashTable<K, T, Hash, keyOfT> HT;typedef __Hash_Iteartor<K, T, Hash, keyOfT> Self;__Hash_Iteartor(Node* node, HT* pht):_node(node), _pht(pht){}__Hash_Iteartor(){}
private://成员变量Node* _node; //指向结点HT* _pht; //指向当前表
};
2. 常用接口
接下来实现一些常用的接口:
T& operator*()
{return _node->_data;
}T* operator->()
{return &_node->_data;
}
bool operator!=(const Self& self)
{return _node != self._node;
}
bool operator==(const Self& self)
{return _node == self._node;
}
3. 迭代器++
STL中迭代器++的实现:
思路如下:
- 判断_node的_next是否存在存在结点,如果存在直接让_node = _node->_next即可
- 如果不存在结点,则当前桶遍历结束,要寻找下一个有数据的桶。
- 根据_node中的data域求出映射位置,然后从映射位置向后遍历哈希表,直到talbe[i]处有数据,有数据则跳出循环
- 当 i 等于哈希表的大小,则表示不存在下一个数据,则将_node赋值为nullptr
- 返回*this,即返回当前对象。
Self& operator++()
{//在当前桶中进行++if (_node->_next){_node = _node->_next;}else //找下一个有效的桶{Hash hash;keyOfT kft;size_t i = hash(kft(_node->_data)) % _pht->_table.size();for (i += 1; i < _pht->_table.size(); i++){if (_pht->_table[i]){_node = _pht->_table[i];break;}}//如果不存在有数据的桶if (i == _pht->_table.size())_node = nullptr;}return *this;
}
注意,此时我们使用了哈希表,具体访问了其中的元素,所以我们要让迭代器作为HashTable的友元类(也要带上模板参数进行声明噢)。
4. begin()、end()
begin就是返回HashTable中第一个存储了数据的桶。如果表中没有存储数据,直接返回end(),而end()迭代器中的_node为nullptr构造的。
typedef __Hash_Iteartor<K, T, Hash, keyOfT> iterator;iteratorbegin()
{for (size_t i = 0; i < _table.size(); i++){if (_table[i])return iterator(_table[i], this);}return end();
}
iterator end()
{return iterator(nullptr, this);
}
5. find的改动
find中,我们返回是直接返回迭代器,在return的地方使用匿名对象进行返回即可。
6. 下标访问[ ]重载
如果要实现map中的下标访问操作符重载,我们要对Insert进行改造,让其返回值为pair结构,其中first为迭代器,second为bool类型,表示插入成功与否(虽然不改变也能实现)。
Insert的改动完成后,接下来就可以在map中添加 [] 下标访问操作符重载了。
V& operator[](const K& key){pair<iterator, bool> ret = _ht.Insert(make_pair(key, V()));return ret.first->second;}
面试题:
一个类型K去做 set 和 unordered_set 的模板参数有什么要求?
set :
set要求支持能进行小于号比较,或者显示提供比较的仿函数unordered_set:
- 要求K类型对象能转化为整形取模,或提供能装化为整形的仿函数
- K类型对象要支持等于比较,或提供等于比较的仿函数 (set有小于,就可以通过左小右大的方式找到数据;而unordered_set会出现冲突,使用key值只能找到映射的桶,遍历桶的时候,就需要进行等于比较了)
四.源码与测试用例
1. 底层HashTable
template<class K>
struct HashFunc
{size_t operator()(const K& key){return (size_t)key;}
};template<>
struct HashFunc<string>
{size_t operator()(const string& key){size_t val = 0;for (auto ch : key)val = val * 131 + ch;return val;}
};template <class T>
struct HashNode
{HashNode(const T& data):_data(data), _next(nullptr){}T _data;HashNode<T>* _next;
};// 对哈希表进行前置声明
template <class K, class T, class Hash, class keyOfT>
class HashTable;template<class K, class T, class Hash, class keyOfT>
class __Hash_Iteartor
{
public:typedef HashNode<T> Node;typedef HashTable<K, T, Hash, keyOfT> HT;typedef __Hash_Iteartor<K, T, Hash, keyOfT> Self;__Hash_Iteartor(Node* node, HT* pht):_node(node), _pht(pht){}__Hash_Iteartor():_node(nullptr), _pht(nullptr){}T& operator*(){return _node->_data;}T* operator->(){return &_node->_data;}Self& operator++(){//在当前桶中进行++if (_node->_next){_node = _node->_next;}else //找下一个有效的桶{Hash hash;keyOfT kft;size_t i = hash(kft(_node->_data)) % _pht->_table.size();for (i += 1; i < _pht->_table.size(); i++){if (_pht->_table[i]){_node = _pht->_table[i];break;}}//如果不存在有数据的桶if (i == _pht->_table.size())_node = nullptr;}return *this;}bool operator!=(const Self& self){return _node != self._node;}bool operator==(const Self& self){return _node == self._node;}private://成员Node* _node; //指向结点HT* _pht; //指向当前表};template <class K, class T, class Hash, class keyOfT>
class HashTable
{
public:typedef HashNode<T> Node;//将迭代器设为友元template<class K, class T, class Hash, class keyOfT>friend class __Hash_Iteartor;typedef __Hash_Iteartor<K, T, Hash, keyOfT> iterator;iterator begin(){for (size_t i = 0; i < _table.size(); i++){if (_table[i])return iterator(_table[i], this);}return end();}iterator end(){return iterator(nullptr, this);}//析构要进行特殊处理,遍历整个表,再删除桶中的数据。~HashTable(){for (size_t i = 0; i < _table.size(); ++i){Node* cur = _table[i];while (cur){Node* next = cur->_next;delete cur;cur = next;}_table[i] = nullptr;}}inline size_t __stl_next_prime(size_t n){static const size_t __stl_num_primes = 28;static const size_t __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};//取下一次扩容的大小:for (size_t i = 0; i < __stl_num_primes; i++){if (__stl_prime_list[i] > n)return __stl_prime_list[i];}return (size_t)-1;}pair<iterator, bool> Insert(const T& data){Hash hash;keyOfT koft;//去重iterator ret = Find(koft(data));if (ret != end()){return make_pair(ret, false);}//扩容 --- 如果插入的数据等于表的大小if (_size == _table.size()){//size_t newSize = _table.size() == 0 ? 10 : _table.size() * 2;vector<Node*> newTable;size_t newSize = __stl_next_prime(_table.size());newTable.resize(newSize, nullptr);//将旧表中的节点移动映射到新表for (size_t i = 0; i < _table.size(); i++){Node* cur = _table[i];while (cur){Node* next = cur->_next;size_t hashi = hash(koft(cur->_data)) % newSize;cur->_next = newTable[hashi];newTable[hashi] = cur;cur = next;}//将旧表i位置处结点清空_table[i] = nullptr;}_table.swap(newTable);}size_t hashi = hash(koft(data)) % _table.size();//头插Node* newNode = new Node(data);newNode->_next = _table[hashi];_table[hashi] = newNode;++_size;return make_pair(iterator(newNode, this), true);}iterator Find(const K& key){if (_table.size() == 0) return end();Hash hash;keyOfT koft;size_t hashi = hash(key) % _table.size();//向桶中进行查找 Node* cur = _table[hashi];while (cur){if (koft(cur->_data) == key){return iterator(cur, this);}cur = cur->_next;}return end();}//单链表不能直接找到该节点并删除bool Erase(const K& key){if (_table.size() == 0) return false;Hash hash;keyOfT koft;size_t hashi = hash(key) % _table.size();Node* pre = nullptr;Node* cur = _table[hashi];while (cur){if (koft(cur->_data) == hash(key)){//如果删除的是链中第一个元素 --- 即头删if (pre == nullptr){_table[hashi] = cur->_next;}//2.中间删除else{pre->_next = cur->_next;}delete cur;--_size;return true;}pre = cur;cur = cur->_next;}return false;}//表的长度size_t BucketSize(){return _table.size();}//数据个数size_t Size(){return _size;}//桶的数量size_t BucketNum(){size_t Num = 0;for (size_t i = 0; i < BucketSize(); i++){if (_table[i]) Num++;}return Num;}//最长的桶size_t MaxBucketLenth(){size_t Max_len = 0;size_t temp = 0;for (size_t i = 0; i < BucketSize(); i++){if (_table[i]){size_t len = 1;Node* cur = _table[i]->_next;while (cur){len++;cur = cur->_next;}if (len > Max_len){Max_len = len;temp = i;}}}printf("Max_len_i:[%u]\n", temp);return Max_len;}void Print_map(){cout << "Print_map:" << endl;for (int i = 0; i < _table.size(); i++){Node* cur = _table[i];while (cur){cout << "i:" << i << " [" << cur->_data.first << " " << cur->_data.second << "] " << endl;cur = cur->_next;}}}void Print_set(){cout << "Print_set:" << endl;for (int i = 0; i < _table.size(); i++){Node* cur = _table[i];while (cur){cout << "i:" << i << " [" << cur->_data << "] " << endl;cur = cur->_next;}}}private:vector<Node*> _table;size_t _size = 0;
};
2. unordered_set/map
unordered_set:
template<class K, class Hash = HashFunc<K>>
class unordered_set
{
public:struct setKeyOfT;typedef typename dianxia::HashTable<K, K, Hash, setKeyOfT>::iterator iterator;iterator begin(){return _ht.begin();}iterator end(){return _ht.end();}pair<iterator, bool> insert(const K& kv){return _ht.Insert(kv);}bool erase(const K& kv){return _ht.Erase(kv);}void print(){_ht.Print_set();}private:struct setKeyOfT{const K& operator()(const K& key){return key;}};HashTable<K, K, Hash, setKeyOfT> _ht;
};
unordered_map:
template<class K, class V, class Hash = HashFunc<K>>
class unordered_map
{
public:struct mapKeyOfT;typedef typename dianxia::HashTable<K, pair<K, V>, Hash, mapKeyOfT>::iterator iterator;iterator begin(){return _ht.begin();}iterator end(){return _ht.end();}pair<iterator, bool> insert(const pair<K, V>& kv){return _ht.Insert(kv);}bool erase(const K& k){return _ht.Erase(k);}V& operator[](const K& key){pair<iterator, bool> ret = _ht.Insert(make_pair(key, V()));return ret.first->second;}void print(){_ht.Print_map();}private://取出pair中的K值 --- 内部类struct mapKeyOfT{const K& operator()(const pair<K, V>& kv){return kv.first;}};HashTable<K, pair<K, V>, Hash, mapKeyOfT> _ht;
};
3. 测试用例
封装测试:
void test_unordered01()
{Brant::unordered_map<int, int> mp1;mp1.insert({ 1,1 });mp1.insert({ 54,54 });mp1.insert({ 2,2 });mp1.insert({ 3,3 });mp1.insert({ 4,4 });mp1.insert({ 6,6 });mp1.insert({ 6,6 });mp1.print();cout << "Erase:---------------" << endl;mp1.erase(1);mp1.erase(54);mp1.print();cout << endl << "--------------------------------------" << endl;Brant::unordered_set<int> st1;st1.insert(1);st1.insert(54);st1.insert(2);st1.insert(3);st1.insert(4);st1.insert(6);st1.insert(6);st1.print();cout << "Erase:---------------" << endl;st1.erase(1);st1.erase(54);st1.print();
}
迭代器测试:
void test_iterator01()
{Brant::unordered_map<string, string> dict;dict.insert({ "sort","排序" });dict.insert({ "left","左边" });dict.insert({ "right","右边" });dict.insert({ "string","字符串" });Brant::unordered_map<string, string>::iterator it = dict.begin();while (it != dict.end()){cout << it->first << " : " << it->second << endl;++it;}cout << endl;
}void test_iterator02()
{Brant::unordered_map<string, int> countMap;string arr[] = { "苹果","西瓜","菠萝","草莓","菠萝","草莓" ,"菠萝","草莓", "西瓜", "菠萝", "草莓", "西瓜", "菠萝", "草莓","苹果" };for (auto e : arr){countMap[e]++;}for (auto kv : countMap){cout << kv.first << " " << kv.second << endl;}
}
本文到此结束,码文不易,还请多多支持哦!!!