【数据结构】排序算法——Lessen1

Hi~!这里是奋斗的小羊,很荣幸您能阅读我的文章,诚请评论指点,欢迎欢迎 ~~
💥💥个人主页:奋斗的小羊
💥💥所属专栏:C语言

🚀本系列文章为个人学习笔记,在这里撰写成文一为巩固知识,二为展示我的学习过程及理解。文笔、排版拙劣,望见谅。


目录

  • 前言
  • 一、常见排序算法
    • 1、插入排序
      • 1.1直接插入排序
      • 1.2希尔排序
    • 2、选择排序
      • 2.1直接选择排序
      • 2.2堆排序
    • 3、交换排序
      • 3.1冒泡排序
      • 3.2快速排序
        • 3.2.1 hoare版本
        • 3.2.2 挖坑法
        • 3.2.3 前后指针法

前言

所谓排序,就是将一组数据按照某种顺序进行排列的过程。
排序在很多应用中都非常重要,比如数据分析、搜索算法、数据库管理等。
本篇文章将详细介绍常见的排序算法。


一、常见排序算法

1、插入排序

1.1直接插入排序

插入排序是一种简单直观的排序算法,它的基本思想是将待排序的元素分为已排序和未排序两部分,然后逐步将未排序的元素插入到已排序的部分中,直到所有元素都被排序。

动图演示:

请添加图片描述

插入排序是比较容易理解的,代码实现也比较简单,唯一需要注意的是下标的取值范围,因为我们是从下标为1的元素开始向前比较的,所以下标最大的取值应该为n-2,这样保证我们刚好取到下标为n-1的最后一个元素。

代码示例:

//插入排序(升序)
void InsertSort(int* arr, int n)
{//先单次,再整体for (int i = 0; i < n - 1; i++){//假设[0,end]有序,将end+1位置的元素插入int end = i;int tmp = arr[end + 1];while (end >= 0){if (tmp < arr[end]){arr[end + 1] = arr[end];end--;}else{break;}}arr[end + 1] = tmp;}
}

插入排序时间复杂度是O(N^2),总体来说效率也是不高的,但在某些场景中还是有发挥空间。


1.2希尔排序

希尔排序是一种插入排序的改进版本,以增量gap(通常是gap = n/3 + 1)来划分元素,使得远离的元素能够交换,从而加快整体排序的速度。它的基本思想是将待排序的序列分成若干个子序列(也称为“增量”),对每个子序列进行直接插入排序,然后逐步减少增量(gap = gap / 3 + 1),最终进行一次普通的插入排序,从而实现整体排序。

  • 当gap > 1时都是预排序,目的是让数组更接近于有序
  • gap = gap / 3 + 1;后面+1是为了保证最后一次gap的值为1

当gap=1时,相当于直接插入排序,希尔排序是直接插入排序算法上改进而来的,综合来说它的效率肯定是要高于直接插入排序的。

在这里插入图片描述

代码示例:
(1)

//希尔排序
void ShellSort(int* arr, int n)
{//一组一组排int gap = n;while (gap > 1){gap = gap / 3 + 1;for (int i = 0; i < gap; i++){int end = i;int tmp = arr[end + gap];for (int j = i; j < n - gap; j += gap){while (end >= 0){if (tmp < arr[end]){arr[end + gap] = arr[end];end -= gap;}else{break;}}arr[end + gap] = tmp;}}}
}

(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){if (tmp < arr[end]){arr[end + gap] = arr[end];end -= gap;}else{break;}}arr[end + gap] = tmp;}}
}

这两种方式都是可以的,虽然循环层数不一样,但是时间复杂度是一样的。
希尔排序的时间复杂度比较特别,理想情况下希尔排序的时间复杂度为:O(N^1.3)。希尔排序和堆排序是一个量级的。

根据希尔排序的特点,可以总结出下面的折线图:
在这里插入图片描述

因此,希尔排序在最初和最后的排序次数都为n,即前一阶段排序次数逐渐上升,当到达某一顶点时,排序次数逐渐下降到n,而该顶点非常难计算。

因为gap的取值很多,导致希尔排序的时间复杂度不好计算,我们只需记住当n在某个特定返回内,时间复杂度约为O(N^1.3)


2、选择排序

2.1直接选择排序

选择排序的基本思想是每一次从待排序的数组中选出最小(或最大)的一个数,存在起始位置,知道全部数据排完。为了提高效率,我们每次从待排序数组中同时找到最小和最大的两个数,分别将它们放到起始、末尾位置。

动图演示:

请添加图片描述

代码示例:

//选择排序
void SelectSort(int* arr, int n)
{int begin = 0;int 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;}}Swap(&arr[mini], &arr[begin]);if (begin == maxi){maxi = mini;}Swap(&arr[maxi], &arr[end]);begin++;end--;}
}

直接选择排序的时间复杂度是O(N^2),效率很低,甚至还比不过冒泡排序,因此在实际中基本不使用,主要用于教学。


2.2堆排序

堆排序在之前的文章中有详解介绍,这里就不再赘述。

void HeapSort(int* arr, int n)
{//升序,建大堆//降序,建小堆//向下调整建堆//O(N)for (int i = (n-1-1) / 2; i >= 0; i--){AdjustDown(arr, i, n);}//O(N*logN)int end = n - 1;while (end > 0){Swap(&arr[0], &arr[end]);AdjustDown(arr, 0, end);end--;}
}

3、交换排序

3.1冒泡排序

冒泡排序是一种简单的排序算法,其基本原理是通过重复遍历待排序的元素,比较相邻的两个元素,如果顺序错误则交换它们,直到没有需要交换的元素为止。

当某一趟并未发生交换说明此时数组已经有序,可以提前退出循环,我们可以设定一个标志来判断某一趟是否未发生交换。

动图演示:

请添加图片描述

代码示例:

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

冒泡排序的时间复杂度是O(N^2),效率是比较低的,在实际应用中很少用到,主要用于教学。


3.2快速排序

快速排序是一种二叉树结构的交换排序法,其基本思想是:任取待排序元素序列中的某元素作为基准值,按照该排序码将待排序集合分割成两个子序列,左子序列中所有元素均小于基准值,右子序列中所有元素均大于基准值,然后最左右子序列重复该过程,直到所有元素都排列有序。

快排主框架:

void _QuickSort(int* arr, int left, int right)
{//递归结束条件if (left >= right){return;}//_QuickSort用于按照基准值将区间[left, right)中的元素进行划分QuickSort(arr, left, keyi - 1);QuickSort(arr, keyi + 1, right);
}

将区间中的元素进行划分的_QuickSort方法主要有以下几种实现方法。


3.2.1 hoare版本

| 算法思路: (默认排升序)

  • 创建左右指针,确定基准值
  • 从右向左找出比基准值小的数据,从左向右找出比基准值大的数据,左右指针数据交换,进入下次循环
  • 如果在最左边找基准值,则右边先走,反之则左边先走

动图演示:

请添加图片描述

我们可以将最左边的数作为基准值,先从右向左找小于基准值的数记住当前位置end,然后从左向右找大于基准值的数记住当前位置begin,交换两个数的位置;重复上述操作直到beginend指向同一位置,此时这个位置的值一定小于基准值,最后再将这个位置的值与基准值交换位置,此时作为基准值的数已经排到了它相应的位置。

代码示例:

//快速排序
void QuickSort(int* arr, int left, int right)
{if (left >= right){return;}int keyi = left;int begin = left;int end = right;while (begin < end){//右边找小while (begin < end && arr[end] >= arr[keyi]){end--;}//左边找大while (begin < end && arr[begin] <= arr[keyi]){begin++;}Swap(&arr[begin], &arr[end]);}Swap(&arr[begin], &arr[keyi]);keyi = begin;QuickSort(arr, left, keyi - 1);QuickSort(arr, keyi + 1, right);
}

但是此时的快排代码还有一些缺陷,当待排集合已经接近有序时,快排的效率是很低的,如果数据量比较大还会因为函数递归太深而导致栈溢出。
因为我们将最左边的数当做基准值时,如果这个数恰好是最小或比较小的数,此时从右向左的数基本都比这个基准值大,所以会有很多次的递归调用。

| 优化一:
解决这个问题的办法就是我们找基准值时尽量找数据中不大不小的数,可以粗略的从数据头、数据中、数据尾三个位置的值中选中间值作为基准值,这样就可以很大的降低递归太深而导致栈溢出的风险。

//三数取中
int GetMidi(int* arr, int left, int right)
{int midi = (left + right) / 2;if (arr[left] < arr[right]){if (arr[right] < arr[midi ]){return right;}else if (arr[left] < arr[midi ])//arr[left]<arr[right] &&//arr[midi ]<arr[right]{return midi ;}else{return left;}}else{if (arr[right] > arr[midi ]){return right;}else if (arr[left] > arr[midi ]){return midi ;}else{return left;}}
}

拿到理想的基准值后,为了保证我们之前写的按最左边数作为基准值,可以将这个基准值和最左边的数交换位置。

//快速排序
void QuickSort(int* arr, int left, int right)
{if (left >= right){return;}int midi = GetMidi(arr, left, right);Swap(&arr[left], &arr[midi ]);int keyi = left;int begin = left;int end = right;while (begin < end){//右边找小while (begin < end && arr[end] >= arr[keyi]){end--;}//左边找大while (begin < end && arr[begin] <= arr[keyi]){begin++;}Swap(&arr[begin], &arr[end]);}Swap(&arr[begin], &arr[keyi]);keyi = begin;QuickSort(arr, left, keyi - 1);QuickSort(arr, keyi + 1, right);
}

| 优化二:
我们知道快速排序是二叉树结构的排序算法,当二叉树的层数较高时,此时分割的区间数据量比较小,而递归次数又占总体的大部分,可以考虑小区间优化,不再递归分割排序,减少递归的次数。
当数据量比较小时,排序算法的效率基本一致,我们优先考虑简单且较为高效的插入排序

//快速排序
void QuickSort(int* arr, int left, int right)
{if (left >= right){return;}//小区间优化,不再递归分割排序,减少递归次数if ((right - left) + 1 < 10){InsertSort(arr + left, (right - left) + 1);}else{//三数取中int midi = GetMidi(arr, left, right);Swap(&arr[left], &arr[midi ]);int keyi = left;int begin = left;int end = right;while (begin < end){//右边找小while (begin < end && arr[end] >= arr[keyi]){end--;}//左边找大while (begin < end && arr[begin] <= arr[keyi]){begin++;}Swap(&arr[begin], &arr[end]);}Swap(&arr[begin], &arr[keyi]);keyi = begin;QuickSort(arr, left, keyi - 1);QuickSort(arr, keyi + 1, right);}
}

有了这两个优化,快排的效率就能得到很大的提升。


3.2.2 挖坑法

| 算法思路: (默认排升序)

首先将最左边的数据作为基准值拿出来,则原位置形成一个空位,创建左右指针,从右向左找出比基准值小的数据,找到后立即放入左边坑中,当前位置变为新的“坑”,重复上述操作直到左右指针相遇,最后再将基准值放入左右指针共同指向的“坑”中。

动图演示:

请添加图片描述
代码示例:

//挖坑法
void QuickSort(int* arr, int left, int right)
{//递归结束条件if (left >= right){return;}//区间优化,减少递归次数if (right - left + 1 < 10){InsertSort(arr + left, right - left + 1);}else{//三数取中int midi = GetMidi(arr, left, right);Swap(&arr[left], &arr[midi]);int keyi = left;int tmp = arr[keyi];int begin = left;int end = right;while (begin < end){while (begin < end && arr[end] >= tmp){end--;}arr[keyi] = arr[end];while (begin < end && arr[begin] <= tmp){begin++;}if (begin == end){arr[begin] = tmp;break;}arr[end] = arr[begin];keyi = begin;}keyi = begin;QuickSort(arr, left, keyi - 1);QuickSort(arr, keyi + 1, right);}
}

挖坑法虽然没有效率的提升,但是相对于hoare版本还是有一些优势:

  • 挖坑法不用分析为什么左边取基准值而右边先走的问题,因为当左边取基准值时左边就自然而然的形成了一个“坑”
  • 也不用分析为什么相遇位置一定比基准值小(或大)的问题,因为它相遇的位置是坑,自然而然的基准值就应该放在坑里

3.2.3 前后指针法

| 算法思路:
创建前后指针,两个指针从左向右分别找遇到的第一个小于和大于基准值的数进行交换,将所有大于基准值的数全都推到最后,使得小的都排在基准值的左边,大的都排在基准值的右边。

动图演示:
请添加图片描述
代码实现:

//前后指针法
int QuickSort3(int* arr, int left, int right)
{//三数取中int midi = GetMidi(arr, left, right);Swap(&arr[left], &arr[midi]);int keyi = left;int prev = left;int cur = prev + 1;while (cur <= right){//先判断大小,再++prev,保证了当不满足大小时prev不再++if (arr[cur] < arr[keyi] && ++prev != cur){Swap(&arr[prev], &arr[cur]);}cur++;}Swap(&arr[keyi], &arr[prev]);return prev;
}

这三种快排算法,前后指针法代码是最为清晰明了的,但三种算法时间复杂度是一样的。

| 三种快排算法汇总:

//三数取中
int GetMidi(int* arr, int left, int right)
{int midi = (left + right) / 2;if (arr[left] < arr[right]){if (arr[right] < arr[midi]){return right;}else if (arr[left] < arr[midi]){return midi;}else{return left;}}else{if (arr[right] > arr[midi]){return right;}else if (arr[left] > arr[midi]){return left;}else{return midi;}}
}//hoare法
int QuickSort1(int* arr, int left, int right)
{//三数取中int midi = GetMidi(arr, left, right);Swap(&arr[left], &arr[midi]);int keyi = left;int begin = left + 1;int end = right;while (begin < end){//左边作基值,右边先走while (begin < end && arr[end] >= arr[keyi]){end--;}while (begin < end && arr[begin] <= arr[keyi]){begin++;}Swap(&arr[begin], &arr[end]);}Swap(&arr[begin], &arr[keyi]);return begin;
}//挖坑法
int QuickSort2(int* arr, int left, int right)
{//三数取中int midi = GetMidi(arr, left, right);Swap(&arr[left], &arr[midi]);int keyi = left;int tmp = arr[keyi];int begin = left + 1;int end = right;while (begin < end){while (begin < end && arr[end] >= tmp){end--;}arr[keyi] = arr[end];while (begin < end && arr[begin] <= tmp){begin++;}if (begin == end){arr[begin] = tmp;break;}arr[end] = arr[begin];keyi = begin;}return begin;
}//前后指针法
int QuickSort3(int* arr, int left, int right)
{//三数取中int midi = GetMidi(arr, left, right);Swap(&arr[left], &arr[midi]);int keyi = left;int prev = left;int cur = prev + 1;while (cur <= right){//先判断大小,再++prev,保证了当不满足大小时prev不再++if (arr[cur] < arr[keyi] && ++prev != cur){Swap(&arr[prev], &arr[cur]);}cur++;}Swap(&arr[keyi], &arr[prev]);return prev;
}void QuickSort(int* arr, int left, int right)
{//递归结束条件if (left >= right){return;}//区间优化,减少递归次数if (right - left + 1 < 10){InsertSort(arr + left, right - left + 1);}else{int keyi = QuickSort3(arr, left, right);QuickSort(arr, left, keyi - 1);QuickSort(arr, keyi + 1, right);}
}

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

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

相关文章

【动态专修】2024年五菱维修手册和电路图资料更新

经过整理&#xff0c;2017-2024年五菱汽车全系列已经更新至汽修帮手资料库内&#xff0c;覆盖市面上99%车型&#xff0c;包括维修手册、电路图、新车特征、车身钣金维修数据、全车拆装、扭力、发动机大修、发动机正时、保养、电路图、针脚定义、模块传感器、保险丝盒图解对照表…

人、智能、机器人……

在遥远的未来之城&#xff0c;智能时代如同晨曦般照亮了每一个角落&#xff0c;万物互联&#xff0c;机器智能与人类智慧交织成一幅前所未有的图景。这座城市&#xff0c;既是科技的盛宴&#xff0c;也是人性与情感深刻反思的舞台。 寓言&#xff1a;《智光与心影》 在智能之…

Python自动化DevOps任务入门

目录 Python自动化DevOps任务入门 一、环境和工具配置 1. 系统环境与Python版本 2. 虚拟环境搭建 3. 必要的库安装 二、自动化部署 1. 使用Fabric进行流式部署 2. 使用Ansible编写部署剧本 三、持续集成和测试 1. 配置CI/CD工具 选择工具 配置工具 构建和测试自动…

【SLAM】最最最简单的直线拟合情形下的多种求解方法

本文我们讨论一个最最最简单情况下的拟合的情形&#xff0c;并尝试使用不同的方法来进行求解。 假如有一组数 x 1 , x 2 , x 3 , . . . , x n x_1,x_2,x_3,...,x_n x1​,x2​,x3​,...,xn​&#xff0c;对应的值为 y 1 , y 2 , y 3 , . . . , y n y_1,y_2,y_3,...,y_n y1​,y2…

10.11和10.8那个大(各种ai的回答)

问题&#xff1a;10.11和10.8两个数哪个大 腾讯混元 ✔️ chatGPT ❎ 通义千问 ❎ 文心一言 ✔️ 智谱清言 ❎ 讯飞星火 ✔️ C知道 ❎ 豆包 ✔️

TCP粘包问题详解和解决方案【C语言】

1.什么是TCP粘包 TCP&#xff08;Transmission Control Protocol&#xff0c;传输控制协议&#xff09;是一种面向连接的、可靠的、基于字节流的传输协议&#xff0c;它保证了数据的可靠性和顺序性。然而&#xff0c;由于TCP是基于字节流而不是消息的&#xff0c;因此在传输过…

【接口自动化_08课_Pytest+Yaml+Allure框架】

上节课一些内容 的补充 1、openxl这个方法&#xff0c;第一个元素是从1开始的&#xff0c;不是从0开始 回写的列在程序里写的是11&#xff0c;是因为是固定值 一、1. Yaml入门及应用 1、什么是yaml YAML&#xff08;/ˈjməl/&#xff0c;尾音类似camel骆驼&#xff09;是一…

Finding columns with a useful data type 找到合适的数据列的类型

Finding columns with a useful data type 在确定了原始查询的数据列数之后&#xff0c;接下来就是要确定合适的数据列的数据类型。可以用 SELECT a 的方式判断对应的数据列方式&#xff0c;有时候可以通过错误信息判断数据列的类型。如果服务器的响应没有报错&#xff0c;而…

Docker启动PostgreSql并设置时间与主机同步

在 Docker 中启动 PostgreSql 时&#xff0c;需要配置容器的时间与主机同步。可以通过在 Dockerfile 或者 Docker Compose 文件中设置容器的时区&#xff0c;或者使用宿主机的时间来同步容器的时间。这样可以确保容器中的 PostgreSql 与主机的时间保持一致&#xff0c;避免在使…

启动流程和切换流程

启动流程 #mermaid-svg-iUWGw8xl1SyAmoo9 {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-svg-iUWGw8xl1SyAmoo9 .error-icon{fill:#552222;}#mermaid-svg-iUWGw8xl1SyAmoo9 .error-text{fill:#552222;stroke:#552222;}#…

C语言·分支和循环语句(超详细系列·全面总结)

前言&#xff1a;Hello大家好&#x1f618;&#xff0c;我是心跳sy&#xff0c;为了更好地形成一个学习c语言的体系&#xff0c;最近将会更新关于c语言语法基础的知识&#xff0c;今天更新一下分支循环语句的知识点&#xff0c;我们一起来看看吧~ 目录 一、什么是语句&#xf…

linux配置podman阿里云容器镜像加速器

1.下载podman yum install -y podman systemctl status podman systemctl start podman 2.获取阿里云个人容器镜像加速器地址 访问阿里云官网&#xff1a;首先&#xff0c;您需要访问阿里云&#xff08;Alibaba Cloud&#xff09;的官方网站。阿里云官网的URL是&#xff1a;…

OS:处理机进程调度

1.BackGround&#xff1a;为什么要进行进程调度&#xff1f; 在多进程环境下&#xff0c;内存中存在着多个进程&#xff0c;其数目往往多于处理机核心数目。这就要求系统可以按照某种算法&#xff0c;动态的将处理机CPU资源分配给处于就绪状态的进程。调度算法的实质其实是一种…

对于品牌方来说,小红书探店应该怎么做?

小红书是中国最大的生活分享社交平台之一&#xff0c;它现在不仅仅是一个购物推荐平台&#xff0c;也是一个探店的好去处。 用户在网络上看到一家心仪的店铺&#xff0c;却又无法亲身到访&#xff0c;对店铺的产品存疑&#xff0c;这时候就会在小红书搜索具体的相关店铺信息。 …

【代码随想录】【算法训练营】【第58天 4】 [卡码104]建造最大岛屿

前言 思路及算法思维&#xff0c;指路 代码随想录。 题目来自 卡码网。 day 58&#xff0c;周四&#xff0c;ding~ 题目详情 [卡码104] 建造最大岛屿 题目描述 卡码104 建造最大岛屿 LeetCode类似题目827 最大人工岛 解题思路 前提&#xff1a; 思路&#xff1a; 重点…

【LeetCode】从前序与中序遍历序列构造二叉树

目录 一、题目二、解法完整代码 一、题目 给定两个整数数组 preorder 和 inorder &#xff0c;其中 preorder 是二叉树的先序遍历&#xff0c; inorder 是同一棵树的中序遍历&#xff0c;请构造二叉树并返回其根节点。 示例 1: 输入: preorder [3,9,20,15,7], inorder [9…

在西藏上大学是一种什么体验?如何解决语言问题?

在西藏地区上大学是一种独特而难忘的经历&#xff0c;它不仅提供了接触壮丽自然风光和深入了解藏族文化的机会&#xff0c;也带来了适应高原气候和生活方式的挑战。学生将在这里体验到丰富的教育资源和学术研究素材&#xff0c;同时在生活和人际交往中培养适应能力和独立性。这…

密码学基础-Hash、MAC、HMAC 的区别与联系

密码学基础-Hash、MAC、HMAC 的区别与联系 Hash Hash 是一种从一段数据中创建小的数字“指纹”的方法。就像一个人的指纹代表一个人的信息一样&#xff0c;Hash 对输入的数据进行整理&#xff0c;生成一个代表该输入数据的“指纹” 数据。通常该指纹数据也可称之为摘要、散列…

文件包含漏洞: 函数,实例[pikachu_file_inclusion_local]

文件包含 文件包含是一种较为常见技术&#xff0c;允许程序员在不同的脚本或程序中重用代码或调用文件 主要作用和用途&#xff1a; 代码重用&#xff1a;通过将通用函数或代码段放入单独的文件中&#xff0c;可以在多个脚本中包含这些文件&#xff0c;避免重复编写相同代码。…

RabbitMQ的学习和模拟实现|Protobuf的介绍和简单使用

protbuf的介绍和简单使用 项目仓库&#xff1a;https://github.com/ffengc/HareMQ protobuf的安装&#xff1a;README-cn.md#环境配置 基于Protobuf的一个小项目&#xff1a;基于protobuf和httplib的在线通讯录项目框架&#xff5c;Protobuf应用小项目 protobuf是什么 Pro…