数据结构--双指针与LeetCodeHOT100

文章目录

      • 1. 引言
        • 简介
        • 目的
      • 2. 双指针技术概述
        • 定义
        • 类型
        • 优势
      • 3. 双指针技术的应用场景
        • 滑动窗口
        • 有序数组
        • 链表问题
      • 4. 判断是否适合使用双指针
        • 1. 问题要求
        • 2. 数据结构特性
        • 3. 问题类型
        • 4. 算法效率
        • 5. 特定问题模式
        • 6. 代码可读性和维护性
        • 7. 实际应用案例
        • 8. 测试和验证
      • 5. 一些双指针的具体示例
        • 示例1:滑动窗口最大值
        • 示例2:快慢指针寻找循环链表的入口
        • 示例3:有序数组的平方对
        • 示例4:合并两个有序链表
      • 6. LeetCode HOT100题目
        • 题目一:283.移动零
        • 题目二:11. 盛最多水的容器
        • 题目三:15. 三数之和
        • 题目四:42. 接雨水

1. 引言

简介

在编程和算法设计中,双指针技术是一种常用且强大的工具。它涉及使用两个指针在数据结构上进行操作,以解决特定的问题。双指针技术常见于数组、链表、字符串等数据结构的处理中,尤其在处理需要在数据集合中进行遍历、搜索和排序的场景时表现出色。

目的

本文的目的是提供一个关于双指针技术的全面概述,并展示它在解决一些经典问题时的应用。通过具体的例子和代码实现,读者将能够理解双指针的核心概念,掌握其使用方法,并学会如何将这一技术应用于解决实际编程问题。

2. 双指针技术概述

定义

双指针是一种算法策略,它使用两个游标(或索引)来遍历或操作数据结构中的元素。这两个指针可以以不同的速度移动,或者从不同的起点开始,以实现不同的算法目标。

类型
  • 固定指针和滑动指针:固定指针保持在数组的起始位置,而滑动指针根据条件移动。
  • 滑动窗口:两个指针定义了一个窗口,这个窗口可以随着滑动指针的移动而扩展或收缩。
  • 快慢指针:一个指针移动得比另一个快,常用于链表中寻找中间节点或检测环。
优势

使用双指针技术可以带来多方面的优势:

  • 减少时间复杂度:通过避免不必要的遍历,可以显著减少算法的执行时间。
  • 空间效率:双指针通常不需要额外的存储空间,因此空间复杂度较低。
  • 简化问题:将复杂问题分解为更简单的子问题,使得解决方案更加直观易懂。

3. 双指针技术的应用场景

滑动窗口

滑动窗口问题是一种常见的双指针应用,其中窗口的两个端点定义了一个连续的子数组。这类问题通常要求在给定大小的窗口内找到最大值、最小值或其他统计数据。例如,找到一个字符串中不重复字符的最大子字符串长度。

有序数组

在有序数组中,双指针可以用于解决诸如“在两个有序数组中找到第k小的元素”的问题。通过从两个数组的两端开始,可以有效地找到满足条件的元素,而不需要合并整个数组。

链表问题

链表问题中,双指针技术可以用来找到链表的中间节点、检测环、合并两个有序链表等。例如,使用快慢指针可以有效地找到单链表的中点,而无需额外的存储空间或复杂的逻辑。

通过上述概述,我们可以看到双指针技术在算法设计中的广泛应用和重要性。在接下来的部分,我们将深入探讨具体的应用实例和代码实现,以进一步展示双指针技术的强大功能。

4. 判断是否适合使用双指针

在编程中,判断一个数据结构是否适合使用双指针技术通常涉及考虑问题的性质和数据结构的特点。以下是一些关键点和步骤,可以帮助你快速做出判断:

1. 问题要求
  • 遍历:如果问题需要多次遍历数据结构,双指针可以减少遍历次数。
  • 比较:如果需要在数据结构中进行元素间的比较,双指针可以方便地进行这种操作。
  • 排序:如果问题涉及到排序或部分排序,双指针可以帮助快速找到特定顺序的元素。
2. 数据结构特性
  • 数组和列表:线性数据结构如数组和列表是双指针技术的理想选择,因为它们允许随机访问。
  • 链表:对于链表,双指针可以用于遍历和解决特定问题,如找到中间节点或检测环。
  • 字符串:双指针技术可以用于处理字符串中的子字符串问题,如滑动窗口或模式匹配。
3. 问题类型
  • 滑动窗口:如果问题涉及到在数据集中移动一个窗口并计算窗口内的数据,双指针是一个很好的选择。
  • 双端搜索:需要从两端向中间搜索满足条件的元素时,双指针可以提高效率。
  • 有序数据:在有序数组中查找特定元素或解决其他相关问题时,双指针可以减少搜索范围。
4. 算法效率
  • 时间复杂度:如果使用单指针会导致较高的时间复杂度,考虑使用双指针可能有助于降低复杂度。
  • 空间复杂度:如果问题需要额外的空间来存储中间结果,双指针可能通过减少这种需求来提高空间效率。
5. 特定问题模式
  • 寻找最大/最小值:在特定条件下寻找最大或最小值,如在窗口内或满足特定顺序的元素。
  • 检测环:在链表中检测环的存在,双指针是经典的解决方案。
  • 合并有序数组:合并两个有序数组,双指针可以有效地找到并合并元素。
6. 代码可读性和维护性
  • 直观性:双指针方法通常更直观,易于理解和维护。
  • 减少复杂性:通过减少嵌套循环和其他复杂逻辑,双指针可以使代码更简洁。
7. 实际应用案例
  • 快速排序:在快速排序的某些变体中,双指针可以用于划分数组。
  • 二分查找:虽然二分查找通常使用单指针,但在某些情况下,双指针可以用于优化二分查找的变体。
8. 测试和验证
  • 初步实现:尝试用双指针实现问题解决方案,并与单指针或其他方法进行比较。
  • 性能测试:通过实际测试验证双指针方法的性能,确保它确实提供了效率上的优势。

通过以上步骤和考虑因素,你可以更系统地评估一个数据结构是否适合使用双指针技术,并据此选择最合适的算法解决方案。

5. 一些双指针的具体示例

当然,以下是一些使用双指针技术的代码示例,涵盖了不同的应用场景:

示例1:滑动窗口最大值

给定一个整数数组 nums 和一个窗口大小 k,找出所有窗口位置的最大值。

def maxSlidingWindow(nums, k):
    max_queue = []
    res = []
    for i in range(len(nums)):
        # 维护窗口内的最大值
        while max_queue and max_queue[0] < i - k + 1:
            max_queue.pop(0)
        if max_queue:
            res.append(nums[max_queue[0]])
        else:
            res.append(nums[i])
        while max_queue and nums[max_queue[-1]] < nums[i]:
            max_queue.pop()
        max_queue.append(i)
    return res

# 示例
nums = [1, 3, -1, -3, 5, 3, 6, 7]
k = 3
print(maxSlidingWindow(nums, k))  # 输出: [3, 3, 5, 5, 6, 7]
示例2:快慢指针寻找循环链表的入口

给定一个可能包含环的链表,使用快慢指针找到环的入口。

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

def detectCycle(head):
    slow = fast = 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

# 示例
# 创建链表 1 -> 2 -> 3 -> 4 -> 5
# 并让 5 -> 3 形成环
node1 = ListNode(1)
node2 = ListNode(2)
node3 = ListNode(3)
node4 = ListNode(4)
node5 = ListNode(5)
node1.next = node2
node2.next = node3
node3.next = node4
node4.next = node5
node5.next = node3

cycle_entry = detectCycle(node1)
if cycle_entry:
    print("Cycle detected at node with value:", cycle_entry.val)
else:
    print("No cycle detected.")
示例3:有序数组的平方对

给定一个有序数组,找出数组中平方和为特定值的所有对。

def find_pairs(nums, target):
    left, right = 0, len(nums) - 1
    pairs = []
    while left < right:
        current_sum = nums[left] + nums[right]
        if current_sum == target:
            pairs.append((nums[left], nums[right]))
            left += 1
            right -= 1
        elif current_sum < target:
            left += 1
        else:
            right -= 1
    return pairs

# 示例
nums = [1, 2, 3, 4, 5]
target = 8
print(find_pairs(nums, target))  # 输出: [(3, 5), (4, 4)]
示例4:合并两个有序链表

给定两个有序链表,将它们合并为一个新的有序链表。

def mergeTwoLists(l1, l2):
    dummy = ListNode(0)
    current = dummy
    while l1 and l2:
        if l1.val < l2.val:
            current.next = l1
            l1 = l1.next
        else:
            current.next = l2
            l2 = l2.next
        current = current.next
    current.next = l1 or l2
    return dummy.next

# 示例
# 创建两个有序链表 1 -> 2 -> 4 和 1 -> 3 -> 4
l1 = ListNode(1)
l1.next = ListNode(2)
l1.next.next = ListNode(4)

l2 = ListNode(1)
l2.next = ListNode(3)
l2.next.next = ListNode(4)

merged_list = mergeTwoLists(l1, l2)
while merged_list:
    print(merged_list.val, end=" -> ")
    merged_list = merged_list.next
print("None")

6. LeetCode HOT100题目

题目一:283.移动零
  • 问题描述:给定一个数组 nums,编写一个函数将所有 0 移动到数组的末尾,同时保持非零元素的相对顺序。

请注意 ,必须在不复制数组的情况下原地对数组进行操作。

示例 1:

输入: nums = [0,1,0,3,12]
输出: [1,3,12,0,0]
示例 2:

输入: nums = [0]
输出: [0]

  • 解决方案

  • 初始化指针:设置一个指针 last_non_zero 指向数组的开始位置,这个指针用于记录最后一个非零元素的位置。

  • 遍历数组:从左到右遍历数组 nums 中的每个元素。

  • 检查当前元素:对于数组中的每个元素:

    • 如果当前元素 nums[i] 不等于零(即 nums[i] != 0),则需要将其移动到 last_non_zero 指向的位置。
    • 将 nums[last_non_zero] 赋值为当前元素 nums[i],这样非零元素就被移动到了数组的前端。
    • 更新 last_non_zero 指针,将其向前移动一位,指向下一个非零元素应该放置的位置。
  • 填充零:在遍历结束后,last_non_zero 指针之后的所有位置都应该是零。由于题目要求保持原地操作,我们可以通过一个循环,从 last_non_zero 开始,将所有剩余的位置填充为零。

  • 返回结果:遍历完成后,数组的前 last_non_zero 个位置是非零元素,且它们的相对顺序保持不变,之后的所有位置都是零。此时,返回修改后的数组。

  • 代码示例

class Solution(object):
    def moveZeroes(self, nums):
        # 初始化最后一个非零元素的位置
        last_non_zero = 0
        for i in range(len(nums)):
            # 如果当前元素是非零的,将其移动到数组前端
            if nums[i] != 0:
                nums[last_non_zero] = nums[i]
                last_non_zero += 1
        # 将剩余的位置填充为0
        while last_non_zero < len(nums):
            nums[last_non_zero] = 0
            last_non_zero += 1
        return nums
  • 结果分析
  • 在这里插入图片描述
题目二:11. 盛最多水的容器
  • 问题描述

给定一个长度为 n 的整数数组 height 。有 n 条垂线,第 i 条线的两个端点是 (i, 0) 和 (i, height[i]) 。

找出其中的两条线,使得它们与 x 轴共同构成的容器可以容纳最多的水。

返回容器可以储存的最大水量。

说明:你不能倾斜容器。
在这里插入图片描述

  • 解决方案
  • 初始化两个指针 l 和 r,分别指向数组的左右两端,即 l = 0 和 r = n - 1。
    计算左右指针所指向线段作为容器的两端时,能够存储的水的体积。
    移动导致当前体积更小的一端的指针,然后重复步骤2和3,直到两个指针相遇。
  • 代码示例
class Solution(object):
    def maxArea(self, height):
        """
        :type height: List[int]
        :rtype: int
        """
        l, r = 0, len(height) - 1  # 初始化左右指针
        max_water = 0  # 初始化最大水量为0

        while l <= r:  # 当左指针小于等于右指针时循环
            max_water = max(max_water, (r - l) * min(height[l], height[r]))  # 更新最大水量
            if height[l] < height[r]:  # 如果左侧线段矮,移动左指针
                l += 1
            else:  # 否则移动右指针
                r -= 1

        return max_water  # 返回计算出的最大水量
  • 结果分析在这里插入图片描述
题目三:15. 三数之和
  • 问题描述:给你一个整数数组 nums ,判断是否存在三元组 [nums[i], nums[j], nums[k]] 满足 i != j、i != k 且 j != k ,同时还满足 nums[i] + nums[j] + nums[k] == 0 。请你返回所有和为 0 且不重复的三元组。
  • 解决方案
  • 首先对数组 nums 进行排序。
    使用两个指针 left 和 right 分别指向当前考虑的元素的两侧,第三个数 mid 作为当前指向的元素。
    检查 mid + left + right 是否等于 0。
    如果和小于 0,则移动 left 指针向右增加和。
    如果和大于 0,则移动 right 指针向左减少和。
    如果找到满足条件的三元组,记录下来,并移动指针继续查找下一个可能的三元组。
    确保不重复记录相同的三元组
  • 代码示例
class Solution(object):
    def threeSum(self, nums):
        """
        :type nums: List[int]
        :rtype: List[List[int]]
        """
        nums.sort()  # 对数组进行排序
        result = []  # 初始化结果列表

        for i in range(len(nums) - 2):
            if i > 0 and nums[i] == nums[i - 1]:
                continue  # 跳过重复的元素

            left, right = i + 1, len(nums) - 1
            while left < right:
                total = nums[i] + nums[left] + nums[right]
                if total < 0:
                    left += 1
                elif total > 0:
                    right -= 1
                else:
                    result.append([nums[i], nums[left], nums[right]])
                    while left < right and nums[left] == nums[left + 1]:
                        left += 1  # 跳过重复的元素
                    while left < right and nums[right] == nums[right - 1]:
                        right -= 1
                    left += 1
                    right -= 1

        return result
  • 结果分析在这里插入图片描述
题目四:42. 接雨水
  • 问题描述:给定 n 个非负整数表示每个宽度为 1 的柱子的高度图,计算按此排列的柱子,下雨之后能接多少雨水。
  • 解决方案
  • 初始化两个指针 left 和 right,分别指向数组的起始和结束位置。
    初始化两个变量 left_max 和 right_max 分别存储 left 和 right 指针之前遇到的最大高度。
    初始化 res 为 0,用于存储结果,即能接住的雨水量。
    同时向中间遍历 left 和 right 指针,更新 left_max 和 right_max,并计算雨水量。
    当 left 指针小于 right 指针时,继续循环:
    如果 height[left] < height[right],则当前柱子的积水能力取决于左侧的最大高度,更新 left_max,并计算 res。
    否则,右侧同理。
    返回 res。
  • 代码示例
class Solution(object):
    def trap(self, height):
        """
        :type height: List[int]
        :rtype: int
        """
        n = len(height)
        if n < 3:
            return 0

        left, right = 0, n - 1
        left_max, right_max = 0, 0
        res = 0

        while left < right:
            if height[left] < height[right]:
                left_max = max(left_max, height[left])
                res += max(0, left_max - height[left])
                left += 1
            else:
                right_max = max(right_max, height[right])
                res += max(0, right_max - height[right])
                right -= 1

        return res
  • 结果分析在这里插入图片描述
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值