與堆和堆排序相關的問題
作者:Grey
原文地址:
堆結構說明
堆結構就是用陣列實現的完全二叉樹結構,什麼是完全二叉樹?可以參考如下兩篇部落格:
完全二叉樹中如果每棵子樹的最大值都在頂部就是大根堆;完全二叉樹中如果每棵子樹的最小值都在頂部就是小根堆。
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 最大值出堆
// 注意:出堆後,也要保持大根堆的狀態
}
}
由上述資料結構定義可知,核心方法就是 push
和 pop
,在每次操作後,要動態調整堆結構,使之保持大根堆的結構。
要完成這兩個操作,就需要利用到堆的兩個基本操作:
一個是 HeapInsert,一個是 Heapify。
Heapify 操作
Heapify 就是堆化的過程,以小根堆為例,示例說明
假設原始陣列為:{3,2,1,4,5}
,初始狀態如下
首先從頭結點 3 開始,先找到 3 的左右孩子中較小的一個進行交換,現在較小的是右孩子 1,交換後是如下情況
互換後,3 號結點已經沒有左右孩子了,停止操作。
然後按順序繼續處理 2 結點,2 結點已經比左右孩子都小了,無需進行交換。
接下來是 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];
}
}
}
HeapInsert 操作
整個過程如下,以小根堆為例,從陣列最後一個元素 X 開始,一直找其父節點 A,如果X 比 A 小,X 就和 A 交換,然後來到父節點 A,繼續往上找 A 的父節點 B,如果 A 比 B 小,則把 A 和 B 交換……一直找到某個結點的頭結點不比這個結點大,這個節點就可以停止移動了。以一個示例說明
假設原始陣列為:{3,2,1,4,5}
,初始狀態如下
從最後一個元素 5 開始,5 的父節點是 2,正好滿足,無需繼續往上找父節點,然後繼續找倒數第二個位置 4 的父節點,也比父節點 2 要大,所以 4 節點也不需要動。
接下來是 1 結點,其父結點是 3 結點,所以此時要把 3 和 1 交換,變成如下樣子
然後是 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!");
}
}