常见排序
快速排序
hoare版
1、基本思想:任取待排序元素序列中的某元素作为基准值(key),按照该排序码将待排序集合分割成两子序列,左子序列中所有元素均小于基准值,右子序列中所有元素均大于基准值,然后最左右子序列重复该过程,直到所有元素都排列在相应位置上为止。
2、过程:
a. 单趟排序:(keyi在最左边)right先出发找小于a[keyi]值的值(找到后等待),left后出发找大于a[keyi]值的值, 然后Swap(a[lefti],a[right])。
b. 重复利用单趟排序,keyi左边的全是小于a[keyi]的,右边全是大于a[keyi]的。
c. 划分为[begin,keyi-1], keyi, [keyi+1,end],分别进行递归,类似二叉树的前序遍历。
3、时间复杂度: o ( n l o g n ) o(nlogn) o(nlogn)
部分代码实现
根据上方过程 写出一趟的排序代码:
while (left < right)
{
//右边开始行动 一定要加上等于,因为快速排序找的是一定比它小的值
while (left < right && a[right] >= key)
{
right--;
}
//左边开始行动
while (left < right && a[left] <= a[keyi])
{
left++;
}
swap(&a[left], &a[right]);
}
swap(&(a[keyi]), &(a[right]));
全部代码实现
将key放到合适的位置后(keyi的左边都小于key,keyi的右侧都大于key),将待排序列分为三个部分:[begin,keyi-1] keyi [keyi+1,end],分别递归进行排序。
void QuickSort(int* a, int begin, int end)
{
if (begin >= end)
{
return;
}
int left = begin;
int right = end;
int keyi = left;
while (left < right)//一趟的实现
{
//右边开始行动 一定要加上等于,因为快速排序找的是一定比它小的值
while (left < right && a[right] >= key)
{
right--;
}
//左边开始行动
while (left < right && a[left] <= a[keyi])
{
left++;
}
swap(&a[left], &a[right]);
}
swap(&(a[keyi]), &(a[right]));
keyi = right;
//[begin,keyi-1] keyi [keyi+1,end]
QuickSort(a, begin, keyi - 1);
QuickSort(a, keyi+1, end);
}
注意
刚开始选择的基准值如果是左边第一个值,那么就应该右边right先出发一步步找到小于基准值keyi,这样可以保证最后left和right相遇的地方比基准值小(left和right相遇前,right 先 找到小于基准值的位置,等待left的靠近),交换后,保证了基准值的左边的值都小于基准值。反之亦然。
在left和right向中间移动的过程中(以left为例):循环条件中必须保证:
- left<right 这样在每趟找值的过程中保证了right不会超过left甚至越界
- 当遇到和key值相等的值时left和right任然要继续走当right处的值小于(left遇到大于key的值)时才能停止 。否则遇到以下情况 ,while (left < right && a[keyi] <= a[right])和while (left < right && a[left] <= a[keyi])会陷入死循环。
挖坑法
1、基本思想:挖坑法和hoare仅仅是理解方法上不一样,实际思路大致相同(某些单趟的后的结果可能不同)
2、过程:
a. 单趟排序:先设置坑位(最左为例),用key保存最先坑位的值,right找到比key小的值后将值放入坑位,然后将此处置为新的坑,left也行动开始找值补坑,找到比key大的值将其放入坑位,置为新的坑
b. 重复利用单趟排序,当left与right相遇的时候,将key放入到坑位中,key左边的全是小于key的,右边全是大于key的。
c. 划分为[begin,hole_i-1], hole_i, [hole_i+1,end],分别进行递归,类似二叉树的前序遍历
部分代码实现
int key = a[begin];
while (left < right)//单趟
{
//右边找小于key的
while (left < right && a[right] >= key)
{
--right;
}
a[hole_i] = a[right];
hole_i = right;//新坑位
//左边找大于key的
while (left < right && a[left] <= key)
{
++left;
}
a[hole_i] = a[left];
hole_i = left;//新坑位
}
a[hole_i] = key;//key的左边的值都小于key 右边都大于key
全部代码:
对区间分割,进行递归:
[begin, hole_i - 1] hole_i [hole_i + 1, end]
void QuickSort2(int* a, int begin, int end)
{
if (begin >= end) return;
int left = begin;
int right = end;
int key = a[begin];
int hole_i = begin;
while (left < right)
{
//单趟
//右边找小于key的
while (left < right && a[right] >= key)
{
--right;
}
a[hole_i] = a[right];
hole_i = right;//新坑位
//左边找大于key的
while (left < right && a[left] <= key)
{
++left;
}
a[hole_i] = a[left];
hole_i = left;//新坑位
}
a[hole_i] = key;//key的左边的值都小于key 右边都大于key
//[begin, hole_i - 1] hole_i [hole_i + 1, end]
QuickSort2(a, begin, hole_i - 1);
QuickSort2(a, hole_i + 1, end);
}
前后指针法
1、基本思想:需要两个指针,一个在前一个在后,分别用cur表示前指针,prev表示后指针,初始时,我们规定cur在prev的后一个位置,这里我们还是选择第一个数为基准值
2、过程:
a. cur位于begin+1的位置,prev位于begin位置,keyi先存放begin处的值。
b. cur不断往前+1,直到cur >= end时停止循环。
c. 如果cur处的值小于key处的值,并且prev+1 != cur,则与prev处的值进行交换。
d. 当循环结束时,将prev处的值与keyi的值相交换,并将其置为新的keyi位置。
部分代码:
int key_i = begin;
while (cur <= end)//单趟
{
if ((a[cur] < a[key_i])&&(++prev) != cur)
{
Swap(&a[cur], &a[prev]);
}
++cur;
}
Swap(&a[key_i],&a[prev]);
key_i = prev;
全部代码:
//快速排序--Pointer
void QuickSort3(int* a, int begin, int end)
{
if (begin >= end) return;
int midi = GetMid(a, begin, end);
Swap(&a[midi], &a[begin]);
int prev = begin;
int cur = prev + 1;
int key_i = begin;
while (cur <= end)
{
//单趟
if ((a[cur] < a[key_i])&&(++prev) != cur)
{
Swap(&a[cur], &a[prev]);
}
++cur;
}
Swap(&a[key_i],&a[prev]);
key_i = prev;
QuickSort3(a, begin, key_i - 1);
QuickSort3(a, key_i+1, end);
}
优化
三数取中
如果取第一个元素为key 碰到特殊情况(第一个元素为最大或最小值)会降低效率 ,因此再第一个数、最后一个个数、和数组中随机一个数,这三个的中间值做为key可以避免特殊情况。
这个是取“中值”的代码:
int GetMid(int* a, int begin, int end)
{
int mid = begin + (rand() % (end - begin));//[begin,end]中随机取值
if (a[begin] > a[end])
{
if (a[end] > a[mid])
return end;
else if (a[mid] > a[begin])
return begin;
else
return mid;
}
else
{
if (a[end] < a[mid])
return end;
else if (a[end] < a[begin])
return begin;
else
return mid;
}
}
小区间优化:
当递归到很小的区间(10个数据的区间)时 再采用递归会浪费不必要的时间和栈帧空间,因此当区间很小时 可以采用直接插入法进行排序,
//小区间用直接插入进行优化:当只有10个数以内 可以不需要递归
if (end - begin + 1 <= 10)
{
InsertSort(a + begin, end - begin + 1);
}