下标对中的最大距离 图解题解
这道题到底在问什么
- 输入
- nums1=[55,30,5,4,2], nums2=[100,20,10,10,5]
- 输出
- 2
- 输入
- nums1=[2,2,2], nums2=[10,10,1]
- 输出
- 1
- 输入
- nums1=[7,5,5,2,1], nums2=[9,8,8,7,3,2]
- 输出
- 3
最优解:为什么这么做
一句话答案:LeetCode 1855 下标对中的最大距离:合法对要 i ≤ j 且 nums1[i] ≤ nums2[j]。两数组非递增,对每个 i 二分找最靠后的合法 j,取 j - i 最大。时间 O(m log n)、空间 O(n)。
两个非递增数组里,要挑距离最大的哪种下标对
给两个非递增数组 nums1、nums2(下标从 0 起,每个数不比前一个大)。挑下标 (i, j),同时满足 i ≤ j 和 nums1[i] ≤ nums2[j] 才算有效,距离 j - i。求最大距离,没有有效对返回 0。题面 nums1=[7,5,5,2,1]、nums2=[9,8,8,7,3,2],答案 3。
把每个 i 都配一遍 j,为什么撑不到十万级
蛮力是双层枚举:nums1 每个 i 配 nums2 里每个 j ≥ i,逐对验 nums1[i] ≤ nums2[j]。两数组各长到 10⁵,对数是 m 乘 n 量级、逼近 10¹⁰,跑不完。若能对每个 i 一步跳到够得着的最靠后 j,就省掉这遍扫描。
两数组都非增,对每个 i 二分找最远合法 j
固定 i、记 v = nums1[i],找满足 nums2[j] ≥ v 的最靠后 j。nums2 非递增,某位置一旦掉到 v 以下,右边只会更小、全部出局——满足条件的下标恰是从 0 起的连续前缀。一边全成立、一边全不成立的单调结构正好能二分:几步就卡到前缀最后一格,即这个 i 要的 j。
倒序数组上二分落哪,原下标怎么还原
参考代码先把 nums2 倒过来(nums2[::-1]),非递增变非递减,好套二分。对每个 v,用 bisect_left(第一个 ≥ 目标的插入位置)在倒序数组找到 pos——从 pos 起往右都 ≥ v。
再把 pos 换回原下标:倒序第 k 位对应原第 n-1-k 位,倒序里 ≥ v 的那段对应原数组 [0, n-1-pos],最靠后即 j = len(nums2) - pos - 1。整段都没有 ≥ v 时 pos = n、j = -1,距离为负被丢掉。ans 取所有 j - i 最大。
题面这组数,每个 i 的 j 各是几
nums2=[9,8,8,7,3,2] 倒过来是 [2,3,7,8,8,9],n=6,ans 从 0 起。
nums1[0]=7:bisect_left 在 [2,3,7,8,8,9] 找 7 落下标 2,j 为 6-2-1=3,距离 3-0=3,ans 记 3。nums1[1]=5 与 nums1[2]=5:都插在 3 和 7 间 pos=2、j 为 3,距离分别 2、1。nums1[3]=2:落最前 pos=0,j 为 5,距离 5-3=2。nums1[4]=1:pos=0、j 为 5,距离 1。五轮最大的 3 即答案。
j 平白滑到隔壁一格,多半是下标还原漏了那个 −1
最易错是还原那步:j = len(nums2) - pos - 1,漏掉 -1 会让 j 右移一格、指错位置。二是方向:非递增当成升序二分,前缀后缀全判反。还有兜底:ans 从 0 起——某个 v 谁都配不上(j = -1)距离为负,或 nums1 全大过 nums2 时,从 0 起才返回 0。
复杂度:m 个元素各做一次 O(log n) 二分,合计 O(m log n);Python 切片把 nums2 倒过来新建长度 n 的列表,额外空间 O(n)(原地二分可降到 O(1))。
▶ 动画逐步走查(共 24 步)——想跟着动画一帧帧对照就展开
- 3记牢:两指针 i、j 都只向右。nums1[i] ≤ nums2[j] 就记距离 j 减 i 再让 j 右移(拉大距离);nums1[i] 大于 nums2[j] 就让 i 右移(这个 i 再也配不上更靠后的 j)。
- 4先看清画面。上面这排格子是 nums1 = 7,5,5,2,1,右边面板是 nums2 = 9,8,8,7,3,2,下标从 0 开始,两个数组都非递增。我们要挑一对下标 i 和 j,i 不超过 j,并且 nums1 在 i 处的值不超过 nums2 在 j 处的值,让距离 j 减 i 最大。上排的紫色箭头代表指针 i,右边面板里高亮的那一行代表指针 j。两个指针都从头开始,谁也不回头。
- 5开始配对。指针 i 站在 nums1 的头部下标 0,值是 7;指针 j 站在 nums2 的头部下标 0,值是 9。思路很简单,每一步就比一比 nums1 在 i 处的值和 nums2 在 j 处的值:配得上就记距离再让 j 往后拉,配不上就让 i 往后挪。当前最大距离先记成 0。开始一步步走。
- 6第一步。指针 i 在 nums1 下标 0,值是 7;指针 j 在 nums2 下标 0,值是 9。把这两个值比一比,看 nums1 在 i 处的 7 能不能不超过 nums2 在 j 处的 9。看起来 7 不大于 9,像是能配上,下一帧确认。
- 7确认:7 不大于 9,这是一对有效对,而且此时 i 等于 0 不超过 j 等于 0,下标要求也满足。距离是 j 减 i 等于 0。不过 0 没有超过已经拿到的最大距离 0,最大距离保持不变。接着把 j 右移一格,看能不能让距离更大。
- 8第二步。指针 i 在 nums1 下标 0,值是 7;指针 j 在 nums2 下标 1,值是 8。把这两个值比一比,看 nums1 在 i 处的 7 能不能不超过 nums2 在 j 处的 8。看起来 7 不大于 8,像是能配上,下一帧确认。
- 9确认:7 不大于 8,这是一对有效对,而且此时 i 等于 0 不超过 j 等于 1,下标要求也满足。距离是 j 减 i 等于 1。这比之前的最大距离还大,最大距离刷新成 1。接着把 j 右移一格,看能不能让距离更大。
- 10第三步。指针 i 在 nums1 下标 0,值是 7;指针 j 在 nums2 下标 2,值是 8。把这两个值比一比,看 nums1 在 i 处的 7 能不能不超过 nums2 在 j 处的 8。看起来 7 不大于 8,像是能配上,下一帧确认。
- 11确认:7 不大于 8,这是一对有效对,而且此时 i 等于 0 不超过 j 等于 2,下标要求也满足。距离是 j 减 i 等于 2。这比之前的最大距离还大,最大距离刷新成 2。接着把 j 右移一格,看能不能让距离更大。
- 12第四步。指针 i 在 nums1 下标 0,值是 7;指针 j 在 nums2 下标 3,值是 7。把这两个值比一比,看 nums1 在 i 处的 7 能不能不超过 nums2 在 j 处的 7。看起来 7 不大于 7,像是能配上,下一帧确认。
- 13确认:7 不大于 7,这是一对有效对,而且此时 i 等于 0 不超过 j 等于 3,下标要求也满足。距离是 j 减 i 等于 3。这比之前的最大距离还大,最大距离刷新成 3。接着把 j 右移一格,看能不能让距离更大。
- 14第五步。指针 i 在 nums1 下标 0,值是 7;指针 j 在 nums2 下标 4,值是 3。把这两个值比一比,看 nums1 在 i 处的 7 能不能不超过 nums2 在 j 处的 3。看起来 7 比 3 大,怕是配不上,下一帧见分晓。
- 15确认:7 比 3 大,配不上,这一格标红。关键在于 nums2 是非递增的,j 再往后只会更小,更不可能不小于 7,所以这个 nums1 在 i 处的 7 已经没希望配上任何更靠后的 j 了。那就把 i 右移一格,换一个更小的 nums1 值来试。j 留在原地不动,最大距离仍是 3。
- 16第六步。指针 i 在 nums1 下标 1,值是 5;指针 j 在 nums2 下标 4,值是 3。把这两个值比一比,看 nums1 在 i 处的 5 能不能不超过 nums2 在 j 处的 3。看起来 5 比 3 大,怕是配不上,下一帧见分晓。
- 17确认:5 比 3 大,配不上,这一格标红。关键在于 nums2 是非递增的,j 再往后只会更小,更不可能不小于 5,所以这个 nums1 在 i 处的 5 已经没希望配上任何更靠后的 j 了。那就把 i 右移一格,换一个更小的 nums1 值来试。j 留在原地不动,最大距离仍是 3。
- 18第七步。指针 i 在 nums1 下标 2,值是 5;指针 j 在 nums2 下标 4,值是 3。把这两个值比一比,看 nums1 在 i 处的 5 能不能不超过 nums2 在 j 处的 3。看起来 5 比 3 大,怕是配不上,下一帧见分晓。
- 19确认:5 比 3 大,配不上,这一格标红。关键在于 nums2 是非递增的,j 再往后只会更小,更不可能不小于 5,所以这个 nums1 在 i 处的 5 已经没希望配上任何更靠后的 j 了。那就把 i 右移一格,换一个更小的 nums1 值来试。j 留在原地不动,最大距离仍是 3。
- 20第八步。指针 i 在 nums1 下标 3,值是 2;指针 j 在 nums2 下标 4,值是 3。把这两个值比一比,看 nums1 在 i 处的 2 能不能不超过 nums2 在 j 处的 3。看起来 2 不大于 3,像是能配上,下一帧确认。
- 21确认:2 不大于 3,这是一对有效对,而且此时 i 等于 3 不超过 j 等于 4,下标要求也满足。距离是 j 减 i 等于 1。不过 1 没有超过已经拿到的最大距离 3,最大距离保持不变。接着把 j 右移一格,看能不能让距离更大。
- 22第九步。指针 i 在 nums1 下标 3,值是 2;指针 j 在 nums2 下标 5,值是 2。把这两个值比一比,看 nums1 在 i 处的 2 能不能不超过 nums2 在 j 处的 2。看起来 2 不大于 2,像是能配上,下一帧确认。
- 23确认:2 不大于 2,这是一对有效对,而且此时 i 等于 3 不超过 j 等于 5,下标要求也满足。距离是 j 减 i 等于 2。不过 2 没有超过已经拿到的最大距离 3,最大距离保持不变。接着把 j 右移一格,看能不能让距离更大。
- 24两个指针里,j 已经越过 nums2 的末尾,循环停下来。整趟下来 i 只往右走、j 也只往右走,两个指针各自从头扫到尾,合起来最多走 m 加 n 步。现在把这一路记录到的最大距离揭晓出来。
- 25把最好的那一对摆出来。指针 i 在下标 0,nums1 值是 7;指针 j 在下标 3,nums2 值是 7。7 不超过 7,下标 0 也不超过 3,是一对合法有效对,距离 j 减 i 等于 3。这就是全程能拿到的最大距离。后面 i 挪到 3 之后虽然也能配上,但那时 j 只差 1 到 2,距离都比 3 小。
- 26收个尾。整道题靠两个只往右走的指针:配得上就记距离再把 j 往后拉去争取更大距离,配不上就把 i 往后挪去换个更小的值。nums1 = 7,5,5,2,1 配 nums2 = 9,8,8,7,3,2,最终最大距离是 3。两个指针合起来只扫了一遍,线性时间、常数额外空间,又快又省。
⚠️ 容易写错的地方
✗ 错:只盯着数值 nums1[i] ≤ nums2[j],忘了下标还要 i ≤ j
✓ 对:两个约束一起满足才算有效对:下标 i ≤ j 且数值 nums1[i] ≤ nums2[j]
题目对有效对有两条要求,缺一不可。双指针里真正刷新最大距离的,一定是 i ≤ j 的有效对:当 i 因为配不上而连续右移、暂时跑到 j 前面时,j 减 i 是负数,天然不会成为最大值,并不影响答案;但若换成暴力枚举却漏判 i ≤ j,把 i 大于 j 的非法对也当成正距离算进去,答案就会偏大
✗ 错:把两个数组当成升序来处理,二分方向搞反
✓ 对:认准题目给的是非递增数组,双指针里 j 越往后 nums2 越小
正因为 nums2 非递增,当 nums1[i] 大于 nums2[j] 时,后面更小的 nums2 更配不上,才能果断把 i 右移。若误当升序,这套推进逻辑就不成立,会漏掉或错配下标对
✗ 错:找不到有效对时返回了负数或没初始化的值
✓ 对:最大距离从 0 起步,一个有效对都没有就返回 0
距离 j 减 i 在有效对里最小是 0,题目也规定无有效对时返回 0。若把答案初始化成负数又忘了兜底,像 nums1 全大于 nums2 的情形就会返回一个错误的负数
完整代码(Python / C++ / Java)
Python
from __future__ import annotations
from typing import *
from collections import *
from functools import *
from itertools import *
from math import *
from heapq import *
from bisect import *
class Solution:
def maxDistance(self, nums1: List[int], nums2: List[int]) -> int:
ans = 0
nums2 = nums2[::-1]
for i, v in enumerate(nums1):
j = len(nums2) - bisect_left(nums2, v) - 1
ans = max(ans, j - i)
return ansC++
#include <algorithm>
#include <array>
#include <cctype>
#include <climits>
#include <cmath>
#include <deque>
#include <functional>
#include <iostream>
#include <limits>
#include <map>
#include <numeric>
#include <queue>
#include <set>
#include <sstream>
#include <stack>
#include <string>
#include <tuple>
#include <unordered_map>
#include <unordered_set>
#include <utility>
#include <vector>
using namespace std;
class Solution {
public:
int maxDistance(vector<int>& nums1, vector<int>& nums2) {
int ans = 0;
reverse(nums2.begin(), nums2.end());
for (int i = 0; i < nums1.size(); ++i) {
int j = nums2.size() - (lower_bound(nums2.begin(), nums2.end(), nums1[i]) - nums2.begin()) - 1;
ans = max(ans, j - i);
}
return ans;
}
};Java
import java.util.*;
class Solution {
public int maxDistance(int[] nums1, int[] nums2) {
int ans = 0;
int m = nums1.length, n = nums2.length;
for (int i = 0; i < m; ++i) {
int left = i, right = n - 1;
while (left < right) {
int mid = (left + right + 1) >> 1;
if (nums2[mid] >= nums1[i]) {
left = mid;
} else {
right = mid - 1;
}
}
ans = Math.max(ans, left - i);
}
return ans;
}
}复杂度
时间
O(m log n)
m、n 分别是 nums1、nums2 的长度。代码区的参考写法对 nums1 里最多 m 个元素,每个都在 nums2 里做一次二分查找,每次 O(log n),合起来 O(m log n),这是代码区展示的写法的复杂度。动画演示的双指针写法,i 和 j 都只往右走、合起来最多前进 m 加 n 步,可以把整体降到 O(m+n) 的线性时间,两者答案一致
空间
O(1) 或 O(n)
按峰值算,而且要分语言。双指针写法只用 i、j、ans 几个变量,额外空间 O(1)。参考代码里,C++ 用 reverse 原地反转 nums2、Java 干脆不反转直接在区间上二分,这两种都是额外 O(1);唯独 Python 用切片把 nums2 倒过来时新建了一个长度为 n 的列表,额外空间是 O(n)。所以严格讲空间是 O(1),Python 的切片反转版例外要记 O(n)
看不够?换成动画再走一遍
上面的推演每一步都对应一帧动画。点开交互动画版,能一步步看着 下标对中的最大距离 的数据怎么变、指针怎么走,还能切 Python / Java / C++ 跟着练。
面试官可能追问
既然双指针能做到 O(m+n),为什么代码还写二分?+
双指针利用了另一层规律:i 往后挪时,它够得着的最靠后的 j 不会往回退,所以两个指针都只往右走一遍,合起来最多 m+n 步,时间 O(m+n),还省了倒序数组。二分版则对每个 i 独立地在 nums2 里查一次,单次 O(log n)、合计 O(m log n)。两者枚举的是同一批候选、答案完全一样;二分胜在每个 i 各算各的、思路直接不易错,双指针胜在把那个 log 抹平。面试里能说清两者等价、并点出双指针为什么能省掉二分,是加分项。
为什么满足条件的 j 一定是一段连续前缀,不会中间断开?+
全靠 nums2 非递增。固定 v 后,判断的是 nums2[j] ≥ v 这个条件。非递增意味着从左到右值只减不增,所以一旦在某个位置跌破 v,后面每个都比它还小、条件再不可能成立。于是成立的下标从 0 连到某一格、之后全不成立,中间不会出现「不成立又突然成立」的跳变。正是这条不回头的单调性,让二分只需比较中点一处、就能判断该往哪半收,而不必挨个验。
i ≤ j 这条下标约束,在代码里体现在哪?+
它落在结算 j - i 上,而不是二分过程里。二分只管按数值找每个 i 最靠后的 j,并不检查 i 和 j 的先后。真正兜住约束的是取最大值:当某个 i 配到的 j 比它还靠前(j < i)时,j - i 是负数,和当前 ans 取最大时天然被压掉,不会污染答案;而真正合法的有效对一定 i ≤ j、贡献非负距离。要提醒的是,敢在配不上时放弃当前 i 的依据是 nums2 非递增,不是 i ≤ j 这条——两点各管一段,别混为一谈。
想听吴师兄把这道题讲给你听?
文字版和动画都随便看。开通图解算法年卡,可以听吴师兄把 下标对中的最大距离 一步步讲透(全站已上线 905 份讲透,持续扩充), 卡住的地方还有 AI 私教小欧就着动画帮你拆到懂。
把这道题真正拿下
本平台为独立第三方培训机构,与华为技术有限公司无任何关联;课程的服务内容与权益以购买协议为准,学习效果因个人情况而异。「华为 OD」「华为可信」等仅为对岗位与考试方向的客观描述,相关商标归各自权利人所有。