【算法复习】堆排序

本文详细介绍了堆排序的概念、步骤及其实现方法,通过图解和代码示例,清晰地展示了如何利用大根堆或小根堆对数组进行排序,并提供了完整的C++实现。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

算法复习-堆排序(图解和代码)

一、 概念介绍

1.堆是一棵完全二叉树,常可以用一个一维数组表示。
2.如果一个节点在数组中的下标为k,则其左孩子下标=2k+1,右孩子下标=2k+2。
3.如果一个堆的长度为n,则其最后一个非叶子节点的下标=n/2-1。

大根堆:

如果一个堆的所有节点都大于其子节点,则称为大根堆。

小根堆:

如果一个堆的所有节点都小于其子节点,则称为小根堆。

二、 堆排序

堆排序利用了堆的性质,因此得名。
堆排序的步骤为:
1.将待排序数组调整为堆。
2.交换堆顶元素和堆尾元素,堆的长度-1
3.自顶向下重新调堆
4.重复2~3,直到堆的长度=0。

如果要将数组排为升序,则调堆时调成大根堆;反之则调为小根堆。

三、 调堆

上模堆排序的算法看起来很简单,其实现的一个关键函数是调堆函数,即将一个无序数组调整为堆的过程。

调堆分为两种:

  1. 初始化堆(调整成堆)
  2. 交换首尾元素后的调堆

其中调堆过程相对简单,因为此时数组已被调成堆了,只是由于交换了首尾元素,第一个元素不满足堆的性质。因此只需从堆首开始,比较其和左右孩子的大小,将其与左右孩子中较大者交换;此过程可以递归执行,直到堆首元素下沉到可以满足大于左右孩子时的位置。
而初始化堆则需要从初始堆的最后一个非叶子节点开始,将此节点看做一个子堆进行调堆,一直调到数组的第一个元素。这时整个堆和所有的子堆就都满足了大根堆(小根堆)的性质。

四、 图解:

初始堆
上图是初始堆,对应数组int a[] = {16, 7, 3, 20, 17, 8},接下来我们进行“人工”的堆排序,以显示调堆的过程。
第一步:调整成堆
(1)找到最后一个非叶子节点,根据堆的第3个性质,计算可得,最后一个非叶子节点下标=n/2-1=6/2-1=2,即a[2],对应元素进行调堆。调整之后如图。
在这里插入图片描述
(2)从a[2]开始,依次再对a[1]、a[0]进行调堆。
在这里插入图片描述在这里插入图片描述
此时数组a[]={20,16,8,7,17,3},每个节点都大于它的左右孩子节点,已经满足了一颗大根堆的定义。

第二步:交换堆首尾节点的值,交换后堆的长度减一
在这里插入图片描述
此时数组a[]={3,17,8,7,16,20},其中属于堆的部分为{3,17,8,7,16},20这个元素已经脱离堆。
第三步: 将交换后的堆进行调堆(自顶向下)
在这里插入图片描述在这里插入图片描述
第四步:重复2、3:

至此,a[]={3,7,8,16,17,20},完成排序。
五、c++实现代码
#include "pch.h"
#include <iostream>
using namespace std;

void toHeap(int a[], int length);
void outArray(int a[], int length);
void swap(int *a, int *b);
int getLastNotLeafIndex(int length);
int getLeftChildIndex(int length, int index);
int getRightChildIndex(int length, int index);
void adjustHeap(int a[], int length, int k);
int getMaxChildIndex(int a[], int l, int r);

int main()
{
	int k = 10;
	int a[] = {22,4,5,1,7,9,8, 13, 1, 78};
	toHeap(a, k);
	//swap(&a[0], &a[k - 1]);
	int i = 0;
	while(i < k) {
		adjustHeap(a, k-i, 0);
		swap(&a[0], &a[k-i -1]);
		i++;
	}
	cout << "排序过后:";
	outArray(a, k);
}

//交换两个指针的值
void swap(int *a, int *b) {
	int temp = *a;
	*a = *b;
	*b = temp;
}

/*
  调整成堆
*/
void toHeap(int a[], int length) {
	//todo
	int lastNotLeafIndex = getLastNotLeafIndex(length);
	for (int i = lastNotLeafIndex; i >= 0; i--) {
		adjustHeap(a, length, i);
	}
}

// 调整堆
void adjustHeap(int a[], int length, int k) {
	int l = getLeftChildIndex(length, k);
	int r = getRightChildIndex(length, k);
	int swapIndex = getMaxChildIndex(a, l, r);
	
	if (swapIndex >= 0 && a[k] < a[swapIndex]) {
		swap(a[k], a[swapIndex]);
		adjustHeap(a, length, swapIndex);
	}
}

//获取左右孩子最大者的index
int getMaxChildIndex(int a[], int l, int r) {
	if (l >= 0 && r >= 0) {
		return a[l] > a[r] ? l : r;
	}
	else if (l >= 0 && r < 0) {
		return l;
	}
	else if (l < 0 && r >= 0) {
		return r;
	}
	return -1;
}

// 获取最后一个非叶子节点的index
int getLastNotLeafIndex(int length) {
	return length / 2 - 1;
}

//获取左孩子index
int getLeftChildIndex(int length, int index) {
	int temp = 2 * index + 1;
	return temp >= length ? -1 : temp;
}

//获取右孩子index
int getRightChildIndex(int length, int index) {
	int temp = 2 * index + 2;
	return temp >= length ? -1 : temp;
}

//输出数组
void outArray(int a[], int length) {
	for (int i = 0; i < length; i++) {
		cout << a[i] << " ";
	}
	cout << endl;
}
六、 时间复杂度分析

堆排序的时间消耗主要在调堆的过程, 在n次循环中共调了n次堆,每次调堆消耗为log(n-i),因此总时间复杂度为log(1)+log(2)+…+log(n)=log(n!)。

由于log(n!)和nlogn同阶,一般认作nlogn。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值