之前一直以为手撕排序里面快排和归并排序会考的多一些,因此自己写了这两个排序的代码。对于堆排序,是只在数据结构里看过其原理,从未自己实现过。谁曾想拼多多面试的时候让现场手撕堆排序,这个不太熟有点尴尬,然后和面试官商量了换成了快排。后面李颖面试又被问到堆排序,看来堆排序被问到的概率还挺高。那么就自己来实现堆排序吧。
1. 堆排序的原理
堆排序和选择排序很像,都是从待排序序列中选取最大(或最小)的元素加入已排序部分中。不同的是,堆排序是借助二叉堆这个数据结构来选取最大元素,从而在时间效率上优于选择排序。因此在介绍选择排序前先介绍二叉堆这个数据结构。
1.1 完全二叉堆
我认为完全二叉堆最大的特点是它的存储结构是vector,而逻辑结构则是一颗类似于二叉树的二叉堆结构,该结构不像二叉树维护完全的顺序性,而仅维持相对的偏序性。为了实现顺序存储和逻辑结构的对应,二叉堆的结构必须是完全二叉堆,意思是任意节点的平衡因子只能为0或者+1。这样规定的目的是使得对于存在vector中的元素能够快速地通过下标索引到其父节点和子节点。具体怎么索引用不着记住,需要的时候画一棵完全二叉树观察规律就可以了。
那么偏序性又是什么意思呢?这主要是和二叉搜索树对比而言的,二叉搜索树的顺序是严格的,通过前序遍历能够还原出一个排序的序列。而二叉堆只能告诉你这个序列中最大的元素(大根堆是最大,小根堆是最小),并不保证其他元素的顺序性。显然二叉堆实现的功能弱于二叉树,因此其开销也必须小于二叉树才有存在的意义,事实也是如此。
二叉堆的偏序性的定义是所有父节点的值大于其子节点(大根堆),那么堆顶就是该堆最大的元素。那么对于一个乱序序列如何建造这样一个完全二叉堆呢?不断地上滤和下滤是可以达到的,而实际中往往使用的下滤,这是因为下滤的时间复杂度更低。产生这种结果的原因是,对于一颗完全二叉堆,其形状犹如一个三角形,大多数节点集中在底层,少部分节点在高层,对于上滤而言意味着有大量的底层节点需要做很多步的上滤。这比让小部分节点做多步下滤的开销大得多。因此一般二叉堆都采取下滤操作。
1.2 完全二叉堆的建堆示例
考虑有这样一个序列,需要我们建堆:
1 5 3 7 6 8
它存储在一个数组中,但是其逻辑结构可以视为:
1 / \ 5 3 / \ / 7 6 8
对每一个非叶子节点,都有可能不满足必须大于其子树的约束,因此对每个非叶子节点都需要执行下滤操作。从在vector中索引最大的非叶子节点3开始,该节点不满足堆序性,需要进行下滤操作,和其子节点8进行互换操作:
1 / \ 5 8 / \ / 7 6 3
然后是节点5,它也不满足堆序性,需要进行下滤,和其较大的子节点进行交换:
1 / \ 7 8 / \ / 5 6 3
然后是节点1,它也需要进行下滤:
8 / \ 7 1 / \ / 5 6 3
显然1节点下滤一层后,在新的位置上还是不满足堆序性,需要再下滤一次:
8 / \ 7 3 / \ / 5 6 1
最终所有的非叶子节点都执行了完整的下滤操作,使得整棵树满足堆序性,最大的元素被放置到了堆顶。这些交换操作在示例中是树的节点的交换,实际上是元素在vector中的交换,建堆后在vector中的存储如下:
8 7 3 5 6 1
2. 利用二叉堆进行堆排序
如前所述示例,该序列进过建堆操作后,最大元素被放在了堆顶,也就是vector中第0个元素,那么选择最大的元素就是选择第0个元素,并将其放在最末尾,表示其已排序。
1 7 3 5 6 8
其中8表示已排序部分,剩下未排序的部分长度为5,除了交换过去的1,其他的节点是满足堆序性的,那么只要对1进行下滤操作,就可以使得这个长度为5的序列满足堆序性,称为一个新的完全二叉堆。
1 / \ 7 3 / \ 5 6
7 / \ 1 3 / \ 5 6
7 / \ 6 3 / \ 5 1
执行完对1节点的下滤后,最大的元素7又被放到了堆顶,再次取出堆顶元素加入已排序部分:
7 6 3 5 1 8
交换堆顶:
1 6 3 5 7 8
已排序部分又拓展了1,重复建堆和交换的操作,每次选择堆顶加入已排序部分,知道未排序部分长度为1,整个序列就全部为已排序。
3. 代码实现
#include<iostream>
#include<vector>
using namespace std;
//下滤操作
void goDown(vector<int>& nums, int len, int i) {
int leftIndex = i * 2 + 1;
int rightIndex = (i + 1) * 2;
if (leftIndex < len && nums[leftIndex] > nums[i] && (rightIndex >= len || nums[leftIndex] > nums[rightIndex])) {
swap(nums[i], nums[leftIndex]);
goDown(nums,len,leftIndex);
}
else if (rightIndex < len && nums[rightIndex] > nums[i] && (leftIndex >= len || nums[rightIndex] > nums[leftIndex])) {
swap(nums[i], nums[rightIndex]);
goDown(nums, len, rightIndex);
}
}
//建堆操作
void makeHeap(vector<int>& nums) {
int len = nums.size();
//最后一个非叶子节点
int i = len / 2 - 1;
for (; i >= 0; i--) {
goDown(nums,len, i);
}
}
void heapSort(vector<int>& nums) {
//先建堆
makeHeap(nums);
//选择堆顶最大元素加入已排序部分
int len = nums.size();
while (len > 1) {
swap(nums[0], nums[len - 1]);
goDown(nums, --len, 0);
}
}
int main() {
vector<int> nums = { 1,5,3,7,6,8 };
heapSort(nums);
for (auto num : nums) {
cout << num << " ";
}
cout << endl;
system("pause");
}
以上代码经经过简单的测试,如果存在bug,欢迎评论指出。