冒泡排序
public static int[] sortArray(int[] arr) {
int temp;
for (int i = 0; i < arr.length - 1; i++) {
boolean flag = true; // 优化,标记是否有数据交换
for (int j = 0; j < arr.length - 1 - i; j++) {
if (arr[j] > arr[j + 1]) {
temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
flag = false; // 数据交换了
}
}
if (flag) {
break; // 如果没有数据交换说明数组已经是有序的了,提前退出
}
}
return arr;
}
- 思路
- 对长度为n的数组遍历n次,每次遍历依次比较相邻的两个元素,如果大小关系不符合要求,就交换它俩的位置。这样每次遍历都能保证至少一个元素移动到了它应该在的位置。
-
注意:以从小到大排序为例,第一次循环后数组末尾位置的元素就是最大值,第二次倒数第二位的元素也是第二大的,依次类推...所以代码中内层循环的界限是在一直缩小的。
-
优化:用一个flag标记当次循环中是否有元素进行了交换。如果没有任何元素发生了交换,则证明数组已经是有序的了,可以直接跳出循环,结束排序。
- 分析
-
稳定性:相邻元素大小相等时不做交换,所以不会改变大小相等的元素的顺序,是稳定的排序算法。
-
空间复杂度:O(1),只在交换数据时需要一块常数级的临时空间,是原地排序算法。
-
时间复杂度:最好的情况,数组本来就是有序的,时间复杂度为O(n)。最坏的情况,数组是倒序的,需要n次冒泡,时间复杂度为O(n²)。经过不严格的简单推导,平均时间复杂度还是O(n²)。
插入排序
public static int[] sortArray(int[] arr) {
for (int i = 1; i < arr.length; i++) {
int value = arr[i]; // 记录此次循环要插入的元素
int j = i - 1;
for (; j >= 0; j--) {
if (arr[j] > value) {
arr[j + 1] = arr[j]; // 如果已排序的元素没有待插入元素大,就向后挪一位
} else {
break; // 找到插入位置,结束循环
}
}
arr[j + 1] = value; // 将元素插入找到的位置
}
return arr;
}
- 思路
- 将数组分为两个区间,已排序和未排序。已排序区间最开始就有一个元素,也就是数组的第一个元素。然后取未排序区间的元素,在已排序区间中找到自己的位置,插进去。直到未排序区间为空。
- 代码中,以从小到大排序为例,下标i前的元素为已排序元素,i及i之后的元素为未排序元素。每次取一个未排序的元素,倒着去比较已排序元素,如果发现比某个元素小,就插在这个元素之前。
- 分析
-
稳定性:比较插入时,如果元素相等,可以将未排序的元素插在已排序的元素后边,可以做到保持原有顺序不变,是稳定的排序算法。
-
空间复杂度:O(1),不需要额外的存储空间,是原地排序算法。
-
时间复杂度:最好的情况,数组本来就是有序的,如果从尾到头遍历有序部分,查找插入位置,每次循环只需要比较一次就能确定,时间复杂度为O(n)。最坏的情况,数组是倒序的,每次查找都要把元素插在有序部分的开头,需要多次移动元素,时间复杂度为O(n²)。往数组中插入一个数据的平均时间复杂度是O(n),我们相当于循环n次执行插入操作,所以平均时间复杂度为O(n²)。
冒泡排序 vs 插入排序
- 它们的稳定性和时间、空间复杂度都相同,但如果要追求极致,可以发现冒泡排序在交换元素时需要3个赋值操作,而插入排序只需要一个。当数据量很大的时候这点微小的差距累积起来就可以让插入排序胜过冒泡排序。
- 插入排序的优化:参考 五分钟学会一个高难度算法:希尔排序
选择排序
- 思路
- 类似插入排序,以从小到大排序为例,每次遍历找到未排序部分的最小值放在已排序部分的末尾。
- 分析
- 稳定性:具体实现时会将最小值元素与要放在已排序部分末尾那个位置的元素交换位置。经过多次交换后会破坏稳定性。
- 空间复杂度时间复杂度与冒泡排序和插入排序是一样的。
网友评论