算法基础提升——哈希函数和并查集(C++)

一、认识哈希函数和哈希表

1.1 哈希函数

  1. 哈希函数是一个不定长输入,定长输出的函数,简单来讲,给哈希函数导入任意长度的数据,它都会输出一个固定长度的哈希值。例如MD5,SHA1就是常用的哈希函数,MD5输出长度固定为0~ 2^{64}-1,SHA1输出长度固定为0~ 2^{128}-1
  2. 其次,哈希函数是相同输入,相同输出的函数,也就是不含有随机性,有固定的运算规则。
  3. 但是由于输入可以是无限长度的,所有必然导致不同输入会对应相同输出。这种特点叫做哈希碰撞,虽然理论上可能发生,但实际上发生概率很低。
  4. 离散性和均匀性:离散性是指相似输入会导致完全不一样的输出,也就是说不会存在输入相邻的两个数,输出相邻两个数的情况;均匀性是指所有产生的哈希值完全均匀的分布在整个域中,也就是给出大量相似数据,并计算哈希值,哈希值一定是均匀分布在整个域上的,不会大量聚集在某一处。

1.2 哈希表

哈希表也叫散列表,哈希表是一种数据结构,它提供了快速的插入操作和查找操作,无论哈希表总中有多少条数据,插入和查找的时间复杂度都是为O(1),所以哈希表的查找速度非常快。

哈希表的实现原理:先定义一个哈希表,假设该哈希表有17个空间(0~16)。将所要存储的字符串过一遍哈希函数,得到哈希值out1;然后将哈希值out1除以17取模,假设模为7,那么将这个字符串用链表的方式串联在7号空间上。因为哈希函数的均匀性,所以每个空间后面串联的结点数也是均匀的。

如果哈希表的每个空间中都串联了大量结点,这样会严重影响查找速度(因为要遍历结点)。所以当链的长度到达一个值k后,将哈希表扩容一倍(34个空间)。扩容过程的时间复杂度是O(logN),当k的值越大,时间复杂度越趋近于O(1).

二、找到出现次数最多的数字

2.1 问题介绍

现在有40亿个数,数据均匀分布在0 ~ 2^{32}-1上。要求在1G内存限制下,统计每个数字的出现次数,并输出出现次数最多的数字。

2.2 解题思路

若直接采用键值对来解题:用key来存储数字(4B),用value来存储数字出现次数(4B),那么最差情况下(所有数字都不一样),需要8B*40亿大小的空间,约为32G,明显不行。

采用分治+哈希函数:先将每个数除以100取模,将所有数字分成100组,显然相同的数字会出现在同一组内,然后我们从第一组开始统计每个数字出现的次数,记录出现次数最多的数,释放内存;统计第二组中每个数字出现的次数,记录出现次数最多的数,释放内存……最终将每组中出现次数最多的数做比较,得到答案。使用这种方法只需要大约0.32G内存。

由于哈希函数的性质,数字转换后的哈希值肯定会均匀分布,因此不会出现某一组中数据特别多的情况。另外相同的数字肯定会得到相同的哈希值,因此不会出现同一个数转换成不同哈希值,被分到不同组的情况。

关于碰撞的问题,需要明确针对每一组数字,会先算%10==0的所有数字,放到内存里面,然后统计出现次数最多那个,保留这个数据,其他数据释放掉,开始算%10==1的数据,以此类推,会不断精细操作。忽略不同数计算得到同一哈希值的碰撞问题。


三、设计RandomPool结构

3.1 问题介绍

【题目】设计一种结构,在该结构中有如下三个功能:

  • insert(key):将某个key加入到该结构,做到不重复加入。
  • delete(key):将原本在结构中的某个key移除。
  • getRandom(): 等概率随机返回结构中的任何一个key。

【要求】 Insert、delete和getRandom方法的时间复杂度都是O(1)

3.2 解题思路

参考:http://t.csdnimg.cn/lk2uj

具体操作:

  1. 针对insert我们可以实现两个map同步操作,一个插入(key, index),另外一个插入(index, key),然后使用size计数即可,保持同步。
  2. 针对getRandom,虽然Hash表返回的是近似等概率的,但是不是严格等概率的,所有我们利用随机数从(index, key)中得到一个key。
  3. 针对delete操作,我们确实可以直接在(key, index)进行操作,但是这样我们在使用getRandom函数之后它会产生空洞了,所以一种思路就是我们可以借助最后一行(key,index)进行赋值给需要删除的key,这样就可以消除空洞。

3.3 代码实现

#include<cstdio>
#include<iostream>
#include<cstring>
#include<algorithm>
#include<cmath>
#include<unordered_map>
#include<cstdlib>
using namespace std;

class RandomPool {
public:
    unordered_map<string, int> keyIndexMap;
    unordered_map<int, string> indexKeyMap;
    int size;
    RandomPool() : size(0) {}   // default constructor

    void insertKey(string key) {
        if (keyIndexMap.find(key) == keyIndexMap.end()) {   // if don't have key
            keyIndexMap.emplace(key, size);  // we can also insert({key, size}) instead
            indexKeyMap.emplace(size, key);
            size++;
        }
    }

    void deleteKey(string key) {
        if (keyIndexMap.find(key) != keyIndexMap.end()) {  // if we have key
            int deleteIndex = keyIndexMap.at(key);  // find we we want to delete the index
            int lastIndex = --size;  // last index
            string lastKey = indexKeyMap.at(lastIndex);    // find the last key
            keyIndexMap.erase(key);
            keyIndexMap.erase(lastKey);
            indexKeyMap.erase(deleteIndex);
            indexKeyMap.erase(lastIndex);
            keyIndexMap.emplace(lastKey, deleteIndex);
            indexKeyMap.emplace(deleteIndex, lastKey);
        }
    }

    string getRandomKey() {
        int random = rand() % size;     // get [0, size-1]
        return indexKeyMap.at(random);  // we can also use indexKeyMap[random] instead
    }
};
int main()
{
    RandomPool randomPool;
    randomPool.insertKey("A");
    randomPool.insertKey("B");
    randomPool.insertKey("C");
    cout << "===================Insert key===================" << endl;
    cout << "keyIndexMap: " << endl;
    for (auto& it : randomPool.keyIndexMap) {
        cout << it.first << ": " << it.second << endl;
    }
    cout << "indexKeyMap: " << endl;
    for (auto it = randomPool.indexKeyMap.begin(); it != randomPool.indexKeyMap.end(); ++it) {
        cout << it->first << ": " << it->second << endl;
    }
    cout << "===================Random key===================" << endl;
    string randomKey1 = randomPool.getRandomKey();
    string randomKey2 = randomPool.getRandomKey();
    string randomKey3 = randomPool.getRandomKey();
    cout << "key1: " << randomKey1 << "\n" << "key2: " << randomKey2 << "\n" << "key3: " << randomKey3 << endl;

    cout << "===================Delete key===================" << endl;
    randomPool.deleteKey("A");  // delete "A"
    cout << "keyIndexMap: " << endl;
    for (auto& it : randomPool.keyIndexMap) {
        cout << it.first << ": " << it.second << endl;
    }
    cout << "indexKeyMap: " << endl;
    for (auto it = randomPool.indexKeyMap.begin(); it != randomPool.indexKeyMap.end(); ++it) {
        cout << it->first << ": " << it->second << endl;
    }
    cout << "==================After delete random key========" << endl;
    string randomKey11 = randomPool.getRandomKey();
    string randomKey22 = randomPool.getRandomKey();
    string randomKey33 = randomPool.getRandomKey();
    cout << "key1: " << randomKey11 << "\n" << "key2: " << randomKey22 << "\n" << "key3: " << randomKey33 << endl;
    return 0;
}

 四、布隆过滤器

4.1 问题介绍

布隆过滤器(Bloom Filter)实际上是一个很长的二进制向量和一系列随机映射函数。布隆过滤器可以用于检索一个元素是否在一个集合中。
优点:

  • 可以高效地进行查询,可以用来告诉你“某样东西一定不存在或者可能存在”
  • 可以高效的进行插入(但是不能删除)
  • 相比于传统的List、Set、Map等数据结构,它占用空间更少,因为其本身并不存储任何数据(重点)
  • 适用于海量数据的查找(亿量级)

缺点:

  • 不能提供删除操作
  • 存在失误概率:将白名单中的内容误认为是黑名单中的(反之则不可能失误)。

例如:网络爬虫程序,为了不去爬相同的URL页面,需要记录下所有爬过的URL(极大数据量),在爬一个新的网页时,判断是否已经爬过。

4.2 解题思路

参考:http://t.csdnimg.cn/xJMl4

  1. 定义一个m个空间大小的哈希表(0~m-1)。
  2. 将每个URL分别通过K个不同的哈希函数,并除以m取模,将得到的所有值都在哈希表上表示出来。(例如得到1,2,4,6……那么哈希表的1,2,4,6……空间上都涂黑。如果已经涂黑了则不进行操作)。
  3. 当查验一个URL是否在表上时,先将该URL通过K个哈希函数,并除以m取模,如果得到的所有值都在哈希表上被涂黑了,那么这个URL在表上。(例如得到1,2,4,6……但哈希表6号空间未被涂黑,说明此URL不在表上)。

重点在于如何确定m和K的值。

  • m = -n*lnP /(ln2)^2  (其中n为样本量,P为失误率)。
  • K = ln2 *n/m   (其中n为样本量,m为空间)。
  • P = (1-e^{-n*K/m})^K  (其中P为实际失误率 )。

4.3 代码实现

#include <iostream>
#include <unordered_map>
#include <string>
#include <sys/time.h>
#include <utility>
#include <iomanip>
 
#define MAP_ITEMS 100000
 
using namespace std;
 
int main()
{
	unordered_map<string, bool> unordermp;
	
	timeval startTime, endTime;
	
	//1.插入MAP_ITEMS个元素到map中
	gettimeofday(&startTime, NULL);
	std::string key = "https://blog.csdn.net/qq_41453285";
	for(int i = 0; i < MAP_ITEMS; ++i){
		string sub_key = to_string(i);
		unordermp.insert(std::make_pair(key + sub_key, 1));
	}
	
	gettimeofday(&endTime, NULL);
	long insert_time = (endTime.tv_sec - startTime.tv_sec)*1000 + (endTime.tv_usec-startTime.tv_usec)/1000;
	
	//2.在map中查找一个元素
	gettimeofday(&startTime, NULL);
	if( unordermp.find(key + "10000") == unordermp.end())
		std::cout << "not found!" << std::endl;
	
	gettimeofday(&endTime, NULL);
	long find_time = endTime.tv_usec - startTime.tv_usec;
	
	//3.估算当前key的平均大小
	double key_size = key.size() + to_string(MAP_ITEMS).size()/2;
	
	//4.打印相关信息
	std::cout << "Number of members  " << "key size  " << "insert time(ms)  " << "find time(us)  " << std::endl;
	std::cout << left << setw(19) << MAP_ITEMS;
	std::cout << left << setw(10) << key_size;
	std::cout << left << setw(17) << insert_time;
	std::cout << left << setw(15) << find_time << std::endl;
}

五、一致性哈希原理

5.1 问题介绍

参考:http://t.csdnimg.cn/iKh0k


六、岛问题

6.1 问题介绍

6.2 解题思路

1. 采用感染算法: 首先从第一行开始遍历二维数组,如果发现“1”,那么递归查找和它相连接的所有“1”,并将“1”改成“2”。当下次遍历到同一个岛时,程序会发现该岛已经被标记为“2”,从而避免重复计算。

时间复杂度:O(N*M),遍历时每个位置都会被调用1次,同时每个位置最多可能被它上下左右的4个位置递归调用4次。

2. 采用并查集:若数据量很大,需要并行运算求解,就不太好做了,难点在于多CPU计算结果的合并.
合并时要考虑将边界有重合的岛合并,但问题在于我们不知道多个重合边是否属于同一个岛。

  • 考虑如下情况:有一个大C字形岛屿,被一刀切成三块了,那么在合并的时候遇到两条边,但我们不知道到底有几个岛(要合并几次)。
  • 应用并查集结构,将一个岛屿构造成一个并查集,遇到相同边的时候合并进同一个并查集中,这样避免了重复合并的问题.

6.3 代码实现

#include<cstdio>
#include<iostream>
#include<cstring>
#include<algorithm>
#include<cmath>
#include<unordered_map>
#include<cstdlib>
using namespace std;

int m[100][100];

//1.感染算法实现
void infect(int i, int j, int N, int M)//可变参数只有位置i,j
{
	//base case:当前位置是无效位置或者不是1,返回
	if (i < 0 || i >= N || j < 0 || j >= M || m[i][j] != 1)
		return;
	m[i][j] = 2;
	//递归感染和它连成一片的1
	infect(i + 1, j, N, M);
	infect(i - 1, j, N, M);
	infect(i, j + 1, N, M);
	infect(i, j - 1, N, M);
}
int countIslands(int N, int M)
{
	if (m == NULL || m[0] == NULL)
		return 0;
	int res = 0;
	for (int i = 0; i < N; i++)
	{
		for (int j = 0; j < M; j++)
		{
			if (m[i][j] == 1)
			{
				res++;
				infect(i, j, N, M);
			}
		}
	}
	return res;
}

int main()
{
	int N, M;
	cin >> N >> M;
	for (int i = 0; i < N; i++)
	{
		for (int j = 0; j < M; j++)
		{
			char c = getchar();
			m[i][j] = c - '0';
		}
	}
	cout << countIslands(N, M);
}

七、并查集

参考:http://t.csdnimg.cn/1DGgy

#define N 1005       //指定并查集所能包含元素的个数(由题意决定)
using namespace std;
		
int pre[N];     					//存储每个结点的前驱结点 
int Rank[N];    					//树的高度 
void init(int n)     				//初始化函数,对录入的 n个结点进行初始化 
{
    for (int i = 0; i < n; i++) {
        pre[i] = i;     			//每个结点的上级都是自己 
        Rank[i] = 1;    			//每个结点构成的树的高度为 1 
    }
}
int find(int x)     	 		    //查找结点 x的根结点 
{
    if (pre[x] == x) return x;  		//递归出口:x的上级为 x本身,则 x为根结点 
    return find(pre[x]); 			//递归查找 
}

int find(int x)     				//改进查找算法:完成路径压缩,将 x的上级直接变为根结点,那么树的高度就会大大降低 
{
    if (pre[x] == x) return x;		//递归出口:x的上级为 x本身,即 x为根结点 
    return pre[x] = find(pre[x]);   //此代码相当于先找到根结点 rootx,然后 pre[x]=rootx 
}

bool isSame(int x, int y)      		//判断两个结点是否连通 
{
    return find(x) == find(y);  	//判断两个结点的根结点(即代表元)是否相同 
}

bool join(int x, int y)
{
    x = find(x);						//寻找 x的代表元
    y = find(y);						//寻找 y的代表元
    if (x == y) return false;			//如果 x和 y的代表元一致,说明他们共属同一集合,则不需要合并,返回 false,表示合并失败;否则,执行下面的逻辑
    if (Rank[x] > Rank[y]) pre[y] = x;		//如果 x的高度大于 y,则令 y的上级为 x
    else								//否则
    {
        if (Rank[x] == Rank[y]) Rank[y]++;	//如果 x的高度和 y的高度相同,则令 y的高度加1
        pre[x] = y;						//让 x的上级为 y
    }
    return true;						//返回 true,表示合并成功
}

时间复杂度:当样本量足够大时,时间复杂度为O(N) ,且单次操作时间复杂度趋近于O(1).

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值