算法<初级> - 第三章 贪心,二叉树,优先队列并查集等(完结)
算法<初级> - 第三章 贪心,二叉树,优先队列并查集等
题目十(1):布隆过滤器
-
海量数据管理,在哈希表上再压缩数据,但会存在较低的失误率
- 失误类型:宁可错杀三千不可错放一个,非存储数据小概率判断为存储数据
-
bit位数组存储:eg. int数组每位存储0~31位bit数组
-
思想:准备k个哈希函数,哈希值取模bit数组大小m,每个键经过记录得到k个哈希值范围[0,m-1],将bit数组k个哈希值的对应位置1。查表时,若是查询键中非全部哈希置位为1,则未被记录。
-
若是k个值有重复,则仍然置1,多余的不变
-
所有键共用一个bit数组
-
在bit数组映射整型数组的值:
n_bit/32(int范围)%32
-
-
布隆过滤器大小与失误率(要求)、样本数有关 - 小数向上取整 32
- \(m = - \frac{n * ln^{p}}{({ln^{2})}^2}\)
-
哈希函数多少与布隆过滤器大小,样本数有关 - 小数向上取整
- \(k={ln}^{2} * \frac{m}{n}\)
-
真实失误率
- \(p_{real}={(1-{e}^{-\frac{n * k}{m}})}^k\)
题目十(2):一致性哈希
-
负载均衡结构:哈希key返回value,取哈希域模线性映射,均匀分布各个站点
- 问题:范围改变时需要重新映射,迁移代价过高
-
一致性哈希结构:哈希key返回value, 不取模哈希域为环,根据哈希值分布在环上往后最近站点(二分查找往后最近站点)
-
添加站点时,只需要在后站点往新站点进行数据迁移(删除站点时同理)
-
问题①:当数据少量的时候无法保证负载均衡
-
问题②:当添加 / 删除站点的时候可能无法保证负载均衡
-
解决:使用虚拟节点技术
-
将虚拟节点均匀分配给实际站点,数据哈希值分布在虚拟节点上
-
-
题目一:随时找到数据流的中位数
-
题目:有一个不断吐整数的数据流,假设有足够的空间来存储。设计一个MedianHolder结构,它可以随时取得之前吐出所有数的中位数。
-
要求:结构加入新数的复杂度为O(logn);取中位数的时间复杂度O(1)
-
思想:
-
根据要求可以想到使用大小根堆来实现,中位数是在有序序列正中间,将数据流吐数分成两部分,元素个数相差不超过1,左边大根堆,右边小根堆(根堆在数据结构中就是优先队列,自定义比较优先级)
-
进来的第一个数默认放在大根堆。之后进来的数先跟大根堆堆顶进行比较 - 若是比之小,则加入大根堆;若是比之大,则加入小根堆
-
当大根堆与小根堆元素个数相差超过1时,多的堆弹出堆顶元素加入另一个堆
-
实时查询中位数:① 哪个根堆元素个数多就堆顶弹出元素,就是中位数 ② 若是两边堆元素个数相同,则两堆顶元素都弹出,相加除2即为中位数(左边堆元素都比堆顶元素小,右边堆元素都比堆顶元素大,故这两个元素就是实时数据流排序正中间的两个)
-
-
算法实现(Java)
public static class MedianHolder {
private PriorityQueue<Integer> maxHeap = new PriorityQueue<Integer>(new MaxHeapComparator());
private PriorityQueue<Integer> minHeap = new PriorityQueue<Integer>(new MinHeapComparator());
private void modifyTwoHeapsSize() {
if (this.maxHeap.size() == this.minHeap.size() + 2) {
this.minHeap.add(this.maxHeap.poll());
}
if (this.minHeap.size() == this.maxHeap.size() + 2) {
this.maxHeap.add(this.minHeap.poll());
}
}
public void addNumber(int num) {
if (this.maxHeap.isEmpty()) {
this.maxHeap.add(num);
return;
}
if (this.maxHeap.peek() >= num) {
this.maxHeap.add(num);
} else {
if (this.minHeap.isEmpty()) {
this.minHeap.add(num);
return;
}
if (this.minHeap.peek() > num) {
this.maxHeap.add(num);
} else {
this.minHeap.add(num);
}
}
modifyTwoHeapsSize();
}
public Integer getMedian() {
int maxHeapSize = this.maxHeap.size();
int minHeapSize = this.minHeap.size();
if (maxHeapSize + minHeapSize == 0) {
return null;
}
Integer maxHeapHead = this.maxHeap.peek();
Integer minHeapHead = this.minHeap.peek();
if (((maxHeapSize + minHeapSize) & 1) == 0) {
return (maxHeapHead + minHeapHead) / 2;
}
return maxHeapSize > minHeapSize ? maxHeapHead : minHeapHead;
}
}
public static class MaxHeapComparator implements Comparator<Integer> {
@Override
public int compare(Integer o1, Integer o2) {
if (o2 > o1) {
return 1;
} else {
return -1;
}
}
}
public static class MinHeapComparator implements Comparator<Integer> {
@Override
public int compare(Integer o1, Integer o2) {
if (o2 < o1) {
return 1;
} else {
return -1;
}
}
}
// for test
public static int[] getRandomArray(int maxLen, int maxValue) {
int[] res = new int[(int) (Math.random() * maxLen) + 1];
for (int i = 0; i != res.length; i++) {
res[i] = (int) (Math.random() * maxValue);
}
return res;
}
// for test, this method is ineffective but absolutely right
public static int getMedianOfArray(int[] arr) {
int[] newArr = Arrays.copyOf(arr, arr.length);
Arrays.sort(newArr);
int mid = (newArr.length - 1) / 2;
if ((newArr.length & 1) == 0) {
return (newArr[mid] + newArr[mid + 1]) / 2;
} else {
return newArr[mid];
}
}
public static void printArray(int[] arr) {
for (int i = 0; i != arr.length; i++) {
System.out.print(arr[i] + " ");
}
System.out.println();
}
public static void main(String[] args) {
boolean err = false;
int testTimes = 200000;
for (int i = 0; i != testTimes; i++) {
int len = 30;
int maxValue = 1000;
int[] arr = getRandomArray(len, maxValue);
MedianHolder medianHold = new MedianHolder();
for (int j = 0; j != arr.length; j++) {
medianHold.addNumber(arr[j]);
}
if (medianHold.getMedian() != getMedianOfArray(arr)) {
err = true;
printArray(arr);
break;
}
}
System.out.println(err ? "Oops..what a fuck!" : "today is a beautiful day^_^");
}
题目二:金条切分最少花费
-
题目:一块金条切成两半,花费金条长度一样的钱。一群人分N长度金条,怎么分最省钱。输入一个数组,返回最少花费。
- eg. {10,20,30},一共代表三个人,金条总长60,最少钱分法:先分成30+30,再将一个30分成10+20
-
思想:
-
由题目可知划分结构是一个哈夫曼树结构,哈夫曼编码贪心策略,每次都是取出序列中最小的两个合并。
-
用小根堆来实现哈夫曼树的构造,序列全部加入优先队列,每次弹出两个元素,再加入两元素之和,直至最后队列元素剩一个,就是最少花费值。
-
题目三:项目获得最大收入
-
题目:输入costs[n]成本数组,profits[n]收入数组,k最多做的项目数,m初始资金。求最多收入钱数。
-
思想:
-
贪心策略:在成本小于拥有资金的项目中,选择收益大的项目做。
-
将成本数组构造成一个小根堆,逐一弹出栈顶元素判断是否小于拥有资金直至不满足,弹出的项目将其 (收入-成本) 值加入一个大根堆。
-
小根堆停止弹后将大根堆堆顶元素弹出,若没有元素则结束,否则(资金+堆顶元素),继续重复进行小根堆操作。
-
-
算法实现(Java)
public static class Node {
public int p;
public int c;
public Node(int p, int c) {
this.p = p;
this.c = c;
}
}
public static class MinCostComparator implements Comparator<Node> {
@Override
public int compare(Node o1, Node o2) {
return o1.c - o2.c;
}
}
public static class MaxProfitComparator implements Comparator<Node> {
@Override
public int compare(Node o1, Node o2) {
return o2.p - o1.p;
}
}
public static int findMaximizedCapital(int k, int W, int[] Profits, int[] Capital) {
Node[] nodes = new Node[Profits.length];
for (int i = 0; i < Profits.length; i++) {
nodes[i] = new Node(Profits[i], Capital[i]);
}
PriorityQueue<Node> minCostQ = new PriorityQueue<>(new MinCostComparator());
PriorityQueue<Node> maxProfitQ = new PriorityQueue<>(new MaxProfitComparator());
for (int i = 0; i < nodes.length; i++) {
minCostQ.add(nodes[i]);
}
for (int i = 0; i < k; i++) {
while (!minCostQ.isEmpty() && minCostQ.peek().c <= W) {
maxProfitQ.add(minCostQ.poll());
}
if (maxProfitQ.isEmpty()) {
return W;
}
W += maxProfitQ.poll().p;
}
return W;
}
题目五:二叉树先序、中序、后序遍历的非递归实现
-
递归形式的先中后序遍历
-
算法实现(Java)
public static void preOrderRecur(Node head) { //先序
if (head == null) {
return;
}
System.out.print(head.value + " ");
preOrderRecur(head.left);
preOrderRecur(head.right);
}
public static void inOrderRecur(Node head) { // 中序
if (head == null) {
return;
}
inOrderRecur(head.left);
System.out.print(head.value + " ");
inOrderRecur(head.right);
}
public static void posOrderRecur(Node head) { //后序
if (head == null) {
return;
}
posOrderRecur(head.left);
posOrderRecur(head.right);
System.out.print(head.value + " ");
}
-
先中后序遍历的非递归形式
-
先序遍历:根节点压栈,之后循环。如果栈不为空,弹栈顶元素并打印,左右节点压栈,没有则不压栈。(中,左,右)
-
中序遍历:循环,节点边压栈边循环往左跑(赋值左节点),直至边界指向最左元素,打印节点,赋值右节点。(左,中,右)
-
后序遍历:实际上就是将先序遍历逆序实现,中右左顺序,将打印变成压入另一个栈。最后再将新栈依次弹出打印。
-
-
算法实现(Java)
public static void preOrderUnRecur(Node head) {
System.out.print("pre-order: ");
if (head != null) {
Stack<Node> stack = new Stack<Node>();
stack.add(head);
while (!stack.isEmpty()) {
head = stack.pop();
System.out.print(head.value + " ");
if (head.right != null) {
stack.push(head.right);
}
if (head.left != null) {
stack.push(head.left);
}
}
}
System.out.println();
}
public static void inOrderUnRecur(Node head) {
System.out.print("in-order: ");
if (head != null) {
Stack<Node> stack = new Stack<Node>();
while (!stack.isEmpty() || head != null) {
if (head != null) {
stack.push(head);
head = head.left;
} else {
head = stack.pop();
System.out.print(head.value + " ");
head = head.right;
}
}
}
System.out.println();
}
public static void posOrderUnRecur1(Node head) {
System.out.print("pos-order: ");
if (head != null) {
Stack<Node> s1 = new Stack<Node>();
Stack<Node> s2 = new Stack<Node>();
s1.push(head);
while (!s1.isEmpty()) {
head = s1.pop();
s2.push(head);
if (head.left != null) {
s1.push(head.left);
}
if (head.right != null) {
s1.push(head.right);
}
}
while (!s2.isEmpty()) {
System.out.print(s2.pop().value + " ");
}
}
System.out.println();
}
public static void posOrderUnRecur2(Node h) {
System.out.print("pos-order: ");
if (h != null) {
Stack<Node> stack = new Stack<Node>();
stack.push(h);
Node c = null;
while (!stack.isEmpty()) {
c = stack.peek();
if (c.left != null && h != c.left && h != c.right) {
stack.push(c.left);
} else if (c.right != null && h != c.right) {
stack.push(c.right);
} else {
System.out.print(stack.pop().value + " ");
h = c;
}
}
}
System.out.println();
}
public static void main(String[] args) {
Node head = new Node(5);
head.left = new Node(3);
head.right = new Node(8);
head.left.left = new Node(2);
head.left.right = new Node(4);
head.left.left.left = new Node(1);
head.right.left = new Node(7);
head.right.left.left = new Node(6);
head.right.right = new Node(10);
head.right.right.left = new Node(9);
head.right.right.right = new Node(11);
// recursive
System.out.println("==============recursive==============");
System.out.print("pre-order: ");
preOrderRecur(head);
System.out.println();
System.out.print("in-order: ");
inOrderRecur(head);
System.out.println();
System.out.print("pos-order: ");
posOrderRecur(head);
System.out.println();
// unrecursive
System.out.println("============unrecursive=============");
preOrderUnRecur(head);
inOrderUnRecur(head);
posOrderUnRecur1(head);
posOrderUnRecur2(head);
}
题目四:折纸折痕方向打印
-
题目:将一张纸对折一次再展开,设中间折痕方向向下;对折两次再展开,有三条折痕;对折N次再展开,从头到尾依次打印折痕方向。
-
思想:
-
实际上将每次折痕位置标记可以发现(n次对折有2n-1个折痕),这就是一颗满二叉树的中序遍历。根节点向下,所有左节点向下,右节点向上。
-
eg. 三次对折:折痕方向打印 - 下 (下) 上 (下) 下 (上) 上
-
-
算法实现(Java)
public static void printAllFolds(int N) {
printProcess(1, N, true); // 从根节点第一层开始
}
public static void printProcess(int i, int N, boolean down) {
if (i > N) {
return; // i表示目前层数 N表示总层数 down=true表向上,false表向下
}
printProcess(i + 1, N, true); // 左孩子向下
System.out.println(down ? "down " : "up ");
printProcess(i + 1, N, false); // 右孩子向上
}
public static void main(String[] args) {
int N = 4;
printAllFolds(N);
}
题目六:打印直观的二叉树
-
根节点在左,往后开叉,倒在左边的左置的二叉树,H根节点,>左孩子,<右孩子
-
算法实现(Java)
public static void printTree(Node head) {
System.out.println("Binary Tree:");
printInOrder(head, 0, "H", 17);
System.out.println();
}
public static void printInOrder(Node head, int height, String to, int len) {
if (head == null) {
return;
}
printInOrder(head.right, height + 1, "v", len);
String val = to + head.value + to;
int lenM = val.length();
int lenL = (len - lenM) / 2;
int lenR = len - lenM - lenL;
val = getSpace(lenL) + val + getSpace(lenR);
System.out.println(getSpace(height * len) + val);
printInOrder(head.left, height + 1, "^", len);
}
public static String getSpace(int num) {
String space = " ";
StringBuffer buf = new StringBuffer("");
for (int i = 0; i < num; i++) {
buf.append(space);
}
return buf.toString();
}
public static void main(String[] args) {
Node head = new Node(1);
head.left = new Node(-222222222);
head.right = new Node(3);
head.left.left = new Node(Integer.MIN_VALUE);
head.right.left = new Node(55555555);
head.right.right = new Node(66);
head.left.left.right = new Node(777);
printTree(head);
head = new Node(1);
head.left = new Node(2);
head.right = new Node(3);
head.left.left = new Node(4);
head.right.left = new Node(5);
head.right.right = new Node(6);
head.left.left.right = new Node(7);
printTree(head);
head = new Node(1);
head.left = new Node(1);
head.right = new Node(1);
head.left.left = new Node(1);
head.right.left = new Node(1);
head.right.right = new Node(1);
head.left.left.right = new Node(1);
printTree(head);
}
题目七:输出后继节点
-
题目:现有一种新的节点类型,比平常树节点多一个parent指针指向自己的父节点。给予树中任意一个node节点,返回它的后继节点。 - 中序遍历在它后面打印的节点叫做后继节点
-
思路:
-
直接把树节点按中序遍历输出存储,对应的下一个输出就是后继节点。时间复杂度O(n)
-
根据多加的parent指针,则可以:
-
当node节点有右子树时,则后继节点就是右子树的最左节点;
-
当node节点没有右子树,且是父节点的左孩子,则后继节点是父节点
-
当node节点没有右子树,且是父节点的右孩子,则后继节点一路往上找,找到某节点是某父节点的左孩子时,后继节点是某父节点 - 若直到根节点,则后继节点=null
-
-
-
算法实现(Java)
public static Node getNextNode(Node node) {
if (node == null) {
return node;
}
if (node.right != null) { // 有右子树
return getLeftMost(node.right);
} else {
Node parent = node.parent;
while (parent != null && parent.left != node) { // 有父亲,且不是左子树,一路往上
node = parent;
parent = node.parent;
}
return parent;
}
}
public static Node getLeftMost(Node node) {
if (node == null) {
return node;
}
while (node.left != null) {
node = node.left;
}
return node;
}
题目九:在数组中找一个局部最小的位置
-
题目:数组中元素个数不小于2,任意相邻两数都不同,返回数组中一个局部最小位置即可。
- 局部最小:最左端比后一个位置小,最右端比前一个位置小,中间则要比左右都小,该位置叫做局部最小。
-
思路:
-
实际上就是找极小值点,先看左右两端是否是局部最小点,如果都不是,则趋势是向下凸,中间必存局部最小。
-
二分思想:看mid位置处是否是局部最小,如果不是,则中间点必不是鞍点,则往递减方向继续二分;如果mid是极大值,则左右两边都有极值点,往任意方向都可。
- 二分不是说是有序才能进行二分,而是分成两部分,在某一部分一定有或者能找到,这 是一种快速查找的思想。
-
-
算法实现(Java)
public static int getLessIndex(int[] arr) {
if (arr == null || arr.length == 0) {
return -1; // no exist
}
if (arr.length == 1 || arr[0] < arr[1]) {
return 0;
}
if (arr[arr.length - 1] < arr[arr.length - 2]) {
return arr.length - 1;
}
int left = 1;
int right = arr.length - 2;
int mid = 0;
while (left < right) {
mid = (left + right) / 2;
if (arr[mid] > arr[mid - 1]) {
right = mid - 1;
} else if (arr[mid] > arr[mid + 1]) {
left = mid + 1;
} else {
return mid;
}
}
return left;
}
public static void printArray(int[] arr) {
for (int i = 0; i != arr.length; i++) {
System.out.print(arr[i] + " ");
}
System.out.println();
}
public static void main(String[] args) {
int[] arr = { 6, 5, 3, 4, 6, 7, 8 };
printArray(arr);
int index = getLessIndex(arr);
System.out.println("index: " + index + ", value: " + arr[index]);
}
题目八:认识并查集
-
并查集:一种多叉树结构,用于处理不相交集合的合并与查询
-
用list或者hashset等也可以进行isSameSet(查询两元素是否在同一集合)和union(合并两个元素所在集合)操作,但是时间复杂度O(n)在大数据量下超时。
-
isSameSet(A,B) - union(A,B)
-
初始化让每个元素自身构成集合,然后按一定要求让所属同一组的元素集合合并。题目过程中再用并查集反复查询元素所属集合。
-
-
并查集实现:
-
每个元素node节点:value & next指针,初始化时next指向自己
-
每个集合中next指针指向自己的节点,称为代表节点
-
isSameSet(A,B)元素同一集合查询:AB都一直赋值next指针,直到node.next.isEqual(node),即找到代表节点。若是指向同一元素,则在同一集合
-
union(A,B)集合合并:先判断AB是否属于同一集合;集合元素数少的集合代表节点next指向元素多的集合的代表节点。
-
查询后的优化:在返回查询结果前,若是查询节点不是直接指向代表节点,则一路上非直接指向的节点链展开,转为直接指向代表节点。
-
-
O(n)的元素个数,查询次数+合并次数=O(n)或以上,则平均单次查询 / 单次合并的复杂度接近O(1)。
-
算法实现(Java)
public static class Node {
// whatever you like
}
public static class DisjointSets {
public HashMap<Node, Node> fatherMap; // 用fathermap去代替链表节点next指针
public HashMap<Node, Integer> rankMap; // 用rankmap表示代表节点所在集合的大小 若非代表节点,则该信息无效
public DisjointSets() {
fatherMap = new HashMap<Node, Node>();
rankMap = new HashMap<Node, Integer>();
}
public void makeSets(List<Node> nodes) { // 构造并查集(所有元素提前知晓)
fatherMap.clear();
rankMap.clear();
for (Node node : nodes) {
fatherMap.put(node, node);
rankMap.put(node, 1);
}
}
public Node findFather(Node n) { // isSameSet
Node father = fatherMap.get(n);
if (father != n) {
father = findFather(father); // 查询链展开
}
fatherMap.put(n, father);
return father;
}
public void union(Node a, Node b) { //Union
if (a == null || b == null) {
return;
}
Node aFather = findFather(a);
Node bFather = findFather(b);
if (aFather != bFather) {
int aFrank = rankMap.get(aFather);
int bFrank = rankMap.get(bFather);
if (aFrank <= bFrank) {
fatherMap.put(aFather, bFather);
rankMap.put(bFather, aFrank + bFrank);
} else {
fatherMap.put(bFather, aFather);
rankMap.put(aFather, aFrank + bFrank);
}
}
}
}