算法17:堆结构_相关面试题
什么是堆结构
1)堆结构就是用数组实现的完全二叉树结构
2)完全二叉树中如果每棵子树的最大值都在顶部就是大根堆
3)完全二叉树中如果每棵子树的最小值都在顶部就是小根堆
4)堆结构的heapInsert与heapify操作
5)堆结构的增大和减少
6)优先级队列结构,就是堆结构
如何构建堆结构?
package code2.排序_03.堆排序; /** * 构建大根堆 */ public class Code01_MyMaxHeap { private int limit; private int[] heap; private int size; public Code01_MyMaxHeap(int limit) { this.limit = limit; heap = new int[this.limit]; size = 0; } private void swap(int[] arr, int i, int j) { int temp = arr[i]; arr[i] = arr[j]; arr[j] = temp; } //上移式构建大根堆 public void heapInsert (int[] arr, int index) { while (index < this.limit && arr[index] > arr[(index-1)/2]) { swap(arr, index, (index-1)/2); index = (index-1)/2; } } //下层式构建大根堆 public void heapify(int[] arr, int index, int length) { /** * 二叉树性质5: * 1. 如果index为0,则代表这是第一个节点,则无双亲节点 * 2. 如果index*2 > n(n代表长度),则节点无左孩子。 java中下标是从0开始,因此 index*2 + 1 > n * 3. 如果index*2+1 >n, 则代表无右孩子。 java中下标是从0开始,因此 index*2 + 2 > n */ int left = index * 2 + 1; while (left < length) { int bigger = (left + 1) < length && arr[left] < arr[left + 1] ? left + 1: left; if(arr[index] >= arr[bigger]) { break; } swap(arr, index, bigger); index = bigger; left = index * 2 + 1; } } public void push1(int value) { if (size == this.limit) { throw new RuntimeException("堆已满,无法添加新数据!"); } heap[size] = value; heapInsert(heap, size++); } public void push2(int[] arr) { if (size == this.limit) { throw new RuntimeException("堆已满,无法添加新数据!"); } heap = arr; for (int i = arr.length-1; i >=0; i--,size++) { heapify(heap, i, arr.length); } } public int pop() { if (size < 0) { throw new RuntimeException("堆为空,无法获取值"); } int ans = heap[0]; //swap(heap, 0, heap.length -1); /** * 好处: * 1. --size正好和数组长度相同 * 2. 在heapify中,作为数组长度使用。变相的减少了1个长度。 * 使最后的11不在作为父节点下沉的判断条件。参考代码36行 */ swap(heap, 0, --size); heapify(heap, 0, size); return ans; } public static void main(String[] args) { //case 1: 上移式构建大根堆,结果是11,7,8,6,4,7,0,3 /* int[] arr = {3,7,11,4,6,8,0,7}; MyMaxHeap my = new MyMaxHeap(arr.length); for (int i =0; i < arr.length; i++) { my.push1(arr[i]); }*/ //case 2: 下沉式构建大根堆。 大根堆的结果是11,7,8,7,6,3,0,4 int[] arr = {3,7,11,4,6,8,0,7}; Code01_MyMaxHeap my = new Code01_MyMaxHeap(arr.length); my.push2(arr); //case 3: 删除大根堆最大值,要求剩下的节点依旧维持大根堆的结构 int value = my.pop(); System.out.println("弹出值为:" + value); } }
上面的代码不难,相信阅读完代码以后,会对堆结构有一点了解。提到堆结构,离不开的就是堆排序问题了。
堆排序
package code2.排序_03.堆排序; /** * 构建大根堆 * 1,先让整个数组都变成大根堆结构,建立堆的过程: * 1)从上到下的方法,时间复杂度为O(N*logN) * 2)从下到上的方法,时间复杂度为O(N) * 2,把堆的最大值和堆末尾的值交换,然后减少堆的大小之后,再去调整堆,一直周而复始,时间复杂度为O(N*logN) * 3,堆的大小减小成0之后,排序完成 */ public class Code02_HeapSort { private void swap(int[] arr, int i, int j) { int temp = arr[i]; arr[i] = arr[j]; arr[j] = temp; } public void printArray(int[] arr) { if (arr == null) { return; } for (int i = 0; i < arr.length; i++) { System.out.print(arr[i] + " "); } System.out.println(); } public int[] heapSort(int[] arr) { if (arr == null || arr.length < 2) { return arr; //不满足条件,就返回原数组 } int size = arr.length; //首先构造大根堆, 从下网上,时间复杂度为O(N) for (int i = size-1; i>=0; i--) { heapify(arr, i, size); } //将最大值放在大根堆的最后 swap(arr, 0, --size); //迭代,重新生成大根堆,并且将最大值放在最后. //因为第一次已经交换完毕,所以接下来的会逐个减少,知道最后一个是根元素为止 while(size > 0) { /*for (int i = size; i>=0; i--) { heapify(arr, i, size); }*/ //下面一行代码,等价与上方的一个for循环。 for循环是从数组的最后 //一个元素开始下沉。而下方代码是基于上一次调整完成以后,新的数组的根节点开始下沉 //从上往下,时间复杂度为O(N*logN) heapify(arr, 0, size); swap(arr, 0, --size); } return arr; } //下层式构建大根堆 public void heapify(int[] arr, int index, int length) { /** * 二叉树性质5: * 1. 如果index为0,则代表这是第一个节点,则无双亲节点 * 2. 如果index*2 > n(n代表长度),则节点无左孩子。 java中下标是从0开始,因此 index*2 + 1 > n * 3. 如果index*2+1 >n, 则代表无右孩子。 java中下标是从0开始,因此 index*2 + 2 > n */ int left = index * 2 + 1; while (left < length) { int bigger = (left + 1) < length && arr[left] < arr[left + 1] ? left + 1: left; if(arr[index] >= arr[bigger]) { break; } swap(arr, index, bigger); index = bigger; left = index * 2 + 1; } } public static void main(String[] args) { //case 2: 下沉式构建大根堆。 大根堆的结果是11,7,8,7,6,3,0,4 int[] arr = {3,7,11,4,6,8,0,7}; Code02_HeapSort heap = new Code02_HeapSort(); int[] sortArr = heap.heapSort(arr); heap.printArray(sortArr); } }
了解堆结构和堆排序只是基础,有了以上的铺垫,才能更好的帮助我们理解堆相关的算法以及面试题。
算法一:已知一个几乎有序的数组。几乎有序是指,如果把数组排好顺序的话,每个元素移动的距离一定不超过k,并且k相对于数组长度来说是比较小的。请选择一个合适的排序策略,对这个数组进行排序。
这道题,其实想要考查的就是堆的相关知识。试想一下,如果k是一个比较小的数,例如3. 每个元素移动的距离一点不会超过3. 假设,每个数移动距离都是都是最大距离 3,那么如果数组下标为 0,1,2怎么办 ?必然只有往后移动才可以. 而从 k + 1开始,也就是从4开始,数据既可以往前移动,也可以往后移动。 于此同时,当 数据达到4个的时候,前 个数必然能确定一个最小值(或者最大值),因为每个元素移动的距离一定不超过k。 借助于堆结构对象 PriorityQueue 便能够很好的解决这个问题。 代码如下:
package code2.排序_03.堆排序; import jdk.internal.org.objectweb.asm.util.CheckAnnotationAdapter; import java.util.Arrays; import java.util.PriorityQueue; /** * 已知一个几乎有序的数组。几乎有序是指,如果把数组排好顺序的话,每个元素移动的距离一定不超过k,并且k相对于数组长度来说是比较小的。 * 请选择一个合适的排序策略,对这个数组进行排序。 */ public class Code03_HeapSortForLenthLessK { public static void printArray(int[] arr) { if (arr == null) { return; } for (int i = 0; i < arr.length; i++) { System.out.print(arr[i] + " "); } System.out.println(); } // for test public static int[] randomArrayNoMoveMoreK(int maxSize, int maxValue, int k) { double val = Math.random(); int[] arr = new int[(int) ((maxSize + 1) * val)]; for (int i = 0; i < arr.length; i++) { arr[i] = (int) ((maxValue + 1) * Math.random()) - (int) (maxValue * Math.random()); } // 先排个序 Arrays.sort(arr); // 然后开始随意交换,但是保证每个数距离不超过K // swap[i] == true, 表示i位置已经参与过交换 // swap[i] == false, 表示i位置没有参与过交换 boolean[] isSwap = new boolean[arr.length]; for (int i = 0; i < arr.length; i++) { int j = Math.min(i + (int) (Math.random() * (k + 1)), arr.length - 1); if (!isSwap[i] && !isSwap[j]) { isSwap[i] = true; isSwap[j] = true; int tmp = arr[i]; arr[i] = arr[j]; arr[j] = tmp; } } return arr; } public static void sort (int[] arr, int k) { if (arr == null || arr.length < 2 || k == 0) { return ; } //利用系统提供的堆结构,可以直接实现数组的排序 PriorityQueue<Integer> heap = new PriorityQueue<>(); int index = 0; /** * 每个数组的移动距离都不超过k, 也就是每个数组要么最多往前移动k个, * 要么最多往后移动k个数。 * 从0 到 k-1, 如果要想移动k个数,那么只能往后移动,如果是往前移动 * 那么丢在系统堆中会默认进行排序 */ for (;index <= k-1;index++) { heap.add(arr[index]); } int i = 0; /** * 从此时开始,数据存在往前移动或者往后移动 k 个数的情况 * 因此,如果是往前移动 k 个,那么丢入堆中,poll()出来的必然是最新一次丢入 * 的数据; 如果是往后移动k个,那么堆中的最小值肯定是之前丢进去的,直接拿出来即可 * * 假设k =5, {6,5,4,3,2}, 那么如果出现 1, 那么poll出来的必然是1. 因为数组基本 * 有序且最多移动不超过k, 那么从左到右出现 k+1个数的时候,此时必然存在最小数 */ for (; index < arr.length; index++, i++) { heap.add(arr[index]); arr[i] = heap.poll(); } while (!heap.isEmpty()) { arr[i++] = heap.poll(); } } public static void main(String[] args) { int maxSize = 100; int maxValue = 100; int k = 5; int[] arr = randomArrayNoMoveMoreK(maxSize,maxValue,k); //排序前 printArray(arr); sort(arr, k); //排序后 printArray(arr); } }
算法二 :最大线段重合问题(用堆实现)
给定很多线段,每个线段都有两个数[start, end],表示线段开始位置和结束位置,左右都是闭区间
规定:
1)线段的开始和结束位置一定都是整数值
2)线段重合区域的长度必须>=1
返回线段最多重合区域中,包含了几条线段
解题思路:这个题中每个线段都有一个开始位置start 和 一个结束位置 end。 如果我们按照开始位置start进行初次排序,则会得到一个开始位置有序的线段数组,类似于 {{1,7}, {1,6}, {2,5}, {,63}......}. 然后再遍历数组每个线段的开始位置和结束位置。如果新线段开始的位置值(已经有序)大于堆结构中存储的线段末尾值,那么删除对中对应数据,并且将新线段的 end值存入数组中。为什么能这么操作?因为一开始我们便对线段的start进行了排序,以后的线段只能大于或者等于现在的线段的start值,这一点是非常重要。
package code2.排序_03.堆排序; import java.util.Arrays; import java.util.Comparator; import java.util.PriorityQueue; /** * 最大线段重合问题(用堆的实现) * 给定很多线段,每个线段都有两个数[start, end], * 表示线段开始位置和结束位置,左右都是闭区间 * 规定: * 1)线段的开始和结束位置一定都是整数值 * 2)线段重合区域的长度必须>=1 * 返回线段最多重合区域中,包含了几条线段 */ public class Code04_MaxLineSegment { class Line { public int start; public int end; Line(int start,int end) { this.start = start; this.end = end; } } class MyComparator implements Comparator<Line> { @Override public int compare(Line o1, Line o2) { return o1.start - o2.start; } } /** * 随机构造线段 * @param N 代码线段的条数 * @param L 代表线段的开始值 * @param R 代表线段的结束值 * @return */ public int[][] generateLines(int N, int L, int R) { int size = (int) (Math.random() * N) + 1; int[][] ans = new int[size][2]; for (int i = 0; i < size; i++) { int a = L + (int) (Math.random() * (R - L + 1)); int b = L + (int) (Math.random() * (R - L + 1)); if (a == b) { b = a + 1; } ans[i][0] = Math.min(a, b); ans[i][1] = Math.max(a, b); } return ans; } public int maxValue (int[][] segments) { Line[] lines = new Line[segments.length]; for(int i =0; i < segments.length; i++) { int[] arr = segments[i]; lines[i] = new Line(arr[0], arr[1]); } //按照Line对象的start进行排序 //end > start, 所以此处的排序非常重要 //比如, m = { {5,7}, {1,4}, {2,6} } 跑完如下的code之后变成:{ {1,4}, {2,6}, {5,7} } Arrays.sort(lines, new MyComparator()); //小根堆,默认升序 PriorityQueue<Integer> queue = new PriorityQueue(); int max = 0; for (Line line : lines) { //正因为右了上方Arrays.sort的排序,我们才可以进行此处while的操作 while (!queue.isEmpty() && queue.peek() <= line.start) { queue.poll(); } queue.add(line.end); //这一段代码用的非常的巧妙。举个例子{{1,3},{1,4},{2,5},{4,8}} //{2,3}共出现了3次,{4,8}出现的时候会将1,3},{1,4}都删除。此时的size为2, //但是,我们依旧可以获得最大值为3 max = Math.max(max, queue.size()); } return max; } //仅仅为了测试而准备 public int maxCover1(int[][] lines) { int min = Integer.MAX_VALUE; int max = Integer.MIN_VALUE; for (int i = 0; i < lines.length; i++) { min = Math.min(min, lines[i][0]); max = Math.max(max, lines[i][1]); } int cover = 0; for (double p = min + 0.5; p < max; p += 1) { int cur = 0; for (int i = 0; i < lines.length; i++) { if (lines[i][0] < p && lines[i][1] > p) { cur++; } } cover = Math.max(cover, cur); } return cover; } public static void main(String[] args) { /** * 首先测试简单case * {{1,5},{2,9},{1,4},{1,3},{1,,6}} ==> 我们知道重合最多的的是{2,3},共计5次 */ Code04_MaxLineSegment test = new Code04_MaxLineSegment(); //int[][] arr = {{1,5},{2,9},{1,4},{1,3},{1,6}}; int[][] arr = {{1,3},{1,4},{2,5},{4,8}}; int max = test.maxValue(arr); System.out.println("最多处包含的线段为: " + max); //下面进行一些大批量随机数的测试 int N = 100; int L = 0; int R = 200; int testTimes = 200000; System.out.println("test end"); for (int i = 0; i < testTimes; i++) { int[][] lines = test.generateLines(N, L, R); int ans1 = test.maxCover1(lines); int ans2 = test.maxValue(lines); if (ans1 != ans2) { System.out.println("Oops!"); } } System.out.println("test end"); } }
算法三:手写加强堆
系统提供的堆无法做到的事情:
1)已经入堆的元素,如果参与排序的指标方法变化,
系统提供的堆无法做到时间复杂度O(logN)调整!都是O(N)的调整!
2)系统提供的堆只能弹出堆顶,做不到自由删除任何一个堆中的元素,
或者说,无法在时间复杂度O(logN)内完成!一定会高于O(logN)
根本原因:无反向索引表
所以,有了加强堆,我们便可以新增很多新的功能。比如,删除堆中的任意一个元素,修改任意一个元素的值等等。所以,手写加强对非常的重要。
package code2.排序_03.堆排序; import java.util.ArrayList; import java.util.Comparator; import java.util.HashMap; import java.util.List; /** * 系统提供的堆无法做到的事情: * 1)已经入堆的元素,如果参与排序的指标方法变化, * 系统提供的堆无法做到时间复杂度O(logN)调整!都是O(N)的调整! * 2)系统提供的堆只能弹出堆顶,做不到自由删除任何一个堆中的元素, * 或者说,无法在时间复杂度O(logN)内完成!一定会高于O(logN) * 根本原因:无反向索引表 */ public class Code05_EnhanceHeap<T> { private ArrayList<T> heap; //动态数组替换 int[] private HashMap<T, Integer> indexMap; //索引表 private int heapSize; private Comparator<? super T> comp; //比较器,可以根据客户提供的比较器进行升序和降序排序 Code05_EnhanceHeap(Comparator<? super T> comp) { heap = new ArrayList<>(); indexMap = new HashMap<>(); heapSize = 0; this.comp = comp; } private void swap(int i, int j) { T o1 = heap.get(i); T o2 = heap.get(j); heap.set(i, o2); heap.set(j, o1); indexMap.put(o2, i); indexMap.put(o1, j); } private void resign(T obj) { //下面两个方法只会执行其中的一个 heapInsert(indexMap.get(obj)); heapify(indexMap.get(obj)); } private void heapInsert (int index) { //大根堆還是小根堆,完全取決於比較器。 //如果比較器默認是升序,則生成大根堆 //如果比較器默認是降序,則生成小根堆 while (comp.compare(heap.get(index), heap.get((index-1)/2)) < 0) { swap(index, (index-1)/2); index = (index-1)/2; } } private void heapify(int index) { /** * 二叉树性质5: * 1. 如果index为0,则代表这是第一个节点,则无双亲节点 * 2. 如果index*2 > n(n代表长度),则节点无左孩子。 java中下标是从0开始,因此 index*2 + 1 > n * 3. 如果index*2+1 >n, 则代表无右孩子。 java中下标是从0开始,因此 index*2 + 2 > n */ int left = index * 2 + 1; while (left < heapSize) { int best = (left + 1) < heapSize && comp.compare(heap.get(left+1), heap.get(left)) < 0 ? left + 1: left; //父节点小於等于子节点,无需下沉 if(comp.compare(heap.get(index), heap.get(best)) <= 0) { break; } swap(index, best); index = best; left = index * 2 + 1; } } public boolean isEmpty () { return heapSize == 0; } public int size () { return heapSize; } public boolean contains(T obj) { return indexMap.containsKey(obj); } public T peek() { return heap.get(0); } public void push(T obj) { heap.add(obj); indexMap.put(obj, heapSize); heapInsert(heapSize++); } //删除堆顶 public T pop() { T ans = heap.get(0); swap(0, heapSize-1); indexMap.remove(ans); heap.remove(--heapSize); //此处删除,实际上heap中元素就被删除了,长度需要减1 heapify(0); return ans; } // 请返回堆上的所有元素 public List<T> getAllElements() { List<T> ans = new ArrayList<>(); for (T c : heap) { ans.add(c); } return ans; } public void remove(T obj) { T replace = heap.get(heapSize - 1); //记录最后一个元素 int index = indexMap.get(obj); //获取要删除数的索引 indexMap.remove(obj); //从索引表中删除当前数 heap.remove(--heapSize); //从堆中删除最后一个数 if (obj != replace) { heap.set(index, replace); indexMap.put(replace, index); resign(replace); } } public static void printArray(List list) { if (list == null || list.isEmpty()) { return; } for (int i = 0; i < list.size(); i++) { Customer c = (Customer) list.get(i); System.out.println(c.toString()); } System.out.println(); } static class Customer { int id; int age; String name; Customer(int id, int age, String name) { this.id = id; this.name = name; this.age = age; } @Override public String toString() { return "id :" + id + " age: " + age + " name " + name; } } static class MyIdComprator implements Comparator<Customer> { /** * 統一規範 * 如果為負數, 認為第一個數排前面 * 如果整數,認為第二個數排前面 * 0 無所謂 * @param o1 * @param o2 * @return */ @Override public int compare(Customer o1, Customer o2) { //按照id排序,如果ID相同则按照年龄进行排序 return o1.id - o2.id; } } public static void main(String[] args) { //case1: 测试加强堆的构建 Code05_EnhanceHeap test = new Code05_EnhanceHeap(new MyIdComprator()); test.push(new Customer(1, 11, "zhangsan")); Customer c2 = new Customer(5, 12, "lisi"); test.push(c2); test.push(new Customer(2, 16, "malong")); test.push(new Customer(3, 11, "haoy")); test.push(new Customer(4, 13, "pp")); Customer c6 = new Customer(6, 12, "cy"); test.push(c6); test.push(new Customer(2, 15, "leo")); test.push(new Customer(7, 12, "kiki")); printArray(test.getAllElements()); //case2:测试常用方法 System.out.println("============case 2============="); System.out.println(test.isEmpty()); System.out.println(test.contains(c2)); System.out.println(test.size()); System.out.println(test.peek()); //peek是获取不删除 System.out.println(test.size()); //case3 测试删除一个节点,并且还是保大根堆结构 System.out.println("============case 3============="); test.pop(); printArray(test.getAllElements()); System.out.println("============case 4============="); test.push(new Customer(9, 12, "yy")); printArray(test.getAllElements()); System.out.println("============case 5============="); test.remove(c6); printArray(test.getAllElements()); System.out.println(test.contains(c6)); System.out.println("============case 6 取出全部元素,看看是否有序============="); int i =0; while (!test.isEmpty()) { System.out.println(" 第 " + i + " 次:" + test.pop()); } } }