初识算法之选择排序、冒泡排序、插入排序、归并排序、随机快排
排序算法
排序算法关键指标
排序算法三个重要指标:时间复杂度、空间复杂度、稳定性
排序算法的稳定性
- 稳定性是指同样大小的样本再排序之后不会改变相对次序
- 对基础类型来说,稳定性毫无意义
- 对非基础类型来说,稳定性有重要意义
- 有些排序算法可以实现成稳定的,而有些排序算法无论如何都实现不成稳定的
给各位看官解释下
稳定:如果 a 原本在 b 前面,而 a=b,排序之后 a 仍然在 b 的前面。
不稳定:如果 a 原本在 b 的前面,而 a=b,排序之后 a 可能会出现在 b 的后面。
常见排序算法优劣
排序算法总结
1)不基于比较的排序,对样本数据有严格要求,不易改写
2)基于比较的排序,只要规定好两个样本怎么比大小就可以直接复用
3)基于比较的排序,时间复杂度的极限是O(NlogN)
4)时间复杂度O(NlogN)、额外空间复杂度低于O(N)、且稳定的基于比较的排序是不存在的。
5)为了绝对的速度选快排、为了省空间选堆排、为了稳定性选归并
排序递归的时间复杂度
在以下排序过程中,多次使用递归,形似以下形式的递归,满足master公式
int mid = L + ((R - L) >> 1);
int leftMax = process(arr, L, mid);
int rightMax = process(arr, mid + 1, R);
master公式:
T(N) = a * T(N/b) + O(N^d)(其中的a、b、d都是常数)
的递归函数,可以直接通过Master公式来确定时间复杂度
如果 log(b,a) < d,复杂度为O(N^d)
如果 log(b,a) > d,复杂度为O(N^log(b,a))
如果 log(b,a) == d,复杂度为O(N^d * logN)
其中a是递归调用次数,b为遍历数据规模的1/b,N^d为非递归其他步骤的时间复杂度
如上:
a=2
b=2(规模为之前的1/2,所以b=2)
d=0(除递归外,其他时间复杂度为O(1) = O(N^d),N ^d=1 ,d=0)
时间复杂度 T(N) = O(2*N/2+1)=O(n)
选择排序
选择排序(Select Sort) 是直观的排序,通过确定一个 Key 最大或最小值,再从带排序的的数中找出最大或最小的交换到对应位置。再选择次之。双重循环时间复杂度为 O(n^2)
算法描述
- 在一个长度为 N 的无序数组中,第一次遍历 n-1 个数找到最小的和第一个数交换。
- 第二次从下一个数开始遍历 n-2 个数,找到最小的数和第二个数交换。
- 重复以上操作直到第 n-1 次遍历最小的数和第 n-1 个数交换,排序完成。
过程演示
动图制作网站:VisAlgo
代码实现
public static void selectionSort(int[] arr) {
if (arr == null || arr.length < 2) {
return;
}
// 0~n-1
// 1~n-1
// 2~n-1
for (int i = 0; i < arr.length - 1; i++) { // i ~ N-1
// 最小值在哪个位置上 i~n-1
int minIndex = i;
for (int j = i + 1; j < arr.length; j++) { // i ~ N-1 上找最小值的下标
minIndex = arr[j] < arr[minIndex] ? j : minIndex;
}
swap(arr, i, minIndex);
}
}
public static void swap(int[] arr, int i, int j) {
int tmp = arr[i];
arr[i] = arr[j];
arr[j] = tmp;
}
冒泡排序
冒泡排序(Bubble Sort) 最为简单的一种排序,通过重复走完数组的所有元素,通过打擂台的方式两个两个比较,直到没有数可以交换的时候结束这个数,再到下个数,直到整个数组排好顺序。因一个个浮出所以叫冒泡排序。双重循环时间 O(n^2)
算法描述
- 比较相邻两个数据如果。第一个比第二个大,就交换两个数
- 对每一个相邻的数做同样1的工作,这样从开始一队到结尾一队在最后的数就是最大的数。
- 针对所有元素上面的操作,除了最后一个。
- 重复1~3步骤,知道顺序完成。
过程演示
动图制作网站:VisAlgo
代码实现
public static void bubbleSort(int[] arr) {
if (arr == null || arr.length < 2) {
return;
}
for (int e = arr.length - 1; e > 0; e--) { // 0 ~ e
for (int i = 0; i < e; i++) {
if (arr[i] > arr[i + 1]) {
swap(arr, i, i + 1);
}
}
}
}
// 交换arr的i和j位置上的值
public static void swap(int[] arr, int i, int j) {
arr[i] = arr[i] ^ arr[j];
arr[j] = arr[i] ^ arr[j];
arr[i] = arr[i] ^ arr[j];
}
上面交换的步骤,采用亦或运算,在算法面试之基本概念(一)中有具体讲解。
插入排序
每一步将一个待排序的数据插入到前面已经排好序的有序序列中,直到插完所有元素为止。
算法描述
- 从左往右,从第二个参数开始,依次向后的参数和前面的参数对比
- 假如按照从小到大排列,将参数插入到已经排好序的位置当中
- 递归一次,比较一次,时间复杂度O(n^2)
过程演示
动图制作网站:VisAlgo
代码实现
public static void insertionSort(int[] arr) {
if (arr == null || arr.length < 2) {
return;
}
// 0~0 有序的
// 0~i 想有序
for (int i = 1; i < arr.length; i++) { // 0 ~ i 做到有序
// arr[i]往前看,一直交换到合适的位置停止
// ...(<=) ? <- i
for (int j = i - 1; j >= 0 && arr[j] > arr[j + 1]; j--) {
swap(arr, j, j + 1);
}
}
}
// i和j是一个位置的话,会出错
public static void swap(int[] arr, int i, int j) {
arr[i] = arr[i] ^ arr[j];
arr[j] = arr[i] ^ arr[j];
arr[i] = arr[i] ^ arr[j];
}
归并排序
归并排序(MERGE-SORT)是利用归并的思想实现的排序方法,该算法采用经典的分治(divide-and-conquer)策略(分治法将问题分(divide)成一些小的问题然后递归求解,而治(conquer)的阶段则将分的阶段得到的各答案"修补"在一起,即分而治之)。
算法图解
可以看到这种结构很像一棵完全二叉树,本文的归并排序我们采用递归去实现(也可采用迭代的方式去实现)。分阶段可以理解为就是递归拆分子序列的过程,递归深度为logn。
我们再具体看下治的过程(合并过程)
算法演示
动图制作网站:VisAlgo
代码演示
// 递归方法实现
public static void mergeSort1(int[] arr) {
if (arr == null || arr.length < 2) {
return;
}
process(arr, 0, arr.length - 1);
}
public static void process(int[] arr, int L, int R) {
if (L == R) {
return;
}
int mid = L + ((R - L) >> 1);
process(arr, L, mid);
process(arr, mid + 1, R);
merge(arr, L, mid, R);
}
public static void merge(int[] arr, int L, int M, int R) {
//一个空数组排序使用,以下称为排序数组
int[] help = new int[R - L + 1];
int i = 0;
int p1 = L; //分区,上区排序指针
int p2 = M + 1; //分区,下区排序指针
while (p1 <= M && p2 <= R) { //两个指针都不越界时
//对比,将指针小值复制到排序数组,排序数组和取值数据指针移动
help[i++] = arr[p1] <= arr[p2] ? arr[p1++] : arr[p2++];
}
//注意以下两个while只会执行一次,因为每次p1和p2只移动一个
//将未越界指针指向的分区数组全部复制给排序数组
while (p1 <= M) {
help[i++] = arr[p1++];
}
while (p2 <= R) {
help[i++] = arr[p2++];
}
//最后总体遍历一遍,如果有数组没有赋值,补全数组,排序完成
for (i = 0; i < help.length; i++) {
arr[L + i] = help[i];
}
}
随机快排
快速排序(QuickSort)是排除稳定性因素后最常用的排序,下面用递归方法实现。
算法描述
- 从数列中挑出一个元素作为基准。
- 重新排列数列,把所有的比基准小的放在基准前面,反之放在后面(一样大可任意一边)完成后基准处在分区的中间位置。
- 通过递归调用把小于基准元素和大雨基准元素的子序列进行排序。
算法图示
动图制作网站:VisAlgo
代码实现
public static void quickSort(int[] arr) {
if (arr == null || arr.length < 2) {
return;
}
process(arr, 0, arr.length - 1);
}
public static void process(int[] arr, int L, int R) {
if (L >= R) {
return;
}
//随机快排
int M = partition(arr, L, R);
//分区递归
process(arr, L, M - 1);
process(arr, M + 1, R);
}
public static int partition(int[] arr, int L, int R) {
if (L > R) {
return -1;
}
if (L == R) {
return L;
}
int lessEqual = L - 1;
int index = L;
while (index < R) {
if (arr[index] <= arr[R]) {
swap(arr, index, ++lessEqual);
}
index++;
}
swap(arr, ++lessEqual, R);
return lessEqual;
}
//交换ij位置
public static void swap(int[] arr, int i, int j) {
int tmp = arr[i];
arr[i] = arr[j];
arr[j] = tmp;
}
更多精彩关注微信公众号