圖形化理解二叉樹 -> 堆 -> 堆排序(Java + Python實現)

二叉樹

二叉樹最多隻有左子樹和右子樹兩個子樹,二叉樹的性質如下:

  • 在二叉樹的第ii層最多有2i12^{i-1}個節點
  • 深度爲kk的二叉樹最多有2k12^{k-1}個節點
  • 對於任意一棵二叉樹,如果葉節點數爲N0N_{0},而度數爲2的節點總數爲N2N_{2},則有N0=N2+1N_0 = N_2 + 1
  • 具有nn個節點的完全二叉樹的深度必爲log2(n+1)\log2(n+1)

常用的二叉樹有滿二叉樹完全二叉樹,滿二叉樹指除最後一層無任何子節點外,每一層上的所有結點都有兩個子結點二叉樹;完全二叉樹指當二叉樹的深度爲hh,除第 hh 層外,其它各層 (1h1)(1 \sim h-1) 的結點數都達到最大個數,第 hh 層所有的結點都連續集中在最左邊的二叉樹。
在這裏插入圖片描述

對於一棵使用數組[1,2,3,4,5,6,7,8,10,11][1,2,3,4,5,6,7,8,10,11]表示的完全二叉樹中的第ii個節點來說,那麼它的父節點和孩子節點表示爲:

  • 左孩子:left=2i+1\text{left} = 2i + 1
  • 右孩子:right=left+1\text{right} = \text{left}+ 1或者right=2i+2\text{right} = 2i + 2
  • 父節點:parent=i12\text{parent} = \frac{i - 1}{2}

例如下圖中索引爲1的節點爲2,那麼它的父節點就是索引爲0的節點1,它的左孩子爲索引爲3的節點4,右孩子是索引爲4的節點5。


在這裏插入圖片描述

堆概念

堆是一種經過排序的二叉樹,它是數據結構中可以被看作是一棵樹的數組對象,堆通常滿足如下的兩個性質:

  • 堆中某個節點的值總是不大於或不小於其父節點值
  • 堆總是一棵完全二叉樹

堆通常有大根堆和小根堆兩種:

  • 大根堆:根節點的值大於左右子樹的值,任意子樹也是大根堆
  • 小根堆:根節點的值小於左右子樹的值,任意子樹也是小根堆

例如:


在這裏插入圖片描述


堆的構建

假設現在的數據爲[2,1,3,6,0,4][2, 1, 3, 6, 0, 4],那麼大根堆的構建示意圖如下所示(小根堆的創建類似) :
在這裏插入圖片描述

如上所示,堆的構建過程爲:

  • 2:此時堆爲空,將2作爲根節點
  • 1:將1作爲2的左孩子,構建完全二叉樹,由於1 < 2,不執行交換操作
  • 3:將3作爲2的右孩子,由於3 > 2,將3和它的父節點2交換,此時3已是根節點,動作停止
  • 6:將6作爲1的左孩子,由於6 > 1,執行交換;由於 6 > 3,執行交換根節點
  • 0:將0作爲3的右孩子,由於 0 < 3,不執行交換
  • 4:將4作爲2的左孩子,由於 4 > 2, 執行交換;由於4 < 6,動作停止

代碼實現:

import java.util.Arrays;

public class HeapTest {
    public static void main(String[] args) {
        int[] nums = {2,1,3,6,0,4};
        HeapSort(nums);
        System.out.println(Arrays.toString(nums));  // [6, 3, 4, 1, 0, 2]
    }

    public static void HeapSort(int[] nums) {
        // 如果數組爲空或者只有一個元素,直接返回
        if (nums == null || nums.length < 2){
            return;
        }
        // 否則依次將數組中的節點插入到大根堆中
        for (int i = 0; i < nums.length; i++) {
            HeapInsert(nums, i);
        }
    }

    public static void HeapInsert(int[] nums, int i) {
        // 如果當前節點值大於它的父節點值,將其交換
        // 知道while中的條件不成立,即當前二叉樹已是大根堆
        // 創建小根堆:while (nums[i] < nums[(i- 1) / 2]){...}
        while (nums[i] > nums[(i- 1) / 2]){
            swap(nums, i, (i - 1) / 2);
            // 更新需考慮的索引地址
            i = (i - 1) / 2;
        }
    }
    public static void swap(int[] nums, int i, int j){
        int temp = nums[i];
        nums[i] = nums[j];
        nums[j] = temp;
        }
}


時間複雜度: 由於將新節點插入大根堆調整的過程中,只需要考慮根節點到當前節點路徑上的值,那麼值的個數也就是當前節點的層數,所以堆創建的時間複雜度爲:
O=log1+log2+...+log(N)=i=1Nlogi=O(N) O = \log1 + \log 2 + ... + \log(N) = \sum_{i = 1}^{N} \log i = O(N)


堆的調整

假設經過上面的步驟已經將[2,1,3,6,0,4][2, 1, 3, 6, 0, 4]創建爲對應的大根堆,它的數組存儲形式爲[6,3,4,1,0,2][6, 3, 4, 1, 0, 2]。如果此時數組中的元素髮生了改變,改變後數組對應的二叉樹已經不滿足大根堆的性質,那麼就需要對現在的二叉樹進行調整,使其重新滿足大根堆的性質。假設數組中的6變成了1,那麼大根堆的調整過程爲:
在這裏插入圖片描述

實現代碼:

import java.util.Arrays;

public class HeapTest {
    public static void main(String[] args) {
        int[] array = {6, 3, 4, 1, 0, 2};
     
        array[0] = 1;
        System.out.println(array);
        Heapify(array, 0, array.length);
        System.out.println(Arrays.toString(array)); // [4, 3, 2, 1, 0, 1]
    }
	
	// size表示堆的數值範圍
    public static void Heapify(int[] array, int i, int size){
        // 看左右孩子
        int left = 2 * i + 1;
        while (left < size){
            // 右孩子爲left + 1
            // 尋找左右孩子最大的哪那一個,將其索引賦給largest
            int largest = left + 1 < size && array[left + 1] > array[left] ? left+ 1 : left;
            // 判斷largest指向的節點和當前節點的關係
            // 如果當前節點小於左右孩子中最大的節點,則更新largest
            largest = array[largest] > array[i] ? largest : i;
            // 如果當前已是大根堆,則跳出
            if (largest == i){
                break;
            }
            // 否則執行交換,繼續往下判斷
            swap(array, largest, i);
            i = largest;
            left = 2 * i + 1;
        }
    }

    public static void swap(int[] array, int i, int j){
        int temp = array[i];
        array[i] = array[j];
        array[j] = temp;
    }
}


堆排序

經過前面的講述,我們知道了如何根據給定的數組創建堆,並知道在數組中元素改變破壞已有的堆時如何進行調整,使其重新成爲一個堆。那麼如何根據堆的構建和堆的調整來實現堆排序呢?假設現在構建的是大根堆,那麼堆的根節點值就是當前數組中的最大值,那麼不斷的彈出根節點然後再調整保持堆的性質,直到最後堆爲空,那麼依次彈出的節點值就是有序的,堆排序自然就完成了。

具體操作: 在彈出根節點並調整堆的過程中使用一個變量size,它表示數組中0size0 \sim size區間的元素保持堆的性質

  • 將根節點和堆中最後一個元素交換,size減一
  • 調整剩下的元素使其仍然爲一個大根堆
  • 不斷重複上述過程,直到數組爲空
    在這裏插入圖片描述

代碼實現:

import java.util.Arrays;

public class HeapTest {
    public static void main(String[] args) {
        int[] array = {2,1,3,6,0,4};
        HeapSort(array);
        System.out.println(Arrays.toString(array)); // [0, 1, 2, 3, 4, 6]
    }

    public static void HeapSort(int[] array) {
        if (array == null || array.length < 2){
            return;
        }
        for (int i = 0; i < array.length; i++) {
            HeapInsert(array, i);
        }
        // size維護數組中滿足堆性質的區域
        int heap_size = array.length;
        // 交換根節點和數組的最後一個元素,更新size
        swap(array, 0, --heap_size);
        while (heap_size > 0){
            // 調整剩下的元素使其構成堆
            Heapify(array, 0, heap_size);
            swap(array, 0, --heap_size);
        }
    }

    public static void HeapInsert(int[] array, int i) {
        while (array[i] > array[(i- 1) / 2]){
            swap(array, i, (i - 1) / 2);
            i = (i - 1) / 2;
        }
    }

    public static void Heapify(int[] array, int i, int size){
        // 看左右孩子
        int left = 2 * i + 1;
        while (left < size){
            // 右孩子爲left + 1
            int largest = left + 1 < size && array[left + 1] > array[left] ? left+ 1 : left;
            // 更新largest
            largest = array[largest] > array[i] ? largest : i;
            // 如果當前已是堆則跳出
            if (largest == i){
                break;
            }
            swap(array, largest, i);
            i = largest;
            left = 2 * i + 1;
        }
    }

    public static void swap(int[] array, int i, int j){
        int temp = array[i];
        array[i] = array[j];
        array[j] = temp;
    }
}


Python代碼實現:

class HeapSort:
    def __init__(self, array):
        super().__init__()
        self.array = array

    def HeapSort(self):
        if self.array is None or len(self.array) < 2:
            return

        for i in range(len(self.array)):
            self.HeapInsert(i)

        heap_size = len(self.array)
        heap_size -= 1
        self.swap(0, heap_size)
        while heap_size > 0:
            self.Heapify(0, heap_size)
            heap_size -= 1
            self.swap(0, heap_size)
    
    
    def HeapInsert(self, index):
        while self.array[index] > self.array[(index - 1) // 2] and (index - 1) // 2 >= 0:
            self.swap(index, (index - 1) // 2)
            index = (index - 1) // 2 
            
    def Heapify(self, i, heapsize):
        left = 2 * i + 1
        right = 2 * i + 2
        while left < heapsize:
            if right < heapsize and self.array[right] > self.array[left]:
                largest = right
            else:
                largest = left
            
            largest = largest if self.array[largest] > self.array[i] else i
            if largest == i:
                break
            self.swap(largest, i)
            i = largest
            left = 2 * i + 1

    def swap(self, i, j):
        self.array[i], self.array[j] = self.array[j], self.array[i]


if __name__ == "__main__":
    array = [2,1,3,6,0,4]
    # array = [1, 3, 4, 1, 0, 2]
    heap = HeapSort(array)

    heap.HeapSort()
    # heap.Heapify(0, len(array))
    print (heap.array)

算法複雜度

  • 時間複雜度:O(NlogN)O(N * \log N)
  • 空間複雜度:O(1)O(1)
  • 穩定性:不穩定
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章