有关冒泡排序,选择排序,插入排序,堆排序的总结都在上一篇文章中有介绍:
https://blog.csdn.net/weixin_42647166/article/details/104610010
(五)归并排序
“归并”的中文含义使合并、并入的意思,在数据结构中的定义是将两个或两个以上的有序表组合成一个新的有序表。即分而治之:
- Divide:将n个元素平均划分为n/2个元素。
- Conquer:递归解决两个规模为n/2的问题。
- Combine:合并排序后的两个子序列得到新的有序序列。
归并排序(Mergine Sort)利用分而治之的思想实现的排序算法,原理是假设初始序列含有n个记录,则可看成是n个有序的子序列,每个子序列长度为1,两两归并得到n/2个长度为2或1的有序子序列,再两两归并如此重复知道得到一个长度为n的有序序列,进而完成序列排序。
动画演示:
示例代码:
//二路递归将待排序列先“分”,其中low为数组左下标,high为数组右下标
void MergeSort(int array[], int low, int high) {
if (low != high) {
int mid = (low + high) / 2;
MergeSort(array, low, mid);
MergeSort(array, mid + 1, high);
Merge(array, low, mid, high);
}
}
void Merge(int array[], int low, int mid, int high) {
int i = low, j = mid + 1, k = 0; //low为第1有序区的第1个元素, mid+1为第2有序区的第1个元素
int* temp = new(nothrow) int[high - low + 1]; //暂时存放两个有序序列排序后的新序列
if (!temp) { //分配失败
cout << "error";
return;
}
while (i <= mid && j <= high) {
if (array[i] <= array[j]) //比较两个有序序列谁小谁先进临时数组temp
temp[k++] = array[i++];
else
temp[k++] = array[j++];
}
while (i <= mid)//放入第1有序区最后一个元素
temp[k++] = array[i++];
while (j <= high)//放入第2有序区最后一个元素
temp[k++] = array[j++];
for (i = low, k = 0; i <= high; i++, k++)//将排好序的存回原数组
array[i] = temp[k];
delete[]temp;//删除指针,由于指向的是数组,必须用delete []
}
性能:
归并排序需要将待排序列所有记录都扫描一遍,因此耗费的时间为O(n),由完全二叉树深度可知归并排序需要进行logn次,因此总的时间复杂度为O(nlogn),因为递归是要申请临时栈空间,因此其空间复杂度为O(n+logn).,它是一种比较占用内存,且稳定的排序算法。
上面的代码带入例子运行一遍会发现比较容易理解,但是所有带递归的函数都会有一个缺点就是当待排序列过长时会比较占用内存,因此我们可以考虑下使用非递归的方式实现归并。
优化–非递归实现:
// 第一层while循环是扩大范围2、4、8 第二层while循环是让其两个有序区进去Merge归并
void MergeSort2(int array[], int n)//其中n为数组长 对比MergeSort的形参
{
int size = 1, low, mid, high;
while (size <= n - 1) {
low = 0;
while (low + size <= n - 1) {
mid = low + size - 1;
high = mid + size;
if (high > n - 1)//第二个序列个数不足size
high = n - 1;
Merge(array, low, mid, high);//调用归并子函数
low = high + 1;//下一次归并时第一关序列的下界
}
size *= 2;//范围扩大一倍
}
}
使用非递归后,省去了递归时临时申请的空间,因此其时间复杂度为O(n),在时间上也有所提升,因此我们使用递归排序时应尽量使用非递归。
(六)快速排序
快排应该是排序算法里最经典的了,他被列为20世纪十大算法之一,在以后的实际排序中也会经常用到。
快速排序 (Quick Sort) 的基本思想是:通过一趟排序将待排记录分隔为独立的两部分,其中一部分记录的关键字均比另一部分的关键字小,则可分别对两部分记录继续进行排序,达到整个序列有序。
快排也是使用递归分而治之:
- Divide 找到一个基准值pivot可以是第一个或者最后一个数两个指针指向头尾扫描,比基准数大的放在基准数左边,比基准数小的放在基准数右边。
- Conquer 对左右划分出来的数组递归调用Divide
- Combine 因为基准的作用,使两个子数组有序。
动画演示:
示例代码:
void QuickSort(int array[], int left, int right)
{
if (left < right)
{
int low = left, high = right;
int pivot = array[left]; //设置基准值为待排序列第一个值
while (low < high)
{
while (array[high] >= pivot && low < high)
high--;
array[low] = array[high];
while (array[low] <= pivot && low < high)
low++;
array[high] = array[low];
}
array[low] = pivot;
QuickSort(array, left, low - 1); //递归排基准值左侧部分
QuickSort(array, low + 1, right); 递归排基准值右侧部分
}
}
性能分析:
快速排序的时间性能取决于递归深度,其实也就是基准值pivot的选择,在最好的情况下快排的时间复杂度为O(nlogn),最坏的情况下时间复杂度为O(n^2),平均的情况下其量级为O(nlogn)。就其空间复杂度主要是递归时申请的临时栈空间,**最好的情况空间复杂度为O(logn),最坏的情况O(n),平均情况也为O(logn)。**因为基准值的比较和交换使跳跃式的,因此快排也是一种不稳定的排序方法。
优化–三数取中法:
如果我们选取的pivot刚好处于整个序列的中间位置,这就是最好的情况,其时间复杂度和空间复杂度也是最优的,但如果不是中间的数例如我第一次就选择了一个该数组中极端数字(最大或最小)这样的话以这个极端数为标准的两边的数组就会“一边倒”这样会增加递归的次数降低算法效率,因此我们可以对基准值的选取进行优化–三数取中(median-of-three)法。
void QuickSort(int array[], int left, int right)
{
if (left < right)
{
int low = left, high = right;
dealPivot(array, left, right);
int pivot = array[left];
while (low < high)
{
while (array[high] >= pivot && low < high)
high--;
array[low] = array[high];
while (array[low] <= pivot && low < high)
low++;
array[high] = array[low];
}
array[low] = pivot;
QuickSort(array, left, low - 1);
QuickSort(array, low + 1, right);
}
}
void dealPivot(int arr[], int left, int right) {
int mid = (left + right) / 2;
if (arr[left] > arr[mid]) {
swap(arr[left], arr[mid]);
}
if (arr[left] > arr[right]) {
swap(arr[left], arr[right]);
}
if (arr[right] < arr[mid]) {
swap(arr[right], arr[mid]);
}
swap(arr[left], arr[mid]);
}
我们提取了该数组中间值,左端值,右端值,将这三个值排序取其中间值作为快排基准值pivot,从概率来说去三个数均为最小或最大的可能性很小,因此中间位数位于中间左右的值得概率提高了也就优化了快排得效率。
此外还可优化不必要的的交换、优化小数组时排序方案、优化递归操作,但都不能从本质上改变快排的不足,但也可以达到一定的优化效果。
(七)各种排序算法指标总结
没有完美的算法,只能根据实际的需要选择适合的方法。
- 从平均情况看:堆排、归并、快排要好过冒泡、选择和插入排序。
- 从最好情况看:简单的冒泡和插入反而好过复杂的排序。
- 从最坏情况看:堆排与归并又好于快排和其他简单排序。
- 从时间复杂度看:堆排和归并比较稳定,而快排就取决于你原序列的实际情况了,如果简单序列的排序还是选择简单的排序算法。
- 从空间复杂度看:归并是与原序列所占空间长度有关,快排也有相应要求,而堆排等确是少量索取,大量付出,因此在限制空间复杂度的时候应考虑空间复杂度为O(1)得算法。
- 从稳定性看:首选应该是归并在实际应用中归并也是一个好的算法。
参考书籍:大话数据结构