排序(中_对数阶)

2018年12月23日

归并排序

1,算法思想

递归法(自上而下)

  1. 申请空间,使其大小为两个已经排序序列之和,该空间用来存放合并后的序列
  2. 设定两个指针,最初位置分别为两个已经排序序列的起始位置
  3. 比较两个指针所指向的元素,选择相对小的元素放入到合并空间,并移动指针到下一位置
  4. 重复步骤3直到某一指针到达序列尾
  5. 将另一序列剩下的所有元素直接复制到合并序列尾

迭代法(自下而上)

  1. 将序列每相邻两个数字进行归并操作,形成ceil(\frac{n}{2})(向上取整)个序列,排序后每个序列包含两/一个元素
  2. 若此时序列数不是1个则将上述序列再次归并,形成ceil(\frac{n}{4})个序列,每个序列包含四/三个元素
  3. 重复步骤2,直到所有元素排序完毕,即序列数为1

2,算法图解

3,算法实现

public class Merge<AnyType extends Comparable<? super AnyType>> {
    private AnyType[] aux;

    /**
     * 自上向下
     */
    public void sort(AnyType[] a) {
        aux = (AnyType[]) new Comparable[a.length];
        sort(a, 0, a.length - 1);
    }

    public void sort(AnyType[] a, int lo, int hi) {
        if (lo >= hi) {
            return;
        }
        int mid = lo + (hi - lo) / 2;
        sort(a, lo, mid);
        sort(a, mid + 1, hi);
        /**
         * 因为左右部分各自有序
         * 则只需a[mid]>a[mid+1], 才进行排序
         */
        if (a[mid].compareTo(a[mid + 1]) > 0) {
            merge(a, lo, mid, hi);
        }
    }

    public void merge(AnyType[] a, int lo, int mid, int hi) {
        /**
         * i从左半部分开始
         * j从右半部分开始
         */
        int i = lo;
        int j = mid + 1;
        for (int k = lo; k <= hi; k++) {
            aux[k] = a[k];
        }
        /**
         * 左右两半部分各自有序
         * 若左半部分用尽,则直接取右半部分元素
         * 若右半部分用尽,则直接去左半部分元素
         * 若右半部分当前元素小于左半部分当前元素,则取右半部分元素
         * 若右半部分当前元素大于左半部分当前元素,则取左半部分元素
         */
        for (int k = lo; k <= hi; k++) {
            if (i > mid) {
                a[k] = aux[j++];
            } else if (j > hi) {
                a[k] = aux[i++];
            } else if (aux[j].compareTo(aux[i]) < 0) {
                a[k] = aux[j++];
            } else {
                a[k] = aux[i++];
            }
        }
    }

    /**
     * 自下向上
     * 比较适合用链表组织的数据
     */
    public void sortBU(AnyType[] a) {
        int N = a.length;
        aux = (AnyType[]) new Comparable[a.length];
        for (int sz = 1; sz < N; sz *= 2) {
            for (int lo = 0; lo < N - sz; lo += 2 * sz) {
                /**
                 * mid=lo+((lo+sz+sz-1)-lo)/2=lo+(sz+sz-1)/2=lo+sz-1
                 * 之所以使用Math.min, 是因为当N为奇数时的情况
                 */
                merge(a, lo, lo + sz - 1, Math.min(lo + sz + sz - 1, N - 1));
            }

        }
    }
}

采用自上而下的方法其图解:序列个数为偶数


当序列的个数为奇数时:


采用自下而上的方法其图解:当序列个数为偶数时


当序列个数为奇数时:


4,复杂度分析

  • merge方法在将两个有序数组合并为一个有序数组时,需要借助额外的存储空间,其空间复杂度为O(N)
  • merge方法在合并数组时,不改变相同元素间的前后顺序,因此,归并排序时稳定的。
    对N个数归并排序的用时等于完成两个大小为\frac{N}{2}的递归排序所用时间加上合并所用的时间,对于merge方法,其时间复杂度为O(N),当N=1时,归并排序所用时间为常数,记为1,那么有:
    T(1)=1
    T(N)=2T(\frac{N}{2})+N
    N=\frac{N}{2},再将两边乘2,那么:
    2T(\frac{N}{2})=2(2T(\frac{N}{4})+\frac{N}{2})=4T(\frac{N}{4})+N
    因此可以得到:
    T(N)=4T(\frac{N}{4})+2N
    同理,令N=\frac{N}{4},并在两边乘4,那么:
    4T(\frac{N}{4})=4(2T(\frac{N}{8})+\frac{N}{4})=8T(\frac{N}{8})+N
    又可以得到:
    T(N)=8T(\frac{N}{8})+3N
    总结规律有:
    T(N)=2^kT(\frac{N}{2^k})+kN
    其中k=logN
    最后有:
    T(N)=NT(1)+NlogN=NlogN+N
    因为归并排序的执行效率与待排序数组的有序程度无关,所以其时间复杂度是非常稳定的,因此,无论是最好、最坏还是平均情况下的时间复杂度都为O(NlogN)

快速排序

1,算法思想

快速排序使用分治法(Divide and conquer)策略来把一个序列(list)分为两个子序列(sub-lists)。

步骤为:

  1. 从数列中挑出一个元素,称为“基准”(pivot),
  2. 重新排序数列,所有比基准值小的元素摆放在基准前面,所有比基准值大的元素摆在基准后面(相同的数可以到任何一边)。在这个分割结束之后,该基准就处于数列的中间位置。这个称为分割(partition)操作。
  3. 递归地(recursively)把小于基准值元素的子数列和大于基准值元素的子数列排序。
    递归到最底部时,数列的大小是零或一,也就是已经排序好了。这个算法一定会结束,因为在每次的迭代(iteration)中,它至少会把一个元素摆到属于它的位置去。

2,算法图解

3,算法实现

public class Quick<AnyType extends Comparable<? super AnyType>> {
    public void sort(AnyType[] a) {
        sort(a, 0, a.length - 1);
    }

    private void sort(AnyType[] a, int lo, int hi) {
        if (lo >= hi) {
            return;
        }
        int j = partition(a, lo, hi);
        sort(a, lo, j - 1);
        sort(a, j + 1, hi);
    }

    private int partition(AnyType[] a, int lo, int hi) {
        int i = lo;
        int j = hi + 1;
        AnyType pivot = a[lo];
        int size = hi - lo + 1;
        if (size >= 3) {
            pivot = medianOfThree(a, lo, hi);
        }
        while (true) {
            while (a[++i].compareTo(pivot) < 0) {
                if (i == hi) {
                    break;
                }
            }
            while (pivot.compareTo(a[--j]) < 0) {
                if (j == lo) {
                    break;
                }
            }
            if (i >= j) {
                break;
            }
            swap(a, i, j);
        }
        swap(a, lo, j);
        return j;
    }

    private AnyType medianOfThree(AnyType[] a, int lo, int hi) {
        int mid = lo + (hi - lo) / 2;
        if (a[lo].compareTo(a[hi]) > 0) {
            swap(a, lo, hi);
        }
        if (a[mid].compareTo(a[hi]) > 0) {
            swap(a, mid, hi);
        }
        if (a[mid].compareTo(a[lo]) > 0) {
            swap(a, mid, lo);
        }
        return a[lo];
    }

    private void swap(AnyType[] a, int i, int j) {
        AnyType temp = a[i];
        a[i] = a[j];
        a[j] = temp;
    }
}

4,复杂度分析

  • 最好的情况最多需要 O(logN)次的嵌套递归调用,所以它需要 O(logN)的空间。最坏情况下需要 O(N)次嵌套递归调用,因此需要 O(N)的空间。
  • 如图,以3位基准,两个1前后顺序改变了,因此快速排序时不稳定的

快速排序的运行时间等于两个递归调用的运行时间加上partition方法运行时间:
T(N)=T(i)+T(N-i-1)+N
令T(0)=T(1)=1

  • 最好情况下,基准pivot正好位于中间,那么T(N)=2T(\frac{N}{2})+N,该情况与上面归并排序分析相同,那么快速排序在最好情况下的时间复杂度为O(NlogN)
  • 最坏情况下,每次选取的基准pivot为最小元素,此时i=0
    T(N)=T(N-1)+N
    T(N-1)=T(N-2)+N-1
    T(N-2)=T(N-3)+N-3
    ……
    T(2)=T(1)+2
    将两边相加后得到:
    T(N)=\sum_{i=1}^{N}i=\frac{N(N-1)}{2}
    因此,最坏情况下快速排序的时间复杂度为O(N^2)。为了避免这种极端情况,尽量避免基准pivot为最小元素,可以采用三数取中来选取pivot,即上述medianOfThree方法,该方法取头中尾三个数之间的中位数。
  • 平均情况下,选取基准大小概率为\frac{1}{N},那么T(i)的平均值为\frac{1}{N}\sum_{j=0}^{N-1}T(j),那么
    T(N)=T(i)+T(N-i-1)+N=\frac{2}{N}\sum_{j=0}^{N-1}T(j)+N
    那么:
    NT(N)=2\sum_{j=0}^{N-1}T(j)+N^2
    (N-1)T(N-1)==2\sum_{j=0}^{N-2}T(j)+(N-1)^2
    由上面式子可得:
    NT(N)-(N-1)T(N-1)=2T(N-1)+2N-1
    NT(N)=(N+1)T(N-1)+2N
    又可以得到:
    \frac{T(N)}{N+1}=\frac{T(N-1)}{N}+\frac{2}{N+1}
    \frac{T(N-1)}{N}=\frac{T(N-2)}{N-1}+\frac{2}{N}
    \frac{T(N-2)}{N-1}=\frac{T(N-3)}{N-2}+\frac{2}{N-1}
    ……
    \frac{T(2)}{3}=\frac{T(1)}{2}+\frac{2}{3}
    最终可得:
    \frac{T(N)}{N+1}=\frac{T(1)}{2}+2\sum_{i=3}^{N+1}\frac{1}{i}=ln(N+1)+\gamma-\frac{3}{2}
    其中\gamma\approx0.577叫做欧拉常数,于是
    \frac{T(N)}{N+1}=O(logN)
    T(N)=O(NlogN)
    所以快速排序的平均时间复杂度为O(NlogN)

5,应用

LeetCode 215.数组中第K个最大元素 
在未排序的数组中找到第 k 个最大的元素。
请注意,你需要找的是数组排序后的第 k 个最大的元素,而不是第 k 个不同的元素。

示例 1:
输入: [3,2,1,5,6,4] 和 k = 2
输出: 5

示例 2:
输入: [3,2,3,1,2,4,5,5,6] 和 k = 4
输出: 4

使用快速排序思想,将K左右两边分区,大的在左边,小的在右边

public class FindKthLargest {
    public static int findKthLargest(int[] nums, int k) {
        if (nums == null || nums.length == 0) {
            return Integer.MAX_VALUE;
        }
        return findKthLargest(nums, 0, nums.length - 1, k-1);
    }

    private static int findKthLargest(int[] nums, int start, int end, int k) {
        if (start > end) {
            return Integer.MAX_VALUE;
        }
       
        int pivot = nums[end];
        int left = start;
        for (int i = start; i < end; i++) {
            if (nums[i] > pivot) {
                swap(nums, left++, i);
            }
        }
        swap(nums, left, end);

        if (left == k) {
            return nums[left];
        } else if (left < k) {
            return findKthLargest(nums, left + 1, end, k);
        } else {
            return findKthLargest(nums, start, left - 1, k);
        }

    }

    private static void swap(int[] nums, int i, int j) {
        int tmp = nums[i];
        nums[i] = nums[j];
        nums[j] = tmp;
    }
}

第一次分区查找,需要对大小为N的数组进行分区,即需要遍历n个元素,平均情况下,在第二次分区查找时,需要对大小为\frac{N}{2}的数组进行分区,以此类推,分区遍历元素的个数分别为N\frac{N}{2}\frac{N}{4}……1,我们知道等比数列求和公式:
S_n=\frac{a_1(1-q^n)}{1-q}
N=2^k,那么:
N+\frac{N}{2}+\frac{N}{4}+……+1
=2^k+\frac{2^k}{2}+\frac{2^k}{4}+……+1
=2^k+2^{k-1}+2^{k-2}+……+1
=2^{k+1}-1=2N-1
所以该方法的时间复杂度为O(2N-1)=O(N)

总结

排序算法 空间复杂度 稳定性 最好时间复杂度 最坏时间复杂度 平均时间复杂度
归并 O(N) O(NlogN) O(NlogN) O(NlogN)
快速 O(logN)~O(N) × O(NlogN) O(N^2) O(NlogN)
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,732评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,496评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,264评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,807评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,806评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,675评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,029评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,683评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,704评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,666评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,773评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,413评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,016评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,978评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,204评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,083评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,503评论 2 343

推荐阅读更多精彩内容

  • 简单排序 插入排序 想象一下插队的过程... 一个人通过插队的方式排到前面,而将原本在他前面的人挤到了后面的位置。...
    Kasheem_Lew阅读 1,491评论 0 4
  • 概述 排序有内部排序和外部排序,内部排序是数据记录在内存中进行排序,而外部排序是因排序的数据很大,一次不能容纳全部...
    蚁前阅读 5,164评论 0 52
  • 稳定:如果a原本在b前面,而a=b,排序之后a仍然在b的前面;不稳定:如果a原本在b的前面,而a=b,排序之后a可...
    意识流丶阅读 3,148评论 2 9
  • 绫跑起来,四周静悄悄,空无一人的走廊荡起她的脚步声。 再也出不去了吧,她心想。 凌晨12点,教学楼里异常安静,只听...
    草莓小甜饼阅读 404评论 0 0
  • 只愿岁月太深沉阅读 114评论 0 1