堆排序
思路:首先把一维数组视为一棵完全二叉树,这样才有接下来的的一系列操作和默认的定义
序列满足如下条件
1、L[i]>=L[2i]&&L[i]>=L[2i+1] (大根堆)或者
2、L[i]<=L[2i]&&L[i]<=L[2i+1] (小根堆)
以下只讨论大根堆的情况,小根堆同理
大根堆:任何一个非叶子节点的值都不小于其左右孩子的值,即父亲大孩子小
根节点即为最大的元素
构造
大根堆的排序中的关键操作就是把无序的序列调整成堆
建立大根堆的思想为:
一次对每个节点的为根的子树进行筛选,看节点值是否大于其左右子节点的值,
若不大于,这将其与左右节点中更大的交换,
且交换后可能会破坏下一级的堆,于是要继续进行比较,直到该值大于其左右节点
王道书上的代码是这样的:
// 建立大根堆, A[0]不存储数据, A[1...len]初始为无序序列,
// 且对于堆的定义默认是只有叶子节点满足,所以是从len/2开始一个一个往上构造堆
void BuildMaxHeap(int A[], int len){
for(int i = len/2; i >= 1; i--)
HeadAdjust(A,i,len);
}
// 把节点插入到堆中
void HeadAdjust(int A[], int k, int len){
A[0] = A[k]; // 暂存A[k]
for(int i = 2*k; i <= len; i *= 2){ // i*2为其左孩子的节点号
if(i<len&&A[i]<A[i+1]){ // 取其左右孩子的最大值
i++;
}
if(A[0]>=A[i]) break; // 如果该值大于其左右节点的最大值,则构成堆,退出循环
else { // 否则,进行交换,把A[k]的值变为其中更大的值,同时A[i]的值与A[k]交换
A[k] = A[i];
k = i; // 同时更新要交换的节点
}
}
A[k] = A[0]; // 最后进行交换,在循环中进行交换也可以,但是那样的话会比较麻烦
}
但这种代码应该更容易看懂一点
void Sift(int A[], int k, int len){
int root = k, child = 2*root; // root表示以当前节点为根,child表示当前节点的左孩子
int tmp = A[k]; // tmp暂时存储根节点的值
while(child <= len){ // child范围不超过len
if(child+1<=len && A[child]<A[child+1]){ // 取左右孩子的最大值
child++;
}
if(tmp < A[child]){ // 如果根节点的值小于其孩子的最大值,则进行交换
A[root] = A[child]; // 交换值
root = child; // 同时下个要比较的堆的根变为当前的孩子的节点号
child = root*2; // 孩子的节点号也同时改变
}else break; // 否则跳出循环
}
A[root] = tmp; // 最后把被调整的节点的值放入最终位置,也就是合理位置
}
排序
要从大根堆中得到一个不下降的序列,只需要把根节点1与最后一个节点n互换即可,然后对以1为根的堆在进行一次调整即可,以此类推,即可得到一个不下降的序列
void HeapSort(int A[], int len){
BuildMaxHeap(A, len);
for(int i = len; i >= 2; i--){ //最后一个即为最小的元素,故只需要到2即可
Swap(A[i], A[1]); //与最后一个元素交换
HeadAdjust(A,1,i-1); //然后对以1为根的堆进行调整
}
}
插入
插入节点放在最底层的最右边,然后依次往上调整即可,实现起来也并不困难
// 插入
// 其实差不多和之前的一样,只是一个是往下调整,而插入是往上调整
void HeapInsert(int A[], int key, int len){
len++;
int root = len/2, child = len; // 与之前的同理,root表示以当前节点为根,child表示当前节点的左孩子
while(root>=1){ // 因为是往上找节点故退出循环的条件为root>=1,而不是之前的往下找节点child<=len
if(key>A[root]){ // 只需要判断该值是否大于其根节点即可,不需要取左右孩子的最大值
A[child] = A[root]; // 如果大于的话进行交换,之后继续往上寻找
child = root; // 之后孩子节点变成了当前的根节点
root = child/2; // child/2表示该孩子的父亲节点
}else break;
}
A[child] = key;
}
删除
删除一个节点的话会使堆中出现一个孔,解决办法是把最后一个节点与其互换位置,然后对以这个位置为根的堆进行调整即可
// 删除
// 和排序差不多了
void HeapDelete(int A[], int pos, int len){
swap(A[pos], A[len]); // 和最后一个节点进行交换
HeapAdjust(A, A[pos], --len); // 然后对以该节点为根的堆进行调整即可,注意len--
}
注意插入和删除的基础是该数组为堆,但是还未排序的状态
本博客中所说的最后一个元素均是对于数组而言,当然也可以对树,但是树单纯的说最后一个不太妥当,应说最底层最右边的元素
算法性能
n个节点,对于完全二叉树而言高度为log(n),即对每个节点的调整的时间复杂度为O(logn),循环次数为n/2
故建立一个大根堆的基本操作次数为O(nlogn)
对于排序为调整O(logn),循环次数为n-1
故排序的操作次数为O(nlogn)
加起来总的时间复杂度为O(nlogn)
空间复杂度,为借助辅助空间,故空间复杂度为O(1)
且堆排序最坏时间复杂度也为O(nlogn),不稳定算法
应用
堆排序适合关键字较多的情况
完整代码
#include <cstdio>
#include <bits/stdc++.h>
using namespace std;
/*
建立大根堆的思想为:
一次对每个节点的为根的子树进行筛选,看节点值是否大于其左右子节点的值,
若不大于,这将其与左右节点中更大的交换,
且交换后可能会破坏下一级的堆,于是要继续进行比较,直到该值大于其左右节点
*/
// 建立大根堆, A[0]不存储数据, A[1...len]初始为无序序列,
// 且对于堆的定义默认是只有叶子节点满足,所以是从len/2开始一个一个往上构造堆
// 把节点插入到堆中
void HeadAdjust(int A[], int k, int len){
A[0] = A[k]; // 暂存A[k]
for(int i = 2*k; i <= len; i *= 2){ // i*2为其左孩子的节点号
if(i<len&&A[i]<A[i+1]){ // 取其左右孩子的最大值
i++;
}
if(A[0]>=A[i]) break; // 如果该值大于其左右节点的最大值,则构成堆,退出循环
else { // 否则,进行交换,把A[k]的值变为其中更大的值,同时A[i]的值与A[k]交换
A[k] = A[i];
k = i; // 同时更新要交换的节点
}
}
A[k] = A[0]; // 最后进行交换,在循环中进行交换也可以
}
void BuildMaxHeap(int A[], int len){
for(int i = len/2; i >= 1; i--)
HeadAdjust(A,i,len);
}
// 这种实现方法应该会更好理解一点
void Sift(int A[], int k, int len){
int root = k, child = 2*root; // root表示以当前节点为根,child表示当前节点的左孩子
int tmp = A[k]; // tmp暂时存储根节点的值
while(child <= len){ // child范围不超过len
if(child+1<=len && A[child]<A[child+1]){ // 取左右孩子的最大值
child++;
}
if(tmp < A[child]){ // 如果根节点的值小于其孩子的最大值,则进行交换
A[root] = A[child]; // 交换值
root = child; // 同时下个要比较的堆的根变为当前的孩子的节点号
child = root*2; // 孩子的节点号也同时改变
}else break; // 否则跳出循环
}
A[root] = tmp; // 最后把被调整的节点的值放入最终位置,也就是合理位置
}
// 插入
// 其实差不多和之前的一样,只是一个是往下调整,而插入是往上调整
void HeapInsert(int A[], int key, int len){
len++;
int root = len/2, child = len; // 与之前的同理,root表示以当前节点为根,child表示当前节点的左孩子
while(root>=1){ // 因为是往上找节点故退出循环的条件为root>=1,而不是之前的往下找节点child<=len
if(key>A[root]){ // 只需要判断该值是否大于其根节点即可,不需要取左右孩子的最大值
A[child] = A[root]; // 如果大于的话进行交换,之后继续往上寻找
child = root; // 之后孩子节点变成了当前的根节点
root = child/2; // child/2表示该孩子的父亲节点
}else break;
}
A[child] = key;
}
// 删除
// 和排序差不多了
void HeapDelete(int A[], int pos, int len){
swap(A[pos], A[len]);
HeadAdjust(A, A[pos], --len);
}
// 堆排序算法
void HeapSort(int A[], int len){
BuildMaxHeap(A, len);
// 构建完堆后进行插入
HeapInsert(A, 66, len);
len++;
// 进行删除
HeapDelete(A, 7, len);
len--;
for(int i = len; i >= 2; i--){
swap(A[i], A[1]);
HeadAdjust(A,1,i-1);
}
}
int main(){
int A[20], len = 10;
for(int i = 1; i <= 10; i++) A[i] = 100-i*3;
for(int i = 1; i <= 10; i++) printf("%d ", A[i]);printf("\n");
HeapSort(A, len);
for(int i = 1; i <= 10; i++){
printf("%d ", A[i]);
}
return 0;
}