文章目录
- 滑动窗、双指针(三指针、快慢指针)
- 回溯
- 拓扑排序
- 栈
- 哈希表
- 看规律、归纳
- 动态规划
- 斐波那契数列(用动态规划也可,斐波那契数列可看作是动态规划的优化空间的版本)
- 区间
- 单调栈
- 树
- 链表
- 排序
- 数组
- Trie (前缀树 / 字典树 / 单词查找树)
- 二分
- 记忆化搜索
- 比特位
- 前缀和
滑动窗、双指针(三指针、快慢指针)
滑动窗口
小技巧:用哈希表或者数组统计不同字母的个数,这样可以保证复杂度最低,
- 数组
1、int[] window = new int[26];
一般下标0~25表示a–z;
2、int[] window = new int[128];
一般下标0~127表示各种字符(英文字母、数字、符号和空格)
模板:
- 移动右指针用while或for
1、while ( r < length)
2、for (int r = 0,; r < length; r++)
一般代码逻辑,先判断移动右边界,再移动左边界:
- 窗口移动代码
窗口扩张:left不变,right++
窗口缩减:right不变,left++
窗口滑动:left++,right++
3 无重复字符的最长子串
总的思路:
维护一个符合题目要求的窗口,
窗口里字符串不重复,左指针i
指向不重复字符的左边前面一个
,右指针j
指向不重复字符串的右边
;
- 遍历到一个新字符,
若没出现过
,则向右移动右指针j,并计算更新长度; - 若
出现过,则更新左指针i
,使新字符在(i, j]里只出现一次,再计算更新长度;
windows数组比map更快
class Solution {
public int lengthOfLongestSubstring(String s) {
int[] windows = new int[128];
Arrays.fill(windows , -1);
int l = -1;
int r = 0;
int res = 0;
while(r < s.length()){
char c = s.charAt(r);
if(windows[c] == -1){
res = Math.max(res , r - l);
windows[c] = r;
r++;
}
else{
l = Math.max(l , windows[c]);
res = Math.max(res , r - l);
windows[c] = r;
r++;
}
}
return res;
}
}
class Solution {
public int lengthOfLongestSubstring(String s) {
if(s.length() <= 1) return s.length();
HashMap<Character , Integer> map = new HashMap<>();
int i = -1;
int res = 0;
for(int j =0 ; j < s.length() ; j++){
if(!map.containsKey(s.charAt(j))){
map.put(s.charAt(j) , j);
}
else{
i = Math.max(i , map.get(s.charAt(j)));
map.put(s.charAt(j) , j);
}
res = Math.max(res , j - i);
}
return res;
}
}
76. 最小覆盖子串
参考:https://leetcode-cn.com/problems/minimum-window-substring/solution/zui-xiao-fu-gai-zi-chuan-by-leetcode-solution/
//先递增右指针r,当[l,r]子串包含t所有字符时,停止递增右指针,然后回缩左指针,判断是否有更小的子串符合要求;
//停止递增右指针是因为:我们是找符合要求的最小子串,此时我们找到一个以l开头r结尾的符合要求的子串,那么以l为起点更长的子串就不用看了,
//因为我们求的是最小子串,只需在缩减左边界判断;
class Solution {
HashMap<Character , Integer> tMap =new HashMap<>();
HashMap<Character , Integer> sMap = new HashMap<>();
public String minWindow(String s, String t) {
int tLen = t.length();
int sLen = s.length();
for(int i = 0 ; i < tLen ; i++){
char c = t.charAt(i);
tMap.put(c , tMap.getOrDefault(c , 0) + 1);
}
int l = 0;
int r = 0;//while递增右指针时,若int right = -1,先递增右指针r++;若int right = 0,while结束再递增右指针r++;
int resL = -1;
int resR = -1;
int minL = Integer.MAX_VALUE;
while(r < sLen){//递增右指针
char c1 = s.charAt(r);
if(tMap.containsKey(c1)) sMap.put(c1 , sMap.getOrDefault(c1 , 0) + 1);
while(check() && l <= r){//回缩左指针
if((r - l + 1) < minL){
minL = r - l + 1;
resL = l;
resR = r;
}
char c2 = s.charAt(l);
if(tMap.containsKey(c2)) sMap.put(c2 , sMap.getOrDefault(c2 , 0) - 1);
l++;
}
r++;
}
return resL == -1 ? "" : s.substring(resL , resR + 1);
}
boolean check(){
Set<Character> set = tMap.keySet();
for(Character c : set){
if(sMap.getOrDefault(c , 0) < tMap.get(c)) return false;
}
return true;
}
}
438. 找到字符串中所有字母异位词
[参考】(https://leetcode-cn.com/problems/find-all-anagrams-in-a-string/solution/438-zhao-dao-zi-fu-chuan-zhong-suo-you-z-nx6b/)
class Solution {
public List<Integer> findAnagrams(String s, String p) {
int sLength = s.length();
int pLength = p.length();
ArrayList<Integer> res = new ArrayList<>();
if(sLength < pLength) return res;
int[] sNum = new int[26];
int[] pNum = new int[26];
for(int i = 0 ; i< pLength ; i++){
sNum[s.charAt(i) - 'a']++;
pNum[p.charAt(i) - 'a']++;
}
if(Arrays.equals(sNum , pNum)) res.add(0);
for(int i = pLength ; i < sLength ; i++){
sNum[s.charAt(i - pLength) - 'a']--;
sNum[s.charAt(i) - 'a']++;
if(Arrays.equals(sNum , pNum)) res.add(i - pLength + 1);
}
return res;
}
}
567. 字符串的排列
class Solution {
public boolean checkInclusion(String s1, String s2) {
int len1 = s1.length();
int len2 = s2.length();
if(len1 > len2) return false;
int[] s1Num = new int[128];
int[] s2Num = new int[128];
for(int i = 0 ; i < len1 ; i++){
s1Num[s1.charAt(i)]++;
s2Num[s2.charAt(i)]++;
}
if(Arrays.equals(s1Num , s2Num)) return true;
for(int i = len1 ; i < len2 ; i++){
s2Num[s2.charAt(i)]++;
s2Num[s2.charAt(i - len1)]--;
if(Arrays.equals(s1Num , s2Num)) return true;
}
return false;
}
}
424. 替换后的最长重复字符
【参考】https://leetcode-cn.com/problems/longest-repeating-character-replacement/solution/tong-guo-ci-ti-liao-jie-yi-xia-shi-yao-shi-hua-don/
- 一个子串替换后是重复字符串的条件:
如果当前字符串中的出现次数最多的字母个数 +K 大于串长度,那么这个串就是满足条件的;- right - left在整个过程是非递减的。只要right 的值加进去不满足条件,left和right就一起右滑,因为长度小于right - left的区间就没必要考虑了,所以right - left一直保持为当前的最大值
class Solution {
public int characterReplacement(String s, int k) {
int length = s.length();
if(length == 0) return 0;
if(length <= k) return length;
int[] window = new int[26];
int l = 0;
int r = 0;
int max = 0;
while(r < length){
int index = s.charAt(r) - 'A';
window[index]++;
max = Math.max(max , window[index]);
//满足条件,r++
if(max + k >= r - l + 1) r++;
else{
index = s.charAt(l) - 'A';
window[index]--;
//不满足条件,left和right就一起右滑,保持right - left在整个过程是非递减的;
l++;
r++;
}
}
return r - l;
}
}
239. 滑动窗口最大值
方法一:【优先队列(堆)】(https://leetcode-cn.com/problems/sliding-window-maximum/solution/hua-dong-chuang-kou-zui-da-zhi-by-leetco-ki6m/)
class Solution {
public int[] maxSlidingWindow(int[] nums, int k) {
int length = nums.length;
if(length == 0 || k <= 0 || k > length) return new int[0];
int[] res = new int[length - k + 1];
PriorityQueue<int[]> pq = new PriorityQueue<>(new Comparator<int[]>(){
public int compare(int[] arr1 , int[] arr2){
return arr1[0] != arr2[0] ? arr2[0] - arr1[0] : arr2[1] - arr1[1];
}
});
for(int i = 0 ; i < k ; i++) pq.add(new int[]{nums[i] , i});
res[0] = pq.peek()[0];
for(int i = k ; i < length ; i++){
pq.add(new int[]{nums[i] , i});
while(i - pq.peek()[1] >= k) pq.poll();
res[i - k + 1] = pq.peek()[0];
}
return res;
}
}
方法二:参考 [面试题 59 - I. 滑动窗口的最大值】(https://blog.youkuaiyun.com/qq_42647047/article/details/112931422)
class Solution {
public int[] maxSlidingWindow(int[] nums, int k) {
int length = nums.length;
if(length == 0 || k <= 0 || k > length) return new int[0];
int[] res = new int[length - k + 1];
Deque<Integer> dq = new LinkedList<>();
for(int i = 0 ; i < k ; i++){
if(dq.isEmpty()) dq.addLast(i);
else{
while(!dq.isEmpty() && nums[i] > nums[dq.peekLast()]) dq.removeLast();
dq.addLast(i);
}
}
res[0] = nums[dq.peekFirst()];
for(int i = k ; i < length ; i++){
while(!dq.isEmpty() && nums[i] > nums[dq.peekLast()]) dq.removeLast();
dq.addLast(i);
while(i - dq.peekFirst() >= k) dq.removeFirst();
res[i - k + 1] = nums[dq.peekFirst()];
}
return res;
}
}
209. 长度最小的子数组
class Solution {
public int minSubArrayLen(int target, int[] nums) {
int l = 0;
int r = 0;
int res = nums.length + 1;
int sum = 0;
while(r < nums.length){
sum += nums[r];
if(sum < target) r++;
else{
res = Math.min(res , r -l + 1);
sum -= nums[l];
sum -= nums[r];
l++;
}
}
return res == nums.length + 1? 0 : res;
}
}
151. 翻转字符串里的单词
参考 面试题58 - I 翻转单词顺序https://blog.youkuaiyun.com/qq_42647047/article/details/112181978
- 倒序遍历字符串 s ,记录单词左右索引边界 l , r ;
- 每确定一个单词的边界,则将其添加至单词列表 res ;
- 最终,将单词列表拼接为字符串,并返回即可。
class Solution {
public String reverseWords(String s) {
s = s.trim();
int l = s.length() - 1;
int r = s.length() - 1;
StringBuilder sb = new StringBuilder();
while(l >= 0){
char c = s.charAt(l);
if(c != ' '){
if(l == 0) sb.append(s.substring(l , r + 1));
l--;
}else{
sb.append(s.substring(l + 1 , r + 1));
sb.append(" ");
while(l >= 0 && s.charAt(l) == ' ') l--;
r = l;
}
}
return sb.toString();
}
}
双指针(三指针、快慢指针)
双指针的题一般都能用暴力解法做,所以暴力解法超时(并且是数组型数据),可以考虑用双指针优化;
- 双指针的一次遍历O(n),可以优化暴力解法的后两次遍历O(n^2);
- 一般l = 0 ; r = length - 1;【即,左右指针指向两端】
- 每一次移动哪个指针,需要根据题意决定【关键】;
15. 三数之和
参考](https://leetcode-cn.com/problems/3sum/solution/3sumpai-xu-shuang-zhi-zhen-yi-dong-by-jyd/)
通过双指针,优化暴力解法(三层循环):
具体是:双指针的一次遍历O(n),优化暴力解法的后两次遍历O(n^2)
class Solution {
public List<List<Integer>> threeSum(int[] nums) {
ArrayList<List<Integer>> res = new ArrayList<List<Integer>>();
if(nums.length < 3) return res;
Arrays.sort(nums);
for(int i = 0 ; i < nums.length -2 ; i++){
//剪枝1;
if(nums[i] > 0) break;
//剪枝2;
//如果下一轮的值与上一轮的值相等,这一轮搜索就可以跳过。
//理由也很简单,因为这一轮与上一轮相比,候选元素少了一个,可能的结果在包含在上一轮已经搜索得到的结果中;
if(i > 0 && nums[i] == nums[i - 1]) continue;
int l = i + 1;
int r = nums.length - 1;
while(l < r){
if((nums[i] + nums[l] + nums[r]) > 0){
//剪枝3;
while(l < r && nums[r] == nums[r - 1]) r--;
r--;
}
else if((nums[i] + nums[l] + nums[r]) < 0){
//剪枝3;
while(l < r && nums[l] == nums[l + 1]) l++;
l++;
}
else{
ArrayList<Integer> path = new ArrayList<>();
path.add(nums[i]);
path.add(nums[l]);
path.add(nums[r]);
res.add(path);
//剪枝3;
while(l < r && nums[l] == nums[++l]);
while(l < r && nums[r] == nums[--r]);
}
}
}
return res;
}
}
16. 最接近的三数之和
和上题一样,用双指针的一次遍历O(n),优化暴力解法的后两次遍历O(n^2),
然后计算每一个三数和,同时更新最小差值,和对应的三数和
【参考】https://leetcode-cn.com/problems/3sum-closest/solution/zui-jie-jin-de-san-shu-zhi-he-by-leetcode-solution/
class Solution {
public int threeSumClosest(int[] nums, int target) {
Arrays.sort(nums);
int length = nums.length;
int diff = Integer.MAX_VALUE;
int res = 0;
for(int i = 0 ; i < length ; i++){
if(i > 0 && nums[i] == nums[i - 1]) continue;
int l = i + 1;
int r = length - 1;
int sum = 0;
while(l < r){
sum = nums[i] + nums[l] + nums[r];
if(Math.abs(sum - target) < diff){
diff = Math.abs(sum - target);
res = sum;
}
if(sum > target){
//剪枝
while(l < r && nums[r] == nums[r - 1]) r--;
r--;
}
else if(sum < target){
//剪枝
while(l < r && nums[l] == nums[l + 1]) l++;
l++;
}
else{
return sum;
}
}
}
return res;
}
}
167. 两数之和 II - 输入有序数组
class Solution {
public int[] twoSum(int[] numbers, int target) {
int[] res = new int[2];
if(numbers.length == 0) return res;
int l = 0;
int r = numbers.length - 1;
int sum = 0;
while(l < r){
sum = numbers[l] + numbers[r];
if(sum > target){
while(l < r && numbers[r] == numbers[r - 1]) r--;
r--;
}else if(sum < target){
while(l < r && numbers[l] == numbers[l + 1]) l++;
l++;
}else{
res[0] = l + 1;
res[1] = r + 1;
return res;
}
}
return res;
}
}
11. 盛最多水的容器
参考](https://leetcode-cn.com/problems/container-with-most-water/solution/container-with-most-water-shuang-zhi-zhen-fa-yi-do/)
class Solution {
public int maxArea(int[] height) {
int length = height.length;
if(length < 2) return 0;
int l = 0 ;
int r = length - 1;
int maxA = 0;
while(l < r){
int area = (r - l) * Math.min(height[l] , height[r]);
maxA = Math.max(maxA , area);
if(height[l] < height[r]) l++;
else r--;
}
return maxA;
}
}
42. 接雨水
每个柱子的雨水高度,取决于两边柱子的最大值left_max、right_max中的小的;
而每个柱子两边的min{left_max,right_max},可以通过双指针求出,即:
哪边小更新哪边的最大值,这样可以保证,height[left]和height[right]值小的一边,对应的最大高度也是较小的一个,即积水高度依赖的一边的最大高度;
参考](https://leetcode-cn.com/problems/trapping-rain-water/solution/jie-yu-shui-by-leetcode/)
public int trap(int[] height) {
int left = 0, right = height.length - 1;
int ans = 0;
int left_max = 0, right_max = 0;
while (left < right) {
//雨水的高度取决于矮的一边;
//这里是哪边小更新哪边的最大值,这样可以保证,height[left]和height[right]值小的一边,对应的最大高度也是较小的一个,即积水高度依赖的一边的最大高度;
if (height[left] < height[right]) {
if (height[left] >= left_max) {
left_max = height[left];
} else {
ans += (left_max - height[left]);
}
++left;
} else {
if (height[right] >= right_max) {
right_max = height[right];
} else {
ans += (right_max - height[right]);
}
--right;
}
}
return ans;
}
19. 删除链表的倒数第 N 个结点
参考](https://leetcode-cn.com/problems/remove-nth-node-from-end-of-list/solution/dong-hua-tu-jie-leetcode-di-19-hao-wen-ti-shan-chu/)
/**
* 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();
dummyHead.next = head;
ListNode l = dummyHead;
ListNode r = dummyHead;
int step = n + 1;
for(int i = 0 ; i < step ; i++) r = r.next;
while(r != null){
l = l.next;
r = r.next;
}
ListNode temp = l.next;
l.next = temp.next;
return dummyHead.next;
}
}
283. 移动零
参考](https://leetcode-cn.com/problems/move-zeroes/solution/yi-dong-ling-by-leetcode-solution/)
- 左指针left 表示下一个不为零的元素所在的位置;
- 右指针right找下一个非零值,不是零就交换,是零就继续遍历r++;
class Solution {
public void moveZeroes(int[] nums) {
if(nums.length <= 1) return;
int left = 0;
int right = 0;
while(right < nums.length){
if(nums[right] != 0){
swap(nums , left , right);
left++;
right++;
}
else right++;
}
return;
}
void swap(int[] nums , int left , int right){
int temp = nums[left];
nums[left] = nums[right];
nums[right] = temp;
}
}
647. 回文子串
参考,中心扩散法(更好),还有dp方法(https://blog.youkuaiyun.com/qq_42647047/article/details/108652634)
class Solution {
public int countSubstrings(String s) {
int res = 0;
for(int i = 0 ; i < s.length() - 1 ; i++){
int count1 = expandAroud(s , i , i);
int count2 =expandAroud(s , i , i + 1);
res += (count1 + count2);
}
return res + 1;
}
int expandAroud(String s , int left , int right){
int count = 0;
while(left >=0 && right < s.length()){
if(s.charAt(left) == s.charAt(right)){
count++;
left--;
right++;
}else break;
}
return count;
}
}
26. 删除有序数组中的重复项
pos:下一个不重复元素应该放在的位置;
l、r寻找不重复元素的双指针,l指向的为最终的不重复元素;
class Solution {
public int removeDuplicates(int[] nums) {
int len = nums.length;
if(len == 0) return 0;
int pos = 0;
int l = 0;
int r = 0;
while(r < len){
if(nums[l] == nums[r]) r++;
else{
swap(nums , pos , l);
l = r;
pos++;
}
}
swap(nums , pos , l);
return pos + 1;
}
void swap(int[] nums , int a ,int b){
int temp = nums[a];
nums[a] = nums[b];
nums[b] = temp;
}
}
159. 至多包含两个不同字符的最长子串
给定一个字符串 s ,找出 至多 包含两个不同字符的最长子串 t 。
示例 1:
输入: “eceba”
输出: 3
解释: t 是 “ece”,长度为3。
示例 2:
输入: “ccaabbb”
输出: 5
解释: t 是 “aabbb”,长度为5。
思路:双指针,指针中间的是可能的答案。符合要求右指针向右扩,否则更新答案,左指针往右缩。
class Solution {
public int lengthOfLongestSubstringTwoDistinct(String s) {
int n = s.length();
if (n < 3) return n;
int left = 0;
int right = 0;
//K-V:K是对应字符,V是最后一次出现的位置。
HashMap<Character, Integer> hashmap = new HashMap<Character, Integer>();
int max_len = 2;
while (right < n) {
//符合要求就继续向右扩
if (hashmap.size() < 3){
hashmap.put(s.charAt(right), right++);
}
if (hashmap.size() == 3) {
int index = Collections.min(hashmap.values());
hashmap.remove(s.charAt(index));
left = index + 1;
}
max_len = Math.max(max_len, right - left);
}
return max_len;
}
}
逆向双指针
88. 合并两个有序数组
- 思路的重点一个是
从后往前
确定两组中该用哪个数字 - 另一个是
结束条件以第二个数组nums2全都插入进去为止
;
class Solution {
public void merge(int[] nums1, int m, int[] nums2, int n) {
int i = m - 1;
int j = n - 1;
int pos = m + n - 1;
for( ; i >= 0 && j >=0 ; pos--){
if(nums1[i] >= nums2[j]){
nums1[pos] = nums1[i];
i--;
}else{
nums1[pos] = nums2[j];
j--;
}
}
//因为是添加在nums1上,所以,如果i没遍历完,则下一句不加也行,因为本来就是添加在nums1上。
//结束条件以第二个数组nums2全都插入进去为止
//while(i >= 0) nums1[pos--] = nums1[i--];
while(j >= 0) nums1[pos--] = nums2[j--];
return;
}
}
回溯
-
有的回溯题能画出树状图(这样就容易使用DFS理解,并解答),画出树状图的关键是确定根节点向各子节点(子节点有可能是两个,即二叉树,也有可能是多个)分叉的条件(比如:22题:使用左括号,形成左子树,使用右括号,形成右子树;39题:当前下标元素不用,形成左子树,当前下标元素用,形成右子树)。在编写代码时,一般是二叉树的,可以用分叉条件+前序遍历的方式来编写代码;若一个节点有多个子节点,一般使用for循环的方式处理各dfs()。
-
编码经验:
编码前要确定两件事:
1、画出树状图:每一层dfs里的for循环,对应遍历树状图中的一层,至于从上到下遍历和回溯就自动完成了。
2、确定回溯的状态变量(即,dfs里的参数,也就是每次回溯需要改变的参数):
组合------------int start,选择列表,也代表回溯到数组的哪一个元素,每层dfs的for循环从start开始;
全排列--------boolean[] used = new boolean[len]; 标识选择列表;
17 电话号码的字母组合--------int index ,和start一样,索引,代表回溯到数组的哪一个元素;
22括号生成----------int left , int right 左右括号数; -
【回溯讲解及练习例题一】(https://leetcode-cn.com/problems/permutations/solution/hui-su-suan-fa-python-dai-ma-java-dai-ma-by-liweiw/)
-
【回溯讲解及练习例题二】(https://leetcode-cn.com/problems/subsets/solution/c-zong-jie-liao-hui-su-wen-ti-lei-xing-dai-ni-gao-/)
字符串中的回溯问题
17. 电话号码的字母组合
参考](https://leetcode-cn.com/problems/letter-combinations-of-a-phone-number/solution/dian-hua-hao-ma-de-zi-mu-zu-he-by-leetcode-solutio/)
模式识别
- 关键字:
所有组合
首先想到穷举,需要搜索算法-----回溯
- 向上的箭头代表回溯
回溯(用StringBuilder,最后需要撤销,即回溯)
编码经验:
每一层dfs里的for循环,对应遍历树状图中的一层,至于从上到下遍历和回溯就自动完成了。
class Solution {
HashMap<Character , String> map;
public List<String> letterCombinations(String digits) {
List<String> res = new ArrayList<>();
if(digits.length() == 0) return res;
StringBuilder sb = new StringBuilder();
map = new HashMap<>();
map.put('2' , "abc");
map.put('3' , "def");
map.put('4' , "ghi");
map.put('5' , "jkl");
map.put('6' , "mno");
map.put('7' , "pqrs");
map.put('8' , "tuv");
map.put('9' , "wxyz");
dfs(digits , 0 , sb , res);
return res;
}
void dfs(String digits , int index , StringBuilder sb , List<String> res){
if(sb.length() == digits.length()){
res.add(sb.toString());
return;
}
String tempStr = map.get(digits.charAt(index));
for(int i = 0 ; i < tempStr.length() ; i++){
sb.append(tempStr.charAt(i));
dfs(digits , index + 1 , sb , res);
sb.deleteCharAt(sb.length() - 1);
}
return;
}
}
DFS(用String,最后不需要撤销,即DFS)
class Solution {
ArrayList<String> res;
HashMap<Character , String> map;
public List<String> letterCombinations(String digits) {
res = new ArrayList<>();
if(digits.length() == 0) return res;
map = new HashMap<>();
map.put('2' , "abc");
map.put('3' , "def");
map.put('4' , "ghi");
map.put('5' , "jkl");
map.put('6' , "mno");
map.put('7' , "pqrs");
map.put('8' , "tuv");
map.put('9' , "wxyz");
//StringBuffer tempCombination = new StringBuffer();
String tempCombination = "";
dfs(digits , 0 , tempCombination);
return res;
}
void dfs(String digits , int index , String tempCombination){
if(index == digits.length()) res.add(tempCombination);
else{
char letter = digits.charAt(index);
String s = map.get(letter);
for(int i = 0 ; i < s.length() ; i++){
dfs(digits , index + 1 , tempCombination + s.charAt(i));
//tempCombination.deleteCharAt(index);
}
}
}
}
注:
由上可看出,回溯是使用DFS工具的一个算法,即回溯=DFS+撤销(剪枝)
;
用StringBulider是回溯;用String是DFS的原因
- 【利用String的不可变特性,不用处理,StringBulider的可变特性,得处理】
1、
StringBuilder对象是可变特性,所以元素传入的都是同一个对象,所以在递归完成之后必须撤回上一次的操作,需要删除上一次添加的字符。而String是不可变特性的,每次改变之后,元素传入的都是不同的对象。故无需撤销操作。
比如:
用StringBuilder时,如果不删除,在for循环里,下一次运行时,tempCombination数据就被污染了。(ad,ae)变成(ad,ade)。
流程是:
a进入StringBuilder对象,b接着进入,由于StringBuilder的可变性,下一次的e还是添加在同一个StringBuilder对象,出错;所以在完成一次搜索后要删除上一个字符,作为回溯。
用String时,可理解为递归函数返回时,自动回溯。
用StringBulider和String所占资源对比
由于String的不可变性,每次拼接操作会创建一个新的String对象,会占相对较多资源;而StringBulider是可变的,每次操作的是同一个StringBulider对象,不需要创建多个对象,会占相对较少资源。从下面两种方法的空间复杂度可看出:
22. 括号生成
画出树
- 代码一
left,right参数的加1操作,放在dfs外
,所以对应dfs后需再减1
class Solution {
public List<String> generateParenthesis(int n) {
List<String> res = new ArrayList<>();
StringBuilder sb = new StringBuilder();
int left = 0;//左括号数
int right = 0;//右括号数
dfs(n , left , right , sb , res);
return res;
}
void dfs(int n , int left , int right , StringBuilder sb , List<String> res){
//剪枝很重要,不然超时
if(left < right || left > n || right > n) return;
if(sb.length() == 2*n && left == right ){
res.add(sb.toString());
return;
}
//使用for循环控制树中遍历的方向,i=0,向左括号遍历,i=1,向右括号遍历;
for(int i = 0 ; i < 2 ; i++){
if(i == 0){
sb.append('(');
left++;
dfs(n , left , right , sb , res);
sb.deleteCharAt(sb.length() - 1);
left--;
}else{
sb.append(')');
right++;
dfs(n , left , right , sb , res);
sb.deleteCharAt(sb.length() - 1);
right--;
}
}
return;
}
}
- 代码二
lCount,rCount参数的加1操作,放在dfs内
,所以对应dfs后不需再减1
class Solution {
public List<String> generateParenthesis(int n) {
List<String> res = new ArrayList<>();
StringBuilder sb = new StringBuilder();
int lCount = 0;
int rCount = 0;
dfs(res , sb , lCount , rCount , n);
return res;
}
void dfs(List<String> res , StringBuilder sb , int lCount , int rCount , int n){
if(lCount < rCount || lCount > n || rCount > n) return;
if(sb.length() == 2*n && lCount == rCount){
res.add(sb.toString());
return;
}
sb.append('(');
dfs(res , sb , lCount + 1 , rCount , n);
sb.deleteCharAt(sb.length() - 1);
sb.append(')');
dfs(res , sb , lCount , rCount + 1 , n);
sb.deleteCharAt(sb.length() - 1);
}
}
【原来的题解,推荐上面的,符合解题规律】
参考](https://leetcode-cn.com/problems/generate-parentheses/solution/hui-su-suan-fa-by-liweiwei1419/)
这一类问题是在一棵隐式的树
上求解,可以用深度优先遍历,也可以用广度优先遍历。一般用深度优先遍历。
-
该题刻画出树
-
树出来了,由于树的DFS是树的前序遍历,所以用前序遍历即可。
class Solution {
ArrayList<String> res;
public List<String> generateParenthesis(int n) {
res = new ArrayList<>();
if(n == 0) return res;
StringBuilder sb = new StringBuilder();
int left = n;
int right = n;
dfs(sb , left , right);
return res;
}
void dfs(StringBuilder sb , int left , int right){
if(left < 0 || left > right) return;//剪枝--回溯
if(left == 0 && right ==0){
res.add(sb.toString());
return;
}//正常到叶节点返回
dfs(sb.append('(') , left - 1 , right);
sb.deleteCharAt(sb.length() - 1);
dfs(sb.append(')') , left , right - 1);
sb.deleteCharAt(sb.length() - 1);
}
}
301. 删除无效的括号
题解:
- 所有的「括号匹配」问题,可能会用到的一个重要性质是:
如果当前遍历到的左括号的数目严格小于右括号的数目则表达式无效。
我们利用了这条性质,在「回溯算法」上「剪枝」。
- 由于题目要求我们将
所有
(最长)合法方案输出,因此不可能有别的优化,只能进行「爆搜」。 - 要删除无效的括号,首先要知道删除左右括号的数量。
【参考】(https://leetcode-cn.com/problems/remove-invalid-parentheses/solution/shan-chu-wu-xiao-de-gua-hao-by-leetcode/)
class Solution {
public List<String> removeInvalidParentheses(String s) {
ArrayList<String> res = new ArrayList<>();
HashSet<String> validRes = new HashSet<>();
if(s == "") return res;
char[] c = s.toCharArray();
int leftRemove = 0;
int rightRemove = 0;
for(char val : c){
if(val == '(') leftRemove++;
if(val == ')'){
if(leftRemove > 0) leftRemove--;
else rightRemove++;
}
}
StringBuilder path = new StringBuilder();
dfs(0 , 0 , 0 , leftRemove , rightRemove , s , path , validRes);
for(String str : validRes){
res.add(str);
}
return res;
}
void dfs(int index , int leftCount , int rightCount , int leftRemove , int rightRemove , String s , StringBuilder path , HashSet<String> validRes){
if(leftCount < rightCount) return;
if(index == s.length()){
if(leftRemove == 0 && rightRemove==0) validRes.add(path.toString());
return;
}
char c = s.charAt(index);
if(c == '(' && leftRemove > 0) dfs(index + 1 , leftCount , rightCount ,leftRemove - 1 , rightRemove , s , path , validRes);
if(c == ')' && rightRemove > 0) dfs(index + 1 , leftCount , rightCount , leftRemove , rightRemove - 1 , s , path , validRes);
path.append(c);
if(c != '(' && c != ')') dfs(index + 1 , leftCount , rightCount , leftRemove , rightRemove , s , path , validRes);
else if(c == '(') dfs(index + 1 , leftCount + 1 , rightCount , leftRemove , rightRemove , s , path , validRes);
else if(c == ')') dfs(index + 1 , leftCount , rightCount + 1 , leftRemove , rightRemove , s , path , validRes);
path.deleteCharAt(path.length() - 1);
}
}
子集、组合
总结:
- 子集、组合类问题(不在乎元素顺序),不同子集/组合间因为是不能重复的,所以关键是用一个
start 参数
来控制选择列表
!!并且一个子集/组合不必用完所有元素。- 在需要剪枝的情况下,排序是很有必要的,因为会易于剪枝;
- 判重:首先对题目中给出的nums数组排序,让重复的元素并列排在一起,然后
if(i>start && nums[i]==nums[i-1])
,
78. 子集
- 上面的画全排列树状图过程是: 以
[1, 2,3]
为例,
1、先空列表;
2、然后有一个元素的列表;
3、然后两个元素的列表,在一个元素列表的基础上按在数组中的顺序添加一个元素变成两个元素的列表(注:为了不重复,添加的元素只能是当前元素后面的元素
);
4、以此类推; - 因为是找子集,且
每个节点都是一个子集
,所以每个节点都需加入结果
为了不重复
,添加的元素只能是当前元素后面的元素,这需要使用一个参数start
,来标识当前的选择列表
(当前元素后面的元素)的起始位置。也就是标识每一层的状态,因此被形象的称为"状态变量"。
class Solution {
public List<List<Integer>> subsets(int[] nums) {
int len = nums.length;
List<List<Integer>> res = new ArrayList<>();
List<Integer> path = new ArrayList<>();
if(len == 0) return res;
dfs(nums , 0 , path , res);
return res;
}
void dfs(int[] nums , int start , List<Integer> path , List<List<Integer>> res){
//这一句放在dfs的第一句,即,遍历一个节点,就加入res;
res.add(new ArrayList<>(path));
//下一句终止条件可不写,因为当 start 参数越过数组边界的时候,程序就自己跳过下一层递归了。
if(start == nums.length) return;
//start 参数来控制选择列表
for(int i = start ; i < nums.length ; i++){
path.add(nums[i]);
dfs(nums , i + 1 , path , res);
path.remove(path.size() - 1);
}
}
}
90. 子集 II
- 先排序,这样相同的数会相邻,易于剪枝。【在需要剪枝的情况下,排序是很有必要的,因为会易于剪枝】
class Solution {
public List<List<Integer>> subsetsWithDup(int[] nums) {
int len = nums.length;
List<List<Integer>> res = new ArrayList<>();
List<Integer> path = new ArrayList<>();
if(len == 0) return res;
Arrays.sort(nums);
dfs(nums , 0 , path , res);
return res;
}
void dfs(int[] nums , int start , List<Integer> path , List<List<Integer>> res){
//这一句放在dfs的第一句,即,遍历一个节点,就加入res;
res.add(new ArrayList<>(path));
//下一句终止条件可不写,因为当 start 参数越过数组边界的时候,程序就自己跳过下一层递归了。
if(start == nums.length) return;
//start 参数来控制选择列表
for(int i = start ; i < nums.length ; i++){
if(i > start && nums[i] == nums[i - 1]) continue;
path.add(nums[i]);
dfs(nums , i + 1 , path , res);
path.remove(path.size() - 1);
}
}
}
- 先排序,这样加上一个数大于target,后面的数更大,更会大于target,易于剪枝。
- 因为一个数可重复选择,所以当前选择列表的起始索引start和上一层的起始索引相等,不能加一(加1是不可重复的情况)即,
dfs(candidates , i , path , res , target, sum + candidates[i]);
77. 组合
class Solution {
public List<List<Integer>> combine(int n, int k) {
List<List<Integer>> res = new ArrayList<>();
ArrayList<Integer> path = new ArrayList<>();
if(n < k) return res;
dfs(n , k , 1 , path , res);
return res;
}
void dfs(int n , int k , int start , ArrayList<Integer> path , List<List<Integer>> res){
if(path.size() == k){
res.add(new ArrayList<Integer>(path));
return;
}
for(int i = start ; i <= n ; i++){
path.add(i);
dfs(n , k , i + 1 , path , res);
path.remove(path.size() - 1);
}
}
}
39. 组合总和
class Solution {
public List<List<Integer>> combinationSum(int[] candidates, int target) {
int len = candidates.length;
List<List<Integer>> res = new ArrayList<>();
List<Integer> path = new ArrayList<>();
if(len == 0) return res;
Arrays.sort(candidates);
dfs(candidates , 0 , path , res , target , 0);
return res;
}
void dfs(int[] candidates , int start , List<Integer> path , List<List<Integer>>res , int target , int sum){
if(target == sum){
res.add(new ArrayList<>(path));
return;
}
//start 参数来控制选择列表
for(int i = start ; i < candidates.length ; i++){
sum += candidates[i];
if(sum > target) return;//剪枝放在sum += candidates[i];后
path.add(candidates[i]);
//因为一个数可重复选择,所以当前选择列表的起始索引start和上一层的起始索引相等,不能加一(加1是不可重复的情况)
dfs(candidates , i , path , res , target , sum);
sum -= candidates[i];
path.remove(path.size() - 1);
}
}
}
- 或画成二叉树形式
根据当前下标元素用不用(即,跳不跳过)可画出二叉树,从而用DFS解
参考
class Solution {
List<List<Integer>> res;
List<Integer> temp;
public List<List<Integer>> combinationSum(int[] candidates, int target) {
res = new ArrayList<List<Integer>>();
temp = new ArrayList<>();
int length = candidates.length;
if(length == 0) return res;
dfs(candidates , 0 , target);
return res;
}
void dfs(int[] candidates , int id , int target){
if(id == candidates.length) return;//这里是走左边“跳过的路径”,并没有添加元素,所以不用执行remove操作
if(target == 0){
res.add(new ArrayList<Integer>(temp));
return;
}
dfs(candidates , id + 1 , target);
if(target - candidates[id] >=0){
temp.add(candidates[id]);
dfs(candidates , id , target - candidates[id]);
temp.remove(temp.size() - 1);
}
}
}
40. 组合总和 II
class Solution {
public List<List<Integer>> combinationSum2(int[] candidates, int target) {
List<List<Integer>> res = new ArrayList<>();
ArrayList<Integer> path = new ArrayList<>();
Arrays.sort(candidates);
int sum = 0;
dfs(candidates , 0 , path , res , target , sum);
return res;
}
void dfs(int[] candidates , int start , ArrayList<Integer> path , List<List<Integer>> res , int target , int sum){
if(sum == target){
res.add(new ArrayList<Integer>(path));
return;
}
for(int i = start ; i < candidates.length ; i++){
if(sum > target) break;
if(i > start && candidates[i] == candidates[i-1]) continue;
path.add(candidates[i]);
dfs(candidates , i + 1 , path , res , target , sum + candidates[i]);
path.remove(path.size() - 1);
}
}
}
784. 字母大小写全排列
注意剪枝:因为返回的字符串各元素位置不变,只是改变大小写,所以每一层的回溯,只回溯每层的第一个元素即可,即for循环只需一次,所以循环一次后直接break退出即可;
class Solution {
public List<String> letterCasePermutation(String s) {
List<String> res = new ArrayList<>();
StringBuilder str = new StringBuilder();
char[] c = s.toCharArray();
dfs(c , 0 , str , res);
return res;
}
void dfs(char[] c , int start , StringBuilder str , List<String> res){
if(str.length() == c.length){
res.add(str.toString());
return;
}
for(int i = start ; i < c.length ; i++){
//(下面直接break剪枝更好)剪枝,效率更高,如果start到c.length的长度小于当前还需要的字符数量,直接返回
// if(c.length-str.length()>c.length-deapth) return ;
if((c[i] >= 'a' && c[i] <= 'z') || (c[i] >= 'A' && c[i] <= 'Z')){
str.append(c[i]);
dfs(c , i + 1 , str , res);
str.deleteCharAt(str.length() - 1);
str.append((char)(c[i] ^ 32));
dfs(c , i + 1 , str , res);
str.deleteCharAt(str.length() - 1);
break;//剪枝:因为返回的字符串各元素位置不变,所以每一层的回溯,只回溯每层的第一个元素即可,即for循环只需一次,所以循环一次后直接break退出即可;
}
else{
str.append(c[i]);
dfs(c , i + 1 , str , res);
str.deleteCharAt(str.length() - 1);
break;
}
}
}
}
93. 复原 IP 地址
参考](https://leetcode-cn.com/problems/restore-ip-addresses/solution/hui-su-suan-fa-hua-tu-fen-xi-jian-zhi-tiao-jian-by/)
- 每一层dfs里的for循环:由于每个整数最大是255,即最大只有3位数字,所以每一层循环3次即可;
- 状态变量:
1、depth:由于ip 段最多就 4 个段,因此这棵三叉树最多 4 层;所以一个状态变量为层数depth;
2、start:每一层的IP从哪个字符开始选取,用start表示。
class Solution {
public List<String> restoreIpAddresses(String s) {
List<String> res = new ArrayList<>();
ArrayList<String> path = new ArrayList<>();
// ip 段最多就 4 个段,因此这棵三叉树最多 4 层;
//depth:记录回溯的层数;
//start:每一层的IP从哪个字符开始选取,用start表示
//int depth = 0;
//int start = 0;
dfs(s , 0 , 0 , path , res);
return res;
}
void dfs(String s , int start , int depth , ArrayList<String> path , List<String> res){
if(start == s.length() && depth == 4){//在遍历完s的同时,找到4段IP,才算成功;
res.add(String.join("." , path));
return;
}
for(int i = start ; i < start + 3 && i < s.length() ; i++){
//剪枝1:选取s[start : i]为第depth层IP,判断“剩余的字符数量”是否大于“还需要的最大字符数量”,如果大于,则剪枝;
if((s.length() - i - 1) > 3*(4-depth-1)) continue;
String temp = s.substring(start , i+1);
//剪枝2:isVilid():判断本次取得字符串是否可以是IP中的一段
if(isVilid(temp)){
path.add(temp);
dfs(s , i + 1 , depth + 1 , path , res);
path.remove(path.size()-1);
}
}
}
boolean isVilid(String s){
int len = s.length();
//只有一个数0,才能有前导0,否则,无效;
if(len == 1 && s.charAt(0) == '0') return true;
//如果数字大于1位,第一个还是0,显然无效;
if(s.charAt(0) == '0') return false;
int val = Integer.parseInt(s);
if(val >= 0 && val <= 255) return true;
else return false;
}
}
131. 分割回文串
画出树状图后,发现其实是个组合问题,
只不过不是对每个字符组合,而是对每个子串进行组合,并且子串需要是回文串,否则剪枝;
class Solution {
public List<List<String>> partition(String s) {
List<List<String>> res = new ArrayList<>();
List<String> path = new ArrayList<>();
dfs(s , 0 , path , res);
return res;
}
void dfs(String s , int start , List<String> path , List<List<String>> res){
if(start == s.length()){
res.add(new ArrayList<String>(path));
return;
}
for(int i = start ; i < s.length() ; i++){
String temp = s.substring(start , i + 1);
if(!isHuiwen(temp)) continue;
path.add(temp);
//注意是i+1,不是start+1;
dfs(s , i + 1 , path , res);
path.remove(path.size() - 1);
}
}
boolean isHuiwen(String s) {
int left = 0;
int right = s.length() - 1;
char[] c = s.toCharArray();
while (left < right) {
if (c[left] != c[right]) {
return false;
}
left++;
right--;
}
return true;
}
}
全排列
46. 全排列
全排列:在乎顺序,元素可重复;即两个排列,元素相同,但顺序不同,也是不同的排列,所以不需要start 参数来控制选择列表,但一个排列必须用完所有元素,所以关键是引入一个
used数组
来记录使用过的数字
总结:
- 可以发现“排列”类型问题和“子集、组合”问题不同在于:“排列”问题使用used数组来
标识选择列表
,而“子集、组合”问题则使用start参数。另外还需注意两种问题的判重剪枝!!- 判重:首先对题目中给出的nums数组排序,让重复的元素并列排在一起,然后
if(i>0 && nums[i]==nums[i-1] && !used[i-1])
,修改
:子集组合问题有参数start
;排列有used数组
;将全排列的depth参数去掉,用path.size() == num.length判断,作为dfs递归结束条件
- 去掉depth参数
class Solution {
public List<List<Integer>> permute(int[] nums) {
int len = nums.length;
List<List<Integer>> res = new ArrayList<>();
if(len == 0) return res;
ArrayList<Integer> path = new ArrayList<>();
//这里使用标记数组的原因是:每一轮dfs都从同一个数组中选数,为了避免重复;手机号的不需要标记数组:
//是因为:每一个数字对应的字母不重复,即不用标记数组也能保证每一轮不会选取重复的字母。
boolean[] used = new boolean[len];
dfs(nums , len , path , used , res);
return res;
}
void dfs(int[]nums , int length , ArrayList<Integer>path , boolean[] used , List<List<Integer>>res){
if(path.size() == length){
res.add(new ArrayList<>(path));
return;
}
//使用for循环控制树向一个方向深度遍历
//选择列表,利用used[i]从给定的数中除去用过的,就是当前的选择列表
for(int i = 0 ; i < length ; i++){
if(used[i] == true) continue;
path.add(nums[i]);
used[i] = true;
dfs(nums , length , path , used , res);
used[i] = false;
path.remove(path.size() - 1);
}
}
}
- 有depth参数【不推荐】
class Solution {
public List<List<Integer>> permute(int[] nums) {
int len = nums.length;
List<List<Integer>> res = new ArrayList<>();
if(len == 0) return res;
ArrayList<Integer> path = new ArrayList<>();
//这里使用标记数组的原因是:每一轮dfs都从同一个数组中选数,为了避免重复;手机号的不需要标记数组:
//是因为:每一个数字对应的字母不重复,即不用标记数组也能保证每一轮不会选取重复的字母。
boolean[] used = new boolean[len];
dfs(nums , 0 , len , path , used , res);
return res;
}
void dfs(int[]nums , int depth , int length , ArrayList<Integer>path , boolean[] used , List<List<Integer>>res){
if(depth == length){
res.add(new ArrayList<>(path));
return;
}
//使用for循环控制树向一个方向深度遍历
//选择列表,利用used[i]从给定的数中除去用过的,就是当前的选择列表
for(int i = 0 ; i < length ; i++){
if(used[i] == true) continue;
path.add(nums[i]);
used[i] = true;
dfs(nums , depth + 1 , length , path , used , res);
used[i] = false;
path.remove(path.size() - 1);
}
}
}
47. 全排列 II
- 先排序,这样相同的数会相邻,易于剪枝。【在需要剪枝的情况下,排序是很有必要的,因为会易于剪枝】
- 有了前面“子集、组合”问题的判重经验,同样首先要对题目中给出的nums数组排序,让重复的元素并列排在一起,在
if(i>start&&nums[i]==nums[i-1])
,基础上修改为if(i>0&&nums[i]==nums[i-1]&&!used[i-1])
,语义为:当i可以选第一个元素之后的元素时(因为如果i=0,即只有一个元素,哪来的重复?有重复即说明起码有两个元素或以上,i>0),然后判断当前元素是否和上一个元素相同?如果相同,再判断上一个元素是否能用?如果三个条件都满足,那么该分支一定是重复的,应该剪去 - 下面代码的depth变量用于终止条件判断,可省略换成
if(path.size()==nums.length)
{
res.add(new ArrayList<>(path));
return;
}
- 去掉depth参数
class Solution {
public List<List<Integer>> permuteUnique(int[] nums) {
List<List<Integer>> res = new ArrayList<>();
int len = nums.length;
if(len == 0) return res;
Arrays.sort(nums);
List<Integer> path = new ArrayList<>();
boolean[] used = new boolean[len];
dfs(nums , path , used , res);
return res;
}
void dfs(int[] nums , List<Integer> path , boolean[] used , List<List<Integer>> res){
if(path.size() == nums.length){
res.add(new ArrayList<>(path));
return;
}
for(int i = 0 ; i < nums.length ; i++){
if(used[i] == false){
if(i > 0 && nums[i] == nums[i - 1] && !used[i - 1]) continue;
path.add(nums[i]);
used[i] = true;
dfs(nums , path , used , res);
used[i] = false;
path.remove(path.size() - 1);
}
}
}
}
- 有depth参数【不推荐】
class Solution {
public List<List<Integer>> permuteUnique(int[] nums) {
List<List<Integer>> res = new ArrayList<>();
int len = nums.length;
if(len == 0) return res;
Arrays.sort(nums);
List<Integer> path = new ArrayList<>();
boolean[] used = new boolean[len];
dfs(nums , 0 , path , used , res);
return res;
}
void dfs(int[] nums , int depth , List<Integer> path , boolean[] used , List<List<Integer>> res){
if(depth == nums.length){
res.add(new ArrayList<>(path));
return;
}
for(int i = 0 ; i < nums.length ; i++){
if(used[i] == false){
if(i > 0 && nums[i] == nums[i - 1] && !used[i - 1]) continue;
path.add(nums[i]);
used[i] = true;
dfs(nums , depth + 1 , path , used , res);
used[i] = false;
path.remove(path.size() - 1);
}
}
}
}
剑指 Offer 38. 字符串的排列
其实这题跟上面47题一模一样,换汤不换药,把nums数组换成了字符串,直接上最终代码,记得先用sort对字符串s进行排序,再传进来!
class Solution {
public String[] permutation(String s) {
ArrayList<String> res = new ArrayList<>();
char[] c = s.toCharArray();
int len = c.length;
if(len == 0) return new String[0];
Arrays.sort(c);
StringBuffer path = new StringBuffer();
boolean[] used = new boolean[len];
dfs(c , path , used , res);
return res.toArray(new String[res.size()]);
}
void dfs(char[] c , StringBuffer path , boolean[] used , ArrayList<String> res){
if(path.length() == c.length){
res.add(path.toString());
return;
}
for(int i = 0 ; i < c.length ; i++){
if(!used[i]){
if(i > 0 && c[i] == c[i - 1] && !used[i - 1]) continue;
path.append(c[i]);
used[i] = true;
dfs(c , path ,used , res);
used[i] = false;
path.deleteCharAt(path.length() - 1);
}
}
}
}
60. 排列序列/第k个排列
参考](https://leetcode-cn.com/problems/permutation-sequence/solution/hui-su-jian-zhi-python-dai-ma-java-dai-ma-by-liwei/)
- 剪枝基础版
class Solution {
int i = 1;
public String getPermutation(int n, int k) {
HashMap<Integer , String> map = new HashMap<>();
StringBuilder sb = new StringBuilder();
boolean[] used = new boolean[n + 1];
dfs(n , k , sb , map , used);
return map.get(k);
}
void dfs(int n, int k , StringBuilder sb , HashMap<Integer , String> map , boolean[] used){
if(sb.length() == n){
map.put(i++ , sb.toString());
return;
}
//下面剪枝很重要,不然超时;
//是第k个排列就返回,不再继续回溯;
if(i == k + 1) return;
for(int i = 1 ; i <= n ; i++){
if(used[i] == true) continue;
sb.append(i);
used[i] = true;
dfs(n , k , sb , map , used);
used[i] = false;
sb.deleteCharAt(sb.length() - 1);
}
}
}
- 剪枝精简版
class Solution {
//第k个数前面数的累计和;count < k,可剪枝,不再回溯;
int count = 0;
public String getPermutation(int n, int k) {
StringBuilder sb = new StringBuilder();
boolean[] used = new boolean[n + 1];
dfs(n , k , sb , used);
return sb.toString();
}
void dfs(int n, int k , StringBuilder sb , boolean[] used){
if(sb.length() == n){
return;
}
for(int i = 1 ; i <= n ; i++){
if(used[i] == true) continue;
count += f(n - (sb.length() + 1));
if(count < k) continue;
//当累计的count >= k,说明,第k个在本次回溯中,但本次加上的count要在减去;
count -= f(n - (sb.length() + 1));
sb.append(i);
used[i] = true;
dfs(n , k , sb , used);
return;
// used[i] = false;
// sb.deleteCharAt(sb.length() - 1);
}
}
int f(int n){
if(n == 0) return 1;
int res = 1;
for(int i = 1 ; i<= n ; i++) res *= i;
return res;
}
}
Flood Fill
该类型,通常需要
方向数组int[][] directions = {{-1 , 0} , {1 , 0} , {0 , -1}, {0 , 1}};
和状态数组used[][]
,表示遍历过。
79. 单词搜索
参考 【JZ65矩阵中的路径】(https://blog.youkuaiyun.com/qq_42647047/article/details/110389346)
class Solution {
boolean[][] used;
//int[] num = {-1 , 0 , 1 , 0 , -1};
int[][] nums = {{-1 , 0} , {1 , 0} , {0 , -1}, {0 , 1}};
public boolean exist(char[][] board, String word) {
int row = board.length;
int col = board[0].length;
used = new boolean[row][col];
for(int i = 0 ; i < row ; i++){
for(int j = 0 ; j < col ; j++){
if(dfs(board , i , j , 0 , word)) return true;
}
}
return false;
}
boolean dfs(char[][] board , int i , int j , int pos , String word){
if(i < 0 || i >= board.length || j < 0 || j >= board[0].length) return false;
if(used[i][j] == true || board[i][j] != word.charAt(pos)) return false;
if(pos + 1 == word.length()) return true;
used[i][j] = true;
// for(int k = 0 ; k < num.length - 1 ; k++){
// if(dfs(board , i + num[k] , j + num[k + 1] , pos + 1 , word)){
// return true;
// }
// }
for(int[] num : nums){
if(dfs(board , i + num[0] , j + num[1] , pos + 1 , word)) return true;
}
used[i][j] = false;
return false;
}
}
岛屿问题
参考,带例题】(https://leetcode-cn.com/problems/number-of-islands/solution/dao-yu-lei-wen-ti-de-tong-yong-jie-fa-dfs-bian-li-/)【岛屿类问题的通用解法、DFS 遍历框架】
200. 岛屿数量
class Solution {
boolean[][] used;
int[][] directions = {{0 , 1} , {0 , -1} , {1 , 0} , {-1 , 0}};
public int numIslands(char[][] grid) {
int rows = grid.length;
int cols = grid[0].length;
if(rows == 0) return 0;
used = new boolean[rows][cols];
int res = 0;
for(int i = 0 ; i < rows ; i++){
for(int j = 0 ; j < cols ; j++){
if(grid[i][j] == '1' && used[i][j] == false){
res++;
dfs(grid , i , j);
}
}
}
return res;
}
void dfs(char[][] grid , int i , int j){
if(i < 0 || i >= grid.length || j < 0 || j >= grid[0].length) return;
if(used[i][j] == true || grid[i][j] == '0') return;
used[i][j] = true;
for(int[] num : directions){
dfs(grid , i + num[0] , j + num[1]);
}
}
}
- 这里也可以省略used,直接再原数组上该,
但推荐上面,按规矩来
。
class Solution {
public int numIslands(char[][] grid) {
if(grid.length == 0) return 0;
int rows = grid.length;
int clos = grid[0].length;
int count = 0;
for(int r = 0 ; r < rows ; r++){
for(int c = 0 ; c < clos ; c++){
if(grid[r][c] == '1'){
++count;
dfs(grid , rows , clos , r , c);
}
}
}
return count;
}
void dfs(char[][] grid , int rows , int clos , int r , int c){
if(r < 0 || r >= rows || c < 0 || c >=clos) return;
if(grid[r][c] != '1') return;
grid[r][c] = '2';
dfs(grid , rows , clos , r + 1 , c);
dfs(grid , rows , clos , r - 1, c);
dfs(grid , rows , clos , r , c + 1);
dfs(grid , rows , clos , r , c - 1);
}
}
130. 被围绕的区域
class Solution {
int[][] directions = {{0 , 1} , {0 , -1} , {1 , 0} , {-1 , 0}};
boolean[][] used;
public void solve(char[][] board) {
int rows = board.length;
int cols = board[0].length;
used = new boolean[rows][cols];
// 第 1 步:把四周的'O' 以及与'O' 连通的'O'都设置成'-'
for(int j = 0 ; j < cols ; j++){
if(board[0][j] == 'O') dfs(board , 0 , j);
}
for(int j = 0 ; j < cols ; j++){
if(board[rows - 1][j] == 'O') dfs(board , rows - 1 , j);
}
for(int i = 0 ; i < rows ; i++){
if(board[i][0] == 'O') dfs(board ,i , 0);
}
for(int i = 0 ; i < rows ; i++){
if(board[i][cols - 1] == 'O') dfs(board ,i , cols - 1);
}
// 第 2 步:遍历一次棋盘,
// 1. 剩下的 0 就是被 X 包围的 0,
// 2. - 是原来不能被包围的 0,恢复成 0
for(int i = 0 ; i < rows ; i++){
for(int j = 0 ; j < cols ; j++){
if(board[i][j] == 'O') board[i][j] = 'X';;
if(board[i][j] == '-') board[i][j] = 'O';
}
}
return;
}
void dfs(char[][] board , int i , int j){
if(i < 0 || i >= board.length || j < 0 || j >= board[0].length) return;
if(used[i][j] == true || board[i][j] != 'O') return;
used[i][j] = true;
board[i][j] = '-';
for(int[] num : directions){
dfs(board , i + num[0] , j + num[1]);
}
used[i][j] = false;
return;
}
}
733. 图像渲染
class Solution {
int[][] directions = {{0 , 1} , {0 , -1} , {1 , 0} , {-1 , 0}};
boolean[][] used;
public void solve(char[][] board) {
int rows = board.length;
int cols = board[0].length;
used = new boolean[rows][cols];
// 第 1 步:把四周的'O' 以及与'O' 连通的'O'都设置成'-'
for(int j = 0 ; j < cols ; j++){
if(board[0][j] == 'O') dfs(board , 0 , j);
}
for(int j = 0 ; j < cols ; j++){
if(board[rows - 1][j] == 'O') dfs(board , rows - 1 , j);
}
for(int i = 0 ; i < rows ; i++){
if(board[i][0] == 'O') dfs(board ,i , 0);
}
for(int i = 0 ; i < rows ; i++){
if(board[i][cols - 1] == 'O') dfs(board ,i , cols - 1);
}
// 第 2 步:遍历一次棋盘,
// 1. 剩下的 0 就是被 X 包围的 0,
// 2. - 是原来不能被包围的 0,恢复成 0
for(int i = 0 ; i < rows ; i++){
for(int j = 0 ; j < cols ; j++){
if(board[i][j] == 'O') board[i][j] = 'X';;
if(board[i][j] == '-') board[i][j] = 'O';
}
}
return;
}
void dfs(char[][] board , int i , int j){
if(i < 0 || i >= board.length || j < 0 || j >= board[0].length) return;
if(used[i][j] == true || board[i][j] != 'O') return;
used[i][j] = true;
board[i][j] = '-';
for(int[] num : directions){
dfs(board , i + num[0] , j + num[1]);
}
used[i][j] = false;
return;
}
}
游戏问题
51. N 皇后
参考:https://leetcode-cn.com/problems/n-queens/solution/gen-ju-di-46-ti-quan-pai-lie-de-hui-su-suan-fa-si-/
class Solution {
boolean[] clos;
boolean[] main;
boolean[] sub;
public List<List<String>> solveNQueens(int n) {
List<List<String>> res = new ArrayList<>();
ArrayList<Integer> path = new ArrayList<>();
clos = new boolean[n];
main = new boolean[2*n - 1];
sub = new boolean[2*n - 1];
dfs(n , 0 , res , path);
return res;
}
void dfs(int n , int row , List<List<String>> res , ArrayList<Integer> path){
if(path.size() == n){
List<String> board = convertToBoard(path);
res.add(board);
return;
}
for(int j = 0 ; j < n ; j++){
if(!clos[j] && !main[row - j + (n - 1)] && !sub[row + j]){
path.add(j);
clos[j] = true;
main[row - j + (n - 1)] = true;
sub[row + j] = true;
dfs(n , row + 1 , res , path);
sub[row + j] = false;
main[row - j + (n - 1)] = false;
clos[j] = false;
path.remove(path.size() - 1);
}
}
return;
}
List<String> convertToBoard(ArrayList<Integer> path){
int n = path.size();
ArrayList<String> res = new ArrayList<>();
for(Integer val : path){
StringBuilder row = new StringBuilder();
for(int i = 0 ; i < n ; i++) row.append(".");
row.replace(val , val+1 , "Q");
res.add(row.toString());
}
return res;
}
}
37. 解数独
参考 https://leetcode-cn.com/leetbook/read/learning-algorithms-with-leetcode/9owghv/
递归函数dfs的返回值需要是bool类型,为什么呢?
因为解数独找到一个符合的条件(就在树的叶子节点上)立刻就返回,相当于找从根节点到叶子节点一条唯一路径,所以需要使用bool返回值,用来控制,在找到一条路径后,直接返回,而不是继续回溯,只有在当前选的值没找到路径时,才进行下一步回溯;
- 代码逻辑如下:
if (dfs(board, i + (j + 1) / 9, (j + 1) % 9)) {
//找到一条路径后,直接返回,而不是继续回溯
return true;
}
//只有在当前选的值没找到路径时,才进行下一步回溯;
// 重置变量
board[i][j] = '.';
row[i][index] = false;
col[j][index] = false;
box[i / 3][j / 3][index] = false;
dfs(board, i, j) 表示在board[i][j]处填1~9中的一个数k,能否成功填写完数独,有一个k能找到解,就返回true,尝试了1–9都没找到解,返回false;
- 如果返回true,表示该位置填k [1<=k<=9] 的一条路径(从根节点到叶子节点)对应的叶子节点是一个解,所以如果是true,直接返回,不必在回溯其它路径(即,其它解)【因为
题目数据保证输入数独仅有一个解
,所以我们找到一个解直接返回】;- 如果返回false,表示该位置填k [1<=k<=9] 找不到一个解,则直接返回,继续回溯下一个k。
if (i == 9) { return true; }
// 递归终止条件 1:全部填完
在规定条件下,全部填完,表示才找到了一个解,返回true;if (dfs(board, i + (j + 1) / 9, (j + 1) % 9)) { return true; }
从这个子dfs()
也可看出,一般子dfs完了直接 重置变量,
1、它是如果dfs() = true,就返回了,即找到了解,就返回,不再继续找第二个。【题目也保证输入数独仅有一个解】;
2、如果dfs() = false,即,该格子尝试了1~9都没找到解,此时再回溯,回溯到上一个格子,上一个格子重置变量,换一个值再试;
public class Solution {
private boolean[][] row;
private boolean[][] col;
private boolean[][][] box;
public void solveSudoku(char[][] board) {
row = new boolean[9][9];
col = new boolean[9][9];
box = new boolean[3][3][9];
// 步骤 1:同 N 皇后问题,先遍历棋盘一次,然后每一行,每一列在 row col cell 里占住位置
for (int i = 0; i < 9; i++) {
for (int j = 0; j < 9; j++) {
if (board[i][j] != '.') {
// 计算在布尔数值中的下标,减去 1 使得下标从 0 开始
int index = board[i][j] - '1';
// 在下标为 i 这一行,记录为 true
row[i][index] = true;
// 在下标为 j 这一列,记录为 true
col[j][index] = true;
// 在横坐标为 i / 3、纵坐标 j / 3 的地方, 看到的那个数字记录为 true
box[i / 3][j / 3][index] = true;
}
}
}
// 步骤 2:进行一次深度优先遍历,尝试所有的可能性
dfs(board, 0, 0);
}
/**
* @param board 棋盘
* @param i 棋盘横坐标
* @param j 棋盘纵坐标
* @return 由于存在唯一解,搜索到一个解就可以退出了,递归函数的返回值为是否搜索到一个解
*/
private boolean dfs(char[][] board, int i, int j) {
// 递归终止条件 1:全部填完
if (i == 9) {
return true;
}
// 对 '.' 尝试从 1 填到 9
if (board[i][j] == '.') {
for (char c = '1'; c <= '9'; c++) {
// 如果行、列、box 已经填了 c - '1' 则尝试下一个数字
int index = c - '1';
if (row[i][index] || col[j][index] || box[i / 3][j / 3][index]) {
continue;
}
// 填写当前字符,并且对应 row、col、box 占位
board[i][j] = c;
row[i][index] = true;
col[j][index] = true;
box[i / 3][j / 3][index] = true;
// 题目保证只有唯一解,继续填写下一格
// ①:i + (j + 1) / 9 表示如果 j 已经在一列的末尾(此时 j = 8),跳转到下一行
// (j + 1) % 9 ,当 j = 8 时,j + 1 重置到 0
if (dfs(board, i + (j + 1) / 9, (j + 1) % 9)) {
return true;
}
// 重置变量
board[i][j] = '.';
row[i][index] = false;
col[j][index] = false;
box[i / 3][j / 3][index] = false;
}
} else {
// 填写下一格和 ① 一样
return dfs(board, i + (j + 1) / 9, (j + 1) % 9);
}
// 递归终止条件 2:全部尝试过以后,返回 false
//【对应每一层(每一层dfs表示一个格子填1~9)的dfs,走着这,说明前面没返回true,即该格子尝试了1~9都没找到解,返回 false】
return false;
}
}
树
112. 路径总和
class Solution {
boolean flag;
int sum = 0;
public boolean hasPathSum(TreeNode root, int targetSum) {
if(root == null) return false;
dfs(root , targetSum);
return flag;
}
void dfs(TreeNode root , int targetSum){
if(root == null) return;
sum += root.val;
if(sum == targetSum && root.left == null && root.right == null) flag = true;
dfs(root.left , targetSum);
dfs(root.right , targetSum);
sum -= root.val;
return;
}
}
113. 路径总和 II
参考【JZ24 二叉树中和为某一值的路径】 https://blog.youkuaiyun.com/qq_42647047/article/details/111303308
class Solution {
List<List<Integer>> res = new ArrayList<>();
List<Integer> path = new ArrayList<>();
int sum = 0;
public List<List<Integer>> pathSum(TreeNode root, int targetSum) {
if(root == null) return res;
dfs(root , targetSum);
return res;
}
void dfs(TreeNode root , int targetSum){
if(root == null) return;
sum += root.val;
path.add(root.val);
if(root.left == null && root.right == null && sum == targetSum){
res.add(new ArrayList<>(path));
//这里不能加return,因为当root.left == null && root.right == null但sum != targetSum时,路径不符合题意,需将最后加入的一个数字删除,但如果下面加return的话,就走不到path.remove(path.size() - 1);,就删除不了了。
//return;
}
dfs(root.left , targetSum);
dfs(root.right , targetSum);
sum -= root.val;
path.remove(path.size() - 1);
}
}
129. 求根节点到叶节点数字之和
这道题中,二叉树的每条从根节点到叶子节点的路径都代表一个数字。
其实,每个节点都对应一个数字,等于其父节点对应的数字乘以 10 再加上该节点的值(这里假设根节点的父节点对应的数字是 0)
。只要计算出每个叶子节点对应的数字,然后计算所有叶子节点对应的数字之和,即可得到结果
。
class Solution {
int path = 0;
ArrayList<Integer> list = new ArrayList<>();
public int sumNumbers(TreeNode root) {
if(root == null) return 0;
dfs(root);
int res = 0;
for(int val : list) res += val;
return res;
}
void dfs(TreeNode root){
if(root == null) return;
path = path*10 + root.val;
if(root.left == null && root.right == null){
list.add(path);
}
dfs(root.left);
dfs(root.right);
path = (path - root.val) / 10;
}
}
494. 目标和
该题最好用 动态规划,回溯不推荐
【回溯参考】(https://leetcode-cn.com/problems/target-sum/solution/mu-biao-he-by-leetcode-solution-o0cp/)
class Solution {
int res = 0;
int sum = 0;
int target;
public int findTargetSumWays(int[] nums, int target) {
this.target = target;
dfs(1 , 0 , nums);
sum = 0;
dfs(-1 , 0 , nums);
return res;
}
void dfs(int sign , int i , int[] nums){
sum += nums[i]*sign;
if(i == nums.length - 1){
if(sum == target) res++;
return;
}
dfs(1 , i + 1 , nums);
sum -= nums[i + 1]*1;
dfs(-1 , i + 1 , nums);
sum -= nums[i + 1]*(-1);
return;
}
}
拓扑排序
207. 课程表
参考 https://leetcode-cn.com/problems/course-schedule/solution/tuo-bu-pai-xu-by-liweiwei1419/
- 本题可约化为: 课程安排图是否是
有向无环图(DAG)
。即课程间规定了前置条件,但不能构成任何环路,否则课程前置条件将不成立。- 思路是通过
拓扑排序
判断此课程安排图是否是 有向无环图(DAG) 。拓扑排序原理
: 对 DAG 的顶点进行排序,使得对每一条有向边 (u, v),均有 u(在排序记录中)比 v 先出现。亦可理解为对某点 v 而言,只有当 v 的所有源点均出现了,v 才能出现。- 通过课程前置条件列表 prerequisites 可以得到课程安排图的
邻接表 adjacency
,以降低算法时间复杂度。
两个重要概念:
1、邻接表:
通过结点的索引,我们能够得到这个结点的后继结点;即,保存各节点的后继结点;
2、入度数组:
通过结点的索引,我们能够得到指向这个结点的结点个数。即,保存各节点的入度;
【具体参考】https://leetcode-cn.com/problems/course-schedule/solution/tuo-bu-pai-xu-by-liweiwei1419/
class Solution {
public boolean canFinish(int numCourses, int[][] prerequisites) {
int length = prerequisites.length;
if(length == 0) return true;
int[] inDegree = new int[numCourses];
//外面一层是数组,用new创建了,里面是ArrayList<Integer>,声明了它,但还没创建,所以下面要创建;
//也可用List<List<Integer>> adjacency = new ArrayList<>();
//由于这里每个节点用0~numCourses - 1表示,所以每个节点可借助数组或列表下标表示,下标对应的元素 为该节点的后继结点;
//若节点复杂,不能用下标表示,则可用HashMap,key为节点,value为该节点的后继结点
ArrayList<Integer>[] adjacency = new ArrayList[numCourses];
for(int i = 0 ; i < numCourses ; i++){
adjacency[i] = new ArrayList<>();
}
//入度为0的添加进队列,表示可执行;
Queue<Integer> q = new LinkedList<>();
for(int[] num : prerequisites){
inDegree[num[0]]++;
adjacency[num[1]].add(num[0]);
}
for(int i = 0 ; i < inDegree.length ; i++){
if(inDegree[i] == 0) q.add(i);
}
int count = 0;
while(!q.isEmpty()){
int top = q.poll();
count++;
//弹出来节点的所有后继结点的入度减一;
for(int val : adjacency[top]){
inDegree[val]--;
if(inDegree[val] == 0) q.add(val);
}
}
return count == numCourses;
}
}
栈
20. 有效的括号
参考](https://leetcode-cn.com/problems/valid-parentheses/solution/you-xiao-de-gua-hao-by-leetcode-solution/)
class Solution {
public boolean isValid(String s) {
int len = s.length();
if(len % 2 == 1) return false;
HashMap<Character , Character> map = new HashMap<>();
map.put(')' , '(');
map.put(']' , '[');
map.put('}' , '{');
Deque<Character> stack = new LinkedList<>();
for(int i = 0 ; i < len ; i++){
char c = s.charAt(i);
if(c == '(' || c == '[' || c == '{') stack.push(c);
else{
if(stack.isEmpty() || stack.peek() != map.get(c)) return false;
stack.poll();
}
}
return stack.isEmpty();
}
}
155. 最小栈
剑指offer
394. 字符串解码
参考](https://leetcode-cn.com/problems/decode-string/solution/decode-string-fu-zhu-zhan-fa-di-gui-fa-by-jyd/)
数字一个栈,字母一个栈
class Solution {
public String decodeString(String s) {
if(s == null) return null;
Deque<Integer> numStack = new LinkedList<>();
Deque<StringBuilder> strStack = new LinkedList<>();
StringBuilder res = new StringBuilder();
int num = 0;
for(char c : s.toCharArray()){
if(c >='0' && c <='9') num = num*10 + (c - '0');
else if(c == '['){
numStack.push(num);
strStack.push(res);
num = 0;
res = new StringBuilder();
}else if(c == ']'){
StringBuilder temp = strStack.poll();
int repeatNum = numStack.poll();
for(int i = 0 ; i < repeatNum ; i++) temp.append(res);
res = temp;
}else res.append(c);
}
return res.toString();
}
}
32. 最长有效括号
三种方法写,动态规划,用栈求解,左右遍历求解
参考](https://leetcode-cn.com/problems/longest-valid-parentheses/solution/zui-chang-you-xiao-gua-hao-by-leetcode-solution/)
- 栈
//始终保持栈底元素为当前已经遍历过的元素中「最后一个没有被匹配的右括号的下标」,用来区分各有效括号子串的边界;
//1、因为压入的是左括号,碰到右括号就弹出,若栈弹空了,下一个是‘)’则这个‘)’肯定不属于前面的有效括号子串;这个‘)’的下标就是两个有效括号子串的分界线;
//2、若下一个是‘(’,则这个‘(’属不属于前面的有效括号,不能确定,所以始终保持栈底元素为当前已经遍历过的元素中「最后一个没有被匹配的右括号的下标」,用来区分各有效括号子串的边界;
//3、需要注意的是,如果一开始栈为空,第一个字符为左括号的时候我们会将其放入栈中,这样就不满足提及的「最后一个没有被匹配的右括号的下标」,为了保持统一,我们在一开始的时候往栈中放入一个值为 −1 的元素。
class Solution {
public int longestValidParentheses(String s) {
if(s.length() <= 1) return 0;
Deque<Integer> stack = new LinkedList<Integer>();
int res = 0;
stack.push(-1);
for(int i = 0 ; i < s.length() ; i++){
if(s.charAt(i) == '(') stack.push(i);
else{
stack.pop();
if(stack.isEmpty()) stack.push(i);
else res = Math.max(res , i - stack.peek());
}
}
return res;
}
}
- 简单的左右括号计数器
两次反方向遍历:
从左往右遍历
会漏掉一种情况,就是遍历的时候左括号的数量始终大于右括号的数量,即 (() ,这种时候最长有效括号是求不出来的。
解决的方法也很简单,我们只需要从右往左遍历
用类似的方法计算即可,只是这个时候判断条件反了过来:
class Solution {
public int longestValidParentheses(String s) {
if(s.length() <= 1) return 0;
int left = 0 , right = 0;
int res = 0;
for(int i = 0 ; i < s.length() ; i++){
if(s.charAt(i) == '(') left++;
else right++;
if(left == right) res = Math.max(res , 2*right);
else if(left < right){
left = 0 ;
right = 0;
}else continue;
}
left = 0 ;
right = 0;
for(int i = s.length() - 1 ; i >= 0 ; i--){
if(s.charAt(i) == ')') right++;
else left++;
if(left == right) res = Math.max(res , 2*right);
else if(left > right){
left = 0 ;
right = 0;
}else continue;
}
return res;
}
}
哈希表
49. 字母异位词分组
参考】(https://leetcode-cn.com/problems/group-anagrams/solution/zi-mu-yi-wei-ci-fen-zu-by-leetcode-solut-gyoc/)
128. 最长连续序列
参考】(https://leetcode-cn.com/problems/longest-consecutive-sequence/solution/zui-chang-lian-xu-xu-lie-by-leetcode-solution/)
class Solution {
public int longestConsecutive(int[] nums) {
if(nums.length == 0) return 0;
HashSet<Integer> set = new HashSet<>();
for(int val : nums) set.add(val);
int lengthMax = 1;
for(int val : set){
if(set.contains(val - 1)) continue;
else{
int lengthCurr = 1;
while(set.contains(val + 1)){
lengthCurr++;
val = val + 1;
}
lengthMax = Math.max(lengthMax , lengthCurr);
}
}
return lengthMax;
}
}
原地哈希
41. 缺失的第一个正数
参考https://leetcode-cn.com/problems/first-missing-positive/solution/tong-pai-xu-python-dai-ma-by-liweiwei1419/
class Solution {
public int firstMissingPositive(int[] nums) {
//缺失的正数:[1,n+1]
int n = nums.length;
for(int i = 0 ; i < n ; i++){
while(nums[i] >= 1 && nums[i] <= n && nums[nums[i] - 1] != nums[i]){
swap(nums , i , nums[i] - 1);
}
}
for(int i = 0 ; i < n ; i++){
if(nums[i] != i + 1) return i + 1;
}
return n+1;
}
void swap(int[] nums , int a , int b){
int temp = nums[a];
nums[a] = nums[b];
nums[b] = temp;
}
}
进阶:缺失的第一个比K大的数字
- 此时要找的数的范围是[k+1 , k+1+n],此范围内的数nums[i]应该在下标 i-(k+1) 上,然后和上面的做法一致。
class Solution {
public int firstMissingPositive(int[] nums , int k) {
//缺失的数:[k+1 , k+1+n]
int n = nums.length;
for(int i = 0 ; i < n ; i++){
while(nums[i] >= k+1 && nums[i] <= k+n && nums[nums[i] - (k+1)] != nums[i]) swap(nums , nums[i] - (k+1) , i);
}
for(int i = 0 ; i < n ; i++){
if(nums[i] - (k+1) != i) return i + (k+1);
}
return k+1+n;
}
void swap(int[] nums , int a , int b){
int temp = nums[a];
nums[a] = nums[b];
nums[b] = temp;
}
}
剑指 Offer 03. 数组中重复的数字
长度为 n 的数组 nums 里的所有数字都在 0~n-1 的范围内:
- 即,如果没有重复元素,排序后数字i将出现在下标i的位置;
- 为了降低时间复杂度,不使用排序算法,使用
原地哈希方法
,将数字 i 映射到下标 i 处。然后遍历数组,num[i] != i
的数num[i]即为重复数字。
class Solution {
public int findRepeatNumber(int[] nums) {
int n = nums.length;
for(int i = 0 ; i < n ; i++){
while(nums[i] != nums[nums[i]]){
swap(nums , i , nums[i]);
}
}
for(int i = 0 ; i < n ; i++){
if(nums[i] != i) return nums[i];
}
return -1;
}
void swap(int[] nums , int a , int b){
int temp = nums[a];
nums[a] = nums[b];
nums[b] = temp;
}
}
442. 数组中重复的数据
使用
原地哈希方法
,将数字 i 映射到下标 i - 1 处。然后遍历数组,num[i] - 1 != i
的数num[i]即为重复数字。
class Solution {
public List<Integer> findDuplicates(int[] nums) {
List<Integer> res = new ArrayList<>();
int n = nums.length;
for(int i = 0 ; i < n ; i++){
while(nums[i] != nums[nums[i] - 1]){
swap(nums , i , nums[i] - 1);
}
}
for(int i = 0 ; i < n ; i++){
if(nums[i] - 1 != i) res.add(nums[i]);
}
return res;
}
void swap(int[] nums , int a , int b){
int temp = nums[a];
nums[a] = nums[b];
nums[b] = temp;
}
}
看规律、归纳
31. 下一个排列
[参考】(https://leetcode-cn.com/problems/next-permutation/solution/xia-yi-ge-pai-lie-by-leetcode-solution/)
//总的思想:在保证新排列大于原来排列的情况下(1、),使变大的幅度尽可能小(2、3、)。
//eg:[4,5,2,6,3]
//1、找左边的「较小数」;-----------先变大;
//从后向前找nums[i] < num[i+1], 这样nums[i] 和 num[i+1]交换才会变大;【如果不是最后一个排列,下一个排列一定变大】
//2、找右边的「较大数」;----------再使变大的幅度尽可能小;
//然后再从后向前找nums[i] < num[j],找比num[i]大的,最接近num[i]的数,使变大的幅度尽可能小,即下一个排列;
//3、「较大数」右边的数需要按照升序重新排列
class Solution {
public void nextPermutation(int[] nums) {
int len = nums.length;
int i = len - 2;
while(i >= 0 && nums[i] >= nums[i+1]) i--;
if(i >= 0){
int j = len - 1;
while(j > i && nums[i] >= nums[j]) j--;
swap(nums , i , j);
reverse(nums , i + 1);
}else reverse(nums , 0);
return;
}
void swap(int[] nums , int i , int j){
int temp = nums[i];
nums[i] = nums[j];
nums[j] = temp;
return;
}
void reverse(int[] nums , int left){
int right = nums.length - 1;
while(left < right){
swap(nums , left , right);
left++;
right--;
}
return;
}
}
48. 旋转图像
[参考】(https://leetcode-cn.com/problems/rotate-image/solution/0048xuan-zhuan-tu-xiang-by-jasonchiucc-a-2dfs/)
class Solution {
public void rotate(int[][] matrix) {
int numRotate = matrix.length / 2;
int numMove = matrix[0].length - 1;
int top = 0 , bottom = matrix.length - 1 , left = 0 , right = matrix[0].length - 1;
for(int i = 0 ; i < numRotate ; i++){
for(int j = 0 ; j < numMove ; j++){
//因为java是值传递,所以下面的交换方式不起作用,需将matrix也作为一个参数
// swap(matrix[top][left + j] , matrix[top + j][right]);
// swap(matrix[top][left + j] , matrix[bottom][right - j]);
// swap(matrix[top][left + j] , matrix[bottom - j][left]);
swap(matrix , top , left + j , top + j , right);
swap(matrix , top , left + j , bottom , right - j);
swap(matrix , top , left + j , bottom - j , left);
}
top++;
bottom--;
left++;
right--;
numMove = numMove - 2;
}
return;
}
void swap(int[][] matrix ,int a1 , int a2 , int b1 , int b2){
int temp = matrix[a1][a2];
matrix[a1][a2] = matrix[b1][b2];
matrix[b1][b2] = temp;
}
}
55. 跳跃游戏
class Solution {
public boolean canJump(int[] nums) {
int k = 0;//能达到的最远的位置
for(int i = 0 ; i < nums.length ; i++){
if(i > k) return false;//走到的下标大于能到达的最远位置,则不会走到最后
k = Math.max(k , i + nums[i]);//更新能达到的最远的位置
}
return true;
}
}
- 将
走到的下标大于能到达的最远位置,则不会走到最后
这一条件,放到for循环的判断条件里
class Solution {
public boolean canJump(int[] nums) {
int k = 0;//能达到的最远的位置
for(int i = 0 ; i < nums.length && i <= k ; i++){
k = Math.max(k , i + nums[i]);//更新能达到的最远的位置
}
return k >= (nums.length - 1)? true : false;
}
}
406. 根据身高重建队列
[参考】(https://leetcode-cn.com/problems/queue-reconstruction-by-height/solution/xian-pai-xu-zai-cha-dui-dong-hua-yan-shi-suan-fa-g/)
class Solution {
public int[][] reconstructQueue(int[][] people) {
//首先对数对进行排序,按照数对的元素 1 降序排序,按照数对的元素 2 升序排序;
Arrays.sort(people , new Comparator<int[]>(){
public int compare(int[] a , int[] b){
if(a[0] != b[0]) return b[0] - a[0];
else return a[1] - b[1];
}
});
List<int[]> res = new ArrayList<>();
for(int[] p : people){
if(res.size() <= p[1]) res.add(p);
else res.add(p[1] , p);
}
return res.toArray(new int[res.size()][]);
}
}
628. 三个数的最大乘积
参考https://leetcode-cn.com/problems/maximum-product-of-three-numbers/solution/san-ge-shu-de-zui-da-cheng-ji-by-leetcod-t9sb/
class Solution {
public int maximumProduct(int[] nums) {
int min1 = Integer.MAX_VALUE;
int min2 = Integer.MAX_VALUE;
int max1 = Integer.MIN_VALUE;
int max2 = Integer.MIN_VALUE;
int max3 = Integer.MIN_VALUE;
for(int val : nums){
if(val < min1){
min2 = min1;
min1 = val;
}else if(val < min2){
min2 = val;
}
//更新最大值和最小值需分开更新,这样一个数才能既参与最小值的更新,又能参与最大值的更新;
//因为一个数既可能是最大值有可能是最小值
if(val > max1){
max3 = max2;
max2 = max1;
max1 = val;
}else if(val > max2){
max3 = max2;
max2 = val;
}else if(val >max3){
max3 = val;
}else continue;
}
return Math.max(max1*max2*max3 , min1*min2*max1);
}
}
400. 第 N 位数字
参考:https://leetcode-cn.com/problems/nth-digit/solution/zi-jie-ti-ku-400-zhong-deng-di-nge-shu-zi-1shua-by/
class Solution {
public int findNthDigit(int n) {
if(n <= 9) return n;
int digit = 1;
long start = 1;
long indexCount = digit*9*start;
while(n > indexCount){
n -= indexCount;
digit++;
start = start*10;
indexCount = digit*9*start;
}
long val = start + (n - 1) / digit;
int remainder = (n - 1) % digit;
return String.valueOf(val).charAt(remainder) - '0';
}
}
- 注释版C++
class Solution {
public:
int findNthDigit(int n) {
if(n == 0) {return 0;}
int digit = 1; // 数位(个位/十位/百位/...,就是1/2/3/...)
long start = 1; // 属于该数位的所有数的起始点数(个位是1,十位是10,百位是100)
long index_count = digit * 9 * start; // 该数位的数一共的索引个数(不是数字个数)
while(n > index_count ) {
// 找出 n 属于那个数位里的索引
n -= index_count;
++ digit;
start *= 10;
index_count = digit * 9 * start;
}
// 上面的循环结束后:
// digit 等于原始的 n 所属的数位;start 等于原始的 n 所属数位的数的起始点
// index_count 等于原始的 n 所属数位的索引总个数(不重要了,下面不用)
// n 等于在当前数位里的第 n - 1 个索引(索引从 0 开始算起)
long num = start + (n - 1) / digit; // 算出原始的 n 到底对应那个数字
int remainder = (n - 1) % digit; // 余数就是原始的 n 是这个数字中的第几位
string s_num = to_string(num); // 将该数字转为 string 类型
return int(s_num[remainder] - '0'); // n 对应着第 remainder 位,再转成 int
}
};
976. 三角形的最大周长
参考:https://leetcode-cn.com/problems/largest-perimeter-triangle/solution/san-jiao-xing-de-zui-da-zhou-chang-by-leetcode-sol/
class Solution {
public int largestPerimeter(int[] nums) {
Arrays.sort(nums);
for(int i = nums.length - 1 ; i >= 2 ; i--){
if(nums[i - 2] + nums[i - 1] > nums[i])
return nums[i - 2] + nums[i - 1] + nums[i];
}
return 0;
}
}
440. 字典序的第K小数字
参考https://leetcode-cn.com/problems/k-th-smallest-in-lexicographical-order/solution/yi-tu-sheng-qian-yan-by-pianpianboy/
class Solution {
public int findKthNumber(int n, int k) {
//注意 cur 为long型,防止溢出
long cur = 1;// 当前遍历到的数字,从1(根)出发
// 从1出发开始往后按字典序从小到大的顺序走k-1步到达的就是 字典序的第K小数字
k = k - 1;//扣除掉第一个0节点
while(k>0){
int num = getNode(n,cur,cur+1);
if(num<=k){//第k个数不在以cur为根节点的树上
cur+=1;//cur在字典序数组中从左往右移动
k-=num;
}else{//在子树中
cur*=10;//cur在字典序数组中从上往下移动
k-=1;//刨除根节点
}
}
return (int)cur;// 最后cur停在的数字就是字典序的第K小数字
}
// 计算以first为根的子树节点数目,所有节点的值必须 <= n
public int getNode(int n, long first, long last){
int num = 0;
while(first <= n){
num += Math.min(n+1,last) - first;//比如n是195的情况195到100有96个数
first *= 10;
last *= 10;
}
return num;
}
}
8. 字符串转换整数 (atoi)
参考【面试题Offer 67. 把字符串转换成整数】https://blog.youkuaiyun.com/qq_42647047/article/details/112181978
class Solution {
public int myAtoi(String s) {
int len = s.length();
if(len == 0 ) return 0;
int i = 0;
//找到第一个不是空格的字符,若全是空格,返回0;
while(s.charAt(i) == ' '){
i++;
if(i == len) return 0;
}
int sign = 1;
if(s.charAt(i) == '-') sign = -1;
if(s.charAt(i) == '-' || s.charAt(i) == '+') i++;
//注意:long res,不然溢出
long res = 0;
for(int j = i ; j < len ; j++){
char c = s.charAt(j);
//字符串中的第一个非空格字符不是一个有效整数字符返回 0;
if(c < '0' || c > '9') break;
res = res*10 + (c - '0');
if(res > Integer.MAX_VALUE){
return sign == 1? Integer.MAX_VALUE : Integer.MIN_VALUE;
}
}
return (int)(sign*res);
}
}
6. Z 字形变换
1、首先考虑特殊情况,排成一行,或字符串的长度小于行数--------->>直接返回原字符串;
2、从左到右迭代 s,将每个字符添加到合适的行。可以使用当前行
和当前方向
这两个变量对合适的行进行跟踪。
参考:https://blog.youkuaiyun.com/qq_42647047/article/details/109190557
class Solution {
public String convert(String s, int numRows) {
if(numRows == 1 || s.length() <= numRows) return s;
StringBuilder[] nums = new StringBuilder[numRows];
//注意:数组元素不是基本类型时,需要对每个元素单独创建;
for(int i = 0 ; i < nums.length ; i++) nums[i] = new StringBuilder();
int curRow = 0;//当前行
boolean goDown = false;//当前方向
for(int i = 0 ; i < s.length() ; i++){
char c = s.charAt(i);
nums[curRow].append(c);
if(curRow == 0 || curRow == numRows - 1) goDown = !goDown;
curRow = curRow + (goDown == true ? 1 : -1);
}
//用StringBuilder,比用String效率更高;
StringBuilder res = new StringBuilder();
for(StringBuilder sb : nums) res.append(sb);
return res.toString();
}
}
564. 寻找最近的回文数
参考:https://leetcode-cn.com/problems/find-the-closest-palindrome/solution/564-cchao-100de-shu-xue-jie-fa-by-ffretu-byqa/
1、题目思路其实就是把前面一半拿出来,然后倒过来补全即可: 如 53420 -> 53435/2/其他特殊情况
- 中间是0和9,如 34043->34143 或 595 -> 585 -》 中间位数是 0,+1, 或-1
- 9999 -》 10001, 1001-》999
class Solution {
public String nearestPalindromic(String n) {
int len = n.length();
long nn = Long.parseLong(n);
// 考虑 10000 的情况 -> 9999
if(nn < 10 || nn == Math.pow(10 , len-1)) return String.valueOf(nn - 1);
// 考虑 10001 的情况 -> 9999
else if(nn - 1 == Math.pow(10 , len - 1)) return String.valueOf(nn - 2);
// 考虑 9999 的情况 -> 10001
else if(nn + 1 == Math.pow(10 , len)) String.valueOf(nn + 2);
// 取前一半
long firstHalf = Long.parseLong(n.substring(0 , (len+1)/2));
long minAbs = Integer.MAX_VALUE;
String res = "";
int[] nums = {-1 , 0 , 1};
for(int dx :nums){
String half = String.valueOf(firstHalf + dx);
String rev = new StringBuilder(half).reverse().toString();
String curr = half.substring(0 , len / 2) + rev;
long currAbs = Math.abs(Long.parseLong(curr) - nn);
if (curr != n && currAbs < minAbs && currAbs != 0)
{
minAbs = currAbs;
res = curr;
}
}
return res;
}
}
470. 用 Rand7() 实现 Rand10()
参考:https://leetcode-cn.com/problems/implement-rand10-using-rand7/solution/cong-zui-ji-chu-de-jiang-qi-ru-he-zuo-dao-jun-yun-/
/**
* 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() {
while(true) {
int num = (rand7() - 1) * 7 + rand7(); // 等概率生成[1,49]范围的随机数
if(num <= 40) return num % 10 + 1; // 拒绝采样,并返回[1,10]范围的随机数
}
}
}
- 优化
/**
* 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() {
while(true) {
int a = rand7();
int b = rand7();
int num = (a-1)*7 + b; // rand 49
if(num <= 40) return num % 10 + 1; // 拒绝采样
a = num - 40; // rand 9
b = rand7();
num = (a-1)*7 + b; // rand 63
if(num <= 60) return num % 10 + 1;
a = num - 60; // rand 3
b = rand7();
num = (a-1)*7 + b; // rand 21
if(num <= 20) return num % 10 + 1;
}
}
}
已有一个随机器f,只能生成0、1,生成0的概率是p,生成1的概率是1-p,设计一个生成g,生成0、1的概率相等;
参考:https://blog.youkuaiyun.com/weixin_41888257/article/details/107589772?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522163542766016780271535111%2522%252C%2522scm%2522%253A%252220140713.130102334.pc%255Fall.%2522%257D&request_id=163542766016780271535111&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2allfirst_rank_ecpm_v1~rank_v31_ecpm-1-107589772.pc_search_result_hbase_insert&utm_term=%E5%B7%B2%E6%9C%89%E4%B8%80%E4%B8%AA%E9%9A%8F%E6%9C%BA%E5%99%A8f%EF%BC%8C%E5%8F%AA%E8%83%BD%E7%94%9F%E6%88%900%E3%80%811%EF%BC%8C%E7%94%9F%E6%88%900%E7%9A%84%E6%A6%82%E7%8E%87%E6%98%AFp%EF%BC%8C%E7%94%9F%E6%88%901%E7%9A%84%E6%A6%82%E7%8E%87%E6%98%AF1-p%EF%BC%8C%E8%AE%BE%E8%AE%A1%E4%B8%80%E4%B8%AA%E7%94%9F%E6%88%90g%EF%BC%8C%E7%94%9F%E6%88%900%E3%80%811%E7%9A%84%E6%A6%82%E7%8E%87%E7%9B%B8%E7%AD%89&spm=1018.2226.3001.4187
class Solution {
public int solve(){
Random rand = new Random();
int res = 0;
while(true){
int a = rand.nextInt(2);
int b = rand.nextInt(2);
if(a == 0 && b == 1){
res = 0;
break;
}
else if(a == 1 && b == 0){
res = 1;
break;
}
else continue;
}
return res;
}
}
进阶:生成0~n的概率相等
取n个随机数发生器f,连续生成n个数,生成“只有一个1,其它全0”的n个数,1的位置代表1~n(比如,1000…代表1、0100…代表2),这个生成的每个数的概率都是相同的,即p^(n-1)*p,其他的情况舍弃并重新调用。
例如 :
n=3 ,有 8 中情况000,001,010,011,100,101,110,111,我们取其中的100、010、001表示1、2、3,它们的概率都是p^2∗(1−p),对于其他的情况,舍弃并重新调用。
7. 整数反转
class Solution {
public int reverse(int x) {
long res = 0;
while(x != 0){
int temp = x % 10;
x = x / 10;
res = res*10 + temp;
if(res > Integer.MAX_VALUE || res < Integer.MIN_VALUE) return 0;
}
return (int)res;
}
}
动态规划
剑指 Offer 46. 把数字翻译成字符串
参考面试题46 把数字翻译成字符串 https://blog.youkuaiyun.com/qq_42647047/article/details/110485850
class Solution {
public int translateNum(int num) {
if(num <= 1) return 1;
String s = String.valueOf(num);
int n = s.length();
int[] dp = new int[n + 1];
dp[0] = 1;
dp[1] = 1;
for(int i = 2 ; i <= n ; i++){
int temp = Integer.parseInt(s.substring(i - 2 , i));
if(temp >= 10 && temp <= 25) dp[i] = dp[i-1] + dp[i-2];
else dp[i] = dp[i-1];
}
return dp[n];
}
}
91. 解码方法
参考https://leetcode-cn.com/problems/decode-ways/solution/jie-ma-fang-fa-by-leetcode-solution-p8np/
class Solution {
public int numDecodings(String s) {
int n = s.length();
if(n == 0) return 0;
int[] dp = new int[n + 1];
dp[0] = 1;
if(s.charAt(0) == '0') dp[1] = 0;
else dp[1] = 1;
//总的来说dp[i] = dp[i-1] + dp[i=2];但具体怎么写,决定于s[i],s[i]s[i-1]能不能解码,
for(int i = 2 ; i <= n ; i++){
//当s[i]不能解码,即s[i]=='0'时;接着判断s[i]s[i-1]能不能解码;
if(s.charAt(i - 1) == '0'){
int temp = Integer.parseInt(s.substring(i - 2 , i));
//如果s[i]s[i-1]在[10~26],即能解码,则dp[i] = dp[i-2];
if(temp >= 10 && temp <= 26) dp[i] = dp[i-2];
//否则,不能解码,有一个不能解码的元素,则直接返回0;
else return 0;
}
//当s[i]能解码,即s[i]!='0'时;接着判断s[i]s[i-1]能不能解码;
else{
int temp = Integer.parseInt(s.substring(i - 2 , i));
//如果s[i]s[i-1]在[10~26],即能解码,则dp[i] = dp[i-1] + dp[i-2]
if(temp >= 10 && temp <= 26) dp[i] = dp[i-1] + dp[i-2];
else dp[i] = dp[i-1];
}
}
return dp[n];
}
}
62. 不同路径
class Solution {
public int uniquePaths(int m, int n) {
int[][] dp = new int[m][n];
for(int i = 0 ; i < m ; i++){
for(int j = 0 ; j < n ; j ++){
if(i == 0 && j == 0) dp[i][j] = 1;
else if(i == 0) dp[i][j] = dp[i][j - 1];
else if(j == 0) dp[i][j] = dp[i - 1][j];
else dp[i][j] = dp[i][j - 1] + dp[i - 1][j];
}
}
return dp[m - 1][n - 1];
}
}
63. 不同路径 II
class Solution {
public int uniquePathsWithObstacles(int[][] obstacleGrid) {
int rows = obstacleGrid.length;
int cols = obstacleGrid[0].length;
int[][] dp = new int[rows][cols];
//仅仅是第一行第一列,有障碍物的格子及其后的格子的路径数等0;
for(int j = 0 ; j < cols ; j++){
if(obstacleGrid[0][j] == 0) dp[0][j] = 1;
else{
dp[0][j] = 0;
break;
}
}
for(int i = 0 ; i < rows ; i++){
if(obstacleGrid[i][0] == 0) dp[i][0] = 1;
else{
dp[i][0] = 0;
break;
}
}
for(int i = 1 ; i < rows ; i++){
for(int j = 1 ; j < cols ; j++){
if(obstacleGrid[i][j] == 0) dp[i][j] = dp[i][j-1] + dp[i-1][j];
else{
dp[i][j] = 0;
}
}
}
return dp[rows-1][cols-1];
}
}
64. 最小路径和
class Solution {
public int minPathSum(int[][] grid) {
int row = grid.length;
int col = grid[0].length;
int[][] dp = new int[row][col];
for(int i = 0 ; i< row ; i++){
for(int j = 0 ; j < col ; j++){
if(i == 0 && j == 0) dp[i][j] = grid[i][j];
else if(i == 0) dp[i][j] = dp[i][j - 1] + grid[i][j];
else if(j == 0) dp[i][j] = dp[i - 1][j] + grid[i][j];
else dp[i][j] = Math.min(dp[i - 1][j] , dp[i][j - 1]) + grid[i][j];
}
}
return dp[row - 1][col - 1];
}
}
96. 不同的二叉搜索树
class Solution {
public int numTrees(int n) {
if(n == 0) return 0;
int[] dp = new int[n + 1];
dp[0] = 1;
dp[1] = 1;
for(int i = 2 ; i < n + 1 ; i++){
for(int j = 1 ; j <= i ; j++){
dp[i] += dp[j - 1] * dp[i - j];
}
}
return dp[n];
}
}
152. 乘积最大子数组
“最大子数组和”是DP算法里的经典案例之一,经典到这个解法甚至有一个名称Kadane’s Algorithm。本题是“最大子数组和”的变型,但Kadane’s Algo依然适用。唯一要注意的是“乘法”下由于两个负数的乘积也依然可能得到一个很大的正数,所以必须同时计算“最小子数组和”,除此之外无任何区别。Kadane’s Algo变型可以解决问题还有:
1186. 删除一次得到子数组最大和 - Medium
题解:1186. 删除一次得到子数组最大和
1191. K 次串联后最大子数组之和 - Hard
题解:1191. K 次串联后最大子数组之和
[参考】(https://leetcode-cn.com/problems/maximum-product-subarray/solution/cheng-ji-zui-da-zi-shu-zu-by-leetcode-solution/)
由于存在负数,那么会导致最大的变最小的,最小的变最大的。因此还需要维护当前最小值minTemp,则最大值应为下面三个值的最大值maxTemp = Math.max(maxTemp*nums[i] , Math.max(minTemp*nums[i] , nums[i]));
class Solution {
public int maxProduct(int[] nums) {
int length = nums.length;
int maxdp[] = new int[length];
int mindp[] = new int[length];
maxdp[0] = nums[0];
mindp[0] = nums[0];
for(int i = 1 ; i < length ; i++){
maxdp[i] = Math.max(maxdp[i-1]*nums[i] , Math.max(mindp[i-1]*nums[i] , nums[i]));
mindp[i] = Math.min(maxdp[i-1]*nums[i] , Math.min(mindp[i-1]*nums[i] , nums[i]));
}
int res = maxdp[0];
for (int i = 1; i < length; ++i) {
res = Math.max(res, maxdp[i]);
}
return res;
}
}
- 优化空间
class Solution {
public int maxProduct(int[] nums) {
int length = nums.length;
int maxTemp = nums[0];
int minTemp = nums[0];
int res = nums[0];
for(int i = 1 ; i < length ; i++){
//这里创建两个新的变量,是因为下面两个求最值的式子用的最大最小值,都是上一轮的计算结果,若不创建新变量,下一个求最值的式子会用本轮刚求出的最大值maxTemp ,导致出错
int maxT = maxTemp, minT = minTemp;
maxTemp = Math.max(maxT*nums[i] , Math.max(minT*nums[i] , nums[i]));
minTemp = Math.min(maxT*nums[i] , Math.min(minT*nums[i] , nums[i]));
res = Math.max(res , maxTemp);
}
return res;
}
}
221. 最大正方形
[参考】(https://leetcode-cn.com/problems/maximal-square/solution/zui-da-zheng-fang-xing-by-leetcode-solution/)
dp(i,j) 表示以(i,j) 为右下角,且只包含 1 的正方形的边长最大值。
class Solution {
public int maximalSquare(char[][] matrix) {
if(matrix == null || matrix.length == 0 || matrix[0].length == 0) return 0;
int rows = matrix.length;
int columns = matrix[0].length;
int[][] dp = new int[rows][columns];
int maxSide = 0;
for(int i = 0 ; i < rows ; i++){
for(int j = 0 ; j < columns ; j++){
if(matrix[i][j] == '0'){
dp[i][j] = 0;
}else{
if(i == 0 || j == 0) dp[i][j] = 1;
else dp[i][j] = Math.min(dp[i-1][j] , Math.min(dp[i-1][j-1] , dp[i][j-1])) + 1;
maxSide = Math.max(maxSide , dp[i][j]);
}
}
}
return maxSide * maxSide;
}
}
279. 完全平方数
[参考】(https://leetcode-cn.com/problems/perfect-squares/solution/wan-quan-ping-fang-shu-by-leetcode-solut-t99c/)
class Solution {
public int numSquares(int n) {
if(n == 0) return 0;
int[] dp = new int[n+1];
for(int i = 1 ; i <= n ; i++){
int min = Integer.MAX_VALUE;
for(int j = 1 ; j*j <= i ; j++){
min = Math.min(1 + dp[i - j*j] , min);
}
dp[i] = min;
}
return dp[n];
}
}
300. 最长递增子序列
[参考】
【dp】(https://leetcode-cn.com/problems/longest-increasing-subsequence/solution/zui-chang-shang-sheng-zi-xu-lie-by-leetcode-soluti/)
【贪心算法、二分查找】https://leetcode-cn.com/problems/longest-increasing-subsequence/solution/dong-tai-gui-hua-er-fen-cha-zhao-tan-xin-suan-fa-p/
- dp
class Solution {
public int lengthOfLIS(int[] nums) {
int[] dp = new int[nums.length];
dp[0] = 1;
int res = 1;
for(int i = 1 ; i < nums.length ; i++){
dp[i] = 1;//最小长度是只包含自己,即长度为1;
for(int j = 0 ; j < i ; j++){
if(nums[i] > nums[j]) dp[i] = Math.max(dp[i] , dp[j] + 1);
}
res = Math.max(res , dp[i]);
}
return res;
}
}
- 贪心算法、二分查找
class Solution {
public int lengthOfLIS(int[] nums) {
int len = nums.length;
if (len <= 1) {
return len;
}
// tail 数组的定义:长度为 i + 1 的上升子序列的末尾最小是几
int[] tail = new int[len];
// 遍历第 1 个数,直接放在有序数组 tail 的开头
tail[0] = nums[0];
// end 表示有序数组 tail 的最后一个已经赋值元素的索引
int end = 0;
for (int i = 1; i < len; i++) {
// 【逻辑 1】比 tail 数组实际有效的末尾的那个元素还大
if (nums[i] > tail[end]) {
// 直接添加在那个元素的后面,所以 end 先加 1
end++;
tail[end] = nums[i];
} else {
// 使用二分查找法,在有序数组 tail 中
// 找到第 1 个大于等于 nums[i] 的元素,尝试让那个元素更小
int left = 0;
int right = end;
while (left < right) {
// 选左中位数不是偶然,而是有原因的,原因请见 LeetCode 第 35 题题解
// int mid = left + (right - left) / 2;
int mid = left + ((right - left) >>> 1);
if (tail[mid] < nums[i]) {
// 中位数肯定不是要找的数,把它写在分支的前面
left = mid + 1;
} else {
right = mid;
}
}
// 走到这里是因为 【逻辑 1】 的反面,因此一定能找到第 1 个大于等于 nums[i] 的元素
// 因此,无需再单独判断
tail[left] = nums[i];
}
}
// 此时 end 是有序数组 tail 最后一个元素的索引
// 题目要求返回的是长度,因此 +1 后返回
end++;
return end;
}
}
338. 比特位计数
涉及到位数运算,且涉及到1的个数,要想到x=x & (x−1),该运算将 x 的二进制表示的最后一个 1 变成 0
[参考 方法四:动态规划——最低设置位】(https://leetcode-cn.com/problems/counting-bits/solution/bi-te-wei-ji-shu-by-leetcode-solution-0t1i/)
class Solution {
public int[] countBits(int n) {
int[] res = new int[n + 1];
for(int i = 1 ; i <= n ; i++){
res[i] = res[i & (i - 1)] + 1;
}
return res;
}
}
无后效性【多阶段、有约束 的决策最优化问题】
剑指 Offer II 100. 三角形中最小路径之和
- 找出自顶向下的最小路径和:只求最值,不需求最值的方案------------DP
- 由约束条件得状态定义:dp[i][j],表示到达第i行(下标从0开始),第j列的最小路径。【因为向下移动不能随便移动,所以增加一维表示出每一个点的最小路径和】
解题思路
参考 https://leetcode-cn.com/problems/IlPe0q/solution/dp-by-jia-zhi-tong-1-rhs9/
动态规划
1.状态
dp[i][j],表示到达第i行(下标从0开始),第j列的最小路径。
2.初始化
dp[0][0] = triangle[0][0](因为从顶点开始),其余为无穷,因为要求的是最小路径。
3.状态转移方程
每行的第一个格子只能由上一行的第一个格子过来,最后一个格子也只能由上一行的最后一个格子过来,到达它们的路径和为上一行对应格子的路径和与该格子的值之和(也就是说这两种格子,到达它们的路径唯一)。
对于其它格子,可以由上一行中同列格子和上一行中前一列格子而来,因为题目中要求的是最小路径和,因此应从可以到达它的两个格子中取最小路径和更小的那个,再加上其自身的值,就是到达它的最小路径和。因此,可得状态转移方程dp[r][c] = min(dp[r-1][c-1], dp[r-1][c]) + triangle[r][c]
4.结果
题目中是要求自顶向下的最小路径和,到达最下边一行的任何位置都可以。因此,结果应是最下边一行各个格子的最小路径和的最小值。
class Solution {
public int minimumTotal(List<List<Integer>> triangle) {
int n = triangle.size();
if(n == 0) return 0;
if(n == 1) return triangle.get(0).get(0);
int[][] dp = new int[n][n];
dp[0][0] = triangle.get(0).get(0);
for(int i = 1 ; i < n ; i++){
for(int j = 0 ; j <= i ; j++){
if(j == 0) dp[i][j] = dp[i-1][j] + triangle.get(i).get(j);
else if(j == i) dp[i][j] = dp[i-1][j-1] + triangle.get(i).get(j);
else{
dp[i][j] = Math.min(dp[i-1][j-1] , dp[i-1][j]) + triangle.get(i).get(j);
}
}
}
int min = Integer.MAX_VALUE;
for(int val : dp[n-1]) min = Math.min(min , val);
return min;
}
}
- 题目要求O(n) 的空间复杂度,优化空间
注意第二个循环要倒着计算
。
class Solution {
public int minimumTotal(List<List<Integer>> triangle) {
int n = triangle.size();
if(n == 0) return 0;
if(n == 1) return triangle.get(0).get(0);
int[] dp = new int[n];
//dp[0][0] = triangle.get(0).get(0);
dp[0] = triangle.get(0).get(0);
for(int i = 1 ; i < n ; i++){
//注意这里要倒着计算,因为dp[j]的计算涉及上一层的dp[j-1];原理同完全背包;
for(int j = i ; j >= 0 ; j--){
if(j == 0) dp[j] = dp[j] + triangle.get(i).get(j);
else if(j == i) dp[j] = dp[j-1] + triangle.get(i).get(j);
else{
dp[j] = Math.min(dp[j-1] , dp[j]) + triangle.get(i).get(j);
}
}
}
int min = Integer.MAX_VALUE;
for(int i = 0 ; i < n ; i++) min = Math.min(min , dp[i]);
return min;
}
}
121. 买卖股票的最佳时机
-
约束条件:你只能选择 某一天 买入这只股票,并选择在 未来的某一个不同的日子 卖出该股票
即,某一天最多持有一个股票,即每天只有持有和不持有股票两种情况,所以状态定义为:
dp[i][j] 表示到下标为 i 的这一天,持股状态为 j 时,我们手上拥有的最大现金数。
dp[i][0] ,到第i天,我们不持有股时,手上拥有的最大现金数;
dp[i][1] ,到第i天,我们持有股时,手上拥有的最大现金数; -
假设我们开始时手中现金为0;
public class Solution {
public int maxProfit(int[] prices) {
int len = prices.length;
// 特殊判断
if (len < 2) {
return 0;
}
int[][] dp = new int[len][2];
// dp[i][0] 下标为 i 这天结束的时候,不持股,手上拥有的现金数
// dp[i][1] 下标为 i 这天结束的时候,持股,手上拥有的现金数
// 初始化:不持股显然为 0,持股就需要减去第 1 天(下标为 0)的股价
dp[0][0] = 0;
dp[0][1] = -prices[0];
// 从第 2 天开始遍历
for (int i = 1; i < len; i++) {
dp[i][0] = Math.max(dp[i - 1][0], dp[i - 1][1] + prices[i]);
//由于只买卖一次,dp[i - 1][0]不持有股票的时候肯定现金是0;所有这里不能加dp[i - 1][0],即:
//dp[i][1] = Math.max(dp[i - 1][1], dp[i - 1][0] -prices[i]);
dp[i][1] = Math.max(dp[i - 1][1], -prices[i]);
}
return dp[len - 1][0];
}
}
122. 买卖股票的最佳时机 II
- 状态设计和121题一样
区别是,由于可以交易多次dp[i][1] = Math.max(dp[i - 1][1],dp[i - 1][0]
-prices[i]);
public class Solution {
public int maxProfit(int[] prices) {
int len = prices.length;
if (len < 2) {
return 0;
}
// 0:持有现金
// 1:持有股票
// 状态转移:0 → 1 → 0 → 1 → 0 → 1 → 0
int[][] dp = new int[len][2];
dp[0][0] = 0;
dp[0][1] = -prices[0];
for (int i = 1; i < len; 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], dp[i - 1][0] - prices[i]);
}
return dp[len - 1][0];
}
}
123. 买卖股票的最佳时机 III
参考:https://leetcode-cn.com/problems/best-time-to-buy-and-sell-stock-iii/solution/dong-tai-gui-hua-by-liweiwei1419-7/
- 状态定义
一天结束时,可能有持股、可能未持股、可能卖出过1次、可能卖出过2次、也可能未卖出过所以定义状态转移数组dp[天数][当前是否持股][卖出的次数]
;【自定义:买入为一次交易
】
则,初始状态:
- 初始状态:
//dp[i][j][k]: i:前i天;j是否持股;k:交易次数,以买入为一次交易;
//即,dp[i][j][k]:表示i,j,k时的最大利润;
dp[0][0][0] = 0;
//不可能的情况,求最大值,应设成无穷小Integer.MIN_VALUE,但下面用到dp[0][0][1]时,
//是dp[i-1][0][1] - prices[i],此时减去一个数会溢出,所以要设成0
dp[0][0][1] = 0;
//]不可能的情况,求最大值,设成无穷小Integer.MIN_VALUE,【下面不涉及它的加减,所以不会溢出】
dp[0][0][2] = Integer.MIN_VALUE;
//dp[0][1][0]不可能的情况,下面也没用到,不用写啦。
dp[0][1][1] = -prices[0];
//不可能的情况,求最大值,设成无穷小Integer.MIN_VALUE,【下面有它的加操作,加操作不会溢出】
dp[0][1][2] = Integer.MIN_VALUE;
class Solution {
public int maxProfit(int[] prices) {
int len = prices.length;
if(len <= 1) return 0;
int[][][] dp = new int[len][2][3];
//未持股
dp[0][0][0] = 0;
dp[0][0][1] = 0;
dp[0][0][2] = Integer.MIN_VALUE;
//持股
//dp[0][1][0]不可能的情况,下面也没用到,不用写啦。
dp[0][1][1] = -prices[0];
dp[0][1][2] = Integer.MIN_VALUE;
for(int i = 1 ; i < len ; i++){
dp[i][0][0] = 0;
//考虑可能转移到dp[i][0][1]的情况:
//1、未持股,发生0次交易 能否 转到 dp[i][0][1];
//2、未持股,发生1次交易 能否 转到 dp[i][0][1];
//2、未持股,发生2次交易 能否 转到 dp[i][0][1];
//2、持股,发生0次交易 能否 转到 dp[i][0][1];
//2、持股,发生1次交易 能否 转到 dp[i][0][1];
//2、持股,发生2次交易 能否 转到 dp[i][0][1];
dp[i][0][1] = Math.max(dp[i-1][0][1] , dp[i-1][1][1] + prices[i]);
dp[i][0][2] = Math.max(dp[i-1][0][2] , dp[i-1][1][2] + prices[i]);
dp[i][1][1] = Math.max(dp[i-1][1][1] , dp[i-1][0][0] - prices[i]);
dp[i][1][2] = Math.max(dp[i-1][1][2] , dp[i-1][0][1] - prices[i]);
}
return Math.max(dp[len - 1][0][2] , dp[len - 1][0][1]);
}
}
188. 买卖股票的最佳时机 IV
【含贪心,学完贪心再做】
309. 最佳买卖股票时机含冷冻期
//状态是
是否持股
,但由于卖出后第二天冷冻,不持股又可分以下两种情况:
1、当天卖出的不持股,第二天是冷冻期;
2、不是当天卖出的不持股,第二天不是冷冻期;
class Solution {
public int maxProfit(int[] prices) {
if(prices.length <= 1) return 0;
int[][] dp = new int[prices.length][3];
//因为下面状态转移求dp[1][1] 时,
//会有dp[1][1] = Math.max(dp[0][0] - prices[1], dp[0][1]);
//即,dp[0][0] - prices[1],若dp[0][0] = Integer.MIN_VALUE,会溢出;
dp[0][0] = 0;
dp[0][1] = -prices[0];
dp[0][2] = Integer.MIN_VALUE;
for(int i = 1 ; i < prices.length ; i++){
dp[i][0] = Math.max(dp[i - 1][0], dp[i - 1][2]);
dp[i][1] = Math.max(dp[i - 1][0] - prices[i], dp[i - 1][1]);
dp[i][2] = dp[i - 1][1] + prices[i];
}
return Math.max(dp[prices.length - 1][0] , dp[prices.length - 1][2]);
}
}
714. 买卖股票的最佳时机含手续费
我们规定在卖出股票的时候扣除手续费;
class Solution {
public int maxProfit(int[] prices, int fee) {
int len = prices.length;
if(len <= 1) return 0;
int[][] dp = new int[len][2];
dp[0][0] = 0;
dp[0][1] = -prices[0];
for(int i = 1 ; i < len ; i++){
dp[i][0] = Math.max(dp[i-1][0] , dp[i-1][1] + prices[i] - fee);
dp[i][1] = Math.max(dp[i-1][1] , dp[i-1][0] - prices[i]);
}
return Math.max(dp[len - 1][0] , dp[len - 1][1]);
}
}
- 空间优化:
class Solution {
public int maxProfit(int[] prices, int fee) {
int len = prices.length;
if(len <= 1) return 0;
int[] dp = new int[2];
dp[0] = 0;
dp[1] = -prices[0];
for(int i = 1 ; i < len ; i++){
dp[0] = Math.max(dp[0] , dp[1] + prices[i] - fee);
dp[1] = Math.max(dp[1] , dp[0] - prices[i]);
}
return Math.max(dp[0] , dp[1]);
}
}
198. 打家劫舍
dp[i] 表示前 i 间房屋能偷窃到的最高总金额,由于不能偷相邻的两家,则分两种情况:
- 偷窃第 i 间房屋,那么就不能偷窃第 i−1 间房屋,偷窃总金额为前 i−2 间房屋的最高总金额与第 i 间房屋的金额之和。
- 不偷窃第 i 间房屋,则可以偷第 i−1 间房屋,偷窃总金额为前i−1 间房屋的最高总金额。
[参考]】(https://leetcode-cn.com/problems/house-robber/solution/da-jia-jie-she-by-leetcode-solution/)
class Solution {
public int rob(int[] nums) {
int length = nums.length;
if(length == 0) return 0;
if(length == 1) return nums[0];
int[] dp = new int[length];
dp[0] = nums[0];
dp[1] = Math.max(nums[0] , nums[1]);
for(int i = 2 ; i < length ; i++){
dp[i] = Math.max(dp[i - 2] + nums[i] , dp[i - 1]);
}
return dp[length - 1];
}
}
213. 打家劫舍 II
参考 https://leetcode-cn.com/problems/house-robber-ii/solution/213-da-jia-jie-she-iidong-tai-gui-hua-jie-gou-hua-/
class Solution {
public int rob(int[] nums) {
int length = nums.length;
if(length == 0) return 0;
if(length == 1) return nums[0];
if(length == 2) return Math.max(nums[0] , nums[1]);
//不偷第一个屋子;
int res1 = subRob(Arrays.copyOfRange(nums, 1, nums.length));
//不偷最后一个屋子;
int res2 = subRob(Arrays.copyOfRange(nums, 0, nums.length - 1));
return Math.max(res1 , res2);
}
public int subRob(int[] nums) {
int length = nums.length;
if(length == 0) return 0;
if(length == 1) return nums[0];
int[] dp = new int[length];
dp[0] = nums[0];
dp[1] = Math.max(nums[0] , nums[1]);
for(int i = 2 ; i < length ; i++){
dp[i] = Math.max(dp[i - 2] + nums[i] , dp[i - 1]);
}
return dp[length - 1];
}
}
线性 DP 问题
53. 最大子序和
- dp[i] : 以i结尾的连续子数组的最大和;
class Solution {
public int maxSubArray(int[] nums) {
int len = nums.length;
if(len == 0) return 0;
if(len == 1) return nums[0];
int[] dp = new int[len];
dp[0] = nums[0];
int max = dp[0];
for(int i = 1; i < len; i++){
if(dp[i-1] >= 0) dp[i] = dp[i-1] + nums[i];
else dp[i] = nums[i];
max = Math.max(max , dp[i]);
}
return max;
}
}
- 降低空间复杂度
class Solution {
public int maxSubArray(int[] nums) {
int len = nums.length;
if(len == 0) return 0;
if(len == 1) return nums[0];
int temp = nums[0];
int max = temp;
for(int i = 1; i < len; i++){
if(temp >= 0) temp = temp + nums[i];
else temp = nums[i];
max = Math.max(max , temp);
}
return max;
}
}
918. 环形子数组的最大和
参考:https://leetcode-cn.com/problems/maximum-sum-circular-subarray/solution/wo-hua-yi-bian-jiu-kan-dong-de-ti-jie-ni-892u/
把环形数组分成了两个部分:
最大子数组 不成环 — 53题 也就是maxSum为答案
最大子数组 成环 ,那么最小子数组就不会成环 ---- (total - minSum) 则为答案
所以这最大的环形子数组和res = max(最大子数组和,数组总和-最小子数组和)
特殊情况:
如果说这数组的所有数都是负数
,那么上面的公式还需要变一下,因为这种情况,对于上面的第一种情况res会等于数组中的最大值,而对二种情况res=0(最小的子数组就是本数组,total-total=0)。所以多加一个case,判断最大子数组和是否小于0,小于0,直接返回该maxSum
class Solution {
public int maxSubarraySumCircular(int[] nums) {
int len = nums.length;
if(len == 1) return nums[0];
int sum = 0, maxSum = nums[0], curMax = 0, minSum = nums[0], curMin = 0;
for (int a : nums) {
curMax = Math.max(curMax + a, a);
maxSum = Math.max(maxSum, curMax);
curMin = Math.min(curMin + a, a);
minSum = Math.min(minSum, curMin);
sum += a;
}
return maxSum > 0 ? Math.max(maxSum, sum - minSum) : maxSum;
//return sum == minSum ? maxSum : Math.max(maxSum, sum - minSum);
}
}
双序列 DP 问题
- 从空字符串开始,是因为测试用例里会有空串,如果不特殊判定,需要dp数组多开一个,用dp[0]表示空串的情况,
建议数组多开一位,方便讨论
1143. 最长公共子序列
dp[i][j] :以 i 结尾的text1 和以 j 结尾的text2 的最长公共子序列的长度;
参考:https://leetcode-cn.com/problems/edit-distance/solution/dong-tai-gui-hua-java-by-liweiwei1419/
- 注意和下题 【718. 最长重复子数组】 的区别 ;
class Solution {
public int longestCommonSubsequence(String text1, String text2) {
if(text1 == "" || text2 == "") return 0;
int len1 = text1.length();
int len2 = text2.length();
int[][] dp = new int[len1 + 1][len2 + 1];
for(int i = 0 ; i < len1 ; i++) dp[i][0] = 0;
for(int j = 0 ; j < len2 ; j++) dp[0][j] = 0;
for(int i = 1 ; i <= len1 ; i++){
for(int j = 1 ; j <= len2 ; j++){
if(text1.charAt(i-1) == text2.charAt(j-1)) dp[i][j] = dp[i-1][j-1] + 1;
else dp[i][j] = Math.max(dp[i-1][j] , dp[i][j-1]);
}
}
return dp[len1][len2];
}
}
NC92 最长公共子序列-II
参考:https://blog.nowcoder.net/n/c670b2ffc721430d81f2a7aab63b4604?f=comment
- 除了最大长度,还要求出具体子序列;
import java.util.*;
public class Solution {
public String LCS (String s1, String s2) {
// write code here
int len1 = s1.length();
int len2 = s2.length();
if(len1 == 0 && len2 == 0) return "-1";
int[][] dp = new int[len1+1][len2+1];
for(int i = 0 ; i <= len1 ; i++){
for(int j = 0 ; j <= len2 ; j++){
if(i == 0 || j == 0) dp[i][j] = 0;
else{
if(s1.charAt(i-1) == s2.charAt(j-1)) dp[i][j] = dp[i-1][j-1] + 1;
else dp[i][j] = Math.max(dp[i-1][j] , dp[i][j-1]);
}
}
}
StringBuilder sb = new StringBuilder();
while(len1 > 0 && len2 > 0){
if(s1.charAt(len1-1) == s2.charAt(len2-1)){
sb.append(s1.charAt(len1-1));
len1--;
len2--;
}
else{
if(dp[len1-1][len2] > dp[len1][len2-1]) len1--;
else len2--;
}
}
if(sb.length() == 0) return "-1";
return sb.reverse().toString();
}
}
718. 最长重复子数组
- 状态dp[i][j]:两个数组中,以i和以j结尾的公共的子数组的最长长度;
注意和上题 【1143. 最长公共子序列】 的区别 ;
1、【最长公共子序列】是子序列,子序列各元素不需要连续;
2、本题【最长重复子数组】是子数组,子数组各元素需要连续;
所以当 text1.charAt(i) != text2.charAt(j)时,dp[i][j] =Math.max(dp[i-1][j] , dp[i][j-1]);
而nums1[i] != nums2[j] 时,dp[i][j] = 0;因为子数组各元素需要连续;
class Solution {
public int findLength(int[] nums1, int[] nums2) {
int len1 = nums1.length;
int len2 = nums2.length;
int[][] dp = new int[len1][len2];
int res = 0;
for(int i = 0 ; i < len1 ; i++){
if(nums1[i] == nums2[0]) dp[i][0] = 1;
else dp[i][0] = 0;
res = Math.max(res , dp[i][0]);
}
for(int j = 0 ; j < len2 ; j++){
if(nums1[0] == nums2[j]) dp[0][j] = 1;
else dp[0][j] = 0;
res = Math.max(res , dp[0][j]);
}
for(int i = 1 ; i < len1 ; i++){
for(int j = 1 ; j < len2 ; j++){
if(nums1[i] == nums2[j]) dp[i][j] = dp[i-1][j-1] + 1;
else dp[i][j] = 0;
res = Math.max(res , dp[i][j]);
}
}
return res;
}
}
10. 正则表达式匹配
【参考】(https://leetcode-cn.com/problems/zheng-ze-biao-da-shi-pi-pei-lcof/solution/zhu-xing-xiang-xi-jiang-jie-you-qian-ru-shen-by-je/)
dp[i][j] :以 i 结尾的 s 和以 j 结尾的 p 是否匹配;
class Solution {
public boolean isMatch(String s, String p) {
if(s.length() != 0 && p.length() == 0) return false;
boolean[][] dp = new boolean[s.length() + 1][p.length() + 1];
for(int i = 0 ; i <= s.length() ; i++){
for(int j = 0 ; j <= p.length() ; j++){
if(j == 0) dp[i][j] = (i == 0 ? true : false);
else{
if(p.charAt(j - 1) != '*'){
if(i >=1 && (s.charAt(i - 1) == p.charAt(j - 1) || p.charAt(j - 1) == '.'))
dp[i][j] = dp[i - 1][j - 1];
}else{
//表示*前面的字符出现0次的情况;
if(j >= 2)
dp[i][j] = dp[i][j - 2];
if((i>=1 && j>=2) && (s.charAt(i - 1) == p.charAt(j - 2) || p.charAt(j - 2) == '.'))
dp[i][j] = dp[i][j] | dp[i - 1][j];
}
}
}
}
return dp[s.length()][p.length()];
}
}
72. 编辑距离
参考:https://leetcode-cn.com/problems/edit-distance/solution/dong-tai-gui-hua-java-by-liweiwei1419/
- dp[i][j] :以 i-1 结尾的 word1 转换成以 j-1 结尾的 word2 所使用的最少操作数 ;
注
:由于要考虑空字符串,我们用下标i=0来表示空串,所以这里的下标 i 表示 word[i-1],同理下标 j 表示 word[j-1]。
class Solution {
public int minDistance(String word1, String word2) {
int len1 = word1.length();
int len2 = word2.length();
int[][] dp = new int[len1 + 1][len2 + 1];
for(int i = 0 ; i <= len1 ; i++) dp[i][0] = i;
for(int j = 0 ; j <= len2 ; j++) dp[0][j] = j;
for(int i = 1 ; i <= len1 ; i++){
for(int j = 1 ; j <= len2 ; j++){
if(word1.charAt(i - 1) == word2.charAt(j - 1)) dp[i][j] = dp[i-1][j-1];
else dp[i][j] = Math.min(Math.min(dp[i][j-1] , dp[i-1][j]) ,dp[i-1][j-1]) + 1;
}
}
return dp[len1][len2];
}
}
- 空字符串单独讨论【不推荐:讨论麻烦】
class Solution {
public int minDistance(String word1, String word2) {
int len1 = word1.length();
int len2 = word2.length();
if(len1 == 0 || len2 == 0) return len1 == 0 ? len2 : len1;
int[][] dp = new int[len1][len2];
if(word1.charAt(0) == word2.charAt(0)) dp[0][0] = 0;
else dp[0][0] = 1;
for(int i = 1 ; i < len1 ; i++){
if(word1.charAt(i) == word2.charAt(0)) dp[i][0] = i;
else dp[i][0] = dp[i-1][0] + 1;
}
for(int j = 1 ; j < len2 ; j++){
if(word1.charAt(0) == word2.charAt(j)) dp[0][j] = j;
else dp[0][j] = dp[0][j-1] + 1;
}
for(int i = 1 ; i < len1 ; i++){
for(int j = 1 ; j < len2 ; j++){
if(word1.charAt(i) == word2.charAt(j)) dp[i][j] = dp[i-1][j-1];
else{
dp[i][j] = Math.min(Math.min(dp[i][j-1] , dp[i-1][j]) , dp[i-1][j-1]) +1;
}
}
}
return dp[len1-1][len2-1];
}
}
区间 DP 与划分型 DP 问题
5. 最长回文子串
dp[i][j],表示子串s[i;j]是否是回文串。
参考 https://blog.youkuaiyun.com/qq_42647047/article/details/108652634
class Solution {
public String longestPalindrome(String s) {
int len = s.length();
// 特殊情况判段
if (len < 2){
return s;
}
int maxLen = 1;
int begin = 0;
// 1. 状态定义
// dp[i][j] 表示s[i...j] 是否是回文串
// 2. 初始化
boolean[][] dp = new boolean[len][len];
for (int i = 0; i < len; i++) {
dp[i][i] = true;
}
char[] chars = s.toCharArray();
// 3. 状态转移
// 注意:先填左下角
// 填表规则:先一列一列的填写,再一行一行的填,保证左下方的单元格先进行计算
for (int j = 1;j < len;j++){
for (int i = 0; i < j; i++) {
// 头尾字符不相等,不是回文串
if (chars[i] != chars[j]){
dp[i][j] = false;
}else {
// 相等的情况下
// 考虑头尾去掉以后没有字符剩余,或者剩下一个字符的时候,肯定是回文串
if (j - i < 3){
dp[i][j] = true;
}else {
// 状态转移
dp[i][j] = dp[i + 1][j - 1];
}
}
// 只要dp[i][j] == true 成立,表示s[i...j] 是否是回文串
// 此时更新记录回文长度和起始位置
if (dp[i][j] && j - i + 1 > maxLen){
maxLen = j - i + 1;
begin = i;
}
}
}
// 4. 返回值
return s.substring(begin,begin + maxLen);
}
}
647. 回文子串
[参考,还有中心扩散法(更好)】(https://blog.youkuaiyun.com/qq_42647047/article/details/108652634)
class Solution {
public int countSubstrings(String s) {
if(s == "") return 0;
int n = s.length();
if(n == 1) return 1;
//dp[i][j]:s[i : j]是不是回文串,i <= j;
boolean[][] dp = new boolean[n][n];
//对角线上都为true;
for(int i = 0 ; i < n ; i++) dp[i][i] = true;
int count = n;
for(int j = 1 ; j < n ; j++){
//因为i <= j,且对角线已初始化,所以初始化j = i + 1;
for(int i = 0 ; i < j ; i++){
dp[i][j] = (s.charAt(i) == s.charAt(j)) && ((j - i)<=2 || dp[i+1][j-1]);
if(dp[i][j] == true) count++;
}
}
return count;
}
}
312. 戳气球
【区间DP】:由小区间推出大区间;
求dp[i][j]代码套路就是2层循环起步
,第一层是遍历 i 到 j 的宽度,第二层是遍历左端点 i (由第一层的宽度和第二层的左端点,可等价等到 j 右端点)。这样从小到大得到最终解。
[区间DP例题】(https://leetcode-cn.com/problems/burst-balloons/solution/yi-wen-tuan-mie-qu-jian-dp-by-bnrzzvnepe-2k7b/)
class Solution {
public int maxCoins(int[] nums) {
int n = nums.length;
int[] assist = new int[n + 2];
assist[0] = 1;
assist[n + 1] = 1;
int dp[][] = new int[n+2][n+2];
for(int i = 1 ; i <= n ; i++) assist[i] = nums[i - 1];
for(int i = 0 ; i < n; i++) dp[i][i+2] = assist[i]*assist[i+1]*assist[i+2];
//循环区间长度
for(int len = 3 ; len <= n+2 ; len++){
//循环左边界(即,循环固定长度的区间)
for(int i = 0 ; i <= (n + 2) -len ; i++){
//循环区间里的k,确定一个区间 所能获得硬币的最大数量。
for(int k = i + 1 ; k < i + len - 1 ; k++){
dp[i][i+len-1] = Math.max(dp[i][i+len-1] , dp[i][k] + assist[i]*assist[k]*assist[i+len-1] + dp[k][i+len-1]);
}
}
}
return dp[0][n+1];
}
}
树形 DP 问题
动态规划问题画出的状态图本身就是一个树状图,然后动态规划从底向上求解
,即对树而言,是后序遍历
;
- 树形问题的
初始化
,即是递归终止条件:当遍历到的结点为空节点时; - 一个经验是:树中的问题通常都是自底向上求解:通过后序遍历求得问题的答案,「无后效性」和「后序遍历」几乎是树形 dp 的解题思想。
337. 打家劫舍 III
[参考】(https://leetcode-cn.com/problems/house-robber-iii/solution/da-jia-jie-she-iii-by-leetcode-solution/)
class Solution {
HashMap<TreeNode , Integer> f;
HashMap<TreeNode , Integer> g;
public int rob(TreeNode root) {
f = new HashMap<>();
g = new HashMap<>();
if(root == null) return 0;
dfs(root);
return Math.max(f.getOrDefault(root , 0) , g.getOrDefault(root , 0));
}
void dfs(TreeNode root){
if(root == null) return;
dfs(root.left);
dfs(root.right);
f.put(root , g.getOrDefault(root.left , 0) + g.getOrDefault(root.right , 0) + root.val);
g.put(root , Math.max(f.getOrDefault(root.left , 0) , g.getOrDefault(root.left , 0)) + Math.max(f.getOrDefault(root.right , 0) , g.getOrDefault(root.right , 0)));
}
}
- 空间优化
public class Solution {
public int rob(TreeNode root) {
int[] res = dfs(root);
return Math.max(res[0], res[1]);
}
private int[] dfs(TreeNode root) {
if (root == null) {
return new int[2];
}
// 分类讨论的标准是:当前结点偷或者不偷
// 需要后序遍历,所以先计算左右子结点,然后计算当前结点的状态值
int[] left = dfs(root.left);
int[] right = dfs(root.right);
// dp[0]:以当前 node 为根结点的子树能够偷取的最大价值,规定 node 结点不偷
// dp[1]:以当前 node 为根结点的子树能够偷取的最大价值,规定 node 结点偷
int[] dp = new int[2];
// 根结点不打劫 = max(左子树不打劫, 左子树打劫) + max(右子树不打劫, 右子树打劫)
dp[0] = Math.max(left[0], left[1]) + Math.max(right[0], right[1]);
// 根结点打劫 = 左右孩子结点都不能打劫
dp[1] = left[0] + right[0] + root.val;
return dp;
}
}
124. 二叉树中的最大路径和【线性DP】
[参考视频】(https://leetcode-cn.com/problems/binary-tree-maximum-path-sum/solution/er-cha-shu-zhong-de-zui-da-lu-jing-he-by-leetcode-/)
- 首先递归函数
dfs(TreeNode root)
,为以root节点为根节点,且没有弯曲
的最大路径的和的; - 则通过节点root
且没有弯曲
的最大路径和,为以下前1、2情况的最大值,3是通过root节点的最大路径和:
1、root.val + dfs(root.left);
2、root.val + dfs(root.right);
3、dfs(root.left) + root.val + dfs(root.right); - 定义一个全局变量
res
,表示二叉树中的最大路径和,初始化为Integer.MIN_VALUE
,更新res = Math.max(tempMaxPathSum , res);
,初始化为Integer.MIN_VALUE
不为0
,是因为,如果二叉树的值全为负数,则最大路径和为负数,res初始化为0,显然会出错; - 1、2、两种情况的最大值,作为以root节点为根节点的最大路径和,继续向上返回,作为root父节点【如果有父节点的话】的一个子节点的最大路径和;
- 另外如果一个节点的最大路径和是负数,则当做0处理,因为负数对一个节点的最大路径和不做贡献,反而会使路径和变小,即,
int left = Math.max(dfs(root.left) , 0);
class Solution {
int res = Integer.MIN_VALUE;
public int maxPathSum(TreeNode root) {
if(root == null) return 0;
dfs(root);
return res;
}
int dfs(TreeNode root){
if(root == null) return 0;
int left = Math.max(dfs(root.left) , 0);
int right = Math.max(dfs(root.right) , 0);
int returnVal = Math.max(left , right) + root.val;
int tempMaxPathSum = left + root.val + right;
res = Math.max(res , tempMaxPathSum);
return returnVal;
}
}
543. 二叉树的直径
dfs(root):root节点的深度
。
[参考】(https://leetcode-cn.com/problems/diameter-of-binary-tree/solution/er-cha-shu-de-zhi-jing-by-leetcode-solution/)
class Solution {
int res;
public int diameterOfBinaryTree(TreeNode root) {
if(root == null) return 0;
//递归函数,返回的是root的深度;
dfs(root);
return res - 1;
}
int dfs(TreeNode root){
if(root == null) return 0;
int leftDepth = dfs(root.left);
int rightDepth = dfs(root.right);
res = Math.max(res , leftDepth + rightDepth + 1);
return Math.max(leftDepth , rightDepth) + 1;
}
}
背包问题
416. 分割等和子集
思路参考](https://leetcode-cn.com/problems/partition-equal-subset-sum/solution/fen-ge-deng-he-zi-ji-by-leetcode-solution/)
参考,更详细的题解 和 相似例题](https://leetcode-cn.com/problems/partition-equal-subset-sum/solution/0-1-bei-bao-wen-ti-xiang-jie-zhen-dui-ben-ti-de-yo/)
class Solution {
public boolean canPartition(int[] nums) {
if(nums.length <= 1) return false;
int sum = 0;
int maxValue = 0;
for(int val : nums){
sum += val;
maxValue = Math.max(maxValue , val);
}
if((sum & 1) == 1) return false;
int target = sum >> 1;
if(maxValue > target) return false;
boolean[][] dp = new boolean[nums.length][target + 1];
dp[0][nums[0]] = true;
for (int i = 0; i < nums.length; i++) {
dp[i][0] = true;
}
for(int i = 1 ; i < nums.length ; i++){
for(int j = 0 ; j <= target ; j++){
dp[i][j] = dp[i-1][j];
if(nums[i] <= j) dp[i][j] = dp[i][j] || dp[i-1][j - nums[i]];
}
}
return dp[nums.length-1][target];
}
}
- 空间优化
class Solution {
public boolean canPartition(int[] nums) {
if(nums.length <= 1) return false;
int sum = 0;
int maxValue = 0;
for(int val : nums){
sum += val;
maxValue = Math.max(maxValue , val);
}
if((sum & 1) == 1) return false;
int target = sum >> 1;
if(maxValue > target) return false;
boolean[] dp = new boolean[target + 1];
dp[0] = true;//前面的代表数组的一维取消了,这里的初始dp[0]实际上i=0时的dp[0][0];
dp[nums[0]] = true;//第一行代表从[0~0]下标中选,即选nums[0],此时第一行只有dp[0][0]和dp[0][nums[0]]为true,其他未false;
for(int i = 1 ; i < nums.length ; i++){
for(int j = target ; j > nums[i] ; j--){
dp[j] = dp[j] || dp[j - nums[i]];
}
}
return dp[target];
}
}
494. 目标和
[参考】(https://leetcode-cn.com/problems/target-sum/solution/494-mu-biao-he-dong-tai-gui-hua-zhi-01be-78ll/)
注意:所有符号为+的元素和x,可以为0,即没有符号为正的,全是负的;这对dp的初始化很关键。
class Solution {
public int findTargetSumWays(int[] nums, int target) {
int sum = 0;
for(int val : nums) sum += val;
if(target > sum || (target + sum) % 2 == 1) return 0;
int amount = (target + sum) / 2;
int n = nums.length;
int[][] dp = new int[n][amount + 1];
if(nums[0] != 0){
dp[0][0] = 1;
if(nums[0] <= amount) dp[0][nums[0]] = 1;
}
else dp[0][0] = 2;
for(int i = 1 ; i < n ; i++){
for(int j = 0 ; j <= amount ; j++){
dp[i][j] = dp[i-1][j];
if(nums[i] <= j) dp[i][j] += dp[i-1][j-nums[i]];
}
}
return dp[n-1][amount];
}
}
- 空间优化
class Solution {
public int findTargetSumWays(int[] nums, int target) {
int sum = 0;
for(int val : nums) sum += val;
if(target > sum || (target + sum) % 2 == 1) return 0;
int amount = (target + sum) / 2;
int n = nums.length;
int[] dp = new int[amount + 1];
if(nums[0] != 0){
dp[0]=1;
if(nums[0] <= amount) dp[nums[0]] = 1;
}
else dp[0] = 2;
for(int i = 1 ; i < n ; i++){
for(int j = amount ; j >= nums[i] ; j--){
dp[j] = dp[j] + dp[j-nums[i]];
}
}
return dp[amount];
}
}
322. 零钱兑换
[参考】(https://leetcode-cn.com/problems/coin-change/solution/javadi-gui-ji-yi-hua-sou-suo-dong-tai-gui-hua-by-s/)
- 完全背包
class Solution {
public int coinChange(int[] coins, int amount) {
if(amount == 0) return 0;
int[] dp = new int[amount + 1];
//初始化dp数组为最大值,兑换的所有硬币都是1元的,数量最大,最大是amount,即不可能达到amount+1;
Arrays.fill(dp, amount + 1);
//当金额为0时需要的硬币数目为0
dp[0] = 0;
for(int i = 0 ; i < coins.length ; 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];
}
}
- 正常dp
class Solution {
public int coinChange(int[] coins, int amount) {
if(amount == 0) return 0;
int[] dp = new int[amount + 1];
dp[0] = 0;
for(int i = 1 ; i <= amount ; i++){
int min = Integer.MAX_VALUE;
for(int j = 0 ; j < coins.length ; j++){
if(i - coins[j] >= 0 && dp[i-coins[j]] < min){
min = dp[i-coins[j]] + 1;
}
}
dp[i] = min;
}
return dp[amount] == Integer.MAX_VALUE ? -1 : dp[amount];
}
}
139. 单词拆分
转化为是否可以用 wordDict 中的词组合成 s,完全背包问题,并且为“考虑排列顺序的完全背包问题”,外层循环为 target ,内层循环为选择池 wordDict。
[参考】(https://leetcode-cn.com/problems/word-break/solution/yi-tao-kuang-jia-jie-jue-bei-bao-wen-ti-kchg9/)
class Solution {
public boolean wordBreak(String s, List<String> wordDict) {
boolean[] dp = new boolean[s.length() + 1];
dp[0] = true;
for(int i = 1 ; i <= s.length() ; i++){
for(String word : wordDict){
int size = word.length();
if(i - size >= 0 && s.substring(i - size , i).equals(word))
dp[i] = dp[i] || dp[i - size];
}
}
return dp[s.length()];
}
}
377. 组合总和 Ⅳ
[参考】(https://leetcode-cn.com/problems/combination-sum-iv/solution/dai-ma-sui-xiang-lu-377-zu-he-zong-he-iv-pj9s/)
class Solution {
public int combinationSum4(int[] nums, int target) {
int[] dp = new int[target + 1];
dp[0] = 1;
for(int j = 1 ; j <= target ; j++){
for(int i = 0 ; i < nums.length ; i++){
if(j >= nums[i]) dp[j] = dp[j] + dp[j - nums[i]];
}
}
return dp[target];
}
}
斐波那契数列(用动态规划也可,斐波那契数列可看作是动态规划的优化空间的版本)
70. 爬楼梯
class Solution {
public int climbStairs(int n) {
if(n < 3) return n;
int f1 = 1;
int f2 = 2;
int fn = f1 + f2;
for(int i = 4 ; i <= n ; i++){
f1 = f2;
f2 = fn;
fn = f1 + f2;
}
return fn;
}
}
区间
56. 合并区间
[重叠区间、合并区间、插入区间 讲解】(https://mp.weixin.qq.com/s/ioUlNa4ZToCrun3qb4y4Ow)
class Solution {
public int[][] merge(int[][] intervals) {
if(intervals.length == 1) return intervals;
Arrays.sort(intervals , (v1 , v2) -> v1[0] - v2[0]);
ArrayList<int[]> res = new ArrayList<int[]>();
res.add(intervals[0]);
//index:res中最后一个元素的下标
int index = 0;
for(int i = 1 ; i < intervals.length ; i++){
if(intervals[i][0] > res.get(index)[1]){
res.add(intervals[i]);
//index++ 要放在下面,不能放在if()语句里,即if(intervals[i][0] > res.get(index)[1]),否则会出现越界错误;
//因为不管if语句成不成立,都会执行if()语句里的判断,则在不成立的情况下index也加了1,导致错误
index++;
}
else{//确定res中最后一个元素的右边界
res.get(index)[1] = Math.max(res.get(index)[1] , intervals[i][1]);
}
}
return res.toArray(new int[res.size()][2]);
}
}
132. 会议室1
给定一个会议时间安排的数组intervals,每个会议时间都会包括开始和结束的时间intervals[i]=[starti,endi],请你判断一个人是否能够参加这里面的全部会议。
示例 1::
输入: intervals = [[0,30],[5,10],[15,20]]
输出: false
解释: 存在重叠区间,一个人在同一时刻只能参加一个会议。
示例 2::
输入: intervals = [[7,10],[2,4]]
输出: true
解释: 不存在重叠区间。
题解思路
判断数组是否存在重叠区间
public class CanAttendMeetingsTest {
public static boolean canAttendMeetings(int[][] intervals) {
Arrays.sort(intervals, (o1,o2)->o1[0]-o2[0]);
for (int i = 1; i < intervals.length; i++) {
if (intervals[i][0] < intervals[i - 1][1]) {
return false;
}
}
return true;
}
}
253. 会议室II:最小堆
参考:https://blog.youkuaiyun.com/yinianxx/article/details/105785284
1.先将数组进行升序排序
2.将会议的结束时间存入优先队列(升序排序)
,队首元素为最早会议结束时间(end)【使用小顶堆的原因,可使会议室数量最少】,如果另一场会议的开始时间早于end,则没有空闲会议室,需要新开一间;否则,弹出队首元素。
3.最后队列中的剩余元素为会议室数量
class Solution {
public int minMeetingRooms(int[][] intervals) {
if(intervals == null || intervals.length == 0){
return 0;
}
Arrays.sort(intervals,(o1,o2)->o1[0]-o2[0]);
PriorityQueue<Integer> queue = new PriorityQueue<>((o1,o2)->o1-o2);
queue.offer(intervals[0][1]);
for(int i = 1;i < intervals.length;i++){
if(intervals[i][0]>=queue.peek()){
queue.poll();
}
queue.offer(intervals[i][1]);
}
return queue.size();
}
}
时间复杂度:
排序:O(NlogN);插入和删除:O(logN)
所以总的时间复杂度为O(NlogN)
空间复杂度: O(N)
单调栈
84. 柱状图中最大的矩形
[参考】(https://leetcode-cn.com/problems/largest-rectangle-in-histogram/solution/dong-hua-yan-shi-dan-diao-zhan-84zhu-zhu-03w3/)
class Solution {
public int largestRectangleArea(int[] heights) {
int n = heights.length;
if(n == 0) return 0;
int[] newHeights = new int[n + 2];
newHeights[0] = 0;
newHeights[n + 1] = 0;
for(int i = 1 ; i <= n ; i++){
newHeights[i] = heights[i - 1];
}
int res = 0;
Deque<Integer> stack = new LinkedList<>();
for(int i = 0 ; i < n + 2 ; i++){
while(!stack.isEmpty() && newHeights[i] < newHeights[stack.peek()]){
int hight = newHeights[stack.pop()];
int left = stack.peek();
int tempArea = hight * (i - left - 1);
res = Math.max(res , tempArea);
}
stack.push(i);
}
return res;
}
}
85. 最大矩形
-
在84题的基础上
[参考】(https://leetcode-cn.com/problems/maximal-rectangle/solution/zui-da-ju-xing-by-leetcode-solution-bjlu/) -
首先计算出矩阵的每个元素的左边连续 1 的数量,使用二维数组 left 记录,其中
left[i][j]
为矩阵第 i 行第 j 列元素的左边连续 1 的数量。 -
比如其中一列是:
-
将其转换成柱状图,高度为该点的值,即该点左边有多少个连续的1,如下:
-
则,从该列转换成的柱状图,求该列只包含 1 的最大矩形,即求柱状图中最大的矩形【即84题】,对每列转换的柱状图求最大的矩形,然后取最大值,即为结果。
class Solution {
public int maximalRectangle(char[][] matrix) {
int m = matrix.length;
if (m == 0) {
return 0;
}
int n = matrix[0].length;
int[][] leftArray = new int[m][n];
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
if (matrix[i][j] == '1') {
leftArray[i][j] = (j == 0 ? 0 : leftArray[i][j - 1]) + 1;
}
}
}
int res = 0;
for (int j = 0; j < n; j++) { // 对于每一列,使用基于柱状图的方法
int[] newHeights = new int[m + 2];
newHeights[0] = 0;
newHeights[m + 1] = 0;
for(int i = 1 ; i <= m ; i++){
newHeights[i] = leftArray[i - 1][j];
}
Deque<Integer> stack = new LinkedList<>();
for(int i = 0 ; i < m + 2 ; i++){
while(!stack.isEmpty() && newHeights[i] < newHeights[stack.peek()]){
int hight = newHeights[stack.pop()];
int left = stack.peek();
int tempArea = hight * (i - left - 1);
res = Math.max(res , tempArea);
}
stack.push(i);
}
//栈可清除,也可不清除,因为每一列处理完,栈都会全部弹出
//stack.clear();
}
return res;
}
}
739. 每日温度
参考84题单调栈思路即可
- 栈存放的是下标;
- 在每一个元素放入栈之前,都要和栈顶元素比较,若大于栈顶元素,则可以确定栈顶元素的“更高的温度”的一天,并弹出确定的栈顶元素,然后循环和栈顶元素比较,直到栈为空或小于等于栈顶元素;
若小于等于栈顶元素,则直接入栈;由上可得栈里保留的是递减序列
;
class Solution {
public int[] dailyTemperatures(int[] temperatures) {
int length = temperatures.length;
if(length == 1) return new int[]{0};
Deque<Integer> stack = new LinkedList<>();
int[] res = new int[length];
for(int i = 0 ; i < length ; i++){
while(!stack.isEmpty() && temperatures[i] > temperatures[stack.peek()]){
int cur = stack.pop();
res[cur] = i - cur;
}
stack.push(i);
}
return res;
}
}
42. 接雨水
- 前面的单调栈里,存储的都是点掉递增;本题是单调递减;
[参考】(https://leetcode-cn.com/problems/trapping-rain-water/solution/jie-yu-shui-by-leetcode/)
class Solution {
public int trap(int[] height) {
if(height == null || height.length <= 2) return 0;
Deque<Integer> stack = new LinkedList<Integer>();
int res = 0;
for(int i = 0 ; i < height.length ; i++){
while(!stack.isEmpty() && height[i] > height[stack.peek()]){
int top = stack.pop();
if(stack.isEmpty()) break;
if(height[top] == height[stack.peek()]) continue;
int boundHeight = Math.min(height[i] , height[stack.peek()]) - height[top];
int distancce = i - stack.peek() - 1;
int area = boundHeight * distancce;
res += area;
}
stack.push(i);
}
return res;
}
}
ZJ求区间最小数乘区间和的最大值
参考https://mp.weixin.qq.com/s/UFv7pt_djjZoK_gzUBrRXA
Java注释版:https://www.yuque.com/docs/share/b931ffef-46c6-4cb4-a827-60577e476480?#
- 暴力
枚举的每个元素(设为x)作为区间最小值,在x左右两侧找到第一个比x小的元素,分别记录左右边界的下标为l,r,寻找边界时计算当前区间的和
class Solution1 {
public int calculateIntervalSum(int[] nums) {
int n = nums.length;
//前缀和便于快速求区间和,例如求[l,r]区间和=dp[r+1]-dp[l]
int[] prefixSum = new int[n + 1];
prefixSum[0] = 0;
for (int i = 1; i <= n; i++) {
prefixSum[i] = prefixSum[i-1] + nums[i-1];
}
int res = 0;
for (int i = 0; i < n; i++) {
int left = i - 1;
int right = i + 1;
while (left >= 0 && nums[left] > nums[i]) left--;
while (right < n && nums[right] > nums[i]) right++;
res = Math.max(res , (prefixSum[right] - prefixSum[left + 1]) * nums[i]);
}
return res;
}
}
- 单调栈
class Solution2 {
public int calculateIntervalSum(int[] nums) {
int len = nums.length;
// 先求前缀和
int[] preSum = new int[len+1];
preSum[0] = 0;
int res = Integer.MIN_VALUE;
for (int i = 1; i <= len; i++) preSum[i] = preSum[i-1] + nums[i-1];
// 单调递增栈
Deque<Integer> stack = new LinkedList<>();
for (int i = 0; i < len; i++) {
while(!stack.isEmpty() && nums[stack.peek()] >= nums[i]){
int smellest = nums[stack.poll()];
//(left , right)内是符合题意的一个区间,这里用开区间表示,不包括left和right;
int right = i;
int left = stack.isEmpty() ? -1 : stack.peek();
res = Math.max(res , (preSum[right] - preSum[left+1])*smellest);
}
stack.push(i);
}
//如果数组部分区间向右一直递增, 一直没有遇到更小元素,栈就不会谈完,下面就是补充这种情况
while(!stack.isEmpty()){
int smellest = nums[stack.poll()];
int right = len;
int left = stack.isEmpty() ? -1 : stack.peek();
res = Math.max(res , (preSum[right] - preSum[left+1])*smellest);
}
return res;
}
}
注释版
public int calculateIntervalSum(int[] nums) {
// 先求前缀和
int[] preSums = new int[nums.length + 1];
preSums[0] = 0;
for (int i = 1; i <= nums.length; i++) {
preSums[i] = preSums[i - 1] + nums[i - 1];
}
// 单调递增栈
Stack<Integer> stack = new Stack<>();
// 区间乘积结果
int res = 0;
// 维护单调递增栈, 栈迭代过程中, 需要 "小 中心 小" 模式的组合,
// 然后计算出 "[小+1, 中心, 小-1]"区间和与中心最小数之间的乘积.
// 其中使用栈维护的递增关系, 找到中心元素前第一个比中心元素小的元素位置,
// 这个比中心元素小的元素位置与中心元素之间的元素都是比中心元素更大的
// 这就维持了中心元素为区间最小元素的关系.
// 同理, 在寻找到中心元素后面第一个比中心元素小的元素位置时, 中心元素与后面
// 第一个最小元素之间的元素都是比中心元素大的, 这也维持了中心元素时区间最小元素的关系
// 上面找到中心元素左右两边的第一个小元素形成的区间就使得中心元素为该区间的最小元素.
for (int i = 0; i < nums.length; i++) {
// 获取当前栈顶元素, 以栈顶元素为中心, 后续元素nums[i] 比他小, 那么right = i - 1位置之前元素都是比他大的, 维持了栈顶元素为最小值的区间
// 同时,再获取栈顶元素下面一个的元素,因为单调递增栈,因此left = 次栈顶元素index + 1 位置之后的元素都是比栈顶元素大的
while (!stack.isEmpty() && nums[stack.peek()] >= nums[i]) {
// 弹出栈顶元素作为中心最小元素
int smallest = nums[stack.pop()];
// 次栈顶元素索引位置, 如果栈顶为空, 意味着左边所有元素都比中心元素大, 所以前缀和从第0个位置计算
int subSmallIndex = stack.isEmpty() ? -1 : stack.peek();
int right = i - 1;
// 左右边界 int left = subSmallIndex + 1; int right = i - 1;
// 计算结果i, 本来是presum[right] - presum[subSmallIndex], 但是前缀和为了处理从0开始计算前缀和的情况往后移动了一位
// 因此计算前缀和时 preSums[right + 1] - preSums[subSmallIndex + 1]
res = Math.max(res, (preSums[right + 1] - preSums[subSmallIndex + 1]) * smallest);
}
// 维护单调递增栈, 以栈顶元素为中心的元素已经计算过了所以前面可以弹出.
stack.push(i);
}
// 上面是计算中心元素是通过找到比中心元素小来圈定范围, 但是如果数组部分区间向右一致递增, 一直没有遇到更小元素
// 下面就是补充这种情况
while (!stack.isEmpty()) {
// 右边都是大元素, 因此栈顶元素就是最小元素
int smallest = nums[stack.pop()];
// 寻找到左边第一个小元素位置 left = subSmallIndex + 1, right = nums.length - 1;
int subSmallIndex = stack.isEmpty() ? -1 : stack.peek();
// 右边元素都是比当前中心元素大的, 因此right = nums.length - 1
int right = nums.length - 1;
res = Math.max(res, (preSums[right + 1] - preSums[subSmallIndex + 1]) * smallest);
}
return res;
}
树
94. 二叉树的中序遍历
https://leetcode-cn.com/problems/binary-tree-inorder-traversal/solution/er-cha-shu-de-zhong-xu-bian-li-by-leetcode-solutio/#comment
- 递归
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode() {}
* TreeNode(int val) { this.val = val; }
* TreeNode(int val, TreeNode left, TreeNode right) {
* this.val = val;
* this.left = left;
* this.right = right;
* }
* }
*/
class Solution {
public List<Integer> inorderTraversal(TreeNode root) {
ArrayList<Integer> res = new ArrayList<>();
if(root == null) return res;
dfs(root , res);
return res;
}
void dfs(TreeNode root , List<Integer> res){
if(root == null) return;
dfs(root.left , res);
res.add(root.val);
dfs(root.right , res);
}
}
- 迭代
class Solution {
public List<Integer> inorderTraversal(TreeNode root) {
ArrayList<Integer> res = new ArrayList<>();
if(root == null) return res;
Deque<TreeNode> stack = new LinkedList<>();
while(root != null || !stack.isEmpty()){
while(root != null){
stack.push(root);
root = root.left;
}
TreeNode node = stack.pop();
res.add(node.val);
root = node.right;
}
return res;
}
}
98. 验证二叉搜索树
- 边中序遍历边判断
/**
* 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 {
//注意用long型,因为测试用例太极限
long pre = Long.MIN_VALUE;
public boolean isValidBST(TreeNode root) {
if(root == null){//空树是二叉搜索树
return true;
}
//如果左子树不是二叉搜索树,则返回false;
if(!isValidBST(root.left)) return false;
if(root.val <= pre) return false;
pre = root.val;
//走到这,说明左子树是二叉搜索树,则返回判断右子树是不是二叉搜索树即可;
return isValidBST(root.right);
}
}
- 或思路简单点,先中序遍历,再对遍历的结果判断
class Solution {
List<Integer> res = new ArrayList<>();
public boolean isValidBST(TreeNode root) {
if(root==null)
return true;
inOrder(root);
for(int i=1;i<res.size();i++){
if(res.get(i)<=res.get(i-1)){
return false;
}
}
return true;
}
private void inOrder(TreeNode root){
if(root!=null){
inOrder(root.left);
res.add(root.val);
inOrder(root.right);
}
}
}
101. 对称二叉树
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode() {}
* TreeNode(int val) { this.val = val; }
* TreeNode(int val, TreeNode left, TreeNode right) {
* this.val = val;
* this.left = left;
* this.right = right;
* }
* }
*/
class Solution {
public boolean isSymmetric(TreeNode root) {
if(root == null) return true;
return dfs(root , root);
}
boolean dfs(TreeNode root1 , TreeNode root2){
if(root1 == null && root2 == null) return true;
if(root1 == null || root2 == null) return false;
if(root1.val != root2.val) return false;
return dfs(root1.left , root2.right) && dfs(root1.right , root2.left);
}
}
102. 二叉树的层序遍历
参考,[面试题32】(https://blog.youkuaiyun.com/qq_42647047/article/details/111303308)
- BFS
class Solution {
public List<List<Integer>> levelOrder(TreeNode root) {
List<List<Integer>> res = new ArrayList<>();
if(root == null) return res;
Queue<TreeNode> q = new LinkedList<>();
q.add(root);
while(!q.isEmpty()){
List<Integer> temp = new ArrayList<>();
int size = q.size();
for(int i = 0 ; i < size ; i++){
TreeNode node = q.remove();
temp.add(node.val);
if(node.left != null) q.add(node.left);
if(node.right != null) q.add(node.right);
}
res.add(temp);
}
return res;
}
}
- DFS
class Solution {
List<List<Integer>> res = new ArrayList<>();
public List<List<Integer>> levelOrder(TreeNode root) {
if(root == null) return res;
dfs(root , 0);
return res;
}
void dfs(TreeNode root , int depth){
if(root == null) return;
//之所以在回溯的时候创建new ArrayList<>(),是因为一开始不知道树的深度,只能边遍历边创建;
if(res.size() == depth) res.add(new ArrayList<>());
res.get(depth).add(root.val);
dfs(root.left , depth+1);
dfs(root.right , depth+1);
}
}
104. 二叉树的最大深度
- 后序遍历
class Solution {
public int maxDepth(TreeNode root) {
if(root == null) return 0;//空树的深度为0;
int leftDeapth = maxDepth(root.left);
int rightDeapth = maxDepth(root.right);
return Math.max(leftDeapth , rightDeapth) + 1;
}
}
- BFS
class Solution {
public int maxDepth(TreeNode root) {
if (root == null) {
return 0;
}
Queue<TreeNode> queue = new LinkedList<TreeNode>();
queue.offer(root);
int ans = 0;
while (!queue.isEmpty()) {
int size = queue.size();
ans++;
for(int i = 0 ; i < size ; i++) {
TreeNode node = queue.poll();
if (node.left != null) {
queue.offer(node.left);
}
if (node.right != null) {
queue.offer(node.right);
}
}
}
return ans;
}
}
105. 从前序与中序遍历序列构造二叉树
参考,面试题7
/**
* 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 {
HashMap<Integer, Integer> indexMap;
public TreeNode buildTree(int[] preorder, int[] inorder) {
if(preorder.length == 0) return null;
indexMap = new HashMap<Integer, Integer>();
for (int i = 0; i < inorder.length; i++) {
indexMap.put(inorder[i], i);
}
TreeNode root = reBuild(preorder , 0 , preorder.length - 1 , inorder , 0 , inorder.length - 1);
return root;
}
TreeNode reBuild(int[] preorder, int leftPre , int rightPre , int[] inorder , int leftIn , int rightIn){
//当遍历到这种条件时,是遍历到空节点时,空节点自然返回null
if(leftPre > rightPre || leftIn > rightIn) return null;
int left = preorder[leftPre];
TreeNode root = new TreeNode(left);
int split = indexMap.get(left);
//用HashMap替换下面的for循环,来快速定位inorder中根节点的位置,可大大降低时间复杂度。
// int split = leftIn;
// for( ; split < inorder.length ; split++){
// if(inorder[split] == left) break;
// }
root.left = reBuild(preorder , leftPre + 1 , leftPre + (split - leftIn) , inorder , leftIn , split - 1);
root.right = reBuild(preorder , leftPre + (split - leftIn) + 1 , rightPre , inorder , split + 1 , rightIn);
return root;
}
}
114. 二叉树展开为链表
124. 二叉树中的最大路径和【线性DP】
由上图的,将一个二叉树简化成一个二叉树单元来分析,一个二叉树单元的最大路径和,为上述3种情况的最大值,而3种情况中设计子节点的最大路径和,即,用递归来计算子节点的最大路径和,递归结束条件,空节点的最大路径和为0,具体如下:
- 首先递归函数
getMaxPathSum(TreeNode root)
,为以root节点为根节点,且没有弯曲
的最大路径的和的; - 则通过一个节点root的最大路径和
tempMaxPathSum
,为以下三种情况的最大值:
1、root.val + getMaxPathSum(root.left);
2、root.val + getMaxPathSum(root.right);
3、getMaxPathSum(root.left) + root.val + getMaxPathSum(root.right); - 定义一个全局变量
res
,表示二叉树中的最大路径和,初始化为Integer.MIN_VALUE
,更新res = Math.max(tempMaxPathSum , res);
,初始化为Integer.MIN_VALUE
不为0
,是因为,如果二叉树的值全为负数,则最大路径和为负数,res初始化为0,显然会出错; - 1、2、两种情况的最大值,作为以root节点为根节点的最大路径和,继续向上返回,作为root父节点【如果有父节点的话】的一个子节点的最大路径和;
- 另外如果一个节点的最大路径和是负数,则当做0处理,因为负数对一个节点的最大路径和不做贡献,即,
int leftMaxPathSum = Math.max(getMaxPathSum(root.left) , 0);
class Solution {
int res = Integer.MIN_VALUE;
public int maxPathSum(TreeNode root) {
if(root == null) return 0;
int returnVal = getMaxPathSum(root);
return res;
}
int getMaxPathSum(TreeNode root){
if(root == null) return 0;
int leftMaxPathSum = Math.max(getMaxPathSum(root.left) , 0);
int rightMaxPathSum = Math.max(getMaxPathSum(root.right) , 0);
int returnVal = Math.max(leftMaxPathSum , rightMaxPathSum) + root.val;
int tempMaxPathSum = Math.max(returnVal , leftMaxPathSum + root.val + rightMaxPathSum);
res = Math.max(tempMaxPathSum , res);
return returnVal;
}
}
226. 翻转二叉树
参考 JZ18 二叉树的镜像
class Solution {
public TreeNode invertTree(TreeNode root) {
if(root == null) return null;
dfs(root);
return root;
}
void dfs(TreeNode root){
if(root == null) return;
if(root.left == null && root.right == null) return;
swap(root , root.left , root.right);
dfs(root.left);
dfs(root.right);
}
void swap(TreeNode root , TreeNode left , TreeNode right){
root.left = right;
root.right = left;
}
}
236. 二叉树的最近公共祖先
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode(int x) { val = x; }
* }
*/
class Solution {
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
if(root == null) return null;
if(root == p || root == q) return root;
//在root.left里找p或q,返回p或q的节点,没有找到,就是null
TreeNode left = lowestCommonAncestor(root.left , p , q);
TreeNode right = lowestCommonAncestor(root.right , p , q);
if(left != null && right != null) return root;
else if(left == null) return right;
else return left;
}
}
297. 二叉树的序列化与反序列化
- BFS,层序遍历(更好)
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode(int x) { val = x; }
* }
*/
public class Codec {
// Encodes a tree to a single string.
public String serialize(TreeNode root) {
StringBuilder res = new StringBuilder("[");
if(root == null) return new String("[]");
Queue<TreeNode> q = new LinkedList<>();
q.add(root);
while(!q.isEmpty()){
TreeNode node = q.poll();
if(node == null) res.append("null" + ",");
else{
res.append(node.val + ",");
q.add(node.left);
q.add(node.right);
}
}
res.deleteCharAt(res.length() - 1);
res.append("]");
return res.toString();
}
// Decodes your encoded data to tree.
public TreeNode deserialize(String data) {
if(data.equals("[]")) return null;
String[] vals = data.substring(1 , data.length() - 1).split(",");
TreeNode[] nodeNum = new TreeNode[vals.length];
for(int i = 0 ; i < vals.length ; i++){
if(vals[i].equals("null")) nodeNum[i] = null;
else nodeNum[i] = new TreeNode(Integer.parseInt(vals[i]));
}
int pos = 1;
TreeNode root = nodeNum[0];
for(int i = 0 ; pos < nodeNum.length ; i++){
if(nodeNum[i] == null) continue;
else{
nodeNum[i].left = nodeNum[pos++];
nodeNum[i].right = nodeNum[pos++];
}
}
return root;
}
}
// Your Codec object will be instantiated and called as such:
// Codec ser = new Codec();
// Codec deser = new Codec();
// TreeNode ans = deser.deserialize(ser.serialize(root));
538. 把二叉搜索树转换为累加树
- 反序中序遍历
class Solution {
int sum = 0;
public TreeNode convertBST(TreeNode root) {
if(root == null) return null;
dfs(root);
return root;
}
void dfs(TreeNode root){
if(root == null) return;
dfs(root.right);
sum += root.val;
root.val = sum;
dfs(root.left);
}
}
543. 二叉树的直径
[参考】(https://leetcode-cn.com/problems/diameter-of-binary-tree/solution/er-cha-shu-de-zhi-jing-by-leetcode-solution/)
class Solution {
int res;
public int diameterOfBinaryTree(TreeNode root) {
if(root == null) return 0;
//递归函数,返回的是root的深度;
dfs(root);
return res - 1;
}
int dfs(TreeNode root){
if(root == null) return 0;
int leftDepth = dfs(root.left);
int rightDepth = dfs(root.right);
res = Math.max(res , leftDepth + rightDepth + 1);
return Math.max(leftDepth , rightDepth) + 1;
}
}
617. 合并二叉树
class Solution {
public TreeNode mergeTrees(TreeNode root1, TreeNode root2) {
if(root1 == null) return root2;
if(root2 == null) return root1;
TreeNode root = new TreeNode(root1.val + root2.val);
root.left = mergeTrees(root1.left , root2.left);
root.right = mergeTrees(root1.right , root2.right);
return root;
}
}
103. 二叉树的锯齿形层序遍历
参考【面试题32-Ⅲ 之字形打印二叉树】 https://blog.youkuaiyun.com/qq_42647047/article/details/111303308
利用双端队列
的两端都能操作的特性,分奇偶层
进行处理;
步骤:
BFS 循环: 循环打印奇 / 偶数层,当 deque 为空时跳出;
-------1打印奇数层: 从左向右打印,先左后右加入下层节点;
-------2若 deque 为空,说明向下无偶数层,则跳出;
-------3打印偶数层: 从右向左打印,先右后左加入下层节点;
class Solution {
public List<List<Integer>> zigzagLevelOrder(TreeNode root) {
List<List<Integer>> res = new ArrayList<>();
if(root == null) return res;
Deque<TreeNode> dq = new LinkedList<>();
dq.addLast(root);
int leval = 1;
while(!dq.isEmpty()){
int size = dq.size();
List<Integer> path = new ArrayList<>();
if(leval % 2 == 1){//奇数层,从左向右;
for(int i = 0 ; i < size ; i++){
TreeNode node = dq.removeFirst();
path.add(node.val);
if(node.left != null) dq.addLast(node.left);
if(node.right != null) dq.addLast(node.right);
}
}else{//偶数层,从右向左;
for(int i = 0 ; i < size ; i++){
TreeNode node = dq.removeLast();
path.add(node.val);
if(node.right != null) dq.addFirst(node.right);
if(node.left != null) dq.addFirst(node.left);
}
}
res.add(new ArrayList<>(path));
leval++;
}
return res;
}
}
199. 二叉树的右视图
- BFS层序遍历
class Solution {
public List<Integer> rightSideView(TreeNode root) {
List<Integer> res = new ArrayList<>();
if(root == null) return res;
Queue<TreeNode> q = new LinkedList<>();
q.add(root);
while(!q.isEmpty()){
int size = q.size();
TreeNode font = q.peek();
res.add(font.val);
for(int i = 0 ; i < size ; i++){
TreeNode node = q.poll();
if(node.right != null) q.add(node.right);
if(node.left != null) q.add(node.left);
}
}
return res;
}
}
- DFS
class Solution {
List<Integer> res = new ArrayList<>();
int depth = 0;
public List<Integer> rightSideView(TreeNode root) {
//depth参数,要放在dfs里做参数,不能不放,否则,会输出全部节点值;具体仔细体会递归过程;
dfs(root , 0);
return res;
}
void dfs(TreeNode root , int depth){
if(root == null) return;
if(depth == res.size()) res.add(root.val);
depth++;
dfs(root.right , depth);
dfs(root.left , depth);
}
}
108. 将有序数组转换为二叉搜索树
题意:根据升序数组,恢复一棵高度平衡的BST🌲。
分析:
- BST的中序遍历是升序的,因此本题等同于
根据中序遍历的序列恢复二叉搜索树
。因此我们可以以升序序列中的任一个元素作为根节点,以该元素左边的升序序列构建左子树,以该元素右边的升序序列构建右子树,这样得到的树就是一棵二叉搜索树啦~- 又因为本题要求高度平衡,因此我们需要选择升序序列的中间元素作为根节点奥~
class Solution {
public TreeNode sortedArrayToBST(int[] nums) {
TreeNode root = dfs(nums , 0 , nums.length - 1);
return root;
}
TreeNode dfs(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 = dfs(nums , l , mid - 1);
root.right = dfs(nums , mid + 1 , r);
return root;
}
}
958. 二叉树的完全性检验
- 参考:https://leetcode-cn.com/problems/check-completeness-of-a-binary-tree/solution/er-cha-shu-de-wan-quan-xing-jian-yan-by-leetcode/
- DFS解法:在完全二叉树中,
用 1 表示根节点编号
,则对于任意一个节点 x,它的左孩子为 2*x,右孩子为 2*x + 1
。那么我们可以发现,一颗二叉树是完全二叉树当且仅当
节点编号依次为 1, 2, 3, …n 且没有间隙。也就是,节点编号最大值等于节点个数
。
- BFS
class Solution {
public boolean isCompleteTree(TreeNode root) {
if(root == null) return true;
Queue<TreeNode> q = new LinkedList<>();
q.add(root);
while(!q.isEmpty()){
TreeNode node = q.remove();
if(node != null){
q.add(node.left);
q.add(node.right);
}else break;
}
for(TreeNode node : q){
if(node != null) return false;
}
return true;
}
}
- DFS
dfs(root , code)节点root的编号为code;
class Solution {
int size = 0;
int maxCode = 1;
public boolean isCompleteTree(TreeNode root) {
if(root == null) return true;
dfs(root , 1);
return maxCode == size;
}
void dfs(TreeNode root , int code){
if(root == null) return;
size++;
maxCode = Math.max(maxCode , code);
dfs(root.left , 2*code);
dfs(root.right , 2*code + 1);
}
}
剑指 Offer 36. 二叉搜索树与双向链表
参考https://blog.youkuaiyun.com/qq_42647047/article/details/111303308
class Solution {
Node pre;//不能Node pre = new Node(),因为这样Node就不等于null了
Node head;
public Node treeToDoublyList(Node root) {
if(root == null) return null;
dfs(root);
head.left = pre;
pre.right = head;
return head;
}
void dfs(Node cur){
if(cur == null) return;
dfs(cur.left);
if(pre == null) head = cur;
else pre.right = cur;
cur.left = pre;
pre = cur;
dfs(cur.right);
}
}
链表
141. 环形链表
细节
为什么我们要规定初始时慢指针在位置 head,快指针在位置 head.next,而不是两个指针都在位置 head(即与「乌龟」和「兔子」中的叙述相同)?
观察下面的代码,我们使用的是 while 循环,循环条件先于循环体。由于循环条件一定是判断快慢指针是否重合,如果我们将两个指针初始都置于 head,那么 while 循环就不会执行。因此,我们可以假想一个在 head 之前的虚拟节点,慢指针从虚拟节点移动一步到达 head,快指针从虚拟节点移动两步到达 head.next,这样我们就可以使用 while 循环了。
当然,我们也可以使用 do-while 循环。此时,我们就可以把快慢指针的初始值都置为 head。
public class Solution {
public boolean hasCycle(ListNode head) {
if(head == null || head.next == null || head.next.next == null) return false;
ListNode slow = head;
ListNode fast = head.next;
while(slow != fast){
if(fast== null || fast.next== null) return false;
else{
slow = slow.next;
fast = fast.next.next;
}
}
return true;
}
}
public class Solution {
public boolean hasCycle(ListNode head) {
if(head == null || head.next == null || head.next.next == null) return false;
ListNode slow = head;
ListNode fast = head;
slow = slow.next;
fast = fast.next.next;
while(slow != fast){
if(fast.next == null || fast.next.next == null) return false;
slow = slow.next;
fast = fast.next.next;
}
return true;
}
}
142. 环形链表 II
/**
* 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) {
ListNode meet = meetingNode(head);
if (meet == null) return null;
ListNode temp = meet;
int length = 1;
while(meet.next != temp){
length++;
meet = meet.next;
}
ListNode slow = head;
ListNode fast = head;
while(length-- != 0) fast = fast.next;
while(slow != fast){
slow = slow.next;
fast = fast.next;
}
return slow;
}
ListNode meetingNode(ListNode head){
if(head == null || head.next == null) return null;
ListNode slow = head;
ListNode fast = head.next;
while(slow != fast){
if(fast== null || fast.next== null) return null;
slow = slow.next;
fast = fast.next.next;
}
return slow;
}
}
146. LRU 缓存机制
[参考】(https://leetcode-cn.com/problems/lru-cache/solution/lru-ce-lue-xiang-jie-he-shi-xian-by-labuladong/)
LRU 算法实际上是让你设计数据结构:首先要接收一个 capacity 参数作为缓存的最大容量,然后实现两个 API【O(1) 的时间复杂度)】,put 和 get 。
LRU是最近最久未使用缓存淘汰策略,我们使用一个数据结构来存储任务,刚使用的放在前面,未访问时间越长的任务放在后面,删除任务时删除最后一个;
显然,这个数据结构必须要有顺序之分,且能前后操作,所以可以用,双端队列,和双向链表;
1、又因需要在O(1)内实现插入和删除,所以需要双向链表;
2、又因需要在O(1)内实现查找,所以需要HashMap;
即,需要哈希链表结构。
- 哈希 + 双向链表(
借助哈希表赋予了链表快速查找的特性
)
1、为什么要是双向链表
我们需要删除操作。删除一个节点不光要得到该节点本身的指针,也需要操作其前驱节点的指针,而双向链表才能支持直接查找前驱,保证操作的时间复杂度 O(1)。
2、为什么要在链表中同时存储 key 和 val,而不是只存储 val;
【注意缓存满了,需要删除最后一个 Node 节点时的代码】
if (cap == cache.size()) {
// 删除链表最后一个数据
Node last = cache.removeLast();
map.remove(last.key);
}
当缓存容量已满,我们不仅仅
要删除最后一个 Node 节点,还要
把 map 中映射到该节点的 key 同时删除,而这个 key 只能由 Node 得到
。如果 Node 结构中只存储 val,那么我们就无法得知 key 是什么,就无法删除 map 中的键,造成错误。
3、很容易犯错的一点是:处理链表节点的同时不要忘了更新哈希表中对节点的映射。
- 双向链表需自己手写,不能用LinkedList否则会超时;
- 小贴士:
在双向链表的实现中,使用一个伪头部(dummy head)和伪尾部(dummy tail)标记界限,这样在添加节点和删除节点的时候就不需要检查相邻的节点是否存在。
不使用伪头部(dummy head)和伪尾部(dummy tail),删除操作需要如下代码一样有多个判断
public void remove(Node node) {
if (head == node && tail == node) {
head = null;
tail = null;
} else if (tail == node) {
node.prev.next = null;
tail = node.prev;
} else if (head == node) {
node.next.prev = null;
head = node.next;
} else {
node.prev.next = node.next;
node.next.prev = node.prev;
}
size--;
}
class LRUCache {
int capacity;
HashMap<Integer , Node> cache;
DoubleList dList;
public LRUCache(int capacity) {
this.capacity = capacity;
cache = new HashMap<>();
dList = new DoubleList();
}
public int get(int key) {
if(cache.containsKey(key)){
put(key , cache.get(key).value);
return cache.get(key).value;
}else return -1;
}
public void put(int key, int value) {
Node node = new Node(key , value);
if(cache.containsKey(key)){
dList.remove(cache.get(key));
dList.addFirst(node);
cache.put(key , node);
}else{
if(dList.size == capacity){
Node last = dList.removeLast();
cache.remove(last.key);
dList.addFirst(node);
cache.put(key , node);
}
else{
dList.addFirst(node);
cache.put(key , node);
}
}
}
class Node{
int key;
int value;
Node pre;
Node next;
public Node(){}
public Node(int key , int value){
this.key = key;
this.value = value;
this.pre = null;
this.next = null;
}
}
class DoubleList{
//小贴士在双向链表的实现中,使用一个伪头部(dummy head)和伪尾部(dummy tail)标记界限,这样在添加节点和删除节点的时候就不需要检查相邻的节点是否存在。
Node dummyHead = new Node(0 , 0);
Node dummyTail = new Node(0 , 0);
int size;
public DoubleList(){
dummyHead.next = dummyTail;
dummyTail.pre = dummyHead;
this.size = 0;
}
void addFirst(Node node){
Node head = dummyHead.next;
dummyHead.next = node;
node.next = head;
head.pre = node;
node.pre = dummyHead;
size++;
}
void remove(Node node){
node.pre.next = node.next;
node.next.pre = node.pre;
size--;
}
Node removeLast(){
Node tail = dummyTail.pre;
remove(tail);
return tail;
}
}
}
/**
* 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);
*/
160. 相交链表
参考 面试题 52 两个链表的第一个公共节点添加链接描述
/**
* 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 curA = headA , curB = headB;
while(curA != curB){
if(curA == null) curA = headA;
else curA = curA.next;
if(curB == null) curB = headB;
else curB = curB.next;
}
return curA;
}
}
206. 反转链表
参考 JZ15反转链表
- 迭代
class Solution {
public ListNode reverseList(ListNode head) {
if(head == null || head.next == null) return head;
ListNode pre = null;
ListNode cur = head;
ListNode next = head.next;
while(cur != null){
next = cur.next;
cur.next = pre;
pre = cur;
cur = next;
}
return pre;
}
}
- 递归
class Solution {
public ListNode reverseList(ListNode head) {
if(head == null || head.next == null) return head;
ListNode newHead = reverseList(head.next);
head.next.next = head;
head.next = null;
return newHead;
}
}
反转链表 II
/**
* 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) {
if(head == null || head.next == null) return head;
if(left == right) return head;
ListNode dummyHead = new ListNode();
dummyHead.next = head;
ListNode cur = dummyHead;
for(int i = 0 ; i < left - 1 ; i++) cur = cur.next;
ListNode node1 = cur;
ListNode subHead = cur.next;
for(int i = 0 ; i < right - left + 1 ; i++) cur = cur.next;
ListNode subTail = cur;
ListNode node2 = cur.next;
node1.next = null;
subTail.next = null;
reverse(subHead);
node1.next = subTail;
subHead.next = node2;
return dummyHead.next;
}
void reverse(ListNode head){
ListNode pre = null;
ListNode cur = head;
ListNode next = new ListNode();
while(cur != null){
next = cur.next;
cur.next = pre;
pre = cur;
cur = next;
}
}
}
234. 回文链表
参考 方法三 快慢指针
比较完成后我们
应该将链表恢复原样
。虽然不需要恢复也能通过测试用例,但是使用该函数的人通常不希望链表结构被更改。
整个流程可以分为以下五个步骤:
1、找到前半部分链表的尾节点。
2、反转后半部分链表。
3、判断是否回文。
4、恢复链表。
5、返回结果。
/**
* 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 boolean isPalindrome(ListNode head) {
if(head == null || head.next == null) return true;
ListNode slow = head;
ListNode fast = head.next;
while(fast != null && fast.next != null){
slow = slow.next;
fast = fast.next.next;
}
ListNode head2 = slow.next;
ListNode headReverse = reverse(head2);
ListNode cur1 = headReverse;
ListNode cur2 = head;
while(cur1 != null){
if(cur1.val != cur2.val) return false;
cur1 = cur1.next;
cur2 = cur2.next;
}
head2 = reverse(headReverse);
slow.next = head2;
return true;
}
ListNode reverse(ListNode head){
ListNode pre = null;
ListNode cur = head;
ListNode next = head.next;
while(cur != null){
next = cur.next;
cur.next = pre;
pre = cur;
cur = next;
}
return pre;
}
}
24. 两两交换链表中的节点
参考:https://leetcode-cn.com/problems/swap-nodes-in-pairs/solution/liang-liang-jiao-huan-lian-biao-zhong-de-jie-di-91/
- 迭代
class Solution {
public ListNode swapPairs(ListNode head) {
ListNode dummyNode = new ListNode();
dummyNode.next = head;
ListNode temp = dummyNode;
while(temp.next != null && temp.next.next != null){
ListNode node1 = temp.next;
ListNode node2 = temp.next.next;
temp.next = node2;
node1.next = node2.next;
node2.next = node1;
temp = node1;
}
return dummyNode.next;
}
}
- 递归
class Solution {
public ListNode swapPairs(ListNode head) {
if(head == null || head.next == null) return head;
ListNode newHead = head.next;
head.next = swapPairs(newHead.next);
newHead.next = head;
return newHead;
}
}
25. K 个一组翻转链表
参考:https://leetcode-cn.com/problems/reverse-nodes-in-k-group/solution/tu-jie-kge-yi-zu-fan-zhuan-lian-biao-by-user7208t/
class Solution {
public ListNode reverseKGroup(ListNode head, int k) {
if(head == null || head.next == null) return head;
ListNode dummyHead = new ListNode();
dummyHead.next = head;
ListNode pre = dummyHead;
ListNode start = dummyHead;
ListNode end = dummyHead;
ListNode next = dummyHead;
while(end.next != null){
for(int i = 0 ; i < k && end != null ; i++) end = end.next;
if(end == null) break;
start = pre.next;
next = end.next;
end.next = null;
end = reverse(start);
pre.next = end;
start.next = next;
pre = start;
end = start;
}
//k个一组反转链表,不足k个的也要反转,加上上面两句即可
// ListNode reverseHead = reverse(pre.next);
// pre.next = reverseHead;
return dummyHead.next;
}
ListNode reverse(ListNode head){
ListNode pre = null;
ListNode cur = head;
ListNode next = head.next;
while(cur != null){
next = cur.next;
cur.next = pre;
pre = cur;
cur = next;
}
return pre;
}
}
删除排序链表中的重复元素 II
递归最基本的是要明白递归函数的定义
:
递归函数直接使用题目给出的函数 deleteDuplicates(head) ,它的含义是 删除以 head 作为开头的有序链表中,值出现重复的节点。返回的是删除重复节点后的 链表的头结点;
参考】https://leetcode-cn.com/problems/remove-duplicates-from-sorted-list-ii/solution/fu-xue-ming-zhu-di-gui-die-dai-yi-pian-t-wy0h/
- 递归
class Solution {
public ListNode deleteDuplicates(ListNode head) {
if(head == null || head.next == null) return head;
if(head.val != head.next.val) head.next = deleteDuplicates(head.next);
else{
ListNode cur = head;
while(cur.next != null && cur.val == cur.next.val) cur = cur.next;
return deleteDuplicates(cur.next);
}
return head;
}
}
- 迭代
class Solution {
public ListNode deleteDuplicates(ListNode head) {
if(head == null || head.next == null) return head;
ListNode dummyHead = new ListNode();
dummyHead.next = head;
ListNode pre = dummyHead;
ListNode cur = head;
ListNode next = head.next;
while(next != null){
if(cur.val != next.val){
pre = cur;
cur = cur.next;
next = next.next;
}else{
while(next != null && cur.val == next.val){
cur = cur.next;
next = next.next;
}
pre.next = next;
cur.next = null;
cur = next;
if(next != null) next = next.next;
}
}
return dummyHead.next;
}
}
21. 合并两个有序链表
参考】(https://leetcode-cn.com/problems/merge-two-sorted-lists/solution/yi-kan-jiu-hui-yi-xie-jiu-fei-xiang-jie-di-gui-by-/)(有图解递归过程)
迭代
新建一个链表,不是在两个链表上合并;
class Solution {
public ListNode mergeTwoLists(ListNode l1, ListNode l2) {
if(l1 == null || l2 == null) return l1 == null ? l2 : l1;
ListNode dummyHead = new ListNode();
ListNode cur = dummyHead;
while(l1 != null && l2 != null){
if(l1.val <= l2.val){
cur.next = l1;
l1 = l1.next;
}else{
cur.next = l2;
l2 = l2.next;
}
cur = cur.next;
}
cur.next = l1 == null ? l2 : l1;
return dummyHead.next;
}
}
- 本题迭代更好,因为空间复杂度是O(1)。
- 递归
class Solution {
public ListNode mergeTwoLists(ListNode l1, ListNode l2) {
if(l1 == null) return l2;
if(l2 == null) return l1;
if(l1.val <= l2.val){
l1.next = mergeTwoLists(l1.next , l2);
return l1;
}else{
l2.next = mergeTwoLists(l1 , l2.next);
return l2;
}
}
}
23. 合并K个升序链表
[参考】(https://leetcode-cn.com/problems/merge-k-sorted-lists/solution/he-bing-kge-pai-xu-lian-biao-by-leetcode-solutio-2/)
- 堆(优先队列)
class Solution {
public ListNode mergeKLists(ListNode[] lists) {
int k = lists.length;
if(k == 0) return null;
if(k == 1) return lists[0];
PriorityQueue<ListNode> pq = new PriorityQueue<>((a , b) -> a.val - b.val);
for(int i = 0 ; i < k ; i++){
if(lists[i] != null) pq.add(lists[i]);
}
ListNode dummyHead = new ListNode();
ListNode pre = dummyHead;
while(!pq.isEmpty()){
ListNode node = pq.poll();
pre.next = node;
pre = pre.next;
node = node.next;
if(node != null) pq.add(node);
else continue;
}
return dummyHead.next;
}
}
- 归并排序
class Solution {
public ListNode mergeKLists(ListNode[] lists) {
int length = lists.length;
if(length == 0) return null;
if(length == 1) return lists[0];
return merge(lists , 0 , length - 1);
}
//将lists里left ~ right链表合并成有序的一个链表;
ListNode merge(ListNode[] lists , int left , int right){
if(left == right) return lists[left];
int mid = left + (right - left) /2;
ListNode leftRes = merge(lists , left , mid);
ListNode rightRes = merge(lists , mid + 1 , right);
return mergeTwoLists(leftRes , rightRes);
}
ListNode mergeTwoLists(ListNode a , ListNode b){
if(a == null || b == null) return a ==null ? b : a;
ListNode preHead = new ListNode(0);
ListNode cur = preHead;
ListNode curA = a;
ListNode curB = b;
while(curA != null && curB != null){
if(curA.val <= curB.val){
cur.next = curA;
curA = curA.next;
}else{
cur.next = curB;
curB = curB.next;
}
cur = cur.next;
}
cur.next = (curA == null ? curB : curA);
return preHead.next;
}
}
328. 奇偶链表
参考】https://leetcode-cn.com/problems/odd-even-linked-list/solution/kuai-lai-wu-nao-miao-dong-qi-ou-lian-biao-by-sweet/
分别定义奇偶链表;
遍历原链表,将当前结点交替插入到奇链表和偶链表(尾插法);
将偶链表拼接在奇链表后面。
class Solution {
public ListNode oddEvenList(ListNode head) {
ListNode dummyOddHead = new ListNode();
ListNode dummyEvenHead = new ListNode();
ListNode preOddHead = dummyOddHead;
ListNode preEvenHead = dummyEvenHead;
boolean isOdd = true;
while(head != null){
if(isOdd){
preOddHead.next = head;
preOddHead = preOddHead.next;
}else{
preEvenHead.next = head;
preEvenHead = preEvenHead.next;
}
head = head.next;
isOdd = !isOdd;
}
preOddHead.next = dummyEvenHead.next;
preEvenHead.next = null;
return dummyOddHead.next;
}
}
ZJ 排序奇升偶降链表
class Solution {
public ListNode sortOddEvenList(ListNode head) {
ListNode dummyOddHead = new ListNode();
ListNode dummyEvenHead = new ListNode();
ListNode preOddHead = dummyOddHead;
ListNode preEvenHead = dummyEvenHead;
boolean isOdd = true;
while(head != null){
if(isOdd){
preOddHead.next = head;
preOddHead = preOddHead.next;
}else{
preEvenHead.next = head;
preEvenHead = preEvenHead.next;
}
head = head.next;
isOdd = !isOdd;
}
preOddHead.next = null;
preEvenHead.next = null;
ListNode reverseHead = reverse(dummyEvenHead.next);
return mergeTwoLists(dummyOddHead.next , reverseHead);
}
ListNode reverse(ListNode head) {
if(head == null || head.next == null) return head;
ListNode pre = null;
ListNode cur = head;
ListNode next = head.next;
while(cur != null){
next = cur.next;
cur.next = pre;
pre = cur;
cur = next;
}
return pre;
}
ListNode mergeTwoLists(ListNode l1, ListNode l2) {
if(l1 == null || l2 == null) return l1 == null ? l2 : l1;
ListNode dummyHead = new ListNode();
ListNode cur = dummyHead;
while(l1 != null && l2 != null){
if(l1.val <= l2.val){
cur.next = l1;
l1 = l1.next;
}else{
cur.next = l2;
l2 = l2.next;
}
cur = cur.next;
}
cur.next = l1 == null ? l2 : l1;
return dummyHead.next;
}
}
2. 两数相加
class Solution {
public ListNode addTwoNumbers(ListNode l1, ListNode l2) {
if(l1 == null || l2 == null) return l1 == null? l2 : l1;
int carry = 0;
int sum = 0;
ListNode dummyHead = new ListNode();
ListNode cur = dummyHead;
while(l1 != null || l2 != null || carry == 1){
int a = (l1 == null ? 0 : l1.val);
int b = (l2 == null ? 0 : l2.val);
sum = a + b + carry;
carry = sum / 10;
cur.next = new ListNode(sum % 10);
cur = cur.next;
if(l1 != null) l1 = l1.next;
if(l2 != null) l2 = l2.next;
}
cur.next = null;
return dummyHead.next;
}
}
445. 两数相加 II
- 将链表反转,即为上面一题
进阶:如果输入链表不能翻转该如何解决
本题的主要难点在于链表中数位的顺序与我们做加法的顺序是相反的,为了逆序处理所有数位,我们可以使用栈
:把所有数字压入栈中,再依次取出相加。计算过程中需要注意进位的情况。
/**
* 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) {
Deque<Integer> stack1 = new LinkedList<>();
Deque<Integer> stack2 = new LinkedList<>();
while(l1 != null){
stack1.push(l1.val);
l1 = l1.next;
}
while(l2 != null){
stack2.push(l2.val);
l2 = l2.next;
}
int carry = 0;
ListNode res = null;
while(!stack1.isEmpty() || !stack2.isEmpty() || carry != 0){
int a = stack1.isEmpty()? 0 : stack1.poll();
int b = stack2.isEmpty()? 0 : stack2.poll();
int sum = a + b + carry;
carry = sum/10;
sum = sum % 10;
ListNode cur = new ListNode(sum);
cur.next = res;
res = cur;
}
return res;
}
}
143. 重排链表
class Solution {
public void reorderList(ListNode head) {
if(head == null || head.next == null || head.next.next == null) return;
ListNode slow = head;
ListNode fast = head.next;
while(fast != null && fast.next != null){
slow = slow.next;
fast = fast.next.next;
}
ListNode reverseHead = slow.next;
slow.next = null;
ListNode head2 = reverse(reverseHead);
ListNode head1 = head;
ListNode next1 = head1.next;
ListNode next2 = head2.next;
while(head1 != null && head2 != null){
next1 = head1.next;
next2 = head2.next;
head1.next = head2;
head2.next = next1;
head1 = next1;
head2 = next2;
}
return;
}
ListNode reverse(ListNode head){
ListNode pre = null;
ListNode cur = head;
ListNode next = cur.next;
while(cur != null){
next = cur.next;
cur.next = pre;
pre = cur;
cur = next;
}
return pre;
}
}
面试题 02.01. 移除重复节点
- 哈希表
class Solution {
public ListNode removeDuplicateNodes(ListNode head) {
if(head == null || head.next == null) return head;
HashMap<Integer , Boolean> map = new HashMap<>();
ListNode dummyHead = new ListNode();
dummyHead.next = head;
ListNode cur = dummyHead;
while(head != null){
if(map.containsKey(head.val)){
cur.next = head.next;
head = cur.next;
}else{
cur.next = head;
cur = cur.next;
map.put(head.val , true);
head = head.next;
}
}
return dummyHead.next;
}
}
- 进阶:O(1)空间复杂度----两次循环
class Solution {
public ListNode removeDuplicateNodes(ListNode head) {
if(head == null || head.next == null) return head;
ListNode cur1 = head;
while(cur1 != null){
ListNode cur2 = cur1;
while(cur2.next != null){
if(cur1.val == cur2.next.val){
cur2.next = cur2.next.next;
}else cur2 = cur2.next;
}
cur1 = cur1.next;
}
return head;
}
}
知识点
虚拟头结点
dummyHead
链表中删除、移动节点等操作,若涉及头结点,则需分类讨论,一般设置虚拟头结点dummyHead避免分类讨论
;
快慢指针中:
ListNode slow = head;
ListNode fast = head.next;
常常慢指针在位置 head,快指针在位置 head.next,而不是两个指针都在位置 head
因为,我们使用的是 while (slow != fast)判断,循环条件先于循环体。循环条件一定是判断快慢指针是否重合,如果我们将两个指针初始都置于 head,那么 while 循环就不会执行。因此,我们可以假想一个在 head 之前的虚拟节点
,慢指针从虚拟节点移动一步到达 head,快指针从虚拟节点移动两步到达 head.next,这样我们就可以使用 while 循环了。
当然,我们也可以使用 do-while 循环。此时,我们就可以把快慢指针的初始值都置为 head。
- 确认中点
ListNode slow = head;
ListNode fast = head.next;
while(fast != null && fast.next != null){
slow = slow.next;
fast = fast.next.next;
}
在确认中点的时候,slow指针,最终指向中点位置(奇数个指向正中间,偶数个指向两个中点的前一个)
排序
再加剑指offer的排序题:https://blog.youkuaiyun.com/qq_42647047/article/details/112287504
148. 排序链表
[参考】(https://leetcode-cn.com/problems/sort-list/solution/sort-list-gui-bing-pai-xu-lian-biao-by-jyd/)
题目的进阶问题要求达到O(nlogn) 的时间复杂度和 O(1) 的空间复杂度,时间复杂度是O(nlogn) 的排序算法包括归并排序、堆排序和快速排序(快速排序的最差时间复杂度是 O(n^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 ListNode sortList(ListNode head) {
if(head == null) return null;
ListNode res = mSort(head);
return res;
}
ListNode mSort(ListNode head){//返回一个排好序链表的头结点
if(head.next == null) return head;
ListNode slow = head;
ListNode fast = head.next;
while(fast != null && fast.next != null){
slow = slow.next;
fast = fast.next.next;
}
ListNode temp = slow.next;
slow.next = null;
ListNode left = mSort(head);
ListNode right = mSort(temp);
ListNode res = merge(left , right);
return res;
}
ListNode merge(ListNode left , ListNode right){
ListNode pre = new ListNode();
ListNode res = pre;
while(left != null && right != null){
if(left.val <= right.val){
pre.next = left;
left = left.next;
}else{
pre.next = right;
right = right.next;
}
pre = pre.next;
}
pre.next = (left == null)? right : left;
return res.next;
}
}
- 迭代
class Solution {
public ListNode sortList(ListNode head) {
if(head == null || head.next == null) return head;
int len = 0;
ListNode node = head;
while(node != null){
len++;
node = node.next;
}
ListNode dummyHead = new ListNode();
dummyHead.next = head;
for(int subLen = 1 ; subLen < len ; subLen *= 2){
ListNode pre = dummyHead;
//注意:这里不能是ListNode cur = head;因为每一轮迭代,顺序都会变,导致头结点都会变,应该是cur = pre.next;
ListNode cur = pre.next;
while(cur != null){
ListNode head1 = cur;
for(int i = 0 ; i < subLen - 1 && cur.next != null ; i++) cur = cur.next;
ListNode head2 = cur.next;
cur.next = null;
cur = head2;
for(int i = 0 ; i < subLen - 1 && cur != null && cur.next != null ; i++) cur = cur.next;
ListNode next = null;
if(cur != null){
next = cur.next;
cur.next = null;
}
ListNode mergeNode = merge(head1 , head2);
pre.next = mergeNode;
while(pre.next != null) pre = pre.next;
cur = next;
}
}
return dummyHead.next;
}
ListNode merge(ListNode l1 , ListNode l2){
if(l1 == null || l2 == null) return l1 == null ? l2 : l1;
ListNode dummyHead = new ListNode();
ListNode cur = dummyHead;
while(l1 != null && l2 != null){
if(l1.val <= l2.val){
cur.next = l1;
l1 = l1.next;
}else{
cur.next = l2;
l2 = l2.next;
}
cur = cur.next;
}
cur.next = (l1 == null ? l2 : l1);
return dummyHead.next;
}
}
- 快排
参考1、
【速度慢,但结构清晰(比枢纽值pivoteKey小的用头插法
放左边,大的用尾插法
放右边)】
【使用快慢指针找中点,作为枢纽值pivoteKey,而不是每次取head,提升了些速度】
https://leetcode-cn.com/problems/sort-list/solution/pai-xu-lian-biao-kuai-pai-fang-shi-by-yx-ahnt/
参考2、【速度快,对等于枢纽值pivoteKey的节点单独进行了串联】
https://leetcode-cn.com/problems/sort-list/solution/lian-biao-pai-xu-kuai-pai-ji-gui-bing-by-m70f/
//参考1
class Solution {
public ListNode sortList(ListNode head) {
return quickSort(head ,null);
}
public ListNode quickSort(ListNode head , ListNode end) {
if(head == end || head.next == end) return head;
ListNode dummyHead = new ListNode();
dummyHead.next = head;
ListNode pre = dummyHead;
ListNode slow = head, fast = head.next;
while(fast != end && fast.next != end){
pre = pre.next;
slow = slow.next;
fast = fast.next.next;
}
//需要将slow节点剔除出来,然后再对slow头插和尾插,不然在遍历链表时,会因为链表不断改变,而遍历出错;
pre.next = slow.next;
slow.next = null;
ListNode lhead = slow;
ListNode tail = slow;
ListNode cur = dummyHead.next;
while(cur != end){
ListNode next = cur.next;
if(cur.val < slow.val){//头插
cur.next = lhead;
lhead = cur;
}else{//尾插
tail.next = cur;
tail = cur;
}
cur = next;
}
tail.next = end;
ListNode node = quickSort(lhead , slow);
slow.next = quickSort(slow.next , end);
return node;
}
}
//参考2
class Solution {
public ListNode sortList(ListNode head) {
return quickSort(head);
}
public ListNode quickSort(ListNode head) {
if(head == null || head.next == null) return head;
ListNode slow = head, fast = head.next;
while(fast != null && fast.next != null){
slow = slow.next;
fast = fast.next.next;
}
ListNode head1 = new ListNode();
ListNode head2 = new ListNode();
ListNode head3 = new ListNode();
ListNode t1 = head1, t2 = head2, t3 = head3;
ListNode cur = head;
while(cur != null){
ListNode next = cur.next;
if(cur.val < slow.val){
t1.next = cur;
t1 = cur;
}else if(cur.val > slow.val){
t3.next = cur;
t3 = cur;
}else{
t2.next = cur;
t2 = cur;
}
cur = next;
}
t1.next = null;
t2.next = null;
t3.next = null;
head1 = quickSort(head1.next);
head2 = head2.next;
head3 = quickSort(head3.next);
t2.next = head3;
if(head1 == null) {
return head2;
} else {
t1 = head1;
while(t1.next != null) {
t1 = t1.next;
}
t1.next = head2;
return head1;
}
}
}
215. 数组中的第K个最大元素【高频,】
注意每种方法的时间复杂度
各方法参考:https://leetcode-cn.com/problems/kth-largest-element-in-an-array/solution/shu-zu-zhong-de-di-kge-zui-da-yuan-su-by-leetcode-/
快排主要参考 [JZ29 最小的k个数】(https://blog.youkuaiyun.com/qq_42647047/article/details/112287504)
手动建堆,参考:https://blog.youkuaiyun.com/qq_42647047/article/details/115024624
一、手动建堆(最好的解法)
我们用一个小根堆实时维护数组的前 k小值。
1、首先针对前 k 个数,构建一个大小为k的小根堆;
2、随后从第 k+1个数开始遍历,如果当前遍历到的数nums[i]比小根堆的堆顶num[0]的数要大,就交换堆顶元素num[0]和nums[i],然后重建堆。
3、遍历完后,num[0]即第K个最大元素。
class Solution {
public int findKthLargest(int[] nums, int k) {
//1、构建一个大小为k的小顶堆
minHeap(nums , k);
//2、
for(int i = k ; i < nums.length ; i++){
if(nums[i] > nums[0]){
swap(nums , 0 , i);
headAdjust(nums , 0 , k - 1);
}else continue;
}
//3、
return nums[0];
}
void minHeap(int[] nums, int k){
for (int i = k/2 - 1 ; i >= 0; i--){
//从下往上,从右到左遍历非叶子节点,将每个非叶子节点依次调整成堆
headAdjust(nums , i , k - 1);
}
return;
}
void headAdjust(int[] nums , int left , int right){
//剪枝:不加也对,当left == right,即只有一个节点,一个节点本身就是一个大(小)顶堆,不需要继续操作,直接返回即可。
//if(left == right) return;
int temp = nums[left];
for (int i = 2*left + 1; i <= right; i = i*2 + 1) {
if(i < right && nums[i] > nums[i+1]) i++;
if(temp <= nums[i]) break;
nums[left] = nums[i];
left = i;
}
nums[left] = temp;
return;
}
void swap(int[] nums , int left , int right){
int temp = nums[left];
nums[left] = nums[right];
nums[right] = temp;
}
}
时间复杂度:O(nlogk)
初始构建堆:遍历数据 O(k);n-k次重建堆 O(logk),即O((n-k)logk),即O(nlogk)
空间复杂度:O(1)
二、利用堆的现成API-----优先队列
class Solution {
public int findKthLargest(int[] nums, int k) {
int length = nums.length;
PriorityQueue<Integer> pq = new PriorityQueue<>();
for(int i = 0 ; i < k ; i++) pq.add(nums[i]);
for(int i = k ; i < length ; i++){
if(nums[i] > pq.peek()){
pq.poll();
pq.add(nums[i]);
}else continue;
}
return pq.peek();
}
}
时间复杂度:O(nlogk)
初始构建堆:遍历数据 O(k);n-k次重建堆 O(logk),即O((n-k)logk),即O(nlogk)
空间复杂度:O(1)
三、快排
- qSort(nums , l , r , target);
//对数组 nums 的 [l,r] 部分排序,使下标target处的元素得到正确排序。- pivot = partition(arr , low , high);
//一次划分,将一个值放到了它正确的位置pivot;左边的元素都比它小,右边的元素都比它大。- 测试用例中有极端用例,需要
“随机选取枢纽值”或“三数取中”
。
这里“随机选取枢纽值”比“三数取中”更快。【理论上是三数取中更快】
class Solution {
int res = 0;
Random random = new Random();
public int findKthLargest(int[] nums, int k) {
int length = nums.length;
int target = length - k;
qSort(nums , 0 , length - 1 , target);
return nums[target];
}
void qSort(int[] arr , int low , int high , int target){
int pivot;
if(low < high){
//一次划分,将一个值放到了它正确的位置pivot;左边的元素都比它小,右边的元素都比它大。
pivot = partition(arr , low , high);
if(pivot == target) return;
//快速排序会根据分界值的下标pivot递归处理划分的两侧,而这里,根据题意我们只需处理划分的一边。
else if(pivot < target) qSort(arr , pivot + 1 , high , target);
else qSort(arr , low , pivot - 1 , target);
}
}
int partition(int[] arr , int low , int high){
int pivotkey;//pivotkey枢轴值
//这里“随机选取枢纽值”比“三数取中”更快。【理论上是三数取中更快】
int rand = random.nextInt(high - low + 1) + low;
swap(arr , low ,rand);
pivotkey = arr[low];
while(low < high){
while(low < high && arr[high] >= pivotkey) high--;
swap(arr , low ,high);
while(low < high && arr[low] <= pivotkey) low++;
swap(arr , low ,high);
}
return low;
}
void swap(int[] arr , int low , int high){
int temp = arr[low];
arr[low] = arr[high];
arr[high] = temp;
}
}
时间复杂度:O(n)
这里 n 是数组的长度,理由可以参考
空间复杂度:O(logn)
四、利用Java内置排序函数,先排序
进阶
347. 前 K 个高频元素
类似于上道题215,上题是求 “数组中的第K个最大元素”,进而也可以求出前k大元素,这里求“出现频率前 k 高的元素” 若将各数字出现的频率形成数组,则和上题一样
- 进阶:你所设计算法的时间复杂度 必须 优于 O(n log n) ,其中 n 是数组大小。要求优于 O(n log n),只有堆排序的时间复杂度O(nlogk)由于 O(n log n),所以使用堆;
class Solution {
public int[] topKFrequent(int[] nums, int k) {
HashMap<Integer , Integer> map = new HashMap<>();
for (int num : nums) {
map.put(num, map.getOrDefault(num, 0) + 1);
}
PriorityQueue<int[]> pq = new PriorityQueue<>(new Comparator<int[]>() {
public int compare(int[] a , int[] b){
return a[1] - b[1];
}
});
for(Map.Entry<Integer , Integer> entry : map.entrySet()){
int num = entry.getKey();
int count = entry.getValue();
if(pq.size() == k){
if(count > pq.peek()[1]){
pq.poll();
pq.add(new int[]{num , count});
}else continue;
}else{
pq.add(new int[]{num , count});
}
}
int[] res = new int[k];
int i = 0;
for(int[] arry : pq){
res[i++] = arry[0];
}
return res;
}
}
23. 合并K个升序链表
[参考】(https://leetcode-cn.com/problems/merge-k-sorted-lists/solution/he-bing-kge-pai-xu-lian-biao-by-leetcode-solutio-2/)
- 堆(优先队列)
class Solution {
public ListNode mergeKLists(ListNode[] lists) {
int length = lists.length;
if(length == 0) return null;
if(length == 1) return lists[0];
PriorityQueue<ListNode> pq = new PriorityQueue<>(new Comparator<ListNode>(){
public int compare(ListNode a , ListNode b){
return a.val - b.val;
}
});
for(ListNode node : lists){
if(node != null) pq.add(node);
}
ListNode preHead = new ListNode(0);
ListNode cur = preHead;
while(!pq.isEmpty()){
ListNode tempNode = pq.poll();
cur.next = tempNode;
cur = cur.next;
if(tempNode.next != null) pq.add(tempNode.next);
}
return preHead.next;
}
}
- 归并排序的合并阶段
归并排序的合并阶段:两个有序的序列,合并成一个有序的序列;
class Solution {
public ListNode mergeKLists(ListNode[] lists) {
int length = lists.length;
if(length == 0) return null;
if(length == 1) return lists[0];
return merge(lists , 0 , length - 1);
}
//将lists里left ~ right链表合并成有序的一个链表;
ListNode merge(ListNode[] lists , int left , int right){
if(left == right) return lists[left];
int mid = left + (right - left) /2;
ListNode leftRes = merge(lists , left , mid);
ListNode rightRes = merge(lists , mid + 1 , right);
return mergeTwoLists(leftRes , rightRes);
}
ListNode mergeTwoLists(ListNode a , ListNode b){
if(a == null || b == null) return a ==null ? b : a;
ListNode preHead = new ListNode(0);
ListNode cur = preHead;
ListNode curA = a;
ListNode curB = b;
while(curA != null && curB != null){
if(curA.val <= curB.val){
cur.next = curA;
curA = curA.next;
}else{
cur.next = curB;
curB = curB.next;
}
cur = cur.next;
}
cur.next = (curA == null ? curB : curA);
return preHead.next;
}
}
面试题51 数组中的逆序对
题解:
1、本题其实就是在对一个数组进行归并排序
的基础上,增加了一个统计逆序对数目的障眼法,其实还是归并排序。
2、如果了解归并排序的话,就会想到我们可以用分治的思想,将给定的 nums 先一分为二,统计左半部分的逆序对数目,再统计右半部分的逆序对数目,以及统计跨越左半部分和右半部分的逆序对数目,然后将三者相加即可。
参考 https://blog.youkuaiyun.com/qq_42647047/article/details/112287504
java:
class Solution {
public int reversePairs(int[] nums) {
if(nums.length <= 1) return 0;
int[] temp = new int[nums.length];
int res = Msort(nums , temp , 0 , nums.length - 1);
return res;
}
int Msort(int[] nums , int[] temp , int low , int high){
if(low == high) return 0;
else{
int mid = low + (high - low) / 2;
int leftCount = Msort(nums , temp , low , mid);
int rightCount = Msort(nums , temp , mid + 1 , high);
//注意这里不能是temp[mid] <= temp[mid + 1],因为向temp里添加元素是在下面Merge函数里,这里还没添加。所以要用num
if(nums[mid] <= nums[mid + 1]){
return leftCount + rightCount;
}
int crossCount = Merge(temp , nums , low , mid ,high);
return leftCount + rightCount + crossCount;
}
}
int Merge(int[] temp , int[] nums , int low , int mid , int high){
for(int i = low ; i <= high ; i++){
temp[i] = nums[i];
}
int count = 0;
int i = low , j = mid + 1 , pos = low;
for(; i <= mid && j <= high ; pos++){
if(temp[i] <= temp[j]){
nums[pos] = temp[i++];
}else{
nums[pos] = temp[j++];
count += (mid - i + 1);
}
}
if(i <= mid){
for(int k = i ; k <= mid ; k++){
nums[pos++] = temp[k];
}
}
if(j <= high){
for(int k = j ; k <= high ; k++){
nums[pos++] = temp[k];
}
}
return count;
}
}
CD21 计算数组的小和
- 和上一题的逆序对思路类似
参考:https://mp.weixin.qq.com/s/rMsbcUf9ZPhvfRoyZGW6HA
原题地址:https://www.nowcoder.com/practice/edfe05a1d45c4ea89101d936cac32469?tpId=101&tqId=33089&tPage=1&rp=1&ru=%2Fta%2Fprogrammer-code-interview-guide
import java.util.Scanner;
//6
//1 3 5 2 4 6
public class Main {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
int n = sc.nextInt();
int[] nums = new int[n];
int[] temp = new int[n];
for (int i = 0; i < n; i++) nums[i] = sc.nextInt();
Solution2 solution2 = new Solution2();
long i = solution2.mSort(nums , temp , 0 , nums.length - 1);
System.out.println(i);
}
}
class Solution2 {
long mSort(int[] nums , int[] temp , int left , int right) {
if(left == right) return 0;
else{
int mid = left + (right - left) / 2;
long leftSum = mSort(nums , temp , left , mid);
long rightSum = mSort(nums , temp , mid+1 , right);
//if(nums[left] > nums[right]) return leftSum + rightSum;
long crossSum = merge(temp , nums , left , mid , right);
return leftSum + crossSum + rightSum;
}
}
long merge(int[] temp , int[] nums , int left , int mid , int right){
for (int i = left; i <= right; i++) temp[i] = nums[i];
int i = left;
int j = mid+1;
int pos = left;
long res = 0;
for ( ; i <= mid && j <= right; pos++) {
if(temp[i] <= temp[j]){
res += (right - j + 1)*temp[i];
nums[pos] = temp[i++];
}else nums[pos] = temp[j++];
}
while(i <= mid) nums[pos++] = temp[i++];
while(j <= right) nums[pos++] = temp[j++];
return res;
}
}
知识点
快慢指针中:
ListNode slow = head;
ListNode fast = head.next;
常常慢指针在位置 head,快指针在位置 head.next,而不是两个指针都在位置 head
因为,我们使用的是 while (slow != fast)判断,循环条件先于循环体。循环条件一定是判断快慢指针是否重合,如果我们将两个指针初始都置于 head,那么 while 循环就不会执行。因此,我们可以假想一个在 head 之前的虚拟节点
,慢指针从虚拟节点移动一步到达 head,快指针从虚拟节点移动两步到达 head.next,这样我们就可以使用 while 循环了。
当然,我们也可以使用 do-while 循环。此时,我们就可以把快慢指针的初始值都置为 head。
堆(优先队列)
对于排序问题,有时候使用堆会较为简单;
最适合链表的排序算法是归并排序
数组
169. 多数元素
- 摩尔投票法(最优解)
参考 JZ28 数组中出现次数超过一半的数字
class Solution {
public int majorityElement(int[] nums) {
//众数可以随便赋初始值;
int x = nums[0];
int vote = 0;
for(int val : nums){
if(vote == 0) x = val;
if(val == x) vote++;
else vote--;
}
int count = 0;
for(int val : nums){
if(val == x) ++count;
}
if(count > nums.length/2) return x;
else return 0;
}
}
238. 除自身以外数组的乘积
注意空间复杂度O(1)的写法
class Solution {
public int[] productExceptSelf(int[] nums) {
int[] res = new int[nums.length];
if(nums.length == 0) return res;
res[0] = 1;
for(int i = 1 ; i < nums.length ; i++){
res[i] = nums[i - 1] * res[i - 1];
}
int R = 1;
for(int i = nums.length - 1 ; i >= 0 ; i--){
res[i] = res[i] * R;
R = R * nums[i];
}
return res;
}
}
240. 搜索二维矩阵 II
参考 面试题4 二维数组中的查找(书上题解)
class Solution {
public boolean searchMatrix(int[][] matrix, int target) {
if(matrix == null || matrix.length == 0 || matrix[0].length == 0) return false;
int rows = matrix.length;
int colums = matrix[0].length;
int r = 0;
int c = colums - 1;
while(r < rows && c >= 0){
if(matrix[r][c] > target) c--;
else if(matrix[r][c] < target) r++;
else return true;
}
return false;
}
}
448. 找到所有数组中消失的数字
- 空间复杂度是 O(n) 的
class Solution {
public List<Integer> findDisappearedNumbers(int[] nums) {
ArrayList<Integer> res = new ArrayList<>();
if(nums.length <= 1) return res;
int[] temp = new int[nums.length + 1];
for(int i = 0 ; i < nums.length ; i++){
temp[nums[i]]++;
}
for(int i = 1 ; i < temp.length ; i++){
if(temp[i] == 0) res.add(i);
}
return res;
}
}
- 空间复杂度到 O(1),原地修改
class Solution {
public List<Integer> findDisappearedNumbers(int[] nums) {
int n = nums.length;
for (int num : nums) {
int x = (num - 1) % n;
nums[x] += n;
}
List<Integer> ret = new ArrayList<Integer>();
for (int i = 0; i < n; i++) {
if (nums[i] <= n) {
ret.add(i + 1);
}
}
return ret;
}
}
54. 螺旋矩阵
参考 https://leetcode-cn.com/problems/spiral-matrix/solution/luo-xuan-ju-zhen-by-leetcode-solution/
- 模拟螺旋矩阵的路径
//按照右、下、左、上 的顺时针顺序进行模拟打印int[][] directions = {{0 , 1} , {1 , 0} , {0 , -1} , {-1 , 0}};
- 判断路径是否进入之前访问过的位置
int[][] used = new int[rows][colums];- 如何判断路径是否结束?
由于矩阵中的每个元素都被访问一次,因此路径的长度即为矩阵中的元素数量;int total = rows * colums;
class Solution {
public List<Integer> spiralOrder(int[][] matrix) {
ArrayList<Integer> res = new ArrayList<>();
if(matrix == null || matrix.length == 0 || matrix[0].length == 0) return res;
int rows = matrix.length;
int colums = matrix[0].length;
int[][] used = new int[rows][colums];
int total = rows * colums;
int row = 0;
int col = 0;
res.add(matrix[row][col]);
used[0][0] = 1;
//按照右、下、左、上 的顺时针顺序进行模拟打印
int[][] directions = {{0 , 1} , {1 , 0} , {0 , -1} , {-1 , 0}};
int directionIndex = 0;
for(int i = 1 ; i < total ; i++){
//按照上一次的遍历方向,先计算下一个要访问的坐标,然后判断是否合理;
int nextRow = row + directions[directionIndex][0];
int nextCol = col + directions[directionIndex][1];
if(nextRow < 0 || nextRow >= rows || nextCol < 0 || nextCol >= colums || used[nextRow][nextCol] == 1){
//如果下一步出界,或已经访问过,则改变遍历方向;
directionIndex = (directionIndex + 1) % 4;
}
row += directions[directionIndex][0];
col += directions[directionIndex][1];
used[row][col] = 1;
res.add(matrix[row][col]);
}
return res;
}
}
581. 最短无序连续子数组
public class Solution {
public int findUnsortedSubarray(int[] nums) {
int n = nums.length;
if(n <= 1) return 0;
int left = n - 1;
int right = 0;
int min = Integer.MAX_VALUE;
int max = Integer.MIN_VALUE;
//找到无序子数组中最小元素
for(int i = 0 ; i < n - 1 ; i++){
if(nums[i] > nums[i + 1]) min = Math.min(min , nums[i+1]);
}
//找到无序子数组中最大元素
for(int i = n - 1 ; i >= 1 ; i--){
if(nums[i-1] > nums[i]) max = Math.max(max , nums[i-1]);
}
//找到无序子数组中最小元素,应该在的位置
for(int i = 0 ; i < n ; i++){
if(nums[i] > min){
left = i;
break;
}
}
//找到无序子数组中最大元素,应该在的位置
for(int i = n - 1 ; i >= 0 ; i--){
if(nums[i] < max){
right = i;
break;
}
}
return (right - left) > 0 ? (right - left + 1) : 0;
}
}
384. 打乱数组
参考:https://leetcode-cn.com/problems/shuffle-an-array/solution/da-luan-shu-zu-by-leetcode/
- Fisher-Yates 洗牌算法
思路:每次迭代,在当前下标到数组末尾元素下标之间的生成一个随机整数。接下来,将当前元素和随机选出的下标所指的元素互相交换
class Solution {
int[] nums;
int[] assist;
Random rand = new Random();
public Solution(int[] nums) {
this.nums = nums;
}
public int[] reset() {
return nums;
}
public int[] shuffle() {
assist = nums.clone();
for(int i = 0 ; i < assist.length ; i++){
int index = i + rand.nextInt(assist.length - i);
swap(assist , i , index);
}
return assist;
}
void swap(int[] nums , int a , int b){
int temp = nums[a];
nums[a] = nums[b];
nums[b] = temp;
}
}
/**
* Your Solution object will be instantiated and called as such:
* Solution obj = new Solution(nums);
* int[] param_1 = obj.reset();
* int[] param_2 = obj.shuffle();
*/
- 暴力
class Solution {
int[] nums;
Random rand = new Random();
public Solution(int[] nums) {
this.nums = nums;
}
public int[] reset() {
return nums;
}
public int[] shuffle() {
//这里用LinkedList,比用ArrayList效率高,因为下面有删除操作。
List<Integer> list = new LinkedList<Integer>();
for (int i = 0; i < nums.length; i++) list.add(nums[i]);
int[] assist = new int[list.size()];
for (int i = 0; i < assist.length; i++) {
int index = rand.nextInt(list.size());
assist[i] = list.get(index);
list.remove(index);
}
return assist;
}
}
Trie (前缀树 / 字典树 / 单词查找树)
208. 实现 Trie (前缀树)
node.children[index]!=null
,表示该数组索引处存在对应的字母,存储的是下一个Trie的地址
Trie 的应用场景,希望你能记住 8 个字:一次建树,多次查询。
参考1、基本概念
class Trie {
private boolean isEnd;
private Trie[] children;
/** Initialize your data structure here. */
public Trie() {
isEnd = false;
children = new Trie[26];
}
/** Inserts a word into the trie. */
public void insert(String word) {
Trie node = this;
for(int i = 0 ; i < word.length() ; i++){
char ch = word.charAt(i);
int index = ch - 'a';
if(node.children[index] == null) node.children[index] = new Trie();
node = node.children[index];
}
//别忘了在最后,将 结尾节点的isEnd置为true
node.isEnd = true;
}
/** Returns if the word is in the trie. */
public boolean search(String word) {
Trie node = searchPrefix(word);
return node != null && node.isEnd == true;
}
/** Returns if there is any word in the trie that starts with the given prefix. */
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++){
char ch = prefix.charAt(i);
int index = ch - 'a';
if(node.children[index] == null) return null;
node = node.children[index];
}
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);
*/
二分
例题,模板参考https://leetcode-cn.com/problems/search-insert-position/solution/te-bie-hao-yong-de-er-fen-cha-fa-fa-mo-ban-python-/
时间复杂度: O(logN)
二分下标
「二分下标」是指在一个有序数组(该条件可以适当放宽)中查找目标元素的下标
33. 搜索旋转排序数组
【参考】(https://leetcode-cn.com/problems/search-in-rotated-sorted-array/solution/sou-suo-xuan-zhuan-pai-xu-shu-zu-by-leetcode-solut/)
- 注意等号
if(nums[mid] >= nums[l])
- 数组旋转后,分为2段递增序列,我们可以通过nums[mid] 和 nums[l]比较,来判断[l,mid]、[mid,r]哪段是递增的;
【再旋转一次怎么做,做法还是一样的,无论旋转几次,最多只有2段递增序列】
class Solution {
public int search(int[] nums, int target) {
int len = nums.length;
if(len == 1 && nums[0] != target) return -1;
int l = 0;
int r = len - 1;
int mid = 0;
while(l <= r){
mid = l + (r - l) / 2;
if(nums[mid] == target) return mid;
else{
//nums[mid] >= nums[l]:表示nums[l,mid]是递增的;
//等于范围缩小到两个情况: [3,1] ,num[l]==num[mid]。此时会出现num[l]==num[mid],此时l~mid(只有一个元素)可看做有序的,和nums[l] < nums[mid]是的情况一样,l~mid有序,可以合并成一种情况。
//如果不加等号,等于的情况会因为if else语句的判断,判断成下面mid~r是有序的,但显然[3,1]不是有序的,所以这里要加上等号;
if(nums[mid] >= nums[l]){
if(nums[l] <= target && target < nums[mid]) r = mid - 1;
else l = mid + 1;
}else{//nums[mid] < nums[l]:表示nums[mid,r]是递增的
if(nums[mid] < target && target <= nums[r]) l = mid + 1;
else r = mid - 1;
}
}
}
return -1;
}
}
81. 搜索旋转排序数组 II
相比33题,元素有重复
针对有重复的情况,是将下面两种**无重复情况**下的划分:
nums[l] <= nums[mid]
nums[l] > nums[mid])
改为下面三种划分,将等于的情况单独提取出来,【适合重复情况】
nums[l] < nums[mid]
nums[l] == nums[mid] //若nums[l]不是目标值,因为相等,所以可以缩小一个范围,即l++;
nums[l] > nums[mid])
class Solution {
public boolean search(int[] nums, int target) {
int length = nums.length;
if(length == 0) return false;
if(length == 1 && nums[0] != target) return false;
int l = 0 ;
int r = length - 1;
while(l<=r){
int mid = l + (r - l)/2;
if(nums[mid] == target) return true;
if(nums[l] < nums[mid]){
if(nums[l] <= target && target < nums[mid]) r = mid - 1;
else l = mid + 1;
}
else if(nums[l] == nums[mid]){
if(nums[l] == target) return true;
else l++;
}
else{
if(nums[mid] < target && target <= nums[r]) l = mid + 1;
else r = mid - 1;
}
}
return false;
}
}
34. 在排序数组中查找元素的第一个和最后一个位置
【参考:面试题53-I 数字在排序数组中出现的次数】(https://blog.youkuaiyun.com/qq_42647047/article/details/110294011)
class Solution {
public int[] searchRange(int[] nums, int target) {
int[] result = {-1 , -1};
int l = 0 , r = nums.length - 1 , mid = 0 , left = 0 , right = 0;
if(nums.length == 0) return result;
while(l <= r){
mid = l + (r - l) / 2;
if(nums[mid] > target) r = mid - 1;
else if(nums[mid] < target) l = mid + 1;
else{
//mid == nums.length - 1 一定要放在前面,由于||的短路特性,前面为真,后面就不用在判断了。这样当mid == nums.length - 1 为真时,后面nums[mid] != nums[mid + 1]不用判断了避免了mid + 1越界的错误;当mid == nums.length - 1 为真假时,mid + 1也不会越界;这样就利用||的短路特性处理了越界问题。
if(mid == nums.length - 1 || nums[mid] != nums[mid + 1]) break;
else l = mid + 1;
}
}
if(nums[mid] != target) return result;
else right = mid;
l = 0 ;
r = nums.length - 1;
while(l <= r){
mid = l + (r - l) / 2;
if(nums[mid] > target) r = mid - 1;
else if(nums[mid] < target) l = mid + 1;
else{
//mid == 0一定要放在前面;
if(mid == 0 || nums[mid] != nums[mid - 1]) break;
else r = mid - 1;
}
}
left = mid;
return new int[]{left , right};
}
}
153. 寻找旋转排序数组中的最小值
class Solution {
public int findMin(int[] nums) {
//情况1 刚好中间值是最小值
//情况2 边界值
int left = 0;
int right = nums.length - 1;
while(left < right){
int mid = left + (right - left) / 2;
if(nums[mid] < nums[right]) {
//拿到比较值的索引,缩小范围(情况1-中间值也许是最小值)
right = mid;
} else {
left = mid + 1;
}
}
return nums[left];
}
}
154.寻找旋转排序数组中的最小值 II
相比153,元素有重复
针对有重复的情况,是将下面两种**无重复情况**下的划分:
nums[l] <= nums[mid]
nums[l] > nums[mid])
改为下面三种划分,将等于的情况单独提取出来,【适合重复情况】
nums[l] < nums[mid]
nums[l] == nums[mid] //若nums[l]不是目标值,因为相等,所以可以缩小一个范围,即l++;
nums[l] > nums[mid])
class Solution {
public int findMin(int[] nums) {
//情况1,中间值就是最小值
//情况2,边界值
//情况3,去掉重复数
int left = 0;
int right = nums.length - 1;
while(left < right) {
//取中间值
int mid = left + (right - left) / 2;
if(nums[mid] < nums[right]) {
right = mid;
} else if(nums[mid] == nums[right]) {
//有重复数据,向左边移一位(调试知道的)
right = right - 1;
} else {
left = mid + 1;
}
}
return nums[left];
}
}
852. 山脉数组的峰顶索引
- 山脉数组中:arr[mid] 与 arr[mid + 1]比较
class Solution {
public int peakIndexInMountainArray(int[] arr) {
int length = arr.length;
int left = 0 ;
int right = length - 1;
int mid = 0;
while(left < right){
mid = left + (right - left) / 2;
//判断是否比mid右边的值大
if(arr[mid] < arr[mid + 1]) left = mid + 1;
else right = mid;
}
return left;
}
}
1095. 山脉数组中查找目标值
参考 :https://leetcode-cn.com/problems/find-in-mountain-array/solution/shi-yong-chao-hao-yong-de-er-fen-fa-mo-ban-python-/
/**
* // This is MountainArray's API interface.
* // You should not implement it, or speculate about its implementation
* interface MountainArray {
* public int get(int index) {}
* public int length() {}
* }
*/
class Solution {
public int findInMountainArray(int target, MountainArray mountainArr) {
int length = mountainArr.length();
int left = 0;
int right = length - 1;
int top = findMountainTop(mountainArr , 0 , length - 1);
if(mountainArr.get(top) == target) return top;
int leftRes = findInLeft(mountainArr , 0 , top - 1 , target);
if(leftRes != -1) return leftRes;
int rightRes = findInRight(mountainArr , top + 1 , length - 1 , target);
return rightRes;
}
int findMountainTop(MountainArray mountainArr , int left , int right){
int mid = 0;
while(left < right){
mid = left + (right - left) / 2;
if(mountainArr.get(mid) < mountainArr.get(mid + 1)) left = mid + 1;
else right = mid;
}
return left;
}
int findInLeft(MountainArray mountainArr , int left , int right , int target){
int mid = 0;
while(left < right){
mid = left + (right - left) / 2;
if(mountainArr.get(mid) < target) left = mid + 1;
else right = mid;
}
if(mountainArr.get(left) == target) return left;
else return -1;
}
int findInRight(MountainArray mountainArr , int left , int right , int target){
int mid = 0;
while(left < right){
mid = left + (right - left) / 2;
if(mountainArr.get(mid) > target) left = mid + 1;
else right = mid;
}
if(mountainArr.get(left) == target) return left;
else return -1;
}
}
4. 寻找两个正序数组的中位数
参考 https://leetcode-cn.com/problems/median-of-two-sorted-arrays/solution/he-bing-yi-hou-zhao-gui-bing-guo-cheng-zhong-zhao-/
在两个排序数组中,找到一个,分割线,满足:
- 当数组的总长度为偶数的时候,分割线左右的数字个数总和相等;
当数组的总长度为奇数的时候,分割线左数字个数比右边仅仅多 1个; - 分割线左边的所有元素都小于等于(不大于)分割线右边的所有元素。
找到分界线i,j后,若总长度length为奇数,则中位数就是分割线左边的最大值;
若总长度length为偶数,中位数就是分割线左边的最大值与分割线右边的最小值的平均数;即,左边取最大,右边取最小;
分割线左边的值 num1 --> num[i-1] ; num2 --> num[j-1]
分割线右边的值 num1 --> num[i] ; num2 --> num[j]
特殊情况处理:
//i == 0 ,中位数左边的数全在num2,i=0,i-1会出界;
int leftMaxNum1 = (i == 0 ? Integer.MIN_VALUE : nums1[i - 1]);
//i == m ,说明num1都是中位数左边的数,i = m,会出界
int rightMinNum1 = (i == m ? Integer.MAX_VALUE : nums1[i]);
//j == 0 ,中位数左边的数全在num1,j =0,j-1会出界;
int leftMaxNum2 = (j == 0 ? Integer.MIN_VALUE : nums2[j - 1]);
//j == n ,说明num2都是中位数左边的数,i = n,会出界
int rightMinNum2 = (j == n ? Integer.MAX_VALUE : nums2[j]);
注:为了使得搜索更快,我们把更短的数组设置为 nums1 ,因为使用二分查找法,在它的长度的对数时间复杂度内完成搜索;
class Solution {
public double findMedianSortedArrays(int[] nums1, int[] nums2) {
if(nums1.length > nums2.length){
int[] temp = nums1;
nums1 = nums2;
nums2 = temp;
}
int m = nums1.length;
int n = nums2.length;
int left = 0;
//注,这里应为right = m,而不是right = m - 1;因为下标m代表前面有m个中位数,所以前面有m个中位数时,需要下标取到m;
int right = m;
int i = 0;
int j = 0;
int countLeft = (n + m + 1) / 2;
while(left < right){
i = left + (right - left) / 2;
j = countLeft - i;
if(nums2[j - 1] > nums1[i]) left = i + 1;
else right = i;
}
//找到分界线i,j后,若总长度length为奇数,则中位数就是分割线左边的最大值;
//若总长度length为偶数,中位数就是分割线左边的最大值与分割线右边的最小值的平均数;即,左边取最大,右边取最小;
//分割线左边的值 num1 --> num[i-1] ; num2 --> num[j-1]
//分割线右边的值 num1 --> num[i] ; num2 --> num[j]
i = left;
j = countLeft - left;
//i == 0 ,中位数左边的数全在num2,i=0,i-1会出界;
int leftMaxNum1 = (i == 0 ? Integer.MIN_VALUE : nums1[i - 1]);
//i == m ,说明num1都是中位数左边的数,i = m,会出界
int rightMinNum1 = (i == m ? Integer.MAX_VALUE : nums1[i]);
//j == 0 ,中位数左边的数全在num1,j =0,j-1会出界;
int leftMaxNum2 = (j == 0 ? Integer.MIN_VALUE : nums2[j - 1]);
//j == n ,说明num2都是中位数左边的数,i = n,会出界
int rightMinNum2 = (j == n ? Integer.MAX_VALUE : nums2[j]);
if(((m + n) % 2) == 1) return (double)Math.max(leftMaxNum1 , leftMaxNum2);
else{
int leftMax = Math.max(leftMaxNum1 , leftMaxNum2);
int rightMin = Math.min(rightMinNum1 , rightMinNum2);
return (double)(leftMax + rightMin) / 2;
}
}
}
二分答案
题目要我们找的是一个整数,并且这个整数我们知道它可能的最小值和最大值,即,
知道答案的范围
,并且这个范围是有序的
。此时,我们可以考虑使用二分查找算法在这个答案范围
中找到这个目标值,即二分答案。
第 69 题:x 的平方根
- x 的平方根,肯定在[1,x]内,从一个已知的范围内找一个数-----二分;
- 通过对暴力解法的分析,我们知道了,需要返回最后一个平方以后小于等于 x 的数。使用二分查找的思路 2,关键在于分析那些数是我们不要的。
- 注意:这里出现了left = mid;mid需要向上取整;否则,会死循环;
class Solution {
public int mySqrt(int x) {
if (x == 0) {
return 0;
}
int left = 1;
//x 可缩小为x/2;
int right = x / 2;
while (left < right){
// 出现left = mid; mid需向上取整
int mid = (left + right + 1) / 2;
//如果判别条件写成 s * s == x ,会发生整型溢出,应该写成 s = x / s ,判别条件 s * s > x 也是类似这样写。
if (mid > x / mid) {
// mid 以及大于 mid 的数一定不是解,下一轮搜索的区间为 [left, mid - 1]
right = mid - 1;
} else {
left = mid;
}
}
return left;
}
}
要求小数点精确到xxx
//precision精确度
//a:求平方根的数据
class Solution1 {
public double mySqrt(double a , double precision) {
if(a == 0) return 0;
if(a == 1) return 1;
double l = 0;
double r = 0;
double mid = 0;
if(a < 1){
l = a;
r = 1;
}else{
l = 1;
r = a;
}
while(l <= r){
mid = l + (r - l + 1) / 2;
//|mid*mid - a| < precision;=====>(mid < (a + precision) / mid && mid > (a - precision) / mid)
//将mid*mid拆开,用 / 表示,是为了防止mid*mid溢出;
if(mid < (a + precision) / mid && mid > (a - precision) / mid) return mid;
else if(mid > a / mid) r = mid;
else l = mid;
}
return -1;
}
}
287. 寻找重复数
类似的题 【面试题53-II. 0~n-1中缺失的数字】(https://blog.youkuaiyun.com/qq_42647047/article/details/110294011)【其对下标进行二分】
- 题目要找的是一个 整数,这个整数有明确的范围(1 到 n) 之前,因此可以使用「二分查找」;
- 每一次猜一个数,然后 遍历整个输入数组,进而缩小搜索区间,最后确定重复的是哪个数;
- 不是在「输入数组」上直接使用「二分查找」,而是在数组 [1, 2, ……, n] (有序数组)上使用「二分查找」。
本题中:我们定义cnt[i] 表示nums 数组中小于等于 i 的数有多少个,由参考的,cnt数组是有序的【target之前,cnt[i] <= i ;之后cnt[i] > i ,这也说明cnt数组递增】,我们对cnt数组二分。
class Solution {
public int findDuplicate(int[] nums) {
int left = 0;
int right = nums.length;
while(left < right){
int cont = 0;
int mid = left + (right - left) / 2;
for(int i = 0 ; i < nums.length ; i++){
if(nums[i] <= mid) cont++;
}
if(cont > mid) right = mid;
else left = mid + 1;
}
return left;
}
}
300. 最长递增子序列
降低复杂度切入点: 动态规划中,遍历计算 dp 列表需 O(N),计算每个 dp[k]需 O(N)。
1、动态规划中,通过线性遍历来计算 dp的复杂度无法降低;
2、每轮计算中,需要通过线性遍历 [0,k)区间元素来得到 dp[k]d。我们考虑:是否可以通过重新设计状态定义,使整个 dp 为一个排序列表;这样在计算每个 dp[k] 时,就可以通过二分法遍历 [0,k) 区间元素,将此部分复杂度由 O(N)降至 O(logN)。
【参考】https://leetcode-cn.com/problems/longest-increasing-subsequence/solution/zui-chang-shang-sheng-zi-xu-lie-dong-tai-gui-hua-2/
【参考】(https://leetcode-cn.com/problems/longest-increasing-subsequence/solution/zui-chang-shang-sheng-zi-xu-lie-by-leetcode-soluti/)
没看懂。。。。
判别条件复杂的二分查找问题(二分答案的进阶)
参考https://leetcode-cn.com/leetbook/read/learning-algorithms-with-leetcode/x4mixi/
在「二分答案」中看到的问题是,根据目标变量具有的 单调 性质编写判别函数。
「力扣」上还有一类问题是这样的:目标变量和另一个变量有相关关系(一般而言是线性关系),目标变量的性质不好推测,但是另一个变量的性质相对容易推测。这样的问题的判别函数通常会写成一个函数的形式,我们希望通过这节课的介绍,大家能够熟悉这一类问题的写法。
875. 爱吃香蕉的珂珂
目标变量:速度;
另一个变量:时间;
- 由于题目限制了珂珂一个小时之内只能选择一堆香蕉吃,因此速度
最大值
就是这几堆香蕉中,数量最多的那一堆。速度的最小值
是 1:目标变量(答案)的范围- 目标变量(速度 )和另一个变量(时间)的关系:
每堆香蕉吃完的耗时(时间) = 这堆香蕉的数量 / 珂珂一小时吃香蕉的数量(速度)
;
因为珂珂一个小时之内只能选择一堆香蕉吃,这里的 / 在不能整除的时候,需要 上取整。
class Solution {
public int minEatingSpeed(int[] piles, int h) {
int maxSpeed = 0;
for(int val : piles) maxSpeed = Math.max(maxSpeed , val);
int minSpeed = 1;
int left = minSpeed;
int right = maxSpeed;
int mid = 0;
while(left < right){
mid = left + (right - left) / 2;
if(f(piles , mid) > h) left = mid + 1;
else right = mid;
}
return left;
}
int f(int[] piles , int speed){
int h = 0;
for(int val : piles){
int temp = 0;
if(val % speed == 0) temp = val / speed;
else temp = val / speed + 1;
h += temp;
}
return h;
}
}
总结
二分的题,当你找到要对「什么」进行二分的时候,就已经成功一半了。
记忆化搜索
322. 零钱兑换
- 不加记忆数组剪枝(普通回溯,超时)
class Solution {
public int coinChange(int[] coins, int amount) {
int res = dfs(coins , amount);
return res;
}
int dfs(int[] coins, int amount){
if(amount == 0) return 0;
if(amount <0) return -1;
int min = Integer.MAX_VALUE;
for(int coin : coins){
int temp = dfs(coins , amount - coin);
if(temp >=0 && temp < min){
min = temp + 1;
}
}
if(min == Integer.MAX_VALUE) return -1;
else return min;
}
}
- 加记忆数组剪枝
class Solution {
public int coinChange(int[] coins, int amount) {
int[] visited = new int[amount + 1];
int res = dfs(coins , amount , visited);
return res;
}
int dfs(int[] coins, int amount , int[] visited){
if(amount == 0) return 0;
if(amount <0) return -1;
if(visited[amount]!= 0) return visited[amount];
int min = Integer.MAX_VALUE;
for(int coin : coins){
int temp = dfs(coins , amount - coin , visited);
if(temp >=0 && temp < min){
min = temp + 1;
}
}
if(min == Integer.MAX_VALUE) visited[amount] = -1;
else visited[amount] = min;
return visited[amount];
}
}
比特位
338. 比特位计数
涉及到位数运算,且涉及到1的个数,要想到x=x & (x−1),该运算将 x 的二进制表示的最后一个 1 变成 0
class Solution {
public int[] countBits(int n) {
int[] res = new int[n + 1];
for(int i = 1 ; i <= n ; i++){
res[i] = res[i & (i - 1)] + 1;
}
return res;
}
}
461. 汉明距离
- 先异或;
- 再运用Brian Kernighan 算法;
class Solution {
public int hammingDistance(int x, int y) {
int res = x ^ y;
int count = 0;
while(res != 0){
++count;
res = res & (res - 1);
}
return count;
}
}
401. 二进制手表
参考https://leetcode-cn.com/problems/binary-watch/solution/er-jin-zhi-shou-biao-by-leetcode-solutio-3559/
class Solution {
public List<String> readBinaryWatch(int turnedOn) {
List<String> res = new ArrayList<>();
if(turnedOn > 8) return res;
for (int h = 0; h < 12; ++h) {
for (int m = 0; m < 60; ++m) {
if (Integer.bitCount(h) + Integer.bitCount(m) == turnedOn) {
res.add(h + ":" + (m < 10 ? "0" : "") + m);
}
}
}
return res;
}
}
136. 只出现一次的数字
class Solution {
public int singleNumber(int[] nums) {
int res = 0;
for(int val : nums){
res ^= val;
}
return res;
}
}
面试题56 - I. 数组只出现一次的两个数字
面试题56 - II. 数组中唯一出现一次的数字
参考https://blog.youkuaiyun.com/qq_42647047/article/details/110097391
总结
1、 Brian Kernighan 算法:
涉及到位数运算,且涉及到1的个数,要想到x=x & (x−1),该运算将 x 的二进制表示的最后一个 1 变成 0
前缀和
- 涉及到
连续子数组和
的题- 前缀和经常与哈希表结合使用,因为希望遍历得到前缀和的时候,一边遍历一边记住结果;
- 哈希表常设map.put(0 , 1) map.put(0 , -1),来处理起点是下标0的情况
- 最后再说下,前缀和不是一定要把列表循环一遍,创建好前缀和数组再使用;
而是可以边计算前缀和边使用前缀和,节省空间。【因为使用前缀和的时候也是从前向后使用,这就保证了可以边计算前缀和边使用前缀和】
560. 和为K的子数组
map.put(0 , 1);的理解
和 面试题57 - II. 和为s的连续正数序列 不同,该题数组不是有序的
参考 思路
思路的基础上更细致的分析
class Solution {
public int subarraySum(int[] nums, int k) {
int n = nums.length;
int res = 0;
int preSum = 0;
HashMap<Integer , Integer> map = new HashMap<>();
map.put(0 , 1);
for(int i =0 ; i < n ; i++){
preSum += nums[i];
res += map.getOrDefault(preSum-k , 0);
map.put(preSum , map.getOrDefault(preSum , 0) + 1);
}
return res;
}
}
437. 路径总和 III
和上题560,类似。
[参考】(https://leetcode-cn.com/problems/path-sum-iii/solution/qian-zhui-he-di-gui-hui-su-by-shi-huo-de-xia-tian/)
map.put(0,1);
如果某个节点的前缀和为target,那么它本身就是一个解,通过公式map.getOrDefault(preSum - targetSum , 0)得,需前缀和为0对应的值为1,才能正好得到值1;
class Solution {
int count;
int preSum;
public int pathSum(TreeNode root, int targetSum) {
if(root == null) return 0;
HashMap<Integer , Integer> map = new HashMap<>();
map.put(0,1);
dfs(root , targetSum , map);
return count;
}
void dfs(TreeNode root , int targetSum , HashMap<Integer , Integer> map){
if(root == null) return;
preSum += root.val;
count += map.getOrDefault(preSum - targetSum , 0);
map.put(preSum , map.getOrDefault(preSum , 0)+1);
//进入下一层
dfs(root.left , targetSum , map);
dfs(root.right , targetSum , map);
//回溯:恢复状态,
//1、map中减1,即减去当前节点的前缀和;
//2、前缀和preSum减去当前节点的值
map.put(preSum , map.getOrDefault(preSum , 0)-1);
preSum -= root.val;
}
}
525. 连续数组
参考:https://leetcode-cn.com/problems/contiguous-array/solution/qian-zhui-he-chai-fen-ha-xi-biao-java-by-liweiwei1/
- map.put(0 , -1);
可能存在nums前N个数字和恰好为0的情况,我们预制字典{0,-1}来规避该问题。
class Solution {
public int findMaxLength(int[] nums) {
int len = nums.length;
int[] prefixSum = new int[len + 1];
prefixSum[0] = 0;
for(int i = 1 ; i <= len ; i++) prefixSum[i] = prefixSum[i - 1] + (nums[i - 1] == 0 ? -1 : 1);
HashMap<Integer , Integer> map = new HashMap<>();
int res = 0;
map.put(0 , -1);
for(int i = 0 ; i < len ; i++){
int preSum = prefixSum[i + 1];
if(!map.containsKey(preSum)) map.put(preSum , i);
// 因为求的是最长的长度,只记录前缀和第一次出现的下标,
//再次出现相同的前缀和时,下面不能更新map中前缀和对应的下标
else res = Math.max(res , i - map.get(preSum));
}
return res;
}
}
523. 连续的子数组和
参考:https://leetcode-cn.com/problems/continuous-subarray-sum/solution/523-lian-xu-de-zi-shu-zu-he-qian-zhui-he-zl78/
- 同余定理:即当两个数除以某个数的余数相等,那么二者相减后肯定可以被该数整除;
Hash表:{余数 : 下标}
class Solution {
public boolean checkSubarraySum(int[] nums, int k) {
int len = nums.length;
if(len < 2) return false;
int preSum = 0;
HashMap<Integer , Integer> map = new HashMap<>();
map.put(0 , -1);
for(int i = 0 ; i < len ; i++){
preSum += nums[i];
int remainder = preSum % k;
if(map.containsKey(remainder)){
if((i - map.get(remainder)) >= 2) return true;
}else map.put(remainder , i);
}
return fal