[230523] 堆排序

[230523] 堆排序(Heap Sort)

Heap Sort

1. 二叉堆的性质

  • 大顶堆:每个节点的值都大于或等于其左右孩子节点的值(不保证大顶堆对应的 array 为降序序列!!!不保证大顶堆对应的 array 为降序序列!!!不保证大顶堆对应的 array 为降序序列!!!我们只是利用大顶堆来构造出升序序列。可以保证的唯一性质是:大顶堆的堆顶元素为大顶堆元素中的最大值。)
  • 小顶堆:每个节点的值都小于或等于其左右孩子节点的值(同理)

2. Heapify 过程

Heapify:把一个给定的 array 转换成大顶堆/小顶堆的 array 序列(递归过程)

下例把一个给定的 array 逐步 heapify 为一个大顶堆:

start:array = {1, 3, 5, 4, 6, 13, 10, 9, 8, 15, 17}
对应二叉堆:
			   1
            /      \
          3          5
        /    \      /  \
      4       6   13    10
     / \     / \
    9   8  15   17

heapify(递归过程)的目的是使原 array 中每一个非叶子节点都下沉至满足大顶堆/小顶堆限制条件的最底层(说大白话就是,如果在一次 heapify 中破坏了大顶堆/小顶堆的性质,则要继续调用 heapify 维护大顶堆/小顶堆的性质)

2.1 第 1 轮 heapify(初始化大顶堆/小顶堆)

第一轮 heapify(即大多数博文中所说的初始化堆):

从最后一个非叶子节点(6)开始 heapify:

  • 如何找到最后一个非叶子节点的下标?
    • 节点总数为 n → 非叶子节点总数为 n / 2 - 1 → 最后一个非叶子节点下标为 n / 2 - 1
  • 如何 heapify?
    • 目标为大顶堆:比较当前节点与两个孩子的值,选择最大值放在当前节点的位置上(swap操作)
    • 目标为小顶堆:比较当前节点与两个孩子的值,选择最小值放在当前节点的位置上(swap操作)
6 heapified:array = {1, 3, 5, 4, 17, 13, 10, 9, 8, 15, 6}
二叉堆:
			    1
             /     \
          3          5
        /    \      /  \
      4      17   13    10
     / \     / \
    9   8  15   6

heapify 4:

4 heapified:array = {1, 3, 5, 9, 17, 13, 10, 4, 8, 15, 6}
二叉堆:
			    1
             /     \
          3          5
        /    \      /  \
      9      17   13    10
     / \     / \
    4   8  15   6

heapify 5:

5 heapified:array = {1, 3, 13, 9, 17, 5, 10, 4, 8, 15, 6}
二叉堆:
			    1
             /     \
          3          13
        /    \      /  \
      9      17    5    10
     / \     / \
    4   8  15   6

heapify 3:

  • 首先 swap 3 和 17
  • 然后 swap 3 和 15
3 heapified:array = {1, 17, 13, 9, 15, 5, 10, 4, 8, 3, 6}
二叉堆:
			    1
             /     \
          17         13
        /    \      /  \
      9      15    5    10
     / \     / \
    4   8   3   6

heapify 1:

  • 首先 swap 1 和 17
  • 然后 swap 1 和 15
  • 最后 swap 1 和 6
1 heapified:array = {17, 15, 13, 9, 6, 5, 10, 4, 8, 3, 1}
二叉堆:
			   17
             /     \
          15         13
        /    \      /  \
      9       6    5    10
     / \     / \
    4   8   3   1

一轮 heapify 结束后,可以让堆顶元素 17 就位(swap(堆顶元素 array[0], array[n - 1]) ),然后 array 规模递减(n–),开始下一轮的 heapify(注意:由于在第一轮 heapify 中我们已经构造好了一个大顶堆,所以在 swap 以后只有堆顶元素是非法的,所以在 heapSort 中只需要对堆顶元素进行 heapify)

所以,可以在此处领会到:为什么是使用大顶堆找到升序序列,使用小顶堆找到降序序列。

2.2 第 2、3、…、n-1 轮 heapify
swap(array[0], array[n - 1]):array = {1, 15, 13, 9, 6, 5, 10, 4, 8, 3, 17}
用如下二叉堆开始下一轮heapify:
			   1
             /    \
          15        13
        /    \     /  \
      9       6   5    10
     / \     / 
    4   8   3

第二轮 heapify 结束,15 就位:

array = {3, 9, 13, 8, 6, 5, 10, 4, 1, 15, 17}
			    3
             /    \
           9       13
        /    \     /  \
      8       6   5    10
     / \     
    4   1

第三轮 heapify 结束,13 就位:

array = {1, 9, 10, 8, 6, 5, 3, 4, 13, 15, 17}
			   1
             /    \
           9       10
         /   \     /  \
        8     6   5    3
      / 
     4

第四轮 heapify 结束,10就位:

array = {4, 9, 5, 8, 6, 1, 3, 10, 13, 15, 17}
			   4
             /    \
           9        5
         /   \     /  \
        8     6   1    3

第五轮 heapify 结束,9 就位:

array = {3, 8, 5, 4, 6, 1, 9, 10, 13, 15, 17}
			   3
             /    \
           8        5
         /   \     /
        4     6   1

第六轮 heapify 结束,8 就位:

array = {1, 6, 5, 4, 3, 8, 9, 10, 13, 15, 17}
			   1
             /    \
           6        5
         /   \
        4     3

第七轮 heapify 结束,6 就位:

array = {3, 4, 5, 1, 6, 8, 9, 10, 13, 15, 17}
			   3
             /    \
           4        5
         /
        1

第八轮 heapify 结束,5 就位:

array = {1, 4, 3, 5, 6, 8, 9, 10, 13, 15, 17}
			   1
             /    \
           4        3

第九轮 heapify 结束,4 就位:

array = {3, 1, 4, 5, 6, 8, 9, 10, 13, 15, 17}
			   3
             /
           1 

第十轮 heapify 结束,3 就位:

array = {1, 3, 4, 5, 6, 8, 9, 10, 13, 15, 17}
			   1

第十轮 heapify 结束后,二叉堆规模为 1,原 array 元素全部就序。

3. 代码实现

#include <iostream>
using namespace std;
 
// 函数作用:对节点i进行heapify
// N为heap的规模
void heapify(int arr[], int N, int i)
{
    
    int largest = i;	//记录非叶子节点及其左右孩子最大值的下标,初始化为非叶子节点的下标
    int l = 2 * i + 1;  //左孩子下标
    int r = 2 * i + 2;  //右孩子下标
 
    //确定largest,要先检查左右孩子的下标是否越界
    if (l < N && arr[l] > arr[largest])
        largest = l;
    if (r < N && arr[r] > arr[largest])
        largest = r;
 
    //如果非叶子节点值不是最大值,则要进行swap
    //并且swap后会使二叉堆中下标为largest的元素非法,所以要继续进行heapify
    if (largest != i) {
        swap(arr[i], arr[largest]);
        //对arr[largest]进行heapify
        heapify(arr, N, largest);
    }
}
 
//堆排序主算法
void heapSort(int arr[], int N)
{
    // 进行第一轮heapify(对所有非叶子节点进行heapify),此时构造好了一个大顶堆
    for (int i = N / 2 - 1; i >= 0; i--)
        heapify(arr, N, i);
 
	//swap逐步构建升序序列,然后减小array规模,此时只需要对array[0]进行heapify
    //i既指示就序位置也指示下一轮heapify的规模
    for (int i = N - 1; i > 0; i--) {
        //swap以构造升序序列,同时减小array规模
        swap(arr[0], arr[i]);
        //对array[0]进行heapify
        heapify(arr, i, 0);
    }
}
 
//工具函数:打印数组
void printArray(int arr[], int N)
{
    for (int i = 0; i < N; ++i)
        cout << arr[i] << " ";
    cout << "\n";
}
 
//驱动代码
int main()
{
    int arr[] = { 12, 11, 13, 5, 6, 7 };
    int N = sizeof(arr) / sizeof(arr[0]);
 
    heapSort(arr, N); //堆排序
 
    cout << "Sorted array is: ";
    printArray(arr, N);
}

4. 性能分析

  • 时间复杂度:O(N log N)
  • 空间复杂度:O(1)
  • 稳定性:不稳定
  • 就地算法:是就地算法

5. 适用场景

6. 堆排序与归并排序、选择排序

  • 哪种排序算法更好?堆排序还是合并排序?
    • 答案在于它们的时间复杂性和空间需求的比较。Merge排序比Heap排序稍快。但另一方面,合并排序需要额外的内存。根据需要,应该选择使用哪一个。
  • 为什么堆排序比选择排序好?
    • 堆排序类似于选择排序,但有更好的方法来获得最大元素。它利用堆数据结构在恒定时间内获得最大元素。

7. 小顶堆代码

补充:利用小顶堆构造降序序列

#include <iostream>
using namespace std;

void swap(int& a, int& b) {
    int tmp = a;
    a = b;
    b = tmp;
}

void heapify(int arr[], int N, int i) {
    int minIdx = i;
    int left = 2 * i + 1;
    int right = 2 * i + 2;
    if(left < N && arr[minIdx] > arr[left]) {
        minIdx = left;
    }
    if(right < N && arr[minIdx] > arr[right]) {
        minIdx = right;
    }
    if(minIdx != i) {
        swap(arr[minIdx], arr[i]);
        heapify(arr, N, minIdx);
    }
}

void heapSort(int arr[], int N) {
    //堆的初始化
    for(int i = N / 2 - 1; i >= 0; --i) {
        heapify(arr, N, i);
    }
    //利用小顶堆构造降序序列
    for(int i = N - 1; i > 0; --i) {
        swap(arr[0], arr[i]);
        heapify(arr, i, 0);
    }
}

int main() {
	int arr[10] = {4, 2, 3, 5, 6, 9, 7, 6, 8, 0};
	heapSort(arr, 10);
	for(int i = 0; i < 10; ++i) {
	    cout << arr[i] << " ";
	}
	return 0;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值