1. 程式人生 > >聊聊演算法--堆的構建和調整

聊聊演算法--堆的構建和調整

先提個問題,完全二叉樹/滿二叉樹,區別?前者是指每一層都是緊湊靠左排列,最後一層可能未排滿,後者是一種特殊的完全二叉樹,

每層都是滿的,即節點總數和深度滿足N=(2^n) -1。堆Heap,一堆蘋果,為了賣相好,越好看的越往上放,就是大頂堆;為了蘋果堆

的穩定,質量越小越往上放,就是小頂堆;堆首先是完全二叉樹,但只確保父節點和子節點大小邏輯,不關心左右子節點的大小關係,

通常是一個可以被看做一棵樹的陣列物件,是個很常見的結構,比如BST物件,都與堆有關係,今天就說下這個重要的資料結構和應用。

 

作者原創文章,謝絕一切轉載,違者必究!

本文只發表在"公眾號"和"部落格園",其他均屬複製貼上!如果覺得排版不清晰,請檢視公眾號文章。 

 

準備:

Idea2019.03/Gradle6.0.1/Maven3.6.3/JDK11.0.4

難度: 新手--戰士--老兵--大師

目標:

1.堆的構建和調整演算法

1 優先順序佇列

為理解堆的原理,先看優先順序佇列,它是一種資料結構,插入或者刪除元素的時候,元素會自動排序,(優先順序不是狹義的數值大小,

但為了通俗理解,這裡以字母序為例),通常使用陣列儲存,我們可以按照下圖進行轉換,序號 0 不用:

優先順序佇列的實現(Java版):

public class PriorityQueue<Key extends Character> {
    /** 儲存元素的陣列 */
    private Key[] keys;
    private int N = 0;

    public PriorityQueue(int capacity){
        // 下標0不用,多分配一個單位
        keys = (Key[]) new Character[capacity + 1];
    }

    public Key max(){
        return keys[1];
    }

    public void insert(Key e){
        N ++;
        keys[N] = e;
        swim(N);
    }
    public Key delMax(){
        Key max = keys[1];
        swap(1,N);
        keys[N] = null;
        N --;
        // 讓第一個元素下沉到合適的位置
        sink(1);
        return max;
    }
    /** 上浮第k個元素*/
    private void swim(int k){
        // 比父節點小,即進行交換,直到根
        while (k > 1 && less(parent(k),k)){
            swap(k,parent(k));
            k = parent(k);
        }
    }
    /** 下沉第 k 個元素*/
    private void sink(int k){
        while(k < N){
            int small = left(k);
            if (right(k) < N && less(right(k),left(k))){
                small = right(k);
            }
            if (less(k,small)){
                swap(k,small);
                k = small;
            }
        }
    }
    private void swap(int i,int j){
        Key temp = keys[i];
        keys[i] = keys[j];
        keys[j] = temp;
    }
    /** 元素i和j大小比較*/
    private boolean less(int i,int j){
//   'a' - 'b' = -1 ;
        return keys[i].compareTo(keys[j]) > 0;
    }
    /** 元素i的父節點*/
    private int parent(int i){
        return i/2;
    }
    /** 元素i的左子節點*/
    private int left(int i){
        return i * 2;
    }
    /** 元素i的右子節點*/
    private int right(int i){
        return i * 2 + 1;
    }
}
 

以上程式碼解析:

1 swim 上浮,對於元素k,是否需要上浮,僅需與其父節點比較,大於父節點則交換,迭代直到根節點;

2 sink 下沉,對於元素k,是否需要下沉,需先比較其左右子節點,找出左右子節點中較小者,較小者若比父節點大,則交換,迭代直到末尾元素;

3 insert 插入,先將元素放到陣列末尾位置,再對其進行上浮操作,直到合適位置;

4 delMax 刪除最大值,大根堆,故第一個元素最大,先將首末元素交換,再刪除末尾元素,再對首元素下沉操作,直到合適位置;

總結:以上只是Java簡化版,java.util.PriorityQueue 是JDK原版,客官可自行研究。但設計還是非常有技巧的,值得思考一番,假設 insert 插入

到首位,會導致陣列大量元素移動。delMax 若直接刪除首位最大值,則需要進一步判斷左右子節點大小,並進行先子節點上浮再首元素下沉操作。

        有了這個堆結構,就可以進行堆排序了,將待排數全部加入此堆結構,然後依次取出,即成有序序列了!

2 堆排序

如要求不使用上述堆資料結構。思路(升序為例):將陣列構建為一個大頂堆,首元素即為陣列最大值,首尾元素交換;排除末尾元素後調整大頂堆,

則新的首元素即為次最大值,交換首尾並再排除末尾元素;如此迴圈,最後的陣列即為升序排列。

public class HeapSort02 {
    public static void main(String []args){
        int []arr = {2,1,8,6,4,7,3,0,9,5};
        sort(arr);
        System.out.println(Arrays.toString(arr));
    }

    public static void sort(int []arr){
        int len = arr.length;
        // 建立一個大頂堆
        for(int i = (int) Math.ceil(len/2 - 1); i >= 0; i--){
            //從第一個非葉子結點從下至上,從右至左調整結構
            adjustHeap(arr,i,len);
        }
        // 交換首尾元素,並重新調整大頂堆
        for(int j = len-1;j > 0;j--){
            swap(arr,0,j);
            adjustHeap(arr,0,j);
        }
    }

    /** 迭代寫法*/
    public static void adjustHeap(int []arr,int i,int length){
        int temp = arr[i];
        for (int k = 2*i + 1; k < length; k=k*2 + 1) {
        // 注意這裡的k + 1 < length
            // 如果右子節點大於左子節點,則比較物件為右子節點
            if (k + 1 < length && arr[k] < arr[k+1]){
                k++;
            }
            if (arr[k] > temp){
                // 不進行值交換
                arr[i] = arr[k];
                i = k;
            }
            else{
                break;
            }
        }
        arr[i] = temp;
    }

    /** 遞迴寫法*/
    private static void adjustHeap2(int[] arr, int i, int len){
        int left = 2 * i + 1;
        int right = 2 * i + 2;
        int maxIndex = i;
        // 注意這裡的 left < len
        if (left < len && arr[left] > arr[maxIndex]){
            maxIndex = left;
        }
        if (right < len && arr[right] > arr[maxIndex]){
            maxIndex = right;
        }
        if (maxIndex != i){
            swap(arr,i,maxIndex);
            adjustHeap2(arr,maxIndex,len);
        }
    }

    /** 交換元素 */
    public static void swap(int []arr,int a ,int b){
        int temp=arr[a];
        arr[a] = arr[b];
        arr[b] = temp;
    }
}
 

以上程式碼解析:

1完全二叉樹結構中,如果根節點順序號為 0,總節點數為 N,則最末節點的父節點即為最後一個非葉子節點,順序號為 ceil(N/2 -1),

2 adjustHeap2 為啥使用三個引數,不用中間的引數可以?使用三個引數,是為了進行遞迴呼叫,因為遞迴肯定是縮小計算規模,而這裡的形參arr和len是固定不變的;

3 adjustHeap是非遞迴寫法,不用中間的引數可以?呼叫一在“構建大頂堆”處,可寫為函式體內初始化 i,並形成雙重 for 迴圈;呼叫二在“重新調整大頂堆”處,

    可見中間引數為 0,可直接去掉。故回答是可以!但需要調整寫法,且影響該方法複用,這裡直接寫為三個形參的函式更為優雅而已。

4非遞迴寫法理解:類似插入排序思想(依次移動並找到合適的位置再插入),先將 arr[i] 取出,然後此節點和左右子樹進行比較,如子樹更大則子節點上升一層,使

    用for迴圈迭代到最終位置,並進行賦值;

 

以 i=0 為例:

5遞迴方式理解:定位目標元素的左右子樹,若子樹值更大,則進行值交換,且因為子樹發生了變化,故需要對子樹進行遞迴處理;

3 前K個最大的數

在N個數中找出前K個最大的數: 思路:從N個數中取出前K個數,形成一個數組[K],將該陣列調整為一個小頂堆,則可知堆頂為K個數中最小值,

然後依次將剩餘 N-K 個數與堆頂比較,若大於,則替換掉並調整堆,直到所有元素加入完畢,堆中元素即為目標集合。

public class HeapSort {
    public static void main(String[] args) {
        int[] arr = new int[100];
        for (int i = 0; i < 100; i++) {
            arr[i] = i + 1;
        }
        // 前10個最大的數
        int k = 10;
        // 構造小頂堆
        for (int i = (int) Math.ceil(k/2 - 1); i >= 0; i--) {
            adjustHeap(arr,i,k);
        }
        // 依次比較剩餘元素
        for (int i = 10; i < arr.length; i++) {
            if (arr[i] > arr[0]){
                swap(arr,0,i);
                adjustHeap(arr,0,k);
            }
        }
        // 輸出結果
        for (int i = 0; i < 10; i++) {
            System.out.print(arr[i]+"-");
        }
    }

    /** 非迭代寫法 ,對arr[i]進行調整 */
    private static void adjustHeap(int[] arr,int i,int length){
        int temp = arr[i];
        for (int k = i * 2 + 1; k < length; k = k * 2 + 1) {
            // 因第一次迴圈中可能越界,故需要 k+1 < length
            if (k + 1 < length && arr[k] > arr[k + 1]){
                k++;
            }
            if (arr[k] < temp){
                arr[i] = arr[k];
                i = k;
            }
            else {
                break;
            }
        }
        arr[i] = temp;
    }
    /** 遞迴寫法 */
    private static void adjustHeap2(int[] arr,int i,int length){
        int left = i * 2 + 1;
        int right = i * 2 + 2;
        int samller = i;
        if (left < length && arr[left] > arr[samller]){
            samller = right;
        }
        if (right < length && arr[right] > arr[samller]){
            samller = right;
        }
        if (samller != i){
            swap(arr,i,samller);
            adjustHeap2(arr,samller,length);
        }
    }

    /** 交換元素 */
    public static void swap(int []arr,int a ,int b){
        int temp=arr[a];
        arr[a] = arr[b];
        arr[b] = temp;
    }
}
 

以上程式碼解析:按照"初始化—構建小頂堆—比較調整—輸出結果"執行。注意for迴圈中,因第一次迴圈中未使用for語句條件判斷,可能越界,故需要 k+1 < length

輸出結果如下:

請看官思考,如果需求變為找出N個數中找出前K個最小的數,該如何實現? 建議動腦且動手的寫一遍!因為魔鬼在細節!

全文完!


我近期其他文章:

  • 1 Dubbo學習系列之十九(Apollo配置中心)
  • 2 聊聊演算法——二分查詢演算法深度分析
  • 3 DevOps系列——Jenkins/Gitlab自動打包部署
  • 4 DevOps系列——Jenkins私服
  • 5 DevOps系列——Gitlab私服

    只寫原創,敬請關注 

相關推薦

聊聊演算法--構建調整

先提個問題,完全二叉樹/滿二叉樹,區別?前者是指每一層都是緊湊靠左排列,最後一層可能未排滿,後者是一種特殊的完全二叉樹, 每層都是滿的,即節點總數和深度滿足N=(2^n) -1。堆Heap,一堆蘋果,為了賣相好,越好看的越往上放,就是大頂堆;為了蘋果堆 的穩定,質量越小越往上放,就是小頂堆;堆首先是完全二叉樹

Java資料結構演算法 -

堆的介紹 Q: 什麼是堆? A: 這裡的“堆”是指一種特殊的二叉樹,不要和Java、C/C++等程式語言裡的“堆”混淆,後者指的是程式設計師用new能得到的計算機記憶體的可用部分 A: 堆是有如下特點的二叉樹: 1) 是一棵完全二叉樹 2) 通常由陣列實現。前面介

演算法導論第三版第六章 合併K個有序連結串列的三種解法(最小分治遞迴法)

題目要求是將k個有序連結串列合併為一個連結串列,時間複雜度限定為O(nlogk)。下面給出應用最小堆方法的兩個程式,最後再貼上利用分治遞迴法的程式碼,雖然時間複雜度不及堆方法,但思路相對簡單好理解。 (1)最小堆方法1 用一個大小為K的最小堆(用優先佇列+自定義降序實現)(

對資料結構演算法的總結思考(五)--排序

本篇分享的內容為堆排序,提到堆排序就不得不提一下堆這個資料結構。 堆實際上是一棵完全二叉樹,因此其任何一非葉節點滿足性質: Key[i]<=key[2i+1]&&Key[i]<=key[2i+2]或者Key[i]>=Key

聊聊演算法——BFSDFS

  如果面試位元組跳動和騰訊,上來就是先撕演算法,阿里就是會突然給你電話,而且不太在意是週末還是深夜, 別問我怎麼知道的,想確認的可以親自去試試。說到演算法,直接力扣hard三百題也是可以的,但似乎會比較傷腦, 有沒一些深入淺出系列呢,看了些經典的演算法,發現其實很多演算法是有框架的,今天就先說下很

數組、鏈表、隊列

數組 經典 clas 刪除 概念 連續 con 一個 這樣的 鏈表,隊列,堆棧的區別1、棧是個有底的口袋,像襪子。隊列是沒底的口袋,像通心粉。所以:棧的特點是先進後出,隊列的特點是先進先出。2、主要區別是適用的地方不一樣, 鏈表實際上可以認為是一種數據的物理組織形式

Mooc數據結構-02隊列

插入數據 內容 其他應用 設計 回溯算法 技術分享 掃描 1.2 後綴 1 堆棧 1.1 堆棧的概念   表達式求值問題     表達式 = 運算數 + 運算符號     不同的運算符號優先級不一樣   一般地, 運算是見到運算符號進行運算, 但是在一般的表達式中

(Heap)棧(Stack)

數據結構 item ext 調用 .com 結束 baidu 決定 text 堆,隊列優先,先進先出(FIFO—first in first out) 棧,先進後出(FILO—First-In/Last-Out) 棧(操作系統):由操作系統自動分配釋放 ,存放函數的參

JAVA中內存分配詳解(摘抄)

如果 public china weight 所有 有道 動態 面試題 class 在Java中,有六個不同的地方可以存儲數據: 1.寄存器:最快的存儲區, 由編譯器根據需求進行分配,我們在程序中無法控制. 2. 棧:存放基本類型的變量數據和對象的引用,但對象本身不存放在棧

線索二叉樹的構建遍歷------小甲魚數據結構算法

-- tag typedef pre == 約定 cnblogs amp scan #include <stdio.h> #include <stdlib.h> typedef char ElemType; // 線索存儲標誌位 // Link

類的構建繼承機制(c++)

三種 ges cnblogs private 有一個 只有一個 public 改變 帶來 構建類: 抽象:將同類事物的共同屬性和行為提取出來並將其用變量和函數表達出來; 封裝:將抽象得來的變量和函數捆綁在一起形成一個完整的類(即這類事物擁有了屬性和行為) 控制訪問權限: p

Jenkins集成Docker實現鏡像構建線上發布

gis and ash macro tom 說明 align docker add 原文地址:http://www.cnblogs.com/keithtt/p/6410229.html 大概過程如下圖: 由於需要用到docker打包鏡像,jenkins宿主機上

Maven01——簡介、安裝配置、入門程序、項目構建依賴管理

setting end open 環境變量配置 關系 date execute att imp 1 Maven的簡介 1.1 什麽是maven 是apache下的一個開源項目,是純java開發,並且只是用來管理java項目的  Svn eclipse maven量級

數據庫服務器構建部署筆記

創建 大服務 阻止 server 戴爾 ror 數據庫日誌 操作系統 diff 1. 機架和電纜服務器 確保每個電源插入不同的電源電路 果可能,請確保網絡電纜已插入不同的網絡交換機 2.SQL Server服務和SQL Server

JAVA中內存分配原理

內存大小 內存分配 tin ati 全限定名 void imm 添加 一個 1、棧、堆 1.寄存器:最快的存儲區, 由編譯器根據需求進行分配,我們在程序中無法控制.2. 棧:存放基本類型的變量數據和對象的引用,但對象本身不存放在棧中,而是存放在堆(new 出來的對象)或者

2..棧內存映射詳解

例子 color 註意 圖片 過程 size ont 大量 span 1.每個線程都有自己專屬的棧(stack),先進後出(LIFO) 2.棧的最大尺寸固定,超出則引起棧溢出 3.變量離開作用範圍後,棧上的數據會自動釋放 4.堆上內存必須手動釋放(C/C++)除非語言執行環

自動化部署之jenkins自動觸發構建發布

jenkins 自動觸發構建 一、下載gitlab pluginjenkins-->系統管理-->管理插件-->下載並安裝gitlab plugin二、配置gitlab認證路徑:Jenkins-->Credentials-->System-->Global cred

5、xamarin.android 中如何對AndroidManifest.xml 進行配置調整

receive custom 5.1 包裝 view 調整 加權 啟動 callable 我們在翻看一些java的源碼經常會說我們要在AndroidManifest.xml 中添加一些東西。而我們使用xamarin裏面實際上是通過C#的特性Attribute進行標記實現的

Android 音視頻深入 十三 OpenSL ES 制作音樂播放器,能暫停調整音量(附源碼下載)

音視頻 OpenSL ES 項目地址https://github.com/979451341/OpenSLAudio OpenSL ES 是基於NDK也就是c語言的底層開發音頻的公開API,通過使用它能夠做到標準化, 高性能,低響應時間的音頻功能實現方法。 這次是使用OpenSL ES來做一個音樂播

如何用Python實現隊列詳細講解

Python語言 Python編程開發 Python案例應用 python實現堆棧   堆棧是一個後進先出的數據結構,其工作方式就像一堆汽車排隊進去一個死胡同裏面,最先進去的一定是最後出來。   我們可以設置一個類,用列表來存放棧中元素的信息,利用列表的append和pop方法可以實現棧的出棧po