找出数组排序后的目标下标 图解题解
这道题到底在问什么
- 输入
- nums=[1,2,5,2,3], target=2
- 输出
- [1,2] 排序后 [1,2,2,3,5],值 2 在下标 1 和 2
- 输入
- nums=[1,2,5,2,3], target=4
- 输出
- [] 数组里没有 4
最优解:为什么这么做
一句话答案:LeetCode 2089 找出数组排序后的目标下标用排序加收集下标:排完序相同值连成一段,从头扫一遍把等于 target 的下标依次收进来即可。时间 O(n log n)、空间 O(1)。
排序之后再找下标,这题到底要返回哪些位置
给一个下标从 0 开始的整数数组 nums 和目标值 target,先把 nums 按非递减(从小到大、允许相等)排好,再返回排序后所有等于 target 的元素所在的下标,按递增排列;一个都没有就返回空列表。题面 nums=[1,2,5,2,3]、target=2 排完是 [1,2,2,3,5],两个 2 落在下标 1 和 2,答案 [1,2];换成 target=4,数组里没有 4,返回空列表 []。
在原数组上直接记下标,为什么整个答案都是错的
最容易踩的一步,是拿到 target 就在原数组里数它出现在哪几个下标、直接返回。可题目要的是排序之后的下标:原数组 [1,2,5,2,3] 里两个 2 待在下标 1 和 3,一排序变成 [1,2,2,3,5],这两个 2 却落到了下标 1 和 2——位置整个挪了。排序前的下标和排序后的下标是两码事,不先排就找,答对纯属碰巧。
排完序之后,相同的值为什么一定连成一段
非递减排序把元素从小到大摆好,等于 target 的那些值大小完全一样,排序时既不会插到比它小的前头、也不会窜到比它大的后头,只能彼此紧挨、连成一整块连续下标。既然它们抱成一团,就用不着东找西找:从下标 0 一路扫到末尾,碰到等于 target 的就把当前下标收进结果,先扫到的下标天然更小,收出来正好是递增顺序。
参考代码就是这两下:nums.sort() 原地排完,再用一句列表推导 [i for i, v in enumerate(nums) if v == target],把命中的下标全数收下。
排序加一遍扫描,具体是怎么走完的
第一步原地排序,把 nums 变成非递减序列。第二步从左到右扫,用下标 i 遍历每个元素 v:v 等于 target 就记下这个 i;v 小于 target 说明还没扫进目标块、继续往右;v 大于 target 说明已经越过目标块(后面只会更大),剩下的也都不是。扫到头,收集起来的下标列表就是答案;如果一路下来没有一个等于 target,列表始终是空的,直接返回空列表。
拿题面 [1,2,5,2,3]、target=2 亲手走一遍
先排序:[1,2,5,2,3] 变成 [1,2,2,3,5]。再从头往右扫:下标 0 处是 1,比 2 小,跳过;下标 1、2 处都是 2,正好命中,依次收下下标 1 和 2,结果攒成 [1,2];下标 3 处是 3、下标 4 处是 5,都比 2 大、越过了目标块,跳过。扫完返回 [1,2],和题面对上。若把 target 换成 4,整趟没有一个等于 4,返回空列表 []。
复杂度卡在排序,还有哪两处收尾容易写反
时间上排序是 O(n log n) 的主导项,后面收集下标只扫一遍是 O(n),合起来仍是 O(n log n);空间除去输出列表只用常数个变量,不计排序内部开销是 O(1)。收尾两处别写反:target 不存在时要返回空列表 [],别返回 [-1] 或抛错,题面白纸黑字要的就是空列表;重复的 target 有几个就得收几个下标,别以为相同值只算一个,题面里两个 2 就对应下标 1、2,漏一个答案就短一截。
▶ 动画逐步走查(共 22 步)——想跟着动画一帧帧对照就展开
- 3记住这套路:先排序让相等的值抱团,再扫一遍把等于 target 的下标一个个收进来。下面先看原始数组长什么样。
- 4这是原始的 nums,一共 8 个数,还没排序。我们要找的目标值是 3。你先扫一眼,3 在这里东一个西一个,毫无规律,直接在原数组上找下标是没有意义的,因为题目要的是排序之后的下标。
- 5把原数组里所有值为 3 的格子标成绿色,它们分别在下标 1、3、5。你看,它们是散开的,中间还夹着别的数。这正是为什么要先排序:排完之后,这三个 3 会被拢到一起。
- 6调用语言内置的排序,把 nums 从小到大排好,现在它是 [1,3,3,3,5,6,8,9]。注意看,三个 3 已经紧紧挨在一块了。接下来从下标 0 开始,一格一格往右扫,遇到等于 3 的就把下标收进答案。
- 7紫色指针走到下标 0,这里的值是 1。拿它和 target 也就是 3 比一比,看是小、是等还是大。
- 81 比 3 小,它排在目标块的左边,不是我们要的,标成蓝色跳过。继续往右走。
- 9紫色指针走到下标 1,这里的值是 3。拿它和 target 也就是 3 比一比,看是小、是等还是大。
- 10正好等于 3,命中!把下标 1 收进答案,这一格标成绿色。目前收集到 [1]。
- 11紫色指针走到下标 2,这里的值是 3。拿它和 target 也就是 3 比一比,看是小、是等还是大。
- 12正好等于 3,命中!把下标 2 收进答案,这一格标成绿色。目前收集到 [1,2]。
- 13紫色指针走到下标 3,这里的值是 3。拿它和 target 也就是 3 比一比,看是小、是等还是大。
- 14正好等于 3,命中!把下标 3 收进答案,这一格标成绿色。目前收集到 [1,2,3]。
- 15紫色指针走到下标 4,这里的值是 5。拿它和 target 也就是 3 比一比,看是小、是等还是大。
- 165 比 3 大了。因为已经排好序,它右边的数只会更大,再也不会碰到 3,这一格标灰。参考代码是老实扫完的,但你心里要清楚:到这里其实已经可以收工了。
- 17紫色指针走到下标 5,这里的值是 6。拿它和 target 也就是 3 比一比,看是小、是等还是大。
- 186 比 3 大了。因为已经排好序,它右边的数只会更大,再也不会碰到 3,这一格标灰。参考代码是老实扫完的,但你心里要清楚:到这里其实已经可以收工了。
- 19紫色指针走到下标 6,这里的值是 8。拿它和 target 也就是 3 比一比,看是小、是等还是大。
- 208 比 3 大了。因为已经排好序,它右边的数只会更大,再也不会碰到 3,这一格标灰。参考代码是老实扫完的,但你心里要清楚:到这里其实已经可以收工了。
- 21紫色指针走到下标 7,这里的值是 9。拿它和 target 也就是 3 比一比,看是小、是等还是大。
- 229 比 3 大了。因为已经排好序,它右边的数只会更大,再也不会碰到 3,这一格标灰。参考代码是老实扫完的,但你心里要清楚:到这里其实已经可以收工了。
- 23这里藏着一个更快的思路。比 3 小的元素只有 1 个,就是那个 1,所以排序后第一个 3 一定落在下标 1。目标块从下标 1 起头,一共 3 个 3,于是下标就是 1、2、3。顺着这个观察,其实连排序都能省掉,后面面试环节细说。
- 24扫完全程,绿色的三格就是等于 3 的位置,下标依次是 1、2、3。所以答案是 [1,2,3],和我们一开始说的对上了。左边蓝色的更小、右边灰色的更大,绿色这一块就是目标下标。
⚠️ 容易写错的地方
✗ 错:在原始数组上直接找下标就返回
✓ 对:必须先排序,再找排序后的下标
题目要的是排序后的下标,原数组里 3 在下标 1、3、5,和排序后的 1、2、3 完全不同
✗ 错:target 不存在时返回 [-1] 或抛错
✓ 对:返回空列表 []
题面明确规定不存在目标下标时返回空列表,不是特殊标记
✗ 错:以为重复的 target 只算一个下标
✓ 对:每一个等于 target 的下标都要收
有几个等于 target 的元素就有几个下标,例子里三个 3 对应三个下标
✗ 错:返回的下标顺序随意
✓ 对:按递增顺序返回
从左到右扫描天然递增,顺手就满足了这个要求
完整代码(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 targetIndices(self, nums: List[int], target: int) -> List[int]:
nums.sort()
return [i for i, v in enumerate(nums) if v == target]C++
#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:
vector<int> targetIndices(vector<int>& nums, int target) {
sort(nums.begin(), nums.end());
vector<int> ans;
for (int i = 0; i < nums.size(); ++i) {
if (nums[i] == target) {
ans.push_back(i);
}
}
return ans;
}
};Java
import java.util.*;
class Solution {
public List<Integer> targetIndices(int[] nums, int target) {
Arrays.sort(nums);
List<Integer> ans = new ArrayList<>();
for (int i = 0; i < nums.length; ++i) {
if (nums[i] == target) {
ans.add(i);
}
}
return ans;
}
}复杂度
时间
O(n log n)
排序是主导,n 个元素排序要 O(n log n);后面从头扫一遍收集下标是 O(n),加起来仍是 O(n log n)
空间
不计排序 O(1);计入排序 C plus plus / Java O(log n),Python 最坏 O(n)
除去输出列表,只用常数个辅助变量。若把排序内部开销计入:C plus plus 与 Java 的排序递归栈约 O(log n),Python 的 Timsort 最坏 O(n);不计排序则 O(1)
看不够?换成动画再走一遍
上面的推演每一步都对应一帧动画。点开交互动画版,能一步步看着 找出数组排序后的目标下标 的数据怎么变、指针怎么走,还能切 Python / Java / C++ 跟着练。
面试官可能追问
能不能不排序,做到 O(n)?+
能,而且更快。不排序,只扫一遍数组数两件事:严格小于 target 的元素有多少个,记为 less;等于 target 的有多少个,记为 equal。排序后比 target 小的都会排在它前面,占掉下标 0 到 less 减 1,所以第一个 target 正好落在下标 less,一共 equal 个,答案就是 less、less 加 1……一直到 less 加 equal 减 1 这串连续下标。拿题面 [1,2,5,2,3]、target=2 试:比 2 小的只有 1 个(那个 1),less=1;等于 2 的有 2 个,equal=2;于是下标从 1 到 2,还是 [1,2]。一遍扫描,时间 O(n)、空间 O(1),连排序都省了。
为什么目标块的起始下标正好等于比 target 小的元素个数?+
排序后,所有严格小于 target 的元素都被摆在 target 前面,一个不多一个不少地占满下标 0 到 less 减 1 这 less 个位置。紧接着的下一个位置——下标 less——就轮到第一个等于 target 的元素了。所以只要数清有多少个比 target 小的,就等于知道了目标块从哪儿起头,连排序都不必真跑。
题目要求下标按递增排列,需要把结果再排一次吗?+
不用。从下标 0 一路往右扫,先遇到的下标一定比后遇到的小,把命中的 i 依次追加进结果,列表天生就是从小到大的,不必再对答案排序。真去多排一趟,不仅白费一次 O(k log k),还容易让人误以为收集顺序有讲究——其实顺着扫下来就已经满足递增了。
想听吴师兄把这道题讲给你听?
文字版和动画都随便看。开通图解算法年卡,可以听吴师兄把 找出数组排序后的目标下标 一步步讲透(全站已上线 905 份讲透,持续扩充), 卡住的地方还有 AI 私教小欧就着动画帮你拆到懂。
本平台为独立第三方培训机构,与华为技术有限公司无任何关联;课程的服务内容与权益以购买协议为准,学习效果因个人情况而异。「华为 OD」「华为可信」等仅为对岗位与考试方向的客观描述,相关商标归各自权利人所有。