快慢指针+双指针

链表中快慢指针

1 链表简介

在这里插入图片描述

2 快慢指针的妙用

  • 快慢指针就是定义两根指针,
    • 移动的速度一快一慢,以此来制造出想要的差值
    • 这个差值可让我们找到链表上相应节点

2.1 找中间值

  • 一般思路:先遍历一次链表,记录住一共有多少个节点,
    • 然后,再次遍历找寻中点。
  • 利用快慢指针
  • 最开始slow与fast指针都指向链表第一个节点,
    • 然后slow每次移动一个指针,fast每次移动两个

在这里插入图片描述

2.2 判断链表中的环

  • 圆环跑道中,两个人有速度差,迟早两人会相遇,只要相遇那么就说明有环。
  • 环上加了额外的两个节点,我们可以想象一下,只要两个指针跑进了环里,那么因为存在速度差,他们之间的距离总会由远及近,然后相遇,在远离。像极了我们人世间某些人在你生命中匆匆而过的感觉。

在这里插入图片描述

2.3 删除倒数第n个节点

  • 先找出待删除元素前一个元素,也就是第n-1个节点。
  • 转化为找链表上的某个节点的问题了,这是快慢指针最擅长的场景。
  • 如何找第(n-1)个元素?
  • 一开始就让fast指针比slow指针快n+1个元素,接下来,两个指针都是一步一步来往下走。
  • 当fast走完时,slow指针就刚刚好停留在第(n-1)个元素上。
  • n=2时的情形(dummyHead是手动加上去的虚拟头节点):

在这里插入图片描述

双指针

  • 双指针技巧分两类,
    • 快慢指针,
    • 左右指针。
  • 前者解决主要解决链表中的问题,
    • 如的判定链表中是否包含环;
    • 后者主要解决数组(或者字符串)中的问题,
    • 比如二分查找。

一、快慢指针的常见算法

已知链表中含有环,返回环的起始位置

ListNode detectCycle(ListNode head) {
    ListNode fast, slow;
    fast = slow = head;
    while (fast != null && fast.next != null) {
        fast = fast.next.next;
        slow = slow.next;
        if (fast == slow)
            break;
    }
    
    slow = head;
    while (slow != fast) {
        fast = fast.next;
        slow = slow.next;
    }
    return slow;
}
  • 第一次相遇时,slow 走k 步,那么快指针走2k
    • fast比 slow 多走k 步
    • k就是环的长度
    • 因为他们速度差只是1啊!!!
  • 设相遇点距环的起点的距离为m,
    • 环的起点距头结点 head 的距离为 k - m,
    • 也就是说如果从 head 前进 k - m 步就能到达环起点。
    • 如果从相遇点继续前进 k - m ,也恰好到达环起点。
  • 只要我们把快慢指针中的任一个重新指向 head,然后两个指针同速前进,k - m 步后就会相遇,相遇之处就是环的起点了。

在这里插入图片描述

寻找链表的中点

ListNode slow, fast;
slow = fast = head;
while (fast != null && fast.next != null) {
    fast = fast.next.next;
    slow = slow.next;
}
// slow 就在中间位置
return slow;

  • 长度是奇数时,slow 恰巧停在中点位置;
  • 偶数,slow 最终的位置是中间偏右:

  • 寻找链表中点的一个重要作用是对链表进行归并排序。

回想数组的归并排序:求中点索引递归地把数组二分,最后合并两个有序数组。对于链表,合并两个有序链表是很简单的,难点就在于二分。

但是现在你学会了找到链表的中点,就能实现链表的二分了。关于归并排序的具体内容本文就不具体展开了。

寻找链表的倒数第 k 个元素

  • 让快指针先走 k 步,然后快慢指针开始同速前进。
  • 这样当快指针走到链表末尾 null 时,慢指针所在的位置就是倒数第 k 个链表节点(为了简化,假设 k 不会超过链表长度):

ListNode slow, fast;
slow = fast = head;
while (k-- > 0)
fast = fast.next;

while (fast != null) {
slow = slow.next;
fast = fast.next;
}
return slow;

二、左右指针的常用算法

  • 左右指针在数组中实际是指两个索引值,
  • 一般初始化left = 0, right = nums.length - 1 。

1、二分查找

前文 二分查找算法详解 有详细讲解,这里只写最简单的二分算法,旨在突出它的双指针特性:

int binarySearch(int[] nums, int target) {
int left = 0;
int right = nums.length - 1;
while(left <= right) {
int mid = (right + left) / 2;
if (nums[mid] == target)
return mid;
else if (nums[mid] < target)
left = mid + 1;
else if (nums[mid] > target)
right = mid - 1;
}
}

2、两数之和

直接看一道 LeetCode 题目吧:

只要数组有序,就应该想到双指针技巧。这道题的解法有点类似二分查找,通过调节 left 和 right 可以调整 sum 的大小:

3、反转数组

void reverse(int[] nums) {
int left = 0;
int right = nums.length - 1;
while (left < right) {
// swap(nums[left], nums[right])
int temp = nums[left];
nums[left] = nums[right];
nums[right] = temp;
left++;
right–;
}
}

4、滑动窗口算法

这也许是双指针技巧的最高境界了,如果掌握了此算法,可以解决一大类子字符串匹配的问题,不过「滑动窗口」算法比上述的这些算法稍微复杂些。

幸运的是,这类算法是有框架模板的,下篇文章就准备讲解「滑动窗口」算法模板,帮大家秒杀几道 LeetCode 子串匹配的问题。

链接

  • https://www.cnblogs.com/kyoner/p/11087755.html
发布了589 篇原创文章 · 获赞 300 · 访问量 8万+

猜你喜欢

转载自blog.csdn.net/zhoutianzi12/article/details/105495515
今日推荐