面试算法系列-04比较器与堆

比较器与堆

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接口

	@Overridepublic 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 手写的堆结构

​ 取决于,你有没有动态改信息的需求!

​ 语言提供的堆结构,如果你动态改数据,不保证依然有序

​ 手写堆结构,因为增加了对象的位置表,所以能够满足动态改信息的需求

猜你喜欢

转载自blog.csdn.net/huihui5210/article/details/112689752