【十八】常见十种排序算法总结笔记

一、排序算法简介

1.排序算法分类

1.比较类排序

在排序的最终结果里,元素之间的次序依赖于它们之间的比较。每个数都必须和其他数进行比较,才能确定自己的位置 。

在冒泡排序之类的排序中,问题规模为n,又因为需要比较n次,所以平均时间复杂度为O(n²)。在归并排序、快速排序之类的排序中,问题规模通过分治法消减为logN次,所以时间复杂度平均O(nlogn)。

比较排序的优势是,适用于各种规模的数据,也不在乎数据的分布,都能进行排序。可以说,比较排序适用于一切需要排序的情况。

  • 插入排序:直接插入排序、希尔排序
  • 选择排序:直接选择排序、堆排序 
  • 交换排序:冒泡排序法、快速排序法
  • 归并排序

2.非比较类排序

非比较排序是通过确定每个元素之前,应该有多少个元素来排序。针对数组arr,计算arr[i]之前有多少个元素,则唯一确定了arr[i]在排序后数组中的位置 。

非比较排序只要确定每个元素之前的已有的元素个数即可,所有一次遍历即可解决。算法时间复杂度O(n)。

非比较排序时间复杂度底,但由于非比较排序需要占用空间来确定唯一位置。所以对数据规模和数据分布有一定的要求。

  • 基数排序 
  • 计数排序
  • 桶排序

2.术语说明

  • 稳定 :如果a原本在b前面,而a=b,排序之后a仍然在b的前面;
  • 不稳定 :如果a原本在b的前面,而a=b,排序之后a可能会出现在b的后面;
  • 内排序 :所有排序操作都在内存中完成;
  • 外排序 :由于数据太大,因此把数据放在磁盘中,而排序通过磁盘和内存的数据传输才能进行;
  • 时间复杂度 : 一个算法执行所耗费的时间。
  • 空间复杂度 :运行完一个程序所需内存的大小。

3 算法复杂度

图片名词解释:

  • n: 数据规模
  • k: “桶”的个数
  • In-place: 占用常数内存,不占用额外内存
  • Out-place: 占用额外内存

二、插入排序 

算法基本思想:

从初始有序的子集合开始,不断地把新的数据元素插入到已排列有序子集合的合适位置上,使子集合中数据元素的个数不断增多,当子集合等于集合时,插入排序算法结束。

常用的插入排序算法:

1.直接插入排序

2.希尔排序

2.1直接插入排序

 算法基本思想:

1.顺序地把待排序的数据元素按其值的大小插入到已排序数据元素子集合的适当位置。

2.子集合的数据元素个数从只有一个开始逐次增加。

3.当子集合大小最终和集合大小相同时排序完毕。

实现步骤:

  1. 从第一个元素开始,该元素可以认为已经被排序;
  2. 取出下一个元素,在已经排序的元素序列中从后向前扫描;
  3. 如果该元素(已排序)大于新元素,将该元素移到下一位置;
  4. 重复步骤3,直到找到已排序的元素小于或者等于新元素的位置;
  5. 将新元素插入到该位置后;
  6. 重复步骤2~5。

 实现代码:

public static int[] insertionSort(int[] arr){
        int length = arr.length;
        for(int i = 1 ; i < length ; i++){
            int current = arr[i];
            int preIndex = i - 1;
            while (preIndex >= 0 && arr[preIndex] > current){
                arr[preIndex+1] = arr[preIndex];
                preIndex--;
            }
            arr[preIndex+1] = current;
        }
        return arr;
    }

算法分析:

插入排序在实现上,通常采用in-place排序(即只需用到O(1)的额外空间的排序),因而在从后向前扫描过程中,需要反复把已排序元素逐步向后挪位,为最新元素提供插入空间。

2.2 希尔排序

1959年Shell发明,第一个突破O(n2)的排序算法,是直接插入排序的改进版。它与直接插入排序的不同之处在于,它会优先比较距离较远的元素。希尔排序又叫缩小增量排序

算法基本思想:

1. 把待排序的数据元素分成若干个小组,对同一小组内的数据元素用直接插入法排序。

2.小组的个数逐次减少,当完成了所有元素都在一个组内的排序后排序过程结束。

实现步骤:

  1. 选择一个增量序列t1,t2,…,tk,其中ti>tj,tk=1;
  2. 按增量序列个数k,对序列进行k 趟排序;
  3. 每趟排序,根据对应的增量ti,将待排序列分割成若干长度为m 的子序列,分别对各子表进行直接插入排序。仅增量因子为1 时,整个序列作为一个表来处理,表长度即为整个序列的长度。

第一次步长是:arr.len / 2 = s1

这样会有s1个待排序数据,对他们进行插入排序

第二次步长是 : s1 / 2 = s2

这样会有s2个待排序数据,对他们进行插入排序

第三次步长是: s2 / 2 = s3

这样会有s3个待排序数据,对他们进行插入排序

一直到步长为1。 

这样会有1个待排序数据,对他们进行插入排序

 实现代码:

public static int[] shellSort(int[] ins){
        int n = ins.length;
        int gap = n/2;
        while(gap > 0){
            for(int j = gap; j < n; j++){
                int i=j;
                while(i >= gap && ins[i-gap] > ins[i]){
                    int temp = ins[i-gap]+ins[i];
                    ins[i-gap] = temp-ins[i-gap];
                    ins[i] = temp-ins[i-gap];
                    i -= gap;
                }
            }
            gap = gap/2;
        }
        return ins;
    }

算法分析:

希尔排序的核心在于间隔序列的设定。既可以提前设定好间隔序列,也可以动态的定义间隔序列。动态定义间隔序列的算法是《算法(第4版)》的合著者Robert Sedgewick提出的。

三、选择排序

算法基本思想:

1.每次从待排序的数据元素集合中选取最小(或最大)的数据元素放到数据元素集合的最前(或最后),数据元素集合不断缩小,当数据元素集合为空时排序过程结束。

常用的选择排序

1.直接选择排序

2.堆排序 

3.1直接选择排序

算法基本思想:

1.从待排序的数据元素集合中选取最小的数据元素,将它与原始数据元素集合中的第一个数据元素交换位置。

2.从不包括第一个位置上数据元素的集合中选取最小的数据源,将它与原始数据元素集合中的第二个数据元素交换位置。

从不包括第三个....选取最小的....将它与.......第三个数据元素交换位置....如此重复,直到数据元素集合中只剩一个数据元素为止。

实现代码:

public static int[] selectionSort(int[] arr){
        int length = arr.length;
        int minIndex;
        for(int i = 0 ; i < length - 1 ; i++){
            minIndex = i;
            
            // 找出最小的数
            for(int j = i+1; j <length ; j++){
                if(arr[j] < arr[minIndex]){  
                    // 保存最小的数的下标
                    minIndex = j;
                }
            }
            
            // 交换位置
            int temp  = arr[i];
            arr[i] = arr[minIndex];
            arr[minIndex] = temp;
        }
        return arr;
    }

算法分析:

表现最稳定的排序算法之一,因为无论什么数据进去都是O(n2)的时间复杂度,所以用到它的时候,数据规模越小越好。唯一的好处可能就是不占用额外的内存空间了吧。理论上讲,选择排序可能也是平时排序一般人想到的最多的排序方法了吧。

3.2堆排序

大顶堆:每个节点的值都大于或等于其子节点的值,在堆排序算法中用于升序排列。

小顶堆:每个节点的值都小于或等于其子节点的值,在堆排序算法中用于降序排列。

从这里我们可以得出以下性质(重点)

对于大顶堆:arr[i] >= arr[2i + 1] && arr[i] >= arr[2i + 2]

对于小顶堆:arr[i] <= arr[2i + 1] && arr[i] <= arr[2i + 2]

算法基本思想:

1、将带排序的序列构造成一个大顶堆,根据大顶堆的性质,当前堆的根节点(堆顶)就是序列中最大的元素;

2、将堆顶元素和最后一个元素交换,然后将剩下的节点重新构造成一个大顶堆;

3、重复步骤2,如此反复,从第一次构建大顶堆开始,每一次构建,我们都能获得一个序列的最大值,然后把它放到大顶堆的尾部。

最后,就得到一个有序的序列了。

实现步骤:

1. 构造初始堆。将给定无序序列构造成一个大顶堆(一般升序采用大顶堆,降序采用小顶堆)。

假设给定无序序列结构如下

arr[13,60,65,10,35,20,30,91,65,31,77,96,22,81,46]

2.构建大顶堆。从最后一个非叶子结点开始,从左至右,从下至上进行调整。如此重复直到构建成一个大顶堆。 

如何确定最后一个非叶子节点?

对于一个完全二叉树,在填满的情况下(非叶子节点都有两个子节点),每一层的元素个数是上一层的二倍,根节点数量是1,所以最后一层的节点数量,一定是之前所有层节点总数+1,所以,我们能找到最后一层的第一个节点的索引,即(arr.length/2)节点总数/2(根节点索引为0),也就是说最后一个非叶子节点的索引就是第一个叶子结点的索引-1。所以最后一个非叶子节点的索引就是arr.length / 2 -1。15/2-1=6,也就是上图的46结点。

找到了最后一个非叶子节点,也就是元素值为46,比较它的左右节点的值,是否比他大,如果大就换位置。由于它的左叶子节点81>46,所以交换位子,交换后如下图

序号为6(arr.length/2-1)的最后一个非叶子节点已经调整完了,接着调整下一个“最后一个非叶子节点”既是arr.length/2-1-1,也就是原来的6-1=5.也就是元素值为65的节点。发现65>77,即是65的右叶子节点大于它自己所以需要调整,交换位子后如下图:

接着调整序号为4的非叶子节点,发现它大于自己的左右叶子节点,所以不用调整。

接着调整序号为3的非叶子节点,发现它的左叶子节点为65,65>13,所以交换位子,交换后图下图:

接着调整序号为2的非叶子节点,发现它大于自己的左右叶子节点,所以不用调整。

接着调整序号为1的非叶子节点,发现它的左叶子节点为65,65>60,所以交换位子,交换后图下图:

接着调整序号为0的非叶子节点,发现它的右叶子节点为96,96>90,所以交换位子,交换后图下图:

此时第一次的大顶堆构建完成,整个大顶堆中最大的数字就是根节点的96

3. 将堆顶元素和最后一个元素交换

4.把剩下的节点重复构建大顶堆,然后将堆顶元素和最后一个元素交换

所谓剩下的节点就是下图中红框部分,非红框部分是已经排好序了的,不要动了:

实现代码:

package com.sid.algorithm;

public class HeapSort2 {
    public static void heapSort(int[] arr) {
        if (arr == null || arr.length == 0) {
            return;
        }
        int len = arr.length;
        // 构建大顶堆,这里其实就是把待排序序列,变成一个大顶堆结构的数组
        buildMaxHeap(arr, len);

        // 交换堆顶和当前末尾的节点,重置大顶堆
        for (int i = len - 1; i > 0; i--) {
            swap(arr, 0, i);
            len--;
            heapify(arr, 0, len);
        }
    }

    private static void buildMaxHeap(int[] arr, int len) {
        // 从最后一个非叶节点开始向前遍历,调整节点性质,使之成为大顶堆
        for (int i = (int)Math.floor(len / 2) - 1; i >= 0; i--) {
            heapify(arr, i, len);
        }
    }

    private static void heapify(int[] arr, int i, int len) {
        // 先根据堆性质,找出它左右节点的索引
        int left = 2 * i + 1;
        int right = 2 * i + 2;
        // 默认当前节点(父节点)是最大值。
        int largestIndex = i;
        if (left < len && arr[left] > arr[largestIndex]) {
            // 如果有左节点,并且左节点的值更大,更新最大值的索引
            largestIndex = left;
        }
        if (right < len && arr[right] > arr[largestIndex]) {
            // 如果有右节点,并且右节点的值更大,更新最大值的索引
            largestIndex = right;
        }

        if (largestIndex != i) {
            // 如果最大值不是当前非叶子节点的值,那么就把当前节点和最大值的子节点值互换
            swap(arr, i, largestIndex);
            // 因为互换之后,子节点的值变了,如果该子节点也有自己的子节点,仍需要再次调整。
            heapify(arr, largestIndex, len);
        }
    }

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

算法分析:

1.因为堆排序无关乎初始序列是否已经排序已经排序的状态,始终有两部分过程,构建初始的大顶堆的过程时间复杂度为O(n),交换及重建大顶堆的过程中,需要交换n-1次,重建大顶堆的过程根据完全二叉树的性质,[log2(n-1),log2(n-2)...1]逐步递减,近似为nlogn。所以它最好和最坏的情况时间复杂度都是O(nlogn)

2.空间复杂度O(1)。

四、交换排序

算法基本思想:

利用交换数据元素的位置进行排序的方法称为交换排序。

常用的交换排序:

1.冒泡排序法

2.快速排序法

4.1 冒泡排序

算法基本思想:

它重复地走访过要排序的数列,一次比较两个元素,如果它们的顺序错误就把它们交换过来。

这个算法的名字由来是因为越小的元素会经由交换慢慢“浮”到数列的顶端。

实现步骤:

  1. 比较相邻的元素。如果第一个比第二个大,就交换它们两个;
  2. 对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对,这样在最后的元素应该会是最大的数;
  3. 针对所有的元素重复以上的步骤,除了最后一个;
  4. 重复步骤1~3,直到排序完成

实现代码:

public static int[] bubbleSort(int[] arr){
        int length = arr.length;
        for(int i = 0 ; i < length - 1; i++){
            for(int j = 0 ; j < length - 1 - i ; j ++){
                if(arr[j+1] > arr[j]){
                    int tmp = arr[j];
                    arr[j] = arr[j+1];
                    arr[j+1] = tmp;
                }
            }
        }
        return arr;
    }

4.2 快速排序

参考:https://wiki.jikexueyuan.com/project/easy-learn-algorithm/fast-sort.html

算法基本思想:

通过一趟排序将待排记录分隔成独立的两部分,其中一部分记录的关键字均比另一部分的关键字小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序系列。

例子:6 1 2 7 9 3 4 5 10 8

选基准为6

分别从初始队列的两端开始探测。先从找一个小于 6 的数,再从找一个大于 6 的数,然后交换他们。这里可以用两个变量 i 和 j,分别指向序列最左边和最右边

首先哨兵 j 开始出动。因为此处设置的基准数是最左边的数,所以需要让哨兵 j 先出动,这一点非常重要(请自己想一想为什么)。哨兵 j 一步一步地向左挪动(即 j--),直到找到一个小于 6 的数停下来。接下来哨兵 i 再一步一步向右挪动(即 i++),直到找到一个数大于 6 的数停下来。最后哨兵 j 停在了数字 5 面前,哨兵 i 停在了数字 7 面前。 

现在交换哨兵 i 和哨兵 j 所指向的元素的值。交换之后的序列如下。

6 1 2 5 9 3 4 7 10 8

到此,第一次交换结束。接下来开始哨兵 j 继续向左挪动(再友情提醒,每次必须是哨兵 j 先出发)。他发现了 4(比基准数 6 要小,满足要求)之后停了下来。哨兵 i 也继续向右挪动的,他发现了 9(比基准数 6 要大,满足要求)之后停了下来。此时再次进行交换,交换之后的序列如下。

6 1 2 5 4 3 9 7 10 8

第二次交换结束,“探测”继续。哨兵 j 继续向左挪动,他发现了 3(比基准数 6 要小,满足要求)之后又停了下来。哨兵 i 继续向右移动,糟啦!此时哨兵 i 和哨兵 j 相遇了,哨兵 i 和哨兵 j 都走到 3 面前。说明此时“探测”结束。我们将基准数 6 和 3 进行交换。交换之后的序列如下。

3 1 2 5 4 6 9 7 10 8

到此第一轮“探测”真正结束。此时以基准数 6 为分界点,6 左边的数都小于等于 66 右边的数都大于等于 6

现在基准数 6 已经归位,它正好处在序列的第 6 位。此时我们已经将原来的序列,以 6 为分界点拆分成了两个序列,左边的序列是“3 1 2 5 4”,右边的序列是“ 9 7 10 8 ”

左边的序列是“3 1 2 5 4”。请将这个序列以 3 为基准数进行调整,使得 3 左边的数都小于等于 33 右边的数都大于等于 3

调整完毕之后的序列的顺序应该是。

2 1 3 5 4

现在 3 已经归位。接下来需要处理 3 左边的序列“ 2 1 ”和右边的序列“5 4”。对序列“ 2 1 ”以 2 为基准数进行调整,处理完毕之后的序列为“1 2”,到此 2 已经归位。序列“1”只有一个数,也不需要进行任何处理。至此我们对序列“ 2 1 ”已全部处理完毕,得到序列是“1 2”。序列“5 4”的处理也仿照此方法,最后得到的序列如下。

1 2 3 4 5 6 9 7 10 8

对于序列“9 7 10 8”也模拟刚才的过程,直到不可拆分出新的子序列为止。最终将会得到这样的序列,如下。

1 2 3 4 5 6 7 8 9 10

实现步骤:

  1. 从数列中挑出一个元素,称为 “基准”(pivot);
  2. 重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。在这个分区退出之后,该基准就处于数列的中间位置。这个称为分区(partition)操作;
  3. 递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序。

实现代码:

public static void main(String[] args) {
        int[] arr = {15,96,16,13,95,8,5,78,64,3,20,9,1,55};
         quickSort1(arr, 0, arr.length - 1);
       for(int i = 0 ; i < arr.length ; i++){
           System.out.println(arr[i]);
       }
    }
    
public static void quickSort1(int[] arr,int left,int right){
        if(left >= right)
            return ;
        int pivot = arr[left]; //基准
        int i = left; // 左边从左往右扫
        int j = right; // 右边从右往左扫
        while (i < j){
            while(i < j && arr[j] >= pivot){
                j -- ;
            }
            while(i < j && arr[i] <= pivot ){
                i ++;
            }
            if(i < j)//交换两个数在数组中的位置
            {
                int tmp = arr[i];
                arr[i] = arr[j];
                arr[j] = tmp;
            }
        }
        //最终将基准数归位
        arr[left] = arr[i];
        arr[i] = pivot;

        quickSort1(arr,left, i-1);//继续处理左边的,这里是一个递归的过程
        quickSort1(arr,i+1, right);//继续处理右边的 ,这里是一个递归的过程
    }

五、归并排序

算法基本思想:

该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。

常用归并排序:

1.二路归并排序

5.1 二路归并排序

算法基本思想:

若将两个有序表合并成一个有序表,称为2-路归并。 

实现步骤:

  1. 把长度为n的输入序列分成两个长度为n/2的子序列;
  2. 对这两个子序列分别采用归并排序;
  3. 将两个排序好的子序列合并成一个最终的排序序列。

实现代码:

    public static void mergeSort(int[] arr, int left, int right) {
        if(null == arr) {
            return;
        }

        if(left < right) {
            //找中间位置进行划分
            int mid = (left+right)/2;
            //对左子序列进行递归归并排序
            mergeSort(arr, left, mid);
            //对右子序列进行递归归并排序
            mergeSort(arr, mid+1, right);
            //“合”。 进行归并
            merge(arr, left, mid, right);
        }
    }

    /**
     * 进行归并
     * @param arr
     * @param left
     * @param mid
     * @param right
     */
    private static void merge(int[] arr, int left, int mid, int right) {
        int[] tempArr = new int[arr.length];
        int leftStart = left;
        int rightStart = mid+1;
        int tempIndex = left;

        while(leftStart <= mid && rightStart <= right) {
            if(arr[leftStart] < arr[rightStart]) {
                tempArr[tempIndex++] = arr[leftStart++];
            } else {
                tempArr[tempIndex++] = arr[rightStart++];
            }
        }

        while(leftStart <= mid) {
            tempArr[tempIndex++] = arr[leftStart++];
        }

        while(rightStart <= right) {
            tempArr[tempIndex++] = arr[rightStart++];
        }

        while(left <= right) {
            arr[left] = tempArr[left++];
        }
    }

算法分析:

归并排序是一种稳定的排序方法。和选择排序一样,归并排序的性能不受输入数据的影响,但表现比选择排序好的多,因为始终都是O(nlogn)的时间复杂度。代价是需要额外的内存空间。

六、基数排序

算法基本思想:

基数排序是按照低位先排序,然后收集;再按照高位排序,然后再收集;依次类推,直到最高位。有时候有些属性是有优先级顺序的,先按低优先级排序,再按高优先级排序。

基数排序是把每一个元素拆成多个关键字,一个关键字可以在每一个元素上同等的位置进行计数排序,一个元素拆成多个关键字可以看作是要进行几轮分桶,以一个元素最长的长度为准。

基数排序的思想是将待排序序列中的每组关键字进行桶排序。例如整数序列[103, 9, 1,7,11,15, 25, 201, 209, 107, 5]上每个位、十位和百位上的数字看成是一个关键字。

基数排序有两种排序方式:LSD和MSD,最小位优先(从右边开始)和最大位优先(从左边开始)

字符串排序中也有低位优先和高位优先排序。

实现步骤:

以LSD为例,原始数组为[103, 9, 1,7,11,15, 25, 201, 209, 107, 5]

1.设定10个桶,分别存放位数为0-9的元素。

注意,这里是数字排序,所以设定10个桶。其他情况下设定多少个桶可以用hash算法计算,发生哈希碰撞的就在一个桶里面

2.遍历初始序列,将元素放入不同的桶中

3.按照桶的顺序,把桶中元素全部拿出来重新组装成一个序列。

4.重复2、3步骤,直到最高位。即可完成排序

 

实现代码:

package com.sid.algorithm;

import java.util.ArrayList;

public class RadixSort2 {
    /**
     * 基数排序
     * @param array
     * @return
     */
    public static int[] RadixSort(int[] array,int 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;
    }
}

算法分析:

基数排序基于分别排序,分别收集,所以是稳定的。但基数排序的性能比桶排序要略差,每一次关键字的桶分配都需要O(n)的时间复杂度,而且分配之后得到新的关键字序列又需要O(n)的时间复杂度。假如待排数据可以分为d个关键字,则基数排序的时间复杂度将是O(d*2n) ,当然d要远远小于n,因此基本上还是线性级别的。

基数排序的空间复杂度为O(n+k),其中k为桶的数量。一般来说n>>k,因此额外空间需要大概n个左右。

七、计数排序

这个参考:一文弄懂计数排序算法! - 程序员小川 - 博客园 我觉得讲得挺好 

另外,橘黄色封面那本外国人写的《算法》第四版中,第五章字符串,5.1.1键索引计数法,就是计数排序的一种,思想跟这是类似的,实现上有细微差别。 

算法基本思想:

计数排序 的核心在于将输入的数据值转化为键存储在额外开辟的数组空间中。 作为一种线性时间复杂度的排序,计数排序要求输入的数据必须是有确定范围的整数。

实现步骤:

以数组A = {101,109,107,103,108,102,103,110,107,103}为例。

第一步:找出数组中的最大值max、最小值min

第二步:创建一个新数组count,其长度是max-min加1,其元素默认值都为0。

第三步:遍历原数组中的元素,以原数组中的元素作为count数组的索引,以原数组中的元素出现次数作为count数组的元素值。

第四步:对count数组变形新元素的值是前面元素累加之和的值,即count[i+1] = count[i+1] + count[i];

第五步:创建结果数组result,长度和原始数组一样。

第六步:遍历原始数组中的元素,当前元素A[j]减去最小值min,作为索引,在计数数组中找到对应的元素值count[A[j]-min],再将count[A[j]-min]的值减去1,就是A[j]在结果数组result中的位置,做完上述这些操作,count[A[j]-min]自减1。

是不是对第四步和第六步有疑问?为什么要这样操作?

第四步操作,是让计数数组count存储的元素值,等于原始数组中相应整数的最终排序位置,即计算原始数组中的每个数字在结果数组中处于的位置

比如索引值为9的count[9],它的元素值为10,而索引9对应的原始数组A中的元素为9+101=110(要补上最小值min,才能还原),即110在排序后的位置是第10位,即result[9] = 110,排完后count[9]的值需要减1,count[9]变为9。

再比如索引值为6的count[6],他的元素值为7,而索引6对应的原始数组A中的元素为6+101=107,即107在排序后的位置是第7位,即result[6] = 107,排完后count[6]的值需要减1,count[6]变为6。

如果索引值继续为6,在经过上一次的排序后,count[6]的值变成了6,即107在排序后的位置是第6位,即result[5] = 107,排完后count[6]的值需要减1,count[6]变为5。

至于第六步操作,就是为了找到A中的当前元素在结果数组result中排第几位,也就达到了排序的目的。

实现代码:

package com.sid.algorithm;

public class countSort3 {
    public int[] countSort(int[] A) {
        // 找出数组A中的最大值、最小值
        int max = Integer.MIN_VALUE;
        int min = Integer.MAX_VALUE;
        for (int num : A) {
            max = Math.max(max, num);
            min = Math.min(min, num);
        }
        // 初始化计数数组count
        // 长度为最大值减最小值加1
        int[] count = new int[max-min+1];
        // 对计数数组各元素赋值
        for (int num : A) {
            // A中的元素要减去最小值,再作为新索引
            count[num-min]++;
        }
        // 计数数组变形,新元素的值是前面元素累加之和的值
        for (int i=1; i<count.length; i++) {
            count[i] += count[i-1];
        }
        // 创建结果数组
        int[] result = new int[A.length];
        // 遍历A中的元素,填充到结果数组中去
        for (int j=0; j<A.length; j++) {
            result[count[A[j]-min]-1] = A[j];
            count[A[j]-min]--;
        }
        return result;
    }
}

算法分析:

计数排序是一个稳定的排序算法。当输入的元素是 n 个 0到 k 之间的整数时,时间复杂度是O(n+k),空间复杂度也是O(n+k),其排序速度快于任何比较排序算法。当k不是很大并且序列比较集中时,计数排序是一个很有效的排序算法。

八、桶排序

算法基本思想:

桶排序是计数排序的升级版。它利用了函数的映射关系,高效与否的关键就在于这个映射函数的确定。

桶排序 (Bucket sort)的工作的原理:假设输入数据服从均匀分布,将数据分到有限数量的桶里,每个桶再分别排序(有可能再使用别的排序算法或是以递归方式继续使用桶排序进行排)。

实现步骤:

步骤1:人为设置一个BucketSize,作为每个桶所能放置多少个不同数值(例如当BucketSize==5时,该桶可以存放{1,2,3,4,5}这几种数字,但是容量不限,即可以存放100个3);

步骤2:遍历输入数据,并且把数据一个一个放到对应的桶里去(这里放数据的方式就是计数排序放桶的方式,比如根据第一位数来放);

步骤3:对每个不是空的桶进行排序,可以使用其它排序方法,也可以递归使用桶排序;

步骤4:从不是空的桶里把排好序的数据拼接起来。 

注意,如果递归使用桶排序为各个桶排序,则当桶数量为1时要手动减小BucketSize增加下一循环桶的数量,否则会陷入死循环,导致内存溢出。

实现代码:

package com.sid.algorithm;

import java.util.ArrayList;

public class BucketSort {
    /**
     * 桶排序
     *
     * @param array
     * @param bucketSize
     * @return
     */
    public static ArrayList<Integer> BucketSort(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 < 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 = BucketSort(bucketArr.get(i), bucketSize);
                for (int j = 0; j < temp.size(); j++)
                    resultArr.add(temp.get(j));
            }
        }
        return resultArr;

    }
}

算法分析:

桶排序最好情况下使用线性时间O(n),桶排序的时间复杂度,取决与对各个桶之间数据进行排序的时间复杂度,因为其它部分的时间复杂度都为O(n)。很显然,桶划分的越小,各个桶之间的数据越少,排序所用的时间也会越少。但相应的空间消耗就会增大。 

  • 5
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 15
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 15
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值