简介
双指针技术是一种常见的算法设计思路,它通常适用于处理有序数组或链表等数据结构。双指针算法通过使用两个指针,同时从不同的方向遍历数组或链表,来解决一些特定的问题。
背景
双指针解题思路的产生背景主要有以下几个方面:
数据结构的特点
- 许多需要解决的算法问题都涉及到对有序数组或链表等数据结构的操作。这类数据结构通常有一些特殊的性质,比如前后元素之间存在某种关系,这为使用双指针技术提供了可能。
问题的性质
- 很多算法问题都涉及到在数组或链表中寻找、删除、插入、反转等操作。这些操作通常需要同时处理数据结构的两端或中间元素,双指针技术非常适合解决这类问题。
时间复杂度的要求
- 许多实际问题都需要在线性时间内解决,双指针技术恰好能够在O(n)的时间复杂度内完成许多常见的操作,因此被广泛应用。
空间复杂度的需求
- 相比于使用额外的数据结构,双指针技术通常只需要常量级的额外空间,这在一些对空间敏感的场景下非常有优势。
问题的分解
- 一些复杂的问题可以通过引入双指针的方式进行适当的分解和简化,从而更容易找到最终的解决方案。
总之,双指针技术产生的背景主要源于算法问题的特点以及对时间复杂度和空间复杂度的需求。它是一种非常实用且高效的算法设计思路,在各种类型的算法问题中都有广泛的应用。掌握好这种技巧对于提高算法解题能力非常重要。
应用场景
双指针技术主要有以下几种常见的应用场景:
寻找数组/链表中的重复/unique元素
- 例如:给定一个有序数组,去除其中的重复元素。可以使用双指针,一个指针遍历原数组,另一个指针指向要写入的位置,从而去重。
实例:
def removeDuplicates(nums):"""给定一个有序数组 nums ,在原地删除重复出现的元素,使每个元素只出现一次,返回删除后数组的新长度。"""if not nums:return 0# 使用两个指针i = 0for j in range(1, len(nums)):if nums[j] != nums[i]:i += 1nums[i] = nums[j]return i + 1
这个例子中,我们使用两个指针 i 和 j 来遍历数组。i 指针指向要写入的位置,而 j 指针用于遍历数组。当 nums[j] 与 nums[i] 不同时,我们将 nums[j] 写入 nums[i+1],并将 i 指针向前移动一位。这样就完成了数组去重的过程。
反转数组/链表
- 例如:反转一个链表。可以使用两个指针,一个指向当前节点,另一个指向前一个节点,进行交换操作。
实例:
class ListNode:def __init__(self, val=0, next=None):self.val = valself.next = nextdef reverseList(head):"""反转一个单链表。"""prev, curr = None, headwhile curr:next_node = curr.nextcurr.next = prevprev = currcurr = next_nodereturn prev
在这个例子中,我们使用三个指针:prev、curr 和 next_node。prev 指针指向前一个节点,curr 指针指向当前节点,next_node 指针指向下一个节点。我们通过不断交换这三个指针的指向,最终反转整个链表。
寻找数组/链表中的中点
- 例如:找出一个链表的中间节点。可以使用快慢两个指针,快指针每次移动两步,慢指针每次移动一步,这样当快指针到达末尾时,慢指针正好在中间。
实例:
def middleNode(head):"""给定一个头结点为 head 的非空单链表,返回链表的中间结点。如果有两个中间结点,则返回第二个中间结点。"""slow = fast = headwhile fast and fast.next:slow = slow.nextfast = fast.next.nextreturn slow
在这个例子中,我们使用了快慢两个指针。slow 指针每次移动一步,fast 指针每次移动两步。当 fast 指针到达链表末尾时,slow 指针正好指向链表的中间节点。这是一个经典的找链表中间节点的双指针算法。
寻找数组/链表中的目标元素
- 例如:在一个有序数组中寻找目标元素。可以使用两个指针,分别指向数组的首尾,然后根据目标值与中间元素的大小关系,移动指针缩小搜索范围。
实例1:
def searchElement(nums, target):"""给定一个排序数组和一个目标值,在数组中找到目标值,并返回其索引。如果目标值不存在于数组中,返回它将会被按顺序插入的位置。"""left, right = 0, len(nums) - 1while left <= right:mid = left + (right - left) // 2if nums[mid] == target:return midelif nums[mid] < target:left = mid + 1else:right = mid - 1return left
这是一个在有序数组中查找目标元素的典型双指针算法,也就是二分查找。我们使用左右两个指针left和right来确定查找范围,然后不断缩小范围直到找到目标元素或确定目标元素不存在于数组中。
这种算法在数组中查找目标元素的时间复杂度为O(log n),非常高效。
实例二
类似地,我们也可以在链表中使用双指针来查找目标元素:
class ListNode:def __init__(self, val=0, next=None):self.val = valself.next = nextdef searchElementInList(head, target):"""给定一个链表和一个目标值,找到链表中第一个值等于该目标值的节点,并返回该节点的值。如果链表中不存在这样的节点,返回-1。"""slow, fast = head, headwhile fast:if fast.val == target:return fast.valslow = slow.nextfast = fast.nextreturn -1
在链表中查找目标元素,我们同样使用快慢两个指针来遍历链表。当fast指针找到目标元素时,我们就返回该元素的值。如果遍历完整个链表都没有找到目标元素,我们返回-1表示不存在。
滑动窗口问题
- 例如:找出一个字符串中长度为k的最长无重复子串。可以使用两个指针表示滑动窗口的边界,通过移动指针来维护窗口内无重复元素。
实例:
def lengthOfLongestSubstring(s):"""给定一个字符串,找出其中不含有重复字符的最长子串的长度。"""left, right = 0, 0char_index = {}max_len = 0while right < len(s):if s[right] in char_index and char_index[s[right]] >= left:left = char_index[s[right]] + 1char_index[s[right]] = rightmax_len = max(max_len, right - left + 1)right += 1return max_len
在这个例子中,我们使用左右两个指针来维护一个滑动窗口。right 指针不断向右移动,而当遇到重复字符时,我们移动 left 指针来缩小窗口。这种方法可以高效地找到最长不重复子串。