與堆和堆排序相關的問題

Grey Zeng 發表於 2022-11-28

與堆和堆排序相關的問題

作者:Grey

原文地址:

部落格園:與堆和堆排序相關的問題

CSDN:與堆和堆排序相關的問題

堆結構說明

堆結構就是用陣列實現的完全二叉樹結構,什麼是完全二叉樹?可以參考如下兩篇部落格:

使用二叉樹的遞迴套路來解決的問題

快速求完全二叉樹的節點個數

完全二叉樹中如果每棵子樹的最大值都在頂部就是大根堆;完全二叉樹中如果每棵子樹的最小值都在頂部就是小根堆。

Java 語言中的 java.util.PriorityQueue,就是堆結構。

因為是用用陣列表示完全二叉樹,所以有如下兩個換算關係,也就是堆的兩種表示情況:

情況一,如果使用陣列 0 號位置,那麼對於 i 位置來說,它的:

  • 左孩子下標:2 * i + 1

  • 右孩子下標: 2 * i + 2

  • 父節點下標: (i - 1)/ 2

情況二,如果不用陣列 0 號位置,那麼對於 i 位置來說,它的:

  • 左孩子下標:2 * i 即:i << 1

  • 右孩子下標:2 * i + 1 即:i << 1 | 1

  • 父節點下標:i / 2 即:i >> 1

如果是小根堆(下標從 0 開始),

對每個元素 A[i],都需要滿足 A[i * 2 + 1] >= A[i]A[i * 2 + 2] >= A[i]

如果是小根堆(下標從 0 開始),

對每個元素 A[i],都需要滿足 A[i * 2 + 1] <= A[i]A[i * 2 + 2] <= A[i]

大根堆同理。

堆的資料結構定義如下,以大根堆為例,以下是虛擬碼

  // 大根堆
  public static class MyMaxHeap {
    // 用於存堆的資料
    private int[] heap;
    // 堆最大容納資料的數量
    private final int limit;
    // 堆當前的容量
    private int heapSize;
    
    // 堆初始化
    public MyMaxHeap(int limit) {
      heap = new int[limit];
      this.limit = limit;
      heapSize = 0;
    }
    // 判斷堆是否為空
    public boolean isEmpty() {
      return heapSize == 0;
    }
    // 判斷堆是否滿
    public boolean isFull() {
      return heapSize == limit;
    }
    public void push(int value) {
      // TODO 入堆
      // 注意:入堆後,也要保持大根堆的狀態
    }
    public int pop() {
      // TODO 最大值出堆
      // 注意:出堆後,也要保持大根堆的狀態
    }
  }

由上述資料結構定義可知,核心方法就是 pushpop,在每次操作後,要動態調整堆結構,使之保持大根堆的結構。

要完成這兩個操作,就需要利用到堆的兩個基本操作:

一個是 HeapInsert,一個是 Heapify。

Heapify 操作

Heapify 就是堆化的過程,以小根堆為例,示例說明

假設原始陣列為:{3,2,1,4,5},初始狀態如下

image

首先從頭結點 3 開始,先找到 3 的左右孩子中較小的一個進行交換,現在較小的是右孩子 1,交換後是如下情況

image

互換後,3 號結點已經沒有左右孩子了,停止操作。

然後按順序繼續處理 2 結點,2 結點已經比左右孩子都小了,無需進行交換。

image

接下來是 4 結點和 5 結點,都沒有左右孩子,就無需再做操作。

整個流程就是,每個結點(假設為 X )去找自己的左右孩子中較小的那個(加設為 Y),然後X 和 Y 交換位置,交換後,看 X 是否繼續有孩子結點,往復這個過程,一直到整個二叉樹遍歷完成。

完整程式碼如下:

public class Solution {
  public static void heapify(int[] arr) {
    if (arr == null || arr.length <= 1) {
      return;
    }
    for (int i = arr.length - 1; i >= 0; i--) {
      heapify(arr, i, arr.length);
    }
  }
  private static void heapify(int[] arr, int i, int n) {
    int left = 2 * i + 1;
    while (left < n) {
      int min = left + 1 < n && arr[left + 1] < arr[left] ? left + 1 : left;
      if (arr[i] <= arr[min]) {
        break;
      }
      swap(arr, i, min);
      i = min;
      left = 2 * i + 1;
    }
  }

  private static void swap(int[] arr, int i, int j) {
    if (i != j) {
      arr[i] = arr[i] ^ arr[j];
      arr[j] = arr[i] ^ arr[j];
      arr[i] = arr[i] ^ arr[j];
    }
  }
}

測評連結:LintCode 130 · Heapify

HeapInsert 操作

整個過程如下,以小根堆為例,從陣列最後一個元素 X 開始,一直找其父節點 A,如果X 比 A 小,X 就和 A 交換,然後來到父節點 A,繼續往上找 A 的父節點 B,如果 A 比 B 小,則把 A 和 B 交換……一直找到某個結點的頭結點不比這個結點大,這個節點就可以停止移動了。以一個示例說明

假設原始陣列為:{3,2,1,4,5},初始狀態如下

image

從最後一個元素 5 開始,5 的父節點是 2,正好滿足,無需繼續往上找父節點,然後繼續找倒數第二個位置 4 的父節點,也比父節點 2 要大,所以 4 節點也不需要動。

image

接下來是 1 結點,其父結點是 3 結點,所以此時要把 3 和 1 交換,變成如下樣子

image

然後是 2 結點,2 結點的父節點 是 1 ,無需交換,然後是 1 結點,頭結點,停止遍歷,整個過程完畢。

HeapInsert 操作的完整程式碼如下

    private void heapInsert(int[] arr, int i) {
      while (arr[i] > arr[(i - 1) / 2]) {
        // 一直網上找
        swap(arr, i, (i - 1) / 2);
        i = (i - 1) / 2;
      }
    }

無論是 HeapInsert 還是 Heapify,整個過程時間複雜度是 O(logN),N 是二叉樹結點個數,其高度是 logN。

有了 Heapify 和 HeapInsert 兩個過程,整個堆的 pop 操作和 push 操作都迎刃而解。

    public void push(int value) {
    // 堆滿了,不能入堆
      if (heapSize == limit) {
        throw new RuntimeException("heap is full");
      }
      // 把最後一個位置填充上,然後往小做 heapInsert 操作
      heap[heapSize] = value;
      // value  heapSize
      heapInsert(heap, heapSize++);
    }

    public int pop() {
      // 彈出的值一定是頭結點
      int ans = heap[0];
      // 頭結點彈出後,直接放到最後一個位置,然後往上做 heapify
      // 由於 heapSize 來標識堆的大小,heapSize--,就等於把頭結點刪掉了。
      swap(heap, 0, --heapSize);
      heapify(heap, 0, heapSize);
      return ans;
    }

堆排序

瞭解了 HeapInsert 和 Heapify 過程,堆排序過程,也就是利用了這兩個方法,流程如下

第一步:先讓整個陣列都變成大根堆結構,建立堆的過程:

如果使用從上到下的方法,時間複雜度為O(N*logN)

如果使用從下到上的方法,時間複雜度為O(N)

第二步:把堆的最大值和堆末尾的值交換,然後減少堆的大小之後,再去調整堆,一直週而復始,時間複雜度為O(N*logN)

第三步:把堆的大小減小成0之後,排序完成。

堆排序額外空間複雜度O(1)

堆排序完整程式碼如下

import java.util.Arrays;
import java.util.PriorityQueue;

public class Code_HeapSort {

  public static void heapSort(int[] arr) {
    if (arr == null || arr.length < 2) {
      return;
    }
    // O(N*logN)
    //  for (int i = 0; i < arr.length; i++) { // O(N)
    //   heapInsert(arr, i); // O(logN)
    //  }
    // O(N)
    for (int i = arr.length - 1; i >= 0; i--) {
      heapify(arr, i, arr.length);
    }
    int heapSize = arr.length;
    swap(arr, 0, --heapSize);
    // O(N*logN)
    while (heapSize > 0) { // O(N)
      heapify(arr, 0, heapSize); // O(logN)
      swap(arr, 0, --heapSize); // O(1)
    }
  }

  // arr[index]剛來的數,往上
  public static void heapInsert(int[] arr, int index) {
    while (arr[index] > arr[(index - 1) / 2]) {
      swap(arr, index, (index - 1) / 2);
      index = (index - 1) / 2;
    }
  }

  // arr[index]位置的數,能否往下移動
  public static void heapify(int[] arr, int index, int heapSize) {
    int left = index * 2 + 1;
    while (left < heapSize) {
      int largest = left + 1 < heapSize && arr[left + 1] > arr[left] ? left + 1 : left;
      largest = arr[largest] > arr[index] ? largest : index;
      if (largest == index) {
        break;
      }
      swap(arr, largest, index);
      index = largest;
      left = index * 2 + 1;
    }
  }

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

與堆排序相關的一個問題

題目描述

已知一個幾乎有序的陣列,幾乎有序是指,如果把陣列排好順序的話,每個元素移動的距離一定不超過k,並且k相對於陣列長度來說是比較小的,請選擇一個合適的排序策略,對這個陣列進行排序。(從小到大)

本題的主要思路就是利用堆排序:

先把 k 個數進堆,然後再加入一個,彈出一個(加入和彈出過程一定不會超過 k 次),最後堆裡面剩下的繼續彈出即可。

時間複雜度是O(N*logK)

完整程式碼如下(含對數程式)

import java.util.Arrays;
import java.util.PriorityQueue;

public class Code_DistanceLessK {
  public static void sortedArrDistanceLessK(int[] arr, int k) {
    k = Math.min(arr.length - 1, k);
    PriorityQueue<Integer> heap = new PriorityQueue<>();
    int i = 0;
    for (; i < k + 1; i++) {
      heap.offer(arr[i]);
    }
    int index = 0;
    for (; i < arr.length; i++) {
      heap.offer(arr[i]);
      arr[index++] = heap.poll();
    }
    while (!heap.isEmpty()) {
      arr[index++] = heap.poll();
    }
  }

  // for test
  public static void comparator(int[] arr, int k) {
    Arrays.sort(arr);
  }

  // for test
  public static int[] randomArrayNoMoveMoreK(int maxSize, int maxValue, int K) {
    int[] arr = new int[(int) ((maxSize + 1) * Math.random())];
    for (int i = 0; i < arr.length; i++) {
      arr[i] = (int) ((maxValue + 1) * Math.random()) - (int) (maxValue * Math.random());
    }
    // 先排個序
    Arrays.sort(arr);
    // 然後開始隨意交換,但是保證每個數距離不超過K
    // swap[i] == true, 表示i位置已經參與過交換
    // swap[i] == false, 表示i位置沒有參與過交換
    boolean[] isSwap = new boolean[arr.length];
    for (int i = 0; i < arr.length; i++) {
      int j = Math.min(i + (int) (Math.random() * (K + 1)), arr.length - 1);
      if (!isSwap[i] && !isSwap[j]) {
        isSwap[i] = true;
        isSwap[j] = true;
        int tmp = arr[i];
        arr[i] = arr[j];
        arr[j] = tmp;
      }
    }
    return arr;
  }

  // for test
  public static int[] copyArray(int[] arr) {
    if (arr == null) {
      return null;
    }
    int[] res = new int[arr.length];
    for (int i = 0; i < arr.length; i++) {
      res[i] = arr[i];
    }
    return res;
  }

  // for test
  public static boolean isEqual(int[] arr1, int[] arr2) {
    if ((arr1 == null && arr2 != null) || (arr1 != null && arr2 == null)) {
      return false;
    }
    if (arr1 == null) {
      return true;
    }
    if (arr1.length != arr2.length) {
      return false;
    }
    for (int i = 0; i < arr1.length; i++) {
      if (arr1[i] != arr2[i]) {
        return false;
      }
    }
    return true;
  }

  // for test
  public static void printArray(int[] arr) {
    if (arr == null) {
      return;
    }
    for (int j : arr) {
      System.out.print(j + " ");
    }
    System.out.println();
  }

  // for test
  public static void main(String[] args) {
    System.out.println("test begin");
    int testTime = 500000;
    int maxSize = 100;
    int maxValue = 100;
    boolean succeed = true;
    for (int i = 0; i < testTime; i++) {
      int k = (int) (Math.random() * maxSize) + 1;
      int[] arr = randomArrayNoMoveMoreK(maxSize, maxValue, k);
      int[] arr1 = copyArray(arr);
      int[] arr2 = copyArray(arr);
      sortedArrDistanceLessK(arr1, k);
      comparator(arr2, k);
      if (!isEqual(arr1, arr2)) {
        succeed = false;
        System.out.println("K : " + k);
        printArray(arr);
        printArray(arr1);
        printArray(arr2);
        break;
      }
    }
    System.out.println(succeed ? "Nice!" : "Fucking fucked!");
  }
}

更多

演算法和資料結構筆記

參考資料

演算法和資料結構體系班-左程雲