排序算法整理

今天整理几种比较喜欢的排序算法:

bubble sort

原理:每一个元素都和它后面的所有元素比较,如果有必要,则交换,以此类推,直到倒数第二个元素和最后一个元素比较并交换完毕。

这是最简单的排序法,虽然他的时间复杂度不是最小的,甚至可以说是比较大的。但是由于代码结构十分简单,所以实现起来十分容易,对于小规模数据的排序来说,bubble sort是很好的。

    /** * bubble sort * best: O(n) * worst: O(n^2) * average: O(n^2) */
    public static void bubbleSort() {

        int tmp;
        for (int i = 0; i < a.length - 1; i++) {
            for (int j = i + 1; j < a.length; j++) {
                if (a[i] > a[j]) {
                    tmp = a[i];
                    a[i] = a[j];
                    a[j] = tmp;
                }
            }
        }
    }

insertion sort

原理:首先将整个数组看成两部分,前面是已经排序完成的数组,后面是还未排序完成的。每次从未排序的数组当中选择一个(其实就是第一个啦~~方便遍历嘛),对排序好的数组进行遍历对比,找到合适的位置并插入。

插入法排序其实和冒泡法从代码上看十分相似。区别在于冒泡法是两次同向遍历,而插入法是一前一后异向遍历

此法需要一点逆向思维,虽然不如冒泡法好记忆。但也是十分简单的一种排序法。

    /** * insertion Sort * best: O(n) * worst: O(n^2) * average: O(n^2) */
    public static void insertSort() {

        int tmp;
        for (int i = 1; i < a.length; i++) {
            for (int k = i; k > 0; k--) {
                if (a[k] < a[k - 1]) {
                    tmp = a[k];
                    a[k] = a[k - 1];
                    a[k - 1] = tmp;
                }
            }
        }
    }

selection sort

原理:选择排序就更简单了,首先从全部数组中选出最小的,放在第一位,再从剩下的数组当中选出最小的,放在第二位,依此类推直到所有的数都选择完毕。

选择排序可以说是最容易理解的自然方法(就是一般人在日常生活中使用的方法)了,但是在程序当中却可以说是最慢的,因为无论何种情况下,它的时间复杂度都是O(n^2)。

    /** * Selection Sort * best: O(n^2) * worst: O(n^2) * average: O(n^2) */
    public static void selectionSort() {

        for (int i = 0; i < a.length; i++) {
            int min = a[i];//假设当前元素就是最小数
            int p = i;//记录当前元素的坐标
            for (int j = i; j < a.length; j++) {//向后遍历,找到最小数,并记录坐标
                if (a[j] < min) {
                    min = a[j];
                    p = j;
                }
            }
            //将找到的最小数与当前元素交换
            int tmp = a[p];
            a[p] = a[i];
            a[i] = tmp;
        }
    }

merge sort

原理:归并排序,将整个数列两分,然后再将子数组两份,然后再两分,一直到不可分为止,然后再将这些子数组比较组合,直到得到完整的数组。

排序算法整理_第1张图片

归并排序,就是所谓的Divide and Conquer,利用递归的优势,将整个数组”分而治之”,最后再整合起来。

这个方法应该是比较快的(当然还有更快的所谓bucket sort,不过呵呵。。。那种方法局限性太大了),就是对编程的要求略高。

    /** * Merge Sort * best: O(n*logn) * worst: O(n*logn) * average: O(n*logn) */
    public static void mergeSort() {

        mergeSort(0, a.length - 1);
    }

    private static void mergeSort(int first, int last) {

        if (first < last) {
            int mid = (first + last) / 2;
            mergeSort(first, mid);//递归左边数组
            mergeSort(mid + 1, last);//递归右边数组
            merge(first, mid, last);//组合当前的数组
        }
    }

    private static void merge(int first, int mid, int last) {

        int[] tmp = new int[last - first + 1];
        int ll = first;
        int rl = mid + 1;
        int k = 0;

        //两个都不为空
        while (ll <= mid && rl <= last) {
            tmp[k++] = (a[ll] < a[rl]) ? a[ll++] : a[rl++];
        }

        //左边不空
        while (ll <= mid) {
            tmp[k++] = a[ll++];
        }

        //右边不空
        while (rl <= last) {
            tmp[k++] = a[rl++];
        }

        for (int i = 0; i < k; i++)
            a[first + i] = tmp[i];

    }

quick sort

原理:先选定一个中心数pivot,随便选,可以是第一个,也可以是最后一个,甚至可以是中间随便一个,然后遍历整个数组,将大于pivot的数放在右边,小于pivot的数放在左边,然后再分别递归左右两边的子数组。

这个算法也是采用分治的思想(Divid and Conquer),基本上采用分治思想的算法,时间复杂度都和n*logn有关,所以效率都还行~~~~

这个算法其实好理解,难点应该在代码的实现上,如果你没有记住代码的话,重新编写可能会遇到两个问题

  • 数组的坐标要搞清楚,因为是直接在原数组上操作,一步错,步步错。
  • 怎么样将数组分成两段也是难点,我的思路是,遍历整个数组,将所有小于pivot的数都弄到数组前半段,并记录第一个大于pivot数的坐标s。那么一旦发现有新的小数,则可以直接和位于s的那个数交换,并重新记录,这样一来,就保证被记录的坐标之间的所有数都小于pivot,最后再把pivot换到s位置。实现了数组的分段。
    public static void quickSort() {

        quickSort(0, a.length - 1);
    }

    private static synchronized void quickSort(int first, int last) {

        if (first < last) {
            int pivot = partition(first, last);
            //这里要注意,递归的时候需要绕过已经选出来了的中心数pivot
            quickSort(first, pivot - 1);
            quickSort(pivot + 1, last);
        }
    }

    private static int partition(int first, int last) {

        int s = first;
        int tmp;

        for (int l = first; l < last; l++) {
            if (a[l] < a[last]) {
                tmp = a[l];
                a[l] = a[s];
                a[s] = tmp;
                s++;
            }

        }
        tmp = a[last];
        a[last] = a[s];
        a[s] = tmp;

        return s;
    }

heap sort

原理:

  1. 首先将整个数组重新排列成符合最大堆(或者最小堆)结构的数。
  2. 既然这个数组符合堆结构,那么它的根元素必然是最大或者最小的,这时我们可以取出这个根元素,就可以得到整个数列的最大值或者最小值了。
  3. 剩下的元素再重复刚才的步骤,直到所有的元素都取出,得到排列好的数组。

heap sort放在最后面讲,其实是因为它最难以理解,无论从代码实现还是理论知识方面都是。但是它的效率又相当优秀,所以不得不学习。。。

学习heap sort(堆排序)之前,首先要理解binary tree(二叉树)的概念,然后再理解binary heap(二叉堆)。如果懒得看wiki也没有关系,简单来说,所谓的二叉堆就是下面这个样子。

上图是一个最大二叉堆,它有三个特性,我归纳如下:

  1. 每个结点最多两个子结点。
  2. 越上面越大(或者越小)。
  3. 除了最下面,其余的结点必须长满!

理解二叉堆的概念以后,我们就可以知道,如果一个数组的排列符合二叉堆的结构,我们就可以轻易的得到它的最大值或是最小值,所谓排序,就是不断取出这些最值的过程

所以说堆排序的主要目的,其实就是把目标数组弄成二叉堆。

说起来简单,但是做起来却不容易,第一个难点,就是一般人很难将上图的二叉堆,和代码里面的数组联系起来,也就是说无法理解,这么一棵二维的树,怎么能放进一维的数组里面呢?

先说方法:

将二叉堆放入数组

如图所示,根结点放在0位,左儿子放在1位,右儿子放在2位,以此类推,左儿子的左儿子放在3位,左儿子的右儿子放在4位。

直观点儿来讲,就是把二叉堆,从上到下,从左到右依次放入数组就行了。。。

但是这还是按照”人“的思路来操作的,那么如何在代码中实现呢?或者说,数学规律是什么呢?

总的来说,就是对于第 n 个结点,它的左儿子,放在 2n+1 位,右儿子则放在 2n+2 了。

反过来讲,对于一个数组来说,无论它里面的元素如何排列,只要我们按照上面的规律来访问它,那么它就可以看成是一个二叉树

我至今还对想出这个方法的前辈表示无尽的膜拜 Orz。思路怎么来的,我就不讲了,数学证明什么的,你也不要想了。。。因为我也说不清楚,我只能拾人牙慧,直接讲解决方法了。

找到了这个规律,我们就可以把一个数组当成是binary tree(二叉树)来操作,并把它弄成binary heap(二叉堆)。

如何在代码中实现二叉树到二叉堆的转换呢?

有两种方式:从上往下,或者从下往上,本文只介绍从下往上的方式。

以最大二叉堆为例

通过二叉堆的特性我们可以看出,如果把它放入数组当中,那么最后一个元素必定是叶子,而这个叶子的根,也应该是整个二叉堆当中的最后一个拥有叶子的结点,在它之前的所有结点,都应该拥有叶子!

如果我们从这个结点开始,遍历之前所有的结点,那么我们实际上就对所有的元素进行了操作。

根据数学规律,假设最后一个元素的坐标是 n=2i+1 n=2i+2 ,那么最后一个拥有叶子的结点的坐标就是 i=n/21 ,因为无论 n 是基数还是偶数,对于对于代码来说都一样,因为 n/2 是向下取整的。

知道了从哪里开始,还要知道怎么做。

我们需要做的就是保证以该结点为根,它下面所有的结点所组成的二叉树,是二叉堆。

我们先找出结点所对应的两片叶子当中较大的那片,再与结点比较,如果结点比该叶子大,则表明这是二叉堆,退出操作,如果结点比该叶子小,则交换结点与叶子,交换以后,因为叶子变成了较小的数,所以它可能比它的叶子还要小。所以如果这片叶子向下还有叶子,则应该将这片叶子看成结点,循环执行上面的操作,如果这片叶子下面没有叶子,则程退出循环。

moveDown流程图:

Created with Raphaël 2.1.0 开始 是否有叶子? 找出最大叶子 比结点大? 交换结点与叶子 结束 yes no yes no

moveDown函数代码:

    private static void moveDown(int i, int n) {

        int lc = 2 * i + 1;
        while (lc < n) {
            if (lc < n - 1 && a[lc] < a[lc + 1]) {
                lc++;
            }
            if (a[i] < a[lc]) {
                int tmp = a[i];
                a[i] = a[lc];
                a[lc] = tmp;

                i = lc;
                lc = 2 * i + 1;
            }
            else {
                lc = n;
            }
        }
    }

这里需要说明的是,如果是从上往下的做法,这个函数并不适用,因为它无法完全遍历整个二叉树,故而它没办法保证整个二叉树就是二叉堆,它只能保证,有交换动作的那一个分支符合二叉堆的特性,但由于我们是从下往上操作,在所有下层的数据率先符合二叉堆的前提下,这种不完全的遍历反而大大的提高了执行效率

主函数分成两步:

  1. 从下往上遍历所有的根,完成最大堆的初始化。
  2. 将根元素和最后一个叶元素交换,此时,整个数组的最后一个元素可以确定就是最大值,而前面N-1个元素当中,除了根元素(第1个元素),其余的元素都符合二叉堆的结构,所以我们只需要再执行一次moveDown操作,让根元素融入到整个二叉堆当中,就可以了。
    public static void heapSort() {

        for (int i = a.length / 2 - 1; i >= 0; i--) {
            moveDown(i, a.length);
        }

        for (int i = a.length - 1; i >= 1; i--) {
            int tmp = a[i];
            a[i] = a[0];
            a[0] = tmp;
            moveDown(0, i);
        }
    }

最后贴出完整的代码如下:

    public static void heapSort() {

        for (int i = a.length / 2 - 1; i >= 0; i--) {
            moveDown(i, a.length);
        }

        for (int i = a.length - 1; i >= 1; i--) {
            int tmp = a[i];
            a[i] = a[0];
            a[0] = tmp;
            moveDown(0, i);
        }
    }

        private static void moveDown(int i, int n) {

        int lc = 2 * i + 1;
        while (lc < n) {
            if (lc < n - 1 && a[lc] < a[lc + 1]) {
                lc++;
            }
            if (a[i] < a[lc]) {
                int tmp = a[i];
                a[i] = a[lc];
                a[lc] = tmp;

                i = lc;
                lc = 2 * i + 1;
            }
            else {
                lc = n;
            }
        }
    }

关于时间复杂度的计算

计算步骤放这里方便以后复习用~~

以merge sort为例:

假设数组的长度是 2k ,分治法会将这个数组一分为二,每段数组长度就是 2k1

然后再将这两个数组组合起来,以上面的程序为例,组合的函数就是

    private static void merge(int first, int mid, int last) {

        int[] tmp = new int[last - first + 1];
        int ll = first;
        int rl = mid + 1;
        int k = 0;

        //两个都不为空
        while (ll <= mid && rl <= last) {
            tmp[k++] = (a[ll] < a[rl]) ? a[ll++] : a[rl++];
        }

        //左边不空
        while (ll <= mid) {
            tmp[k++] = a[ll++];
        }

        //右边不空
        while (rl <= last) {
            tmp[k++] = a[rl++];
        }

        for (int i = 0; i < k; i++)
            a[first + i] = tmp[i];

    }

中,赋值有4步,比较有n次,最后把临时数组放入原数组又用了n次。那么一共有 2n+4 步。
这样一来,得出递推公式:

n=2k,i<k,iN

T(n)=2T(n2)+2n+4=2T(n2)+4(n2+1)=...

T(n)=2iT(n2i)+i2n+8(2i1)

i=k

T(n)=2kT(1)+k2n+8(2k1)

因为 T(1) 时,数组不需要操作,故 T(1)=0

T(n)=k2n+8(2k1)

又因为 2k=nk=logn

T(n)=2nlogn+8(n1)

O(n)=nlogn

所以时间复杂度就是 O(nlogn) !!!!!

你可能感兴趣的:(排序算法整理)