目录
学习预热:基础知识
一、什么是排序
二、为什么要排序
三、排序的稳定性
四、排序稳定性的意义
五、排序分类方式
方式一:内外分类
方式二:比较分类
六、排序算法性能评估
1. 算法的时间复杂度
2. 算法的空间复杂度
七、知识小结
1. 10种经典排序算法表格图
2. 排序算法选择
2.1. 稳定性比较
2.2. 平均时间复杂度
2.3. 排序算法的选择
1> 数据规模较小(9W内)
2> 数据规模很大
3> 基数排序 (稳定)
4> 希尔排序 (不稳定)
------------------------------------
排序算法一:冒泡排序
一、简介
二、示例代码
三、变种
1. 鸡尾酒排序
1.1. 简介
1.2. 代码
1.3. 复杂度
2. 短冒泡排序
2.1. 简介
2.2. 代码
3. 奇偶排序
3.1. 简介
3.2. 题目
3.3. 代码
3.3.1. 方法一:两次遍历
3.3.2. 方法二:双指针 + 一次遍历
3.3.3. 方法三:原地交换
四、应用场景
五、框架应用
1. 冒泡排序在spring 中的应用
六、算法分析
------------------------------------
排序算法二:选择排序
一、基本介绍
二、示例代码
方式一:原始版
方式二:递归版
方式三:优化版
方式四:优化递归版
三、变种
四、应用场景
五、框架应用
1. 选择排序在spring 中的应用
六、算法分析
------------------------------------
排序算法三:插入排序
一、基本介绍
二、示例代码
方法一:交换法
方法二:插入法
三、变种
1. 直接插入排序
1.1. 简介
1.2. 代码
1.3. 算法分析
2. 折半插入排序
2.1. 简介
2.2. 代码
2.3. 算法分析
3. 希尔排序(Donald.L.Shell)
3.1. 简介
3.2. 代码
四、应用场景
五、框架应用
六、算法分析
------------------------------------
第二个步骤,堆排序
三、完整代码
四、应用场景
五、框架应用
六、算法分析
------------------------------------
排序算法六:快速排序
一、基本介绍
二、示例代码
三、快排变种
1. 基于小于区的递归版本的快排
2. 荷兰国旗问题的递归版本的快排
2.1. 什么是荷兰国旗问题
2.2. 荷兰国旗问题的抽象
2.3. 解决的思路
2.4. 代码实现
3. 递归版本的随机快排
4. 非递归版本的随机快排
四、应用场景
五、框架应用
六、算法分析
------------------------------------
排序算法七:拓扑排序
参考文献
------------------------------------
排序算法八:不基于比较的排序(3种)
前置知识
一、桶排序
1. 什么是桶排序
2. 算法思想
3. 案例分析
4. 代码实现
5. 算法分析
二、计数排序
1. 什么是计数排序
2. 算法步骤
3. maven依赖
4. 流程解析
4.1. 计数流程图
4.2. 计数数组变形
4.3. 排序过程
4.4. 代码实现
三、基数排序
1. 什么是基数排序
2. 计数排序 vs 桶排序 vs 基数排序
3. 算法步骤
4. maven依赖
5. 流程解析
5.1. 个位数排序
5.2. 十位数排序
5.3. 百位数排序
6. 代码实现
学习预热:基础知识
一、什么是排序
所谓排序,就是整理表中的数据元素,使之按元素的关键字递增/递减的顺序排列。
二、为什么要排序
查找是计算机应用中必不可少并且使用频率很高的一个操作。
在一个排序表中查找一个元素,要比在一个无序表中查找效率高得多。
所以为了提高查找效率,节省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),但在部分已排序的序列中,其效率可能会高于其他排序
算法,如冒泡排序等。
此外,插入排序还具有稳定性,即相等的元素在排序后保持原有的顺序不变。
二、示例代码
方法一:交换法
/*** 希尔排序*/@Testpublic 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));}}
方法二:插入法
/*** 希尔排序*/@Testpublic 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);}//打印堆顶元素,应该为最大元素9System.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=0HeapAdjust(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=0HeapAdjust(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>Rreturn 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 testpublic static void main(String[] args) {TimesUtil.test("优化:基于荷兰国旗问题递归版本的快速排序:", new TimesUtil.Task() {@Overridepublic 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>Rreturn 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 testpublic static void main(String[] args) {TimesUtil.test("递归版本的随机快排:", new TimesUtil.Task() {@Overridepublic 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.rif (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>Rreturn 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 testpublic static void main(String[] args) {TimesUtil.test("优化:基于荷兰国旗问题的非递归版本随机快速排序:", new TimesUtil.Task() {@Overridepublic void execute() {int[] orginNums = {1,2,6,8,2,4};System.out.println(Arrays.toString(orginNums));quickSort(orginNums);System.out.println(Arrays.toString(orginNums));}});}}
四、应用场景
五、框架应用
六、算法分析
------------------------------------
排序算法七:拓扑排序
待更新
参考文献
- Java手写拓扑排序和拓扑排序应用拓展案例
- 拓扑排序与关键路径
------------------------------------
排序算法八:不基于比较的排序(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; // 桶的个数,保证桶的个数至少为1List<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];// 计数数组中,对应的统计值减1countArray[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个空间,采用LinkedListLinkedList<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 = 1for (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]