目录

题目描述

1122. 数组的相对排序

image-20230311221941197

题意分析

要对 arr1 重新排序,但排序的「大小关系」不是数值本身,而是由 arr2 定义的一张自定义优先级表:凡是在 arr2 中出现过的值,按它在 arr2 里的下标先后排;没在 arr2 中出现过的值,统一排在所有已定义元素之后,并且内部按数值升序。

题目额外承诺 arr2 中的元素互不相同,且每个都在 arr1 中出现过。前半句保证了「值 → 优先级」是一个良定义的映射,不会出现同一个值有两个不同优先级;后半句保证按 arr2 顺序输出时每个值至少能取出一个,不会出现空转。

最关键的信号藏在数据范围里:arr1.lengtharr2.length 都不超过 1000,而所有元素的取值范围是 0 到 1000。值域和数组长度同阶且都很小,这种「有限且已知的整数值域」几乎是在明示可以用值本身当下标,而不必依赖比较。

注意 arr1 中的值是可以重复的,同一个值出现多次时它们彼此不可区分,全部连续排在一起即可,不存在稳定性问题。

边界上要考虑:arr1 中的值全部都在 arr2 里(第二段为空);arr1 中的值全都不在 arr2 里(第一段为空,退化成普通升序);值为 0 的元素(下标从 0 开始,计数数组不能少开这一格);值恰好为 1000 的元素(数组长度必须是 1001 而不是 1000)。

解法:计数排序

核心思路

元素值只在 [0, 1000] 内,适合用计数排序:先统计每个值在 arr1 中出现的次数,再按题目要求的顺序把频次展开。与“哈希优先级 + 比较排序”相比,这直接利用了小值域,避免 O(n log n) 排序和比较器分支。

输出分两段。第一段遍历 arr2,按其给定顺序写出每个值的全部副本,并把对应计数减到 0;第二段从 0 到 1000 扫描计数表,把剩余值按数值升序写出。第一段已经清零的值在第二段自然被跳过,不需要额外集合。

维护不变量:count[v] 始终表示值 v 尚未写入结果的个数,写指针 index 等于已写元素数。本文直接把 arr1 当作输出缓冲区,不再申请同长度结果数组。

正确性说明:第一段严格按 arr2 顺序输出所有受约束元素;每个值一次性写完,因此同值连续且数量不变。第二段按值从小到大输出所有剩余元素,正好满足“未出现在 arr2 中的元素升序”。每次输出都同步减少频次,所有计数最终归零,所以结果不重不漏。

解题步骤

  • 创建长度 1001 的计数数组,统计 arr1 中每个值的频次。
  • index = 0,按 arr2 顺序逐个值展开全部频次,写回 arr1。
  • 从值 0 扫描到 1000,展开计数表中仍未消费的元素。
  • 返回已经重排的 arr1。

对样例 arr1 = [2,3,1,3,2,4,6,7,9,2,19]arr2 = [2,1,4,3,9,6],第一段写出 [2,2,2,1,4,3,3,9,6],计数表只剩 7 和 19;第二段升序补成 [2,2,2,1,4,3,3,9,6,7,19]。值 0 与 1000 都是合法边界,计数数组必须覆盖二者。

代码实现

class Solution {
    public int[] relativeSortArray(int[] arr1, int[] arr2) {
        int[] count = new int[1001];
        for (int value : arr1) {
            count[value]++;
        }

        int index = 0;
        for (int value : arr2) {
            while (count[value] > 0) {
                arr1[index++] = value;
                count[value]--;
            }
        }
        for (int value = 0; value < count.length; value++) {
            while (count[value] > 0) {
                arr1[index++] = value;
                count[value]--;
            }
        }
        return arr1;
    }
}
func relativeSortArray(arr1 []int, arr2 []int) []int {
	count := make([]int, 1001)
	for _, value := range arr1 {
		count[value]++
	}

	index := 0
	for _, value := range arr2 {
		for count[value] > 0 {
			arr1[index] = value
			index++
			count[value]--
		}
	}
	for value := 0; value < len(count); value++ {
		for count[value] > 0 {
			arr1[index] = value
			index++
			count[value]--
		}
	}
	return arr1
}

复杂度分析

  • 时间复杂度:O(n + m + C)。n、m 分别是 arr1、arr2 的长度,C = 1001 是值域大小;所有频次展开的总次数为 n。
  • 空间复杂度:O(C)。只额外使用计数数组;在本题固定值域下也可视为 O(1)

关键点总结

  • 小而连续的整数值域是计数排序的选择依据;若值域远大于数组长度,再考虑哈希表和比较排序。
  • 第一段按 arr2 控制组间顺序,第二段按数值控制剩余元素顺序,两类规则不能混在一次扫描里。
  • “消费频次并清零”同时完成输出和已处理标记,无需额外集合。
  • arr1 本身可以复用为输出数组,计数完成后原排列已不再需要。

易错点总结

  • 计数数组只开 1000 个位置arr1 = [1000] 会访问越界;闭区间 [0,1000] 需要 1001 个槽位。
  • 展开频次时使用 if 而不是 whilearr1 = [2,2,2,1], arr2 = [2,1] 会先只写一个 2,剩余 2 被错误放到尾部。
  • 写出后忘记减少计数:循环条件永远成立,写指针最终越过 arr1 末尾;Java 会数组越界,Go 会索引越界并 panic。
  • 第二段从 1 开始或在 1000 前停止arr1 = [0,1000], arr2 = [] 会漏掉合法边界值。
  • 第二段仍按 arr1 原出现顺序写arr1 = [19,7], arr2 = [] 会得到 [19,7],而题目要求剩余元素升序为 [7,19]
  • 先覆盖 arr1、后统计频次:复用 arr1 作为输出的前提是计数已经完整保存了原数据;统计未结束就写回会污染后续计数。

相似题目

题目 难度 考察点
LCR 075. 数组的相对排序 简单 与本题同构的另一编号版本,可用来检验同一套计数模板能否脱稿默写
1365. 有多少小于当前数字的数字 简单 计数表要再做一次前缀和才能回答「有多少个更小」,多了一步累加而非直接读取
1636. 按照频率将数组升序排序 简单 排序键是频次本身且频次相同时按值降序,需要双关键字比较而不是照抄外部次序
75. 颜色分类 中等 值域只有三个且要求原地一趟完成,考察三路划分指针而非另开计数数组
347. 前 K 个高频元素 中等 值域无界必须用哈希计数,再靠桶排序或堆取前 K,考察从计数到选择的衔接
451. 根据字符出现频率排序 中等 对象是字符且要按频次降序重建字符串,考察定长字符表加桶排序的组合