1. 引言
集合是 Java 开发中使用频率最高的数据结构之一,而围绕集合的算法操作(排序、查找、去重、分组、取交集并集等)更是日常编码中绕不开的核心技能。无论是面试中的手写算法题,还是生产环境下的性能调优,理解 Java 集合框架(Collection Framework)背后的算法原理,都能帮助你写出更高效、更健壮的代码。
本文将从集合的底层数据结构出发,系统梳理 Java 中常用的集合算法,包括排序与查找、去重与统计、集合运算、洗牌与旋转等,并结合 JDK 源码分析其时间复杂度与适用场景,最后给出实战中的选型建议。
2. 集合框架与算法基础
在深入具体算法之前,先回顾 Java 集合框架的整体结构,以及算法复杂度分析的基本概念。
2.1 集合框架总览
Java 集合框架主要分为两大体系:
- Collection 接口:单列集合的根接口,包括 List(有序可重复)、Set(无序不可重复)、Queue(队列)。
- Map 接口:键值对集合,包括 HashMap、TreeMap、LinkedHashMap 等。
常用实现类的底层数据结构如下:
| 接口 | 实现类 | 底层结构 | 特点 |
|---|---|---|---|
| List | ArrayList | 动态数组 | 随机访问快,插入删除慢 |
| List | LinkedList | 双向链表 | 插入删除快,随机访问慢 |
| Set | HashSet | HashMap | 无序,O(1) 查找 |
| Set | TreeSet | 红黑树 | 有序,O(log n) 查找 |
| Map | HashMap | 数组 + 链表/红黑树 | O(1) 查找 |
| Map | TreeMap | 红黑树 | 按键有序,O(log n) |
2.2 时间复杂度速查
集合算法的核心是理解不同数据结构的时间复杂度差异:
- ArrayList :
get(index)为 O(1),contains()为 O(n)。 - HashSet/HashMap :
containsKey()/contains()平均 O(1)。 - TreeSet/TreeMap:查找、插入、删除均为 O(log n)。
- LinkedList :
get(index)为 O(n),头尾插入为 O(1)。
实际开发中,很多性能问题源于「在 ArrayList 上做频繁的 contains 判断」,此时换成 HashSet 往往立竿见影。
3. 排序算法
排序是集合操作中最常见的需求。Java 为数组和集合提供了丰富的排序 API。
3.1 使用 Collections.sort 与 List.sort
java
List<Integer> numbers = new ArrayList<>(Arrays.asList(5, 3, 8, 1, 9, 2));
Collections.sort(numbers);
System.out.println(numbers); // [1, 2, 3, 5, 8, 9]
// 自定义排序:按字符串长度排序
List<String> words = Arrays.asList("banana", "apple", "cherry", "date");
words.sort(Comparator.comparingInt(String::length));
System.out.println(words); // [date, apple, banana, cherry]
3.2 对象排序:Comparable 与 Comparator
java
public class Student implements Comparable<Student> {
private final String name;
private final int score;
// 构造方法、getter 省略
@Override
public int compareTo(Student other) {
return Integer.compare(this.score, other.score);
}
}
// 使用 Comparator 实现多重排序
List<Student> students = getStudents();
students.sort(Comparator.comparing(Student::getScore)
.reversed()
.thenComparing(Student::getName));
3.3 底层原理:TimSort
JDK 对对象数组的排序采用 TimSort 算法,它是一种结合了归并排序和插入排序的稳定排序算法:
- 时间复杂度:最好 O(n),最坏 O(n log n)。
- 空间复杂度:O(n)。
- 稳定性:稳定排序,相等元素的相对顺序不变。
java
// Arrays.sort 对对象数组使用 TimSort
Student[] arr = students.toArray(new Student[0]);
Arrays.sort(arr, Comparator.comparingInt(Student::getScore));
注意:
Arrays.sort对int[]等基本类型数组使用 Dual-Pivot QuickSort(双轴快排),它是不稳定排序,但对基本类型没有影响。
4. 查找算法
4.1 线性查找与二分查找
java
// 线性查找:适用于未排序集合
List<String> list = Arrays.asList("Java", "Python", "Go", "Rust");
boolean found = list.contains("Go"); // O(n)
// 二分查找:要求集合已排序
List<Integer> sorted = Arrays.asList(1, 3, 5, 7, 9, 11);
int index = Collections.binarySearch(sorted, 7);
System.out.println(index); // 3
4.2 利用 HashSet 加速查找
当需要频繁判断元素是否存在时,应使用 HashSet 替代 List:
java
// 低效:O(n) 每次
List<String> blacklist = loadBlacklist();
boolean blocked = blacklist.contains(userIp);
// 高效:O(1) 每次
Set<String> blacklistSet = new HashSet<>(loadBlacklist());
boolean blocked = blacklistSet.contains(userIp);
4.3 查找最大最小值
java
List<Integer> scores = Arrays.asList(88, 95, 72, 100, 63);
int max = Collections.max(scores);
int min = Collections.min(scores);
// 查找自定义对象的最大值
Student top = Collections.max(students, Comparator.comparingInt(Student::getScore));
5. 去重与统计
5.1 集合去重
java
// 方法一:利用 HashSet 去重(不保留顺序)
List<Integer> list = Arrays.asList(3, 1, 2, 3, 4, 1, 5);
Set<Integer> unique = new HashSet<>(list);
// 方法二:利用 LinkedHashSet 去重(保留插入顺序)
List<Integer> orderedUnique = new ArrayList<>(new LinkedHashSet<>(list));
// 方法三:Java 8 Stream 去重
List<Integer> streamUnique = list.stream().distinct().collect(Collectors.toList());
5.2 统计元素频率
java
// 使用 Collections.frequency 统计单个元素
List<String> words = Arrays.asList("a", "b", "a", "c", "a", "b");
int countA = Collections.frequency(words, "a"); // 3
// 统计所有元素的频率
Map<String, Long> freqMap = words.stream()
.collect(Collectors.groupingBy(Function.identity(), Collectors.counting()));
System.out.println(freqMap); // {a=3, b=2, c=1}
5.3 找出重复元素
java
List<Integer> numbers = Arrays.asList(1, 2, 3, 2, 4, 5, 3, 6);
Set<Integer> seen = new HashSet<>();
Set<Integer> duplicates = new HashSet<>();
for (Integer num : numbers) {
if (!seen.add(num)) {
duplicates.add(num);
}
}
System.out.println(duplicates); // [2, 3]
6. 集合运算:交集、并集、差集
6.1 使用 Set 进行集合运算
java
Set<Integer> setA = new HashSet<>(Arrays.asList(1, 2, 3, 4));
Set<Integer> setB = new HashSet<>(Arrays.asList(3, 4, 5, 6));
// 交集
Set<Integer> intersection = new HashSet<>(setA);
intersection.retainAll(setB); // [3, 4]
// 并集
Set<Integer> union = new HashSet<>(setA);
union.addAll(setB); // [1, 2, 3, 4, 5, 6]
// 差集(A - B)
Set<Integer> difference = new HashSet<>(setA);
difference.removeAll(setB); // [1, 2]
6.2 使用 Stream 进行集合运算
java
List<Integer> listA = Arrays.asList(1, 2, 3, 4, 5);
List<Integer> listB = Arrays.asList(4, 5, 6, 7, 8);
// 交集
List<Integer> inter = listA.stream()
.filter(listB::contains)
.collect(Collectors.toList());
// 差集
List<Integer> diff = listA.stream()
.filter(item -> !listB.contains(item))
.collect(Collectors.toList());
当数据量较大时,建议先将 List 转为 HashSet 再做过滤,避免 O(n²) 的 contains 调用。
7. 洗牌、旋转与反转
7.1 洗牌(Shuffle)
java
List<String> deck = new ArrayList<>(Arrays.asList("A", "2", "3", "4", "5"));
Collections.shuffle(deck);
System.out.println(deck); // 每次运行结果不同
// 指定随机源,便于测试复现
Collections.shuffle(deck, new Random(42));
7.2 旋转(Rotate)
java
List<Integer> list = new ArrayList<>(Arrays.asList(1, 2, 3, 4, 5));
// 向右旋转 2 位
Collections.rotate(list, 2);
System.out.println(list); // [4, 5, 1, 2, 3]
// 向左旋转 2 位(等价于向右旋转 size - 2)
Collections.rotate(list, -2);
System.out.println(list); // [3, 4, 5, 1, 2]
7.3 反转(Reverse)
java
List<Integer> list = new ArrayList<>(Arrays.asList(1, 2, 3, 4, 5));
Collections.reverse(list);
System.out.println(list); // [5, 4, 3, 2, 1]
8. 实战:综合案例
下面通过一个完整的案例,综合运用上述集合算法。假设我们需要处理一份订单数据,完成以下任务:
- 找出所有下单用户中的重复用户;
- 统计每个用户的订单金额总和;
- 按金额降序输出 Top 3 用户。
java
import java.util.*;
import java.util.stream.Collectors;
public class OrderAnalyzer {
record Order(String userId, double amount) {}
public static void main(String[] args) {
List<Order> orders = Arrays.asList(
new Order("u001", 120.0),
new Order("u002", 85.5),
new Order("u001", 200.0),
new Order("u003", 45.0),
new Order("u002", 60.0),
new Order("u004", 300.0)
);
// 1. 找出重复下单的用户
Set<String> seen = new HashSet<>();
Set<String> duplicates = new HashSet<>();
for (Order order : orders) {
if (!seen.add(order.userId())) {
duplicates.add(order.userId());
}
}
System.out.println("重复下单用户: " + duplicates);
// 2. 统计每个用户的订单金额总和
Map<String, Double> totalByUser = orders.stream()
.collect(Collectors.groupingBy(
Order::userId,
Collectors.summingDouble(Order::amount)
));
System.out.println("用户金额统计: " + totalByUser);
// 3. 按金额降序输出 Top 3 用户
List<Map.Entry<String, Double>> top3 = totalByUser.entrySet().stream()
.sorted(Map.Entry.<String, Double>comparingByValue().reversed())
.limit(3)
.collect(Collectors.toList());
System.out.println("Top 3 用户: " + top3);
}
}
输出结果:
重复下单用户: [u001, u002]
用户金额统计: {u001=320.0, u002=145.5, u003=45.0, u004=300.0}
Top 3 用户: [u001=320.0, u004=300.0, u002=145.5]
9. 性能优化建议
9.1 集合选型速查
| 场景 | 推荐集合 | 原因 |
|---|---|---|
| 频繁随机访问 | ArrayList | O(1) 索引访问 |
| 频繁头尾插入删除 | LinkedList / ArrayDeque | O(1) 头尾操作 |
| 频繁查找去重 | HashSet / HashMap | 平均 O(1) 查找 |
| 需要有序遍历 | TreeSet / TreeMap | 按键自然排序 |
| 保持插入顺序 | LinkedHashSet / LinkedHashMap | 双向链表维护顺序 |
9.2 常见性能陷阱
- 在循环中调用
list.contains():整体 O(n²),应改用 HashSet。 - 频繁扩容的 ArrayList :预估容量,使用
new ArrayList<>(expectedSize)。 - 在 LinkedList 上随机访问 :
get(i)是 O(n),应改用迭代器或 ArrayList。 - 字符串拼接使用
+:在循环中应使用StringBuilder。
9.3 初始容量设置
java
// HashMap 默认容量 16,负载因子 0.75
// 预估存储 1000 个元素时,建议:
Map<String, Integer> map = new HashMap<>(1000 / 0.75 + 1);
// 即 new HashMap<>(1334),避免频繁扩容
10. 总结
本文系统梳理了 Java 集合算法的核心内容:
- 排序 :掌握
Collections.sort、Comparator与 TimSort 原理; - 查找:区分线性查找与二分查找,善用 HashSet 加速;
- 去重与统计:利用 Set 特性与 Stream API 高效处理;
- 集合运算:掌握交集、并集、差集的两种实现方式;
- 洗牌与旋转 :了解
Collections工具类的实用方法。
理解集合的底层数据结构是写出高效代码的前提。在实际开发中,建议先分析数据规模与操作频率,再选择合适的集合类型与算法,避免盲目使用默认实现导致性能瓶颈。