前言
本文主要介绍了堆和比较器:堆包括大根堆和小根堆;比较器的实质就是重载比较运算符,可以用于普通方式的排序和自定义的排序。
1.堆
完全二叉树:
上面层的节点都是满的,最下层要么是满的,要么左边节点是满的且连续的。
数组也能实现完全二叉树:
有的实现中,0位置不用,从1开始:
这样做的原因是可以直接使用位运算代替算术运算,提高运算速度。
堆结构:
1)堆结构就是用数组实现的完全二叉树结构;
2)完全二叉树中如果每棵子树的最大值都在顶部就是大根堆;
3)完全二叉树中如果每棵子树的最小值都在顶部就是小根堆;
4)堆结构的heapInsert与heapify操作;
5)堆结构的增大和减少;
6)优先级队列结构,就是堆结构。
堆是在完全二叉树的基础上实现的,分为大根堆和小根堆:
大根堆:
每一个子树的最大值都是该子树头节点的值。
举例如下:
小根堆:
每一个子树的最小值都是该子树头节点的值。
将给定的一系列数组成为大根堆。
图示如下:
实现大根堆如下:
package heap04;
/**
* @author Corley
* @date 2021/10/12 20:16
* @description LeetCodeAlgorithmZuo-heap04
*/
public class Heap {
class MaxHeap {
private int[] heap;
private final int limit;
private int heapSize;
MaxHeap(int limit) {
heap = new int[limit];
this.limit = limit;
heapSize = 0;
}
/*
加入新元素
*/
public void push(int value) {
if (heapSize == limit) {
throw new RuntimeException("Heap is full!");
}
heap[heapSize++] = value;
heapInsert(heap, heapSize);
}
/*
新加进来的数,现在停在了index位置,请依次往上移动
*/
private void heapInsert(int[] arr, int index) {
while (arr[index] > arr[(index - 1) / 2]) {
swap(arr, index, (index - 1) / 2);
}
}
public void swap(int[] arr, int i, int j) {
int tmp = arr[i];
arr[i] = arr[j];
arr[j] = tmp;
}
}
}
现在分析加入元素时调整堆结构的代价:
堆的深度为LogN级别;
所以加入新元素时调整为二根堆的代价也为LogN。
实现找到大根堆中的最大值,并删除最大值,然后再调整为大根堆。
示意如下:
实现如下:
/*
* 实现找到大根堆中的最大值,并删除最大值,然后再调整为大根堆
* */
public int pop() {
int ans = heap[0];
swap(heap, 0, --heapSize);
heapify(heap, 0, heapSize);
return ans;
}
/*
从index位置,往下看,不断的下沉
停:较大的子节点都不再比index位置的数大;已经没子节点
*/
public void heapify(int[] arr, int index, int heapSize) {
int left = 2 * index + 1;
while (left < heapSize) {
int largest = left + 1 < heapSize && arr[left] < arr[left + 1] ? left + 1 : left;
largest = arr[largest] > arr[index] ? largest : index;
if (largest == index) {
break;
}
swap(arr, largest, index);
index = largest;
left = 2 * index + 1;
}
}
显然,heapify方法的复杂度也是LogN。
现在进一步实现堆排序:
package heap04;
/**
* @author Corley
* @date 2021/10/12 20:16
* @description LeetCodeAlgorithmZuo-heap04
*/
public class HeapSort {
/*
新加进来的数,现在停在了index位置,请依次往上移动
*/
private void heapInsert(int[] arr, int index) {
while (arr[index] > arr[(index - 1) / 2]) {
swap(arr, index, (index - 1) / 2);
}
}
public void swap(int[] arr, int i, int j) {
int tmp = arr[i];
arr[i] = arr[j];
arr[j] = tmp;
}
/*
从index位置,往下看,不断的下沉
停:较大的子节点都不再比index位置的数大;已经没子节点
*/
public void heapify(int[] arr, int index, int heapSize) {
int left = 2 * index + 1;
while (left < heapSize) {
int largest = left + 1 < heapSize && arr[left] < arr[left + 1] ? left + 1 : left;
largest = arr[largest] > arr[index] ? largest : index;
if (largest == index) {
break;
}
swap(arr, largest, index);
index = largest;
left = 2 * index + 1;
}
}
/*
* 堆排序
* */
public void heapSort(int[] arr) {
if (null == arr || arr.length < 2) {
return;
}
for (int i = 0; i < arr.length; i++) {
heapInsert(arr, i);
}
int heapSize = arr.length;
swap(arr, 0, --heapSize);
while (heapSize > 0) {
heapify(arr, 0, heapSize);
swap(arr, 0, --heapSize);
}
}
}
现在分析时间复杂度:
建堆时,调用heapInsert(arr, i)
方法N次,时间复杂度为O(N*LogN)
,while循环也是O(N*LogN)
,整体时间复杂度也是O(N*LogN)
。
如果只是将数组变成大根堆,而不一定数组有序,可以优化为O(N)
的时间复杂度,如下:
实现如下:
public void heapSort(int[] arr) {
if (null == arr || arr.length < 2) {
return;
}
/*for (int i = 0; i < arr.length; i++) {
heapInsert(arr, i);
}*/
for (int i = arr.length - 1; i >= 0; i--) {
heapify(arr, i, arr.length);
}
int heapSize = arr.length;
swap(arr, 0, --heapSize);
while (heapSize > 0) {
heapify(arr, 0, heapSize);
swap(arr, 0, --heapSize);
}
}
现在进行证明,时间复杂度为O(N)
:
堆排序的优势:
堆排序实现了O(N*LogN)
的时间复杂度,但是只使用了有限的几个变量,达到了空间复杂度O(1)
。
综上,堆排序:
1,先让整个数组都变成大根堆结构,建立堆的过程:
1)从上到下的方法heapinsert,时间复杂度为O(N*logN);
2)从下到上的方法heapify,时间复杂度为O(N)。
直观说明如下:
2,把堆的最大值和堆末尾的值交换,然后减少堆的大小之后,再去调整堆。一直周而复始,时间复杂度为O(N*logN)
3,堆的大小减小成0之后,排序完成
现在实现使用系统的堆:
package heap04;
import java.util.PriorityQueue;
/**
* @author Corley
* @date 2021/10/13 14:56
* @description LeetCodeAlgorithmZuo-heap04
*/
public class HeapUse {
public static void main(String[] args) {
PriorityQueue<Integer> heap = new PriorityQueue<>();
heap.add(5);
heap.add(7);
heap.add(3);
heap.add(0);
heap.add(5);
heap.add(2);
while (!heap.isEmpty()) {
System.out.println(heap.poll());
}
}
}
优先队列的底层就是堆,默认是小根堆。
与堆有关的题目:
已知一个几乎有序的数组。几乎有序是指,如果把数组排好顺序的话,每个元素移动的距离一定不超过k,并且k相对于数组长度来说是比较小的。
请选择一个合适的排序策略,对这个数组进行排序。
分析:
方式是准备一个k+1大小的堆,放入
实现如下:
package heap04;
import java.util.PriorityQueue;
/**
* @author Corley
* @date 2021/10/13 15:12
* @description LeetCodeAlgorithmZuo-heap04
*/
public class SortArrayDistanceLessK {
public static void SortedArrayDistanceLessK(int[] arr, int k) {
if (0 == k) {
return;
}
// 小根堆
PriorityQueue<Integer> heap = new PriorityQueue<>();
int index = 0;
for (; index <= k; index++) {
heap.add(arr[index]);
}
int i = 0;
for (; index < arr.length; i++, index++) {
arr[i] = heap.poll();
heap.add(arr[index]);
}
while (!heap.isEmpty()) {
arr[i++] = heap.poll();
}
}
}
复杂度为:O(N*LogK)
。
2.比较器
比较器:
1)比较器的实质就是重载比较运算符;
2)比较器可以很好的应用在特殊标准的排序上;
3)比较器可以很好的应用在根据特殊标准排序的结构上;
4)写代码变得异常容易,还用于范型编程。
先实现应用在特殊标准的排序,如下:
package heap04;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.TreeMap;
/**
* @author Corley
* @date 2021/10/13 17:22
* @description LeetCodeAlgorithmZuo-heap04
*/
public class CustomComparator {
static class Student {
private final String name;
private final int id;
private final int age;
public Student(String name, int id, int age) {
this.name = name;
this.id = id;
this.age = age;
}
@Override
public String toString() {
return "Student{" +
"name='" + name + '\'' +
", id=" + id +
", age=" + age +
'}';
}
}
/*
* id升序比较器
* */
static class IdAscendingComparator implements Comparator<Student> {
@Override
public int compare(Student o1, Student o2) {
return o1.id - o2.id;
}
}
/*
* 年龄降序比较器
* */
static class AgeDescendingComparator implements Comparator<Student> {
@Override
public int compare(Student o1, Student o2) {
return o2.age - o1.age;
}
}
/*
* 复杂比较器,年龄降序、id升序
* */
static class AgeDescendingIdAscedingComparator implements Comparator<Student> {
@Override
public int compare(Student o1, Student o2) {
return o1.age != o2.age ? o2.age - o1.age : o1.id - o2.id;
}
}
public static void main(String[] args) {
Student student1 = new Student("A", 3, 40);
Student student2 = new Student("B", 2, 21);
Student student3 = new Student("C", 4, 12);
Student student4 = new Student("D", 5, 62);
Student student5 = new Student("E", 3, 42);
Student[] studentArray = new Student[] {student1, student2, student3, student4, student5};
Arrays.sort(studentArray, new IdAscendingComparator());
System.out.println(Arrays.toString(studentArray));
System.out.println("-------------------------------------");
ArrayList<Student> studentList = new ArrayList<>();
studentList.add(student1);
studentList.add(student2);
studentList.add(student3);
studentList.add(student4);
studentList.add(student5);
studentList.sort(new AgeDescendingComparator());
System.out.println(studentList);
System.out.println("-------------------------------------");
TreeMap<Student, String> studentMap = new TreeMap<>(new AgeDescendingIdAscedingComparator());
studentMap.put(student1, "我是学生1,我的名字叫A");
studentMap.put(student2, "我是学生2,我的名字叫B");
studentMap.put(student3, "我是学生3,我的名字叫C");
studentMap.put(student4, "我是学生4,我的名字叫D");
studentMap.put(student5, "我是学生5,我的名字叫E");
System.out.println(studentMap.keySet());
}
}
输出:
[Student{name='B', id=2, age=21}, Student{name='A', id=3, age=40}, Student{name='E', id=3, age=42}, Student{name='C', id=4, age=12}, Student{name='D', id=5, age=62}]
-------------------------------------
[Student{name='D', id=5, age=62}, Student{name='E', id=3, age=42}, Student{name='A', id=3, age=40}, Student{name='B', id=2, age=21}, Student{name='C', id=4, age=12}]
-------------------------------------
[Student{name='D', id=5, age=62}, Student{name='E', id=3, age=42}, Student{name='A', id=3, age=40}, Student{name='B', id=2, age=21}, Student{name='C', id=4, age=12}]
再实现应用在根据特殊标准排序的结构,如下:
package heap04;
import java.util.Comparator;
import java.util.PriorityQueue;
/**
* @author Corley
* @date 2021/10/13 14:56
* @description LeetCodeAlgorithmZuo-heap04
*/
public class HeapUse {
/*
* 整型比较器
* */
static class IntegerComparator implements Comparator<Integer> {
@Override
public int compare(Integer o1, Integer o2) {
return o2 - o1;
}
}
public static void main(String[] args) {
// PriorityQueue<Integer> heap = new PriorityQueue<>();
// 大根堆
PriorityQueue<Integer> heap = new PriorityQueue<>(new IntegerComparator());
heap.add(5);
heap.add(7);
heap.add(3);
heap.add(0);
heap.add(5);
heap.add(2);
while (!heap.isEmpty()) {
System.out.println(heap.poll());
}
}
}
可以看到,用PriorityQueue实现了大根堆。
语言提供的堆结构vs手写的堆结构:
取决于有没有动态改信息的需求;
语言提供的堆结构,如果你动态改数据,不保证依然有序;
手写堆结构,因为增加了对象的位置表,所以能够满足动态改信息的需求。
思路如下:
实现如下:
package heap04;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap;
import java.util.PriorityQueue;
/**
* @author Corley
* @date 2021/10/13 19:24
* @description LeetCodeAlgorithmZuo-heap04
*/
public class CustomHeap {
static class SelfHeap<T> {
private final ArrayList<T> heap; // 小根堆
private final HashMap<T, Integer> indexMap;
private int heapSize;
private final Comparator<? super T> comparator;
public SelfHeap(Comparator<? super T> comparator) {
heap = new ArrayList<>();
indexMap = new HashMap<>();
heapSize = 0;
this.comparator = comparator;
}
public boolean isEmpty() {
return heapSize == 0;
}
public int size() {
return heapSize;
}
public boolean contains(T key) {
return indexMap.containsKey(key);
}
public void push(T value) {
heap.add(value);
indexMap.put(value, heapSize);
heapInsert(heapSize++);
}
public T pop() {
T ans = heap.get(0);
int end = heapSize - 1;
swap(0, end);
heap.remove(end);
indexMap.remove(ans);
heapify(0, --heapSize);
return ans;
}
public void resign(T value) {
int valueIndex = indexMap.get(value);
heapInsert(valueIndex);
heapify(valueIndex, heapSize);
}
private void heapInsert(int index) {
while (comparator.compare(heap.get(index), heap.get((index - 1) / 2)) < 0) {
swap(index, (index - 1) / 2);
index = (index - 1) / 2;
}
}
private void heapify(int index, int heapSize) {
int left = index * 2 + 1;
while (left < heapSize) {
int smallest = left + 1 < heapSize && (comparator.compare(heap.get(left + 1), heap.get(left)) < 0) ?
left + 1 : left;
smallest = comparator.compare(heap.get(smallest), heap.get(index)) < 0 ? smallest : index;
if (smallest == index) {
break;
}
swap(smallest, index);
index = smallest;
left = index * 2 + 1;
}
}
private void swap(int i, int j) {
T t1 = heap.get(i);
T t2 = heap.get(j);
heap.set(i, t2);
heap.set(j, t1);
indexMap.put(t1, j);
indexMap.put(t2, i);
}
}
public static class Student {
public int classNo;
public int age;
public int id;
public Student(int c, int a, int i) {
classNo = c;
age = a;
id = i;
}
}
public static class StudentComparator implements Comparator<Student> {
@Override
public int compare(Student o1, Student o2) {
return o1.age - o2.age;
}
}
public static void main(String[] args) {
Student s1 = new Student(2, 50, 11111);
Student s2 = new Student(1, 60, 22222);
Student s3 = new Student(6, 10, 33333);
Student s4 = new Student(3, 20, 44444);
Student s5 = new Student(7, 72, 55555);
Student s6 = new Student(1, 14, 66666);
SelfHeap<Student> myHeap = new SelfHeap<>(new StudentComparator());
myHeap.push(s1);
myHeap.push(s2);
myHeap.push(s3);
myHeap.push(s4);
myHeap.push(s5);
myHeap.push(s6);
while (!myHeap.isEmpty()) {
Student cur = myHeap.pop();
System.out.println(cur.classNo + "," + cur.age + "," + cur.id);
}
}
}
之所以这种方式可以实现修改其中的元素后能重新排序,一个重要的原因是因为存入堆中的其实只是元素的引用,在修改元素的属性之后,引用对应的堆区对象的属性就会发生改变,所以相当于堆中的某个元素的属性发生变化,此时如果以这个属性为排序依据,则需要调用heapInsert和heapify方法对堆进行调整,以满足大根堆或小根堆的条件。
在实际使用中,选择堆时,如果只是单纯向堆中加入或取元素,则可以直接使用系统提供的堆;
如果需要对堆中的元素进行修改,则需要自己实现堆和对应的比较器。
总结
堆是在完全二叉树的基础上实现的,分为大根堆和小根堆,堆排序实现了O(N*LogN)
的时间复杂度。比较器可以很好的应用在特殊标准的排序和根据特殊标准排序的结构上。