1. 快速排序
1.1 算法思想
快速排序是Hoare于1962年提出的一种二叉树结构的交换排序方法,其基本思想为:任取待排序元素序列中的某元素作为基准值,按照该排序码将待排序集合分割成两子序列,左子序列中所有元素均小于基准值,右子序列中所有元素均大于基准值,然后最左右子序列重复该过程,直到所有元素都排列在相应位置上为止。
1.2 算法实现
快速排序的实现方式有很多,我们主要介绍三种方法。
1.2.1 Hoare 法
利用两个变量left,right分别指向数组的起始位置与末尾位置。并且以数组第一个元素作为key值。
right先从右往左依次遍历找到比key小的数,left从左往右依次遍历找到比key大的数。然后交换left与right下标对应的值。重复步骤2直至right>=left。
之后交换key与left或者right对应的值,并且把该位置记为mid。
最后划分区间[left,mid-1]与[mid+1,right]继续重复1,2步骤。直至不能划分。

思考:为什么最后相遇位置一定小于或等于 key 值?
我们知道right与left
相遇无非两种情况:
情况一:right停住,left移动与right相遇·。因为right一直再找比key小的值,所以right停下位置一定比key小,相遇位置也一定比key小。
情况二:left停住,right移动与left相遇·。此时又分为两种情况:
left从未移动,右侧数据都比key大,相遇位置就是key,交换不变。
left移动过至少一次,也就是至少交换过一次,此时left停留位置的值是上一轮right所对应的值,又因为right一直在找比key小的值,所以相遇位置也一定比key小。
代码实现:
void swap(int* p1, int* p2)
{
int tmp = *p1;
*p1 = *p2;
*p2 = tmp;
}
int PartSort1(int* arr, int begin, int end)
{
int left = begin, right = end;
int keyi = begin;
while (left < right)
{
//left<right防止越界
//使用>=而不是>防止数据出现死循环
while (left<right && arr[right]>=arr[keyi])
//寻找比key小的值
{
right--;
}
while (left < right && arr[left] <= arr[keyi])
//寻找比key大的值
{
left++;
}
swap(&arr[left], &arr[right]);
}
int mid = left;
swap(&arr[keyi], &arr[mid]);
return mid;
}
void QuickSort(int* arr, int left, int right)
{
if (left >= right)//不能划分
{
return;
}
int mid = PartSort1(arr, left, right);
QuickSort(arr, left, mid - 1);//左区间
QuickSort(arr, mid+1, right);//右区间
}
1.2.2 挖坑法
先将起始位置key值设为坑,之后right从右往左找比key值小的值,找到之后放入坑位,此时right就形成新的坑。然后left从左往右找比key大的值, 找到之后放入坑位,此时left就又形成新的坑。
最后left与right相遇,将key放入最后一个坑,并将该位置记为mid,。·
最后划分区间[left,mid-1]与[mid+1,right]继续重复1,2步骤。直至不能划分。

代码实现:
int PartSort2(int* arr, int begin, int end)
{
int left = begin, right = end;
int hole = begin;//记录坑位
int key = arr[left];
while (left < right)
{
//left<right防止越界
//使用>=而不是>防止数据出现死循环
while (left < right && arr[right] >= key)
//寻找比key小的值
{
right--;
}
arr[hole] = arr[right];
hole = right;
while (left < right && arr[left] <= key)
//寻找比key大的值
{
left++;
}
arr[hole] = arr[left];
hole = left;
}
arr[hole] = key;
return hole;
}
void QuickSort(int* arr, int left, int right)
{
if (left >= right)//不能划分
{
return;
}
int mid = PartSort2(arr, left, right);
QuickSort(arr, left, mid - 1);//左区间
QuickSort(arr, mid+1, right);//右区间
}
1.2.3 双指针法
先定义一个prev指向数组首元素,然后定义一个cur指向第二个位置。
cur从左往右依次遍历找key小的值,找到之后++prev,然后交换prev与cur指向的值。之后cur++继续遍历。(key为起始位置的值)
当cur遍历完之后,此时交换prev指向的值与key。将此时位置记为mid。
最后划分区间[left,mid-1]与[mid+1,right]继续重复1,2,3步骤。直至不能划分。

代码实现:
int PartSort3(int* arr, int begin, int end)
{
int prev = begin;
int cur = begin + 1;
int keyi = begin;
while (cur <= end)
{
if (arr[cur] < arr[keyi])//小于则交换
{
swap(&arr[++prev], &arr[cur]);
}
cur++;
}
swap(&arr[prev], &arr[keyi]);
return prev;
}
void QuickSort(int* arr, int left, int right)
{
if (left >= right)//不能划分
{
return;
}
int mid = PartSort3(arr, left, right);
QuickSort(arr, left, mid - 1);//左区间
QuickSort(arr, mid+1, right);//右区间
}
1.3 算法优化
1.3.1 改变基准元素
当数组有序时,我们再对其进行快速排序,其时间复杂度讲话劣化为O(N2)。
这时候我们为了防止这种现象,可以选择提前改变基准元素 key。
① 三数取中:即取出数组首尾以及中间元素,选取数值位于中间的元素作为准元素 key。
int GetMidNum(int*arr, int left, int right)
{
int mid = (left + right) >> 1;
if (arr[mid] > arr[left])
{
if (arr[mid] < arr[right])
{ //left mid right
return mid;
}
else if (arr[left] > arr[right])
{ //right left mid
return left;
}
else
{ //left right mid
return right;
}
}
}
int PartSort3(int* arr, int begin, int end)
{
int prev = begin;
int cur = begin + 1;
int keyi = begin;
int mid=GetMidNum(arr, begin, end);
swap(&arr[begin], &arr[mid]);
while (cur <= end)
{
if (arr[cur] < arr[keyi])//小于则交换
{
swap(&arr[++prev], &arr[cur]);
}
cur++;
}
swap(&arr[prev], &arr[keyi]);
return prev;
}
② 随机数取中:三数取中有时候也并不能保证基准元素的准确性,这时候我们最好使用随机数获取基准值。
int GetRanNum(int*arr, int left, int right)
{
srand(time(0));//生成随机种子
int mid = rand() % (right - left) + left;//随机数
return mid;
}
1.3.2 区间优化
我们进行递归调用时,递归越深递归调用的次数就会越多,为了优化这个问题,我们可以当区间较小时采用其他排序,如插入排序。
void QuickSort(int* arr, int left, int right)
{
if (left >= right)//不能划分
return;
if ((right - left + 1) < 10)//小区间优化
{
InsertSort(arr+left, right - left + 1);
return ;
}
int mid = PartSort3(arr, left, right);
QuickSort(arr, left, mid - 1);//左区间
QuickSort(arr, mid+1, right);//右区间
}
2. 归并排序
2.1 算法思想
归并排序(Merge Sort)是建立在归并操作上的一种有效的排序算法, 该算法是采用分治法的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。
2.2 算法步骤
创建一个与待排序数组同等大小的tmp数组。
然后将待排序数组分为两个子数组,让两个子数组有序。为了让这两个子数组有序,我们又要将每个子数组分为两个子数组,让其有序。
当子数组没有元素或者只有一个元素时,我们可以认为其有序,然后将两个子数组开始归并。
归并时因为两个子数组有序,我们可以定义两个指针begin1,begin2分别指向两个数组起始位置。然后遍历比较arr[begin1]与arr[begin2],取较小的元素尾插进tmp数组。
最后tmp数组数据拷贝回原数组。
2.3 动画演示

2.4 代码实现
void _MergeSort(int* arr, int begin, int end, int* tmp)
{
if (begin >= end)
return;
int mid = (begin + end) >> 1;
_MergeSort(arr, begin, mid, tmp);//归并左区间
_MergeSort(arr, mid+1, end, tmp);//归并右区间
int i = begin;
int begin1 = begin, end1 = mid;
int begin2 = mid + 1, end2 = end;
while (begin1 <= end1 && begin2 <= end2)
{
if (arr[begin1] < arr[begin2])
{
tmp[i++] = arr[begin1++];
}
else
{
tmp[i++] = arr[begin2++];
}
}
//若是还有区间存在数据
while (begin1 <= end1)
{
tmp[i++] = arr[begin1++];
}
while (begin2 <= end2)
{
tmp[i++] = arr[begin2++];
}
//最后将归并完后后的数据拷贝回原数组
memcpy(arr + begin, tmp + begin, sizeof(int) * (end - begin + 1));
}
void MergeSort(int* arr, int n)
{
int* tmp = (int*)malloc(sizeof(int) * n);
if (tmp == NULL)
{
perror("malloc fail:");
return;
}
_MergeSort(arr, 0, n - 1, tmp);
free(tmp);
tmp = NULL;
}
2.5 算法优化
2.5.1 区间优化
当递归调用层数越多时,最后三层的递归调用会浪费大量时间。为了避免这种情况,这时我们就可以采用小区间使用插入排序的方法。
void _MergeSort(int* arr, int begin, int end, int* tmp)
{
if (begin >= end)
return;
if (end - begin + 1 < 10)//小区间优化
{
InsertSort(arr + begin, end - begin + 1);
return;
}
int mid = (begin + end) >> 1;
_MergeSort(arr, begin, mid, tmp);//归并左区间
_MergeSort(arr, mid+1, end, tmp);//归并右区间
int i = begin;
int begin1 = begin, end1 = mid;
int begin2 = mid + 1, end2 = end;
while (begin1 <= end1 && begin2 <= end2)
{
if (arr[begin1] < arr[begin2])
{
tmp[i++] = arr[begin1++];
}
else
{
tmp[i++] = arr[begin2++];
}
}
//若是还有区间存在数据
while (begin1 <= end1)
{
tmp[i++] = arr[begin1++];
}
while (begin2 <= end2)
{
tmp[i++] = arr[begin2++];
}
//最后将归并完后后的数据拷贝回原数组
memcpy(arr + begin, tmp + begin, sizeof(int) * (end - begin + 1));
}
2.5.2 判断区间有序
在归并排序合并时,如果两个区间是有序,即 arr[end1] <= arr[begin2] 时就不需要对其进行归并。
void _MergeSort(int* arr, int begin, int end, int* tmp)
{
if (begin >= end)
return;
int mid = (begin + end) >> 1;
_MergeSort(arr, begin, mid, tmp);//归并左区间
_MergeSort(arr, mid+1, end, tmp);//归并右区间
int i = begin;
int begin1 = begin, end1 = mid;
int begin2 = mid + 1, end2 = end;
if (arr[begin2] < arr[end1])//区间有序则不合并
{
while (begin1 <= end1 && begin2 <= end2)
{
if (arr[begin1] < arr[begin2])
{
tmp[i++] = arr[begin1++];
}
else
{
tmp[i++] = arr[begin2++];
}
}
//若是还有区间存在数据
while (begin1 <= end1)
{
tmp[i++] = arr[begin1++];
}
while (begin2 <= end2)
{
tmp[i++] = arr[begin2++];
}
//最后将归并完后后的数据拷贝回原数组
memcpy(arr + begin, tmp + begin, sizeof(int) * (end - begin + 1));
}
}