数据结构与算法篇 之归并排序与快速排序

今天我们来学习两种时间复杂度为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]。此趟遍历结束!

猜你喜欢

转载自blog.csdn.net/weixin_38452632/article/details/83211320