比较器与堆
1、堆
1.1、本质:根节点是最值的完全二叉树
树是满的状态,或者从左到右依次变满
1.2、堆: 逻辑结构
本质:二叉树(按照宽度优先遍历建立)
i指的是位置下标!通过公式可以找到对应的父节点或者子节点!
情况2、 i从1开始:
好处:位运算快
1.3、大小根堆
1.3.1、大根堆:任何子树中最大值都是头节点
1、插入结点 通过2.1公式计算父节点位置,然后比较值,大则交换,然后一直向上直到根结点(ONlogN)
1.3.2、小根堆:任何子树的最小值都是头节点
1.4 大根堆的构建
:
1.4.1 堆的push和pop
pop:抛出堆的最大值,然后将剩下的堆依然保持大根堆的形式

push: 添加一个值,让他到正确的地方
push和heapInsert有关,pop与heapify有关
堆结构的heapInsert与heapify操作
1、heapify:将index位置的值下沉到正确位置
思路:我们删除了最大值,也就是arr[0]位置,之后我们把堆最末尾的位置调整到arr[0]位置,堆大小减一。让现在arr[0]位置的数找左右孩子比较…,进行hearify操作,让其沉下去。沉到合适的位置之后,仍然是大根堆。对应代码的pop方法
heapify实际只有下沉操作,需要配合其他操作才能抛出堆的最大值
堆结构很重要很重要
//从index位置,往下看,不断的下沉
// 停的条件:我的孩子都不再比我大;已经没孩子了
/** 1、将根结点纪录
* 2、将heapSize的位置的值顶替根节点 (最后一个孩子顶替)
* 3、比较后重置位置heapify (最后一个孩子向下降)
* 复杂度 O(logN)
*/
private void heapify(int[] arr, int index, int heapSize) {
int left = index * 2 + 1;
while (left < heapSize) {
//左右孩子,谁大,把下标给largest
//右 ->1)有右孩子, 2)右孩子的值比左孩子大
//否则 就是左 右孩子 左孩子
int largest = left + 1 < heapSize && arr[left + 1] > arr[left] ? left + 1 : left;
//父节点和子最大值比较
largest = arr[largest] > arr[index] ? largest : index;
if (largest == index) {
break; // 当中间一个子树满足(大根堆),则不需要向下
}
//父干不过子 交换
swap(arr, largest, index);
index = largest;
//向下走
left = index * 2 + 1;
}
}
//用户,让你返回最大值,然后删除,并剩下的值保持大根堆
public int pop() {
int ans = heap[0];
swap(heap, 0,--heapSize); //注意下标从0开始
heapify(heap, 0, heapSize);
return ans;
}
2、heapInsert: 向上移动到正确的位置
思路:例如我们要构建一个大根堆,我们把所有的数依次添加到一个数组(下标从0开始)中去,每次添加一个数的时候,要去用找父亲节点的公式parent = (i-1) / 2找到父节点区比较,如果比父节点大就和父节点交换向上移动,移动后再用自己当前位置和父亲节点比较…,小于等于父节点不做处理。这样用户每加一个数,我们都能保证该结构是大根堆,对应代码的push方法
我们的调整代价实际上就是这颗树的高度层数,logN
//与父节点比较,直到下面俩情况
// 1、子比父小: arr[index] 比 arr[(index - 1) / 2]小
// 2、没有父: index = 0
private void heapInsert(int[] arr, int index) {
while (arr[index] > arr[(index - 1) / 2]) {
//包含了上面的俩情况
swap(arr, index, (index - 1) / 2);
index = (index - 1) / 2;
}
}
//push value
public void push(int value) {
if (heapSize == limit) {
throw new RuntimeException("heap is full");
}
//放到heapSize位置
heap[heapSize] = value;
//向上比较父节点尾添加 使用size去取代了index是因为堆只能尾插
heapInsert(heap, heapSize++);
}
1.5 堆排序 (升序)
1、形成大根堆
method1、O(NlogN) (while(每个点从heapsize插入))
method2、O(N) while(每个数是否能向下沉)
2、while(heap.isempty){ ans[i++] =pop() ; } //每次都抛出最大值在数组末尾,剩下的·数组形成大根堆
3、新形成的数组就是升序
1.6 与堆有关的题目
已知一个几乎有序的数组。几乎有序是指,如果把数组排好顺序的话,每个元素移动的距离一定不超过k,并且k相对于数组长度来说是比较小的。
请选择一个合适的排序策略,对这个数组进行排序。
1、先将0-k个数放入小根堆(K+1之后的数不可能成为最小值,找到最小值,弹出),
2、k+1的数放入小根堆,循环,直到遍历
package class04;
import java.util.Arrays;
import java.util.PriorityQueue;
public class Code05_SortArrayDistanceLessK {
public static void sortedArrDistanceLessK(int[] arr, int k) {
if (k == 0) {
return;
}
// 默认小根堆
PriorityQueue<Integer> heap = new PriorityQueue<>();
int index = 0;
// 0...K-1
for (; index <= Math.min(arr.length - 1, k - 1); index++) {
heap.add(arr[index]);
}
int i = 0;
for (; index < arr.length; i++, index++) {
heap.add(arr[index]);
arr[i] = heap.poll();
}
while (!heap.isEmpty()) {
arr[i++] = heap.poll();
}
}
// for test
public static void comparator(int[] arr, int k) {
Arrays.sort(arr);
}
// for test
public static int[] randomArrayNoMoveMoreK(int maxSize, int maxValue, int K) {
int[] arr = new int[(int) ((maxSize + 1) * Math.random())];
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;
}
// for test
public static int[] copyArray(int[] arr) {
if (arr == null) {
return null;
}
int[] res = new int[arr.length];
for (int i = 0; i < arr.length; i++) {
res[i] = arr[i];
}
return res;
}
// for test
public static boolean isEqual(int[] arr1, int[] arr2) {
if ((arr1 == null && arr2 != null) || (arr1 != null && arr2 == null)) {
return false;
}
if (arr1 == null && arr2 == null) {
return true;
}
if (arr1.length != arr2.length) {
return false;
}
for (int i = 0; i < arr1.length; i++) {
if (arr1[i] != arr2[i]) {
return false;
}
}
return true;
}
// for test
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 void main(String[] args) {
System.out.println("test begin");
int testTime = 500000;
int maxSize = 100;
int maxValue = 100;
boolean succeed = true;
for (int i = 0; i < testTime; i++) {
int k = (int) (Math.random() * maxSize) + 1;
int[] arr = randomArrayNoMoveMoreK(maxSize, maxValue, k);
int[] arr1 = copyArray(arr);
int[] arr2 = copyArray(arr);
sortedArrDistanceLessK(arr1, k);
comparator(arr2, k);
if (!isEqual(arr1, arr2)) {
succeed = false;
System.out.println("K : " + k);
printArray(arr);
printArray(arr1);
printArray(arr2);
break;
}
}
System.out.println(succeed ? "Nice!" : "Fucking fucked!");
}
}
1.7、优先级队列
1.7.1 底层: 默认小根堆
2 比较器
2.1定义
1)比较器的实质就是重载比较运算符
2)比较器可以很好的应用在特殊标准的排序上 : (比较器实现大根堆)
3)比较器可以很好的应用在根据特殊标准排序的结构上
4)写代码变得异常容易,还用于范型编程
2.2统一定义
即如下方法:比较器实现comparator接口
@Override
public int compare(T o1, T o2) ;
返回负数的情况,就是o1比o2优先的情况
返回正数的情况,就是o2比o1优先的情况
返回0的情况,就是o1与o2同样优先的情况
public static class IdAscendingComparator implements Comparator<Student> {
// 返回负数的时候,第一个参数排在前面
// 返回正数的时候,第二个参数排在前面
// 返回0的时候,谁在前面无所谓
@Override
public int compare(Student o1, Student o2) {
return o1.id - o2.id;
}
}
3、语言提供的堆结构 vs 手写的堆结构
取决于,你有没有动态改信息的需求!
语言提供的堆结构,如果你动态改数据,不保证依然有序
手写堆结构,因为增加了对象的位置表,所以能够满足动态改信息的需求