C语言-排序

常见的排序算法分为以下四种,插入排序,选择排序,交换排序,归并排序。

一、插入排序

(一)直接插入排序

直接插入排序,将一段数组看做被分成已排序序列和未排序序列,排序过程是从未排序序列的元素开始,该元素与已排序序列的元素从后向前扫描,找到第一个小于(或大于)该元素的已排序项,然后将该未排序项插入到已排序序列中的适当位置。

void InsertSort(int* a, int n)
{//最后一组,[0, n-2]for (int i = 0; i < n - 1; i++)//是n不是n-1!!!{int end = i;int tmp = a[end + 1];//[0,end]是有序的,从end+1开始插入while (end >= 0){if (tmp < a[end])//比tmp值大的,往后挪{a[end + 1] = a[end];end--;}else{//不在里面放是因为,如果要插入的,比所有值都小//end此时为-1,循环结束,跳不到elsebreak;}}a[end + 1] = tmp;}
}

(二)希尔排序

希尔排序是针对直接插入排序的改进

将数组元素分为gap组,即每隔gap个的元素为一组,组内排序,然后修改gap值,当gap为1时,就是直接插入排序

void ShellSort(int* a, int n)
{int gap = n;while (gap > 1){//gap = gap / 2;//除几都可以,除3比较好,但是最后不能保证是1gap = gap / 3 + 1;//这样就保证了最后结果一定是1for (size_t i = 0; i < n - gap; i++){int end = i;int tmp = a[end + gap];while (end >= 0){if (tmp < a[end]){a[end + gap] = a[end];end -= gap;}elsebreak;}a[end + gap] = tmp;}OP()把打印注释//printf("gap:%2d->", gap);//PrintArray(a, n);}
}

二、选择排序

(一)选择排序

选择排序,遍历整个数组,每次选择出最大的

进行升级,每次遍历找出最小最大的

注意下面代码中的
        if (begin == maxi)
            maxi = mini;

这个是很有必要的

比如说begin开始是最大值,和mini交换之后,接下来最大maxi要和begin进行交换,但是begin此时的值不是最大值

void SelectSort(int* a, int n)
{int begin = 0, end = n - 1;while (begin < end){int mini = begin, maxi = begin;for (int i = begin + 1; i < end; i++)//从begin+1是因为,自己跟自己比没意义,<end也是{if (a[i] > a[maxi])maxi = i;if (a[i] < a[mini])mini = i;}Swap(&a[begin], &a[mini]);if (begin == maxi)maxi = mini;Swap(&a[end], &a[maxi]);++begin;--end;}
}

(二)堆排序

void AdjustDown(int* a, int n, int parent)
{// 先假设左孩子小int child = parent * 2 + 1;while (child < n)  // 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;}}
}
//堆排序
void HeapSort(int* a, int n)
{// 向下调整建堆 O(N)for (int i = (n - 1 - 1) / 2; i >= 0; i--){AdjustDown(a, n, i);}// O(N*logN)int end = n - 1;while (end > 0){Swap(&a[0], &a[end]);AdjustDown(a, end, 0);--end;}
}

三、交换排序

(一)冒泡排序

两层循环,内层单趟是找出最大的

void BubbleSort(int* a, int n)
{for (int i = 0; i < n; i++){//里面是单趟int flag = 0;for (int j = 0; j < n - 1 - i; j++){if (a[j - 1] > a[j]){Swap(&a[j - 1], &a[j]);flag = 1;}}if (flag == 0)break;}
}

(二)快速排序

1、hora方法

以第一个数为key,将比key小的元素放在基准元素的左边,将比key大的元素放在基准元素的右边。

遍历结束后,key左边所有的都比key小,右边都比key大,key就排序完成了,然后在递归调用快排函数排序左右两侧。

但是这样有个问题,假如数组一开始就有序,那么排序的深度很大(需要递归多次),这就可以使用三数取中来优化代码,left,right,以及(left+right)/2这三个是数的下标,找到中间值给left进行交换。

同时,快排的递归调用类似于满二叉树,后三层递归次数占总次数很大,可以使用小区间优化,当该区间元素个数不够时,使用一种排序方法排序,这里选择直接插入排序,(直接插入排序在小规模数据上表现良好)。

int GetMidi(int* a, int left, int right)
{//取得是大小在中间的值int midi = (left + right) / 2;if (a[left] < a[midi]){if (a[midi] < a[right])return midi;else if (a[left] < a[right])//走到这里说明a[left] < a[midi], a[right] < a[midi]return right;elsereturn left;}else//a[left] >= a[midi]{if (a[midi] > a[right])return midi;else if (a[left] < a[right])return left;elsereturn right;}
}
void QuickSort(int* a, int left, int right)
{if (left >= right)return;// 小区间优化,不再递归分割排序,减少递归的次数if ((right - left + 1) < 10){InsertSort(a + left, right - left + 1);//a+left是因为InsertSort需要数组起始地址}else{// 三数取中int midi = GetMidi(a, left, right);Swap(&a[left], &a[midi]);int keyi = left;int begin = left, end = right;while (begin < end){// 右边找小while (begin < end && a[end] >= a[keyi]){--end;}// 左边找大while (begin < end && a[begin] <= a[keyi]){++begin;}Swap(&a[begin], &a[end]);}//while运行完,左边全是比key小,右边全是比key大Swap(&a[keyi], &a[begin]);keyi = begin;// [left, keyi-1] keyi [keyi+1, right]QuickSort(a, left, keyi - 1);QuickSort(a, keyi + 1, right);}
}

2、前后指针方法

prev和cur

cur先向右移动,找到比key小的,然后++prev,交换prev和cur的值

遍历一遍,prev最后位置左边,都是比key值小的,跟Quick相似,左边全比他小,右边比key大

int PartSort2(int* a, int left, int right)
{三数取中//int midi = GetMidi(a, left, right);//Swap(&a[left], &a[midi]);int keyi = left;int prev = left;int cur = prev + 1;while (cur <= right)//还在区间{//由于一开始紧挨着,后面判断是为了防止自己交换//顺序也不能反,如果a[cur] > a[keyi],就不会执行后面的,也就是说cur++但是prev不++if (a[cur] < a[keyi] && ++prev != cur){Swap(&a[prev], &a[cur]);}//不要写else,cur无论什么情况都要++cur++;}Swap(&a[prev], &a[keyi]);return prev;
}void QuickSort(int* a, int left, int right)
{if (left >= right)return;// 小区间优化,不再递归分割排序,减少递归的次数if ((right - left + 1) < 10){InsertSort(a + left, right - left + 1);//a+left是因为InsertSort需要数组起始地址}else{int keyi = PartSort2(a,left,right);//修改此处即可// [left, keyi-1] keyi [keyi+1, right]QuickSort(a, left, keyi - 1);QuickSort(a, keyi + 1, right);}
}

3、非递归方法

快排非递归 -- 数据用栈模拟(队列也可以,队列先进先出,数据就不用倒着进)
 用栈类似于DFS(深度优先遍历)
 用队列类似于BFS(广度优先遍历 -- 二叉树的广度就是层序遍历)
递归会建立栈帧,(溢出跟深度有关)
在递归里,栈帧存的核心数据是 -- 区间(所以非递归方式,栈存放区间)
循环每走一次,取栈顶区间,进行单趟排序,右左子区间入栈(右左是因为栈后进先出)
 函数调用是在栈区取空间(栈只有8M)
 数据结构的栈空间不够去扩容是在堆(堆在32位下是2G)

void QuickSortNonR(int* a, int left, int right)
{ST st;STInit(&st);STPush(&st, right);STPush(&st, left);while (!STEmpty(&st))// 循环每走一次,相当于一次递归{int begin = STTop(&st);STPop(&st);int end = STTop(&st);STPop(&st);int keyi = PartSort2(a, begin, end);// [begin, keyi - 1] keyi [keyi + 1, end]// 先入右[keyi + 1, end]if (keyi + 1 < end){STPush(&st, end);STPush(&st, keyi + 1);}//再入左[begin, keyi - 1]if (begin < keyi - 1){STPush(&st, keyi - 1);STPush(&st, begin);}}
}

四、归并排序

假设把数组分成两段,如果左右区间有序,两个指针指向左右区间第一个,一个比另一个小,就比所有的小
把小的尾插到一个tmp数组
类似二叉树的后序,有序就归并,无序就递归

1、递归

void _MergeSort(int* a, int* tmp, int begin, int end)//_从习惯和风格上是子函数
{//只有一个值,返回if (begin >= end)return;int midi = (begin + end) / 2;//[begin , midi] [midi + 1, end] -- 有序就可以进行归并//不能是[begin , midi - 1] [midi, end]!!!//假设begin=0,end=1,此时midi=0,如果-1得到的分组就是[0,-1],[0,1]//[偶数,偶数+1]分组得到,[偶数,偶数],[偶数,偶数+1] -- [偶数,偶数+1]一直出现,造成栈溢出x//子问题递归来实现有序_MergeSort(a, tmp, begin, midi);_MergeSort(a, tmp, midi + 1, end);//归并int begin1 = begin, end1 = midi;int begin2 = midi+1, end2 = end;int i = begin;//有一个满足是结束的条件,while循环里要写继续的条件!!!while (begin1 <= end1 && begin2 <=end2){if (a[begin1] <= a[begin2])tmp[i++] = a[begin1++];elsetmp[i++] = a[begin2++];}//执行完之后,只排序了一个,再把两个里面的都进一遍就行while(begin1 <= end1)tmp[i++] = a[begin1++];while(begin2 <= end2)tmp[i++] = a[begin2++];//数据还在tmp数组中,需要拷贝回去memcpy//不是拷整个区间,是begin到endmemcpy(a + begin, tmp + begin, (end - begin + 1) * sizeof(int));
}
void MergeSort(int* a, int n)
{int* tmp = (int*)malloc(sizeof(int) * n);if (tmp == NULL){perror("malloc fail!");return;}_MergeSort(a, tmp, 0, n-1);free(tmp);tmp = NULL;
}

2、非递归

 使用非递归 -- 一开始每两个数归并,然后每四个数归并...把递归倒着看
 gap -- 每组归并数据的数据个数(gap是其中一组归并数据的个数)
 gap, gap 这样归并,所以数据个数*2
 [i, i + gap -1], [gap + i,i + 2*gap - 1]

void MergeSortNonR(int* a, int n)
{int* tmp = (int*)malloc(sizeof(int) * n);if (tmp == NULL){perror("malloc fail");return;}int gap = 1;while (gap < n){for (int i = 0; i < n; i += 2 * gap)//i代表每组归并的起始位置{//[begin1,end1][begin2,end2]//end = 起点 + 个数 - 1int begin1 = i, end1 = i + gap - 1;int begin2 = i + gap, end2 = i + gap * 2 - 1;printf("[%d, %d] [%d, %d]", begin1, end1, begin2, end2);//第二组都越界 -- [begin1,end1] n [begin2,end2]if (begin2 >= n){break;}//第二组begin2没越界,end2越界,需要修正一下继续归并 -- [begin1,end1][begin2, (n) end2]if (end2 >= n){end2 = n - 1;}int j = i;while (begin1 <= end1 && begin2 <= end2){if (a[begin1] <= a[begin2])//归并排序,这里加个=才是稳定的,当相等的时候取前一个tmp[j++] = a[begin1++];elsetmp[j++] = a[begin2++];}while (begin1 <= end1)tmp[j++] = a[begin1++];while (begin2 <= end2)tmp[j++] = a[begin2++];memcpy(a + i, tmp + i, (end2 - i + 1) * sizeof(int));}gap *= 2;printf("\n");}free(tmp);tmp = NULL;
}

五、各个排序的时间复杂度及稳定性

稳定性指的是相同的值排完后,相对顺序不变

直接插入排序

两层循环

时间复杂度O(N^2),每次插入大约移动一半元素

最坏时间复杂度O(N^2),数组完全逆序

稳定,因为插入操作不会改变相同元素的相对顺序

希尔排序

时间复杂度O(N^1.3) - O(N^2),O(N^1.3)即可

最坏时间复杂度O(N^2),取决于分组情况gap

不稳定,元素的移动可能跨越多个间隔,导致相同元素相对顺序改变

选择排序

时间复杂度O(N^2),每次插入大约移动一半元素

最坏时间复杂度O(N^2),数组完全逆序

不稳定,每次选择最小元素并交换,会改变相同元素的相对顺序

堆排序

时间复杂度O(nlogn),

最坏时间复杂度O(nlogn),

由于堆的调整操作,无论输入数组的初始状态如何,时间复杂度不变

不稳定,在调整堆的过程中,会交换元素,可能改变相同元素的相对顺序

冒泡排序

时间复杂度O(N^2),

最坏时间复杂度O(N^2),当数组完全逆序时

稳定,相邻元素比较和交换,相同元素不会交换位置

快速排序

时间复杂度O(nlogn),

最坏时间复杂度O(N^2),当每次选择的基准元素都为当前子数组中的最大或最小元素时,导致划分极不均衡,递归深度达到最大

不稳定,分区过程中,基准元素的交换可能导致相同元素相对顺序改变

归并排序

时间复杂度O(nlogn),

最坏时间复杂度O(nlogn),

无论数组初始状态如何,通过不断将数组对半划分并合并,所以其时间复杂度不会变

稳定,在合并过程中,可以保证相同元素的相对顺序不变

    while (begin1 <= end1 && begin2 <=end2)
    {
        if (a[begin1] <= a[begin2])
            tmp[i++] = a[begin1++];
        else
            tmp[i++] = a[begin2++];
    }

if中的<=,=可以确保相同值时相对顺序不变(递归非递归都是这个=)

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

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

相关文章

【Java笔记】LinkedList 底层结构

一、LinkedList 的全面说明 LinkedList底层实现了双向链表和双端队列特点可以添加任意元素(元素可以重复)&#xff0c;包括null线程不安全&#xff0c;没有实现同步 二、LinkedList 的底层操作机制 三、LinkedList的增删改查案例 public class LinkedListCRUD { public stati…

网管平台(基础篇):路由器的介绍与管理

路由器简介 路由器&#xff08;Router&#xff09;是一种计算机网络设备&#xff0c;它的主要作用是将数据通过打包&#xff0c;并按照一定的路径选择算法&#xff0c;将网络传送至目的地。路由器能够连接两个或更多个网络&#xff0c;并根据信道的情况自动选择和设定路由&…

排序算法(2):选择排序

问题 排序 [30, 24, 5, 58, 18, 36, 12, 42, 39] 选择排序 选择排序每次从待排序序列中选出最小&#xff08;或最大&#xff09;的元素&#xff0c;将其放到序列的起始位置&#xff0c;然后&#xff0c;再从剩余未排序元素中继续寻找最小&#xff08;或最大&#xff09;元素…

Tongweb8命令行使用收集(by lqw)

文章目录 声明对应版本修改thanos用户密码部署应用到默认实例节点相关操作新增节点(一般一个服务器ip只能装一个节点)启动节点(需确认节点没有运行)停止节点删除节点节点新增应用节点查看应用节点启动应用节点停止应用节点卸载应用(谨慎操作,卸载后应用就没有了,建议备份后…

Artec Leo3D扫描仪在重型机械设备定制中的应用【沪敖3D】

挑战&#xff1a;一家加拿大制造商需要有效的方法&#xff0c;为富于变化且难度较高的逆向工程&#xff0c;快速、安全、准确地完成重型机械几何采集。 解决方案&#xff1a;Artec Leo, Artec Studio, Geomagic for SOLIDWORKS 效果&#xff1a;Artec Leo三维扫描代替过去的手动…

题海拾贝:力扣 141.环形链表

Hello大家好&#xff01;很高兴我们又见面啦&#xff01;给生活添点passion&#xff0c;开始今天的编程之路&#xff01; 我的博客&#xff1a;<但凡. 我的专栏&#xff1a;《编程之路》、《数据结构与算法之美》、《题海拾贝》 欢迎点赞&#xff0c;关注&#xff01; 1、题…

Vite快速构建Vue教程

步骤 1: 初始化项目目录 创建一个名为 projects 的文件夹&#xff0c;作为存放所有 Vite 项目的根目录。这个文件夹将容纳多个独立的 Vite 项目。 步骤 2: 创建 Vite 项目 右键点击 projects 文件夹并选择“在此处打开终端”或使用您偏好的代码编辑器&#xff08;如 VSCode&…

深入理解 CSS 文本换行: overflow-wrap 和 word-break

前言 正常情况下&#xff0c;在固定宽度的盒子中的中文会自动换行。但是&#xff0c;当遇到非常长的英文单词或者很长的 URL 时&#xff0c;文本可能就不会自动换行&#xff0c;而会溢出所在容器。幸运的是&#xff0c;CSS 为我们提供了一些和文本换行相关的属性&#xff1b;今…

HarmonyOS 5.0应用开发——属性动画

【高心星出品】 文章目录 属性动画animateTo属性动画animation属性动画 属性动画 属性接口&#xff08;以下简称属性&#xff09;包含尺寸属性、布局属性、位置属性等多种类型&#xff0c;用于控制组件的行为。针对当前界面上的组件&#xff0c;其部分属性&#xff08;如位置属…

《探索视频数字人:开启未来视界的钥匙》

一、引言 1.1视频数字人技术的崛起 在当今科技飞速发展的时代&#xff0c;视频数字人技术如一颗璀璨的新星&#xff0c;正逐渐成为各领域瞩目的焦点。它的出现&#xff0c;犹如一场科技风暴&#xff0c;彻底改变了传统的视频制作方式&#xff0c;为各个行业带来了前所未有的机…

免费下载 | 2024算网融合技术与产业白皮书

《2024算网融合技术与产业白皮书&#xff08;2023年&#xff09;》的核心内容概括如下&#xff1a; 算网融合发展概述&#xff1a; 各国细化算网战略&#xff0c;指引行业应用创新升级。 算网融合市场快速增长&#xff0c;算力互联成为投资新热点。 算网融合产业模式逐渐成型…

基于卷积神经网络的图像二分类检测模型训练与推理实现教程 | 幽络源

前言 对于本教程&#xff0c;说白了&#xff0c;就是期望能通过一个程序判断一张图片是否为某个物体&#xff0c;或者说判断一张图片是否为某个缺陷。因为本教程是针对二分类问题&#xff0c;因此主要处理 是 与 不是 的问题&#xff0c;比如我的模型是判断一张图片是否为苹果…

RabbitMQ个人理解与基本使用

目录 一. 作用&#xff1a; 二. RabbitMQ的5中队列模式&#xff1a; 1. 简单模式 2. Work模式 3. 发布/订阅模式 4. 路由模式 5. 主题模式 三. 消息持久化&#xff1a; 消息过期时间 ACK应答 四. 同步接收和异步接收&#xff1a; 应用场景 五. 基本使用 &#xff…

前端怎么预览pdf

1.背景 后台返回了一个在线的pdf地址&#xff0c;需要我这边去做一个pdf的预览&#xff08;需求1&#xff09;&#xff0c;并且支持配置是否可以下载&#xff08;需求2&#xff09;&#xff0c;需要在当前页就能预览&#xff08;需求3&#xff09;。之前我写过一篇预览pdf的文…

滑动窗口算法专题

滑动窗口简介 滑动窗口就是利用单调性&#xff0c;配合同向双指针来优化暴力枚举的一种算法。 该算法主要有四个步骤 1. 先进进窗口 2. 判断条件&#xff0c;后续根据条件来判断是出窗口还是进窗口 3. 出窗口 4.更新结果&#xff0c;更新结果这个步骤是不确定的&#xff0c…

C# 中的Task

文章目录 前言一、Task 的基本概念二、创建 Task使用异步方法使用 Task.Run 方法 三、等待 Task 完成使用 await 关键字使用 Task.Wait 方法 四、处理 Task 的异常使用 try-catch 块使用 Task.Exception 属性 五、Task 的延续使用 ContinueWith 方法使用 await 关键字和异步方法…

【AIGC】如何高效使用ChatGPT挖掘AI最大潜能?26个Prompt提问秘诀帮你提升300%效率的!

还记得第一次使用ChatGPT时&#xff0c;那种既兴奋又困惑的心情吗&#xff1f;我是从一个对AI一知半解的普通用户&#xff0c;逐步成长为现在的“ChatGPT大神”。这一过程并非一蹴而就&#xff0c;而是通过不断的探索和实践&#xff0c;掌握了一系列高效使用的技巧。今天&#…

浩辰CAD教程004:柱梁板

文章目录 柱梁板标准柱角柱构造柱柱齐墙边绘制梁绘制楼板 柱梁板 标准柱 绘制标准柱&#xff1a; ①&#xff1a;点选插入柱子②&#xff1a;沿着一根轴线布置柱子③&#xff1a;指定的矩形区域内的轴线交点插入柱子 替换现有柱子&#xff1a;选择替换之后的柱子形状&#x…

UNIX数据恢复—UNIX系统常见故障问题和数据恢复方案

UNIX系统常见故障表现&#xff1a; 1、存储结构出错&#xff1b; 2、数据删除&#xff1b; 3、文件系统格式化&#xff1b; 4、其他原因数据丢失。 UNIX系统常见故障解决方案&#xff1a; 1、检测UNIX系统故障涉及的设备是否存在硬件故障&#xff0c;如果存在硬件故障&#xf…

桥接模式的理解和实践

桥接模式&#xff08;Bridge Pattern&#xff09;&#xff0c;又称桥梁模式&#xff0c;是一种结构型设计模式。它的核心思想是将抽象部分与实现部分分离&#xff0c;使它们可以独立地进行变化&#xff0c;从而提高系统的灵活性和可扩展性。本文将详细介绍桥接模式的概念、原理…