如果想在 V2EX 获得更好的推广效果,欢迎了解 PRO 会员机制:
https://www.v2ex.com/pro/about

如果你经常使用铜币置顶主题,持有 V2EX Solana Token 会在每日签到时获得额外铜币:
https://www.v2ex.com/solana
jarryli
V2EX  ›  推广

Java 数组去重的 20 种实现方式,用不同思路解决问题,更好地指导 AI 编程

  •  
  •   jarryli · 19 days ago · 312 views

    Java 数组去重的 20 种实现方式,用不同思路解决问题

    数组与列表去重是最常见的算法。看似简单,但不同实现方式的性能差异可能高达几百倍。整理 Java 数组去重的 20 种写法,按 5 个策略分类,帮你理解每类的核心思路。AI 时代,可以不写代码,但需要理解不同解决问题的方式。

    第 1 类:基础循环(方法 1-6 )

    策略原理:不依赖任何集合工具类,纯靠数组下标、嵌套循环、indexOf 这种"原始"手段来完成去重。每一步判断都是 O(n),整体复杂度是 O(n²)。

    // 方法 1:双循环索引比较——当前项跟左侧逐个比较
    static int[] unique1(int[] arr) {
        int[] newArr = new int[arr.length];
        int x = 0;
        // 当前项跟左侧全量比较,是否首次出现
        for (int i = 0; i < arr.length; i++) {
            for (int j = 0; j <= i; j++) {
              // i 与左侧每个 j 比对
                if (arr[i] == arr[j]) {
                    // 值和位置均相同,表示前面没有相同值,是首次出现
                    if (i == j) {
                        newArr[x++] = arr[i];
                    }
                    break;
                }
            }
        }
        return Arrays.copyOf(newArr, x);
    }
    
    // 方法 2:List.indexOf 索引法,跟第一种原理一致
    // indexOf 返回首次出现的下标,等于当前下标即首次出现
    static Integer[] unique2(Integer[] arr) {
        List<Integer> list = new ArrayList<>(Arrays.asList(arr));
        List<Integer> result = new ArrayList<>();
        for (int i = 0; i < list.size(); i++) {
            // indexOf 得到下标,比较是否跟当前下标一致,如果一致则表示首次出现
            if (list.indexOf(list.get(i)) == i) {
                result.add(list.get(i));
            }
        }
        return result.toArray(new Integer[0]);
    }
    
    // 方法 3:从后往前原地删除
    // 倒序遍历,与左侧任意相同则删除自身
    // 倒序删除的好处:删除点之后的元素都已处理,不会影响下标
    static Integer[] unique3(Integer[] arr) {
        List<Integer> list = new ArrayList<>(Arrays.asList(arr));
        int l = list.size();
        // 自后往前遍历原数组
        while (l-- > 0) {
            int i = l;
            // 当前项与左侧逐个比较是否存在重复项,若存在则删除自身
            while (i-- > 0) {
                if (list.get(l).equals(list.get(i))) {
                    list.remove(l);
                    break;
                }
            }
        }
        return list.toArray(new Integer[0]);
    }
    
    // 方法 4:从前往后原地删除(删除后面相同项),与方法 3 相同
    static Integer[] unique4(Integer[] arr) {
        List<Integer> list = new ArrayList<>(Arrays.asList(arr));
        int l = list.size();
        // 自前往后遍历原数组
        for (int i = 0; i < l; i++) {
            // 当前项与右侧全部项逐个比较,若有相同则删除相同项
            for (int j = i + 1; j < l; j++) {
                if (list.get(i).equals(list.get(j))) {
                    list.remove(j);
                    // 删除后 j 位置的元素是原来的 j+1 ,j 不前进
                    // l 同步减一
                    j--;
                    l--;
                }
            }
        }
        return list.toArray(new Integer[0]);
    }
    
    // 方法 5:Iterator 遍历,原理与方法 1 同
    // 用 Iterator 风格遍历,结果列表用 contains 判重
    static Integer[] unique5(Integer[] arr) {
        List<Integer> source = Arrays.asList(arr);
        List<Integer> result = new ArrayList<>();
        Iterator<Integer> it = source.iterator();
        // 逐个迭代,判断是否包含在新数组中
        while (it.hasNext()) {
            Integer item = it.next();
            // ArrayList.contains 是 O(n),整体仍是 O(n²)
            if (!result.contains(item)) {
                result.add(item);
            }
        }
        return result.toArray(new Integer[0]);
    }
    
    // 方法 6:从右往左跳过重复,不删除(使用 break 跳出)
    // 倒序扫描,若当前元素在左侧已存在则跳过,否则保留
    static Integer[] unique6(Integer[] arr) {
        int len = arr.length;
        Integer[] result = new Integer[len];
        int x = len;
        // 自后往前遍历数组
        for (int i = len - 1; i >= 0; i--) {
            boolean duplicate = false;
            // 检查左侧是否有相同元素
            for (int j = i - 1; j >= 0; j--) {
                if (arr[i].equals(arr[j])) {
                    duplicate = true;
                    break;   // 找到重复立即退出内层循环
                }
            }
            // 没有重复则保留当前元素,倒序填入结果
            if (!duplicate) {
                result[--x] = arr[i];
            }
        }
        return Arrays.copyOfRange(result, x, len);
    }
    

    第 2 类:集合容器(方法 7-11 )

    策略原理:Java 集合框架本身就提供了"键唯一"的语义。把数据塞进 Set 或 Map ,去重就自然完成。

    • HashSet / HashMap:哈希结构,O(1) 判重,结果无序
    • LinkedHashSet:哈希 + 双向链表,保留插入顺序
    • TreeSet:红黑树,O(logn),自动排序
    • LinkedHashMap:保序的 Map ,能在去重时携带额外信息
    // 方法 7:HashSet——最快,但结果无序
    static Integer[] unique7(Integer[] arr) {
        // HashSet 自动去重,但底层哈希散列后顺序不可预测
        Set<Integer> set = new HashSet<>(Arrays.asList(arr));
        return set.toArray(new Integer[0]);
    }
    
    // 方法 8:LinkedHashSet——保序,工程首选
    static Integer[] unique8(Integer[] arr) {
        // LinkedHashSet 内部用双向链表维护插入顺序
        Set<Integer> set = new LinkedHashSet<>(Arrays.asList(arr));
        return set.toArray(new Integer[0]);
    }
    
    // 方法 9:TreeSet——自动排序
    static Integer[] unique9(Integer[] arr) {
        // TreeSet 基于红黑树,插入即有序(默认升序)
        // 需要降序就用 .descendingSet() 或自定义 Comparator
        Set<Integer> set = new TreeSet<>(Arrays.asList(arr));
        return set.toArray(new Integer[0]);
    }
    
    // 方法 10:HashMap 显式判重
    // 等价于方法 7 ,但更便于扩展(值可以放统计信息、首次位置等)
    static Integer[] unique10(Integer[] arr) {
        Map<Integer, Integer> map = new HashMap<>();
        List<Integer> result = new ArrayList<>();
        for (Integer item : arr) {
            // putIfAbsent 内部判 containsKey + put ,等价但更紧凑
            if (map.putIfAbsent(item, item) == null) {
                result.add(item);
            }
        }
        return result.toArray(new Integer[0]);
    }
    
    // 方法 11:LinkedHashMap——保序的 Map
    // 适合"按某字段去重并携带其他信息"的场景
    static Integer[] unique11(Integer[] arr) {
        // 用 LinkedHashMap 保留插入顺序,键去重,值可携带统计
        Map<Integer, Integer> map = new LinkedHashMap<>();
        for (Integer item : arr) {
            // merge:键不存在则放 1 ,存在则累加(这里相当于做了频次统计)
            map.merge(item, 1, Integer::sum);
        }
        return map.keySet().toArray(new Integer[0]);
    }
    

    第 3 类:排序后去重(方法 12-14 )

    策略原理:先 sort 让相同元素相邻,再扫一遍删除相邻相同项。复杂度由排序决定,O(nlogn)。优点是不需要额外的哈希结构,"相邻判等"是最便宜的判重方式;缺点是会破坏原顺序,且要求元素可比较(实现 Comparable 或提供 Comparator)。

    // 方法 12:排序后从后往前删
    static Integer[] unique12(Integer[] arr) {
        List<Integer> list = new ArrayList<>(Arrays.asList(arr));
        Collections.sort(list); // 升序,相同元素聚到一起
        // 倒序扫描,自后往前:相邻两项相同就删掉后一项
        for (int l = list.size() - 1; l > 0; l--) {
            if (list.get(l).equals(list.get(l - 1))) {
                list.remove(l);
            }
        }
        return list.toArray(new Integer[0]);
    }
    
    // 方法 13:排序后从前往后删
    static Integer[] unique13(Integer[] arr) {
        List<Integer> list = new ArrayList<>(Arrays.asList(arr));
        Collections.sort(list, Collections.reverseOrder()); // 降序
        int l = list.size() - 1;
        int i = 0;
        while (i < l) {
            if (list.get(i).equals(list.get(i + 1))) {
                list.remove(i);
                // 删完不前进,长度同步减一
                i--;
                l--;
            }
            i++;
        }
        return list.toArray(new Integer[0]);
    }
    
    // 方法 14:排序 + 双指针(类似 LeetCode 原题)
    // 先排序,再原地去重(修改原数组),最后返回去重后的新数组
    static int[] unique14(int[] arr) {
        if (arr.length == 0) return arr;
        Arrays.sort(arr);
        int slow = 0; // 慢指针:指向去重后区间的末尾(初始为第一个)
        for (int fast = 1; fast < arr.length; fast++) {
            // 当前项与去重区间最后一个元素不同,说明有新的值
            if (arr[fast] != arr[slow]) {
                // 将慢指针右移一步,并将新值写入到去重区间末尾
                arr[++slow] = arr[fast];
            }
        }
        // [0, slow] 是去重后的结果
        return Arrays.copyOf(arr, slow + 1);
    }
    

    第 4 类:Stream 与函数式(方法 15-17 )

    策略原理:Java 8 引入的 Stream API 把"管道 + 操作"形式化。distinct() 内部用 LinkedHashSet 实现去重;Collectors.toMap 提供按业务键去重的便捷工具;reduce 则可以把累加过程显式表达出来。

    // 方法 15:基于 Stream.distinct() 一行去重
    static Integer[] unique15(Integer[] arr) {
        // distinct 内部基于 LinkedHashSet ,保序、O(n)、写法最短
        return Arrays.stream(arr)
                .distinct()
                .toArray(Integer[]::new);
    }
    
    // 方法 16:通过 Stream.filter + 外部 Set
    // 演示如何在函数式管道里携带"已出现"的状态
    static Integer[] unique16(Integer[] arr) {
        Set<Integer> seen = new HashSet<>();
        // filter 谓词带副作用:seen.add 返回 true 表示首次加入
        // 整个表达式语义:仅保留首次见到的元素
        return Arrays.stream(arr)
                .filter(seen::add)
                .toArray(Integer[]::new);
    }
    
    // 方法 17:通过 Collectors.toMap 按业务键去重
    // 适合自定义对象按某字段去重的场景
    static Integer[] unique17(Integer[] arr) {
        // 第三个参数是冲突合并函数:(existing, replacement) -> existing 保留首项
        // 用 LinkedHashMap::new 作为 Map 工厂,保留插入顺序
        Map<Integer, Integer> map = Arrays.stream(arr).collect(
                Collectors.toMap(
                        item -> item,                  // keyMapper:用值本身做键
                        item -> item,                  // valueMapper
                        (existing, replacement) -> existing,
                        LinkedHashMap::new
                )
        );
        return map.values().toArray(new Integer[0]);
    }
    

    第 5 类:递归与位图(方法 18-20 )

    策略原理:递归用自调用替代循环,是函数式思维的体现,主要用于教学与算法练习;位图(BitSet)用每一位标记一个非负整数是否出现过,对整数集合有极致的空间效率( 10 亿个 int 只要 128MB ),是大数据去重的常见选型。

    // 方法 18:递归--从后往前构建去重列表,跟方法 1 类似,也是新建数组获得不重复项
    // 递归检查末尾元素是否在前面出现过,若不重复则插入到结果列表头部,最终保持原顺序
    static Integer[] uniqueRecursive(Integer[] arr, int length, List<Integer> result) {
        // 递归终止:只剩一项,直接收尾
        if (length <= 1) {
            if (length == 1) result.add(0, arr[0]);
            return result.toArray(new Integer[0]);
        }
    
        int last = length - 1;
        Integer lastItem = arr[last];
        boolean isRepeat = false;
        // 在 [0, last) 区间找是否已出现过 lastItem
        for (int i = last - 1; i >= 0; i--) {
            if (lastItem.equals(arr[i])) {
                isRepeat = true;
                break;
            }
        }
        // 不重复则把 lastItem 加入结果(往前插入以保持原顺序)
        if (!isRepeat) {
            result.add(0, lastItem);
        }
        return uniqueRecursive(arr, length - 1, result);
    }
    
    // 方法 19:递归--拼接返回(纯函数式,不修改原数组和外部集合)
    // 核心思路:先递归处理前 n-1 个元素得到去重结果,再判断第 n 个元素是否在它前面出现过。
    //         若未出现过,则追加到结果末尾;否则直接返回。最终保持原数组的首次出现顺序。
    static List<Integer> uniqueRecursiveConcat(List<Integer> list, int length) {
        if (length <= 1) {
            // 长度≤1 时,直接返回原列表的前 length 个元素(避免后续越界)
            return new ArrayList<>(list.subList(0, length));
        }
    
        int last = length - 1;
        Integer lastItem = list.get(last);
        boolean isRepeat = false;
        // 检查当前项 lastItem 是否在它前面的子数组中出现过
        for (int i = last - 1; i >= 0; i--) {
            if (lastItem.equals(list.get(i))) {
                isRepeat = true;
                break;
            }
        }
    
        // 递归深入:此时尚未得到前 length-1 项的去重结果,暂停当前层,进入子问题求解
        List<Integer> head = uniqueRecursiveConcat(list, length - 1);
        // 递归回溯:子问题已返回结果( head 中已包含前 length-1 项的去重结果,且保序)
      
        // 当前项不重复则追加到结果末尾(保持原顺序)
        if (!isRepeat) {
            head.add(lastItem);
        }
        return head;
    }
    
    // 方法 20:BitSet 位图法(仅适用于非负整数)
    // BitSet 是用一个 long[] 数组来模拟一个无限长的位序列。( 0 表示 false/未出现,1 表示 true/已出现)
    // 每个 int 用一位标记是否出现过,10 亿规模也只要 ~128MB
    static int[] uniqueBitSet(int[] arr) {
        BitSet bitSet = new BitSet();
        int[] result = new int[arr.length];
        int x = 0;
        for (int item : arr) {
            // 注意 BitSet 的下标必须非负,负数需要先做偏移
            if (item < 0) {
                throw new IllegalArgumentException("BitSet 不支持负数,需要先偏移");
            }
            // 第 item 位为 0 表示首次出现
            if (!bitSet.get(item)) {
                bitSet.set(item); // 标记为已出现
                result[x++] = item;
            }
        }
        return Arrays.copyOf(result, x);
    }
    

    实际项目里怎么选?

    详细请见原文: https://github.com/microwind/algorithms/blob/main/array/unique/Java%E6%95%B0%E7%BB%84%E5%8E%BB%E9%87%8D%E7%9A%8420%E7%A7%8D%E6%96%B9%E5%BC%8F.md

    AI 时代,程序员不一定要手写代码,但一定要懂得编程思路,这样才能更好地指导 AI 。

    更多算法

    No Comments Yet
    About   ·   Help   ·   Advertise   ·   Blog   ·   API   ·   FAQ   ·   Solana   ·   3328 Online   Highest 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 47ms · UTC 10:47 · PVG 18:47 · LAX 03:47 · JFK 06:47
    ♥ Do have faith in what you're doing.