【算法】【学习笔记】双指针
双指针
双指针是一种十分常用的算法方法,一般有滑动窗口、快慢指针等使用方式。
在下文就根据题目详细解释下。
❔题目
1. 环形链表
解题思路
是否有环是十分经典的快慢指针题目,就像龟兔赛跑一样。
只需要一个步长为1的slow指针,和一个步长为2的fast指针即可解决。
- 只要fast能走到终点,说明无环;
- 假如fast和slow相遇,说明有环。
证明:
假设有环,进入环后,fast相对于slow每轮靠近1个节点,
因此一定能够追上。
实现
function hasCycle(head: ListNode | null): boolean {
let slow = head, fast = head;
while(fast !== null && fast.next !== null){
slow = slow!.next; // TS类型需要非空断言,算法上slow一定存在
fast = fast.next.next;
if(slow === fast){
return true;
}
}
return false;
};
public class Solution {
public bool HasCycle(ListNode head) {
var slow = head;
var fast = head;
while(fast != null && fast.next != null){
slow = slow.next;
fast = fast.next.next;
if(slow == fast){
return true;
}
}
return false;
}
}
class Solution {
public:
bool hasCycle(ListNode *head) {
auto slow = head, fast = head;
while(fast != NULL && fast->next != NULL){
slow = slow->next;
fast = fast->next->next;
if(slow == fast){
return true;
}
}
return false;
}
};
复杂度分析
- 时间复杂度: \(O(N)\)
无环时快指针遍历 \(N\) 个节点;有环时快指针追上慢指针最多多走一圈(环的长度),总步数依然在 \(O(N)\) 级别。 - 空间复杂度: \(O(1)\)
仅使用了 slow 和 fast 两个节点指针,占用常数级别额外空间。
2. 最接近的三数之和
解题思路
一开始思考是否可以用滑动窗口,不过此题不需要连续,所以不可以用滑动窗口。
这时候可以用排序+双指针。
当定好第一个数 \(x\) 时,问题就转变为了 \(l+r\) 要尽可能接近 \(target-x\)。
首先,进行排序,排序的目的就是利用单调性,使双指针移动具有方向性。
其中,\(l\) 为 \(x\) 往后的第一个数,\(r\) 为数组的最后一个数:
- 当 \(l+r+x < target\) ,说明数值需要增大,由于已经排序了,即 \(l\) 往右移即可;
- 当 \(l+r+x > target\) ,说明数值需要缩小,由于已经排序了,即 \(r\) 往左移即可;
- 当 \(l+r+x = target\) ,说明已经是最优解,返回即可。
实现
function threeSumClosest(nums: number[], target: number): number {
// 先排序了,定好一个数字,然后双指针找最近target的
nums.sort((a,b)=>a-b);
let t = nums[0] + nums[1] + nums[2];
const n = nums.length;
for(let i = 0; i < n-2; i++){
const x = nums[i];
// 当当前数与上次数一样,效果不会有变化的,跳过
if (i > 0 && x === nums[i - 1]) continue;
let l = i+1, r = n-1;
while(l < r){
const cur = nums[l] + nums[r] + nums[i];
if(cur === target){
return target;
}
if(Math.abs(target - t) > Math.abs(target - cur)){
t = cur;
}
if(cur < target){
l++;
}else{
r--;
}
}
}
return t;
};
public class Solution {
public int ThreeSumClosest(int[] nums, int target) {
Array.Sort(nums);
int n = nums.Length;
if(n < 3) return -1;
int res = nums[0]+nums[1]+nums[2];
for(int i = 0; i < n-2; i++){
int num = nums[i];
if(i > 0 && num == nums[i-1]) continue;
int l = i + 1, r = n - 1;
while(l < r){
int cur = nums[l] + nums[r] + num;
if(cur == target){
return target;
}
if(Math.Abs(target-cur) < Math.Abs(target-res)){
res = cur;
}
if(cur < target){
l++;
}else{
r--;
}
}
}
return res;
}
}
class Solution {
public:
int threeSumClosest(vector<int>& nums, int target) {
int n = nums.size();
if(n < 3) return -1;
sort(nums.begin(), nums.end());
int res = nums[0]+nums[1]+nums[2];
for(int i = 0; i < n-2; i++){
int num = nums[i];
if (i > 0 && num == nums[i - 1]) continue;
int l = i+1, r = n-1;
while(l < r){
int cur = nums[l] + nums[r] + num;
if(cur == target){
return target;
}
if(abs(target-cur)<abs(target-res)){
res = cur;
}
if(cur < target){
l++;
}else{
r--;
}
}
}
return res;
}
};
复杂度分析
- 时间复杂度: \(O(N^2)\)
数组排序需要 \(O(N \log N)\),外层循环 \(O(N)\) 配合内层双指针 \(O(N)\) 的总耗时为 \(O(N^2)\)。综合时间复杂度为 \(O(N \log N) + O(N^2) = O(N^2)\)。 - 空间复杂度: \(O(1)\)
没有使用额外的非常数级空间,只是使用了左右指针。
3. 神奇字符串
解题思路
神奇字符串是一个无限序列,题目只要求它的前 n 个字符。
因此我们只需要模拟生成过程直到长度达到 n。
可以看出1和2都至多只能连续出现2次。
我们可以使用一边构造神奇字符串,一边对1计数来获得答案。
构造的方法可以先构造出默认的最初状态s='122',然后每次插入的数字num为1和2的轮换——即插入1后,下一次插入2:
- 假如读取到的 s[i] 为 2,说明当前数字 num 需要连续插入 2 次;
- 假如读取到的 s[i] 为 1,说明当前数字 num 需要连续插入 1 次。
实现
function magicalString(n: number): number {
// 由此可以知道,1和2的连续范围也只在[1,2]
if(n <= 0) return 0;
if(n <= 3) return 1;
// 初始化种子字符串和指针
let res = 1;
const s = [1,2,2];
let i = 2;
let num = 1; // 下一个要填的数字,交替出现,填了1就变2,填了2就变1。
while(s.length < n){
const count = s[i]; // 决定当前数字要加几次
for(let j = 0; j < count && s.length < n; j++){
s.push(num);
if(num === 1){
res++;
}
}
num = 3 - num; // 2变1,1变2
i++; // 检查下一个
}
return res;
};
public class Solution {
public int MagicalString(int n) {
if(n <= 0) return 0;
if(n <= 3) return 1;
List<int> s = [1,2,2];
int res = 1;
int i = 2;
int num = 1;
while(s.Count < n){
int count = s[i++];
for(int j = 0; j < count && s.Count < n; j++){
s.Add(num);
if(num == 1){
res++;
}
}
num = 3 - num;
}
return res;
}
}
class Solution {
public:
int magicalString(int n) {
if(n <= 0) return 0;
if(n <= 3) return 1;
vector<int> s = {1,2,2};
int res = 1, i = 2, num = 1;
while(s.size() < n){
int count = s[i++];
for(int j = 0; j < count && s.size() < n; j++){
s.push_back(num);
if(num == 1){
res++;
}
}
num = 3 - num;
}
return res;
}
};
复杂度分析
- 时间复杂度: \(O(N)\)
每个字符最多被生成一次,因此生成前 n 个字符需要 O(n) 时间。 - 空间复杂度: \(O(N)\)
需要一个长度为n的数组存储神奇字符串。
4. 神奇字符串
解题思路
神奇字符串是一个无限序列,题目只要求它的前 n 个字符。
因此我们只需要模拟生成过程直到长度达到 n。
可以看出1和2都至多只能连续出现2次。
我们可以使用一边构造神奇字符串,一边对1计数来获得答案。
构造的方法可以先构造出默认的最初状态s='122',然后每次插入的数字num为1和2的轮换——即插入1后,下一次插入2:
- 假如读取到的 s[i] 为 2,说明当前数字 num 需要连续插入 2 次;
- 假如读取到的 s[i] 为 1,说明当前数字 num 需要连续插入 1 次。
实现
function magicalString(n: number): number {
// 由此可以知道,1和2的连续范围也只在[1,2]
if(n <= 0) return 0;
if(n <= 3) return 1;
// 初始化种子字符串和指针
let res = 1;
const s = [1,2,2];
let i = 2;
let num = 1; // 下一个要填的数字,交替出现,填了1就变2,填了2就变1。
while(s.length < n){
const count = s[i]; // 决定当前数字要加几次
for(let j = 0; j < count && s.length < n; j++){
s.push(num);
if(num === 1){
res++;
}
}
num = 3 - num; // 2变1,1变2
i++; // 检查下一个
}
return res;
};
public class Solution {
public int MagicalString(int n) {
if(n <= 0) return 0;
if(n <= 3) return 1;
List<int> s = [1,2,2];
int res = 1;
int i = 2;
int num = 1;
while(s.Count < n){
int count = s[i++];
for(int j = 0; j < count && s.Count < n; j++){
s.Add(num);
if(num == 1){
res++;
}
}
num = 3 - num;
}
return res;
}
}
class Solution {
public:
int magicalString(int n) {
if(n <= 0) return 0;
if(n <= 3) return 1;
vector<int> s = {1,2,2};
int res = 1, i = 2, num = 1;
while(s.size() < n){
int count = s[i++];
for(int j = 0; j < count && s.size() < n; j++){
s.push_back(num);
if(num == 1){
res++;
}
}
num = 3 - num;
}
return res;
}
};
复杂度分析
- 时间复杂度: \(O(N)\)
每个字符最多被生成一次,因此生成前 n 个字符需要 O(n) 时间。 - 空间复杂度: \(O(N)\)
需要一个长度为n的数组存储神奇字符串。
5. 神奇字符串
解题思路
神奇字符串是一个无限序列,题目只要求它的前 n 个字符。
因此我们只需要模拟生成过程直到长度达到 n。
可以看出1和2都至多只能连续出现2次。
我们可以使用一边构造神奇字符串,一边对1计数来获得答案。
构造的方法可以先构造出默认的最初状态s='122',然后每次插入的数字num为1和2的轮换——即插入1后,下一次插入2:
- 假如读取到的 s[i] 为 2,说明当前数字 num 需要连续插入 2 次;
- 假如读取到的 s[i] 为 1,说明当前数字 num 需要连续插入 1 次。
实现
function magicalString(n: number): number {
// 由此可以知道,1和2的连续范围也只在[1,2]
if(n <= 0) return 0;
if(n <= 3) return 1;
// 初始化种子字符串和指针
let res = 1;
const s = [1,2,2];
let i = 2;
let num = 1; // 下一个要填的数字,交替出现,填了1就变2,填了2就变1。
while(s.length < n){
const count = s[i]; // 决定当前数字要加几次
for(let j = 0; j < count && s.length < n; j++){
s.push(num);
if(num === 1){
res++;
}
}
num = 3 - num; // 2变1,1变2
i++; // 检查下一个
}
return res;
};
public class Solution {
public int MagicalString(int n) {
if(n <= 0) return 0;
if(n <= 3) return 1;
List<int> s = [1,2,2];
int res = 1;
int i = 2;
int num = 1;
while(s.Count < n){
int count = s[i++];
for(int j = 0; j < count && s.Count < n; j++){
s.Add(num);
if(num == 1){
res++;
}
}
num = 3 - num;
}
return res;
}
}
class Solution {
public:
int magicalString(int n) {
if(n <= 0) return 0;
if(n <= 3) return 1;
vector<int> s = {1,2,2};
int res = 1, i = 2, num = 1;
while(s.size() < n){
int count = s[i++];
for(int j = 0; j < count && s.size() < n; j++){
s.push_back(num);
if(num == 1){
res++;
}
}
num = 3 - num;
}
return res;
}
};
复杂度分析
- 时间复杂度: \(O(N)\)
每个字符最多被生成一次,因此生成前 n 个字符需要 O(n) 时间。 - 空间复杂度: \(O(N)\)
需要一个长度为n的数组存储神奇字符串。
6. 神奇字符串
解题思路
神奇字符串是一个无限序列,题目只要求它的前 n 个字符。
因此我们只需要模拟生成过程直到长度达到 n。
可以看出1和2都至多只能连续出现2次。
我们可以使用一边构造神奇字符串,一边对1计数来获得答案。
构造的方法可以先构造出默认的最初状态s='122',然后每次插入的数字num为1和2的轮换——即插入1后,下一次插入2:
- 假如读取到的 s[i] 为 2,说明当前数字 num 需要连续插入 2 次;
- 假如读取到的 s[i] 为 1,说明当前数字 num 需要连续插入 1 次。
实现
function magicalString(n: number): number {
// 由此可以知道,1和2的连续范围也只在[1,2]
if(n <= 0) return 0;
if(n <= 3) return 1;
// 初始化种子字符串和指针
let res = 1;
const s = [1,2,2];
let i = 2;
let num = 1; // 下一个要填的数字,交替出现,填了1就变2,填了2就变1。
while(s.length < n){
const count = s[i]; // 决定当前数字要加几次
for(let j = 0; j < count && s.length < n; j++){
s.push(num);
if(num === 1){
res++;
}
}
num = 3 - num; // 2变1,1变2
i++; // 检查下一个
}
return res;
};
public class Solution {
public int MagicalString(int n) {
if(n <= 0) return 0;
if(n <= 3) return 1;
List<int> s = [1,2,2];
int res = 1;
int i = 2;
int num = 1;
while(s.Count < n){
int count = s[i++];
for(int j = 0; j < count && s.Count < n; j++){
s.Add(num);
if(num == 1){
res++;
}
}
num = 3 - num;
}
return res;
}
}
class Solution {
public:
int magicalString(int n) {
if(n <= 0) return 0;
if(n <= 3) return 1;
vector<int> s = {1,2,2};
int res = 1, i = 2, num = 1;
while(s.size() < n){
int count = s[i++];
for(int j = 0; j < count && s.size() < n; j++){
s.push_back(num);
if(num == 1){
res++;
}
}
num = 3 - num;
}
return res;
}
};
复杂度分析
- 时间复杂度: \(O(N)\)
每个字符最多被生成一次,因此生成前 n 个字符需要 O(n) 时间。 - 空间复杂度: \(O(N)\)
需要一个长度为n的数组存储神奇字符串。
注:本文为个人学习与刷题笔记,部分文本结构与排版格式由 AI 辅助整理。

浙公网安备 33010602011771号