Go のマップ(辞書)のソート

GolangBeginner
オンラインで実践に進む

はじめに

他の言語とは異なり、Go 言語の辞書(マップ)は順序が保証されないコレクションです。この実験では、マップのソート方法や、より柔軟にマップを活用する方法について学びます。

主な学習項目:

  • マップのソート
  • マップのキーと値の入れ替え
  • マップのスライス
  • 値としてスライスを持つマップ
  • 参照型としてのマップの特性

マップのソート

まず、~/project ディレクトリに map.go ファイルを作成します。

touch ~/project/map.go

map.go ファイルに以下のコードを記述します。

package main

import (
 "fmt"
)

func main() {
 // string 型のキーと int 型の値を持つマップを宣言し、初期化します
 // このマップは学生の名前とスコアを保持します
 m := map[string]int{
  "Alice":   99, // 各キーと値のペアは学生とそのスコアを表します
  "Bob":     38,
  "Charlie": 84,
 }

 // for-range ループを使用してマップを反復処理します
 // 'key' は学生の名前、'value' はスコアを表します
 for key, value := range m {
  fmt.Println(key, value)
 }

 fmt.Println("\nInsert Data")

 // マップに新しいキーと値のペアを追加する方法を示します
 // 構文は map[key] = value です
 m["David"] = 25

 // 更新されたマップの内容を表示するため、再度反復処理を行います
 // 実行ごとに順序が異なる可能性があることに注意してください
 for key, value := range m {
  fmt.Println(key, value)
 }
}

プログラムを実行します。

go run ~/project/map.go

出力結果は以下のようになります(順序は異なる場合があります)。

Charlie 84
Bob 38
Alice 99

Insert Data
David 25
Charlie 84
Bob 38
Alice 99

挿入されたデータの順序が固定されていないため、出力結果は実行のたびに変わる可能性があります。これは Go のマップの核心的な特性です。マップを反復処理する際、要素の特定の順序は保証されません。

プログラムを数回実行して、データの順序が変わることを確認してみてください。これにより、マップ内の要素の順序に依存してはいけないことが理解できるはずです。

しかし、データを挿入した後にマップをソートする必要がある場合があります。どのように実現すればよいでしょうか?

マップ自体を直接ソートすることはできないため、マップをスライスに変換してから、そのスライスをソートするという手法をとります。

キーによるソート

まず、マップをキー(Key)に基づいてソートする方法を学びましょう。

手順は以下の通りです。

  • マップのキーをスライスに変換します。スライスは順序を持つ動的配列であり、ソートが可能です。
  • Go の標準パッケージである sort を使用してスライスをソートします。
  • ソートされたスライスのキーを使って、マップから対応する値を取得します。スライス内のキーがソートされているため、マップから値を取り出す際もソートされた順序で表示されます。

map.go ファイルに以下のコードを記述します。

package main

import (
 "fmt"
 "sort"
)

func main() {
 // マップを初期化します
 m1 := map[int]string{
  3: "Bob",
  1: "Alice",
  2: "Charlie",
 }
 keys := make([]int, 0, len(m1)) // キャパシティを指定してスライスを初期化します。これはパフォーマンスの最適化であり、最初からマップと同じサイズのメモリを割り当てることで、再割り当てを避けます。
 for key := range m1 {
  // キーをスライスに追加します
  keys = append(keys, key)
 }
 // sort パッケージを使用してキーのスライスをソートします。`sort.Ints()` 関数は整数スライスを昇順にソートします。
 sort.Ints(keys)
 for _, key := range keys {
  // これで順序通りに出力されます
  fmt.Println(key, m1[key])
 }
}

プログラムを実行します。

go run ~/project/map.go

出力結果は以下の通りです。

1 Alice
2 Charlie
3 Bob

このアプローチにより、キーに基づいたマップのソートが実現できました。キーをスライスに抽出し、ソートしてから、そのキーを使ってマップの値を参照しています。

マップのキーと値の入れ替え

値によるソート方法を説明する前に、マップのキーと値を入れ替える方法を学びましょう。

キーと値の入れ替えとは、以下の図に示すように、マップ内のキーと値の立場を交換することを意味します。

Dictionary key value swap

実装は非常にシンプルです。map.go ファイルに以下のコードを記述します。

package main

import "fmt"

func main() {
 m := map[string]int{
  "Alice":   99,
  "Bob":     38,
  "Charlie": 84,
 }
 m2 := map[int]string{}
 for key, value := range m {
  m2[value] = key
 }
 fmt.Println(m2)
}

プログラムを実行します。

go run ~/project/map.go

出力結果は以下の通りです。

map[38:Bob 84:Charlie 99:Alice]

このコードの本質は、元のマップからキーと値を取り出し、役割を入れ替えて新しいマップに再挿入することです。非常に直感的です。ただし、マップは順序を保持しないため、入れ替えた後のマップの出力順序も不定であることに注意してください。

値によるソート

「キーによるソート」と「キーと値の入れ替え」のロジックを組み合わせることで、マップを値(Value)に基づいてソートすることができます。

仕組みはこうです。まずキーと値を入れ替え、入れ替えた後のキー(元の値)をソートの基準にします。その後、ソートされた「キー」(元の値)を使用して、入れ替えたマップから元のキーを検索します。

map.go ファイルに以下のコードを記述します。

package main

import (
 "fmt"
 "sort"
)

func main() {
 // マップを初期化します
 m1 := map[string]int{
  "Alice":   99,
  "Bob":     38,
  "Charlie": 84,
 }

 // 反転させたマップを初期化します
 m2 := map[int]string{}
 for key, value := range m1 {
  // キーと値を入れ替えて反転マップ m2 を生成します
  m2[value] = key
 }

 values := make([]int, 0) // ソート用のスライスを初期化します
 for _, value := range m1 {
  // 元のマップの値をスライスに変換します
  values = append(values, value)
 }
 // sort パッケージを使用して値のスライスをソートします
 sort.Ints(values)

 for _, value := range values {
  // これで順序通りに出力されます
  fmt.Println(m2[value], value)
 }
}

プログラムを実行します。

go run ~/project/map.go

出力結果は以下の通りです。

Bob 38
Charlie 84
Alice 99

これで、値に基づいたマップのソートが完了しました。値をスライスに変換してソートし、そのソートされた値を使って反転マップから元のキーを取得して表示しています。

sort.Slice によるソート

Go のバージョンが 1.7 以降であれば、sort.Slice 関数を使用して、キーまたは値でマップを素早くソートできます。sort.Slice では、カスタムの比較関数を指定できます。

以下に例を示します。

package main

import (
 "fmt"
 "sort"
)

func main() {
 m1 := map[int]int{
  21: 99,
  12: 98,
  35: 17,
  24: 36,
 }

 type kv struct {
  Key   int
  Value int
 }

 var s1 []kv

 for k, v := range m1 {
  s1 = append(s1, kv{k, v})
 }

 sort.Slice(s1, func(i, j int) bool {
  return s1[i].Key < s1[j].Key
 })

 fmt.Println("Sorted in ascending order by key:")
 for _, pair := range s1 {
  fmt.Printf("%d, %d\n", pair.Key, pair.Value)
 }
}

プログラムを実行します。

go run ~/project/map.go

出力結果は以下の通りです。

Sorted in ascending order by key:
12, 98
21, 99
24, 36
35, 17

このプログラムでは、マップのキーと値のペアを保持するために構造体 kv を使用しました。次に、構造体のスライスを sort.Slice() 関数と無名比較関数を使用してソートしました。この比較関数 (func(i, j int) bool) は、構造体の Key フィールドに基づいてソート順を決定します。

この比較関数を修正することで、キーによる降順ソートや、値による昇順ソートなども sort.Slice で実現できます。これにより、マップデータのソート方法に大きな柔軟性が生まれます。

小テスト

map2.go ファイルを作成し、前のセクションのコードを修正して、マップを値に基づいて降順にソートしてください。

期待される出力:

プログラムを実行します。

go run ~/project/map2.go
Sorted in descending order by value:
21, 99
12, 98
24, 36
35, 17

要件:

  • map2.go ファイルは ~/project ディレクトリに配置してください。
  • sort.Slice 関数を使用する必要があります。前の例で使用した比較関数を修正してください。

マップのスライス

関連するデータを保存するために配列やスライスを使用するのと同様に、要素がマップであるスライスを使用することもできます。これにより、マップデータのコレクションを保持でき、構造化された情報を扱う際に非常に便利です。

map.go ファイルに以下のコードを記述します。

package main

import "fmt"

func main() {
 // マップのスライスを宣言し、make を使用して初期化します
 var mapSlice = make([]map[string]string, 3) // 各要素が map[string]string である、キャパシティ 3 のスライスを作成します。
 for index, value := range mapSlice {
  fmt.Printf("index:%d value:%v\n", index, value)
 }
 fmt.Println("Initialization")
 // スライスの最初の要素に値を割り当てます
 mapSlice[0] = make(map[string]string, 10) // 最初のインデックスにマップを作成します。
 mapSlice[0]["name"] = "labex"
 mapSlice[0]["password"] = "123456"
 mapSlice[0]["address"] = "Paris"
 for index, value := range mapSlice {
  fmt.Printf("index:%d value:%v\n", index, value)
 }
}

プログラムを実行します。

go run ~/project/map.go

出力結果は以下の通りです。

index:0 value:map[]
index:1 value:map[]
index:2 value:map[]
Initialization
index:0 value:map[address:Paris name:labex password:123456]
index:1 value:map[]
index:2 value:map[]

このコードは、マップのスライスの初期化方法を示しています。最初はスライスの各要素は空のマップです。その後、最初の要素にマップを作成して割り当て、データを入力しています。これにより、マップのリストを管理する方法がわかります。

値としてスライスを持つマップ

マップの「値」としてスライスを使用することもできます。これにより、マップ内の 1 つのキーに対して複数の値を関連付けることができ、実質的に「一対多」の関係を構築できます。

map.go ファイルに以下のコードを記述します。

package main

import "fmt"

func main() {
 var sliceMap = make(map[string][]string, 3) // キーが string、値が string のスライスであるマップを宣言します。3 はキャパシティのヒントです。
 key := "labex"
 value, ok := sliceMap[key]  // キーが存在するか確認します
 if !ok {
  value = make([]string, 0, 2) // 存在しない場合は、キャパシティを指定して新しいスライスを初期化します。
 }
 value = append(value, "Paris", "Shanghai") // スライスに値を追加します。
 sliceMap[key] = value // マップ内のキーに対応する値としてスライスをセットします。
 fmt.Println(sliceMap)
}

プログラムを実行します。

go run ~/project/map.go

出力結果は以下の通りです。

map[labex:[Paris Shanghai]]

このコードでは、まず値がスライスであるマップ型を宣言しています。関連付けられたスライスに新しい項目を追加する前にキーの存在を確認するという、スライスを持つマップを扱う際の一般的なパターンを示しています。

参照型としてのマップの特性

配列は「値型」であるため、関数に渡したり代入したりするとコピーが作成されます。コピーへの変更は元の配列には影響しません。しかし、マップは「参照型」です。つまり、マップを関数に渡したり代入したりしても、完全なコピーは作成されません。代わりに、マップは参照として渡されます。

これは非常に重要です。関数内で行われた変更は、呼び出し元の元のマップデータにも影響を与えます。

map.go ファイルに以下のコードを記述します。

package main

import "fmt"

func modifyMap(x map[string]int) {
 x["Bob"] = 100 // 引数として渡されたマップを修正します。
}

func main() {
 a := map[string]int{
  "Alice":   99,
  "Bob":     38,
  "Charlie": 84,
 }
 // マップは参照渡しされるため、modifyMap 内での変更は元のマップに反映されます
 modifyMap(a)
 fmt.Println(a) // map[Alice:99 Bob:100 Charlie:84]
}

プログラムを実行します。

go run ~/project/map.go

出力結果は以下の通りです。

map[Alice:99 Bob:100 Charlie:84]

この例では、マップの参照渡しの特性を実証しました。a は基底となる同じマップデータへの参照であるため、modifyMap 関数は元のマップを書き換えます。マップを関数に渡す際、この挙動を理解しておくことは極めて重要です。

まとめ

この実験では、以下のような Go 言語におけるマップの高度な使い方を学びました。

  • キーまたは値によるマップのソート:マップをスライスに変換してからソートする手法。
  • マップのキーと値の入れ替え:役割を反転させた新しいマップを作成する方法。
  • マップのスライス:関連するマップデータのコレクションを作成する方法。
  • 値としてスライスを持つマップ:1 つのキーに複数の値を関連付ける方法。
  • 参照型としてのマップの特性:渡されたマップへの変更が元のマップに反映される仕組み。

これらの概念を理解することで、実際のアプリケーション開発において Go のマップをより効果的に活用できるようになります。

✨ 解答を確認して練習