1. 二叉树

二叉树最多只有左子树和右子树两个子树,二叉树的性质如下:

  • 在二叉树的第堆和堆排序 - 图1层最多有堆和堆排序 - 图2个节点
  • 深度为堆和堆排序 - 图3的二叉树最多有堆和堆排序 - 图4个节点
  • 对于任意一棵二叉树,如果叶节点数为堆和堆排序 - 图5,而度数为2的节点总数为堆和堆排序 - 图6,则有堆和堆排序 - 图7
  • 具有堆和堆排序 - 图8个节点的完全二叉树的深度必为堆和堆排序 - 图9#card=math&code=%5Clog2%28n%2B1%29&height=18&width=75)

常用的二叉树有满二叉树完全二叉树,满二叉树指除最后一层无任何子节点外,每一层上的所有结点都有两个子结点二叉树;完全二叉树指当二叉树的深度为$h$,除第 $h$ 层外,其它各层 $(1 \sim h-1)$ 的结点数都达到最大个数,第 $h$ 层所有的结点都连续集中在最左边的二叉树。

对于一棵使用数组
堆和堆排序 - 图10堆和堆排序 - 图11

  • 左孩子:堆和堆排序 - 图12
  • 右孩子:堆和堆排序 - 图13或者堆和堆排序 - 图14
  • 父节点:堆和堆排序 - 图15

例如下图中索引为1的节点为2,那么它的父节点就是索引为0的节点1,它的左孩子为索引为3的节点4,右孩子是索引为4的节点5。

2. 堆概念

堆是一种经过排序的二叉树,它是数据结构中可以被看作是一棵树的数组对象,堆通常满足如下的两个性质:

  • 堆中某个节点的值总是不大于或不小于其父节点值
  • 堆总是一棵完全二叉树

堆通常有大根堆和小根堆两种:

  • 大根堆:根节点的值大于左右子树的值,任意子树也是大根堆
  • 小根堆:根节点的值小于左右子树的值,任意子树也是小根堆

例如:


3. 堆的构建

假设现在的数据为
堆和堆排序 - 图16

如上所示,对的构建过程为:

  • 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,动作停止

代码实现:

  1. import java.util.Arrays;
  2. public class HeapTest {
  3. public static void main(String[] args) {
  4. int[] nums = {2,1,3,6,0,4};
  5. HeapSort(nums);
  6. System.out.println(Arrays.toString(nums)); // [6, 3, 4, 1, 0, 2]
  7. }
  8. public static void HeapSort(int[] nums) {
  9. // 如果数组为空或者只有一个元素,直接返回
  10. if (nums == null || nums.length < 2){
  11. return;
  12. }
  13. // 否则依次将数组中的节点插入到大根堆中
  14. for (int i = 0; i < nums.length; i++) {
  15. HeapInsert(nums, i);
  16. }
  17. }
  18. public static void HeapInsert(int[] nums, int i) {
  19. // 如果当前节点值大于它的父节点值,将其交换
  20. // 知道while中的条件不成立,即当前二叉树已是大根堆
  21. // 创建小根堆:while (nums[i] < nums[(i- 1) / 2]){...}
  22. while (nums[i] > nums[(i- 1) / 2]){
  23. swap(nums, i, (i - 1) / 2);
  24. // 更新需考虑的索引地址
  25. i = (i - 1) / 2;
  26. }
  27. }
  28. public static void swap(int[] nums, int i, int j){
  29. int temp = nums[i];
  30. nums[i] = nums[j];
  31. nums[j] = temp;
  32. }
  33. }

时间复杂度: 由于将新节点插入大根堆调整的过程中,只需要考虑根节点到当前节点路径上的值,那么值的个数也就是当前节点的层数,所以堆创建的时间复杂度为:

堆和堆排序 - 图17%20%3D%20%5Csum%7Bi%20%3D%201%7D%5E%7BN%7D%20%5Clog%20i%20%3D%20O(N)%0A#card=math&code=O%20%3D%20%5Clog1%20%2B%20%5Clog%202%20%2B%20…%20%2B%20%5Clog%28N%29%20%3D%20%5Csum%7Bi%20%3D%201%7D%5E%7BN%7D%20%5Clog%20i%20%3D%20O%28N%29%0A&height=47&width=325)


4. 堆的调整

假设经过上面的步骤已经将
堆和堆排序 - 图18堆和堆排序 - 图19

实现代码:

  1. import java.util.Arrays;
  2. public class HeapTest {
  3. public static void main(String[] args) {
  4. int[] array = {6, 3, 4, 1, 0, 2};
  5. array[0] = 1;
  6. System.out.println(array);
  7. Heapify(array, 0, array.length);
  8. System.out.println(Arrays.toString(array)); // [4, 3, 2, 1, 0, 1]
  9. }
  10. // size表示堆的数值范围
  11. public static void Heapify(int[] array, int i, int size){
  12. // 看左右孩子
  13. int left = 2 * i + 1;
  14. while (left < size){
  15. // 右孩子为left + 1
  16. // 寻找左右孩子最大的哪那一个,将其索引赋给largest
  17. int largest = left + 1 < size && array[left + 1] > array[left] ? left+ 1 : left;
  18. // 判断largest指向的节点和当前节点的关系
  19. // 如果当前节点小于左右孩子中最大的节点,则更新largest
  20. largest = array[largest] > array[i] ? largest : i;
  21. // 如果当前已是大根堆,则跳出
  22. if (largest == i){
  23. break;
  24. }
  25. // 否则执行交换,继续往下判断
  26. swap(array, largest, i);
  27. i = largest;
  28. left = 2 * i + 1;
  29. }
  30. }
  31. public static void swap(int[] array, int i, int j){
  32. int temp = array[i];
  33. array[i] = array[j];
  34. array[j] = temp;
  35. }
  36. }

5. 堆排序

经过前面的讲述,我们知道了如何根据给定的数组创建堆,并知道在数组中元素改变破坏已有的堆时如何进行调整,使其重新成为一个堆。那么如何根据堆的构建和堆的调整来实现堆排序呢?假设现在构建的是大根堆,那么堆的根节点值就是当前数组中的最大值,那么不断的弹出根节点然后再调整保持堆的性质,直到最后堆为空,那么依次弹出的节点值就是有序的,堆排序自然就完成了。

具体操作:在弹出根节点并调整堆的过程中使用一个变量size,它维护了数组中
堆和堆排序 - 图20

  • 将根节点和堆中最后一个元素交换,size减一
  • 调整剩下的元素使其仍然为一个大根堆
  • 不断重复上述过程,直到数组为空

代码实现:

  1. import java.util.Arrays;
  2. public class HeapTest {
  3. public static void main(String[] args) {
  4. int[] array = {2,1,3,6,0,4};
  5. HeapSort(array);
  6. System.out.println(Arrays.toString(array)); // [0, 1, 2, 3, 4, 6]
  7. }
  8. public static void HeapSort(int[] array) {
  9. if (array == null || array.length < 2){
  10. return;
  11. }
  12. for (int i = 0; i < array.length; i++) {
  13. HeapInsert(array, i);
  14. }
  15. // size维护数组中满足堆性质的区域
  16. int heap_size = array.length;
  17. // 交换根节点和数组的最后一个元素,更新size
  18. swap(array, 0, --heap_size);
  19. while (heap_size > 0){
  20. // 调整剩下的元素使其构成堆
  21. Heapify(array, 0, heap_size);
  22. swap(array, 0, --heap_size);
  23. }
  24. }
  25. public static void HeapInsert(int[] array, int i) {
  26. while (array[i] > array[(i- 1) / 2]){
  27. swap(array, i, (i - 1) / 2);
  28. i = (i - 1) / 2;
  29. }
  30. }
  31. public static void Heapify(int[] array, int i, int size){
  32. // 看左右孩子
  33. int left = 2 * i + 1;
  34. while (left < size){
  35. // 右孩子为left + 1
  36. int largest = left + 1 < size && array[left + 1] > array[left] ? left+ 1 : left;
  37. // 更新largest
  38. largest = array[largest] > array[i] ? largest : i;
  39. // 如果当前已是堆则跳出
  40. if (largest == i){
  41. break;
  42. }
  43. swap(array, largest, i);
  44. i = largest;
  45. left = 2 * i + 1;
  46. }
  47. }
  48. public static void swap(int[] array, int i, int j){
  49. int temp = array[i];
  50. array[i] = array[j];
  51. array[j] = temp;
  52. }
  53. }

Python代码实现:

  1. class HeapSort:
  2. def __init__(self, array):
  3. super().__init__()
  4. self.array = array
  5. def HeapSort(self):
  6. if self.array is None or len(self.array) < 2:
  7. return
  8. for i in range(len(self.array)):
  9. self.HeapInsert(i)
  10. heap_size = len(self.array)
  11. heap_size -= 1
  12. self.swap(0, heap_size)
  13. while heap_size > 0:
  14. self.Heapify(0, heap_size)
  15. heap_size -= 1
  16. self.swap(0, heap_size)
  17. def HeapInsert(self, index):
  18. while self.array[index] > self.array[(index - 1) // 2] and (index - 1) // 2 >= 0:
  19. self.swap(index, (index - 1) // 2)
  20. index = (index - 1) // 2
  21. def Heapify(self, i, heapsize):
  22. left = 2 * i + 1
  23. right = 2 * i + 2
  24. while left < heapsize:
  25. if right < heapsize and self.array[right] > self.array[left]:
  26. largest = right
  27. else:
  28. largest = left
  29. largest = largest if self.array[largest] > self.array[i] else i
  30. if largest == i:
  31. break
  32. self.swap(largest, i)
  33. i = largest
  34. left = 2 * i + 1
  35. def swap(self, i, j):
  36. self.array[i], self.array[j] = self.array[j], self.array[i]
  37. if __name__ == "__main__":
  38. array = [2,1,3,6,0,4]
  39. # array = [1, 3, 4, 1, 0, 2]
  40. heap = HeapSort(array)
  41. heap.HeapSort()
  42. # heap.Heapify(0, len(array))
  43. print (heap.array)

6. 算法复杂度

  • 时间复杂度:堆和堆排序 - 图21#card=math&code=O%28N%20%2A%20%5Clog%20N%29&height=18&width=85)
  • 空间复杂度:堆和堆排序 - 图22#card=math&code=O%281%29&height=18&width=30)
  • 稳定性:不稳定