今天我们来学习两种时间复杂度为O(nlogn)的排序算法,归并排序与快速排序,这两种排序比上次讲的冒泡,选择,插入要常用的多,这两种排序比较适合与大规模的数据排序。
其中归并排序和快速排序都是用到了分治思想
归并排序的核心思想:先把一个数组分成前后两个部分,对前后两个部分进行分别排序,再将排序好的两部分合并起来,这样数组就有序了。
分治思想跟递归的思想很想,就是把大问题分成一个一个的小问题,然后小问题解决了,那么大问题也就解决了,分治是一种思想,递归是一种编程技巧
merge_sort(p...r)=merge(merge_sort(p,q),merge_sort(q+1,r)),递归的终止条件是p >= r其中q=(p+r)/2
/*
* 将一个数组中的两个相邻有序区间合并成一个
*
* 参数说明:
* a -- 包含两个有序区间的数组
* start -- 第1个有序区间的起始地址。
* mid -- 第1个有序区间的结束地址。也是第2个有序区间的起始地址。
* end -- 第2个有序区间的结束地址。
*/
void merge(int a[], int start, int mid, int end)
{
int *tmp = (int *)malloc((end-start+1)*sizeof(int)); // tmp是汇总2个有序区的临时区域
int i = start; // 第1个有序区的索引
int j = mid + 1; // 第2个有序区的索引
int k = 0; // 临时区域的索引
while(i <= mid && j <= end)
{
if (a[i] <= a[j])
tmp[k++] = a[i++];
else
tmp[k++] = a[j++];
}
while(i <= mid)
tmp[k++] = a[i++];
while(j <= end)
tmp[k++] = a[j++];
// 将排序后的元素,全部都整合到数组a中。
for (i = 0; i < k; i++)
a[start + i] = tmp[i];
free(tmp);
}
/*
* 归并排序(从上往下)
*
* 参数说明:
* a -- 待排序的数组
* start -- 数组的起始地址
* endi -- 数组的结束地址
*/
void merge_sort_up2down(int a[], int start, int end)
{
if(a==NULL || start >= end)
return ;
int mid = (end + start)/2;
merge_sort_up2down(a, start, mid); // 递归排序a[start...mid]
merge_sort_up2down(a, mid+1, end); // 递归排序a[mid+1...end]
// a[start...mid] 和 a[mid...end]是两个有序空间,
// 将它们排序成一个有序空间a[start...end]
merge(a, start, mid, end);
}
快速排序(QuickSort)也是采用了分治的思想
快速排序跟归并排序
快排的思想:在要排序的数组中下标从p到r之间的一组数组中任意选择一个分区点pivot,把小于pivot的放左边,大于pivot的放右边,继续用递归处理当p>=r的时候就是终止递归的时候
quicksort(q,r)= quicksort( q,p-1 )+quicksort( p+1,r )
快速排序并不是稳定的排序算法
快速排序的时间复杂度在最坏情况下是O(N2),平均的时间复杂度是O(N*lgN)
想达到最好的时间复杂度就是每次分区点都选择的非常好,当数组本来就是有序的时候,1,2,3,4,5,6我们再选择,那每次分区的区间都是不等的,我们需要进行大约,每次分区需要扫描n/2个元素,这时候就退化成O(n^2)
关于快速排序的优化
选择分区点是一个非常重要的事情,有两种常用的分区方法,
第一种是三数取中法,就是从区间的首,中,尾,分别取一个数进行对比大小,取三个数的中间值作为分区点,当数组比较大的时候可以采用十数取中,或者是五数取中。
第二种是随机法,,每次从排序的区间随机选择一个元素作为分区点。
快排是基于递归实现的,所以一定要避免递归过深导致堆栈溢出,第一种方法可以限制递归深度;第二种方法可以在堆上模拟实现一个函数调用栈,手动模拟递归压栈和出栈,这样就没有了系统栈大小的限制。
代码n次分区的操作,才能完成快排的整个过程实现
来自于https://www.cnblogs.com/skywang12345/p/3596746.html
/*
* 快速排序
*
* 参数说明:
* a -- 待排序的数组
* l -- 数组的左边界(例如,从起始位置开始排序,则l=0)
* r -- 数组的右边界(例如,排序截至到数组末尾,则r=a.length-1)
*/
void quick_sort(int a[], int l, int r)
{
if (l < r)
{
int i,j,x;
i = l;
j = r;
x = a[i];
while (i < j)
{
while(i < j && a[j] > x)
j--; // 从右向左找第一个小于x的数
if(i < j)
a[i++] = a[j];
while(i < j && a[i] < x)
i++; // 从左向右找第一个大于x的数
if(i < j)
a[j--] = a[i];
}
a[i] = x;
quick_sort(a, l, i-1); /* 递归调用 */
quick_sort(a, i+1, r); /* 递归调用 */
}
}
上图只是给出了第1趟快速排序的流程。在第1趟中,设置x=a[i],即x=30。
(01) 从"右 --> 左"查找小于x的数:找到满足条件的数a[j]=20,此时j=4;然后将a[j]赋值a[i],此时i=0;接着从左往右遍历。
(02) 从"左 --> 右"查找大于x的数:找到满足条件的数a[i]=40,此时i=1;然后将a[i]赋值a[j],此时j=4;接着从右往左遍历。
(03) 从"右 --> 左"查找小于x的数:找到满足条件的数a[j]=10,此时j=3;然后将a[j]赋值a[i],此时i=1;接着从左往右遍历。
(04) 从"左 --> 右"查找大于x的数:找到满足条件的数a[i]=60,此时i=2;然后将a[i]赋值a[j],此时j=3;接着从右往左遍历。
(05) 从"右 --> 左"查找小于x的数:没有找到满足条件的数。当i>=j时,停止查找;然后将x赋值给a[i]。此趟遍历结束!