数据结构与算法之美-排序(下)

分治思想

分治思想

分治,顾明思意就是分而治之,将一个大问题分解成小的子问题来解决,小的子问题解决了,大问题也就解决了。

分治与递归的区别

分治算法一般都用递归来实现的。分治是一种解决问题的处理思想,递归是一种编程技巧。

 

归并排序

算法原理

归并的思想

先把数组从中间分成前后两部分,然后对前后两部分分别进行排序,

再将排序好的两部分合并到一起,这样整个数组就有序了。

这就是归并排序的核心思想。如何用递归实现归并排序呢?

写递归代码的技巧就是分写得出递推公式,然后找到终止条件,最后将递推公式翻译成递归代码。

 

递推公式

merge_sort(p…r) = merge(merge_sort(p…q), merge_sort(q+1…r))

终止条件

p >= r 不用再继续分解

代码实现

public static void MergeSort(int[] data,int n){
    //传入数组、索引0和最后一位的索引
    Merge_c(data, 0, n - 1);
}
public static void Merge_c(int[] data,int p,int r){
    //递归终止条件:p与r相等或p大于r即细分到每个数据成员
    if (p >= r) return;
    //定义q为中间值
    int q = (p + r) / 2;
    //对q和中间值、中间值和r继续细分
    Merge_c(data,p,q);
    Merge_c(data,q+1,r);
    //直到细分到每个数据成员返回后,开始两两合并
    Merge(data,p,q,r);
}
public static void Merge(int[] data,int front,int mid,int back){
    //定义data数组中第front到mid的数组组成的数组
    int[] frontArray = new int[mid + 1];
    for (int n = front; n < frontArray.Length; n++)
        frontArray[n] = data[n];
    //定义data数组中第mid到back的数组组成的数组
    int[] backArray = new int[back - mid];
    for (int n = mid; n < backArray.Length; n++)
        backArray[n] = data[n];
    //定义临时数组,长度为数组中第front到back间的数据的长度
    int[] temp = new int[back - front + 1];
    //定义三个临时变量作为游标,分别初始化为front和mid+1,以及临时数组中的最后一个数据的位置为0
    int i = front, j = mid+1, k = 0;
    //循环直到i超过了mid或者j超过了back
    while(i<=mid&&j<=back){
        //根据大小,将data的第i/j的数据存入temp数组
        if (data[i] < data[j])
            temp[k++] = data[i++];
        else
            temp[k++] = data[j++];
    }
    //定义两个临时变量为记录起始位置,初始化为合并的两数组中的前一个数组的头尾索引
    int start = i, end = mid;
    //如果是后一个数组没有遍历完,就改为后一个数组的头尾索引
    if (j <= back){
        start = j;
        end = back;
    }
    //将未遍完的数组剩余的数据存入temp数组
    while (start <= end)
        temp[k++] = data[start++];
    //将完成排序的temp数组合并到对应的data数组位置
    for (int l = 0; l < temp.Length; l++)
        data[front + l] = temp[l];
}

性能分析

算法稳定性

归并排序是一种稳定排序算法。

时间复杂度

归并排序的时间复杂度是O(nlogn)。

空间复杂度

归并排序算法不是原地排序算法,空间复杂度是O(n)

因为归并排序的合并函数,在合并两个数组为一个有序数组时,需要借助额外的存储空间

 

快速排序

算法原理

快排的思想

如果要排序数组中下标从p到r之间的一组数据,我们选择p到r之间的任意一个数据作为pivot(分区点)。

然后遍历p到r之间的数据,将小于pivot的放到左边,将大于pivot的放到右边,将povit放到中间。

经过这一步之后,数组p到r之间的数据就分成了3部分,前面p到q-1之间都是小于povit的,中间是povit,后面的q+1到r之间是大于povit的。

根据分治、递归的处理思想,我们可以用递归排序下标从p到q-1之间的数据和下标从q+1到r之间的数据,直到区间缩小为1,就说明所有的数据都有序了。

 

递推公式

quick_sort(p…r) = quick_sort(p…q-1) + quick_sort(q+1, r)

终止条件

p >= r

代码实现

专栏写的快排拆成三个方法让人头疼,我用C#改写了群里算法大佬用c写的快排,简单明了。

public static void QuickSort(int[] data,int front,int back)
{
    //定义头尾索引、分区点
    int i = front, j = back, mid = data[front + (back - front) / 2];
    //循环到i大于j
    while (i <= j)
    {
        //头尾索引步进
        while (data[i] < mid) i++;
        while (data[j] > mid) j--;
        //i指向mid左侧,j仍指向mid右侧,就交换二者位置,使mid左侧元素比其小,右侧比其大
        if (i <= j)
        {
            int temp = data[i];
            data[i] = data[j];
            data[j] = temp;
            i++;j--;
        }
    }
    //循环结束后mid左侧是比他小的,右侧是比他大的。
    //对左右两侧递归排序,直到范围为一个数
    if (i < back) QuickSort(data,i, back);
    if (front < j) QuickSort(data,front, j);
}

性能分析

算法稳定性

快速排序是不稳定的排序算法。

时间复杂度

如果每次分区操作都能正好把数组分成大小接近相等的两个小区间,

那快排的时间复杂度递推求解公式跟归并的相同。快排的时间复杂度也是O(nlogn)。

如果数组中的元素原来已经有序了,快排的时间复杂度就是O(n^2)。

前面两种情况,一个是分区及其均衡,一个是分区极不均衡,

它们分别对应了快排的最好情况时间复杂度和最坏情况时间复杂度。

T(n)大部分情况下是O(nlogn),只有在极端情况下才是退化到O(n^2)。

空间复杂度

快排是一种原地排序算法,空间复杂度是O(1)

 

归并排序与快速排序的区别

归并排序

先递归调用,再进行合并,合并的时候进行数据的交换。所以它是自下而上的排序方式。

何为自下而上?就是先解决子问题,再解决父问题。
快速排序

先分区,在递归调用,分区的时候进行数据的交换。所以它是自上而下的排序方式。

何为自上而下?就是先解决父问题,再解决子问题。

思考

O(n)时间复杂度内求无序数组中第K大元素


有10个访问日志文件,每个日志文件大小约为300MB,每个文件里的日志都是按照时间戳从小到大排序的。现在需要将这10个较小的日志文件合并为1个日志文件,合并之后的日志仍然按照时间戳从小到大排列。如果处理上述任务的机器内存只有1GB,你有什么好的解决思路能快速地将这10个日志文件合并

posted @ 2018-11-11 12:15  田错  阅读(247)  评论(0编辑  收藏  举报