できない.dev

配列から重複を除くには

配列(リスト)から重複した要素を取り除く基本形を各言語で示す。
出現順を保つ書き方と、順序を捨てて集合に変換する書き方の違いまでを扱う。

公開:

各言語見出しの横のバッジは検証状態を表す。実行確認済みはコードを実際に実行して確認したもの、静的確認は構文と公式 API ドキュメントで確認したものである。

Python 実行確認済み

nums = [3, 1, 3, 2, 1]
print(list(dict.fromkeys(nums)))  # 出現順を保つ → [3, 1, 2]
print(sorted(set(nums)))          # 順序が不要なら set → [1, 2, 3]
words = ["b", "a", "b"]
print(list(dict.fromkeys(words)))  # ['b', 'a']

dict はキーの挿入順を保つため、dict.fromkeys を経由すると出現順のまま重複を除ける。
順序が不要なら set が最短だが、set は順序を保証しないので並びが要るときは sorted と組み合わせる。

JavaScript 実行確認済み

const nums = [3, 1, 3, 2, 1];
console.log([...new Set(nums)]); // 挿入順のまま → [3, 1, 2]
 
// オブジェクトは参照で比較されるので、キーを決めて Map に詰め直す
const users = [{ id: 1, name: "a" }, { id: 2, name: "b" }, { id: 1, name: "c" }];
console.log([...new Map(users.map((u) => [u.id, u])).values()]);

Set は挿入順を保つので、スプレッドで配列に戻すだけで出現順のまま重複が消える。
オブジェクトは参照で比較され中身が同じでも別物と扱われるため、id などのキーで判定したいときは Map に詰め直す。
この書き方では同じキーが後勝ちになる点に注意する。

TypeScript 実行確認済み

const nums: number[] = [3, 1, 3, 2, 1];
const uniq: number[] = [...new Set(nums)];
console.log(uniq); // [3, 1, 2]
 
type User = { id: number; name: string };
const users: User[] = [{ id: 1, name: "a" }, { id: 2, name: "b" }, { id: 1, name: "c" }];
const byId: User[] = [...new Map(users.map((u) => [u.id, u])).values()];
console.log(byId.map((u) => u.id)); // [1, 2]

書き方は JavaScript と同じで、[...new Set(nums)] の結果に number[] の型が付くだけである。
Map を使う重複排除ではキーと値の型が推論されるので、キーに使うプロパティを取り違えたときにコンパイル時点で気付ける。

Go 静的確認

package main
 
import "fmt"
 
func main() {
	nums := []int{3, 1, 3, 2, 1}
	seen := make(map[int]struct{}, len(nums))
	uniq := make([]int, 0, len(nums))
	for _, n := range nums {
		if _, ok := seen[n]; ok {
			continue
		}
		seen[n] = struct{}{}
		uniq = append(uniq, n)
	}
	fmt.Println(uniq) // [3 1 2]
}

Go には出現順を保つ重複排除の標準関数が無いため、出現済みを map に記録しながら新しいスライスへ積み直す。
slices.Compact は隣り合った重複しか潰さないので、単独で使う前に並べ替えが要る。

Rust 静的確認

use std::collections::HashSet;
 
fn main() {
    let nums = vec![3, 1, 3, 2, 1];
    let mut seen = HashSet::new();
    // insert は新規のときだけ true を返すので、初出だけが残る
    let uniq: Vec<i32> = nums.into_iter().filter(|n| seen.insert(*n)).collect();
    println!("{:?}", uniq); // [3, 1, 2]
}

HashSet::insert は新規挿入のときだけ true を返すので、filter と組み合わせると初出だけが残り出現順を保てる。
Vec::dedup は連続した重複しか除去しないため、そちらを使うなら先に sort する必要がある。

Java 実行確認済み

import java.util.*;
 
public class Main {
    public static void main(String[] args) {
        List<Integer> nums = List.of(3, 1, 3, 2, 1);
        List<Integer> uniq = new ArrayList<>(new LinkedHashSet<>(nums));
        System.out.println(uniq); // [3, 1, 2]
    }
}

LinkedHashSet は挿入順を保つ集合なので、これを経由すると出現順のまま重複が消える。
順序が不要なら HashSet で十分だが、その場合は並びが保証されない。

C# 実行確認済み

using System;
using System.Linq;
 
class Program {
    static void Main() {
        int[] nums = { 3, 1, 3, 2, 1 };
        var uniq = nums.Distinct().ToArray(); // 列挙順を保つ
        Console.WriteLine(string.Join(",", uniq)); // 3,1,2
    }
}

LINQ の Distinct は列挙順を保ったまま重複を除き、元の配列は変更しない。
int や string は既定の等値比較で判定されるため、そのまま渡せば期待どおりに動く。

つまずき

重複排除の手段には、出現順を保つものと保たないものがある。
Python の set、Java の HashSet、Rust の HashSet は順序を保証しないため、表示順に意味がある場面でそのまま使うと並びが崩れる。
出現順を保ちたいなら Python は dict.fromkeys、Java は LinkedHashSet を選び、JavaScript の Set と C# の Distinct はもともと順序を保つのでそのまま使える。

連続した重複しか消えない関数

Go の slices.Compact、Rust の Vec::dedup、Unix の uniq は、隣り合った同じ値だけをまとめる関数である。
離れた位置にある重複は残るため、配列全体から重複を除きたいときは先に並べ替えるか、集合で出現済みを記録する方法に切り替える。
並べ替えを挟むと当然ながら元の順序は失われる。

要素がオブジェクトのとき

JavaScript のように既定の比較が参照の同一性で行われる言語では、中身が同じオブジェクトでも別インスタンスなら重複と見なされない。
id のような判定キーを決めて Map に詰め直すか、その言語の比較子(Java の equals / hashCode、C# の IEqualityComparer)を用意して初めて意図どおりに重複が消える。

この記事は役立ちましたか?