[the summarization of algorithm]Heap and Heapsort

1、什么是堆

        1)堆是一个树,或完全二叉树,或近似完全二叉树。

        (注:完全二叉树是指:若设二叉树的深度为h,除第 h 层外,其它各层 (1~h-1) 的结点数都达到最大个数,第 h 层所有的结点都连续集中在最左边的二叉树。)

        2)堆需要满足这样的一个性质:

                1.父结点的键值总是大于或等于(小于或等于)任何一个子节点的键值(大根堆/最大堆、小根堆/最小堆)。

        (注:当父结点的键值总是大于或等于任何一个子节点的键值时为最大堆。当父结点的键值总是小于或等于任何一个子节点的键值时为最小堆。)

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

       (注:即每个结点的值都大于<或小于>左子树及右子树中的值。)

                3. 堆是一个平衡树。

       (注:平衡树是指一棵空树或其的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树。)



2、堆的存储

        首先我们对堆中的元素进行一个编号。从根结点开始依次编号1,2,3,……

       (注:当然可以按照数组编号的方式,编为0,1,2,……我们这里采用略过a[0]来处理。如果采用数组编号的方式,之下的规律会和此处展示的规律有所区别。)

        我们观察这些编号之间的关系。

        通过观察我们可以发现如下规律:

               1、假设父亲结点的编号为 i ,则其左子树的编号为 2*i ,右子树的编号为 2*i + 1。

               2、假设子结点的编号为 i ,则其父亲结点的编号为 i/2(整数除法)。

        正是因为这样一种好的规律,我们完全可以用数组来构建堆。



3、堆的基本操作-建堆

        对一个从1到n的数据,我们先以乱序将其存入数组。

        然后我们堆这个数组的前n项进行建堆操作。根据2中堆结点编号所满足的性质,用数组来模拟建立一个堆。堆中最后一个元素的父亲结点编号一定是n/2(整数除法)。

        因此,我们从n/2结点开始,到编号为1的结点结束,顺次检查并使其满足1中堆的性质。这样,我们就可以建立一个堆。 

        我们先假设“检查并使其满足1中堆的性质"这一操作为一个函数:int Heapify(int i) ; 其作用为使编号为 i 的结点之下的树满足1中堆的性质。

        下面,我们完成实现上述操作的函数:

void BuildHeap(int n)//建堆函数(按照1,2,3,……编号时,且n,a[N]当为全局变量时)
{
	for(int i = n/2;i>=1;i--)
	{
		Heapify(i);//从下往上维护 
	}
}
void BuildHeap(int a[], int n) //建堆函数(按照0,1,2,……编号时)

{    
    for(int i = (n-1)/2; i >= 0; i--)  
    {  
        Heapify(a, i);  
    }  
}  

4、堆的基本操作-维护堆

        3中提到的函数Heapify(int i) , 他想要实现的操作有:

                1、使编号为 i 的结点和其子结点比较,判断结点 i 和其子结点是否满足1中的性质。

                2、如果满足则结束。如果不满足,则通过交换元素的值来使其满足这样的性质。

                3、如果进行了交换,则说明子结点被换为了较大的数(小根堆)或较小的数(大根堆)。则这个子结点以下的部分是否满足条件,我们不得而知。因此,我们还要对这个子结点一下的部分运行对维护函数,以使整个树满足这样的性质。


        下面,我们展示维护堆的函数(小根堆):<n、a[N]的传入问题,可以将n,a[N]设置为全局变量,也可以将n,a[N]作为参数传入Heapify()函数中>

void Heapify(int i)//堆维护函数(从结点i以下的堆进行维护)(按照1,2,3,……编号时,且n,a[N]当为全局变量时)

{
	int min = i, left = 2 *i,right = 2*i +1,temp;
	min = ((left<=n)&&(a[left] < a[min]))?left:min;
	min = ((right<=n)&&(a[right] < a[min]))?right:min;//找出结点i和其两分支的最小值的索引值 
	if(min != i)
	{
		temp = a[i];
		a[i] = a[min];
		a[min] = temp;//如果结点不是 三个元素中的最小值则交换结点和最小值的值 
		Heapify(min); //原为最小值的值现在变大了,所以要对其后的结点堆维护 
	}
}
void Heapify(int a[],int i,int n)//堆维护函数(从结点i以下的堆进行维护)
  // (按照0,1,2,……编号时,由于在BuildHeap()函数中调用时,BuildHeap()函数中已经有n,所以不必再将n传入,当然传入n也可以。如果要应用堆排序,尽量传入)

{
	int min = i, left = 2 *i + 1,right = 2*i + 2,temp;
	min = ((left<n)&&(a[left] < a[min]))?left:min;
	min = ((right<n)&&(a[right] < a[min]))?right:min;//找出结点i和其两分支的最小值的索引值 
	if(min != i)
	{
		temp = a[i];
		a[i] = a[min];
		a[min] = temp;//如果结点不是 三个元素中的最小值则交换结点和最小值的值 
		Heapify(min); //原为最小值的值现在变大了,所以要对其后的结点堆维护 
	}
}

注意:

min = ((left<=n)&&(a[left] < a[min]))?left:min;
min = ((right<=n)&&(a[right] < a[min]))?right:min;
中边界条件(left<=n)和(right<=n)一定不要忘记,否则不能限制对数组前n个元素进行建堆。

5、堆的基本操作-插入元素

        在已经形成的堆中插入元素,我们通常将新加入的元素放在堆的最末,然后自下而上对堆进行一个维护。

        由于从这个新数据的父结点到根结点必然为一个有序的数列,因此只需要让这个数据和这个数据的父结点进行比较,如果不满足则交换,并进一步与父结点的父结点进行比较,一直进行这样的操作,知道出现了满足的情况,那么此时一定已经是一个满足1中所有条件的堆了。

        维护函数(小根堆):(注:插入排序的思想)

//  新加入i结点  其父结点为 i / 2      (按照1,2,3,……编号时)

void MinHeapFixup(int a[], int i)
{
    int j, temp;
	
	temp = a[i];//记录加入的值
	j = i / 2;      //父结点
	while (j >= 0 && i != 0)
	{
		if (a[j] <= temp)
			break;
		
		a[i] = a[j];     //把较大的子结点往下移动,替换它的子结点
		i = j;
		j = i / 2;
	}
	a[i] = temp;//插入元素
}

//  新加入i结点  其父结点为(i - 1) / 2      (按照0,1,2,……编号时)

void MinHeapFixup(int a[], int i)
{
    int j, temp;
	
	temp = a[i];//记录加入的值
	j = (i - 1) / 2;      //父结点
	while (j >= 0 && i != 0)
	{
		if (a[j] <= temp)
			break;
		
		a[i] = a[j];     //把较大的子结点往下移动,替换它的子结点
		i = j;
		j = (i - 1) / 2;
	}
	a[i] = temp;//插入元素
}

6、堆的基本操作-删除元素

        我们使用堆,通常是要提取出堆中的最小元素(小根堆)或堆中的最大元素(大根堆)。

        因此,我们删除元素通常只是用于删除堆顶元素。

        但是当我们删除了堆顶元素后,怎样将堆重建起来呢?

        我们可以将堆的堆尾元素转移到堆顶,然后将堆的大小减小,然后从顶部开始对堆进行一个维护。使其满足1中的性质。

        所以,我们使用4中的函数Heapify(1)(按照1,2,3,……编号时)或Heapify(0)(按照0,1,2,……编号时)


7、堆排序

        由于堆得堆顶元素是所有元素的最小或最大值,因此,我们可以通过建堆来实现排序。

        首先,取下堆顶元素得到最小元素,然后将堆尾元素填入堆顶元素进行堆维护,形成一个新堆,继续截取堆顶元素,以此类推,便可得到升序或降序的有序数列。

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

        用上述方法的话,如果想得到升序数列,则需要建立大根堆,建立降序数列则需要建立小根堆。


void Heapsort(int a[], int n) (编号为0,1,2,……时)
{
	for (int i = n - 1; i >= 1; i--)
	{
		Swap(a[i], a[0]);
		Heapify(a, 0, i);
	}
}


        由于每次重新恢复堆的时间复杂度为O(logN),共N - 1次重新恢复堆操作,再加上前面建立堆时N / 2次向下调整,每次调整时间复杂度也为O(logN)。二次操作时间相加还是O(N * logN)。故堆排序的时间复杂度为O(N * logN)。


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
资源包主要包含以下内容: ASP项目源码:每个资源包中都包含完整的ASP项目源码,这些源码采用了经典的ASP技术开发,结构清晰、注释详细,帮助用户轻松理解整个项目的逻辑和实现方式。通过这些源码,用户可以学习到ASP的基本语法、服务器端脚本编写方法、数据库操作、用户权限管理等关键技术。 数据库设计文件:为了方便用户更好地理解系统的后台逻辑,每个项目中都附带了完整的数据库设计文件。这些文件通常包括数据库结构图、数据表设计文档,以及示例数据SQL脚本。用户可以通过这些文件快速搭建项目所需的数据库环境,并了解各个数据表之间的关系和作用。 详细的开发文档:每个资源包都附有详细的开发文档,文档内容包括项目背景介绍、功能模块说明、系统流程图、用户界面设计以及关键代码解析等。这些文档为用户提供了深入的学习材料,使得即便是从零开始的开发者也能逐步掌握项目开发的全过程。 项目演示与使用指南:为帮助用户更好地理解和使用这些ASP项目,每个资源包中都包含项目的演示文件和使用指南。演示文件通常以视频或图文形式展示项目的主要功能和操作流程,使用指南则详细说明了如何配置开发环境、部署项目以及常见问题的解决方法。 毕业设计参考:对于正在准备毕业设计的学生来说,这些资源包是绝佳的参考材料。每个项目不仅功能完善、结构清晰,还符合常见的毕业设计要求和标准。通过这些项目,学生可以学习到如何从零开始构建一个完整的Web系统,并积累丰富的项目经验。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值