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

这篇具有很好参考价值的文章主要介绍了从C语言到C++_31(unordered_set和unordered_map介绍+哈希桶封装)。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

目录

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

从C语言到C++_31(unordered_set和unordered_map介绍+哈希桶封装),④从C语言到C++,c++,哈希算法,数据结构,算法,STL

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

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

从C语言到C++_31(unordered_set和unordered_map介绍+哈希桶封装),④从C语言到C++,c++,哈希算法,数据结构,算法,STL


1.2 unordered_set

从C语言到C++_31(unordered_set和unordered_map介绍+哈希桶封装),④从C语言到C++,c++,哈希算法,数据结构,算法,STL

  • 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的函数使用,还有一些关于哈希的接口后面再讲解

从C语言到C++_31(unordered_set和unordered_map介绍+哈希桶封装),④从C语言到C++,c++,哈希算法,数据结构,算法,STL


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的一样)

从C语言到C++_31(unordered_set和unordered_map介绍+哈希桶封装),④从C语言到C++,c++,哈希算法,数据结构,算法,STL

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类用法,这里讲一个小技巧:可以将字符串中每个单词按空格隔开。

从C语言到C++_31(unordered_set和unordered_map介绍+哈希桶封装),④从C语言到C++,c++,哈希算法,数据结构,算法,STL

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;
    }
};

如果解开注释:

从C语言到C++_31(unordered_set和unordered_map介绍+哈希桶封装),④从C语言到C++,c++,哈希算法,数据结构,算法,STL


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

从C语言到C++_31(unordered_set和unordered_map介绍+哈希桶封装),④从C语言到C++,c++,哈希算法,数据结构,算法,STL

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

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

从C语言到C++_31(unordered_set和unordered_map介绍+哈希桶封装),④从C语言到C++,c++,哈希算法,数据结构,算法,STL


2.1 哈希桶的迭代器

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

从C语言到C++_31(unordered_set和unordered_map介绍+哈希桶封装),④从C语言到C++,c++,哈希算法,数据结构,算法,STL

 如上图所示,一个哈希表,其中有四个哈希桶,迭代器是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的友元)

从C语言到C++_31(unordered_set和unordered_map介绍+哈希桶封装),④从C语言到C++,c++,哈希算法,数据结构,算法,STL


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封装迭代器,并给迭代器传对应的模板参数。还需要在哈希表中增加获取迭代器起始位置和结束位置的接口:

从C语言到C++_31(unordered_set和unordered_map介绍+哈希桶封装),④从C语言到C++,c++,哈希算法,数据结构,算法,STL

  • 在获取其实位置时,需要从头开始遍历哈希表项,寻找到第一个桶的头节点作为起始位置。
  • 使用空指针代替迭代器的结束位置。
  • 在构造迭代器时,直接传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;
}

从C语言到C++_31(unordered_set和unordered_map介绍+哈希桶封装),④从C语言到C++,c++,哈希算法,数据结构,算法,STL


3. 题外话+笔试选择题

        还有一些接口函数和仿函数参数这里并没有实现,正如以前说的,模拟实现不是为了造一个更好的轮子,而是理解它的底层实现。还有就是库里面unordered系列都提供了比较key相不相等的仿函数:

从C语言到C++_31(unordered_set和unordered_map介绍+哈希桶封装),④从C语言到C++,c++,哈希算法,数据结构,算法,STL

        本篇模拟实现的是直接调用的等于,这样就写死了,比如key是日期类的指针比较就是比较指针的地址了,但是我们想要比较的是指针指向的内容。所以应该是要加上这个仿函数的,我们以前写过类似的这里就不加上去了。

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

从C语言到C++_31(unordered_set和unordered_map介绍+哈希桶封装),④从C语言到C++,c++,哈希算法,数据结构,算法,STL


笔试选择题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的底层结构是哈希桶

答案及解析:

笔试选择题1:

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

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

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

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

选D

笔试选择题2:

A:正确,结合文档说明

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

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

D:正确

选C


本篇完。

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

穿越回来复习顺便贴个下篇链接:

从C语言到C++_32(哈希的应用)位图bitset+布隆过滤器+哈希切割_c++中hash32-CSDN博客文章来源地址https://www.toymoban.com/news/detail-635276.html

到了这里,关于从C语言到C++_31(unordered_set和unordered_map介绍+哈希桶封装)的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 【C++】unordered_set与unordered_map的封装

    🌇个人主页:平凡的小苏 📚学习格言:命运给你一个低的起点,是想看你精彩的翻盘,而不是让你自甘堕落,脚下的路虽然难走,但我还能走,比起向阳而生,我更想尝试逆风翻盘 。 🛸 C++专栏 : C++内功修炼基地 家人们更新不易,你们的👍点赞👍和⭐关注⭐真的对我真

    2024年02月08日
    浏览(48)
  • 【C++】unordered_map和unordered_set的使用

    文章目录 前言 一、unordered_map的使用及性能测试 二、unordered_set的使用 1.习题练习 总结 unordered 系列关联式容器 : 在 C++98 中, STL 提供了底层为红黑树结构的一系列关联式容器,在查询时效率可达到O(logN) ,即最差情况下需要比较红黑树的高度次,当树中的节点非常多时,

    2024年02月06日
    浏览(48)
  • 【C++】unordered_set 和 unordered_map 使用 | 封装

    unordered_map官方文档 unordered_set 官方文档 set / map与unordered_set / unordered_map 使用功能基本相同,但是两者的底层结构不同 set/map底层是红黑树 unordered_map/unordered_set 底层是 哈希表 红黑树是一种搜索二叉树,搜索二叉树又称为排序二叉树,所以 迭代器遍历是有序的 而哈希表对应的

    2024年02月06日
    浏览(48)
  • 改造哈希表,封装unordered_map和unordered_set

    正文开始前给大家推荐个网站,前些天发现了一个巨牛的 人工智能 学习网站, 通俗易懂,风趣幽默 ,忍不住分享一下给大家。点击跳转到网站。 unordered_map是存的是pair是K,V型的,而unordered_set是K型的,里面只存一个值,那我们如何利用一个数据结构将他们都封装出来呢?

    2024年02月03日
    浏览(48)
  • 【高阶数据结构】封装unordered_map 和 unordered_set

    (꒪ꇴ꒪(꒪ꇴ꒪ )🐣,我是 Scort 目前状态:大三非科班啃C++中 🌍博客主页:张小姐的猫~江湖背景 快上车🚘,握好方向盘跟我有一起打天下嘞! 送给自己的一句鸡汤🤔: 🔥真正的大师永远怀着一颗学徒的心 作者水平很有限,如果发现错误,可在评论区指正,感谢🙏 🎉🎉

    2024年02月03日
    浏览(42)
  • 【C++】哈希表封装实现 unordered_map 和 unordered_set

    在 C++98 中,STL 提供了底层为红黑树结构的一系列关联式容器,在查询时效率可达到 O(logN),即最差情况下只需要比较红黑树的高度次;但是当树中的节点非常多时,其查询效率也不够极致。 最好的查询是,不进行比较或只进行常数次比较就能够将元素找到,因此在 C++11 中,

    2023年04月16日
    浏览(49)
  • 从哈希桶角度看 unordered_map 与 unordered_set 的实现

    哈希函数与哈希桶是计算机科学中用于实现高效数据检索的重要工具。在之前的博客中,我们已经详细探讨了哈希的基本概念、哈希函数的构造方法以及哈希桶的工作原理等内容。在本篇博客中,我们将进一步深入探索C++中的unordered系列数据结构,并利用之前介绍的哈希桶原

    2024年03月22日
    浏览(39)
  • 【C++】用哈希桶模拟实现unordered_set和unordered_map

    顺序结构中(数组)查找一个元素需要遍历整个数组,时间复杂度为O(N);树形结构中(二叉搜索树)查找一个元素,时间复杂度最多为树的高度次logN。理想的搜索方法: 可以不经过任何比较,一次直接从表中得到要搜索的元素。 构造一种存储结构, 通过某种函数使元素的

    2024年04月11日
    浏览(52)
  • 【C++】unordered_map和unordered_set的使用 及 OJ练习

    在前面的文章中,我们已经学习了STL中底层为红黑树结构的一系列关联式容器——set/multiset 和 map/multimap(C++98) 在C++98中,STL提供了底层为红黑树结构的一系列关联式容器,在查询时效率可达到 l o g 2 N log_2 N l o g 2 ​ N ,即最差情况下需要比较红黑树的高度次。 在C++11中,

    2024年02月11日
    浏览(43)
  • 由[哈希/散列]模拟实现[unordered_map/unordered_set] (手撕迭代器)

    以下两篇文章均为笔者的呕心沥血 想要搞懂本篇文章的uu请自行查阅 哈希/散列的细节实现 哈希/散列–哈希表[思想到结构][==修订版==] 手撕迭代器的细节处理 模拟实现map/set[改编红黑树实现map/set容器底层]

    2024年02月07日
    浏览(46)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包