当前位置: 首页 > news >正文

【排序算法】选择排序、堆排序

文章目录

  • 选择排序
    • 选择排序的概念
    • 选择排序的基本步骤:
    • 选择排序的特点
    • 选择排序的代码实现(C语言)
  • 选择排序-优化
    • 双向选择排序的步骤
    • 堆的基本概念
    • 堆排序详细步骤
    • 堆排序代码讲解

选择排序

选择排序的概念

选择排序是一种简单直观的排序算法。它的工作原理是每次从未排序的部分中选择一个最小(或最大)的元素,并将其与未排序部分的第一个元素进行交换。这个过程重复 n-1 次,直到数组排序完毕。

选择排序的基本步骤:

  1. 未排序的部分中找到最小的元素。
  2. 将这个元素与未排序部分的第一个元素交换
  3. 每次遍历数组,未排序部分就会减少,已排序部分逐渐扩大。
  4. 当所有元素都经过选择排序后,数组就变为有序的。

选择排序的特点

  • 时间复杂度: O(n²),因为对于每个元素,都需要扫描剩下的未排序元素来找到最小值。
  • 空间复杂度: O(1),因为它是原地排序算法,不需要额外的存储空间。
  • 稳定性: 选择排序是不稳定的排序算法。因为元素交换时,可能会改变相同值元素的相对顺序。
  • 虽然很好理解,效率不好,实际中很少使用。

选择排序的代码实现(C语言)

#include <stdio.h>// 选择排序函数
void SelectionSort(int arr[], int n) {// 外层循环,逐渐缩小未排序部分for (int i = 0; i < n - 1; i++) {// 初始化最小值的索引为当前未排序部分的第一个元素int minIndex = i;
//注意i<n-1,因为i之后会比较j=i+1的值
//但j是<n,可以走到最后一个// 内层循环,从 i+1 开始查找未排序部分的最小元素for (int j = i + 1; j < n; j++) {// 如果找到比当前最小值更小的元素,更新最小值索引if (arr[j] < arr[minIndex]) {minIndex = j;}}// 如果最小值的索引不是 i,则交换元素if (minIndex != i) {int temp = arr[i];arr[i] = arr[minIndex];arr[minIndex] = temp;}}
}// 打印数组函数
void PrintArray(int arr[], int n) {for (int i = 0; i < n; i++) {printf("%d ", arr[i]);}printf("\n");
}int main() {// 示例数组int arr[] = {64, 25, 12, 22, 11};int n = sizeof(arr) / sizeof(arr[0]);printf("排序前的数组: ");PrintArray(arr, n);// 执行选择排序SelectionSort(arr, n);printf("排序后的数组: ");PrintArray(arr, n);return 0;
}

输出示例

排序前的数组: 64 25 12 22 11 
排序后的数组: 11 12 22 25 64 

详细讲解

  1. 外层循环 for (int i = 0; i < n - 1; i++):
    • 控制循环次数,每次选择一个最小元素并将其放到正确位置。
    • 循环结束后,前 i 个元素已经排好序。
  2. 初始化最小值索引int minIndex = i;:
    • 在每一轮选择中,将当前未排序部分的第一个元素假设为最小元素。
  3. 内层循环 for (int j = i + 1; j < n; j++):
    • 通过从 i+1 开始的未排序部分中寻找最小值。
    • 如果发现比当前最小值更小的元素,更新 minIndex,标记该元素的索引。
  4. 元素交换 if (minIndex != i):
    • 如果最小值不是当前元素,就交换最小值和未排序部分第一个元素。
    • 交换之后,已排序部分扩大一个元素。
  5. PrintArray()** 函数**:
    • 一个简单的辅助函数,用于打印数组的当前状态。

选择排序的运行步骤举例

假设排序数组 arr[] = {64, 25, 12, 22, 11}:

  • 第1轮:
    • i = 0,未排序部分为 {64, 25, 12, 22, 11}
    • 找到最小值 11,将其与 64 交换,数组变为 {11, 25, 12, 22, 64}
  • 第2轮:
    • i = 1,未排序部分为 {25, 12, 22, 64}
    • 找到最小值 12,将其与 25 交换,数组变为 {11, 12, 25, 22, 64}
  • 第3轮:
    • i = 2,未排序部分为 {25, 22, 64}
    • 找到最小值 22,将其与 25 交换,数组变为 {11, 12, 22, 25, 64}
  • 第4轮:
    • i = 3,未排序部分为 {25, 64}
    • 最小值是 25,不需要交换,数组保持 {11, 12, 22, 25, 64}

最后得到有序数组 {11, 12, 22, 25, 64}

总结

选择排序的核心思想就是每次从未排序的部分中选择最小的元素,并把它放到正确的位置。虽然算法实现简单,但它的时间复杂度为 O(n²),效率较低,适用于小规模数组的排序问题。

选择排序-优化

双向选择排序双端选择排序,这种方法通过同时从两个方向进行选择,以减少排序的时间复杂度。具体来说,它在每一轮中既选择最小值也选择最大值,并将它们放到数组的两端。

双向选择排序的步骤

  1. 初始化:设置两个指针,一个指向数组的开始位置(left),一个指向数组的结束位置(right)。
  2. 选择最小值和最大值
    • 遍历当前的未排序部分,找到最小值和最大值。
    • 将最小值放到left指针指向的位置,将最大值放到right指针指向的位置。
  3. 更新指针
    • left指针向右移动一位(因为最小值已经放到正确的位置)。
    • right指针向左移动一位(因为最大值已经放到正确的位置)。
  4. 重复步骤2和3,直到left指针与right指针交叉或重叠。

双向选择排序的C语言实现

#include <stdio.h>// 函数用于交换两个整数的值
void swap(int *x, int *y) {int temp = *x;*x = *y;*y = temp;
}// 双向选择排序函数
void biDirectionalSelectionSort(int arr[], int n) {int left = 0;          // 指向未排序部分的起始位置int right = n - 1;     // 指向未排序部分的结束位置while (left < right) {int minIndex = left;   // 初始化最小值的索引int maxIndex = right;  // 初始化最大值的索引// 遍历未排序部分,找到最小值和最大值for (int i = left; i <= right; i++) {if (arr[i] < arr[minIndex]) {minIndex = i;}if (arr[i] > arr[maxIndex]) {maxIndex = i;}}// 交换最小值到当前左边界swap(&arr[left], &arr[minIndex]);// 如果最大值在左边界位置,交换最大值到右边界位置if (maxIndex == left) {maxIndex = minIndex; // 更新最大值的索引}// 交换最大值到当前右边界swap(&arr[right], &arr[maxIndex]);// 更新边界left++;right--;}
}// 辅助函数:打印数组元素
void printArray(int arr[], int size) {for (int i = 0; i < size; i++) {printf("%d ", arr[i]);}printf("\n");
}// 测试双向选择排序的主函数
int main() {int arr[] = {64, 25, 12, 22, 11, 90};int n = sizeof(arr) / sizeof(arr[0]);printf("原始数组:\n");printArray(arr, n);biDirectionalSelectionSort(arr, n);printf("排序后的数组:\n");printArray(arr, n);return 0;
}
  1. swap函数
    • 用于交换两个整数的值,以便在数组中重新排列元素。
  2. biDirectionalSelectionSort函数
    • 初始化left指针从数组的开始位置开始,right指针从数组的结束位置开始。
    • 找到最小值和最大值
      • 遍历当前未排序的部分,记录最小值和最大值的索引。
    • 交换最小值和最大值
      • 将最小值交换到left位置。
      • 如果最大值在left位置(也就是最小值交换后,最大值可能在左边),需要更新最大值的索引,然后将最大值交换到right位置。
    • 更新边界
      • left指针向右移动一位,将right指针向左移动一位,准备处理下一轮。
  3. printArray函数
    • 用于打印数组中的元素,帮助观察排序的结果。
  4. main函数
    • 初始化一个数组,打印原始数组,调用biDirectionalSelectionSort函数进行排序,然后打印排序后的数组。

性能分析

  • 时间复杂度:最坏情况下是O(n^2),因为每一轮的遍历操作都需要O(n)的时间,总共会执行n/2轮。
  • 空间复杂度O(1),因为排序过程在原地完成,没有使用额外的存储空间。

双向选择排序通过同时选择最小值和最大值来减少了需要排序的次数,从而比传统的选择排序略微优化了性能。

堆的基本概念

堆是一种特殊的完全二叉树,满足以下条件:

  • 最大堆(Max-Heap):在最大堆中,每个父节点的值都大于或等于其子节点的值。因此,堆顶(根节点)是最大值。
  • 最小堆(Min-Heap):在最小堆中,每个父节点的值都小于或等于其子节点的值,因此堆顶是最小值。

堆是一个完全二叉树,因此它可以用数组来高效地表示。对于任意节点i

  • 左子节点的索引是2 * i + 1
  • 右子节点的索引是2 * i + 2
  • 父节点的索引是(i - 1) / 2

堆排序详细步骤

1. 构建最大堆

构建最大堆的过程是从最后一个非叶子节点开始,依次对每一个节点执行“堆化”操作。堆化(Heapify)是一个过程,它将以某个节点为根的子树调整为最大堆。

构建最大堆的过程

  • 从数组的最后一个非叶子节点开始(索引为n/2 - 1),向前逐步调整每个节点,保证每个子树都是最大堆。

2. 排序过程

  • 将堆顶(最大值)与堆的最后一个元素交换,堆的大小减1。
  • 重新调整堆(heapify),以确保剩下的部分仍然是最大堆。
  • 重复上述步骤,直到堆的大小减小到1。

代码实现

#include <stdio.h>// 函数用于交换两个整数的值
void swap(int *x, int *y) {int temp = *x;*x = *y;*y = temp;
}// 函数用于维护最大堆的性质
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) {// 构建最大堆for (int i = n / 2 - 1; i >= 0; i--) {heapify(arr, n, i);}// 一个一个取出元素进行排序for (int i = n - 1; i >= 0; i--) {// 当前最大值(根节点)与堆的最后一个元素交换swap(&arr[0], &arr[i]);// 重新调整堆,排除已排序的部分heapify(arr, i, 0);}
}// 辅助函数:打印数组元素
void printArray(int arr[], int size) {for (int i = 0; i < size; i++) {printf("%d ", arr[i]);}printf("\n");
}// 测试堆排序的主函数
int main() {int arr[] = {12, 11, 13, 5, 6, 7};int n = sizeof(arr) / sizeof(arr[0]);printf("原始数组:\n");printArray(arr, n);heapSort(arr, n);printf("排序后的数组:\n");printArray(arr, n);return 0;
}

代码逐步讲解

  1. swap函数
    • 交换两个整数的值,用于调整堆中元素的位置。
  2. heapify函数
    • heapify用于维护最大堆的性质。它从节点i开始,调整以i为根的子树,确保子树符合最大堆的特性。
    • largest变量用来追踪当前子树中的最大值索引。
    • 如果largest不是i,说明子树不符合最大堆的特性,需要交换并递归调用heapify
  3. heapSort函数
    • 首先构建最大堆。for循环从最后一个非叶子节点开始,通过heapify将整个数组调整为最大堆。
    • 然后逐个取出堆顶元素(最大值)并将其与数组的最后一个元素交换,接着调整剩余部分,保持最大堆的性质。
  4. printArray函数
    • 打印数组中的元素,帮助观察排序的结果。
  5. main函数
    • 初始化一个数组,打印原始数组,调用heapSort函数进行排序,然后打印排序后的数组。

希望这些详细解释和代码示例能帮助你更好地理解堆排序。如果还有其他问题或需要更深入的解释,请告诉我!

堆排序代码讲解

堆排序简介

堆排序是一种基于堆(Heap)数据结构的排序算法。堆是一棵完全二叉树,可以通过数组表示:

  • 最大堆:每个节点的值都大于或等于其子节点的值。根节点是最大值。
  • 最小堆:每个节点的值都小于或等于其子节点的值。根节点是最小值。

堆排序的核心思想是:

  1. 首先将无序数组构建为一个最大堆
  2. 然后,将堆顶(即最大值)与数组的最后一个元素交换,缩小堆的范围,对剩下的元素继续调整成最大堆,直到排序完成。

1. `AdjustDown` 函数分析(向下调整)

该函数用于维护最大堆的性质。它从父节点开始,将其与子节点比较,并向下调整,使得父节点的值始终大于等于它的子节点。

void AdjustDown(int* a, int n, int parent)
{int child = parent * 2 + 1; // 左孩子的索引while (child < n) // 确保孩子节点存在{// 找出两个孩子中较大的那个if (child + 1 < n && a[child + 1] > a[child]){++child; // 右孩子比左孩子大,选择右孩子}// 如果孩子的值大于父节点,则进行交换if (a[child] > a[parent]){Swap(&a[child], &a[parent]); // 交换父节点和较大孩子// 继续向下调整,将孩子当作新的父节点parent = child;child = parent * 2 + 1; // 更新左孩子的索引}else{break; // 如果父节点已经大于等于孩子,则无需再调整}}
}
解释:
  • child = parent * 2 + 1:左孩子节点的索引。因为堆是一棵完全二叉树,所以对于索引为 parent 的节点,左孩子的索引为 2 * parent + 1,右孩子的索引为 2 * parent + 2
  • 核心逻辑:找到较大的孩子(如果右孩子存在并且比左孩子大,则选择右孩子),并与父节点进行比较,如果父节点比孩子小,则交换它们的值,并继续向下调整。
  • while 循环确保只要存在孩子节点,继续调整,直到父节点大于等于孩子节点或到达树的底部。

2. `HeapSort` 函数分析(堆排序的实现)

void HeapSort(int* a, int n)
{// 建堆过程,从最后一个非叶子节点开始调整,逐步向上调整for (int i = (n - 1 - 1) / 2; i >= 0; i--){AdjustDown(a, n, i);}// 逐步将堆顶元素(最大值)与最后一个未排序元素交换int end = n - 1;while (end > 0){Swap(&a[0], &a[end]); // 将堆顶元素与当前未排序部分的最后一个元素交换AdjustDown(a, end, 0); // 重新调整堆,使剩余部分维持最大堆的性质--end; // 排除最后一个已排序的元素}
}
详细讲解:
  1. 建堆过程
    • for (int i = (n - 1 - 1) / 2; i >= 0; i--)
      • (n - 1 - 1) / 2 计算的是最后一个非叶子节点的索引。叶子节点不需要调整,所以从最后一个非叶子节点开始向上遍历,逐一调整每个节点,以确保整个数组满足最大堆的性质。
      • 调用 AdjustDown(a, n, i) 对从 i 开始的子树进行调整,确保以 i 为根节点的子树是一个最大堆。
  2. 排序过程
    • Swap(&a[0], &a[end]):将当前堆的堆顶元素(最大值)与最后一个未排序的元素交换。
    • AdjustDown(a, end, 0):交换后,堆顶元素可能破坏了堆的性质,需要再次从堆顶(即索引 0)开始进行向下调整,恢复最大堆性质。
    • --end:每次处理完一个最大元素后,end 减少1,表示缩小待排序区域,直到所有元素都排序完成。

示例:堆排序的执行过程

假设我们有一个数组 [4, 10, 3, 5, 1],我们将详细分析堆排序的执行过程。

  1. 建堆
    • 原数组为 [4, 10, 3, 5, 1]
    • 从最后一个非叶子节点开始(i = 1,即 10)。
      • 比较 10 与其孩子(51),10 已经大于它的孩子,不需要调整。
    • 继续处理 i = 04)。
      • 比较 4 与其孩子(103),10 更大,交换 410
      • 现在数组变为 [10, 4, 3, 5, 1]
      • 继续向下调整,4 与它的孩子 5 比较,交换 45,得到 [10, 5, 3, 4, 1]
    • 完成建堆后,最大堆为 [10, 5, 3, 4, 1]
  2. 排序
    • 交换堆顶 10 和最后一个元素 1,得到 [1, 5, 3, 4, 10]
    • 调整堆 [1, 5, 3, 4],结果是 [5, 4, 3, 1, 10]
    • 继续交换堆顶 51,得到 [1, 4, 3, 5, 10]
    • 调整堆 [1, 4, 3],结果是 [4, 1, 3, 5, 10]
    • 交换堆顶 43,得到 [3, 1, 4, 5, 10],调整堆 [3, 1],结果是 [3, 1, 4, 5, 10]
    • 最后交换 31,得到 [1, 3, 4, 5, 10]

时间复杂度

  • 建堆的时间复杂度O(n),因为每个节点调整的次数与其深度成正比,而总的深度是 log(n)
  • 排序的时间复杂度O(n*log(n)),因为需要执行 n 次堆调整,而每次堆调整的时间是 O(log(n))

堆排序的特点

  • 空间复杂度O(1),因为堆排序是在原地排序,不需要额外的数组存储空间。
  • 不稳定性:堆排序是不稳定的,因为元素在交换时可能破坏它们的相对顺序。

通过这段代码,整个堆排序的逻辑就清晰地展示了出来:先建堆,再逐步通过调整最大堆进行排序。

  1. 📜 [ 声明 ] 由于作者水平有限,本文有错误和不准确之处在所难免,
  2. 本人也很想知道这些错误,恳望读者批评指正!
  3. 我是:勇敢滴勇~感谢大家的支持!

相关文章:

  • 北京网站建设多少钱?
  • 辽宁网页制作哪家好_网站建设
  • 高端品牌网站建设_汉中网站制作
  • Vue3:shallowRef与shallowReactive
  • JS手写Promise以及promise.all方法
  • 【算法】贪心+堆排序实现大根堆及标准库容器类的融合使用
  • 车载网络测试实操源码_使用CAPL脚本实现安全访问解锁,并模拟各种测试场景
  • C语言中易混淆概念的关键字
  • Qt/C++ 多线程同步机制详解及应用
  • redis 十大应用场景
  • 特种作业管理系统 —— 企业安全与效率的卓越保障
  • EfficientViT(2023CVPR):具有级联组注意力的内存高效视觉Transformer!
  • 8. 详细描述一条 SQL 语句在 MySQL 中的执行过程。
  • jQuery国内大厂CDN加速链接
  • 本地生活商城开发搭建 同城O2O线上线下推广
  • 【SpringBoot整合Redis测试Redis集群案例】
  • 一、Kafka入门
  • Cursor免费 GPT-4 IDE 工具的保姆级使用教程
  • CentOS学习笔记 - 12. Nginx搭建Centos7.5远程repo
  • Java方法详解
  • magento2项目上线注意事项
  • Netty 4.1 源代码学习:线程模型
  • React as a UI Runtime(五、列表)
  • UMLCHINA 首席专家潘加宇鼎力推荐
  • 基于Dubbo+ZooKeeper的分布式服务的实现
  • 基于OpenResty的Lua Web框架lor0.0.2预览版发布
  • 如何优雅地使用 Sublime Text
  • 实战:基于Spring Boot快速开发RESTful风格API接口
  • 提醒我喝水chrome插件开发指南
  • 一起来学SpringBoot | 第十篇:使用Spring Cache集成Redis
  • Java总结 - String - 这篇请使劲喷我
  • puppet连载22:define用法
  • ​数据链路层——流量控制可靠传输机制 ​
  • ​云纳万物 · 数皆有言|2021 七牛云战略发布会启幕,邀您赴约
  • #Datawhale AI夏令营第4期#多模态大模型复盘
  • #pragma once
  • (1)无线电失控保护(二)
  • (pojstep1.1.2)2654(直叙式模拟)
  • (SERIES10)DM逻辑备份还原
  • (二) Windows 下 Sublime Text 3 安装离线插件 Anaconda
  • (二)基于wpr_simulation 的Ros机器人运动控制,gazebo仿真
  • (二刷)代码随想录第16天|104.二叉树的最大深度 559.n叉树的最大深度● 111.二叉树的最小深度● 222.完全二叉树的节点个数
  • (附源码)php新闻发布平台 毕业设计 141646
  • (附源码)springboot电竞专题网站 毕业设计 641314
  • (几何:六边形面积)编写程序,提示用户输入六边形的边长,然后显示它的面积。
  • (一)Spring Cloud 直击微服务作用、架构应用、hystrix降级
  • (原創) 是否该学PetShop将Model和BLL分开? (.NET) (N-Tier) (PetShop) (OO)
  • (转)ABI是什么
  • (转)c++ std::pair 与 std::make
  • (轉貼) 2008 Altera 亞洲創新大賽 台灣學生成果傲視全球 [照片花絮] (SOC) (News)
  • .babyk勒索病毒解析:恶意更新如何威胁您的数据安全
  • .NET 快速重构概要1
  • .net 怎么循环得到数组里的值_关于js数组
  • .net6 当连接用户的shell断掉后,dotnet会自动关闭,达不到长期运行的效果。.NET 进程守护
  • .net网站发布-允许更新此预编译站点
  • .skip() 和 .only() 的使用
  • /bin/bash^M: bad interpreter: No such file ordirectory
  • @Transactional注解下,循环取序列的值,但得到的值都相同的问题