核心思路是我们 new 一个新的数组preSum出来,preSum[i]记录nums[0…i-1]的累加和,看图 10 = 3 + 5 + 2:
看这个preSum数组,如果我想求索引区间[1, 4]内的所有元素之和,就可以通过preSum[5] - preSum[1]得出。
这样,sumRange函数仅仅需要做一次减法运算,避免了每次进行 for 循环调用,最坏时间复杂度为常数O(1)。
class NumArray {
int[] preSum;
public NumArray(int[] nums) {
preSum = new int[nums.length +1];
for(int i = 1;i < preSum.length;i++){
preSum[i] = preSum[i-1]+nums[i-1];
}
}
public int sumRange(int left, int right) {
return preSum[right+1]-preSum[left];
}
}
如果我想计算红色的这个子矩阵的元素之和,可以用绿色矩阵减去蓝色矩阵减去橙色矩阵最后加上粉色矩阵,而绿蓝橙粉这四个矩阵有一个共同的特点,就是左上角就是(0, 0)原点。
那么我们可以维护一个二维preSum数组,专门记录以原点为顶点的矩阵的元素之和,就可以用几次加减运算算出任何一个子矩阵的元素和。
class NumMatrix {
int[][] preSum;
public NumMatrix(int[][] matrix) {
int m = matrix.length, n = matrix[0].length;
preSum = new int[m+1][n+1];
for(int i = 1;i <= m;i++){
for(int j = 1;j <= n;j++){
preSum[i][j] = preSum[i-1][j]+preSum[i][j-1]+matrix[i-1][j-1]-preSum[i-1][j-1];
}
}
}
public int sumRegion(int row1, int col1, int row2, int col2) {
return preSum[row2+1][col2+1]-preSum[row2+1][col1]-preSum[row1][col2+1]+preSum[row1][col1];
}
}
nt subarraySum(int[] nums, int k) {
int n = nums.length;
// 构造前缀和
int[] preSum = new int[n + 1];
preSum[0] = 0;
for (int i = 0; i < n; i++)
preSum[i + 1] = preSum[i] + nums[i];
int res = 0;
// 穷举所有子数组
for (int i = 1; i <= n; i++)
for (int j = 0; j < i; j++)
// 子数组 nums[j..i-1] 的元素和
if (preSum[i] - preSum[j] == k)
res++;
return res;
}
这个解法的时间复杂度O(N^2)空间复杂度O(N),并不是最优的解法。不过通过这个解法理解了前缀和数组的工作原理之后,可以使用一些巧妙的办法把时间复杂度进一步降低。
第二层 for 循环在干嘛呢?翻译一下就是,在计算,有几个j能够使得preSum[i]和preSum[j]的差为k。毎找到一个这样的j,就把结果加一。
我直接记录下有几个preSum[j]和preSum[i] - k相等,直接更新结果,就避免了内层的 for 循环。我们可以用哈希表,在记录前缀和的同时记录该前缀和出现的次数。
class Solution {
public int subarraySum(int[] nums, int k) {
Map<Integer, Integer> preSum = new HashMap<>();
int res = 0, sum_i = 0;
preSum.put(0, 1);
for(int i = 0;i < nums.length;i++){
sum_i +=nums[i];
int sum_j = sum_i-k;
if(preSum.containsKey(sum_j)){
res+=preSum.get(sum_j);
}
preSum.put(sum_i, preSum.getOrDefault(sum_i, 0)+1);
}
return res;
}
}
差分数组的主要适用场景是频繁对原始数组的某个区间的元素进行增减。
我给你输入一个数组nums,然后又要求给区间nums[2…6]全部加 1,再给nums[3…9]全部减 3,再给nums[0…4]全部加 2,再给…
类似前缀和技巧构造的prefix数组,我们先对nums数组构造一个diff差分数组,diff[i]就是nums[i]和nums[i-1]之差:
int[] res = new int[diff.length];
// 根据差分数组构造结果数组
res[0] = diff[0];
for (int i = 1; i < diff.length; i++) {
res[i] = res[i - 1] + diff[i];
}
这样构造差分数组diff,就可以快速进行区间增减的操作,如果你想对区间nums[i…j]的元素全部加 3,那么只需要让diff[i] += 3,然后再让diff[j+1] -= 3即可。
只要花费 O(1) 的时间修改diff数组,就相当于给nums的整个区间做了修改。多次修改diff,然后通过diff数组反推,即可得到nums修改后的结果。
// 差分数组工具类
class Difference {
// 差分数组
private int[] diff;
/* 输入一个初始数组,区间操作将在这个数组上进行 */
public Difference(int[] nums) {
assert nums.length > 0;
diff = new int[nums.length];
// 根据初始数组构造差分数组
diff[0] = nums[0];
for (int i = 1; i < nums.length; i++) {
diff[i] = nums[i] - nums[i - 1];
}
}
/* 给闭区间 [i,j] 增加 val(可以是负数)*/
public void increment(int i, int j, int val) {
diff[i] += val;
if (j + 1 < diff.length) {
diff[j + 1] -= val;
}
}
/* 返回结果数组 */
public int[] result() {
int[] res = new int[diff.length];
// 根据差分数组构造结果数组
res[0] = diff[0];
for (int i = 1; i < diff.length; i++) {
res[i] = res[i - 1] + diff[i];
}
return res;
}
}
public void increment(int i, int j, int val) {
diff[i] += val;
if (j + 1 < diff.length) {
diff[j + 1] -= val;
}
}
当j+1 >= diff.length时,说明是对nums[i]及以后的整个数组都进行修改,那么就不需要再给diff数组减val了。
核心思路是我们 new 一个新的数组preSum出来,preSum[i]记录nums[0…i-1]的累加和,看图 10 = 3 + 5 + 2:
看这个preSum数组,如果我想求索引区间[1, 4]内的所有元素之和,就可以通过preSum[5] - preSum[1]得出。
这样,sumRange函数仅仅需要做一次减法运算,避免了每次进行 for 循环调用,最坏时间复杂度为常数O(1)。
int[] getModifiedArray(int length, int[][] updates) {
// nums 初始化为全 0
int[] nums = new int[length];
// 构造差分解法
Difference df = new Difference(nums);
for (int[] update : updates) {
int i = update[0];
int j = update[1];
int val = update[2];
df.increment(i, j, val);
}
return df.result();
}
给你输入一个长度为n的数组nums,其中所有元素都是 0。再给你输入一个bookings,里面是若干三元组(i,j,k),每个三元组的含义就是要求你给nums数组的闭区间[i-1,j-1]中所有元素都加上k。请你返回最后的nums数组是多少?
class Solution {
public int[] corpFlightBookings(int[][] bookings, int n) {
int[] nums = new int[n];
Difference diff = new Difference(nums);
for(int[] arr:bookings){
int first = arr[0]-1;
int last = arr[1]-1;
int val = arr[2];
diff.increment(first, last, val);
}
return diff.result();
}
}
class Difference{
int[] diff;
public Difference(int[] nums){
diff = new int[nums.length];
diff[0] = nums[0];
for(int i = 1; i < nums.length;i++){
diff[i] = nums[i]-nums[i-1];
}
}
public void increment(int i, int j, int val){
diff[i]+=val;
if(j+1<diff.length){
diff[j+1]-=val;
}
}
public int[] result(){
int[] res = new int[diff.length];
res[0] = diff[0];
for(int i = 1; i < diff.length;i++){
res[i] = diff[i]+res[i-1];
}
return res;
}
}
trips[i]代表着一组区间操作,旅客的上车和下车就相当于数组的区间加减;只要结果数组中的元素都小于capacity,就说明可以不超载运输所有旅客。
class Solution {
public boolean carPooling(int[][] trips, int capacity) {
int[] nums = new int[1001];
Difference diff = new Difference(nums);
for(int[] t:trips){
int val = t[0];
int from = t[1];
int to = t[2]-1;
diff.increment(from, to, val);
}
int[] res = diff.result();
for(int i = 0;i < res.length;i++){
if(res[i] > capacity){
return false;
}
}
return true;
}
}
class Difference{
int[] diff;
Difference(int[] nums){
diff = new int[nums.length];
diff[0] = nums[0];
for(int i = 1;i < diff.length;i++){
diff[i] = nums[i]-nums[i-1];
}
}
public void increment(int i,int j, int val){
diff[i]+=val;
if(j+1 < diff.length){
diff[j+1]-=val;
}
}
public int[] result(){
int[] res = new int[diff.length];
res[0] = diff[0];
for(int i = 1; i < diff.length;i++){
res[i] = diff[i]+res[i-1];
}
return res;
}
}
这个算法的逻辑类似于「拉拉链」,l1, l2类似于拉链两侧的锯齿,指针p就好像拉链的拉索,将两个有序链表合并。
代码中还用到一个链表的算法题中是很常见的「虚拟头节点」技巧,也就是dummy节点。
class Solution {
public ListNode mergeTwoLists(ListNode list1, ListNode list2) {
if(list1 == null){
return list2;
}
if(list2 == null){
return list1;
}
ListNode pHead = new ListNode(-1);
ListNode p = pHead;
while(list1 != null && list2 != null){
if(list1.val <= list2.val){
pHead.next = list1;
list1 = list1.next;
}else{
pHead.next = list2;
list2 = list2.next;
}
pHead = pHead.next;
}
if(list1 != null){
pHead.next = list1;
}
if(list2 != null){
pHead.next = list2;
}
return p.next;
}
}
优先队列pq中的元素个数最多是k,所以一次poll或者add方法的时间复杂度是O(logk);所有的链表节点都会被加入和弹出pq,所以算法整体的时间复杂度是O(Nlogk),其中k是链表的条数,N是这些链表的节点总数。
class Solution {
public ListNode mergeKLists(ListNode[] lists) {
PriorityQueue<ListNode> queue = new PriorityQueue<>((a, b)->(a.val-b.val));
for(ListNode list :lists){
if(list != null){
queue.add(list);
}
}
ListNode pHead = new ListNode(-1);
ListNode p = pHead;
while(!queue.isEmpty()){
ListNode node = queue.poll();
p.next = node;
if(node.next != null){
queue.add(node.next);
}
p = p.next;
}
return pHead.next;
}
}
要想删除倒数第N个节点,首先你要找到这个节点,下面双指针是非常经典的思路:
// 返回链表的倒数第 k 个节点
ListNode findFromEnd(ListNode head, int k) {
ListNode p1 = head;
// p1 先走 k 步
for (int i = 0; i < k; i++) {
p1 = p1.next;
}
ListNode p2 = head;
// p1 和 p2 同时走 n - k 步
while (p1 != null) {
p2 = p2.next;
p1 = p1.next;
}
// p2 现在指向第 n - k 个节点
return p2;
}
class Solution {
public ListNode removeNthFromEnd(ListNode head, int n) {
ListNode dummy = new ListNode(-1);
dummy.next = head;
ListNode p1 = dummy, p2 = head;
while(n > 0 && p2 != null){
n--;
p2 = p2.next;
}
while(p1 != null && p2 != null){
p1 = p1.next;
p2 = p2.next;
}
p1.next = p1.next.next;
return dummy.next;
}
}
两个指针slow和fast分别指向链表头结点head。
每当慢指针slow前进一步,快指针fast就前进两步,这样,当fast走到链表末尾时,slow就指向了链表中点。
class Solution {
public ListNode middleNode(ListNode head) {
ListNode slow = head, fast = head;
while(fast != null && fast.next != null){
slow = slow.next;
fast = fast.next.next;
}
return slow;
}
}
每当慢指针slow前进一步,快指针fast就前进两步。
如果fast最终遇到空指针,说明链表中没有环;如果fast最终和slow相遇,那肯定是fast超过了slow一圈,说明链表中含有环。
public class Solution {
public boolean hasCycle(ListNode head) {
ListNode slow = head, fast = head;
while(fast != null && fast.next != null){
slow = slow.next;
fast = fast.next.next;
if(slow == fast){
return true;
}
}
return false;
}
}
fast一定比slow多走了k步,这多走的k步其实就是fast指针在环里转圈圈,所以k的值就是环长度的「整数倍」。
假设相遇点距环的起点的距离为m,那么结合上图的 slow 指针,环的起点距头结点head的距离为k - m,也就是说如果从head前进k - m步就能到达环起点。
巧的是,如果从相遇点继续前进k - m步,也恰好到达环起点。因为结合上图的 fast 指针,从相遇点开始走k步可以转回到相遇点,那走k - m步肯定就走到环起点了:
public class Solution {
public ListNode detectCycle(ListNode head) {
if(head == null || head.next == null){
return null;
}
ListNode slow = head, fast = head;
while(fast != null && fast.next != null){
slow = slow.next;
fast = fast.next.next;
if(fast == slow){
break;
}
}
if(fast == null || fast.next == null){
return null;
}
slow = head;
while(slow != fast){
slow = slow.next;
fast = fast.next;
}
return slow;
}
}
解决这个问题的关键是,通过某些方式,让p1和p2能够同时到达相交节点c1。
所以,可以让p1遍历完链表A之后开始遍历链表B,让p2遍历完链表B之后开始遍历链表A,这样相当于「逻辑上」两条链表接在了一起。
如果这样进行拼接,就可以让p1和p2同时进入公共部分,也就是同时到达相交节点c1:
public class Solution {
public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
ListNode p1 = headA, p2 = headB;
while(p1 != p2){
if(p1 == null){
p1 = headB;
}else{
p1 = p1.next;
}
if(p2 == null){
p2 = headA;
}else{
p2 = p2.next;
}
}
return p1;
}
}
非递归版:
class Solution {
public ListNode reverseList(ListNode head) {
if(head == null || head.next == null){
return head;
}
ListNode pre = null, cur = head;
while(cur != null){
ListNode next = cur.next;
cur.next = pre;
pre = cur;
cur = next;
}
return pre;
}
}
递归版本:
class Solution {
public ListNode reverseList(ListNode head) {
if(head == null || head.next == null){
return head;
}
ListNode last = reverseList(head.next);
head.next.next = head;
head.next = null;
return last;
}
}
因为是反转的前N个节点和全部反转不同,链表后半部分还是原来顺序,所以要有一个successor的连接点,这个连接点就是反转后head.next需要连接的。原来整体反转后head就是尾节点可以设置为null,但现在部分反转后是不行的。
ListNode successor = null; // 后驱节点
// 反转以 head 为起点的 n 个节点,返回新的头结点
ListNode reverseN(ListNode head, int n) {
if (n == 1) {
// 记录第 n + 1 个节点
successor = head.next;
return head;
}
// 以 head.next 为起点,需要反转前 n - 1 个节点
ListNode last = reverseN(head.next, n - 1);
head.next.next = head;
// 让反转之后的 head 节点和后面的节点连起来
head.next = successor;
return last;
}
如果m != 1怎么办?如果把head的索引视为 1,那么是想从第m个元素开始反转对吧;如果把head.next的索引视为 1 呢?那么相对于head.next,反转的区间应该是从第m - 1个元素开始的;那么对于head.next.next呢……
class Solution {
ListNode successor = null;
public ListNode reverseBetween(ListNode head, int left, int right) {
if(left == 1){
return reverseN(head, right);
}
head.next = reverseBetween(head.next, left-1, right-1);
return head;
}
public ListNode reverseN(ListNode head, int n){
if(n == 1){
successor = head.next;
return head;
}
ListNode last = reverseN(head.next, n-1);
head.next.next = head;
head.next = successor;
return last;
}
}
由于数组已经排序,所以重复的元素一定连在一起,找出它们并不难。
让慢指针slow走在后面,快指针fast走在前面探路,找到一个不重复的元素就赋值给slow并让slow前进一步。
这样,就保证了nums[0…slow]都是无重复的元素,当fast指针遍历完整个数组nums后,nums[0…slow]就是整个数组去重之后的结果。
class Solution {
public int removeDuplicates(int[] nums) {
if(nums.length == 0){
return 0;
}
int slow = 0, fast = 0;
while(fast < nums.length){
if(nums[slow] != nums[fast]){
slow++;
nums[slow] = nums[fast];
}
fast++;
}
return slow+1;
}
}
思路同LeetCode 26. 删除有序数组中的重复项,唯一区别数组变链表,循环完毕后,slow节点next置为null。
class Solution {
public ListNode deleteDuplicates(ListNode head) {
if(head == null){
return null;
}
ListNode slow = head, fast = head;
while(fast != null){
if(slow.val != fast.val){
slow.next = fast;
slow = slow.next;
}
fast = fast.next;
}
slow.next = null;
return head;
}
}
和有序数组(LeetCode 26. 删除有序数组中的重复项)去重的解法有一个细节差异,这里是先给nums[slow]赋值然后再给slow++,**因为数组第一位可能就是等于val的元素,所以slow++操作要后置。而数组元素去重,第一位一定可以保留,从第二位开始判断是否和前一位元素重复。**这样保证nums[0…slow-1]是不包含值为val的元素的,最后的结果数组长度就是slow。
class Solution {
public int removeElement(int[] nums, int val) {
int slow = 0, fast = 0;
while(fast < nums.length){
if(nums[fast] != val){
nums[slow] = nums[fast];
slow++;
}
fast++;
}
return slow;
}
}
该题配合LeetCode 27. 移除元素即可完成,先将元素为0的进行剔除,最后剩下的直接赋值为0即可。
class Solution {
public void moveZeroes(int[] nums) {
int idx = removeEle(nums, 0);
for(; idx < nums.length;idx++){
nums[idx] = 0;
}
}
public int removeEle(int[] nums, int val){
int slow = 0, fast = 0;
while(fast < nums.length){
if(nums[fast] != val){
nums[slow] = nums[fast];
slow++;
}
fast++;
}
return slow;
}
}
左右指针置换即可。
class Solution {
public void reverseString(char[] s) {
int left = 0, right = s.length-1;
while(left < right){
char tmp = s[left];
s[left] = s[right];
s[right] = tmp;
left++;
right--;
}
}
}
回文串的的长度可能是奇数也可能是偶数,解决该问题的核心是从中心向两端扩散的双指针技巧。
如果回文串的长度为奇数,则它有一个中心字符;如果回文串的长度为偶数,则可以认为它有两个中心字符。
该段代码主要计算字符串(i,j)为中心的最长回文串:
public String subStr(String s, int i, int j){
while(i >= 0 && j < s.length() && s.charAt(i) == s.charAt(j)){
i--;
j++;
}
return s.substring(i+1, j);
}
之后遍历字符串时,每次都计算奇数和偶数情况下的最长回文串,然后比对长度。
class Solution {
public String longestPalindrome(String s) {
String res = "";
for(int i = 0;i < s.length();i++){
String s1 = subStr(s, i, i);
String s2 = subStr(s, i, i+1);
res = res.length() > s1.length()? res:s1;
res = res.length() > s2.length()? res:s2;
}
return res;
}
public String subStr(String s, int i, int j){
while(i >= 0 && j < s.length() && s.charAt(i) == s.charAt(j)){
i--;
j++;
}
return s.substring(i+1, j);
}
}
需要对两个数组排序,但是nums2中元素的顺序不能改变,因为计算结果的顺序依赖nums2的顺序,所以不能直接对nums2进行排序,而是利用其他数据结构来辅助。
int n = nums1.length;
sort(nums1); // 田忌的马
sort(nums2); // 齐王的马
// 从最快的马开始比
for (int i = n - 1; i >= 0; i--) {
if (nums1[i] > nums2[i]) {
// 比得过,跟他比
} else {
// 比不过,换个垫底的来送人头
}
}
class Solution {
public int[] advantageCount(int[] nums1, int[] nums2) {
int n = nums1.length;
int[] res = new int[n];
PriorityQueue<int[]> queue = new PriorityQueue<>(
(int[] p1, int[] p2)-> {
return p2[1]-p1[1];
}
);
for(int i = 0;i < n;i++){
queue.offer(new int[]{i, nums2[i]});
}
Arrays.sort(nums1);
int left = 0, right = n-1;
while(!queue.isEmpty()){
int[] pair = queue.poll();
int idx = pair[0], val = pair[1];
if(val >= nums1[right]){
res[idx] = nums1[left];
left++;
}else{
res[idx] = nums1[right];
right--;
}
}
return res;
}
}
本题求的是数组下标集合,如果是数组元素集合,可以先对数组排序,然后执行下面逻辑,即可求出所有结果集合:
while (lo < hi) {
int sum = nums[lo] + nums[hi];
// 记录索引 lo 和 hi 最初对应的值
int left = nums[lo], right = nums[hi];
if (sum < target) lo++;
else if (sum > target) hi--;
else {
res.push_back({left, right});
// 跳过所有重复的元素
while (lo < hi && nums[lo] == left) lo++;
while (lo < hi && nums[hi] == right) hi--;
}
}
class Solution {
public int[] twoSum(int[] nums, int target) {
int left = 0, right = nums.length-1;
Map<Integer, Integer> map = new HashMap<>();
int[] res = new int[2];
for(int i = 0; i < nums.length;i++){
if(map.containsKey(target-nums[i])){
res[0] = i;
res[1] = map.get(target-nums[i]);
}
map.put(nums[i], i);
}
return res;
}
}
class Solution {
public List<List<Integer>> threeSum(int[] nums) {
List<List<Integer>> res = new ArrayList<>();
Arrays.sort(nums);
for(int i = 0;i < nums.length-2;i++){
int target = 0-nums[i];
int left = i+1, right = nums.length-1;
if(nums[i] > 0){
break;
}
if(i > 0 && nums[i] == nums[i-1]){
continue;
}
while(left < right){
int sum = nums[left]+nums[right];
if(target == sum){
res.add(Arrays.asList(nums[i], nums[left], nums[right]));
while(left < right && nums[left] == nums[left+1]){
left++;
}
while(left < right && nums[right] == nums[right-1]){
right--;
}
left++;
right--;
}else if(sum < target){
left++;
}else if(sum > target){
right--;
}
}
}
return res;
}
}
class Solution {
public List<List<Integer>> fourSum(int[] nums, int target) {
List<List<Integer>> res = new ArrayList<>();
List<List<Integer>> result = new ArrayList<>();
Arrays.sort(nums);
for(int i = 0;i < nums.length-2;i++){
for(int j = i+1; j < nums.length-2;j++){
int tar = target-nums[i]-nums[j];
int low = j+1, high = nums.length-1;
if (i==0||nums[i]!=nums[i-1]||nums[j]!=nums[j-1]){
while(low < high){
int left = nums[low], right = nums[high];
int sum = nums[low]+nums[high];
if(sum == tar){
res.add(Arrays.asList(nums[i], nums[j], left, right));
while(low < high && nums[low+1] == left){
low++;
}
while(low < high && nums[high-1] == right){
high--;
}
low++;
high--;
}else if(sum < tar){
low++;
}else if(sum > tar){
high--;
}
}
}
}
}
for (List<Integer> tmp:res){
if (!result.contains(tmp)){
result.add(tmp);
}
}
return result;
}
}
/* 注意:调用这个函数之前一定要先给 nums 排序 */
vector<vector<int>> nSumTarget(
vector<int>& nums, int n, int start, int target) {
int sz = nums.size();
vector<vector<int>> res;
// 至少是 2Sum,且数组大小不应该小于 n
if (n < 2 || sz < n) return res;
// 2Sum 是 base case
if (n == 2) {
// 双指针那一套操作
int lo = start, hi = sz - 1;
while (lo < hi) {
int sum = nums[lo] + nums[hi];
int left = nums[lo], right = nums[hi];
if (sum < target) {
while (lo < hi && nums[lo] == left) lo++;
} else if (sum > target) {
while (lo < hi && nums[hi] == right) hi--;
} else {
res.push_back({left, right});
while (lo < hi && nums[lo] == left) lo++;
while (lo < hi && nums[hi] == right) hi--;
}
}
} else {
// n > 2 时,递归计算 (n-1)Sum 的结果
for (int i = start; i < sz; i++) {
vector<vector<int>>
sub = nSumTarget(nums, n - 1, i + 1, target - nums[i]);
for (vector<int>& arr : sub) {
// (n-1)Sum 加上 nums[i] 就是 nSum
arr.push_back(nums[i]);
res.push_back(arr);
}
while (i < sz - 1 && nums[i] == nums[i + 1]) i++;
}
}
return res;
}
/* 滑动窗口算法框架 */
void slidingWindow(string s, string t) {
unordered_map<char, int> need, window;
for (char c : t) need[c]++;
int left = 0, right = 0;
int valid = 0;
while (right < s.size()) {
// c 是将移入窗口的字符
char c = s[right];
// 增大窗口
right++;
// 进行窗口内数据的一系列更新
...
/*** debug 输出的位置 ***/
printf("window: [%d, %d)\n", left, right);
/********************/
// 判断左侧窗口是否要收缩
while (window needs shrink) {
// d 是将移出窗口的字符
char d = s[left];
// 缩小窗口
left++;
// 进行窗口内数据的一系列更新
...
}
}
}
使用 Java 的读者要尤其警惕语言特性的陷阱。Java 的 Integer,String 等类型判定相等应该用equals方法而不能直接用等号==,这是 Java 包装类的一个隐晦细节。
class Solution {
public String minWindow(String s, String t) {
Map<Character, Integer> window = new HashMap<>();
Map<Character, Integer> need = new HashMap<>();
int valid = 0;
int left = 0, right = 0;
int start = 0, len = Integer.MAX_VALUE;
for(char c : t.toCharArray()){
need.put(c, need.getOrDefault(c,0)+1);
}
while(right < s.length()){
char d = s.charAt(right);
right++;
if(need.containsKey(d)){
window.put(d, window.getOrDefault(d, 0)+1);
if(need.get(d).equals(window.get(d))){
valid++;
}
}
while(valid == need.size()){
if(right-left < len){
start = left;
len = right-left;
}
char c = s.charAt(left);
left++;
if(need.containsKey(c)){
if(need.get(c).equals(window.get(c))){
valid--;
}
window.put(c, window.getOrDefault(c, 0)-1);
}
}
}
return len == Integer.MAX_VALUE ? "" : s.substring(start, start+len);
}
}
在《LeetCode 76. 最小覆盖子串》的基础之上,额外修改两个地方:
本题移动left缩小窗口的时机是窗口大小大于t.size()时,应为排列嘛,显然长度应该是一样的。
当发现valid == need.size()时,就说明窗口中就是一个合法的排列,所以立即返回true。
class Solution {
public boolean checkInclusion(String s1, String s2) {
Map<Character, Integer> need = new HashMap<>();
Map<Character, Integer> window = new HashMap<>();
int valid = 0;
for(char c: s1.toCharArray()){
need.put(c, need.getOrDefault(c, 0)+1);
}
int left = 0, right = 0;
while(right < s2.length()){
char d = s2.charAt(right);
right++;
if(need.containsKey(d)){
window.put(d, window.getOrDefault(d,0)+1);
if(need.get(d).equals(window.get(d))){
valid++;
}
}
while(right-left>=s1.length()){
if(need.size() == valid){
return true;
}
char c = s2.charAt(left);
left++;
if(need.containsKey(c)){
if(window.get(c).equals(need.get(c))){
valid--;
}
window.put(c, window.get(c)-1);
}
}
}
return false;
}
}
核心记住下面:
异位词即长度相同,那么窗口缩小实际就是right-left=p字符串长度,更进一步如果valid=needs.size(),说明找到了一个符合条件的异位词。
class Solution {
public List<Integer> findAnagrams(String s, String p) {
Map<Character, Integer> needs = new HashMap<>();
Map<Character, Integer> window = new HashMap<>();
int valid = 0;
int left = 0, right = 0;
for(char c: p.toCharArray()){
needs.put(c, needs.getOrDefault(c,0)+1);
}
List<Integer> res = new ArrayList<>();
while(right < s.length()){
char d = s.charAt(right);
right++;
if(needs.containsKey(d)){
window.put(d, window.getOrDefault(d,0)+1);
if(needs.get(d).equals(window.get(d))){
valid++;
}
}
while(right-left == p.length()){
if(valid == needs.size()){
res.add(left);
}
char c = s.charAt(left);
left++;
if(needs.containsKey(c)){
if(needs.get(c).equals(window.get(c))){
valid--;
}
window.put(c, window.get(c)-1);
}
}
}
return res;
}
}
连need和valid都不需要,而且更新窗口内数据也只需要简单的更新计数器window即可。
当window[c]值大于 1 时,说明窗口中存在重复字符,不符合条件,就该移动left缩小窗口了嘛。
唯一需要注意的是,在哪里更新结果res呢?我们要的是最长无重复子串,哪一个阶段可以保证窗口中的字符串是没有重复的呢?
这里和之前不一样,要在收缩窗口完成后更新res,因为窗口收缩的 while 条件是存在重复元素,换句话说收缩完成后一定保证窗口中没有重复嘛。
class Solution {
public int lengthOfLongestSubstring(String s) {
Map<Character, Integer> window = new HashMap<>();
int left = 0, right = 0;
int res = 0;
while(right < s.length()){
char d = s.charAt(right);
right++;
window.put(d, window.getOrDefault(d,0)+1);
while(window.get(d)>1){
char c = s.charAt(left);
left++;
window.put(c, window.get(c)-1);
}
res = Math.max(res, right-left);
}
return res;
}
}
int binarySearch(int[] nums, int target) {
int left = 0;
int right = nums.length - 1; // 注意
while(left <= right) {
int mid = left + (right - left) / 2;
if(nums[mid] == target)
return mid;
else if (nums[mid] < target)
left = mid + 1; // 注意
else if (nums[mid] > target)
right = mid - 1; // 注意
}
return -1;
}
int left_bound(int[] nums, int target) {
if (nums.length == 0) return -1;
int left = 0;
int right = nums.length; // 注意
while (left < right) { // 注意
int mid = (left + right) / 2;
if (nums[mid] == target) {
right = mid;
} else if (nums[mid] < target) {
left = mid + 1;
} else if (nums[mid] > target) {
right = mid; // 注意
}
}
// target 比所有数都大
if (left == nums.length) return -1;
// 类似之前算法的处理方式
return nums[left] == target ? left : -1;
}
int right_bound(int[] nums, int target) {
if (nums.length == 0) return -1;
int left = 0, right = nums.length;
while (left < right) {
int mid = (left + right) / 2;
if (nums[mid] == target) {
left = mid + 1; // 注意
} else if (nums[mid] < target) {
left = mid + 1;
} else if (nums[mid] > target) {
right = mid;
}
}
if (left == 0) return -1;
return nums[left-1] == target ? (left-1) : -1;
}
int binary_search(int[] nums, int target) {
int left = 0, right = nums.length - 1;
while(left <= right) {
int mid = left + (right - left) / 2;
if (nums[mid] < target) {
left = mid + 1;
} else if (nums[mid] > target) {
right = mid - 1;
} else if(nums[mid] == target) {
// 直接返回
return mid;
}
}
// 直接返回
return -1;
}
int left_bound(int[] nums, int target) {
int left = 0, right = nums.length - 1;
while (left <= right) {
int mid = left + (right - left) / 2;
if (nums[mid] < target) {
left = mid + 1;
} else if (nums[mid] > target) {
right = mid - 1;
} else if (nums[mid] == target) {
// 别返回,锁定左侧边界
right = mid - 1;
}
}
// 最后要检查 left 越界的情况
if (left >= nums.length || nums[left] != target)
return -1;
return left;
}
int right_bound(int[] nums, int target) {
int left = 0, right = nums.length - 1;
while (left <= right) {
int mid = left + (right - left) / 2;
if (nums[mid] < target) {
left = mid + 1;
} else if (nums[mid] > target) {
right = mid - 1;
} else if (nums[mid] == target) {
// 别返回,锁定右侧边界
left = mid + 1;
}
}
// 最后要检查 right 越界的情况
if (right < 0 || nums[right] != target)
return -1;
return right;
}
x:吃香蕉速度,f(x)吃香蕉耗时为x的递减函数。
吃香蕉最小值为1,最大就是数组元素最大值(开区间right要加1)
class Solution {
public int minEatingSpeed(int[] piles, int h) {
int left = 1, right = 1000000000+1;
while(left < right){
int mid = left+(right-left)/2;
if(f(piles, mid) == h){
right = mid;
}else if(f(piles, mid) > h){
left = mid+1;
}else if(f(piles, mid) < h){
right = mid;
}
}
return left;
}
public int f(int[] piles, int k){
int res = 0;
for(int p:piles){
res+=p/k;
if(p%k > 0){
res++;
}
}
return res;
}
}
思路同《LeetCode 875. 爱吃香蕉的珂珂》,只不过区间最值不同,一天至少能装满一船,所以最小值为数组中最重的包裹,最大值就是一天把所有包裹全部运完。
class Solution {
public int shipWithinDays(int[] weights, int days) {
int left = 0, right = 1;
for(int w:weights){
left = Math.max(left, w);
right+=w;
}
while(left < right){
int mid = left+(right-left)/2;
if(f(weights, mid) == days){
right = mid;
}else if(f(weights, mid) > days){
left = mid+1;
}else if(f(weights, mid) < days){
right = mid;
}
}
return left;
}
public int f(int[] weights, int x){
int day = 0;
for(int i = 0;i < weights.length;){
int cap = x;
while(i < weights.length){
if(cap < weights[i]){
break;
}else{
cap-=weights[i];
}
i++;
}
day++;
}
return day;
}
}
本题代码与《LeetCode 1011. 在 D 天内送达包裹的能力》一模一样,但是你是不是还没缓过神来? 那么我把题目翻译一下:现在有一堆货物,重量为nums[i],要求在m天内运送完毕,求货船最小载重为多少? 这本质是不是和题目一个意思!
class Solution {
public int splitArray(int[] nums, int k) {
int left = 0, right = 1;
for(int c : nums){
left = Math.max(c, left);
right+=c;
}
while(left < right){
int mid = left+(right-left)/2;
if(f(nums, mid) == k){
right = mid;
}else if(f(nums, mid) > k){
left = mid+1;
}else if(f(nums, mid) < k){
right = mid;
}
}
return left;
}
public int f(int[] nums, int x){
int res = 0;
for(int i = 0;i < nums.length;){
int c = x;
while(i < nums.length){
if(c < nums[i]){
break;
}else{
c-=nums[i];
}
i++;
}
res++;
}
return res;
}
}
class Solution {
public int removeCoveredIntervals(int[][] intervals) {
Arrays.sort(intervals, (a, b) -> {
return a[0]-b[0] == 0 ? b[1]-a[1]:a[0]-b[0];
});
int left = intervals[0][0];
int right = intervals[0][1];
int res = 0;
for(int i = 1; i < intervals.length;i++){
int[] arr = intervals[i];
if(arr[0] >= left && arr[1] <= right){
res++;
}
if(right < arr[0]){
left = arr[0];
right = arr[1];
}
if(right > arr[0] && right < arr[1]){
right = arr[1];
}
}
return intervals.length - res;
}
}
对于几个相交区间合并后的结果区间x,x.start一定是这些相交区间中start最小的,x.end一定是这些相交区间中end最大的。
class Solution {
public int[][] merge(int[][] intervals) {
Arrays.sort(intervals, (a, b) -> {
if(a[0] - b[0] == 0){
return b[0]-a[0];
}
return a[0]-b[0];
});
List<int[]> res = new ArrayList<>();
for(int i = 0;i < intervals.length;i++){
int left = intervals[i][0];
int right = intervals[i][1];
while(i < intervals.length-1 && right >= intervals[i+1][0]){
i++;
right = Math.max(right, intervals[i][1]);
}
res.add(new int[]{left, right});
}
int n = res.size();
int[][] result = new int[n][2];
for(int i = 0;i < n;i++){
int[] a = res.get(i);
result[i][0] = a[0];
result[i][1] = a[1];
}
return result;
}
}
对于两个区间,我们用[a1,a2]和[b1,b2]表示在A和B中的两个区间,那么什么情况下这两个区间有交集呢:
# 不等号取反,or 也要变成 and
if b2 >= a1 and a2 >= b1:
[a1,a2] 和 [b1,b2] 存在交集
两个区间存在交集的情况有哪些呢
如果交集区间是[c1,c2],那么c1=max(a1,b1),c2=min(a2,b2)!这一点就是寻找交集的核心.
class Solution {
public int[][] intervalIntersection(int[][] firstList, int[][] secondList) {
List<int[]> res = new ArrayList<>();
int i = 0, j = 0;
while(i < firstList.length && j < secondList.length){
int a1 = firstList[i][0], a2 = firstList[i][1];
int b1 = secondList[j][0], b2 = secondList[j][1];
if(a2 >= b1 && b2 >= a1){
res.add(new int[]{Math.max(a1, b1), Math.min(a2, b2)});
}
//单独走if分支判断,因为有交集时,也符合b2 >= a2
if(b2 >= a2){
i++;
}else{
j++;
}
}
int n = res.size();
int[][] r = new int[n][2];
for(int k = 0; k < n;k++){
int[] arr = res.get(k);
r[k][0] = arr[0];
r[k][1] = arr[1];
}
return r;
}
}
本题来源于Leetcode中 归属于数组、链表类型题目。
同许多在算法道路上不断前行的人一样,不断练习,修炼自己!
如有博客中存在的疑问或者建议,可以在下方留言一起交流,感谢各位!
觉得本博客有用的客官,可以给个点赞+收藏哦! 嘿嘿
喜欢本系列博客的可以关注下,以后除了会继续更新面试手撕代码文章外,还会出其他系列的文章!