速通数据结构与算法第七站 排序

系列文章目录

速通数据结构与算法系列

1   速通数据结构与算法第一站 复杂度          http://t.csdnimg.cn/sxEGF

2   速通数据结构与算法第二站 顺序表          http://t.csdnimg.cn/WVyDb

3   速通数据结构与算法第三站 单链表          http://t.csdnimg.cn/cDpcC

4   速通数据结构与算法第四站 双链表          http://t.csdnimg.cn/0VyDl

5   速通数据结构与算法第五站 栈&&队列     http://t.csdnimg.cn/MRU83

6   速通数据结构与算法第六站 树&&堆        速通数据结构与算法第六站 树&&堆-CSDN博客

感谢佬们支持!


目录

系列文章目录

  • 前言
  • 一、插入排序
  •         复杂度分析
  • 二、希尔排序
  •         复杂度分析
  • 三、选择排序
  •         复杂度分析
  • 四、堆排序
  •         复杂度分析
  • 五、冒泡排序
  •         复杂度分析
  • 六、快排
  •       1 hoare法
  •       2 挖坑法
  •       3 前后指针法(recommend)
  •       4 非递归
  •       5 优化之三数取中(随机选key)
  •       6 优化之小区间优化
  •       7 优化之三路划分
  •       8 SGI sort设计
  •       9 复杂度分析
  • 七、归并排序
  •       1 递归版本
  •       2 非递归但梭哈实现法
  •       3 非递归但不梭哈实现法
  •       4 复杂度分析
  • 八、其他排序的介绍
  •       1 计数排序
  •               复杂度分析
  •       2 桶排序
  •                复杂度分析
  •       3 基数排序
  • 九、排序总结
  • 总结

前言

   这一节是速通数据结构的最后一节,我们要来学习排序。排序,看起来是个很简单的话题,实则一点也不简单。

举个例子,为了达到极致的效率,STL(SGI)算法库中的排序优化一大堆,首先要用快排,当数据个数小于16为了减少递归层数,改调插排,为了key的大小更适中搞了三数取中

一旦"划分恶化"改调堆排,防止时间复杂度恶化到O(n方)。所以排序还是很值得我们学习滴~

注:这篇文章收稿时达到了2w字!最用心的一集


一、插入排序

插入排序类似于我们整理扑克的过程

学排序首先要从单趟排序开始

假设现在有一个[2,4,7]的有序序列,我们要往其中插入一个数

1、假设我们插入的是1,2、4、7就都要往后移动

2、假设我们插入的是5,那需要挪的就是7

3、假设我们插入的是8,那就不需要挪,直接在最后插入即可

有了思路之后,我们就可以先来搞单趟排序了

单趟排序的逻辑是将tmp插入[0,end]的有序区间时

“我比你小,你挪;我比你大,插你后面”

void InsertSort(int*a,int tmp)
{int end;int tmp;while(end>=0){if(a[end]>tmp){a[end+1]=a[end];}   else{break;}//我比你大||我比所有人都小,即end=-1a[end+1]=tmp;}
}

有了单趟排序,就能推出整个排序了,显然,第一个数不用排,最后一个数的位置是i-1

所以每次的end为i-1,tmp为a [i]

最后得出的代码就是这样的。


void InsertSort(int *a,int n)
{assert(a);for (int i = 1; i < n; ++i){int end = i-1;int tmp=a[i];while (end >= 0){if (a[end] >tmp){a[end + 1] = a[end];--end;}else//a[end]<=x{break;}}a[end + 1] = tmp;}
}//SGI°æ²åÈëÅÅÐòint main()
{int arr[] = { 3,5.2,9,8,10,2 };int size = sizeof(arr) / sizeof(arr[0]);InsertSort(arr, size);
}

打印结果:


 复杂度分析

按最坏来看,如果是完全逆序的情况

时间复杂度就是1+2+……+n,最后就是O(n^2)

但是如果数据是完全有序/接近有序的情况,我们仅需一次遍历即可

也就是O(n)


空间复杂度是O(1)


二、希尔排序


希尔排序算是对插入的升级

希尔排序又称缩小增量法,希尔排序的基本增量是:先选定一个整数,将待排序中的所有记录分成个组,所有距离为选定整数的记录分在同一组内,并对每一组的记录做好排序

。然后将选定整数除以一个数,重复上述步骤,当这个整数到达1时,进行的最后一次排序

会得到一个有序的结果

看似逻辑很复杂,实际很简单,就是分组进行插入排序而已,利用了插入排序数据越有序

时间复杂度越小这一特性 

如何分组?

我们通过选取一个gap来操作,让间隔为gap的数为一组

例:

gap=3

[9,1,2,5,7,4,8,6,3,5]

我们将其分为红、绿、蓝三组

现对红色一组进行插入排序

蓝色一组

绿色一组


我们先来写红色组排队的逻辑

只需一次让i+=gap即可

            int gap=3;for (int i = 0; i < n - gap; i+=gap){int end=i;int tmp = a[end + gap];while (end >= 0){if (a[end] > tmp){a[end + gap] = a[end];end -= gap;}else//a[end]<=x{break;}}a[end + gap] = tmp;}

下来如果要排三组,就要再加一层for循环

由于只有3组,所以我们给一个gap次的for循环即可

            int gap=3;for(int j=0;j<gap;++j){for (int i = j; i < n - gap; i+=gap){int end=i;int tmp = a[end + gap];while (end >= 0){if (a[end] > tmp){a[end + gap] = a[end];end -= gap;}else//a[end]<=x{break;}}a[end + gap] = tmp;}}

或者用另一种方法,这样就可以在代码层面上减少一层循环(实际循环是没有减少的)

我们不让i一次+=gap了,而是一次加一个

        int gap=3;for (int i = 0; i < n - gap; i++){int end=i;int tmp = a[end + gap];while (end >= 0){if (a[end] > tmp){a[end + gap] = a[end];end -= gap;}else//a[end]<=x{break;}}a[end + gap] = tmp;}

这样就从之前的一组一组插排变成了多组同时开始插排


在此基础上,我们要变化gap的值,最终让gap变为1,这样的话,最终gap就会等于1,最后一次排序的时候就会变成插排,实现有序

#include<stdio.h>
#include<assert.h>
//ϣvoid ShellSort(int* a, int n)
{assert(a);int gap = n;while (gap > 1)//gap==1 {gap /= 2;for (int i = 0; i < n - gap; i++){int end = i;int tmp = a[end + gap];while (end >= 0){if (a[end] > tmp){a[end + gap] = a[end];end -= gap;}else//a[end]<=x{break;}}a[end + gap] = tmp;}}
}int main()
{int arr[] = { 3,5,2,9,8,10,2,2,9,8 };int size = sizeof(arr) / sizeof(arr[0]);ShellSort(arr, size);
}

 打印结果


复杂度分析

希尔排序按我们的常理来看,其时间复杂度取决于gap的选择

所以其时间复杂度很难计算,这里给到两本权威书上的解释

由于咱们的gap是按照knuth的方式来提出的,所以时间复杂度就O(n^1.25)~1.6*O(n^1.25)来算

所以这个排序是个很玄学的排序,就像跳表中选取每次插入数据是否提高层数的概率p一样


 空间复杂度依旧是O(1) 


三、选择排序

先介绍一下选择排序的基本思想

每一次从待排序的数据元素中选出最大/最小的一个元素,存放在序列的起始位置,直到待排序的所有元素全部排完

我们首先容易想到的就是直接选择排序,每次遍历我们可以同时选出最大/最小的数,将其放在序列的最左/最右

单趟就很容易想了,就是每次遍历选出最小/最大即可


int left = 0, right = n - 1;int maxi = left, mini = left;for (int i = left; i <= right; ++i)//ұ{if (a[i] > a[maxi]){maxi = i;}if (a[i] < a[mini]){mini = i;}}swap(&a[left], &a[mini]);swap(&a[right], &a[maxi]);

剩下只要每次维护left,right即可

但是还有一个坑,如果某一波,maxi正好就在left的位置

那更新最小值的时候就把最大值换走了

所以我们加个特判即可

#include<stdio.h.>void swap(int* a, int* b)
{int tmp = *a;*a = *b;*b = tmp;
}void SelectSort(int*a,int n)
{int left = 0, right = n - 1;while (left < right){int maxi = left, mini = left;for (int i = left; i <= right; ++i)//ұ{if (a[i] > a[maxi]){maxi = i;}if (a[i] < a[mini]){mini = i;}}swap(&a[left], &a[mini]);if (maxi == left){maxi = mini;}swap(&a[right], &a[maxi]);++left;--right;}
}
int main()
{int arr[] = { 3,5,2,9,8,10,2,2,9,8 };int size = sizeof(arr) / sizeof(arr[0]);SelectSort(arr, size);for (int i = 0; i < size; ++i){printf("%d ", arr[i]);}

打印结果


复杂度分析

选择排序由于每次都需要遍历找最大/最小值,所以时间复杂度最好最坏情况都是O(n^2),确实是最fvv的排序


空间复杂度是O(1)


四、堆排序

堆排序就是从直接选择排序变成了用堆选数。

关于堆排序的逻辑我们在上篇博客已经介绍过了,这里就不再赘述了

直接上代码

#include<stdio.h>//swap
void swap(int* px, int* py)
{int tmp = *px;*px = *py;*py = tmp;
}//向上调整
void AdjustUp(int* a, int child)
{int parent = (child - 1) / 2;while (child > 0)//最多调到根{if (a[child] > a[parent]){swap(&a[child], &a[parent]);child = parent;parent = (child - 1) / 2;}else{break;}}
}//向下调整
void AdjustDown(int* a, const int n, int parent)
{//找左右孩子大的一个交换int child = parent * 2 + 1;//suppose左孩子大,经典玩法while (child < n)//如果孩子超出了数组范围,说明parent是叶子节点{if (child + 1 < n && a[child + 1] > a[child])//防止越界{child++;}if (a[child] > a[parent]){swap(&a[parent], &a[child]);parent = child;child = parent * 2 + 1;}else{break;}}
}//排升序--建大堆(向上调整建堆)
void HeapSort1(int* a, int n)
{for (int i = 1; i < n; ++i)//O(lgN){AdjustUp(a, i);}int end = n - 1;while(end>0){swap(&a[0], &a[end]);AdjustDown(a, end, 0);end--;}
}//排升序--建大堆(向下调整建堆)
void HeapSort2(int* a, int n)
{for (int i = (n - 1 - 1) / 2; i >= 0; --i){AdjustDown(a, n, i);}int end = n - 1;while (end > 0){swap(&a[0], &a[end]);AdjustDown(a, end, 0);end--;}
}int main()
{int arr[] = { 2,4,5,6,7,2,6 };const int n = sizeof(arr) / sizeof(arr[0]);HeapSort2(arr, n);for (int i = 0; i < n; ++i){printf("%d ", arr[i]);}printf("\n");return 0;
}

打印结果


复杂度分析 

关于堆排序的时间复杂度我们也分析过了

如果是向下调整建堆,就是O(n*lgn)


由于是原地建堆,所以空间复杂度是O(1)


五、冒泡排序

冒泡排序属于交换排序

我们来介绍一下交换排序的思想

所谓交换,就是根据序列中的两个记录键值对的比较结果来swap这两个记录在序列的位置

交换排序的特点是:将键值较大的记录先后移动,将键值较小的记录先前移动

冒泡排序也是我们非常熟悉的排序了

这里直接上代码

#include<stdio.h>
#include<stdbool.h>void swap(int* a, int* b)
{int tmp = *a;*a = *b;*b = tmp;
}void BubbleSort(int* a, int n)
{for (int i = 0; i < n; ++i){bool flag = false;for (int j = i + 1; j < n; ++j){if (a[i] > a[j]){flag = true;swap(&a[i], &a[j]);}}if (flag == false){break;}}
}int main()
{int arr[] = { 3,5,2,9,8,10,2,2,9,8 };int size = sizeof(arr) / sizeof(arr[0]);BubbleSort(arr, size);for (int i = 0; i < size; ++i){printf("%d ", arr[i]);}
}

打印结果


 复杂度分析

显然,如果序列本身有序,遍历一次即可排好序

所以时间复杂度最好情况为O(n)

如果是最坏情况,则是O(n^2)

需要注意的是,在相对有序的情况下,冒泡排序的时间复杂度还是不如插入排序的


空间复杂度是O(1) 


六、快排

1 hoare法

单趟:选出一个基准值key,把他放到正确的位置(最终排好序要蹲的事)

例:

最终就会变成

我们有三种方法,第一种是这样的:

法一:

1、具体操作为左边找严格比key大的,右边找严格比key小的,然后swap

2、最后在最后停下的位置和key所在的位置交换一下

一波单趟排序就做好啦~

//hoare法
int partition1(int* a, int left, int right)
{int mid = GetMidNumi(a, left, right);swap(&a[left], &a[mid]);int keyi = left;while (left < right){//左边是key,右边先走//带上=,表示找严格大/严格小while (left < right && a[right] >= a[keyi]){right--;}while (left < right && a[left] <= a[keyi]){left++;}swap(&a[left], &a[right]);}//最后left和key换一下swap(&a[left], &a[keyi]);return keyi;
}

注:由于是循环套循环,所以做好边界判断

为什么要找严格大于/小于的?

如果某个用例为[2,2,2,2,2,2];那就完了,会一直死循环


2 挖坑法

由于hoare大佬的这个方法太拉拉了,又给出了第二种(感觉更复杂了,意思差不多)

法二:

1、先将key值存在一个变量里,就会形成一个坑位

2、依然是左边找大,右边找小

右边找小,放到坑里,更新坑的位置

左边再找大,放进坑里,更新坑的位置

3、当left和right相遇,将key填入坑中

按图来看,就是这样

//挖坑法
int partition2(int* a, int left, int right)
{int mid = GetMidNumi(a, left, right);swap(&a[left], &a[mid]);int key = a[left];int hole = left;//此时 left为坑while (left < right){//左边是key,右边先走//带上=,表示找严格大/严格小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;}//最后坑的位置放kea[left] = key;return hole;
}

3 前后指针法(recommend)

这个方法还是很清爽很简洁的

法三

1、初始时,prev指向序列开头,cur指针指向prev指针后的一个位置,就像这样

2、cur所在位置比key小的,

如果找到了,就++prev,和cur交换

交换完以后,++cur

3 如果cur所在位置比key大

就++cur

在排序过程中,大概就是两种情况

1 prev紧跟着cur

2 prev和cur隔着比key大的一段数的区间 

按图来看,就是这样

//前后指针法
int partition3(int* a, int left, int right)
{int mid = GetMidNumi(a, left, right);swap(&a[left], &a[mid]);int keyi = left;int cur = left+1;int prev = left;while (cur <= right){if (a[cur] < a[keyi] && (++prev) < cur)//不要自己和自己换~swap(&a[cur], &a[prev]);++cur;}swap(&a[keyi], &a[prev]);return prev;
}

那么问题来了,我们已经学习了3种单趟排序的方法?

如何实现完整的排序呢?

我们知道,第一波单趟排序排好的是最终key的位置

所以下来的操作非常简单

1 我们对key的左右区间递归使用该函数即可

2 当区间只有一个数/区间不存在时,递归调用结束

有了上一节我们学到的递归的经验,这波函数体就会是这样(以第三种方法为例)

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);
}

需要注意的是,三种单趟排序对同一序列的结果可能不同,如果有数据结构题目问

单趟排序的结果,我们需要考虑3种方式 


  4 非递归

非递归的思路也很简单,只要将递归的逻辑用栈代替即可

1、每次将left、right入栈;

2、每次取两次栈顶组成begin、end调用单趟排序

3、将单趟排序返回的keyi左右两端区间的左右顶点入栈

4、当栈为空时,循环结束

//前后指针法
int partition3(int* a, int left, int right)
{int keyi = left;int cur = left + 1;int prev = left;while (cur <= right){if (a[cur] < a[keyi] && (++prev) < cur)//不要自己和自己换~swap(&a[cur], &a[prev]);++cur;}swap(&a[keyi], &a[prev]);return prev;
}void QuickSortNonR(int* a, int left, int right)
{stack<int> st;st.push(left);st.push(right);while (!st.empty()){int end = st.top();st.pop();int begin = st.top();st.pop();int keyi = partition3(a, begin, end);//begin,keyi-1 ;  keyi+1,endif (begin < keyi - 1){st.push(begin);st.push(keyi-1);}if ( keyi + 1<end){st.push(keyi+1);st.push(end);}}}int main()
{int arr[] = { 3,5,2,9,8,10,2,2,9,8 };int size = sizeof(arr) / sizeof(arr[0]);QuickSortNonR(arr, 0, size - 1);for (int i = 0; i < size; ++i){printf("%d ", arr[i]);}
}

打印结果


5 优化之三数取中(随机选key)

三数取中其实很简单,由于我们固定选最左边的数为key会有不确定性,所以我们选取

左中右三个数中中间的那个为key

//三数取中
int GetMidNumi(int* a, int left, int right)
{int mid = (right + left) / 2;if (a[left] < a[mid]){if (a[mid] < a[right]){return mid;}//说明mid是最大的else if (a[right] > a[left]){return right;}else{return left;}}else{if (a[mid] > a[right]){return mid;}//说明mid是最小的else if (a[right] > a[left]){return left;}else{return right;}}
}

6 优化之小区间优化

我们注意到,当递归到一定深度时,每次的区间长度不长

但仍需要递归,这就会导致递归次数太多,有栈溢出的风险

所以我们限制一个长度,在此长度以下,我们直接调插入排序

由于是接近有序,所以效率不会太拉还可以减少递归层数

void QuickSort(int* a, int left, int right)
{if (left >= right)return;// 小区间优化--小区间直接使用插入排序if ((right - left + 1) > 10){int keyi = PartSort3(a, left, right);QuickSort(a, left, keyi - 1);QuickSort(a, keyi + 1, right);}else{InsertSort(a+left, right - left + 1);}
}

7 优化之三路划分

当你有了以上优化之后,你会发现,你依然无法通过力扣的那道排序题

. - 力扣(LeetCode)

因为有个用例是大量重复的数

这对我们的快排是极为不利的

大量的重复意味着大量的递归+遍历,而每次单趟排序都不能做事

所以有人提出了所谓三路划分


之前我们通过选取key找到大于key/小于key的区间,算的上是一种双路划分

所以三路划分是这样的

单独划分出一个等于key的区间

核心思想是这样的

1、 跟key相等的值,往后推

2、比key小的在左边,比key大的在右边,和key相等的在中间

所以我们的双指针法就变成了三指针法了,

1、a[cur]<key,交换left和cur,left++,cur++

2、a[cur]>key,交换right和cur,right--,cur++

3、a[cur]==key,cur++(只动cur!)

搞定了单趟其他就简单了

我们和之前一样递归即可

简单实现一波

void QuickSort(int* a, int begin, int end)
{if (begin >= end)return;if((end-begin+1)<15){InsertSort(a+begin, end - begin + 1);}// 小区间优化--小区间直接使用插入排序else{int mid=GetMidNumi(a,begin,end);swap(a[begin],a[mid]);int left=begin;int right=end;int key=a[begin];int cur=begin+1;while(cur<=right){if(a[cur]<key){left++;cur++;}else if(a[cur]>key){swap(a[cur],a[right]);--right;}else{cur++;}}QuickSort(a,begin,left-1);QuickSort(a,right+1,end);}
}

有了这个优化,我们就可以通过力扣的排序题啦 


8 SGI sort设计

(参考侯捷老师的《STL源码剖析》)

学习了上述优化,我们就可以来评鉴一下C++的算法库中的sort了

总述:STL的sort算法,数据量大时采用QuickSort,分段递归排序,一旦分段后的数据量小于某个门槛,为避免QuickSort的递归调用带来过大的额外负荷,就改用InsertSort

如果递归层数过深,还会改用HeapSort

InsertSort

SGI的InsertSort有两个版本,一种是递增,另一种是仿函数

版本二的可以先忽略,我们重点来看版本一的

这个是插入排序的外循环

template<class RandomAccessIterator>
void __insertion_sort(RandomAccessIterator first,RandomAccessIterator last)
{if(first==last)return ;for(RandomAccessIterator i=first+1;i!=last;++i)//外循环{__liner_insert(first,i,value_type(first));}
}

__linear_insert是这么做的

template<class RandomAccessIterator,class T>
inline void __liner_insert(RandomAccessIterator first,RandomAccessIterator last,T*)
{T value =*last;//记录尾元素//和我们写的不同的是,这里分了两种情况,是否尾比头还小(头为最小元素)if(value<*first){copy_backward(first,last,last+1);//整体后移*first=value;}else{__unguarded_linear_insert(last,value); }
}

这里的__unguarded的命名很讲究,表明这波不用判断是否超过边界(也就是我们所写代码中end>=0的逻辑),

因为我们的源码确保了最小值必然在内循环区间的最边缘

void __unguarded_linear_insert( RandomAccessIterator last,T value)
{RandomAccessIterator next=last;while(value<*next)//内循环的逻辑{*last=*next;last=next;--next;}*last=value;
}

细节满满,看似只是省下了一个简单的判断,但是在大数据量下,影响还是很可观的


QuickSort

SGI的QuickSort提供了两种单趟排序(partioning)分别是我们的单趟1和单趟3

别的都差不多,没什么好说的


threshold

SGI sort还认为,对于一个小数据量序列,甚至简单的插入排序可能更快

鉴于这种情况,适度评估序列的大小,再决定选择插排还是快排,是值得采纳的优化措施

侯捷老师认为并无定论,5~20都有可能,因设备而定


final Insert Sort

SGI也采用了当序列小于一个值就调用插入排序的做法

(在源码中,这个值是16)

const int __stl_threshold =16;

原文:如果我们令某个大小以下的序列滞留在"几近排序但尚未成功"的状态,最后再以

一次InsertSort将所有这些"几近排序但尚未成功"的子序列做一次完整的排序,其效率一般认为会比"将所有子序列彻底排好"更好,这是因为InsertSort在面对"几近排序"的序列时,有更好的表现


introsort

不当的轴承(就是key值)选择,可能导致QuickSort退化为O(n^2),所以David大佬提出了一种混合式排序,内省式排序,简称introsort。在分割行为有二次行为的倾向时,能自动检测,转而使用HeapSort,保住O(n*lgn)的下限

自我检测的函数为__lg,其实就是在找2^k<=n的最大值k

template<class Size>
inline Size __lg(Size n)
{Size k;for(k=0;n>1;n>>=1)++k;return k;
}

所以,最终的sort本体是这样的~

template<class RandomAccessIterator>
inline void sort(RandomAccessIterator first,RandomAccessIterator last)
{__introsort_loop(first,last,value_type(first),__lg(last-first)*2);__final_insertion_sort(first,last);
}
template<class RandomAccessIterator,class T,class Size>
void __introsort_loop(RandomAccessIterator first, RandomAccessIterator last,T* ,Size depth_limit)
{while(last-first>__stl_threshold){if(depth_limit==0){//分裂恶化//调用堆排!partial_sort(first,last,last);return ;}--depth_limit;//下来就是单趟排序+三数取中的逻辑了,这里简化一下,就不写了RandomAccessIterator cut;//递归左右区间调用__introsort_loop,这里也简化一下__introsort_loop();__introsort_loop()}

当__introsort_loop()结束后,[first,last)会存在多个元素大于16的元素

此时回到主函数sort,再进行__final_insertion_sort(first,last);

template<class RandomAccessIterator>
void __final_insertion_sort(RandomAccessIterator first,RandomAccessIterator last)
{if(last-first>__stl_threshold){__insertion_sort(first,first+__stl_threshold);__unguarded_insertion_sort(first+__stl_threshold,last);//就是循环调用__unguarded_linear_insert; }else{__insertion_sort(first,last);}
}

该函数首先判断元素个数是否大于16,如果为否,直接调用__insertion_sort

如果为是,就分割为一段长为16的区间和剩下的区间,分别调用__insertion_sort和——__unguarded__insertion_sort处理


这就是SGI STL sort的故事了,设计非常巧妙(最用心的一集)

9 复杂度分析 

根据我们在SGI sort分析的那样,如果是普通快排,时间复杂度一般是O(n*lgn)

但是在极端情况下,会退化至O(n^2)。

但是SGI sort,我们可以保证一个O(n*lgn)的下限


空间复杂度的来源是栈帧的建立,为O(lgn)~O(n)


七、归并排序

归并排序是建立在归并操作上的一种有效的排序算法,采用了经典的分治法。

将已有序的子序列合并,得到完全有序的序列

归并的逻辑很简单,就是我们之前的合并两个有序数组的逻辑最后再拷回原数组即可 

1 递归版本
void _merge(int* a, int left, int right,int *tmp)
{//一个值时/区间不存在时结束if (left >= right){return;}int mid = (left+right) / 2;//子区间递归排序_merge(a, left, mid, tmp);_merge(a, mid + 1, right, tmp);//归并int begin1 = left;int end1 = mid;int begin2 = mid + 1;int end2 = right;printf("begin1:%d end1:%d\n", begin1, end1);printf("begin2:%d end2:%d\n", begin2, end2);int i = left;while (begin1 <= end1 && begin2 <= end2){if (a[begin1] < a[begin2]){tmp[i++] = a[begin1++];}else{tmp[i++] = a[begin2++];}}if (begin1 > end1){while (begin2 <= end2){tmp[i++] = a[begin2++];}}if (begin2 > end2){while (begin1 <= end1){tmp[i++] = a[begin1++];}}//记住加left!memcpy(a + left, tmp + left, sizeof(int) * (right - left + 1));
}void MergeSort(int* a, int n)
{int* nums = (int*)malloc(sizeof(int) * n);_merge(a,0, n-1,nums);free(nums);
}int main()
{int arr[] = { 3,5,2,9,8,10,2,2};int size = sizeof(arr) / sizeof(arr[0]);MergeSort(arr, size);for (int i = 0; i < size; ++i){printf("%d ", arr[i]);}
}

 2 非递归但梭哈实现法

非递归如何模拟模拟归并的过程呢?

归并的逻辑是先一一归并,再二二归并,在此基础上四四归并,他的逻辑更像一个后序遍历

而栈则对应的是前序遍历,所以这波不能用栈,而可以控制一个gap,实现一一、二二、四四……归并

gap==1,此时为一一归并

gap*=2,就变成了二二归并

……


 光这样还不够,我们只考虑了元素个数是二的次方的情况

如果是9,20个数据,end2,begin2,begin1会有越界的风险

所以我们要修正边界


光修正边界还不够,我们要考虑拷回原数组的问题,这里分为一把拷(梭哈)和归一部分拷一部分(不梭哈)两种情况

我们可以打印一波边界来看看情况

我们试试9个元素

//一把梭哈
void MergeSortNonRS(int* a, int n)
{int* nums = (int*)malloc(sizeof(int) * n);int gap = 1;while (gap < n){for (int i = 0; i < n; i += 2 * gap){//归并int begin1 = i;int end1 = i+gap-1;int begin2 = i+gap;int end2 = i+2*gap-1;printf("begin1:%d end1:%d\n", begin1, end1);printf("begin2:%d end2:%d\n", begin2, end2);int j = i;while (begin1 <= end1 && begin2 <= end2){if (a[begin1] < a[begin2]){nums[j++] = a[begin1++];}else{nums[j++] = a[begin2++];}}if (begin1 > end1){while (begin2 <= end2){nums[j++] = a[begin2++];}}if (begin2 > end2){while (begin1 <= end1){nums[j++] = a[begin1++];}}}//记住加left!memcpy(a , nums, sizeof(int) *n);gap *= 2;}free(nums);
}int main()
{int a[] = { 6,1,2,6,9,3,4,6,10};MergeSortNonRS(a, 9);
}

由于begin1为i,所以不可能越界

下来,越界有这么几种情况


1、end1越了,不归并了,但是是要拷贝的(因为只剩一个数了)

【8,11】【12,15】

由于我们要一次梭哈拷贝,所以这波我们要修正边界,才能不被覆盖


2、end1没越界,begin2越界了

同理,不用归并,也要修正边界

【8,8】【9,9】


3、只有end2越界了

他是需要归并的,修正end2

【0,7】【8,15】

不归并的修正边界很简单,我们只需修成一个不存在的区间,就不进循环了

而最后需要归并的end2,我们需要计算一下修正到的值,即n-1

//一把梭哈
void MergeSortNonRS(int* a, int n)
{int* nums = (int*)malloc(sizeof(int) * n);int gap = 1;while (gap < n){for (int i = 0; i < n; i += 2 * gap){//归并int begin1 = i;int end1 = i+gap-1;int begin2 = i+gap;int end2 = i+2*gap-1;printf("begin1:%d end1:%d\n", begin1, end1);printf("begin2:%d end2:%d\n", begin2, end2);//修正if (end1 >= n){end1 = n - 1;begin2 = n;end2 = n - 1;}else if (begin2 >= n){///修成一个不存在的区间begin2 = n;end2 = n - 1;}else if (end2 >= n){end2 = n - 1;}int j = i;while (begin1 <= end1 && begin2 <= end2){if (a[begin1] < a[begin2]){nums[j++] = a[begin1++];}else{nums[j++] = a[begin2++];}}if (begin1 > end1){while (begin2 <= end2){nums[j++] = a[begin2++];}}if (begin2 > end2){while (begin1 <= end1){nums[j++] = a[begin1++];}}}//记住加left!memcpy(a , nums, sizeof(int) *n);gap *= 2;}free(nums);
}

3 非递归但不梭哈实现法

好消息是,不拷贝,前两个就可以不用修正边界了,直接break出去

只需修正第二个区间的右边界即可

//归一部分拷一部分
void MergeSortNonR(int* a, int n)
{int* nums = (int*)malloc(sizeof(int) * n);int gap = 1;while (gap < n){for (int i = 0; i < n; i += 2 * gap){//归并int begin1 = i;int end1 = i + gap - 1;int begin2 = i + gap;int end2 = i + 2 * gap - 1;printf("begin1:%d end1:%d\n", begin1, end1);printf("begin2:%d end2:%d\n", begin2, end2);//修正if (end1 >= n || begin2 >= n){break;}else if (end2 >= n){end2 = n - 1;}int j = i;while (begin1 <= end1 && begin2 <= end2){if (a[begin1] < a[begin2]){nums[j++] = a[begin1++];}else{nums[j++] = a[begin2++];}}if (begin1 > end1){while (begin2 <= end2){nums[j++] = a[begin2++];}}if (begin2 > end2){while (begin1 <= end1){nums[j++] = a[begin1++];}}memcpy(a +i , nums+i, sizeof(int) * (end2 - i + 1));}gap *= 2;}
free(nums);
}

复杂度分析

归并排序的时间复杂度也是标准的O(n*lgn)


空间复杂度来源于开辟的新数组,为O(n)


归并排序还可以用作外排序,大家感兴趣的话,可以再了解一下~


八、其他排序的介绍

计数排序

计数排序又称鸽巢原理,是对哈希直接定址法的应用

操作为:

1、统计相同元素个数

2、根据统计结果将序列回收回原来的序列

既然是模仿哈希,那么原理是这样的

思考一下元素的大致范围,开一个大的哈希表(数组),通过定址法将带牌序列的元素映射至

哈希表,最后再从哈希表提取元素即可

例:

{6,1,2,1,9,3,3,2,2,8}

我们直接开一个大小为10的数组就够了

然后遍历数组,直接定址,再对应下标值加1即可

遍历之后是这样的

下来就简单,遍历哈希表排序即可


看似很简单的计数排序,我们要考虑一些别的问题

如果序列是这样

{100,101,101,103,109,120},我们再从0开始定址就有些浪费了

所以我们可以统计最大,最小值,就能最大程度的节省空间了

需要注意的是,如果序列含有负数,我们的排序也可以解决

那代码是这样的~


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);
}int main()
{int arr[] = { 2,10,3,90,589,184,505.29,8,83 };for (auto e : arr){std::cout << e << " ";}std::cout << std ::endl;int sz = sizeof(arr) / sizeof(arr[0]);CountSort(arr, 0, sz);for (auto e : arr){std::cout << e << " ";}return 0;
}

 复杂度分析

显然,如果序列内的值相差不太大,计数排序的时间复杂度能逼急O(n),非常逆天

其实是O(n+range)

但是由于我们用到了哈希表,那就意味着你得能哈希出一个值来才能排序

所以浮点数肯定是寄了,但是字符串可以(字符串哈希)。 


空间复杂度是O(range)


桶排序

桶排序(Bucket Sort)是一种基于分布的排序算法,特别适用于数据分布比较均匀的情况。它的基本思想是将数据分成若干个桶(Bucket),然后分别对每个桶中的数据进行排序,最后再将各个桶中的数据按顺序合并,得到最终的有序数据。

桶排序的基本步骤:

  1. 创建桶
    • 根据数据范围创建一定数量的桶,每个桶代表一个区间范围。
  2. 将元素分配到桶中
    • 遍历输入数据,将每个数据根据其值分配到对应的桶中。
  3. 对每个桶内的数据排序
    • 对每个桶内的数据单独进行排序(可以使用任何排序算法,通常使用快速排序或插入排序)。
  4. 合并桶中的数据
    • 最后按顺序将各个桶中的数据合并成一个整体,即完成排序

复杂度分析

时间复杂度:

  1. 最好情况: O(n+k)O(n + k)O(n+k)

    • 如果输入数据均匀分布到桶中,且每个桶内的数据可以使用一个高效的排序算法(如插入排序)来进行排序,桶排序的时间复杂度接近线性,等于 O(n)O(n)O(n)。
    • 此外,每个桶的排序时间是与桶内元素数量有关的。如果使用插入排序,最好情况下每个桶的排序复杂度是 O(1)O(1)O(1),总的时间复杂度为 O(n+k)O(n + k)O(n+k),其中 nnn 是输入数据的数量,kkk 是桶的数量。
  2. 平均情况: O(n+k)O(n + k)O(n+k)

    • 当数据大致均匀分布到桶中时,桶排序的平均时间复杂度与最好情况类似,也是 O(n+k)O(n + k)O(n+k),因为分配数据到桶中的过程是线性的。
  3. 最坏情况: O(n2)O(n^2)O(n2)

    • 最坏情况下,所有数据都被分配到同一个桶中,此时桶排序退化为在单个桶中对 nnn 个元素进行排序。若该桶内使用插入排序或其他 O(n2)O(n^2)O(n2) 时间复杂度的排序算法,则总的时间复杂度为 O(n2)O(n^2)O(n2)。

空间复杂度:

  • 空间复杂度: O(n+k)

这种排序一般没什么用,大家了解一下即可


基数排序​​​​​​​

首先我们要知道,基数排序也是一个和之前不同,即不需要比较、移动的排序

是一种借助一种多关键字的排序对单关键字进行排序的方法

例:

我们的扑克牌就有两种关键字进行排序

一种是花色,♠️,♣️,♦️,♥️

另一种是数字大小,1,2,3,4,5,6,7,8,9,10,J,Q,K

再介绍两个概念

最高位优先(MSD)&最低位优先(LSD)

以扑克牌为例 

MSD为:每个子序列花色相同但数字不同

LSD为:4个1,4个2,4个3……


给一个例子

我们以低位优先为例

由于是低位优先,所以我们先按个位排,

将其遍历,重组回原数组(注意,这波063先进,所以先出063)


下来再排十位


排好百位,这波就结束啦


原理还是很简单的,建一个挂队列的哈希表

然后进行数位次操作

每次操作先分发,再组合

写好的代码就是这样的~ 

#define K 3
#define RADIX 10std::queue<int> Q[RADIX];int GetKey(int value,int k)
{int key = 0;while (k >= 0){key = value % 10;value /= 10;k--;}return key;
}void Distribute(int*arr,int left,int right,int k)
{for (int i = left; i < right; ++i){int key = GetKey(arr[i],k);Q[key].push(arr[i]);}
}void Collect(int*arr)
{int k = 0;for (int i = 0; i < RADIX; ++i){while (!Q[i].empty()){arr[k++] = Q[i].front();Q[i].pop();}}
}void RadixSort(int* arr, int left, int right)
{for (int i = 0; i < K; ++i){//分发Distribute(arr,left,right,i);//组合Collect(arr);}
}int main()
{int arr[] = { 278,10,63,930,589,184,505.269,8,83 };for (auto e : arr){std::cout << e << " ";}std::cout << std ::endl;int sz = sizeof(arr) / sizeof(arr[0]);RadixSort(arr, 0, sz);for (auto e : arr){std::cout << e << " ";}return 0;
}

复杂度分析

假设我们搞了r个队列

那我们的空间复杂度就是O(r)


假设我们要进行k次操作

每次分发和组合的时间复杂度就是(n+r)

所以总体就是O(k(n+r))


九、排序总结

这里我们再介绍一个所谓的稳定性的概念

假定在待排序的记录序列中,存在多个具有相同关键字的记录,若经过排序,这些记录的

相对次序保持不变。

即在原序列中,r[i]=r[j],且r[i]在r[j]之前,在排序之后,如果r[i]仍在r[j]之前,就说明这种排序是稳定的,否则就是不稳定的

说人话就是,假设某个序列有相同的数字,经过排序之后,这些相同的数的顺序不变,就是稳定的

有什么用呢?

假定考试时规定分数相同先交卷的人更牛逼,我们就用得着这个稳定性了

根据这些排序的原理,我们便可推出一个排序是否稳定了

排序名是否稳定?解释
插入排序稳定

显然,如果规定了相等了不往后

挪即可

希尔排序不稳定相同的数可能被分到不同的组
选择排序不稳定每次选到的数不固定
堆排序不稳定每次作为堆顶的元素不确定,而堆顶之下可能会有重复元素
冒泡排序稳定相同不交换就可以做到稳定
快速排序不稳定每次的key值不确定
归并排序稳定
计数排序稳定
基数排序稳定辅助数组元素是队列,可以通过先进先出保证

总结

 做总结,这篇博客我们学习了排序,这将是速通数据结构算法初阶的最后一集,接下来我们将迅速的投入沉淀C++的篇章中

水平有限,还请各位大佬指正。如果觉得对你有帮助的话,还请三连关注一波。希望大家都能拿到心仪的offer哦。

每日gitee侠:今天你交gitee了嘛

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

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

相关文章

1. 如何在服务器上租GPU跑实验 (以AutoDL为例) - 深度学习·科研实践·从0到1

目录 前言 1. 在AutoDL上注册账号 2. 在算力市场选择GPU 3. 创建实例 4. 控制台-容器实例界面&#xff08;核心&#xff09; 4.1 无卡模式&#xff08;常用&#xff09; 5. 帮助文档 前言 好记性不如烂笔头&#xff0c;本专栏将详细记录下本人学习深度学习工程实践&…

【Python】YOLO牛刀小试:快速实现视频物体检测

YOLO牛刀小试&#xff1a;快速实现视频物体检测 在深度学习的众多应用中&#xff0c;物体检测是一个热门且重要的领域。YOLO&#xff08;You Only Look Once&#xff09;系列模型以其快速和高效的特点&#xff0c;成为了物体检测的首选之一。本文将介绍如何使用YOLOv8模型进行…

【Git原理与使用】Git初识基本操作

Git初识&&基本操作 1.初识Git2.Git安装3.Git基本操作3.1创建Git本地仓库3.2配置Git3.3认识工作区、暂存区、版本库3.4添加文件3.5修改文件3.6版本回退3.7撤销修改3.8删除文件 点赞&#x1f44d;&#x1f44d;收藏&#x1f31f;&#x1f31f;关注&#x1f496;&#x1f…

基于单片机8路数字电压表电压采集系统

**单片机设计介绍&#xff0c;基于单片机8路数字电压表电压采集系统 文章目录 前言概要功能设计设计思路 软件设计效果图 程序设计程序文章目录 前言 &#x1f497;博主介绍&#xff1a;✌全网粉丝10W,CSDN特邀作者、博客专家、CSDN新星计划导师&#xff0c;一名热衷于单片机技…

数据集-目标检测系列-鼠检测数据集 mouse >> DataBall

数据集-目标检测系列-鼠检测数据集 mouse >> DataBall 数据集-目标检测系列-老鼠检测数据集 mouse 数据量&#xff1a;6k 想要进一步了解&#xff0c;请联系。 DataBall 助力快速掌握数据集的信息和使用方式&#xff0c;会员享有 百种数据集&#xff0c;持续增加中。…

Redis 基础数据改造

优质博文&#xff1a;IT-BLOG-CN 一、服务背景 基础数据查询服务&#xff1a;提供航司、机场、票台、城市等基础数据信息。 痛点一&#xff1a;因为基础数据不属于频繁更新的数据&#xff0c;所以每个应用都有自己和缓存&#xff0c;当基础数据更新后&#xff0c;各个应用缓存…

用友U8+CRM leadconversion.php SQL注入复现

0x01 产品描述&#xff1a; 用友U8-CRM是企业利用信息技术&#xff0c;是一项商业策略&#xff0c;它通过依据市场细分组织企业资源、培养以客户为中心的经营行为、执行以客户为中心的业务流程等手段来优化企业的客户满意度和获利能力。 0x02 漏洞描述&#xff1a; 用友 U8 CR…

指定PDF或图片多个识别区域,识别区域文字,并导出到Excel文件中

常见场景 用户有大量图片/PDF文件&#xff0c;期望能将图片/PDF中的多个区域中的文字批量识别出来&#xff0c;并导入到Excel文件中。期望工具可以批量处理、离线识别&#xff08;保证数据安全性&#xff09;。手工操作麻烦。具体场景&#xff1a;用户有工程现场照片&#xff…

【学习笔记】基于 Wasserstein距离的分布鲁棒优化

衡量不同分布间距离 在构建模糊集的方式上&#xff0c;除了利用矩信息之外&#xff0c;另一种思路是衡量真实分布与经验分布之间的距离。在这种情况下&#xff0c;我们以经验分布为中心&#xff0c;将与经验分布不超过某一距离的所有分布纳入模糊集中。于是&#xff0c;如何定…

onlyoffice连接器(connector)开发使用精讲 二次开发 深入开发【二】

前言 这篇教程开始&#xff0c;全部为进阶版使用&#xff0c;你需要先熟悉使用最基础的连接器教程&#xff0c;如果你没有正常接入&#xff0c;请参考教程【一】&#xff1a;onlyoffice连接器(connector)开发使用精讲 二次开发 深入开发【一】_onlyoffice 连接器-CSDN博客 该教…

Jira Cloud涨价5%-20%,钉钉项目Teambition成优选替代

近日&#xff0c;Jira再次宣布涨价&#xff0c;Cloud版涨幅达到5%-20%&#xff0c;这一消息来源于Atlassian官方面向合作伙伴发布的2024年最新涨价通知。 Atlassian旗下核心产品&#xff0c;包括Jira、Confluence、JiraServiceManagement等的Cloud版本价格将有所提高&#xff…

一站式大语言模型API调用:快速上手教程

智匠MindCraft是一个强大的AI工具及开发平台&#xff0c;支持多种大语言模型和多模态AI模型。本文将详细介绍如何通过API调用智匠MindCraft中的大语言模型&#xff0c;帮助开发者快速上手。 注册与登录 访问智匠MindCraft官网&#xff0c;注册并登录账号。 进入开发者平台&…

如何做好接口测试?||关于京东平台商品API接口测试

探讨主题&#xff1a;如何做好接口测试&#xff1f; 一、接口测试简介 1、什么是接口测试&#xff1f; 接口测试是测试系统组件间接口的一种测试。接口测试主要用于检测外部系统与系统之间以及内部各个子系统之间的交互点。测试的重点是要检查数据的交换&#xff0c;传递和控制…

损失函数篇 | YOLOv10 更换损失函数之 SIoU / EIoU / WIoU / Focal_xIoU 最全汇总版

文章目录 更换方式CIoUDIoUEIoUGIoUSIoUWIoUFocal_CIoUFocal_DIoUFocal_EIoUFocal_GIoUFocal_SIoU提示更换方式 第一步:将ultralytics/ultralytics/utils/metrics.py文件中的bbox_iou替换为如下的代码:class WIoU_Scale: if monotonous = None , v1if monotonous = True , v…

领夹麦克风性价比最高?一文看懂领夹麦克风什么牌子的好

近几年随着网络直播、短视频等新兴行业的发展&#xff0c;筑就了一个全民视频创作的时代。而领夹麦克风也是凭借轻便、便携的特性&#xff0c;获得了广大短视频创作者的青睐&#xff0c;领夹麦克风的需求量也是不断增加。也正是因为如此&#xff0c;如今市面上的领夹麦克风品牌…

【小程序】微信小程序课程 -4 项目实战

目录 1、 效果图 2、创建项目 2.1 创建小程序端 2.1.1 先创建纯净项目 2.1.2 删除components 2.1.4 删除app.json红色部分 2.1.5 删除index.json红色部分 2.1.6 删除index.wxss全部内容 2.1.7 删除index.wxml全部内容 2.1.8 app.json创建4个页面 2.1.9 app.json添加…

算法闭关修炼百题计划(一)

多看优秀的代码一定没有错&#xff0c;此篇博客属于个人学习记录 1.两数之和2.前k个高频元素3.只出现一次的数字4.数组的度5.最佳观光组合6.整数反转7.缺失的第一个正数8.字符串中最多数目的子序列9.k个一组翻转链表10.反转链表II11. 公司命名12.合并区间13.快速排序14.数字中的…

项目学习笔记

Downloads – Oracle VirtualBoxhttps://www.virtualbox.org/wiki/Downloads

Nginx基础详解2(首页解析过程、进程模型、处理Web请求机制、nginx.conf语法结构)

续&#xff1a;Nginx基础详解1&#xff08;单体部署与集群部署、负载均衡、正反代理、nginx安装&#xff09;-CSDN博客 目录 4.Nginx默认首页的过程解析 5.Nginx进程模型的详解 5.1启动nginx后的关于nginx的进程查看 5.2master进程与process进程 5.3Nginx进程图解 5.4wo…

STM32 OLED

文章目录 前言一、OLED是什么&#xff1f;二、使用步骤1.复制 OLED.C .H文件1.1 遇到问题 2.统一风格3.主函数引用头文件3.1 oled.h 提供了什么函数 4.介绍显示一个字符的函数5. 显示十进制函数的讲解 三、使用注意事项3.1 配置符合自己的引脚3.2 花屏总结 前言 提示&#xff…