本文已参与「新人创作礼」活动,一起开启掘金创作之路。
根据数据范围估算时间复杂度
算法的时间复杂度是大致确定的,但是数据范围却千变万化。所以根据数据范围选择最优算法是一种简单而准确的方法。
以下介绍几种根据数据范围大致对应的时间复杂度。
十大经典排序算法
1、插入排序
public static void insertSort(int[] nums) {
// [0, i]是有序的
for (int i = 0; i < nums.length - 1; ++i) {
int j = i + 1;
int tmp = nums[i + 1];
// 帮i+1找到一个合适的位置,并插入
for (; j - 1 >= 0 && tmp < nums[j - 1]; --j) {
nums[j] = nums[j - 1];
}
nums[j] = tmp;
}
}
2、选择排序
public static void selectSort(int[] nums) {
for (int i = 0; i < nums.length; ++i) {
int min = nums[i];
int minIdx = i;
// 在[i+1, nums.length-1]中找到小于nums[i]且最小的元素进行交换
for (int j = i + 1; j < nums.length; ++j) {
if (nums[j] < min) {
min = nums[j];
minIdx = j;
}
}
nums[minIdx] = nums[i];
nums[i] = min;
}
}
3、冒泡排序
public static void bubbleSort(int[] nums) {
for (int i = 0; i < nums.length; ++i) {
// 不停地暴力交换
for (int j = 0; j < nums.length - 1; ++j) {
if (nums[j] > nums[j + 1]) {
swap(nums, j, j + 1);
}
}
}
}
4、希尔排序
public static void shellSort(int[] nums) {
for (int gap = nums.length / 2; gap > 0; gap /= 2) {
// 同插入排序,但是每次处理的数据间隙是逐步减小的
for (int i = 0; i + gap < nums.length; i += gap) {
int tmp = nums[i + gap];
int j = i + gap;
for (; j - gap >= 0 && tmp < nums[j - gap]; j -= gap) {
nums[j] = nums[j - gap];
}
nums[j] = tmp;
}
}
}
5、归并排序 merge(nums, 0, nums.length - 1)
public static void merge(int[] nums, int left, int right) {
int mid = left + (right - left) /2;
if (left < right) {
merge(nums, left, mid);
merge(nums, mid + 1, right);
mergeSort(nums, left, mid, right);
}
}
private static void mergeSort(int[] nums, int left, int mid, int right) {
int[] tmp = new int[right - left + 1];
int index = 0;
int indexA = left, indexB = right;
while (indexA <= mid && indexB <= right) {
if (nums[indexA] <= nums[indexB]) {
tmp[index++] = nums[indexA++];
} else {
tmp[index++] = nums[indexB++];
ans += mid - indexA + 1;
}
}
while (indexA <= mid) {
tmp[index++] = nums[indexA++];
}
while (indexB <= right) {
tmp[index++] = nums[indexB++];
}
System.arraycopy(tmp, 0, nums, from, tmp.length);
}
6、快速排序
public static void quickSort(int[] nums) {
quickSort0(nums, 0, nums.length-1);
}
// [from, to]
private static void quickSort0(int[] nums, int from, int to) {
if (from >= to) {
return ;
}
int cmp = nums[from];
int l = from, r = to;
while (l < r) {
while (l <= r && nums[l] <= cmp) {
++l;
}
while (l <= r && nums[r] > cmp) {
--r;
}
if (l < r) {
swap(nums, l, r);
}
}
swap(nums, from, r);
quickSort0(nums, from, r-1);
quickSort0(nums, r+1, to);
}
public void add(int[] nums, int i, int val){
nums[i] = val;
int curIndex = i;
while (curIndex > 0) {
int parentIndex = (curIndex - 1) / 2;
if (nums[parentIndex] < nums[curIndex])
swap(nums, parentIndex, curIndex);
else break;
curIndex = parentIndex;
}
}
public int remove(int[] nums, int size){
int result = nums[0];
nums[0] = nums[size - 1];
int curIndex = 0;
while (true) {
int leftIndex = curIndex * 2 + 1;
int rightIndex = curIndex * 2 + 2;
if (leftIndex >= size) break;
int maxIndex = leftIndex;
if (rightIndex < size && nums[maxIndex] < nums[rightIndex])
maxIndex = rightIndex;
if (nums[curIndex] < nums[maxIndex])
swap(nums, curIndex, maxIndex);
else break;
curIndex = maxIndex;
}
return result;
}
private static void swap(int[] nums, int idxA, int idxB) {
int tmp = nums[idxA];
nums[idxA] = nums[idxB];
nums[idxB] = tmp;
}
递归和动态规划 DP
斐波那契数列
1, 1, 2, 3, 5, 8, ... 求第n项;
1)暴力递归 --
public int f1(int n) {
if (n < 1) return 0;
if (n == 1 || n == 2) return 1;
return f1(n - 1) + f1(n - 2);
}
2)顺序计算 --
public int f2(int n) {
if (n < 1) return 0;
if (n == 1 || n == 2) return 1;
int res = 1, pre = 1, tmp = 0;
for (int i = 3; i <= n; i++) {
tmp = res;
res += pre;
pre = tmp;
}
return res;
}
3)加速矩阵乘法的动态规划 --
// 求矩阵m的p次方
public int[][] matrixPower(int[][] m, int p) {
int[][] res = new int[m.length][m[0].length];
// 先设res为单位矩阵
for (int i = 0; i < res.length; i++) res[i][i] = 1;
int [][] tmp = m;
// 根据p的二进制数形式进行累乘
for (; p != 0; p >>= 1) {
if ((p & 1) != 0) {
res = multiMatrix(res, tmp);
}
tmp = multiMatrix(tmp, tmp);
}
return res;
}
// 两矩阵相乘
public int[][] multiMatrix(int[][] m1, int[][] m2) {
int[][] res = new int[m1.length][m2[0].length];
for (int i = 0; i < m1.length; i++) {
for (int j = 0; j < m2[0].length; j++) {
for (int k = 0; k < m2.length; k++) {
res[i][j] += m1[i][k] * m2[k][j];
}
}
}
return res;
}
public int f3(int n) {
if (n < 1) return 0;
if (n == 1 || n == 2) return 1;
int [][] base = {{1, 1}, {1, 0}};
int [][] res = matrixPower(base, n - 2);
return res[0][0] + res[1][0];
}
矩阵的最小路径和
给定一个矩阵m,从左上角开始只能向右或者向下走,最后达到右下角,返回路径上所有数字加起来的最小和;
输入: 1 3 5 9 8 1 3 4 5 0 6 1 8 8 4 0 输出:12 解释:1-3-1-0-6-1-0
1)dp 矩阵 -- 时间 - 空间
首先确定第一行和第一列的路径和,其次根据上边和左边的较小值得到整个路径和的矩阵;
public int minPathSum1(int[][] m) {
if (m == null || m.length == 0 || m[0] == null || m[0].length == 0) {
return 0;
}
int row = m.length, col = m[0].length;
// 初始化dp矩阵
int[][] dp = new int[row][col];
dp[0][0] = m[0][0];
for (int i = 1; i < row; i++) {
dp[i][0] = dp[i - 1][0] + m[i][0];
}
for (int j = 1; j < col; j++) {
dp[0][j] = dp[0][j - 1] + m[0][j];
}
for (int i = 1; i < row; i++) {
for (int j = 1; j < col; j++) {
dp[i][j] = Math.min(dp[i - 1][j], dp[i][j - 1]) + m[i][j];
}
}
// 返回结果
return dp[row - 1][col - 1];
}
2)空间压缩法(仅适于求最优解,不可回溯) -- 时间 - 空间
public int minPathSum2(int[][] m) {
if (m == null || m.length == 0 || m[0] == null || m[0].length == 0) {
return 0;
}
// 定义行数与列数中的较大和较小的身份
int more = Math.max(m.length, m[0].length);
int less = Math.min(m.length, m[0].length);
// 行数是否大于列数
boolean rowMore = more == m.length;
int[] arr = new int[less]; // 辅助数组的长度仅为较小值
arr[0] = m[0][0];
for (int i = 1; i < less; i++) {
arr[i] = arr[i - 1] + (rowMore ? m[0][i] : m[i][0]);
}
for (int i = 1; i < more; i++) {
arr[0] += rowMore ? m[i][0] : m[0][i];
for (int j = 1; j < less; j++) {
arr[j] = Math.min(arr[j - 1], arr[j]) + (rowMore ? m[i][j] : m[j][i]);
}
}
return arr[less - 1];
}
换钱的最少货币数
arr中所有值都是正数且不重复,每个值代表一种面值的货币(可以使用任意张),求组成 aim(要找的钱数)的最少货币数;
eg. arr=[5,2,3], aim=20 4张5元,return 4 arr=[5,2,3], aim=0 不使用任何货币,return 0 arr=[3,5], aim=2 不能找开,return -1
空间压缩法 -- 时间 - 空间
public int minCoins2(int[] arr, int aim) {
if (arr == null || arr.length == 0 || aim < 0) return -1;
int max = Integer.MAX_VALUE;
int[] dp = new int[aim + 1];
for (int j = 1; j <= aim; j++) {
dp[j] = max;
if (j - arr[0] >= 0 && dp[j - arr[0]] != max) {
dp[j] = dp[j - arr[0]] + 1;
}
}
int tmp;
for (int i = 1; i < arr.length; i++) {
for (int j = 1; j <= aim; j++) {
tmp = max;
if (j - arr[i] >= 0 && dp[j - arr[i]] != max) {
tmp = dp[j - arr[i]] + 1;
}
dp[j] = Math.min(tmp, dp[j]);
}
}
return dp[aim] == max ? -1 : dp[aim];
}
换钱的方法数
arr中所有值都是正数且不重复,每个值代表一种面值的货币(可以使用任意张),求组成aim(要找的钱数)的方法数;
eg. arr=[5,10,25,1], aim=0 return 1 arr=[5,10,25,1], aim=15 return 6 arr=[3,5], aim=2 return 0
1)暴力递归 -- 时间
public int coins1(int[] arr, int aim) {
if (arr == null || arr.length == 0 || aim < 0) return 0;
return process1(arr, 0, aim);
}
// 如果用arr[index..N-1]面值的钱组成aim,返回总的方法数
public int process1(int[] arr, int index, int aim) {
int res = 0;
if (index == arr.length) {
res = aim == 0 ? 1 : 0; // 递归出口
} else {
for (int i = 0; arr[index] * i <= aim; i++) {
res += process1(arr, index + 1, aim - arr[index] * i);
}
}
return res;
}
2)记忆化搜索方法(n维map存储已经计算出的值,n为递归影响因素的个数) -- 时间
记忆化搜索是对必须要计算的递归过程才去计算并记录的。
3)动态规划的优化 -- 时间
public int coins4(int[] arr, int aim) {
if (arr == null || arr.length == 0 || aim < 0) return 0;
int[][] dp = new int[arr.length][aim + 1];
for (int i = 0; i < arr.length; i++) {
dp[i][0] = 1;
}
for (int j = 1; arr[0] * j <= aim; j++) {
dp[0][arr[0] * j] = 1;
}
for (int i = 1; i < arr.length; i++) {
for (int j = 1; j <= aim; j++) {
dp[i][j] = dp[i - 1][j]; // 0张此面值的钱的情况
dp[i][j] += j - arr[i] >= 0 ? dp[i][j - arr[i]] : 0; // 判断是不是越界了
}
}
return dp[arr.length - 1][aim];
}
4)动态规划的优化 + 空间压缩 -- 时间 - 空间
public int coins5(int[] arr, int aim) {
if (arr == null || arr.length == 0 || aim < 0) return 0;
int[] dp = new int[aim + 1];
for (int j = 0; arr[0] * j <= aim; j++) {
dp[arr[0] * j] = 1;
}
for (int i = 1; i < arr.length; i++) {
for (int j = 1; j <= aim; j++) {
dp[j] += j - arr[i] >= 0 ? dp[j - arr[i]] : 0;
}
}
return dp[aim];
}
正则表达式匹配
给你一个字符串 s 和一个字符规律 p,请你来实现一个支持 '.' 和 '*' 的正则表达式匹配。
'.' 匹配任意单个字符 '*' 匹配零个或多个前面的那一个元素 所谓匹配,是要涵盖 整个 字符串 s 的,而不是部分字符串。
示例 1: 输入:s = "aa", p = "a" 输出:false 解释:"a" 无法匹配 "aa" 整个字符串。
示例 2: 输入:s = "aa", p = "a*" 输出:true 解释:因为 '*' 代表可以匹配零个或多个前面的那一个元素, 在这里前面的元素就是 'a'。因此,字符串 "aa" 可被视为 'a' 重复了一次。
示例 3: 输入:s = "ab", p = ".*" 输出:true 解释:".*" 表示可匹配零个或多个('*')任意字符('.')。
public boolean isMatch(String s, String p) {
int m = s.length(), n = p.length();
boolean[][] dp = new boolean[m + 1][n + 1];
dp[0][0] = true;
for (int i = 0; i <= m; i++) {
for (int j = 1; j <= n; j++) {
if (p.charAt(j - 1) != '*') {
if (i >= 1 && (s.charAt(i - 1) == p.charAt(j - 1) || p.charAt(j - 1) == '.')) {
dp[i][j] = dp[i - 1][j - 1];
}
} else {
if (j >= 2 && i >= 1 && (s.charAt(i - 1) == p.charAt(j - 2) || p.charAt(j - 2) == '.')) {
dp[i][j] = dp[i - 1][j];
}
if (j >= 2) {
dp[i][j] |= dp[i][j - 2];
}
}
}
}
return dp[m][n];
}
经典题目
接雨水
给定 n 个非负整数表示每个宽度为 1 的柱子的高度图,计算按此排列的柱子,下雨之后能接多少雨水。
![]()
输入:height = [0, 1, 0, 2, 1, 0, 1, 3, 2, 1, 2, 1] 输出:6 解释:上面是由数组 [0, 1, 0, 2, 1, 0, 1, 3, 2, 1, 2, 1] 表示的高度图,在这种情况下,可以接 6 个单位的雨水(蓝色部分表示雨水)。
public int trap(int[] height) {
int n = height.length;
if (n == 0) {
return 0;
}
// 统计左投影下的雨水量
int[] leftMax = new int[n];
leftMax[0] = height[0];
for (int i = 1; i < n; ++i) {
leftMax[i] = Math.max(leftMax[i - 1], height[i]);
}
// 统计右投影下的雨水量
int[] rightMax = new int[n];
rightMax[n - 1] = height[n - 1];
for (int i = n - 2; i >= 0; --i) {
rightMax[i] = Math.max(rightMax[i + 1], height[i]);
}
int ans = 0;
for (int i = 0; i < n; ++i) {
ans += Math.min(leftMax[i], rightMax[i]) - height[i];
}
return ans;
}
计算右侧小于当前元素的个数
给你一个整数数组 nums ,按要求返回一个新数组 counts 。数组 counts 有该性质: counts[i] 的值是 nums[i] 右侧小于 nums[i] 的元素的数量。
示例 1: 输入:nums = [5,2,6,1] 输出:[2,1,1,0] 解释: 5 的右侧有 2 个更小的元素 (2 和 1) 2 的右侧仅有 1 个更小的元素 (1) 6 的右侧有 1 个更小的元素 (1) 1 的右侧有 0 个更小的元素
示例 2: 输入:nums = [-1] 输出:[0]
示例 3: 输入:nums = [-1,-1] 输出:[0,0]
class Solution {
static int[] ansNum;
static int[] indexes;
public List<Integer> countSmaller(int[] nums) {
indexes = new int[nums.length];
ansNum = new int[nums.length];
for (int i = 0; i < nums.length; i++) {
indexes[i] = i;
}
merge(nums, 0, nums.length - 1);
List<Integer> ans = new ArrayList<>();
for (int num : ansNum) {
ans.add(num);
}
return ans;
}
void merge(int[] nums, int left, int right) {
int mid = left + (right - left) / 2;
if (left < right) {
merge(nums, left, mid);
merge(nums, mid + 1, right);
mergeSort(nums, left, mid, right);
}
}
void mergeSort(int[] nums, int left, int mid, int right) {
int[] tmp = new int[right - left + 1];
int[] tmp_indexes = new int[right - left + 1];
int index = 0;
int indexA = left, indexB = mid + 1;
while (indexA <= mid && indexB <= right) {
if (nums[indexA] <= nums[indexB]) {
tmp_indexes[index] = indexes[indexA];
// 增加符合条件的个数
ansNum[indexes[indexA]] += indexB - mid - 1;
tmp[index++] = nums[indexA++];
} else {
tmp_indexes[index] = indexes[indexB];
tmp[index++] = nums[indexB++];
}
}
while (indexA <= mid) {
tmp_indexes[index] = indexes[indexA];
// 增加符合条件的个数
ansNum[indexes[indexA]] += indexB - mid - 1;
tmp[index++] = nums[indexA++];
}
while (indexB <= right) {
tmp_indexes[index] = indexes[indexB];
tmp[index++] = nums[indexB++];
}
System.arraycopy(tmp, 0, nums, left, tmp.length);
System.arraycopy(tmp_indexes, 0, indexes, left, tmp_indexes.length);
}
}
数组题目
和为K的子数组
给你一个整数数组 nums 和一个整数 k ,请你统计并返回该数组中和为 k 的子数组的个数。
示例 1: 输入:nums = [1,1,1], k = 2 输出:2
示例 2: 输入:nums = [1,2,3], k = 3 输出:2
// 构建前缀和数组,空间换时间
public int subarraySum(int[] nums, int k) {
int n = nums.length;
int[] preSum = new int[n + 1];
preSum[0] = 0;
for (int i = 0; i < n; i++) {
preSum[i + 1] = nums[i] + preSum[i];
}
int ans = 0;
for (int left = 0; left < n; left++) {
for (int right = left; right < n; right++) {
if (preSum[right + 1] - preSum[left] == k) {
ans++;
}
}
}
return ans;
}
回溯法
全排列
给定一个不含重复数字的数组 nums ,返回其所有可能的全排列。你可以按任意顺序返回答案。
示例 1: 输入:nums = [1,2,3] 输出:[[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]]
示例 2: 输入:nums = [0,1] 输出:[[0,1],[1,0]]
示例 3: 输入:nums = [1] 输出:[[1]]
public List<List<Integer>> permute(int[] nums) {
List<List<Integer>> res = new ArrayList<List<Integer>>();
List<Integer> output = new ArrayList<Integer>();
for (int num : nums) {
output.add(num);
}
backtrack(nums.length, output, res, 0);
return res;
}
public void backtrack(int n, List<Integer> output, List<List<Integer>> res, int first) {
// 所有数都填完了
if (first == n) {
res.add(new ArrayList<Integer>(output));
}
for (int i = first; i < n; i++) {
// 动态维护数组
Collections.swap(output, first, i);
// 继续递归填下一个数
backtrack(n, output, res, first + 1);
// 撤销操作
Collections.swap(output, first, i);
}
}