常见编程模式 看了一篇推文,整理一下,未完
滑动窗口 概念 一种高级双指针技巧的算法框架,用于在给定数组或链表的特定窗口上执行所需的操作,可以将嵌套的循环问题,转化为单循环问题,降低时间复杂度 一半从第一个元素开始滑动,并且逐个元素向右滑,并且根据所求的问题来调整窗口的长度(也就是移动左指针)
特点
问题的输入是一种线性结构,比如链表、数组或者字符串
被要求查找最长,最短的子字符串,子数组或所需要的值,往往类似于“请找到满足xx的最x的区间(子串、子数组)的xx”这类问题都可以使用该方法进行解决。
常见问题
大小为K的子数组的最大和
带有K个不同字符的最长子字符串
寻找字符相同,但是排序不一样的字符串
示例 最小覆盖子串 hard
给定一个字符串S,字符串T,在字符串S中找到,包含T所有字母的最小子串
(寻找解)不断增加right指针扩大窗口,直到窗口的字符串符合要求(包含了T中的所有字符)
(优化解)停止增加right,不断增加left缩小窗口,直到不再符合要求
重复上面两个步骤,直到right到达了字符串S的尽头
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 public String minWindow (String s, String t) { if (s.length()<t.length()){ return "" ; } int left = 0 ; int right = 0 ; int resL = s.length()+1 ; Map<Character, Integer> needs = new HashMap<>(); Map<Character, Integer> window = new HashMap<>(); String res = new String(s); for (int i = 0 ; i < t.length(); i++) { needs.put(t.charAt(i), needs.getOrDefault(t.charAt(i), 0 ) + 1 ); } while (right < s.length()) { window.put(s.charAt(right), window.getOrDefault(s.charAt(right), 0 ) + 1 ); while (satisfied(needs, window) && left <= right) { if (right - left + 1 < resL) { res = s.substring(left, right + 1 ); resL = res.length(); } window.put(s.charAt(left), window.getOrDefault(s.charAt(left), 0 ) - 1 ); left++; } right++; } if (resL == s.length()+1 ){ return "" ; } return res; } public boolean satisfied (Map<Character, Integer> needs, Map<Character, Integer> window) { for (Character c : needs.keySet()) { if (needs.get(c).intValue() > window.getOrDefault(c, 0 )) { return false ; } } return true ; }
可以用两个map来记录需要的字符和当前已有的字符,分别为need和window
爱生气的书店老板 Medium
https://leetcode-cn.com/problems/grumpy-bookstore-owner/
简单的滑动窗口,窗口长度恒定为X
先计算初始化X可以带来的收益,然后不断移动
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 public int maxSatisfied (int [] customers, int [] grumpy, int X) { int total = 0 ; int n = customers.length; for (int i = 0 ; i < n; i++) { if (grumpy[i] == 0 ) { total += customers[i]; } } int increase = 0 ; for (int i = 0 ; i < X; i++) { increase += customers[i] * grumpy[i]; } int maxIncrease = increase; for (int i = X; i < n; i++) { increase = increase - customers[i - X] * grumpy[i - X] + customers[i] * grumpy[i]; maxIncrease = Math.max(maxIncrease, increase); } return total + maxIncrease; }
找到字符串中所有字母异位词 Medium
给定一个字符串s和一个非空字符串p,找到s中所有是p的字母异位词的子串,返回这些子串的初始索引
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 public List<Integer> findAnagrams (String s, String p) { List<Integer> resultList = new ArrayList<>(); int [] pFreq = new int [26 ]; for (int i = 0 ; i < p.length(); i++) { pFreq[p.charAt(i)-'a' ]++; } int start = 0 , end = -1 ; while (start <s.length()) { if (end+1 < s.length() && end-start+1 <p.length()) { end++; }else { start++; } if (end-start+1 == p.length() && isAnagrams(s.substring(start,end+1 ), pFreq)) { resultList.add(start); } } return resultList; } private boolean isAnagrams (String window, int [] pFreq) { int [] windowFreq = new int [26 ]; for (int i = 0 ; i < window.length(); i++) { windowFreq[window.charAt(i)-'a' ]++; } for (int j = 0 ; j < 26 ; j++) { if (windowFreq[j] != pFreq[j]) { return false ; } } return true ; }
无重复最长子串
不含有重复字符的最长子串的长度
1 2 3 4 5 6 7 8 9 10 11 public int lengthOfLongestSubstring (String s) { Map<Character, Integer> dic = new HashMap<>(); int i = -1 , res = 0 ; for (int j = 0 ; j < s.length(); j++) { if (dic.containsKey(s.charAt(j))) i = Math.max(i, dic.get(s.charAt(j))); dic.put(s.charAt(j), j); res = Math.max(res, j - i); } return res; }。
至多包含两个不同字符的最长子串
给定一个字符串 s ,找出 至多 包含两个不同字符的最长子串 t 。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 int lengthOfLongestSubstringTwoDistinct (string s) { unordered_map <char , int > m; int cnt = 0 ; int left = 0 , right = 0 ; int maxLen = 0 ; while (right < s.size()){ if (m[s[right]] == 0 ) cnt ++; m[s[right++]]++; while (cnt > 2 ){ m[s[left]]--; if (m[s[left++]] == 0 ){ cnt --; } } maxLen = max(maxLen, right - left); } return maxLen; }
滑动窗口最大值
给你一个整数数组 nums,有一个大小为 k 的滑动窗口从数组的最左侧移动到数组的最右侧。你只可以看到在滑动窗口内的 k 个数字。滑动窗口每次只向右移动一位。
返回每个滑动窗口中的最大值。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 class Solution { public int [] maxSlidingWindow(int [] nums, int k) { int n = nums.length; PriorityQueue<int []> pq = new PriorityQueue<int []>(new Comparator<int []>() { public int compare (int [] pair1, int [] pair2) { return pair1[0 ] != pair2[0 ] ? pair2[0 ] - pair1[0 ] : pair2[1 ] - pair1[1 ]; } }); for (int i = 0 ; i < k; ++i) { pq.offer(new int []{nums[i], i}); } int [] ans = new int [n - k + 1 ]; ans[0 ] = pq.peek()[0 ]; for (int i = k; i < n; ++i) { pq.offer(new int []{nums[i], i}); while (pq.peek()[1 ] <= i - k) { pq.poll(); } ans[i - k + 1 ] = pq.peek()[0 ]; } return ans; } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 public int [] maxSlidingWindow(int [] nums, int k) { int n = nums.length; Deque<Integer> deque = new LinkedList<Integer>(); for (int i = 0 ; i < k; ++i) { while (!deque.isEmpty() && nums[i] >= nums[deque.peekLast()]) { deque.pollLast(); } deque.offerLast(i); } int [] ans = new int [n - k + 1 ]; ans[0 ] = nums[deque.peekFirst()]; for (int i = k; i < n; ++i) { while (!deque.isEmpty() && nums[i] >= nums[deque.peekLast()]) { deque.pollLast(); } deque.offerLast(i); while (deque.peekFirst() <= i - k) { deque.pollFirst(); } ans[i - k + 1 ] = nums[deque.peekFirst()]; } return ans; }
总结 1 2 3 4 5 6 7 8 9 10 11 int left = 0 , right = 0 ;while (right < s.size()) { window.add(s[right]); right++; while (valid) { window.remove(s[left]); left++; } }
二指针 概念 两个指针以一前一后的模式在数据结构中进行迭代,直到一个或者两个指针达到了某种特定的条件,比如你必须将一个数组的每个元素与其他元素做比对的时候
特点
处理排序数组并且需要查找满足某些条件的一组元素的问题
数组中的元素集是配对,三元组甚至是子数组
快慢指针
常见问题 三数之和
在数组中找到三元组,其中元素个数之和为0
先排序,方便去除重复
先确定一个元素,然后定好target,在剩下的元素中找
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 public List<List<Integer>> threeSum(int [] nums) { List<List<Integer>> ans = new ArrayList<>(); if (nums == null || nums.length <= 2 ) return ans; Arrays.sort(nums); for (int i = 0 ; i < nums.length - 2 ; i++) { if (nums[i] > 0 ) break ; if (i > 0 && nums[i] == nums[i - 1 ]) continue ; int target = -nums[i]; int left = i + 1 , right = nums.length - 1 ; while (left < right) { if (nums[left] + nums[right] == target) { ans.add(new ArrayList<>(Arrays.asList(nums[i], nums[left], nums[right]))); left++; right--; while (left < right && nums[left] == nums[left - 1 ]) left++; while (left < right && nums[right] == nums[right + 1 ]) right--; } else if (nums[left] + nums[right] < target) { left++; } else { right--; } } } return ans; }
四数之和
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 public List<List<Integer>> fourSum(int [] nums, int target) { Arrays.sort(nums); List<List<Integer>> res = new ArrayList<>(); if (nums==null || nums.length<=3 ){ return res; } int len = nums.length; for (int i = 0 ;i<len;i++){ if (i-1 >=0 && nums[i]==nums[i-1 ]){ continue ; } for (int j=i+1 ;j<len;j++){ if (j-1 >=i+1 && nums[j-1 ]==nums[j]){ continue ; } int tr = target - nums[i] - nums[j]; int left = j+1 ,right = len-1 ; while (left<right){ if (nums[left]+nums[right]==tr){ List<Integer> tp = new ArrayList<>(); tp.add(nums[i]); tp.add(nums[j]); tp.add(nums[left]); tp.add(nums[right]); res.add(tp); while (left<right && nums[left]==nums[++left]); while (left<right && nums[right]==nums[--right]); }else if (nums[left]+nums[right]>tr){ right--; }else { left++; } } } } return res; }
比较含退格的字符串
给定 S 和 T 两个字符串,当它们分别被输入到空白的文本编辑器后,判断二者是否相等,并返回结果。 # 代表退格字符。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 public boolean backspaceCompare (String S, String T) { int i = S.length() - 1 , j = T.length() - 1 ; int skipS = 0 , skipT = 0 ; while (i >= 0 || j >= 0 ) { while (i >= 0 ) { if (S.charAt(i) == '#' ) { skipS++; i--; } else if (skipS > 0 ) { skipS--; i--; } else { break ; } } while (j >= 0 ) { if (T.charAt(j) == '#' ) { skipT++; j--; } else if (skipT > 0 ) { skipT--; j--; } else { break ; } } if (i >= 0 && j >= 0 ) { if (S.charAt(i) != T.charAt(j)) { return false ; } } else { if (i >= 0 || j >= 0 ) { return false ; } } i--; j--; } return true ; }
大餐记数
在数组中,找到一个二元组,其中元素的和是2的幂,可以重复
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 public int countPairs (int [] deliciousness) { Map<Integer,Integer> map = new HashMap<>(); long ans = 0 ; for (int i = 0 ;i < deliciousness.length;++i){ for (int j = 0 ;j < 22 ;++j){ int target = (int )Math.pow(2 ,j); if (target - deliciousness[i] < 0 ) continue ; if (map.containsKey(target - deliciousness[i])){ ans += map.get(target - deliciousness[i]); } } map.put(deliciousness[i],map.getOrDefault(deliciousness[i],0 ) + 1 ); } ans %= (1e9 + 7 ); return (int )ans; }
字符串相加
两个字符串,模拟加法
不禁会想起当年写ALU的时候
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 public String addStrings (String num1, String num2) { StringBuilder res = new StringBuilder("" ); int i = num1.length() - 1 , j = num2.length() - 1 , carry = 0 ; while (i >= 0 || j >= 0 ){ int n1 = i >= 0 ? num1.charAt(i) - '0' : 0 ; int n2 = j >= 0 ? num2.charAt(j) - '0' : 0 ; int tmp = n1 + n2 + carry; carry = tmp / 10 ; res.append(tmp % 10 ); i--; j--; } if (carry == 1 ) res.append(1 ); return res.reverse().toString(); }
链表翻转
如题
1 2 3 4 5 6 7 8 9 10 11 public ListNode reverseList (ListNode head) { ListNode prev = null ; ListNode curr = head; while (curr != null ) { ListNode next = curr.next; curr.next = prev; prev = curr; curr = next; } return prev; }
快慢指针 概念 使用两个在数组,字符串或者链表中以不同速度移动的指针,在处理循环链表或者数组的时候非常有用 通过以不同速度(人为规定的速度,或者一定的间距)进行移动,比如在一个循环链表中,只要这两个指针在同一个循环中,快速指针就会追赶上慢速指针,并且在环中相遇
特点
当我们需要处理链表或者数组中的循环问题
当我们需要直到特定元素的位置或者链表的总长度的时候
什么时候快慢指针而不是二指针呢
常见问题
示例 环形链表
给定一个链表,判断链表中是否有环。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 public boolean hasCycle (ListNode head) { if (head == null || head.next == null ) { return false ; } ListNode slow = head; ListNode fast = head.next; while (slow != fast) { if (fast == null || fast.next == null ) { return false ; } slow = slow.next; fast = fast.next.next; } return true ; }
环形链表2
判断一个环路开始的位置
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 public ListNode detectCycle (ListNode head) { if (head == null || head.next == null ) { return null ; } ListNode fast = head; ListNode slow = head; while (true ) { if (fast == null || fast.next == null ) { return null ; } fast = fast.next.next; slow = slow.next; if (fast == slow) { break ; } } slow = head; while (fast != slow) { fast = fast.next; slow = slow.next; } return fast; }
寻找重复的数字
给定一个包含 n + 1 个整数的数组 nums ,其数字都在 1 到 n 之间(包括 1 和 n),可知至少存在一个重复的整数。
由于数字都在1-n之间,可不可以理解成,这是一个用数组表示的图
那么就相当于在图中找环的入口
floyed算法,两个指针在有环的情况下一定会相遇
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 public int findDuplicate (int [] nums) { int slow = 0 , fast = 0 ; do { slow = nums[slow]; fast = nums[nums[fast]]; } while (slow != fast); int finder = 0 ; while (slow != finder) { slow = nums[slow]; finder = nums[finder]; } return slow; }
回文链表
判断一个链表是否是回文链表
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 public boolean isPalindrome (ListNode head) { if (head == null || head.next == null ) { return true ; } ListNode fast = head.next; ListNode slow = head; while (fast != null && fast.next != null ) { fast = fast.next.next; slow = slow.next; } slow = slow.next; ListNode p = slow; ListNode q = p.next; while (q!= null ) { ListNode tmp = q.next; q.next = p; p = q; q= tmp; } slow.next = null ; while (p!= null ){ if (p.val != head.val) { return false ; } p = p.next; head = head.next; } return true ; }
删除链表倒数N个节点
如题
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 public ListNode removeNthFromEnd (ListNode head, int n) { ListNode dummy = new ListNode(0 , head); ListNode first = head; ListNode second = dummy; for (int i = 0 ; i < n; ++i) { first = first.next; } while (first != null ) { first = first.next; second = second.next; } second.next = second.next.next; ListNode ans = dummy.next; return ans; }
重排链表
给定一个单链表 L:L0→L1→…→Ln-1→Ln , 将其重新排列后变为: L0→Ln→L1→Ln-1→L2→Ln-2→…
快满指针找到链表的中点
将后半部分翻转
前半部分和后半部分合并
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 public void reorderList (ListNode head) { if (head == null ) { return ; } ListNode mid = middleNode(head); ListNode l1 = head; ListNode l2 = mid.next; mid.next = null ; l2 = reverseList(l2); mergeList(l1, l2); } public ListNode middleNode (ListNode head) { ListNode slow = head; ListNode fast = head; while (fast.next != null && fast.next.next != null ) { slow = slow.next; fast = fast.next.next; } return slow; } public ListNode reverseList (ListNode head) { ListNode prev = null ; ListNode curr = head; while (curr != null ) { ListNode nextTemp = curr.next; curr.next = prev; prev = curr; curr = nextTemp; } return prev; } public void mergeList (ListNode l1, ListNode l2) { ListNode l1_tmp; ListNode l2_tmp; while (l1 != null && l2 != null ) { l1_tmp = l1.next; l2_tmp = l2.next; l1.next = l2; l1 = l1_tmp; l2.next = l1; l2 = l2_tmp; } }
合并区间 概念 合并区间模式是一种处理重叠区间的有效技术。在很多涉及区间的问题中,你既需要找到重叠的区间,也需要在这些区间重叠时合并它们。
a和b对应的区间不重叠
重叠一部分,并且b在a后面,(1,4)和(2,5)
重叠一部分,并且a在b后面,(2,5)和(1,4)
a完全包含b,(1,4)和(2,3)
b完全包含a
特点
如果被要求得到一个仅包含互斥区间的列表
如果你听到了术语重叠区间
常见问题
示例 合并区间
以数组 intervals 表示若干个区间的集合,其中单个区间为 intervals[i] = [starti, endi] 。请你合并所有重叠的区间,并返回一个不重叠的区间数组,该数组需恰好覆盖输入中的所有区间。
首先,我们将列表中的区间按照左端点升序排序。然后我们将第一个区间加入 merged 数组中,并按顺序依次考虑之后的每个区间:
如果当前区间的左端点在数组 merged 中最后一个区间的右端点之后,那么它们不会重合,我们可以直接将这个区间加入数组 merged 的末尾;
否则,它们重合,我们需要用当前区间的右端点更新数组 merged 中最后一个区间的右端点,将其置为二者的较大值。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 public int [][] merge(int [][] intervals) { if (intervals.length == 0 ) { return new int [0 ][2 ]; } Arrays.sort(intervals, (v1, v2) -> v1[0 ] - v2[0 ]); List<int []> merged = new ArrayList<int []>(); for (int i = 0 ; i < intervals.length; ++i) { int L = intervals[i][0 ], R = intervals[i][1 ]; if (merged.size() == 0 || merged.get(merged.size() - 1 )[1 ] < L) { merged.add(new int []{L, R}); } else { merged.get(merged.size() - 1 )[1 ] = Math.max(merged.get(merged.size() - 1 )[1 ], R); } } return merged.toArray(new int [merged.size()][]); }
插入区间
给出一个无重叠的 ,按照区间起始端点排序的区间列表。 在列表中插入一个新的区间,你需要确保列表中的区间仍然 有序且不重叠(如果有必要的话,可以 合并区间)。
首先将新区间左边且相离的区间加入结果集(遍历时,如果当前区间的结束位置小于新区间的开始位置,说明当前区间在新区间的左边且相离);
接着判断当前区间是否与新区间重叠,重叠的话就进行合并,直到遍历到当前区间在新区间的右边且相离,将最终合并后的新区间加入结果集;
最后将新区间右边且相离的区间加入结果集。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 public int [][] insert(int [][] intervals, int [] newInterval) { int [][] res = new int [intervals.length + 1 ][2 ]; int idx = 0 ; int i = 0 ; while (i < intervals.length && intervals[i][1 ] < newInterval[0 ]) { res[idx++] = intervals[i++]; } while (i < intervals.length && intervals[i][0 ] <= newInterval[1 ]) { newInterval[0 ] = Math.min(intervals[i][0 ], newInterval[0 ]); newInterval[1 ] = Math.max(intervals[i][1 ], newInterval[1 ]); i++; } res[idx++] = newInterval; while (i < intervals.length) { res[idx++] = intervals[i++]; } return Arrays.copyOf(res, idx); }
会议室
给定一个会议时间安排的数组 intervals ,每个会议时间都会包括开始和结束的时间 intervals[i] = [starti, endi] ,请你判断一个人是否能够参加这里面的全部会议。
1 2 3 4 5 6 7 8 9 10 11 public boolean canAttendMeetings (int [][] intervals) { Arrays.sort(intervals, (v1, v2) -> v1[0 ] - v2[0 ]); for (int i = 1 ; i < intervals.length; i++) { if (intervals[i][0 ] < intervals[i - 1 ][1 ]) { return false ; } } return true ; }
删除被覆盖区间
给你一个区间列表,请你删除列表中被其他区间所覆盖的区间。在完成所有删除操作后,请你返回列表中剩余区间的数目。(对于区间 [a, b) 和区间 [c, d),若 c <= a 且 d >= b ,则区间 [a, b) 被区间 [c, d) 覆盖)
本题和本文第一题的做法一样,首先按照区间起始端点进行排序,然后遍历区间,将第一题的判断区间是否重叠改为判断区间是否覆盖即可。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 public int removeCoveredIntervals (int [][] intervals) { Arrays.sort(intervals, new Comparator<int []>() { @Override public int compare (int [] o1, int [] o2) { return o1[0 ] == o2[0 ] ? o2[1 ] - o1[1 ]: o1[0 ] - o2[0 ]; } }); int count = 0 ; int end, prev_end = 0 ; for (int [] curr : intervals) { end = curr[1 ]; if (prev_end < end) { ++count; prev_end = end; } } return count; }
俄罗斯套娃信封
给定一些标记了宽度和高度的信封,宽度和高度以整数对形式 (w, h) 出现。当另一个信封的宽度和高度都比这个信封大的时候,这个信封就可以放进另一个信封里,如同俄罗斯套娃一样。
请计算最多能有多少个信封能组成一组“俄罗斯套娃”信封(即可以把一个信封放到另一个信封里面)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 public int maxEnvelopes (int [][] envelopes) { Arrays.sort(envelopes, new Comparator<int []>() { public int compare (int [] arr1, int [] arr2) { if (arr1[0 ] == arr2[0 ]) { return arr2[1 ] - arr1[1 ]; } else { return arr1[0 ] - arr2[0 ]; } } }); int [] secondDim = new int [envelopes.length]; for (int i = 0 ; i < envelopes.length; ++i) secondDim[i] = envelopes[i][1 ]; return lengthOfLIS(secondDim); } public int lengthOfLIS (int [] nums) { int [] dp = new int [nums.length]; int len = 0 ; for (int num : nums) { int i = Arrays.binarySearch(dp, 0 , len, num); if (i < 0 ) { i = -(i + 1 ); } dp[i] = num; if (i == len) { len++; } } return len; }
最长子序列
1 2 3 4 5 6 7 8 9 10 11 12 13 14 public int lengthOfLIS (int [] nums) { if (nums.length == 0 ) return 0 ; int [] dp = new int [nums.length]; int res = 0 ; Arrays.fill(dp, 1 ); for (int i = 0 ; i < nums.length; i++) { for (int j = 0 ; j < i; j++) { if (nums[j] < nums[i]) dp[i] = Math.max(dp[i], dp[j] + 1 ); } res = Math.max(res, dp[i]); } return res; }
循环排序 https://zhuanlan.zhihu.com/p/117347353
概念 这一模式描述了一种有趣的方法,处理的是涉及包含给定范围内数值的数组的问题。循环排序模式一次会在数组上迭代一个数值,如果所迭代的当前数值不在正确的索引处,就将其与其正确索引处的数值交换。你可以尝试替换其正确索引处的数值,但这会带来 O(n^2) 的复杂度,这不是最优的,因此要用循环排序模式。
特点
问题涉及到 数组需要在给定范围内进行数值排序
问题要求在一个排序,旋转的数组中找到确实的值/重复的值/最小的值
常见问题
举例 缺失的第一个正数
给你一个未排序的整数数组 nums ,请你找出其中没有出现的最小的正整数
把每个数字换到应该存在的位置上去,比如5就该放到nums[4]
如果这个数字大于length了,就不管,反正最后要检查
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 public int firstMissingPositive (int [] nums) { int n = nums.length; for (int i = 0 ; i < n; ++i) { while (nums[i] > 0 && nums[i] <= n && nums[nums[i] - 1 ] != nums[i]) { int temp = nums[nums[i] - 1 ]; nums[nums[i] - 1 ] = nums[i]; nums[i] = temp; } } for (int i = 0 ; i < n; ++i) { if (nums[i] != i + 1 ) { return i + 1 ; } } return n + 1 ; }
丢失的数字
给定一个包含 [0, n] 中 n 个数的数组 nums ,找出 [0, n] 这个范围内没有出现在数组中的那个数。(本应该有n+1个数字)
与上面的题差不多
也可以用异或来做,nums中所有的数和0-n一共n+1个数异或,最后得到的数就是需要的数
数组中重复的数据
给定一个整数数组 a,其中1 ≤ a[i] ≤ n (n为数组长度), 其中有些元素出现两次而其他元素出现一次。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 public List<Integer> findDuplicates (int [] nums) { List<Integer> res = new ArrayList<>(); int len = nums.length; if (len == 0 ) { return res; } for (int i = 0 ; i < len; i++) { while (nums[nums[i] - 1 ] != nums[i]) { swap(nums, i, nums[i] - 1 ); } } for (int i = 0 ; i < len; i++) { if (nums[i] - 1 != i) { res.add(nums[i]); } } return res; } private void swap (int [] nums, int index1, int index2) { if (index1 == index2) { return ; } nums[index1] = nums[index1] ^ nums[index2]; nums[index2] = nums[index1] ^ nums[index2]; nums[index1] = nums[index1] ^ nums[index2]; }
原地翻转列表 概念 在很多问题中,你可能会被要求反转一个链表中一组节点之间的链接。通常而言,你需要原地完成这一任务,即使用已有的节点对象且不占用额外的内存。
该模式会从一个指向链表头的变量(current)开始一次反转一个节点,然后一个变量(previous)将指向已经处理过的前一个节点。以锁步的方式,在移动到下一个节点之前将其指向前一个节点,可实现对当前节点的反转。
另外,也将更新变量「previous」,使其总是指向已经处理过的前一个节点。
特点
类似与双指针问题
一般要求不使用额外内存的前提下反转链表
常见问题
例题 反转链表
1 2 3 4 5 6 7 8 9 10 11 public ListNode reverseList (ListNode head) { ListNode prev = null ; ListNode curr = head; while (curr != null ) { ListNode next = curr.next; curr.next = prev; prev = curr; curr = next; } return prev; }
1 2 3 4 5 6 7 8 9 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
反转index为m-n的子链表
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 public ListNode reverseBetween (ListNode head, int m, int n) { if (head == null ) { return null ; } ListNode cur = head, prev = null ; while (m > 1 ) { prev = cur; cur = cur.next; m--; n--; } ListNode con = prev, tail = cur; ListNode third = null ; while (n > 0 ) { third = cur.next; cur.next = prev; prev = cur; cur = third; n--; } if (con != null ) { con.next = prev; } else { head = prev; } tail.next = cur; return head; }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 ListNode reverseBetween (ListNode head, int m, int n) { if (m == 1 ) { return reverseN(head, n); } head.next = reverseBetween(head.next, m - 1 , n - 1 ); return head; } ListNode successor = null ; ListNode reverseN (ListNode head, int n) { if (n == 1 ) { successor = head.next; return head; } ListNode last = reverseN(head.next, n - 1 ); head.next.next = head; head.next = successor; return last; }
树的宽度优先搜索 概念 基于宽度有限搜索技术,遍历一个树,并且使用一个队列来跟踪一个层级的所有节点,任何涉及到以逐层级方式遍历树的问题的都可以使用这种方法有效解决
工作模式:将根节点加入到队列中,然后连续迭代直到队列为空,在每次迭代中,移除队列头部的节点并且访问这个节点,然后将其所有自节点插入到队列当中
特点 所有需要层次遍历树的问题
常见问题
示例 从上到下打印二叉树 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 public List<List<Integer>> levelOrder(TreeNode root) { Queue<TreeNode> queue = new LinkedList<>(); List<List<Integer>> res = new ArrayList<>(); if (root != null ) queue.add(root); while (!queue.isEmpty()) { List<Integer> tmp = new ArrayList<>(); for (int i = queue.size(); i > 0 ; i--) { TreeNode node = queue.poll(); tmp.add(node.val); if (node.left != null ) queue.add(node.left); if (node.right != null ) queue.add(node.right); } res.add(tmp); } return res; }
特定深度节点链表 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 public ListNode[] listOfDepth(TreeNode tree) { Queue<TreeNode> queue=new LinkedList<>(); List<List<Integer>> lists=new ArrayList<>(); if (tree != null ){ queue.add(tree); } while (!queue.isEmpty()){ List<Integer> list=new ArrayList<>(); for (int i=queue.size();i>0 ;i--){ TreeNode tmp=queue.poll(); list.add(tmp.val); if (tmp.left !=null ){ queue.add(tmp.left); } if (tmp.right != null ){ queue.add(tmp.right); } } lists.add(list); } ListNode[] a=new ListNode[lists.size()]; for (int i=0 ;i<lists.size();i++){ ListNode l=new ListNode(0 ); ListNode ll=l; for (int j=0 ;j<lists.get(i).size();j++){ ListNode tmp=new ListNode(lists.get(i).get(j)); l.next=tmp; l=l.next; } a[i]=ll.next; } return a; }
二叉树的锯齿形层序遍历
Z形层次遍历
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 public List<List<Integer>> zigzagLevelOrder(TreeNode root) { List<List<Integer>> res = new ArrayList<>(); travel(root, res, 0 ); return res; } private void travel (TreeNode cur, List<List<Integer>> res, int level) { if (cur == null ) return ; if (res.size() <= level) { List<Integer> newLevel = new LinkedList<>(); res.add(newLevel); } List<Integer> list = res.get(level); if (level % 2 == 0 ) list.add(cur.val); else list.add(0 , cur.val); travel(cur.left, res, level + 1 ); travel(cur.right, res, level + 1 ); }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 List<List<Integer>> ans = new LinkedList<List<Integer>>(); if (root == null ) { return ans; } Queue<TreeNode> nodeQueue = new LinkedList<TreeNode>(); nodeQueue.offer(root); boolean isOrderLeft = true ; while (!nodeQueue.isEmpty()) { Deque<Integer> levelList = new LinkedList<Integer>(); int size = nodeQueue.size(); for (int i = 0 ; i < size; ++i) { TreeNode curNode = nodeQueue.poll(); if (isOrderLeft) { levelList.offerLast(curNode.val); } else { levelList.offerFirst(curNode.val); } if (curNode.left != null ) { nodeQueue.offer(curNode.left); } if (curNode.right != null ) { nodeQueue.offer(curNode.right); } } ans.add(new LinkedList<Integer>(levelList)); isOrderLeft = !isOrderLeft; } return ans; }
二叉树的右视图
给定一棵二叉树,想象自己站在它的右侧,按照从顶部到底部的顺序,返回从右侧所能看到的节点值。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 public List<Integer> rightSideView (TreeNode root) { List<Integer> res = new ArrayList<>(); if (root == null ) { return res; } Queue<TreeNode> queue = new LinkedList<>(); queue.offer(root); while (!queue.isEmpty()) { int size = queue.size(); 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); } if (i == size - 1 ) { res.add(node.val); } } } return res; }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 class Solution { List<Integer> res = new ArrayList<>(); public List<Integer> rightSideView (TreeNode root) { dfs(root, 0 ); return res; } private 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); } }
树的深度优先搜索 概念 Tree DFS 是基于深度优先搜索(DFS)技术来遍历树。
你可以使用递归(或该迭代方法的技术栈)来在遍历期间保持对所有之前的(父)节点的跟踪。
Tree DFS 模式的工作方式是从树的根部开始,如果这个节点不是一个叶节点,则需要做三件事:
决定现在是处理当前的节点(pre-order),或是在处理两个子节点之间(in-order),还是在处理两个子节点之后(post-order)
为当前节点的两个子节点执行两次递归调用以处理它们
特点
如果你被要求用 in-order、pre-order 或 post-order DFS 来遍历一个树
如果问题需要搜索其中节点更接近叶节点的东西
常见问题
示例 具有最深节点的最小子树
返回最靠近叶的子树的根节点,让他满足左右子树包含最深的节点
如果 node 没有左右子树,返回 node。
如果 node 左右子树的后代中都有最深节点,返回 node。
如果只有左子树或右子树中有且拥有所有的最深节点,返回这棵子树的根节点(即 node 的左/右孩子)。
否则,当前子树中不存在答案。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 public TreeNode subtreeWithAllDeepest (TreeNode root) { return dfs(root).node; } public Result dfs (TreeNode node) { if (node == null ) return new Result(null , 0 ); Result L = dfs(node.left), R = dfs(node.right); if (L.dist > R.dist) return new Result(L.node, L.dist + 1 ); if (L.dist < R.dist) return new Result(R.node, R.dist + 1 ); return new Result(node, L.dist + 1 ); } class Result { TreeNode node; int dist; Result(TreeNode n, int d) { node = n; dist = d; } }
监控二叉树
返回能够监控二叉树需要的最小的摄像头数量
每个节点一共维持三种类型的状态
状态0: 自己没有摄像头,自节点也没有
状态1: 自己有摄像头
状态2: 子节点有摄像头
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 private final int NO_CAMERA = 0 ;private final int HAS_CAMERA_SELF = 1 ;private final int HAS_CAMERA_CHILD = 2 ;private final int NULL = 3 ;private int res = 0 ; public int minCameraCover (TreeNode root) { if (root == null ) { return 0 ; } if (dfs(root) == NO_CAMERA) { res++; } return res; } public int dfs (TreeNode root) { if (root == null ) { return NULL; } int left = dfs(root.left), right = dfs(root.right); if (left == NO_CAMERA || right == NO_CAMERA) { res++; return HAS_CAMERA_SELF; } if (left == HAS_CAMERA_SELF || right == HAS_CAMERA_SELF) { return HAS_CAMERA_CHILD; } if (left == NULL && right == NULL) { return NO_CAMERA; } return NO_CAMERA; }
电话号码的字母组合
给定一串数字,单个数字范围为0-9,返回在九宫格输入法下能打印的所有字母序列
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 class Solution { private String letterMap[] = { " " , "" , "abc" , "def" , "ghi" , "jkl" , "mno" , "pqrs" , "tuv" , "wxyz" }; private ArrayList<String> res; public List<String> letterCombinations (String digits) { res = new ArrayList<String>(); if (digits.equals("" )) return res; findCombination(digits, 0 , "" ); return res; } private void findCombination (String digits, int index, String s) { if (index == digits.length()){ res.add(s); return ; } Character c = digits.charAt(index); String letters = letterMap[c - '0' ]; for (int i = 0 ; i < letters.length() ; i ++){ findCombination(digits, index+1 , s + letters.charAt(i)); } return ; } Map<Character, String> phoneMap = new HashMap<Character, String>() {{ put('2' , "abc" ); put('3' , "def" ); put('4' , "ghi" ); put('5' , "jkl" ); put('6' , "mno" ); put('7' , "pqrs" ); put('8' , "tuv" ); put('9' , "wxyz" ); }}; List<String> combinations = new ArrayList<String>(); public List<String> letterCombinations (String digits) { if (digits.length() == 0 ) { return combinations; } backtrack(digits, 0 , new StringBuffer()); return combinations; } public void backtrack (String digits, int index, StringBuffer combination) { if (index == digits.length()) { combinations.add(combination.toString()); } else { char digit = digits.charAt(index); String letters = phoneMap.get(digit); for (int i = 0 ; i < letters.length(); i++) { combination.append(letters.charAt(i)); backtrack(digits, index + 1 , combination); combination.deleteCharAt(index); } } } }
验证二叉搜索树
给定一个二叉树,判断是否是一个有效的二叉搜索树
1 2 3 4 5 6 7 8 9 10 11 12 13 public boolean isValidBST (TreeNode root) { return isValidBST(root, Long.MIN_VALUE, Long.MAX_VALUE); } public boolean isValidBST (TreeNode node, long lower, long upper) { if (node == null ) { return true ; } if (node.val <= lower || node.val >= upper) { return false ; } return isValidBST(node.left, lower, node.val) && isValidBST(node.right, node.val, upper); }
对称二叉树
判断一个二叉树是不是镜像对称
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 class Solution { public boolean isSymmetric (TreeNode root) { return check(root, root); } public boolean check (TreeNode p, TreeNode q) { if (p == null && q == null ) { return true ; } if (p == null || q == null ) { return false ; } return p.val == q.val && check(p.left, q.right) && check(p.right, q.left); } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 public boolean isSymmetric (TreeNode root) { return check(root, root); } public boolean check (TreeNode u, TreeNode v) { Queue<TreeNode> q = new LinkedList<TreeNode>(); q.offer(u); q.offer(v); while (!q.isEmpty()) { u = q.poll(); v = q.poll(); if (u == null && v == null ) { continue ; } if ((u == null || v == null ) || (u.val != v.val)) { return false ; } q.offer(u.left); q.offer(v.right); q.offer(u.right); q.offer(v.left); } return true ; }
二叉树的最大深度
1 2 3 4 5 6 7 8 9 public int maxDepth (TreeNode root) { if (root == null ) { return 0 ; } else { int leftHeight = maxDepth(root.left); int rightHeight = maxDepth(root.right); return Math.max(leftHeight, rightHeight) + 1 ; } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 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(); while (size > 0 ) { TreeNode node = queue.poll(); if (node.left != null ) { queue.offer(node.left); } if (node.right != null ) { queue.offer(node.right); } size--; } ans++; } return ans; }
二叉树的遍历 先序遍历
1 2 3 4 5 6 7 8 public void getPreorder (TreeNode root) { if (root == null ) { return ; } System.out.print(root.element + " " ); getPreorder(root.left); getPreorder(root.right); }
1 2 3 4 5 6 7 8 9 10 11 12 13 public void getPreorder (TreeNode root) { Deque<TreeNode> stack = new LinkerList<>(); TreeNode p = root; while (p != null || !stack.isEmpty()) { if (p != null ) { System.out.println(p.getElement()); stack.push(p); p = p.left; } else { p = stack.pop().right; } } }
中序遍历
1 2 3 4 5 6 7 8 public void getInorder (TreeNode root) { if (root == null ) { return ; } getInorder(root.left); System.out.print(root.element + " " ); getInorder(root.right); }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 public void getInorder (TreeNode<T> root) { Deque<TreeNode> stack = new LinkedList<>(); TreeNode p = root; while (p != null || !stack.isEmpty()) { if (p != null ) { stack.push(p); p = p.left; } else { TreeNode temp = stack.pop(); System.out.println(temp.getElement()); p = temp.right; } } }
后序遍历
1 2 3 4 5 6 7 8 private void getPostorder (TreeNode root) { if (root == null ) { return ; } getPostorder(root.left); getPostorder(root.right); System.out.println(root.element); }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 public void getPostorder (TreeNode root) { Deque<TreeNode> stack = new LinkedList<>(); TreeNode p = root; while (p != null || !stack.isEmpty()) { if (p != null ) { stack.push(p); p = p.left; } else { TreeNode temp = stack.pop(); if (第一次被pop出来){ stack.push(temp); p = temp.right; } else { System.out.println(temp.getElement()); } } } }
二叉树的最大路径和
路径 被定义为一条从树中任意节点出发,沿父节点-子节点连接,达到任意节点的序列。同一个节点在一条路径序列中 至多出现一次 。该路径 至少包含一个 节点,且不一定经过根节点。
每条路径肯定有一个父节点,这个父节点的两侧肯定没有分叉
最大贡献值:空节点为0,非空节点为自己+子节点中的最大贡献值
先递归计算每个节点的最大贡献值
对于二叉树中的一个节点,该节点的最大路径和取决于该节点的值与该节点的左右子节点的最大贡献值,如果子节点的最大贡献值为正,则计入该节点的最大路径和,否则不计入该节点的最大路径和。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 class Solution { int maxSum = Integer.MIN_VALUE; public int maxPathSum (TreeNode root) { maxGain(root); return maxSum; } public int maxGain (TreeNode node) { if (node == null ) { return 0 ; } int leftGain = Math.max(maxGain(node.left), 0 ); int rightGain = Math.max(maxGain(node.right), 0 ); int priceNewpath = node.val + leftGain + rightGain; maxSum = Math.max(maxSum, priceNewpath); return node.val + Math.max(leftGain, rightGain); } }
Two Heaps 概念 在很多问题中,我们要将给定的一组元素分为两部分。 为了求解这个问题,我们感兴趣的是了解一部分的最小元素以及另一部分的最大元素。 这一模式是求解这类问题的一种有效方法。该模式要使用两个堆(heap):一个用于寻找最小元素的 Min Heap 和一个用于寻找最大元素的 Max Heap。 该模式的工作方式是:先将前一半的数值存储到 Max Heap,这是由于你要寻找前一半中的最大数值。然后再将另一半存储到 Min Heap,因为你要寻找第二半的最小数值。 在任何时候,当前数值列表的中位数都可以根据这两个 heap 的顶部元素计算得到。
特点
优先级队列,调度等场景
需要找到一个集合的最小,最大,中间元素
有时候可以用于二叉树结构的问题
常见问题
示例 数据流的中位数 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 class MedianFinder { private int count; private PriorityQueue<Integer> maxheap; private PriorityQueue<Integer> minheap; public MedianFinder () { count = 0 ; maxheap = new PriorityQueue<>((x, y) -> y - x); minheap = new PriorityQueue<>(); } public void addNum (int num) { count += 1 ; maxheap.offer(num); minheap.add(maxheap.poll()); if ((count & 1 ) != 0 ) { maxheap.add(minheap.poll()); } } public double findMedian () { if ((count & 1 ) == 0 ) { return (double ) (maxheap.peek() + minheap.peek()) / 2 ; } else { return (double ) maxheap.peek(); } } }
滑动窗口中位数
不用大小堆,只是二分查找旧的替换新的,然后冒泡放在该防的位置
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 public double [] medianSlidingWindow(int [] nums, int k) { double [] res = new double [nums.length - k + 1 ]; int [] window = new int [k]; for (int i = 0 ; i < k; i++) { window[i] = nums[i]; } Arrays.sort(window); res[0 ] = getMid(window); for (int i = 0 ; i < nums.length - k; i++) { int index = search(window, nums[i]); window[index] = nums[i + k]; while (index < window.length - 1 && window[index] > window[index + 1 ]) { swap(window, index, index + 1 ); index++; } while (index > 0 && window[index] < window[index - 1 ]) { swap(window, index, index - 1 ); index--; } res[i + 1 ] = getMid(window); } return res; } private void swap (int [] window, int i, int j) { int temp = window[i]; window[i] = window[j]; window[j] = temp; } private double getMid (int [] window) { int len = window.length; if (window.length % 2 == 0 ) { return window[len / 2 ] / 2.0 + window[len / 2 - 1 ] / 2.0 ; } else { return window[len / 2 ]; } } private int search (int [] window, int target) { int start = 0 ; int end = window.length - 1 ; while (start <= end) { int mid = start + (end - start) / 2 ; if (window[mid] > target) { end = mid - 1 ; } else if (window[mid] < target) { start = mid + 1 ; } else { return mid; } } return -1 ; }
子集 概念 很多编程面试问题都涉及到处理给定元素集合的排列和组合。子集(Subsets)模式描述了一种用于有效处理所有这些问题的宽度优先搜索(BFS)方法。
特点 当你需要找到给定集合的组合或者排列的问题
常见问题
示例 括号生成
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 public class Solution { public List<String> generateParenthesis (int n) { List<String> res = new ArrayList<>(); if (n == 0 ) { return res; } dfs("" , n, n, res); return res; } private void dfs (String curStr, int left, int right, List<String> res) { if (left == 0 && right == 0 ) { res.add(curStr); return ; } if (left > right) { return ; } if (left > 0 ) { dfs(curStr + "(" , left - 1 , right, res); } if (right > 0 ) { dfs(curStr + ")" , left, right - 1 , res); } } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 public class Solution { public List<String> generateParenthesis (int n) { List<String> ans = new ArrayList<String>(); backtrack(ans, new StringBuilder(), 0 , 0 , n); return ans; } public void backtrack (List<String> ans, StringBuilder cur, int open, int close, int max) { if (cur.length() == max * 2 ) { ans.add(cur.toString()); return ; } if (open < max) { cur.append('(' ); backtrack(ans, cur, open + 1 , close, max); cur.deleteCharAt(cur.length() - 1 ); } if (close < open) { cur.append(')' ); backtrack(ans, cur, open, close + 1 , max); cur.deleteCharAt(cur.length() - 1 ); } } }
组合总和
给定一个数组和一个目标数,找出数组元素的所有组合,满足和为目标数字
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 class Solution { private List<List<Integer>> res = new ArrayList<>(); public List<List<Integer>> combinationSum(int [] candidates, int target) { List<Integer> path = new ArrayList<>(); Arrays.sort(candidates); backtrack(path,candidates,target,0 ,0 ); return res; } private void backtrack (List<Integer> path,int [] candidates,int target,int sum,int index) { if (sum == target) { res.add(new ArrayList<>(path)); return ; } for (int i = index;i < candidates.length;i++) { int newSum = candidates[i] + sum; if (rs <= target) { path.add(candidates[i]); backtrack(path,candidates,target,newSum,i); path.remove(path.size()-1 ); } else { break ; } } } }
组合总和2
组合需要去重复
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 public List<List<Integer>> combinationSum2(int [] candidates, int target) { int len = candidates.length; List<List<Integer>> res = new ArrayList<>(); if (len == 0 ) { return res; } Arrays.sort(candidates); Deque<Integer> path = new ArrayDeque<>(len); dfs(candidates, len, 0 , target, path, res); return res; } private void dfs (int [] candidates, int len, int begin, int target, Deque<Integer> path, List<List<Integer>> res) { if (target == 0 ) { res.add(new ArrayList<>(path)); return ; } for (int i = begin; i < len; i++) { if (target - candidates[i] < 0 ) { break ; } if (i > begin && candidates[i] == candidates[i - 1 ]) { continue ; } path.addLast(candidates[i]); dfs(candidates, len, i + 1 , target - candidates[i], path, res); path.removeLast(); } }
动态规划
不属于原文的一部分,但我刷题觉得用的太多了
概念 用空间换时间,先统计出已经计算过的数值,方便下一步使用
示例 最长回文子串
给定一个字符串,找到最长的回文子串
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 class Solution {public String longestPalindrome (String s) { if (s == null || s.length() < 2 ) { return s; } int strLen = s.length(); int maxStart = 0 ; int maxEnd = 0 ; int maxLen = 1 ; boolean [][] dp = new boolean [strLen][strLen]; for (int r = 1 ; r < strLen; r++) { for (int l = 0 ; l < r; l++) { if (s.charAt(l) == s.charAt(r) && (r - l <= 2 || dp[l + 1 ][r - 1 ])) { dp[l][r] = true ; if (r - l + 1 > maxLen) { maxLen = r - l + 1 ; maxStart = l; maxEnd = r; } } } } return s.substring(maxStart, maxEnd + 1 ); } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 class Solution { public String longestPalindrome (String s) { if (s == null || s.length() < 1 ) { return "" ; } int start = 0 , end = 0 ; for (int i = 0 ; i < s.length(); i++) { int len1 = expandAroundCenter(s, i, i); int len2 = expandAroundCenter(s, i, i + 1 ); int len = Math.max(len1, len2); if (len > end - start) { start = i - (len - 1 ) / 2 ; end = i + len / 2 ; } } return s.substring(start, end + 1 ); } public int expandAroundCenter (String s, int left, int right) { while (left >= 0 && right < s.length() && s.charAt(left) == s.charAt(right)) { --left; ++right; } return right - left - 1 ; } }
也可以得到一个倒序的字符串,然后返回正反字符串的最长公共子串
最长回文子序列
给定一个字符串,找到其中最长的回文子序列,并返回该序列的长度。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 class Solution { public int longestPalindromeSubseq (String s) { int length = s.length(); int [][] dp = new int [length][length]; for (int i = length - 1 ; i >= 0 ; i--) { dp[i][i] = 1 ; for (int j = i+1 ; j < length; j += 1 ) { if (s.charAt(i) == s.charAt(j)) { dp[i][j] = dp[i + 1 ][j - 1 ] + 2 ; } else { dp[i][j] = Math.max(dp[i + 1 ][j], dp[i][j - 1 ]); } } } return dp[0 ][length - 1 ]; } }
最长公共子串
给出两个字符串的最长公共子串
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 public static int getLCS (String s1, String s2) { char [] a = s1.toCharArray(); char [] b = s2.toCharArray(); int [][] result = new int [a.length + 1 ][b.length + 1 ]; int max = 0 ; for (int i = 0 ; i < a.length; i++) { for (int j = 0 ; j < b.length; j++) { if (a[i] == b[j]) { result[i + 1 ][j + 1 ] = result[i][j] + 1 ; max = Math.max(max, result[i + 1 ][j + 1 ]); } } } return max; }
最长公共子序列
和最长公共子串类似,只不过子序列是可以不连续的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 public static int getLCS (String s1, String s2) { char [] a = s1.toCharArray(); char [] b = s2.toCharArray(); int [][] result = new int [a.length + 1 ][b.length + 1 ]; for (int i = 0 ; i < a.length; i++) { for (int j = 0 ; j < b.length; j++) { if (a[i] == b[j]) { result[i + 1 ][j + 1 ] = result[i][j] + 1 ; } else { result[i + 1 ][j + 1 ] = Math.max(result[i][j + 1 ], result[i + 1 ][j]); } } } return result[a.length][b.length];
打家劫舍
一个数组,代表每个房屋存放的金额的非负整数数组,不能相邻被偷
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 class Solution { public int rob (int [] nums) { if (nums == null || nums.length == 0 ) { return 0 ; } int length = nums.length; 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 ]; } }
打家劫舍II
一个数组,代表每个房子存放的金额,不能相邻被偷,房子是连在一起的
两个打家劫舍的问题:从第一间偷到倒数第二间,从第二间偷到倒数第一间
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 class Solution { public int rob (int [] nums) { if (nums.length == 0 ) return 0 ; if (nums.length == 1 ) return nums[0 ]; return Math.max(myRob(Arrays.copyOfRange(nums, 0 , nums.length - 1 )), myRob(Arrays.copyOfRange(nums, 1 , nums.length))); } private int myRob (int [] nums) { int pre = 0 , cur = 0 , tmp; for (int num : nums) { tmp = cur; cur = Math.max(pre + num, cur); pre = tmp; } return cur; } }
打家劫舍III
所有房屋的排列类似于一颗二叉树,两个直接相连的房子在同一天晚上被打劫,自动报警
每一个节点存储两个值,自己不偷能得到的钱result[0]和自己偷能得到的钱result[1]
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 class Solution { public int rob (TreeNode root) { int [] result = robInternal(root); return Math.max(result[0 ], result[1 ]); } public int [] robInternal(TreeNode root) { if (root == null ) return new int [2 ]; int [] result = new int [2 ]; int [] left = robInternal(root.left); int [] right = robInternal(root.right); result[0 ] = Math.max(left[0 ], left[1 ]) + Math.max(right[0 ], right[1 ]); result[1 ] = left[0 ] + right[0 ] + root.val; return result; } }