面試算法:如何利用堆排序實現系統的Timer機制

更詳細的講解和代碼調試演示過程,請點擊鏈接

做過系統編程的人都知道,幾乎任何系統都會提供一種時鐘機制,也就是SetTimer調用,你給系統提供一個回調函數,順帶一個超時時間,一旦時間過去后,系統就會回調你提供的函數。問題是,你如何實現一個系統的Timer機制。

實現Timer機制的辦法是使用堆排序。所謂的'堆'是一種特殊的二叉樹,它跟內存管理上的'堆'沒有任何聯系。數據結構'堆',實際上是把一個數組中的元素按照某種次序組合成一顆二叉樹,二叉樹根節點的特性決定了堆的特性,如果二叉樹根節點的值是數組元素中的最大值,那么這顆二叉樹表示的堆叫大堆,如果二叉樹根節點的值是元素中的最小值,那么二叉樹表示的堆叫小堆。

例如給定一個數組:
heapArray: 16, 14, 10, 8, 7 , 9 , 3, 2, 4 , 1
把他們構建成一個大堆,結果如下:


這里寫圖片描述

節點旁邊的數字代表它在數組中的下標,我們可以看到,給定一個節點下標i,那么它在對中對應的父節點就是i/2, 必然下標為2和3的節點,其父節點是16,而16的下標恰好是1.

給定一個節點下標為i,那么它在堆中的左孩子對應在數組中的下標為2*i,右孩子對應的下標為2*i+1,于是我們有如下三種操作:

int  parent(int i) {
    return i/2;
}

int left(int i ) {
    return 2*i;
}

int right(int i) {
    return 2*i+1;
}

堆雖然是二叉樹,但不是排序二叉樹,所以無需要求做子樹節點一定小于父節點,父節點一定小于右子樹節點。但它必須維持一個特性,那就是如果堆是大堆,那么必須保證父節點大于孩子節點;如果是小堆,那么必須保證父節點小于孩子節點。于是如果二叉樹是大堆,那么根節點肯定是所以元素中的最大那個,如果是小堆,那么根節點肯定是所有元素中值最小的那個。

由于堆是一顆二叉樹,所以堆有高度之分,一個含有n個元素的數組轉換為堆時,堆的高度就是lg(n). 接下來我們看看,如果大堆中,某個節點的值變得比它孩子的值還要小,那么我們應該如何調整才能使得前面所說的大堆性質保持不變,我們假定當前的大堆元素個數用heapSize來表示。假如下標為i的元素它的數值被改變了,我們可以通過下面代碼調整大堆,從而使得堆的性質保持不變:

 private void maxHeapify(int i) {
        //這里+1的原因是,數組下標是從0開始,而算法對數組下標是從1開始
        i++;
        int l = left(i);
        int r = right(i);
        //減1的原因是,數組元素的下標是由0起始的,而算法對數組下標的起始是由1開始
        i--;
        l--;
        r--;
        
        int largest = -1;
        if (l < heapSize && heapArray[l] > heapArray[i]) {
            largest = l;
        } else {
            largest = i;
        }
        
        if (r < heapSize && heapArray[r] > heapArray[largest]) {
            largest = r;
        }
        
        if (largest != i) {
            int temp = heapArray[i];
            heapArray[i] = heapArray[largest];
            heapArray[largest] = temp;
            maxHeapify(largest);
        }
        
    }

它的思路很簡單,如果某個節點值變小了,那么它就找到他左右孩子節點中值最大的那個,然后兩種互換,這個動作一直進行,直到走到堆的底部為止。注意,上面代碼的邏輯假設節點i的左子樹和右子樹都滿足大堆的性質。假設一個滿足條件的二叉樹如下:


這里寫圖片描述

節點4比它的左右孩子要小,但以節點14和7為根的二叉樹是滿足大堆性質的,由此我們可以根據上面代碼邏輯進行調整。先從節點4的左右孩子中找到最大的一個,顯然左孩子是14是最大的,于是把4和14交換:

這里寫圖片描述

節點4下降一層,然后再和他的左右節點中值最大的那個互換,從上圖看,節點4應該和節點8互換,交換后結果如下:


這里寫圖片描述

我們看到,這一次調換后,整個二叉樹滿足大堆的性質。在調整過程中,每調整一次,原來元素就下降一層,由于堆的高度是lg(n),所以上面調整代碼的時間復雜度是O(lg(n))。

接下來,我們可以使用maxHeapify來將一個數組構建成一個大堆,前面我們說如果一個元素在數組中的下標是i,那么它的左孩子下標為2*i,右孩子下標為2*i+1,這樣一來,所有處于數組后半部分的元素只能處于大堆最底部,也就是只能做葉子。這里要注意的是,任何單個節點本身就是一個大堆。我們使用下面的算法來把一個數組構建成大堆:

private int[] buildMaxHeap() {
        for (int i = heapSize / 2; i >= 0; i--) {
            maxHeapify(i);
        }
        
        return heapArray;
    }

代碼里的循環是從數組的中點開始的,前面我們提到過,中部后面的元素都是葉子節點,葉子節點自己就滿足大堆屬性。在前面我們也演示過,maxHeapify這個調用能夠保證每個以每個節點為根的二叉樹滿足大堆屬性,于是循環結束后的數組就變成了一個滿足大堆性質的二叉樹。舉個例子,假設有數組
A:1,5,3,4,2
數組長度是5,上面代碼則從下標為2處,也就是元素3開始循環,3的左孩子是下標為4的元素2,由此3和2兩個節點構成了一個大堆,接著循環往前走到元素5,5的左孩子是3,右孩子是4,此時節點5和它的兩個孩子滿足大堆性質,循環繼續往前走,來到元素1,它的左孩子是節點5,右孩子是3,此時maxHeapify調用將對元素進行調整,它先把節點1和節點5對換,然后再把節點1和節點4對換,最后數組A的情況如下:
A: 5, 4, 3, 1, 2
數組元素的排列是滿足大堆屬性的。

綜合起來,我們得到構建一個大堆的代碼如下:

public class HeapSort {
    private int heapSize = 0;
    private int[] heapArray = null;
    
    public HeapSort(int[] arr) {
        heapSize = arr.length;  
        heapArray = arr;
    }
    
    private int parent(int i) {
        return i/2;
    }
    
    private int left(int i) {
        return i*2;
    }
    
    private int right(int i) {
        return i*2+1;
    }
    
    private void maxHeapify(int i) {
        //這里+1的原因是,數組下標是從0開始,而算法對數組下標是從1開始
        i++;
        int l = left(i);
        int r = right(i);
        //減1的原因是,數組元素的下標是由0起始的,而算法對數組下標的起始是由1開始
        i--;
        l--;
        r--;
        
        int largest = -1;
        if (l < heapSize && heapArray[l] > heapArray[i]) {
            largest = l;
        } else {
            largest = i;
        }
        
        if (r < heapSize && heapArray[r] > heapArray[largest]) {
            largest = r;
        }
        
        if (largest != i) {
            int temp = heapArray[i];
            heapArray[i] = heapArray[largest];
            heapArray[largest] = temp;
            maxHeapify(largest);
        }
        
    }
    
    public int[] buildMaxHeap() {
        for (int i = heapSize / 2; i >= 0; i--) {
            maxHeapify(i);
        }
        
        return heapArray;
    }
}

利用上面代碼,對給定任意數組構建一個大堆:

public class Heap {
   public static void main(String[] s) {
       int A[] = new int[]{1,2 ,3,4,7,8,9,10,14,16};
       HeapSort  hs = new HeapSort(A);
       int[] heap = hs.buildMaxHeap();
       
       for (int i = 0; i < heap.length; i++) {
          System.out.print(heap[i] + " ");   
       }
   }
}

上面代碼運行后,輸出的數組結果為:

16 14 9 10 7 8 3 1 4 2 

上面數組對應的大堆二叉樹為:


這里寫圖片描述

maxHeapify其時間復雜度為lg(n),所以buildMaxHeap的時間復雜度為n*lg(n).

大堆的特點是,所有元素的最大值在根節點,對應的也就是最大值會放置到數組的頭部,我們可以利用這個特性來對數組進行排序,這種排序法叫堆排序。對含有n個元素的數組,我們構建一個大堆,那么最大值肯定在堆的根節點,對應于數組就是最大值會在數組下標為0處,然后我們把第0個元素和最后一個元素互換,這樣最大值就排在了數組的n-1處。接著對數組的前n-1個元素進行相同的步驟,這樣第二大的元素就會排在數組的n-2處,就這樣反復進行,整個數組就能實現從小到大排序了,對應代碼如下:

 public  void heapSort() {
        buildMaxHeap();
        
        for (int i = heapArray.length - 1; i > 0; i--) {
            //值最大的元素在根節點對應到數組就是值最大的元素在數組的起始位置
            int temp = heapArray[i];
            heapArray[i] = heapArray[0];
            heapArray[0] = temp;
            
            heapSize--;
            maxHeapify(0);
        }
    }

上面代碼上運行后,再把元素的內容打印出來:

       hs.heapSort();
       System.out.println("\nArray after heap sort is :");
       for (int i = 0; i < heap.length; i++) {
           System.out.print(A[i] + " ");
       }

打印后結果如下:
1 2 3 4 7 8 9 10 14 16
由此可見,數組中的元素已經被排好序了。heapSort執行時,調用了buildMaxHeap(),其時間復雜度我們前面分析過是n*lg(n),其中的for循環次數是n,每次循環調用了maxHeapify(),其復雜度是lg(n),所以for循環所需要的時間復雜度是n*lg(n),由此一來heapSort的時間復雜度,也就是堆排序的時間復雜度是O(n*lg(n))。

使用堆排序實現優先級隊列
最大優先級隊列是這樣一種數據結構,它是一組元素的集合,同時它支持以下三種操作:
1, insert(x), 把一個元素加入集合。
2, maximun(), 返回整個集合中的最大值
3, extractMax(), 把集合中的最大值取出來并從集合中去除。
4, increaseKey(i ,k), 把集合中的第i個元素的值增加為k。

假設我們有一系列任務,任務的優先級不同,優先級越大任務越重要,這樣的話我們就可以把任務使用最大優先級隊列進行調度。與最大優先級隊列等價的是最小優先級隊列,它的原理跟最大優先級隊列一樣,只不過把返回最大值改為最小值,把元素的值增加k變為減少k,系統的Timer機制就是使用最小優先級隊列實現的。

我們看看上面的幾種操作如何基于最大堆實現,首先Maximun()實現簡單,我們只要把數組構建成大堆后,返回數組的首個元素即可。于是有

public int maximun() {
        return heapArray[0];
    }

Extract_Maximun()需要把最大值拿掉后,剩下的元素任然能滿足相應操作,假設我們數組中元素最大是16,第二大是14,那么調用一次Extract_Maximun()后返回16,然后調用maximun()時要返回14,它的做法是把返回堆的根元素,把數組最后一個元素跟根元素互換,接著把堆的大小由原來的n變為n-1,然后調用maxHeapify對前n-1個元素調整成大堆。代碼如下:

int extractMax() {
        if (heapSize < 1) {
            return -1;
        }
        
        int max = heapArray[0];
        heapArray[0] = heapArray[heapSize - 1];
        heapSize--;
        maxHeapify(0);
        
        return max;
    }

extractMax內部調用了maxHeapify(),由于后者的時間復雜度是O(lgn),所以extractMax()的時間復雜度是O(lg(n)).

完成上面代碼后,通過以下代碼進行調試運行:

System.out.println("\nmaximun is : " + hs.maximun());
       hs.extractMax();
       System.out.println("the maximun after extractMax is : " + hs.maximun());

程序輸出結果為:
maximun is : 16
the maximun after extractMax is : 14

我們把最大值16從堆中抽出后,再次調用maximun()得到結果為第二大的節點14,由此可見我們代碼實現的邏輯是正確的。

我們再看看increaseKey的實現:

public void increaseKey(int i, int k) {
        if (heapArray[i] > k) {
            return;
        }
        
        heapArray[i] = k;
        while (i > 0 && heapArray[parent(i)] < heapArray[i]) {
            int temp = heapArray[parent(i)];
            heapArray[parent(i)] = heapArray[i];
            heapArray[i] = temp;
            i = parent(i);
        }
    }

當某個節點值變大后,為了保證大堆性質不變,如果它的值比父節點大,那么把它和父節點互換,這個流程一直執行到根節點,這樣就可以保持整個大堆的性質不變了,該操作的時間復雜度是O(lg(n))。完成代碼后,我們在入口處添加如下代碼來檢驗效果:

 hs.increaseKey(9, 17);
 System.out.println("\nThe maximun after increaseKey is : " + hs.maximun());

數組第9個元素,也就是最后一個元素的值是2,我們通過increaseKey調用把它增加到17,此時它變成了整個大堆里的最大值,再次調用maximun()得到的結果應該是17,上面代碼運行后結果如下:

The maxmimun after increaseKey is : 17

由此可見,程序的實現是正確的。我們再看最后一個insert操作,給當前大堆插入一個元素并且保持堆的性質不變:

public int[] insert(int val) {
        int[] mem = new int[heapArray.length + 1];
        for (int i = 0; i < heapArray.length; i++) {
            mem[i] = heapArray[i];
        }
        
        heapArray = mem;
        heapArray[heapArray.length - 1] = Integer.MIN_VALUE;
        heapSize++;
        increaseKey(heapSize - 1, val);
        return heapArray;
    }

由于要插入一個元素,插入后堆的元素要比原來多1,因此代碼先分配一個比原來大堆對應的數組多一個元素的內存,然后把原數組內容拷貝過去,接著把最后一個元素的值設置為最小值,最后使用increaseKey調用把最后的元素值增加到要插入的元素值,這樣我們就能保證插入的元素可以添加到大堆的適當位置,適當堆的性質不會遭到破壞,該操作的時間復雜度與increaseKey一樣,都是O(lg(n))。

我們在程序入口處使用如下代碼測試上面實現的正確性:

hs.insert(20);
System.out.println("Maximun after insert is: " + hs.maximun());

我們往堆中插入一個值為20的新元素,插入后這個值將最為堆的最大值從maximun調用中返回,上面代碼運行后結果如下:
Maximun after insert is: 20

由此可見,我們代碼的實現在邏輯上是正確的。

到此,堆的數據結構及其相關算法原理和實現我們就介紹完了,要實現系統的Timer機制,我們只要實現一個小堆,然后在此基礎上實現最小優先級隊列即可。

更詳細的講解和調試演示,請參看視頻

更多技術信息,包括操作系統,編譯器,面試算法,機器學習,人工智能,請關照我的公眾號:


這里寫圖片描述
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,443評論 6 532
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,530評論 3 416
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,407評論 0 375
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,981評論 1 312
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,759評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,204評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,263評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,415評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,955評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,782評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,983評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,528評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,222評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,650評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,892評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,675評論 3 392
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,967評論 2 374

推薦閱讀更多精彩內容

  • 1 序 2016年6月25日夜,帝都,天下著大雨,拖著行李箱和同學在校門口照了最后一張合照,搬離寢室打車去了提前租...
    RichardJieChen閱讀 5,118評論 0 12
  • 堆排序 堆排序基本簡介 1991年的計算機先驅獎獲得者、斯坦福大學計算機科學系教授羅伯特·弗洛伊德(Robert ...
    BlackMammba閱讀 1,852評論 0 10
  • 本文分析冒泡、選擇、插入、希爾、快速、歸并和堆排序,為不影響閱讀體驗,將關于時間、空間復雜度和穩定性的概念放在博文...
    DeppWang閱讀 431評論 0 2
  • 該系列文章主要是記錄下自己暑假這段時間的學習筆記,暑期也在實習,抽空學了很多,每個方面的知識我都會另起一篇博客去記...
    Yanci516閱讀 12,252評論 6 19
  • 該篇文章主要介紹了算法基礎以及幾種常見的排序算法:選擇排序、插入排序、冒泡排序、快速排序、堆排序。 一、算法基礎 ...
    ZhengYaWei閱讀 1,263評論 0 12