目录
写在前面的话
小伙伴们大家好啊!小菜鸡森哩又来了,今天依旧更新有关排序的内容。那么今天首先是冒泡排序,然后是快速排序的三种实现以及快排的非递归实现。
一,冒泡排序
1.1思路实现
冒泡排序,顾明思议就是找出最大或者最小的数,然后将其与最后一个或者第一个元素交换,然后在不包括这个元素的一组数中,再次找出次大值或者次小值,然后再将其与倒数第二个或者第二个元素交换。
然后一直重复,直到需要排序的数只剩一个,这样最后得到的便是一个有序的序列了。
因为冒泡排序思路是比较简单的,所以其实现是比较冗余的,故其时间复杂度是很高的为O(N^2),所以一般我们在排序的时候,是不会用它去实现排序的。
1.2思路优化
当然,如果我们一定要用它来排序的话,那么一些小小的优化我们还是需要做的,比如在某一次排序之后我们发现没有元素移动,此时说明当前序列已经有序了,此时我们就不需要再排序了,直接结束即可!
那么对于这种情况来说,我们使用一个标杆来记录,如果有排序,就改变标杆的值,那么每次一趟排序之后,如果我们监测到标杆的值没有发变化,则直接退出循环即可。
1.3代码实现
//冒泡排序
void bubbleSort(int* a, int n)
{
//flag的意义是 当数组进行到中途的时候,发现数组已经有序了,那么我们就不需要再进行下一次循环了
int flag = 0;
//多层循环嵌套,每次将最后一个元素不包括在范围内
for (int j = n; j > 0; j--)
{
//一次循环,从 0 位置开始到 j-1 位置结束。
for (int i = 0; i < j - 1; i++)
{
if (a[i] > a[i + 1])
{
swap(&a[i], &a[i + 1]);
flag = 1;
}
}
if (flag == 0)
break;
}
}
二,快速排序
那么因为快速排序对于其他排序来说是比较有优势的,所以这里我们对于快速排序的几种实现都了解一下。
2.1快排的第一种实现
2.1.1递归函数主要思路实现
那么首先我们是最初始的快排思路,如下图所示,首先我们需要两个下标指针 begin 和 end ,以及一个对比元素 key。
第一步,首先让 end 指针从最后一个元素开始往左走,第一次找出一个比 key 值小的元素的时候,就停下;然后 begin 再从第一个元素开始找,第一次找出比 key 值大的元素就停下。
第二步,将现在 begin 和 end 指向的两个元素交换。重新执行第一步。
然后重复上面两个步骤,直到两个指针指向同一个位置,此时我们再将该位置元素元素和 key 元素交换,同时将 key 的位置移动到当前它移动之后的位置。就得到了下面这个结果。
如下图所示:
那么经过上面这次排序之后,对于原数组的变化,可以看到,在 key 值左边的元素都是小于等于改值的,而对于 key 值右边的元素,则是全部大于 key 的。接下来我们将 key 值下标返回,然后递归调用 key 之前的一部分,和 key 之后的另一部分,直到递归主函数的 begin 大于等于 end 的时候,此时整个排序结束。
tips:
1.对于递归主函数我们在实现了三种思路之后,总体实现,因为三种实现都需要递归。
2.如果是左 key 的话,首先得从右边找小值;而如果是右 key 的话,则需要首先先从左边找大,这是一个规律,而且必须这样做,至于为什么这样实现,大家可以自行理解哦!我们在最后回答!
2.1.2第一种方法代码实现
int Partion1(int *a,int begin,int end)
{
//一层循环
int mini = GetMidIndex(a,begin,end);
swap(&a[mini], &a[begin]);
int key = begin;
while (begin < end)
{
//左key,先走右边
//快排里面的两个条件,一个都不能缺,而且一个是将等于它自己的数不算做不符合条件的数,继续往下走了。
while (begin<end && a[end] >= a[key])
{
end--;
}
//再走左边
while (begin<end && a[begin] <= a[key])
{
begin++;
}
swap(&a[begin],&a[end]);
}
swap(&a[end],&a[key]);
return end;
}
那么对于当前代码而言,最开始的几行代码是选出了一个 key 值,那么为什么那样去选,因为三种方式都需要这样去选,所以我们在后面统一处理。
2.2快排的第二种实现
第二种思路我们将其简称为挖坑法。
2.2.1思路实现
如下图所示,对于指针以及 key 值的设定,以及那个指针先移动,都是和第一种方法一样的。
那么接下来我们首先将 key 值记录下来,然后将该位置作为 “坑位”。接着同样的,首先移动 end 指针找大于 key 值的数,第一次找到了之后,将该数填入到 “坑位”,然后它自己的位置变成新 “坑位”;
接着 begin 指针开始移动找小于 key 值的第一个元素,找到了则将其填入刚才的“坑位”,然后自己又变成了新的“坑位”。
那么我们重复进行刚才的两个步骤,直到两个指针指向同一个地方,此时表示本趟排序结束。
然后就是同样的,通过递归主函数将这组数分为两部分,然后对这两部分分别进行排序。然后就是一直递归,直到排完序。
那么如果有小伙伴对于这部分具体的细节还不是很了解的话,可以通过下面的图再梳理哦!
2.2.2代码实现
//挖坑法
int Partion2(int* a, int begin, int end)
{
int ret = GetMidIndex(a, begin, end);
swap(&a[ret], &a[begin]);
int key = a[begin];
int pivot = begin;
while (begin < end)
{
//先走右边
if (begin < end && a[end] >= key)
{
end--;
}
a[pivot] = a[end];
pivot = end;
//再走左边
if (begin < end && a[begin] <= key)
{
begin++;
}
a[pivot] = a[begin];
pivot = begin;
}
//最后一个坑位补满
a[pivot] = key;
return pivot;
}
2.3快排的第三种实现
第三种我们将其称为前后指针法,相对于前两种实现,前后指针法是比较简洁的。
那么首先如下图所示:
对于上面两种思路实现,其实都是差不多的,接下来我们首先以第一种为例。
2.3.1思路实现(第一种)
如上图所示,首先我们需要将 cur 移动找小于 key 值的元素,第一次在 “ 2 ” 的位置找到了。所以接下来我们再让 prve 自增一位,然后将 prev 位置的元素和 cur 位置元素交换。这就是一趟排序中的一次循环。
接着我们进行同样的操作,直到如上图所示 cur 超过数组范围表明本趟排序结束,然后就得到一个新序列,同样的和上面两种操作是一样的,我们将其分为两部分,再分别进行排序,直到所有元素排完。
2.3.2思路实现(第二种)
那么上面图所示的是,第一种情况下的,排序的思路。同样的,选择第二种的右 key 实现也是一样的,只不过此时 cur 的找的是小于 key 的数,然后接下来的步骤都是一样的,交换 cur 的值和 prev 的值,最后再将 key 值和 prev 的值交换。
这里我们不在赘述。
2.3.3思路优化
那么当然了我们在排序中有可能出现以下问题,cur 从移动的时候开始第一个数就是满足条件的数,那么如果指针 prev 移动一位,则直接指向了 cur 的位置,这样的交换是没有必要的,所以如果是这样的情况,那么我们可以直接跳过。
2.3.4源码实现
这里我们实现的是第一种思路的代码,但是两种思路都是差不多的。
//前后指针法
int Partion3(int *a,int begin,int end)
{
int ret = GetMidIndex(a, begin, end);
swap(&a[ret], &a[begin]);
int prev = begin;
int key = begin;
int cur = prev + 1;
while (cur <= end)
{
//if语句中也可以做自增,第一次遇到
if (a[cur] < a[key] && ++prev!=cur)
{
swap(&a[cur], &a[prev]);
}
cur++;
}
swap(&a[key], &a[prev]);
return prev;
}
2.4快排主函数实现
那么对于快排的三种思路我们在上面都做了介绍,但是上面介绍的都是一趟排序,那么对于主函数而言,这里我们有两种思路实现。
2.4.1递归主函数
那么我们知道,如果用递归去实现的话,思路是比较简单的。对于上面每一种思路,因为每次一趟排序结束之后,都会返回 key 值的下标,此时我们就可以通过 key 的下标得到两边的两个小序列,然后将其再进行排序,最后递归结束的条件是当 begin 的值大于 end 的时候。
代码实现:
void QuickSort(int *a, int begin,int end)
{
if (begin >= end)
return;
//小区间优化
//当只有十个数的时候,使用直接插入更加高效
if (end - begin + 1 < 10)
{
InsertSort(a+begin,end-begin+1);
}
else
{
int ret = Partion3(a, begin, end);
QuickSort(a, begin, ret - 1);
QuickSort(a, ret + 1, end);
}
}
2.4.2递归主函数的优化
那么对于快速排序而言,因为追求的是比较高的效率,所以这里当 begin 和 end 之间的数为十个以内的时候,因为对于小范围的数直接插入排序的效率是相对比较高的,所以我们选择直接插入排序来进行最后小范围的排序。
2.5快排主函数非递归实现
那么对于非递归而言,其实和递归思路是差不多的,只是这里我们用栈的先进后出的特性去实现。
首先就是我们将 begin 和 end 这两个数依次插入栈中,然后将其出栈,送入排序函数去排序,接着会返回一个 key 值,然后我们只需要和递归一样,将该数组分为两半,然后分别将两个小数组的begin 和 end 入栈,然后重复进行上面的步骤,直到栈为空,此时表明所有数都排完序了。
代码实现:
//类似于递归,只不过是通过栈的先进后出
//每一次将begin和end的值入栈,然后将放入一趟排序函数排序
void QuickNonr(int* a, int begin, int end)
{
ST st;
StackInit(&st);
StackPush(&st, begin);
StackPush(&st, end);
while (!StackEmpty(&st))
{
int end = StackTop(&st);
StackPop(&st);
int begin = StackTop(&st);
StackPop(&st);
int key = Partion3(a,begin,end);
if (key + 1 < end)
{
StackPush(&st, key+1);
StackPush(&st, end);
}
if (begin < key)
{
StackPush(&st, begin);
StackPush(&st, key-1);
}
}
}
好的,那么对于两种交换排序就结束啦!如有问题,还请指正呀!