你好陌生人,请不要试图越过权限文章,谢谢!

十大排序算法

冒泡排序

(1)时间复杂度

在设置标志变量之后:

当原始序列“正序”排列时,冒泡排序总的比较次数为n-1,移动次数为0,也就是说冒泡排序在最好情况下的时间复杂度为O(n);

当原始序列“逆序”排序时,冒泡排序总的比较次数为n(n-1)/2,移动次数为3n(n-1)/2次,所以冒泡排序在最坏情况下的时间复杂度为O(n^2);

当原始序列杂乱无序时,冒泡排序的平均时间复杂度为O(n^2)。

(2)空间复杂度

冒泡排序排序过程中需要一个临时变量进行两两交换,所需要的额外空间为1,因此空间复杂度为O(1)。

(3)稳定性

冒泡排序在排序过程中,元素两两交换时,相同元素的前后顺序并没有改变,所以冒泡排序是一种稳定排序算法。

基本思想

  冒泡排序的基本思想就是:从无序序列头部开始,进行两两比较,根据大小交换位置,直到最后将最大(小)的数据元素交换到了无序队列的队尾,从而成为有序序列的一部分;下一次继续这个过程,直到所有数据元素都排好序。

  算法的核心在于每次通过两两比较交换位置,选出剩余无序序列里最大(小)的数据元素放到队尾。引用自CSDN

代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
package cn.jianfreespace;


import java.util.Arrays;

/**
* 冒泡排序-稳定的排序算法
*/
public class Main {
public static void main(String[] args) {

int[] numbers = {4,2,5,7,9,23,53,16,76,45,1,6};


int temp;
boolean swap;
for (int x = 0;x < numbers.length;x++) {
swap = false;
for (int y = x;y < numbers.length;y++) {
if (numbers[x] > numbers[y]) {
temp = numbers[x];
numbers[x] = numbers[y];
numbers[y] = temp;
swap = true;
}
}
//代码优化
if (!swap) {
break;
}
}

System.out.println(Arrays.toString(numbers));

}
}

选择排序

稳定性

  一般情况下选择排序是不稳定的,因为默认情况下是用数组进行实现,比如{5,5,2}这么一个数组,但如果是用链表去实现的话,那么他又是稳定的。

复杂度

  复杂度平均为(On2)

基本思想

  选择排序是一种简单直观的排序算法,它也是一种交换排序算法,和冒泡排序有一定的相似度,可以认为选择排序是冒泡排序的一种改进。其中核心的思想是,对待排序的数中寻找一个最小的数,然后把最小的数存放到起始数组中去,重复以上的步骤,不断找到待排序的最小数,直到遍历完毕待排序的数组,可能得到一个从小到大的排序数组,也可以找最大值,同理。下图可清晰看到过程,引用知乎图。

实现代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
package cn.jianfreespace;


import java.util.Arrays;

/**
* 选择排序-用数组实现的选择排序是不稳定的,用链表实现的选择排序是稳定的
*/
public class Main {
public static void main(String[] args) {
int[] numbers = {4,2,5,7,9,23,53,16,76,45,1,6};
int temp = 0;
boolean swap = false;

for (int item = 0; item < numbers.length;item++) {
int min = item;
for (int j = item; j < numbers.length;j++) {
if (numbers[min] > numbers[j]) {
min = j;
swap = true;
}
}
if (swap) {
temp = numbers[item];
numbers[item] = numbers[min];
numbers[min] = temp;
}
}

System.out.println(Arrays.toString(numbers));
}
}

插入排序

平均时间复杂度:O(N^2)
最差时间复杂度:O(N^2)
空间复杂度:O(1)
排序方式:In-place
稳定性:稳定

基本思想

  插入排序是一种简单直观的排序算法,算法核心是把数组分为有序-无序两种,对无序数组遍历,每一个值与有序数组中的值进行比对,因为有序数组是有序的,可以在有序数组中从后往前判断,若当前值小于有序数组中的最后值,则有序数组的下标往前递归,直到寻找到合适的位置插入,对应的位置数依次向后移动一位,直到遍历完所有的无序值后,整个排序就完毕了。

实现代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
package cn.jianfreespace;


import java.util.Arrays;

/**
* 插入排序-稳定的排序算法
*/
public class Main {

public static void main(String[] args) {
int[] arr = {6,3,1,5,9,10,2,4,8,7};
inserSort(arr);
System.out.println(Arrays.toString(arr));
}


private static void inserSort(int[] arr) {
for (int i = 1; i < arr.length; i++) {
int temp = arr[i];
int j;
for (j = i - 1; j >= 0; j--) {
if (arr[j] > temp) {
arr[j + 1] = arr[j];
} else {
break;
}
}

arr[j + 1] = temp;
}


}

}

归并排序

  归并排序是稳定排序,它也是一种十分高效的排序,能利用完全二叉树特性的排序一般性能都不会太差。java中Arrays.sort()采用了一种名为TimSort的排序算法,就是归并排序的优化版本。每次合并操作的平均时间复杂度为O(n),而完全二叉树的深度为|log2n|。总的平均时间复杂度为O(nlogn)。而且,归并排序的最好,最坏,平均时间复杂度均为O(nlogn)。引用-图解排序算法之归并排序

基本思想

  归并排序体现是分治的思想。将已经有序的子序列合并得到新的有序的序列,即先使每个子序列都有序,在使得子序列段间有序,然后讲两个有序表合并成一个有序表。有点像表达上空间换时间,因为会重新开辟一个新的temp空间用来存储新的有序列表。使用递归进行实现该归并排序算法。

实现代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
package cn.jianfreespace;


import java.util.Arrays;

/**
* 归并排序
*/
public class Main {
public static void main(String[] args) {
int[] numbers = {6,3,1,5,9,10,2,4,8,7};
int[] temp = new int[numbers.length];
sort(numbers,0,numbers.length - 1,temp);
System.out.println(Arrays.toString(numbers));
}


/**
* 递归排序
* @param numbers
* @param left
* @param right
* @param temp
*/
private static void sort(int[] numbers,int left,int right,int[] temp) {
if (left < right) {
int mid = (left + right) / 2;
//左序列进行排序
sort(numbers,left,mid,temp);
//右序列进行排序
sort(numbers,mid + 1,right,temp);
merge(numbers,left,mid,right,temp);
}
}

/**
* 归并排序逻辑
* @param numbers
* @param left
* @param mid
* @param right
* @param temp
*/
private static void merge(int[] numbers,int left,int mid,int right,int[] temp) {
int i = left;
int j = mid + 1;
int t = 0;
while (i <= mid && j <= right) {
if (numbers[i] <= numbers[j]) {
temp[t++] = numbers[i++];
} else {
temp[t++] = numbers[j++];
}
}

while (i <= mid) {
temp[t++] = numbers[i++];
}
while (j <= right) {
temp[t++] = numbers[j++];
}

t = 0;
while (left <= right) {
numbers[left++] = temp[t++];
}

}


}

快速排序

  选择一个基准数,通过一趟排序将要排序的数据分割成独立的两部分;其中一部分的所有数据都比另外一部分的所有数据都要小。然后,再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列。《引用自Java全栈知识体系》

基本思想

  选择一个基准点,然后对数组的元素进行左右放置,若小于该基准点的,放置在基准点的左侧,大于的放置基准点的右侧.然后又分别对左右两侧的序列进行再分,在寻找新的基准点,在左右分类,直至不可在分。此算法的含义就是分类进行排序,比如{6,3,1,5,9,10,2,4,8,7},以7为基准点后分类变成了:{6,3,1,5,2,4,7,10,8,9},这样会发现在以7为基准点的情况下,它的左边永远被右边的要小,我们在后面处理的时候就可以去处理{6,3,1,5,2,4},不论怎么处理,他们的位置不会在变到7的右边,那么就可以继续对左边的数组进行分类,以4为基准点分后为:{3,1,2,4,5,6},然后在左边各自分类,直到不能在分,就保证了最小序列的有序,从而保证了整体的有序性。

实现代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
package cn.jianfreespace;

import java.util.Arrays;


/**
* 快速排序-不稳定的排序算法
*/
public class Main {
public static void main(String[] args) {
int[] numbers = {6,3,1,5,9,10,2,4,8,7};
qsort(numbers,0,numbers.length - 1);
System.out.println(Arrays.toString(numbers));
}


private static void qsort(int[] numbers,int low,int high) {

// 若左边已经不可分割则退出
if (low >= high) {
return;
}
//进行分割排序
int pivot = partition(numbers,low,high);
//对左子序列进行排序
qsort(numbers,low,pivot - 1);
//对右子序列进行排序
qsort(numbers,pivot + 1,high);


}

private static int partition(int[] numbers,int low,int high) {
//寻找一个基准点,以最右边为基准点
int pivot = numbers[high];
//初始化 ij,控制轮询,用来保证左右数字的大小情况
int i = low,j = low;
//临时变量,控制交换
int temp;

//循环,j轮询直到最后
while (j < high) {
//判断若j位上数字小于基准数,则对当前的i下标与j下标的数字进行交换,并且使得 i往后递增,j往后递增
if (numbers[j] < pivot) {
temp = numbers[j];
numbers[j] = numbers[i];
numbers[i] = temp;
i++;
j++;
} else {
//若大于,则不变,保持轮询。
j++;
}
}

//轮询完成后,所有的数字都已进行排序,则交换当前i坐标与基准点进行交换。返回当前的i下标基准
temp = numbers[high];
numbers[high] = numbers[i];
numbers[i] = temp;

return i;
}
}

堆排序

  堆排序是利用堆这种数据结构而设计的一种排序算法,堆排序是一种选择排序,它的最坏,最好,平均时间复杂度均为O(nlogn),它也是不稳定排序。可以把它看做一个完全二叉树,堆并且具有以下性质。

堆节点的公式:

  • Parent(i) = floor((i-1)/2),i 的父节点下标
  • Left(i) = 2i + 1,i 的左子节点下标
  • Right(i) = 2(i + 1),i 的右子节点下标

基本思想

  将待排序序列构造成一个大顶堆,此时,整个序列的最大值就是堆顶的根节点。将其与末尾元素进行交换,此时末尾就为最大值。然后将剩余n-1个元素重新构造成一个堆,这样会得到n个元素的次小值。如此反复执行,便能得到一个有序序列了。比如{6,3,1,5,9,10,2,4,8,7},最大堆得出最大数10,此事最大堆一定在数组的0下标位置,则有当前堆中的最后一位进行调用,后面反复生成最大堆。依次找到次小值,从后往前直到堆已没有数值,排序就结束。

实现代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
package cn.jianfreespace;


import java.util.Arrays;

/**
* 堆排序-不稳定的排序算法
*/
public class Main {

public static void main(String[] args) {
/**
* Parent(i) = floor((i-1)/2),i 的父节点下标
* Left(i) = 2i + 1,i 的左子节点下标
* Right(i) = 2(i + 1),i 的右子节点下标
*/
int[] arr = {6,3,1,5,9,10,2,4,8,7};
sort(arr);
System.out.println(Arrays.toString(arr));;
}


/**
* 构建一个大顶堆
* @param arr
*/
private static void sort(int[] arr) {

//构建一个大顶堆
for (int i = arr.length / 2 - 1;i >= 0;i--) {
adjustHeap(arr,i,arr.length);
}

for (int j = arr.length - 1; j >= 0; j--) {
swap(arr,0,j);
adjustHeap(arr,0,j);

}


}

/**
* 调整大顶堆
*/
private static void adjustHeap(int[] arr,int i,int length) {

// 树的基本性质(数组从0开始),当前节点左边为i,则子节点左坐标为: 2i +1 右坐标为 2(i+1) 父节点坐标为 (i - 1) / 2
//存储当前节点的值
int temp = arr[i];
//循环遍历他的叶节点
for (int k = 2 * i + 1;k < length;k = 2 * k + 1) {
// 判断的是左节点是否小于右节点的值,若小于,取右节点,需要判断是否存在右节点,所以需要做一下判断越界
if (k + 1 < length && arr[k] < arr[k + 1]) {
k++;
}
//判断左右节点中的其中一个是否大于父节点,若大于,则把父节点的值直接替换为子节点,开始继续循环k左边下的左右节点值,因为上面的节点产生了交换,那么可能存在子节点为父节点的情况值会没有所属的子节点大,所以需要递归遍历循环。
if (arr[k] > temp) {
arr[i] = arr[k];
i = k;
} else {
break;
}
}

arr[i] = temp;
}

/**
* 交换两个元素
*/
private static void swap(int[] arr,int i,int j) {
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
}

希尔排序

  希尔排序也是一种插入排序,它是简单插入排序经过改进之后的一个更高效的版本,也称为缩小增量排序,同时该算法是冲破O(n ^2^ )的第一批算法之一。

基本思想

  简单插入排序很循规蹈矩,不管数组分布是怎么样的,依然一步一步的对元素进行比较,移动,插入,比如[5,4,3,2,1,0]这种倒序序列,数组末端的0要回到首位置很是费劲,比较和移动元素均需n-1次。而希尔排序在数组中采用跳跃式分组的策略,通过某个增量将数组元素划分为若干组,然后分组进行插入排序,随后逐步缩小增量,继续按组进行插入排序操作,直至增量为1。希尔排序通过这种策略使得整个数组在初始阶段达到从宏观上看基本有序,小的基本在前,大的基本在后。然后缩小增量,到增量为1时,其实多数情况下只需微调即可,不会涉及过多的数据移动。——希尔排序-引用

  希尔排序对当前数组进行增量分组,比如{6,3,1,5,9,10,2,4,8,7},以数组长度 / 2作为间隔,分为5组,然后对5组分别进行插入排序,继续对增量 /2 = 2,分为两组,对两组进行插入排序,直到增量为1对整个数组进行插入,此主要是保证可能存在的大数造成的性能问题。

实现代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
package cn.jianfreespace;

import java.util.Arrays;



/**
* 希尔排序-稳定的排序算法
*/

public class Main {
public static void main(String[] args) {
int[] arr = {6,3,1,5,9,10,2,4,8,7};
shellSort(arr,arr.length);
System.out.println(Arrays.toString(arr));
}

/**
* 希尔排序
* @param arr
* @param n
*/
private static void shellSort(int[] arr,int n) {
int temp;
for (int gap = n / 2; gap > 0 ; gap = gap/ 2) {
for (int i = gap; i < n;i++) {
for(int j = i - gap;j >= 0 && arr[j] > arr[j + gap]; j -= gap) {
temp = arr[j];
arr[j] = arr[j+gap];
arr[j + gap] = temp;
}
}
}
}
}

计数排序

  计数排序不是一个比较排序算法,该算法于1954年由 Harold H. Seward提出,通过计数将时间复杂度降到了 On;

基本思想

  计数排序本质上是创建一个新的空间数组,比如有一个数组{6,3,1,5,9,10,2,4,8,7}。那么我们就开辟出一个10个空间用来存储每一个数值所在新空间的下标出现的次数。newArr = [0,0,0,0,0,0,0,0,0,0];则统计时候就能够知道每个下标的值与次数,直接输出即可。但这种只能在小数情况下,若数组存在一个极大值,则空间会造成大量的浪费,所以需要对代码进行优化。我们可以先从数组中找到最大值与最小值。把他们相减得到他们数组的区间范围,比如最大值900与最小值843,如果我们直接创建一个最大值的空间,需要new[900],但实际上我们根本用不到这么多,在843之前的空间都浪费了。我们只取他们的区间范围,就只需要创建57个空间大小。把数组中的每个数减去最小值就一定会落在我们创建的空间下标中,可以极大的节省空间资源。本质上是以空间换取了时间。比较容易理解。

  代码中对统计数组需要向后叠加是因为需要找到每个位置他们所对应的元素,比较难理解,推荐查看这篇文件辅助自己理解:一文弄懂计数排序算法!。里面详细了描述了如何进行叠加,如何取到位置值。

实现代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
package cn.jianfreespace;


import java.util.Arrays;

/**
* 计数排序——不是进行比较的排序
*/
public class Main {
public static void main(String[] args) {
int[] arr = {6,3,1,5,9,10,2,4,8,7};
int max = arr[0],min = arr[0];
for (int item = 0;item < arr.length;item++) {
if (max < arr[item]) {
max = arr[item];
}
if (min > arr[item]) {
min = arr[item];
}
}

int[] result = countSort(arr,max,min);
System.out.println(Arrays.toString(result));
}

/**
* 计数排序
*/
private static int[] countSort(int[] arr,int max,int min) {

//统计数组
int[] count = new int[max - min + 1];

//填充0
for (int item = min;item <= max;item++) {
count[max - min] = 0;
}
for (int i = 0;i < arr.length;i++) {
int num = arr[i];
count[num - min] ++;
}
//向后填充
for (int item = 1;item < count.length;item++) {
count[item] += count[item - 1];
}

//创建结果数组
int[] result = new int[arr.length];
for (int item = 0;item < arr.length;item++) {
result[count[arr[item] - min] - 1] = arr[item];
count[arr[item] - min] --;
}

return result;

}
}

桶排序

  桶排序是计数排序的升级版,判断他是是否稳定取决于我们在对每个桶进行排序的使用哪种排序算法,这里我们使用了快速排序,显然是不稳定排序的算法。桶排序可用于最大最小值相差较大的数据情况,但桶排序要求数据的分布必须均匀,否则可能导致数据都集中到一个桶中。比如[104,150,123,132,20000], 这种数据会导致前4个数都集中到同一个桶中。导致桶排序失效。

注意:

  • 基数排序是对 传统桶排序 的扩展,速度很快
  • 是经典的空间换时间的方式,占用内存空间很大

基本思想

  基本的思路是创建数量为(数组最大值/数组最小值) /数组长度 + 1的桶,初始化桶,然后变量数组元素,当前的值 - min值 / 数组长度为条件,选择进入到哪个桶中,遍历完毕后,对每个桶进行快速排序,依次输出桶中的值结果就是最后的过程。

实现代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
package cn.jianfreespace;


import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;

/**
* 桶排序——稳定的排序算法
*/
public class Main {
public static void main(String[] args) {
int[] arr = {6,3,1,5,9,10,2,4,8,7,89,56,45};
int max = arr[0],min = arr[0];
for (int item = 0;item < arr.length;item++) {
if (max < arr[item]) {
max = arr[item];
}
if (min > arr[item]) {
min = arr[item];
}
}
bucketSort(arr,max,min);
}


/**
* 桶排序
* @param arr
*/
private static void bucketSort(int[] arr,int max,int min) {
//计算桶数
int backetNum = (max / min) /arr.length + 1;
//初始化桶
ArrayList<ArrayList<Integer>> backetArr = new ArrayList<>(backetNum);
for (int item = 0;item < backetNum;item++) {
backetArr.add(new ArrayList<Integer>());
}

for (int i = 0; i < arr.length;i++) {
int num = (arr[i] - min) / arr.length;
backetArr.get(num).add(arr[i]);
}

for (int item = 0;item < backetArr.size();item++) {
Collections.sort(backetArr.get(item));
}

System.out.println(backetArr.toString());

}
}

基数排序

  基数排序(radix sort)属于 分配式排序 (distribution sort),又称 桶子法 (bucket sort 或 bin sort),顾名思义,它是通过键值的各个位的值,将要排序的 元素分配 至某些「桶」中,达到排序的作用。

基本思想

  1. 将所有待比较数值 统一为同样的数位长度 ,数位较短的数 前面补零
  2. 然后从最低位开始,依次进行一次排序
  3. 这样从最低位排序一直到最高位排序完成以后,序列就变成了一个有序序列

实现过程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
package cn.jianfreespace;


import java.util.Arrays;

/**
* 基数排序
*/
public class Main {
public static void main(String[] args) {
//
int[] arr = {6,3,1,5,9,10,2,4,8,7};
radixSort(arr);
}


/**
* 基数排序
* @param arr
*/
private static void radixSort(int[] arr) {

//获取最大值,用于判断需要循环几轮
int max = arr[0];
for (int i = 1; i < arr.length; i++) {
if (arr[i] > max) {
max = arr[i];
}
}
//判断有效的位数
int maxLength = (max + "").length();

//声明桶的长度,无法确认桶中的元素
int[][] buckets = new int[10][arr.length];

//桶的长度为数组大小、那么每一个桶中存放了几个有效的需要用一个变量出接收
int[] bucketCount = new int[arr.length];

for (int k = 1,n = 1;k <= maxLength;k++,n *= 10) {

for (int i = 0;i < arr.length;i++) {

int bucketIndex = arr[i] / n % 10;
//存储桶组中
buckets[bucketIndex][bucketCount[bucketIndex]] = arr[i];
bucketCount[bucketIndex]++;
}

//把每个数放回到原数组中
int index = 0;
for(int i = 0;i < buckets.length;i++) {
if (bucketCount[i] == 0) {
continue;
}

for (int j = 0;j < bucketCount[i];j++) {
arr[index++] = buckets[i][j];
//index++;
}

bucketCount[i] = 0;

}
System.out.println("第" + k + "轮排序后:" + Arrays.toString(arr));
}


}
}

结尾

  十大排序算法总结下来,每个算法都有自己独特的思路。有一些排序算法又是其他算法的提升与补充。