《前端算法宝典:双指针问题解析与应用》

双指针

双指针,指的是在遍历对象的过程中使用两个相同方向(快慢指针)或者相反方向(对撞指针)的指针或者是两个指针构成一个滑动窗口进行扫描,从而达到相应的目的。

双指针方法在某些情况下可以对有序数组的运算做一些优化。

接雨水

题目如图所示

在这里插入图片描述

解题思路

利用双指针从数组两侧向中间遍历,过程中分别计算左指针及其左侧最高高度和右指针及其右侧最高高度。找到对应的最高高度较低的那一列直接更新结果。

code

    window.onload = function () {

        var trap = function () {
            //定义数组长度
            let height = [0, 1, 0, 2, 1, 0, 1, 3, 2, 1, 2, 1];
            let n = height.length;
            //定义左指针及其左侧最高,右指针及其右侧最高
            let left = 0;//左指针指向最左边的位置
            let leftMax = 0;
            let right = n - 1;//定义右指针指向数组末尾的位置
            let rightMax = 0;
            let res = 0;//定义变量记录结果

            while (left < right) {
                //找出数组两侧的最大值
                leftMax = Math.max(leftMax, height[left]);
                rightMax = Math.max(rightMax, height[right]);
                // console.log("数组左右两侧的最大值" + leftMax, rightMax)
                // 存水量取决于短板,数组两次最大值中较小的一个用指针遍历
                if (leftMax < rightMax) {
                    res += leftMax - height[left];
                    left++;
                    // console.log("数组左侧最大值较小的时候的res" + res)
                } else {
                    res += rightMax - height[right];
                    right--;
                    // console.log("数组右侧最大值较小的时候的res" + res)
                }
            }
            return res;
        };
        trap();
    }

代码中的console.log是为了更直观的看出数组两侧最大值和res的变化过程。

下面附上控制台的输出截图

在这里插入图片描述

对撞指针

对撞指针是指在数组中,将指向最左侧的索引定义为左指针(left),最右侧的定义为右指针(right),然后从两头向中间进行数组遍历。

对撞数组适用于连续数组和字符串,也就是说当你遇到题目给定连续数组和字符串时,应该第一时间想到用对撞指针解题。

验证回文串

题目如图所示

在这里插入图片描述

解题思路

先都转成大写(不然会出现 a A 判定为不相同)
设置头尾双指针,开启循环
如果指向的元素是不是有效的(不是字母和数字),则跳过
如果指向的元素有效,但不相同,则不是回文,返回false
否则有效,且相同,收缩指针,继续循环
直至指针相遇,循环结束,始终没有返回false,返回true。

code

/**
 * @param {string} s
 * @return {boolean}
 */
var isPalindrome = function (s) {
    // 将字符都转换为大写,避免大小写不同而误判
    s = s.toUpperCase();
    // 设置头尾双指针
    let left = 0;
    let right = s.length - 1;

    while (left < right) {
        // 如果左指针指向元素不是字母或者数字,跳过(左指针右移直接下一个)
        if (!isValid(s[left])) {
            left++;
            continue;
        }
        // 如果右指针指向元素不是字母或者数字,跳过(右指针左移直接下一个)
        if (!isValid(s[right])) {
            right--;
            continue;
        }
        // 如果左右指针指向字母和数字,但不相同,不是回文返回false;
        if (s[left] != s[right]) {
            return false;
        }
        left++;
        right--;
    }
    return true;
};

盛最多水的容器

题目如图所示

在这里插入图片描述

解题思路

定义双指针left,right对应数组的两侧,循环遍历height数组,left和right对应的高度较小的先向内移动,不断计算面积更新最大面积

复杂度:时间复杂度O(n),n是数组height的长度,遍历一次。空间复杂度O(1)

code

/**
 * @param {number[]} height
 * @return {number}
 */
var maxArea = function (height) {
    let maxArea = 0; // 用于存储最大面积
    let left = 0; // 左边界指针
    let right = height.length - 1; // 右边界指针

    // 当左指针小于右指针时循环
    while (left < right) {
        const minHeight = Math.min(height[left], height[right]); // 计算左右指针所指高度的最小值
        const width = right - left; // 计算当前宽度
        const area = minHeight * width; // 计算当前面积

        maxArea = Math.max(maxArea, area); // 更新最大面积

        // 移动指针,让高度较小的一侧向内移动,以寻找可能更大的面积
        if (height[left] < height[right]) {
            left++;
        } else {
            right--;
        }
    }

    return maxArea; // 返回最大面积
};

两数之和 II - 输入有序数组

题目如图所示

在这里插入图片描述

解题思路

定义左右指针位于数组两侧,左指针指向数组起始位,右指针指向末尾位。
计算当前左右指针对应值的和(两数之和)是否大于或者小于还是等于目标值。

因为数组是有序递增的,所以若两数之和大于目标值,应该右指针左移减小两数之和使其逼近目标值;

若两数之和小于目标值则需要减小和来逼近目标值,左指针右移,当两数之和等于目标值的时候,返回左右指针的索引,切记本题题目索引值是从1开始的,所以需要
返回的索引值加一。

在这里插入图片描述

code

/**
 * @param {number[]} numbers
 * @param {number} target
 * @return {number[]}
 */
var twoSum = function (numbers, target) {
    // 定义变量n获取数组的长度
    let n = numbers.length;
    // 定义左右指针
    let left = 0;
    let right = n - 1;

    while (left < right) {
        //比较当前数组两端最大值和最小值之和与目标值的大小
        //若当前数组两侧之和大于目标值右指针左移
        if (numbers[left] + numbers[right] > target) {
            right--;
            //若当前数组两侧之和小于目标值左指针右移
        } else if (numbers[left] + numbers[right] < target) {
            left++;
            // 若等于目标值则返回左右指针的索引(注意题目的索引是从1开始的,而代码是从0开始的,所以返回的索引值需要加一)
        } else {
            return [left + 1, right + 1]
        }
    }
    return [left + 1, right + 1];
};

三数之和

题目如图所示

在这里插入图片描述

解题思路

标签:数组遍历
首先对数组进行排序,排序后固定一个数 nums[i]nums[i]nums[i],再使用左右指针指向 nums[i]后面的两端,数字分别为 nums[L] 和 nums[R],计算三个数的和 sum 判断是否满足为 0,满足则添加进结果集
如果 nums[i]大于 0,则三数之和必然无法等于 0,结束循环
如果nums[i] == nums[i−1]nums,则说明该数字重复,会导致结果重复,所以应该跳过
当 sum == 0 时,nums[L] == nums[L+1]则会导致结果重复,应该跳过,L++
当 sum == 0 时,nums[R] == nums[R−1] 则会导致结果重复,应该跳过,R−−
时间复杂度:O(n^2) n 为数组长度

code

/**
 * @param {number[]} nums
 * @return {number[][]}
 */
//  碰撞指针解题
var threeSum = function (nums) {
    let n = nums.length;//定义n为数组长度
    let ans = [] //记录结果的数组
    // 首先对数组进行排序
    nums.sort((a, b) => a - b);
    // 遍历数组
    for (let i = 0; i < n; i++) {
        // 若nums[i]大于零,那么三数之和一定大于零
        if (nums[i] > 0) break;
        // 如果当前数字与上一个数字一样,直接跳过
        // (注意数组越界问题)
        if (i > 0 && nums[i] == nums[i - 1]) continue;

        let left = i + 1;
        let right = n - 1;
        while (left < right) {
            //计算三数之和
            let s = nums[i] + nums[left] + nums[right]
            if (s == 0) {
               ans.push([nums[i],nums[left],nums[right]]);

                //指针当前指向和刚才的一样就直接跳过
                // 左指针所指当前数字和前一个数字一样,直接跳过去重
                while (left < right && nums[left] == nums[left + 1]) {
                    left++;
                }
                while (left < right && nums[right] == nums[right - 1]) {
                    right--;
                }
                left++;
                right--;
            } else if (s < 0) {
                left++;
            } else if (s > 0) {
                right--;
            }
        }
    }
    return ans
};

滑动窗口

滑动窗口实际是两个指针之间形成的区域,下面是滑动窗口的两个指针的移动方式

  1. 初始时,左右指针left,right都指向第0个元素,窗口为[left,right),注意这里是左闭右开,因此初始窗口[0,0)区间没有元素,符合我们的初始定义
  2. 开始循环遍历整个数组元素,判断当前right指针是否超过整个数组的长度,是退出循环,否则执行第3步
  3. 然后right指针开始向右移动一个长度,并更新窗口内的区间数据
  4. 当窗口区间的数据满足我们的要求时,右指针right就保持不变,左指针left开始移动,直到移动到一个不再满足要求的区间时,left不再移动位置
  5. 执行第2步

无重复字符的最长子串

题目如图

在这里插入图片描述

解题思路

在字符串s中定义两个指针,左指针指向数组首位索引,右指针指向数组第二索引

用一个临时数组temp保存[left,right)的字符(包含左指针不包含右指针);

若temp集合中包含s中right所指元素,左指针右移,不包含右指针右移

定义变量max用来存储最长子串。

右指针索引-左指针索引大于max的时候,将右指针索引-左指针索引赋给max

在while循环外返回max;

code

var lengthOfLongestSubstring = function (s) {
    if (s.length <= 1) {
        return s.length;
    }
    let left = 0;
    let right = 1;
    let max = 0;
    let temp;
    //    指针右移过程
    while (right < s.length) {
        //temp保存s中[left,right)的元素
        temp = s.slice(left, right);
        //若temp集合中包含s中right所指元素,左指针右移
        if (temp.indexOf(s.charAt(right)) > -1) {
            left++;
            continue;//跳出当前循环中之后的代码,进入下一次循环
        } else {
            right++;
        }
        if (right - left > max) {
            max = right - left;
        }

    }
    return max;
};

找到字符串中所有字母异位词

题目如图所示
在这里插入图片描述

解题思路

首先定义两个空对象 need 和 window,分别用来存储字符串 t 中每个字符的出现次数以及当前窗口中每个字符的出现次数。

遍历字符串 t,统计每个字符出现的次数,并保存在 need 对象中。

使用两个指针 left 和 right 分别表示窗口的左、右边界。另外定义了 valid 表示窗口中与 t 中字符匹配的数量,res 用来存储匹配成功的起始索引。

通过一个 while 循环,不断右移 right 指针,将新字符加入窗口。如果新字符在 need 中,则更新 window 中相应字符的出现次数,并检查是否与 need 中对应字符的出现次数匹配。

另外,在判断窗口大小超过 t 的长度时,通过一个内层的 while 循环,不断左移 left 指针,并更新窗口中字符的出现次数。同时,检查当前窗口中是否所有字符的出现次数与 need 中相同,如果是则说明找到一个符合条件的子串,将起始索引添加到结果数组 res 中。

最后返回结果数组 res,即为找到的所有符合条件的子串的起始索引数组。

这个算法的时间复杂度是 O(n),其中 n 为字符串 s 的长度。

code

/**
 * @param {string} s
 * @param {string} p
 * @return {number[]}
 */
var findAnagrams = function (s, t) {
    // 需要的
    let need = {};
    // 窗口中的字符
    let window = {};
    for (let a of t) {
        // 统计需要的字符
        need[a] = (need[a] || 0) + 1;
    }
    // 左右指针
    let left = 0,
        right = 0;
    let valid = 0;
    let res = [];
    while (right < s.length) {
        // 即将移入窗口的字符
        let c = s[right];
        // 右移窗口
        right++;
        if (need[c]) {
            // 当前字符在需要的字符中,则更新当前窗口统计
            window[c] = (window[c] || 0) + 1;
            if (window[c] == need[c]) {
                // 当前窗口和需要的字符匹配时,验证数量增加1
                valid++;
            }
        }
        while (right - left >= t.length) {
            if (valid == Object.keys(need).length) {
                res.push(left);
            }
            let d = s[left];
            left++;
            if (need[d]) {
                if (window[d] == need[d]) {
                    valid--;
                }
                window[d]--;
            }
        }
    }
    return res;
};


快慢指针

快慢指针方法会使用两个在数组(或序列/链表)中以不同速度移动的指针。该方法在处理循环链表或数组时非常有用。

应用场景:

  1. 处理链表或数组中的循环的问题
  2. 找链表中点或需要知道特定元素的位置

何时应该优先选择快慢指针,而不是上面提到的普通双指针方法?

有些情况不适合使用二指针方法,比如在不能反向移动的单链接链表中。使用快速和慢速模式的一个案例是当你想要确定一个链表是否为回文(palindrome)时。

移动零问题

题目如图

在这里插入图片描述

快慢指针解题思路

右指针自左向右遍历数组,右指针当前所指元素不为0的时候,交换左右指针对应元素,左指针右移,之后右指针继续遍历。
直到右指针遍历到数组的最后一位且右指针对应数字不为0
在这里插入图片描述

code

var moveZeroes = function (nums) {
    let len = nums.length;
    let left = 0;
    let right = 0;
    while (right < len) {
        //右指针所指的元素不为0时
        if (nums[right] !== 0) {
            // 交换左右指针的值
            let temp = nums[right];
            nums[right] = nums[left];
            nums[left] = temp;
            // 左指针右移
            left++;
        }
        // 右指针右移
        right++;
    }
};

移动零的其他解题方法

技巧一行code

var moveZeroes = function(nums) {
    nums.sort((a,b) => b? 0: -1)
};

如果 b 是真值,保持 a 和 b 的顺序不变;如果 b 是假值,将 a 排在 b 的前面。

这段代码实际上将数组中的元素按照它们在数组中出现的顺序排列,但将所有零值元素放在非零值元素的前面。

单个指针解题思路

1、先获取数组长度
2、然后循环遍历数组
3、当数组哪个位置是0 ,就删除这个这个位置的元素,然后在末尾补 0
从而就达到了 把所有0移动到数组末尾的要求且保持的非零元素的相对顺序
注意:当为0的元素删除后,下一个元素就会前进一位占据该位置,所以要从该位置在进行判断
当移动到末尾的元素,就不用再一次进行遍历了,所以遍历的长度要减去1位

code

var moveZeroes = function(nums) {
        //获取数组长度
        let len = nums.length;
        //循环遍历数组
        for(let i =0;i<len;i++){
            //当数组哪个位置是0 ,就删除这个这个位置的元素,然后在末尾补 0
            //从而就达到了 把所有0移动到数组末尾的要求且保持的非零元素的相对顺序
            if(nums[i]==0){
                nums.splice(i,1)
                nums.push(0)
            //当为0的元素删除后,下一个元素就会前进一位占据该位置,所以要从该位置在进行判断
                i--
            //当移动到末尾的元素,就不用再一次进行遍历了,所以遍历的长度要减去1位
                len--
            }
        }
        return nums
}

总结

    //循环遍历数组
    for(let i =0;i<len;i++){
        //当数组哪个位置是0 ,就删除这个这个位置的元素,然后在末尾补 0
        //从而就达到了 把所有0移动到数组末尾的要求且保持的非零元素的相对顺序
        if(nums[i]==0){
            nums.splice(i,1)
            nums.push(0)
        //当为0的元素删除后,下一个元素就会前进一位占据该位置,所以要从该位置在进行判断
            i--
        //当移动到末尾的元素,就不用再一次进行遍历了,所以遍历的长度要减去1位
            len--
        }
    }
    return nums

}

# 总结
算法的解决方法一定不唯一,所以上述的所有双指针算法可以为一些问题提供很好的解决思路,但是解法并不唯一,以上可供学习参考。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/609518.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

sbt安装

一、sbt介绍 在Spark中&#xff0c;sbt&#xff08;Scala Build Tool&#xff09;是一个用于构建Scala项目的工具。它是Spark项目的主要构建工具之一&#xff0c;用于编译Scala代码、管理依赖项、打包应用程序以及执行其他与项目构建相关的任务。 sbt的用途在Spark开发中主要…

『大模型笔记』Google CEO Sundar Pichai(桑达尔·皮查伊)谈人工智能的未来!

Google CEO Sundar Pichai(桑达尔皮查伊)谈人工智能的未来! 文章目录 一. Google CEO谈人工智能的未来总结摘要观点时间线二. 参考文献简短总结:一. Google CEO谈人工智能的未来 总结 主要介绍了Google CEO Sundar Pichai对人工智能未来的看法,以及Google在AI领域的战略…

JavaScript异步编程——06-Promise入门详解【万字长文,感谢支持】

前言 Promise 是 JavaScript 中特有的语法。可以毫不夸张得说&#xff0c;Promise 是ES6中最重要的语法&#xff0c;没有之一。初学者可能对 Promise 的概念有些陌生&#xff0c;但是不用担心。大多数情况下&#xff0c;使用 Promise 的语法是比较固定的。我们可以先把这些固定…

三月份饮料行业线上市场销售数据分析

2024年3月&#xff0c;中国饮料市场呈现出多样化和健康趋势的明显特征。从消费场景、消费端需求以及销售渠道来看&#xff0c;饮料市场正在经历多元化的发展&#xff0c;这不仅体现在产品种类上&#xff0c;也体现在消费者的购买行为和偏好上。 据鲸参谋数据统计&#xff0c;线…

LLM大语言模型(十五):LangChain的Agent中使用自定义的ChatGLM,且底层调用的是remote的ChatGLM3-6B的HTTP服务

背景 本文搭建了一个完整的LangChain的Agent&#xff0c;调用本地启动的ChatGLM3-6B的HTTP server。 为后续的RAG做好了准备。 增加服务端role&#xff1a;observation ChatGLM3的官方demo&#xff1a;openai_api_demo目录 api_server.py文件 class ChatMessage(BaseModel…

LeetCode HOT 100刷题总结

文章目录 1 哈希1.1 1-1.两数之和&#x1f7e2;1.2 2-49.字母异位词分组&#x1f7e1;1.3 3-128.最长连续序列&#x1f7e1; 2 双指针2.1 4-283.移动零&#x1f7e2;2.2 6-15.三数之和&#x1f7e1;2.3 7-11.盛最多水的容器&#x1f7e1;2.4 8-42.接雨水&#x1f534; 3 滑动窗…

传输层协议——UDP协议

目录 一、传输层 二、再谈端口号 端口号的划分 知名端口号 pidof netstat命令 三、UDP协议 1、UDP协议格式 2、UDP协议特点 3、UDP协议的缓冲区 四、基于UDP的应用层协议 一、传输层 上一篇文章我们所讲到的HTTP协议和HTTPS协议&#xff0c;是属于应用层协议。我们…

【小笔记】问答系统可视化实现的三种方式

下面三种方式都是基于Python的哈&#xff0c;从简单到复杂。 方式一&#xff1a;命令行交互问答 优点&#xff1a;原始简单直接 方式二&#xff1a;使用Python可视化框架 优点&#xff1a;无需学习前端技术栈即可搭建一个web。 streamlit&#xff1a;⭐️⭐️⭐️⭐️gra…

【服务器优化】LVS负载均衡

LVS负载均衡 LVS简介 ​ LVS&#xff08;Linux Virtual Server&#xff09;即Linux虚拟服务器&#xff0c;是由章文嵩博士主导的开源负载均衡项目&#xff0c;目前LVS已经被集成到Linux内核模块中。该项目在Linux内核中实现了基于IP的数据请求负载均衡调度方案&#xff0c;终…

nginx的应用部署nginx

这里写目录标题 nginxnginx的优点什么是集群常见的集群什么是正向代理、反向代理、透明代理常见的代理技术正向代理反向代理透明代理 nginx部署 nginx nginx&#xff08;发音同enginex&#xff09;是一款轻量级的Web服务器/反向代理服务器及电子邮件&#xff08;IMAP/POP3&…

Java设计模式 _结构型模式_外观模式

一、外观模式 1、外观模式 外观模式&#xff08;Facade Pattern&#xff09;是一种结构型模式。主要特点为隐藏系统的复杂性&#xff0c;并向客户端提供了一个客户端可以访问系统的接口。这有助于降低系统的复杂性&#xff0c;提高可维护性。当客户端与多个子系统之间存在大量…

FPGA+海思ARM方案,可同时接收HDMI/VGA 两种信号,远程控制

FPGA海思ARM方案&#xff0c;可同时接收HDMI/VGA 两种信号&#xff0c;通过配置输出任一图像或者拼接后的图像 客户应用&#xff1a;无线远程控制 主要特性&#xff1a; 1.支持2K以下任意分辨率格式 2.支持H264压缩图像 3.支持WIFI/4G无线传输 4.支持自适应输入图像分辨率 …

如何编辑百度百科里面的资料

编辑百度百科资料是一个相对简单的过程&#xff0c;但同时也需要遵循一定的规则和流程。以下是百科优化网yajje整理的编辑百度百科资料的步骤和注意事项。 登录账户 首先&#xff0c;编辑百度百科需要一个百度账号。如果没有&#xff0c;你需要先注册一个。登录后&#xff0c;…

西奥机电CLRT-01:重塑碳酸饮料质检新纪元

西奥机电CLRT-01&#xff1a;重塑碳酸饮料质检新纪元 在追求品质生活的今天&#xff0c;碳酸饮料的品质检测成为了行业内外关注的焦点。西奥机电&#xff0c;作为行业创新的领跑者&#xff0c;携其最新研发的CLRT-01二氧化碳气容量测试仪&#xff0c;为碳酸饮料行业带来了革命性…

一文详解|影响成长的关键思考(二)

之前写过一篇《一文详解&#xff5c;影响成长的关键思考》&#xff0c;里面对自己工作前几年的心法进行了总结&#xff0c;并分享了出来。现在又工作了一段时间后&#xff0c;有了一些新的体会&#xff0c;想进一步分析一下&#xff0c;于是便有了此文。的确&#xff0c;思考也…

LeetCode63:不同路径Ⅱ

题目描述 一个机器人位于一个 m x n 网格的左上角 &#xff08;起始点在下图中标记为 “Start” &#xff09;。 机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角&#xff08;在下图中标记为 “Finish”&#xff09;。 现在考虑网格中有障碍物。那么从左上角…

【NPS】微软NPS配置802.1x,验证域账号,动态分配VLAN(NPS篇)

NPS简介 Network Policy Server&#xff08;NPS&#xff09;是微软Windows Server中的一个网络服务&#xff0c;它作为RADIUS服务器实现&#xff0c;用于集中管理网络接入请求。NPS处理对网络资源的认证、授权和审计请求&#xff0c;通常用于控制远程访问VPN和无线网络的接入。…

Python 数据库操作- sqlite3 模块

Python sqlite3 模块 1. 安装 SQLite3 可使用 sqlite3 模块与 Python 进行集成。sqlite3 模块是由 Gerhard Haring 编写的。它提供了一个与 PEP 249 描述的 DB-API 2.0 规范兼容的 SQL 接口。用户不需要单独安装该模块&#xff0c;因为 Python 2.5.x 以上版本默认自带了该模块…

(动画详解)LeetCode225.用队列实现栈

. - 力扣&#xff08;LeetCode&#xff09; 题目描述 解题思路 这道题的思路就是使用两个队列来实现 入栈就是入队列 出栈就是将非空队列的前n-1个元素移动到新的队列中去 再将最后一个元素弹出 动画详解 代码实现 #define _CRT_SECURE_NO_WARNINGS 1#include <stdio.…

打车遇到臭车的底层逻辑!修炼的法门居然又捡起来了!——早读(逆天打工人爬取热门微信文章解读)

冥冥之中自有天意 引言Python 代码第一篇 洞见 热搜上“打车遇到臭车”话题&#xff0c;戳到了650万成年人的尴尬第二篇 冯站长之家 三分钟新闻早餐结尾 生命不息 探索不止 在生命的旅程中 我不断探索不断发现 永不停歇 引言 记 ​一大突破 炁​机启动 ​没想到 ​去年9月份…