排序--数据结构初阶(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

为省5-10美元差点毁库!Claude一条指令删光200万条数据、网站停摆24小时,创始人坦言:全是我的错

为省5-10美元差点毁库!Claude一条指令删光200万条数据、网站停摆24小时,创始人坦言:全是我的错

编译 | 屠敏 出品 | ZEEKLOG(ID:ZEEKLOGnews) AI 时代,一次看似普通的操作,竟能让整套生产环境与近 200 万条数据瞬间「归零」。 近日,数据科学社区 DataTalks.Club 创始人 Alexey Grigorev 就遭遇了这样的惊魂时刻,他在使用 AI 编程工具 Claude Code 管理网站服务器时,意外清空了平台积累 2.5 年的核心数据,甚至连数据库快照也未能幸免,导致网站停摆整整 24 小时。 这起事故不仅在开发者社区引发热议,更给所有依赖 AI 工具与自动化运维的从业者敲响了警钟。事后,Alexey Grigorev 公开复盘了整个过程,并揭露了此次事故的核心问题。让我们一起看看。 一次看似很普通的网站迁移 这场“删库”事件的前因,其实并不复杂。

By Ne0inhk
星标超 28 万,OpenClaw 两天两次大更!适配GPT 5.4,告别“抽卡式 Prompt”

星标超 28 万,OpenClaw 两天两次大更!适配GPT 5.4,告别“抽卡式 Prompt”

整理 | 梦依丹 出品 | ZEEKLOG(ID:ZEEKLOGnews) “We don’t do small releases.” 这是 OpenClaw 在发布 2026.3.7 版本时写下的一句话。 刚刚过去的周六与周日,这个 GitHub 星标已超 28 万 的 AI Agent 开源项目再次迎来两轮重量级更新。 两天两次更新:OpenClaw 做了一次“真正的大版本升级” 打开 OpenClaw 的 GitHub 更新日志,你会发现这次版本更新的规模确实不小。在 3 月 7 日发布更新后,第二天又迅速推出 2026.3.8-beta.1 和

By Ne0inhk
苹果最贵手机要来了!折叠屏iPhone将于9月亮相;部分高校严禁校内使用OpenClaw;黄仁勋预言:传统软件和APP或将消失 | 极客头条

苹果最贵手机要来了!折叠屏iPhone将于9月亮相;部分高校严禁校内使用OpenClaw;黄仁勋预言:传统软件和APP或将消失 | 极客头条

「极客头条」—— 技术人员的新闻圈! ZEEKLOG 的读者朋友们好,「极客头条」来啦,快来看今天都有哪些值得我们技术人关注的重要新闻吧。(投稿或寻求报道:[email protected]) 整理 | 郑丽媛 出品 | ZEEKLOG(ID:ZEEKLOGnews) 一分钟速览新闻点! * 多所高校要求警惕 OpenClaw 安全风险,部分严禁校内使用 * 荣耀 CEO 李健:荣耀机器人全栈自研,将聚焦消费市场 * 马化腾凌晨 2 点发声:还有一批龙虾系产品陆续赶来 * 前快手语言大模型中心负责人张富峥,已加入智源人工智能研究院,负责 LLM 方向 * 最新全球 AI 应用百强榜发布,豆包/DeepSeek/千问上榜 * 苹果折叠 iPhone 将于九月亮相,融合 iPhone 与 iPad 体验

By Ne0inhk
不止“996”!曝硅谷AI创业圈「极限工作制」:每天16小时、凌晨3点下班、周末也在写代码

不止“996”!曝硅谷AI创业圈「极限工作制」:每天16小时、凌晨3点下班、周末也在写代码

编译 | 郑丽媛 出品 | ZEEKLOG(ID:ZEEKLOGnews) “如果你周日去旧金山的咖啡馆,会发现几乎每个人都在工作。” 这是 AI 创业公司 Mythril 联合创始人 Sanju Lokuhitige 最近最直观的感受。去年 11 月,他特地搬到旧金山,只为了更接近 AI 创业浪潮的中心。但很快,他也被卷入了这股浪潮带来的另一面——一种越来越极端的工作文化。 Lokuhitige 坦言,他现在几乎每天工作 12 小时,每周 7 天。除了每周少数几场刻意安排的社交活动(主要是为了和创业者们建立联系),其余时间几乎都在写代码、做产品。 “有时候我整整一天都在编程,”他说,“我基本没有什么工作与生活的平衡。”而这样的生活,在如今的 AI 创业圈里并不算罕见。 旧金山 AI 创业圈的真实日常 一位在旧金山一家 AI

By Ne0inhk