一、概述
最近在复习算法和数据结构,一直以来对堆(heap)不是特别熟练,今天算是好好从头到尾梳理了一下,现在做个记录,以后可别再忘了。
二、构造最大堆
1. 最大堆的定义(性质)
算法导论上是这么说的
二叉堆可以分为两种形式:最大堆和最小堆。在这两种堆中,结点的值都要满足堆的性质,但一些细节定义则有所差异。在最大堆中,最大堆性质是除了根以外的所有节点 i i i 都要满足: A [ P A R E N T ( i ) ] > = A [ i ] A[PARENT(i) ]>= A[i] A[PARENT(i)]>=A[i], 最小堆则恰好相反。
就是说最大堆的每个结点的值肯定要大于等于其孩子节点的值,最小堆则是每个结点的值小于等于孩子节点的值。
首先,我们是通过数组模拟堆(二叉树)的结构的。堆的存储方式则是按照数组从头到尾由二叉树的层序遍历存储的。在构造最大堆之前,要了解这么两个性质:
a. 对于一个存储了n个数据的堆(
A
[
n
]
=
{
a
[
0
]
,
a
[
1
]
,
⋯
,
a
[
n
−
1
]
}
A[n] = \{a[0],a[1], \cdots,a[n- 1]\}
A[n]={a[0],a[1],⋯,a[n−1]})来说,其最后一个具有叶子节点的节点的下标为 n / 2 - 1
b. a[i] 节点的左右孩子节点分别为 a[2 * i + 1] 和 a[2 * i + 2],如果存在的话
2. 最大堆的构造
构造最大堆是一个循环迭代(或者递归)的过程,每一次迭代,都会将当前子树中的最大值移动到当前子树的根节点。所以,我们应该从最后一个包含叶子节点的节点(即 a [ n / 2 − 1 ] a[n/2- 1] a[n/2−1])开始进行位置更换,比较当前节点与字节点的大小,将最大节点的值与根节点交换。
例如,对于数组
a
=
{
3
,
5
,
1
,
2
,
4
}
a =\{3,5,1,2,4\}
a={3,5,1,2,4}来说,它有5个数据,我们每次从
a
[
5
/
2
−
1
]
a[5/2-1]
a[5/2−1]这个节点开始进行位置更换。如下图所示,红色节点表示当前访问的节点,对于数组
a
a
a来说,只要两轮即可将其构造成一个最大堆。
第一轮:首先访问
a
[
1
]
=
5
a[1] = 5
a[1]=5,比较其与两个孩子节点(分别为
a
[
2
∗
1
+
1
]
=
2
,
a
[
2
∗
1
+
2
]
=
4
a[2*1+1]=2,a[2*1+2]=4
a[2∗1+1]=2,a[2∗1+2]=4)的大小,发现 5 是最大的,不用更换位置,然后向前一个节点移动,移动到
a
[
0
]
=
3
a[0] = 3
a[0]=3,发现
a
[
1
]
=
5
a[1]=5
a[1]=5是最大的,所以将 3 和 5 的值互换,到此第一轮结束。
第二轮:首先访问 a [ 1 ] = 3 a[1] = 3 a[1]=3,比较其与两个孩子节点(分别为 2 和 4)的大小,发现 4 是最大的,将 4 与 3 互换,因为 a [ 0 ] a[0] a[0]已经是最大,不用再向前移动,所以第二轮到此结束。所以对于数组 { 3 , 5 , 1 , 2 , 4 } \{3,5,1,2,4\} {3,5,1,2,4}最终构造出来的最大堆为 { 5 , 4 , 1 , 2 , 3 } \{5,4,1,2,3\} {5,4,1,2,3}。
三、堆排序
算法导论上是这么说的
初始时候,堆排序算法利用BUILD-MAX-HEAP将输入数组A[1…n]建成最大堆,其中 n = A.length。因为数组中的最大元素总在根节点A[1]中,通过把它与A[n]进行互换,我们可以让该元素放到正确的位置。
这里需要注意的是,算法导论中说的下标是从1开始的。
意思就是,堆排序是基于最大堆(最小堆是降序,最大堆是升序)进行的。最大堆的根节点,也就是
a
[
0
]
a[0]
a[0]是所有元素中最大值,每次只要将最大堆的根节点即
a
[
0
]
a[0]
a[0]与数组最后一位
a
[
n
−
1
]
a[n-1]
a[n−1]交换,则
a
[
n
−
1
]
a[n-1]
a[n−1]变为最大值,然后对剩下的
{
a
[
0
]
,
⋯
,
a
[
n
−
2
]
}
\{a[0],\cdots, a[n - 2]\}
{a[0],⋯,a[n−2]}维护最大堆,这样循环迭代(或递归)下去,就可以得到一个升序排列的数组,即完成了堆排序。堆排序过程示意如下图:
最大堆的维护
每次在将根节点与剩下数据的最后一位元素交换后,可能会出现违反最大堆性质的情况(即根节点的值小于子节点),例如第一轮 5 与 3 交换后,3 小于其左子节点 4,则需要将 3 与 4 交换,直到大于等于子节点(子节点不包含已经排好序的 5),重复此过程,即可得到堆排序的结果为
{
1
,
2
,
3
,
4
,
5
}
\{1,2,3,4,5\}
{1,2,3,4,5}。
四、代码实现
递归实现
#include <iostream>
#include <vector>
using namespace std;
//时间复杂度O(logN),维护堆的性质
void maxHeapfy(vector<int>& nums, int cur, int endmark)
{
int father = cur;
int son = 2 * father + 1;
while(son < endmark) //避免排好序的值又被换回去
{
if(son + 1 < endmark && nums[son] < nums[son + 1])
son++;
if(nums[father] < nums[son])
{
swap(nums[son], nums[father]);
father = son;
}
son = 2 * son + 1;
}
}
//时间复杂度 O(N),建堆
void buildMaxHeap(vector<int>& nums, int n)
{
for(int i = n / 2 - 1; i >= 0; i--)
maxHeapfy(nums, i, n);
}
//堆排序,递归方式
void heapSort(vector<int>& nums, int endmark)
{
if(endmark == 0) return;
maxHeapfy(nums, 0, endmark);
swap(nums[0], nums[endmark - 1]);
heapSort(nums, endmark - 1);
}
//堆排序,迭代方式,时间复杂度O(NlogN)
void heapSort(vector<int>& nums)
{
for(int i = nums.size(); i > 0; i--)
{
maxHeapfy(nums, 0, i);
swap(nums[0], nums[i - 1]);
}
}
int main()
{
vector<int> nums;
int n;
cin >> n;
nums.resize(n);
for(int i = 0; i < n; i++)
cin >> nums[i];
buildMaxHeap(nums, n);
heapSort(nums);
for(auto a : nums)
cout << a << " ";
cout << endl;
}