数据结构练习(三)——HeapSort

本文详述了HeapSort算法,探讨了Heap(堆)数据结构的特性,包括child、parent和root的关系,max-heap与min-heap的区别。文章还介绍了维护堆属性、构建堆以及Heap Sort的具体步骤、算法复杂度分析和C++实现,揭示了算法与数据结构的紧密联系。
摘要由CSDN通过智能技术生成

前言

通过两天的时间终于看完了Heap Sort相关的内容,通过对于heap这一新的数据结构的学习,也算是初步理解了为什么算法和数据结构是密不可分的了,同时也对后面其他的数据结构及其对应算法产生了更浓厚的兴趣;当然在学习heap的过程中也遇到了一些令人头大的问题,尤其是关于计数问题,本文将梳理heap sort涉及到的二叉树的一些概念和一些学习思路。

HEAP

Heap是一种基于数组的新的数据的组织形式,传统的数组按照下表顺序连续的储存在内存中,同一数组中的元素间的联系在于内存地址连续。Heap虽然依赖于数组结构,但却采用了新的组织形式;Heap本质上是一种binary tree,而且这个树状结构仅可能最后一层不满,树中的元素和数组中的元素的下表的对应关系如下图:
heap示意图
根据需要,数组中的元素并不需要每一个都是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=1kSi=2k+11
由上可知,第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 k1层一共有 ∑ i = 1 k − 1 S i = 2 k − 1 \displaystyle\sum_{i=1}^{k-1} S_i=2^{k}-1 i=1k1Si=2k1个元素,因此树中一共有[ 2 k − 1 + x 2^k-1+x 2k1+x]个元素。
下面针对于x的奇偶性分开进行讨论
x = 2 y x=2y x=2y时(偶数)
树中一共有[ 2 k − 1 + 2 y 2^k-1+2y 2k1+2y],在第 k − 1 k-1 k1层中有⌈ x 2 \frac{x}{2} 2x = y =y =y个元素有子节点,又考虑到第 k − 1 k-1 k1层的第一个元素的序号为 2 k − 1 2^{k-1} 2k1,从该节点按序号顺序向后数,第一个叶子节点的序号为 2 k − 1 + y 2^{k-1}+y 2k1+y
而⌊ 2 k − 1 + 2 y 2 \frac{2^k-1+2y}{2} 22k1+2y + 1 = +1= +1= 2 k − 1 + y − 0.5 {2^{k-1}+y-0.5} 2k1+y0.5 + 1 = 2 k − 1 + y +1={2^{k-1}+y} +1=2k1+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 k1层元素全满,第 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=2k1+22k=3×2k11,而对于root节点而言,其左子树为一个k-1层的满树,规模大小为 n ′ = 2 k − 1 n&#x27;=2^{k}-1 n=2k1,显然有 n × 2 3 = 2 k − 2 3 &gt; n ′ n×\frac{2}{3}=2^k-\frac{2}{3}&gt;n&#x27; n×32=2k32>n
T ( n ) = Θ ( 1 ) + T ( n ′ ) &lt; T ( 2 n 3 ) + Θ ( 1 ) T(n) = Θ(1) +T(n&#x27;)&lt;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=0logn2h+1n×O(h)=O(nh=0logn2hh)=O(n(221logn2lognlogn))
考虑到 O Ο O估计可以忽略掉常数产生的影响,因此 2 ⌈ log ⁡ n ⌉ = n 2^{⌈\log{n}⌉}=n 2logn=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(221logn2lognh))=O(2n1logn)=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 n1次,每次的交换和heap-size修改操作花费 Θ ( 1 ) Θ(1) Θ(1)
调用
MAX-HEAPIFY(A,1)**花费 O ( log ⁡ h e a p s i z e ) &lt; O ( n log ⁡ n ) Ο(\log{{heapsize}})&lt;Ο(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();
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值