基本排序算法总结与对比 之七 ——堆排序
1、堆排序 递归版本
首先放递归的版本,因为递归版本更容易理解过程。
堆排序实际是把数列看成一颗完全二叉树,而不是真的去用 指针结构体 构造一颗二叉树。数列在堆中从上到下,从左至右依次排成一棵树。
下面是递归版本的代码
//用来调整节点,保证节点大于左右孩子
template<typename T>
void adjustNode(T arr[], int node, int len) { //node为节点的下标
int lchNode = 1 + (node << 1); //node节点 左孩子 的下标
int rchNode = 2 + (node << 1); //node节点 右孩子 的下标
if (rchNode < len) { //说明该节点一定存在右孩子
if (arr[node] < arr[lchNode] || arr[node] < arr[rchNode]) //如果左右孩子有一个大于节点
{
if (arr[lchNode] > arr[rchNode]) { //如果左孩子大于右孩子
std::swap(arr[node], arr[lchNode]); //交换左孩子 与 节点
if (lchNode <= (len >> 1) - 1) //如果这个左孩子还有孩子
adjustNode(arr, lchNode, len); //调整这个左孩子作为节点的分支
}
else { //如果右孩子大于等于左孩子,必须留一个等于的口子,要不然左右相等时无作为
std::swap(arr[node], arr[rchNode]); //交换右孩子 与 节点
if (rchNode <= (len >> 1) - 1) //如果这个右孩子还有孩子
adjustNode(arr, rchNode, len); //调整这个右孩子作为节点的分支
}
}
}
else { //如果该节点只有左孩子
if (arr[node] < arr[lchNode]) { //如果左孩子大于右孩子
std::swap(arr[node], arr[lchNode]);
if(lchNode <= (len >> 1) - 1)
adjustNode(arr, lchNode, len);
}
}
}
//调整树中所有的节点
template<typename T>
void adjustHeep(T arr[], int len) {
for (int node = (len >> 1) - 1; node >= 0; node--) { // (len >> 1) - 1 为最后一个 有孩子的节点 的下标
adjustNode(arr, node, len); //从最后一个节点,一直调整到堆顶
}
}
template<typename T>
void heapSort(T arr[], int lo, int hi) {
adjustHeep(arr + lo, hi - lo); //首先从尾到头调整一遍
while (hi - lo > 2) {
std::swap(arr[lo], arr[hi - 1]); //每次将堆顶元素与最后元素交换
adjustNode(arr + lo, 0, --hi - lo); //重新调整堆顶节点
}
std::swap(arr[lo], arr[hi - 1]); //最后一次调整了,还没有交换; 交换了就不需要再调整了,所以放到循环外
}
2、堆排序 迭代版本
上述递归版本的adjustNode()函数中逻辑判断虽然明显易读,但是判断多余冗余;另外,迭代版本 效率 一般低于迭代版本。下面给出迭代版本的代码。
//用来调整节点,保证节点大于左右孩子
template<typename T>
void adjustNode(T arr[], int node, int len) { //node为节点的下标
int lchNode = 1 + (node << 1); //node节点 左孩子 的下标
while (lchNode < len) { //至少得有个左孩子 才能往下走吧
if (lchNode != len - 1) { //即该节点有 左右 孩子,只有左孩子则不能进来
if (arr[lchNode + 1] > arr[lchNode]) lchNode++; //如果右孩子大于左孩子,下标记录为右孩子
}
if (arr[lchNode] > arr[node]) { //不管有没有右孩子,现在lchNode记录的是 最大的 或者 唯一的 孩子的下标,并且与节点node比较
std::swap(arr[lchNode], arr[node]);
node = lchNode; //节点得改成刚刚交换完的孩子
lchNode = 1 + (node << 1); //同时更新左孩子下标
}
else break; //没有交换了就跳出
}
}
//调整树中所有的节点
template<typename T>
void adjustHeep(T arr[], int len) {
for (int node = (len >> 1) - 1; node >= 0; node--) { // (len >> 1) - 1 为最后一个 有孩子的节点 的下标
adjustNode(arr, node, len); //从最后一个节点,一直调整到堆顶
}
}
template<typename T>
void heapSort(T arr[], int lo, int hi) {
adjustHeep(arr + lo, hi - lo); //首先从尾到头调整一遍
while (hi - lo > 2) {
std::swap(arr[lo], arr[hi - 1]); //每次将堆顶元素与最后元素交换
adjustNode(arr + lo, 0, --hi - lo); //重新调整堆顶节点
}
std::swap(arr[lo], arr[hi - 1]); //最后一次调整了,还没有交换; 交换了就不需要再调整了,所以放到循环外
}
堆排序的时间复杂度:初始的n/2次调整节点 + (n - 1)次 从堆顶开始的 从上往下的log2n次调整 = n/2 + (n-1)log2n = nlog2n;所以时间复杂度为O(nlogn)。显然,堆排序的常数时间较大,所以堆排序速度比快排要慢。另外,堆排序为不稳定的排序方法。