大数查重(哈希表&位图&Bloom Filter)

一、哈希表

1. 哈希表理论知识

有序关联容器:set、multiset、map、multimap,底层用红黑树
无序关联容器:unordered_set和unordered_map,底层用链式哈希表。

  • 面试介绍哈希表:一个关键字,经过散列函数进行映射,得到的它在表中的存储位置,符合这样的key及其映射关系后得到在表中的存储位置,这样的表就叫做散列表或者哈希表。然后可以解释哈希冲突的问题,再解释解决哈希冲突的线性探测哈希表和链式哈希表。

在这里插入图片描述
哈希表的时间复杂度趋近于O(1),由于哈希冲突的增加,可能会达到O(n)
在这里插入图片描述

  • 优势:适用于快速查找,时间复杂度趋近于O(1)
  • 缺点:占用内存空间比较大,空间效率不高
  • 散列函数
    在这里插入图片描述
  • 散列冲突处理
    在这里插入图片描述

2. 线性探测哈希表

在这里插入图片描述
在这里插入图片描述
代码实现

#include<iostream>
using namespace std;

enum State
{
	STATE_UNUSE,//从未使用过的桶
	STATE_USING,//正在使用的桶
	STATE_DEL,//元素被删除的桶
};
class Bucket
{
public:
	Bucket(int key=0,State state=STATE_UNUSE)
		:_key(key),_state(state){}
	int _key;//存储数据
	State _state;//存储状态
};
class HashTable
{
private:
	Bucket* _table;//动态开辟的哈希表
	int _tableSize;//桶的大小
	int _useBucketNum;//正在使用的桶个数
	double _loadFactor;//装载因子
	//在C++11以后,静态常量的整型类型在类里面可以直接初始化
	static const int _primeSize = 10;//素数大小
	static int _primes[_primeSize];//素数表
	int _primeIdx;//当前使用的素数下标
	void expand()
	{
		++_primeIdx;
		if (_primeIdx == _primeSize)
		{
			throw "hashtable is too large,can not expand anymore";
		}
		Bucket* newTable = new Bucket[_primes[_primeIdx]];
		for (int i = 0; i < _tableSize; i++)
		{
			if (_table[i]._state == STATE_USING)
			{
				int idx = _table[i]._key % _primes[_primeIdx];
				int k = idx;
				do
				{
					if (newTable[k]._state == STATE_UNUSE)
					{
						newTable[k]._key = _table[i]._key;
						newTable[k]._state = STATE_USING;
						break;
					}
					k = (k + 1) % _primes[_primeIdx];
				} while (k != idx);
			}
		}
		delete[]_table;
		_table = newTable;
		_tableSize = _primes[_primeIdx];
	}
public:
	HashTable(int size = _primes[0], double loadFactor = 0.75)
		:_useBucketNum(0), _loadFactor(loadFactor), _primeIdx(0)
	{
		//把用户传入的size调整到最近的比较大的素数上
		if (size != _primes[0])
		{
			for (; _primeIdx < _primeSize; _primeIdx++)
			{
				if (_primes[_primeIdx] > size)
				{
					break;
				}
			}
			//用户传入的size过大已经超过了最后一个数,调整为最后一个数
			if(_primeIdx == _primeSize)
				_primeIdx--;
		}
		_tableSize = _primes[_primeIdx];
		_table = new Bucket[_tableSize];
	}
	~HashTable()
	{
		delete[]_table;
		_table = nullptr;
	}
	bool insert(int key)
	{
		//考虑扩容
		double factor = _useBucketNum * 1.0 / _tableSize;
		if (factor > 0.75)
		{
			expand();
		}
		int idx = key % _tableSize;
		int i = idx;
		do
		{
			if (_table[i]._state == STATE_UNUSE)
			{
				_table[i]._key = key;
				_table[i]._state = STATE_USING;
				_useBucketNum++;
				return true;
			}
			i = (i + 1) % _tableSize;
		} while (i != idx);
	}
	bool erase(int key)
	{
		int idx = key % _tableSize;
		int i = idx;
		do
		{
			if (_table[i]._state == STATE_USING && _table[i]._key == key)
			{
				_table[i]._state = STATE_DEL;
				_useBucketNum--;
				break;
			}
			i = (i + 1) % _tableSize;
		} while (_table[i]._state != STATE_UNUSE && i != idx);
		return true;
	}
	bool find(int key)
	{
		int idx = key % _tableSize;
		int i = idx;
		do
		{
			if (_table[i]._key == key && _table[i]._state == STATE_USING)
			{
				return true;
			}
			i = (i + 1) % _tableSize;
		} while (_table[i]._state != STATE_UNUSE && i != idx);
		return false;
	}
};
int HashTable::_primes[_primeSize] = { 3,7,23,47,97,251,443,911,1471,42773 };

int main()
{
	HashTable htable;
	htable.insert(12);
	htable.insert(24);
	htable.insert(38);
	htable.insert(15);
	htable.insert(14);
	cout << htable.find(12) << endl;
	htable.erase(12);
	cout << htable.find(12) << endl;
}

3. 链式哈希表

在这里插入图片描述

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

class HashTable
{
private:
	vector<list<int>> _table;
	int _useBucketNum;
	double _loadFactor;
	static const int _primeSize = 10;
	static int _primes[_primeSize];
	int _primeIdx;
	void expand()
	{
		++_primeIdx;
		if (_primeIdx == _primeSize)
			throw "hashtable is too large,can not expand anymore";
		vector<list<int>> oldTable;
		_table.swap(oldTable);
		_table.resize(_primes[_primeIdx]);
		for (auto list : oldTable)
		{
			for (auto key : list)
			{
				int idx = key % _primes[_primeIdx];
				if (_table[idx].empty())
				{
					_useBucketNum++;
				}
				_table[idx].emplace_front(key);
			}
		}

	}
public:
	HashTable(int size = _primes[0], double loadFactor = 0.75)
		:_useBucketNum(0), _loadFactor(loadFactor), _primeIdx(0)
	{
		if (size != _primes[0])
		{
			for (; _primeIdx < _primeSize; ++_primeIdx)
			{
				if (_primes[_primeIdx] > size)
				{
					break;
				}
			}
			if (_primeIdx == _primeSize)
			{
				_primeIdx--;
			}
		}
		_table.resize(_primes[_primeIdx]);
	}
	void insert(int key)
	{
		double factor = _useBucketNum * 1.0 / _table.size();
		cout << factor << endl;
		if (factor > 0.75)
		{
			expand();
		}
		int idx = key % _table.size();
		if (_table[idx].empty())//O(1)
		{
			_useBucketNum++;
			_table[idx].emplace_front(key);
		}
		else
		{
			auto it = ::find(_table[idx].begin(), _table[idx].end(), key);//O(n)
			if (it == _table[idx].end())
			{
				//key不存在
				_table[idx].emplace_front(key);
			}

		}
	}
	void erase(int key)
	{
		int idx = key % _table.size();//O(1)
		//如果链表节点过长,如果散列函数比较集中,(散列函数有问题!)
		//				  如果散列比较离散,链表长度一般不会过长,因为有装载因子
		auto it = ::find(_table[idx].begin(), _table[idx].end(), key);//O(n)
		if (it != _table[idx].end())
		{
			_table[idx].erase(it);
			if (_table[idx].empty())
			{
				_useBucketNum--;
			}
		}
	}
	bool find(int key)
	{
		int idx = key % _table.size();//O(1)
		auto it = ::find(_table[idx].begin(), _table[idx].end(), key);//O(n)
		return it != _table[idx].end();
	}
};
int HashTable::_primes[_primeSize] = { 3,7,23,47,97,251,443,911,1471,42773 };
int main()
{
	HashTable htable;
	htable.insert(12);
	htable.insert(24);
	htable.insert(38);
	htable.insert(15);
	htable.insert(14);
	htable.insert(40);
	htable.insert(93);
	cout << htable.find(12) << endl;
	htable.erase(12);
	cout << htable.find(12) << endl;
}

vec1.swap(vec2):如果两个容器使用的空间配置器allocator是一样的,那么直接交换两个容器的成员变量即可,效率高!如果两个容器使用的空间配置器allocator是不一样的,那么意味着两个容器管理外部堆内存的方式不一样,需要效率低的整个数据的交换。

4. 哈希表应用

查重或者统计重复的次数,查询的效率高但是占用内存空间较大。

(1)找出第一个重复出现的数字
#include<iostream>
#include<time.h>
#include<vector>
#include<unordered_set>
#include<unordered_map>
using namespace std;

int main()
{
	vector<int> vec;
	srand(time(0));
	for (int i = 0; i < 1000; i++)
	{
		vec.push_back(rand() % 1000 + 1);
	}
	unordered_set<int> s1;
	for (auto key : vec)
	{
		auto it = s1.find(key);
		if (it == s1.end())
		{
			s1.insert(key);
		}
		else
		{
			cout << "key:" << key << endl;
			break;
		}
	}
	return 0;
}
(2)找出所有重复的数字
#include<iostream>
#include<time.h>
#include<vector>
#include<unordered_set>
#include<unordered_map>
using namespace std;

int main()
{
	vector<int> vec;
	srand(time(0));
	for (int i = 0; i < 1000; i++)
	{
		vec.push_back(rand() % 1000 + 1);
	}
	unordered_set<int> s1;
	for (auto key : vec)
	{
		auto it = s1.find(key);
		if (it == s1.end())
		{
			s1.insert(key);
		}
		else
		{
			cout << "key:" << key << endl;
		}
	}
	return 0;
}
(3)统计重复出现的数字以及出现的次数
#include<iostream>
#include<time.h>
#include<vector>
#include<unordered_set>
#include<unordered_map>
using namespace std;

int main()
{
	vector<int> vec;
	srand(time(0));
	for (int i = 0; i < 1000; i++)
	{
		vec.push_back(rand() % 1000 + 1);
	}
	unordered_map<int, int> m1;
	for (auto key : vec)
	{
		/*
		auto it = m1.find(key);
		if (it == m1.end())
		{
			m1.emplace(key, 1);
		}
		else
		{
			it->second += 1;
		}
		*/
		m1[key]++;
	}
	for (auto pair : m1)
	{
		if (pair.second>1)
		{
			cout << "key:" << pair.first << "  num:" << pair.second << endl;
		}
	}
	return 0;
}

(4)一组数据有些数字是重复的,把重复的数字过滤掉,每个数字只出现一次
#include<iostream>
#include<time.h>
#include<vector>
#include<unordered_set>
#include<unordered_map>
using namespace std;

int main()
{
	vector<int> vec;
	srand(time(0));
	for (int i = 0; i < 1000; i++)
	{
		vec.push_back(rand() % 1000 + 1);
	}
	unordered_set<int> s1;
	for (auto key : vec)
	{
		s1.emplace(key);
	}
	return 0;
}

(5)找出第一个没有重复出现的字符
#include<iostream>
#include<time.h>
#include<vector>
#include<unordered_set>
#include<unordered_map>
#include<string.h>
using namespace std;

int main()
{
	string s = "dwefwerfcsadwar";
	unordered_map<int, int> m1;
	for (auto ch : s)
	{
		m1[ch]++;
	}
	for (auto ch : s)
	{
		if (m1[ch] == 1)
		{
			cout << ch << endl;
			return 0;
		}
	}
	cout << "所有字符都重复出现过" << endl;
	return 0;
}

(6)有两个文件分别是a和b,里面放了很多ip地址(url地址、email地址),让你找出两个文件重复的ip并输出出来。

(7)有两个文件分别是a和b,各自存放约1亿条ip地址,每个ip地址是4B, 限制使用100M,让找出来两个文件中重复的ip地址并输出。

采用分治思想。
在这里插入图片描述

二、 位图算法

在这里插入图片描述
数字是否出现过的状态,存储在一个位数组当中。
位图数组的长度:根据最大值来确定位图数组的长度,要找出这组数字中的最大值, 因为小的值在位图数组里肯定是靠前存放,大的值肯定是靠后存放,只要保证大的位能够表示出来,那么前面这些小的位肯定能表示出来。
在这里插入图片描述
问题:
在这里插入图片描述
用哈希表解决
在这里插入图片描述
用位图算法

在这里插入图片描述
可以发现相比哈希表,位图算法非常省内存的,但是它有一些限制,需要知道数组中的最大值。

位图数组代码实现:

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

int main()
{
	vector<int> vec = { 12,34,56,12,43,67,87,62,43 };
	//定义位图数组
	int max=vec[0];
	for (auto key : vec)//O(n)
	{
		if (max < key)
		{
			max = key;
		}
	}
	//定义的位图数组所有位要初始化0,表示元素没出现过
	int* bitmap = new int[max / 32 + 1]();
	unique_ptr<int> ptr(bitmap);
	for (auto key : vec)
	{
		int index = key / 32;
		int offset = key % 32;
		//取k对应的位的值
		if (0 == (bitmap[index] & (1 << offset)))
		{
			bitmap[index] |= (1 << offset);
		}
		else
		{
			//找第一个重复出现的数
			//cout << key << "是第一个重复出现的数字" << endl;
			//break;
			
			//找所有重复出现过的数
			cout << key << "重复出现过" << endl;
		}
	}
	return 0;
}

位图算法的缺陷:

  1. 不好处理谁是第一个不重复的,因为找第一个不重复的要统计次数,但是在位图数组里只能记录是否出现过,而不能记录出现了几次,表示不了这么多状态。可以这样做:原来用1个位保存数据的状态,现在用2个位保存数据的状态,比较麻烦。
  2. 比如有一组数据1,3,1000000000
    在这里插入图片描述
    位图算法推荐使用的场景:数据的个数>=序列里面数字的最大值

三、 布隆过滤器Bloom Filter

布隆过滤器在大数据查重、缓存服务器redis里面、黑名单过滤、钓鱼网站过滤、url过滤这些场景中非常常见。

布隆过滤器可以弥补哈希表占用内存空间比较大,位图确实省空间,但是存在像1,3,1000000这种问题比较浪费空间。布隆过滤器可以结合位图和哈希,是一种更高级的位图解决方案,效率高而且省内存。
在这里插入图片描述
Bloom Filter注意事项:
在这里插入图片描述

在这里插入图片描述

在这里插入图片描述
只要能接受一定的误判率用布隆过滤器也是可以的。
在这里插入图片描述
在这里插入图片描述
代码实现:

class BloomFilter
{
private:
	int _bitSize;//位图长度
	vector<int> _bitMap;//位图数组
public:
	BloomFilter(int bitSize = 1471)
		:_bitSize(bitSize)
	{
		_bitMap.resize(bitSize / 32 + 1);
	}
	//添加元素 O(1)
	void setBit(const char* str)
	{
		//计算k组哈希函数的值
		int idx1 = BKDHash(str) % _bitSize;
		int idx2 = RSHash(str) % _bitSize;
		int idx3 = APHash(str) % _bitSize;

		//把相应的idx1,idx2,idx3这几位全置为1
		int index = 0;
		int offset = 0;

		index = idx1 / 32;
		offset = idx1 % 32;
		_bitMap[index] |= (1 << offset);

		index = idx2 / 32;
		offset = idx2 % 32;
		_bitMap[index] |= (1 << offset);

		index = idx3 / 32;
		offset = idx3 % 32;
		_bitMap[index] |= (1 << offset);
	}
	//查询元素 O(1)
	bool getBit(const char* str)
	{
		//计算k组哈希函数的值
		int idx1 = BKDHash(str) % _bitSize;
		int idx2 = RSHash(str) % _bitSize;
		int idx3 = APHash(str) % _bitSize;

		int index = 0;
		int offset = 0;

		index = idx1 / 32;
		offset = idx1 % 32;
		if (0 == (_bitMap[index] & (1 << offset)))
		{
			return false;
		}

		index = idx2 / 32;
		offset = idx2 % 32;
		if (0 == (_bitMap[index] & (1 << offset)))
		{
			return false;
		}

		index = idx3 / 32;
		offset = idx3 % 32;
		if (0 == (_bitMap[index] & (1 << offset)))
		{
			return false;
		}
		return true;
	}
};

//URL黑名单
class BlackList
{
private:
	BloomFilter _blockList;
public:
	void add(string url)
	{
		_blockList.setBit(url.c_str());
	}
	bool query(string url)
	{
		return _blockList.getBit(url.c_str());
	}
};

int main()
{
	BlackList list;
	list.add("http://www.baidu.com");
	list.add("http://www.360buv.com");
	list.add("http://www.tmall.com");
	list.add("http://www.tencent.com");

	string url = "http://www.tencent.com";
	cout << list.query(url) << endl;

	return 0;
}
  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值