排序算法上篇
# 排序算法说明
# 1. 排序的定义
对一序列对象根据某个关键字进行排列。
# 2. 术语说明
- 稳定:如果a原本在b前面,而a=b,排序之后a仍然在b的前面;
- 不稳定:如果a原本在b的前面,而a=b,排序之后a可能会出现在b的后面;
- 内排序:所有排序操作都在内存中完成;
- 外排序:由于数据太大,因此把数据放在磁盘中,而排序通过磁盘和内存的数据传输才能进行;
常见的排序算法分类:

# 3. 稳定性的意义
有多个排序关键字的时候,稳定排序可以让前一个关键字排序的结果服务于后一个关键字排序中数值相等的那些数。
举个栗子,给同学们的考试成绩排序,
# 冒泡排序
# 1. 基本思想
冒泡排序(Bubble Sort)是一种简单的排序算法。它重复地走访过要排序的数列,一次比较两个元素,如果他们的顺序错误就把他们交换过来。走访数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。这个算法的名字由来是因为越小的元素会经由交换慢慢“浮”到数列的顶端。
# 2. 算法描述
- 比较相邻的元素。如果第一个比第二个大,就交换他们两个;
- 对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对,这样在最后的元素应该会是最大的数;
- 针对所有的元素重复以上的步骤,除了最后一个;
- 持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较;

# 3. 代码实现
public static int[] bubbleSort(int[] arr){
if (arr.length == 0) return arr;
// 排到最后一轮实际上只有一个元素,不需要排序了,所以一共循环n-1轮
for (int i = 0; i < arr.length-1; i++) {
// 每轮排序完会把最大的元素放到最后,所以最后一个元素不需要再比较
for (int j = 0; j < arr.length-1-i; j++) {
// 比较相邻的元素,如果第一个比第二个大,就交换他们两个
if (arr[j+1]<arr[j]) {
int temp = arr[j+1];
arr[j+1] = arr[j];
arr[j] = temp;
}
}
}
return arr;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 4. 优化思路
因为排序的过程中,各元素不断接近自己的位置,如果一趟比较下来没有进行过交换,就说明序列有序,因此要在排序过程中设置一个标志flag判断元素是否进行过交换。从而减少不必要的比较。
public static void bubbleSort(int[] arr){
if (arr.length == 0) return;
// 标识变量,表示是否进行过交换
boolean flag = false;
// 排到最后一轮实际上只有一个元素,不需要排序了,所以一共循环n-1轮
for (int i = 0; i < arr.length-1; i++) {
// 每次遍历标志位都要先置为false,才能判断后面的元素是否发生了交换
flag = false;
// 每轮排序完会把最大的元素放到最后,所以最后一个元素不需要再比较
for (int j = 0; j < arr.length-1-i; j++) {
// 比较相邻的元素,如果第一个比第二个大,就交换他们两个
if (arr[j+1]<arr[j]) {
flag = true;
int temp = arr[j+1];
arr[j+1] = arr[j];
arr[j] = temp;
}
}
// 判断标志位是否为false,如果为false,说明后面的元素已经有序,就直接return
if (!flag) break;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 5. 思考总结
冒泡排序是最容易实现的排序, 最坏的情况是每次都需要交换, 共需遍历并交换将近n²/2次, 时间复杂度为O(n²). 最佳的情况是内循环遍历一次后发现排序是对的, 因此退出循环, 时间复杂度为O(n). 平均来讲, 时间复杂度为O(n²). 由于冒泡排序中只有缓存的temp变量需要内存空间, 因此空间复杂度为常量O(1).
由于冒泡排序只在相邻元素大小不符合要求时才调换他们的位置, 它并不改变相同元素之间的相对顺序, 因此它是稳定的排序算法。
| 平均时间复杂度 | 最好情况 | 最坏情况 | 空间复杂度 |
|---|---|---|---|
| O(n²) | O(n) | O(n²) | O(1) |
# 选择排序
# 1. 基本思想
选择排序(Selection-sort)是一种简单直观的排序算法。它的工作原理:首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置,然后,再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。以此类推,直到所有元素均排序完毕。
# 2. 算法描述
n个记录的直接选择排序可经过n-1趟直接选择排序得到有序结果。具体算法描述如下:
- 初始状态:无序区为R[1..n],有序区为空;
- 第i趟排序(i=1,2,3…n-1)开始时,当前有序区和无序区分别为R[1..i-1]和R(i..n)。该趟排序从当前无序区中-选出关键字最小的记录R[k],将它与无序区的第1个记录R交换,使R[1..i]和R[i+1..n)分别变为记录个数增加1个的新有序区和记录个数减少1个的新无序区;
- n-1趟结束,数组有序化了

# 3. 代码实现
public static void selectionSort(int[] arr){
if (arr.length == 0) return;
for (int i = 0; i < arr.length-1; i++) {
int minIndex = i; // 最小值的索引,假定为i
int min = arr[i];
for (int j = i + 1; j < arr.length; j++) {
if (min > arr[j]) { // 说明假定的最小值,并不是最小
min = arr[j]; // 重置min
minIndex = j; // 重置minIndex
}
}
// 将最小值,放在arr[0], 即交换
if (minIndex != i) {
arr[minIndex] = arr[i];
arr[i] = min;
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 4. 思考总结
从简单选择排序的过程来看,它最大的特点就是交换移动数据次数相当少,这样也就节约了相应的时间。分析它的时间复杂度发现,无论最好最差的情况,其比较次 数都是一样的多,时间复杂度总是为:O(n^2^)。
| 平均时间复杂度 | 最好情况 | 最坏情况 | 空间复杂度 |
|---|---|---|---|
| O(n²) | O(n²) | O(n²) | O(1) |
应该说,尽管与冒泡排序同为O(n^2^),但简单选择排序的性能上还是要略优于冒泡排序。
# 插入排序
# 1. 基本思想
插入排序(Insertion-Sort)的算法描述是一种简单直观的排序算法。它的工作原理是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。插入排序在实现上,通常采用in-place排序(即只需用到O(1)的额外空间的排序),因而在从后向前扫描过程中,需要反复把已排序元素逐步向后挪位,为最新元素提供插入空间。
# 2. 算法描述
一般来说,插入排序都采用in-place在数组上实现。具体算法描述如下:
- 从第一个元素开始,该元素可以认为已经被排序;
- 取出下一个元素,在已经排序的元素序列中从后向前扫描;
- 如果该元素(已排序)大于新元素,将该元素移到下一位置;
- 重复步骤3,直到找到已排序的元素小于或者等于新元素的位置;
- 将新元素插入到该位置后;
- 重复步骤2~5;

# 3. 代码实现
public static void insertSort(int[] arr) {
if (arr.length == 0) return;
// 定义待插入的数
int insertIndex, insertVal;
for (int i = 1; i < arr.length; i++) {
insertIndex = i-1;
insertVal = arr[i];
// 给insertVal 找到插入的位置
while (insertIndex >= 0 && arr[insertIndex] > insertVal) {
arr[insertIndex + 1] = arr[insertIndex];
insertIndex--;
}
// 当退出while循环时,说明插入的位置找到, insertIndex + 1
// 判断是否需要赋值
if (insertIndex + 1 != i) {
arr[insertIndex + 1] = insertVal;
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 4. 思考总结
| 平均时间复杂度 | 最好情况 | 最坏情况 | 空间复杂度 |
|---|---|---|---|
| O(n²) | O(n) | O(n²) | O(1) |
同样的O(n^2^)时间复杂度,直接插入排序法比冒泡和简单选择排序的性能要好一些。
# 希尔排序
# 1. 基本思想
希尔排序也是一种插入排序,它是简单插入排序经过改进之后的一个更高效的版本,也称为缩小增量排序。希尔排序是把记录按下标的一定增量分组,对每组使用直接插入排序算法排序;随着增量逐渐减少,每组包含的关键词越来越多,当增量减至1时,整个文件恰被分成一组,算法便终止。
# 2. 算法描述
我们来看下希尔排序的基本步骤,在此我们选择增量gap=length/2,缩小增量继续以gap = gap/2的方式,这种增量选择我们可以用一个序列来表示,{n/2,(n/2)/2...1},称为增量序列。希尔排序的增量序列的选择与证明是个数学难题,我们选择的这个增量序列是比较常用的,也是希尔建议的增量,称为希尔增量,但其实这个增量序列不是最优的。此处我们做示例使用希尔增量。 先将整个待排序的记录序列分割成为若干子序列分别进行直接插入排序,具体算法描述:
- 选择一个增量序列t1,t2,…,tk,其中ti>tj,tk=1;
- 按增量序列个数k,对序列进行k 趟排序;
- 每趟排序,根据对应的增量ti,将待排序列分割成若干长度为m 的子序列,分别对各子表进行直接插入排序。仅增量因子为1 时,整个序列作为一个表来处理,表长度即为整个序列的长度。

# 3. 代码实现
public static void shellSort(int[] arr){
// 增量gap, 并逐步的缩小增量
for (int gap = arr.length/2; gap > 0; gap /= 2) {
// 从第gap个元素,逐个对其所在的组进行直接插入排序
for (int i = gap; i < arr.length; i++) {
int j = i;
int temp = arr[j];
if (arr[j] < arr[j - gap]) {
while (j - gap >= 0 && temp < arr[j - gap]) {
//移动
arr[j] = arr[j-gap];
j -= gap;
}
//当退出while后,就给temp找到插入的位置
arr[j] = temp;
}
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 4. 思考总结
希尔排序的时间复杂度与增量序列的选取有关,需要注意的是,増量序列的最后一个増量值必须等于1才行。另外由于记录是跳跃式的移动,希尔排序并不是一种稳定的排序算法。
| 平均时间复杂度 | 最好情况 | 最坏情况 | 空间复杂度 |
|---|---|---|---|
| O(nlogn) | O(nlog2n) | O(nlog2n) | O(1) |
# 快速排序
# 1. 基本思想
快速排序(Quicksort)是对冒泡排序的一种改进。基本思想是:通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列。
# 2. 算法描述
快速排序使用分治法来把一个串(list)分为两个子串(sub-lists)。具体算法描述如下:
- 从数列中挑出一个元素,称为 基准;
- 重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。在这个分区退出之后,该基准就处于数列的中间位置。这个称为分区操作;
- 递归地把小于基准值元素的子数列和大于基准值元素的子数列排序;

# 3. 代码实现
public static void quickSort(int[] arr, int left, int right) {
// 左下标一定小于右下标,否则就越界了
if (left < right) {
// 对数组进行分割,取出下次分割的基准标号
int base = division(arr, left, right);
// 对“基准标号“左侧的一组数值进行递归的切割,以至于将这些数值完整的排序
quickSort(arr, left, base - 1);
// 对“基准标号“右侧的一组数值进行递归的切割,以至于将这些数值完整的排序
quickSort(arr, base + 1, right);
}
}
private static int division(int[] arr, int left, int right) {
// 以最左边的数(left)为基准
int base = arr[left];
while (left < right) {
// 从序列右端开始,向左遍历,直到找到小于base的数
while (left < right && arr[right] >= base) {
right--;
}
// 找到了比base小的元素,将这个元素放到最左边的位置
arr[left] = arr[right];
// 从序列左端开始,向右遍历,直到找到大于base的数
while (left < right && arr[left] <= base) {
left++;
}
// 找到了比base大的元素,将这个元素放到最右边的位置
arr[right] = arr[left];
}
// 最后将base放到left位置。此时,left位置的左侧数值应该都比left小;
// 而left位置的右侧数值应该都比left大。
arr[left] = base;
return left;
}
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
# 4. 思考总结
由于关键字的比较和交换是跳跃进行的,因此,快速排序是一种不稳定的排序方法。
| 平均时间复杂度 | 最好情况 | 最坏情况 | 空间复杂度 |
|---|---|---|---|
| O(nlogn) | O(nlogn) | O(n²) | O(1) |