排序算法就是将一个数组排成有序的样子。
排序算法可以分为基于比较的排序算法和非比较的排序算法。
基于比较的排序算法:
选择排序、冒泡排序、插入排序、希尔排序
归并排序、快速排序、堆排序
非比较的排序算法:
计数排序、基数排序、桶排序(一般不了解,是因为这些算法的应用很有限的。)
选择排序
就是一个一个的选出来,先选出最小的元素。再选出第二小的元素。依次往下进行
因为数组的长度是有限的,左移就这样通过有限定的次数一定能给数组排好序。
代码演示:
import java.util.Arrays;
public class Demo912 {
public static void main(String[] args) {
int[] nums = {-1,2,-8,-10}; //给定一个数组
int[] after = sortArray(nums); //的带排序后的数组
System.out.println(Arrays.toString(after)); //打印输出得到数组
}
public static int[] sortArray(int[] nums) {
int len = nums.length; //获得数组的长度
for (int i = 0; i < len - 1; i++) {
int minIndex = i; //记录当前查询比较小的数的下标索引。
for (int j = i + 1; j < len; j++) {
if (nums[j] < nums[minIndex]) { // 当在后面发现有比当前大的
minIndex = j; // 将较小数的索引给记录下
}
}
swap(nums, minIndex, i); //将数组当中后面的较小数与前面调换
}
return nums;
}
/**
* 对数组中的两个数做调换
* @param nums 数组
* @param minIndex 需要调换的数
* @param i 调换的位置的数
*/
private static void swap(int[] nums, int minIndex, int i) {
int temp = nums[minIndex];
nums[minIndex] = nums[i];
nums[i] = temp;
}
}
当前这个算法的时间复杂度就是
选择排序的特点和优化方向
执行时间与数据无关。
交换次数是最少的。
所以选择排序就适用于交换成本较高的排序算法。
选择排序主要耗时的部分就在于需要遍历还未排定的部分选出最小的元素。
所以在这我们就可以选择使用堆排序来帮助选择排序在还未排好序的部分选择出最值问题。
冒泡排序
就是每一轮把还未排定的部分中的最大的元素通过量两两交换的方式交换到还未排定部分的末尾。
在每一轮都确定一个元素在排好序以后最终的位置,
冒泡就像鱼吐泡泡一样,大的数一点一点网上冒泡上去。
冒泡排序每一轮都是观察相邻的两个元素之间的数值关系。经过这么一轮,数组中最大的一个肯定就来到数组中的最后一位。
且每一轮都可以排定一个元素的位置。
代码演示:
import java.util.Arrays;
public class Demo912 {
public static void main(String[] args) {
int[] nums = {-1,2,-8,-10}; //给定一个数组
int[] after = sortArray(nums); //的带排序后的数组
System.out.println(Arrays.toString(after)); //打印输出得到数组
}
public static int[] sortArray(int[] nums){
int len = nums.length; //获得数组的长度
for (int i = 0; i < len-1; i++) {
for (int j = 1; j < len-i; j++) {
//因为每次循环一圈,最后一个肯定就固定的,所以下次比较就不用再考虑上次最后的排序
if (nums[j -1] > nums [j]){ //将数组当中上一个数与当前数进行比较,如果前面的大
swap(nums,j-1,j);//就交换两数的位置
}
}
}
return nums;
}
/**
* 对数组中的两个数做调换
* @param nums 数组
* @param minIndex 需要调换的数
* @param i 调换的位置的数
*/
private static void swap(int[] nums, int minIndex, int i) {
int temp = nums[minIndex];
nums[minIndex] = nums[i];
nums[i] = temp;
}
}
当前这个算法的时间复杂度就是
冒泡排序的优化方向
在一轮扫描当中,如果发现没有元素进行交换,就说明该数组已经有序,就可以终止程序了。
代码实现:
插入排序
主要思想:把一个数插入到有序数组当中去
插入排序可以看作就是将每一个元素插入到位于它左边的有序数组当中去。
代码演示:
import java.util.Arrays;
public class Demo912 {
public static void main(String[] args) {
int[] nums = {-1,2,-8,-10}; //给定一个数组
int[] after = sortArray(nums); //的带排序后的数组
System.out.println(Arrays.toString(after)); //打印输出得到数组
}
public static int[] sortArray(int[] nums){
int len = nums.length; //获得数组的长度
for (int i = 1; i < len; i++) { //记录需要循环几次,i为数组下标,从数组当中第二个数开始
for (int j = i; j > 0 ; j--) { //将当前的数与前面有序数组进行比较
if (nums[j-1] > nums[j]){ //如果前面的数大于后面的,进行调换
swap(nums,j-1,j);
}
}
}
return nums;
}
/**
* 对数组中的两个数做调换
* @param nums 数组
* @param minIndex 需要调换的数
* @param i 调换的位置的数
*/
private static void swap(int[] nums, int minIndex, int i) {
int temp = nums[minIndex];
nums[minIndex] = nums[i];
nums[i] = temp;
}
}
可以看出插入排序是将当前要比的数与左边的有序数组做比较。
因为左边的是一个有序的数组,所以要比的数只要比左边数组最右边的数大,就可以直接停止比较了。
当前这个算法的时间复杂度就是
特点
插入排序就是有可能会提前终止排序。
举个例子,如果一个数组已经是有序的了。插入排序每一轮都只需要做一次比较就可以了。确认当前位置的元素一定大于等于它前面一个位置的元素的值,只需要做数组长度-1次后的比较后程序就终止了。
而选择排序作用在有序的数组上,每一轮都要把剩下的海没有排完的部分所有的元素都要看一遍。
所以插入排序作用在接近有序的数组上效果会更好一点。
接近有序的数组意思就是每个元素距离它最终所在的位置都不远。
推论:插入排序作用在规模比较小的数组上的作用效果会比较更好一些。
优点:
插入排序在接近有序的数组上有着姣好的性能;
插入排序在数据规模较小的排序任务上有着姣好的性能。
缺点:
较小的数如果在数组靠后的位置,就只能一步一步来到靠前的位置。
在《算法》第四版这本书上,告诉我们:
在绝大数情况下,插入排序应用长度为6到16之间的任意值的排序任务上都能令人满意
在Java的排序工具类的源代码里高级的排序算法归并排序和快速排序的底层就转换成了插入排序。
插入排序最终成为了许多高级排序的底层算法。
插入排序的优化
这样插入排序就变成了只有比较和赋值,而没有交换操作的排序方法,这样的优化,可以减少由于交换而产生的赋值操作。
代码:
public class Demo912 {
public static void main(String[] args) {
int[] nums = {-1, 2, -8, -10}; //给定一个数组
int[] after = sortArray(nums); //的带排序后的数组
System.out.println(Arrays.toString(after)); //打印输出得到数组
}
public static int[] sortArray(int[] nums){
int len = nums.length;
for (int i = 1; i < len; i++) { //将i这个位置的数据插入到它前面的有序列表当中去
int num = nums[i];
int j;//
for (j = i; j > 0 ; j--) {
if (nums[j-1] > num){
nums[j] = nums[j-1];
}else {
break;
}
}
nums[j] = num;
}
return nums;
}
}
也可以写成:
可以少一点点时间复杂度
此题也是力扣912题:
在使用插入排序的时候,在查找插入的位置式,可以使用二分查找,但再插入的时候,任然需要把比插入元素严格大的元素逐个后移,时间复杂度是不变的。
这两种插入排序的方式在时间复杂度上都是一样的
希尔排序
在《算法》第四版中有说到:
如果你需要解决一个排序问题而又没有系统排序函数可用(例如:直接接触硬件或是运行与嵌入式系统中的代码),可用先用希尔排序,然后再考虑是否值得将它替换为更加复杂的排序算法。
它是一个时间复杂度低于O(N^2)的排序算法;
它的表现在绝大数情况下是没有归并排序和快速排序好的。
基本思想
希尔排序是一种基于插入排序的算法;
分组插入排序或者是带间隔的插入排序。
不断地把数组整理成接近有序的样子,最后执行一次标准的插入排序。最终完成排序任务
根据插入排序的优缺点进行了一下排序。
希尔排序是逐渐缩小的间隔的插入排序,且最后一次的间隔一定为1。
在逐渐缩小间隔的过程但在中,数组变得越来越有序,通过分组,将数值很小,且很靠后的数值一下子就来到了最前面了。充分利用了插入排序的优点。
代码演示:
import java.util.Arrays;
public class Demo912 {
public static void main(String[] args) {
int[] nums = {-1, 2, -8, -10}; //给定一个数组
int[] after = sortArray(nums); //的带排序后的数组
System.out.println(Arrays.toString(after)); //打印输出得到数组
}
public static int[] sortArray(int[] nums){
int len = nums.length; //获得数组的长度
for (int delete = len / 2; delete > 0; delete /= 2) {//delete 每一组的间隔
//第一个循环创造区间间隔第一个以数组长度的一半,后面都采用上一次间隔的一半创造间隔
for (int start = 0; start < delete; start++) {
//start作为下标进行区分,间隔是多少,就分多少组的
//执行分组插入排序的代码
for (int i = start + delete; i < len; i += delete) {
int num = nums[i];
int j;//
for (j = i; j - delete >= 0 && nums[j - delete] > num; j -= delete) {
nums[j] = nums[j - delete];
}
nums[j] = num;
}
}
}
return nums;
}
}
以力扣912道题来看:
希尔排序的时间复杂度是一个比较复杂的数学过程,希尔排序的时间复杂度是与选择的步长序列有关。
步长序列是希尔排序的超参数,不同的步长序列对应着不同的实现算法。
我们上面的代码使用的希尔排序的步长序列就是一个等比序列。
步长序列为1,2,4,8,16,32…
公式为: 2^k;
时间复杂度为O(N^2)
这种步长序列在遇到2的幂次方长度的数列的时候,可能会有好几轮都是在空转。
其他的步长序列:
Hibbard 提出的步长序列:
步长序列: 1, 3, 5, 7,15,31;
公式为: (2^K)-1;
时间复杂度: O( N ^( 3 / 2 ))
就是在基本的希尔提出的步长序列的基础上每一个值减去1。
Donald Knuth 提出的步长序列:
步长序列: 1, 4 , 7, 15, 31 …
公式为: h = 3h +1;
时间复杂度为: O( N ^( 3 / 2 ))
Robert Sedgewick提出的步长序列:
他是Donald Knuth的学生,他提出的步长也是现在公认已知的最好步长序列。
步长序列:1, 5, 19, 41, 109, 209, 505, 929, …
在维基百科当中对这个步长序列介绍到:
用这样步长序列的希尔排序是比插入排序的要快,甚至在小数组中比快速排序的和堆排序好快,但是在涉及大量数据时希尔排序还是比快速排序要慢的。
希尔排序在这四种基本的排序当中是最快的排序方式。
个人见解,大家多多交流