1. 程式人生 > >優先佇列,java版本

優先佇列,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迴圈只是在維護一個無序陣列變成一個有序的堆.也就是我們佇列那樣的約束

然後 後面遍歷每一個去排序

這裡可以自己試試