数据结构--双指针与LeetCodeHOT100

devtools/2024/10/18 5:57:10/

文章目录

      • 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 = xself.next = Nonedef detectCycle(head):slow = fast = headwhile fast and fast.next:slow = slow.nextfast = fast.next.nextif slow == fast:breakif not fast or not fast.next:return Noneslow = headwhile slow != fast:slow = slow.nextfast = fast.nextreturn 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 = node3cycle_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) - 1pairs = []while left < right:current_sum = nums[left] + nums[right]if current_sum == target:pairs.append((nums[left], nums[right]))left += 1right -= 1elif current_sum < target:left += 1else:right -= 1return 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 = dummywhile l1 and l2:if l1.val < l2.val:current.next = l1l1 = l1.nextelse:current.next = l2l2 = l2.nextcurrent = current.nextcurrent.next = l1 or l2return 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 = 0for i in range(len(nums)):# 如果当前元素是非零的,将其移动到数组前端if nums[i] != 0:nums[last_non_zero] = nums[i]last_non_zero += 1# 将剩余的位置填充为0while last_non_zero < len(nums):nums[last_non_zero] = 0last_non_zero += 1return 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  # 初始化最大水量为0while l <= r:  # 当左指针小于等于右指针时循环max_water = max(max_water, (r - l) * min(height[l], height[r]))  # 更新最大水量if height[l] < height[r]:  # 如果左侧线段矮,移动左指针l += 1else:  # 否则移动右指针r -= 1return 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) - 1while left < right:total = nums[i] + nums[left] + nums[right]if total < 0:left += 1elif total > 0:right -= 1else: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 -= 1left += 1right -= 1return 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 0left, right = 0, n - 1left_max, right_max = 0, 0res = 0while left < right:if height[left] < height[right]:left_max = max(left_max, height[left])res += max(0, left_max - height[left])left += 1else:right_max = max(right_max, height[right])res += max(0, right_max - height[right])right -= 1return res
  • 结果分析在这里插入图片描述

http://www.ppmy.cn/devtools/93974.html

相关文章

使用 Node.js 模拟执行 JavaScript

准备工作 正确安装好 Node.js ,安装好之后&#xff0c;能正常使用 node 和 npm 两个命令 模拟执行 关于案例分析 写文章-CSDN创作中心 这里就不做分析了&#xff0c;直接使用 我们的目的是&#xff1a; 使用 node.js 加载 Crypto 库&#xff0c; 并执行 getToken 方法 …

月薪5W的项目经理是如何面试的?这份面试攻略请收好!

面试是项目经理求职必须经历的一关&#xff0c;但很多经验不够丰富的项目经理不知道面试会问些什么问题&#xff0c;也不知道要怎么回答&#xff0c;无疑会直接影响面试企业的判断&#xff0c;使项目经理求职受阳.所以&#xff0c;项目经理想要顺利求职&#xff0c;还是有必要掌…

# [0813] Task01 Datawhale AI 夏令营 —— 数据合成与清洗

参考教程链接&#xff1a;注意查看 评论区相关内容 Q & A 汇总 赛事链接&#xff1a;天池 Better Synth 多模态大模型数据合成挑战赛 算力资源 挺烧钱的。。 之前的教程有些问题没考虑到&#xff0c;排坑花了 200 了。。 跑通 baseline 至少需要 120G 内存以上的 A10&am…

Qt读写sysfs

本文介绍Qt读写sysfs。 在嵌入式Linux系统上开发Qt应用程序&#xff0c;经常会涉及到外设的控制&#xff0c;比如GPIO&#xff0c;PWM的控制&#xff0c;Linux环境下可以像操作文件一样操作它们&#xff0c;这通常会涉及到sysfs的读写。本文以读写GPIO为例&#xff0c;简要介绍…

Linux部署MySQL8.0

目录 一、部署前准备1.1、查看系统版本和位数&#xff08;32位或64位&#xff09;1.2、下载对应安装包 二、开始部署1、将安装包解压并且移动到目标安装目录2、准备MySQL数据和日志等存储文件夹3、准备MySQL配置文件 my.cnf4、创建mysql单独用户组和用户&#xff0c;将安装目录…

Android控件详解

在Android应用程序中&#xff0c;界面由布局和组件组成。布局相当于框架&#xff0c;而控件则是框架里面的内容。了解过Android布局后&#xff0c;如果要设计ui界面&#xff0c;还需要了解和掌握各个控件的应用。 一个界面的设计&#xff0c;先从创建容器开始&#xff0c;再向…

重启redis服务时报错:Failed to start redis.service: Unit not found

重启redis服务时报错&#xff1a;Failed to start redis.service: Unit not found redis配合安全修改了bind和auth配置&#xff0c;重启的时候报错了&#xff0c;试了很多方法&#xff0c;最后才通过日志解决了 1 重新加载systemd 配置并启动&#xff1a; sudo systemctl da…

Mojo中低水平的IR介绍

Mojo是一种具有广泛现代功能的高级编程语言。Mojo还为程序员提供了访问所有底层原语的机会,以便编写强大而零成本的抽象。 ​ 这些原语用MLIR实现,MLIR是一种用于编译器设计的可扩展中间表示格式。许多不同的编程语言和编译器将它们的源程序翻译成MLIR,因为Mojo提供了直接访…