Go 言語の辞書のソート

GolangGolangBeginner
今すぐ練習

💡 このチュートリアルは英語版からAIによって翻訳されています。原文を確認するには、 ここをクリックしてください

はじめに

他の言語とは異なり、Go では辞書(マップ)は順序付きのコレクションではありません。この実験では、辞書のソートと、もっと柔軟に使う方法について学びます。

キーコンセプト

  • 辞書のソート
  • 辞書内のキーと値の交換
  • 辞書のスライス
  • 値としてスライスを持つ辞書
  • 辞書の参照型の特性

Skills Graph

%%%%{init: {'theme':'neutral'}}%%%% flowchart RL go(("Golang")) -.-> go/BasicsGroup(["Basics"]) go(("Golang")) -.-> go/DataTypesandStructuresGroup(["Data Types and Structures"]) go(("Golang")) -.-> go/FunctionsandControlFlowGroup(["Functions and Control Flow"]) go(("Golang")) -.-> go/AdvancedTopicsGroup(["Advanced Topics"]) go/BasicsGroup -.-> go/values("Values") go/DataTypesandStructuresGroup -.-> go/slices("Slices") go/DataTypesandStructuresGroup -.-> go/maps("Maps") go/DataTypesandStructuresGroup -.-> go/structs("Structs") go/FunctionsandControlFlowGroup -.-> go/for("For") go/FunctionsandControlFlowGroup -.-> go/functions("Functions") go/AdvancedTopicsGroup -.-> go/sorting("Sorting") subgraph Lab Skills go/values -.-> lab-149095{{"Go 言語の辞書のソート"}} go/slices -.-> lab-149095{{"Go 言語の辞書のソート"}} go/maps -.-> lab-149095{{"Go 言語の辞書のソート"}} go/structs -.-> lab-149095{{"Go 言語の辞書のソート"}} go/for -.-> lab-149095{{"Go 言語の辞書のソート"}} go/functions -.-> lab-149095{{"Go 言語の辞書のソート"}} go/sorting -.-> lab-149095{{"Go 言語の辞書のソート"}} end

辞書のソート

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

touch ~/project/map.go

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

package main

import (
	"fmt"
)

func main() {
	// 文字列をキーとし、整数を値とするマップを宣言して初期化します
	// このマップは生徒の名前とその点数を格納します
	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("\nデータの挿入")

	// マップに新しいキーと値のペアを追加する方法を示します
	// 構文は: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

データの挿入
David 25
Charlie 84
Bob 38
Alice 99

挿入データの順序が固定されていないため、出力は異なる場合があります。これは Go マップの核心となる特性であり、要素を反復処理する際には特定の順序を保証しません。

プログラムを何度も実行してみると、挿入データの順序が変わることがわかります。これは、マップ内の要素の順序に依存できないことを示しています。

しかし、時々、データを挿入した後に辞書をソートする必要があります。それをどのように達成することができるでしょうか?

マップ自体はソートできないため、マップをスライスに変換してからスライスをソートすることができます。

キーでソートする

まず、辞書をキーでソートする方法を学びましょう。

以下が手順です。

  • 辞書のキーをスライスに変換します。スライスは順序付きの動的配列であり、ソートすることができます。
  • 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]

このコードの本質は、元の辞書からキーと値を抽出し、その後、役割を入れ替えて辞書に再挿入することです。シンプルで明快です。マップは順序付きでないため、交換後のマップの出力順序は異なる場合があることに注意してください。

値でソートする

辞書をキーでソートするロジックと、辞書内のキーと値を交換するロジックを組み合わせることで、辞書を値でソートすることができます。

その仕組みは以下の通りです。キーと値を交換し、その後、交換されたキー(元の値)をソートの基準とします。そして、ソートされた「キー」(元の値)を使って、交換後のマップから対応する元のキーを検索します。

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("キーで昇順にソートされました:")
	for _, pair := range s1 {
		fmt.Printf("%d, %d\n", pair.Key, pair.Value)
	}
}

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

go run ~/project/map.go

出力は以下の通りです。

キーで昇順にソートされました:
12, 98
21, 99
24, 36
35, 17

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

この比較関数を変更することで、キーで降順にソートしたり、値で昇順にソートしたりすることもできます。これにより、マップデータのソート方法に大きな柔軟性が得られます。

小テスト

map2.go ファイルを作成し、前節のコードを変更して、値に基づいてマップを降順にソートします。

期待される出力

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

go run ~/project/map2.go
値で降順にソートされました:
21, 99
12, 98
24, 36
35, 17

要件

  • map2.go ファイルは ~/project ディレクトリに配置する必要があります。
  • sort.Slice 関数を使用する必要があります。前の例で sort.Slice で使用した比較関数を変更する必要があります。
✨ 解答を確認して練習

辞書のスライス

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

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

package main

import "fmt"

func main() {
	// マップのスライスを宣言し、makeを使用して初期化します
	var mapSlice = make([]map[string]string, 3) // 容量3のスライスを作成し、各要素は `map[string]string` とすることができます。
	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) // キーが文字列で値が文字列のスライスであるマップを宣言します。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]

この例では、辞書の参照渡しの特性を示しました。modifyMap 関数は、a が同じ基礎となるマップデータへの参照であるため、元のマップを変更します。マップを関数に渡す際にこの動作を理解することは重要です。

まとめ

この実験では、Goにおけるマップの高度な使い方について学びました。これには以下が含まれます。

  • キーまたは値に基づいてマップをソートすること。これには、マップをスライスに変換してからスライスをソートすることが必要になります。
  • マップ内のキーと値を入れ替えること。これにより、役割が逆転した新しいマップが作成されます。
  • マップのスライスを使用すること。これにより、関連するマップデータのコレクションを作成できます。
  • 値としてスライスを持つマップを使用すること。これにより、単一のキーに複数の値を関連付けることができます。
  • マップの参照型の特性。渡されたマップの変更が元のマップに反映されます。

これらの概念を理解することで、現実世界のアプリケーションでGoのマップをより効果的に使用できるようになります。