2021秋招-算法-双指针技巧总结

算法-双指针技巧总结

大佬总结框架-双指针技巧汇总-转载

同上-排版好一些
同上-进一步对于题目展开

我认为双指针技巧还可以分为两类,一类是「快慢指针」,一类是「左右指针」。前者解决主要解决链表中的问题,比如典型的判定链表中是否包含环;后者主要解决数组(或者字符串)中的问题,比如二分查找
2021秋招-算法-双指针技巧总结_第1张图片

一、快慢指针的常见算法

快慢指针中,快指针和慢指针都初始化指向链表的头结点 head,前进时快指针 fast 在前,慢指针 slow 在后,巧妙解决一些链表中的问题。

1.1 判断链表中是否含有环-leetcode-141. 环形链表-简单
141. 环形链表
给定一个链表,判断链表中是否有环。
为了表示给定链表中的环,我们使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。 
如果 pos 是 -1,则在该链表中没有环。

示例 1:
输入:head = [3,2,0,-4], pos = 1
输出:true
解释:链表中有一个环,其尾部连接到第二个节点。

2021秋招-算法-双指针技巧总结_第2张图片

这应该属于链表最基本的操作了,如果读者已经知道这个技巧,可以跳过。

单链表的特点是每个节点只知道下一个节点,所以一个指针的话无法判断链表中是否含有环的。

如果链表中不含环,那么这个指针最终会遇到空指针 null 表示链表到头了,这还好说,可以判断该链表不含环。

boolean hasCycle(ListNode head) {
    while (head != null)
        head = head.next;
    return false;
}

但是如果链表中含有环,那么这个指针就会陷入死循环,因为环形数组中没有 null 指针作为尾部节点。

经典解法就是用两个指针,一个跑得快,一个跑得慢。如果不含有环,跑得快的那个指针最终会遇到 null,说明链表不含环;如果含有环,快指针最终会超慢指针一圈,和慢指针相遇,说明链表含有环

boolean hasCycle(ListNode head) {
    ListNode fast, slow;
    fast = slow = head;
    //注意这个循环条件!!!
    while (fast != null && fast.next != null) {
        fast = fast.next.next;
        slow = slow.next;

        if (fast == slow) return true;
    }
    return false;
}
# python -AC 
# Definition for singly-linked list.
# class ListNode:
#     def __init__(self, x):
#         self.val = x
#         self.next = None

class Solution:
    def hasCycle(self, head: ListNode) -> bool:
        fastPointer = slowPointer = head
        while fastPointer and fastPointer.next:
            fastPointer = fastPointer.next.next
            slowPointer = slowPointer.next
            
            if fastPointer == slowPointer:
                return True
        
        return False

注意上述的循环条件,它避免了fast指针的一些空值问题!!!

1.2 已知链表中含有环,返回这个环的起始位置-142. 环形链表 II-中等
142. 环形链表 II
给定一个链表,返回链表开始入环的第一个节点。 如果链表无环,则返回 null。
为了表示给定链表中的环,我们使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。
 如果 pos 是 -1,则在该链表中没有环。
说明:不允许修改给定的链表。

示例 1:
输入:head = [3,2,0,-4], pos = 1
输出:tail connects to node index 1
解释:链表中有一个环,其尾部连接到第二个节点。

2021秋招-算法-双指针技巧总结_第3张图片

2021秋招-算法-双指针技巧总结_第4张图片
这个问题一点都不困难,有点类似脑筋急转弯,先直接看代码:

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;
    }
    // 上面的代码类似 hasCycle 函数
    slow = head;
    while (slow != fast) {
        fast = fast.next;
        slow = slow.next;
    }
    return slow;
}

python-AC 代码

# Definition for singly-linked list.
# class ListNode:
#     def __init__(self, x):
#         self.val = x
#         self.next = None

class Solution:
    def detectCycle(self, head: ListNode) -> ListNode:
        # 思路快慢指针,找到环以后一起走,相遇即环入口
        fastPointer = slowPointer = head
        flag = 0 
        while fastPointer and fastPointer.next:
            fastPointer = fastPointer.next.next
            slowPointer = slowPointer.next
            
            if fastPointer == slowPointer:
                flag = 1
                break
        if flag == 0:
            return None
        slowPointer = head
        # 重新同步走
        while slowPointer != fastPointer:
            fastPointer = fastPointer.next
            slowPointer = slowPointer.next
            
        return slowPointer

可以看到,当快慢指针相遇时,让其中任一个指针指向头节点,然后让它俩以相同速度前进,再次相遇时所在的节点位置就是环开始的位置。这是为什么呢?

第一次相遇时,假设慢指针 slow 走了 k 步,那么快指针 fast 一定走了 2k 步,也就是说比 slow 多走了 k 步。

具体的数学证明,此处省略;

2021秋招-算法-双指针技巧总结_第5张图片
设相遇点距环的起点的距离为 m,那么环的起点距头结点 head 的距离为 k - m,也就是说如果从 head 前进 k - m 步就能到达环起点。

巧的是,如果从相遇点继续前进 k - m 步,也恰好到达环起点。
2021秋招-算法-双指针技巧总结_第6张图片
所以,只要我们把快慢指针中的任一个重新指向 head,然后两个指针同速前进,k - m 步后就会相遇,相遇之处就是环的起点了。

⭐1.3 寻找链表的中点–链表归并排序

类似上面的思路,我们还可以让快指针一次前进两步,慢指针一次前进一步,当快指针到达链表尽头时,慢指针就处于链表的中间位置。

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

当链表的长度是奇数时,slow 恰巧停在中点位置;如果长度是偶数,slow 最终的位置是中间偏右:
2021秋招-算法-双指针技巧总结_第7张图片
寻找链表中点的一个重要作用是对链表进行归并排序。

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

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

4.寻找链表的倒数第 k 个元素-剑指 Offer 22. 链表中倒数第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;
# Definition for singly-linked list.
# class ListNode:
#     def __init__(self, x):
#         self.val = x
#         self.next = None

class Solution:
    def getKthFromEnd(self, head: ListNode, k: int) -> ListNode:
        slowPointer = fastPointer = head
        while k != 0:
            fastPointer = fastPointer.next
            k -= 1
        while fastPointer:
            slowPointer = slowPointer.next
            fastPointer = fastPointer.next
        
        return slowPointer
5.面试题 02.02. 返回倒数第 k 个节点

二、左右指针的常用算法

1、二分查找

前文 二分查找算法详解 有详细讲解,这里只写最简单的二分算法,旨在突出它的双指针特性:
2021秋招-算法-双指针技巧总结_第8张图片

2.两数之和-剑指offer-167. 两数之和 II - ⭐ 只要数组有序,就应该想到双指针技巧。
class Solution:
    def twoSum(self, numbers: List[int], target: int) -> List[int]:
        # hasMap: 时间复杂度: O(n)       有序数组: 二分查找: O(logN), 最差情况退化为: O(N)
        left, right = 0, len(numbers)-1
        while left <= right:
            # mid = left + (right - left) // 2
            sumValue = numbers[left] + numbers[right]
            if sumValue < target:
                left = left + 1
            elif sumValue > target:
                right = right - 1
            elif sumValue == target:
                return [left+1, right+1]
        return None
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 子串匹配的问题。

快慢指针-leetcode刷题

leetcode-19. 删除链表的倒数第N个节点

# Definition for singly-linked list.
# class ListNode:
#     def __init__(self, x):
#         self.val = x
#         self.next = None

class Solution:
    def removeNthFromEnd(self, head: ListNode, n: int) -> ListNode:
        # 快慢指针
        fastPointer = head
        dummy = slowPointer = ListNode(-1)
        dummy.next = head
        
        while n != 0:
            n -= 1
            fastPointer = fastPointer.next
        while fastPointer:
            slowPointer = slowPointer.next
            fastPointer = fastPointer.next
        slowPointer.next = slowPointer.next.next
        
        return dummy.next

左右指针-leetcode刷题

leetcode-15. 三数之和-中等-未AC

15. 三数之和
给你一个包含 n 个整数的数组 nums,判断 nums 中是否存在三个元素 a,b,c ,使得 a + b + c = 0 ?请你找出所有满足条件且不重复的三元组。
注意:答案中不可以包含重复的三元组。
 
示例:
给定数组 nums = [-1, 0, 1, 2, -1, -4],
满足要求的三元组集合为:
[
  [-1, 0, 1],
  [-1, -1, 2]
]

leetcode-11. 盛最多水的容器-中等-未AC

11. 盛最多水的容器
给你 n 个非负整数 a1,a2,...,an,每个数代表坐标中的一个点 (i, ai) 。在坐标内画 n 条垂直线,垂直线 i 的两个端点分别为 (i, ai) 和 (i, 0)。找出其中的两条线,使得它们与 x 轴共同构成的容器可以容纳最多的水。

说明:你不能倾斜容器,且 n 的值至少为 2。

2021秋招-算法-双指针技巧总结_第9张图片
图中垂直线代表输入数组 [1,8,6,2,5,4,8,3,7]。在此情况下,容器能够容纳水(表示为蓝色部分)的最大值为 49。

示例:
输入:[1,8,6,2,5,4,8,3,7]
输出:49

leetcode-42. 接雨水

209. 长度最小的子数组-中等-未作

你可能感兴趣的:(2021秋招)