欢迎光临
我们一直在努力

力扣实训 _ [4].寻找两个正序数组的中位数 _ [136].只出现一次的数字

寻找两个正序数组的中位数

1. 题目回顾

  • 问题: 给定两个大小分别为 mm 和 nn 的正序数组 nums1 和 nums2,找出这两个正序数组的中位数。
  • 约束: 算法的时间复杂度应该为 O(log⁡(m+n))O(log(m+n)) 。
  • 示例: [1,3] 和 [2] →→ 中位数为 2.0;[1,2] 和 [3,4] →→ 中位数为 2.5。

2. 核心思路:线性扫描与模拟归并

虽然这道题的最优解是二分查找,但这段代码采用了更朴素的 “模拟归并” 思路:

  • 基本思想: 既然两个数组已经有序,那么它们合并后的整体也是有序的。我们不需要真的创建一个新数组来存储所有元素,只需要用两个指针分别指向两个数组的头部,每次比较两个指针所指的值,将较小的值“取出”,直到取到中位数所在的位置为止。
  • 统一奇偶: 无论总长度是奇数还是偶数,中位数总是由中间的一个或两个数决定。通过计算 leftPos 和 rightPos,代码巧妙地用一个循环同时处理了这两种情况。

3. 算法详细步骤

3.1 递归终止条件(边界处理)

虽然本解法是迭代而非递归,但同样存在关键的边界判断逻辑,主要体现在 if 语句的判断顺序上:

  • 越界检查优先: i < m && (j >= n || nums1[i] <= nums2[j])
    • 首先检查 i < m,确保 nums1 还有元素。
    • 接着检查 j >= n,如果 nums2 已经遍历完了,那么只能取 nums1 的元素。
    • 最后才是数值比较 nums1[i] <= nums2[j]。这种短路求值机制防止了数组下标越界异常。

3.2 分解过程:双指针移动

  • 初始化: 定义两个指针 i 和 j 分别指向 nums1 和 nums2 的起始位置。
  • 确定目标位置:
    • leftPos = (totalLen – 1) / 2:这是中位数左边的索引(对于奇数长度,它等于右边索引;对于偶数长度,它是中间偏左的那个)。
    • rightPos = totalLen / 2:这是中位数右边的索引(对于奇数长度,它等于左边索引;对于偶数长度,它是中间偏右的那个)。
  • 循环推进: 循环从 k=0 开始,一直走到 k == rightPos。每一步都代表我们在合并后的序列中找到了第 k+1 小的数。

3.3 解决过程:值的捕获

  • 选取较小值: 在循环内部,比较 nums1[i] 和 nums2[j],将较小的那个赋值给 rightVal,并将对应的指针后移。此时 rightVal 保存的是当前遍历到的最大值(即第 k 小的数)。
  • 记录左侧中位数:
    • 当循环变量 k 刚好等于 leftPos 时,说明当前的 rightVal 就是我们要找的中位数的左半部分(或者唯一部分)。
    • 此时将 rightVal 备份到 leftVal 中。

3.4 合并过程:结果计算

  • 循环结束: 当循环结束时,rightVal 必定停留在 rightPos 对应的值上。
  • 公式计算: (leftVal + rightVal) / 2.0
    • 若总长为奇数: leftPos == rightPos。在最后一次循环中,leftVal 被赋值为当前的 rightVal,随后循环结束,rightVal 没变。两者相加除以 2 等于其本身。
    • 若总长为偶数: leftPos 比 rightPos 小 1。倒数第二次循环记录了 leftVal,最后一次循环更新了 rightVal。两者相加除以 2 即为平均值。

4. 代码实现

class Solution {
public double findMedianSortedArrays(int[] nums1, int[] nums2) {
int m = nums1.length, n = nums2.length;
int totalLen = m + n;

// 1. 确定我们需要遍历到的目标索引
// 例如长度为4,目标是索引1和2;长度为5,目标是索引2和2
int leftPos = (totalLen – 1) / 2;
int rightPos = totalLen / 2;

int i = 0, j = 0; // 双指针
int leftVal = 0, rightVal = 0; // 用于存储结果的两个关键值

// 2. 线性扫描,只需走到 rightPos 即可停止
for (int k = 0; k <= rightPos; k++) {
// 核心逻辑:谁小取谁,注意处理某个数组先遍历完的情况
if (i < m && (j >= n || nums1[i] <= nums2[j])) {
rightVal = nums1[i++];
} else {
rightVal = nums2[j++];
}

// 3. 如果当前步数到达了左中位数的位置,记录下来
if (k == leftPos) {
leftVal = rightVal;
}
}

// 4. 利用数学特性统一返回结果
// 奇数时 leftVal == rightVal,偶数时取平均
return (leftVal + rightVal) / 2.0;
}
}

5. 复杂度分析

这是该解法最大的短板,也是面试中需要注意的点。

  • 时间复杂度: O(m+n)
    • 在最坏情况下(例如一个数组的所有元素都小于另一个数组),我们需要遍历大约 (m+n)/2 次。
    • 这属于线性时间复杂度。虽然对于小规模数据很快,但对于大规模数据,它不满足题目要求的 O(log⁡(m+n))) (对数级)。
  • 空间复杂度: O(1)
    • 只使用了 i, j, k, leftVal, rightVal 等几个变量,没有开辟额外的数组空间,空间效率非常高。

只出现一次的数字

1. 题目回顾

  • 问题描述: 给定一个非空整数数组,除了某个元素只出现一次以外,其余每个元素均出现两次。找出那个只出现了一次的元素。
  • 约束条件: 你的算法应该具有线性时间复杂度 O(n)O(n) 。你能不使用额外空间来实现吗?
  • 示例:
    • 输入:[2, 2, 1] →→ 输出:1
    • 输入:[4, 1, 2, 1, 2] →→ 输出:4

2. 核心思路:位运算与异或性质

虽然题目可以归类为“查找”类问题,但最优雅的解法并非传统的排序或哈希表,而是利用位运算(Bit Manipulation)中的异或(XOR)操作。这其实是一种数学层面的“线性扫描”。

  • 异或运算的三大特性:

  • 归零律: 任何数与自身异或等于 0 ( a⊕a=0a⊕a=0 )。
  • 恒等律: 任何数与 0 异或等于其本身 ( a⊕0=aa⊕0=a )。
  • 交换律与结合律: 运算顺序不影响结果 ( a⊕b⊕a=(a⊕a)⊕b=0⊕b=ba⊕b⊕a=(a⊕a)⊕b=0⊕b=b )。
  • 解题策略: 将数组中所有数字进行累积异或。成对出现的数字会互相抵消变成 0,最终剩下的结果就是那个唯一的数字。

3. 算法详细步骤

3.1 递归终止条件(边界处理)

虽然最优解通常使用迭代(循环),但如果从逻辑完整性角度考虑边界:

  • 单元素数组: 如果数组长度仅为 1,无需计算,直接返回该元素。
  • 初始状态: 我们需要一个“累加器”变量,初始值必须设为 0。因为 0 是异或运算的单位元,不会改变第一个参与运算的数字的值。

3.2 分解过程:线性扫描

我们将问题分解为 NN 个简单的子步骤,即遍历数组中的每一个元素。

  • 指针移动: 设置一个循环变量 i 从 0 遍历到 nums.length – 1。
  • 数据流: 在每一步中,我们取出当前元素 nums[i],将其视为待处理的“新信息”。

3.3 解决过程:值的捕获(异或消除)

这是算法的核心执行阶段。我们不需要额外的存储空间来记录谁出现过,而是通过位运算实时“消除”重复项。

  • 操作: 执行 result = result ^ nums[i]。
  • 动态演示:
    • 假设数组为 [4, 1, 2, 1, 2]。
    • 遇到 4:结果变为 4。
    • 遇到 1:结果变为 4^1。
    • 遇到 2:结果变为 4^1^2。
    • 遇到 1:结果变为 4^(1^1)^2 →→ 4^0^2 →→ 4^2。(此时第一个 1 被成功消除)。
    • 遇到 2:结果变为 4^(2^2) →→ 4^0 →→ 4。(此时 2 也被消除)。

3.4 合并过程:结果计算

  • 最终态: 当循环结束时,所有的“对子”都已经变成了 0。
  • 输出: 累加器中保留的唯一非零数值即为答案。由于异或运算的特性,这个结果天然就是我们要找的“单身”数字,无需再进行额外的比较或筛选。

4. 代码实现

class Solution {
public int singleNumber(int[] nums) {
// 初始化结果为0,作为异或的起始基准
int result = 0;

// 线性扫描整个数组
for (int num : nums) {
// 核心操作:异或运算
// 相同的数字会抵消为0,不同的数字会保留
result ^= num;
}

// 返回最终剩下的数字
return result;
}
}

5. 复杂度分析

  • 时间复杂度: O(n)
    • 我们只需要遍历数组一次,其中 n 是数组的长度。每个元素的异或操作是常数时间 O(1) 的。
  • 空间复杂度: O(1)
    • 这是一个原地算法。我们只使用了一个额外的变量 result 来存储中间状态,没有使用哈希表或辅助数组,完全符合题目“不使用额外空间”的要求。
赞(0)
未经允许不得转载:171主机测评 » 力扣实训 _ [4].寻找两个正序数组的中位数 _ [136].只出现一次的数字
分享到: 更多 (0)

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址