排序算法
衡量排序算法的几个概念
衡量排序算法的几个概念,如下:
执行效率(时间复杂度)
内存消耗(空间复杂度)
原地排序/Sorted in place (空间复杂度为 O(1) )
原地排序算法,就是特指空间复杂度是 O(1) 的排序算法。(即,在排序时,不产生新的数列,只在原数列对数列中的元素进行交换位置)
属于原地排序算法有:冒泡排序、选择排序、插入排序(希尔排序)、快排
稳定性
简单的说,两个值相等的元素在排序后前后顺序不变,称为稳定;反之(即,不一定不变),称为不稳定。
一、冒泡排序
每一次遍历,较大数都要向右(or 向左)移动(像冒泡效果)。
时间复杂度为:
- 最优时间复杂度 -- O(n),即只遍历一遍
- 最坏时间复杂度 -- O(n^2),跑完所有循环
稳定性:稳定
public static void main(String[] args) {
int[] disorderArray = {54, 26, 93, 17, 77, 31, 44, 55, 20}; // 常规打乱
int[] disorderArray2 = {17, 20, 26, 31, 44, 54, 55, 77, 93}; // 正序
int[] disorderArray3 = {93, 77, 55, 54, 44, 31, 26, 20, 17}; // 倒序
bubbleSort(disorderArray);
bubbleSort(disorderArray2);
bubbleSort(disorderArray3);
}
public static void bubbleSort(int[] disorderArray) {
int k = 0; // 循环的次数
for (int i = disorderArray.length - 1; i > 0; i--) {
// i 为下面循环的终点。下面每次循环后,最大的数就放在最后了,所以终点减1
boolean flag = true;
for (int j = 0; j < i; j++) { // 使用小于号,所以 j 只到 i - 1, 而最后的 j + 1 刚好是 i
k++;
if (disorderArray[j] > disorderArray[j + 1]) {
// 每一次将较大的数后移,即冒泡效果
int temp = disorderArray[j];
disorderArray[j] = disorderArray[j + 1];
disorderArray[j + 1] = temp;
flag = false;
}
}
if (flag) { // 优化排序
// 内层没有发生交换即这一次的循环是顺的,所以整个已经顺了,则退出循环;
break;
}
}
System.out.println("循环的次数:" + k);
}
二、选择排序
移动次数最少。
选择一个最小(or 最大)数与首位(or 末位)交换位置,放好的数在下一次内存层循环时剔除。
时间复杂度:
- 最优时间复杂度-- O(n^2)
- 最坏时间复杂度-- O(n^2)
稳定性:不稳定
public static void main(String[] args) {
int[] disorderArray = {54, 26, 93, 17, 77, 31, 44, 55, 20}; // 常规打乱
int[] disorderArray2 = {17, 20, 26, 31, 44, 54, 55, 77, 93}; // 正序
int[] disorderArray3 = {93, 77, 55, 54, 44, 31, 26, 20, 17}; // 倒序
selectionSort(disorderArray);
selectionSort(disorderArray2);
selectionSort(disorderArray3);
}
public static void selectionSort(int[] disorderArray) {
// 其中外层循环中的 i 表示:待排数列的 首位(or 末位)
int k = 0;
int n = disorderArray.length;
for (int i = 0; i < n - 1; i++) { // 需要进行 n - 1 次交换位置
int minNumIndex = i;
// 每完成内层循环,都找出当前情况下最小数所在的位置
for (int j = i + 1; j < n; j++) { // j 的最大值为 n - 1
k++;
if (disorderArray[j] < disorderArray[minNumIndex]) {
minNumIndex = j;
}
}
// 将最小数所在的位置放置在第 i 位
if (minNumIndex != i) {
int temp = disorderArray[i];
disorderArray[i] = disorderArray[minNumIndex];
disorderArray[minNumIndex] = temp;
}
}
System.out.println("循环的次数:" + k);
}
三、插入排序
选择一个数,插入到有序数列中
思路步骤:
- 先将第一个位置的元素是为有序序列,
- 从第二个位置(即下标为1的元素)开始取出(即,待插入的数),
- 向前比较插入有序序列里,此时有序序列的长度加一。
其中
边界条件:待插入的数,比较完有序序列的所有数后,仍然没找到位置,那说明它的位置应该是放在首位
时间复杂度:和冒泡一样
- 最优时间复杂度:O(n)
- 最坏时间复杂度:O(n2)
稳定性:稳定
public static void main(String[] args) {
int[] disorderArray = {54, 26, 93, 17, 77, 31, 44, 55, 20}; // 常规打乱
int[] disorderArray2 = {17, 20, 26, 31, 44, 54, 55, 77, 93}; // 正序
int[] disorderArray3 = {93, 77, 55, 54, 44, 31, 26, 20, 17}; // 倒序
insertSort(disorderArray);
insertSort(disorderArray2);
insertSort(disorderArray3);
}
public static void insertSort(int[] disorderArray) {
int k = 0;
for (int i = 1; i < disorderArray.length; i++) {
int temp = disorderArray[i]; // 待插入的数为:disorderArray[i],并将其取出,取出后该位置可用,后续较大的数可往此方向移动 或 为插入使用
for (int j = i - 1; j >= 0; j--) { // 往前插入,往前算,所以使用 --
k++;
if (temp < disorderArray[j]) {
disorderArray[j + 1] = disorderArray[j];
if (j == 0) { // 已经没得比较了,就把它放在首位。(边界条件)
disorderArray[0] = temp;
}
} else {
disorderArray[j + 1] = temp;
break;
}
}
}
System.out.println("循环的次数:" + k);
}
四、快速排序
这里举例是从小到大排序。快排使用的递归实现,体现分治思想。
思路步骤:
-
(分治思想的执行方式之一)选择一个基准(一般选择数列中的第一元素),然后遍历数列,
将不小于基准的数放右边(称为--较大数列),小于基准的放左边(较小数列),基准放中间。
-
然后再分别对 较大数列 和 较小数列 执行上面一步(即,递归的方式)。
-
递归结束的条件:数列只有1个数或0个数时,即数列 的 开始位置start >= 结束位置end
问题1(步骤2的细节):如何切出 较大(小)数列---从原数列开始位置start(or 结束位置end) 到 基准位置来切
问题2(步骤1的细节):如何将较小数抛到左边,较大数抛到右边 --- 定义两个游标,轮流从数列的左右两边遍历数列
- 将最左边的数(即,数列的开始位置)取出,作为基准数。此时左边该位置可用。
- 从右边遍历,直到找到一个较小数抛到左边可用的位置。此时右边便有一个可用位置(找到就较小数就停止遍历)
- 接着从左边遍历,直到找到一个较小数抛到右边可用的位置。此时左边便有一个可用位置。(找到就较大数就停止遍历)
- 2 和 3 循环轮流,直到 左右两边的游标重合时,结束所有循环(2、3的遍历,以及2、3之间的循环轮流)
问题3(步骤1的细节):基准数放在哪个位置 --- 当两个游标重合时,该位置就是基准数的位置。
时间复杂度:
- 最优时间复杂度:O(nlogn)
- 最坏时间复杂度:O(n2)
稳定性:不稳定
public static void main(String[] args) {
int[] disorderArray = {54, 26, 93, 17, 77, 31, 44, 55, 20}; // 常规打乱
int[] disorderArray2 = {17, 20, 26, 31, 44, 54, 55, 77, 93}; // 正序
int[] disorderArray3 = {93, 77, 55, 54, 44, 31, 26, 20, 17}; // 倒序
quickSort(disorderArray, 0, disorderArray.length-1);
quickSort(disorderArray2, 0, disorderArray.length-1);
quickSort(disorderArray3, 0, disorderArray.length-1);
}
public static void quickSort(int[] disorderArray, int start, int end) {
if (start >= end) { // 步骤3
return;
}
int low = start; // 问题2
int high = end; // 问题2
int mid = disorderArray[start]; // 问题2.1
while (low < high) { // 问题2.4
while (low < high && disorderArray[high] >= mid) { // 问题2.2
times++;
high--;
}
disorderArray[low] = disorderArray[high]; // 问题2.2
while (low < high && disorderArray[low] < mid) { // 问题2.3
times++;
low++;
}
disorderArray[high] = disorderArray[low]; // 问题2.3
}
disorderArray[low] = mid; // 问题3
// 步骤2、问题1
quickSort(disorderArray, start, low - 1);
quickSort(disorderArray, low + 1, end);
}
合理选择分区点
如果数据原来就是有序的或者接近有序的,每次分区点都选择最后一个数据,那快速排序算法就会变得非常糟糕,时间复杂度就会退化为 O(n2)。 最理想的分区点是:被分区点分开的两个分区中,数据的数量差不多。
三数取中法
从区间的首、尾、中间,分别取出一个数,然后对比大小,取这 3 个数的中间值作为分区点。这样每间隔某个固定的长度,取数据出来比较,将中间值作为分区点的分区算法,肯定要比单纯取某一个数据更好。 但是,如果要排序的数组比较大,那“三数取中”可能就不够了,可能要“五数取中”或者“十数取中”。
随机法
每次从要排序的区间中,随机选择一个元素作为分区点。这种方法并不能保证每次分区点都选的比较好,但是从概率的角度来看,也不大可能会出现每次分区点都选的很差的情况,所以平均情况下,这样选的分区点是比较好的。时间复杂度退化为最糟糕的 O(n2) 的情况,出现的可能性不大。
警惕堆栈溢出
快排是用递归实现的,递归要警惕堆栈溢出。为了避免快速排序里,递归过深而堆栈过小,导致堆栈溢出,我们有两种解决办法:
第一种是限制递归深度。一旦递归过深,超过了我们事先设定的阈值,就停止递归。 第二种是通过在堆上模拟实现一个函数调用栈,手动模拟递归压栈、出栈的过程,这样就没有了系统栈大小的限制。
五、归并排序
也是分治的思想(二分法来分)。将数列递归分成最小数列(一个元素的数列,视为有序数列),再有序两两合并成有序数列(递归到将所有数列合成为一个)
思路步骤: 先分后合
-
先将数列分为两个子数列,再将子数列分为两个子子数列(即,递归),直到细分后的数列元素小于等于1个。
1-1. 方式一、将拆分的数列放在新的数列里
1-2.方式二:用游标来表示查分,(没有新的数列产生)---- 代码采用次方式
-
将切分后的数列排序 并且向上合并。(关键点:最小的元素为1个,视为有序。合并操作要保证 合并后的数列也要有序)
时间复杂度:
- 最优时间复杂度:O(nlogn)
- 最坏时间复杂度:O(nlogn)
稳定性:稳定
public static void main(String[] args) {
int[] disorderArray = {54, 26, 93, 17, 77, 31, 44, 55, 20}; // 常规打乱
int[] disorderArray2 = {17, 20, 26, 31, 44, 54, 55, 77, 93}; // 正序
int[] disorderArray3 = {93, 77, 55, 54, 44, 31, 26, 20, 17}; // 倒序
mergeSort(disorderArray, 0, disorderArray.length-1);
mergeSort(disorderArray2, 0, disorderArray.length-1);
mergeSort(disorderArray3, 0, disorderArray.length-1);
}
public static void mergeSort(int[] disorderArray, int leftIndex, int rightIndex) {
int mid = leftIndex + (rightIndex - leftIndex) / 2;
if (leftIndex < rightIndex) { //
mergeSort(disorderArray, leftIndex, mid); // 步骤1
mergeSort(disorderArray, mid + 1, rightIndex); // 步骤1
merge(disorderArray, leftIndex, mid, rightIndex); // 步骤2
}
}
public static void merge(int[] disorderArray, int leftIndex, int mid, int rightIndex) {
int i = leftIndex;
int j = mid + 1;
int k = 0;
int[] tmp = new int[rightIndex - leftIndex + 1];
while (i <= mid && j <= rightIndex) {
tmp[k++] = disorderArray[i] < disorderArray[j] ? disorderArray[i++] : disorderArray[j++];
}
while (i <= mid) {
tmp[k++] = disorderArray[i++];
}
while (j <= rightIndex) {
tmp[k++] = disorderArray[j++];
}
// 将tmp中的数据拷贝到a[low...high]
for (int x = 0; x < tmp.length; ++x) {
disorderArray[x + leftIndex] = tmp[x];
}
}
补充:
合并后的数列是新的数列。也可将新数列拷贝到原数列上,但本质还是产生了新数列。所以空间复杂度高(是 O(n))(即,不是原地排序)
总结:常见排序算法效率比较
排序算法 | 平均情况 | 最好情况 | 最差情况 | 稳定性 | 备注 |
---|---|---|---|---|---|
冒泡排序 | O(n^2) | O(n) | O(n^2) | 稳定 | 适合数据量小 |
选择排序 | O(n^2) | O(n^2) | O(n^2) | 不稳定 | 适合数据量小 |
插入排序 | O(n^2) | O(n) | O(n^2) | 稳定 | 适合数列原时序较好的 |
希尔排序 | 不稳定 | 特殊的插入排序,与步长有关 | |||
快速排序 | O(nlogn) | O(nlogn) | O(n^2) | 不稳定 | 适合数据量大的 |
归并排序 | O(nlogn) | O(nlogn) | O(nlogn) | 稳定 | 适合数据量大的 |