面试中的 10 大排序算法总结

点击查看原文html

前言

查找和排序算法是算法的入门知识,其经典思想能够用于不少算法当中。由于其实现代码较短,应用较常见。因此在面试中常常会问到排序算法及其相关的问题。但万变不离其宗,只要熟悉了思想,灵活运用也不是难事。通常在面试中最常考的是快速排序和归并排序,而且常常有面试官要求现场写出这两种排序的代码。对这两种排序的代码必定要信手拈来才行。还有插入排序、冒泡排序、堆排序、基数排序、桶排序等。面试官对于这些排序可能会要求比较各自的优劣、各类算法的思想及其使用场景。还有要会分析算法的时间和空间复杂度。一般查找和排序算法的考察是面试的开始,若是这些问题回答很差,估计面试官都没有继续面试下去的兴趣都没了。因此想开个好头就要把常见的排序算法思想及其特色要熟练掌握,有必要时要熟练写出代码。java

接下来咱们就分析一下常见的排序算法及其使用场景。限于篇幅,某些算法的详细演示和图示请自行寻找详细的参考。面试

冒泡排序

冒泡排序是最简单的排序之一了,其大致思想就是经过与相邻元素的比较和交换来把小的数交换到最前面。这个过程相似于水泡向上升同样,所以而得名。举个栗子,对5,3,8,6,4这个无序序列进行冒泡排序。首先从后向前冒泡,4和6比较,把4交换到前面,序列变成5,3,8,4,6。同理4和8交换,变成5,3,4,8,6,3和4无需交换。5和3交换,变成3,5,4,8,6,3.这样一次冒泡就完了,把最小的数3排到最前面了。对剩下的序列依次冒泡就会获得一个有序序列。冒泡排序的时间复杂度为O(n^2)。算法

实现代码:shell

public class SelectSort {
    public static void selectSort(int[] arr) {
        if (arr == null || arr.length == 0)
            return;
        int minIndex = 0;
        for (int i = 0; i < arr.length - 1; i++) { //只须要比较n-1次
            minIndex = i;
            for (int j = i + 1; j < arr.length; j++) { //从i+1开始比较,由于minIndex默认为i了,就不必比了
                if (arr[j] < arr[minIndex]) {
                    minIndex = j;
                    if (minIndex == i) { //若是minIndex不为i,说明找到了更小的筒,交换之。
                        swap(arr, i, minIndex);
                    }
                }
            }
        }
    }

    public static void swap(int[] arr, int i, int j) {
        int temp = arr[i];
        arr[i] = arr[j];
        arr[j] = temp;
    }
}

插入排序

插入排序不是经过交换位置而是经过比较找到合适的位置插入元素来达到排序的目的的。相信你们都有过打扑克牌的经历,特别是牌数较大的。在分牌时可能要整理本身的牌,牌多的时候怎么整理呢?就是拿到一张牌,找到一个合适的位置插入。这个原理其实和插入排序是同样的。举个栗子,对5,3,8,6,4这个无序序列进行简单插入排序,首先假设第一个数的位置时正确的,想一下在拿到第一张牌的时候,不必整理。而后3要插到5前面,把5后移一位,变成3,5,8,6,4.想一下整理牌的时候应该也是这样吧。而后8不用动,6插在8前面,8后移一位,4插在5前面,从5开始都向后移一位。注意在插入一个数的时候要保证这个数前面的数已经有序。简单插入排序的时间复杂度也是O(n^2)。数组

实现代码:数据结构

public class InsertSort {
    public static void insertSort(int[] arr) {
        if (arr == null || arr.length == 0)
            return;

        for (int i = 1; i < arr.length; i++) { //假设第一个数位置是正确的;要日后移·必需要假设第一个
            int j = i;
            int target = arr[i]; //待插入的

            //后移
            while (j > 0 && target < arr[j - 1]) {
                arr[j] = arr[j - 1];
                j--;
            }
            //插入
            arr[j] = target;
        }
    }
}

快速排序

快速排序一听名字就以为很高端,在实际应用当中快速排序确实也是表现最好的排序算法。冒泡排序虽然高端,但其实其思想是来自冒泡排序,冒泡排序是经过相邻元素的比较和交换把最小的冒泡到最顶端,而快速排序是比较和交换小数和大数,这样一来不只把小数冒泡到上面同时也把大数沉到下面。函数

举个栗子:对5,3,8,6,4这个无序序列进行快速排序,思路是右指针找比基准数小的,左指针找比基准数大的,交换之。性能

5,3,8,6,4 用5做为比较的基准,最终会把5小的移动到5的左边,比5大的移动到5的右边。大数据

5,3,8,6,4 首先设置i,j两个指针分别指向两端,j指针先扫描(思考一下为何?)4比5小中止。而后i扫描,8比5大中止。交换i,j位置。

5,3,4,6,8 而后j指针再扫描,这时j扫描4时两指针相遇。中止。而后交换4和基准数。

4,3,5,6,8 一次划分后达到了左边比5小,右边比5大的目的。以后对左右子序列递归排序,最终获得有序序列。

上面留下来了一个问题为何必定要j指针先动呢?首先这也不是绝对的,这取决于基准数的位置,由于在最后两个指针相遇的时候,要交换基准数到相遇的位置。通常选取第一个数做为基准数,那么就是在左边,因此最后相遇的数要和基准数交换,那么相遇的数必定要比基准数小。因此j指针先移动才能先找到比基准数小的数。

快速排序是不稳定的,其时间平均时间复杂度是O(nlgn)。

实现代码:

public class QuickSort {
    public static void sort(int[] arr) {
        if (arr == null || arr.length == 0)
            return;
        quickSort(arr, 0, arr.length - 1);
    }

    public static void quickSort(int[] arr, int left, int right) {
        if (left >= right)
            return;
        int pivotPos = partition(arr, left, right);
        quickSort(arr, left, pivotPos - 1);
        quickSort(arr, pivotPos + 1, right);
    }
    
    //一次划分
    public static int partition(int[] arr, int left, int right) {
        int pivotKey = arr[left];
        int pivotPointer = left;

        while (left < right) {
            while (left < right && arr[right] >= pivotKey)
                right--;
            while (left < right && arr[left] <= pivotKey)
                left++;
            swap(arr, left, right); //把大的交换到右边,把小的交换到左边。
        }
        swap(arr, pivotPointer, left); //最后把pivot交换到中间
        return left;
    }

    public static void swap(int[] arr, int left, int right) {
        int temp = arr[left];
        arr[left] = arr[right];
        arr[right] = temp;
    }
}

其实上面的代码还能够再优化,上面代码中基准数已经在pivotKey中保存了,因此不须要每次交换都设置一个temp变量,在交换左右指针的时候只须要前后覆盖就能够了。这样既能减小空间的使用还能下降赋值运算的次数。优化代码以下:

public static int partition(int[] arr, int left, int right) {
    int pivotKey = arr[left];
    int pivotPointer = left;

    while (left < right) {
        while (left < right && arr[right] >= pivotKey)
            right--;
        // 去除swap方法
        arr[left] = arr[right]; //小的移动到左边
        while (left < right && arr[left] <= pivotKey)
            left++;
        arr[right] = arr[left]; //大的移动到右边
    }
    arr[left] = pivotKey;
    return left;
}

总结快速排序的思想:冒泡+二分+递归分治,慢慢体会。。。

堆排序

堆排序是借助堆来实现的选择排序,思想同简单的选择排序,如下以大顶堆为例。注意:若是想升序排序就使用大顶堆,反之使用小顶堆。缘由是堆顶元素须要交换到序列尾部。

首先,实现堆排序须要解决两个问题:

\1. 如何由一个无序序列键成一个堆?

\2. 如何在输出堆顶元素以后,调整剩余元素成为一个新的堆?

第一个问题,能够直接使用线性数组来表示一个堆,由初始的无序序列建成一个堆就须要自底向上从第一个非叶元素开始挨个调整成一个堆。

第二个问题,怎么调整成堆?首先是将堆顶元素和最后一个元素交换。而后比较当前堆顶元素的左右孩子节点,由于除了当前的堆顶元素,左右孩子堆均知足条件,这时须要选择当前堆顶元素与左右孩子节点的较大者(大顶堆)交换,直至叶子节点。咱们称这个自堆顶自叶子的调整成为筛选。

从一个无序序列建堆的过程就是一个反复筛选的过程。若将此序列当作是一个彻底二叉树,则最后一个非终端节点是n/2取底个元素,由此筛选便可。举个栗子:

49,38,65,97,76,13,27,49序列的堆排序建初始堆和调整的过程以下:

实现代码:

/**
 * 堆排序算法的突現,以大顶堆为例
 */
public class HeapSort {
    /**
     * 堆筛选,除了start以外,start-end均満足大顶堆的定义,凋整以后start-end称为一个大顶堆。
     *
     * @param arr   待凋整数組
     * @param start 起始指針
     * @param end   結束指針
     */
    public static void heapAdjust(int[] arr, int start, int end) {
        int temp = arr[start];

        for (int i = 2 * start + 1; i <= end; i *= 2) {
            //左右孩子的节点分別内2*i+1,2*i+2

            //选择出左右孩子较小的下标
            if (i < end && arr[i] < arr[i + 1]) {
                i++;
            }

            if (temp >= arr[i]) {
                break; //已经为大顶堆,保持穏定性。
            }
            arr[start] = arr[i]; //将子节点上移
            start = i; //下一轮篩迭
        }
        arr[start] = temp; //插入正确的位置
    }

    public static void heapSortl(int[] arr) {
        if (arr == null || arr.length == 0) {
            return;
        }

        //创建大顶堆
        for (int i = arr.length / 2; i >= 0; i--) {
            heapAdjust(arr, i, arr.length - 1);
        }

        for (int i = arr.length - 1; i >= 0; i--) {
            swap(arr, 0, i);
            heapAdjust(arr, 0, i - 1);
        }
    }

    public static void swap(int[] arr, int i, int j) {
        int temp = arr[i];
        arr[i] = arr[j];
        arr[j] = temp;
    }
}

希尔排序

希尔排序是插入排序的一种高效率的实现,也叫缩小增量排序。简单的插入排序中,若是待排序列是正序时,时间复杂度是O(n),若是序列是基本有序的,使用直接插入排序效率就很是高。希尔排序就利用了这个特色。基本思想是:先将整个待排记录序列分割成为若干子序列分别进行直接插入排序,待整个序列中的记录基本有序时再对全体记录进行一次直接插入排序。

举个栗子:

从上述排序过程可见,希尔排序的特色是,子序列的构成不是简单的逐段分割,而是将某个相隔某个增量的记录组成一个子序列。如上面的例子,第一堂排序时的增量为5,第二趟排序的增量为3。因为前两趟的插入排序中记录的关键字是和同一子序列中的前一个记录的关键字进行比较,所以关键字较小的记录就不是一步一步地向前挪动,而是跳跃式地往前移,从而使得进行最后一趟排序时,整个序列已经作到基本有序,只要做记录的少许比较和移动便可。所以希尔排序的效率要比直接插入排序高。

希尔排序的分析是复杂的,时间复杂度是所取增量的函数,这涉及一些数学上的难题。可是在大量实验的基础上推出当n在某个范围内时,时间复杂度能够达到O(n^1.3)。

实现代码:

public class ShellSort {
    /**
     * 希尔排序的一趟插入
     *
     * @param arr 待排数组
     * @param d   增量
     */
    public static void shellInsert(int[] arr, int d) {
        for (int i = d; i < arr.length; i++) {
            int j = i - d;
            int temp = arr[i];
            while (j >= 0 && arr[j] > temp) { //从后向前, 找到此其小的数的位置
                arr[j + d] = arr[j];  //向后挪动j-=d;

                if (j != i - d)  //存在比其小的数
                    arr[j + d] = temp;

            }
        }
    }

    public static void shellSort(int[] arr) {
        if (arr == null || arr.length == 0)
            return;
        int d = arr.length / 2;
        while (d >= 1) {
            shellInsert(arr, d);
            d /= 2;
        }
    }
}

归并排序

归并排序是另外一种不一样的排序方法,由于归并排序使用了递归分治的思想,因此理解起来比较容易。其基本思想是,先递归划分子问题,而后合并结果。把待排序列当作由两个有序的子序列,而后合并两个子序列,而后把子序列当作由两个有序序列。。。。。倒着来看,其实就是先两两合并,而后四四合并。。。最终造成有序序列。空间复杂度为O(n),时间复杂度为O(nlogn)。

举个栗子:

实现代码:

public class MergeSort {
    public static void mergeSort(int[] arr) {
        mSort(arr, 0, arr.length - 1);
    }

    /**
     * 递归分治
     *
     * @param left  左指針
     * @param right 右指針
     * @param. arr 待排数組*
     */
    public static void mSort(int[] arr, int left, int right) {
        if (left >= right)
            return;
        int mid = (left + right) / 2;

        mSort(arr, left, mid); //递归排序左边
        mSort(arr, mid + 1, right); //递归排序右边
        merge(arr, left, mid, right); //合并;
    }

    /**
     * 合并兩个有序数組
     *
     * @param arr   待合并数組
     * @param left  左指針
     * @param mid   中间指針
     * @param right 右指針
     */
    public static void merge(int[] arr, int left, int mid, int right) {
        //[left, mid] [mid+1, right]
        int[] temp = new int[right - left + 1]; //中间数組

        int i = left;
        int j = mid + 1;
        int k = 0;
        while (i <= mid && j <= right) {
            if (arr[i] <= arr[j]) {
                temp[k++] = arr[i++];
            } else {
                temp[k++] = arr[j++];

            }
        }
        while (i <= mid) {
            temp[k++] = arr[i++];
        }

        while (j <= right) {
            temp[k++] = arr[j++];
        }

        for (int p = 0; p < temp.length; p++) {
            arr[left + p] = temp[p];
        }
    }
}

计数排序

若是在面试中有面试官要求你写一个O(n)时间复杂度的排序算法,你千万不要马上说:这不可能!虽然前面基于比较的排序的下限是O(nlogn)。可是确实也有线性时间复杂度的排序,只不过有前提条件,就是待排序的数要知足必定的范围的整数,并且计数排序须要比较多的辅助空间。其基本思想是,用待排序的数做为计数数组的下标,统计每一个数字的个数。而后依次输出便可获得有序序列。

实现代码:

public class CountSort {
    public static void countSort(int[] arr) {
        if (arr == null || arr.length == 0)
            return;

        int max = max(arr);
        int[] count = new int[max + 1];
        Arrays.fill(count, 0);

        for (int i = 0; i < arr.length; i++) {
            count[arr[i]]++;
        }

        int k = 0;
        for (int i = 0; i <= max; i++) {
            for (int j = 0; j < count[i]; j++)
                arr[k++] = i;
        }
    }

    public static int max(int[] arr) {
        int max = Integer.MIN_VALUE;
        for (int ele : arr) {
            if (ele > max)
                max = ele;
        }
        return max;
    }
}

桶排序

桶排序算是计数排序的一种改进和推广,可是网上有许多资料把计数排序和桶排序混为一谈。其实桶排序要比计数排序复杂许多。

对桶排序的分析和解释借鉴这位兄弟的文章(有改动):http://hxraid.iteye.com/blog/647759

桶排序的基本思想:

假设有一组长度为N的待排关键字序列K[1....n]。首先将这个序列划分红M个的子区间(桶) 。而后基于某种映射函数 ,将待排序列的关键字k映射到第i个桶中(即桶数组B的下标 i) ,那么该关键字k就做为B[i]中的元素(每一个桶B[i]都是一组大小为N/M的序列)。接着对每一个桶B[i]中的全部元素进行比较排序(可使用快排)。而后依次枚举输出B[0]….B[M]中的所有内容便是一个有序序列。bindex=f(key) 其中,bindex 为桶数组B的下标(即第bindex个桶), k为待排序列的关键字。桶排序之因此可以高效,其关键在于这个映射函数,它必须作到:若是关键字k1<k2,那么f(k1)<=f(k2)。也就是说B(i)中的最小数据都要大于B(i-1)中最大数据。很显然,映射函数的肯定与数据自己的特色有很大的关系。

举个栗子:

假如待排序列K= {4九、 38 、 3五、 97 、 7六、 73 、 2七、 49 }。这些数据所有在1—100之间。所以咱们定制10个桶,而后肯定映射函数f(k)=k/10。则第一个关键字49将定位到第4个桶中(49/10=4)。依次将全部关键字所有堆入桶中,并在每一个非空的桶中进行快速排序后获得如图所示。只要顺序输出每一个B[i]中的数据就能够获得有序序列了。

桶排序分析:

桶排序利用函数的映射关系,减小了几乎全部的比较工做。实际上,桶排序的f(k)值的计算,其做用就至关于快排中划分,希尔排序中的子序列,归并排序中的子问题,已经把大量数据分割成了基本有序的数据块(桶)。而后只须要对桶中的少许数据作先进的比较排序便可。

对N个关键字进行桶排序的时间复杂度分为两个部分:

(1) 循环计算每一个关键字的桶映射函数,这个时间复杂度是O(N)。

(2) 利用先进的比较排序算法对每一个桶内的全部数据进行排序,其时间复杂度为 ∑ O(Ni*logNi) 。其中Ni 为第i个桶的数据量。

很显然,第(2)部分是桶排序性能好坏的决定因素。尽可能减小桶内数据的数量是提升效率的惟一办法(由于基于比较排序的最好平均时间复杂度只能达到O(N*logN)了)。所以,咱们须要尽可能作到下面两点:

(1) 映射函数f(k)可以将N个数据平均的分配到M个桶中,这样每一个桶就有[N/M]个数据量。

(2) 尽可能的增大桶的数量。极限状况下每一个桶只能获得一个数据,这样就彻底避开了桶内数据的“比较”排序操做。固然,作到这一点很不容易,数据量巨大的状况下,f(k)函数会使得桶集合的数量巨大,空间浪费严重。这就是一个时间代价和空间代价的权衡问题了。

对于N个待排数据,M个桶,平均每一个桶[N/M]个数据的桶排序平均时间复杂度为:

O(N)+O(M(N/M)log(N/M))=O(N+N(logN-logM))=O(N+NlogN-N*logM)

当N=M时,即极限状况下每一个桶只有一个数据时。桶排序的最好效率可以达到O(N)。

总结: 桶排序的平均时间复杂度为线性的O(N+C),其中C=N*(logN-logM)。若是相对于一样的N,桶数量M越大,其效率越高,最好的时间复杂度达到O(N)。 固然桶排序的空间复杂度 为O(N+M),若是输入数据很是庞大,而桶的数量也很是多,则空间代价无疑是昂贵的。此外,桶排序是稳定的。

实现代码:

public class BucketSort {

    public static void bucketSort(int[] arr) {
        if (arr == null && arr.length == 0)
            return;

        // 默认为10,规定待排数[0,100]
        int bucketNums = 10;

        List<List<Integer>> buckets = new ArrayList<List<Integer>>(); //桶的索引

        for (int i = 0; i < 10; i++) {
            buckets.add(new LinkedList<Integer>()); //用链表比较合适
        }

        //划分桶
        for (int i = 0; i < arr.length; i++) {
            buckets.get(f(arr[i])).add(arr[i]);
        }

        // 对每一个桶进行排序
        for (int i = 0; i < buckets.size(); i++) {
            if (!buckets.get(i).isEmpty()) {
                Collections.sort(buckets.get(i));
            }
        }

        //还原排好序的数组
        int k = 0;
        for (List<Integer> bucket : buckets) {
            for (int ele : bucket) {
                arr[k++] = ele;
            }
        }
    }

    /**
     * 映射函数
     */
    public static int f(int x) {
        return x / 10;
    }
}

基数排序

基数排序又是一种和前面排序方式不一样的排序方式,基数排序不须要进行记录关键字之间的比较。基数排序是一种借助多关键字排序思想对单逻辑关键字进行排序的方法。所谓的多关键字排序就是有多个优先级不一样的关键字。好比说成绩的排序,若是两我的总分相同,则语文高的排在前面,语文成绩也相同则数学高的排在前面。。。若是对数字进行排序,那么个位、十位、百位就是不一样优先级的关键字,若是要进行升序排序,那么个位、十位、百位优先级一次增长。基数排序是经过屡次的收分配和收集来实现的,关键字优先级低的先进行分配和收集。

举个栗子:

实现代码:

public class RadixSort {
    public static void radixSort(int[] arr) {
        if (arr == null && arr.length == 0)
            return;
        int maxBit = getMaxBit(arr);
        for (int i = 1; i <= maxBit; i++) {
            List<List<Integer>> buf = distribute(arr, i);//分配
            collecte(arr, buf); //收集
        }
    }

    /**
     * 分配
     * @param arr 待分配数組
     * @param iBit 要分配第几位
     */
    public static List<List<Integer>> distribute(int[] arr, int iBit) {
        List<List<Integer>> buf = new ArrayList<List<Integer>>();
        for (int j = 0; j < 10; j++) {
            buf.add(new LinkedList<Integer>());
        }
        for (int i = 0; i < arr.length; i++) {
            buf.get(getNBit(arr[i], iBit)).add(arr[i]);
        }
        return buf;
    }

    /**
     * 收集
     * @param arr 把分的数据收集到arr中
     */
    public static void collecte(int[] arr, List<List<Integer>> buf) {
        int k = 0;
        for (List<Integer> bucket : buf) {
            for (int ele : bucket) {
                arr[k++] = ele;
            }
        }
    }

    /**
     * 获取最大位数
     */
    public static int getMaxBit(int[] arr) {
        int max = Integer.MIN_VALUE;
        for (int ele : arr) {
            int len = (ele + "").length();
            if (len > max)
                max = len;
        }
        return max;
    }

    /**
     * 获取x的第n位,若是没有则为0.
     */
    public static int getNBit(int x, int n) {
        String sx = x + "";
        if (sx.length() < n)
            return 0;
        else
            return sx.charAt(sx.length() - n) - '0';
    }
}

总结

在前面的介绍和分析中咱们提到了冒泡排序、选择排序、插入排序三种简单的排序及其变种快速排序、堆排序、希尔排序三种比较高效的排序。后面咱们又分析了基于分治递归思想的归并排序还有计数排序、桶排序、基数排序三种线性排序。咱们能够知道排序算法要么简单有效,要么是利用简单排序的特色加以改进,要么是以空间换取时间在特定状况下的高效排序。可是这些排序方法都不是固定不变的,须要结合具体的需求和场景来选择甚至组合使用。才能达到高效稳定的目的。没有最好的排序,只有最适合的排序。

下面就总结一下排序算法的各自的使用场景和适用场合。

\1. 从平均时间来看,快速排序是效率最高的,但快速排序在最坏状况下的时间性能不如堆排序和归并排序。然后者相比较的结果是,在n较大时归并排序使用时间较少,但使用辅助空间较多。

\2. 上面说的简单排序包括除希尔排序以外的全部冒泡排序、插入排序、简单选择排序。其中直接插入排序最简单,但序列基本有序或者n较小时,直接插入排序是好的方法,所以常将它和其余的排序方法,如快速排序、归并排序等结合在一块儿使用。

\3. 基数排序的时间复杂度也能够写成O(d*n)。所以它最使用于n值很大而关键字较小的的序列。若关键字也很大,而序列中大多数记录的最高关键字均不一样,则亦可先按最高关键字不一样,将序列分红若干小的子序列,然后进行直接插入排序。

\4. 从方法的稳定性来比较,基数排序是稳定的内排方法,全部时间复杂度为O(n^2)的简单排序也是稳定的。可是快速排序、堆排序、希尔排序等时间性能较好的排序方法都是不稳定的。稳定性须要根据具体需求选择。

\5. 上面的算法实现大多数是使用线性存储结构,像插入排序这种算法用链表实现更好,省去了移动元素的时间。具体的存储结构在具体的实现版本中也是不一样的。

附:基于比较排序算法时间下限为O(nlogn)的证实:

基于比较排序下限的证实是经过决策树证实的,决策树的高度Ω(nlgn),这样就得出了比较排序的下限。

首先要引入决策树。 首先决策树是一颗二叉树,每一个节点表示元素之间一组可能的排序,它予以京进行的比较相一致,比较的结果是树的边。 先来讲明一些二叉树的性质,令T是深度为d的二叉树,则T最多有2^片树叶。 具备L片树叶的二叉树的深度至少是logL。 因此,对n个元素排序的决策树必然有n!片树叶(由于n个数有n!种不一样的大小关系),因此决策树的深度至少是log(n!),即至少须要log(n!)次比较。 而 log(n!)=logn+log(n-1)+log(n-2)+…+log2+log1 >=logn+log(n-1)+log(n-2)+…+log(n/2) >=(n/2)log(n/2) >=(n/2)logn-n/2 =O(nlogn) 因此只用到比较的排序算法最低时间复杂度是O(nlogn)。

参考资料:

  • 《数据结构》 严蔚敏 吴伟民 编著
  • 桶排序分析:http://hxraid.iteye.com/blog/647759
  • 部分排序算法分析与介绍:http://www.cnblogs.com/weixliu/archive/2012/12/23/2829671.html
相关文章
相关标签/搜索