402. remove-k-digits
- 给一个纯数字组成的字符串,给一个整数k,要求删除k个数字后得到的数尽可能地小。例如
112
删除2个后需要得到1
。 - 找到规律,使用stack的思路+贪心法,每次将数字入栈,若当前数字比栈顶小,说明前面的数字应当删除,一直删除到栈顶数字不再比当前数字大,再入栈。最后保留下来的数字不一定删够了k个,需要twick索引只保留一部分栈里剩下的数字。1234567891011121314151617181920212223class Solution {public String removeKdigits(String num, int k) {if (num == null || num.length() == 0 || k >= num.length()) {return "0";}char[] numChars = num.toCharArray();char[] newNumChars = new char[num.length()];int index = 0;for (int i = 0; i < numChars.length; i++) {while (index > 0 && newNumChars[index - 1] > numChars[i] && k > 0) {index--;k--; // 持续删除直到栈顶数字足够小}newNumChars[index++] = numChars[i];}int start = 0;while (start < index && newNumChars[start] == '0') {start++;}// 最后剩下index个数字,只保留[start, index - start - k]的部分return start == index ? "0" : new String(newNumChars, start, index - start - k);}}
403. frog-jump
- 给一个数组表示石头所处的x坐标,青蛙每次只能跳上一次跳跃长度的-1,0,1三种可能,判断能否跳到最后一个石头。例如
[0,1,3,5,6,8,12,17]
是可以的,而[0,1,2,3,4,8,9,11]
就不行。 - 相当于BFS,每个石头处维护一个set存放他可以跳的长度,然后每次都往后跳看看能否有新的石头,有就更新那个石头的可跳长度。12345678910111213141516171819202122232425262728293031323334353637// BFS,从开头出发,不断更新后续可达石头的新步数,若中途更新到了最后一个石头,就可达public boolean canCross(int[] stones) {if (stones == null || stones.length == 0) {return true;}if (stones[0] != 0) {return false;}// 记录每个坐标的石头所能跳的长度Map<Integer, Set<Integer>> stone2step = new HashMap<>();for (int i = 0; i < stones.length; i++) {stone2step.put(stones[i], new HashSet<>());}stone2step.get(0).add(1); // 第一步起码要能往后挪一步// 从第一个石头开始,往后更新每个石头的能跳步数for (int i = 0; i < stones.length; i++) {int currStone = stones[i];Set<Integer> steps = stone2step.get(currStone);for (int step: steps) {int newStone = currStone + step;if (newStone == stones[stones.length - 1]) {return true;}Set<Integer> newStep = stone2step.get(newStone);if (newStep != null) { // 表示有这个新石头的坐标newStep.add(step + 1);newStep.add(step);if (step - 1 > 0) {newStep.add(step - 1);}}}}return false;}
404. sum-of-left-leaves
- 给一个数,求所有左叶子的和。
- 递归求,只有是左孩子且是叶子才返回node.val。1234567891011121314class Solution {public int sumOfLeftLeaves(TreeNode root) {return sumOfLeftLeaves(root, false);}private int sumOfLeftLeaves(TreeNode node, boolean isLeft) {if (node == null) {return 0;}if (node.left == null && node.right == null) {return isLeft ? node.val : 0;}return sumOfLeftLeaves(node.left, true) + sumOfLeftLeaves(node.right, false);}}
405. convert-a-number-to-hexadecimal
- 将数字转换为十六进制字符串。
方法一:利用mask每次只取最后四个bit,然后直接map到字符拼接到hexStr的最前面,然后unsigned shift四位。
12345678910111213final private char[] map = {'0', '1', '2', '3','4','5','6','7','8','9','a','b','c','d','e','f'};final private int mask = 15;public String toHex(int num) {if (num == 0) {return "0";}String hexStr = "";while (num != 0) {hexStr = map[(num & mask)] + hexStr;num = (num >>> 4); // 不保留最高位}return hexStr;}方法二:更general的做法,可以推广到十进制转任意进制字符串。不过首先需要转成long并且过滤掉long前面填充的一堆符号位,然后取模得到的就是当前位的数值,直接map到字符;然后继续除。
12345678910111213final private char[] map = {'0', '1', '2', '3','4','5','6','7','8','9','a','b','c','d','e','f'};public String toHex(int num) {if (num == 0) {return "0";}long longNum = num & 0x00000000ffffffffL; // 不能直接强制转换,不然会保留符号String hexStr = "";while (longNum != 0) {hexStr = map[(int)(longNum % 16)] + hexStr;longNum /= 16;}return hexStr;}
409. valid-word-abbreviation
- 给两个字符串word和abbr,word只含有小写字母,abbr除小写字母外含有数字表示可以替换成多少个字母,判断abbr是否是word的简写。例如
word
可以简写成w2d
。 - 注意corner case,例如
w4
就不是word
的简写了,01
也不是a
的简写。123456789101112131415161718192021222324public boolean validWordAbbreviation(String word, String abbr) {if (word == null || word.length() == 0) {return word == abbr || (word != null && word.equals(abbr));}int indexWord = 0, indexAbbr = 0, lenAbbr = abbr.length(), lenWord = word.length();while (indexAbbr < lenAbbr && indexWord < lenWord) {char c = abbr.charAt(indexAbbr);if (Character.isLowerCase(c)) {if (word.charAt(indexWord) != c) {return false;}indexWord++;indexAbbr++;} else {int num = (int)(abbr.charAt(indexAbbr++) - '0');if (num == 0) return false;while (indexAbbr < lenAbbr && Character.isDigit(abbr.charAt(indexAbbr))) {num = 10 * num + (abbr.charAt(indexAbbr++) - '0');}indexWord += num;}}return indexAbbr == lenAbbr && indexWord == lenWord;}
410. split-array-largest-sum
- 给一个只含有非负整数的int数组和一个subArray数目m,将这个数组分成m个连续subarray,求他们的最大值最小是多少。
方法一:在学c++时老师讲过,最大值最小化,经典二分查找问题。一波流求最大值和sum分别作为下界和上界,然后进行二分查找,mid作为targetSum,即如果每个subarray都不超过这个targetSum需要划分成多少个子数组,如果多了说明targetSum太小,需要往前收缩;注意需要尽量biase到尽可能小的targetSum,联想到求first occurance的二分查找。
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748class Solution {public int splitArray(int[] nums, int m) {if (nums == null || nums.length == 0) {return 0;}// 最大值为下界、和为上界进行二分查找long sum = 0;int max = 0;for (int num : nums) {sum += num;max = Math.max(max, num);}long start = max - 1, end = sum + 1;while (end - start > 1) {long mid = start + (end - start) / 2;int count = getSubarrayCount(nums, mid);if (count <= m) { // 当前的targetSum太大导致subarray个数过少end = mid; // biase to front} else {start = mid;}}return (int)end;}private int getSubarrayCount(int[] nums, long targetSum) {int count = 0;long sum = 0;for (int i = 0; i < nums.length; i++) {long tempSum = sum + nums[i];if (tempSum < targetSum) {sum = tempSum;} else {if (tempSum == targetSum) {sum = 0; // 恰好相等,则归零} else {sum = nums[i];}count++;}}if (sum != 0) {count++;}return count;}}方法二:DP。。。
412. FizzBuzz
- 根据是否为3、5的倍数输出指定的字符串。skip。
413. arithmetic-slices
- 给一个int数组,求其中有多少段是等差数列。等差数列要求长度为3+。
- DP。当前的长度只取决于以之前一个元素结尾的等差数列长度,如果不等差了则长度归零。1234567891011121314151617class Solution {public int numberOfArithmeticSlices(int[] A) {if (A == null || A.length == 0) {return 0;}int prevLen = 0, retVal = 0;for (int i = 2; i < A.length; i++) {if (A[i - 1] - A[i - 2] == A[i] - A[i - 1]) {prevLen++;} else {prevLen = 0;}retVal += prevLen;}return retVal;}}
414. third-maximum-number
- 给一个数组,求其中第三大的数字。
[2, 2, 3, 1] -> 1
.若第三大不存在(例如就两个数字)则返回最大的数字。[1, 2] -> 2
. 用pq搞定,pass。12345678910111213141516171819202122class Solution {public int thirdMax(int[] nums) {if (nums == null || nums.length == 0) {return 0;}int K = 3;PriorityQueue<Integer> pq = new PriorityQueue<>();Set<Integer> set = new HashSet<>();for (int num : nums) {if (set.add(num)) {pq.offer(num);if (pq.size() > K) {pq.poll();}}}if (pq.size() == 2) {pq.poll();}return pq.peek();}}
416. partition-equal-subset-sum
- 给一个只含有正数的int数组,判断是否可以划分成两个和相等的数组。
和698类似的暴力做法,直接遍历找所有可能的组合。
12345678910111213141516171819202122232425262728293031class Solution {public boolean canPartition(int[] nums) {if (nums == null || nums.length == 0) {return false;}int sum = IntStream.of(nums).sum();Arrays.sort(nums);if (nums[nums.length - 1] > sum / 2) {return false;}return sum % 2 == 0 && canPartition(nums, new boolean[nums.length], sum / 2, 0);}private boolean canPartition(int[] nums, boolean[] visited, int targetSum, int currSum) {if (currSum > targetSum) {return false;}if (currSum == targetSum) {return true;}for (int i = nums.length - 1; i >= 0; i--) {if (!visited[i]) {visited[i] = true;if (canPartition(nums, visited, targetSum, currSum + nums[i])) {return true;}visited[i] = false;}}return false;}}其实这题想考察的是DP。
dp[i][j]
表示i
个数组成的和为j
,期中这i
个数不是全部都取。对于dp[i][j]
来说如果不取第i个数(对应索引为i - 1
),则直接来自dp[i - 1][j]
;如果取了第i个数,则是从dp[i - 1][j - nums[i - 1]]
转移过来。12345678910111213141516171819202122232425class Solution {public boolean canPartition(int[] nums) {if (nums == null || nums.length == 0) {return false;}int sum = IntStream.of(nums).sum();if ((sum & 1) == 1) {return false;}sum /= 2;boolean[][] dp = new boolean[nums.length + 1][sum + 1];for (int i = 0; i <= nums.length; i++) {dp[i][0] = true;}for (int i = 1; i <= nums.length; i++) {for (int j = 1; j <= sum; j++) {dp[i][j] = dp[i - 1][j]; // 假设不取nums[i - 1]到达当前位置if (j >= nums[i - 1]) { // 防止数组越界dp[i][j] = dp[i][j] || dp[i - 1][j - nums[i - 1]]; // 这里只用了前一行,因此其实dp只用一维数组也够了}}}return dp[nums.length][sum];}}
417. pacific-atlantic-water-flow
- 给一个int二维数组,左边和上边连接pacific,右边和下边连接atlantic。若当前元素大于相邻元素,则可达。求所有可以同时到达两个海洋的坐标。
- 从四条边进行DFS。与平时的区别在于visited数组,这里需要维护两种visited信息,一种是从pacific来,一种是从atlantic来,因此考虑用bit小技巧。注意DFS的visited可以放到进入后进行,而BFS需要在进入之前进行,防止重复放入queue。123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051class Solution {private final int PACIFIC_BIT = 1, ATLANTIC_BIT = 1 << 1;private final int[] dir = new int[] {0, 1, 0, -1, 0};public List<List<Integer>> pacificAtlantic(int[][] matrix) {List<List<Integer>> ans = new ArrayList<>();if (matrix == null || matrix.length == 0 || matrix[0].length == 0) {return ans;}int rows = matrix.length, cols = matrix[0].length;int[][] bits = new int[rows][cols];for (int col = 0; col < cols; col++) {dfs(matrix, bits, 0, col, PACIFIC_BIT);dfs(matrix, bits, rows - 1, col, ATLANTIC_BIT);}for (int row = 0; row < rows; row++) {dfs(matrix, bits, row, 0, PACIFIC_BIT);dfs(matrix, bits, row, cols - 1, ATLANTIC_BIT);}for (int row = 0; row < rows; row++) {for (int col = 0; col < cols; col++) {if ((bits[row][col] & PACIFIC_BIT) != 0 &&(bits[row][col] & ATLANTIC_BIT) != 0) {List<Integer> list = new ArrayList<>();list.add(row);list.add(col);ans.add(list);}}}return ans;}private void dfs(int[][] matrix, int[][] bits, int row, int col, int targetBit) {if ((bits[row][col] & targetBit) != 0) {return;}bits[row][col] |= targetBit;int rows = matrix.length, cols = matrix[0].length;for (int i = 0; i < 4; i++) {int rowNew = row + dir[i];int colNew = col + dir[i + 1];if (rowNew >= 0 && rowNew < rows &&colNew >= 0 && colNew < cols &&matrix[row][col] <= matrix[rowNew][colNew]) {dfs(matrix, bits, rowNew, colNew, targetBit);}}}}
419. battleships-in-a-board
- 给一个二维char数组,其中含有
.
和X
字符,X
表示船,船只会横或者竖着放,船之间至少有一个.
。求船的个数。 - 直接算「第一个」出现的
X
,即左边和上面都不是X
的。
424. longest-repeating-character-replacement
- 给一个仅包含大写字母的字符串,再给一个k,表示假设可以有k次机会将其中的某些字母任意变成另一个字母,返回最长的相同字母的substring的长度。例如
ABAB
变2次,最长长度为4(AAAA
);AAABAABB
变1次,为6. - 这个替换是一一对应的吗?(不是,可以任意换成需要的字符。也就是可以多对一)
- 双指针 + producer/consumer的方法,快指针先往后求各个字母的计数,同时更新一个出现最多的字母的频数maxCount。当前后两指针所夹字母数大于了maxCount + k,说明已经超过了可以替换的数目,此时就需要挪慢指针来consume计数。至于为什么不需要每次都保持最精确的maxCount,因为我们只关心最大的,当前最大的挪出窗口后,计数肯定是减掉的,那么后续再出现的时候,不会错误地产生更大的计数,最大值再大也大不过历史峰值。解释来自这里.1234567891011121314151617181920class Solution {public int characterReplacement(String s, int k) {if (s == null || s.length() == 0) {return 0;}int[] count = new int [26];char[] sChar = s.toCharArray();int start = 0, maxCount = 0, maxLen = 0;for (int end = 0; end < sChar.length; end++) {maxCount = Math.max(maxCount, ++count[sChar[end] - 'A']); // 更新最多的字母的频数while (end - start + 1 > k + maxCount) { // 指针覆盖的字母过多count[sChar[start] - 'A']--; // 挪动慢指针,并consume掉计数start++;// 注意并不需要重置maxCount,因为当前这样求出来的就是最大的,只有当新的字符超过了历史最大的maxCount才会有更长的长度需要更新}maxLen = Math.max(maxLen, end - start + 1);}return maxLen;}}
425. word-squares
- 给一个wordList,将List中的String放入matrix中使得行、列的单词都来自于这个List。
- Trie + DFS,对于每个Trie节点,除了正常的nexts数组、isWord布尔值,额外维护一个List
保存以「到达当前TrieNode路径」为prefix的所有word。固定一个word之后,下一个词的prefix可以通过纵向append得到,具体规律见这个discussion. 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677class Solution {class TrieNode {TrieNode[] nexts;List<String> prefixWords;boolean isWord;public TrieNode() {nexts = new TrieNode[26];prefixWords = new ArrayList<>();isWord = false;}}class Trie {TrieNode root;public Trie(String[] words) {root = new TrieNode();for (String word : words) {TrieNode curr = root;for (int i = 0; i < word.length(); i++) {int index = word.charAt(i) - 'a';if (curr.nexts[index] == null) {curr.nexts[index] = new TrieNode();}curr.prefixWords.add(word);curr = curr.nexts[index];}curr.isWord = true;}}public List<String> getByPrefix(String prefix) {List<String> ans = new ArrayList<>();TrieNode curr = root;for (int i = 0; i < prefix.length(); i++) {int index = prefix.charAt(i) - 'a';if (curr.nexts[index] == null) {return ans;}curr = curr.nexts[index];}ans.addAll(curr.prefixWords);return ans;}}private void dfs(int len, Trie trie, List<List<String>> ans, List<String> curr) {if (curr.size() == len) {ans.add(new ArrayList<>(curr));return;}int index = curr.size();StringBuilder prefix = new StringBuilder();for (String s : curr) {prefix.append(s.charAt(index));}List<String> candidates = trie.getByPrefix(prefix.toString());for (String candidate : candidates) {curr.add(candidate);dfs(len, trie, ans, curr);curr.remove(curr.size() - 1);}}public List<List<String>> wordSquares(String[] words) {List<List<String>> ans = new ArrayList<>();if (words == null || words.length == 0) {return ans;}int len = words[0].length();Trie root = new Trie(words);List<String> curr = new ArrayList<>();for (String word : words) {curr.add(word);dfs(len, root, ans, curr);curr.remove(curr.size() - 1);}return ans;}}
426. convert-binary-search-tree-to-sorted-doubly-linked-list
- 给一个BST,将它转换成排好序的循环双向链表(头尾相接),返回头部。可以把左右孩子就看作是当前节点的前后链表节点,不需要额外搞ListNode类。
BST要维持顺序,那就是用中序遍历。利用全局变量prev来记录每一个子树的结尾节点,先build左子树,然后把当前节点拼到prev的后面,再去继续build右子树即可。
123456789101112131415161718192021222324private Node prev = null;public Node treeToDoublyList(Node root) {if (root == null) {return null;}Node dummy = new Node(); // 伪头部的next就是headprev = dummy;buildDoublyList(root);prev.right = dummy.right;dummy.right.left = prev;dummy.left = dummy.right = null; // 清理dummyreturn prev.right;}// 执行buildDoublyList后会将node下面的部分都形成双向链表public void buildDoublyList(Node node) {if (node == null) {return;}buildDoublyList(node.left); // 先对左子树build一下,prev会指向最后一个节点prev.right = node; // 将node拼进去node.left = prev;prev = node; // 左半部分+当前节点的结尾就是nodebuildDoublyList(node.right);// 继续build右子树}方法二:分治法。先把左右子树的循环双向链表build好,再把当前节点塞到中间,同时把新的前后循环连接一下。注意在分别build的时候,需要把root本身设一个自循环,这样就可以重复使用connect方法了。
12345678910111213141516171819202122232425public Node treeToDoublyList(Node root) {if (root == null) {return null;}Node leftHead = treeToDoublyList(root.left);Node rightHead = treeToDoublyList(root.right);root.left = root;root.right = root;return connect(connect(leftHead, root), rightHead);}public Node connect(Node leftHead, Node rightHead) {if (leftHead == null) {return rightHead;}if (rightHead == null) {return leftHead;}Node leftTail = leftHead.left;Node rightTail = rightHead.left;leftTail.right = rightHead;rightHead.left = leftTail;leftHead.left = rightTail;rightTail.right = leftHead;return leftHead;}
428. serialize-and-deserialize-n-ary-tree
- 对N叉树实现与string的互相转换。实现方式只有一个限制,就是必须是stateless,即不能用函数外面的全局变量。
- 对于这个话题很多讨论比较有意思。首先是如何将字符串拼接起来,事实上直接
+
从performance的角度来说是最快的,但消耗的内粗很大,因为需要复制很多次。若已经有一个list of strings,直接用join最快。如果是on-the-fly,那就用StringBuilder. - 实现本身不难,因为实现很自由。例如直接存节点值和孩子节点数量,pre-order的方式递归拼入数组,用
,
连接。如果可以假设数字值域为[0, 65535]
,则连,
都不用,直接将数值转成unicode character即可,(char) (value + '0')
.1234567891011121314151617181920212223242526272829303132333435363738394041class Codec {final private String SPLITTER = ",";// Encodes a tree to a single string.public String serialize(Node root) {List<String> list = new ArrayList<>();serialize(root, list);return String.join(SPLITTER, list);}private void serialize(Node node, List<String> list) {if (node != null) {list.add(String.valueOf(node.val));list.add(String.valueOf(node.children.size()));for (Node child : node.children) {serialize(child, list);}}}// Decodes your encoded data to tree.public Node deserialize(String data) {if (data == null || data.length() == 0) {return null;}String[] arr = data.split(SPLITTER);Queue<String> q = new LinkedList<>(Arrays.asList(arr));return deserialize(q);}private Node deserialize(Queue<String> q) {Node node = new Node();node.val = Integer.parseInt(q.poll());node.children = new ArrayList<Node>();int childrenSize = Integer.parseInt(q.poll());while (childrenSize-- > 0) {node.children.add(deserialize(q));}return node;}}
430. flatten-a-multilevel-doubly-linked-list
- 给一个nested的双向链表,将所有child节点都插入到父节点的后面。
方法一:递归,利用一个全局变量存放上一次flatten之后的最后一个节点,对于每一个节点都连到prev后面,然后flatten当前节点的子节点,然后再继续往后。
12345678910111213141516Node prev = null;public Node flatten(Node head) {if (head == null) {return null;}if (prev != null) {prev.next = head;head.prev = prev;}prev = head;Node next = head.next;flatten(head.child);head.child = null;flatten(next);return head;}方法二:简单的遍历,当前节点有child时就到它的child那一层找到尾巴节点接到后面,然后继续遍历即可。但无法保证每个节点只遍历到一次。
1234567891011121314151617181920212223public Node flatten(Node head) {if (head == null) {return null;}Node curr = head;while (curr != null) {if (curr.child != null) {Node tail = curr.child;while (tail.next != null) { // 找到下一层的尾巴tail = tail.next;}tail.next = curr.next; // 与下一个节点相连if (curr.next != null) {curr.next.prev = tail;}curr.next = curr.child;curr.child.prev = curr;curr.child = null;}curr = curr.next; // 继续遍历链表}return head;}方法三:用stack记录next节点和child节点(注意push的顺序,保证先处理child节点),每次从stack中取节点连接。
123456789101112131415161718192021222324public Node flatten(Node head) {if (head == null) {return null;}Stack<Node> stack = new Stack<>();Node prev = null;stack.push(head);while (!stack.isEmpty()) {Node curr = stack.pop();if (prev != null) {prev.next = curr;curr.prev = prev;}if (curr.next != null) {stack.push(curr.next);}if (curr.child != null) {stack.push(curr.child);curr.child = null;}prev = curr;}return head;}
432. encode-n-ary-tree-to-binary-tree
- 实现一个类,能够将N-ary树转换为二叉树、也能把二叉树转回N-ary。
- 对于N-ary的孩子放到左子树,对于同一级的节点则一直放到右子树。12345678910111213141516171819202122232425262728293031323334353637383940414243class Codec {// Encodes an n-ary tree to a binary tree.public TreeNode encode(Node root) {if (root == null) {return null;}TreeNode rootBT = new TreeNode(root.val);if (root.children == null || root.children.isEmpty()) {return rootBT;}TreeNode prevChildBT = null, firstChildBT = null;for (Node child : root.children) {TreeNode childBT = encode(child);if (prevChildBT != null) {prevChildBT.right = childBT;}if (firstChildBT == null) {firstChildBT = childBT;}prevChildBT = childBT;}rootBT.left = firstChildBT;return rootBT;}// Decodes your binary tree to an n-ary tree.public Node decode(TreeNode rootBT) {if (rootBT == null) {return null;}Node root = new Node(rootBT.val, new ArrayList<>());TreeNode currBT = rootBT.left;while (currBT != null) {root.children.add(decode(currBT));currBT = currBT.right;}return root;}}// Your Codec object will be instantiated and called as such:// Codec codec = new Codec();// codec.decode(codec.encode(root));
435. non-overlapping-intervals
- 给一个interval数组,求至少需要删除多少个interval才能让剩余的不重叠。相关的问题见56、252、253、452.
方法一:首先按照interval的起点从小到大排序,对于前后两个求overlap的部分,若有重叠,则无论如何至少得删掉一个,但是只保留重叠部分与后续比较。时间
O(NlogN)
.1234567891011121314151617181920212223242526class Solution {public int eraseOverlapIntervals(int[][] intervals) {if (intervals == null || intervals.length == 0) {return 0;}Arrays.sort(intervals, (a, b) -> a[0] - b[0]);int count = 0;int[] currInterval = intervals[0];for (int i = 1; i < intervals.length; i++) {int[] overlap = getOverlap(currInterval, intervals[i]);if (overlap == null) {currInterval = intervals[i];} else {count++;currInterval = overlap;}}return count;}private int[] getOverlap(int[] interval1, int[] interval2) {if (interval2[0] < interval1[1] && interval2[1] > interval1[0]) {return new int[] { Math.max(interval1[0], interval2[0]), Math.min(interval1[1], interval2[1])};}return null;}}转换思路,这题和求最多的non-overlapping interval个数一摸一样。统计最多的独立interval个数,首先根据interval的右界进行排序,然后取左界与当前的右界进行比较,如果左界大,说明是新的独立interval,直接计数++。最后返回总interval数减去独立interval数即可。时间
O(NlogN)
.1234567891011121314151617class Solution {public int eraseOverlapIntervals(int[][] intervals) {if (intervals == null || intervals.length == 0) {return 0;}Arrays.sort(intervals, (a, b) -> a[1] - b[1]);int nonOverlapCount = 1;int currEnd = intervals[0][1];for (int i = 1; i < intervals.length; i++) {if (intervals[i][0] >= currEnd) {currEnd = intervals[i][1];nonOverlapCount++;}}return intervals.length - nonOverlapCount;}}
437. path-sum-iii
- 给一个二叉树,给一个目标值sum,求有几条从上往下累加的路径之和等于sum。
递归DFS,每次深入之前都先减掉当前节点的值。
123456789101112131415class Solution {public int pathSum(TreeNode root, int sum) { // calculate path num starting from rootif (root == null) {return 0;}return dfs(root, sum) // taking the given node+ pathSum(root.left, sum) + pathSum(root.right, sum); // start from left/right child}private int dfs(TreeNode node, int target) { // dig to find path num taking current nodeif (node == null) {return 0;}return (node.val == target? 1 : 0) + dfs(node.left, target - node.val) + dfs(node.right, target - node.val);}}优化:显然上面最原始的方式会有许多重复性计算,所以考虑使用memo存放从各个可能的根到当前节点的各种sum出现的次数,这样看看”当前sum减去目标和”出现过多少次即可。
1234567891011121314151617181920212223class Solution {private int count = 0;public int pathSum(TreeNode root, int sum) {Map<Integer, Integer> sumCount = new HashMap<>();sumCount.put(0, 1);dfs(root, 0, sum, sumCount);return count;}private void dfs(TreeNode root, int currSum, int target, Map<Integer, Integer> sumCount) {if (root == null) {return;}currSum += root.val;count += sumCount.getOrDefault(currSum - target, 0);sumCount.put(currSum, sumCount.getOrDefault(currSum, 0) + 1);dfs(root.left, currSum, target, sumCount);dfs(root.right, currSum, target, sumCount);sumCount.put(currSum, sumCount.get(currSum) - 1);}}
438. find-all-anagrams-in-a-string
- 给一个字符串s和一个字符串p,求s中所有p的anagram子串的起始位置的List。
- 双指针 + producer/consumer的方法,map中存放p中每个字符及其对应出现的次数,快指针负责consume直到没有可用的字符(只需要管map中有的字符),这时看看快慢指针所夹字符的个数是否等于p;之后就挪动慢指针provide补回字符。123456789101112131415161718192021222324252627282930313233343536373839404142class Solution {// O(N)双指针。先一波流统计p中各个字符出现的频数,然后consume掉map中的字符直到没有available的// 此时判断左右指针之间长度是否等于目标的长度,然后挪动左指针重新往map中加回去,直到出现可选字符public List<Integer> findAnagrams(String s, String p) {List<Integer> ans = new ArrayList<Integer>();if (s == null || p == null || s.length() == 0 || p.length() == 0) {return ans;}Map<Character, Integer> map = new HashMap<>();char[] pChar = p.toCharArray();int pLen = pChar.length;for (char c: pChar) {map.put(c, map.getOrDefault(c, 0) + 1);}char[] sChar = s.toCharArray();int count = map.size(); // 还有count个不同的字符可选int left = 0, right = 0;while (right < sChar.length) {if (map.containsKey(sChar[right])) {map.put(sChar[right], map.get(sChar[right]) - 1);if (map.get(sChar[right]) == 0) {count--; // 可选字符少了一个}}while (count == 0) {if (right - left + 1 == pLen) {ans.add(left);}if (map.containsKey(sChar[left])) {map.put(sChar[left], map.get(sChar[left]) + 1);if (map.get(sChar[left]) > 0) {count++; // 恢复可选字符}}left++;}right++;}return ans;}}
439. ternary-expression-parser
- 给一个三元运算的
? :
字符串,布尔表达式直接就是T或者F,其余的都是0-9的一位数字,求最终结果。例如F?1:T?4:5
最后就是4. 方法一:Stack。想到了要用Stack,但是没有想出确切的方法。其实就是需要从后往前遍历,确定最终需要保留的是什么数字就好了,在stack中存放两个值以及问号,这样当下一个字符(准确说是前一个)出现时只需要判断栈顶是否是问号就知道当前字符是作为bool还是值。
12345678910111213141516171819202122232425class Solution {public String parseTernary(String expression) {if (expression == null || expression.length() == 0) {return "";}Stack<Character> stack = new Stack<>(); // 存放数值和问号int len = expression.length();for (int i = len - 1; i >= 0; i--) { // 从末尾往前遍历char curr = expression.charAt(i);if (!stack.isEmpty() && stack.peek() == '?') {stack.pop();char first = stack.pop();char second = stack.pop();if (curr == 'T') {stack.push(first);} else {stack.push(second);}} else if (curr != ':') {stack.push(curr);}}return stack.peek() + "";}}方法二:递归DFS。对于每一个问号都进行计数++,每个冒号进行计数–,这样当计数归0的时候就找到了与问号对应的冒号,然后根据T/F递归找前后其中一部分的结果就好。
123456789101112131415161718192021222324252627public class Solution {public String parseTernary(String expression) {if (expression == null || expression.length() == 0) {return expression;}char[] exp = expression.toCharArray();return DFS(exp, 0, exp.length - 1) + "";}public char DFS(char[] c, int start, int end){if (start == end) {return c[start];}int count = 0, i = start;for (; i <= end; i++) {if (c[i] == '?') {count++;} else if (c[i] == ':') {count--;if (count == 0) {break;}}}return c[start] == 'T'? DFS(c, start + 2, i - 1) : DFS(c, i + 1, end);}}
442. find-all-duplicates-in-an-array
- 给一个
size = n
的int数组,其中每个int都在[1, n]
范围内。其中部分元素恰好出现了两次,剩余元素出现一次。求所有出现两次的元素,顺序无所谓。 - 暴力就是用Set,但如果要求不用额外空间且O(N)时间复杂度,就不行了。
- 充分利用
[1, n]
这个条件,每个int都可以作为index,那么就在index上面做文章,出现过的index就将元素标记一下,例如变为负数。这样在遍历数组过程中如果发现对应index已经是负数了,说明之前出现过。1234567891011121314151617class Solution {public List<Integer> findDuplicates(int[] nums) {List<Integer> ans = new ArrayList<>();if (nums == null || nums.length == 0) {return ans;}for (int i = 0; i < nums.length; i++) {int index = Math.abs(nums[i]) - 1;if (nums[index] < 0) {ans.add(index + 1);} else {nums[index] = -nums[index];}}return ans;}}
443. string-compression
- 给一个字符数组,统计字符出现的个数实现压缩。例如
a,a,a,a,a,b,c,c,c,c,c,c,c,c,c,c,c,c,c,c,c,c,c
就是a,5,b,c,1,7
。要求in-place。 - 每次从头开始,以第一个为基准往后判断,直到不想等,再根据个数往里塞。1234567891011121314151617181920212223class Solution {public int compress(char[] chars) {if (chars == null || chars.length == 0) {return 0;}int i = 0, len = 0;while (i < chars.length) {char curr = chars[i++]; // 以第一个字符为判断标准chars[len++] = curr;int count = 1;while (i < chars.length && curr == chars[i]) { // 统计个数直到不匹配i++;count++;}if (count != 1) { // 超过一个才拼接上数字for (char c : Integer.toString(count).toCharArray()) {chars[len++] = c;}}}return len;}}
444. sequence-reconstruction
- 给一个int数组org,再给一个List of list,这些list是某个original sequence的子序列,问根据这些子序列是否可以唯一地还原成一个完整的序列且正是org.
- 经典的拓扑排序。用
Map<Integer, Set<Integer>>
维护邻接关系,用Map<Integer>
维护inDegrees关系12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455class Solution {public boolean sequenceReconstruction(int[] org, List<List<Integer>> seqs) {if (org == null || org.length == 0 || seqs == null) {return false;}int n = org.length;Map<Integer, Integer> inDegrees = new HashMap<>();Map<Integer, Set<Integer>> graph = new HashMap<>();for (List<Integer> seq : seqs) {if (seq.size() == 1) { // 可能存在只有一个节点的seqgraph.putIfAbsent(seq.get(0), new HashSet<>());inDegrees.putIfAbsent(seq.get(0), 0);} else {for (int i = 1; i < seq.size();i++) { // 依次取每条seq的节点形成graphgraph.putIfAbsent(seq.get(i - 1), new HashSet<Integer>());graph.putIfAbsent(seq.get(i), new HashSet<Integer>());if (i == 1) {inDegrees.put(seq.get(i - 1), inDegrees.getOrDefault(seq.get(i - 1), 0));}if (graph.get(seq.get(i - 1)).add(seq.get(i))) {inDegrees.put(seq.get(i), inDegrees.getOrDefault(seq.get(i), 0) + 1);}}}}Queue<Integer> q = new LinkedList<>();for (int key : inDegrees.keySet()) {if (inDegrees.get(key) == 0) {q.offer(key);}}int index = 0;while (q.size() == 1) { // 必须保证每次只有一个入度为0的节点int startIndex = q.poll();if (startIndex != org[index]) {return false;}if (++index == n) {break;}Set<Integer> neighbors = graph.get(startIndex);if (neighbors == null) { // 没有后续节点可以判断break;}for (int neighbor : neighbors) {inDegrees.put(neighbor, inDegrees.get(neighbor) - 1);if (inDegrees.get(neighbor) == 0) {q.offer(neighbor);}}}return index == n && index == graph.size();}}
445. add-two-numbers-ii
- 给两个链表的头节点,每个节点表示一个数位,将两个数字相加得到的链表.
- 方法一:stack,将节点统统存入之后,逐个pop相加。
- 方法二:先求得长度,然后根据长度差来加。1234567891011121314151617181920212223242526272829303132333435363738public ListNode addTwoNumbers(ListNode l1, ListNode l2) {if (l1 == null || l2 == null) {return l1 == null ? l2 : l1;}int len1 = getLen(l1);int len2 = getLen(l2);ListNode head = new ListNode(0);ListNode next = len1 > len2 ? addTwoNumbers(l1, l2, len1 - len2) : addTwoNumbers(l2, l1, len2 - len1);boolean hasCarry = adjustCarry(head, next);return hasCarry ? head : next;}private ListNode addTwoNumbers(ListNode l1, ListNode l2, int offset) {if (l1 == null) {return null;}ListNode ret = offset > 0 ? new ListNode(l1.val) : new ListNode(l1.val + l2.val);ListNode next = addTwoNumbers(l1.next, offset > 0 ? l2 : l2.next, offset > 0 ? offset - 1 : offset);adjustCarry(ret, next);return ret;}private boolean adjustCarry(ListNode head, ListNode next) {boolean hasCarry = false;if (next != null && next.val > 9) {hasCarry = true;next.val -= 10;head.val += 1;}head.next = next;return hasCarry;}private int getLen(ListNode node) {int len = 0;while (node != null) {len++;node = node.next;}return len;}
448. find-all-numbers-disappeared-in-an-array
- 给一个长度为N的int数组,其中的值都在
1~N
之间,每个值可能出现1或2次,求中落下的数字。例如[4,3,2,7,8,2,3,1]
中,落下的数字是[5, 6]
. - 有点类似于swap求missing number,于是就想到用索引和value的关系来swap,最后和索引对应关系错误的就是missing的。但事实上不需要真的swap,而是可以在第一次遍历的时候,直接根据当前值跳到对应索引,然后将该处的值变为负数,这样就知道这个索引的值出现过了,第二次遍历直接将正值索引对应的数加入列表即可。1234567891011121314151617181920class Solution {public List<Integer> findDisappearedNumbers(int[] nums) {if (nums == null || nums.length == 0) {return new ArrayList<>();}for (int i = 0; i < nums.length; i++) {int index = Math.abs(nums[i]) - 1;if (nums[index] > 0) {nums[index] = -nums[index];}}List<Integer> retVal = new ArrayList<>();for (int i = 0; i < nums.length; i++) {if (nums[i] > 0) {retVal.add(i + 1);}}return retVal;}}
449. serialize-and-deserialize-bst
- 给一个BST,实现序列化和反序列化,即和String互相转换。
- 如果只是一个普通的二叉树,直接暴力写preorder和StringBuilder拼接没问题。但对于BST这个条件要怎么用呢?root一定比所有左边节点大、比所有右边节点小,那么在反序列化的时候直接通过val找到属于左边那半部分subtree去build即可。123456789101112131415161718192021222324252627282930313233343536373839404142434445public class Codec {// Encodes a tree to a single string.public String serialize(TreeNode root) {StringBuilder sb = new StringBuilder();preorder(root, sb);return sb.toString();}private void preorder(TreeNode root, StringBuilder sb) {if (root == null) {return;}sb.append(root.val);sb.append(",");preorder(root.left, sb);preorder(root.right, sb);}// Decodes your encoded data to tree.public TreeNode deserialize(String data) {if (data == null || data.length() == 0) {return null;}String[] vals = data.split(",");Queue<Integer> q = new LinkedList<>();for (String val : vals) {q.offer(Integer.parseInt(val));}return deserialize(q);}private TreeNode deserialize(Queue<Integer> q) {if (q.isEmpty()) {return null;}TreeNode root = new TreeNode(q.poll());Queue<Integer> smallerQueue = new LinkedList<>();while (!q.isEmpty() && q.peek() < root.val) {smallerQueue.offer(q.poll());}root.left = deserialize(smallerQueue);root.right = deserialize(q);return root;}}
450. delete-node-in-a-bst
- 给一个BST和一个key,如果存在这个key就删除这个值,返回root(可能会更新)。
- 经典。首先是搜索这个key,然后就是删除节点。如果这个节点是叶子,直接返回null;如果是单边,返回非空child;如果是双边children,则需要取中序遍历的下一个节点来替换掉当前节点。一种做法是去右子树的最左节点的值放到当前节点,再把该最左节点删除。另一种是真正的替换1234567891011121314151617181920212223242526272829303132333435363738class Solution {public TreeNode deleteNode(TreeNode root, int key) {if (root == null) {return root;}if (key < root.val) {root.left = deleteNode(root.left, key);} else if (key > root.val) {root.right = deleteNode(root.right, key);} else {if (root.left == null && root.right == null) {return null;}if (root.left == null) {return root.right;}if (root.right == null) {return root.left;}TreeNode minNode = getMinNode(root.right);// 方法一:覆盖value// root.val = minNode.val;// root.right = deleteNode(root.right, minNode.val);// 方法二:真正的替换minNode.right = deleteNode(root.right, minNode.val);minNode.left = root.left;root = minNode;}return root;}private TreeNode getMinNode(TreeNode root) {while (root.left != null) {root = root.left;}return root;}}
452. minimum-number-of-arrows-to-burst-balloons
- 给一个二维int数组,每一项表示一个气球的跨度,求最少用几根针可以扎破所有气球,注意气球的边界也算有效扎破范围。例如
[[10,16], [2,8], [1,6], [7,12]]
就至少需要两根针。 贪心做法,既然针蹭到也算扎破,那就按照气球的右边界从小到大排序,然后从头开始遍历,一旦发现当前区间的左边界大于了之前的右边界,就说明前面需要用新的针来继续扎后面的了。
1234567891011121314public int findMinArrowShots(int[][] points) {if (points == null || points.length == 0) {return 0;}Arrays.sort(points, (a, b) -> a[1] - b[1]);int prevPos = points[0][1], arrowCount = 1;for (int i = 1; i < points.length; i++) {if (points[i][0] > prevPos) {prevPos = points[i][1];arrowCount++;}}return arrowCount;}相似的题有56 Merge Intervals, 435 Non-overlapping Intervals, 252 Meeting Rooms, 253 Meeting Rooms II.
453. minimum-moves-to-equal-array-elements
- 给一个int数组,返回move几次能够让每个元素相等。move指的是对某位置以外的所有元素加1.
- 这题其实是个math problem,推导在此,假设所有数之和为sum,最小值为minNum,加了m次达到x,则有
sum + m * (n - 1) = x * n
以及minNum + m = x
,代入抵消一下就得到sum + mn - m = minNum * n + mn
,所求的m为sum - minNum * n = SUM(num_i - minNum)
。直接根据公式来,先求一波最小值,然后累加即可。12345678910111213141516class Solution {public int minMoves(int[] nums) {if (nums == null || nums.length == 0) {return 0;}int min = nums[0];for (int i = 1; i < nums.length; i++) {min = Math.min(min, nums[i]);}int minMove = 0;for (int num: nums) {minMove += (num - min);}return minMove;}}
454. 4sum-ii
- 给四个数组,求其中有多少个组合使得
A[i] + B[j] + C[k] + D[l] = 0
. - Map统计A和B的所有和出现的次数,然后遍历C+D的组合,到map中找对应项。skip。
456. 132-pattern
- 给一个int数组,判断其中是否有满足
i < k < j
且A[i] < A[j] < A[k]
的形式,类似于1, 3, 2
. - 暴力的想法就是固定i和j,在中间找k。但事实上在中间找k这个操作可以利用stack优化。首先走一波存放minBefore,表示当前索引及之前的元素中的最小值。然后从后往前遍历原数组,如果出现了
minBefore < num
,相当于固定A[i]
和A[k]
,从stack中找有没有落在范围内的值。如果持续弹出栈顶,直到比minBefore大,这时再看看是否小于num。12345678910111213141516171819202122232425class Solution {public boolean find132pattern(int[] nums) {if (nums == null || nums.length < 3) {return false;}Stack<Integer> stack = new Stack<>();int[] minBefore = new int[nums.length];minBefore[0] = nums[0];for (int i = 1; i < nums.length; i++) {minBefore[i] = Math.min(minBefore[i - 1], nums[i]);}for (int j = nums.length - 1; j >= 0; j--) {if (nums[j] > minBefore[j]) {// 固定(minBefore[j], nums[j]),找stack中有没有在之间的while (!stack.isEmpty() && stack.peek() <= minBefore[j]) {stack.pop();}if (!stack.isEmpty() && stack.peek() < nums[j]) {return true; // 出现了minBefore[j] < stack.peek() < nums[j]}stack.push(nums[j]);// 若到空了都没有,说明后面的都太小了}}return false;}}
457. circular-array-loop
- 给一个int数组,从任意一点出发,跳动步数就是数组的值,判断是否存在一个单一方向的、含有多于一个element的loop。数组中不含0.尝试不实用额外空间。
- 如果可以使用extra space,可以直接用Set记录到过的index以及某个path的index。如果不用额外空间呢?联想LinkedList找loop用快慢指针,这里也是一样。slow每次往后一步、fast每次两步,若全程能保持同一个方向移动且slow和fast相遇,则有loop.注意这里需要filter掉self-loop的元素。为了标记是否访问过而不使用Set,可以利用条件「原数组不含有0元素」,将访问过的元素改成0。保证一个方向的loop则是通过每一步经过的元素是否符号相同来判定的,也就是相乘大于0.1234567891011121314151617181920212223242526272829303132333435class Solution {public boolean circularArrayLoop(int[] nums) {if (nums == null || nums.length == 0) {return false;}for (int i = 0; i < nums.length; i++) {if (nums[i] == 0) {continue;}int n = nums.length, slow = i, fast = getNextIndex(i, nums[i], n);while (nums[fast] * nums[i] > 0 && nums[getNextIndex(fast, nums[fast], n)] * nums[i] > 0) {if (slow == fast) {if (slow == getNextIndex(slow, nums[slow], n)) { // 排除只含有一个element的cyclebreak;}return true;}slow = getNextIndex(slow, nums[slow], n); // 慢快指针分别移动一步两步fast = getNextIndex(fast, nums[fast], n);fast = getNextIndex(fast, nums[fast], n);}int j = i, val = nums[i];while (nums[j] * val > 0) {int nextIndex = getNextIndex(j, nums[j], n);nums[j] = 0;j = nextIndex;}}return false;}private int getNextIndex(int index, int step, int len) {int nextIndex = index + step;return nextIndex >= 0 ? nextIndex % len : nextIndex % len + len; // java的%求的是余数而不是真正的modulo}}
459. repeated-substring-pattern
- 给一个字符串,判断它是否能够通过子字符串自行重复拼接而成。
- 方法一:
O(N^2)
, 从小到达遍历各种子字符串长度,逐个判断是否能拼接成整个字符串。 - 方法二:
O(N)
使用KMP找字符串的common prefix和suffix的长度l
,n - l
表示非prefix或非suffix的部分,如果这个部分能够被总长度整除,就说明这个部分重复之后就能得到这个那个字符串。关于KMP,可以看1392. longest-happy-prefix加深理解。123456789101112131415161718192021class Solution {public boolean repeatedSubstringPattern(String s) {if (s == null || s.length() == 0) {return false;}int len = s.length();int[] dp = new int[len];for (int i = 1; i < len; i++) {int j = dp[i - 1];while (j > 0 && s.charAt(i) != s.charAt(j)) {j = dp[j - 1];}if (s.charAt(i) == s.charAt(j)) {j++;}dp[i] = j;}int l = dp[len - 1];return l > 0 && len % (len - l) == 0;}}
460. Least Frequently Used Cache
- 实现最近最频繁使用的有限容量缓存,到达容量上限时evict最不频繁使用的key,若频繁情况相同则evict掉最早插入的。
- 首先考虑如何实现根据频数来evict,这就需要一个
(freq -> 元素)
的map来维护,由于频数持平时需要evict掉更久远的元素,因此这个map中就需要维护一个List,根据LRU使用doublelinkedlist可以在O(1)
删除。当缓存满了,需要丢掉最小的freq中最早插入的元素,这个“最小的freq”只需要一个变量维护即可,而不需要保持整个freq都是有序的。1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192class LFUCache {class Node {int key, val, freq;Node next, prev;public Node(int key, int val) {this.key = key;this.val = val;this.freq = 1;}}class DoubleLinkedList {Node fakeHead, fakeTail;int size;public DoubleLinkedList() {fakeHead = new Node(-1, -1);fakeTail = new Node(-1, -1);fakeHead.next = fakeTail;fakeTail.prev = fakeHead;size = 0;}public void append(Node node) {node.next = fakeTail;node.prev = fakeTail.prev;fakeTail.prev.next = node;fakeTail.prev = node;size++;}public void remove(Node node) {node.prev.next = node.next;node.next.prev = node.prev;size--;}}private int capacity, minFreq;private Map<Integer, Node> nodeMap;private Map<Integer, DoubleLinkedList> freqMap;public LFUCache(int capacity) {this.capacity = capacity;minFreq = 0;nodeMap = new HashMap<>();freqMap = new TreeMap<>();}public int get(int key) {if (!nodeMap.containsKey(key)) {return -1;}Node node = nodeMap.get(key);updateFreq(node);return node.val;}public void put(int key, int value) {if (capacity == 0) {return;}if (!nodeMap.containsKey(key)) {Node node = new Node(key, value);if (nodeMap.size() == capacity) {DoubleLinkedList minFreqNodes = freqMap.get(minFreq);nodeMap.remove(minFreqNodes.fakeHead.next.key);minFreqNodes.remove(minFreqNodes.fakeHead.next);if (minFreqNodes.size == 0) {freqMap.remove(minFreq);}}nodeMap.put(node.key, node);freqMap.putIfAbsent(node.freq, new DoubleLinkedList());freqMap.get(node.freq).append(node);minFreq = node.freq;} else {Node node = nodeMap.get(key);node.val = value;updateFreq(node);}}private void updateFreq(Node node) {DoubleLinkedList oldList = freqMap.get(node.freq);oldList.remove(node);if (oldList.size == 0) {freqMap.remove(node.freq);minFreq += (minFreq == node.freq ? 1 : 0);}node.freq++;freqMap.putIfAbsent(node.freq, new DoubleLinkedList());DoubleLinkedList newList = freqMap.get(node.freq);newList.append(node);}}
461. hamming-distance
- 给两个int,求hamming distance. hamming distance指的是两个数不同的bit的个数,例如1001和0011就有两位不同。
- 异或之后看多少个bit。12345678910111213class Solution {public int hammingDistance(int x, int y) {int n = x ^ y;int ans = 0;for (int i = 0; i < 32; i++) {int mask = (1 << i);if ((n & mask) != 0) {ans++;}}return ans;}}
463. island-perimeter
- 给一个0/1二维矩阵,求其中为1的island的周长。
方法一:BFS,每个1都先算它有四条边,然后根据邻接情况减掉不是边的即可��
1234567891011121314151617181920212223242526272829303132333435363738394041424344class Solution {public int islandPerimeter(int[][] grid) {if (grid == null || grid.length == 0) {return 0;}boolean[][] visited = new boolean[grid.length][grid[0].length];int perimeter = 0;for (int i = 0; i < grid.length; i++) {for (int j = 0; j < grid[0].length; j++) {if (grid[i][j] == 1 && !visited[i][j]) {perimeter += bfs(grid, i, j, visited);}}}return perimeter;}private int[][] dirs = new int[][] {{1, 0}, {-1, 0}, {0, 1}, {0, -1}};private int bfs(int[][] grid, int i, int j, boolean[][] visited) {Queue<int[]> q = new LinkedList<>();q.add(new int[] {i, j});visited[i][j] = true;int perimeter = 0;while (!q.isEmpty()) {int[] curr = q.poll();perimeter += 4;for (int[] dir : dirs) {int row = curr[0] + dir[0], col = curr[1] + dir[1];if (isIsland(grid, row, col)) {perimeter--;if (!visited[row][col]) {q.add(new int[] {row, col});visited[row][col] = true;}}}}return perimeter;}private boolean isIsland(int[][] grid, int i, int j) {return i >= 0 && i < grid.length&& j >= 0 && j < grid[0].length&& grid[i][j] == 1;}}方法二:找规律。对于每一个1的cell,看它的下方和右方neighbor是不是1,记录neighbor数,最后周长就是islands 4 - neighbours 2。
1234567891011121314public int islandPerimeter(int[][] grid) {int islands = 0, neighbours = 0;for (int i = 0; i < grid.length; i++) {for (int j = 0; j < grid[i].length; j++) {if (grid[i][j] == 1) {islands++; // count islandsif (i < grid.length - 1 && grid[i + 1][j] == 1) neighbours++; // count down neighboursif (j < grid[i].length - 1 && grid[i][j + 1] == 1) neighbours++; // count right neighbours}}}return islands * 4 - neighbours * 2;}
464. can-i-win
- 给定一个最大可取的int,再给个目标值target,两个人轮流从
[1, int]
之间取值,用过的值就不能再用了,两个人取的值不断累加,恰好达到或超过target的人就赢了。问是否能稳赢。 - 经典的DFS递归。有两个状态需要维护,一个是可选的数字需要用map或者数组bucket存起来,一个是剩余的target。同样为了避免DFS重复计算,需要用一个map将中间结果存起来。如果不加memorize的话时间复杂度
O(N!)
,相当于从1到N每一步都有N, N-1, N-2…个选择。如果加了memorize,则提升到O(2^N)
,相当于1到N每个数字都有取或不取两种状态,可以保证访问过的状态不会重复heo访问,那么就是2^N
。12345678910111213141516171819202122232425262728293031class Solution {public boolean canIWin(int maxChoosableInteger, int desiredTotal) {if (desiredTotal <= 0) {return true;}int maxTotal = (1 + maxChoosableInteger) * maxChoosableInteger / 2;if (maxTotal < desiredTotal) {return false;}return checkWin(desiredTotal, new boolean [maxChoosableInteger], new HashMap<String, Boolean>());}private boolean checkWin(int total, boolean[] bucket, Map<String, Boolean> map) {String state = Arrays.toString(bucket);if (map.containsKey(state)) {return map.get(state);}for (int i = 0; i < bucket.length; i++) {if (!bucket[i]) {bucket[i] = true;if (i + 1 >= total || !checkWin(total - (i + 1), bucket, map)) { // 超过或者对方必输,我就赢了map.put(state, true);bucket[i] = false;return true;}bucket[i] = false;}}map.put(state, false); // 遍历了所有可能值都不行,必输return false;}}
468. validate-ip-address
- 给一个字符串,返回他属于的IP地址类型,若不属于IPv4或IPv6,返回Neither. IPv4的规定是有四个数字组成,用
.
分开。 - 很没意思的一题,就是split之后各种判断,需要考虑各种edge case,例如
-0
,192.0.0.1.
。1234567891011121314151617181920212223242526272829303132333435363738394041424344454647class Solution {private final String NEITHER = "Neither";private final String IPV6 = "IPv6";private final String IPV4 = "IPv4";public String validIPAddress(String IP) {if (IP == null || IP.length() == 0) {return NEITHER;}if (validIPv4(IP)) {return IPV4;} else if (validIPv6(IP)) {return IPV6;} else {return NEITHER;}}private boolean validIPv4(String addr) {String[] parts = addr.split("\\.", -1);if (parts.length != 4) {return false;}for (String part : parts) {try {int value = Integer.valueOf(part);if (part.length() > 3 || value < 0 || value > 255|| !String.valueOf(value).equals(part)) {return false;}} catch(NumberFormatException ex) {return false;}}return true;}private boolean validIPv6(String addr) {String[] parts = addr.split(":", -1);if (parts.length != 8) {return false;}for (String part : parts) {if (!part.matches("^[0-9a-fA-F]{1,4}$")) {return false;}}return true;}}
470. implement-rand10-using-rand7
- 利用rand7实现rand10,返回随机数1-10.
- 把它想像成掷骰子,用7面骰子模拟10面骰子,最直接的想法是投多次。例如投两次的话,总共有
7*7
种可能性,为了映射到1~10
,最直接的办法就是取模。但是1~49直接取模并不是均匀分布的,因此可以直接将尾巴去掉,即只取到1~40
,之所以可以这样是因为每个数字取到的概率都是一样的,我筛掉不取也一样。具体证明见这里-Using-RandN()%22)。 - 由此可以推广,大模拟小的情况下可以直接忽略较大值或取较小值的模,例如用6面骰子模拟2面骰子,可以只取1、2,或者取
(x % 2) + 1
. 小模拟大则需要找到超过大值的最小幂,例如用3面骰子模拟11面骰子,则可以掷3面骰子3次(3*3*3 = 27
),取1~22
,最后取x % 11 + 1
。1234567891011121314/*** The rand7() API is already defined in the parent class SolBase.* public int rand7();* @return a random integer in the range 1 to 7*/class Solution extends SolBase {public int rand10() {int retVal = 40;while (retVal >= 40) {retVal = 7 * (rand7() - 1) + (rand7() - 1);}return retVal % 10 + 1;}}
471. encode-string-with-shortest-length
- 给一个字符串,将它压缩成
k[part]
的形式,要求压缩后的长度比原来短(答案不唯一)。 DFS + memo,尝试将字符串拆分成一半、1/3、1/4以此类推的子串,如果发现了重复出现的pattern就可以压缩,同时也尝试将字符串劈成左右两部分分别压缩,最后最小长度的那个。时间复杂度应该是O(N^4)。其中查找最短的重复出现pattern可以用KMP算法,KMP求的是“最长共同prefix和suffix”,也就对应“最短的重复pattern”。
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152class Solution {public String encode(String s) {return encode(s, new HashMap<String, String>());}private String encode(String s, Map<String, String> map) {if (s == null || s.length() == 0) {return "";}int len = s.length();if (len <= 4) {return s;}if (map.containsKey(s)) {return map.get(s);}String retVal = s;// 从一半开始,尝试将字符串拆分成重复出现的部分for (int k = len / 2; k < len; k++) {String pattern = s.substring(k);int count = countOccurance(s, pattern);if (count > 1) {String formatted = count + "[" + encode(pattern, map) + "]";if (formatted.length() < retVal.length()) {retVal = formatted;}}}for (int k = 1; k < len; k++) {String left = s.substring(0, k), right = s.substring(k);String formatted = encode(left, map) + encode(right, map);if (formatted.length() < retVal.length()) {retVal = formatted;}}map.put(s, retVal);return retVal;}private int countOccurance(String s, String pattern) {if (s.length() % pattern.length() != 0) {return 0;}int count = 0;for (int i = 0; i < s.length(); i += pattern.length()) {if (s.substring(i).startsWith(pattern)) {count++;} else {return 0;}}return count;}}DP也是类似的思路,
dp[i][j]
表示以i为起点、j为长度的子字符串的压缩结果,因此后面的结果需要依赖于前面所有更短的字符串的压缩结果,所以dp最外层循环根据的就是长度,然后内层循环尝试将字符串本身压缩、或者divide and conquer左右两部分的压缩结果。这里用到了KMP。1234567891011121314151617181920212223242526272829303132333435363738394041424344454647class Solution {public String encode(String s) {if (s == null || s.length() == 0) {return "";}int len = s.length();String[][] dp = new String[len + 1][len + 1];for (int l = 1; l <= len; l++) {for (int i = 0; i + l <= len; i++) {int j = i + l;String substr = s.substring(i, j);dp[i][j] = substr;if (l > 4) {String pattern = getShortestPattern(substr);if (pattern.length() > 0) {String formatted = String.valueOf(substr.length() / pattern.length()) + "[" + dp[i][i + pattern.length()] + "]";if (formatted.length() < dp[i][j].length()){dp[i][j] = formatted;}}for (int k = i + 1; k < j; k++) {if (dp[i][k].length() + dp[k][j].length() < dp[i][j].length()) {dp[i][j] = dp[i][k] + dp[k][j];}}}}}return dp[0][len];}private String getShortestPattern(String s) {int len = s.length();int[] kmp = new int[len];for (int i = 1; i < len; i++) {int j = kmp[i - 1];while (j > 0 && s.charAt(j) != s.charAt(i)) {j = kmp[j - 1];}if (s.charAt(j) == s.charAt(i)) {j++;}kmp[i] = j;}int prefixLen = kmp[len - 1], patternLen = len - prefixLen;return prefixLen > 0 && len % patternLen == 0 ? s.substring(0, patternLen) : "";}}
472. concatenated-words
- 给一个字符串数组,求所有能通过其他字符串拼接而成的字符串。例如
[a, b, abb, bbab, c]
返回[abb, bbab]
. - 近似于暴力的做法:首先将数组按照长度进行排序,然后逐个判断是否能够通过更短的部分组成。这个判断的过程可以用到双指针+DP,利用双指针来取子字符串看看是否在先前出现过,如果出现过且子字符串往前部分也可以由别的部分组成,则到当前索引为止也是可以由其他组成的。假设字符串们平均长度为
L
,则判断部分O(L*L)
;主函数对字符串进行排序为O(NlogN)
,对每个字符串遍历调用判断,整体时间复杂度为O(N*L*L)
。12345678910111213141516171819202122232425262728293031323334353637class Solution {public List<String> findAllConcatenatedWordsInADict(String[] words) {List<String> retVal = new ArrayList<>();if (words == null || words.length == 0) {return retVal;}Arrays.sort(words, new Comparator<String>() {public int compare(String a, String b) {return a.length() - b.length();}});Set<String> preWordSet = new HashSet<>();for (String word : words) {if (canForm(word, preWordSet)) {retVal.add(word);}preWordSet.add(word);}return retVal;}private boolean canForm(String word, Set<String> preWordSet) {if (preWordSet.isEmpty()) {return false;}boolean[] dp = new boolean[word.length() + 1];dp[0] = true;for (int right = 1; right <= word.length(); right++) {for (int left = 0; left < right; left++) {if (dp[left] && preWordSet.contains(word.substring(left, right))) {dp[right] = true;break;}}}return dp[word.length()];}}
475. heaters
- 给两个数组,一个表示房屋的水平坐标(非负int)、另一个表示暖气的位置。求最小的暖气半径使得每个房子都能被覆盖。
- greedy其实就需要找到每个房子距离最近的暖气的距离,取最大值即可保证当暖气为该距离时所有的房子都会被覆盖到。因此对两个数组分别排序后,固定房子坐标,然后取「与当前暖气的距离」跟「与后续暖气的距离」比较,若与当前暖气的距离更近就不需要用到后续暖气来覆盖了,否则就继续往后找暖气。12345678910111213141516171819class Solution {public int findRadius(int[] houses, int[] heaters) {if (houses == null || houses.length == 0|| heaters == null || heaters.length == 0) {return 0;}Arrays.sort(houses);Arrays.sort(heaters);int i = 0, ans = 0;for (int house : houses) {while (i < heaters.length - 1 &&Math.abs(heaters[i] - house) >= Math.abs(heaters[i + 1] - house)) {i++;}ans = Math.max(ans, Math.abs(heaters[i] - house));}return ans;}}
476. number-complement
- 给一个正整数,求它的所有bit flip之后的正整数。例如
5 = 101
,就对应2 = 010
。 - 可以构造一个全是1的数字,直接异或/减去原数即可。
111 ^ 101 = 010
123456789101112class Solution {public int findComplement(int num) {if (num <= 0) {return 0;}int ones = 0;while (ones < num) {ones = (ones << 1) | 1;}return ones ^ num;}}
477. total-hamming-distance
- 给一个int数组,求这些数两两之间的hamming distance之和。hamming distance指的是两个数不同的bit的个数,例如1001和0011就有两位不同。
- 直接用mask每一个bit地看有多少个数x该位为1,然后x乘一下(N - x)就得到该位不同的数的个数了。12345678910111213141516171819class Solution {public int totalHammingDistance(int[] nums) {if (nums == null || nums.length == 0) {return 0;}int ans = 0;for (int i = 0; i < 32; i++) {int mask = (1 << i);int count = 0;for (int j = 0; j < nums.length; j++) {if ((nums[j] & mask) != 0) {count++;}}ans += (count * (nums.length - count));}return ans;}}
480. sliding-window-median
- 给一个数组,给一个窗口size = k,从前往后滑动窗口,求每一个范围的median。
- 用两个PriorityQueue分别维护大根堆(存的是较小的值)和小根堆(存的是较大的值),在往里存元素的时候先尝试往minHeap中存比堆顶大的值,不行就直接存入maxHeap。两个堆加起来存够k个元素之后,还需要根据两个堆的size进行调整,因为不一定刚好一半一半。匀完了之后,每次从两个堆中取最大、最小值,再根据size = k决定中位数是直接取中间还是求平均。当窗口往后挪了之后,需要从两个堆的其中一个中删除,PriorityQueue的
offer, poll, remove
是O(logN)
的,contains
是O(N)
的,retreive peek
是O(1)
的。1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950class Solution {// 和前面的slidingWindowMax单调队列有点像// 用一个单调队列可以求一个最大值/最小值,那么用两个单调队列维护大根堆和小根堆// 分别取一个元素出来,看看是否需要求平均public double[] medianSlidingWindow(int[] nums, int k) {if (nums == null || nums.length == 0) {return new double[0];}Queue<Integer> maxHeap = new PriorityQueue<>(k, Collections.reverseOrder());Queue<Integer> minHeap = new PriorityQueue<>(k);double[] ans = new double[nums.length - k + 1];for (int i = 0; i < nums.length; i++) {if (i >= k) {if (minHeap.contains(nums[i - k])) {minHeap.remove(nums[i - k]);} else {maxHeap.remove(nums[i - k]);}}if (minHeap.size() > 0 && nums[i] > minHeap.peek()) {minHeap.add(nums[i]);} else {maxHeap.add(nums[i]);}adjustHeaps(maxHeap, minHeap);if (i >= k - 1) {ans[i - (k - 1)] = getMedian(maxHeap, minHeap);}}return ans;}private double getMedian(Queue<Integer> minHeap, Queue<Integer> maxHeap) {return minHeap.size() == maxHeap.size() ?((double)minHeap.peek() + (double)maxHeap.peek()) / 2.0 :minHeap.size() > maxHeap.size() ? minHeap.peek() : maxHeap.peek();}private void adjustHeaps(Queue<Integer> maxHeap, Queue<Integer> minHeap) {while (Math.abs(maxHeap.size() - minHeap.size()) > 1) {if (maxHeap.size() > minHeap.size()) {minHeap.add(maxHeap.poll());} else {maxHeap.add(minHeap.poll());}}}}
482. license-key-formatting
- 给一个字符串,只含有数字和字母,再给一个K,将字符以K个为一组组成licenseKey。skip.
484. find-permutation
- 给一个只有D和I的字符串,表示相邻两个数的大小关系下降和上升。求lexicographical最小的、符合这个升降关系的
1~n+1
的数组。 - 贪心做法,先一波升序填进去,然后再对应遍历字符串,对于D就一直往后找连续的D,将这一段reverse即可。123456789101112131415161718192021public int[] findPermutation(String s) {int n = s.length(), arr[] = new int[n + 1];for (int i = 0; i <= n; i++) arr[i] = i + 1; // sortedfor (int h = 0; h < n; h++) {if (s.charAt(h) == 'D') {int l = h;while (h < n && s.charAt(h) == 'D') h++;reverse(arr, l, h);}}return arr;}void reverse(int[] arr, int l, int h) {while (l < h) {arr[l] ^= arr[h]; // 酷炫的异或swap整数arr[h] ^= arr[l];arr[l] ^= arr[h];l++; h--;}}
485. max-consecutive-ones
- 给一个只含有0和1的数组,求最多连续出现1的个数。
- 用一个lastIndex记录上一个出现的1的位置,然后不断往后遍历数组,如果是1就更新count、否则就把lastIndex更新过来。12345678910111213141516171819class Solution {public int findMaxConsecutiveOnes(int[] nums) {if (nums == null || nums.length == 0) {return 0;}int max = 0, lastIndex = 0;for (int i = 0; i < nums.length; i++) {if (nums[i] == 1) {if (nums[lastIndex] == 0) {lastIndex = i;}max = Math.max(i - lastIndex + 1, max);} else {lastIndex = i;}}return max;}}
486. predict-the-winner
- 给一个int数组,两个玩家每次可以从两端任取一个数,轮流取完后比较取出数字之和,谁大谁赢(相等则player 1赢)。判断先选数字的player 1是否稳赢(两个玩家都会走最优)
- DP。是否赢需要依赖之前选数字的状态,最终胜负并不在意具体的sum是多少,而是比较两个玩家的sum,因此
dp[i][j]
存储nums[i...j]
中此时选择的玩家会比另一个玩家多多少分。相比上一步,当前玩家可以在两端分别选nums[i]或者nums[j],每次会选让自己分更多的,即Math.max(nums[i] - dp[i + 1][j], nums[j] - dp[i][j - 1]
(注意上一步存的是另一个玩家比自己多多少分,因此需要取负).注意到dp每次只会用到左侧和下侧相邻的cell,可以优化成只用一维数组的dp-space-complexity.)。12345678910111213141516171819class Solution {public boolean PredictTheWinner(int[] nums) {if (nums == null || nums.length == 0) {return false;}int n = nums.length;int[][] dp = new int[n][n];for (int i = 0; i < n; i++) {dp[i][i] = nums[i];}for (int len = 1; len < n; len++) {for (int i = 0; i < n - len; i++) {int j = i + len;dp[i][j] = Math.max(nums[i] - dp[i + 1][j], nums[j] - dp[i][j - 1]);}}return dp[0][n - 1] >= 0;}}
487. max-consecutive-ones-ii
- 给一个只含有0和1的数组,至多可以将一个0 flip成1,求最长连续出现1的个数。
利用双指针维护一个至多含有一个0的window,当0过多就移动左指针直到恢复。
123456789101112131415161718192021class Solution {public int findMaxConsecutiveOnes(int[] nums) {if (nums == null || nums.length == 0) {return 0;}int max = 0, zeroCount = 0;for (int left = 0, right = 0; right < nums.length; right++) {if (nums[right] == 0) {zeroCount++;}while (zeroCount > 1) { // 将1改为k即可处理flip k个零的情况if (nums[left] == 0) {zeroCount--;}left++;}max = Math.max(max, right - left + 1);}return max;}}follow-up: 如果输入的数组无法全部存入内存?输入将以stream的形式传入,这样的话就不能直接存放整个数组,可以将零出现的index存入queue,当queue的size超过k的时候就说明window中零的个数过多,此时就将left移到
q.poll() + 1
即可。
489. robot-room-cleaner
- 给一个Robot机器人类,这个机器人初始化在一个房间内面朝上,0表示障碍、1表示空地。这个机器人可以左转、右转、前进(若遇到障碍/边界则返回false)、清扫。要求将这个房间完全清扫。
- 本质上就是考图的遍历,区别是只能用一个机器人通过DFS做。每次机器人进入一个cell的时候需要转到四个方向尝试前进,可以固定只向右转。四个方向都DFS完后会回到进入当前cell的方向,此时就需要回退到上一个cell,此时就需要实现一个掉头、前进、再掉头的步骤。时间复杂度为
O(1cells - 0cells)
,因为每个cell只会进入一次。1234567891011121314151617181920212223242526class Solution {private int[][] dirs = {{-1, 0}, {0, 1}, {1, 0}, {0, -1}};private void clean(Robot robot, int row, int col, int dirIndex, Set<String> visited) {robot.clean();visited.add(row + " " + col);for (int i = 0; i < 4; i++) {int index = (dirIndex + i) % 4;int rowNext = row + dirs[index % 4][0];int colNext = col + dirs[index % 4][1];if (!visited.contains(rowNext + " " + colNext) && robot.move()) {clean(robot, rowNext, colNext, index, visited);// 回退到进入dfs前的状态robot.turnRight();robot.turnRight();robot.move();robot.turnRight();robot.turnRight();}robot.turnRight();}}public void cleanRoom(Robot robot) {clean(robot, 0, 0, 0, new HashSet<>());}}
490. the-maze
- 给一个grid,0表示空地1表示障碍物,一颗球在里面滚动,只有碰到障碍物或者边缘才会停下,给起点和终点坐标判断能否到达。BFS. skip.
494. target-sum
- 给一个只含有非负数的int数组和一个target,给这些int加正号或负号进行求和,求共有多少中加符号的方式使得sum等于target。其中所有数的sum不会超过1000,且数组长度不超过20.
- 注意到了限制“所有数的sum不会超过1000”,就联想到了木桶法。每个bucket[index]表示和为index有多少种方式,那么读入新的数x时将当前index的数加到[index+x]和[index-x]处即可。注意不可对原数组直接操作。12345678910111213141516171819202122232425262728293031class Solution {public int findTargetSumWays(int[] nums, int S) {if (nums == null || nums.length == 0 || Math.abs(S) > 1000) {return 0;}int[] bucket = new int [2001]; // 全部平移1000int[] bucketTemp = new int [2001];bucket[1000] = 1; // sum = 0初始有一种情况,即空的输入for (int i = 0; i < nums.length; i++) {for (int j = 0; j < bucket.length; j++) {if (bucket[j] > 0) {if (nums[i] != 0) {bucketTemp[j - nums[i]] += bucket[j]; // 以当前值为中心点往两边拓展bucketTemp[j + nums[i]] += bucket[j];} else {bucketTemp[j] = bucket[j] * 2; // 当前值为0则直接翻倍(+0 / -0)}bucket[j] = 0;}}for (int j = 0; j < bucketTemp.length; j++) {if (bucketTemp[j] != 0) {bucket[j] = bucketTemp[j];bucketTemp[j] = 0;}}}return bucket[S + 1000];}}
495. teemo-attacking
- pass。
496. next-greater-element-i
- 给两个int数组,都不含重复元素,求nums1中元素在nums2中的next greater.不存在则设为-1.
- 首先处理一波nums2,从前往后入栈,每次入栈之前需要判断是否小于栈顶,如果大于了栈顶,说明栈中元素的next greater就是当前元素,用一个map存起来(不含重复元素就可以这样搞),最后遍历nums1的时候直接从map中取就可以了。1234567891011121314151617181920class Solution {public int[] nextGreaterElement(int[] nums1, int[] nums2) {if (nums1 == null || nums2 == null) {return new int [0];}Map<Integer, Integer> map = new HashMap<>();Stack<Integer> stack = new Stack<>();for (int num: nums2) {while (!stack.isEmpty() && stack.peek() < num) {map.put(stack.pop(), num);}stack.push(num);}int[] ans = new int [nums1.length];for (int i = 0; i < nums1.length; i++) {ans[i] = map.getOrDefault(nums1[i], -1);}return ans;}}
498. diagonal-traverse
- 给一个matrix,求蛇形遍历后得到的一维数组。
- 分情况讨论,向右上遍历的时候可能从上、右侧出去,向左下遍历的时候可能从左、下侧出去,对应调整index即可。pass。
499. the-maze-iii
- 给一个grid,0表示空地1表示障碍物,一颗球在里面滚动,只有碰到障碍物或者边缘才会停下,给起点和终点(和另两题的区别是这是一个洞,球滚到这里就会掉进去)坐标,求最短滚过的格子的路径(用u, d, l, r代替走过的上下左右),若有多个最短路径则取lexicographical更小的那个路径,若无法到达则返回
impossible
. - 和前面第二题相比又需要存多一个path的信息,同时PriorityQueue的比较函数也需要更新当滚过的路程一样长时需要比较String。1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374class Solution {class Point implements Comparable<Point> {int row, col, step;String path;public Point(int row, int col, int step, String path) {this.row = row;this.col = col;this.step = step;this.path = path;}public int compareTo(Point that) {return this.step == that.step ? this.path.compareTo(that.path) : this.step - that.step;}}private final String IMP = "impossible";public String findShortestWay(int[][] maze, int[] ball, int[] hole) {if (maze == null || maze.length == 0 || maze[0].length == 0|| ball == null || ball.length != 2|| hole == null || hole.length != 2) {return IMP;}int rows = maze.length, cols = maze[0].length;boolean[][] visited = new boolean[rows][cols];PriorityQueue<Point> q = new PriorityQueue<>();q.offer(new Point(ball[0], ball[1], 0, ""));while (!q.isEmpty()) {Point curr = q.poll();if (curr.row == hole[0] && curr.col == hole[1]) {return curr.path;}visited[curr.row][curr.col] = true;Point up = getNext(curr, -1, 0, maze, hole);if (!visited[up.row][up.col]) {up.path += "u";q.offer(up);}Point down = getNext(curr, 1, 0, maze, hole);if (!visited[down.row][down.col]) {down.path += "d";q.offer(down);}Point left = getNext(curr, 0, -1, maze, hole);if (!visited[left.row][left.col]) {left.path += "l";q.offer(left);}Point right = getNext(curr, 0, 1, maze, hole);if (!visited[right.row][right.col]) {right.path += "r";q.offer(right);}}return IMP;}private Point getNext(Point curr, int rowShift, int colShift, int[][] maze, int[] hole) {Point next = new Point(curr.row, curr.col, curr.step, curr.path);while (next.row + rowShift >= 0 && next.row + rowShift < maze.length&& next.col + colShift >= 0 && next.col + colShift < maze[0].length&& maze[next.row + rowShift][next.col + colShift] == 0) {next.row += rowShift;next.col += colShift;next.step++;if (next.row == hole[0] && next.col == hole[1]) {break;}}return next;}}
503. next-greater-element-ii
- 给一个circular数组,求每一个元素的下一个更大的元素的索引,如果不存在则设为-1。例如
[1,2,1]
返回[2,-1,2]
。 - 这种circular性质的,容易想到直接拼接一段重复的元素到后方,转换成普通数组找后续更大值,时间O(N^2)。此外还能利用stack存放索引,首先从后往前把所有元素都丢进去,然后i还是从后往前遍历原数组并与栈顶索引对应的元素比较,直到小于栈顶时才将栈顶存入结果数组。此时还需要将当前索引push到栈中,因为循环的下一步是往前一个,所以需要将当前的元素存入stack作为最接近的next candidate.123456789101112131415161718192021222324class Solution {public int[] nextGreaterElements(int[] nums) {if (nums == null) {return new int [0];}int[] ans = new int [nums.length];Stack<Integer> stack = new Stack<>();for (int i = nums.length - 1; i >= 0; i--) { // 从后往前将元素入栈stack.push(nums[i]);ans[i] = -1;}for (int i = nums.length - 1; i >= 0; i--) { // 从后往前找greaterwhile (!stack.isEmpty() && nums[i] >= stack.peek()) {stack.pop();}if (!stack.isEmpty()) {ans[i] = stack.peek();}stack.push(nums[i]); // 因为下一个要遍历i - 1,所以就直接把i给入栈}return ans;}}
505. the-maze-ii
- 给一个grid,0表示空地1表示障碍物,一颗球在里面滚动,只有碰到障碍物或者边缘才会停下,给起点和终点坐标,求最短滚过的格子数,若无法到达则返回-1.
- 仍然是BFS,只不过此时需要利用PriorityQueue代替传统BFS的Queue,这个PQ的比较函数是将从起点滚过距离最短的放在前面,这样率先到达终点的就一定是最短路径。
508. most-frequent-subtree-sum
- 给一个二叉树,求所有subtree sum中出现最频繁的,tie则全都输出。
- 递归求sum过程中直接用map统计出现次数,最后导出来存入数组返回即可。skip。
509. fibonacci-number
- 实现斐波那契数列。
- 递归/memo递归/双变量都可以。还有两个没有见过的方法,一是用矩阵Matrix Exponentiation,这样只需要
O(logN)
时间&空间复杂度就可以了。二是用纯数学,用黄金比例可以O(1)
实现。具体见这里
510. inorder-successor-in-bst-ii
- 给一个BST中的节点(除了left, right, val还有parent),求在in-order遍历中它的下一个节点,即比他大的最小节点。
- 根据BST的定义,节点左子树全部比它小、右子树全部比它大。给定节点,如果它有右子树,则取右子树的最左节点即可。如果它没有右子树,说明在它上方某处,一直向上回溯,直到找到一个节点是parent的左子树,那么这个parent就是恰好比所有这堆左子树大的了。时间复杂度为
O(树高度)
,平均为O(logN)
,最坏O(N)
.12345678910111213141516171819202122232425262728/*// Definition for a Node.class Node {public int val;public Node left;public Node right;public Node parent;};*/class Solution {public Node inorderSuccessor(Node node) {if (node == null) {return null;}if (node.right == null) {while (node.parent != null && node == node.parent.right) {node = node.parent;}return node.parent;} else {node = node.right;while (node.left != null) {node = node.left;}return node;}}}
513. find-bottom-left-tree-value
- 给一个二叉树,求最下面一层最左边的节点。
方法一:Iterative,从右往左进行层级遍历,最后一个遍历到的节点就是最下层的最左节点。
1234567891011121314151617class Solution {public int findBottomLeftValue(TreeNode root) {Queue<TreeNode> q = new LinkedList<>();q.offer(root);TreeNode curr = null;while (!q.isEmpty()) {curr = q.poll();if (curr.right != null) {q.offer(curr.right);}if (curr.left != null) {q.offer(curr.left);}}return curr.val;}}方法二:Recursive, 正常地从左到右前序遍历,但是会track深度,第一个达到新的深度的节点就一定是最左边的。
12345678910111213141516171819class Solution {int ans = 0, h = 0;public int findBottomLeftValue(TreeNode root) {find(root, 1);return ans;}public void find(TreeNode root, int level) {if (h < level) {ans = root.val;h = level;}if (root.left != null) {find(root.left, level + 1);}if (root.right != null) {find(root.right, level + 1);}}}
515. find-largest-value-in-each-tree-row
- 给一个树,返回它每一个level节点的最大值。
- 层级遍历嘛。DFS和BFS。12345678910111213141516171819202122232425class Solution {List<Integer> levelMax;public List<Integer> largestValues(TreeNode root) {levelMax = new ArrayList<>();dfsLevelMax(root, 0);return levelMax;}private void dfsLevelMax(TreeNode node, int level) {if (node == null) {return;}updateLevelMax(node.val, level);dfsLevelMax(node.left, level + 1);dfsLevelMax(node.right, level + 1);}private void updateLevelMax(int val, int level) {if (levelMax.size() == level) {levelMax.add(val);} else {levelMax.set(level, Math.max(val, levelMax.get(level)));}}}
516. longest-palindromic-subsequence
- 给一个字符串,求其中最长的自对称的subsequence(顺序与原字符串一样但不一定是连续的)的长度。例如
bbbab
最长为4(bbbb
)。 - DP。和647一样也是从后往前更新DP数组,
dp[i][j]
表示从i到j(inclusive)的最长长度。12345678910111213141516171819202122class Solution {public int longestPalindromeSubseq(String s) {if (s == null || s.length() == 0) {return 0;}char[] sChar = s.toCharArray();int[][] dp = new int [sChar.length][sChar.length];for (int row = sChar.length - 1; row >= 0; row--) { // 从最后一个字符往前更新dp[row][row] = 1;for (int col = row + 1; col < sChar.length; col++) {if (sChar[row] == sChar[col]) {dp[row][col] = 2 + dp[row + 1][col - 1]; // 取中间夹的部分的最长长度加上头尾两个// compare prev or adding curr palindrome. no worry about exceeding boundary} else { // 不取当前字符dp[row][col] = Math.max(dp[row][col - 1], dp[row + 1][col]); // keep at previous one}}}return dp[0][sChar.length - 1];}}
518. coin-change-2
- 给一个数组表示有哪些面额的硬币,每个面额的硬币有无限多个可以任取。给定一个目标值,求总共有多少种组合方式。
- DP。和前面的那个硬币题类似,这里dp[i]表示达到i这个值有多少组合方式。如果当前硬币的面额是x,则dp[i] = dp[i] + dp[i - x].12345678910111213141516171819202122class Solution {public int change(int amount, int[] coins) {if (amount == 0) {return 1;}if (coins == null || coins.length == 0) {return 0;}// dp[amount]表示凑成amount有几种ways// 对于每个dp[i + coin] = dp[i + coin] + dp[i]int[] dp = new int [amount + 1];dp[0] = 1;for (int i = 0; i < coins.length; i++) { // 固定coin值遍历所有可能值for (int j = 0; j < dp.length; j++) {if (j >= coins[i]) {dp[j] += dp[j - coins[i]]; // 从j - coins[i]增加到j}}}return dp[amount];}}
523. continuous-subarray-sum
- 给一个非负int数组和一个非负整数k,判断是否含有长度大于等于2的连续subarray whose sum是k的multiple。可以保证将原数组所有数加起来不会爆int。
- 方法一:暴力法,求膜的方式O(N^2)两重循环求sum。
- 方法二:求是否含有一部分使得sum是n*k,那么如果利用Map记录sum对应的索引,一路往后加x%k,当出现前面已经存在的sum的时候,说明这一路加的x刚好膜k得到0,即为k的倍数。注意需要特殊处理0,不可以膜0.12345678910111213141516171819202122232425class Solution {public boolean checkSubarraySum(int[] nums, int k) {if (nums == null || nums.length == 0) {return true;}// 记录得到当前sum对应的索引Map<Integer, Integer> sum2Index = new HashMap<>();sum2Index.put(0, -1);int sum = 0;for (int i = 0; i < nums.length; i++) {sum += nums[i];if (k != 0) {sum %= k; // 膜来处理倍数问题}if (sum2Index.containsKey(sum)) { // 说明膜了一波等于0,中间可能有k的倍数if (i - sum2Index.get(sum) > 1) { // 注意需要长度大于等于2return true;}} else {sum2Index.put(sum, i);}}return false;}}
524. longest-word-in-dictionary-through-deleting
- 给一个字符串s,给一个单词List,求将s中部分字母删除后能得到的最长的在List中的单词,若长度相等则取lexicographical更短的。
- 遍历List中的单词,与s比较看是否是subsequence。基本是暴力做法,只不过prune掉了一些情况,只有当candidate比之前的结果更长或者lexicographical order更前才会计算。还有一种做法是先对List进行排序,长度长的在前、字典序小的在前,然后再直接遍历List,第一个subsequence即为所求。
525. contiguous-array
- 给一个只含0,1的int数组,求最长subarray的长度使得其中的0,1个数相等。
- 方法一:暴力破解,固定start和end依次看当前窗口的0,1个数。O(N^2)超时。
- 方法二:既然需要的是0,1相等的个数而不关心具体位置/顺序,也就是如果经过一波操作之后0和1的个数差回到原点,说明一波操作没有造成什么后果,也就是0,1个数相等。或者更近一步,将0先一波流替换成-1,这样只要sum为0就意味着相等,而且如果当前的sum在之前出现过,说明经过一波操作又回到原点,那么-1,1的个数也是相等的。因此想到用map记录sum出现的各个位置,只记录最早出现的位置,这样一旦后面出现了这个sum就可以求得最长长度了。123456789101112131415161718192021222324class Solution {public int findMaxLength(int[] nums) {if (nums == null || nums.length == 0) {return 0;}for (int i = 0; i < nums.length; i++) {if (nums[i] == 0) {nums[i] = -1;}}Map<Integer, Integer> sum2Index = new HashMap<>();sum2Index.put(0, -1);int sum = 0, maxLen = 0;for (int i = 0; i < nums.length; i++) {sum += nums[i];if (sum2Index.containsKey(sum)) {maxLen = Math.max(maxLen, i - sum2Index.get(sum));} else {sum2Index.put(sum, i);}}return maxLen;}}
526. beautiful-arrangement
- 美丽安排指的是在数组中元素和下标之间有整除关系(谁除谁都行)。
- DFS即可,每次尝试不同元素。时间复杂度
O(n!)
.123456789101112131415161718192021222324class Solution {private int count = 0;public int countArrangement(int n) {if (n < 1) {return 0;}boolean[] used = new boolean[n + 1];dfs(1, n, used);return count;}private void dfs(int curr, int n, boolean[] used) {if (curr > n) {count++;return;}for (int i = 1; i <= n; i++) {if (!used[i] && (curr % i == 0 || i % curr == 0)) {used[i] = true;dfs(curr + 1, n, used);used[i] = false;}}}}
528. random-pick-with-weight
- 给一个只含有正整数的数组,表示该index所占的权重,实现一个随机生成器按照这个权重来取对应的索引。
- 利用一个累加数组,将所有权重累加起来。然后根据总和生成随机数,再利用二分查找看看这个数应该落在哪个权重和的区间内,就可以取对应的索引了。123456789101112131415161718192021222324252627282930313233343536class Solution {Random r;int[] sums;public Solution(int[] w) {r = new Random();sums = new int[w.length];sums[0] = w[0];for (int i = 1; i < w.length; i++) {sums[i] = sums[i - 1] + w[i]; // 累加数组}}public int pickIndex() {int target = r.nextInt(sums[sums.length - 1]) + 1; // 在1~最终累加和之间取随机数return binarySearchInsertPos(sums, target); // 找随机数所处的区间}private int binarySearchInsertPos(int[] nums, int target) {int left = -1, right = nums.length;while (right - left > 1) {int mid = left + (right - left) / 2;if (nums[mid] >= target) {right = mid;} else {left = mid;}}return right;}}/*** Your Solution object will be instantiated and called as such:* Solution obj = new Solution(w);* int param_1 = obj.pickIndex();*/
529. minesweeper
- 点击一个位置,根据是否有雷更新棋盘。如果是雷,直接改成叉叉并返回;如果没有点开并且周围八个相邻格子有雷,则改成雷的数目并返回;如果周围都没有雷,就扩散到相邻格子继续更新。
方法一:根据描述本身就蕴含了递归,所以自然想到DFS。
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354class Solution {public char[][] updateBoard(char[][] board, int[] click) {if (board == null || board.length == 0 || board[0].length == 0|| click == null || click.length < 2) {return new char [0][0];}int rowCount = board.length, colCount = board[0].length;int row = click[0], col = click[1];if (board[row][col] == 'B') { // 已经点开了就直接返回return board;}if (board[row][col] == 'M') { // 是雷就ggboard[row][col] = 'X';} else {int count = 0; // 计算雷的个数for (int i = -1; i < 2; i++) {int tempRow = row + i;if (tempRow < 0 || tempRow >= rowCount) {continue;}for (int j = -1; j < 2; j++) {int tempCol = col + j;if (tempCol < 0 || tempCol >= colCount) {continue;}if (board[tempRow][tempCol] == 'M') {count++;}}}if (count > 0) {board[row][col] = (char)('0' + count);} else {board[row][col] = 'B'; // 改为已经reveal,并扩散到周围unreveal的邻居for (int i = -1; i < 2; i++) {int tempRow = row + i;if (tempRow < 0 || tempRow >= rowCount) {continue;}for (int j = -1; j < 2; j++) {int tempCol = col + j;if (tempCol < 0 || tempCol >= colCount) {continue;}if (board[tempRow][tempCol] == 'E') { // DFS the unrevealedupdateBoard(board, new int[]{tempRow, tempCol});}}}}}return board;}}方法二:BFS,Queue中存放点击之后扩散的点(如果有的话),一直扩散直到Queue为空。但是和DFS需要区别的是,由于BFS会将相邻的所有E的格子都入队,很可能会出现重复,所以就直接在更新时就赋值为B,防止重复入队。
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657class Solution {public char[][] updateBoard(char[][] board, int[] click) {if (board == null || board.length == 0 || board[0].length == 0|| click == null || click.length < 2) {return new char [0][0];}Queue<int[]> q = new LinkedList<>();int rowCount = board.length, colCount = board[0].length;q.add(click);while (!q.isEmpty()) {int[] curr = q.poll();int row = curr[0], col = curr[1]; // 和DFS相比少了判断为B的步骤if (board[row][col] == 'M') {board[row][col] = 'X';} else {int count = 0;for (int i = -1; i < 2; i++) {int tempRow = row + i;if (tempRow < 0 || tempRow >= rowCount) {continue;}for (int j = -1; j < 2; j++) {int tempCol = col + j;if (tempCol < 0 || tempCol >= colCount) {continue;}if (board[tempRow][tempCol] == 'M') {count++;}}}if (count > 0) {board[row][col] = (char)('0' + count);} else {board[row][col] = 'B';for (int i = -1; i < 2; i++) {int tempRow = row + i;if (tempRow < 0 || tempRow >= rowCount) {continue;}for (int j = -1; j < 2; j++) {int tempCol = col + j;if (tempCol < 0 || tempCol >= colCount) {continue;}if (board[tempRow][tempCol] == 'E') { // BFS the unrevealedq.add(new int[]{tempRow, tempCol});board[tempRow][tempCol] = 'B'; // IMPORTANT!!!}}}}}}return board;}}
530. minimum-absolute-difference-in-bst
- 给一个BST,求任意两个节点之差的绝对值的最小值。
- BST的重要特性就是中序遍历能得到有序序列,而最小的差一定是相邻的两个节点。因此可以中序遍历的时候利用prev减一减。12345678910111213141516class Solution {private TreeNode prev = null;private int minDiff = Integer.MAX_VALUE;public int getMinimumDifference(TreeNode root) {if (root == null) {return Integer.MAX_VALUE;}getMinimumDifference(root.left);if (prev != null) {minDiff = Math.min(minDiff, root.val - prev.val);}prev = root;getMinimumDifference(root.right);return minDiff;}}
532. k-diff-pairs-in-an-array
- 给一个int数组,找其中unique对儿使得两个数的差的绝对值等于k,求对儿数。
- 方法一:用map来count每个数字出现的次数,如果是新出现的数字且map中有对应的另一半就累加。注意用map是因为要处理
k = 0
的情况。 - 方法二:排序后双指针,如果发现gap较大说明需要挪动左指针、gap过小则需要挪动右指针。需要注意处理左指针连续相同值的情况,这就需要连续挪动直到前后值不相等。123456789101112131415161718192021class Solution {public int findPairs(int[] nums, int k) {if (nums == null || nums.length == 0 || k < 0) {return 0;}Arrays.sort(nums);int left = 0, right = 0, count = 0;while (right < nums.length) {if (right <= left || nums[right] - nums[left] < k) { // 有可能左指针skip了很多相同的值right++;} else if ((left > 0 && nums[left] == nums[left - 1]) ||nums[right] - nums[left] > k) {left++;} else {left++;count++;}}return count;}}
535. encode-and-decode-tinyurl
- 实现一个含有encode和decode方法的类,能够转码和解码tinyURL。
- 这种映射关系必定需要Map,关键是如何建立这种从长到短的映射?利用随机数随机从一长串字符串中取字符,取够6个即形成了短码。如果这个短码已经存在,就需要再来一次生成新的短码,直到出现新的。12345678910111213141516171819202122232425262728293031323334353637public class Codec {private static final String HOST = "http://tinyurl.com/";private static final String CHAR = "abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";private static final int LIMIT = CHAR.length();Map<String, String> url2tiny = new HashMap<>();Map<String, String> tiny2url = new HashMap<>();// Encodes a URL to a shortened URL.public String encode(String longUrl) {if (url2tiny.containsKey(longUrl)) {return HOST + url2tiny.get(longUrl);}String ret = null;do {StringBuilder sb = new StringBuilder();for (int i = 0; i < 6; i++) {int index = (int) (Math.random() * LIMIT);sb.append(CHAR.charAt(index));}ret = sb.toString();} while (tiny2url.containsKey(ret));url2tiny.put(longUrl, ret);tiny2url.put(ret, longUrl);return HOST + ret;}// Decodes a shortened URL to its original URL.public String decode(String shortUrl) {return tiny2url.get(shortUrl.replace(HOST, ""));}}// Your Codec object will be instantiated and called as such:// Codec codec = new Codec();// codec.decode(codec.encode(url));
536. construct-binary-tree-from-string
- 给一个由数字、
-
、括号组成的字符串,将它还原成一个二叉树,注意空树由""
表示而不是()
。一开始完全无法理解-
是干啥的,原来只是用来表示负数,囧。 方法一:看到这种括号嵌套就想到利用stack,到
(
直接忽略,碰到数字或者-
则将这个value提取出来后,再看看栈顶节点是否已经有left/right,对应添加上去后再入栈,这样就可以保证后续的孩子节点能够继续连到当前节点,碰到)
则说明栈顶节点已经处理完毕,直接弹出即可。123456789101112131415161718192021222324252627282930public TreeNode str2tree(String s) {if (s == null || s.length() == 0) {return null;}Stack<TreeNode> stack = new Stack<>();for (int i = 0; i < s.length(); i++) {if (s.charAt(i) == '(') {continue;} else if (s.charAt(i) == ')') {stack.pop();} else {int start = i;while (i < s.length() &&(Character.isDigit(s.charAt(i)) || s.charAt(i) == '-')) {i++;}TreeNode curr = new TreeNode(Integer.valueOf(s.substring(start, i)));if (!stack.isEmpty()) {if (stack.peek().left == null) {stack.peek().left = curr;} else {stack.peek().right = curr;}}stack.push(curr);i--;}}return stack.pop();}方法二:更自然的是想到递归,但是这样就不是one-pass的,因为每次需要根据括号的情况找到后续节点对应的字符串,需要不断地count括号,比如这个。
123456789101112131415161718192021222324252627public TreeNode str2tree(String s) {if (s == null || s.length() == 0) {return null;}int i = 0;while (i < s.length() && (Character.isDigit(s.charAt(i)) || s.charAt(i) == '-')) {i++;}TreeNode root = new TreeNode(Integer.valueOf(s.substring(0, i++))); // 挪到下一个位置int count = 1, start = i;while (i < s.length() && count > 0) {if (s.charAt(i) == '(') {count++;} else if (s.charAt(i) == ')') {count--;}i++;}// 此时i指向字符末尾或者(if (start < i - 1) { // 取start到i-1部分作为前半部分root.left = str2tree(s.substring(start, i - 1));}if (i + 1 < s.length() - 1) { // 取(的后一个字符到)之前的字符root.right = str2tree(s.substring(i + 1, s.length() - 1));}return root;}
538. convert-bst-to-greater-tree
- 给一个BST,将每个节点的值加上BST中所有比他大的节点值,返回修改之后的greater树。
既然是BST,那么在中序遍历的时候就可以得到从小到大的序列了,因此使用一个全局的running sum从右往左求和即可滚雪球似的得到比当前节点大的所有值之和了。
12345678910111213class Solution {private int sum = 0;public TreeNode convertBST(TreeNode root) {if (root != null) {convertBST(root.right);sum += root.val;root.val = sum;convertBST(root.left);}return root;}}如果不能用全局变量,就考虑传入一个参数作为running sum。对于每一个节点,比他大的节点可以出现在右孩子,或者他的祖先(如果他本身是左孩子的话)。由于没有prev链接,因此在递归时需要直接传入他的祖先中比他大的值之和。
123456789101112131415class Solution {public TreeNode convertBST(TreeNode root) {dfs(root, 0);return root;}private int dfs(TreeNode root, int val) {if (root == null) {return val;}int right = dfs(root.right, val); // 对于右孩子来说,比它大的就是val,返回的就是所有比当前节点大的值之和int left = dfs(root.left, root.val + right); // 对于左孩子来说,比它大的除了上一步的right还有当前节点root.val = root.val + right;return left; // 返回的left包括了以当前节点为根的所有节点之和}}
539. minimum-time-difference
- 给一个表示时间的字符串数组,包含的字符串都是合法的
00:00 ~ 23:59
时间,求最小的两个时间之分钟差。注意23:59 + 1 = 00:00
,因此之差一分钟。 - 将全部都转为分钟,排个序之后相邻两个做对比.
540. single-element-in-a-sorted-array
- 给一个排好序的数组,每个数字都出现了两次except其中一个,求这个毒瘤。要求时间复杂度O(logN),空间(1).
- 二分查找,利用pair的特性,以偶数index与下一个元素为判断依据,若元素相等说明毒瘤出现在后半部分,若不想等说明出现在前面。edge case需要考虑单独元素出现在数组首位和末尾的情况。1234567891011121314151617181920class Solution {public int singleNonDuplicate(int[] nums) {if (nums == null || nums.length == 0) {return 0;}int left = 0, right = nums.length - 1;while (right > left) {int mid = left + (right - left) / 2;if (mid % 2 == 1) {mid--;}if (nums[mid] == nums[mid + 1]) {left = mid + 2;} else {right = mid;}}return nums[left];}}
543. diameter-of-binary-tree
- Zillow面试题刻骨铭心。给一个二叉树,求其中任意两个节点的path距离中最长长度。例如下面的树就有
4-2-1-3
和5-2-1-3
两个最长路径,都是3. - 对于当前节点有取和不取两种情况(但),取根则等于左深度加右深度,不取则在往下求深度的时候就可以顺便用一个全局变量去更新,时间复杂度就为O(N)了。1234567891011121314151617class Solution {private int max = 0;public int diameterOfBinaryTree(TreeNode root) {getMaxDepth(root);return max;}private int getMaxDepth(TreeNode root) {if (root == null) {return 0;}int left = getMaxDepth(root.left);int right = getMaxDepth(root.right);max = Math.max(max, left + right); // 取根的情况return Math.max(left, right) + 1; // 求深度}}
544. output-contest-matches
- 给一个整数n表示有多少支队伍,返回这些队伍的对决情况。例如n = 4时,返回
((1,4),(2,3))
. - 有点类似动态规划,n个格子存放当前的对阵情况,队伍减半后再继续套。12345678910111213141516171819class Solution {public String findContestMatch(int n) {if (n < 2) {return "";}String[] bucket = new String[n];for (int i = 0; i < n; i++) {bucket[i] = String.valueOf(i + 1); // 初始化每个队伍}while (n > 1) {for (int i = 0; i * 2 < n; i++) {bucket[i] = "(" + bucket[i] + "," + bucket[n - i - 1] + ")"; // 前后对应着取对阵情况}n /= 2; // 对半折后继续取}return bucket[0];}}
545. boundary-of-binary-tree
- 给一个二叉树,求它从左到下方到右的所有boundary的元素。
方法一:根据定义来写,则是老老实实先把所有左边界找出来、再把叶子节点找出来、再把右边界节点找出来。需要注意的是在遍历左边界时遵循preorder,当所有的左侧叶子节点都遍历完了再重新潜入一次找所有最下方节点(叶子节点),找完所有叶子结点之后再开始遍历右边界节点,遵循postorder.
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849class Solution {private List<Integer> nodes;public List<Integer> boundaryOfBinaryTree(TreeNode root) {nodes = new ArrayList<>();if (root == null) {return nodes;}nodes.add(root.val);traverseLeftBoundary(root.left);traverseLeaves(root.left);traverseLeaves(root.right);traverseRightBoundary(root.right);return nodes;}private void traverseLeftBoundary(TreeNode root) {if (root == null || root.left == null && root.right == null) {return;}nodes.add(root.val);if (root.left != null) {traverseLeftBoundary(root.left);} else {traverseLeftBoundary(root.right);}}private void traverseRightBoundary(TreeNode root) {if (root == null || root.left == null && root.right == null) {return;}if (root.right != null) {traverseRightBoundary(root.right);} else {traverseRightBoundary(root.left);}nodes.add(root.val);}private void traverseLeaves(TreeNode root) {if (root == null) {return;}if (root.left == null && root.right == null) {nodes.add(root.val);return;} else {traverseLeaves(root.left);traverseLeaves(root.right);}}}方法二:如何one-pass只遍历一次呢?在潜入的时候其实就已经知道节点的身份了:若当前节点是左边界,潜入左child时肯定还是左边界;右边界亦然。若当前节点是左边界,潜入右child时,若左child是null,则当前继续是左边界,也就是一个AND的关系。若当前节点是右边界,潜入左child时,若右child为null,则继续时右边界。
12345678910111213141516171819202122232425262728class Solution {public List<Integer> boundaryOfBinaryTree(TreeNode root) {List<Integer> ans = new ArrayList<>();if (root == null) {return ans;}ans.add(root.val);dfs(root.left, true, false, ans);dfs(root.right, false, true, ans);return ans;}private void dfs(TreeNode root, boolean isLeft, boolean isRight, List<Integer> ans) {if (root == null) {return;}if (isLeft) {ans.add(root.val);}dfs(root.left, isLeft, isRight && root.right == null, ans);if (!isLeft && !isRight && root.left == null && root.right == null) {ans.add(root.val);}dfs(root.right, isLeft && root.left == null, isRight, ans);if (isRight) {ans.add(root.val);}}}
547. friend-circles
- 给一个只含有0/1的二维矩阵,1表示row和col互为好友,求总共有多少个独立的朋友圈子。
- 经典的并查集。需要讨论的是时间复杂度。root数组相当于保存了N-ary的树,利用size可以保证每次union的时候都尽量保持树balance(size小的往大的merge),所以每次union相当于从最深处走到根,最大深度也就是log(N),所以时间复杂度是
log(N)
.TODO【并查集的复杂度分析】123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657class Solution {class UnionFind {private int count;private int[] root;private int[] size;public UnionFind(int N) {count = N;root = new int[N];size = new int[N];for (int i = 0; i < N; i++) {root[i] = i; // 根是它本身size[i] = 1; // 只有本身一个节点y9\h z}}public int find(int i) {while (root[i] != i) {root[i] = root[root[i]];i = root[i];}return i;}public void union(int i, int j) {int rootI = find(i);int rootJ = find(j);if (rootI == rootJ) {return;} else {if (size[rootI] < size[rootJ]) { // 小的往大的合并root[rootI] = root[rootJ];size[rootJ] += size[rootI]; // 大的就含有所有小的节点了} else {root[rootJ] = root[rootI];size[rootI] += size[rootJ];}count--;}}public int getCount() {return count;}}public int findCircleNum(int[][] M) {if (M == null || M.length == 0) {return 0;}int N = M.length;UnionFind uf = new UnionFind(N);for (int i = 0; i < N; i++) {for (int j = i + 1; j < N; j++) {if (M[i][j] == 1) {uf.union(i, j);}}}return uf.getCount();}}
548. split-array-with-equal-sum
- 给一个int数组,判断是否存在三个分割点i, j, k使得被这三个点分割出来的四个部分(不包含分割点)的sum相等。
方法一:类似于4sum,利用Set存储在之前出现过的相等的sum,然后固定中间点j,遍历可能的i,将前面两部分相等的sum存入set,然后遍历后半部分k,若也存在两部分相等的和且set中存在,则说明确实可以分成四个相等的部分。
123456789101112131415161718192021222324252627282930class Solution {public boolean splitArray(int[] nums) {if (nums == null || nums.length < 7) {return false;}int[] sum = new int[nums.length];sum[0] = nums[0];for (int i = 1; i < nums.length; i++) {sum[i] = sum[i - 1] + nums[i];}for (int j = 3; j + 3 < nums.length; j++) {Set<Integer> set = new HashSet<>();for (int i = 1; i + 1 < j; i++) {int sumBeforeI = sum[i - 1];int sumItoJ = sum[j - 1] - sum[i];if (sumBeforeI == sumItoJ) {set.add(sumBeforeI);}}for (int k = j + 1; k + 1 < nums.length; k++) {int sumJtoK = sum[k - 1] - sum[j];int sumAfterK = sum[nums.length - 1] - sum[k];if (sumJtoK == sumAfterK && set.contains(sumJtoK)) {return true;}}}return false;}}DFS也可破。枚举所有可能的part sum,然后dfs到后续部分判断是否可以根据这个part sum恰好得到四个部分。注意为了避免无效DFS,需要ignore连续出现的0。
1234567891011121314151617181920212223242526272829class Solution {public boolean splitArray(int[] nums) {if (nums == null || nums.length < 7) {return false;}int sum = IntStream.of(nums).sum();return dfs(nums, 0, 0, sum, 0);}private boolean dfs(int[] nums, int start, int target, int sumRemain, int depth) {if (depth == 3) {if (target == sumRemain)return true;elsereturn false;}int currSum = 0;for (int i = start + 1; i + 5 - (2 * depth) < nums.length; i++) {if (i != 1 && nums[i - 1] == 0 && nums[i] == 0) { // 忽略连续出现的0continue;}currSum += nums[i - 1];if ((depth == 0 || currSum == target)&& dfs(nums, i + 1, currSum, sumRemain - currSum - nums[i], depth + 1)) {return true;}}return false;}}
549. binary-tree-longest-consecutive-sequence-ii
- 给一个二叉树,求其中路径最长的连续increasing或decreasing的长度,这个路径不一定是parent-child的,怎么上下左右都行,只要是连续即可。
- 不论怎么弯折,对于每一个节点来说其实就是看和孩子能否形成increase和decrease,可以则加一下就是一个弯折的路径了。1234567891011121314151617181920212223242526272829303132333435class Solution {int max = 0;public int longestConsecutive(TreeNode root) {if (root == null) {return 0;}helper(root);return max;}private int[] helper(TreeNode root) {if (root == null) {return new int[] {0, 0};}int[] left = helper(root.left), right = helper(root.right);int inc = 1, dec = 1;if (root.left != null) {if (root.left.val == root.val + 1) { // 确认是否能和孩子形成升序/降序列inc = left[0] + 1;}if (root.left.val == root.val - 1) {dec = left[1] + 1;}}if (root.right != null) {if (root.right.val == root.val + 1) {inc = Math.max(inc, right[0] + 1); // 取左右中最大的}if (root.right.val == root.val - 1) {dec = Math.max(dec, right[1] + 1);}}max = Math.max(inc + dec - 1, max);return new int[] {inc, dec};}}
551. student-attendance-record-i
- 给一个字符串,若连续出现2次以上
L
、总共出现1次以上A
则返回false。pass.
554. brick-wall
- 给一个二维List表示每一行每一块砖各自的长度。求纵向切下来最少穿过的墙的个数。
- 最少的穿过的个数也就是求穿过最多缝隙的个数,也就事给定一个结束长度,最多的那个即为所求。用Map记录每一行的累加的结尾长度,不断更新最多的那个长度,最后wall的行数减去count即可。123456789101112131415161718192021class Solution {public int leastBricks(List<List<Integer>> wall) {if (wall == null || wall.size() == 0) {return 0;}Map<Integer, Integer> map = new HashMap<>();int count = 0;for (List<Integer> list : wall) {int len = 0;for (int i = 0; i < list.size() - 1; i++) {len += list.get(i);map.put(len, map.getOrDefault(len, 0) + 1); // 求对应长度结尾的墙有多少count = Math.max(count, map.get(len)); // 更新以len结尾的最多的if (count == wall.size()) {return 0;}}}return wall.size() - count; // 减一下就是穿过墙最少的}}
556. next-greater-element-iii
- 给一个32bit的正整数,求十进制中相同数字组成的下一个更大值。例如
12
的下一个就是21
,1342
则是1423
。 - 若已经是最大的可能值?(返回-1)
- 对输入的数字从后往前遍历,找到连续的两个数字使得前面的(
i - 1
)小于后面的(i
),然后再从i + 1
开始往后找最小的大于i - 1
的数字,互换他俩,最后从i开始往后从小到大排个序即可。1234567891011121314151617181920212223242526272829303132333435class Solution {public int nextGreaterElement(int n) {if (n < 1) {return -1;}String s = String.valueOf(n);char[] sChar = s.toCharArray();int i = sChar.length - 1; // 从后往前while (i > 0) {if (sChar[i - 1] < sChar[i]) { // 找到第一个连续的顺序对break;}i--;}if (i == 0) { // 到最后都没找到return -1;}int j = i + 1, minIndex = i;char first = sChar[i - 1];while (j < sChar.length) { // 从i + 1开始从前往后不断更新最小的大于i - 1的元素if (sChar[j] <= sChar[minIndex] && sChar[j] > first) {minIndex = j;}j++;}sChar[i - 1] = sChar[minIndex]; // 交换该ceiling值和i - 1sChar[minIndex] = first;Arrays.sort(sChar, i, sChar.length); // 从i开始拍个序long ans = Long.valueOf(new String(sChar));return ans > Integer.MAX_VALUE? -1: (int)ans;}}
559. maximum-depth-of-n-ary-tree
- 给一个多叉树,求最深的深度。DFS/BSF均可,pass。
560. subarray-sum-equals-k
- 给一个int数组和一个k,问有多少连续的subarray之和等于k。这些int都在
[-1000, 1000]
,数组长度最多20000。 - 一开始想用双指针,但有正有负,更新条件不好搞。正解是使用Map,在计算sum的时候顺便看看之前是否出现过sum - k。这其实和path sum III 很像,都是利用prefix sum.123456789101112131415161718class Solution {public int subarraySum(int[] nums, int k) {if (nums == null || nums.length == 0) {return 0;}Map<Integer, Integer> map = new HashMap<>();map.put(0, 1);int count = 0, sum = 0;for (int i = 0; i < nums.length; i++) {sum += nums[i];if (map.containsKey(sum - k)) { // 看之前是否已经出现了count += map.get(sum - k);}map.put(sum, map.getOrDefault(sum, 0) + 1);}return count;}}
562. longest-line-of-consecutive-one-in-matrix
- 给一个只含有0和1的二维数组,求横、竖、两个斜对角的连续出现1的最长长度。
方法一:O(N^2)从每个点出发往四个方向分别遍历,求最长长度。
123456789101112131415161718192021222324252627282930313233343536373839class Solution {// O(N^3):矩阵遍历每个点是N^2,对每个点在扫四个方向是4Npublic int longestLine(int[][] M) {if (M == null || M.length == 0 || M[0].length == 0) {return 0;}int longest = 0;for (int i = 0; i < M.length; i++) {for (int j = 0; j < M[0].length; j++) {if (M[i][j] == 1) {longest = Math.max(longest, getLongest(M, i, j));}}}return longest;}// 右、下、右下、左下final private int[][] directions = new int[][] {{0, 1}, {1, 0}, {1, 1}, {1, -1}};private int getLongest(int[][] M, int row, int col) {int maxLen = 1;for (int[] direction: directions) {int len = 1;int newRow = row + direction[0];int newCol = col + direction[1];// 持续在一个方向上继续走while (isValidPosition(M, newRow, newCol) && M[newRow][newCol] == 1) {len++;newRow += direction[0];newCol += direction[1];}maxLen = Math.max(maxLen, len);}return maxLen;}private boolean isValidPosition(int[][] M, int row, int col) {return row >= 0 && col >= 0 &&row < M.length && col < M[0].length;}}方法二:类似于DP,记录下四个方向各自的最大长度。参考这个被lz
和N皇后问题很像。1234567891011121314151617181920212223242526272829303132333435class Solution {// O(N^2)时间,O(M+N)空间public int longestLine(int[][] M) {// validationif (M == null || M.length == 0 || M[0].length == 0) {return 0;}int rows = M.length, cols = M[0].length;int longest = 0;int[] bucketCol = new int [cols];int[] bucketDiag1 = new int [rows + cols];int[] bucketDiag2 = new int [rows + cols];for (int i = 0; i < rows; i++) {int row = 0; // 新行初始化为0for (int j = 0; j < cols; j++) {if (M[i][j] == 1) { // 当前为1,对应更新bucketrow++;bucketCol[j]++;bucketDiag1[j + i]++;bucketDiag2[j - i + M.length]++;longest = Math.max(longest, row);longest = Math.max(longest, bucketCol[j]);longest = Math.max(longest, bucketDiag1[j + i]);longest = Math.max(longest, bucketDiag2[j - i + M.length]);} else {row = 0;bucketCol[j] = 0;bucketDiag1[j + i] = 0;bucketDiag2[j - i + M.length] = 0;}}}return longest;}}
563. binary-tree-tilt
- 给一个二叉树,求每一个节点的左右子树和的差的绝对值之和。
- 直接后序遍历求sum搞定。pass。
565. array-nesting
- 给一个长度为N的int数组,其中元素为
0 ~ N-1
。从nums[i]
开始往后跳,nums[nums[i]]
持续跳直到回到原处。求所有这些自循环链接中的最长长度。 - 方法一:利用visited数组,模拟跳动。
- 方法二:不需要额外数组,利用
0 ~ N-1
这个条件直接用一些trick来重复使用input array. 常见手段包括:- Negating: 既然都是正数,不妨用负数表示visited。
- Modulo: 加上n来表示visited,最后
% N
即可得到原值。 - Sorting: 将
nums[i]
放到i
处,但这里就不适用了。
566. reshape-the-matrix
- 给一个二维数组,返回reshape之后的。pass.
567. permutation-in-string
- 给两个字符串,判断s1的permutation是否包含在s2中,例如
s1 = "ab" s2 = "eidbaooo"
返回true,因为s2包含了ba
。 - 其实这个s1的permutation并不用真的一个个求出来,在意的只是s1的每个字符及其出现次数,因此用一个map O(N)扫一波就好了。然后就对s2进行双指针 + producer/consumer操作,如果消耗完map中所有字符的时候恰好前后指针间距等于s1的长度,说明就是permutation的一种了,返回true。123456789101112131415161718192021222324252627282930313233343536373839class Solution {public boolean checkInclusion(String s1, String s2) {if (s1 == null || s2 == null || s1.length() == 0 || s2.length() == 0 ) {return false;}char[] s1Char = s1.toCharArray();Map<Character, Integer> map = new HashMap<>();int len1 = s1Char.length;for (int i = 0; i < len1; i++) { // 统计s1中每个字母出现个数,作为producermap.put(s1Char[i], map.getOrDefault(s1Char[i], 0) + 1);}char[] s2Char = s2.toCharArray();int count = map.size();int left = 0, right = 0;while (right < s2Char.length) { // 遍历s2作为consumer消耗字符,直到map中所有字符消耗完if (map.containsKey(s2Char[right])) {map.put(s2Char[right], map.get(s2Char[right]) - 1);if (map.get(s2Char[right]) == 0) {count--;}}while (count == 0) { // 左指针补回来,直到map中出现available的字符if (right - left + 1 == len1) {return true;}if (map.containsKey(s2Char[left])) {map.put(s2Char[left], map.get(s2Char[left]) + 1);if (map.get(s2Char[left]) > 0) {count++;}}left++;}right++;}return false;}}
572. subtree-of-another-tree
- 给两棵树,判断后者是不是前者的子树。子树指的是还有一个任意节点为根的子树,结构、数值完全一样。
- 递归搞定。先判断两个树是否相同,若相同直接就是子树了。若不同则需要到左右子树进行递归,判断t是否是左/右的子树。1234567891011121314151617181920class Solution {public boolean isSubtree(TreeNode s, TreeNode t) {if (s == null && t == null) {return true;}if (s == null || t == null) {return false;}return isSame(s, t) ? true : isSubtree(s.left, t) || isSubtree(s.right, t);}private boolean isSame(TreeNode s, TreeNode t) {if (s == null && t == null) {return true;}if (s == null || t == null) {return false;}return s.val == t.val ? isSame(s.left, t.left) && isSame(s.right, t.right) : false;}}
575. distribute-candies
- 给一个数组表示糖果的id,其中可能有重复,且糖果数量一定是偶数。要求将其分成两部分,问如果想尝最多的不同的糖果,有多少种。例如
[3,2,2,1,3,1]
就有三种,[7,3,1,4,3,7,4,3,7]
就有四种。要求时间复杂度最差O(N*lgN)
,空间复杂度O(N)。 - 先排序,然后双指针一个从前往后,一个从后往前。left指针负责将糖果存入set,right则是调取后方的糖果往前替换,当left发现当前糖果吃过了,就从right那里swap过来继续判断,直到出现新的糖果或者穷尽。123456789101112131415161718192021private static int maxCandy(int[] candies) {if (candies == null || candies.length == 0) {return 0;}Arrays.sort(candies);Set<Integer> set = new HashSet<>();int half = candies.length / 2;int left = 0, right = candies.length - 1;while (left < half) {while (set.contains(candies[left]) && right >= half) {swap(candies, left, right--);}if (right < half) {break;} else {set.add(candies[left++]);}}set.add(candies[left]);return set.size();}
581. shortest-unsorted-continuous-subarray
- 给一个部分部分有序的数组,求将其中哪一部分排序之后整个数组就都有序了,求最短的区间的长度。
- 首先从左往右逆序对,然后从右往左找逆序对。这样就有了一个大致区间,但是还需要找区间内的min和max,分别往前和往后遍历看看是否真的完全符合,否则还需要扩展区间。例如
[1,3,6,4,8,2,7,10]
在前两步之后找到得失[6,4]
和[8,2]
实际上前面的3和后面的1都需要加入进来。12345678910111213141516171819202122232425262728293031323334353637383940class Solution {public int findUnsortedSubarray(int[] nums) {if (nums == null || nums.length == 0) {return 0;}// 找第一个逆序对int left = 1;while (left < nums.length) {if (nums[left - 1] > nums[left]) {left--;break;}left++;}if (left == nums.length) { // 说明已经有序了return 0;}int right = nums.length - 1;while (right > 0) {if (nums[right - 1] > nums[right]) {break;}right--;}// 找[left, right]之间的最小、最大int min = Integer.MAX_VALUE, max = Integer.MIN_VALUE;for (int i = left; i <= right; i++) {min = Math.min(min, nums[i]);max = Math.max(max, nums[i]);}// 前面的必须小于min,后面的必须大于max,否则扩散while (left > 0 && nums[left - 1] > min) {left--;}while (right + 1 < nums.length && nums[right + 1] < max) {right++;}return right - left + 1;}}
582. kill-process
- 给两个pid的int list,分别表示进程id和parent进程id。给一个id,从它出发找出所有后续pid,返回所有被杀掉的进程id。
- 直接map表示
pid -> list of child id
,然后递归/迭代找后续pid即可。pass.
594. longest-harmonious-subsequence
- 给一个int数组,求其中最大和最小值恰好为1的非连续子序列。
- 用Map存每个值出现的次数,然后遍历Map取key相差1都存在的进行更新。123456789101112131415161718class Solution {public int findLHS(int[] nums) {if (nums == null || nums.length == 0) {return 0;}Map<Integer, Integer> map = new HashMap<>();for (int num : nums) {map.put(num, map.getOrDefault(num, 0) + 1);}int max = 0;for (int num : map.keySet()) {if (map.containsKey(num + 1)) {max = Math.max(max, map.get(num) + map.get(num + 1));}}return max;}}
598. range-addition-ii
- 给一个二维数组的规模m和n,初始值为0,再给一个ops二维数组,每个op表示前x行和前y列都加1.求最终最大值的个数。例如
m = 3, n = 3, operations = [[2,2],[3,3]]
,那么就是先给左上方2*2
加1,再给3*3
加1,最后就有4个最大值(2)。 - 直接找行和列的最小值,相乘即可。m和n甚至都没啥用。123456789101112131415class Solution {// 直接找最小的行和列数,相乘即得public int maxCount(int m, int n, int[][] ops) {if (ops == null || ops.length == 0) {return m * n;}int rowMin = Integer.MAX_VALUE, colMin = Integer.MAX_VALUE;for (int[] op : ops) {rowMin = Math.min(op[0], rowMin);colMin = Math.min(op[1], colMin);}return rowMin * colMin;}}
600. non-negative-integers-without-consecutive-ones
- 给一个正数num,求[0, num]之间的bitString不含连续1的数字的个数。
方法一:DP。对于bitString有两种可能,以1结尾或以0结尾。为了不出现连续的1,对于0结尾的数字可以拼上0或1,而对于1结尾的数字就只能拼上0.Geeks4Geeks上面给的是bitString的长度,这里也可以用类似的方法。不过由于这里num的存在可能需要砍掉一部分结果,因此在最后需要多一步判断。如果num中出现了连续的
xx00xxx
(注意是从右往左遍历),而如果没有这个限制xx10xxx, xx01xxx
等都可能算进去,因此需要减去前一个0到最前面的数字位数所保存的endWithOne的值。1234567891011121314151617181920212223242526272829303132333435class Solution {// 对于一个二进制数字,最后一位可能为1也可能为0.后者可以append数字0或1,而前者只能append数字0了。牵涉到DP了。// end0表示不含连续1的、bit长度为i + 1的、以0结尾的数字个数,end1表示不含连续1的、bit长度为i + 1的、以1结尾的数字的个数// 初始状态为end0[0] = 1, end1[0] = 1.// end0只在最后拼0,因此上一步来自0或1都可以;而end1只拼1,因此上一步只能来自0.// 状态转换为end0[i] = end0[i - 1] + end1[i - 1], end1[i] = end0[i - 1]// 但这是根据bitString长度来的,而题目给的是num,因此最后还需要过滤看看有没有多算// 如果在i, i+1处出现连续的0,说明i往前的部分就多算了,因此根据0~i-1的长度找endWithOne减掉即可。public int findIntegers(int num) {if (num <= 0) {return 0;}StringBuilder sb = new StringBuilder(Integer.toBinaryString(num));int n = sb.length();int[] endWithZero = new int [n];int[] endWithOne = new int [n];endWithZero[0] = 1;endWithOne[0] = 1;for (int i = 1; i < n; i++) {endWithZero[i] = endWithZero[i - 1] + endWithOne[i - 1];endWithOne[i] = endWithZero[i - 1];}int count = endWithZero[n - 1] + endWithOne[n - 1];for (int i = 1; i < n; i++) {if (sb.charAt(i) == '1' && sb.charAt(i - 1) == '1') { // x11xx说明已经是最大了,后面都不可能多算了break;} else if (sb.charAt(i) == '0' && sb.charAt(i - 1) == '0') { // x00xx说明前一个0处不应该含有endWithOnecount -= endWithOne[n - 1 - i];}}return count;}}方法二:constant space的DP。
604. design-compressed-string-iterator
- 给一个字符加数字组成的字符串,实现next、hasNext函数遍历这个字符串。例如
L20J3B8
这样。 - 一开始直接根据频数全部存入queue,后来发现如果某个数频数特别大,而实际用不到那么多项就很不划算。因此就自定义类来搞了。写出来是这样.
605. can-place-flowers
- 给一个只含有0和1的数组,1表示该处被种了花,0表示可以种,同时相邻的位置不能同时种花。给一个花数n,判断能否种到所给的花池中。
方法一:想到了greedy的方法,每次判断0前后是否都为0,可以就直接设为1.
123456789101112131415161718class Solution {public boolean canPlaceFlowers(int[] flowerbed, int n) {if (flowerbed == null || flowerbed.length == 0 || n == 0) {return true;}for (int i = 0; i < flowerbed.length; i++) {if (flowerbed[i] == 0&& (i == flowerbed.length - 1 || flowerbed[i + 1] != 1) // 后一个为0&& (i == 0 || flowerbed[i - 1] != 1)) { // 前一个为0flowerbed[i] = 1;if (--n == 0) {return true;}}}return false;}}方法二:计算0的个数,碰到1就计算前面最多可以放多少0,同时重置计数。注意初始化count = 1,比如
0 0 1
在碰到1的时候需要保证slot为1,如果count初始化为0就无法得到了。123456789101112131415161718class Solution {public boolean canPlaceFlowers(int[] flowerbed, int n) {if (flowerbed == null || flowerbed.length == 0 || n == 0) {return true;}int count = 1, slot = 0;for (int i = 0; i < flowerbed.length; i++) {if (flowerbed[i] == 0) {count++;} else {slot += (count - 1) / 2;count = 0;}}slot += count / 2;return slot >= n;}}
606. construct-string-from-binary-tree
- 给一个二叉树,encode成括号分割的字符串。
- 递归拼接。注意左子树和右子树为空时加不加括号需要加判断。12345678910111213141516171819202122class Solution {public String tree2str(TreeNode t) {if (t == null) {return "";}StringBuilder sb = new StringBuilder();sb.append(t.val);String left = tree2str(t.left);String right = tree2str(t.right);if (left.length() > 0 || right.length() > 0) {sb.append("(");sb.append(left);sb.append(")");}if (right.length() > 0) {sb.append("(");sb.append(right);sb.append(")");}return sb.toString();}}
609. find-duplicate-file-in-system
- 给一个字符串数组,每个字符串中表示某路径下的所有文件以及内容,求相同文件内容的文件路径并存入List。比较有意思的是follow-up,解答参考这里:在真实的文件系统中你会选择BFS还是DFS?(BFS。虽然会消耗更多空间,但是可以利用locality提速)如果每个文件内容非常巨大怎么办?(不直接hash文件内容,而是首先根据文件大小判断是否是同一个文件,然后再取文件其中一部分进行hash)
- 直接以内容为key、路径&文件名为value存入map。最坏时间复杂度是O(N^2 * k),N是文件个数,k是文件大小。123456789101112131415161718192021222324252627282930313233class Solution {public List<List<String>> findDuplicate(String[] paths) {List<List<String>> ans = new ArrayList<>();if (paths == null || paths.length == 0) {return ans;}Map<String, List<String>> map = new HashMap<>(); // 以文件内容为key、路径+文件名为valuefor (String path : paths) {String[] arr = path.split("\\s+"); // 不可以直接用空格,要正则表达式!!!!!!String dir = arr[0];for (int i = 1; i < arr.length; i++) {int start = arr[i].indexOf("(");String file = arr[i].substring(0, start);String content = arr[i].substring(start, arr[i].length() - 1);List<String> temp = map.get(content);if (temp == null) {temp = new ArrayList<String>();}temp.add(dir + "/" + file);map.put(content, temp);}}for (String content : map.keySet()) {List<String> temp = map.get(content);if (temp.size() > 1) {ans.add(temp);}}return ans;}}
611. valid-triangle-number
- 给一个int数组表示边长,问这些边可以组成多少个三角形。(这些边可能重复,但是算作不同的边)
- 三角形任意两边之和大于第三边,这个任意其实指的是起码较小的两边之和大于最大边。12345678910111213141516171819202122class Solution {// 两边之和大于第三边,那么每次都取最小都两个边相加大于最大边即可public int triangleNumber(int[] nums) {if (nums == null || nums.length == 0) {return 0;}Arrays.sort(nums); // 排序int count = 0;for (int i = nums.length - 1; i >= 2; i--) {int left = 0, right = i - 1; // 双指针取比当前指针小的两条边while (left < right) {if (nums[left] + nums[right] > nums[i]) {count += (right - left); // 相当于固定right取left开始的边right --;} else {left ++;}}}return count;}}
616. add-bold-tag-in-string
- 给一个字符串s和一个字符串数组dict,若dict中的词在s中出现则需要加粗,例如
abc
加粗成<b>abc</b>
。 方法一:近似于暴力的做法,s逐位往后挪,对于每一个dict中的词都取出来用startsWith判断,用一个boolean数组标记是否加粗。
12345678910111213141516171819202122232425262728293031323334class Solution {public String addBoldTag(String s, String[] dict) {if (s == null || s.length() == 0|| dict == null || dict.length == 0) {return s;}boolean[] isBold = new boolean[s.length()];for (int i = 0, end = 0; i < s.length(); i++) {for (String word : dict) {if (s.startsWith(word, i)) {end = Math.max(end, i + word.length());}}if (i < end) {isBold[i] = true;}}StringBuilder sb = new StringBuilder();int i = 0;while (i < isBold.length) {if (isBold[i]) {sb.append("<b>").append(s.charAt(i++));while (i < isBold.length && isBold[i]) {sb.append(s.charAt(i++));}sb.append("</b>");} else {sb.append(s.charAt(i++));}}return sb.toString();}}方法二:直接使用indexOf将所有词出现的位置标出来,同样用一个boolean数组标记。
12345678910111213141516171819202122232425262728293031323334353637class Solution {public String addBoldTag(String s, String[] dict) {if (s == null || s.length() == 0|| dict == null || dict.length == 0) {return s;}boolean[] isBold = new boolean[s.length()];for (String word : dict) {int index = s.indexOf(word);while (index >= 0) {fill(isBold, index, index + word.length());index = s.indexOf(word, index + 1);}}StringBuilder sb = new StringBuilder();int i = 0;while (i < isBold.length) {if (isBold[i]) {sb.append("<b>").append(s.charAt(i++));while (i < isBold.length && isBold[i]) {sb.append(s.charAt(i++));}sb.append("</b>");} else {sb.append(s.charAt(i++));}}return sb.toString();}private void fill(boolean[] isBold, int start, int end) {while (start < end) {isBold[start++] = true;}}}方法二:首先将所有dict中出现的位置找出来,用Interval表示,然后就转化成了merge intervals的问题。
621. task-scheduler
- 给一个char数组,每个char表示一个task的名字;然后给一个interval表示相同的task必须经过这么多时间之后才能再次执行。求执行完所有任务所需要的时间。
- greedy可解,即以所给的interval作为周期,每次按频数从多到少地放task,如果周期没有用完而后续还有任务则需要把idle的时间也加进去。12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455class Solution {class Task {char name;int freq;public Task(char name, int freq) {this.name = name;this.freq = freq;}}public int leastInterval(char[] tasks, int n) {if (tasks == null || tasks.length == 0) {return 0;}// 统计每个任务的频数Map<Character, Task> map = new HashMap<>();for (char c : tasks) {if (!map.containsKey(c)) {map.put(c, new Task(c, 1));} else {Task t = map.get(c);t.freq++;map.put(c, t);}}// 根据频数维护优先队列PriorityQueue<Task> q = new PriorityQueue<>((a, b) -> {return b.freq == a.freq? a.name - b.name : b.freq - a.freq; // freq大的在前});for (Character c : map.keySet()) {q.add(map.get(c));}// 以n+1为周期,从pq中根据freq从高到低取taskint totalTime = 0;while (!q.isEmpty()) {List<Task> temp = new ArrayList<>();int period = n + 1;while (period > 0 && !q.isEmpty()) {Task t = q.poll();t.freq--;temp.add(t);period--;totalTime++;}for (Task t : temp) {if (t.freq > 0) {q.add(t);}}if (!q.isEmpty()) { // 后面还有task,确实需要隔多这么多时间totalTime += period;}}return totalTime;}}
[622. design-circular-queue] (https://leetcode.com/problems/design-circular-queue/)
- 设计并实现一个循环队列,给定初始长度为k,根据操作(enqueue/dequeue)成功与否返回boolean.
- 其实不难,只是需要想清楚逻辑。将数组想像成无限长的,利用头尾两个index和len,每次enqueueu挪动尾指针(初始为-1)、dequeue挪动头指针。
623. add-one-row-to-tree
- 给一个二叉树,root深度为1,给一个value和深度d,在d处插入值为value的节点,继承原先的左、右子树。返回插入完成后的root。
- 有三种方法。一是直接BFS层级遍历,在
d - 1
层停下来插入。二是DFS并利用currDepth来跟踪当前节点的深度从而决定是否插入。三是无需额外递归函数的DFS,通过直接修改传入的d
值来决定是否到达了需要插入的层数,并巧妙利用d = 1或0来决定插入左边还是右边。12345678910111213141516171819class Solution {public TreeNode addOneRow(TreeNode root, int v, int d) {if (d < 2) {TreeNode newNode = new TreeNode(v);if (d == 0) {newNode.right = root;} else {newNode.left = root;}return newNode;}if (root == null) {return null;}root.left = addOneRow(root.left, v, d - 1);root.right = addOneRow(root.right, v, d == 2 ? 0 : d - 1);return root;}}
624. design-search-autocomplete-system
- 实现一个搜索框按照历史搜索频数排序的自动推荐搜索词条系统。初始化时会提供一串历史搜索词条和对应频数,每次用户输入一个字符就需要看历史中是否出现过以此为开头的,按照出现频数返回前三个词条。当用户输入
#
时表示输入结束,将当前的输入词条补充到频数统计中。输入的只有小写字母和空格。 方法一:传统的bucket + Map方法。根据第一个字母划分成26个buckets,首先将最开始给出的词条存入对应bucket的map中。用户没输入一个字符就到对应的bucket中取map,看看是否有key是以当前输入的字符串开头的,将所有符合的词条排序返回。初始化时间复杂度为
O(k*l + 26)
,其中k为词条平均长度、总共有l个不同的词条,这里主要是消耗在对各个词条字符串计算哈希值。input时间复杂度为O(s + mlogm)
,其中s为bucket中取出map的规模、m符合以当前输入开头的key的个数,需要排序。12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455class AutocompleteSystem {class Node {String str;int count;Node(String str, int count) {this.str = str;this.count = count;}}private StringBuilder sb;private Map<String, Integer>[] bucket;private final int LIMIT = 3;public AutocompleteSystem(String[] sentences, int[] times) {if (sentences == null || times == null ||sentences.length == 0 || times.length == 0 ||sentences.length != times.length) {return;}sb = new StringBuilder();bucket = new HashMap[26];for (int i = 0; i < bucket.length; i++) {bucket[i] = new HashMap<>();}for (int i = 0; i < sentences.length; i++) {int index = sentences[i].charAt(0) - 'a';bucket[index].put(sentences[i], times[i]);}}public List<String> input(char c) {List<String> retVal = new ArrayList<>();if (c == '#') {String query = sb.toString();int index = query.charAt(0) - 'a';bucket[index].put(query, bucket[index].getOrDefault(query, 0) + 1);sb.setLength(0);} else {List<Node> nodeList = new ArrayList<>();sb.append(c);int index = sb.charAt(0) - 'a';for (String key : bucket[index].keySet()) {if (key.startsWith(sb.toString())) {nodeList.add(new Node(key, bucket[index].get(key)));}}Collections.sort(nodeList, (a, b) -> a.count == b.count ? a.str.compareTo(b.str) : b.count - a.count);for (int i = 0; i < Math.min(LIMIT, nodeList.size()); i++) {retVal.add(nodeList.get(i).str);}}return retVal;}}方法二:既然都是小写字母+空格,完全可以用Trie来实现搜索。对于每个Trie节点存放后续节点数组,共27个(a-z加上空格)。与一般Trie搜索不同的是,只要在traverse过程中碰到节点的count不为0,那么经过路径所拼接成的string就是一个合法的词条了,需要加到返回的list中。假设l个词条、平均长度为k,初始化时间复杂度为
O(k*l)
;input时假设当前已输入的长度为p、trie有效后续节点为q,时间复杂度为O(p + q + mlogm)
.12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091class AutocompleteSystem {class Node {String str;int count;Node(String str, int count) {this.str = str;this.count = count;}}class Trie {int count = 0;Trie[] branches = new Trie[27];}private int getIndex(char c) {return c == ' ' ? 26 : c - 'a';}private void insert(Trie t, String s, int count) {for (int i = 0; i < s.length(); i++) {int index = getIndex(s.charAt(i));if (t.branches[index] == null) {t.branches[index] = new Trie();}t = t.branches[index];}t.count += count;}private List<Node> search(Trie t, String s) {List<Node> nodeList = new ArrayList<>();for (int i = 0; i < s.length(); i++) {int index = getIndex(s.charAt(i));if (t.branches[index] == null) {return nodeList;}t = t.branches[index];}collectCount(s, t, nodeList);return nodeList;}private void collectCount(String s, Trie t, List<Node> nodeList) {if (t.count > 0) {nodeList.add(new Node(s, t.count));}for (char c = 'a'; c <= 'z'; c++) {int index = getIndex(c);if (t.branches[index] != null) {collectCount(s + c, t.branches[index], nodeList);}}if (t.branches[26] != null) {collectCount(s + ' ', t.branches[26], nodeList);}}private StringBuilder sb;private Trie root;private final int LIMIT = 3;public AutocompleteSystem(String[] sentences, int[] times) {if (sentences == null || times == null ||sentences.length == 0 || times.length == 0 ||sentences.length != times.length) {return;}root = new Trie();sb = new StringBuilder();for (int i = 0; i < sentences.length; i++) {insert(root, sentences[i], times[i]);}}public List<String> input(char c) {List<String> retVal = new ArrayList<>();if (c == '#') {insert(root, sb.toString(), 1);sb.setLength(0);} else {sb.append(c);List<Node> nodeList = search(root, sb.toString());Collections.sort(nodeList, (a, b) -> a.count == b.count ? a.str.compareTo(b.str) : b.count - a.count);for (int i = 0; i < Math.min(3, nodeList.size()); i++) {retVal.add(nodeList.get(i).str);}}return retVal;}}
628. maximum-product-of-three-numbers
- 给一个int数组,求其中任意三个数的最大乘积(不用考虑越界问题)。
- naive的想法是排序后取三个max,但实际上只需要用到三个max以及两个可能为负数的min。12345678910111213141516171819202122232425262728class Solution {public int maximumProduct(int[] nums) {if (nums == null || nums.length == 0) {return 0;}int max1 = Integer.MIN_VALUE, max2 = Integer.MIN_VALUE, max3 = Integer.MIN_VALUE,min1 = Integer.MAX_VALUE, min2 = Integer.MAX_VALUE;for (int num : nums) {if (num <= min1) {min2 = min1;min1 = num;} else if (num <= min2) {min2 = num;}if (num >= max1) {max3 = max2;max2 = max1;max1 = num;} else if (num >= max2) {max3 = max2;max2 = num;} else if (num >= max3) {max3 = num;}}return Math.max(max1 * max2 * max3, max1 * min1 * min2);}}
631. design-excel-sum-formula
- 实现一个excel类,能够set、get、sum,单元格如果修改了对应的sum也要更新。
根据需求确定实现方式。假如是read heavy,那就需要让get最快,牺牲set和sum的时间。将单元格之间的关系想像成subscribe的关系,单元格永远保持在最新状态,set时更新所有包含它的sum所处的单元格,同时取消旧的sum subscription;sum的时候就是subscribe到对应的单元格中。subscribe关系通过两个map实现。
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899class Excel {Map<String, Map<String, Integer>> subscriberMap;Map<String, Set<String>> subscribeeMap;int[][] boards;public Excel(int H, char W) {boards = new int[H][W - 'A' + 1];subscriberMap = new HashMap<>();subscribeeMap = new HashMap<>();}public void set(int r, char c, int v) {int[] coordinate = decode(c + "" + r);set(coordinate[0], coordinate[1], v);}private String encode(int row, int col) {return encode(new int[] {row, col});}private String encode(int[] coordinate) {return (char)(coordinate[1] + 'A') + "" + (coordinate[0] + 1);}private int[] decode(String str) {int targetCol = str.charAt(0) - 'A', targetRow = Integer.valueOf(str.substring(1)) - 1;return new int[] {targetRow, targetCol};}private void set(int row, int col, int v) {set(row, col, v, true);}private void set(int row, int col, int v, boolean rawSet) {String key = encode(row, col);int oldVal = boards[row][col], delta = v - oldVal;if (subscribeeMap.containsKey(key) && rawSet) { // 不再接收其他cell的更新for (String upstream : subscribeeMap.get(key)) {subscriberMap.get(upstream).remove(key);}subscribeeMap.remove(key);}if (subscriberMap.containsKey(key)) { // 推送到其他cellMap<String, Integer> cellCount = subscriberMap.get(key);for (String str : cellCount.keySet()) {int[] coordinate = decode(str);int subVal = boards[coordinate[0]][coordinate[1]] + (cellCount.get(str) * delta);set(coordinate[0], coordinate[1], subVal, false);}}boards[row][col] = v;}public int get(int r, char c) {int row = r - 1, col = c - 'A';return boards[row][col];}public int sum(int r, char c, String[] strs) {int row = r - 1, col = c - 'A';String currKey = encode(row, col);int sum = 0;for (String str : strs) {if (str.indexOf(":") != -1) {String[] splitted = str.split(":");int colMin = splitted[0].charAt(0) - 'A', colMax = splitted[1].charAt(0) - 'A';int rowMin = Integer.valueOf(splitted[0].substring(1)) - 1, rowMax = Integer.valueOf(splitted[1].substring(1)) - 1;for (int targetRow = rowMin; targetRow <= rowMax; targetRow++) {for (int targetCol = colMin; targetCol <= colMax; targetCol++) {sum += boards[targetRow][targetCol];String targetKey = encode(targetRow, targetCol);subscriberMap.putIfAbsent(targetKey, new HashMap<>());subscriberMap.get(targetKey).put(currKey, subscriberMap.get(targetKey).getOrDefault(currKey, 0) + 1);subscribeeMap.putIfAbsent(currKey, new HashSet<>());subscribeeMap.get(currKey).add(targetKey);}}} else {int targetCol = str.charAt(0) - 'A', targetRow = Integer.valueOf(str.substring(1)) - 1;sum += boards[targetRow][targetCol];String targetKey = encode(targetRow, targetCol);subscriberMap.putIfAbsent(targetKey, new HashMap<>());subscriberMap.get(targetKey).put(currKey, subscriberMap.get(targetKey).getOrDefault(currKey, 0) + 1);subscribeeMap.putIfAbsent(currKey, new HashSet<>());subscribeeMap.get(currKey).add(targetKey);}}set(row, col, sum, false);return sum;}}/*** Your Excel object will be instantiated and called as such:* Excel obj = new Excel(H, W);* obj.set(r,c,v);* int param_2 = obj.get(r,c);* int param_3 = obj.sum(r,c,strs);*/假如是set heavy,那么可以set的时候直接保存值,sum的时候保存需要sum的单元格,get的时候再从各个单元格中取值相加。
632. smallest-range
- 给一个
List<List<Integer>>
,每行List内部是排好序的,求一个区间使其包括每一行的某个元素。例如[[4,10,15,24,26], [0,9,12,20], [5,18,22,30]]
,返回[20,24]
. - 类似于merge k sorted list,自定义一个Node类存放值、所属的行数、所处的列数信息,每次从每个List中取值存入PriorityQueue,然后每次从pq中poll掉元素的下一个作为新的元素存入pq。1234567891011121314151617181920212223242526272829303132333435363738394041424344454647class Solution {class Node {int val;int row;int index;public Node(int val, int row, int index) {this.val = val;this.row = row;this.index = index;}}public int[] smallestRange(List<List<Integer>> nums) {if (nums == null || nums.size() == 0 || nums.get(0).size() == 0) {return new int [0];}int[] ans = new int [] {0, Integer.MAX_VALUE}; // warning: need to be max at firstint k = nums.size();PriorityQueue<Node> pq = new PriorityQueue<>((a, b) -> a.val - b.val);// new PriorityQueue<Node>(new Comparator() {// public int compare(Node a, Node b) {// return a.val - b.val;// }// });int max = Integer.MIN_VALUE;for (int i = 0; i < k; i++) {int currVal = nums.get(i).get(0);max = Math.max(max, currVal);pq.offer(new Node(currVal, i, 0));}while (pq.size() == nums.size()) { // 已经没有新的元素了加进来说明能跨所有行的间距已经遍历完成Node node = pq.poll();if (max - node.val < ans[1] - ans[0]) { // 发现间距更小的window就更新ans[1] = max;ans[0] = node.val;}if (node.index + 1 < nums.get(node.row).size()) {Node nextNode = new Node(nums.get(node.row).get(node.index + 1), node.row, node.index + 1);if (nextNode.val > max) {max = nextNode.val;}pq.offer(nextNode);}}return ans;}}
633. sum-of-square-numbers
- 给一个非负数,判断它是否是两个整数的平方和。
方法一:类似two sum,把每个整数的平方和存入set,判断set中是否有target - curr即可。
12345678910111213141516class Solution {public boolean judgeSquareSum(int c) {if (c < 0) {return false;}Set<Integer> set = new HashSet<>();int sqrtC = (int) Math.sqrt(c);for (int i = 0; i <= sqrtC; i++) {set.add(i * i);if (set.contains(c - i * i)) {return true;}}return false;}}方法二:双指针,同时从0和sqrt(num)出发往中间逼近,若平方和大了则左移右指针、若小了则右移左指针。但
1234567891011121314151617public boolean judgeSquareSum(int c) {if (c < 0) {return false;}int left = 0, right = (int) Math.sqrt(c);while (left <= right) {int curr = left * left + right * right;if (curr > c) {right--;} else if (curr < c) {left++;} else {return true;}}return false;}
636. exclusive-time-of-functions
- 给一个log数组,每个log包含
function_id:start_or_end:timestamp
形式的字符串,求每个function执行时间长度。注意这些function的执行可能嵌套、也可能递归调用。 - 经典的Stack题,需要用Stack记录function_id,这样在后续log来的时候,如果是start,说明栈顶函数暂停执行了,需要先把它的时间存起来(当前时间戳减去之前的时间戳);如果是end,说明栈顶函数彻底执行完了,此时的时间计算需要加上end的这个时间戳。1234567891011121314151617181920212223242526class Solution {public int[] exclusiveTime(int n, List<String> logs) {int[] ans = new int [n];if (logs == null) {return ans;}Stack<Integer> stack = new Stack<>();int prev = 0; // 记录上一次log的时间戳for (int i = 0; i < logs.size(); i++) {String[] log = logs.get(i).split(":");if (log[1].equals("start")) {int curr = Integer.valueOf(log[2]);if (!stack.isEmpty()) {ans[stack.peek()] += (curr - prev);}prev = curr;stack.push(Integer.valueOf(log[0]));} else {int curr = Integer.valueOf(log[2]);ans[stack.pop()] += (curr - prev + 1); // end包含当前这个时间点,因此要加1prev = curr + 1;}}return ans;}}
637. average-of-levels-in-binary-tree
- 给一个二叉树,求每一层的平均数。
- 还是层级遍历的变形,主要是防止求平均值的时候越界,用了double,这样求平均值的时候也方便很多。写出来是这样。
638. shopping-offers
- 给一个list表示商品单价,然后给一些购买组合,最后给一个target,求组成target所需的最小开销。
- DFS+memo。对于每一个优惠组合都进行尝试,如果有商品数小于target,则可以取当前开销后继续DFS,否则说明当前组合超过需要的了,直接break。在暴力尝试所有购买组合的过程中,target可能会被反复访问到,所以用一个map表示target对应的最小开销即可。12345678910111213141516171819202122232425262728293031323334353637class Solution {public int shoppingOffers(List<Integer> price, List<List<Integer>> special, List<Integer> needs) {if (price == null || special == null || needs == null) {return 0;}Map<List<Integer>, Integer> map = new HashMap<>();return dfs(price, special, needs, map);}private int dfs(List<Integer> price, List<List<Integer>> special, List<Integer> needs, Map<List<Integer>, Integer> map) {if (map.containsKey(needs)) {return map.get(needs);}int i = 0, min = buySingle(price, needs);for (List<Integer> bundle : special) {List<Integer> currNeeds = new ArrayList<>(needs);for (i = 0; i < currNeeds.size(); i++) {int diff = currNeeds.get(i) - bundle.get(i);if (diff < 0) {break;}currNeeds.set(i, diff);}if (i == needs.size()) {min = Math.min(min, bundle.get(i) + dfs(price, special, currNeeds, map));}}map.put(needs, min);return min;}private int buySingle(List<Integer> price, List<Integer> needs) {int sum = 0;for (int i = 0; i < price.size(); i++) {sum += price.get(i) * needs.get(i);}return sum;}}
646. maximum-length-of-pair-chain
- 给一个pair的数组,每个pair可以作为chain的节点。节点
[c, d]
能够连到[a, b]
后面的条件是b < c
。求最长的链长度。 - DP。
len[i]
表示以pair[i]结尾的链的长度,那么在双重循环时,就需要找到pairs[j]使得pairs[j][1] < pairs[i][0]
。12345678910111213141516171819202122class Solution {public int findLongestChain(int[][] pairs) {if (pairs == null || pairs.length == 0) {return 0;}Arrays.sort(pairs, (a, b) -> {return a[0] - b[0];});int[] len = new int [pairs.length]; // len[i]表示以pairs[i]结尾的链的长度Arrays.fill(len, 1);int maxLen = 1;for (int i = 1; i < pairs.length; i++) {for (int j = 0; j < i; j++) {if (pairs[i][0] > pairs[j][1]) {len[i] = Math.max(len[i], len[j] + 1); // 上一个节点为pairs[j]}}maxLen = Math.max(maxLen, len[i]);}return maxLen;}}
647. palindromic-substrings
- 给一个字符串,求其中自对称的子串的个数。例如
aaa
就有6个,aba
就有4个。 - DP。
dp[i][j]
表示从第i个字符到第j个字符是否对称,当判断第i
和第j
个字符的时候,如果相等则需要用到i + 1
到j - 1
之间的结果(若也对称则当前这个也是对称的),因此需要从后往前递推更新DP数组才行。1234567891011121314151617181920212223class Solution {public int countSubstrings(String s) {if (s == null) {return 0;}int count = 0;char[] sChar = s.toCharArray();boolean[][] dp = new boolean [sChar.length][sChar.length]; // dp[i][j] means take substr from i to jfor (int row = dp.length - 1; row >= 0; row--) { // 从最后一行开始往前dp[row][row] = true; // 最后一列设为truecount++; // 每个字符本身是自对称的for (int col = row + 1; col < dp.length; col++) { // 只更新对角线之后的元素if (sChar[col] == sChar[row]) {dp[row][col] = row + 1 > col - 1 || dp[row + 1][col - 1];}if (dp[row][col]) {count++;}}}return count;}}
648. replace-words
- 给一个List的dict表示词根,然后给一个sentence String,将其中以词根开头的单词替换成词根,返回修改后的String。
方法一:直接用Set存放这些词根,然后split之后暴力取每一个单词,再逐一取字符append判断是否在Set中,有就替换过去。
123456789101112131415161718192021222324252627282930313233343536373839class Solution {public String replaceWords(List<String> dict, String sentence) {if (dict == null || dict.size() == 0 || sentence == null || sentence.length() == 0) {return sentence;}Set<String> set = new HashSet<>();int minLen = Integer.MAX_VALUE;for (String str : dict) {set.add(str);minLen = Math.min(str.length(), minLen);}String[] words = sentence.split("\\s+");for (int i = 0; i < words.length; i++) {int wordLen = words[i].length();if (wordLen < minLen) {continue;}StringBuilder temp = new StringBuilder(words[i].substring(0, minLen));if (set.contains(temp.toString())) {words[i] = temp.toString();continue;}for (int j = minLen; j < wordLen; j++) {temp.append(words[i].charAt(j));if (set.contains(temp.toString())) {words[i] = temp.toString();break;}}}StringBuilder sb = new StringBuilder();for (String word : words) {sb.append(word);sb.append(" ");}sb.setLength(sb.length() - 1);return sb.toString();}}方法二:使用Trie,将dict中的所有词根都存入Trie,然后还是取出来判断这些word是否有前缀出现在Trie中。
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657class Solution {class TrieNode {boolean isWord;TrieNode[] next;public TrieNode() {next = new TrieNode[26];Arrays.fill(next, null);isWord = false;}}public String replaceWords(List<String> dict, String sentence) {if (dict == null || dict.size() == 0 || sentence == null || sentence.length() == 0) {return sentence;}TrieNode root = buildTrie(dict);String[] words = sentence.split("\\s+");StringBuilder ans = new StringBuilder();for (int i = 0; i < words.length; i++) {String prefix = getPrefix(root, words[i]);ans.append(prefix == null ? words[i] : prefix);ans.append(' ');}ans.setLength(ans.length() - 1);return ans.toString();}private TrieNode buildTrie(List<String> dict) {TrieNode root = new TrieNode();for (String str : dict) {insert(root, str);}return root;}private void insert(TrieNode root, String word) {for (char c : word.toCharArray()) {if (root.next[c - 'a'] == null) {root.next[c - 'a'] = new TrieNode();}root = root.next[c - 'a'];}root.isWord = true;}// return the prefix in trie if exist, or null if not.private String getPrefix(TrieNode root, String word) {StringBuilder sb = new StringBuilder();for (char c : word.toCharArray()) {if (root.next[c - 'a'] == null) {return null;}sb.append(c);if (root.next[c - 'a'].isWord) {return sb.toString();}root = root.next[c - 'a'];}return root.isWord ? word : null;}}
650. 2-keys-keyboard
- 假设有一个2键键盘,分别为全选屏幕上的字符复制、粘贴。开始时屏幕上有一个字符A,求输出n个A最少需要按几次键盘。
朴素DP:最后按下的键一定是粘贴,不知道的是在前方何时按下复制,因此需要暴力遍历。
dp[n]
表示产生n个A所需的最小按键次数,最开始屏幕上就有一个A,因此dp[1] = 0
. 对于dp[k]
,假设在dp[i]
处按下了复制键复制了屏幕上的i
个A,之后粘贴(k - i)/i
次,加上按下的一次复制,总共为k / i
次。1234567891011121314151617class Solution {public int minSteps(int n) {if (n == 0) {return 0;}int[] dp = new int[n + 1];for (int k = 2; k <= n; k++) {dp[k] = Integer.MAX_VALUE;for (int i = k - 1; i > 0; i--) {if (k % i == 0) {dp[k] = Math.min(dp[k], dp[i] + k / i);}}}return dp[n];}}方法二:较为specific的方法。假设分为
[cpp][cpppp][cp]
组,每组的按键次数为l1, l2, l3,则最后得到的总数N = l1 * l2 * l3
,因此问题转换为求N的所有素数factors,而且这个按键次数一定是最少的,因为pq >= p + q
.12345678910111213class Solution {public int minSteps(int n) {int retVal = 0, divisor = 2;while (n > 1) {while (n % divisor == 0) {retVal += divisor;n /= divisor;}divisor++;}return retVal;}}
651. 4-keys-keyboard
- 假设有一个4键键盘,分别为输入A、全选、复制、粘贴。求一共按N次的情况下最多能够输出多少A。
- DP。
652. find-duplicate-subtrees
给一个二叉树,返回一个包含所有duplicate的子树根节点的List。例如下面的树就有2-4和4两个duplicate的子树。
12345671/ \2 3/ / \4 2 4/4利用encoding tree的思路,对于每一个节点为root的树都进行一波类似前序遍历点操作,将遍历结果encode成字符串作为key、计数作为value存入map。一旦出现了两次就加入结果List。
123456789101112131415161718192021class Solution {public List<TreeNode> findDuplicateSubtrees(TreeNode root) {List<TreeNode> ans = new ArrayList<>();Map<String, Integer> map = new HashMap<>();check(root, map, ans);return ans;}private String check(TreeNode node, Map<String, Integer> map, List<TreeNode> ans) {if (node == null) {return "";}// 通过前序遍历拼接出pattern并存入mapString pattern = node.val + "," + check(node.left, map, ans) + "," + check(node.right, map, ans);int count = map.getOrDefault(pattern, 0) + 1;if (count == 2) { // the second oneans.add(node);}map.put(pattern, count);return pattern;}}
653. two-sum-iv-input-is-a-bst
- 给一个二叉搜索树和一个sum值,判断树中是否存在两个node之和等于sum。
朴素想法,对于每个可能的值进行O(logN)的搜索,因此总的时间复杂度就是O(NlogN),而空间复杂度如果考虑递归栈的话就是O(TreeHeight)。
12345678910111213141516171819202122232425262728class Solution {public boolean findTarget(TreeNode root, int k) {return dfs(root, root, k);}private boolean dfs(TreeNode node, TreeNode root, int k) {if (node == null) {return false;}int target = k - node.val;if (target != node.val && search(root, target)) {return true;} else {return dfs(node.left, root, k) || dfs(node.right, root, k);}}private boolean search(TreeNode root, int val) {if (root == null) {return false;}if (root.val == val) {return true;} else if (val < root.val) {return search(root.left, val);} else {return search(root.right, val);}}}方法二:用BST中序遍历转换成有序数组,再用双指针分别从头和尾往中间找。时间O(N),空间O(N).
1234567891011121314151617181920212223242526class Solution {public boolean findTarget(TreeNode root, int k) {List<Integer> list = new ArrayList<>();inorder(root, list);int left = 0, right = list.size() - 1;while (left < right) { // 双指针查找有序数组中的pairint sum = list.get(left) + list.get(right);if (sum < k) {left++;} else if (sum > k) {right--;} else {return true;}}return false;}private void inorder(TreeNode root, List<Integer> list) {if (root == null) {return;}inorder(root.left, list);list.add(root.val);inorder(root.right, list);}}
654. maximum-binary-tree
- 给一个不重复的int数组表示的二叉树节点value,最大值作为root,左侧子数组对应root左子树,右侧子数组对应root右子树,重新建树并返回root节点。
方法一:直接递归解决,但是时间复杂度最坏情况是O(N^2)。
123456789101112131415161718192021public TreeNode constructMaximumBinaryTree(int[] nums) {if (nums == null || nums.length == 0) {return null;}return constructMaximumBinaryTree(nums, 0, nums.length);}private TreeNode constructMaximumBinaryTree(int[] nums, int start, int end) {if (start == end) {return null;}int maxIndex = start;for (int i = start + 1; i < end; i++) {if (nums[i] > nums[maxIndex]) {maxIndex = i;}}TreeNode root = new TreeNode(nums[maxIndex]);root.left = constructMaximumBinaryTree(nums, start, maxIndex);root.right = constructMaximumBinaryTree(nums, maxIndex + 1, end);return root;}方法二:利用Stack可以达到O(N)时间复杂度,关键是发现规律,即「当前节点的左孩子一定是在左侧大于它的最远节点右侧的小于当前val的最左节点,同理右孩子一定是在右边较大节点左侧的小于当前节点值的最右节点」。在Stack中需要栈底到栈顶是从大到小的,一旦新的节点更大,就要不断pop,同时将此时pop出来的节点作为新节点的左节点,这样就能实现第一个要求。第二个要求则是在插入的时候,将栈顶的右孩子暂时指向新节点,同时将新节点入栈,之后如果有比新节点更大但没有前一个节点大的节点加入,则又回指向该节点了。
1234567891011121314151617181920212223public TreeNode constructMaximumBinaryTree(int[] nums) {if (nums == null || nums.length == 0) {return null;}Stack<TreeNode> stack = new Stack<>();for (int i = 0; i < nums.length; i++) {TreeNode curr = new TreeNode(nums[i]);// 将左边小于当前节点的值作为当前节点的左孩子while (!stack.isEmpty() && stack.peek().val < curr.val) {curr.left = stack.pop();}// 找到左边第一个大于当前节点的值,当前节点作为它的右孩子if (!stack.isEmpty()) {stack.peek().right = curr;}stack.push(curr);}TreeNode root = null;while (!stack.isEmpty()) { // 栈顶是rootroot = stack.pop();}return root;}
655. print-binary-tree
- 给一个二叉树,将每一层每个节点的值打印成字符串List,若为null则打印成空字符串。
方法一:递归。首先需要知道树的高度H才能确定每一层需要打印多少字符串。将List全部初始化为
""
后,对每一层List的终点进行赋值,然后递归到下一层对左右子节点继续赋值。1234567891011121314151617181920212223242526272829303132333435class Solution {public List<List<String>> printTree(TreeNode root) {List<List<String>> retVal = new ArrayList<>();if (root == null) {retVal.add(Arrays.asList(""));return retVal;}int rows = getHeight(root);int cols = (int)Math.pow(2, rows) - 1;List<String> temp = new ArrayList<>();for (int i = 0; i < cols; i++) {temp.add("");}for (int i = 0; i < rows; i++) {retVal.add(new ArrayList<>(temp));}populate(root, retVal, 0, rows, 0, cols - 1);return retVal;}private void populate(TreeNode curr, List<List<String>> retVal, int row, int rows, int left, int right) {if (curr == null || row == rows) {return;}int mid = (left + right) / 2;retVal.get(row).set(mid, Integer.toString(curr.val));populate(curr.left, retVal, row + 1, rows, left, mid - 1);populate(curr.right, retVal, row + 1, rows, mid + 1, right);}private int getHeight(TreeNode root) {if (root == null) {return 0;}return 1 + Math.max(getHeight(root.left), getHeight(root.right));}}方法二:在populate中不递归调用,二是使用Queue进行BFS,对于每一层也是取中点进行赋值,然后将当前节点的左右孩子入队。
657. judge-route-circle
- 给一个字符串表示一个移动的seq,判断最终是否回到远点。skip.
658. find-k-closest-elements
- 在一个排好序的数组中��找距离x最近的k个元素,若有tie则尽量取更小的值。
- 二分查找找到x所在位置/若x存在则应该处在的index,然后取它左边的元素作为left、本身作为right,双指针前后取即可。为了提高insert效率,使用linkedlist的addfirst。123456789101112131415161718192021222324252627282930313233343536class Solution {public List<Integer> findClosestElements(int[] arr, int k, int x) {LinkedList<Integer> ans = new LinkedList<>();if (arr == null || arr.length == 0) {return ans;}int pos = binarySearch(arr, x);int left = pos - 1, right = pos;while (ans.size() < k && left >= 0 && right < arr.length) {if (x - arr[left] <= arr[right] - x) { // delta相等时尽量取小的ans.addFirst(arr[left--]);} else {ans.add(arr[right++]);}}while (ans.size() < k && left >= 0) {ans.addFirst(arr[left--]);}while (ans.size() < k && right < arr.length) {ans.add(arr[right++]);}return ans;}private int binarySearch(int[] arr, int target) {int left = -1, right = arr.length;while (right - left > 1) {int mid = left + (right - left) / 2;if (target <= arr[mid]) {right = mid;} else {left = mid;}}return right;}}
662. maximum-width-of-binary-tree
- 给一个二叉树,求最大宽度,即最左节点和最右节点之间的间隔。
- 在dfs过程中记录最左节点的id(类似于数组存储二叉树的形式),然后在遍历过程中根据level和当前id求间隔。1234567891011121314151617class Solution {public int widthOfBinaryTree(TreeNode root) {List<Integer> leftMostIds = new ArrayList<>();return dfs(root, 1, 0, leftMostIds);}private int dfs(TreeNode node, int nodeId, int level, List<Integer> leftMostIds) {if (node == null) {return 0;}if (level >= leftMostIds.size()) { // 每一个level最左的node idleftMostIds.add(nodeId);}return Math.max(nodeId - leftMostIds.get(level) + 1,Math.max(dfs(node.left, 2 * nodeId, level + 1, leftMostIds),dfs(node.right, 2 * nodeId + 1, level + 1, leftMostIds)));}}
663. equal-tree-partition
- 给一个二叉树,判断是否可能通过移除一个edge使得两个拆分出来的树的节点sum相等。
- 方法一:使用Set存放所有可能的sum,然后在根部直接
/ 2
看看在Set中是否存在。时间、空间都是O(N)
. 方法二:首先一波流求总的sum,然后在递归逐个节点求sum,看看总sum减去当前sum是否就是当前sum。需要注意edge case
[0, -1, 1]
,只要当前节点不是root即可。这样时间也是O(N)
,空间为O(Height)
。123456789101112131415161718192021222324252627class Solution {private boolean found = false;private TreeNode allRoot;public boolean checkEqualTree(TreeNode root) {int allSum = getSum(root);allRoot = root;dfs(root, allSum);return found;}private int getSum(TreeNode root) {if (root == null) {return 0;}return getSum(root.left) + getSum(root.right) + root.val;}private int dfs(TreeNode root, int allSum) {if (root == null || found) {return 0;}int currSum = root.val + dfs(root.left, allSum) + dfs(root.right, allSum);if (root != allRoot && allSum - currSum == currSum) {found = true;return 0;}return currSum;}}方法三:直接修改节点值,存放每个子树的sum。逐层判断即可,也需要排除root。
665. non-decreasing-array
- 给一个int数组,问能否只修改其中一个元素使得整个数组non-decending.
- greedy。对于当前元素与之前一位的比较,如果之前一位更大,就需要考虑是将当前元素拔高还是将之前元素降低。如果要保证后续尽量少操作,应当尽量把之前元素降低,但是还需要考虑再之前一个元素:如果再之前一个元素比当前元素大,即形成
3,4,2
的局面,则为了保证操作数最少,只能将当前元素拔高;否则就是类似与3,6,4
的局面,将6
降低即可。12345678910111213141516171819class Solution {public boolean checkPossibility(int[] nums) {if (nums == null || nums.length == 0) {return false;}int count = 0;for (int i = 1; i < nums.length && count <= 1; i++) {if (nums[i - 1] > nums[i]) {if (i - 2 < 0 || nums[i - 2] <= nums[i]) {nums[i - 1] = nums[i];} else {nums[i] = nums[i - 1];}count++;}}return count <= 1;}}
666. path-sum-iv
- 给一个int数组,每个数字都有三个digit,表示一个二叉树中的
深度、从左到右第几个、值
。求这个树的所有root到leaf的path sum. - 不用构造树,可以直接根据层数、索引来确定后续的节点是什么,和普通的遍历一样进行DFS就可以了。1234567891011121314151617181920212223242526272829class Solution {private int sum;Map<Integer, Integer> map;public int pathSum(int[] nums) {sum = 0;map = new HashMap<>();for (int num : nums) {map.put(num / 10, num % 10);}dfs(nums[0] / 10, 0);return sum;}private void dfs(int root, int currSum) {int level = root / 10, index = root % 10;int leftIndex = index * 2 - 1, rightIndex = index * 2;int leftKey = (level + 1) * 10 + leftIndex, rightKey = (level + 1) * 10 + rightIndex;currSum += map.get(root);if (!map.containsKey(leftKey) && !map.containsKey(rightKey)) {sum += currSum;}if (map.containsKey(leftKey)) {dfs(leftKey, currSum);}if (map.containsKey(rightKey)) {dfs(rightKey, currSum);}}}
669. trim-a-binary-search-tree
- 给一个BST和一个值域[L, R],只保留BST中属于该值域的节点。递归搞定,skip。
670. maximum-swap
- 给一个非负数,求至多将其中两个digit互换位置之后所能得到的最大数。例如
9987
就是本身,9978
是9987
,958469
是998465
. - 暗中观察规律就是找其中一个小的数字并找在它右侧的最大数,交换。因此首先需要记录每个数字最后出现的位置,然后从原数字第一位开始遍历,从最大的数字开始比较,一旦找到比自己大且排在自己后面的数字就可以直接交换位置了。12345678910111213141516171819202122232425class Solution {public int maximumSwap(int num) {if (num <= 0) {return 0;}char[] numStr = Integer.toString(num).toCharArray();int[] bucket = new int [10];for (int i = 0; i < numStr.length; i++) {bucket[numStr[i] - '0'] = i; // 每个数字最后出现的位置}// 找到最靠右的、比当前数字大的数字,交换位置即可for (int i = 0; i < numStr.length; i++) {for (int index = 9; index > numStr[i] - '0'; index--) { // 注意只跟比当前数字大的比位置if (bucket[index] > i) { // 在当前位置之后char temp = numStr[bucket[index]];numStr[bucket[index]] = numStr[i];numStr[i] = temp;return Integer.valueOf(new String(numStr));}}}return num;}}
671. second-minimum-node-in-a-binary-tree
- 给一个特殊的二叉树,每一个节点只有0或2个children,且值是两个子节点的较小值。求树中第二小的值,若没有,返回-1.
- 既然找的是比最小值大的最小的值,就用递归的方法在左右两边分别找比最小值的大的最小值,然后比较一下即可。12345678910111213141516171819202122232425262728class Solution {public int findSecondMinimumValue(TreeNode root) {if (root == null || root.left == null || root.right == null) {return -1;}int leftLeastGreater = getLeastGreater(root.left, root.val);int rightLeastGreater = getLeastGreater(root.right, root.val);int min = Math.min(leftLeastGreater, rightLeastGreater);if (leftLeastGreater > root.val && rightLeastGreater > root.val) {return min;}return leftLeastGreater == root.val ? (rightLeastGreater == root.val ? -1 : rightLeastGreater) : leftLeastGreater;}private int getLeastGreater(TreeNode root, int val) {if (root.left == null && root.right == null) {return root.val;}int leftLeastGreater = getLeastGreater(root.left, val);int rightLeastGreater = getLeastGreater(root.right, val);if (leftLeastGreater > val && rightLeastGreater > val) {return Math.min(leftLeastGreater, rightLeastGreater);} else if (leftLeastGreater > val) {return leftLeastGreater;} else {return rightLeastGreater;}}}
673. number-of-longest-increasing-subsequence
- 给一个数组,求其中最长递增的subsequence的个数。如
[1,3,5,4,7]
中有两个长度为4的subsequence。 - DP。用一个len[k]数组记录以k结尾的字符处的最长长度,
count[k]
记录对应的计数。双重循环时,当nums[i] > nums[j]
,若len[j] + 1 > len[i]
则需要更新i处的长度,同时count也直接更新;若len[j] + 1 == len[i]
,则说明刚好从j过来可以形成递增sequence,直接累加就行了(一开始以为是lc300求长度,就用stack做了,然而stack这个greedy做法也是错误的,还是需要上DP。。1234567891011121314151617181920212223242526272829303132class Solution {public int findNumberOfLIS(int[] nums) {if (nums == null || nums.length == 0) {return 0;}int[] len = new int [nums.length];int[] count = new int [nums.length];int maxLen = 1, ans = 0;Arrays.fill(len, 1);Arrays.fill(count, 1);for (int i = 1; i < nums.length; i++) {for (int j = 0; j < i; j++) {if (nums[i] > nums[j]) { // 保证递增关系int tempLen = len[j] + 1; // 递增后的长度if (tempLen > len[i]) {len[i] = tempLen;count[i] = count[j];} else if (tempLen == len[i]) { // 从j跳过来的递增count[i] += count[j];}}}maxLen = Math.max(len[i], maxLen); // 记录最大长度,后续用于对比并累计count}for (int i = 0; i < nums.length; i++) {if (len[i] == maxLen) {ans += count[i];}}return ans;}}
674. longest-continuous-increasing-subsequence
- 给一个数组,求其中最长递增的连续subarray的长度。如
[1,2,3,4,5,6,5,4,3,4,5]
就是6,[2,2,2,2,2]
就是1. - 贪心法,只有大于前面元素才更新,一旦小于就更新到总的里面。注意最后返回之前还要取一次max。1234567891011121314151617class Solution {public int findLengthOfLCIS(int[] nums) {if (nums == null || nums.length == 0) {return 0;}int ans = 1, curr = 1;for (int i = 1; i < nums.length; i++) {if (nums[i] > nums[i - 1]) {curr++;} else {ans = Math.max(ans, curr);curr = 1;}}return Math.max(ans, curr);}}
676. implement-magic-dictionary
- 实现
buildDict
和search
方法,search时判断能否通过「替换一个字符」的方式使得修改后的字符串包含在dict中,返回boolean。例如给["hello", "leetcode"]
,搜索hhllo
就返回true、搜索hello
返回false。 方法一:dict就想到选择map,在build时对于每个字符串,将其中每个字符替换成特殊字符如
*
,将替换后的字符串作为key、被替换的字符作为value存入map。如果出现相同的key,则说明这个位置可以放任何字符(例如hello, hallo
的第二位);在搜索时也是对每个字符替换,然后看map中有没有,判断一下当前字符是否是value的字符(防止indentical)。123456789101112131415161718192021222324252627282930313233343536373839404142434445464748class MagicDictionary {// 将每个单词的每个字符都替换成*之后,插入map,key是替换后的字符串,value是被替换掉的那个字符// 如果出现替换过后的key一样,则该位可以放任意字符,因为例如hello和hallo只有在第二位不同,如果替换后是h*llo可以任选一个,一定是true。Map<String, Character> map = null;/** Initialize your data structure here. */public MagicDictionary() {map = new HashMap<String, Character>();}/** Build a dictionary through a list of words */public void buildDict(String[] dict) {if (dict == null || dict.length == 0) {return;}for (String word : dict) {StringBuilder sb = new StringBuilder(word);int len = word.length();for (int i = 0; i < len; i++) {sb.setCharAt(i, '*');Character c = map.get(sb.toString());if (c == null) {map.put(sb.toString(), word.charAt(i));} else {map.put(sb.toString(), '*'); // 表示可以放任何字符}sb.setCharAt(i, word.charAt(i));}}}/** Returns if there is any word in the trie that equals to the given word after modifying exactly one character */public boolean search(String word) {if (word == null || word.length() == 0) {return false;}int len = word.length();StringBuilder sb = new StringBuilder(word);for (int i = 0; i < len; i++) {sb.setCharAt(i, '*');Character c = map.get(sb.toString());if (c != null && (c == '*' || c != word.charAt(i))) {return true;}sb.setCharAt(i, word.charAt(i));}return false;}}方法二:使用Trie。build的时候就正常地创建Trie,然后在search的时候逐个位置替换26个字符,每换一次就在Trie中搜索。Trie的效率感觉不高啊,感觉有square级别。
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455class MagicDictionary {class TrieNode {TrieNode[] children = new TrieNode[26];boolean isWord;public TrieNode() {}}TrieNode root;/** Initialize your data structure here. */public MagicDictionary() {root = new TrieNode();}/** Build a dictionary through a list of words */public void buildDict(String[] dict) {for (String s : dict) {TrieNode node = root;for (char c : s.toCharArray()) {if (node.children[c - 'a'] == null) {node.children[c - 'a'] = new TrieNode();}node = node.children[c - 'a'];}node.isWord = true;}}/** Returns if there is any word in the trie that equals to the given word after modifying exactly one character */public boolean search(String word) {char[] arr = word.toCharArray();for (int i = 0; i < word.length(); i++) {for (char c = 'a'; c <= 'z'; c++) {if (arr[i] == c) {continue;}char org = arr[i];arr[i] = c;if (checkTrie(new String(arr), root)) {return true;}arr[i] = org;}}return false;}public boolean checkTrie(String s, TrieNode root) {TrieNode node = root;for (char c : s.toCharArray()) {if (node.children[c - 'a'] == null) {return false;}node = node.children[c - 'a'];}return node.isWord;}}
678. valid-parenthesis-string
- 给一个只含有
(
,*
,)
的字符串,其中*
可以是任何一种括号,或者为空,判断字符串是否合法。 方法一:与传统的valid parenthesis相比就是多了一个
*
,那就把它分别带入左右和空三种情况递归判断即可。1234567891011121314151617181920212223242526public boolean checkValidString(String s) {if (s == null || s.length() == 0) {return true;}return check(s.toCharArray(), 0, 0);}private boolean check(char[] charArray, int start, int leftCount) {if (leftCount < 0) {return false;}for (int i = start; i < charArray.length; i++) {if (charArray[i] == '(') {leftCount++;} else if (charArray[i] == ')') {if (leftCount == 0) {return false;}leftCount--;} else {return check(charArray, i + 1, leftCount + 1)|| check(charArray, i + 1, leftCount - 1)|| check(charArray, i + 1, leftCount);}}return leftCount == 0;}方法二:延续stack的做法,左括号和星号的index入栈,每次遇到右括号先把左括号的stack弹出。最后比较左括号的星号栈的索引大小即可。
1234567891011121314151617181920212223242526272829public boolean checkValidString(String s) {if (s == null || s.length() == 0) {return true;}Stack<Integer> leftIds = new Stack<>();Stack<Integer> starIds = new Stack<>();for (int i = 0; i < s.length(); i++) {char c = s.charAt(i);if (c == '(') {leftIds.push(i);} else if (c == '*') {starIds.push(i);} else {if (leftIds.isEmpty() && starIds.isEmpty()) {return false;} else if (!leftIds.isEmpty()) {leftIds.pop();} else {starIds.pop();}}}while (!leftIds.isEmpty() && !starIds.isEmpty()) {if (starIds.pop() < leftIds.pop()) {return false;}}return leftIds.isEmpty();}方法三:从左到右扫一遍,分别统计左括号和星号,优先匹配左括号,当左括号不够时再调用右括号;但是这样一波无法判断
(*()
这样的情况,因为星号始终是当成左的。因此需要再从右往左走一波,把星号当成有括号。12345678910111213141516171819202122232425262728293031323334353637public boolean checkValidString(String s) {if (s == null || s.length() == 0) {return true;}int left = 0, leftStar = 0, right = 0, rightStar = 0;for (int i = 0; i < s.length(); i++){if (s.charAt(i) == '(') {left++;} else if (s.charAt(i) == '*') {leftStar++;} else {if (left > 0) {left--;} else if (leftStar > 0) {leftStar--;} else {return false;}}}for (int i = s.length() - 1; i >= 0; i--) {if (s.charAt(i) == ')') {right++;} else if (s.charAt(i) == '*') {rightStar++;} else {if (right > 0) {right--;} else if (rightStar > 0) {rightStar--;} else {return false;}}}return true;}
680. valid-palindrome-ii
- 给一个字符串,判断能否通过“最多删掉一个字符”形成自对称字符串。例如
aba
本身就是,abca
可以通过删掉b或c变成自对称。 - 直接前后指针往中间并拢,一旦发现不同的字符就把不同的两个字符分别遮住继续判断,如果还不行那就一定不能自对称了。123456789101112131415161718192021222324class Solution {public boolean validPalindrome(String s) {if (s == null || s.length() == 0) {return false;}int left = 0, right = s.length() - 1;while (left < right) {if (s.charAt(left) != s.charAt(right)) { // 发现对应位置不匹配,尝试遮掉其中一个继续判断return isPalin(s, left + 1, right) || isPalin(s, left, right - 1);}left++;right--;}return true;}private boolean isPalin(String s, int left, int right) {while (left < right) {if (s.charAt(left++) != s.charAt(right--)) {return false;}}return true;}}
681. next-closest-time
- 给一个字符串表示时间,求由这些数组组成的、下一个最近的时间(数字可无限使用)。
- greedy方法,从末尾开始替换,如果有恰好比他大的数字,替换之后直接就返回了。否则就替换成这些数字中最小的数字。注意每一个位置的限制都不同,例如第二位就需要根据第一位是否为2来决定最大值是3还是9.12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849class Solution {public String nextClosestTime(String time) {if (time == null || time.length() == 0) {return null;}char[] digits = getDigits(time.split(":"));char[] digitsOrigin = Arrays.copyOf(digits, digits.length);char[] ans = new char[5];ans[0] = digitsOrigin[0];ans[1] = digitsOrigin[1];ans[2] = ':';ans[3] = digitsOrigin[2];ans[4] = digitsOrigin[3];Arrays.sort(digits);ans[4] = getNextGreater(digitsOrigin[3], '9', digits);if (ans[4] > digitsOrigin[3]) {return String.valueOf(ans);}ans[3] = getNextGreater(digitsOrigin[2], '5', digits);if (ans[3] > digitsOrigin[2]) {return String.valueOf(ans);}ans[1] = getNextGreater(digitsOrigin[1], digitsOrigin[0] == '2' ? '3' : '9', digits);if (ans[1] > digitsOrigin[1]) {return String.valueOf(ans);}ans[0] = getNextGreater(digitsOrigin[0], '2', digits);return String.valueOf(ans);}private char[] getDigits(String[] timeSplitted) {char[] digits = new char[4];digits[0] = (char)('0' + Integer.parseInt(timeSplitted[0]) / 10);digits[1] = (char)('0' + Integer.parseInt(timeSplitted[0]) % 10);digits[2] = (char)('0' + Integer.parseInt(timeSplitted[1]) / 10);digits[3] = (char)('0' + Integer.parseInt(timeSplitted[1]) % 10);return digits;}private char getNextGreater(char curr, char limit, char[] digits) {int pos = Arrays.binarySearch(digits, curr) + 1;while (pos < 4 && digits[pos] <= limit && digits[pos] == curr) {pos++;}return pos < 4 && digits[pos] <= limit ? digits[pos] : digits[0];}}
682. baseball-game
- 定义一个积分规则,没有什么好说的,用List即可。
683. k-empty-slots
- 给一个flowers数组表示第
i + 1
天,开花的索引是flowers[i]
。再给一个k,问是否存在某一天使得存在连续k个花未开且左右两边都已经开放,返回这个天数,若不存在则返回-1。 方法一:维护一个days数组表示该index的花在第
days[index]
天开。需要求得一个子序列left, left+1, left+2, ..., left+k, right
使得days[left]和days[right]开放时间比中间任何一个都要早。从左往右遍历days数组,一旦发现中间某天开花时间早于left或者right就说明这个子序列中断了,更新left为i即可。时间空间都是O(N).1234567891011121314151617181920212223class Solution {public int kEmptySlots(int[] flowers, int k) {if (flowers == null || flowers.length <= k) {return -1;}int[] days = new int[flowers.length];for (int i = 0; i < flowers.length; i++) {days[flowers[i] - 1] = i + 1;}int left = 0, right = k + 1, ans = Integer.MAX_VALUE; // 从左开始往右遍历for (int i = 0; right < days.length; i++) {if (days[i] < days[left] || days[i] <= days[right]) { // 若其中某个i开花时刻早于左或者右侧window,就以该处为新的起点if (i == right) { // 若遍历到right都没有问题,就是一个新的ansans = Math.min(ans, Math.max(days[left], days[right]));}left = i;right = i + k + 1;}}return ans == Integer.MAX_VALUE ? -1 : ans;}}方法二:利用TreeSet存储flowers,随着天数增加,利用TreeSet的lower找小于该索引处的最大值、利用higher找大于该索引的最小值,这样求出来就能碰到完美等于k的子序列了。
123456789101112131415161718class Solution {public int kEmptySlots(int[] flowers, int k) {if (flowers == null || flowers.length <= k) {return -1;}TreeSet<Integer> bloomIndex = new TreeSet<>();for (int day = 0; day < flowers.length; day++) {bloomIndex.add(flowers[day]);Integer left = bloomIndex.lower(flowers[day]);Integer right = bloomIndex.higher(flowers[day]);if ((left != null && flowers[day] - left == k + 1)|| (right != null && right - flowers[day] == k + 1)) {return day + 1;}}return -1;}}
684. redundant-connection
- 给一系列边组成无向图,其中恰好多了一个边使图无法形成树,求多出来的这个边即最后出现的这个边。例如
[[1,2], [2,3], [3,4], [1,4], [1,5]]
,返回[1,4]
。 - 并查集,维护每个节点的祖先,首次出现时默认以自己为祖先,然后就判断edge中的两个节点祖先是否一样,一样就说明成环了,否则就把前者的祖先指向后者的祖先。123456789101112131415161718192021222324252627282930313233343536class Solution {// 并查集:维护一个Map,表示每个编号的点的祖先// 一旦找到两个点的祖先一样,就说明这个边就是让前面成环的,直接返回// 若祖先不同,则直接将前者的祖先归位后者祖先的后代,这样就把两个部分直接合并了,后面判断成环就可以直接判断public int[] findRedundantConnection(int[][] edges) {if (edges == null || edges.length == 0 || edges[0].length == 0) {return null;}Map<Integer, Integer> parentMap = new HashMap<>();for (int[] edge: edges) {int from = edge[0];int to = edge[1];if (!parentMap.containsKey(from)) {parentMap.put(from, from); // 刚开始设为本身}if (!parentMap.containsKey(to)) {parentMap.put(to, to);}int fromParent = findParent(parentMap, from);int toParent = findParent(parentMap, to);if (fromParent == toParent) { // 二者的祖先是一样的说明成环了return edge;} else {parentMap.put(fromParent, toParent);}}return new int [2];}private int findParent(Map<Integer, Integer> parentMap, int node) {int parent = parentMap.get(node);if (parent != node) {parentMap.put(node, findParent(parentMap, parent));}return parentMap.get(node);}}
685. redundant-connection-ii
- 给一系列边组成有向图,其中恰好多了一个边使图无法形成rooted tree,求多出来的这个边即最后出现的这个边。例如
[[2,1],[3,1],[4,2],[1,4]]
,返回[2,1]
。 - 与684相比这里的边都是有向的了,有两种情况来判别边invalid:形成了环,或者一个节点同时有两个parent节点。做法分为两步:首先check看是否有节点有两个parent,有的话就设为candidate A和B,并把B设置为invalid(设一个节点为0即可);然后进行union-find,如果此时树已经是valid的了,就直接返回candidate B(因为是后出现的)。123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051class Solution {public int[] findRedundantDirectedConnection(int[][] edges) {if (edges == null || edges.length == 0 || edges[0].length == 0) {return null;}int[] candidate1 = new int [2];int[] candidate2 = new int [2]; // 后出现的int[] parent = new int [edges.length + 1];// 先找有没有节点有两个parentfor (int i = 0; i < edges.length; i++) {if (parent[edges[i][1]] == 0) { // 设置每个孩子节点的parentparent[edges[i][1]] = edges[i][0];} else { // 发现有重复设置的情况candidate2[0] = edges[i][0];candidate2[1] = edges[i][1];candidate1[0] = parent[edges[i][1]]; // 原本存的parent是谁candidate1[1] = edges[i][1];edges[i][1] = 0; // 这个孩子节点暂时设为无效的节点值,比如0}}// 重新初始化parent,设为本身for (int i = 0; i < edges.length; i++) {parent[i] = i;}for (int[] edge : edges) {if (edge[1] == 0) {continue;}int father = edge[0], child = edge[1]; // 前->后if (root(parent, father) == child) { // 判断两个节点是不是连到一起了,注意这里是直接判断是否以child作为parent,而不像上一题两个节点都要求parentif (candidate1[0] == 0) { // 没有多parent的情况return edge;} else {return candidate1;}}parent[child] = father;}return candidate2;}private int root(int[] parent, int i) {while (parent[i] != i) {parent[i] = parent[parent[i]];i = parent[i];}return i;}}
686. repeated-string-match
- 给两个字符串A和B,求A需要重复几次才能让B成为它的substring.
狗家实习的OA,自己想的方法。先看看起始字符都出现在哪些索引,统统入queue;然后先拼一波使得A的长度不小于B;然后从queue中取起始索引,比较看B是否包含其中;如果queue还没用完则还需要拼多一次。
123456789101112131415161718192021222324252627282930313233343536373839404142class Solution {public int repeatedStringMatch(String A, String B) {if (A == null || B == null) {return -1;}String AOld = A;Queue<Integer> startIndexQueue = new LinkedList<>();char[] AChar = A.toCharArray(), BChar = B.toCharArray();char startChar = BChar[0];// O(N) get start positionfor (int i = 0; i < AChar.length; i++) {if (AChar[i] == startChar) {startIndexQueue.add(i);}}int repeatCount = 1;while (!startIndexQueue.isEmpty() && A.length() - startIndexQueue.peek() < BChar.length) {A += AOld; // append if not long enoughrepeatCount++;}while (!startIndexQueue.isEmpty() && startIndexQueue.peek() + BChar.length <= A.length()) {int startIndex = startIndexQueue.poll();if (B.equals(A.substring(startIndex, startIndex + BChar.length))) {return repeatCount;}}// if there is still startIndex in queue, need to repeat and check moreif (!startIndexQueue.isEmpty()) {A += AOld;repeatCount++;while (!startIndexQueue.isEmpty()) {if (B.equals(A.substring(startIndexQueue.peek(), startIndexQueue.peek() + BChar.length))) {return repeatCount;} else {startIndexQueue.poll();}}}return -1;}}或者直接用拼接的方式,一直拼接A直到超过B的长度,然后看是否包含。如果不包含还需要额外的一次拼接再看。
1234567891011public int repeatedStringMatch(String A, String B) {int count = 0;StringBuilder sb = new StringBuilder();while (sb.length() < B.length()) {sb.append(A);count++;}if (sb.toString().contains(B)) return count;if (sb.append(A).toString().contains(B)) return ++count;return -1;}
687. longest-univalue-path
- 给一个二叉树,求其中最长的连续边数使得经过的节点值都一样。不一定是完全笔直的路径。
对于左子树和右子树递归调用求最长路径(不取当前节点的情况),然后根据当前节点的值进行DFS(也就是取当前节点的情况)。最后取最大。
123456789101112131415class Solution {public int longestUnivaluePath(TreeNode root) {if (root == null) {return 0;}int child = Math.max(longestUnivaluePath(root.left), longestUnivaluePath(root.right));return Math.max(child, dfs(root.left, root.val) + dfs(root.right, root.val));}private int dfs(TreeNode node, int val) {if (node == null || node.val != val) {return 0;}return 1 + Math.max(dfs(node.left, val), dfs(node.right, val)); // two nodes forms one edge}}但是上面的这个方法存在大量重复访问节点,时间复杂度O(N^2)。因此考虑和之前diameter题一样,使用全局变量求最大path,同时在dfs每步直接将两侧中较大深度返回给上一层。
12345678910111213141516171819class Solution {int maxLen = 0;public int longestUnivaluePath(TreeNode root) {if (root == null) {return 0;}dfs(root, root.val);return maxLen;}private int dfs(TreeNode node, int val) {if (node == null) {return 0;}int left = dfs(node.left, node.val);int right = dfs(node.right, node.val);maxLen = Math.max(maxLen, left + right); // 取当前node为转折点return node.val == val ? Math.max(left, right) + 1 : 0;}}
688. knight-probability-in-chessboard
- 给一个整数N表示是
N*N
的棋盘,其中一个knight/马在坐标(r, c),问走K步后它还在棋盘有效范围内的概率是多少。 - 概率就是走到棋盘上的情况数/总的情况数即
8^K
种走法。当前位置取决于上一步的位置,但是我们并不知道上一步有多少种走法,因此想到bottom-up的DP方法,即假设当前是走完K步之后的位置,可以从之前的哪些位置走过来呢,倒推上一步的走法,然后再继续倒推。注意只有上一步是在有效棋盘内才能update。12345678910111213141516171819202122232425262728293031class Solution {final private int[][] moves = {{1, 2}, {1, -2}, {2, 1}, {2, -1}, {-1, 2}, {-1, -2}, {-2, 1}, {-2, -1}};public double knightProbability(int N, int K, int r, int c) {if (!isValid(r, c, N)) {return 0.0;}double[][] dp = new double[N][N];for (double[] row : dp) {Arrays.fill(row, 1);}for (int k = 0; k < K; k++) {double[][] dpFrom = new double[N][N];for (int i = 0; i < N; i++) {for (int j = 0; j < N; j++) {for (int[] move : moves) { // 倒推:从i, j跳到当前位置的int row = i + move[0];int col = j + move[1];if (isValid(row, col, N)) { // 需要得到跳过来的那个位置的情况数dpFrom[i][j] = dp[row][col] + dpFrom[i][j];}}}}dp = dpFrom;}return dp[r][c] / Math.pow(8, K);}private boolean isValid(int r, int c, int N) {return r >= 0 && r < N && c >= 0 && c < N;}}
689. maximum-sum-of-3-non-overlapping-subarrays
- 给一个正整数数组,找出三个互不重叠的、size为k的子数组,使得总和最大。返回的形式是每个subarray的起始索引。例如
[1,2,1,2,6,7,5,1], k=2
则返回[0, 3, 5]
。 - 有唯一解吗?(可能有多个,只需返回最先出现索引)k本身会不会很大?(不会,不大于len/3)
- 首先是如何快速求某个区间内的和?如果数值都不大的话,可以通过累加把sum都给缓存下来,用的时候直接减一下就行了。然后是如何求subarray的结果?可以用二位dp数组,行表示划分成row个subarray,列表示从0到col处为止能得到的最大的总sum。此外还需要一个二维index数组记录第row个subarray对应的起始位置。从第一个subarray开始循环,固定求size为k的subarray使之和最大,其实就是贪心的思想,只要过程中求的每个subarray的和都最大那么最终的总和就是最大的。在循环过程中就可以不断求总和,为了防止重叠求和时必须求往前k个元素为end的dp结果。123456789101112131415161718192021222324252627282930313233343536373839class Solution {public int[] maxSumOfThreeSubarrays(int[] nums, int k) {// 动态规划if (nums == null || nums.length == 0) {return new int [0];}// 缓存到i为止到所有项之和int[] sumArray = new int [nums.length];sumArray[0] = nums[0];for (int i = 1; i < nums.length; i++) {sumArray[i] = sumArray[i - 1] + nums[i];}// dp[i][j]表示求i个non-overlap sum的时候从0~j能得到的最大总sumint[][] dp = new int [4][nums.length];// index[i][j]表示求第i个non-overlap sum的时候的starting indexint[][] index = new int [4][nums.length];for (int i = 1; i < 4; i++) { // 从求第1个最大的k-size subarray开始直到第3个for (int j = k - 1; j < nums.length; j++) {int tempMax = j == k - 1? sumArray[j] : // 快速求区间内的和sumArray[j] - sumArray[j - k] + dp[i - 1][j - k]; // 加上上一行前一个block为止的最大和if (j > k - 1) { // 先直接沿用同一行的前面的结果dp[i][j] = dp[i][j - 1];index[i][j] = index[i][j - 1];}if (j > 0 && tempMax > dp[i][j - 1]) { // 若发现有更大的就更新当前最大和到dpdp[i][j] = tempMax;index[i][j] = j - k + 1; // 同时更新最大和出现的下标}}}int[] ans = new int [3];ans[2] = index[3][nums.length - 1]; // 最后一行的最后一位就是第三个block的indexans[1] = index[2][ans[2] - 1]; // 倒数第二行的index[3]之前的存的就是第二行的ans[0] = index[1][ans[1] - 1];return ans;}}
690. employee-importance
- 给一个List of Employee,包含id、importance、下属List等属性。给定id,求这个id对应员工及其所有下属(不一定是直接下属)的imp之和。
- DFS。用一个Map先存储id-Employee的键值对,然后可以给定一个id快速访问到该Employee的信息,然后DFS递归搞定。1234567891011121314151617181920212223242526272829303132333435/*// Employee infoclass Employee {// It's the unique id of each node;// unique id of this employeepublic int id;// the importance value of this employeepublic int importance;// the id of direct subordinatespublic List<Integer> subordinates;};*/class Solution {public int getImportance(List<Employee> employees, int id) {if (employees == null || employees.size() == 0) {return 0;}Map<Integer, Employee> map = new HashMap<>();for (Employee e : employees) {map.put(e.id, e);}return getImp(map, id);}private int getImp(Map<Integer, Employee> map, int id) {if (!map.containsKey(id)) {return 0;}Employee e = map.get(id);int imp = e.importance;for (Integer i : e.subordinates) {imp += getImp(map, i);}return imp;}}
692. top-k-frequent-words
- 给一个String数组,求出现频率top k的字符串。
跟347类似。先用Map存每个单词出现的频数,再自定义根据频数minHeap存这些Map.Entry,然后每次都check堆的规模,一旦大于k就直接把最小的poll出来,这样就保证在minHeap中的一定是top k.最后就直接逆序插入结果List。
123456789101112131415161718192021222324252627282930313233class Solution {public List<String> topKFrequent(String[] words, int k) {List<String> ans = new ArrayList<>();if (words == null || words.length == 0 || k == 0) {return ans;}// 统计每个单词出现的频数Map<String, Integer> map = new HashMap<>();for (String word : words) {if (!map.containsKey(word)) {map.put(word, 1);} else {map.put(word, map.get(word) + 1);}}// 将entry按照频数从小到大插入PQ,若规模>k则直接poll掉最小的PriorityQueue<Map.Entry<String, Integer>> pq = new PriorityQueue<>((a, b) -> {return a.getValue() != b.getValue()?a.getValue() - b.getValue() : b.getKey().compareTo(a.getKey());});for (Map.Entry<String, Integer> e : map.entrySet()) {pq.offer(e);if (pq.size() > k) {pq.poll();}}// 取PQ元素,逆序插入结果while (!pq.isEmpty()) {ans.add(0, pq.poll().getKey());}return ans;}}follow-up:如果给定的输入不是一个完整的数组,而是一个stream,即每次都只能取得一个单词,然后立即返回top k,如何改进?
- 关键在于无法在最开始就获得完整的频数统计Map,在插入minHeap的时候就无法知道后续的Entry需不需要覆盖PQ中的值。因此需要一个额外的Map记录具体哪些Entry目前被存放在minHeap中;当新的单词出现,就先更新map中的项,然后再看看它是否在PQ中,在则需要更新(我只能想到把k个元素全抖出来再加进去),不在则跟minHeap的peek比较决定是否需要替换。这样时间是O(N)的统计频数、O(logN)的插入minHeap、O(KlogK)的更新(?)和最后O(K)的倒入List。
694. number-of-distinct-islands
- 给一个grid,其中含有0和1,求所有distinct的连续1的团簇的个数,distinct指的是通过位移无法完全匹配的连续的1的区域。
- 与统计number of island的区别在于这里的island需要通过某种方式辨别该形状是否出现过,联想到encode方法,利用上下左右标记走过的路程。对于DFS,需要额外标出回溯的字母。对于BFS,也需要在结束当前节点的邻接点enqueue后加上标记符。12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455class Solution {private final int[][] directions = new int[][] {{-1, 0}, {1, 0}, {0, -1}, {0, 1}};private final char[] directionsChar = new char[] {'u', 'd', 'l', 'r'};public int numDistinctIslands(int[][] grid) {if (grid == null || grid.length == 0 || grid[0].length == 0) {return 0;}int rowTotal = grid.length, colTotal = grid[0].length;Set<String> set = new HashSet<>();for (int i = 0; i < rowTotal; i++) {for (int j = 0; j < colTotal; j++) {if (grid[i][j] == 1) {StringBuilder sb = new StringBuilder();// dfs(grid, i, j, sb, 'o');bfs(grid, i, j, sb);set.add(sb.toString());}}}return set.size();}private void bfs(int[][] grid, int row, int col, StringBuilder sb) {Queue<int[]> q = new LinkedList<>();q.offer(new int[] {row, col});grid[row][col] = 0; // BFS在加入queue的时候就要将grid标记为已访问了while (!q.isEmpty()) {int[] curr = q.poll();for (int i = 0; i < directions.length; i++) {int rowNeighbor = curr[0] + directions[i][0];int colNeighbor = curr[1] + directions[i][1];if (validatePos(grid, rowNeighbor, colNeighbor)) {grid[rowNeighbor][colNeighbor] = 0;q.offer(new int[] {rowNeighbor, colNeighbor});sb.append(directionsChar[i]);}}sb.append(','); // 表示将当前的所有neighbor都enqueue了}}private void dfs(int[][] grid, int row, int col, StringBuilder sb, char dir) {grid[row][col] = 0;sb.append(dir);for (int i = 0; i < directions.length; i++) {int rowNeighbor = row + directions[i][0];int colNeighbor = col + directions[i][1];if (validatePos(grid, rowNeighbor, colNeighbor)) {dfs(grid, rowNeighbor, colNeighbor, sb, directionsChar[i]);}}sb.append('b'); // Trick!!! DFS回溯的时候需要标出来,不然无法区分是否回溯之后的位移}private boolean validatePos(int[][] grid, int row, int col) {return row >= 0 && row < grid.length && col >= 0 && col < grid[0].length && grid[row][col] == 1;}}
695. max-area-of-island
- 给一个grid,表示水和小岛,求最大的岛的面积。DFS搞定,skip。
696. count-binary-substrings
- 给一个只含有0和1的字符串,求其中有多少个子字符串使得0和1分别连续出现且个数相等,例如
01
,1100
就满足要求。 - 从头到尾遍历,然后双指针扩散判断。123456789101112131415161718192021222324class Solution {public int countBinarySubstrings(String s) {if (s == null || s.length() < 2) {return 0;}int left = 0, right = 1, ans = 0; // 同时比较相邻两个字符while (right < s.length()) {if (s.charAt(left) != s.charAt(right)) { // 往左和右分别扩展char charLeft = s.charAt(left), charRight = s.charAt(right);int i = left - 1, j = right + 1, count = 1;while (i >= 0 && s.charAt(i) == charLeft &&j < s.length() && s.charAt(j) == charRight) {i--;j++;count++;}ans += count;}left++;right++;}return ans;}}
697. degree-of-an-array
- 给一个int数组,degree为其中元素出现的最大频数。求和原数组degree相同的最小subarray长度。
- 对每个元素计数,同时记录第一个出现的index,出现新degree直接就是当前索引到第一个出现索引的距离。pass。
698. partition-to-k-equal-sum-subsets
- 给一个只含有(0, 10000)的int数组和一个k,判断是否可以将该数组划分为k个相等sum的partition。
似乎是个NP-hard的问题。只能用暴力办法,DFS+标记数组,每次累加过后进入下一层看看是否达到了targetSum,达到了就清空继续往后找新的一组subset.最后如果只剩下一组了,直接返回true,因为此时其他k - 1个组都已经达到targetSum了,当前的
sum = k * targetSum - (k - 1) * targetSum = targetSum
.1234567891011121314151617181920212223242526272829303132333435class Solution {public boolean canPartitionKSubsets(int[] nums, int k) {if (nums == null || nums.length == 0 || k > nums.length || k < 1) {return false;}if (k == 1) {return true;}int sum = IntStream.of(nums).sum(); // Java8的stream!if (sum % k != 0) {return false;}int targetSum = sum / k;boolean[] visited = new boolean[nums.length];return checkPartition(nums, visited, 0, targetSum, 0, k);}public boolean checkPartition(int[] nums, boolean[] visited, int startIndex, int targetSum, int currSum, int k) {if (k == 1) { // 提前breakreturn true;}if (currSum == targetSum) {return checkPartition(nums, visited, 0, targetSum, 0, k - 1);}for (int i = startIndex; i < nums.length; i++) {if (!visited[i]) {visited[i] = true;if (checkPartition(nums, visited, i + 1, targetSum, currSum + nums[i], k)) {return true;}visited[i] = false;}}return false;}}方法二:更快的做法是先对数组排序,然后存k个bucket,每个bucket从后往前取元素不断累加.
12345678910111213141516171819202122232425262728293031323334353637383940class Solution {public boolean canPartitionKSubsets(int[] nums, int k) {if (nums == null || nums.length == 0 || k > nums.length || k < 1) {return false;}if (k == 1) {return true;}int sum = 0;for (int num : nums) {sum += num;}if (sum % k != 0) {return false;}int targetSum = sum / k;Arrays.sort(nums);return checkPartition(nums, targetSum, new int[k], nums.length - 1);}public boolean checkPartition(int[] nums, int targetSum, int[] buckets, int numsIndex) {if (numsIndex < 0) {for (int bucket : buckets) {if (bucket != targetSum) {return false;}}return true;}for (int i = 0; i < buckets.length; i++) {if (buckets[i] + nums[numsIndex] <= targetSum) {buckets[i] += nums[numsIndex];if (checkPartition(nums, targetSum, buckets, numsIndex - 1)) {return true;}buckets[i] -= nums[numsIndex];}}return false;}}follow-up: 如果去掉正数的限制,允许出现负数和0?
- 上面的方法只考虑了直接累积叠加,无法解决负数问题。因此需要引入一个elementCount来统计当前这一波存入了多少element,当达到targetSum的时候需要判断当前这一波是否真的存入了元素。1234567891011121314151617181920212223242526272829303132333435class Solution {public boolean canPartitionKSubsets(int[] nums, int k) {if (nums == null || nums.length == 0 || k > nums.length || k < 1) {return false;}if (k == 1) {return true;}int sum = IntStream.of(nums).sum();if (sum % k != 0) {return false;}int targetSum = sum / k;boolean[] visited = new boolean[nums.length];return checkPartition(nums, visited, 0, targetSum, 0, k, 0);}public boolean checkPartition(int[] nums, boolean[] visited, int startIndex, int targetSum, int currSum, int k, int elementCount) {if (k == 0) { // 必须算完才行return true;}if (currSum == targetSum && elementCount > 0) {return checkPartition(nums, visited, 0, targetSum, 0, k - 1, 0);}for (int i = startIndex; i < nums.length; i++) {if (!visited[i]) {visited[i] = true;if (checkPartition(nums, visited, i + 1, targetSum, currSum + nums[i], k, elementCount + 1)) {return true;}visited[i] = false;}}return false;}}
699. falling-squares
- 给一个二维数组,每一行表示一个方块的起始坐标和边长。方块按照数组的顺序下落,方块底部可以粘在下面的方块上,无限堆叠,求一个List表示当前已下落的所有方块中的最大高度。
方法一:naive的O(N^2)暴力遍历法。对于每个方块都往前遍历所有的方块求当前方块的高度,然后和List的前一个元素比较,获取最大高度。
12345678910111213141516171819202122232425262728293031323334class Solution {class Square {int left, right, height;public Square(int left, int right, int height) {this.left = left;this.right = right;this.height = height;}}public List<Integer> fallingSquares(int[][] positions) {List<Integer> ans = new ArrayList<>();if (positions == null || positions.length == 0 || positions[0].length < 2) {return ans;}Square[] squares = new Square[positions.length];for (int i = 0; i < positions.length; i++) {squares[i] = new Square(positions[i][0], positions[i][0] + positions[i][1], positions[i][1]);int currHeight = getHeight(squares, i);squares[i].height = currHeight;ans.add(Math.max(i > 0 ? ans.get(i - 1) : 0, currHeight));}return ans;}private int getHeight(Square[] squares, int i) {int maxHeight = squares[i].height;for (int j = 0; j < i; j++) {if (squares[i].left >= squares[j].right || squares[i].right <= squares[j].left) {continue;}maxHeight = Math.max(maxHeight, squares[i].height + squares[j].height);}return maxHeight;}}方法二:TreeMap
- 方法三:Segment Tree
700. search-in-a-binary-search-tree
- 给一个BST和val,返回对应节点。pass.
701. insert-into-a-binary-search-tree
- 给一个BST和一个val,将val插入BST,返回插入后的BST。
方法一:递归。val与当前节点比较,小就深入左子树、大就深入右子树。
12345678910111213class Solution {public TreeNode insertIntoBST(TreeNode root, int val) {if (root == null) {return new TreeNode(val);}if (val < root.val) {root.left = insertIntoBST(root.left, val);} else {root.right = insertIntoBST(root.right, val);}return root;}}方法二:迭代。思路类似,根据大小决定潜入左还是右子树。
1234567891011121314151617181920212223class Solution {public TreeNode insertIntoBST(TreeNode root, int val) {TreeNode curr = root;while (curr != null) {if (val < curr.val) {if (curr.left == null) {curr.left = new TreeNode(val);return root;} else {curr = curr.left;}} else {if (curr.right == null) {curr.right = new TreeNode(val);return root;} else {curr = curr.right;}}}return new TreeNode(val);}}
703. kth-largest-element-in-a-stream
- 实现一个能handle stream of int的类,调用add时能返回第k大的数。
- 和求top k element一个道理,PriorityQueue搞定。skip.
704. binary-search
- 实现二分查找,直接套了模版。
706. design-hashmap
- 实现一个HashMap。
- 基本上就是考察CS基础了。最直接的想法就是用bucket,输入的key范围是多少就用多少空间。如果没有那么多空间,那就要考虑缩小空间并处理collision. 这里可以使用
ListNode chaining
解决。123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384class MyHashMap {class Node {int key, val;Node next;public Node(int key, int val) {this.key = key;this.val = val;}}private Node[] bucket;private int size = 10000;/** Initialize your data structure here. */public MyHashMap() {bucket = new Node[size];}/** value will always be non-negative. */public void put(int key, int value) {int index = key % size;if (bucket[index] == null) {bucket[index] = new Node(key, value);} else {Node head = bucket[index];Node prev = new Node(0, 0);prev.next = head;while (head != null) {if (head.key == key) {head.val = value;return;}prev = head;head = head.next;}prev.next = new Node(key, value);}}/** Returns the value to which the specified key is mapped, or -1 if this map contains no mapping for the key */public int get(int key) {int index = key % size;if (bucket[index] == null) {return -1;} else {Node head = bucket[index];while (head != null) {if (head.key == key) {return head.val;}head = head.next;}return -1;}}/** Removes the mapping of the specified value key if this map contains a mapping for the key */public void remove(int key) {int index = key % size;if (bucket[index] != null) {Node head = bucket[index];if (head.key == key) {bucket[index] = head.next;} else {Node prev = head;head = head.next;while (head != null) {if (head.key == key) {prev.next = head.next;return;}prev = head;head = head.next;}}}}}/*** Your MyHashMap object will be instantiated and called as such:* MyHashMap obj = new MyHashMap();* obj.put(key,value);* int param_2 = obj.get(key);* obj.remove(key);*/
708. insert-into-a-cyclic-sorted-list
- 给一个环状链表的任意一个节点,这个链表是从小到大有序的,插入一个新节点,返回原本给的那个节点。若一开始为null,则返回新插入的节点。
- 方法一:各种条件判断,边缘情况讨论,各种if,没有成功。
- 方法二:先走一波找到最大的节点,即给的节点开始往后走,一旦当前节点比后续节点大了就说明拐点到了。暂时将最大节点与后续节点断开,后续节点就是最小节点,在它之前插入一个dummy节点,以方便插入,然后从dummy开始往后一个个看,一旦当前节点不比要插入的节点小了,就是插入位置了,或者一直到最后都没找到插入点,就插在末尾。最后再把最大节点接回最小节点即可,注意需要判断最大节点有没有变化,即看看之前的那个「最大节点」后面是否为null。123456789101112131415161718192021222324252627class Solution {public Node insert(Node head, int insertVal) {Node newNode = new Node(insertVal, null);if (head == null) {head = newNode;head.next = newNode;}// 1. 找到最大的节点,变成单向链表Node curr = head;while (curr.next != head && curr.val <= curr.next.val) {curr = curr.next;}// 2. 将最大节点断开,从dummy开始往后找插入点Node maxNode = curr, dummy = new Node(Integer.MIN_VALUE, curr.next);maxNode.next = null; // 最大之后就是最小的了,断开curr = dummy; // dummy之后就是最小节点while (curr.next != null && curr.next.val < insertVal) {curr = curr.next;}newNode.next = curr.next;curr.next = newNode;Node newMax = maxNode.next == null ? maxNode : maxNode.next;newMax.next = dummy.next;return head;}}
709. to-lower-case
- 实现转化成小写的函数。pass。
711. number-of-distinct-islands-ii
- 给一个二维grid表示小岛,求其中形状distinct的小岛数量。这些形状可以任意平移、轴对称、翻转。
- 暴力方法,遍历完一个小岛的时候就将所有的轴对称、翻转形式统统列出来,然后根据某个统一标准取一个root form来代表所有这些形状,这里就直接使用encode之后字典序最小的作为key。12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364class Solution {final private int[][] directions = {{-1, 0}, {1, 0}, {0, -1}, {0, 1}};final private int[][] transitions = {{1, 1}, {1, -1}, {-1, 1}, {-1, -1}};public int numDistinctIslands2(int[][] grid) {if (grid == null || grid.length == 0 || grid[0].length == 0) {return 0;}int rowTotal = grid.length, colTotal = grid[0].length;Set<String> islandSet = new HashSet<>();for (int i = 0; i < rowTotal; i++) {for (int j = 0; j < colTotal; j++) {if (grid[i][j] == 1) {List<int[]> island = new ArrayList<>();dfs(grid, i, j, island);islandSet.add(getRootShape(island));}}}return islandSet.size();}private void dfs(int[][] grid, int row, int col, List<int[]> island) {island.add(new int[] {row, col});grid[row][col] = 0;for (int[] direction : directions) {int rowNeighbor = row + direction[0];int colNeighbor = col + direction[1];if (validatePos(grid, rowNeighbor, colNeighbor)) {dfs(grid, rowNeighbor, colNeighbor, island);}}}private boolean validatePos(int[][] grid, int row, int col) {return row >= 0 && row < grid.length && col >= 0 && col < grid[0].length && grid[row][col] == 1;}private String getRootShape(List<int[]> island) {List<String> shapes = new ArrayList<>();// 对于shape中的一个点,总共有八种形式// (x, y), (x, -y), (-x, y), (-x, -y)// (y, x), (-y, x), (y, -x), (-y, -x)for (int[] transition : transitions) {List<int[]> list1 = new ArrayList<>();List<int[]> list2 = new ArrayList<>();for (int[] point : island) {list1.add(new int[] {point[0] * transition[0], point[1] * transition[1]});list2.add(new int[] {point[1] * transition[1], point[0] * transition[0]});}shapes.add(getKey(list1)); // 获取每个方向上的root encode形式shapes.add(getKey(list2));}Collections.sort(shapes); // 用最小的代表所有这些shapereturn shapes.get(0);}private String getKey(List<int[]> points) {Collections.sort(points, (a, b) -> a[0] != b[0] ? a[0] - b[0] : a[1] - b[1]);StringBuilder sb = new StringBuilder();int row = points.get(0)[0], col = points.get(0)[1];for (int[] point : points) {// 注意是与最小的那个点的delta作为坐标,即相对坐标sb.append(point[0] - row).append(',').append(point[1] - col).append(';');}return sb.toString();}}
712. minimum-ascii-delete-sum-for-two-strings
- 给两个字符串,需要删除适当的字符使得他们相等,求最小的所有被删除字符的ascii之和。
- 每个字符是否保留都是独立的决策,因此考虑DP。
dp[i][j]
表示s1[i]
到末尾、s2[j]
到末尾的需要被删字符的最小ascii之和。如果其中一个字符串为空,显然ascii之和就是非空字符串各个字符从后往前累加,最后dp[0][0]
即为所求。12345678910111213141516171819202122232425class Solution {public int minimumDeleteSum(String s1, String s2) {if (s1 == null || s2 == null) {return 0;}int[][] dp = new int[s1.length() + 1][s2.length() + 1];for (int i = s1.length() - 1; i >= 0; i--) {dp[i][s2.length()] = dp[i + 1][s2.length()] + s1.codePointAt(i);}for (int j = s2.length() - 1; j >= 0; j--) {dp[s1.length()][j] = dp[s1.length()][j + 1] + s2.codePointAt(j);}for (int i = s1.length() - 1; i >= 0; i--) {for (int j = s2.length() - 1; j >= 0; j--) {if (s1.charAt(i) == s2.charAt(j)) {dp[i][j] = dp[i + 1][j + 1];} else {dp[i][j] = Math.min(dp[i + 1][j] + s1.codePointAt(i), dp[i][j + 1] + s2.codePointAt(j));}}}return dp[0][0];}}
713. subarray-product-less-than-k
- 给一个正整数数组,和一个k,求有多少个子数组的积小于k。
- 滑动窗口,一旦发现当前积大于k就挪动左指针,直到小于。此时左右指针中间的即为valid的子数组。12345678910111213141516class Solution {public int numSubarrayProductLessThanK(int[] nums, int k) {if (nums == null || nums.length == 0 || k <= 1) {return 0;}int left = 0, product = 1, ans = 0;for (int right = 0; right < nums.length; right++) {product *= nums[right];while (product >= k) {product /= nums[left++];}ans += right - left + 1;}return ans;}}
714. best-time-to-buy-and-sell-stock-with-transaction-fee
- 给一个数组表示股票价格,每次交易(买卖完成算一次)都会收取手续费。求最大收益。
- (思路来自覃超说算法)DP。
profit[i][0]
表示第i天不持有股票手头的资金,profit[i][1]
表示第i天持有股票手头的资金.初始化时第一天如果不持有股票则手头为0,若持有股票则需要消耗资金,因此是-price[0]
。之后的状态转换为profit[i][0] = profit[i - 1][0](前一天也没有买入)和profit[i - 1][1] + prices[i](前一天是持有的,今天卖出)的较大者
。类似地,profit[i][1] = profit[i - 1][1](前一天也持有)和profit[i - 1][0] - prices[i](前一天没有,今天买入)的较大者
。最后返回最后一天不持有股票的profit即可。123456789101112131415class Solution {public int maxProfit(int[] prices, int fee) {if (prices == null || prices.length == 0) {return 0;}int[][] profit = new int [prices.length][2];profit[0][0] = 0;profit[0][1] = -prices[0];for (int i = 1; i < profit.length; i++) {profit[i][0] = Math.max(profit[i - 1][0], profit[i - 1][1] + prices[i] - fee);profit[i][1] = Math.max(profit[i - 1][1], profit[i - 1][0] - prices[i]);}return profit[prices.length - 1][0];}}
716. max-stack
- 实现一个maxStack类,支持普通栈的pop, top,还有peekMax, popMax操作。
方法一:maxStack经典做法就是双栈,一个作为普通stack,一个记录以对应元素为栈顶时的最大元素。tricky的时popMax时需要暂时将普通栈的元素转存到另一个栈中,直到找到当前max元素,pop掉之后,再push回去,这样时间复杂度为
O(N)
,其中N为操作数。12345678910111213141516171819202122232425262728293031323334353637383940class MaxStack {Stack<Integer> stack, maxStack;/** initialize your data structure here. */public MaxStack() {stack = new Stack<>();maxStack = new Stack<>();}public void push(int x) {stack.add(x);maxStack.add(maxStack.isEmpty() ? x : Math.max(x, maxStack.peek()));}public int pop() {maxStack.pop();return stack.pop();}public int top() {return stack.peek();}public int peekMax() {return maxStack.peek();}public int popMax() {int currMax = this.peekMax();Stack<Integer> temp = new Stack<>();while (stack.peek() != currMax) {temp.add(this.pop());}this.pop();while (!temp.isEmpty()) {this.push(temp.pop());}return currMax;}}方法二:如果希望提升peekMax的速度到logN(其他O(1)的操作可以降速),就需要思考如何提升删除元素的速度了。在LRU中我们学习过double linked list可以在给定node时O(1)删除元素,因此可以用来作为stack本体。至于max,就可以考虑tree结构来维护最大堆value -> node,由于stack中可以重复出现元素,对于每一个value需要维护一个node的list,在list后面的就是后插入的,也就是需要先行挪出的。这样所有操作的时间复杂度都是
O(logN)
(因为涉及treeMap的查询)123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384class MaxStack {DoubleLinkedList doubleLinkedList;TreeMap<Integer, LinkedList<Node>> treeMap;/** initialize your data structure here. */public MaxStack() {doubleLinkedList = new DoubleLinkedList();treeMap = new TreeMap<>(Collections.reverseOrder());}public void push(int x) {treeMap.putIfAbsent(x, new LinkedList<Node>());treeMap.get(x).offer(doubleLinkedList.insert(x));}public int pop() {int val = doubleLinkedList.pop();treeMap.get(val).pollLast();if (treeMap.get(val).size() == 0) {treeMap.remove(val);}return val;}public int top() {return doubleLinkedList.top();}public int peekMax() {return treeMap.firstKey();}public int popMax() {Node maxNode = treeMap.firstEntry().getValue().pollLast();if (treeMap.get(maxNode.val).size() == 0) {treeMap.remove(maxNode.val);}doubleLinkedList.delete(maxNode);return maxNode.val;}}class Node {int val;Node prev, next;public Node(int val) {this.val = val;prev = next = null;}}class DoubleLinkedList {Node head, tail;public DoubleLinkedList() {head = new Node(0);tail = new Node(0);head.next = tail;tail.prev = head;}public Node insert(int val) {Node node = new Node(val);node.prev = tail.prev;node.next = tail;tail.prev.next = node;tail.prev = node;return node;}public Node delete(Node node) {node.prev.next = node.next;node.next.prev = node.prev;return node;}public int pop() {return delete(tail.prev).val;}public int top() {return tail.prev.val;}}
717. 1-bit-and-2-bit-characters
- 给一个只含有0、1的int数组,读取时只可以读成
0
或11
或10
。判断最后一个数字是否必须是1-bit. - 贪心即可。每次向后挪动,碰到0挪动1位、碰到1挪动2位,最后看看停留的索引即可。pass。
718. maximum-length-of-repeated-subarray
- 给两个int数组,求其中相同的subarray的最大长度。
- DP,
dp[i][j]
表示A[0, i)
和B[0, j)
的相同subarray的最大长度。如果两个元素相同,则以当前元素结尾的子数组长度取决于以前一个元素结尾的长度。123456789101112131415161718class Solution {public int findLength(int[] A, int[] B) {if (A == null || B == null) {return 0;}int[][] dp = new int[A.length + 1][B.length + 1];int retVal = 0;for (int i = 1; i <= A.length; i++) {for (int j = 1; j <= B.length; j++) {if (A[i - 1] == B[j - 1]) {dp[i][j] = dp[i - 1][j - 1] + 1;}retVal = Math.max(retVal, dp[i][j]);}}return retVal;}}
719. find-k-th-smallest-pair-distance
- 给一个int数组,求每两个数之差中第k小的值。例如
[1,3,8,4,5,45]
,当k = 1,返回1,当k = 3,返回2. - 先对所有元素从小到大排序,那么间距最小值为0、最大值为最右减最左。用二分查找的思想,假设mid为第k小的值,然后O(N^2)遍历求有多少对儿数之差小于mid;若对儿数小于k,说明猜的值太小了排太前了;大于k则说明猜太大了。12345678910111213141516171819202122232425class Solution {public int smallestDistancePair(int[] nums, int k) {if (nums == null || nums.length == 0) {return 0;}Arrays.sort(nums);int lo = 0, hi = nums[nums.length - 1] - nums[0];while (lo < hi) {int mid = lo + (hi - lo) / 2; // 猜一个距离int count = 0, left = 0;for (int right = 1; right < nums.length; right++) {while (nums[right] - nums[left] > mid) {left++;}count += right - left;}if (count < k) { // 说明猜的不够大lo = mid + 1;} else {hi = mid;}}return lo;}}
720. longest-word-in-dictionary
- 给一个string数组,只含有小写字母,这些word可能形成链式如
a, ap, app, appl, apple
,求能形成链式的最长的单词,若有多个则取lexicographical最小的。 - 用sort + Set的方式比较trivial。还有一种Trie + DFS/BFS的更考察基本功,构建trie之后从root节点开始尝试从后往前遍历邻接点并更新最长word,这样就可以保证是lexicograpchical最小的了。
721. accounts-merge
- 给一堆字符串List,每个List中首先是名字,然后是这个人的各种邮箱。最后放回经过merge的姓名、邮箱List,并且要求将邮箱从小到大排序。
这种多对一的关系查找,特别适合用并查集。首先将每个邮箱的parent设为自己,然后将每个List靠后面的邮箱统一把parent设成第一个邮箱。然后利用TreeSet实现邮箱的排序,最后导出到List返回。
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253class Solution {public List<List<String>> accountsMerge(List<List<String>> accounts) {List<List<String>> ans = new ArrayList<>();if (accounts == null || accounts.size() == 0) {return ans;}// 初始化并查集,并存放每个email的主人Map<String, String> parent = new HashMap<>();Map<String, String> emailOwner = new HashMap<>();for (List<String> account : accounts) {for (int i = 1; i < account.size(); i++) {parent.put(account.get(i), account.get(i)); // 老大初始化为自己emailOwner.put(account.get(i), account.get(0));}}// 将同一个人的邮箱存入并查集mapfor (List<String> account : accounts) {String root = find(parent, account.get(1));for (int i = 2; i < account.size(); i++) {parent.put(find(parent, account.get(i)), root);}}// 将同属一个老大的email存入TreeSet以排序Map<String, Set<String>> map = new HashMap<>();for (List<String> account : accounts) {String root = find(parent, account.get(1));if (!map.containsKey(root)) {map.put(root, new TreeSet<String>());}for (int i = 1; i < account.size(); i++) {map.get(root).add(account.get(i));}}// 最后导出到List中返回for (String email : map.keySet()) {String owner = emailOwner.get(email);List<String> list = new ArrayList<>(map.get(email));list.add(0, owner);ans.add(list);}return ans;}private String find(Map<String, String> parent, String s) {while (!parent.get(s).equals(s)) {parent.put(s, parent.get(parent.get(s))); // 将当前的parent设为"parent的parent"s = parent.get(s);}return s;}}其实更直白的看,这题就是个图论题,整理出图的联通部分。首先是构建graph,每个邮箱都是节点,用Set存每一行所给邮箱组成的subgraph,每个邮箱都用Set存放可达邻居(需要双向添加)。然后开始DFS或BFS搜索图,将每一行的第一个邮箱作为起点,把所有可达的邮箱都加进来,在这个过程中需要标记visited。之后如果再遇到visited的邮箱就说明已经在之前“可达”掉了。
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950class Solution {public List<List<String>> accountsMerge(List<List<String>> accounts) {// build the graphMap<String,Set<String>> graph = new HashMap<>();for (List<String> ls : accounts) {for (int i = 1; i < ls.size();i ++) {if (!graph.containsKey(ls.get(i))) graph.put(ls.get(i), new HashSet<String>());graph.get(ls.get(i)).add(ls.get(1));graph.get(ls.get(1)).add(ls.get(i));}}// traverse the graph, find out all the connected subgraphSet<String> visited = new HashSet<>();List<List<String>> result = new ArrayList<>();for (List<String> ls : accounts) {if (!visited.contains(ls.get(1))) {List<String> ans = new ArrayList<>();bfs(graph, visited, ls.get(1), ans); // or dfs(graph,visited,ls.get(1),ans)Collections.sort(ans);ans.add(0,ls.get(0));result.add(ans);}}return result;}public void dfs(Map<String, Set<String>> graph, Set<String> visited, String s, List<String> ans) {ans.add(s);visited.add(s);for (String str : graph.get(s)) {if (!visited.contains(str)) {dfs(graph, visited, str, ans);}}}public void bfs(Map<String, Set<String>> graph, Set<String> visited, String s, List<String> ans) {Queue<String> q = new LinkedList<>();q.add(s);visited.add(s);while (!q.isEmpty()) {String t = q.poll();ans.add(t);for (String str : graph.get(t)) {if (!visited.contains(str)) {q.add(str);visited.add(str);}}}}}
722. remove-comments
- 给一个string数组,每一个string代表一行C++代码,要求将其中的comment清除。可以保证这些comment不会存在于字符串中。
- 没什么意思。用一个inComment布尔值存放当前是否在注释块中。遍历每行代码的时候若仍在注释块中则关注
*/
;正常状态下则先关注是否是//
,这样后续就不用继续append了,否则关注/*
。
723. candy-crush
- 模拟candy crush小游戏,给一个初始的board。若水平或者垂直方向连续三个相同的int,则需要消掉并将上面的数字往下填充,最顶上如果掉下了则补充0表示空。
- 其实主要就是模拟这个过程。如果监测到水平/垂直连续三个相同绝对值的int,则将这三个标记为负。最终再从下往上将负的值用更上面的正值覆盖。12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849class Solution {public int[][] candyCrush(int[][] board) {if (board == null || board.length == 0 || board[0].length == 0) {return board;}int rowCount = board.length, colCount = board[0].length;boolean shouldCrush = true;while (shouldCrush) {shouldCrush = false;for (int i = 0; i < rowCount; i++) {for (int j = 0; j < colCount; j++) {int val = Math.abs(board[i][j]);if (val == 0) continue;if (j + 2 < colCount &&Math.abs(board[i][j + 1]) == val &&Math.abs(board[i][j + 2]) == val) {shouldCrush = true;board[i][j] = -val;board[i][j + 1] = -val;board[i][j + 2] = -val;}if (i + 2 < rowCount &&Math.abs(board[i + 1][j]) == val &&Math.abs(board[i + 2][j]) == val) {shouldCrush = true;board[i][j] = -val;board[i + 1][j] = -val;board[i + 2][j] = -val;}}}if (shouldCrush) {for (int j = 0; j < colCount; j++) {int lowerIndex = rowCount - 1;for (int i = lowerIndex; i >= 0; i--) {if (board[i][j] > 0) {board[lowerIndex--][j] = board[i][j];}}while (lowerIndex >= 0) {board[lowerIndex--][j] = 0;}}}}return board;}}
724. find-pivot-index
- 给一个int数组,求其中一个index使得左侧数字之和与右侧数字之和相等。zillow面试原题,维护leftSum和rightSum即可。
726. number-of-atoms
- 给一个字符串表示化学物质,统计其中的元素及出现次数,按字典序输出。例如
H2(O(Mn)2)3
输出H2Mn6O3
. - 只有1个元素是输出1还是不输出?(不输出数字,只输出元素)
- 由于含有括号,联想运算符算式就知道要用Stack进行吞吐来处理括号嵌套的情况。如果是字母,就一直找到小写的为止作为元素名字,之后跟着的数字就是count,存入map。若出现左括号,则当前的这个map(保存了括号之前的元素及count)直接入栈,然后用新的map继续统计,一旦遇到右括号,说明当前部分结束(注意需要检查右括号之后还有没有数字),则与栈顶弹出的map合并一下就好了。1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465class Solution {public String countOfAtoms(String formula) {if (formula == null || formula.length() == 0) {return "";}Map<String, Integer> map = new HashMap<>();Stack<Map> stack = new Stack<>();char[] fChar = formula.toCharArray();int i = 0, fLen = fChar.length;while (i < fLen) {if (fChar[i] == '(') { // 将之前的map压栈stack.push(map);map = new HashMap<>();i++;} else if (fChar[i] == ')') { // 将当前的合并入之前的mapint count = 0;i++;// 取括号后的数值while (i < fLen && Character.isDigit(fChar[i])) {count = (10 * count) + fChar[i++] - '0';}if (count == 0) {count = 1;}if (!stack.isEmpty()) {Map<String, Integer> prevMap = stack.pop();for (String atom: map.keySet()) { // 取当前map中的atom放入之前的prevMap.put(atom, prevMap.getOrDefault(atom, 0) + map.get(atom) * count);}map = prevMap; // 用回原来的map}} else {// 以字母开头,直到非小写字母为一个原子int end = i + 1;while (end < fLen && Character.isLowerCase(fChar[end])) {end++;}String atom = formula.substring(i, end);// 看看字母之后是否跟着数字int count = 0;while (end < fLen && Character.isDigit(fChar[end])) {count = 10 * count + fChar[end++] - '0';}if (count == 0) {count = 1;}// 更新原子数值map.put(atom, map.getOrDefault(atom, 0) + count);i = end;}}StringBuilder sb = new StringBuilder();List<String> atoms = new ArrayList<>(map.keySet());Collections.sort(atoms); // 字母顺序for (String atom: atoms) {sb.append(atom);if (map.get(atom) > 1) {sb.append(map.get(atom));}}return sb.toString();}}
727. minimum-window-subsequence
- 给一个source字符串和一个target字符串,求在source的最短子串使得包含target的所有字符(个数和出现顺序都必须一致)。
- DP(感觉是野路子,不太好想)。。。纵向行为target,横向列为source,第一行全部初始化为
0,1,2....sLen
表示从第几位开始取,之后所有值初始化为-1表示无解。然后O(M*N)逐个字符遍历两个字符串,若匹配上了则从左上方取起始位置(看前一个字符的情况),若匹配不上则默认继续取source(直接取左侧的值)。123456789101112131415161718192021222324252627282930313233343536373839404142434445464748class Solution {public String minWindow(String S, String T) {// 动态规划, 行为tLen + 1, 列为sLen + 1, dp[][]表示从dp[i][j]到j到这部分字符串是所求,// 即T[0, j)是S[0, i)的subsequence with substring S[dp[i][j], j).// 初始状态为// 状态转换为,若当前字符不匹配,则根据左侧(即S前一个字符)情况决定起始位置(保证最短)// 若匹配,则依赖于左上方的结果,即T前一个字符的起始位置。if (S == null || T == null || S.length() == 0 || T.length() == 0) {return "";}char[] sChar = S.toCharArray();char[] tChar = T.toCharArray();int sLen = sChar.length, tLen = tChar.length;int[][] startFrom = new int [tLen + 1][sLen + 1];for (int i = 0; i <= tLen; i++) {for (int j = 0; j <= sLen; j++) {if (i == 0) {startFrom[i][j] = j;} else {startFrom[i][j] = -1; // -1表示无解}}}for (int i = 1; i <= tLen; i++) {for (int j = 1; j <= sLen; j++) {if (sChar[j - 1] == tChar[i - 1]) {startFrom[i][j] = startFrom[i - 1][j - 1];} else {startFrom[i][j] = startFrom[i][j - 1];}}}int start = 0, end = sLen, minLen = Integer.MAX_VALUE;for (int j = 1; j <= sLen; j++) {System.out.print(startFrom[tLen][j] + " ");if (startFrom[tLen][j] != -1) {int currLen = j - startFrom[tLen][j];if (currLen < minLen) {start = startFrom[tLen][j];end = j;minLen = currLen;}}}return minLen == Integer.MAX_VALUE? "" : S.substring(start, end);}}
729. my-calendar-i
- 给若干开始时间+结束时间pair,实现book函数判断能否成功添加事件,不能有时间重叠。
方法一:暴力法,从头到尾遍历链表,无冲突就插入。效率O(N)。
123456789101112131415161718192021222324252627282930313233343536373839404142class MyCalendar {// 定义一个链表,每次遍历所有节点判断有没有重合,没有就插入到末尾class Node {int start;int end;Node next;public Node(int start, int end) {this.start = start;this.end = end;next = null;}}Node head = null;public MyCalendar() {head = null;}public boolean book(int start, int end) {if (head == null) {head = new Node(start, end);return true;} else {return checkAndAdd(new Node(start, end));}}// brute force: check with every existing intervals and insert at the endprivate boolean checkAndAdd(Node node) {Node curr = head;Node prev = null;while (curr != null) {if (node.end > curr.start && node.start < curr.end) {return false;} else {prev = curr;curr = curr.next;}}prev.next = node;return true;}}方法二:利用TreeMap,对于每个[start, end]对,从TreeMap中找start的floor,取出它对应的end。一旦这个end大于start,就说明有重叠了。同理,也要从TreeMap中找start的ceiling,如果这个ceiling小于end,说明与后面有重叠。
1234567891011121314151617181920212223242526class MyCalendar {// 维护start-end的TreeMap,每次尝试取输入的start的在TreeeMap中的下界和上界// 分别判断输入的start是否在最大的不大于start的floorStart对应的end之间,// 再判断最小的不小于start的ceilingStart会不会落在end之前TreeMap<Integer, Integer> calendar;public MyCalendar() {calendar = new TreeMap<>();}public boolean book(int start, int end) {// floorStart, start, floorStart'sEndInteger floorStart = calendar.floorKey(start);if (floorStart != null && calendar.get(floorStart) > start) {return false;}// start, ceilingStart, endInteger ceilingStart = calendar.ceilingKey(start);if (ceilingStart != null && ceilingStart < end) {return false;}calendar.put(start, end);return true;}}
731. my-calendar-ii
- 与729相比变成了不能出现triple的重叠就算是可以book。
- 注意不能简单地理解为一个interval同时与两个interval重叠,因为
[2,6]
和[1,3]&[5,7]
同时重叠,但没有形成triplet. - 同样是维护TreeMap,对于每个新加入的interval,遍历已有的interval并把重叠的部分插入treemap。如果插入时发现有重叠,说明“重叠部分之间也有重叠”,这样就是triple了。123456789101112131415161718192021222324252627282930313233343536373839404142434445464748class MyCalendarTwo {// 原本一位和之前相比就只是多了一个map,这个存不了就尝试另一个map,都不行就说明triple了。// 但题目给的样例都不行,例如最后一个25~55,因为这个overlap分别和两个map里都interval重叠,但是没有形成triple;// 正解应该是用一个list维护所有的interval,然后用treemap只维护当前重叠的部分,如果后续又出现了和list里的重叠,// 就再去treemap中看看有没有第三次重叠。// 注意每次遍历都需要清空TreeMap,因为我在遍历List的时候只关心新加入的这个会不会和别的重叠。List<int[]> intervals;TreeMap<Integer, Integer> overlap;public MyCalendarTwo() {intervals = new ArrayList<>();overlap = new TreeMap<>();}public boolean book(int start, int end) {overlap.clear();// 遍历所有interval看看有没有重叠for (int[] interval: intervals) {if (start >= interval[0] && start < interval[1]) {// interval: ________// newInter: _____...if (!addOverlap(start, Math.min(end, interval[1]))) {return false;}} else if (end > interval[0] && start < interval[0]) {// interval: ________// newInter: ______...if (!addOverlap(interval[0], Math.min(end, interval[1]))) {return false;}}}intervals.add(new int[] {start, end});return true;}private boolean addOverlap(int start, int end) {Integer floorStart = overlap.floorKey(start);if (floorStart != null && overlap.get(floorStart) > start) {return false;}Integer ceilingStart = overlap.ceilingKey(start);if (ceilingStart != null && ceilingStart < end) {return false;}overlap.put(start, end);return true;}}
733. flood-fill
- 给一个二维int数组,其中包含0-65535的值。给定坐标i,j,和一个newColor,将该cell周围和它值相等的cell都赋值为newColor.
- DFS,直接赋值。需要注意如果原色和newColor相等就直接返回了,否则会stack overflow。12345678910111213141516171819202122class Solution {public int[][] floodFill(int[][] image, int sr, int sc, int newColor) {if (image == null || image.length == 0 || image[0].length == 0 || image[sr][sc] == newColor) { // warning!!return image;}dfs(image, sr, sc, image[sr][sc], newColor);return image;}private int getValue(int[][] image, int i, int j) {return i < 0 || i >= image.length || j < 0 || j >= image[0].length ? -1 : image[i][j];}private void dfs(int[][] image, int i, int j, int oldColor, int newColor) {if (getValue(image, i, j) != oldColor) {return;}image[i][j] = newColor;dfs(image, i - 1, j, oldColor, newColor);dfs(image, i + 1, j, oldColor, newColor);dfs(image, i, j - 1, oldColor, newColor);dfs(image, i, j + 1, oldColor, newColor);}}
734. sentence-similarity
- 给一堆同义词
[("restaurant", "cafe"), ("ratings", "reviews"), ...]
,再给一些queries[("restaurant ratings", "cafe reviews"), ...]
,要求返回每个query里的对应词是否都是synonym。同义词没有传递性。 - 直接把字符串作为key、对应的所有同义词的set作为value存入map,正反都放一次,比如
map.get("restaurant").add("cafe")), map.get("restaurant").add("cafe")
,这样在query的时候就可以直接调用了。123456789101112131415161718192021222324252627282930// 维护一个总的map,每个单词作为key,对等的单词塞入它维护的Set中// 有对称性所以需要正反都加public boolean areSentencesSimilar(String[] words1, String[] words2, String[][] pairs) {if (words1 == null || words2 == null || pairs == null|| words1.length != words2.length) {return false;}Map<String, Set<String>> map = new HashMap<>();for (int i = 0; i < pairs.length; i++) {if (!map.containsKey(pairs[i][0])) {map.put(pairs[i][0], new HashSet<>());}if (!map.containsKey(pairs[i][1])) {map.put(pairs[i][1], new HashSet<>());}map.get(pairs[i][0]).add(pairs[i][1]); // 构建a->bmap.get(pairs[i][1]).add(pairs[i][0]); // 构建b->a}for (int i = 0; i < words1.length; i++) {if (words1[i].equals(words2[i])) {continue;}if (map.get(words1[i]) == null || !map.get(words1[i]).contains(words2[i])) {return false;}}return true;}
735. asteroid-collision
- 给一个int数组,表示原子。正数向右移动,负数向左移动,可能会发生碰撞,如果绝对值相等则会抵消,否则绝对值更大的会把小的给干掉。求碰撞完后的数组。
直接用一个List,正数直接存,负数就需要与list中末尾的元素比较,如果是负就直接push,是正就需要比较看看谁更大。
1234567891011121314151617181920212223242526272829303132333435363738class Solution {public int[] asteroidCollision(int[] asteroids) {if (asteroids == null || asteroids.length == 0) {return asteroids;}List<Integer> list = new ArrayList<>();for (int asteroid : asteroids) {if (asteroid < 0) {boolean needAdd = true;while (!list.isEmpty()) {if (list.get(list.size() - 1) < 0) {break;}if (-asteroid > list.get(list.size() - 1)) {list.remove(list.size() - 1);} else if (-asteroid == list.get(list.size() - 1)) {list.remove(list.size() - 1);needAdd = false;break; // cancelled with each other} else {needAdd = false;break;}}if (needAdd) {list.add(asteroid);}} else {list.add(asteroid);}}int[] ret = new int[list.size()];for (int i = 0; i < ret.length; i++) {ret[i] = list.get(i);}return ret;}}更巧妙的做法是直接用一个数组模拟stack,然后用加法判断是否抵消。每次都强行取栈顶元素出来,如果没有被干掉就再放回去。
12345678910111213141516171819202122232425class Solution {public int[] asteroidCollision(int[] asteroids) {if (asteroids == null || asteroids.length == 0) {return asteroids;}int[] stack = new int[asteroids.length + 1];int size = 1;stack[0] = -1; // 第一个放负数placeholder让pop终止for (int asteroid : asteroids) {while (stack[size - 1] > 0 && asteroid < 0) {int sum = asteroid + stack[size - 1];if (sum > 0) { // 负数被干掉了asteroid = stack[size - 1];} else if (sum == 0) {asteroid = 0;}size--; // 始终pop栈顶}if (asteroid != 0) {stack[size++] = asteroid;}}return Arrays.copyOfRange(stack, 1, size);}}
737. sentence-similarity-ii
- 给一堆同义词
[("restaurant", "cafe"), ("ratings", "reviews"), ...]
,再给一些queries[("restaurant ratings", "cafe reviews"), ...]
,要求返回每个query里的对应词是否都是synonym。注意这些同义词具有传递性,a=b, b=c -> a=c
。 方法一:图论题,每个词都是一个节点,对于每个节点维护一个Set,通过DFS遍历所有可达的节点。
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849// 和前一个版本的区别是这个可以无限传递a=b=c=d...// 还是map维护每个单词等价的单词,但匹配不上的话还得看它的set里所有单词对应的单词是否能匹配到public boolean areSentencesSimilarTwo(String[] words1, String[] words2, String[][] pairs) {if (words1 == null || words2 == null || pairs == null|| words1.length != words2.length) {return false;}// 表示每个单词直接相连的同义词Map<String, Set<String>> map = new HashMap<>();for (int i = 0; i < pairs.length; i++) { // O(N)if (!map.containsKey(pairs[i][0])) {map.put(pairs[i][0], new HashSet<>());}if (!map.containsKey(pairs[i][1])) {map.put(pairs[i][1], new HashSet<>());}map.get(pairs[i][0]).add(pairs[i][1]); // 构建a->bmap.get(pairs[i][1]).add(pairs[i][0]); // 构建b->a}for (int i = 0; i < words1.length; i++) { // O(N*N)if (words1[i].equals(words2[i])) {continue;}if (!map.containsKey(words1[i])) {return false;}if (!dfs(words1[i], words2[i], map, new HashSet<String>())) {return false;}}return true;}private boolean dfs(String start, String end, Map<String, Set<String>> map, Set<String> visited) {if (map.get(start).contains(end)) { // 终止条件:start连接着endreturn true;}visited.add(start);Set<String> neighbors = map.get(start);if (neighbors == null) {return false;}for (String neighbor : neighbors) {if (!visited.contains(neighbor) && dfs(neighbor, end, map, visited)) {return true;}}return false;}方法二:并查集,初始化时每个单词都是自己的root;然后根据同义词关系将前者的老大设为后者。判断句子是否同义时就找两个单词的老大是否相等即可。
123456789101112131415161718192021222324252627282930313233343536373839// 并查集。初始化时每个单词都是自己的root;然后根据同义词关系将前者的老大设为后者。// 判断句子是否同义时就找两个单词的老大是否相等即可public boolean areSentencesSimilarTwo(String[] words1, String[] words2, String[][] pairs) {if (words1.length != words2.length) {return false;}Map<String,String> map = new HashMap<>();for (String[] pair : pairs) { // 开始时每个老大都是自己map.put(pair[0], pair[0]);map.put(pair[1], pair[1]);}for (String[] pair : pairs) {String par1 = findParent(pair[0], map);String par2 = findParent(pair[1], map);if (!par1.equals(par2)) {map.put(par1, par2); // par1的老大设为par2}}for (int i = 0; i < words1.length; i++) {if (words1[i].equals(words2[i])) {continue;}if (!map.containsKey(words1[i]) || !map.containsKey(words2[i])) {return false;}String par1 = findParent(words1[i], map);String par2 = findParent(words2[i], map);if (!par1.equals(par2)) {return false;}}return true;}public String findParent(String str, Map<String,String> map){while (!str.equals(map.get(str))) { // 追溯str的老大str = map.get(str);}return str;}
739. daily-temperatures
- 给一个int数组表示气温,返回一个数组表示该日最短需要多少天才会有更温暖的日子。例如
[73, 74, 75, 71, 69, 72, 76, 73]
输出[1, 1, 4, 2, 1, 1, 0, 0]
。 - 解法:维护一个Stack存放索引,每次读入新的温度时就和栈顶对应的温度比较,如果更高,就弹出并设置该索引处的天数。12345678910111213141516171819202122class Solution {public int[] dailyTemperatures(int[] temperatures) {if (temperatures == null || temperatures.length == 0) {return new int [0];}int[] nextWarmer = new int [temperatures.length];Stack<Integer> stack = new Stack<>();for (int i = 0; i < temperatures.length; i++) {// 比较当前温度和栈顶索引对应温度while (!stack.isEmpty()&& temperatures[i] > temperatures[stack.peek()]) {int index = stack.pop();nextWarmer[index] = i - index;}stack.push(i); // 栈剩下的都比当前大}while (!stack.isEmpty()) {nextWarmer[stack.pop()] = 0; // 其实Java数组原本就是0}return nextWarmer;}}
740. delete-and-earn
- 给一个int数组,每次从中取出一个数字
num
作为score,同时将所有num + 1
和num - 1
抹去。求能得到的最多的score。 - DP。对于每个数字只有两种情况,要么选中成为score,要么忽略等着选中其他的num被抹去,因此维护一个take、一个skip,take的前一位一定是skip(相邻的不可能同时pick)、skip的前一位取不取都可以,选更大者即可。1234567891011121314151617181920class Solution {public int deleteAndEarn(int[] nums) {if (nums == null || nums.length == 0) {return 0;}int[] bucket = new int[10001];for (int num : nums) {bucket[num] += num;}int skip = 0, take = 0;for (int i = 1; i <= 10000; i++) {int takeNew = bucket[i] + skip;int skipNew = Math.max(take, skip);take = takeNew;skip = skipNew;}return Math.max(skip, take);}}
741. cherry-pickup
- 给一个只含有
-1, 0, 1
的NxN
棋盘,从左上角出发只能往右/往下挪到右下角后,再往左/往上挪回起点。-1表示障碍物,1为cherry,求往返之后能收集到的最多的cherry数目。 既然只能往右、往下,那么第一趟所能走过的步数是固定的,就是2N,往回走也是一样。为了简化问题,可以想像成有两个人同时从起点往终点向右、向下挪,收集cherry时如果在同一个格子就只计算一次。采用dfs即可遍历所有可能的row1, col1, row2, col2组合。时间复杂度为
O(N^4)
. 相比之下如果不加memo就是纯暴力对于每个可能都要来一次递归,就是O(4^(N*N))
.12345678910111213141516171819202122232425262728293031class Solution {public int cherryPickup(int[][] grid) {if (grid == null || grid.length == 0 || grid[0].length == 0) {return 0;}int N = grid.length;return Math.max(dfs(grid, 0, 0, 0, 0, new Integer[N][N][N][N]), 0);}private int dfs(int[][] grid, int row1, int col1, int row2, int col2, Integer[][][][] memo) {int N = grid.length;if (row1 >= N || col1 >= N ||row2 >= N || col2 >= N ||grid[row1][col1] == -1 || grid[row2][col2] == -1) {return Integer.MIN_VALUE;}if (memo[row1][col1][row2][col2] != null) {return memo[row1][col1][row2][col2];}if ((row1 == N - 1 && col1 == N - 1) ||(row2 == N - 1 && col2 == N - 1)) {return grid[N - 1][N - 1];}int currCount = row1 == row2 && col1 == col2 ? grid[row1][col1] : grid[row1][col1] + grid[row2][col2];currCount += Math.max(Math.max(dfs(grid, row1 + 1, col1, row2 + 1, col2, memo), dfs(grid, row1 + 1, col1, row2, col2 + 1, memo)),Math.max(dfs(grid, row1, col1 + 1, row2 + 1, col2, memo),dfs(grid, row1, col1 + 1, row2, col2 + 1, memo)));memo[row1][col1][row2][col2] = currCount;return currCount;}}一个优化是,这里row1 + col1 == row2 + col2,因此其实只需要3个变量,三维的memo即可。
742. closest-leaf-in-a-binary-tree
- 给一个二叉树和一个其中必定存在的值k,求这个节点到最近的leaf节点的距离。注意不是BST。
- 纯粹用Tree来思考有点困难,需要抽象成graph来思考:给定一个target点,怎么找最近的满足一定条件的neighbor?BFS。因此先dfs一波找到target节点,同时记录target节点怎么往parent走。然后从target节点开始BFS找最近的leaf节点即可。123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051class Solution {public int findClosestLeaf(TreeNode root, int k) {if (root == null) {return 0;}Map<TreeNode, TreeNode> prevNodeMap = new HashMap<>();Queue<TreeNode> q = new LinkedList<>();Set<TreeNode> visited = new HashSet<>();TreeNode targetNode = dfs(root, k, prevNodeMap);q.offer(targetNode);while (!q.isEmpty()) {TreeNode curr = q.poll();visited.add(curr);if (curr.left == null && curr.right == null) {return curr.val;}if (curr.left != null && !visited.contains(curr.left)) {q.offer(curr.left);}if (curr.right != null && !visited.contains(curr.right)) {q.offer(curr.right);}if (prevNodeMap.containsKey(curr) && !visited.contains(prevNodeMap.get(curr))) {q.offer(prevNodeMap.get(curr));}}return 0;}private TreeNode dfs(TreeNode root, int k, Map<TreeNode, TreeNode> prevNodeMap) {if (root.val == k) {return root;}if (root.left != null) {prevNodeMap.put(root.left, root);TreeNode left = dfs(root.left, k, prevNodeMap);if (left != null) {return left;}}if (root.right != null) {prevNodeMap.put(root.right, root);TreeNode right = dfs(root.right, k, prevNodeMap);if (right != null) {return right;}}return null;}}
743. network-delay-time
- 总共有1、2、3、…、N个节点,给一个times数组表示从node A到node B需要传播的时间,给定起始点K,求最长需要消耗的时间。类似于求最短路,BFS搞定。注意需要更新到达节点所需要的时间。
744. find-smallest-letter-greater-than-target
- 给一个排好序的a-z的char数组,给一个target,求比他大的字符,若是最后一个则wrap到前面如比z大的就是最前面的字符。
- 二分查找,找last occurence,直接返回「下一个」字符。123456789101112131415161718class Solution {public char nextGreatestLetter(char[] letters, char target) {if (letters == null || letters.length == 0) {return target;}int start = -1, end = letters.length;while (end - start > 1) {int mid = start + (end - start) / 2;if (letters[mid] <= target) { // 相等也要把start往后推,找到最后一个occurencestart = mid;} else {end = mid;}}// 不论start处是不是target,直接返回下一位即可return start < letters.length - 1 ? letters[start + 1] : letters[0];}}
745. prefix-and-suffix-search
- 给一个字符串数组,之后给一些query,这些query含有前缀和后缀(0~10个字符),求符合前缀的单词的索引。
- 有多个答案怎么办?返回最后一个出现的。
方法一:encode的方式将每个单词所有可能的前缀后缀组合作为key存入map,索引作为value,这样在query的时候直接再encode一下就可以直接get了。初始化时间复杂度O(N wordLen^2),query时间复杂度O(1),空间占用O(N wordLen^2).
1234567891011121314151617181920class WordFilter {Map<String, Integer> map;public WordFilter(String[] words) {map = new HashMap<>();for (int index = 0; index < words.length; index++) {int wordLen = words[index].length();for (int i = 0; i <= 10 && i <= wordLen; i++) {String front = words[index].substring(0, i);for (int j = 0; j <= 10 && j <= wordLen; j++) {String back = words[index].substring(wordLen - j); // 组成"a...#p.."的keymap.put(front + "#" + back, index);}}}}public int f(String prefix, String suffix) {String key = prefix + "#" + suffix;return map.containsKey(key)? map.get(key) : -1;}}方法二:拆分成两个map,一个专门维护前缀、一个专门维护后缀,value都是索引的List。在query的时候需要把两个List取出来,然后O(N)扫描看看有没有交点。初始化时间复杂度O(N wordLen),query时间复杂度O(N),空间占用O(N wordLen).
123456789101112131415161718192021222324252627282930313233343536373839404142class WordFilter {Map<String, List<Integer>> mapPrefix;Map<String, List<Integer>> mapSuffix;public WordFilter(String[] words) {mapPrefix = new HashMap<>();mapSuffix = new HashMap<>();for (int index = 0; index < words.length; index++) {int wordLen = words[index].length();for (int i = 0; i <= 10 && i <= wordLen; i++) {String front = words[index].substring(0, i);if (!mapPrefix.containsKey(front)) {mapPrefix.put(front, new ArrayList<Integer>());}mapPrefix.get(front).add(index);String back = words[index].substring(wordLen - i);if (!mapSuffix.containsKey(back)) {mapSuffix.put(back, new ArrayList<Integer>());}mapSuffix.get(back).add(index);}}}public int f(String prefix, String suffix) {List<Integer> listPrefix = mapPrefix.get(prefix);List<Integer> listSuffix = mapSuffix.get(suffix);if (listPrefix == null || listSuffix == null) {return -1;}int i = listPrefix.size() - 1, j = listSuffix.size() - 1;while (i >= 0 && j >= 0) { // 因为要返回最后一个,所以要从后往前找交点if (listPrefix.get(i) > listSuffix.get(j)) {i--;} else if (listPrefix.get(i) < listSuffix.get(j)) {j--;} else {return listPrefix.get(i); // 注意不能直接对Integer用==判断相等!}}return -1;}}方法三:直接使用内建函数startsWith和endsWith。初始化时间复杂度O(1),query时间复杂度O(N * wordLen),空间占用O(1).
123456789101112131415class WordFilter {String[] words;public WordFilter(String[] words) {this.words = words;}public int f(String prefix, String suffix) {for (int i = words.length - 1; i >= 0; i--) {if (words[i].startsWith(prefix) && words[i].endsWith(suffix)) {return i;}}return -1;}}
747. largest-number-at-least-twice-of-others
- 给一个只包含
[1, 50]
范围内的数组,求其中最大的、且至少是次大的数两倍的数的index。若不存在则返回-1. 第一次提交遗漏了更新次大数的条件。123456789101112131415161718class Solution {public int dominantIndex(int[] nums) {if (nums == null) {return -1;}int max = 0, secondMax = 0, maxIndex = -1;for (int i = 0; i < nums.length; i++) {if (max <= nums[i]) {secondMax = max;max = nums[i];maxIndex = i;} else if (secondMax < nums[i]) {secondMax = nums[i];}}return (secondMax == 0 && max > secondMax) || (max >= 2 * secondMax) ? maxIndex : -1;}}
748. shortest-completing-word
- 给一个licensePlate,再给一个words数组,求其中最短的字符串使得licensePlate出现过的字母都有。例如
licensePlate = "1s3 PSt"中只用关注S P S T, words = ["step", "steps", "stripe", "stepple"]
,输出steps
。 - licensePlate有哪些字符、确定只关注字母?(-是的)字母大小写?(-忽略大小写)
- 解法:先统计licensePlate中字母出现次数,然后
O(N)
遍历字符串数组,对于每一个单词判断是否包含licensePlate的所有字母(O(26)
),再找最短的返回。12345678910111213141516171819202122232425262728293031323334353637383940414243class Solution {final private int LETTER_NUM = 26;public String shortestCompletingWord(String licensePlate, String[] words) {if (licensePlate == null || words == null) {return "";}// 统计licensePlate里字母的出现次数char[] letterCount = new char [LETTER_NUM];char[] lChar = licensePlate.toLowerCase().toCharArray();for (int i = 0; i < lChar.length; i++) {if (Character.isLetter(lChar[i])) {letterCount[lChar[i] - 'a']++;}}// 对于每个单词判断是否complete,然后找最短的String ans = null;int minLen = Integer.MAX_VALUE;for (String word: words) {if (checkComplete(word, letterCount)) {if (word.length() < minLen) {minLen = word.length();ans = word;}}}return ans;}// 统计当前单词的字母出现次数,然后再一波O(26)和licensePlate的字母Count比较private boolean checkComplete(String word, char[] letterCount) {char[] wChar = word.toCharArray();char[] thisCount = new char [LETTER_NUM];for (int i = 0; i < wChar.length; i++) {thisCount[wChar[i] - 'a']++;}for (int i = 0; i < LETTER_NUM; i++) {if (letterCount[i] > 0 && thisCount[i] < letterCount[i]) {return false;}}return true;}}
749. contain-virus
- 给一个只含有0和1的二维数组,1表示带病毒的细胞、0是健康细胞,每分钟病毒会传染给上下左右直接相邻的健康细胞。假设从最开始可以选择一团带病毒的细胞在他们和健康细胞之间建隔离带,
0|1
这样会消耗一个隔离带。每次选择相邻健康细胞最多的一团进行隔离,然后经过一分钟剩下部分完成感染后在继续建隔离带。求这样的策略下最终需要多少隔离带(结果可能是完成了隔离、抑或全部都被感染)。 - 本质上就是DFS。先找出所有团簇,然后相邻健康细胞取最多的进行隔离,然后模拟感染扩散,然后再循环找团簇隔离。。。1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677class Solution {class Region {Set<Integer> infectedSet, uninfectedSet;int wallCount;Region() {this.infectedSet = new HashSet<>();this.uninfectedSet = new HashSet<>();this.wallCount = 0;}}public int containVirus(int[][] grid) {if (grid == null || grid.length == 0) {return 0;}int rows = grid.length, cols = grid[0].length, retVal = 0;while (true) {LinkedList<Region> regions = new LinkedList<>();boolean[][] visited = new boolean[rows][cols];for (int row = 0; row < rows; row++) {for (int col = 0; col < cols; col++) {// 找到未被访问过的染病细胞if (grid[row][col] == 1 && !visited[row][col]) {Region region = new Region();dfs(grid, row, col, visited, region);// 只有当它有相邻的健康细胞才可能继续传染if (!region.uninfectedSet.isEmpty()) {regions.add(region);}}}}if (regions.isEmpty()) {break;}regions.sort((a, b) -> b.uninfectedSet.size() - a.uninfectedSet.size());Region most = regions.pollFirst();retVal += most.wallCount;for (int point : most.infectedSet) {int row = point / cols;int col = point % cols;grid[row][col] = -1;}while (!regions.isEmpty()) {Region region = regions.pollFirst();for (int point : region.uninfectedSet) {int row = point / cols;int col = point % cols;grid[row][col] = 1;}}}return retVal;}private int[] dirs = new int[] {1, 0, -1, 0, 1};private void dfs(int[][] grid, int row, int col, boolean[][] visited, Region region) {int rows = grid.length, cols = grid[0].length;if (row < 0 || row >= rows || col < 0 || col >= cols || grid[row][col] < 0) {return;}if (grid[row][col] == 1) {if (!visited[row][col]) {visited[row][col] = true;region.infectedSet.add(row * cols + col);for (int i = 0; i < 4; i++) {dfs(grid, row + dirs[i], col + dirs[i + 1], visited, region);}}} else {region.uninfectedSet.add(row * cols + col);region.wallCount++;}}}
750. number-of-corner-rectangles
- 给一个只含有0和1的二维数组,求其中四个1所能组成矩形的个数。矩形的边必须横、竖两个方向。
- 扫描线的思路,取两个行,同时扫竖线,统计同时出现1的pair数作为竖线,然后把这些竖线组合一下即可。123456789101112131415161718192021class Solution {// 两行一起往右挪找是否有同时为1的,然后根据该平行线的pair数简单排列组合就可以了public int countCornerRectangles(int[][] grid) {if (grid == null || grid.length == 0 || grid[0].length == 0) {return 0;}int ans = 0;for (int i = 0; i < grid.length - 1; i++) {for (int j = i + 1; j < grid.length; j++) {int count = 0;for (int k = 0; k < grid[0].length; k++) {if (grid[i][k] == 1 && grid[j][k] == 1) { // 两平行线同一列同为1count++;}}ans += count * (count - 1) / 2; // combination}}return ans;}}
752. open-the-lock
- 假如有一个锁头从
0000
开始转,每次只能只能转四位中的一位,给一个String数组表示这些数字不可以转到,给一个target表示最终开锁的密码。求最短需要多少次才能开锁,不可能打开则返回-1。 这种最短路径问题就想到了BFS,不过需要小心的是target本身不可达以及起始点就不可达的情况。
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354class Solution {public int openLock(String[] deadends, String target) {if (deadends == null || deadends.length == 0) {return 0;}Set<String> visited = new HashSet<>();for (String deadend : deadends) {visited.add(deadend);}if (visited.contains(target)) {return -1;}Queue<StringBuilder> q = new LinkedList<>();StringBuilder start = new StringBuilder("0000");if (visited.contains(start.toString())) {return -1;}q.offer(start);visited.add(start.toString());int count = 0;while (!q.isEmpty()) {int size = q.size();count++;while (size-- > 0) {StringBuilder curr = q.poll();for (int i = 0; i < 4; i++) {char c = curr.charAt(i);for (int j = -1; j <= 1; j += 2) {if (c == '0' && j == -1) {curr.setCharAt(i, '9');} else if (c == '9' && j == 1) {curr.setCharAt(i, '0');} else {curr.setCharAt(i, (char)(c + j));}String currStr = curr.toString();if (!visited.contains(currStr)) {if (currStr.equals(target)) {return count;}q.offer(new StringBuilder(currStr));visited.add(currStr);}}curr.setCharAt(i, c);}}}return -1;}}以上是basic的BFS,如果要提速,可以进行双向的BFS,也就是维护两个Set,一个begin作为开始、一个end作为结束,每次switch角色,搞完begin就将end补过来、将当前begin的邻居点作为新的end。
759. employee-free-time
- 给一个List of List,每一个子List存放每个employee的工作时间,求所有employee的共同空闲时间。没有则返回空List。
- 直接将List flatten,然后按照工作开始时间从小到大排序,再往后依次取工作时间,若当前开始时间比之前的结束时间长,说明出现了空闲时间;否则需要更新prev的end,保证prev的结束时间cover到所有结束时间,即取max。12345678910111213141516171819public List<Interval> employeeFreeTime(List<List<Interval>> schedule) {List<Interval> ans = new ArrayList<>();if (schedule == null || schedule.size() == 0) {return ans;}List<Interval> workingTimes = new ArrayList<>();schedule.forEach(e -> workingTimes.addAll(e));Collections.sort(workingTimes, (a, b) -> a.start - b.start);Interval prev = workingTimes.get(0);for (int i = 1; i < workingTimes.size(); i++) {if (workingTimes.get(i).start > prev.end) {ans.add(new Interval(prev.end, workingTimes.get(i).start));prev = workingTimes.get(i);} else {prev.end = Math.max(prev.end, workingTimes.get(i).end);}}return ans;}
760. find-anagram-mappings
- 给两个数组,求对应出现的位置。skip.
763. partition-labels
- 给一个字符串,尝试将它进行partition使得每个字符至多只出现在一个partition,划分处尽量多的partition,求每个partition的长度。
- 方法一:LinkedHashMap搞定。统计每个字符出现的初始位置和最后一次出现的位置,然后遍历,当前后无法相连则说明这是一个新的partition。
- 方法二:不需要对每一个字母同时存放start和end,而是对于一个partition维护start和end。遍历字符串时若当前索引正是当前字符的最后一次出现的索引,且到达了当前partition的end,说明partition结束;否则需要适当更新end。1234567891011121314151617181920212223242526272829class Solution {public List<Integer> partitionLabels(String S) {List<Integer> ans = new ArrayList<>();if (S == null || S.length() == 0) {return ans;}int[] lastOccurance = new int[26];for (int i = 0; i < S.length(); i++) {lastOccurance[S.charAt(i) - 'a'] = i;}int start = 0, end = 0;for (int i = 0; i < S.length(); i++) {int index = S.charAt(i) - 'a';if (lastOccurance[index] != i) {if (lastOccurance[index] > end) {end = lastOccurance[index]; // 若有更靠后的索引则更新end}} else {if (i == end) {ans.add(end - start + 1);end = i + 1;start = i + 1; // start只在每个partition开始时更新一次}}}return ans;}}
764. largest-plus-sign
- 给定N表示棋盘的长和宽,默认棋盘中每个cell都是1,再给一些坐标表示该处是0。求棋盘中最大的加号的长度。
- 可以利用grid本身记录上下左右四个方向最长延伸出去多长。最原始的想法是每个方向都维护一个二维数组专门记录到该cell的最长长度是多少,不过经过改进可以将所有的计算都合并到一个grid中完成。开始时初始化每个cell的长度都为N,然后直接从前、后、上、下同时更新,不过每一步循环中更新的其实是四个不同的cell,最后循环结束时每个cell都会被更新四次。时间复杂度O(N^2)。123456789101112131415161718192021222324252627282930313233class Solution {public int orderOfLargestPlusSign(int N, int[][] mines) {if (N < 1) {return 0;}int[][] grid = new int [N][N];for (int i = 0; i < N; i++) {Arrays.fill(grid[i], N);}for (int[] mine : mines) {grid[mine[0]][mine[1]] = 0;}for (int i = 0; i < N; i++) {for (int j = 0, k = N - 1, l = 0, r = 0, t = 0, b = 0; j < N; j++, k--) {// 分别统计左右、上下有多少延伸出去的长度,每个cell会被访问到4次,每次都取mingrid[i][j] = Math.min(grid[i][j], l = grid[i][j] == 0 ? 0 : l + 1);grid[i][k] = Math.min(grid[i][k], r = grid[i][k] == 0 ? 0 : r + 1);grid[j][i] = Math.min(grid[j][i], t = grid[j][i] == 0 ? 0 : t + 1);grid[k][i] = Math.min(grid[k][i], b = grid[k][i] == 0 ? 0 : b + 1);}}int ans = 0;for (int i = 0; i < N; i++) {for (int j = 0; j < N; j++) {ans = Math.max(grid[i][j], ans);}}return ans;}}
765. couples-holding-hands
- 给一个int数组表示每个位置坐的人,假设0-1, 2-3, 4-5…是一对,问最少通过几次交换座位可以让没对情侣都相邻而坐��
- 可以用greedy的办法,每发现一个坐错位置的人,就让他和应该在这个位置的人swap一次。123456789101112131415161718192021class Solution {public int minSwapsCouples(int[] row) {if (row == null || row.length == 0 || row.length % 2 != 0) {return 0;}int[] pos = new int[row.length];for (int i = 0; i < row.length; i++) {pos[row[i]] = i;}int count = 0;for (int i = 0; i < row.length; i += 2) {int expected = row[i] % 2 == 0 ? row[i] + 1 : row[i] - 1;if (row[i + 1] != expected) {pos[row[i + 1]] = pos[expected];row[pos[expected]] = row[i + 1];count++;}}return count;}}
767. reorganize-string
- 给一个只含有小写字母的字符串,将这些字符重新排列使得相邻字母不同,若不存在则返回””;
- greedy,先统计每一个字符出现的次数,存入priorityqueue使得次数多的先取,取后若仍有剩余则需要更新次数并重新放回pq。注意若当前取出的次数最多的字母是上一次append的,则需要取第二多的字符,若没有后续字符说明没法这么存,返回””即可。
768. max-chunks-to-make-sorted-ii
- 给一个int数组,将这个数组分成若干个chunk后在每个chunk内部排序之后拼接能得到sorted的数组,求最多划分成多少个这样的chunk。
- 形成chunk的条件是「chunk中最大值小于等于右侧所有数的最小值」。因此维护两个数组存放当前位置的左侧最大值和右侧最小值即可。123456789101112131415161718192021222324252627class Solution {public int maxChunksToSorted(int[] arr) {if (arr == null || arr.length == 0) {return 0;}int[] maxOfLeft = new int[arr.length]; // 左侧的最大值maxOfLeft[0] = arr[0];for (int i = 1; i < arr.length; i++) {maxOfLeft[i] = Math.max(maxOfLeft[i - 1], arr[i]);}int[] minOfRight = new int[arr.length]; // 右侧的最小值minOfRight[arr.length - 1] = arr[arr.length - 1];for (int i = arr.length - 2; i >= 0; i--) {minOfRight[i] = Math.min(minOfRight[i + 1], arr[i]);}int count = 0;for (int i = 0; i < arr.length - 1; i++) {// 当左侧最大值都小于右侧最小值,说明当前位之前都可以自己排个序了if (maxOfLeft[i] <= minOfRight[i + 1]) {count++;}}return count + 1;}}
769. max-chunks-to-make-sorted
- 给一个int数组,其中含有元素
[0, 1, ..., arr.length - 1]
(顺序不一定是这样的,是一个permutation),将这个数组分成若干个chunk后在每个chunk内部排序之后拼接能得到sorted的数组,求最多划分成多少个这样的chunk。如[4,3,2,1,0]
必须整个作为一个chunk排序,[1,0,2,3,4]
则分成[1, 0], [2], [3], [4]
来排序。 - 既然元素和index是能对应填充的,考虑他们之间的关系。要想形成chunk,必须chunk的最大值在最右侧index的左侧,维护一个max即可。123456789101112131415class Solution {public int maxChunksToSorted(int[] arr) {if (arr == null || arr.length == 0) {return 0;}int count = 0, max = 0;for (int i = 0; i < arr.length; i++) {max = Math.max(max, arr[i]);if (i == max) {count++;}}return count;}}
771. jewels-and-stones
- 简单的bucket计数,pass。
773. sliding-puzzle
- 给一个2x3的board,只含有数字0-5。只有0可以与相邻的元素swap,问最少经过多少swap能得到
[[1, 2, 3], [5, 6, 0]]
. - 转换为BFS问题,每个状态就是一个可达的节点,最关键的其实就是看
0
能否和周围数字swap。从当前节点到下一个节点,就是找出所有0
与邻居swap之后的状态。这里还有一个trick就是将二维数组转化成一维的string,直接通过字符串比较高效很多。时间复杂度为O(row * col * (row * col)!)
.123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657class Solution {private int[][] neighbors = new int[][] {{ 1, 3 },{ 0, 4, 2 },{ 1, 5 },{ 0, 4 },{ 3, 1, 5 },{ 4, 2 }};private final String target = "123450";private final int rows = 2, cols = 3;public int slidingPuzzle(int[][] board) {if (board == null) {return -1;}StringBuilder sb = new StringBuilder();for (int row = 0; row < rows; row++) {for (int col = 0; col < cols; col++) {sb.append(board[row][col]);}}Queue<String> q = new LinkedList<>();Set<String> visited = new HashSet<>();q.offer(sb.toString());int steps = 0;while (!q.isEmpty()) {int size = q.size();while (size-- > 0) {String curr = q.poll();if (curr.equals(target)) {return steps;}int index0 = 0;for (char c : curr.toCharArray()) {if (c == '0') {break;}index0++;}for (int neighbor : neighbors[index0]) {StringBuilder next = new StringBuilder(curr);next.setCharAt(index0, curr.charAt(neighbor));next.setCharAt(neighbor, curr.charAt(index0));String nextStr = next.toString();if (!visited.contains(nextStr)) {visited.add(nextStr);q.add(nextStr);}}}steps++;}return -1;}}
775. global-and-local-inversions
- 给一个长度为N、元素值为
0 ~ N-1
的int数组,判断它的global inversion数量和local inversion数量是否相等。global表示任意一对儿数字逆序了、local表示相邻前后数字逆序了。 - 本质上就是判断是否有「非相邻前后数字」逆序的情况。直接loop判断
A[i] - i
的绝对值即可,只要他们差值大于1就说明有跨度超过1的逆序对了。12345678910111213class Solution {public boolean isIdealPermutation(int[] A) {if (A == null || A.length == 0) {return true;}for (int i = 0; i < A.length; i++) {if (Math.abs(A[i] - i) > 1) {return false;}}return true;}}
776. split-bst
- 给一个BST和一个value,这个value不一定存在于BST中,要求以这个value为临界点将BST分成小于等于&大于两部分。
- 在split的时候比较节点与value,若value更大则split点会出现在右子树且split后会得到两个split之后的子树,需要将小于等于的那个子树拼接到当前节点的右侧。若value不大于当前节点,则需要到左子树去搜索split点,同样需要将split之后较大的那个子树拼接到当前子树的左侧。12345678910111213141516public TreeNode[] splitBST(TreeNode root, int V) {if (root == null) {return new TreeNode[] {null, null};}if (root.val <= V) {TreeNode[] splitted = splitBST(root.right, V);root.right = splitted[0];splitted[0] = root;return splitted;} else {TreeNode[] splitted = splitBST(root.left, V);root.left = splitted[1];splitted[1] = root;return splitted;}}
777. swap-adjacent-in-lr-string
- 给两个字符串
start
和end
,其中类似RX
的可以转化成XR
,XL
的可以转化成LX
,但是RL
就无法挪动了。判断是否可以成功转化。 - 首先需要判断两个字符串中L和R的相对位置是否一致,如果可以match上,再逐步判断
start
中的R
是否都再end
中R
的前面、start
中的L
是否都再end
中L
的后面。12345678910111213141516171819202122232425262728293031323334class Solution {public boolean canTransform(String start, String end) {if (start == null || end == null) {return false;}if (start.length() != end.length()) {return false;}if (!start.replaceAll("X", "").equals(end.replaceAll("X", ""))) {return false;}int startIndex = 0, endIndex = 0;while (startIndex < start.length()) {while (startIndex < start.length() && start.charAt(startIndex) == 'X') {startIndex++;}while (endIndex < end.length() && end.charAt(endIndex) == 'X') {endIndex++;}if (startIndex == start.length()) {return true;}if (start.charAt(startIndex) == 'R' && endIndex < startIndex) {return false;}if (start.charAt(startIndex) == 'L' && startIndex < endIndex) {return false;}startIndex++;endIndex++;}return true;}}
778. swim-in-rising-water
- 给一个
NxN
的grid,其中的值为[1,…, N*N]。如果当前时间t > val则可以瞬间到达该坐标,求最小的t使得从左上角能移动到右下角。 方法一:最朴素的想法,二分查找来“猜”最终答案,每次对猜的答案进行DFS/BFS验证是否可以从左上到右下,若可以到达,则缩小上界以尽可能找最小值,若不能到达说明t不够,提升下界。时间复杂度为
log(N*N)*N*N
.12345678910111213141516171819202122232425262728293031323334353637383940414243444546class Solution {public int swimInWater(int[][] grid) {if (grid == null || grid.length == 0 || grid[0].length == 0) {return 0;}int left = -1, right = grid.length * grid.length;while (right - left > 1) {int mid = left + (right - left) / 2;if (canGo(grid, mid)) {right = mid;} else {left = mid;}}return right;}private final int[] dir = {0, 1, 0, -1, 0};private boolean canGo(int[][] grid, int currTime) {Queue<int[]> q = new LinkedList<>();int rows = grid.length, cols = grid[0].length;boolean[][] visited = new boolean[rows][cols];if (currTime >= grid[0][0]) {q.offer(new int[] {0, 0});visited[0][0] = true;}while (!q.isEmpty()) {int[] curr = q.poll();for (int i = 0; i < 4; i++) {int rowNext = curr[0] + dir[i];int colNext = curr[1] + dir[i + 1];if (rowNext >= 0 && rowNext < rows &&colNext >= 0 && colNext < cols &&!visited[rowNext][colNext] &&currTime >= grid[rowNext][colNext]) {if (rowNext == rows - 1 && colNext == cols - 1) {return true;} else {q.offer(new int[] {rowNext, colNext});visited[rowNext][colNext] = true;}}}}return false;}}方法二:贪心算法,使用priorityqueue取代queue进行BFS,每次push的时候存入当前坐标已经到达当前坐标所需的时间,每次从pq中取坐标都可以找到当前可达范围内的最小的时间。时间复杂度和上面其实一样,毕竟需要在pq中维护顺序
O(NlogN)
,也就是这里的N*N*log(N*N)
。1234567891011121314151617181920212223242526272829303132class Solution {public int swimInWater(int[][] grid) {if (grid == null || grid.length == 0 || grid[0].length == 0) {return 0;}PriorityQueue<int[]> pq = new PriorityQueue<>((a, b) -> a[2] - b[2]);int N = grid.length;boolean[][] visited = new boolean[N][N];pq.offer(new int[] {0, 0, grid[0][0]});while (!pq.isEmpty()) {int[] curr = pq.poll();for (int i = 0; i < 4; i++) {int rowNext = curr[0] + dir[i];int colNext = curr[1] + dir[i + 1];int currTime = curr[2];if (rowNext < 0 || rowNext >= N ||colNext < 0 || colNext >= N ||visited[rowNext][colNext]) {continue;}visited[rowNext][colNext] = true;int newTime = Math.max(currTime, grid[rowNext][colNext]);if (rowNext == N - 1 && colNext == N - 1) {return newTime;}pq.offer(new int[] {rowNext, colNext, newTime});}}return 0;}private final int[] dir = {0, 1, 0, -1, 0};}
785. is-graph-bipartite
- 给一个数组,每个index对应着该node的所有邻接点。问这个graph能否只用两个颜色给node上色使得相邻两个点的颜色都不一样。
方法一:DFS。对于每一个点都一波直接深度搜索上色,用一个数组存储上色状态,0表示未访问过,-1和1分别表示两个颜色,在dfs时对于当前的点若已经访问过就判断是否符合当前给定的颜色,若未访问则直接上色并DFS到它所有邻接点。需要注意可能有若干独立的cluster,因此不能一次DFS搜索就结束了,而是需要一个循环保证对所有未访问过的点都进行一次DFS。
1234567891011121314151617181920212223242526class Solution {public boolean isBipartite(int[][] graph) {if (graph == null || graph.length == 0) {return true;}int[] colors = new int[graph.length];for (int i = 0; i < graph.length; i++) {if (colors[i] == 0 && !checkColor(graph, colors, -1, i)) {return false;}}return true;}private boolean checkColor(int[][] graph, int[] colors, int currColor, int currNode) {if (colors[currNode] != 0) {return colors[currNode] == currColor;}colors[currNode] = currColor;for (int neighbor : graph[currNode]) {if (!checkColor(graph, colors, -currColor, neighbor)) {return false;}}return true;}}方法二:BFS,还是用queue存放所有邻接点然后逐一上色。
1234567891011121314151617181920212223242526272829303132333435363738class Solution {public boolean isBipartite(int[][] graph) {if (graph == null || graph.length == 0) {return true;}int[] colors = new int[graph.length];for (int i = 0; i < graph.length; i++) { // 只check没有访问过的点,即各个独立的clusterif (colors[i] == 0 && !checkColor(graph, colors, -1, i)) {return false;}}return true;}private boolean checkColor(int[][] graph, int[] colors, int currColor, int currNode) {if (colors[currNode] != 0) {return colors[currNode] == currColor;}Queue<Integer> q = new LinkedList<>();int color = currColor;q.offer(currNode);while (!q.isEmpty()) {int size = q.size();while (size-- > 0) {int node = q.poll();if (colors[node] != 0) {if (colors[node] != color) return false;} else {colors[node] = color;}for (int neighbor : graph[node]) {if (colors[neighbor] == 0) q.offer(neighbor); // 避免回头路}}color = -color;}return true;}}
787. cheapest-flights-within-k-stops
- 给一组city和航班信息,在最多stop k次的情况下求从src到dst最便宜的航班价格。
- 经典grpah最短路问题,首先用Map记录每一个city的邻居city航班,然后从src出发BFS,维护一个minPrice数组,发现更低价格时就更新对应city的到达所需价格。在BFS过程中,每扩散一轮就算作stop一次,利用一个flightCount保证最多停k次。123456789101112131415161718192021222324252627282930313233343536373839class Solution {public int findCheapestPrice(int n, int[][] flights, int src, int dst, int K) {if (flights == null || flights.length == 0) {return 0;}Map<Integer, List<int[]>> flightMap = getGraph(flights);int[] minPrices = new int[n];Arrays.fill(minPrices, Integer.MAX_VALUE);Queue<int[]> q = new LinkedList<>();int flightCount = K + 1;q.offer(new int[] {src, 0});while (!q.isEmpty() && flightCount-- > 0) {int size = q.size();while (size-- > 0) {int[] currCity = q.poll();List<int[]> neighbors = flightMap.get(currCity[0]);if (neighbors == null) {continue;}for (int[] neighbor : neighbors) {int price = currCity[1] + neighbor[2];if (price < minPrices[neighbor[1]]) {minPrices[neighbor[1]] = price;q.offer(new int[] {neighbor[1], price});}}}}return minPrices[dst] == Integer.MAX_VALUE ? -1 : minPrices[dst];}private Map<Integer, List<int[]>> getGraph(int[][] flights) {Map<Integer, List<int[]>> flightMap = new HashMap<>();for (int[] flight : flights) {flightMap.putIfAbsent(flight[0], new ArrayList<int[]>());flightMap.get(flight[0]).add(flight);}return flightMap;}}
788. rotated-digits
- 给一个数字N,求1~N范围内将各位数字上下反转180度后能得到与它本身不同数字的个数。
- 当前数字需要依赖之前几位数字的情况,因此考虑简单的DP。第一想法是维护一个map存反转信息,这样确实更直接明白,但速度会慢很多。其实可以直接用if判断。1234567891011121314151617181920212223242526272829303132class Solution {private Map<Integer, Integer> map = Stream.of(new Integer[][] {{0, 0},{1, 1},{2, 5},{5, 2},{6, 9},{8, 8},{9, 6}}).collect(Collectors.toMap(data -> data[0], data -> data[1]));public int rotatedDigits(int N) {int[] dp = new int[N + 1]; // -1不合法,0合法相同数字,1不同数字int count = 0;for (int i = 1; i <= N; i++) {int newDigit = i % 10;int prevNum = i / 10;if (map.containsKey(newDigit)) {if (map.get(newDigit) == newDigit) {dp[i] = dp[prevNum];} else {dp[i] = dp[prevNum] >= 0 ? 1 : -1;}} else {dp[i] = -1;}if (dp[i] == 1) {count++;}}return count;}}
791. custom-sort-string
- 给两个String,S只含有不重复的小写字母表示custom定义的顺序,需要将T按照这个给定顺序进行排序,对于没有出现过的字母随便放哪里都可以。
- 方法一:遍历S,在循环内层遍历T,遇到当前字符就往前swap,时间复杂度O(N^2).
- 方法二:木桶排序,既然只会出现小写字母,就用26个木桶统计出现个数,然后遍历S取处相应的append即可,最后再把S中没有的字母拼接到最后即可。时间O(N).
792. number-of-matching-subsequences
- 给一个源字符串S和一个字符串数组,求数组中有多少个是S的子序列。
方法一:朴素的逐个判断。缓存入seq和nonSeq两个集合,遍历数组中每个字符串,判断是seq,对应存入两个set即可。这两个set并不是必须的,而是提速的,作为缓存来应付重复call的情况。
12345678910111213141516171819202122232425262728293031323334class Solution {public int numMatchingSubseq(String S, String[] words) {if (S == null || S.length() == 0 || words == null || words.length == 0) {return 0;}int count = 0;Set<String> isSeqSet = new HashSet<>();Set<String> notSeqSet = new HashSet<>();for (String word : words) {if (isSeqSet.contains(word)) {count++;} else if (notSeqSet.contains(word)) {continue;} else if (isSubSeq(S, word)) {isSeqSet.add(word);count++;} else {notSeqSet.add(word);}}return count;}private boolean isSubSeq(String source, String target) {char[] targetCharArray = target.toCharArray();int index = -1;for (char targetChar : targetCharArray) {index = source.indexOf(targetChar, index + 1);if (index == -1) {return false;}}return true;}}方法二:为了加速在源字符串中的查找,将每个字母在S中出现的索引存入TreeSet中,每次查找时直接从TreeSet中拿大于等于当前SIndex的索引,如果找不到就直接false。
123456789101112131415161718192021222324252627282930313233343536373839404142class Solution {public int numMatchingSubseq(String S, String[] words) {if (S == null || S.length() == 0 || words == null || words.length == 0) {return 0;}TreeSet<Integer>[] bucket = buildBucket(S);int count = 0;for (String word : words) {if (isMatch(word, bucket)) {count++;}}return count;}private TreeSet<Integer>[] buildBucket(String S) {TreeSet<Integer>[] bucket = new TreeSet[26];char[] charArray = S.toCharArray();for (int i = 0; i < charArray.length; i++) {int index = charArray[i] - 'a';if (bucket[index] == null) {bucket[index] = new TreeSet<>();}bucket[index].add(i);}return bucket;}private boolean isMatch(String word, TreeSet<Integer>[] bucket) {int indexInS = -1;char[] charArray = word.toCharArray();for (int i = 0; i < charArray.length; i++) {if (bucket[charArray[i] - 'a'] == null) {return false;}Integer index = bucket[charArray[i] - 'a'].ceiling(indexInS);if (index == null) {return false;}indexInS = index + 1;}return true;}}
794. valid-tic-tac-toe-state
- 给一个3x3的棋盘,判断是否是一个合法的OOXX状态。默认X先走,三连就退出。
- 和之前design-tic-tac-toe类似,X持续加,O持续减。最后来几个if判断即可。没啥意思。123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869class Solution {public boolean validTicTacToe(String[] board) {if (board == null || board.length != 3) {return false;}int[] rows = new int[3];int[] cols = new int[3];int diag1 = 0, diag2 = 0;int countX = 0, countO = 0;boolean winX = false, winO = false;for (int row = 0; row < 3; row++) {for (int col = 0; col < 3; col++) {if (board[row].charAt(col) == 'X') {countX++;rows[row] += 1;cols[col] += 1;if (row == col) {diag1 += 1;}if (row + col == 2) {diag2 += 1;}} else if (board[row].charAt(col) == 'O') {countO++;rows[row] -= 1;cols[col] -= 1;if (row == col) {diag1 -= 1;}if (row + col == 2) {diag2 -= 1;}}if (rows[row] == 3) {winX = true;} else if (rows[row] == -3) {winO = true;}if (cols[col] == 3) {winX = true;} else if (cols[col] == -3) {winO = true;}if (diag1 == 3) {winX = true;} else if (diag1 == -3) {winO = true;}if (diag2 == 3) {winX = true;} else if (diag2 == -3) {winO = true;}}}if (countX != countO && countX != countO + 1) {return false;}if ((winX && winO) || (winO && countX != countO) ||(winX && countX != countO + 1)) {return false;}return true;}}
795. number-of-subarrays-with-bounded-maximum
- 给一个数组和一个范围[L, R],求该数组有多少个子数组,使得子数组的最大值落在[L, R]中。
首先想到DP,
dp[i][j]
表示从i到j的子数组的最大值,O(N^2)遍历的时候就可以顺便判断。1234567891011121314151617181920212223class Solution {public int numSubarrayBoundedMax(int[] A, int L, int R) {if (A == null || A.length == 0) {return 0;}int len = A.length, ans = 0;int[][] dp = new int[len][len];for (int i = 0; i < len; i++) {for (int j = i; j < len; j++) {if (i == j) {dp[i][j] = A[j];} else {dp[i][j] = Math.max(dp[i][j - 1], A[j]);}if (dp[i][j] >= L && dp[i][j] <= R) {ans++;}}}return ans;}}follow-up:进一步优化,不使用额外的空间,同时时间复杂度降到O(N).
- 子数组的总量是一定的,既然求的是range,那首先求所有数都不超过max的子数组数量,然后求所有数都不超过(min - 1)的子数组的数量,两个一减就得到了在[min, max]之间的子数组的数量。12345678910111213141516class Solution {public int numSubarrayBoundedMax(int[] A, int L, int R) {if (A == null || A.length == 0) {return 0;}return countSubarray(A, R) - countSubarray(A, L - 1);}private int countSubarray(int[] nums, int max) {int curr = 0, total = 0;for (int num : nums) {curr = num <= max ? curr + 1 : 0;total += curr;}return total;}}
796. rotate-string
- 给两个String,判断A能否通过shift变成B。
经典。观察可以得出A拼接上自身之后必须包含B才能保证shift得到B。时间复杂度
O(N^2)
.123public boolean rotateString(String A, String B) {return A != null && B != null && A.length() == B.length() && (A + A).contains(B);}既然涉及在一个str中找另一个str,那么KMP也可以应用。首先对B做一个KMP存放common prefix/suffix,然后在
S = A+A
中对B进行查找,注意S的索引只会一路向前移动,B的索引会在没匹配上时就能回退到”上一个同样前缀最后出现的位置”。12345678910111213141516171819202122232425262728293031323334353637383940class Solution {public boolean rotateString(String A, String B) {if (A == null || B == null || A.length() != B.length()) {return false;}if (A.equals(B)) {return true;}// 对目标B构建fail function的数组int len = B.length();int[] dp = new int[len];for (int i = 1; i < len; i++) {int j = dp[i - 1];while (j > 0 && B.charAt(i) != B.charAt(j)) {j = dp[j - 1];}if (B.charAt(i) == B.charAt(j)) {j++;}dp[i] = j;}String s = A + A;int j = 0;for (int i = 0; i < s.length(); i++) {// 尝试比对B和S,不匹配就直接从fail function对应的前一段出现的相同前缀尾巴处继续比较,直到匹配或者耗尽可用前缀while (j > 0 && B.charAt(j) != s.charAt(i)) {j = dp[j - 1];}if (B.charAt(j) == s.charAt(i)) {j++;}if (j == len) {return true;}}return false;}}