排序算法:堆排序算法实现及分析

堆排序介绍

堆排序(Heap Sort)就来利用堆(假设利用大顶堆)进行排序的方法。它的基本思想是,将待排序的序列构成一个大顶堆。此时,整个序列的最大值就是堆顶的根结点。将它移走(其实就是将其与堆数组的末尾元素交换,此时末尾元素就是最大值),然后将剩余的n-1个序列重新构造成一个堆,这样就会得到n个元素中的次小值。如何反复执行,便能得到一个有序序列了。

定义看懂没有?没看懂没关系,下面看图解。

堆排序图解

我们先来说说堆,我们这来看大顶堆,说大顶堆前,我先看 一个数组如何模拟成树的。请看下图,将数组元素从下标0开始,看做完全二叉树结点,请以层序遍历的角度看待它


我先来构造大顶堆,在构造堆之前,我们先看下堆的定义堆是具有下列性质的完全二叉树每个结点的值都大于或等于其左右孩子结点的值,称为大顶堆;或者每个结点的值都小于或等于其左右孩子结点的值,称为小顶堆我们从后往前将每个结点构成一个大顶堆不就得了?想想不是呢?因为最下层的结点已经满足堆条件,然后调整上层结点,使其满足大顶堆条件,那么整个完全二叉树不就成了大顶堆了么。我们应该从后面的那个结点开始呢?当然是从非叶子结点 9 开始呀,因为叶子结点没有孩子结点呗。将结点9看做一棵树将其调整问堆,然后将3位根结点的树调整为堆,然后将2为根结点的树调整为堆........,整棵完全二叉树就调整好了,成为了一个大顶堆。请看下图的代码和说明。



将堆的初始化看懂了,就已经成功了一大半了。还有一小部分就是 将最值(下标为0)放到序列的尾部,然后将剩下的元素继续进行堆调整,堆顶(下标为0)的元素 又是次最值,将其放到尾部,这样一直循环 直到将每个元素放到堆顶,则堆排序完毕。

堆排序代码

下面请堆排序代码,然后再进行解说。

//堆调整 大堆顶,将最大值放在根结点
void BigHeadAdjust(int *arr,int index,int length)
{
	int lchild = 2 * index + 1;
	int rchild = 2 * index + 2;
	int max = index;
	if (lchild<length&&arr[lchild]>arr[max])
	{
		max = lchild;
	}
	if (rchild<length&&arr[rchild]>arr[max])
	{
		max = rchild;
	}
	if (max != index)
	{
		Swap(&arr[max], &arr[index]);
		BigHeadAdjust(arr, max, length);
	}
	return;
}

//堆排序,采用大顶堆 升序
void HeapSort_Up(int *arr, int length)
{
	//初始化堆,将每个非叶子结点倒叙进行大顶堆调整。
	//循环完毕 初始大顶堆(每个根结点都比它孩子结点值大)形成
	for (int i = length / 2 - 1; i >= 0; i--)
	{
		BigHeadAdjust(arr, i, length);
	}
	printf("大堆顶初始化顺序:");
	PrintArr(arr, length);
	//将堆顶值放到数组尾部,然后又进行大堆顶调整,一次堆调整最值就到堆顶了。 
	for (int i = length - 1; i >= 0; i--)
	{
		Swap(&arr[0], &arr[i]);
		BigHeadAdjust(arr, 0, i);
	}
	return;
}

细心的朋友,会发现初始化化堆 是从非叶子结点从后往前进行调整为堆,而后序的堆调整 都是从下标为0处开始调整为堆因为起初的完全二叉树是完全无效的,所有只能从后往前调整。在初始化完堆之后,尽管将最值移动到尾部,打乱了堆,因为原来的堆结构已经基本形成,只需要递归调用BigHeadAdjust即可

堆排序复杂度分析

堆排序,它的运行时间主要是消耗在构建堆和在重建堆时的反复筛选上。在构建堆的过程,因为我们是从完全二叉树最下层的非叶子结点开始构建的,将它与其孩子结点进行比较和有必要的互换,对于每个非叶子结点来说,其实最多2次比较和互换,故初始化堆的时间复杂度为O(n)。在正式排序的时候,第i次取堆顶记录和重建堆需要O(logi)的时间(完全二叉树的某个结点到根结点的距离为log2i+1),并且需要取n-1次堆顶记录,因此重建堆的时间复杂度为O(nlogn)。所以总的来说,堆排序的时间复杂度为O(nlogn)。由于堆排序对元素记录的排序状态不敏感,因此它无论最好,最坏,和平均时间复杂度均为O(nlogn)

完整代码

#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <sys/timeb.h>
#define MAXSIZE 1000000
//交换值
void Swap(int* a, int* b)
{
	int temp = *a;
	*a = *b;
	*b = temp;
}
//打印数组元素
void PrintArr(int* arr, int length)
{
	for (int i = 0; i < length; i++)
	{
		printf("%d ", arr[i]);
	}
	printf("\n");
	return;
}
long GetSysTime()
{
	struct timeb tb;
	ftime(&tb);
	return tb.time * 1000 + tb.millitm;
}

//堆调整 大堆顶,将最大值放在根结点
void BigHeadAdjust(int *arr,int index,int length)
{
	int lchild = 2 * index + 1;
	int rchild = 2 * index + 2;
	int max = index;
	if (lchild<length&&arr[lchild]>arr[max])
	{
		max = lchild;
	}
	if (rchild<length&&arr[rchild]>arr[max])
	{
		max = rchild;
	}
	if (max != index)
	{
		Swap(&arr[max], &arr[index]);
		BigHeadAdjust(arr, max, length);
	}
	return;
}

//堆排序,采用大顶堆 升序
void HeapSort_Up(int *arr, int length)
{
	//初始化堆,将每个非叶子结点倒叙进行大顶堆调整。
	//循环完毕 初始大顶堆(每个根结点都比它孩子结点值大)形成
	for (int i = length / 2 - 1; i >= 0; i--)
	{
		BigHeadAdjust(arr, i, length);
	}
	//printf("大堆顶初始化顺序:");
	//PrintArr(arr, length);
	//将堆顶值放到数组尾部,然后又进行大堆顶调整,一次堆调整最值就到堆顶了。 
	for (int i = length - 1; i >= 0; i--)
	{
		Swap(&arr[0], &arr[i]);
		BigHeadAdjust(arr, 0, i);
	}
	return;
}

//堆调整 小堆顶,将最小值放在根结点
void SmallHeadAdjust(int *arr, int index, int length)
{
	int lchild = 2 * index + 1;
	int rchild = 2 * index + 2;
	int min = index;
	if (lchild<length&&arr[lchild]<arr[min])
	{
		min = lchild;
	}
	if (rchild<length&&arr[rchild]<arr[min])
	{
		min = rchild;
	}
	if (min != index)
	{
		Swap(&arr[min], &arr[index]);
		SmallHeadAdjust(arr, min, length);
	}
	return;
}

//堆排序,采用小顶堆 降序
void HeapSort_Down(int *arr, int length)
{
	for (int i = length / 2 - 1; i >= 0; i--)
	{
		SmallHeadAdjust(arr, i, length);
	}
	for (int i = length - 1; i >= 0; i--)
	{
		Swap(&arr[0], &arr[i]);
		SmallHeadAdjust(arr, 0, i);
	}
	return;
}
//希尔排序 升序
//根据插入排序的原理,将原来的一个大组,采用间隔的形式分成很多小组,分别进行插入排序
//每一轮结束后 继续分成更小的组进行 插入排序,直到分成的小组长度为1,说明插入排序完毕
void ShellSort_Up(int* arr, int length)
{
	int increase = length;
	int i, j, k, temp;
	do
	{
		increase = increase / 3 + 1;//每个小组的长度
									//每个小组的第0个元素
		for (i = 0; i < increase; i++)
		{
			//对每个小组进行插入排序,因为是间隔的形式分组,每个小组下一个元素为 j+=increse
			for (j = i + increase; j < length; j += increase)
			{
				temp = arr[j];//待插入元素

				for (k = j - increase; k >= 0 && temp < arr[k]; k -= increase)
				{
					arr[k + increase] = arr[k];
				}
				arr[k + increase] = temp;
			}
		}
	} while (increase>1);
}

int main(int argc, char *argv[])
{
	srand((size_t)time(NULL));//设置随机种子
	int arr[10] = { 6,8,2,3,9,7,4,1,5,10 };
	int *arr2 = (int*)malloc(sizeof(int)*MAXSIZE);
	int *arr3 = (int*)malloc(sizeof(int)*MAXSIZE);
	//给每个元素设置一个随机值
	for (int i = 0; i < MAXSIZE; i++)
	{
		int num = rand() % MAXSIZE;
		arr2[i] = num;
		arr3[i] = num;
	}
	//printf("排序前:\n");
	//PrintArr(arr, 10);
	//printf("堆排序升序:\n");
	//HeapSort_Up(arr, 10);
	//PrintArr(arr, 10);
	//printf("堆排序降序:\n");
	//HeapSort_Down(arr, 10);
	//PrintArr(arr, 10);

	long start1 = GetSysTime();
	ShellSort_Up(arr2, MAXSIZE);
	long end1 = GetSysTime();
	printf("%d个元素 希尔排序耗费%d毫秒\n",MAXSIZE,end1-start1);

	long start2 = GetSysTime();
	HeapSort_Up(arr3, MAXSIZE);
	long end2 = GetSysTime();
	printf("%d个元素 堆排序耗费%d毫秒\n", MAXSIZE, end2 - start2);
	return 0;
}

运行结果检测

正确性验证


和希尔排序比较

排序100万个数据,比较下他们的效率。希尔排序居然比堆排序高,运行了好几遍都是这个结果。。。





  • 10
    点赞
  • 64
    收藏
    觉得还不错? 一键收藏
  • 17
    评论
选择排序算法: 1. 从数组中选择最小的元素,将其放置在数组的起始位置。 2. 在剩余的元素中选择最小的元素,将其放置在已排序元素的末尾。 3. 重复步骤2直到所有元素都已排序。 代码实现: ``` function selectionSort(arr) { for (let i = 0; i < arr.length - 1; i++) { let minIndex = i; for (let j = i + 1; j < arr.length; j++) { if (arr[j] < arr[minIndex]) { minIndex = j; } } if (minIndex !== i) { [arr[i], arr[minIndex]] = [arr[minIndex], arr[i]]; } } return arr; } ``` 堆排序算法: 1. 构建一个最大堆(或最小堆)。 2. 将堆顶元素与最后一个元素交换,然后将堆的大小减1。 3. 对新的堆顶元素进行堆化,重复步骤2和3直到堆的大小为1。 代码实现: ``` function heapSort(arr) { buildMaxHeap(arr); let heapSize = arr.length; for (let i = arr.length - 1; i > 0; i--) { [arr[0], arr[i]] = [arr[i], arr[0]]; heapSize--; maxHeapify(arr, 0, heapSize); } return arr; } function buildMaxHeap(arr) { const heapSize = arr.length; for (let i = Math.floor(heapSize / 2); i >= 0; i--) { maxHeapify(arr, i, heapSize); } } function maxHeapify(arr, i, heapSize) { const left = 2 * i + 1; const right = 2 * i + 2; let largest = i; if (left < heapSize && arr[left] > arr[largest]) { largest = left; } if (right < heapSize && arr[right] > arr[largest]) { largest = right; } if (largest !== i) { [arr[i], arr[largest]] = [arr[largest], arr[i]]; maxHeapify(arr, largest, heapSize); } } ``` 选择排序算法的时间复杂度为$O(n^2)$,堆排序算法的时间复杂度为$O(n\log n)$。虽然堆排序的时间复杂度比选择排序更优,但是堆排序的常数项较大,因此在实际应用中需要根据具体情况选择合适的排序算法

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值