樹、二叉樹的簡單介紹
可以用陣列表示一顆二叉樹(陣列下標從0開始)
- 左子節點下標是 2n+1 (n是父節點下標)
- 右子節點下標是 2n+2 (n是父節點下標)
- 父節點下標是 n/2-1 (n是左子節點或者右子節點下標)
堆的概念
- 二叉堆是完全二叉樹或者是近似完全二叉樹
- 二叉堆滿足兩個特性:
- 父節點的鍵值總是大於或等於(小於或等於)任何一個子節點的鍵值
- 每個節點的左子樹和右子樹都是一個二叉堆(都是最大堆或最小堆)
- 任意節點的值都大於其子節點的值————大頂堆,堆的最大值在根節點。
- 任意節點的值都小於其子節點的值————小頂堆,堆的最小值在根節點。
堆排序
步驟
-
第一步:堆化
-
反向調整使得每個子樹都是大頂堆或者小頂堆
-
從n/2-1個元素開始向下修復,將每個節點修復為大(小)頂堆,修復完成後,陣列具有大(小)頂堆的性質
舉個例子:
初始給定陣列為[2,7,26,25,19,17]
它所形成的二叉堆是
堆化以後變成大頂堆
陣列變為[26,25,17,7,19,2]
-
-
第二步:調整
- 不停地把堆頂和未處理陣列的最後一個元素交換,把堆頂也就是較大元素放到陣列末端,每交換一次都要進行調整,維持二叉堆結構。
程式碼實現
法一:採用遞迴方式進行調整操作
public static void main(String[] args) {
int[] arr = {2,5,6,21,6,4,2,6,2};
heapSort(arr);
for (int i : arr) {
System.out.print(i+" ");
}
}
private static void heapSort(int[] arr){
mikeMaxHeap(arr); //1.先進行堆化
for(int x = arr.length-1;x>=0;x--){ //2.再進行調整
swap(arr,0,x); //把堆頂(0號元素)和最後一個元素對調
maxHeapFixDown(arr,0,x); //縮小堆的範圍,對堆頂元素進行向下調整
}
}
private static void mikeMaxHeap(int[] arr) {
int n = arr.length;
for (int i = (n - 1 - 1) / 2; i >= 0; i--) { //從「第一個非葉子節點」開始向前調整,葉子節點沒必要調整
maxHeapFixDown(arr, i, n);
}
}
private static void maxHeapFixDown(int[] arr, int i, int size) {
// 1.找到左右孩子
int left = 2 * i + 1;
int right = 2 * i + 2;
// 2.讓max指向了左右孩子中較大的那個
if (left >= size) //左孩子已經越界,i就是葉子節點
return;
int max = left; //先預設左右孩子中較大的那個是左孩子,簡化程式碼
if (right >= size) //右孩子越界,孩子中較大的就是左孩子
max = left;
else { //左右孩子都沒越界,max指向較大的那個
if (arr[right] > arr[left])
max = right;
}
// 3.交換孩子節點和父親節點
if(arr[i]>=arr[max]) // 如果arr[i]比兩個孩子都要大,不用調整
return;
swap(arr,i,max); //否則,找到兩個孩子中較大的,和i交換
// 4.大孩子那個位置的值發生了變化,i變更為大孩子那個位置,遞迴調整
maxHeapFixDown(arr, max, size);
}
private static void swap(int[] arr, int a, int b) {
int tmp = arr[a];
arr[a] = arr[b];
arr[b] = tmp;
}
法二:採用迴圈方式進行調整操作
只需要修改maxHeapFixDown方法
public static void main(String[] args) {
int[] arr = {2,5,6,21,6,4,2,6,2};
heapSort(arr);
for (int i : arr) {
System.out.print(i+" ");
}
}
private static void heapSort(int[] arr) {
mikeMaxHeap(arr); //1.先進行堆化
for (int x = arr.length - 1; x >= 0; x--) { //2.再進行調整
swap(arr, 0, x); //把堆頂(0號元素)和最後一個元素對調
maxHeapFixDown(arr, 0, x); //縮小堆的範圍,對堆頂元素進行向下調整
}
}
private static void mikeMaxHeap(int[] arr) {
int n = arr.length ;
//從「第一個非葉子節點」開始向前調整,葉子節點沒必要調整
for (int i = (n - 1 - 1) / 2; i >= 0; i--) {
maxHeapFixDown(arr, i, n);
}
}
private static void maxHeapFixDown(int[] arr, int index, int size) {
int left = index * 2 + 1;
while (left < size) {
//如果存在右節點,則largest等於左右節點中較大者的下標
//如果不存在右節點,則largest等於左節點的下標
//一條語句同時滿足兩個條件,選出子節點中的較大者給largest
int largest = left + 1 < size && 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; //進入下一次迴圈(下沉),重新計算index和left
left = index * 2 + 1;
}
}
private static void swap(int[] arr, int a, int b) {
int tmp = arr[a];
arr[a] = arr[b];
arr[b] = tmp;
}
時間複雜度分析
第一步:建立大根堆的過程。
只從陣列第2n-1(n 是陣列長度)個元素開始倒著往前處理。
最後一層節點數大約n/2,因為是葉子節點,所以對其操作就只是看了一眼,運算元為1。
倒數第二層節點數大約n/4,操作是看一眼加上一次交換,運算元為2。
同理,倒數第三層節點數為n/8,運算元為3。
寫出總的複雜度公式:
\(\mathrm{T}\left( \mathrm{n} \right) =\frac{\mathrm{n}}{2}\times 1+\frac{\mathrm{n}}{4}\times 2+\frac{\mathrm{n}}{8}\times 3+\mathrm{……}\)
利用錯位相減法可以求出通項公式
\[\mathrm{T}\left( \mathrm{n} \right) =\frac{\mathrm{n}}{2}\times 1+\frac{\mathrm{n}}{4}\times 2+\frac{\mathrm{n}}{8}\times 3+\frac{\mathrm{n}}{16}\times 4+\mathrm{……}
\\
2\mathrm{T}\left( \mathrm{n} \right) =\frac{\mathrm{n}}{2}\times 2+\frac{\mathrm{n}}{2}\times 2+\frac{\mathrm{n}}{4}\times 3+\frac{\mathrm{n}}{8}\times 4+\mathrm{……}
\\
\mathrm{T}\left( \mathrm{n} \right) =\mathrm{n}+\frac{\mathrm{n}}{2}+\frac{\mathrm{n}}{4}+\frac{\mathrm{n}}{8}+\mathrm{……}
\\
\approx \mathrm{O}\left( \mathrm{n} \right)
\]
所以把陣列變成大根堆的時間複雜度是O(n)
第二步:調整的過程需要交換 n 次,每交換一次都需要進行調整,每次調整複雜度是logn級別,總的複雜度是O(nlogn)
所以堆排序的總時間複雜度是O(nlogn)
空間複雜度分析
沒用申請額外空間,空間複雜度是 O(1)