双指针(算是对之前题目的双指针又做一遍)

本文详细介绍了双指针技术在处理链表和数组问题中的应用,如计算链表中点、判断环的存在与起点、求环长度、找倒数第k个元素等。此外,还探讨了双指针在二分查找、求和问题、滑动窗口、字符串处理及链表操作中的使用,例如反转字符串、替换空格、翻转单词、删除链表节点、寻找链表交点、环形链表检测以及解决三数之和、四数之和问题。双指针作为一种高效策略,能简化复杂问题的解决过程。
摘要由CSDN通过智能技术生成

双指针

双指针是一种思想,一种技巧或一种方法,并不是什么特别具体的算法,在二分查找等算法中经常用到这个技巧。具体就是用两个变量动态存储两个或多个结点,来方便我们进行一些操作。通常用在线性的数据结构中,比如链表和数组,有时候也会用在图算法中。

在我们遇到像数组,链表这类数据结构的算法题目的时候,应该要想得到双指针的套路来解决问题。特别是链表类的题目,经常需要用到两个或多个指针配合来记忆链表上的节点,完成某些操作。链表这种数据结构也是树形结构和图的原型,所以有时候在关于图和树形结构的算法题目中也会用到双指针。

  1. 快慢指针

    1. 计算链表的中点:快慢指针从头节点出发,每轮迭代中,快指针向前移动两个节点,慢指针向前移动一个节点,最终当快指针到达终点的时候,慢指针刚好在中间的节点。
    2. 判断链表是否有环:如果链表中存在环,则在链表上不断前进的指针会一直在环里绕圈子,且不能知道链表是否有环。使用快慢指针,当链表中存在环时,两个指针最终会在环中相遇。
    3. 判断链表中环的起点:当我们判断出链表中存在环,并且知道了两个指针相遇的节点,我们可以让其中任一个指针指向头节点,然后让它俩以相同速度前进,再次相遇时所在的节点位置就是环开始的位置。
    4. 求链表中环的长度:只要相遇后一个不动,另一个前进直到相遇算一下走了多少步就好了
    5. 求链表倒数第k个元素:先让其中一个指针向前走k步,接着两个指针以同样的速度一起向前进,直到前面的指针走到尽头了,则后面的指针即为倒数第k个元素。(严格来说应该叫先后指针而非快慢指针)
  2. 碰撞指针

    1. 二分查找问题

    2. n数之和问题:比如两数之和问题,先对数组排序然后左右指针找到满足条件的两个数。如果是三数问题就转化为一个数和另外两个数的两数问题。以此类推。

  3. 滑动窗口法

    两个指针,一前一后组成滑动窗口,并计算滑动窗口中的元素的问题。

    这类问题一般包括

    1. 字符串匹配问题

    2. 子数组问题

1. 移除元素

  1. [leetCode 27]移除元素

给你一个数组 nums 和一个值 val,你需要 原地 移除所有数值等于 val 的元素,并返回移除后数组的新长度。

不要使用额外的数组空间,你必须仅使用 O(1) 额外空间并 原地 修改输入数组。

元素的顺序可以改变。你不需要考虑数组中超出新长度后面的元素。

思路
const removeElement = function(nums, val) {
    let j = 0
    for (let i = 0; i < nums.length; i++) {
        if (nums[i] != val) {
            nums[j] = nums[i]
            j++
        }
    }
    return j
}

2. 反转字符串

  1. leetCode 344反转字符串

编写一个函数,其作用是将输入的字符串反转过来。输入字符串以字符数组 char[] 的形式给出。

不要给另外的数组分配额外的空间,你必须原地修改输入数组、使用 O(1) 的额外空间解决这一问题。

const reverseString = function(s) {
    let i = 0,
        j = s.length - 1
    while (i < j) {;
        [s[i], s[j]] = [s[j], s[i]]
        i++
        j--
    }
    return s
}
console.log(reverse(['h', 'e', 'l', 'l', 'o']))

3. 替换空格

  1. leetCode 剑指 Offer 05. 替换空格

请实现一个函数,把字符串 s 中的每个空格替换成"%20"。

这些题其实在字符串之前章节已经都写过题解了。这里使用双指针来解题
const replaceSpace = function(s) {
    // 1. 首先转换为数组形式,计算有多少个空格,那么就可以知道长度是多少
    const newArr = s.split('')
    space = 0
    for (let i = 0; i < newArr.length; i++) {
        if (newArr[i] == ' ') {
            space++
        }
    }
    //2. 获取到了应该变为的长度之后,就从后开始遍历
    let length = newArr.length + 2 * space
    //3. 从后面开始遍历
    let r = length - 1,
        l = s.length - 1
    let result = []
    while (l >= 0) {
        if (s[l] == ' ') {
            result[r] = '0'
            result[r - 1] = '2'
            result[r - 2] = '%'
            r -= 2
        } else {
            result[r] = newArr[l]
        }
        l--
        r--
    }
    return result.join('')
}
console.log(replaceSpace('We are happy'))

4. 反转单词

  1. leetCode 翻转字符串里的单词

给你一个字符串 s ,逐个翻转字符串中的所单词 。单词 是由非空格字符组成的字符串。s 中使用至少一个空格将字符串中的 单词 分隔开。请你返回一个翻转 s 中单词顺序并用单个空格相连的字符串。

在这里,我们要求空间复杂度为O(1)

思路(使用双指针的方法)
  1. 首先,设置left和right两个指针,都在字符串的尾部
  2. 如果最后边是空格的话,就向前一步,直到不为空格
  3. 然后移动left指针,直到s[left]不为空,此时left指向最后一个单词的头部,right指向最后一个单词的尾部。
  4. 遍历这个单词加到result中
  5. 跳到下一个单词,进入第二次
  6. 如果result中有单词(不为空),就给上一个单词后边加一个空格。
const reverseWords = function(s) {
    let right = s.length - 1,
        left = s.length - 1
    let result = ''
    while (left >= 0) {
        //找到最后一个单词的最后一个字母
        while (s[right] == ' ') {
            right--
        }
        left = right

        if (result && left >= 0) {
            result += ' '
        }
        //然后开始移动left
        while (s[left] && s[left] != ' ') {
            left--
        }
        for (let i = left + 1; i <= right; i++) {
            result += s[i]
        }
        right = left
    }
    return result
}
console.log(reverseWords(' hello world '))
  1. leetCode 206反转链表
var reverseList = function (head) {
  let prev = null       // 尾随cur的prev指针,开始时指向null
  let cur = head        // 推进指针,开始时指向头结点
  while (cur) {         // cur指针推进到null节点,则退出循环
    let next = cur.next // 暂存cur的下一节点
    cur.next = prev     // 将cur的next指针指向prev
    prev = cur          // 将prev更新为cur节点
    cur = next          // 将cur指针推进一个节点
  }                     // 退出while时,cur指向null,prev指向原链的尾节点
  return prev
};

5. 删除链表中倒数第N个节点

  1. leetCode 19删除链表的倒数第 N 个结点

双指针的经典应用,如果要删除倒数第n个节点,让fast移动n步,然后让fast和slow同时移动,直到fast指向链表末尾。删掉slow所指向的节点就可以了。

思路
  1. 定义fast指针和slow指针,初始化在虚拟的头节点
  2. 然后让快指针先走n+1步,本来是n步,但是数据结构是链表,这样子方便一点
  3. fast和slow同时移动,直到fast走到末尾
  4. 然后删除slow指向的下一个节点 。slow.next=slow.next.next
const removeNthFromEnd = (head, n) => {
    // 定义虚拟节点
    const dummy = new ListNode(0, head)
        // 定义左右指针,都指向虚拟节点
    let slow = dummy,
        fast = dummy
        // 右指针先走n+1步
    while (1 + n--) {
        fast = fast.next
    }
    // 如果此时右指针到null了,说明删除的是第一个节点
    // 直接返回dummy.next.next;
    if (!fast) return dummy.next.next
        // 右指针没到头
    while (fast) {
        // 左右指针一起走
        fast = fast.next
        slow = slow.next
    }
    // 右指针走到null之后,删除左指针的下一个节点即可
    slow.next = slow.next.next
        // 返回虚拟节点的next
    return dummy.next
}

6. 链表相交

  1. leetCode 面试题 02.07. 链表相交

给你两个单链表的头节点 headAheadB ,请你找出并返回两个单链表相交的起始节点。如果两个链表没有交点,返回 null

思路

对于交点,是从不相交到相交。

  1. 我们需要先求助出两个链表的长度
  2. 获取两个链表之间的差值
  3. 然后让长一点的链表移动到与短一点的链表相同的位置开始
  4. 此时我们就可以比较两个指针是否相同,如果相同,就找到了交点,如果不相同直到尾部,就表示没有交点
  5. 如果遇到curA == curB,则找到焦点。否则循环退出返回空指针。
const getLength = (head) => {
    let len = 0,
        cur = head
    while (cur) {
        len++
        cur = cur.next
    }
    return len
}
const getIntersectionNode = function(headA, headB) {
    let curA = headA,
        curB = headB,
        lenA = getLength(headA),
        lenB = getLength(headB)
    if (lenA < lenB) {;
        [curA, curB] = [curB, curA];
        [lenA, lenB] = [lenB, lenA]
    }
    let number = lenA - lenB
    while (number > 0) {
        curA = curA.next
        number--
    }
    while (curA && curA != curB) {
        curA = curA.next
        curB = curB.next
    }
    return curA
}

7. 环形链表

  1. leetCode 环形链表 II

有环的话,必在环中相遇,详情见carl代码随想录

var detectCycle = function (head) {
  let slow = head;
  let fast = head;
  while (fast) {
    if (fast.next == null) { // fast.next走出链表了,说明无环
      return null;
    }
    slow = slow.next;        // 慢指针走一步
    fast = fast.next.next;   // 快指针走两步
    if (slow == fast) {      // 首次相遇
      fast = head;           // 让快指针回到头节点
      while (true) {         // 开启循环,让快慢指针相遇
        if (slow == fast) {  // 相遇,在入环处
          return slow;
        }
        slow = slow.next;
        fast = fast.next;    // 快慢指针都走一步
      }
    }
  }
  return null;
};

8. 三数之和

l. leetCode 三数之和

给你一个包含 n 个整数的数组 nums,判断 nums 中是否存在三个元素 a,b,c ,使得 a + b + c = 0 ?请你找出所有和为 0 且不重复的三元组。

思路
  1. 使用双指针来求解
  2. 首先对数组进行从小到大排序
  3. 一次for循环,令开始指针为当前数的下一位,结束指针就最后一个
  4. 判断三者的和
var threeSum = function(nums) {
    const result = [];
    //为方便去重,首先将数组排序
    nums.sort((a,b) => a - b);
    for(let i=0;i<nums.length;i++){
        //跳过重复数字
        if(i && nums[i] === nums[i - 1]){
            continue;
        }
        let left = i + 1;
        let right = nums.length - 1;
        while(left < right){
            const sum = nums[i] + nums[left] + nums[right];
            if(sum > 0){
                //将right向左移动一位
                right--;
            }else if(sum < 0){
                left++;
            }else{
                //当三数之和为0时,将数组添加到result中,同时将left和right分别沿各自方向移动到下一位
                result.push([nums[i], nums[left++], nums[right--]]);
                //跳过重复数字
                while(nums[left] === nums[left - 1]){
                    left++;
                }
                while(nums[right] === nums[right + 1]){
                    right--;
                }
            }
        }
    }
    return result;
};

9. 四数之和

  1. leetCode . 四数之和

给你一个由 n 个整数组成的数组 nums ,和一个目标值 target 。请你找出并返回满足下述全部条件且不重复的四元组 [nums[a], nums[b], nums[c], nums[d]] :

  • 0 <= a, b, c, d < n

  • a、b、c 和 d 互不相同

  • nums[a] + nums[b] + nums[c] + nums[d] == target

  输入:nums = [1,0,-1,0,-2,2], target = 0
  输出:[[-2,-1,1,2],[-2,0,0,2],[-1,0,0,1]]
思路

双指针

var fourSum = function(nums, target) {
    const len = nums.length
    if (len < 4) return []
    nums.sort((a, b) => a - b)
    const res = []
    for (let i = 0; i < len - 3; i++) {
        // 去重i
        if (i > 0 && nums[i] === nums[i - 1]) continue
        for (let j = i + 1; j < len - 2; j++) {
            // 去重j
            if (j > i + 1 && nums[j] === nums[j - 1]) continue
            let l = j + 1,
                r = len - 1
            while (l < r) {
                const sum = nums[i] + nums[j] + nums[l] + nums[r]
                if (sum < target) {
                    l++
                    continue
                }
                if (sum > target) {
                    r--
                    continue
                }
                res.push([nums[i], nums[j], nums[l], nums[r]])
                while (l < r && nums[l] === nums[++l]);
                while (l < r && nums[r] === nums[--r]);
            }
        }
    }
    return res
}

参考

  1. https://zhuanlan.zhihu.com/p/95747836
  2. 代码随想录
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值