【初阶数据结构】星河中的光影 “排” 象:排序(下)

【初阶数据结构】星河中的光影 “排” 象:排序(下)

文章目录

接上一篇,解决掉剩余的排序方法,本篇有些许难度,建议巩固好上一篇再来进行本篇的学习

传送门:【初阶数据结构】星河中的光影 “排” 象:排序(上)

4.交换排序

4.1 冒泡排序(BubbleSort)

基本思想: 所谓交换,就是根据序列中两个记录键值的比较结果来对换这两个记录在序列中的位置,交换排序的特点是:将键值较大的记录向序列的尾部移动键值较小的记录向序列的前部移动

冒泡排序在C语言部分进行过详细的解析,这里就不过多赘述

传送门:关于我、重生到500年前凭借C语言改变世界科技vlog.14——常见C语言算法

4.2 快速排序(QuickSort)

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

4.2.1 hoare版本

动图理解:

请添加图片描述

💻排序实现:

voidQuickSort1(int* a,int left,int right){if(left >= right)return;int begin = left, end = right;//随机选keyint randi = left +(rand()%(right - left +1));Swap(&a[left],&a[randi]);//三数取中//int midi = GetMidNumi(a, left, right);//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;QuickSort1(a, begin, keyi -1);QuickSort1(a, keyi +1, end);}

⌨️代码解读:

  1. if (left >= right), 如果左边界大于等于右边界,说明子数组已经有序或为空,直接返回
  2. 随机选 key
  3. 当左边界小于右边界时,继续分区操作。右边找小,从右向左找到第一个小于基准元素的元素;左边找大,从左向右找到第一个大于基准元素的元素。交换左右找到的元素
  4. 最后将基准元素放到正确的位置,更新基准元素的索引
  5. 此时基准元素所放置的位置就是正确的排序位置,基准元素左右两边任然无序,所以对左右两边进行循环操作,每循环一次就确定一个数的位置

🔥值得注意的是:
选取 key 的方式有三种

  1. 选第一个数为 key
  2. 三数取中值
intGetMidNumi(int* a,int left,int right){int mid =(left + right)/2;if(a[left]< a[mid]){if(a[left]> a[right])return left;elseif(a[mid]< a[right])return mid;elsereturn right;}else//a[left] > a[mid]{if(a[mid]> a[right])return mid;elseif(a[right]> a[left])return left;elsereturn right;}}
  1. 随机选 key,经过算法研究发现该选 key 方式是最有效的

4.2.2 挖坑法

动图理解:

请添加图片描述

💻排序实现:

voidQuickSort2(int* a,int left,int right){if(left >= right)return;int begin = left, end = right;//随机选keyint randi = left +(rand()%(right - left +1));Swap(&a[left],&a[randi]);//三数取中//int midi = GetMidNumi(a, left, right);//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;QuickSort2(a, begin, hole -1);QuickSort2(a, hole +1, end);}

⌨️代码解读:

  1. 选择左边界元素作为基准元素 key,并将其位置标记为 hole(坑)
  2. 从右向左扫描,找到第一个小于 key 的元素,将其填入 hole 位置,并更新 hole 为该元素的位置。
  3. 从左向右扫描,找到第一个大于 key 的元素,将其填入 hole 位置,并更新 hole 为该元素的位置。
  4. 不断重复上述两个步骤,直到 leftright 相遇。
  5. 最后将基准元素 key 填入最终的 hole 位置

🔥值得注意的是: 在从右向左查找小于 key 的元素时,right 指针会不断向左移动。如果不添加 left < right 这个条件,right 指针可能会一直向左移动,越过 left 指针,导致访问到数组范围之外的元素,从而引发越界错误。例如,当数组中的元素都大于等于 key 时,如果没有 left < right 的限制,right 指针会一直向左移动,最终可能访问到数组的负索引位置,这是不合法的。从左向右查找大于 key 的元素也是同理

4.2.3 前后指针法

动图理解:

请添加图片描述

💻排序实现:

voidQuickSort3(int* a,int left,int right){if(left >= right)return;int randi = left +(rand()%(right - left +1));Swap(&a[left],&a[randi]);//定义基准值以及cur和destint keyi = left;int dest = left, cur = left +1;while(cur <= right){if(a[cur]< a[keyi]&&++dest != cur){Swap(&a[cur],&a[dest]);}//无论cur位置的值是否小于基准值,cur都要++,所以留到这里做调整 cur++;}Swap(&a[keyi],&a[dest]);QuickSort3(a, left, dest -1);QuickSort3(a, dest +1, right);}

⌨️代码解读:

  1. keyi 是基准元素的索引,初始化为左边界 leftdest 指针用于标记小于基准元素的元素应该放置的位置,初始化为左边界 leftcur 指针用于遍历数组,初始化为 left + 1
  2. 如果 cur 位置的值小于基准值,要和 dest 后面的元素进行交换,但是有可能 dest 后面就是 cur,所以我们可以让 dest++ ,再和 cur 比较,如果 ++dest == cur,说明它们暂时指向同一个元素,无需交换;如果 ++dest != cur,说明它们不指向同一个元素,直接交换

🖱️复杂度分析:

时间复杂度:

最好情况: 当每次选择的基准元素都能将数组均匀地分成两部分时,递归树的深度为 log n ,每层需要处理的元素总数为 n,因此总的时间复杂度为 O(n * log n)

最坏情况: 最坏情况下,快速排序的时间复杂度会退化为 O(n²)

平均情况: 时间复杂度为 O(n * log n)

空间复杂度: 时间复杂度为 O(log n)

总结:快速排序整体的综合性能和使用场景都是比较好的,所以才敢叫快速排序

4.2.4 非递归实现

因为上面的三种方法均涉及递归,考虑到递归太多数据会导致栈溢出的风险,所以非递归实现快排也很重要

那么根据我们学过的发现其原理:先进后出,比较符合我们排序的效果

这里我们引入栈的头文件和源文件

传送门:【初阶数据结构】先来后到的秩序:栈和队列

💻排序实现:

intPartSort(int* a,int left,int right){if(left >= right)return;int randi = left +(rand()%(right - left +1));Swap(&a[left],&a[randi]);int keyi = left;int dest = left, cur = left +1;while(cur <= right){if(a[cur]< a[keyi]&&++dest != cur){Swap(&a[cur],&a[dest]);} cur++;}Swap(&a[keyi],&a[dest]);return dest;}voidQuickSortNonR(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 =PartSort(a, begin, 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. 将初始的排序区间 [left, right] 压入栈中。注意,先压入右边界 right,再压入左边界 left,后续出栈时会先得到左边界,符合后续处理逻辑
  2. 从栈中弹出一个区间 [begin, end],先弹出的是左边界 begin,再弹出右边界 end
  3. 调用 PartSort 函数对当前区间 [begin, end] 进行分区操作,得到基准元素的最终位置 keyi
  4. 根据基准元素的位置 keyi,将左右子区间压入栈中。如果基准元素右边还有元素(keyi + 1 < end),则将右子区间 [keyi + 1, end] 压入栈;如果基准元素左边还有元素(begin < keyi - 1),则将左子区间 [begin, keyi - 1] 压入栈。这样后续会继续对这些子区间进行排序

🔥值得注意的是: 非递归的方式本质上就是借助栈来存区间,然后 PartSort(a, begin, end) 会对数进行交换排序,进行实质的交换

5.归并排序(MergeSort)

5.1 递归实现

动图理解:

请添加图片描述
在这里插入图片描述


假设有这么个区间,两个有序区间,就是不断对两个移动指针比较,把较小的数插入到新空间,形成新的有序序列,然后再赋值回原来的空间

💻排序实现:

void_MergeSort(int* a,int begin,int end,int* tmp){if(begin >= end)return;int mid =(begin + end)/2;_MergeSort(a, begin, mid, tmp);_MergeSort(a, mid +1, end, tmp);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));}voidMergeSort(int* a,int n){int* tmp =(int*)malloc(sizeof(int)* n);if(tmp ==NULL){perror("malloc fail");return;}_MergeSort(a,0, n -1, tmp);free(tmp);}

⌨️代码解读:

  1. 计算中间索引 mid,将当前数组分成两个子数组,分别递归调用 _MergeSort 函数对左右子数组进行排序,从最下面的子数组依次往上归并
  2. 初始化两个指针 begin1begin2 分别指向左右子数组的起始位置,end1end2 指向左右子数组的结束位置
  3. 比较 a[begin1]a[begin2] 的大小,将较小的元素放入临时数组 tmp 中,并将相应的指针后移
  4. 当其中一个子数组遍历完后,将另一个子数组中剩余的元素依次放入临时数组 tmp
  5. 使用 memcpy 函数将临时数组 tmp 中排好序的元素复制回原数组 a

🔥值得注意的是:memcpy(a + begin, tmp + begin, sizeof(int) * (end - begin + 1)),而不是 memcpy(a, tmp, sizeof(int) * (end - begin + 1)),为了在临时数组中准确找到当前子数组合并结果的起始位置,以便将排好序的数据正确地复制回原数组

5.2 非递归实现

同理,为了避免栈溢出归并排序也有非递归实现

为什么不用栈处理?

栈的非递归实现其实是类似于前序遍历的,而归并的非递归实现类似于后序遍历

💻排序实现:

5.2.1 一次性全部拷贝回去:memcpy(a, tmp, sizeof(int) * n)

voidMergeSortNonR1(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){int begin1 = i, end1 = i + gap -1;int begin2 = i + gap, end2 = i +2* gap -1;if(end1 >= n){ end1 = n -1; begin2 = n; end2 = n -1;}elseif(begin2 >= n){ begin2 = n; end2 = n -1;}elseif(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, tmp,sizeof(int)* n);} gap *=2;}free(tmp);}

⌨️代码解读:

在这里插入图片描述


gap 表示一组有多少个数据,gap = 1 时一个一个归并,gap = 2 时两个两个归并,依次向后推,不断扩大 gap 实现有序

根据规律写出 int begin1 = i, end1 = i + gap - 1int begin2 = i + gap, end2 = i + 2 * gap - 1

但是我们发现这套规律只适用于偶数个数的归并,当为奇数时会出现`多出来一个数无法进行归并的情况,所以我们要针对这种情况进行修正

  1. end1 越界示例
    假设我们有一个长度 n = 5 的数组 arr = [1, 2, 3, 4, 5],当 gap = 4 时,开始进行合并操作
在这里插入图片描述

计算过程:

i = 0 时:begin1 = i = 0end1 = i + gap - 1 = 0 + 4 - 1 = 3,这是正常的,因为数组索引 3 在有效范围内(数组索引范围是 04

i = 4 时:begin1 = i = 4end1 = i + gap - 1 = 4 + 4 - 1 = 7,而数组的最大有效索引是 4,此时 end1 超出了数组的边界,出现越界情况

修正方法:end1 = n - 1begin2 = nend2 = n - 1,因为是整体赋值,让有效的数据按原路返回,后面的修正为一个不存在的区间,也就不会归并了,那么就有个问题,后面的数不就无序了吗?其实前面的排序保证了其有序性,在 gap 进一步扩大的过程中会将其逐步变为有序

  1. begin2 越界示例
    同样使用长度 n = 5 的数组 arr = [1, 2, 3, 4, 5],当 gap = 3 时进行分析
在这里插入图片描述

计算过程:

i = 2 时:begin1 = i = 2end1 = i + gap - 1 = 2 + 3 - 1 = 4,这是正常的,begin2 = i + gap = 2 + 3 = 5,数组的最大有效索引是 4,所以 begin2 超出了数组的边界,出现越界情况

修正方法:end1 越界处理情况相同

  1. end2 越界示例

还是以长度 n = 5 的数组 arr = [1, 2, 3, 4, 5] 为例,当 gap = 3 时进行分析

在这里插入图片描述

计算过程:

i = 0 时:begin1 = i = 0end1 = i + gap - 1 = 0 + 3 - 1 = 2begin2 = i + gap = 0 + 3 = 3end2 = i + 2 * gap - 1 = 0 + 2 * 3 - 1 = 5,数组的最大有效索引是 4,所以 end2 超出了数组的边界,出现越界情况

修正方法:end2 = n - 1,将有效数据纳入进行排序,剩余的数在 gap 扩大时会得到处理

5.2.2 分批次拷贝回去:memcpy(a + i, tmp + i, sizeof(int) * (end2 - i + 1))

voidMergeSortNonR2(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){int begin1 = i, end1 = i + gap -1;int begin2 = i + gap, end2 = i +2* gap -1;if(end1 >= n || begin2 >= n){break;}elseif(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);}

⌨️代码解读:

一次性拷贝回去需要调整 end1、begin1 等边界:

一次性拷贝回去意味着我们要把合并好的结果一次性从临时数组 tmp 复制回原数组 a。在这个过程中,如果出现越界情况,比如 end1end2 超出了数组长度,我们需要调整这些边界,以确保只处理有效的数组元素,避免访问非法内存

分批次拷贝回去直接 break:

分批次拷贝回去通常是指在发现越界情况时,由于后续没有有效的子数组可供合并,直接终止当前的合并操作,等待下一轮更大的 gap 再处理剩余元素。这种方式简化了越界处理逻辑,因为不需要对越界的边界进行复杂的调整

end1、begin2 越界:break,等待 gap 增大时处理未处理的数据

end2 越界: 和一次性拷贝的处理相同

🔥值得注意的是:memcpy(a + i, tmp + i, sizeof(int) * (end2 - i + 1)) 不能写成 memcpy(a + begin1, tmp + begin1, sizeof(int) * (end2 - begin1 + 1)),因为 begin1 被移动了,i 才是原来的初始位置

6.排序复杂度及效率对比

请添加图片描述

💻测试代码:

voidTestOP(){srand(time(NULL));constint N =100000;int* a1 =(int*)malloc(sizeof(int)* N);int* a2 =(int*)malloc(sizeof(int)* N);int* a3 =(int*)malloc(sizeof(int)* N);int* a4 =(int*)malloc(sizeof(int)* N);int* a5 =(int*)malloc(sizeof(int)* N);int* a6 =(int*)malloc(sizeof(int)* N);int* a7 =(int*)malloc(sizeof(int)* N);int* a8 =(int*)malloc(sizeof(int)* N);int* a9 =(int*)malloc(sizeof(int)* N);int* a10 =(int*)malloc(sizeof(int)* N);int* a11 =(int*)malloc(sizeof(int)* N);int* a12 =(int*)malloc(sizeof(int)* N);for(int i =0; i < N;++i){ a1[i]=rand(); a2[i]= a1[i]; a3[i]= a1[i]; a4[i]= a1[i]; a5[i]= a1[i]; a6[i]= a1[i]; a7[i]= a1[i]; a8[i]= a1[i]; a9[i]= a1[i]; a10[i]= a1[i]; a11[i]= a1[i]; a12[i]= a1[i];}int begin1 =clock();InsertSort(a1, N);int end1 =clock();int begin2 =clock();ShellSort(a2, N);int end2 =clock();int begin3 =clock();SelectSort(a3, N);int end3 =clock();int begin4 =clock();HeapSort(a4, N);int end4 =clock();int begin5 =clock();QuickSort1(a5,0, N -1);int end5 =clock();int begin8 =clock();QuickSort2(a8,0, N -1);int end8 =clock();int begin9 =clock();QuickSort3(a9,0, N -1);int end9 =clock();int begin10 =clock();QuickSortNonR(a9,0, N -1);int end10 =clock();int begin6 =clock();MergeSort(a6, N);int end6 =clock();int begin11 =clock();MergeSortNonR1(a11, N);int end11 =clock();int begin12 =clock();MergeSortNonR1(a12, N);int end12 =clock();int begin7 =clock();BubbleSort(a7, N);int end7 =clock();printf("InsertSort:%d\n", end1 - begin1);printf("ShellSort:%d\n", end2 - begin2);printf("SelectSort:%d\n", end3 - begin3);printf("HeapSort:%d\n", end4 - begin4);printf("BubbleSort:%d\n", end7 - begin7);printf("QuickSort1:%d\n", end5 - begin5);printf("QuickSort2:%d\n", end8 - begin8);printf("QuickSort3:%d\n", end9 - begin9);printf("QuickSortNonR:%d\n", end10 - begin10);printf("MergeSort:%d\n", end6 - begin6);printf("MergeSortNonR1:%d\n", end11 - begin11);printf("MergeSortNonR2:%d\n", end12 - begin12);free(a1);free(a2);free(a3);free(a4);free(a5);free(a6);free(a7);free(a8);free(a9);free(a10);free(a11);free(a12);}

⌨️测试结果:

在这里插入图片描述

希望读者们多多三连支持

小编会继续更新

你们的鼓励就是我前进的动力!

Read more

docker+playwright+python+Jenkins跑web自动化

# Docker部署Jenkins以及安装Playwright跑Web自动化的那些坑 在使用Docker部署Jenkins以及安装Playwright进行Web自动化测试的过程中,我遇到了不少问题。这里分享一下我的经验和解决方法,希望能帮助到同样在探索这条技术路线的朋友们。 ## 一、云主机上部署Docker的坑 ### 1.1 默认安装Docker的问题 在云主机上部署Docker时,我发现默认安装的Docker版本可能会带来各种问题。例如,我最初安装的Docker版本较旧,导致后续操作中出现了不少奇怪的错误。(tips:由于我是在云主机上部署了一个docker,在这一步的时候就遇到很多的坑,如下: 云主机版本:) 默认去 install 安装docker,安装的docker版本是: 然后遇到各种奇奇怪怪的问题,这里就不在赘述。安装最新的docker,假如你之前存在一些镜像容器,可以按照图下去备份: ### 1.2 升级Docker的建议 如果之前已经安装了Docker,可以按照以下步骤进行升级: - **备份现有镜像和容器**:在升级之前,建议备份现有的镜像和容器,以

By Ne0inhk
【C++STL :stack && queue (一) 】STL:stack与queue全解析|深入使用(附高频算法题详解)

【C++STL :stack && queue (一) 】STL:stack与queue全解析|深入使用(附高频算法题详解)

🔥艾莉丝努力练剑:个人主页 ❄专栏传送门:《C语言》、《数据结构与算法》、C/C++干货分享&学习过程记录、Linux操作系统编程详解、笔试/面试常见算法:从基础到进阶 ⭐️为天地立心,为生民立命,为往圣继绝学,为万世开太平 🎬艾莉丝的简介: 🎬艾莉丝的C++专栏简介: 目录 C++的两个参考文档 1  ~>  stack && queue的使用层 1.1  stack的使用 1.1.1  使用:表格整理 1.2  queue的使用 1.2.1  文档内容理解 1.2.2  使用表格整理 1.

By Ne0inhk
基于Python的量化交易实盘部署与风险管理指南

基于Python的量化交易实盘部署与风险管理指南

基于Python的量化交易实盘部署与风险管理指南 一、模拟交易与参数优化 1.1 券商API接入与模拟交易 在量化交易落地前,模拟交易是策略验证的“安全沙箱”,其核心价值在于用零成本环境暴露策略缺陷。以股票市场为例,同花顺与通达信模拟盘接口覆盖A股全品种行情与交易功能,但接口特性存在显著差异: * 同花顺采用HTTP轮询获取行情,适合低频策略测试,认证流程需通过MD5加密密码与时间戳生成签名,确保请求合法性; * 通达信提供WebSocket实时行情推送,延迟低至50ms,适合高频策略验证,需通过IP白名单+Token双重认证。 代码示例中,auth_ths函数演示了同花顺的签名算法,而WebSocket连接实现了实时行情的无阻塞接收,为策略实时计算提供数据源。 数字货币领域,Binance Testnet是最佳实践平台,其与主网完全一致的API接口支持现货、杠杆、永续合约全场景模拟。通过base_url参数切换至测试网,配合CCXT库统一多交易所接口,可实现策略的跨平台迁移测试。示例中市价单下单逻辑需注意:测试网的USDT通常为虚拟资产,需提前通过Faucet获

By Ne0inhk

【良好C++编程习惯】写出更安全、更高效、更优雅的 C++ 代码:10 个你必须掌握的现代编程技巧

写出更安全、更高效、更优雅的 C++ 代码:10 个你必须掌握的现代编程技巧 “写 C++ 不难,难的是写出正确、高效且可维护的 C++。” —— 每一位经历过段错误和内存泄漏的开发者 C++ 是一门“多范式”语言,功能强大却暗藏陷阱。幸运的是,随着 C++11/14/17/20 的演进,许多旧日痛点已被优雅解决。本文总结了我在多年开发中反复验证、真正提升生产力的 10 个核心编程技巧,助你写出更现代、更健壮的 C++ 代码。 技巧 1:永远优先使用智能指针,而非裸指针 问题:手动 new/delete 极易导致内存泄漏、重复释放或悬空指针。 解决方案: * std::unique_ptr&

By Ne0inhk