求和问题
1. 两数之和
给定一个整数数组 nums 和一个整数目标值 target,请你在该数组中找出 和为目标值 target 的那 两个 整数,并返回它们的数组下标。
示例 1: 输入:nums = [2,7,11,15], target = 9 输出:[0,1] 解释:因为 nums[0] + nums[1] == 9 ,返回 [0, 1]
思路:两数之和,数组未排序,考虑哈希图,边找边存,一次遍历即可完成,空间换时间。
class Solution {
public int[] twoSum(int[] nums, int target) {
int[] res = new int[2];
Map<Integer, Integer> map = new HashMap<>();
for(int i = 0; i < nums.length;i++) {
if(map.containsKey(target - nums[i])) {
res[0] = map.get(target - nums[i]);
res[1] = i;
return res;
}
map.put(nums[i], i);
}
return res;
}
}
167. 两数之和 II - 输入有序数组
给定一个已按照 升序排列 的整数数组 numbers ,请你从数组中找出两个数满足相加之和等于目标数 target 。
函数应该以长度为 2 的整数数组的形式返回这两个数的下标值。numbers 的下标 从 1 开始计数 ,所以答案数组应当满足 1 <= answer[0] < answer[1] <= numbers.length 。
思路:排序过的数组,使用双指针,两头往里找,直到二者相加等于target。
class Solution {
public int[] twoSum(int[] numbers, int target) {
int i = 0;
int j = numbers.length - 1;
int[] res = new int[2];
while(i < j) {
if(numbers[i] + numbers[j] < target) {
i++;
} else if(numbers[i] + numbers[j] > target) {
j--;
} else {
res[0] = i + 1;
res[1] = j + 1;
return res;
}
}
return res;
}
}
15. 三数之和
给你一个包含 n 个整数的数组 nums,判断 nums 中是否存在三个元素 a,b,c ,使得 a + b + c = 0 ?请你找出所有和为 0 且不重复的三元组。
思路:先排序,第一个指针循环从0到n-2,如果相同注意剪枝,后两个指针参考两数之和算法,当找到一答案时,如果后两个指针值相同注意剪枝。
class Solution {
public List<List<Integer>> threeSum(int[] nums) {
int len = nums.length;
List<List<Integer>> res = new ArrayList<>();
if(len == 0) {
return res;
}
if(nums[0] == 0 && len == 1) {
return res;
}
Arrays.sort(nums);
for(int i = 0; i < len; i++) {
// 剪枝
if(i > 0 && nums[i] == nums[i - 1]) continue;
int j = i + 1, k = len - 1;
while(j < k) {
if(nums[i] + nums[j] + nums[k] == 0) {
List<Integer> list = new ArrayList<Integer>();
list.add(nums[i]);
list.add(nums[j]);
list.add(nums[k]);
res.add(list);
j++;
k--;
// 剪枝
while (j < k && nums[j] == nums[j - 1]) j++;
while (j < k && nums[k] == nums[k + 1]) k--;
} else if(nums[i] + nums[j] + nums[k] > 0) {
k--;
} else {
j++;
}
}
}
return res;
}
}
18. 四数之和
给定一个包含 n 个整数的数组 nums 和一个目标值 target,判断 nums 中是否存在四个元素 a,b,c 和 d ,使得 a + b + c + d 的值与 target 相等?找出所有满足条件且不重复的四元组。
思路:第一个指针遍历0到n-3,注意剪枝,后面同三数求和思路
class Solution {
public List<List<Integer>> fourSum(int[] nums, int target) {
int len = nums.length;
List<List<Integer>> res = new ArrayList<>();
if (nums == null || len < 4) {
return res;
}
Arrays.sort(nums);
for(int i = 0; i < len - 3; i++) {
if(i > 0 && nums[i] == nums[i - 1] ) {
continue;
}
for(int j = i + 1; j < len - 2; j++) {
if(j > i + 1 && nums[j] == nums[j - 1]) {
continue;
}
int l = j + 1, r = len - 1;
while(l < r) {
if(nums[i] + nums[j] + nums[l] + nums[r] == target) {
List<Integer> list = new ArrayList<Integer>();
list.add(nums[i]);
list.add(nums[j]);
list.add(nums[l]);
list.add(nums[r]);
res.add(list);
l++;
r--;
while(l < r && nums[l] == nums[l - 1]) l++;
while(l < r && nums[r] == nums[r + 1]) r--;
} else if(nums[i] + nums[j] + nums[l] + nums[r] > target) {
r--;
} else {
l++;
}
}
}
}
return res;
}
}
动态规划
509. 斐波那契数
斐波那契数,通常用 F(n) 表示,形成的序列称为 斐波那契数列 。该数列由 0 和 1 开始,后面的每一项数字都是前面两项数字的和。也就是:
F(0) = 0,F(1) = 1 F(n) = F(n - 1) + F(n - 2),其中 n > 1 给你 n ,请计算 F(n) 。
思路:递归 && dp->不用数组,节省内存。
class Solution {
public int fib(int n) {
if(n == 0) return 0;
if(n == 1) return 1;
return fib(n - 1) + fib(n - 2);
}
}
class Solution {
public int fib(int n) {
if (n < 2) {
return n;
}
int p = 0, q = 0, r = 1;
for (int i = 2; i <= n; ++i) {
p = q;
q = r;
r = p + q;
}
return r;
}
}
70. 爬楼梯
假设你正在爬楼梯。需要 n 阶你才能到达楼顶。
每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢?
思路:动态规划
class Solution {
public int climbStairs(int n) {
int p = 0, q = 0, r = 1;
for (int i = 1; i <= n; ++i) {
p = q;
q = r;
r = p + q;
}
return r;
}
}
53. 最大子序和
给定一个整数数组 nums ,找到一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。
思路:dp
class Solution {
public int maxSubArray(int[] nums) {
int pre = 0;
int max = nums[0];
for(int x : nums) {
pre = Math.max(pre + x, x);
max = Math.max(max, pre);
}
return max;
}
}
416. 分割等和子集
给你一个 只包含正整数 的 非空 数组 nums 。请你判断是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。
思路:转化为是否可以选出若干个数达到数组之和的一半。数组长度小于2,就一个数不能完成。数组之和为奇数,不能平均分为两份。数组的最大值大于数组之和的一半,也不能平均分。
创建boolean dp[前i个数][是否能达到j值]
初始化:前多少个数,只要我不选,j都可以为0;选择第i个数,就可以达到nums[i]值。(官解只初始化第0个数达到nums[0]值)
状态转移方程:如果当前值nums[i]比要达到的j值小,可以选这个数,也可以不选。 如果选:dp[i][j] = dp[i - 1][j] || dp[i - 1][j - num]; 不选:dp[i][j]=dp[i−1][j] 如果大于j值,则只能不选:dp[i][j]=dp[i−1][j]
class Solution {
public boolean canPartition(int[] nums) {
int n = nums.length;
// 长度小于2返回false
if(n < 2) {
return false;
}
int max = nums[0];
int sum = 0;
for(int x : nums) {
sum += x;
max = Math.max(max, x);
}
int target = sum / 2;
//总数无法整除2返回false
if(sum % 2 != 0) {
return false;
}
//最大值大于目标值返回false
if(target < max) {
return false;
}
boolean[][] dp = new boolean[n][target + 1];
//只要不选,前n个数之和可以为0
for(int i = 0; i < n; i++) {
dp[i][0] = true;
dp[i][nums[i]] = true;
}
//选择第一个数,可以达成第一个数的值
// dp[0][nums[0]] = true;
for(int i = 1; i < n; i++) {
int num = nums[i];
for(int j = 1; j <= target; j++) {
// j比当前值大,可以选也可以不选
if(j >= num) {
dp[i][j] = dp[i - 1][j] || dp[i - 1][j - num];
} else {
dp[i][j] = dp[i - 1][j];
}
}
}
return dp[n - 1][target];
}
}
注意到:每行dp只与上一行dp有关,可以降低空间复杂度,此时状态转移方程 dp[j] = dp[j] || dp[j - nums[i]];则后半段代码可以优化为
boolean[] dp = new boolean[target + 1];
dp[0] = true;
for (int i = 0; i < n; i++) {
int num = nums[i];
//由于如果j < target,只能不选,此时dp[j] = dp[j] 因此忽略
for (int j = target; j >= num; --j) {
dp[j] |= dp[j - num];
}
}
return dp[target];
322. 零钱兑换
给你一个整数数组 coins ,表示不同面额的硬币;以及一个整数 amount ,表示总金额。
计算并返回可以凑成总金额所需的 最少的硬币个数 。如果没有任何一种硬币组合能组成总金额,返回 -1 。
你可以认为每种硬币的数量是无限的。
思路:数组dp[i]表示最少多少个数能到达i值,初始化给每个值设置为总金额+1,此时就算只用1元也无法实现,从而排除使用Math.min中初始dp[i]的问题,dp[0]设置为0,啥都不用也能达到0元。同时排查各个面值时,如果面值大于当前到达值,则会超出该值,不使用该面值。
class Solution {
public int coinChange(int[] coins, int amount) {
int[] dp = new int[amount + 1];
for(int i = 0; i <= amount; i++) {
if(i != 0) {
dp[i] = amount + 1;
}
for(int j = 0; j < coins.length; j++) {
if(i - coins[j] >= 0) {
dp[i] = Math.min(dp[i], dp[i - coins[j]] + 1);
}
}
}
return dp[amount] > amount ? -1 : dp[amount];
}
}
堆栈问题
20. 有效的括号
给定一个只包括 '(',')','{','}','[',']' 的字符串 s ,判断字符串是否有效。
有效字符串需满足:
左括号必须用相同类型的右括号闭合。 左括号必须以正确的顺序闭合。
思路:遍历进栈,如果为左括号,将([{改为)]},入栈。否则如果栈为空,或者出栈元素不等于当前元素则返回false,最后栈非空返回true。
class Solution {
public boolean isValid(String s) {
Deque<Character> dq = new LinkedList<>();
for(char c : s.toCharArray()) {
if(c == '(') {
dq.push(')');
} else if(c == '[') {
dq.push(']');
} else if(c == '{') {
dq.push('}');
} else if(dq.isEmpty() || dq.pop() != c) {
return false;
}
}
return dq.isEmpty();
}
}
496. 下一个更大元素 I
给你两个 没有重复元素 的数组 nums1 和 nums2 ,其中nums1 是 nums2 的子集。
请你找出 nums1 中每个元素在 nums2 中的下一个比其大的值。
nums1 中数字 x 的下一个更大元素是指 x 在 nums2 中对应位置的右边的第一个比 x 大的元素。如果不存在,对应位置输出 -1 。
思路:问题重新描述:nums1是nums2子集,nums1每个元素对应nums2相同元素对应的位置后面是否有大于该元素的值。 使用单调栈,对nums2操作,得到大于每个元素的值放入图中。 遍历nums2,如果栈为空或者当前元素小于栈顶元素,入栈。 如果大于栈顶元素,该元素即大于当前元素,出栈并且更新图,直到栈为空或者栈顶元素大于当前元素。
class Solution {
public int[] nextGreaterElement(int[] nums1, int[] nums2) {
int len1 = nums1.length;
int len2 = nums2.length;
Map<Integer, Integer> map = new HashMap<>();
Deque<Integer> dq = new LinkedList<>();
for(int i = 0; i < len2; i++) {
// 有更大的则更新图
while(!dq.isEmpty() && nums2[i] > dq.peek()) {
map.put(dq.pop(), nums2[i]);
}
dq.push(nums2[i]);
}
int[] res = new int[len1];
for (int i = 0; i < len1; i++) {
res[i] = map.getOrDefault(nums1[i], -1);
}
return res;
}
}
456. 132 模式
给你一个整数数组 nums ,数组中共有 n 个整数。132 模式的子序列 由三个整数 nums[i]、nums[j] 和 nums[k] 组成,并同时满足:i < j < k 和 nums[i] < nums[k] < nums[j] 。
如果 nums 中存在 132 模式的子序列 ,返回 true ;否则,返回 false 。
思路:考虑从末尾开始遍历,最后一个数入栈(单调栈),同时维护max_val作为最大的可以作为2的值。 首先判断当前元素是否可以作为1,如果大于max_val说明可以作为1,满足要求返回true
判断是否可以作为3,与栈顶元素比较,如果大于说明当前元素作为3,栈顶元素可以作为2,弹出栈顶元素并且更新栈顶可以作为最大的值max_val。此过程为建立单调栈过程
最后如果该元素大于max_val说明可能可以作为2的备选,入栈,如果不大于max_val入栈也没啥用,并不能影响结果。
栈中就是个32模式,所以只需要找到一个比2(max_val)小的数。
class Solution {
public boolean find132pattern(int[] nums) {
int max_val = Integer.MIN_VALUE;
int len = nums.length;
Deque<Integer> dq = new LinkedList<>();
dq.push(nums[len - 1]);
for(int i = len - 2; i >= 0; i--) {
if(nums[i] < max_val) {
return true;
}
while(!dq.isEmpty() && nums[i] > dq.peek()){
max_val = dq.pop();
}
if(nums[i] > max_val) {
dq.push(nums[i]);
}
}
return false;
}
}
数学问题
119. 杨辉三角 II
给定一个非负索引 rowIndex,返回「杨辉三角」的第 rowIndex **行。
在「杨辉三角」中,每个数是它左上方和右上方的数的和。
输入: rowIndex = 3
输出: [1,3,3,1]
思路:硬解
class Solution {
public List<Integer> getRow(int rowIndex) {
List<List<Integer>> triangle = new ArrayList<>();
for(int i = 0; i <= rowIndex; i ++) {
List<Integer> list = new ArrayList<>();
for(int j = 0; j <= i; j++) {
if(j == 0 || j == i) {
list.add(1);
} else {
list.add(triangle.get(i - 1).get(j - 1) + triangle.get(i - 1).get(j));
}
}
triangle.add(list);
}
return triangle.get(rowIndex);
}
}
注意到第n行仅用到n-1行数据,可以用滚动数组,优化空间复杂度
class Solution {
public List<Integer> getRow(int rowIndex) {
List<Integer> triangle = new ArrayList<>();
for(int i = 0; i <= rowIndex; i ++) {
List<Integer> list = new ArrayList<>();
for(int j = 0; j <= i; j++) {
if(j == 0 || j == i) {
list.add(1);
} else {
list.add(triangle.get(j - 1) + triangle.get(j));
}
}
triangle = list;
}
return triangle;
}
}
进一步优化,如果里层遍历从右到左,可以只用一个数组
class Solution {
public List<Integer> getRow(int rowIndex) {
List<Integer> triangle = new ArrayList<>();
triangle.add(1);
for(int i = 1; i <= rowIndex; i ++) {
triangle.add(0);
for(int j = i; j > 0; j--) {
triangle.set(j, triangle.get(j - 1) + triangle.get(j));
}
}
return triangle;
}
}
279. 完全平方数
给定正整数 n,找到若干个完全平方数(比如 1, 4, 9, 16, ...)使得它们的和等于 n。你需要让组成和的完全平方数的个数最少。
给你一个整数 n ,返回和为 n 的完全平方数的 最少数量 。
完全平方数 是一个整数,其值等于另一个整数的平方;换句话说,其值等于一个整数自乘的积。例如,1、4、9 和 16 都是完全平方数,而 3 和 11 不是。
思路:数学/动态规划
class Solution {
public int numSquares(int n) {
int[] dp = new int[n + 1];
for(int i = 1; i <= n; i++) {
int min = Integer.MAX_VALUE;
for(int j = 1; j*j <= i;j++) {
min = Math.min(dp[i - j * j], min);
}
dp[i] = min + 1;
}
return dp[n];
}
}
树
112. 路径总和
给你二叉树的根节点 root 和一个表示目标和的整数 targetSum ,判断该树中是否存在 根节点到叶子节点 的路径,这条路径上所有节点值相加等于目标和 targetSum 。
叶子节点 是指没有子节点的节点。
思路:递归即可
class Solution {
public boolean hasPathSum(TreeNode root, int sum) {
if(root == null) {
return false;
}
if(root.left == null && root.right == null) {
return sum == root.val;
}
return hasPathSum(root.left, sum - root.val) || hasPathSum(root.right, sum - root.val);
}
}
230. 二叉搜索树中第K小的元素
给定一个二叉搜索树的根节点 root ,和一个整数 k ,请你设计一个算法查找其中第 k 个最小元素(从 1 开始计数)。
思路:递归/迭代
public int kthSmallest(TreeNode root, int k) {
Deque<TreeNode> dq = new LinkedList<>();
while(true) {
while(root != null) {
dq.push(root);
root = root.left;
}
root = dq.pop();
if(--k == 0) {
return root.val;
}
root = root.right;
}
}
class Solution {
int count = 0;
int target;
int res;
public void dfs(TreeNode root) {
if(root == null) {
return;
}
dfs(root.left);
count++;
if(count == target){
res = root.val;
return;
}
dfs(root.right);
}
public int kthSmallest(TreeNode root, int k) {
target = k;
dfs(root);
return res;
}
}
720. 词典中最长的单词
给出一个字符串数组words组成的一本英语词典。从中找出最长的一个单词,该单词是由words词典中其他单词逐步添加一个字母组成。若其中有多个可行的答案,则返回答案中字典序最小的单词。
若无答案,则返回空字符串。
思路:前缀树
class Solution {
public String longestWord(String[] words) {
Trie trie = new Trie();
int index = 0;
for(String word : words) {
trie.insert(word, ++index);
}
trie.words = words;
return trie.dfs();
}
}
class Node {
char c;
Map<Character, Node> children = new HashMap<>();
int end;
public Node(char c) {
this.c = c;
}
}
class Trie {
Node root;
String[] words;
public Trie() {
root = new Node('0');
}
public void insert(String word, int index) {
Node cur = root;
for(char c : word.toCharArray()) {
cur.children.putIfAbsent(c, new Node(c));
cur = cur.children.get(c);
}
cur.end = index;
}
public String dfs() {
String ans = "";
Stack<Node> stack = new Stack();
stack.push(root);
while(!stack.isEmpty()) {
Node node = stack.pop();
// 该位置有单词或为根节点
if(node.end > 0 || node == root) {
// 如果有单词
if(node != root) {
// 对应到该单词
String word = words[node.end - 1];
// 该单词长度大于最终答案长度 或者 相等且当前word字典序列小
if(word.length() > ans.length() ||
word.length() == ans.length() && word.compareTo(ans) < 0) {
ans = word;
}
}
for(Node n : node.children.values()) {
stack.push(n);
}
}
}
return ans;
}
}
补充:208. 实现 Trie (前缀树)
class Trie {
private Trie[] children;
private Boolean isEnd;
/** Initialize your data structure here. */
public Trie() {
children = new Trie[26];
isEnd = false;
}
/** Inserts a word into the trie. */
public void insert(String word) {
Trie node = this;
for(int i = 0; i < word.length(); i++) {
char c = word.charAt(i);
int index = c - 'a';
if(node.children[index] == null) {
node.children[index] = new Trie();
}
node = node.children[index];
}
node.isEnd = true;
}
/** Returns if the word is in the trie. */
public boolean search(String word) {
Trie node = this;
for(int i = 0; i < word.length(); i++) {
char c = word.charAt(i);
int index = c - 'a';
if(node.children[index] == null) {
return false;
}
node = node.children[index];
}
if(node.isEnd == false) {
return false;
}
return true;
}
/** Returns if there is any word in the trie that starts with the given prefix. */
public boolean startsWith(String prefix) {
Trie node = this;
for(int i = 0; i < prefix.length(); i++) {
char c = prefix.charAt(i);
int index = c - 'a';
if(node.children[index] == null) {
return false;
}
node = node.children[index];
}
return true;
}
}
3. 无重复字符的最长子串
给定一个字符串 s ,请你找出其中不含有重复字符的 最长子串 的长度。
思路:滑动窗口,双指针
class Solution {
public int lengthOfLongestSubstring(String s) {
Set<Character> set = new HashSet<>();
int ans = 0;
int j = 0;
for(int i = 0; i < s.length(); i++) {
while(j < s.length() && !set.contains(s.charAt(j))) {
set.add(s.charAt(j++));
}
if(j - i > ans) {
ans = j - i;
}
set.remove(s.charAt(i));
}
return ans;
}
}
97. 交错字符串
给定三个字符串 s1、s2、s3,请你帮忙验证 s3 是否是由 s1 和 s2 交错 组成的。
两个字符串 s 和 t 交错 的定义与过程如下,其中每个字符串都会被分割成若干 非空 子字符串:
s = s1 + s2 + ... + sn t = t1 + t2 + ... + tm |n - m| <= 1 交错 是 s1 + t1 + s2 + t2 + s3 + t3 + ... 或者 t1 + s1 + t2 + s2 + t3 + s3 + ... 提示:a + b 意味着字符串 a 和 b 连接。
思路:dp
class Solution {
public boolean isInterleave(String s1, String s2, String s3) {
int n = s1.length(), m = s2.length(), t = s3.length();
if (n + m != t) {
return false;
}
boolean[][] f = new boolean[n + 1][m + 1];
f[0][0] = true;
for (int i = 0; i <= n; ++i) {
for (int j = 0; j <= m; ++j) {
int p = i + j - 1;
if (i > 0) {
f[i][j] = f[i][j] || (f[i - 1][j] && s1.charAt(i - 1) == s3.charAt(p));
}
if (j > 0) {
f[i][j] = f[i][j] || (f[i][j - 1] && s2.charAt(j - 1) == s3.charAt(p));
}
}
}
return f[n][m];
}
}
字符串搜索
28. 实现 strStr()
实现 strStr() 函数。
给你两个字符串 haystack 和 needle ,请你在 haystack 字符串中找出 needle 字符串出现的第一个位置(下标从 0 开始)。如果不存在,则返回 -1 。
思路:kmp,前缀和后缀相同位置直接跳着找就行。
class Solution {
public int strStr(String haystack, String needle) {
int len1 = haystack.length();
int len2 = needle.length();
int[] pi = new int[len2];
if(len2 == 0) {
return 0;
}
for(int i = 1, j = 0; i < len2; i++) {
while(j > 0 && needle.charAt(j) != needle.charAt(i)) {
j = pi[j - 1];
}
if(needle.charAt(i) == needle.charAt(j)) {
j++;
}
pi[i] = j;
}
for(int i = 0, j = 0; i < len1; i++) {
while(j > 0 && haystack.charAt(i) != needle.charAt(j)) {
j = pi[j - 1];
}
if(haystack.charAt(i) == needle.charAt(j)) {
j++;
}
if(j == len2) {
return i - len2 + 1;
}
}
return -1;
}
}
题目来源:力扣