什么是堆结构
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());
}
}
}