1 堆结构
1.1 相关概念
- 堆结构就是用数组实现的完全二叉树
- 大根堆:每棵子树的最大值都在顶部
- 小根堆:每课子树的最小值都在顶部
- 堆结构的heapInsert(上升)与heapify(下沉)操作
- 堆结构的增大和减小
- 优先级队列就是堆结构(PriorityQueue)
1.2 实现堆结构
全部代码:
public static class MyMaxHeap2{
//数组:用于存放堆数据
private int[] heap;
//用于指定数组大小
private final int limit;
//用于标记堆大小
private int heapSize;
public MyMaxHeap2(int limit){
heap = new int[limit];
this.limit = limit;
//初始堆大小为0
heapSize = 0;
}
//判断堆是否为空
public boolean isEmpty(){
return heapSize == 0;
}
//判断是否将数组所有元素都存入堆中了【是否满了】
public boolean isFull(){
return limit == heapSize;
}
//将元素放入堆中
public void push(int val){
//堆中元素已满
if(heapSize == limit){
throw new RuntimeException("heap is full");
}
heap[heapSize] = val;
heapInsert(heap, heapSize++);
}
//pop
//返回最大值,并且在大根堆中,将最大值删掉【逻辑删除】
//剩下的数,依然保持大根堆结构
public int pop(){
//0位置为最大值【大根堆】
int ans = heap[0];
//让0位置与数组最后一个位置交换
swap(heap, 0, --heapSize);
heapify(heap, 0, heapSize);
return ans;
}
//上浮
//新加进来的数,现在停在了index位置 -> 上浮
//移动到0位置或干不掉自己的父亲节点了,停
private void heapInsert(int[] arr, int index){
//大于自己的父亲,交换
while(arr[index] > arr[(index - 1) / 2]){
swap(heap, index, (index - 1) / 2);
//来到自己父亲的位置
index = (index - 1) / 2;
}
}
//下沉
//从index位置往下看,不断地下沉
//停:较大的孩子不再比index位置的数大;或已经没孩子了
private void heapify(int[] arr, int index, int heapSize){
//左孩子位置
int left = index * 2 + 1;
while(left < heapSize){//如果有左孩子;有没有右孩子,可能没有
//把较大孩子的下标给largest
//left:index下面的左孩子
//left+1:右孩子
int largest = left + 1 < heapSize && arr[left + 1] > arr[left] ? left + 1 : left;
//将孩子节点的最大值与index的值比较,看谁大
largest = arr[largest] > arr[index] ? largest : index;
if(largest == index){
//index的值大,不用交换,直接返回
break;
}
//将index与较大孩子交换
swap(arr, largest, index);
//继续下沉
index = largest;
left = index * 2 + 1;
}
}
public void swap(int[] arr, int i, int j){
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
}
1.3 拓展
我们也可以使用Java内部自带的优先级队列来实现堆结构(默认小跟堆),如果想要创建大根堆,我们可以通过自定义比较器来实现(O2 - O1)
代码:
//自定义比较器,实现大根堆
public static class MyComparator1 implements Comparator<Integer> {
@Override
public int compare(Integer o1, Integer o2) {
//- (o1 - o2)
return o2 - o1;
}
}
public static void main(String[] args) {
//默认小跟堆
PriorityQueue<Integer> heap1 = new PriorityQueue<>();
heap1.add(2);
heap1.add(9);
heap1.add(1);
heap1.add(-2);
while(!heap1.isEmpty()){
System.out.print(heap1.poll() + " ");
}
System.out.println();
//创建自定义比较器【大的放在前】
MyComparator1 myComparator1 = new MyComparator1();
PriorityQueue<Integer> heap2 = new PriorityQueue<>(myComparator1);
heap2.add(4);
heap2.add(1);
heap2.add(-4);
heap2.add(7);
heap2.add(8);
while(!heap2.isEmpty()){
System.out.print(heap2.poll() + " ");
}
//8 7 4 1 -4
System.out.println();
}
2 堆排序
- 先让整个数组都变成大根堆,建立堆的过程:
1)从上到下:O(N*logN)
2)从下到上:O(N) - 把堆的最大值与堆末尾的值(数组末尾)交换,然后减小堆的大小(heapSize)之后,再去调整堆,周而复始,时间复杂度为O(N*logN)【每次选出最大的】
- 堆的大小减少到0之后,排序完成
2.1 heapInsert(int[] arr, int index) 上浮
堆排序,上浮功能实现
/**
* 上浮 :只用传入index下标即可,因为是往上走,不会越界
* @param arr
* @param index
*/
public static void heapInsert(int[] arr, int index){
while(arr[index] > arr[(index - 1) / 2]){
swap(arr, index, (index - 1) / 2);
index = (index - 1) / 2;
}
}
2.2 heapify(int[] arr, int index, int heapSize) 下沉
/**
* 下沉:传入arr数组【堆结构】及heapSize(堆元素个数)
* @param arr
* @param index
* @param heapSize
*/
public static void heapify(int[] arr, int index, int heapSize){
int left = index * 2 + 1;//左孩子下标
while(left < heapSize){//下方还有孩子【为什么是用left比较呢?是因为默认以完全二叉树来排列】
//两个孩子谁大,谁把下标值给largest
//1)只有左孩子, left -> largest
//2)同时有左孩子和右孩子, 右孩子的值 <= 左孩子的值, left -> largest
//同时有左孩子和右孩子,右孩子的值 > 左孩子, right -> largest
int largest = left + 1 < heapSize && arr[left + 1] > arr[left] ? left + 1 : left;
//父和较大孩子之前,谁的值大,谁就把下标给largest
largest = arr[largest] > arr[index] ? largest : index;
if (largest == index) {
break;
}
swap(arr, largest, index);
index = largest;
left = index * 2 + 1;
}
}
2.3 swap(int[] arr, int i, int j) 交换
public static void swap(int[] arr, int i, int j) {
int tmp = arr[i];
arr[i] = arr[j];
arr[j] = tmp;
}
2.4 heapSort(int[] arr) 核心逻辑
首先,将数组中的数全部下沉,进行堆结构,然后再交换首末位置,每次找出最大值,最后再上浮
//堆排序
public static void heapSort(int[] arr){
if(arr == null || arr.length < 2){
return;
}
//第一种方法:
//O(N)
for(int i = arr.length - 1; i >= 0; i--){
//下沉
heapify(arr, i, arr.length);
}
//第二种
// O(N*logN)
//for (int i = 0; i < arr.length; i++) { // O(N)
// heapInsert(arr, i); // O(logN)
//}
int heapSize = arr.length;
swap(arr, 0, --heapSize);
//O(N*logN)
while(heapSize > 0){//O(N)
heapify(arr, 0, heapSize);
swap(arr, 0, --heapSize);//每次找到最大
}
}
具体实现逻辑中:
①下沉 - 交换 - 上浮 【O(N)】:提前知道所有数组(堆)的组成元素
②上浮 - 交换 - 上浮 【O(N*logN)】
3 相关算法题
3.1 最大线段重合问题
给定很多线段,每个线段都有两个数[start, end],表示线段开始和结束为止
规定:
1)线段的开始和结束位置都是整数值
2)线段重合区域长度必须>=1
返回线段最多重合区域中,包含了几条线段
思路:
3.1.1 创建内部类,用来表示每条线段
//定义内部类用来表示线段
public static class Line{
private int start;
private int end;
public Line(int s, int e){
this.start = s;
this.end = e;
}
}
3.1.2 自定义比较器,按照线段的起始点排序
//自定义比较器,将数组按照start大小排序
public static class MyComparator implements Comparator<Line>{
@Override
public int compare(Line o1, Line o2) {
return o1.start - o2.start;
}
}
3.1.3 主要逻辑【小根堆】
创建小根堆,每次将line.start与堆顶元素比较
public static int maxCover2(int[][] m){
Line[] lines = new Line[m.length];
for(int i = 0; i < lines.length; i++){
lines[i] = new Line(m[i][0], m[i][1]);
}
//按照起始点排序【创建自定义比较器】
Arrays.sort(lines, new MyComparator());
//创建小根堆【优先级队列,默认小根堆】
PriorityQueue<Integer> heap = new PriorityQueue<>();
int res = 0;
for(int i = 0; i < lines.length; i++){
//堆不为空 同时 堆顶元素 <= line.start【小于等于起始:刚好在范围外】
// |__|__|
// |____|
while(!heap.isEmpty() && heap.peek() <= lines[i].start){
//出堆
heap.poll();
}
//将新Line的end加入堆
heap.add(lines[i].end);
res = Math.max(res, heap.size());//每次与堆中元素个数比较,保留最大值
}
return res;
}
3.1.4 全部代码
//定义内部类用来表示线段
public static class Line{
private int start;
private int end;
public Line(int s, int e){
this.start = s;
this.end = e;
}
}
//自定义比较器,将数组按照start大小排序
public static class MyComparator implements Comparator<Line>{
@Override
public int compare(Line o1, Line o2) {
return o1.start - o2.start;
}
}
public static int maxCover2(int[][] m){
Line[] lines = new Line[m.length];
for(int i = 0; i < lines.length; i++){
lines[i] = new Line(m[i][0], m[i][1]);
}
//按照起始点排序【创建自定义比较器】
Arrays.sort(lines, new MyComparator());
//创建小根堆【优先级队列,默认小根堆】
PriorityQueue<Integer> heap = new PriorityQueue<>();
int res = 0;
for(int i = 0; i < lines.length; i++){
//堆不为空 同时 堆顶元素 <= line.start【小于等于起始:刚好在范围外】
// |__|__|
// |____|
while(!heap.isEmpty() && heap.peek() <= lines[i].start){
//出堆
heap.poll();
}
//将新Line的end加入堆
heap.add(lines[i].end);
res = Math.max(res, heap.size());//每次与堆中元素个数比较,保留最大值
}
return res;
}