0
点赞
收藏
分享

微信扫一扫

【开源】基于JAVA语言的二手车交易系统

 本期学习目标:了解unordered关联式容器,什么是哈希,哈希冲突怎么解决,哈希的模拟实现

一、unordered系列关联式容

1、undordered_map

常见的接口说明

unordered_map的构造:

函数声明功能介绍
unordered_map构造不同格式的unordered_map对象

unordered_map的容量:

函数声明功能介绍
bool empty() const检测unordered_map是否为空
size_t size() const获取unordered_map的有效元素个数

 unordered_map的迭代器:

函数声明功能介绍
begin返回unordered_map第一个元素的迭代器
end返回unordered_map最后一个元素下一个位置的迭代器
cbegin返回unordered_map第一个元素的const迭代器
cend返回unordered_map最后一个元素下一个位置的const迭代器

unordered_map的元素访问:

函数声明功能介绍
operator[]返回与key对应的value,没有一个默认值

注意:该函数中实际调用哈希桶的插入操作,用参数key与V()构造一个默认值往底层哈希桶 中插入,如果key不在哈希桶中,插入成功返回V()插入失败,说明key已经在哈希桶中, 将key对应的value返回

unordered_map的查询:

函数声明功能介绍
iterator find(const K& key)返回key在哈希桶中的位置
size_t count(const K& key)返回哈希桶中关键码为key的键值对的个数

 . unordered_map的修改操作 :

函数声明功能介绍
insert向容器中插入键值对
erase删除容器中的键值对
void clear()清空容器中有效元素个数
void swap(unordered_map&)交换两个容器中的元素

 unordered_map的桶操作:

函数声明功能介绍
size_t bucket_count()const返回哈希桶中桶的总个数
size_t bucket_size(size_t n)const返回n号桶中有效元素的总个数
size_t bucket(const K& key)返回元素key所在的桶号

 undordered_map最重要的功能是他的查找能力非常厉害,时间复杂度为 O(1)。

查找的运用:

这里我们将数组的元素入哈希表,然后遍历哈希表,键值对对中的value为N即是重复数 

class Solution {
public:
    int repeatedNTimes(vector<int>& nums)
    {
        sort(nums.begin(),nums.end());
        int n = nums.size()/2;
        unordered_map<int,int> counMap;
        for(auto& e:nums)
        {
            counMap[e]++;
        }
        for(auto& kv:counMap)
        {
            if(kv.second==n)
            {
                return kv.first;
            }
        }
        return -1;
    }
};

2、undordered_set

undordered_set和map接口基本相同,这里不在过多介绍,下面我们将对他们的相异点进行比对

  • 元素类型:

存储方式:

 使用方式:

接口差异:

迭代器:

unordered_ste去重的运用

 这里运用了unordered_set去重

class Solution {
public:
	vector<int> intersection(vector<int>& nums1, vector<int>& nums2) {

		// 用unordered_set对nums1中的元素去重
		unordered_set<int> s1;
		for (auto e : nums1)
			s1.insert(e);
		// 用unordered_set对nums2中的元素去重
		unordered_set<int> s2;
		for (auto e : nums2)
			s2.insert(e);
		// 遍历s1,如果s1中某个元素在s2中出现过,即为交集
		vector<int> vRet;
		for (auto e : s1)
		{
			if (s2.find(e) != s2.end())
				vRet.push_back(e);
		}
		return vRet;
	}
};

3、 有序关联容器和无序关联容器

区别总结:

 二、哈希

1、哈希概念

从前:

现在: 

结构模型:

简单的说,就是让元素的存储位置,形成一种映射,然后我们通过映射的关系很快找到该元素。 

 该方式即为哈希(散列)方法,哈希方法中使用的转换函数称为哈希(散列)函数,构造出来的结构称 为哈希表(Hash Table)(或者称散列表)

下面我们通过哈希函数,将我们要存放的值,通过映射关系存放。

但是如果我们继续按照上面的逻辑存放,44发生什么:

计算位置:hash(44)  =44%10=4,但是4的位置,我们不是已经存放了4了,这种现象我们称为

哈希冲突:

2、哈希函数

引起哈希冲突的一个原因可能是:哈希函数设计不够合理。

哈希函数设计原则:

常见的哈希函数:

直接定址法:

除留余数法 :

3、哈希冲突解决 

解决哈希冲突两种常见的方法是:闭散列和开散列

3.1 闭散列

 闭散列是通过线性探测的方法来解决哈希冲突的,那什么又是线性探测,这里我们还是以上面我们通过哈希函数重新插入44为例子:

插入:

线性探测:从发生冲突的位置开始,依次向后探测,直到寻找到下一个空位置为止。

删除

 线性探测的实现模拟实现:


//这里是为了保证进入哈希表的数据能够正常取模
//通用
template<class K>
struct HashFunc
{
	size_t operator()(const K& key)
	{
		return (size_t)key;
	}
};
namespace closehash
{
	enum State
	{
		EMPTY,//空
		EXIST,//存在
		DELETE,//删除
	};

	template<class K, class V>
	struct HashData
	{
		pair<K, V> _kv;
		State _state = EMPTY;//默认为空
	};

	template<class K, class V, class Hash = HashFunc<K>>
	class HashTable
	{
		typedef HashData<K, V> Data;
	public:
		HashTable()
			:_n(0)
		{
			_tables.resize(10);//默认哈希表中开10个空间
		}
		bool Insert(const pair<K, V>& kv)
		{
			//哈希表中存在相同的数就,不在插入
			if (Find(kv.first))
				return false;
			//当负载因子大于等于0.7,为了避免哈希冲突带来更多的消耗要扩容
			if (_n * 10 / _tables.size() >= 7)
			{
				HashTable<K, V, Hash> newHT;
				newHT._tables.resize(_tables.size() * 2);
				//插入数据到新的哈希表中
				for (auto& e : _tables)
				{
					if (e._state == EXIST)
					{
						newHT.Insert(e._kv);
					}
				}
				//交换新旧表指针
				_tables.swap(newHT._tables);
			}

			//找映射位置,存在就向后找空位置
			Hash hf;
			size_t hashi = hf(kv.first) % _tables.size();
			//找到空位置
			while (_tables[hashi]._state == EXIST)
			{
				++hashi;
				hashi %= _tables.size();
			}
			_tables[hashi]._kv = kv;
			_tables[hashi]._state = EXIST;
			++_n;
			return true;
		}
		Data* Find(const K& key)
		{
			Hash hf;
			size_t hashi = hf(key) % _tables.size();
			while (_tables[hashi]._state != EMPTY)
			{
				if (_tables[hashi]._state == EXIST &&
					_tables[hashi]._kv.first == key)
				{
					return &_tables[hashi];
				}
				//不在映射位置就在没有被占用的下一个位置
				hashi++;
				//控制在数组范围内找
				hashi %= _tables.size();
			}
			//到这里就没找到
			return nullptr;
		}
		bool Erase(const K& key)
		{
			Data* ret = Find(key);
			if (ret)
			{
				ret->_state = DELETE;
				--_n;
				return true;
			}
			else
			{
				false;
			}
		}
	private:
		vector<Data> _tables;
		size_t _n = 0;//表中有效数据的个数
	};
}

测试: 

对于上面的模拟实现,我们要注意一下细节:

为解决堆积问题的出现,可以进行二次探测 :思想是探测相隔较远的单元,而不是和原始位置相邻的单元

3.2 开散列

开散列法又叫链地址法(开链法),首先对关键码集合用散列函数计算散列地址,具有相同地址的关键码归于同一子集合,每一个子集合称为一个桶,各个桶中的元素通过一个单链表链 接起来,各链表的头结点存储在哈希表中。

开散列实现:

template<class K>
struct HashFunc
{
	size_t operator()(const K& key)
	{
		return (size_t)key;
	}
};
namespace buckethash
{
	template<class T>
	struct HashNode
	{
		T _data;
		HashNode<T>* _next;

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

	// 前置声明
	template<class K, class T, class Hash, class KeyOfT>
	class HashTable;
	//迭代器
	template<class K, class T, class Ref, class Ptr, class Hash, class KeyOfT>
	struct __HTIterator
	{
		typedef HashNode<T> Node;
		typedef __HTIterator<K, T, Ref, Ptr, Hash, KeyOfT> Self;

		typedef HashTable<K, T, Hash, KeyOfT> HT;
		Node* _node;
		HT* _ht;

		//构造函数
		__HTIterator(Node* node, HT* ht)
			:_node(node)
			, _ht(ht)
		{}

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

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

		bool operator != (const Self& s) const
		{
			return _node != s._node;
		}
		//++
		Self& operator++()
		{
			if (_node->_next)
			{
				_node = _node->_next;
			}
			else
			{
				//当前桶找完了
				KeyOfT kot;
				Hash hash;
				size_t hashi = hash(kot(_node->_data)) % _ht->_tables.size();
				++hashi;
				while (hashi < _ht->_tables.size())
				{
					if (_ht->_tables[hashi])
					{
						_node = _ht->_tables[hashi];
						break;//++完成
					}
					else
					{
						++hashi;
					}
				}
				//后面没有桶数据
				if (hashi == _ht->_tables.size())
				{
					_node = nullptr;
				}
			}
			return *this;
		}
	};

	//哈希表
	//K: 表示哈希表中键(Key)的类型。
	//T: 表示哈希表中值(Value)的类型。
	//Hash: 表示用于计算哈希值的哈希函数对象的类型。
	//KeyOfT: 表示一个用于从值 T 中提取键 K 的函数对象的类型。
	template<class K, class T, class Hash, class KeyOfT>
	class HashTable
	{
		typedef HashNode<T> Node;
		template<class K, class T, class Ref, class Ptr, class Hash, class KeyOfT>
		friend struct __HTIterator;

	public:
		typedef __HTIterator<K, T, T&, T*, Hash, KeyOfT> iterator;
		typedef __HTIterator<K, T, const T&, const T*, Hash, KeyOfT> const_iterator;

		iterator begin()
		{
			for (size_t i = 0; i < _tables.size(); i++)
			{
				if (_tables[i])
				{
					return iterator(_tables[i], this);
				}
			}
			return iterator(nullptr, this);
		}

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

		//构造函数
		HashTable()
			:_n(0)
		{
			_tables.resize(__stl_next_prime(0));//开默认的空间
		}

		//析构函数
		~HashTable()
		{
			for (int i = 0; i < _tables.size(); i++)
			{
				Node* cur = _tables[i];
				//释放桶
				while (cur)
				{


					Node* next = cur->_next;
					delete cur;
					cur = next;
				}
				_tables[i] = nullptr;
			}
		}

		pair<iterator, bool> Insert(const T& data)
		{
			KeyOfT kot;
			//表中有数据就不插入
			iterator it = Find(kot(data));
			if (it != end())
				return make_pair(it, false);
			// 负载因子控制在1,超过就扩容
			if (_tables.size() == _n)
			{
				vector<Node*> newTables;
				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 = Hash()(kot(cur->_data)) % newTables.size();
						//头插入到新链表
						cur->_next = newTables[hashi];
						newTables[hashi] = cur;
						cur = next;
					}
					_tables[i] = nullptr;
				}
				_tables.swap(newTables);
			}
			//插入
			size_t hashi = Hash()(kot(data)) % _tables.size();
			Node* newnode = new Node(data);
			//继续头插
			newnode->_next = _tables[hashi];
			_tables[hashi] = newnode;
			++_n;
			return make_pair(iterator(newnode, this), true);
		}
		//查找
		iterator Find(const K& key)
		{
			KeyOfT kot;
			size_t hashi = Hash()(key) % _tables.size();
			Node* cur = _tables[hashi];
			while (cur)
			{
				if (kot(cur->_data) == key)
				{
					return iterator(cur, this);//this 指针代表当前对象(即哈希表对象)的地址
				}
				else
				{
					cur = cur->_next;
				}
			}
			return end();
		}

		bool Erase(const K& key)
		{
			KeyOfT kot;
			size_t hashi = Hash()(key) % _tables.size();
			Node* prev = nullptr;
			Node* cur = _tables[hashi];
			//删除
			while (cur)
			{
				if (kot(cur->_data) == key)
				{
					//删除
					if (cur == _tables[hashi])
					{
						_tables[hashi] = cur->_next;
					}
					else
					{
						prev->_next = cur->_next;
					}
					//找到删除 
					delete cur;
					--_n;
					return true;
				}
				else
				{
					prev = cur;
					cur = cur->_next;
				}
			}
			return false;
		}
		//确保哈希表的大小是一个质数,从而提高散列性能。
		inline unsigned long __stl_next_prime(unsigned long n)
		{
			static const int __stl_num_primes = 28;
			static const unsigned long __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 (int i = 0; i < __stl_num_primes; ++i)
			{
				if (__stl_prime_list[i] > n)
				{
					return __stl_prime_list[i];
				}
			}

			return __stl_prime_list[__stl_num_primes - 1];
		}
	private:
		vector<Node*> _tables;//指针数组
		size_t _n;

	};
}

上面一连串的模拟实现,大家可能会看的有点费劲,其实的实现思路是非常简单的,就是创建一个指针数组,指针数组中放定义的哈希桶。但是实现起来细节却是非常多的,

细节问题

 1、哈希表怎样进行正常的取模?

大家心里可能会想,不直接对数据进行取模不就行了,数据如果是整形进行取模,但是如果数据是字符、字符串、自定义对象呢?

这里我们就要进行复杂的hashfun进行取模值的获取

可隐式类型转换哈希函数

template<class K>
struct HashFunc
{
	size_t operator()(const K& key)
	{
		return (size_t)key;
	}
};

 字符串哈希函数

template<class K>
struct HashFunc
{
    size_t operator()(const K& key)
    {
        size_t hashValue = 0;

        for (char c : key) {
            // 加法哈希
            hashValue = (hashValue * 31) + static_cast<size_t>(c);
        }

        return hashValue;
    }
};

2、开散列在什么情况下进行扩容

开散列最好的情况是:每个哈希桶中刚好挂一个节点, 再继续插入元素时,每一次都会发生哈希冲突,因此,在元素个数刚好等于桶的个数时,可以给哈希表增容

3.3 开散列与闭散列比较

应用链地址法处理溢出,需要增设链接指针,似乎增加了存储开销。事实上: 由于开地址法必须保持大量的空闲空间以确保搜索效率,如二次探查法要求装载因子a <= 0.7,而表项所占空间又比指针大的多,所以使用链地址法反而比开地址法节省存储空间。

三、哈希的应用

1、位图操作

所谓位图,就是用每一位来存放某种状态,适用于海量数据,数据无重复的场景。通常是用 来判断某个数据存不存在的。

我们先看一道面试题目:


问题1:给40亿个不重复的无符号整数,没排过序。给一个无符号整数,如何快速判断一个数是否在 这40亿个数中。


思路: 

数据是否在给定的整形数据中,结果是在或者不在,刚好是两种状态,那么可以使用一 个二进制比特位来代表数据是否存在的信息,如果二进制比特位为1,代表存在,为0 代表不存在。比如 

位图的实现:

template<size_t N>
	class bitset
	{
	public:
		bitset()
		{
			//初始化位图空间
			_bits.resize((N >> 3) + 1, 0);
		}
		void set(size_t x)
		{
			size_t i = x >> 3;
			size_t j = x % 8;
			_bits[i] |= (1 << j);
		}
		void reset(size_t x)
		{
			size_t i = x / 8;//x位于第几个字符
			size_t j = x % 8;//x位于第i个字符的第j位
			_bits[i] &= (~(1 << j));//1 << j 会创建一个只有第 j 位为 1 的数值
		}
		//测试
		bool test(size_t x)
		{
			size_t i = x >> 3;
			size_t j = x % 8;

			return _bits[i] & (1 << j);
		}

	private:
		vector<char> _bits;
	};

在这段代码中,使用位操作 (N >> 3)(等价于 N / 8)而不是直接的除法操作是因为这样的做法更为高效。使用位操作在某些情况下能够提高代码的执行效率,尤其是在涉及到计算机底层的位运算时。

  1. 位移操作的效率更高: 在许多计算机体系结构中,位移操作(>><<)通常比除法操作更为高效。对于2的幂次方的除法,位移操作是特别快速的。因此,将 N 右移3位(相当于除以8)可以更有效地计算出 N 除以8的结果。

  2. 代码的可读性: 通过使用位操作,可以传达一种意图,即在这里我们只关心字节的偏移,而不是简单的数学除法。这种表达方式更能突显代码的目的,即在位图中存储位信息。

那这里我们是如何将数据和位图进行映射的呢,假设输入的数据为x怎么在位图是表示?

 位图的应用

2、布隆过滤器

我们在使用新闻客户端看新闻时,它会给我们不停地推荐新的内容,它每次推荐时要去重,去掉 那些已经看过的内容。问题来了,新闻客户端推荐系统如何实现推送去重的? 用服务器记录了用 户看过的所有历史记录,当推荐系统推荐新闻时会从每个用户的历史记录里进行筛选,过滤掉那 些已经存在的记录。 如何快速查找呢?

对于去重,我们肯定会想到,用哈希表去存放,用户看过的信息,但是这样会造成大量的空间浪费,而位图又一般只能处理整形。

这时候有人就想到将哈希与位图结合,即布隆过滤器。

布隆过滤器是由布隆(Burton Howard Bloom)在1970年提出的 一种紧凑型的、比较巧妙的概 率型数据结构,特点是高效地插入和查询,可以用来告诉你 “某样东西一定不存在或者可能存 在”,它是用多个哈希函数,将一个数据映射到位图结构中。此种方式不仅可以提升查询效率,也 可以节省大量的内存空间。

布隆过滤器插入:

 布隆过滤器的查找

布隆过滤器的思想是将一个元素用多个哈希函数映射到一个位图中,因此被映射到的位置的比特 位一定为1。所以可以按照以下方式进行查找:分别计算每个哈希值对应的比特位置存储的是否为 零,只要有一个为零,代表该元素一定不在哈希表中否则可能在哈希表中。 注意:布隆过滤器如果说某个元素不存在时,该元素一定不存在,如果该元素存在时,该元素可 能存在,因为有些哈希函数存在一定的误判。 比如:在布隆过滤器中查找"你好"时,假设3个哈希函数计算的哈希值为:3、5、7,刚好和其 他元素的比特位重叠,此时布隆过滤器告诉该元素存在,但实该元素是不存在的。

布隆过滤器删除

布隆过滤器不能直接支持删除工作,因为在删除一个元素时,可能会影响其他元素。

缺陷: 1. 无法确认元素是否真正在布隆过滤器中 2. 存在计数回绕

布隆过滤器优点:

布隆过滤器缺陷 :

简单实现:

struct BKDRHash
{
	size_t operator()(const string& s)
	{
		// BKDR
		size_t value = 0;
		for (auto ch : s)
		{
			value *= 31;
			value += ch;
		}
		return value;
	}
};
struct APHash
{
	size_t operator()(const string& s)
	{
		size_t hash = 0;
		for (long i = 0; i < s.size(); i++)
		{
			if ((i & 1) == 0)
			{
				hash ^= ((hash << 7) ^ s[i] ^ (hash >> 3));
			}
			else
			{
				hash ^= (~((hash << 11) ^ s[i] ^ (hash >> 5)));
			}
		}
		return hash;
	}
};
struct DJBHash
{
	size_t operator()(const string& s)
	{
		size_t hash = 5381;
		for (auto ch : s)
		{
			hash += (hash << 5) + ch;
		}
		return hash;
	}
};
template<size_t N,
	size_t X = 5,
	class K = string,
	class HashFunc1 = BKDRHash,
	class HashFunc2 = APHash,
	class HashFunc3 = DJBHash>
class BloomFilter
{
public:
	void Set(const K& key)
	{
		size_t len = X * N;
		size_t index1 = HashFunc1()(key) % len;
		size_t index2 = HashFunc2()(key) % len;
		size_t index3 = HashFunc3()(key) % len;
		/* cout << index1 << endl;
		cout << index2 << endl;
		cout << index3 << endl<<endl;*/
		_bs.set(index1);
		_bs.set(index2);
		_bs.set(index3);
	}
	bool Test(const K& key)
	{
		size_t len = X * N;
		size_t index1 = HashFunc1()(key) % len;
		if (_bs.test(index1) == false)
			return false;
		size_t index2 = HashFunc2()(key) % len;
		if (_bs.test(index2) == false)
			return false;
		size_t index3 = HashFunc3()(key) % len;
		if (_bs.test(index3) == false)
			return false;
		return true;  // 存在误判的
	}
	// 不支持删除,删除可能会影响其他值。
	void Reset(const K& key);
private:
	bitset<X* N> _bs;
};
举报

相关推荐

0 条评论