Java 集合算法:从排序、查找到底层原理的实战指南

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 时间复杂度速查

集合算法的核心是理解不同数据结构的时间复杂度差异:

  • ArrayListget(index) 为 O(1),contains() 为 O(n)。
  • HashSet/HashMapcontainsKey() / contains() 平均 O(1)。
  • TreeSet/TreeMap:查找、插入、删除均为 O(log n)。
  • LinkedListget(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.sortint[] 等基本类型数组使用 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. 实战:综合案例

下面通过一个完整的案例,综合运用上述集合算法。假设我们需要处理一份订单数据,完成以下任务:

  1. 找出所有下单用户中的重复用户;
  2. 统计每个用户的订单金额总和;
  3. 按金额降序输出 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.sortComparator 与 TimSort 原理;
  • 查找:区分线性查找与二分查找,善用 HashSet 加速;
  • 去重与统计:利用 Set 特性与 Stream API 高效处理;
  • 集合运算:掌握交集、并集、差集的两种实现方式;
  • 洗牌与旋转 :了解 Collections 工具类的实用方法。

理解集合的底层数据结构是写出高效代码的前提。在实际开发中,建议先分析数据规模与操作频率,再选择合适的集合类型与算法,避免盲目使用默认实现导致性能瓶颈。

相关推荐
devpotato1 小时前
缓存与数据库更新顺序不一致问题
java·数据库·redis
shehuiyuelaiyuehao1 小时前
算法34,位运算符操作,总结
算法
xcl09251 小时前
酒馆预约系统开发实战:从需求分析到上线全流程指南
java·spring boot·需求分析
Navigator_Z1 小时前
LeetCode //C - 1224. Maximum Equal Frequency
c语言·算法·leetcode
Kyrie_kk2 小时前
Java--IO--Path文件访问
java·后端
qinqinzqq2 小时前
Maven POM 格式、Schema 与 XSD:一篇讲透
java·maven
o盟2 小时前
maven本地仓库有 总是去私仓中下载
java·maven·intellij-idea
devpotato2 小时前
Java 批量并发请求:从“能并发“到“结果按完成顺序可用“的三种写法与选型
java
Navigator_Z2 小时前
LeetCode //C++ - 1226. The Dining Philosophers
c语言·算法·leetcode