双指针算法是一种非常常见且实用的算法技巧,在解决各种类型的问题时都有广泛应用。下面我将为您详细介绍双指针算法的基本概念、常见应用场景,并给出具体的实战示例。
一、双指针算法概述
双指针算法,顾名思义,就是使用两个指针来解决问题。这两个指针通常从数组/链表的两端或者一端开始移动,逐步缩小搜索范围,直到找到满足条件的解。相比于暴力枚举法,双指针算法往往具有更高的时间和空间复杂度优势。
双指针算法的基本思路如下:
- 初始化两个指针 left 和 right,分别指向数组/链表的开始和结束位置。
- 根据具体问题,定义指针移动的规则和终止条件。通常是当 left 小于 right 时继续移动指针。
- 根据指针指向的元素值执行相应的操作,直到找到满足条件的解。
与传统的单指针算法相比,双指针算法更加高效和灵活。它可以应用于解决各种类型的问题,如数组、链表、字符串等。下面我们来看一些常见的双指针算法应用场景。
二、双指针算法的常见应用
- 数组问题
(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) 反转链表 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) 回文字符串
给定一个字符串 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。
通过以上几个示例,相信您已经对双指针算法有了初步的了解。下面我们来看一些更加复杂的双指针算法应用。
三、双指针算法的进阶应用
- 滑动窗口
滑动窗口是一种特殊的双指针算法,用于解决数组/字符串中某个子数组/子串的问题。它维护一个窗口,通过移动窗口的边界来调整窗口大小,从而找到满足条件的子数组/子串。
示例: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。
- 双指针排序
在某些问题中,我们需要对数组进行排序,此时可以使用双指针算法来优化排序过程。
示例: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)。
- 快慢指针
快慢指针是双指针算法的一个变种,通常用于解决链表相关的问题。
示例: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。
通过以上三种进阶应用,相信您已经对双指针算法有了更深入的了解。接下来我们再总结一下双指针算法的优缺点。
四、双指针算法的优缺点
优点:
- 时间复杂度低:相比于暴力枚举法,双指针算法的时间复杂度通常为 O(n)。
- 空间复杂度低:大多数情况下,双指针算法只需要常量级的额外空间。
- 代码简洁易懂:双指针算法的思路清晰,代码实现简单明了。
缺点:
- 适用场景有限:双指针算法主要适用于数组、链表和字符串等线性数据结构,对于树、图等非线性数据结构的应用较
该博文为原创文章,未经博主同意不得转载。本文章博客地址:https://blog.csdn.net/weixin_39145520/article/details/134889410