優先佇列或堆及堆排序介紹
1 堆的基本概念
堆也叫優先佇列,堆是一種特殊的完全二叉樹資料結構,堆分為兩種,最大堆,最小堆。
最大堆:根節點大於左右兩個子節點的完全二叉樹
最小堆:根節點小於左右兩個子節點的完全二叉樹
堆可以用陣列來儲存,a[i]處存根節點,a[2*i] a[2*i + 1]分別存左子樹的根節點,右子樹的根節點。i從1開始
所以對於一個堆,結點i,其父結點為a[i/2],左子節點a[2*i],右子節點a[2*i + 1]
2 最大堆
根節點大於左右兩個子節點的完全二叉樹叫最大堆
1 堆的上浮
對於最大堆來說,如果某個結點比其父結點要大,那麼我們應該調整該結點的位置,從而使其滿足堆的結構。調整的思路很簡單,就是將與其父結點交換。這樣一直迴圈下去,直到所有結點滿足堆的結構。這個過程叫做堆的上浮
/**
* 堆的上浮,解決子節點比父結點大的問題
* @param k 節點k上浮
*/
private void swim(int k){
//子節點比父結點大
while (k > 1 && less(k/2,k)){
//交換兩個節點
exch(k/2,k);
k = k/2;
}
}
時間複雜度O(logN)
2 堆的下沉
對於最大堆來說,如果我們發現某個結點小於子節點(同時小於兩個結點,或者小於一個結點)。這個時候我們也需要調整堆結構。調整思路就是將該結點與子節點中較大的結點做交換,遞迴下去。直到滿足堆的結構。
/**
* 堆的下沉 父結點小於子節點,將父節點與較大的子節點交換
* @param k
*/
private void sink(int k){
if (k > count){
return;
}
int i = 2 * k;
if (i > count ){
return;
}
//只有左子節點 i = count;
if ((i + 1 ) > count){
if (less(k,i)){
exch(k,i);
}
return;
}
//兩個孩子都大於父節點
if (less(k,i) || less(k,i + 1)){
if (less(i,i+1)){
exch(k,i+1);
sink(i+1);
}else {
exch(k,i);
sink(i);
}
}
}
時間複雜度O(logN)
3 堆的插入
只要將插入的數放在陣列末尾,然後上浮這個結點即可
/**
* 插入一個元素,並上浮
* @param t
*/
public void insert(T t){
pq[++count] = t;
swimBetter(count);
}
時間複雜度O(logN)
4 堆的刪除
這裡刪除指的是刪除最大的元素,即刪除頂點元素。刪除思路很簡單:獲取陣列第一個元素,然後將第一個元素與最後一個元素交換,並下沉第一個元素即可。
/**
* 刪除最大的元素
* @return
*/
public T delMax(){
T t = (T) pq[1];
//刪除最大的,然後將末尾元素交換,並下沉
exch(1,count);
pq[count] = null;
count--;
sinkBetter(1);
return t;
}
時間複雜度O(logN)
5最大堆實現
接下來我們看堆的完整程式碼:
**
* @author Created by qiyei2015 on 2018/3/25.
* @version: 1.0
* @email: 1273482124@qq.com
* @description:
*/
public class BaseHeap<T extends Comparable<T>> {
/**
* 長度需N + 1
*/
protected Comparable[] pq;
/**
* 容量 pq[1....N]
*/
protected int N;
/**
* 堆中元素個數
*/
protected int count;
/**
* 構造方法
*/
public BaseHeap() {
pq = new Comparable[0];
}
/**
* 建立一個初始容量為max的堆
* @param max
*/
public BaseHeap(int max) {
pq = new Comparable[max + 1];
count = 0;
N = max;
}
/**
* 陣列建立
* @param array
*/
public BaseHeap(Comparable[] array) {
this.pq = new Comparable[array.length + 1];
System.arraycopy(array,0,pq,1,array.length);
N = array.length;
count = N;
}
/**
* 時候為null
* @return
*/
public boolean isEmpty(){
return count == 0;
}
/**
* 返回堆中元素個數
* @return
*/
public int size(){
return count;
}
/**
* 比較 i < j
* @param i
* @param j
* @return
*/
protected boolean less(int i ,int j){
return pq[i].compareTo(pq[j]) < 0;
}
/**
* 交換兩個數
* @param i
* @param j
*/
protected void exch(int i,int j){
Comparable temp = pq[i];
pq[i] = pq[j];
pq[j] = temp;
}
}
/**
* @author Created by qiyei2015 on 2018/3/25.
* @version: 1.0
* @email: 1273482124@qq.com
* @description: 最大堆
*/
public class MaxPQ<T extends Comparable<T>> extends BaseHeap{
/**
* 無參構造方法
*/
public MaxPQ() {
super();
}
/**
* 建立一個初始容量為max的堆
* @param max
*/
public MaxPQ(int max) {
super(max);
}
/**
* 用陣列建立堆,時間複雜度 O(N)
* @param a
*/
public MaxPQ(Comparable[] a){
super(a);
//最後一個父結點是 count / 2 [count/2...1]這個結點區間的結點都下沉,就是堆了
for (int i = count/2; i >= 1 ; i--){
sinkBetter(i);
}
}
/**
* 插入一個元素,並上浮
* @param t
*/
public void insert(T t){
pq[++count] = t;
swimBetter(count);
}
/**
* 刪除最大的元素
* @return
*/
public T delMax(){
T t = (T) pq[1];
//刪除最大的,然後將末尾元素交換,並下沉
exch(1,count);
pq[count] = null;
count--;
sinkBetter(1);
return t;
}
/**
* 堆的上浮,解決子節點比父結點大的問題
* @param k 節點k上浮
*/
private void swim(int k){
//子節點比父結點大
while (k > 1 && less(k/2,k)){
//交換兩個節點
exch(k/2,k);
k = k/2;
}
}
/**
* 堆的上浮,解決子節點比父結點大的問題,少交換,優化堆的上浮過程
* @param k 節點k上浮
*/
private void swimBetter(int k){
Comparable temp = pq[k];
//子節點比父結點大
while (k > 1 && less(k/2,k)){
//父結點移到子節點 子節點暫存 不用每次都去新建一個臨時變數來交換
pq[k] = pq[k/2];
pq[k/2] = temp;
k = k/2;
}
}
/**
* 堆的下沉 父結點小於子節點,將父節點與較大的子節點交換
* @param k
*/
private void sink(int k){
if (k > count){
return;
}
int i = 2 * k;
if (i > count ){
return;
}
//只有左子節點 i = count;
if ((i + 1 ) > count){
if (less(k,i)){
exch(k,i);
}
return;
}
//兩個孩子都大於父節點
if (less(k,i) || less(k,i + 1)){
if (less(i,i+1)){
exch(k,i+1);
sink(i+1);
}else {
exch(k,i);
sink(i);
}
}
// //判斷有左孩子,有孩子就行
// while (2 * k <= N){
// int j = 2 * k; //此輪迴圈中 k 與j交換
// if ((j +1) <= N && less(j,j+1)){
// j++; //更新為右孩子
// }
// //父結點大於子節點
// if (!less(k,j)){
// break;
// }
// exch(k,j);
// k = j; //更新k的位置
// }
}
/**
* 堆的下沉 父結點小於子節點,將父節點與較大的子節點交換
* @param k
*/
private void sinkBetter(int k) {
Comparable temp = pq[k];
//判斷有左孩子,有孩子就行
while (2 * k <= count) {
int j = 2 * k; //此輪迴圈中 k 與j交換
if ((j + 1) <= count && less(j, j + 1)) {
j++; //更新為右孩子
}
//父結點大於子節點
if (!less(k, j)) {
break;
}
//將子節點移到父結點,父結點移到子節點 不用每次都去新建一個臨時變數來交換
pq[k] = pq[j];
pq[j] = temp;
k = j; //更新k的位置
}
}
}
3 最小堆
與最大堆類似,我們只需要在堆的上浮和下沉改變一下條件,即可建立最小堆
1 最小堆上浮
結點比父節點小,該結點與父節點交換
/**
* 堆的上浮,如果子節點比父節點小,就交換
* @param k 節點k上浮
*/
private void swim(int k){
//子節點比父結點大
while (k > 1 && less(k,k/2)){
//交換兩個節點
exch(k/2,k);
k = k/2;
}
}
時間複雜度O(logN)
2 最小堆下沉
父結點大於子節點,該結點與子結點中較小的節點交換
/**
* 堆的下沉 父結點大於子節點,將父節點與較小的子節點交換,並下沉子節點
* @param k
*/
private void sink(int k){
if (k > count){
return;
}
int i = 2 * k;
if (i > count ){
return;
}
//只有左子節點 i = count;
if ((i+1) > count){
if (less(i,k)){
exch(k,i);
}
return;
}
//父節點與較小的子節點交換
if (less(i,k) || less(i+1,k)){
//將父節點與較小的節點交換
if (less(i,i+1)){
exch(k,i);
sink(i);
}else {
exch(k,i+1);
sink(i+1);
}
}
}
時間複雜度O(logN)
3 最小堆實現
完整最小堆程式碼:
package com.qiyei.heap;
/**
* @author Created by qiyei2015 on 2018/4/16.
* @version: 1.0
* @email: 1273482124@qq.com
* @description: 最小堆
*/
public class MinPQ<T extends Comparable<T>> extends BaseHeap {
/**
* 無參構造方法
*/
public MinPQ() {
super();
}
/**
* 建立一個初始容量為max的堆
* @param n
*/
public MinPQ(int n) {
super(n);
}
/**
* 用陣列建立堆,時間複雜度 O(N)
* @param a
*/
public MinPQ(Comparable[] a){
super(a);
for (int i = count/2; i >= 1 ; i--){
sinkBetter(i);
}
}
/**
* 插入一個元素
* @param t
*/
public void insert(T t){
pq[++count] = t;
swimBetter(count);
}
/**
* 刪除最小的元素
* @return
*/
public T delMin(){
T t = (T) pq[1];
exch(1,count);
pq[count] = null;
count--;
sinkBetter(1);
return t;
}
/**
* 堆的上浮,如果子節點比父節點小,就交換
* @param k 節點k上浮
*/
private void swim(int k){
//子節點比父結點大
while (k > 1 && less(k,k/2)){
//交換兩個節點
exch(k/2,k);
k = k/2;
}
}
/**
* 堆的上浮,如果子節點比父節點小,就交換。優化堆的上浮過程
* @param k 節點k上浮
*/
private void swimBetter(int k){
Comparable temp = pq[k];
//子節點比父結點大
while (k > 1 && less(k,k/2)){
//父結點移到子節點 子節點暫存 不用每次都去新建一個臨時變數來交換
pq[k] = pq[k/2];
pq[k/2] = temp;
k = k/2;
}
}
/**
* 堆的下沉 父結點大於子節點,將父節點與較小的子節點交換,並下沉子節點
* @param k
*/
private void sink(int k){
if (k > count){
return;
}
int i = 2 * k;
if (i > count ){
return;
}
//只有左子節點 i = count;
if ((i+1) > count){
if (less(i,k)){
exch(k,i);
}
return;
}
//父節點與較小的子節點交換
if (less(i,k) || less(i+1,k)){
//將父節點與較小的節點交換
if (less(i,i+1)){
exch(k,i);
sink(i);
}else {
exch(k,i+1);
sink(i+1);
}
}
}
/**
* 堆的下沉 父結點大於子節點,將父節點與較小的子節點交換
* @param k
*/
private void sinkBetter(int k) {
Comparable temp = pq[k];
//判斷有左孩子,有孩子就行
while (2 * k <= count) {
int j = 2 * k; //此輪迴圈中 k 與j交換
if ((j + 1) <= count && less(j+1, j)) {
j++; //更新為右孩子
}
//父結點大於子節點
if (!less(j, k)) {
break;
}
//將子節點移到父結點,父結點移到子節點 不用每次都去新建一個臨時變數來交換
pq[k] = pq[j];
pq[j] = temp;
k = j; //更新k的位置
}
}
}
4 堆排序
有了最大最小堆,堆排序變得非常簡單。首先根據陣列構造堆,然後依次取堆頂元素即可,對於最大堆,需要把倒序賦值陣列。
堆排序的時間複雜度是O(nlogn)
/**
* 堆排序
* @param array
*/
@Override
public void sort(Comparable[] array) {
//時間複雜度O(n)
MaxPQ maxPQ = new MaxPQ(array);
for (int i = array.length - 1 ; i >= 0 ; i--){
//時間複雜度O(logn)
array[i] = maxPQ.delMax();
}
}
最後:
原始碼github https://github.com/qiyei2015/Algorithms heap部分