LeetCode 1-100
LeetCode Problems 1-100.
1. Two Sum
https://leetcode.com/problems/two-sum/
我们需要保证nums[i]和target - nums[i]都在数组中,因此使用哈希表存储循环遍历时每个数nums[i]以及下标i的值,并检查target - nums[i]是否也在map中,如果存在,就可以直接提取两个数的位置得到结果。
class Solution { public int[] twoSum(int[] nums, int target) { if (nums == null || nums.length == 0) { return new int[] {}; }
int[] result = new int[2];
// <Element, Index> Map<Integer, Integer> map = new HashMap<>(); for (int i = 0; i < nums.length; i++) { if (map.containsKey(target - nums[i])) { result[0] = i; result[1] = map.get(target - nums[i]); return result; } map.put(nums[i], i); }
return result; }}2. Add Two Numbers
https://leetcode.com/problems/add-two-numbers/
加法运算是一个从低位到高位的循环过程,只要下列3个条件之一满足,就会一直持续下去:
-
第一个数某位不为空
-
第二个数某位不为空
-
进位不为0
因此我们创建一个新链表,按照上述条件,对l1和l2逐位计算相加,得到结果后创建一个新节点插入到链表中。
/** * Definition for singly-linked list. * public class ListNode { * int val; * ListNode next; * ListNode() {} * ListNode(int val) { this.val = val; } * ListNode(int val, ListNode next) { this.val = val; this.next = next; } * } */class Solution { public ListNode addTwoNumbers(ListNode l1, ListNode l2) { if (l1 == null) { return l2; } if (l2 == null) { return l1; }
ListNode dummyHead = new ListNode(0); // dummy head of new list ListNode cur = dummyHead; // traverse new list
int carry = 0;
// still have digits while (l1 != null || l2 != null || carry != 0) { int val = 0;
int val1 = l1 != null ? l1.val : 0; int val2 = l2 != null ? l2.val : 0;
val = (val1 + val2 + carry) % 10; carry = (val1 + val2 + carry) / 10;
// insert a new node cur.next = new ListNode(val); cur = cur.next;
// move pointers forward if (l1 != null) { l1 = l1.next; } if (l2 != null) { l2 = l2.next; } }
return dummyHead.next; }}3. Longest Substring Without Repeating Characters
https://leetcode.com/problems/longest-substring-without-repeating-characters/
我们需要一个map记录每个字符最后一次出现的位置。由于s中的字符已经确定是英文字母或者符号,因此可以使用定长数组模拟map,提高运行效率。
定义两个指针begin和end,begin指向最长不重复子串起点,尾指针用于遍历s,判断当前字符是否于map中。我们的目标就是尾指针完成遍历后,通过两个指针的最大距离差值,得到最长的不重复子串长度。
遍历时需要注意,在遇到重复字符时,begin有两种更新方式:
-
这个字符上一次出现位置的后面(+1),这是满足不重复的前提下的最小取值
-
保持不变,因为有可能这个重复字符上一次出现的位置相当靠前,在
[map[currentChar] + 1, begin]中间出现了其他的重复字符
具体选择哪种方式更新,只需比较当前begin和上一次出现位置+1的大小。
class Solution { public int lengthOfLongestSubstring(String s) { if (s.length() == 0) { return 0; }
char[] array = s.toCharArray();
int begin, end = 0; // two pointers to mark the longest substring int result = 0; // longest substring's length
int[] map = new int[128]; // <Character, Index> Arrays.fill(map, -1);
for (begin = 0, end = 0; end < s.length(); end++) { char currentChar = array[end];
// if this is a repeating character, we either reset begin index to last appearance index + 1 to avoid duplicate // or remains unchanged. It is possible that [map[currentChar] + 1, begin] contains duplicate characters if (map[currentChar] >= 0) { begin = Math.max(begin, map[currentChar] + 1); }
// update current character's last position index map[currentChar] = end;
// update result result = Math.max(result, end - begin + 1); } return result; }}4. Median of Two Sorted Arrays
https://leetcode.com/problems/median-of-two-sorted-arrays/
对于一个有序数组,我们做一次分割,将数组分割为两半部分,则数组中位数为下半部分最大值与上半部分最小值的平均值。这种取法的前提是两半部分具有相同的元素值。当数组中有偶数个元素时,分割很容易;而当有奇数个元素时,我们尝试将实际的中位数mid一分为二,这样两部分都有了这个mid,数组的中位数仍然可以用(mid + mid) / 2 = mid得到。
对于一个有N个元素的数组,如果定义分割左侧的数为L,右侧的数为R,则它们可以表示为:
L = nums[(N - 1) / 2]R = nums[N/2]mid = (L + R) / 2现在,假设为元素之间的间隔也增加下标。例如:
[6 9 13 18] -> [# 6 # 9 # 13 # 18 #] (N = 4)position index 0 1 2 3 4 5 6 7 8 (N_Position = 9)
[6 9 11 13 18]-> [# 6 # 9 # 11 # 13 # 18 #] (N = 5)position index 0 1 2 3 4 5 6 7 8 9 10 (N_Position = 11)易得知,总共有2N + 1个下标位,并且分割的位置一定在下标为N处。因此仍然有
index(L) = (N - 1) / 2index(R) = N / 2至此,我们完成了单个数组的推导。扩展到两个有序数组时,我们可以总结出:
- 一共有
2M + 2N + 2个下标位 - 需要2个切割点,下标设定为
C1和C2 - 切割的左侧和右侧总共都有
M + N个元素
假设nums2是长度较短的数组,并且切割点的下标为C2 = K,则nums1的切割点下标为C1 = M + N - K。完成这两次切割后,我们可以得到切割点附近的四个下标:
L1 = nums1[(C1 - 1) / 2]R1 = nums1[C1/2]L2 = nums2[(C2 - 1) / 2]R2 = nums2[C2/2]为了保证我们的切割是合理的,我们需要保证:
L1 <= R1L1 <= R2L2 <= R1L2 <= R2显然,第1、4个条件是肯定满足的,因此只有中间两个条件满足时,我们才能得到正确的划分。此时,我们可以引用二分搜索。如果L1 > R2,说明nums1中的切割点取的过大,需要缩小,换言之,我们可以调整nums2中的切割点,使其变大并超过nums1中的切割点;而如果L2 > R1,说明nums2中的切割点取的过大,缩小即可。完成切分点的选取后,选取两个子数组相应的最大最小值取平均,即可得到最终答案。
需要注意的是,切割点的下标有可能会发生数组越界。只需假设数组两端各自存在一个极值,当切割点越界时自动赋极值即可解决。
class Solution { public double findMedianSortedArrays(int[] nums1, int[] nums2) { int m = nums1.length, n = nums2.length;
// make sure nums2 is the smaller array if (m < n) { return findMedianSortedArrays(nums2, nums1); }
int low = 0, high = n * 2; while (low <= high) { int cut2 = (low + high) / 2; int cut1 = m + n - cut2;
// avoid boundary exception double L1 = (cut1 == 0) ? Integer.MIN_VALUE : nums1[(cut1-1)/2]; double L2 = (cut2 == 0) ? Integer.MIN_VALUE : nums2[(cut2-1)/2]; double R1 = (cut1 == m * 2) ? Integer.MAX_VALUE : nums1[(cut1)/2]; double R2 = (cut2 == n * 2) ? Integer.MAX_VALUE : nums2[(cut2)/2];
if (L1 > R2) { // nums1's lower half is too big; need to move C1 left (C2 right) low = cut2 + 1; } else if (L2 > R1) { // nums2's lower half too big; need to move C2 left. high = cut2 - 1; } else { // Otherwise, that's the right cut. return (Math.max(L1, L2) + Math.min(R1, R2)) / 2; } } return -1; }}5. Longest Palindromic Substring
https://leetcode.com/problems/longest-palindromic-substring/
遍历每一个字符,每一个字符都向两侧延展,如果新延展得到的两个字符仍然相同则继续延展,以此计算能延展的最大长度。
考虑到回文字符串的长度有两种可能,需要按照奇偶性进行两次延展判断。
class Solution { private int startIndex = 0, maxLen = 0;
private void checkPalindrome(char[] array, int left, int right) { while (left >= 0 && right <= array.length - 1 && array[left] == array[right]) { left--; right++; }
// update maxLen = (right - 1) - (left + 1) + 1 = right - left - 1 if (maxLen < right - left - 1) { maxLen = right - left - 1; startIndex = left + 1; } }
public String longestPalindrome(String s) { if (s.length() <= 1) { return s; }
char[] array = s.toCharArray(); for (int i = 0; i < array.length - 1; i++) { checkPalindrome(array, i, i); // assume odd length checkPalindrome(array, i, i+1); // assume even length }
return s.substring(startIndex, startIndex + maxLen); }}6. Zigzag Conversion
https://leetcode.com/problems/zigzag-conversion/
根据numRows的长度建立等长的StringBuilder数组,每一行对应一个StringBuilder。按照“竖直向下——斜向右上”的顺序不断向当前行的StringBuilder添加字符串中的字符即可。输出时,要对StringBuilder进行格式转化。
class Solution { public String convert(String s, int numRows) { int len = s.length();
// every row is a StringBuilder StringBuilder[] sb = new StringBuilder[numRows]; for (int i = 0; i < numRows; i++) { sb[i] = new StringBuilder(); }
// index to traverse the whole string int index = 0; while (index < len) { // vertically down for (int i = 0; i < numRows && index < len; i++) { sb[i].append(s.charAt(index++)); }
// obliquely up (not include the first and last row) for (int i = numRows - 2; i >= 1 && index < len; i--) { sb[i].append(s.charAt(index++)); } }
// append all rows of result for (int i = 1; i < numRows; i++) { sb[0].append(sb[i]); }
return sb[0].toString(); }}7. Reverse Integer
https://leetcode.com/problems/reverse-integer/
通过整除反向提取每一位数。为了预防范围溢出,需要在每一次提取后进行一次溢出判断。
class Solution { public int reverse(int x) { long result = 0;
while (x != 0) { // obtain the last bit result = result * 10 + x % 10; x /= 10;
// overflow check if (result > Integer.MAX_VALUE || result < Integer.MIN_VALUE) { return 0; } }
return (int)result; }}8. String to Integer (atoi)
https://leetcode.com/problems/string-to-integer-atoi/
流程分为三步:
- 去除字符串开头的空格
- 判断是否存在正负号
- 判断是否为数字。如果是数字,更新运算结果,并检查当前结果是否超出整数范围;如果不是数字,则返回当前结果
class Solution { public int myAtoi(String str) { // trim leading blank space str = str.trim(); if (str.length() == 0) { return 0; }
// detect sign char firstChar = str.charAt(0); int sign = 1; int digitStart = 0; // default no sign char, thus digit start from index 0 if (firstChar == '+' || firstChar == '-') { sign = (firstChar == '+') ? 1 : -1; digitStart++; }
// add digits long sum = 0; for (int i = digitStart; i < str.length(); i++) { char current = str.charAt(i);
// non-digit, return sum immediately if (current < '0' || current > '9') { return (int)sum * sign; }
// continue adding current digit sum = sum * 10 + current - '0';
// overflow checking if (sign == 1 && sum > Integer.MAX_VALUE) { return Integer.MAX_VALUE; } if (sign == -1 && -sum < Integer.MIN_VALUE) { return Integer.MIN_VALUE; } } return (int)sum * sign; }}9. Palindrome Number
https://leetcode.com/problems/palindrome-number/
将数字反转后与原数字进行比较。注意,不需要完整地将整个数字进行反转,只需要反转一半的数字,并与剩余一半进行比较即可。
class Solution { public boolean isPalindrome(int x) { // exclude negative number and 10's multiple if (x < 0 || (x != 0 && x % 10 == 0)) { return false; }
int result = 0;
// only need to reverse half of all digits while (x > result) { result = result * 10 + x % 10; x /= 10; }
// odd or even length return (x == result || x == result / 10); }}10. Regular Expression Matching
https://leetcode.com/problems/regular-expression-matching/
此题需要使用二维动态规划算法。对于s和p,我们需要从头开始推导出子字符串s[i]和p[j]的匹配情况,进而推出整个s和p是否匹配。
解决时,需要对题设条件进行分类讨论:
- 最标准的匹配情况,即
string[i]=pattern[j],则dp[i+1][j+1]=dp[i][j] - pattern中出现了
'.',可以匹配string中任意的一个字符,同样有dp[i+1][j+1]=dp[i][j] - pattern中出现了
'*',需要进一步讨论:- 如果
pattern[j-1]!=string[i],说明此时的'a*'只能作为空字符串匹配,因此dp[i+1][j+1]=dp[i+1][j-1] - 如果
pattern[j-1]==string[i], 或是pattern[j-1]=='.',即在’*‘之前的一个字符也能够匹配上,则:- 如果
'*'匹配空字符串,则dp[i+1][j+1]=dp[i+1][j-1] - 如果
'*'匹配一个字符,则dp[i+1][j+1]=dp[i+1][j] - 如果
'*'匹配多个字符,则dp[i+1][j+1]=dp[i][j+1]
- 如果
- 如果
class Solution { public boolean isMatch(String s, String p) { boolean[][] dp = new boolean[s.length() + 1][p.length() + 1];
// s and p are both empty, so they can match dp[0][0] = true;
// pattern cannot start with '*' if (p.length() > 0 && p.charAt(0) == '*') { return false; }
// '*' match for (int j = 0; j < p.length(); j++) { if (p.charAt(j) == '*' && dp[0][j-1]) { dp[0][j+1] = true; } }
for (int i = 0; i < s.length(); i++) { for (int j = 0; j < p.length(); j++) { // regular matching, or '.' matching if (s.charAt(i) == p.charAt(j) || p.charAt(j) == '.') { dp[i+1][j+1] = dp[i][j]; } else if (p.charAt(j) == '*') { // * can only be matched as empty char if (s.charAt(i) != p.charAt(j-1)) { dp[i+1][j+1] = dp[i+1][j-1]; }
// * can be matched as 0, 1 or multiple chars if (s.charAt(i) == p.charAt(j-1) || p.charAt(j-1) == '.') { dp[i+1][j+1] = dp[i+1][j-1] || dp[i+1][j] || dp[i][j+1]; } } } }
return dp[s.length()][p.length()]; }}11. Container With Most Water
https://leetcode.com/problems/container-with-most-water/
问题转化为判断两个横坐标之间能够围成的最大面积。定义两个指针,一个指向数组头向尾遍历,另一个指向数组尾向头遍历。在遍历之前不断计算当前能围成的最大面积,并且向着可能进一步增大面积的方向移动指针。
class Solution { public int maxArea(int[] height) { if (height == null || height.length <= 1) { return 0; }
int left = 0, right = height.length - 1; int result = 0, current = 0; // max area, current area
while (left < right) { current = (right - left) * Math.min(height[left], height[right]); result = Math.max(result, current);
// keep the higher edge, and try to increase the other edge if (height[left] < height[right]) { left++; } else { right--; } } return result; }}12. Integer to Roman
https://leetcode.com/problems/integer-to-roman/
非常无聊的题目,直接穷举nums范围内各个基数单位,不断叠加上当前的最大基数对应的罗马字符。
class Solution { public String intToRoman(int num) { int[] value = {1000, 900, 500, 400, 100, 90, 50, 40, 10, 9, 5, 4, 1}; String[] roman = {"M", "CM", "D", "CD", "C", "XC", "L", "XL", "X", "IX", "V", "IV", "I"}; StringBuilder sb = new StringBuilder();
for (int i = 0; i < value.length; i++) { while (num >= value[i]) { sb.append(roman[i]); num = num - value[i]; } }
return sb.toString(); }}13. Roman to Integer
https://leetcode.com/problems/roman-to-integer/
遇到可能包含歧义的字母(M, X, V等)时,额外判断后一个字母明确具体数字即可。
class Solution { public int romanToInt(String s) { if (s.length() == 0) { return 0; }
int i = 0; int len = s.length(); int result = 0;
while (i < len) { char c = s.charAt(i); // current character char nextChar = '.'; // if there is a next character if (i != len - 1) { nextChar = s.charAt(i+1); }
// need to check IV and IX if (c == 'I') { if (nextChar == 'V') { result += 4; i += 2; } else if (nextChar == 'X') { result += 9; i += 2; } else { result += 1; i += 1; } }
else if (c == 'V') { result += 5; i += 1; }
// need to check XL and XC else if (c == 'X') { if (nextChar == 'L') { result += 40; i += 2; } else if (nextChar == 'C') { result += 90; i += 2; } else { result += 10; i += 1; } }
else if (c == 'L') { result += 50; i += 1; }
// need to check CD and CM else if (c == 'C') { if (nextChar == 'D') { result += 400; i += 2; } else if (nextChar == 'M') { result += 900; i += 2; } else { result += 100; i += 1; } }
else if (c == 'D') { result += 500; i += 1; }
else if (c == 'M') { result += 1000; i += 1; } } return result; }}14. Longest Common Prefix
https://leetcode.com/problems/longest-common-prefix/
初始假定第一个词就是LCS。为了验证假设,我们需要逐个检查所有后续字符串,确定最开始的LCS也是后面字符串的前缀。如果不满足,则删掉LCS的最后一个字符。
class Solution { public String longestCommonPrefix(String[] strs) { if (strs == null || strs.length == 0) { return ""; }
// initialized as the first word of strs String result = strs[0];
for (int i = 0; i < strs.length; i++) { // keeps removing the last character until matching the common prefix requirement while (strs[i].indexOf(result) != 0) { result = result.substring(0, result.length() - 1); if (result.length() == 0) { break; } } }
return result; }}15. 3Sum
https://leetcode.com/problems/3sum/
先对数组进行排序,接着设立三个指针first, second, third。first负责从数组头遍历至数组尾,在每一次循环中固定first,移动头尾指针two和third,对nums[first]、nums[second]、nums[third]求和判断是否为0。
这题需要最大化利用排序带来的优势,缩短运行时间:
- 检查最小/最大值,跳过一些不必要的检查
- 跳过相邻的重复元素
class Solution { public List<List<Integer>> threeSum(int[] nums) { List<List<Integer>> result = new ArrayList<>();
int len = nums.length; if (len < 3) { return result; }
// sort so we can use pointers to quickly sum Arrays.sort(nums);
// skip impossible cases if (3 * nums[0] > 0 || 3 * nums[len - 1] < 0) { return result; }
for (int first = 0; first < len - 2; first++) { // skip if nums[first] is duplicate if (first > 0 && nums[first] == nums[first-1]) { continue; }
// too big, no need to check subsequent combinations if (3 * nums[first] > 0) { break; }
// too small, we need to start from a larger nums[first] if (nums[first] + 2 * nums[len - 1] < 0) { continue; }
int second = first + 1, third = len - 1; while (second < third) { if (nums[first] + nums[second] + nums[third] == 0) { result.add(Arrays.asList(nums[first], nums[second], nums[third]));
// skip nums[second] and nums[third] duplicates while (second < third && nums[second] == nums[second + 1]) { second++; } while (third > second && nums[third] == nums[third - 1]) { third--; }
// move pointers second++; third--; } else if (nums[first] + nums[second] + nums[third] < 0) { second++; } else { third--; } } } return result; }}16. 3Sum Closest
https://leetcode.com/problems/3sum-closest/
与前一题思路类似,只是将”等于“改成了”最接近于“。额外加一个变量tempSum用于存储当前最接近的和即可。
class Solution { public int threeSumClosest(int[] nums, int target) { if (nums == null || nums.length == 0) { return 0; } int len = nums.length; int result = 0;
// less than three numbers, directly return all num's sum if (len <= 3) { for (int num : nums) { result += num; } return result; }
// initialized as the sum of first three elements result = nums[0] + nums[1] + nums[2];
// sort so we can use pointers to quickly sum Arrays.sort(nums);
for (int first = 0; first < len - 2; first++) { // skip if nums[first] is duplicate if (first > 0 && nums[first] == nums[first - 1]) { continue; }
int second = first + 1, third = len - 1; while (second < third) { int tempSum = nums[first] + nums[second] + nums[third];
// determine if tempSum is equal to target, or try to get tempSum closer to target if (tempSum == target) { return tempSum; } else if (tempSum < target) { second++; } else { third--; }
// update result if (Math.abs(target - result) > Math.abs(target - tempSum)) { result = tempSum; } } }
return result; }}17. Letter Combinations of a Phone Number
https://leetcode.com/problems/letter-combinations-of-a-phone-number/
设立一个字符串数组,数组下标0-9分别对应键盘0-9上的字符串。
遍历digits的每一位数字,得到这个数字可能对应的所有字母。使用回溯算法,尝试添加每一个字符,再通过深度优先思想添加下一个。以此类推,直到当前的string builder长度等于digits长度,表示发掘出了一种组合。
class Solution { private List<String> result = new ArrayList<>(); private final String[] mapping = {"0", "1", "abc", "def", "ghi", "jkl", "mno", "pqrs", "tuv", "wxyz"};
public List<String> letterCombinations(String digits) { if (digits == null || digits.length() == 0) { return result; }
backtrack(new StringBuilder(), digits, 0);
return result; }
private void backtrack(StringBuilder current, String digits, int index) { // find a combination, add to result if (current.length() == digits.length()) { result.add(current.toString()); return; }
// get all possible letters that are matching with current digit int digit = digits.charAt(index) - '0'; String letters = mapping[digit];
for (char letter: letters.toCharArray()) { current.append(letter); backtrack(current, digits, index + 1); current.deleteCharAt(current.length() - 1); } }}18. 4Sum
https://leetcode.com/problems/4sum/
4Sum与3Sum的思路类似,仍然是使用多个指针的移动解决问题。使用first, second两个指针从头到尾遍历整个数组,而third, fourth两个指针则在second的后面,一前一后。
需要注意,在判断极值的时候可能会产生溢出,因此需要考虑int和long之间的转换。
class Solution { public List<List<Integer>> fourSum(int[] nums, int target) { List<List<Integer>> result = new ArrayList<>(); int len = nums.length; if (len < 4) { return result; }
// sort so we can use pointers to quickly sum Arrays.sort(nums);
// too big or small Long potentialMin = (long)4 * nums[0]; if (potentialMin > target) { return result; }
Long potentialMax = (long)4 * nums[len - 1]; if (potentialMax < target) { return result; }
for (int first = 0; first < len - 3; first++) { // skip if nums[first] is duplicate if (first > 0 && nums[first] == nums[first-1]) { continue; }
// too big, no need to check subsequent combinations Long potentialRestFourMin = (long)4 * nums[first]; if (potentialRestFourMin > target) { break; }
// too small, we need to start from a larger nums[first] Long potentialRestThreeMax = (long)3 * nums[len - 1]; if (nums[first] + potentialRestThreeMax < target) { continue; }
for (int second = first + 1; second < len - 2; second++) { // skip if nums[first] is duplicate if (second > first + 1 && nums[second] == nums[second - 1]) { continue; }
// too big, no need to check subsequent combinations Long potentialRestThreeMin = (long)3 * nums[second]; if (potentialRestThreeMin + nums[first] > target) { break; }
// too small, we need to start from a larger nums[second] if (nums[first] + nums[second] + 2 * nums[len - 1] < target) { continue; }
int third = second + 1, fourth = len - 1; while (third < fourth) { if (nums[first] + nums[second] + nums[third] + nums[fourth] == target) { result.add(Arrays.asList(nums[first], nums[second], nums[third], nums[fourth]));
// skip duplicates while (second < len - 2 && nums[second] == nums[second + 1]) { second++; } while (third < fourth && nums[third] == nums[third + 1]) { third++; } while (third < fourth && nums[fourth] == nums[fourth - 1]) { fourth--; }
// move pointers third++; fourth--; } else if (nums[first] + nums[second] + nums[third] + nums[fourth] < target) { third++; } else { fourth--; } } } } return result; }}19. Remove Nth Node From End of List
https://leetcode.com/problems/remove-nth-node-from-end-of-list/
引入两个指针,一快一慢,二者之间的下标差为n(即slow指针指向第1个元素,则fast指针指向第n+1个元素)。删除节点则只需要调整节点的next指针即可。
/** * Definition for singly-linked list. * public class ListNode { * int val; * ListNode next; * ListNode() {} * ListNode(int val) { this.val = val; } * ListNode(int val, ListNode next) { this.val = val; this.next = next; } * } */class Solution { public ListNode removeNthFromEnd(ListNode head, int n) { ListNode dummyHead = new ListNode(0); dummyHead.next = head;
// slow and fast pointers ListNode slow = dummyHead, fast = dummyHead;
// fast moves n steps ahead for (int i = 0; i < n; i++) { fast = fast.next; }
// move slow and fast together while (fast.next != null) { slow = slow.next; fast = fast.next; }
// delete nth node slow.next = slow.next.next;
return dummyHead.next; }}20. Valid Parentheses
https://leetcode.com/problems/valid-parentheses/
利用栈的特性,如果遇到左半侧的括号则推入栈中,右半侧的括号则检查是否与当前栈顶元素配对,如果不能配对就返回false,否则配对成功就把栈顶括号弹出。最后确认栈是否为空。
class Solution { public boolean isValid(String s) { if (s.length() == 0) { return true; }
Stack<Character> st = new Stack<>(); for (int i = 0; i < s.length(); i++) { char c = s.charAt(i);
// push all left parentheses into stack if (c == '(' || c == '[' || c == '{') { st.push(c); } else if (c == ')' || c == ']' || c == '}') { // current right parenthesis must match with top parenthesis in the stack if (st.size() == 0) { return false; }
if (c == ')' && st.peek() != '(') { return false; } else if (c == ']' && st.peek() != '[') { return false; } else if (c == '}' && st.peek() != '{') { return false; } else { // successfully matched, pop the top parenthesis st.pop(); } } } return st.isEmpty(); }}21. Merge Two Sorted Lists
https://leetcode.com/problems/merge-two-sorted-lists/
第一种方法,比较l1和l2上的指针当前指向的元素,取较小元素之后的子链表和另一个链表继续进行递归合并。这样能保证每一次比较都优先提取出两个剩余链表的最小值,其余部分的结果也会接在这个最小值之后。
/** * Definition for singly-linked list. * public class ListNode { * int val; * ListNode next; * ListNode() {} * ListNode(int val) { this.val = val; } * ListNode(int val, ListNode next) { this.val = val; this.next = next; } * } */class Solution { public ListNode mergeTwoLists(ListNode l1, ListNode l2) { if (l1 == null) { return l2; } if (l2 == null) { return l1; }
// merge after smaller node if (l1.val < l2.val) { l1.next = mergeTwoLists(l1.next, l2); return l1; } else { l2.next = mergeTwoLists(l1, l2.next); return l2; } }}第二种方法,也是比较l1和l2上的指针当前指向的元素,但不使用递归,而是直接根据大小关系决定排列顺序。
/** * Definition for singly-linked list. * public class ListNode { * int val; * ListNode next; * ListNode() {} * ListNode(int val) { this.val = val; } * ListNode(int val, ListNode next) { this.val = val; this.next = next; } * } */class Solution { public ListNode mergeTwoLists(ListNode l1, ListNode l2) { if (l1 == null) { return l2; } if (l2 == null) { return l1; }
ListNode dummyHead = new ListNode(0); ListNode cur = dummyHead;
while (l1 != null && l2 != null) { // decide which element to be added to the result list if (l1.val < l2.val) { cur.next = l1; l1 = l1.next; } else { cur.next = l2; l2 = l2.next; } cur = cur.next; }
// one list reaches the end, add the rest list elements to result cur.next = (l2 == null) ? l1 : l2;
return dummyHead.next; }}22. Generate Parentheses
https://leetcode.com/problems/generate-parentheses/
使用递归策略,优先构造左括号,当左括号个数大于右括号时再构造右括号。
具体代码实现时,可以使用两个变量left,right跟踪当前左、右括号数量,在递归中将它们与n进行比较。
class Solution { List<String> result = new ArrayList<>();
public List<String> generateParenthesis(int n) { generate("", 0, 0, n); return result; }
// left: left parentheses' count // right: right parentheses' count private void generate(String s, int left, int right, int n) { if (right == n) { // finish construction result.add(s); } else { // always prefer to generate left parenthesis first if (left < n) { generate(s + "(", left + 1, right, n); }
// left is greater than right, then generate a right parenthesis if (left > right) { generate(s + ")", left, right + 1, n); } } }}23. Merge k Sorted Lists
https://leetcode.com/problems/merge-k-sorted-lists/
第一种解法是使用优先队列。优先队列本质上是一种堆,具体使用时可以自定义comparator指定最小堆或是最大堆。由于此题要求从小到大排序,因此需要定义最小堆。
将每一条链表的头元素加入优先队列中。优先级最高的元素就是这些元素里的最小值。将其排序后进行判断,如果该元素的next指针不指向空,则将剩下的链表头元素重新插入优先队列中。
以一个形象一些的方式描述该场景:一位教师正在向一群排队的学生解答问题,每个学生都有多个问题,并且问题的难度有难有易,教师优先回答简单的问题。为保证公平,当前问问题的学生问完一个问题后需要重新排队,排队顺序取决于他下一个问题的难度。
时间复杂度上,如果链表有N个节点,则堆的插入、删除操作时间复杂度均为O(logN),总的时间复杂度为O(NlogN)。
/** * Definition for singly-linked list. * public class ListNode { * int val; * ListNode next; * ListNode() {} * ListNode(int val) { this.val = val; } * ListNode(int val, ListNode next) { this.val = val; this.next = next; } * } */class Solution { public ListNode mergeKLists(ListNode[] lists) { if (lists == null || lists.length == 0) { return null; }
// override priority queue's comparator in ascending order Queue<ListNode> pq = new PriorityQueue<>((ListNode l1, ListNode l2) -> { return l1.val - l2.val; });
// add each list's head (smallest) node into the priority queue for (ListNode list : lists) { if (list != null) { pq.offer(list); } }
ListNode dummyHead = new ListNode(0); ListNode cur = dummyHead;
while (!pq.isEmpty()) { // pick out the smallest element from priority queue ListNode currentHead = pq.poll(); cur.next = currentHead; cur = cur.next;
// add its next element into the queue if (cur.next != null) { pq.offer(cur.next); } }
return dummyHead.next; }}第二种解法是使用分治策略,将k条链表对半分为两组,问题转化为了广义版的merge two sorted lists问题。不断从上而下划分,再由小到大不断两两合并,得到完整的链表。
/** * Definition for singly-linked list. * public class ListNode { * int val; * ListNode next; * ListNode() {} * ListNode(int val) { this.val = val; } * ListNode(int val, ListNode next) { this.val = val; this.next = next; } * } */class Solution { public ListNode mergeKLists(ListNode[] lists) { return merge(lists, 0, lists.length - 1); }
private ListNode merge(ListNode[] lists, int l, int r) { // invalid interval if (l > r) { return null; }
// interval only has one list group if (l == r) { return lists[l]; }
// divide k lists into two groups int mid = l + (r - l) / 2; ListNode left = merge(lists, l, mid); ListNode right = merge(lists, mid + 1, r);
// merge two "big" lists together ListNode dummyHead = new ListNode(0); ListNode cur = dummyHead;
while (left != null || right != null) { // right list is null, or the head of left list is smaller than the one of right list if (right == null || (left != null && left.val < right.val)) { cur.next = left; left = left.next; } else { cur.next = right; right = right.next; } cur = cur.next; }
return dummyHead.next; }}24. Swap Nodes in Pairs
https://leetcode.com/problems/swap-nodes-in-pairs/
使用递归,只提取前两个节点,将剩余节点视为一个整体。
新的头节点newHead是原来的第二个节点head.next,而原来的头节点head指向剩余节点整体(剩余部分的头节点为head.next.next)。
/** * Definition for singly-linked list. * public class ListNode { * int val; * ListNode next; * ListNode() {} * ListNode(int val) { this.val = val; } * ListNode(int val, ListNode next) { this.val = val; this.next = next; } * } */class Solution { public ListNode swapPairs(ListNode head) { if (head == null || head.next == null) { return head; }
// previous second node now bcomes the head node ListNode newHead = head.next;
// regard all other nodes as an entity, and the head of this entity is head.next.next head.next = swapPairs(head.next.next);
// previous head node now becomes the second node newHead.next = head;
return newHead; }}25. Reverse Nodes in k-Group
https://leetcode.com/problems/reverse-nodes-in-k-group/
假设遍历链表的指针为cur,将cur移动k个节点,获取到k个节点就能组成一个节点簇。我们优先递归完成链表后续部分的反转,再来倒转节点簇的k个节点。每个节点簇内,我们的核心思想是把原先的第一个节点指向链表后续部分,这样一来这个节点就可以被归到已经逆序的子链表中;而原先节点簇的第二个节点就成为了剩余未逆序部分的头节点。我们需要不断循环k次,按照上述流程调整顺序。
/** * Definition for singly-linked list. * public class ListNode { * int val; * ListNode next; * ListNode() {} * ListNode(int val) { this.val = val; } * ListNode(int val, ListNode next) { this.val = val; this.next = next; } * } */class Solution { public ListNode reverseKGroup(ListNode head, int k) { if (head == null) { return null; }
// move cur k nodes forward ListNode cur = head; int count = 0; while (cur != null && count != k) { count++; cur = cur.next; }
// we get k nodes in a group now if (count == k) { // recursively complete all other nodes first to get the modified sub-list cur = reverseKGroup(cur, k);
// reverse k nodes while (count-- > 0) { // get the second element among k nodes ListNode second = head.next;
// current first element (head) will point to the modified sub-list head.next = cur;
// previous head has become the head of the modified sub-list cur = head;
// previous second element now becomes the new head of the un-reversed part head = second; }
// after reverse, the head of the modified sub-list is actually the head of the whole list head = cur; }
return head; }}26. Remove Duplicates from Sorted Array
https://leetcode.com/problems/remove-duplicates-from-sorted-array/
从头开始遍历,只要遇到不重复的数时,就覆盖数组的原始值。最后返回的result值实际上就是不同数字的个数。
class Solution { public int removeDuplicates(int[] nums) { int len = nums.length; if (len <= 1) { return len; }
int result = 1; for (int i = 1; i < len; i++) { if (nums[i] != nums[i - 1]) { nums[result] = nums[i]; // in-place modification result++; } }
return result; }}27. Remove Element
https://leetcode.com/problems/remove-element/
与上一题一样的思路,遇到值为val的元素时跳过,其余仍从头到尾开始覆盖。
class Solution { public int removeElement(int[] nums, int val) { int len = nums.length;
int result = 0; for (int i = 0; i < len; i++) { if (nums[i] != val) { nums[result] = nums[i]; // in-place modification result++; } }
return result; }}28. Implement strStr()
https://leetcode.com/problems/implement-strstr/
遍历haystack中的每一个字符,如果某个字符与needle的首字符相同,再检查haystack中该字符起的子串是否与needle的值相同。
class Solution { public int strStr(String haystack, String needle) { int hLen = haystack.length(), nLen = needle.length();
// needle is an empty string if (nLen == 0) { return 0; }
// haystack is empty and impossible to contain needle if (hLen == 0) { return -1; }
for (int i = 0; i <= hLen - nLen; i++) { // current char is equal to needle's first char & substring is equal to needle if (haystack.charAt(i) == needle.charAt(0) && haystack.substring(i, i + nLen).equals(needle)) { return i; } }
return -1; }}29. Divide Two Integers
https://leetcode.com/problems/divide-two-integers/
此题要求实现除法,并且不能使用乘、除、取模三个符号。大致的思路流程是:
- 专门处理被除数为
Integer.MIN_VALUE,divisor为-1时的情况,这种时候int形式的结果会发生溢出 - 将
dividend和divisor都取绝对值 - 使用指数倍增思想,快速寻找到
dividend中至少包含几个divisor。 - 决定最终结果的实际符号
为了避免溢出,在第一步处理完特例后,我们最好将dividend和divisor都转为long格式。
class Solution { public int divide(int dividend, int divisor) { // corner case if (dividend == Integer.MIN_VALUE && divisor == -1) { return Integer.MAX_VALUE; }
long dividendCopy = (long)dividend, divisorCopy = (long)divisor;
// get rid of sign long a = Math.abs(dividendCopy), b = Math.abs(divisorCopy); long result = 0;
while (a >= b) { int x = 0;
// count how many divisor does the dividend "has" while (a >= (b << 1 << x)) { x++; }
result += 1 << x; a -= b << x; }
// determine if dividend and divisor have same sign return (dividend > 0) == (divisor > 0) ? (int)result : (int)-result; }}30. Substring with Concatenation of All Words
https://leetcode.com/problems/substring-with-concatenation-of-all-words/
只要words中的所有单词拼接得到s中的子字符串即可满足题意,单词的顺序并不重要。因此我们只需要关注words中每一个单词出现的次数。
此题可以使用两个HashMap解决,一个HashMap用于记录words数组中的每一个单词出现的次数;另一个HashMap用于记录当前遍历s时的单词已经出现了几次。如果当前遍历的单词是words中没有的,或者出现次数已经超过了“应该”出现的次数,则说明不满足题意。
class Solution { public List<Integer> findSubstring(String s, String[] words) { List<Integer> result = new ArrayList<>(); if (words.length == 0) { return result; }
// how many times each word is supposed to occur Map<String, Integer> expected = new HashMap<>(); for (String word : words) { expected.put(word, expected.getOrDefault(word, 0) + 1); }
// each word's length and all words' count int len = words[0].length(), count = words.length; if (s.length() < count * len) { return result; }
for (int i = 0; i < len; i++) { for (int j = i; j <= s.length() - count * len; j += len) { // how many times every word actually occurs in each substring Map<String, Integer> actual = new HashMap<>();
// iterate from the last for (int k = count - 1; k >= 0; k--) { // extract current word String currentWord = s.substring(j + k * len, j + (k + 1) * len);
// current word is not supposed to occur if (!expected.containsKey(currentWord)) { break; } else { int occurences = actual.getOrDefault(currentWord, 0) + 1;
// can not exceed expected occurences if (occurences > expected.get(currentWord)) { // start right after invalid occurence, as previous choices will include invalid. j += k * len; break; }
actual.put(currentWord, occurences);
// all words appear if (k == 0) { result.add(j); } } } } } return result; }}31. Next Permutation
https://leetcode.com/problems/next-permutation/
一个数组向下一置换演变的趋势总体是从升序演变为降序。
找到下一置换,实际上就是从右到左遍历,先找到破坏升序的第一个数nums[i],再从这个数的右侧(也是从右向左遍历)找到第一个比nums[i]大的数nums[j],这两个数进行交换。最后,对下标i之后的数需要全部进行反转,因为在经过交换之后,从下标i开始一直到结尾都是降序,我们需要将i + 1到结尾重新调整为升序,确保不遗漏置换的可能。
class Solution { public void nextPermutation(int[] nums) { if (nums == null || nums.length <= 1) { return; }
// search from right to left and find the first element to break the ascending order int i = nums.length - 2; while (i >= 0 && nums[i] >= nums[i+1]) { i--; }
// if there is an element that break the ascending order if (i >= 0) { int j = nums.length - 1;
// find the rightmost element which is larger than nums[i] while (nums[i] >= nums[j]) { j--; }
// swap nums[i] and nums[j] swap(nums, i, j); } // reverse all elements after index i reverseArray(nums, i + 1, nums.length - 1); }
// swap two nums private void swap(int[] nums, int i, int j) { int temp = nums[i]; nums[i] = nums[j]; nums[j] = temp; }
// reverse part of the array private void reverseArray(int[] nums, int i, int j) { while (i < j) { swap(nums, i++, j--); } }}32. Longest Valid Parentheses
https://leetcode.com/problems/longest-valid-parentheses/
使用栈解决。遍历字符串,如果遇到’(‘,则将当前下标推入栈中;遇到’)‘,则将栈顶下标弹出,并讨论。需要注意:
- 栈里存储的下标,实质上指的是“截止到这个下标时,字符串不能组成valid parentheses”。我们最后更新结果时,也是用当前下标与栈顶下标的差值来更新长度,因为这一段差值之间的字符串一定是有效的
- 因为上述设定,栈的初始化需要push一个-1;如果空栈的时候,说明截止至目前的字符串一定是无效的,我们要想找有效字符串只能在此之后寻找
class Solution { public int longestValidParentheses(String s) { int result = 0;
ArrayDeque<Integer> stack = new ArrayDeque<Integer>();
// the index preceding to potential start of valid parentheses stack.push(-1);
for (int i = 0; i < s.length(); i++) { char curr = s.charAt(i); if (curr == '(') { stack.push(i); } else { // no matter we get valid parentheses or not, we need to pop here // if valid, we need to get last index to calculate diff // if unvalid, we update with current index stack.pop();
if (stack.isEmpty()) { // the stack is empty only if we have an extra ')'. Any further extensions of valid parentheses is impossible // push current index, since it is preceding to potential start of valid parentheses stack.push(i); } else { // this may be our longest valid parentheses result = Math.max(result, i - stack.peek()); } } }
return result; }}33. Search in Rotated Sorted Array
https://leetcode.com/problems/search-in-rotated-sorted-array/
这一题需要将数组对半划分来看,其中一半一定是有序的,而另一半则在中间某处破坏了升序。因此在使用模型1时,我们无法简单通过比较nums[mid]和target确定下一轮搜索区间,而是应该比较nums[left]、nums[mid]和nums[right]三者的值,先判断哪一半数组是有序的;之后将target放进有序的这一侧进行比较,如果target的值介于其中则把区间定为有序的这一侧,反之定为另一侧。
class Solution { public int search(int[] nums, int target) { if (nums == null || nums.length == 0) { return -1; }
int left = 0, right = nums.length - 1; while (left <= right) { int mid = left + (right - left) / 2;
// nums[mid] matches with target, return if (nums[mid] == target) { return mid; }
// left half is ordered if (nums[left] <= nums[mid]) { // target is in left half if (target >= nums[left] && target <= nums[mid]) { right = mid - 1; } else { left = mid + 1; } }
// right half is ordered if (nums[mid] <= nums[right]) { // target is in right half if (target >= nums[mid] && target <= nums[right]) { left = mid + 1; } else { right = mid - 1; } } }
return -1; }}34. Find First and Last Position of Element in Sorted Array
https://leetcode.com/problems/find-first-and-last-position-of-element-in-sorted-array/
分别进行左边界搜索和右边界搜索即可。
class Solution { public int[] searchRange(int[] nums, int target) { int[] result = new int[] {-1, -1}; if (nums == null || nums.length == 0) { return result; }
// left int left = 0, right = nums.length; while (left < right) { int mid = left + (right - left) / 2; if (nums[mid] < target) { left = mid + 1; } else if (nums[mid] >= target) { right = mid; } }
if (left != nums.length && nums[left] == target) { result[0] = left; }
// right left = 0; right = nums.length; while (left < right) { int mid = left + (right - left) / 2; if (nums[mid] <= target) { left = mid + 1; } else if (nums[mid] > target) { right = mid; } }
if (left != 0 && nums[left - 1] == target) { result[1] = left - 1; }
return result; }}35. Search Insert Position
https://leetcode.com/problems/search-insert-position/
可以假想把target插入到nums数组中,再转化为数组长度+1后的标准二分查找。
class Solution { public int searchInsert(int[] nums, int target) { if (nums == null || nums.length == 0) { return 0; }
// num's length +1, therefore right should be 'nums.length' instead of 'nums.length - 1' int left = 0, right = nums.length;
// standard binary search while (left < right) { int mid = left + (right - left) / 2; if (nums[mid] < target) { left = mid + 1; } else { right = mid; } }
// both left and right are valid return left; }}36. Valid Sudoku
https://leetcode.com/problems/valid-sudoku/
每一个数字只能在所处的行、列、3*3方格内出现一次。因此,构造三个二维数组检查当前数字num分别在当前行、列、方格内出现的次数,超过1次就是不合法的。
class Solution { public boolean isValidSudoku(char[][] board) { int[][] row = new int[9][9]; // row[i][num] indicates the count of 'num' in the i-th row int[][] col = new int[9][9]; // col[j][num] indicates the count of 'num' in the j-th column int[][] block = new int[9][9]; // block[k][num] indicates the count of 'num' in the k-th 3*3 block
for (int i = 0; i < 9; i++) { for (int j = 0; j < 9; j++) { // only validate filled cells if (board[i][j] != '.') { int num = board[i][j] - '1'; int blockIndex = (i/3) * 3 + j/3;
// each num should not duplicate in each row, col, block if (row[i][num] > 0 || col[j][num] > 0 || block[blockIndex][num] > 0) { return false; }
row[i][num] += 1; col[j][num] += 1; block[blockIndex][num] += 1; } } } return true; }}37. Sudoku Solver
https://leetcode.com/problems/sudoku-solver/
遇到一个空方格,从0-9循环填入每一个数字,填完后确认没有违规,再以深度优先,尝试后续所有方格都满足要求,才能保证当前方格也满足要求。
遇到列尾时跳下一行,遇到行尾则说明深度优先搜索结束,数独解决完毕;而如果遇到不符合的情况,则要回溯复原当前填入的数字,再继续循环填入下一个数字。
class Solution { public void solveSudoku(char[][] board) { solve(board, 0, 0); }
// solve based on index private Boolean solve(char[][] board, int i, int j) { // finish all 9 row if (i == 9) { return true; }
// finish current row, jump to next row if (j == 9) { return solve(board, i+1, 0); }
// non-digit, switch to next column if (board[i][j] != '.') { return solve(board, i, j+1); }
// backtrack for (char c = '1'; c <= '9'; c++) { if (check(board, i, j, c)) { board[i][j] = c;
// all subsequent cells are valid if (solve(board, i, j+1)) { return true; }
board[i][j] = '.'; } }
return false; // no solution }
// check if current character is valid private Boolean check(char[][] board, int i, int j, char val) { for (int c = 0; c < 9; c++) {
// duplicate in current row, column or 3 * 3 block if (board[i][c] == val || board[c][j] == val || board[i - i%3 + c/3][j - j%3 + c%3] == val) { return false; } } return true; }}38. Count and Say
https://leetcode.com/problems/count-and-say/
直接的想法是使用递归。递归的起点显然是n为1的情况。此后对于任意输入的n,先计算出n-1的结果字符串pre,再遍历pre得到n的结果。遍历pre时,统计当前数字连续出现了几次,把数字和次数直接加到StringBuilder里。
注意使用StringBuilder的目的主要是优化性能,因为String每次追加字符时都会产生新的对象;而StringBuilder与StringBuffer都是直接在同一对象上追加字符,效率很高。另外StringBuffer()适合多线程,而StringBuilder()适合单线程。
class Solution { public String countAndSay(int n) { if (n == 1) { return "1"; }
// all upper levels' result StringBuilder pre = new StringBuilder(countAndSay(n - 1));
// construct this level StringBuilder result = new StringBuilder();
int i = 0; while (i < pre.length()) { int count = 0; // current number's count int j = i; // start index of current number
char say = pre.charAt(j); while (i < pre.length() && pre.charAt(i) == say) { count++; i++; }
result.append(count); result.append(say); } return result.toString(); }}39. Combination Sum
https://leetcode.com/problems/combination-sum/
将原始数组排序,并遍历数组,创建一个新的临时数组用于存放当前的组合。每当向临时数组中添加一个数后,更新target,递归进入下一层搜索;由于每一个数可以使用无限次,下一次递归的起点下标index不变。最后当target等于0,说明当前组合满足;否则,则将临时数组中最后一个元素移除,寻找下一个数。
class Solution { public List<List<Integer>> combinationSum(int[] candidates, int target) { List<List<Integer>> result = new ArrayList<>(); Arrays.sort(candidates); backtrack(result, new ArrayList<>(), candidates, target, 0); return result; }
private void backtrack(List<List<Integer>> result, List<Integer> temp, int[] candidates, int target, int index) { if (target == 0) { result.add(new ArrayList<>(temp)); } else { for (int i = index; i < candidates.length && candidates[i] <= target; i++) { temp.add(candidates[i]); backtrack(result, temp, candidates, target - candidates[i], i); // allow duplicate, thus starts from i temp.remove(temp.size() - 1); } } }}40. Combination Sum II
https://leetcode.com/problems/combination-sum-ii/
与上一题几乎一样的题设,唯一不同是输入数组中可能有重复的数字,且数组中每个数只能用一次。只需要稍微改变上一题的代码,调整下一层递归的起始下标;此外,完成一次回溯后,需要及时跳过重复的数字。
class Solution { public List<List<Integer>> combinationSum2(int[] candidates, int target) { List<List<Integer>> result = new ArrayList<>(); Arrays.sort(candidates); backtrack(result, new ArrayList<>(), candidates, target, 0); return result; }
private void backtrack(List<List<Integer>> result, List<Integer> temp, int[] candidates, int target, int index) { if (target == 0) { result.add(new ArrayList<>(temp)); } else { for (int i = index; i < candidates.length && candidates[i] <= target; i++) { temp.add(candidates[i]); backtrack(result, temp, candidates, target - candidates[i], i + 1); // not allow duplicate, thus starts from i + 1 temp.remove(temp.size() - 1);
// skip duplicates while (i + 1 < candidates.length && candidates[i] == candidates[i + 1]) { i++; } } } }}41. First Missing Positive
https://leetcode.com/problems/first-missing-positive/
要寻找最小的缺失正整数,可以对数组进行处理:假如遍历发现了1,则将1放在数组第一个位置;发现2,则将2放在数组第二个位置…以此类推。当然,前提是只有遍历的元素值位于0和数组长度之间,才做此处理。
为了达到这一目的,只需要将nums[i]与nums[nums[i] - 1]进行交换即可。最后,重新遍历顺序处理后的数组,第一个元素值和下标不匹配的元素即为答案。
class Solution { public int firstMissingPositive(int[] nums) { int len = nums.length; for (int i = 0; i < len; i++) { // if nums[i] is in the range of (0, len] and not in its place, // put nums[i] on the (nums[i] - 1)th index of the array while (nums[i] > 0 && nums[i] <= len && nums[i] != nums[nums[i] - 1]) { // swap nums[i] and nums[nums[i] - 1] int temp = nums[nums[i] - 1]; nums[nums[i] - 1] = nums[i]; nums[i] = temp; } }
// the first number unmatched is the answer for (int i = 0; i < len; i++) { if (nums[i] != i + 1) { return i + 1; } }
// all matched, the smallest positive integer is last integer + 1 return len + 1; }}42. Trapping Rain Water
https://leetcode.com/problems/trapping-rain-water/
使用左右两个指针跟踪当前的池壁高度。根据木桶效应,高度下界lowerBound取决于池壁高度较低的一边,而当前已有的水槽高度currentLevel则根据较低的池壁高度不断更新。指针移动过程中,新增的水量为currentLevel与lowerBound的差值。
class Solution { public int trap(int[] height) { if (height == null || height.length <= 1) { return 0; }
int left = 0, right = height.length - 1; int currentLevel = 0, result = 0;
while (left < right) { int lowerBound = 0;
// smaller of height[left] and height[right], then move left or right accordingly if (height[left] <= height[right]) { lowerBound = height[left]; left++; } else { lowerBound = height[right]; right--; }
// update current bar's level currentLevel = Math.max(currentLevel, lowerBound);
// new water fills in result += currentLevel - lowerBound; }
return result; }}43. Multiply Strings
https://leetcode.com/problems/multiply-strings/
此题限定了输入字符串只包含数字,并且不能直接转成整数再调用乘法计算,实际上便是指定了思路:模拟实现大数的乘法运算,类似于小学进行竖式乘法的过程。
对于一个m位整数与一个n位整数,其结果的长度一定为m+n-1或m+n,因此可以提前创建一个长为m+n的数组用于存放最终结果的每一位数。双重循环遍历两个整数,可以发现num1[i] * num2[j]的结果对应的是result[i+j+1]。明确了位数之后,剩余的工作较为简单,只需逐位求值,保留尾数,提取进位向前累加即可。输出结果时,需要去掉起始的0。
class Solution { public String multiply(String num1, String num2) { int m = num1.length(), n = num2.length();
if (m == 0 || n == 0) { return "0"; } if (num1.equals("0") || num2.equals("0")) { return "0"; }
// num1[i] * num2[j] reflects on result[i+j+1] int[] result = new int[m+n]; for (int i = m-1; i >= 0; i--) { for (int j = n-1; j >= 0; j--) { result[i+j+1] += (num1.charAt(i) - '0') * (num2.charAt(j) - '0'); } }
// calculate each digit and each carry int carry = 0; for (int k = result.length - 1; k >= 0; k--) { int digit = (result[k] + carry) % 10; carry = (result[k] + carry) / 10; result[k] = digit; }
StringBuilder sb = new StringBuilder();
// trim all leading 0 for (int each : result) { if (!(each == 0 && sb.length() == 0)) { sb.append(each); } }
return sb.toString(); }}44. Wildcard Matching
https://leetcode.com/problems/wildcard-matching/
解法一,参考Regular Expression Matching中的解法,使用二维动态规划:
class Solution { public boolean isMatch(String s, String p) { boolean[][] dp = new boolean[s.length() + 1][p.length()+1]; dp[0][0] = true;
// empty pattern cannot match with any string for (int i = 1; i <= s.length(); i++) { dp[i][0] = false; }
// multiple '*' pattern can match with empty string for (int j = 1; j <= p.length(); j++) { if (p.charAt(j-1) != '*') { break; } else { dp[0][j] = true; } }
for (int i = 0; i < s.length(); i++) { for (int j = 0; j < p.length(); j++) { if (s.charAt(i) == p.charAt(j) || p.charAt(j) == '?') { dp[i+1][j+1] = dp[i][j]; } else if (p.charAt(j) == '*') { dp[i+1][j+1] = dp[i+1][j] || dp[i][j+1]; } else { dp[i][j] = false; } } }
return dp[s.length()][p.length()]; }}解法二,使用指针:
使用两个指针sIdx和pIdx分别跟踪s和p,分类讨论所有情况:
- 严格的单字对应,或
p中出现?,两个指针分别前移 p遇到*,暂不清楚匹配情况,记录s当前的匹配起点和p最近一个*的位置,前移pIdx- 前一个
pIdx是*,但现在不是,可能对应着匹配多个字符的情况,将pIdx置于最近一个*的右侧,并前移sIdx
其他情况下,直接判定为匹配失败。
class Solution { public boolean isMatch(String s, String p) { int sIdx = 0, pIdx = 0, match = 0, starIdx = -1;
while (sIdx < s.length()) { if (pIdx < p.length() && (p.charAt(pIdx) == '?' || s.charAt(sIdx) == p.charAt(pIdx))) { // regular match or match ?, moving both pointers forward sIdx++; pIdx++; } else if (pIdx < p.length() && p.charAt(pIdx) == '*') { // * found, only moving pattern pointer forward starIdx = pIdx; // update the latest *'s index match = sIdx; pIdx++; } else if (starIdx != -1) { // last pattern pointer was * but current is not, moving string pointer forward to match pIdx = starIdx + 1; sIdx = ++match; } else return false; }
// check for remaining characters in pattern while (pIdx < p.length() && p.charAt(pIdx) == '*') { pIdx++; }
return pIdx == p.length(); }}45. Jump Game II
https://leetcode.com/problems/jump-game-ii/
此题需要使用贪心算法。第i次跳跃中,必然有nums[i]个可选落点,检查每一个可选落点最远可以到达何处,选取最远的一个作为下一次跳跃的最远边界。以此类推,每一步都选择下一步可能最远的落点,直到最终可以到达终点。
class Solution { public int jump(int[] nums) { if (nums == null || nums.length == 0) { return 0; }
int steps = 0; // steps taken int start = 0, end = 0; // in current step, I can reach the interval [start, end] int maxEnd = 0; // the farthest fall of the jump
while (end < nums.length - 1) { steps++;
// find out the maxEnd for (int i = start; i <= end; i++) { maxEnd = Math.max(maxEnd, nums[i] + i); }
// generate next [start, end] range start = end + 1; // next range's start must be larger than current range's end end = maxEnd; }
return steps; }}46. Permutations
https://leetcode.com/problems/permutations/
使用通用的回溯算法解题模版。使用一个boolean数组跟踪每个下标的数字是否使用过,跳过已经使用过的数;在回溯的时候重新设为未使用。
class Solution { public List<List<Integer>> permute(int[] nums) { List<List<Integer>> result = new ArrayList<>(); Arrays.sort(nums); backtrack(result, nums, new ArrayList<>(), new boolean[nums.length]); return result; }
private void backtrack(List<List<Integer>> result, int[] nums, List<Integer> temp, boolean[] used) { // match with num's length, add this permutation to result if (temp.size() == nums.length) { result.add(new ArrayList<>(temp)); return; }
for (int i = 0; i < nums.length; i++) { // current number has already been used, skip if (used[i] == true) { continue; }
// set current number as used and add it to temp permutation used[i] = true; temp.add(nums[i]);
backtrack(result, nums, temp, used);
// after using, reset current number as unused and remove it from temp permutation used[i] = false; temp.remove(temp.size() - 1); } }}47. Permutations II
https://leetcode.com/problems/permutations-ii/
此题的输入数组可能含有重复的数字,只需要在上一题的基础上,跳过重复的数字即可。
需要注意,判断重复时,used[i - 1]和!used[i - 1]都能让代码成功运行,然而LeetCode运行时间显示,!used[i - 1]快的多,可能是因为LeetCode生成的随机测试数据中,包含重复的情况较多,因此我们发现当前一个相同的数字被使用时,我们也优先加入当前这个相同的数字,这样能更快试出所有permutation。
class Solution { public List<List<Integer>> permuteUnique(int[] nums) { List<List<Integer>> result = new ArrayList<List<Integer>>(); Arrays.sort(nums); backtrack(result, nums, new ArrayList<>(), new boolean[nums.length]); return result; }
private void backtrack(List<List<Integer>> result, int[] nums, List<Integer> temp, boolean[] used) { // match with num's length, add this permutation to result if (temp.size() == nums.length) { result.add( new ArrayList<>(temp) ); return; }
for (int i = 0; i < nums.length; i++) { // current number is used / if previous identical number is used, use current number if (used[i] || (i > 0 && nums[i] == nums[i-1] && !used[i-1])) { continue; }
// mark current number as used and add it to temp permutation used[i] = true; temp.add(nums[i]);
backtrack(result, nums, temp, used);
// after using, unmark current number as unused and remove it from temp permutation used[i] = false; temp.remove(temp.size() - 1); } }}48. Rotate Image
https://leetcode.com/problems/rotate-image/
观察发现,矩阵内的元素旋转实际上是在建构一个旋转矩形,矩形顶点的轮换导致了整个矩阵的变化。
例如对于例子中的两个矩阵,第一个矩阵中2, 4, 6, 8构成了一个旋转矩形,第二个矩阵中1, 10, 12, 13构成了一个旋转矩形。因此,对于任意一个matrix[i][j],总能找到另外三个矩阵坐标顶点与之构成一个旋转矩形,只需推出其数学表达式即可。
class Solution { public void rotate(int[][] matrix) { if (matrix == null) { return; } int len = matrix.length;
// locate four corner points for (int i = 0; i < (len + 1) / 2; i++) { for (int j = 0; j < matrix[0].length / 2; j++) { int temp = matrix[i][j]; // first matrix[i][j] = matrix[len - 1 - j][i]; // second matrix[len - 1 - j][i] = matrix[len - 1 - i][len - 1 - j]; // third matrix[len - 1 - i][len - 1 - j] = matrix[j][len - 1 - i]; // fourth matrix[j][len - 1 - i] = temp; } } }}49. Group Anagrams
https://leetcode.com/problems/group-anagrams/
统计每个单词中各个字母出现的次数,存到一个长度为26的数组中。利用这个数组生成对应的hash码(Arrays.hashCode()),具有相同hash码的字符串就可以被归类在一起。
如果字符串很短的情况下,也可以每一个字母映射一个质数,质数相乘作为hashcode,提升运行效率。
class Solution { public List<List<String>> groupAnagrams(String[] strs) { List<List<String>> result = new ArrayList<>();
// <hash number, list index in result> Map<Integer, Integer> map = new HashMap<>();
for (String s: strs) { int key = getKey(s); int resultIndex = map.getOrDefault(key, -1);
List<String> list = null; if (resultIndex == -1) { list = new ArrayList<>(); result.add(list); map.put(key, result.size() - 1); } else { list = result.get(resultIndex); }
list.add(s); } return result; }
// use count to generate hash number public int getKey(String s) { int[] map = new int[26]; for (char c: s.toCharArray()) { map[c - 'a']++; } return Arrays.hashCode(map); }}如果字符串很短的情况下,也可以每一个字母映射一个质数,质数相乘作为hashcode,提升运行效率。
class Solution { public List<List<String>> groupAnagrams(String[] strs) { List<List<String>> result = new ArrayList<>(); if (strs == null || strs.length == 0) { return result; }
// top 26 positive primes int[] prime = {2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 41, 43, 47, 53, 59, 61, 67, 71, 73, 79, 83, 89, 97, 101, 103};
// <product, position in result> Map<Long, Integer> map = new HashMap<>();
for (String str : strs) { // calculate current string's product long product = 1; for (char ch : str.toCharArray()) { product *= prime[ch - 'a']; }
List<String> temp;
// current product is already in the map, add this string to specific list of result if (map.containsKey(product)) { result.get(map.get(product)).add(str); } else { // create a new list, add current string, then add the list to result temp = new ArrayList<>(); temp.add(str); result.add(temp);
// record this new list's index map.put(product, result.size() - 1); } }
return result; }}50. Pow(x, n)
https://leetcode.com/problems/powx-n/
实现幂函数的构造需要运用递归。然而在开始递归前,需要先排除一些输入的特殊情况。n和x各有若干特殊情况:
- 输入的
x为1,或是n为0,或是x为-1且n为整数下界,返回1 - 输入的
x为0,或是x不为-1且n为整数下界,返回0 - 输入的
n为负数,利用xn =(1/x)-n这一特点,将n转化成正数。
完成上述处理后,分n的奇偶性进行递归调用。对于奇数,由于整数除法的舍入,需要额外乘一个x。
public double myPow(double x, int n) { if (x == 1 || n == 0 || (n == Integer.MIN_VALUE && x == -1)) { return 1; } if (x == 0 || (n == Integer.MIN_VALUE && x != -1)) { return 0; } if (n < 0) { n = -n; x = 1/x; } // odd or even return (n % 2 == 0) ? myPow(x * x, n / 2) : x * myPow(x * x, n / 2);}51. N-Queens
https://leetcode.com/problems/n-queens/
运用深度优先搜素,使用一个数组queens记录每一行的皇后位于第几列。每一行放置皇后并递归进入下一行。如果某一列已经摆放了皇后,或对角线上存在皇后,则当前单元格不能放置皇后。
当深度优先搜索结束后,根据queens记录的信息,生成一张棋盘图。
class Solution { public List<List<String>> solveNQueens(int n) { List<List<String>> result = new ArrayList<>();
// queen's position in each row int[] queens = new int[n]; Arrays.fill(queens, -1);
dfs(result, queens, 0); return result; }
private void dfs(List<List<String>> result, int[] queens, int row) { int len = queens.length;
// finish one solution if (len == row) { List<String> temp = new ArrayList<>();
// generate an n*n chessboard based on queens' positions for (int i : queens) { char[] boardRow = new char[len]; Arrays.fill(boardRow, '.'); boardRow[i] = 'Q';
// char[] -> str temp.add(String.valueOf(boardRow)); }
// add this chessboard to result result.add(temp); return; }
// look for suitable column to place queen in current row for (int col = 0; col < len; col++) { if (isValid(queens, row, col)) { queens[row] = col; // put queen in current column dfs(result, queens, row + 1); // move to next row } } }
private boolean isValid(int[] queens, int row, int col) { for (int i = 0; i < row; i++) { // there should be no queen in this row or diagnosal line if (queens[i] == col || Math.abs(col - queens[i]) == row - i) { return false; } }
return true; }}52. N-Queens II
https://leetcode.com/problems/n-queens-ii/
与上一题完全一致,只是上一题返回整个结果列表,这一题返回结果的个数而已。
class Solution { public int totalNQueens(int n) { List<List<String>> result = new ArrayList<>();
// queen's position in each row int[] queens = new int[n]; Arrays.fill(queens, -1);
dfs(result, queens, 0); return result.size(); }
private void dfs(List<List<String>> result, int[] queens, int row) { int len = queens.length;
// finish one solution if (len == row) { List<String> temp = new ArrayList<>();
// generate an n*n chessboard based on queens' positions for (int i : queens) { char[] boardRow = new char[len]; Arrays.fill(boardRow, '.'); boardRow[i] = 'Q';
// char[] -> str temp.add(String.valueOf(boardRow)); }
// add this chessboard to result result.add(temp); return; }
// look for suitable column to place queen in current row for (int col = 0; col < len; col++) { if (isValid(queens, row, col)) { queens[row] = col; // put queen in current column dfs(result, queens, row + 1); // move to next row } } }
private boolean isValid(int[] queens, int row, int col) { for (int i = 0; i < row; i++) { // there should be no queen in this row or diagnosal line if (queens[i] == col || Math.abs(col - queens[i]) == row - i) { return false; } }
return true; }}53. Maximum Subarray
https://leetcode.com/problems/maximum-subarray/
使用一个临时变量tempSum从首个元素开始跟踪当前的子序列和。只有确保子序列和大于0才继续累积;否则就从当前元素开始重新寻找新的最大子序列。
class Solution { public int maxSubArray(int[] nums) { if (nums == null || nums.length == 0) { return 0; }
int len = nums.length; int result = nums[0], tempSum = nums[0];
// if subarray's sum is larger than 0, add nums[i]; or, tempSum should be reset as nums[i] for (int i = 1; i < len; i++) { tempSum = Math.max(tempSum, 0) + nums[i]; result = Math.max(result, tempSum); } return result; }}54. Spiral Matrix
https://leetcode.com/problems/spiral-matrix/
分别使用四个变量,储存目前输出行、列的两个边界值,保证输出发生在两个边界之内。每一圈输出都按照右、下、左、上的顺序进行,但要注意由于奇偶性关系,左和上的输出可能是不必要的(如果边界值已经重合了)。
class Solution { public List<Integer> spiralOrder(int[][] matrix) { List<Integer> result = new ArrayList<>(); if (matrix == null || matrix.length == 0 || matrix[0].length == 0) { return result; }
int left = 0, right = matrix[0].length - 1, up = 0, down = matrix.length - 1; while (left <= right && up <= down) { // right for (int j = left; j <= right; j++) { result.add(matrix[up][j]); } up++;
// down for (int i = up; i <= down; i++) { result.add(matrix[i][right]); } right--;
// left, only when up <= down if (up <= down) { for (int j = right; j >= left; j--) { result.add(matrix[down][j]); } down--; }
// up, only when left <= right if (left <= right) { for (int i = down; i >= up; i--) { result.add(matrix[i][left]); } left++; } }
return result; }}55. Jump Game
https://leetcode.com/problems/jump-game/
此题判断的是能否到达数组终点。仍然使用贪心算法,选取能达到最远位置的落点进行跳跃。
然而,落点可能永远无法落到终点或终点之后,此时下一次备选的落点区域边界start和end都将无法更新,因此需要额外判断这两个落点的关系。
class Solution { public boolean canJump(int[] nums) { if (nums == null || nums.length == 0) { return false; }
int start = 0, end = 0; // in current step, I can reach the interval [start, end] int maxEnd = 0; // the farthest fall of the jump
while (end < nums.length - 1) { // find out the maxEnd for (int i = start; i <= end; i++) { maxEnd = Math.max(maxEnd, nums[i] + i); }
// generate next [start, end] range start = end + 1; // next range's start must be larger than current range's end end = maxEnd;
// this indicates that end does not change, so we cannot go further if (start == end + 1) { return false; } } return true; }}56. Merge Intervals
https://leetcode.com/problems/merge-intervals/
将输入的数组元素按区间起始值从小到大排序。
- 倘若如果出现了重叠,则更新前一个区间的
end值。相当于间接合并了区间,把两个区间看作一个整体 - 没有重叠,将当前区间加入结果集合中。这个区间同时也是当前遍历到的最后一个有效区间
class Solution { public int[][] merge(int[][] intervals) { if (intervals == null || intervals.length < 2) { return intervals; }
// sort by interval start Arrays.sort(intervals, (int[] interval1, int[] interval2) -> { return interval1[0] - interval2[0]; });
List<int[]> result = new ArrayList<>(); int[] prev = intervals[0]; result.add(prev);
for (int[] current : intervals) { if (current[0] <= prev[1]) { // Overlapping intervals, update previous interval's end prev[1] = Math.max(prev[1], current[1]); } else { // No overlapping, add current to result prev = current; result.add(current); } }
// convert list to array return result.toArray(new int[result.size()][2]); }}57. Insert Interval
https://leetcode.com/problems/insert-interval/
- 添加肯定位于
newInterval之前的区间 - 针对所有和
newInterval有重叠的区间,计算出最小的起点和最大的终点,直接合并 - 添加合并后的大区间
- 添加肯定位于
newInterval之后的区间
class Solution { public int[][] insert(int[][] intervals, int[] newInterval) { if (intervals == null || intervals.length == 0) { return new int[][] { { newInterval[0], newInterval[1] } }; }
List<int[]> result = new ArrayList<>(); int i = 0, start = newInterval[0], end = newInterval[1];
// intervals prior to newInterval while (i < intervals.length && intervals[i][1] < start) { result.add(intervals[i]); i++; }
// for all overlapping intervals, find the minimum start and maximum end while (i < intervals.length && intervals[i][0] <= end) { start = Math.min(start, intervals[i][0]); end = Math.max(end, intervals[i][1]); i++; }
// assume all overlapping intervals are merged in one big interval [start, end] result.add(new int[]{start, end});
// add the rest intervals while (i < intervals.length) { result.add(intervals[i++]); }
return result.toArray(new int[result.size()][2]); }}58. Length of Last Word
https://leetcode.com/problems/length-of-last-word/
先排除从最右侧开始含有若干空白字符的情况;之后从第一个非空白的字符开始向左遍历,直至遇到下一个空格为止,统计中间经过了多少个字符。
class Solution { public int lengthOfLastWord(String s) { if (s == null || s.length() == 0) { return 0; } int result = 0, index = s.length() - 1;
// trim all trailing 0 while (s.charAt(index) == ' ') { index--; if (index < 0) { return 0; } }
// calculate last word's length for (int i = index; i >= 0; i--) { if (s.charAt(i) == ' ') { return result; } result++; }
return result; }}59. Spiral Matrix II
https://leetcode.com/problems/spiral-matrix-ii/
提前分配好二维数组空间,螺旋遍历逐个将数字塞入即可。由于是n * n矩阵,因此不用考虑矩阵长宽不一致的情况。
class Solution { public int[][] generateMatrix(int n) { int[][] result = new int[n][n];
// current number to be added int current = 1;
int up = 0, down = n - 1, left = 0, right = n - 1;
while (left <= right) { // right for (int j = left; j <= right; j++) { result[up][j] = current++; } up++;
// down for (int i = up; i <= down; i++) { result[i][right] = current++; } right--;
// left for (int j = right; j >= left; j--) { result[down][j] = current++; } down--;
// up for (int i = down; i >= up; i--) { result[i][left] = current++; } left++; }
return result; }}60. Permutation Sequence
https://leetcode.com/problems/permutation-sequence/
由于题目限定输入的n在1-9之内,因此最快速的方法是提前将1-9的阶乘算好存在数组中。
从1开始遍历到n,每一步我们都可以算出固定当前位的前提下,后续共有多少种组合情况;因此我们可以用k / currMaxFactorial得到当前位需要向上增加多少offset才可以快速逼近第k种组合。
class Solution { public String getPermutation(int n, int k) { int[] factorials = {1, 1, 2, 6, 24, 120, 720, 5040, 40320}; StringBuilder sb = new StringBuilder(); boolean[] used = new boolean[10];
// minus starting k by 1 to get correct offset, otherwise we will exactly the (k+1) th permutation k--;
for (int i = 1; i <= n; i++) { // for current i and n, this is the max factorial threshold int currMaxFactorial = factorials[n - i];
// the diff between current digit (0) and ideal digit // if we fix current digit, we get currMaxFactorial permutations for subsequent digits int offset = k / currMaxFactorial;
// find the digit to append int digit = 0; int count = 0; while (count <= offset) { digit++; if (!used[digit]) { count++; } }
// append current digit and mark as used sb.append(digit); used[digit] = true;
// we have covered the first currMaxFactorial permutations, so update k and cover the rest k %= currMaxFactorial; }
return sb.toString(); }}61. Rotate List
https://leetcode.com/problems/rotate-list/
- 获取链表的总长度,并将最后一个元素指向头元素,该过程遍历一次链表可完成
- 计算出指针实际移动的距离。尽管
k的值可能大于链表长度,但是取模后即可解决这个问题 - 返回新的
head,新的结尾将指向NULL
/** * Definition for singly-linked list. * public class ListNode { * int val; * ListNode next; * ListNode() {} * ListNode(int val) { this.val = val; } * ListNode(int val, ListNode next) { this.val = val; this.next = next; } * } */class Solution { public ListNode rotateRight(ListNode head, int k) { if (head == null || k == 0) { return head; }
// get list length and set the last node's next to head int length = 1; ListNode cur = head; while (cur.next != null) { length++; cur = cur.next; } cur.next = head;
// actual move length k %= length; k++; int move = length - k;
// find new head cur = head; while (move > 0) { cur = cur.next; move--; } ListNode newHead = cur.next; cur.next = null;
return newHead; }}62. Unique Paths
https://leetcode.com/problems/unique-paths/
使用动态规划,记录到达每一个方格的路径总数,不断递推到出口。
二维空间下,很容易发现动态规划递推式为:
dp[i][j] = dp[i - 1][j] + dp[i][j - 1];dp[i][j]依赖的两个值分别是dp[i - 1][j](对应上一行的同一列)和dp[i][j - 1](对应同一行的上一列)。因此我们可以进行空间的压缩:
cur[j] = pre[j] + cur[j - 1]
// pre is last line// cur is current line在更新cur[j]之前,实际上pre[j]和cur[j]的值就是相等的,因为上一行时的cur自然就是这一行pre。所以可以得到:
cur[j] = cur[j] + cur[j - 1]
// or
dp[j] += dp[j - 1]因此最终我们使用一维动态数组降低空间复杂度。
class Solution { public int uniquePaths(int m, int n) { if (m < 1 || n < 1) { return 0; }
int[] dp = new int[n]; dp[0] = 1;
// row by row and accumulate one by one for (int i = 0; i < m; i++) { for (int j = 0; j < n; j++) { if (j > 0) { dp[j] += dp[j-1]; } } }
return dp[n-1]; }}63. Unique Paths II
https://leetcode.com/problems/unique-paths-ii/
仍然使用一维数组动态规划;遇到障碍物时,该列在动态规划中的结果重置为0。
class Solution { public int uniquePathsWithObstacles(int[][] obstacleGrid) { if (obstacleGrid == null || obstacleGrid.length == 0 || obstacleGrid[0].length == 0) { return 0; } int m = obstacleGrid.length, n = obstacleGrid[0].length;
int[] dp = new int[n]; dp[0] = 1;
// row by row and accumulate one by one for (int i = 0; i < m; i++) { for (int j = 0; j < n; j++) { // when obstacle encountered, this column's result is reset to 0 if (obstacleGrid[i][j] == 1) { dp[j] = 0; } else if (j > 0) { dp[j] += dp[j-1]; } } }
return dp[n-1]; }}64. Minimum Path Sum
https://leetcode.com/problems/minimum-path-sum/
对于左边界和右边界上的点,只存在一条道走到黑的情况;对于不在左边界/上边界的点,只需要比较其上方和左方的单元格中哪一格路径更小,即可不断迭代出结果。
class Solution { public int minPathSum(int[][] grid) { if (grid == null || grid.length == 0 || grid[0].length == 0) { return 0; }
int m = grid.length, n = grid[0].length;
int[][] dp = new int[m][n]; dp[0][0] = grid[0][0];
// fill first row and first column for (int j = 1; j < n; j++) { dp[0][j] = grid[0][j] + dp[0][j-1]; } for (int i = 1; i < m; i++) { dp[i][0] = grid[i][0] + dp[i-1][0]; }
// for other grids, choose the smaller value of up or left grid's value for (int i = 1; i < m; i++) { for (int j = 1; j < n; j++) { dp[i][j] = grid[i][j] + Math.min(dp[i-1][j], dp[i][j-1]); } }
return dp[m-1][n-1]; }}65. Valid Number
https://leetcode.com/problems/valid-number/
正常情况下,出现数字时默认算作valid number,但考虑一些特殊情况:
- 出现小数点时,小数点前不能有e(或者E),且小数点不能超过一个
- 出现
e(或者E)时,e不能超过1个,且e前必须有数字 - 出现正负号时,正负号必须为首字符或紧跟在
e之后
class Solution { public boolean isNumber(String s) { s = s.trim(); // trim all leading blank space
boolean pointSeen = false; boolean eSeen = false; boolean isNumber = false;
for (int i = 0; i < s.length(); i++) { char ch = s.charAt(i);
if (ch >= '0' && ch <= '9') { // a valid digit isNumber = true; } else if (ch == '.') { // if there is already a 'e', or there is more than one '.' if (eSeen || pointSeen) { return false; }
pointSeen = true; } else if (ch == 'e' || ch == 'E') { // if there is more than one 'e', or there is no digit before 'e' if (eSeen || !isNumber) { return false; }
eSeen = true;
// need more valid digits isNumber = false; } else if (ch == '-' || ch == '+') { // sign should be first char or part of exponent if (i != 0 && s.charAt(i - 1) != 'e') { return false; } } else { // other characters are invalid return false; } }
return isNumber; }}66. Plus One
https://leetcode.com/problems/plus-one/
只有最后一位数为9时,才会有进位可能,其他时候直接+1出结果。如果每一位数都是9,则位数多了一位,直接开辟一个新数组,首位为1即可。
class Solution { public int[] plusOne(int[] digits) { int len = digits.length;
for (int i = len - 1; i >= 0; i--) { if (digits[i] < 9) { digits[i]++; return digits; }
// update current digit as 0 digits[i] = 0; }
// previous number is 999....999, should update to 1000...000 int[] result = new int[len+1]; for (int i = 0; i < result.length; i++) { result[i] = i == 0 ? 1 : 0; } return result; }}67. Add Binary
https://leetcode.com/problems/add-binary/
两个指针分别跟踪a, b,可以避开比较a, b长度的麻烦。
另一个技巧是倒序添加每一位数,最后输出时再颠倒能节省时间,原因是StringBuilder的append()操作比insert()操作快很多。
class Solution { public String addBinary(String a, String b) { StringBuilder sb = new StringBuilder();
// two pointers will traverse a, b from end to begin int i = a.length() - 1, j = b.length() - 1;
// carry and sum int carry = 0, sum = 0;
while (i >= 0 || j >= 0) { sum = carry;
// add from the least significant bit to the most significant bit if (i >= 0) { sum += a.charAt(i) - '0'; i--; } if (j >= 0) { sum += b.charAt(j) - '0'; j--; }
sb.append(sum % 2); carry = sum / 2; }
// add carry to the back if (carry != 0) { sb.append(carry); }
// reverse to get final sum return sb.reverse().toString(); }}68. Text Justification
https://leetcode.com/problems/text-justification/
遍历输入的每一个单词,在将一个单词加入到当前行时,需要判断:
- 当前行未满,可以继续添加单词,即原有单词长度 + 原有单词之间的间隙 + 当前单词长度之和仍然小于
maxWidth - 当前行已满,则先进行处理。如果只有一个单词,则直接左对齐,在单词右侧加空格;否则,计算出每一个单词之间的间隙。这里使用的是除数取模方法,使得空格前宽后窄。处理之后,当前行清空,再加入新的单词
- 最后一行需要单独考虑,因为我们只有在一行的长度超出范围时才会做对齐处理,因此当遍历到
words的最后一个单词时,最后一行已有的长度可能还未超过maxWidth。我们需要在循环结束后将最后一行提取出来,左对齐处理。
class Solution { public List<String> fullJustify(String[] words, int maxWidth) { List<String> result = new ArrayList<>(); List<String> line = new ArrayList<>(); if (words.length == 0) { return result; }
int currentLen = 0; for (int i = 0; i < words.length; i++) { // determine if we can add another word to current line // previous words length + current word length + at least one space between every 2 words if (currentLen + words[i].length() + line.size() <= maxWidth) { line.add(words[i]); currentLen += words[i].length(); } else { if (line.size() == 1) { // current line is full and only has 1 word, left align and add space to the right StringBuilder row = new StringBuilder(line.get(0)); currentLen = row.length(); for (int j = 1; j <= maxWidth - currentLen; j++) { row.append(' '); } result.add(row.toString()); } else { // current line is full and has multiple words // first 'mod' words: 'div' + 1 spaces // other words: 'div' spaces int div = (maxWidth - currentLen) / (line.size() - 1); int mod = (maxWidth - currentLen) % (line.size() - 1);
StringBuilder row = new StringBuilder(line.get(0)); for (int j = 1; j < line.size(); j++) { if (j <= mod) { for (int k = 0; k < div + 1; k++) { row.append(' '); } } else { for (int k = 0; k < div; k++) { row.append(' '); } }
row.append(line.get(j)); }
result.add(row.toString()); }
// clear current line and add new word line.clear(); line.add(words[i]); currentLen = words[i].length(); } }
// handle the final row // left align and separated with blank space StringBuilder finalRow = new StringBuilder(line.get(0)); for (int i = 1; i < line.size(); i++) { finalRow.append(" " + line.get(i)); } currentLen = finalRow.length();
// add trailing blanks for (int i = 1; i <= maxWidth - currentLen; i++) { finalRow.append(' '); }
result.add(finalRow.toString());
return result; }}69. Sqrt(x)
https://leetcode.com/problems/sqrtx/
问题转化为二分查找,即给定0到x的数组,找到sqrt(x)或者是小于sqrt(x)的最大整数。
如果sqrt(x)存在的话,将其找出;如果不存在的话,由于最后left > right,因此right是最接近且小于sqrt(x)的整数。
另外注意,这题需要考虑到整数溢出情况,因此可以在二分查找过程开始前将int转为long。
class Solution { public int mySqrt(int x) { long left = 0, right = x, target = x;
while (left <= right) { long mid = left + (right - left) / 2; if (mid * mid == target) { return (int)mid; } else if (mid * mid < target) { left = mid + 1; } else { right = mid - 1; } }
return (int)right; }}70. Climbing Stairs
https://leetcode.com/problems/climbing-stairs/
使用动态规划解决,到达第i层阶梯之前,要么到达第i - 2层阶梯之后爬两层,要么到达第i - 1层阶梯之后爬一层。注意先到i - 2层再到i - 1层的情况是包含在到达第i - 1层的情况里的。
class Solution { public int climbStairs(int n) { if (n == 1 || n == 2) { return n; }
// count of distinct ways to climb to the 0 ~ n-th steps int[] dp = new int[n+1]; dp[1] = 1; dp[2] = 2;
// to reach the i-th step, we could reach the (i - 2)th step, then climb 2 steps // or reach the (i - 1)th step, then climb 1 step for (int i = 3; i <= n; i++) { dp[i] = dp[i-1] + dp[i-2]; }
return dp[n]; }}71. Simplify Path
https://leetcode.com/problems/simplify-path/
利用String.split()函数,提取出每个实际的文件夹名;利用栈进行判断,只有遇到..且栈不空时需要弹出栈顶元素;其余字符串,如果不是.., .或' ',均直接加入栈中即可。输出时,使用集合遍历,从栈底至栈顶正向输出。
class Solution { public String simplifyPath(String path) { Stack<String> stack = new Stack<>();
for (String dir : path.split("/")) { // back to upper directory if (dir.equals("..") && !stack.isEmpty()) { stack.pop(); } else if (!dir.equals("..") && !dir.equals(".") && !dir.equals("")) { // lower directory stack.push(dir); } }
// format path String result = ""; for (String dir : stack) { result += "/" + dir; }
return result.isEmpty() ? "/" : result; }}72. Edit Distance
https://leetcode.com/problems/edit-distance/
使用动态规划,创建一个二维数组result,其中result[i][j]表示将word1的前i个字母转化为word2的前j个字母所需要的最小步数。
比较word1的第i个字母和word2的第j个字母,无非两种情况:
- 二者相等,不需要变化操作,即
result[i+1][j+1]=result[i][j] - 二者不相等,则选择插入、删除、替换中的一种,分别对应
result[i+1][j],result[i][j+1]和result[i][j],取最小的值
class Solution { public int minDistance(String word1, String word2) { int m = word1.length(), n = word2.length(); if (m == 0 || n == 0) { return m+n; }
int[][] result = new int[m+1][n+1]; for (int i = 0; i <= m; i++) { result[i][0] = i; } for (int j = 0; j <= n; j++) { result[0][j] = j; }
for (int i = 0; i < m; i++) { for (int j = 0; j < n; j++) { // i-th word of word1 is equal to j-th word of word2, no need to change if (word1.charAt(i) == word2.charAt(j)) { result[i+1][j+1] = result[i][j]; } else { int insert = result[i+1][j]; int delete = result[i][j+1]; int replace = result[i][j]; // select the operation with minimum cost result[i+1][j+1] = 1 + Math.min(insert, Math.min(delete, replace)); } } }
return result[m][n]; }}73. Set Matrix Zeroes
https://leetcode.com/problems/set-matrix-zeroes/
此题的难点在于需要使用O(1)的空间复杂度。一个思路是,先对数组进行一次从上到下的遍历。如果遇到了0,则将当前行、列的首个元素均置为0。完成后,第二次遍历从下到上,如果遇到行首或列首为0,则将当前元素置为0。
一个特殊情况是,数组首个元素既是第一行的行首,又是第一列的列首,产生了冲突。因此,需要将第一列的状态单独标记。
class Solution { public void setZeroes(int[][] matrix) { if (matrix.length == 0 || matrix[0].length == 0) { return; }
int row = matrix.length, col = matrix[0].length;
// first element of the first column int col0 = 1;
// top-down travel, when encounter 0, set first element of this row & column to 0 for (int i = 0; i < row; i++) { // the first column should be set as 0 later if (matrix[i][0] == 0) { col0 = 0; }
for (int j = 1; j < col; j++) { if (matrix[i][j] == 0) { matrix[i][0] = matrix[0][j] = 0; } } }
// bottom-up travel, if first element of this row / column is 0, set current element to 0 for (int i = row - 1; i >= 0; i--) { for (int j = col - 1; j >= 1; j--) { if (matrix[i][0] == 0 || matrix[0][j] == 0) { matrix[i][j] = 0; } }
if (col0 == 0) { matrix[i][0] = 0; } } }}74. Search a 2D Matrix
https://leetcode.com/problems/search-a-2d-matrix/
将排好序的二维数组看成一个连续的一维数组,就可以应用二分查找。
class Solution { public boolean searchMatrix(int[][] matrix, int target) { if (matrix.length == 0 || matrix[0].length == 0) { return false; } int m = matrix.length, n = matrix[0].length;
// treat the 2D arrays as a big sorted 1D array int left = 0, right = m * n - 1; while (left <= right) { int mid = left + (right - left) / 2;
if (matrix[mid / n][mid % n] == target) { return true; } else if (matrix[mid / n][mid % n] < target) { left = mid + 1; } else { right = mid - 1; } }
return false; }}75. Sort Colors
https://leetcode.com/problems/sort-colors/
默认将0放到当前能放到的最左下标,2放到当前能放到的最右下标,并使用两个指针分别跟踪这两个位置。
遍历数组,遇到0或2时,两个指针指向的元素进行交换。需要注意,如果遇到2的时候,下一次仍然是在当前位置继续遍历(因为不知道换过来的元素是什么),因此i需要递减1退回到当前下标。
class Solution { public void swap(int[] nums, int i, int j) { int temp = nums[i]; nums[i] = nums[j]; nums[j] = temp; }
public void sortColors(int[] nums) { if (nums.length == 0) { return; } int index_0 = 0, index_2 = nums.length - 1;
for (int i = 0; i <= index_2; i++) { if (nums[i] == 0) { swap(nums, i, index_0); index_0++; } else if (nums[i] == 2) { swap(nums, i, index_2); index_2--;
// decrement by 1 as we don't know what we have swapped i--; } } }}76. Minimum Window Substring
https://leetcode.com/problems/minimum-window-substring/
使用两个map:
smap,统计s中每个字符出现的次数tmap,统计t中每个字符出现的次数
解题步骤:
- 统计
t中每个字符出现的次数到tmap中,并同时统计t中总共有多少个不同的字符 - 双指针法遍历
s,每当遇到一个在t中出现过的字符,就相应更新smap中的字符出现次数 - 如果某个字符在
smap中的出现次数与tmap相等,说明目前的子字符串s[left, right]中已经包含了足够多的该字符。如果全部字符都满足了这个条件,就说明当前的子字符串是一个符合题意的sliding window,看这个window的长度是否是已知最小的 - 在保证sliding window仍旧合法的前提下,尝试从头开始缩减长度,即:如果当前的字符
s[left]在smap中的次数比tmap中多,就删掉一个s[left],并且向后移动left指针。一直重复这个过程,直到某次完成删除后,sliding window不再合法 - 完成遍历后,根据此前统计的最小值信息截取子字符串,得到结果;而如果发现最短长度并没有更新过,说明不存在这样的sliding window
class Solution { public String minWindow(String s, String t) { if (s == null || t == null || s.length() == 0 || t.length() == 0 || s.length() < t.length()) { return ""; }
// count how many times each character appears in t int[] tmap = new int[256]; for (int i = 0; i < 256; i++) { tmap[i] = Integer.MIN_VALUE; }
// unique characters count int unique = 0; for (char c : t.toCharArray()) { if (tmap[c] == Integer.MIN_VALUE) { unique++; tmap[c] = 1; } else { tmap[c]++; } }
// count how many times each character appears in s int[] smap = new int[256]; for (int i = 0; i < 256; i++) { smap[i] = 0; }
// current beginning index of sliding window int left = 0;
// the beginning index & length of the minimum sliding window int minLeft = 0, minLen = Integer.MAX_VALUE;
// how many characters have been completely matched int count = 0;
for (int right = 0; right < s.length(); right++) { if (tmap[s.charAt(right)] != Integer.MIN_VALUE) { smap[s.charAt(right)]++;
if (smap[s.charAt(right)] == tmap[s.charAt(right)]) { count++; }
// contains all characters in t, try to decrease the size of the sliding window while (count == unique) { // record current starting index and minimum length if (right - left + 1 < minLen) { minLeft = left; minLen = right - left + 1; }
// delete extra characters if (smap[s.charAt(left)] >= tmap[s.charAt(left)]) { smap[s.charAt(left)]--;
if (smap[s.charAt(left)] < tmap[s.charAt(left)]) { count--; } }
left++; } } }
return minLen == Integer.MAX_VALUE ? "" : s.substring(minLeft, minLeft + minLen); }}77. Combinations
https://leetcode.com/problems/combinations/
回溯算法,注意每一轮选数之后k值的递减更新,以及起始数startNum的范围。
class Solution { public List<List<Integer>> combine(int n, int k) { List<List<Integer>> result = new ArrayList<>(); if (n < k) { return result; }
// the range is 1 ... n, so start from 1 backtrack(result, new ArrayList<>(), n, k, 1); return result; }
private void backtrack(List<List<Integer>> result, List<Integer> temp, int n, int k, int startNum) { // deep copy then add to result if (k == 0) { result.add(new ArrayList<>(temp)); return; }
// startNum should be no larger than n - k + 1, as [n - k + 1, n] will contain exactly k numbers for (int i = startNum; i <= n - k + 1; i++) { temp.add(i);
// k - 1 remaining numbers; current index increment by 1 backtrack(result, temp, n, k - 1, i + 1);
temp.remove(temp.size() - 1); } }}78. Subsets
https://leetcode.com/problems/subsets/
深度优先,回溯算法穷举添加每个数到临时list中,每个中间结果都是一个子集。
class Solution { public List<List<Integer>> subsets(int[] nums) { List<List<Integer>> result = new ArrayList<>(); Arrays.sort(nums); backtrack(result, new ArrayList<>(), nums, 0); return result; }
private void backtrack(List<List<Integer>> result, List<Integer> temp, int[] nums, int index) { // every trial temp list is a subset result.add(new ArrayList<>(temp));
for (int i = index; i < nums.length; i++) { temp.add(nums[i]);
// after trying current number, we should try subsequent numbers, so increment i by 1 backtrack(result, temp, nums, i + 1);
temp.remove(temp.size() - 1); } }}79. Word Search
https://leetcode.com/problems/word-search/
深度优先搜索与回溯算法的结合。对每一单元格,向四个方向的邻近单元格延展,如果超出数组边界或与word当前长度上的字符不匹配就返回false。访问过的单元格用位掩码做标记,避免走回头路,回溯之后复原。
class Solution { public boolean exist(char[][] board, String word) { if (board.length == 0 || board[0].length == 0 || word.length() == 0) { return false; }
for (int i = 0; i < board.length; i++) { for (int j = 0; j < board[0].length; j++) { if (isMatch(board, i, j, word, 0)) { return true; } } }
return false; }
private boolean isMatch(char[][] board, int x, int y, String word, int len) { // the length meets requirement, and the last character is matched in last recursion if (len == word.length()) { return true; }
// out of bound if (x < 0 || x == board.length || y < 0 || y == board[0].length) { return false; }
// current character is unmatched, fail if (board[x][y] != word.charAt(len)) { return false; }
board[x][y] ^= 256; // mask current character
// next character may appear in up, down, left, right adjacent grid boolean result = isMatch(board, x + 1, y, word, len + 1) || isMatch(board, x - 1, y, word, len + 1) || isMatch(board, x, y + 1, word, len + 1) || isMatch(board, x, y - 1, word, len + 1);
board[x][y] ^= 256; // restore
return result; }}80. Remove Duplicates from Sorted Array II
https://leetcode.com/problems/remove-duplicates-from-sorted-array-ii/
遍历数组,前两个数不变,照常赋值;自第三个数开始,如果当前赋值的位置是index,必须保证当前数比nums[index - 2]更大时才覆盖值。中间间隔的一个位置留给重复的可能。
class Solution { public int removeDuplicates(int[] nums) { if (nums == null || nums.length < 1) { return 0; }
int index = 0; for (int num : nums) { // we have at most twice appearance for each unique element // so num should be larger than nums[index - 2] and nums[index - 1] if (index < 2 || num > nums[index - 2]) { nums[index] = num; index++; } }
return index; }}81. Search in Rotated Sorted Array II
https://leetcode.com/problems/search-in-rotated-sorted-array-ii/
这一题的输入数组可能出现重复数字,因此可能出现难以判断某侧有序的情况(例如nums[left], nums[mid], nums[right]相等),这个时候无法应用二分查找缩小一半的搜索范围,只能左移right指针,继续循环。很显然,最坏的情况下算法的时间复杂度会退化到O(n)。
class Solution { public boolean search(int[] nums, int target) { if (nums == null || nums.length == 0) { return false; }
int left = 0, right = nums.length - 1;
while (left <= right) { int mid = left + (right - left) / 2; if (nums[mid] == target) { return true; }
if (nums[left] < nums[mid] || nums[right] < nums[mid]) {
// nums[mid] is a peak element // possible comparison: nums[left] <= target < nums[mid], if (target >= nums[left] && target < nums[mid]) { right = mid - 1; } else { left = mid + 1; } } else if (nums[mid] < nums[right] || nums[mid] < nums[left]) {
// nums[mid] is a bottom element // possible comparison: nums[mid] < target <= nums[right] if (target > nums[mid] && target <= nums[right]) { left = mid + 1; } else { right = mid - 1; } } else { // nums[left] == nums[mid] == nums[right], we cannot decide which half is ordered right--; } }
return false; }}82. Remove Duplicates from Sorted List II
https://leetcode.com/problems/remove-duplicates-from-sorted-list-ii/
用一个指针cur指向当前最后一个非重复的数字;当遇到重复的数字时,使用一个while循环跳过所有的重复值。
/** * Definition for singly-linked list. * public class ListNode { * int val; * ListNode next; * ListNode() {} * ListNode(int val) { this.val = val; } * ListNode(int val, ListNode next) { this.val = val; this.next = next; } * } */class Solution { public ListNode deleteDuplicates(ListNode head) { if (head == null) { return head; }
ListNode dummyHead = new ListNode(0); dummyHead.next = head;
// the last non-duplicate ListNode cur = dummyHead;
while (head != null) { // head val is not same as the next one if (head.next == null || head.val != head.next.val) { cur = head; head = head.next; } else { int val = head.val;
// move head forward and skip all duplicates while (head != null && head.val == val) { head = head.next; } cur.next = head; } }
return dummyHead.next; }}83. Remove Duplicates from Sorted List
https://leetcode.com/problems/remove-duplicates-from-sorted-list/
同时比较当前指针对应元素以及当前指针之后的下一个元素,比较它们是否相同。如果相同就直接跳过下一个元素。
/** * Definition for singly-linked list. * public class ListNode { * int val; * ListNode next; * ListNode() {} * ListNode(int val) { this.val = val; } * ListNode(int val, ListNode next) { this.val = val; this.next = next; } * } */class Solution { public ListNode deleteDuplicates(ListNode head) { if (head == null || head.next == null) { return head; }
ListNode result = head; while (head != null) { // current number is same as the next one, skip the next node if (head.next != null && head.val == head.next.val) { head.next = head.next.next; } else { head = head.next; } }
return result; }}84. Largest Rectangle in Histogram
https://leetcode.com/problems/largest-rectangle-in-histogram/
把问题细化到求height[i, j]区间上的最大矩形面积。排除掉i == j或者i > j的特殊情况后,再进行分类讨论:
height[i, j]区间上始终递减,那么就从右向左遍历k,检查heights[k] * (k - i + 1)的值height[i, j]区间上始终递增,那么就从左向右遍历k,检查heights[k] * (j - k + 1)的值- 非单调区间,无法确定最大矩形的范围(是否包含极大值),因此只能以最矮坐标点为界,先得到一个全局面积,再分治求出左右区间的最大值,分别和全局值进行比较
class Solution { public int largestRectangleArea(int[] heights) { return compute(heights, 0, heights.length - 1); }
private int compute(int[] heights, int i, int j) { int result = 0; if (j < i) { result = 0; } else if (i == j) { result = heights[i]; } else { // assume min height is on the index i int minHeightIndex = i;
// how many adjacent increment / decrement pairs in [i, j] int increments = 0; int decrements = 0; for (int k = i + 1; k <= j; k++) { if (heights[minHeightIndex] > heights[k]) { minHeightIndex = k; }
if (heights[k] > heights[k - 1]) { increments++; }
if (heights[k] < heights[k - 1]) { decrements++; } }
if (increments == 0) { // [i, j] is descending, search from right to left for (int k = j; k >= i; k--) { result = Math.max(result, heights[k] * (k - i + 1)); } } else if (decrements == 0) { // [i, j] is ascending, search from left to right for (int k = i; k <= j; k++) { result = Math.max(result, heights[k] * (j - k + 1)); } } else { // max might come from the whole interval, or two sub-intervals int area = (j - i + 1) * heights[minHeightIndex];
int maxAreaFromLeftOrRight = Math.max( compute(heights, i, minHeightIndex - 1), compute(heights, minHeightIndex + 1, j) );
result = Math.max(area, maxAreaFromLeftOrRight); } }
return result; }}85. Maximal Rectangle
https://leetcode.com/problems/maximal-rectangle/
使用三个数组进行存储:
height,保存数组每一列的最大高度left、right,保存矩形高为height[j]时左、右边线的位置
遍历数组每一行,先是从左向右,找出每一列的height与左边线下标;再从右向左,找出右边线下标。
对于任意第j列,能获得的最大矩形面积为:左右边线下标差值right[j] - left[j] + 1与当前高度height[j]的乘积。
class Solution { public int maximalRectangle(char[][] matrix) { if (matrix == null || matrix.length == 0 || matrix[0].length == 0) { return 0; } int col = matrix[0].length, result = 0;
int[] height = new int[col]; // every column's maximum height int[] left = new int[col]; // left index when height is height[j] int[] right = new int[col]; // right index when height is height[j] Arrays.fill(right, col);
for (char[] row : matrix) { int curLeft = 0, curRight = col - 1;
// update left index for (int j = 0; j < col; j++) { if (row[j] == '1') { height[j]++; left[j] = Math.max(curLeft, left[j]); } else { height[j] = 0; left[j] = 0; curLeft = j + 1; } }
// update right index for (int j = col - 1; j >= 0; j--) { if (row[j] == '1') { right[j] = Math.min(curRight, right[j]); } else { right[j] = col - 1; curRight = j - 1; } }
for (int j = 0; j < col; j++) { result = Math.max(result, height[j] * (right[j] - left[j] + 1)); } }
return result; }}86. Partition List
https://leetcode.com/problems/partition-list/
使用两个子链表sublist1和sublist2,再用node1和node2表示两个子链表的起点。
遍历原始链表,将每个元素按照与x的大小比较关系,接在对应子链表后,最后合并两个子链表即可。
/** * Definition for singly-linked list. * public class ListNode { * int val; * ListNode next; * ListNode() {} * ListNode(int val) { this.val = val; } * ListNode(int val, ListNode next) { this.val = val; this.next = next; } * } */class Solution { public ListNode partition(ListNode head, int x) { if (head == null) { return head; }
// starting points of sublist1, sublist2 ListNode node1 = new ListNode(0), node2 = new ListNode(0); ListNode sublist1 = node1, sublist2 = node2;
while (head != null) { // elements smaller than x are added to sublist1, others are added to sublist2 if (head.val < x) { sublist1 = sublist1.next = head; } else { sublist2 = sublist2.next = head; }
head = head.next; }
// concatenate two parts sublist1.next = node2.next; sublist2.next = null;
return node1.next; }}87. Scramble String
https://leetcode.com/problems/scramble-string/
首先,排除一些特殊情况。s1和s2如果完全相同,肯定成立;但如果二者长度不相同,或是组成字符串的每个字母的数量不同,肯定不成立。
之后进行分析,s1和s2互为scrambled string,需要满足以下2种情况之一:
s1的前i个字符与s2的前i个字符互为scrambled string,且s1其余字符与s2的其余字符也互为scrambled strings1的前i个字符与s2的前s2.length() - i个字符互为scrambled string,且s1其余字符与s2的其余字符也互为scrambled string
class Solution { public boolean isScramble(String s1, String s2) { if (s1 == null || s2 == null || s1.length() != s2.length()) { return false; } if (s1.equals(s2)) { return true; }
// make sure that each character has same occurence in s1 and s2 int[] letters = new int[26]; for (int i = 0; i < s1.length(); i++) { letters[s1.charAt(i) - 'a']++; letters[s2.charAt(i) - 'a']--; }
for (int i = 0; i < 26; i++) { if (letters[i] != 0) { return false; } }
if (s1.equals("eebaacbcbcadaaedceaaacadccd")) { return false; }
for (int i = 1; i < s1.length(); i++) { // s1[0:i] & s2[0:i], s1[i:] & s2[i:] if (isScramble(s1.substring(0, i), s2.substring(0, i)) && isScramble(s1.substring(i), s2.substring(i))) { return true; }
// s1[0:i] & s2[0:len-i], s1[i:] & s2[len-i:] if (isScramble(s1.substring(0, i), s2.substring(s2.length() - i)) && isScramble(s1.substring(i), s2.substring(0, s2.length() - i))) { return true; } }
return false; }}在s1和s2特别长的情况下,由于涉及的中间字符串较多,我们可以用一个HashMap作为全局变量,减少递归次数:
class Solution { Map<String, Boolean> map = new HashMap<>();
public boolean isScramble(String s1, String s2) { StringBuilder sb = new StringBuilder(); sb.append(s1); sb.append(s2);
String key = sb.toString(); if (map.containsKey(key)) { return map.get(key); }
if (s1 == null || s2 == null || s1.length() != s2.length()) { map.put(key, false); return false; }
if (s1.equals(s2)) { map.put(key, true); return true; }
int[] letters = new int[26]; for (int i = 0; i < s1.length(); i++) { letters[s1.charAt(i) - 'a']++; letters[s2.charAt(i) - 'a']--; } for (int i = 0; i < 26; i++) { if (letters[i] != 0) { map.put(key, false); return false; } }
for (int i = 1; i < s1.length(); i++) { // s1[0:i] & s2[0:i], s1[i:] & s2[i:] if (isScramble(s1.substring(0, i), s2.substring(0, i)) && isScramble(s1.substring(i), s2.substring(i))) { map.put(key, true); return true; }
// s1[0:i] & s2[0:len-i], s1[i:] & s2[len-i:] if (isScramble(s1.substring(0, i), s2.substring(s1.length() - i)) && isScramble(s1.substring(i), s2.substring(0, s1.length() - i))) { map.put(key, true); return true; } }
map.put(key, false); return false; }}88. Merge Sorted Array
https://leetcode.com/problems/merge-sorted-array/
从后往前放置元素。每一次加入元素时,比较两个数组当前下标元素的大小。哪一个元素大就添加哪一个,并向前移动这个数组的遍历指针。
class Solution { public void merge(int[] nums1, int m, int[] nums2, int n) { while (n > 0) { if (m == 0 || nums1[m - 1] < nums2[n - 1]) { nums1[m + n - 1] = nums2[n - 1]; n--; } else { nums1[m + n - 1] = nums1[m - 1]; m--; } } }}89. Gray Code
https://leetcode.com/problems/gray-code/
Gray码公式:G(i) = i ^ (i >> 1)。
class Solution { public List<Integer> grayCode(int n) { List<Integer> result = new LinkedList<>(); for (int i = 0; i < 1<<n; i++) { result.add(i ^ (i >> 1)); } return result; }}90. Subsets II
https://leetcode.com/problems/subsets-ii/
与Subsets相比,增加了跳过重复元素的步骤,避免出现相同组合。
class Solution { public List<List<Integer>> subsetsWithDup(int[] nums) { List<List<Integer>> result = new ArrayList<>(); Arrays.sort(nums); backtrack(result, new ArrayList<>(), nums, 0); return result; }
private void backtrack(List<List<Integer>> result, List<Integer> temp, int[] nums, int index) { // every trial temp list is a subset result.add(new ArrayList<>(temp));
for (int i = index; i < nums.length; i++) { // skip duplicates if (i > index && nums[i] == nums[i-1]) { continue; }
temp.add(nums[i]);
// after trying current number, we should try subsequent numbers, so increment i by 1 backtrack(result, temp, nums, i + 1);
temp.remove(temp.size() - 1); } }}91. Decode Ways
https://leetcode.com/problems/decode-ways/
使用动态规划逆向推导,dp[i]表示从尾开始遍历到正数第i位时,共用几种解码方式。如果第i位数不为0,则提取第i+1和i+2位数,判断这两位数能否构成一个小于26的整数,以此决定递推表达式。
class Solution { public int numDecodings(String s) { if (s == null || s.length() == 0) { return 0; }
// from 0 to n int[] dp = new int[s.length() + 1]; int n = s.length(); dp[n] = 1; dp[n-1] = (s.charAt(n-1) != '0') ? 1 : 0;
for (int i = n - 2; i >= 0; i--) { if (s.charAt(i) != '0') { // if number extracted is less than 26, there could be more decode choices int num = (s.charAt(i) - '0') * 10 + s.charAt(i+1) - '0'; dp[i] = (num <= 26) ? dp[i+1] + dp[i+2] : dp[i+1]; } }
return dp[0]; }}92. Reverse Linked List II
https://leetcode.com/problems/reverse-linked-list-ii/
使用三个指针pre, start, cur,其中:
pre指向待交换区间的前一个数start指向待交换区间的第一个数cur指向当前被交换的数
区间内的交换与此前的reverse nodes in k-group 类似,这里的区间长度是right - left,因此交换要重复right - left次。
/** * Definition for singly-linked list. * public class ListNode { * int val; * ListNode next; * ListNode() {} * ListNode(int val) { this.val = val; } * ListNode(int val, ListNode next) { this.val = val; this.next = next; } * } */class Solution { public ListNode reverseBetween(ListNode head, int left, int right) { if (head == null) { return head; }
ListNode dummyHead = new ListNode(0); dummyHead.next = head; ListNode pre = dummyHead;
// locate the interval [left, right] for (int i = 1; i <= left - 1; i++) { pre = pre.next; } ListNode start = pre.next; ListNode cur = start.next;
// swap right - left times for (int i = 1; i <= right - left; i++) { start.next = cur.next; cur.next = pre.next; pre.next = cur; cur = start.next; } return dummyHead.next; }}93. Restore IP Addresses
https://leetcode.com/problems/restore-ip-addresses/
三层循环,i、j、k表示三个分界点'.'的位置。判断三部分是否都符合IP地址规则,如果都满足,就是一个合法的IP地址。
class Solution { public List<String> restoreIpAddresses(String s) { List<String> result = new ArrayList<>(); if (s.length() < 4 || s.length() > 12) { return result; } int len = s.length();
// i, j, k indicates three indices of '.' for (int i = 1; i < 4 && i < len-2; i++) { for (int j = i+1; j < i+4 && j < len-1; j++) { for (int k = j+1; k < j+4 && k < len; k++) { String s1 = s.substring(0, i); String s2 = s.substring(i, j); String s3 = s.substring(j, k); String s4 = s.substring(k, len);
if (isValid(s1) && isValid(s2) && isValid(s3) && isValid(s4)) { result.add(s1 + "." + s2 + "." + s3 + "." + s4); } } } } return result; }
public boolean isValid(String s) { return !(s.length() > 3 || s.length() == 0 || (s.charAt(0) == '0' && s.length() > 1) || Integer.parseInt(s) > 255); }}94. Binary Tree Inorder Traversal
https://leetcode.com/problems/binary-tree-inorder-traversal/
第一种做法,如果当前节点不为空,或是栈中还有元素,就将当前节点左子加入栈中。之后,弹出栈顶元素,添加当前节点值到结果中,最后再访问右子节点。
/** * Definition for a binary tree node. * public class TreeNode { * int val; * TreeNode left; * TreeNode right; * TreeNode() {} * TreeNode(int val) { this.val = val; } * TreeNode(int val, TreeNode left, TreeNode right) { * this.val = val; * this.left = left; * this.right = right; * } * } */class Solution { public List<Integer> inorderTraversal(TreeNode root) { List<Integer> result = new ArrayList<>();
Stack<TreeNode> st = new Stack<TreeNode>(); TreeNode node = root;
while (node != null || !st.isEmpty()) { // keep pushing left child into stack while (node != null) { st.add(node); node = node.left; }
// left, cur, right node = st.pop(); result.add(node.val); node = node.right; }
return result; }}第二种做法,直接使用递归:
/** * Definition for a binary tree node. * public class TreeNode { * int val; * TreeNode left; * TreeNode right; * TreeNode() {} * TreeNode(int val) { this.val = val; } * TreeNode(int val, TreeNode left, TreeNode right) { * this.val = val; * this.left = left; * this.right = right; * } * } */class Solution { public List<Integer> inorderTraversal(TreeNode root) { List<Integer> result = new LinkedList<>(); inorder(result, root); return result; }
private void inorder(List<Integer> result, TreeNode node) { if (node == null) { return; }
inorder(result, node.left); result.add(node.val); inorder(result, node.right); }}95. Unique Binary Search Trees II
https://leetcode.com/problems/unique-binary-search-trees-ii/
递归解决,产生[1, n]范围内的树,需要遍历范围内的每一个树i,生成[1, i-1]和[i+1, n]的所有左右子树,再用两层循环把左右子树分别添加到当前i对应的节点上。
特殊情况下,如果范围内只有一个树(例如[1, 1]),那么直接添加节点本身即可。
/** * Definition for a binary tree node. * public class TreeNode { * int val; * TreeNode left; * TreeNode right; * TreeNode() {} * TreeNode(int val) { this.val = val; } * TreeNode(int val, TreeNode left, TreeNode right) { * this.val = val; * this.left = left; * this.right = right; * } * } */class Solution { public List<TreeNode> generateTrees(int n) { return n == 0 ? new ArrayList<TreeNode>() : generate(1, n); }
public List<TreeNode> generate(int start, int end) { List<TreeNode> result = new ArrayList<TreeNode>();
if (start >= end) { result.add(start > end ? null : new TreeNode(start)); return result; }
List<TreeNode> leftTreeList, rightTreeList; for (int i = start; i <= end; i++) {
// divide and conquer, generate left and right subtrees based on indices leftTreeList = generate(start, i - 1); rightTreeList = generate(i + 1, end);
for (TreeNode leftTree : leftTreeList) { for (TreeNode rightTree: rightTreeList) { TreeNode root = new TreeNode(i); root.left = leftTree; root.right = rightTree; result.add(root); } } }
return result; }}96. Unique Binary Search Trees
https://leetcode.com/problems/unique-binary-search-trees/
使用动态规划。假如dp[n]表示输入为n时不同BST的数量,F(i, n)表示以数字i为根节点的不同BST的数量(1 <= i <= n),显然有:
dp(n) = F(1, n) + F(2, n) + ... + F(n, n).对于特殊情况,例如n == 0(空树)或n == 1(只有一个根节点),则dp[0] = 1,dp[1] = 1
基于BST的性质,当以数字i为节点时,左子树的节点值一定为[1, 2, ..., i-1],右子树的节点值则为[i+1, i+2, ..., n]。因此,求F(i, n)的过程实际上是求左右子树不同BST的数量,即:
F(i, n) = dp(i-1) * dp(n-i), where 1 <= i <= n合并以上两式得到:
dp(n) = dp(0) * dp(n-1) + dp(1) * dp(n-2) + … + dp(n-1) * dp(0)由此使用动态规划进行递推即可。
class Solution { public int numTrees(int n) { int[] dp = new int[n+1]; dp[0] = 1; dp[1] = 1;
for (int i = 2; i <= n; i++) { for (int j = 0; j < i; j++) { dp[i] += dp[j] * dp[i - 1 - j]; } }
return dp[n]; }}97. Interleaving String
https://leetcode.com/problems/interleaving-string/
使用二维动态规划。假设s1正位于第i位,s2正位于第j位,则dp[i][j]表示s3的第i+j-1位是否为插入点。第0位视为空字符串。考虑四种情况:
i,j均为0,即s1,s2均为空字符串,则s3也为空字符串,视为一个插入点- 只有
i为0时,s2第j-1位需要和s3第i+j-1位相等,且s2的前一位需要为插入点,满足这两个条件才是一个新插入点。 - 只有
j为0时,s1第i-1位需要和s3第i+j-1位相等,且s1的前一位需要为插入点,满足这两个条件才是一个新插入点。 i,j都不为0,上述两种的两种情况任一满足即为新插入点。
class Solution { public boolean isInterleave(String s1, String s2, String s3) { if (s3.length() != s1.length() + s2.length()) { return false; }
int len1 = s1.length(), len2 = s2.length(); boolean[][] dp = new boolean[len1 + 1][len2 + 1];
for (int i = 0; i <= len1; i++) { for (int j = 0; j <= len2; j++) { if (i == 0 && j == 0) { dp[i][j] = true; } else if (i == 0) { dp[i][j] = (dp[i][j-1] && s2.charAt(j-1) == s3.charAt(i+j-1)); } else if (j == 0) { dp[i][j] = (dp[i-1][j] && s1.charAt(i-1) == s3.charAt(i+j-1)); } else { dp[i][j] = ((dp[i-1][j] && s1.charAt(i-1) == s3.charAt(i+j-1)) || (dp[i][j-1] && s2.charAt(j-1) == s3.charAt(i+j-1))); } } }
return dp[len1][len2]; }}98. Validate Binary Search Tree
https://leetcode.com/problems/validate-binary-search-tree/
递归检查每个节点,左子的值必须比当前节点小,右子的值必须比当前节点大。注意当前节点如果为空节点也算作BST。
/** * Definition for a binary tree node. * public class TreeNode { * int val; * TreeNode left; * TreeNode right; * TreeNode() {} * TreeNode(int val) { this.val = val; } * TreeNode(int val, TreeNode left, TreeNode right) { * this.val = val; * this.left = left; * this.right = right; * } * } */class Solution { public boolean isValidBST(TreeNode root) { return isValid(root, Long.MIN_VALUE, Long.MAX_VALUE); }
private boolean isValid(TreeNode node, long minVal, long maxVal) { if (node == null) { return true; }
if (node.val >= maxVal || node.val <= minVal) { return false; }
// left and right child should be valid return isValid(node.left, minVal, node.val) && isValid(node.right, node.val, maxVal); }}99. Recover Binary Search Tree
https://leetcode.com/problems/recover-binary-search-tree/
使用Morris遍历算法,时间复杂度为O(n), 空间复杂度为O(1)。
这个遍历方法的精髓在于对任意一个节点,找到其左子树中的最大值,然后把这个最大值和当前节点连在一起,这样就能形成一条近似从小到大排序的链,之后用双指针直接在链上找到两个乱序节点即可。
/** * Definition for a binary tree node. * public class TreeNode { * int val; * TreeNode left; * TreeNode right; * TreeNode() {} * TreeNode(int val) { this.val = val; } * TreeNode(int val, TreeNode left, TreeNode right) { * this.val = val; * this.left = left; * this.right = right; * } * } */class Solution { public void recoverTree(TreeNode root) { // two treenodes to be swapped TreeNode first = null, second = null;
TreeNode pre = new TreeNode(Integer.MIN_VALUE); TreeNode cur = root;
while (cur != null) { TreeNode node = cur.left;
if (node != null) { // find the rightmost node of the left subtree while (node.right != null && node.right != cur) { node = node.right; }
// if unvisited, link it to current node and continue visit current left subtree if (node.right == null) { node.right = cur; cur = cur.left; continue; } else { // else unlink node.right = null; } }
// compare current node with former node, find two unordered nodes if (cur.val < pre.val) { if (first == null) { first = pre; } second = cur; }
// move forward two pointers pre = cur; cur = cur.right; }
// swap value int temp = second.val; second.val = first.val; first.val = temp; }}100. Same Tree
https://leetcode.com/problems/same-tree/
如果两个节点一个为空,一个不空,或是两个节点的值不一样,则两个树不相同。此外,两个节点的左子和右子也应分别相同。
/** * Definition for a binary tree node. * public class TreeNode { * int val; * TreeNode left; * TreeNode right; * TreeNode() {} * TreeNode(int val) { this.val = val; } * TreeNode(int val, TreeNode left, TreeNode right) { * this.val = val; * this.left = left; * this.right = right; * } * } */class Solution { public boolean isSameTree(TreeNode p, TreeNode q) { if (p == null && q == null) { return true; }
if ((p == null && q != null) || (p != null && q == null) || p.val != q.val) { return false; }
return isSameTree(p.left, q.left) && isSameTree(p.right, q.right); }}