限制条件下元素之间的最小绝对差

标签: 数组 二分查找 有序集合

难度: Medium

给你一个下标从 0 开始的整数数组 nums 和一个整数 x 。

请你找到数组中下标距离至少为 x 的两个元素的 差值绝对值 的 最小值 。

换言之,请你找到两个下标 i 和 j ,满足 abs(i - j) >= x 且 abs(nums[i] - nums[j]) 的值最小。

请你返回一个整数,表示下标距离至少为 x 的两个元素之间的差值绝对值的 最小值 。

示例 1:

输入:nums = [4,3,2,4], x = 2
输出:0
解释:我们选择 nums[0] = 4 和 nums[3] = 4 。
它们下标距离满足至少为 2 ,差值绝对值为最小值 0 。
0 是最优解。

示例 2:

输入:nums = [5,3,2,10,15], x = 1
输出:1
解释:我们选择 nums[1] = 3 和 nums[2] = 2 。
它们下标距离满足至少为 1 ,差值绝对值为最小值 1 。
1 是最优解。

示例 3:

输入:nums = [1,2,3,4], x = 3
输出:3
解释:我们选择 nums[0] = 1 和 nums[3] = 4 。
它们下标距离满足至少为 3 ,差值绝对值为最小值 3 。
3 是最优解。

提示:

  • 1 <= nums.length <= 105
  • 1 <= nums[i] <= 109
  • 0 <= x < nums.length

Submission

运行时间: 506 ms

内存: 31.4 MB

class Solution:
    def minAbsoluteDifference(self, nums: List[int], x: int) -> int:
        if x == 0: return 0
        pre = []
        cur_len = 0
        n = len(nums)
        ans = abs(nums[0] - nums[-1])
        for i in range(x,n):
            num = nums[i]
            pre.insert(bisect_left(pre, nums[i-x]), nums[i-x])
            cur_len += 1
            idx = bisect_left(pre, num)
            if idx == 0:
                ans = min(ans, pre[0]-num)
            elif idx == cur_len:
                ans = min(ans, num - pre[-1])
            else:
                ans = min(ans, min(pre[idx]-num, num - pre[idx-1]))
        return ans






Explain

这道题目的基本思路是利用滑动窗口和二分查找。首先,定义一个列表 `pre` 用以存储滑动窗口中的元素(窗口大小为 `x`),并且保持这个列表有序。遍历数组 `nums` 从下标 `x` 开始,每次将下标 `i-x` 的元素插入到 `pre` 中的适当位置以保持列表的有序性。对于每个 `i`,我们使用二分查找确定 `nums[i]` 在 `pre` 中的位置,然后计算 `nums[i]` 和它前后元素的差的绝对值,以此来找出最小的差值。这个方法通过动态维护一个有序数组来快速计算任意元素与其距离至少为 `x` 的元素的最小绝对差。

时间复杂度: O((n-x) log x)

空间复杂度: O(x)

# 定义 Solution 类

class Solution:
    def minAbsoluteDifference(self, nums: List[int], x: int) -> int:
        if x == 0: return 0  # 如果 x 为0,直接返回0(根据题设,这种情况不会发生)
        pre = []  # 初始化窗口数组
        cur_len = 0  # 当前窗口数组的长度
        n = len(nums)  # 数组的总长度
        ans = abs(nums[0] - nums[-1])  # 初始化答案为第一个元素和最后一个元素的差的绝对值
        for i in range(x, n):  # 从下标 x 开始遍历
            num = nums[i]  # 当前考察的元素
            # 将距离当前元素 x 位置的元素插入窗口数组,并保持有序
            pre.insert(bisect_left(pre, nums[i-x]), nums[i-x])
            cur_len += 1  # 窗口长度加1
            idx = bisect_left(pre, num)  # 二分查找当前元素在窗口数组中的位置
            # 根据位置计算可能的最小差值
            if idx == 0:
                ans = min(ans, pre[0]-num)
            elif idx == cur_len:
                ans = min(ans, num - pre[-1])
            else:
                ans = min(ans, min(pre[idx]-num, num - pre[idx-1]))
        return ans  # 返回计算的最小差值

Explore

滑动窗口和二分查找的组合在这个问题中被使用是因为它们可以有效地维护和查询有序的数据集合。滑动窗口允许我们在遍历数组时动态地更新窗口内元素,保持窗口大小固定,而二分查找则提供了快速查找和插入元素的能力,使得每次操作的时间复杂度为O(log x)。尽管这种方法有效,但使用平衡二叉搜索树(如AVL树或红黑树)可能会提供相同甚至更好的效率,因为它们在插入、删除和查找操作中也能保持O(log x)的时间复杂度,并且更直接地支持动态数据的快速访问和更新。

在使用`pre.insert(bisect_left(pre, nums[i-x]), nums[i-x])`操作时,虽然二分查找能以O(log x)的时间复杂度找到插入位置,但实际的插入操作可能需要O(x)的时间复杂度,因为它涉及到数组元素的移动。这确实可能成为性能瓶颈特别是当窗口x较大时。为了避免这种情况,可以考虑使用数据结构如平衡二叉搜索树,它在维护有序元素集合时,插入和删除操作都可以保持O(log x)的时间复杂度,这样可以更有效地处理大量元素的动态插入和删除。

初始化答案为`abs(nums[0] - nums[-1])`是一种假设性的初始条件,可能并不总是合理,因为它只考虑了数组的第一个元素和最后一个元素的差值,而实际上最小绝对差可能出现在数组的任何两个距离至少为x的元素之间。更合理的初始化方法是设置一个非常大的数(如正无穷),这样可以确保在初始状态不会错误地限制了答案的下界,使算法能够正确地更新最小差值。

遍历从x开始而不是从0开始,是因为题目要求比较的是任意两个距离至少为x的元素之间的差值。如果从0开始遍历,那么在下标小于x的情况下,无法找到距离至少为x的另一个元素来进行比较。因此,从x开始遍历可以直接跳过那些无法找到有效比较目标的初始元素,这样做既可以节省计算时间也符合题目要求。