数据结构——堆排序

news2024/9/23 23:35:29

目录

引言

堆排序

1.算法思想

2.算法步骤

3.代码实现

3.1 构建堆

(1)小堆

(2)大堆

3.2 交换与调整

3.3 重复上述过程

4.复杂度分析

5.完整代码

5.1算法实现代码

5.2 示例

6.堆排序的优势

结束语


引言

本篇博客,我们将利用堆结构实现的高效排序算法,深入理解其原理与应用。

如果还不了解堆这一数据结构,可以先看看这篇博客:数据结构——堆

堆排序

1.算法思想

堆排序(Heap Sort)是一种基于堆数据结构实现的排序算法。利用堆这种数据结构的高效性,通过构建和调整堆来实现排序,是一种性能优秀的排序算法。

堆排序的核心思想是将待排序的元素构建成一个最大堆或最小堆,然后依次将堆顶元素与堆中最后一个元素交换,并重新调整堆,使剩余元素重新满足堆的性质。重复这一过程直到所有元素都被取出,最终得到了一个有序的序列。

2.算法步骤

堆排序的核心思想确实是将待排序的元素构建成一个最大堆(对于升序排序)或最小堆(对于降序排序),然后依次执行以下步骤:

(1)构建堆:首先,将待排序的序列通过一系列调整操作构建成一个最大堆(或最小堆)。在构建过程中,从最后一个非叶子节点开始,向上进行堆调整(Heapify),确保每个子树都满足堆的性质。

(2)交换堆顶元素与堆尾元素:将堆顶元素(即当前序列中的最大或最小值)与堆的最后一个元素进行交换。这样,堆顶元素就被放到了它在排序后序列中正确的位置上。

(3)重新调整堆:将交换后的堆(此时最后一个元素是已知的最大或最小值,因此不再参与后续的堆操作)的大小减一,然后对新的堆顶元素执行堆调整操作,以恢复堆的性质。这一步是为了确保剩余的元素仍然构成一个最大堆(或最小堆)。

(4)重复步骤2和3:不断重复交换堆顶元素与堆尾元素,并重新调整堆的过程,直到堆的大小减至1。此时,整个序列就被排序完成了。

堆排序的实现基于上面的步骤。

3.代码实现

3.1 构建堆

借助建堆算法,降序建小堆,升序建大堆,选择向上或者向下调整算法。

向上调整建堆和向下调整建堆是构建堆的两种主要方法,它们各有特点,但都能达到同样的目的:将一组元素组织成一个堆结构。

向上调整建堆:从最后一个非根节点开始向前遍历,对每个节点进行上浮操作,确保每个节点都满足堆的性质,即父节点的值大于等于(或小于等于)其子节点的值。

向下调整建堆:从最后一个非叶子节点开始向上遍历到根节点,对每个节点执行下沉操作,通过比较和交换,确保每个节点都满足堆的性质,从而构建出堆结构。

向下调整算法优于向上调整,我们在这里选择向下调整算法。

(1)小堆
// 建小堆
void AdjustDownSmall(int* arr, int n, int parent)
{
	// 假设左孩子小
	int child = parent * 2 + 1;
	while (child < n)
	{
		// 如果右孩子存在且比左孩子小,则选择右孩子进行比较
		if (child + 1 < n && arr[child + 1] < arr[child])
		{
			++child;
		}
		// 如果选中的孩子节点小于父节点,则交换它们,并继续向下调整  
		if (arr[child] < arr[parent])
		{
			Swap(&arr[child], &arr[parent]); // 交换父节点和孩子节点  
			parent = child;			// 更新父节点为原来的孩子节点  
			child = parent * 2 + 1; // 更新child为新的父节点的左孩子索引  
		}
		else
		{
			// 如果孩子节点不小于父节点,则无需继续调整,跳出循环
			break;
		}
	}
}
(2)大堆
// 建大堆
void AdjustDownLarge(int* arr, int n, int parent)
{
	int child = parent * 2 + 1;
	while (child < n)
	{
		// 如果右孩子存在且比左孩子大,则选择右孩子进行比较  
		if (child + 1 < n && arr[child + 1] > arr[child])
		{
			++child; // 更新child为右孩子的索引  
		}
		// 如果选中的孩子节点大于父节点,则交换它们,并继续向下调整  
		if (arr[child] > arr[parent])
		{
			Swap(&arr[child], &arr[parent]); // 交换父节点和孩子节点  
			parent = child;			// 更新父节点为原来的孩子节点  
			child = parent * 2 + 1; // 更新child为新的父节点的左孩子索引  
		}
		else
		{
			// 如果孩子节点不大于父节点,则无需继续调整,跳出循环  
			break;
		}
	}
}
3.2 交换与调整

建完堆之后,就到排序
我们在这里以降序为例子:

每次将堆顶与堆尾数据交换(相当于将当前的最小值挪到最后),然后堆尾数据伪删除(有效数据个数--,不是真删除)进行一轮向下调整,恢复堆的结构。

代码如下:

Swap(&arr[0], &arr[end]);
AdjustDownSmall(arr, end, 0);
--end;

动图演示:

3.3 重复上述过程

重复上述交换与调整的过程,直到堆的大小为1,排序完成。

4.复杂度分析

时间复杂度:向下调整建堆的时间复杂度为O(N),向下调整的时间复杂度为O(logN),一共N次。因此总时间为O(N+NlogN),时间复杂度为O(NlogN)。

空间复杂度:由于没有开辟额外的空间,因此空间复杂度为O(1)。

5.完整代码

5.1算法实现代码
#include<stdio.h>

void Swap(int* p1, int* p2)
{
	int tmp = *p1;
	*p1 = *p2;
	*p2 = tmp;
}

// 建小堆
void AdjustDownSmall(int* arr, int n, int parent)
{
	// 假设左孩子小
	int child = parent * 2 + 1;
	while (child < n)
	{
		// 如果右孩子存在且比左孩子小,则选择右孩子进行比较
		if (child + 1 < n && arr[child + 1] < arr[child])
		{
			++child;
		}
		// 如果选中的孩子节点小于父节点,则交换它们,并继续向下调整  
		if (arr[child] < arr[parent])
		{
			Swap(&arr[child], &arr[parent]); // 交换父节点和孩子节点  
			parent = child;			// 更新父节点为原来的孩子节点  
			child = parent * 2 + 1; // 更新child为新的父节点的左孩子索引  
		}
		else
		{
			// 如果孩子节点不小于父节点,则无需继续调整,跳出循环
			break;
		}
	}
}

// 建大堆
void AdjustDownLarge(int* arr, int n, int parent)
{
	int child = parent * 2 + 1;
	while (child < n)
	{
		// 如果右孩子存在且比左孩子大,则选择右孩子进行比较  
		if (child + 1 < n && arr[child + 1] > arr[child])
		{
			++child; // 更新child为右孩子的索引  
		}
		// 如果选中的孩子节点大于父节点,则交换它们,并继续向下调整  
		if (arr[child] > arr[parent])
		{
			Swap(&arr[child], &arr[parent]); // 交换父节点和孩子节点  
			parent = child;			// 更新父节点为原来的孩子节点  
			child = parent * 2 + 1; // 更新child为新的父节点的左孩子索引  
		}
		else
		{
			// 如果孩子节点不大于父节点,则无需继续调整,跳出循环  
			break;
		}
	}
}

void HeapSort_Down(int* arr, int n)
{
	// 向下调整建堆 
	// 下标为i的节点的父节点下标:(i-1)/2
	// i=n - 1
	for (int i = (n - 1 - 1) / 2; i >= 0; i--)
	{
		AdjustDownSmall(arr, n, i);
	}
	int end = n - 1;
	while (end > 0)
	{
		Swap(&arr[0], &arr[end]);
		AdjustDownSmall(arr, end, 0);
		--end;
	}
}

void HeapSort_Up(int* arr, int n)
{
	// 向下调整建堆 
	// 下标为i的节点的父节点下标:(i-1)/2
	// i=n - 1
	for (int i = (n - 1 - 1) / 2; i >= 0; i--)
	{
		AdjustDownLarge(arr, n, i);
	}
	int end = n - 1;
	while (end > 0)
	{
		Swap(&arr[0], &arr[end]);
		AdjustDownLarge(arr, end, 0);
		--end;
	}
}
5.2 示例

在这里,我们使用一个简单的示例来测试一下堆排序。

代码如下:

int main()
{
	int arr[] = { 3,1,4,5,9,2,6,6,4,2 };
	int n = sizeof(arr) / sizeof(arr[0]);
	HeapSort_Down(arr, n);
	printf("降序排序:");
	for (int i = 0; i < n; i++)
	{
		printf("%d ", arr[i]);
	}
	printf("\n");
	HeapSort_Up(arr, n);
	printf("升序排序:");
	for (int i = 0; i < n; i++)
	{
		printf("%d ", arr[i]);
	}
	return 0;
}

输出结果:

6.堆排序的优势

大数据集排序:在处理包含数百万或数十亿条记录的大型数据集时,堆排序的高效性使其成为首选算法之一。它能够快速地将数据排序,为后续的数据分析、挖掘或可视化工作提供有力支持。

实时系统:在需要快速响应的实时系统中,如实时数据分析、在线交易处理等,堆排序的快速排序能力能够确保系统的高效运行,减少用户等待时间。

优先级队列:堆排序的核心——堆数据结构,本身就是实现优先级队列的理想选择。优先级队列在任务调度、网络路由选择、页面置换算法等多个领域都有广泛应用。通过堆排序,可以高效地维护优先级队列的顺序,确保高优先级任务得到优先处理。

结束语

呀呼,简要的介绍了一下堆排序这一排序方法。

求点赞收藏评论关注!!!

感谢各位大佬!!!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2105440.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

版本控制的核心:Git中的哈希与默克尔树解析

Git是最常用的代码版本控制工具。它帮助我们跟踪代码的更改、管理代码版本&#xff0c;同时保证代码库的完整性和安全性。我们知道 Git 中有一些基本的操作&#xff0c;比如commit、merge、rebase等&#xff0c;但这些操作的底层机制是如何实现的呢&#xff1f;哈希函数和默克尔…

深度学习|模型推理:端到端任务处理

文章目录 引言端到端的能力任务与模型简介手写数字识别数据准备训练集与测试集模型介绍 推理过程前向传播权重参数推理与评估 结语 引言 通过前文「深度学习&#xff5c;感知机&#xff1a;神经网络之始」中 XOR Gate 的示例&#xff0c;我们知道叠加层可以增强感知机的表达能…

单向链表排序及双向链表

单向链表的优缺点 优点&#xff1a;存储空间没有上限&#xff0c;插入删除效率高 缺点&#xff1a;修改和查找效率低&#xff0c;只能单向的向后遍历后续节点&#xff0c;不能向前遍历前驱节点 单向链表快慢指针法查找&#xff1a; 链表的排序 双向链表 由于单向链表只能通…

Linux 性能调优:策略与实践

引言 随着云计算和虚拟化技术的发展&#xff0c;Linux 已经成为企业和个人用户的首选操作系统。Linux 性能调优不仅有助于提高系统资源利用率&#xff0c;还能确保应用程序的高效运行。本文将探讨 Linux 性能调优的基本原则、常用工具和方法&#xff0c;以及实际案例分析。 一…

go中的并发处理

. Goroutines 概念&#xff1a; Goroutines 是 Go 的核心并发机制。它们是由 Go 运行时管理的轻量级线程&#xff0c;具有比操作系统线程更少的开销。每个 goroutine 只需少量的内存&#xff08;大约 2KB&#xff09;&#xff0c;并且由 Go 运行时负责调度和管理,哪怕是java发…

哈希表与统计——594、350、554、609、454(2简3中)

594. 最长和谐子序列&#xff08;简单&#xff09; 和谐数组是指一个数组里元素的最大值和最小值之间的差别 正好是 1 。 现在&#xff0c;给你一个整数数组 nums &#xff0c;请你在所有可能的子序列中找到最长的和谐子序列的长度。 数组的子序列是一个由数组派生出来的序列&a…

华大HC32F460移植FreeRTOS

参考&#xff1a; 关于MCU M4内核移植FreeRTOS的笔记 主要参考这位大佬的&#xff0c;照做就行了&#xff0c;用的也是IAR HC32F460 freeRTOS移植 这位是用Keil的 MCU&#xff1a;华大HC32F460 库版本&#xff1a;hc32f460_ddl_Rev2.2.0 IDE&#xff1a; IAR FreeRTOS版本&…

【多线程】概述

进程与线程 概述 一个正在运行过程中的程序&#xff0c;就是一个进程&#xff0c;也称一个任务。进程是一个重要的软件资源&#xff0c;是由操作系统内核管理和组织的。 并行&#xff1a;微观上同一个时刻&#xff0c;两个核心上的进程&#xff0c;就是同时进行的。 并发&…

遗传算法Github初学

遗传算法的理论是根据达尔文进化论而设计出来的算法&#xff1a;人类是朝着好的方向&#xff08;最优解&#xff09;进化&#xff0c;进化过程中&#xff0c;会自动选择优良基因&#xff0c;淘汰劣等基因 遗传算法&#xff08;genetic algorithm——GA&#xff09;是计算数学中…

【JavaScript】LeetCode:11-15

文章目录 11 最长连续序列12 移动零13 盛最多水的容器14 三数之和15 接雨水 11 最长连续序列 数组排序、去重遍历数组中的元素&#xff0c;当元素连续时&#xff0c;长度加1&#xff0c;当不连续时&#xff0c;长度设置为1。 /*** param {number[]} nums* return {number}*/ v…

AI科学家:自动化科研的未来之路

随着人工智能&#xff08;AI&#xff09;技术的不断进步&#xff0c;AI已经在众多领域中展现了强大的潜力&#xff0c;尤其是在科研方面的应用正在引起广泛关注。最近&#xff0c;Sakana AI与牛津大学和不列颠哥伦比亚大学联合推出了一款被称为“AI科学家”的自动化科研工具&am…

第4章-01-学会从Chrome浏览器中Network

🏆作者简介,黑夜开发者,CSDN领军人物,全栈领域优质创作者✌,CSDN博客专家,阿里云社区专家博主,2023年CSDN全站百大博主。 🏆数年电商行业从业经验,历任核心研发工程师,项目技术负责人。 🏆本文已收录于专栏:Web爬虫入门与实战精讲,后续完整更新内容如下。 文章…

【Canvas与纹饰】环形小蜜蜂纹饰

【成图】 【代码】 <!DOCTYPE html> <html lang"utf-8"> <meta http-equiv"Content-Type" content"text/html; charsetutf-8"/> <head><title>环形小蜜蜂纹饰</title><style type"text/css"&g…

Mysql基础练习题 1378.使用唯一标识符替换员工ID (力扣)

1378. 展示每位用户的 唯一标识码&#xff08;unique ID &#xff09;&#xff1b;如果某位员工没有唯一标识码&#xff0c;使用 null 填充即可。 你可以以任意顺序返回结果表。 题目链接&#xff1a; https://leetcode.cn/problems/replace-employee-id-with-the-unique-i…

k8s调度、污点、容忍、不可调度、排水、数据卷挂载

一、Kubernetes的list-watch机制 1、List-watch K8S集群中&#xff0c;通过List-watch机制进行每个组件的协作&#xff0c;保持数据同步。这种设计可以实现每个组件之间的解耦 kubectl配置文件&#xff0c;统一向集群内部apiserver发送命令——通过apiserver把命令发送到各个…

C# 不安全代码

当一个代码块使用 unsafe 修饰符标记时&#xff0c;C# 允许在函数中使用指针变量。不安全代码或非托管代码是指使用了指针变量的代码块。 指针变量 指针 是值为另一个变量的地址的变量&#xff0c;即&#xff0c;内存位置的直接地址。就像其他变量或常量&#xff0c;您必须在…

【系统架构设计师-2022年】综合知识-答案及详解

文章目录 【第1题】【第2题】【第3题】【第4题】【第5题】【第6~7题】【第8题】【第9题】【第10题】【第11~12题】【第13题】【第14题】【第15题】【第16题】【第17~18题】【第19题】【第20题】【第21题】【第22题】【第23题】【第24题】【第25题】【第26题】【第27题】【第28题…

力扣刷题(复习版2)

题目&#xff1a; 计数质数 原题链接&#xff1a; 计数质数 题解 public int countPrimes(int n) {if (n < 2) return 0;// boolean 数组&#xff0c;初始时假设所有数都是质数boolean[] isPrime new boolean[n];Arrays.fill(isPrime, true); // 全部初始化为true// 从 …

网页版修改本地数据器:重新布局,引入 highlight.js高亮显示代码

<!DOCTYPE html> <html lang"zh-CN"><head><meta charset"UTF-8"><title>修改数据器</title><!-- 引入 highlight.js 的 CSS 文件 --><link rel"stylesheet" href"https://cdnjs.cloudflare.…

Tauri应用开发实践指南(4)— Tauri 原生能力

本文首发于微信公众号&#xff1a;前端徐徐。欢迎关注&#xff0c;获取更多前端技能分享。 原生能力简介 Tauri 是一个用于构建安全的小型桌面应用程序的框架,它结合了 Web 前端和系统后端技术。Tauri 提供了一些原生能力,让您的 Web 应用程序能够访问本地系统资源和 API,主要…