基于堆的优先队列和堆排序
代码参考《算法》第四版
堆有序就是每一个结点元素值大于子结点元素值。
向一个有序堆中,在堆尾部采用上浮法插入排序,可以使堆再次有序。
向一个有序堆中,将堆首部结点元素替换成要插入的元素,再对首结点采用下沉法排序,最后堆也是有序的。
package algorithms.sort;
/*基于堆的最大优先队列
* 算法和数据结构中,要细致到每一个条件和判断,因为每一个条件和判断对应另一番处理逻辑,另一番场景。
* 每一个方法背后可能有复杂的操作逻辑,每一个方法都要测试。
* 使堆有序的方法:从左向右遍历数组,上浮法逐个元素插入,可使堆有序
* 对无序堆,对一半有子节点的根节点递归使用下沉排序,可使得堆有序
* */
public class MaxPQ2 {
private int[] pq; //pq数组第一位置元素不使用,MaxPQBasedOnDui的存储结构,不会暴露给外部
private int N; //N是有序队列的长度,也是数组pq最后一个元素的索引; 可以限制从pq中取值,因为pq中剩余位置元素值为0;
public MaxPQ2(int maxSize) {
this.pq = new int[maxSize + 1]; //maxSize为优先队列底层最大可用长度
this.pq[0] = 0; //pq数组第一位置元素不使用
this.N = 0;
}
//优先队列长度
public int size() {
return this.N;
}
//优先队列是否为空
public boolean isEmpty() {
return N == 0;
}
//优先队列是否存满,即是否超出底层数组pq的最大长度
public boolean isFull() {
return N >= pq.length - 1; //pq.length - 1 = maxSize
}
//顺序插入元素到pq中,不是用最大优先队列中的堆结构存储数据
private void insertSequence(int v) {
this.pq[++N] = v;
}
//insertSequence()测试, isFull()没有问题后,添加isFull()判断
private void insertSequenceIfNotFull(int v) {
if(!isFull())
this.pq[++N] = v;
}
//测试,实际不会暴露给外部
private int[] pq() {
return this.pq;
}
//插入操作, 上浮法构造有序堆,保证为最大优先队列;数组从左向右遍历,类似于插入排序(向优先队列插入元素)
public void insert(int val) {
if(!isFull()) {
pq[++N] = val; //数组中添加元素
swim(N); //并上浮到合适位置
}
}
//插入一组元素,构造有序堆。
public void insertAll(int[] a) {
for(int v : a) {
if(!isFull()) { //即insert()方法
pq[++N] = v;
swim(N);
}
else break; //跳出之后的for循环,因为之后的循环已经没有意义
}
}
//下沉法构造有序堆; 递归地对一半带有子节点的每一个节点进行下沉排序,从而使整个堆有序;数组从右向左遍历
//针对不是有序堆的数组
public void sink() { //从根节点出发,不含有子节点的节点无法使用下沉排序,只能等待父节点下沉操作时被访问到
for(int k = this.N/2; k >= 1; k--) //k=1时仍执行一次下沉操作
sink(k);
}
//获取最大值元素
//时间复杂度为1,因为插入的时候付出了额外的代价,并且堆结构优于无序数据的插入过程
public int peek() {
return pq[1];
}
//获取优先队列中某一位置元素
public int get(int i) {
if(i > N - 1) //超过优先队列长度
throw new NullPointerException();
return pq[i + 1];
}
//删除最大值,delMax()方法破坏了堆的有序性
public int delMax() {
int max = pq[1];
exch(1, N--); //堆首元素和最后一个元素进行交换,缩减优先队列索引长度,不被索引的元素不会被再次索引到,因为N变小,下次插入时会被覆盖掉
sink(1); //此时堆首元素再进行下沉操作,下沉到合适位置
return max;
}
//按照堆结构顺序打印优先队列元素
public void print() {
for(int i = 1; i <= N; i++)
System.out.print(pq[i]+" ");
}
//堆排序。使用下沉法对有序堆排序,使数组有序、
public void sort() {
int M = N;
//下沉法将堆无序构造为有序堆(针对最大优先队列这步已经操作过了)
for(int k = N/2; k >= 1; k--)
sink(k);
//在有序堆的基础上采用下沉法使得数组有序
while(M > 1) {
exch(1, M--);//1, M--都是当前限定长度数组中的首尾元素
sink(1, M); //除了堆首元素外其他节点都是堆有序的,所以将堆首元素下沉到合适位置就行。(仅考虑3个节点情况会非常清晰这一点)
}
}
//此方法仅适用于有序堆,使数组有序
public void sortOnlyForSequenceHeap() {
int M = this.N;
while(M >= 1) {
exch(1, M--);
sink(1, M);
}
}
//下沉方法,把pq中指定位置元素下沉到合适位置,此方法中较小值下沉,较大值上浮。
private void sink(int k) { //操作的是pq数组, k为当前元素索引
if(k < 1) return;
while(2*k <= N) {//=时还可进行一次比较;2*k<N时,有左右两个子节点;2*k=N时,只有左子节点;2*k>N时,没有子节点;
int j = 2*k; //j是偶数,是左子节点;2*k<N时,有左右两个子节点
if(j < N && less(j, j + 1)) j++; //有左右两个子节点时,进行比较
if(less(j, k)) break;
exch(k, j);
k = j;
}
}
//在指定(限定)的pq数组范围1~M内排序指定元素,其中 M<=N
private void sink(int k, int M) { //操作的是pq数组, k为当前元素索引
if(k < 1) return;
while(2*k <= M) {//=时还可进行一次比较;2*k<N时,有左右两个子节点;2*k=N时,只有左子节点;2*k>N时,没有子节点;
int j = 2*k; //j是偶数,是左子节点;2*k<N时,有左右两个子节点
if(j < M && less(j, j + 1)) j++; //j < M不要写错,有左右两个子节点时,进行比较
if(less(j, k)) break;
exch(k, j);
k = j; //继续往下比较
}
}
//上浮方法,将指定位置的元素上浮到合适位置,此方法中较大值上浮,较小值下沉
private void swim(int k) {//操作的是pq数组, k为当前元素索引
while(k > 1 && less(k/2, k)) { //k/2为当前节点k的父节点;
exch(k/2, k);
k = k/2;
}
}
//数组元素比较大小
private boolean less(int i, int j) { //操作的是pq数组
return pq[i] < pq[j];
}
//数组元素交换
private void exch(int i, int j) { //操作的是pq数组
int temp = pq[i];
pq[i] = pq[j];
pq[j] = temp;
}
public static void main(String[] args) {
//测试pq
MaxPQ2 maxpq = new MaxPQ2(5);
for(int i : maxpq.pq())
System.out.print(i + " ");
//测试isEmpty()
System.out.println(maxpq.isEmpty());
//测试isFull()
int k = 5; //5会填满数组,6会超过数组存储长度
for(int i = 0; i < k; i++)
maxpq.insertSequence(i);
//insertSequenceIfNotFull(i); //k = 6 时可用
for(int i : maxpq.pq())
System.out.print(i + " ");
System.out.println(maxpq.isFull());
//测试size()
System.out.println(maxpq.size());
MaxPQ2 maxpq1 = new MaxPQ2(5);
//测试insert()
for(int i = 0; i < 5; i++)
maxpq1.insert(i);
for(int i : maxpq1.pq())
System.out.print(i + " ");
System.out.println();
//测试print()方法
maxpq1.print();
int[] a = {12,9,28,13,4,5,11,1,71};
MaxPQ2 maxpq2 = new MaxPQ2(a.length + 1);
for(int v : a)
maxpq2.insert(v);
System.out.println();
System.out.print("maxpq2 = ");
maxpq2.print();
//测试peek()方法
System.out.println(" ; max = " + maxpq2.peek());
//测试get()方法
System.out.println("maxpq2.get(8) = " + maxpq2.get(8));
//System.out.println(maxpq2.get(9));
//测试insertAll()方法
MaxPQ2 maxpq3 = new MaxPQ2(a.length + 1);
maxpq3.insertAll(a);
System.out.print("maxpq3 = ");
maxpq3.print();
System.out.println();
int len = maxpq3.size();
System.out.print("maxpq3.delMax() = ");
for(int i = 0; i < len; i++)
System.out.print(maxpq3.delMax() + " "); //并不是按大小顺序输出的
int[] b = {12,9,28,13,4,5,11,1,71};
MaxPQ2 maxpq4 = new MaxPQ2(b.length + 1);
maxpq4.insertAll(b);
System.out.println();
maxpq4.sort();
maxpq4.print();
}
}
输出:
0 0 0 0 0 0 true
0 0 1 2 3 4 true
5
0 4 3 1 0 2
4 3 1 0 2
maxpq2 = 71 28 12 13 4 5 11 1 9 ; max = 71
maxpq2.get(8) = 9
maxpq3 = 71 28 12 13 4 5 11 1 9
maxpq3.delMax() = 71 28 13 12 11 9 5 4 1
1 4 5 9 11 12 13 28 71