[算法] 排序算法总结

插入排序

算法过程

算法初始认为第一个元素是有序的,第二个元素及其后面的元素都是无序的,算法的过程就是将后面无序的元素依次插入到前面有序的元素中合适的位置的过程,期间可能需要移动前面部分有序的元素。

代码实现

private static void insertSort(int[] array) {
        int length = array.length;
        int i, j;
        for (i = 1; i < length; i++) {
            int tmp = array[i];
            for (j = i; j > 0 && array[j-1] > tmp; j--) {
                array[j] = array[j-1];
            }
            array[j] = tmp;
        }
    }

复杂度分析

  • 最好的情况:数组本省就是有序的,那就只需要比较 n 1 次,不需要移动元素;
  • 最坏的情况:数组是逆序的情况,对每一个待排序的元素,它都要和它前面的元素进行比较,总共需要比较的次数为 1 + 2 + 3 + . . . + ( n 1 ) = n ( n 1 ) 2 次,需要移动的次数为 1 + 2 + 3 + . . . + ( n 1 ) = n ( n 1 ) 2 次;
  • 平均时间复杂度为 O ( n 2 ) ;

希尔排序

算法分析

希尔排序是插入排序的一种更高效的改进版本,它的作法不是每次一个元素挨一个元素的比较。而是初期选用大跨步(增量较大)间隔比较,使记录跳跃式接近它的排序位置;然后增量缩小;最后增量为 1 ,这样记录移动次数大大减少,提高了排序效率。希尔排序对增量序列的选择没有严格规定。

希尔排序是基于插入排序的以下两点性质而提出改进方法的:
- 插入排序在对几乎已经排好序的数据操作时, 效率高, 即可以达到线性排序的效率;
- 但插入排序一般来说是低效的, 因为插入排序每次只能将数据移动一位;

希尔排序的基本思想是:先将整个待排序的记录序列分割成为若干子序列分别进行直接插入排序,待整个序列中的记录“基本有序”时,再对全体记录进行依次直接插入排序。

算法过程

  1. 先取一个正整数 d 1 ( d 1 < n ) ,把全部记录分成 d 1 个组,所有距离为 d 1 的倍数的记录看成一组,然后在各组内进行插入排序;
  2. 然后取 d 2 ( d 2 < d 1 ) ;
  3. 重复上述分组和排序操作,直到取 d i = 1 ( i >= 1 ) 位置,即所有记录成为一个组,最后对这个组进行插入排序。一般选 d 1 约为 n 2 d 2 d 1 2 d 3 d 2 2 , , d i = 1 ;

代码实现

private static void shellSort(int[] array) {
        int length = array.length;
        int i, j, d;
        for (d = length/2; d > 0; d /= 2) {
            for (i = d; i < length; i++) {  // 从各组内的第二个元素开始进行插入排序
                int tmp = array[i];
                for (j = i; j > 0 && array[j-d] > tmp; j-=d) {
                    array[j] = array[j-d];
                }
                array[j] = tmp;
            }
        }
    }

选择排序

算法步骤

  1. 首先在未排好序的序列中找到最小(大)的元素,存放到排序序列的起始位置(交换最大值和第一个位置的元素);
  2. 再从剩余未排序的元素中继续寻找最小(最大)元素,然后放到已排序序列的末尾(交换剩余元素中最大值和已排序序列末尾的元素);
  3. 从都第二步,直到所有元素均排序完毕;

代码实现

private static void selectSort(int[] array) {
        int length = array.length;
        for (int i = 0; i < length; i++) {
            int minIndex = i;
            for (int j = i+1; j < length; j++) {
                if (array[j] < array[minIndex]) { // 找到更小的元素,记录下元素位置
                    minIndex = j;
                }
            }
            int tmp = array[minIndex];
            array[minIndex] = array[i];
            array[i] = tmp;
        }
    }

时间复杂度

时间复杂度为 O ( n 2 )

冒泡排序(Bubble Sort)

算法过程

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

代码实现

private static void bubbleSort(int[] array) {
        int length = array.length;
        int index = length-1;
        while (index > 0) {
            for (int i = 0; i < index; i++) {
                if (array[i] > array[i+1]) {
                    int tmp = array[i+1];
                    array[i+1] = array[i];
                    array[i] = tmp;
                }
            }
            index--;
        }
    }

时间复杂度

算法时间复杂度为 O ( n 2 )

归并排序

算法过程

将数组 A [ 0... n 1 ] 中元素分成两个子数组: A 1 [ 0... n 2 ] A 2 [ n 2 + 1... n 1 ] ,分别对这两个子数组进行排序,然后将已排好序的子数组归并成一个含有 n 个元素的有序数组。归并排序利用了对两个已经有序的数组合并只需要 O ( n ) 时间复杂度的优势。归并排序可以使用递归和非递归方式实现。

递归实现

递归的方式是从大序列开始,逐步分解,直到最小的只含有单个元素的序列,然后再返回合并。

private static void mergerSort(int[] array, int start, int end) {
        if (start >= end) {
            return;
        }
        int middle = (start + end) >>> 1;
        mergerSort(array, start, middle);
        mergerSort(array, middle+1, end);
        merger(array, start, middle, end);
    }

    private static void merger(int[] array, int start, int middle, int end) {
        int i = start, j = middle+1, index = 0;
        int[] tmp = new int[end-start+1];
        for (;i <= middle && j <= end;) {
            if (array[i] > array[j]) {
                tmp[index++] = array[j];
            } else {
                tmp[index++] = array[i];
            }
        }
        while (i <= middle) {
            tmp[index++] = array[i++];
        }

        while (j <= end) {
            tmp[index++] = array[j++];
        }

        for (int k = 0; k < end-start+1; k++) {
            array[start+k] = tmp[k];
        }
    }

非递归实现

非递归的方式则恰恰相反,它是从单个元素的序列开始,逐步和相邻的部分排序、合并,最终形成一个大的序列。

private static void mergeSortNoRecursive(int[] array) {
        int length = array.length;
        for (int i = 1; i < length; i *= 2) {
            int start = 0, end = start+2*i-1;
            int middle = (start+end)>>>1;
            while (end <= length-1) {
                merger(array, start, middle, end);
                start = end + 1;
                end = start + 2*i - 1;
                middle = (start + end) >>> 1;
            }
            if (start < length-1) {
                middle = (start + length-1) >>> 1;
                merger(array, start, middle, length-1);
            }
        }
    }

    private static void merger(int[] array, int start, int middle, int end) {
        int i = start, j = middle+1, index = 0;
        int[] tmp = new int[end-start+1];
        for (;i <= middle && j <= end;) {
            if (array[i] > array[j]) {
                tmp[index++] = array[j++];
            } else {
                tmp[index++] = array[i++];
            }
        }
        while (i <= middle) {
            tmp[index++] = array[i++];
        }

        while (j <= end) {
            tmp[index++] = array[j++];
        }

        for (int k = 0; k < end-start+1; k++) {
            array[start+k] = tmp[k];
        }
    }

时间复杂度分析

T ( n ) 为对长度为 n 的数组进行排序的时间复杂度,则拆2部分子数组的各自的时间复杂度为 T ( n 2 ) ,对这两个子数组合并的时间复杂度为 c n ( c 为常数),因此时间复杂度的地推公式如下所示:

T ( n ) = 2 T ( n 2 ) + c n

n = 2 k ,则:
T ( n ) = 2 k T ( 1 ) + k c n = a n + c n l o g 2 n

所以归并排序的时间复杂度为 O ( n l o g 2 n )

归并排序的两点改进

  • 在数组长度比较短的情况下,不进行递归,而是选择其他排序方案:如插入排序等;
  • 合并数组过程中,可以用记录数组下标的方式代替申请新内存空间;

note:基于关键字比较的排序算法的时间复杂度的下界为 O ( n l o g 2 n )

归并排序用来做外部排序

  • 外部排序是指处理超过内存限度的数据的排序算法,通常将中间结果放在读写较慢的外存储器上(通常是硬盘);
  • 外部排序常采用”排序-归并”策略:
    • 排序阶段,读入能放在内存中的数据量,将其排序输出到临时文件,依次进行,将待排序数据组织为多个有序的临时文件;
    • 归并阶段,将这些有序的临时文件组合为大的有序文件;

外部排序举例

  • 使用100M内存对900M数据进行排序:
    • 读入100M数据至内存,用常规方式(如堆排序)排序;
    • 将排序后的数据写入磁盘;
    • 重复前两个步骤,9个100M的块(临时文件);
    • 将100M内存划为10份,前9份为输入缓冲区,第10份为输出缓冲区;
      • 例如前9份个8M,第10份28M,第10份为输出缓冲区;
    • 执行九路归并算法,将结果输出到输出缓冲区:
      • 若输出缓冲区满,将数据写至目标文件,清空缓冲区;
      • 若输入缓冲区空,读入相应文件的下一份数据;

快速排序

算法分析

  • 快速排序是一种基于划分的排序算法
  • 划分Partitioning:选取待排序集合A中的某个元素t,按照与t的大小关系重新整理A中的元素,使得整理后的序列中所有在t以前出现的元素均小于t,而所有在t以后出现的元素均大于t,元素t称为划分元素;
  • 快速排序通过反复的对A进行划分达到排序的目的;
  • 例如对于数组 a [ 0... n 1 ]
    • 设置两个变量 i = 0 j = n 1
    • a [ 0 ] 作为关键数据,即 k e y = a [ 0 ]
    • j 开始向前搜索,直到找到第一个小于 k e y 的值 a [ j ] ,将 a [ i ] = a [ j ]
    • i 开始向后搜索,直到找到第一个大于等于 k e y 的值 a [ i ] ,将 a [ j ] = a [ i ]
    • 重复第三、第四步,直到 i j

代码实现

    private static void qucikSort(int[] array) {
        quickSortInner(array, 0, array.length-1);
    }

    private static void quickSortInner(int[] array, int from, int to) {
        if (to - from < 10) {
            // 使用冒泡或排序进行排序
        }

        int key = selectKey(array[from], array[to], array[(from+to)/2]);
        int index = patition(array, key, from, to);
        quickSortInner(array, from, index);
        quickSortInner(array, index+1, to);
    }

    private static int patition(int[] array, int key, int from, int to) {
        int tmp;
        while (from < to) {
            while (from < to && array[from] < key) {
                from++;
            }
            while (from < to && array[to] > key) {
                to--;
            }

            tmp = array[from];
            array[from] = array[to];
            array[to] = tmp;
        }
        return from;
    }

时间复杂度分析

  • 在最好的情况下,每次运行一次分区,我们会把一个数列分为两个几近相等的片段,然后,递归调用两个一半大小的数列;
  • 一次分区中, i j 一共遍历了 n 个数,即 O ( n ) ,记快速排序的时间复杂度为 T ( n ) ,则:
    T ( n ) = 2 T ( n 2 ) + c n ( c 是常数)

    因此 T ( n ) = O ( n l o g 2 n ) ;
  • 在最坏的情况下,两个子数组的长度为1和 n 1 ,因此 T ( n ) = T ( 1 ) + T ( n 1 ) + c n ,计算得到 T ( n ) = O ( n 2 )

堆排序

堆的相关定义

  • 堆定义:对于一颗完全二叉树,若树种任一非叶子节点的关键字均不大于(或不小于)其左右孩子(若存在)节点的关键字,则这颗二叉树叫做小顶堆(大顶堆);
  • 完全二叉树可以用数组来存储,对于长度为 n 的数组 a [ 0... n 1 ] ,若:
    • 0 i n 1 a [ i ] a [ 2 i + 1 ] a [ i ] a [ 2 i + 2 ] ,则a表示一个小顶堆;
  • 小顶堆的堆顶元素是最小的,大顶堆的堆顶元素是最大的;
  • 堆的存储和树型表示如下图所示:
    这里写图片描述 这里写图片描述

    对应的数组分别为 [ 16 , 14 , 10 , 8 , 7 , 9 , 3 , 3 , 2 , 4 , 1 ] [ 9 , 8 , 3 , 4 , 7 , 1 , 2 ]
  • 数组中索引为 k 的节点的孩子节点是索引为 2 k + 1 2 k + 2 (如果存在)的节点,而对于 k 的父节点来说:
    • k 为左孩子,则 k 的父节点为 k 2
    • k 为右孩子,则 k 的父节点为 k 2 1 ;
      左孩子和右孩子计算父节点的公式不一样,但我们发现:
    • k 为左孩子,则 k 为奇数,则 ( ( k + 1 ) 2 1 ) k 2 相等;
    • k 为右孩子,则 k 为偶数,则 ( ( k + 1 ) 2 1 ) 2 2 1 ;
  • 因此:若待考察的节点为 k ,记 k + 1 K ,则 k 的父节点为 K 2 1 ;

堆排序过程

  1. 初始化操作:将 a [ 0... n 1 ] 构造为堆(如大顶堆);
  2. 对于第 i 趟排序:将堆顶记录 a [ 0 ] a [ n i ] 交换,然后将 a [ 0... n i 1 ] 调整为堆(即重建大顶堆);
  3. 进行 n 1 趟,完成排序;
  4. 如下图所示展示了堆排序过程:
    这里写图片描述

代码实现

private static void headSort(int[] array) {
        for (int i = array.length/2-1; i >= 0; i--) {
            adjustHeap(array, i, array.length);
        }
        for (int j = array.length-1; j >= 0; j--) {
            int tmp = array[0];
            array[0] = array[j];
            array[j] = tmp;
            adjustHeap(array, 0, j);
        }
    }

    /**
     * 调整大顶堆
     * @param array
     * @param adjustIndex
     */
    private static void adjustHeap(int[] array, int adjustIndex, int length) {
        int tmp = array[adjustIndex];
        int k = 2 * adjustIndex + 1;
        for (; k < length; k = 2 * k +1) { // 从左子节点开始
            if (array[k] < array[k+1]) { // 左子节点小于右子节点
                k++;
            }
            if (tmp < array[k]) { // 找到比tmp更大的子节点
                array[adjustIndex] = array[k];
                adjustIndex = k;
            } else {
                break;
            }
        }
        array[adjustIndex] = tmp;
    }

时间复杂度

调整堆的时间复杂度为 l o g 2 n ,将所有元素排好序需要调整 n 次,因此总的时间复杂度为 O ( n l o g 2 n )

基数排序

算法分析

基数排序(Radix Sort)是桶排序的扩展,它的基本思想是:将整数按位数切割成不同的数字,然后按每个位数分别比较。具体做法是:将所有待比较数值统一为同样的数位长度,数位较短的数前面补零。然后,从最低位开始,依次进行一次排序。这样从最低位排序一直到最高位排序完成以后, 数列就变成一个有序序列。

使用基数排序对数组{53, 3, 542, 748, 14, 214, 154, 63, 616}进行排序的过程如下图所示:

这里写图片描述

在上图中,首先将所有待比较树脂统一为统一位数长度,接着从最低位开始,依次进行排序:

  1. 按照个位数进行排序;
  2. 按照十位数进行排序;
  3. 按照百位数进行排序;

排序后,数列就变成了一个有序序列。

代码实现

    /**
     * 对数组按照"某个位数"进行排序(桶排序)
     *  例如,对于数组a={50, 3, 542, 745, 2014, 154, 63, 616};
     *      当exp=1表示按照"个位"对数组a进行排序
     *      当exp=10表示按照"十位"对数组a进行排序
     *      当exp=100表示按照"百位"对数组a进行排序
     * @param array 待排序数组
     * @param exp 指数,对数组array按照该指数进行排序
     */
    private static void countSort(int[] array, int exp) {
        int length = array.length;
        int[] output = new int[length];
        int[] buckets = new int[]{0, 0, 0, 0, 0, 0, 0, 0, 0, 0};
        int i;

        // 将数据出现的次数存储在buckets[]中
        for (i = 0; i < length; i++) {
            buckets[array[i]/exp-1]++;
        }

        // 调整buckets[i]。目的是让更改后的buckets[i]的值,是该数据在output[]中的位置
        for (i = 1; i < length; i++) {
            buckets[i] += buckets[i-1];
        }

        // 将数据存储到临时数组output[]中
        for (i = length-1; i >= 0; i--) {
            output[buckets[(array[i]/exp)%10] - 1] = array[i];
            buckets[(array[i]/exp)%10]--;
        }

        // 将排序好的数据赋值给array[]
        for (i = 0; i < length; i++) {
            array[i] = output[i];
        }
    }

    private static void radixSort(int[] array) {
        int exp; // 指数,当对数组按个位进行排序时,exp=1;按10位进行排序时,exp=10
        int max = getMax(array);
        for (exp = 1; max/exp > 0; exp *= 10) {
            countSort(array, exp);
        }
    }

radixSort(array)的作用是对数组array进行排序:

  1. 首先通过getMax(array)获取数组array中的最大值,获取最大值的目的是计算出数组a的最大指数;
  2. 获取到数组array中的最大指数之后,再从指数1开始,根据位数对数组array中的元素进行排序,排序的时候采用了桶排序;
  3. countSort(array, exp)的作用是对数组array按照指数exp进行排序;

下面简单介绍一下对数组{53, 3, 542, 748, 14, 214, 154, 63, 616}按个位数进行排序的流程:

  1. 个位的数值范围是[0,10)。因此,参见桶数组buckets[],将数组按照个位数值添加到桶中:
    这里写图片描述
  2. 接着是根据桶数组buckets[]来进行排序,假设将排序后的数组存在output[]中;找出output[]和buckets[]之间的联系就可以对数据进行排序了:
    这里写图片描述

猜你喜欢

转载自blog.csdn.net/zkp_java/article/details/80870370