八大排序之堆排序

堆排序是一种基于堆数据结构的高效排序算法,其核心是通过构建大顶堆(升序)或小顶堆(降序),反复提取堆顶元素并调整堆结构。堆排序有递归和非递归两种实现方式,主要区别在于堆调整(Heapify)函数的实现方式

一、核心原理回顾

堆是一种完全二叉树(除最后一层外,每层节点均满,最后一层节点靠左排列),分为两种类型:

  • 大顶堆:每个父节点的值 ≥ 子节点的值(用于升序排序)。
  • 小顶堆:每个父节点的值 ≤ 子节点的值(用于降序排序)。

堆的存储:通常用数组实现,若父节点索引为i,则:

  • 左子节点索引 = 2i + 1
  • 右子节点索引 = 2i + 2
  • 父节点索引 = (i - 1) / 2(向下取整)

堆排序的基本流程:

  1. 构建初始堆:将无序数组转换为大顶堆(父节点 >= 子节点)。
  2. 提取堆顶元素:将堆顶(最大值)与数组末尾元素交换,缩小堆的范围。
  3. 调整堆结构:对新的堆顶元素执行堆调整,维持大顶堆性质。
  4. 重复步骤 2-3:直到堆的范围缩小为 1,数组完全有序。

关键操作是堆调整(Heapify):当某个节点的值小于其子节点时,将其与最大的子节点交换,并递归或迭代处理受影响的子树。

示例演示:

以数组 [4, 10, 3, 5, 1] 为例,升序排序过程:

  1. 初始数组[4, 10, 3, 5, 1](对应完全二叉树)

  2. 构建大顶堆

    • 最后一个非叶子节点是索引 1(值 10),无需调整。
    • 处理索引 0(值 4):左子节点 5(值 5)、右子节点 2(值 3),最大值为 5 → 交换 4 和 5 → [5, 10, 3, 4, 1]
    • 再次调整索引 3(值 4):无有效子节点,大顶堆构建完成 → [10, 5, 3, 4, 1]
  3. 排序阶段

    • 第 1 轮:交换堆顶 10 和末尾 1 → [1, 5, 3, 4, 10],调整堆顶 1 → 重建大顶堆 [5, 4, 3, 1, 10]
    • 第 2 轮:交换堆顶 5 和倒数第二元素 1 → [1, 4, 3, 5, 10],调整堆顶 1 → 重建大顶堆 [4, 1, 3, 5, 10]
    • 第 3 轮:交换堆顶 4 和倒数第三元素 3 → [3, 1, 4, 5, 10],调整堆顶 3 → 重建大顶堆 [3, 1, 4, 5, 10](无需调整)。
    • 第 4 轮:交换堆顶 3 和倒数第四元素 1 → [1, 3, 4, 5, 10],排序完成。

二、递归版堆排序

递归版堆调整通过函数自身调用处理子树,代码简洁直观,但可能在数据规模极大时引发栈溢出。

void Heap_Adjust(int arr[], int start, int end) {
		int lastindex = start;
		int left = 2 * start + 1;
		int right = 2 * start + 2;
		if (left <= end && arr[left] > arr[lastindex]) {
			lastindex = left;
		}
		if (right <= end && arr[right] > arr[lastindex]) {
			lastindex = right;
		}
		if (lastindex == start)return;
		int tmp = arr[start];
		arr[start] = arr[lastindex];
		arr[lastindex] = tmp;
		Heap_Adjust(arr, lastindex, end);
}
void Heap_Sort(int arr[], int len) {
	for (int i = (len - 1 - 1) / 2; i >= 0; i--) {
		Heap_Adjust(arr, i, len - 1);
	}
	for (int i = 0; i < len - 1; i++) {
		int tmp = arr[0];
		arr[0] = arr[len - 1 - i];
		arr[len - 1 - i] = tmp;
		Heap_Adjust(arr, 0, len - 2 - i);
	}
}

三、非递归版堆排序

非递归版堆调整通过循环迭代处理子树,避免了递归调用的栈开销,更适合大规模数据,但代码稍复杂。

void Heap_Adjust(int arr[], int start, int end) {
	while (1) {
		int lastindex = start;
		int left = 2 * start + 1;
		int right = 2 * start + 2;
		if (left <=end && arr[left] > arr[lastindex]) {
			lastindex = left;
		}
		if (right <=end && arr[right] > arr[lastindex]) {
			lastindex = right;
		}
		if (lastindex == start)break;
		int tmp = arr[start];
		arr[start] = arr[lastindex];
		arr[lastindex] = tmp;
		start = lastindex;

	}
}
void Heap_Sort(int arr[], int len) {
	for (int i = (len - 1 - 1) / 2; i >= 0; i--) {
		Heap_Adjust(arr, i, len-1);
	}
	for (int i = 0; i < len - 1; i++) {
		int tmp = arr[0];
		arr[0] = arr[len - 1 - i];
		arr[len - 1 - i] = tmp;
		Heap_Adjust(arr, 0, len - 2 - i);
	}
}

四、递归版与非递归版对比

  1. 时间复杂度

    • 建堆阶段:O (n)(整体复杂度,非 O (n log n))。
    • 排序阶段:O (n log n)(n-1 次堆调整,每次调整 O (log n))。
    • 总时间复杂度:O(n log n)(最好、最坏、平均情况均相同,稳定性强)。
  2. 空间复杂度

    • 仅使用常数级临时变量,空间复杂度为 O(1),属于原地排序。
  3. 稳定性

    • 不稳定排序:堆调整过程中,相等元素的相对位置可能被交换(如父子节点值相等时交换会改变顺序)。
  4. 适用场景

    • 大规模数据排序(时间复杂度稳定,空间效率高)。
    • 实时获取最大值 / 最小值的场景(如优先队列)。
    • 内存受限的环境(原地排序特性)。
特性递归版堆排序非递归版堆排序
实现复杂度代码简洁,逻辑直观(递归调用)代码稍复杂,需手动控制循环流程
空间复杂度O (log n)(递归调用栈开销)O (1)(仅用少量临时变量,原地排序)
性能递归调用有额外开销,大规模数据可能较慢无函数调用开销,性能更优
栈溢出风险数据规模极大时可能栈溢出(递归深度 O (log n))无栈溢出风险,适合超大规模数据
可读性高,符合堆调整的递归逻辑中等,需理解循环迭代的子树处理

五、选择建议

  • 递归版:适合小规模数据或教学场景,代码易写易理解。
  • 非递归版:适合大规模数据或生产环境,无栈溢出风险,性能更稳定。

两种版本的核心思想完全一致,仅堆调整的实现方式不同。实际应用中,非递归版因空间效率更高而更常用,尤其是在嵌入式系统或内存受限的环境中。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值