優先佇列,java版本
優先佇列(自己動手寫API系列二)
前言:假設你有n個數據 然後輸入一個m 刪除其中前m大的值
有人就想到了 遍歷嘛 找到一個最大的 一刪.
沒錯 這樣可以 但是如果這個n和m都很大呢? 是不是要跑的很慢
有人又想了 那就排個序嘛 這樣也可以 其實也不是很好
所以 接下來就有了 這個資料結構--優先佇列
這裡會給大家先介紹一下優先佇列 然後給大家講一下堆排序
先來看一下優先佇列. 其實優先佇列的原型有點像二叉樹
但是其實我們在底層是用陣列實現的 不會二叉樹的也別怕
這是一個子樹 其中結點的數字代表這個結點的編號
多畫兩個
讓我們接下來看一個細思極恐的事情 先來看 4號結點和5號結點
4號結點除2是幾? 4/2 = 2 對嗎?
5號結點除2是幾? 5/2 = 2.5 但是我們在裡面用int型儲存 所以 你得到的結果是整除
也就是 5/2 = 2
而 2號結點是什麼呢? 是他們的父結點?是不是有點恐怖
巧合? 不存在的 再讓我們看 2 和 3號結點
2/2 = 1 3/2 = 1
怎麼樣?
至於 再多結點
你可以驗證一下
好了 當前結點除2就回到了父結點
那怎麼從父結點去到子結點呢?
其實就是 乘2 和乘2+1
有人就想了 為什麼回到父結點只是/2 去子結點要乘 2 或者 乘2+1呢?
因為當前結點的父結點只有1個 而子結點卻有兩個啊
我們現在有一個約定
1.父結點比左右結點都大.
2.當一個結點有子結點 有右兒子 必須有左兒子
也就是不容許這種情況
不容許啊 也就是我們必須依次從左向右填充元素
按照我們剛才的約定 其實你應該已經發現了 1號結點就是最大的.
你要是想維持最小的也可以
好了 你先看到這裡 讓我們看一下 關於自己動手寫API系列二 優先佇列的方法
public class MaxPQ<Key extends Comparable<Key>> //類的宣告 其實你接受過來的物件比如實現了Comparable介面
MaxPQ() //無參構造方法
MaxPQ(int size) // 帶大小的構造方法
public boolean isEmpty()
//判斷佇列是否為空 是返回true
public int size()//返回佇列大小
public void insert(Key Date)//插入一個數
public Key delMax() //刪除最大值
這是所有的共有方法 因為 佇列其實在內部處理了很多 主要是私有方法 讓我們先看一下 很多東西都要要講
private void resize(int max)
//調整陣列大小 提高陣列利用率 待會講
private void swap(int i, int j)
//交換下表為 i 和 j的兩個值
private boolean cmp(int i, int j)
//比較兩個值的大小
private void swim(int k)//上浮操作
private void sink(int k)//下沉操作
其中上述方法 核心是上浮和下沉操作
讓我們慢慢來講.
先來看一下講的順序(我覺得這樣會比較好理解)
1.兩個構造方法
2.isEmpty() 和 size()方法
3.resize()方法 swap()方法 cmp()方法
4.swim() 和 sink() 方法
5.insert() 和 delMax()方法
先給大家看一下 成員變數把
private Key[] elements; // 物件陣列
private int N = 0; // 你記錄的個數
private int size; // resize的時候會用到 你陣列的開闢的空間
1.兩個構造方法
MaxPQ() {
this.size = 10;
elements = (Key[]) new Comparable[size];
}
MaxPQ(int size) {
this.size = size;
elements = (Key[]) new Comparable[this.size];
}
就是建立陣列 預設的開始大小為10 第二個 有設定大小.
這個應該很好理解 就是給陣列開闢空間
2.isEmpty() 和 size()方法
public boolean isEmpty() { return N == 0; }
public int size() { return N; }
然後這也是兩個簡單的方法
第一個判斷佇列是不是空 是空的返回true 當N==0代表沒有任何元素 他就是0
第二個 返回佇列的大小 其實就是N的個數 因為我們用的就是N在記錄
3.resize()方法 swap()方法 cmp()方法
a>resize()方法 其實我之前系列一 中寫的比較詳細 下面貼出來 在這裡再講一下
https://blog.csdn.net/qq_42011541/article/details/80671518
private void resize(int max) {
this.size = max;
Key[] temp = (Key[]) new Comparable[this.size];
for (int i = 1; i <= N; i++) {
temp[i] = elements[i];
}
elements = temp;
}
其實就是對陣列擴容 用temp 建造一個 傳過來要擴容為max大小的陣列
再把之前的複製進來 讓elements 的引用 指向temp 這個時候之前的陣列就會被當做垃圾
JVM會去回收你的垃圾
通俗點講解 本來你現在大小隻有10 現在你又放進來一個數據 但是你發現你的陣列填滿了
所以你需要呼叫這個resize()方法 引數傳20就可以 這樣就實現了擴容
private void swap(int i, int j) {
Key temp = elements[i];
elements[i] = elements[j];
elements[j] = temp;
}
swap()方法這個簡單的可追溯到有兩個數a,b用一個c去完成交換a,b
不說了把
private boolean cmp(int i, int j) {
return elements[i].compareTo(elements[j]) < 0;
}
cmp這個方法也很簡單 comparaTo()這個方法你覺得實在繞 就自己寫.
其實就是v.comparaTo(m) v 比 m 小 返回 小於0的值
相等 返回0 v 比 m大 返回大於0的值
其實就是做判斷的 一般和swap() 連起來用
4.swim() 和 sink() 方法
這兩個方法 我一開始其實有提到
我們想一想 k/2的那個操作 回到父結點
而swim() 這個方法其實就是上浮的意思.
想一想
如果你有一個新數 肯定被排到了末尾了 而這個數非常大呢?
他應該在的位置是不是不應該在末尾啊
所以 讓他上浮到他應有的位置就好了.
其實我每進來一個數 都讓他上浮 其實 本來就是遵循我們的約定的
隨便畫幾個給你看一下過程
畫圖理解一下
注:這下結點裡面的數字我就填寫數字的大小了!!
然後現在有一個新的結點 數值為17
明顯他不應該在這裡 他其實 應該最大的 在最上層對吧
所以 第一步 先問一下 我17比父節點大還是小
然後發現我比我父結點都大 那我做父結點把
然後 現在我還得再問 我17比我的父結點10哪個大.
嗯. 我比你大. 那還是我做父結點吧
然後繼續問 因為我沒有碰到一個比我大我的 我也還沒有碰到頭
假設 如果我不是17 是 9的話 是不是和8換一次就停止了?
但是 現在我大 我還得換 發現又比13大了
最後就是這樣的了 你發現換完 其實 結點還都是符合我們的約定的
父結點都比子結點大
其實 每一個進來都上浮 不會破壞這個佇列的結構
所以 swim() 程式碼 你可以理解了嘛? 這裡 你可以大概先寫一下 不要著急往後看
原始碼
private void swim(int k) {
while (k > 1 && cmp(k/2, k)) {
swap(k/2,k);
k = k/2;
}
}
看看這個k是不是已經是最大的父結點了 所以 k>1 這個很重要
而且 k>1 &&cmp(k/2, k)的順序不能顛倒
你可以試試為什麼
可能會報一個空指標異常 原理是什麼 動一下小腦子就明白了
cmp()判斷一下k/2(父結點) 是不是比 k(子結點)小了
成立我就交換 然後 k = k/2 繼續搜尋
sink() 可能會比swim()稍微難一下 不過也並沒有那麼難
我們怎麼刪除最大的 你看看我這樣做合理不?
1.我第一個結點也就是最大的了.我讓第一個結點直接等於null
然後判斷一下 左兒子大還是右兒子 讓大的當父結點
嗯...貌似有點道理. 但是這是錯的!!! 我們這裡是陣列啊.. 並不是連結串列
有人可能說了 你可以全部往前挪一位嘛. 那這樣是不是還要跑一個線性(也就是遍歷一遍)
這樣就破壞了我們 高效的初衷
最好的操作應該是.
第一步 : 用一個臨時變數儲存最大的.
第二步 : 交換最大的和最後一位.
第三步 : 讓最後一位變成null 因為第二步的時候 最大的已經到最後一位了
我們現在這個第三步就相當於刪除了
第四步 : 讓現在的第一位也就是後換上去的 那個 進行 下沉操作 下沉到他應該有的地方
看一下圖解
還是這個樣子 我現在要刪除17 你看怎麼辦?
第一步 先找個臨時變數 儲存 最大的
然後 最後一個和第一個交換位置
外面多出來的那個就臨時的
然後 最後一位和第一位交換位置
然後 讓 最後一位 為空
然後就是讓 8 的那個節點 下沉了
我現在要問的就是 我 8 和我的左兒子(13) 右兒子(12) 有沒有比我大的 有的話 我肯定得換一個最大的
才能完美的保證那個約定
所以 我第一步判斷一下我的左兒子大還是我的右兒子大.
左兒子大 就拿左二子和父結點比較 否則拿右兒子 和 父結點比較
我發現 我左兒子最大 且比父結點大 所以換位置
還得繼續判斷啊 發現左二子 10大 而且也比8大 所以交換
看一下我還有兒子 發現 我兒子都沒我大 返回 這是不是又歸位了? 看一下 8個數我只比較了3次
完美的全是了 log2N的時間複雜度
想一想看看自己能不能寫出來 我要扔原始碼了
private void sink(int k) {
while ( k*2 <= N ) {//判斷還有沒有左兒子 因為你有兒子肯定有左兒子 不一定有右兒子
int temp = k * 2; //temp 指向左兒子的結點
if (temp < N && cmp(temp,temp+1)) temp++; // 判斷一下是不是隻有左兒子 並且哪個兒子大
if (cmp(temp, k)) break;//判斷一下最大的兒子和父結點大還是小
swap(temp, k);
k = temp;
}
}
還是一樣的
if (temp < N && cmp(temp,temp+1)) temp++;
這一行的判斷順序很重要
然後temp就是你最大的兒子的那個結點編號了
問一下父結點和最大兒子哪個大 兒子大 交換 否則返回
好了 sink() 講完了
其實我的insert() 和 delMax() 也講完了
哈哈
public void insert(Key Date) {
if (N + 1 == size) resize(size << 1);
elements[++N] = Date;
swim(N);
}
public Key delMax() {
Key temp = elements[1];
swap(1, N);
elements[N--] = null;
sink(1);
if(N < size/4) resize(size >> 1);
return temp;
}
看一下 是不是我們之前講的步驟?
其實 你在這裡多理解一下sink和 swim的真諦 堆排序真的就很簡單了
public static void sort(Comparable[] arr) {
int n = arr.length;
for (int k = n/2; k >= 1; k--) {
sink(arr, k, n);
}
while (n > 1) {
swap(arr,1,n--);
sink(a,1,n);
}
}
這是堆排程式碼 我之前sink 你改一下 引數就好 加了個數組物件也就是之前的elements
n代表個數.
其實 第一個for迴圈只是在維護一個無序陣列變成一個有序的堆.也就是我們佇列那樣的約束
然後 後面遍歷每一個去排序
這裡可以自己試試