从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介绍+哈希桶封装)_第1张图片

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

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

从C语言到C++_31(unordered_set和unordered_map介绍+哈希桶封装)_第2张图片

1.2 unordered_set

从C语言到C++_31(unordered_set和unordered_map介绍+哈希桶封装)_第3张图片

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介绍+哈希桶封装)_第4张图片

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& nums) {

    }
};

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

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

        unordered_map 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 intersection(vector& nums1, vector& nums2) {

    }
};

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

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

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

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

        vector 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& nums) {

    }
};

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

从C语言到C++_31(unordered_set和unordered_map介绍+哈希桶封装)_第5张图片

class Solution {
public:
    bool containsDuplicate(vector& nums) {
        unordered_set 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 uncommonFromSentences(string s1, string s2) {

    }
};

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

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

从C语言到C++_31(unordered_set和unordered_map介绍+哈希桶封装)_第6张图片

class Solution {
public:
    vector uncommonFromSentences(string s1, string s2) {
        unordered_map m;
        vector 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介绍+哈希桶封装)_第7张图片

2. 实现unordered_set和unordered_map

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

#pragma once

#include 
#include 
using namespace std;

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

	HashNode(const pair& kv)
		:_kv(kv)
		, _next(nullptr)
	{}
};

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

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

		return val;
	}
};

template>
class HashTable
{
public:
	typedef HashNode 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& 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 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 _tables; // 指针数组
	size_t _size;
};

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

UnorderedSet.h:

#pragma once

#include "HashTable.h"

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

	protected:
		HashTable _ht;
	};
}

UnorderedMap.h:

#pragma once

#include "HashTable.h"

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

	protected:
		HashTable, Hash, MapKeyOfT> _ht;
	};

}

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

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

从C语言到C++_31(unordered_set和unordered_map介绍+哈希桶封装)_第8张图片

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

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

从C语言到C++_31(unordered_set和unordered_map介绍+哈希桶封装)_第9张图片

2.1 哈希桶的迭代器

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

从C语言到C++_31(unordered_set和unordered_map介绍+哈希桶封装)_第10张图片

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

++it操作:

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

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

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

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

template
class HashTable; // 前置声明

template
class __HashIterator
{
public:
	typedef HashNode Node;
	typedef HashTable HT;
	typedef __HashIterator 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介绍+哈希桶封装)_第11张图片

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 unordered_map
	{
		struct MapKeyOfT
		{
			const K& operator()(const pair& kv)
			{
				return kv.first;
			}
		};
	public:
		typedef typename HashTable, Hash, MapKeyOfT>::iterator iterator;

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

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

		pair insert(const pair& 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 ret = _ht.Insert(make_pair(key, V()));
			return ret.first->second;
		}

	protected:
		HashTable, Hash, MapKeyOfT> _ht;
	};
}

完整unordered_set.h

#pragma once

#include "HashTable.h"

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

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

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

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

2.3 修改哈希桶

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

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

从C语言到C++_31(unordered_set和unordered_map介绍+哈希桶封装)_第12张图片

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

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

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

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

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

		return val;
	}
};

template
class HashTable; // 前置声明

template
class __HashIterator
{
public:
	typedef HashNode Node;
	typedef HashTable HT;
	typedef __HashIterator 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 HashTable
{
public:
	template
	friend class __HashIterator;

	typedef HashNode Node;
	typedef __HashIterator 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 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 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 _tables; // 指针数组
	size_t _size;
};

Test.cpp:

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

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

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

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

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

		unordered_map 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介绍+哈希桶封装)_第13张图片

3. 题外话+笔试选择题

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

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

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

从C语言到C++_31(unordered_set和unordered_map介绍+哈希桶封装)_第14张图片

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

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

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

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

从C语言到C++_31(unordered_set和unordered_map介绍+哈希桶封装)_第15张图片

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

笔试选择题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+布隆过滤器+哈希切割。

你可能感兴趣的:(④从C语言到C++,c++,哈希算法,数据结构,算法,STL)