一.选择排序
选择排序就是选择一个数组的最大的数字或者最小的数字,放在一整个数组的最后或者开头的位置。
1.选择排序的实现
我们可以对选择排序进行一些加强,普通的选择排序是选择最小的数,然后进行交换。这个加强之后就是我们既要选择出最大的还有选择出最小的进行交换。
具体代码如下:
void SelectSort(int* a, int n)
{
int begin = 0;//俩个变量,一个在头一个在尾
int end = n - 1;
while (begin < end)//这俩个变量互相靠近
{
int mini = begin;
int maxi = begin;//假设最大和最小都是开始的那个位置
for (int i = begin + 1; i <= end; i++)//从a[1]开始
{
if (a[i] < a[mini])//如果比最小的小,就把此处的i值赋值给mini
mini = i;
if (a[i] > a[maxi])//同理,如果此处的比最大的大,就把i给maxi
maxi = i;
}
Swap(&a[begin], &a[mini]); //最小值找到了,开始排序,把最小值给初始位置
if (maxi == begin)//这里需要处理一个特殊情况,如果最大值在begin处的话,上面在换的过程中,换的就是maxi处的值
maxi = mini;//换完之后maxi指向的地方是begin,begin处的值就是最小的,mini指向的值是最大的,需要把mini的值赋值给maxi
Swap(&a[end], &a[maxi]);//之后再交换最后和maxi处的值
++begin;
--end;
}
}
2.选择排序的时间复杂度
选择排序很好理解,但是效率不高。如果在逆序,最坏的情况下,它的效率最低,拍好所消耗的时间最长。
需要进行(n-1)+(n-2)+...+2+1次比较,即(n^2-n)/2次比较。每一次比较都要交换元素,所以平均需要(n^2-n)/2次交换操作。时间复杂度就是O(N^2)。
二.快速排序
快速排序理解起来就有点复杂了。而且也有不同的方式可以来实现快速排序,比如:hoare方法,挖坑法,前后指针法。
1.hoare方法
Hoare方法通过选择一个基准值(pivot),将数组分为两个部分,小于等于基准值的部分和大于等于基准值的部分。然后,递归对这两个部分进行快速排序。
像是上图中的L和R,我们给一个基准值key,L往右走找比key大的值,R往左走找比key小的值,它们两个找到了就停止,然后交换两个位置的值,直到它们两个相遇停止,再交换此处和key位置的值。这是整体的进行交换,从它们相遇的地方再次分隔成两个区间,再一次进行上面的步骤。
值得考虑一下的是,它们相遇的位置一定比key位置的值小:
假如我们最左边作为key,让R先走,那么相遇的位置一定比key小。我们可以分为两种情况来看:
(1)一种是L遇到R,R先走,停下来,R停下的条件是遇到比key小的值,R停的位置一定比key小,L没有找到大的,遇到R直接就停下来了。
(2)还有一种的R遇到L,R先走,找小,没有找到比key小的,直接就跟L相遇了,L停留的位置就是上一轮交换的位置,上一轮的交换,把比key小的值交换到L位置了。
来写代码:
void Swap(int* a, int* b)
{
int tmp = *a;
*a = *b;
*b = tmp;
}
void QuickSort(int* a, int left, int right)
{
if (left >= right)
return;
int keyi = left;//我们假设我们要对比的值是最左边的值
int begin = left;
int end = right;//一个从最左边开始一个从最右边开始
while (begin < end)
{
while (a[end] >= a[keyi] && begin < end)//end往前找比keyi位置小的值
--end;
while (a[begin] <= a[keyi] && begin < end)//begin找后找比begin大的值,值得注意的是,这里都有一个限定条件begin<end
//主要是为了防止end减到begin前面,或者begin加到end后面,因为这都是在一个大的while循环内部,没结束这一层循环之前,不会在大的while循环里判断begin<end
++begin;
Swap(&a[begin], &a[end]);//每一次找到比begin位置大的值和比end小的值后就交换
}
Swap(&a[keyi], &a[begin]);//出去循环后再交换keyi和begin位置的值就行了
keyi = begin;//此时的begin的值和end的值是一样的
QuickSort(a, left, keyi - 1);//之后就是分成两半,左边和右边分别开始递归
QuickSort(a, keyi + 1, right);
}
注意这里用到的递归方式跟我们的二叉树比较像的,那个东西理解到位了,这里就非常好理解了。
2.挖坑法
这里的挖坑法的这种写法是我自己根据动图想的(可能跟其他人有点小出入),这个方法比上面上面的hoare方法更好理解一点。
void QuickSort1(int* a, int left, int right)
{
if (left >= right)
return;
int key = a[left];//我们默认最左边的值是坑位,我们先把最左边的值保存起来
int keng = left;
int begin = left;
int end = right;
while (begin < end)
{
while (a[end] >= key && begin < end)//找到比坑位的值小的值
--end;
a[keng] = a[end];//找到了我们就把这里的值放到坑里面去
keng = end;//移动坑位到我们的end位置
while (a[begin] <= key && begin < end)//然后从左边找到比坑位的值大的值
++begin;
a[keng] = a[begin];//把这里的值放到坑里面
keng = begin;//然后这里的begin成为新的坑,继续循环
}
a[keng] = key;//最后begin和end相遇的位置就是坑,把我们保存的key放在这里,后面就开始递归
QuickSort1(a, left, keng - 1);
QuickSort1(a, keng + 1, right);
}
3.前后指针法
void QuickSort2(int* a, int left, int right)
{
if (left >= right)
return;
int keyi = left;
int prev = left;//一个指针
int cur = prev + 1;//另一个指针指向下一个位置
while (cur <= right)//到指针越界跳出循环
{
if (a[cur] < a[keyi])//如果cur位置的值小于keyi位置的,进入if语句
{
++prev;//交换之前prev要先移动到下一位,如果prev和cur只相差一位的话,实际上就是交换自身等于没有交换
Swap(&a[cur], &a[prev]);
}
++cur;//不论cur位置的值是大于keyi位置的还是小于,cur都要往后走,如果cur位置的值一直大于keyi位置的,它们两个之间的距离会越来越大
}
Swap(&a[prev], &a[keyi]);//prev位置的值一定小于或者等于keyi位置的,交换它们两个
QuickSort2(a, left, prev - 1);
QuickSort2(a, prev + 1, right);
}
简单说一下思想,就是前面走一个指针,一直找比keyi位置的值小的值,找到了就跟prev的下一个位置进行交换(因为prev的位置一定是left或者上一轮交换完毕的比keyi位置小的值)。
4.快速排序的改进
本来这一个应该在前面就应该说的,但是我想快速排序就只是上面的代码就可以了。虽然上面的代码已经比较好了,但是依然有很大的弊端。一个是关于keyi的取值,我们都是默认在最左边的那个值的,但是这种方式有很大的弊端。
对于已经有序的数组,快速排序在每次选择基准元素时都选择最左边或者最右边的元素作为基准,这样导致快速排序的时间复杂度变为O(n^2),而不是理想情况下的O(nlogn)。
在数组中存在大量重复元素的情况下,快速排序可能出现分割不均匀的情况,导致快速排序的时间复杂度退化为O(n^2)。
而且如果待排数组的量非常大的时候,递归深度也会非常大,可能导致栈溢出。
还有一个就是我们可以优化一下当数据量很小的时候的排序。假如我们有10个数,我们用递归遍历从中间分开,就跟二叉树一样:
我们在递归的时候,就像是一个金字塔形,我们在排列最下面的数据的时候我们不用递归了,用其他的排序方式排列。我们可以省下将近百分之八十的效率。
4.1三数取中
这个就纯纯的逻辑问题了,注意看就行了:
int GetMidi(int* a, int left, int right)
{
int midi = (left + right) / 2;
// left midi right
if (a[left] < a[midi])
{
if (a[midi] < a[right])
{
return midi;
}
else if (a[left] < a[right])
{
return right;
}
else
{
return left;
}
}
else // a[left] > a[midi]
{
if (a[midi] > a[right])
{
return midi;
}
else if (a[left] < a[right])
{
return left;
}
else
{
return right;
}
}
}
4.2小区间优化
小区间我们就用插入排序来实现,小于10的意思就是,把下面三层的代码用插入排序代替。
if ((right - left + 1) < 10)
{
InsertSort(a+left, right - left + 1);
}
else
{
//快速排序代码...
}
假如优化代码的话就是这样:
void QuickSort(int* a, int left, int right)
{
if (left >= right)//如果只剩一个元素,直接跳出这一层
return;
if ((right - left + 1) < 10)
{
InsertSort(a + left, right - left + 1);
}
else
{
int midi = GetMidi(a, left, right);
Swap(&a[left], &a[midi]);
int keyi = left;//我们假设我们要对比的值是最左边的值
int begin = left;
int end = right;//一个从最左边开始一个从最右边开始
while (begin < end)
{
while (a[end] >= a[keyi] && begin < end)//end往前找比keyi位置小的值
--end;
while (a[begin] <= a[keyi] && begin < end)//begin找后找比begin大的值,值得注意的是,这里都有一个限定条件begin<end
//主要是为了防止end减到begin前面,或者begin加到end后面,因为这都是在一个大的while循环内部,没结束这一层循环之前,不会在大的while循环里判断begin<end
++begin;
Swap(&a[begin], &a[end]);//每一次找到比begin位置大的值和比end小的值后就交换
}
Swap(&a[keyi], &a[begin]);//出去循环后再交换keyi和begin位置的值就行了
keyi = begin;//此时的begin的值和end的值是一样的
QuickSort(a, left, keyi - 1);//之后就是分成两半,左边和右边分别开始递归
QuickSort(a, keyi + 1, right);
}
}
5.非递归的方式
我们上面用的都是递归的方式,这种方式当然很好的实现了快速排序,但是我们可不可以不用递归的方式来实现这个问题?
这里我们就需要用到一种我们之前学过的一种东西叫做栈。我们了解栈的特性就是先进后出,如果不了解的可以看我的另一篇博客:栈和队列
这里我就直接写函数的主体了。
int PartSort(int* a, int left, int right)
{
int midi = GetMidi(a, left, right);
Swap(&a[midi], &a[left]);
int prev = left;
int cur = prev+1;
int keyi = left;
while(cur<=right)
{
if (a[cur] < a[keyi])
{
++prev;
Swap(&a[cur], &a[prev]);
}
++cur;
}
Swap(&a[prev], &a[keyi]);
return prev;
}
void QuickSortNonR(int* a, int left, int right)
{
ST st;
STInit(&st);
STPush(&st, right);//先把右区间入栈
STPush(&st, left);//左区间值入栈
while (!STEmpty(&st))//栈里不为空就一直循环
{
int begin = STTop(&st);//提取出来的第一个元素是刚才最后入栈的元素
STTop(&st);
int end = STTop(&st);//紧接着就是刚才入栈的倒数第二个元素
STTop(&st);
int keyi = PartSort(a, begin, end);//这里的值是上面取出的中间的那个值,它左边都比它小,右边都比它大
//紧接着又是入栈的过程
if (keyi + 1 < end)//这里是右区间先入栈
{
STPush(&st, end);//依旧是先入最右边的数据
STPush(&st, keyi + 1);//然后入左边的
}
if (begin < keyi - 1)//这里是左区间入栈
{
STPush(&st, keyi - 1);//依旧是先入最右边的数据
STPush(&st, begin);//然后是左边的
}
//到这里栈里可能有四个元素,继续循环
}
STDestory(&st);
}
这里就是非递归的方式。其实每进行一次循环就是跟遍历相同的效果。
6.快速排序的时间复杂度
快速排序的时间复杂度为O(nlogn)。在最坏的情况下,即待排序的序列已经有序或者近乎有序时,快速排序的时间复杂度接近O(n^2)。但是平均情况下,快速排序的时间复杂度为O(nlogn)。
到这里这两种排序就差不多结束了,感谢大家的观看如有错误还请多多指正。