目录
------------------------------------
------------------------------------
------------------------------------
------------------------------------
------------------------------------
------------------------------------
------------------------------------
学习预热:基础知识
一、什么是排序
所谓排序,就是整理表中的数据元素,使之按元素的关键字递增/递减的顺序排列。
二、为什么要排序
查找是计算机应用中必不可少并且使用频率很高的一个操作。
在一个排序表中查找一个元素,要比在一个无序表中查找效率高得多。
所以为了提高查找效率,节省CPU时间,需要排序。
三、排序的稳定性
稳定性:稳定性指的是相同两个数,排序后他们的相对顺序不变。
什么时候需要稳定的排序方法?什么时候不需要呢?
待更新. . .
四、排序稳定性的意义
待更新. . .
五、排序分类方式
方式一:内外分类
我们根据待排序的数据元素是否全部在内存中,我们把排序方法,分为两类:
内排序:整个排序元素都在内存中处理,不涉及内、外存的数据交换。
外排序:待排序元素有一部分不在内存(如:内存装不下)
方式二:比较分类
- 比较类排序:通过比较来决定元素间的相对次序,由于其时间复杂度不能突破O(nlogn),因此也称为非线性时间比较类排序。
- 非比较类排序:不通过比较来决定元素间的相对次序,它可以突破基于比较排序的时间下界,以线性时间运行,因此也称为线性时间非比较类排序。
待画结构图:非比较排序和比较排序!
六、排序算法性能评估
1. 算法的时间复杂度
评估一下算法 运行时间
待更新. . .
2. 算法的空间复杂度
评估一下算法 所用空间
七、知识小结
1. 10种经典排序算法表格图
待更新:各排序算法的描述
2. 排序算法选择
2.1. 稳定性比较
(1)稳定:冒泡排序、插入排序、二分插入排序、归并排序和基数排序。
(2)不稳定:选择排序、快速排序、希尔排序、堆排序。
2.2. 平均时间复杂度
(1)O(n^2):直接插入排序,简单选择排序,冒泡排序、二分插入排序。
(2)O(nlogn):快速排序,归并排序,希尔排序,堆排序。快排是最好的, 其次是归并和希尔,
堆排序在数据量很大时效果明显。
(3)在数据规模较小时(9W内):直接插入排序,简单选择排序差不多。当数据较大时:冒泡排
序算法的时间代价最高。
性能为O(n^2)的算法基本上是相邻元素进行比较,基本上都是稳定的。
2.3. 排序算法的选择
1> 数据规模较小(9W内)
(1)直接插入排序、冒泡排序:待排序列基本有序的情况下,对稳定性有要求;
(2)直接选择排序:待排序列无序,对稳定性不作要求;
2> 数据规模很大
(1)归并排序:序列本身基本有序,对稳定性有要求,空间允许下。
(2)快速排序:序列本身无序。完全可以用内存空间,对稳定性没有要求,此时要付出log(n)的额外空
间。
(3)堆排序:对稳定性没有要求,所需的辅助空间少于快速排序,
3> 基数排序 (稳定)
(1)在某个数字可能很大的时候,基数排序没有任何性能上的优势,还会浪费非常多的内存。
(2)一组数,这组数的最大值不是很大,更加准确的说,是要排序的对象的数目 和排序对象的最大值之
间相差不多。比如,这组数 1 4 5 2 2,要排序对象的数目是 5 ,排序对象的最大值也是 5. 这样的情况
很适合。
4> 希尔排序 (不稳定)
(1)对于中等大小的数组它的运行时间是可以接受的。 它的代码量很小,且不需要使用额外的内
存空间。虽然有更加高效的算法,但除了对于很大的 N,它们可能只会比希尔排序快两倍(可能还
达不到),而且更复杂。如果你需要解决一个排序问题而又没有系统排序函数可用,可以先用希尔
排序,然后再考虑是否值得将它替换为更加复杂的排序算法。
(2)希尔排序是对直接插入排序的一种优化,可以用于大型的数组,希尔排序比插入排序和选择
排序要快的多,并且数组越大,优势越大。
------------------------------------
排序算法一:冒泡排序
一、简介
排序(Bubble Sort)是一种简单的排序算法,它的基本思想是通过不断交换相邻两个元素的位
置,使得较大的元素逐渐往后移动,直到最后一个元素为止。冒泡排序的时间复杂度为 O(n^2),
空间复杂度为 O
(1),是一种稳定的排序算法。
其实现过程可以概括为以下几个步骤:
- 从序列的第一个元素开始,对相邻的两个元素进行比较,如果它们的顺序错误就交换它们的位置,即将较大的元素往后移动,直到遍历到序列的最后一个元素。
- 对剩下的元素重复上述步骤,直到整个序列都已经有序。
二、示例代码
public class BubbleSort {
public static void bubbleSort(int[] arr) {
int n = arr.length;
for (int i = 0; i < n; i++) {
// 每轮遍历将最大的数移到末尾
for (int j = 0; j < n - i - 1; j++) {
if (arr[j] > arr[j+1]) {
int temp = arr[j];
arr[j] = arr[j+1];
arr[j+1] = temp;
}
}
}
}
public static void main(String[] args) {
int[] arr = {64, 34, 25, 12, 22, 11, 90};
bubbleSort(arr);
System.out.println(Arrays.toString(arr)); // [11, 12, 22, 25, 34, 64, 90]
}
}
三、变种
冒泡排序有一些变种,其中比较常见的有以下几种:
- 鸡尾酒排序(Cocktail Sort):又称为双向冒泡排序,它是一种改进的冒泡排序算法。
与普通冒泡排序不同的是,它是从左到右遍历序列,然后从右到左遍历序列,交替进行,直到序列有序为止。
这样可以在一定程度上减少排序的时间。 - 短冒泡排序(Short Bubble Sort):在冒泡排序的基础上进行改进,当某一轮遍历中没有发生元素交换时,说明序列已经有序,可以提前结束排序。这样可以在序列已经有序的情况下减少不必要的比较次数。
- 奇偶排序(Odd-Even Sort):也称为交替排序,它是一种并行排序算法,可以同时比较和交换序列中的奇数和偶数位置上的元素,直到序列有序为止。这样可以在一定程度上减少排序的时间,但是它只适用于能够并行处理的情况。
这些变种算法都是基于冒泡排序的基本思想,并对其进行了不同的优化和改进,使得排序效率更
高。
1. 鸡尾酒排序
1.1. 简介
鸡尾酒排序是一种定向的冒泡排序(又叫快乐小时排序),排序是 从低到高 再 从高到低 的反复。
而冒泡排序是从低到高的排序。
先来看看冒泡排序
举个栗子:8个数组成一个无序数列:3、2、4、5、6、7、1、8,希望从小到大排序
第一轮结果( 3 和 2 交换,1 和 8 交换)
2、3、4、5、6、7、1、8
第二轮结果( 7 和 1 交换)
、
第三轮结果( 6 和 1 交换)
接下来(5和1交换,4和1交换,3和1交换,2和1交换)
最后结果为
总共进行了7次交换
下面用鸡尾酒排序该无序数列
第一轮( 3 和 2 交换,8 和 1 交换)
第二轮
此时开始不一样了,我们要从右到左(即高到低)进行交换、比较
即在这里8已经在有序区域了,不考虑。让1和7比较,1小于7,7和1交换
2、3、4、5、6、7、1、8
然后 6 和 1 交换,
2、3、4、5、1、6、7、8
5 和 1 交换,4 和 1 交换,3 和 1 交换, 2 和 1 交换
最终结果:
1、2、3、4、5、6、7、8
第三轮(结果已经有序了,但流程并没有结束)
第三轮需要重新从左到右(从低到高)比较和交换
1和2比较,位置不变;2和3比较,位置不变, ...... ,6和7比较,位置不变
没有元素位置交换,证明已经有序,排序结束
对于双向鸡尾酒排序,我们可以在每一轮排序的最后,记录下最后一次元素交换的位置( rightChange 和
leftChange ),那个位置就是无序数列的边界,再往后就是有序区了。
1.2. 代码
下面给出双向的鸡尾酒排序的java代码
public class CocktailSort {
public static void main(String[] args) {
int[] arr = {11, 95, 45, 15, 51, 12, 24};
sort(arr);
}
public static void sort(int[] arr) {
boolean sorted1 = true;
boolean sorted2 = true;
int len = arr.length;
for (int j = 0; j < len / 2; j++) { //趟数
sorted1 = true; //假定有序
sorted2 = true;
for (int i = 0; i < len - 1 - j; i++) { //次数
if (arr[i] > arr[i + 1]) {
int temp = arr[i];
arr[i] = arr[i + 1];
arr[i + 1] = temp;
sorted1 = false; //假定失败
}
}
for (int i = len - 1 - j; i > j; i--) { //次数
if (arr[i] < arr[i - 1]) {
int temp = arr[i];
arr[i] = arr[i + 1];
arr[i + 1] = temp;
sorted2 = false; //假定失败
}
}
System.out.println(Arrays.toString(arr));
if (sorted1 && sorted2) { //减少趟数,已有序则结束
break;
}
}
}
}
1.3. 复杂度
鸡尾酒排序最糟或是平均所花费的次数都是O(n²),但如果序列在一开始已经大部分排序过的话,
会接近O(n)。
2. 短冒泡排序
2.1. 简介
冒泡排序是一种基础的排序算法,短冒泡排序是冒泡排序的一种改进,主要是为了解决冒泡排序在
数组初始状态为逆序时效率低的问题。
在冒泡排序中,如果序列已经有序,则无需进行交换,但是在传统的冒泡排序中,会进行n-1次全
排列,这就造成了不必要的计算。
短冒泡排序的思路是在每次遍历的过程中,记录下本次遍历的最大位置i,
在下一次遍历时,只需要遍历到i位置,这就减少了全排列的次数。
2.2. 代码
public class ShortBubbleSort {
public static void sort(int[] array) {
if (array == null || array.length <= 1) {
return;
}
int len = array.length;
for (int i = 0; i < len; i++) {
// 记录本次遍历的最大位置
int maxPos = i;
for (int j = i + 1; j < len; j++) {
if (array[j] > array[maxPos]) {
maxPos = j;
}
}
// 如果最大值不在最后的位置,交换最大值和当前位置的值
if (maxPos != i) {
int temp = array[i];
array[i] = array[maxPos];
array[maxPos] = temp;
}
}
}
}
这段代码首先判断了数组是否为空或者只有一个元素,如果是的话,那么就没有排序的必要。
然后开始进行短冒泡排序,外层循环遍历数组,内层循环找出当前未排序部分的最大值,并记录其
位置。
如果最大值不在当前位置,那么就交换这两个位置的值。
这样,每次外层循环结束后,最大的元素就会被放到正确的位置。重复这个过程,直到整个数组都
被排序。
3. 奇偶排序
3.1. 简介
奇偶排序(Odd-Even Sort):也称为交替排序,它是一种并行排序算法,可以同时比较和交换序
列中的奇数和偶数位置上的元素,直到序列有序为止。这样可以在一定程度上减少排序的时间,但
是它只适用于能够并行处理的情况。
3.2. 题目
3.3. 代码
3.3.1. 方法一:两次遍历
代码:
class Solution {
public int[] sortArrayByParity(int[] nums) {
int n = nums.length, index = 0;
int[] res = new int[n];
for (int num : nums) {
if (num % 2 == 0) {
res[index++] = num;
}
}
for (int num : nums) {
if (num % 2 == 1) {
res[index++] = num;
}
}
return res;
}
}
3.3.2. 方法二:双指针 + 一次遍历
代码:
class Solution {
public int[] sortArrayByParity(int[] nums) {
int n = nums.length;
int[] res = new int[n];
int left = 0, right = n - 1;
for (int num : nums) {
if (num % 2 == 0) {
res[left++] = num;
} else {
res[right--] = num;
}
}
return res;
}
}
3.3.3. 方法三:原地交换
代码:
class Solution {
public int[] sortArrayByParity(int[] nums) {
int left = 0, right = nums.length - 1;
while (left < right) {
while (left < right && nums[left] % 2 == 0) {
left++;
}
while (left < right && nums[right] % 2 == 1) {
right--;
}
if (left < right) {
int temp = nums[left];
nums[left] = nums[right];
nums[right] = temp;
left++;
right--;
}
}
return nums;
}
}
四、应用场景
冒泡排序虽然时间复杂度较高,但是它的实现简单,容易理解,并且在某些特定场景下仍然有着广
泛的应用。
以下是一些冒泡排序的应用场景:
- 数据量较小的排序:当待排序的数据量较小时,冒泡排序的效率并不比其他排序算法低,甚至在某些情况下可能更优。
- 数据基本有序的排序:当待排序的数据基本有序时,冒泡排序的效率比其他排序算法更高。
因为冒泡排序可以在一轮遍历中将已经有序的元素排除在外,从而减少比较和交换的次数。 - 学习排序算法:冒泡排序是最基本的排序算法之一,它的实现简单,容易理解,是学习排序算法的入门算法。
需要注意的是,如果待排序的数据量较大,或者数据分布比较随机,冒泡排序的效率会比较低,不如其他排序
法。因此,在实际应用中,需要根据具体的情况选择适合的排序算法。
五、框架应用
1. 冒泡排序在spring 中的应用
在 Spring 框架中,冒泡排序算法并没有直接应用到核心模块中,但是它可以作为一种排序算法被使用在 Spring
的某些模块中,例如:
- Spring Security 模块中的权限排序:Spring Security 是一个基于 Spring 的安全框架,它提供了一套完整的安全解决方案,包括认证、授权、攻击防护等功能。在 Spring Security 中,权限可以通过冒泡排序算法来进行排序,以便于在授权时按照顺序进行匹配。
- Spring Batch 模块中的数据排序:Spring Batch 是一个基于 Spring 的批处理框架,它可以帮助用户快速构建和执行大规模、复杂的批处理作业。在 Spring Batch 中,数据排序是一个常见的操作,可以使用冒泡排序算法来实现。
需要注意的是,冒泡排序算法虽然简单,但是在实际应用中效率较低,因此在处理大规模数据时不
建议使用。
在 Spring 框架中,如果需要进行排序操作,建议使用更高效的排序算法,例如快速排序、归并排
序等。
六、算法分析
冒泡排序的时间复杂度为 O(n^2) ,空间复杂度为 O (1),是一种稳定的排序算法。
- 时间复杂度:冒泡排序的时间复杂度是 O(n^2),其中 n 是待排序序列的长度。
冒泡排序的比较次数和交换次数都是 n(n-1)/2,因此时间复杂度为 O(n^2)。 - 空间复杂度:冒泡排序的空间复杂度是 O(1),即只需要使用常数级别的额外空间来存储临时变量。
- 算法稳定性:冒泡排序是一种稳定的排序算法,即相等的元素在排序前后的相对位置不会发生改变。
------------------------------------
排序算法二:选择排序
一、基本介绍
从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,然后再从剩余的未排序元素中
寻找到最小(大)元素,继续放在起始位置,直到未排序元素个数为0。
核心思想
- 首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置。
- 再从剩余未排序元素中继续寻找最小(大)元素,然后放到未排序序列的起始位置。
- 重复第二步,直到所有元素均排序完毕。
二、示例代码
方式一:原始版
public static void sort(int[] num){
if(num.length < 2 || num == null) return;
int index = 0;//标记最小值处
int count = 0;//循环次数
int count1 = 0;//交换次数
for (int i = 0; i < num.length-1; i++) {
index = i;
for (int j = i; j < num.length ; j++) {
count++;
if(num[j] < num[index]) index = j;//发现更小值,获取下标
}
if(i != index){//交换
count1++;
int c = num[i];
num[i] = num[index];
num[index] = c;
System.out.print("交换了"+num[i]+"和"+num[index]+"结果:");
}
for (int m = 0; m < num.length; m++) {
System.out.print(num[m]+",");
}
System.out.println();
}
System.out.println("循环次数"+count+"交换次数"+count1);
}
方式二:递归版
public static void sort1(int[] num,int m,int length,int count){
if( length < 2 ) return;
int index = m;//标记最小值
for (int i = m; i < length+m; i++) {
count++;
if( num[i] < num[index] ) index = i;//发现更小值,获取下标
}
if( index != 0 ){ //交换
int c = num[m];
num[m] = num[index];
num[index] = c;
System.out.print("交换了" + num[m] + "和" + num[index] + "结果:");
}
for (int n = 0; n < num.length; n++) {
System.out.print(num[n]+",");
}
System.out.println("递归次数:" + m + "循环次数:" + count);
sort1(num,m+1,--length,count);
}
方式三:优化版
public static void sort2(int[]num){
if( num.length < 2 || num == null )return;
int left,right;//标记左右
int min,max;//标记最大和最小值
left = 0;
right = num.length - 1;
int m = 0;
while ( left < right ){
min = left;
max = right;
for (int i = left; i < right+1 ; i++) {
m++;
if(num[i] <= num[min]) min = i;//扫描获得最小值下标
if(num[i] >= num[max]) max = i;//扫描获得最大值下标
}
if( min != left ){
int c = num[left];
num[left] = num[min];
num[min] = c;
System.out.print("交换了"+num[left]+"和"+num[min]+"结果:");
}
if( max == left )min=max;
if( max != right ){
int c = num[right];
num[right] = num[max];
num[max] = c;
System.out.print("交换了"+num[right]+"和"+num[max]+"结果:");
}
left++;
right--;
for (int n = 0; n < num.length; n++) {
System.out.print(num[n]+",");
}
System.out.println("循环次数"+m);
}
}
方式四:优化递归版
public static void sort3(int[] num,int m,int length){
if( length < 2 )return;
int index = m;//左标
int index1 = num.length - m - 1;//右标
for (int i = m; i < length; i++) {
if( num[i] <= num[index] ) index=i;//扫描获得最小值下标
}
if( index != m ){
int c = num[m];
num[m] = num[index];
num[index] = c;
System.out.print("交换了"+num[m]+"和"+num[index]+"结果:");
}
for (int i = m; i < length; i++) {
if(num[i] >= num[index1]) index1 = i;//扫描获得最大值下标
}
if( index1 != ( num.length - m - 1 ) ){
int c = num[num.length-m-1];
num[num.length-m-1] = num[index1];
num[index1] = c;
System.out.print("交换了"+num[num.length - m - 1]+"和"+num[index1]+"结果:");
}
for (int n = 0; n < num.length; n++) {
System.out.print(num[n]+",");
}
System.out.println("递归次数"+m);
if( m < ( num.length - m - 1) ) sort3(num,m+1,--length);
}
三、变种
四、应用场景
选择排序虽然时间复杂度较高,但是它的实现简单,容易理解,并且在某些特定场景下仍然有着广泛的应用。
以下是一些适合使用选择排序的场景:
- 数据量较小:当待排序序列的数据量较小时,选择排序的效率还是比较高的。在这种情况下,选择排序比其他高级排序算法(如快速排序、归并排序等)更容易实现和理解。
- 内存限制:选择排序是一种原地排序算法,即不需要额外的内存空间来存储临时变量。因此,当内存空间有限时,选择排序是一种比较合适的排序算法。
- 部分有序:当待排序序列已经有一部分有序时,选择排序的效率会比其他排序算法高。这是因为选择排序每次只选择最小的元素进行交换,因此不会破坏已经有序的部分。
需要注意的是,选择排序的时间复杂度较高,因此在处理大规模数据时,应该使用其他更高效的排序算法。
五、框架应用
1. 选择排序在spring 中的应用
在 Spring 中,选择排序并不是一个常用的算法,因此它并没有被直接应用在 Spring 框架中。
然而,选择排序的思想可以启发我们在 Spring 中的一些实践,例如:
- Bean 的排序:在 Spring 中,我们可以通过实现 org.springframework.core.Ordered接口或者使用 @Order 注解来控制 Bean的加载顺序。这种方式类似于选择排序中的选择最小元素,即通过指定 Bean 的优先级来控制其加载顺序。
- AOP 切面的优先级:在 Spring AOP 中,我们可以通过 org.springframework.core.annotation.Order 注解来控制切面的优先级。这种方式也类似于选择排序中的选择最小元素,即通过指定切面的优先级来控制其执行顺序。
- Spring Security 中的 Filter 链:在 Spring Security 中,Filter 链是一种类似于责任链模式的机制,它由多个 Filter 组成,每个 Filter负责不同的安全检查。这种方式也类似于选择排序中的选择最小元素,即通过指定 Filter 的执行顺序来控制安全检查的顺序。
虽然选择排序并不是 Spring 中的常用算法,但是它的思想可以启发我们在 Spring 中的一些实践,从而提高代码
的可读性和可维护性。
六、算法分析
- 时间复杂度
最好的情况全部元素已经有序,则 交换次数为0;最差的情况,全部元素逆序,就要交换 n-1 次;
所以最优的时间复杂度和最差的时间复杂度和平均时间复杂度 都为 :O(n^2) - 空间复杂度
最优的情况下(已经有顺序)复杂度为:O(0) ;
最差的情况下(全部元素都要重新排序)复杂度为:O(n );
那么选择排序算法平均的时间复杂度为:O(1) - 算法稳定性
选择排序是给每个位置选择当前元素最小的,比如给第一个位置选择最小的,在剩余元素里面给第二个元素选择第二小的,依次类推,直到第n-1个元素,第n个元素不用选择了,因为只剩下它一个最大的元素了。那么,在一趟选择,如果一个元素比当前元素小,而该小的元素又出现在一个和当前元素相等的元素后面,那么交换后稳定性就被破坏了。所以选择排序是不稳定的。
------------------------------------
排序算法三:插入排序
一、基本介绍
插入排序是一种简单直观的排序算法,它的工作原理是通过构建有序序列,对于未排序数据,在已
排序序列中从后向前扫描,找到相应位置并插入。
具体步骤如下:
- 从第一个元素开始,该元素可以认为已经被排序。
- 取出下一个元素,在已经排序的元素序列中从后向前扫描。
- 如果该元素(已排序)大于新元素,将该元素移到下一位置。
- 重复步骤3,直到找到已排序的元素小于或者等于新元素的位置。
- 将新元素插入到该位置后。
- 重复步骤2~5,直至所有元素都已排序完毕。1
在实际应用中,插入排序通常适用于较小规模的数据排序。
虽然其时间复杂度在最坏情况下为O(n^2),但在部分已排序的序列中,其效率可能会高于其他排序
算法,如冒泡排序等。
此外,插入排序还具有稳定性,即相等的元素在排序后保持原有的顺序不变。
二、示例代码
方法一:交换法
/**
* 希尔排序
*/
@Test
public void testShellSort() {
int[] arr = new int[]{1, 4, 6, 3, 8, 9, 2, 23};
exchangeShellSort(arr);
// insertShellSort(arr);
}
/**
* 希尔排序-交换法
* @param arr
*/
public void exchangeShellSort(int arr[]) {
int temp;// 临时数据
boolean flag = false;// 是否交换
int count = 1;// 计数
// 分而治之,将数值分组排序,i为步长
for (int i = arr.length / 2; i > 0; i /= 2) {
// 遍历分治的每一个分组
for (int j = i; j < arr.length; j++) {
// 遍历分治的每一个分组的每一个值
for (int k = j - i; k >= 0; k -= i) {
if (arr[k + i] < arr[k]) {
temp = arr[k + i];
arr[k + i] = arr[k];
arr[k] = temp;
flag = true;
}
if (!flag) {
break;
} else {
// 为了下次判断
flag = false;
}
}
}
System.out.println("希尔排序交换法第" + (count++) + "次排序后" + Arrays.toString(arr));
}
}
方法二:插入法
/**
* 希尔排序
*/
@Test
public void testShellSort() {
int[] arr = new int[]{1, 4, 6, 3, 8, 9, 2, 23};
// exchangeShellSort(arr);
insertShellSort(arr);
}
/**
* 希尔排序-插入法
* @param arr
*/
public void insertShellSort(int[] arr) {
int count = 1;//计数
// 分而治之,循环为每次总数除二
for (int i = arr.length / 2; i > 0; i /= 2) {
// 循环分治的每一个分组
for (int j = i; j < arr.length; j++) {
int index = j;
int temp = arr[index];
// 比较每一组的值
if (arr[index] < arr[index - i]) {
// 如果比前面小就把前面的数值往后移,将合适的数值插入
while (index - i > 0 && temp < arr[index - i]) {
arr[index] = arr[index - i];
index -= i;
}
arr[index] = temp;
}
}
System.out.println("希尔排序插入法第" + (count++) + "次排序后" + Arrays.toString(arr));
}
}
三、变种
1. 直接插入排序
1.1. 简介
直接插入排序(Insertion Sort)是一种基本的排序算法。
其思想是将数组分为已有部分和未排序部分,然后逐个比较并移动元素来达到排序目标。
一个简单的例子:斗地主揭牌
我们从牌堆揭了一张A,现在要放到手牌中,一般来说都会插入到 2 与 J 之间
而直接插入排序就是这种思想
红线左边认为是有序的,红线右边是无序的
每一次从红线右边取一个值放到左边进行排序
第一次可以省略掉,直接从第二个数据开始处理
例如,在斗地主揭牌,第一张牌我们不需要看,揭第二张牌,我才需要看一下这张牌是多少,是向左放还是向右
放
第二次,取出22这个数据,与已经排好的数据比较,22大于11,所以不需要挪动,直接插入即可
第三次取出 7 这个数,与已经排好的数据比较,7 比 22 小,向左继续比较,7比11小,向左继续比较,此时触底,所以直接插入7.
小,向左继续比较,此时触底,所以直接插入5.
第五次取出17这个数,与已经排好的数据比较,17 比 22 小,向左继续比较,17比11大,停止比较,插入17.
第六次的数据排好后,红线右边无数据,认为数组已经有序
用一句话描述调整过程:
将待插入的值 和 有序数组从右向左依次比较,找到小于等于自己的值 ,停下来,插入即可。
数据越有序,我们调用直接插入排序去比较挪动的次数越少,所以时间复杂度越低。
1.2. 代码
public class InsertionSort {
// 插入排序
public static void insertionSort(int[] arr) {
// 数组长度
int n = arr.length;
// 第一循环,从第二个元素开始
for (int i = 1; i < n; ++i) {
// 确定关键字
int key = arr[i];
// 从已经排好序的最右边开始向左依次与key进行比较
int j = i - 1;
//
while (j >= 0 && arr[j] > key) {
arr[j + 1] = arr[j];
--j;
}
arr[j + 1] = key;
}
}
public static void main(String[] args) {
int[] array = {5, 2, 8, 3, 9};
System.out.println("原始数组:");
printArray(array);
insertionSort(array);
System.out.println("\n排序结果:");
printArray(array);
}
private static void printArray(int[] arr) {
for (int num : arr) {
System.out.print(num + " ");
}
System.out.println();
}
}
1.3. 算法分析
- 时间复杂度:O(N ^ 2)
- 空间复杂度:O(1)
- 稳定性:稳定
- 最好的情况:序列已经有序,只需要判断是否满足条件,不需要移动,时间复杂度为O(N)。
- 中间的情况:数据需要后移一部分,那时间复杂度就是O(N/2)即O(N);
- 最坏的情况:序列是逆序插入,那么每插入一个数据都要和前面的比较、插入N个数据,此时时间复杂度就是O(N^2);
2. 折半插入排序
2.1. 简介
折半插入排序(binary insertion sort)是对插入排序算法的一种改进,由于排序算法过程中,就是
不断的依次将元素插入前面已排好序的序列中。由于前半部分为已排好序的数列,这样我们不用按
顺序依次寻找插入点,可以采用折半查找的方法来加快寻找插入点的速度。
折半插入排序图文说明
注:蓝色代表已排序序列,白色代表未排序序列,红色箭头指向未排序序列的第一个元素位置。
如图所示,现在有一个待排序序列[8 5 4 2 3],首先默认初始状态下,位置0的数字8作为已排序序
列[8],位置1--位置4的[5 4 2 3 1] 为待排序序列,之后就逐一从[5 4 2 3 1]中取出数字向前进行比
较,插入到已排序序列的合适位置。寻找过程中将蓝色的已排序区域不断进行折半。
初始状态下,已排序区只有一个数据元素8,low位置和high位置都指向了该位置,mid为中间位
置,此时很显然也是0位(0+0)/ 2。
此时temp < mid,将high指向mid的前一位,这里也就是-1,这个时候high=-1,low=1,很显然
high<low,每当这个时候,就到了移动元素的时候了,将(high+1)到(i-1)的元素都向后移一位,再
把(high+1)位置上插入要插入的元素。
2.2. 代码
public class BinaryInsertionSort{
public static void main(String[] args){
// 待排序的数组
int[] array = { 1, 0, 2, 5, 3, 4, 9, 8, 10, 6, 7};
// 折半插入排序开始
binaryInsertSort(array);
// 显示排序后的结果。
System.out.print("排序后: ");
for(int i = 0; i < array.length; i++){
// 打印数组元素
System.out.print(array[i] + " ");
}
}
// Binary Insertion Sort method
private static void binaryInsertSort(int[] array){
// 循环遍历
for(int i = 1; i < array.length; i++){
// 临时保存
int temp = array[i];
// 左边界
int low = 0;
// 右边界
int high = i - 1;
// 循环遍历,直到超出high边界
while(low <= high){
int mid = (low + high) / 2;
if(temp < array[mid]){
high = mid - 1;
}else{
low = mid + 1;
}
}
for(int j = i; j >= low + 1; j--){
array[j] = array[j - 1];
}
array[low] = temp;
}
}
}
2.3. 算法分析
折半查找只是减少了比较次数,但是元素的移动次数不变。
- 时间复杂度:O(N^2)
- 空间复杂度:O(1)
- 稳定性:稳定
3. 希尔排序(Donald.L.Shell)
3.1. 简介
希尔排序(Shell Sort)是一种基于插入排序的高效算法。
其主要思想是将数组分成多个子数组进行插入排序,然后逐渐合并这些子数组直到最终完全有序。
如图示例:
(1)初始增量第一趟 gap = length / 2 = 4
(2)第二趟,增量缩小为 2
(3)第三趟,增量缩小为 1,得到最终排序结果
3.2. 代码
public class ShellSort {
public static void main(String[] args) {
int[] arr = {9, 5, 2, 7, 1}; // 原始数组
System.out.println("初始数组:");
for (int num : arr) {
System.out.print(num + " ");
}
shellSort(arr); // 调用希尔排序函数
System.out.println("\n排序结果:");
for (int num : arr) {
System.out.print(num + " ");
}
}
private static void shellSort(int[] arr) {
int n = arr.length;
for (int gap = n / 2; gap > 0; gap /= 2) {
for (int i = gap; i < n; i++) {
int temp = arr[i];
int j = i - gap;
while (j >= 0 && arr[j] > temp) {
arr[j + gap] = arr[j];
j -= gap;
}
arr[j + gap] = temp;
}
System.out.println();
System.out.print("第" + (gap == n ? "" : "间距") + "次排序结果:");
for (int num : arr) {
System.out.print(num + " ");
}
}
}
}
四、应用场景
适用于:数据量不大,并且对稳定性有要求并且数据局部或者整体有序的情况。
五、框架应用
六、算法分析
- 时间复杂度:最好(排序表本身有序):O(n) / 最坏(排序表逆序)O(n²)
- 空间复杂度:O(1)
- 算法稳定性:稳定
------------------------------------
//堆排序
public static int[] HeapSort(int[] arr){
int len = arr.length;
/**
* 第一步,初始化堆,最大堆,从小到大
* i从完全二叉树的第一个非叶子节点开始,也就是len/2-1开始(数组下标从0开始)
*/
for(int i = len / 2 - 1;i >= 0;i--){
HeapAdjust(arr,i,len);
}
//打印堆顶元素,应该为最大元素9
System.out.println(arr[0]);
return arr;
}
上述代码就是从完全二叉树的第一个非叶子节点开始调换,还顺便打印堆顶元素,此时应为9;
至此,第一个步骤,初始化堆完成,最后的结果应该为下图:
第二个步骤,堆排序
堆排序的过程就是将堆顶元素(最大值或者最小值)与二叉堆的最末尾叶子节点进行调换,不停的调换,直到二
叉堆的顺序变成从小到大
或者从大到小,也就实现了我们的目的。
我们这里以最大堆的堆顶元素(最大元素)为例,最后调换的结果就是从小到大排序的结果。
第一次交换,我们直接将元素9与元素0交换,此时堆顶元素为0,设当前节点index=0;
代码:
/**
* 第二步,交换堆顶(最大)元素和二叉堆的最后一个叶子节点元素。目的是交换元素
* i从二叉堆的最后一个叶子节点元素开始,也就是len-1开始(数组下标从0开始)
*/
for(int i = len - 1;i >= 0;i--){
int temp = arr[i];
arr[i] = arr[0];
arr[0] = temp;
//交换完之后需要重新调整二叉堆,从头开始调整,此时Index=0
HeapAdjust(arr,0,i);
}
注意:这里有个小细节问题,前面我们写的初始化堆方法有三个参数,分别是数组arr,当前节点index以及数组长
度len,如下:
HeapAdjust(int[] arr,int index,int len)
那么,为何不直接传入两个参数即可,数组长度直接用arr.length表示不就行了吗?
初始化堆的时候是可以,但是后面在交换堆顶元素和末尾的叶子节点时,在对剩下的元素进行排序时,
此时的数组长度可不是arr.length了,应该是变化的参数i,因为交换之后的元素
(比如9)就不计入堆中排序了,所以需要3个参数。
我们进行第二次交换,我们直接将元素8与元素2交换,此时堆顶元素为2,设当前节点index=2;
这时,我们需要将剩下的元素(排除元素9和8)进行堆排序,直到下面这个结果:
到这个时候,我们再重复上述步骤,不断调换堆顶和最末尾的节点元素即可,再不断地对剩下的元
素进行排序,最后就能得到从小到大排序好的堆了,如下图所示,这就是我们想要的结果:
三、完整代码
import java.util.Arrays;
public class Head_Sort {
public static void main(String[] args) {
int[] arr = {4,2,8,0,5,7,1,3,9};
System.out.println("排序前:"+Arrays.toString(arr));
System.out.println("排序后:"+Arrays.toString(HeapSort(arr)));
}
//堆排序
public static int[] HeapSort(int[] arr){
int len = arr.length;
/**
* 第一步,初始化堆,最大堆,从小到大。目的是对元素排序
* i从完全二叉树的第一个非叶子节点开始,也就是len/2-1开始(数组下标从0开始)
*/
for(int i = len/2-1;i >=0;i--){
HeapAdjust(arr,i,len);
}
/**
* 第二步,交换堆顶(最大)元素和二叉堆的最后一个叶子节点元素。目的是交换元素
* i从二叉堆的最后一个叶子节点元素开始,也就是len-1开始(数组下标从0开始)
*/
for(int i = len - 1;i >= 0;i--){
int temp = arr[i];
arr[i] = arr[0];
arr[0] = temp;
//交换完之后需要重新调整二叉堆,从头开始调整,此时Index=0
HeapAdjust(arr,0,i);
}
return arr;
}
/**
*初始化堆
* @param arr 待调整的二叉树(数组)
* @param index 待调整的节点下标,二叉树的第一个非叶子节点
* @param len 待调整的数组长度
*/
public static void HeapAdjust(int[] arr,int index,int len){
//先保存当前节点的下标
int max = index;
//保存左右孩子数组下标
int lchild = index*2+1;
int rchild = index*2+2;
//开始调整,左右孩子下标必须小于len,也就是确保index必须是非叶子节点
if(lchild <len && arr[lchild] > arr[max]){
max = lchild;
}
if(rchild < len && arr[rchild] > arr[max]){
max = rchild;
}
//若发生了交换,则max肯定不等于index了。此时max节点值需要与index节点值交换,上面交换索引值,这里交换节点值
if(max != index){
int temp = arr[index];
arr[index] = arr[max];
arr[max] = temp;
//交换完之后需要再次对max进行调整,因为此时max有可能不满足最大堆
HeapAdjust(arr,max,len);
}
}
}
运行结果:
四、应用场景
- 大数据量排序:堆排序可以处理大数据量,特别是当数据以堆结构存储时,它可以利用堆结构高效地进行排序。
- 优先队列:堆结构本身就是一种优先队列,因此堆排序可以用于实现优先队列,如在操作系统中,根据进程的优先级进行排序。
- 最大/最小值查询:堆排序可以通过堆结构实现最大/最小值查询,如在电商网站中,根据商品价格进行排序。
- 求前K大/小元素:堆排序还可以通过堆结构实现求前K大/小元素,如在学生成绩排序中,根据学生的成绩进行排序1。
综上所述,堆排序在大数据处理、优先队列、最大/最小值查询和前K大/小元素查询等领域具有广
泛的应用价值。
五、框架应用
六、算法分析
- 时间复杂度
建堆的时候初始化堆过程(HeapAdjust)是堆排序的关键,时间复杂度为O(log n),下面看堆排序的两个过程;
第一步,初始化堆,这一步时间复杂度是O(n);
第二步,交换堆顶元素过程,需要用到n-1次循环,且每一次都要用到(HeapAdjust),所以时间复杂度为((n-1)*log n)~O(nlog n);
最终时间复杂度:O(n)+O(nlog n),后者复杂度高于前者,所以堆排序的时间复杂度为O(nlogn); - 空间复杂度
空间复杂度是O(1),因为没有用到额外开辟的集合空间。 - 算法稳定性
堆排序是不稳定的,比方说二叉树[6a,8,13,6b],(这里的6a和6b数值上都是6,只不过为了区别6所以这样)经过堆初始化后以及排序过后就变成[6b,6a,8,13];可见堆排序是不稳定的。
------------------------------------
排序算法六:快速排序
一、基本介绍
快速排序(quicksort)是分治算法技术的一个实例,也称为分区交换排序。
划分:
数组A[low..high]被分成两个非空子数组 A[low..q]和 A[q+1..high],
使得A[low..q]中的每一个元素都小于或等于 A[q+1..high]中的元素。
在划分过程中需要计算索引q的位置。
分而治之:
对两个子数组A[low..q]和A[q+1..high]递归调用快速排序。
问题1
给定一个数组arr,和一个整数num。请把小于等于num的数放在数组的左边,大于num的数放在数
组的右边。
要求额外空间复杂度O(1),时间复杂度O(N)
二、示例代码
三、快排变种
1. 基于小于区的递归版本的快排
public static void quickSort(int[] arr) {
// 数组为空或者元素只有一个没必要排序
if (arr == null || arr.length < 2) {
return;
}
// 不满足以上条件进一步处理
process(arr, 0, arr.length - 1);
}
public static void process(int[] arr, int L, int R) {
// 递归终止条件
if (L >= R) {
return;
}
// 分值
int M = partition(arr, L, R);
// 左区域进一步处理
process(arr, L, M - 1);
// 右区域进一步处理
process(arr, M + 1, R);
}
public static int partition(int[] arr, int L, int R) {
// 左边界不能小于右边界
if (L > R) {
return -1;
}
// 左边界等于右边界,结束,说明区域分到只剩一个元素
if (L == R) {
return L;
}
// 小于等于区
int lessEqual = L - 1;
int index = L;
// 迭代
while (index < R) {
if (arr[index] <= arr[R]) {
// 小于等于区右移,交换
swap(arr, index, ++lessEqual);
}
index++;
}
// 右边界作为划分值,最终需要交换到左区域最后一个位置
swap(arr, ++lessEqual, R);
return lessEqual;
}
public static void swap(int[] arr, int i, int j) {
int tmp = arr[i];
arr[i] = arr[j];
arr[j] = tmp;
}
2. 荷兰国旗问题的递归版本的快排
2.1. 什么是荷兰国旗问题
荷兰国旗是由红白蓝3种颜色的条纹拼接而成,如下图所示:
假设这样的条纹有多条,且各种颜色的数量不一,并且随机组成了一个新的图形,
新的图形可能如下图所示,但不仅仅只有这一种情况:
需求是:把这些条纹按照颜色排好,红色的在上半部分,白色的在中间部分,蓝色的在下半部分,
我们把这类问题称作荷兰国旗问题。
2.2. 荷兰国旗问题的抽象
我们把荷兰国旗问题抽象成数组的形式是下面这样的:
给定一个整数数组和一个值M(存在于原数组中),
要求把数组中小于M的元素放到数组的左边,等于M的元素放到数组的中间,大于M的元素放到数
组的右边,最终返回一个整数数组,只有两个值,0位置是等于M的数组部分的左下标值、1位置是
等于M的数组部分的右下标值。
进一步抽象为更加通用的形式是下面这样的:
给定数组arr,将[l, r]范围内的数(当然默认是 [ 0 - arr.length - 1 ]),
小于arr[r](这里直接取数组最右边的值进行比较)放到数组左边,
等于arr[r]放到数组中间,
大于arr[r]放到数组右边。
最后返回等于arr[r]的左, 右下标值。
2.3. 解决的思路
定义一个小于区,一个大于区;遍历数组,挨个和arr[r]比较,
(1)小于arr[r],与小于区的后一个位置交换,当前位置后移;
(2)等于arr[r],当前位置直接后移;
(3)大于arr[r],与大于区的前一个位置交换,当前位置不动(交换到此位置的数还没比较过,所
以不动)。
遍历完后,arr[r]和大于区的最左侧位置交换。
最后返回,此时小于区的后一个位置,大于区的位置,即是最后的等于arr[r]的左, 右下标值。
2.4. 代码实现
public class QuickSort2 {
public static void quickSort(int[] arr) {
// 数组为null或者元素只有一个没必要排序
if (arr == null || arr.length < 2) {
return;
}
// 不只1个数,进一步处理
process(arr, 0, arr.length - 1);
}
// arr[L...R] 排有序,快排2.0方式
public static void process(int[] arr, int L, int R) {
// 递归终止条件
if (L >= R) {
return;
}
// 等于区,荷兰国旗问题后,返回的数组
int[] equalArea = netherlandsFlag(arr, L, R);
// 0位置是等于M的数组部分的左下标值
process(arr, L, equalArea[0] - 1);
// 1位置是等于M的数组部分的右下标值
process(arr, equalArea[1] + 1, R);
}
// arr[L...R] 玩荷兰国旗问题的划分,以arr[R]做划分值
// <arr[R] ==arr[R] > arr[R]
public static int[] netherlandsFlag(int[] arr, int L, int R) {
// 左边界大于右边界,没必要划分了
if (L > R) { // L...R L>R
return new int[] { -1, -1 };
}
// 左边界等于右边界,返回左右边界位置
if (L == R) {
return new int[] { L, R };
}
// 以上不满足,开始荷兰国旗问题
int less = L - 1; // < 区 右边界
int more = R; // > 区 左边界
int index = L;
while (index < more) { // 当前位置,不能和 > 区的左边界撞上
if (arr[index] == arr[R]) {
index++;
} else if (arr[index] < arr[R]) {
// 小于区右移一步再与当前位置交换,当前位置往后走一步
swap(arr, index++, ++less);
} else { // >
// 都不满足,就是大于,右边区往左走一步再与当前位置交换
swap(arr, index, --more);
}
}
// 最终还有一个划分值需要移动到中间位置,即右边区与划分值交换位置
swap(arr, more, R); // <[R] =[R] >[R]
// 完成划分,返回一个只有两个元素的数组
return new int[] { less + 1, more };
}
public static void swap(int[] arr, int i, int j) {
int tmp = arr[i];
arr[i] = arr[j];
arr[j] = tmp;
}
// for test
public static void main(String[] args) {
TimesUtil.test("优化:基于荷兰国旗问题递归版本的快速排序:", new TimesUtil.Task() {
@Override
public void execute() {
int[] orginNums = {1,2,6,8,2,4};
System.out.println(Arrays.toString(orginNums));
quickSort(orginNums);
System.out.println(Arrays.toString(orginNums));
}
});
}
}
3. 递归版本的随机快排
发现,每次都是固定你划分值,接下来进行随机分配划分值
public class QuickSort3 {
// 随机快排
public static void quickSort(int[] arr) {
// 数组为null或者元素只有一个没必要排序
if (arr == null || arr.length < 2) {
return;
}
// 不只1个数,进一步处理
process(arr, 0, arr.length - 1);
}
public static void process(int[] arr, int L, int R) {
// 递归终止条件
if (L >= R) {
return;
}
// 随机选取1个位置上的数换到R位置上,就是随机一个划分值
swap(arr, L + (int) (Math.random() * (R - L + 1)), R);
// 等于区,荷兰国旗问题后,返回的数组
int[] equalArea = netherlandsFlag(arr, L, R);
// 0位置是等于M的数组部分的左下标值
process(arr, L, equalArea[0] - 1);
// 1位置是等于M的数组部分的右下标值
process(arr, equalArea[1] + 1, R);
}
// arr[L...R] 玩荷兰国旗问题的划分,以arr[R]做划分值
// <arr[R] ==arr[R] > arr[R]
public static int[] netherlandsFlag(int[] arr, int L, int R) {
// 左边界大于右边界,没必要划分了
if (L > R) { // L...R L>R
return new int[] { -1, -1 };
}
// 左边界等于右边界,返回左右边界位置
if (L == R) {
return new int[] { L, R };
}
// 以上不满足,开始荷兰国旗问题
int less = L - 1; // < 区 右边界
int more = R; // > 区 左边界
int index = L;
while (index < more) { // 当前位置,不能和 >区的左边界撞上
if (arr[index] == arr[R]) {
index++;
} else if (arr[index] < arr[R]) {
// 小于区右移一步再与当前位置交换,当前位置往后走一步
swap(arr, index++, ++less);
} else { // >
// 都不满足,就是大于,右边区往左走一步再与当前位置交换
swap(arr, index, --more);
}
}
// 最终还有一个划分值需要移动到中间位置,即右边区与划分值交换位置
swap(arr, more, R); // <[R] =[R] >[R]
// 完成划分,返回一个只有两个元素的数组
return new int[] { less + 1, more };
}
public static void swap(int[] arr, int i, int j) {
int tmp = arr[i];
arr[i] = arr[j];
arr[j] = tmp;
}
/**
* 生成随机数组
* @param maxSize 随机数组的封顶大小
* @param maxValue 随机数值的封顶值
* @return
*/
public static int[] generateRandomArray(int maxSize, int maxValue) {
int[] arr = new int[(int) ((maxSize + 1) * Math.random())];
for (int i = 0; i < arr.length; i++) {
arr[i] = (int) ((maxValue + 1) * Math.random()) - (int) (maxValue * Math.random());
}
return arr;
}
// for test
public static void main(String[] args) {
TimesUtil.test("递归版本的随机快排:", new TimesUtil.Task() {
@Override
public void execute() {
int[] orginArrays = generateRandomArray(10,10);
System.out.println(Arrays.toString(orginArrays));
quickSort(orginArrays);
System.out.println(Arrays.toString(orginArrays));
}
});
}
}
4. 非递归版本的随机快排
public class QuickSort4 {
// 快排非递归版本需要的辅助类
// 要处理的是什么范围上的排序
public static class Op {
public int l;
public int r;
public Op(int left, int right) {
l = left;
r = right;
}
}
// 快排3.0 非递归版本
public static void quickSort(int[] arr) {
// 数组为null或者元素只有一个没必要排序
if (arr == null || arr.length < 2) {
return;
}
// 获取数组长度
int R = arr.length;
// 随机选取1个位置上的数换到R位置上,就是随机一个划分值
swap(arr, (int) (Math.random() * R), R - 1);
// 等于区,荷兰国旗问题后,返回的数组
int[] equalArea = netherlandsFlag(arr, 0, R - 1);
// 获取等于区的左边数
int el = equalArea[0];
// 获取等于区的右边数
int er = equalArea[1];
Stack<Op> stack = new Stack<>();
stack.push(new Op(0, el - 1));
stack.push(new Op(er + 1, R - 1));
while (!stack.isEmpty()) {
Op op = stack.pop(); // op.l ... op.r
if (op.l < op.r) {
swap(arr, op.l + (int) (Math.random() * (op.r - op.l + 1)), op.r);
equalArea = netherlandsFlag(arr, op.l, op.r);
el = equalArea[0];
er = equalArea[1];
stack.push(new Op(op.l, el - 1));
stack.push(new Op(er + 1, op.r));
}
}
}
// arr[L...R] 玩荷兰国旗问题的划分,以arr[R]做划分值
// <arr[R] ==arr[R] > arr[R]
public static int[] netherlandsFlag(int[] arr, int L, int R) {
// 左边界大于右边界,没必要划分了
if (L > R) { // L...R L>R
return new int[] { -1, -1 };
}
// 左边界等于右边界,返回左右边界位置
if (L == R) {
return new int[] { L, R };
}
// 以上不满足,开始荷兰国旗问题
int less = L - 1; // < 区 右边界
int more = R; // > 区 左边界
int index = L;
while (index < more) { // 当前位置,不能和 >区的左边界撞上
if (arr[index] == arr[R]) {
index++;
} else if (arr[index] < arr[R]) {
// 小于区右移一步再与当前位置交换,当前位置往后走一步
swap(arr, index++, ++less);
} else { // >
// 都不满足,就是大于,右边区往左走一步再与当前位置交换
swap(arr, index, --more);
}
}
// 最终还有一个划分值需要移动到中间位置,即右边区与划分值交换位置
swap(arr, more, R); // <[R] =[R] >[R]
// 完成划分,返回一个只有两个元素的数组
return new int[] { less + 1, more };
}
// 两两交换算法
public static void swap(int[] arr, int i, int j) {
int tmp = arr[i];
arr[i] = arr[j];
arr[j] = tmp;
}
// for test
public static void main(String[] args) {
TimesUtil.test("优化:基于荷兰国旗问题的非递归版本随机快速排序:", new TimesUtil.Task() {
@Override
public void execute() {
int[] orginNums = {1,2,6,8,2,4};
System.out.println(Arrays.toString(orginNums));
quickSort(orginNums);
System.out.println(Arrays.toString(orginNums));
}
});
}
}
四、应用场景
五、框架应用
六、算法分析
------------------------------------
排序算法七:拓扑排序
待更新
参考文献
------------------------------------
排序算法八:不基于比较的排序(3种)
前置知识
桶排序思想下的排序:计数排序 & 基数排序
桶排序思想下的排序都是不基于比较的排序
时间复杂度O(N),额外空间复杂度O(M)
应用范围有限,需要样本的数据状况满足桶的划分
一、桶排序
1. 什么是桶排序
桶排序(Bucket Sort)又称箱排序,是一种比较常用的排序算法。其算法原理是将数组分到有限
数量的桶里,再对每个桶分别排好序(可以是递归使用桶排序,也可以是使用其他排序算法将每个
桶分别排好序),最后一次将每个桶中排好序的数输出。
2. 算法思想
桶排序的思想就是把待排序的数尽量均匀地放到各个桶中,再对各个桶进行局部的排序,最后再按序将各个桶中
的数输出,即可得到排好序的数。
- 首先确定桶的个数。因为桶排序最好是将数据均匀地分散在各个桶中,那么桶的个数最好是应该根据数据的分散情况来确定。
首先找出所有数据中的最大值mx和最小值mn;
根据mx和mn确定每个桶所装的数据的范围 size,有size = (mx - mn) / n + 1,n为数据的个数,
需要保证至少有一个桶,故而需要加个1;
求得了size即知道了每个桶所装数据的范围,还需要计算出所需的桶的个数cnt,
有cnt = (mx - mn) / size + 1,需要保证每个桶至少要能装1个数,故而需要加个1; - 求得了size和cnt后,即可知第一个桶装的数据范围为 [mn, mn + size),第二个桶为 [mn + size, mn + 2 * size),…,以此类推
因此步骤2中需要再扫描一遍数组,将待排序的各个数放进对应的桶中。 - 对各个桶中的数据进行排序,可以使用其他的排序算法排序,例如快速排序;也可以递归使用桶排序进行排序;
- 将各个桶中排好序的数据依次输出,最后得到的数据即为最终有序。
3. 案例分析
例如,待排序的数为:3, 6, 9, 1
1)求得 mx = 9,mn = 1,n = 4
size = (9 - 1) / n + 1 = 3
cnt = (mx - mn) / size + 1 = 3
2)由上面的步骤可知,共3个桶,每个桶能放3个数,第一个桶数的范围为 [1, 4),第二个[4, 7),
第三个[7, 10)扫描一遍待排序的数,将各个数放到其对应的桶中,放完后如下图所示:
4)依次输出各个排好序的桶中的数据,即为:1, 3, 6, 9
可见,最终得到了有序的排列。
4. 代码实现
import java.util.ArrayList;
public class BucketSort {
public void bucketSort(int[] nums) {
int n = nums.length;
int mn = nums[0], mx = nums[0];
// 找出数组中的最大最小值
for (int i = 1; i < n; i++) {
mn = Math.min(mn, nums[i]);
mx = Math.max(mx, nums[i]);
}
int size = (mx - mn) / n + 1; // 每个桶存储数的范围大小,使得数尽量均匀地分布在各个桶中,保证最少存储一个
int cnt = (mx - mn) / size + 1; // 桶的个数,保证桶的个数至少为1
List<Integer>[] buckets = new List[cnt]; // 声明cnt个桶
for (int i = 0; i < cnt; i++) {
buckets[i] = new ArrayList<>();
}
// 扫描一遍数组,将数放进桶里
for (int i = 0; i < n; i++) {
int idx = (nums[i] - mn) / size;
buckets[idx].add(nums[i]);
}
// 对各个桶中的数进行排序,这里用库函数快速排序
for (int i = 0; i < cnt; i++) {
buckets[i].sort(null); // 默认是按从小打到排序
}
// 依次将各个桶中的数据放入返回数组中
int index = 0;
for (int i = 0; i < cnt; i++) {
for (int j = 0; j < buckets[i].size(); j++) {
nums[index++] = buckets[i].get(j);
}
}
}
public static void main(String[] args) {
int[] nums = {19, 27, 35, 43, 31, 22, 54, 66, 78};
BucketSort bucketSort = new BucketSort();
bucketSort.bucketSort(nums);
for (int num: nums) {
System.out.print(num + " ");
}
System.out.println();
}
}
5. 算法分析
二、计数排序
1. 什么是计数排序
计数排序:核心在于将输入的数据值转化为键存储在额外开辟的数组空间中。
作为一种线性时间复杂度的排序,计数排序要求输入的数据必须是有确定范围的整数。
2. 算法步骤
我们大概讲一下算法的步骤。
- 找出待排序的数组中的最大元素max和最小元素min
- 统计数组中每个元素num出现的次数,存入数组countArray的countArray[num-min]项中
- 计数数组变形,对所有的计数累加(从第一项开始countArray[i] = countArray[i] + countArray[i - 1])
- 反向填充目标数组arr:从后往前遍历待排序的数列copyArray(拷贝份),
由数组元素num计算出对应的计数数组的索引countIndex为num - min,
从而推断出num在arr的位置为index为countArray[num-min] - 1,
然后num填充到arr[index],
最后记得计数数组的值减1,即countArray[countIndex]–
3. maven依赖
pom.xml
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
<version>2.6.0</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.16.14</version>
</dependency>
</dependencies>
4. 流程解析
假设我们要排序的数据是:8, 10, 12, 9, 8, 12, 8
4.1. 计数流程图
首先我们看看计数统计是什么一回事,计数统计就是把数组中每个元素出现的次数都记录下来,并
且能够通过元素找到对应的次数。
4.2. 计数数组变形
那么计数数组变形又是干啥呢?计数数组变形是从第一项开始,每一项都等于它本身和前一项的和,这样做,得
到的值的意思是当前值前面还有多个数字,比如arr[1]=4,表示当前值前面有4-1=3个数字;arr[2]=5,表示当前值
前面有5-1=4个数字。
4.3. 排序过程
最后我们看下具体是怎么排序的,又我们的数组的值推导得到索引,然后从计数数组中找到应该要排的位置,最
后插入到对应的数组中,这种方式也是一种稳定的排序方式。
4.4. 代码实现
具体的编码实现如下,下面的实现方式也是稳定排序的方式。
/**
* 计数排序
*
* @param arr
* @return
*/
public static void countingSort(int[] arr) {
if (arr.length == 0) {
return;
}
// 原数组拷贝一份
int[] copyArray = Arrays.copyOf(arr, arr.length);
// 初始化最大最小值
int max = Integer.MIN_VALUE;
int min = Integer.MAX_VALUE;
// 找出最小值和最大值
for (int num : copyArray) {
max = Math.max(max, num);
min = Math.min(min, num);
}
// 新开辟一个数组用于统计每个元素的个数(范围是:最大数-最小数+1)
int[] countArray = new int[max - min + 1];
// 增强for循环遍历
for (int num : copyArray) {
// 加上最小偏差是为了让最小值索引从0开始,同时可有节省空间,每出现一次数据就加1
// 真实值+偏差=索引值
countArray[num - min]++;
}
log.info("countArray的初始值:{}", countArray);
// 获取数组的长度
int length = countArray.length;
// 计数数组变形,新元素的值是前面元素累加之和的值
for (int i = 1; i < length; i++) {
countArray[i] = countArray[i] + countArray[i - 1];
}
log.info("countArray变形后的值:{}", countArray);
// 遍历拷贝数组中的元素,填充到原数组中去,从后往前遍历
for (int j = copyArray.length - 1; j >= 0; j--) {
// 数据对应计数数组的索引
int countIndex = copyArray[j] - min;
// 数组的索引获取(获取到的计数数组的值n就是表示当前数据前有n-1个数据,数组从0开始,故当前元素的索引就是n-1)
int index = countArray[countIndex] - 1;
// 数组中的值直接赋值给原数组
arr[index] = copyArray[j];
// 计数数组中,对应的统计值减1
countArray[countIndex]--;
log.info("countArray操作后的值:{}", countArray);
}
log.info("排列结果的值:{}", arr);
}
public static void main(String[] args) {
int[] arr = new int[]{8, 10, 12, 9, 8, 12, 8};
log.info("要排序的初始化数据:{}", arr);
//从小到大排序
countingSort(arr);
log.info("最后排序后的结果:{}", arr);
}
运行结果:
要排序的初始化数据:[8, 10, 12, 9, 8, 12, 8]
countArray的初始值:[3, 1, 1, 0, 2]
countArray变形后的值:[3, 4, 5, 5, 7]
countArray操作后的值:[2, 4, 5, 5, 7]
countArray操作后的值:[2, 4, 5, 5, 6]
countArray操作后的值:[1, 4, 5, 5, 6]
countArray操作后的值:[1, 3, 5, 5, 6]
countArray操作后的值:[1, 3, 5, 5, 5]
countArray操作后的值:[1, 3, 4, 5, 5]
countArray操作后的值:[0, 3, 4, 5, 5]
排列结果的值:[8, 8, 8, 9, 10, 12, 12]
最后排序后的结果:[8, 8, 8, 9, 10, 12, 12]
三、基数排序
1. 什么是基数排序
基数排序:(radix sort)属于“分配式排序”(distribution sort),又称“桶子法”(bucket sort)或
bin sort,顾名思义,它是透过键值的部份资讯,将要排序的元素分配至某些“桶”中,从而达到排序
的作用,基数排序法是属于稳定性的排序。
2. 计数排序 vs 桶排序 vs 基数排序
- 计数排序
每个桶只存储单一键值 - 桶排序
每个桶存储一定范围的数值 - 基数排序
根据键值的每位数字来分配桶
3. 算法步骤
- 找出待排序的数组中的最大元素max
- 根据指定的桶数创建桶,本文使用的桶是LinkedList结构
- 从个位数开始到最高位进行遍历:num/ divider % 10 = 1,divider 取值为[1,10,100,100,…]
- 遍历数组中每一个元素,先进行分类,然后进行收集,分类就是按位放到对应的桶中,比如个位、十位、百位等
(只看指定的位(个位、十位、百位等),如果此位没有数据则以0填充,比如8,按十位分类,那么就是08,放0号桶) - 收集就是把桶的数据取出来放到我们的数组中,完成排序
当然算法也可以对字母类排序,本文主要是对数字排序,大家比较好理解。
4. maven依赖
pom.xml
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
<version>2.6.0</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.16.14</version>
</dependency>
</dependencies>
5. 流程解析
假设我们要排序的数据是:10, 19, 32, 200, 23, 22, 8, 12, 9, 108
5.1. 个位数排序
5.2. 十位数排序
5.3. 百位数排序
6. 代码实现
/**
* 基数排序
*/
public static void radixSort(int[] arr) {
// 初始化最大值
int max = Integer.MIN_VALUE;
// 找出最大值
for (int num : arr) {
max = Math.max(max, num);
}
// 我们这里是数字,所以初始化10个空间,采用LinkedList
LinkedList<Integer>[] list = new LinkedList[10];
for (int i = 0; i < 10; i++) {
list[i] = new LinkedList<>();// 确定桶的格式为ArrayList
}
// 个位数:123 / 1 % 10 = 3
// 十位数:123 / 10 % 10 = 2
// 百位数: 123 / 100 % 10 = 1
for (int divider = 1; divider <= max; divider *= 10) {
// 分类过程(比如个位、十位、百位等)
for (int num : arr) {
int no = num / divider % 10;
list[no].offer(num);
}
log.info("分类结果为:{}", Arrays.asList(list));
int index = 0; // 遍历arr原数组
// 收集的过程
for (LinkedList<Integer> linkedList : list) {
while (!linkedList.isEmpty()) {
arr[index++] = linkedList.poll();
}
}
log.info("排序后结果为:{}", arr);
log.info("---------------------------------");
}
}
public static void main(String[] args) {
int[] arr = {10, 19, 32, 200, 23, 22, 8, 12, 9, 108};
log.info("要排序的数据为:{}", arr);
radixSort(arr);
log.info("基数排序的结果为:{}", arr);
}
运行结果:
要排序的数据为:[10, 19, 32, 200, 23, 22, 8, 12, 9, 108]
分类结果为:[[10, 200], [], [32, 22, 12], [23], [], [], [], [], [8, 108], [19, 9]]
排序后结果为:[10, 200, 32, 22, 12, 23, 8, 108, 19, 9]
---------------------------------
分类结果为:[[200, 8, 108, 9], [10, 12, 19], [22, 23], [32], [], [], [], [], [], []]
排序后结果为:[200, 8, 108, 9, 10, 12, 19, 22, 23, 32]
---------------------------------
分类结果为:[[8, 9, 10, 12, 19, 22, 23, 32], [108], [200], [], [], [], [], [], [], []]
排序后结果为:[8, 9, 10, 12, 19, 22, 23, 32, 108, 200]
---------------------------------
基数排序的结果为:[8, 9, 10, 12, 19, 22, 23, 32, 108, 200]