title: 十大经典排序算法总结
date: 2023-05-18 15:21:13
tags:
- 算法
categories: - 数据结构与算法
cover: https://cover.png
feature: false
1. 简介
排序算法可以分为:
- 内部排序: 数据记录在内存中进行排序
- 外部排序: 因排序的数据很大,一次不能容纳全部的排序记录,在排序过程中需要访问外存,把数据放在磁盘中,而排序通过磁盘和内存的数据传输才能进行
常见的内部排序算法有: 插入排序 、 希尔排序 、 选择排序 、 冒泡排序 、 归并排序 、 快速排序 、 堆排序 、基数排序等
- n :数据规模
- k :“桶” 的个数
- In-place :占用常数内存,不占用额外内存
- Out-place :占用额外内存
十种常见排序算法可以分为两大类别:比较类排序和非比较类排序
1、比较类排序,优势是适用于各种规模的数据,也不在乎数据的分布,都能进行排序。可以说,比较排序适用于一切需要排序的情况
常见的快速排序 、 归并排序 、堆排序以及冒泡排序等都属于比较类排序算法 。比较类排序是通过比较来决定元素间的相对次序,由于其时间复杂度不能突破 O(nlogn)
,因此也称为非线性时间比较类排序。在冒泡排序之类的排序中,问题规模为 n
,又因为需要比较 n
次,所以平均时间复杂度为 O(n²)
。在归并排序 、快速排序之类的排序中,问题规模通过分治法消减为 logn
次,所以时间复杂度平均 O(nlogn)
2、非比较排序时间复杂度低,但由于非比较排序需要占用空间来确定唯一位置。所以对数据规模和数据分布有一定的要求
计数排序 、 基数排序 、桶排序则属于非比较类排序算法 。非比较排序不通过比较来决定元素间的相对次序,而是通过确定每个元素之前,应该有多少个元素来排序。由于它可以突破基于比较排序的时间下界,以线性时间运行,因此称为线性时间非比较类排序。 非比较排序只要确定每个元素之前的已有的元素个数即可,所以一次遍历即可解决。算法时间复杂度 O(n)
2. 冒泡排序(Bubble Sort)
冒泡排序是一种简单的排序算法。它重复地遍历要排序的序列,依次比较两个元素,如果它们的顺序错误就把它们交换过来。遍历序列的工作是重复地进行直到没有再需要交换为止,此时说明该序列已经排序完成。这个算法的名字由来是因为越小的元素会经由交换慢慢 “浮” 到数列的顶端
2.1 算法步骤
- 比较相邻的元素。如果第一个比第二个大,就交换它们两个
- 对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对,这样在最后的元素应该会是最大的数
- 针对所有的元素重复以上的步骤,除了最后一个
- 重复步骤 1~3,直到排序完成
2.2 代码实现
public class BubbleSort {
public static void main(String[] args) {
int[] data = {6, 4, 3, 23, 27, 23, 14, 10};
bubbleSort(data);
for (int datum : data) {
System.out.println(datum + " ");
}
}
/**
* <ul>
* <li> 每次比较相邻的两个数, 将大的数放到后面. 遍历一次后, 最后一位的数肯定是最大的 </li>
* <li> 然后第二次遍历, 已经得到的最后一位最大的数不参与遍历, 将第二大的数放到倒数第二位 </li>
* <li> 每次遍历都找到一个当前最大的数, 所有都遍历完后, 则排序完成 </li>
* </ul>
*
* @param data 待排序数组
* @author Fan
* @since 2023/3/1 15:23
*/
public static void bubbleSort(int[] data) {
// 排序的遍历次数, 每一遍让当前最大的一个数 "沉" 到最后, 到最后一个数时已经排好序了, 即最后一遍不需要考虑, 次数为 length - 1
for (int i = 1; i < data.length; i++) {
// 定义一个布尔变量来判断当前数组是不是已经排序好的
boolean flag = true;
// 排序, 交换相邻的元素, 让大的元素在后面, 排完一遍后最后的数是最大的, 下次排序就不用遍历这个数, length - 遍历次数
for (int j = 0; j < data.length - i; j++) {
if (data[j] > data[j + 1]) {
int temp = data[j];
data[j] = data[j + 1];
data[j + 1] = temp;
// 到这里, 说明该数组不是排序好的, 置为 false
flag = false;
}
}
// 假如数组是排序好的, 该遍循环直接跳出, 从而减少后续不必要的循环
if (flag) {
break;
}
}
}
}
此处做了一个小优化,加入了 is_sorted
Flag,目的是将算法的最佳时间复杂度优化为 O(n)
,即当原输入序列就是排序好的情况下,该算法的时间复杂度就是 O(n)
2.3 算法分析
- 稳定性 :稳定
- 时间复杂度 :最佳:O(n) ,最差:O(n2)O(n^2)O(n2), 平均:O(n2)O(n^2)O(n2)
- 空间复杂度 :O(1)
- 排序方式 :In-place
3. 选择排序(Selection Sort)
选择排序是一种简单直观的排序算法,无论什么数据进去都是 O(n²)
的时间复杂度。所以用到它的时候,数据规模越小越好。唯一的好处可能就是不占用额外的内存空间。它的工作原理:首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置,然后,再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。以此类推,直到所有元素均排序完毕
3.1 算法步骤
- 首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置
- 再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾
- 重复第 2 步,直到所有元素均排序完毕
3.2 代码实现
public class SelectionSort {
public static void main(String[] args) {
int[] data = {6, 4, 3, 23, 27, 23, 14, 10};
selectionSort(data);
for (int datum : data) {
System.out.println(datum + " ");
}
}
/**
* 与插入排序类似, 同样将数组分为已排序区间和未排序区间, 不同的是遍历未排序区间, 找到最小的元素, 将其放到已排序区间的末尾,
* 或者说放到未排序区间的开头, 这里是交换位置, 不是插入, 不需要移动元素
*
* @param data 待排序数组
* @author Fan
* @since 2023/3/2 14:08
*/
public static void selectionSort(int[] data) {
// 遍历次数, 每一遍找到最小的元素, 放到该遍循环的最开头, 前面已遍历的的不参与遍历, 最后一遍时数组已经是排好序的了, 次数为 length - 1
for (int i = 0; i < data.length - 1; i++) {
// 定义最小元素的下标, 为当前循环的首位元素
int minIndex = i;
// 循环比较, 找到最小元素的下标
for (int j = i + 1; j < data.length; j++) {
if (data[j] < data[minIndex]) {
minIndex = j;
}
}
// 判断最小元素的下标是否就是首位元素, 有变动再进行调换双方的值, 让首位元素为最小值
if (minIndex != i) {
int temp = data[i];
data[i] = data[minIndex];
data[minIndex] = temp;
}
}
}
}
2.3 算法分析
- 稳定性 :不稳定
- 时间复杂度 :最佳:O(n2)O(n^2)O(n2) ,最差:O(n2)O(n^2)O(n2), 平均:O(n2)O(n^2)O(n2)
- 空间复杂度 :O(1)
- 排序方式 :In-place
4. 插入排序 (Insertion Sort)
插入排序是一种简单直观的排序算法。它的工作原理是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。插入排序在实现上,通常采用 in-place 排序(即只需用到 O(1)
的额外空间的排序),因而在从后向前扫描过程中,需要反复把已排序元素逐步向后挪位,为最新元素提供插入空间
插入排序和冒泡排序一样,也有一种优化算法,叫做拆半插入
4.1 算法步骤
- 从第一个元素开始,该元素可以认为已经被排序
- 取出下一个元素,在已经排序的元素序列中从后向前扫描
- 如果该元素(已排序)大于新元素,将该元素移到下一位置
- 重复步骤 3,直到找到已排序的元素小于或者等于新元素的位置
- 将新元素插入到该位置后
- 重复步骤 2~5
4.2 代码实现
public class InsertionSort {
public static void main(String[] args) {
int[] data = {6, 4, 3, 23, 27, 23, 14, 10};
insertionSort(data);
for (int datum : data) {
System.out.println(datum + " ");
}
}
/**
* <ul>
* <li> 将数组分为已排序和未排序两个区间, 每次都遍历已排序的区间, 取未排序区间的第一位插入到已排序区间里合适有序的位置 </li>
* <li> 第一个数不需要比较, 直接插入, 即不需要考虑, 直接从第二个数开始 </li>
* <li> 第二个数开始从后面往前面遍历比较, 假如第一个数比第二个数大, 则将第一位数后移一位到第二位, 第二位数赋值给第一位 </li>
* <li> 如上, 后面的数从自身开始从后面往前面遍历比较, 插入到合适有序的位置, 最后完成排序 </li>
* </ul>
*
* @param data 待排序数组
* @author Fan
* @since 2023/3/1 15:29
*/
public static void insertionSort(int[] data) {
// 排序的遍历次数, 第一个数不需要考虑, 直接插入, 次数为 length - 1
for (int i = 1; i < data.length; i++) {
// 存储当前元素的值
int current = data[i];
// 当前元素的前一个元素的下标
int preIndex = i - 1;
// 从当前元素开始从后向前排序, 假如前面的元素大于当前元素, 则往后移一位, 直到当前元素插入到合适的位置
while (preIndex >= 0 && data[preIndex] > current) {
data[preIndex + 1] = data[preIndex];
preIndex -= 1;
}
// 插入当前元素, 因为前面减了 1, 因此这里要加上 1 才为当前元素的位置
data[preIndex + 1] = current;
}
}
}
4.3 算法分析
- 稳定性 :稳定
- 时间复杂度 :最佳:O(n) ,最差:O(n2)O(n^2)O(n2), 平均:O(n2)O(n^2)O(n2)
- 空间复杂度 :O(1)
- 排序方式 :In-place
5. 希尔排序 (Shell Sort)
希尔排序是希尔 (Donald Shell) 于 1959 年提出的一种排序算法。希尔排序也是一种插入排序,它是简单插入排序经过改进之后的一个更高效的版本,也称为递减增量排序算法,同时该算法是冲破 O(n²)
的第一批算法之一
希尔排序的基本思想是:先将整个待排序的记录序列分割成为若干子序列分别进行直接插入排序,待整个序列中的记录 “基本有序” 时,再对全体记录进行依次直接插入排序
5.1 算法步骤
我们来看下希尔排序的基本步骤,在此我们选择增量 gap = length/2
,缩小增量继续以 gap = gap/2
的方式,这种增量选择我们可以用一个序列来表示,{n/2, (n/2)/2, ..., 1}
,称为增量序列 。希尔排序的增量序列的选择与证明是个数学难题,我们选择的这个增量序列是比较常用的,也是希尔建议的增量,称为希尔增量,但其实这个增量序列不是最优的。此处我们做示例使用希尔增量。
先将整个待排序的记录序列分割成为若干子序列分别进行直接插入排序,具体算法描述:
- 选择一个增量序列
{t1, t2, …, tk}
,其中(ti > tj, i < j, tk = 1)
- 按增量序列个数 k,对序列进行 k 趟排序
- 每趟排序,根据对应的增量
t
,将待排序列分割成若干长度为m
的子序列,分别对各子表进行直接插入排序。仅增量因子为 1 时,整个序列作为一个表来处理,表长度即为整个序列的长度
5.2 代码实现
public class ShellSort {
public static void main(String[] args) {
int[] data = {6, 4, 3, 23, 27, 23, 14, 10};
shellSort(data);
for (int datum : data) {
System.out.println(datum + " ");
}
}
/**
* 将整个待排序的记录序列分割成为若干子序列分别进行直接插入排序, 待整个序列中的记录 “基本有序” 时, 再对全体记录进行依次直接插入排序
*
* @param arr 待排序数组
* @author Fan
* @since 2023/5/17 15:14
*/
public static void shellSort(int[] arr) {
int length = arr.length;
int gap = length / 2;
while (gap > 0) {
// 子序列进行插入排序
for (int i = gap; i < length; i++) {
int current = arr[i];
int preIndex = i - gap;
while (preIndex >= 0 && arr[preIndex] > current) {
arr[preIndex + gap] = arr[preIndex];
preIndex -= gap;
}
arr[preIndex + gap] = current;
}
gap /= 2;
}
}
}
5.3 算法分析
- 稳定性 :稳定
- 时间复杂度 :最佳:O(nlogn), 最差:O(n2)O(n^2)O(n2) ,平均:O(nlogn)
- 空间复杂度 :O(1)
6. 归并排序(Merge Sort)
归并排序是建立在归并操作上的一种有效的排序算法。该算法是采用分治法 (Divide and Conquer) 的一个非常典型的应用。归并排序是一种稳定的排序方法。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为 2 - 路归并
和选择排序一样,归并排序的性能不受输入数据的影响,但表现比选择排序好的多,因为始终都是 O(nlogn)
的时间复杂度。代价是需要额外的内存空间
6.1 算法步骤
归并排序算法是一个递归过程,边界条件为当输入序列仅有一个元素时,直接返回,具体过程如下:
- 如果输入内只有一个元素,则直接返回,否则将长度为
n
的输入序列分成两个长度为n/2
的子序列 - 分别对这两个子序列进行归并排序,使子序列变为有序状态
- 设定两个指针,分别指向两个已经排序子序列的起始位置
- 比较两个指针所指向的元素,选择相对小的元素放入到合并空间(用于存放排序结果),并移动指针到下一位置
- 重复步骤 3 ~4 直到某一指针达到序列尾
- 将另一序列剩下的所有元素直接复制到合并序列尾
6.2 代码实现
@Slf4j
public class MergeSort {
public static void main(String[] args) {
int[] data = {6, 4, 3, 23, 27, 23, 14, 10};
int[] mergeSort = mergeSort(data);
for (int sort : mergeSort) {
log.info(sort + " ");
}
}
/**
* <ul>
* <li> 核心思想就是分治法, 将一个大问题转换为多个小问题来求解. 对一个数组进行排序, 可以转换为对数组的前一半和后一半进行排序 </li>
* <li> 前半数组和后半数组排好序后, 再把这两个排好序的数组合并为一个排好序的数组, 这里就需要一个核心的方法,
* 用来将传入的两个有序数组合并为一个有序的数组, 这样就能返回一个最终的有序数组 </li>
* <li> 同时, 前一半数组又能分为前半和后半, 后一半数组也能分为前半和后半, 一直这样递归细分 "递" 下去, 直到前半和后半的长度为 1 或 0,
* 这时直接返回数组, 因为 1 位的数组可以说就是有序的了, 然后再通过上面说到的核心方法, 将两个有序数组合并为一个, 然后一步步 "归" 回来,
* 最终得到一个排好序的数组 </li>
* </ul>
*
* @param data 待排序数组
* @return {@link int[]}
* @author Fan
* @since 2023/3/3 10:28
*/
public static int[] mergeSort(int[] data) {
if (data.length <= 1) {
return data;
}
// 分治, 一分为二, 划分为两个子问题来求解
int middle = data.length / 2;
int[] arr1 = Arrays.copyOfRange(data, 0, middle);
int[] arr2 = Arrays.copyOfRange(data, middle, data.length);
return merge(mergeSort(arr1), mergeSort(arr2));
}
/**
* 将两个有序数组合并为一个有序数组
*
* @param arr1 有序数组1
* @param arr2 有序数组2
* @return {@link int[]}
* @author Fan
* @since 2023/3/3 10:40
*/
public static int[] merge(int[] arr1, int[] arr2) {
// 定义一个数组来存两个有序数组合并后的有序数组
int[] sortArr = new int[arr1.length + arr2.length];
int index = 0;
int index1 = 0;
int index2 = 0;
// 两个有序数组互相比较, 依次找到最小的元素插入到合并后的数组中
while (index1 < arr1.length && index2 < arr2.length) {
if (arr1[index1] < arr2[index2]) {
sortArr[index++] = arr1[index1++];
} else {
sortArr[index++] = arr2[index2++];
}
}
// 当两个数组的其中一个比较完后, 直接将另一个数组全部插入合并后的数组里
if (index1 < arr1.length) {
while (index1 < arr1.length) {
sortArr[index++] = arr1[index1++];
}
} else {
while (index2 < arr2.length) {
sortArr[index++] = arr2[index2++];
}
}
return sortArr;
}
}
6.3 算法分析
- 稳定性:稳定
- 时间复杂度:最佳:O(nlogn), 最差:O(nlogn), 平均:O(nlogn)
- 空间复杂度:O(n)
7. 快速排序 (Quick Sort)
快速排序用到了分治思想,同样的还有归并排序。乍看起来快速排序和归并排序非常相似,都是将问题变小,先排序子串,最后合并,但是思路完全不一样
快速排序的基本思想:如果要排序数组中下标从 p 到 r 之间的一组数据,选择 p 到 r 之间的任意一个数据作为 pivot(分区点)
遍历 p 到 r 之间的数据,将小于 pivot 的放到左边,将大于 pivot 的放到右边,将 pivot 放到中间。经过这一步骤之后,数组 p 到 r 之间的数据就被分成了三个部分,前面 p 到 q-1 之间都是小于 pivot 的,中间是 pivot,后面的 q+1 到 r 之间是大于 pivot 的
7.1 算法步骤
- 选择序列中任意一个元素,作为 “基准”(
pivot
) - 重新排列序列,将所有比基准值小的元素摆放在基准前面,所有比基准值大的摆在基准的后面(相同的数可以到任一边)。在这个操作结束之后,该基准就处于数列的中间位置。这个称为分区(partition)操作
- 递归地把小于基准值元素的子序列和大于基准值元素的子序列进行快速排序
7.2 代码实现
@Slf4j
public class QuickSort {
public static void main(String[] args) {
int[] data = {6, 4, 3, 23, 27, 23, 14, 10};
quickSort(data, 0, data.length - 1);
for (int sort : data) {
log.info(sort + " ");
}
}
/**
* <ul>
* <li> 选择 p 到 r 之间的任意一个数据作为 pivot(分区点), 遍历 p 到 r 之间的数据, 将小于 pivot 的放到左边,
* 将大于 pivot 的放到右边, 将 pivot 放到中间 </li>
* <li> 经过这一步骤之后, 数组 p 到 r 之间的数据就被分成了三个部分, 前面 p 到 q-1 之间都是小于 pivot 的, 中间是 pivot,
* 后面的 q+1 到 r 之间是大于 pivot 的 </li>
* </ul>
*
* @param arr 待排序数组
* @param low 起始位置
* @param high 终止位置
* @author Fan
* @since 2023/5/17 15:06
*/
private static void quickSort(int[] arr, int low, int high) {
if (low < high) {
int pivot = partition(arr, low, high);
quickSort(arr, low, pivot - 1);
quickSort(arr, pivot + 1, high);
}
}
/**
* 分区函数, 返回 pivot 的位置
*
* @param arr 要分区的数组
* @param low 起始位置
* @param high 终止位置
* @return {@link int}
* @author Fan
* @since 2023/5/17 15:09
*/
private static int partition(int[] arr, int low, int high) {
int pointer = low;
// 分区比较次数
for (int i = low; i < high; i++) {
// 这里取最后一位元素作为 pivot
if (arr[i] <= arr[high]) {
// 将小于 pivot 的依次交换到前面的位置
int temp = arr[i];
arr[i] = arr[pointer];
arr[pointer] = temp;
pointer++;
}
}
// 交换 pivot 到两个序列中间位置
int temp = arr[pointer];
arr[pointer] = arr[high];
arr[high] = temp;
return pointer;
}
}
7.3 算法分析
- 稳定性:不稳定
- 时间复杂度:最佳:O(nlogn), 最差:O(nlogn),平均:O(nlogn)
- 空间复杂度:O(nlogn)
8. 计数排序 (Counting Sort)
计数排序的核心在于将输入的数据值转化为键存储在额外开辟的数组空间中。 作为一种线性时间复杂度的排序,计数排序要求输入的数据必须是有确定范围的整数
计数排序 (Counting sort) 是一种稳定的排序算法。计数排序使用一个额外的数组 C
,其中第 i
个元素是待排序数组 A
中值等于 i
的元素的个数。然后根据数组 C
来将 A
中的元素排到正确的位置。它只能对整数进行排序
8.1算法步骤
- 找出数组中的最大值
max
、最小值min
- 创建一个新数组
C
,其长度是max - min + 1
,其元素默认值都为 0 - 遍历原数组
A
中的元素A[i]
,以A[i]-min
作为C
数组的索引,以A[i]
的值在A
中元素出现次数作为C[A[i]-min]
的值 - 对
C
数组变形,新元素的值是该元素与前一个元素值的和,即当i>1
时C[i] = C[i] + C[i-1]
- 创建结果数组
R
,长度和原始数组一样 - 从后向前遍历原始数组
A
中的元素A[i]
,使用A[i]
减去最小值min
作为索引,在计数数组C
中找到对应的值C[A[i]-min]
,C[A[i]-min]-1
就是A[i]
在结果数组R
中的位置,做完上述这些操作,将count[A[i]-min]
减小 1
8.2 代码实现
@Slf4j
public class CountingSort {
public static void main(String[] args) {
int[] data = {6, 4, 3, 23, 27, 23, 14, 10};
int[] countingSort = countingSort(data);
for (int datum : countingSort) {
log.info(datum + " ");
}
}
/**
* <ul>
* <li> 先得到数组的数据范围, 然后根据数据范围创建计数数组, 即为每一个范围内的值都分配一个 "桶",
* 这里的 "桶" 和桶排序的桶有点不同, 由于只需要记录元素的个数即可, 所以 "桶" 为数组的 1 位 </li>
* <li> 然后计算数组相同值的元素个数, 即计数数组对应的索引的值. 然后对计数数组进行累加,
* 表示小于等于该值的元素有几个, 也就是说该值是数组中的第几个元素 </li>
* <li> 最后参照计数数组, 将数组值一个个放到对应的位置 </li>
* </ul>
*
* @param arr 待排序数组
* @return {@link int[]}
* @author Fan
* @since 2023/5/17 16:33
*/
private static int[] countingSort(int[] arr) {
if (arr.length < 2) {
return arr;
}
// 获取数组中数据范围
int[] extremum = getMinAndMax(arr);
int minValue = extremum[0];
int maxValue = extremum[1];
// 根据数据范围创建对应的计数数组
int[] countArr = new int[maxValue - minValue + 1];
int[] result = new int[arr.length];
// 计算每个元素的个数
for (int i = 0; i < arr.length; i++) {
countArr[arr[i] - minValue] += 1;
}
// 依次累加
for (int i = 1; i < countArr.length; i++) {
countArr[i] += countArr[i - 1];
}
for (int i = arr.length - 1; i >= 0; i--) {
// 获取数组值在计数数组中的值, 即表示数组的第几位元素, 减 1 即为索引位置
int index = countArr[arr[i] - minValue] - 1;
// 对应位置赋值
result[index] = arr[i];
// 计数数组相应的元素个数减 1
countArr[arr[i] - minValue] -= 1;
}
return result;
}
/**
* 获取数组中值最小的元素和最大的元素
*
* @param arr 数组
* @return {@link int[]}
* @author Fan
* @since 2023/5/17 16:32
*/
private static int[] getMinAndMax(int[] arr) {
int maxValue = arr[0];
int minValue = arr[0];
for (int v : arr) {
if (v > maxValue) {
maxValue = v;
} else if (v < minValue) {
minValue = v;
}
}
return new int[]{minValue, maxValue};
}
}
8.3 算法分析
当输入的元素是 n
个 0
到 k
之间的整数时,它的运行时间是 O(n+k)
。计数排序不是比较排序,排序的速度快于任何比较排序算法。由于用来计数的数组 C
的长度取决于待排序数组中数据的范围(等于待排序数组的最大值与最小值的差加上 1),这使得计数排序对于数据范围很大的数组,需要大量额外内存空间
- 稳定性:稳定
- 时间复杂度:最佳:O(n+k) 最差:O(n+k) 平均:O(n+k)
- 空间复杂度:O(k)
9. 桶排序 (Bucket Sort)
桶排序是计数排序的升级版,或者说计数排序是一种特殊的桶排序。它利用了函数的映射关系,高效与否的关键就在于这个映射函数的确定。为了使桶排序更加高效,我们需要做到这两点:
- 在额外空间充足的情况下,尽量增大桶的数量
- 使用的映射函数能够将输入的 N 个数据均匀的分配到 K 个桶中
桶排序的工作的原理:假设输入数据服从均匀分布,将数据分到有限数量的桶里,每个桶再分别排序(有可能再使用别的排序算法或是以递归方式继续使用桶排序进行)
9.1 算法步骤
- 设置一个 BucketSize,作为每个桶所能放置多少个不同数值
- 遍历输入数据,并且把数据依次映射到对应的桶里去
- 对每个非空的桶进行排序,可以使用其它排序方法,也可以递归使用桶排序
- 从非空桶里把排好序的数据拼接起来
9.2 代码实现
public class BucketSort {
public static void main(String[] args) {
int[] data = {6, 4, 3, 23, 27, 23, 14, 10};
List<Integer> list = bucketSort(Arrays.stream(data).boxed().collect(Collectors.toList()), 3);
System.out.println(list);
}
/**
* <ul>
* <li> 先得到数组的数据范围, 然后除以每个桶的大小计算出桶的数量, 再创建对应数量的桶, 添加进桶的集合里 </li>
* <li> 然后将数据分配进对应的桶里, 如果数据不均匀导致某些桶的数据过多, 则对该桶再进行划分 </li>
* <li> 然后再将每个桶的数据依次放入结果集里 </li>
* </ul>
*
* @param arr 待排序数组
* @param bucketSize 桶的大小
* @return {@link List<Integer>}
* @author Fan
* @since 2023/5/18 9:14
*/
private static List<Integer> bucketSort(List<Integer> arr, int bucketSize) {
if (arr.size() < 2 || bucketSize == 0) {
return arr;
}
// 数组数据范围
int[] extremum = getMinAndMax(arr);
int minValue = extremum[0];
int maxValue = extremum[1];
// 桶的数量
int bucketCount = (maxValue - minValue) / bucketSize + 1;
// 各个桶的集合
List<List<Integer>> buckets = new ArrayList<>();
// 有几个桶即创建几个, 添加到桶的集合中
for (int i = 0; i < bucketCount; i++) {
buckets.add(new ArrayList<>());
}
// 将数据分配到各个桶中
for (Integer v : arr) {
int index = (v - minValue) / bucketSize;
buckets.get(index).add(v);
}
// 单独对每个桶进行排序
for (int i = 0; i < buckets.size(); i++) {
// 这里的排序继续使用桶排序递归的方式
if (buckets.get(i).size() > 1) {
buckets.set(i, bucketSort(buckets.get(i), bucketSize / 2));
}
}
// 结果集
List<Integer> result = new ArrayList<>();
// 将各个桶中的数据合并到结果集中
for (List<Integer> bucket : buckets) {
result.addAll(bucket);
}
return result;
}
private static int[] getMinAndMax(List<Integer> arr) {
int maxValue = arr.get(0);
int minValue = arr.get(0);
for (Integer v : arr) {
if (v > maxValue) {
maxValue = v;
} else if (v < minValue) {
minValue = v;
}
}
return new int[]{minValue, maxValue};
}
}
9.3 算法分析
- 稳定性:稳定
- 时间复杂度:最佳:O(n+k) 最差:O(n2)O(n^2)O(n2) 平均:O(n+k)
- 空间复杂度:O(k)
10. 基数排序 (Radix Sort)
基数排序也是非比较的排序算法,对元素中的每一位数字进行排序,从最低位开始排序,复杂度为 O(n×k)
,n
为数组长度,k
为数组中元素的最大的位数
基数排序是按照低位先排序,然后收集;再按照高位排序,然后再收集;依次类推,直到最高位。有时候有些属性是有优先级顺序的,先按低优先级排序,再按高优先级排序。最后的次序就是高优先级高的在前,高优先级相同的低优先级高的在前。基数排序基于分别排序,分别收集,所以是稳定的
10.1 算法步骤
- 取得数组中的最大数,并取得位数,即为迭代次数
N
(例如:数组中最大数值为 1000,则N=4
) A
为原始数组,从最低位开始取每个位组成radix
数组- 对
radix
进行计数排序(利用计数排序适用于小范围数的特点) - 将
radix
依次赋值给原数组 - 重复 2~4 步骤
N
次
10.2 代码实现
@Slf4j
public class RadixSort {
public static void main(String[] args) {
int[] data = {6, 4, 3, 23, 27, 23, 14, 10};
int[] radixSort = radixSort(data);
for (int datum : radixSort) {
log.info(datum + " ");
}
}
/**
* <ul>
* <li> 先得到数组数据的最大值, 然后计算最大值的位数, 位数即遍历次数, 从低位开始排序 </li>
* <li> 每一位的排序方式采用稳定排序, 这里使用桶排序, 不过桶的大小为 1, 每排好一位就重新替换原来的数组顺序为排好当前位后的顺序 </li>
* </ul>
*
* @param arr 待排序数组
* @return {@link int[]}
* @author Fan
* @since 2023/5/18 15:16
*/
private static int[] radixSort(int[] arr) {
if (arr.length < 2) {
return arr;
}
// 获取数组数据的最大值
int maxValue = arr[0];
for (int v : arr) {
if (v > maxValue) {
maxValue = v;
}
}
// 求最大值的位数
int n = 1;
while (maxValue / 10 != 0) {
maxValue = maxValue / 10;
n += 1;
}
// 位数即遍历次数, 从最低位开始, 对每一位进行排序
for (int i = 0; i < n; i++) {
// 其中对每一位的排序需要采用稳定排序, 则使用桶排序, 桶大小为 1
// 创建桶集合, 由于一位最多只有 0-9 十个数, 即最多十个桶
List<List<Integer>> radix = new ArrayList<>();
for (int k = 0; k < 10; k++) {
radix.add(new ArrayList<>());
}
// 取数组数据的一位分配到对应的桶里
for (int v : arr) {
int index = (v / (int) Math.pow(10, i)) % 10;
radix.get(index).add(v);
}
// 将各个桶的数据依次取出形成新的数组顺序
int index = 0;
for (List<Integer> rdx : radix) {
for (Integer v : rdx) {
arr[index++] = v;
}
}
}
return arr;
}
}
10.3 算法分析
- 稳定性:稳定
- 时间复杂度:最佳:O(n×k) 最差:O(n×k) 平均:O(n×k)
- 空间复杂度:O(n+k)
11. 堆排序
堆排序是指利用堆这种数据结构所设计的一种排序算法。堆是一个近似完全二叉树的结构,并同时满足堆的性质:即子结点的值总是小于(或者大于)它的父节点
11.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
,则整个排序过程完成
11.2 代码实现
public class HeapSort {
public static void main(String[] args) {
// 下面的计算都是以数组从下标 1 的位置开始的, 所以前面存了一个 0. 假如要从下标 0 的位置开始计算, 那么计算子节点的公式再都加上 1 即可
int[] data = {0, 6, 4, 3, 23, 27, 23, 14, 10};
heapSort(data, data.length - 1);
for (int datum : data) {
System.out.print(datum + " ");
}
}
/**
* 建堆, 即从上往下堆化的过程, 因为叶子节点往下堆化只能自己跟自己比较, 所以直接从第一个非叶子节点开始,依次堆化就行了 <br>
* 由于堆是一个完全二叉树, 所以 n/2 到 n 的节点都是叶子节点, 即只需要堆化 1 到 n/2 的节点就可以了
*
* @param arr 数组
* @param n 数据个数
* @author Fan
* @since 2023/6/7 14:26
*/
private static void buildHeap(int[] arr, int n) {
for (int i = n / 2; i >= 1; i--) {
heapify(arr, n, i);
}
}
/**
* 从上往下进行堆化, 即判断左右子节点的值是否大于父节点的值, 大于则交换位置
*
* @param storage 数组
* @param count 元素数量
* @param i 开始下标
* @author Fan
* @since 2023/6/6 14:26
*/
private static void heapify(int[] storage, int count, int i) {
while (true) {
int max = i;
// 左子节点是否大于父节点
if (2 * i <= count && storage[max] < storage[2 * i]) {
max = 2 * i;
}
// 右子节点是否大于父节点或左子节点
if (2 * i + 1 <= count && storage[max] < storage[2 * i + 1]) {
max = 2 * i + 1;
}
// 不大于则直接退出
if (max == i) {
break;
}
// 大于则交换位置
swap(storage, i, max);
// 从新的下标继续开始堆化
i = max;
}
}
/**
* 交换下标为 i 和 j 的两个元素
*
* @param arr 数据
* @param i 下标 i
* @param j 下标 j
* @author Fan
* @since 2023/6/6 11:32
*/
private static void swap(int[] arr, int i, int j) {
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
/**
* 排序, 即将元素一个个放到堆顶, 依次堆化, 直到最后一个元素, 则排序完成
*
* @param arr 数组
* @param n 元素个数
* @author Fan
* @since 2023/6/7 14:37
*/
public static void heapSort(int[] arr, int n) {
buildHeap(arr, n);
int k = n;
while (k > 1) {
swap(arr, 1, k--);
heapify(arr, k, 1);
}
}
}
11.3 算法分析
- 稳定性:不稳定
- 时间复杂度:最佳:O(nlogn), 最差:O(nlogn), 平均:O(nlogn)
- 空间复杂度:O(1)