彻底理解堆排序

本文详细介绍了堆排序的过程,包括如何构建大根堆,并通过调整堆来实现排序。文章提供了具体的算法步骤和性能分析,同时附带了C++实现代码。

堆排序过程

堆分为大根堆和小根堆,是完全二叉树。大根堆的要求是每个节点的值都不大于其父节点的值,即A[PARENT[i]] >= A[i]。在数组的非降序排序中,需要使用的就是大根堆,因为根据大根堆的要求可知,最大的值一定在堆顶。

既然是堆排序,自然需要先建立一个堆,而建堆的核心内容是调整堆,使二叉树满足堆的定义(每个节点的值都不大于其父节点的值)。调堆的过程应该从最后一个非叶子节点开始,假设有数组A = {1, 3, 4, 5, 7, 2, 6, 8, 0}。那么调堆的过程如下图,数组下标从0开始,A[3] = 5开始。分别与左孩子和右孩子比较大小,如果A[3]最大,则不用调整,否则和孩子中的值最大的一个交换位置,在图1中是A[7] > A[3] > A[8],所以A[3]与A[7]对换,从图1.1转到图1.2。

image

所以建堆的过程就是

   1: for ( i = headLen/2; i >= 0; i++)
   2: 
   3:        do AdjustHeap(A, heapLen, i)

调堆:如果初始数组是非降序排序,那么就不需要调堆,直接就满足堆的定义,此为最好情况,运行时间为Θ(1);如果初始数组是如图1.5,只有A[0] = 1不满足堆的定义,经过与子节点的比较调整到图1.6,但是图1.6仍然不满足堆的定义,所以要递归调整,一直到满足堆的定义或者到堆底为止。如果递归调堆到堆底才结束,那么是最坏情况,运行时间为O(h) (h为需要调整的节点的高度,堆底高度为0,堆顶高度为floor(logn) )。

建堆完成之后,堆如图1.7是个大根堆。将A[0] = 8 与 A[heapLen-1]交换,然后heapLen减一,如图2.1,然后AdjustHeap(A, heapLen-1, 0),如图2.2。如此交换堆的第一个元

素和堆的最后一个元素,然后堆的大小heapLen减一,对堆的大小为heapLen的堆进行调堆,如此循环,直到heapLen == 1时停止,最后得出结果如图3。

image

image

   1: /*
   2:     输入:数组A,堆的长度hLen,以及需要调整的节点i
   3:     功能:调堆
   4: */
   5: 
   6: void AdjustHeap(int A[], int hLen, int i)
   7: {
   8:     int left = LeftChild(i);  //节点i的左孩子
   9:     int right = RightChild(i); //节点i的右孩子节点
  10:     int largest = i;
  11:     int temp;
  12: 
  13:     while(left < hLen || right < hLen)
  14:     {
  15:         if (left < hLen && A[largest] < A[left])
  16:         {
  17:             largest = left;
  18:         }
  19:         
  20:         if (right < hLen && A[largest] < A[right])
  21:         {
  22:             largest = right;
  23:         }
  24: 
  25:         if (i != largest)   //如果最大值不是父节点
  26:         {
  27:              temp = A[largest]; //交换父节点和和拥有最大值的子节点交换
  28:              A[largest] = A[i];
  29:              A[i] = temp;
  30: 
  31:             i = largest;         //新的父节点,以备迭代调堆
  32:             left = LeftChild(i);  //新的子节点
  33:             right = RightChild(i);
  34:         }
  35:         else
  36:         {
  37:             break;
  38:         }
  39:     }
  40: }
  41: 
  42: /*
  43:     输入:数组A,堆的大小hLen
  44:     功能:建堆
  45: */
  46: void BuildHeap(int A[], int hLen)
  47: {
  48:     int i;
  49:     int begin = hLen/2 - 1;  //最后一个非叶子节点
  50:     for (i = begin; i >= 0; i--)
  51:     {
  52:         AdjustHeap(A, hLen, i);  
  53:     }
  54: }
  55: 
  56: /*
  57:     输入:数组A,待排序数组的大小aLen
  58:     功能:堆排序
  59: */
  60: void HeapSort(int A[], int aLen)
  61: {
  62:     int hLen = aLen;
  63:     int temp;
  64: 
  65:     BuildHeap(A, hLen);      //建堆
  66: 
  67:     while (hLen > 1)
  68:     {
  69:         temp = A[hLen-1];    //交换堆的第一个元素和堆的最后一个元素
  70:         A[hLen-1] = A[0];
  71:         A[0] = temp;
  72:         hLen--;        //堆的大小减一
  73:         AdjustHeap(A, hLen, 0);  //调堆
  74:     }
  75: }

性能分析

  • 调堆:上面已经分析了,调堆的运行时间为O(h)。
  • 建堆:每一层最多的节点个数为n1 = ceil(n/(2^(h+1))),

image

因此,建堆的运行时间是O(n)。

  • 循环调堆(代码67-74),因为需要调堆的是堆顶元素,所以运行时间是O(h) = O(floor(logn))。所以循环调堆的运行时间为O(nlogn)。

总运行时间T(n) = O(nlogn) + O(n) = O(nlogn)。对于堆排序的最好情况与最坏情况的运行时间,因为最坏与最好的输入都只是影响建堆的运行时间O(1)或者O(n),而在总体时间中占重要比例的是循环调堆的过程,即O(nlogn) + O(1) =O(nlogn) + O(n) = O(nlogn)。因此最好或者最坏情况下,堆排序的运行时间都是O(nlogn)。而且堆排序还是原地算法(in-place algorithm)

下面贴出堆排序的算法:

// HeapSort.cpp : 定义控制台应用程序的入口点。
//

#include "stdafx.h"
#include <iostream>
using namespace std;

//以下是针对堆进行调整
void HeapAjust(int data[],int i,int length)
{
int nChild;
int nTemp;
for(nTemp=data[i];2*i+1<length;i=nChild)
{
nChild=2*i+1;
if(nChild<length-1&&data[nChild+1]>data[nChild])//比较哪个孩子比自己大,如果是右孩子的话,就要将nChild++;
{
nChild++;
}

if(nTemp<data[nChild])//如果比自己的最大的孩子小,就交换
{
data[i]=data[nChild];
data[nChild]=nTemp;
}
else//如果比最大的孩子还大,就不交换
break;
}
}

//堆排序
void HeapSort(int data[],int length)
{
for(int i=(length>>1)-1;i>=0;i--)//注意这个地方:i=(length>>1)-1,加上括号,原因:优先级的问题
{
HeapAjust(data,i,length);//初始化一个堆
}
for(int j=length-1;j>0;--j)
{
int temp=data[j];//最大的和最小的数据进行交换
data[j]=data[0];
data[0]=temp;
HeapAjust(data,0,j);//将堆进行重新调整
}
}

int _tmain(int argc, _TCHAR* argv[])
{
int data[]={2,3,1,5,6,-1 };
int len = sizeof(data)/sizeof(int);
HeapSort(data,len);
for (int i=0;i<len;i++)
{
cout<<data[i]<<" ";
}

system("pause");
return 0;
}

// HeapSort.cpp : 定义控制台应用程序的入口点。
//

#include "stdafx.h"
#include <iostream>
using namespace std;

//以下是针对堆进行调整
void HeapAjust(int data[],int i,int length)
{
	int nChild;
	int nTemp;
	for(nTemp=data[i];2*i+1<length;i=nChild)
	{
		nChild=2*i+1;
		if(nChild<length-1&&data[nChild+1]>data[nChild])//比较哪个孩子比自己大,如果是右孩子的话,就要将nChild++;
		{
			nChild++;
		}

		if(nTemp<data[nChild])//如果比自己的最大的孩子小,就交换
		{
			data[i]=data[nChild];
			data[nChild]=nTemp;
		}
		else//如果比最大的孩子还大,就不交换
			break;
	}
}

//堆排序
void HeapSort(int data[],int length)
{
	for(int i=(length>>1)-1;i>=0;i--)//注意这个地方:i=(length>>1)-1,加上括号,原因:优先级的问题
	{
		HeapAjust(data,i,length);//初始化一个堆
	}
	for(int j=length-1;j>0;--j)
	{
		int temp=data[j];//最大的和最小的数据进行交换
		data[j]=data[0];
		data[0]=temp;
		HeapAjust(data,0,j);//将堆进行重新调整
	}
}

int _tmain(int argc, _TCHAR* argv[])
{
	int data[]={2,3,1,5,6,-1 };
	int len = sizeof(data)/sizeof(int);
	HeapSort(data,len);
	for (int i=0;i<len;i++)
	{
		cout<<data[i]<<" "; 
	}

	system("pause");
	return 0;
}


<think>好的,我现在需要详细解释如何使用C++实现堆排序算法,并且要分步骤进行说明。首先,我需要回顾堆排序的基本原理,确保自己理解正确。堆排序是一种基于二叉堆数据结构的排序算法,属于选择排序的一种。它的主要步骤包括构建堆(通常是最大堆或最小堆),然后反复取出堆顶元素(最大或最小值),并将其放到数组的末尾,调整剩余元素使其重新成为堆,直到所有元素有序。 接下来,我需要考虑如何在C++中具体实现这个算法。首先,堆通常用数组来表示,因为二叉堆是一个完全二叉树,数组结构可以很好地对应其层次结构。对于最大堆,每个父节点的值都大于或等于其子节点的值。堆排序的关键操作包括堆的构建(heapify)和堆的调整。 我应该从如何将无序数组构建成最大堆开始。构建堆的过程通常是从最后一个非叶子节点开始,向上调整每个节点,确保其子树满足堆的性质。最后一个非叶子节点的位置可以通过数组长度计算得到,通常是(n/2)-1,这里n是数组的长度。 然后,堆排序的主要步骤是重复将堆顶元素(即当前最大值)数组末尾元素交换,然后减少堆的大小,并对新的堆顶元素进行下沉调整,以维持堆的性质。这个过程需要循环执行,直到堆的大小为1,此时数组已经有序。 在编写具体的C++代码时,需要注意几个关键函数:一个是heapify函数,用于调整某个节点及其子树成为最大堆;另一个是堆排序的主函数,负责构建初始堆并执行排序过程。 接下来,我需要考虑如何编写heapify函数。该函数需要接收数组、堆的大小以及当前节点的索引作为参数。函数会比较当前节点其左右子节点的值,如果子节点的值更大,则交换它们,并递归调整受影响的子树。这个过程需要循环或递归实现,但为了效率,通常使用循环结构。 然后是堆排序的主函数。首先,需要构建初始的最大堆,从最后一个非叶子节点开始向前遍历,逐个调用heapify函数。构建完成后,开始交换堆顶元素和当前堆的末尾元素,每交换一次,堆的大小减一,并对新的堆顶元素进行heapify操作,以恢复堆的结构。 此外,需要注意数组的索引是否正确。在C++中,数组通常从0开始索引,而堆的父节点和子节点之间的关系需要正确计算。例如,对于节点i,其左子节点是2*i + 1,右子节点是2*i + 2,父节点是(i-1)/2。 在代码实现时,可能会有一些常见的错误,比如数组越界、循环条件不正确或者堆调整不彻底。因此,需要仔细检查索引的计算和循环的终止条件。例如,在heapify函数中,需要确保子节点的索引不超过当前堆的大小。 测试代码也是重要的一环。可以使用一个示例数组,如{12, 11, 13, 5, 6, 7},运行堆排序后,检查是否得到有序数组。此外,可以测试边界情况,如空数组、只有一个元素的数组,或者已经有序的数组,以确保代码的鲁棒性。 最后,需要考虑时间复杂度和空间复杂度。堆排序的时间复杂度是O(n log n),无论是最好、最坏还是平均情况。空间复杂度是O(1),因为排序是原地进行的,不需要额外的存储空间。 总结一下,实现堆排序的步骤包括: 1. 构建最大堆:从最后一个非叶子节点开始,向上调整每个节点。 2. 交换堆顶元素和当前堆的末尾元素,缩小堆的范围。 3. 对新的堆顶元素进行heapify操作,恢复堆的结构。 4. 重复步骤2和3,直到堆的大小为1。 现在,我需要将这些思路转化为具体的C++代码,并确保每一部分都清晰且正确。同时,在解释代码时,要分步骤说明每个函数的作用和实现细节,帮助用户理解堆排序的运作机制。</think>以下是用C++实现堆排序的详细步骤和代码解析: ### 一、算法原理 堆排序是基于完全二叉树结构的排序算法,时间复杂度为$O(n \log n)$。核心步骤: 1. 构建最大堆(父节点值 ≥ 子节点) 2. 重复交换堆顶(最大值)堆尾,缩小堆范围 3. 每次交换后调整堆结构 ### 二、实现步骤 ```cpp #include <iostream> using namespace std; // 堆调整函数(下沉操作) void heapify(int arr[], int n, int i) { int largest = i; // 初始化最大值为当前节点 int left = 2 * i + 1; // 左子节点索引 int right = 2 * i + 2; // 右子节点索引 // 比较左子节点 if (left < n && arr[left] > arr[largest]) largest = left; // 比较右子节点 if (right < n && arr[right] > arr[largest]) largest = right; // 若最大值不是当前节点 if (largest != i) { swap(arr[i], arr[largest]); // 交换节点值 heapify(arr, n, largest); // 递归调整受影响的子树 } } // 堆排序主函数 void heapSort(int arr[], int n) { // 1. 构建最大堆(从最后一个非叶子节点开始) for (int i = n / 2 - 1; i >= 0; i--) heapify(arr, n, i); // 2. 逐个提取堆顶元素 for (int i = n - 1; i > 0; i--) { swap(arr[0], arr[i]); // 将最大值移到数组末尾 heapify(arr, i, 0); // 调整剩余元素的堆结构 } } // 测试代码 int main() { int arr[] = {12, 11, 13, 5, 6, 7}; int n = sizeof(arr)/sizeof(arr[0]); heapSort(arr, n); cout << "排序结果: "; for (int i=0; i<n; i++) cout << arr[i] << " "; return 0; } ``` ### 三、关键点解析 1. **堆的表示**: - 使用数组模拟完全二叉树 - 节点关系: - 父节点索引:$(i-1)/2$ - 左子节点:$2i+1$ - 右子节点:$2i+2$ 2. **heapify函数**: - 时间复杂度:$O(\log n)$ - 递归进行下沉操作,确保子树符合堆性质 - 通过比较父子节点找到最大值 3. **构建初始堆**: - 从最后一个非叶子节点(索引$n/2-1$)开始调整 - 自底向上保证每个子树都是最大堆 4. **排序过程**: - 每次将堆顶(arr[0])当前堆尾交换 - 堆大小减1(i--) - 对新堆顶执行heapify ### 四、执行流程示例 初始数组:$[12, 11, 13, 5, 6, 7]$ 1. 构建最大堆后:$[13, 11, 12, 5, 6, 7]$ 2. 第一次交换后:$[7, 11, 12, 5, 6, 13]$,调整堆得到$[12, 11, 7, 5, 6]$ 3. 继续交换和调整,最终得到升序排列 ### 五、复杂度分析 - 时间复杂度: - 构建堆:$O(n)$ - 每次heapify:$O(\log n)$,执行$n-1$次 - 总体:$O(n \log n)$ - 空间复杂度:$O(1)$(原地排序) ### 六、优缺点 **优点**: - 最坏情况下仍保持$O(n \log n)$ - 不需要额外存储空间 **缺点**: - 不稳定排序 - 对缓存不友好(跳跃访问)
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值