- 数组和链表类似,都是用双指针,但数组不需要额外的指针,可以使用索引来当作指针。(链表的时候要注意,什么时候是移动的指针,什么时候是改变的节点)
- 删除有序数组中的重复项
注意,本题中的数组是有序的,但是有重复项,我们要进行去重,这和链表类似,用快慢指针,快指针不等于慢指针的时候,我们用快指针将慢指针覆盖,然后移动指针。相同的时候移动快指针就好。
代码中有个需要注意的地方,我们在while循环中,当快慢指针不同的时候,我们需要先移动慢指针,然后再替换,因为这样才能避免慢指针这个地方的元素被覆盖,我们是要去除慢指针后面那个重复元素。
我们要返回的是数组中不重复元素的个数,所以应该索引+1.
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;
}
下面是链表的代码(slow.next=fast,就是改变的链表节点连接,而slow = slow.next则是移动的指针,走向下一个节点进行判断)
ListNode deleteDuplicates(ListNode head) {
if (head == null) return null;
ListNode slow = head, 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;
}
- 移除元素
这个是给定了一个值,让我们移除数组中和这个值相同的元素,其实做法也是快慢指针,和上面题类似,但这里有个细节,我们在判断当这个值和快指针不相等的时候,我们要先替换慢指针的地方,然后再移动慢指针,因为我们是要删除掉与x相等的元素,只要有就删,一个不能留。快指针是用来遍历元素,找到与x不同的值,然后保留在慢指针的位置,慢指针是存储位置的索引。
int removeElement(int[] nums, int val) {
int fast = 0, slow = 0;
while (fast < nums.length) {
if (nums[fast] != val) {
nums[slow] = nums[fast];
slow++;
}
fast++;
}
return slow;
}
- 移动0
题中要求将数组的值为0的元素都移动到数组末尾。我们可以先移除数组中的0,然后在数组后面进行补0即可。
所以我们可以借助上题的删除指定元素的代码,然后删除0,然后再用0补齐数组即可
void moveZeroes(int[] nums) {
// 去除 nums 中的所有 0,返回不含 0 的数组长度
int p = removeElement(nums, 0);
// 将 nums[p..] 的元素赋值为 0
for (; p < nums.length; p++) {
nums[p] = 0;
}
}
int removeElement(int[] nums, int val) {
int fast = 0, slow = 0;
while (fast < nums.length) {
if (nums[fast] != val) {
nums[slow] = nums[fast];
slow++;
}
fast++;
}
return slow;
}
- 二分查找
二分查找我们可以选择左闭右开或者左闭右闭的区间。选择左闭右开,那么左和右肯定不会相等,while(left<right),然后left=mid+1,right=mid
class Solution {
public int search(int[] nums, int target) {
int left = 0, right = nums.length;
while (left < right) {
int mid = left + ((right - left) >> 1);
if (nums[mid] == target)
return mid;
else if (nums[mid] < target)
left = mid + 1;
else if (nums[mid] > target)
right = mid;
}
return -1;
}
}
- 两数之和Ⅱ
给你一个下标从 1 开始的整数数组 numbers ,该数组已按 非递减顺序排列 ,请你从数组中找出满足相加之和等于目标数 target 的两个数。如果设这两个数分别是 numbers[index1] 和 numbers[index2] ,则 1 <= index1 < index2 <= numbers.length
数组有序,我们用双指针,为了满足题中索引要求,我们可以使用相向双指针,即left=0,right=nums.length-1。然后来进行判断。注意,两个索引是不相等的,所以我们的while循环中,应该是left<right
int[] twoSum(int[] nums, int target) {
// 一左一右两个指针相向而行
int left = 0, right = nums.length - 1;
while (left < right) {
int sum = nums[left] + nums[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};
}
- 翻转数组
void reverseString(char[] s) {
// 一左一右两个指针相向而行
int left = 0, right = s.length - 1;
while (left < right) {
// 交换 s[left] 和 s[right]
char temp = s[left];
s[left] = s[right];
s[right] = temp;
left++;
right--;
}
}
- 回文串判断
回文串就是正着读和反着读都一样的字符串
判断字符串是不是回文串
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;
}
- 最长回文子串
我们可以用双指针来做,但我们要找最长的回文子串,是连续的,我们可以从中心向两端扩散的双指针。
因为我们是从中心往两边走,找最长的回文子串,如果整个子串都是回文的,那我们走到最左边0的位置,和最右边nums.length-1的位置也符合,这个时候我们的指针还会移动,此时L已经走到了-1,而R已经走到了nums.length,所以我们在截取字符串的时候是截取的substring(l+1,r)。
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 = longest(res, s1, s2)
res = res.length() > s1.length() ? res : s1;
res = res.length() > s2.length() ? res : s2;
}
return res;
}
// 在 s 中寻找以 s[l] 和 s[r] 为中心的最长回文串
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);
}
- 合并两个有序数组
我们不能用新数组,但是如果从前向后遍历的话,会覆盖掉nums1中的元素,我们可以将双指针初始化在数组的尾部,然后从后向前进行合并。
class Solution {
public void merge(int[] nums1, int m, int[] nums2, int n) {
// 两个指针分别初始化在两个数组的最后一个元素(类似拉链两端的锯齿)
int i = m - 1, j = n - 1;
// 生成排序的结果(类似拉链的拉锁)
int p = nums1.length - 1;
// 从后向前生成结果数组,类似合并两个有序链表的逻辑
while (i >= 0 && j >= 0) {
if (nums1[i] > nums2[j]) {
nums1[p] = nums1[i];
i--;
} else {
nums1[p] = nums2[j];
j--;
}
p--;
}
// 可能其中一个数组的指针走到尽头了,而另一个还没走完
// 因为我们本身就是在往 nums1 中放元素,所以只需考虑 nums2 是否剩元素即可
while (j >= 0) {
nums1[p] = nums2[j];
j--;
p--;
}
}
}
- 有序数组的平方
本题其实也是合并数组,只不过把远数字换成平方,我们仍采用双指针,因为这个数组元素的平方的最值,肯定是在数组的两端。我们采用相向双指针,注意,新数组应该从后往前添加,因为我们是从两端开始算的最大值
class Solution {
public int[] sortedSquares(int[] nums) {
int n = nums.length;
// 两个指针分别初始化在正负子数组绝对值最大的元素索引
int i = 0, j = n - 1;
// 得到的有序结果是降序的
int p = n - 1;
int[] res = new int[n];
// 执行双指针合并有序数组的逻辑
while (i <= j) {
if (Math.abs(nums[i]) > Math.abs(nums[j])) {
res[p] = nums[i] * nums[i];
i++;
} else {
res[p] = nums[j] * nums[j];
j--;
}
p--;
}
return res;
}
}
- 有序序列转数组
一个有序数组,给定abc,对每一个元素计算f(x) = ax2 + bx + c,按照升序返回数组
所以这道题的关键也是在 nums 的开头和结尾设置 i, j 双指针相向而行,执行合并有序数组的逻辑
class Solution {
public int[] sortTransformedArray(int[] nums, int a, int b, int c) {
// 双指针,相向而行,逼近对称轴
int i = 0, j = nums.length - 1;
// 如果开口朝上,越靠近对称轴函数值越小
// 如果开口朝下,越靠近对称轴函数值越大
int p = a > 0 ? nums.length - 1 : 0;
int[] res = new int[nums.length];
// 执行合并两个有序数组的逻辑
while (i <= j) {
int v1 = f(nums[i], a, b, c);
int v2 = f(nums[j], a, b, c);
if (a > 0) {
// 如果开口朝上,越靠近对称轴函数值越小
if (v1 > v2) {
res[p--] = v1;
i++;
} else {
res[p--] = v2;
j--;
}
} else {
// 如果开口朝下,越靠近对称轴函数值越大
if (v1 > v2) {
res[p++] = v2;
j--;
} else {
res[p++] = v1;
i++;
}
}
}
return res;
}
int f(int x, int a, int b, int c) {
return a*x*x + b*x + c;
}
}
- 区域和检索
用一维前缀和,前缀和数组比之前数组多一位,并且在presum[0]=0,然后再进行计算,公式为presum[i]=presum[i-1]+nums[i-1],计算两个索引left和right之间的元素和直接可以用presum[right+1]-presum[left]。因为presum[right+1]是计算到了nums中索引从0到right的位置,而presum[left]是计算到了nums中索引从0到left-1的位置,所以二者相减就是left到right之间的元素和。
class NumArray {
// 前缀和数组
private int[] preSum;
/* 输入一个数组,构造前缀和 */
public NumArray(int[] nums) {
// preSum[0] = 0,便于计算累加和
preSum = new int[nums.length + 1];
// 计算 nums 的累加和
for (int i = 1; i < preSum.length; i++) {
preSum[i] = preSum[i - 1] + nums[i - 1];
}
}
/* 查询闭区间 [left, right] 的累加和 */
public int sumRange(int left, int right) {
return preSum[right + 1] - preSum[left];
}
}
- 二维前缀和
同理,二维前缀和数组我们也多建一行一列,索引从1开始遍历,然后,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] 的元素和
preSum[x2+1][y2+1] - preSum[x1][y2+1] - preSum[x2+1][y1] + preSum[x1][y1];
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];
}
}