排序--数据结构初阶(4)(C/C++)

排序--数据结构初阶(4)(C/C++)

文章目录

前言

这是数据结构初阶的最后一期,虽然来说在C++的库函数里面有sort函数可以代替这里所有的方法,并且时间复杂度也是优于他们的,但是sort函数是由他们写出来的,因此,还是是有必要学习一下的

理论部分:

这里的代码实现都是按升序来的 排序的话建议先写单趟再写整体 这些排序在两数相等的时候一般是不进行操作的(一般这么写) 

1.直接插入排序

就是目前在最后的那个数跟前面每个数比,看看要插哪

时间复杂度:O(n2

最好的情况下是O(n)

在小段小段有序时有极大的优势(相对于选择排序跟冒泡排序)
代码实现: void InsertSort(int* a, int n) { for (int i = 0; i < n-1; i++) { int end = i; int tmp = a[i+1]; // 将tmp插入到[0,end]区间中,保持有序 while (end >= 0) { if (tmp < a[end]) { a[end + 1] = a[end]; --end; } else { break; } } a[end + 1] = tmp; } } 

2.希尔排序

主要思想:先预排序,再直接插入排序(也就是对插入排序的一种优化)

这里的预排序就是以gap为间隔来分组去排,首个为i的话,那就是i,i+gap,……来先排一波

这里的gap的计算有些人取得gap/=2;有些人取得gap = gap/3+1

个人比较喜欢gap/=2

时间复杂度:O(n1.3) n是多少个数
void ShellSort(int* a, int n) { int gap = n; while (gap > 1) { gap /= 2; for (int i = 0; i < n - gap; i++) { int end = i; int tmp = a[i + gap]; while (end >= 0) { if (tmp < a[end]) { a[end + gap] = a[end]; end -= gap; } else { break; } } a[end + gap] = tmp; } } } 

3.直接选择排序

核心思想:遍历一遍选出当前遍历中最小的和最大的,把最小的放在头,最大的放在尾,然后这两个位置固定,重复操作即可

时间复杂度:O(n2
代码实现: void SelectSort(int* a, int n) { int left = 0, right = n - 1; while (left < right) { int mini = left, maxi = left; for (int i = left + 1; i <= right; i++) { if (a[i] < a[mini]) { mini = i; } if (a[i] > a[maxi]) { maxi = i; } } Swap(&a[left], &a[mini]); // 如果left和maxi重叠,交换后修正一下 if (left == maxi) { maxi = mini; } Swap(&a[right], &a[maxi]);//这里的Swap是自己之前实现的那个 //不是C++的函数那个swap,所以这里传的是地址 ++left; --right; } } 

4.堆排序

时间复杂度:O(n*logn)
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; } } } void HeapSort(int* a, int n) { // 建堆 -- 向下调整建堆 -- O(N) for (int i = (n - 1 - 1) / 2; i >= 0; --i) { AdjustDown(a, n, i); } //给数组排序 int end = n - 1; while (end > 0) { Swap(&a[end], &a[0]); AdjustDown(a, end, 0); --end; } } 

5.冒泡排序

核心思想:前后俩个数依次比较,每次都选出最大的数到最后,然后end–;

时间复杂度:O(n2)

最好的情况下是O(n)

这个排序的话也就适合教学用用,实际中基本不会用(但也要记)
void BubbleSort(int* a, int n) { for (int j = 0; j < n; j++) { bool exchange = false; for (int i = 1; i < n-j; i++) { if (a[i - 1] > a[i]) { Swap(&a[i - 1], &a[i]); exchange = true; } } if (exchange == false) { break; } } } 

6.快速排序

时间复杂度:O(n2)(没有加三数取中或随机取key优化时)

最好情况下是O(n*logn)

空间复杂度:O(logn)

快速排序有三种方法:霍尔(Hoare),挖坑法和前后指针法
三数取中: int GetMidNumi(int* a, int left, int right) { int mid = (left + right) / 2; if (a[left] < a[mid]) { if (a[mid] < a[right]) { return mid; } else if (a[left] > a[right]) { return left; } else { return right; } } else // 也就是a[left] >= a[mid] { if (a[mid] > a[right]) { return mid; } else if (a[left] < a[right]) { return left; } else { return right; } } } 随机取key: int randi = left + (rand() % (right - left)); Swap(&a[left], &a[randi]); 总结:最好使用三数取中 
前后指针法的核心:

1.cur找到比key小的值,++prev,cur和prev位置的值交换,++cur

挖坑法的核心思想:

1.一边找到其对应的符合的值就把坑移到哪,然后轮到无坑的那个去找

2.最后把坑的位置里面的值填成key

Hoare的核心思想:

1.先找一边,一边找到了轮到另一边找

2.然后把相遇位置的值改成key

总结:实际过程中比较推荐使用前后指针法,不容易出错。但是那两种方法也要掌握
// Hoare int PartSort1(int* a, int left, int right) { // 这里以三数取中举例 int midi = GetMidNumi(a, left, right); if (midi != left) Swap(&a[midi], &a[left]); int keyi = left; while (left < right) { // 右边找小 while (left < right && a[right] >= a[keyi]) --right; // 左边找大 while (left < right && a[left] <= a[keyi]) ++left; Swap(&a[left], &a[right]); } Swap(&a[keyi], &a[left]); keyi = left; return keyi; } // 挖坑法 int PartSort2(int* a, int left, int right) { // 这里以三数取中举例 int midi = GetMidNumi(a, left, right); if (midi != left) Swap(&a[midi], &a[left]); //这里最好换行一下,不然可读性差 int key = a[left]; int hole = left;//->注意刚开始坑的位置 while (left < right) { // 右边找小 while (left < right && a[right] >= key) --right; a[hole] = a[right]; hole = right; // 左边找大 while (left < right && a[left] <= key) ++left; a[hole] = a[left]; hole = left; } a[hole] = key; return hole; } // 前后指针法 int PartSort3(int* a, int left, int right) { // 这里以三数取中举例 int midi = GetMidNumi(a, left, right); if (midi != left) Swap(&a[midi], &a[left]); int keyi = left; int prev = left; int cur = left + 1; while (cur <= right) { if (a[cur] < a[keyi] && ++prev != cur)//->这里++prev!=cur非常巧妙 Swap(&a[cur], &a[prev]); ++cur; } Swap(&a[prev], &a[keyi]); keyi = prev; return keyi; } void QuickSort(int* a, int left, int right) { if (left >= right) return; int keyi = PartSort3(a, left, right);//这里以第三种举例 QuickSort(a, left, keyi - 1); QuickSort(a, keyi+1, right); } 相比上面那个,下面这个进行了小区间优化: void QuickSort(int* a, int left, int right) { if (left >= right) return; // 小区间优化--小区间直接使用插入排序--优化时间复杂度 if ((right - left + 1) > 10)//这个不一定取10,但是一般是取10左右是最好的 { int keyi = PartSort3(a, left, right); QuickSort(a, left, keyi - 1); QuickSort(a, keyi + 1, right); } else { InsertSort(a+left, right - left + 1);//插入排序 } } 
在用快速排序时要注意:(Hoare和挖坑法要用到)

要左边做key,让右边先走,这样才能保证相遇位置的值比key要小或者相遇位置就是现在key所在的位置

证明:

相遇的几种情况:(R表示right,L表示left)

1.R找到小,L找大没有找到->L遇到R

2.R找小,没有找到->直接遇到L

…………之后就好证了

类似道理,右边做key,左边先走,相遇位置就比key要大

(降序的话,把这个改一下就行,但是还是要左边取大,右边取小)
快速排序的非递归形式: 核心:1.栈里面取一段区间,单趟排序 2.单趟分割子区间入栈 3.子区间只有一个值或者不存在就不入栈 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 = PartSort3(a, begin, end); // [begin,keyi-1] keyi [keyi+1, end] if (keyi + 1 < end) { STPush(&st, end); STPush(&st, keyi+1); } if (begin < keyi-1) { STPush(&st, keyi-1); STPush(&st, begin); } } STDestroy(&st); } 
递归改非递归是必备技能:
1.面试官会问
2.工作时如果用递归导致栈溢出了,需要改非递归

递归改非递归的常用方法:用栈,栈的特性很符合改递归成非递归(这个方法也有可能不行)

归并排序

时间复杂度:O(n*logn) n为元素个数

空间复杂度:O(n)

核心思想:把区间一直分,分到有序之后;再两个有序区间比较,小的尾插入tmp数组中,这样依次扩大有序区间
void _MergeSort(int* a, int begin, int end, int* tmp) { if (begin >= end) return; int mid = (begin + end) / 2; // [begin, mid] [mid+1,end],子区间递归排序 _MergeSort(a, begin, mid, tmp); _MergeSort(a, mid+1, end, tmp); // [begin, mid] [mid+1,end]归并 int begin1 = begin, end1 = mid; int begin2 = mid+1, end2 = end; int i = begin; while (begin1 <= end1 && begin2 <= end2) { if (a[begin1] < a[begin2]) { tmp[i++] = a[begin1++]; } else { tmp[i++] = a[begin2++]; } } while (begin1 <= end1) { tmp[i++] = a[begin1++]; } while (begin2 <= end2) { tmp[i++] = a[begin2++]; } memcpy(a + begin, tmp + begin, sizeof(int) * (end - begin + 1)); //这里可以用for循环替代(memcpy一般都可以这样替代) //每次归并完了记得拷贝回去 } void MergeSort(int* a, int n) { int* tmp = (int*)malloc(sizeof(int) * n); if (tmp == NULL) { perror("malloc fail\n"); return; } _MergeSort(a, 0, n - 1, tmp); free(tmp); } 
改成非递归: gap是归并中,每组数据的个数 void MergeSortNonR(int* a, int n) { int* tmp = (int*)malloc(sizeof(int) * n); if (tmp == NULL) { perror("malloc fail\n"); return; } int gap = 1; while (gap < n) { for (int i = 0; i < n; i += 2 * gap) { // [begin1,end1][begin2, end2],是这两个一起排 int begin1 = i, end1 = i + gap - 1; int begin2 = i + gap, end2 = i + 2 * gap - 1; //处理越界情况 if (end1 >= n || begin2 >= n) { break; } if (end2 >= n) { end2 = n - 1; } int j = i; while (begin1 <= end1 && begin2 <= end2) { if (a[begin1] < a[begin2]) { tmp[j++] = a[begin1++]; } else { tmp[j++] = a[begin2++]; } } while (begin1 <= end1) { tmp[j++] = a[begin1++]; } while (begin2 <= end2) { tmp[j++] = a[begin2++]; } // 归并一部门拷贝一部分(一把梭哈的话也行,但是分类那里的操作就要改了) memcpy(a+i, tmp+i, sizeof(int) *(end2-i+1)); } gap *= 2; } free(tmp); } 

非比较排序

有1.计数排序 2.基数排序 3.桶排序 但是2和3在实际中没有应用价值,校招也不考,就不讲了

计数排序

适用范围:适合范围集中的整形数组排序(负数也行)

注意:要是整形才行,比如字符串,浮点数那些就不行

时间复杂度:O(N+range)–这样写好 空间复杂度:O(range)

核心思想:1.先统计每个数据出现的次数(相对位置法) 2.然后再排序即可
void CountSort(int* a, int n) { int max = a[0], min = a[0]; for (int i = 1; i < n; i++) { if (a[i] > max) { max = a[i]; } if (a[i] < min) { min = a[i]; } } int range = max - min + 1; int* countA = (int*)malloc(sizeof(int) * range); if (countA == NULL) { perror("malloc fail\n"); return; } memset(countA, 0, sizeof(int) * range); // 计数 for (int i = 0; i < n; i++) { countA[a[i] - min]++;//这里用的相对位置的那个方法,绝对位置那个不好用(而且负数不行) } // 排序 int j = 0; for (int i = 0; i < range; i++) { while (countA[i]--) { a[j++] = i + min; } } free(countA); } 
排序里面对于稳定和不稳定概念的理解:(计数排序一般不谈这个)(会考多久不稳定) 稳定排序:如果排序算法能够保证相等的元素在排序后的顺序与排序前的顺序相同,则称该算法是稳定的 也就是假如两个都是5,在排序过程中第一个5永远在第二个5前面才行(注意是永远!) 注意!!!:稳定不稳定,是一种就事论事的问题;但是大家都会按照最初版本(自己这写的)去定型。 如果给出了代码,那就要按具体情况去分析 1.冒泡排序--稳定 2.选择排序--不稳定 场景: 本为2(1) 2(2) 1 1之后会变成2(2) 1 1 2(1) 3.直接插入排序--稳定 4.希尔排序--不稳定 场景:相同的数据可能会被分到不同的组去进行预排 5.堆排序--不稳定 场景: 9 9 8 7 8 (图在下面) 6.归并排序--稳定 7.快速排序--不稳定 场景:看下面的图 
在这里插入图片描述


在这里插入图片描述

作业部分

对n个元素执行快速排序,需要的额外空间的大小为( C ) A.O(1) B.O(n) C.O(logn) D.O(nlogn) 这里的递归里面没有占据空间,但是一次递归本身就会占用O(1)空间 (注意这里的递归的操作对象是那n个元素,这n个元素不需要每次递归都开辟) 
下列选项中,(如果是升序的话)不可能是快速排序第2趟排序后的结果的是(C) A.2 3 5 4 6 7 9 B.2 7 5 6 4 3 9 C.3 2 5 4 7 6 9 D.4 2 3 5 7 6 9 技巧:因为第二趟完了,则应该有两个数的左边都比他小,右边都比他大 注意:没有说明的话,不能默认是升序 
下列排序方法中,哪一种是不稳定的(C) A.直接插入排序 B.归并排序 C.选择排序 //当然选择排序也可以变成稳定的,只要保证相同的值选择第一个就可以 //但是默认写的那一种选择不行 D.冒泡排序 
下列关于归并排序的说法中正确的是(C) A.归并排序不需要辅助空间 B.归并排序的时间复杂度是O(logn) C.归并排序是稳定排序 D.归并排序的操作方式类似二叉树的前序遍历//这个应该是后序遍历 
在询问有关排序的问题时,一般默认是还没优化的(区分优化和不同实现方法),也没有递归改成非递归的 快速排序初始数据集的排列顺序对算法的性能也有影响 拓展:其他排序都是内排序(在内存中排序),归并排序既可以是内排序,也可以是外排序(在磁盘中排序) 
工程级别的代码,逻辑性是最重要的 分类讨论可以简化问题! 

Read more

【OpenClaw从入门到精通】第10篇:OpenClaw生产环境部署全攻略:性能优化+安全加固+监控运维(2026实测版)

【OpenClaw从入门到精通】第10篇:OpenClaw生产环境部署全攻略:性能优化+安全加固+监控运维(2026实测版)

摘要:本文聚焦OpenClaw从测试环境走向生产环境的核心痛点,围绕“性能优化、安全加固、监控运维”三大维度展开实操讲解。先明确生产环境硬件/系统选型标准,再通过硬件层资源管控、模型调度策略、缓存优化等手段提升响应速度(实测响应效率提升50%+);接着从网络、权限、数据三层构建安全防护体系,集成火山引擎安全方案拦截高危操作;最后落地TenacitOS可视化监控与Prometheus告警体系,配套完整故障排查清单和虚拟实战案例。全文所有配置、代码均经实测验证,兼顾新手入门实操性和进阶读者的生产级部署需求,帮助开发者真正实现OpenClaw从“能用”到“放心用”的跨越。 优质专栏欢迎订阅! 【DeepSeek深度应用】【Python高阶开发:AI自动化与数据工程实战】【YOLOv11工业级实战】 【机器视觉:C# + HALCON】【大模型微调实战:平民级微调技术全解】 【人工智能之深度学习】【AI 赋能:Python 人工智能应用实战】【数字孪生与仿真技术实战指南】 【AI工程化落地与YOLOv8/v9实战】【C#工业上位机高级应用:高并发通信+性能优化】 【Java生产级避坑指南:

By Ne0inhk
ARM Linux 驱动开发篇--- Linux 并发与竞争实验(互斥体实现 LED 设备互斥访问)--- Ubuntu20.04互斥体实验

ARM Linux 驱动开发篇--- Linux 并发与竞争实验(互斥体实现 LED 设备互斥访问)--- Ubuntu20.04互斥体实验

🎬 渡水无言:个人主页渡水无言 ❄专栏传送门: 《linux专栏》《嵌入式linux驱动开发》《linux系统移植专栏》 ❄专栏传送门: 《freertos专栏》《STM32 HAL库专栏》 ⭐️流水不争先,争的是滔滔不绝  📚博主简介:第二十届中国研究生电子设计竞赛全国二等奖 |国家奖学金 | 省级三好学生 | 省级优秀毕业生获得者 | ZEEKLOG新星杯TOP18 | 半导纵横专栏博主 | 211在读研究生 在这里主要分享自己学习的linux嵌入式领域知识;有分享错误或者不足的地方欢迎大佬指导,也欢迎各位大佬互相三连 目录 前言  一、实验基础说明 1.1、互斥体简介 1.2 本次实验设计思路 二、硬件原理分析(看过之前博客的可以忽略) 三、实验程序编写 3.1 互斥体 LED 驱动代码(mutex.c) 3.2.1、设备结构体定义(28-39

By Ne0inhk
Flutter for OpenHarmony:swagger_dart_code_generator 接口代码自动化生成的救星(OpenAPI/Swagger) 深度解析与鸿蒙适配指南

Flutter for OpenHarmony:swagger_dart_code_generator 接口代码自动化生成的救星(OpenAPI/Swagger) 深度解析与鸿蒙适配指南

欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.ZEEKLOG.net 前言 后端工程师扔给你一个 Swagger (OpenAPI) 文档地址,你会怎么做? 1. 对着文档,手写 Dart Model 类(容易写错字段类型)。 2. 手写 Retrofit/Dio 的 API 接口定义(容易拼错 URL)。 3. 当后端修改了字段名,你对着报错修半天。 这是重复劳动的地狱。 swagger_dart_code_generator 可以将 Swagger (JSON/YAML) 文件直接转换为高质量的 Dart 代码,包括: * Model 类:支持 json_serializable,带 fromJson/

By Ne0inhk
Linux 开发别再卡壳!makefile/git/gdb 全流程实操 + 作业解析,新手看完直接用----《Hello Linux!》(5)

Linux 开发别再卡壳!makefile/git/gdb 全流程实操 + 作业解析,新手看完直接用----《Hello Linux!》(5)

文章目录 * 前言 * make/makefile * 文件的三个时间 * Linux第一个小程序-进度条 * 回车和换行 * 缓冲区 * 程序的代码展示 * git指令 * 关于gitee * Linux调试器-gdb使用 * 作业部分 前言 做 Linux 开发时,你是不是也遇到过这些 “卡脖子” 时刻?写 makefile 时,明明语法没错却报错,最后发现是依赖方法行没加 Tab;想提交代码到 gitee,记不清 git add/commit/push 的 “三板斧”,还得反复搜教程;用 gdb 调试程序,输了命令没反应,才想起编译时没加-g生成 debug 版本;甚至连写个进度条,都搞不懂\r和\n的区别,导致进度条乱跳…… 其实这些问题,

By Ne0inhk