堆排序学习笔记

学习资料出处:白话经典算法系列之七 堆与堆排序 

Note:MoreWindows Blog所写的堆排序和快速排序极好,深入浅出,通俗易懂。严重推荐!以下是我的学习笔记:


一、为什么要用堆排序

堆排序、快速排序和递归排序是排序算法中效率最高的三种,时间复杂度均为O(n*log(n)),应用极广。


二、二叉堆的定义

二叉堆本质上是完全(或近似完全)二叉树,简称堆。满足以下两条特性:

1. 父节点的键值不小于(或不小于)其子孙节点,分别对应最大堆和最小堆;

2.每个节点的左/右子树都是一个二叉堆。

注意:二叉堆不是二叉搜索树。一方面,二叉搜索树很难通过数组这种简单的数据结构表示,二叉搜索树要求中序遍历序列单调,而二叉堆数组从头到尾轮询对应二叉树的层次遍历;另一方面,二叉堆的层次遍历序列不保证单调性。

下图展示一个最小堆,满足二叉堆的两条特性;其层次遍历为:1 -> 2 -> 3 -> 4 -> 6 -> 5,并不满足单调升序或降序。


图1. 最小堆

一般用数组来表示堆,i节点的父节点下标为(i-1)/2,左子结点下标为(2i+1),右子节点下标为(2i+2)。图1最小堆的存储结构为:

123465

三、堆操作之算法

1. 插入(尾部,从后往前调整)

在二叉堆的尾部插入一个元素,然后对二叉堆局部(自下而上沿父节点到根节点的路径)进行调整,使得两条特性依然满足。

对图1的最小堆进行插入操作:


图 2. 插入元素0


图 3. 插入元素1

在最小堆化数组a插入节点i,编程思路如下:

1) 保存节点i的键值到临时变量,tmp = a[i]

2) 节点i与父节点j=(i-1)/2比较键值,如果a[i] < a[j],则父节点下移,即a[j] = a[i],然后转到3);否则转到4)

3) 当节点i到达根节点,转到4),否则i=j,转到2)

4) 在节点i插入新元素

复杂度分析:对于规模为n的堆,高度为log(n),遍历到的节点只限于比较、交换操作,因此节点插入的复杂度为O(log(n))。

编程实现

/*
@调整新加入的节点i
*/
template
   
   
    
    
void MinHeapFixup(T *a, std::size_t i)
{
	T tmp = a[i];
	size_t j = (i-1)/2;
	while(j>=0 && i>0){//在有序(升序)队列中(从后往前)插入新元素
		if(a[j] <= tmp)//找到新元素的插入位置
			break;
		a[i] = a[j];//否则,父节点下移
		i = j;//调整搜素位置,前移至其父节点,继续;直到抵达根节点
		j = (i-1)/2; 
	}
	a[i] = tmp;	
}

/*
@插入元素e到堆中
*/
template
    
    
     
     
void MinHeapInsert(T *a, std::size_t n, T e)
{
	a[n] = e;
	MinHeapFixup
     
     
      
      (a, n);
}

     
     
    
    
   
   

2. 删除(头部,从前往后)

堆元素的删除只能针对第0个元素。实际操作是将最后一个元素赋给根节点,然后从前往后进行一次调整。调整时先在左右孩节点中挑选最小者和当前父节点比较,如果当前父节点比左右孩节点的最小者还小,则不需要进行交换;否则交换父节点和左右孩最小节点,依次往后进行,直到不再需要交换为止。这相当于根节点“下沉”的过程。

复杂度分析:对于规模为n的堆,高度为log(n),遍历到的节点只限于比较、交换操作,因此节点删除的复杂度为O(log(n))。

编码实现

/*
@从前往后调整节点i, 堆化数组大小为n
*/
template
   
   
    
    
void MinHeapFixdown(T *a, std::size_t i, std::size_t n)
{
	T tmp = a[i];
	size_t j = 2*i+1;
	while(j
    
    
     
     = tmp)
			break;
		a[i] = a[j];
		i = j;
		j = 2*i+1;		
	}
	a[i] = tmp;
}

/*
@删除第0节点
*/
template
     
     
      
      
void MinHeapDelete(T *a, std::size_t n)
{
	std::swap(a[0], a[n-1]);
	MinHeapFixdown(a, 0, n-1);
}

     
     
    
    
   
   

3. 堆化数组

1) 笨办法是依次取出原数组的元素,一一插入新建的堆数组。由于每次插入操作需要O(log(n))的时间,时间复杂度总计为O(n*log(n))。好处是不会破坏原数组元素的排列顺序。

2) 由于叶节点可视为二叉堆,且叶节点数量为总的节点数量的一半,因此只需要对另一半节点进行调整即可,调整方向为自底而上。

复杂度分析:O(n*log(n)),采用方法2效率提高一倍。

编程实现

/*
@从非叶节点开始降序(叶节点从大到小)进行调整,调整方向为从前往后,保证每一棵子二叉堆成立
*/
template
   
   
    
    
void makeMinHeap(T *a, std::size_t n)
{
	for(int i=n/2-1; i>=0; i--){//i需要取值i=0,否则根节点没有参与调整
		MinHeapFixdown(a, i, n);
	}
}

   
   

4. 排序

首先可以看到堆建好之后堆中第0个数据是堆中最小的数据。取出这个数据再执行下堆的删除操作。这样堆中第0个数据又是堆中最小的数据,重复上述步骤直至堆中只有一个数据时就直接取出这个数据。

由于堆也是用数组模拟的,故堆化数组后,第一次将A[0]与A[n - 1]交换,再对A[0…n-2]重新恢复堆。第二次将A[0]与A[n – 2]交换,再对A[0…n - 3]重新恢复堆,重复这样的操作直到A[0]与A[1]交换。由于每次都是将最小的数据并入到后面的有序区间,故操作完成后整个数组就有序了。有点类似于直接选择排序。

采用最小堆排列后是递减数组,最大堆对应的递增数组。

编程实现

/*
@ 对二叉堆进行排序
@ 最小堆实现数组的降序排列,最大堆则对应实现升序排列
@ 调用MinHeapFixdown,自顶而下进行调整,前提是除根节点外均满足二叉堆的两条特性
*/
template
   
   
    
    
void MinHeapSort_1(T *a, std::size_t n)
{
	for(int i=n-1; i>0; i--){
		std::swap(a[0], a[i]);
		MinHeapFixdown(a, 0, i);
	}
}

/*
@ 试图对二叉堆进行排序,但结果错误!!!
@ 调用MinHeapFixdown,试图自底而上进行调整,前提是除当前叶节点外均满足二叉堆的两条特性
@ 很显然,变化中的根节点无法满足特性1
*/
template
    
    
     
     
void MinHeapSort_2(T *a, std::size_t n)
{
	for(int i=n-1; i>0; i--){
		std::swap(a[0], a[i]);
		MinHeapFixup(a, i-1);//并不能保证a[0..i-2]满足二叉堆的特性1,即父节点的键值小于其子孙节点
	}
}

    
    
   
   
时间复杂度分析:
堆化数组需要n/2次向下调整,每次调整时间为O(log(n)),此后还有n-1次调整操作,每次时间复杂度也是O(log(n)),因此总的时间复杂度为O(n*log(n))。
此外,STL也有实现堆的相关函数,参阅 《STL系列之四heap堆》

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值