算法学习05:哈希&并查集
认识哈希函数和哈希表
经典哈希函数:
输入域无穷大的,输出域有穷尽的,
输入参数固定时,返回值一定是一样的
输入值不同时,有可能得到相同的返回值,(哈希碰撞)
哈希函数的离散性:给我很多不同的书的话,我将在整个s域上均匀返回其输出值
认识布隆过滤器
- 布隆过滤器: 用于封杀url,这种方法有一定的失误率:有可能会误封url.
- 做法:
- 创建一个bit数组,每个bit代表一个 哈希结果 的true或false.
- 将每个黑名单url经过多个独立的哈希函数得到的哈希值对应的bit位描黑
- 要对比url是否存在时,只需查看将待对比的url经过几个哈希函数的哈希结果节点.若所有哈希结果对应的点均被描黑,则认为此url属于url
- 公式:(设
n
为样本量,p
为目标失误率,k
为哈希函数个数)- 数组位数 m = − n ∗ ln p l n 2 2 m = -\frac{n*\ln{p}}{ln^2{2}} m=−ln22n∗lnp
- 哈希函数个数 k = ln 2 ∗ m n k = \ln{2}*\frac{m}{n} k=ln2∗nm
- 实际失误率 p ′ = ( 1 − e − ( m ∗ k n ) ) k p' = {(1-e^{-(\frac{m*k}{n})})}^k p′=(1−e−(nm∗k))k
认识一致性哈希
对于分布式系统,需要将哈希结果 限制在[0-232]以内,然后将哈希结果区间平均分配给几个服务器.
并查集
并查集主要解决两个问题:
- 判断某元素是否存在于集合中
- 集合的合并
并查集的设计
- 并查集的结构: 每个节点都有一个父指针指向自己的父节点,父子节点属于同一个集合,根节点作为一个集合的
代表节点
. - 并查集的构造: 初始时每一个点自成一个集合,每一个节点的父指针都指向自己,每一个集合的
代表节点
为集合元素自身. - 集合的合并: 是把一个集合的
代表节点
的父节点指向另一个节点的代表节点
. - 并查集的路径压缩: 查找根节点时顺便做路径压缩操作: 将沿途节点的父指针设为所查询到的根节点.
并查集的实现
并查集是用map实现的,某节点的fatherMap
值表示该节点的父指针
public class 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>();
}
// 初始化并查集(使用Node列表)
public void UnionFindSet(List<Node> nodes) {
UnionFindSet()
fatherMap.clear();
sizeMap.clear();
for (Node node : nodes) {
// 初始时每个节点自成一个并查集
// 每个集合的代表节点为其自身,每个并查集的size为1
fatherMap.put(node, node);
sizeMap.put(node, 1);
}
}
// 找到并查集代表节点(路径压缩)
private Node findHead(Node node) {
Node father = fatherMap.get(node);
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);
// 将size小并查集的代表节点加到size大并查集中
if (aSetSize <= bSetSize) {
fatherMap.put(aHead, bHead);
sizeMap.put(bHead, aSetSize + bSetSize);
} else {
fatherMap.put(bHead, aHead);
sizeMap.put(aHead, aSetSize + bSetSize);
}
}
}
}
}
当节点数量足够多时,并查集单次查询和合并的效率是O(1),与数据规模无关.
并查集应用举例:岛问题
一个矩阵中只有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
这个矩阵中有三个岛。
解法
- 作为经典问题,比较好做:一顿遍历,若遍历到1,就进入感染函数,将与其相连的部分感染成2,岛屿数量加一.
public static void infect(char[][] grid, int y, int x) { int yLen = grid.length; int xLen = grid[0].length; if (y >= 0 && y < yLen && x >= 0 && x < xLen) { if (grid[y][x] == '1') { grid[y][x] = '2'; infect(grid, y + 1, x); infect(grid, y - 1, x); infect(grid, y, x + 1); infect(grid, y, x - 1); } } }
- 若数据量很大,需要并行运算求解,就不太好做了,难点在于多CPU计算结果的合并.
- 合并时要考虑将边界有重合的岛合并,但问题在于我们不知道多个重合边是否属于同一个岛.
- 考虑如下情况:有一个大C字形岛屿,被一刀切成三块了,那么在合并的时候遇到两条边,但我们不知道到底有几个岛(要合并几次).
- 应用并查集结构,将一个岛屿构造成一个并查集,遇到相同边的时候合并进同一个并查集中,这样避免了重复合并的问题.