详解双指针算法

双指针算法是一种非常常见且实用的算法技巧,在解决各种类型的问题时都有广泛应用。下面我将为您详细介绍双指针算法的基本概念、常见应用场景,并给出具体的实战示例。

一、双指针算法概述

双指针算法,顾名思义,就是使用两个指针来解决问题。这两个指针通常从数组/链表的两端或者一端开始移动,逐步缩小搜索范围,直到找到满足条件的解。相比于暴力枚举法,双指针算法往往具有更高的时间和空间复杂度优势。

双指针算法的基本思路如下:

  1. 初始化两个指针 left 和 right,分别指向数组/链表的开始和结束位置。
  2. 根据具体问题,定义指针移动的规则和终止条件。通常是当 left 小于 right 时继续移动指针。
  3. 根据指针指向的元素值执行相应的操作,直到找到满足条件的解。

与传统的单指针算法相比,双指针算法更加高效和灵活。它可以应用于解决各种类型的问题,如数组、链表、字符串等。下面我们来看一些常见的双指针算法应用场景。

二、双指针算法的常见应用

  1. 数组问题

(1) 排序数组去重

给定一个排序数组,你需要在 原地 删除重复出现的元素,使每个元素只出现一次,返回移除后数组的新长度。

示例代码:

python

def removeDuplicates(nums):

if not nums:

    return 0



left = 0

for right in range(1, len(nums)):

    if nums[right] != nums[left]:

        left += 1

        nums[left] = nums[right]



return left + 1

算法解析:

  • 初始化两个指针 left 和 right,left 指向已处理元素的最后一个位置。
  • 遍历数组,当 nums[right] 与 nums[left] 不同时,说明 nums[right] 是一个新元素,将其赋值给 nums[left+1],并将 left 指针向右移动一位。
  • 最终 left 指针的值加 1 就是数组的新长度。

(2) 两数之和

给定一个整数数组 nums 和一个目标值 target,请你在该数组中找出和为目标值的那 两个 整数,并返回他们的数组下标。

示例代码:

def twoSum(nums, target):
    left, right = 0, len(nums) - 1
    while left < right:
        curr_sum = nums[left] + nums[right]
        if curr_sum == target:
            return [left, right]
        elif curr_sum < target:
            left += 1
        else:
            right -= 1
    return []

算法解析:

  • 初始化两个指针 left 和 right,分别指向数组的首尾。
  • 计算当前两个指针指向的元素之和 curr_sum。
  • 如果 curr_sum 等于 target,则返回两个元素的下标。
  • 如果 curr_sum 小于 target,说明需要增大当前和,因此将 left 指针向右移动一位。
  • 如果 curr_sum 大于 target,说明需要减小当前和,因此将 right 指针向左移动一位。
  • 重复上述步骤直到找到满足条件的两个元素。
  1. 链表问题

(1) 反转链表 II

给你单链表的头指针 head 和两个整数 left 和 right ,其中 left <= right。请你反转从位置 left 到位置 right 的链表节点。

示例代码:

class ListNode:
    def __init__(self, val=0, next=None):
        self.val = val
        self.next = next

def reverseBetween(head, left, right):
    if not head or left == right:
        return head
    
    dummy = ListNode(0)
    dummy.next = head
    pre = dummy
    
    # 找到 left 位置的前一个节点
    for _ in range(left - 1):
        pre = pre.next
    
    curr = pre.next
    for _ in range(right - left):
        next_node = curr.next
        curr.next = next_node
        next_node.next, pre.next = curr, next_node
        
    return dummy.next

算法解析:

  • 创建一个虚拟头节点 dummy,将链表头节点 head 连接到 dummy 上。
  • 找到 left 位置的前一个节点 pre。
  • 反转从 pre 开始的 right-left+1 个节点。
  • 更新 pre.next 和 curr.next 的指向,完成反转操作。
  • 返回 dummy.next 作为新的头节点。

(2) 环形链表 II

给定一个链表,返回链表开始入环的第一个节点。 如果链表无环,则返回 null。

示例代码:

class ListNode:
    def __init__(self, val=0, next=None):
        self.val = val
        self.next = next

def detectCycle(head):
    if not head or not head.next:
        return None
    
    slow, fast = head, head
    while fast and fast.next:
        slow = slow.next
        fast = fast.next.next
        if slow == fast:
            break
    
    if not fast or not fast.next:
        return None
    
    slow = head
    while slow != fast:
        slow = slow.next
        fast = fast.next
    
    return slow

算法解析:

  • 使用快慢指针traversal 链表,当快慢指针相遇时,说明链表存在环。
  • 此时快指针和慢指针到环的入口距离相同,只需要将慢指针重新指向链表头,两个指针以相同速度移动,直至再次相遇,即为环的入口节点。
  • 如果链表不存在环,fast 指针会先到达链表末尾,此时返回 None。
  1. 字符串问题

(1) 回文字符串

给定一个字符串 s,验证 s 是否是回文串。

示例代码:

def isPalindrome(s):
    left, right = 0, len(s) - 1
    while left < right:
        while left < right and not s[left].isalnum():
            left += 1
        while left < right and not s[right].isalnum():
            right -= 1
        if left < right and s[left].lower() != s[right].lower():
            return False
        left += 1
        right -= 1
    return True

算法解析:

  • 初始化左右指针 left 和 right。
  • 当 left 小于 right 时,进行以下操作:
    • 如果 s[left] 不是数字或字母,将 left 指针向右移动。
    • 如果 s[right] 不是数字或字母,将 right 指针向左移动。
    • 如果 s[left] 和 s[right] 对应的字符不相同(忽略大小写),返回 False。
    • 否则,将 left 和 right 指针分别向内移动一位。
  • 如果循环结束,说明字符串是回文串,返回 True。

(2) 有效的括号

给定一个只包括 ‘(’、‘)’、‘{’、‘}’、‘[‘和’]’ 的字符串 s ,判断字符串是否有效。

示例代码:

def isValid(s):
    stack = []
    for char in s:
        if char in ['(', '[', '{']:
            stack.append(char)
        else:
            if not stack:
                return False
            current_char = stack.pop()
            if current_char == '(':
                if char != ')':
                    return False
            if current_char == '[':
                if char != ']':
                    return False
            if current_char == '{':
                if char != '}':
                    return False
    return len(stack) == 0

算法解析:

  • 遍历字符串 s,对于每个字符:
    • 如果是左括号,将其压入栈中。
    • 如果是右括号,检查栈是否为空,为空则返回 False;否则弹出栈顶元素,检查是否与当前右括号匹配,不匹配则返回 False。
  • 遍历完成后,如果栈为空,说明所有括号都已经匹配,返回 True;否则返回 False。

通过以上几个示例,相信您已经对双指针算法有了初步的了解。下面我们来看一些更加复杂的双指针算法应用。

三、双指针算法的进阶应用

  1. 滑动窗口

滑动窗口是一种特殊的双指针算法,用于解决数组/字符串中某个子数组/子串的问题。它维护一个窗口,通过移动窗口的边界来调整窗口大小,从而找到满足条件的子数组/子串。

示例:Longest Substring Without Repeating Characters

给定一个字符串 s ,请你找出其中不含有重复字符的 最长子串 的长度。

def lengthOfLongestSubstring(s):
    left, right = 0, 0
    seen = set()
    max_len = 0
    
    while right < len(s):
        if s[right] not in seen:
            seen.add(s[right])
            max_len = max(max_len, right - left + 1)
            right += 1
        else:
            seen.remove(s[left])
            left += 1
    
    return max_len

算法解析:

  • 初始化左右指针 left 和 right,以及一个用于记录当前窗口中字符的集合 seen。
  • 使用 right 指针不断向右移动,并将新字符加入 seen 集合。
  • 如果 seen 集合中已经存在该字符,则使用 left 指针移除左侧字符,直到该字符从 seen 集合中移除。
  • 更新最大长度 max_len。
  • 重复上述步骤直到 right 指针到达字符串末尾。
  • 返回最大长度 max_len。
  1. 双指针排序

在某些问题中,我们需要对数组进行排序,此时可以使用双指针算法来优化排序过程。

示例:Merge Sorted Array

给你两个有序整数数组 nums1 和 nums2,请你将 nums2 合并到 nums1 中,使 nums1 成为一个有序数组。

def merge(nums1, m, nums2, n):
    # 从后向前遍历
    p1, p2, p = m - 1, n - 1, m + n - 1
    while p1 >= 0 and p2 >= 0:
        if nums1[p1] > nums2[p2]:
            nums1[p] = nums1[p1]
            p1 -= 1
        else:
            nums1[p] = nums2[p2]
            p2 -= 1
        p -= 1
    
    # 如果 nums2 还有剩余,则直接合并到 nums1 前面
    nums1[:p2 + 1] = nums2[:p2 + 1]
    
    return nums1

算法解析:

  • 从 nums1 和 nums2 的末尾开始比较,将较大的元素放到 nums1 的末尾。
  • 当 nums1 中的元素都放完后,如果 nums2 中还有剩余,则直接合并到 nums1 的前面。
  • 这样可以避免额外的空间消耗,时间复杂度为 O(m+n)。
  1. 快慢指针

快慢指针是双指针算法的一个变种,通常用于解决链表相关的问题。

示例:Linked List Cycle

给定一个链表,判断链表中是否有环。

class ListNode:
    def __init__(self, val=0, next=None):
        self.val = val
        self.next = next

def hasCycle(head):
    if not head or not head.next:
        return False
    
    slow, fast = head, head.next
    while fast and fast.next:
        slow = slow.next
        fast = fast.next.next
        if slow == fast:
            return True
    
    return False

算法解析:

  • 初始化两个指针 slow 和 fast,slow 指针每次移动一步,fast 指针每次移动两步。
  • 如果链表中存在环,那么 slow 指针和 fast 指针最终一定会相遇。
  • 如果链表中不存在环,fast 指针最终会到达链表末尾(next 为 None),此时 fast 或 fast.next 为 None,函数返回 False。

通过以上三种进阶应用,相信您已经对双指针算法有了更深入的了解。接下来我们再总结一下双指针算法的优缺点。

四、双指针算法的优缺点

优点:

  1. 时间复杂度低:相比于暴力枚举法,双指针算法的时间复杂度通常为 O(n)。
  2. 空间复杂度低:大多数情况下,双指针算法只需要常量级的额外空间。
  3. 代码简洁易懂:双指针算法的思路清晰,代码实现简单明了。

缺点:

  1. 适用场景有限:双指针算法主要适用于数组、链表和字符串等线性数据结构,对于树、图等非线性数据结构的应用较

该博文为原创文章,未经博主同意不得转载。本文章博客地址:https://blog.csdn.net/weixin_39145520/article/details/134889410

  • 11
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
数据结构和算法是计算机科学中非常重要的两个概念。数据结构是一种组织和存储数据的方式,而算法是解决问题的步骤和方法。在计算机科学中,有许多经典的数据结构和算法,被广泛应用于各种领域。 以下是十大经典数据结构和算法的简要介绍: 1. 数组(Array):是一种线性数据结构,可以存储相同类型的元素。数组的访问速度快,但插入和删除操作较慢。 2. 链表(Linked List):也是一种线性数据结构,由节点组成,每个节点包含数据和指向下一个节点的指针。链表适用于频繁的插入和删除操作。 3. 栈(Stack):是一种后进先出(LIFO)的数据结构,只能在栈顶进行插入和删除操作。 4. 队列(Queue):是一种先进先出(FIFO)的数据结构,只能在队尾插入,在队头删除。 5. 树(Tree):是一种非线性数据结构,由节点和边组成。树有许多种类型,如二叉树、平衡树、堆等。 6. 图(Graph):也是一种非线性数据结构,由节点和边组成。图可以表示各种实际问题,如网络、社交关系等。 7. 哈希表(Hash Table):使用哈希函数将数据存储在数组中,可以快速查找、插入和删除数据。 8. 排序算法(Sorting Algorithm):如冒泡排序、插入排序、快速排序等,用于将数据按照某种规则进行排序。 9. 查找算法(Search Algorithm):如线性查找、二分查找等,用于在数据集中查找特定元素。 10. 图算法(Graph Algorithm):如最短路径算法(Dijkstra算法)、深度优先搜索算法(DFS)、广度优先搜索算法(BFS)等,用于解决图相关的问题。 以上是十大经典数据结构和算法的简要介绍,每个数据结构和算法都有其特点和适用场景,深入学习它们可以帮助我们更好地理解和解决实际问题。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

一个天秤座的程序猿

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值