基本思想:所谓交换,就是根据序列中两个记录键值的比较结果来对换这两个记录在序列中的位置,交换排序的特点是:将键值较大的记录向序列的尾部移动,键值较小的记录向序列的前部移动。冒泡排序和快速排序是两个经典的交换排序
冒泡排序(Bubble Sort)也是一种简单直观的排序算法。它重复地走访过要排序的数列,一次比较两个元素,如果他们的顺序错误就把他们交换过来。走访数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。这个算法的名字由来是因为越小的元素会经由交换慢慢"浮"到数列的顶端。
动图演示:
// 冒泡排序
void BubbleSort(int* a, int n)
{
for (int i = 0; i < n; ++i)
{
// 标志位,判断过程中是否已经有序
int exchange = 0;
for (int j = 0; j < n - i - 1; ++j)
{
if (a[j] > a[j + 1])
{
Swap(&a[j], &a[j + 1]);
exchange = 1;
}
}
if (exchange == 0)
break;
}
}
在代码中,设置了一个变量exchange
来判断在第一趟冒泡排序的过程中,序列是否已经有序,如果已经有序就无需进行其余的判断操作。
冒泡排序的特性总结:
快速排序是对冒泡排序的一种改进,由 C.A.R.Hoare(Charles Antony Richard Hoare,东尼·霍尔)在 1962 年提出。
快速排序的基本思想是:任取待排序元素序列中的某元素作为基准值,按照该排序码将待排序集合分割成两子序列,左子序列中所有元素均小于基准值,右子序列中所有元素均大于基准值,然后最左右子序列重复该过程,直到所有元素都排列在相应位置上为止。
void QuickSort(int* a, int left, int right)
{
if (left >= right)
return;
// 按照基准值对array数组的 [left, right)区间中的元素进行划分
int mid = PartSort(a, left, right);
//[left, mid-1] mid [mid+1, right]
QuickSort(a, left, mid - 1);
QuickSort(a, mid + 1, right);
}
上述为快速排序递归实现的主框架,发现与二叉树前序遍历规则非常像:
将区间按照基准值划分为左右两半部分的常见方式有:hoare版本,挖坑法和前后指针法:
经过一次单趟排序,最终使得key左边的数据全部都小于key,key右边的数据全部都大于key。
参考代码:
int PartSort1(int* a, int left, int right)
{
int keyi = left;
while (left < right)
{
while (left < right && a[keyi] <= a[right])
right--;
while (left < right && a[keyi] >= a[left])
left++;
if (left < right)
Swap(&a[left], &a[right]);
}
Swap(&a[keyi], &a[left]);
return left;
}
动图演示:
基本步骤:
参考代码:
int PartSort2(int* a, int left, int right)
{
int key = a[left];
int pivot = left;
while (left < right)
{
while (left < right && a[right] > key)
right--;
a[pivot] = a[right];
pivot = right;
while (left < right && a[left] < key)
left++;
a[pivot] = a[left];
pivot = left;
}
a[pivot] = key;
return pivot;
}
动图演示:
基本步骤:
参考代码:
int PartSort3(int* a, int left, int right)
{
int keyi = left;
int prev = left;
int cur = prev + 1;
while (cur <= right)
{
// ++prev != cur 保证交换的不是同一个位置
if (a[cur] < a[keyi] && ++prev != cur)
Swap(&a[cur], &a[prev]);
cur++;
}
Swap(&a[prev], &a[keyi]);
return prev;
}
快速排序的时间复杂度是O(N*logN),这是我们假设的理想情况,每次走完一次单趟排序,以key值分割的序列的左右区间的长度是相同的,每次递归将当前序列分割成两半,一直递归下去:
但是在极端情况下,key选取到了序列中最小或者最大的数,也就是序列本身已经有序或者接近有序,那么快速排序的时间复杂度就会被降到O(N^2):
由此我们可以看出,影响快速排序效率的因素之一是单趟排序中选取的key值,key越接近于序列的中位数,快速排序的效率就越高,由此就可以得到快速排序的第一个优化三数取中:
参考代码:
// 三数取中
int GetMidIndex(int* a, int left, int right)
{
int mid = left + ((right - left) >> 1);
if (a[mid] < a[left])
{
if (a[left] < a[right])
{
return left;
}
else if (a[right] < a[mid])
{
return mid;
}
else
{
return right;
}
}
else // a[mid] > a[left]
{
if (a[mid] < a[right])
{
return mid;
}
else if (a[right] < a[left])
{
return left;
}
else
{
return right;
}
}
}
在单趟排序的最开始,先三数取中取得key值,再与最左边的值交换作为key值(以hoare版本为例):
int PartSort1(int* a, int left, int right)
{
int midi = GetMidIndex(a, left, right);
Swap(&a[midi], &a[left]);
int keyi = left;
while (left < right)
{
while (left < right && a[keyi] <= a[right])
right--;
while (left < right && a[keyi] >= a[left])
left++;
if (left < right)
Swap(&a[left], &a[right]);
}
Swap(&a[keyi], &a[left]);
return left;
}
通过我们的分析,就算快速排序在理想情况下,每次都取到中间值作为key值,每次往下递归也必须开辟上一层递归层数的两倍的函数栈帧,而栈的空间是有限的,如果遇到数据比较大的序列,有可能会导致栈溢出,所以这也是快速排序可以优化的一个点。
当快速排序向下递归,越往下递归的层数会越来越多,所以我们可以控制在下层的小区间进行优化,让小区间的数不进行递归排序,进行直接插入排序,这样就无需额外开辟过多的栈空间,可以避免栈溢出。
参考代码:
// 快速排序
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 mid = PartSort1(a, left, right);
//[left, mid-1] mid [mid+1, right]
QuickSort(a, left, mid - 1);
QuickSort(a, mid + 1, right);
}
}
为了解决快速排序在遇到海量数据的情况下,我们可以实现一个非递归的版本来解决这个问题。
参考代码:
// 快速排序非递归实现
void QuickSortNonR(int* a, int left, int right)
{
Stack st;
StackInit(&st);
StackPush(&st, left);
StackPush(&st, right);
while (!StackEmpty(&st))
{
int end = StackTop(&st);
StackPop(&st);
int begin = StackTop(&st);
StackPop(&st);
int keyi = PartSort1(a, begin, end);
//[begin, keyi - 1] keyi [key + 1, end]
if (begin <= keyi - 1) {
StackPush(&st, begin);
StackPush(&st, keyi - 1);
}
if (keyi + 1 <= end) {
StackPush(&st, keyi + 1);
StackPush(&st, end);
}
}
StackDestroy(&st);
}
快速排序的特性总结: