数组
1、双指针
双指针技巧主要分为两类:左右指针和快慢指针。
快慢指针
26. 删除有序数组中的重复项
给你一个 升序排列 的数组 nums
,请你 原地删除重复出现的元素,使每个元素 只出现一次 ,返回删除后数组的新长度。元素的 相对顺序 应该保持 一致 。
由于在某些语言中不能改变数组的长度,所以必须将结果放在数组 nums
的第一部分。更规范地说,如果在删除重复项之后有 k
个元素,那么 nums
的前 k
个元素应该保存最终结果。
将最终结果插入 nums
的前 k
个位置后返回 k
。
不要使用额外的空间,你必须在 原地 修改输入数组 并在使用 O(1) 额外空间的条件下完成。
class Solution { public int removeDuplicates(int[] nums) { if(nums.length == 0) return 0; int slow = 0, fast = 0; while(fast < nums.length){ if(nums[fast] != nums[slow]){ slow++; // 维护 nums[0..slow] 无重复 nums[slow] = nums[fast]; } fast++; } // 数组长度为 索引 + 1 return slow + 1; } }
扩展:
83. 删除排序链表中的重复元素
给定一个已排序的链表的头 head
, 删除所有重复的元素,使每个元素只出现一次 。返回 已排序的链表 。
/** * Definition for singly-linked list. * public class ListNode { * int val; * ListNode next; * ListNode() {} * ListNode(int val) { this.val = val; } * ListNode(int val, ListNode next) { this.val = val; this.next = next; } * } */ class Solution { public ListNode deleteDuplicates(ListNode head) { if(head == null) return null; ListNode slow, fast; slow = fast = head; while(fast != null){ if(fast.val != slow.val){ // nums[slow] = nums[fast]; slow.next = fast; // slow++; slow = slow.next; } // fast++ fast = fast.next; } // 断开与后面重复元素的连接 slow.next = null; return head; } }
总结:类比上面的有序数组原地去重,有个细节地方:链表去重要先赋值,再slow++
27. 移除元素
给你一个数组 nums
和一个值 val
,你需要 原地 移除所有数值等于 val
的元素,并返回移除后数组的新长度。
不要使用额外的数组空间,你必须仅使用 O(1)
额外空间并 原地 修改输入数组。
元素的顺序可以改变。你不需要考虑数组中超出新长度后面的元素。
说明:
为什么返回数值是整数,但输出的答案是数组呢?
请注意,输入数组是以**「引用」**方式传递的,这意味着在函数里修改输入数组对于调用者是可见的。
class Solution { public int removeElement(int[] nums, int val) { if(nums.length == 0) return 0; int slow = 0, fast = 0; while(fast < nums.length){ if(nums[fast] != val){ nums[slow] = nums[fast]; slow++; } fast++; } return slow; } }
注意:这里和有序数组去重的解法有一个细节,我们这里是先给 nums[slow]
赋值然后再给 slow++
,这样可以保证 nums[0..slow-1]
是不包含值为 val
的元素的,最后的结果数组长度就是 slow
,slow下标记得不用+1
扩展:
283. 移动零
给定一个数组 nums
,编写一个函数将所有 0
移动到数组的末尾,同时保持非零元素的相对顺序。
请注意 ,必须在不复制数组的情况下原地对数组进行操作。
思路:题目让我们将所有 0 移到最后,其实就相当于移除 nums
中的所有 0,然后再把后面的元素都赋值为 0 即可。
class Solution { public void moveZeroes(int[] nums) { // 去除 nums 中的所有 0,返回不含 0 的数组长度 int len = removeElement(nums, 0); // 将 nums[len...] 的元素赋值为 0 for(; len < nums.length; len++){ nums[len] = 0; } } private int removeElement(int[] nums, int val){ if(nums.length == 0) return 0; int slow = 0, fast = 0; while(fast < nums.length){ if(nums[fast] != val){ nums[slow] = nums[fast]; slow++; } fast++; } return slow; } }
两数之和
167. 两数之和 II - 输入有序数组
给你一个下标从 1 开始的整数数组 numbers
,该数组已按 非递减顺序排列 ,请你从数组中找出满足相加之和等于目标数 target
的两个数。如果设这两个数分别是 numbers[index1]
和 numbers[index2]
,则 1 <= index1 < index2 <= numbers.length
。
以长度为 2 的整数数组 [index1, index2]
的形式返回这两个整数的下标 index1
和 index2
。
你可以假设每个输入 只对应唯一的答案 ,而且你 不可以 重复使用相同的元素。
你所设计的解决方案必须只使用常量级的额外空间。
思路:只要数组有序,就应该想到双指针技巧。这道题的解法有点类似二分查找,通过调节 left
和 right
就可以调整 sum
的大小:
class Solution { public int[] twoSum(int[] numbers, int target) { // 一左一右两个指针相向而行 int left = 0, right = numbers.length - 1; while(left < right){ int sum = numbers[left] + numbers[right]; if(sum == target){ // 题目要求的索引是从 1 开始的 return new int[]{left+1, right+1}; } else if (sum < target){ left++;// 让 sum 大一点 } else if (sum > target){ right--;// 让 sum 小一点 } } return new int[]{-1, -1}; } }
反转数组
344. 反转字符串
编写一个函数,其作用是将输入的字符串反转过来。输入字符串以字符数组 s
的形式给出。
不要给另外的数组分配额外的空间,你必须**原地修改输入数组**、使用 O(1) 的额外空间解决这一问题。
class Solution { public void reverseString(char[] s) { // 一左一右两个指针相向而行 int left = 0, right = s.length - 1; while(left < right){ // 交换 s[left] 和 s[right] char temp = s[right]; s[right] = s[left]; s[left] = temp; right--; left++; } } }
回文串判断
判断一个字符串是不是回文串
boolean isPalindrome(String s) { // 一左一右两个指针相向而行 int left = 0, right = s.length() - 1; while (left < right) { if (s.charAt(left) != s.charAt(right)) { return false; } left++; right--; } return true; }
5. 最长回文子串
给你一个字符串 s
,找到 s
中最长的回文子串。
如果字符串的反序与原始字符串相同,则该字符串称为回文字符串。
找回文串的难点在于,回文串的的长度可能是奇数也可能是偶数,解决该问题的核心是从中心向两端扩散的双指针技巧。
如果回文串的长度为奇数,则它有一个中心字符;如果回文串的长度为偶数,则可以认为它有两个中心字符。
class Solution { public String longestPalindrome(String s) { String res = ""; for(int i = 0; i < s.length(); i++){ // 以 s[i] 为中心的最长回文子串 String s1 = palindrome(s, i, i); // 以 s[i] 和 s[i+1] 为中心的最长回文子串 String s2 = palindrome(s, i, i+1); // 没看懂这里??? res = res.length() > s1.length() ? res : s1; res = res.length() > s2.length() ? res : s2; } return res; } // 在 s 中寻找以 s[l] 和 s[r] 为中心的最长回文串 private String palindrome(String s, int l, int r) { // 防止索引越界 while(l >= 0 && r < s.length() && s.charAt(l) == s.charAt(r)){ // 双指针,向两边展开 l--; r++; } // 返回以 s[l] 和 s[r] 为中心的最长回文串 return s.substring(l + 1, r); } }
总结:最长回文子串使用的左右指针和之前题目的左右指针有一些不同:之前的左右指针都是从两端向中间相向而行,而回文子串问题则是让左右指针从中心向两端扩展。
2、数组求和技巧
前缀和数组
前缀和主要适用的场景是原始数组不会被修改的情况下,频繁查询某个区间的累加和。
303. 区域和检索 - 数组不可变
给定一个整数数组 nums
,处理以下类型的多个查询:
计算索引 left
和 right
(包含 left
和 right
)之间的 nums
元素的 和 ,其中 left <= right
1、一般人的解法:
class NumArray { private int[] arr; public NumArray(int[] nums) { arr = nums; } public int sumRange(int left, int right) { int res = 0; for(int i = left; i <= right; i++){ res += arr[i]; } return res; } } /** * Your NumArray object will be instantiated and called as such: * NumArray obj = new NumArray(nums); * int param_1 = obj.sumRange(left,right); */
这样,可以达到效果,但是效率很差,因为 sumRange
方法会被频繁调用,而它的时间复杂度是 O(N)
,其中 N
代表 nums
数组的长度
这道题的最优解法是使用前缀和技巧,将 sumRange
函数的时间复杂度降为 O(1)
,说白了就是不要在 sumRange
里面用 for 循环
class NumArray{ // 前缀和数组 private int[] preSum; /* 输入一个数组,构造前缀和 */ public NumArray(int[] nums) { // 前缀和数组比原始数组索引多1,因为一个都不累加就是0 preSum = new int[nums.length + 1]; // 计算 nums 的累加和 for(int i = 1; i < preSum.length; i++){ preSum[i] = preSum[i -1] + nums[i - 1]; } } public int sumRange(int left, int right) { //如果我想求索引区间 [1, 4] 内的所有元素之和,就可以通过 preSum[5] - preSum[1] return preSum[right + 1] - preSum[left]; } }
304. 二维区域和检索 - 矩阵不可变
给定一个二维矩阵 matrix
,以下类型的多个请求:
- 计算其子矩形范围内元素的总和,该子矩阵的 左上角 为
(row1, col1)
,右下角 为(row2, col2)
。 - 实现
NumMatrix
类: NumMatrix(int[][] matrix)
给定整数矩阵matrix
进行初始化int sumRegion(int row1, int col1, int row2, int col2)
返回 左上角(row1, col1)
、右下角(row2, col2)
所描述的子矩阵的元素 总和 。
class NumMatrix { // 定义:preSum[i][j] 记录 matrix 中子矩阵 [0, 0, i-1, j-1] 的元素和 private int[][] preSum; public NumMatrix(int[][] matrix) { int m = matrix.length, n = matrix[0].length; if (m == 0 || n == 0) return; // 构造前缀和矩阵 preSum = new int[m + 1][n + 1]; for (int i = 1; i <= m; i++) { for (int j = 1; j <= n; j++) { // 计算每个矩阵 [0, 0, i, j] 的元素和 preSum[i][j] = preSum[i-1][j] + preSum[i][j-1] + matrix[i - 1][j - 1] - preSum[i-1][j-1]; } } } // 计算子矩阵 [x1, y1, x2, y2] 的元素和 public int sumRegion(int x1, int y1, int x2, int y2) { // 目标矩阵之和由四个相邻矩阵运算获得 return preSum[x2+1][y2+1] - preSum[x1][y2+1] - preSum[x2+1][y1] + preSum[x1][y1]; } } /** * Your NumMatrix object will be instantiated and called as such: * NumMatrix obj = new NumMatrix(matrix); * int param_1 = obj.sumRegion(row1,col1,row2,col2); */
差分数组
差分数组的主要适用场景是频繁对原始数组的某个区间的元素进行增减。
构造差分数组
先对 nums
数组构造一个 diff
差分数组,diff[i]
就是 nums[i]
和 nums[i-1]
之差:
int[] diff = new int[nums.length]; // 构造差分数组 diff[0] = nums[0]; for (int i = 1; i < nums.length; i++) { diff[i] = nums[i] - nums[i - 1]; }
通过这个 diff
差分数组是可以反推出原始数组 nums
的
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
即可:
最终的差分工具类
// 差分数组工具类 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; } }
这是个会员题目
区间加法
class Solution{ public 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(); } } // 差分数组工具类 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; } }
1109. 航班预订统计
这里有 n
个航班,它们分别从 1
到 n
进行编号。
有一份航班预订表 bookings
,表中第 i
条预订记录 bookings[i] = [firsti, lasti, seatsi]
意味着在从 firsti
到 lasti
(包含 firsti
和 lasti
)的 每个航班 上预订了 seatsi
个座位。
请你返回一个长度为 n
的数组 answer
,里面的元素是每个航班预定的座位总数。
注意:因为题目说的 n
是从 1 开始计数的,而数组索引从 0 开始,所以对于输入的三元组 (i, j, k)
,数组区间应该对应 [i-1,j-1]
。
class Solution { public int[] corpFlightBookings(int[][] bookings, int n) { // nums 初始化为全 0 int[] nums = new int[n]; // 构造差分解法 Difference df = new Difference(nums); for(int[] booking : bookings){ int i = booking[0] - 1; int j = booking[1] - 1; int val = booking[2]; df.increment(i, j, val); } return df.result(); } } // 差分数组工具类 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; } }
1094. 拼车
车上最初有 capacity 个空座位。车 只能 向一个方向行驶(也就是说,不允许掉头或改变方向)
给定整数 capacity 和一个数组 trips , trip[i] = [numPassengersi, fromi, toi] 表示第 i 次旅行有 numPassengersi 乘客,接他们和放他们的位置分别是 fromi 和 toi 。这些位置是从汽车的初始位置向东的公里数。
当且仅当你可以在所有给定的行程中接送所有乘客时,返回 true,否则请返回 false。
思路:trips[i] 代表着一组区间操作,旅客的上车和下车就相当于数组的区间加减;只要结果数组中的元素都小于 capacity,就说明可以不超载运输所有旅客
车站编号从 0 开始,最多到 1000,也就是最多有 1001 个车站,那么我们的差分数组长度可以直接设置为 1001,这样索引刚好能够涵盖所有车站的编号
class Solution { public boolean carPooling(int[][] trips, int capacity) { // 最多有 1001 个车站 int[] nums = new int[1001]; // 构造差分解法 Difference df = new Difference(nums); for(int[] trip : trips){ // 第 trip[1] 站乘客上车 int i = trip[1]; // 第 trip[2] 站乘客已经下车, // 即乘客在车上的区间是 [trip[1], trip[2] - 1] int j = trip[2] - 1; // 乘客数量 int val = trip[0]; df.increment(i,j,val); } int[] res = df.result(); for(int i = 0; i < res.length; i++){ if(capacity < res[i]) return false; } return true; } } // 差分数组工具类 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; } }