第一遍!
一、哈希
1. 两数之和
题目描述
题目链接
给定一个整数数组 nums 和一个整数目标值 target,请你在该数组中找出 和为目标值 target 的那 两个 整数,并返回它们的数组下标。
你可以假设每种输入只会对应一个答案。但是,数组中同一个元素在答案里不能重复出现。
你可以按任意顺序返回答案。
解题思路
HashMap
存储:值-下标- 遍历数组元素
x
放入HashMap
之前先看一下存不存在target - x
代码
class Solution {
public int[] twoSum(int[] nums, int target) {
HashMap<Integer, Integer> map = new HashMap<>();
for(int i=0; i<nums.length; i++) {
if(map.containsKey(target - nums[i])) {
return new int[]{i, map.get(target - nums[i])};
}
map.put(nums[i], i);
}
return null;
}
}
2. 字母异位词分组⭕
题目描述
题目链接
给你一个字符串数组,请你将 字母异位词 组合在一起。可以按任意顺序返回结果列表。
字母异位词 是由重新排列源单词的所有字母得到的一个新单词。
解题思路
- 所有字母异位词排序后都是同一个单词,所以用排序后的单词做
HashMap
的key - 用
List<String>
做value
代码
class Solution {
//单词排序->异位词key-[value1, value2, ...]
public List<List<String>> groupAnagrams(String[] strs) {
HashMap<String, List<String>> map = new HashMap<>();
for(String each : strs) {
char[] word = each.toCharArray();
Arrays.sort(word);
String key = String.valueOf(word);
List<String> list = map.getOrDefault(key, new ArrayList<>());
list.add(each);
map.put(key, list);
}
return new ArrayList<>(map.values());
}
}
3. 最长连续序列🔺
题目描述
题目链接
给定一个未排序的整数数组 nums ,找出数字连续的最长序列(不要求序列元素在原数组中连续)的长度。
请你设计并实现时间复杂度为 O(n)
的算法解决此问题。
解题思路
- 先排序再找最长连续序列的时间复杂度是O(nlogn),所以不能排序
- 对于元素x,查询元素x+1,x+2,...,存不存在,用
HashSet
,时间复杂度是O(1)
- 对于一个元素x,只有当它是一个连续序列的起始元素,才往后找x+1,x+2,... 什么是起始元素?x-1不存在的x
- 由于每个元素只会被查一次,所以时间复杂度是O(n)
代码
class Solution {
public int longestConsecutive(int[] nums) {
if(nums.length == 0) return 0;
int ans = 0;
HashSet<Integer> set = new HashSet<>();
for(int num : nums) {
set.add(num);
}
for(int num : set) {
if(!set.contains(num-1)) {
int curLength = 1;
int curNum = num;
while(set.contains(curNum + 1)) {
curLength++;
curNum++;
}
ans = Math.max(ans, curLength);
}
}
return ans;
}
}
二、双指针
1. 移动零
题目描述
题目链接
给定一个数组 nums,编写一个函数将所有 0 移动到数组的末尾,同时保持非零元素的相对顺序。
请注意 ,必须在不复制数组的情况下原地对数组进行操作。
解题思路
- 双指针,j负责指向写入不是0的数字的位置,i负责往后遍历数组寻找不是0的数字
- 最后把j到末尾全部填充为0
代码
class Solution {
public void moveZeroes(int[] nums) {
int j = 0;
for(int i=0; i<nums.length; i++) {
if(nums[i] != 0) {
nums[j++] = nums[i];
}
}
while(j < nums.length) {
nums[j++] = 0;
}
}
}
2. 盛最多水的容器🔺
题目描述
题目链接
给定一个长度为 n 的整数数组 height 。有 n 条垂线,第 i 条线的两个端点是 (i, 0) 和 (i, height[i]) 。
找出其中的两条线,使得它们与 x 轴共同构成的容器可以容纳最多的水。
返回容器可以储存的最大水量。
说明:你不能倾斜容器。
解题思路
- 关键在于:两根柱子之间的盛水两取决于短的那根柱子!
- 双指针,分别指向两头的柱子,往里缩的时候:
向内移动短板,短板可能变小也可能不变也可能变大,所以面积有可能变小或变大
向内移动长板,短板可能变小或不变,所以面积一定变小
所以要移动短板。
代码
class Solution {
public int maxArea(int[] height) {
int ans = 0;
int i = 0;
int j = height.length - 1;
while(i < j) {
int area = height[i] < height[j] ? (j-i) * height[i++] : (j-i) * height[j--];
ans = Math.max(ans, area);
}
return ans;
}
}
3. 三数之和🔺
题目描述
题目链接
给你一个整数数组 nums ,判断是否存在三元组 [nums[i], nums[j], nums[k]]
满足 i != j
、i != k
且 j != k
,同时还满足 nums[i] + nums[j] + nums[k] == 0
。请你返回所有和为 0 且不重复的三元组。
注意:答案中不可以包含重复的三元组。
解题思路
- 不用返回下标,所以可以排序,排序之后就可以用双指针啦!
- 遍历
i
,在[i+1, ..., n-1]
中用双指针找j
和k
- 重点在于去重,排序之后相同的元素都是连续的,但是前面的
i
的结果包含后面的i
的结果,所以要对i
去重。同理,j
和k
也需要进行类似的去重,即用过一次j
元素后就要跳过后面所有相同的j
元素,当然啦,k
是跳过前面的。
代码
class Solution {
public List<List<Integer>> threeSum(int[] nums) {
List<List<Integer>> ans = new ArrayList<>();
Arrays.sort(nums);
for(int i=0; i<nums.length-2; i++) {
if(nums[i] > 0) return ans; //剪枝
if(i > 0 && nums[i] == nums[i-1]) continue; //去重i
int j = i + 1;
int k = nums.length - 1;
while(j < k) {
if(nums[i] + nums[j] + nums[k] < 0) {
j++;
} else if(nums[i] + nums[j] + nums[k] > 0) {
k--;
} else {
ans.add(Arrays.asList(nums[i], nums[j], nums[k]));
j++;
k--;
while(j < k && nums[j] == nums[j-1]) j++; //去重j
while(j < k && nums[k] == nums[k+1]) k--; //去重k
}
}
}
return ans;
}
}
4. 接雨水🔺
题目描述
题目链接
给定 n 个非负整数表示每个宽度为 1 的柱子的高度图,计算按此排列的柱子,下雨之后能接多少雨水。
解题思路
- 双指针!
- 每一列能接的雨水取决于:该列左边最高的柱子和右边最高的柱子中,最矮的那个,所以要先求出每一列左边最高的柱子和右边最高的柱子的高度。
代码
class Solution {
//每一列能接的雨水取决于:该列左边最高的柱子和右边最高的柱子中,最矮的那个
public int trap(int[] height) {
int[] lHeight = new int[height.length]; //每一列左边最高的柱子的高度
int[] rHeight = new int[height.length]; //每一列右边最高的柱子的高度
int maxHeight = 0;
for(int i=0; i<height.length; i++) {
lHeight[i] = maxHeight;
maxHeight = Math.max(maxHeight, height[i]);
}
maxHeight = 0;
for(int i=height.length-1; i>=0; i--) {
rHeight[i] = maxHeight;
maxHeight = Math.max(maxHeight, height[i]);
}
int ans = 0;
for(int i=1; i<height.length-1; i++) {
int deta = Math.min(lHeight[i], rHeight[i]) - height[i];
if(deta > 0) {
ans += deta;
}
}
return ans;
}
}
三、滑动窗口
关于滑动窗口的一点粗糙的小总结:start
和end
记录窗口在原始序列中的起始和结束位置,window
一般是某种数据结构,比如HashSet
或HashMap
。start先往后找直到满足/不满足某种条件,然后end开始收缩直到不满足/满足某种条件。
1. 无重复字符的最长子串🔺
题目描述
题目链接
给定一个字符串 s ,请你找出其中不含有重复字符的 最长子串 的长度。(注意,子串是连续的)
解题思路
- 滑动窗口
- 用一个
set
存当前窗口中的字符,如果当前遍历到的字符在set中出现过,就更新ans,即窗口容量的最大值
。同时把窗口中重复字符以及前面的字符全删除.
代码
class Solution {
public int lengthOfLongestSubstring(String s) {
HashSet<Character> window = new HashSet<>();
int n = s.length();
int ans = 0;
int start = 0;
for(int end=0; end<n; end++) {
if(!window.contains(s.charAt(end))) {
window.add(s.charAt(end));
} else {
ans = Math.max(ans, window.size());
// 删除重复元素以及之前的全部元素
while(start < n && s.charAt(start) != s.charAt(end)) {
window.remove(s.charAt(start));
start++;
}
start++; // start指向重复元素之后的元素
}
}
return Math.max(ans, window.size());
}
}
2. 找到字符串中所有字母异位词🔺
题目描述
题目链接
给定两个字符串 s 和 p,找到 s 中所有 p 的 异位词 的子串,返回这些子串的起始索引。不考虑答案输出的顺序。
异位词 指由相同字母重排列形成的字符串(包括相同的字符串)。
解题思路
- 使用滑动窗口
- 窗口的大小是固定的,即 p 的长度,所以循环的每一步都要移除一个起始元素,加入一个末尾元素
- 如果用HashMap来记录字符和对应数量的话(就像题目最小覆盖子串一样),需要判断是否是异位词
优化:
int[] count = new int[26];
记录每种字母不平衡的数量,s中的字母贡献+1,p中的字母贡献-1,当26个字母全部平衡(count[i]=0
)时,为一种答案- 如何判断count中的元素是否全为0?设置一个平衡标志differ,只在 移动窗口会导致 count[i] 从0到非0 和 从 非0到0 时修改differ
代码
class Solution {
public List<Integer> findAnagrams(String s, String p) {
int sLen = s.length();
int pLen = p.length();
List<Integer> ans = new ArrayList<>();
if(sLen < pLen) return ans;
int[] count = new int[26]; //记录当前窗口状态下,26个字母的状态,<0代表缺少多少个,>0代表多余多少个
int differ = 0; //记录当前窗口的平衡状态,即有多少个count不为0,当count全部为0时说明是异位词
for(int i=0; i<pLen; i++) {
count[s.charAt(i)-'a']++;
count[p.charAt(i)-'a']--;
}
for(int i=0; i<26; i++) {
if(count[i] != 0) {
differ++;
}
}
if(differ == 0) {
ans.add(0);
}
for(int i=0; i<s.length()-pLen; i++) {
//移出窗口
if(count[s.charAt(i)-'a'] == 0){ //移出窗口的元素是不多不少的
differ++;
} else if(count[s.charAt(i)-'a'] == 1) { //移出窗口的元素是刚好多一个的
differ--;
}
count[s.charAt(i)-'a']--;
//移入窗口
if(count[s.charAt(i+pLen)-'a'] == 0) { //移入窗口的元素是不多不少的
differ++;
} else if(count[s.charAt(i+pLen)-'a'] == -1) { //移入窗口的元素是刚好少一个的
differ--;
}
count[s.charAt(i+pLen)-'a']++;
if(differ == 0) {
ans.add(i+1);
}
}
return ans;
}
}
四、子串
1. 和为k的子数组🔺
题目描述
题目链接
给你一个整数数组 nums 和一个整数 k ,请你统计并返回 该数组中和为 k 的子数组的个数 。
子数组是数组中元素的连续非空序列。
解题思路
方法1:
- 2层for循环确定子数组的start和end,求子数组的元素和
方法2:
- 前缀和+HashMap优化
- 什么是前缀和?
preSum[i] = sum(0,...,i)
,nums[i]
的前缀和包括nums[i]
自己在内,这样的话sum(i,...,j) = preSum[j] - preSum[i]
- 对于
i
,只需要知道在i
前面有多少个前缀和为preSum[i] - k
的元素就行,用HashMap
存储前缀和-数量
即可
注:因为数组中存在负数,比如[-1,-1,1],所以没法用滑动窗口(curSum<k end就继续往后加,curSum>k就收缩start).
代码
方法1:
class Solution {
public int subarraySum(int[] nums, int k) {
int ans = 0;
for(int start=0; start<nums.length; start++) {
int curSum = 0;
for(int end=start; end<nums.length; end++) {
curSum += nums[end];
if(curSum == k) {
ans++;
}
}
}
return ans;
}
}
方法2:
class Solution {
public int subarraySum(int[] nums, int k) {
int ans = 0;
HashMap<Integer, Integer> map = new HashMap<>();
map.put(0, 1); //这里必须将前缀和为0的元素的数量设为1,否则nums=[3,...],k=3这种情况时会出错
int preSum = 0;
for(int num : nums) {
preSum += num;
if(map.containsKey(preSum-k)) {
ans += map.get(preSum-k);
}
map.put(preSum, map.getOrDefault(preSum, 0) + 1);
}
return ans;
}
}
2. 滑动窗口的最大值🔺
题目描述
题目链接
给你一个整数数组 nums,有一个大小为 k 的滑动窗口从数组的最左侧移动到数组的最右侧。你只可以看到在滑动窗口内的 k 个数字。滑动窗口每次只向右移动一位。
返回 滑动窗口中的最大值 。
解题思路
- 窗口和队列是两个东西
- 队列中只存单调递减的元素
- 在往队列中存新值时,要删除队列中比新值小的所有元素
- 移动窗口需要删除队列中对应的元素吗?当然需要,但是这个元素可能已经在加新值时被移除了,所以需要判断这个元素还在不在,当队首元素==nums[移除下标]时,删除队首元素。
这里需要注意一点,当待移除元素和当前最大值相等时,要删除的是待移除元素而不是当前最大值,因此队列不应该是严格递减的,如果是严格递减的,待移除元素已经被移除了,就会误删当前最大元素。
代码
class Solution {
//内部类:单调栈(递减)
class MyQueue {
private Deque<Integer> q = new LinkedList<>();
//放入新的元素时,保持队列的单调递减
public void offerLast(int x) {
while(!q.isEmpty() && q.peekLast() < x) { //注意:非严格递减
q.pollLast();
}
q.offerLast(x);
}
//只有当要被弹出的窗口元素等于队列出口元素时,才弹出
public void pollFirst(int x) {
if(x == q.peekFirst()) {
q.pollFirst();
}
}
//返回队列头部元素(最大元素)
public int peekFirst() {
return q.peekFirst();
}
}
public int[] maxSlidingWindow(int[] nums, int k) {
int n = nums.length;
int[] ans = new int[n-k+1];
MyQueue q = new MyQueue();
for(int i=0; i<k; i++) {
q.offerLast(nums[i]);
}
ans[0] = q.peekFirst();
for(int i=k; i<n; i++) {
q.offerLast(nums[i]);
q.pollFirst(nums[i-k]);
ans[i-k+1] = q.peekFirst();
}
return ans;
}
}
3. 最小覆盖子串🔺
题目描述
题目链接
给你一个字符串 s 、一个字符串 t 。返回 s 中涵盖 t 所有字符的最小子串。如果 s 中不存在涵盖 t 所有字符的子串,则返回空字符串 "" 。
解题思路
- 滑动窗口
start
和end
Map<Character, Integer> cnt
记录t
中所有字符以及出现的次数Map<Character, Integer> window
记录当前窗口中的字符以及出现的次数(一点小优化,只记录t
中存在的字符)end
往后找,直到满足 当前窗口包含t中的所有字符,开始收缩start
,直到当前窗口不满足条件,记录并更新最小长度及位置
代码
class Solution {
public String minWindow(String s, String t) {
Map<Character, Integer> cnt = new HashMap<>();
Map<Character, Integer> window = new HashMap<>();
for(int i=0; i<t.length(); i++) {
char ch = t.charAt(i);
cnt.put(ch, cnt.getOrDefault(ch, 0) + 1);
}
int start = 0, end = 0;
int ansL = 0, ansR = 0, ansLen = Integer.MAX_VALUE;
for(end=0; end < s.length(); end++) {
if(cnt.containsKey(s.charAt(end))) {
window.put(s.charAt(end), window.getOrDefault(s.charAt(end), 0) + 1);
}
while(start <= end && isOk(cnt, window)) {
if(end - start + 1 < ansLen) {
ansLen = end - start + 1;
ansL = start;
ansR = end;
}
if(cnt.containsKey(s.charAt(start))) {
window.put(s.charAt(start), window.get(s.charAt(start)) - 1);
}
start++;
}
}
return ansLen == Integer.MAX_VALUE ? "" : s.substring(ansL, ansR + 1);
}
boolean isOk(Map<Character, Integer> cnt, Map<Character, Integer> window) {
for(char key : cnt.keySet()) {
if(window.getOrDefault(key, 0) < cnt.get(key)) {
return false;
}
}
return true;
}
}
五、普通数组
1. 最大子数组和⭕
题目描述
题目链接
给你一个整数数组 nums ,请你找出一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。
子数组 是数组中的一个连续部分。
解题思路
- 动态规划,以nums[i]结尾的连续子数组的最大和 可以由 以nums[i-1]结尾的连续子数组的最大和 得出
代码
class Solution {
//dp[i] = 以nums[i]结尾的连续子数组的最大和
//dp[i] = max(dp[i-1] + nums[i], nums[i])
//dp[0] = nums[0]
public int maxSubArray(int[] nums) {
int[] dp = new int[nums.length];
dp[0] = nums[0];
int ans = nums[0];
for(int i=1; i<nums.length; i++) {
dp[i] = Math.max(dp[i-1] + nums[i], nums[i]);
ans = Math.max(ans, dp[i]);
}
return ans;
}
}
滚动数组:
class Solution {
public int maxSubArray(int[] nums) {
int pre = nums[0];
int ans = nums[0];
for(int i=1; i<nums.length; i++) {
pre = Math.max(pre + nums[i], nums[i]);
ans = Math.max(ans, pre);
}
return ans;
}
}
2. 合并区间
题目描述
题目链接
以数组 intervals
表示若干个区间的集合,其中单个区间为 intervals[i] = [starti, endi]
。请你合并所有重叠的区间,并返回 一个不重叠的区间数组,该数组需恰好覆盖输入中的所有区间 。
解题思路
- 将intervals数组按照第一维排序,设置start和end,之后遍历每个区间,挨个比较当前遍历到的区间的起始位置和end的大小
- 注意:起始位置排在后面的区间的结束位置有可能排在前面哦
代码
class Solution {
public int[][] merge(int[][] intervals) {
Arrays.sort(intervals, (i1, i2) -> Integer.compare(i1[0], i2[0]));
List<int[]> ans = new ArrayList<>();
int start = intervals[0][0], end = intervals[0][1];
for(int i=1; i<intervals.length; i++) {
if(intervals[i][0] <= end) {
end = Math.max(end, intervals[i][1]);
} else {
ans.add(new int[]{start, end});
start = intervals[i][0];
end = intervals[i][1];
}
}
ans.add(new int[]{start, end});
return ans.toArray(new int[ans.size()][2]);
}
}
3. 轮转数组⭕
题目描述
题目链接
给定一个整数数组 nums,将数组中的元素向右轮转 k 个位置,其中 k 是非负数。
解题思路
- 方法1:每个元素
i
都往后移动了i+k
个位置,新的位置变成了(i+k)%n
,复制nums
到一个新的数组arr
,然后nums[(i+k)%n] = arr[i]
,这种做法的空间复杂度是O(n) - 方法2:分别反转,整体反转,空间复杂度是O(1)
代码
方法1:
class Solution {
public void rotate(int[] nums, int k) {
int n = nums.length;
int[] arr = nums.clone();
for(int i=0; i<n; i++) {
nums[(i+k)%n] = arr[i]; //对于i而言,它的新的位置是(i+k)%n
}
}
}
方法2:
class Solution {
public void rotate(int[] nums, int k) {
int n = nums.length;
k %= n; //!!!
reverse(nums, 0, n-k-1);
reverse(nums, n-k, n-1);
reverse(nums, 0, n-1);
}
void reverse(int[] nums, int start, int end) { //左闭右闭
while(start < end) {
int tmp = nums[start];
nums[start++] = nums[end];
nums[end--] = tmp;
}
}
}
4. 除自身以外数组的乘积⭕
题目描述
题目链接
给你一个整数数组 nums,返回 数组 answer ,其中 answer[i] 等于 nums 中除 nums[i] 之外其余各元素的乘积 。
题目数据 保证 数组 nums之中任意元素的全部前缀元素和后缀的乘积都在 32 位 整数范围内。
请 不要使用除法,且在 O(n) 时间复杂度内完成此题。
解题思路
- 前缀乘积,后缀乘积。
- 空间复杂度优化:用ans存放前缀,然后乘以后缀得到答案
代码
class Solution {
public int[] productExceptSelf(int[] nums) {
int n = nums.length;
int[] preMulti = new int[n];
int[] postMulti = new int[n];
preMulti[0] = 1;
postMulti[n-1] = 1;
for(int i=1; i<nums.length; i++) {
preMulti[i] = preMulti[i-1] * nums[i-1];
}
for(int i=n-2; i>=0; i--) {
postMulti[i] = postMulti[i+1] * nums[i+1];
}
int[] ans = new int[n];
for(int i=0; i<n; i++) {
ans[i] = preMulti[i] * postMulti[i];
}
return ans;
}
}
class Solution {
public int[] productExceptSelf(int[] nums) {
int n = nums.length;
int[] ans = new int[n];
ans[0] = 1;
for(int i=1; i<nums.length; i++) {
ans[i] = ans[i-1] * nums[i-1];
}
int R = 1;
for(int i=n-1; i>=0; i--) {
ans[i] = ans[i] * R;
R *= nums[i];
}
return ans;
}
}
5. 缺失的第一个正数🔺😒
题目描述
题目链接
给你一个未排序的整数数组 nums ,请你找出其中没有出现的最小的正整数。
请你实现时间复杂度为 O(n) 并且只使用常数级别额外空间的解决方案。
解题思路
- 方法1:将nums放到HashSet中,然后从1开始遍历,返回HashSet中第一个不存在的数,时空复杂度都是O(n)
- 方法2:数组长度为n,如果1-n都出现在了数组中,那么答案是n+1,否则答案是1-n中的某个数
打标记:由于我们只关心[1...n],首先将小于等于0的数替换成n+1,那么数组中就只有正数了
然后遍历每个数,如果这个数的绝对值小于等于n,就将他应该在的位置(比如x应该在x-1位置上)置为负数
最后从头开始遍历,第一个不是负数的数的位置就是答案
代码
class Solution {
public int firstMissingPositive(int[] nums) {
int n = nums.length;
for(int i=0; i<n; i++) {
if(nums[i] <= 0) {
nums[i] = n + 1;
}
}
for(int i=0; i<n; i++) {
int num = Math.abs(nums[i]);
if(num <= n) {
nums[num-1] = -Math.abs(nums[num-1]);
}
}
for(int i=0; i<n; i++) {
if(nums[i] > 0) {
return i+1;
}
}
return n+1;
}
}
6. 合并两个有序数组
题目描述
代码
class Solution {
public void merge(int[] nums1, int m, int[] nums2, int n) {
int idx = m + n - 1;
int i = m-1, j = n-1;
while(i >= 0 && j >= 0) {
nums1[idx--] = nums1[i] < nums2[j] ? nums2[j--] : nums1[i--];
}
while(j >= 0) {
nums1[idx--] = nums2[j--];
}
}
}
六、矩阵
1. 矩阵置零🔺
题目描述
题目链接
给定一个 m x n 的矩阵,如果一个元素为 0 ,则将其所在行和列的所有元素都设为 0 。请使用 原地 算法。
解题思路
非原地算法:
- 因为同一行/列只要有一个元素为0,这一行/列都要置为0,所以设置两个标记数组,分别负责标记每一行是否置0、每一列是否置为0
- 遍历每个位置,只要行标记或列标记中有一个置0,就把这个位置置为0
原地算法:
- 先看和 第一行和第一列 同行同列的元素有咩有0,从而确定 第一行和第一列 是否需要置0,用flagRow0和flagCol0两个int变量来记录
- 然后就可以用 第一行和第一列 取代非原地算法中的标记数组,用来标记 除了第一行和第一列之外 的元素
- 填好标记数组后,就可以用标记数组来指导内部数组的置0与否
- 然后根据flagRow0和flagCol0修改 第一行和第一列
- 可能存在的疑问:在用 第一行和第一列 做内部数据的标记数组的时候,会修改掉 第一行和第一列原来的值吗?答:这是不影响的,因为用第一行和第一列做标记时是将相应位置置为0,但是这同样也说明它的同行/同列存在0,本来也应该置为0
代码
原地算法:
class Solution {
public void setZeroes(int[][] matrix) {
int m = matrix.length;
int n = matrix[0].length;
boolean flagRow0 = false, flagCol0 = false;
for(int i=0; i<m; i++) {
if(matrix[i][0] == 0) {
flagCol0 = true;
}
}
for(int i=0; i<n; i++) {
if(matrix[0][i] == 0) {
flagRow0 = true;
}
}
for(int i=1; i<m; i++) {
for(int j=1; j<n; j++) {
if(matrix[i][j] == 0) {
matrix[i][0] = 0;
matrix[0][j] = 0;
}
}
}
for(int i=1; i<m; i++) {
for(int j=1; j<n; j++) {
if(matrix[i][0] == 0 || matrix[0][j] == 0) {
matrix[i][j] = 0;
}
}
}
if(flagCol0) {
for(int i=0; i<m; i++) {
matrix[i][0] = 0;
}
}
if(flagRow0) {
for(int j=0; j<n; j++) {
matrix[0][j] = 0;
}
}
}
}
非原地算法:
class Solution {
public void setZeroes(int[][] matrix) {
int m = matrix.length, n = matrix[0].length;
boolean[] row = new boolean[m];
boolean[] col = new boolean[n];
for(int i=0; i<m; i++) {
for(int j=0; j<n; j++) {
if(matrix[i][j] == 0) {
row[i] = true;
col[j] = true;
}
}
}
for(int i=0; i<m; i++) {
for(int j=0; j<n; j++) {
if(row[i] || col[j]) {
matrix[i][j] = 0;
}
}
}
}
}
2. 螺旋矩阵⭕
题目描述
题目链接
给你一个 m 行 n 列的矩阵 matrix ,请按照 顺时针螺旋顺序 ,返回矩阵中的所有元素。
解题思路
- 设置一个
directions
数组标记行走的方向 - 每次走一步之前需要判断
下一个的位置是否合法
(合法指在矩阵内且没有走过的地方,可以将已经走过的地方标记为一个不存在的值),如果下一个位置合法就继续走过去,如果不合法就转向
代码
class Solution {
public List<Integer> spiralOrder(int[][] matrix) {
int[][] directions = {{0, 1}, {1, 0}, {0, -1}, {-1, 0}};
List<Integer> ans = new ArrayList<>();
int m = matrix.length, n = matrix[0].length;
int row = 0, col = 0;
int curDirection = 0;
for(int i=0; i<m*n; i++) {
ans.add(matrix[row][col]);
matrix[row][col] = 101;
int newRow = row + directions[curDirection][0];
int newCol = col + directions[curDirection][1];
if(newRow < 0 || newRow >= m || newCol < 0 || newCol >= n || matrix[newRow][newCol] == 101) {
curDirection = (curDirection + 1) % 4;
}
row += directions[curDirection][0];
col += directions[curDirection][1];
}
return ans;
}
}
3. 旋转图像🔺😒
题目描述
题目链接
给定一个 n × n 的二维矩阵 matrix 表示一个图像。请你将图像顺时针旋转 90 度。
你必须在 原地 旋转图像,这意味着你需要直接修改输入的二维矩阵。请不要 使用另一个矩阵来旋转图像。
解题思路
非原地算法:
row,col
旋转之后位置变成了col,n-row-1
,设置一个辅助数组来存储旋转后的矩阵,然后再复制到原始数组即可matrix_new[col][n-row-1] = matrix[row][col]
原地算法1:
matrix[row][col]
旋转后去了哪里?matrix[col][n-row-1] = matrix[row][col]
matrix[col][n-row-1]
旋转后去了哪里?matrix[n-row-1][n-col-1] = matrix[col][n-row-1]
matrix[n-row-1][n-col-1]
旋转后去了哪里?matrix[n-col-1][row] = matrix[n-row-1][n-col-1]
matrix[n-col-1][row]
旋转后去了哪里?matrix[row][col] = matrix[n-col-1][row]
观察发现这四个位置形成了一个循环,因此只需要用一个临时变量tmp
记录matrix[row][col]
就可以了,然后这四个变量依次赋值- 那么
matrix[row][col]
应该遍历哪些位置呢?如下图所示,i从0到n/2
,j从0到(n+1)/2
,就可以不重复不遗漏。
🔺原地算法2:
matrix[row][col]
---水平轴翻转---> matrix[n−row−1][col]
---主对角线翻转---> matrix[col][n−row−1]
,这个转换和上面两个是等价的。
代码
原地算法2:
class Solution {
public void rotate(int[][] matrix) {
int n = matrix.length;
//水平翻转
for(int i=0; i<n/2; i++) {
for(int j=0; j<n; j++) {
int tmp = matrix[i][j];
matrix[i][j] = matrix[n-i-1][j];
matrix[n-i-1][j] = tmp;
}
}
//主对角线翻转
for(int i=0; i<n; i++) {
for(int j=0; j<i; j++) {
int tmp = matrix[i][j];
matrix[i][j] = matrix[j][i];
matrix[j][i] = tmp;
}
}
}
}
非原地算法:
class Solution {
public void rotate(int[][] matrix) {
int m = matrix.length, n = matrix[0].length;
int[][] tmpMatrix = new int[m][n];
for(int i=0; i<m; i++) {
for(int j=0; j<n; j++) {
tmpMatrix[j][n-i-1] = matrix[i][j];
}
}
for(int i=0; i<m; i++) {
for(int j=0; j<n; j++) {
matrix[i][j] = tmpMatrix[i][j];
}
}
}
}
原地算法1:
class Solution {
public void rotate(int[][] matrix) {
int n = matrix.length;
for(int i=0; i<n/2; i++) {
for(int j=0; j<(n+1)/2; j++) {
int tmp = matrix[i][j];
matrix[i][j] = matrix[n-j-1][i];
matrix[n-j-1][i] = matrix[n-i-1][n-j-1];
matrix[n-i-1][n-j-1] = matrix[j][n-i-1];
matrix[j][n-i-1] = tmp;
}
}
}
}
4. 搜索二维矩阵II🔺
题目描述
题目链接
编写一个高效的算法来搜索 m x n 矩阵 matrix 中的一个目标值 target 。该矩阵具有以下特性:
- 每行的元素从左到右升序排列。
- 每列的元素从上到下升序排列。
解题思路
- 方法1:遍历矩阵
- 方法2:对每一行分别二分查找
- 🔺方法3:Z 字形查找,从矩阵的右上角开始查找(其实这样看就像一棵二叉搜索树)。如果
matrix[x][y] < target
,则说明第 x 行全部小于 target ,所以x++
;如果matrix[x][y] > target
,则说明第 y 列全部大于 target ,所以y--
。直到 x 或 y 越界说明不存在 target 。
代码
方法3:
class Solution {
public boolean searchMatrix(int[][] matrix, int target) {
int m = matrix.length, n = matrix[0].length;
int x = 0, y = n - 1;
while(x < m && y >= 0) {
if(matrix[x][y] == target) {
return true;
} else if(matrix[x][y] < target) {
x++;
} else {
y--;
}
}
return false;
}
}
七、链表
链表的重点:dummyHead
如果需要根据key对链表中的某个节点进行定位,通常用HashMap<key, node>
1. 相交链表⭕
题目描述
题目链接
给你两个单链表的头节点 headA 和 headB ,请你找出并返回两个单链表相交的起始节点。如果两个链表不存在相交节点,返回 null 。
解题思路
- 无论两个链表相交不相交,设置
两个指针pA和pB各自走相同长度的路
,如果相交就会在交点相遇,不相交就会在各自都走完两条链表时指向null - 循环条件是
pA != pB
- 允许
pA
和pB
走到null
,不然不相交时就无限循环了
代码
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode(int x) {
* val = x;
* next = null;
* }
* }
*/
public class Solution {
public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
if(headA == null || headB == null) return null;
ListNode pA = headA, pB = headB;
while(pA != pB) {
pA = pA == null ? headB : pA.next;
pB = pB == null ? headA : pB.next;
}
return pA;
}
}
2. 反转链表⭕
题目描述
题目链接
给你单链表的头节点 head ,请你反转链表,并返回反转后的链表。
解题思路
- 每次交换指针时,先用
tmp
记录p2.next
代码
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode() {}
* ListNode(int val) { this.val = val; }
* ListNode(int val, ListNode next) { this.val = val; this.next = next; }
* }
*/
class Solution {
public ListNode reverseList(ListNode head) {
if(head == null || head.next == null) return head;
ListNode p1 = null, p2 = head;
while(p2 != null) {
ListNode tmp = p2.next;
p2.next = p1;
p1 = p2;
p2 = tmp;
}
return p1;
}
}
反转链表II
题目描述
题目链接
给你单链表的头指针 head 和两个整数 left 和 right ,其中 left <= right 。请你反转从位置 left 到位置 right 的链表节点,返回 反转后的链表 。
代码
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode() {}
* ListNode(int val) { this.val = val; }
* ListNode(int val, ListNode next) { this.val = val; this.next = next; }
* }
*/
class Solution {
public ListNode reverseBetween(ListNode head, int left, int right) {
ListNode dummyHead = new ListNode(0, head);
ListNode p = dummyHead;
for(int i=1; i<left; i++) {
p = p.next;
}
ListNode pre = p;
ListNode begin = p.next;
pre.next = null;
p = begin;
for(int i=0; i<right-left; i++) {
p = p.next;
}
ListNode end = p;
ListNode next = end.next;
end.next = null;
pre.next = reverse(begin);
begin.next = next;
return dummyHead.next;
}
private ListNode reverse(ListNode head) {
ListNode pre = null;
ListNode post = head;
while(post != null) {
ListNode next = post.next;
post.next = pre;
pre = post;
post = next;
}
return pre;
}
}
3. 回文链表🔺
题目描述
题目链接
给你一个单链表的头节点 head ,请你判断该链表是否为回文链表。如果是,返回 true ;否则,返回 false 。
要求时间复杂度O(n),空间复杂度O(1).
解题思路
- 方法1:如果不是链表是数组的话,就可以用双指针判断是否是回文,所以可以把链表值复制到数组中,然后双指针判断,这样空间复杂度为O(n)
- 方法2:如果空间复杂度必须为O(1)就得修改链表,首先用
快慢指针找到链表的中间节点
,然后反转后一半链表
,就可以判断前一半和后一半是否一样了,最后记得恢复链表(再反转回去)。需要注意循环条件等细节。
代码
方法2:
class Solution {
public boolean isPalindrome(ListNode head) {
ListNode firstHalfEnd = findFirstHalfEnd(head);
ListNode secondHalfStart = reverse(firstHalfEnd.next);
ListNode p1 = head, p2 = secondHalfStart;
boolean ans = true;
while(p1 != null && p2 != null) {
if(p1.val != p2.val) {
ans = false;
break;
}
p1 = p1.next;
p2 = p2.next;
}
firstHalfEnd.next = reverse(secondHalfStart);
// printList(head);
return ans;
}
//快慢指针找中间节点(前一半的尾节点)
ListNode findFirstHalfEnd(ListNode head) {
ListNode slow = head, fast = head;
while(fast.next != null && fast.next.next != null) {
fast = fast.next.next;
slow = slow.next;
}
return slow;
}
//反转链表
ListNode reverseList(ListNode head) {
ListNode p1 = null, p2 = head;
while(p2 != null) {
ListNode tmp = p2.next;
p2.next = p1;
p1 = p2;
p2 = tmp;
}
return p1;
}
}
方法1:
class Solution {
public boolean isPalindrome(ListNode head) {
List<Integer> list = new ArrayList<>();
ListNode curNode = head;
while(curNode != null) {
list.add(curNode.val);
curNode = curNode.next;
}
int i = 0, j = list.size()-1;
while(i<j) {
if(list.get(i) != list.get(j)) {
return false;
}
i++;
j--;
}
return true;
}
}
4. 环形链表⭕
题目描述
题目链接
如果链表中有某个节点,可以通过连续跟踪 next 指针再次到达,则链表中存在环。 为了表示给定链表中的环,评测系统内部使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。注意:pos 不作为参数进行传递 。仅仅是为了标识链表的实际情况。
如果链表中存在环 ,则返回 true 。 否则,返回 false 。
解题思路
- 快慢指针
fast.next != null && fast.next.next != null
代码
/**
* Definition for singly-linked list.
* class ListNode {
* int val;
* ListNode next;
* ListNode(int x) {
* val = x;
* next = null;
* }
* }
*/
public class Solution {
public boolean hasCycle(ListNode head) {
if(head == null) {
return false;
}
ListNode slow = head, fast = head;
while(fast.next != null && fast.next.next != null) {
slow = slow.next;
fast = fast.next.next;
if(slow == fast) {
return true;
}
}
return false;
}
}
5. 环形链表II⭕
题目描述
题目链接
给定一个链表的头节点 head ,返回链表开始入环的第一个节点。 如果链表无环,则返回 null。
如果链表中有某个节点,可以通过连续跟踪 next 指针再次到达,则链表中存在环。 为了表示给定链表中的环,评测系统内部使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。如果 pos 是 -1,则在该链表中没有环。注意:pos 不作为参数进行传递,仅仅是为了标识链表的实际情况。
不允许修改 链表。
解题思路
- 如果有环的话,一个指针从快慢指针相遇的节点开始走,另一个指针从head开始走,各走一步,再次相遇时即为入口节点。
代码
/**
* Definition for singly-linked list.
* class ListNode {
* int val;
* ListNode next;
* ListNode(int x) {
* val = x;
* next = null;
* }
* }
*/
public class Solution {
public ListNode detectCycle(ListNode head) {
if(head == null) {
return null;
}
ListNode slow = head, fast = head;
while(fast.next != null && fast.next.next != null) {
slow = slow.next;
fast = fast.next.next;
if(slow == fast) {
slow = head;
while(slow != fast) {
slow = slow.next;
fast = fast.next;
}
return fast;
}
}
return null;
}
}
6. 合并两个有序链表
题目描述
题目链接
将两个升序链表合并为一个新的 升序 链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。
解题思路
- dummyHead,双指针
代码
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode() {}
* ListNode(int val) { this.val = val; }
* ListNode(int val, ListNode next) { this.val = val; this.next = next; }
* }
*/
class Solution {
public ListNode mergeTwoLists(ListNode list1, ListNode list2) {
ListNode dummyHead = new ListNode();
ListNode p = dummyHead;
while(list1 != null && list2 != null) {
if(list1.val < list2.val) {
p.next = list1;
list1 = list1.next;
} else {
p.next = list2;
list2 = list2.next;
}
p = p.next;
}
if(list1 == null) {
p.next = list2;
} else {
p.next = list1;
}
return dummyHead.next;
}
}
7. 两数相加⭕
题目描述
题目链接
给你两个 非空 的链表,表示两个非负的整数。它们每位数字都是按照 逆序 的方式存储的,并且每个节点只能存储 一位 数字。
请你将两个数相加,并以相同形式返回一个表示和的链表。
你可以假设除了数字 0 之外,这两个数都不会以 0 开头。
解题思路
- 指针为空就加0,否则加val
- 进位
代码
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode() {}
* ListNode(int val) { this.val = val; }
* ListNode(int val, ListNode next) { this.val = val; this.next = next; }
* }
*/
class Solution {
public ListNode addTwoNumbers(ListNode l1, ListNode l2) {
ListNode dummyHead = new ListNode();
ListNode p = dummyHead;
int c = 0;
while(l1 != null || l2 != null) {
int a = l1 == null ? 0 : l1.val;
int b = l2 == null ? 0 : l2.val;
int sum = a + b + c;
p.next = new ListNode(sum%10);
p = p.next;
c = sum / 10;
if(l1 != null) l1 = l1.next;
if(l2 != null) l2 = l2.next;
}
if(c != 0) {
p.next = new ListNode(c);
}
return dummyHead.next;
}
}
8. 删除链表的倒数第N个节点⭕
题目描述
题目链接
给你一个链表,删除链表的倒数第 n 个结点,并且返回链表的头结点。
解题思路
- fast先比slow多走n步
- 因为要删除倒数第n个节点,所以slow要在被删除的节点的前一个节点。考虑删除的是head,这样的话slow最开始要指向head的前一个节点也就是dummyHead。
代码
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode() {}
* ListNode(int val) { this.val = val; }
* ListNode(int val, ListNode next) { this.val = val; this.next = next; }
* }
*/
class Solution {
public ListNode removeNthFromEnd(ListNode head, int n) {
ListNode dummyHead = new ListNode(0, head);
ListNode slow = dummyHead, fast = head;
for(int i=0; i<n; i++) {
fast = fast.next;
}
while(fast != null) {
fast = fast.next;
slow = slow.next;
}
slow.next = slow.next.next;
return dummyHead.next;
}
}
9. 两两交换链表中的节点
题目描述
题目链接
给你一个链表,两两交换其中相邻的节点,并返回交换后链表的头节点。你必须在不修改节点内部的值的情况下完成本题(即,只能进行节点交换)。
解题思路
- dummyHead
代码
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode() {}
* ListNode(int val) { this.val = val; }
* ListNode(int val, ListNode next) { this.val = val; this.next = next; }
* }
*/
class Solution {
public ListNode swapPairs(ListNode head) {
ListNode dummyHead = new ListNode();
dummyHead.next = head;
ListNode p = dummyHead;
while(p.next != null && p.next.next != null) {
ListNode p1 = p.next, p2 = p.next.next;
p1.next = p2.next;
p.next = p2;
p2.next = p1;
p = p1;
}
return dummyHead.next;
}
}
10. k个一组翻转链表🔺
题目描述
题目链接
给你链表的头节点 head ,每 k 个节点一组进行翻转,请你返回修改后的链表。
k 是一个正整数,它的值小于或等于链表的长度。如果节点总数不是 k 的整数倍,那么请将最后剩余的节点保持原有顺序。
你不能只是单纯的改变节点内部的值,而是需要实际进行节点交换。
解题思路
- 找到需要翻转的一段链表的start和end,假设当前需要翻转[start, end]这k个节点,pre指向start节点的前一个,next指向end节点的后一个。为了方便翻转函数reverse的设计,置end.next = null
- 单独写一个翻转链表函数,只需要提供head作为参数,返回翻转后的head。
- 让pre连接上返回的翻转后的head,然后让pre继续指向下一段要翻转的链表的start的前一个
代码
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode() {}
* ListNode(int val) { this.val = val; }
* ListNode(int val, ListNode next) { this.val = val; this.next = next; }
* }
*/
class Solution {
public ListNode reverseKGroup(ListNode head, int k) {
if(k == 1) return head;
ListNode dummyHead = new ListNode(0, head);
ListNode pre = dummyHead, end = dummyHead;
while(end != null) {
for(int i=0; i<k && end != null; i++) {
end = end.next;
}
if(end == null) break;
ListNode start = pre.next;
ListNode next = end.next;
end.next = null;
pre.next = reverse(start);
start.next = next;
pre = start;
end = pre;
}
return dummyHead.next;
}
ListNode reverse(ListNode start) {
ListNode p1 = null, p2 = start;
while(p2 != null) {
ListNode next = p2.next;
p2.next = p1;
p1 = p2;
p2 = next;
}
return p1;
}
}
11. 随机链表的复制🔺
题目描述
题目链接
给你一个长度为 n 的链表,每个节点包含一个额外增加的随机指针 random ,该指针可以指向链表中的任何节点或空节点。
构造这个链表的 深拷贝。 深拷贝应该正好由 n 个 全新 节点组成,其中每个新节点的值都设为其对应的原节点的值。新节点的 next 指针和 random 指针也都应指向复制链表中的新节点,并使原链表和复制链表中的这些指针能够表示相同的链表状态。复制链表中的指针都不应指向原链表中的节点 。
例如,如果原链表中有 X 和 Y 两个节点,其中 X.random --> Y 。那么在复制链表中对应的两个节点 x 和 y ,同样有 x.random --> y 。
返回复制链表的头节点。
用一个由 n 个节点组成的链表来表示输入/输出中的链表。每个节点用一个 [val, random_index] 表示:
val:一个表示 Node.val 的整数。
random_index:随机指针指向的节点索引(范围从 0 到 n-1);如果不指向任何节点,则为 null 。
你的代码 只 接受原链表的头节点 head 作为传入参数。
题目太长了,总之就是深拷贝一个如下图所示的链表,每个节点不仅有next,还有random
解题思路
- 递归的去创建每个新的Node并连接上他的next和random,和前序构造一棵树差不多。
- 注意:需要标记当前节点是否已经构造出来了,不然会重复构造,所以需要用HashMap<原链表节点, 新链表节点>来标记,每创建一个新节点就放进去。
- 也可以不递归,还是需要用Map存储原始节点及其对应的拷贝节点
代码
/*
// Definition for a Node.
class Node {
int val;
Node next;
Node random;
public Node(int val) {
this.val = val;
this.next = null;
this.random = null;
}
}
*/
class Solution {
HashMap<Node, Node> map = new HashMap<>(); //存放原始节点及其对应的拷贝节点
public Node copyRandomList(Node head) {
if(head == null) return null;
if(map.containsKey(head)) {
return map.get(head);
}
Node node = new Node(head.val);
map.put(head, node); //注意:创建之后要先存入map,再进行next和random的赋值,防止死循环
node.next = copyRandomList(head.next);
node.random = copyRandomList(head.random);
return node;
}
}
class Solution {
HashMap<Node, Node> map = new HashMap<>(); //存放原始节点及其对应的拷贝节点
public Node copyRandomList(Node head) {
Node p = head;
while(p != null) {
Node q = null;
if(map.containsKey(p)) {
q = map.get(p);
} else {
q = new Node(p.val);
map.put(p, q);
}
if(p.next == null) {
q.next = null;
} else if(map.containsKey(p.next)) {
q.next = map.get(p.next);
} else {
q.next = new Node(p.next.val);
map.put(p.next, q.next);
}
if(p.random == null) {
q.random = null;
} else if(map.containsKey(p.random)) {
q.random = map.get(p.random);
} else {
q.random = new Node(p.random.val);
map.put(p.random, q.random);
}
p = p.next;
}
return map.get(head);
}
}
12. 排序链表🔺😒
题目描述
题目链接
给你链表的头结点 head ,请将其按 升序 排列并返回 排序后的链表 。
解题思路
方法1:自顶向下的归并排序
- 归并排序的时间复杂度是O(nlogn),空间复杂度是O(logn),其中n是链表长度,空间复杂度主要取决于递归调用的栈空间。
方法2:自底向上归并排序
- subLength从1到开始,每次*2,直到≥length,对subLength长的子链表两两合并
- 代码细节很多需要慢慢调
- 时间复杂度是O(nlogn),空间复杂度是O(1)
代码
方法2:自底向上归并排序
class Solution {
public ListNode sortList(ListNode head) {
if(head == null) {
return head;
}
//统计链表长度
int length = 0;
ListNode p = head;
while(p != null) {
length++;
p = p.next;
}
ListNode dummyHead = new ListNode(0, head);
for(int subLength = 1; subLength < length; subLength <<= 1) {
ListNode pre = dummyHead, cur = dummyHead.next;
while(cur != null) {
ListNode head1 = cur;
//找head2,cur是head1子链表的尾节点
for(int i=1; i<subLength && cur.next != null; i++) {
cur = cur.next;
}
ListNode head2 = cur.next;
cur.next = null; //切开head1和head2两个子链表
cur = head2;
//找head2子链表之后的链表的头,cur是head2子链表的尾节点
for(int i=1; i<subLength && cur != null && cur.next != null; i++) {
cur = cur.next;
}
ListNode next = null; //head2子链表之后的链表的头
if(cur != null) {
next = cur.next;
cur.next = null; //切开head2和next两个子链表
}
ListNode merged = merge(head1, head2);
pre.next = merged;
//pre指向已经合并好的链表的末尾
while(pre.next != null) {
pre = pre.next;
}
cur = next;
}
}
return dummyHead.next;
}
//合并两个有序链表
public ListNode merge(ListNode head1, ListNode head2) {
ListNode dummyHead = new ListNode(0);
ListNode pre = dummyHead, p1 = head1, p2 = head2;
while(p1 != null && p2 != null) {
if(p1.val < p2.val) {
pre.next = p1;
p1 = p1.next;
} else {
pre.next = p2;
p2 = p2.next;
}
pre = pre.next;
}
if(p1 != null) {
pre.next = p1;
} else if(p2 != null) {
pre.next = p2;
}
return dummyHead.next;
}
}
方法1:自顶向下的归并排序
class Solution {
//归并排序
//分成两个链表
//合并两个有序链表
public ListNode sortList(ListNode head) {
return sortList(head, null);
}
public ListNode sortList(ListNode head, ListNode tail) { //左闭右开
if(head == null) {
return null;
}
if(head.next == tail) {
head.next = null; //反正后面要合并,不如把子链表的尾巴置为null,便于合并函数的书写
return head;
}
//快慢指针找中间节点
//注意这里,fast最远只能走到tail,分两种情况:fast恰好走到tail则不能再走了;fast差一步走到tail,因为fast每次走两步,所以也不能再走了
ListNode slow = head, fast = head;
while(fast != tail && fast.next != tail) {
slow = slow.next;
fast = fast.next.next;
}
ListNode mid = slow;
//递归分治
ListNode list1 = sortList(head, mid);
ListNode list2 = sortList(mid, tail);
//合并两个有序链表
ListNode sorted = merge(list1, list2);
return sorted;
}
//合并两个有序链表
public ListNode merge(ListNode head1, ListNode head2) {
ListNode dummyHead = new ListNode(0);
ListNode pre = dummyHead, p1 = head1, p2 = head2;
while(p1 != null && p2 != null) {
if(p1.val < p2.val) {
pre.next = p1;
p1 = p1.next;
} else {
pre.next = p2;
p2 = p2.next;
}
pre = pre.next;
}
if(p1 != null) {
pre.next = p1;
} else if(p2 != null) {
pre.next = p2;
}
return dummyHead.next;
}
}
13. 合并K个升序链表🔺
题目描述
题目链接
给你一个链表数组,每个链表都已经按升序排列。
请你将所有链表合并到一个升序链表中,返回合并后的链表。
解题思路
- 方法1:合并k个升序链表,可以合并两个,再把这个合并后的和另一个合并,...。优化:分组合并
- 方法2:合并两个有序链表是每次从两个链表的当前节点中找最小的,合并k个就从k个链表的当前节点中找最小的,
k个数中的最小值 -> 最小优先队列
代码
最小优先队列:
class Solution {
public ListNode mergeKLists(ListNode[] lists) {
PriorityQueue<ListNode> queue = new PriorityQueue<>(Comparator.comparingInt(node -> node.val));
for(ListNode head : lists) {
if(head != null) {
queue.offer(head);
}
}
ListNode dummyHead = new ListNode();
ListNode p = dummyHead;
while(!queue.isEmpty()) {
p.next = queue.poll();
p = p.next;
if(p.next != null) {
queue.offer(p.next);
}
}
return dummyHead.next;
}
}
14. LRU缓存🔺
题目描述
题目链接
请你设计并实现一个满足 LRU (最近最少使用) 缓存 约束的数据结构。
实现 LRUCache 类:
LRUCache(int capacity) 以 正整数 作为容量 capacity 初始化 LRU 缓存
int get(int key) 如果关键字 key 存在于缓存中,则返回关键字的值,否则返回 -1 。
void put(int key, int value) 如果关键字 key 已经存在,则变更其数据值 value ;如果不存在,则向缓存中插入该组 key-value 。如果插入操作导致关键字数量超过 capacity ,则应该 逐出 最久未使用的关键字。
函数 get 和 put 必须以 O(1) 的平均时间复杂度运行。
上面一堆废话,总结就是:写一个数据结构,存储key-value,容量大小为capacity,遵循最近最少使用原则,即当size超过capacity时,就把最近最少使用的元素删除。实现get、put方法,要求平均时间复杂度为 O(1) 。
解题思路
- 首先key-value需要用
HashMap<key, node>
存,根据key定位node - 为了标记 最近最少使用 的元素,设置一个双向链表,将最近使用的元素放置在链表头部,当超出容量需要删除元素时,删尾部元素。
- 为什么使用双向链表?便于删除node
- 需要额外添加的函数:
将一个node添加到双向链表的头部
将一个节点在双向链表中删除
将一个双向链表中的node移动到链表头部:先删除,再添加到头部
删除双向链表的尾节点
代码
class LRUCache {
//双向链表的节点
class DLinkedNode {
int key;
int value;
DLinkedNode prev;
DLinkedNode next;
public DLinkedNode() {}
public DLinkedNode(int key, int value) {
this.key = key;
this.value = value;
}
}
private Map<Integer, DLinkedNode> cache; //记录key-node映射,用于根据key定位node
private int capacity; //总容量
private int size; //当前占用容量
private DLinkedNode head, tail; //伪头节点/伪尾节点
//初始化
public LRUCache(int capacity) {
cache = new HashMap<>();
this.capacity = capacity;
this.size = 0;
head = new DLinkedNode();
tail = new DLinkedNode();
head.next = tail;
tail.prev = head;
}
public int get(int key) {
DLinkedNode node = cache.get(key);
//如果key不存在返回-1
if(node == null) {
return -1;
}
//如果key存在,移动到链表头部,返回value
moveToHead(node);
return node.value;
}
public void put(int key, int value) {
DLinkedNode node = cache.get(key);
if(node != null) {
//如果key存在,直接修改value,并移动到链表头部
node.value = value;
moveToHead(node);
} else {
//如果key不存在,创建新节点,放到链表头部,加入cache
node = new DLinkedNode(key, value);
addToHead(node);
cache.put(key, node);
size++;
//如果新增节点后超出容量,删除尾部节点(最久未使用的节点)
if(size > capacity) {
DLinkedNode delete = removeTail();
cache.remove(delete.key);
size--;
}
}
}
//将一个node添加到双向链表的头部
private void addToHead(DLinkedNode node) {
node.prev = head;
node.next = head.next;
head.next = node;
node.next.prev = node;
}
//将一个节点在双向链表中删除
private void removeNode(DLinkedNode node) {
node.prev.next = node.next;
node.next.prev = node.prev;
}
//将一个双向链表中的node移动到链表头部
private void moveToHead(DLinkedNode node) {
removeNode(node);
addToHead(node);
}
//删除双向链表的尾节点
private DLinkedNode removeTail() {
DLinkedNode delete = tail.prev;
removeNode(delete);
return delete;
}
}
/**
* Your LRUCache object will be instantiated and called as such:
* LRUCache obj = new LRUCache(capacity);
* int param_1 = obj.get(key);
* obj.put(key,value);
*/
15. 重排链表
题目描述
给定一个单链表 L 的头节点 head ,单链表 L 表示为:
L0 → L1 → … → Ln - 1 → Ln
请将其重新排列后变为:
L0 → Ln → L1 → Ln - 1 → L2 → Ln - 2 → …
不能只是单纯的改变节点内部的值,而是需要实际的进行节点交换。
解题思路
- 快慢指针找中间节点(其实是找第一半的最后一个节点,因为要断开)
- 反转后一半链表
- 合并
代码
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode() {}
* ListNode(int val) { this.val = val; }
* ListNode(int val, ListNode next) { this.val = val; this.next = next; }
* }
*/
class Solution {
public void reorderList(ListNode head) {
ListNode firstEnd = findFirstEnd(head); // 这里找的是第一半的末尾节点,记得断开!
ListNode h2 = firstEnd.next;
firstEnd.next = null;
h2 = reverse(h2);
merge(head, h2);
}
private ListNode findFirstEnd(ListNode head) {
ListNode slow = head;
ListNode fast = head;
while(fast.next != null && fast.next.next != null) {
fast = fast.next.next;
slow = slow.next;
}
return slow;
}
private ListNode reverse(ListNode node) {
ListNode pre = null;
ListNode post = node;
while(post != null) {
ListNode next = post.next;
post.next = pre;
pre = post;
post = next;
}
return pre;
}
private void merge(ListNode h1, ListNode h2) {
ListNode p1 = h1, p2 = h2;
while(p1 != null && p2 != null) {
ListNode next1 = p1.next;
ListNode next2 = p2.next;
p1.next = p2;
p2.next = next1;
p1 = next1;
p2 = next2;
}
}
}
16. 排序奇升偶降链表
题目描述
给定一个奇数位升序,偶数位降序的链表,将其重新排序。
输入: 1->8->3->6->5->4->7->2->NULL
输出: 1->2->3->4->5->6->7->8->NULL
代码
class ListNode {
int val;
ListNode next;
ListNode(int x) {
val = x;
next = null;
}
}
class Solution {
public ListNode sortOddEvenList(ListNode head) {
if (head == null || head.next == null) {
return head;
}
ListNode[] partitionResult = partition(head);
ListNode oddList = partitionResult[0];
ListNode evenList = partitionResult[1];
evenList = reverse(evenList);
return merge(oddList, evenList);
}
public ListNode[] partition(ListNode head) {
ListNode evenHead = head.next;
ListNode odd = head;
ListNode even = evenHead;
while (even!= null && even.next!= null) {
odd.next = even.next;
odd = odd.next;
even.next = odd.next;
even = even.next;
}
odd.next = null;
ListNode[] result = {head, evenHead};
return result;
}
public ListNode reverse(ListNode head) {
ListNode dummy = new ListNode(-1);
ListNode p = head;
while (p!= null) {
ListNode temp = p.next;
p.next = dummy.next;
dummy.next = p;
p = temp;
}
return dummy.next;
}
public ListNode merge(ListNode p, ListNode q) {
ListNode head = new ListNode(-1);
ListNode r = head;
while (p!= null && q!= null) {
if (p.val <= q.val) {
r.next = p;
p = p.next;
} else {
r.next = q;
q = q.val;
}
r = r.next;
}
if (p!= null) {
r.next = p;
}
if (q!= null) {
r.next = q;
}
return head.next;
}
}
17. 删除排序链表中的重复元素II
题目描述
题目链接
给定一个已排序的链表的头 head , 删除原始链表中所有重复数字的节点,只留下不同的数字 。返回 已排序的链表 。
代码
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode() {}
* ListNode(int val) { this.val = val; }
* ListNode(int val, ListNode next) { this.val = val; this.next = next; }
* }
*/
class Solution {
public ListNode deleteDuplicates(ListNode head) {
if (head == null) {
return head;
}
ListNode dummy = new ListNode(0, head);
ListNode cur = dummy;
while (cur.next != null && cur.next.next != null) {
if (cur.next.val == cur.next.next.val) {
int x = cur.next.val;
while (cur.next != null && cur.next.val == x) {
cur.next = cur.next.next;
}
} else {
cur = cur.next;
}
}
return dummy.next;
}
}
删除排序链表中的重复元素I
代码
class Solution {
public ListNode deleteDuplicates(ListNode head) {
if (head == null) {
return head;
}
ListNode cur = head;
while (cur.next != null) {
if (cur.val == cur.next.val) {
cur.next = cur.next.next;
} else {
cur = cur.next;
}
}
return head;
}
}
八、二叉树
1. 二叉树的中序遍历
题目描述
题目链接
给定一个二叉树的根节点 root ,返回 它的 中序 遍历 。
解题思路
- 方法1:递归
- 方法2:迭代,用栈模拟递归的过程
对于一个cur节点,如果它的左孩子不为空,就将cur入栈,继续移动到它的左孩子,把左孩子当成cur
如果cur的左孩子为空,处理cur,然后移动到它的右孩子,把右孩子当成cur
代码
方法1:递归
/**
* 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 {
List<Integer> ans = new ArrayList<>();
public List<Integer> inorderTraversal(TreeNode root) {
inorder(root);
return ans;
}
void inorder(TreeNode root) {
if(root == null) {
return;
}
inorder(root.left);
ans.add(root.val);
inorder(root.right);
}
}
方法2:迭代
class Solution {
public List<Integer> inorderTraversal(TreeNode root) {
List<Integer> ans = new ArrayList<>();
Deque<TreeNode> stack = new LinkedList<>();
TreeNode cur = root;
while(cur != null || !stack.isEmpty()) {
if(cur != null) {
stack.push(cur); //入栈
cur = cur.left; //找左孩子
} else {
//处理中间节点
cur = stack.pop();
ans.add(cur.val);
//转到右孩子
cur = cur.right;
}
}
return ans;
}
}
2. 二叉树的最大深度
题目描述
题目链接
给定一个二叉树 root ,返回其最大深度。
二叉树的 最大深度 是指从根节点到最远叶子节点的最长路径上的节点数。
解题思路
- 后序遍历
代码
//root的深度 = max(左孩子的深度,右孩子的深度) + 1
class Solution {
public int maxDepth(TreeNode root) {
if(root == null) {
return 0;
}
return Math.max(maxDepth(root.left), maxDepth(root.right)) + 1;
}
}
3. 翻转二叉树
题目描述
题目链接
给你一棵二叉树的根节点 root ,翻转这棵二叉树,并返回其根节点。
解题思路
- 后序遍历,翻转完root的左右子树,再交换root的左右子树
- 切记不要直接赋值
root.left = invertTree(root.left);
,不然再翻转右子树的时候就是翻转翻转过的子树了
代码
class Solution {
public TreeNode invertTree(TreeNode root) {
if(root == null) {
return root;
}
TreeNode left = invertTree(root.left);
TreeNode right = invertTree(root.right);
root.left = right;
root.right = left;
return root;
}
}
4. 对称二叉树⭕
题目描述
题目链接
给你一个二叉树的根节点 root , 检查它是否轴对称。
解题思路
- 对于root,先比较其左右孩子,如果不相等直接返回false;如果相等,再分别比较其
left.left和right.right
,left.right和right.left
- 方法1:递归,先序遍历
- 方法2:迭代,使用队列模拟
代码
方法1:递归
class Solution {
public boolean isSymmetric(TreeNode root) {
if(root == null) return true;
return compare(root.left, root.right);
}
boolean compare(TreeNode left, TreeNode right) {
if(left == null && right == null) return true;
if(left == null || right == null) return false;
if(left.val != right.val) return false;
return compare(left.left, right.right) && compare(left.right, right.left);
}
}
方法2:迭代
class Solution {
public boolean isSymmetric(TreeNode root) {
if(root == null) return true;
Queue<TreeNode> queue = new LinkedList<>();
queue.offer(root.left);
queue.offer(root.right);
while(!queue.isEmpty()) {
TreeNode left = queue.poll();
TreeNode right = queue.poll();
if(left == null && right == null) {
continue;
}
if(left == null || right == null || left.val != right.val) {
return false;
}
queue.offer(left.left);
queue.offer(right.right);
queue.offer(left.right);
queue.offer(right.left);
}
return true;
}
}
5. 二叉树的直径⭕
题目描述
题目链接
给你一棵二叉树的根节点,返回该树的 直径 。
二叉树的 直径 是指树中任意两个节点之间最长路径的 长度 。这条路径可能经过也可能不经过根节点 root 。
两节点之间路径的 长度 由它们之间边数表示。
解题思路
- 最长路径有可能经过
root
,也可能不经过,但是一定会经过一个根节点r
,那么长度就是r的左子树的深度 + 右子树的深度 + 1
。但是在哪个r取得最大值是不确定的,所以要设置一个全局变量记录最大值,而递归函数用于返回root的深度。 - 注意:路径长度 = 路径结点数 - 1
- 这道题和
二叉树的最大深度
的区别就在于需要设置一个全局变量记录最大值
代码
class Solution {
int ans = 0;
public int diameterOfBinaryTree(TreeNode root) {
depth(root);
return ans - 1;
}
int depth(TreeNode root) {
if(root == null) {
return 0;
}
int L = depth(root.left);
int R = depth(root.right);
ans = Math.max(ans, L + R + 1);
return Math.max(L, R) + 1;
}
}
6. 二叉树的层序遍历
题目描述
题目链接
给你二叉树的根节点 root ,返回其节点值的 层序遍历 。 (即逐层地,从左到右访问所有节点)。
解题思路
- 使用队列,因为每一层要单独存放,所以可以在取出每一层的第一个节点之前,先获取队列的长度length(当前层的节点数),并且设置一个新的空list用于存储当前层的答案,然后for(length)循环处理这一层的节点。
代码
class Solution {
public List<List<Integer>> levelOrder(TreeNode root) {
List<List<Integer>> ans = new ArrayList<>();
Queue<TreeNode> queue = new LinkedList<>();
if(root == null) {
return ans;
}
queue.offer(root);
while(!queue.isEmpty()) {
int length = queue.size();
List<Integer> list = new ArrayList<>();
for(int i=0; i<length; i++) {
TreeNode cur = queue.poll();
list.add(cur.val);
if(cur.left != null) {
queue.offer(cur.left);
}
if(cur.right != null) {
queue.offer(cur.right);
}
}
ans.add(list);
}
return ans;
}
}
7. 将有序数组转换为二叉搜索树⭕
题目描述
题目链接
给你一个整数数组 nums ,其中元素已经按 升序 排列,请你将其转换为一棵 高度平衡 二叉搜索树。
高度平衡 二叉树是一棵满足「每个节点的左右两个子树的高度差的绝对值不超过 1 」的二叉树。
解题思路
- 二分法 + 递归
- 每次分两半,这样一定是平衡的,每一半都要找到各自的中间节点作为返回值,连在root的两边
代码
class Solution {
public TreeNode sortedArrayToBST(int[] nums) {
return backtrack(nums, 0, nums.length);
}
TreeNode backtrack(int[] nums, int l, int r) { //左闭右开
if(l >= r) {
return null;
}
int mid = l + (r - l) / 2;
TreeNode root = new TreeNode(nums[mid]);
root.left = backtrack(nums, l, mid);
root.right = backtrack(nums, mid + 1, r);
return root;
}
}
8. 验证二叉搜索树⭕
题目描述
题目链接
给你一个二叉树的根节点 root ,判断其是否是一个有效的二叉搜索树。
有效 二叉搜索树定义如下:
节点的左子树只包含 小于 当前节点的数。
节点的右子树只包含 大于 当前节点的数。
所有左子树和右子树自身必须也是二叉搜索树。
解题思路
- 二叉搜索树的中序遍历是递增的,设置一个preVal记录前一个值,和当前值进行比较,如果是false要直接返回false,如果是true要继续判断。
- 比
Integer.MIN_VALUE
更小的数,Long.MIN_VALUE
。
代码
class Solution {
long preVal = Long.MIN_VALUE;
public boolean isValidBST(TreeNode root) {
if(root == null) {
return true;
}
boolean left = isValidBST(root.left);
if(left == false) return false;
if(root.val <= preVal) return false;
preVal = root.val;
return isValidBST(root.right);
}
}
9. 二叉搜索树中第K小的元素⭕
题目描述
题目链接
给定一个二叉搜索树的根节点 root ,和一个整数 k ,请你设计一个算法查找其中第 k 个最小元素(从 1 开始计数)。
解题思路
- BST的中序遍历是递增的,但是递归不方便返回,需要遍历整棵树?所以用迭代中序遍历,可以直接返回
- 迭代:用栈模拟
代码
class Solution {
int curNum = 0;
public int kthSmallest(TreeNode root, int k) {
Deque<TreeNode> stack = new LinkedList<>();
TreeNode cur = root;
while(cur != null || !stack.isEmpty()) {
if(cur != null) {
stack.push(cur);
cur = cur.left;
} else {
cur = stack.pop();
k--;
if(k == 0) {
return cur.val;
}
cur = cur.right;
}
}
return cur.val;
}
}
10. 二叉树的右视图
题目描述
题目链接
给定一个二叉树的 根节点 root,想象自己站在它的右侧,按照从顶部到底部的顺序,返回从右侧所能看到的节点值。
解题思路
- 层序遍历,只保存每一层的最后一个节点
代码
class Solution {
public List<Integer> rightSideView(TreeNode root) {
Queue<TreeNode> queue = new LinkedList<>();
List<Integer> ans = new ArrayList<>();
if(root != null) {
queue.offer(root);
}
while(!queue.isEmpty()) {
int length = queue.size();
TreeNode node = null;
for(int i=0; i<length; i++) {
node = queue.poll();
if(node.left != null) queue.offer(node.left);
if(node.right != null) queue.offer(node.right);
}
ans.add(node.val);
}
return ans;
}
}
第9、10题分别是用
栈模拟递归进行中序遍历
、用队列进行层序遍历
,这是二叉树题目里为数不多的迭代方法之二
11. 二叉树展开为链表🔺
题目描述
题目链接
给你二叉树的根结点 root ,请你将它展开为一个单链表:
展开后的单链表应该同样使用 TreeNode ,其中 right 子指针指向链表中下一个结点,而左子指针始终为 null 。
展开后的单链表应该与二叉树 先序遍历 顺序相同。
解题思路
- 方法1:用一个List
记录前序遍历顺序,最后再构建 - 方法2:空间复杂度为O(1)。对于当前node来说,node的左子树的最右边的节点的右孩子应该连到node的右孩子,所以:先记录下node的左孩子,然后找到node的左子树的最右边的节点,让它的右孩子指针指向node的右孩子,然后让node的右孩子指针指向刚才记录下的node的左孩子,让node的左孩子指针指向null。然后让node指向node的右孩子,继续这么处理。
代码
方法2:
class Solution {
public void flatten(TreeNode root) {
TreeNode cur = root;
while(cur != null) {
//只有cur的左子树不为null才需要处理左子树
if(cur.left != null) {
TreeNode next = cur.left; //记录cur下一个要处理的节点,也就是cur将来的右孩子
TreeNode go = next; //寻找cur的左子树中的最右边的节点go
while(go.right != null) {
go = go.right;
}
go.right = cur.right; //go的右孩子指向cur的右孩子
cur.right = next; //cur的左孩子指向刚才记录的下一个要处理的节点
cur.left = null; //cur的左孩子置空
}
cur = cur.right; //处理cur的下一个节点
}
}
}
方法1:递归
class Solution {
List<TreeNode> list = new ArrayList<>();
public void flatten(TreeNode root) {
TreeNode pre = root;
preorder(root);
for(int i=1; i<list.size(); i++) {
pre.right = list.get(i);
pre.left = null;
pre = pre.right;
}
}
void preorder(TreeNode root) {
if(root == null) return;
list.add(root);
preorder(root.left);
preorder(root.right);
}
}
方法1:迭代
class Solution {
public void flatten(TreeNode root) {
List<TreeNode> list = new ArrayList<>();
Deque<TreeNode> stack = new LinkedList<>();
TreeNode cur = root;
while(cur != null || !stack.isEmpty()) {
while(cur != null) {
list.add(cur);
stack.push(cur);
cur = cur.left;
}
cur = stack.pop().right;
}
TreeNode pre = root;
for(int i=1; i<list.size(); i++) {
pre.right = list.get(i);
pre.left = null;
pre = pre.right;
}
}
}
12. 从前序与中序遍历序列构造二叉树⭕
题目描述
题目链接
给定两个整数数组 preorder 和 inorder ,其中 preorder 是二叉树的先序遍历, inorder 是同一棵树的中序遍历,请构造二叉树并返回其根节点。
解题思路
- 首先在preorder中找到当前根节点root(当前区间的第一个节点),然后根据root将inorder划分成左右子树两半,然后再根据inorder的划分结果将preorder划分成左右子树两半,然后分别递归地构建左右子树。
- 如何找到root在inorder中的位置,也就是cutPoint的位置呢?因为题目节点值是不重复的,所以可以用哈希map存储:
HashMap<inorder中的值, 在inorder中的下标>
代码
class Solution {
Map<Integer, Integer> map = new HashMap<>();
public TreeNode buildTree(int[] preorder, int[] inorder) {
for(int i=0; i<inorder.length; i++) {
map.put(inorder[i], i);
}
return backtrack(preorder, 0, preorder.length, 0, inorder.length);
}
TreeNode backtrack(int[] preorder, int preBegin, int preEnd, int inBegin, int inEnd) { //左闭右开
if(preBegin == preEnd) return null;
int rootValue = preorder[preBegin];
TreeNode root = new TreeNode(rootValue);
if(preEnd - preBegin == 1) {
return root;
}
//在preorder中找到的根节点将inorder分成两半
int cutPoint = map.get(rootValue);
int leftInBegin = inBegin;
int leftInEnd = cutPoint;
int rightInBegin = cutPoint + 1;
int rightInEnd = inEnd;
//再根据inorder的分割结果将preorder分割
int leftPreBegin = preBegin + 1;
int leftPreEnd = leftPreBegin + (leftInEnd - leftInBegin);
int rightPreBegin = leftPreEnd;
int rightPreEnd = preEnd;
root.left = backtrack(preorder, leftPreBegin, leftPreEnd, leftInBegin, leftInEnd);
root.right = backtrack(preorder, rightPreBegin, rightPreEnd, rightInBegin, rightInEnd);
return root;
}
}
13. 路径总和III🔺
题目描述
题目链接
给定一个二叉树的根节点 root ,和一个整数 targetSum ,求该二叉树里节点值之和等于 targetSum 的 路径 的数目。
路径 不需要从根节点开始,也不需要在叶子节点结束,但是路径方向必须是向下的(只能从父节点到子节点)。
解题思路
- 方法1:递归遍历每一个节点node,再递归得到以node为起点的路径中和为target的路径
- 方法2:前缀和,前序遍历
用一个HashMap记录从root到当前节点的路径上的前缀和以及出现的次数
,注意,只记录从root到当前节点的路径上的前缀和以及出现的次数,所以要在处理完当前层逻辑之后,递归回溯之前,恢复状态,取出当前节点的前缀和数量
代码
方法2:
class Solution {
Map<Long, Integer> prefixSumCount = new HashMap<>(); //记录从root到当前节点的路径上的前缀和以及出现的次数
int ans = 0;
public int pathSum(TreeNode root, int targetSum) {
prefixSumCount.put(0L, 1); //前缀和为0的路径设为1,不然后面全是0
backtrack(root, targetSum, 0);
return ans;
}
void backtrack(TreeNode node, int targetSum, long curSum) {
if(node == null) {
return;
}
//从root到node的路径和(包括node)
curSum += node.val;
//在root到node这条路径上,以node结尾的路径中,满足条件的路径数
ans += prefixSumCount.getOrDefault(curSum - targetSum, 0);
//更新前缀和及次数
prefixSumCount.put(curSum, prefixSumCount.getOrDefault(curSum, 0) + 1);
//递归进入下一层
backtrack(node.left, targetSum, curSum);
backtrack(node.right, targetSum, curSum);
//回到本层,恢复状态,取出当前节点的前缀和数量
prefixSumCount.put(curSum, prefixSumCount.get(curSum) - 1);
}
}
方法1:自己写的好理解的代码
class Solution {
int ans = 0;
public int pathSum(TreeNode root, int targetSum) {
preorder(root, targetSum);
return ans;
}
//前序遍历树
void preorder(TreeNode root, int targetSum) {
if(root == null) {
return;
}
ans += rootSum(root, targetSum);
preorder(root.left, targetSum);
preorder(root.right, targetSum);
}
//rootSum: 以root为起点向下且路径和为targetSum的路径数
int rootSum(TreeNode root, long targetSum) {
if(root == null) {
return 0;
}
int ret = 0;
long val = (long) root.val;
if(val == targetSum) {
ret++;
}
ret += rootSum(root.left, targetSum - val);
ret += rootSum(root.right, targetSum - val);
return ret;
}
}
方法1:题解写的不太好理解但有点高级的代码
class Solution {
public int pathSum(TreeNode root, int targetSum) {
if(root == null) {
return 0;
}
int ret = rootSum(root, targetSum);
ret += pathSum(root.left, targetSum);
ret += pathSum(root.right, targetSum);
return ret;
}
//rootSum: 以root为起点向下且路径和为targetSum的路径数
int rootSum(TreeNode root, long targetSum) {
if(root == null) {
return 0;
}
int ret = 0;
long val = (long) root.val;
if(val == targetSum) {
ret++;
}
ret += rootSum(root.left, targetSum - val);
ret += rootSum(root.right, targetSum - val);
return ret;
}
}
14. 二叉树的最近公共祖先⭕
题目描述
题目链接
给定一个二叉树, 找到该树中两个指定节点的最近公共祖先。
百度百科中最近公共祖先的定义为:“对于有根树 T 的两个节点 p、q,最近公共祖先表示为一个节点 x,满足 x 是 p、q 的祖先且 x 的深度尽可能大(一个节点也可以是它自己的祖先)。
解题思路
- 思路在代码里了,画个回溯的图更好理解
代码
class Solution {
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
if(root == null || root == p || root == q) return root; //遇见p或q之一就不用往下遍历这个子树了,懂的都懂
TreeNode left = lowestCommonAncestor(root.left, p, q);
TreeNode right = lowestCommonAncestor(root.right, p, q);
if(left != null && right != null) { //此时p和q分别在root的两侧
return root;
}
if(left == null && right == null) { //此时root的左右子树中都没有p或q
return null;
}
return left == null ? right : left; //此时只有root的一个子树中有 p 或 q 或 p和q
}
}
15. 二叉树中的最大路径和🔺
题目描述
题目链接
二叉树中的 路径 被定义为一条节点序列,序列中每对相邻节点之间都存在一条边。同一个节点在一条路径序列中 至多出现一次 。该路径 至少包含一个 节点,且不一定经过根节点。
路径和 是路径中各节点值的总和。
给你一个二叉树的根节点 root ,返回其 最大路径和。
解题思路
- 和
二叉树的直径
很像,但是需要注意这里的节点值有可能为负,所以计算贡献值时要抛弃负数。
代码
class Solution {
int ans = Integer.MIN_VALUE;
public int maxPathSum(TreeNode root) {
maxGain(root);
return ans;
}
//在计算以root为起点的路径的最大和的同时,计算以root为中间节点的路径的最大和,并更新最大值ans
int maxGain(TreeNode root) {
if(root == null) {
return 0;
}
//递归计算左右子节点的最大贡献值
int leftGain = Math.max(0, maxGain(root.left));
int rightGain = Math.max(0, maxGain(root.right));
//计算以root为中间节点的路径的最大和
ans = Math.max(ans, root.val + leftGain + rightGain);
//返回root节点的最大贡献值
return root.val + Math.max(leftGain, rightGain);
}
}
16. 求根节点到叶节点数字之和
题目描述
题目链接
给你一个二叉树的根节点 root ,树中每个节点都存放有一个 0 到 9 之间的数字。
每条从根节点到叶节点的路径都代表一个数字:
例如,从根节点到叶节点的路径 1 -> 2 -> 3 表示数字 123 。
计算从根节点到叶节点生成的 所有数字之和 。
叶节点 是指没有子节点的节点。
代码
class Solution {
public int sumNumbers(TreeNode root) {
return dfs(root, 0);
}
public int dfs(TreeNode root, int prevSum) {
if (root == null) {
return 0;
}
int sum = prevSum * 10 + root.val;
if (root.left == null && root.right == null) {
return sum;
} else {
return dfs(root.left, sum) + dfs(root.right, sum);
}
}
}
17. 二叉树最大宽度
题目描述
题目链接
给你一棵二叉树的根节点 root ,返回树的 最大宽度 。
树的 最大宽度 是所有层中最大的 宽度 。
每一层的 宽度 被定义为该层最左和最右的非空节点(即,两个端点)之间的长度。将这个二叉树视作与满二叉树结构相同,两端点间会出现一些延伸到这一层的 null 节点,这些 null 节点也计入长度。
题目数据保证答案将会在 32 位 带符号整数范围内。
代码
class Solution {
Map<Integer, Integer> levelMin = new HashMap<Integer, Integer>();
public int widthOfBinaryTree(TreeNode root) {
return dfs(root, 1, 1);
}
public int dfs(TreeNode node, int depth, int index) {
if (node == null) {
return 0;
}
levelMin.putIfAbsent(depth, index); // 每一层最先访问到的节点会是最左边的节点,即每一层编号的最小值
return Math.max(index - levelMin.get(depth) + 1, Math.max(dfs(node.left, depth + 1, index * 2), dfs(node.right, depth + 1, index * 2 + 1)));
}
}
18. 子结构判断
题目描述
题目链接
给定两棵二叉树 tree1 和 tree2,判断 tree2 是否以 tree1 的某个节点为根的子树具有 相同的结构和节点值 。
注意,空树 不会是以 tree1 的某个节点为根的子树具有 相同的结构和节点值 。
代码
class Solution {
public boolean isSubStructure(TreeNode A, TreeNode B) {
return (A != null && B != null) && (recur(A, B) || isSubStructure(A.left, B) || isSubStructure(A.right, B));
}
boolean recur(TreeNode A, TreeNode B) {
if(B == null) return true;
if(A == null || A.val != B.val) return false;
return recur(A.left, B.left) && recur(A.right, B.right);
}
}
19. 将二叉搜索树转化为排序的双向链表
题目描述
题目链接
将一个 二叉搜索树 就地转化为一个 已排序的双向循环链表 。
对于双向循环列表,你可以将左右孩子指针作为双向循环链表的前驱和后继指针,第一个节点的前驱是最后一个节点,最后一个节点的后继是第一个节点。
特别地,我们希望可以 就地 完成转换操作。当转化完成以后,树中节点的左指针需要指向前驱,树中节点的右指针需要指向后继。还需要返回链表中最小元素的指针。
代码
/*
// Definition for a Node.
class Node {
public int val;
public Node left;
public Node right;
public Node() {}
public Node(int _val) {
val = _val;
}
public Node(int _val,Node _left,Node _right) {
val = _val;
left = _left;
right = _right;
}
};
*/
class Solution {
Node first = null;
Node last = null;
public Node treeToDoublyList(Node root) {
if (root == null) {
return null;
}
inorder(root);
first.left = last;
last.right = first;
return first;
}
private void inorder(Node node) {
if (node == null) {
return;
}
inorder(node.left);
if (last == null) {
first = node;
} else {
last.right = node;
node.left = last;
}
last = node;
inorder(node.right);
}
}
20. 二叉树的完全性校验
题目描述
题目链接
给你一棵二叉树的根节点 root ,请你判断这棵树是否是一棵 完全二叉树 。
在一棵 完全二叉树 中,除了最后一层外,所有层都被完全填满,并且最后一层中的所有节点都尽可能靠左。最后一层(第 h 层)中可以包含 1 到 2h 个节点。
解题思路
- 完全二叉树的层序遍历中,null不会出现在非null节点前,也就是说null只会出现在层序遍历的末尾。
代码
class Solution {
public boolean isCompleteTree(TreeNode root) {
Deque<TreeNode> q = new LinkedList<>();
q.offer(root);
boolean flag = false;
while(!q.isEmpty()) {
TreeNode node = q.poll();
if(node == null) {
flag = true;
} else {
if(flag) {
return false;
}
q.offer(node.left);
q.offer(node.right);
}
}
return true;
}
}
九、图论
1. 岛屿数量
题目描述
题目链接
给你一个由 '1'(陆地)和 '0'(水)组成的的二维网格,请你计算网格中岛屿的数量。
岛屿总是被水包围,并且每座岛屿只能由水平方向和/或竖直方向上相邻的陆地连接形成。
此外,你可以假设该网格的四条边均被水包围。
解题思路
- dfs
- bfs
代码
dfs:
class Solution {
int[][] directions = {{-1, 0}, {1, 0}, {0, -1}, {0, 1}};
boolean[][] visited;
int m;
int n;
public int numIslands(char[][] grid) {
m = grid.length;
n = grid[0].length;
visited = new boolean[m][n];
int ans = 0;
for(int i=0; i<m; i++) {
for(int j=0; j<n; j++) {
if(!visited[i][j] && grid[i][j] == '1') {
dfs(grid, i, j);
ans++;
}
}
}
return ans;
}
void dfs(char[][] grid, int curX, int curY) {
visited[curX][curY] = true;
for(int i=0; i<4; i++) {
int newX = curX + directions[i][0];
int newY = curY + directions[i][1];
if(newX < 0 || newX >= m || newY < 0 || newY >= n) {
continue;
}
if(grid[newX][newY] == '1' && !visited[newX][newY]) {
dfs(grid, newX, newY);
}
}
}
}
bfs:
class Solution {
int[][] directions = {{-1, 0}, {1, 0}, {0, -1}, {0, 1}};
boolean[][] visited;
int m;
int n;
public int numIslands(char[][] grid) {
m = grid.length;
n = grid[0].length;
visited = new boolean[m][n];
int ans = 0;
for(int i=0; i<m; i++) {
for(int j=0; j<n; j++) {
if(!visited[i][j] && grid[i][j] == '1') {
bfs(grid, i, j);
ans++;
}
}
}
return ans;
}
void bfs(char[][] grid, int curX, int curY) {
Queue<int[]> queue = new LinkedList<>();
queue.offer(new int[]{curX, curY});
visited[curX][curY] = true;
while(!queue.isEmpty()) {
int[] cur = queue.poll();
for(int i=0; i<4; i++) {
int newX = cur[0] + directions[i][0];
int newY = cur[1] + directions[i][1];
if(newX < 0 || newX >= m || newY < 0 || newY >= n) {
continue;
}
if(!visited[newX][newY] && grid[newX][newY] == '1') {
visited[newX][newY] = true;
queue.offer(new int[]{newX, newY});
}
}
}
}
}
2. 腐烂的橘子🔺
题目描述
题目链接
在给定的 m x n 网格 grid 中,每个单元格可以有以下三个值之一:
值 0 代表空单元格;
值 1 代表新鲜橘子;
值 2 代表腐烂的橘子。
每分钟,腐烂的橘子 周围 4 个方向上相邻 的新鲜橘子都会腐烂。
返回 直到单元格中没有新鲜橘子为止所必须经过的最小分钟数。如果不可能,返回 -1 。
解题思路
- 图未必是树,但是树一定是图啊。虽然这个图它不是个树,但是可以借鉴树的思路。
BFS 可以看成是层序遍历。从某个结点出发,BFS 首先遍历到距离为 1 的结点,然后是距离为 2、3、4…… 的结点。因此,BFS 可以用来求最短路径问题。BFS 先搜索到的结点,一定是距离最近的结点。再看看这道题的题目要求:返回直到单元格中没有新鲜橘子为止所必须经过的最小分钟数。翻译一下,实际上就是求腐烂橘子到所有新鲜橘子的最短路径。那么这道题使用 BFS,应该是毫无疑问的了。
但是用 BFS 来求最短路径的话,这个队列中第 1 层和第 2 层的结点会紧挨在一起,无法区分。因此,我们需要稍微修改一下代码,在每一层遍历开始前,记录队列中的结点数量 n ,然后一口气处理完这一层的 n 个结点,也就是多源广度优先搜索。
- 没错,重点就是:在每一层遍历开始前,记录队列中的结点数量 n ,然后一口气处理完这一层的 n 个结点!
- 小细节:1. 在最开始就将所有烂橘子放到队列里。2. 设置一个count记录好橘子的数量,每次腐坏一个好橘子count--,最后如果count为0就返回round,不为0就返回-1
代码
class Solution {
public int orangesRotting(int[][] grid) {
int[][] directions = {{-1, 0}, {1, 0}, {0, -1}, {0, 1}};
int m = grid.length;
int n = grid[0].length;
Queue<int[]> queue = new LinkedList<>();
int count = 0;
for(int i=0; i<m; i++) {
for(int j=0; j<n; j++) {
if(grid[i][j] == 1) {
count++;
} else if(grid[i][j] == 2) {
queue.offer(new int[]{i, j});
}
}
}
int round = 0;
while(count > 0 && !queue.isEmpty()) {
round++;
int size = queue.size();
for(int i=0; i<size; i++) {
int[] cur = queue.poll();
for(int j=0; j<4; j++) {
int newX = cur[0] + directions[j][0];
int newY = cur[1] + directions[j][1];
if(newX < 0 || newX >= m || newY < 0 || newY >= n) {
continue;
}
if(grid[newX][newY] == 1) {
grid[newX][newY] = 2;
count--;
queue.offer(new int[]{newX, newY});
}
}
}
}
return count > 0 ? -1 : round;
}
}
3. 课程表🔺
题目描述
题目链接
你这个学期必须选修 numCourses
门课程,记为 0
到 numCourses - 1
。
在选修某些课程之前需要一些先修课程。 先修课程按数组 prerequisites
给出,其中 prerequisites[i] = [ai, bi]
,表示如果要学习课程 ai
则 必须 先学习课程 bi
。
例如,先修课程对 [0, 1] 表示:想要学习课程 0 ,你需要先完成课程 1 。
请你判断是否可能完成所有课程的学习?如果可以,返回 true ;否则,返回 false 。
解题思路
- 拓扑排序,判断有向图是否存在有向环
给定一个包含 n 个节点的有向图 G,我们给出它的节点编号的一种排列,如果满足:
对于图 G 中的任意一条有向边 (u,v),u 在排列中都出现在 v 的前面。
那么称该排列是图 G 的「拓扑排序」。根据上述的定义,我们可以得出两个结论:
如果图 G 中存在环(即图 G 不是「有向无环图」),那么图 G 不存在拓扑排序。
如果图 G 是有向无环图,那么它的拓扑排序可能不止一种。举一个最极端的例子,如果图 G 值包含 n 个节点却没有任何边,那么任意一种编号的排列都可以作为拓扑排序。
- 方法1:DFS
不太好想也不太好记住,还是看BFS吧
- 方法2:BFS
让入度为 0 的课入列,它们是能直接选的课。
然后逐个出列,出列代表着课被选,需要减小相关课的入度。
如果相关课的入度新变为 0,安排它入列、再出列……直到没有入度为 0 的课可入列。BFS 前的准备工作:
每门课的入度需要被记录,我们关心入度值的变化。-> int[] indegree
课程之间的依赖关系也要被记录,我们关心选当前课会减小哪些课的入度。-> List<List> edges 怎么判断能否修完所有课?
BFS 结束时,如果仍有课的入度不为 0,无法被选,完成不了所有课。否则,能找到一种顺序把所有课上完。
或者:用一个变量 count 记录入列的顶点个数,最后判断 count 是否等于总课程数。
代码
方法2:BFS
写代码的时候就想象自己在上本科的数据结构在用手画图模拟拓扑排序的过程,逻辑是一样的,只不过写代码需要选择合适的数据结构。
多做几次这就是一个拓扑排序的模板题,如果还需要找到一种拓扑排序,只需要用一个列表记录在出队时记录一下就可以了。
class Solution {
List<List<Integer>> edges = new ArrayList<>(); //每个节点有一个可以直达的节点列表 -> 邻接表
int[] indegree; //入度
public boolean canFinish(int numCourses, int[][] prerequisites) {
for(int i=0; i<numCourses; i++) {
edges.add(new ArrayList<>());
}
indegree = new int[numCourses];
for(int[] info : prerequisites) {
edges.get(info[1]).add(info[0]);
indegree[info[0]]++;
}
Queue<Integer> queue = new LinkedList<>(); //存储当前入度为0,且尚未加入拓扑排序的全部节点
for(int i=0; i<numCourses; i++) {
if(indegree[i] == 0) {
queue.offer(i);
}
}
int visited = 0; //记录已经加入拓扑排序的节点
while(!queue.isEmpty()) {
//从当前入度为0的节点中取出一个,挨个处理其直达的节点v
int u = queue.poll();
visited++;
for(int v : edges.get(u)) {
indegree[v]--;
if(indegree[v] == 0) {
queue.offer(v);
}
}
}
return visited == numCourses; //是否还有没加入拓扑排序的节点
}
}
方法1:DFS
class Solution {
List<List<Integer>> edges = new ArrayList<>(); //每个节点有一个可以直达的节点列表
int[] visited; // 0未搜索 1搜索中 2已完成
boolean valid = true; //当存在环时马上置为false,代表此图不存在拓扑排序,并及时返回
public boolean canFinish(int numCourses, int[][] prerequisites) {
for(int i=0; i<numCourses; i++) {
edges.add(new ArrayList<>());
}
for(int[] info : prerequisites) {
edges.get(info[1]).add(info[0]);
}
visited = new int[numCourses];
for(int i=0; i<numCourses && valid; i++) {
if(visited[i] == 0) {
dfs(i);
}
}
return valid;
}
void dfs(int u) {
visited[u] = 1; //节点u搜索中
for(int v : edges.get(u)) {
if(visited[v] == 1) {
valid = false;
return;
}
if(visited[v] == 0) {
dfs(v);
if(!valid) {
return; //!及时返回
}
}
}
visited[u] = 2; //节点u已完成
}
}
3. 检测循环依赖
题目描述
现有n个编译项,编号为0 ~ n-1。给定一个二维数组,表示编译项之间有依赖关系。如[0, 1]表示1依赖于0。
若存在循环依赖则返回空;不存在依赖则返回可行的编译顺序。
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.Queue;
public class Main {
public static void main(String[] args) {
int[][] prerequisites = {{0,2}, {1,2},{2,3},{2,4}};
System.out.println(haveCircularDependency(5, prerequisites));
}
public static List<Integer> haveCircularDependency(int n, int[][] prerequisites) {
List<Integer>[] g = new ArrayList[n];
for (int i = 0; i < n; i++) {
g[i] = new ArrayList<>();
}
int[] indeg = new int[n];
for (int[] prerequisite : prerequisites) {
int a = prerequisite[0], b = prerequisite[1];
g[a].add(b);
indeg[b]++;
}
Queue<Integer> q = new LinkedList<>();
for (int i = 0; i < n; i++) {
if (indeg[i] == 0) {
q.add(i);
}
}
List<Integer> res = new ArrayList<>();
while (!q.isEmpty()) {
int t = q.poll();
res.add(t);
List<Integer> list = g[t];
for (int j : list) {
indeg[j]--;
if (indeg[j] == 0) {
q.add(j);
}
}
}
if (res.size() == n) {
return res;
} else {
return new ArrayList<>();
}
}
}
4. 实现Trie(前缀树)🔺
题目描述
题目链接
Trie(发音类似 "try")或者说 前缀树 是一种树形数据结构,用于高效地存储和检索字符串数据集中的键。这一数据结构有相当多的应用情景,例如自动补完和拼写检查。
请你实现 Trie 类:
- Trie() 初始化前缀树对象。
- void insert(String word) 向前缀树中插入字符串 word 。
- boolean search(String word) 如果字符串 word 在前缀树中,返回 true(即,在检索之前已经插入);否则,返回 false 。
- boolean startsWith(String prefix) 如果之前已经插入的字符串 word 的前缀之一为 prefix ,返回 true ;否则,返回 false 。
解题思路
Trie,又称前缀树或字典树,是一棵有根树,其每个节点包含以下字段:
- 指向子节点的指针数组
children
。对于本题而言,数组长度为 26,即小写英文字母的数量。此时 children[0]对应小写字母 a,children[1]对应小写字母 b,…,children[25]对应小写字母 z。 - 布尔字段
isEnd
,表示该节点是否为字符串的结尾。
插入字符串:
我们从字典树的根开始,插入字符串。对于当前字符对应的子节点,有两种情况:
- 子节点存在。沿着指针移动到子节点,继续处理下一个字符。
- 子节点不存在。创建一个新的子节点,记录在 children 数组的对应位置上,然后沿着指针移动到子节点,继续搜索下一个字符。
重复以上步骤,直到处理字符串的最后一个字符,然后将当前节点标记为字符串的结尾。
查找前缀:
我们从字典树的根开始,查找前缀。对于当前字符对应的子节点,有两种情况:
- 子节点存在。沿着指针移动到子节点,继续搜索下一个字符。
- 子节点不存在。说明字典树中不包含该前缀,返回空指针。
重复以上步骤,直到返回空指针或搜索完前缀的最后一个字符。
若搜索到了前缀的末尾,就说明字典树中存在该前缀。此外,若前缀末尾对应节点的 isEnd 为真,则说明字典树中存在该字符串。
代码
class Trie {
private Trie[] children;
private boolean isEnd;
public Trie() {
children = new Trie[26];
isEnd = false;
}
public void insert(String word) {
Trie node = this;
for(int i=0; i<word.length(); i++) {
int idx = word.charAt(i) - 'a';
if(node.children[idx] == null) {
node.children[idx] = new Trie();
}
node = node.children[idx];
}
node.isEnd = true;
}
public boolean search(String word) {
Trie node = searchPrefix(word);
return node != null && node.isEnd;
}
public boolean startsWith(String prefix) {
Trie node = searchPrefix(prefix);
return node != null;
}
Trie searchPrefix(String prefix) {
Trie node = this;
for(int i=0; i<prefix.length(); i++) {
int idx = prefix.charAt(i) - 'a';
if(node.children[idx] == null) {
return null;
}
node = node.children[idx];
}
return node;
}
}
/**
* Your Trie object will be instantiated and called as such:
* Trie obj = new Trie();
* obj.insert(word);
* boolean param_2 = obj.search(word);
* boolean param_3 = obj.startsWith(prefix);
*/
5. 字典序的第k小数字
题目描述
题目链接
给定整数 n 和 k,返回 [1, n] 中字典序第 k 小的数字。
解题思路
代码
class Solution {
public int findKthNumber(int n, int k) {
int curr = 1;//从1开始,1是字典序最小的
k--;//如果k=1,则不进入下面的循环,直接返回1,否则说明1不是目标,找第k-1个小的数
while (k > 0) {
int steps = count(curr, n);//steps=当前节点curr下有多少比n小的子节点(包括n)
if (steps <= k) {//不够,需要去邻近节点找
curr++;//+1意味着到达了邻近兄弟节点
k = k - steps;
//意味着前面的steps个数包含在在curr节点下,接下来进入兄弟节点找第k-steps小的数
} else {//否则,在curr下
k--;//减去当前节点
curr = curr * 10;//从最左侧开始搜寻
}
}
return curr;
}
public static int count(int curr, int n) {//计算节点curr下有多少比n小的子节点
int steps = 0;
long first = curr;
long last = curr;
while (first <= n) {//当前层有符合要求的节点
steps += Math.min(last, n) - first + 1;//汇入
first = first * 10;//进入到下一层
last = last * 10 + 9;//进入到下一层
}
return steps;
}
}
十、回溯
1. 全排列⭕
题目描述
题目链接
给定一个不含重复数字的数组 nums ,返回其 所有可能的全排列 。你可以 按任意顺序 返回答案。
解题思路
- 二叉树,树枝,树层
- visited数组标记在同一树枝是否访问过
- 排列与顺序有关,所以不能设置startIdx,因为还要找之前的
代码
class Solution {
List<List<Integer>> ans = new ArrayList<>();
List<Integer> path = new ArrayList<>();
public List<List<Integer>> permute(int[] nums) {
boolean[] visited = new boolean[nums.length];
backtrack(nums, visited);
return ans;
}
void backtrack(int[] nums, boolean[] visited) {
if(path.size() == nums.length) {
ans.add(new ArrayList<>(path));
return;
}
for(int i=0; i<nums.length; i++) {
if(!visited[i]) {
visited[i] = true;
path.add(nums[i]);
backtrack(nums, visited);
path.remove(path.size()-1);
visited[i] = false;
}
}
}
}
2. 子集⭕
题目描述
题目链接
给你一个整数数组 nums ,数组中的元素 互不相同 。返回该数组所有可能的子集(幂集)。
解集 不能 包含重复的子集。你可以按 任意顺序 返回解集。
解题思路
- 子集,与顺序无关,如果还回去找之前的,就会生成重复的子集(指顺序不同但元素相同),所以要设置startIdx
- 本质上还是选或不选当前元素
代码
class Solution {
List<List<Integer>> ans = new ArrayList<>();
List<Integer> path = new ArrayList<>();
public List<List<Integer>> subsets(int[] nums) {
backtrack(nums, 0);
return ans;
}
void backtrack(int[] nums, int startIdx) {
ans.add(new ArrayList(path));
for(int i=startIdx; i<nums.length; i++) {
path.add(nums[i]); //选当前元素
backtrack(nums, i + 1);
path.remove(path.size()-1); //不选当前元素
}
}
}
3. 电话号码的字母组合⭕
题目描述
题目链接
给定一个仅包含数字 2-9 的字符串,返回所有它能表示的字母组合。答案可以按 任意顺序 返回。
给出数字到字母的映射如下(与电话按键相同)。注意 1 不对应任何字母。
解题思路
- 递归到当前数字,有几种选择。
- 注意StringBuilder的使用,当需要对字符串进行添加删除操作时可以用StringBuilder。
stringBuilder.append(ch);
stringBuilder.deleteCharAt(i);
代码
class Solution {
String[] dict = {
"",
"",
"abc",
"def",
"ghi",
"jkl",
"mno",
"pqrs",
"tuv",
"wxyz"
};
List<String> ans = new ArrayList<>();
StringBuilder path = new StringBuilder();
public List<String> letterCombinations(String digits) {
if(digits.length() == 0) {
return ans;
}
backtrack(digits, 0);
return ans;
}
void backtrack(String digits, int curIdx) {
if(curIdx == digits.length()) {
ans.add(path.toString());
return;
}
String str = dict[digits.charAt(curIdx)-'0'];
for(int i=0; i<str.length(); i++) {
path.append(str.charAt(i));
backtrack(digits, curIdx+1);
path.deleteCharAt(path.length()-1);
}
}
}
4. 组合总和⭕
题目描述
题目链接
给你一个 无重复元素 的整数数组 candidates 和一个目标整数 target ,找出 candidates 中可以使数字和为目标数 target 的 所有 不同组合 ,并以列表形式返回。你可以按 任意顺序 返回这些组合。
candidates 中的 同一个 数字可以 无限制重复被选取
。如果至少一个数字的被选数量不同,则两种组合是不同的。
对于给定的输入,保证和为 target 的不同组合数少于 150 个。
解题思路
- sort + 剪枝
- 可以无限制选取,所以每次 backtrack 的 curIdx 不变
代码
class Solution {
List<List<Integer>> ans = new ArrayList<>();
List<Integer> path = new ArrayList<>();
public List<List<Integer>> combinationSum(int[] candidates, int target) {
Arrays.sort(candidates);
backtrack(candidates, target, 0, 0);
return ans;
}
void backtrack(int[] candidates, int target, int preSum, int curIdx) {
if(preSum == target) {
ans.add(new ArrayList(path));
return;
}
for(int i=curIdx; i<candidates.length; i++) {
if(preSum + candidates[i] > target) { //剪枝
return;
}
path.add(candidates[i]);
backtrack(candidates, target, preSum+candidates[i], i); //第一次循环到i是取i, i++后是不取i
path.remove(path.size()-1);
}
}
}
5. 括号生成⭕
题目描述
题目链接
数字 n 代表生成括号的对数,请你设计一个函数,用于能够生成所有可能的并且 有效的 括号组合。
解题思路
- 只有
()
这一种括号,假设(
数量为l
,)
数量为r
,什么情况下不合法呢?l > n
或r > n
或r > l
,这些情况下直接返回,否则继续递归添加括号,知道l == n
且r == n
时找到一种有效组合。
代码
class Solution {
List<String> ans = new ArrayList<>();
StringBuilder path = new StringBuilder();
public List<String> generateParenthesis(int n) {
backtrack(n, 0, 0);
return ans;
}
void backtrack(int n, int l, int r) {
if(l == n && r == n) {
ans.add(path.toString());
return;
}
if(l < r || l > n || r > n) {
return;
}
path.append('(');
backtrack(n, l + 1, r);
path.deleteCharAt(path.length()-1);
path.append(')');
backtrack(n, l, r + 1);
path.deleteCharAt(path.length()-1);
}
}
6. 单词搜索⭕
题目描述
题目链接
给定一个 m x n 二维字符网格 board 和一个字符串单词 word 。如果 word 存在于网格中,返回 true ;否则,返回 false 。
单词必须按照字母顺序,通过相邻的单元格内的字母构成,其中“相邻”单元格是那些水平相邻或垂直相邻的单元格。同一个单元格内的字母不允许被重复使用。
解题思路
代码
class Solution {
int[][] directions = {{-1, 0}, {1, 0}, {0, -1}, {0, 1}};
int m, n;
boolean[][] visited;
public boolean exist(char[][] board, String word) {
m = board.length;
n = board[0].length;
visited = new boolean[m][n];
//起点不一定是(0, 0),所以要遍历起点
for(int i=0; i<m; i++) {
for(int j=0; j<n; j++) {
boolean ans = backtrack(board, word, 0, i, j);
if(ans) return true; //提前返回
}
}
return false;
}
boolean backtrack(char[][] board, String word, int curIdx, int curX, int curY) {
//注意这里的处理逻辑
if(board[curX][curY] != word.charAt(curIdx)) {
return false;
}
//在word.length()-1处就进行末尾条件判断,否则的话 [["a"]],"a" 这种情况会直接返回false
if(curIdx == word.length()-1) {
return true;
}
visited[curX][curY] = true;
for(int i=0; i<4; i++) {
int newX = curX + directions[i][0];
int newY = curY + directions[i][1];
if(newX < 0 || newX >= m || newY < 0 || newY >= n) continue;
if(!visited[newX][newY]) {
boolean ans = backtrack(board, word, curIdx + 1, newX, newY);
if(ans) return ans; //提前返回
}
}
visited[curX][curY] = false;
return false;
}
}
7. 分割回文串🔺
题目描述
题目链接
给你一个字符串 s,请你将 s 分割成一些子串,使每个子串都是 回文串 。返回 s 所有可能的分割方案。
回文串 是正着读和反着读都一样的字符串。
解题思路
- 初始化
isHW
数组,避免重复计算 -> 动态规划 - 对于当前
curIdx
,从curIdx
开始 +1 遍历end
,只有s[curIdx : end]
是回文(idHW[curIdx][end] == true
)才继续递归回溯
代码
class Solution {
List<List<String>> ans = new ArrayList<>();
List<String> path = new ArrayList<>();
boolean[][] isHW;
public List<List<String>> partition(String s) {
isHW = new boolean[s.length()][s.length()];
computeHW(s); //初始化isHW数组,避免重复计算
backtrack(s, 0);
return ans;
}
void backtrack(String s, int curIdx) {
if(curIdx == s.length()) {
ans.add(new ArrayList(path));
return;
}
//固定start,遍历end,只有是回文才继续分割
for(int i=curIdx; i<s.length(); i++) {
if(isHW[curIdx][i]) {
path.add(s.substring(curIdx, i+1));
backtrack(s, i+1);
path.remove(path.size()-1);
}
}
}
//初始化回文数组函数
//动态规划
//s[i:j]是否是回文串,取决于左下角
void computeHW(String s) {
for(int i=0; i<s.length(); i++) {
isHW[i][i] = true;
}
for(int i=s.length()-1; i>=0; i--) {
for(int j=i+1; j<s.length(); j++) {
if(j-i == 1) {
isHW[i][j] = (s.charAt(i) == s.charAt(j));
} else {
isHW[i][j] = (s.charAt(i) == s.charAt(j) && isHW[i+1][j-1]);
}
}
}
}
}
8. N皇后🔺
题目描述
题目链接
按照国际象棋的规则,皇后可以攻击与之处在同一行或同一列或同一斜线上的棋子。
n 皇后问题 研究的是如何将 n 个皇后放置在 n×n 的棋盘上,并且使皇后彼此之间不能相互攻击。
给你一个整数 n ,返回所有不同的 n 皇后问题 的解决方案。
每一种解法包含一个不同的 n 皇后问题 的棋子放置方案,该方案中 'Q' 和 '.' 分别代表了皇后和空位。
解题思路
- 方法1:设置
char[][] chessboard
表示棋盘,棋盘的每一行代表树层,每一列代表树枝,也就是递归到某一行时,遍历其列。
由于Java中的String是不可变的,所以用char[][]
表示棋盘,最后再转换为List<String>
isValid
函数判断在当前棋盘的(row, col)位置放置Q是否合法。注意:因为递归函数每次只在同一行的某一列放置Q,所以不会出现行冲突,只需要检查列冲突和对角线冲突。 - 方法2:方法1的
isValid
函数每次判断合法性时时间复杂度为O(n),如何优化呢?设置三个HashSet<Integer>
分别记录每一列、每一个对角线的情况。列用列标就可以,对角线呢?同一个正对角线上:row - col = 一个数
,同一个反对角线上:row + col = 一个数
.
另外,可以不用char[][] chessboard
,而是用int[] queens
记录每一行的Q放置在哪一列。 - 方法3:应位运算优化方法2的空间复杂度,看不明白。
代码
方法1:
class Solution {
List<List<String>> ans = new ArrayList<>();
char[][] chessboard; //String不可变,所以用char[][],后面再转换
public List<List<String>> solveNQueens(int n) {
chessboard = new char[n][n];
for(int i=0; i<n; i++) {
for(int j=0; j<n; j++) {
chessboard[i][j] = '.';
}
}
backtrack(chessboard, 0);
return ans;
}
void backtrack(char[][] chessboard, int curRow) {
if(curRow == chessboard.length) {
ans.add(array2List(chessboard));
return;
}
for(int col=0; col<chessboard.length; col++) {
if(isOK(chessboard, curRow, col)) {
chessboard[curRow][col] = 'Q';
backtrack(chessboard, curRow + 1);
chessboard[curRow][col] = '.';
}
}
}
boolean isOK(char[][] chessboard, int row, int col) {
//同行不会出现冲突,因为递归函数每次只在这一行的某一列放置Q
//同列
for(int i=0; i<row; i++) {
if(chessboard[i][col] == 'Q') {
return false;
}
}
//主对角线
for(int i=row-1, j=col-1; i>=0 && j>=0; i--, j--) {
if(chessboard[i][j] == 'Q') {
return false;
}
}
//副对角线
for(int i=row-1, j=col+1; i>=0 && j<chessboard.length; i--, j++) {
if(chessboard[i][j] == 'Q') {
return false;
}
}
return true;
}
List<String> array2List(char[][] chessboard) {
List<String> ans = new ArrayList<>();
for(char[] each : chessboard) {
ans.add(String.valueOf(each));
}
return ans;
}
}
方法2:
class Solution {
List<List<String>> ans = new ArrayList<>();
int[] queens; //queens[row] = col;
Set<Integer> columns = new HashSet<>(); //列冲突标记
Set<Integer> diagonals1 = new HashSet<>(); //主对角线标记 row - col
Set<Integer> diagonals2 = new HashSet<>(); //副对角线标记 row + col
public List<List<String>> solveNQueens(int n) {
queens = new int[n];
Arrays.fill(queens, -1);
backtrack(n, 0);
return ans;
}
void backtrack(int n, int row) {
if(row == n) {
List<String> chessboard = generateBoard(queens);
ans.add(chessboard);
return;
}
for(int col=0; col<n; col++) {
if(columns.contains(col)) continue;
int diagonal1 = row - col;
if(diagonals1.contains(diagonal1)) continue;
int diagonal2 = row + col;
if(diagonals2.contains(diagonal2)) continue;
queens[row] = col;
columns.add(col);
diagonals1.add(diagonal1);
diagonals2.add(diagonal2);
backtrack(n, row + 1);
queens[row] = -1;
columns.remove(col);
diagonals1.remove(diagonal1);
diagonals2.remove(diagonal2);
}
}
//根据 int[] queens 生成 List<String> chessboard
List<String> generateBoard(int[] queens) {
List<String> chessboard = new ArrayList<>();
for(int i=0; i<queens.length; i++) {
char[] row = new char[queens.length];
Arrays.fill(row, '.');
row[queens[i]] = 'Q';
chessboard.add(new String(row));
}
return chessboard;
}
}
9. 复原IP地址
题目描述
题目链接
有效 IP 地址 正好由四个整数(每个整数位于 0 到 255 之间组成,且不能含有前导 0),整数之间用 '.' 分隔。
例如:"0.1.2.201" 和 "192.168.1.1" 是 有效 IP 地址,但是 "0.011.255.245"、"192.168.1.312" 和 "192.168@1.1" 是 无效 IP 地址。
给定一个只包含数字的字符串 s ,用以表示一个 IP 地址,返回所有可能的有效 IP 地址,这些地址可以通过在 s 中插入 '.' 来形成。你 不能 重新排序或删除 s 中的任何数字。你可以按 任何 顺序返回答案。
代码
class Solution {
List<String> ans;
StringBuilder sb;
public List<String> restoreIpAddresses(String s) {
ans = new ArrayList<>();
if(s.length() < 4 || s.length() > 12) { //剪枝
return ans;
}
sb = new StringBuilder(s);
backtrack(sb, 0, 0);
return ans;
}
void backtrack(StringBuilder sb, int startIdx, int pointNum) {
if(pointNum == 3) { //pointNum记录'.'的个数
if(isIpPart(sb, startIdx, sb.length() - 1)) {
System.out.println(sb.toString());
ans.add(sb.toString());
}
return;
}
for(int i=startIdx; i<sb.length(); i++) {
if(isIpPart(sb, startIdx, i)) {
sb.insert(i+1, '.'); //直接在原始字符串中插入'.'
backtrack(sb, i+2, pointNum+1);
sb.deleteCharAt(i+1);
} else {
break; //sb[startIdx:i]如果不合法,sb[startIdx:i+1]就更不合法了
}
}
}
/*
* 判断是否是IP地址的部分
*/
boolean isIpPart(StringBuilder sb, int start, int end) { //左闭右闭
int length = end - start + 1;
if(length <= 0 || length > 3) return false; //长度不合法
if(length > 1 && sb.charAt(start) == '0') return false; //有前导0,不合法
int num = 0;
for(int i=start; i <= end; i++) {
if(sb.charAt(i) < '0' || sb.charAt(i) > '9') return false; //非数字,不合法
num = num * 10 + sb.charAt(i) - '0';
if(num > 255) return false; //大于255,不合法
}
return true;
}
}
//只有当前位置合理才在这里插入.
十一、二分查找
1. 搜索插入位置
题目描述
题目链接
给定一个排序数组和一个目标值,在数组中找到目标值,并返回其索引。如果目标值不存在于数组中,返回它将会被按顺序插入的位置。
请必须使用时间复杂度为 O(log n) 的算法。
解题思路
- 如果找不到,要插入的位置是left
!!!!!!!!!!!!
代码
class Solution {
public int searchInsert(int[] nums, int target) {
int l = 0, r = nums.length; //左闭右开
while(l < r) {
int m = l + (r - l)/2;
if(nums[m] == target) {
return m;
}
if(nums[m] > target) {
r = m;
} else {
l = m + 1;
}
}
return l; //返回要插入的位置:l
}
}
2. 搜索二维矩阵⭕
题目描述
题目链接
给你一个满足下述两条属性的 m x n 整数矩阵:
每行中的整数从左到右按非严格递增顺序排列。
每行的第一个整数大于前一行的最后一个整数。
给你一个整数 target ,如果 target 在矩阵中,返回 true ;否则,返回 false 。
解题思路
- 由题意可知,不仅是每一行,如果把这个矩阵展开成一维的,它也是递增的,所以可以把这个矩阵想象成一维的,从而进行二分查找
- 但是并不是真的展开,而是
int curNum = matrix[mid/n][mid%n];
代码
class Solution {
public boolean searchMatrix(int[][] matrix, int target) {
int m = matrix.length, n = matrix[0].length;
int left = 0, right = m * n;
while(left < right) {
int mid = left + (right-left) / 2;
int curNum = matrix[mid/n][mid%n];
if(curNum == target) {
return true;
}
if(curNum < target) {
left = mid + 1;
} else {
right = mid;
}
}
return false;
}
}
扩展:搜索二维矩阵II⭕
编写一个高效的算法来搜索 m x n 矩阵 matrix 中的一个目标值 target 。该矩阵具有以下特性:
每行的元素从左到右升序排列。
每列的元素从上到下升序排列。
解题思路
- 这道题目并没有上一题那样的条件
- 对于这个矩阵的左下角的元素,他上面的元素都比他小,右边的元素都比他大
- 以二维数组左下角为原点,建立直角坐标轴。
若当前数字大于了查找数,查找往上移一位。
若当前数字小于了查找数,查找往右移一位。
代码
class Solution {
public boolean searchMatrix(int[][] matrix, int target) {
int m = matrix.length, n = matrix[0].length;
int curRow = m - 1, curCol = 0;
while(curRow >= 0 && curCol < n) {
int curNum = matrix[curRow][curCol];
if(curNum == target) {
return true;
}
if(curNum < target) {
curCol++;
} else {
curRow--;
}
}
return false;
}
}
3. 在排序数组中查找元素的第一个和最后一个位置🔺
题目描述
题目链接
给你一个按照非递减顺序排列的整数数组 nums,和一个目标值 target。请你找出给定目标值在数组中的开始位置和结束位置。
如果数组中不存在目标值 target,返回 [-1, -1]。
你必须设计并实现时间复杂度为 O(log n) 的算法解决此问题。
解题思路
-
笨方法:二分查找找到target后,分别往前往后遍历。最坏时间复杂度为
O(logn + n) = O(n)
-
正经方法:
考虑 target 开始和结束位置,其实我们要找的就是数组中「第一个等于 target 的位置」(记为 leftIdx)和「第一个大于 target 的位置减一」(记为 rightIdx)。二分查找中,寻找 leftIdx 即为在数组中寻找第一个大于等于 target 的下标,寻找 rightIdx 即为在数组中寻找第一个大于 target 的下标,然后将下标减一。两者的判断条件不同,为了代码的复用,我们定义 binarySearch(nums, target, lower) 表示在 nums 数组中二分查找 target 的位置,如果 lower 为 true,则查找第一个大于等于 target 的下标,否则查找第一个大于 target 的下标。
最后,因为 target 可能不存在数组中,因此我们需要重新校验我们得到的两个下标 leftIdx 和 rightIdx,看是否符合条件,如果符合条件就返回 [leftIdx,rightIdx],不符合就返回 [−1,−1]。
寻找第一个大于等于 target 的下标:
if(nums[m] >= target) {r = m; ans = m;}
寻找第一个大于 target 的下标:if(nums[m] > target) {r = m; ans = m;}
代码
(推荐)虽然没复用代码,但是更好理解的正经方法:
class Solution {
public int[] searchRange(int[] nums, int target) {
int first = -1, last = -1;
//第一次二分查找:第一个等于target的位置
int l = 0, r = nums.length;
while(l < r) {
int m = l + (r - l) / 2;
if(nums[m] == target) {
first = m;
r = m; //here!!!
} else if(nums[m] > target) {
r = m;
} else {
l = m + 1;
}
}
//第二次二分查找:最后一个等于target的位置
l = 0;
r = nums.length;
while(l < r) {
int m = l + (r - l) / 2;
if(nums[m] == target) {
last = m;
l = m + 1; //here!!!
} else if(nums[m] > target) {
r = m;
} else {
l = m + 1;
}
}
return new int[]{first, last};
}
}
正经方法:
class Solution {
public int[] searchRange(int[] nums, int target) {
int first = binarySearch(nums, target, true);
int last = binarySearch(nums, target, false) - 1;
//满足条件才返回
if(first <= last && nums[first] == target && nums[last] == target) {
return new int[]{first, last};
}
//不满足条件则返回-1,-1
return new int[]{-1, -1};
}
int binarySearch(int[] nums, int target, boolean lower) {
int l = 0, r = nums.length;
int ans = nums.length;
while(l < r) {
int m = l + (r - l) / 2;
//1.找第一个比target大的元素 2.找第一个等于target的元素(如果存在,不存在找的就是第一个比target大的元素)
if(nums[m] > target || (lower && nums[m] >= target)) {
r = m;
ans = m;
} else {
l = m + 1;
}
}
return ans;
}
}
4. 搜索旋转排序数组🔺
题目描述
题目链接
整数数组 nums 按升序排列,数组中的值 互不相同 。
在传递给函数之前,nums 在预先未知的某个下标 k(0 <= k < nums.length)上进行了 旋转,使数组变为 [nums[k], nums[k+1], ..., nums[n-1], nums[0], nums[1], ..., nums[k-1]](下标 从 0 开始 计数)。例如, [0,1,2,4,5,6,7] 在下标 3 处经旋转后可能变为 [4,5,6,7,0,1,2] 。
给你 旋转后 的数组 nums 和一个整数 target ,如果 nums 中存在这个目标值 target ,则返回它的下标,否则返回 -1 。
你必须设计一个时间复杂度为 O(log n) 的算法解决此问题。
解题思路
对于有序数组,可以使用二分查找的方法查找元素。
但是这道题中,数组本身不是有序的,进行旋转后只保证了数组的局部是有序的,这还能进行二分查找吗?答案是可以的。
可以发现的是,我们将数组从中间分开成左右两部分的时候,一定有一部分的数组是有序的。拿示例来看,我们从 6 这个位置分开以后数组变成了 [4, 5, 6] 和 [7, 0, 1, 2] 两个部分,其中左边 [4, 5, 6] 这个部分的数组是有序的,其他也是如此。
这启示我们可以在常规二分查找的时候查看当前 mid 为分割位置分割出来的两个部分 [l, mid] 和 [mid + 1, r] 哪个部分是有序的,并根据有序的那个部分确定我们该如何改变二分查找的上下界,因为我们能够根据有序的那部分判断出 target 在不在这个部分:
如果 [l, mid - 1] 是有序数组,且 target 的大小满足 [nums[l],nums[mid]),则我们应该将搜索范围缩小至 [l, mid - 1],否则在 [mid + 1, r] 中寻找。
如果 [mid, r] 是有序数组,且 target 的大小满足 (nums[mid+1],nums[r]],则我们应该将搜索范围缩小至 [mid + 1, r],否则在 [l, mid - 1] 中寻找。
总结:
分成两半,一半有序,一半无序,可以确定target是否在有序的一半里面。然后去在的那一半继续找。
代码
class Solution {
public int search(int[] nums, int target) {
int l = 0, r = nums.length;
while(l < r) {
int m = l + (r - l) / 2;
if(nums[m] == target) {
return m;
}
if(nums[l] < nums[m]) {
//这一半有序且target在有序区间内
if(nums[l] <= target && target < nums[m]) {
r = m;
} else {
//target不在有序区间内
l = m + 1;
}
} else {
//这一半有序且target在有序区间内
if(nums[m] < target && target <= nums[r-1]) {
l = m + 1;
} else {
//target不在有序区间内
r = m;
}
}
}
return -1;
}
}
4-1. 搜索旋转排序数组II🔺
题目描述
题目链接
已知存在一个按非降序排列的整数数组 nums ,数组中的值不必互不相同。
在传递给函数之前,nums 在预先未知的某个下标 k(0 <= k < nums.length)上进行了 旋转 ,使数组变为 [nums[k], nums[k+1], ..., nums[n-1], nums[0], nums[1], ..., nums[k-1]](下标 从 0 开始 计数)。例如, [0,1,2,4,4,4,5,6,6,7] 在下标 5 处经旋转后可能变为 [4,5,6,6,7,0,1,2,4,4] 。
给你 旋转后 的数组 nums 和一个整数 target ,请你编写一个函数来判断给定的目标值是否存在于数组中。如果 nums 中存在这个目标值 target ,则返回 true ,否则返回 false 。
你必须尽可能减少整个操作步骤。
解题思路
对于数组中有重复元素的情况,二分查找时可能会有 a[l]=a[mid]=a[r],此时无法判断区间 [l,mid] 和区间 [mid+1,r] 哪个是有序的。
例如 nums=[3,1,2,3,3,3,3],target=2,首次二分时无法判断区间 [0,3] 和区间 [4,6] 哪个是有序的。
对于这种情况,我们只能将当前二分区间的左边界加一,右边界减一,然后在新区间上继续二分查找。
代码
class Solution {
public boolean search(int[] nums, int target) {
int n = nums.length;
if (n == 0) {
return false;
}
if (n == 1) {
return nums[0] == target;
}
int l = 0, r = n - 1;
while (l <= r) {
int mid = (l + r) / 2;
if (nums[mid] == target) {
return true;
}
if (nums[l] == nums[mid] && nums[mid] == nums[r]) {
++l;
--r;
} else if (nums[l] <= nums[mid]) {
if (nums[l] <= target && target < nums[mid]) {
r = mid - 1;
} else {
l = mid + 1;
}
} else {
if (nums[mid] < target && target <= nums[n - 1]) {
l = mid + 1;
} else {
r = mid - 1;
}
}
}
return false;
}
}
5. 寻找旋转排序数组中的最小值🔺
题目描述
题目链接
已知一个长度为 n 的数组,预先按照升序排列,经由 1 到 n 次 旋转 后,得到输入数组。例如,原数组 nums = [0,1,2,4,5,6,7] 在变化后可能得到:
若旋转 4 次,则可以得到 [4,5,6,7,0,1,2]
若旋转 7 次,则可以得到 [0,1,2,4,5,6,7]
注意,数组 [a[0], a[1], a[2], ..., a[n-1]] 旋转一次 的结果为数组 [a[n-1], a[0], a[1], a[2], ..., a[n-2]] 。
给你一个元素值 互不相同 的数组 nums ,它原来是一个升序排列的数组,并按上述情形进行了多次旋转。请你找出并返回数组中的 最小元素。
你必须设计一个时间复杂度为 O(log n) 的算法解决此问题。
解题思路
- 和上一题很像的思路,毕竟都是旋转有序数组
- 分成两半,一半有序,一半无序。可以确定有序的那一半的最小值,然后更新最小值,继续去和无序的那一半比较。
代码
class Solution {
public int findMin(int[] nums) {
int l = 0, r = nums.length;
int ans = nums[0];
while(l < r) {
int m = l + (r - l) / 2;
if(nums[l] < nums[m]) {
//左边有序 -> 左边的最小值
ans = Math.min(ans, nums[l]);
//和右边继续比较
l = m + 1;
} else {
//右边有序 -> 右边的最小值
ans = Math.min(ans, nums[m]);
//和左边继续比较
r = m;
}
}
return ans;
}
}
5-1. 寻找旋转排序数组中的最小值II
题目描述
题目链接
和上一题的区别在于存在重复元素。
解题思路
- 我们考虑数组中的最后一个元素 x:在最小值右侧的元素,它们的值一定都小于等于 x;而在最小值左侧的元素,它们的值一定都大于等于 x。
代码
class Solution {
public int findMin(int[] nums) {
int low = 0;
int high = nums.length - 1;
while (low < high) {
int pivot = low + (high - low) / 2;
if (nums[pivot] < nums[high]) {
high = pivot;
} else if (nums[pivot] > nums[high]) {
low = pivot + 1;
} else {
high -= 1;
}
}
return nums[low];
}
}
6. 寻找两个正序数组的中位数🔺
题目描述
题目链接
给定两个大小分别为 m 和 n 的正序(从小到大)数组 nums1 和 nums2。请你找出并返回这两个正序数组的 中位数 。
算法的时间复杂度应该为 O(log (m+n)) 。
解题思路
- 寻找两个正序数组的中位数 -> 寻找这两个数组中的第k小的数 (分两种情况:总长度为奇数,总长度为偶数)
- 如何寻找两个数组中第k小的数呢?
如果用双指针的话,最坏时间复杂度就是O(m+n),所以要用二分查找优化。
对于nums1
和nums2
,分别设置两个偏移量offset1
和offset2
,每次从offset
开始,各选k/2
个元素,如果nums1
中选的k/2
个元素的最后一个元素(也就是最大的元素)小于等于nums2
中选取的k/2
个元素的最后一个元素,那么nums1
中选取的这k/2
个元素,一定都属于前k
小的元素(用脑子想一想就明白了),所以把这k/2
个元素全部加入 前k
小元素集合(这个集合并不存在,只是把nums1
的offset1
往后移过这k/2
个元素就行了),另外,k
值也需要相应的减小到k'
,然后再在剩余的没有加入 前k
小元素集合 的元素中继续找前k'
小元素。
代码
class Solution {
public double findMedianSortedArrays(int[] nums1, int[] nums2) {
int totalLen = nums1.length + nums2.length;
//下标从0开始,但是k从1开始
if(totalLen % 2 == 0) {
return (getKthElement(nums1, nums2, totalLen/2) + getKthElement(nums1, nums2, totalLen/2 + 1)) / 2.0;
} else {
return getKthElement(nums1, nums2, totalLen/2 + 1);
}
}
double getKthElement(int[] nums1, int[] nums2, int k) {
int n1 = nums1.length, n2 = nums2.length;
int offset1 = 0, offset2 = 0;
while(true) {
//如果一个数组的全部元素都已经被选中了,则直接返回另一个数组剩余的的第k小元素
if(offset1 == n1) return nums2[offset2 + k - 1];
if(offset2 == n2) return nums1[offset1 + k - 1];
//如果只剩下一个元素需要选了,则直接比较
if(k == 1) return Math.min(nums1[offset1], nums2[offset2]);
//在nums1和nums2中各抽出 k/2 的元素进行比较
int newOffset1 = Math.min(n1 - 1, offset1 + k/2 - 1); //需要防止越界
int newOffset2 = Math.min(n2 - 1, offset2 + k/2 - 1);
if(nums1[newOffset1] <= nums2[newOffset2]) {
//选择nums1中的这k/2个元素
k -= (newOffset1 - offset1 + 1);
offset1 = newOffset1 + 1;
} else {
//选择nums2中的这k/2个元素
k -= (newOffset2 - offset2 + 1);
offset2 = newOffset2 + 1;
}
}
}
}
7. x的平方根
题目描述
题目链接
给你一个非负整数 x ,计算并返回 x 的 算术平方根 。
由于返回类型是整数,结果只保留 整数部分 ,小数部分将被 舍去 。
注意:不允许使用任何内置指数函数和算符,例如 pow(x, 0.5) 或者 x ** 0.5 。
解题思路
二分查找
代码
class Solution {
public int mySqrt(int x) {
int l = 0;
int r = x;
int ans = 0;
while(l <= r) {
int mid = l + (r - l) / 2;
if((long) mid * mid == x) return mid;
if((long) mid * mid < x) {
ans = mid;
l = mid + 1;
} else {
r = mid - 1;
}
}
return ans;
}
}
8. 寻找峰值
题目描述
题目链接
峰值元素是指其值严格大于左右相邻值的元素。
给你一个整数数组 nums,找到峰值元素并返回其索引。数组可能包含多个峰值,在这种情况下,返回 任何一个峰值 所在位置即可。
你可以假设 nums[-1] = nums[n] = -∞ 。
你必须实现时间复杂度为 O(log n) 的算法来解决此问题。
解题思路
因为起点是负无穷,开始一定是上坡,目标是寻找序列中第一个下降点,序列从左到右是从“不满足”状态到“满足”状态的。如果nums[mid] < nums[mid+1],说明仍然不满足,不必包含mid,继续向右找,即l = mid +1;如果nums[mid] > nums[mid+1],说明此时这个mid位置满足了,但不一定是第一个满足的,所以要把mid包含在内,向左找,即r = mid;退出条件是l == r,也就是框出了唯一的一个位置,此时退出,返回l即可。这是一个很经典的二分框架~
代码
class Solution {
public int findPeakElement(int[] nums) {
int l = 0, r = nums.length - 1;
while (l < r) {
int mid = l + ((r - l) >> 1);
if (nums[mid] < nums[mid+1]) {
l = mid + 1;
} else {
r = mid;
}
}
return l;
}
}
9. 木头切割问题
题目描述
给定长度为n的数组,每个元素代表一个木头的长度,木头可以任意截断,从这堆木头中截出至少k个相同长度为m的木块。已知k,求max(m)。
输入两行,第一行n, k,第二行为数组序列。输出最大值。
输入
5 5
4 7 2 10 5
输出
4
解释:如图,最多可以把它分成5段长度为4的木头
解题思路
- 二分,木头长度范围是[1, max]
代码
public class Main {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
int n = scanner.nextInt();
int k = scanner.nextInt();
int[] a = new int[n];
int r = 0;
for (int i = 0; i < n; i++) {
a[i] = scanner.nextInt();
r = Math.max(r, a[i]);
}
int l = 1;
while (l < r) {
int mid = (l + r + 1) / 2;
if (check(a, mid) >= k) {
l = mid;
} else {
r = mid - 1;
}
}
System.out.println(l);
}
public static int check(int[] a, int mid) {
int res = 0;
for (int num : a) {
res += num / mid;
}
return res;
}
}
十二、栈
1. 有效的括号⭕
题目描述
题目链接
给定一个只包括 '(',')','{','}','[',']' 的字符串 s ,判断字符串是否有效。
有效字符串需满足:
- 左括号必须用相同类型的右括号闭合。
- 左括号必须以正确的顺序闭合。
- 每个右括号都有一个对应的相同类型的左括号。
解题思路
- 类似于
[](){}、[{()}]
这种是正确的,[(])
这种是不正确的 - 遇到左括号就入栈,遇到右括号就查看栈顶元素是否匹配,匹配就出栈,不匹配就返回错误。直到遍历完字符串,栈为空则正确,否则错误。
- 使用
Deque
代替Stack
作栈
代码
class Solution {
public boolean isValid(String s) {
HashMap<Character, Character> dict = new HashMap<>();
dict.put(')', '(');
dict.put(']', '[');
dict.put('}', '{');
Deque<Character> stack = new LinkedList<>();
for(int i=0; i<s.length(); i++) {
char ch = s.charAt(i);
if(ch == '(' || ch == '[' || ch == '{') {
stack.push(ch);
} else {
if(stack.isEmpty() || stack.peek() != dict.get(ch)) {
return false;
}
stack.pop();
}
}
return stack.isEmpty();
}
}
2. 最小栈🔺
题目描述
题目链接
设计一个支持 push ,pop ,top
操作,并能在常数时间内检索到最小元素的栈。
实现 MinStack
类:
MinStack()
初始化堆栈对象。
void push(int val)
将元素val推入堆栈。
void pop()
删除堆栈顶部的元素。
int top()
获取堆栈顶部的元素。
int getMin()
获取堆栈中的最小元素。
解题思路
- 方法1:辅助栈
使用一个栈作为存放元素的栈,另一个栈存放当前栈中的最小值,每次push
元素时也push
一个当前最小值 - 方法2:不用辅助栈
设置一个minValue
属性记录当前栈中的最小值,但是问题是:如果pop
时把最小值pop
出来了,怎么找到下一个最小值呢?
解决:每次push x
时,如果x
小于等于minValue
,则先push
一次minValue
,再push x
,再更新minValue
;每次pop
时,如果栈顶元素等于最小值,则先pop
一次,再更新minValue
,再pop
一次。这样的话,内存占用会比辅助栈少一些。
代码
方法2:√
class MinStack {
private Deque<Integer> stack;
private int minValue;
public MinStack() {
stack = new LinkedList<>();
minValue = Integer.MAX_VALUE;
}
public void push(int val) {
if(val <= minValue) { //注意这里是小于等于
stack.push(minValue);
minValue = val;
}
stack.push(val);
}
public void pop() {
if(stack.peek() == minValue) {
stack.pop();
minValue = stack.peek();
}
stack.pop();
}
public int top() {
return stack.peek();
}
public int getMin() {
return minValue;
}
}
/**
* Your MinStack object will be instantiated and called as such:
* MinStack obj = new MinStack();
* obj.push(val);
* obj.pop();
* int param_3 = obj.top();
* int param_4 = obj.getMin();
*/
3. 字符串解码🔺
题目描述
题目链接
给定一个经过编码的字符串,返回它解码后的字符串。
编码规则为: k[encoded_string]
,表示其中方括号内部的 encoded_string
正好重复 k
次。注意 k
保证为正整数。
你可以认为输入字符串总是有效的;输入字符串中没有额外的空格,且输入的方括号总是符合格式要求的。
此外,你可以认为原始数据不包含数字,所有的数字只表示重复的次数 k
,例如不会出现像 3a 或 2[4] 的输入。
示例 1:
输入:s = "3[a]2[bc]"
输出:"aaabcbc"
示例 2:
输入:s = "3[a2[c]]"
输出:"accaccacc"
示例 3:
输入:s = "2[abc]3[cd]ef"
输出:"abcabccdcdcdef"
示例 4:
输入:s = "abc3[cd]xyz"
输出:"abccdcdcdxyz"
提示:
1 <= s.length <= 30
s 由小写英文字母、数字和方括号 '[]' 组成
s 保证是一个 有效 的输入。
s 中所有整数的取值范围为 [1, 300]
解题思路
- 题目需要注意的点:1. 存在嵌套;2. 重复次数k不一定是个位数
Deque<Integer> kStack
:k栈
Deque<StringBuilder> sbStack
:前缀sb栈(还没有做自己的*k操作,等待拼接后序子token 的sb们)
StringBuilder sb
:当前正在处理的子token的sb- 遍历ch,每次有四种情况:'[', ']', 字母, 数字,分别处理这四种情况,详解在代码里
代码
class Solution {
public String decodeString(String s) {
int k = 0;
StringBuilder sb = new StringBuilder();
Deque<Integer> kStack = new LinkedList<>();
Deque<StringBuilder> sbStack = new LinkedList<>();
for(char ch : s.toCharArray()) {
if(ch == '[') {
//碰到[,要开始一个新的子token了
//将k入栈,将当前还没有做*k操作,等着后面的子token的结果的 sb 入栈
//k置零,sb清空,准备迎接新的子token
kStack.push(k);
sbStack.push(sb);
k = 0;
sb = new StringBuilder();
} else if(ch == ']') {
//碰到],代表一个子token结束
//将这个子token的sb做*k操作
//将之前放进去的上一个sb拿出来,拼接上子token的处理结果,作为当前sb,继续处理
int curK = kStack.pop();
StringBuilder tmp = new StringBuilder();
for(int i=0; i<curK; i++) {
tmp.append(sb);
}
sb = sbStack.pop().append(tmp);
} else if(ch >= '0' && ch <= '9') {
//更新当前k,如果k不是个位数,需要乘以10
k = k * 10 + ch - '0';
} else {
//遇到普通字母,添加进当前sb
sb.append(ch);
}
}
return sb.toString();
}
}
4. 每日温度⭕
题目描述
题目链接
给定一个整数数组 temperatures ,表示每天的温度,返回一个数组 answer ,其中 answer[i] 是指对于第 i 天,下一个更高温度出现在几天后。如果气温在这之后都不会升高,请在该位置用 0 来代替。
解题思路
- 找右边第一个比当前元素大的元素 -> 单调栈
- 答案是要找比当前元素大的,所以这个单调栈是递减的,只存入递减的元素,当遇到比栈顶大的元素时,挨个出栈得出答案。
代码
代码简化(上面的代码方便理解)
class Solution {
public int[] dailyTemperatures(int[] temperatures) {
Deque<Integer> monoStack = new LinkedList<>();
int[] ans = new int[temperatures.length];
for(int i=0; i<temperatures.length; i++) {
while(!monoStack.isEmpty() && temperatures[monoStack.peek()] < temperatures[i]) {
ans[monoStack.peek()] = i - monoStack.peek();
monoStack.pop();
}
monoStack.push(i);
}
return ans;
}
}
class Solution {
public int[] dailyTemperatures(int[] temperatures) {
Deque<Integer> monoStack = new LinkedList<>();
int[] ans = new int[temperatures.length];
monoStack.push(0);
for(int i=1; i<temperatures.length; i++) {
if(temperatures[i] <= temperatures[monoStack.peek()]) {
monoStack.push(i);
} else {
while(!monoStack.isEmpty() && temperatures[monoStack.peek()] < temperatures[i]) {
ans[monoStack.peek()] = i - monoStack.peek();
monoStack.pop();
}
monoStack.push(i);
}
}
return ans;
}
}
5. 柱状图中最大的矩形⭕
题目描述
题目链接
给定 n 个非负整数,用来表示柱状图中各个柱子的高度。每个柱子彼此相邻,且宽度为 1 。
求在该柱状图中,能够勾勒出来的矩形的最大面积。
解题思路
- 对于每个柱子, 求以它的高度能扩展的最大面积。需要找左边第一个比它矮的和右边第一个比它矮的 -> 两次单调栈
代码
class Solution {
//对于每个柱子, 以它的高度能扩展的最大面积
//需要找左边第一个比它矮的和右边第一个比它矮的
public int largestRectangleArea(int[] heights) {
int n = heights.length;
int[] left = new int[n], right = new int[n];
Arrays.fill(left, -1);
Arrays.fill(right, -1);
//找右边第一个比它小的
Deque<Integer> monoStack = new LinkedList<>();
for(int i=0; i<n; i++) {
while(!monoStack.isEmpty() && heights[i] < heights[monoStack.peek()]) {
right[monoStack.peek()] = i;
monoStack.pop();
}
monoStack.push(i);
}
//找左边第一个比它小的
monoStack.clear();
for(int i=n-1; i>=0; i--) {
while(!monoStack.isEmpty() && heights[i] < heights[monoStack.peek()]) {
left[monoStack.peek()] = i;
monoStack.pop();
}
monoStack.push(i);
}
//计算以每个柱子的高度为矩形高度的扩展面积
int ans = 0;
for(int i=0; i<n; i++) {
//area = (右边第一个比它矮的柱子的坐标 - 左边第一个比它矮的柱子的坐标 - 1) * 柱子高度
//当左边没有比它矮的柱子时,从-1开始计算
//当右边没有比它矮的柱子时,从n开始计算
int area = heights[i] * ((right[i] == -1 ? n : right[i]) - (left[i] == -1 ? -1 : left[i]) - 1);
ans = Math.max(ans, area);
}
return ans;
}
}
6. 移掉K位数字
题目描述
题目链接
给你一个以字符串表示的非负整数 num 和一个整数 k ,移除这个数中的 k 位数字,使得剩下的数字最小。请你以字符串形式返回这个最小的数字。
示例 1 :
输入:num = "1432219", k = 3
输出:"1219"
解释:移除掉三个数字 4, 3, 和 2 形成一个新的最小的数字 1219 。
示例 2 :
输入:num = "10200", k = 1
输出:"200"
解释:移掉首位的 1 剩下的数字为 200. 注意输出不能有任何前导零。
示例 3 :
输入:num = "10", k = 2
输出:"0"
解释:从原数字移除所有的数字,剩余为空就是 0 。
解题思路
- 单调栈(递增)
- 如果全部遍历完还没删够k个,则从末尾再删k个
class Solution {
public String removeKdigits(String num, int k) {
Deque<Character> deque = new LinkedList<Character>();
int length = num.length();
for (int i = 0; i < length; ++i) {
char digit = num.charAt(i);
while (!deque.isEmpty() && k > 0 && deque.peekLast() > digit) {
deque.pollLast();
k--;
}
deque.offerLast(digit);
}
for (int i = 0; i < k; ++i) {
deque.pollLast();
}
StringBuilder ret = new StringBuilder();
boolean leadingZero = true;
while (!deque.isEmpty()) {
char digit = deque.pollFirst();
if (leadingZero && digit == '0') {
continue;
}
leadingZero = false;
ret.append(digit);
}
return ret.length() == 0 ? "0" : ret.toString();
}
}
7. 移除无效的括号
题目描述
题目链接
给你一个由 '('、')' 和小写字母组成的字符串 s。
你需要从字符串中删除最少数目的 '(' 或者 ')' (可以删除任意位置的括号),使得剩下的「括号字符串」有效。
请返回任意一个合法字符串。
有效「括号字符串」应当符合以下 任意一条 要求:
空字符串或只包含小写字母的字符串
可以被写作 AB(A 连接 B)的字符串,其中 A 和 B 都是有效「括号字符串」
可以被写作 (A) 的字符串,其中 A 是一个有效的「括号字符串」
代码
class Solution {
public String minRemoveToMakeValid(String s) {
Set<Integer> indexesToRemove = new HashSet<>();
Stack<Integer> stack = new Stack<>(); // 栈中只放(,遇到)就看栈中有没有和他匹配的
for (int i = 0; i < s.length(); i++) {
if (s.charAt(i) == '(') {
stack.push(i);
} if (s.charAt(i) == ')') {
if (stack.isEmpty()) {
indexesToRemove.add(i);
} else {
stack.pop();
}
}
}
// Put any indexes remaining on stack into the set.
while (!stack.isEmpty()) indexesToRemove.add(stack.pop()); // 栈里剩下的没有匹配的(也是要删除的
StringBuilder sb = new StringBuilder();
for (int i = 0; i < s.length(); i++) {
if (!indexesToRemove.contains(i)) {
sb.append(s.charAt(i));
}
}
return sb.toString();
}
}
十三、堆
1. 数组中的第k个最大元素🔺
题目描述
题目链接
给定整数数组 nums 和整数 k,请返回数组中第 k 个最大的元素。
请注意,你需要找的是数组排序后的第 k 个最大的元素,而不是第 k 个不同的元素。
你必须设计并实现时间复杂度为 O(n) 的算法解决此问题。
解题思路
-
方法1:
找出无序数组中第k大/第k小的元素 -> 【快速选择】
快速选择是一种基于快速排序的方法。快速选择的基本思想是选择一个基准元素(通常是数组的第一个元素),然后对数组进行分区,将小于基准的元素移到左侧,将大于基准的元素移到右侧。然后,根据基准元素的位置,决定继续在左侧或右侧进行查找,直到找到第k小或第k大的元素。
这里的代码实现没有移动元素,而是直接设置了small
和big
集合,将小于pivot
的元素放到small
中,将大于pivot
的元素放到big
中,然后根据small
和big
的大小确定k
在哪个集合中,如果k
在big
中,则递归划分big
,若k
在small
中,则递归划分small
。
优化:【三路划分】。对于包含大量重复元素的数组,每轮的哨兵划分都可能将数组划分为长度为1
和n−1
的两个部分,这种情况下快速排序的时间复杂度会退化至O(N^2)
。「三路划分」,即每轮将数组划分为三个部分:小于、等于和大于基准数的所有元素。这样当发现第k
大数字处在“等于基准数”的子数组中时,便可以直接返回该元素。
为了进一步提升算法的稳健性,我们采用随机选择的方式来选定基准数。 -
方法2:
基于堆排序的选择。需要自己手写堆,而不是直接使用PriorityQueue。
怎么手写堆呢?
首先堆是一个完全二叉树。对于一个初始的堆,需要从
完全二叉树的的最后一个非叶子节点的位置:数组长度的一半减一
开始,倒着进行下沉调整操作,直至调整至一个大顶堆。
然后移除前k-1
个最大元素,每次的操作是:将堆顶元素和堆尾元素交换,将堆尾元素移除(heapSize--
),调整堆。
最后返回堆顶元素,也就是第k
大元素。
代码
方法1:三路快速选择
class Solution {
public int findKthLargest(int[] nums, int k) {
List<Integer> numsList = new ArrayList<>();
for(int num : nums) {
numsList.add(num);
}
return quickSelect(numsList, k);
}
int quickSelect(List<Integer> nums, int k) {
//随机选择基准数
Random rand = new Random();
int pivot = nums.get(rand.nextInt(nums.size()));
//将大于、小于、等于 pivot 的元素划分至 big, small, equal 中
List<Integer> big = new ArrayList<>();
List<Integer> small = new ArrayList<>();
List<Integer> equal = new ArrayList<>();
for(int num : nums) {
if(num > pivot) {
big.add(num);
} else if(num < pivot) {
small.add(num);
} else {
equal.add(num);
}
}
//第 k 大元素在 big 中,递归划分 big
if(k <= big.size()) {
return quickSelect(big, k);
}
//第 k 大元素在 small 中,递归划分 small
if(nums.size() - small.size() < k) {
return quickSelect(small, k - big.size() - equal.size());
}
//第 k 大元素在 equal 中,直接返回 pivot
return pivot;
}
}
方法2:手写大顶堆
class Solution {
public int findKthLargest(int[] nums, int k) {
int heapSize = nums.length, n = nums.length;
//建立大顶堆
buildMaxHeap(nums, heapSize);
//移除前k-1个最大元素
//每次移除最大元素,并重新调整大顶堆
for(int i=n-1; i>=n-k+1; --i) {
swap(nums, 0, i);
--heapSize;
//从顶部开始递归调整
maxHeapify(nums, 0, heapSize);
}
return nums[0];
}
void buildMaxHeap(int[] nums, int heapSize) {
//完全二叉树的的最后一个非叶子节点的位置:数组长度的一半减一
for(int i=heapSize/2; i>=0; i--) {
maxHeapify(nums, i, heapSize);
}
}
//从i位置开始向下递归调整
void maxHeapify(int[] nums, int i, int heapSize) {
int l = i * 2 + 1, r = i * 2 + 2, largest = i;
if(l < heapSize && nums[l] > nums[largest]) {
largest = l;
}
if(r < heapSize && nums[r] > nums[largest]) {
largest = r;
}
if(largest != i) {
swap(nums, i, largest);
maxHeapify(nums, largest, heapSize);
}
}
void swap(int[] nums, int i, int j) {
int tmp = nums[i];
nums[i] = nums[j];
nums[j] = tmp;
}
}
使用PriorityQueue:
class Solution {
public int findKthLargest(int[] nums, int k) {
PriorityQueue<Integer> numsQueue = new PriorityQueue<>(Collections.reverseOrder());
for(int num : nums) {
numsQueue.offer(num);
}
while(k-1 > 0) {
numsQueue.poll();
k--;
}
return numsQueue.peek();
}
}
2. 前k个高频元素⭕
题目描述
题目链接
给你一个整数数组 nums 和一个整数 k ,请你返回其中出现频率前 k 高的元素。你可以按 任意顺序 返回答案。
解题思路
- 维护一个小顶堆,每次加入新的元素时,如果堆大小小于k就直接加入,如果堆大小等于k,就比较新元素和堆顶元素的大小,只有新元素大于堆顶元素时才加入。
代码
class Solution {
public int[] topKFrequent(int[] nums, int k) {
Map<Integer, Integer> map = new HashMap<>();
for(int num : nums) {
map.put(num, map.getOrDefault(num, 0) + 1);
}
PriorityQueue<int[]> pq = new PriorityQueue<>((n1, n2) -> n1[1] - n2[1]);
for(Map.Entry<Integer, Integer> entry : map.entrySet()) {
pq.offer(new int[]{entry.getKey(), entry.getValue()});
if(pq.size() > k) {
pq.poll();
}
}
int[] ans = new int[k];
for(int i=0; i<k; i++) {
ans[i] = pq.poll()[0];
}
return ans;
}
}
3. 数据流的中位数🔺
题目描述
题目链接
中位数是有序整数列表中的中间值。如果列表的大小是偶数,则没有中间值,中位数是两个中间值的平均值。
- 例如 arr = [2,3,4] 的中位数是 3 。
- 例如 arr = [2,3] 的中位数是 (2 + 3) / 2 = 2.5 。
实现 MedianFinder 类:
- MedianFinder() 初始化 MedianFinder 对象。
- void addNum(int num) 将数据流中的整数 num 添加到数据结构中。
- double findMedian() 返回到目前为止所有元素的中位数。与实际答案相差 10-5 以内的答案将被接受。
解题思路
- 小顶堆A保存较大的一半,大顶堆B保存较小的一半。
- 当数量为奇数时,A中多保存一个元素。
代码
class MedianFinder {
Queue<Integer> A;
Queue<Integer> B;
public MedianFinder() {
A = new PriorityQueue<>(); //小顶堆,保存较大的一半
B = new PriorityQueue<>((n1, n2) -> n2 - n1); //大顶堆,保存较小的一半
}
public void addNum(int num) {
//将新元素加入B,并将B中最大的元素转移到A(奇数时A中多一个元素)
if(A.size() == B.size()) {
B.add(num);
A.add(B.poll());
} else {
//将新元素加入A,并将A中最小的元素转移到B(偶数时AB元素相等)
A.add(num);
B.add(A.poll());
}
}
public double findMedian() {
return A.size() == B.size() ? (A.peek() + B.peek()) / 2.0 : A.peek();
}
}
/**
* Your MedianFinder object will be instantiated and called as such:
* MedianFinder obj = new MedianFinder();
* obj.addNum(num);
* double param_2 = obj.findMedian();
*/
十四、贪心算法
1. 买卖股票的最佳时机⭕
题目描述
题目链接
给定一个数组 prices ,它的第 i 个元素 prices[i] 表示一支给定股票第 i 天的价格。
你只能选择 某一天 买入这只股票,并选择在 未来的某一个不同的日子 卖出该股票。设计一个算法来计算你所能获取的最大利润。
返回你可以从这笔交易中获取的最大利润。如果你不能获取任何利润,返回 0 。
解题思路
- 方法1:动态规划,注意:只能买卖一次!
dp[i][0] = 第i天不持有股票的最大利润
dp[i][1] = 第i天持有股票的最大利润
dp[i][0] = max(dp[i-1][0], dp[i-1][1] + prices[i])
dp[i][1] = max(dp[i-1][1], - prices[i])
- 方法2:第二种解析其实是一种动态的变化,在遍历向前推进时,找到一个最小买入价格minprice,然后,在没有找到下一个更小的买入价格时,计算接下来每一天的利润,记录其中最大利润。如果找到下一个最小买入价格minprice,继续计算接下来未找到下一个更小买入价格时的利润最大值,直到遍历完prices数组,maxProfit就是历史最大差值!
代码
方法1:
class Solution {
public int maxProfit(int[] prices) {
int n = prices.length;
int[][] dp = new int[n][2];
dp[0][0] = 0;
dp[0][1] = -prices[0];
for(int i=1; i<n; i++) {
dp[i][0] = Math.max(dp[i-1][0], dp[i-1][1] + prices[i]);
dp[i][1] = Math.max(dp[i-1][1], -prices[i]);
}
return Math.max(0, dp[n-1][0]);
}
}
方法2:
class Solution {
public int maxProfit(int[] prices) {
int minPrice = Integer.MAX_VALUE;
int ans = 0;
for(int i=0; i<prices.length; i++) {
if(prices[i] < minPrice) {
minPrice = prices[i];
} else {
ans = Math.max(ans, prices[i] - minPrice);
}
}
return ans;
}
}
2. 跳跃游戏⭕
题目描述
题目链接
给你一个非负整数数组 nums ,你最初位于数组的 第一个下标 。数组中的每个元素代表你在该位置可以跳跃的最大长度。
判断你是否能够到达最后一个下标,如果可以,返回 true ;否则,返回 false 。
解题思路
方法1:
- 关键在于最大覆盖范围是否能覆盖终点,不用在乎怎么走过来的。
- 但是只有能够走到这个位置,才能计算这个位置能覆盖的范围。所以for循环的终止条件cover是不断更改的
方法2:我觉得更好懂
- cover记录当前能覆盖的最大位置,如果当前走到的位置 > cover,直接返回false;否则更新cover,如果cover能覆盖终点返回true
代码
方法2:
class Solution {
public boolean canJump(int[] nums) {
int cover = 0;
for(int i=0; i<nums.length; i++) {
if(cover < i) {
return false;
}
cover = Math.max(cover, i + nums[i]);
if(cover >= nums.length-1) {
return true;
}
}
return true;
}
}
方法1:
class Solution {
public boolean canJump(int[] nums) {
int cover = 0;
for(int i=0; i<=cover; i++) {
cover = Math.max(cover, i + nums[i]);
if(cover >= nums.length - 1) {
return true;
}
}
return false;
}
}
3. 跳跃游戏II🔺
题目描述
题目链接
和上一题的条件一样,但是问题是求到达终点的最小跳跃次数
解题思路
如果某一个作为 起跳点 的格子可以跳跃的距离是 3,那么表示后面 3 个格子都可以作为 起跳点。
11. 可以对每一个能作为 起跳点 的格子都尝试跳一次,把 能跳到最远的距离 不断更新。
如果从这个 起跳点 起跳叫做第 1 次 跳跃,那么从后面 3 个格子起跳 都 可以叫做第 2 次 跳跃。
所以,当一次 跳跃 结束时,从下一个格子开始,到现在 能跳到最远的距离,都 是下一次 跳跃 的 起跳点。
31. 对每一次 跳跃 用 for 循环来模拟。
跳完一次之后,更新下一次 起跳点 的范围。
在新的范围内跳,更新 能跳到最远的距离。
记录 跳跃 次数,如果跳到了终点,就得到了结果。
代码
class Solution {
public int jump(int[] nums) {
int ans = 0;
int cover = 0; //当前覆盖范围
int next = 0; //下一个最大覆盖范围
for(int i=0; i<nums.length-1; i++) {
next = Math.max(next, i + nums[i]); //更新下一步可以走到的最大范围
if(i == cover) { //走到当前覆盖范围的尽头,需要再走一步才能到达下一个最大覆盖范围
//此时已经进入了下一个最大覆盖范围
ans++;
cover = next;
}
if(cover >= nums.length-1) {
break;
}
}
return ans;
}
}
4. 划分字母区间⭕
题目描述
题目链接
给你一个字符串 s 。我们要把这个字符串划分为尽可能多的片段,同一字母最多出现在一个片段中。
注意,划分结果需要满足:将所有划分结果按顺序连接,得到的字符串仍然是 s 。
返回一个表示每个字符串片段的长度的列表。
解题思路
- 首先遍历字符串s,记录每个字母出现的最后位置
- 然后遍历字符串,curStart表示当前子串的开始位置,curEnd表示当前子串中的所有字母的最后出现位置中的最大值,如果当前遍历到的位置
i == curEnd
,则添加一个答案
代码
class Solution {
public List<Integer> partitionLabels(String s) {
int[] dict = new int[26]; //记录每种字符出现的最后位置
for(int i=0; i<s.length(); i++) {
dict[s.charAt(i)-'a'] = i;
}
List<Integer> ans = new ArrayList<>();
int curStart = 0, curEnd = 0;
for(int i=0; i<s.length(); i++) {
curEnd = Math.max(curEnd, dict[s.charAt(i)-'a']);
if(curEnd == i) {
ans.add(curEnd - curStart + 1);
curStart = curEnd + 1;
}
}
return ans;
}
}
十五、动态规划
1. 杨辉三角⭕
题目描述
题目链接
给定一个非负整数 numRows,生成「杨辉三角」的前 numRows 行。
在「杨辉三角」中,每个数是它左上方和右上方的数的和。
解题思路
nums[i][j] = nums[i-1][j-1] + nums[i-1][j]
- 每一行的首尾元素都是1,直接填就行,这样也不需要进行边界判断
代码
class Solution {
public List<List<Integer>> generate(int numRows) {
List<List<Integer>> ans = new ArrayList<>();
for(int i=0; i<numRows; i++) {
List<Integer> tmp = new ArrayList<>();
tmp.add(1);
for(int j=1; j<i; j++) {
int n1 = ans.get(i-1).get(j-1);
int n2 = ans.get(i-1).get(j);
tmp.add(n1 + n2);
}
if(i > 0) {
tmp.add(1);
}
ans.add(new ArrayList(tmp));
}
return ans;
}
}
2. 打家劫舍
题目描述
题目链接
你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。
给定一个代表每个房屋存放金额的非负整数数组,计算你 不触动警报装置的情况下 ,一夜之内能够偷窃到的最高金额。
解题思路
dp[i] = nums[0...i]能偷到的最大值
在i位置时,分为偷i和不偷i
dp[i] = max(nums[i] + dp[i-2], dp[i-1])
dp[0] = nums[0]
dp[1] = max(nums[1], nums[0])
代码
可以用滚动数组优化
class Solution {
public int rob(int[] nums) {
int n = nums.length;
if(n == 1) return nums[0];
int[] dp = new int[n];
dp[0] = nums[0];
dp[1] = Math.max(nums[0], nums[1]);
for(int i=2; i<n; i++) {
dp[i] = Math.max(nums[i] + dp[i-2], dp[i-1]);
}
return dp[n-1];
}
}
3. 完全平方数🔺
题目描述
题目链接
给你一个整数 n ,返回 和为 n 的完全平方数的最少数量 。
完全平方数 是一个整数,其值等于另一个整数的平方;换句话说,其值等于一个整数自乘的积。例如,1、4、9 和 16 都是完全平方数,而 3 和 11 不是。
解题思路
- 背包:总和,物品:完全平方数
- 用 完全平方数 装满 总和
- 每个完全平方数可以取无限次 -> 完全背包
完全背包的模板:
外层遍历物品,内层遍历背包,背包必须从小到大。
dp[j] = max(dp[j], dp[j-weight[i]] + value[i])
物品i
的遍历边界是什么?i*i <= n
代码
class Solution {
//完全背包
//物品是完全平方数,背包是总和
public int numSquares(int n) {
int[] dp = new int[n+1];
Arrays.fill(dp, n + 1);
dp[0] = 0;
for(int i=0; i*i <= n; i++) {
for(int j=i*i; j <= n; j++) {
dp[j] = Math.min(dp[j], dp[j-i*i] + 1);
}
}
return dp[n];
}
}
4. 零钱兑换🔺
题目描述
题目链接
给你一个整数数组 coins ,表示不同面额的硬币;以及一个整数 amount ,表示总金额。
计算并返回可以凑成总金额所需的 最少的硬币个数 。如果没有任何一种硬币组合能组成总金额,返回 -1 。
你可以认为每种硬币的数量是无限的。
解题思路
- 完全背包模板
代码
class Solution {
//背包:amount,物品:coins[i]
//每种硬币无限取 -> 完全背包
//dp[j] = 凑成金额j最少需要多少枚硬币
public int coinChange(int[] coins, int amount) {
int[] dp = new int[amount+1];
int n = coins.length;
Arrays.fill(dp, amount+1);
dp[0] = 0;
for(int i=0; i<n; i++) {
for(int j=coins[i]; j<=amount; j++) {
dp[j] = Math.min(dp[j], dp[j-coins[i]] + 1);
}
}
return dp[amount] == amount + 1 ? -1 : dp[amount];
}
}
5. 单词拆分🔺
题目描述
题目链接
给你一个字符串 s 和一个字符串列表 wordDict 作为字典。请你判断是否可以利用字典中出现的单词拼接出 s 。
注意:不要求字典中出现的单词全部都使用,并且字典中的单词可以重复使用。
解题思路
- 到底是不是排列问题?
代码
class Solution {
//每个单词可以取无数次 -> 完全背包
//背包:字符串s,物品:单词
//dp[j] = true 表示 s[0...j]可以由字典中的单词组成
//dp[j] |= dp[j-len(word)] && word in dict
//word是什么? word是以j结尾的s的子串
//dp[0] = true
public boolean wordBreak(String s, List<String> wordDict) {
boolean[] dp = new boolean[s.length() + 1];
dp[0] = true;
HashSet<String> dict = new HashSet<>(wordDict);
for(int j=1; j<=s.length(); j++) {
for(int i=0; i<j; i++) {
String word = s.substring(i, j);
boolean inDict = dict.contains(word);
if(dp[i] && inDict) {
// dp[i]表示s[i]之前的子串都能在字典里找到,inDict表示现在这个s[i,..,j]也能在字典里找到
dp[j] = true;
break;
}
}
}
return dp[s.length()];
}
}
6. 最长递增子序列⭕
题目描述
题目链接
给你一个整数数组 nums ,找到其中最长严格递增子序列的长度。
子序列 是由数组派生而来的序列,删除(或不删除)数组中的元素而不改变其余元素的顺序。例如,[3,6,2,7] 是数组 [0,3,1,6,2,2,7] 的子序列。
解题思路
- 计算以每个
nums[i]
结尾的最长递增子序列,然后取最大值 - 对于每个
nums[i]
,需要遍历其前面的每个nums[j]
,找到nums[j] < nums[i] 的 dp[j] + 1
的最大值
代码
class Solution {
//dp[i] = 以nums[i]结尾的最长递增子序列
//dp[i] = 1, nums[i-1] >= nums[i]
//dp[i] = dp[i-1] + 1, nums[i-1] < nums[i]
public int lengthOfLIS(int[] nums) {
int n = nums.length;
int[] dp = new int[n];
Arrays.fill(dp, 1);
int ans = 1;
for(int i=1; i<n; i++) {
for(int j=0; j<i; j++) {
if(nums[j] < nums[i]) {
dp[i] = Math.max(dp[i], dp[j] + 1);
}
}
ans = Math.max(ans, dp[i]);
}
return ans;
}
}
7. 乘积最大子数组🔺
题目描述
题目链接
给你一个整数数组 nums ,请你找出数组中乘积最大的非空连续子数组(该子数组中至少包含一个数字),并返回该子数组所对应的乘积。
测试用例的答案是一个 32-位 整数。
子数组 是数组的连续子序列。
解题思路
- 按照本来的思路是:
dp[i] = Math.max(nums[i], nums[i] * dp[i-1]);
,dp[i]表示以nums[i]结尾的最大子数组乘积 - 但是如果 负数 * 负数,可以让最小的(也就是最负的)变成最大的,所以我们不仅需要记录以nums[i]结尾的最大子数组乘积
dpMax[i]
,还需要记录以nums[i]结尾的最小子数组乘积dpMin[i]
,比较时是比较max(nums[i], nums[i] * dpMax[i-1], nums[i] * dpMin[i-1])
三个人
代码
可以使用滚动数组优化空间
class Solution {
//dp[i] = 以nums[i]结尾的乘积最大子数组
//dp[i] = max(dp[i-1] * nums[i], nums[i])
public int maxProduct(int[] nums) {
int n = nums.length;
int[] dpMax = new int[n];
int[] dpMin = new int[n];
System.arraycopy(nums, 0, dpMax, 0, n);
System.arraycopy(nums, 0, dpMin, 0, n);
int ans = dpMax[0];
for(int i=1; i<n; i++) {
dpMax[i] = Math.max(nums[i], Math.max(nums[i] * dpMax[i-1], nums[i] * dpMin[i-1]));
dpMin[i] = Math.min(nums[i], Math.min(nums[i] * dpMax[i-1], nums[i] * dpMin[i-1]));
ans = Math.max(ans, dpMax[i]);
}
return ans;
}
}
8. 分割等和子集🔺
题目描述
题目链接
给你一个 只包含正整数 的 非空 数组 nums 。请你判断是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。
解题思路
- 也就是判断是否有一个子集的和为数组总和的一半target
- 判断是否有一个子集的和为target -》 容量为target的背包能取得的最大价值是否等于target
- 每个元素只能取一次 -》 0/1背包 -》 外层物品内层背包,背包必须倒序
dp[j] = max(dp[j-1], dp[j-weight[i]] + value[i])
代码
class Solution {
//判断是否可以得到一个和为总和一半的子序列
//每个元素只能取一次 -> 0/1背包
//背包容量:子集和,物品:nums[i]
//dp[j] = 容量为j的背包,能装下的最大价值
//只有当dp[target] == target时,返回true
//dp[j] = max(dp[j-1], dp[j-weight[i]] + value[i]), 即dp[j] = max(dp[j], dp[j-nums[i]] + nums[i])
public boolean canPartition(int[] nums) {
int total = Arrays.stream(nums).sum();
if(total % 2 == 1) return false;
int target = total / 2;
int[] dp = new int[target + 1];
for(int i=0; i<nums.length; i++) {
for(int j=target; j>=nums[i]; j--) {
dp[j] = Math.max(dp[j], dp[j-nums[i]] + nums[i]);
}
}
return dp[target] == target;
}
}
9. 最长有效括号🔺
题目描述
题目链接
给你一个只包含 '(' 和 ')' 的字符串,找出最长有效(格式正确且连续)括号子串的长度。
解题思路
- 关键词:最长,长度,-》动态规划
- dp[i] = 以s[i]结尾的最长有效括号的长度
- 对于
s[i] = '('
,dp[i] = 0
- 对于
s[i] = ')'
, 需要根据s[i-1]
来判断, - 如果
s[i-1] == '('
,dp[i] = dp[i-2] + 2
- 如果
s[i-1] == ')'
,需要判断s[i-1]
是否有效,如果有效的话(dp[i-1] > 0
),需要判断s[i-dp[i-1]-1]
是否能和s[i]
匹配,如果匹配的话,还要判断一下,dp[i-dp[i-1]-2]
是否有效 - 上述还要进行边界条件判断
代码
class Solution {
//dp[i] = 以s[i]结尾的最长有效括号的长度
public int longestValidParentheses(String s) {
int n = s.length();
int[] dp = new int[n];
int ans = 0;
for(int i=1; i<n; i++) {
char ch = s.charAt(i);
if(ch == ')') {
if(s.charAt(i-1) == '(') {
dp[i] = 2;
if(i-2 >= 0) {
dp[i] += dp[i-2];
}
} else if(s.charAt(i-1) == ')') {
if(dp[i-1] > 0) { //只有以s[i-1]结尾的子串有效,以s[i]结尾的子串才有可能有效
if((i - dp[i-1] - 1 >= 0) && s.charAt(i - dp[i-1] - 1) == '(') {
dp[i] = dp[i-1] + 2;
if(i - dp[i-1] - 2 >= 0) {
dp[i] += dp[i - dp[i-1] - 2];
}
}
}
}
}
ans = Math.max(ans, dp[i]);
}
return ans;
}
}
十六、多维动态规划
1. 不同路径
题目描述
题目链接
一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为 “Start” )。
机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为 “Finish” )。
问总共有多少条不同的路径?
解题思路
- dp[i][j] = 到达(i, j)有多少种不同的路径
- dp[i][j] = dp[i-1][j] + dp[i][j-1]
- 因为每次只能向下/向右移动,所以上边界和左边界的位置都只有一条路径
空间优化:
dp[i][j]
依赖于dp[i-1][j]
(dp[j]
)和dp[i][j-1]
(dp[j-1]
)
代码
class Solution {
//dp[i][j] = 到达(i, j)有多少种不同的路径
//dp[i][j] = dp[i-1][j] + dp[i][j-1]
public int uniquePaths(int m, int n) {
int[][] dp = new int[m][n];
for(int i=0; i<m; i++) {
dp[i][0] = 1;
}
for(int i=0; i<n; i++) {
dp[0][i] = 1;
}
for(int i=1; i<m; i++) {
for(int j=1; j<n; j++) {
dp[i][j] = dp[i-1][j] + dp[i][j-1];
}
}
return dp[m-1][n-1];
}
}
空间优化:
class Solution {
//dp[i][j] = 到达(i, j)有多少种不同的路径
//dp[i][j] = dp[i-1][j] + dp[i][j-1]
public int uniquePaths(int m, int n) {
int[] dp = new int[n];
Arrays.fill(dp, 1);
for(int i=1; i<m; i++) {
for(int j=1; j<n; j++) {
dp[j] = dp[j] + dp[j-1];
}
}
return dp[n-1];
}
}
2. 最小路径和⭕
题目描述
题目链接
给定一个包含非负整数的 m x n 网格 grid ,请找出一条从左上角到右下角的路径,使得路径上的数字总和为最小。
说明:每次只能向下或者向右移动一步。
解题思路
- 和上一个题大差不差
- dp[i][j] = min(dp[i-1][j], dp[i][j-1]) + grid[i][j]
- 也可以优化空间,每次只存储上一行的dp值,但是注意左边界和上边界的dp值是前面的累加,所以每次要更新dp[0]
代码
空间优化后的代码:
class Solution {
//dp[i][j] = 到(i, j)的最小路径和
//dp[i][j] = min(dp[i-1][j], dp[i][j-1]) + grid[i][j]
public int minPathSum(int[][] grid) {
int m = grid.length, n = grid[0].length;
int[] dp = new int[n];
dp[0] = grid[0][0];
for(int i=1; i<n; i++) {
dp[i] = dp[i-1] + grid[0][i];
}
for(int i=1; i<m; i++) {
dp[0] += grid[i][0];
for(int j=1; j<n; j++) {
dp[j] = Math.min(dp[j], dp[j-1]) + grid[i][j];
}
}
return dp[n-1];
}
}
3. 最长回文子串🔺
题目描述
题目链接
给你一个字符串 s,找到 s 中最长的回文子串。
解题思路
方法1:动态规划
dp[i][j] = s[i...j]
是否是回文串dp[i][j] = dp[i+1][j-1] && s[i] == s[j]
- i倒序,j正序
dp[i][i] = true
方法2:中心扩展
- 对于每个位置,有两个扩展中心,
i
和i+1
- 遍历每个位置,计算可以扩展的最大长度
代码
方法1:
class Solution {
public String longestPalindrome(String s) {
int n = s.length();
boolean[][] dp = new boolean[n][n];
for(int i=0; i<n; i++) {
dp[i][i] = true;
}
int start = 0, end = 0;
for(int i=n-1; i>=0; i--) {
for(int j=i+1; j<n; j++) {
if(s.charAt(i) != s.charAt(j)) {
dp[i][j] = false;
} else {
if(j == i+1) {
dp[i][j] = true;
} else {
dp[i][j] = dp[i+1][j-1];
}
}
if(dp[i][j] && j-i > end-start) {
start = i;
end = j;
}
}
}
return s.substring(start, end+1);
}
}
方法2:
class Solution {
public String longestPalindrome(String s) {
int start = 0, end = 0;
for(int i=0; i<s.length(); i++) {
int len1 = expandAroundCenter(s, i, i);
int len2 = expandAroundCenter(s, i, i+1);
int len = Math.max(len1, len2);
if(len > end - start + 1) {
start = i - (len-1)/2;
end = i + len/2;
}
}
return s.substring(start, end+1);
}
//从回文中心开始扩展,能够扩展的最大长度
int expandAroundCenter(String s, int left, int right) {
while(left >= 0 && right < s.length() && s.charAt(left) == s.charAt(right)) {
left--;
right++;
}
return right - left - 1;
}
}
4. 编辑距离🔺
题目描述
题目链接
给你两个单词 word1 和 word2, 请返回将 word1 转换成 word2 所使用的最少操作数。
你可以对一个单词进行如下三种操作:
- 插入一个字符
- 删除一个字符
- 替换一个字符
解题思路
dp[i][j] = 将word1[0...i-1]转换成word2[0...j-1]所使用的最少操作数
- 为什么是
dp[i][j]
代表word1[0...i-1]
和word2[0...j-1]
? 因为包含了长度为0的串,也就是word[0...0]
- 遍历
word1[i]
和word2[j]
:
如果word1[i] == word2[j]
,则dp[i][j] = dp[i-1][j-1]
否则,将word1[0...i]
和word2[0...j]
转换为相同的字符串有三种操作方式(删除/插入/替换 一个字符),dp[i][j]
取其中的最小值即可
代码
class Solution {
//dp[i][j] = 将word1[0...i-1]转换成word2[0...j-1]所使用的最少操作数
//为什么是dp[i][j]代表word1[0...i-1]和word2[0...j-1]? 因为包含了长度为0的串,也就是word[0...0]
public int minDistance(String word1, String word2) {
int[][] dp = new int[word1.length()+1][word2.length()+1];
//有一个串的长度为0时,需要的最少步骤:将另一个串的字符全部删除
for(int i=0; i<=word1.length(); i++) dp[i][0] = i;
for(int j=0; j<=word2.length(); j++) dp[0][j] = j;
for(int i=1; i<=word1.length(); i++) {
for(int j=1; j<=word2.length(); j++) {
if(word1.charAt(i-1) == word2.charAt(j-1)) {
dp[i][j] = dp[i-1][j-1];
} else {
//如果当前两个字符不相等,有三种操作
//删除串a的当前字符 dp[i-1][j] + 1
//删除串b的当前字符 dp[i][j-1] + 1
//插入和删除是一样的,删除串a的字符相当于在串b中插入一个字符
//替换串a 和 替换串b 的当前字符是一样的 dp[i-1][j-1] + 1
dp[i][j] = Math.min(Math.min(dp[i-1][j], dp[i][j-1]), dp[i-1][j-1]) + 1;
}
}
}
return dp[word1.length()][word2.length()];
}
}
5. 最大正方形
题目描述
在一个由 '0' 和 '1' 组成的二维矩阵内,找到只包含 '1' 的最大正方形,并返回其面积。
解题思路
动态规划:
- dp[i][j] = 以(i, j)为右下角,且只包含1的正方形的最大边长。
class Solution {
public int maximalSquare(char[][] matrix) {
int maxSide = 0;
if (matrix == null || matrix.length == 0 || matrix[0].length == 0) {
return maxSide;
}
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]);
}
}
}
int maxSquare = maxSide * maxSide;
return maxSquare;
}
}
6. 圆环回原点
题目描述
圆环上有10个点,编号为0~9。从0点出发,每次可以逆时针和顺时针走一步,问走n步回到0点共有多少种走法。
输入: 2
输出: 2
解释:有2种方案。分别是0->1->0和0->9->0
解题思路
走n步到0的方案数=走n-1步到1的方案数+走n-1步到9的方案数。
代码
package com.walegarrett.programming;
import org.junit.Test;
/**
* @Author WaleGarrett
* @Date 2022/4/6 16:16
*/
public class Addition_2 {
/**
* 圆环上有10个点,编号为0~9。从0点出发,每次可以逆时针和顺时针走一步,问走n步回到0点共有多少种走法。
* 输入: 2
* 输出: 2
* 解释:有2种方案。分别是0->1->0和0->9->0
*/
public int circleSteps(int n, int k){
// 圆环中有n个节点,走k步回答原点有几种走法
// 走k步走到0的走法=走k-1步走到1的走法 + 走k-1步走到num-1的走法
// dp[i][j]表示走i步走到j点的走法种类
// dp[i][j] = dp[i-1][(j+1)%len] + dp[i-1][(j-1+len)%len]
int[][] dp = new int[k+1][n];
dp[0][0] = 1;
for(int i=1; i<=k; i++){
for(int j = 0; j<n; j++){
dp[i][j] = dp[i-1][(j+1)%n] + dp[i-1][(j-1+n)%n];
}
}
return dp[k][0];
}
@Test
public void testCircleSteps(){
System.out.println(circleSteps(10, 2));
}
}
十七、技巧
1. 只出现一次的数字🔺
题目描述
题目链接
给你一个 非空 整数数组 nums ,除了某个元素只出现一次以外,其余每个元素均出现两次。找出那个只出现了一次的元素。
你必须设计并实现线性时间复杂度的算法来解决此问题,且该算法只使用常量额外空间。
解题思路
- 时间复杂度要求O(n),空间复杂度要求O(1),只能用 位运算
异或
代码
class Solution {
public int singleNumber(int[] nums) {
int ans = 0;
for(int num : nums) {
ans ^= num;
}
return ans;
}
}
2. 多数元素🔺
题目描述
题目链接
给定一个大小为 n 的数组 nums ,返回其中的多数元素。多数元素是指在数组中出现次数 大于 ⌊ n/2 ⌋ 的元素。
你可以假设数组是非空的,并且给定的数组总是存在多数元素。
解题思路
- 摩尔投票
- 核心思想--抵消原则: 在一个数组中,如果某个元素的出现次数超过了数组长度的一半,那么这个元素与其他所有元素一一配对,最后仍然会剩下至少一个该元素。 通过“投票”和“抵消”的过程,可以逐步消除不同的元素,最终留下的候选人就是可能的主要元素。
代码
class Solution {
public int majorityElement(int[] nums) {
int x = 0, votes = 0;
for(int num : nums) {
if(votes == 0) {
x = num;
}
votes += num == x ? 1 : -1;
}
return x;
}
}
3. 颜色分类🔺
题目描述
题目链接
给定一个包含红色、白色和蓝色、共 n 个元素的数组 nums ,原地对它们进行排序,使得相同颜色的元素相邻,并按照红色、白色、蓝色顺序排列。
我们使用整数 0、 1 和 2 分别表示红色、白色和蓝色。
必须在不使用库内置的 sort 函数的情况下解决这个问题。
解题思路
题目就是把一个包含0,1,2的数组,按照0,1,2的顺序排序。
方法1:单指针
- 使用一个ptr标记前面待替换的位置,i 往后遍历,第一遍将所有0移到前面,第二遍将所有1移到0的后面
- 需要遍历两遍
方法2:双指针
- 指针p0用来标记0的待放位置,指针p1用来标记1的待放位置,i 往后遍历
- 碰到1时可以直接
交换 nums[p1] 和 nums[i] 并后移p1
,但是碰到0时需要根据 p1是否在p0的后面 来具体情况具体分析 - p1只会和p0在同一个位置,或者在p0的后面。如果p0和p1在同一个位置,只需要
交换nums[p0] 和 nums[i],后移p0和p1
,如果p1在p0的后面,说明一串0的后面已经放了一串1了,也就是说p0当前指向的是已经交换过的1,所以要:先swap(nums[p0], nums[i])
,再swap(nums[p1], nums[i])
,再后移p0和p1
代码
方法2:
class Solution {
public void sortColors(int[] nums) {
int n = nums.length;
int p0 = 0, p1 = 0;
for(int i=0; i<n; i++) {
if(nums[i] == 1) {
swap(nums, i, p1);
p1++;
} else if(nums[i] == 0) {
swap(nums, i, p0);
if(p0 < p1) {
swap(nums, i, p1);
}
p0++;
p1++; //p1只能在p0的后面或者和p0在一个位置,p0移动时,这两种情况p1都要++
}
}
}
void swap(int[] nums, int i, int j) {
int tmp = nums[i];
nums[i] = nums[j];
nums[j] = tmp;
}
}
方法1:
class Solution {
public void sortColors(int[] nums) {
int ptr = 0;
int n = nums.length;
for(int i=0; i<n; i++) {
if(nums[i] == 0) {
swap(nums, i, ptr);
ptr++;
}
}
for(int i=ptr; i<n; i++) {
if(nums[i] == 1) {
swap(nums, i, ptr);
ptr++;
}
}
}
void swap(int[] nums, int i, int j) {
int tmp = nums[i];
nums[i] = nums[j];
nums[j] = tmp;
}
}
4. 下一个排列🔺
题目描述
题目链接
整数数组的一个 排列 就是将其所有成员以序列或线性顺序排列。
例如,arr = [1,2,3] ,以下这些都可以视作 arr 的排列:[1,2,3]、[1,3,2]、[3,1,2]、[2,3,1] 。
整数数组的 下一个排列 是指其整数的下一个字典序更大的排列。更正式地,如果数组的所有排列根据其字典顺序从小到大排列在一个容器中,那么数组的 下一个排列 就是在这个有序容器中排在它后面的那个排列。如果不存在下一个更大的排列,那么这个数组必须重排为字典序最小的排列(即,其元素按升序排列)。
例如,arr = [1,2,3] 的下一个排列是 [1,3,2] 。
类似地,arr = [2,3,1] 的下一个排列是 [3,1,2] 。
而 arr = [3,2,1] 的下一个排列是 [1,2,3] ,因为 [3,2,1] 不存在一个字典序更大的排列。
给你一个整数数组 nums ,找出 nums 的下一个排列。
必须 原地 修改,只允许使用额外常数空间。
解题思路
-
如何得到下一个更大的数?
将【后面的大数】与【前面的小数】交换 -
如何满足增加的幅度尽可能的小?
- 在尽可能靠右的低位进行交换(增大的幅度小),需要从后向前查找
- 将一个尽可能小的大数与前面的小数交换。比如123465,下一个排列应该交换4和5,而不是6和5
- 将「大数」换到前面后,需要将「大数」后面的所有数 重置为升序,升序排列就是最小的排列。以 123465 为例:首先按照上一步,交换 5 和 4,得到 123564;然后需要将 5 之后的数重置为升序,得到 123546
算法过程:
标准的 “下一个排列” 算法可以描述为:
- 从后向前 查找第一个 相邻升序 的元素对 (i,j),满足 A[i] < A[j]。此时 [j,end) 必然是降序
- 在 [j,end) 从后向前 查找第一个满足 A[i] < A[k] 的 k。A[i]、A[k] 分别就是上文所说的「小数」、「大数」
- 将 A[i] 与 A[k] 交换
- 可以断定这时 [j,end) 必然是降序,逆置 [j,end),使其升序
- 如果在步骤 1 找不到符合的相邻元素对,说明当前 [begin,end) 为一个降序顺序,则直接跳到步骤 4
代码
class Solution {
public void nextPermutation(int[] nums) {
int n = nums.length;
// 1. 从后向前找第一对相邻升序元素
int i = n-2, j = n-1;
while(i >= 0) {
if(nums[i] < nums[j]) {
break;
} else {
i--;
j--;
}
}
// 2. 若找不到,说明整个序列是降序的
if(i < 0) {
Arrays.sort(nums);
return;
}
// 3. 在[j, end]从后往前找第一个大于nums[i]的nums[k]
int k = 0;
for(k = n-1; k >= j; k--) {
if(nums[k] > nums[i]) {
break;
}
}
// 4. 交换nums[i]和nums[k]
int tmp = nums[i];
nums[i] = nums[k];
nums[k] = tmp;
// 5. 倒置nums[j...end]
Arrays.sort(nums, j, n); //对 nums[j, n) 进行排序
}
}
5. 寻找重复数🔺
题目描述
题目链接
给定一个包含 n + 1 个整数的数组 nums ,其数字都在 [1, n] 范围内(包括 1 和 n),可知至少存在一个重复的整数。
假设 nums 只有 一个重复的整数 ,返回 这个重复的数 。
你设计的解决方案必须 不修改 数组 nums 且只用常量级 O(1) 的额外空间。
解题思路
- 下标的范围是
[0, n]
,数字的范围是[1, n]
,因此必定存在至少一个重复数 - 如果没有重复数字,下标和数字的映射关系是一对一的。但是存在重复数字,所以映射关系是多对一的(也就是有多个下标会映射到同一个数字)。
- 然后不知道怎么就可以转化成 环形链表 这道题目了,不同之处在于这道题目不用判断是否有环(必定有环),环的入口就是重复元素。
- tips:如何跳过第一次条件判断?用
do-while
代码
class Solution {
public int findDuplicate(int[] nums) {
int slow = 0, fast = 0;
do {
slow = nums[slow];
fast = nums[nums[fast]];
} while(slow != fast);
slow = 0;
while(slow != fast) {
slow = nums[slow];
fast = nums[fast];
}
return slow;
}
}
补充
1. 快速排序
题目描述
题目链接
给你一个整数数组 nums,请你将该数组升序排列。
你必须在 不使用任何内置函数 的情况下解决问题,时间复杂度为 O(nlog(n)),并且空间复杂度尽可能小。
代码
class Solution {
public int[] sortArray(int[] nums) {
randomizedQuicksort(nums, 0, nums.length - 1);
return nums;
}
public void randomizedQuicksort(int[] nums, int l, int r) {
if (l < r) {
int pos = randomizedPartition(nums, l, r);
randomizedQuicksort(nums, l, pos - 1);
randomizedQuicksort(nums, pos + 1, r);
}
}
public int randomizedPartition(int[] nums, int l, int r) {
int i = new Random().nextInt(r - l + 1) + l; // 随机选一个作为我们的主元
swap(nums, r, i);
return partition(nums, l, r);
}
public int partition(int[] nums, int l, int r) {
int pivot = nums[r];
int i = l - 1;
for (int j = l; j <= r - 1; ++j) {
if (nums[j] <= pivot) {
i = i + 1;
swap(nums, i, j);
}
}
swap(nums, i + 1, r);
return i + 1;
}
private void swap(int[] nums, int i, int j) {
int temp = nums[i];
nums[i] = nums[j];
nums[j] = temp;
}
}
2. 用rand7实现rand
题目描述
题目链接
给定方法 rand7 可生成 [1,7] 范围内的均匀随机整数,试写一个方法 rand10 生成 [1,10] 范围内的均匀随机整数。
你只能调用 rand7() 且不能调用其他方法。请不要使用系统的 Math.random() 方法。
每个测试用例将有一个内部参数 n,即你实现的函数 rand10() 在测试时将被调用的次数。请注意,这不是传递给 rand10() 的参数。
代码
/**
* The rand7() API is already defined in the parent class SolBase.
* public int rand7();
* @return a random integer in the range 1 to 7
*/
class Solution extends SolBase {
public int rand10() {
int ans = 0;
do {
ans = (rand7() - 1) * 7 + (rand7() - 1);
} while(ans >= 40);
return ans % 10 + 1;
}
}
3. 设计缓存类
题目描述
实现一个缓存类:成员函数有set()和get();
1、含有最大容量n的限制;
2、有超时限制;
3、满了以后剔除队头,从尾端插入新的数据
代码
import java.util.HashMap;
import java.util.LinkedList;
import java.util.Map;
public class CachedData {
private int maxCapacity;
private Map<String, CachedItem> cacheMap;
private LinkedList<String> cacheOrder;
public CachedData(int maxCapacity) {
this.maxCapacity = maxCapacity;
cacheMap = new HashMap<>();
cacheOrder = new LinkedList<>();
}
public void set(String key, Object value, long timeoutMillis) {
if (cacheMap.size() >= maxCapacity) {
String oldestKey = cacheOrder.removeFirst();
cacheMap.remove(oldestKey);
}
long expirationTime = System.currentTimeMillis() + timeoutMillis;
CachedItem item = new CachedItem(value, expirationTime);
cacheMap.put(key, item);
cacheOrder.addLast(key);
}
public Object get(String key) {
CachedItem item = cacheMap.get(key);
if (item == null) {
return null;
}
if (System.currentTimeMillis() > item.expirationTime) {
cacheOrder.remove(key);
cacheMap.remove(key);
return null;
}
return item.value;
}
private static class CachedItem {
Object value;
long expirationTime;
public CachedItem(Object value, long expirationTime) {
this.value = value;
this.expirationTime = expirationTime;
}
}
}
public class Main {
public static void main(String[] args) {
CachedData cache = new CachedData(3);
cache.set("key1", "value1", 5000);
cache.set("key2", "value2", 4000);
cache.set("key3", "value3", 3000);
System.out.println(cache.get("key1"));
System.out.println(cache.get("key2"));
System.out.println(cache.get("key3"));
try {
Thread.sleep(3500);
} catch (InterruptedException e) {
e.printStackTrace();
}
cache.set("key4", "value4", 2000);
System.out.println(cache.get("key1"));
System.out.println(cache.get("key2"));
System.out.println(cache.get("key3"));
System.out.println(cache.get("key4"));
}
}
4. 字符串相乘
题目描述
给定两个以字符串形式表示的非负整数 num1 和 num2,返回 num1 和 num2 的乘积,它们的乘积也表示为字符串形式。
注意:不能使用任何内置的 BigInteger 库或直接将输入转换为整数。
解题思路
代码
class Solution {
public String multiply(String num1, String num2) {
if (num1.equals("0") || num2.equals("0")) {
return "0";
}
int m = num1.length(), n = num2.length();
int[] ansArr = new int[m + n];
for (int i = m - 1; i >= 0; i--) {
int x = num1.charAt(i) - '0';
for (int j = n - 1; j >= 0; j--) {
int y = num2.charAt(j) - '0';
ansArr[i + j + 1] += x * y;
}
}
for (int i = m + n - 1; i > 0; i--) {
ansArr[i - 1] += ansArr[i] / 10;
ansArr[i] %= 10;
}
int index = ansArr[0] == 0 ? 1 : 0;
StringBuilder ans = new StringBuilder();
while (index < m + n) {
ans.append(ansArr[index++]);
}
return ans.toString();
}
}
5. 字符串转换整数(atoi)
题目描述
请你来实现一个 myAtoi(string s) 函数,使其能将字符串转换成一个 32 位有符号整数。
函数 myAtoi(string s) 的算法如下:
- 空格:读入字符串并丢弃无用的前导空格(" ")
- 符号:检查下一个字符(假设还未到字符末尾)为 '-' 还是 '+'。如果两者都不存在,则假定结果为正。
- 转换:通过跳过前置零来读取该整数,直到遇到非数字字符或到达字符串的结尾。如果没有读取数字,则结果为0。
- 舍入:如果整数数超过 32 位有符号整数范围 [−2^31, 2^31 − 1] ,需要截断这个整数,使其保持在这个范围内。具体来说,小于 −2^31 的整数应该被舍入为 −2^31 ,大于 2^31 − 1 的整数应该被舍入为 2^31 − 1 。
返回整数作为最终结果。
代码
class Solution {
public int myAtoi(String s) {
int sign = 1, ans = 0, index = 0;
char[] array = s.toCharArray();
while (index < array.length && array[index] == ' ') {
index ++;
}
if (index < array.length && (array[index] == '-' || array[index] == '+')) {
sign = array[index++] == '-' ? -1 : 1;
}
while (index < array.length && array[index] <= '9' && array[index] >= '0') {
int digit = array[index++] - '0';
if (ans > (Integer.MAX_VALUE - digit) / 10) {
return sign == 1 ? Integer.MAX_VALUE : Integer.MIN_VALUE;
}
ans = ans * 10 + digit;
}
return ans * sign;
}
}
6. 分发糖果
题目描述
n 个孩子站成一排。给你一个整数数组 ratings 表示每个孩子的评分。
你需要按照以下要求,给这些孩子分发糖果:
每个孩子至少分配到 1 个糖果。
相邻两个孩子评分更高的孩子会获得更多的糖果。
请你给每个孩子分发糖果,计算并返回需要准备的 最少糖果数目 。
代码
class Solution {
public int candy(int[] ratings) {
int n = ratings.length;
int[] candies = new int[n];
// 初始化每个孩子至少有1个糖果
for (int i = 0; i < n; i++) {
candies[i]=1;
}
// 从左到右遍历
for (int i = 1; i < n; i++) {
if (ratings[i]>ratings[i - 1]) {
candies[i]=candies[i - 1]+1;
}
}
// 从右到左遍历
for (int i = n - 2; i >= 0; i--) {
if (ratings[i]>ratings[i + 1] && candies[i]<=candies[i + 1]) {
candies[i]=candies[i + 1]+1;
}
}
int totalCandies = 0;
for (int num : candies) {
totalCandies += num;
}
return totalCandies;
}
}
7. 对角线遍历
题目描述
给你一个大小为 m x n 的矩阵 mat ,请以对角线遍历的顺序,用一个数组返回这个矩阵中的所有元素。
解题思路
- 每一个对角线上的元素坐标x+y等于一个固定的值
代码
class Solution {
public int[] findDiagonalOrder(int[][] mat) {
int m = mat.length;
int n = mat[0].length;
int[] nums = new int[m*n];
int idx = 0;
int sum = 0; // i = x + y
while (sum < m + n) {
// 第 1, 3, 5... 趟
int x1 = (sum < m)? sum : m - 1; // 超过这个阈值之后就不会再增加了,只能从最后一行开始
int y1 = sum - x1;
while (x1 >= 0 && y1 < n) {
nums[idx++] = mat[x1][y1];
x1--;
y1++;
}
sum++;
if (sum >= m + n) break;
// 第 2, 4, 6... 趟
int y2 = (sum < n)? sum : n - 1;
int x2 = sum - y2;
while (y2 >= 0 && x2 < m) {
nums[idx++] = mat[x2][y2];
x2++;
y2--;
}
sum++;
}
return nums;
}
}
8. 36进制加法
题目描述
36进制由0-9,a-z,共36个字符表示。
要求按照加法规则计算出任意两个36进制正整数的和,如1b + 2x = 48 (解释:47+105=152)
要求:不允许使用先将36进制数字整体转为10进制,相加后再转回为36进制的做法
代码
public class Main {
public static char getChar(int n) {
if (n <= 9) {
return (char) (n + '0');
} else {
return (char) (n - 10 + 'a');
}
}
public static int getInt(char ch) {
if ('0' <= ch && ch <= '9') {
return ch - '0';
} else {
return ch - 'a' + 10;
}
}
public static String add36Strings(String num1, String num2) {
int carry = 0;
int i = num1.length() - 1;
int j = num2.length() - 1;
int x, y;
StringBuilder res = new StringBuilder();
while (i >= 0 || j >= 0 || carry > 0) {
x = i >= 0? getInt(num1.charAt(i)) : 0;
y = j >= 0? getInt(num2.charAt(j)) : 0;
int temp = x + y + carry;
res.append(getChar(temp % 36));
carry = temp / 36;
i--;
j--;
}
return res.reverse().toString();
}
public static void main(String[] args) {
String a = "1b";
String b = "2x";
String c = add36Strings(a, b);
System.out.println(c);
}
}
9. 高效的生产范围m内的n个随机数
代码
public class Main {
public static void main(String[] args) {
int[] a = new int[100];
for (int i = 0; i < 100; i++) {
a[i] = i;
}
Random random = new Random();
for (int i = 99; i >= 1; i--) {
int randomIndex = random.nextInt(i);
int temp = a[i];
a[i] = a[randomIndex];
a[randomIndex] = temp;
}
for (int num : a) {
System.out.print(num + " ");
}
}
}
10. 将一个数组划分成m份,令每份的和相等,求m的最大值
代码
import java.util.Arrays;
class Main {
public static void main(String[] args) {
int[] arr = {3,2,4,3,6};
System.out.println(divideArray(arr, arr.length));
}
static int divideArray(int[] arr, int size) {
if (size <= 1) {
return size >= 0? size : 0;
}
// 计算数组元素之和
int sum = 0, m = 0, i = 0;
for (; i<size; i++) {
sum += arr[i];
}
boolean[] tags = new boolean[size]; // 标记数组中的元素是否已经被分配到某个子数组中
// 尝试不同的m
for (m = size; m > 1; m--) {
if (sum % m!= 0) {
continue;
}
Arrays.fill(tags, false);
for (i = 0; i < size; i++) {
if (!tags[i]) {
// 如果元素 i 没有分块,就尝试将其分配到当前子数组中
tags[i] = true;
// 如果 i 元素分块失败,就跳出 for 循环,m 值失败。
if (!divideArray(arr, tags, size, sum / m - arr[i])) {
break;
}
}
}
if (i >= size) {
// 如果 i>=size,表示数组元素都被成功分配,此时 m 为最大值。
break;
}
}
return m;
}
// 从数组中找出和为 sum 的组合。
static boolean divideArray(int[] arr, boolean[] tags, int size, int sum) {
if (sum < 0) {
return false;
} else if (sum == 0) {
return true;
}
/*
从后往前遍历数组,对于未分配且不超过目标和的元素,尝试将其分配到当前子数组中,
并递归地检查剩余元素是否能够组成目标和减去该元素后的和。如果分配成功则返回 true,
否则将该元素的标记恢复为未分配并继续尝试下一个元素。
*/
for (int i = size - 1; i >= 0; i--) {
if ((!tags[i]) && arr[i] <= sum) {
tags[i] = true;
if (divideArray(arr, tags, size, sum - arr[i])) {
return true;
} else {
tags[i] = false;
}
}
}
return false;
}
}
11. 最大为 N 的数字组合
题目描述
给定一个按 非递减顺序 排列的数字数组 digits 。你可以用任意次数 digits[i] 来写的数字。例如,如果 digits = ['1','3','5'],我们可以写数字,如 '13', '551', 和 '1351315'。
返回 可以生成的小于或等于给定整数 n 的正整数的个数 。
解题思路
不知道,这题如果出了可以理解为面试官不想要你。。。
dp[i][0]=m+dp[i−1][0]×m+dp[i−1][1]×C[i]
其实重点要理解这个公式的推导过程 就像题解一中对dp的定义
- dp[i][0]表示由digits构成且小于n的前i位的数字的个数
- dp[i][1]表示由digits构成且等于n的前i位数字的个数
- 那么对于公式中的各个部分
m 就是 dp[i][0] 中单个字符所组成的数量
- dp[i-1][0] × m :dp[i-1][0]小于n的前i-1位的数字个数,第i位就可以是m个字符其中之一,所以乘以m
- dp[i−1][1]×C[i] :C[i] 表示数组digits中小于n的第i位数字的元素个数,dp[i-1][1]等于n的前i-1位的数字个数,因为前边都相等了,所以这个后边的这一位必须小于n[i] ,那么所组成的字符串个数就是乘以C[i]。
代码
class Solution {
public int atMostNGivenDigitSet(String[] digits, int n) {
String s = Integer.toString(n);
int m = digits.length, k = s.length();
int[][] dp = new int[k + 1][2];
dp[0][1] = 1;
for (int i = 1; i <= k; i++) {
for (int j = 0; j < m; j++) {
if (digits[j].charAt(0) == s.charAt(i - 1)) {
dp[i][1] = dp[i - 1][1];
} else if (digits[j].charAt(0) < s.charAt(i - 1)) {
// dp[i][0]=m+dp[i−1][0]×m+dp[i−1][1]×C[i]
// C[i] 表示数组digits中小于n的第i位数字的元素个数 ,这里的 s[i-1] 就是 n[i],
// 这个循环分支就是在数 c[i]的个数,循环了几次就是加了几次 相当于 ×C[i]
dp[i][0] += dp[i - 1][1];
} else {
break;
}
}
if (i > 1) {
dp[i][0] += m + dp[i - 1][0] * m;
}
}
return dp[k][0] + dp[k][1];
}
}
12. 找到离给定两个节点最近的节点
题目描述
题目链接
给你一个 n 个节点的 有向图 ,节点编号为 0 到 n - 1 ,每个节点 至多 有一条出边。
有向图用大小为 n 下标从 0 开始的数组 edges 表示,表示节点 i 有一条有向边指向 edges[i] 。如果节点 i 没有出边,那么 edges[i] == -1 。
同时给你两个节点 node1 和 node2 。
请你返回一个从 node1 和 node2 都能到达节点的编号,使节点 node1 和节点 node2 到这个节点的距离 较大值最小化。如果有多个答案,请返回 最小 的节点编号。如果答案不存在,返回 -1 。
注意 edges 可能包含环。
解题思路
- 分别求出node1和node2到每个节点的距离,然后遍历找答案
代码
class Solution {
public int closestMeetingNode(int[] edges, int node1, int node2) {
int[] d1 = calcDis(edges, node1), d2 = calcDis(edges, node2);
int ans = -1, n = edges.length;
// 遍历每个节点,看能否到达n1和n2,且最大距离最小化
for (int i = 0, minDis = n; i < n; ++i) {
int d = Math.max(d1[i], d2[i]);
if (d < minDis) {
minDis = d;
ans = i;
}
}
return ans;
}
// 计算x到每个节点的距离
int[] calcDis(int[] edges, int x) {
int n = edges.length;
int[] dis = new int[n];
Arrays.fill(dis, n);
// 从节点x出发,只要节点合法且没被访问过,就更新到他的最短距离
for (int d = 0; x >= 0 && dis[x] == n; x = edges[x])
dis[x] = d++;
return dis;
}
}
13. 串联所有单词的子串
题目描述
题目链接
给定一个字符串 s 和一个字符串数组 words。 words 中所有字符串 长度相同。
s 中的 串联子串 是指一个包含 words 中所有字符串以任意顺序排列连接起来的子串。
例如,如果 words = ["ab","cd","ef"], 那么 "abcdef", "abefcd","cdabef", "cdefab","efabcd", 和 "efcdab" 都是串联子串。 "acdbef" 不是串联子串,因为他不是任何 words 排列的连接。
返回所有串联子串在 s 中的开始索引。你可以以 任意顺序 返回答案。
解题思路
- 滑动窗口
代码
class Solution {
List<Integer> res = new ArrayList<>();
public List<Integer> findSubstring(String s, String[] words) {
int wordLen = words[0].length(), wordNum = words.length;
Map<String, Integer> map = new HashMap<>(); // word出现的次数
for(String w: words){
map.put(w, map.getOrDefault(w,0) + 1);
}
for(int i = 0; i < wordLen; i++){
// 定义一个count表征当前窗口的size
int left = i, right = i,count = 0;
// 定义一个窗口
Map<String,Integer> window = new HashMap<>();
while(right <= s.length() - wordLen){
String w = s.substring(right, right + wordLen);
window.put(w, window.getOrDefault(w,0) + 1); // 取一个单词放进窗口
// right移动
right += wordLen;
count++;
// 收缩窗口(包含了words中没有这个word的情况)
while(window.getOrDefault(w,0) > map.getOrDefault(w,0)){
String tmp_w = s.substring(left, left + wordLen);
window.put(tmp_w, window.getOrDefault(tmp_w, 0) - 1);
count--;
// left移动
left += wordLen;
}
if(count == wordNum) res.add(left);
}
}
return res;
}
}
14. 分割平衡字符串
题目描述
题目链接
平衡字符串 中,'L' 和 'R' 字符的数量是相同的。
给你一个平衡字符串 s,请你将它分割成尽可能多的子字符串,并满足:
每个子字符串都是平衡字符串。
返回可以通过分割得到的平衡字符串的 最大数量 。
解题思路
- 贪心
- 一个平衡字符串,拆分出了一些平衡字符串后,剩下的一定还是平衡字符串
代码
class Solution {
public int balancedStringSplit(String s) {
int ans = 0, d = 0;
for (int i = 0; i < s.length(); ++i) {
char ch = s.charAt(i);
if (ch == 'L') {
++d;
} else {
--d;
}
if (d == 0) {
++ans;
}
}
return ans;
}
}