判断排序算法是否文档我们可以分析一下如下两个关键指标:
排序的稳定性:对于有序的算法,排序完之后,相对位置是否发生该表
是否是原地排序:排序的时候,是否有另外增加空间,还是直接使用了几个变量就在原本的空间进行原地排序
选择排序算法
选择排序不具备排序的稳定性。
选择排序就是你再没有接触过其他排序之前最容易想到的一种排序方法,时间复杂度是N的二次方。
会嵌套两次循环进行排序,每次寻找都需要遍历一次,找到最小值。
java
class Solution {
public int[] sortArray(int[] nums) {
//选择排序
//数组大小
int n = nums.length;
//提供一个遍历位置的索引变量
int sortIndex = 0;
//外层循环
while( sortIndex < n){
//每次外层循环都需要找到一个最小值的索引
//所以需要定义一个每次外循环需要找到的一个最小值索引变量
//每次遍历初始化的值就是已经排序好的索引的最后一个索引
int minIndex = sortIndex;
//内层遍历
for(int i = sortIndex+1;i< n;i++){
if(nums[i] < nums[minIndex]){
minIndex = i;
}
}
int t = nums[sortIndex];
nums[sortIndex] = nums[minIndex];
nums[minIndex] = t;
sortIndex++;
}
return nums;
}
}
结果是超出时间限制
选择排序算法的优化想法
选择排序的时间复杂度太高高了,提交任务显示时间超时了
,既然算法逻辑没问题,时间复杂度太高了,造成时间复杂度太高的原因,那就是有数据冗余了;
那快速排序是在哪个地方出现冗余了呢,仔细看一下步骤,冗余数据一般就是遍历数组的时候每次都会重复遍历的地方,在快速排序中就有:我每次遍历从0-n,我第二次还要再遍历一次从1 - n,南无1-n就又被遍历了一次,能不能再遍历0-n的时候就得到1-n的遍历结果呢?答案是不能的,我们要找最小值,1-n的最小值无处可找,没有其他的信息提供就没办法计算得到。
那是否尝试一下从尾部开始呢,细想一下,好像确实是可以的,再仔细想一想,的确是可以的。从最后一个元素开始计算,如果我要计算nums【0】的那就是:nums[0...n] = min(nums[0],nums[1...n])
当然这个是不行的,原因是因为如果原数组发生了值的变化,那就没办法通过这种方式计算了。所以快速排序没有什么能优化落地的方法
冒泡排序前身(对选择排序进行稳定性优化)
这个我就不多说了,在代码里面直接注释了
但是需要提出的是,这个只是做了稳定新优化,复杂度没有优化,而且还多增加了一个for循环,所以代码提交还是运行超时,但是代码逻辑是正确的
java
class Solution {
public int[] sortArray(int[] nums) {
//选择排序
//数组大小
int n = nums.length;
//提供一个遍历位置的索引变量
int sortIndex = 0;
//外层循环
while( sortIndex < n){
//每次外层循环都需要找到一个最小值的索引
//所以需要定义一个每次外循环需要找到的一个最小值索引变量
//每次遍历初始化的值就是已经排序好的索引的最后一个索引
int minIndex = sortIndex;
//内层遍历
for(int i = sortIndex+1;i< n;i++){
if(nums[i] < nums[minIndex]){
minIndex = i;
}
}
//接下里不使用快速排序的这种直接替代的方式,虽然快,但是打破了排序的稳定性
//因为他是不相连接的两个索引的元素直接铁环,那么元素与元素之间的相对位置就发生了变化,那么排序就不稳定了
// int t = nums[sortIndex];
// nums[sortIndex] = nums[minIndex];
// nums[minIndex] = t;
//现在我们想让我们的排序是稳定的
//那么再每次替换最小值的时候不要偷懒,一个一个像冒泡一样的从后往前推,遇到一个左大右小的就交换位置
//把sortIndex到minIndex的元素整体移动一位
//这个就是冒泡排序的前身
int minVal = nums[minIndex];
//把sortIndex到minIndex的元素整体移动一位,给minVal腾出位置
for(int i = minIndex; i > sortIndex;i--){
nums[i] = nums[i-1];
}
nums[sortIndex] = minVal;
sortIndex++;
}
return nums;
}
}

冒泡排序(基于快速排序优化稳定性的 再次优化)
上面基于快速排序优化稳定性的 再次优化的复杂度还是N的二次方,for循环还多了一个,现在怎么优化,才能去掉for循环,收一个时间复杂度就下去了。如果可以,那就是再选择排序的基础上,优化了稳定性,又优化了时间复杂度(减少一个for循环).
答案确实是可以的。
我们分析一下我们的两个循环:
首先我们的第一个循环:while循环的遍历:是循环(sortIndex...N)
第二个循环是在第一个循环的基础上找到最小值并和sortIndex替换位置
那么现在我们能不能优化一下,就是我们在第二个循环的时候,能不能再找最小值的时候,最小值已经在应该在的位置上了,而不是还需要我们去替换,这确实是可以的
看一下代码,我是这届像冒泡一样进行处理,遇到可以交换的就交换掉,而且每次都是直接交换相邻的
java
class Solution {
public int[] sortArray(int[] nums) {
//选择排序
//数组大小
int n = nums.length;
//提供一个遍历位置的索引变量
int sortIndex = 0;
//外层循环
while( sortIndex < n){
//这个就是基于快速排序的第二次优化了,保证稳定性的基础上优化for循环
//内层遍历
for(int i = n -1;i > sortIndex;i--){
if(nums[i] < nums[i-1]){
int tmp = nums[i-1];
nums[i-1] = nums[i];
nums[i] = tmp;
}
}
//每次外层循环都需要找到一个最小值的索引
//所以需要定义一个每次外循环需要找到的一个最小值索引变量
//每次遍历初始化的值就是已经排序好的索引的最后一个索引
// int minIndex = sortIndex;
// //内层遍历
// for(int i = sortIndex+1;i< n;i++){
// if(nums[i] < nums[minIndex]){
// minIndex = i;
// }
// }
//接下里不使用快速排序的这种直接替代的方式,虽然快,但是打破了排序的稳定性
//因为他是不相连接的两个索引的元素直接铁环,那么元素与元素之间的相对位置就发生了变化,那么排序就不稳定了
// int t = nums[sortIndex];
// nums[sortIndex] = nums[minIndex];
// nums[minIndex] = t;
//现在我们想让我们的排序是稳定的
//那么再每次替换最小值的时候不要偷懒,一个一个像冒泡一样的从后往前推,遇到一个左大右小的就交换位置
//把sortIndex到minIndex的元素整体移动一位
//这个就是冒泡排序的前身
// int minVal = nums[minIndex];
//把sortIndex到minIndex的元素整体移动一位,给minVal腾出位置
// for(int i = minIndex; i > sortIndex;i--){
// nums[i] = nums[i-1];
// }
// nums[sortIndex] = minVal;
sortIndex++;
}
return nums;
}
}

但是结果依然是超出时间限制,我们分析一下我们这个代码,还是两层嵌套循环,相比叫上次的2个for循环我们优化掉一个for循环,就是实现了在找内存循环的最小值的时候顺便就把最小值通过相邻索引交换的方式放到准确位置上,从而减少的,但是实际上还是2层嵌套循环,时间复杂度上还是N得平方,可以理解常是O(N的平方 / 2)。
这个就是冒泡排序了,是对快速排序进行2次的优化得到的。
接下来我们看看还能不能继续优化,其实是可以的,快速排序能优化的点我们都优化了,那么冒泡排序还有没有优化的点呢,之前选择排序不管你遍历的是不是有序的,都会遍历一遍,那么现在冒泡排序是不是就不存在这个限制了,如果识别到时有序的了,是不是就可以提前终止算法了,答案是的,
对冒泡排序进行的有序情况提前终止程序优化
java
class Solution {
public int[] sortArray(int[] nums) {
//选择排序
//数组大小
int n = nums.length;
//提供一个遍历位置的索引变量
int sortIndex = 0;
//外层循环
while( sortIndex < n){
//这个就是基于快速排序的第二次优化了,保证稳定性的基础上优化for循环
//内层遍历
//计入一个变量
boolean swapped = false;
for(int i = n -1;i > sortIndex;i--){
if(nums[i] < nums[i-1]){
int tmp = nums[i-1];
nums[i-1] = nums[i];
nums[i] = tmp;
//外层循环没次循环,但凡遇到一次,说明就不是有序的
swapped = true;
}
}
if(!swapped){
break;
}
//每次外层循环都需要找到一个最小值的索引
//所以需要定义一个每次外循环需要找到的一个最小值索引变量
//每次遍历初始化的值就是已经排序好的索引的最后一个索引
// int minIndex = sortIndex;
// //内层遍历
// for(int i = sortIndex+1;i< n;i++){
// if(nums[i] < nums[minIndex]){
// minIndex = i;
// }
// }
//接下里不使用快速排序的这种直接替代的方式,虽然快,但是打破了排序的稳定性
//因为他是不相连接的两个索引的元素直接铁环,那么元素与元素之间的相对位置就发生了变化,那么排序就不稳定了
// int t = nums[sortIndex];
// nums[sortIndex] = nums[minIndex];
// nums[minIndex] = t;
//现在我们想让我们的排序是稳定的
//那么再每次替换最小值的时候不要偷懒,一个一个像冒泡一样的从后往前推,遇到一个左大右小的就交换位置
//把sortIndex到minIndex的元素整体移动一位
//这个就是冒泡排序的前身
// int minVal = nums[minIndex];
//把sortIndex到minIndex的元素整体移动一位,给minVal腾出位置
// for(int i = minIndex; i > sortIndex;i--){
// nums[i] = nums[i-1];
// }
// nums[sortIndex] = minVal;
sortIndex++;
}
return nums;
}
}

因为这次本质上是一次捷径优化,对时间复杂度没有过多的影响。
插入排序(是对选择排序进行稳定性优化之后的另一种优化思路)
插入排序是对快速排序进行稳定性优化之后的的另一种优化,上面我们做的冒泡排序也是对快速排序济宁话稳定性优化之后的一种优化思路,上面做冒泡排序的时候,已经做了详细的说明好代码演示,接下来就着重讲一下插入排序是怎么通过选择排序进行稳定性优化之后进行再次优化得来的。
上面说到冒泡排序实际上是对sortIndex后的无序数据进行排序,那有没有想过对从0开始到sortIndex的有序数组,每次遍历到的sortIndex元素插入到前面的有序数组中呢,答案是可以的,而且因为数组是有序的,那虽然时间复杂度还是N得平凡,但是效率一定要比冒泡排序快,冒泡排序是N的平方/2,那么插入排序的时间复杂度是要小于等于冒泡排序的时间复杂度。
java
class Solution {
public int[] sortArray(int[] nums) {
//选择排序
//数组大小
int n = nums.length;
//提供一个遍历位置的索引变量
int sortIndex = 0;
//外层循环
while( sortIndex < n){
//插入排序
//主要就是把遍历到的元素插入到已经排序好的有序数组中
//内层遍历
for(int i = sortIndex;i > 0;i--){
if(nums[i] < nums[i-1]){
int tmp = nums[i];
nums[i] = nums[i-1];
nums[i-1] = tmp;
}
}
//每次外层循环都需要找到一个最小值的索引
//所以需要定义一个每次外循环需要找到的一个最小值索引变量
//每次遍历初始化的值就是已经排序好的索引的最后一个索引
// int minIndex = sortIndex;
// //内层遍历
// for(int i = sortIndex+1;i< n;i++){
// if(nums[i] < nums[minIndex]){
// minIndex = i;
// }
// }
//接下里不使用快速排序的这种直接替代的方式,虽然快,但是打破了排序的稳定性
//因为他是不相连接的两个索引的元素直接铁环,那么元素与元素之间的相对位置就发生了变化,那么排序就不稳定了
// int t = nums[sortIndex];
// nums[sortIndex] = nums[minIndex];
// nums[minIndex] = t;
//现在我们想让我们的排序是稳定的
//那么再每次替换最小值的时候不要偷懒,一个一个像冒泡一样的从后往前推,遇到一个左大右小的就交换位置
//把sortIndex到minIndex的元素整体移动一位
//这个就是冒泡排序的前身
// int minVal = nums[minIndex];
//把sortIndex到minIndex的元素整体移动一位,给minVal腾出位置
// for(int i = minIndex; i > sortIndex;i--){
// nums[i] = nums[i-1];
// }
// nums[sortIndex] = minVal;
sortIndex++;
}
return nums;
}
}

提交后虽然还是超出时间限制,但是确实比冒泡排序更快
那么是否可以针对插入排序,进一步进行优化呢,答案是肯定的,希尔排序就是对插入排序的进一步优化
希尔排序
希尔排序是对插入排序的进一步优化,时间复杂度小于N的平方从比较直观的角度阐述希尔算法的实现,其实就是再插入排序的基础上按照指数递增实现索引之间的交换,插入排序是一个一个的插入到有序数组里面的,是排序稳定的,而希尔排序是一批批的插入有序数组里面的,但是是排序不稳定的,因为每次变动改变元素的相对顺序。
当然,上述只是从直观上所做的优化了,具体怎么实现呢,实现代码之前,需要知道h有序数组的概念,h有序数组的概念相隔h的数组是有序的,比如h=1的时候数组就是有序数组嘛,插入排序就是按照h=1的有序数组进行插入的,其余的h>1的方式就是希尔排序了
说完了上面的希尔排序和H有序数组的盖概念之后,就可以对插入排序的代码进行优化了,优化步骤看代码注释
java
class Solution {
public int[] sortArray(int[] nums) {
//希尔排序
//数组大小
int n = nums.length;
// 我们使用的生成函数是 2^(k-1)
// 即 h = 1, 2, 4, 8, 16...
int h = 1;
while (h < n / 2) {
h = 2 * h;
}
// 改动一,把插入排序的主要逻辑套在 h 的 while 循环中
while(h>=1){
//提供一个遍历位置的索引变量
// 改动二,sortedIndex 初始化为 h,而不是 1
int sortIndex = h;
//外层循环
while( sortIndex < n){
//插入排序 --> 希尔排序
//主要就是把遍历到的元素插入到已经排序好的有序数组中
//内层遍历
// 改动三,把比较和交换元素的步长设置为 h,而不是相邻元素
for(int i = sortIndex;i >= h;i-=h){
if(nums[i] < nums[i-h]){
int tmp = nums[i];
nums[i] = nums[i-h];
nums[i-h] = tmp;
}
}
sortIndex++;
}
// 按照递增函数的规则,缩小 h
h /= 2;
}
return nums;
}
}