LeetCode算法笔记(二)
一.括号生成
给出 n 代表生成括号的对数,请你写出一个函数,使其能够生成所有可能的并且有效的括号组合。
例如,给出 n = 3,生成结果为:
暴力法:采用递归的方法罗列出所有的组合,然后进行判断
class Solution { public List<String> generateParenthesis(int n) { List<String> combinations = new ArrayList<String>(); char[] current = new char[2*n]; generateAll(current, 0, combinations); return combinations; } public void generateAll(char[] current, int pos, List<String> result){ if(current.length == pos){ System.out.println(current); if(valid(current)){ result.add(new String(current)); } }else{ current[pos] = '('; generateAll(current, pos+1, result); current[pos] = ')'; generateAll(current, pos+1, result); } } public boolean valid(char[] current){ int balance = 0; for(int i = 0; i < current.length; i++){ if(current[i] == '('){ balance++; }else{ balance--; } if(balance < 0) return false; } return (balance == 0); } }
回溯法:只有在我们知道序列仍然保持有效时才添加 '(' or ')',而不是像 方法一 那样每次添加。我们可以通过跟踪到目前为止放置的左括号和右括号的数目来做到这一点,
如果我们还剩一个位置,我们可以开始放一个左括号。 如果它不超过左括号的数量,我们可以放一个右括号。
class Solution { public List<String> generateParenthesis(int n) { List<String> result = new ArrayList<String>(); generateAll(result, "", 0, 0, n); return result; } public void generateAll(List<String> result, String ans, int open, int close, int max){ if(close + open == 2*max){ result.add(ans); return; } if(open < max) generateAll(result, ans + "(", open+1, close, max); if(close < open) generateAll(result, ans + ")", open, close+1, max); } }
二.最小覆盖子串
滑动窗口法,需要验证每个子串是否符合条件,保留最小的子串。该方法超时
class Solution { public String minWindow(String s, String t) { if(s.length() < t.length()) return ""; int lens = s.length(); int left = 0; int right = t.length()-1; String minStr = ""; int minSize = Integer.MAX_VALUE; while(right < lens){ right++; while(containsT(s.substring(left, right), t)){ if(minSize > (right-left)){ minStr = s.substring(left, right); minSize = right - left; } left++; if(right - left < t.length()) { left = right; right += t.length() - 1; break; } } } return minStr; } public boolean containsT(String s, String t){ for(int i = 0; i < t.length(); i++){ if(!s.contains(t.substring(i, i+1))){ return false; }else{ s = s.replaceFirst(t.substring(i, i+1), ""); } } return true; } }
优化的滑动窗口法
public static String minWindow(String s, String t) { if (s == null || s == "" || t == null || t == "" || s.length() < t.length()) { return ""; } //用来统计t中每个字符出现次数 int[] needs = new int[128]; //用来统计滑动窗口中每个字符出现次数 int[] window = new int[128]; for (int i = 0; i < t.length(); i++) { needs[t.charAt(i)]++; } int left = 0; int right = 0; String res = ""; //目前有多少个字符 int count = 0; //用来记录最短需要多少个字符。 int minLength = s.length() + 1; while (right < s.length()) { char ch = s.charAt(right); window[ch]++; if (needs[ch] > 0 && needs[ch] >= window[ch]) { count++; } //移动到不满足条件为止 while (count == t.length()) { ch = s.charAt(left); if (needs[ch] > 0 && needs[ch] >= window[ch]) { count--; } if (right - left + 1 < minLength) { minLength = right - left + 1; res = s.substring(left, right + 1); } window[ch]--; left++; } right++; } return res; }
三.串联所有单词的子串
给定一个字符串 s 和一些长度相同的单词 words。找出 s 中恰好可以由 words 中所有单词串联形成的子串的起始位置。
注意子串要与 words 中的单词完全匹配,中间不能有其他字符,但不需要考虑 words 中单词串联的顺序。
暴力法:每次截取words*words.length长的单词数量,然后使用map记录单词数量,如果数量大于words中的单词数量则跳出,不存在也退出。
class Solution { public List<Integer> findSubstring(String s, String[] words) { List<Integer> res = new ArrayList<Integer>(); int wordNum = words.length; if (wordNum == 0) { return res; } int wordLen = words[0].length(); //HashMap1 存所有单词 HashMap<String, Integer> allWords = new HashMap<String, Integer>(); for (String w : words) { int value = allWords.getOrDefault(w, 0); allWords.put(w, value + 1); } //遍历所有子串 for (int i = 0; i < s.length() - wordNum * wordLen + 1; i++) { //HashMap2 存当前扫描的字符串含有的单词 HashMap<String, Integer> hasWords = new HashMap<String, Integer>(); int num = 0; //判断该子串是否符合 while (num < wordNum) { String word = s.substring(i + num * wordLen, i + (num + 1) * wordLen); //判断该单词在 HashMap1 中 if (allWords.containsKey(word)) { int value = hasWords.getOrDefault(word, 0); hasWords.put(word, value + 1); //判断当前单词的 value 和 HashMap1 中该单词的 value if (hasWords.get(word) > allWords.get(word)) { break; } } else { break; } num++; } //判断是不是所有的单词都符合条件 if (num == wordNum) { res.add(i); } } return res; } }
public List<Integer> findSubstring(String s, String[] words) { List<Integer> res = new ArrayList<Integer>(); int wordNum = words.length; if (wordNum == 0) { return res; } int wordLen = words[0].length(); HashMap<String, Integer> allWords = new HashMap<String, Integer>(); for (String w : words) { int value = allWords.getOrDefault(w, 0); allWords.put(w, value + 1); } //将所有移动分成 wordLen 类情况 for (int j = 0; j < wordLen; j++) { HashMap<String, Integer> hasWords = new HashMap<String, Integer>(); int num = 0; //记录当前 HashMap2(这里的 hasWords 变量)中有多少个单词 //每次移动一个单词长度 for (int i = j; i < s.length() - wordNum * wordLen + 1; i = i + wordLen) { boolean hasRemoved = false; //防止情况三移除后,情况一继续移除 while (num < wordNum) { String word = s.substring(i + num * wordLen, i + (num + 1) * wordLen); if (allWords.containsKey(word)) { int value = hasWords.getOrDefault(word, 0); hasWords.put(word, value + 1); //出现情况三,遇到了符合的单词,但是次数超了 if (hasWords.get(word) > allWords.get(word)) { // hasWords.put(word, value); hasRemoved = true; int removeNum = 0; //一直移除单词,直到次数符合了 while (hasWords.get(word) > allWords.get(word)) { String firstWord = s.substring(i + removeNum * wordLen, i + (removeNum + 1) * wordLen); int v = hasWords.get(firstWord); hasWords.put(firstWord, v - 1); removeNum++; } num = num - removeNum + 1; //加 1 是因为我们把当前单词加入到了 HashMap 2 中 i = i + (removeNum - 1) * wordLen; //这里依旧是考虑到了最外层的 for 循环,看情况二的解释 break; } //出现情况二,遇到了不匹配的单词,直接将 i 移动到该单词的后边(但其实这里 //只是移动到了出现问题单词的地方,因为最外层有 for 循环, i 还会移动一个单词 //然后刚好就移动到了单词后边) } else { hasWords.clear(); i = i + num * wordLen; num = 0; break; } num++; } if (num == wordNum) { res.add(i); } //出现情况一,子串完全匹配,我们将上一个子串的第一个单词从 HashMap2 中移除 if (num > 0 && !hasRemoved) { String firstWord = s.substring(i, i + wordLen); int v = hasWords.get(firstWord); hasWords.put(firstWord, v - 1); num = num - 1; } } } return res; }
四.最长有效括号
给定一个只包含 '('
和 ')'
的字符串,找出最长的包含有效括号的子串的长度。
暴力法:窃取偶数个子字符串,判断是否是有效的括号,此方法超出时间限制
public class Solution { public boolean isValid(String s) { Stack<Character> stack = new Stack<Character>(); for (int i = 0; i < s.length(); i++) { if (s.charAt(i) == '(') { stack.push('('); } else if (!stack.empty() && stack.peek() == '(') { stack.pop(); } else { return false; } } return stack.empty(); } public int longestValidParentheses(String s) { int maxlen = 0; for (int i = 0; i < s.length(); i++) { for (int j = i + 2; j <= s.length(); j+=2) { if (isValid(s.substring(i, j))) { maxlen = Math.max(maxlen, j - i); } } } return maxlen; } }
动态规划:
public class Solution { public int longestValidParentheses(String s) { int maxans = 0; int dp[] = new int[s.length()]; for (int i = 1; i < s.length(); i++) { if (s.charAt(i) == ')') { if (s.charAt(i - 1) == '(') { dp[i] = (i >= 2 ? dp[i - 2] : 0) + 2; } else if (i - dp[i - 1] > 0 && s.charAt(i - dp[i - 1] - 1) == '(') { dp[i] = dp[i - 1] + ((i - dp[i - 1]) >= 2 ? dp[i - dp[i - 1] - 2] : 0) + 2; } maxans = Math.max(maxans, dp[i]); } } return maxans; } }
栈:
public class Solution { public int longestValidParentheses(String s) { int maxans = 0; Stack<Integer> stack = new Stack<>(); stack.push(-1); for (int i = 0; i < s.length(); i++) { if (s.charAt(i) == '(') { stack.push(i); } else { stack.pop(); if (stack.empty()) { stack.push(i); } else { maxans = Math.max(maxans, i - stack.peek()); } } } return maxans; } }
方法 4:不需要额外的空间 算法:在这种方法中,我们利用两个计数器 leftleft 和 rightright 。首先,我们从左到右遍历字符串,对于遇到的每个‘(’,我们增加 leftleft 计算器,
对于遇到的每个‘)’,我们增加rightright计数器。每当leftleft 计数器与rightright计数器相等时,我们计算当前有效字符串的长度,并且记录目前为止找到的最长子字符串。
如果 rightright计数器比leftleft计数器大时,我们将leftleft和rightright计数器同时变回0。 public class Solution { public int longestValidParentheses(String s) { int left = 0, right = 0, maxlength = 0; for (int i = 0; i < s.length(); i++) { if (s.charAt(i) == '(') { left++; } else { right++; } if (left == right) { maxlength = Math.max(maxlength, 2 * right); } else if (right >= left) { left = right = 0; } } left = right = 0; for (int i = s.length() - 1; i >= 0; i--) { if (s.charAt(i) == '(') { left++; } else { right++; } if (left == right) { maxlength = Math.max(maxlength, 2 * left); } else if (left >= right) { left = right = 0; } } return maxlength; } }
五.合并k个排序链表
合并 k 个排序链表,返回合并后的排序链表。请分析和描述算法的复杂度。
暴力法:写一个方法合并两个链表,然后遍历链表数组,两两组合。
class Solution { public ListNode mergeKLists(ListNode[] lists) { if(lists == null || lists.length < 1) return null; if(lists.length == 1) return lists[0]; ListNode temp = merge2Lists(lists[0], lists[1]); for(int i = 2; i < lists.length; i++){ temp = merge2Lists(temp, lists[i]); } return temp; } public ListNode merge2Lists(ListNode l1, ListNode l2){ ListNode dump = new ListNode(-1); ListNode cur1 = l1, cur2 = l2, cur = dump; while(cur1 != null || cur2 != null){ if(cur1 == null){ cur.next = cur2; break; } if(cur2 == null){ cur.next = cur1; break; } if(cur1.val <= cur2.val){ cur.next = cur1; cur1 = cur1.next; }else{ cur.next = cur2; cur2 = cur2.next; } cur = cur.next; } return dump.next; } }
暴力法2:使用for循环,每次都遍历所有的字符序列
class Solution { public ListNode mergeKLists(ListNode[] lists) { ListNode[] cur = new ListNode[lists.length]; for(int i = 0; i < lists.length; i++){ cur[i] = lists[i]; } ListNode result = new ListNode(-1); ListNode res = result; int index = -1; while(true){ ListNode temp = new ListNode(Integer.MAX_VALUE); boolean flag = false; for(int i = 0; i < lists.length; i++){ if(cur[i] != null && cur[i].val < temp.val){ flag = true; temp = cur[i]; index = i; res.next = cur[i]; } } if(!flag){ break; } cur[index] = cur[index].next; res = res.next; } return result.next; } }
利用堆做排序,小根堆
class Solution { public ListNode mergeKLists(ListNode[] lists) { if(lists==null || lists.length==0) { return null; } //创建一个堆,并设置元素的排序方式 PriorityQueue<ListNode> queue = new PriorityQueue(new Comparator<ListNode>() { public int compare(ListNode o1, ListNode o2) { return (o1.val - o2.val); } }); //遍历链表数组,然后将每个链表的每个节点都放入堆中 for(int i=0;i<lists.length;i++) { while(lists[i] != null) { queue.add(lists[i]); lists[i] = lists[i].next; } } ListNode dummy = new ListNode(-1); ListNode head = dummy; //从堆中不断取出元素,并将取出的元素串联起来 while( !queue.isEmpty() ) { dummy.next = queue.poll(); dummy = dummy.next; } dummy.next = null; return head.next; } }
优化的堆排序,一次只放k个元素
class Solution { public ListNode mergeKLists(ListNode[] lists) { if(lists==null || lists.length==0) { return null; } //创建一个小根堆,并定义好排序函数 PriorityQueue<ListNode> queue = new PriorityQueue(new Comparator<ListNode>() { public int compare(ListNode o1, ListNode o2) { return (o1.val - o2.val); } }); ListNode dummy = new ListNode(-1); ListNode cur = dummy; //这里跟上一版不一样,不再是一股脑全部放到堆中 //而是只把k个链表的第一个节点放入到堆中 for(int i=0;i<lists.length;i++) { ListNode head = lists[i]; if(head!=null) { queue.add(head); } } //之后不断从堆中取出节点,如果这个节点还有下一个节点, //就将下个节点也放入堆中 while(queue.size()>0) { ListNode node = queue.poll(); cur.next = node; cur = cur.next; if(node.next!=null) { queue.add(node.next); } } cur.next = null; return dummy.next; } }
分治法
class Solution { public ListNode mergeKLists(ListNode[] lists) { return mergeKListHelper(lists, 0, lists.length - 1); } public ListNode mergeKListHelper(ListNode[] lists, int low, int high){ if (low == high) { return lists[low]; } else if (low < high) { int mid = low + (high - low)/2; ListNode left = mergeKListHelper(lists, low, mid); ListNode right = mergeKListHelper(lists, mid + 1, high); return merge(left, right); } else { return null; } } public ListNode merge(ListNode l1, ListNode l2) { ListNode dummyNode = new ListNode(0); ListNode current = dummyNode; while (l1 != null && l2 != null) { if (l1.val <= l2.val) { current.next = l1; l1 = l1.next; } else { current.next = l2; l2 = l2.next; } current = current.next; } if (l1 != null) { current.next = l1; } else { current.next = l2; } return dummyNode.next; } }
六.接雨水
给定 n 个非负整数表示每个宽度为 1 的柱子的高度图,计算按此排列的柱子,下雨之后能接多少雨水。
public int trap(int[] height) { int sum = 0; int max_left = 0; int max_right = 0; int left = 1; int right = height.length - 2; // 加右指针进去 for (int i = 1; i < height.length - 1; i++) { //从左到右更 if (height[left - 1] < height[right + 1]) { max_left = Math.max(max_left, height[left - 1]); int min = max_left; if (min > height[left]) { sum = sum + (min - height[left]); } left++; //从右到左更 } else { max_right = Math.max(max_right, height[right + 1]); int min = max_right; if (min > height[right]) { sum = sum + (min - height[right]); } right--; } } return sum; }
动态规划
class Solution { public int trap(int[] height) { //动态规划 int[] max_left = new int[height.length]; int[] max_right = new int[height.length]; int result = 0; for(int i = 1; i < height.length; i++){ max_left[i] = Math.max(height[i-1], max_left[i - 1]); } for(int i = height.length - 2; i > 0; i--){ max_right[i] = Math.max(height[i + 1], max_right[i + 1]); } for(int i = 1; i < height.length - 1; i++){ int min = Math.min(max_left[i], max_right[i]); if(min > height[i]){ result += min - height[i]; } } return result; } }
class Solution { public int trap(int[] height) { //栈 Stack<Integer> stack = new Stack<Integer>(); int result = 0; int current = 0; while(current < height.length){ while(!stack.isEmpty() && height[current] > height[stack.peek()]){ int h = stack.pop(); if(stack.isEmpty()){ break; } int min = Math.min(height[current], height[stack.peek()]); result = result + (min - height[h])*(current - stack.peek() - 1); } stack.push(current); current++; } return result; } }