从C语言到C++_31(unordered_set和unordered_map介绍+哈希桶封装)

news2024/11/25 22:53:11

目录

1. unordered_set和unordered_map

1.1 unordered_map

1.2 unordered_set

1.3 unordered系列写OJ题

961. 在长度 2N 的数组中找出重复 N 次的元素 - 力扣(LeetCode)

349. 两个数组的交集 - 力扣(LeetCode)

217. 存在重复元素 - 力扣(LeetCode)

884. 两句话中的不常见单词 - 力扣(LeetCode)

2. 实现unordered_set和unordered_map

2.1 哈希桶的迭代器

2.2 封装unordered_set和unordered_map

完整unordered_map.h

完整unordered_set.h

2.3 修改哈希桶

完整HashTable.h:

Test.cpp:

3. 题外话+笔试选择题

本篇完。


1. unordered_set和unordered_map

 在C++98中,STL提供了底层为红黑树结构的一系列关联式容器,在查询时效率可达到(logN),

即最差情况下需要比较红黑树的高度次,当树中的节点非常多时,查询效率也不理想。最好的查询是,进行很少的比较次数就能够将元素找到,因此在C++11中,STL又提供了4个unordered系列的关联式容器,这四个容器与红黑树结构的关联式容器使用方式基本类似,只是其底层结构不同,其底层结构是前一篇讲的哈希结构,本文中只对unordered_set和unordered_map进行介绍,

unordered_multiset和unordered_multimap可查看文档介绍。

unordered系列和我们前面学习的map和set几乎一模一样,只是多了前面的unordered。

正如它的名字一样,unordered系列和map/set比起来,unordered系列打印出来的数据是无序的。

1.1 unordered_map

1. unordered_map是存储<key, value>键值对的关联式容器,其允许通过keys快速的索引到与其对应的value。
2. 在unordered_map中,键值通常用于惟一地标识元素,而映射值是一个对象,其内容与此
键关联。键和映射值的类型可能不同。
3. 在内部, unordered_map没有对<kye, value>按照任何特定的顺序排序, 为了能在常数范围内找到key所对应的value,unordered_map将相同哈希值的键值对放在相同的桶中。
4. unordered_map容器通过key访问单个元素要比map快,但它通常在遍历元素子集的范围迭代方面效率较低。
5. unordered_maps实现了直接访问操作符(operator[]),它允许使用key作为参数直接访问value。
6. 它的迭代器至少是前向迭代器。

常用接口函数:可以参考map的函数使用,还有一些关于哈希的接口后面再讲解

1.2 unordered_set

1、无序集是一种容器,它以不特定的顺序存储惟一的元素,并允许根据元素的值快速检索单个元素。
2、在unordered_set中,元素的值同时是唯一标识它的键。键是不可变的,只可增删,不可修改
3、在内部,unordered_set中的元素没有按照任何特定的顺序排序,而是根据它们的散列值组织成桶,从而允许通过它们的值直接快速访问单个元素(平均时间复杂度为常数)。
4、unordered_set容器比set容器更快地通过它们的键访问单个元素,尽管它们在元素子集的范围迭代中通常效率较低。
5、容器中的迭代器至少是前向迭代器。

unordered_set 容器提供了和 unordered_map 相似的能力,

但 unordered_set 可以用保存的元素作为它们自己的键。

T 类型的对象在容器中的位置由它们的哈希值决定,因而需要定义一个 Hash< T > 函数。

基本类型可以省去Hash< T >方法。不能存放重复元素。

可指定buckets个数,可进行初始化,也可后期插入元素

常用接口函数:可以参考set的函数使用,还有一些关于哈希的接口后面再讲解

1.3 unordered系列写OJ题

(困难题我唯唯诺诺,简单题我多次重拳出击)

961. 在长度 2N 的数组中找出重复 N 次的元素 - 力扣(LeetCode)

难度简单

给你一个整数数组 nums ,该数组具有以下属性:

  • nums.length == 2 * n.
  • nums 包含 n + 1 个 不同的 元素
  • nums 中恰有一个元素重复 n 次

找出并返回重复了 n 次的那个元素。

示例 1:

输入:nums = [1,2,3,3]
输出:3

示例 2:

输入:nums = [2,1,2,5,3,2]
输出:2

示例 3:

输入:nums = [5,1,5,2,5,3,5,4]
输出:5

提示:

  • 2 <= n <= 5000
  • nums.length == 2 * n
  • 0 <= nums[i] <= 10^4
  • nums 由 n + 1 个 不同的 元素组成,且其中一个元素恰好重复 n 次
class Solution {
public:
    int repeatedNTimes(vector<int>& nums) {

    }
};

解析代码:(和map一样用)(以下代码改成map也能过,OJ平均效率低一些,后面就知道了)

class Solution {
public:
    int repeatedNTimes(vector<int>& nums) {
        unordered_map<int,int> countMap;
        for(const auto& e : nums)
        {
            countMap[e]++;
        }

        unordered_map<int,int> Map;
        for(const auto& kv : countMap)
        {
            if(kv.second == nums.size() / 2)
            {
                return kv.first;
            }
        }
        return -1; // 不会走到这,顺便返回一个值
    }
};

349. 两个数组的交集 - 力扣(LeetCode)

难度简单

给定两个数组 nums1 和 nums2 ,返回 它们的交集 。输出结果中的每个元素一定是 唯一 的。我们可以 不考虑输出结果的顺序 。

示例 1:

输入:nums1 = [1,2,2,1], nums2 = [2,2]
输出:[2]

示例 2:

输入:nums1 = [4,9,5], nums2 = [9,4,9,8,4]
输出:[9,4]
解释:[4,9] 也是可通过的

提示:

  • 1 <= nums1.length, nums2.length <= 1000
  • 0 <= nums1[i], nums2[i] <= 1000
class Solution {
public:
    vector<int> intersection(vector<int>& nums1, vector<int>& nums2) {

    }
};

解析代码:(这题在从C语言到C++_26讲过了)(当时用set排序了,现在不排序写写)

当时是力扣题解2,现在是力扣题解1:使用哈希集合存储元素,则可以在O(1)的时间内判断一个元素是否在集合中,从而降低时间复杂度。首先使用两个集合分别存储两个数组中的元素,然后遍历较小的集合(顺便遍历一个也行,就是效率低点),判断其中的每个元素是否在另一个集合中,如果元素也在另一个集合中,则将该元素添加到返回值。

该方法的时间复杂度可以降低到O(m+n)。

class Solution {
public:
    vector<int> intersection(vector<int>& nums1, vector<int>& nums2) {
        unordered_set s1(nums1.begin(),nums1.end()); // 去重
        unordered_set s2(nums2.begin(),nums2.end());

        vector<int> retV;
        if(s1.size() <= s2.size())
        {
            for(const auto& e : s1)
            {
                if(s2.find(e) != s2.end())
                {
                    retV.push_back(e);
                }
            }
        }
        else
        {
            for(const auto& e : s2)
            {
                if(s1.find(e) != s1.end())
                {
                    retV.push_back(e);
                }
            }
        }
        return retV;
    }
};

217. 存在重复元素 - 力扣(LeetCode)

难度简单

给你一个整数数组 nums 。如果任一值在数组中出现 至少两次 ,返回 true ;如果数组中每个元素互不相同,返回 false 。

示例 1:

输入:nums = [1,2,3,1]
输出:true

示例 2:

输入:nums = [1,2,3,4]
输出:false

示例 3:

输入:nums = [1,1,1,3,3,4,3,2,4,2]
输出:true

提示:

  • 1 <= nums.length <= 10^5
  • -10^9 <= nums[i] <= 10^9
class Solution {
public:
    bool containsDuplicate(vector<int>& nums) {

    }
};

解析代码:(看看返回值,前两个和模拟实现set的一样)

class Solution {
public:
    bool containsDuplicate(vector<int>& nums) {
        unordered_set<int> s;
        for(const auto& e : nums)
        {
            if(s.insert(e).second == false)
            {
                return true;
            }
        }
        return false;
    }
};

884. 两句话中的不常见单词 - 力扣(LeetCode)

难度简单

句子 是一串由空格分隔的单词。每个 单词 仅由小写字母组成。

如果某个单词在其中一个句子中恰好出现一次,在另一个句子中却 没有出现 ,那么这个单词就是 不常见的 

给你两个 句子 s1 和 s2 ,返回所有 不常用单词 的列表。返回列表中单词可以按 任意顺序 组织。

示例 1:

输入:s1 = "this apple is sweet", s2 = "this apple is sour"
输出:["sweet","sour"]

示例 2:

输入:s1 = "apple apple", s2 = "banana"
输出:["banana"]

提示:

  • 1 <= s1.length, s2.length <= 200
  • s1 和 s2 由小写英文字母和空格组成
  • s1 和 s2 都不含前导或尾随空格
  • s1 和 s2 中的所有单词间均由单个空格分隔
class Solution {
public:
    vector<string> uncommonFromSentences(string s1, string s2) {

    }
};

解析代码:(等价于:在两个句子中一共只出现一次的单词。)

大家可以百度stringstream类用法,这里讲一个小技巧
可以将字符串中每个单词按空格隔开。

class Solution {
public:
    vector<string> uncommonFromSentences(string s1, string s2) {
        unordered_map<string, int> m;
        vector<string> retV;

        stringstream a, b; // 创建流对象
        string s;
        a << s1;  // 向流中传值
        b << s2;

        while (a >> s)
        {
            m[s]++;  //流向s中写入值,并且空格会自断开
            //cout << s << "+";
        }
        while (b >> s)
        {
            m[s]++;
        }
        for (const auto& m : m)
        {
            if (m.second == 1)
            {
                retV.push_back(m.first); //只需要看出现次数是1的单词
            }
        }
        return retV;
    }
};

如果解开注释:

2. 实现unordered_set和unordered_map

这里用我们上一篇写的开散列哈希桶的代码,闭散列不用就删掉,去掉命名空间复制一份过来:

#pragma once

#include <iostream>
#include <vector>
using namespace std;

template<class K, class V>
struct HashNode
{
	pair<K, V> _kv;
	HashNode* _next; // 不用存状态栏了,存下一个结点指针

	HashNode(const pair<K, V>& kv)
		:_kv(kv)
		, _next(nullptr)
	{}
};

template<class K>
struct HashFunc // 可以把闭散列的HashFunc放在外面直接用,但是这就不放了
{
	size_t operator()(const K& key)
	{
		return (size_t)key; // 负数,浮点数,指针等可以直接转,string不行
	}
};

template<>
struct HashFunc<string> // 上面的特化
{
	size_t operator()(const string& key)
	{
		size_t val = 0;
		for (const auto& ch : key)
		{
			val *= 131;
			val += ch;
		}

		return val;
	}
};

template<class K, class V, class Hash = HashFunc<K>>
class HashTable
{
public:
	typedef HashNode<K, V> Node;

	~HashTable()
	{
		for (size_t i = 0; i < _tables.size(); ++i)
		{
			Node* cur = _tables[i];
			while (cur)
			{
				Node* next = cur->_next;
				delete cur;
				cur = next;
			}
			_tables[i] = nullptr;
		}
	}

	Node* Find(const K& key)
	{
		if (_tables.size() == 0)
		{
			return nullptr;
		}

		Hash hs;
		size_t hashi = hs(key) % _tables.size();
		Node* cur = _tables[hashi];
		while (cur)
		{
			if (cur->_kv.first == key)
			{
				return cur;
			}
			cur = cur->_next;
		}
		return 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 -1; // 不会走到这,随便返回一个值
	}

	bool Insert(const pair<K, V>& kv)
	{
		if (Find(kv.first))
		{
			return false;
		}

		Hash hs;
		if (_size == _tables.size()) // 负载因子到1就扩容
		{
			//size_t newSize = _tables.size() == 0 ? 10 : _tables.size() * 2;
			vector<Node*> newTables;
			//newTables.resize(newSize, nullptr);
			newTables.resize(__stl_next_prime(_tables.size()), nullptr); //取素数,前两注释改成这一条

			for (size_t i = 0; i < _tables.size(); ++i) // 旧表中节点移动映射新表
			{
				Node* cur = _tables[i];
				while (cur)
				{
					Node* next = cur->_next;

					size_t hashi = hs(cur->_kv.first) % newTables.size();
					cur->_next = newTables[hashi];
					newTables[hashi] = cur;

					cur = next;
				}

				_tables[i] = nullptr;
			}

			_tables.swap(newTables);
		}

		size_t hashi = hs(kv.first) % _tables.size(); // 哈希映射
		Node* newnode = new Node(kv); // 头插
		newnode->_next = _tables[hashi];
		_tables[hashi] = newnode;
		++_size;
		return true;
	}

	bool Erase(const K& key)
	{
		if (_tables.size() == 0) // 防止除零错误
		{
			return false;
		}

		Hash hs;
		size_t hashi = hs(key) % _tables.size();
		Node* cur = _tables[hashi];
		Node* prev = nullptr;
		while (cur)
		{
			if (cur->_kv.first == key)
			{
				if (prev == nullptr) // 头插,先把指针数组存的指针指向cur的下一个
				{
					_tables[hashi] = cur->_next;
				}
				else // 中间删
				{
					prev->_next = cur->_next;
				}
				delete cur; // 统一在这delete
				return true;
			}

			prev = cur; // 往后走
			cur = cur->_next;
		}
		return false; // 没找到
	}

	size_t Size() // 存的数据个数
	{
		return _size;
	}

	size_t TablesSize() // 表的长度
	{
		return _tables.size();
	}

	size_t BucketNum() // 桶的个数
	{
		size_t num = 0;
		for (size_t i = 0; i < _tables.size(); ++i)
		{
			if (_tables[i]) // 如果不是空就有桶
			{
				++num;
			}
		}
		return num;
	}

	size_t MaxBucketLenth() // 最长桶的长度
	{
		size_t maxLen = 0;
		for (size_t i = 0; i < _tables.size(); ++i)
		{
			size_t len = 0;
			Node* cur = _tables[i];
			while (cur)
			{
				++len;
				cur = cur->_next;
			}
			if (len > maxLen)
			{
				maxLen = len;
			}
		}
		return maxLen;
	}

protected:
	vector<Node*> _tables; // 指针数组
	size_t _size;
};

有了封装set和map的和学习了哈希的经验,直接写出框架:

UnorderedSet.h:

#pragma once

#include "HashTable.h"

namespace rtx
{
	template<class K, class K>
	class unordered_map
	{
		struct SetKeyOfT
		{
			const K& operator()(const K& key)
			{
				return key;
			}
		};
	public:

	protected:
		HashTable<K, k, Hash, MapKeyOfT> _ht;
	};
}

UnorderedMap.h:

#pragma once

#include "HashTable.h"

namespace rtx
{
	template<class K, class V, class Hash = HashFunc<K>>
	class unordered_map
	{
		struct MapKeyOfT
		{
			const K& operator()(const pair<K, V>& kv)
			{
				return kv.first;
			}
		};
	public:

	protected:
		HashTable<K, pair<K, V>, Hash, MapKeyOfT> _ht;
	};

}

用命名空间和STL库区分,第二个参数对于unordered_set是key,对于unordered_map是piar,

现在应该把ashNode的两个参数改为一个参数T,_pair 改为 _data

 再把HashTable的第二个参数改为T,再加一个获取key的仿函数:

(这里不能在第三个仿函数给默认的了)

2.1 哈希桶的迭代器

迭代器是所有容器必须有的,先来看迭代器的++是如何实现的:

 如上图所示,一个哈希表,其中有四个哈希桶,迭代器是it。

++it操作:

  • 如果it不是某个桶的最后一个元素(桶里数据下一个不为空),则it指向下一个节点。
  • 如果it是桶的最后一个元素(桶里数据下一个为空),则it指向下一个桶的头节点。

要想实现上面的操作,迭代器中不仅需要一个_node来记录当前节点,

还需要一个哈希表的指针,以便找下一个桶,代码如下:

(顺便写迭代器中的其他操作,如解引用,箭头,以及相等等运算符的重载就不再详细介绍了:)

template<class K, class T, class Hash, class KeyOfT>
class HashTable; // 前置声明

template<class K, class T, class Hash, class KeyOfT>
class __HashIterator
{
public:
	typedef HashNode<T> Node;
	typedef HashTable<K, T, Hash, KeyOfT> HT;
	typedef __HashIterator<K, T, Hash, KeyOfT> Self;

	Node* _node; // 数据结点
	HT* _pht; // 哈希表指针

	__HashIterator(Node* node, HT* pht)
		:_node(node)
		, _pht(pht)
	{}

	Self& operator++()
	{
		if (_node->_next) // 不是桶中的最后一个数据
		{
			_node = _node->_next;
		}
		else // 是桶中的最后一个数据,找下一个桶
		{
			Hash hs;
			KeyOfT kot;
			size_t i = hs(kot(_node->_data)) % _pht->_tables.size() + 1;//没+1是当前桶位置
			for (; i < _pht->_tables.size(); ++i)
			{
				if (_pth->tables[i]) // 向后迭代找到了有桶的位置
				{
					_node = _pth->tables[i]; // 把这个位置给_node
					break;
				}
			}
			if (_pht == _tables.size()) // 后面都没桶了
			{
				_node = nullptr;
			}
		}
		return *this; // this调用该函数的对象(迭代器),指向下一个后解引用返回
	}

	T& operator*()
	{
		return _node->_data;
	}

	T* operator->()
	{
		return &_node->_data;
	}

	bool operator!=(const Self& s) const
	{
		return s._node != _node;
	}

	bool operator==(const Self& s) const
	{
		return s._node == _node;
	}
};
  • t不是处于某个桶的末尾,直接指向下一个节点。
  • 当it是某个桶的末尾时,指向下一个桶。

首先需要确定当前桶的位置:
使用KeyOfT仿函数获取当前数据的key值(因为不知道是map还是set在调用)。
再使用Hash仿函数将key值转换成可以模的整形(因为不知道key是整形还是字符串再或者其他自定义类型)。

然后开始寻找下一个桶:
从当前哈希表下标开始向后寻找,直到找到下一个桶,将桶的头节点地址赋值给_node。
如果始终没有找到,说明没有桶了,也就是没有数据了,it指向end,这里使用空指针来代替end。 将++后的迭代器返回。

迭代器中有一个成员变量是哈希表的指针,如上图所示,

所以在迭代器中typedef了HashTable成为 HT,方便我们使用。

根据我们前面实现迭代器的经验,迭代器其实是封装在Hashtable中的,也就是说,在HashTable中也会typedef迭代器:此时HashTable和HashIterator就构成了相互typedef的关系。哈希表和迭代器类的定义势必会有一个先后顺序,这里在定义的时候,在代码顺序上就是先定义迭代器,再定义的哈希表。此时迭代器在typedef的时候就找不到哈希表的定义,因为编译器只会向上寻找而不会向下寻找。所以必须在HashIterator类前面先声明一下HashTable类,这种操作被叫做前置声明。

  • 前置声明一定要放在类外面,如果放在迭代器类里面,编译器只会在迭代器的命名空间中寻找哈希表的定义,这样是找不到的。
  • 前置声明放在类外面的时候,编译器会在整个命名空间中寻找哈希表的定义,就可以找到。

在++迭代器的时候,会使用到哈希表指针,哈希表指针又会使用到HashTable中的_tables。

  • HashTable中的_tables是保护成员,在类外是不能访问的。

解决这个问题可以在HashTable中写一个公有的访问函数,也可以采用友元,这里用下友元。

类模板的友元声明需要写模板参数,在类名前面加friend关键字。

(迭代器要访问HashTable的保护,所以迭代器要成为HashTable的友元)

2.2 封装unordered_set和unordered_map

 有了前面的经验(map的方括号重载要改insert的返回值),这里先把完整的unordered_set.h和

unordered_map.h写出来,看看需要怎么改。封装就是套一层,还是很容易的:

完整unordered_map.h

#pragma once

#include "HashTable.h"

namespace rtx
{
	template<class K, class V, class Hash = HashFunc<K>>
	class unordered_map
	{
		struct MapKeyOfT
		{
			const K& operator()(const pair<K, V>& kv)
			{
				return kv.first;
			}
		};
	public:
		typedef typename 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); // 先看下面,所以insert要返回插入后的键值对
		}

		bool find(const K& key)
		{
			return _ht.Find(key);
		}

		bool erase(const K& key)
		{
			return _ht.Erase(key);
		}

		V& operator[](const K& key) // 根据原功能,返回的是键值对中key对应的value的引用。
		{   // 当key不存在时,operator[]用默认value与key构造键值对然后插入
			pair<iterator, bool> ret = _ht.Insert(make_pair(key, V()));
			return ret.first->second;
		}

	protected:
		HashTable<K, pair<K, V>, Hash, MapKeyOfT> _ht;
	};
}

完整unordered_set.h

#pragma once

#include "HashTable.h"

namespace rtx
{
	template<class K, class Hash = HashFunc<K>>
	class unordered_set
	{
		struct SetKeyOfT
		{
			const K& operator()(const K& key)
			{
				return key;
			}
		};
	public:
		typedef typename HashTable<K, K, Hash, SetKeyOfT>::iterator iterator;

		iterator begin()
		{
			return _ht.begin();
		}

		iterator end()
		{
			return _ht.end();
		}

		pair<iterator, bool> insert(const K& key) //和unordered_map保持一致
		{
			return _ht.Insert(key);
		}

		bool find(const K& key)
		{
			return _ht.Find(key);
		}

		bool erase(const K& key)
		{
			return _ht.Erase(key);
		}
	protected:
		HashTable<K, K, Hash, SetKeyOfT> _ht;
	};
}

2.3 修改哈希桶

先给哈希桶的模板参数增加两个仿函数,用typedef封装迭代器,并给迭代器传对应的模板参数。

还需要在哈希表中增加获取迭代器起始位置和结束位置的接口:

  • 在获取其实位置时,需要从头开始遍历哈希表项,寻找到第一个桶的头节点作为起始位置。
  •  使用空指针代替迭代器的结束位置。
  •  在构造迭代器时,直接传this指针去定义迭代器中的哈希表指针。

在插入中,凡是使用到key值以及用key取模的地方,都要用仿函数取获得。包括删除和删除中也是,插入之前要查找下,先把查找改了:

让其返回迭代器,如果存在,返回key所在位置的迭代器,如果不存在,返回末尾的迭代器。

	iterator Find(const K& key)
	{
		if (_tables.size() == 0)
		{
			return end();
		}

		Hash hs;
		KeyOfT kot;
		size_t hashi = hs(key) % _tables.size();
		Node* cur = _tables[hashi];
		while (cur)
		{
			if (kot(cur->_data) == key)
			{
				return iterator(cur,this);
			}
			cur = cur->_next;
		}
		return end();
	}

然后修改哈希表的Inerst,返回由迭代器和布尔值组成的键值对。

  •  先进行查找,如果存在,则返回key所在位置的迭代器和false组成的键值对。
  •  查找结构不存在,则返回插入新节点后key所在位置的迭代器和true组成的键值对。
	pair<iterator, bool> Insert(const T& data)
	{
		KeyOfT kot;
		iterator ret = Find(kot(data));
		if (ret != end())
		{
			return make_pair(ret, false);
		}

		Hash hs;
		if (_size == _tables.size()) // 负载因子到1就扩容
		{
			//size_t newSize = _tables.size() == 0 ? 10 : _tables.size() * 2;
			vector<Node*> newTables;
			//newTables.resize(newSize, nullptr);
			newTables.resize(__stl_next_prime(_tables.size()), nullptr); //取素数,前两注释改成这一条

			for (size_t i = 0; i < _tables.size(); ++i) // 旧表中节点移动映射新表
			{
				Node* cur = _tables[i];
				while (cur)
				{
					Node* next = cur->_next;

					size_t hashi = hs(kot(cur->_data) % newTables.size();
					cur->_next = newTables[hashi];
					newTables[hashi] = cur;

					cur = next;
				}

				_tables[i] = nullptr;
			}

			_tables.swap(newTables);
		}

		size_t hashi = hs((kot(data) % _tables.size(); // 哈希映射
		Node* newnode = new Node(data); // 头插
		newnode->_next = _tables[hashi];
		_tables[hashi] = newnode;
		++_size;
		return make_pair(iterator(newnode, this), true);
	}

删除只需在移除用上KeyOfT仿函数,然后就改完了,程序就能跑起来了:

完整HashTable.h:

#pragma once

#include <iostream>
#include <vector>
using namespace std;

template<class T>
struct HashNode
{
	T _data;
	HashNode* _next; // 不用存状态栏了,存下一个结点指针

	HashNode(const T& data)
		:_data(data)
		, _next(nullptr)
	{}
};

template<class K>
struct HashFunc // 可以把闭散列的HashFunc放在外面直接用,但是这就不放了
{
	size_t operator()(const K& key)
	{
		return (size_t)key; // 负数,浮点数,指针等可以直接转,string不行
	}
};

template<>
struct HashFunc<string> // 上面的特化
{
	size_t operator()(const string& key)
	{
		size_t val = 0;
		for (const auto& ch : key)
		{
			val *= 131;
			val += ch;
		}

		return val;
	}
};

template<class K, class T, class Hash, class KeyOfT>
class HashTable; // 前置声明

template<class K, class T, class Hash, class KeyOfT>
class __HashIterator
{
public:
	typedef HashNode<T> Node;
	typedef HashTable<K, T, Hash, KeyOfT> HT;
	typedef __HashIterator<K, T, Hash, KeyOfT> Self;

	Node* _node; // 数据结点
	HT* _pht; // 哈希表指针

	__HashIterator(Node* node, HT* pht)
		:_node(node)
		, _pht(pht)
	{}

	Self& operator++()
	{
		if (_node->_next) // 不是桶中的最后一个数据
		{
			_node = _node->_next;
		}
		else // 是桶中的最后一个数据,找下一个桶
		{
			Hash hs;
			KeyOfT kot;
			size_t i = hs(kot(_node->_data)) % _pht->_tables.size() + 1;//没+1是当前桶位置
			for (; i < _pht->_tables.size(); ++i)
			{
				if (_pht->_tables[i]) // 向后迭代找到了有桶的位置
				{
					_node = _pht->_tables[i]; // 把这个位置给_node
					break;
				}
			}
			if (i == _pht->_tables.size()) // 后面都没桶了
			{
				_node = nullptr;
			}
		}
		return *this; // this调用该函数的对象(迭代器),指向下一个后解引用返回
	}

	T& operator*()
	{
		return _node->_data;
	}

	T* operator->()
	{
		return &_node->_data;
	}

	bool operator!=(const Self& s) const
	{
		return s._node != _node;
	}

	bool operator==(const Self& s) const
	{
		return s._node == _node;
	}
};

template<class K, class T, class Hash, class KeyOfT>
class HashTable
{
public:
	template<class K, class T, class Hash, class KeyOfT>
	friend class __HashIterator;

	typedef HashNode<T> Node;
	typedef __HashIterator<K, T, Hash, KeyOfT> iterator;

	iterator begin()
	{
		for (size_t i = 0; i < _tables.size(); ++i)
		{
			if (_tables[i])
			{
				return iterator(_tables[i], this); // 构造:(Node * node, HT * pht)
			}
		}
		return end();
	}

	iterator end()
	{
		return iterator(nullptr, this);
	}

	~HashTable()
	{
		for (size_t i = 0; i < _tables.size(); ++i)
		{
			Node* cur = _tables[i];
			while (cur)
			{
				Node* next = cur->_next;
				delete cur;
				cur = next;
			}
			_tables[i] = nullptr;
		}
	}

	iterator Find(const K& key)
	{
		if (_tables.size() == 0)
		{
			return end();
		}

		Hash hs;
		KeyOfT kot;
		size_t hashi = hs(key) % _tables.size();
		Node* cur = _tables[hashi];
		while (cur)
		{
			if (kot(cur->_data) == key)
			{
				return iterator(cur,this);
			}
			cur = cur->_next;
		}
		return end();
	}

	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 -1; // 不会走到这,随便返回一个值
	}

	pair<iterator, bool> Insert(const T& data)
	{
		KeyOfT kot;
		iterator ret = Find(kot(data));
		if (ret != end())
		{
			return make_pair(ret, false);
		}

		Hash hs;
		if (_size == _tables.size()) // 负载因子到1就扩容
		{
			//size_t newSize = _tables.size() == 0 ? 10 : _tables.size() * 2;
			vector<Node*> newTables;
			//newTables.resize(newSize, nullptr);
			newTables.resize(__stl_next_prime(_tables.size()), nullptr); //取素数,前两注释改成这一条

			for (size_t i = 0; i < _tables.size(); ++i) // 旧表中节点移动映射新表
			{
				Node* cur = _tables[i];
				while (cur)
				{
					Node* next = cur->_next;

					size_t hashi = hs(kot(cur->_data)) % newTables.size();
					cur->_next = newTables[hashi];
					newTables[hashi] = cur;

					cur = next;
				}

				_tables[i] = nullptr;
			}

			_tables.swap(newTables);
		}

		size_t hashi = hs(kot(data)) % _tables.size(); // 哈希映射
		Node* newnode = new Node(data); // 头插
		newnode->_next = _tables[hashi];
		_tables[hashi] = newnode;
		++_size;
		return make_pair(iterator(newnode, this), true);
	}

	bool Erase(const K& key)
	{
		if (_tables.size() == 0) // 防止除零错误
		{
			return false;
		}

		Hash hs;
		KeyOfT kot;
		size_t hashi = hs(key) % _tables.size();
		Node* cur = _tables[hashi];
		Node* prev = nullptr;
		while (cur)
		{
			if (kot(cur->_data) == key)
			{
				if (prev == nullptr) // 头插,先把指针数组存的指针指向cur的下一个
				{
					_tables[hashi] = cur->_next;
				}
				else // 中间删
				{
					prev->_next = cur->_next;
				}
				delete cur; // 统一在这delete
				return true;
			}

			prev = cur; // 往后走
			cur = cur->_next;
		}
		return false; // 没找到
	}

	size_t Size() // 存的数据个数
	{
		return _size;
	}

	size_t TablesSize() // 表的长度
	{
		return _tables.size();
	}

	size_t BucketNum() // 桶的个数
	{
		size_t num = 0;
		for (size_t i = 0; i < _tables.size(); ++i)
		{
			if (_tables[i]) // 如果不是空就有桶
			{
				++num;
			}
		}
		return num;
	}

	size_t MaxBucketLenth() // 最长桶的长度
	{
		size_t maxLen = 0;
		for (size_t i = 0; i < _tables.size(); ++i)
		{
			size_t len = 0;
			Node* cur = _tables[i];
			while (cur)
			{
				++len;
				cur = cur->_next;
			}
			if (len > maxLen)
			{
				maxLen = len;
			}
		}
		return maxLen;
	}

protected:
	vector<Node*> _tables; // 指针数组
	size_t _size;
};

Test.cpp:

#include "UnorderedSet.h"
#include "UnorderedMap.h"

namespace rtx
{
	void test_unordered_set()
	{
		unordered_set<int> s;
		s.insert(2);
		s.insert(3);
		s.insert(1);
		s.insert(2);
		s.insert(5);

		unordered_set<int>::iterator it = s.begin();
		//auto it = s.begin();
		while (it != s.end())
		{
			cout << *it << " ";
			++it;
		}
		cout << endl << endl;;
	}

	void test_unordered_map()
	{
		unordered_map<string, string> dict;
		dict.insert(make_pair("sort", "排序"));
		dict.insert(make_pair("string", "字符串"));
		dict.insert(make_pair("left", "左边"));

		unordered_map<string, string>::iterator it = dict.begin();
		while (it != dict.end())
		{
			cout << it->first << ":" << it->second << endl;
			++it;
		}
		cout << endl;

		unordered_map<string, int> countMap;
		string arr[] = { "苹果", "西瓜", "苹果", "西瓜", "苹果", "苹果", "西瓜", "苹果", "香蕉", "苹果", "香蕉" };
		for (const auto& e : arr)
		{
			countMap[e]++;
		}

		for (const auto& kv : countMap)
		{
			cout << kv.first << ":" << kv.second << endl;
		}
	}
}

int main()
{
	rtx::test_unordered_set();
	rtx::test_unordered_map();

	return 0;
}

3. 题外话+笔试选择题

还有一些接口函数和仿函数参数这里并没有实现,正如前面说的,

模拟实现不是为了造一个更好的轮子,而是理解它的底层实现。

值得一提的是库里面unordered系列都提供了比较key相不相等的仿函数:

本篇模拟实现的是直接调用的等于,这样就写死了,

比如key是日期类的指针比较就是比较指针的地址了,但是我们想要比较的是指针指向的内容。

所以应该是要加上这个仿函数的,我们以前写过类似的这里就不加上去了。

所以就会有下面的面试题:

------------------------------------------------分割----------------------------------------------------------------

笔试选择题1:关于unordered_map和unordered_set说法错误的是()

A.它们中存储元素的类型不同,unordered_map存储键值对,而unordered_set中只存储key

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

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

D.它们在进行元素插入时,都得要通过key的比较去找待插入元素的位置

笔试选择题2:关于unordered_map和unordered_set说法错误的是()

A.它们中都存储的键值对

B.map适合key有序的场景,unordered_map没有有序的要求

C.它们中元素查找的方式相同

D.map的底层结构是红黑树,unordered_map的底层结构是哈希桶

答案:

A:正确,参考unordered_map和unordered_set的文档说明

B:正确,都采用的是哈希桶来实现的

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

D:错误,不需要比较,只需要通过哈希函数,就可以确认元素需要存储的位置

选D

A:正确,结合文档说明

B:正确,因为map的底层是红黑树,红黑树中序遍历可以得到关于key有序的序列,而unordered _map底层是哈希     桶,哈希对于其存储的元素是否有序,并不关心

C:错误,map按照二叉搜索树的规则查找,unordered_map按照哈希方式进行查找

D:正确

选C

本篇完。

下一篇是又到高阶数据结构的内容:从C语言到C++_32(哈希的应用)位图bitset+布隆过滤器+哈希切割。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/859233.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

NIO 非阻塞式IO

NIO Java NIO 基本介绍 Java NIO 全称 Java non-blocking IO&#xff0c;是指 JDK 提供的新 API。从 JDK1.4 开始&#xff0c;Java 提供了一系列改进的输入/输出的新特性&#xff0c;被统称为 NIO&#xff08;即 NewIO&#xff09;&#xff0c;是同步非阻塞的。NIO 相关类都被…

AIGC 浪潮下,鹅厂新一代前端人的真实工作感受

点击链接了解详情 原创作者&#xff1a;张波 腾小云导读 AIGC 这一时代潮流已然不可阻挡&#xff0c;我们要做的不是慌乱&#xff0c;而是把握住这个时代的机会。本文就和大家一起来探索在 AIGC 下&#xff0c;前端工程师即将面临的挑战和机遇。聊聊从以前到现在&#xff0c;A…

诸神之战:数字时代的低代码服务商与代理商究竟谁更强?

随着数字化转型浪潮的推进&#xff0c;企业对数字化应用开发的需求迅速增长。低代码作为一种新的软件开发范式&#xff0c;以其可视化和快速构建应用的能力&#xff0c;被广泛应用于成千上万家企业中。当低代码行业的逐渐发展成熟&#xff0c;越来越多的人看到了低代码的商业价…

使用乐观锁解决超卖问题

目录 什么是超卖&#xff1f; 乐观锁和悲观锁的定义 悲观锁&#xff1a; 乐观锁&#xff1a; 乐观锁的实现方式 1.版本号 2.CAS法 什么是超卖&#xff1f; 举个例子&#xff1a;订单系统中&#xff0c;用户在执行下单操作时&#xff0c;可能同一时间有无数个用户同时下单&…

平替版Airtag

Airtag是什么&#xff1f; AirTag是苹果公司设计的一款定位神奇&#xff0c;它通过一款纽扣电池进行供电&#xff0c;即可实现长达1-2年的关键物品的定位、查找的功能。 按照苹果公司自己的话说—— 您“丢三落四这门绝技&#xff0c;要‍失‍传‍了”。 AirTag 可帮你轻松追…

USB(二):Type-C

一、引脚定义 Type-C口有 4对TX/RX差分线&#xff0c;2对USB D/D-&#xff0c;1对SBU&#xff0c;2个CC&#xff0c;4个VBUS和4个地线Type-C母座视图&#xff1a; Type-C公头视图&#xff1a; 二、关键名词 DFP(Downstream Facing Port)&#xff1a; 下行端口&#xff0c…

【云原生】Pod的进阶

目录 一、资源限制二、重启策略三、健康检查 &#xff0c;又称为探针&#xff08;Probe&#xff09;3.1示例1&#xff1a;exec方式3.2示例2&#xff1a;httpGet方式3.3示例3&#xff1a;tcpSocket方式3.4示例4&#xff1a;就绪检测3.5示例5&#xff1a;就绪检测2 四、启动、退出…

设置VsCode 将打开的多个文件分行(栏)排列,实现全部显示

目录 1. 前言 2. 设置VsCode 多文件分行(栏)排列显示 1. 前言 主流编程IDE几乎都有排列切换选择所要查看的文件功能&#xff0c;如下为Visual Studio 2022的该功能界面&#xff1a; 图 1 图 2 当在Visual Studio 2022打开很多文件时&#xff0c;可以按照图1、图2所示找到自…

价格监测与数据分析的关系

所谓的价格监测&#xff0c;其实可以理解为是低价数据的监测&#xff0c;当监测价格时&#xff0c;其他页面上的商品数据也会被同时采集监测&#xff0c;如标题、库存、销量、评价等内容&#xff0c;所以品牌在做电商价格监测时&#xff0c;其实也可以对数据进行分析。 力维网络…

【React学习】—jsx语法规则(三)

【React学习】—jsx语法规则&#xff08;三&#xff09; 一、jsx语法规则&#xff1a; 1、定义虚拟DOM&#xff0c;不要写引号&#xff0c; 2、标签中混入JS表达式要用{} 3、样式的类名指定不要用class&#xff0c;要用className 4、内联样式&#xff0c;要用style{{key:value}…

linux环形缓冲区kfifo实践2:配合等待队列使用

基础 struct __wait_queue_head {spinlock_t lock;struct list_head task_list; }; typedef struct __wait_queue_head wait_queue_head_t; 初始化等待队列&#xff1a;init_waitqueue_head 深挖init_waitqueue_head宏的定义可知&#xff0c;传递给它的参数q是一个wait_queu…

pytest 编写规范

一、pytest 编写规范 1、介绍 pytest是一个非常成熟的全功能的Python测试框架&#xff0c;主要特点有以下几点&#xff1a; 1、简单灵活&#xff0c;容易上手&#xff0c;文档丰富&#xff1b;2、支持参数化&#xff0c;可以细粒度地控制要测试的测试用例&#xff1b;3、能够…

分享之python 协程

线程和进程的操作是由程序触发系统接口&#xff0c;最后的执行者是系统&#xff1b;协程的操作则是程序员。 协程存在的意义&#xff1a;对于多线程应用&#xff0c;CPU通过切片的方式来切换线程间的执行&#xff0c;线程切换时需要耗时&#xff08;保存状态&#xff0c;下次继…

Redux中reducer 中为什么每次都要返回新的state!!!

Redux中reducer 中为什么每次都要返回新的state&#xff01;&#xff01;&#xff01; 最近在学习react相关的知识&#xff0c;学习redux的时候遇到看到一个面试题&#xff1a; 如果Redux没返回新的数据会怎样&#xff1f; 这就是要去纠结为什么编写reducer得时候为什么不允许直…

LT8711HE 是一款高性能的Type-C/DP1.2到HDMI2.0转换器

LT8711HE 1.描述 LT8711HE是一种高性能的Type-C/DP1.2到HDMI2.0转换器&#xff0c;设计用于连接USB Type-C源或DP1.2源到HDMI2.0接收器。LT8711HE集成了一个DP1.2兼容的接收器&#xff0c;和一个HDMI2.0兼容的发射机。此外&#xff0c;还包括两个CC控制器&#xff0c;用于CC通…

Linux Maven 安装与配置

目录 Maven 下载 解压缩下载的文件 移动Maven文件夹 配置环境变量 验证安装 注意 Maven 下载 官方地址 Maven – Download Apache Maven&#xff0c;下载完成后&#xff0c;解压到合适的位置即可&#xff1b; 解压缩下载的文件 解压缩下载的文件&#xff1a; 使用以下命…

Malloc动态内存分配

在C语言中我们会使用malloc来动态地分配内存&#xff0c;这样做的一个主要理由是有些数据结构的大小只有在运行时才能确定。例如&#xff0c;如果你正在编写一个程序&#xff0c;需要用户输入一些数据&#xff0c;但你不知道用户会输入多少数据&#xff0c;那么你就需要使用动态…

VGPU理解与实践包含虚拟机显卡直通,k8s安装,GPU-manager使用与实践测试

提示&#xff1a;文章分为三部分&#xff1a;物理GPU绑定虚拟机、k8s安装、gpu-manager虚拟化实现与测试 文章目录 前言一、什么是VGPU&#xff1f;二、此文件会拆分成三部分&#xff1a;1.物理机显卡直通虚拟机2.安装K8S3.安装GPU-manager、测试全流程 总结 前言 用户角度GPU…

【Linux】HTTPS协议——应用层

1 HTTPS是什么&#xff1f; HTTPS也是⼀个应⽤层协议.是在 HTTP 协议的基础上引⼊了⼀个加密层. HTTP 协议内容都是按照⽂本的⽅式明⽂传输的. 这就导致在传输过程中出现⼀些被篡改的情况. HTTP VS HTTPS 早期很多公司刚起步的时候&#xff0c;使用的应用层协议都是HTTP&am…

7.7 通俗易懂详解稠密连接网络DenseNet 手撕稠密连接网络DenseNet

一.思想 与ResNet的区别 DenseNet这样拼接有什么好处&#xff1f;DenseNet优点 对于每一层&#xff0c;使用前面所有层的特征映射作为输入&#xff0c;并且其自身的特征映射作为所有后续层的输入。 DenseNet的优点: 缓解了消失梯度问题&#xff0c;加强了特征传播&#xff0c…