[数据结构] 堆排序_剖析

目录

一. 知识铺垫(满二叉树和完全二叉树的一些性质)

二.堆的结构和性质

三.堆排序

步骤一:建堆

      向上调整建堆(时间复杂度:N*logN):

   时间复杂度的计算:

     ​​​​​ 向下调整建堆(时间复杂度:O(N))

            时间复杂度的计算:

步骤二:利用堆的删除思想来讲新排序

最终代码实现

堆排序的总体时间复杂度

TOP K问题


一. 知识铺垫(满二叉树和完全二叉树的一些性质)

二叉树具有很多性质,这边主要介绍几个在堆中应用比较多的几个重要性质:

     1. 若规定根节点的层数为1,则一棵非空二叉树的第i层上最多有   2^(i-1) 个结点.

     2. 若规定根节点的层数为1,则深度为h的二叉树的最大结点数是  2^h-1.

     3. 若规定根节点的层数为1,则二叉树的深度是  log₂(n-1).

     4.对于具有n个结点的完全二叉树,如果按照从上至下从左至右的数组顺序对所有节点从0开始编号,则对于序号为i的结点有:

          i=0,i为根节点编号,无双亲节点 

          i位置节点的 双亲序号(i-1)/2

          i位置节点的 左孩子序号: 2i+1,   若2i+1>=size 则无左孩子 ;

          i位置节点的 右孩子序号: 2i+2,   若2i+2>=size 则无左孩子 ;     

     

ps:如果记不下这些结论,可以随手画出  如上图所示的二叉树,标上下标,结论就显而易见了 

二.堆的结构和性质

  • 堆 总是 一棵完全二叉树 , 所以我们通常用一块连续的空间来存储堆(所以他具备完全二叉树所拥有的性质 包含但不限于上面那些重要性质)
  • 堆中的某个节点的值总是大于或小于其孩子节点(所以他的根节点永远是结构中的最大值或最小值)
  • 父亲大于孩子的叫大堆,根节点为最大值
  • 父亲小于孩子的叫小堆,根节点为最小值

三.堆排序

堆排序(英语:Heapsort)是指利用这种数据结构所设计的一种排序算法。堆是一个近似完全二叉树的结构,并同时满足堆的性质即子结点的键值或索引总是小于(或者大于)它的父节点

堆排序的实现大体可以分为两步:

        1.建堆:         升序建大堆,    降序建小堆

        2.利用堆的删除思想来讲新排序:  将堆顶的数据(以升序为例,堆顶元素即为最大值)和最后一个数据交换(找到了最大值,并放到相应的位置)然后后对堆顶的数据再进行向下调整算法(维护堆结构),如此往复.便可以实现排序

      步骤一:建堆

        关于堆排序最关键的步骤就是建堆了,建堆有2种方法,分别是向上调整和向下调整.孰优孰劣,我们接下来便会具体分析

向上调整建堆(时间复杂度:N*logN):

        从第一个孩子(下标为 1 的元素)开始遍历,依次向上调整,直到遍历完所有元素,即完成建堆.

        向上调整代码示例:

//向上调整
void AdjustUp(int* a, size_t child)
{
	size_t parent = (child - 1) / 2;
	while (child > 0)
	{
		//if (a[child] < a[parent])      //建立小堆 如果孩子小于父母
		if (a[child] > a[parent])     //建立大堆    如果孩子大于父母
		{
			Swap(&a[child], &a[parent]);  //交换 a[child]和a[parent] 元素交换
			child = parent;
			parent = (child - 1) / 2;
		}
		else
		{
			break;
		}
	}
}
//向上调整--建堆

for(int i =1;i<n;++i){        //从第一个孩子开始向上调整
        
    AdjustUp(a,i);
}

        时间复杂度的计算:

       

向下调整建堆(时间复杂度:O(N)):

        从最后一个非叶子节点(下标(n-1-1)/2)依次往前遍历,进行向下调整,直到遍历到第一个元素,即完成建堆

        向下调整代码示例:

bigJustDown(int* a, int size, int root){
	int prent = root;
	int child = prent * 2 + 1;    //假定 左孩子
	while (child<size){    //向下调整,直到最后一层
        //选择比较大的孩子
		if (child + 1 < size && a[child + 1] > a[child]){  判断左右孩子大小
			child++;        //右孩子比较大
		}                
        //判断父节点 和 孩子节点 大小
		if (a[prent] < a[child]){    //孩子比较大, 父节点向下调整
			int tmp = a[prent];        
			a[prent] = a[child];        
			a[child] = tmp;
			prent = child;
			child = prent * 2 + 1;
		}
		else{                //孩子比较小,调整完成
			break;
		}

	}

}//向下调整建立大堆
void HeapSort(int* a, int n){
	int child = (n - 1 - 1) / 2;    //选择第一个非叶子节点向下调整
	//建立大堆
	for (child; child >= 0; child--){    //依次往上选择节点,进行向下调整
		bigJustDown(a, n, child);
	}

    /*
        ...  
            堆排序    
        ...    
    */
}

 时间复杂度的计算:

 

向下调整只需要O(N)次就可以完成,之所以比向上调整来得快,根本原因在于它从最后一个
非叶子节点开始,而在堆当中,每层的节点都以指数级增长,最后一层的节点最多可以是其他非叶子节点的总和还要多一个,

eg:第4层节点数2^3而根据等比数列求和公式,前三层节点个数之和也就(2^3)-1个

由于该方法由 Floyd 提出,因此又称 Floyd 算法

ps:关于向上调整和向下调整,比较简单容易理解,这里就不上图片说明了,如果有问题,可以移

步骤二:利用堆的删除思想来讲新排序

将堆顶元素与末尾元素进行交换,使末尾元素最大.然后继续调整堆(向下调整), 将堆顶元素与末尾元素交换, 得到第二大元素.如此反复进行交换、重建、交换

最终代码实现

//向下调整建立大堆
bigJustDown(int* a, int size, int root){
	int prent = root;
	int child = prent * 2 + 1;
	while (child<size){
		if (child + 1 < size && a[child + 1] > a[child]){
			child++;
		}

		if (a[prent] < a[child]){
			int tmp = a[prent];
			a[prent] = a[child];
			a[child] = tmp;
			prent = child;
			child = prent * 2 + 1;
		}
		else{
			break;
		}

	}

}

//堆排序的实现 升序
void HeapSort(int* a, int n){
   
  //步骤一::建立大堆

	int child = (n - 1 - 1) / 2;
	for (child; child >= 0; child--){
		bigJustDown(a, n, child);
	}    
                                    //建立大堆时间复杂度为  o(N)   (前面已经计算过)
 
 //步骤二:: 利用堆的删除思想进行排序
	
    int end = n - 1;    
	while (end >=0){                        //遍历N-1次
		//交换,出最大元素
		int tmp = a[0];
		a[0] = a[end];
		a[end] = tmp;
	  
		//向下调整
		bigJustDown(a, end--, 0);    //每次向下调整复杂度为 o(log N)


	}


}                               //总体时间复杂度:  N+(N-1)*logN ==  o(N* logN)

堆排序的总体时间复杂度

步骤一: 建立大堆时间复杂度为  o(N)   (前面已经计算过)
步骤二:利用堆的删除思想进行排序
        遍历  N-1
        每次向下调整复杂度为 o(log N)
 总体时间复杂度:  o(N)+ (N-1)*logN ==  o(N* logN)

TOP K问题

即求数据结合中前K个最大的元素或者最小的元素,一般情况下数据量都比较大。

比如:专业前10名、世界500强、富豪榜、游戏中前100的活跃玩家等。

对于Top-K问题,能想到的最简单直接的方式就是排序,但是:如果数据量非常大,排序就不太可取了(可能 数据都不能一下子全部加载到内存中)。最佳的方式就是用堆来解决,基本思路如下:

        1. 用数据集合中前K个元素来建堆

                        前k个最大的元素,则建小堆

                        前k个最小的元素,则建大堆

        2. 用剩余的N-K个元素依次与堆顶元素来比较,不满足则替换堆顶元素

                       将剩余N-K个元素依次与堆顶元素比完之后,堆中剩余的K个元素就是所求的前K个最小或者最大的元素。

这种方式的优势不仅仅在于它的时间复杂度低,同时当数据空间很大时,它的空间复杂度也很低,更适合决解实际问题

// TopK问题:找出N个数里面最大/最小的前K个问题。
// 比如:未央区排名前10的泡馍,西安交通大学王者荣耀排名前10的韩信,全国排名前10的李白。等等问题都是Topk问题,
// 需要注意:
// 找最大的前K个,建立K个数的小堆
// 找最小的前K个,建立K个数的大堆
void PrintTopK(int* a, int n, int k){
	int* top = (int*)malloc(sizeof(int) * k);
	assert(top);
	int i = 0;
	for (i; i < k; i++){
		top[i] = a[i];
	}
	for (int child = (k - 1 - 1) / 2; child >= 0; child--){
		AdjustDown(top, k, child);

	}
	for (i; i < n; i++){
		if (a[i] > top[0]){	//和小堆比较最小元素比较,大就插入;
			top[0] = a[i];
			AdjustDown(top, k, 0);
		}
	}
	for (; k >0;k--){
		printf("%d ", top[0]);
		top[0] = top[k-1];
		AdjustDown(top, k, 0);
	}
	free(top);

}


//测试函数
void TestTopk(){
	int n = 10000;
	int* a = (int*)malloc(sizeof(int) * n);
	srand(time(0));
	for (size_t i = 0; i < n; ++i){
		a[i] = rand() % 1000000;
	}
	a[5] = 1000000 + 1;
	a[1231] = 1000000 + 2;
	a[531] = 1000000 + 3;
	a[5121] = 1000000 + 4;
	a[115] = 1000000 + 5;
	a[2305] = 1000000 + 6;
	a[99] = 1000000 + 7;
	a[76] = 1000000 + 8;
	a[423] = 1000000 + 9;
	a[0] = 1000000 + 1000;
	PrintTopK(a, n, 10);
}

  • 14
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 12
    评论
评论 12
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值