排序算法--堆排序--详解与代码实例

堆排序:

Heapsort类似于 选择排序我们反复选择最大的项目并将其移动到列表的末尾。主要的区别在于,我们不是扫描整个列表来查找最大的项目,而是将列表转换为最大堆父节点的值总是大于子节点,反之最小堆以加快速度。

注意:堆一定是一棵完全二叉树;

 

Heapify堆化

我们的第一步是将输入列表转换为堆(也称为“堆化”它)

  1. 把数列的数值视为完全二叉树的结点(从0开始)
  2. 从倒数第二层开始,进行heapify,即父节点与子节点依次比较,把最大值交换到父节点
  3. 以此类推,使这颗完全二叉树符合最大堆的性质

规律:父节点的下标 = (i-1)/ 2    例如下图:数值7的下标为3,其父节点的下标为(3-1)/2=1;

   左子节点的下标 = 2*i+1      例:数值2的下标为2,其左子节点的下标为  (2*2)+1=5;

   右子节点的下标= 2*i+2        例:数值2的下标为2,其左子节点的下标为  (2*2)+2=6;

以此示例输入:

 

                                                        

我们可以将它视为完全二叉树中的节点, 而不是像列表那样处理输入。该第0 个位置在堆是根; 

                                                   

  • 我们实际上并没有在这里创建一棵树。我们只是处理输入,就像它指定树中的节点一样。
  • 当我们通过移动项目或移除元素来操纵树时,我们实际上正在 重新排列基础输入列表(而不是一些单独的树结构)。
  • 我们将向树都 该名单在我们的图表,但树只是为了帮助我们想象我们如何解释列表作为一个堆。在内存中,我们只是存储 列表。

我们的树需要一些修正才能使它成为一个有效的堆。例如,它现在肯定无效,因为最大的元素(9)不在根。

要将树转换为堆,我们将从树的底部向上工作。我们将每个节点与其子节点进行比较并移动节点,以使父节点始终大于其子节点。

这会导致较小的节点在树中向下移动,“ 向下冒泡 ”以允许较大的值到达顶部。

  1. 首先,让我们看看叶子。叶节点没有任何子节点,因此它们根本不需要向下移动。                                                             

                                        

2.让我们看看下一级别的节点:

                                               

我们将从左节点(3)及其子节点开始:由于7和9都大于3,我们肯定需要移动。我们将交换3和9以使父母大于其子女。(如果我们将3与7交换,那么我们仍然会遇到问题,因为父节点(7)将小于其子节点(9)

                                                        

接下来,我们将查看正确的节点(2)及其子节点。由于4大于2,我们将交换它们。

                                                            

向上移动,我们在根部有一个8。

                                                                    

因为8小于9,所以8个气泡向下,与更大的孩子交换位置:9。

                


然后,我们需要将8与其两个孩子-7和3进行比较。由于8比他们两个都大,我们已经完成了冒泡并且不需要进行任何额外的交换。

此时,我们已将输入树转换为有效的最大堆

 

HeapSort堆排序:

反复删除最大值:

每次我们从堆中删除一个元素时,它都是底层列表中的最大项。因此,按排序顺序,它属于列表的末尾。正如我们将看到的,从堆中删除元素可以方便地释放底层列表末尾的空间,我们可以在其中放置已删除的元素。

我们删除max元素:

                                                               

这留下了需要填补根的空白。我们将最后一个元素放在那里。

                 

但是这样之后就不是一个有效堆了,所以我们要进行heapify,把它堆化,堆化完成后把9放入数组的末尾;如图:

                                                    

下一个最大的元素是8.我们将它从根中移除,用最底部的最右边的元素(1)填充它的位置

                                                             

再次heapify,堆化它,

                                                     

以此类推:7,4,3,2,1;

 

复杂度:

对于heapify步骤,我们检查树中的每个元素并将其向下移动,直到它比其子项大。因为我们的树高是O(l g(n)),我们可以做到(l g())移动。所以n个节点,总的时间复杂度为的nl g())。

我们已经证明了heapify步骤是 为nl g())。通过 更复杂的分析,事实证明它实际上是 上)

将树转换为堆后,我们删除所有的n个元素 - 一次一个元素。从堆中删除需要(l g())时间,因为我们必须将新值移动到堆的根并向下冒泡。所以删除操作为nl g())时间。

更彻底的分析表明这样做 删除仍然是为nl g())。 

把这些步骤放在一起,nl g())为最坏情况下(平均)的时间。

每次我们从树根中删除一个元素时,替换它的元素根本不会向下冒泡。在这种情况下,每次删除都需要O(1)时间,并做 n次删除操作)。

所以,最好的情况下,时间复杂度为 。当输入中的所有内容相同时。

向量O(1)整体空间用于堆垛。

代码:

对于代码部分,我说一下:

有一个前提:对一个节点做heapify的时候,必须保证它的所有子树都已经是堆。
所以,在这个前提下,如果要做heapify的节点已经符合“父节点 > 子节点”的性质,那么这就已经是一个堆了;就没有必要往下走了。
另外,我们的build_heap函数是从最后一个不是叶节点的点开始往前做heapify操作的,所以最后是可以形成一个堆

 

对于函数Build_Heap()

例如:

如果我们要做heapify,肯定是从下标3开始(因为3结点是最下面的有两个子节点的父节点),然后是2,1,0;

所以才有了这个循环:

	for (int i = n - 1; i >= 0; i--)        
	{
		swap(arr[0], arr[i]);
		Heapify(arr, i, 0);
	}

               

#include<iostream>
using namespace std;

void Heapify(int* tree, int n, int m)  //tree表示数组,n表示数组长度,m表示对第几个结点进行heapify操作
{
	if (m >= n) return;   //递归出口
	int c1 = 2 * m + 1;
	int c2 = 2 * m + 2;
	int max = m;               //假设m为最大值
	if (c1<n && tree[c1] > tree[max])        //比较左子节点与父节点
		max = c1; 
	if (c2<n && tree[c2] > tree[max])       //比较右子节点与刚刚比较完之后的父节点进行比较
		max = c2;
	if (max != m) {                          
		swap(tree[max], tree[m]);
		Heapify(tree, n, max);
	}
		
}


void Build_Heap(int* arr, int n)     //arr为数组,n为数组长度
{
	int last_node = n - 1;          //堆的最后一个结点
	int last_heapify_node = (n-1)/2;   //最后一个结点的父节点,我们需要从这里开始heapify
	for (int i = last_heapify_node; i >= 0; i--)
	{
		Heapify(arr, n, i);
	}

}

void heap_sort(int* arr, int n)              //排序
{
	Build_Heap(arr, n);
	for (int i = n - 1; i >= 0; i--)        
	{
		swap(arr[0], arr[i]);
		Heapify(arr, i, 0);
	}
}

void Show(int* arr, int n)           //输出
{
	for (int i = 0; i < n; i++)
	{
		cout << arr[i] << ",";
	}
	cout << endl;
}

int main()
{
	int arr[10] = {1,4,2,6,8,3,9,5,0,7 };       //前提:对一个节点做heapify的时候,必须保证它的所有子树都已经是堆。
	heap_sort(arr, 10);
	Show(arr, 10);
	return 0;
}

 

 

十大经典算法复杂度及稳定性比较:https://blog.csdn.net/alzzw/article/details/98100378

冒泡排序:https://blog.csdn.net/alzzw/article/details/97906690

选择排序:https://blog.csdn.net/alzzw/article/details/97964320

插入排序:https://blog.csdn.net/alzzw/article/details/97967278

快速排序:https://blog.csdn.net/alzzw/article/details/97970371

归并排序:https://blog.csdn.net/alzzw/article/details/98047030

基数排序:https://blog.csdn.net/alzzw/article/details/98240042

计数排序:https://blog.csdn.net/alzzw/article/details/98245871

 

 

 

 

 

  • 8
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

阿尔兹

如果觉得有用就推荐给你的朋友吧

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值