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

排序(Sort)方法详解(冒泡、插入、希尔、选择、堆、快速、归并)

目录

  • 一. 冒泡排序
  • 二. 插入排序
  • 三. 希尔排序
  • 四. 选择排序
    • 1. 选择排序(Selection Sort)
    • 2. 优化版选择排序(双向选择排序)
  • 五. 堆排序
  • 六. 快速排序
    • 1. Hoare版本
    • 2. 挖坑法版本
    • 3. 双指针版本
  • 7. 归并排序

⬇点击下方即可跳转⬇
各个排序算法动态演示效果

一. 冒泡排序

冒泡排序(Bubble Sort)是一种简单的比较排序算法,其核心思想是通过多次遍历数组,相邻元素两两比较并交换,将较大的元素逐步“冒泡”到数组末尾。

核心思想:

1. 外层循环:控制排序的轮数(共需要n-1轮,n为数组长度)。
2. 内层循环:在每一轮中比较相邻的两个元素,如果前一个的元素比后一个元素大,则交换位置。
3. 优化:如果在某一轮内层循环中没有发生任何交换,则说明数组已有序,提前退出排序。

代码实现:

void Swap(int* a, int* b)
{int t = *a;*a = *b;*b = t;
}//冒泡排序
void BubbleSort(int* arr, int n)
{for (int i = 0;i < n ;i++){int flag = 0;for (int j = 0;j < n -i - 1;j++){if (arr[j]>arr[j+1]){flag = 1;Swap(&arr[j], &arr[j + 1]);}}if (flag == 0)break;}
}

动图演示:
请添加图片描述

特点分析:

  • 时间复杂度:
    最好情况(有序数组):O(n)
    最坏情况(逆序数组):O(n2)
    平均情况:O(n2)
  • 空间复杂度:O(1)
  • 稳定性:稳定(相对顺序不变)。

二. 插入排序

插入排序(Insertion Sort)是一种简单直观的基于比较的排序算法,其核心思想是通过构建有序序列,逐步将未排序部分的元素插入到已排序部分的正确位置。

核心思想:

插入排序的工作方式就像许多人排序一手扑克牌:
1. 开始时,你左手是空的,所有牌都背面朝上放在桌上(未排序序列)。
2. 你从桌上拿起一张牌(当前要插入的元素,称为 Key),放到左手。
3. 之后,你从桌上拿起下一张牌,将它从右到左与左手中已有的每张牌进行比较,找到它正确的位置并插入。
4. 重复第3步,直到桌上的所有牌都被拿到左手。此时,左手中的牌就是有序的。

将待排序的元素逐个插入到一个已经排好序的序列中的正确位置,直到所有元素都插入完毕。

代码实现:


void InserSort(int* arr, int n)
{for (int i = 0;i < n - 1;i++){int end = i;int key = arr[end + 1];while (end >= 0 && arr[end] > key){arr[end + 1] = arr[end];end--;}arr[end + 1] = key;}
}

动图演示:
请添加图片描述

特点分析:

  • 时间复杂度:
    最好情况:O(n)
    最坏情况:O(n2)
    平均情况:O(n2)
  • 空间复杂度:O(1)
  • 稳定性:稳定(比较arr[end] > key时,使用的>而不是>=,保证了相等元素的相对顺序不变)。

三. 希尔排序

希尔排序(Shell Sort),它是插入排序的一种高效改进版本。

核心思想

  1. 对元素进行分组插入排序,随着增量逐渐减小,整个数组也越来越接近有序状态,当增量为1时,就是标准的插入排序,但此时数组已经基本有序,所以效率很高。
  2. 希尔排序会先每隔几个位置比较一次,让数组大致有序,然后再用标准的插入排序来调整成有序数组。

代码实现:

void ShellSort(int* arr, int n)
{int gap = n;while (gap > 1){gap = gap / 3 + 1;for (int i = 0;i < n - gap;i++){int end = i;int tmp = arr[end + gap];while (end >= 0 && arr[end] > tmp){arr[end + gap] = arr[end];end -= gap;}arr[end + gap] = tmp;}}
}

动图演示:

请添加图片描述

特点分析:

  • 时间复杂度:
    最好情况:O(n1.3)
    最坏情况:O(n2)
    平均情况:O(n logn)~O(n2)
  • 空间复杂度:O(1)
  • 稳定性:不稳定,在排序过程中相等元素的相对位置会被改变。

四. 选择排序

1. 选择排序(Selection Sort)

核心思想:

1. 每次从未排序部分选择最小(或最大)的元素放到已排序部分的末尾。

代码实现:

void Swap(int* a, int* b)
{int t = *a;*a = *b;*b = t;
}void SelectSort(int* arr, int n)
{for (int i = 0;i < n;i++){int key = i;for (int j = i + 1;j < n;j++){if (arr[j] < arr[key]){key= j;}}Swap(&arr[key], &arr[i]);}
}

动图演示:
请添加图片描述
特点分析:

  • 时间复杂度:
    最好情况:O(n2)
    最坏情况:O(n2)
    平均情况:O(n2)
  • 空间复杂度:O(1)
  • 稳定性:不稳定。交换操作可能改变相等元素的相对顺序([2 , 2 , 1]排序后第一个2会与1交换位置)。

2. 优化版选择排序(双向选择排序)

  • 在选择排序上的优化,在每次遍历中同时找最大值和最小值,并将最小值放在数组的开头,最大值放在数组的末尾,从而减少遍历次数。

核心思想:

1. 定义begin 和 end 分别表示当前未排序部分的起始和结束位置。
2. 在[begin , end]范围内同时查找最小值(mini)和最大值(maxi)。
3. 将mini的值交换到begin,maxi的值交换到end。
4. begin++ 和 end–,缩小未排序范围,直到begin >= end,完成排序

void Swap(int* a, int* b)
{int t = *a;*a = *b;*b = t;
}void SelectSort2(int* arr, int n)
{int begin = 0, end = n - 1;while (begin < end){int mini = begin;int maxi = begin;for (int i = begin + 1;i <= end;i++){if (arr[i] < arr[mini]){mini = i;}if (arr[i] > arr[maxi]){maxi = i;}}if (maxi == mini){maxi = mini;}Swap(&arr[mini], &arr[begin]);Swap(&arr[maxi], &arr[end]);begin++;end--;}
}

优化效果:

  • 双向选择排序的实际运行时间通常比传统选择排序快50%(因为每次遍历处理两个元素)。
  • 但是时间复杂度仍是O(n2)
  • 仍然是不稳定排序。

五. 堆排序

堆排序(Heap Sort)是一种基于二叉树数据结构的比较排序算法,它结合了选择排序的思想和堆的高效操作。

核心思想:

1. 将一个无序数组调整成一个大顶堆(或小顶堆)。
2. 反复将对顶数据最大值(或最小值)与堆的最后一个元素交换,并缩小堆的范围。
3. 最终数组按升序(大顶堆)或降序(小顶堆)排列。

代码实现:

void Swap(int* a, int* b)
{int t = *a;*a = *b;*b = t;
}//向下建堆
void AdjustDown(int* arr,int parent, int n)
{int child = parent * 2 + 1;while (child < n){//找出左右子节点中较大的节点if (child + 1 < n && arr[child] < arr[child + 1]){child++;}if (arr[child] > arr[parent]){Swap(&arr[child], &arr[parent]);parent = child;child = parent * 2 + 1;}elsebreak;}
}void HeapSort(int* arr, int n)
{for (int i = (n - 1 - 1) / 2;i >= 0;i--){AdjustDown(arr, i, n);}int end = n - 1;while (end > 0){//交换堆顶和堆尾元素Swap(&arr[0], &arr[end]);AdjustDown(arr, 0, end);end--;}
}

代码详细步骤:

例:arr[] = {4 ,8 , 3 , 5 , 1}, n = 5;

  1. 构建大堆 调用向下建堆函数,调整索引1(值8)比左右子树都大。调整索引0(值4)比左子树小(值8),交换4和8,4比5小再交换4和5。数组变为arr[] = {8 , 5 , 3 , 4 , 1};
  2. 排序过程:
    第一次循环:end = 4,交换堆顶和堆尾,通过向下建堆,数组变为arr[] = {5 , 4 , 3 , 1 , 8};
    第二次循环:end = 3,交换堆顶和堆尾,通过向下建堆,数组变为arr[] = {4 , 1 , 3 , 5 , 8};
    第三次循环:end = 2,交换堆顶和堆尾,通过向下建堆,数组变为arr[] = {3 , 1 , 4 , 5 , 8};
    第四次循环:end = 1,交换堆顶和堆尾,通过向下建堆,数组变为arr[] = {1 , 3 , 4 , 5 , 8};
    排序完成 arr[1 , 3 , 4 , 5 , 8};

特点分析:

  • 时间复杂度:
    最好情况:O(n logn)
    最坏情况:O(n logn)
    平均情况:O(n logn)
  • 空间复杂度:O(1)
  • 稳定性:不稳定。交换操作可能改变相等元素的相对顺序([5 , 5 , 2]排序后第一个5会与2交换位置)。

六. 快速排序

快速排序(Quick Sort)是一种基于分治策略的比较排序算法,它通过递归地将数组分成较小的子数组,并分别排序,最终实现整个数组的有序排列。

1. Hoare版本

算法步骤:

  1. 选择最左边元素作为基准
  2. 设置左右指针分别指向数组首尾
  3. 右指针向左找第一个小于基准的元素
  4. 左指针向右找第一个大于基准的元素
  5. 交换这两个元素
  6. 重复直到左右指针相遇
  7. 将基准与相遇点交换

代码实现:

void Swap(int* a, int* b)
{int t = *a;*a = *b;*b = t;
}int _QuickSort1(int* arr, int left, int right)
{int keyi = left;left++;while (left <= right){//right:从右向左找比基准值要小的while(arr[right] > arr[keyi]){--right;}//left:从左向右找比基准值要大的while(arr[left] < arr[keyi]){left++;}//left和right交换if(left<=right)Swap(&arr[left++], &arr[right--]);}//基准值归位Swap(&arr[keyi], &arr[right]);return right;
}void QuickSort(int* arr, int left, int right)
{if (left >= right){return;}//找基准值int keyi = _QuickSort1(arr, left, right);QuickSort(arr, left, keyi - 1);QuickSort(arr, keyi + 1, right);
}

动图演示:
请添加图片描述


2. 挖坑法版本

算法步骤:

  1. 选择基准,留下一个"坑"
  2. 右指针向左找小于基准的数填到左坑
  3. 左指针向右找大于基准的数填到右坑
  4. 重复直到指针相遇
  5. 将基准填入最后的坑

int _QuickSort2(int* arr, int left, int right)
{int hole = left;int key = arr[hole];while (left < right){while (left < right && arr[right] > key){right--;}arr[hole] = arr[right];hole = right;while (left < right && arr[left] < key){left++;}arr[hole] = arr[left];hole = left;}arr[hole] = key;return hole;
}void QuickSort(int* arr, int left, int right)
{if (left >= right){return;}//找基准值int keyi = _QuickSort2(arr, left, right);QuickSort(arr, left, keyi - 1);QuickSort(arr, keyi + 1, right);
}

动图演示:
请添加图片描述


3. 双指针版本

算法步骤:

  1. 选择基准
  2. 使用两个指针:一个遍历指针,一个分界指针
  3. 分界指针左边都是小于基准的元素
  4. 遍历指针扫描整个数组,将小于基准的元素交换到分界指针左边

void Swap(int* a, int* b)
{int t = *a;*a = *b;*b = t;
}int _QuickSort3(int* arr, int left, int right)
{int prev = left, cur = prev + 1;int key = left;while (cur <= right){if (arr[cur] < arr[key] && ++prev != cur){Swap(&arr[prev], &arr[cur]);}cur++;}Swap(&arr[prev], &arr[key]);return prev;
}void QuickSort(int* arr, int left, int right)
{if (left >= right){return;}//找基准值int keyi = _QuickSort3(arr, left, right);QuickSort(arr, left, keyi - 1);QuickSort(arr, keyi + 1, right);
}

动图演示:

请添加图片描述

特点分析:

  • 时间复杂度:
    最好情况:O(n logn)
    最坏情况:O(n2)(每次选择基准值都是最大值和最小值)。
    平均情况:O(n logn)
  • 空间复杂度:O(logn)
  • 稳定性:三种版本都是不稳定排序。交换操作可能改变相等元素的相对顺序。

7. 归并排序

归并排序(Merge Sort),采用分治策略的排序算法。

核心思想:

1. 将数组递归地分成两半,直到每个子数组中只有一个元素。
2. 将两个已排序的子数组合并成一个有序的数组。

算法步骤:

  1. 分解
    例:原始数组: [8, 4, 5, 7, 1, 3, 6, 2]
    第1次分: [8, 4, 5, 7] 和 [1, 3, 6, 2]
    第2次分: [8, 4] [5, 7] 和 [1, 3] [6, 2]
    第3次分: [8] [4] [5] [7] [1] [3] [6] [2] ← 单个元素
  2. 第一层合并
    [8] 和 [4] → [4, 8]
    [5] 和 [7] → [5, 7]
    [1] 和 [3] → [1, 3]
    [6] 和 [2] → [2, 6]
    第二层合并
    [4, 8] 和 [5, 7] → [4, 5, 7, 8]
    [1, 3] 和 [2, 6] → [1, 2, 3, 6]
    第三层合并
    [4, 5, 7, 8] 和 [1, 2, 3, 6] → [1, 2, 3, 4, 5, 6, 7, 8]
    最终合并成有序的数组。

代码实现:

void _MergeSort(int* arr, int left, int right,int* tmp)
{if (left >= right){return;}int mid = (left + right) / 2;_MergeSort(arr, left, mid, tmp);_MergeSort(arr, mid + 1, right, tmp);int begin1 = left, end1 = mid;int begin2 = mid + 1, end2 = right;int index = left;while (begin1 <= end1 && begin2 <= end2){if (arr[begin1] < arr[begin2]){tmp[index++] = arr[begin1++];}else{tmp[index++] = arr[begin2++];}}while (begin1 <= end1){tmp[index++] = arr[begin1++];}while (begin2 <= end2){tmp[index++] = arr[begin2++];}for (int i = left;i <= right;i++){arr[i] = tmp[i];}
}void MergeSort(int* arr, int n)
{int* tmp = (int*)malloc(sizeof(int) * n);if (tmp == NULL){perror("malloc fail");exit(1);}_MergeSort(arr, 0, n - 1,tmp);free(tmp);tmp = NULL;
}

动图演示:

请添加图片描述

特点分析:

  • 时间复杂度:
    最好情况:O(n logn)
    最坏情况:O(n logn)
    平均情况:O(n logn)
  • 空间复杂度:O(n)
  • 稳定性:稳定。保持相等元素的相对顺序。

总结
以上就是本篇文章的全部了。排序算法就像计算机科学的一面镜子,反射出算法设计的智慧、效率与美的追求。从最简单的两两比较到精巧的分治策略,每一种算法都告诉我们:复杂的问题往往有优雅的解决方案。愿你在编程的道路上,既能欣赏简单算法的朴素美,也能领悟复杂算法的精妙处。记住,最好的算法不是最复杂的那个,而是最适合当前问题的那一个。排序结束,但探索永不停止。 🎯最后感谢大家的点赞、评论、收藏和关注。

往期回顾

《数据结构》——二叉树从概念到代码的详解
《数据结构》——轻松掌握队列(Queue)
力扣(LeetCode) ——100. 相同的树(C语言)

http://www.dtcms.com/a/354698.html

相关文章:

  • 详细介绍Linux 内存管理 struct page数据结构中有一个锁,请问trylock_page()和lock_page()有什么区别?
  • 开源工具新玩法:cpolar提升Penpot协作流畅度
  • 8.28日QT
  • 分布式锁过期危机:4大续命方案拯救超时任务
  • 2025年机械工程与机器人国际研讨会(CMER2025)
  • PAT 1086 Tree Traversals Again
  • React 动画库
  • 2025.8.28总结
  • Docker Swarm vs Kubernetes vs Nomad:容器编排方案对比与选型建议
  • GitHub宕机自救指南技术文章大纲
  • 图论基础篇
  • Oracle 数据库权限管理的艺术:从入门到精通
  • 【第四章】BS 架构测试全解析:从功能验证到问题定位​
  • @HAProxy 介绍部署使用
  • DM LSN 与 Oracle SCN 对比
  • UNIX网络编程笔记:共享内存区和远程过程调用
  • 机器学习基本概述
  • 小白入门:支持深度学习的视觉数据库管理系统
  • 神经网络为何能 “学习”?从神经元到深度学习模型的层级结构解析
  • 【OS】IO
  • 不同业务怎么选服务器?CPU / 内存 / 带宽配置表
  • [肥用云计算] Serverless 多环境配置
  • 【SpringBoot 版本升级整合Redis异常解决】Unable to connect to 127.0.0.1:6379
  • 云计算学习100天-第32天
  • InnoDB存储引擎底层拆解:从页、事务到锁,如何撑起MySQL数据库高效运转(上)
  • 音频转PCM
  • PCM转音频
  • 底层音频编程的基本术语 PCM 和 Mixer
  • docker 1分钟 快速搭建 redis 哨兵集群
  • GD32VW553-IOT OLED移植