java-Android-常用十大排序算法-面试必备


排序就是将一组对象按照某种逻辑顺序重新排列的过程。比如,订单按照日期排序的——这种排序很可能使用了某种排序算法。现在计算机的广泛使用使得数据无处不在,而整理数据的第一步通常就是进行排序。

学习排序算法三大实际意义

  1. IT从业人员必备技能,也是互联网公司面试的必考点
  2. 其中包含的技术和思想也能有效解决其他类型的问题
  3. 排序算法常常是我们解决其他问题的第一步
    图片来源于网络
    十大排序算法:冒泡排序,选择排序,插入排序,归并排序,堆排序,快速排序、希尔排序、计数排序,基数排序,桶排序

一、冒泡排序算法

一种简单的排序算法。它反复地走访过要排序的数列,一次比较两个元素,如果它们的顺序错误就把它们交换过来。这个工作重复地进行直到没有元素再需要交换,也就是说该数列已经排序完成。这个算法的名字由来是因为元素会经由交换慢慢“浮”到数列的顶端。

具体步骤

  1. 从数组头开始,比较相邻的元素。如果第一个比第二个大(小),就交换它们两个;
  2. 对每一对相邻元素作同样的工作,从开始第一对到尾部的最后一对,这样在最后的元素应该会是最大(小)的数;
  3. 重复步骤1~2,重复次数等于数组的长度,直到排序完成

原理图如下

代码如下

public static int[] sort(int[] array) {  
        if (array.length == 0)  
            return array;  
        /*循环数组长度的次数*/  
        for (int i = 0; i < array.length; i++){  
            /*从第0个元素开始,依次和后面的元素进行比较 
             * j < array.length - 1 - i表示第[array.length - 1 - i] 
             * 个元素已经冒泡到了合适的位置,无需进行比较,可以减少比较次数*/  
            for (int j = 0; j < array.length - 1 - i; j++){  
                /*如果第j个元素比后面的第j+1元素大,交换两者的位置*/  
                if (array[j + 1] < array[j]) {  
                    int temp = array[j + 1];  
                    array[j + 1] = array[j];  
                    array[j] = temp;  
                }  
                PrintArray.print(array);  
            }  
            System.out.println("---------------");  
            //PrintArray.print(array);  
        }  
  
        return array;  
    }  

二、简单选择排序

选择排序的思想其实和冒泡排序有点类似,都是在一次排序后把最小的元素放到最前面。但是过程不同,冒泡排序是通过相邻的比较和交换。而选择排序是通过对整体的选择。
其实选择排序可以看成冒泡排序的优化,因为其目的相同,只是选择排序只有在确定了最小数的前提下才进行交换,大大减少了交换的次数。
举个例子,对5,3,8,6,4这个无序序列进行简单选择排序,首先要选择5以外的最小数来和5交换,也就是选择3和5交换,一次排序后就变成了3,5,8,6,4.对剩下的序列继续进行选择和交换,最终就会得到一个有序序列。

具体步骤

  1. 首先,找到数组中最大(小)的那个元素;
  2. 其次,将它和数组的第一个元素交换位置(如果第一个元素就是最大(小)元素那么它就和自己交换);
  3. 再次,在剩下的元素中找到最大(小)的元素,将它与数组的第二个元素交换位置。如此往复,直到将整个数组排序。

原理图如下

代码如下

public static int[] sort(int[] array) {
        if (array.length == 0)
            return array;
        for (int i = 0; i < array.length; i++) {
            int minIndex=i;/*最小数的下标,每个循环开始总是假设第一个数最小*/
            for (int j = i; j < array.length; j++) {
                if (array[j] < array[minIndex]) /*找到最小的数*/
                    minIndex = j; /*将最小数的索引保存*/
            }
            System.out.println("最小数为:"+array[minIndex]);
            /*交换最小数和i当前所指的元素*/
            int temp = array[minIndex];
            array[minIndex] = array[i];
            array[i] = temp;
            PrintArray.print(array);
            System.out.println("---------------");
        }
        return array;
    }

三、简单插入排序

插入排序不是通过交换位置而是通过比较找到合适的位置插入元素来达到排序的目的的。相信大家都有过打扑克牌的经历,特别是牌数较大的。在分牌时可能要整理自己的牌,牌多的时候怎么整理呢?就是拿到一张牌,找到一个合适的位置插入。这个原理其实和插入排序是一样的。
举个例子,我们将要收到5,3,4,,8,6这几张牌,我们先收到5这张牌,毫无疑问,这张牌的位置是正确的,没必要整理。接着收到了牌3,然后3要插到5前面,把5后移一位,变成3,5。接着又收到了牌4,现在我们会怎么做?把4插入到5前面,把5后移一位。

具体步骤

  1. 对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。
  2. 为了给要插入的元素腾出空间,我们需要将插入位置之后的已排序元素在都向右移动一位。
    插入排序所需的时间取决于输入中元素的初始顺序。例如,对一个很大且其中的元素已经有序(或接近有序)的数组进行排序将会比对随机顺序的数组或是逆序数组进行排序要快得多。
    总的来说,插入排序对于部分有序的数组十分高效,也很适合小规模数组。

原理图如下

代码如下

public static int[] sort(int[] array) {
        if (array.length == 0)
            return array;
        int currentValue;/*当前待排序数据,该元素之前的元素均已被排序过*/
        for (int i = 0; i < array.length - 1; i++) {
            int preIndex = i;/*已被排序数据的索引*/
            currentValue = array[preIndex + 1];
            System.out.println("待排序元素索引:"+(i + 1)+",值为:" +currentValue+
                    ",已被排序数据的索引:"+preIndex);

            /*在已被排序过数据中倒序寻找合适的位置,如果当前待排序数据比比较的元素要小,
            将比较的元素元素后移一位*/
            while (preIndex >= 0 && currentValue < array[preIndex]) {
                //将当前元素后移一位
                array[preIndex + 1] = array[preIndex];
                preIndex--;
                PrintArray.print(array);
            }
            /*while循环结束时,说明已经找到了当前待排序数据的合适位置,插入*/
            array[preIndex + 1] = currentValue;
            System.out.println("本轮被插入排序后的数组");
            PrintArray.print(array);
            System.out.println("--------------------");
        }
        return array;
    }

四、希尔排序

一种基于插入排序的快速的排序算法(请大家先学习插入排序,了解基本的插入排序的思想。对于大规模乱序数组插入排序很慢,因为元素只能一点一点地从数组的一端移动到另一端。例如,如果主键最小的元素正好在数组的尽头,要将它挪到正确的位置就需要N-1 次移动。
希尔排序为了加快速度简单地改进了插入排序,也称为缩小增量排序,同时该算法是冲破O(n^2)的第一批算法之一。
希尔排序是把待排序数组按一定增量的分组,对每组使用直接插入排序算法排序;然后缩小增量继续分组排序,随着增量逐渐减少,每组包含的元素越来越多,当增量减至 1 时,整个数组恰被分成一组,排序便完成了。这个不断缩小的增量,就构成了一个增量序列。

原理图如下

希尔排序中的增量序列

  1. 在先前较大的增量下每个子序列的规模都不大,用直接插入排序效率都较高,尽管在随后的增量递减分组中子序列越来越大,由于整个序列的有序性也越来越明显,则排序效率依然较高。
  2. 从理论上说,只要一个数组是递减的,并且最后一个值是1,都可以作为增量序列使用。有没有一个步长序列,使得排序过程中所需的比较和移动次数相对较少,并且无论待排序列记录数有多少,算法的时间复杂度都能渐近最佳呢?但是目前从数学上来说,无法证明某个序列是“最好的”。
  3. 常用的增量序列
    希尔增量序列 :{N/2, (N / 2)/2, ..., 1},其中N为原始数组的长度,这是最常用的序列,但却不是最好的
    Hibbard序列:{2^k-1, ..., 3,1}
    Sedgewick序列: {... , 109 , 41 , 19 , 5,1}

代码如下

 public static int[] sort(int[] array) {
        int len = array.length;
        /*按增量分组后,每个分组中,temp代表当前待排序数据,该元素之前的元素均已被排序过*/
        /*gap指用来分组的增量,会依次递减*/
        int currentValue, gap = len / 2;
        while (gap > 0) {
            for (int i = gap; i < len; i++) {
                currentValue = array[i];
                /*组内已被排序数据的索引*/
                int preIndex = i - gap;
                /*在组内已被排序过数据中倒序寻找合适的位置,如果当前待排序数据比比较的元素要小,
                并将比较的元素元素在组内后移一位*/
                while (preIndex >= 0 && array[preIndex] > currentValue) {
                    array[preIndex + gap] = array[preIndex];
                    preIndex -= gap;
                }
                /*while循环结束时,说明已经找到了当前待排序数据的合适位置,插入*/
                array[preIndex + gap] = currentValue;
            }
            System.out.println("本轮增量【"+gap+"】排序后的数组");
            PrintArray.print(array);
            System.out.println("--------------------");
            gap /= 2;
        }
        return array;
    }

五、归并排序

归并排序是建立在归并操作上的一种有效的排序算法。该算法是采用分治法的一个非常典型的应用。
对于给定的一组数据,利用递归与分治技术将数据序列划分成为越来越小的半子表,在对半子表排序后,再用递归方法将排好序的半子表合并成为越来越大的有序序列。
为了提升性能,有时我们在半子表的个数小于某个数(比如15)的情况下,对半子表的排序采用其他排序算法,比如插入排序。
若将两个有序表合并成一个有序表,称为2-路归并,与之对应的还有多路归并

原理图如下

代码实现

public static int[] sort(int[] array) {
        if (array.length < 2) return array;
        /*切分数组,然后递归调用*/
        int mid = array.length / 2;
        int[] left = Arrays.copyOfRange(array, 0, mid);
        int[] right = Arrays.copyOfRange(array, mid, array.length);
        return merge(sort(left), sort(right));
    }
    
    /**
     * 归并排序——将两段排序好的数组结合成一个排序数组
     * @param left
     * @param right
     * @return
     */
    public static int[] merge(int[] left, int[] right) {
        int[] result = new int[left.length + right.length];
        for (int index = 0, i = 0, j = 0; index < result.length; index++) {
            if (i >= left.length)/*左边数组已经取完,完全取右边数组的值即可*/
                result[index] = right[j++];
            else if (j >= right.length)/*右边数组已经取完,完全取左边数组的值即可*/
                result[index] = left[i++];
            else if (left[i] > right[j])/*左边数组的元素值大于右边数组,取右边数组的值*/
                result[index] = right[j++];
            else/*右边数组的元素值大于左边数组,取左边数组的值*/
                result[index] = left[i++];
        }
        System.out.print("左子数组:");
        PrintArray.print(left);
        System.out.print("右子数组:");
        PrintArray.print(right);
        System.out.print("合并后数组:");
        PrintArray.print(result);
        System.out.println("--------------------");
        return result;
    }

六、快速排序

快速排序被誉为20 世纪科学和工程领域的十大算法之一。快速排序(Quicksort)是对冒泡排序的一种改进,也是采用分治法的一个典型的应用。

具体步骤

  1. 首先任意选取一个数据(比如数组的第一个数)作为关键数据,我们称为基准数,然后将所有比它小的数都放到它前面,所有比它大的数都放到它后面,这个过程称为一趟快速排序,也称为分区(partition)操作。在实际实现时,一般会在原数组上直接操作。
  2. 通过一趟快速排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列。
  3. 为了提升性能,有时我们在分割后独立的两部分的个数小于某个数(比如15)的情况下,会采用其他排序算法,比如插入排序。

原理图如下

基准数

  1. 基准的选取:最优的情况是基准值刚好取在无序区数值的中位数,这样能够最大效率地让两边排序,同时最大地减少递归划分的次数,但是一般很难做到最优。基准的选取一般有三种方式,选取数组的第一个元素,选取数组的最后一个元素,以及选取第一个、最后一个以及中间的元素的中位数(如4 5 6 7, 第一个4, 最后一个7, 中间的为5, 这三个数的中位数为5, 所以选择5作为基准)。
  2. Dual-Pivot快排:双基准快速排序算法,其实就是用两个基准数, 把整个数组分成三份来进行快速排序,在这种新的算法下面,比经典快排从实验来看节省了10%的时间。

代码如下

public static int[] sort(int[] array, int start, int end) {
        if (array.length < 1 || start < 0 || end >= array.length || start > end)
            return null;
        /*数据分割成独立的两部分时,从哪儿分区的指示器*/
        int zoneIndex = partition(array, start, end);
        if (zoneIndex > start)
            sort(array, start, zoneIndex - 1);
        if (zoneIndex < end)
            sort(array, zoneIndex + 1, end);
        System.out.println("本轮排序后的数组");
        PrintArray.printIndex(array,start,end);
        System.out.println("--------------------");
        return array;
    }
    /**
     * 快速排序算法——partition
     * @param array
     * @param start
     * @param end
     * @return
     */
    public static int partition(int[] array, int start, int end) {
        int pivot = (int) (start + Math.random() * (end - start + 1));
        System.out.println("开始下标:"+start+",结束下标:"+end+",基准数下标:"
                +pivot+",元素值:"+array[pivot]);
        /*zoneIndex是分割指示器
        从业务上来说:比基准数小的,放到指示器的左边,比基准数大的,放到指示器的右边,
        * 但在实际实现时,通过移动比基准数小的元素和分割指示器本身也可以达到一样的效果*/
        int zoneIndex = start - 1;
        swap(array, pivot, end);/*将基准数和数组尾元素交换位置*/
        for (int i = start; i <= end; i++){
            if (array[i] <= array[end]) {/*当前元素小于等于基准数*/
                zoneIndex++;/*首先分割指示器累加*/
                if (i > zoneIndex)/*当前元素在分割指示器的右边时,交换当前元素和分割指示器元素*/
                    swap(array, i, zoneIndex);
            }
            System.out.println("zoneIndex:"+zoneIndex+",i:"+i);
            PrintArray.printIndex(array,start,end);
        }
        System.out.println("---------------");
        return zoneIndex;
    }

    /**
     * 交换数组内两个元素
     * @param array
     * @param i
     * @param j
     */
    public static void swap(int[] array, int i, int j) {
        int temp = array[i];
        array[i] = array[j];
        array[j] = temp;
    }

七、堆排序

许多应用程序都需要处理有序的元素,但不一定要求他们全部有序,或者不一定要一次就将他们排序,很多时候,我们每次只需要操作数据中的最大元素(最小元素),那么有一种基于二叉堆的数据结构可以提供支持。

具体步骤

  1. 所谓二叉堆,是一个完全二叉树的结构,同时满足堆的性质:即子结点的键值或索引总是小于(或者大于)它的父节点。在一个二叉堆中,根节点总是最大(或者最小)节点。
  2. 堆排序算法就是抓住了这一特点,每次都取堆顶的元素,然后将剩余的元素重新调整为最大(最小)堆,依次类推,最终得到排序的序列。

完全二叉树

原理图如下

代码如下

public class HeapSort {
    //声明全局变量,用于记录数组array的长度;
    private static int len;

    public static int[] sort(int[] array) {
        len = array.length;
        if (len < 1) return array;
        //1.构建一个最大堆
        buildMaxHeap(array);
        //2.循环将堆首位(最大值)与末位交换,然后在重新调整最大堆
        while (len > 0) {
            swap(array, 0, len - 1);
            len--;
            adjustHeap(array, 0);
            PrintArray.print(array);
            System.out.println("--------------------");
        }
        return array;
    }
    /**
     * 建立最大堆
     *
     * @param array
     */
    public static void buildMaxHeap(int[] array) {
        //从最后一个非叶子节点开始向上构造最大堆
        for (int i = (len/2-1); i >= 0; i--) {
            adjustHeap(array, i);
        }
        System.out.println("构造完成最大堆");
        PrintArray.print(array);
        System.out.println("============================================");
    }
    /**
     * 调整使之成为最大堆
     *
     * @param array
     * @param i
     */
    public static void adjustHeap(int[] array, int i) {
        int maxIndex = i;
        int left = 2*i+1;
        int right = 2*(i+1);
        //如果有左子树,且左子树大于父节点,则将最大指针指向左子树
        if (left < len && array[left] > array[maxIndex])
            maxIndex = left;
        //如果有右子树,且右子树大于父节点,则将最大指针指向右子树
        if (right < len && array[right] > array[maxIndex])
            maxIndex = right;
        //如果父节点不是最大值,则将父节点与最大值交换,并且递归调整与父节点交换的位置。
        if (maxIndex != i) {
            swap(array, maxIndex, i);
            adjustHeap(array, maxIndex);
        }
    }

    /**
     * 交换数组内两个元素
     * @param array
     * @param i
     * @param j
     */
    public static void swap(int[] array, int i, int j) {
        int temp = array[i];
        array[i] = array[j];
        array[j] = temp;
    }

    public static void main(String[] args) {
        PrintArray.print(PrintArray.SRC);
        System.out.println("============================================");
        int[] dest = HeapSort.sort(PrintArray.SRC);
        PrintArray.print(dest);
    }
}

八、计数排序

具体步骤

  1. 计数排序对一定范围内的整数排序时候的速度非常快,一般快于其他排序算法。但计数排序局限性比较大,只限于对整数进行排序,而且待排序元素值分布较连续、跨度小的情况。
  2. 计数排序是一个排序时不比较元素大小的排序算法。
  3. 如果一个数组里所有元素都是整数,而且都在0-K以内。对于数组里每个元素来说,如果能知道数组里有多少项小于或等于该元素,就能准确地给出该元素在排序后的数组的位置。

原理图如下

实际应用中我们会同时找出数组中的max和min,主要是为了尽量节省空间。试想[1003, 1001, 1030, 1050]这样的数据要排序,真的需要建立长度为1050 + 1的数组吗?我们只需要长度为1050 - 1003 + 1= 48的数组(先不考虑额外+1的长度),就能囊括从最小到最大元素之间的所有元素了。
如果待排序数组的元素值跨度很大,比如[99999, 1, 2],为三个元素排序要使用99999 - 1 + 1的空间,实在是浪费。所以计数排序适用于待排序元素值分布较连续、跨度小的情况。

代码如下

public static int[] sort(int[] array) {
        if (array.length == 0) return array;
        /*寻找数组中最大值,最小值
        * bias:偏移量,用以定位原始数组每个元素在计数数组中的下标位置*/
        int bias, min = array[0], max = array[0];
        for (int i = 1; i < array.length; i++) {
            if (array[i] > max)
                max = array[i];
            if (array[i] < min)
                min = array[i];
        }
        bias = 0 - min;
        /*获得计数数组的容量*/
        int[] counterArray = new int[max - min + 1];
        Arrays.fill(counterArray, 0);
        /*遍历整个原始数组,将原始数组中每个元素值转化为计数数组下标,
        并将计数数组下标对应的元素值大小进行累加*/
        for (int i = 0; i < array.length; i++) {
            counterArray[array[i] + bias]++;
        }
        System.out.println("计数数组为:");
        PrintArray.print(counterArray);
        System.out.println("============================================");
        int index = 0;/*访问原始数组时的下标计数器*/
        int i = 0;/*访问计数数组时的下标计数器*/
        /*访问计数数组,将计数数组中的元素转换后,重新写回原始数组*/
        while (index < array.length) {
            /*只要计数数组中当前下标元素的值不为0,就将计数数组中的元素转换后,重新写回原始数组*/
            if (counterArray[i] != 0) {
                array[index] = i - bias;
                counterArray[i]--;
                index++;
            } else
                i++;
            PrintArray.print(counterArray);
            PrintArray.print(array);
            System.out.println("--------------------");
        }
        return array;
    }

    final static int[] src = {5,4,5,0,3,6,2,0,2,4,3,3};

    public static void main(String[] args) {

        PrintArray.print(src);
        System.out.println("============================================");
        int[] dest = CountingSort.sort(src);
        PrintArray.print(dest);
    }

九、桶排序

简介

桶排序 (Bucket sort)的工作的原理:假设输入数据服从均匀分布,利用某种函数的映射关系将数据分到有限数量的桶里,每个桶再分别排序(有可能再使用别的排序算法或是以递归方式继续使用桶排序)。
桶排序利用函数的映射关系,减少了几乎所有的比较工作。实际上,桶排序的f(k)值的计算,其作用就相当于快排中划分,已经把大量数据分割成了基本有序的数据块(桶)。然后只需要对桶中的少量数据做排序即可。

原理图如下

代码如下

public class BucketSort {
    /**
     *
     * @param array
     * @param bucketSize BucketSize,作为每个桶所能放置多少个不同数值
     *                   (例如当BucketSize==5时,该桶可以存放{1,2,3,4,5}这几种数字,
     *                   但是容量不限,即可以存放100个3);
     * @return
     */
    public static ArrayList<Integer> sort(ArrayList<Integer> array, int bucketSize) {
        if (array == null || array.size() < 2)
            return array;
        int max = array.get(0), min = array.get(0);
        // 找到最大值最小值
        for (int i = 0; i < array.size(); i++) {
            if (array.get(i) > max)
                max = array.get(i);
            if (array.get(i) < min)
                min = array.get(i);
        }
        /*获得桶的数量*/
        int bucketCount = (max - min) / bucketSize + 1;
        /*构建桶*/
        ArrayList<ArrayList<Integer>> bucketArr = new ArrayList<>(bucketCount);
        ArrayList<Integer> resultArr = new ArrayList<>();
        for (int i = 0; i < bucketCount; i++) {
            bucketArr.add(new ArrayList<Integer>());
        }
        /*将原始数组中的数据分配到桶中*/
        for (int i = 0; i < array.size(); i++) {
            bucketArr.get((array.get(i) - min) / bucketSize).add(array.get(i));
        }
        /*看看桶中数据的分布*/
        for (int i = 0; i < bucketArr.size(); i++) {
            System.out.print("第"+i+"个桶包含数据:");
            PrintArray.printObject(bucketArr.get(i));
        }
        for (int i = 0; i < bucketCount; i++) {
            if (bucketSize == 1) {
                for (int j = 0; j < bucketArr.get(i).size(); j++)
                    resultArr.add(bucketArr.get(i).get(j));
            } else {
                if (bucketCount == 1)
                    bucketSize--;
                /*对桶中的数据再次用桶进行排序*/
                ArrayList<Integer> temp = sort(bucketArr.get(i), bucketSize);
                for (int j = 0; j < temp.size(); j++)
                    resultArr.add(temp.get(j));
            }
        }
        return resultArr;
    }

    public static void main(String[] args) {
        ArrayList<Integer> array = new ArrayList<>();
        array.add(86);
        array.add(11);
        array.add(77);
        array.add(23);
        array.add(32);
        array.add(45);
        array.add(58);
        array.add(63);
        array.add(93);
        array.add(4);
        array.add(37);
        array.add(22);
        PrintArray.printObject(array);
        System.out.println("============================================");
        ArrayList<Integer> dest = BucketSort.sort(array,2);
        PrintArray.printObject(dest);
    }
}

十、基数排序

原理

常见的数据元素一般是由若干位组成的,比如字符串由若干字符组成,整数由若干位0~9数字组成。基数排序按照从右往左的顺序,依次将每一位都当做一次关键字,然后按照该关键字对数组排序,同时每一轮排序都基于上轮排序后的结果;当我们将所有的位排序后,整个数组就达到有序状态。比如对于数字2985,从右往左就是先以个位为关键字进行排序,然后是十位、百位、千位,总共需要四轮。
基数是什么意思?对于十进制整数,每一位都只可能是0~9中的某一个,总共10种可能。那10就是它的基,同理二进制数字的基为2;对于字符串,如果它使用的是8位的扩展ASCII字符集,那么它的基就是256。

原理图如下

基数排序 vs 计数排序 vs 桶排序

基数排序有两种方法:
  1. MSD 从高位开始进行排序
  2. LSD 从低位开始进行排序
这三种排序算法都利用了桶的概念,但对桶的使用方法上有明显差异:
  1. 基数排序:根据键值的每位数字来分配桶
  2. 计数排序:每个桶只存储单一键值
  3. 桶排序:每个桶存储一定范围的数值

代码如下

public static int[] sort(int[] array) {
        if (array == null || array.length < 2)
            return array;
        /*找出最大数*/
        int max = array[0];
        for (int i = 1; i < array.length; i++) {
            max = Math.max(max, array[i]);
        }

        /*先算出最大数的位数*/
        int maxDigit = 0;
        while (max != 0) {
            max /= 10;
            maxDigit++;
        }
        int mod = 10, div = 1;
        /*构建桶*/
        ArrayList<ArrayList<Integer>> bucketList = new ArrayList<ArrayList<Integer>>();
        for (int i = 0; i < 10; i++)
            bucketList.add(new ArrayList<Integer>());
        /*按照从右往左的顺序,依次将每一位都当做一次关键字,然后按照该关键字对数组排序,
        每一轮排序都基于上轮排序后的结果*/
        for (int i = 0; i < maxDigit; i++, mod *= 10, div *= 10) {
            /*遍历原始数组,投入桶中*/
            for (int j = 0; j < array.length; j++) {
                int num = (array[j] % mod) / div;
                bucketList.get(num).add(array[j]);
            }
            /*桶中的数据写回原始数组,清除桶,准备下一轮的排序*/
            int index = 0;
            for (int j = 0; j < bucketList.size(); j++) {
                for (int k = 0; k < bucketList.get(j).size(); k++)
                    array[index++] = bucketList.get(j).get(k);
                bucketList.get(j).clear();
            }
        }
        return array;
    }

十一、外部排序

原理

有时,待排序的文件很大,计算机内存不能容纳整个文件,这时候对文件就不能使用内部排序了(我们一般的排序都是在内存中做的,所以称之为内部排序,而外部排序是指待排序的内容不能在内存中一下子完成,它需要做内外存的内容交换),外部排序常采用的排序方法也是归并排序,这种归并方法由两个不同的阶段组成:

  1. 采用适当的内部排序方法对输入文件的每个片段进行排序,将排好序的片段(成为归并段)写到外部存储器中(通常由一个可用的磁盘作为临时缓冲区),这样临时缓冲区中的每个归并段的内容是有序的。
  2. 利用归并算法,归并第一阶段生成的归并段,直到只剩下一个归并段为止。

1、将内存空间划分为三份,每份大小250个记录,其中两个用作输入缓冲区,另外一个用作输出缓冲区。首先对Segment_1和Segment_2进行归并,先从每个归并段中读取250个记录到输入缓冲区,对其归并,归并结果放到输出缓冲区,当输出缓冲区满后,将其写到临时缓冲区(由一个可用的磁盘充当)内,如果某个输入缓冲区空了,则从相应的归并段中再读取250个记录进行继续归并,反复以上步骤,直至Segment_1和Segment_2全都排好序,形成一个大小为1500的记录,然后对Segment_3和Segment_4、Segment_5和Segment_6进行同样的操作。
2、对归并好的大小为1500的记录进行如同步骤1一样的操作,进行继续排序,直至最后形成大小为4500的归并段,至此,排序结束。

排序算法总结

算法的稳定性

  1. 稳定:如果a原本在b前面,而a=b,排序之后a仍然在b的前面
  2. 不稳定:如果a原本在b的前面,而a=b,排序之后a可能会出现在b的后面;
    排序算法如果是稳定的,那么从一个键上排序,然后再从另一个键上排序,前一个键排序的结果可以为后一个键排序所用。

算法的复杂度

算法的复杂度往往取决于数据的规模大小和数据本身分布性质。

  1. 时间复杂度: 一个算法执行所耗费的时间。
  2. 空间复杂度:对一个算法在运行过程中临时占用存储空间大小的量度。
  3. 常见复杂度由小到大:O(1) < O(logn) < O(n) < O(nlogn) < O(n^2) < O(n^3) < O(2^n)
    在各种不同算法中,若算法中语句执行次数(占用空间)为一个常数,则复杂度为O(1);
    当一个算法的复杂度与以2为底的n的对数成正比时,可表示为O(log n);
    当一个算法的复杂度与n成线性比例关系时,可表示为O (n),依次类推。

时间复杂度记忆

  1. 冒泡、选择、插入排序需要两个for循环,每次只关注一个元素,平均时间复杂度为O(n的平方)(一遍找元素O(n),一遍找位置O(n))
  2. 快速、归并、堆基于分治思想,log以2为底,平均时间复杂度往往和O(nlogn)(一遍找元素O(n),一遍找位置O(logn))相关
  3. 而希尔排序依赖于所取增量序列的性质,但是到目前为止还没有一个最好的增量序列 。例如希尔增量序列时间复杂度为O(n²),而Hibbard增量序列的希尔排序的时间复杂度为O(n3/2次方) , 有人在大量的实验后得出结论;当n在某个特定的范围后希尔排序的最小时间复杂度大约为n^1.3。
    从平均时间来看,快速排序是效率最高的:
  • 快速排序中平均时间复杂度O(nlog n),这个公式中隐含的常数因子很小,比归并排序的O(nlog n)中的要小很多,所以大多数情况下,快速排序总是优于合并排序的。
  • 而堆排序的平均时间复杂度也是O(nlog n),但是堆排序存在着重建堆的过程,它把根节点移除后,把最后的叶子结点拿上来后需要重建堆,但是,拿上的值是要比它的两个叶子结点要差很多的,一般要比较很多次,才能回到合适的位置。堆排序就会有很多的时间耗在堆调整上。
  • 虽然快速排序的最坏情况为排序规模(n)的平方关系,但是这种最坏情况取决于每次选择的基准, 对于这种情况,已经提出了很多优化的方法,比如三取样划分和Dual-Pivot快排。 同时,当排序规模较小时,划分的平衡性容易被打破,而且频繁的方法调用超过了O(nlog n)为O(n的平方)省出的时间,所以一般排序规模较小时,会改用插入排序或者其他排序算法。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章