双指针算法详解 - LeetCode 27/26/283/844/977 题解
LeetCode 27.移除元素
题目描述
给你一个数组 nums
和一个值 val
,你需要 原地 移除所有数值等于 val
的元素。元素的顺序可能发生改变。然后返回 nums
中与 val
不同的元素的数量。
假设 nums
中不等于 val
的元素数量为 k
,要通过此题,您需要执行以下操作:
- 更改
nums
数组,使nums
的前k
个元素包含不等于val
的元素。nums
的其余元素和nums
的大小并不重要。 - 返回
k
。
示例 1:
输入:
nums = [3,2,2,3]
,val = 3
输出:2
,nums = [2,2,_,_]
解释: 你的函数函数应该返回 k = 2, 并且 nums 中的前两个元素均为 2。你在返回的 k 个元素之外留下了什么并不重要(因此它们并不计入评测)。
示例 2:
输入:
nums = [0,1,2,2,3,0,4,2]
,val = 2
输出:5
,nums = [0,1,4,0,3,_,_,_]
解释: 你的函数应该返回 k = 5,并且 nums 中的前五个元素为 0,0,1,3,4。注意这五个元素可以任意顺序返回。
提示:
- $0 \le {nums.length} \le 100$
- $0 \le {nums[i]} \le 50$
- $0 \le {val} \le 100$
思路
题目要求删除数组中等于 val 的元素,因此输出数组的长度一定小于等于输入数组的长度,我们可以把输出的数组直接写在输入数组上。数组的元素在内存地址中是连续的,不能单独删除数组中的某个元素,只能覆盖。
最容易想到的就是暴力解法,使用两个 for
循环,第一个 for
循环遍历数组元素 ,第二个 for
循环更新数组。
可以使用双指针进行优化:使用快指针和慢指针在一个 for
循环下完成两个 for
循环的工作
快慢指针的定义:
- 快指针:指向当前需要处理的元素
- 慢指针:指向下一个将要赋值的位置
如果快指针指向的元素不等于 val
,它一定是输出数组的一个元素,就将快指针指向的元素复制到慢指针的位置,快慢指针同时后移。
如果快指针指向的元素等于 val
,它不能在输出数组里,此时慢指针不动,快指针右移一位。
整个过程保持不变的性质是:区间 [0, slow)
中的元素都不等于 val
。
当快指针遍历完输入数组以后,slow
的值就是输出数组的长度。
C++代码
1 | class Solution { |
- 时间复杂度:
O(n)
- 空间复杂度:
O(1)
LeetCode 26.删除有序数组中的重复项
题目描述
给你一个 非严格递增排列 的数组 nums
,请你原地删除重复出现的元素,使每个元素 只出现一次 ,返回删除后数组的新长度。元素的 相对顺序 应该保持 一致 。然后返回 nums
中唯一元素的个数。
考虑 nums
的唯一元素的数量为 k
,你需要做以下事情确保你的题解可以被通过:
- 更改数组
nums
,使nums
的前k
个元素包含唯一元素,并按照它们最初在nums
中出现的顺序排列。nums
的其余元素与nums
的大小不重要。 - 返回
k
。
示例 1:
输入:
nums = [1,1,2]
输出:2
,nums = [1,2,_]
解释: 函数应该返回新的长度 2 ,并且原数组 nums 的前两个元素被修改为 1, 2 。不需要考虑数组中超出新长度后面的元素。
示例 2:
输入:
nums = [0,0,1,1,1,2,2,3,3,4]
输出:5
,nums = [0,1,2,3,4]
解释: 函数应该返回新的长度 5 , 并且原数组 nums 的前五个元素被修改为 0,1,2,3,4。不需要考虑数组中超出新长度后面的元素。
提示:
- $1 \le {nums.length} \le 3*10^{4}$
- $-10^4 \le {nums[i]} \le 10^4$
- $nums$ 已按 非严格递增 排列
思路
nums
已按 非严格递增 排列,说明重复元素一定是彼此相邻的,只需要保留一个即可。那么我们可以理解为每个非空数组的第一个元素一定的需要保留的,可以考虑一种极端情况 nums = [1,1,1,1,1]
,只需要保留数组中第一个即可。
同样可以使用双指针,只需要遍历一边数组即可:
- 快指针:指向当前需要处理的元素
- 慢指针:指向已经处理好的非重复元素的最后一个,这里与上一题有所不同,这里的有效区间是
[0, slow]
,这里是闭区间,所以最后需要返回的是slow + 1
如果快指针指向的元素不等于慢指针指向的元素 ,它一定是输出数组的一个元素,就将快指针指向的元素复制到慢指针的后一个位置,快慢指针同时后移。
如果快指针指向的元素等于慢指针指向的元素,说明该元素重复了,慢指针不动,快指针右移一位。
整个过程保持不变的性质是:区间 [0, slow]
中的元素都不重复。
当快指针遍历完输入数组以后,slow + 1
的值就是输出数组的长度。
C++代码
1 | class Solution { |
- 时间复杂度:
O(n)
- 空间复杂度:
O(1)
LeetCode 283.移动零
题目描述
给定一个数组 nums
,编写一个函数将所有 0
移动到数组的末尾,同时保持非零元素的相对顺序。
请注意 ,必须在不复制数组的情况下原地对数组进行操作。
示例 1:
输入:
nums = [0,1,0,3,12]
输出:[1,3,12,0,0]
示例 2:
输入:
nums = [0]
输出:[0]
提示:
- $1 \le {nums.length} \le 10^{4}$
- $-2^{31} \le {nums[i]} \le 2^{31} - 1$
思路
使用双指针,左指针指向当前已经处理好的序列的尾部,右指针指向待处理序列的头部。
右指针不断向右移动,若右指针指向非零数,则将左右指针对应的数交换,同时左指针右移。
注意到以下性质:
- 左指针左边均为非零数;
- 右指针左边直到左指针处均为零(左指针指向的可能是非零数也可能是零)
因此每次交换,都是将左指针的零与右指针的非零数交换,且非零数的相对顺序并未改变。
(极端情况,若数组中没有零,则每次交换的都是自己,相当于位置不变)
C++代码
1 | class Solution { |
- 时间复杂度:
O(n)
- 空间复杂度:
O(1)
LeetCode 844.比较含退格的字符串
题目描述
给定 s
和 t
两个字符串,当它们分别被输入到空白的文本编辑器后,如果两者相等,返回 true
。#
代表退格字符。
**注意:**如果对空文本输入退格字符,文本继续为空。
示例 1:
输入:
s = "ab#c", t = "ad#c"
输出:true
示例 2:
输入:
s = "ab##", t = "c#d#"
输出:true
示例 3:
输入:
s = "a#c", t = "b"
输出:false
提示:
- $1 \le {s.length, t.length} \le 200$
s
和t
只含有小写字母以及字符'#'
思路
**一个字符是否会被删掉,只取决于该字符后面的退格符,而与该字符前面的退格符无关。**因此当我们逆序地遍历字符串,就可以立即确定当前字符是否会被删掉。
定义 skip
表示当前待删除的字符的数量。每次我们遍历到一个字符:
- 若该字符为退格符,则我们需要多删除一个普通字符,让
skip
加1
- 若该字符为普通字符:
- 若
skip
为 0,则说明当前字符不需要删去; - 若
skip
不为 0,则说明当前字符需要删去,我们让skip
减1
。
- 若
定义两个指针,分别指向两字符串的末尾。每次我们让两指针逆序地遍历两字符串,直到两字符串能够各自确定一个字符,然后将这两个字符进行比较。重复这一过程直到找到的两个字符不相等,或遍历完字符串为止。
C++代码
1 | class Solution { |
- 时间复杂度:
O(N + M)
, 其中 N 和 M 分别为字符串 S 和 T 的长度。我们需要遍历两字符串各一次。 - 空间复杂度:
O(1)
LeetCode 977.有序数的平方
题目描述
给你一个按 非递减顺序 排序的整数数组 nums
,返回 每个数字的平方 组成的新数组,要求也按 非递减顺序 排序。
示例 1:
输入:
nums = [-4,-1,0,3,10]
输出:[0,1,9,16,100]
示例 2:
输入:
nums = [-7,-3,2,3,11]
输出:[4,9,9,49,121]
提示:
- $1 \le {nums.length} \le 10^4$
- $-10^4 \le {nums[i]} \le 10^4$
nums
已按 非递减顺序 排序
思路
使用两个指针分别指向位置 0
和 n−1
,每次比较两个指针对应的数,选择较大的那个逆序放入答案并移动指针。逆序插入
C++代码
1 | class Solution { |
- 时间复杂度:
O(n)
, 其中 n 是数组 nums 的长度。 - 空间复杂度:
O(1)
, 除了存储答案的数组以外,我们只需要维护常量空间。