堆排序以及Top K問題-Java實現


一.問題背景

  如果做過參加過面試或者做過一些面試題,應該知道特別經典的top K問題,比如“找出無序數組中的最大或者最小K個數”:

  這種題可以排序后再輸出最大或者最小的幾個。但是不論是使用快排還是歸並排序,毫無疑問,空間和時間復雜度的開銷都是不滿足面試官的要求的;而使用“堆”這種數據結構就比較好的解決這種問題,空間開銷O(1),時間開銷O(N logK)。

  需要注意的是,這里說的“堆”不是指堆棧的堆,而是一種數據結構,更准確的說是“完全二叉樹”。

  下面就詳細對堆這種數據結構進行介紹,注:本文的內容是在學習浙江大學何欽銘教授的數據結構課程后整理的。

  標注原文地址:https://www.cnblogs.com/-beyond/p/13084115.html

 

二.堆的介紹

2.1數組和鏈表實現優先隊列

  再說Top K之前,先說一下調度算法。學過操作系統就知道,進程調度有多種算法,而最簡單的就是“先到先服務”算法,這種算法可以簡單的使用隊列來實現,但是存在一個問題就是無法根據進程的優先級調整執行順序,比如有兩個進程,一個進程只是連接打印機打印一張紙,另外一個進程負責核心功能處理,很明顯,核心功能處理的進程優先級更高,但是操作系統按照先到先服務算法來調度時,核心功能處理的進程並不是優先調度;

  這個時候可以切換為“按照優先級”進行調度,只需要每次選擇最高優先級的進程執行,自己進行實現的話,有多種方式:

  

  仔細想一下,Top K的問題,和這里說的調度優先級其實是一樣的問題。

  

2.2堆的介紹

  1.堆是一種樹結構,准確的說是“二叉樹”,更准確的說是“完全二叉樹”,根據完全二叉樹的特點,可以使用數組來存儲堆。

  2.堆是有序的,任一節點的關鍵字是其子樹所有節點的最大值或者最小值。

  3.如果根節點是最大值的堆,稱為“最大堆”或者“大頂堆”、“大根堆”;

  4.如果根節點是最小值的堆,稱為“最小堆”、“小頂堆”、“小根堆”;

 

   

 

三.堆的各種操作

3.1創建堆

  因為堆滿足完全二叉樹的特點,所以可以使用數組來存儲堆;下面是代碼:

package cn.ganlixin.tree.heap;

import java.util.Scanner;

/**
 * 描述:
 * 數據結構-堆(此處為最大堆)
 *
 * @author ganlixin
 * @create 2020-06-09
 */
public class MaxHeap {

    /**
     * 保存堆元素的數組
     */
    private int[] elements;

    /**
     * 堆的大小
     */
    private int size;

    /**
     * 堆的容量
     */
    private int capacity;

    public MaxHeap() {
        this.size = 0;
        this.capacity = 0;
    }

    /**
     * 建堆並調整堆
     */
    public void createMaxHeap() {
        Scanner scanner = new Scanner(System.in);

        System.out.print("請輸入堆的最大容量:");
        this.capacity = scanner.nextInt();
        this.size = 0;

        // 數組長度為容量加1,0號元素為哨兵元素
        this.elements = new int[this.capacity + 1];
        this.elements[0] = Integer.MAX_VALUE;

        System.out.print("請輸入元素個數:");
        this.size = scanner.nextInt();

        if (this.size > this.capacity) {
            throw new RuntimeException("元素個數不能超過最大容量!");
        }

        System.out.print("請依次輸入" + this.size + "個元素:");
        for (int i = 1; i <= this.size; i++) {
            elements[i] = scanner.nextInt();
        }

        buildHeap(); // 構建堆(因為初始狀態,數組並不滿足堆的有序性特點,所以需要進行調整構建,后面會介紹)
        System.out.println("已經完成堆的建立和調整");
    }
}

  

3.2堆的插入

  新元素,插入堆時,默認是插入到最后一個位置,這樣保證滿足完全二叉樹的特點,但是可能不滿足有序性的特點,所以需要進行一些調整;

  對於最大堆來說,任一根節點都比子節點的值大,所以如果插入的元素(默認是在最后),就需要和其父節點進行比較,如果比父節點大,則需要與父節點交換位置;這是一次調整,但是調整完以后,新插入的節點也許還會比新的父節點大,所以還需要繼續比較,直到父節點比自己大,才停止比較,此時才找到新增元素應該插入的位置。

/**
 * 向最大堆中新增一個元素<br>
 * 空間復雜度O(1),時間復雜度O(logN)
 *
 * @param newItem 新增的元素值
 */
public void insertElement(int newItem) {
    // 新元素插入的位置,默認為最后一個元素的后面
    int nextIndex = ++this.size;

    // 將新元素放到最后,可以滿足完全二叉樹的要求,但是有序性不一定能保證,所以需要和父節點進行比較(父節點就是index/2)
    while (elements[nextIndex / 2] < newItem) {
        // 當父節點比新插入的節點小的時候,將父節點移到新節點准備插入的位置
        elements[nextIndex] = elements[nextIndex / 2];

        // 修改新節點准備插入的位置(此時為父節點的舊位置)
        nextIndex /= 2;
    }

    // 到此,nextIndex就指向了應該插入的位置(比子節點都大,比父節點小)
    elements[nextIndex] = newItem;
}

  

3.3堆元素的刪除

  堆元素的刪除(最大堆),是指將堆的最大值刪除(也就是對頂元素給刪除),刪除對頂元素后,需要進行調整,默認是使用最后一個元素來頂替對頂元素,這樣可以滿足完全二叉樹的特點,但是不一定滿足有序性,所以需要調整;

  調整的過程,就是比較堆頂節點(此時已經替換為最后一個節點值)與子節點,當根節點比子節點小的時候,就交換根節點和子節點的位置,知道根節點大於子節點(左右子節點),才能確定根節點應該插入的位置。

/**
 * 刪除最大堆的最大值(堆頂元素)
 *
 * @return 堆最大值
 */
public int deleteMaxItem() {
    // 第一個元素就是最大值
    int maxItem = elements[1];

    // 將最后一個元素取出來(刪除,size減一),用來替補第一個元素(被刪除的最大值)
    int lastItem = elements[size];
    size--;

    // 最后一個元素存放的位置,默認為1,表示第一個位置
    int insertIndex = 1;

    // 最后一個元素不一定是最大的,放到堆頂不一定合適,所以需要調整
    while (insertIndex * 2 <= size) {
        // childIndex默認指向左孩子
        int childIndex = insertIndex * 2;

        // 如果父節點有右孩子,並且左孩子比右孩子小,則childIndex指向較大的元素(也就是右孩子)
        if (childIndex != size && elements[childIndex] < elements[childIndex + 1]) {
            childIndex++;
        }

        // 當最后一個元素大於指向的元素時,證明找到了插入位置,則中斷循環
        if (lastItem >= elements[childIndex]) {
            break;
        } else {
            // 最后一個元素比子節點小(比較大的節點小),則交換較大節點和父節點的位置
            elements[insertIndex] = elements[childIndex];
        }

        // 父節點指向空出來的子節點位置
        insertIndex = childIndex;
    }

    elements[insertIndex] = lastItem;
    return maxItem;
}

  

3.3將無序數組調整堆

  就以top K的問題來說,只需要建立一個堆的數據結構,然后彈出堆頂的K個元素,就是top K。

  但現在的問題是,提供的數組是無序的,怎么講無序數組轉換為堆:

  1.一種方式是從空堆開始一個一個添加元素,添加元素過程中會進行調整,元素插入完畢,堆也就建好了,這樣的時間復雜度是N log(N),比較低效;

  2.直接在無序數組上進行調整,將期調整為堆結構,時間復雜度為O(logN);

  下面就介紹一下第二種方式。

  直接在無序數組上調整,不是從對頂元素開始調整,而是從最后一個元素進行調整,調整的過程和插入的過程相似:找到節點的父節點,以父節點為根調整為最大堆(根節點與左右子節點選最大值作為根),如此反復

/**
 * 建立最大堆<br>
 * 兩種方案<br>
 * 方案一:建立空堆,N個數,N次插入,時間復雜度O(N*logN),舍棄!<br>
 * 方案二:先順序輸入,滿足完全二叉樹要求,再進行調整堆,滿足有序性,時間復雜度O(N)
 */
private void buildHeap() {
    for (int i = size / 2; i > 0; i--) { // size/2是最后一個元素的父節點位置
        adjustHeap(i);
    }
}

/**
 * 以index指向的節點作為根,將該子堆調整為最大堆
 *
 * @param root 子堆的根節點
 */
private void adjustHeap(int root) {

    // 取出根節點存的值
    int rootVal = elements[root];

    // insertIndex指向根節點值應該插入的位置
    int insertIndex = root;
    while (insertIndex * 2 <= size) {
        int childIndex = insertIndex * 2;
        if (childIndex != size && elements[childIndex] < elements[childIndex + 1]) {
            childIndex++;
        }

        // 如果根節點的值大於子節點,則證明找到了插入的位置
        if (rootVal > elements[childIndex]) {
            break;
        } else {
            elements[insertIndex] = elements[childIndex];
        }

        insertIndex = childIndex;
    }

    elements[insertIndex] = rootVal;
}

  

四.完成代碼

  封裝在MaxHeap.java中(最大堆)

package cn.ganlixin.tree.heap;

import java.util.Scanner;

/**
 * 描述:
 * 數據結構-堆(此處為最大堆)
 * 完全二叉樹,使用數組存儲
 *
 * @author ganlixin
 * @create 2020-06-09
 */
public class MaxHeap {

    /**
     * 保存堆元素的數組
     */
    private int[] elements;

    /**
     * 堆的大小
     */
    private int size;

    /**
     * 堆的容量
     */
    private int capacity;

    public MaxHeap() {
        this.size = 0;
        this.capacity = 0;
    }

    /**
     * 建堆並調整堆
     */
    public void createMaxHeap() {
        Scanner scanner = new Scanner(System.in);

        System.out.print("請輸入堆的最大容量:");
        this.capacity = scanner.nextInt();
        this.size = 0;

        // 數組長度為容量加1,0號元素為哨兵元素
        this.elements = new int[this.capacity + 1];
        this.elements[0] = Integer.MAX_VALUE;

        System.out.print("請輸入元素個數:");
        this.size = scanner.nextInt();

        if (this.size > this.capacity) {
            throw new RuntimeException("元素個數不能超過最大容量!");
        }

        System.out.print("請輸入" + this.size + "個元素:");
        for (int i = 1; i <= this.size; i++) {
            elements[i] = scanner.nextInt();
        }

        buildHeap();
        System.out.println("已經完成堆的建立和調整");
    }

    /**
     * 建立最大堆<br>
     * 兩種方案<br>
     * 方案一:建立空堆,N個數,N次插入,時間復雜度O(N*logN),舍棄!<br>
     * 方案二:先順序輸入,滿足完全二叉樹要求,再進行調整堆,滿足有序性,時間復雜度O(N)
     */
    private void buildHeap() {
        if (isEmpty()) {
            throw new RuntimeException("堆為空,無法完成建堆操作");
        }

        for (int i = size / 2; i > 0; i--) {
            adjustHeap(i);
        }
    }

    /**
     * 以index指向的節點作為根,將該子堆調整為最大堆
     *
     * @param root 子堆的根節點
     */
    private void adjustHeap(int root) {

        // 取出根節點存的值
        int parentVal = elements[root];

        // parentIndex指向根節點值應該插入的位置
        int parentIndex = root;
        while (parentIndex * 2 <= size) {
            int childIndex = parentIndex * 2;
            if (childIndex != size && elements[childIndex] < elements[childIndex + 1]) {
                childIndex++;
            }

            // 如果根節點的值大於子節點,則證明找到了插入的位置
            if (parentVal > elements[childIndex]) {
                break;
            } else {
                elements[parentIndex] = elements[childIndex];
            }

            parentIndex = childIndex;
        }

        elements[parentIndex] = parentVal;
    }

    /**
     * 向最大堆中新增一個元素<br>
     * 空間復雜度O(1),時間復雜度O(logN)
     *
     * @param newItem 新增的元素值
     */
    public void insertElement(int newItem) {
        if (isFull()) {
            throw new RuntimeException("堆已滿,無法再添加元素");
        }

        // 新元素插入的位置,默認為最后一個元素的后面
        int nextIndex = ++this.size;

        // 將新元素放到最后,可以滿足完全二叉樹的要求,但是有序性不一定能保證,所以需要和父節點進行比較(父節點就是index/2)
        while (elements[nextIndex / 2] < newItem) {
            // 當父節點比新插入的節點小的時候,將父節點移到新節點准備插入的位置
            elements[nextIndex] = elements[nextIndex / 2];

            // 修改新節點准備插入的位置(此時為父節點的舊位置)
            nextIndex /= 2;
        }

        // 到此,nextIndex就指向了應該插入的位置(比子節點都大,比父節點小)
        elements[nextIndex] = newItem;
    }

    /**
     * 刪除最大堆的最大值(堆頂元素)
     *
     * @return 堆最大值
     */
    public int deleteMaxItem() {
        if (isEmpty()) {
            throw new RuntimeException("堆為空,不能進行刪除操作");
        }

        // 第一個元素就是最大值
        int maxItem = elements[1];

        // 將最后一個元素取出來(刪除,size減一),用來替補第一個元素(被刪除的最大值)
        int lastItem = elements[size];
        size--;

        // 最后一個元素存放的位置,默認為1,表示第一個位置
        int insertIndex = 1;

        // 最后一個元素不一定是最大的,放到堆頂不一定合適,所以需要調整
        while (insertIndex * 2 <= size) {
            // childIndex默認指向左孩子
            int childIndex = insertIndex * 2;

            // 如果父節點有右孩子,並且左孩子比右孩子小,則childIndex指向較大的元素(也就是右孩子)
            if (childIndex != size && elements[childIndex] < elements[childIndex + 1]) {
                childIndex++;
            }

            // 當最后一個元素大於指向的元素時,證明找到了插入位置,則中斷循環
            if (lastItem >= elements[childIndex]) {
                break;
            } else {
                // 最后一個元素比子節點小(比較大的節點小),則交換較大節點和父節點的位置
                elements[insertIndex] = elements[childIndex];
            }

            // 父節點指向空出來的子節點位置
            insertIndex = childIndex;
        }

        elements[insertIndex] = lastItem;
        return maxItem;
    }

    /**
     * 打印排序后的堆
     */
    public void printSortedHeap() {
        for (int i = 1; i <= this.size; i++) {
            System.out.print(elements[i] + " ");
        }
        System.out.println();
    }

    /**
     * 判斷堆是否已經滿了(size>=capacity)
     *
     * @return true堆已滿;false堆未滿
     */
    public boolean isFull() {
        return this.size >= capacity;
    }

    /**
     * 判斷堆是否為空
     *
     * @return true堆為空;false堆不為空
     */
    public boolean isEmpty() {
        return this.size == 0;
    }
}

  測試:

package cn.ganlixin.tree.heap;

/**
 * 描述:
 * 測試最大堆
 *
 * @author ganlixin
 * @create 2020-06-09
 */
public class Main {
    public static void main(String[] args) {
        MaxHeap maxHeap = new MaxHeap();

        // 輸入元素,並調整堆
        maxHeap.createMaxHeap();

        System.out.print("輸出堆:");
        maxHeap.printSortedHeap();

        int deleteMaxItem = maxHeap.deleteMaxItem();
        System.out.println("刪除堆中最大元素:" + deleteMaxItem);
        System.out.print("輸出堆:");
        maxHeap.printSortedHeap();
    }
}

  輸出:

請輸入堆的最大容量:10
請輸入元素個數:6
請依次輸入6個元素:8 5 9 6 4 2
已經完成堆的建立和調整
輸出堆:9 6 8 5 4 2 
刪除堆中最大元素:9
輸出堆:8 6 2 5 4 

  

五.再說Top K問題

  其實上面介紹完堆的各種操作后,對於Top K的問題已經能夠解決了,此處以最大的top K問題為例:

  需要注意的是,在建堆的時候,並不是將整個數組的N個元素都調整,而是只調整前K個元素,讓前K個元素保持堆的結構,也就是說,堆的容量,是K,而不是N。步驟如下:

  1.將前K個元素調整為最小堆(也稱“小頂堆”、“小根堆”);

  2.依次將K+1后面的元素(看做新元素),與堆頂元素(堆的最小值)進行比較:

a.如果堆頂元素比新元素要大,則新元素不用入堆,忽略;

b.如果堆頂元素比新元素要小,則將新元素替換掉堆頂元素,然后進行調整堆(始終保證堆頂元素是堆中元素的最小值);

  3.不斷重復步驟2,直至比較完N-K個元素;

  4.比較完后,堆中的元素就是最大的K個元素。

  說直白點,就是進行N-K+1次調整堆,整個流程的時間復雜度為O(N logK)

  如果需要按照排序輸出K個元素,則進行K次刪除堆頂元素即可(每次刪除都會調整堆,保證堆頂最小)。

  下面是代碼:

package cn.ganlixin.tree.heap;

import java.util.Scanner;

/**
 * 描述:
 *
 * @author ganlixin
 * @create 2020-06-10
 */
public class TopK {

    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);

        System.out.print("請輸入元素總個數:");
        int capacity = scanner.nextInt();

        System.out.print("請輸入要找最大的幾個數:");
        int k = scanner.nextInt();

        // 申請一個K+1的數組(因為建立的堆包含K個元素,而不是N個元素,0號元素用來做哨兵)
        int[] arr = new int[k + 1];
        arr[0] = Integer.MAX_VALUE;

        System.out.print("請輸入全部元素:");

        // 先前K個元素進行建堆調整
        for (int i = 1; i <= k; i++) {
            arr[i] = scanner.nextInt();
        }

        // 先將前K個元素進行調整為最小堆(小頂堆)
        adjustHeap(arr, 1, k);

        // 繼續處理后面的n-k個元素,和堆頂元素進行比較,如果比堆頂元素大,則替換堆頂元素,並進行調整堆
        for (int i = k + 1; i <= capacity; i++) {
            int newItem = scanner.nextInt();
            if (newItem > arr[1]) {
                arr[1] = newItem; // 替換為新元素

                // 每次整個堆都調整
                adjustHeap(arr, 1, k);
            }
        }

        System.out.print("最大的" + k + "個數是:");
        for (int i = 1; i <= k; i++) {
            System.out.print(arr[i] + " ");
        }
    }

    /**
     * 將數組調整為滿足最小堆的結構
     *
     * @param arr   要調整的數組
     * @param start 要調整的開始位置(index)
     * @param end   要調整的結束為止(index)
     */
    private static void adjustHeap(int[] arr, int start, int end) {
        for (int i = end / 2; i > 0; i--) { // end/2是最后一個節點的父節點
            int parentVal = arr[i];
            int parentIndex = i;

            while (parentIndex * 2 <= end) {
                int childIndex = parentIndex * 2; // 左孩子節點

                // childIndex指向兩個子節點中較小的一個
                if (childIndex != end && arr[childIndex] > arr[childIndex + 1]) {
                    childIndex++;
                }

                // 比較父節點和較大一個子節點的值,小頂堆需要父節點比子節點小
                if (parentVal < arr[childIndex]) {
                    break;
                } else {
                    arr[parentIndex] = arr[childIndex];
                }

                parentIndex = childIndex;
            }

            arr[parentIndex] = parentVal;
        }
    }
}

  測試:

請輸入元素總個數:14
請輸入要找最大的幾個數:5
請輸入全部元素:20 5 2 8 10 3 23 5 99 24 0 -7 8 100
最大的5個數是:20 23 100 24 99 

  


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM