冒泡
时间空间复杂度
排序原理
🍪 原理:通过对待排序序列从前向后(从下标较小的元素开始),依次对相邻两个元素的值进行两两比较,如果发现顺序跟想要的不一样则交换这两个数据的位置,使值较大(或较小)的数据逐渐从前移向后部,就如果水底下的气泡一样逐渐向上冒。
✅正如图中我们看到的这样,把比前一个小的的数据,想泡泡一样慢慢的“浮出”
代码
🥝升序代码:
#include <stdio.h>
//冒泡排序(升序)
void Bobblesort(int* arr, int n)
{
assert(arr);
int pos = 1;//为了提高效率,增加一个判断,假设整个数据已经有序
for (int i = 0; i < n; i++)//控制遍历的趟数
{
for (int j = 0; j < n - i - 1; j++)//控制每一趟比较的次数
{
if (arr[j] > arr[j + 1])
{
pos = 0;//存在交换情况,则证明这组数据还有可能乱序,修改POS的值为假
int tmp = arr[j]; //设置中间变量tmp记录要交换的其中一个数据
arr[j] = arr[j + 1];
arr[j + 1] = tmp;
}
}
if (pos)//当数据已经有序则提前退出
break;
}
}
//打印数据
void print(int arr[], int n)
{
for (int i = 0; i < n; i++)
{
printf("%d ", arr[i]);
}
printf("\n");
}
//测试函数正确性
int main()
{
int arr[10] = { 2,3,5,1,6,9,0,4,7,8 };
int sz = sizeof(arr) / sizeof(arr[0]);
printf("排序前:");
print(arr, sz);
Bobblesort(arr, sz);
printf("排序后:");
print(arr, sz);
return 0;
}
✅运行结果:
🥝降序代码:
#include <stdio.h>
//冒泡排序(降序)
void Bobblesort(int* arr, int n)
{
assert(arr);
int pos = 1;//为了提高效率,增加一个判断,假设整个数据已经有序
for (int i = 0; i < n; i++)//控制遍历的趟数
{
for (int j = 0; j < n - i - 1; j++)//控制每一趟比较的次数
{
if (arr[j] < arr[j + 1])
{
pos = 0;//存在交换情况,则证明这组数据还有可能乱序,修改POS的值为假
int tmp = arr[j]; //设置中间变量tmp记录要交换的其中一个数据
arr[j] = arr[j + 1];
arr[j + 1] = tmp;
}
}
if (pos)//当数据已经有序则提前退出
break;
}
}
//打印数据
void print(int arr[], int n)
{
for (int i = 0; i < n; i++)
{
printf("%d ", arr[i]);
}
printf("\n");
}
//测试函数正确性
int main()
{
int arr[10] = { 2,3,5,1,6,9,0,4,7,8 };
int sz = sizeof(arr) / sizeof(arr[0]);
printf("排序前:");
print(arr, sz);
Bobblesort(arr, sz);
printf("排序后:");
print(arr, sz);
return 0;
}
✅运行结果:
插入
🍪 插入排序
🍟英文名:Insertion Sort
原理
🍁 插入排序的工作原理是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。插入排序在实现上,通常采用in-place排序(即只需用到O(1)的额外空间的排序),因而在从后向前扫描过程中,需要反复把已排序元素逐步向后挪位,为最新元素提供插入空间。
🍔趣味解释:插入排序操作类似于摸牌并将其从大到小排列。每次摸到一张牌后,根据其点数插入到确切位置。
🍔如上图:表示的是摸到梅花7后进行插入的过程。忽略最右边的梅花10,相当于一开始7在最右边,然后逐个与左边的排相比较(当然左边的牌早已排好顺序),将其放置在合适的位置。当摸到后面的牌后重复上述过程即可。
现实逻辑
① 从第一个元素开始,该元素可以认为已经被排序
② 取出下一个元素,在已经排序的元素序列中从后向前扫描
③如果该元素(已排序)大于新元素,将该元素移到下一位置
④ 重复步骤③,直到找到已排序的元素小于或者等于新元素的位置
⑤将新元素插入到该位置后
⑥ 重复步骤②~⑤
动图解释
时间复杂度
🍟最好情况就是全部有序,此时只需遍历一次,最好的时间复杂度为O(n)
🍟最坏情况全部反序,内层每次遍历已排序部分,最坏时间复杂度为O()
🚨因此直接插入排序的平均时间复杂度为O()
空间复杂度
🥝这个很好想,也就是每次遍历空间复杂度都是O(1)
代码
#include <stdio.h>
//插入排序
void InsertSort(int arr[], int n)
{
int i = 1;
for (i = 1; i < n; i++)
{
int end = i - 1;
int tmp = arr[i];
while (end >= 0)
{
if (tmp < arr[end])
{
arr[end + 1] = arr[end];
end--;
}
else
{
break;
}
}
arr[end + 1] = tmp;
}
}
//打印数据
void print(int arr[], int n)
{
for (int i = 0; i < n; i++)
{
printf("%d ", arr[i]);
}
printf("\n");
}
int main()
{
int arr[10] = { 2,3,5,1,6,9,0,4,7,8 };
int sz = sizeof(arr) / sizeof(arr[0]);
printf("排序前:");
print(arr, sz);
InsertSort(arr, sz);
printf("排序后:");
print(arr, sz);
return 0;
}
运行结果
算法优化改进
方法一
场景分析:
⭕直接插入排序每次往前插入时,是按顺序依次往前查找,数据量较大时,必然比较耗时,效率低。
⭕改进思路: 在往前找合适的插入位置时采用二分查找的方式,即折半插入。
💧二分插入排序相对直接插入排序而言:平均性能更快,时间复杂度降至O(NlogN),排序是稳定的,但排序的比较次数与初始序列无关,相比直接插入排序,在速度上有一定提升。逻辑步骤:
① 从第一个元素开始,该元素可以认为已经被排序
② 取出下一个元素,在已经排序的元素序列中二分查找到第一个比它大的数的位置
③将新元素插入到该位置后
④ 重复上述两步
改进代码
// 插入排序改进:二分插入排序
void BinaryInsertSort(int arr[], int len)
{
int key, left, right, middle;
for (int i=1; i<len; i++)
{
key = a[i];
left = 0;
right = i-1;
while (left<=right)
{
middle = (left+right)/2;
if (a[middle]>key)
right = middle-1;
else
left = middle+1;
}
for(int j=i-1; j>=left; j--)
{
a[j+1] = a[j];
}
a[left] = key;
}
}
希尔排序
🚩希尔排序是对直接插入排序的优化。
🍟希尔排序(Shell's Sort)是插入排序(插入排序-C语言实现_硕硕C语言的博客-CSDN博客)的一种又称“缩小增量排序”(Diminishing Increment Sort),是直接插入排序算法的一种更高效的改进版本。希尔排序是非稳定排序算法。该方法因 D.L.Shell 于 1959 年提出而得名。
⭕中文名 :希尔排序 ⭕外文名:Shell's Sort
⭕别 名:缩小增量排序 ⭕类 型:插入排序
⭕空间复杂度:O(1) ⭕稳定性:不稳定
发展历史
🍟希尔排序按其设计者希尔(Donald Shell)的名字命名,该算法由希尔在 1959 年所发表的论文“A high-speed sorting procedure” 中所描述。希尔排序是基于插入排序的以下两点性质而提出改进方法的:
⭕插入排序在对几乎已经排好序的数据操作时,效率高,即可以达到线性排序的效率。但插⭕入排序一般来说是低效的,因为插入排序每次只能将数据移动一位。
🍟1961年,IBM 公司的女程序员 Marlene Metzner Norton(玛琳·梅茨纳·诺顿)首次使用FORTRAN语言编程实现了希尔排序算法。在其程序中使用了一种简易有效的方法设置希尔排序所需的增量序列:第一个增量取待排序记录个数的一半,然后逐次减半,最后一个增量为 1。
🍟该算法后来被称为 Shell-Metzner 算法 ,Metzner 本人在2003年的一封电子邮件中说道:“我没有为这种算法做任何事,我的名字不应该出现在算法的名字中。”
基本思想
🚩希尔排序是对插入排序的优化,基本思路是先选定一个整数作为增量,把待排序文件中的所有数据分组,以每个距离的等差数列为一组,对每一组进行排序,然后将增量缩小,继续分组排序,重复上述动作,直到增量缩小为1时,排序完正好有序。
🚩希尔排序原理是每一对分组进行排序后,整个数据就会更接近有序,当增量缩小为1时,就是插入排序,但是现在的数组非常接近有序,移动的数据很少,所以效率非常高,所以希尔排序又叫:缩小增量排序。
🚨每次排序让数组接近有序的过程叫做预排序,最后一次插入是直接插入排序
时间复杂度
🍪希尔排序的时间的时间复杂度为:O( ),希尔排序时间复杂度的下界是n*log2n。
🥝希尔排序没有快速排序算法快 O(n(logn)),因此中等大小规模表现良好,对规模非常大的数据排序不是最优选择。但是比O()复杂度的算法快得多。并且希尔排序非常容易实现,算法代码短而简单。
🍔Shell算法的性能与所选取的分组长度序列有很大关系。只对特定的待排序记录序列,可以准确地估算关键词的比较次数和对象移动次数。想要弄清关键词比较次数和记录移动次数与增量选择之间的关系,并给出完整的数学分析,今仍然是数学难题。
🥰具体我们以一组数字来说操作说明:
🔴 假设有一组{9, 1, 2, 5, 7, 4, 8, 6, 3, 5}无需序列。
⭕第一趟排序:
🥝设 gap1 = N / 2 = 5,即相隔距离为 5 的元素组成一组,可以分为 5 组。接下来,按照直接插入排序的方法对每个组进行排序。
⭕第二趟排序:
🥝将上次的 gap 缩小一半,即 gap2 = gap1 / 2 = 2 (取整数)。这样每相隔距离为 2 的元素组成一组,可以分为2组。按照直接插入排序的方法对每个组进行排序。
⭕第三趟排序:
🥝再次把 gap 缩小一半,即gap3 = gap2 / 2 = 1。 这样相隔距离为1的元素组成一组,即只有一组。按照直接插入排序的方法对每个组进行排序。此时,排序已经结束。
🚨注:需要注意一下的是,图中有两个相等数值的元素5和5。我们可以清楚的看到,在排序过程中,两个元素位置交换了。
gap的选取
🍁希尔排序的效率取决于增量值gap的选取,时间复杂度并不是一个定值。开始时,gap取值较大,子序列中的元素较少,排序速度快,克服了直接插入排序的缺点;其次,gap值逐渐变小后,虽然子序列的元素逐渐变多,但大多元素已基本有序,所以继承了直接插入排序的优点,能以近线性的速度排好序。
🍁步长的选择是希尔排序的重要部分,只要最终步长为1任何步长序列都可以工作。算法最开始以一定的步长进行排序,然后会继续以一定步长进行排序,最终算法以步长为1进行排序。当步长为1时,算法变为插入排序,这就保证了数据一定会被排序。
🍁最初的建议是折半再折半知道最后的步长为1<也就是插入排序>,虽然这样取可以比O(n2)类的算法(插入排序)更好,但这样仍然有减少平均时间和最差时间的余地。可能希尔排序最重要的地方在于当用较小步长排序后,以前用的较大步长仍然是有序的。比如, 如果一个数列以步长5进行了排序然后再以步长3进行排序,那么该数列不仅是以步长3有序,而且是以步长5有序。如果不是这样,那么算法在迭代过程中会打乱以前的顺序,那就不会以如此短的时间完成排序了。
🍁最优的空间复杂度为开始元素已排序,则空间复杂度为 0;最差的空间复杂度为开始元素为逆排序,则空间复杂度为 O(N);平均的空间复杂度为O(1)希尔排序并不只是相邻元素的比较,有许多跳跃式的比较,难免会出现相同元素之间的相对位置发生变化。比如上面的例子中希尔排序中相等数据5就交换了位置,所以希尔排序是不稳定的算法。
动图演示
代码:
//希尔排序
void ShellSort(int a[], int n)
{
// 1、gap > 1 预排序
// 2、gap == 1 直接插入排序
int gap = n;
while (gap > 1)
{
gap = gap / 3 + 1; // +1可以保证最后一次一定是1
for (int i = 0; i < n - gap; i++)
{
int end = i;
int tmp = a[end + gap];
while (end >= 0)
{
if (tmp < a[end])
{
a[end + gap] = a[end];
end -= gap;
}
else
{
break;
}
}
a[end + gap] = tmp;
}
}
}
总结:
🍎希尔排序通过将比较的全部元素分为几个区域来提升插入排序的性能,交换不相邻的元素以对数组的局部进行排序,最终用插入排序将局部有序的数组排序。
🍎希尔排序时效分析很难,关键码的比较次数与记录移动次数依赖于增量因子序列d的选取,特定情况下可以准确估算出关键码的比较次数和记录的移动次数。目前还没有人给出选取最好的增量因子序列的方法。增量因子序列可以有各种取法,有取奇数的,也有取质数的,但需要注意:因子中除1外增量没有公因子,且最后一个增量因子必须为1。
🍟后面硕硕也会整理一些快速排序,以及更快的排序方法,谢谢大家的观看。如果发现硕硕有什么错误的地方欢迎到评论区留言。一起加油吧🥰🥰🥰
选择排序
基本思想
🍔首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置,然后,再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。以此类推,直到所有元素均排序完毕。
🍔选择排序的思想其实和冒泡排序有点类似,都是在一次排序后把最小的元素放到最前面,或者将最大值放在最后面。但是过程不同,冒泡排序是通过相邻的比较和交换,而选择排序是通过对整体的选择,每一趟从前往后查找出无序区最小值,将最小值交换至无序区最前面的位置。
实现逻辑
⭕ 第一轮从下标为 1 到下标为 n-1 的元素中选取最小值,若小于第一个数,则交换
⭕ 第二轮从下标为 2 到下标为 n-1 的元素中选取最小值,若小于第二个数,则交换
⭕ 依次类推下去……
动图演示
🚨🚨注:红色表示当前最小值,黄色表示已排序序列,绿色表示当前位置。
复杂度分析
✅平均时间复杂度:O(N^2)
✅最佳时间复杂度:O(N^2)
✅最差时间复杂度:O(N^2)
✅空间复杂度:O(1)
✅稳定性:不稳定
代码实现
void Swap(int* p1, int* p2)
{
int tmp = *p1;
*p1 = *p2;
*p2 = tmp;
}
void SimpleSelectSort(int *a,int len)
{
int min;
for (int i = 0;i < len - 1;i++)
{
min = i;
for (int j = i + 1;j < len;j++)
{
if (a[min] > a[j])
{
min = j;
}
}
if (min != i)
{
Swap(&a[min], &a[i]);
}
}
}
🚩优化改进-->二元选择排序
😍改进思路: 简单选择排序,每趟循环只能确定一个元素排序后的定位。根据之前冒泡排序的经验,我们可以考虑改进为每趟循环确定两个元素(当前趟最大和最小记录)的位置,从而减少排序所需的循环次数。改进后对n个数据进行排序,最多只需进行[n/2]趟循环即可。
改进代码
//二元选择排序
void Swap(int* p1, int* p2)
{
int tmp = *p1;
*p1 = *p2;
*p2 = tmp;
}
void SelectSort(int* a, int n)
{
int begin = 0, end = n - 1;
while (begin < end)
{
int mini = a[begin];
int maxi = a[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]);
// 如果maxi和begin重叠,修正一下即可
if (begin == maxi)
maxi = mini;
Swap(&a[end], &a[maxi]);
begin++;
end--;
}
}
快速排序
快速排序,又称划分交换排序(partition-exchange sort)
中文名 快速排序算法 外文名 quick sort
别 名 快速排序 提出者 C. A. R. Hoare
提出时间 1960年 时间复杂度 O()
空间复杂度 O ()
快速排序是Hoare于1962年提出的一种二叉树结构的交换排序方法,其基本思想为:任取待排序元素序列中的某元素作为基准值,按照该排序码将待排序集合分割成两子序列,左子序列中所有元素均小于基准值,右子序列中所有元素均大于基准值,然后最左右子序列重复该过程,直到所有元素都排列在相应位置上为止。
实现逻辑
快速排序使用分治法(Divide and conquer)策略来把一个序列(list)分为两个子序列(sub-lists)。
① 从数列中挑出一个元素,称为 “基准”(pivot),
② 重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。在这个分区退出之后,该基准就处于数列的中间位置。这个称为分区(partition)操作。
③ 递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序。
递归到最底部时,数列的大小是零或一,也就是已经排序好了。这个算法一定会结束,因为在每次的迭代(iteration)中,它至少会把一个元素摆到它最后的位置去。
// 假设按照升序对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);
}
上述为快速排序递归实现的主框架,发现与二叉树前序遍历规则非常像,同学们在写递归框架时可想想二叉树前序遍历规则即可快速写出来,后序只需分析如何按照基准值来对区间中数据进行划分的方式即可。将区间按照基准值划分为左右两半部分的常见方式有:
1. hoare版本
代码
int PartSort1(int* a, int begin, int end)
{
int left = begin, right = end;
int keyi = left;
while (left < right)
{
//右边先走,找比a[keyi]小的
while (left < right && a[right] >= a[keyi])
{
right--;
}
//左边先走,找比a[keyi]大的
while (left < right && a[left] <= a[keyi])
{
left++;
}
//交换左右边
Swap(&a[left], &a[right]);
}
//交换keyi与交点的值
Swap(&a[left], &a[keyi]);
keyi = left;
}
2. 挖坑法
代码
// 挖坑法
int PartSort2(int* a, int begin, int end)
{
int key = a[begin];
int piti = begin;
while (begin < end)
{
// 右边找小,填到左边的坑里面去。这个位置形成新的坑
while (begin < end && a[end] >= key)
{
--end;
}
a[piti] = a[end];
piti = end;
// 左边找大,填到右边的坑里面去。这个位置形成新的坑
while (begin < end && a[begin] <= key)
{
++begin;
}
a[piti] = a[begin];
piti = begin;
}
a[piti] = key;
return piti;
}
3. 前后指针版本
代码
int PartSort3(int* a, int begin, int end)
{
int keyi = begin;
int cur = begin + 1;
int prev = begin;
// 加入三数取中的优化
int midi = GetMidIndex(a, begin, end);
Swap(&a[keyi], &a[midi]);
while (cur <= end)
{
if (a[cur] < a[keyi])
{
prev++;
Swap(&a[cur], &a[prev]);
}
cur++;
}
Swap(&a[begin], &a[prev]);
keyi = prev;
return keyi;
}
快速排序优化
1. 三数取中法选key
当我们知道这组无序数列的首和尾后,我们便可以求出这个无需数列的中间位置的数,我们只需要在首,中,尾这三个数据中,选择一个排在中间的数据作为基准值,进行快速排序,即可进一步提高快速排序的效率。那么为什么要取中间呢?我们可以假设待排序的数列是一组高度有序的数列,显然首极大可能是最小值,尾极大可能是最大值,此时如果我们选取一个排在中间的值,哪怕是在最坏的情况下,begin和end只需要走到中间位置,那么这个中间值的位置也就确定下来,而不需要begin或end指针要把整个数列遍历一边,从而大大提高快速排序的效率。即取数组最左端最右端以及数组中间三个数的中间数为分区点,减少采用左右端点碰到极端顺序的出现的最坏情况( 当选取左右端点,碰到数据有序,从大到小或是从小到大的情况 ,算法时间复杂度就会变成最坏时间复杂度)。
int GetMidIndex(int* a, int begin, int end) // 三数取中,优化算法,避免发生最坏的情况
{
int mid = (begin + end) / 2;
if (a[begin] < a[mid])
{
if (a[mid] < a[end])
{
return mid;
}
else if (a[begin] < a[end])
{
return end;
}
else
{
return begin;
}
}
else // (a[begin] >= a[mid])
{
if (a[mid] > a[end])
{
return mid;
}
else if (a[begin] < a[end])
{
return begin;
}
else
{
return end;
}
}
}
2. 递归到小的子区间时,可以考虑使用插入排序
序列长度达到一定大小时,使用插入排序当快排达到一定深度后,划分的区间很小时,再使用快排的效率不高。当待排序列的长度达到一定数值后,可以使用插入排序。由《数据结构与算法分析》(Mark Allen Weiness所著)可知,当待排序列长度为5~20之间,此时使用插入排序能避免一些有害的退化情形。
void Qsort(int* a, int begin, int end)
{
if (begin >= end) return;
if (end - begin > 10)// 优化二:在递归到剩余数据量小于一定值的时候跳出递归,进行小数据量的插入排序
{
int keyi = PartSort3(a, begin, end);
// [begin, keyi-1] keyi [keyi+1, end]
Qsort(a, begin, keyi - 1);
Qsort(a, keyi + 1, end);
}
else
{
InsertSort(a + begin, end - begin + 1);
}
}
快速排序非递归(用栈实现)
void QsortStack(int* a, int begin, int end)
{
ST st;
STInit(&st);
STPush(&st, end);
STPush(&st, begin);
while (!STEmpty(&st))
{
int left = STTop(&st);
STPop(&st);
int right = STTop(&st);
STPop(&st);
int keyi = PartSort3(a, left, right);
// [left, keyi-1] keyi[keyi+1, right]
if (keyi + 1 < right)
{
STPush(&st, right);
STPush(&st, keyi + 1);
}
if (left < keyi - 1)
{
STPush(&st, keyi - 1);
STPush(&st, left);
}
}
STDestroy(&st);
}
快速排序的特性总结
1. 快速排序整体的综合性能和使用场景都是比较好的,所以才敢叫快速排序
2. 时间复杂度:O(N*logN)
3. 空间复杂度:O(logN)
4. 稳定性:不稳定
全部代码
//快速排序
int PartSort1(int* a, int begin, int end)
{
int left = begin, right = end;
int keyi = left;
while (left < right)
{
//右边先走,找比a[keyi]小的
while (left < right && a[right] >= a[keyi])
{
right--;
}
//左边先走,找比a[keyi]大的
while (left < right && a[left] <= a[keyi])
{
left++;
}
//交换左右边
Swap(&a[left], &a[right]);
}
//交换keyi与交点的值
Swap(&a[left], &a[keyi]);
keyi = left;
}
// 挖坑法
int PartSort2(int* a, int begin, int end)
{
int key = a[begin];
int piti = begin;
while (begin < end)
{
// 右边找小,填到左边的坑里面去。这个位置形成新的坑
while (begin < end && a[end] >= key)
{
--end;
}
a[piti] = a[end];
piti = end;
// 左边找大,填到右边的坑里面去。这个位置形成新的坑
while (begin < end && a[begin] <= key)
{
++begin;
}
a[piti] = a[begin];
piti = begin;
}
a[piti] = key;
return piti;
}
int GetMidIndex(int* a, int begin, int end) // 三数取中,优化算法,避免发生最坏的情况
{
int mid = (begin + end) / 2;
if (a[begin] < a[mid])
{
if (a[mid] < a[end])
{
return mid;
}
else if (a[begin] < a[end])
{
return end;
}
else
{
return begin;
}
}
else // (a[begin] >= a[mid])
{
if (a[mid] > a[end])
{
return mid;
}
else if (a[begin] < a[end])
{
return begin;
}
else
{
return end;
}
}
}
int PartSort3(int* a, int begin, int end)
{
int keyi = begin;
int cur = begin + 1;
int prev = begin;
// 加入三数取中的优化
int midi = GetMidIndex(a, begin, end);
Swap(&a[keyi], &a[midi]);
while (cur <= end)
{
if (a[cur] < a[keyi])
{
prev++;
Swap(&a[cur], &a[prev]);
}
cur++;
}
Swap(&a[begin], &a[prev]);
keyi = prev;
return keyi;
}
void Qsort(int* a, int begin, int end)
{
if (begin >= end) return;
if (end - begin > 10)// 优化二:在递归到剩余数据量小于一定值的时候跳出递归,进行小数据量的插入排序
{
int keyi = PartSort3(a, begin, end);
// [begin, keyi-1] keyi [keyi+1, end]
Qsort(a, begin, keyi - 1);
Qsort(a, keyi + 1, end);
}
else
{
InsertSort(a + begin, end - begin + 1);
}
}
void QsortStack(int* a, int begin, int end)
{
ST st;
STInit(&st);
STPush(&st, end);
STPush(&st, begin);
while (!STEmpty(&st))
{
int left = STTop(&st);
STPop(&st);
int right = STTop(&st);
STPop(&st);
int keyi = PartSort3(a, left, right);
// [left, keyi-1] keyi[keyi+1, right]
if (keyi + 1 < right)
{
STPush(&st, right);
STPush(&st, keyi + 1);
}
if (left < keyi - 1)
{
STPush(&st, keyi - 1);
STPush(&st, left);
}
}
STDestroy(&st);
}
归并排序
算法思路
归并排序算法有两个基本的操作,一个是分,也就是把原数组划分成两个子数组的过程。另一个是治,它将两个有序数组合并成一个更大的有序数组。
将待排序的线性表不断地切分成若干个子表,直到每个子表只包含一个元素,这时,可以认为只包含一个元素的子表是有序表。
将子表两两合并,每合并一次,就会产生一个新的且更长的有序表,重复这一步骤,直到最后只剩下一个子表,这个子表就是排好序的线性表。
归并操作的工作原理如下(非递归形式):
第一步:申请空间,使其大小为两个已经排序序列之和,该空间用来存放合并后的序列
第二步:设定两个指针,最初位置分别为两个已经排序序列的起始位置
第三步:比较两个指针所指向的元素,选择相对小的元素放入到合并空间,并移动指针到下一位置
重复步骤3直到某一指针超出序列尾将另一序列剩下的所有元素直接复制到合并序列尾
如下图所示 初始的数组是{ 16, 17, 13, 10, 9, 15, 3, 2, 5, 8, 12, 1, 11, 4, 6 }排序过程如下:
下面还有动图的演示初始数据为{ 3, 44, 38, 5, 47, 15, 36, 26, 27, 2, 46, 4, 19, 50, 48 }排序的详细过程。
递归代码
void _MergeSort(int* a, int begin, int end, int* tmp)
{
if (begin >= end)
return;
int mid = (begin + end) / 2;
// [begin, mid] [mid+1, end] 分治递归,让子区间有序
_MergeSort(a, begin, mid, tmp);
_MergeSort(a, mid + 1, end, tmp);
//归并 [begin, mid] [mid+1, end]
int begin1 = begin, end1 = mid;
int begin2 = mid + 1, end2 = end;
int i = begin1;
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, (end - begin + 1) * sizeof(int));
}
void MergeSort(int* a, int n)
{
int* tmp = (int*)malloc(sizeof(int) * n);
if (tmp == NULL)
{
printf("malloc fail\n");
exit(-1);
}
_MergeSort(a, 0, n - 1, tmp);
free(tmp);
}
非递归代码
类型一(修正边界法)
void MergeSortNonR1(int* a, int n)
{
int* tmp = (int*)malloc(sizeof(int) * n);
if (tmp == NULL)
{
printf("malloc fail\n");
exit(-1);
}
int gap = 1;
while (gap < n)
{
for (int i = 0; i < n; i += 2 * gap)
{
// [i,i+gap-1][i+gap, i+2*gap-1]
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;
}
else if(begin2 >= n)
{
begin2 = n;
end2 = n - 1;
}
else if(end2 >= n)
{
end2 = n - 1;
}
int j = begin1;
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);
}
类型二(越界跳出归并法)
void MergeSortNonR2(int* a, int n)
{
int* tmp = (int*)malloc(sizeof(int) * n);
if (tmp == NULL)
{
printf("malloc fail\n");
exit(-1);
}
int gap = 1;
while (gap < n)
{
//printf("gap=%d->", gap);
for (int i = 0; i < n; i += 2 * gap)
{
// [i,i+gap-1][i+gap, i+2*gap-1]
int begin1 = i, end1 = i + gap - 1;
int begin2 = i + gap, end2 = i + 2 * gap - 1;
// end1越界或者begin2越界,则可以不归并了
if (end1 >= n || begin2 >= n)
{
break;
}
else if (end2 >= n)
{
end2 = n - 1;
}
int m = end2 - begin1 + 1;
int j = begin1;
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) * m);
}
gap *= 2;
}
free(tmp);
}
总结
归并排序和选择排序一样,归并排序的性能不受输入数据的影响,但表现比选择排序好的多,因为始终都是O(n log n)的时间复杂度。代价是需要额外的内存空间。