【Java數(shù)組去重的20種實現(xiàn)方式——指導(dǎo)AI解決不同問題的思路】

Java 數(shù)組去重的 20 種實現(xiàn)方式,理解不同解決問題的思路

數(shù)組與列表去重是最常見的算法。看似簡單,但不同實現(xiàn)方式的性能差異可能高達幾百倍。整理Java數(shù)組去重的20種寫法,按5個策略分類,幫你理解每類的核心思路。AI時代,可以不寫代碼,但需要理解不同解決問題的方式。

為什么性能差異這么大?

最簡單的寫法,新建數(shù)組,然后把不存在的添加進來。

static Integer[] unique(Integer[] arr) {
    List<Integer> result = new ArrayList<>();
    for (Integer item : arr) {
        // ArrayList.contains 是 O(n) 線性掃描,但每次掃描則是 O(n2)
        if (!result.contains(item)) {
            result.add(item);
        }
    }
    return result.toArray(new Integer[0]);
}

問題在于每次 contains 都要全量掃一遍 result,復(fù)雜度是 O(n2)。

優(yōu)化思路:用更快的方式判重

  • 集合結(jié)構(gòu) O(1) 查詢:HashSetLinkedHashSet
  • 排序 O(nlogn):相同元素相鄰后去重
  • 流式APIstream().distinct()
  • 位圖BitSet 對非負整數(shù)極其高效
  • 遞歸:換種表達方式,本質(zhì)仍是上面的思路

下面具體來分析5大類,20種不同的寫法

第1類:基礎(chǔ)循環(huán)(方法1-6)

策略原理:不依賴任何集合工具類,純靠數(shù)組下標、嵌套循環(huán)、indexOf 這種"原始"手段來完成去重。每一步判斷都是 O(n),整體復(fù)雜度是 O(n2)。

適用場景:教學(xué)、面試手撕、嵌入式或受限環(huán)境,生產(chǎn)代碼不建議使用。

%%{init: {'flowchart': {'nodeSpacing': 30, 'rankSpacing': 25, 'padding': 8}}}%%
graph LR
    A([原數(shù)組]) --> B[取下一個元素]
    B --> C{遍歷結(jié)果數(shù)組<br/>是否已存在?}
    C -->|否| D[追加到結(jié)果]
    C -->|是| E[跳過]
    D --> F([繼續(xù)])
    E --> F
    F --> B

    classDef start fill:#2E8B57,color:#fff,stroke:#1e5c3a,stroke-width:2px
    classDef step  fill:#3A86FF,color:#fff,stroke:#2b63c4,stroke-width:2px
    classDef check fill:#FFB703,color:#000,stroke:#cc8c00,stroke-width:2px
    class A,F start
    class B,D,E step
    class C check
// 方法1:雙循環(huán)索引比較——當前項跟左側(cè)逐個比較
static int[] unique1(int[] arr) {
    int[] newArr = new int[arr.length];
    int x = 0;
    // 當前項跟左側(cè)全量比較,是否首次出現(xiàn)
    for (int i = 0; i < arr.length; i++) {
        for (int j = 0; j <= i; j++) {
          // i 與左側(cè)每個 j 比對
            if (arr[i] == arr[j]) {
                // 值和位置均相同,表示前面沒有相同值,是首次出現(xiàn)
                if (i == j) {
                    newArr[x++] = arr[i];
                }
                break;
            }
        }
    }
    return Arrays.copyOf(newArr, x);
}

// 方法2:List.indexOf 索引法,跟第一種原理一致
// indexOf 返回首次出現(xiàn)的下標,等于當前下標即首次出現(xiàn)
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得到下標,比較是否跟當前下標一致,如果一致則表示首次出現(xiàn)
        if (list.indexOf(list.get(i)) == i) {
            result.add(list.get(i));
        }
    }
    return result.toArray(new Integer[0]);
}

// 方法3:從后往前原地刪除
// 倒序遍歷,與左側(cè)任意相同則刪除自身
// 倒序刪除的好處:刪除點之后的元素都已處理,不會影響下標
static Integer[] unique3(Integer[] arr) {
    List<Integer> list = new ArrayList<>(Arrays.asList(arr));
    int l = list.size();
    // 自后往前遍歷原數(shù)組
    while (l-- > 0) {
        int i = l;
        // 當前項與左側(cè)逐個比較是否存在重復(fù)項,若存在則刪除自身
        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();
    // 自前往后遍歷原數(shù)組
    for (int i = 0; i < l; i++) {
        // 當前項與右側(cè)全部項逐個比較,若有相同則刪除相同項
        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 風(fēng)格遍歷,結(jié)果列表用 contains 判重
static Integer[] unique5(Integer[] arr) {
    List<Integer> source = Arrays.asList(arr);
    List<Integer> result = new ArrayList<>();
    Iterator<Integer> it = source.iterator();
    // 逐個迭代,判斷是否包含在新數(shù)組中
    while (it.hasNext()) {
        Integer item = it.next();
        // ArrayList.contains 是 O(n),整體仍是 O(n2)
        if (!result.contains(item)) {
            result.add(item);
        }
    }
    return result.toArray(new Integer[0]);
}

// 方法6:從右往左跳過重復(fù),不刪除(使用break跳出)
// 倒序掃描,若當前元素在左側(cè)已存在則跳過,否則保留
static Integer[] unique6(Integer[] arr) {
    int len = arr.length;
    Integer[] result = new Integer[len];
    int x = len;
    // 自后往前遍歷數(shù)組
    for (int i = len - 1; i >= 0; i--) {
        boolean duplicate = false;
        // 檢查左側(cè)是否有相同元素
        for (int j = i - 1; j >= 0; j--) {
            if (arr[i].equals(arr[j])) {
                duplicate = true;
                break;   // 找到重復(fù)立即退出內(nèi)層循環(huán)
            }
        }
        // 沒有重復(fù)則保留當前元素,倒序填入結(jié)果
        if (!duplicate) {
            result[--x] = arr[i];
        }
    }
    return Arrays.copyOfRange(result, x, len);
}

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

策略原理:Java 集合框架本身就提供了"鍵唯一"的語義。把數(shù)據(jù)塞進 Set 或 Map,去重就自然完成。

  • HashSet / HashMap:哈希結(jié)構(gòu),O(1) 判重,結(jié)果無序
  • LinkedHashSet:哈希 + 雙向鏈表,保留插入順序
  • TreeSet:紅黑樹,O(logn),自動排序
  • LinkedHashMap:保序的 Map,能在去重時攜帶額外信息

代價是元素必須正確實現(xiàn) hashCodeequals。基本類型的包裝類、String 都已經(jīng)實現(xiàn)好了,自定義對象需要自己重寫。

適用場景:日常項目首選。需要保序選 LinkedHashSet,需要排序選 TreeSet,需要鍵值對選 LinkedHashMap。

%%{init: {'flowchart': {'nodeSpacing': 30, 'rankSpacing': 25, 'padding': 8}}}%%
graph LR
    A([原數(shù)組]) --> B[逐個加入 Set/Map]
    B --> C{已在容器中?<br/>哈希查詢}
    C -->|否| D[加入容器]
    C -->|是| E[自動忽略]
    D --> F([最后轉(zhuǎn)回 List])
    E --> F

    classDef start fill:#2E8B57,color:#fff,stroke:#1e5c3a,stroke-width:2px
    classDef step  fill:#8338EC,color:#fff,stroke:#5e27a8,stroke-width:2px
    classDef check fill:#FFB703,color:#000,stroke:#cc8c00,stroke-width:2px
    class A,F start
    class B,D,E step
    class C check
// 方法7:HashSet——最快,但結(jié)果無序
static Integer[] unique7(Integer[] arr) {
    // HashSet 自動去重,但底層哈希散列后順序不可預(yù)測
    Set<Integer> set = new HashSet<>(Arrays.asList(arr));
    return set.toArray(new Integer[0]);
}

// 方法8:LinkedHashSet——保序,工程首選
static Integer[] unique8(Integer[] arr) {
    // LinkedHashSet 內(nèi)部用雙向鏈表維護插入順序
    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,但更便于擴展(值可以放統(tǒng)計信息、首次位置等)
static Integer[] unique10(Integer[] arr) {
    Map<Integer, Integer> map = new HashMap<>();
    List<Integer> result = new ArrayList<>();
    for (Integer item : arr) {
        // putIfAbsent 內(nèi)部判 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 保留插入順序,鍵去重,值可攜帶統(tǒng)計
    Map<Integer, Integer> map = new LinkedHashMap<>();
    for (Integer item : arr) {
        // merge:鍵不存在則放 1,存在則累加(這里相當于做了頻次統(tǒng)計)
        map.merge(item, 1, Integer::sum);
    }
    return map.keySet().toArray(new Integer[0]);
}

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

策略原理:先 sort 讓相同元素相鄰,再掃一遍刪除相鄰相同項。復(fù)雜度由排序決定,O(nlogn)。優(yōu)點是不需要額外的哈希結(jié)構(gòu),"相鄰判等"是最便宜的判重方式;缺點是會破壞原順序,且要求元素可比較(實現(xiàn) Comparable 或提供 Comparator)。

適用場景:輸出本就需要排序、不在意原順序、內(nèi)存敏感(不想再開一個 Set)。

%%{init: {'flowchart': {'nodeSpacing': 30, 'rankSpacing': 25, 'padding': 8}}}%%
graph LR
    A([原數(shù)組]) --> B[排序<br/>相同元素相鄰]
    B --> C{相鄰是否相同?}
    C -->|是| D[刪后者]
    C -->|否| E[保留]
    D --> F([結(jié)果])
    E --> F

    classDef start fill:#2E8B57,color:#fff,stroke:#1e5c3a,stroke-width:2px
    classDef step  fill:#FF6B6B,color:#fff,stroke:#cc4444,stroke-width:2px
    classDef check fill:#FFB703,color:#000,stroke:#cc8c00,stroke-width:2px
    class A,F start
    class B,D,E step
    class C check
// 方法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原題)
// 先排序,再原地去重(修改原數(shù)組),最后返回去重后的新數(shù)組
static int[] unique14(int[] arr) {
    if (arr.length == 0) return arr;
    Arrays.sort(arr);
    int slow = 0; // 慢指針:指向去重后區(qū)間的末尾(初始為第一個)
    for (int fast = 1; fast < arr.length; fast++) {
        // 當前項與去重區(qū)間最后一個元素不同,說明有新的值
        if (arr[fast] != arr[slow]) {
            // 將慢指針右移一步,并將新值寫入到去重區(qū)間末尾
            arr[++slow] = arr[fast];
        }
    }
    // [0, slow] 是去重后的結(jié)果
    return Arrays.copyOf(arr, slow + 1);
}

第4類:Stream 與函數(shù)式(方法15-17)

策略原理:Java 8 引入的 Stream API 把"管道 + 操作"形式化。distinct() 內(nèi)部用 LinkedHashSet 實現(xiàn)去重;Collectors.toMap 提供按業(yè)務(wù)鍵去重的便捷工具;reduce 則可以把累加過程顯式表達出來。

適用場景:現(xiàn)代 Java 工程的常態(tài)寫法。可讀性高、鏈式組合方便,特別適合"先過濾再去重再排序再收集"的多步處理。

%%{init: {'flowchart': {'nodeSpacing': 30, 'rankSpacing': 25, 'padding': 8}}}%%
graph LR
    A([原數(shù)組]) --> B[Stream 管道]
    B --> C{選擇算子}
    C -->|distinct| D[內(nèi)部 LinkedHashSet<br/>去重]
    C -->|filter+seen| E[外部狀態(tài)<br/>判重過濾]
    C -->|toMap| F[按 key 去重<br/>合并沖突值]
    D --> G([collect 收集])
    E --> G
    F --> G

    classDef start fill:#2E8B57,color:#fff,stroke:#1e5c3a,stroke-width:2px
    classDef step  fill:#0F3460,color:#fff,stroke:#0a2647,stroke-width:2px
    classDef check fill:#FFB703,color:#000,stroke:#cc8c00,stroke-width:2px
    class A,G start
    class B,D,E,F step
    class C check
// 方法15:基于 Stream.distinct() 一行去重
static Integer[] unique15(Integer[] arr) {
    // distinct 內(nèi)部基于 LinkedHashSet,保序、O(n)、寫法最短
    return Arrays.stream(arr)
            .distinct()
            .toArray(Integer[]::new);
}

// 方法16:通過 Stream.filter + 外部 Set
// 演示如何在函數(shù)式管道里攜帶"已出現(xiàn)"的狀態(tài)
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 按業(yè)務(wù)鍵去重
// 適合自定義對象按某字段去重的場景
static Integer[] unique17(Integer[] arr) {
    // 第三個參數(shù)是沖突合并函數(shù):(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)

策略原理:遞歸用自調(diào)用替代循環(huán),是函數(shù)式思維的體現(xiàn),主要用于教學(xué)與算法練習(xí);位圖(BitSet)用每一位標記一個非負整數(shù)是否出現(xiàn)過,對整數(shù)集合有極致的空間效率(10 億個 int 只要 128MB),是大數(shù)據(jù)去重的常見選型。

適用場景:遞歸教學(xué)、算法刷題;BitSet——大規(guī)模非負整數(shù)(如用戶 ID、訂單號)的去重統(tǒng)計。

%%{init: {'flowchart': {'nodeSpacing': 30, 'rankSpacing': 25, 'padding': 8}}}%%
graph LR
    A([列表 length=n]) --> B{length <= 1?}
    B -->|是| C([返回])
    B -->|否| D[檢查末尾元素<br/>是否在前面出現(xiàn)]
    D --> E{重復(fù)?}
    E -->|是| F[丟棄末尾]
    E -->|否| G[保留末尾]
    F --> H[遞歸處理 length-1]
    G --> H
    H --> A

    classDef start fill:#2E8B57,color:#fff,stroke:#1e5c3a,stroke-width:2px
    classDef step  fill:#118AB2,color:#fff,stroke:#0b5f7a,stroke-width:2px
    classDef check fill:#FFB703,color:#000,stroke:#cc8c00,stroke-width:2px
    class A,C start
    class D,F,G,H step
    class B,E check
// 方法18:遞歸--從后往前構(gòu)建去重列表,跟方法1類似,也是新建數(shù)組獲得不重復(fù)項
// 遞歸檢查末尾元素是否在前面出現(xiàn)過,若不重復(fù)則插入到結(jié)果列表頭部,最終保持原順序
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) 區(qū)間找是否已出現(xiàn)過 lastItem
    for (int i = last - 1; i >= 0; i--) {
        if (lastItem.equals(arr[i])) {
            isRepeat = true;
            break;
        }
    }
    // 不重復(fù)則把 lastItem 加入結(jié)果(往前插入以保持原順序)
    if (!isRepeat) {
        result.add(0, lastItem);
    }
    return uniqueRecursive(arr, length - 1, result);
}

// 方法19:遞歸--拼接返回(純函數(shù)式,不修改原數(shù)組和外部集合)
// 核心思路:先遞歸處理前 n-1 個元素得到去重結(jié)果,再判斷第 n 個元素是否在它前面出現(xiàn)過。
//         若未出現(xiàn)過,則追加到結(jié)果末尾;否則直接返回。最終保持原數(shù)組的首次出現(xiàn)順序。
static List<Integer> uniqueRecursiveConcat(List<Integer> list, int length) {
    if (length <= 1) {
        // 長度≤1時,直接返回原列表的前 length 個元素(避免后續(xù)越界)
        return new ArrayList<>(list.subList(0, length));
    }

    int last = length - 1;
    Integer lastItem = list.get(last);
    boolean isRepeat = false;
    // 檢查當前項 lastItem 是否在它前面的子數(shù)組中出現(xiàn)過
    for (int i = last - 1; i >= 0; i--) {
        if (lastItem.equals(list.get(i))) {
            isRepeat = true;
            break;
        }
    }

    // 遞歸深入:此時尚未得到前 length-1 項的去重結(jié)果,暫停當前層,進入子問題求解
    List<Integer> head = uniqueRecursiveConcat(list, length - 1);
    // 遞歸回溯:子問題已返回結(jié)果(head 中已包含前 length-1 項的去重結(jié)果,且保序)
  
    // 當前項不重復(fù)則追加到結(jié)果末尾(保持原順序)
    if (!isRepeat) {
        head.add(lastItem);
    }
    return head;
}

// 方法20:BitSet 位圖法(僅適用于非負整數(shù))
// BitSet 是用一個 long[] 數(shù)組來模擬一個無限長的位序列。(0 表示 false/未出現(xiàn),1 表示 true/已出現(xiàn))
// 每個 int 用一位標記是否出現(xiàn)過,10 億規(guī)模也只要 ~128MB
static int[] uniqueBitSet(int[] arr) {
    BitSet bitSet = new BitSet();
    int[] result = new int[arr.length];
    int x = 0;
    for (int item : arr) {
        // 注意 BitSet 的下標必須非負,負數(shù)需要先做偏移
        if (item < 0) {
            throw new IllegalArgumentException("BitSet 不支持負數(shù),需要先偏移");
        }
        // 第 item 位為 0 表示首次出現(xiàn)
        if (!bitSet.get(item)) {
            bitSet.set(item); // 標記為已出現(xiàn)
            result[x++] = item;
        }
    }
    return Arrays.copyOf(result, x);
}

這么多寫法該如何選擇?

%%{init: {'flowchart': {'nodeSpacing': 25, 'rankSpacing': 15, 'padding': 5}}}%%
graph TD
    Start(["數(shù)組/列表去重"]) --> Need{"是否需要保序?"}

    Need -->|不需要| Fast["看數(shù)據(jù)特征"]
    Need -->|需要| Ordered["保留原順序"]

    Fast --> Q1{"數(shù)據(jù)規(guī)模與類型"}
    Q1 -->|大量非負整數(shù)| BitSet["BitSet<br/>極致空間效率"]
    Q1 -->|順便要排序| TreeSet["TreeSet<br/>插入即有序"]
    Q1 -->|一般規(guī)模| HashSet["HashSet<br/>O(n) 最快"]

    Ordered --> Q2{"側(cè)重點"}
    Q2 -->|代碼簡潔| Stream["stream().distinct()<br/>一行解決"]
    Q2 -->|工程清晰| LinkedHashSet["LinkedHashSet<br/>顯式語義"]
    Q2 -->|按字段去重| ToMap["Collectors.toMap<br/>攜帶業(yè)務(wù)鍵"]

    classDef start fill:#2E8B57,color:#fff,stroke:#1e5c3a
    classDef decision fill:#FE8B57,color:#fff,stroke:#141b2d
    classDef fast fill:#3A86FF,color:#fff,stroke:#2b63c4
    classDef ordered fill:#8338EC,color:#fff,stroke:#5e27a8
    classDef method fill:#0f3460,color:#fff,stroke:#0a2647

    class Start start
    class Need,Q1,Q2 decision
    class Fast fast
    class Ordered ordered
    class BitSet,TreeSet,HashSet,Stream,LinkedHashSet,ToMap method
類別 時間復(fù)雜度 是否保序 主要場景
基礎(chǔ)循環(huán) O(n2) 學(xué)習(xí)算法、理解編程
集合容器 O(n) 看具體類 日常項目首選
排序后去重 O(nlogn) 否(變排序) 順便要排序
Stream O(n) 是(distinct) 現(xiàn)代 Java 寫法
遞歸 / 位圖 視實現(xiàn) 看實現(xiàn) 教學(xué) / 海量整數(shù)

性能實測

10 萬個隨機整數(shù)(約 5 萬不重復(fù)):

方法 耗時
HashSet 8 ms
LinkedHashSet 10 ms
stream().distinct() 12 ms
BitSet 4 ms
排序+相鄰去重 35 ms
雙循環(huán) 1500 ms
List.contains 循環(huán) 1400 ms

100 萬個數(shù)據(jù)時差距進一步拉開:

方法 耗時 相對 HashSet
HashSet 80 ms
LinkedHashSet 110 ms 1.4×
BitSet 30 ms 0.4×
雙循環(huán) 估算 150 秒 約 1900×

數(shù)據(jù)從 10 萬到 100 萬:O(n) 方法慢 10 倍左右,O(n2) 方法慢 100 倍以上。算法選擇的影響在數(shù)據(jù)變大時呈非線性放大。


實際項目里怎么選

絕大多數(shù)情況一行就夠,你不需要記住具體的寫法,但你需要告訴AI該怎么選:

// 保序、O(n)、寫法最短,工程首選
List<Integer> result = list.stream().distinct().collect(Collectors.toList());

// 或顯式用 LinkedHashSet,語義更清晰
List<Integer> result = new ArrayList<>(new LinkedHashSet<>(list));

不在意順序:

List<Integer> result = new ArrayList<>(new HashSet<>(list));

需要排序:

List<Integer> result = new ArrayList<>(new TreeSet<>(list));

海量非負整數(shù):

BitSet bitSet = new BitSet();
for (int x : data) bitSet.set(x);
// bitSet.cardinality() 即不重復(fù)元素個數(shù)

帶業(yè)務(wù)邏輯的去重

實際工作里經(jīng)常遇到這樣的情況:遇到重復(fù)時不能簡單丟棄,要按某個規(guī)則做處理。比如:

  • id 去重,但要保留分數(shù)最高的那條記錄
  • 去重的同時累加重復(fù)次數(shù)
  • 數(shù)值在某個區(qū)間內(nèi)才參與去重

這類需求 Set 直接搞不定,需要把"判重"和"處理"兩步拆開來寫。Java 里通常用 LinkedHashMap + 合并函數(shù):

/**
 * 帶業(yè)務(wù)規(guī)則的去重。
 *
 * @param data         原數(shù)據(jù)
 * @param keyFn        從元素提取去重鍵
 * @param onDuplicate  遇到重復(fù)時如何合并 (舊值, 新值) -> 新代表值
 */
static <T, K> List<T> uniqueBy(List<T> data,
                               Function<T, K> keyFn,
                               BinaryOperator<T> onDuplicate) {
    // LinkedHashMap 保證遍歷順序與首次出現(xiàn)順序一致
    Map<K, T> chosen = new LinkedHashMap<>();
    for (T item : data) {
        K key = keyFn.apply(item);
        // merge:鍵不存在則直接放入,存在則用 onDuplicate 決定保留誰
        chosen.merge(key, item, onDuplicate);
    }
    return new ArrayList<>(chosen.values());
}

例 1:按 id 去重,保留分數(shù)最高的:

class Student {
    int id;
    String name;
    int score;
    // 構(gòu)造器、getter 略
}

List<Student> students = List.of(
        new Student(1, "張三", 90),
        new Student(1, "張三", 95),   // 同 id,分數(shù)更高
        new Student(2, "李四", 85)
);

List<Student> result = uniqueBy(
        students,
        Student::getId,
        // 重復(fù)時保留分數(shù)高的那條
        (old, news) -> news.getScore() > old.getScore() ? news : old
);
// 結(jié)果只剩 [{id:1,score:95}, {id:2,score:85}]

例 2:去重同時統(tǒng)計頻次(Stream 的標準做法):

// groupingBy + counting 是 Java 里的"頻次統(tǒng)計"慣用法
Map<String, Long> counts = data.stream()
        .collect(Collectors.groupingBy(s -> s, LinkedHashMap::new, Collectors.counting()));
// counts.keySet() 即保序的去重結(jié)果

例 3:區(qū)間過濾——只對 [0, 100] 區(qū)間內(nèi)的值去重,區(qū)間外原樣保留:

Set<Integer> seen = new HashSet<>();
List<Integer> result = new ArrayList<>();
for (int x : data) {
    if (x >= 0 && x <= 100) {
        // 區(qū)間內(nèi)才參與去重
        if (seen.add(x)) result.add(x);
    } else {
        // 區(qū)間外原樣保留
        result.add(x);
    }
}

這三個例子是同一種思路:把判重與業(yè)務(wù)規(guī)則分開。判重用哈希結(jié)構(gòu)保證 O(n),規(guī)則部分留給回調(diào)或顯式分支處理,這樣既不丟性能,又能容納各種業(yè)務(wù)變化。


自定義對象去重:equals 與 hashCode

Java 集合判重靠兩個方法:hashCode 決定散列到哪個桶,equals 決定桶內(nèi)是否真的相等。兩個都必須正確實現(xiàn),否則 HashSet 的去重會失效——同一對象塞進去兩次都不報錯。

class User {
    private final long id;
    private final String name;

    public User(long id, String name) {
        this.id = id;
        this.name = name;
    }

    // equals:業(yè)務(wù)上認為 id 相同就是同一個用戶
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof User)) return false;
        return id == ((User) o).id;
    }

    // hashCode:必須與 equals 一致——equals 相等的對象 hashCode 必須相等
    @Override
    public int hashCode() {
        return Long.hashCode(id);
    }
}

// 現(xiàn)在塞進 HashSet 就會按 id 去重
Set<User> uniqueUsers = new HashSet<>(users);

重點:重寫 equals 必須同時重寫 hashCode?,F(xiàn)代IDE 都能一鍵生成,但前提是你知道用哪個字段做"業(yè)務(wù)相等"。如果懶得寫,用 Collectors.toMap(keyFn, ...) 按某個字段去重也能繞過這個問題。


總結(jié)

工程上的快捷選擇:

  • 默認用 list.stream().distinct().collect(toList()):保序、一行、O(n)
  • 顯式語義用 new ArrayList<>(new LinkedHashSet<>(list)):意圖更清晰
  • 不要順序用 new HashSet<>(list)
  • 順便排序用 new TreeSet<>(list)
  • 海量非負整數(shù)用 BitSet
  • 自定義對象先實現(xiàn) equals 和 hashCode,或用 Collectors.toMap
  • 業(yè)務(wù)規(guī)則干預(yù)用 LinkedHashMap.merge

核心思路:

  1. 同一個問題可以從多個角度切入
  2. 選對數(shù)據(jù)結(jié)構(gòu)往往比寫更聰明的代碼更重要
  3. O(n2) 與 O(n) 在數(shù)據(jù)變大時是幾百倍的實際差距
  4. 不要過度優(yōu)化——能用 distinct 就別繞彎
  5. 遇到新問題先寫最直觀的版本,再按瓶頸逐步優(yōu)化

AI 時代,程序員不一定要手寫代碼,但一定要懂得編程思路,這樣才能更好地指導(dǎo) AI。

更多算法

不同語言算法實現(xiàn):https://github.com/microwind/algorithms

AI編程核心知識庫:https://microwind.github.io

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容