排序算法
# 排序算法
排序是计算机内经常进行的一种操作,其目的是将一组“无序”的记录序列调整为“有序”的记录序列。分内部排序和外部排序,若整个排序过程不需要访问外存便能完成,则称此类排序问题为内部排序。反之,若参加排序的记录数量很大,整个序列的排序过程不可能在内存中完成,则称此类排序问题为外部排序。内部排序的过程是一个逐步扩大记录的有序序列长度的过程。
下面列举了常见的排序算法,每个算法的swap方法见附录。
# 选择排序
原理:第一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,然后再从剩余的未排序元素中寻找到最小(大)元素,然后放到已排序的序列的末尾。以此类推,直到全部待排序的数据元素的个数为零。
时间复杂度:
public static void selectionSort(int[] arr) {
if (arr == null || arr.length < 2) {
return;
}
for (int i = 0; i < arr.length - 1; i++) {
int minIndex = i;
// i ~ N-1 上找最小值的下标
for (int j = i + 1; j < arr.length; j++) {
minIndex = arr[j] < arr[minIndex] ? j : minIndex;
}
swap(arr, i, minIndex);
}
}
# 冒泡排序
原理:重复地走访过要排序的元素列,依次比较两个相邻的元素,如果顺序(如从大到小、首字母从Z到A)错误就把他们交换过来。走访元素的工作是重复地进行直到没有相邻元素需要交换,也就是说该元素列已经排序完成。 这个算法的名字由来是因为越小的元素会经由交换慢慢“浮”到数列的顶端(升序或降序排列),就如同碳酸饮料中二氧化碳的气泡最终会上浮到顶端一样,故名“冒泡排序”。
时间复杂度:
public static void bubbleSort(int[] arr) {
if (arr == null || arr.length < 2) {
return;
}
// 0 ~ N-1 每次走访元素,将最大的排到最后
for (int e = arr.length - 1; e > 0; e--) {
for (int i = 0; i < e; i++) {
if (arr[i] > arr[i + 1]) {
swap(arr, i, i + 1);
}
}
}
}
# 插入排序
原理:对于少量元素的排序,它是一个有效的算法。基本思想是将一个元素插入到已经排好序的有序表中,从而得到一个新的、记录数增1的有序表。在其实现过程使用双层循环,外层循环对除了第一个元素之外的所有元素,内层循环对当前元素前面有序表进行待插入位置查找,并进行移动。
时间复杂度:
public static void insertionSort(int[] arr) {
if (arr == null || arr.length < 2) {
return;
}
// 0 ~ i 做到有序
for (int i = 1; i < arr.length; i++) {
for (int j = i - 1; j >= 0 && arr[j] > arr[j + 1]; j--) {
swap(arr, j, j + 1);
}
}
}
# 归并排序
原理:归并排序整体是递归的:左边排好序 + 右边排好序 + merge让整体有序,让整体有序的过程使用了排外序方法,此外,归并排序也可以使用非递归的方式实现。
时间复杂度: (递归时间复杂度根据master公式求解,也是这个复杂度)
/**
* 递归写法
*/
public static void mergeSort1(int[] arr) {
if (arr == null || arr.length < 2) {
return;
}
process(arr, 0, arr.length - 1);
}
/**
* master公式:T(N) = 2 * T(N / 2) + O(N)
*/
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;
// 谁小移动谁的索引 ,同时help移动
while (p1 <= M && p2 <= R) {
help[i++] = arr[p1] <= arr[p2] ? arr[p1++] : arr[p2++];
}
// 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];
}
}
/**
* 非递归写法
*/
public static void mergeSort2(int[] arr) {
if (arr == null || arr.length < 2) {
return;
}
int N = arr.length;
// 每组步长
int mergeSize = 1;
while (mergeSize < N) {
// 当前左组的,第一个位置
int L = 0;
while (L < N) {
if (mergeSize >= N - L) {
break;
}
int M = L + mergeSize - 1;
int R = M + Math.min(mergeSize, N - M - 1);
merge(arr, L, M, R);
L = R + 1;
}
// 防止溢出
if (mergeSize > N / 2) {
break;
}
mergeSize <<= 1;
}
}
# 面试题
在一个数组中,一个数左边比它小的数的总和,叫数的小和,所有数的小和累加起来,叫数组小和,求数组小和。
例如:[1, 3, 4, 2, 5]
- 1左边比1小的数:无
- 3左边比3小的数:1
- 4左边比4小的数:1、3
- 2左边比2小的数:1
- 5左边比2小的数:1、3、4、2
数组小和为:1 + 1 + 3 +1 + 1 + 3 + 4 + 2 = 16。
public static int smallSum(int[] arr) {
if (arr == null || arr.length < 2) {
return 0;
}
return process(arr, 0, arr.length - 1);
}
public static int process(int[] arr, int l, int r) {
if (l == r) {
return 0;
}
// l < r
int mid = l + ((r - l) >> 1);
return process(arr, l, mid) + process(arr, mid + 1, r) + merge(arr, l, mid, r);
}
public static int 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;
int res = 0;
while (p1 <= m && p2 <= r) {
// 累加操作
res += arr[p1] < arr[p2] ? (r - p2 + 1) * arr[p1] : 0;
// 相等时候移动右组索引
help[i++] = arr[p1] < arr[p2] ? arr[p1++] : arr[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];
}
return res;
}
# 快速排序
# Partition过程
给定一个数组arr和一个整数num,请把小于等于num的数放在数组的左边,大于num的数放在数组的右边(不要求要序)。要求额外空间复杂度,时间复杂度。
算法逻辑以及流程:数组划分为了两个区域<=区域,以及>区域。
- 当arr[i] <= num, arr[i] 与<=区域的右一个元素交换,<=区域右扩,i++;
- 当arr[i] > num, 直接i++;
/**
* arr[L..R]上,以arr[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;
}
进一步拓展,如果需要将数组区域划分为三个区域:<区域,=区域以及>区域(三色国旗),那么算法逻辑与流程如下:
- 当arr[i] == num,i++;
- 当arr[i] < num, arr[i] 与<区域的右一个元素交换,<区域右扩,i++;
- 当arr[i] > num, arr[i] 与>区域的左一个元素交换,>区域左扩,i不动;
/**
* arr[L...R] 玩荷兰国旗问题的划分,以arr[R]做划分值
* 返回值是个长度为2的数组,分别表示<区右边界,>区左边界
*/
public static int[] netherlandsFlag(int[] arr, int L, int R) {
if (L > R) {
return new int[] { -1, -1 };
}
if (L == R) {
return new int[] { L, R };
}
int less = L - 1; // < 区 右边界
int more = R; // > 区 左边界
int index = L;
while (index < more) { // 当前位置,不能和 >区的左边界撞上
if (arr[index] == arr[R]) {
index++;
} else if (arr[index] < arr[R]) {
swap(arr, index++, ++less);
} else {
swap(arr, index, --more);
}
}
swap(arr, more, R); // 最后R元素与>区左边界交换
return new int[] { less + 1, more };
}
# 1.0-递归版本
也就是直接使用Partition的简单版本,时间复杂度:
/**
* 1.0-递归版本
*/
public static void quickSort1(int[] arr) {
if (arr == null || arr.length < 2) {
return;
}
process1(arr, 0, arr.length - 1);
}
public static void process1(int[] arr, int L, int R) {
if (L >= R) {
return;
}
// L..R partition arr[R]为边界, [ <=arr[R] arr[R] >arr[R] ]
int M = partition(arr, L, R);
process1(arr, L, M - 1);
process1(arr, M + 1, R);
}
# 2.0-荷兰国旗递归版本
也就是在Partition上进一步划分区域的荷兰国旗版本,时间复杂度:
/**
* 2.0-荷兰国旗递归版本
*/
public static void quickSort2(int[] arr) {
if (arr == null || arr.length < 2) {
return;
}
process2(arr, 0, arr.length - 1);
}
public static void process2(int[] arr, int L, int R) {
if (L >= R) {
return;
}
// 使用荷兰国旗的方式,对等于区域两侧进行递归
int[] equalArea = netherlandsFlag(arr, L, R);
process2(arr, L, equalArea[0] - 1);
process2(arr, equalArea[1] + 1, R);
}
# 3.0-随机快排递归版
在荷兰旗版本版本上,引入随机挑选划分num的机制,在时间复杂度上长期期望为:
public static void quickSort3(int[] arr) {
if (arr == null || arr.length < 2) {
return;
}
process3(arr, 0, arr.length - 1);
}
public static void process3(int[] arr, int L, int R) {
if (L >= R) {
return;
}
// 随机挑选划分值
swap(arr, L + (int) (Math.random() * (R - L + 1)), R);
int[] equalArea = netherlandsFlag(arr, L, R);
process3(arr, L, equalArea[0] - 1);
process3(arr, equalArea[1] + 1, R);
}
快排的时间复杂度
通过分析知道,划分值越靠近中间,性能越好;越靠近两边,性能越差;随机选一个数进行划分的目的就是让好情况和坏情况都变成概率事件,每一种情况都有各自的时间复杂度,但是概率都是1/N,那么综合情况来说,时间复杂度就是这种概率模型下的长期期望: ,同样的额外空间复杂度也是长期期望
# 堆排序
# 附录:swap方法
# 常见交换写法
public static void swap(int[] arr, int i, int j) {
int tmp = arr[i];
arr[i] = arr[j];
arr[j] = tmp;
}
# 与运算交换写法
这种写法,如果 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];
}