题目描述

✅ 补充题 17. 两个有序数组第 k 小的数

给定两个分别按非递减顺序排列的整数数组 nums1、nums2,以及合法排名 k,请返回两个数组合并后的第 k 小元素。

排名从 1 开始。重复元素按出现次数分别计入排名,不是寻找第 k 个不同的值。

示例 1:

输入:nums1 = [1,3,5], nums2 = [2,4,6], k = 4
输出:4

示例 2:

输入:nums1 = [], nums2 = [2,2,5], k = 2
输出:2

提示:

  • 两个数组都已按非递减顺序排列,可以包含重复值。
  • 1 <= k <= nums1.length + nums2.length。
  • 允许其中一个数组为空,不需要构造完整的合并数组。

题意分析

两个数组各自按升序排列,要求返回它们合并后的第 k 小数值,排名从 1 开始。重复元素按出现次数分别占据排名,不是找第 k 个不同的数,也不是返回某个原数组下标。

这里按合法排名处理,即 1 <= k <= 两数组长度之和。允许一侧为空,或两侧长度相差很大;无需真正创建完整的合并数组,只要定位目标排名。

解法:每轮淘汰约一半候选

核心思路

[!blue]

用 index1、index2 表示两侧尚未排除的后缀起点,k 表示目标在这两个剩余后缀中的排名。每次删除一批确定可以排在目标之前的元素,同时从 k 中扣除数量,就能保持寻找的数值不变。

当 k > 1 时,每侧先取至多 k / 2 个元素,数量分别为 p、q,比较两段的最后一个值 a、b。若 a <= b,准备删除第一侧的这 p 个元素。

为什么可以删?第一侧前 p 个值都不大于 a,它后面的值不小于 a;第二侧从第 q 个值开始都不小于 b,也就不小于 a。因此把相等值中准备删除的这一段排在前面时,a 的位置至多是 p + q - 1,而它不超过 2 × floor(k / 2) - 1 < k。整段都可以排在第 k 个出现之前,删除它后改找第 k - p 小,答案数值不变。a > b 时对称删除第二侧。

相等时也只删除一侧。被删元素的数值可能恰好等于答案,但另一侧仍保留对应的相同值,排名扣减后能够继续找到它;若同时删除两侧,就可能连目标所处的出现次数一起越过。

某侧耗尽后,直接在另一侧按剩余排名取值;k = 1 时,答案就是两侧首项的较小者。若没有直接结束,通常会删掉约一半排名;当选中前缀不足半数时,该侧会被直接耗尽,下一轮即可返回。

解题步骤

  1. 将两个后缀起点都初始化为 0。
  2. 优先检查某侧是否耗尽;若耗尽,返回另一侧起点之后第 k - 1 个偏移位置的值。
  3. 两侧都非空且 k = 1 时,返回两个首项的较小值。
  4. 分别取 min(k / 2, 当前侧剩余长度) 个候选元素,比较各自末项。
  5. 删除末项较小的一侧前缀,相等时选第一侧;对应起点前移多少,排名就减去多少。
  6. 对缩小后的两个后缀重复,直到命中直接返回条件。

代码实现

class Solution {
    public int findKth(int[] nums1, int[] nums2, int k) {
        int index1 = 0;
        int index2 = 0;

        while (true) {
            if (index1 == nums1.length) {
                return nums2[index2 + k - 1];
            }

            if (index2 == nums2.length) {
                return nums1[index1 + k - 1];
            }

            if (k == 1) {
                return Math.min(nums1[index1], nums2[index2]);
            }

            int half = k / 2;
            // 候选前缀长度不能超过当前后缀实际剩余长度
            int step1 = Math.min(half, nums1.length - index1);
            int step2 = Math.min(half, nums2.length - index2);
            int candidate1 = nums1[index1 + step1 - 1];
            int candidate2 = nums2[index2 + step2 - 1];

            if (candidate1 <= candidate2) {
                index1 += step1;
                // 淘汰数量与目标排名同步扣减,保留同一个答案
                k -= step1;
            } else {
                index2 += step2;
                k -= step2;
            }
        }
    }
}
func findKth(nums1 []int, nums2 []int, k int) int {
    index1, index2 := 0, 0

    for {
        if index1 == len(nums1) {
            return nums2[index2+k-1]
        }
        if index2 == len(nums2) {
            return nums1[index1+k-1]
        }
        if k == 1 {
            if nums1[index1] < nums2[index2] {
                return nums1[index1]
            }
            return nums2[index2]
        }

        half := k / 2
        // 候选前缀长度不能超过当前后缀实际剩余长度
        step1 := half
        if remain := len(nums1) - index1; step1 > remain {
            step1 = remain
        }
        step2 := half
        if remain := len(nums2) - index2; step2 > remain {
            step2 = remain
        }

        candidate1 := nums1[index1+step1-1]
        candidate2 := nums2[index2+step2-1]
        if candidate1 <= candidate2 {
            index1 += step1
            // 淘汰数量与目标排名同步扣减,保留同一个答案
            k -= step1
        } else {
            index2 += step2
            k -= step2
        }
    }
}

复杂度分析

  • 时间复杂度:$O(\log(k+1))$,每轮约减半或耗尽一侧。
  • 空间复杂度:$O(1)$,后缀起点与排名。

关键点总结

[!green]

  • k 始终是剩余集合中的排名,移动起点与扣减排名必须同步。
  • 一次删除整段依赖前缀末项比较和排名上界,不是只比较两个数后随意跳步。
  • 相同数值可以占多个排名,删除部分相同值不会改变相应剩余排名的答案数值。

易错点总结

[!yellow]

  • 直接访问第 k / 2 个剩余元素,而不限制实际长度,会在较短数组中越界。
  • 忽略 k = 1,会算出零步长,既无法正确选候选,也无法推进循环。
  • 更新数组起点但不扣减排名,寻找的就变成原目标之后的元素。
  • 候选相等时同时淘汰两侧,可能删除过多元素,越过目标排名。
  • 一侧耗尽后直接把 k 当数组下标,会遗漏当前起点以及排名从 1 开始的偏移。
  • 为了去重而合并相等值,会改变多重集合中的排名定义。

相似题目

题目 难度 关联与区别
4. 寻找两个正序数组的中位数 困难 中位数是合并后一个或两个特定名次,本题将查询推广到任意合法k,重复值仍分别计数。
补充题 193. 两个等长有序数组的上中位数 困难 该题限定两个数组等长,取合并后的第 n 小元素;本题允许长度不同,并将目标名次推广为任意合法 k。
转载与许可
作者
链接 https://hgnulb.github.io/blog/2025/73382911
许可 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处!