经典排序算法(Java 实现)笔记

一、排序算法说明

查找和排序算法是算法的入门知识,其经典思想可以用于很多算法当中。因为其实现代码较短,应用较常见,所以在面试中经常会问到排序算法及其相关的问题。

但万变不离其宗,只要熟悉了思想,灵活运用也不是难事。一般在面试中最常考的是快速排序和归并排序,并且经常有面试官要求现场写出这两种排序的代码,对这两种排序的代码一定要信手拈来才行。

还有插入排序、冒泡排序、堆排序、基数排序、桶排序等。面试官对于这些排序可能会要求比较各自的优劣、各种算法的思想及其使用场景。

还有要会分析算法的时间和空间复杂度。通常查找和排序算法的考察是面试的开始,如果这些问题回答不好,估计面试官都没有继续面试下去的兴趣都没了。

所以想开个好头就要把常见的排序算法思想及其特点要熟练掌握,有必要时要熟练写出代码。

下面主要介绍经典排序算法。

1、排序的定义

对一序列对象根据某个关键字进行排序。

2、术语说明

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

3、算法总结

在这里插入图片描述

中文名称英文名称平均时间复杂度最坏时间复杂度最好时间复杂度空间复杂度稳定性
选择排序Selection1不稳
冒泡排序Bubblen1
插入排序Insertionn1
堆排序heapn log₂ nn log₂ nn log₂ n1不稳
希尔排序Shelln^1.3n1不稳
归并排序Mergen log₂ nn log₂ nn log₂ nn
快速排序Quickn log₂ nn log₂ nn log₂ n不稳
桶排序Bucketn + knn + k
计数排序Countingn + kn + kn + kn + k
基数排序Radixn * kn * kn * kn + k

4、算法分类

在这里插入图片描述

5、比较和非比较的区别

常见的快速排序归并排序堆排序冒泡排序等属于比较排序。在排序的最终结果里,元素之间的次序依赖于它们之间的比较。每个数都必须和其他数进行比较,才能确定自己的位置。

在冒泡排序之类的排序中,问题规模为n,又因为需要比较n次,所以平均时间复杂度为O(n²)。

在归并排序、快速排序之类的排序中,问题规模通过分治法消减为log₂ n次,所以时间复杂度平均O(n log₂ n)。

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

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

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

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

6、统一使用的测试代码

public static void main(String[] args) {
    int[] array = {3, 44, 38, 5, 47, 15, 36, 26, 27, 2, 46, 4, 19, 50, 48};
	// 只需要修改成对应的方法名就可以了
    静态方法名(array);

    System.out.println(Arrays.toString(array));
}

二、选择排序(Selection Sort)

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

选择排序(Selection-sort)是一种简单直观的排序算法。它的工作原理:首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置,然后,再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。以此类推,直到所有元素均排序完毕。

1、算法描述

n 个记录的直接选择排序可经过 n-1 趟直接选择排序得到有序结果。具体算法描述如下:

  • 初始状态:无序区为R[1…n],有序区为空;
  • 第i趟排序(i=1,2,3…n-1)开始时,当前有序区和无序区分别为R[1…i-1]和R(i…n)。该趟排序从当前无序区中选出关键字最小的记录 R[k],将它与无序区的第1个记录R交换,使R[1…i]和R[i+1…n)分别变为记录个数增加1个的新有序区和记录个数减少1个的新无序区;
  • n-1趟结束,数组有序化了。

2、动图演示

在这里插入图片描述

3、代码实现

/**
 * Description: 选择排序
 *
 * @param array
 * @return void
 */
public static void selectionSort(int[] array) {
    if (array == null || array.length <= 1) {
        return;
    }

    // 数组长度
    int length = array.length;

    // 1.外层循环控制比较轮数i
    for (int i = 0; i < length - 1; i++) {
        // 2.保存最小数的索引[初始为 i = 0 索引位置元素]
        int minIndex = i;

        // 3.从当前 i 索引位置开始
        for (int j = i + 1; j < length; j++) {
            // 4.找到最小的数[数组中 j 索引位置元素 小于 最小数索引]
            if (array[j] < array[minIndex]) {
                // 5.将当前 j 索引作为最小元素索引, 赋值给 minIndex
                minIndex = j;
            }
        }

        // 交换元素位置[判断当前内循环开始位置 i 是否就已经是最小值了]
        if (i != minIndex) {
            swap(array, minIndex, i);
        }
    }
}

/**
 * 交换元素位置
 *
 * @param array
 * @param a
 * @param b
 */
public static void swap(int[] array, int a, int b) {
    int temp = array[a];
    array[a] = array[b];
    array[b] = temp;
}

4、算法分析

最佳情况:T(n) = O(n²)

最差情况:T(n) = O(n²)

平均情况:T(n) = O(n²)

三、冒泡排序(Bubble Sort)

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

1、算法描述

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

2、动图演示

在这里插入图片描述

3、代码实现

/**
 * 冒泡排序
 *
 * @param array
 */
public static void bubbleSort(int[] array) {
    if (array == null || array.length <= 1) {
        return;
    }

    // 数组长度
    int length = array.length;

    // 1.外层循环控制比较轮数i
    for (int i = 0; i < length - 1; i++) {
        // 内层循环控制每一轮比较次数,每进行一轮排序都会找出一个较大值
        // (array.length - 1)防止索引越界, (array.length - 1 - i)减少比较次数,把最大的一个数排在了最右边(默认升序排)
        for (int j = 0; j < length - 1 - i; j++) {
            if (array[j] > array[j + 1]) {
                // 左侧数据比右侧数据大, 进行交换
                swap(array, j, j + 1);
            }
        }
    }

}

/**
 * 交换元素位置
 *
 * @param array
 * @param a
 * @param b
 */
public static void swap(int[] array, int a, int b) {
    int temp = array[a];
    array[a] = array[b];
    array[b] = temp;
}

4、算法分析

最佳情况:T(n) = O(n)

最差情况:T(n) = O(n²)

平均情况:T(n) = O(n²)

四、插入排序(Insertion Sort)

插入排序(Insertion-Sort)的算法描述是一种简单直观的排序算法。它的工作原理是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。插入排序在实现上,通常采用 in-place 序(即只需用到 O(1) 的额外空间的排序),因而在从后向前扫描过程中,需要反复把已排序元素逐步向后挪位,为最新元素提供插入空间。

1、算法描述

一般来说,插入排序都采用in-place在数组上实现。具体算法描述如下:

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

2、动图演示

在这里插入图片描述

3、代码实现

/**
 * 插入排序
 *
 * @param array
 */
public static void insertionSort(int[] array) {
    if (array == null || array.length <= 1) {
        return;
    }

    // 数组长度
    int length = array.length;

    // 1.外层循环控制比较轮数i, 直到循环到 i = length - 1, array[0] 默认为有序的
    for (int i = 1; i < length; i++) {
        // 从 i 位置, 从后到前循环, 进行比较
        for (int j = i; j > 0; j--) {
            // 如果当前位置元素小于紧邻的前面那个数, 则进行交换
            if (array[j] < array[j - 1]) {
                swap(array, j, j - 1);
            }
        }
    }
}

/**
 * 交换元素位置
 *
 * @param array
 * @param a
 * @param b
 */
public static void swap(int[] array, int a, int b) {
    int temp = array[a];
    array[a] = array[b];
    array[b] = temp;
}

去掉 swap 方法

/**
 * 插入排序
 *
 * @param array
 */
public static void insertionSort(int[] array) {
    if (array == null || array.length <= 1) {
        return;
    }

    // 数组长度
    int length = array.length;

    // 要插入的数
    int insertNum;

    // 1.外层循环控制比较轮数i, 直到循环到 i = length - 1, array[0] 默认为有序的
    for (int i = 1; i < length; i++) {
        // 将要排序的数赋值给 insertNum
        insertNum = array[i];

        int j = i;

        for (; j > 0 && array[j - 1] > insertNum; j--) {
            // 从 i 位置, 从后到前循环, 将大于 insertNum 的数向后移动一格
            array[j] = array[j - 1];
        }

        // j-- 后 array[j] = null 值, 直接插入
        array[j] = insertNum;
    }
}

4、算法分析

最佳情况:T(n) = O(n)

最坏情况:T(n) = O(n2)

平均情况:T(n) = O(n2)

五、希尔排序(Shell Sort)

希尔排序是希尔(Donald Shell)于 1959 年提出的一种排序算法。希尔排序也是一种插入排序,它是简单插入排序经过改进之后的一个更高效的版本,也称为缩小增量排序,同时该算法是冲破 O(n2)的第一批算法之一。它与插入排序的不同之处在于,它会优先比较距离较远的元素。希尔排序又叫缩小增量排序。

希尔排序是把记录按下表的一定增量分组,对每组使用直接插入排序算法排序;随着增量逐渐减少,每组包含的关键词越来越多,当增量减至 1 时,整个文件恰被分成一组,算法便终止。

1、算法描述

我们来看下希尔排序的基本步骤,在此我们选择增量 gap = length/2,缩小增量继续以 gap = gap/2 的方式,这种增量选择我们可以用一个序列来表示,{n/2,(n/2)/2…1},称为增量序列。希尔排序的增量序列的选择与证明是个数学难题,我们选择的这个增量序列是比较常用的,也是希尔建议的增量,称为希尔增量,但其实这个增量序列不是最优的。此处我们做示例使用希尔增量。

先将整个待排序的记录序列分割成为若干子序列分别进行直接插入排序,具体算法描述:

  • 选择一个增量序列t1,t2,…,tk,其中 ti > tj ,tk=1。

  • 按增量序列个数 k,对序列进行 k 趟排序。

  • 每趟排序,根据对应的增量 ti,将待排序列分割成若干长度为 m 的子序列,分别对各子表进行直接插入排序。仅增量因子为 1 时,整个序列作为一个表来处理,表长度即为整个序列的长度。

2、过程演示

在这里插入图片描述

3、代码实现

/**
 * 希尔排序
 *
 * @param array
 */
public static void shellSort(int[] array) {
    if (array == null || array.length <= 1) {
        return;
    }

    // 数组长度
    int length = array.length;

    // 间隔变化
    // 设置初始间隔为: 数据长度/2, 每次间隔缩小一倍
    for (int gap = length / 2; gap > 0; gap = gap / 2) {

        // 排序
        // 1.外层循环控制比较轮数 i, i = gap 间隔固定
        for (int i = gap; i < length; i++) {
            // 从 i 位置, 从后到前循环, 间隔,进行比较
            for (int j = i; j > gap - 1; j = j - gap) {
                // 如果当前位置元素小于 间隔之后 前面那个数, 则进行交换
                if (array[j] < array[j - gap]) {
                    swap(array, j, j - gap);
                }
            }
        }

    }
}

/**
 * 交换元素位置
 *
 * @param array
 * @param a
 * @param b
 */
public static void swap(int[] array, int a, int b) {
    int temp = array[a];
    array[a] = array[b];
    array[b] = temp;
}

Knuth 序列

  • h = 1
  • h = h * 3 + 1
/**
 * 希尔排序
 *
 * @param array
 */
public static void shellSort(int[] array) {
    if (array == null || array.length <= 1) {
        return;
    }

    // 数组长度
    int length = array.length;

    // Knuth
    int h = 1;
    while (h <= length / 3) {
        h = h * 3 + 1;
    }
    System.out.println(h);

    // 间隔变化 13 , 4, 1
    for (int gap = h; gap > 0; gap = (gap - 1) / 3) {
        // 排序
        // 1.外层循环控制比较轮数 i, i = gap 间隔固定
        for (int i = gap; i < length; i++) {
            // 从 i = gap 位置, 从后到前循环, 间隔进行比较
            for (int j = i; j > gap - 1; j = j - gap) {
                // 如果当前位置元素小于 间隔之后 前面那个数, 则进行交换
                if (array[j] < array[j - gap]) {
                    swap(array, j, j - gap);
                }
            }
        }
    }

}

/**
 * 交换元素位置
 *
 * @param array
 * @param a
 * @param b
 */
public static void swap(int[] array, int a, int b) {
    int temp = array[a];
    array[a] = array[b];
    array[b] = temp;
}

去掉 swap 方法

/**
 * 希尔排序
 *
 * @param array
 */
public static void shellSort(int[] array) {
    if (array == null || array.length <= 1) {
        return;
    }

    // 数组长度
    int length = array.length;

    // Knuth
    int h = 1;
    while (h <= length / 3) {
        h = h * 3 + 1;
    }
    System.out.println(h);

    // 间隔变化 13 , 4, 1
    for (int gap = h; gap > 0; gap = (gap - 1) / 3) {

        // 要插入的数
        int insertNum;

        // 排序
        // 1.外层循环控制比较轮数 i, i = gap 间隔固定
        for (int i = gap; i < length; i++) {

            // 将要排序的数赋值给 insertNum
            insertNum = array[i];

            // 从 i = gap 位置, 从后到前循环, 间隔进行比较
            int j = i;

            for (; j > gap - 1 && array[j - gap] > insertNum; j = j - gap) {
                // 从 gap 位置, 从后到前循环, 将大于 insertNum 的数向后移动一格
                array[j] = array[j - gap];
            }
            
            // j-- 后 array[gap] = null 值, 直接插入
            array[j] = insertNum;
        }
        
    }

}

4、算法分析

最佳情况:T(n) = O(nlog2 n)

最坏情况:T(n) = O(nlog2 n)

平均情况:T(n) =O(nlog2n)

六、归并排序(Merge Sort)

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

归并排序是建立在归并操作上的一种有效的排序算法。该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。归并排序是一种稳定的排序方法。将已有序的子序列合并,得到完全有序的序列,即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为 2 - 路归并

1、算法描述

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

2、动图演示

在这里插入图片描述

3、代码实现

/**
 * 归并排序
 *
 * @param array
 */
public static void mergeSort(int[] array) {
    if (array == null || array.length <= 1) {
        return;
    }
    // 数组长度
    int length = array.length;
    
	// 数组排序
    sort(array, 0, length - 1);
}

/**
 * 2.数组排序(递归)
 *
 * @param array
 * @param leftPtr    左指针: 数组起始位置(index)
 * @param rightBound 右边界: 数组结束位置(index)
 */
public static void sort(int[] array, int leftPtr, int rightBound) {
    if (leftPtr == rightBound) {
        return;
    }
    // 分成两半: == (leftPtr + rightBound) / 2, 这样写是防止超过 int Max.
    // mid : 左一半数组结束位置(中间位置)
    int mid = leftPtr + (rightBound - leftPtr) / 2;
    // 左边排序(递归)
    sort(array, leftPtr, mid);
    // 右边排序(递归)
    sort(array, mid + 1, rightBound);
    // 数组归并
    merge(array, leftPtr, mid + 1, rightBound);
}

/**
 * 1.数组归并(假定左右数组已经排好顺序)
 *
 * @param array
 * @param leftPrt    左指针: 左一半数组起始位置(index)
 * @param rightPrt   右指针: 右一半数组起始位置(index)
 * @param rightBound 右边界: 右一半数组结束位置(index)
 */
public static void merge(int[] array, int leftPrt, int rightPrt, int rightBound) {
    /** 给新数组 temp 分配空间 */
    int[] temp = new int[rightBound - leftPrt + 1];

    /** 左一半数组起始位置 */
    int i = leftPrt;
    /** 左一半数组结束位置(中间位置) */
    int mid = rightPrt - 1;
    /** 右一半数组起始位置 */
    int j = rightPrt;
    /** 新数组 temp 起始位置 */
    int k = 0;

    // 条件判断: 两个数组都没有遍历完
    while (i <= mid && j <= rightBound) {
        // 比较左右两部分的元素, 哪个小, 把那个元素填入 temp 中
        temp[k++] = (array[i] <= array[j]) ? array[i++] : array[j++];
        // k++, i++, j++: 移动数组下标
    }

    // 上面的循环退出后, 把剩余的元素依次填入到 temp 中, 以下两个 while 只有一个会执行
    // 条件判断: 前一半数组没有遍历完
    while (i <= mid) {
        temp[k++] = array[i++];
    }
    // 条件判断: 后一半数组没有遍历完
    while (j <= rightBound) {
        temp[k++] = array[j++];
    }

    // 把最终的排序的结果复制给原数组
    for (int m = 0; m < temp.length; m++) {
        array[leftPrt + m] = temp[m];
    }
}

4、算法分析

最佳情况:T(n) = O(n)

最差情况:T(n) = O(nlogn)

平均情况:T(n) = O(nlogn)

七、快速排序(Quick Sort)

快速排序 (单轴) 的基本思想:通过一趟排序将待排记录分隔成独立的两部分,其中一部分记录的关键字均比另一部分的关键字小,则可分别对这两部分记录继续进行排序,以达到整个序列有序。

了解:双轴快排

1、算法描述

快速排序使用分治法来把一个串(list)分为两个子串(sub-lists)。具体算法描述如下:

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

2、动图演示

在这里插入图片描述

3、代码实现

/**
 * 快速排序
 *
 * @param array
 */
public static void quickSort(int[] array) {
    if (array == null || array.length <= 1) {
        return;
    }
    // 数组长度
    int length = array.length;

    sort(array, 0, length - 1);
}

/**
 * 2.数组排序(递归)
 *
 * @param array
 * @param leftBound  左边界: 数组起始位置(index)
 * @param rightBound 右边界: 数组结束位置(index)
 */
public static void sort(int[] array, int leftBound, int rightBound) {
    if (leftBound >= rightBound) {
        return;
    }

    // 轴的位置
    int mid = partition(array, leftBound, rightBound);
    sort(array, leftBound, mid -1);
    sort(array, mid + 1, rightBound);
}

/**
 * 1.数组分区
 *
 * @param array
 * @param leftBound  左边界: 数组起始位置(index)
 * @param rightBound 右边界: 数组结束位置(index)
 * @return 基准(轴)的位置
 */
public static int partition(int[] array, int leftBound, int rightBound) {
    // 基准(选择数组最右侧): 分区在其左
    int pivot = array[rightBound];
    // 左侧指针指在 分区最左侧
    int leftPrt = leftBound;
    // 右侧指针指在 基准左侧第一个元素位置
    int rightPrt = rightBound - 1;

    // 遍历数组分区 左指针 < 右指针
    while (leftPrt <= rightPrt) {
        // (前提: 左指针一直在右指针左侧, 或相等)
        // 左指针 向右移动, 直到找到某个值大于 array[left] > pivot 基准
        while (leftPrt <= rightPrt && array[leftPrt] <= pivot) {
            // 数组元素 小于等于 基准 pivot
            leftPrt++;
        }

        // (前提: 左指针一直在右指针左侧, 或相等)
        // 右指针 向左移动, 直到找到某个值小于 array[right] <= pivot 基准
        while (leftPrt <= rightPrt && array[rightPrt] > pivot) {
            // 数组元素 大于等于 基准 pivot
            rightPrt--;
        }

        System.out.println("before swap: leftPrt = " + leftPrt + ", rightPrt = " + rightPrt);

        // 如果 左指针、右指针在找到元素后, 仍然 leftPrt < rightPrt
        if (leftPrt < rightPrt) {
            // 交换元素位置
            swap(array, leftPrt, rightPrt);
        }

        print(array);
    }
    // 交换基准到分区中间
    swap(array, leftPrt, rightBound);

    return leftPrt;
}

/**
 * 交换元素位置
 *
 * @param array
 * @param a
 * @param b
 */
public static void swap(int[] array, int a, int b) {
    int temp = array[a];
    array[a] = array[b];
    array[b] = temp;
}

/**
 * 打印数组
 *
 * @param array
 */
private static void print(int[] array) {
    for (int i = 0; i < array.length; i++) {
        System.out.print(array[i] + " ");
    }
    System.out.println();
}

4、算法分析

最佳情况:T(n) = O(nlogn)

最差情况:T(n) = O(n2)

平均情况:T(n) = O(nlogn)

八、计数排序(Counting Sort)

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

计数排序(Counting sort)是一种稳定的排序算法。计数排序使用一个额外的数组C,其中第 i 个元素是待排序数组 A 中值,等于 i 的元素的个数。然后根据数组 C 来将 A 中的元素排到正确的位置。它只能对整数进行排序

1、算法描述

  • 找出待排序的数组中最大和最小的元素。
  • 统计数组中每个值为 i 的元素出现的次数,存入数组 C 的第 i 项。
  • 对所有的计数累加(从 C 中的第一个元素开始,每一项 和 前一项相加)。
  • 反向填充目标数组:将每个元素 i 放在新数组的第 C(i) 项,每放一个元素就将 C(i) 减去 1。

2、动图演示

在这里插入图片描述

3、代码实现

/**
 * 计数排序
 *
 * @param array
 */
public static void countingSort(int[] array) {
    if (array == null || array.length <= 1) {
        return;
    }
    // 数组长度
    int length = array.length;

    // 最小值、最大值
    int min = array[0];
    int max = array[0];

    // 遍历原数组, 获取其最大、最小值
    for (int i = 0; i < length; i++) {
        // 取最小值
        if (min > array[i]) {
            min = array[i];
        }
        // 取最大值
        if (max < array[i]) {
            max = array[i];
        }
    }

    // 5 3 2
    // max = 5, min =2
    // 5-2 = 3 --> 2, 3, 4, 5
    // --> offset = 4 = 3 + 1

    // 最大最小元素之间范围 [min, max] 的长度
    int offset = max - min + 1;

    // 计数(桶)数组
    int[] count = new int[offset];

    System.out.println("before sort: min = " + min
                       + ", max = " + max
                       + ", offset(计数数组长度) = " + offset);

    // 遍历 原数组
    for (int i = 0; i < length; i++) {
        // 将原数组的 (元素值 - min) 作为 count 数组的 index,
        // 并将对应 index 元素的值 +1, 即计算该 index 出现的次数: count[index] + 1
        count[array[i] - min]++;
    }

    System.out.println(Arrays.toString(count));

    // 遍历计数数组 count[]
    for (int i = 0, j = 0; i < offset; i++) {
        // 只要 count[i] 元素值 大于 0
        while (count[i]-- > 0) {
            // 数据回写: 将 count[] 数组的 index + min 赋值给 array[j], 并 j++
            array[j++] = i + min;
        }
    }
}

保证稳定性

/**
 * 计数排序
 *
 * @param array
 */
public static int[] countingSort(int[] array) {

    if (array == null || array.length <= 1) {
        return array;
    }
    // 数组长度
    int length = array.length;

    // 最终数组
    int[] result = new int[length];

    // 最小值、最大值
    int min = array[0];
    int max = array[0];

    // 遍历原数组, 获取其最大、最小值
    for (int i = 0; i < length; i++) {
        // 取最小值
        if (min > array[i]) {
            min = array[i];
        }
        // 取最大值
        if (max < array[i]) {
            max = array[i];
        }
    }

    // 5 3 2
    // max = 5, min =2
    // 5-2 = 3 --> 2, 3, 4, 5
    // --> offset = 4 = 3 + 1

    // 最大最小元素之间范围 [min, max] 的长度
    int offset = max - min + 1;

    // 计数(桶)数组
    int[] count = new int[offset];

    System.out.println("before sort: min = " + min
                       + ", max = " + max
                       + ", offset(计数数组长度) = " + offset);

    // 遍历 原数组
    for (int i = 0; i < length; i++) {
        // 将原数组的 (元素值 - min) 作为 count 数组的 index,
        // 并将对应 index 元素的值 +1, 即计算该 index 出现的次数: count[index] + 1
        count[array[i] - min]++;
    }

    System.out.println(Arrays.toString(count));

    /**
     * 遍历计数数组 count[], 修改数据, 保持 数组稳定
     *
     * count 计数数组 原本结构
     * count = [3, 5, 2]
     *          0  1  2
     * 改变之后 -->>
     * count = [3, 8, 10]  --> count[1] = count[1] + count[0], ...
     */
    for (int i = 1; i < offset; i++) {
        count[i] = count[i] + count[i - 1];
    }

    System.out.println("count 改变之后 >> " + Arrays.toString(count));

    // 遍历原数组, 倒叙插入数据
    for (int i = length - 1; i >= 0; i--) {
        // 找到原数组的元素 array[i]
        // 找到 count[] 数组对应 array[i] 元素的下标 index = array[i] - min
        // (count[array[i] - min]) - 1 才为 result[] 数组的对应插入位置
        // 所以, 要先 -1, 即 --count[array[i] - min], 再插入
        result[--count[array[i] - min]] = array[i];
    }

    System.out.println("排序结果 >> " + Arrays.toString(result));

    return result;
}

4、算法分析

当输入的元素是 n 个 0 到 k 之间的整数时,它的运行时间是 O(n + k)。计数排序不是比较排序,排序的速度快于任何比较排序算法。由于用来计数的数组 C 的长度取决于待排序数组中数据的范围(等于待排序数组的最大值与最小值的差加上1),这使得计数排序对于数据范围很大的数组,需要大量时间和内存。

最佳情况:T(n) = O(n+k)

最差情况:T(n) = O(n+k)

平均情况:T(n) = O(n+k)

九、基数排序(Radix Sort)

基数排序也是非比较的排序算法,对每一位进行排序,从最低位开始排序,复杂度为O(kn),n 为数组长度,k 为数组中的数的最大的位数。

基数排序是按照低位先排序,然后收集;再按照高位排序,然后再收集;依次类推,直到最高位。有时候有些属性是有优先级顺序的,先按低优先级排序,再按高优先级排序。最后的次序就是高优先级高的在前,高优先级相同的低优先级高的在前。基数排序基于分别排序,分别收集,所以是稳定的。

1、算法描述

  • 取得数组中的最大数,并取得位数。
  • array 为原始数组,从最低位开始取每个位组成 radix 数组。
  • 对 radix 进行计数排序(利用计数排序适用于小范围数的特点)。

2、动图演示

在这里插入图片描述

3、代码实现

/**
 * 基数排序(基于计数排序)
 *
 * @param array
 */
public static void radixSort(int[] array) {
    if (array == null || array.length <= 1) {
        return;
    }
    // 数组长度
    int length = array.length;
    // 最终数组
    int[] result = new int[length];
    // 每位数字范围 0~9 , 基为 10
    int radix = 10;
    // 基数(桶)数组
    int[] count = new int[radix + 1];

    // 求最高位数
    // 以关键字来排序的轮数,由位数最多的数字决定,其余位数少的数字在比较高位时,自动用0进行比较
    // 将数字转换成字符串,字符串的长度就是数字的位数,字符串最长的那个数字也拥有最多的位数
    int digit = Arrays.stream(array).map(s -> String.valueOf(s).length()).max().getAsInt();

    System.out.println("digit(位数) = " + digit);
    // 共需要 i 轮计数排序, 从 i = 0 开始, 说明是从个位开始比较
    for (int i = 0; i < digit; i++) {

        // 1. 计算频率, 遍历 原数组
        for (int j = 0; j < length; j++) {
            count[getOperator(array[j], i)]++;
        }
        System.out.println("第 " + i + " 轮 count 改变之前 >> " + Arrays.toString(count));

        /**
         * 2. 遍历计数数组 count[], 修改数据, 保持 数组稳定
         *
         * count 计数数组 原本结构
         * count = [3, 5, 2]
         *          0  1  2
         * 改变之后 -->>
         * count = [3, 8, 10]  --> count[1] = count[1] + count[0], ...
         */
        for (int j = 1; j < count.length; j++) {
            count[j] = count[j] + count[j - 1];
        }
        System.out.println("第 " + i + " 轮 count 改变之后 >> " + Arrays.toString(count));

        // 3. 遍历原数组, 倒叙插入数据
        for (int j = length - 1; j >= 0; j--) {
            result[--count[getOperator(array[j], i)]] = array[j];
        }
        System.out.println("第 " + i + " 轮排序结果 >> " + Arrays.toString(result));

        // 4. 数据回写
        for (int j = 0; j < length; j++) {
            array[j] = result[j];
        }
        System.out.println("第 " + i + " 轮 array 结果 >> " + Arrays.toString(array));

        // 5. 重置 count[], 以便下一轮统计使用
        for (int j = 0; j < count.length; j++) {
            count[j] = 0;
        }

    }
}

/**
 * 获取某个值的个位、十位、百位...
 * <p>
 * 根据 i, 获取某个值的个位、十位、百位等,i = 0 取出个位,i = 1取出十位,以此类推。
 * 对于不存在的高位, 用 0 补, Math.pow(10, i) 取出 10 的 i 次幂
 *
 * @param value
 * @param i
 * @return
 */
public static int getOperator(int value, int i) {
    return (value / (int) Math.pow(10, i)) % 10;
}

4、算法分析

最佳情况:T(n) = O(n * k)

最差情况:T(n) = O(n * k)

平均情况:T(n) = O(n * k)

基数排序有两种方法:

  • MSD 从高位开始进行排序
  • LSD 从低位开始进行排序

十、桶排序(Bucket Sort)

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

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

1、算法描述

  • 人为设置一个 BucketSize,作为每个桶所能放置多少个不同数值(例如当 BucketSize==5 时,该桶可以存放{1,2,3,4,5}这几种数字,但是容量不限,即可以存放 100 个 3 )。
  • 遍历输入数据,并且把数据一个一个放到对应的桶里去。
  • 对每个不是空的桶进行排序,可以使用其它排序方法,也可以递归使用桶排序。
  • 从不是空的桶里把排好序的数据拼接起来。

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

2、图片演示

在这里插入图片描述

3、代码实现

/**
 * 桶排序
 *
 * @param array
 */
public static void bucketSort(int[] array) {
    if (array == null || array.length <= 1) {
        return;
    }
    // 数组长度
    int length = array.length;

    // 建立桶, 个数和待排序数组长度一样
    LinkedList<Integer>[] bucket = new LinkedList[length];

    // 待排序数组中的最大值
    int maxValue = Arrays.stream(array).max().getAsInt();

    // 根据每个元素的值, 分配到对应范围的桶中
    for (int i = 0; i < length; i++) {
        // 将值转换为应存放到的桶数组的索引
        int index = (array[i] * length) / (maxValue + 1);

        // 没有桶才建立桶(延时)
        if (bucket[index] == null) {
            bucket[index] = new LinkedList<>();
        }

        // 有桶直接使用
        bucket[index].add(array[i]);
    }

    // List 中已经有序
    List<Integer> temp = new ArrayList<>();
    for (int i = 0; i < length; i++) {
        // 对每个非 null 的桶排序
        if (bucket[i] != null) {
            Collections.sort(bucket[i]);
            // 排序后顺便存入临时的 List
            temp.addAll(bucket[i]);
        }
    }

    // 将 temp 中的数据写入原数组
    for (int i = 0; i < length; i++) {
        array[i] = temp.get(i);
    }
}

4、算法分析

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

最佳情况:T(n) = O(n+k)

最差情况:T(n) = O(n+k)

平均情况:T(n) = O(n2)

这三种排序算法都利用了桶的概念,但对桶的使用方法上有明显差异:

  • 基数排序:根据键值的每位数字来分配桶
  • 计数排序:每个桶只存储单一键值
  • 桶排序:每个桶存储一定范围的数值

十一、堆排序(Heap Sort)

堆排序(Heapsort)是指利用堆这种数据结构所设计的一种排序算法。堆积是一个近似完全二叉树的结构,并同时满足堆积的性质:即子结点的键值或索引总是小于(或者大于)它的父节点

在这里插入图片描述

1、算法描述

  • 将初始待排序关键字序列(R1,R2….Rn)构建成大顶堆,此堆为初始的无序区。
  • 将堆顶元素 R[1] 与最后一个元素 R[n] 交换,此时得到新的无序区(R1,R2,……Rn-1)和新的有序区 (Rn),且满足R[1,2…n-1]<=R[n]。
  • 由于交换后新的堆顶 R[1] 可能违反堆的性质,因此需要对当前无序区(R1,R2,……Rn-1)调整为新堆,然后再次将 R[1] 与无序区最后一个元素交换,得到新的无序区(R1,R2….Rn-2)和新的有序区(Rn-1,Rn)。不断重复此过程直到有序区的元素个数为 n-1,则整个排序过程完成。

2、动图演示

在这里插入图片描述

3、代码实现

/**
 * 堆排序
 * 3.交换最大和最小节点, '砍断' "最后一个节点(最大元素节点)"
 *
 * @param array
 */
public static void heapSort(int[] array) {
    if (array == null || array.length <= 1) {
        return;
    }
    // 数组长度(n 个节点 = 二叉树长度(数组长度) = 元素个数)
    int n = array.length;

    // 构建大顶堆
    buildHeap(array, n);

    // 交换最大和最小节点, '砍断' "最后一个节点(最大元素节点)"
    for (int i = n - 1; i >= 0; i--) {
        // 交换 "最后一个节点" 和 "顶节点"
        swap(array, i, 0);
        // 重新调整大顶堆, 这里 i 可以代表剩下节点数量
        heapify(array, 0, i);
    }

}

/**
 * 2. 构建大顶堆(保证每个 父节点 都会大于其 子节点)
 * 从 "最后一个节点的父节点位置" 遍历所有节点
 *
 * @param tree 要调整的树(数组)
 * @param n    "n 个节点" = 二叉树长度(数组长度) = 元素个数
 */
public static void buildHeap(int[] tree, int n) {
    // 最后一个节点位置
    int lastNode = n - 1;

    // 最后一个节点的父节点位置
    int parentNode = (lastNode - 1) / 2;
    // 倒序遍历
    for (int i = parentNode; i >= 0; i--) {
        // 对堆进行调整
        heapify(tree, i, n);
    }

}

/**
 * 1.调整大顶堆(仅是调整过程, 建立在大顶堆已构建的基础上)
 *
 * @param tree 要调整的树(数组)
 * @param i    第 i 节点
 * @param n    "n 个节点" = 二叉树长度(数组长度) = 元素个数
 */
public static void heapify(int[] tree, int i, int n) {
    // 方法出口
    if (i >= n) {
        return;
    }

    // 第一个子节点
    int c1 = 2 * i + 1;

    // 第二个子节点
    int c2 = 2 * i + 2;

    // 假设 i 节点元素是最大值 tree[max] = tree[i]
    int max = i;

    // 如果 "左子节点元素 tree[c1]" 大于 "最大值 tree[max], 即当前节点 i节点(父元素) 元素值"
    if (c1 < n && tree[c1] > tree[max]) {
        // 较大值为 tree[c1]
        max = c1;
    }

    // 如果 "右子节点元素 tree[c2]" 大于 "最大值 tree[max] (已经跟父节点比较了)"
    if (c2 < n && tree[c2] > tree[max]) {
        // 较大值为 tree[c2]
        max = c2;
    }

    // 如果 "max 节点" 和 "i 节点" 不相等
    if (max != i) {
        // 交换父子节点
        swap(tree, i, max);
        // 递归, 重新对堆进行调整
        heapify(tree, max, n);
    }
}

/**
 * 交换元素位置
 *
 * @param array
 * @param a
 * @param b
 */
public static void swap(int[] array, int a, int b) {
    int temp = array[a];
    array[a] = array[b];
    array[b] = temp;
}

4、算法分析

最佳情况:T(n) = O(nlogn)

最差情况:T(n) = O(nlogn)

平均情况:T(n) = O(nlogn)

十二、总结

package collate;

import java.util.*;

/**
 * @Author: ***
 * @Create: 2020-07-23 11:06
 * @Description: 选择排序
 */
public class Collate {

    public static void main(String[] args) {
        int[] array = {3, 44, 38, 5, 47, 15, 36, 26, 27, 2, 46, 4, 19, 50, 48};
//        int[] array = {4, 10, 3, 5, 1, 2};
        // 只需要修改成对应的方法名就可以了
        heapSort(array);
        System.out.println(Arrays.toString(array));
    }

    /**
     * 选择排序
     *
     * @param array
     * @return void
     */
    public static void selectionSort(int[] array) {
        if (array == null || array.length <= 1) {
            return;
        }

        // 数组长度
        int length = array.length;

        // 1.外层循环控制比较轮数i
        for (int i = 0; i < length - 1; i++) {
            // 2.保存最小数的索引[初始为 i = 0 索引位置元素]
            int minIndex = i;

            // 3.从当前 i 索引位置开始
            for (int j = i + 1; j < length; j++) {
                // 4.找到最小的数[数组中 j 索引位置元素 小于 最小数索引]
                if (array[j] < array[minIndex]) {
                    // 5.将当前 j 索引作为最小元素索引, 赋值给 minIndex
                    minIndex = j;
                }
            }

            // 交换元素位置[判断当前内循环开始位置 i 是否就已经是最小值了]
            if (i != minIndex) {
                swap(array, minIndex, i);
            }
        }
    }

    /**
     * 冒泡排序
     *
     * @param array
     */
    public static void bubbleSort(int[] array) {
        if (array == null || array.length <= 1) {
            return;
        }

        // 数组长度
        int length = array.length;

        // 1.外层循环控制比较轮数i
        for (int i = 0; i < length - 1; i++) {
            // 内层循环控制每一轮比较次数,每进行一轮排序都会找出一个较大值
            // (array.length - 1)防止索引越界, (array.length - 1 - i)减少比较次数
            for (int j = 0; j < length - 1 - i; j++) {
                if (array[j] > array[j + 1]) {
                    swap(array, j, j + 1);
                }
            }
        }
    }

    /**
     * 插入排序
     *
     * @param array
     */
    public static void insertionSort(int[] array) {
        if (array == null || array.length <= 1) {
            return;
        }

        // 数组长度
        int length = array.length;

        // 要插入的数
        int insertNum;

        // 1.外层循环控制比较轮数i, 直到循环到 i = length - 1, array[0] 默认为有序的
        for (int i = 1; i < length; i++) {
            // 将要排序的数赋值给 insertNum
            insertNum = array[i];

            int j = i;

            for (; j > 0 && array[j - 1] > insertNum; j--) {
                // 从 i 位置, 从后到前循环, 将大于 insertNum 的数向后移动一格
                array[j] = array[j - 1];
            }
            // j-- 后 array[j] = null 值, 直接插入
            array[j] = insertNum;


//            // 从 i 位置, 从后到前循环, 进行比较
//            for (int j = i; j > 0; j--) {
//                // 如果当前位置元素小于紧邻的前面那个数, 则进行交换
//                if (array[j] < array[j - 1]) {
//                    swap(array, j, j - 1);
//                }
//            }
        }
    }

    /**
     * 希尔排序
     *
     * @param array
     */
    public static void shellSort(int[] array) {
        if (array == null || array.length <= 1) {
            return;
        }

        // 数组长度
        int length = array.length;

        // Knuth
        int h = 1;
        while (h <= length / 3) {
            h = h * 3 + 1;
        }
        System.out.println(h);

        // 间隔变化 13 , 4, 1
        for (int gap = h; gap > 0; gap = (gap - 1) / 3) {

            // 要插入的数
            int insertNum;

            // 排序
            // 1.外层循环控制比较轮数 i, i = gap 间隔固定
            for (int i = gap; i < length; i++) {

                // 将要排序的数赋值给 insertNum
                insertNum = array[i];

                // 从 i = gap 位置, 从后到前循环, 间隔进行比较
                int j = i;

                for (; j > gap - 1 && array[j - gap] > insertNum; j = j - gap) {
                    // 从 gap 位置, 从后到前循环, 将大于 insertNum 的数向后移动一格
                    array[j] = array[j - gap];
                }
                // j-- 后 array[gap] = null 值, 直接插入
                array[j] = insertNum;
            }
        }

    }

    /**
     * 交换元素位置
     *
     * @param array
     * @param a
     * @param b
     */
    public static void swap(int[] array, int a, int b) {
        int temp = array[a];
        array[a] = array[b];
        array[b] = temp;
    }

    /**
     * 归并排序
     *
     * @param array
     */
    public static void mergeSort(int[] array) {
        if (array == null || array.length <= 1) {
            return;
        }

        // 数组长度
        int length = array.length;

        sort(array, 0, length - 1);
    }

    /**
     * 2.数组排序(递归)
     *
     * @param array
     * @param leftPtr    左指针: 数组起始位置(index)
     * @param rightBound 右边界: 数组结束位置(index)
     */
    public static void sort(int[] array, int leftPtr, int rightBound) {
        if (leftPtr == rightBound) {
            return;
        }
        // 分成两半: == (leftPtr + rightBound) / 2, 这样写是防止超过 int Max.
        // mid : 左一半数组结束位置(中间位置)
        int mid = leftPtr + (rightBound - leftPtr) / 2;
        // 左边排序
        sort(array, leftPtr, mid);
        // 右边排序
        sort(array, mid + 1, rightBound);
        // 数组归并
        merge(array, leftPtr, mid + 1, rightBound);
    }

    /**
     * 1.数组归并(假定左右数组已经排好顺序)
     *
     * @param array
     * @param leftPrt    左指针: 左一半数组起始位置(index)
     * @param rightPrt   右指针: 右一半数组起始位置(index)
     * @param rightBound 右边界: 右一半数组结束位置(index)
     */
    public static void merge(int[] array, int leftPrt, int rightPrt, int rightBound) {
        /** 给新数组 temp 分配空间 */
        int[] temp = new int[rightBound - leftPrt + 1];

        /** 左一半数组起始位置 */
        int i = leftPrt;
        /** 左一半数组结束位置(中间位置) */
        int mid = rightPrt - 1;
        /** 右一半数组起始位置 */
        int j = rightPrt;
        /** 新数组 temp 起始位置 */
        int k = 0;

        // 条件判断: 两个数组都没有遍历完
        while (i <= mid && j <= rightBound) {
            // 比较左右两部分的元素, 哪个小, 把那个元素填入 temp 中
            temp[k++] = (array[i] <= array[j]) ? array[i++] : array[j++];
            // k++, i++, j++: 移动数组下标
        }

        // 上面的循环退出后, 把剩余的元素依次填入到 temp 中, 以下两个 while 只有一个会执行
        // 条件判断: 前一半数组没有遍历完
        while (i <= mid) {
            temp[k++] = array[i++];
        }
        // 条件判断: 后一半数组没有遍历完
        while (j <= rightBound) {
            temp[k++] = array[j++];
        }

        for (int m = 0; m < temp.length; m++) {
            array[leftPrt + m] = temp[m];
        }

        print(temp);
    }

    /**
     * 打印数组
     *
     * @param array
     */
    private static void print(int[] array) {
        for (int i = 0; i < array.length; i++) {
            System.out.print(array[i] + " ");
        }
        System.out.println();
    }

    /**
     * 快速排序
     *
     * @param array
     */
    public static void quickSort(int[] array) {
        if (array == null || array.length <= 1) {
            return;
        }
        // 数组长度
        int length = array.length;

        qSort(array, 0, length - 1);
    }

    /**
     * 2.数组排序(递归)
     *
     * @param array
     * @param leftBound  左边界: 数组起始位置(index)
     * @param rightBound 右边界: 数组结束位置(index)
     */
    public static void qSort(int[] array, int leftBound, int rightBound) {
        if (leftBound >= rightBound) {
            return;
        }

        // 轴的位置
        int mid = partition(array, leftBound, rightBound);
        qSort(array, leftBound, mid - 1);
        qSort(array, mid + 1, rightBound);
    }

    /**
     * 1.数组分区
     *
     * @param array
     * @param leftBound  左边界: 数组起始位置(index)
     * @param rightBound 右边界: 数组结束位置(index)
     * @return 基准(轴)的位置
     */
    public static int partition(int[] array, int leftBound, int rightBound) {
        // 基准(选择数组最右侧): 分区在其左右
        int pivot = array[rightBound];
        // 左侧指针指在 分区最左侧
        int leftPrt = leftBound;
        // 右侧指针指在 基准左侧第一个元素位置
        int rightPrt = rightBound - 1;

        // 遍历数组分区 左指针 < 右指针
        while (leftPrt <= rightPrt) {
            // (前提: 左指针一直在右指针左侧, 或相等)
            // 左指针 向右移动, 直到找到某个值大于 array[left] > pivot 基准
            while (leftPrt <= rightPrt && array[leftPrt] <= pivot) {
                // 数组元素 小于等于 基准 pivot
                leftPrt++;
            }

            // (前提: 左指针一直在右指针左侧, 或相等)
            // 右指针 向左移动, 直到找到某个值小于 array[right] <= pivot 基准
            while (leftPrt <= rightPrt && array[rightPrt] > pivot) {
                // 数组元素 大于等于 基准 pivot
                rightPrt--;
            }

            System.out.println("before swap: leftPrt = " + leftPrt + ", rightPrt = " + rightPrt);

            // 如果 左指针、右指针在找到元素后, 仍然 leftPrt < rightPrt
            if (leftPrt < rightPrt) {
                // 交换元素位置
                swap(array, leftPrt, rightPrt);
            }

            print(array);
        }
        // 交换基准到分区中间
        swap(array, leftPrt, rightBound);
        return leftPrt;
    }

    /**
     * 计数排序
     *
     * @param array
     */
    public static int[] countingSort(int[] array) {

        if (array == null || array.length <= 1) {
            return array;
        }
        // 数组长度
        int length = array.length;

        // 最终数组
        int[] result = new int[length];

        // 最小值、最大值
        int min = array[0];
        int max = array[0];

        // 遍历原数组, 获取其最大、最小值
        for (int i = 0; i < length; i++) {
            // 取最小值
            if (min > array[i]) {
                min = array[i];
            }
            // 取最大值
            if (max < array[i]) {
                max = array[i];
            }
        }

        // 5 3 2
        // max = 5, min =2
        // 5-2 = 3 --> 2, 3, 4, 5
        // --> offset = 4 = 3 + 1

        // 最大最小元素之间范围 [min, max] 的长度
        int offset = max - min + 1;

        // 计数(桶)数组
        int[] count = new int[offset];

        System.out.println("before sort: min = " + min
                + ", max = " + max
                + ", offset(计数数组长度) = " + offset);

        // 遍历 原数组
        for (int i = 0; i < length; i++) {
            // 将原数组的 (元素值 - min) 作为 count 数组的 index,
            // 并将对应 index 元素的值 +1, 即计算该 index 出现的次数: count[index] + 1
            count[array[i] - min]++;
        }

        System.out.println(Arrays.toString(count));

        // 遍历计数数组 count[]
//        for (int i = 0, j = 0; i < offset; i++) {
//            // 只要 count[i] 元素值 大于 0
//            while (count[i]-- > 0) {
//                // 数据回写: 将 count[] 数组的 index + min 赋值给 array[j], 并 j++
//                array[j++] = i + min;
//            }
//        }

        /**
         * 遍历计数数组 count[], 修改数据, 保持 数组稳定
         *
         * count 计数数组 原本结构
         * count = [3, 5, 2]
         *          0  1  2
         * 改变之后 -->>
         * count = [3, 8, 10]  --> count[1] = count[1] + count[0], ...
         */
        for (int i = 1; i < offset; i++) {
            count[i] = count[i] + count[i - 1];
        }

        System.out.println("count 改变之后 >> " + Arrays.toString(count));

        // 遍历原数组, 倒叙插入数据
        for (int i = length - 1; i >= 0; i--) {
            // 找到原数组的元素 array[i]
            // 找到 count[] 数组对应 array[i] 元素的下标 index = array[i] - min
            // (count[array[i] - min]) - 1 才为 result[] 数组的对应插入位置
            // 所以, 要先 -1, 即 --count[array[i] - min], 再插入
            result[--count[array[i] - min]] = array[i];
        }

        System.out.println("排序结果 >> " + Arrays.toString(result));

        return result;
    }

    /**
     * 基数排序(基于计数排序)
     *
     * @param array
     */
    public static void radixSort(int[] array) {
        if (array == null || array.length <= 1) {
            return;
        }
        // 数组长度
        int length = array.length;
        // 最终数组
        int[] result = new int[length];
        // 每位数字范围 0~9 , 基为 10
        int radix = 10;
        // 基数(桶)数组
        int[] count = new int[radix + 1];

        // 求最高位数
        // 以关键字来排序的轮数,由位数最多的数字决定,其余位数少的数字在比较高位时,自动用0进行比较
        // 将数字转换成字符串,字符串的长度就是数字的位数,字符串最长的那个数字也拥有最多的位数
        int digit = Arrays.stream(array).map(s -> String.valueOf(s).length()).max().getAsInt();

        System.out.println("digit(位数) = " + digit);
        // 共需要 i 轮计数排序, 从 i = 0 开始, 说明是从个位开始比较
        for (int i = 0; i < digit; i++) {

            // 1. 计算频率, 遍历 原数组
            for (int j = 0; j < length; j++) {
                count[getOperator(array[j], i)]++;
            }
            System.out.println("第 " + i + " 轮 count 改变之前 >> " + Arrays.toString(count));

            /**
             * 遍历计数数组 count[], 修改数据, 保持 数组稳定
             *
             * count 计数数组 原本结构
             * count = [3, 5, 2]
             *          0  1  2
             * 改变之后 -->>
             * count = [3, 8, 10]  --> count[1] = count[1] + count[0], ...
             */
            for (int j = 1; j < count.length; j++) {
                count[j] = count[j] + count[j - 1];
            }
            System.out.println("第 " + i + " 轮 count 改变之后 >> " + Arrays.toString(count));

            // 遍历原数组, 倒叙插入数据
            for (int j = length - 1; j >= 0; j--) {
                result[--count[getOperator(array[j], i)]] = array[j];
            }
            System.out.println("第 " + i + " 轮排序结果 >> " + Arrays.toString(result));

            // 数据回写
            for (int j = 0; j < length; j++) {
                array[j] = result[j];
            }
            System.out.println("第 " + i + " 轮 array 结果 >> " + Arrays.toString(array));

            // 重置 count[], 以便下一轮统计使用
            for (int j = 0; j < count.length; j++) {
                count[j] = 0;
            }

        }
    }

    /**
     * 获取某个值的个位、十位、百位...
     * <p>
     * 根据 i, 获取某个值的个位、十位、百位等,i = 0 取出个位,i = 1取出十位,以此类推。
     * 对于不存在的高位, 用 0 补, Math.pow(10, i) 取出 10 的 i 次幂
     *
     * @param value
     * @param i
     * @return
     */
    public static int getOperator(int value, int i) {
        return (value / (int) Math.pow(10, i)) % 10;
    }

    /**
     * 桶排序
     *
     * @param array
     */
    public static void bucketSort(int[] array) {
        if (array == null || array.length <= 1) {
            return;
        }
        // 数组长度
        int length = array.length;

        // 建立桶, 个数和待排序数组长度一样
        LinkedList<Integer>[] bucket = new LinkedList[length];

        // 待排序数组中的最大值
        int maxValue = Arrays.stream(array).max().getAsInt();

        // 根据每个元素的值, 分配到对应范围的桶中
        for (int i = 0; i < length; i++) {
            // 将值转换为应存放到的桶数组的索引
            int index = (array[i] * length) / (maxValue + 1);

            // 没有桶才建立桶(延时)
            if (bucket[index] == null) {
                bucket[index] = new LinkedList<>();
            }

            // 有桶直接使用
            bucket[index].add(array[i]);
        }

        // List 中已经有序
        List<Integer> temp = new ArrayList<>();
        for (int i = 0; i < length; i++) {
            // 对每个非 null 的桶排序
            if (bucket[i] != null) {
                Collections.sort(bucket[i]);
                // 排序后顺便存入临时的 List
                temp.addAll(bucket[i]);
            }
        }

        // 将 temp 中的数据写入原数组
        for (int i = 0; i < length; i++) {
            array[i] = temp.get(i);
        }
    }

    /**
     * 堆排序
     * 3.交换最大和最小节点, '砍断' "最后一个节点(最大元素节点)"
     *
     * @param array
     */
    public static void heapSort(int[] array) {
        if (array == null || array.length <= 1) {
            return;
        }
        // 数组长度(n 个节点 = 二叉树长度(数组长度) = 元素个数)
        int n = array.length;

        // 构建大顶堆
        buildHeap(array, n);

        // 交换最大和最小节点, '砍断' "最后一个节点(最大元素节点)"
        for (int i = n - 1; i >= 0; i--) {
            // 交换 "最后一个节点" 和 "顶节点"
            swap(array, i, 0);
            // 重新调整大顶堆, 这里 i 可以代表剩下节点数量
            heapify(array, 0, i);
        }

    }

    /**
     * 2. 构建大顶堆(保证每个 父节点 都会大于其 子节点)
     * 从 "最后一个节点的父节点位置" 遍历所有节点
     *
     * @param tree 要调整的树(数组)
     * @param n    "n 个节点" = 二叉树长度(数组长度) = 元素个数
     */
    public static void buildHeap(int[] tree, int n) {
        // 最后一个节点位置
        int lastNode = n - 1;

        // 最后一个节点的父节点位置
        int parentNode = (lastNode - 1) / 2;
        for (int i = parentNode; i >= 0; i--) {
            // 对堆进行调整
            heapify(tree, i, n);
        }

    }

    /**
     * 1.调整大顶堆(仅是调整过程, 建立在大顶堆已构建的基础上)
     *
     * @param tree 要调整的树(数组)
     * @param i    第 i 节点
     * @param n    "n 个节点" = 二叉树长度(数组长度) = 元素个数
     */
    public static void heapify(int[] tree, int i, int n) {
        // 方法出口
        if (i >= n) {
            return;
        }

        // 第一个子节点
        int c1 = 2 * i + 1;

        // 第二个子节点
        int c2 = 2 * i + 2;

        // 假设 i 节点元素是最大值 tree[max] = tree[i]
        int max = i;

        // 如果 "左子节点元素 tree[c1]" 大于 "最大值 tree[max], 即当前节点 i节点(父元素) 元素值"
        if (c1 < n && tree[c1] > tree[max]) {
            // 较大值为 tree[c1]
            max = c1;
        }

        // 如果 "右子节点元素 tree[c2]" 大于 "最大值 tree[max] (已经跟父节点比较了)"
        if (c2 < n && tree[c2] > tree[max]) {
            // 较大值为 tree[c2]
            max = c2;
        }

        // 如果 "max 节点" 和 "i 节点" 不相等
        if (max != i) {
            // 交换父子节点
            swap(tree, i, max);
            // 递归, 重新对堆进行调整
            heapify(tree, max, n);
        }
    }
}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值