目录
- *连续子数组的最大和
- *回文子串
- *最短无序连续子数组
- *分割等和子集
- *比特位计数
- *打家劫舍 III
- *零钱兑换
- 戳气球*
- *最佳买卖股票时机含冷冻期
- *最长递增子序列
- *完全平方数
- *最大正方形
- *打家劫舍
- *区域和检索 - 数组不可变
- *乘积最大子数组
- *单词拆分
- 数字序列中某一位的数字*
- 不用加减乘除做加法*
- *二进制中1的个数
- *不同的二叉搜索树
- *编辑距离
- *爬楼梯
- *最小路径和
- *跳跃游戏
- *最大子序和
- *最长有效括号
- *礼物的最大价值
- *最长公共子串(牛客网)
- *股票的最大利润
- *构建乘积数组
- *把数字翻译成字符串
- *最长不含重复字符的子字符串
- *接雨水
- *斐波那契数列
- *青蛙跳台阶问题
- *剪绳子
- *统计字典序元音字符串的数目
- *丑数
- *n个骰子的点数
- *最长回文子串
- *正则表达式匹配
- *小朋友过河问题
*连续子数组的最大和
1 题目描述
输入一个整型数组,数组中的一个或连续多个整数组成一个子数组。求所有子数组的和的最大值。
要求时间复杂度为O(n)。
示例1:
输入: nums = [-2,1,-3,4,-1,2,1,-5,4]
输出: 6
解释: 连续子数组 [4,-1,2,1] 的和最大,为 6。
提示:
1 <= arr.length <= 10^5
-100 <= arr[i] <= 100
2 解题(java)
2.1 动态规划解析
1 状态定义: 设动态规划列表 dp ,dp[i] 代表以元素 nums[i] 为结尾的连续子数组最大和;
2 转移方程: 若dp[i−1]≤0 ,说明 dp[i−1] 对 dp[i] 产生负贡献,即 dp[i−1]+nums[i] 还不如 nums[i] 本身大:
- 当 dp[i−1]>0 时:执行 dp[i] = dp[i-1] + nums[i];
- 当 dp[i−1]≤0 时:执行 dp[i] = nums[i];
3 初始状态: dp[0]=nums[0],即以 nums[0] 结尾的连续子数组最大和为 nums[0] ;
4 返回值: 返回 dp 列表中的最大值,代表全局最大值;
2.2 空间复杂度降低
- 由于 dp[i] 只与 dp[i-1] 和 nums[i] 有关系,因此每次只保留dp[i-1]即可;
- 由于省去 dp 列表使用的额外空间,因此空间复杂度从 O(N) 降至 O(1);
2.3 Java代码
class Solution {
public int maxSubArray(int[] nums) {
int pre = 0, max = Integer.MIN_VALUE;
for(int num : nums) {
pre = Math.max(num, pre+num);
max = Math.max(max, pre);
}
return max;
}
}
3 复杂性分析
- 时间复杂度 O(N) : 遍历一次数组 nums 即可获得结果,使用 O(N) 时间;
- 空间复杂度 O(1) : 占用常数大小的额外空间;
*回文子串
1 题目描述
给定一个字符串,你的任务是计算这个字符串中有多少个回文子串。
具有不同开始位置或结束位置的子串,即使是由相同的字符组成,也会被视作不同的子串。
示例 1:
输入:“abc”
输出:3
解释:三个回文子串: “a”, “b”, “c”
示例 2:
输入:“aaa”
输出:6
解释:6个回文子串: “a”, “a”, “a”, “aa”, “aa”, “aaa”
提示:
- 输入的字符串长度不会超过 1000 。
2 解题(Java)
2.1 动态规划法
class Solution {
public int countSubstrings(String s) {
int n = s.length();
boolean[][] dp = new boolean[n][n];
int res = 0;
for (int j=0; j<n; j++) {
for (int i=0; i<=j; i++) {
if (s.charAt(i)==s.charAt(j) && (j-i<3 || dp[i+1][j-1])) {
dp[i][j] = true;
res++;
}
}
}
return res;
}
}
复杂性分析
时间复杂度为 O(N2),空间复杂度为 O(N2)。
2.2 中心扩展法
两种情况:
- 以1个点作为中心点向两端扩展;
- 以2个点作为中心点向两端扩展;
class Solution {
public int countSubstrings(String s) {
int res = 0;
for (int i=0; i<=s.length()*2-2; i++) {
int left = i / 2;
int right = (i + 1) / 2;
while (left >= 0 && right < s.length() && s.charAt(left) == s.charAt(right)) {
res++;
left--;
right++;
}
}
return res;
}
}
复杂性分析
时间复杂度为 O(N2),空间复杂度为 O(1)。
*最短无序连续子数组
1 题目描述
给你一个整数数组 nums ,你需要找出一个 连续子数组 ,如果对这个子数组进行升序排序,那么整个数组都会变为升序排序。
请你找出符合题意的 最短 子数组,并输出它的长度。
示例 1:
输入:nums = [2,6,4,8,10,9,15]
输出:5
解释:你只需要对 [6, 4, 8, 10, 9]进行升序排序,那么整个表都会变为升序排序。
示例 2:
输入:nums = [1,2,3,4]
输出:0
示例 3:
输入:nums = [1]
输出:0
提示:
- 1 <= nums.length <= 104
- -105 <= nums[i] <= 105
进阶:你可以设计一个时间复杂度为 O(n) 的解决方案吗?
2 解题(Java)
解题思路:
从左向右遍历,只要碰到比已经遍历过路径内的最大值要小的元素,说明该元素需要被纳入到重排序的子数组中,最终得到右边界点;再从右向左遍历,只要碰到比已经遍历过的路径内的最小值还要大的元素,说明该元素需要被纳入到重排序的子数组中,最终得到左边界点。
class Solution {
public int findUnsortedSubarray(int[] nums) {
// max为遍历过程中最大值的下标,right为目标边界的右边界
int right = 0, max = 0;
for (int i=1; i<nums.length; i++) {
if (nums[i] >= nums[max]) max = i;
else right = i;
}
// min为遍历过程中最小值的下标,left为目标边界的左边界
int left = nums.length - 1, min = nums.length - 1;
for (int i=nums.length-2; i>=0; i--) {
if (nums[i] <= nums[min]) min = i;
else left = i;
}
return right <= left ? 0 : right - left + 1;
}
}
3 复杂性分析
- 时间复杂度O(n);
- 空间复杂度O(1);
*分割等和子集
1 题目描述
给你一个 只包含正整数 的 非空 数组 nums 。请你判断是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。
示例 1:
输入:nums = [1,5,11,5]
输出:true
解释:数组可以分割成 [1, 5, 5] 和 [11] 。
示例 2:
输入:nums = [1,2,3,5]
输出:false
解释:数组不能分割成两个元素和相等的子集。
提示:
- 1 <= nums.length <= 200
- 1 <= nums[i] <= 100
2 解题(Java)
2.1 解题思路:动态规划
1 状态定义:
dp[i][j]表示从数组的[0,i]区间内挑选一些正整数,每个数只能使用一次,使得这些数的和恰好等于j。
2 初始化
if (nums[0] > target) return false;
else dp[0][nums[0]] = true;
3 状态转移方程:
for (int i = 1; i < len; i++) {
for (int j = 1; j <= target; j++) {
// 1 不用nums[i]
dp[i][j] |= dp[i - 1][j];
// 2 用nums[i],前提是j >= nums[i]
if (j >= nums[i]) {
dp[i][j] |= (j == nums[i]) ? true : dp[i - 1][j - nums[i]];
}
}
}
2.2 Java代码
class Solution {
public boolean canPartition(int[] nums) {
int sum = 0;
for (int num : nums) sum += num;
if (sum % 2 == 1) return false;
int target = sum / 2;
// dp[i][j]表示从数组[0, i]区间内挑选一些正整数,每个数最多使用一次,使得这些数的和恰好等于j,是否成立
boolean[][] dp = new boolean[nums.length][target+1];
// 初始化判断
if (nums[0] > target) return false;
dp[0][nums[0]] = true;
for (int i=1; i<nums.length; i++) {
for (int j=1; j<=target; j++) {
// 1 不用nums[i]
dp[i][j] |= dp[i-1][j];
// 2 用nums[i],前提是j >= nums[i]
if(j >= nums[i]) {
dp[i][j] |= j == nums[i] ? true : dp[i-1][j-nums[i]];
}
}
}
return dp[nums.length-1][target];
}
}
3 复杂性分析
- 时间复杂度:O(NC)。其中N 是数组元素的个数,C 是数组元素和的一半;
- 空间复杂度:O(NC);
*比特位计数
1 题目描述
给定一个非负整数 num。对于 0 ≤ i ≤ num 范围中的每个数字 i ,计算其二进制数中的 1 的数目并将它们作为数组返回。
示例 1:
输入: 2
输出: [0,1,1]
示例 2:
输入: 5
输出: [0,1,1,2,1,2]
进阶:
- 给出时间复杂度为O(n*sizeof(integer))的解答非常容易。但你可以在线性时间O(n)内用一趟扫描做到吗?
- 要求算法的空间复杂度为O(n)。
- 你能进一步完善解法吗?要求在C++或任何其他语言中不使用任何内置函数(如 C++ 中的__builtin_popcount)来执行此操作。
2 解题(Java)
class Solution {
public int[] countBits(int n) {
int[] res = new int[n+1];
for (int i=1; i<=n; i++) {
if (i % 2 == 0) res[i] = res[i / 2];
else res[i] = res[i - 1] + 1;
}
return res;
}
}
3 复杂性分析
- 时间复杂度:O(n);
- 空间复杂度:O(n);
*打家劫舍 III
1 题目描述
在上次打劫完一条街道之后和一圈房屋后,小偷又发现了一个新的可行窃的地区。这个地区只有一个入口,我们称之为“根”。 除了“根”之外,每栋房子有且只有一个“父“房子与之相连。一番侦察之后,聪明的小偷意识到“这个地方的所有房屋的排列类似于一棵二叉树”。 如果两个直接相连的房子在同一天晚上被打劫,房屋将自动报警。
计算在不触动警报的情况下,小偷一晚能够盗取的最高金额。
示例 1:
输入: [3,2,3,null,3,null,1]
3
/ \
2 3
\ \
3 1
输出: 7
解释: 小偷一晚能够盗取的最高金额 = 3 + 3 + 1 = 7.
示例 2:
输入: [3,4,5,1,3,null,1]
3
/ \
4 5
/ \ \
1 3 1
输出: 9
解释: 小偷一晚能够盗取的最高金额 = 4 + 5 = 9.
2 解题(Java)
动态规划+后序遍历:
打劫节点A分两种情况:
- 打劫A:node.val + 不打劫A左节点的A左节点 + 不打劫A右节点的A右节点;
- 不打劫A:Math.max(打劫A左节点,不打劫A左节点)+ Math.max(打劫A右节点,不打劫A右节点);
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode() {}
* TreeNode(int val) { this.val = val; }
* TreeNode(int val, TreeNode left, TreeNode right) {
* this.val = val;
* this.left = left;
* this.right = right;
* }
* }
*/
class Solution {
public int rob(TreeNode root) {
int[] rootStatus = dfs(root);
return Math.max(rootStatus[0], rootStatus[1]);
}
public int[] dfs(TreeNode node) {
if (node == null) {
return new int[]{0, 0};
}
int[] l = dfs(node.left);
int[] r = dfs(node.right);
int selected = node.val + l[1] + r[1];
int notSelected = Math.max(l[0], l[1]) + Math.max(r[0], r[1]);
return new int[]{selected, notSelected};
}
}
3 复杂性分析
- 时间复杂度:O(n);
- 空间复杂度:O(n);
*零钱兑换
1 题目描述
给你一个整数数组 coins ,表示不同面额的硬币;以及一个整数 amount ,表示总金额。
计算并返回可以凑成总金额所需的最少的硬币个数 。如果没有任何一种硬币组合能组成总金额,返回 -1 。
你可以认为每种硬币的数量是无限的。
示例 1:
输入:coins = [1, 2, 5], amount = 11
输出:3
解释:11 = 5 + 5 + 1
示例 2:
输入:coins = [2], amount = 3
输出:-1
示例 3:
输入:coins = [1], amount = 0
输出:0
示例 4:
输入:coins = [1], amount = 1
输出:1
示例 5:
输入:coins = [1], amount = 2
输出:2
提示:
- 1 <= coins.length <= 12
- 1 <= coins[i] <= 231 - 1
- 0 <= amount <= 104
2 解题(Java)
动态规划:
class Solution {
public int coinChange(int[] coins, int amount) {
int[] dp = new int[amount+1];
Arrays.fill(dp, Integer.MAX_VALUE);
dp[0] = 0;
for (int i=1; i<=amount; i++) {
for (int j=0; j<coins.length; j++) {
if (i - coins[j] >= 0 && dp[i-coins[j]] != Integer.MAX_VALUE) dp[i] = Math.min(dp[i-coins[j]] + 1, dp[i]);
}
}
return dp[amount] == Integer.MAX_VALUE ? -1 : dp[amount];
}
}
3 复杂性分析
- 时间复杂度:O(Sn),其中 S 是金额,n 是硬币数;
- 空间复杂度:O(S);
戳气球*
1 题目描述
有 n 个气球,编号为0 到 n - 1,每个气球上都标有一个数字,这些数字存在数组 nums 中。
现在要求你戳破所有的气球。戳破第 i 个气球,你可以获得 nums[i - 1] * nums[i] * nums[i + 1] 枚硬币。 这里的 i - 1 和 i + 1 代表和 i 相邻的两个气球的序号。如果 i - 1或 i + 1 超出了数组的边界,那么就当它是一个数字为 1 的气球。
求所能获得硬币的最大数量。
示例 1:
输入:nums = [3,1,5,8]
输出:167
解释: nums = [3,1,5,8] --> [3,5,8] --> [3,8] --> [8] --> [] coins = 315 + 358 + 138 + 181 = 167
示例 2:
输入:nums = [1,5]
输出:10
提示:
- n == nums.length
- 1 <= n <= 500
- 0 <= nums[i] <= 100
2 解题(Java)
动态规划
class Solution {
public int maxCoins(int[] nums) {
if(nums == null || nums.length == 0) {
return 0;
}
int n = nums.length;
//处理边界:创建一个n+2的数组,开头和末尾都填1
int[] arr = new int[n+2];
Arrays.fill(arr, 1);
for(int i=0; i<n; i++){
arr[i+1] = nums[i];
}
int[][] dp = new int[n+2][n+2];
//dp[i][j]为(i,j)开区间内最大值
for(int j=2; j<=n+1; j++) {
for(int i=j-2; i>=0; i--) {
for(int k=i+1; k<j; k++) {//戳破第k个气球
dp[i][j] = Math.max(dp[i][j], arr[i] * arr[k] * arr[j] + dp[i][k] + dp[k][j]);
}
}
}
return dp[0][n+1];
}
}
3 复杂性分析
- 时间复杂度:O(n3),其中状态数为n2,状态转移复杂度为O(n),总的时间复杂度为O(n2 * n);
- 空间复杂度:O(n2),状态数为n2;
*最佳买卖股票时机含冷冻期
1 题目描述
给定一个整数数组,其中第 i 个元素代表了第 i 天的股票价格 。
设计一个算法计算出最大利润。在满足以下约束条件下,你可以尽可能地完成更多的交易(多次买卖一支股票):
- 你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。
- 卖出股票后,你无法在第二天买入股票 (即冷冻期为 1 天)。
示例:
输入: [1,2,3,0,2]
输出: 3
解释: 对应的交易状态为: [买入, 卖出, 冷冻期, 买入, 卖出]
2 解题(Java)
动态规划:
class Solution {
public int maxProfit(int[] prices) {
if (prices==null || prices.length==0) {
return 0;
}
int n = prices.length;
// dp[i][0]: 手上持有股票的累计最大收益
// dp[i][1]: 手上不持有股票,并且进入冷冻期中的累计最大收益
// dp[i][2]: 手上不持有股票,并且不进入冷冻期中的累计最大收益
int[][] dp = new int[n][3];
dp[0][0] = -prices[0];
for (int i = 1; i < n; i++) {
dp[i][0] = Math.max(dp[i - 1][0], dp[i - 1][2] - prices[i]);
dp[i][1] = dp[i - 1][0] + prices[i];
dp[i][2] = Math.max(dp[i - 1][1], dp[i - 1][2]);
}
return Math.max(dp[n - 1][1], dp[n - 1][2]);
}
}
3 复杂性分析
- 时间复杂度:O(n);
- 空间复杂度:O(n);
*最长递增子序列
1 题目描述
给你一个整数数组 nums ,找到其中最长严格递增子序列的长度。
子序列是由数组派生而来的序列,删除(或不删除)数组中的元素而不改变其余元素的顺序。例如,[3,6,2,7] 是数组 [0,3,1,6,2,2,7] 的子序列。
示例 1:
输入:nums = [10,9,2,5,3,7,101,18]
输出:4
解释:最长递增子序列是 [2,3,7,101],因此长度为4。
示例 2:
输入:nums = [0,1,0,3,2,3]
输出:4
示例 3:
输入:nums = [7,7,7,7,7,7,7]
输出:1
提示:
- 1 <= nums.length <= 2500
- -104 <= nums[i] <= 104
进阶:
- 你可以设计时间复杂度为 O(n2) 的解决方案吗?
- 你能将算法的时间复杂度降低到 O(n log(n)) 吗?
2 解题(Java)
解题思路:动态规划+二分查找
-
新建数组 cell,用于保存最长上升子序列;
-
对原序列进行遍历,将每位元素二分插入 cell 中:
- 如果 cell 中元素都比它小,将它插到最后;
- 否则,用它覆盖掉大于等于它的元素中最小的那个;
-
cell 未必是真实的最长上升子序列,但长度是对的;
代码
class Solution {
public int lengthOfLIS(int[] nums) {
List<Integer> cell = new ArrayList<>();
cell.add(nums[0]);
for (int i=1; i<nums.length; i++) {
if (nums[i] > cell.get(cell.size()-1)) {
cell.add(nums[i]);
} else {
int left = 0, right = cell.size() - 1;
while (left < right) {
int mid = (left + right) / 2;
if (cell.get(mid) < nums[i]) left = mid + 1;
else right = mid;
}
cell.remove(left);
cell.add(left, nums[i]);
}
}
return cell.size();
}
}
3 复杂性分析
- 时间复杂度:O(nlog(n)) ;
- 空间复杂度:O(n);
*完全平方数
1 题目描述
给定正整数 n,找到若干个完全平方数(比如 1, 4, 9, 16, …)使得它们的和等于 n。你需要让组成和的完全平方数的个数最少。
给你一个整数n,返回和为n的完全平方数的最少数量 。
完全平方数 是一个整数,其值等于另一个整数的平方;换句话说,其值等于一个整数自乘的积。例如,1、4、9 和 16 都是完全平方数,而 3 和 11 不是。
示例 1:
输入:n = 12
输出:3
解释:12 = 4 + 4 + 4
示例 2:
输入:n = 13
输出:2
解释:13 = 4 + 9
提示:
- 1 <= n <= 104
2 解题(Java)
动态规划:
class Solution {
public int numSquares(int n) {
int[] dp = new int[n + 1];
Arrays.fill(dp, Integer.MAX_VALUE);
dp[0] = 0;
for (int i = 1; i <= n; i++) {
for (int j = 1; j * j <= i; j++) {
dp[i] = Math.min(dp[i], dp[i - j * j] + 1);
}
}
return dp[n];
}
}
3 复杂性分析
- 时间复杂度:O(n n \sqrt{n} n);
- 空间复杂度:O(n);
*最大正方形
1 题目描述
在一个由 ‘0’ 和 ‘1’ 组成的二维矩阵内,找到只包含 ‘1’ 的最大正方形,并返回其面积。
示例 1:
输入:matrix =
[[“1”,“0”,“1”,“0”,“0”],[“1”,“0”,“1”,“1”,“1”],[“1”,“1”,“1”,“1”,“1”],[“1”,“0”,“0”,“1”,“0”]]
输出:4
示例 2:
输入:matrix = [[“0”,“1”],[“1”,“0”]]
输出:1
示例 3:
输入:matrix = [[“0”]]
输出:0
提示:
- m == matrix.length
- n == matrix[i].length
- 1 <= m, n <= 300
- matrix[i][j] 为 ‘0’ 或 ‘1’
2 解题(Java)
动态规划:
class Solution {
public int maximalSquare(char[][] matrix) {
if (matrix == null || matrix.length == 0 || matrix[0].length == 0) {
return 0;
}
int maxSide = 0;
int rows = matrix.length, columns = matrix[0].length;
int[][] dp = new int[rows][columns];
for (int i=0; i<rows; i++) {
for (int j=0; j<columns; j++) {
if (matrix[i][j] == '1') {
if (i == 0 || j == 0) dp[i][j] = 1;
else dp[i][j] = Math.min(Math.min(dp[i-1][j], dp[i][j-1]), dp[i-1][j-1]) + 1;
}
maxSide = Math.max(maxSide, dp[i][j]);
}
}
return maxSide * maxSide;
}
}
3 复杂性分析
- 时间复杂度O(mn):其中 m 和 n 是矩阵的行数和列数。需要遍历原始矩阵中的每个元素计算 dp 的值;
- 空间复杂度O(mn):其中 m 和 n 是矩阵的行数和列数。创建了一个和原始矩阵大小相同的矩阵 dp;
*打家劫舍
1 题目描述
你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。
给定一个代表每个房屋存放金额的非负整数数组,计算你不触动警报装置的情况下,一夜之内能够偷窃到的最高金额。
示例 1:
输入:[1,2,3,1]
输出:4
解释:偷窃 1 号房屋 (金额 = 1) ,然后偷窃 3 号房屋 (金额 = 3)。
偷窃到的最高金额 = 1 + 3 = 4 。
示例 2:
输入:[2,7,9,3,1]
输出:12
解释:偷窃 1 号房屋 (金额 = 2), 偷窃 3 号房屋 (金额 = 9),接着偷窃 5 号房屋 (金额 = 1)。
偷窃到的最高金额 = 2 + 9 + 1 = 12 。
提示:
- 1 <= nums.length <= 100
- 0 <= nums[i] <= 400
2 解题(Java)
class Solution {
public int rob(int[] nums) {
int prev_prev = 0;
int prev = 0;
// 每次循环,计算“偷到当前房子为止的最大金额”
for (int num : nums) {
// 循环开始时,prev表示 dp[k-1],prev_prev表示 dp[k-2]
// dp[k] = max{dp[k-1], dp[k-2] + num}
int temp = Math.max(prev, prev_prev + num);
prev_prev = prev;
prev = temp;
// 循环结束时,prev 表示 dp[k],prev_prev 表示 dp[k-1]
}
return prev;
}
}
3 复杂性分析
- 时间复杂度:O(n)
- 空间复杂度:O(1)
*区域和检索 - 数组不可变
1 题目描述
给定一个整数数组 nums,求出数组从索引 i 到 j(i ≤ j)范围内元素的总和,包含 i、j 两点。
实现 NumArray 类:
- NumArray(int[] nums) 使用数组 nums 初始化对象;
- int sumRange(int i, int j) 返回数组 nums 从索引 i 到 j(i ≤ j)范围内元素的总和,包含 i、j 两点(也就是 sum(nums[i], nums[i + 1], … , nums[j]));
示例:
输入:
[“NumArray”, “sumRange”, “sumRange”, “sumRange”]
[[[-2, 0, 3, -5, 2, -1]], [0, 2], [2, 5], [0, 5]]
输出:
[null, 1, -1, -3]解释:
NumArray numArray = new NumArray([-2, 0, 3, -5, 2, -1]);
numArray.sumRange(0, 2); // return 1 ((-2) + 0 + 3)
numArray.sumRange(2, 5); // return -1 (3 + (-5) + 2 + (-1))
numArray.sumRange(0, 5); // return -3 ((-2) + 0 + 3 + (-5) + 2 + (-1))
提示:
- 0 <= nums.length <= 10 ^ 4
- -10 ^ 5 <= nums[i] <= 10 ^ 5
- 0 <= i <= j < nums.length
- 最多调用 10 ^ 4 次 sumRange 方法
2 解题(Java)
前缀和:
class NumArray {
int[] preSum;
public NumArray(int[] nums) {
int n = nums.length;
preSum = new int[n + 1];
for (int i = 0; i < n; i++) {
preSum[i + 1] = preSum[i] + nums[i];
}
}
public int sumRange(int left, int right) {
return preSum[right + 1] - preSum[left];
}
}
3 复杂性分析
- 时间复杂度:初始化 O(n),每次检索 O(1);
- 空间复杂度:O(n);
*乘积最大子数组
1 题目描述
给你一个整数数组 nums ,请你找出数组中乘积最大的连续子数组(该子数组中至少包含一个数字),并返回该子数组所对应的乘积。
示例 1:
输入: [2,3,-2,4]
输出: 6
解释: 子数组 [2,3] 有最大乘积 6。
示例 2:
输入: [-2,0,-1]
输出: 0
解释: 结果不能为 2, 因为 [-2,-1] 不是子数组。
2 解题(Java)
class Solution {
public int maxProduct(int[] nums) {
int maxF = nums[0], minF = nums[0], ans = nums[0];
for (int i = 1; i < nums.length; i++) {
int mx = maxF, mn = minF;
maxF = Math.max(mx * nums[i], Math.max(nums[i], mn * nums[i]));
minF = Math.min(mn * nums[i], Math.min(nums[i], mx * nums[i]));
ans = Math.max(maxF, ans);
}
return ans;
}
}
3 复杂性分析
- 时间复杂度O(N)
- 空间复杂度O(1)
*单词拆分
1 题目描述
给定一个非空字符串 s 和一个包含非空单词的列表 wordDict,判定 s 是否可以被空格拆分为一个或多个在字典中出现的单词。
说明:
- 拆分时可以重复使用字典中的单词。
- 你可以假设字典中没有重复的单词。
示例 1:
输入: s = “leetcode”, wordDict = [“leet”, “code”]
输出: true
解释: 返回 true 因为 “leetcode” 可以被拆分成 “leet code”。
示例 2:
输入: s = “applepenapple”, wordDict = [“apple”, “pen”]
输出: true
解释: 返回 true 因为 “applepenapple” 可以被拆分成 “apple pen apple”。 注意你可以重复使用字典中的单词。
示例 3:
输入: s = “catsandog”, wordDict = [“cats”, “dog”, “sand”, “and”, “cat”]
输出: false
2 解题(Java)
解题思路
- 初始化dp=[False,⋯,False],长度为 n+1。n 为字符串长度。dp[i] 表示 s 的前 i 位是否可以用 wordDict 中的单词表示;
- 初始化 dp[0]=True,假定空字符串可以被表示;
- 遍历字符串的所有子串,遍历开始索引 i,遍历区间 [0,n):
- 遍历结束索引 j,遍历区间 [i+1,n+1):若 dp[i]=True 且 s[i,⋯,j) 在 wordlist 中:dp[j]=True。解释:dp[i]=True 说明 s 的前 i 位可以用 wordDict 表示,且 s[i,⋯,j) 出现在 wordDict中,说明 s 的前 j 位可以表示;
- 返回dp[n];
代码
public class Solution {
public boolean wordBreak(String s, List<String> wordDict) {
// dp[i]表示s的前i个位是否可以用wordDict中的单词表示
boolean[] dp = new boolean[s.length() + 1];
// 设定边界条件,假定空字符串可以被表示
dp[0] = true;
// 遍历字符串的所有子串,遍历开始索引i,遍历区间[0,n)
for (int i = 0; i < s.length(); i++) {
// 遍历结束索引j,遍历区间[i+1,n+1)
for (int j = i+1; j <= s.length(); j++) {
if (dp[i] && wordDict.contains(s.substring(i, j))) {
dp[j] = true;
}
}
}
return dp[s.length()];
}
}
3 复杂性分析
- 时间复杂度O(n ^ 2);
- 空间复杂度O(n);
数字序列中某一位的数字*
1 题目描述
数字以0123456789101112131415…的格式序列化到一个字符序列中。在这个序列中,第5位(从下标0开始计数)是5,第13位是1,第19位是4,等等。
请写一个函数,求任意第n位对应的数字。
示例 1:
输入:n = 3
输出:3
示例 2:
输入:n = 11
输出:0
限制:
0 <= n < 2 ^ 31
2 解题Java
2.1 解题思路
2.1.1 定义变量
- 将101112⋯ 中的每一位称为数位,记为n;
- 将10,11,12,⋯ 称为数字,记为num;
- 数字10是一个两位数,称此数字的位数为2,记为digit;
- 每digit位数的起始数字(即:1,10,100,⋯),记为 start;(为方便处理,暂忽略0)
- 各digit下的数位数量count = 9×start×digit;
根据以上分析,可将求解分为3步:
- 确定n所在数字的位数,记为digit;
- 确定n所在的数字,记为num;
- 确定n是num中的哪一数位,并返回结果;
2.1.2 确定n所在数字的位数digit
- 循环执行 n 减去 一位数、两位数、… 的数位数量 count ,直至n ≤ count 时跳出;
- 由于 n 已经减去了一位数、两位数、…、(digit−1) 位数的数位数量count,因而此时的 n 是从起始数字 start 开始计数的;
digit, start, count = 1, 1, 9
while n > count:
n -= count
start *= 10 # 1, 10, 100, ...
digit += 1 # 1, 2, 3, ...
count = 9 * start * digit # 9, 180, 2700, ...
2.1.3 确定n所在的数字num
num = start + (n - 1) / digit
2.1.4 确定n是num中的哪一数位
res = Long.toString(num).charAt((n - 1) % digit) - '0';
2.2 代码
class Solution {
public int findNthDigit(int n) {
int digit = 1;
long start = 1;
long count = 9;
while (n > count) {
n -= count;
digit += 1;
start *= 10;
count = digit * start * 9;
}
long num = start + (n - 1) / digit;
return String.valueOf(num).charAt((n - 1) % digit) - '0';
}
}
3 复杂性分析
- 时间复杂度 O(log N):第1步最多循环 O(log N) 次,第3步中将 num 转化为字符串使用 O(log N) 时间,因此总体为 O(log N);
- 空间复杂度 O(log N) : 将数字 num 转化为字符串占用 O(log N) 的额外空间;
不用加减乘除做加法*
1 题目描述
写一个函数,求两个整数之和,要求在函数体内不得使用 “+”、“-”、“*”、“/” 四则运算符号。
示例:
输入: a = 1, b = 1
输出: 2
提示:
a, b 均可能是负数或 0
结果不会溢出 32 位整数
2 解题(Java)
2.1 解题思路
- 设两数字的二进制形式 a, b,其求和 s = a + b,a(i)代表 a 的二进制第 i 位,则分为以下四种情况:
a(i) | b(i) | 无进位和n(i) | 进位c(i+1) |
---|---|---|---|
0 | 0 | 0 | 0 |
0 | 1 | 1 | 0 |
1 | 0 | 1 | 0 |
1 | 1 | 0 | 1 |
- 观察发现,无进位和与异或运算规律相同,进位与与运算规律相同(并需左移一位)。因此,无进位和n与进位c的计算公式如下:
- n = a ^ b(无进位和,异或运算)
- c = a&b<<1(进位,与运算 + 左移一位)
- (和s)=(无进位和n) + (进位c)。即可将s = a + b转化为:s = n + c;
- 循环求n和c,直至进位c=0;此时s = n,返回n即可;
问:若数字a和b中有负数,则变成了减法,如何处理?
答:在计算机系统中,数值一律用补码来表示和存储。补码的优势:加法、减法可以统一处理(CPU只有加法器)。因此,以上方法同时适用于正数和负数的加法。
2.2 代码
class Solution {
public int add(int a, int b) {
while(b != 0) { // 当进位为 0 时跳出
int c = (a & b) << 1; // c = 进位
a ^= b; // a = 非进位和
b = c;
}
return a;
}
}
3 复杂性分析
- 时间复杂度 O(1): 最差情况下(例如 a= 0x7fffffff , b=1 时),需循环 32 次,使用 O(1) 时间;每轮中的常数次位操作使用 O(1) 时间;
- 空间复杂度 O(1) : 辅助变量c使用常数大小的额外空间;
*二进制中1的个数
1 题目描述
请实现一个函数,输入一个整数,输出该数二进制表示中 1 的个数。例如,把 9 表示成二进制是 1001,有 2 位是 1。因此,如果输入 9,则该函数输出 2。
示例 1:
输入:00000000000000000000000000001011
输出:3
示例 2:
输入:11111111111111111111111111111101
输出:31
2 解题(Java)
方法:逐位判断
根据 与运算 定义,设二进制数字 n ,则有:
- 若 n&1=0 ,则 n 二进制 最右一位 为 0 ;
- 若 n&1=1 ,则 n 二进制 最右一位 为 1 。
算法流程:
- 初始化数量统计变量 res = 0。
- 循环逐位判断: 当 n = 0 时跳出。
- res += n & 1 : 若 n&1=1 ,则统计数 res 加1。
- n >>>= 1 : 将二进制数字 n 无符号右移一位( Java 中无符号右移为 “>>>” ) 。
- 返回统计数量 res。
代码:
public class Solution {
public int hammingWeight(int n) {
int res = 0;
while(n != 0) {
res += n & 1;
n >>>= 1;
}
return res;
}
}
3 复杂性分析
- 时间复杂度 O(log2n): 此算法循环内部仅有移位、与、加等基本运算,占用 O(1) ;逐位判断需循环log2 n 次,其中log2 n 代表数字n最高位1的所在位数(例如 log 2 4=2, log2 16=4);
- 空间复杂度 O(1) : 变量 res 使用常数大小额外空间;
*不同的二叉搜索树
1 题目描述
给定一个整数 n,求以 1 … n 为节点组成的二叉搜索树有多少种?
示例:
输入: 3
输出: 5
解释:
给定 n = 3, 一共有 5 种不同结构的二叉搜索树:
2 解题(Java)
动态规划:
class Solution {
public int numTrees(int n) {
int[] dp = new int[n+1];//dp[n]代表节点数为n所能构成的二叉搜索树的数目
dp[0] = 1; dp[1] = 1;
for (int i=2; i<=n; i++) {
for (int j=1; j<=i; j++) {
dp[i] += dp[j-1] * dp[i-j]; // 以j作为根节点
}
}
return dp[n];
}
}
3 复杂性分析
- 时间复杂度O(n ^ 2):n 为二叉搜索树的节点个数,共 n 个值需要求解,每个值求解平均需要 O(n) 时间复杂度,因此总时间复杂度为 O(n ^ 2);
- 空间复杂度O(n):需要 O(n) 空间存储 dp 数组;
*编辑距离
1 题目描述
给你两个单词 word1 和 word2,请你计算出将 word1 转换成 word2 所使用的最少操作数 。
你可以对一个单词进行如下三种操作:
- 插入一个字符
- 删除一个字符
- 替换一个字符
示例 1:
输入:word1 = “horse”, word2 = “ros”
输出:3
解释:
horse -> rorse (将 ‘h’ 替换为’r’)
rorse -> rose (删除 ‘r’)
rose -> ros (删除 ‘e’)
示例 2:
输入:word1 = “intention”, word2 = “execution”
输出:5
解释:
intention ->inention (删除 ‘t’)
inention -> enention (将 ‘i’ 替换为 ‘e’)
enention ->exention (将 ‘n’ 替换为 ‘x’)
exention -> exection (将 ‘n’ 替换为 ‘c’)
exection -> execution (插入 ‘u’)
提示:
- 0 <= word1.length, word2.length <= 500
- word1 和 word2 由小写英文字母组成
2 解题(Java)
解题思路:
- dp[i][j] 代表 word1 前i个字符转换成 word2 前j个字符需要的最少步数;
- 当 word1[i] == word2[j],dp[i][j] = dp[i-1][j-1];
- 当 word1[i] != word2[j],dp[i][j] = min(dp[i-1][j-1], dp[i-1][j], dp[i][j-1]) + 1(其中,dp[i-1][j-1] 表示替换操作,dp[i-1][j] 表示删除操作,dp[i][j-1] 表示插入操作(插入j抵消j,于是j-1));
- 针对第一行,第一列要单独考虑;
- 第一行,是 word1 为空变成 word2 最少步数,即插入操作;
- 第一列,是 word2 为空,需要的最少步数,即删除操作;
代码
class Solution {
public int minDistance(String word1, String word2) {
int m = word1.length();
int n = word2.length();
int[][] dp = new int[m+1][n+1];
// 计算边界值第一行
for (int i = 1; i <= n; i++) dp[0][i] = dp[0][i - 1] + 1;
// 计算边界值第一列
for (int i = 1; i <= m; i++) dp[i][0] = dp[i - 1][0] + 1;
// 计算其他位置
for (int i = 1; i <= m; i++) {
for (int j = 1; j <= n; j++) {
if (word1.charAt(i - 1) == word2.charAt(j - 1)) dp[i][j] = dp[i - 1][j - 1];
else dp[i][j] = Math.min(Math.min(dp[i - 1][j - 1], dp[i][j - 1]), dp[i - 1][j]) + 1;
}
}
return dp[m][n];
}
}
3 复杂性分析
- 时间复杂度O(mn) :其中 m 为 word1 长度,n 为 word2 长度,双层for循环时间复杂度O(mn);
- 空间复杂度O(mn) :需要大小为 O(mn)数组来记录状态值;
*爬楼梯
1 题目描述
假设你正在爬楼梯,需要 n 阶你才能到达楼顶。
每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢?
注意:给定 n 是一个正整数。
示例 1:
输入: 2 输出: 2 解释: 有两种方法可以爬到楼顶。
- 1 阶 + 1 阶
- 2 阶
示例 2:
输入: 3 输出: 3 解释: 有三种方法可以爬到楼顶。
- 1 阶 + 1 阶 + 1 阶
- 1 阶 + 2 阶
- 2 阶 + 1 阶
2 解题(Java)
动态规划:
f(x)=f(x−1)+f(x−2)
class Solution {
public int climbStairs(int n) {
int pre = 1, cur = 1;
for (int i=2; i<=n; i++) {
int temp = pre + cur;
pre = cur;
cur = temp;
}
return cur;
}
}
3 复杂性分析
- 时间复杂度O(n):循环执行 O(n) 次,每次常数时间;
- 空间复杂度O(1):几个变量占用常数大小的额外空间;
*最小路径和
1 题目描述
给定一个包含非负整数的 m x n 网格 grid ,请找出一条从左上角到右下角的路径,使得路径上的数字总和为最小。
说明:每次只能向下或者向右移动一步。
示例 1:
输入:grid = [[1,3,1],[1,5,1],[4,2,1]]
输出:7
解释:因为路径 1→3→1→1→1 的总和最小。
示例 2:
输入:grid = [[1,2,3],[4,5,6]]
输出:12
提示:
- m == grid.length
- n == grid[i].length
- 1 <= m, n <= 200
- 0 <= grid[i][j] <= 100
2 解题(Java)
动态规划:
class Solution {
public int minPathSum(int[][] grid) {
int[][] dp = new int[grid.length][grid[0].length];
dp[0][0] = grid[0][0];
for (int i=1; i<grid.length; i++) {
dp[i][0] = grid[i][0] + dp[i-1][0];
}
for (int i = 1; i<grid[0].length; i++) {
dp[0][i] = grid[0][i] + dp[0][i-1];
}
for (int i = 1; i<grid.length; i++) {
for (int j=1; j<grid[0].length; j++) {
dp[i][j] = Math.min(dp[i-1][j], dp[i][j-1]) + grid[i][j];
}
}
return dp[grid.length-1][grid[0].length-1];
}
}
3 复杂性分析
- 时间复杂度O(mn):其中 m 和 n 分别是网格的行数和列数,需要对整个网格遍历一次;
- 空间复杂度O(1):原地修改;
*跳跃游戏
1 题目描述
给定一个非负整数数组 nums ,你最初位于数组的第一个下标。
数组中的每个元素代表你在该位置可以跳跃的最大长度。
判断你是否能够到达最后一个下标。
示例 1:
输入:nums = [2,3,1,1,4]
输出:true
解释:可以先跳 1 步,从下标 0 到达下标 1, 然后再从下标 1 跳 3 步到达最后一个下标。
示例 2:
输入:nums = [3,2,1,0,4]
输出:false
解释:无论怎样,总会到达下标为 3 的位置。但该下标的最大跳跃长度是 0 , 所以永远不可能到达最后一个下标。
提示:
- 1 <= nums.length <= 3 * 10 ^ 4
- 0 <= nums[i] <= 10 ^ 5
2 解题(Java)
动态规划:
- 依次遍历数组中的每一个位置,并实时维护 最远可以到达的位置rightmost,rightmost代表当前位置下可以到达的最远位置;
- 对于当前遍历到的位置 x,如果它在 rightmost 的范围内,可通过Math.max(rightmost, i + nums[i]) 更新 rightmost(动规思想);如果不在rightmost的范围内,那么x之后的位置都不可达,可提前返回false;
- 在遍历的过程中,如果 rightmost 大于等于数组的最后一个位置,那就说明最后一个位置可达,可提前返回 true ;
class Solution {
public boolean canJump(int[] nums) {
int righMost = 0;
for (int i=0; i<nums.length; i++) {
if (i <= righMost) {
righMost = Math.max(righMost, i+nums[i]);
if (righMost >= nums.length-1) {
return true;
}
} else {
return false;
}
}
return false;
}
}
3 复杂性分析
- 时间复杂度O(n):其中 n 为数组长度,最多遍历一遍数组即可求得答案;
- 空间复杂度O(1):只需常数空间存放若干变量;
*最大子序和
1 题目描述
给定一个整数数组 nums ,找到一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。
示例 1:
输入:nums = [-2,1,-3,4,-1,2,1,-5,4]
输出:6
解释:连续子数组 [4,-1,2,1] 的和最大,为 6 。
示例 2:
输入:nums = [1]
输出:1
示例 3:
输入:nums = [0]
输出:0
示例 4:
输入:nums = [-1]
输出:-1
示例 5:
输入:nums = [-100000]
输出:-100000
提示:
- 1 <= nums.length <= 3 * 10 ^ 4
- -10 ^ 5 <= nums[i] <= 10 ^ 5
2 解题(Java)
动态规划:
- 我们用 f(i) 代表以第 i 个数结尾的「连续子数组的最大和」,可以给出一个时间复杂度 O(n)、空间复杂度 O(n) 的实现,即用一个 f 数组来保存 f(i) 的值,用一个循环求出所有 f(i),f(n-1)即为最终解;
- 考虑到 f(i)只和 f(i-1) 相关,于是我们可以只用一个变量 pre 来维护对于当前 f(i) 来说的 f(i−1)的值,从而让空间复杂度降低到 O(1),这有点类似「滚动数组」的思想;
class Solution {
public int maxSubArray(int[] nums) {
int pre = 0, maxAns = Integer.MIN_VALUE;
for (int x : nums) {
pre = Math.max(pre + x, x);
maxAns = Math.max(maxAns, pre);
}
return maxAns;
}
}
3 复杂性分析
- 时间复杂度O(n):其中 n 为 nums 数组的长度,只需遍历一遍数组即可求得答案;
- 空间复杂度O(1):只需常数空间存放若干变量;
*最长有效括号
1 题目描述
给你一个只包含 ‘(’ 和 ‘)’ 的字符串,找出最长有效(格式正确且连续)括号子串的长度。
示例 1:
输入:s = “(()”
输出:2
解释:最长有效括号子串是 “()”
示例 2:
输入:s = “)()())”
输出:4
解释:最长有效括号子串是 “()()”
示例 3:
输入:s = “”
输出:0
提示:
0 <= s.length <= 3 * 104
s[i] 为 ‘(’ 或 ‘)’
2 解题(Java)
动态规划:定义dp[i] 表示以下标 i 字符结尾的最长有效括号的长度.
class Solution {
public int longestValidParentheses(String s) {
int len = s.length();
int[] dp = new int[len];
int max = 0;
for (int i = 1; i < len; i++) {
if (s.charAt(i) == ')') {
if (s.charAt(i-1) == '(') {
dp[i] = i < 2 ? 2 : dp[i-2] + 2;
} else if (i - dp[i-1] - 1 >= 0 && s.charAt(i - dp[i -1] - 1) == '(') {
dp[i] = 2 + dp[i - 1] + ((i - dp[i - 1] - 2) >= 0 ? dp[i - dp[i - 1] - 2] : 0);
}
max = Math.max(max, dp[i]);
}
}
return max;
}
}
3 复杂性分析
- 时间复杂度O(N):只需遍历整个字符串一次,即可将 dp 数组求出来;
- 空间复杂度O(N):需要一个大小为 n 的 dp 数组;
*礼物的最大价值
1 题目描述
在一个 m*n 的棋盘的每一格都放有一个礼物,每个礼物都有一定的价值(价值大于 0)。你可以从棋盘的左上角开始拿格子里的礼物,并每次向右或者向下移动一格、直到到达棋盘的右下角。给定一个棋盘及其上面的礼物的价值,请计算你最多能拿到多少价值的礼物?
示例 1:
提示:
- 0 < grid.length <= 200
- 0 < grid[0].length <= 200
2 解题(Java)
动态规划:
class Solution {
public int maxValue(int[][] grid) {
int[][] dp = new int[grid.length][grid[0].length];
dp[0][0] = grid[0][0];
for (int i=1; i<grid.length; i++) {
dp[i][0] = grid[i][0] + dp[i-1][0];
}
for (int i=1; i<grid[0].length; i++) {
dp[0][i] = grid[0][i] + dp[0][i-1];
}
for (int i=1; i<grid.length; i++) {
for (int j=1; j<grid[0].length; j++) {
dp[i][j] = Math.max(dp[i-1][j] ,dp[i][j-1]) + grid[i][j];
}
}
return dp[dp.length-1][dp[0].length-1];
}
}
3 复杂性分析
- 时间复杂度 O(MN) : M,N 分别为矩阵行高、列宽;动态规划需遍历整个 grid 矩阵,使用 O(MN) 时间;
- 空间复杂度 O(1) : 原地修改使用常数大小的额外空间;
*最长公共子串(牛客网)
1 题目描述
给定两个字符串str1和str2,输出两个字符串的最长公共子串。
题目保证str1和str2的最长公共子串存在且唯一。
示例1
输入
“1AB2345CD”,“12345EF”
返回值
“2345”
备注:
1 <= len(str1),len(str2) <= 5000
2 解题(Java)
import java.util.*;
class Solution {
/**
* longest common substring
* @param str1 string字符串 the string
* @param str2 string字符串 the string
* @return string字符串
*/
public String LCS(String str1, String str2) {
int m = str1.length();
int n = str2.length();
// dp[i][j] str1前i个字符和str2前j个字符(子字符串以str1[i-1]和str2[j-1]结尾)的最长公共子串长度
int[][] dp = new int[m+1][n+1];
int maxLen = 0, end = 0;
//开始填表
for(int i = 1; i <= m; i++) {
for(int j = 1; j <= n; j++) {
if(str1.charAt(i-1) == str2.charAt(j-1)) dp[i][j] = dp[i-1][j-1] + 1;
else dp[i][j] = 0;
if(dp[i][j] > maxLen) {
maxLen = dp[i][j];
end = i - 1;
}
}
}
return str1.substring(end-maxLen+1, end+1);
}
}
3 复杂性分析
- 时间复杂度O(MN):M、N分别为str1和str2的长度,双层for循环遍历使用O(MN)的时间复杂度;
- 空间复杂度O(MN):需建立一个M*N大小的数组;
*股票的最大利润
1 题目描述
假设把某股票的价格按照时间先后顺序存储在数组中,请问买卖该股票一次可能获得的最大利润是多少?
示例 1:
输入: [7,1,5,3,6,4]
输出: 5
解释: 在第 2 天(股票价格 = 1)的时候买入,在第 5 天(股票价格 = 6)的时候卖出,最大利润 = 6-1 = 5 。
注意利润不能是 7-1 = 6, 因为卖出价格需要大于买入价格。
示例 2:
输入: [7,6,4,3,1]
输出: 0
解释: 在这种情况下, 没有交易完成, 所以最大利润为 0。
限制:
0 <= 数组长度 <= 10^5
2 解题(Java)
动态规划:
class Solution {
public int maxProfit(int[] prices) {
int cost = Integer.MAX_VALUE, profit = 0;
for (int price : prices) {
cost = Math.min(cost, price);
profit = Math.max(profit, price - cost);
}
return profit;
}
}
3 复杂性分析
- 时间复杂度 O(N) : 其中 N 为数组prices的长度,动态规划需遍历prices ;
- 空间复杂度 O(1) : 变量cost和profit占用常数大小的空间;
*构建乘积数组
1 题目描述
给定一个数组 A[0,1,…,n-1],请构建一个数组 B[0,1,…,n-1],其中 B 中的元素 B[i]=A[0]×A[1]×…×A[i-1]×A[i+1]×…×A[n-1]。不能使用除法。
示例:
输入: [1,2,3,4,5]
输出: [120,60,40,30,24]
提示:
- 所有元素乘积之和不会溢出 32 位整数
- a.length <= 100000
2 解题(Java)
- 定义数组b,b[0] = 1;
- 计算b[i]正三角各元素的乘积;
- 定义赋值变量tmp = 1,计算b[i]倒三角各元素的乘积;
- 返回b;
class Solution {
public int[] constructArr(int[] a) {
if(a.length == 0) return new int[0];
int[] b = new int[a.length];
//正三角
b[0] = 1;
for(int i = 1; i < a.length; i++) {
b[i] = b[i - 1] * a[i - 1];
}
//倒三角
int tmp = 1;
for(int i = a.length - 2; i >= 0; i--) {
tmp *= a[i + 1];
b[i] *= tmp;
}
return b;
}
}
3 复杂性分析
- 时间复杂度 O(N): 其中 N 为数组长度,两轮遍历数组 a ,使用 O(N) 时间;
- 空间复杂度 O(1) : 变量 tmp 使用常数大小额外空间(数组 b 作为返回值,不计入复杂度考虑);
*把数字翻译成字符串
1 题目描述
给定一个数字,我们按照如下规则把它翻译为字符串:0 翻译成 “a” ,1 翻译成 “b”,……,11 翻译成 “l”,……,25 翻译成 “z”。一个数字可能有多个翻译。请编程实现一个函数,用来计算一个数字有多少种不同的翻译方法。
示例 1:
输入: 12258
输出: 5
解释: 12258有5种不同的翻译,分别是"bccfi", “bwfi”, “bczi”, “mcfi"和"mzi”
提示:
0 <= num < 2^31
2 解题(Java)
动态规划:
class Solution {
public int translateNum(int num) {
int curRes = 1, preRes = 1, preDigit = num % 10;
while (num != 0) {
num /= 10;
int curDigit = num % 10;
int judge = 10 * curDigit + preDigit;
int res = (judge >= 10 && judge <= 25) ? curRes + preRes : curRes;
preRes = curRes;
curRes = res;
preDigit = curDigit;
}
return curRes;
}
}
3 复杂性分析
- 时间复杂度 O(lgN) : 即数字 num 的位数,决定了循环次数;
- 空间复杂度 O(1) : 几个变量占用常数大小的额外空间;
*最长不含重复字符的子字符串
1 题目描述
请从字符串中找出一个最长的不包含重复字符的子字符串,计算该最长子字符串的长度。
示例 1:
输入: “abcabcbb”
输出: 3
解释: 因为无重复字符的最长子串是 “abc”,所以其长度为 3。
示例 2:
输入: “bbbbb”
输出: 1
解释: 因为无重复字符的最长子串是 “b”,所以其长度为 1。
示例 3:
输入: “pwwkew”
输出: 3
解释: 因为无重复字符的最长子串是 “wke”,所以其长度为 3。请注意,你的答案必须是 子串 的长度,“pwke” 是一个子序列,不是子串。
提示:
s.length <= 40000
2 解题(Java)
class Solution {
public int lengthOfLongestSubstring(String s) {
Map<Character, Integer> dic = new HashMap<>();
int res = 0, tmp = 0;
for(int right = 0; right < s.length(); right++) {
int left = dic.getOrDefault(s.charAt(right), -1); // 获取索引left
dic.put(s.charAt(right), right); // 更新哈希表
tmp = tmp < right - left ? tmp + 1 : right - left; // dp[right-1] -> dp[right]
res = Math.max(res,tmp);
}
return res;
}
}
3 复杂性分析
- 时间复杂度 O(N) : 其中 N 为字符串长度,动态规划需遍历计算字符串各字符;
- 空间复杂度 O(1) : 字符的 ASCII 码范围为 0 ~ 127 ,哈希表 dic 最多使用 O(128)=O(1)大小的额外空间;
*接雨水
1 题目描述
给定 n 个非负整数表示每个宽度为 1 的柱子的高度图,计算按此排列的柱子,下雨之后能接多少雨水。
示例 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 个单位的雨水(蓝色部分表示雨水)。
2 解题(Java)
class Solution {
public int trap(int[] height) {
if (height==null || height.length==0) {
return 0;
}
int ans = 0;
int size = height.length;
int[] leftMax = new int[size];
int[] rightMax = new int[size];
leftMax[0] = height[0];
for (int i=1; i<size; i++) {
leftMax[i] = Math.max(height[i], leftMax[i-1]);
}
rightMax[size-1] = height[size-1];
for (int i=size-2; i>=0; i--) {
rightMax[i] = Math.max(height[i], rightMax[i+1]);
}
for (int i=1; i<size-1; i++) {
ans += Math.min(leftMax[i], rightMax[i]) - height[i];
}
return ans;
}
}
3 复杂性分析
- 时间复杂度O(N):存储最大高度数组,需要两次遍历,每次 O(n) ;最终使用存储的数据更新ans,O(n);
- 空间复杂度O(N) :使用了额外的 O(n)空间用来放置 left_max 和 right_max 数组;
*斐波那契数列
1 题目描述
写一个函数,输入 n ,求斐波那契(Fibonacci)数列的第 n 项。斐波那契数列的定义如下:
F(0) = 0, F(1) = 1
F(N) = F(N - 1) + F(N - 2), 其中 N > 1.
斐波那契数列由 0 和 1 开始,之后的斐波那契数就是由之前的两数相加而得出。
答案需要取模 1e9+7(1000000007),如计算初始结果为:1000000008,请返回 1。
示例 1:
输入:n = 2
输出:1
示例 2:
输入:n = 5
输出:5
2 解题(Java)
class Solution {
public int fib(int n) {
if(n == 0) return 0;
if(n == 1) return 1;
int pre = 0, cur = 1;
for (int i = 2; i <= n; i++) {
int temp = (pre + cur) % 1000000007;
pre = cur;
cur = temp;
}
return cur;
}
}
3 复杂性分析
- 时间复杂度O(N):需循环n次,每轮循环内计算操作使用 O(1)。
- 空间复杂度O(1):几个标志变量占用常数大小的额外空间。
*青蛙跳台阶问题
1 题目描述
一只青蛙一次可以跳上1级台阶,也可以跳上2级台阶。求该青蛙跳上一个 n 级的台阶总共有多少种跳法。
答案需要取模 1e9+7(1000000007),如计算初始结果为:1000000008,请返回 1。
示例 1:
输入:n = 2
输出:2
示例 2:
输入:n = 7
输出:21
示例 3:
输入:n = 0
输出:1
2 解题(Java)
解题思路无限接近斐波那契数列,唯一不同是初始值不一样。
当n >= 2时:
n级有两类跳法:n-1级跳1级,以及n-2级跳2级;
因此n级跳法数 = n-1级跳法数 + n-2级跳法数。
class Solution {
public int numWays(int n) {
if (n==0 || n==1) return 1;
int pre = 1, cur = 1;
for (int i = 2; i <= n; i++) {
int temp = (pre + cur) % 1000000007;
pre = cur;
cur = temp;
}
return cur;
}
}
3 复杂性分析
- 时间复杂度O(N):需循环n次,每轮循环内计算操作使用 O(1);
- 空间复杂度O(1): 几个变量占用常数大小的额外空间;
*剪绳子
1 题目描述
给你一根长度为 n 的绳子,请把绳子剪成整数长度的 m 段(m、n都是整数,n>1并且m>1),每段绳子的长度记为 k[0],k[1]…k[m-1] 。请问 k[0]k[1]…*k[m-1] 可能的最大乘积是多少?例如,当绳子的长度是8时,我们把它剪成长度分别为2、3、3的三段,此时得到的最大乘积是18。
示例 1:
输入: 2
输出: 1
解释: 2 = 1 + 1, 1 × 1 = 1
示例 2:
输入: 10
输出: 36
解释: 10 = 3 + 3 + 4, 3 × 3 × 4 = 36
提示:
2 <= n <= 58
2 解题(动态规划)
2.1 解题思路
- 当n >= 2时,可以拆分成至少两个正整数;
- 设k是拆分出的第一个正整数,剩下的部分是n-k,n-k可以拆分,也可以不拆分;
- 创建数组dp,dp[i]表示将正整数i拆分成至少两个正整数后,这些正整数的最大乘积;
- i >= 2,假设对i拆分出的第一个正整数是j(1<=j<i),有以下两种方案:
- i - j不再拆分,乘积为j * (i - j);
- i - j继续拆分,乘积为j * dp[i - j];
- 因此当j固定时,有dp[i] = max(j * (i - j),j * dp[i - j])。由于j的范围是[1, i-1],因此需遍历所有的j才能得到dp[i]的最大值,故而得到状态转移方程如下:
- 最终得到dp[n]的值即为n拆分成至少两个正整数的和之后,这些正整数的最大乘积;
2.2 代码
class Solution {
public int cuttingRope(int n) {
int[] dp = new int[n+1];
for (int i=2; i<=n; i++) {
int curMax = 0;
for (int j=1; j<i; j++) {
curMax = Math.max(curMax, Math.max(j * (i - j), j * dp[i - j]));
}
dp[i] = curMax;
}
return dp[n];
}
}
2.3 复杂性分析
- 时间复杂度O(N ^ 2):双层for循环使用O(N ^ 2)时间;
- 空间复杂度O(N):数组dp占用O(N)空间;
3 解题(数学)
3.1 解题思路
- 数学推论一:将绳子以相等的长度等分,得到的乘积最大;
- 数学推论二:尽可能将绳子以长度3等分时,乘积最大;
- 切分规则:当把绳子尽可能切分成多个长度为3的片段时,可能剩余一段长度为1或2的情况:如果最后一段为2,保留,不再拆分为1+1;如果最后一段为1,把一份3+1替换为2+2,因为2 * 2 > 3 * 1;
算法流程:
- 当n <= 3时,按照规则应不切分,但由于题目要求至少剪成两段,因此剪出一段长度为1的绳子,返回n-1;
- 当n > 3时,求n除以3的整数部分a和余数部分b,共有三种情况:
- b=0,直接返回3 ^ a;
- b=1,将一个1+3转换为2+2,因此返回3 a-1 * 4;
- b=2,返回3 a * 2;
3.2 代码
class Solution {
public int cuttingRope(int n) {
if(n <= 3) return n - 1;
int a = n / 3, b = n % 3;
if(b == 0) return (int)Math.pow(3, a);
if(b == 1) return (int)Math.pow(3, a - 1) * 4;
return (int)Math.pow(3, a) * 2;
}
}
3.3 复杂性分析
- 时间复杂度O(1):仅有求整、求余、次方运算;
- 空间复杂度O(1):a和b占用常数大小额外空间;
*统计字典序元音字符串的数目
1 题目描述
给你一个整数 n,请返回长度为 n 、仅由元音 (a, e, i, o, u) 组成且按 字典序排列 的字符串数量。
字符串 s 按 字典序排列 需要满足:对于所有有效的 i,s[i] 在字母表中的位置总是与 s[i+1] 相同或在 s[i+1] 之前。
示例 1:
输入:n = 1
输出:5
解释:仅由元音组成的 5 个字典序字符串为 [“a”,“e”,“i”,“o”,“u”]
示例 2:
输入:n = 2
输出:15
解释:仅由元音组成的 15 个字典序字符串为[“aa”,“ae”,“ai”,“ao”,“au”,“ee”,“ei”,“eo”,“eu”,“ii”,“io”,“iu”,“oo”,“ou”,“uu”]
注意,“ea” 不是符合题意的字符串,因为 ‘e’ 在字母表中的位置比 ‘a’ 靠后
示例 3:
输入:n = 33
输出:66045
提示:
1 <= n <= 50
2 解题(Java)
动态规划:定义dp[n+1][5]数组,其中dp[i][0-4]表示长度为i的以a-u结尾的字符串的个数。
class Solution {
public int countVowelStrings(int n) {
int[][] dp = new int[n+1][5];
//初始化n=1的情况
for (int i = 0; i < 5; i++){
dp[1][i] = 1;
}
for (int i = 2; i <= n; i++){
dp[i][0] = dp[i-1][0];
dp[i][1] = dp[i-1][0] + dp[i-1][1];
dp[i][2] = dp[i-1][0] + dp[i-1][1] + dp[i-1][2];
dp[i][3] = dp[i-1][0] + dp[i-1][1] + dp[i-1][2] + dp[i-1][3];
dp[i][4] = dp[i-1][0] + dp[i-1][1] + dp[i-1][2] + dp[i-1][3] + dp[i-1][4];
}
//最终答案求和
return dp[n][0] + dp[n][1] + dp[n][2] + dp[n][3] + dp[n][4];
}
}
3 复杂性分析
- 时间复杂度O(N):从1遍历到n;
- 空间复杂度O(N):需要借助一个int[n+1][5]的二维数组;
*丑数
1 题目描述
我们把只包含质因子 2、3 和 5 的数称作丑数(Ugly Number)。求按从小到大的顺序的第 n 个丑数。
示例:
输入: n = 10
输出: 12
解释: 1, 2, 3, 4, 5, 6, 8, 9, 10, 12 是前 10 个丑数。
说明:
- 1 是丑数。
- n 不超过1690。
2 解题(Java)
动态规划:
设已知长度为n的丑数序列x1,x2,…xn,求第n+1个丑数xn+1。根据递推性质,丑数xn+1只可能是以下三种情况之一(索引a,b,c为未知数):
由于xn+1是最接近xn的丑数,因此索引a,b,c需满足以下条件:
因此,可设置指针 a,b,c 指向首个丑数,循环根据递推公式得到下个丑数,并每轮将对应指针执行 +1 即可。
class Solution {
public int nthUglyNumber(int n) {
int preIndex2 = 0, preIndex3 = 0, preIndex5 = 0;
int[] dp = new int[n];
dp[0] = 1;
for (int i=1; i<n; i++) {
int res2 = dp[preIndex2] * 2, res3 = dp[preIndex3] * 3, res5 = dp[preIndex5] * 5;
dp[i] = Math.min(Math.min(res2, res3), res5);
if (dp[i]==res2) preIndex2++;
if (dp[i]==res3) preIndex3++;
if (dp[i]==res5) preIndex5++;
}
return dp[n-1];
}
}
3 复杂性分析
- 时间复杂度 O(N) : 其中 N=n ,动态规划需遍历计算 dp 数组;
- 空间复杂度 O(N) : 长度为 N 的 dp 列表使用 O(N) 的额外空间;
*n个骰子的点数
1 题目描述
把n个骰子扔在地上,所有骰子朝上一面的点数之和为s。输入n,打印出s的所有可能的值出现的概率。
你需要用一个浮点数数组返回答案,其中第 i 个元素代表这 n 个骰子所能掷出的点数集合中第 i 小的那个的概率。
示例 1:
输入: 1
输出: [0.16667,0.16667,0.16667,0.16667,0.16667,0.16667]
示例 2:
输入: 2
输出: [0.02778,0.05556,0.08333,0.11111,0.13889,0.16667,0.13889,0.11111,0.08333,0.05556,0.02778]
限制:
1 <= n <= 11
2 解题(Java)
class Solution {
public double[] dicesProbability(int n) {
//根据动态规划的思想分解子问题
//把n个骰子的点数分解为n-1个骰子的点数加上一个骰子的点数
double[] pre = {1/6d, 1/6d, 1/6d, 1/6d, 1/6d, 1/6d};
for(int i = 2; i <= n; i++) {
double[] temp = new double[5*i+1];
for (int j = 0; j < pre.length; j++) {
for(int x = 0; x < 6; x++) {
//构造状态转移方程:tmp[x+y]+=pre[x]*num[y],num[y]在本题恒等于1/6
temp[j+x] += pre[j] / 6;
}
}
pre = temp;
}
return pre;
}
}
3 复杂性分析
- 时间复杂度O(N^2):外层for循环O(N),中层for循环O(N),内层for循环O(1),总的时间复杂度O(N ^ 2);
- 空间复杂度O(N):辅助数组占用O(N)空间;
*最长回文子串
1 题目描述
给定一个字符串 s,找到 s 中最长的回文子串。你可以假设 s 的最大长度为 1000。
示例 1:
输入: "babad" 输出: "bab" 注意: "aba" 也是一个有效答案。
示例 2:
输入: "cbbd"
输出: "bb"
"回文串”是一个正读和反读都一样的字符串
2 解题(Java)
2.1 动态规划法
1 定义状态
dp[i][j] 表示子串 s[i…j] 是否为回文子串,这里子串 s[i…j] 定义为左闭右闭区间,可以取到 s[i] 和 s[j];
2 状态转移方程
dp[i][j] = (s[i] == s[j]) and dp[i + 1][j - 1];
3 边界条件
表达式 [i + 1, j - 1] 不构成区间,即长度严格小于 2,即 j - 1 - (i + 1) + 1 < 2 ,整理得 j - i < 3;
4 输出
只要得到 dp[i][j] = true且长度大于之前得到的最大长度,就记录子串的长度和起始位置,没有必要截取,这是因为截取字符串也要消耗性能,记录此时的回文子串的「起始位置」和「回文长度」即可;
public class Solution {
public String longestPalindrome(String s) {
int n = s.length();
int maxLen = 1;
int begin = 0;
// dp[i][j]表示s[i..j]是否是回文串,左闭右闭
boolean[][] dp = new boolean[n][n];
for (int j=1; j<n; j++) {
for (int i=0; i<j; i++) {
if (s.charAt(i) == s.charAt(j) && (j-i < 3 || dp[i+1][j-1])) {
dp[i][j] = true;
if (j-i+1 > maxLen) {
maxLen = j - i + 1;
begin = i;
}
}
}
}
return s.substring(begin, begin + maxLen);
}
}
复杂性分析
时间复杂度为 O(N2),空间复杂度为 O(N2)。
2.2 中心扩展法
两种情况:
- 以1个点作为中心点向两端扩展;
- 以2个点作为中心点向两端扩展;
public class Solution {
public String longestPalindrome(String s) {
int n = s.length();
int maxLen = 1;
int begin = 0;
for (int i = 0; i < n * 2 - 1; i++) {
int left = i / 2;
int right = (i + 1) / 2;
while (left >= 0 && right < n && s.charAt(left) == s.charAt(right)) {
if (right - left + 1 > maxLen) {
maxLen = right - left + 1;
begin = left;
}
left--;
right++;
}
}
return s.substring(begin, begin + maxLen);
}
}
复杂性分析
时间复杂度为 O(N2),空间复杂度为 O(1)。
*正则表达式匹配
1 题目描述
给你一个字符串 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
解释:".*" 表示可匹配零个或多个('*')任意字符('.')。
示例 4:
输入:s = "aab" p = "c*a*b"
输出:true
解释:因为 '*' 表示零个或多个,这里 'c' 为 0 个, 'a' 被重复一次。因此可以匹配字符串 "aab"。
示例 5:
输入:s = "mississippi" p = "mis*is*p*."
输出:false
提示:
- 0 <= s.length <= 20
- 0 <= p.length <= 30
- s 可能为空,且只包含从 a-z 的小写字母。
- p 可能为空,且只包含从 a-z 的小写字母,以及字符 . 和 *。
- 保证每次出现字符 * 时,前面都匹配到有效的字符
2 解题(Java)
class Solution {
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=2; i<=n; i+=2) {
if (p.charAt(i-1) == '*') dp[0][i] = dp[0][i-2]; // a*代表0个a
}
// 状态转移:一步步填充表中其他的空格,dp[i][j]表示s的前i个字符与p的前j个字符是否能够匹配
for (int i=1; i<=m; i++) {
for (int j=1; j<=n; j++) {
if (s.charAt(i-1) == p.charAt(j-1) || p.charAt(j-1) == '.') {// 题目保证每次出现字符*时,前面都匹配到有效的字符,因此不会出现j-2<0的越界情况
dp[i][j] = dp[i-1][j-1];
} else if (p.charAt(j-1) == '*') {
if (p.charAt(j-2) == s.charAt(i-1) || p.charAt(j-2) == '.') {
dp[i][j] |= dp[i][j-2]; // a*代表0个a
dp[i][j] |= dp[i][j-1]; // a*代表1个a
dp[i][j] |= dp[i-1][j-1];// a*代表2个a
dp[i][j] |= dp[i-1][j]; // a*代表大于2个a
} else {
dp[i][j] = dp[i][j-2]; // a*代表0个a
}
}
}
}
return dp[m][n];
}
}
3 复杂性分析
- 时间复杂度O(MN):其中 M 和 N 分别是字符串 s 和 p 的长度。我们需要计算出所有的状态,并且每个状态在进行转移时的时间复杂度为 O(1);
- 空间复杂度O(MN):即为存储所有状态使用的空间;
*小朋友过河问题
1 题目描述
每次只允许不大于两人通过,他们只有一个手电筒,所以每次过桥的两个人需要把手电筒带回来,i号小朋友过桥的时间为T[i],两个人过桥的总时间为二者中时间长者。问所有小朋友过桥的总时间最短是多少?
2 解题(Java)
2.1 解题思路
- 先将所有人按花费时间递增进行排序,排序后数组假设为a,假设前i个人过河花费的最少时间为dp[i];
- 当人数<=3使,方案显而易见;
- 当人数>3时,有两种方案:
方案一(河那边剩1个人):最快和最慢过去,最快回来,这是希望最快速度送回来,dp(n) = dp(n-1) + a[n] + a[1];
方案二(河那边剩2个人):最快和次快过去,最快回来,最慢和次慢过去,次快回来,最快和次快过去,这是希望一次过俩最慢的,利用率高,dp(n) = dp(n-2) + a[n] + a[1] + 2a[2];
2.2 代码
import java.util.Arrays;
import java.util.Scanner;
public class CrossRiver {
public static void main(String[] args)
{
Scanner sc = new Scanner(System.in);
int num = sc.nextInt();
int[] timeCost = new int[num];
for(int i = 0; i < num; i++)
{
timeCost[i] = sc.nextInt();
}
Arrays.sort(timeCost);
if (num < 3) {
System.out.println(timeCost[num - 1]);
} else if (num == 3) {
System.out.println(timeCost[1] + timeCost[0] + timeCost[2]);
} else {
int[] dp = new int[num];
dp[0] = timeCost[0];
dp[1] = timeCost[1];
dp[2] = timeCost[1] + timeCost[0] + timeCost[2];
for(int i = 3; i < num; i++)
{
dp[i] = Math.min(dp[i-1] + timeCost[0] + timeCost[i], dp[i-2] + timeCost[0] + 2 * timeCost[1] + timeCost[i]);
}
System.out.println(dp[num - 1]);
}
}
}