前言:本文是在作者学习排序算法时所撰写,主要分享一些学习的经验以及一些细节。
排序算法稳定性、时间空间复杂度透析
1.选择排序
问题:选择排序为什么是不稳定的排序,能不能在不改变时间和空间复杂度的情况将其优化为稳定的排序算法。
1.1回顾选择排序原理
选择排序分为升序和降序排序,升序:让第一个元素与后面的每一个元素比较,如果发现有比自己小的就交换位置(其实优化后的排序只是更新最小值的位置并没了立即交换位置,而是等待第一轮循环结束后才交换位置),然后继续往下比较,第一轮比较完毕后,最小的元素就被交换到了index = 0的位置,下一轮循环就从第二个元素开始比较,以此类推。降序:每次循环找出最大的那个元素,让它与第一个元素交换位置,第一轮比较完毕后,最大的元素就被交换到了index = 0的位置,以此类推。
备注:无论是将最小值放到最后一个位置来达到降序排序还是将最大值放到最后一个位置来达到升序排序结果都是一样,取决于个人的编码习惯。
1.2选择排序稳定性透析
先来看一个例子,对【5,2,3,5,1,2,1】使用选择排序进行升序排序。代码示例如下!
public static void selectionSortAsc(int[] arr) {
System.out.println("原数组:" + Arrays.toString(arr));
System.out.println("-------------------------------");
for (int i = 0; i < arr.length - 1; i++) {
int minIndex = i;
for (int j = i + 1; j < arr.length; j++) {
if (arr[minIndex] > arr[j]) {
minIndex = j;
}
}
//如果交换的位置是自己 就不交换
if (minIndex == i) continue;
int temp = arr[minIndex];
arr[minIndex] = arr[i];
arr[i] = temp;
System.out.println(Arrays.toString(arr));
}
}
在排序过程中,第一个5和第二个5的相对位置发生了变化,第一个2和第二个2的相对位置也发生了变化,排序完成后,两个5的相对位置又和排序前保持一致相当于恢复了稳定,但是两个2的相对位置较排序前依然发生了改变,依然不稳定。
问题:既然两个5在排序过程中失去了平衡,但最终还是恢复了平衡,会不会存在一种情况,在排序过程中有许多的元素失去了平衡,但是排序结束后又都恢复了平衡呢?
我的答案是肯的,肯定会存在这种情况,但是一个排序算法的稳定性是要对每一组排序数据都稳定,也就是说只要存在一组排序数据不稳定,那么这个算法就是不稳定的排序算法。
1.3选择排序写法2
这种写法效率并没有上述写法搞,我想表达的内容是无论你采用哪种写法,对于选择排序来说,稳定性都不会发生变化,依然不稳定。
public static void selectionSort(int[] arr) {
System.out.println(Arrays.toString(arr));
System.out.println("------------------------");
for (int i = 0; i < arr.length - 1; i++) {
for (int j = i + 1; j < arr.length; j++) {
if (arr[j] < arr[i]) {
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
}
System.out.println(Arrays.toString(arr));
}
}
选择排序写法2过程以及稳定性分析,从图中可以看出,排序完成后,两个2和两个5的相对位置发生了改变,所以说选择排序是一个不稳定的排序算法。
在实际应用中,笔者建议使用第一种写法,因为写法2的交换次数要比写法1的交换次数多,效率就没有写法一的高。
1.4选择排序时间复杂度分析
时间复杂度分为最好、最坏和平均时间复杂度三种情况,那么什么情况下是最好,什么情况下是最坏呢?
最好情况:对一个已经有序的数组进行排序,而且排序顺序和该数组的排序顺序一致,例如对【0,1,2,3,4,5,6,7,8,9】进行升序排序,这个就是最好情况,从写法1的代码中就可以看出,就算是有序的数组,使用选择排序对数组进行排序的时间复杂度仍然是O(n^2),比较次数不会减少,但是交换次数减少到了0次,但是时间复杂度依旧没变。
最坏情况:对一个已经有序的数组进行排序,但是排序顺序和该数组的排序顺序相反,例如对【9,8,7,6,5,4,3,2,1,0】进行升序排序,这个就是最坏情况,和最好的情况是相反的,比较次数依旧不会减少,交换次数增加了,最终的时间复杂度还是O(n^2)。
平均时间复杂度为O(n^2)。
所以选择排序的最好、最坏和平均时间复杂度都是O(n^2)。
1.5选择排序的空间复杂度
选择排序的空间复杂度为O(1),属于原地算法In-place Algorithm。
In-place Algorithm:
不依赖额外的资源或者依赖少数的额外资源,仅依靠输出来覆盖输入。
空间复杂度为0(1)的都可以认为是原地算法。
2.冒泡排序
3.插入排序
插入排序,适用待排序数组部分有序或者相对有序时。
原理:首先默认数组第一个值为有序,然后从数组第二个值开始一直与前面的值比较,如果上一个值大于当前值就将上一个值往后移动一个位置,直到上一个值小于当前值,再把当前值移动至上一个值后面的位置,逐渐构建一个有序的数组头。
插入排序动态过程图
/**
需要根据上述原理 手写出代码 注意不要借助IDE工具 建议先使用文本编辑器编写 最终达到用笔在纸上编写 注意代码排版
此处以ACM模式展示事例代码
*/
import java.util.Scanner;
// 注意类名必须为 Main, 不要有任何 package xxx 信息
public class Main {
public static void main(String[] args) {
//Scanner in = new Scanner(System.in);
int[] arr = {3,44,38,5,47,15,36,26,27,2,46,4,19,50,48};
for (int i = 1; i < arr.length; i++) {
int curval = arr[i];
int j;
for (j = i; j > 0 && arr[j-1] > curval; j--) {
arr[j] = arr[j-1];
}
arr[j] = curval;
}
for (int i = 0; i < arr.length; i++) {
System.out.print(arr[i] + "\t");
}
}
}
4.堆排序
5.归并排序
6.快速排序
6.1快速排序原理
从待排序序列中随机选择一个轴点,为了方便编程,一般选择数组的第一个元素作为节点,然后遍历数组,将大于节点的元素放到轴点的右边,将小于等于轴点的元素放到轴点的左边,然后再让轴点左边和右边的序列执行以上操作,直到序列只剩一个元素。
声明两个指针begin、end和一个扫描方向标识flag,先从右边向左边扫描数组元素,碰到小于等于轴点的元素就让它覆盖begin位置的元素,begin往右移动一个位置,然后改变扫描的方向flag。从左边往右边扫描,碰大于轴点的元素就让它覆盖end位置的元素,end往左移动一个位置,然后改变扫描的方向flag从右向左扫描。直到begin等于end,就不在扫描,最后把拷贝出来的轴点元素放到begin最后指向的位置,并返回begin,继续执行递归操作。为了简化操作,图中没有画出begin和end指针,flag方向默认为true,表示从右向左扫描。
从上图可以看出,经过一轮排序后,数组元素被分轴点元素分为左右两部分,左边的元素全部小于等于轴点元素,右边的元素全部大于轴点元素。然后分别将轴点左右两边的元素一直进行以上操作,直到轴点两边只剩一个元素或者没有元素就结束排序。
6.2快速排序代码实现
static int[] array;
public static void main(String[] args) {
array = new int[]{9, 8, 7, 6, 5, 4, 3, 2, 1};
sort(0, array.length);
System.out.println(Arrays.toString(array));
}
public static void sort(int begin, int end) {
if (end - begin < 2) return;
//先将 节点元素拷贝 一份
int pivot = pivot(begin, end);
sort(begin, pivot);
sort(pivot + 1, end);
}
/**
* 将数组元素以轴点为界一分为二
* 左边的元素小于等于轴点元素
* 右边的元素大于轴点元素
* @param begin
* @param end
* @return 返回操作后原轴点元素的索引
*/
public static int pivot(int begin, int end) {
//优化一下 随机产生 轴点元素 并与 begin 位置元素 交换位置
int index = begin + (int) (Math.random() * (end - begin));
//先将轴点元素拷贝一份
int pivot = array[index];
//让begin位置元素覆盖 随机产生的轴点元素即可
//循环结束后 轴点元素会覆盖begin位置的元素
array[index] = array[begin];
//先将轴点元素拷贝一份
//int pivot = array[begin];
//先让end指针有指向
end--;
//从右往左扫描数组元素
boolean flag = true;
while (begin < end) {
if (flag) {
//从右边扫描
if (array[end] > pivot) {
end--;
}else {
array[begin++] = array[end];
//begin++;
flag = !flag;
}
}else {
//从左边扫描
if (array[begin] < pivot) {
begin++;
}else {
array[end--] = array[begin];
//end--;
flag = !flag;
}
}
}
array[begin] = pivot;
return begin;
}
6.3快速排序稳定性分析
快速排序是不稳定的排序,这是快速排序原理决定的,例如对下面数组进行排序:【2,3,2,4,4,2,0,1】
6.4快速排序的小细节
//从右边扫描
if (array[end] > pivot) {
end--;
}else {
array[begin++] = array[end];
//begin++;
flag = !flag;
}
//从左边扫描
if (array[begin] < pivot) {
begin++;
}else {
array[end--] = array[begin];
//end--;
flag = !flag;
}