概述
概念
双指针主要用于遍历数组,两个指针可以相向而行或同向而行。双指针主要用于序列、链表这样的线性结构,和滑动窗口一样,是解决一类问题的常用和有效的技巧。
优势
- 提高效率:双指针可以帮助我们以线性时间复杂度解决问题,避免了使用嵌套循环等导致时间复杂度膨胀的操作。
- 降低复杂度:将复杂问题拆解为通过移动指针就可以解决的问题,使问题解决方案更容易理解和实现。
- 优化空间:双指针只需要常量级别的空间,不需要额外的存储空间。
适用场景
- 对撞指针:对于有序的数组或者链表,可以让左右两个指针向中间靠拢,寻找目标值,如“两数之和”、“反转链表”等问题。
- 快慢指针:可以用于检测链表中是否存在环、寻找链表的中间节点或者寻找环的交点等问题。
- 滑动窗口:在解决需要连续子序列的最大值或者满足某个条件的所有连续子序列等问题时,可以使用双指针作为滑动窗口的两个边界。
刷题
移动零
- 思路: 双指针大法,设置快慢两个指针,如果快指针对应的值不为0,则将其于慢指针交换,慢指针 index+1
- 时间复杂度: O(n),其中 n 是输入数组 nums 的长度
- 空间复杂度: O(1),主要由常数级别的辅助空间决定
/**
* @param {number[]} nums
* @return {void} Do not return anything, modify nums in-place instead.
*/
var moveZeroes = function(nums) {
const len = nums.length
if(len <= 1) return nums
let slow = 0
for(let fast = 0; fast < len; fast++){
if(nums[fast] !== 0){
[nums[slow], nums[fast]] = [nums[fast], nums[slow]]
slow++
}
}
return nums
};
盛最多水的容器
- 思路: 设置两个首尾指针,以其是否相遇作为跳出条件;关键点在于如何移动这两个指针:即始终移动当前值较小的那个指针。
- 时间复杂度: O(n),其中 n 是输入数组 height 的长度。
- 空间复杂度: O(1),主要由常数级别的辅助空间决定。
/**
* @param {number[]} height
* @return {number}
*/
var maxArea = function(height) {
const len = height.length
let first = 0
let last = len - 1
let maxValue = 0
while(first < last){
const current = (last - first) * Math.min(height[first], height[last])
maxValue = Math.max(current, maxValue)
if(height[first] < height[last]){
first++
}else{
last--
}
}
return maxValue
};
三数之和
思路:
- 这题还是折腾了我很久的,依旧是双指针大法,先对数组进行升序排序,之后最外层for循环,以这个index值作为起始值,再在此基础上设置两首尾指针,根据这三个值的和判断指针处理方式;
- 当sum等于0时,将此时的三个值push到res数组去,再同时令首尾指针各移动一位(因为此时的和已经为0了,如果只移动其中一个指针,则在数组已排序的基础上,新的三数之和必然不为0或者是已经重复出现过的和为0的情况);
- 当sum大于或者小于0时,分别让尾指针减一或者头指针加一即可;
- 对于题目中要求的不能出现重复的三元组条件,最开始想到的是通过Set结构对三个值组成的字符串key进行判断,如果已经存在这个key值了,就不用再重复向res数组添加了;
- 另一种更好些的优化方式是通过循环判断当前指针与其下一个指针指向的值是否相等,如果相等,则跳过当前的元素,同样的,在进入最外层循环的时候也可以用这种方法来避免重复计算;
时间复杂度:
- 排序的时间复杂度为 O(n log n),其中 n 是输入数组的长度。
- 外层循环遍历数组,时间复杂度为 O(n)。
- 内层循环使用双指针法,时间复杂度为 O(n)。
- 总体时间复杂度为 O(n log n) + O(n) * O(n) = O(n^2)。
空间复杂度:
- 结果数组 res 占用的空间为 O(n)。
- 没有使用额外的数据结构,空间复杂度为 O(1)。
- 因此,该解法的总体空间复杂度为 O(n)。
Tips: 还有一点需要强调的是,在使用sort排序时一定一定一定要写完整形式!!!!!!!nums.sort()和nums.sort((a,b)=>a-b)的效果是不一样的,因为习惯了偷懒使用前者方式,导致有的测试用例一直过不了{{{(>_<)}}} 两者的区别在于排序的方式:
- nums.sort((a, b) => a - b): 这是使用自定义比较函数进行排序,比较函数的逻辑是按照数字的升序排列。(a, b) => a - b 表示如果 a 小于 b,返回负数;如果 a 等于 b,返回零;如果 a 大于 b,返回正数。这样就实现了按数字升序排列。
- nums.sort(): 这是使用默认的比较函数进行排序。默认的比较函数将元素转换为字符串,然后按照字符串的 Unicode 代码单元值进行比较。这意味着它执行的是字典排序,而不是数字排序。对于数字排序来说,可能不会按照预期的升序排列。
- 在处理数字数组时,推荐使用 nums.sort((a, b) => a - b),以确保按照数字的升序进行排序。这样可以避免一些意外的排序结果。
/**
* @param {number[]} nums
* @return {number[][]}
*/
var threeSum = function(nums) {
nums.sort((a, b) => a - b)
const res = []
const len = nums.length
// const mySet = new Set()
for(let i = 0; i < len - 2; i++){
if(i > 0 && nums[i] === nums[i - 1]){
// 避免重复计算
continue
}
let first = i + 1
let last = len - 1
while(first < last){
const sum = nums[i] + nums[first] + nums[last]
if(sum > 0) last--
else if(sum < 0) first++
else{
// 去重
/* const setKey = `${nums[first]}-${nums[i]}-${nums[last]}`
if(!mySet.has(setKey)){
mySet.add(setKey)
res.push([nums[i], nums[first], nums[last]])
} */
res.push([nums[i], nums[first], nums[last]])
// 跳过相同的元素
while(first < last && nums[first] === nums[first + 1]){
first++
}
while(first < last && nums[last] === nums[last - 1]){
last--
}
first++
last--
}
}
}
return res
};
接雨水
- 思路: 函数的主要思路是使用两个指针,一个从头开始,一个从尾开始。首先,更新两个指针对应的最大高度。然后,比较两个指针处的高度,低的那个可以盛放的雨水就是当前最大高度减去该位置的高度。比较过后就移动对应的指针,直到两个指针相遇。
- 时间复杂度:每个元素只需要遍历一次,所以时间复杂度为 O(n),其中 n 是数组的长度。
- 空间复杂度:没有使用到额外的数据结构,除了输入数据外,空间复杂度为 O(1)。
/**
* @param {number[]} height
* @return {number}
*/
var trap = function(height) {
const len = height.length
let res = 0
// 首尾指针对应的最大值
let firstMax = 0
let lastMax = 0
// 首尾指针位置
let firstIndex = 0
let lastIndex = len - 1
while(firstIndex < lastIndex){
firstMax = Math.max(firstMax, height[firstIndex])
lastMax = Math.max(lastMax, height[lastIndex])
if(firstMax < lastMax){
res += firstMax - height[firstIndex]
firstIndex++
}else{
res += lastMax - height[lastIndex]
lastIndex--
}
}
return res
};
验证回文串
- 思路: 依旧是左右双指针,需要特殊处理的是数据,通过自定义函数依据ASIIC码大小将不符合数字和字母要求的剔除
- 时间复杂度:在循环中,双指针分别从字符串的两端向中间移动,遍历整个字符串,复杂度为 O(n)
- 空间复杂度:没有使用额外的数据结构,空间复杂度为 O(1)
/**
* @param {string} s
* @return {boolean}
*/
var isPalindrome = function(s) {
if(!s) return true
const newStr = s.toLowerCase()
let left = 0
let right = newStr.length - 1
while(left < right){
if(!isValid(newStr[left])){
left++
continue
}
if(!isValid(newStr[right])){
right--
continue
}
if(newStr[left] !== newStr[right]){
return false
}
left++
right--
}
function isValid(str){
return str >= 'a' && str <= 'z' || str >= '0' && str <= '9'
}
return true
};
判断子序列
- 思路: 采用快慢指针的方式,分别代表s和t的下标,开启一个循环,跳出条件为当其中任意一个字符串遍历完成,在这个过程中只有t中出现了和s当前相同的字符慢指针才可加一,最后判断但判断慢指针是否到达s字符串的末端
- 时间复杂度:O(n)
- 空间复杂度:O(1)
/**
* @param {string} s
* @param {string} t
* @return {boolean}
*/
var isSubsequence = function(s, t) {
let slow = 0
let fast = 0
while(slow < s.length && fast < t.length){
if(s[slow] === t[fast]){
slow++
}
fast++
}
return slow === s.length
};
两数之和 II - 输入有序数组
思路: 左右两指针,依据题目要求,本来已经是排好序的了,故只需比较左右两指针对应的值,如果和小于 target,说明左边的元素较小,需要向右移动以增大和;否则,如果和大于 target,说明右边的元素较大,需要向左移动以减小和。
时间复杂度:O(n)
空间复杂度:O(1)
/**
* @param {number[]} numbers
* @param {number} target
* @return {number[]}
*/
var twoSum = function(numbers, target) {
const res = []
let left = 0
let right = numbers.length - 1
while(left < right){
if(numbers[left] + numbers[right] < target){
left++
}else if(numbers[left] + numbers[right] > target){
right--
}
else {
res.push(left + 1, right + 1)
return res
}
}
return res
};