排序算法-交换排序
目录
基本思想
一、冒泡排序
二、快速排序分析
1. hoare版本
2. 挖坑法
3. 前后指针版本
4. 快速排序的优化
三、代码示例
1. hoare版本
2. 挖坑法
3. 前后指针版本
四、快速排序(三路划分)
五、总结
基本思想
基本思想:所谓交换,就是根据序列中两个记录键值的比较结果来对换这两个记录在序列中的位置,交换排序的特点是:将键值较大的记录向序列的尾部移动,键值较小的记录向序列的前部移动。
一、冒泡排序
下面我们将以数组[5,3,1,6,2,4]来进行升序排序
排序过程图如下所示
第一趟排序结构为:
当第一趟结束后数组顺序为[3,1,5,2,4,6],因为这里演示的为升序,此时数组中最大值已经排序成功,接下来重复上面的步骤,并且只用排[0-n-1](n为数组的最大下标)区间的数据
第二趟结果为:
第三趟结果为:
第四趟结果:
第五趟结果:
外层循环的结束条件为:i=0时结束最终循环。
代码示例:
//冒泡排序
void BubbleSort(int* a, int n)
{
for (int j = n - 1; j > 0; j--)
{
int flag = true;
for (int i = 0; i < j; i++)
{
if (a[i] > a[i + 1])
{
Swap(&a[i], &a[i + 1]);
flag = false;
}
}
if (flag)
{
return;
}
}
}
其中我们可以定义一个flag来优化代码,这样如果循环结束前数据已经有序则不用再继续循环排序。
冒泡排序的特性总结:
1. 冒泡排序是一种非常容易理解的排序
2. 时间复杂度:O(N^2)
3. 空间复杂度:O(1)
4. 稳定性:稳定
二、快速排序分析
快速排序是Hoare于1962年提出的一种二叉树结构的交换排序方法,其基本思想为:任取待排序元素序列中的某元素作为基准值,按照该排序码将待排序集合分割成两子序列,左子序列中所有元素均小于基准值,右子序列中所有元素均大于基准值,然后最左右子序列重复该过程,直到所有元素都排列在相应位置上为止。
快速排序经过多年发展目前有三种不同的实现形式,但本质都是一样。
1. hoare版本
首先我们要对下面这组数进行排序,先标记左端的位置,再标记右端的位置,将最左端的位置的数作为基准值。
第一步:先从右向左移动R,当R找到比key值小的值的时候停下来,接着从左向右移动L,当L找到大于key值时停下来,随后交换L和R位置的数据。如图所示:
接下来每一步都重复上面步骤
最后当L和R指向同一个位置时结束循环。
当我门排完一次后基准值回到了他所在的正确位置,此时基准值左侧和右侧的数据还是乱序,我们只需递归左右两个空间即可完成排序。
递归过程大致为如图:
图中绿色的表示递归调用,红色的表示递归返回。
最终图中红色的数据就是最终排好序的结果。
2. 挖坑法
挖坑法首先是要记录下基准值key的值,然后标记好L和R的位置,还是得先让R从右往左移找小,再将找的小值填入坑的位置,并且让R成为坑,随后再移动L找大,将大值填入坑,再让自己成为坑。
第一道排序
第二道排序:
第三道排序:
第四道排序:
第五道排序:
第六道排序;
这种排序可以更加直观理解为什么必须先移动R位置才能够正确的排序。
3. 前后指针版本
开始状态如下图所示
第一步:
第二步:
第三步:
第三步:
第四步:
第五步:
最终循环结束时,基准值归为,接下来同样递归两边区间就可以实现快速排序。
4. 快速排序的优化
对于快速排序存在一种极端情况例如:存在一组降序的数据需要排升序,当出现这样的情况我们key值如果还取左左端的值,递归的层数就会从原来的扩大到N层,最终时间复杂度也会变成,为了避免这种情况,我们有两种优化方案:取随机值作为key、三数取中法选key。
但我们常用的是三数取中,下面是代码示例:
//三数取中(返回中间数的索引位置)
int GetMid(int* a, int left, int right)
{
int midindex = (right - left) / 2;
if (a[left] < a[midindex])
{
if (a[midindex] < a[right])
{
return midindex;
}
//a[midindex] > a[right]
if (a[right] > a[left])
{
return right;
}
if (a[left] > a[right])
{
return left;
}
}
//a[left] > a[midindex]
if (a[left] > a[midindex])
{
if (a[midindex] > a[right])
{
return midindex;
}
if (a[right] > a[midindex])
{
return right;
}
if (a[right] > a[left])
{
return left;
}
}
return midindex;
}
代码看起来很复杂,其实分析一下很简单,就是取数组第一个数,中间的数,和最后一个数,然后判断大小,找到值为中间的那个数,并返回该数的索引下标。
三、代码示例
1. hoare版本
//1.hoare版本
void QuickSort1(int* a, int left, int right)
{
//递归结束条件
if (left >= right)
return;
int begin = left, end = right;
//随机取key
//int randkeyi = left + rand() % (right - left)+1;
//Swap(&a[left], &a[randkeyi]);
//三数取中
int midindex = GetMid(a, left, right);
Swap(&a[left], &a[midindex]);
int keyi = left;
while (left < right)
{
//找小
while (left < right && a[right] >= a[keyi])
{
right--;
}
//找大
while (left < right && a[left] <= a[keyi])
{
left++;
}
Swap(&a[left], &a[right]);
}
Swap(&a[keyi], &a[left]);
//[begin,left-1] left [left+1,end]
QuickSort1(a, begin, left - 1);
QuickSort1(a, left + 1, end);
}
2. 挖坑法
//2.挖坑法
void QuickSort2(int* a, int left, int right)
{
//递归结束条件
if (left >= right)
return;
int begin = left, end = right;
//三数取中
int midindex = GetMid(a, left, right);
Swap(&a[left], &a[midindex]);
int value = a[left];
int hole = left; //需要填坑的位置
while (left < right)
{
while (left < right && a[right] >= value)
{
right--;
}
a[hole] = a[right];
hole = right;
while (left < right && a[left] <= value)
{
left++;
}
a[hole] = a[left];
hole = left;
}
a[hole] = value;
//[begin,hole-1] hole [hole+1,end]
QuickSort2(a, begin, hole - 1);
QuickSort2(a, hole + 1, end);
}
3. 前后指针版本
//3.前后指针法
void QuickSort3(int* a, int left, int right)
{
//递归结束条件
if (left >= right)
return;
//三数取中
int midindex = GetMid(a, left, right);
Swap(&a[left], &a[midindex]);
int prev = left ;
int cur = left+1;
while (cur <= right)
{
if (a[cur] >= a[left])
{
cur++;
}
else
{
prev++;
Swap(&a[prev], &a[cur]);
cur++;
}
}
Swap(&a[prev], &a[left]);
//[begin,prev-1] prev [prev+1,end]
QuickSort3(a, left, prev - 1);
QuickSort3(a, prev + 1, right);
}
四、快速排序(三路划分)
快速排序还存在一个及其特别的场景会导致效率变慢,如果一组数全是(大量)一样的数据也会使递归的层数就会从原来的扩大到N层,对于这种情况有一种特殊的解决办法叫做三路划分。
下面是全是一样数据的递归图:
其中箭头表示每一层递归排完序后的结果,绿色表示递归调用,红色是递归返回,这组n个数据都相同,调用的深度到达了n-1,也就相当于n层调用了当数据越多时调用深度是及其大的。
接下来我们用三路划分来演示对于大量相同数的排序:
先进行初始化
排序大致过程为:
判断cur与L的值大小
1. cur==L时cur就一直往前走
2. cur<L时 交换L与cur的值 cur++ L++
3. cur>L时 交换R与cur的值 R--
结束条件cur超过了R或者L超过了R。
上图是第一趟排序,可见排完序后再次递归的空间少了很多,大大解决了对于大量相同数据的递归太深问题。
代码示例:
//4.三路划分
void QuickSortThreeWay(int* a, int left, int right)
{
if (left >= right)
{
return;
}
int begin = left, end = right;
//三数取中
int keyi = GetMid(a, left, right);
Swap(&a[keyi], &a[left]);
int cur = left + 1;
while (cur <= right && left <= right)
{
if (a[cur] < a[left])
{
Swap(&a[cur], &a[left]);
cur++;
left++;
}
else if (a[cur] > a[left])
{
Swap(&a[cur], &a[right]);
right--;
}
else
{
cur++;
}
}
//排完一次序后:
//[begin,left-1][left,right][right+1,end]
QuickSortThreeWay(a, begin, left - 1);
QuickSortThreeWay(a, right + 1, end);
}
五、总结
快速排序的特性总结:
1. 快速排序整体的综合性能和使用场景都是比较好的,所以才敢叫快速排序
2. 时间复杂度:O(N*logN)
如果还想了解更多排序欢迎大家前往我的主页。