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 渐进更新。

相关推荐
求梦8206 小时前
前端八股文【CSS核心面试题库】
前端·css·面试
NAGNIP13 小时前
万字长文!回归模型最全讲解!
算法·面试
qq_3181215914 小时前
互联网大厂Java面试故事:从Spring Boot到微服务架构的技术挑战与解答
java·spring boot·redis·spring cloud·微服务·面试·内容社区
且去填词15 小时前
Go 语言的“反叛”——为什么少即是多?
开发语言·后端·面试·go
青莲84318 小时前
RecyclerView 完全指南
android·前端·面试
青莲84318 小时前
Android WebView 混合开发完整指南
android·前端·面试
37手游后端团队21 小时前
gorm回读机制溯源
后端·面试·github
C雨后彩虹21 小时前
竖直四子棋
java·数据结构·算法·华为·面试
CC码码1 天前
不修改DOM的高亮黑科技,你可能还不知道
前端·javascript·面试
indexsunny1 天前
互联网大厂Java面试实战:微服务、Spring Boot与Kafka在电商场景中的应用
java·spring boot·微服务·面试·kafka·电商