代码随想录算法训练营第二天(数组part2)|977.有序数组的平方 ,209.长度最小的子数组 ,59.螺旋矩阵II

📝977.有序数组的平方 (Easy)

题目建议: 本题关键在于理解双指针思想

题目链接:. - 力扣(LeetCode)

文章讲解:代码随想录

视频讲解: 双指针法经典题目 | LeetCode:977.有序数组的平方_哔哩哔哩_bilibili


👩‍💻思路:

1.暴力解法:

先设置一个新的数组。需要两个for loop循环遍历整个数组,然后根据绝对值大小排列元素顺序以确保最终数组按绝对值升序排列。这样做的目的是解决这道题的难点,就是负数在平方后顺序会乱套,所以我们先把他狸猫换太子当成正数一样排序,反之之后平方啥都不会影响。最后,把按排列好的元素平方后放入新的数组就好啦!

2.双指针:

同样,也需要一个新的数组。这次不一样的是,我们直接对平方后的元素进行比较。双指针分布在一头一尾。这样分布很巧妙因为这取决于我们对于这个数组在全员平方后的大值猜测,就是:两头比较大,中间小,至于两头到底谁是老大,我们也不清楚,所以让双指针来当救兵吧。由于双指针是比较大的两头儿,所以我们不得不在新数组中先放大的再放小的,所以需要反向思维一下,就是把第一个摘出来的大的放在末尾,index依次往前推

❌易错点/难点总结:

1.我在设置新数组时忘记设置好等同于旧的数组的size了而是创建了空的数组,写成了new_array = [ ];我忘记很重要的一点,我的操作并不会append元素进去。因此,当我尝试访问 new_array 的特定索引时,会收到 IndexError 错误,因为空数组没有这些索引位置的元素。(更正版:new_array = [0] * len(nums))

2.对于我这种之前没接触过双指针的初学者来讲,难点无非在于到底放到两端还是放在同一头让他们一快一慢?不过看了卡老师的视频后发现了他的小技巧,就是做之前稍微花一点时间观察一下这个数组的结构以及大小分布。

3.双指针法涉及到了多种index的更新,一会儿加一会儿减,层层相套,不仔细看的话容易写错。

📊视频讲解要点总结:

我借鉴了老师的方法才写出来双指针法。感觉这次视频要点内容都在第一部分【思路】中提到了,所以就不写了~

📸解题代码:

1.暴力:

时间复杂度:O(n^2)
空间复杂度:O(n)
class Solution(object):
    def sortedSquares(self, nums):
        """
        :type nums: List[int]
        :rtype: List[int]
        """
        n = len(nums)
        new_array = [0] * n
        for i in range(n):
            for j in range(i + 1, n):
        # 比较当前元素和下一个元素的绝对值,以确保最终数组按绝对值升序排列
                if abs(nums[i]) > abs(nums[j]):
                    # 交换元素以确保前一个元素的绝对值小于或等于后一个元素
                    nums[i], nums[j] = nums[j], nums[i]
        for i in range(n):
            # 将已经按绝对值排序好的元素的平方放入新数组中
            new_array[i] = nums[i] ** 2
        return new_array

2.双指针:

时间复杂度:O(n)
空间复杂度:O(n)
class Solution(object):
    def sortedSquares(self, nums):
        """
        :type nums: List[int]
        :rtype: List[int]
        """
        left = 0
        right = len(nums)-1
        # 提前设置好new_array的大小等同于nums的size
        new_array = [0] * len(nums)
        #提前设置好new array的index
        i = len(nums) - 1
        #设置条件,当左侧小于等于右侧时执行
        while left <= right: #l=r时,指向同一个元素不可省略
            if nums[left]**2 > nums[right]**2:
                new_array[i] = nums[left]**2
                i -= 1
                left += 1
            else: #这里用else就说明包含了等于的情况,
            #当元素相等时先放左边 和先放右边没有区别。
                new_array[i] = nums[right]**2
                i -= 1
                right -= 1
        return new_array
                

 📝209.长度最小的子数组 (Medium)

题目建议: 本题关键在于理解滑动窗口,这个滑动窗口看文字讲解 还挺难理解的,建议大家先看视频讲解。 拓展题目可以先不做。

题目链接:. - 力扣(LeetCode)

文章讲解:代码随想录

视频讲解:拿下滑动窗口! | LeetCode 209 长度最小的子数组_哔哩哔哩_bilibili


👩‍💻思路:

1.暴力解法:

用两个for loop一前一后遍历整个数组,寻找出大于等于target的最小数组的长度。

2.滑动窗口:

首先初始化两个指针 `start` 和 `end`,并将当前和 `sum` 设为 0,结果 `result` 设为一个比数组长度大的值 (`len(nums) + 1`) 用于存储最小子数组长度。然后使用 `for` 循环遍历数组,扩展右边界 `end` 并将当前元素加入 `sum`,接着使用 `while sum >= target` 循环收缩左边界 `start`,直到 `sum` 小于 `target`,在收缩过程中更新最小子数组长度 `result` 并从 `sum` 中减去 `start` 指针所指的元素,最后返回 `result`,如果 `result` 仍为初始值则返回 0 表示没有符合条件的子数组,否则返回 `result`。

🔋知识点补充(新手友好版):

1.滑动窗口定义:(取自chatgpt)

滑动窗口(Sliding Window)是一种在数组或序列中处理连续子序列的技术。它通常用于解决需要对连续区间进行计算或比较的问题,比如求最大值、最小值、平均值等。这个窗口在数组上从左到右移动,每次移动一个元素的位置,从而形成一个新的子序列,然后在这个子序列上进行计算或操作。下面是一个简单的例子来说明滑动窗口的概念:假设有一个数组 [1, 3, -1, -3, 5, 3, 6, 7] 和一个窗口size为 3。我们可以从数组的开始位置开始,取连续的三个元素构成第一个窗口 [1, 3, -1],然后进行一些操作(比如求和、求平均值等)。接着,窗口向右移动一个位置,得到下一个窗口 [3, -1, -3],再进行相同的操作。如此继续,直到窗口无法再向右移动为止。

2.优势:

滑动窗口的好处在于它可以在一次遍历数组的情况下解决某些问题,而不需要进行多次遍历。这在处理大规模数据时可以节省时间和空间。

3.和双指针的区别在哪里?(取自chatgpt)

  • 滑动窗口(Sliding Window):

   - 滑动窗口是一种处理连续子数组或子字符串的技巧。它通常用于解决需要在一个序列中寻找满足特定条件的子序列的问题。
   - 窗口会随着算法的执行而移动,每次移动一个元素(或多个元素),从而形成新的窗口。通过调整窗口的位置和大小,可以在线性时间内解决很多需要遍历整个序列的问题。
   - 例如,在一个数组中找到最长的连续子数组,使得子数组中的元素之和不超过某个给定值,就可以使用滑动窗口来解决这类问题。

  • 双指针(Two Pointers):

   - 双指针技巧通常用于解决数组或链表中的问题,其中两个指针同时遍历数组或链表,从而减少问题的时间复杂度。
   - 双指针可以有多种应用方式,例如快慢指针用于解决环形链表问题、左右指针用于解决数组或字符串中的查找或判断问题等。
   - 双指针技巧的优势在于避免了一些不必要的遍历,提高了算法的效率。但它的适用范围相对滑动窗口来说更加局限。

  • 总体来说,滑动窗口更适用于连续子数组或子字符串的问题,而双指针则更适用于在数组或链表中进行查找或判断的问题。在实际应用中,根据问题的特点选择合适的算法技巧可以提高算法的效率和可读性。

❌易错点/难点总结:

1.初始化result的值:

这个我一开始写成了result = len(nums),我以为设置成size相同就足够了,因为我们的原则是让result达到max。但是,以下情况会报错:

设定 result = len(nums) + 1 的目的是为了确保在所有可能的情况下都能正确返回结果。我们初始化 result 为一个比任何可能的子数组长度都大的值,以便在代码逻辑中可以通过 min 函数来找到更小的子数组长度。如果我们没有找到满足条件的子数组,那么 result 将保持其初始值 len(nums) + 1,最终返回 0 表示没有符合条件的子数组。

2.while vs if???

这个问题卡老师也在视频中提到了,是 while sum>= target 还是 if sum >= target 呢?答案是while。使用 while 的原因是连续缩小窗口。当我们发现当前窗口的和 sum 大于或等于目标值 target 时,需要尽可能缩小窗口以找到最小的子数组长度。使用 while 循环允许我们连续移动 start 指针,缩小窗口的大小,直到 sum 小于 target。在某些情况下,单次调整窗口(即用 if)可能不足以找到最优解。请看例子:

3.计算合规的数组长度

在这一步,我写成了len_sum = end - start,错就错在了题目让我返回长度,而我返回了index,所以应该修改为:len_sum = end - start + 1。

4.start += 1应该放在哪里?

第一次,我把它放在了sum -= nums[start]的上面,后来发现,这样写会导致提前修改start的位置多删除一个无辜的elements。所以,切记!要在更新完sum的value再更新start。

📊视频讲解要点总结:

1.核心思想:

我们的目标是用一个for loop完成两个for loop才能做到的事!!

2.第一步:谁先走?

假设两个小人儿都在起点的位置 0,我们要先决定窗口的起点小人儿先走还是终点小人儿先走。答案是:终点。如果起点先走的话,终点必须要跟着走,因为只有这样才能形成一个有效的窗口,这不是又掉入了暴力法的陷阱里了?所以,先走终点才是硬道理。

3.第二步:那起点啥时候走呢?

当终点小人儿独自走到一定的位置,惊喜地发现数组已经被他拉长到大于等于target啦,这就意味着,起点可以出发,起点的出发意味着窗口会被缩小,如果能实现缩小又恰好保证范围符合规定(有点儿冒险心理的意思hhha),那么我们的愿望不就实现了。如果发现不成,起点的缩小导致sum小于target了,那也不要担心,我们可以继续移动终点。总而言之,这是一种非常灵活的方法,通过不断调整起始位置从而形成最合适的数组组合。你们发现了吗?我们没有一个一个遍历是因为题目不需要我们这样做,我们算的是sum,所以在这种情况下滑动窗口的作用就可以凸显出来了。

4.具体操作:

具体操作要考虑到更细致的东西,比方说该用while还是if,怎么计算符合规定的数组长度,这些问题我会在【易错点/难点总结】内提到。

📸解题代码:

滑动窗口:

时间复杂度:O(n)
空间复杂度:O(1)
    def minSubArrayLen(self, target, nums):
        """
        :type target: int
        :type nums: List[int]
        :rtype: int
        """
        start = 0
        sum = 0
        result = len(nums)+1  # 初始化为len(nums) + 1,确保足够大
        #开始用end遍历array
        for end in range(len(nums)):
            sum += nums[end] #累加浏览过的元素
            while sum >= target:
                len_sum = end - start + 1
                result = min(result, len_sum)
                sum -= nums[start]
                start += 1

        return result if result != len(nums) + 1 else 0

📝59.螺旋矩阵II (Medium)

题目建议: 本题关键还是在转圈的逻辑,在二分搜索中提到的区间定义,在这里又用上了。

题目链接:. - 力扣(LeetCode)

文章讲解:代码随想录

视频讲解:一入循环深似海 | LeetCode:59.螺旋矩阵II_哔哩哔哩_bilibili


👩‍💻思路:

这个方法首先初始化一个大小为 `n x n` 的矩阵,并设置初始边界值 `top`, `bottom`, `left`, `right` 和起始填充值 `num`。在 `while` 循环中按顺序填充矩阵的边界,依次从左到右填充上边界、从上到下填充右边界、从右到左填充下边界以及从下到上填充左边界,每次填充后调整相应边界,缩小填充区域,直到所有元素填充完毕。终止条件是 `top` 小于等于 `bottom` 且 `left` 小于等于 `right`。最终返回填充完成的螺旋矩阵。如果 `n` 小于等于 0,则返回空矩阵。注意边界更新的顺序和正确的终止条件,以确保矩阵不会越界或重叠填充。

❌易错点/难点总结:

1.初始化矩阵大小:
确保矩阵的初始化大小为 n x n,并且初始值为 0。使用列表推导式 [ [0]*n for _ in range(n) ] 来初始化矩阵。

2.边界更新顺序:
每次填充完成后,需要更新相应的边界:填充上边界后更新 top,填充右边界后更新 right,填充下边界后更新 bottom,填充左边界后更新 left。
注意:更新边界的顺序必须严格按照螺旋顺序进行,以避免覆盖已经填充的元素。

3.终止条件:
while 循环的终止条件是 top <= bottom 且 left <= right。确保在每次边界更新后检查该条件,以避免越界错误。

4.填充顺序:
在填充每一层时,注意保持顺序:先从左到右,再从上到下,然后从右到左,最后从下到上。这一顺序确保矩阵的螺旋填充不会出现跳跃或重叠。

📊视频讲解要点总结:

1.正方形边界四个点的处理很重要:不可以反复处理节点。处理每个边时, 对于节点的处理规则是不一样的。

2.遵循循环不变量的原则:

在循环时,坚持一个规则来处理每一条边。要么全都左闭右闭,要么全都左闭右开。例如,左闭右开,我们统一只处理第一个点,不处理最后一个,最后一个点留给下一条边处理。

3.循环转几圈?

不管n等于几,能达到一次正方形循环♻️的次数是n//2。这是因为每转一圈,先是最外层四个边被处理,横着看上下少了两行,竖着看,左右少了两列;因此,n x n 的矩阵变成了(n-2) x (n-2)。要想知道能循环多少次就得看看n有多大能耐,每一层都是2个点,看看里面有几层所以除以二。换句话说,像扒洋葱一样一层一层的数,扒一层就减少了2厘米的厚度,看到最后能扒出来几层洋葱(正方形)。

📸解题代码:

class Solution(object):
    def generateMatrix(self, n):
        if n <= 0:
            return []
        
        # 初始化 n x n 矩阵
        matrix = [[0]*n for _ in range(n)]

        # 初始化边界和起始值
        top, bottom, left, right = 0, n-1, 0, n-1
        num = 1

        while top <= bottom and left <= right:
            # 从左到右填充上边界
            for i in range(left, right + 1):
                matrix[top][i] = num
                num += 1
            top += 1

            # 从上到下填充右边界
            for i in range(top, bottom + 1):
                matrix[i][right] = num
                num += 1
            right -= 1

            # 从右到左填充下边界
            for i in range(right, left - 1, -1):
                matrix[bottom][i] = num
                num += 1
            bottom -= 1

            # 从下到上填充左边界
            for i in range(bottom, top - 1, -1):
                matrix[i][left] = num
                num += 1
            left += 1

        return matrix
  • 18
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
第二十二天的算法训练主要涵盖了Leetcode题目中的三道题目,分别是Leetcode 28 "Find the Index of the First Occurrence in a String",Leetcode 977 "有序数组的平方",和Leetcode 209 "长度最小的数组"。 首先是Leetcode 28题,题目要求在给定的字符串中找到第一个出现的字符的索引。思路是使用双指针来遍历字符串,一个指向字符串的开头,另一个指向字符串的结尾。通过比较两个指针所指向的字符是否相等来判断是否找到了第一个出现的字符。具体实现的代码如下: ```python def findIndex(self, s: str) -> int: left = 0 right = len(s) - 1 while left <= right: if s[left == s[right]: return left left += 1 right -= 1 return -1 ``` 接下来是Leetcode 977题,题目要求对给定的有序数组中的元素进行平方,并按照非递减的顺序返回结果。这里由于数组已经是有序的,所以可以使用双指针的方法来解决问题。一个指针指向数组的开头,另一个指针指向数组的末尾。通过比较两个指针所指向的元素的绝对值的大小来确定哪个元素的平方应该放在结果数组的末尾。具体实现的代码如下: ```python def sortedSquares(self, nums: List[int]) -> List[int]: left = 0 right = len(nums) - 1 ans = [] while left <= right: if abs(nums[left]) >= abs(nums[right]): ans.append(nums[left ** 2) left += 1 else: ans.append(nums[right ** 2) right -= 1 return ans[::-1] ``` 最后是Leetcode 209题,题目要求在给定的数组中找到长度最小的数组,使得数组的和大于等于给定的目标值。这里可以使用滑动窗口的方法来解决问题。使用两个指针来表示滑动窗口的左边界和右边界,通过移动指针来调整滑动窗口的大小,使得滑动窗口中的元素的和满足题目要求。具体实现的代码如下: ```python def minSubArrayLen(self, target: int, nums: List[int]) -> int: left = 0 right = 0 ans = float('inf') total = 0 while right < len(nums): total += nums[right] while total >= target: ans = min(ans, right - left + 1) total -= nums[left] left += 1 right += 1 return ans if ans != float('inf') else 0 ``` 以上就是第二十二天的算法训练的内容。通过这些题目的练习,可以提升对双指针和滑动窗口等算法的理解和应用能力。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值