选择排序
从下标 开始遍历,每次从 选择一个最小的数,放在位置 。 的起始位置为 0,结束位置为 。
代码:
public static void selectionSort(int[] arr) {
if (arr == null || arr.length < 2) {
return;
}
for (int i = 0; i < arr.length - 1; i++) {
int minIdx = i;
for (int j = i + 1; j < arr.length; j++) {
if (arr[j] < arr[minIdx]) {
minIdx = j;
}
}
swap(arr, i, minIdx);
}
}
冒泡排序
以 为最终位置,从 开始,每次比较 和 ,如果 ,则交换,直到 ,最后一轮交换完成,最大数“冒泡”到了 的位置。 的起始位置为 ,结束位置为 。
代码:
public static void bubbleSort(int[] arr) {
if (arr == null || arr.length < 2) {
return;
}
for (int end = arr.length - 1; end > 0; end--) {
for (int i = 0; i < end; i++) {
if (arr[i] > arr[i + 1]) {
swap(arr, i, i + 1);
}
}
}
}
插入排序
假设 已经有序,每次 要进来,需要选一个位置插入进去。具体过程为从 开始( 已经有序),令 ,每次比较 和 ,如果 ,则两两交换,直到 , 的结束位置为 (即最后一个元素要插入进去)。
代码:
public static void insertionSort(int[] arr) {
if (arr == null || arr.length < 2) {
return;
}
for (int i = 1; i < arr.length; i++) {
int j = i - 1;
while (j >= 0 && arr[j] > arr[j + 1]) {
swap(arr, j, j + 1);
j--;
}
}
}
归并排序
递归实现
归并排序运用了分治的思想,既然要将 上的数进行排序,可以先对左半边 进行排序,然后再对右半边 进行排序,最后,将两个半边进行合并,使用双指针的方法很容易实现。
代码:
public static void mergeSort(int[] arr, int l, int r) {
if (l == r) {
return;
}
int mid = (l + r) / 2;
mergeSort(arr, l, mid);
mergeSort(arr, mid + 1, r);
merge(l, mid, r);
}
merge 过程需要一个辅助数组来实现:
public static void merge(int l, int mid, int r) {
int i = l;
int j = mid + 1;
int k = l;
while (i <= mid && j <= r) {
help[k++] = arr[i] <= arr[j] ? arr[i++] : arr[j++];
}
while (i <= mid) {
help[k++] = arr[i++];
}
while (j <= r) {
help[k++] = arr[j++];
}
for (k = l; k <= r; k++) {
arr[k] = help[k];
}
}
非递归实现
取步长为 ,每次进行 merge。
边界情况:左边有元素,右边没有元素(即 ),直接不用 merge 了,此步长结束。左边有元素,右边也有但不够,可以直接合并, 不能超过 。
public static void mergeSort2() {
for (int step = 1; step < n; step *= 2) {
int l = 0, mid, r;
while (l < n) {
mid = l + step - 1;
if (mid + 1 >= n) {
break;
}
r = Math.min(l + (step * 2) - 1, n - 1);
merge(l, mid, r);
l = r + 1;
}
}
}
应用
分治思想:看一个问题在大范围上的答案是否等于,左半边的答案 + 右半边的答案 + 跨越左右产生的答案。
归并排序特性:左半边和右半边都有序,在这种性质下,计算跨越左右产生的答案是是否能受益(即加快计算)。
例题一
计算数组的小和,在求解跨越左右产生的答案时,如果左右元素都有序,可以使用双指针做到 的时间复杂度。 指向 , 指向 ,如果 且 ,则将 累加到 ,直到不满足条件,将 累积到 。然后 向后移动一位, 从上次循环的位置重复上述过程。
代码:
import java.io.*;
import java.util.*;
public class Main {
private static int MAXN = 100001;
private static int n;
private static int[] s = new int[MAXN];
private static int[] help = new int[MAXN];
public static void main(String[] args) throws IOException {
BufferedReader bf = new BufferedReader(new InputStreamReader(System.in));
StreamTokenizer in = new StreamTokenizer(bf);
PrintWriter out = new PrintWriter(new OutputStreamWriter(System.out));
in.nextToken();
n = (int) in.nval;
for (int i = 0; i < n; i++) {
in.nextToken();
s[i] = (int) in.nval;
}
out.println(smallSum(0, n - 1));
out.flush();
bf.close();
out.close();
}
private static long smallSum(int l, int r) {
if (l == r) {
return 0;
}
int mid = (l + r) / 2;
return smallSum(l, mid) + smallSum(mid + 1, r) + merge(l, mid, r);
}
private static long merge(int l, int mid, int r) {
long ans = 0;
for (int i = l, j = mid + 1; j <= r; j++) {
long sum = 0;
while (i <= mid && s[i] <= s[j]) {
sum += s[i++];
}
ans += (sum * (r - j + 1));
}
int a = l, b = mid + 1, i = l;
while (a <= mid && b <= r) {
help[i++] = s[a] <= s[b] ? s[a++] : s[b++];
}
while (a <= mid) {
help[i++] = s[a++];
}
while (b <= r) {
help[i++] = s[b++];
}
for (i = l; i <= r; i++) {
s[i] = help[i];
}
return ans;
}
}
例题二
493. 翻转对,在求解跨越左右产生的答案时,同样如果左右元素都有序,可以使用双指针做到 的时间复杂度。 指向 , 指向 ,如果 并且 , 向后移动,表示 不能加入到答案中,当不满足条件时,此时对于 , 及其后面的元素都能加入到答案中,即 。然后 向后移动一位, 从上次循环结束的位置重复上述过程。
代码:
class Solution {
private static int MAXN = 50001;
private static int[] help = new int[MAXN];
public int reversePairs(int[] nums) {
return count(nums, 0, nums.length - 1);
}
public int count(int[] arr, int l, int r) {
if (l == r) {
return 0;
}
int mid = (l + r) / 2;
return count(arr, l, mid) + count(arr, mid + 1, r) + merge(arr, l, mid, r);
}
public int merge(int[] arr, int l, int mid, int r) {
int ans = 0;
for (int i = l, j = mid + 1; j <= r; j++) {
while (i <= mid && (long) arr[i] <= (long) arr[j] * 2) {
i++;
}
ans += (mid - i + 1);
}
int a = l, b = mid + 1, i = l;
while (a <= mid && b <= r) {
help[i++] = arr[a] <= arr[b] ? arr[a++] : arr[b++];
}
while (a <= mid) {
help[i++] = arr[a++];
}
while (b <= r) {
help[i++] = arr[b++];
}
for (i = l; i <= r; i++) {
arr[i] = help[i];
}
return ans;
}
}
快速排序
实现
对于一个待排序的数组,随机选择一个元素 ,以 对数组进行划分。划分完成后, 的元素都小于 , 的元素都等于 , 的元素都大于 。
partition 代码:
public static int[] partition(int l, int r, int x) {
/**
* 1) arr[i] < x,发配到小于 x 的区域,交换 arr[i] 和 arr[first],first++, i++
* 2) arr[i] = x, 中间区域就是等于 x, 直接 i++
* 3) arr[i] > x, 发配到大于 x 的区域,交换 arr[i] 和 arr[last],last--,i 不变,last 交换过来后它的值还不确定
*/
int first = l, last = r;
int i = l;
while (i <= last) {
if (arr[i] == x) {
i++;
} else if (arr[i] < x) {
swap(arr, first++, i++);
} else {
swap(arr, i, last--);
}
}
return new int[]{first, last};
}
划分完成后,数组变成了 ,等于 的部分起始位置为 ,终止位置为 ,接下来只需要递归地对 和 进行快速排序。
代码:
public static void quickSort(int[] arr, int l, int r) {
if (l >= r) {
return;
}
int x = arr[l + (int) (Math.random() * (r - l + 1))];
int[] res = partition(l, r, x);
int left = res[0], right = res[1];
quickSort(arr, l, left - 1);
quickSort(arr, right + 1, r);
}
应用
例题一
数组中的第K个最大元素,初始时, 指向 , 指向 ,进行 partition 操作,如果 落在了 之间,直接返回 ,如果 ,说明左半边的元素个数大于 ,需要去左边半找,即 , 同理。
class Solution {
public int findKthLargest(int[] nums, int k) {
return randomSelect(nums, nums.length - k);
}
// 求第 i 小的数(下标从 0 开始)
public int randomSelect(int[] arr, int i) {
int ans = 0;
int l = 0, r = arr.length - 1;
while (l <= r) {
int x = arr[l + (int) (Math.random() * (r - l + 1))];
int[] res = partition(arr, l, r, x);
int first = res[0], last = res[1];
if (i < first) {
r = first - 1;
} else if (i > last) {
l = last + 1;
} else {
ans = arr[i];
break;
}
}
return ans;
}
public int[] partition(int[] arr, int l, int r, int x) {
int first = l, last = r, i = l;
while (i <= last) {
if (arr[i] < x) {
swap(arr, i++, first++);
} else if (arr[i] > x) {
swap(arr, i, last--);
} else {
i++;
}
}
return new int[]{first, last};
}
public void swap(int[] arr, int i, int j) {
int tmp = arr[i];
arr[i] = arr[j];
arr[j] = tmp;
}
}
堆排序
堆的结构是一颗完全二叉树,堆分为大根堆和小根堆,对于每颗子树,最上面的元素都是最大的或最小的。
使用数组存储堆: 的父亲节点为 , 的左孩子为 ,右孩子为 ,使用 标识堆的大小,防止越界操作。
操作(以大根堆为例):
- 向上调整大根堆:如果 大于它的父亲 ,则相互交换,重复该过程。
- 向下调整大根堆:自己和左孩子、右孩子比较,最大的那个数放到当前位置,令 指向该位置,重复过程。
public static void heapInsert(int i) {
while (arr[i] > arr[(i - 1) / 2]) {
swap(arr, i, (i - 1) / 2);
i = (i - 1) / 2;
}
}
public static void heapify(int i, int size) {
int l = i * 2 + 1;
while (l < size) {
int best = l + 1 < size && arr[l + 1] > arr[l] ? l + 1 : l;
best = arr[best] > arr[i] ? best : i;
// 如果最大元素是它自己,则直接结束 heapify 过程
if (best == i) {
break;
}
swap(arr, best, i);
i = best;
l = i * 2 + 1;
}
}
堆排序:对于一个数组,首先需要建堆,可以等价于依次对每个数进行 heapInsert 操作,最后 为 。然后进行排序,此时 位置为最大元素 ,直接把它与最后位置的数 交换,并对 位置进行 heapify 操作,重复此过程直到 。
public static void heapSort() {
for (int i = 0; i < n; i++) {
// 向堆中插入元素
heapInsert(i);
}
int size = n;
while (size > 1) {
// 把最大的数放在最后
swap(arr, 0, --size);
// 刚才最后的数来到了 0,做一次 heapify 操作,保持大根堆的性质
heapify(0, size);
}
}
上述建堆过程时间复杂度为 ,此建堆过程为自顶向下,如果自底向上建堆,可以做到 。
public static void heapSort() {
// 从最后一个父节点开始进行 heapify
for (int i = (n - 1) / 2; i >= 0; i--) {
heapify(i, n);
}
int size = n;
while (size > 1) {
// 把最大的数放在最后
swap(arr, 0, --size);
// 刚才最后的数来到了 0,做一次 heapify 操作,保持大根堆的性质
heapify(0, size);
}
}
基数排序
假设数组中元素都是非负数,如果有负数,可以加上最小值全部变为正值。
// round 为数组元素中的最大位数
public static void radixSort(int round) {
for (int offset = 1; round > 0; offset *= 10, round--) {
int[] cnt = new int[10];
for (int i = 0; i < n; i++) {
cnt[(i / offset) % 10]++;
}
/**
* 前缀数量分区技巧
* 统计出 cnt 后,进行前缀和计算,表示:
* <= 0 的数有 cnt[0] 个
* <= 1 的数有 cnt[1] 个
* ...
* <= 9 的数有 cnt[9] 个
* 目标:将 arr 中的元素按照 (arr[i] / offset) % 10 放入 help 中
* 从后往前遍历 arr,保证稳定性
* 假设当前 (arr[i] / offset) % 10 = 2,并且 <= 2 的数有 8 个
* 所以可以将它放在 help[7]
*/
for (int i = 1; i < 10; i++) {
cnt[i] += cnt[i - 1];
}
for (int i = n - 1; i >= 0; i--) {
help[--cnt[(arr[i] / offset) % 10]] = arr[i];
}
for (int i = 0; i < n; i++) {
arr[i] = help[i];
}
}
}