文章目录
插入排序
插入排序就跟我们玩扑克一下,当我们码牌的时候,不断选大 的往后 插,选小的 往前插
代码如何实现呢?
我们可以 看做 第一个数看做有序的, 直接枚 举 (end作为枚举变量) 第 二个数(下标为1),同时让一个tmp变量存储它下一个位置的值,判断此时a[end]与tmp的大小 ,如果 a[end] > tmp 那么此时就说明 tmp该放前面 ,所 以 a[end]后 移 , 同 时 end-- 往 前 移 动 , 然 后 再 判 断 此 时a[end]与tmp的 大小,这时要注意, 如果此时我们 的tmp是最小值,那么end比下标为0的都要小 ,那 么end有可能会小于0,所以我们每次放tmp都是a[end+1]= tmp,因为此时我们把a[end]往后移, end--,如果停下来了,就说明此时a[end] < tmp,那么a[end+1]不就是我们应该放的位置吗。
代码实现:
c
//1.插入排序
//时间复杂度: O(N^2)
void InsertSort(int* a, int n)
{
int end = 0;
int tmp = 0;
for (end = 1; end < n-1; end++)
{
tmp = a[end + 1];
while (end >= 0 && a[end] > tmp)
{
a[end + 1] = a[end];
end--;
}
a[end + 1] = tmp;
}
}
希尔排序
希尔排序可以看做是插入排序的优化版,插入排序如果排的是有序数组,那将非常快,我们这次先对原数组进行预排序,先让数组部分有序,怎么做呢?
我 们 可 以 定 义 一 个 gap ,gap初始化 n/2 ,每次比较的时候,tmp 这时候不是a[end+1]了,而是a[end+gap] ,如果 a[end] > tmp , 就让 a[end]向后移动到gap , 同时 end -= gap ,然后再让 tmp 去到end位 置 ,这时候就相当 于对gap进行了预排序 ,当gap == 1 的时候就是 插入排序 ,不过经过预排序后,此时插入排序的速度会有很大的优化。
时间复杂度: O(N*lgN)
这里其实跟插入排序一样,只不过1变成了gap,为了保证end+gap小于n,所以我们的end只能枚举到n-gap-1
代码实现:
c
void ShellSort(int* a, int n)
{
int gap = n;
int end = 0;
int tmp = 0;
int i = 0;
while (gap > 1)
{
gap /= 2;
for (i = 0; i < n - gap; i++)
{
end = i;
tmp = a[end + gap];
while (end >= 0)
{
if (a[end] > tmp)
{
a[end + gap] = a[end];
end -= gap;
}
else
break;
}
a[end + gap] = tmp;
}
}
}
测试排序消耗时间
那究竟有没有优化呢?
这里有个函数clock,他可以返回程序消耗的时间,单位是毫秒
如图定义一个大小为10万的数组,每个元素都是rand随机出来的,再分别对他们进行插入和希尔排序,记录所需要的时间。
这里我们测试可以调到release版本,可以看到希尔排序对于插入排序来说是一个很大的优化
快速排序
核心思路 :快排的总体思路就是,在数组中选一个key 出来,它可以是数组中的任意元素 ,但是一般 我们都选首元素 和尾元素 ,第一趟 的结果就是让数组以key为界限 ,key左边的元素 一定小于 它,key右边的元素 一定大于 它,然后再不断的缩小左区间 和右区间 ,直到排到一个元素为止
时间复杂度: O(N*lgN)
快速排序作为最快的排序,我们这里分为递归和不递归版本
根据核心思路,我们这里有三种做法
- 挖坑法
- 左右指针法
- 前后指针法
不管哪种方法,目的都是核心思路
挖坑法
首先我们先定义一个key ,表示我们要依据它划分 ,下图中我们选择第一个数作为key ,用pivot(坑)记录此时的下标,从end往前找 只要找到比他小 的我们就交换 两个数的位置,同时pivot指向end
找到比key还小的数后,start 从头开始找比key大的 ,找到了就交换 ,同时pivot 就变为start此时的位置了 ,一直找直到start >= end 为止
当找完一遍过后,此时数组元素如下图:
我们可以看以看到 ,此时 红色49左边 都 是比他小的数字 ,红色49右边 的 都比它大 ,这个步骤就是我们快排的核心步骤 ,此时红色49的下标为pivot,我们只需要再按照上面的步骤遍历 **[0.pivot-1][pivot+1,rihgt]**就可以得到一个有序的数组了。
代码实现:
c
//挖坑法
这里的right注意我们调用的时候要传入闭区间
void PartSort1(int* a, int left, int right)
{
if (left >= right)//如果left == right就说明只有一个元素了,那就不用比了,直接return返回
return;
int begin = left;
int end = right;
int pivot = left;
int key = a[pivot];
while (begin < end)
{
//从后找小去前面
while (end > begin && a[end] >= key)
{
end--;
}
//此时a[end] < a[pivot]
//1.交换
swap(&a[end], &a[pivot]);
//2.把end给Pivot
pivot = end;
while (end > begin && a[begin] <= key)
{
begin++;
}
swap(&a[begin], &a[pivot]);
pivot = begin;
}
PartSort1(a, left, pivot - 1);
PartSort1(a, pivot + 1, right);
}
但是快排还有个缺陷,就是当此时如果我们用现在写的快排去排一个有序的数组时,效率会很低,为什么呢?
如果数组有序 的话,那么我们的end就会找到start为止,此时Pivot指向0 ,pivot-1肯定执行不了了,所以我们只能执行**[pivot+1,right]** ,那么end还是会找到头,指向不断找,最后的执行次数就是 1+2+3+ ...+ n-1 ,那最后的时间复杂度就为O(N^2)。
怎么解决呢?
三数取中
这里有个方法叫三数取中,既然数组有序的情况下,我们如果取第一个那就会去到最小值 ,这样就会造成O(N^2)的时间复杂度,那么我们就取中间的值就好了 ,
如何取呢,当我们拿到一个数组的时候,我们可以判断在**[l,r]这个区间呢** , nums[l],nums[mid],nums[r] 的大小,我们把**nums[mid]这个和nums[l]**的值一交换,那么无论数组是有序还是无序,我们都不会取到极端情况,这样就能保证快排的效率。
代码实现:
c
int TreeNumMid(int* a, int left, int right)
{
int mid = left + ((right - left) >> 1);
if (a[left] < a[right])
{
if (a[right] < a[mid])//left < right < mid
return right;
else if (a[left] > a[mid]) // mid < left < right
return left;
else //right >= mid left <= mid
return mid;
}
else // a[left] > right
{
if (a[left] < a[mid]) // right < left <mid
return left;
else if (a[right] > mid)
return right;
else
return mid;
}
return -1;
}
小区间优化
由于我们是用递归实现的,如果数据很大,比如100万,1000万,这么多数要进行排序,那么我们会有大量的小区间排序,这里我们以10个数排序为例,如果数据量在1000万
那么在最后在开辟大约10个元素这样的小区间,就会开辟大量的栈帧 ,就会消耗时间 ,所以我们当数据元素在10个或者15个的时候 ,我们就不使用用快排,直接使用插入排序 ,为什么使用插入排序呢?因为我们此时快排已经排了一点序了 ,所以相对来说是比较有序 的,插入排序 此时排已经进过预排序的数组会很快 ,所以我们使用插入排序。
代码实现:
c
void PartSort1(int* a, int left, int right)
{
if (left >= right)
return;
//1.优化2,小区间优化
if (right - left + 1 < 14)
{
InsertSort(a + left, right - left + 1);
}
else
{
//优化1.三数取中
int Minindex = TreeNumMid(a, left, right);
swap(&a[left], &a[Minindex]);
int begin = left;
int end = right;
int pivot = left;
int key = a[pivot];
while (begin < end)
{
//从后找小去前面
while (end > begin && a[end] >= key)
{
end--;
}
//此时a[end] < a[pivot]
//1.交换
swap(&a[end], &a[pivot]);
//2.把end给Pivot
pivot = end;
while (end > begin && a[begin] <= key)
{
begin++;
}
swap(&a[begin], &a[pivot]);
pivot = begin;
}
PartSort1(a, left, pivot - 1);
PartSort1(a, pivot + 1, right);
}
}
左右指针
这里借用一下佬的动图
以上只是第一趟的结果,我们还需要不断的调整,[left,keyi-1][keyi+1,right]的区间的顺序,最终才能有序,就是我提到的核心思路
快排这里我们都选第一个值当做key
不过这里我们除了记录key值以外,还要记录此时的keyi(key值下标),然后end往前遍历找小,start往后遍历找大,只有都找到了,才能交换,当start >= end的时候,还有与keyi交换,因为我们是以key作为分界点的。
**注意:**这里我们要先让end先找
我们是找严格大于或小于key值的数,就是 > 或 < 而不是 >=,<=,所以如果我们先让start开始找的话,那么最后start和keyi交换的值就是比key要大的值,如下图:
那么此时这个76比key还要大的值就会跑到key值的左边。
代码实现:
c
//左右指针法
void PartSort2(int* a, int left, int right)
{
if (left >= right)
return;
if (right - left + 1 < 14)
{
InsertSort(a + left, right - left + 1);
}
else
{
//优化1.三数取中
int Minindex = TreeNumMid(a, left, right);
swap(&a[left], &a[Minindex]);
int begin = left;
int end = right;
int keyi = begin;
//如果先从后面找大,就让keyi等于left
//错误原因:
/* keyi = begin, 如果我们先从Begin的位置找小, 那么最后begin停下来的位置一定是大于keyi位置的元素的, 此时只要交换就把
比keyi位置元素大的元素交换过去了,而我们是要begin左边比keyi小,右边比他大,所以出错了*/
//否则就让keyi等于right
while (begin < end)
{
while (begin < end && a[end] >= a[keyi])
{
end--;
}
while (begin < end && a[begin] <= a[keyi])
{
begin++;
}
swap(&a[begin], &a[end]);
}
swap(&a[begin], &a[keyi]);
PartSort2(a, left, begin - 1);
PartSort2(a, begin + 1, right);
}
}
前后指针法
网上找的动图 [doeg]
以上只是第一趟的结果,我们还需要不断的调整,[left,keyi-1][keyi+1,right]的区间的顺序,最终才能有序,就是我提到的核心思路
这里定义一个prev 指向left ,而cur 指向prev+1 ,同时选第一个为key ,并用keyi记录此时key值的下标 ,cur不断移动取找比key要小 的,放在key的左边 ,然后prev首先要++ ,然后才交换,因为最终我们的keyi位置是要当做分界点的 ,直到cur > right,然后交换keyi和prev ,再继续遍历**[left,prev-1],[prev+1,right]**的子区间
代码实现:
c
//前后指针法
void PartSort3(int* a, int left, int right)
{
if (left >= right)
return;
//1.优化2,小区间优化
if (right - left + 1 < 14)
{
InsertSort(a + left, right-left + 1);
}
else
{
//优化1.三数取中
int Minindex = TreeNumMid(a, left, right);
swap(&a[left], &a[Minindex]);
int begin = left;
int end = right;
int keyi = begin;
int prev = left;
int cur = prev + 1;
while (cur <= end)
{
if (a[cur] < a[keyi])
{
++prev;
swap(&a[prev], &a[cur]);
}
cur++;
}
swap(&a[prev], &a[keyi]);
PartSort1(a, left, prev - 1);
PartSort1(a, prev + 1, right);
}
}
//快速排序
//核心思想: 取一个值当做中间的值,不断地比较,最终使得mid 左边的值一定比mid这个位置的值小,右边一定大于它
快排-非递归版
上述中我们使用的都是递归,也可以不适用递归,其实我们递归主要是用来划分我们要调整的子区间,那么我们也可以使用栈 来存储我们每次需要调整的区间 ,因为递归所需要的数据结构其实就是栈 ,特点是: 后入先出
如图我们只需要往栈里面存储我们需要调整区间的下标即可,当我们用完之后直接Pop掉就可以了
如上图: 这里我选则先调整左区间,那么我们就要先把有区间给弄进去,然后再把左区间给压进去,红色序号代表压栈的顺序。
以左区间为例子:
只要我们此时的left < pivot-1 ,那我们就先把pivot-1 压栈然后再把left 压栈,这样取出来的就是left,pivot-1
右区间就是只要 pivot+1 < right,就依次压入,right,pivot+1
代码实现:
这里我们以挖坑法作为调整的方法,但是此时我们就要让挖坑法返回pivot的下标
c
//快排-非递归
int PartSort1_NonR(int* a, int left, int right)
{
//优化1.三数取中
int Minindex = TreeNumMid(a, left, right);
swap(&a[left], &a[Minindex]);
int begin = left;
int end = right;
int pivot = left;
int key = a[pivot];
while (begin < end)
{
//从后找小去前面
while (end > begin && a[end] >= key)
{
end--;
}
//此时a[end] < a[pivot]
//1.交换
swap(&a[end], &a[pivot]);
//2.把end给Pivot
pivot = end;
while (end > begin && a[begin] <= key)
{
begin++;
}
swap(&a[begin], &a[pivot]);
pivot = begin;
}
return pivot;
}
void QuickSortNonR(int* a, int n)
{
Stack st;
StackInit(&st);
StackPush(&st,n-1);
StackPush(&st,0);
int left = 0;
int right = 0;
while (!StackEmpty(&st))
{
left = StackTop(&st);
StackPop(&st);
right = StackTop(&st);
StackPop(&st);
int mid = PartSort1_NonR(a,left,right);
if (mid + 1 < right)
{
StackPush(&st, right);
StackPush(&st,mid+1);
}
if (mid - 1 > left)
{
StackPush(&st,mid-1);
StackPush(&st, left);
}
}
StackDestroy(&st);
}
归并排序
归并排序 就是排序两个有序数组 ,只要两个区间有序 ,那我把他俩按照顺序 排列起来不就是有序数组了吗?
那怎么让两个区间有序呢? ,把每个区间再划分为两个子区间 ,再让两个子区间分别有序 ,然后再排列,一直分分分直到子区间只有一个元素 时,那他肯定有序
如图,我们把数组一半一半 分为两个子区间,再分到只有一个元素,然后排序,保证每个子区间都有序,最后一合并就为一个有序数组,所以这里我们会创建一个数组 来存储合并后的有序数组 ,然后再把数据拷回去。
归并递归版
我们直到了应该划分子区间直到不可划分之后再合并,那如何划分呢?
我们可以求出mid 下标,然后再不断递归**[0,mid]和[mid+1,right]** ,只要此时区间只有一个元素的时候我们就直接return,此时都不用归并。这样我们只需要写一个合并两个有序数组就可以了。
代码实现:
c
//归并排序
void MergeSort(int* a, int left, int right, int* tmp)
{
if (left >= right)
return;
int mid = left + ((right - left) >> 1);
MergeSort(a,left,mid,tmp);
MergeSort(a,mid+1,right,tmp);
int begin1 = left;
int begin2 = mid + 1;
int begin3 = left;
int j = 0;
while (begin1 <= mid && begin2 <= right)
{
if (a[begin1] < a[begin2])
{
tmp[begin3++] = a[begin1++];
}
else
{
tmp[begin3++] = a[begin2++];
}
}
while (begin1 <= mid)
{
tmp[begin3++] = a[begin1++];
}
while (begin2 <= right)
{
tmp[begin3++] = a[begin2++];
}
for (j = left; j <= right; j++)
{
a[j] = tmp[j];
}
}
归并非递归版
归并非递归版我们要一个一个开始合并,然后再依次增大合并的范围,定义一个gap表示每次归并的范围,这个gap应该每次都要*2
如下图:
至于i怎么取,可以看到我们是有一个end2 ,他作为第二个数组的边界 ,他是不能大于等于数组长度的 ,而且每次我们的i应该要跨越2*gap,2*gap就我们合并的两个有序数组的长度
**注意:**我们的gap每次*2那就会是偶数,那如果我们在这个的基础上多一个元素呢?
如下图:
当我们此时begin1来到新的结尾后,此时begin2都超出数组长度 了,那么此时我们就不应该再继续 了,直接break跳出循环 ,同时gap*2 就可以了
当我们的gap此时等于原数组长度时(8) ,那么此时begin2此时指向了3 ,但是此时的end2就已经超了 ,那这个时候我们就要把end2修正了 ,只要begin2还在,end2超出数组长度,我们就要给他修正一下,直接赋值为n-1。
总结一下:
- 只要begin2此时大于等于数组长度直接break,不要再调整了
- begin2还没有超出,但是end2已经超出了,及时修复end2,给end2赋值为n-1
代码实现:
c
void MergeSortNonR(int* a,int n, int* tmp)
{
int gap = 1;
int i = 0;
int j = 0;
while (gap < n)
{
for (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;
int begin3 = begin1;
if (begin2 >= n)
break;
if (end2 >= n)
end2 = n - 1;
while (begin1 <= end1 && begin2 <= end2)
{
if (a[begin1] < a[begin2])
{
tmp[begin3++] = a[begin1++];
}
else
{
tmp[begin3++] = a[begin2++];
}
}
while (begin1 <= end1)
{
tmp[begin3++] = a[begin1++];
}
while (begin2 <= end2)
{
tmp[begin3++] = a[begin2++];
}
for (j = i; j <= end2; j++)
{
a[j] = tmp[j];
}
}
gap *= 2;
}
}
结言:
到这里我要说的排序差不多说完了,后期学完二叉树的时候会把堆排序写在二叉树章节,如果有问题的欢迎指出来~我们下期再见~