zoukankan      html  css  js  c++  java
  • 算法<初级>

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

    题目十(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);
    				}
    			}
    		}
    	}
    
  • 相关阅读:
    pagination分页插件使用
    ajax实现图片文件上传和图片预览
    屏幕护眼色调调节
    css画三角形以及实现带三角的阴影效果
    软件测试homework3
    使用Junit包进行单元测试并导入eclemma包。
    软件测试homework2
    软件测试homework1 编程过程的一个小错误
    ReentrantLock
    Elasticsearch-基本概念
  • 原文地址:https://www.cnblogs.com/ymjun/p/12202547.html
Copyright © 2011-2022 走看看