双指针简析(二分法|快慢指针|滑动窗口)
一、简述
应用场景:
双指针通常出现在从一个长序列中寻找目标子序列(子节点、或者某种特殊状态)的问题之中。对于此类问题如果采用双循环的方式,我们通常也能达成目标结果,但是需要开销的时间复杂度往往是
O
(
n
2
)
O({n^2})
O(n2) , 在一些有运算时间限制的问题中是不能满足条件的, 而通过二指针的方法,可以将时间复杂度降低到
O
(
n
)
O(n)
O(n) 。
原理:
双指针简单来说就是在一个长序列中设置两个指针,其中一个指针作为左边界(端点), 另一个作为右边界(端点)让它们共同包裹出一个区间。不断地去维护这两根指针(左右移动两根指针),使得在被包裹出的子区间中(子节点)能够在最大程度上满足我们所想要达成的某种特定条件。
二、双指针问题细化
双指针通常有三种比较常见的形式,分别是:二分法,快慢指针, 滑动窗口。本着具体问题,具体分析的形式。对于以上的每种形式, 在此处都通过一道在leetcode上的题目来分析。
1. 普通双指针(二分法)
题目描述
给定一个排序数组和一个目标值,在数组中找到目标值,并返回其索引。
如果目标值不存在于数组中,返回它将会被按顺序插入的位置。
请必须使用时间复杂度为 O ( l o g n ) O({log n}) O(logn) 的算法。
解题步骤:
本道题题目实际上就是典型的二分法,具体步骤如下
-
设置左右指针分别为left 和 right, 分别指向数组的俩个端点。
-
获取中间指针: mid =(left + right)/ 2
-
判断 mid 所指向的数值,条件如下:
(1) 如果 mid 所指向的数值大于目标值,那么 left 指针指向 mid + 1
(2) 如果 mid 所指向的数值小于目标值,那么 right 指针指向 mid - 1
(3) 如果 mid 所指向的数值恰好是目标值, 则返回 mid
-
重复 2,3步直到 left 指针 越过了 right 指针 (left > right), 表示目标不存在于数组之中。
tips: 本题考察的一点就是,如果搜索的目标不存在于数组之中, 那么该目标所要插入的位置, 恰恰是二分法寻找失败后 left 指针所指向的地址。
简单的实现代码如下(递归的方式)
var searchInsert = function(nums, target) {
let binarySearch = (left, right) => {
if(left > right)
return left;
let mid = parseInt((left + right) / 2);
if(nums[mid] == target)
return mid;
return (nums[mid] > target) ? binarySearch(left, mid - 1) : binarySearch(mid + 1, right)
}
return binarySearch(0, nums.length - 1)
};
2. 快慢指针
简单描述|应用场景
快慢指针,一般被应用于寻找目标串中的某个特定位置如中间节点 或者 是解决链表环路问题之中。(快慢指针主要用于存储地址不连续的情况,即无法直接用下标检索目标数据的情况)
题目描述
给定一个头结点为 head
的非空单链表,返回链表的中间结点。
如果有两个中间结点,则返回第二个中间结点。
解题步骤
1. 分别设置两个指针slow 和 fast
2. 每次让 fast 指针移动两个步长, slow 指针移动一个步长
3. 重复执行第二步,直到fast指针的下一个节点或者下下个节点为空
4. 返回 slow 指针
tips: 本题目中, 有一个雷点, 就是需要判断长序列是奇数还是偶数。
简单的实现如下
var middleNode = function(head) {
let fast, slow;
for(fast = head,slow = head; fast.next && fast.next.next; fast = fast.next.next, slow = slow.next);
return (fast.next) ? slow.next:slow;
};
3 滑动窗口
简单描述|应用场景
滑动窗口, 主要被应用于寻找最优目标子序列的情况下。
其思路如下:
1. 设置两个左右指针, 分别指向长序列初始位置。
2. 不断移动右指针, 来扩大子序列所包裹的范围
3. 当子序列所包裹的范围无法满足目标条件时, 调整右指针位置, 直到子序列恢复到满足目标条件的状态
4. 重复执行2,3步骤, 直到右指针移动到长序列的中止位置。
题目描述
给定一个字符串 s
,请你找出其中不含有重复字符的 最长子串 的长度。
解题步骤
-
设置左右指针, 使它们同时指向第一个元素, 将首元素存进set之中。
-
移动右指针, 如果 set中存在右指针所指向的元素, 执行第 3 步, 否则执行第 4 步。
-
移动左指针, 并在 set中删除 左指针所指向的元素, 直到 set 中不存在右指针所指向的元素,然后将右指针所指向的元素
4. 移动右指针, 将右指针所指向的元素添加进 set 容器之中。
- 重复 2 — 4 步骤, 直到遍历完一遍整个数组。
执行流程
原数组: d => a => c => a => b => c => d
右指针:#
左指针:#
第一次执行到 步骤3 时的状态:
原数组: d => a => c => a => b => c => d
右指针:# => #=> # => #
左指针:#
被左右指针包裹的子数组: d, a, c, a
执行 步骤3:
原数组: d => a => c => a => b => c => d
右指针:# => # => # => #
左指针:# => # => #
被左右指针包裹的子数组:c, a
简单的实现如下:
var lengthOfLongestSubstring = function(s) {
if(!s||s == "")
return 0;
if(s.length == 1)
return 1;
let left = 0,
right = 0,
set = new Set(), count = 0, max = 0;
while(s[right]) {
if(set.has(s[right])) {
// 移动左指针
while(set.has(s[right])) {
set.delete(s[left++])
count--;
}
// 将右指针当前元素并入set
set.add(s[right])
count++;
}
else {
set.add(s[right])
count++;
}
if(count > max)
max = count;
right++;
}
return max;
};
三、其他二指针问题|题目解析
四 、总结
综合上述分析, 我们可以发现, 快慢指针 和 滑动窗口 和 普通二指针方法是略有区别的, 其区别在于普通二指针方法对于左右指针的移动比较随意, 而 快慢指针 和 滑动指针在对左右指针的移动上有着一定的约束。 普通的二指针方法(特指二分法)往往不需要遍历整个长串,复杂度较低, 而后续两种方法往往都要完成一次完整的遍历。
当然,它们都非常适合被应用于在一个序列中匹配最优子序列的问题之中!
题目来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/search-insert-position
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。