C++学习笔记——关联式容器(下)

前面的树形结构的关联式容器介绍完,下面要说的是哈希结构的关联式容器。

1. unordered系列

看到unordered就知道其是无序的,在前面的map和set当中,我们知道,这个容器会根据数据的key值进行排序,但是这个系列是无序的。其也分unordered_map和unordered_set等,这两种系列关联式容器是无缝对接的,也就是说,这两种系列的各种类型的接口是相同的,所以这里不在介绍接口,直接用一些题来认识一下吧。
两句话中不常见的单词

class Solution {
public:
    vector<string> uncommonFromSentences(string A, string B) {
        unordered_map<string, int> cmap;
        vector<string> ret;
        
        FenCi(cmap, A);
        FenCi(cmap, B);
        
        for(const auto& str : cmap)
        {
            if(str.second == 1)
                ret.push_back(str.first);
        }
        return ret;
    }
private:
void FenCi(unordered_map<string, int>& m, string& str)
{
	string tmp;
	for (size_t i = 0; i <= str.size(); ++i)
	{
		if (str[i] != ' ' && i != str.size())
			tmp += str[i];
		else
		{
			++m[tmp];
			tmp.clear();
		}
	}
}
};

两个数组的交集

class Solution {
public:
    vector<int> intersection(vector<int>& nums1, vector<int>& nums2) {
        unordered_map<int, int> m1;
        unordered_map<int, int> m2;
        vector<int> ret;
        for(const auto& num : nums1)
            ++m1[num];
        for(const auto& num : nums2)
            ++m2[num];
        for(const auto& m : m1)
            if(m2[m.first] >= 1)
                ret.push_back(m.first);
        return ret;
    }
};

现在完成上面的用法,接下来就要看看底层的实现。

2. 哈希

2.1 概念

在前面的搜索树中,每次查询元素的时候,都需要进行比较,所以是一个O(logN)的算法。在哈希中,通过对哈希函数对 key值进行计算,得到一个位置来存储元素,当通过key值查询元素时,用哈希计算位置后,直接访问该位置,所以这个效率还是比较高的。
哈希函数又可以叫散列函数,构造出来的结构称为哈希表(Hash Table)(或者称散列表)。
在这里插入图片描述

2.2 哈希冲突

其实通过上面的图可以很容易的发现,如果两个数计算的位置在同一个位置时,就会有冲突,如key=8,和key=1时两个都在1的位置上,这就是哈希冲突。后面的一系列方法都是为了解决这个冲突或者说是优化这个冲突。
不同关键字通过相同哈希哈数计算出相同的哈希地址,该种现象称为哈希冲突或哈希碰撞。

2.3 哈希函数

下面说两种哈希函数,很常见的。在第一个图中用的哈希函数叫除留余数法。

2.3.1 除留余数法

设散列表中允许的地址数为m,取一个不大于m,但最接近或者等于m的质数p作为除数,按照哈希函数:Hash(key) = key% p(p<=m),将关键码转换成哈希地址。
例如第一个图。
所以如果p太小的话,这个方法的哈希冲突就很严重。

2.3.2 直接定址法

取关键字的某个线性函数为散列地址:Hash(Key)= A*Key + B。
这个函数的几乎不会造成哈希冲突,但是有一个问题,就是浪费空间。例如A很大的时候,key很大的时候,空间的需求是一个线性增长的。
A = 100,key = 100,这是就需要10000以上的位置,如果数据的大小不是一个字节的时候,更需要很大的空间。
后续的方法基本是采用第一种,第二种只适合查找比较小且连续的情况。

2.4 哈希冲突的解决

哈希冲突的解决方式有两种:闭散列和开散列

2.4.1闭散列

闭散列又有线性探测和二次探测。

2.4.1.1 线性探测

线性探测就是说,计算出位置后,如果这个位置已经有数据了,我就往后找位置,知道找到一个空位置。而找这个值的时候,也是同时位置,如果该位置不是这个元素,就往后找,如果找到空位置都没有找到,说明哈希表里面没有这个值。
在这里插入图片描述但是如果在找1之前,8的位置的数删除了怎么办,在线性探测中,数据删除的时候,不是真的删除,而是把结点的标记置为DELETE,当查询的时候,直接跳过这个数据就好。
下面来实现一下。
定义结点状态

//定义结点的状态
	enum STATUS
	{
		EMPTY,//空
		EXIST,//存在
		DELETE//删除
	};

定义成员

//哈希结点的成员
	template<class K, class V>
	struct HashNode
	{
		pair<K, V> kv_;
		STATUS status_ = EMPTY;
	};
//哈希表
	template<class K, class V>
	class HashTable
	{
		typedef HashNode<K, V> Node;
		typedef Node* pNode;

	public:
		HashTable(size_t N = 10)
		{
			ht_.resize(N);
			size_ = 0;
		}
	private:
		//实现简单,扩容都交给vector
		vector<Node> ht_;
		//有效元素的数量
		size_t size_;
	};
}

而且哈希表不会存满,因为会有一个负载因子进行控制,也就是数据到达一定程度,哈希表进行扩容。

		void CheckCapacity()
		{
			if (size_ * 10 / ht_.size() >= 7)
			{
				size_t newsize = ht_.size() * 2;
				HashTable<K, V> tmp(newsize);
				for (size_t i = 0; i < ht_.size(); ++i)
				{
					if (ht_[i].status_ == EXIST)
					{
						tmp.Insert(ht_[i].kv_);
					}
				}
				ht_.swap(tmp.ht_);
			}
		}

插入数据

	//插入
		bool Insert(const pair<K, V>& kv)
		{
			//首先需要检查容量
				//设置负载因子,降低哈希冲突概率
			CheckCapacity();

			size_t index = kv.first % ht_.size();
			while (ht_[index].status_ == EXIST)//找到不是存在状态的结点位置
			{
				if (ht_[index].kv_.first == kv.first)//如果该位置的key和插入结点的key相同,则不能插入
					return false;
				++index;
				if (index == ht_.size())
					index = 0;
			}

			ht_[index].kv_ = kv;
			ht_[index].status_ = EXIST;//设置为存在数据状态
			++size_;
			return true;
		}

查询数据,从计算出的位置一直往后找,如果找到空了,说明没有该节点。

		pNode Find(const K& key)
		{
			size_t index = key % ht_.size();
			while (ht_[index].status_ != EMPTY)
			{
				if (ht_[index].status_ == EXIST && ht_[index].kv_.first == key)
					return &ht_[index];
				++index;
				if (index == ht_.size())
					index = 0;
			}
			return nullptr;
		}

删除数据,不是真的删除,只是设置状态,下次有数据来,直接覆盖。

		bool Erase(const K& key)
		{
			pNode ret = Find(key);
			if (ret)
			{
				ret->status_ == DELETE;
				--size_;
				return true;
			}
			return false;
		}

注意当前的哈希表只能存储整型值,所以后续再慢慢优化,目前先记录思想。

2.4.1.2 二次探测

对于线性探测来说,每次一个一个的往后找,一旦发生哈希冲突,所有的冲突连在一起,容易产生数据“堆积”,即:不同关键码占据了可利用的空位置,使得寻找某关键码的位置需要许多次比较,导致搜索效率降低效率比较低,于是出现了二次探测,跳着找位置。
找下一个空位置的方法为:
H i H_i Hi = ( H 0 H_0 H0 + i 2 i^2 i2)% m,
或者:
H i H_i Hi = ( H 0 H_0 H0 - i 2 i^2 i2) % m。
其中:i = 0,1,2,3…, 是通过散列函数Hash(x)对元素的关键码 key 进行计算得到的位置,m是表的大小
在这里插入图片描述

2.4.2 开散列
2.4.2.1 哈希桶

闭散列介绍完了,下面来说一下开散列吧。开散列采用的方式是,哈希桶。
开散列法又叫链地址法(开链法),首先对关键码集合用散列函数计算散列地址,具有相同地址的关键码归于同一子集合,每一个子集合称为一个桶,各个桶中的元素通过一个单链表链接起来,各链表的头结点存储在哈希表中。
在这里插入图片描述所以开散列中每个桶中放的都是发生哈希冲突的元素。

2.4.2.2 具体实现

定义哈希结点

	template<class V>
	struct HashNode
	{
		V data_;
		HashNode<V>* next_;
		
		HashNode(const V& data)
			:data_(data)
			,next_(nullptr)//桶中的元素指向下一个元素,如果是桶中的最后一个元素,就指向空
		{}
	};

哈希表的定义,哈希表中存的都是指向这些同的指针。

		typedef HashNode<V> Node;
		typedef Node* pNode;
		size_t size_;
		vector<pNode> ht_;

这里只实现一个插入。开散列的插入方式如下,举个例子
在这里插入图片描述

由于哈希表里面存的都是指向同的指针,因此,如果有新的结点来了,计算出位置之后,将给哈希表位置的指针,指向这个结点,这个新的结点在只想哈希表的当前位置原本指向的那个结点。
整个过程就是链表的头插。
这其中有一个哈希表的扩容过程。检测就是,如果结点的个数和哈希表的长度一样大,则扩容。

		void CheckCapacity()
		{
			if (size_ == ht_.size())
			{
				size_t newsize = 2 * ht_.size();
				
				vector<pNode> newht;
				newht.resize(newsize);
				for (size_t i = 0; i < size_; ++i)
				{
					pNode cur = ht_[i];
					while (cur)
					{
						pNode next = cur->next_;
						size_t index = kov(cur->data_) % newsize;

						cur->next = newht[i];
						cur = next;
					}
					ht_[i] = nullptr;
				}
				ht_.swap(newht);
			}
		}

插入的具体实现

		bool Insert(const V& data)
		{
			CheckCapacity();
			//仿函数实现
			KeyOfVal kov;//获取V的K,通过数据获取key

			size_t index = kov(data) % ht_.size();
			pNode cur = ht_[index];
			while (cur)
			{
				if (kov(cur->data_) == kov(data))
					return false;
				cur = cur->next;
			}
			//头插
			cur = new Node(data);
			cur->next = ht_[index];
			ht_[index] = cur;
			++size_;
		}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值