堆排序
预备知识
满二叉树: 除最后一层结点均无任何子节点外,每一层的所有结点都有两个子结点的树。也就是每一层节点个数都是最大值的二叉树(每层的节点个数=),也可以说除了叶子结点之外的每一个结点都有两个孩子,每一层(当然包含最后一层)都被完全填充。
完全二叉树: 如果二叉树中除去最后一层节点为满二叉树,且最后一层的结点依次从左到右分布,则此二叉树被称为完全二叉树。
所以一个满二叉树一定是完全二叉树,完全二叉树不一定是满二叉树。
堆: 堆是一个完全二叉树,可以分为大根堆(最大堆)和小根堆(最小堆)
- 大根堆:对于任意一个子树,父节点的值大于等于其左右孩子的值
- 小根堆:对于任意一个子树,父节点的值小于等于其左右孩子的值
一些性质: 假如用一个数组存储一个堆,那么节点 的左右孩子的下标分别为 和 ,其父节点的下标为 。如果设叶子节点的高度为1,那么一个长度为 的数字可以构建的完全二叉树的高度为 。
数组构建堆
根据上面的预备知识,一个堆可以分为大根堆和小根堆,如何使用一个数组构建一个堆呢(大根堆为例)。如给定一个数组 arr = [2,1,3,6,0,4],构建的目标大根堆为 arr = [6,3,4,1,0,2]。下面描述一下构建步骤:
- 首先把下标为2作为二叉树的根节点 ,heap = [2]
- 添加1作为2的左子树,此时符合大根堆的要求,heap = [2,1]
- 添加3作为2的右子树,heap = [2,1,3],此时不符合,因为根节点2小于右孩子3,那么交换根节点和右孩子的数值,变为 heap = [3,1,2]
- 添加6作为1的左孩子,heap = [ 3,1,2,6],1的左孩子6比较大,所以交换1和6的位置,heap = [3,6,2,1],此时2的左孩子6比较大,交换二者的位置,heap = [6,3,2,1]
- 添加0作为数字3的右孩子,heap = [6,3,2,1,0],符合要求
- 添加4作为数字2的左孩子,heap = [6,3,2,1,0,4],2的左孩子4比较大,交换位置,heap = [6,3,4,1,0,2]
- 构建完成
下图就是上面的构建过程
代码实现
#include <iostream>
#include <vector>
using namespace std;
/*
* 交换两个数字
*/
void mySwap(vector<int>& vec, int index1, int index2) {
int temp = vec[index1];
vec[index1] = vec[index2];
vec[index2] = temp;
}
/*
* 在已有的大根堆中插入一个数字
*/
void heapInsert(vector<int>& vec, int index) {
// 如果孩子节点大于父节点,那么就交换二者的位置
while (vec[index] > vec[(index - 1) / 2]) {
mySwap(vec, index, (index - 1) / 2);
index = (index - 1) / 2; // 更新孩子节点的位置
}
}
/*
* 构建大根堆
*/
void maxHeap(vector<int>& vec) {
// 遍历每一个元素,插入到已有的大根堆中
for (int i = 0; i < vec.size(); i++) {
heapInsert(vec, i);
}
}
int main() {
vector<int> arr = {2,1,3,6,0,4};
maxHeap(arr);
for (int i = 0; i < arr.size(); i++) {
cout << arr[i] << " ";
}
cout << endl;
return 0;
}
构建大根堆的时间复杂度
假设有 个节点,设叶节点的高度为1,那么构建的完全二叉树的高度 ,那么叶节点的父节点最多需要调整 次(倒数第一层的父节点),最后一层总共包括 个节点,在最坏的情况下,每个节点都需要调整,需要调整的次数就为 ;倒数第二层的父节点最多需要调整 次,包含的节点个数为 ,总共需要调整 次,以此类推,根节点需要调整 次,总共需要调整 次。那么整个的时间复杂度为
两侧乘以2,并相减
带入到 中的 ,所以构建大根度需要的时间复杂度为 。
调整大根堆
假设给定的一个数组已经是大根堆,arr = [6, 3, 4, 1, 0, 2],如果某个下标的数字**向下变小**,那么原来的大根堆就有可能被破坏,如下标为0的数字变为0,arr = [0, 3, 4, 1, 0, 2],那么原来的大根堆就被破坏了。那么如何再把它调整为大根堆,如果需要调整的数的下标index=0,堆的长度heapSize=6,采用如下步骤:
- 首先计算出来下标0的左孩子和右孩子的下标,并判断他们是否越界,如果都不存在,表示是叶子节点,那么直接结束整个流程;如果任意一个存在,那么就进行下一步的操作,此例中,index=0的left=1,right=2
- 选择arr[index]、arr[left]、arr[right]三个数中最大的数,如果vec[index]是最大的那么直接结束整个流程,此例中,vec[right]比较大,所以交换过后 arr=[4, 3, 0, 1, 0, 2],把right的值更新给index
- 此时index=2,那么left=5,right=6,right越界,舍弃right;在arr[index],arr[left]两个数中选择最大的数,此例中,arr[left]比较大,所以交换index中位置 arr = [4, 3, 2, 1, 0, 0],把left的值更新给index
- 此时index=5,其为叶子节点,arr = [4, 3, 2, 1, 0, 0]所以结束流程
以上就是调整的过程,其调整的核心思想就是,判断当前的节点与其左右孩子比较,选择较大的,然后下沉,直到比较到叶子节点,如果中间的任何一个过程,index的值最大,也是结束该过程。通过上述的描述,如果有 个节点,那么可以构建的完全二叉树高度为 ,那么如果调整的是根节点,最坏的情况需要调整 次,所以这个过程的时间复杂度为 。
代码实现
#include <iostream>
#include <vector>
using namespace std;
/*
* 交换两个数
*/
void mySwap(vector<int>& vec, int index1, int index2) {
int temp = vec[index1];
vec[index1] = vec[index2];
vec[index2] = temp;
}
/*
* 根据给定的index和heapSize调整,使其成为大根堆
*/
void heapify(vector<int>& vec, int index, int heapSize) {
int left = index * 2 + 1; // 计算左孩子的下标
while (left < heapSize) { // 如果左孩子下标超过heapSize,那么就直接结束
int largest = left; // 最大数的下标初始化为左孩子的下标
// 当右孩子没有越界,切右孩子大于左孩子的时候,更新largest
if ((left + 1) < heapSize && vec[left + 1] > vec[left]) {
largest = left + 1;
}
// 把largest和index对应的数字比较,即比较三者的值
largest = (vec[largest] > vec[index] ? largest : index);
// 如果最大数的下标还是index,那么表示现在的堆已经是最大堆,直接结束
if (largest == index) {
break;
}
// 交换两个数
mySwap(vec, largest, index);
index = largest; // index 下沉,即更新index的值
left = index * 2 + 1;
}
}
int main() {
vector<int> arr = {0,3,4,1,0,2};
heapify(arr, 0, arr.size());
for (int i = 0; i < arr.size(); i++) {
cout << arr[i] << " ";
}
cout << endl;
return 0;
}
堆排序
有了上面的知识以后,怎么实现堆排序的呢?
算法思想如下:
- 把给定的序列调整为大根堆,定义heapSize的长度(整个数组的长度)
- 把堆heapSize-1对于元素与根节点位置的元素交换,此时heapSize-1对于的是最大的数
- heapSize的长度减小1,调整现有长度的堆为最大堆
- 重复1~3过程,直到heapSize的长度为0
通过下图的动画了解一下堆排序的整体过程:
只要熟练了上文的内容,下面堆排序的代码就很好写了
代码实现
#include <iostream>
#include <vector>
using namespace std;
void mySwap(vector<int>& vec, int index1, int index2) {
int temp = vec[index1];
vec[index1] = vec[index2];
vec[index2] = temp;
}
/*
* 在已有的大根堆中插入一个数字
*/
void heapInsert(vector<int>& vec, int index) {
// 如果孩子节点大于父节点,那么就交换二者的位置
while (vec[index] > vec[(index - 1) / 2]) {
mySwap(vec, index, (index - 1) / 2);
index = (index - 1) / 2; // 更新孩子节点的位置
}
}
/*
* 根据给定的index和heapSize调整,使其成为大根堆
*/
void heapify(vector<int>& vec, int index, int heapSize) {
int left = index * 2 + 1; // 计算左孩子的下标
while (left < heapSize) { // 如果左孩子下标超过heapSize,那么就直接结束
int largest = left; // 最大数的下标初始化为左孩子的下标
// 当右孩子没有越界,切右孩子大于左孩子的时候,更新largest
if ((left + 1) < heapSize && vec[left + 1] > vec[left]) {
largest = left + 1;
}
// 把largest和index对应的数字比较,即比较三者的值
largest = (vec[largest] > vec[index] ? largest : index);
// 如果最大数的下标还是index,那么表示现在的堆已经是最大堆,直接结束
if (largest == index) {
break;
}
// 交换两个数
mySwap(vec, largest, index);
index = largest; // index 下沉,即更新index的值
left = index * 2 + 1;
}
}
/*
* 堆排序函数
*/
void heapSort(vector<int>& vec) {
// 给定的序列长度少于2个直接返回
if (vec.size() < 2) {
return;
}
// 把数组调整为大根堆
for (int i = 0; i < vec.size(); i++){
heapInsert(vec, i);
}
int heapSize = vec.size(); // 定义堆的长度
// 堆的长度为0时,排序完毕
while (heapSize > 0) {
mySwap(vec, 0, heapSize - 1); // 交换根节点和堆尾
heapSize--; // 堆的长度减少一
heapify(vec, 0, heapSize); // 调整现有的数组为大根堆
}
}
int main() {
vector<int> arr = { 6, 4, 8, 9, 2, 3, 1};
heapSort(arr);
for (int i = 0; i < arr.size(); i++) {
cout << arr[i] << " ";
}
cout << endl;
return 0;
}
根据上述的代码,产生大根堆的时间复杂度为 ,在堆排序的过程中,需要对数组遍历 次,每次都需要调整,最坏的情况需要调整 次,所以整个的时间复杂度为 ,取最高大项,所以整体堆排序的时间复杂度为 。在堆排序的过程中,只用了常数个的变量,所以空间复杂度为 。堆排序是一种不稳定的排序算法,如 arr=[8,6,6],是一个大根堆,根节点index=0需要和right=2交换,即arr=[6,6,8],两个6的下标顺序发生了改变,所以是不稳定的排序算法。
总结
- 稳定性:不稳定
- 时间复杂度:
- 空间复杂度:
欢迎大家关注我的个人公众号,同样的也是和该博客账号一样,专注分享技术问题,我们一起学习进步
来源:CSDN
作者:黑暗主宰
链接:https://blog.csdn.net/EngineerHe/article/details/104152119