[算法] - 哈希函数 + RandomPool + 布隆过滤器 + 一致性哈希原理 + 并查集结构

目录

哈希函数

哈希函数的作用:通过 f 哈希函数使得数据变均匀分布,离散化

哈希表的实现:

扩容:

笔试tricks:

在C++里面,不怎么用哈希表,而是使用int[1000]这样的数组,时间复杂度会低。

在刷题的过程中只要找到O(N*logN)就足够了,N不可能过大,不用找O(N))

根据数据范围,来找时间复杂度大概多大就过分了。

C++里面1s可执行10^8~9操作。不能超过10^8~9。

如果告诉你数据是10^6,O(N^2)肯定过不了。10^12超过了10^8~9。

设计RandomPool结构

详解布隆过滤器【可能误报,但是不会错报,意思就是在名单里的不会错,但不在的可能会误报】

布隆过滤器公式:

作用范围:

详解一致性哈希原理

作用范围

实现的过程【确定一个东西的归属】

上面的说法还隐含着负载不均衡的问题

【解决办法】

并查集结构的详解和实现 【岛屿问题】

切分矩阵,并行

issameset

 union

优化向上查找的过程

时间复杂度


哈希函数

1.经典的,输入域无穷,输出范围有限,比如MD5,输入无限字符串,输出有限2^64。输入大,输出小。

2.不随机,同输入,同输出。不同的输入也 可能输出相同【哈希碰撞】

3. 如果输入特别大,输入小,那么输出上每个点是均匀变厚的,如下图,虽然并不一定没个都是33,但是基本差不多。【均匀性,离散型】

多说一句,哈希函数的均匀性,代表着将原始的输入数据完完全全打乱,就算是两个很相近的数据,经过哈希函数之后,对应的值相差也会很远的。

第三点的性质很重要,哈希函数处理把不同的数分开的效果很好。比如20亿个数,找频数最大的数。【只有2g内存,一次处理不完,内存爆炸】

哈希函数的作用:通过 f 哈希函数使得数据变均匀分布,离散化

每个数经过哈希函数f,【作用:原先的输入是挺大的,但不一定是均匀分布的,哈希函数的首个作用就是让其通过f变均匀】然后%10,按照余数分成10份,每一种数肯定对应10份里面的一个,所以求众数可以直接10个里面各自找,而且均匀分布保证了10个里面数的数目差不多,2亿左右】然后10个里面找出每一个频数最大的数,再比较。

输入无限离散,但不一定均匀分布,哈希函数的作用就是让它变均匀分布。然后上面的之后%10也是均匀分布。

 

哈希表的实现:

0~16的桶+单链表的结构。【java里改进单链表变成有序表】

原始数据经过f之后再%17,比如说13,就在之后链接上一个数,如下图。

寻找的时候,比如说zuo 经过f之后 在%17==13,在13里面,遍历查找,对应的32得出。

扩容:

如果出现某一个桶里面太大,我们扩两倍不是%17而是%34,,将旧的全部O(N)到新的桶。

虽然上面说O(N)但使用时增删是O(1),离线变成%34,最终全部扩容完,才让用户接触%34。

 

===================================================================

笔试tricks:

  1. 在C++里面,不怎么用哈希表,而是使用int[1000]这样的数组,时间复杂度会低。

  2. 在刷题的过程中只要找到O(N*logN)就足够了,N不可能过大,不用找O(N))

  1. 根据数据范围,来找时间复杂度大概多大就过分了。

C++里面1s可执行10^8~9操作。不能超过10^8~9。

如果告诉你数据是10^6,O(N^2)肯定过不了。10^12超过了10^8~9。

 

===================================================================

 

设计RandomPool结构


【 题目】
设计一种结构, 在该结构中有如下三个功能:
insert(key):将某个key加入到该结构, 做到不重复加入
delete(key):将原本在结构中的某个key移除
getRandom(): 等概率随机返回结构中的任何一个key。
【 要求】
Insert、 delete和getRandom方法的时间复杂度都是O(1) 

 

解法:准备两张hash表(一张hash表无法做到严格等概率随机返回一个)

HashMap<String,Integer> keyIndexMap = new HashMap<String, Integer>();
HashMap<Integer,String> indexKeyMap = new HashMap<Integer, String>();

做法
A 第0个进入hash表 , 表A key A value 0 表B key 0 value A 
B 第1个进入hash表 , 表A key B value 1 表B key 1 value B
insert(key)代码实现:

public void insert(String key){
    if(keyIndexMap.containsKey(key)){
        return;
    }else{
        keyIndexMap.put(key,number);
        indexKeyMap.put(number,key);
        number++;
    }
}

利用math的random函数,随机从size取一个数字,在哈希表2取对应数字的key,就是随机等概率的
getRandom()代码实现:

public String getRandom(){
    if(size ==0){
        return null;
    }
    int index = (int)(Math.random()*size);
    return map2.get(index);
}

如果要remove呢?
直接remove会出现问题:删除key对应要删除某个index,那么就会产生“洞”,调用getRandom就一次调用得到等概率结果。
那么该如何去删呢?
如假设有1000个key,要删除str17,那么找到index17, 把str999在keyIndexMap的index变为17,map2的17改为str999,删除index999的洞,即产生洞的时候删除最后一条,再删除函数需要删除的key。通过交换最后一行数据保证index是连续的。

public void delete(String key){
    if(keyIndexMap.containsKey(key)){
        Integer deleteIndex = keyIndexMap.get(key);
        int lastIndex = --number;
        String lastKey = indexKeyMap.get(lastIndex);
        indexKeyMap.put(deleteIndex,lastKey);
        keyIndexMap.put(lastKey,deleteIndex);
        keyIndexMap.remove(key);
        indexKeyMap.remove(number);
    }
}

 

两个哈希表可以做到。

不考虑删除的话,根据index直接math.random 26等概率取,然后对应就好。

删除的情况是,删除这一条记录之前,将其与最后一条记录25置换,然后删掉最后一个,这样中间没有空洞,size-1就好、

import java.util.HashMap;

public class Code02_RandomPool {

	public static class Pool<K> {
		private HashMap<K, Integer> keyIndexMap;
		private HashMap<Integer, K> indexKeyMap;
		private int size;

		public Pool() {
			this.keyIndexMap = new HashMap<K, Integer>();
			this.indexKeyMap = new HashMap<Integer, K>();
			this.size = 0;
		}

		public void insert(K key) {
			if (!this.keyIndexMap.containsKey(key)) {
				this.keyIndexMap.put(key, this.size);
				this.indexKeyMap.put(this.size++, key);
			}
		}

		public void delete(K key) {
			if (this.keyIndexMap.containsKey(key)) {
				int deleteIndex = this.keyIndexMap.get(key);
				int lastIndex = --this.size;
				K lastKey = this.indexKeyMap.get(lastIndex);
				this.keyIndexMap.put(lastKey, deleteIndex);
				this.indexKeyMap.put(deleteIndex, lastKey);
				this.keyIndexMap.remove(key);
				this.indexKeyMap.remove(lastIndex);
			}
		}

		public K getRandom() {
			if (this.size == 0) {
				return null;
			}
			int randomIndex = (int) (Math.random() * this.size); // 0 ~ size -1
			return this.indexKeyMap.get(randomIndex);
		}

	}

	public static void main(String[] args) {
		Pool<String> pool = new Pool<String>();
		pool.insert("zuo");
		pool.insert("cheng");
		pool.insert("yun");
		System.out.println(pool.getRandom());
		System.out.println(pool.getRandom());
		System.out.println(pool.getRandom());
		System.out.println(pool.getRandom());
		System.out.println(pool.getRandom());
		System.out.println(pool.getRandom());

	}

}


 

 

=========================================================================

详解布隆过滤器【可能误报,但是不会错报,意思就是在名单里的不会错,但不在的可能会误报】

 

设置黑名单url的时候,如果用hashset 内存640G太过了。

 

一个url经过k个哈希函数[特征点],%m之后进入位图,抹黑,将0-1,1还是1,。

来了一个新的url,经过k个哈希函数[特征点],%m后如果都是黑就说在黑名单里,有一个位置不是黑就肯定不在黑名单。

 

布隆过滤器公式:

只和样本量和失误率有关

向上取整数

3)是指真实的失误率,因为你给的不是12.7而是给13,所以真实的失误率比p0.0001要小。

作用范围:

黑名单系统,爬虫去重的系统、在不在一个集合里,样本很大的情况里,多嘴问一句允不允许用有误报率。系统上给我的内存空间多大。

 

=====================================================================

 

详解一致性哈希原理

 

作用范围

最常使用的后台服务器架构之一,分布式数据库啊什么的就会用到。

是为了解决经典服务器的弊端产生的,处理数据,在将数据分开放到不同的机器上的时候要用到一致性哈希。

经典服务器如下:request之后通过前端,那么多端都经过哈希和%得出2就去后端2.

弊端:增减后端机器,代价是全量的。

 

通过md5生成哈希,这里看成环结构。

后端m1 m2 m3都有各自独特的hostkey ip等,把他通过哈希,打到环上。来了一个新的请求request直接f哈希函数,但是不用%,顺时针遇到的第一个后端就是对应的处理的机器。

下图中r代表着request,m代表着后端的服务器

 

实现的过程【确定一个东西的归属】

 

上面的说法还隐含着负载不均衡的问题

你不能保证三台m服务器均分任务,即便是认为分开三个,再新加一个进来,没上就负载不均衡了。

【解决办法】

虚拟节点

a表示虚拟节点,和m服务器构成路由表。

虚拟节点去抢环。当增加m4机器的时候,几乎是向其他三个等要一些,最后1/4。

最后对应到哪个m机器。

而且如果m1特别好的机器,可以多给写虚拟节点,达到管理负载、分配负载的功能。

 

===========================================================================

并查集结构的详解和实现 【岛屿问题】

 

【 题目】
一个矩阵中只有0和1两种值, 每个位置都可以和自己的上、 下、 左、 右 四个位置相连, 如果有一片1连在一起, 这个部分叫做一个岛, 求一个矩阵中有多少个岛?
【 举例】
001010
111010
100100
000000
这个矩阵中有三个岛
【 进阶】
如何设计一个并行算法解决这个问题 

 

将矩阵从左向右,从上向下遍历,遇到1的时候,实现一个感染过程,把所有能接触到的1变成2【递归】.岛的数目+1.

 

public class Code03_Islands {

	public static int countIslands(int[][] m) {
		if (m == null || m[0] == null) {
			return 0;
		}
		int N = m.length;
		int M = m[0].length;
		int res = 0;
		for (int i = 0; i < N; i++) {
			for (int j = 0; j < M; j++) {
				if (m[i][j] == 1) {
					res++;
					infect(m, i, j, N, M);
				}
			}
		}
		return res;
	}

	public static void infect(int[][] m, int i, int j, int N, int M) {
		if (i < 0 || i >= N || j < 0 || j >= M || m[i][j] != 1) {
			return;
		}
		m[i][j] = 2;
		infect(m, i + 1, j, N, M);
		infect(m, i - 1, j, N, M);
		infect(m, i, j + 1, N, M);
		infect(m, i, j - 1, N, M);
	}


 

切分矩阵,并行

核心逻辑:如下图切一刀。发现有abcd4个岛屿,记录岛屿的四个边界信息。

我们把刀附近的2标记为属于a,之后合并的时候看一下,ac是不是一个集合,不是,但是刀左右,所以合并为一个岛屿。

 

并查集结构

刚开始将每一个数据,搞成一个集合,有两个操作

1.查是不是一个集合里的 issameset

2.合并操作 union

虽然说list和hash都可以实现,但是不高效,都涉及到了数据整体迁移的过程。【数据整体迁移——》不高效】

代表节点:头结点

issameset

不断往上查,是否有同一个头。

 

 union

先用issameset查一下,是不是同一个,不是就可以union

比较少节点的挂多的。小集合的代表节点指向大集合的代表节点。

删除掉小集合,把大集合中的个数增加2.

优化向上查找的过程

比如上面5-3-4,变成3-4,5-4,意思就是如果不是直接与head相连,变成直接的。【扁平化】

 

时间复杂度

如果a+b的次数大于N,时间复杂度是O(1)

 

import java.util.HashMap;
import java.util.List;
import java.util.Stack;

public class Code04_UnionFind {

	public static class Element<V> {//加了个封装
		public V value;

		public Element(V value) {
			this.value = value;
		}

	}

	public static class UnionFindSet<V> {
		public HashMap<V, Element<V>> elementMap;//v为值【用户课件】,Element为元素【自己用】
		public HashMap<Element<V>, Element<V>> fatherMap;//父节点
		public HashMap<Element<V>, Integer> sizeMap;//代表节点的集合有多少个节点

		public UnionFindSet(List<V> list) {
			elementMap = new HashMap<>();
			fatherMap = new HashMap<>();
			sizeMap = new HashMap<>();
			for (V value : list) {
				//初始化
				Element<V> element = new Element<V>(value);
				elementMap.put(value, element);
				fatherMap.put(element, element);//每个节点是自己的父
				sizeMap.put(element, 1);//每个节点是自己代表节点
			}
		}

		private Element<V> findHead(Element<V> element) {
			Stack<Element<V>> path = new Stack<>();//沿途的路
			while (element != fatherMap.get(element)) {
				path.push(element);//一直走到代表节点
				element = fatherMap.get(element);
			}
			while (!path.isEmpty()) {
				fatherMap.put(path.pop(), element);//扁平化,所有的节点都是直接指向代表节点的
			}
			return element;
		}

		public boolean isSameSet(V a, V b) {
			//看a有没有注册过,一个值都有对应的element
			if (elementMap.containsKey(a) && elementMap.containsKey(b)) {
				return findHead(elementMap.get(a)) == findHead(elementMap.get(b));
			}
			return false;
		}

		public void union(V a, V b) {
			if (elementMap.containsKey(a) && elementMap.containsKey(b)) {
				//先找代表节点【头结点】
				Element<V> aF = findHead(elementMap.get(a));//找a对应的元素的代表节点
				Element<V> bF = findHead(elementMap.get(b));//代表节点里面有这个结合的大小
				if (aF != bF) {
					//将少的挂在大的下面
					Element<V> big = sizeMap.get(aF) >= sizeMap.get(bF) ? aF : bF;//big是大集合的代表节点
					Element<V> small = big == aF ? bF : aF;
					//直接将少的集合的代表节点换成大集合的代表节点
					fatherMap.put(small, big);
					//改变代表节点对应的size的大小
					sizeMap.put(big, sizeMap.get(aF) + sizeMap.get(bF));
					sizeMap.remove(small);//删除这个代表节点的记录
				}
			}
		}

	}

}

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值