Heap Sort
前言
通过两天的时间终于看完了Heap Sort相关的内容,通过对于heap这一新的数据结构的学习,也算是初步理解了为什么算法和数据结构是密不可分的了,同时也对后面其他的数据结构及其对应算法产生了更浓厚的兴趣;当然在学习heap的过程中也遇到了一些令人头大的问题,尤其是关于计数问题,本文将梳理heap sort涉及到的二叉树的一些概念和一些学习思路。
HEAP
Heap是一种基于数组的新的数据的组织形式,传统的数组按照下表顺序连续的储存在内存中,同一数组中的元素间的联系在于内存地址连续。Heap虽然依赖于数组结构,但却采用了新的组织形式;Heap本质上是一种binary tree,而且这个树状结构仅可能最后一层不满,树中的元素和数组中的元素的下表的对应关系如下图:
根据需要,数组中的元素并不需要每一个都是heap中的元素,但heap中的元素在数组中下表一定是连续的。比如可以将A[1…10]视作heap(如上图),我也可以将数组中的部分元素A[2…9]视作heap,但绝不能将A[1…5,7…10]视作heap。
child & parent &root
子节点(child)、父节点(parent)和根节点(root)是分析树结构时重要的概念,我个人认为这三个概念是heap结构(由于目前本人只学过heap,并不了解树结构的其他性质)的核心。
叶子节点:没有子节点的节点,如上图中的6、7、8、9、10号节点
父节点(PARENT):与节点i直接连接的上一层的节点
PARENT(i) = ⌊i/2⌋
左子节点(LEFT):与节点i直接连接的左侧的子节点
LEFT(i) = 2i
右子节点(RIGHT):与节点i直接连接的右侧的子节点
PARENT(i) = 2i+1
在普通数组中我们绝不会认为第 i 号元素会和 2i 号元素存在什么直接联系,但heap结构建立了联系。
max-heap & min-heap
针对于heap这种结构,提出了两种特殊的heap,分别为max-heap和min-heap。
max-heap:A[PARENT(i)] ≥ A[i]
min-heap:A[PARENT(i)] < A[i]
在max-heap中,所有的父元素均大于或等于其对应的子元素,因此其root元素一定是所有元素中最大的。
在min-heap中,所有的父元素均小于其对应的子元素,因此其root元素一定是所有元素中最小的。
heap的部分性质
为方便后续讨论,先推出一些简单的性质作为引理。
首先声明:令k表示树的层,root节点位于k = 0层,S表示树中总的元素个数,Sk表示第k层中元素的个数,n表示树中元素的总个数。
借助上述符号表示如下性质:
树中共有:⌈ log 2 ( n ) \log_2^ (n) log2 (n)⌉层
S
k
=
2
k
S_k = 2^k
Sk=2k
满树时 :
n
=
∑
i
=
1
k
S
i
=
2
k
+
1
−
1
n=\displaystyle\sum_{i=1}^{k} S_i=2^{k+1}-1
n=i=1∑kSi=2k+1−1
由上可知,第k层的第一个元素的序号为
2
k
2^k
2k。
引理:叶子元素的下表一定为⌊
n
2
\frac{n}{2}
2n⌋+1,⌊
n
2
\frac{n}{2}
2n⌋+2,…,n
证明:不妨假设最后一层(记为第
k
k
k层)一共有
x
x
x个元素
显然前
k
−
1
k-1
k−1层一共有
∑
i
=
1
k
−
1
S
i
=
2
k
−
1
\displaystyle\sum_{i=1}^{k-1} S_i=2^{k}-1
i=1∑k−1Si=2k−1个元素,因此树中一共有[
2
k
−
1
+
x
2^k-1+x
2k−1+x]个元素。
下面针对于x的奇偶性分开进行讨论
x
=
2
y
x=2y
x=2y时(偶数)
树中一共有[
2
k
−
1
+
2
y
2^k-1+2y
2k−1+2y],在第
k
−
1
k-1
k−1层中有⌈
x
2
\frac{x}{2}
2x⌉
=
y
=y
=y个元素有子节点,又考虑到第
k
−
1
k-1
k−1层的第一个元素的序号为
2
k
−
1
2^{k-1}
2k−1,从该节点按序号顺序向后数,第一个叶子节点的序号为
2
k
−
1
+
y
2^{k-1}+y
2k−1+y,
而⌊
2
k
−
1
+
2
y
2
\frac{2^k-1+2y}{2}
22k−1+2y⌋
+
1
=
+1=
+1=⌊
2
k
−
1
+
y
−
0.5
{2^{k-1}+y-0.5}
2k−1+y−0.5⌋
+
1
=
2
k
−
1
+
y
+1={2^{k-1}+y}
+1=2k−1+y
和第一个叶子元素的序号相等,
x
x
x为奇数的情况同理,不在详细说明
证明完毕
Heap相关算法
Maintaining the heap property
将某一节点 i i i视作根节点,该节点对应的左、右子树均为max-heap,但 i i i节点元素可能小于其child元素,为维护该heap为一max-heap,提出如该算法。
伪代码
MaxHeapify(A,i)
1 l = LEFT(i);
2 r = RIGHT(i);
3 if l <= A.heap-size && A[l] > A[i]
4 largest = l;
5 else largest = i;
6 if r <= A:heap-size && A[r] > A[largest]
7 largest = r;
8 if largest != i
exchange A[i],A[largest];
10 MaxHeapify(A,largest);
算法及复杂度分析
对于输入规模为n的heap,设其时间复杂度为T(n),
上述算法先检查节点
i
i
i和其子节点的大小关系,如果满足max-heap的性质,那么算法结束
如果不满足则交换节点
i
i
i和最大的子节点(序号为largest(
i
i
i)),并递归检查交换后的节点largest(
i
i
i)和其子节点之间的大小关系。
考虑最差的情况,节点
i
i
i处的元素为整个heap中的最小值,因此需要下放到叶子节点才能结束算法;
对于大小为n的子树,该算法消耗T(n),经过Θ(1)时间递归调用,递归调用的对象为子树规模大小;
根据heap 的生成方式,当该树为半满树的时候,子树的规模最大,即:考虑root根节点,其左子树比右子树多一层元素时是左子树规模最大的时候。
假设某binary tree第
k
−
1
k-1
k−1层元素全满,第
k
k
k层只有一半的元素,此时该树的总规模大小为
n
=
2
k
−
1
+
2
k
2
=
3
×
2
k
−
1
−
1
n=2^{k}-1+{\frac{2^k}{2}}=3×2^{k-1}-1
n=2k−1+22k=3×2k−1−1,而对于root节点而言,其左子树为一个k-1层的满树,规模大小为
n
′
=
2
k
−
1
n'=2^{k}-1
n′=2k−1,显然有
n
×
2
3
=
2
k
−
2
3
>
n
′
n×\frac{2}{3}=2^k-\frac{2}{3}>n'
n×32=2k−32>n′
T
(
n
)
=
Θ
(
1
)
+
T
(
n
′
)
<
T
(
2
n
3
)
+
Θ
(
1
)
T(n) = Θ(1) +T(n')<T(\frac{2n}{3})+Θ(1)
T(n)=Θ(1)+T(n′)<T(32n)+Θ(1)
因此
T
(
n
)
=
O
(
log
n
)
T(n) =Ο(\log n)
T(n)=O(logn)
C++实现
//代码中的printf函数用于输出代码运行步骤,方便调试
template <typename T>
void MaxHeapify(T &A,int i ,int _HeapSize)
{
int HeapSize = _HeapSize;
int l = LEFT(i);
int r = RIGHT(i);
printf("%d:left ----A[%d] = %d\n%d:right----A[%d] = %d,heapsize = %d\n",i,l-1,A[l-1],i,r-1,A[r-1],HeapSize);
int largest = 0;
if(l <= HeapSize && A[l-1] > A[i-1])
{
largest = l;
printf("A[%d] > A[%d],largest = %d\n",l-1,i-1,largest);
}
else
{
largest = i;
printf("A[%d] <= A[%d],largest = %d\n",l-1,i-1,largest);
}
if(r <= HeapSize && A[r-1] > A[largest-1])
{
largest = r;
}
printf("largest = %d\n",largest);
if(largest != i)
{
std::swap(A[i-1],A[largest-1]);
printf("swap A[%d] A[%d]\n",i-1,largest-1);
A.show();
printf("MaxHeapify(A,%d)\n",largest);
MaxHeapify(A,largest,HeapSize);
}
}
Build Heap
当给定的一个任意数组,其对应的binary tree可能有多个元素不符合 max-heap property,为迅速建立起一个max-heap,提出本算法。
伪代码
BUILD-MAX-HEAP(A)
1 A.heap-size = A:length
2 for i = ⌊A.length/2⌋ downto 1
3 MaxHeapify(A,i)
算法及复杂度分析
显然对于任意叶子节点的父节点
i
i
i,其child一定满足max-heap(因为其子树只包含单个元素),因此按照元素序号的逆序,从第一个不为叶子元素的节点开始,依次循环调用MaxHeapify(A,
i
i
i),从而保证节点
i
i
i的父元素PARENT(
i
i
i)的child均为max-heap,最终调用MaxHeapify(A,1),使得整个树变为max-heap,根据前面的引理部分可知,第一个叶子元素的序号为⌊
n
2
\frac{n}{2}
2n⌋+1,因此⌊
n
2
\frac{n}{2}
2n⌋为第一个非叶子元素。
该算法一共循环
n
2
\frac{n}{2}
2n次,每次调用MaxHeapify(A,
i
i
i) 的时间复杂度为
O
(
log
n
)
Ο(\log n)
O(logn),因此该算法的运行时间为
O
(
n
log
n
)
Ο(n\log n)
O(nlogn)。
然而
O
(
n
log
n
)
Ο(n\log n)
O(nlogn)作为一个上界不够贴近实际运行消耗。我们不难注意到MaxHeapify(A,
i
i
i) 的实际复杂度和
i
i
i元素对应的高度有关,比如当
i
i
i元素位于次底层时和位于root节点时复杂度并不相同。
树的根节点height最大,叶子节点最小。对于n规模的树的height为⌊
log
n
\log{n}
logn⌋,并且每层(对应的height为h)中的节点个数为⌈
n
2
h
+
1
\frac{n}{2^h+1}
2h+1n⌉
因此对于上述算法,每一层的复杂度变为⌈
n
2
h
+
1
\frac{n}{2^h+1}
2h+1n⌉
×
O
(
h
)
×Ο(h)
×O(h)
∑
h
=
0
⌈
log
n
⌉
⌈
n
2
h
+
1
⌉
×
O
(
h
)
=
O
(
n
∑
h
=
0
⌈
log
n
⌉
h
2
h
)
=
O
(
n
(
2
−
1
2
⌈
log
n
⌉
−
⌈
log
n
⌉
2
⌈
log
n
⌉
)
)
\displaystyle\sum_{h=0}^{⌈\log{n}⌉} {⌈\frac{n}{2^h+1}⌉×Ο(h)}=Ο(n\displaystyle\sum_{h=0}^{⌈\log{n}⌉}{\frac{h}{2^h}})=Ο(n(2-\frac{1}{2}^{⌈\log{n}⌉}-\frac{⌈\log{n}⌉}{2^{⌈\log{n}⌉}}))
h=0∑⌈logn⌉⌈2h+1n⌉×O(h)=O(nh=0∑⌈logn⌉2hh)=O(n(2−21⌈logn⌉−2⌈logn⌉⌈logn⌉))
考虑到
O
Ο
O估计可以忽略掉常数产生的影响,因此
2
⌈
log
n
⌉
=
n
2^{⌈\log{n}⌉}=n
2⌈logn⌉=n
故
O
(
n
(
2
−
1
2
⌈
log
n
⌉
−
h
2
⌈
log
n
⌉
)
)
=
O
(
2
n
−
1
−
⌈
log
n
⌉
)
=
O
(
n
)
Ο(n(2-\frac{1}{2}^{⌈\log{n}⌉}-\frac{h}{2^{⌈\log{n}⌉}}))=Ο(2n-1-⌈\log{n}⌉)=Ο(n)
O(n(2−21⌈logn⌉−2⌈logn⌉h))=O(2n−1−⌈logn⌉)=O(n)
所以Build Heap的真正的时间复杂度为
O
(
n
)
Ο(n)
O(n)
C++实现
template <typename T1>
void BuildMaxHeap(T1 &A,int _HeapSize)
{
int HeapSize = _HeapSize;
for(int i = HeapSize/2;i > 0 ;i -- )
{
printf("MaxHeapify(A,%d)\n",i);
MaxHeapify(A,iteration,HeapSize);
}
}
Heap Sort
伪代码
1 BUILD-MAX-HEAP(A)
2 for i = A.length downto 2
3 exchange A[1] with A[i]
4 A.heap-size = A:heap-size - 1
5 MAX-HEAPIFY(A,1)
算法及复杂度分析
首先调用BUILD-MAX-HEAP(A),将A转换为一个max-heap
max-heap的首元素一定是heap中的最大元素,因此将首元素取出,取出通过交换首末元素,并将heap-size减少1来实现,此时首元素为原heap的末尾元素,首元素的左、右子树均为max-heap,因此通过调用**MAX-HEAPIFY(A,1)**便可将新heap转化为max-heap,然后重复取出最大元素(首元素)。
**BUILD-MAX-HEAP(A)花费
O
(
n
)
Ο(n)
O(n)
循环
n
−
1
n-1
n−1次,每次的交换和heap-size修改操作花费
Θ
(
1
)
Θ(1)
Θ(1)
调用MAX-HEAPIFY(A,1)**花费
O
(
log
h
e
a
p
s
i
z
e
)
<
O
(
n
log
n
)
Ο(\log{{heapsize}})<Ο(n\log{{n}})
O(logheapsize)<O(nlogn)
C++实现
/* 此处调用的RandArray类是我个人编写的一种能够生成随机数组的类
* 读者只需要将A(10,10,10,0)理解为一个长度为10且内部元素最大不
* 超过10的随机数组即可。
* A.show()即打印数组A中的元素,方便检查运行过程和最终结果
*
*
*/
void MaxHeapifyApp()
{
RandArray<int> A(10,10,10,0);
std::cout << "disorder heap!!!!" << std::endl;
A.show();
int HeapSize = A.size();
BuildMaxHeap(A,HeapSize);
std::cout << "max heap!!!!" << std::endl;
A.show();
for(int i = A.size() - 1;i > 1 ;i --)
{
std::swap(A[i],A[0]);
A.show();
HeapSize--;
MaxHeapify(A,1,HeapSize);
}
std::swap(A[1],A[0]);
std::cout << "sorted heap!!!!" << std::endl;
A.show();
}