Rust入門

【Rust入門】HashSet 型の基本について分かりやすく解説

【Rust入門】HashSet 型の基本について分かりやすく解説
naoki-hn

Rust で重複しない要素の集合を管理するための HashSet 型の基本を初心者にも分かりやすく解説します。

HashSet<T>

Rust には、同じ型のデータをまとめて管理するための「コレクション(Collections)」が用意されています。コレクションは、実行中に要素数を変更できる柔軟なデータ構造です。

この記事では、コレクション型の代表例である HashSet<T> 型について解説します。

HashSet<T> 型とは

HashSet<T>は重複しない値の集合を管理するためのコレクション型です。

HashSet には、次のような特徴があります。

  • 重複した要素を保持できない
  • 要素の順序は保証されない
  • 要素の検索が高速

例えば、「ユーザーIDの重複チェック」「タグ一覧の管理」「特定の値の存在確認」などの目的のためにデータの集合として利用できます。

その他の代表的なコレクションである VecHashMap については、以下を参考にしてください。

HashSet の生成方法

HashSet を使う準備

HashSet は標準ライブラリの std::collections モジュールで提供されています。利用するには以下のようにインポートします。

use std::collections::HashSet;

以降で紹介する各プログラムでも先頭でインポートしています。

関連関数 HashSet::new() を使用する

HashSet を生成する最も基本的な方法は、new メソッドを使用する方法です。値を追加するには、insert メソッドを使用します。

use std::collections::HashSet;

fn main() {
    // HashSet を生成する
    let mut set = HashSet::new();

    // 値を追加する
    set.insert(String::from("Alice"));
    set.insert(String::from("Bob"));
    set.insert(String::from("Charlie"));

    println!("{:?}", set);
}
【実行結果】
{"Alice", "Charlie", "Bob"}

なお、HashSet は要素の追加順序を保証しません。そのため、実行により表示順序が異なる場合があります。以降の例についても同様です。

配列から HashSet を生成する

HashSet は配列から生成することも可能です。into_iter().collect() を使うことで配列から HashSet を生成できます。

use std::collections::HashSet;

fn main() {
    // 配列から HashSet を生成する
    let set: HashSet<_> = [1, 2, 3, 4].into_iter().collect();

    println!("生成された HashSet : {set:?}");
}
【実行結果】
生成された HashSet : {3, 1, 4, 2}

Vec から HashSet を生成する

配列同様に Vec から HashSet を生成することもできます。なお、重複した値が Vec に含まれている場合は、自動的に除かれます。

use std::collections::HashSet;

fn main() {
    let numbers = vec![1, 2, 2, 3, 3, 4];

    // Vec から HashSet を生成する(重複は自動で除かれる)
    let set: HashSet<_> = numbers.into_iter().collect();

    println!("元の Vec : [1, 2, 2, 3, 3, 4]");
    println!("生成された HashSet : {set:?}");
}
【実行結果】
元の Vec : [1, 2, 2, 3, 3, 4]
生成された HashSet : {1, 2, 4, 3}

上記のように、重複していた 23 は除かれてユニークな要素のみ保持できていることが確認できます。

【参考】HashSet から Vec に変換する

逆に HashSet から Vec に変換することも可能です。HashSet は順序を持たないため、必要に応じて sort で並び替えることもできます。

use std::collections::HashSet;

fn main() {
    let set: HashSet<_> = [3, 1, 4, 2].into_iter().collect();

    // HashSet から Vec に変換する
    let mut numbers: Vec<_> = set.into_iter().collect();

    // HashSet は順序を持たないため、見やすいように並べ替える
    numbers.sort();

    println!("変換後の Vec : {numbers:?}");
}
【実行結果】
変換後の Vec : [1, 2, 3, 4]

HashSet の操作(要素の取得・追加・削除など)

HashSet 型では、要素の取得・追加・削除などの各種メソッドが用意されています。

要素の取得

HashSet の要素値を取得するには、get メソッドにより取得する値を指定します。

use std::collections::HashSet;

fn main() {
    let mut names = HashSet::new();

    names.insert(String::from("Alice"));
    names.insert(String::from("Bob"));

    // 要素を取得する
    let alice = names.get("Alice");
    match alice {
        Some(name) => println!("取得できた要素 : {name}"),
        None => println!("要素が見つかりませんでした。"),
    }

    let charlie = names.get("Charlie");
    match charlie {
        Some(name) => println!("取得できた要素 : {name}"),
        None => println!("Charlie は存在しません。"),
    }
}
【実行結果】
取得できた要素 : Alice
Charlie は存在しません。

get は、Option<&T> 型 を返却するため、match を使って値の有無を確認して安全に処理することが可能です。指定した値が存在しない場合には、None となります。

要素の追加

新しい要素の追加や更新には、insert メソッドを使用します。例のように、既に同じ値が存在する場合は、追加されず HashSet の内容は変化しません。

use std::collections::HashSet;

fn main() {
    let mut names = HashSet::new();

    println!("追加前 : {names:?}");

    // 要素を追加する
    names.insert(String::from("Alice"));
    names.insert(String::from("Bob"));

    println!("追加後 : {names:?}");

    // 同じ値を追加しても重複しない
    names.insert(String::from("Alice"));

    println!("同じ値を再追加した後 : {names:?}");
}
【実行結果】
追加前 : {}
追加後 : {"Alice", "Bob"}
同じ値を再追加した後 : {"Alice", "Bob"}

要素の削除

要素を削除する場合には、remove メソッドを使用します。存在しない値を削除しようとしても何も起こりません。

use std::collections::HashSet;

fn main() {
    let mut names = HashSet::new();

    names.insert(String::from("Alice"));
    names.insert(String::from("Bob"));
    names.insert(String::from("Charlie"));

    println!("削除前 : {names:?}");

    // 要素を削除する
    names.remove("Alice");

    println!("Alice 削除後 : {names:?}");

    // 存在しない要素を削除しても変化しない
    names.remove("Dave");

    println!("Dave 削除後(変化なし) : {names:?}");
}
【実行結果】
削除前 : {"Bob", "Charlie", "Alice"}
Alice 削除後 : {"Bob", "Charlie"}
Dave 削除後(変化なし) : {"Bob", "Charlie"}

要素数の取得

HashSet に含まれる要素数を取得するには、len メソッドを使用します。

use std::collections::HashSet;

fn main() {
    let mut names = HashSet::new();

    let len = names.len();
    println!("初期状態の要素数 : {len}");

    names.insert(String::from("Alice"));
    names.insert(String::from("Bob"));

    let len = names.len();
    println!("追加後の要素数 : {len}");

    names.remove("Alice");

    let len = names.len();
    println!("削除後の要素数 : {len}");
}
【実行結果】
初期状態の要素数 : 0
追加後の要素数 : 2
削除後の要素数 : 1

要素の存在確認

HashSet に指定した値が存在するかどうかを確認するには、contains メソッドを使用します。指定値が存在するかは bool 型の返却値として確認できます。

use std::collections::HashSet;

fn main() {
    let mut names = HashSet::new();

    names.insert(String::from("Alice"));
    names.insert(String::from("Bob"));

    // 要素の存在を確認する
    let contains_alice = names.contains("Alice");
    let contains_charlie = names.contains("Charlie");
    println!("Alice は存在するか : {contains_alice}");
    println!("Charlie は存在するか : {contains_charlie}");
}
【実行結果】
Alice は存在するか : true
Charlie は存在するか : false

繰り返し処理(for)での利用方法

HashSet は複数の要素を管理するため、for を利用した繰り返し処理を行うことがよくあります。Rust の繰り返しでは、イテレータを取得して動作します。

参照のみの繰り返し処理

HashSet の要素を 1 つずつ取り出して参照するだけの繰り返しの場合には、以下のように不変参照 (&) を使って for を使用します。ループ後も元の HashSet を引き続き利用できます。

use std::collections::HashSet;

fn main() {
    let mut names = HashSet::new();

    names.insert(String::from("Alice"));
    names.insert(String::from("Bob"));
    names.insert(String::from("Charlie"));

    // 所有権を移動せず、参照としてループする
    for name in &names {
        println!("名前 : {name}");
    }

    // ループ後も names を使える
    println!("ループ後の要素数 : {}", names.len());
}
【実行結果】
名前 : Charlie
名前 : Bob
名前 : Alice
ループ後の要素数 : 3

なお、HashSet は要素がキーなので、要素を直接変更するという考え方が基本的にありません。そのため、HashMap ではあった値を変更を伴う繰り返し (iter_mut) はありません。

所有権を移動する繰り返し処理

HashSet の所有権を移動して繰り返し処理をする場合には、以下のように in の部分に HashSet の変数を直接指定します。所有権を移動する繰り返し処理をした場合には、繰り返し後には変数を使うことができなくなる点に注意が必要です。

use std::collections::HashSet;

fn main() {
    let mut names = HashSet::new();

    names.insert(String::from("Alice"));
    names.insert(String::from("Bob"));
    names.insert(String::from("Charlie"));

    // 所有権を移動してループする
    for name in names {
        println!("名前 : {name}");
    }

    // 所有権が移動したため、ここで names は使えない
    // println!("{:?}", names); // コンパイルエラー
}
【実行結果】
名前 : Charlie
名前 : Bob
名前 : Alice

この方法は、ループ後に元の HashSet を使わない場合や、大きいサイズの HashSet であるためループ終了にあわせてメモリ解放したい場合などに利用できます。

集合演算と包含関係の判定

集合演算

HashSet には、数学の集合演算に対応した各種メソッドが用意されています。

積集合

積集合を計算するには、intersection メソッドを使用します。積集合では、両方の集合に含まれる要素を取得できます。

use std::collections::HashSet;

fn main() {
    let set_a: HashSet<_> = [1, 2, 3, 4].into_iter().collect();
    let set_b: HashSet<_> = [3, 4, 5, 6].into_iter().collect();

    // 積集合(両方に含まれる要素)
    let intersection: HashSet<_> = set_a.intersection(&set_b).copied().collect();

    println!("A: {set_a:?}");
    println!("B: {set_b:?}");
    println!("A ∩ B: {intersection:?}");
}
【実行結果】
A: {3, 4, 2, 1}
B: {3, 4, 5, 6}
A ∩ B: {4, 3}

intersection などの集合演算メソッドは、元の集合の要素への参照を返すイテレータになっています。例の i32 のように Copy トレイトを実装している場合は、copied() を呼ぶことで参照 (&i32) を値 (i32) にコピーし、collect により HashSet<i32> として集約できます。

なお、所有権を持つ String のような Copy トレイトを実装していない型の場合は、cloned() を使用するか、参照のままの HashSet<&String> として扱うことになります。

和集合

和集合を計算するには、union メソッドを使用します。和集合では、どちらかの集合に含まれる要素が取得され、重複要素は自動で除かれます。

use std::collections::HashSet;

fn main() {
    let set_a: HashSet<_> = [1, 2, 3].into_iter().collect();
    let set_b: HashSet<_> = [3, 4, 5].into_iter().collect();

    // 和集合(どちらかに含まれる要素)
    let union: HashSet<_> = set_a.union(&set_b).copied().collect();

    println!("A: {set_a:?}");
    println!("B: {set_b:?}");
    println!("A ∪ B: {union:?}");
}
【実行結果】
A: {1, 2, 3}
B: {3, 4, 5}
A ∪ B: {1, 3, 5, 2, 4}

差集合

差集合を計算するには、differenceメソッドを使用します。差集合の場合は、順序が重要になり、以下の例で A - B の場合は、A にのみ含まれる要素を取得できます。一方で、B - A の場合は、B にのみ含まれる要素を取得できます。

use std::collections::HashSet;

fn main() {
    let set_a: HashSet<_> = [1, 2, 3, 4].into_iter().collect();
    let set_b: HashSet<_> = [3, 4, 5].into_iter().collect();

    // 差集合(A にだけ含まれる要素)
    let difference_a_b: HashSet<_> = set_a.difference(&set_b).copied().collect();

    // 差集合(B にだけ含まれる要素)
    let difference_b_a: HashSet<_> = set_b.difference(&set_a).copied().collect();

    println!("A: {set_a:?}");
    println!("B: {set_b:?}");
    println!("A - B: {difference_a_b:?}");
    println!("B - A: {difference_b_a:?}");
}
【実行結果】
A: {4, 2, 1, 3}
B: {3, 4, 5}
A - B: {1, 2}
B - A: {5}

排他的論理和

排他的論理和を計算するには、symmetric_difference メソッドを使用します。排他的論理和では、どちらか一方にのみ含まれる要素を取得します。

use std::collections::HashSet;

fn main() {
    let set_a: HashSet<_> = [1, 2, 3, 4].into_iter().collect();
    let set_b: HashSet<_> = [3, 4, 5, 6].into_iter().collect();

    // 排他的論理和(片方にだけ含まれる要素)
    let symmetric_difference: HashSet<_> = set_a.symmetric_difference(&set_b).copied().collect();

    println!("A: {set_a:?}");
    println!("B: {set_b:?}");
    println!("A △ B: {symmetric_difference:?}");
}
【実行結果】
A: {1, 4, 3, 2}
B: {4, 5, 6, 3}
A △ B: {6, 1, 2, 5}

集合の包含関係の判定方法

HashSet には、集合同士の包含関係を判定するメソッドも各種用意されています。

集合が等しいかどうかの判定

集合が等しいかを判定するには、== 演算子を使用します。要素の順序は関係ありません。

use std::collections::HashSet;

fn main() {
    let set_a: HashSet<_> = ["Alice", "Bob", "Charlie"].into_iter().collect();
    let set_b: HashSet<_> = ["Charlie", "Alice", "Bob"].into_iter().collect();
    let set_c: HashSet<_> = ["Alice", "Bob"].into_iter().collect();

    // 集合が等しいか判定する
    let eq_ab = set_a == set_b;
    let eq_ac = set_a == set_c;
    println!("A と B は等しいか : {eq_ab}");
    println!("A と C は等しいか : {eq_ac}");
}
【実行結果】
A と B は等しいか : true
A と C は等しいか : false

set_aset_b は順序は異なっていますが、含まれる要素が同じため true となります。

上位集合の判定

ある集合が別の集合の全要素を含む場合を上位集合と言います。上位集合かどうかを判定するには、is_superset メソッドを使用します。

use std::collections::HashSet;

fn main() {
    let all_members: HashSet<_> = ["Alice", "Bob", "Charlie"].into_iter().collect();
    let team_a: HashSet<_> = ["Alice", "Bob"].into_iter().collect();

    // 上位集合かどうかを判定する
    let is_superset_all = all_members.is_superset(&team_a);
    let is_superset_team = team_a.is_superset(&all_members);
    println!("all_members は team_a の上位集合か : {is_superset_all}");
    println!("team_a は all_members の上位集合か : {is_superset_team}");
}
【実行結果】
all_members は team_a の上位集合か : true
team_a は all_members の上位集合か : false

部分集合の判定

ある集合が別の集合に完全に含まれる場合を部分集合と言います。部分集合かどうかを判定するには、is_subset メソッドを使用します。

use std::collections::HashSet;

fn main() {
    let all_members: HashSet<_> = ["Alice", "Bob", "Charlie"].into_iter().collect();
    let team_a: HashSet<_> = ["Alice", "Bob"].into_iter().collect();

    // 部分集合かどうかを判定する
    let is_subset_team = team_a.is_subset(&all_members);
    let is_subset_all = all_members.is_subset(&team_a);
    println!("team_a は all_members の部分集合か : {is_subset_team}");
    println!("all_members は team_a の部分集合か : {is_subset_all}");
}
【実行結果】
team_a は all_members の部分集合か : true
all_members は team_a の部分集合か : false

共通要素の有無の判定

2 つの集合に共通する要素が存在しないかどうかを判定するには、is_disjoint メソッドを使います。共通要素がなければ true、1 つでもあれば false が返ります。

use std::collections::HashSet;

fn main() {
    let set_a: HashSet<_> = [1, 2, 3].into_iter().collect();
    let set_b: HashSet<_> = [4, 5, 6].into_iter().collect();
    let set_c: HashSet<_> = [3, 7, 8].into_iter().collect();

    // 共通要素がないかどうかを判定する
    let is_disjoint_ab = set_a.is_disjoint(&set_b);
    let is_disjoint_ac = set_a.is_disjoint(&set_c);
    println!("A と B に共通要素はない : {is_disjoint_ab}");
    println!("A と C に共通要素はない : {is_disjoint_ac}");
}
【実行結果】
A と B に共通要素はない : true
A と C に共通要素はない : false

intersection を使って確認することも可能ですが、単純に共通要素の有無だけを確認したい場合は、is_disjoint を使うと簡潔に記載できます。

まとめ

Rust で重複しない要素の集合を管理するための HashSet 型の基本を解説しました。

HashSet は、重複しない要素の集合を管理するためのコレクション型です。要素の追加・削除、存在確認といった基本操作に加えて、積集合・和集合・差集合などの集合演算や包含関係判定などについて紹介しました。

重複削除や高速な存在確認が必要な場面では、VecHashMap の代わりに HashSet の利用を検討してみてください。

あわせて読みたい
Rust プログラミング入門
Rust プログラミング入門

ABOUT ME
ホッシー
ホッシー
システムエンジニア
はじめまして。当サイトをご覧いただきありがとうございます。
私は製造業のメーカーで、DX推進や業務システムの設計・開発・導入を担当しているシステムエンジニアです。これまでに転職も経験しており、以前は電機メーカーでシステム開発に携わっていました。

これまでの業務を通じてさまざまなプログラミング言語や技術に触れてきましたが、その中でもRustの設計思想に惹かれ、この言語についてもっと深く学びたい、そしてその魅力を発信していきたいと思い、このサイトを立ち上げました。

自身の学びを整理しつつ、同じようにRustに興味を持つ方のお役に立てるような情報を発信していければと思っています。どうぞよろしくお願いいたします。

※キャラクターデザイン:ゼイルン様
記事URLをコピーしました