左神基础算法笔记-五

1. 认识哈希函数和哈希表

哈希函数

哈希函数有很多种实现方式,md5,sha1。相似输入经过哈希函数后天差地别。

  1. 标准哈希函数输入域是无穷的,返回值是有范围的。
  2. same input same out
  3. different input maybe same out(哈希碰撞)
  4. 离散性:输入域中的所有值均匀分布在返回范围上。(返回范围模后也均匀分布)(离散性越好的哈希函数越优良)

哈希表

哈希表的大小为返回值的域,哈希表中存放的是链表头结点,头结点中存放了哈希函数返回值,如下图中“zuo”这个字符串经过哈希运算之后的到的值再%17得到15,就挂在哈希表中挂在15的位置。

因为哈希函数的离散性,不断地增加哈希表中的数据时,每个链表的长度几乎均匀的增长。

哈希表的增删改查操作都认为是O(1)的操作,但常数项比较大,求hash值比较麻烦。

哈希表扩容

每次扩容,表的大小增加一倍,重新计算表中每一的数据应该挂到哪个位置。

工程上扩容可以离线。这样扩容的时间复杂度是O(N)的,因为成倍扩容,扩一次以后很久不用扩。

Java实现

哈希表在java中的实现是HashMap,头结点以后原来是链表,现在是红黑树(TreeMap)。

java中的HashSet与HashMap是一样的,只不过HashMap中value作为key的伴随数据。

2. 设计一种RandomPool结构,在该结构中有如下三个功能:

insert(key):将某个key加入到该结构,做到不重复加入。

delete(key):将原本在结构中的某个key移除。

getRandom():等概率随机返回结构中的任何一个key。

要求:三个功能是时间复杂度都是O(1)

  • hashMap1 hashMap2 index map1中key是数据,value是出现顺序,map2中反过来
  • delete 时将最后一条记录移到要删掉的地方,然后删除最后一条记录

3. 认识布隆过滤器

应用场景:实现黑名单的功能,比如要拦截 100亿个 色情暴力的 URL,使用哈希表必然会占据内存大量的空间(工程上为了速度会存在内存),这时候就可以使用布隆过滤器。

布隆过滤器包括一个大小为 m 的比特数组,k 个哈希函数。URL 通过 k个哈希函数 得到的值再模 m 得到 k 个 0~m-1 的值,将数组上这 k 个位置的 0 置为 1;将 100亿个URL 都通过布隆过滤器。

需要检验一个 URL 是否在黑名单中时,将这个 URL 通过 k个哈希函数 算出所有位置,若数组上这些位置上全部是1,则在黑名单中,有一个是 0, 就不在黑名单中。

布隆过滤器是有失误率(p)的,有失误率会把不在黑名单中的URL当成黑名单中的;m 越大失误率越低,m 越小失误率越高。

给定样本量 n,预期失误率 p,求比特数组大小 m(向上取整),哈希函数个数 k(向上取整)。

m=-\frac{n*\ln p}{(\ln2)^2}

k=\ln2\frac{m}{n}≈0.7\frac{m}{n}

p_真=(1-e^{-\frac{n*k}{m}})^k

类似采集指纹(URL)时需要按好多下收集多个(k个)样本。

4. 认识一致性哈希

经典服务器结构

由前端服务器组和后端服务器组构成,前端服务器组提供提供的服务无差别。

一个 request(比如查询”zuo”的年龄,年龄31) 过来,请过任意一台前端服务器转发到后端,那么转发到那一台后端服务器上呢?通过哈希函数得到“zuo”这个字符串的 hashValue,hashValue%3(假设后端服务器有三台)得到 0,1,2 中的一个编号。

存储的时候也是这样存储的,不同的数据存放在不同的服务器上,这样的经典服务器结构是负载均衡的。

经典服务器结构的弊端在于加减机器时数据迁移的代价过高,比如要增加一台后端服务器,就需要把所有的数据都重新经过哈希函数计算他们新的所在位置,这样才能够继续维持负载均衡。

一致性哈希可以解决经典服务器结构的问题。

将哈希函数所产生的哈希域想象成一个环(比如0~2^64),每一台后端服务器的特有信息(hostname,MAC地址,IP地址)经过哈希函数算出的值落在环上形成服务器的点,将数据经过哈希函数后也落到环上形成数据的点,数据的点顺时针之后的第一个服务器的点上就是负责存储、查询该数据的服务器。

如何实现在环上查找顺时针方向的后一个服务器点?可以在前端服务器上实现,每台前端服务器上都保存着 所有排好序的后端服务器点(0~2^64范围)的数组,可以使用二分查找,若哈希值大于数组最后一个服务器点的值,则由第一台后端服务器负责。

加一台服务器,比如环上按顺时针方向有 m1, m2, m3 三台服务器,这时候如果想加一台服务器 m4,m4 的特有信息经过哈希函数的值落在 m2 和 m3 之间。首先前端服务器上的数组中要加入 m4,按照顺时针方向找 m4 下一个服务器(m3),将原本由这台服务器(m3)负责的 m2-m4 的数据迁移到 m4 上。

减一台服务器,比如要拿掉 m4,就按照顺时针方向找 m4 下一个服务器(m3),将 m4 的数据迁移到 m3 上,再把前端那个数组中 m4 去除。

这时候有两个问题:

  1. 初始时服务器数量很少,不能均分环(哈希域)
  2. 就算均分了环,再加入服务器时也会破坏均分。
    • 比如就算有两台服务器均分了环,这时候再加入一台服务器,无论落到环上哪个位置都会破坏环。

这两个问题是有哈希函数的性质造成的,哈希函数在量少的时候是离散的,要量起来以后才能均匀分布。好比就算分子是不断流动的,但房间里只有两个香水分子的话,是不能保证香水在房间里均匀分布的;但是房间里如果洒了两瓶香水的话,就可以看作香水在房间里均匀分布了。

虚拟节点技术可以解决这两个问题,每个服务器建立 1w 个虚拟节点,建立路由表存放服务器和虚拟节点的对应关系,每个虚拟节点负责环上的一小段,每个服务器负责自己所有的虚拟节点所负责的,这样就保证了初始时服务器少不能均分的问题。

添加新服务器时只需要给新服务器也分配 1w 个虚拟结点,在从另外的服务器要回该由自己负责的数据即可。这样添加新服务器时还可以继续保证负载均衡。

5. 岛问题

一个矩阵中只有0和1两种值,每个位置都可以和自己的上、下、左、右
四个位置相连,如果有一片1连在一起,这个部分叫做一个岛,求一个
矩阵中有多少个岛?
举例:
0 0 1 0 1 0
1 1 1 0 1 0
1 0 0 1 0 0
0 0 0 0 0 0
这个矩阵中有三个岛。



6. 认识并查集结构

  • 逻辑概念

    有很多元素,这些元素属于不同的集合。刚开始的时候每个元素各自属于一个集合。并查集支持两种操作:

    • isSameSet(A,B)

      检查A,B两个元素是否属于同一个集合

    • union(A,B)

      合并A,B两个集合

  • 落地结构

    要实现这样的操作可以使用 List 或 HashSet,但这样都避不开遍历的代价。并查集的实际结构如下:

    • 其中指向自己的节点为一个集合的代表节点
    • 每个节点通过不断向上找的方式找到代表点,从而确定自己所在的集合
    • 在查找自己所在集合的过程中,有一个将链“打平”的操作,例如下图查找5,在查找过程中,将 5 到 代表节点之间的节点全部“打平”,都直接挂到代表节点上
    • 合并两个集合时,先判断A,B是否属于同一集合,若不属于则将 size 小的集合的代表节挂在 size 大的集合的代表节点下
    • 通过“打平”的操作,随着查找/合并操作次数的增加,时间复杂度将会被优化
    • 如果查询和合并的次数相加达到了 O(N) 或以上,那么单次查询/合并的平均时间复杂度将达到 O(1)(实际是 O(α(N)),α(N) 是一个成长超慢的函数,当 N 接近 10^80,α(N) 的返回值也不会超过6,10^80 是宇宙中人类已探明的原子总数)
/**
* fatherMap: valueNode 是 keyNode 的父节点
* sizeMap: 若 key 是代表点,则 value 是集合的 size;
* 若 key 不是代表点,则该条数据无效
*/
public class Code_04_UnionFind {

public static class Node {
// whatever you like
}

public static class UnionFindSet {
public HashMap<Node, Node> fatherMap;
public HashMap<Node, Integer> sizeMap;

public UnionFindSet() {
fatherMap = new HashMap<Node, Node>();
sizeMap = new HashMap<Node, Integer>();
}

public void makeSets(List<Node> nodes) {
fatherMap.clear();
sizeMap.clear();
for (Node node : nodes) {
fatherMap.put(node, node);
sizeMap.put(node, 1);
}
}

private Node findHead(Node node) {
Node father = fatherMap.get(node);
// 这个 if 判断 node 的 father 还是不是代表节点,只有代表节点的 father 是等于自己的
if (father != node) {
father = findHead(father);
}
fatherMap.put(node, father);
return father;
}

public boolean isSameSet(Node a, Node b) {
return findHead(a) == findHead(b);
}

public void union(Node a, Node b) {
if (a == null || b == null) {
return;
}
Node aHead = findHead(a);
Node bHead = findHead(b);
if (aHead != bHead) {
int aSetSize= sizeMap.get(aHead);
int bSetSize = sizeMap.get(bHead);
if (aSetSize <= bSetSize) {
fatherMap.put(aHead, bHead);
sizeMap.put(bHead, aSetSize + bSetSize);
} else {
fatherMap.put(bHead, aHead);
sizeMap.put(aHead, aSetSize + bSetSize);
}
}
}

}

public static void main(String[] args) {

}

}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值