List排序/查找最佳实践

一、核心结论(先记住这 10 条)

  1. 默认排序用稳定算法 :List.sort / Collections.sort / Kotlin list.sortWith {} → 底层 TimSort(稳定,适合多字段排序与 UI 列表)。
  2. 数字比较不要相减:用 Integer.compare(a,b) / Comparator.comparingInt(::field),避免溢出与符号错误。
  3. 字符串排序面向用户要用 Collator(按语言/Locale 排序),不要用简单 String.compareTo;中英文混排或阿语等尤其明显。
  4. Null 安全:用 Comparator.nullsFirst/Last 或 Kotlin nullsFirst/Last() 包装比较器。
  5. 多字段排序:用链式比较器(thenComparing / thenBy),稳定排序可保留同一主键下的原相对顺序。
  6. 频繁"判断是否包含" :不要每次 list.contains()(O(n)),改用 HashSet/HashMap 预建索引(O(1))。
  7. 二分查找只适用于已排序区间 :Collections.binarySearch / Kotlin binarySearch,找不到时返回"插入点编码值",要解码 idx = -pos - 1。
  8. 重复按同一 key 排序:先"装饰-排序-去装饰"(Schwartzian transform)或缓存 key,减少重复计算。
  9. 大规模实时增删 :不要反复全量排序;用二分插入维持有序,或改 TreeSet/PriorityQueue/数据库排序。
  10. RecyclerView 变更:排序后用 DiffUtil 计算差异,避免整表闪烁和 O(n) 级刷新。

二、排序(Sorting)

1) 常用 API 与稳定性

  • Java

    • list.sort(cmp)(JDK8+)/Collections.sort(list, cmp) → TimSort,稳定,O(n log n),局部有序时更快。
    • Arrays.sort(Object[]) → 稳定;Arrays.sort(int[])(原生类型)→ 双轴快速排序,不稳定
  • Kotlin

    • 就地:mutableList.sort() / sortBy { } / sortWith(cmp)(稳定)。

    • 返回新列表:sorted()/sortedBy/with(拷贝后排序,适合不可变 Pipeline)。

2) 多字段排序(链式比较器)

Java

scss 复制代码
Comparator<User> cmp = Comparator
  .comparing(User::getVip).reversed()          // VIP 优先
  .thenComparingInt(User::getLevel).reversed() // 等级高在前
  .thenComparing(User::getName, Collator.getInstance(Locale.CHINA)); // 中文名
users.sort(cmp);

Kotlin

css 复制代码
users.sortWith(
  compareByDescending<User> { it.vip }
    .thenByDescending { it.level }
    .thenComparator { a, b -> Collator.getInstance(Locale.CHINA).compare(a.name, b.name) }
)

3) Null 与倒序

less 复制代码
Comparator<Foo> cmp = Comparator
  .comparing(Foo::getScore, Comparator.nullsLast(Integer::compare))
  .reversed(); // 全局倒序

Kotlin:nullsLast(naturalOrder()) / reversed()。

4) 字符串的"语言学排序"

用户可见文本排序用:

ini 复制代码
Collator collator = Collator.getInstance(locale); // e.g. Locale.SIMPLIFIED_CHINESE / ARABIC
list.sort(Comparator.comparing(Item::getTitle, collator));

需要忽略大小写/重音:配置 collator.setStrength(Collator.PRIMARY)。

5) 性能与内存

  • TimSort 适合"部分有序" :滚动列表、分页合并、时间线排序性能更佳。
  • 装饰-排序-去装饰(Key 缓存) :避免在比较器里反复计算昂贵 key。
kotlin 复制代码
data class Decor<T>(val key: Int, val v: T)
val decorated = items.map { Decor(calcExpensiveKey(it), it) }
decorated.sortBy { it.key }
val sorted = decorated.map { it.v }

6) 大列表与并行

  • Server/JVM 大数据可用 Arrays.parallelSort(对象/原生数组)。Android 端一般不建议并行排序以免与主线程/电量打架;把重活挪到后台线程。

三、查找(Searching)

1) 线性查找(通用)

  • indexOf/lastIndexOf/find:O(n) ,小列表或一次性查找可用。

  • Kotlin 流式:firstOrNull { it.id == target }。

2) 二分查找(已排序)

Java

ini 复制代码
int idx = Collections.binarySearch(list, target, cmp);
if (idx >= 0) {
  // 找到了
} else {
  int insertionPoint = -idx - 1; // 未找到,应插入的位置
}

Kotlin

ini 复制代码
val idx = list.binarySearch(key = 123, comparator = compareBy<Item> { it.id })
  • 必须同一比较逻辑 :排序与查找用同一 Comparator,否则结果未定义。

  • 稳定插入 :list.add(insertionPoint, x) 实现边查边插维持有序。

3) 高频"包含/去重/按 id 查"

  • 预建索引更高效:
javascript 复制代码
val index: Map<Long, Item> = items.associateBy { it.id }  // O(n) 构建,O(1) 查
val set: Set<Long> = items.mapTo(hashSetOf()) { it.id }   // O(1) contains
  • 需要顺序 + 快查:LinkedHashMap 按插入/访问顺序 + O(1) 查找。

4) 前缀/模糊搜索(UI 实时)

  • 小规模:lowerBound/upperBound 在已排序数组上做前缀范围。
  • 大规模或频繁:用 Trie / FST / 索引库;或把"搜索"后移到数据库/后端。

四、易踩坑 & 规约

  1. a - b 比较(可能溢出):
rust 复制代码
// ❌
(o1, o2) -> o1.score - o2.score
// ✅
Comparator.comparingInt(Foo::getScore)
  1. 比较器不自洽(非传递/不一致)会导致排序/二分/有序集合失效:
  • 保证 自反、反对称、传递,compare(a,b)==0 时尽可能与 equals 一致。

  • 需要"稳定分组"时优先 稳定排序 + 明确二级/三级字段。

  1. 排序后用于二分但 Comparator 不同 → 结果未定义(可能错位/死循环)。
  2. subList 排序 :subList 是父列表的视图,排序/修改会作用于父列表;跨结构修改可能抛 CME(见 fail-fast 规则)。
  3. UI 列表闪烁:排序后直接 notifyDataSetChanged() 会整表重绘;用 DiffUtil 或 ListAdapter 计算差异。

五、常用范式代码

1) "时间倒序 + 同时长升序 + 置顶优先"

less 复制代码
val collator = Collator.getInstance(Locale.getDefault())
items.sortWith(
  compareByDescending<Item> { it.pinned }               // 置顶
    .thenByDescending { it.publishTime }                // 时间新
    .thenBy { it.durationSec }                          // 时长短
    .thenComparator { a, b -> collator.compare(a.title, b.title) } // 本地化
)

2) 维持有序插入(二分 + 插入点)

kotlin 复制代码
fun MutableList<Int>.insertSorted(x: Int) {
  val pos = binarySearch(x)
  val i = if (pos >= 0) pos else -pos - 1
  add(i, x)
}

3) 用户可见字符串排序(忽略大小写/重音)

ini 复制代码
Collator c = Collator.getInstance(locale);
c.setStrength(Collator.PRIMARY); // 忽略大小写与重音
list.sort(Comparator.comparing(Item::getTitle, c));

4) RecyclerView 排序后的增量刷新

css 复制代码
val diff = DiffUtil.calculateDiff(object: DiffUtil.Callback() { /* ... */ })
items.sortWith(cmp)
diff.dispatchUpdatesTo(adapter)

六、选型与流程建议

  • 一次性展示/小数据:sortedBy/sortWith 即可。

  • 实时搜索/频繁增删:维持有序(二分插入)或改用 TreeSet/数据库 ORDER BY。

  • 用户文本:一律 Collator;需要多语言按 Locale 切换。

  • "是否存在/按 id 找"高频:先建 HashSet/HashMap 索引。

  • 端上性能:重活放后台协程;UI 层用 Diff 渐进更新。

相关推荐
南北是北北3 小时前
List视图与不可变
面试
绝无仅有3 小时前
面试技巧之Linux相关问题的解答
后端·面试·github
绝无仅有3 小时前
某跳动大厂 MySQL 面试题解析与总结实战
后端·面试·github
道可到3 小时前
阿里面试原题 面试通关笔记05 | 异常、泛型与反射——类型擦除的成本与优化
java·后端·面试
晴殇i3 小时前
告别 localStorage!探索前端存储新王者 IndexedDB
前端·javascript·面试
干翻秋招3 小时前
Java知识点复习
后端·面试
聪明的笨猪猪5 小时前
Java Spring “MVC ”面试清单(含超通俗生活案例与深度理解)
java·经验分享·笔记·面试
007php00714 小时前
某大厂跳动面试:计算机网络相关问题解析与总结
java·开发语言·学习·计算机网络·mysql·面试·职场和发展
倔强青铜三14 小时前
苦练Python第63天:零基础玩转TOML配置读写,tomllib模块实战
人工智能·python·面试