算法<初级> - 第三章 贪心,二叉树,优先队列并查集等(完结)

算法<初级> - 第三章 贪心,二叉树,优先队列并查集等

题目十(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);
				}
			}
		}
	}
posted @ 2020-01-16 17:53  黄龙士  阅读(153)  评论(0编辑  收藏  举报