【leetcode】数组专场,循序渐进,双指针技巧
在处理数组和链表相关问题时,双指针技巧是经常用到的,双指针技巧主要分为两类:左右指针和快慢指针。
-
左右指针:就是两个指针相向而行 或 相背而行
-
快慢指针:就是两个指针同向而行,一快一慢
一、快慢指针技巧
26.删除有序数组中的重复项
数组问题中比较常见且难度不高的的快慢指针技巧,就是原地修改数组。
什么是原地修改?
-
如果不是原地修改的话,可以直接
new
一个int[]
数组,把去重之后的元素放进这个新数组中,然后返回这个新数组即可。 -
但是现在题目让你原地删除,不允许
new
新数组,只能在原数组上操作,然后返回一个长度,这样就可以通过返回的长度和原始数组得到我们去重后的元素有哪些了。
由于数组已经排序,所以重复的元素一定连在一起。但如果毎找到一个重复元素就立即原地删除它,由于数组中删除元素涉及数据搬移,整个时间复杂度是会达到 O ( N 2 ) O(N^2) O(N2)。
高效解决这道题就要用到快慢指针技巧:
- 可以让慢指针
slow
走在后面,快指针fast
走在前面,当找到一个不重复的元素时(nums[slow] != nums[fast]
),就让slow
先向前进一步,再将fast
的值赋给slow
。
这样,就保证了 nums[0...slow]
都是无重复的元素,当 fast
指针遍历完整个数组 nums
后,nums[0...slow]
就是整个数组去重之后的结果。
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;
}
再扩展一下,如果给你一个有序的单链表,如何去重?
其实和数组去重是一模一样的,唯一的区别是把数组赋值操作变成操作指针而已,对照着之前的代码来看:
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;
}
27.移除元素
除了在有序数组/链表中去重,题目还可能让你对数组中的某些元素进行「原地删除」。
题目要求我们把 nums
中所有值为 val
的元素原地删除,依然需要使用快慢指针技巧:
如果 fast
遇到值为 val
的元素,则直接跳过,否则就赋值给 slow
指针,并让 slow
前进一步。
这和前面的数组去重问题解法思路基本一样,直接上代码:
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;
}
注意:这里和有序数组去重的解法有一个细节差异,我们这里是先给 nums[slow]
赋值然后再给 slow++
,这样可以保证 nums[0..slow-1]
是不包含值为 val
的元素的,最后的结果数组长度就是 slow
。
283.移动零
比如说给你输入 nums = [0,1,4,0,2]
,你的算法没有返回值,但是会把 nums
数组原地修改成 [1,4,2,0,0]
。
题目让我们将所有 0
移到最后,其实就相当于移除 nums
中的所有 0
,然后再把后面的元素都赋值为 0
即可。
所以我们可以复用上一题的 removeElement
函数:
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);
二、左右指针技巧
167.两数之和 II
只要数组有序,就应该想到双指针技巧。
这道题的解法有点类似二分查找,通过调节 left
和 right
就可以调整 sum
的大小:
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};
}
344.反转数组
一般编程语言都会提供 reverse
函数,其实这个函数的原理比较简单,就是交换数组的值。
反转字符串这道题,就是类似的功能,让你反转一个 char[]
类型的字符数组,直接看代码吧:
// while循环
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--;
}
}
// for循环
void reverseString(char[] s) {
for (int left = 0, right = s.length - 1; left < right; left++, right--) {
char tmp = s[left];
s[left] = s[right];
s[right] = tmp;
}
}
5.最长回文子串
什么是回文串?回文串就是正着读和反着读都一样的字符串。
比如说字符串 aba
和 abba
都是回文串,因为它们是对称的,反过来还是和本身一样;反之,字符串 abab
就不是回文串。
可以感觉到回文串问题和左右指针肯定有关联,比如让判断一个字符串是不是回文串,可以写出下面这段代码:
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;
}
找回文串的难点在于,回文串的的长度可能是奇数也可能是偶数,解决该问题的核心是从中心向两端扩散的双指针技巧。
-
如果回文串的长度为奇数,则它有一个中心字符;
-
如果回文串的长度为偶数,则可以认为它有两个中心字符。
所以我们可以先实现这样一个函数:
// 在 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);
}
这样,如果输入相同的 l
和 r
,就相当于寻找长度为奇数的回文串,如果输入相邻的 l
和 r
,则相当于寻找长度为偶数的回文串。
那么回到最长回文串的问题,解法的大致思路就是:
for 0 <= i < len(s):
找到以 s[i] 为中心的回文串
找到以 s[i] 和 s[i+1] 为中心的回文串
更新答案
翻译成代码,就可以解决最长回文子串这个问题:
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;
}
可以发现最长回文子串使用的左右指针和之前题目的左右指针有一些不同:
-
之前的左右指针都是从两端向中间相向而行,
-
而回文子串问题则是让左右指针从中心向两端扩展。
一般这种情况就是回文串这类问题会遇到。