二叉樹
二叉樹最多隻有左子樹和右子樹兩個子樹,二叉樹的性質如下:
- 在二叉樹的第層最多有個節點
- 深度爲的二叉樹最多有個節點
- 對於任意一棵二叉樹,如果葉節點數爲,而度數爲2的節點總數爲,則有
- 具有個節點的完全二叉樹的深度必爲
常用的二叉樹有滿二叉樹和完全二叉樹,滿二叉樹指除最後一層無任何子節點外,每一層上的所有結點都有兩個子結點二叉樹;完全二叉樹指當二叉樹的深度爲,除第 層外,其它各層 的結點數都達到最大個數,第 層所有的結點都連續集中在最左邊的二叉樹。
對於一棵使用數組表示的完全二叉樹中的第個節點來說,那麼它的父節點和孩子節點表示爲:
- 左孩子:
- 右孩子:或者
- 父節點:
例如下圖中索引爲1的節點爲2,那麼它的父節點就是索引爲0的節點1,它的左孩子爲索引爲3的節點4,右孩子是索引爲4的節點5。
堆概念
堆是一種經過排序的二叉樹,它是數據結構中可以被看作是一棵樹的數組對象,堆通常滿足如下的兩個性質:
- 堆中某個節點的值總是不大於或不小於其父節點值
- 堆總是一棵完全二叉樹
堆通常有大根堆和小根堆兩種:
- 大根堆:根節點的值大於左右子樹的值,任意子樹也是大根堆
- 小根堆:根節點的值小於左右子樹的值,任意子樹也是小根堆
例如:
堆的構建
假設現在的數據爲,那麼大根堆的構建示意圖如下所示(小根堆的創建類似) :
如上所示,堆的構建過程爲:
- 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;
}
}
時間複雜度: 由於將新節點插入大根堆調整的過程中,只需要考慮根節點到當前節點路徑上的值,那麼值的個數也就是當前節點的層數,所以堆創建的時間複雜度爲:
堆的調整
假設經過上面的步驟已經將創建爲對應的大根堆,它的數組存儲形式爲。如果此時數組中的元素髮生了改變,改變後數組對應的二叉樹已經不滿足大根堆的性質,那麼就需要對現在的二叉樹進行調整,使其重新滿足大根堆的性質。假設數組中的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,它表示數組中區間的元素保持堆的性質
- 將根節點和堆中最後一個元素交換,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)
算法複雜度
- 時間複雜度:
- 空間複雜度:
- 穩定性:不穩定