前言
记录刷 LeetCode 时遇到的双指针相关题目
27.移除元素 - 快慢双指针
public static int removeElement(int[] nums, int val) {
//0个元素或数组为null
if(nums == null || nums.length == 0){
return 0;
}
int length = nums.length;
//下面这个算法的步骤如果看不明白,代入几个边界用例以及几个一般用例之后会发现是符合的
int slow = 0,fast = 0;
while (fast < length){
//如果当前fast指向的值是val,就让fast向后移动直到指向的值不是val,然后就把fast指向的值赋值给slow,
//然后两个指针同时向前移动直到fast再次指向val,然后循环该操作
if(nums[fast] == val){
while (fast < length && nums[fast] == val){
fast++;
}
while (fast < length && nums[fast] != val){
//在slow和fast中间的都是val,而slow是先赋值完才++的,所以结束循环时slow指向的也是val
nums[slow] = nums[fast];
slow++;
fast++;
}
}else {
//当两个指针都从0开始移动时,可能前面的值都不是val,此时两个指针同步移动,一旦进入了上面的if分支,
//在每次循环一遍后,不是fast >= length就是 nums[fast] == val 所以不会再经过这个else分支
slow++;
fast++;
}
}
return slow;
}
26.删除有序数组中的重复项 - 快慢双指针
与27题一个思路
public int removeDuplicates(int[] nums) {
if(nums.length == 0){
return 0;
}
//slow指的是上次被赋值的位置
int slow = 0,fast = 0,record,length = nums.length;
while (true){
record = nums[fast];
while (fast < length && nums[fast] == record) {
fast++;
}
if(fast < length){
slow++;
nums[slow] = nums[fast];
}else {
return slow + 1;
}
}
}
283.移动零 - 快慢双指针
public static void moveZeroes(int[] nums) {
//0个元素或数组为null
if (nums == null || nums.length == 0 || nums.length == 1) {
return;
}
int val = 0;
int length = nums.length;
// 下面这个算法的步骤是基于27题的,所以思路完全一样
int slow = 0, fast = 0;
while (fast < length) {
//如果当前fast指向的值是val,就让fast向后移动直到指向的值不是val,然后就把fast指向的值赋值给slow,
//然后两个指针同时向前移动直到fast再次指向val,然后循环该操作
if (nums[fast] == val) {
while (fast < length && nums[fast] == val) {
fast++;
}
while (fast < length && nums[fast] != val) {
//在slow和fast中间的都是val,而slow是先赋值完才++的,所以结束循环时slow指向的也是val
nums[slow] = nums[fast];
slow++;
fast++;
}
} else {
//当两个指针都从0开始移动时,可能前面的值都不是val,此时两个指针同步移动,一旦进入了上面的if分支,在每次循环一遍后,不是fast >= length
//就是 nums[fast] == val 所以不会再经过这个else分支
slow++;
fast++;
}
}
for(int i = slow;i < length;i++){
nums[i] = 0;
}
}
844.比较含退格的字符串 - 快慢双指针
根据题意一开始就想到用栈来做,但是事实证明浪费空间,而且时间花费也不理想:
public static boolean backspaceCompare(String s, String t) {
if(s == null || t == null){
return true;
}
LinkedList<Character> stack1 = new LinkedList<>();
LinkedList<Character> stack2 = new LinkedList<>();
for (int i = 0; i < s.length(); i++) {
char c = s.charAt(i);
if(c == '#'){
if(stack1.size() > 0){
stack1.poll();
}
}else {
stack1.push(c);
}
}
for (int i = 0; i < t.length(); i++) {
char c = t.charAt(i);
if(c == '#'){
if(stack2.size() > 0){
stack2.poll();
}
}else {
stack2.push(c);
}
}
int size1 = stack1.size();
int size2 = stack2.size();
if(size2 != size1){
return false;
}
for (int i = 0; i < size1; i++) {
if(!stack1.get(i).equals(stack2.get(i))){
return false;
}
}
return true;
}
理想做法应该是快慢双指针:
class Solution {
public boolean backspaceCompare(String s, String t) {
return dealString(s).equals(dealString(t));
}
//对字符串进行退格处理
public String dealString(String s){
int slow = 0,fast = 0;
char[] charArray = s.toCharArray();
int length = charArray.length;
while (fast < length){
if(charArray[fast] == '#'){
while(fast < length && charArray[fast] == '#'){
//遇到退格,slow就后退
slow = slow > 0 ? --slow : slow;
fast++;
}
while (fast < length && charArray[fast] != '#'){
//覆盖掉被退格删除的字符
charArray[slow++] = charArray[fast++];
}
}else{
slow++;
fast++;
}
}
return new String(charArray).substring(0,slow);
}
}
1498. 满足条件的子序列数目
首先要清楚:对于选取的子序列,我们只需要关注其中的最大元素和最小元素,而不需要考虑其中元素的相对位置,所以我们可以先变治,排序简化问题
那么思路就是:对数组 排序,然后使用双指针枚举每个 最小元素 + 最大元素 小于等于 target 的区间,然后在这个区间中选取子序列,只要其中的最小元素一定被选,那么其它元素选不选,得到的子序列都是满足条件,所以如果区间长度为 l ,那么该区间可以得到的子序列数就是 2l-1 (区间长度为 l,最小元素必选,剩下的 l - 1 个元素可选可不选,每一个有选跟不选两种做法,根据组合公式有 2l-1 种做法)
public int numSubseq(int[] nums, int target) {
Arrays.sort(nums); //先排序
if (nums[0] * 2 > target) { //如果最小元素的两倍都大于target那就肯定找不到符合条件的区间
return 0;
}
int left = 0;
int right = nums.length - 1;
int res = 0;
int[] pow = new int[nums.length]; //维护2次幂数组,pow[i] = 2的i次幂
pow[0] = 1;
int modNum = 1_000_000_007;
for (int i = 1; i < nums.length; i++) {
pow[i] = pow[i - 1] * 2;
pow[i] %= modNum; //先取模防止溢出
}
while (left <= right) {
if (nums[left] + nums[right] <= target) {
res += pow[right - left];
res %= modNum;
left++; //以left为最小元素的子序列已被计算过,left右移
}else {
//最小最大元素和太大,要让其变小,right左移
right--;
}
}
return res;
}
在官方题解中看到下面两个公式,拿过来记录一下:
658. 找到 K 个最接近的元素
要找到 k 个最接近的元素,反过来就是要去除掉 len - k 个最不接近的元素,所以可以用头尾双指针,l 初始指向 arr[0],r 初始指向 arr[len - 1],然后判断 l 跟 r 对应元素谁更靠近 x,如果 l 更靠近,就 r–;否则 l++,判断 len - k 次之后,[l,r] 中的元素就是最接近 x 的 K 个元素
public List<Integer> findClosestElements(int[] arr, int k, int x) {
if(k == 0) return new LinkedList<>();
int len = arr.length;
int count = len - k;
int l = 0;
int r = len - 1;
for(int i = 0;i < count;i++){
if(Math.abs(arr[l] - x) <= Math.abs(arr[r] - x)){
r--;
}else{
l++;
}
}
List<Integer> list = new LinkedList<>();
for(int i = l;i <= r;i++){
list.add(arr[i]);
}
return list;
}
986. 区间列表的交集
双指针遍历 A,B 每一个区间,然后取交集,但是不能每遍历完一对区间就都考虑下一对区间,不能两个指针都前移,不能认为两个区间列表就是每一对区间得到一个交集,因为考虑到可能某个数组中有一个大区间,其中包含了另一个数组中多个区间,这样这个 大区间就会参与到多个交集中。
所以要判断,如果某个数组中当前区间的右边界大于另一个数组中当前区间的右边界,就不前移这个数组的指针。注意到每个区间列表中区间都是不相交的,所以不用担心区间列表中任意两个区间会出 现在同一个交集中
public int[][] intervalIntersection(int[][] A, int[][] B) {
List<int[]> ans = new LinkedList<>();
int i = 0, j = 0;
while (i < A.length && j < B.length) {
//取交集
int lo = Math.max(A[i][0], B[j][0]);
int hi = Math.min(A[i][1], B[j][1]);
//要判断左右边界合法性
if (lo <= hi) ans.add(new int[]{lo, hi});
//判断哪个指针要前移
if (A[i][1] < B[j][1]) i++;
else j++;
}
return ans.toArray(new int[0][]);
}
11. 盛最多水的容器
头尾双指针 l 及 r,每次两个指针之间所能盛的水量为 Math.min(height[l],height[r]) * (r - l)
移动指针时,如果 左指针指向的高度小于右指针,那么左指针右移一步;如果右指针指向的高度小于左指针,右指针左移一步;两个指针指向的高度相等时移动哪个指针都可以,这里我只移动左指针
直到左右指针重合时,计算过的所有可盛水量中的最大值就是本题答案
public int maxArea(int[] height) {
intlen = height.length;
int max = -1;
int l = 0,r = len - 1;
while(l < r){
max = Math.max( max, Math.min(height[l],height[r]) * (r - l) );
if(height[l] <= height[r]){
l++;
}else{
r--;
}
}
return max;
}
9. 回文数
首先负数肯定不可能是回文数。对于有 i 位的正数,根据题意,依次比较第 0 位跟 第 i - 1 位,第 1 位跟第 i - 2 位,…,如果每次比较都相同,说明 x 是个回文数,否则一旦有一次比较不相同都能直接说明 x 不是回文数。为了方便比较,将 x 转化为字符串,使用头尾双指针进行比较
public boolean isPalindrome(int x) {
if(x < 0){
return false;
}
//转化成字符数组方便比较
String s = String.valueOf(x);
char[] chars = s.toCharArray();
int i = 0,j = chars.length - 1;
while (i < j){
if(chars[i] != chars[j]){
return false;
}
i++;
j--;
}
return true;
}
剑指 Offer 21. 调整数组顺序使奇数位于偶数前面
左指针指向最左边,右指针指向最右边,如果左指针指向的数为奇数,则左指针右移,直到指向偶数;如果右指针指向的数为偶数,则右指针左移,直到指向奇数,然后将两个指针指向的数交换。然后继续循环,直到左右指针重合
public int[] exchange(int[] nums) {
int l = 0,r = nums.length - 1;
while(l < r){
while(l < r && (nums[l] & 1) == 1) l++;
while(l < r && (nums[r] & 1) == 0) r--;
if(l < r){
nums[l] ^= nums[r];
nums[r] ^= nums[l];
nums[l] ^= nums[r];
}
}
return nums;
}
75. 颜色分类
参与排序的值的取值只有三个,直接使用常规的排序算法肯定是小题大做,更好的做法应该是使用双指针
用一个指针初始指向最左边,标记 0 所在的位置;另一个指针从左遍历到右,当遇到 0 时就放到第一个指针的位置,然后第一个指针右移一个单位。当第二个指针遍历到末尾时,数组的最左边就都是 0 了。那么同理,用两个指针从数组最右边开始移动,就能使数组的最右边都是 2。
但这种做法需要遍历两遍数组。下面这种做法只需要遍历一遍:
一个指针 p1 指向最左边,标记 0 的位置;一个指针 p2 指向最右边,标记 2 的位置,用一个指针从左往右遍历,判断指针指向的值,是 0 就换到 p1 的位置,然后 p1 右移;是 1 就换到 p2 的位置,然后 p2 左移
基本思路是这样,但存在几个问题:
首先,指针是从左到右的,因此我们可以保证 p1 的左边肯定全是 0,且 p1 的右边直到指针的左边肯定都是 1,所以跟 p1 进行交换的话指针所指的位置肯定会是 1,此时指针可以右移;但 p2 指向的值具有不确定性,跟 p2 进行交换的话指针指向的可能是 0,1,2 任意一个数,所以如果跟 p2 进行交换的话需要循环判断指针指向的数是不是还是 2,是的话需要继续跟 p2 交换,直到指针指向的不是 2,那么此时指向的就有可能是 0 或 1,所以指针不能直接右移,否则如果指向的是 0 就会导致这个 0 没有出现在左边。所以应该先判断是不是 2,然后再判断是不是 0,而且判断是不是 2 时需要循环判断
第二,p2 的右边肯定都是 2,但 p2 指向的值不确定,所以指针的右边界为 p2,即i <= p2。但是在判断指针指向的是不是 2 时只需要 i < p2 即可,因为如果 i == p2 && nums[i] == 2,i 跟 p2 是不用交换的,如果使用的是三步异或法进行交换的话,这一步自己跟自己异或会导致数变为 0,会出错
class Solution {
public void sortColors(int[] nums) {
int p1 = 0,p2 = nums.length - 1;
for(int i = 0;i <= p2;i++){ //这里是i <= p2
while(i < p2 && nums[i] == 2){ //这里是i < p2
swap(nums,p2,i);
p2--;
}
if(nums[i] == 0){
swap(nums,p1,i);
p1++;
}
}
}
public void swap(int[] nums,int i,int j){
nums[j] ^= nums[i];
nums[i] ^= nums[j];
nums[j] ^= nums[i];
}
}