排序算法
常见的算法主要分为两类,一是比较排序,而是非比较排序。(哈哈,感觉有点废话)
- 比较排序 :比较排序主要是通过比较的方式来决定元素之间的相对次序,它的时间复杂度(平均)是不能突破 O ( n l o g n ) O(nlogn) O(nlogn)的,因此是非线性的
- 非比较排序:非比较的排序就是为了突破比较类的时间复杂度,它是不通过比较来决定元素之间的相对次序,它是以线性的时间来运行的,因此为线性时间非比较类排序
这是整个算法的一张简单罗列,这里简单的将希尔排序放到了插入排序中,希尔排序当间隔为1的时候应该就是插入排序了吧。堆排序思想上和选择排序很像。
算法复杂度
在各种笔试题目中经常会涉及到一些算法时间复杂度的考试啥的,给你张表罗列一下,方便快速查找。当然要是对这些算法都熟悉和理解之后,肯定是不用死记硬背的,先来张表吧,方便回忆和检查
排序方法 | 平均时间复杂度 | 最坏时间复杂度 | 最好时间复杂度 | 空间复杂度 | 稳定性 |
---|---|---|---|---|---|
冒泡排序 | O ( n 2 ) O(n^2) O(n2) | O ( n 2 ) O(n^2) O(n2) | O ( n ) O(n) O(n) | O ( 1 ) O(1) O(1) | 稳定 |
插入排序 | O ( n 2 ) O(n^2) O(n2) | O ( n 2 ) O(n^2) O(n2) | O ( n ) O(n) O(n) | O ( 1 ) O(1) O(1) | 稳定 |
选择排序 | O ( n 2 ) O(n^2) O(n2) | O ( n 2 ) O(n^2) O(n2) | O ( n 2 ) O(n^2) O(n2) | O ( 1 ) O(1) O(1) | 不稳定 |
希尔排序 | O ( n l o g n ) o r O ( O 1.3 ) O(nlogn) \ or \ O(O^{1.3}) O(nlogn) or O(O1.3) | O ( n 2 ) O(n^2) O(n2) | O ( n ) O(n) O(n) | O ( 1 ) O(1) O(1) | 不稳定 |
归并排序 | O ( n l o g n ) O(nlogn) O(nlogn) | O ( n l o g n ) O(nlogn) O(nlogn) | O ( n l o g n ) O(nlogn) O(nlogn) | O ( n ) O(n) O(n) | 稳定 |
堆排序 | O ( n l o g n ) O(nlogn) O(nlogn) | O ( n l o g n ) O(nlogn) O(nlogn) | O ( n l o g n ) O(nlogn) O(nlogn) | O ( 1 ) O(1) O(1) | 不稳定 |
快速排序 | O ( n l o g n ) O(nlogn) O(nlogn) | O ( n 2 ) O(n^2) O(n2) | O ( n l o g n ) O(nlogn) O(nlogn) | O ( l o g n ) O(logn) O(logn) | 不稳定 |
计数排序 | O ( n + k ) O(n+k) O(n+k) | O ( n + k ) O(n+k) O(n+k) | O ( n + k ) O(n+k) O(n+k) | O ( n + k ) O(n+k) O(n+k) | 稳定 |
桶排序 | O ( n + k ) O(n+k) O(n+k) | O ( n 2 ) O(n^2) O(n2) | O ( n ) O(n) O(n) | O ( n + k ) O(n+k) O(n+k) | 稳定 |
基数排序 | O ( n ∗ k ) O(n*k) O(n∗k) | O ( n ∗ k ) O(n*k) O(n∗k) | O ( n ∗ k ) O(n*k) O(n∗k) | O ( n + k ) O(n+k) O(n+k) | 稳定 |
其中选择排序的最好、最坏、平均时间复杂度均为
O
(
n
2
)
O(n^2)
O(n2),归并和堆排序的时间复杂度都是
O
(
n
l
o
g
n
)
O(nlogn)
O(nlogn)
对于堆排序的空间复杂度是
O
(
1
)
O(1)
O(1) ,堆排序的初始建堆的时间复杂度是
O
(
n
)
O(n)
O(n),每次进行调整的时间复杂度为
O
(
l
o
g
n
)
O(logn)
O(logn)
- 稳定性: 在原始序列中,如果a=b,并且a排在b的前面,排序完成后a仍然排列在b的前面,则该排序算法是稳定的;
- 不稳定性: 在原始序列中,如果a=b,排序之后a可能会排列在b的后面,那么该算法时不稳定的;
具体算法
1. 冒泡排序
冒泡排序是一种简单的排序算法,相对较为简单。它重复遍历要排序的数列,相邻两个元素之间进行比较,如果顺序错了就将它们交换过来。遍历数列的工作是重复重复进行没有再需要进行交换的了,也就是说该数列已经排序完成了。这个算法的名字由来是因为越小的元素会经由交换慢慢浮到数列的顶端
1.1 算法描述
- 比较相邻的元素。如果第一个比第二个大,就交换它们两个;
- 对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对,这样在最后的元素应该会是最大的数;
- 针对所有的元素重复以上的步骤,除了最后一个;
- 重复步骤1~3,直到排序完成。
1.2 动图展示
1.3 代码实现(java版)
import java.util.Arrays;
import java.util.List;
// import static special_topic.sort.basic.Swap.swapTwoNumber;
/**
* 冒泡排序
* Function: 正常的冒泡排序是先将最大的数据归好位置,然后是次大,以此类推,最后将整个数列排列好
* 冒泡排序是一种简单的排序算法。它重复地走访过要排序的数列,一次比较两个元素,如果它们的顺序错误就把它们交换过来。走访数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。这个算法的名字由来是因为越小的元素会经由交换慢慢浮到数列的顶端
* ######算法描述######
* 比较相邻的元素。如果第一个比第二个大,就交换它们两个;
* 对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对,这样在最后的元素应该会是最大的数
* 针对所有的元素重复以上的步骤,除了最后一个
* 重复步骤1-3,直到排序完成
* ####################
*
* 动态图: https://mmbiz.qpic.cn/mmbiz_gif/951TjTgiabkzow2ORRzgpfHIGAKIAWlXmYTANnYjhiaAU3RyxWw2WyWaicgZmfuyYP1s5iaUQaJh3ShGog3XliceqTw/640?wx_fmt=gif&tp=webp&wxfrom=5&wx_lazy=1
* 时间复杂度: 平均 O(n^2), 最坏 O(n^2), 最好O(n), 空间复杂度O(1), 是稳定的
* */
public class BubbleSort {
// 两数交换
public static void swapTwoNumber(List<Integer> arr, int index1, int index2) {
if(index1 >= arr.size() || index2 > arr.size() || arr == null)
return;
Integer temp = arr.get(index1);
arr.set(index1, arr.get(index2));
arr.set(index2, temp);
}
// 冒泡排序
public static List<Integer> BubbleSort(List<Integer> bubbleArr) {
for(int i = 0; i < bubbleArr.size() - 1; i ++) { // 外层循环,用于判断
for(int j = 1; j < bubbleArr.size() - i ; j ++) {
if(bubbleArr.get(j) < bubbleArr.get(j - 1)) {
swapTwoNumber(bubbleArr, j, j - 1); // 交换两数
}
}
}
return bubbleArr;
}
// 带有哨兵的冒泡排序
public static List<Integer> BubbleSort_EalyStop_v(List<Integer> bubbleArr) {
for(int i = 0; i < bubbleArr.size() - 1; i ++) {
boolean flag = true; // 用于标记,记录该数组是否已经为有序序列
// j的最大值为arr.size() - 1 - i, 减去i是因为大的数据已经在对应的位置上了
for (int j = 0; j < bubbleArr.size() - 1 - i; j++) {
if(bubbleArr.get(j) > bubbleArr.get(j + 1)) { // 相邻两个数进行比较,如果顺序错位则进行替换
flag = false;
swapTwoNumber(bubbleArr, j, j + 1);
}
}
if (flag) // 该数列已经为有序序列了
break;
}
return bubbleArr;
}
// 测试
public static void main(String[] args) {
// List<Integer> arr = new ArrayList<>();
Integer[] source = {3, 44, 38, 5, 47, 15, 36, 26, 27, 2, 46, 4, 19, 50, 48};//{1, 3, 5, 7, 8, 2, 4, 6, 9, 7};
List<Integer> bubbleArr = Arrays.asList(source); // 将数据替换成List的方式
// 冒泡排序
System.out.println("冒泡排序前: " + bubbleArr);
List<Integer> bubbleRes = BubbleSort(bubbleArr);
System.out.println("冒泡排序的结果为: " + bubbleRes);
}
}
1.4 算法分析
时间复杂度: 平均 O ( n 2 ) O(n^2) O(n2), 最坏 O ( n 2 ) O(n^2) O(n2), 最好 O ( n ) O(n) O(n), 空间复杂度 O ( 1 ) O(1) O(1), 是稳定的 。 上面BubbleSort是冒泡排序的最原始的版本,正常的冒泡排序是先将最大的数据归好位置,然后是次大,以此类推,最后将整个数列排列好。但上面的冒泡排序能够进行改进,添加一个哨兵,监控是否序列是否已经有序,提前停止。
2. 选择排序
选择排序(Selection-sort)是一种简单直观的排序算法。它的工作原理:首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置,然后,再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。以此类推,直到所有元素均排序完毕。
2.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.2 动图展示
对比选择排序和冒泡排序
相同点: 数据都是从小到大或者从大到小的顺序依次确定,都是先确定最小(大)数据的位置,每扫描依次都是找到无序序列中最小(大)的数,并将它们放到最终的位置上。
不同点: 冒泡排序时稳定的,选择排序时不稳定的,从一次排序的结果可以看出,冒泡排序会发生两两交换,无序区中很多数据的位置都发生了变换,而选择排序一般就只有两个位置上的数据发生变化,一个是无序区的第一个位置,一个是无序区中最小(大)的那个数。
2.3 代码实现(java版)
import java.util.Arrays;
import java.util.List;
import static special_topic.sort.basic.Swap.swapTwoNumber;
/**
* 选择排序
* Function: 选择排序就是将整个数列划分成两个部分,第一部分为有序序列,第二部分为无序序列,每操作一次都是从无序序列中取一个数加入到有序序列中
* 选择排序是一种简单直观的排序算法。它的工作原理:首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置,然后,再从剩余未排序元素中继续寻找最小(大)元素,然后放到排序序列的末尾。以此类推,直到所有元素均排序完毕。
* ######算法描述######
* 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趟结束,数据有序化了
* ####################
*
* 动态图: https://mmbiz.qpic.cn/mmbiz_gif/951TjTgiabkzow2ORRzgpfHIGAKIAWlXmx157gYXLDH8zyJibDXSKolcr2PCXv7p6SkTloUJm502BLGnSFQw1ibVQ/640?wx_fmt=gif&tp=webp&wxfrom=5&wx_lazy=1
* 时间复杂度: 平均 O(n^2), 最坏 O(n^2), 最好O(n^2), 空间复杂度O(1), 是不稳定的
*
* 算法分析: 表现最稳定的排序算法之一,因为无论什么数据进去都是O(n^2)的时间复杂度,所以用到它的时候,数据规模越小越好。唯一的好处可能就是不占用额外的内存
* 空间吧。理论上讲,选择排序可能也是平常排序一般人想到的最多的排序方法吧。
* */
public class SelectionSort {
// 两数交换
public static void swapTwoNumber(List<Integer> arr, int index1, int index2) {
if(index1 >= arr.size() || index2 > arr.size() || arr == null)
return;
Integer temp = arr.get(index1);
arr.set(index1, arr.get(index2));
arr.set(index2, temp);
}
// 选择排序
public static List<Integer> SelectionSort(List<Integer> selectionArr) {
for(int i = 0; i < selectionArr.size() - 1; i ++) { // selectionArr.size() - 1:因为selectionArr.size() - 1个数据都排列好了,最后一个也就自动确定好位置了
int minIndex = i; // 寻找最小数的下标
for (int j = i + 1; j < selectionArr.size(); j++) {
if (selectionArr.get(minIndex) > selectionArr.get(j)) // 寻找最小的数, 确定获取当前无序区中最小的那个数的下标
minIndex = j; // 将最小数的索引保留
}
if (minIndex != i) {
swapTwoNumber(selectionArr, i, minIndex); // 将无序区中的最小数加入到有序区中;
}
}
return selectionArr;
}
public static void main(String[] args) {
// List<Integer> arr = new ArrayList<>();
Integer[] source = {3, 44, 38, 5, 47, 15, 36, 26, 27, 2, 46, 4, 19, 50, 48};//{1, 3, 5, 7, 8, 2, 4, 6, 9, 7};
List<Integer> selectionArr = Arrays.asList(source); // 将数据替换成List的方式
// 选择排序
System.out.println("交换前: " + Arrays.asList(selectionArr));
List<Integer> selectionRes = SelectionSort(selectionArr);
System.out.println("选择排序的结果为: " + Arrays.asList(selectionRes));
}
}
2.4 算法分析
时间复杂度: 平均 O ( n 2 ) O(n^2) O(n2), 最坏 O ( n 2 ) O(n^2) O(n2), 最好 O ( n 2 ) O(n^2) O(n2), 空间复杂度 O ( 1 ) O(1) O(1), 是不稳定的。 表现最稳定的排序算法之一了,是几个平均时间复杂度为 O ( n 2 ) O(n^2) O(n2)里面唯一一个最坏、最好、平均时间复杂度不变的排序算法了,数据规模越小越好。唯一的好处可能就是不占用额外的内存空间了吧。
3. 插入排序
插入排序(Insertion-Sort)的算法描述是一种简单直观的排序算法。它的工作原理是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。有一点是比较重要的:在一定条件下,当序列基本有序时,优先选用插入排序时比较好的方法。【一定条件可能就是指的是在比较排序中 😐】
3.1 算法描述
一般来说,插入排序都采用in-place在数组上实现。具体算法描述如下:
- 从第一个元素开始,该元素可以认为已经被排序;
- 取出下一个元素,在已经排序的元素序列中从后向前扫描;
- 如果该元素(已排序)大于新元素,将该元素移到下一位置;
- 重复步骤3,直到找到已排序的元素小于或者等于新元素的位置;
- 将新元素插入到该位置后;
- 重复步骤2~5。
3.2 动图展示
插入排序就是到当前位置时,将当前位置之前的所有元素都排列好。
3.3 代码实现(java版)
/**
* Author: snowy Time: 2019/03/30
* 插入排序
* Function: 插入排序其实是每扫描一个数据,将到当前数据为止的数列排好序,每扫一个数据,将数据插入到已排好序的列表中
* 插入排序的算法是一种简单直观的排序算法。它的工作原理是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。
* ######算法描述######一般来说,插入排序都采用in-place在数组上实现
* 从第一个元素开始,该元素可以认为已经被排序;
* 取出下一个元素,在已经排序的元素序列中从后向前扫描;
* 如果该元素(已排序)大于新元素,将该元素移到下一位置;
* 重复步骤3,直到找到已排序的元素小于或者等于新元素的位置;
* 将新元素插入到该位置后;
* 重复步骤2-5。
* ####################
*
* 动态图: https://mmbiz.qpic.cn/mmbiz_gif/951TjTgiabkzow2ORRzgpfHIGAKIAWlXmC96MboiaHeWD5mX8KcO6ZnQTeicJ4oT33nA0fLFjY6dkDdickB4sjq6CA/640?wx_fmt=gif&tp=webp&wxfrom=5&wx_lazy=1
* 时间复杂度: 平均 O(n^2), 最坏 O(n^2), 最好O(n), 空间复杂度O(1), 是稳定的
* 算法分析: 插入排序在实现上,通常采用in-place排序(即只需要用到O(1)的额外空间的排序),因而在从后向前扫描过程中,需要反复把已排序元素逐步向后挪位,为新元素提供插入空间
* */
import java.util.Arrays;
import java.util.List;
public class InsertionSort {
public static List<Integer> InsertionSort(List<Integer> insertionArr) {
for(int i = 1; i < insertionArr.size(); i++) { // 遍历数组
Integer curNum = insertionArr.get(i);
int j = i - 1;
for (; j >= 0 && insertionArr.get(j) > curNum; j --) { // 遍历已经排好序的序列,找到当前数应该插入的位置
insertionArr.set(j + 1, insertionArr.get(j)); // 把已排序元素逐步向后挪位
}
insertionArr.set(j + 1, curNum); // 将数据插入到当前序列相应的位置
}
return insertionArr;
}
public static void main(String[] args){
Integer[] source = {3, 44, 38, 5, 47, 15, 36, 26, 27, 2, 46, 4, 19, 50, 48};
List<Integer> insertionArr = Arrays.asList(source);
// 插入排序
System.out.println("插入排序前: " + Arrays.asList(insertionArr));
List<Integer> insertionRes = InsertionSort(insertionArr);
System.out.println("插入排序的结果为: " + Arrays.asList(insertionRes));
}
}
3.4 算法分析
时间复杂度: 平均 O ( n 2 ) O(n^2) O(n2), 最坏 O ( n 2 ) O(n^2) O(n2), 最好 O ( n ) O(n) O(n), 空间复杂度 O ( 1 ) O(1) O(1), 是稳定的。 插入排序在实现上,通常采用in-place排序(即只需用到O(1)的额外空间的排序),因而在从后向前扫描过程中,需要反复把已排序元素逐步向后挪位,为最新元素提供插入空间。
4. 希尔排序
1959年Shell发明,第一个突破O(n2)的排序算法,是简单插入排序的改进版。它与插入排序的不同之处在于,它会优先比较距离较远的元素。希尔排序又叫缩小增量排序。
4.1 算法描述
先将整个待排序的记录序列分割成为若干子序列分别进行直接插入排序,具体算法描述:
- 选择一个增量序列t1,t2,…,tk,其中ti>tj,tk=1;
- 按增量序列个数k,对序列进行k 趟排序;
- 如果该元素(已排序)大于新元素,将该元素移到下一位置;
- 每趟排序,根据对应的增量ti,将待排序列分割成若干长度为m 的子序列,分别对各子表进行直接插入排序。仅增量因子为1 时,整个序列作为一个表来处理,表长度即为整个序列的长度。
4.2 动图展示
希尔排序当间隔为1的时候就是插入排序了
4.3 代码实现(java版)
/**
* Author: snowy Time: 2019/3/31
* 希尔排序
* Function: 希尔排序是由插入排序改进得到的。当希尔排序的间隔为1时,其实就是插入排序,因此插入排序是特殊的希尔排序
* 1959年Shell发明,第一个突破O(n^2)的排序算法,是简单插入排序的改进版,它与插入排序的不同之处在于,它会优先比较距离较远的元素。希尔排序又叫缩小增量排序
* ######算法描述###### 现将整个待排序的记录序列分割成为若干个子序列分别进行直接插入排序
* 选择一个增量序列t1, t2, ..., tk, 其中ti > tj, tk = 1
* 按增量序列个数k,对序列进行k趟排序
* 每趟排序,根据对应的增量ti,将待排序列分割成若干长度为m的子序列,分别对各子表进行直接插入排序。仅增量因子为1时,整个序列作为一个表来处理,表长度即为整个序列的长度
* ####################
*
* 动态图: https://mmbiz.qpic.cn/mmbiz_gif/951TjTgiabkzow2ORRzgpfHIGAKIAWlXm6GpRDRhiczgOdibbGBtpibtIhX4YRzibicUyEOSVh3JZBHtiaZPN30X1WOhA/640?wx_fmt=gif&tp=webp&wxfrom=5&wx_lazy=1
* 时间复杂度: 平均 O(n^1.3) || O(nlogn), 最坏 O(n^2), 最好O(n), 空间复杂度O(1), 是不稳定的
* 算法分析:希尔排序的核心在于间隔序列的设定。既可以提前设定好间隔序列,也可以动态的定义间隔序列。动态定义间隔序列的算法是《算法(第4版)》的合著者Robert Sedgewick提出的
* */
import java.util.Arrays;
import java.util.List;
public class ShellSort {
public static List<Integer> ShellSort(List<Integer> shellArr) {
for(int gap = shellArr.size() / 2; gap > 0; gap /= 2) { // 间隔序列, 逐步缩小增量
for (int i = gap; i < shellArr.size(); i ++ ) { // 从第gap个元素,逐个对所在组进行直接插入排序操作
int curNum = shellArr.get(i);
int j = i - gap;
for(; j >= 0 && shellArr.get(j) > curNum; j -= gap) { // 寻找最终插入的位置
shellArr.set(j + gap, shellArr.get(j));
}
shellArr.set(j + gap, curNum); // 插入
}
}
return shellArr;
}
public static void main(String[] args) {
// List<Integer> arr = new ArrayList<>();
Integer[] source = {3, 44, 38, 5, 47, 15, 36, 26, 27, 2, 46, 4, 19, 50, 48};//{1, 3, 5, 7, 8, 2, 4, 6, 9, 7};
List<Integer> shellArr = Arrays.asList(source); // 将数据替换成List的方式
// 希尔排序
System.out.println("希尔排序前: " + Arrays.asList(shellArr));
List<Integer> shellRes = ShellSort(shellArr);
System.out.println("希尔排序的结果为: " + Arrays.asList(shellRes));
}
}
4.4 算法分析
时间复杂度: 平均 O ( n l o g n ) O(nlogn) O(nlogn), 最坏 O ( n 2 ) O(n^2) O(n2), 最好 O ( n ) O(n) O(n), 空间复杂度 O ( 1 ) O(1) O(1), 是不稳定的。 希尔排序的核心在于间隔序列的设定。既可以提前设定好间隔序列,也可以动态的定义间隔序列。动态定义间隔序列的算法是《算法(第4版)》的合著者Robert Sedgewick提出的。
5. 归并排序
归并排序是建立在归并操作上的一种有效的排序算法。该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为2-路归并。
5.1 算法描述
- 把长度为n的输入序列分成两个长度为n/2的子序列;
- 对这两个子序列分别采用归并排序;
- 将两个排序好的子序列合并成一个最终的排序序列。
5.2 动图展示
5.3 代码实现(java版)
/**
* Author: snowy Time: 2019/3/31
* 归并排序
* Function: 归并排序使用分治方法进行排序,使用递归的方式进行实现
* 归并排序是建立在归并操作上的一种有效的排序算法。该算法是采用分治法(Divude and Conquer)的一个非常典型的应用。将已有的子序列合并,得到完全有序的序列;
* 即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,成为2-路归并。
* ######算法描述######
* 把长度为n的输入序列分成两个长度为n/2的子序列;
* 对这两个子序列分别采用归并排序
* 将两个排序好的子序列合并成一个最终的排序序列
* ####################
*
* 动态图: https://mmbiz.qpic.cn/mmbiz_gif/951TjTgiabkzow2ORRzgpfHIGAKIAWlXme9BR4OOicbVtD5OYT4HZ36lhMtlKiava7mcgbtNR8o9ibGdqOL7xgAFAQ/640?wx_fmt=gif&tp=webp&wxfrom=5&wx_lazy=1
* 时间复杂度: 平均 O(nlogn), 最坏 O(nlogn), 最好O(nlogn), 空间复杂度O(n), 是稳定的
* 算法分析:归并排序是一种稳定的排序方法。和选择排序一样,归并排序的性能不受输入数据的影响,但表现比选择排序好的多,因为始终都是O(nlogn)的时间复杂度。代价是需要额外的存储空间
* */
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
public class MergeSort {
// 二路归并, sortedArr1, sortedArr2均为有序序列
public static List<Integer> Merge(List<Integer> sortedArr1, List<Integer> sortedArr2) {
List<Integer> sortRes = new ArrayList<>();
int i = 0, j = 0, k = 0;
while(i < sortedArr1.size() && j < sortedArr2.size()) {
// 比较sortedArr1和sortedArr2中的元素大小
if (sortedArr1.get(i) <= sortedArr2.get(j)) {
sortRes.add(k, sortedArr1.get(i));
i ++;
} else {
sortRes.add(k, sortedArr2.get(j));
j ++;
}
k ++;
}
// 如果sortedArr1中的元素已经全部加入到sortRes中了,将sortedArr2剩余的元素添加到sortedRes中
if (i == sortedArr1.size()) {
sortRes.addAll(k, sortedArr2.subList(j, sortedArr2.size()));
}
// 如果sortedArr2中的元素已经全部加入到sortRes中了,将sortedArr1剩余的元素添加到sortedRes中
if (j == sortedArr2.size()) {
sortRes.addAll(k, sortedArr1.subList(i, sortedArr1.size()));
}
return sortRes;
}
public static List<Integer> MergeSort(List<Integer> mergeArr) {
if(mergeArr.size() <= 1) // 递归结束的条件
return mergeArr;
// 将序列递归切分
List<Integer> leftArr = MergeSort(mergeArr.subList(0, mergeArr.size() / 2));
List<Integer> rightArr = MergeSort(mergeArr.subList(mergeArr.size() / 2, mergeArr.size()));
return Merge(leftArr, rightArr); // 进行递归合并
}
public static void main(String[] args) {
// List<Integer> arr = new ArrayList<>();
Integer[] source = {3, 44, 38, 5, 47, 15, 36, 26, 27, 2, 46, 4, 19, 50, 48};//{1, 3, 5, 7, 8, 2, 4, 6, 9, 7};
List<Integer> arr = Arrays.asList(source); // 将数据替换成List的方式
// 归并排序
System.out.println("归并排序前: " + Arrays.asList(arr));
List<Integer> mergeRes = MergeSort(arr);
System.out.println("归并排序的结果为: " + Arrays.asList(mergeRes));
}
}
5.4 算法分析
时间复杂度: 平均 O ( n l o g n ) O(nlogn) O(nlogn), 最坏 O ( n l o g n ) O(nlogn) O(nlogn), 最好 O ( n l o g n ) O(nlogn) O(nlogn), 空间复杂度 O ( n ) O(n) O(n), 是稳定的。 归并排序是一种稳定的排序方法。和选择排序一样,归并排序的性能不受输入数据的影响,但表现比选择排序好的多,因为始终都是O(nlogn)的时间复杂度。代价是需要额外的内存空间。并且归并排序通常能够用于大数排序中
6、快速排序(Quick Sort)
快速排序的基本思想:通过一趟排序将待排记录分隔成独立的两部分,其中一部分记录的关键字均比另一部分的关键字小,则可分别对这两部分记录继续进行排序,以达到整个序列有序。
6.1 算法描述
快速排序使用分治法来把一个串(list)分为两个子串(sub-lists)。具体算法描述如下:
- 从数列中挑出一个元素,称为 “基准”(pivot);
- 重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。在这个分区退出之后,该基准就处于数列的中间位置。这个称为分区(partition)操作;
- 递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序。
6.2 动图展示
6.3 代码实现(java版)
/**
* Author: snowy Time: 2019/3/31
* 归并排序
* Function: 归并排序使用分治方法进行排序,使用递归的方式进行实现
* 归并排序是建立在归并操作上的一种有效的排序算法。该算法是采用分治法(Divude and Conquer)的一个非常典型的应用。将已有的子序列合并,得到完全有序的序列;
* 即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,成为2-路归并。
* ######算法描述######
* 把长度为n的输入序列分成两个长度为n/2的子序列;
* 对这两个子序列分别采用归并排序
* 将两个排序好的子序列合并成一个最终的排序序列
* ####################
*
* 动态图: https://mmbiz.qpic.cn/mmbiz_gif/951TjTgiabkzow2ORRzgpfHIGAKIAWlXme9BR4OOicbVtD5OYT4HZ36lhMtlKiava7mcgbtNR8o9ibGdqOL7xgAFAQ/640?wx_fmt=gif&tp=webp&wxfrom=5&wx_lazy=1
* 时间复杂度: 平均 O(nlogn), 最坏 O(nlogn), 最好O(nlogn), 空间复杂度O(n), 是稳定的
* 算法分析:归并排序是一种稳定的排序方法。和选择排序一样,归并排序的性能不受输入数据的影响,但表现比选择排序好的多,因为始终都是O(nlogn)的时间复杂度。代价是需要额外的存储空间
* */
/**
* Author: snowy Time: 2019/3/31
* 快速排序
* Function: 快速排序是每次将选中的数送到该数对应的最终位置上
* 快速排序的基本思想:通过一趟排序将待排记录分隔成独立的两部分,其中一部分记录的关键字均比另一部分的关键字小,则可分别对这两部分记录继续进行排序,以达到整个序列有关
* ######算法描述###### 快速排序使用分治法来把一个串(list)分成两个子串(sub-lists)。
* 从数列中挑出一个元素,称为“基准”(pivot)
* 重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。在这个分区退出之后,该基准就处于数列的中间位置。这个称为分区操作
* 递归地把小于基准值元素的子序列和大于基准值元素的子序列排序
* ####################
*
* 动态图: https://mmbiz.qpic.cn/mmbiz_gif/951TjTgiabkzow2ORRzgpfHIGAKIAWlXmia2UqkZMibKlXzZ6OqEATKJ7iajmEc9zRiciav9Z05mP6P4ZqfJia0ia2djUw/640?wx_fmt=gif&tp=webp&wxfrom=5&wx_lazy=1
* * 时间复杂度: 平均 O(nlogn), 最坏 O(n^2), 最好O(nlogn), 空间复杂度O(nlogn), 是不稳定的
* */
import java.util.Arrays;
import java.util.List;
// import static special_topic.sort.basic.Swap.swapTwoNumber;
public class QuickSort {
// 两数交换
public static void swapTwoNumber(List<Integer> arr, int index1, int index2) {
if(index1 >= arr.size() || index2 > arr.size() || arr == null)
return;
Integer temp = arr.get(index1);
arr.set(index1, arr.get(index2));
arr.set(index2, temp);
}
// 寻找分割的位置
public static int partition_v1(List<Integer> quickArr, int left, int right) {
int temp = quickArr.get(left); // 基准数据
while(left < right) {
// 当队尾的元素大于等于基准数据时,向前挪动right指针
while(quickArr.get(right) >= temp && left < right) {
right --;
}
if(left < right)
quickArr.set(left, quickArr.get(right)); // 如果队尾元素小于temp了,需要将其赋值给left的位置,因为当前这个位置left上可以认为值是无效的
// 当队首元素小于等于temp时,向前挪动left指针
while(quickArr.get(left) <= temp && left < right) {
left ++;
}
if(left < right)
quickArr.set(right, quickArr.get(left)); // 如果队尾元素小于temp了,需要将其赋值给left的位置,因为当前right这个位置上可以认为值是无效的
}
// 最后left和right是相等的,这个位置是用来安置temp的
quickArr.set(left, temp);
System.out.println(quickArr);
return left;
}
// 寻找分割的位置2
public static int partition_v2(List<Integer> quickArr, int left, int right) {
int current = quickArr.get(left);
int index = left; // 用于记录left上的值最终的位置,即记录下比current小的数有多少
for(int i = left + 1; i <= right; i ++) { // 在整个for循环中left上的值没有发生变化
if(quickArr.get(i) < current){
swapTwoNumber(quickArr, ++index, i); // 如果将小于current的数存放到一边
}
}
swapTwoNumber(quickArr, left, index); // 将序列从left到right中最后一个小于current的数与current交换位置
System.out.println(quickArr);
return index;
}
public static List<Integer> QuickSort(List<Integer> quickArr, int left, int right) {
if(left < right) {
int partitionIndex = partition_v1(quickArr, left, right);
// int partitionIndex = partition_v2(quickArr, left, right);
QuickSort(quickArr, left, partitionIndex - 1);
QuickSort(quickArr, partitionIndex + 1, right);
}
return quickArr;
}
// 三路快速排序: 三路快排划分为:大于当前值的数, 等于当前值的数和小于当前值的数
public static List<Integer> QuickSortThird(List<Integer> quickArr, int left, int right) {
if (left >= right) {
return quickArr;
}
int current = quickArr.get(left);
int small = left, large = right + 1; // small记录比当前值小的数,large记录比当前值大的数
int i = left;
while(i < large) {
if (quickArr.get(i) < current ) { // 小于当前值的数,则将这些数存放到数组前面来
swapTwoNumber(quickArr, i, small + 1);
small ++;
i++;
} else if (quickArr.get(i) > current) { // 大于当前值的数,则将这些数存放到数组的后面
swapTwoNumber(quickArr, i, large - 1);
large --;
} else { // 等于当前值的数
i ++;
}
}
swapTwoNumber(quickArr, left, small);
System.out.println(quickArr);
QuickSortThird(quickArr, left, small);
QuickSortThird(quickArr, large, right);
return quickArr;
}
public static void main(String[] args) {
// List<Integer> arr = new ArrayList<>();
Integer[] source = {3, 3, 44, 38, 5, 47, 15, 2, 4, 4, 36, 26, 27, 2, 46, 4, 19, 50, 48};//{1, 3, 6, 9, 7}; //
List<Integer> quickArr = Arrays.asList(source);
// 快速排序
System.out.println("快速排序前: " + Arrays.asList(quickArr));
List<Integer> quickRes = QuickSortThird(quickArr, 0, quickArr.size() - 1);
System.out.println("快速排序的结果为: " + quickRes);
System.out.println(Arrays.asList(source)); // source已经变成有序序列啦
// System.out.println("三路快速排序前: " + Arrays.asList(quickArr));
// List<Integer> quickRes3 = QuickSortThird(quickArr, 0, quickArr.size() - 1);
// System.out.println("三路快速排序结果为: " + quickRes3);
}
}
6.4 算法分析
时间复杂度: 平均 O ( n l o g n ) O(nlogn) O(nlogn), 最坏 O ( n 2 ) O(n^2) O(n2), 最好 O ( n l o g n ) O(nlogn) O(nlogn), 空间复杂度 O ( l o g n ) O(logn) O(logn), 是不稳定的。 动图中对应的是partition_v2,见过有好些题目会问快排的中间过程啥的,哪些好多都是针对partition_v1提问的,这种快排是挖空。快速排序每一趟排序结束都至少能够确定一个元素最终位置。
7. 堆排序
堆排序(Heapsort)是指利用堆这种数据结构所设计的一种排序算法。堆积是一个近似完全二叉树的结构,并同时满足堆积的性质:即子结点的键值或索引总是小于(或者大于)它的父节点。
7.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,则整个排序过程完成。
7.2 动图展示
7.3 代码实现(java版)
/**
* 堆排序: 参考 https://www.cnblogs.com/jingmoxukong/p/4303826.html#java版本
* Function: 堆排序主要分为两个部分,第一部分为初始建堆, 第二部分为得到当前最大值或者是最小值
* 堆排序的基本思想:堆排序是指利用堆这种数据结构所设计的一种排序算法。堆积是一个近似完全二叉树的结构,并同时满足堆积的性质:即子结点的键值或索引总是小于(或者大于)它的父节点
* ######算法描述######
* 将初始待排序关键字序列(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,则整个排序过程完成
* ####################
*
* 动态图: https://mmbiz.qpic.cn/mmbiz_gif/951TjTgiabkzow2ORRzgpfHIGAKIAWlXmcW0GonHkXQajLiaj8g5b6J4AofRe4iaofXUutpk61UpzTv2fUzWvRUjg/640?wx_fmt=gif&tp=webp&wxfrom=5&wx_lazy=1
* 时间复杂度: 平均 O(nlogn), 最坏 O(nlogn), 最好O(nlogn), 空间复杂度O(1), 是不稳定的
* 解析: 堆排序在排序算法中算是比较复杂的,要点在于使用数据实现堆的时候,父节点与其孩子结点的下标对应,
* 当前元素在数组中以R[i]表示,那么:
* (1)它的左孩子结点是: R[2 * i +1]
* (2)它的右孩子结点是: R[2 * i +2]
* (3)它的父结点为: R[(i - 1) / 2]
* */
import java.util.Arrays;
import java.util.List;
public class HeapSort {
// 两数交换
public static void swapTwoNumber(List<Integer> arr, int index1, int index2) {
if(index1 >= arr.size() || index2 > arr.size() || arr == null)
return;
Integer temp = arr.get(index1);
arr.set(index1, arr.get(index2));
arr.set(index2, temp);
}
public static void HeapAdjust(List<Integer> heapArr, int parent, int length) {
int current = heapArr.get(parent);
int child = 2 * parent + 1; // 左孩子
while(child < length) {
if (child + 1 < length && heapArr.get(child) < heapArr.get(child + 1)) // 如果有右孩子结点, 并且右孩子结点的值大于左孩子结点, 则取右孩子结点
child += 1; // 右孩子
// 如果父节点已经大于孩子结点的值,则直接结束
if(current >= heapArr.get(child))
break;
// 把孩子结点的值赋给父节点
heapArr.set(parent, heapArr.get(child));
// 选取孩子结点的左孩子结点,继续向下筛选
parent = child;
child = 2 * parent + 1;
}
heapArr.set(parent, current);
}
public static List<Integer> HeapSort(List<Integer> heapArr) {
// 第一步: 循环从而建立起初始堆
for(int i = heapArr.size() / 2 - 1; i >= 0; i --) { // heapArr.size()/2 为叶子结点的个数,
// 从第一个非叶子结点开始构建大顶堆
HeapAdjust(heapArr, i, heapArr.size());
}
//第二步: 调整堆结构+交换堆顶元素与末尾元素
for(int i = heapArr.size() - 1; i >= 0; i --) {
//将堆顶元素与末尾元素进行交换
swapTwoNumber(heapArr, 0, i);
//重新对堆进行调整
HeapAdjust(heapArr, 0, i);
}
return heapArr;
}
public static void main(String[] args) {
Integer[] source = {3, 44, 38, 5, 47, 15, 36, 26, 27, 2, 46, 4, 19, 50, 48};
List<Integer> heapArr = Arrays.asList(source);
// 堆排序
System.out.println("堆排序前: " + heapArr);
List<Integer> heapRes = HeapSort(heapArr);
System.out.println("堆排序结果为: " + heapRes);
}
}
7.4 算法分析
时间复杂度: 平均 O ( n l o g n ) O(nlogn) O(nlogn), 最坏 O ( n l o g n ) O(nlogn) O(nlogn), 最好 O ( n l o g n ) O(nlogn) O(nlogn), 空间复杂度 O ( 1 ) O(1) O(1), 是不稳定的。 。 堆排序可以用来寻找topK的元素,时间复杂度为 O ( k l o g n ) O(klogn) O(klogn)。 数列需要从小到大排序, 数列特征是基本逆序 (多数数字从大到小,个别乱序)也是优先选用堆排序。
8. 计数排序
计数排序不是基于比较的排序算法,其核心在于将输入的数据值转化为键存储在额外开辟的数组空间中。 作为一种线性时间复杂度的排序,计数排序要求输入的数据必须是有确定范围的整数。
8.1 算法描述
- 找出待排序的数组中最大和最小的元素;
- 统计数组中每个值为i的元素出现的次数,存入数组C的第i项;
- 对所有的计数累加(从C中的第一个元素开始,每一项和前一项相加);
- 反向填充目标数组:将每个元素i放在新数组的第C(i)项,每放一个元素就将C(i)减去1。
8.2 动图展示
8.3 代码实现(java版)
/**
* 计数排序
* 计数排序不是基于比较的排序算法,其核心在于将输入的数据值转化为键存储在额外开辟的数组空间中。作为一种线性时间复杂度的排序,计数排序要求输入的数据必须是有确定范围的整数
* ######算法描述######
* 找到待排序的数组中最大和最小的元素
* 统计数组中每个值为i的元素出现的次数,存入数组C的第i项
* 对所有的计数累加(从C中的第一个元素开始,每一项和前一项相加)
* 反向填充目标数组:将每个元素i放在新数组的第C(i)项,每放一个元素就将C(i)减去1
*
* 动态图:https://mmbiz.qpic.cn/mmbiz_gif/951TjTgiabkzow2ORRzgpfHIGAKIAWlXmRdDMUVcrboBpKl9lfLic2TE8zHNcHfCktRhQ9EfVbFoEAet0iaQOge1w/640?wx_fmt=gif&tp=webp&wxfrom=5&wx_lazy=1
* 时间复杂度: 平均 O(n + k), 最坏 O(n+k), 最好O(n + k) 空间复杂度: O(n + k) 是稳定的
* 算法分析: 计数排序是一个稳定的排序算法。当输入的元素是n个0到k之间的整数时,时间复杂度是O(n+k),空间复杂度也是O(n+k),其牌序速度快于任何比较排序算法。当k不是很大并且序列比较集中时,计数排序是一个很有效的排序算法。
* */
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
public class CountingSort {
public static List<Integer> CountingSort(List<Integer> countingArr) {
int max = Collections.max(countingArr);
int min = Collections.min(countingArr);
int[] countArr = new int[max - min + 1]; // 用于统计的数组
Arrays.fill(countArr, 0);
for (int i = 0; i < countingArr.size(); i ++) {
countArr[countingArr.get(i) - min] ++ ; // 进行统计
}
List<Integer> res = new ArrayList<>();
for (int i = 0; i < countArr.length; i ++) {
if(countArr[i] == 0)
continue;
for(int j = 0; j < countArr[i]; j ++)
res.add(i + min);
}
return res;
}
public static void main(String[] args) {
// List<Integer> arr = new ArrayList<>();
Integer[] source = {3, 44, 38, 5, 47, 15, 36, 26, 27, 2, 46, 4, 19, 50, 48};//{1, 3, 5, 7, 8, 2, 4, 6, 9, 7};
List<Integer> arr = Arrays.asList(source); // 将数据替换成List的方式
// 计数排序
System.out.println("计数排序前: " + arr);
List<Integer> countingRes = CountingSort(arr);
System.out.println("计数排序结果为: " + countingRes);
}
}
8.4 算法分析
时间复杂度: 平均 O ( n + k ) O(n + k) O(n+k), 最坏 O ( n + k ) O(n + k) O(n+k), 最好 O ( n + k ) O(n + k) O(n+k), 空间复杂度 O ( n + k ) O(n + k) O(n+k), 是稳定的。 。计数排序是一个稳定的排序算法。当输入的元素是 n 个 0到 k 之间的整数时,时间复杂度是O(n+k),空间复杂度也是O(n+k),其排序速度快于任何比较排序算法。当k不是很大并且序列比较集中时,计数排序是一个很有效的排序算法。
9. 桶排序
桶排序是计数排序的升级版。它利用了函数的映射关系,高效与否的关键就在于这个映射函数的确定。桶排序 (Bucket sort)的工作的原理:假设输入数据服从均匀分布,将数据分到有限数量的桶里,每个桶再分别排序(有可能再使用别的排序算法或是以递归方式继续使用桶排序进行排)。
9.1 算法描述
- 设置一个定量的数组当作空桶;
- 遍历输入数据,并且把数据一个一个放到对应的桶里去;
- 对每个不是空的桶进行排序;
- 从不是空的桶里把排好序的数据拼接起来。
9.2 图片展示
9.3 代码实现(java版)
/**
* 桶排序
* 桶排序是计数排序的升级版。它利用了汉书的映射关系,高效与否的关键就在于这个映射函数的确定。桶排序的工作原理:假设输入数据服从均匀分布,将数据分到有限数量的桶里,每个桶再分别排序(有可能再使用别的排序算法或者以递归方式继续使用桶排序进行排序)
* ######算法描述######
* 设置一个定量的数组当作空桶
* 遍历输入数据,并且把数据一个一个方的放到对应的桶里去
* 对每个不是空的桶进行排序
* 从不是空的桶里把排好序的数据拼接起来
*
* 图片演示: https://mmbiz.qpic.cn/mmbiz_jpg/951TjTgiabkzow2ORRzgpfHIGAKIAWlXmk7Gjoat6hJ4VYSNvz1A9mudGMVE7FkKMAJsZUKutDx1L1HYibPlOoSg/640?wx_fmt=jpeg&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1
* 时间复杂度: 平均 O(n + k), 最坏 O(n+k), 最好O(n) 空间复杂度: O(n + k) 是稳定的
* 算法分析: 桶排序最好情况下使用限行时间O(n),桶排序的时间复杂度,取决于对各个桶之间数据进行排序的时间复杂度,因为其它部分的时间复杂度都为O(n)。很显然,桶划分的越小,各个桶之间的数据越少,排序所用的时间也会越少。但相应的空间消耗就会增大。
* */
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
public class BucketSort {
public static List<Integer> BucketSort(List<Integer> bucketArr) {
if(bucketArr.size() == 0)
return bucketArr;
// 获得最小值
int minValue = Collections.min(bucketArr);
if(minValue < 0) // 这里桶排序考虑的均为非负整数
return bucketArr;
// 定义桶的数量
int bucketCount = 10;
List<List> buckets = new ArrayList<>(); // 定义桶
for(int i = 0; i < bucketCount; i ++) {
buckets.add(new ArrayList());
}
// 将数据放入到桶中
for(int num : bucketArr) {
buckets.get(num / bucketCount).add(num);
}
// 每个桶中使用插入排序
for(int i = 0; i < bucketCount; i++){
InsertionSort.InsertionSort(buckets.get(i));
}
List<Integer> res = new ArrayList<>();
// 保存结果
for(int i = 0; i < bucketCount; i++) {
res.addAll(buckets.get(i));
}
return res;
}
public static void main(String[] args) {
// List<Integer> arr = new ArrayList<>();
Integer[] source = {3, 44, 38, 5, 47, 15, 36, 26, 27, 2, 46, 4, 19, 50, 48};//{1, 3, 5, 7, 8, 2, 4, 6, 9, 7};
List<Integer> arr = Arrays.asList(source); // 将数据替换成List的方式
// 桶排序
System.out.println("桶排序前: " + arr);
List<Integer> bucketRes = BucketSort(arr);
System.out.println("桶排序结果为:" + bucketRes);
}
}
9.4 算法分析
时间复杂度: 平均 O ( n + k ) O(n + k) O(n+k), 最坏 O ( n 2 ) O(n ^2) O(n2), 最好 O ( n ) O(n) O(n), 空间复杂度 O ( n + k ) O(n + k) O(n+k), 是稳定的。 。桶排序最好情况下使用线性时间O(n),桶排序的时间复杂度,取决与对各个桶之间数据进行排序的时间复杂度,因为其它部分的时间复杂度都为O(n)。很显然,桶划分的越小,各个桶之间的数据越少,排序所用的时间也会越少。但相应的空间消耗就会增大
10. 基数排序
基数排序是按照低位先排序,然后收集;再按照高位排序,然后再收集;依次类推,直到最高位。有时候有些属性是有优先级顺序的,先按低优先级排序,再按高优先级排序。最后的次序就是高优先级高的在前,高优先级相同的低优先级高的在前。
10.1 算法描述
- 取得数组中的最大数,并取得位数;
- arr为原始数组,从最低位开始取每个位组成radix数组;
- 对radix进行计数排序(利用计数排序适用于小范围数的特点);
10.2 动图展示
10.3 代码实现(java版)
/**
* 基数排序
* 基数排序是按照低位先排序,然后收集;再按照高位排序,然后再收集;以此类推,直到最高位。有时候有些属性是有优先级顺序的,先按低优先级排序,再按高优先级排序。最后的次序就是高优先级高的在前,高优先级相同的低优先级高的在前
* ######算法描述######
* 取得数组中的最大数,并取得位数
* arr为原始数组,从最低位开始取每个位组成radix数组
* 对radix进行计数排序(利用计数排序适用于小范围的特点)
*
* 动态图: https://mmbiz.qpic.cn/mmbiz_gif/951TjTgiabkzow2ORRzgpfHIGAKIAWlXm8de0kHuzHxicmibEQaHYX65g9cwicr5aGeWWoib3uSg0XGO3QTp2vsuCxw/640?wx_fmt=gif&tp=webp&wxfrom=5&wx_lazy=1
* 时间复杂度: 平均 O(n * k), 最坏 O(n*k), 最好O(n*k) 空间复杂度: O(n + k) 是稳定的
* 算法分析: 基数排序基于分别排序,分别收集,所以是稳定的。但基数排序的性能比桶排序要略差,每一次关键字的桶分配都需要O(n)的时间复杂度,而且分配之后得到新的关键字序列又需要O(n)的时间复杂度。假如待排数据可以分为d个关键字,则基数排序的时间复杂度将是O(d*2n) ,当然d要远远小于n,因此基本上还是线性级别的。
* */
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
public class RadixSort {
public static List<Integer> RadixSort(List<Integer> radixArr) {
List<Integer> arrCopy = radixArr;
int maxValue = Collections.max(arrCopy);
for(int mod = 1; maxValue/mod > 0; mod *= 10) { // 对基数进行桶排序
arrCopy = count_sort(arrCopy, mod);
}
return arrCopy;
}
/**
* 对数组按照“某个位数”进行排序(桶排序)
*
*/
public static List<Integer> count_sort(List<Integer> radixArr, int exp) {
Integer[] output = new Integer[radixArr.size()]; // 存储"被排序数据"的临时数组
int buckCount = 10;
int[] buckets = new int[buckCount];
Arrays.fill(buckets, 0);
// 将数据出现的次数存储在buckets[]中
for (int i = 0; i < radixArr.size(); i++)
buckets[ (radixArr.get(i)/exp)%10 ]++;
// 更改buckets[i]。目的是让更改后的buckets[i]的值,是该数据在output[]中的位置。
for (int i = 1; i < buckCount; i++)
buckets[i] += buckets[i - 1];
// 将数据存储到临时数组output[]中
for (int i = radixArr.size() - 1; i >= 0; i--)
{
output[buckets[ (radixArr.get(i)/exp)%10 ] - 1] = radixArr.get(i);
buckets[ (radixArr.get(i)/exp)%10 ]--;
}
// 将排序好的数据赋值给a[]
radixArr = Arrays.asList(output);
return radixArr;
}
public static void main(String[] args) {
// List<Integer> arr = new ArrayList<>();
Integer[] source = {3, 44, 38, 5, 47, 15, 36, 26, 27, 2, 46, 4, 19, 50, 48};//{1, 3, 5, 7, 8, 2, 4, 6, 9, 7};
List<Integer> radixArr = Arrays.asList(source); // 将数据替换成List的方式
// 基数排序
System.out.println("基数排序前: " + radixArr);
List<Integer> radixRes = RadixSort(radixArr);
System.out.println("基数排序后: " + radixRes);
System.out.println("基数排序前: " + radixArr);
}
}
10.4 算法分析
时间复杂度: 平均 O ( n ∗ k ) O(n*k) O(n∗k), 最坏 O ( n ∗ k ) O(n *k) O(n∗k), 最好 O ( n ∗ k ) O(n*k) O(n∗k), 空间复杂度 O ( n + k ) O(n + k) O(n+k), 是稳定的。 基数排序基于分别排序,分别收集,所以是稳定的。但基数排序的性能比桶排序要略差,每一次关键字的桶分配都需要O(n)的时间复杂度,而且分配之后得到新的关键字序列又需要 O ( n ) O(n) O(n)的时间复杂度。假如待排数据可以分为d个关键字,则基数排序的时间复杂度将是 O ( d ∗ 2 n ) O(d*2n) O(d∗2n) ,当然d要远远小于n,因此基本上还是线性级别的。
基数排序的空间复杂度为 O ( n + k ) O(n+k) O(n+k),其中k为桶的数量。一般来说n>>k,因此额外空间需要大概n个左右。
本文主要参考以及排序动态图来源于:十大经典排序算法(动图演示,收藏好文),在此表示感谢