贪心算法(又称贪婪算法)是指,在对问题求解时,总是做出在当前看来是最好的选择。也就是说,不从整体最优上加以考虑,算法得到的是在某种意义上的局部最优解。
贪心算法不是对所有问题都能得到整体最优解,关键是贪心策略的选择
贪心算法一般按如下步骤进行:
①建立数学模型来描述问题。
②把求解的问题分成若干个子问题。
③对每个子问题求解,得到子问题的局部最优解。
④把子问题的解局部最优解合成原来解问题的一个解。
题解:
优先满足胃口最小的孩子,把大于等于这个孩子饥饿度g[i],且最小的饼干s[j]分配给这个孩子。
class Solution { public int findContentChildren(int[] g, int[] s) { Arrays.sort(g); Arrays.sort(s); int i = 0, j = 0; while (i < g.length && j < s.length) { if (g[i] <= s[j]) ++i; ++j; } return i; } }
执行用时: 8 ms 内存消耗: 39.2 MB
官方题解:
class Solution { public int findContentChildren(int[] g, int[] s) { Arrays.sort(g); Arrays.sort(s); int numOfChildren = g.length, numOfCookies = s.length; int count = 0; for (int i = 0, j = 0; i < numOfChildren && j < numOfCookies; i++, j++) { while (j < numOfCookies && g[i] > s[j]) { j++;//饼干尺寸小于孩子胃口把饼干序号加1 } if (j < numOfCookies) { count++; } } return count; } }
执行用时: 9 ms 内存消耗: 39.4 MB
题解:
把所有孩子的糖果数初始化为 1,先从左往右遍历一遍,如果右边孩子的评分比左边的高,则右边孩子的糖果数更新为左边孩子的糖果数加 1;再从右往左遍历一遍,如果左边孩子的评分比右边的高,且左边孩子当前的糖果数不大于右边孩子的糖果数,则左边孩子的糖果数更新为右边孩子的糖果数加 1。
class Solution { public int candy(int[] ratings) { int n = ratings.length; if (n < 2) {//只有1个孩子情况直接返回1 return n; } int[] num = new int[n];//创建糖果数组初始值默认为0,返回值要加上n for (int i = 1; i < n; ++i) {//从左往右遍历 if (ratings[i] > ratings[i-1]) {//如果右侧孩子评分大于左侧 num[i] = num[i-1] + 1;//右侧孩子糖果加1 } } for (int i = n - 1; i > 0; --i) {//从右往左遍历 if (ratings[i] < ratings[i-1]) {//如果右侧孩子评分小于左侧 num[i-1] = Math.max(num[i-1], num[i] + 1);//左侧糖果为原数和右侧加1的较大值 } } return Arrays.stream(num).sum() + n;//数组num求和并加上初始糖果数n } }
执行用时: 7 ms 内存消耗: 39.2 MB(在for循环中++i和i++的结果是一样的都在循环一次之后执行,但++i性能更好)
官方题解:
class Solution { public int candy(int[] ratings) { int n = ratings.length; int[] left = new int[n]; for (int i = 0; i < n; i++) { if (i > 0 && ratings[i] > ratings[i - 1]) { left[i] = left[i - 1] + 1; } else { left[i] = 1; } } int right = 0, ret = 0; for (int i = n - 1; i >= 0; i--) { if (i < n - 1 && ratings[i] > ratings[i + 1]) { right++; } else { right = 1; } ret += Math.max(left[i], right); } return ret; } }
执行用时: 3 ms 内存消耗: 39.5 MB
题解:
遍历花坛中的元素,满足种花条件则n减1,返回判断n是否<=0
class Solution { public boolean canPlaceFlowers(int[] flowerbed, int n) { for (int i = 0; i < flowerbed.length; ++i) { if (flowerbed[i] == 0 && (i + 1 == flowerbed.length || flowerbed[i + 1] == 0)) { n--; i++; } else if (flowerbed[i] == 1) { i++; } } return n <= 0; } }
执行用时: 1 ms 内存消耗: 40 MB
官方题解:
贪心策略:在不打破种植规则的情况下种入尽可能多的花,然后判断可以种入的花的最多数量是否大于或等于 n。
实现方法:
维护 prev
表示上一朵已经种植的花的下标位置,初始时prev=−1
,表示尚未遇到任何已经种植的花。
从左往右遍历数组flowerbed
,当遇到flowerbed[i]=1
时根据prev
和 i
的值计算上一个区间内可以种植花的最多数量,然后令prev=i
,继续遍历数组 flowerbed
剩下的元素。
遍历数组flowerbed
结束后,根据数组prev
和长度 m
的值计算最后一个区间内可以种植花的最多数量。
判断整个花坛内可以种入的花的最多数量是否大于或等于 n
。
class Solution { public boolean canPlaceFlowers(int[] flowerbed, int n) { int count = 0; int m = flowerbed.length; int prev = -1; for (int i = 0; i < m; i++) { if (flowerbed[i] == 1) { if (prev < 0) { count += i / 2; } else { count += (i - prev - 2) / 2; } prev = i; } } if (prev < 0) { count += (m + 1) / 2; } else { count += (m - prev - 1) / 2; } return count >= n; } }
执行用时: 1 ms 内存消耗: 39.8 MB
题解:
贪心策略:优先保留结尾小且不相交的区间。
实现方法:先把区间按照结尾的大小进行增序排序,每次选择结尾最小且和前一个选择的区间不重叠的区间。
class Solution { public int eraseOverlapIntervals(int[][] intervals) { if (intervals.length == 0) { return 0; } Arrays.sort(intervals, new Comparator<int[]>() {//将数组集合按第二个元素大小排序 public int compare(int[] interval1, int[] interval2) { return interval1[1] - interval2[1]; } }); int n = intervals.length; int total = 0, prev = intervals[0][1];//预置为数组中的第二个元素 for (int i = 1; i < n; ++i) { if (intervals[i][0] < prev) {//该数组的第一个元素与前一个数组的第二元素进行比较 ++total;//如果prev大,区间有重叠,移除该区间 } else { prev = intervals[i][1];//将prev置为第i个数组的第二个元素 } } return total; } }
执行用时: 3 ms 内存消耗: 38.1 MB
官方题解:
class Solution { public int eraseOverlapIntervals(int[][] intervals) { if (intervals.length == 0) { return 0; } Arrays.sort(intervals, new Comparator<int[]>() { public int compare(int[] interval1, int[] interval2) { return interval1[1] - interval2[1]; } }); int n = intervals.length; int right = intervals[0][1]; int ans = 1; for (int i = 1; i < n; ++i) { if (intervals[i][0] >= right) { ++ans; right = intervals[i][1]; } } return n - ans; } }
执行用时: 3 ms 内存消耗: 38.1 MB
题解:
贪心策略:相当于435题的反面,尽量使区间重叠,注意区间可为负数
实现方法:先把区间按照结尾的大小进行增序排序 (区间可以为负数,注意此处排序方式),每次选择结尾最小且和前一个选择的区间重叠的区间。
class Solution { public int findMinArrowShots(int[][] points) { if (points.length == 0) { return 0; } Arrays.sort(points, new Comparator<int[]> () { public int compare (int[] point1, int[] point2) { return point1[1] > point2[1] ? 1 : -1; } }); int n = points.length; int total = n, prev = points[0][1]; for (int i = 1; i < n; ++i) { if (points[i][0] <= prev) { --total; } else { prev = points[i][1]; } } return total; } }
执行用时: 19 ms 内存消耗: 45.4 MB
官方题解:
class Solution { public int findMinArrowShots(int[][] points) { if (points.length == 0) { return 0; } Arrays.sort(points, new Comparator<int[]>() { public int compare(int[] point1, int[] point2) { if (point1[1] > point2[1]) { return 1; } else if (point1[1] < point2[1]) { return -1; } else { return 0; } } }); int pos = points[0][1]; int ans = 1; for (int[] balloon: points) { if (balloon[0] > pos) { pos = balloon[1]; ++ans; } } return ans; } }
执行用时: 18 ms 内存消耗: 45.7 MB
双指针,指的是在遍历对象的过程中,不是普通的使用单个指针进行访问,而是使用两个相同方向(快慢指针)或者相反方向(对撞指针)的指针进行扫描,从而达到相应的目的。
题解:
采用方向相反的双指针来寻找这两个数字,一个初始指向最小的元素l,即数组最左边,向右遍历;一个初始指向最大的元素r,即数组最右边,向左遍历。
class Solution { public int[] twoSum(int[] numbers, int target) { int l = 0, r = numbers.length - 1, sum;//新建变量l、r、sum while (l < r) { sum = numbers[l] + numbers[r]; if (sum == target) break; if (sum < target) ++l;//和小于sum时把较小数增大 else --r;//反之把较大数减小 } return new int[]{l + 1, r + 1};//返回新建数组元素指针从0开始需要加1 } }
执行用时: 1 ms 内存消耗: 38.6 MB
官方题解:
class Solution { public int[] twoSum(int[] numbers, int target) { int low = 0, high = numbers.length - 1; while (low < high) { int sum = numbers[low] + numbers[high]; if (sum == target) { return new int[]{low + 1, high + 1}; } else if (sum < target) { ++low; } else { --high; } } return new int[]{-1, -1}; } }
执行用时: 1 ms 内存消耗: 38.8 MB
题解:
使用方向相反的两个指针,假设这两个数存在,最大范围为0~sqrt©,指针l指向最左侧0,向右遍历,指针r指向sqrt©取整,向左遍历。
class Solution { public boolean judgeSquareSum(int c) { int l = 0, sum; int r = (int) Math.sqrt(c); while (l <= r) { sum = l * l + r * r; if (sum == c) return true; if (sum < c) ++l; else --r; } return false; } }
执行用时: 2 ms 内存消耗: 35.1 MB
官方题解:
class Solution { public boolean judgeSquareSum(int c) { long left = 0; long right = (long) Math.sqrt(c); while (left <= right) { long sum = left * left + right * right; if (sum == c) { return true; } else if (sum > c) { right--; } else { left++; } } return false; } }
执行用时: 4 ms 内存消耗: 35.2 MB
题解:
1.创建两个指针,左指针l指向0,向右遍历,右指针r指向最右,向左遍历;
2.比较两个字符是否相等,返回相应的三种情况;
3.创建新方法isPalindrome()
判断删去一个字符后的字符串是否时回文字符串。
class Solution { public boolean validPalindrome(String s) { int l = 0, r = s.length() - 1; while (l < r && s.charAt(l) == s.charAt(r)) { ++l; --r; } //返回三种情况,第一种s本身即为回文字符串,第二种为判断删去一个字符的情况 return l >= r || isPalindrome(s, l, r - 1) || isPalindrome(s, l + 1, r); } public boolean isPalindrome(String s, int l, int r) { while (l < r && s.charAt(l) == s.charAt(r)) { ++l; --r; } return l >= r; } }
执行用时: 7 ms 内存消耗: 38.5 MB
官方题解:
class Solution { public boolean validPalindrome(String s) { int low = 0, high = s.length() - 1; while (low < high) { char c1 = s.charAt(low), c2 = s.charAt(high); if (c1 == c2) { ++low; --high; } else { return validPalindrome(s, low, high - 1) || validPalindrome(s, low + 1, high); } } return true; } public boolean validPalindrome(String s, int low, int high) { for (int i = low, j = high; i < j; ++i, --j) { char c1 = s.charAt(i), c2 = s.charAt(j); if (c1 != c2) { return false; } } return true; } }
执行用时: 7 ms 内存消耗: 39 MB
题解:
把两个指针分别放在两个数组的末尾,即 nums1 的m − 1 位和 nums2 的 n − 1位。每次将较大的那个数字复制到 nums1 的后边,然后向前移动一位。直接利用m和n当作两个数组的指针,在创建一个pos用于确定新数组nums1的指针位置。
class Solution { public void merge(int[] nums1, int m, int[] nums2, int n) { int pos = m-- + n-- -1;//pos起始位置为m+n-1 while (m >= 0 && n >= 0) { nums1[pos--] = nums1[m] > nums2[n] ? nums1[m--] : nums2[n--];//两数组中较大的元素加入到nums1中 } while (n >= 0) {//nums2还有剩余数组 nums1[pos--] = nums2[n--]; } } }
执行用时: 0 ms 内存消耗: 38.8 MB
官方题解:
class Solution { public void merge(int[] nums1, int m, int[] nums2, int n) { int p1 = 0, p2 = 0; int[] sorted = new int[m + n]; int cur; while (p1 < m || p2 < n) { if (p1 == m) { cur = nums2[p2++]; } else if (p2 == n) { cur = nums1[p1++]; } else if (nums1[p1] < nums2[p2]) { cur = nums1[p1++]; } else { cur = nums2[p2++]; } sorted[p1 + p2 - 1] = cur; } for (int i = 0; i != m + n; ++i) { nums1[i] = sorted[i]; } } }
执行用时: 0 ms 内存消耗: 38.6 MB
/** * Definition for singly-linked list. * class ListNode { * int val; * ListNode next; * ListNode(int x) { * val = x; * next = null; * } * } */ public class Solution { public ListNode detectCycle(ListNode head) { ListNode slow = head, fast = head;//创建快慢指针 //判断是否存在环路 do { if (fast == null || fast.next == null) return null;//不存在结点或只有一个结点返回null fast = fast.next.next;//fast移动两个位置 slow = slow.next; //slow移动一个位置 } while (fast != slow);//不断循环当快慢指针不能相遇则不存在环路跳出循环 fast = head;//第一次相遇将fast指针移动到链表开头 //查找环路结点,快慢指针都只移动一个位置第二次相遇位置即为结点 while (fast != slow) { slow = slow.next; fast = fast.next; } return fast; } }
执行用时: 0 ms 内存消耗: 38.7 MB
官方题解:
public class Solution { public ListNode detectCycle(ListNode head) { if (head == null) { return null; } ListNode slow = head, fast = head; while (fast != null) { slow = slow.next; if (fast.next != null) { fast = fast.next.next; } else { return null; } if (fast == slow) { ListNode ptr = head; while (ptr != slow) { ptr = ptr.next; slow = slow.next; } return ptr; } } return null; } }
执行用时: 0 ms 内存消耗: 38.7 MB
题解:
1.遍历字符串t并记录需要的字符及其个数,用数组need映射;
2.在字符串s中建立滑动窗口,左边界l,有边界r,不断增加r使滑动窗口增大直到包含了t中的所有元素,need中所有元素<=0且count也为0;
3.不断增加l减小滑动窗口直到碰到第一个必须包含的元素,记录此时的长度;
4.再增加i,重复上述步骤直到滑动窗口再一次满足要求。
class Solution { public String minWindow(String s, String t) { //特殊判空情况 if (s == null || s.length() == 0 || t == null || t.length() == 0){ return ""; } //用长度128的数组need映射字符 int[] need = new int[128]; //遍历字符串t,记录需要的字符的个数 for (int i = 0; i < t.length(); i++) { need[t.charAt(i)]++; } //l是当前左边界,r是当前右边界,size记录窗口大小,count是需求的字符个数,start是最小覆盖串开始的index int l = 0, r = 0, size = Integer.MAX_VALUE, count = t.length(), start = 0; //遍历s的所有字符 while (r < s.length()) { if (need[s.charAt(r)] > 0) { count--; } need[s.charAt(r)]--;//把右边的字符加入窗口,s中的字符减少1 if (count == 0) {//窗口中已经包含所有字符 while (l < r && need[s.charAt(l)] < 0) { need[s.charAt(l)]++;//释放右边移动出窗口的字符 l++;//指针右移 } if (r - l + 1 < size) {//不能右移时候挑战最小窗口大小,更新最小窗口开始的start size = r - l + 1; start = l;//记录下最小值时候的开始位置,最后返回覆盖串时候会用到 } //l向右移动后窗口肯定不能满足了 重新开始循环 need[s.charAt(l)]++; l++; count++; } r++; } return size == Integer.MAX_VALUE ? "" : s.substring(start, start + size); } }
官方题解:
class Solution { Map<Character, Integer> ori = new HashMap<Character, Integer>(); Map<Character, Integer> cnt = new HashMap<Character, Integer>(); public String minWindow(String s, String t) { int tLen = t.length(); for (int i = 0; i < tLen; i++) { char c = t.charAt(i); ori.put(c, ori.getOrDefault(c, 0) + 1); } int l = 0, r = -1; int len = Integer.MAX_VALUE, ansL = -1, ansR = -1; int sLen = s.length(); while (r < sLen) { ++r; if (r < sLen && ori.containsKey(s.charAt(r))) { cnt.put(s.charAt(r), cnt.getOrDefault(s.charAt(r), 0) + 1); } while (check() && l <= r) { if (r - l + 1 < len) { len = r - l + 1; ansL = l; ansR = l + len; } if (ori.containsKey(s.charAt(l))) { cnt.put(s.charAt(l), cnt.getOrDefault(s.charAt(l), 0) - 1); } ++l; } } return ansL == -1 ? "" : s.substring(ansL, ansR); } public boolean check() { Iterator iter = ori.entrySet().iterator(); while (iter.hasNext()) { Map.Entry entry = (Map.Entry) iter.next(); Character key = (Character) entry.getKey(); Integer val = (Integer) entry.getValue(); if (cnt.getOrDefault(key, 0) < val) { return false; } } return true; } }
二分查找(Binary Search)也叫作折半查找。二分查找有两个要求,一个是数列有序,另一个是数列使用顺序存储结构(比如数组)。长度为 O(n) 的数组,二分查找的时间复杂度为 O(log n)。
题解:
将开方转化为给定一个非负整数x,求 f ® = r^2 − x = 0 的解。
单独考虑x == 0的情况,然后再[1,x]区间内用二分查找法。
class Solution { public int mySqrt(int x) { if (x == 0) return x; int l = 1, r = x, mid, sqrt; while (l <= r) { mid = l + (r - l) /2; sqrt = x / mid; if (sqrt == mid) { return mid; } else if (mid > sqrt) { r = mid - 1; } else { l = mid + 1; } } return r; } }
执行用时: 1 ms 内存消耗: 35.4 MB
官方题解:
class Solution { public int mySqrt(int x) { int l = 0, r = x, ans = -1; while (l <= r) { int mid = l + (r - l) / 2; if ((long) mid * mid <= x) { ans = mid; l = mid + 1; } else { r = mid - 1; } } return ans; } }
执行用时: 1 ms 内存消耗: 35.6 MB
题解:
用二分法分别查找数组中的target。
class Solution { public int[] searchRange(int[] nums, int target) { if (nums == null || nums.length == 0) return new int[]{-1, -1}; int lower = lower_bound(nums, target); int upper = upper_bound(nums, target) - 1; if (lower == nums.length || nums[lower] != target) { return new int[]{-1, -1}; } return new int[]{lower, upper}; } //二分查找序号低的target int lower_bound(int[] nums, int target) { int l = 0, r = nums.length, mid; while (l < r) { mid = (l + r) / 2; if (nums[mid] >= target) { r = mid; } else { l = mid + 1; } } return l; } //二分查找序号高的target int upper_bound(int[] nums, int target) { int l = 0, r = nums.length, mid; while (l < r) { mid = (l + r) / 2; if (nums[mid] > target) { r = mid; } else { l = mid + 1; } } return l; } }
执行用时: 0 ms 内存消耗: 41.7 MB
官方题解:
class Solution { public int[] searchRange(int[] nums, int target) { int leftIdx = binarySearch(nums, target, true); int rightIdx = binarySearch(nums, target, false) - 1; if (leftIdx <= rightIdx && rightIdx < nums.length && nums[leftIdx] == target && nums[rightIdx] == target) { return new int[]{leftIdx, rightIdx}; } return new int[]{-1, -1}; } public int binarySearch(int[] nums, int target, boolean lower) { int left = 0, right = nums.length - 1, ans = nums.length; while (left <= right) { int mid = (left + right) / 2; if (nums[mid] > target || (lower && nums[mid] >= target)) { right = mid - 1; ans = mid; } else { left = mid + 1; } } return ans; } }
执行用时: 0 ms 内存消耗: 41.4 MB
题解:
class Solution { public int singleNonDuplicate(int[] nums) { int l = 0, r = nums.length - 1; while (l < r) { int mid = l + (r - l) / 2; //mid为偶数且与后一个元素相等,只出现一次元素在mid右侧 if (mid % 2 == 0 && nums[mid] == nums[mid + 1]) { l = mid + 2; //mid为奇数且与前一个元素相等,只出现一次元素在mid右侧 } else if (mid % 2 == 1 && nums[mid - 1] == nums[mid]) { l = mid + 1; //只出现一次元素在左侧 } else { r = mid; } } return nums[l]; } }
执行用时: 0 ms 内存消耗: 38.7 MB
官方题解:
class Solution { public int singleNonDuplicate(int[] nums) { int lo = 0; int hi = nums.length - 1; while (lo < hi) { int mid = lo + (hi - lo) / 2; if (mid % 2 == 1) mid--; if (nums[mid] == nums[mid + 1]) { lo = mid + 2; } else { hi = mid; } } return nums[lo]; } }
执行用时: 0 ms 内存消耗: 38.5 MB
题解:
数组旋转后,仍旧保留部分有序和递增,利用这个特性进行二分查找。
对于当前mid,如果该值<=右侧,那么右侧有序,反之左侧有序。
如果target位于有序区间内,继续对这个区间二分查找,反之对另一半区间二分查找。
因为数组存在重复数字,如果中点和左端的数字相同,我们并不能确定是左区间全部相同,还是右区间完全相同。在这种情况下,我们可以简单地将左端点右移一位,然后继续进行二分查找。
class Solution { public boolean search(int[] nums, int target) { int start = 0, end = nums.length - 1; while (start <= end) { int mid = (start + end) / 2; //恰好找到target直接返回true if (nums[mid] == target) { return true; } if (nums[start] == nums[mid]) { //无法判断增序 ++start; } else if (nums[mid] <= nums[end]) { //右区间增序 if (target > nums[mid] && target <= nums[end]) { start = mid + 1; } else { end = mid - 1; } } else { //左区间增序 if (target >= nums[start] && target < nums[mid]) { end = mid - 1; } else { start = mid + 1; } } } return false; } }
执行用时: 1 ms 内存消耗: 38.1 MB
官方题解:
class Solution { public boolean search(int[] nums, int target) { int n = nums.length; if (n == 0) { return false; } if (n == 1) { return nums[0] == target; } int l = 0, r = n - 1; while (l <= r) { int mid = (l + r) / 2; if (nums[mid] == target) { return true; } if (nums[l] == nums[mid] && nums[mid] == nums[r]) { ++l; --r; } else if (nums[l] <= nums[mid]) { if (nums[l] <= target && target < nums[mid]) { r = mid - 1; } else { l = mid + 1; } } else { if (nums[mid] < target && target <= nums[n - 1]) { l = mid + 1; } else { r = mid - 1; } } } return false; } }
执行用时: 1 ms 内存消耗: 37.6 MB
题解:
当 nums[mid] > nums[right]时,最小值一定在mid右侧,此时i一定满足 mid < i <= right,因此执行 left = mid + 1;
当 nums[mid] < nums[right] 时,最小值一定在mid左侧,此时i一定满足 left < i <= mid,因此执行 right = mid;
当 nums[mid] == nums[right] 时,有重复元素无法判断,因此将最右减-1.
class Solution { public int findMin(int[] nums) { int left = 0, right = nums.length - 1; while (left < right) { int mid = (left + right) / 2; //最小值在右侧 if (nums[mid] > nums[right]) left = mid + 1; //最小值在左侧 else if (nums[mid] < nums[right]) right = mid; //重复数字无法判断 else right = right - 1; } return nums[left]; } }
执行用时: 0 ms 内存消耗: 38.1 MB
官方题解:
class Solution { public int findMin(int[] nums) { int low = 0; int high = nums.length - 1; while (low < high) { int pivot = low + (high - low) / 2; if (nums[pivot] < nums[high]) { high = pivot; } else if (nums[pivot] > nums[high]) { low = pivot + 1; } else { high -= 1; } } return nums[low]; } }
执行用时: 1 ms 内存消耗: 38.2 MB
题解:
class Solution { public int findKthLargest(int[] nums, int k) { int l = 0, r = nums.length - 1, target = nums.length - k; while (l < r) { int mid = quickSelection(nums, l, r); if (mid == target) { return nums[mid]; } if (mid < target) { l = mid + 1; } else { r = mid - 1; } } return nums[l]; } private int quickSelection(int[] nums, int l, int r) { int i = l + 1, j = r; while (true) { while (i < r && nums[i] <= nums[l]) { ++i; } while (l < j && nums[j] >= nums[l]) { --j; } if (i >= j) { break; } swap(nums, i, j); } swap(nums, l, j); return j; } private void swap(int[] nums, int i, int j) { int temp = nums[i]; nums[i] = nums[j]; nums[j] = temp; } }
执行用时: 18 ms 内存消耗: 38.1 MB
官方题解:
改进快速排序算法来解决这个问题:在分解的过程当中,我们会对子数组进行划分,如果划分得到的 q 正好就是我们需要的下标,就直接返回 a[q]a[q];否则,如果 qq 比目标下标小,就递归右子区间,否则递归左子区间。这样就可以把原来递归两个区间变成只递归一个区间,提高了时间效率。这就是「快速选择」算法。
class Solution { Random random = new Random(); public int findKthLargest(int[] nums, int k) { return quickSelect(nums, 0, nums.length - 1, nums.length - k); } public int quickSelect(int[] a, int l, int r, int index) { int q = randomPartition(a, l, r); if (q == index) { return a[q]; } else { return q < index ? quickSelect(a, q + 1, r, index) : quickSelect(a, l, q - 1, index); } } public int randomPartition(int[] a, int l, int r) { int i = random.nextInt(r - l + 1) + l; swap(a, i, r); return partition(a, l, r); } public int partition(int[] a, int l, int r) { int x = a[r], i = l - 1; for (int j = l; j < r; ++j) { if (a[j] <= x) { swap(a, ++i, j); } } swap(a, i + 1, r); return i + 1; } public void swap(int[] a, int i, int j) { int temp = a[i]; a[i] = a[j]; a[j] = temp; } }
执行用时: 2 ms 内存消耗: 38.5 MB
题解:
首先依旧使用哈希表统计频率,统计完成后,创建一个数组,将频率作为数组下标,对于出现频率不同的数字集合,存入对应的数组下标即可。
class Solution { public int[] topKFrequent(int[] nums, int k) { List<Integer> res = new ArrayList(); // 使用字典,统计每个元素出现的次数,元素为键,元素出现的次数为值 HashMap<Integer,Integer> map = new HashMap(); for(int num : nums){ if (map.containsKey(num)) { map.put(num, map.get(num) + 1); } else { map.put(num, 1); } } //桶排序 //将频率作为数组下标,对于出现频率不同的数字集合,存入对应的数组下标 List<Integer>[] list = new List[nums.length+1]; for(int key : map.keySet()){ // 获取出现的次数作为下标 int i = map.get(key); if(list[i] == null){ list[i] = new ArrayList(); } list[i].add(key); } // 倒序遍历数组获取出现顺序从大到小的排列 for(int i = list.length - 1;i >= 0 && res.size() < k;i--){ if(list[i] == null) continue; res.addAll(list[i]); } return res.stream().mapToInt(Integer::valueOf).toArray(); } }
执行用时: 16 ms 内存消耗: 40.5 MB
题解:
class Solution { public String frequencySort(String s) { List<String> res = new ArrayList(); HashMap<String, Integer> map = new HashMap(); for (int i = 0; i < s.length(); ++i) { if (map.containsKey(s.substring(i, i + 1))) { map.put(s.substring(i, i + 1), map.get(s.substring(i, i + 1)) + 1); } else { map.put(s.substring(i, i + 1), 1); } } List<String>[] list = new List[s.length() + 1]; for (String key : map.keySet()) { int i = map.get(key); if (list[i] == null) list[i] = new ArrayList(); list[i].add(key); } for (int i = list.length - 1; i >= 0; --i) { if (list[i] == null) continue; res.addAll(list[i]); } return String.join("", res); } }
深度优先搜索属于图算法的一种,英文缩写为DFS即Depth First Search.其过程简要来说是对每一个可能的分支路径深入到不能再深入为止,而且每个节点只能访问一次。遍历需要用先入后出的栈来实现。
题解:
深度优先搜索+递归
(1)maxAreaOfIsland()
方法用于遍历所有的搜索位置,判断是否可以开始搜索;
(2)dfs()
方法用于深度优先搜索的递归调用。
class Solution { //四个方向遍历创建一个数组[-1,0,1,0,-1],每相邻两位即为上下左右四个方向之一。 int[] direction = new int[]{-1, 0, 1, 0, -1}; //遍历数组所有搜索的位置 public int maxAreaOfIsland(int[][] grid) { if (grid == null || grid[0] == null) return 0; int max_area = 0; for (int i = 0; i < grid.length; ++i) { for (int j = 0; j < grid[0].length; ++j) { if (grid[i][j] == 1) { max_area = Math.max(max_area, dfs(grid, i, j)); } } } return max_area; } //深度优先搜索 int dfs(int[][] grid, int r, int c) { if (grid[r][c] == 0) return 0; grid[r][c] = 0; int x, y, area = 1; for (int i = 0; i < 4; ++i) { x = r + direction[i]; y = c + direction[i + 1]; if (x >= 0 && x < grid.length && y >= 0 && y < grid[0].length) { area += dfs(grid, x, y); } } return area; } }
执行用时: 3 ms 内存消耗: 39 MB
官方题解:
深度优先搜索+栈
class Solution { public int maxAreaOfIsland(int[][] grid) { int ans = 0; for (int i = 0; i != grid.length; ++i) { for (int j = 0; j != grid[0].length; ++j) { int cur = 0; Deque<Integer> stacki = new LinkedList<Integer>(); Deque<Integer> stackj = new LinkedList<Integer>(); stacki.push(i); stackj.push(j); while (!stacki.isEmpty()) { int cur_i = stacki.pop(), cur_j = stackj.pop(); if (cur_i < 0 || cur_j < 0 || cur_i == grid.length || cur_j == grid[0].length || grid[cur_i][cur_j] != 1) { continue; } ++cur; grid[cur_i][cur_j] = 0; int[] di = {0, 0, 1, -1}; int[] dj = {1, -1, 0, 0}; for (int index = 0; index != 4; ++index) { int next_i = cur_i + di[index], next_j = cur_j + dj[index]; stacki.push(next_i); stackj.push(next_j); } } ans = Math.max(ans, cur); } } return ans; } }
个人题解:
图的表示方法:每一行(列)表示要给结点,它的每列(行)表示是否存在一个相邻结点。n x n 的矩阵 isConnected表示有n个结点,每个结点最多有n条边,表示和该城市和所有城市相连,最少只有一条边,表示该城市自成一省。
class Solution { //遍历数组中的每个结点 public int findCircleNum(int[][] isConnected) { int n = isConnected.length, count = 0; //visited记录每个城市是否被访问过,默认值为false boolean[] visited = new boolean[n]; for (int i = 0; i < n; ++i) { //i行已经被访问过 if (!visited[i]) { dfs(isConnected, i, visited); ++count; } } return count; } //对i行进行深度优先搜索 void dfs(int[][] isConnected, int i, boolean[] visited) { visited[i] = true; //遍历第k列 for (int k = 0; k < isConnected.length; ++k) { //该节点为1并且该节点没有被访问过 if (isConnected[i][k] == 1 && !visited[k]) { dfs(isConnected, k, visited); } } } }
执行用时: 1 ms 内存消耗: 39.4 MB
官方题解:
class Solution { public int findCircleNum(int[][] isConnected) { int provinces = isConnected.length; boolean[] visited = new boolean[provinces]; int circles = 0; for (int i = 0; i < provinces; i++) { if (!visited[i]) { dfs(isConnected, visited, provinces, i); circles++; } } return circles; } public void dfs(int[][] isConnected, boolean[] visited, int provinces, int i) { for (int j = 0; j < provinces; j++) { if (isConnected[i][j] == 1 && !visited[j]) { visited[j] = true; dfs(isConnected, visited, provinces, j); } } } }
执行用时: 1 ms 内存消耗: 39.5 MB
题解:
1.建立两个矩阵Atlantic和Pacific, 当Atlantic[i][j]和Pacific[i][j]同时为true时表示该位置可以同时到达Atlantic和Pacific;
2.只需要从四个边界开始遍历即可(类似泛洪的思想, 只要可以从边界出发到达, 就说明该位置的水可以流向对应的海洋)
class Solution { public List<List<Integer>> pacificAtlantic(int[][] heights) { List<List<Integer>> ret = new ArrayList<>(); int m = heights.length, n = heights[0].length; if (m < 1) return ret; //新建两个二维数组分别代表太平洋二号大西洋 boolean[][] Pacific = new boolean[m][n]; boolean[][] Atlantic = new boolean[m][n]; for (int i = 0; i < m; ++i) { dfs(heights, i, 0, Pacific, heights[i][0]); dfs(heights, i, n-1, Atlantic, heights[i][n-1]); } for (int i = 0; i < n; ++i) { dfs(heights, 0, i, Pacific, heights[0][i]); dfs(heights, m-1, i, Atlantic, heights[m-1][i]); } for (int i = 0; i < m; ++i) { for (int j = 0; j < n; ++j) { //两个数组都为true则水可以到达 if (Pacific[i][j] && Atlantic[i][j]) { List<Integer> list = new ArrayList<Integer>(); Collections.addAll(list, i, j); ret.add(list); } } } return ret; } private void dfs(int[][] h, int x, int y, boolean[][] visited, int pre) { if (x < 0 || y < 0 || x >= m.length || y >= m[0].length || visited[x][y] || m[x][y] < pre) return; visited[x][y] = true; //四个边界遍历 dfs(h, x+1, y, visited, m[x][y]); dfs(h, x-1, y, visited, m[x][y]); dfs(h, x, y+1, visited, m[x][y]); dfs(h, x, y-1, visited, m[x][y]); } }
执行用时: 5 ms 内存消耗: 39.3 MB
回溯法(探索与回溯法)是一种选优搜索法,又称为试探法,按选优条件向前搜索,以达到目标。但当探索到某一步时,发现原先选择并不优或达不到目标,就退回一步重新选择,这种走不通就退回再走的技术为回溯法,而满足回溯条件的某个状态的点称为“回溯点”。
题解:
递归结构:按顺序枚举每一位可能出现的情况,已经选择的数字在 当前 要选择的数字中不能出现。借助系统栈空间,保存所需要的状态变量,在编码中只需要注意遍历到相应的结点的时候,状态变量的值是正确的,具体的做法是:往下走一层的时候,path 变量在尾部追加,而往回走的时候,需要撤销上一次的选择,也是在尾部操作,因此 path 变量是一个栈;
状态变量:
List<List<Integer>> res
:用于储存所有全排列
List<Integer> path
:表示当前状态,当向下一层移动时,path变量在尾部add,返回上一层时,撤销上一次的选择,后进先出的数据结构,是一个栈;
int depth
:表示当前程序递归的层数;
boolean[] used
:表示该位置的数是否被选择,默认false 表示这些数还没有被选择,当选定一个数后该位置变量值为true;
public class Solution { public List<List<Integer>> permute(int[] nums) { // 首先是特判 int len = nums.length; // 使用一个动态数组保存所有可能的全排列 List<List<Integer>> res = new ArrayList<>(); if (len == 0) { return res; } boolean[] used = new boolean[len]; List<Integer> path = new ArrayList<>(); dfs(nums, len, 0, path, used, res); return res; } private void dfs(int[] nums, int len, int depth, List<Integer> path, boolean[] used, List<List<Integer>> res) { if (depth == len) { //不用拷贝,因为每一层传递下来的 path 变量都是新建的 res.add(path); return; } for (int i = 0; i < len; i++) { if (!used[i]) { //每一次尝试都创建新的变量表示当前的"状态" List<Integer> newPath = new ArrayList<>(path); newPath.add(nums[i]); boolean[] newUsed = new boolean[len]; System.arraycopy(used, 0, newUsed, 0, len); newUsed[i] = true; dfs(nums, len, depth + 1, newPath, newUsed, res); //无需回溯 } } } }
执行用时: 1 ms 内存消耗: 38.3 MB
官方题解:
class Solution { public List<List<Integer>> permute(int[] nums) { List<List<Integer>> res = new ArrayList<List<Integer>>(); List<Integer> output = new ArrayList<Integer>(); for (int num : nums) { output.add(num); } int n = nums.length; backtrack(n, output, res, 0); return res; } public void backtrack(int n, List<Integer> output, List<List<Integer>> res, int first) { // 所有数都填完了 if (first == n) { res.add(new ArrayList<Integer>(output)); } for (int i = first; i < n; i++) { // 动态维护数组 Collections.swap(output, first, i); // 继续递归填下一个数 backtrack(n, output, res, first + 1); // 撤销操作 Collections.swap(output, first, i); } } }
执行用时: 1 ms 内存消耗: 38.7 MB
题解:
递归结构:在以n为结尾的候选数组里,选出k个元素。
状态变量:
List<List<Integer>> res
:储存返回的数组;
int begin
:搜索起点,表示在区间 [begin, n] 里选出若干个数的组合;
Deque<Integer> path
:表示路径的变量,是一个列表,用栈方式储存;
剪枝:当数组中的剩余元素小于k时已经不满足情况,不需要继续遍历下去,对深度优先搜索遍历范围进行剪枝。
总结规律可得:搜索起点的最大值 = n - (k - path.size()) + 1
public class Solution { public List<List<Integer>> combine(int n, int k) { List<List<Integer>> res = new ArrayList<>(); if (k <= 0 || n < k) { return res; } Deque<Integer> path = new ArrayDeque<>(); //题目设定从1开始搜索 dfs(n, k, 1, path, res); return res; } private void dfs(int n, int k, int begin, Deque<Integer> path, List<List<Integer>> res) { //当path等于k时找到满足要求数组,递归终止 if (path.size() == k) { res.add(new ArrayList<>(path)); return; } //遍历所有搜索起点 for (int i = begin; i <= n - (k - path.size()) + 1; ++i) { path.addLast(i); //下一轮路径变量加1,不允许重复 dfs(n, k, i + 1, path, res); //返回到上一层 path.removeLast(); } } }
执行用时: 2 ms 内存消耗: 39.5 MB
题解:
递归结构:遍历上下左右元素与word中相应字符进行匹配;
状态变量:
int x:board中的行;
int y:board中的列;
int begin:word中的开始位置;
boolean[][] visited:记录当前节点状态
public class Solution { //新建偏移量数组DIRECTIONS private static final int[][] DIRECTIONS = {{-1, 0}, {0, -1}, {0, 1}, {1, 0}}; private int rows;//行 private int cols;//列 private int len;//word长度 private boolean[][] visited;//节点是否被访问变量 private char[] charArray;//word字符转换 private char[][] board; public boolean exist(char[][] board, String word) { rows = board.length; if (rows == 0) { return false; } cols = board[0].length; visited = new boolean[rows][cols]; this.len = word.length(); this.charArray = word.toCharArray(); this.board = board; for (int i = 0; i < rows; i++) { for (int j = 0; j < cols; j++) { if (dfs(i, j, 0)) { return true; } } } return false; } private boolean dfs(int x, int y, int begin) { //结束条件 if (begin == len - 1) { return board[x][y] == charArray[begin]; } if (board[x][y] == charArray[begin]) { visited[x][y] = true;//修改当前结点状态 for (int[] direction : DIRECTIONS) {//递归子节点 int newX = x + direction[0]; int newY = y + direction[1]; if (inArea(newX, newY) && !visited[newX][newY]) { if (dfs(newX, newY, begin + 1)) { return true; } } } visited[x][y] = false;//回退当前结点状态 } return false; } private boolean inArea(int x, int y) { return x >= 0 && x < rows && y >= 0 && y < cols; } }
执行用时: 106 ms 内存消耗: 36.6 MB
题解:
递归结构:将棋盘的行列转换为决策树,从树的根节点[]开始遍历,每一层表示棋盘中的每一行,该节点可以选择任意一列放置一个Queen;
状态变量:
int row:遍历位置的行;
int col:遍历位的列;
剪枝:
数组是从上往下遍历,当前行和下方没有放置Queen不需要检查,只需要检查左上方、上方、右上方三个方向。
class Solution { List<List<String>> res = new ArrayList<List<String>>(); public List<List<String>> solveNQueens(int n) { //初始化棋盘 char[][] board = new char[n][n]; for (char[] c : board) Arrays.fill(c, '.'); backtracking(n, board, 0); return res; } //board[row][col]位置是否可以放置Queen public boolean isValid(char[][] board, int row, int col, int n) { for (int i = 0; i < row; ++i) {//检查该列中是否有Queen if (board[i][col] == 'Q') return false; } for (int i = row - 1, j = col + 1; i >= 0 && j < n; --i, ++j) {//检查右上方是否有Queen if (board[i][j] == 'Q') return false; } for (int i = row - 1, j = col - 1; i >= 0 && j >= 0; --i, --j) {//检查左上方是否有Queen if (board[i][j] == 'Q') return false; } return true; } //将char[][]的行转换成List集合 public List<String> Array2List(char[][] board) { List<String> list = new ArrayList<>(); for (char[] c : board) { list.add(String.copyValueOf(c)); } return list; } public void backtracking(int n, char[][] board, int row) { //结束条件 if (row == n) { res.add(Array2List(board)); return; } //选择列表 for (int col = 0; col < n; ++col) { if (!isValid(board, row, col, n)) continue;//该位置不能放置Queen继续遍历该行中的列 board[row][col] = 'Q';//修改当前节点 backtracking(n, board, row + 1); board[row][col] = '.';//回溯当前节点 } } }
执行用时: 3 ms 内存消耗: 38.4 MB
广度优先搜索属于图算法的一种,英文缩写为BFS即Breadth First Search.,一层层遍历,遍历完一层再遍历下一层。遍历需要用先入先出的队列来实现。
题解:
本题转化为两个岛屿之间的最短距离,先深度优先搜索得到一个岛屿,然后广度优先搜索查找与另一个岛屿的最短距离。
class Solution { public int shortestBridge(int[][] A) { //新建偏移量数组 int [][] direction = new int [][]{{1,0},{-1,0},{0,1},{0,-1}}; Deque<int[]> queue = new ArrayDeque<>(); int ans = -1; boolean[][] visited = new boolean[A.length][A[0].length]; boolean flag = true; for(int i = 0; i < A.length && flag; i++){ for(int j = 0; j < A[0].length; j++) { if (A[i][j] == 1) { dfs(A, i, j, queue, visited); flag = false; break; } } } while (!queue.isEmpty()){ int size = queue.size(); ans++; for(int i = 0; i < size; i++){ int[] node = queue.poll(); for(int j = 0; j < 4; j++){ int nx = node[0] + direction[j][0]; int ny = node[1] + direction[j][1]; if(nx<0 || nx >= A.length || ny<0 || ny>=A[0].length || visited[nx][ny]) continue; if(A[nx][ny] == 1) return ans; visited[nx][ny] = true; queue.add(new int[]{nx,ny}); } } } return ans; } private void dfs(int[][] A, int i, int j, Deque queue, boolean[][] visited){ if(i<0 || i>=A.length || j<0 || j>=A[0].length || visited[i][j] || A[i][j]!=1) return; visited[i][j] = true; queue.add(new int []{i,j}); dfs(A, i-1, j, queue, visited); dfs(A, i+1, j, queue, visited); dfs(A, i, j-1, queue, visited); dfs(A, i, j+1, queue, visited); } }
执行用时: 9 ms 内存消耗: 39.1 MB
题解:
把起始字符串、终止字符串、以及单词表里所有的字符串作为节点,则:
1)若两个字符串只有一个字符不同,则它们相连,BFS遍历构建图;
2)使用DFS求起始节点到终止节点的最短距离即为所求最短转换序列。
class Solution { public List<List<String>> findLadders(String beginWord, String endWord, List<String> wordList) { //新建集合储存返回值 List<List<String>> res = new ArrayList<>(); //将 wordList 存入哈希表命名为dict,快速判断单词是否存在wordList中 Set<String> dict = new HashSet<>(wordList); //特殊情况判断 if (!dict.contains(endWord)) { return res; } //dict中不包含beginWord dict.remove(beginWord); /** *BFS遍历构建图 **/ //创建集合steps记录已经访问过的word集合和在第几层访问到,其中key为单词,value为BFS的层数 Map<String, Integer> steps = new HashMap<>(); steps.put(beginWord, 0); //创建集合from记录单词从哪些单词扩展而得,其中key为单词,value为单词列表,两者关系为一对多 Map<String, Set<String>> from = new HashMap<>(); boolean found = bfs(beginWord, endWord, dict, steps, from); //DFS遍历找到所有解,从endWord恢复到beginWord if (found) { //新建双端队列path Deque<String> path = new ArrayDeque<>(); path.add(endWord); dfs(from, path, beginWord, endWord, res); } return res; } private boolean bfs(String beginWord, String endWord, Set<String> dict, Map<String, Integer> steps, Map<String, Set<String>> from) { int wordLen = beginWord.length(); int step = 0; boolean found = false; Queue<String> queue = new LinkedList<>(); queue.offer(beginWord); while (!queue.isEmpty()) { step++; int size = queue.size(); for (int i = 0; i < size; i++) { String currWord = queue.poll(); char[] charArray = currWord.toCharArray(); for (int j = 0; j < wordLen; j++) { char origin = charArray[j]; for (char c = 'a'; c <= 'z'; c++) { //每一位替换成26个小写字母 charArray[j] = c; String nextWord = String.valueOf(charArray); if (steps.containsKey(nextWord) && steps.get(nextWord) == step) { from.get(nextWord).add(currWord); } if (!dict.contains(nextWord)) { continue; } dict.remove(nextWord); queue.offer(nextWord); from.putIfAbsent(nextWord, new HashSet<>()); from.get(nextWord).add(currWord); steps.put(nextWord, step); if (nextWord.equals(endWord)) { found = true; } } charArray[j] = origin; } } if (found) { break; } } return found; } private void dfs(Map<String, Set<String>> from, Deque<String> path, String beginWord, String cur, List<List<String>> res) { if (cur.equals(beginWord)) { res.add(new ArrayList<>(path)); return; } for (String precursor : from.get(cur)) { path.addFirst(precursor); dfs(from, path, beginWord, precursor, res); path.removeFirst(); } } }
执行用时: 14 ms 内存消耗: 39 MB
定义
动态规划(Dynamic Programming, DP)是一种在数学、管理科学、计算机科学、经济学和生物信息学中使用的,通过把原问题分解为相对简单的子问题的方式求解复杂问题的方法。动态规划常常适用于有重叠子问题和最优子结构性质的问题。
解题套路
如果一个问题,可以把所有可能的答案穷举出来,并且穷举出来后,发现存在重叠子问题,就可以考虑使用动态规划,如最长递增子序列、最小编辑距离、背包问题、凑零钱问题等等。
解题思路
动态规划的核心思想就是拆分子问题,记住过往,减少重复计算,并且动态规划一般都是自底向上的。
1)穷举分析:
2)确定边界:找出特殊边界,确定动态规划的范围;
3)找出规律确定最优子结构:一道动态规划问题,其实就是一个递推问题。假设当前决策结果是f(n),则最优子结构就是要让 f(n-k) 最优,最优子结构性质就是能让转移到n的状态是最优的,并且与后面的决策没有关系,即让后面的决策安心地使用前面的局部最优解的一种性质;
4)写出状态转移方程
题解:
斐波那契数列问题,每次走一步或两步,则到达第i阶可以从i-1阶或i-2阶开始,可得到状态转移方程dp[i] = dp[i - 1] + dp[i - 2]
class Solution { public int climbStairs(int n) { int[] dp = new int[n + 1]; dp[0] = 1; dp[1] = 1; for (int i = 2; i <= n; ++i) { dp[i] = dp[i - 1] + dp[i - 2]; } return dp[n]; } }
执行用时: 0 ms 内存消耗: 35 MB
题解:
数组dp[i]表示抢劫到第i个房子时,可抢到的最大数量,有如下两种情况:
1)不抢劫第i个房子,则累计金额为dp[i-1];
2)抢劫第i个 房子,第i个房子序号为i-1,则累计金额为nums[i-1] + dp[i-2]。
状态转移方程为dp[i] = Math.max(dp[i-1], nums[i-1] + dp[i-2])
class Solution { public int rob(int[] nums) { if (nums == null) return 0; int n = nums.length; int[] dp = new int[n + 1]; dp[1] = nums[0]; for (int i = 2; i <= n; ++i) { dp[i] = Math.max(dp[i-1], nums[i-1] + dp[i-2]); } return dp[n]; } }
执行用时: 0 ms 内存消耗: 35.9 MB
题解:
class Solution { public int numberOfArithmeticSlices(int[] nums) { int n = nums.length; int[] dp = new int[n]; int sum = 0; for (int i = 2; i < n; ++i) { //满足等差数列条件 if (nums[i] - nums[i-1] == nums[i-1] - nums[i-2]) { dp[i] = dp[i-1] + 1; sum += dp[i]; } } return sum; } }
执行用时: 0 ms 内存消耗: 36.3 MB
输入:grid = [[1,3,1],[1,5,1],[4,2,1]]
输出:7
解释:因为路径 1→3→1→1→1 的总和最小。
题解:
穷举分析:假设此时要移动到grid[1][1] = 5,有两种情况,①从上面dp[0][1]移动到该位置 ,则此时最和为grid[0][0]+grid[0][1]+grid[1][1]=1+3+5=9;②从左侧dp[1][0]移动该位置,则此时最和为grid[0][0]+grid[1][0]+grid[1][1]=1+1+5=7;
确定边界:行和列都是从0开始;
最优子结构:假设第i行第j列的最小和为dp[i][j],可得上侧dp[i-1][j]和左侧dp[i][j-1]的较小值加上该位置的值;
状态转移方程:dp[i][j] = min(dp[i-1][j], dp[i][j-1]) + grid[i][j]
空间压缩:矩阵中每一个值只与左侧或上侧值相关,可将dp数组压缩为一维数组,dp[j-1] = dp[i][j-1],dp[j]=dp[i-1][j],转移方程可写为dp[j] = Math.min(dp[j], dp[j-1]) + grid[i][j]
class Solution { public int minPathSum(int[][] grid) { int[] dp = new int[grid[0].length]; for (int i = 0; i < grid.length; ++i) { for (int j = 0; j < grid[0].length; ++j) { if (i == 0 && j == 0) { dp[j] = grid[i][j]; } else if (i == 0) { dp[j] = dp[j-1] + grid[i][j]; } else if (j == 0) { dp[j] = dp[j] + grid[i][j]; } else { dp[j] = Math.min(dp[j], dp[j-1]) + grid[i][j]; } } } return dp[grid[0].length-1]; } }
执行用时: 3 ms 内存消耗: 41.1 MB
题解:
穷举分析:matrix[1][1]上下左右四个方向到0的距离都是1;
确定边界:从0到n;
最优子结构:最近的0的位置,是在左上,右上,左下,右上4个方向中的最小值;
状态转移方程:
class Solution { public int[][] updateMatrix(int[][] matrix) { int m = matrix.length, n = matrix[0].length; int[][] dp = new int[m][n]; //和官方题解的差异设置dp[]的默认值增加了时间和空间 for (int i = 0; i < m; i++) { for (int j = 0; j < n; j++) { dp[i][j] = matrix[i][j] == 0 ? 0 : 10000; } } // 从左上角开始(右上方已经包含其中) for (int i = 0; i < m; i++) { for (int j = 0; j < n; j++) { //上侧 if (i - 1 >= 0) { dp[i][j] = Math.min(dp[i][j], dp[i - 1][j] + 1); } //左侧 if (j - 1 >= 0) { dp[i][j] = Math.min(dp[i][j], dp[i][j - 1] + 1); } } } // 从右下角开始(左下方已经包含其中) for (int i = m - 1; i >= 0; i--) { for (int j = n - 1; j >= 0; j--) { //下侧 if (i + 1 < m) { dp[i][j] = Math.min(dp[i][j], dp[i + 1][j] + 1); } //右侧 if (j + 1 < n) { dp[i][j] = Math.min(dp[i][j], dp[i][j + 1] + 1); } } } return dp; } }
执行用时: 9 ms 内存消耗: 41.9 MB
官方题解:
class Solution { static int[][] dirs = {{-1, 0}, {1, 0}, {0, -1}, {0, 1}}; public int[][] updateMatrix(int[][] matrix) { int m = matrix.length, n = matrix[0].length; // 初始化动态规划的数组,所有的距离值都设置为一个很大的数 int[][] dist = new int[m][n]; for (int i = 0; i < m; ++i) { Arrays.fill(dist[i], Integer.MAX_VALUE / 2); } // 如果 (i, j) 的元素为 0,那么距离为 0 for (int i = 0; i < m; ++i) { for (int j = 0; j < n; ++j) { if (matrix[i][j] == 0) { dist[i][j] = 0; } } } // 只有 水平向左移动 和 竖直向上移动,注意动态规划的计算顺序 for (int i = 0; i < m; ++i) { for (int j = 0; j < n; ++j) { if (i - 1 >= 0) { dist[i][j] = Math.min(dist[i][j], dist[i - 1][j] + 1); } if (j - 1 >= 0) { dist[i][j] = Math.min(dist[i][j], dist[i][j - 1] + 1); } } } // 只有 水平向右移动 和 竖直向下移动,注意动态规划的计算顺序 for (int i = m - 1; i >= 0; --i) { for (int j = n - 1; j >= 0; --j) { if (i + 1 < m) { dist[i][j] = Math.min(dist[i][j], dist[i + 1][j] + 1); } if (j + 1 < n) { dist[i][j] = Math.min(dist[i][j], dist[i][j + 1] + 1); } } } return dist; } }
执行用时: 7 ms 内存消耗: 41.7 MB
题解:
穷举分析:新建dp数组,dp[i][j]表示以(i,j)为右下角全由1构成的最大正方形,穷举见下图;
确定边界:行遍历1到m,列遍历1到n;
最优子结构:dp[i][j]的值为其左、左上、上三个位置的最小值,再加1;
状态转移方程:dp[i][j] = Math.min(dp[i-1][j-1], Math.min(dp[i][j-1], dp[i-1][j])) + 1
class Solution { public int maximalSquare(char[][] matrix) { if (matrix == null || matrix[0] == null) { return 0; } int m = matrix.length, n = matrix[0].length, max_side = 0; int[][] dp = new int[m+1][n+1]; for (int i = 1; i <= m; ++i) { for (int j = 1; j <= n; ++j) { if (matrix[i-1][j-1] == '1') { dp[i][j] = Math.min(dp[i-1][j-1], Math.min(dp[i][j-1], dp[i-1][j])) + 1; } max_side = Math.max(max_side, dp[i][j]); } } return max_side * max_side; } }
执行用时: 6 ms 内存消耗: 41.6 MB
官方题解:
class Solution { public int maximalSquare(char[][] matrix) { int maxSide = 0; if (matrix == null || matrix.length == 0 || matrix[0].length == 0) { return maxSide; } int rows = matrix.length, columns = matrix[0].length; int[][] dp = new int[rows][columns]; for (int i = 0; i < rows; i++) { for (int j = 0; j < columns; j++) { if (matrix[i][j] == '1') { if (i == 0 || j == 0) { dp[i][j] = 1; } else { dp[i][j] = Math.min(Math.min(dp[i - 1][j], dp[i][j - 1]), dp[i - 1][j - 1]) + 1; } maxSide = Math.max(maxSide, dp[i][j]); } } } int maxSquare = maxSide * maxSide; return maxSquare; } }
执行用时: 6 ms 内存消耗: 41.5 MB
题解:
class Solution { public int numSquares(int n) { int[] dp = new int[n + 1]; for (int i = 1; i <= n; ++i) { dp[i] = i; for (int j = 1; j * j <= i; ++j) { dp[i] = Math.min(dp[i], dp[i-j*j] + 1); } } return dp[n]; } }
执行用时: 41 ms 内存消耗: 37.6 MB
官方题解:
class Solution { public int numSquares(int n) { int[] f = new int[n + 1]; for (int i = 1; i <= n; i++) { int minn = Integer.MAX_VALUE; for (int j = 1; j * j <= i; j++) { minn = Math.min(minn, f[i - j * j]); } f[i] = minn + 1; } return f[n]; } }
执行用时: 30 ms 内存消耗: 37.5 MB
题解:
特殊边界:在字符串前加入空格避免出现两个数中前一个为0的情况,同时将边界定为从1开始,避免负数;
最优子结构:对于字符串s中的位置i,解码条件只有①i独立解码成字符,②i与i-1解码成字符;
状态转移方程:定义 f[i] 为考虑前 i个字符的解码方案数
f[i]=f[i−1],1⩽a≤9
f[i]=f[i−2],10⩽b⩽26
f[i]=f[i−1]+f[i−2],1⩽a≤9,10⩽b⩽26
空间优化:转换f[i]时只与前两个元素有关,可将数组定义为只有三个元素的数组。
class Solution { public int numDecodings(String s) { int n = s.length(); s = " " + s; char[] cs = s.toCharArray(); int[] dp = new int[3]; dp[0] = 1; for (int i = 1; i <= n; ++i) { dp[i % 3] = 0; //a代表当前位置单独转码,b代表当前位置和前一个位置共同转码 int a = cs[i] - '0', b = (cs[i - 1] - '0') * 10 + (cs[i] - '0'); if (1 <= a && a <= 9) dp[i % 3] = dp[(i - 1) % 3]; if (10 <= b && b <= 26) dp[i % 3] += dp[(i - 2) % 3]; } return dp[n % 3]; } }
执行用时: 2 ms 内存消耗: 36.6 MB
官方题解:
class Solution { public int numDecodings(String s) { int n = s.length(); int[] f = new int[n + 1]; f[0] = 1; for (int i = 1; i <= n; ++i) { if (s.charAt(i - 1) != '0') { f[i] += f[i - 1]; } if (i > 1 && s.charAt(i - 2) != '0' && ((s.charAt(i - 2) - '0') * 10 + (s.charAt(i - 1) - '0') <= 26)) { f[i] += f[i - 2]; } } return f[n]; } }
执行用时: 1 ms 内存消耗: 36.9 MB
题解:
最优子结构:枚举 s[0…i-1]中的分割点 j ,看 s[0…j-1]组成的字符串 s1(默认j=0 时s1为空串)和s[j…i−1] 组成的字符串 s2是否都合法,如果两个字符串均合法,那么按照定义s1和 s2拼接成的字符串也同样合法。
边界条件:定义 dp[0]=true 表示空串且合法
状态转移方程:dp[i] = dp[j] && wordDictSet.contains(s.substring(j, i))
public class Solution { public boolean wordBreak(String s, List<String> wordDict) { Set<String> wordDictSet = new HashSet(wordDict); boolean[] dp = new boolean[s.length() + 1]; dp[0] = true; for (int i = 1; i <= s.length(); i++) { for (int j = 0; j < i; j++) { //s1是否包含在字典判断dp[j]是否为真可知且s2包含在字典里 if (dp[j] && wordDictSet.contains(s.substring(j, i))) { dp[i] = true; break; } } } return dp[s.length()]; } }
题解:
状态定义:dp[i] 表示以 i 结尾的、最长子序列长度;
最优子结构:对于每一个位置 i,如果其之前的某个位置 j 所对应的数字小于位置 i 所对应的数字,则可以得到一个以 i 结尾的、长度为 dp[j] + 1
的子序列。
状态转移方程:dp[i] = Math.max(dp[i], dp[j] + 1)
class Solution { public int lengthOfLIS(int[] nums) { int maxLength = 0, n = nums.length; if (n <= 1) return n; int[] dp = new int[n + 1]; //所有元素置1,含义是每个元素都至少可以单独成为子序列,此时长度都为1 Arrays.fill(dp, 1); for (int i = 0; i < n; ++i) { for (int j = 0; j < i; ++j) { if (nums[i] > nums[j]) { dp[i] = Math.max(dp[i], dp[j] + 1); } } maxLength = Math.max(maxLength, dp[i]); } return maxLength; } }
执行用时: 74 ms 内存消耗: 38.1 MB
使用二分查找代替遍历降低时间复杂度:
遍历每一个位置 i,如果其对应的数字大于 dp 数组中所有数字的值,那么我们把它放在 dp 数组尾部,表示最长递增子序列长度加 1;如果这个数字在 dp 数组中比数字 a 大、比数字 b 小,则我们将 b 更新为此数字,使得之后构成递增序列的可能性增大。
class Solution { public int lengthOfLIS(int[] nums) { int[] tails = new int[nums.length]; int res = 0; for(int num : nums) { int i = 0, j = res; while(i < j) { int m = (i + j) / 2; if(tails[m] < num) i = m + 1; else j = m; } tails[i] = num; if(res == j) ++res; } return res; } }
执行用时: 4 ms 内存消耗: 38.1 MB
题解:
状态定义:定义 dp[i][j] 表示 text1[0,i-1] 和 text2[0,j-1] 的最长公共子序列;
穷举分析:
①两个字符串最后一位相等ac和bc,dp[1][1] = dp0][0] + 1;
②两个字符串最后一位不相等ace和bc,dp[2][1] = max(dp[1][1], dp[2][0]),即ace和b的最长子序列长度与ac和bc最长子序列长度的较大值;
状态转移方程:
当 text1[i - 1] == text2[j - 1]时,dp[i][j]=dp[i−1][j−1]+1;
当 text1[i - 1] != text2[j - 1]时,dp[i][j] = max(dp[i - 1][j], dp[i][j - 1]);
状态初始化:
当 i=0 或 j=0 时 dp[0][j] 或 dp[i][0] 都为0,因此 i 和 j 从1开始遍历;
class Solution { public int longestCommonSubsequence(String text1, String text2) { int m = text1.length(), n = text2.length(); int[][] dp = new int[m+1][n+1]; for (int i = 1; i <= m; ++i) { for (int j = 1; j <= n; ++j) { if (text1.charAt(i-1) == text2.charAt(j-1)) { dp[i][j] = dp[i-1][j-1] + 1; } else { dp[i][j] = Math.max(dp[i-1][j], dp[i][j-1]); } } } return dp[m][n]; } }
执行用时: 12 ms 内存消耗: 42.2 MB
定义:有N个物品和容量为W的背包,每个物品都有自己的体积w和价值v,求选取哪些物品填充背包可以使背包装下物品的总价值最大。有两种类型:①0-1问题:限定每种物品只能选择0个或1个;②无界或完全背包问题:不限定每种物品的数量。
0-1问题:一个物品只能拿一次或不拿
状态定义: dp[i][j] 表示前 i 件物品体积不超过 j 的情况下能达到的最大价值;
穷举分析:遍历第i件物品,此时包的容量为j,有两种情况:①不将物品i放入包中,前i个物品的最大价值等于前i-1个物品的最大价值;②将物品i放入包中,假设物品i的体积为w,价值为v,前i个物品的最大价值为前i-1个物品,体积为j-w的最大价值加i的价值v;
状态转移方程:
dp[i][j] = max(dp[i-1][j], dp[i-1[j-w] + v)
状态初始化:当i=0,j=0时,即不选择,不考虑,i和j从1开始遍历
返回值:i 和 j 取最大值即dp[N][W]
public int knapsack(int[] weights, int[] value, int N, int W) { int[][] dp = new int[N+1][W+1]; for (int i = 1; i <= N; ++i) { int w = weights[i-1], v = value[i-1]; for(int j = 1; j <= W; ++j) { if (j >= w) { dp[i][j] = Math.max(dp[i-1][j], dp[i-1][j-w] + v); } else { dp[i][j] = dp[i-1][j]; } } } retrun dp[N][W]; }
时间复杂度:O(NW) 空间复杂度:O(NW)
空间优化:
假设当前遍历物品 i=2,体积为 w=1,v=3,对于当前背包容量j,根据状态转移方程:dp[i][j] = max(dp[i-1][j], dp[i-1][j-w] + v)
可知dp[2][j]=max(dp[1][j], dp[1][j-1]+3
,即 i=2 的最大价值仅与其上一排 i=1 有关,因此可以省略维度 i ,此时状态转移方程变为dp[j] = max(dp[j], dp[j-w] + v)
。在遍历背包容量 j 时必须使用 --j 逆向遍历数组,从下图可如果正向遍历,遍历到 j 前,dp[j-w]已经更新成 i 的值。
public int knapsack(int[] weights, int[] values, int N, int W){ int[] dp = new int[W + 1]; for (int i = 1; i <= N; ++i) { int w = weights[i-1], v = values[i-1]; for (int j = W, j >= w; --j) { dp[j] = Math.max(dp[j], dp[j-w] + v); } } return dp[W];
空间复杂度:O(W)
完全背包问题:一个物品可拿多次
状态定义: dp[i][j] 表示前 i 件物品体积不超过 j 的情况下能达到的最大价值;
穷举分析:假设遍历到物品 i=2 ,体积 w=2 ,价值 v=3,此时背包容量 j=5,那么该背包只能容纳最多两个物品 i,有如下三种情况:①不放入物品 i,则dp[2][5] = dp[1][5]; ②放入一个物品 i,dp[2][5] = dp[1][3] + 3; ③放入两个物品 i,dp[2][5] = dp[1][1] + 6.在遍历到dp[2][3]时已经计算过dp[1][3]和dp[2][1],在计算dp[2][1]时也计算过dp[1][1],因此情况②③可以用仅考虑dp[2][3]代替,即dp[2][5] = max(dp[1][5], dp[2][3] + 3;
状态转移方程:dp[i][j] = max(dp[i-1][j], dp[i][j-w] + v)
public int knapsack(int[] weights, int[] value, int N, int W) { int[][] dp = new int[N+1][W+1]; for (int i = 1; i <= N; ++i) { int w = weights[i-1], v = value[i-1]; for(int j = 1; j <= W; ++j) { if (j >= w) { dp[i][j] = Math.max(dp[i-1][j], dp[i][j-w] + v); } else { dp[i][j] = dp[i-1][j]; } } } retrun dp[N][W]; }
空间优化:
与0-1背包问题不同,完全背包问题遍历 j 必须正向遍历,因为当前的价值与前面 j-w 列有关。
public int knapsack(int[] weights, int[] values, int N, int W){ int[] dp = new int[W + 1]; for (int i = 1; i <= N; ++i) { int w = weights[i-1], v = values[i-1]; for (int j = w, j >= W; ++j) { dp[j] = Math.max(dp[j], dp[j-w] + v); } } return dp[W];
题解:
此问题可转化为从输入数组中挑选出一些正整数,使得这些数的和等于整个数组元素的和的一半。
状态定义:dp[i][j]表示从数组的 [0, i] 这个子区间内挑选一些正整数,每个数只能用一次,使得这些数的和恰好等于 j;
穷举分析:①选择nums[i],如果在[0,i-1]区间内有一部分元素的和为 j ,则dp[i][j] = true;②不选择nums[i],如果在[0,i-1]区间内有一部分元素和为 j - nums[i],则dp[i][j] = true;
确定边界:当容量j为0s
状态转移方程:dp[i][j] = dp[i-1][j] || dp[i-1][j-nums[i-1]]
空间优化:见章首,去掉一个空间
class Solution { public boolean canPartition(int[] nums) { //求数组和 int sum = Arrays.stream(nums).sum(); //数组和不能平分直接返回false if (sum % 2 != 0) return false; int target = sum / 2, n = nums.length; boolean[] dp = new boolean[target + 1]; dp[0] = true; for (int i = 1; i <= n; ++i) { for (int j = target; j >= nums[i-1]; --j) { dp[j] = dp[j] || dp[j-nums[i-1]]; } } return dp[target]; } }
时间复杂度:O(NC) 其中N是数组元素的个数,C是数组元素和的一半
空间复杂度:O©
执行用时: 22 ms 内存消耗: 37.9 MB
技巧:Java数组求和
int sum = Arrays.stream(nums).sum();
题解:
状态定义:dp[i][j][k]
表示输入字符串在[0, i] 区间内能够使用 j 个 0 和 k 个 1 的字符串的最大数量;
穷举分析:当遍历到物品 i 时存在两种情况:①不选择该物品,此时字符串的最大数量与物品 i-1 相同;②选择该物品,此时 j = j - count0 , k = k - count1,即dp[i][j][k] = dp[i-1][j-count0][k-count1] +1 ;
状态转移方程:
dp[i][j][k] = Math.max(dp[i - 1][j][k], dp[i - 1][j - count0][k - count1] + 1)
返回结果:满足题目要求的最大值dp[strs.length][m][n]
空间优化:根据状态转移方程可知当前物品 i 的最大子集数仅与物品 i-1 的最大子集数有关,可省略一层;因为正序遍历使用的上层数据可能以前被之前的遍历所改变,所以需要使用倒序遍历。
public class Solution { public int findMaxForm(String[] strs, int m, int n) { int[][] dp = new int[m + 1][n + 1]; dp[0][0] = 0; for (String s : strs) { int[] count = count(s); int count0 = count[0]; int count1 = count[1]; for (int i = m; i >= count0; i--) { for (int j = n; j >= count1; j--) { dp[i][j] = Math.max(dp[i][j], dp[i - count0][j - count1] + 1); } } } return dp[m][n]; } //计算当前字符串中0和1的数量 private int[] count(String str) { int[] res = new int[2]; for (char c : str.toCharArray()) { res[c - '0']++; } return res; } }
执行用时: 24 ms 内存消耗: 38 MB
题解:
状态定义:dp[i]为组成金额 i 所需最小的金币数量;
穷举分析:对于当前金额 i ,遍历到金币coin,存在两种情况:①不选择该金币,dp[i] = dp[i] ;②选择该金币,dp[i] = dp[i - coin] + 1;
状态转移方程:dp[i] = Math.min(dp[i], dp[i-coin] + 1)
返回结果:需要判断所需金币数是否大于金额(即金币金额全部为 1 的情况)。
class Solution { public int coinChange(int[] coins, int amount) { if (coins == null) return -1; int[] dp = new int[amount + 1]; //遍历初始化数组值 for (int i = 0; i < amount + 1; ++i) { dp[i] = amount + 1; } //数组填充方法 //Arrays.fill(dp, amount + 1); dp[0] = 0; for (int i = 1; i <= amount; ++i) { for (int coin : coins) { if (i >= coin) { dp[i] = Math.min(dp[i], dp[i-coin] + 1); } } } return dp[amount] > amount ? -1 : dp[amount]; } }
执行用时: 11ms 内存消耗: 37.9 MB
时间复杂度:O{Sn),S为金额,n为硬币数;
空间复杂度:O(S)
技巧:
使用数组遍历方式初始化数组值相比于Arrays.fill()
方法可提升执行时间
题解:
状态定义:dp[i][j] 代表 word1 中前 i 个字符,变换到 word2 中前 j 个字符,所使用的最小操作数;
穷举分析:对于当前字母 i 存在三种操作:①增,在数组中向右遍历,则dp[i][j] = dp[i][j-1] + 1
;②减,在数组中向下遍历,则dp[i][j] = dp[i-1][j] + 1
;③改,在数组中向右下遍历,则dp[i][j] = dp[i-1][j-1] + 1
;
确定边界:i = 0 或 j = 0 属于特殊情况单独考虑;
状态转移方程:dp[i][j] = Math.min(dp[i-1][j-1] + ((word1.charAt(i-1) == word2.charAt(j-1)) ? 0 : 1), Math.min(dp[i-1][j] + 1, dp[i][j-1] + 1));
返回结果:i,j的最大值为m,n,返回值为数组终点即dp[m][n]
。
class Solution { public int minDistance(String word1, String word2) { int m = word1.length(), n = word2.length(); int[][] dp = new int[m+1][n+1]; for (int i = 0; i <= m; ++i) { for (int j = 0; j <= n; ++j) { if (i == 0) { dp[i][j] = j; } else if (j == 0) { dp[i][j] = i; } else { dp[i][j] = Math.min(dp[i-1][j-1] + ((word1.charAt(i-1) == word2.charAt(j-1)) ? 0 : 1), Math.min(dp[i-1][j] + 1, dp[i][j-1] + 1)); } } } return dp[m][n]; } }
执行用时: 5 ms 内存消耗: 38.4 MB
时间复杂度:O{mn)
空间复杂度:O(mn)
题解:
状态定义:dp[i] 表示得到 i 个A需要的最少操作次数;
穷举分析:①如果 n 为质数,则需要copy一次,paste n-1 次,则dp[i] = i
; ②如果 n 为合数,则 n 可以被分解,当 j 可以被 i 整除,该 j 为 i 的一个因子,例:
当 i = 12,
j = 2,12 = 2 * 6,dp[j]
=CPdp[i/j]
=CPPPPP共8步
j = 3,12 = 3 * 4,dp[j]
=CPPdp[i/j]
=CPPP共7步
j = 4,12 = 4 * 3,dp[j]
=CPPPdp[i/j]
=CPP共7步
j = 6,12 = 6 * 2,dp[j]
=CPPPPPdp[i/j]
=CP共8步
i 与 i / j 交换得到的结果相等,那么 j 的遍历终点可以设置为根号 n ,dp[j]为得到因子 j 需要的操作步数,dp[i/j]为由 j 得到 i 需要的操作步数;
确定边界:i = 1
只有一个字母,需要0次操作,从 2 开始遍历;
状态转移方程:dp[i] = dp[j] + dp[i/j]
返回结果:遍历到 n ,返回为 dp[n] 。
class Solution { public int minSteps(int n) { int[] dp = new int[n+1]; //初始化int值 h 为根号 n int h = (int) Math.sqrt(n); for (int i = 2; i <= n; ++i) { dp[i] = i; //遍历获得 i 的最大因子 j for (int j = 2; j <= h; ++j) { //如果 i 可以被 j 整除,则因子 j 存在 if (i % j == 0) { dp[i] = dp[j] + dp[i/j]; break; } } } return dp[n]; } }
执行用时: 1 ms 内存消耗: 35.5 MB
技巧:使用sqrt函数需要转义字符才能赋值给int
int h = (int) Math.sqrt(n);
题解:
状态定义:
class Solution { public boolean isMatch(String s, String p) { int m = s.length(), n = p.length(); boolean[][] dp = new boolean[m+1][n+1]; dp[0][0] = true; for (int i = 1; i < n + 1; ++i) { if (p.charAt(i-1) == '*') { dp[0][i] = dp[0][i-2]; } } for (int i = 1; i < m + 1; ++i) { for (int j = 1; j < n + 1; ++j) { if (p.charAt(j-1) == '.') { dp[i][j] = dp[i-1][j-1]; } else if (p.charAt(j-1) != '*') { dp[i][j] = dp[i-1][j-1] && p.charAt(j-1) == s.charAt(i-1); } else if (p.charAt(j-2) != s.charAt(i-1) && p.charAt(j-2) != '.') { dp[i][j] = dp[i][j-2]; } else { dp[i][j] = dp[i][j-1] || dp[i-1][j] || dp[i][j-2]; } } } return dp[m][n]; } }
执行用时: 2 ms 内存消耗: 36.8 MB
股票交易类问题通常可以用动态规划来解决。对于稍微复杂一些的股票交易类问题,比如需
要冷却时间或者交易费用,则可以用通过动态规划实现的状态机来解决。
题解:
class Solution { public int maxProfit(int[] prices) { int sell = 0, buy = -2147483648; for (int i = 0; i < prices.length; ++i) { buy = Math.max(buy, -prices[i]); sell = Math.max(sell, buy + prices[i]); } return sell; } }
题解:
class Solution { public int maxProfit(int k, int[] prices) { int days = prices.length; if (days < 2) { return 0; } if (k >= days) { return maxProfitUnlimited(prices); } int[] buy = new int[k + 1]; for (int i = 0; i < k + 1; ++i) { buy[i] = Integer.MIN_VALUE; } int[] sell = new int[k + 1]; for (int i = 0; i < days; ++i) { for (int j = 1; j <= k; ++j) { buy[j] = Math.max(buy[j], sell[j-1] - prices[i]); sell[j] = Math.max(sell[j], buy[j] + prices[i]); } } return sell[k]; } int maxProfitUnlimited(int[] prices) { int maxProfit = 0; for (int i = 1; i < prices.length; ++i) { if (prices[i] > prices[i-1]) { maxProfit += prices[i] - prices[i-1]; } } return maxProfit; } }
官方题解:
class Solution { public int maxProfit(int k, int[] prices) { if (prices.length == 0) { return 0; } int n = prices.length; k = Math.min(k, n / 2); int[] buy = new int[k + 1]; int[] sell = new int[k + 1]; buy[0] = -prices[0]; sell[0] = 0; for (int i = 1; i <= k; ++i) { buy[i] = sell[i] = Integer.MIN_VALUE / 2; } for (int i = 1; i < n; ++i) { buy[0] = Math.max(buy[0], sell[0] - prices[i]); for (int j = 1; j <= k; ++j) { buy[j] = Math.max(buy[j], sell[j] - prices[i]); sell[j] = Math.max(sell[j], buy[j - 1] + prices[i]); } } return Arrays.stream(sell).max().getAsInt(); } }
技巧:Java数组初始值为0,可以通过遍历的方式改变数组的初始值。
for (int i = 1; i <= k; ++i) { buy[i] = sell[i] = Integer.MIN_VALUE / 2; }
题解:
class Solution { public int maxProfit(int[] prices) { int n = prices.length; if (n == 0) { return 0; } int[] buy = new int[n]; int[] sell = new int[n]; int[] s1 = new int[n]; int[] s2 = new int[n]; s1[0] = buy[0] = -prices[0]; sell[0] = s2[0] = 0; for (int i = 1; i < n; i++) { buy[i] = s2[i-1] - prices[i]; s1[i] = Math.max(buy[i-1], s1[i-1]); sell[i] = Math.max(buy[i-1], s1[i-1]) + prices[i]; s2[i] = Math.max(s2[i-1], sell[i-1]); } return Math.max(sell[n-1], s2[n-1]); } }
官方题解:
class Solution { public int maxProfit(int[] prices) { if (prices.length == 0) { return 0; } int n = prices.length; int f0 = -prices[0]; int f1 = 0; int f2 = 0; for (int i = 1; i < n; ++i) { int newf0 = Math.max(f0, f2 - prices[i]); int newf1 = f0 + prices[i]; int newf2 = Math.max(f1, f2); f0 = newf0; f1 = newf1; f2 = newf2; } return Math.max(f1, f2); } }
概念: 通过把原问题分为子问题,再将子问题进行处理合并,从而实现对原问题的求解。如:归并排序,将大数组分解为小数组再合成一个大数组。
题解:
利用分治思想
class Solution { Map<String, List<Integer>> map = new HashMap(); public List<Integer> diffWaysToCompute(String input) { if (input == null || input.length() <= 0) { return new ArrayList<Integer>(); } if (map.containsKey(input)) return map.get(input); List<Integer> curRes = new ArrayList<Integer>(); int length = input.length(); char[] charArray = input.toCharArray(); for (int i = 0; i < length; i++) { char aChar = charArray[i]; if (aChar == '+' || aChar == '-' || aChar == '*') { // 当前字符为 操作符 List<Integer> leftList = diffWaysToCompute(input.substring(0, i)); List<Integer> rightList = diffWaysToCompute(input.substring(i + 1)); for (int leftNum : leftList) { for (int rightNum : rightList) { if (aChar == '+') { curRes.add(leftNum + rightNum); } else if (aChar == '-') { curRes.add(leftNum - rightNum); } else { curRes.add(leftNum * rightNum); } } } } } if (curRes.isEmpty()) { curRes.add(Integer.valueOf(input)); } map.put(input, curRes); return curRes; } }
官方题解:
class Solution { public class Status { public int lSum, rSum, mSum, iSum; public Status(int lSum, int rSum, int mSum, int iSum) { this.lSum = lSum; this.rSum = rSum; this.mSum = mSum; this.iSum = iSum; } } public int maxSubArray(int[] nums) { return getInfo(nums, 0, nums.length - 1).mSum; } public Status getInfo(int[] a, int l, int r) { if (l == r) { return new Status(a[l], a[l], a[l], a[l]); } int m = (l + r) >> 1; Status lSub = getInfo(a, l, m); Status rSub = getInfo(a, m + 1, r); return pushUp(lSub, rSub); } public Status pushUp(Status l, Status r) { int iSum = l.iSum + r.iSum; int lSum = Math.max(l.lSum, l.iSum + r.lSum); int rSum = Math.max(r.rSum, r.iSum + l.rSum); int mSum = Math.max(Math.max(l.mSum, r.mSum), l.rSum + r.lSum); return new Status(lSum, rSum, mSum, iSum); } }
执行用时: 1 ms 内存消耗: 38.9 MB
辗转相除法:
int gcd(int a, int b) { return b == 0 ? a : gcd(b, a % b); } int lcm(int a, int b) { return a * b / gcd(a, b); }
质数又称素数,指的是指在大于 1 的自然数中,除了 1 和它本身以外不再有其他因数的自然数。每一个数都可以分解成质数的乘积。
题解:
埃拉托斯特尼筛法:埃拉托斯特尼筛法,简称埃氏筛或爱氏筛,是一种由希腊数学家埃拉托斯特尼所提出的一种简单检定素数的算法。要得到自然数 n 以内的全部素数,必须把不大于根号n的所有素数的倍数剔除,剩下的就是素数。
class Solution { public int countPrimes(int n) { int[] isPrime = new int[n]; Arrays.fill(isPrime, 1); int ans = 0; for (int i = 2; i < n; ++i) { if (isPrime[i] == 1) { ans += 1; if ((long) i * i < n) { for (int j = i * i; j < n; j += i) { isPrime[j] = 0; } } } } return ans; } }
执行用时: 57 ms 内存消耗: 56.4 MB
时间复杂度:O(nlog logn)
空间复杂度:O(n)
题解:
class Solution { public String convertToBase7(int num) { if (num == 0) return "0"; boolean isNegative = num < 0; if (isNegative) num = -num; String res = ""; while (num != 0) { int a = num / 7, b = num % 7; res = Integer.toString(b) + res; num = a; } return isNegative ? "-" + res : res; } }
执行用时: 1 ms 内存消耗: 35.7 MB
技巧:int转义字符串使用
Integer.toString(int)
题解:
class Solution { public int trailingZeroes(int n) { return n == 0 ? 0 : n / 5 + trailingZeroes(n / 5); } }
执行用时: 0 ms 内存消耗: 35.4 MB
题解:
对数取模
class Solution { public boolean isPowerOfThree(int n) { return Math.log10(n) / Math.log10(3) % 1 == 0; } }
执行用时: 16 ms 内存消耗: 38.2 MB
题解:
class Solution { public boolean isPowerOfThree(int n) { return n > 0 && 1162261467 % n == 0; } }
执行用时: 14 ms 内存消耗: 38.4 MB
题解:
class Solution { public String addStrings(String num1, String num2) { StringBuilder res = new StringBuilder(""); int i = num1.length() - 1, j = num2.length() - 1, carry = 0; while(i >= 0 || j >= 0){ int n1 = i >= 0 ? num1.charAt(i) - '0' : 0; int n2 = j >= 0 ? num2.charAt(j) - '0' : 0; int tmp = n1 + n2 + carry; carry = tmp / 10; res.append(tmp % 10); i--; j--; } if(carry == 1) res.append(1); return res.reverse().toString(); } }
执行用时: 2 ms 内存消耗: 38.2 MB
题解:
class Solution { private int[] array; private int[] original; Random rand = new Random(); private int randRange(int min, int max) { return rand.nextInt(max - min) + min; } private void swapAt(int i, int j) { int temp = array[i]; array[i] = array[j]; array[j] = temp; } public Solution(int[] nums) { array = nums; original = nums.clone(); } /** Resets the array to its original configuration and return it. */ public int[] reset() { array = original; original = original.clone(); return original; } /** Returns a random shuffling of the array. */ public int[] shuffle() { for (int i = 0; i < array.length; ++i) { swapAt(i, randRange(i, array.length)); } return array; } } /** * Your Solution object will be instantiated and called as such: * Solution obj = new Solution(nums); * int[] param_1 = obj.reset(); * int[] param_2 = obj.shuffle(); */
题解:
前缀和+二分查找
概率:
p[取下标 0 ] = p[取范围[0,1)]
p[取下标 1 ] = p[取范围[1,4)]
所以将取下标 i 问题转化为取前缀和的问题,使用二分查找找到最近一个前缀和大于 target 的下标。
class Solution { int[] prefix;//前缀和数组 int sum = 0;//总和 Random random = new Random();//生成随机数 public Solution(int[] w) { //初始化前缀和数组 int n = w.length; prefix = new int[n]; prefix[0] = w[0]; //数组求和 for (int i = 1; i < n; ++i) prefix[i] = prefix[i-1] + w[i]; sum = prefix[n-1]; } public int pickIndex() { int target = random.nextInt(sum); //二分查找 int left = 0, right = prefix.length - 1; while (left < right) { int mid = left + (right - left) / 2; //区间为左闭右开,等于的情况也要将左指针右移 if (prefix[mid] <= target) left = mid + 1; else right = mid; } return left; } } /** * Your Solution object will be instantiated and called as such: * Solution obj = new Solution(w); * int param_1 = obj.pickIndex(); */
执行用时: 23 ms 内存消耗: 43.6 MB
技巧:
不带参数的nextInt()
会生成所有有效的整数(包含正数,负数,0);
带参的nextInt(int x)
则会生成一个范围在0~x(不包含 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 { //初始化链表集合 list List<Integer> list=new ArrayList<>(); //生成随机数 Random random = new Random(); /** @param head The linked list's head. Note that the head is guaranteed to be not null, so it contains at least one node. */ public Solution(ListNode head) { while(head != null){ list.add(head.val); head=head.next; } } /** Returns a random node's value. */ public int getRandom() { //生成范围内的任意正整数 int n = random.nextInt(list.size()); return list.get(n); } } /** * Your Solution object will be instantiated and called as such: * Solution obj = new Solution(head); * int param_1 = obj.getRandom(); */
执行用时: 9 ms 内存消耗: 39.9 MB
位运算符号:
符号 | 名称 |
---|---|
^ | 按位异或 |
& | 按位与 |
| | 按位或 |
~ | 取反 |
<< | 算术左移 |
>> | 算术右移 |
技巧:
n & (n-1) 删除 n 中 1 的最低位;
n & (-n) 得到 n 中 1 的最低位。
题解:
移位计数法
class Solution { public int hammingDistance(int x, int y) { int diff = x ^ y, ans = 0; while (diff != 0) { ans += diff & 1; diff >>= 1; } return ans; } }
执行用时: 0 ms 内存消耗: 35.2 MB
官方题解:
Brian Kernighan算法
class Solution { public int hammingDistance(int x, int y) { int s = x ^ y, ret = 0; while (s != 0) { s &= s - 1; ret++; } return ret; } }
题解:
逐位颠倒
public class Solution { // you need treat n as an unsigned value public int reverseBits(int n) { int ans = 0; for (int i = 0; i < 32; ++i) { ans <<= 1; ans += n & 1; n >>= 1; } return ans; } }
执行用时: 1 ms 内存消耗: 38.1 MB
官方题解:
位运算分治
public class Solution { private static final int M1 = 0x55555555; // 01010101010101010101010101010101 private static final int M2 = 0x33333333; // 00110011001100110011001100110011 private static final int M4 = 0x0f0f0f0f; // 00001111000011110000111100001111 private static final int M8 = 0x00ff00ff; // 00000000111111110000000011111111 public int reverseBits(int n) { n = n >>> 1 & M1 | (n & M1) << 1; n = n >>> 2 & M2 | (n & M2) << 2; n = n >>> 4 & M4 | (n & M4) << 4; n = n >>> 8 & M8 | (n & M8) << 8; return n >>> 16 | n << 16; } }
执行用时: 1 ms 内存消耗: 38.1 MB
题解:
class Solution { public int singleNumber(int[] nums) { int ans = 0; for (int num : nums) { ans ^= num; } return ans; } }
执行用时: 1 ms 内存消耗: 38.7 MB
时间复杂度:O(n)
空间复杂度:O(1)
题解:
class Solution { public boolean isPowerOfFour(int n) { return (n > 0) && ((n & (n-1)) == 0) && (n % 3 == 1); } }
执行用时: 1 ms 内存消耗: 35.2 MB
时间复杂度:O(1)
空间复杂度:O(1)
题解:
class Solution { public int maxProduct(String[] words) { int res = 0; int[] wordsNum = new int[words.length]; for(int i = 0; i < words.length; i++) { String word = words[i]; int num = 0; for(int j = 0; j < word.length(); j++) { int wei = word.charAt(j)-'a'; num = num | (1<<wei); } wordsNum[i] = num; } for(int i = 0; i < words.length; i++) { for(int j = i+1; j < words.length; j++) { if((wordsNum[i]&wordsNum[j]) != 0) continue; res = Math.max(res, words[i].length()*words[j].length()); } } return res; } }
执行用时: 7 ms 内存消耗: 38.4 MB
class Solution { public int[] countBits(int n) { int[] dp = new int[n+1]; for (int i = 1; i <= n; ++i) { dp[i] = dp[i&(i-1)] + 1; } return dp; } }
执行用时: 2 ms 内存消耗: 42.7 MB
题解:
排序+相邻元素比较
class Solution { public boolean containsDuplicate(int[] nums) { Arrays.sort(nums); int n = nums.length; for (int i = 0; i < n - 1; i++) { if (nums[i] == nums[i + 1]) { return true; } } return false; } }
执行用时: 3 ms 内存消耗: 41.8 MB
题解:
空间旋转法,每次只考虑四个间隔90°的位置。
class Solution { public void rotate(int[][] matrix) { int temp = 0, n = matrix.length - 1; for (int i = 0; i <= n/2; ++i) { for (int j = i; j < n-i; ++j) { //右到临时 temp = matrix[j][n-i]; //上转到右 matrix[j][n-i] = matrix[i][j]; //左转到上 matrix[i][j] = matrix[n-j][i]; //下转到左 matrix[n-j][i] = matrix[n-i][n-j]; //临时到下 matrix[n-i][n-j] = temp; } } } }
执行用时: 0 ms 内存消耗: 38.4 MB
时间复杂度:O(N²)
空间复杂度:O(1)
官方题解:
class Solution { public void rotate(int[][] matrix) { int n = matrix.length; // 水平翻转 for (int i = 0; i < n / 2; ++i) { for (int j = 0; j < n; ++j) { int temp = matrix[i][j]; matrix[i][j] = matrix[n - i - 1][j]; matrix[n - i - 1][j] = temp; } } // 主对角线翻转 for (int i = 0; i < n; ++i) { for (int j = 0; j < i; ++j) { int temp = matrix[i][j]; matrix[i][j] = matrix[j][i]; matrix[j][i] = temp; } } } }
执行用时: 0 ms 内存消耗: 38.7 MB
题解:
利用行和列已经排序号的特性
class Solution { public boolean searchMatrix(int[][] matrix, int target) { int m = matrix.length; if (m == 0) { return false; } int n = matrix[0].length; //从最小行和最大列的元素开始遍历 int i = 0, j = n - 1; while (i < m && j >= 0) { if (matrix[i][j] == target) { return true; //该值大于目标值,减小列 } else if (matrix[i][j] > target) { --j; //该值小于目标值,增大行 } else { ++i; } } return false; } }
执行用时: 4 ms 内存消耗: 44 MB
时间复杂度:O(n+m),每次迭代时都是增加 m 行或减小 n 列,所以是常数相加
空间复杂度:O(1),不需要额外空间仅维护两个指针
题解:
从左向右遍历,同时记录最大值:
(1)如果当前最大值大于数组位置,说明右侧一定有比当前位置小的数字,重新排列后不会升序,不能分割;
(2)如果当前最大值等于数组位置,说明右侧不再包含小于该位置的数,可分割一次。
class Solution { public int maxChunksToSorted(int[] arr) { int chunks = 0, cur = 0; //从左向右遍历 for (int i = 0; i < arr.length; ++i) { //记录最大值 cur = Math.max(cur, arr[i]); //当前最大值与数组位置相等,分割块数加1 if (cur == i) ++chunks; } return chunks; } }
执行用时: 0 ms 内存消耗: 35.8 MB
时间复杂度:O(N),N为数组长度需要遍历整个数组
空间复杂度:O(1)固定空间
题解:
用两个栈实现一个队列,通过额外栈反转数组。
一个栈表示队列输入,用于压入 push 操作;
一个栈表示队列输出,用于弹出 pop 和查看堆顶 peek。
class MyQueue { Stack<Integer> in = new Stack<Integer>(); Stack<Integer> out = new Stack<Integer>(); /** Initialize your data structure here. */ public MyQueue() { } /** Push element x to the back of queue. */ public void push(int x) { in.push(x); } /** Removes the element from in front of queue and returns that element. */ public int pop() { in2Out(); int x = out.peek(); out.pop(); return x; } /** Get the front element. */ public int peek() { in2Out(); return out.peek(); } //反转 void in2Out() { if (out.empty()) { while (!in.empty()) { int x = in.peek(); in.pop(); out.push(x); } } } /** Returns whether the queue is empty. */ public boolean empty() { return in.empty() && out.empty(); } } /** * Your MyQueue object will be instantiated and called as such: * MyQueue obj = new MyQueue(); * obj.push(x); * int param_2 = obj.pop(); * int param_3 = obj.peek(); * boolean param_4 = obj.empty(); */
执行用时: 0 ms 内存消耗: 36.2 MB
时间复杂度:O(1),push 和 empty 为 1 ,pop he peek 均摊复杂度为 1;
空间复杂度:O(n),n 为操作数。对于有 n 次 push 操作的情况,队列中会有 n 个元素。
题解:
辅助栈:数值栈+最小值栈
class MinStack { private Stack<Integer> s; private Stack<Integer> min_s; /** initialize your data structure here. */ public MinStack() { s = new Stack<>(); min_s = new Stack<>(); } public void push(int val) { s.push(val); if (min_s.empty() || min_s.peek() >= val) { min_s.push(val); } } //泛型类型比较,不能用== public void pop() { if (s.pop().equals(min_s.peek())) { min_s.pop(); } } public int top() { return s.peek(); } public int getMin() { return min_s.peek(); } } /** * Your MinStack object will be instantiated and called as such: * MinStack obj = new MinStack(); * obj.push(val); * obj.pop(); * int param_3 = obj.top(); * int param_4 = obj.getMin(); */
执行用时: 6 ms 内存消耗: 40.4 MB
时间复杂度:O(1),push、pop和getMin的最小时间复杂度为O(1)
空间复杂度:O(n),含 n 个元素的辅助栈的额外空间
技巧:
s.pop().equals(min_s.peek())
泛型类型比较不能用 == ,要用 equals
题解:
数组栈:数值和最小值放入一个数组中
class MinStack { private Stack<int[]> s = new Stack<>(); /** initialize your data structure here. */ public MinStack() {} public void push(int x) { if (s.isEmpty()) { s.push(new int[]{x, x}); } else { s.push(new int[]{x, Math.min(x, s.peek()[1])}); } } public void pop() { s.pop(); } public int top() { return s.peek()[0]; } public int getMin() { return s.peek()[1]; } } /** * Your MinStack object will be instantiated and called as such: * MinStack obj = new MinStack(); * obj.push(val); * obj.pop(); * int param_3 = obj.top(); * int param_4 = obj.getMin(); */
执行用时: 4 ms 内存消耗: 40.3 MB
题解:
链表模拟栈:数值与最小值绑定放入链表中
class MinStack { // 链表模拟栈 private Node node; // 内部类,栈中存储的就是一个个Node // 每个Node包含其元素本身的大小,同时还包含当前栈中最小的值,还有其先驱节点 private class Node { int val; int min; Node prev; private Node(int val, int min){ this.val = val; this.min = min; } private Node(int val, int min, Node prev){ this.val = val; this.min = min; this.prev = prev; } } public MinStack() { } // 添加元素 public void push(int x) { // 没有时,新建头结点 if (node == null){ node = new Node(x, x); } else { // 新建节点 Node next = new Node(x, Math.min(x, node.min)); // 指示节点后移 next.prev = node; node = next; } } // 反正不要返回结果,直接将指示节点前移 public void pop() { node = node.prev; } // 返回指示节点的值 public int top() { return node.val; } // 返回指示节点中记录的最小值 public int getMin() { return node.min; } }
执行用时: 5 ms 内存消耗: 40.3 MB
题解:
遍历+栈
字符转换成char数组并遍历:
①遍历到左括号时,右括号入栈;
②遍历到右括号时,与栈顶对比是否相等,不相等返回 false,若栈为空也返回 false;
遍历完后栈 stack 为空,则返回true
class Solution { public boolean isValid(String s) { Stack<Character> stack = new Stack<Character>(); for(char c: s.toCharArray()){ if(c=='(') stack.push(')'); else if(c=='[') stack.push(']'); else if(c=='{') stack.push('}'); else if(stack.isEmpty() || c != stack.pop()) return false; } return stack.isEmpty(); } }
执行用时: 1 ms 内存消耗: 36.1 MB
题解:
HashMap+链表
创建哈希表字典,key 为左括号, value 为右括号;创建链表 stack ,stack 不能弹出空,因此默认初始化加入一个 ? ;
遍历字符串转换成的 char 数组:
①遍历到左括号时即map.containsKey(c)
,stack 加入该字符;
②遍历到右括号时,map.get(stack.removeLast()
与栈顶对比是否相等,不相等返回 false;
返回结果stack.size() == 1
,只有 ?结果为true,否则还有剩余左括号返回 false。
class Solution { private static final Map<Character,Character> map = new HashMap<Character,Character>(){{ put('{','}'); put('[',']'); put('(',')'); put('?','?'); }}; public boolean isValid(String s) { if(s.length() > 0 && !map.containsKey(s.charAt(0))) return false; LinkedList<Character> stack = new LinkedList<Character>() {{ add('?'); }}; for(Character c : s.toCharArray()){ if(map.containsKey(c)) stack.addLast(c); else if(map.get(stack.removeLast()) != c) return false; } return stack.size() == 1; } }
执行用时: 1 ms 内存消耗: 36.4 MB
题解:
单调递减栈indices
:存放温度的位置,表示日期,即数组的序号;
数组ans
:储存日期差值
遍历数组,对于当前日期i
与当前栈顶序号代表的温度进行比较①如果i
的温度比栈顶pre_index
的温度高,则取出栈顶indices.pop()
,并记录当前日期的差值ans[pre_index] = i - pre_index
;②如果i
的温度比栈顶pre_index
的温度低,则将i
插入栈顶。栈内数组永远单调递减,避免使用排序进行比较。
class Solution { public int[] dailyTemperatures(int[] temperatures) { int n = temperatures.length; int[] ans = new int[n]; Stack<Integer> indices = new Stack<Integer>(); for (int i = 0; i < n; ++i) { while (!indices.empty()) { int pre_index = indices.peek(); //当前温度与栈顶代表温度比较 if (temperatures[i] <= temperatures[pre_index]) break; //栈顶弹出 indices.pop(); ans[pre_index] = i - pre_index; } //当前日期入栈 indices.push(i); } return ans; } }
执行用时: 32 ms 内存消耗: 47.4 MB
时间复杂度:O(n)
空间复杂度:O(n)
优先队列(PriorityQueue)是一个基于优先级的无界优先级队列,可以在 O(1) 时间内获得最大值,并且可以在 O(log n) 时间内取出最大值或插入任意值。
在优先级队列中,添加的对象根据其优先级。默认情况下,优先级由对象的自然顺序决定。队列构建时提供的比较器可以覆盖默认优先级:
详解优先队列
题解:
比较k
个节点(即每个链表的首节点),获得最小值的节点,将选中的节点放在最终有序链表的后端,使用优先队列可优化这个比较获取最小值的过程。
head : 保存合并之后的链表的头部;
tail : 记录下一个插入位置的前一个位置
/** * 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) { //声明优先队列,返回两个节点比较 PriorityQueue<ListNode> q = new PriorityQueue<>((x,y) -> x.val - y.val); //将输入节点放入优先队列 for (ListNode node : lists) { if (node != null) { q.add(node); } } //声明头节点和构建链表节点 ListNode head = new ListNode(); ListNode tail = head; //循环迭代队列,每次取出最小节点放入有序链表尾部 while (!q.isEmpty()) { tail.next = q.poll(); //将指针放入下一个节点 tail = tail.next; //如果下一个节点不为空则将下一个节点放入队列中 if (tail.next != null) { q.add(tail.next); } } //返回头节点的下一个节点 return head.next; } }
执行用时: 4 ms 内存消耗: 39.9 MB
时间复杂度:O(Nlogk), k 是链表的数目,N 是节点的数目;
空间复杂度:O(n) 创新一个新的链表的开销
题解:
优先队列+哈希表延迟删除
关键点选取规则:
如果是「从下到上」转向「水平方向」,纵坐标最大的点是关键点;
如果是「从上到下」转向「水平方向」,纵坐标第二大的点是关键点。
第一步:遍历建筑,将左右边缘转折点加入到数组buildingPoints中,将左边缘的高度处理成负数再加入数组中,将数组buildingPoints按照横坐标排序;
第二步:遍历边缘点,高度为负数即左边缘加入优先队列,右边缘加入到延迟删除哈希表delayed中;
第三步:通过高度差来判断当前转折点是否符合关键点选取规则,如果当前高度curheight与前一个高度不相等,则次转折点符合规则,curHeight为返回点的纵坐标,其对应的buildingPoints[0]为横坐标。
class Solution { public List<List<Integer>> getSkyline(int[][] buildings) { //新建储存端点的List List<int[]> buildingPoints = new ArrayList<>(); for (int[] b : buildings) { //左端点,纵坐标取负数做区分 buildingPoints.add(new int[]{b[0], -b[2]}); //右端点 buildingPoints.add(new int[]{b[1], b[2]}); } //按照横坐标排序,横坐标相同时高度高的排在前面 buildingPoints.sort((a, b) -> { return a[0] != b[0] ? a[0] - b[0] : a[1] - b[1]; }); PriorityQueue<Integer> maxHeap = new PriorityQueue<Integer>(Collections.reverseOrder()); //哈希表记录「延迟删除」的元素,key 为元素,value 为需要删除的次数 Map<Integer, Integer> delayed = new HashMap<>(); //为计算高度差初始化一个宽高为0的建筑 maxHeap.offer(0); //保存之前的最高高度,初始化为0 int lastHeight = 0; List<List<Integer>> res = new ArrayList<>(); for (int[] buildingPoint : buildingPoints) { if (buildingPoint[1] < 0) { //把负数高度翻转并加入优先队列 maxHeap.offer(-buildingPoint[1]); } else { //不是真的删除 buildingPoint[1],把它放进 delayed,等到堆顶元素是 buildingPoint[1] 的时候,才真的删除 delayed.put(buildingPoint[1], delayed.getOrDefault(buildingPoint[1], 0) + 1); } // 如果堆顶元素在延迟删除集合中,才真正删除,这一步可能执行多次,所以放在 while 中 // while (true) 都是可以的,因为 maxHeap 一定不会为空 while (!maxHeap.isEmpty()) { int curHeight = maxHeap.peek(); if (delayed.containsKey(curHeight)) { delayed.put(curHeight, delayed.get(curHeight) - 1); if (delayed.get(curHeight) == 0) { delayed.remove(curHeight); } maxHeap.poll(); } else { break; } } int curHeight = maxHeap.peek(); //当前高度与前一个高度不相等产生高度差,此时该转折点符合要求是一个关键点 if (curHeight != lastHeight) { res.add(Arrays.asList(buildingPoint[0], curHeight)); //改变高度 lastHeight = curHeight; } } return res; } }
执行用时: 26 ms 内存消耗: 42.4 MB
时间复杂度:O(NlogN)
技巧:延迟删除
官方题解:
扫描线+优先队列
关键点选取规则:
横坐标:每一座建筑物的边缘;
纵坐标:包含该横坐标的所有建筑物的最大高度,建筑的左边缘小于等于该横坐标,右边缘大于该横坐标(即不包含该右边缘)
第一步:初始化优先队列pq
,储存边缘的数组boundaries
并将数组排序;
第二步:把符合选取规则的边缘点加入到优先队列,确定关键点的横坐标;
第三步:新建返回数组ret
,根据高度判断当前边缘点是否符合选取规则,把符合规则的边缘点加入返回数组。
class Solution { public List<List<Integer>> getSkyline(int[][] buildings) { //使用延迟队列寻找最大高度 PriorityQueue<int[]> pq = new PriorityQueue<int[]>((a, b) -> b[1] - a[1]); //数组保存所有的边缘 List<Integer> boundaries = new ArrayList<Integer>(); //遍历建筑物并把边缘加入到边缘横坐标加入到数组 boundaries 中 for (int[] building : buildings) { //加入左边缘 boundaries.add(building[0]); //加入右边缘 boundaries.add(building[1]); } //将数组 boundaries 排序 Collections.sort(boundaries); //创建储存返回结果的List List<List<Integer>> ret = new ArrayList<List<Integer>>(); //初始化建筑横坐标 int n = buildings.length, idx = 0; //遍历边缘数组中的边缘点 for (int boundary : boundaries) { //判断建筑是否符合选取规则 while (idx < n && buildings[idx][0] <= boundary) { //将符合选取规则的建筑加入到队列中 pq.offer(new int[]{buildings[idx][1], buildings[idx][2]}); idx++; } //队首元素不符合关键点选取规则 while (!pq.isEmpty() && pq.peek()[0] <= boundary) { //队首元素弹出队列 pq.poll(); } //maxn 记录最大高度,当优先队列为空最大高度为0,否则最大高度为队首元素 int maxn = pq.isEmpty() ? 0 : pq.peek()[1]; //如果当前关键点的高度 maxn 与前一个前一个关键点ret.get(ret.size() - 1)的高度不相等时,将该关键点加入返回的ret if (ret.size() == 0 || maxn != ret.get(ret.size() - 1).get(1)) { ret.add(Arrays.asList(boundary, maxn)); } } return ret; } }
执行用时: 17 ms 内存消耗: 41.4 MB
时间复杂度:O(NlogN),N为建筑数量,优先队列入队出队复杂度为logN;
空间复杂度:O(N),N 为建筑数量,边缘数组和优先队列空间占用均为 N
题解:
class Solution { public int[] maxSlidingWindow(int[] nums, int k) { // 窗口个数 int[] res = new int[nums.length - k + 1]; LinkedList<Integer> queue = new LinkedList<>(); // 遍历数组中元素,right表示滑动窗口右边界 for(int right = 0; right < nums.length; right++) { // 如果队列不为空且当前考察元素大于等于队尾元素,则将队尾元素移除。 // 直到,队列为空或当前考察元素小于新的队尾元素 while (!queue.isEmpty() && nums[right] >= nums[queue.peekLast()]) { queue.removeLast(); } // 存储元素下标 queue.addLast(right); // 计算窗口左侧边界 int left = right - k +1; // 当队首元素的下标小于滑动窗口左侧边界left时 // 表示队首元素已经不再滑动窗口内,因此将其从队首移除 if (queue.peekFirst() < left) { queue.removeFirst(); } // 由于数组下标从0开始,因此当窗口右边界right+1大于等于窗口大小k时 // 意味着窗口形成。此时,队首元素就是该窗口内的最大值 if (right +1 >= k) { res[left] = nums[queue.peekFirst()]; } } return res; } }
执行用时: 28 ms 内存消耗: 52.5 MB
官方题解:
class Solution { public int[] maxSlidingWindow(int[] nums, int k) { int n = nums.length; PriorityQueue<int[]> pq = new PriorityQueue<int[]>(new Comparator<int[]>() { public int compare(int[] pair1, int[] pair2) { return pair1[0] != pair2[0] ? pair2[0] - pair1[0] : pair2[1] - pair1[1]; } }); for (int i = 0; i < k; ++i) { pq.offer(new int[]{nums[i], i}); } int[] ans = new int[n - k + 1]; ans[0] = pq.peek()[0]; for (int i = k; i < n; ++i) { pq.offer(new int[]{nums[i], i}); while (pq.peek()[1] <= i - k) { pq.poll(); } ans[i - k + 1] = pq.peek()[0]; } return ans; } }
执行用时: 76 ms 内存消耗: 58.8 MB
题解:
class Solution { public List<Integer> findDisappearedNumbers(int[] nums) { LinkedList<Integer> list = new LinkedList<>(); for(int i = 0; i < nums.length; ++i){ //把相应的nums中的数对应的-1下标位置,乘以-1 表明这个下标也就是这个数出现过了 if(nums[Math.abs(nums[i]) - 1] > 0) nums[Math.abs(nums[i]) - 1] *= -1; } for(int i = 0; i < nums.length; ++i){ if(nums[i] > 0) list.add(i+1); } return list; } }
题解:
将数组中的每个元素插入到哈希表中,如果该元素已经存在于哈希表中,则存在重复元素,返回true。
class Solution { public boolean containsDuplicate(int[] nums) { Set<Integer> set = new HashSet<>(); for (int num : nums) { if (set.contains(num)) return true; set.add(num); } return false; } }
执行用时: 8 ms 内存消耗: 44.2 MB
时间复杂度:O(N)
空间复杂度:O(N)
题解:
创建一个哈希表,对于每一个 x,我们首先查询哈希表中是否存在 target - x,然后将 x 插入到哈希表中,保证不会让 x 和自己匹配。通过哈希表,可以将寻找 target - x 的时间复杂度降低到从 O(N) 降低到 O(1)。
class Solution { public int[] twoSum(int[] nums, int target) { Map<Integer, Integer> map = new HashMap<>(); for(int i = 0; i< nums.length; i++) { if(map.containsKey(target - nums[i])) { return new int[] {map.get(target-nums[i]),i}; } map.put(nums[i], i); } throw new IllegalArgumentException("No two sum solution"); } }
执行用时: 1 ms 内存消耗: 38.6 MB
时间复杂度:O(N)
空间复杂度:O(N)
题解:
首先把所有数字放入一个哈希表,然后不断从哈希表中任取一值并删除其前后的所有连续数字,最后更新目前的最长连续序列长度,重复上述过程,找到所有连续数字序列。
class Solution { public int longestConsecutive(int[] nums) { Set<Integer> hash = new HashSet<Integer>(); for(int x : nums) hash.add(x); //放入hash表中 int res = 0; for(int x : hash) { if(!hash.contains(x-1)) { int y = x; //以当前数x向后枚举 while(hash.contains(y + 1)) y++; res = Math.max(res, y - x + 1); //更新答案 } } return res; } }
执行用时: 14 ms 内存消耗: 53.2 MB
时间复杂度:O(N),N为数组长度,需要遍历数组中的元素,判断hash表中是否存在y的复杂度为O(1);
空间复杂度:O(N)哈希表维护的空间
题解:
直线公式:y = kx + b,一直斜率 k 和一点(x0,y0)即可唯一确认该直线;
对于数组中的每个点,对其他点建立哈希表,统计同一斜率的点一共存在多少,同时考虑斜率不存在的直线。
class Solution { public int maxPoints(int[][] points) { //新建哈希表,key 为斜率,value 为点的个数 HashMap<Double, Integer> hash = new HashMap<>(); 初始化点最大个数 int max_count = 1; //遍历数组中的点 for (int i = 0; i < points.length; ++i) { //遍历 i 之后的点 for (int j = i + 1; j < points.length; ++j) { //横坐标差值和纵坐标差值 double dx = points[i][0] - points[j][0], dy = points[i][1] - points[j][1]; //直线斜率,特别考虑水平直线和竖直直线两种情况,Java除法 0 和 infinity 有正负区别,正负两种斜率属于同一条直线要特别处理 double slope = (points[i][0] == points[j][0] && points[i][1] < points[j][1]) || (points[i][1] == points[j][1] && points[i][0] < points[j][0])? -dy/dx : dy/dx; //先对当前斜率判空,判断哈希表中是否存在该key int count = hash.containsKey(slope) ? hash.get(slope) : 1; //对应的 value 加1 hash.put(slope, count + 1); } //遍历HashMap中的 key for (Double item : hash.keySet()) { max_count = Math.max(max_count, hash.get(item)); } //清除哈希表 hash.clear(); } return max_count; } }
执行用时: 8 ms 内存消耗: 38 MB
时间复杂度:O(n² × logm),其中 n 为点的个数,m 为最大横纵坐标的差值;
空间复杂度:O(n),n 为点的个数,为哈希表占用的空间。
技巧:
(1)HashMap中的 value 自增
int count = hash.containsKey(slope) ? hash.get(slope) : 0; hash.put(slope, count + 1);
(2)Java除法分母为 0 ,分子为正数,结果为 infinity,分子为负数,结果为 -infinity
官方题解:
class Solution { public int maxPoints(int[][] points) { int n = points.length; if (n <= 2) { return n; } int ret = 0; for (int i = 0; i < n; i++) { if (ret >= n - i || ret > n / 2) { break; } Map<Integer, Integer> map = new HashMap<Integer, Integer>(); for (int j = i + 1; j < n; j++) { int x = points[i][0] - points[j][0]; int y = points[i][1] - points[j][1]; if (x == 0) { y = 1; } else if (y == 0) { x = 1; } else { if (y < 0) { x = -x; y = -y; } int gcdXY = gcd(Math.abs(x), Math.abs(y)); x /= gcdXY; y /= gcdXY; } int key = y + x * 20001; map.put(key, map.getOrDefault(key, 0) + 1); } int maxn = 0; for (Map.Entry<Integer, Integer> entry: map.entrySet()) { int num = entry.getValue(); maxn = Math.max(maxn, num + 1); } ret = Math.max(ret, maxn); } return ret; } public int gcd(int a, int b) { return b != 0 ? gcd(b, a % b) : a; } }
执行用时: 10 ms 内存消耗: 37.9 MB
一维前缀和:
一个一维数组
x
x
x 和该数组的一维前缀和数组
y
y
y ,则
x
x
x 和
y
y
y 满足以下关系:
y
0
=
x
0
、
y
1
=
x
0
+
x
1
、
y
2
=
x
0
+
x
1
+
x
2
、
y
n
=
x
0
+
x
1
+
x
2
+
.
.
.
+
x
n
y_0 = x_0、y_1 = x_0 + x_1、y_2 = x_0 + x_1 + x_2、y_n = x_0 + x_1 + x_2 + ... + x_n
y0=x0、y1=x0+x1、y2=x0+x1+x2、yn=x0+x1+x2+...+xn
代码实现:
y
n
=
y
n
−
1
+
x
n
y_n = y_{n-1} + x_n
yn=yn−1+xn
for (int i = 0; i < n; i++) { if (i == 0) y[i] = x[i]; else y[i] = y[i-1] + x[i]; }
二维前缀和(积分图):
b
0
,
0
=
a
0
,
0
、
b
0
,
1
=
a
0
,
0
+
a
0
,
1
、
b
1
,
0
=
a
0
,
0
+
a
1
,
0
、
b
1
,
1
=
a
0
,
0
+
a
0
,
1
+
a
1
,
0
+
a
1
,
1
b_{0,0} = a_{0,0}、b_0,_1 = a_0,_0 + a_0,_1、b_1,_0 = a_0,_0 + a_1,_0、b_1,_1 = a_0,_0 + a_0,_1 + a_1,_0 + a_1,_1
b0,0=a0,0、b0,1=a0,0+a0,1、b1,0=a0,0+a1,0、b1,1=a0,0+a0,1+a1,0+a1,1
示例:
代码实现:
二维前缀和 = 矩阵内值和
b
x
,
y
=
b
x
−
1
,
y
+
b
x
,
y
−
1
−
b
x
−
1
,
y
+
a
x
,
y
b_{x,y} = b_{x-1,y} + b_{x,y-1} - b_{x-1,y} + a_{x,y}
bx,y=bx−1,y+bx,y−1−bx−1,y+ax,y
for (int y=0; y<n; y++) {//n行 for (int x=0; x<m; x++){//m行 if (x==0 && y==0) b[y][x]=a[y][x];//左上角的值 else if (x==0) b[y][x] = b[y-1][x] + a[y][x];//第一列 else if (y==0) b[y][x] = b[y][x-1] + a[y][x];//第一行 else b[y][x] = b[y-1][x] + b[y][x-1] -b[y-1][x-1] + a[y][x]; } }
题解:
sums[i]
:第 0 项到第 i 项的和,即
sums[i] = nums[0] + nums[1] + ... + nums[i]
两个相邻前缀和的差与数组 nums 中某项相等,即
nums[i] = sums[i] - sums[i - 1]
则数组 nums 中 i 到 j 的元素和为
nums[i] + ... + nums[j] = sum[j] - nums[i - 1]
为方便计算,不考虑 i=0 的情况,将数组 sum 长度设为 n + 1,则 sum[i] 表示 0 到 i-1 的前缀和
nums[i] + ... + nums[j] = sum[j + 1] - nums[i]
class NumArray { //前缀和数组 int[] sums; public NumArray(int[] nums) { int n = nums.length; sums = new int[n + 1]; for (int i = 0; i < n; ++i) { sums[i + 1] = sums[i] + nums[i]; } } public int sumRange(int left, int right) { return sums[right + 1] - sums[left]; } }
执行用时: 7 ms 内存消耗: 41.2 MB
时间复杂度:初始化O(n),n是数组 nums 的长度,初始化需要遍历数组计算前缀和,
每次检索O(1),每次得到两个下标的前缀和并计算差值;
空间复杂度:O(n),n 是数组 nums 的长度。
题解:
二维前缀和(积分图)
sums[i][j] = matrix[i-1][j-1] + sums[i-1][j] + sums[i][j-1] - sums[i-1][j-1]
class NumMatrix { int[][] sums; public NumMatrix(int[][] matrix) { int m = matrix.length, n = m > 0 ? matrix[0].length : 0; sums = new int[m + 1][n + 1]; for (int i = 0; i < m; i++) { for (int j = 0; j < n; j++) { sums[i + 1][j + 1] = sums[i][j + 1] + sums[i + 1][j] - sums[i][j] + matrix[i][j]; } } } public int sumRegion(int row1, int col1, int row2, int col2) { return sums[row2 + 1][col2 + 1] - sums[row1][col2 + 1] - sums[row2 + 1][col1] + sums[row1][col1]; } }
执行用时: 137 ms 内存消耗: 63 MB
时间复杂度:
初始化O(mn),m 和 n 为矩阵 matrix 的行数和列数,遍历矩阵计算前缀和;
每次检索O(1);
空间复杂度:O(mn),m 和 n 为矩阵 matrix 的行数和列数,需要创建一个 m+1 行 n +1 列的矩阵。
题解:
前缀和+哈希表
初始化哈希表,key 为前缀和,value 为该前缀和出现的次数;
假设 psum[i] 为 [0,i] 的前缀和,可得:
psum[i] == psum[i-1] + nums[i]
则 [j,i] 区间内数组和为 k 可得:
psum[i] - psum[j-1] == k
class Solution { public int subarraySum(int[] nums, int k) { //count 为前缀和出现次数,psum 为当前前缀和 int count = 0, psum = 0; Map<Integer, Integer> hashmap = new HashMap<>(); hashmap.put(0, 1);//初始化 for (int i : nums) { //遍历数组并获得前缀和 psum += i; count = hashmap.containsKey(psum-k) ? hashmap.get(psum - k) + count : count; //当前前缀和数量增加 1 int cnt = hashmap.containsKey(psum) ? hashmap.get(psum) : 0; hashmap.put(psum, cnt + 1); } return count; } }
执行用时: 21 ms 内存消耗: 40.7 MB
时间复杂度:O(N),n 为数组长度,遍历数组的时间复杂度为 O(n),哈希表删除操作时间复杂度为O(1)
空间复杂度:O(N),n 为数组长度,哈希表最坏情况下每个键值都不同,因此需要和数组长度相同的空间。
操作 | 字符数组 | 字符串 |
---|---|---|
声明 | char[] a | String s |
根据索引访问字符 | a[i] | s.charAt(i) |
获取字符串长度 | a.length | s.length() |
表示方法转换 | a = s.toCharArray(); | s = new String(a); |
题解:
利用数组统计两个字符串中每个数字出现的次数,若频次相同,则说明他们包含的字符完全相同。
class Solution { public boolean isAnagram(String s, String t) { //长度不相同直接返回 false if (s.length() != t.length()) { return false; } //字符串只包含26个字母可以初始化一个只有26个元素的统计频次的数组 int[] counts = new int[26]; //遍历字符串 s for (int i = 0; i < s.length(); ++i) { //记录字符串 s 中字符出现的频次 ++counts[s.charAt(i) - 'a']; //减去字符串 t 中出现该字符的频次 --counts[t.charAt(i) - 'a']; } for (int i = 0; i < 26; ++i) { //还有多余的字母返回 false if (counts[i] != 0) { return false; } } return true; } }
执行用时: 5 ms 内存消耗: 38.7 MB
时间复杂度:O(n),n 为字符串 s 的长度
空间复杂度:O(S),S 为字符集大小,此处为 26
技巧:
字符串-‘a’
的原因,根据ASCII码,26个字母减去小写字母 a 恰好对应数字 0~25
官方题解:
class Solution { public boolean isAnagram(String s, String t) { if (s.length() != t.length()) { return false; } int[] table = new int[26]; for (int i = 0; i < s.length(); i++) { table[s.charAt(i) - 'a']++; } for (int i = 0; i < t.length(); i++) { table[t.charAt(i) - 'a']--; if (table[t.charAt(i) - 'a'] < 0) { return false; } } return true; } }
执行用时: 4 ms 内存消耗: 38.5 MB
题解:
将该问题转化为**记录两个字符串每个位置的第一次出现的位置,如果两个字符串中相同位置的字符与它们第一次出现的位置一样那么两个字符串同构。 **
class Solution { public boolean isIsomorphic(String s, String t) { //在ASCII码范围创建数组,初始化为256 int[] s_first_index = new int[256]; int[] t_first_index = new int[256]; for (int i = 0; i < s.length(); ++i) { if (s_first_index[s.charAt(i)] != t_first_index[t.charAt(i)]) { return false; } s_first_index[s.charAt(i)] = t_first_index[t.charAt(i)] = i + 1; } return true; } }
执行用时: 7 ms 内存消耗: 38.1 MB
时间复杂度:O(n)
空间复杂度:O(S)
技巧:
s_first_index[s.charAt(i)]
charAt()
输出的字符串可按照ASCII码强制转换成 int 类型的数组
官方题解:
class Solution { public boolean isIsomorphic(String s, String t) { Map<Character, Character> s2t = new HashMap<Character, Character>(); Map<Character, Character> t2s = new HashMap<Character, Character>(); int len = s.length(); for (int i = 0; i < len; ++i) { char x = s.charAt(i), y = t.charAt(i); if ((s2t.containsKey(x) && s2t.get(x) != y) || (t2s.containsKey(y) && t2s.get(y) != x)) { return false; } s2t.put(x, y); t2s.put(y, x); } return true; } }
执行用时: 29 ms 内存消耗: 38.1 MB
题解:
从字符串的 i 位置开始,向左和向右延长,判断当前位置 i 为中心的回文字符串存在多少,并返回 count
class Solution { public int countSubstrings(String s) { int count = 0; for (int i = 0; i < s.length(); ++i) { //一个字符开始延长,奇数长度 count += extendSubstrings(s, i, i); //两个字符开始延长,偶数长度 count += extendSubstrings(s, i, i + 1); } return count; } //延长子字符串方法 public int extendSubstrings(String s, int l, int r) { int count = 0; while (l >= 0 && r < s.length() && s.charAt(l) == s.charAt(r)) { --l;//向左延长 ++r;//向右延长 ++count;//回文子字符串数目增加 } return count;//返回当前位置的回文子字符串数目 } }
执行用时: 3 ms 内存消耗: 36.5 M
时间复杂度:O( n 2 n^2 n2),n为字符串长度,for 遍历一次,拓展字符串 while 遍历一次;
空间复杂度:O(1),不适用额外空间。
官方题解:
Manacher算法:在线性时间内求解最长回文子串的算法;
马拉车算法
原理:在所有的相邻字符中间插入 # ,使得无论奇数还是偶数都变成奇数,如 abaa 会被处理成 #a#b#a#a#
class Solution { public int countSubstrings(String s) { int n = s.length(); //StringBuffer类可多次被修改且不产生新未使用对象 StringBuffer t = new StringBuffer("$#"); //遍历字符串 s 并形成新字符串 t for (int i = 0; i < n; ++i) { t.append(s.charAt(i)); t.append('#'); } n = t.length(); t.append('!'); int[] f = new int[n]; int iMax = 0, rMax = 0, ans = 0; for (int i = 1; i < n; ++i) { // 初始化 f[i] f[i] = i <= rMax ? Math.min(rMax - i + 1, f[2 * iMax - i]) : 1; // 中心拓展 while (t.charAt(i + f[i]) == t.charAt(i - f[i])) { ++f[i]; } // 动态维护 iMax 和 rMax if (i + f[i] - 1 > rMax) { iMax = i; rMax = i + f[i] - 1; } // 统计答案, 当前贡献为 (f[i] - 1) / 2 上取整 ans += f[i] / 2; } return ans; } }
执行用时: 2 ms 内存消耗: 36.4 M
时间复杂度:O(n)
空间复杂度:O(n)
题解:
class Solution { public int countBinarySubstrings(String s) { int pre = 0, cur = 1, count = 0; for (int i = 1; i < s.length(); ++i) { if (s.charAt(i) == s.charAt(i-1)) { ++cur; } else { pre = cur; cur = 1; } if (pre >= cur) { ++count; } } return count; } }
执行用时: 10 ms 内存消耗: 38.8 M
官方题解:
class Solution { public int countBinarySubstrings(String s) { int ptr = 0, n = s.length(), last = 0, ans = 0; while (ptr < n) { char c = s.charAt(ptr); int count = 0; while (ptr < n && s.charAt(ptr) == c) { ++ptr; ++count; } ans += Math.min(count, last); last = count; } return ans; } }
执行用时: 8 ms 内存消耗: 39.1 M
时间复杂度:O(n)
空间复杂度:O(1)
题解:
class Solution { public int calculate(String s) { int i = 0; return parseExpr(s, i); } public int parseExpr(String s, int i) { char op = '+'; long left = 0, right = 0; while (i < s.length()) { if (s.charAt(i) != ' ') { long n = parseNum(s, i); switch (op) { case '+' : left += right; right = n; break; case '-' : left += right; right = -n; break; case '*' : right *= n; break; case '/' : right /= n; break; } if (i < s.length()) { op = s.charAt(i); } } ++i; } return (int) (left + right); } public long parseNum(String s, int i) { long n = 0; while (i < s.length() && Character.isDigit(s.charAt(i))) { n = 10 * n + (s.charAt(i++) - '0'); } return n; } }
执行用时: 10 ms 内存消耗: 38.5 M
官方题解:
class Solution { public int calculate(String s) { Deque<Integer> stack = new LinkedList<Integer>(); char preSign = '+'; int num = 0; int n = s.length(); for (int i = 0; i < n; ++i) { if (Character.isDigit(s.charAt(i))) { num = num * 10 + s.charAt(i) - '0'; } if (!Character.isDigit(s.charAt(i)) && s.charAt(i) != ' ' || i == n - 1) { switch (preSign) { case '+': stack.push(num); break; case '-': stack.push(-num); break; case '*': stack.push(stack.pop() * num); break; default: stack.push(stack.pop() / num); } preSign = s.charAt(i); num = 0; } } int ans = 0; while (!stack.isEmpty()) { ans += stack.pop(); } return ans; } }
执行用时: 13 ms 内存消耗: 41.6 M
题解:
Knuth-Morris-Patt,KMP算法
class Solution { public int strStr(String haystack, String needle) { int n = haystack.length(), m = needle.length(); if (m == 0) { return 0; } int[] pi = new int[m]; for (int i = 1, j = 0; i < m; i++) { while (j > 0 && needle.charAt(i) != needle.charAt(j)) { j = pi[j - 1]; } if (needle.charAt(i) == needle.charAt(j)) { j++; } pi[i] = j; } for (int i = 0, j = 0; i < n; i++) { while (j > 0 && haystack.charAt(i) != needle.charAt(j)) { j = pi[j - 1]; } if (haystack.charAt(i) == needle.charAt(j)) { j++; } if (j == m) { return i - m + 1; } } return -1; } }
执行用时: 5 ms 内存消耗: 38.3 M
时间复杂度:O(m+n),m 是字符串 needle 的长度,n 是字符串 haystack 的长度,至多需要分别遍历两个字符串一次;
空间复杂度:O(m),m 是字符串 needle 的长度,只需要保存字符串 needle 的前缀函数。
链表:由数据和指针构成,链表的指针指向下一个节点。
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; } }
Java链表语法操作
题解:
递归解法
class Solution { public ListNode reverseList(ListNode head) { if (head == null || head.next == null) { return head; } ListNode newHead = reverseList(head.next); head.next.next = head; head.next = null; return newHead; } }
执行用时: 0 ms 内存消耗: 38.4 M
题解:
非递归解法
class Solution { public ListNode reverseList(ListNode head) { ListNode prev = null; ListNode curr = head; while (curr != null) { ListNode next = curr.next; curr.next = prev; prev = curr; curr = next; } return prev; } }
执行用时: 0 ms 内存消耗: 38.2 M
题解:
非递归
class Solution { public ListNode mergeTwoLists(ListNode l1, ListNode l2) { ListNode prehead = new ListNode(-1); ListNode prev = prehead; while (l1 != null && l2 != null) { if (l1.val <= l2.val) { prev.next = l1; l1 = l1.next; } else { prev.next = l2; l2 = l2.next; } prev = prev.next; } prev.next = l1 == null ? l2 : l1; return prehead.next; } }
执行用时: 0 ms 内存消耗: 38.2 M
题解:
递归
class Solution { public ListNode mergeTwoLists(ListNode l1, ListNode l2) { if (l2 == null) { return l1; } if (l1 == null) { return l2; } if (l1.val > l2.val) { l2.next = mergeTwoLists(l1, l2.next); return l2; } l1.next = mergeTwoLists(l1.next, l2); return l1; } }
执行用时: 0 ms 内存消耗: 37.9 M
题解:
非递归
class Solution { public ListNode swapPairs(ListNode head) { ListNode pre = new ListNode(0); pre.next = head; ListNode temp = pre; while(temp.next != null && temp.next.next != null) { ListNode start = temp.next; ListNode end = temp.next.next; temp.next = end; start.next = end.next; end.next = start; temp = start; } return pre.next; } }
执行用时: 0 ms 内存消耗: 36 M
题解:
递归
class Solution { public ListNode swapPairs(ListNode head) { if(head == null || head.next == null){ return head; } ListNode next = head.next; head.next = swapPairs(next.next); next.next = head; return next; } }
执行用时: 0 ms 内存消耗: 36.1 M
题解:
public class Solution { public ListNode getIntersectionNode(ListNode headA, ListNode headB) { if (headA == null || headB == null) return null; ListNode l1 = headA, l2 = headB; while (l1 != l2) { l1 = l1 != null ? l1.next : headB; l2 = l2 != null ? l2.next : headA; } return l1; } }
执行用时: 1 ms 内存消耗: 41.3 M
题解:
class Solution { public boolean isPalindrome(ListNode head) { if(head == null || head.next == null) { return true; } ListNode slow = head, fast = head; ListNode pre = head, prepre = null; while(fast != null && fast.next != null) { pre = slow; slow = slow.next; fast = fast.next.next; pre.next = prepre; prepre = pre; } if(fast != null) { slow = slow.next; } while(pre != null && slow != null) { if(pre.val != slow.val) { return false; } pre = pre.next; slow = slow.next; } return true; } }
执行用时: 3 ms 内存消耗: 48 M
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 int maxDepth(TreeNode root) { return root != null ? 1 + Math.max(maxDepth(root.left), maxDepth(root.right)) : 0; } }
执行用时: 3 ms 内存消耗: 48 M
题解:
class Solution { public boolean isBalanced(TreeNode root) { return helper(root) != -1; } public int helper(TreeNode root) { if (root == null) { return 0; } int left = helper(root.left), right = helper(root.right); if (left == -1 || right == -1 || Math.abs(left - right) > 1) { return -1; } return 1 + Math.max(left, right); } }
执行用时: 1 ms 内存消耗: 38.2 M
题解:
class Solution { int diameter; public int diameterOfBinaryTree(TreeNode root) { diameter = 0; helper(root); return diameter; } public int helper(TreeNode node) { if (node == null) return 0; int left = helper(node.left), right = helper(node.right); diameter = Math.max(diameter, left + right); return Math.max(left, right) + 1; } }
执行用时: 0 ms 内存消耗: 37.9 M
题解:
class Solution { public int pathSum(TreeNode root, int targetSum) { return root != null ? pathSumStartWithRoot(root, targetSum) + pathSum(root.left, targetSum) + pathSum(root.right, targetSum) : 0; } public int pathSumStartWithRoot(TreeNode root, int targetSum) { if (root == null) return 0; int count = root.val == targetSum ? 1 : 0; count += pathSumStartWithRoot(root.left, targetSum - root.val); count += pathSumStartWithRoot(root.right, targetSum - root.val); return count; } }
执行用时: 26 ms 内存消耗: 38.4 M
题解:
递归
class Solution { public boolean isSymmetric(TreeNode root) { return root != null ? isSymmetric(root.left, root.right) : true; } public boolean isSymmetric(TreeNode left, TreeNode right) { if (left == null && right == null) return true; if (left == null || right == null) return false; if (left.val != right.val) return false; return isSymmetric(left.left, right.right) && isSymmetric(left.right, right.left); } }
执行用时: 0 ms 内存消耗: 36.3 M
官方题解:
迭代
class Solution { public boolean isSymmetric(TreeNode root) { return check(root, root); } public boolean check(TreeNode u, TreeNode v) { Queue<TreeNode> q = new LinkedList<TreeNode>(); q.offer(u); q.offer(v); while (!q.isEmpty()) { u = q.poll(); v = q.poll(); if (u == null && v == null) { continue; } if ((u == null || v == null) || (u.val != v.val)) { return false; } q.offer(u.left); q.offer(v.right); q.offer(u.right); q.offer(v.left); } return true; } }
执行用时: 1 ms 内存消耗: 37.8 M
题解:
class Solution { //map存储要删的节点的值 private boolean[] map = new boolean[1001]; //ans为要输出的结果 private List<TreeNode> ans = new ArrayList<>(); public List<TreeNode> delNodes(TreeNode root, int[] to_delete) { int n = to_delete.length; for (int i = 0; i < n; i++) { map[to_delete[i]] = true; } //先看看根结点要不要删,不删的话就加进去 if (!map[root.val]) { ans.add(root); search(root, true); } else { search(root, false); } return ans; } //子方法输入参数的布尔值为当前节点要不要删,即是否还存在? //当前节点是否存在决定着接下来的左右节点是否要加入结果集 public void search(TreeNode root, boolean exist) { if (root == null) return; //如果当前节点不存在了,且左节点不要删,就把左节点加入集合 //右节点同理 if (!exist) { if (root.left != null && !map[root.left.val]) { ans.add(root.left); } if (root.right != null && !map[root.right.val]) { ans.add(root.right); } } //不管当前节点还是否存在,如果左节点要删则在深入下一层时传false,表示这个节点已经不存在了 //这里的不存在并不代表已经删除,因为删除操作即断开二叉链表的操作是在递归完这个节点的后续层之后的 //这里的不存在相当于形存神灭的状态,物理上还存在,但精神上已经死亡即早晚要删先留你个躯壳 //右节点同理 if (root.left != null && map[root.left.val]) { search(root.left, false); root.left = null; } else { search(root.left, true); } if (root.right != null && map[root.right.val]) { search(root.right, false); root.right = null; } else { search(root.right, true); } } }
执行用时: 1 ms 内存消耗: 38.7 M
概念:使用 BFS 进行层序遍历;
不需要使用两个队列储存当前层和下一层的节点:当前队列中的节点数 = 当前层的节点数。
题解:
广度优先搜索:从根节点开始搜索,每一轮遍历同一层的全部节点,计算该层的节点数以及该层的节点值之和,然后计算该层的平均值。
class Solution { public List<Double> averageOfLevels(TreeNode root) { //新建返回值链表 List<Double> ans = new LinkedList<>(); //如果根节点为空直接返回 if (root == null) return ans; //创建队列保存当前层的节点 Queue<TreeNode> q = new LinkedList<>(); //初始加入根节点 q.offer(root); while (!q.isEmpty()) { int count = q.size(); double sum = 0; for (int i = 0; i < count; ++i) { TreeNode node = q.poll(); sum += node.val; if (node.left != null) q.offer(node.left); if (node.right != null) q.offer(node.right); } ans.add(sum / count); } return ans; } }
执行用时: 2 ms 内存消耗: 40.3 M
时间复杂度:O(n)
空间复杂度:O(n)
官方题解:
深度优先搜索
class Solution { public List<Double> averageOfLevels(TreeNode root) { List<Integer> counts = new ArrayList<Integer>(); List<Double> sums = new ArrayList<Double>(); dfs(root, 0, counts, sums); List<Double> averages = new ArrayList<Double>(); int size = sums.size(); for (int i = 0; i < size; i++) { averages.add(sums.get(i) / counts.get(i)); } return averages; } public void dfs(TreeNode root, int level, List<Integer> counts, List<Double> sums) { if (root == null) { return; } if (level < sums.size()) { sums.set(level, sums.get(level) + root.val); counts.set(level, counts.get(level) + 1); } else { sums.add(1.0 * root.val); counts.add(1); } dfs(root.left, level + 1, counts, sums); dfs(root.right, level + 1, counts, sums); } }
执行用时: 1 ms 内存消耗: 40.4 M
时间复杂度:O(n)
空间复杂度:O(n)
深度优先搜索的三种遍历方式:
①前序遍历:先遍历父节点,再遍历左节点,最后遍历右节点,遍历顺序为
1→2→4→5→3→6
void preorder(TreeNode root) { visit(root); preorder(root.left); preorder(root.right); }
②中序遍历:先遍历左节点,再遍历父节点,最后遍历左节点,遍历顺序为
4→2→5→1→3→6
void inorder(TreeNode root) { inorder(root.left); visit(root); inorder(root.right); }
③后序遍历:先遍历左节点,再遍历右节点,最后遍历父节点,遍历顺序为
4→5→2→6→3→1
void postorder(TreeNode root) { postorder(root.left); postorder(root.right); visit(root); }
参考下面文章:
一文学会二叉树前中后序遍历
题解:
哈希表把中序遍历数组的每个元素的值和下标存起来,这样寻找根节点的位置就可以直接得到了。
class Solution { public TreeNode buildTree(int[] preorder, int[] inorder) { //哈希表储存中序遍历数组的每个元素的值和下标 HashMap<Integer, Integer> map = new HashMap<>(); for (int i = 0; i < inorder.length; i++) { map.put(inorder[i], i); } //返回辅助函数的结果 return buildTreeHelper(preorder, 0, preorder.length, inorder, 0, inorder.length, map); } //辅助函数 private TreeNode buildTreeHelper(int[] preorder, int p_start, int p_end, int[] inorder, int i_start, int i_end, HashMap<Integer, Integer> map) { //开始结束位置相等,preorder为空,直接返回null if (p_start == p_end) { return null; } int root_val = preorder[p_start]; TreeNode root = new TreeNode(root_val); int i_root_index = map.get(root_val); int leftNum = i_root_index - i_start; //遍历构建左子树 root.left = buildTreeHelper(preorder, p_start + 1, p_start + leftNum + 1, inorder, i_start, i_root_index, map); //遍历构建右子树 root.right = buildTreeHelper(preorder, p_start + leftNum + 1, p_end, inorder, i_root_index + 1, i_end, map); //返回根节点 return root; } }
执行用时: 1 ms 内存消耗: 38.3 M
时间复杂度:O(n),n 为树的节点数;
空间复杂度:O(n),除去返回的答案需要的 O(n) 空间之外,我们还需要使用 O(n)O(n) 的空间存储哈希映射,以及 O(h)(其中 h 是树的高度)的空间表示递归时栈空间。这里 h<n,所以总空间复杂度为 O(n)。
题解:
迭代
递归隐式地维护一个栈,迭代显示地维护一个栈。
class Solution { public List<Integer> preorderTraversal(TreeNode root) { //新建返回链表 List<Integer> res = new ArrayList<Integer>(); if (root == null) { return res; } //创建队列储存链表节点 Deque<TreeNode> stack = new LinkedList<TreeNode>(); //初始化 node 为根节点 TreeNode node = root; //判断终止条件 while (!stack.isEmpty() || node != null) { while (node != null) { //根节点值加入返回结果 res.add(node.val); stack.push(node); //遍历左子节点 node = node.left; } node = stack.pop(); node = node.right; } return res; } }
执行用时: 0 ms 内存消耗: 36.7 M
时间复杂度:O(n), n 为二叉树节点数,每个节点被遍历一次;
空间复杂度:O(n),空间为递归中栈的开销,平均为O(logn),最坏情况为O(n)
题解:
递归
class Solution { public List<Integer> preorderTraversal(TreeNode root) { List<Integer> res = new ArrayList<Integer>(); preorder(root, res); return res; } public void preorder(TreeNode root, List<Integer> res) { //递归终止条件,根节点为空 if (root == null) { return; } //当前根节点值加入返回结果中 res.add(root.val); //遍历左子树 preorder(root.left, res); //遍历右子树 preorder(root.right, res); } }
执行用时: 0 ms 内存消耗: 36.8 M
时间复杂度:O(n), n 为二叉树节点数,每个节点被遍历一次;
空间复杂度:O(n),空间为递归中栈的开销,平均为O(logn),最坏情况为O(n)
题解:
Morris遍历:
Morris遍历可以将非递归遍历中的空间复杂度降为O(1)。从而实现时间复杂度为O(N),而空间复杂度为O(1)的精妙算法。
实质:
建立一种机制,对于没有左子树的节点只到达一次,对于有左子树的节点会到达两次。
实现:
1、记作当前节点为cur。
2、如果 cur 无左子树,cur 向右移动(cur=cur.right)
3、如果 cur 有左子树,找到 cur 左子树上最右的节点,记为 mostright
3.1、如果 mostright 的 right 指针指向空,让其指向 cur,cur 向左移动(cur=cur.left)
3.2、如果 mostright 的 right 指针指向 cur,让其指向空,cur 向右移动(cur=cur.right)
新加的指向关系,就是找到根节点左子树的最右子树,然后将最右子树的right指向根节点。
class Solution { public List<Integer> preorderTraversal(TreeNode root) { List<Integer> res = new ArrayList<Integer>(); if (root == null) { return res; } TreeNode p1 = root, p2 = null; while (p1 != null) { p2 = p1.left; if (p2 != null) { while (p2.right != null && p2.right != p1) { p2 = p2.right; } if (p2.right == null) { res.add(p1.val); p2.right = p1; p1 = p1.left; //进入下一次迭代 continue; } else { p2.right = null; } } else { res.add(p1.val); } p1 = p1.right; } return res; } }
执行用时: 0 ms 内存消耗: 36.4 M
时间复杂度:O(n)
空间复杂度:O(1)
continue 与 break 语句的区别在于:continue 并不是中断循环语句,而是中止当前迭代的循环,进入下一次的迭代。
概念:二叉查找树(Binary Search Tree),(又:二叉搜索树,二叉排序树)它或者是一棵空树,或者是具有下列性质的二叉树: 若它的左子树不空,则左子树上所有结点的值均小于它的根结点的值; 若它的右子树不空,则右子树上所有结点的值均大于它的根结点的值; 它的左、右子树也分别为二叉排序树。二叉搜索树作为一种经典的数据结构,它既有链表的快速插入与删除操作的特点,又有数组快速查找的优势;所以应用十分广泛,例如在文件系统和数据库系统一般会采用这种数据结构进行高效率的排序与检索操作。
题解:
深度优先搜索+递归
class Solution { public void recoverTree(TreeNode root) { List<TreeNode> list = new ArrayList<TreeNode>(); dfs(root,list); TreeNode x = null; TreeNode y = null; //扫面遍历的结果,找出可能存在错误交换的节点x和y for(int i=0;i<list.size()-1;++i) { if(list.get(i).val>list.get(i+1).val) { y = list.get(i+1); if(x==null) { x = list.get(i); } } } //如果x和y不为空,则交换这两个节点值,恢复二叉搜索树 if(x!=null && y!=null) { int tmp = x.val; x.val = y.val; y.val = tmp; } } //中序遍历二叉树,并将遍历的结果保存到list中 private void dfs(TreeNode node,List<TreeNode> list) { if(node==null) { return; } dfs(node.left,list); list.add(node); dfs(node.right,list); } }
执行用时: 2 ms 内存消耗: 38.8 M
时间复杂度:O(n)
空间复杂度:O(n)
题解:
Morris算法
新加的指向关系,就是找到根节点左子树的最右子树,然后将最右子树的right指向根节点。
class Solution { public void recoverTree(TreeNode root) { //记录错误的两个值 TreeNode x = null, y = null; //记录中序遍历当前节点的前驱 TreeNode pre = null; //用来完成Morris连接的寻找前驱的指针 TreeNode predecessor = null; while(root != null) { if(root.left != null) {//左子树不为空,1、链接root节点的前驱,他的前驱还没访问,所以root不能现在访问,继续访问root左子树 2、root节点访问,并且断开root节点的前驱连接,然后访问root的右子树 predecessor = root.left; while(predecessor.right != null && predecessor.right != root) { predecessor = predecessor.right; } if(predecessor.right == root) {//说明了1已经执行过了,执行2 //访问 if(pre != null && pre.val > root.val) { if(x == null) x = pre; y = root; } //更新前驱,为下一个节点做准备 pre = root; //断开前驱连接 predecessor.right = null; //访问root右子树 root = root.right; }else {//predecessor.right != root,说明了还没执行过1 predecessor.right = root; root = root.left; } }else {//root.left == null,root不需要链接节点的前驱(他的前驱其实就是pre(第一个节点pre为null),且已经被访问过了),那么直接访问root //访问 if(pre != null && pre.val > root.val) { if(x == null) x = pre; y = root; } //更新前驱,为下一个节点做准备 pre = root; //访问root的右子树 root = root.right; } } swap(x, y); } public void swap(TreeNode x, TreeNode y) { int tmp = x.val; x.val = y.val; y.val = tmp; } }
执行用时: 2 ms 内存消耗: 38.7 M
时间复杂度: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 TreeNode trimBST(TreeNode root, int low, int high) { if (root == null) return root; if (root.val > high) return trimBST(root.left, low, high); if (root.val < low) return trimBST(root.right, low, high); root.left = trimBST(root.left, low, high); root.right = trimBST(root.right, low, high); return root; } }
执行用时: 0 ms 内存消耗: 38 M
题解:
class Trie { class TrieNode { boolean end; TrieNode[] tns = new TrieNode[26]; } TrieNode root; public Trie() { root = new TrieNode(); } public void insert(String s) { TrieNode p = root; for(int i = 0; i < s.length(); i++) { int u = s.charAt(i) - 'a'; if (p.tns[u] == null) p.tns[u] = new TrieNode(); p = p.tns[u]; } p.end = true; } public boolean search(String s) { TrieNode p = root; for(int i = 0; i < s.length(); i++) { int u = s.charAt(i) - 'a'; if (p.tns[u] == null) return false; p = p.tns[u]; } return p.end; } public boolean startsWith(String s) { TrieNode p = root; for(int i = 0; i < s.length(); i++) { int u = s.charAt(i) - 'a'; if (p.tns[u] == null) return false; p = p.tns[u]; } return true; } }
执行用时: 34 ms 内存消耗: 47.4 M
图基本概念:图(Graph)是一种复杂的非线性结构,在图结构中,每个元素都可以有零个或多个前驱,也可以有零个或多个后继,也就是说,元素之间的关系是任意的。
图的表示方法:
有 n 个节点, m 条边的图,
①邻接矩阵(adjacency matrix)表示法:
二分图又称作二部图,是图论中的一种特殊模型。 设 G=(V,E) 是一个无向图,如果顶点 V 可分割为两个互不相交的子集 (A,B),并且图中的每条边(i,j)所关联的两个顶点 i 和 j 分别属于这两个不同的顶点集(i in A,j in B),则称图G为一个二分图。
二分图算法也成为染色法,是广度优先搜索的一种。如果可以用两种颜色对图中的节点进行着色,并且相邻节点颜色不同,则图为二分图。
题解:
class Solution { public boolean isBipartite(int[][] graph) { int n = graph.length; int[] color = new int[n]; Queue<Integer> q = new LinkedList<>(); for (int i = 0; i < n; ++i) { if (color[i] != 0) { continue; } q.offer(i); color[i] = 1; while (!q.isEmpty()) { int node = q.poll(); for (int j : graph[node]) { if (color[j] == 0) { q.offer(j); color[j] = -color[node]; } else if (color[node] == color[j]) { return false; } } } } return true; } }
执行用时: 1 ms 内存消耗: 39.1 M
题解:
class Solution { public int[] findOrder(int numCourses, int[][] prerequisites) { if (numCourses <= 0) { return new int[0]; } HashSet<Integer>[] adj = new HashSet[numCourses]; for (int i = 0; i < numCourses; i++) { adj[i] = new HashSet<>(); } // [1,0] 0 -> 1 int[] inDegree = new int[numCourses]; for (int[] p : prerequisites) { adj[p[1]].add(p[0]); inDegree[p[0]]++; } Queue<Integer> queue = new LinkedList<>(); for (int i = 0; i < numCourses; i++) { if (inDegree[i] == 0) { queue.offer(i); } } int[] res = new int[numCourses]; // 当前结果集列表里的元素个数,正好可以作为下标 int count = 0; while (!queue.isEmpty()) { // 当前入度为 0 的结点 Integer head = queue.poll(); res[count] = head; count++; Set<Integer> successors = adj[head]; for (Integer nextCourse : successors) { inDegree[nextCourse]--; // 马上检测该结点的入度是否为 0,如果为 0,马上加入队列 if (inDegree[nextCourse] == 0) { queue.offer(nextCourse); } } } // 如果结果集中的数量不等于结点的数量,就不能完成课程任务,这一点是拓扑排序的结论 if (count == numCourses) { return res; } return new int[0]; } }
执行用时: 7 ms 内存消耗: 39.6 M
题解:
class Solution { public int[] findRedundantConnection(int[][] edges) { int nodesCount = edges.length; int[] parent = new int[nodesCount + 1]; for (int i = 1; i <= nodesCount; i++) { parent[i] = i; } for (int i = 0; i < nodesCount; i++) { int[] edge = edges[i]; int node1 = edge[0], node2 = edge[1]; if (find(parent, node1) != find(parent, node2)) { union(parent, node1, node2); } else { return edge; } } return new int[0]; } public void union(int[] parent, int index1, int index2) { parent[find(parent, index1)] = find(parent, index2); } public int find(int[] parent, int index) { if (parent[index] != index) { parent[index] = find(parent, parent[index]); } return parent[index]; } }
执行用时: 1 ms 内存消耗: 39 M
题解:
class LRUCache extends LinkedHashMap<Integer, Integer>{ private int capacity; public LRUCache(int capacity) { super(capacity, 0.75F, true); this.capacity = capacity; } public int get(int key) { return super.getOrDefault(key, -1); } // 这个可不写 public void put(int key, int value) { super.put(key, value); } @Override protected boolean removeEldestEntry(Map.Entry<Integer, Integer> eldest) { return size() > capacity; } } /** * Your LRUCache object will be instantiated and called as such: * LRUCache obj = new LRUCache(capacity); * int param_1 = obj.get(key); * obj.put(key,value); */
执行用时: 42 ms 内存消耗: 107.2 M