绝对差不超过限制的最长连续子数组

标签: 队列 数组 有序集合 滑动窗口 单调队列 堆(优先队列)

难度: Medium

给你一个整数数组 nums ,和一个表示限制的整数 limit,请你返回最长连续子数组的长度,该子数组中的任意两个元素之间的绝对差必须小于或者等于 limit

如果不存在满足条件的子数组,则返回 0

示例 1:

输入:nums = [8,2,4,7], limit = 4
输出:2 
解释:所有子数组如下:
[8] 最大绝对差 |8-8| = 0 <= 4.
[8,2] 最大绝对差 |8-2| = 6 > 4. 
[8,2,4] 最大绝对差 |8-2| = 6 > 4.
[8,2,4,7] 最大绝对差 |8-2| = 6 > 4.
[2] 最大绝对差 |2-2| = 0 <= 4.
[2,4] 最大绝对差 |2-4| = 2 <= 4.
[2,4,7] 最大绝对差 |2-7| = 5 > 4.
[4] 最大绝对差 |4-4| = 0 <= 4.
[4,7] 最大绝对差 |4-7| = 3 <= 4.
[7] 最大绝对差 |7-7| = 0 <= 4. 
因此,满足题意的最长子数组的长度为 2 。

示例 2:

输入:nums = [10,1,2,4,7,2], limit = 5
输出:4 
解释:满足题意的最长子数组是 [2,4,7,2],其最大绝对差 |2-7| = 5 <= 5 。

示例 3:

输入:nums = [4,2,2,2,4,4,2,2], limit = 0
输出:3

提示:

  • 1 <= nums.length <= 10^5
  • 1 <= nums[i] <= 10^9
  • 0 <= limit <= 10^9

Submission

运行时间: 119 ms

内存: 24.0 MB

class Solution:
    def longestSubarray(self, nums: List[int], limit: int) -> int:
        n = len(nums)
        ans = left = 0
        dq_max = deque()
        dq_min = deque()
        for right in range(n):
            num = nums[right]
            while dq_max and num > dq_max[-1]:
                dq_max.pop()
            dq_max.append(num)

            while dq_min and num < dq_min[-1]:
                dq_min.pop()
            dq_min.append(num)

            if dq_max[0] - dq_min[0] <= limit:
                ans = right - left + 1
            else:
                if dq_max[0] == nums[left]:
                    dq_max.popleft()
                if dq_min[0] == nums[left]:
                    dq_min.popleft()
                left += 1
            
        return ans
            

Explain

本题解采用了滑动窗口加双端队列的方法来解决问题。我们用两个双端队列(deque),一个维护窗口内的最大值(dq_max),另一个维护窗口内的最小值(dq_min)。遍历数组,对于每个元素,我们将其加入到两个队列中,保持dq_max队列递减,dq_min队列递增。这样dq_max的队首元素即为窗口内的最大值,dq_min的队首元素即为窗口内的最小值。每次将新元素加入后,比较dq_max和dq_min队首的差是否超过limit,如果不超过,就尝试扩大窗口;如果超过了,就移动窗口的左边界,直到窗口内的最大值与最小值之差小于等于limit。每次迭代更新最长子数组的长度。

时间复杂度: O(n)

空间复杂度: O(n)

class Solution:
    def longestSubarray(self, nums: List[int], limit: int) -> int:
        n = len(nums)
        ans = left = 0
        dq_max = deque()  # 用于存储窗口内的最大值
        dq_min = deque()  # 用于存储窗口内的最小值
        for right in range(n):
            num = nums[right]
            while dq_max and num > dq_max[-1]:  # 维持dq_max递减
                dq_max.pop()
            dq_max.append(num)

            while dq_min and num < dq_min[-1]:  # 维持dq_min递增
                dq_min.pop()
            dq_min.append(num)

            if dq_max[0] - dq_min[0] <= limit:  # 窗口内最大值与最小值之差不超过limit
                ans = max(ans, right - left + 1)  # 更新最长子数组的长度
            else:
                if dq_max[0] == nums[left]:  # 如果窗口左端是dq_max的最大值
                    dq_max.popleft()
                if dq_min[0] == nums[left]:  # 如果窗口左端是dq_min的最小值
                    dq_min.popleft()
                left += 1  # 移动窗口左边界
        
        return ans

Explore

在本题解中,为了保证dq_max始终递减和dq_min始终递增,我们采用了特定的插入和删除策略。当新元素num加入到dq_max时,我们从dq_max的尾部开始比较,如果发现尾部元素小于num,则将尾部元素弹出,直到dq_max为空或尾部元素大于等于num为止。这样可以确保dq_max中的元素从队首到队尾递减。同理,对于dq_min,当新元素num加入时,我们从dq_min的尾部开始比较,如果尾部元素大于num,则将尾部元素弹出,直到dq_min为空或尾部元素小于等于num为止,保持dq_min中的元素从队首到队尾递增。这种方法确保了队列的队首元素分别是当前窗口的最大值和最小值,且可以高效地更新和查询。

当窗口内最大值与最小值之差超过limit时,我们需要缩小这个差值以符合题目要求。由于这个差值是由当前窗口的最大值和最小值决定的,移动窗口的左边界是一种直接有效的方法来尝试减小这个差值,因为它可能会移除当前的最大值或最小值,从而影响差值。如果移动右边界,则仅仅是停止增长窗口而不会立即影响当前的最大值和最小值的差距。目前的方法已经是相对高效的,因为它保证了每个元素只被添加和删除一次。优化策略主要关注维护deque的效率,确保操作为O(1),但基本算法框架(即滑动窗口加双端队列)是解决这种类型问题的最佳实践。

选择双端队列(deque)而不是堆或有序数组的主要原因在于操作效率。双端队列支持在两端高效地添加和删除元素,这对于滑动窗口算法至关重要。使用双端队列,我们可以保证在O(1)时间内插入元素、删除元素以及更新最大值和最小值。相比之下,使用堆虽然可以在O(log n)时间内维护最值,但删除非堆顶元素较为复杂且效率低下。有序数组虽然可以保持元素有序,但在数组中间插入或删除元素的时间复杂度通常是O(n),这在滑动窗口中是不可接受的。因此,双端队列是维护滑动窗口内最大值和最小值的最优选择,它结合了高效的数据更新与最值查询的优势。