题解: 数字配对问题 | 豆包MarsCode AI刷题

147 阅读4分钟

数字配对问题看似简单,但隐藏了许多算法思想和技巧。在这篇文章中,我们将通过对题目的拆解、思路分析、代码实现以及优化方法的探讨,来全面剖析这个问题。

题目解析

问题描述

小F手中有一串数字,需要将这些数字两两配对,配对规则是:

  1. 两个数字的差的绝对值必须大于等于给定值 (M)。
  2. 每个数字只能被配对一次,不能重复使用。

输入与输出

  • 输入:
    • 一个整数 (N),表示数字的个数。
    • 一个整数 (M),表示差异值。
    • 一个数组 (X),包含 (N) 个数字。
  • 输出:最多能成功配对的数字对数。

示例分析

以示例 [1, 3, 3, 7] 和 (M = 2) 为例:

  1. 数组排序后为 [1, 3, 3, 7]。
  2. 有效的配对可以是 (1, 3) 和 (3, 7),共两对。

解题思路

为了高效解决这个问题,我们需要遵循以下步骤:

  1. 排序数组:排序后可以利用数据的有序性快速判断配对。
  2. 双指针法:通过双指针实现匹配,避免暴力枚举导致的效率低下。

以下是关键点分析:

为什么要排序?

排序的目的是让数据的相对大小关系明确化,便于快速找到符合条件的数字对。如果不排序,需要对任意两个数计算差值,复杂度为 (O(N^2)),显然不够高效。

双指针法的优势

双指针是一种常见的技巧,用于在有序数组中寻找满足特定条件的元素:

  • 左指针指向当前待配对的较小数字。
  • 右指针寻找与左指针数字差值满足条件的较大数字。

双指针法的复杂度是 (O(N)),远优于暴力枚举。

代码实现与分析

我们将上面的思路转化为代码:

#include <iostream>
#include <vector>
#include <algorithm>

int solution(int N, int M, std::vector<int> X) {
    // 1. 对数组进行排序
    std::sort(X.begin(), X.end());
    int count = 0;  // 成功配对的计数器
    int i = 0;      // 左指针
    int j = 0;      // 右指针

    // 2. 使用双指针法遍历数组
    while (i < N && j < N) {
        if (i == j || X[j] - X[i] < M) {
            // 如果两个指针重合或差值不满足条件,右指针右移
            j++;
        } else {
            // 找到符合条件的一对,计数器+1,并移动两个指针
            count++;
            i++;
            j++;
        }
    }

    return count;
}

思路详解

初始化与排序

代码开始时对数组进行排序:

std::sort(X.begin(), X.end());

排序的时间复杂度为 (O(N \log N)),这是整个算法中最耗时的一步,但它是必要的,排序可以为后续操作奠定基础。

双指针遍历

双指针的核心逻辑是:

  1. 如果两个指针指向的数满足 (X[j] - X[i] \geq M),则计数器 (count) 增加,并同时移动两个指针。
  2. 如果不满足条件,仅移动右指针 (j),试图寻找更大的数。

这个逻辑能确保每个数字只参与一次配对,符合题目要求。

边界条件

  1. 当左指针 (i) 和右指针 (j) 相同时,跳过配对,因为数字不能和自己配对。
  2. 当右指针越界时((j \geq N)),停止循环。

思考与优化

更好的指针初始化

在当前代码中,右指针从 (j = 0) 开始,实际上可以直接从 (j = 1) 或 (j = N/2) 开始:

  • (j = 1) 的原因是,只有在 (j > i) 的情况下才可能配对。
  • (j = N/2) 是基于理论优化,因为大部分符合条件的配对会在数组的两部分之间发生。

这种优化不会改变时间复杂度,但可能减少实际运行时间。

特殊情况分析

  1. 如果 (M = 0),所有数字都可以配对,但此时应该仔细检查题目是否允许这样的情况。
  2. 如果 (N \leq 1),直接返回 0,配对不可能发生。
  3. 如果数组中所有数字相同,则无法配对,因为所有差值都小于 (M)。

为什么不用贪心?

在某些问题中,贪心法(优先选择差值刚好满足 (M) 的数对)是更直观的解法。但这里的双指针实际上隐含了贪心思想,每次优先处理最小的未配对数字,因此双指针是贪心的一种实现形式。

时间与空间复杂度

  • 时间复杂度:

    1. 排序 (O(N \log N))。
    2. 双指针 (O(N))。 总体复杂度为 (O(N \log N))。
  • 空间复杂度:仅需要常数额外空间 (O(1))。

总结

这个问题虽然不复杂,但能帮助我们熟悉排序、双指针以及贪心思想的实际应用。通过对示例的分析,我们可以总结出以下经验:

  1. 有序性是高效算法的基础:在处理配对、查找等问题时,排序是必备的第一步。
  2. 双指针适合范围问题:尤其是在有序数组中查找满足特定条件的元素。
  3. 边界条件与特殊情况不能忽视:不同的输入会影响最终结果,因此必须进行全面测试。