文章目录
图表总结
排序方式 | 平均情况 | 最好情况 | 最坏情况 | 辅助空间(空间复杂度) | 稳定性 |
---|---|---|---|---|---|
冒泡排序 | O(N^2) | O(N^2) | O(N^2) | O(1) | 稳定 |
选择排序 | O(N^2) | O(N^2) | O(N^2) | O(1) | 不稳定 |
插入排序 | O(N^2) | O(N) | O(N^2) | O(1) | 稳定 |
希尔排序 | O(N*logN)~O(N^2) | O(N^1.3) | O(N^2) | O(1) | 不稳定 |
快速排序 | O(N*logN) | O(N*logN) | O(N^2) | O(logN~N) | 不稳定 |
堆排序 | O(N*logN) | O(N*logN) | O(N*logN) | O(1) | 不稳定 |
归并排序 | O(N*logN) | O(N*logN) | O(N*logN) | O(N) | 稳定 |
计数排序 | O(max(range,N) | O(max(range,N) | O(max(range,N) | O(range) | 稳定 |
冒泡排序
思想:
遍历数组,每次将最大或者最小的数放到最后一位
1.排升序时,将当前的数和其后面的数比较,如果比他小,就交换俩数的位置,如果比他大,就不做操作,继续进行这样的比较,直到到达比较的边界位置处。
2.排降序时,将将当前的数和其后面的数比较,如果比他大,就交换俩数的位置,如果比他小,就不做操作,继续进行这样的比较,直到到达比较的边界位置处。
代码实现:
//冒泡排序
void Bubblesort(int* a, int n)
{
for (int i = 0; i < n; i++)
{
for (int j = 0; j < n - 1 - i; j++)
{
if (a[j] > a[j + 1]) //排升降序控制这里就好了
{
Swap(&a[j], &a[j + 1]);
}
}
}
}
解释:
因为有n个数,所以需要遍历n次,但每一次遍历的数的个数都会比上一次遍历的个数要少一个,举例说明,假设n为10,比如说i等于1时,这相当于第二次遍历,因为前面已经将最大或者最小的数已经放到数组的末尾,所以我们这次遍历只需要遍历(n-i)个数就好了,即9个数,但在数组中坐标是从0开始的,而我们控制边界也是控制数组的下标,所以此时边界为(n-i-1),即8,即a[j+1]中的(j+1)最大就是8,即数组中的第9个位置。
选择排序
注:上面放的图是排升序的
思想:
假设要将n个数排成升序,我们就要遍历n次,每次都记录其中最大(或者最小)的数的下标,遍历结束后,将其丢到最后面(或者最前面)
- 升序:
- 每次找最大丢后面
- 每次找最小丢前面
- 降序
- 每次找最大丢前面
- 每次找最小丢后面
代码实现如下:
//普通版选择排序
void Selectsort2(int* a, int n)
{
int maxi = 0; //默认最大的数为a[0]
int end = n - 1;
for (int i = 0; i < n; i++)
{
for (int j = 0; j < n - i; j++)
{
if (a[j] > a[maxi]) //排升降序控制这里就好了
{
maxi = j;
}
}
--end;
Swap(&a[maxi], &a[end]); //每次将最大的值放到尾巴去
//Swap(&a[maxi], &a[n-i-1]);
}
}
下面也给出升级版的选择排序:
思想:
每次遍历将最大或者最小的数选出来,同时排好范围内最大或者最小的数
//选择排序 升级版
void Selectsort(int* a, int n)
{
int begin = 0; //规定未排序的区间
int end = n - 1; //规定已排序的区间
int maxi = begin; //将数组首元素赋给俩个人
int mini = begin;
while (end > begin)
{
for (int i = begin; i <= end; i++)
{
if (a[i] > a[maxi])
{
maxi = i;
}
if (a[i] < a[mini])
{
mini = i;
}
}
Swap(&a[begin], &a[mini]);
if (begin == maxi) //处理最大的数在begin位置时 及时更新maxi的位置
{
maxi = mini;
}
Swap(&a[end], &a[maxi]);
++begin;
--end;
}
}
注意: 如果我们先交换的begin位置和mini位置处的数,这个时候就可能会有最大的数刚好在begin位置,这时候如果我们直接交换,就会出现end位置放到并不是最大的数,就会使排序出错。
处理方法:每次begin和mini位置交换完之后,进行判断,如果maxi==begin的话,就及时更新maxi。
插入排序
思想:
将一个记录插入到已经排好序的有序表中,从而一个新的、记录数增1的有序表。
下面我们先从将一个数插入到一个有序数组中形成新的数组开始。
思路:从有序数组的末尾开始遍历,如果比要插入的数大,就将其往后挪一位,直到找到比需要插入的数的小,就将要插入的数放在此数后面
int x = 4;//要插入的数据
int end = n - 1;//指向数组末尾
while (end >= 0)
{
if (a[end] > x) //end指向的元素大于x 将往后挪一位
{
a[end + 1] = a[end];
end--;
}
else //找到比x小的数 跳出循环 将x排在该数后面
{
break;
}
}
a[end + 1] = x;
而我们的插入排序的思路正是这个思路:将一个数插入到有序的数组中,形成新的有序数组。而这个怎么原地在数组中实现呢?也很简单,只要我们将排序想成(n-1)次插入数据:从将一个数插入到一个只有一个数的数组中,再到将一个数插入到这个新形成的含有俩个数的有序数组中,以次下去,就排好序了。
实现方法:end从0开始,需要插入的数为a[end+1]
//N次遍历 真插入排序 时间复杂度O(N^2)
for (int i = 0; i < n - 1; i++) //这里end最大为n-1-1,是因为插入数的位置最大为n-1
{
int end = i;//指向数组末尾
int x = a[end + 1];
while (end >= 0)
{
if (a[end] > x) //end指向的元素大于x 将往后挪
{
a[end + 1] = a[end];
end--;
}
else //找到比x小的数 跳出循环 将x排在该数后面
{
break;
}
}
a[end + 1] = x;
}
希尔排序
- 前言:发明这个排序的大佬发现插入排序在排接近有序的序列时,效率很高,时间复杂度几乎接近O(N),于是他就想出来插入排序的升级版,先将序列变得接近有序,最后再使用一次插入排序,而使序列变得更有序的方法就是将距离为gap的数分到一组进行插入排序,反复改变gap,进行操作,且保证最后一次操作时的gap为1,即用插入排序去排解决有序的数组。
思想:
先选定一个整数(gap),把待排序文件中所有记录分成几个组,所有距离为gap的记录分在同一组内,并对每一组内的记录进行排序。然后对gap进行处理,取到新的gap,重复上述分组和排序的工作。当到达gap=1时,所有记录在统一组内排好序。
下面我们举个例子希尔排序是怎么进行排序的说明一下:
第一次我们取gap为5,即距离为5的被分到同一组,所以10个数被分成了[9,4] [1,8] [2,6] [5,3] [7,5] 五组,然后对每组里面数进行排序操作后变成[4,9][1,8][2,6][3,5][5,7]
接着我们对gap进行/2操作,得到gap=2,即距离为2的数被分到一组,10个数又被成[4,2,5,8,5] [1,3,9,6,7]俩组,重复上面的操作得到新的数组
gap为1,就是我们之前所说的插入排序
代码实现如下:
int gap = n;
while (gap >1) //此处应该为 gap>1 因为如果是gap=1时 以gap>0为条件 会时gap进去俩次 多进入一次
{
gap /= 2; //保证gap能等于1就行 gap等于1相当于 原来的插入排序
/*gap = gap / 3 + 1;*/
for (int i = 0; i < n - gap; i++) //当i走到n-gap时就结束了 因为这是以gap为距离的几个小数组的末尾中最小的末尾
{
int end = i;//指向数组末尾
int x = a[end + gap]; //指向end后gap的数据
while (end >= 0)
{
if (a[end] > x) //end指向的元素大于x 将往后挪
{
a[end + gap] = a[end];
end -= gap;
}
else //找到比x小的数 跳出循环 将x排在该数后面
{
break;
}
}
a[end + gap] = x;
}
}
而上面的代码又是怎么来的呢?
解释:
灵魂一式
- 我们先从对1组距离为gap的数进行插入排序的实现开始.
我们先放出代码再进行解释
int gap = 5;
//遍历一次以gap为距离的小区间
for (int i = 0; i < n - gap; i+=gap)
{
int end = i;//指向数组末尾
int x = a[end + gap]; //指向end后gap的数据
while (end >= 0)
{
if (a[end] > x) //end指向的元素大于x 将往后挪
{
a[end + gap] = a[end];
end-=gap;
}
else //找到比x小的数 跳出循环 将x排在该数后面
{
break;
}
}
a[end + gap] = x;
}
拿第一组数{9,4}来解释,end一开始为9的下标0,则x为a[0+gap]即4,然后进行插入排序,因为9是比x大的,就把9丢到end的后gap位去即a[0+gap],然后对end进行一次-=gap操作,此时end为0-gap,已经小于0了,所以会跳出循环,然后将x丢到a[end+gap]处,即a[0]处。
灵魂第二式
- 然后我们接下来进行实现的就是对距离为gap的数进行插入排序
思路:
我们发现每个小数组的首位数据都是俩俩相邻的,我们自然就会想到嵌套循环,而我们那层循环控制为多少次呢,我们会发现一个规律,gap为多少,数组就会被分成多少组数据,所以外层循环次数就为gap次
下面放代码 😗
//遍历gap次以gap为距离的小区间
for (int j = 0; j < gap; j++)
{
for (int i = j; i < n - gap; i += gap)
{
int end = i;//指向数组末尾
int x = a[end + gap]; //指向end后gap的数据
while (end >= 0)
{
if (a[end] > x) //end指向的元素大于x 将往后挪
{
a[end + gap] = a[end];
end -= gap;
}
else //找到比x小的数 跳出循环 将x排在该数后面
{
break;
}
}
a[end + gap] = x;
}
}
灵魂第三式
- 现在的代码已经很接近源代码里面的样子,但还是能做出优化,我们发现可以将外层循环剥掉,将内循环的跳整距离gap改为1也是一样的,就此得出优化版本,我称之为乱炖型遍历
//乱炖遍历数组 按照距离为gap遍历
for (int i = 0; i < n - gap; i++) //当i走到n-gap时将结束了 因为这是以gap为间隔的小数组的末尾中最小的末尾
{
int end = i;//指向数组末尾
int x = a[end + gap]; //指向end后gap的数据
while (end >= 0)
{
if (a[end] > x) //end指向的元素大于x 将往后挪
{
a[end + gap] = a[end];
end -= gap;
}
else //找到比x小的数 跳出循环 将x排在该数后面
{
break;
}
}
a[end + gap] = x;
}
灵魂第四式
- 最后一步:就是控制我们的gap,而控制gap也是有讲究的,必须保证最后的gap=1,一般有俩种方法:
- 每次/2
- 每次/3+1
下面丢出代码:😃
//接下来开始控制gap的大小
int gap = n;
while (gap >1) //此处应该为 gap>1 因为如果是gap=1时 以gap>0为条件 会时gap进去俩次 多进入一次
{
gap /= 2; //保证gap能等于1就行 gap等于1相当于 原来的插入排序
/*gap = gap / 3 + 1;*/
for (int i = 0; i < n - gap; i++) //当i走到n-gap时就结束了 因为这是以gap为距离的几个小数组的末尾中最小的末尾
{
int end = i;//指向数组末尾
int x = a[end + gap]; //指向end后gap的数据
while (end >= 0)
{
if (a[end] > x) //end指向的元素大于x 将往后挪
{
a[end + gap] = a[end];
end -= gap;
}
else //找到比x小的数 跳出循环 将x排在该数后面
{
break;
}
}
a[end + gap] = x;
}
}
快速排序
基本思想:
任取待排序元素序列中的某元素作为基准值,按照该排序码将待排序集合分割成两子序列,左子序列中所有元素均小于基准值,右子序列中所有元素均大于基准值,然后最左右子序列重复该过程,直到所有元素都排列在相应位置上为止。
主框架:
// 假设按照升序对array数组中[left, right)区间中的元素进行排序
void QuickSort(int array[], int left, int right)
{
if(right - left <= 1)
return;
// 按照基准值对array数组的 [left, right)区间中的元素进行划分
int div = partion(array, left, right);
// 划分成功后以div为边界形成了左右两部分 [left, div) 和 [div+1, right)
// 递归排[left, div)
QuickSort(array, left, div);
// 递归排[div+1, right)
QuickSort(array, div+1, right);
}
框架定了我们下面来实现里面的子函数:
注:下面取基准值为key
单趟遍历三式
1. (hoare版本)左右指针法实现
思路:
Left指针每次找比基准值小大的数,Right指针每次找比基准值key小的数。然后交换俩个指针位置的值。当俩指针相遇是,将key位置的值和俩指针相遇处的值交换,从而使左边的值都小于key,右边的值都大于key
通常key有俩种选择:
- 一是最左边left所在处
- 二是最右边right所在处
下面我们先以key取最left处解释,key为最左边元素时,我们先让R指针去寻找比key小的值,然后再找L指针去找比key大的值,这样可以保证最后左右指针相遇处的值永远小于或等于key位置的值,因为最坏的情况就是R指针没有找到比key小的值,一路走到L,L还未出发就相遇了,key原地换。
key取最右边原理也是相同的,让L先走,去找比key位置处的值大的数,然后再让R出发去找比基准值小的值,这样可以保证最后左右指针相遇处的值永远大于或等于key位置的值,然后确保最后交换之后满足左边的值都小于key,右边的值都大于key。
下面是代码实现:
//子函数1 -- 前后指针
int Partion1(int* a, int left, int right)
{
//三值取中 处理有序的情况 防止最坏情况出现
int mid = getMid(a, left, right);
//将此值换到key位置
Swap(&a[mid], &a[left]);
//默认key为left
int keyi = left;
while (left < right) //左右指针还未相遇时
{
//1.右边先挪动找小
while (left < right && a[right] >= a[keyi])
{
--right;
}
//2.左边找大
while (left < right && a[left] <= a[keyi])
{
++left;
}
Swap(&a[left], &a[right]);
}
//最后交换key位置和俩指针相遇位置
Swap(&a[left], &a[keyi] );
return left; //key值即为left和right相遇位置
}
代码实现时需要注意的点:
1.左指针找大和右指针找小时,注意越界问题。
例:右边找小时,一直没有找到比key小的值,这时候没有left<right的限制的话,right就会往前越界。
2.寻找比key值大或小的数,一定不要严格大于或者小于
例:假设Left找大时,我们把a[left] <= a[keyi],里面的=号去掉,第一次L就会继续停在key位置处,然后进行交换,就会把key值一开始就丢出去了。
2. 挖坑法
思路:
选择一个基准值:
- 最左边位基准值,将基准值所在位置处设为坑位,然后R指针出发找小,找到小的了,就将找到值丢到坑位处,然后将R当前所在处设为新坑位,接下来L指针开始找大,找到大的就将值丢到坑位中,然后就地形成新坑位,直接R,L指针相遇为止,然后将key值丢到俩指针相遇处。
注:这里让右指针R先走也是为了确保R,L指针最后相遇时的值是比key值小的。*- 最右边位基准值,将基准值所在位置处设为坑位,然后L指针出发找大的,找到大的了,就将找到值丢到坑位处,然后将L当前所在处设为新坑位,接下来R指针开始找小,找到小的就将值丢到坑位中,然后就地形成新坑位,直接R,L指针相遇为止,然后将key值丢到俩指针相遇处。
注:这里让左指针L先走也是为了确保R,L指针最后相遇时的值是比key值大的。
代码:😃
//挖坑法
int Partion2(int* a, int left, int right)
{
//初始坑设在最左边
int pit = left;
int key = a[pit]; //保留初始坑的值
while (left < right)
{
//1.右边先找小,
while (left<right && a[right] >key)
{
--right;
}
//将找到的值丢到已有都坑,形成新的坑
a[pit] = a[right];
pit = right;
//2.左边再找大
while (left < right && a[left] < key)
{
++left;
}
//将找到的值再丢到现有的坑 生成新的坑
a[pit] = a[left];
pit = left;
}
//将key的值丢到最后的坑位
a[pit] = key;
return pit;
}
3. 前后指针法
前言:此方法也是最多人使用的最上面的动图使用的也是这个方法,这个方法实现较好控制。
思路: 将小的丢到左边,大的翻到右边
1.key值为最左边:
prev初始指向key值处,cur指针每次走在prev的前面,找比key值小的数,找到了之后,先++prev,然后交换俩位置的值,直到cur越界为止,一直重复上述操作,最后将key位置的值和prev所在位置的值交换。
2.key值为最右边:
prev初始指向最左边的前一个位置处,cur从数组首元素位置出发,cur指针每次走在prev的前面,找比key值小的数,找到了之后,先++prev,然后交换俩位置的值,直到cur走到key位置为止,一直重复上述操作,最后先++prve,再将key位置的值和prev所在位置的值交换。
好控制的原因:上面俩种方法都是不会遍历到key值处的
代码实现: 😃
//方法三:前后指针 cur prev
int Partion3(int* a, int left, int right)
{
//默认key为最左边
int keyi = left;
int prev = left;
int cur = prev + 1;
while (cur <= right)
{
//cur找比key值要小的
if (a[cur] < a[keyi] && ++prev != cur) //cur在prev前面时可不交换,因为++prev之后,俩指针就会在同一位置
{
Swap(&a[prev],&a[cur]);
}
++cur;
}
Swap(&a[prev], &a[keyi]);
return prev; //返回key值所在处
}
快排小优化
快排的最坏情况:处理已经有序的数组时,此时每次的key值会一直在最左端或者最右端
,时间复杂度就会到达O(N^2)
解决方法:三值取中,将L,R和中间位置处的值选择出一个大小居中的值作key
int getMid(int* a, int left, int right)
{
int mid = left + (right - left) / 2; //防止 left加right 越界
if (a[mid] > a[left])
{
if (a[mid] < a[right])
{
return mid;
}
else if(a[right]>a[left])
{
return right;
}
else
{
return left;
}
}
else
{
if (a[mid] > a[right])
{
return mid;
}
else if (a[right] > a[left])
{
return left;
}
else
{
return right;
}
}
}
然后在上面3个方法前加上
//三值取中 处理有序的情况 防止最坏情况出现
int mid = getMid(a, left, right);
//将此值换到key位置
Swap(&a[mid], &a[left]);
堆排序
详细堆排序见之前的文章
链接
下面直接丢出代码:😃
void Adjustdown(int* a,int parent,int n)
{
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;
}
}
//堆排序 法一:先搓个堆 然后top k次 法二:原地造堆
void Heapsort(int* a, int n)
{
//原地建堆 向下调整原地建堆
for (int i = (n - 1 - 1) / 2; i>=0 ; i--)
{
Adjustdown(a, i, n );
}
//然后进行调整位置,进行向下调整
int end = n - 1;
while (end>= 1)
{
Swap(&a[0],&a[end]);
Adjustdown(a, 0, end);
//Print(a, n);
end--;
}
}
归并排序
基本思想:
分治的思想:将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有
序,再使子序列段间有序
代码: 😃
void _Mergesort(int* a, int left, int right, int* tmp)
{
//递归结束条件
if (left >= right)
{
return;
}
//先划分区间
int Mid = left + (right - left) / 2;
_Mergesort(a,left, Mid,tmp);
_Mergesort(a, Mid + 1,right, tmp);
//开始归并数组 俩个有序数组合并
int begin1 = left; int end1 = Mid;
int begin2 = Mid+1; int end2 = right;
int cur = left;
while (begin1 <= end1 && begin2 <= end2) //有一个子数组遍历完了就结束了
{
if (a[begin1] < a[begin2])
{
tmp[cur++] = a[begin1++];
}
else
{
tmp[cur++] = a[begin2++];
}
}
//处理还没结束的数组
while (begin1 <= end1)
{
tmp[cur++] = a[begin1++];
}
while (begin2 <= end2) //此处不可写 begin!=end2 会造成越界访问
{
tmp[cur++] = a[begin2++];
}
//将合并好的数组tmp再拷回原数组a
for (int i = left; i <= right; i++)
{
a[i] = tmp[i];
}
}
void Mergesort(int* a, int n)
{
int* tmp = (int*)malloc(sizeof(int) * n);
if (tmp == NULL)
{
printf("malloc fail\n");
}
_Mergesort(a, 0, n - 1, tmp);
//释放归并的数组
free(tmp);
tmp = NULL;
}
计数排序
操作步骤:
- 统计相同元素出现次数
- 根据统计的结果将序列回收到原来的序列中
代码实现: 😃
// 时间复杂度:O(Max(N, Range))
// 空间复杂度:O(range)
// 适合范围比较集中的整数数组
// 范围较大,或者是浮点数等等都不适合排序了
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* count = (int*)malloc(sizeof(int)*range);
memset(count, 0, sizeof(int)*range);
if (count == NULL)
{
printf("malloc fail\n");
exit(-1);
}
// 统计次数
for (int i = 0; i < n; ++i)
{
count[a[i] - min]++;
}
// 根据次数,进行排序
int j = 0;
for (int i = 0; i < range; ++i)
{
while (count[i]--)
{
a[j++] = i + min;
}
}
}
注:上面也可以处理负数,因为使用的是相对映射
例:假设里面最小的数是-2,min即为-2,然后-2就会被放在count[-2-(-2)],即count[0]处,而在拷回原数组时,会以(0+(-2))的形式拷回去。
未完待续……还有归并和快排的非递归版本,俩者都可以用栈实现,归并还可以用循环实现。