处理数组或列表时,重复元素几乎是绕不开的问题。有时我们需要彻底去重,只保留每个元素的唯一副本;有时又不需要那么彻底,只想让每个元素最多出现两三次。Java提供了多种手段来应对这两类需求,从最基础的Set集合到Stream API,再到手写的计数逻辑,各有各的适用场景。本文将从去重原理讲起,逐步过渡到限频控制的实现,帮助你彻底掌握这一类问题的解法。

一、用Set集合实现基础去重:三种实现类的取舍
Set集合天生具有元素不重复的特性,是Java中最直接的去重工具。把数组元素逐个放入Set,再转回数组,就能完成去重。但Set接口有三个常用实现类:HashSet、LinkedHashSet和TreeSet,它们在去重之后的输出顺序上差别很大,选错了往往会导致结果与预期不符。
HashSet基于哈希表实现,插入和查询的时间复杂度都是O(1),性能最好,但它不保证元素顺序。也就是说,你把数组里的元素放进去再取出来,顺序可能与原数组完全不同。如果你的业务对顺序没有要求,比如只是统计有哪些不同的值,HashSet是最快的选择。
LinkedHashSet在哈希表的基础上维护了一条双向链表,能够保持元素的插入顺序。对大多数去重场景来说,我们希望保留元素第一次出现的先后关系,这时候LinkedHashSet就是最稳妥的方案,代价只是比HashSet略多一点内存开销和插入耗时,通常可以忽略不计。
TreeSet基于红黑树实现,会对元素排序,时间复杂度为O(log n)。它适合需要去重同时还要按自然顺序或自定义比较器排序的场景,比如对一批数字去重后从小到大输出。三种方案的取舍可以简单记为:要性能用HashSet,要保序用LinkedHashSet,要排序用TreeSet。
import java.util.*;
public class SetDedupDemo {
public static void main(String[] args) {
Integer[] arr = {3, 1, 3, 7, 1, 9, 7, 3};
// 方式一:HashSet,速度最快,不保证顺序
Set<Integer> hashSet = new HashSet<>(Arrays.asList(arr));
// 方式二:LinkedHashSet,保持元素首次出现的顺序
Set<Integer> linkedSet = new LinkedHashSet<>(Arrays.asList(arr));
// 方式三:TreeSet,去重的同时自动排序
Set<Integer> treeSet = new TreeSet<>(Arrays.asList(arr));
System.out.println("HashSet结果: " + hashSet);
System.out.println("LinkedHashSet结果: " + linkedSet);
System.out.println("TreeSet结果: " + treeSet);
}
}二、Stream API的distinct方法:一行代码完成去重
从JDK 8开始,Stream API为集合处理带来了声明式的写法。去重只需要调用一次distinct()方法,它会根据元素的equals和hashCode逻辑过滤掉重复项,并且保持遇到顺序,效果等同于LinkedHashSet。这种方式代码量极少,可读性也好,特别适合嵌入在流水线式的数据处理逻辑中。
使用Stream去重时有一点需要注意:distinct()的判断依据是equals方法。对于String、Integer这类标准库类型没有问题,但如果数组元素是自定义对象,你必须正确重写equals和hashCode,否则即使两个字段完全相同的对象,也会被当作不同元素保留下来,去重失效。这是实际项目中最常见的坑。
另外,如果数据源是基本类型数组(如int[]),直接调用Arrays.stream得到的是IntStream,它同样支持distinct,但转回数组时要用toArray(),返回的是int[]而不是Integer[],类型上的细微差别在传参时要留意。
import java.util.*;
import java.util.stream.*;
public class StreamDedupDemo {
public static void main(String[] args) {
int[] numbers = {5, 2, 5, 8, 2, 5};
// 基本类型数组去重
int[] unique = Arrays.stream(numbers).distinct().toArray();
System.out.println("int数组去重: " + Arrays.toString(unique));
// 对象数组去重,保持出现顺序
String[] words = {"apple", "banana", "apple", "cherry", "banana"};
String[] result = Arrays.stream(words)
.distinct()
.toArray(String[]::new);
System.out.println("字符串数组去重: " + Arrays.toString(result));
}
}三、进阶:控制元素最多重复N次的实现思路
去重是限频的特例,即每个元素最多出现1次。如果需求是每个元素最多出现2次或3次,Set就不够用了,因为Set只能记“有没有”,记不了“出现几次”。此时需要引入计数结构,最常见的是HashMap或LinkedHashMap(保持顺序),遍历数组时统计每个元素的出现次数,未达到上限就保留,达到上限就丢弃。
这种做法的时间复杂度是O(n),只需遍历一次数组,每个元素的计数查询都是O(1),效率非常高。哈希表的计数方式也是手写词频统计的标准套路,理解了这一段,后续遇到限频、TopK高频元素等问题都能举一反三。比如LeetCode上经典的“删除有序数组中的重复项II”,本质上就是这一思路在有序数组上的变体。
如果使用JDK 8以上的版本,还可以配合merge方法简化计数逻辑,一行代码完成“存在则加一,不存在则置一”的操作,代码更紧凑。下面给出一个完整示例,控制每个元素最多出现2次:
import java.util.*;
public class LimitFrequencyDemo {
public static void main(String[] args) {
Integer[] arr = {4, 4, 4, 6, 6, 6, 6, 8, 4};
int maxCount = 2; // 每个元素最多保留的次数
Map<Integer, Integer> counter = new LinkedHashMap<>();
List<Integer> result = new ArrayList<>();
for (Integer num : arr) {
// merge方法:key存在则旧值加1,不存在则初始化为1
int current = counter.merge(num, 1, Integer::sum);
if (current <= maxCount) {
result.add(num);
}
}
System.out.println("原始数组: " + Arrays.toString(arr));
System.out.println("限频后: " + result);
// 输出: [4, 4, 6, 6, 8, 4] 中第三个4被丢弃,实际输出 [4, 4, 6, 6, 8]
}
}四、性能对比与方案选择建议
从性能角度看,Set去重和Map计数都属于哈希方案,整体时间复杂度为O(n),空间复杂度也是O(n)。对于百万级以内的数据量,两者都能在毫秒级完成,差异主要体现在常数开销上。如果数据本身是有序的,还可以用双指针法在原数组上完成去重或限频,空间复杂度降到O(1),这是面试和底层库中常见的优化手段。
选择方案时可以按这三条判断:只需要去重且要保序,优先用Stream.distinct()或LinkedHashSet,写法简洁不易出错;需要去重还要排序,用TreeSet或者去重后再排序;需要控制重复次数超过1次,必须走HashMap计数路线。另外提醒一点,如果处理的是超大数组且允许近似结果,还可以考虑外部工具库提供的布隆过滤器思路,它能用极小的内存判断元素是否可能出现过,适合去重场景的预筛选。
最后补充一个容易忽视的细节:对数组去重后如果需要回传,注意toArray时的类型问题,List<Integer>的toArray()返回Object数组,强转会抛ClassCastException,正确做法是使用带参数的toArray(new Integer[0])。掌握这些细节,数组元素重复次数的控制就再也不会成为你开发中的绊脚石了。
Java数组去重LinkedHashSetStream API修改时间:2026-09-15 18:04:40