[LeetCode]数组部分记录
[LeetCode]数组部分记录
跟随《代码随想录》刷题,12.11~12.14 小记录。
下面我按照做题顺序,梳理一下目前做的题目。既是一个算法做题的分享,也是自己回顾知识,增强记忆点的方式。最近题目的特点是,基本最优解都是「双指针」解法。
前言—关于双指针算法
双指针算法是一种常用于数组(或链表)操作的算法技巧,它通过维护两个指针,分别指向序列中的不同位置,以解决一些特定问题。这两个指针可以是同方向移动,也可以是相反方向移动。
常见的双指针算法有三种类型:
快慢指针: 主要用于解决链表中的问题,例如判断链表是否有环,找到链表的中间节点等。一个指针移动速度快(快指针),另一个指针移动速度慢(慢指针),从而在链表中实现一种追赶的效果。
左右指针: 主要用于数组或字符串的问题,例如在有序数组中找到两个数使它们的和等于目标值。左指针指向序列的开头,右指针指向序列的末尾,根据题目的要求,移动左右指针来达到特定的条件。
滑动窗口: 通常用于解决子数组或子串的问题,例如找到最小覆盖子串、最长无重复字符的子串等。通过维护一个区间(窗口),其中的元素满足一定的条件,然后根据条件移动窗口的起始位置或结束位置。
这些双指针算法的核心思想是通过控制指针的移动,有效地减小问题的规模,从而提高算法的效率。在解决一些数组、链表或字符串相关的问题时,双指针算法是一个非常有用的工具。
一、快慢指针
1. 典型题目
26. 删除有序数组中的重复项
输入: [0,0,1,1,1,2,2,3,3,4] or [1,2,3,4,4,4,5,5,6]
输出: [0,1,2,3,4] or [1,2,3,4,5,6]
下面是我的题解,核心是初始化:i = 0, j = i + 1
,这样方便比较相邻数字是否重复。如果没重复,i 和 j都往右移,否则,i 不动,移动 j,直到 j 找到一个不相同的数,覆盖掉 i 的右侧重复数据。这样遍历下去,就能把重复项去除,返回 i + 1 表示新数组长度。
1 | class Solution { |
27.移除元素
输入:nums = [3,2,2,3], val = 3
输出:2, nums = [2,2]
题解如下,这里还是用到了快慢指针,也是用快指针 j 遍历,慢指针 i 标记有效元素最后的位置。
1 | class Solution { |
283. 移动零
输入:[0,1,0,3,12]
输出:[1,3,12,0,0]
题解如下,j 用于遍历数组找非 0 元素,找到了就和 i 做交换,让 i 始终指向非 0 元素。
1 | class Solution { |
844. 比较含退格的字符串
输入:s = “ab#c”, t = “ad#c”
输出:true
解释:s 和 t 都会变成 “ac”。
虽然这个题目的难度给的是简单的,但是实际我觉得按照时间复杂度
一、栈处理
代码如下,核心就是创建一个栈stk,便利字符串,如果字符是#
,则不入栈且将栈顶元素出栈;如果是非#
的字符,则入栈。处理完两个字符串后,比较两个栈即可得出是否相等。时间复杂度:$$O(n)$$,空间复杂度:$$O(n)$$。
1 | class Solution { |
二、双指针
代码如下,当字符串中存在退格符 #
时,我们可以使用两个指针从字符串末尾向前遍历,跳过相应的退格符,并比较字符是否相等。这种方法的思想是通过逐个字符的比较,并在遇到退格符时调整指针位置,最终确定两个经过退格操作后的字符串是否相等。这样可以在一次遍历中完成比较,避免使用额外的空间。
定义两个指针
i
和j
分别指向字符串s
和t
的末尾。使用循环,条件为
i >= 0 || j >= 0
,即两个字符串至少有一个还没有遍历完。在循环前,使用
sBackspaceCount
记录遇到的退格符的数量,对字符串s
执行退格操作,这里又使用了一个while
循环,循环继续的条件有两个:(1)字符串没遍历完且当前遍历到的字符是’#’,则需要回退的字符个数+1:
i >= 0 && (s[i] == '#'
;(2)退格符还没处理完:
sBackspaceCount > 0
。进入到
while
循环后,如果当前字符是#
,则增加sBackspaceCount
,否则减少,通过回退字符抵消掉一个#
。通过这个循环,i
移动到有效字符或字符串的开头。类似地,对字符串 t执行相同的退格操作,将
j
移动到有效字符或字符串的开头。比较
s[i]
和t[j]
,先判断不想等的情况:- 存在且不相等,返回
false
; - 一个字符串遍历完而另一个没有,也返回
false
。
- 存在且不相等,返回
比较之后,两个字符相等,则将
i
和j
减一,继续下一轮循环。循环结束,说明两个字符串经过退格操作后相等,返回
true
。
1 | class Solution { |
977. 有序数组的平方
输入:nums = [-4,-1,0,3,10]
输出:[0,1,9,16,100]
解释:平方后,数组变为 [16,1,0,9,100]
排序后,数组变为 [0,1,9,16,100]
一、直接快速排序
最简单的就是先遍历数组做平方,然后对平方后的数组做快速排序。时间复杂度:$$O(nlogn)$$, 没使用额外空间,因此空间复杂度是 $$O(1)$$。
二、使用双指针
代码如下,这里用到双指针是因为有序数组平方之后,其实还是可以确定最大值不是在最左边就是在最右边,那么就可以用 i 指针从左向右移动,用 j 从右向左移动,逐一比较两个值,更大的那个就是整个数组最大的。创建大小相同的数组 res 来存储两端比较之后的较大值,取哪个之后,就移动哪个指针。
1 | class Solution { |
类似的: LCR 008. 长度最小的子数组
二、左右指针
1. 典型题目
三、滑动窗口
1. 典型题目
我是先看过 labuladong 大神的文章:我写了首诗,把滑动窗口算法变成了默写题。其中的第 76 题「最小覆盖子串」难度 Hard,我似懂非懂地看完了,觉得还是理解不够透彻。于是参考着做了另一道难度中等的题目,就是904. 水果成篮。
我首先自己做了一遍,不看参考,自己尝试按照滑动窗口的理解写了如下代码:
1 | class Solution { |
但是结果始终不对。咨询 GPT 老师,得出结论是,我要两处地方有问题:
逻辑错误
在
basket[fruits[right]]++;
之后,计算res
的语句应该放在while (basket.size() > 2)
循环之外,以确保res
在每次循环的开始时都被更新;不恰当的左边界移动
在
basket.count(fruits[left]) == 1
分支中,只检查了水果种类是否在篮子中,而没有检查数量是否为1。应该修改为basket[fruits[left]] == 1
才删除对应水果。
总结:GPT 老师很厉害啊,以后刷题更方便了!后续再刷一些题目,巩固滑动窗口的算法!应当集中在中等难度的题目,困难的题目浪费人生!