目录
堆是一种数据结构,与堆内存是不同的概念,不要混淆。
一.堆的定义
堆是一个可以被看做一棵 完全二叉树 的 数组对象。
二.堆的性质
2.1堆的性质:
1. 堆中某个结点的值总是 不大于或不小于 其父结点的值
2. 堆总是一棵完全二叉树。
2.2堆的分类
按照根结点是 最大值 还是 最小值 分为 大根堆 和 小根堆:
大根堆:
根结点是最大值 的堆,用于维护和查询 max
大根堆的任意结点的值 >= 它所有子结点的值(父 >= 子)
小根堆:
根结点是最小值 的堆,用于维护和查询 min
小根堆的任意结点的值 <= 它所有子结点的值(父 <= 子)
示例图如下:
2.3 数组表示的堆的性质
以下性质主要是由完全二叉树的性质得到的结论。 在数组表示的堆中,数组下标0 为父节点,父结点与子结点的性质如下:
第 i 个结点的 父结点 下标 为 (i-1)/2 ;
第 i 个结点的 左子结点 下标 为 2i+1 ;
第 i 个结点的 右子结点 下标 为 2i+2 ;
最后一个非叶子结点 下标为:n/2 -1
叶子结点是 下标从 n/2 开始,后面所有的都是 叶结点。
三.堆排序的过程
实现堆排序的思想:
将一个长为n的序列构造成一个大顶堆,则整个序列的最大值就是堆顶的根结点。 将最大值结点与末尾结点的值互换,此时末尾结点的值就是最大值。(即数组的最后一个元素为最大值)
然后将剩余的 n-1个序列重新构造成一个大顶堆,再将n-1序列的最大值与末尾结点的值互换,就会得到 次最大值。 如此重复执行,就可以得到一个有序序列了。
3.1图解过程
假设 输入的长度为7的序列为 [7,10,15,30,35,23,40], 图示如下:
建立最大堆的时候是从 最后一个非叶子结点 开始 从下往上调整。下面为序列建堆的过程:
1.先从最后一个非叶子结点 结点2,开始调整:
结点2和结点6的值进行互换后,结点6的值被更改了,但是它不再有叶子结点,因此不需要继续对结点6建大根堆。
2.再往结点2的兄弟结点1,进行调整:
结点1和结点4的值进行互换后,结点4的值被更改了,但是它不再有叶子结点,因此不需要继续对结点4建大根堆。
3.再往结点1的上一个父结点,进行调整:
结点0和结点2的值进行互换后,结点2的值被更改了,结点2有叶子结点,因此需要继续对结点2建大根堆。
这个过程就实现了对长为7的序列 构造大顶堆。
堆排序就是基于构建的堆,将堆顶的(最大值)根结点的值 与 末尾结点的值进行互换:
然后再对剩下的6个结点,再进行构造成一个大顶堆,重复执行下去。
3.2代码实现(C++)
两种写法:
非递归写法:
#include <iostream>
#include <algorithm>
using namespace std;
//构建大顶堆
void max_heapify(int arr[], int start, int end)
{
//建立父节点指标和子节点指标
int dad = start;
//左子节点 下标
int son = dad * 2 + 1;
while (son <= end) //若子节点指标在范围内才做比较
{
//右子结点 && 左右子结点对比 先比较两个子节点大小,选择最大的
if (son + 1 <= end && arr[son] < arr[son + 1])
son++; //使用右子节点
if (arr[dad] > arr[son]) //如果父节点大于子节点代表调整完毕,直接跳出函数
return;
else //否则交换父子内容再继续子节点和孙节点比较
{
swap(arr[dad], arr[son]);
dad = son;
son = dad * 2 + 1;
}
}
}
void heap_sort(int arr[], int len)
{
//初始化,i从最后一个父节点开始调整, 往前都是父节点
for (int i = len / 2 - 1; i >= 0; i--)
max_heapify(arr, i, len - 1);
//先将第一个元素和已经排好的元素前一位做交换,
//再从新调整(刚调整的元素之前的元素),直到排序完毕
for (int i = len - 1; i > 0; i--)
{
swap(arr[0], arr[i]);
max_heapify(arr, 0, i - 1);
}
}
int main()
{
int arr[] = { 7,10,15,30,35,23,40 };
int len = (int)sizeof(arr) / sizeof(*arr);
heap_sort(arr, len);
for (int i = 0; i < len; i++)
cout << arr[i] << ' ';
cout << endl;
system("pause");
return 0;
}
递归写法:
#include<iostream>
#include <algorithm>
using namespace std;
void heapify(int* tree, int n, int i)
{
if (i >= n) return;
//左子结点
int c1 = i * 2 + 1;
//右子结点
int c2 = i * 2 + 2;
//假设最大值坐标是根结点,获取左右子结点的最大值
int max = i;
if (c1<n && tree[c1]>tree[max])
{
max = c1;
}
if (c2<n && tree[c2]>tree[max])
{
max = c2;
}
if (max != i)
{
//将左右子树的最大值赋给父结点
swap(tree[max],tree[i]);
//较小的值,被赋给左子树或右子树,则左子树或右子树 需要重新建堆
heapify(tree,n,max);
}
}
void build_heap(int *tree, int n)
{
int last_node = n - 1;
//最后一个结点的父节点 下标,即最后一个非叶子结点
int parent = (last_node - 1) / 2;
//针对最后一个父节点的 及其前面的父节点进行建堆
for (int i =parent; i>=0; i--)
{
heapify(tree,n,i);
}
}
void heap_sort(int *tree, int n)
{
build_heap(tree,n);
for (int i = n-1; i>=0; i--)
{
//堆顶与末尾结点值交换
swap(tree[i], tree[0]);
//i不断在砍断
heapify(tree,i,0);
}
}
int main()
{
int tree[] = { 7,10,15,30,35,23,40 };
int n = 7;
heap_sort(tree,n);
for (int i=0;i<n;i++)
{
cout << tree[i]<<" ";
}
cout<< endl;
return 0;
}
四.堆排序的时间复杂度
堆排序的时间复杂度是O(n logn),是一个不稳定的排序算法。
(稳定的定义是:如果排序前后它们的相对次序一定保持不变,就称排序算法是稳定的
否则就称排序算法是不稳定的)
在C++中的 priority_queue容器, 其内部实现就是通过二叉堆(默认大根堆)实现,在工作中可以直接使用容器内置算法来做排序。详情见优先队列的用法:
C++中优先队列的priority_queue<int,vector<int>,greater<int>>与priority<int>的用法_Appleeatingboy的博客-CSDN博客