課題8: マップを使ったデータ処理
課題概要
マップを使って、実用的なデータ処理ツールを実装します。頻度カウント、キャッシュ、グルーピング、セット演算など、マップの典型的な使用パターンを学びます。
マンダトリー要件
要件1: 単語カウンターとテキスト解析
テキスト解析ツールを実装してください。
ファイル: textanalyzer/analyzer.go
package textanalyzer
// WordCount は単語の出現回数を返す
func WordCount(text string) map[string]int {
// TODO: 実装してください
// - 単語を空白で分割
// - 大文字小文字を区別しない
// - 記号を除去
}
// CharFrequency は文字の出現頻度を返す
func CharFrequency(text string) map[rune]int {
// TODO: 実装してください
}
// TopWords は出現回数の多い順にn個の単語を返す
func TopWords(text string, n int) []WordFreq {
// TODO: 実装してください
}
type WordFreq struct {
Word string
Count int
}
// UniqueWords はユニークな単語の数を返す
func UniqueWords(text string) int {
// TODO: 実装してください
}
// WordPositions は各単語の出現位置を返す
func WordPositions(text string) map[string][]int {
// TODO: 実装してください
// 各単語がテキスト中の何番目に出現するかを記録
}
// IsAnagram は2つの文字列がアナグラムか判定
func IsAnagram(s1, s2 string) bool {
// TODO: 実装してください
// 文字の出現回数が同じならアナグラム
}
要件2: LRUキャッシュ
LRU(Least Recently Used)キャッシュを実装してください。
ファイル: cache/lru.go
package cache
import (
"errors"
)
var ErrNotFound = errors.New("key not found")
type LRUCache struct {
capacity int
cache map[string]*Node
head *Node
tail *Node
}
type Node struct {
key string
value interface{}
prev *Node
next *Node
}
// NewLRUCache はLRUキャッシュを作成
func NewLRUCache(capacity int) *LRUCache {
// TODO: 実装してください
// head と tail のダミーノードを作成
}
// Get は値を取得し、最近使用済みにする
func (c *LRUCache) Get(key string) (interface{}, error) {
// TODO: 実装してください
// - キーが存在すれば値を返し、先頭に移動
// - 存在しなければ ErrNotFound を返す
}
// Put は値を保存し、最近使用済みにする
func (c *LRUCache) Put(key string, value interface{}) {
// TODO: 実装してください
// - キーが存在すれば更新し、先頭に移動
// - 存在しなければ追加し、先頭に配置
// - 容量を超える場合は最後の要素を削除
}
// Size は現在のサイズを返す
func (c *LRUCache) Size() int {
// TODO: 実装してください
}
// Clear はすべての要素を削除
func (c *LRUCache) Clear() {
// TODO: 実装してください
}
// Keys はすべてのキーを返す(新しい順)
func (c *LRUCache) Keys() []string {
// TODO: 実装してください
}
要件3: データグルーピングとインデックス
データのグルーピングとインデックス作成機能を実装してください。
ファイル: grouping/grouper.go
package grouping
type Person struct {
ID int
Name string
Age int
City string
Email string
}
// GroupByCity は都市ごとにグループ化
func GroupByCity(people []Person) map[string][]Person {
// TODO: 実装してください
}
// GroupByAge は年齢ごとにグループ化
func GroupByAge(people []Person) map[int][]Person {
// TODO: 実装してください
}
// GroupByAgeRange は年齢層ごとにグループ化
func GroupByAgeRange(people []Person, rangeSize int) map[string][]Person {
// TODO: 実装してください
// 例: rangeSize=10 → "0-9", "10-19", "20-29", ...
}
// Index はIDでインデックスを作成
func Index(people []Person) map[int]Person {
// TODO: 実装してください
}
// MultiIndex は複数フィールドでインデックス
func MultiIndex(people []Person) *PersonIndex {
// TODO: 実装してください
}
type PersonIndex struct {
byID map[int]Person
byEmail map[string]Person
byCity map[string][]Person
}
// FindByID はIDで検索
func (pi *PersonIndex) FindByID(id int) (Person, bool) {
// TODO: 実装してください
}
// FindByEmail はメールアドレスで検索
func (pi *PersonIndex) FindByEmail(email string) (Person, bool) {
// TODO: 実装してください
}
// FindByCity は都市で検索
func (pi *PersonIndex) FindByCity(city string) []Person {
// TODO: 実装してください
}
要件4: セット操作
セット型とその操作を実装してください。
ファイル: set/set.go
package set
type Set map[string]struct{}
// New は空のセットを作成
func New(values ...string) Set {
// TODO: 実装してください
}
// Add は要素を追加
func (s Set) Add(value string) {
// TODO: 実装してください
}
// Remove は要素を削除
func (s Set) Remove(value string) {
// TODO: 実装してください
}
// Contains は要素が含まれているか判定
func (s Set) Contains(value string) bool {
// TODO: 実装してください
}
// Size は要素数を返す
func (s Set) Size() int {
// TODO: 実装してください
}
// ToSlice はスライスに変換
func (s Set) ToSlice() []string {
// TODO: 実装してください
}
// Union は和集合を返す
func (s Set) Union(other Set) Set {
// TODO: 実装してください
}
// Intersection は積集合を返す
func (s Set) Intersection(other Set) Set {
// TODO: 実装してください
}
// Difference は差集合を返す(sにあってotherにない)
func (s Set) Difference(other Set) Set {
// TODO: 実装してください
}
// IsSubset はsがotherの部分集合か判定
func (s Set) IsSubset(other Set) bool {
// TODO: 実装してください
}
// IsSuperset はsがotherの上位集合か判定
func (s Set) IsSuperset(other Set) bool {
// TODO: 実装してください
}
// Equal は2つのセットが等しいか判定
func (s Set) Equal(other Set) bool {
// TODO: 実装してください
}
期待される出力
// 単語カウント
text := "hello world hello"
counts := textanalyzer.WordCount(text)
// map[string]int{"hello": 2, "world": 1}
topWords := textanalyzer.TopWords(text, 2)
// []WordFreq{{"hello", 2}, {"world", 1}}
// LRUキャッシュ
cache := cache.NewLRUCache(2)
cache.Put("a", 1)
cache.Put("b", 2)
value, _ := cache.Get("a") // 1, "a"が最近使用
cache.Put("c", 3) // "b"が削除される
_, err := cache.Get("b") // ErrNotFound
// グルーピング
people := []grouping.Person{
{1, "Alice", 25, "Tokyo", "alice@example.com"},
{2, "Bob", 30, "Osaka", "bob@example.com"},
{3, "Carol", 25, "Tokyo", "carol@example.com"},
}
byCity := grouping.GroupByCity(people)
// map[string][]Person{
// "Tokyo": {{1, "Alice", ...}, {3, "Carol", ...}},
// "Osaka": {{2, "Bob", ...}},
// }
// セット
s1 := set.New("a", "b", "c")
s2 := set.New("b", "c", "d")
union := s1.Union(s2) // {"a", "b", "c", "d"}
intersection := s1.Intersection(s2) // {"b", "c"}
ボーナス課題
ボーナス1: タイムベースキャッシュ
TTL(Time To Live)付きキャッシュを実装してください。
package cache
import (
"sync"
"time"
)
type TTLCache struct {
mu sync.RWMutex
cache map[string]*CacheItem
ttl time.Duration
}
type CacheItem struct {
value interface{}
expiration time.Time
}
// NewTTLCache はTTLキャッシュを作成
func NewTTLCache(ttl time.Duration) *TTLCache {
// TODO: 実装してください
// バックグラウンドで期限切れアイテムを削除
}
// Get は値を取得(期限切れならnil)
func (c *TTLCache) Get(key string) interface{} {
// TODO: 実装してください
}
// Set は値を保存(TTL付き)
func (c *TTLCache) Set(key string, value interface{}) {
// TODO: 実装してください
}
// SetWithTTL は個別のTTLで保存
func (c *TTLCache) SetWithTTL(key string, value interface{}, ttl time.Duration) {
// TODO: 実装してください
}
// Delete は削除
func (c *TTLCache) Delete(key string) {
// TODO: 実装してください
}
// Clear はすべて削除
func (c *TTLCache) Clear() {
// TODO: 実装してください
}
ボーナス2: グラフ構造(隣接リスト)
マップを使ったグラフ構造を実装してください。
package graph
type Graph struct {
nodes map[string]*Node
edges map[string]map[string]int // from -> to -> weight
}
type Node struct {
ID string
Value interface{}
}
// NewGraph はグラフを作成
func NewGraph() *Graph {
// TODO: 実装してください
}
// AddNode はノードを追加
func (g *Graph) AddNode(id string, value interface{}) {
// TODO: 実装してください
}
// AddEdge はエッジを追加(重み付き)
func (g *Graph) AddEdge(from, to string, weight int) error {
// TODO: 実装してください
}
// RemoveNode はノードを削除
func (g *Graph) RemoveNode(id string) {
// TODO: 実装してください
}
// Neighbors は隣接ノードを返す
func (g *Graph) Neighbors(id string) []string {
// TODO: 実装してください
}
// HasPath はパスが存在するか判定(BFS)
func (g *Graph) HasPath(from, to string) bool {
// TODO: 実装してください
}
// ShortestPath は最短パスを返す(BFS)
func (g *Graph) ShortestPath(from, to string) []string {
// TODO: 実装してください
}
ボーナス3: トライ木(Prefix Tree)
マップを使ったトライ木を実装してください。
package trie
type Trie struct {
root *TrieNode
}
type TrieNode struct {
children map[rune]*TrieNode
isEnd bool
}
// NewTrie はトライ木を作成
func NewTrie() *Trie {
// TODO: 実装してください
}
// Insert は単語を挿入
func (t *Trie) Insert(word string) {
// TODO: 実装してください
}
// Search は単語が存在するか検索
func (t *Trie) Search(word string) bool {
// TODO: 実装してください
}
// StartsWith は指定プレフィックスで始まる単語が存在するか
func (t *Trie) StartsWith(prefix string) bool {
// TODO: 実装してください
}
// AutoComplete はオートコンプリート候補を返す
func (t *Trie) AutoComplete(prefix string) []string {
// TODO: 実装してください
}
// Delete は単語を削除
func (t *Trie) Delete(word string) bool {
// TODO: 実装してください
}
評価基準
| 項目 | 配点 | 詳細 |
|---|---|---|
| テキスト解析 | 25点 | 単語カウント、頻度解析が正しく動作 |
| LRUキャッシュ | 25点 | LRUアルゴリズムが正しく実装されている |
| グルーピング | 25点 | データのグループ化とインデックスが動作 |
| セット操作 | 25点 | セット演算が正しく実装されている |
| **ボーナス1** | 10点 | TTLキャッシュが動作する |
| **ボーナス2** | 10点 | グラフ操作が正しく実装されている |
| **ボーナス3** | 5点 | トライ木が効率的に実装されている |
提出方法
submission/
├── go.mod
├── textanalyzer/
│ ├── analyzer.go
│ └── analyzer_test.go
├── cache/
│ ├── lru.go
│ └── lru_test.go
├── grouping/
│ ├── grouper.go
│ └── grouper_test.go
├── set/
│ ├── set.go
│ └── set_test.go
└── bonus/
├── ttl/
├── graph/
└── trie/
ヒント
- 単語カウント:
strings.Fieldsで分割、strings.ToLowerで正規化 - LRUキャッシュ: 双方向リンクリストとマップの組み合わせ
- グルーピング: rangeでループしてマップに追加
- セット:
map[T]struct{}で省メモリ - TTLキャッシュ:
time.NewTickerで定期的にクリーンアップ - グラフ: BFSにはキューを使う
テストケース
// textanalyzer/analyzer_test.go
func TestWordCount(t *testing.T) {
text := "Hello world hello"
counts := WordCount(text)
if counts["hello"] != 2 {
t.Errorf("Expected 2 occurrences of 'hello', got %d", counts["hello"])
}
if counts["world"] != 1 {
t.Errorf("Expected 1 occurrence of 'world', got %d", counts["world"])
}
}
// cache/lru_test.go
func TestLRUCache(t *testing.T) {
cache := NewLRUCache(2)
cache.Put("a", 1)
cache.Put("b", 2)
if v, err := cache.Get("a"); err != nil || v != 1 {
t.Error("Failed to get 'a'")
}
cache.Put("c", 3) // "b" should be evicted
if _, err := cache.Get("b"); err != ErrNotFound {
t.Error("Expected 'b' to be evicted")
}
}
// set/set_test.go
func TestSetOperations(t *testing.T) {
s1 := New("a", "b", "c")
s2 := New("b", "c", "d")
union := s1.Union(s2)
if union.Size() != 4 {
t.Errorf("Union size = %d; want 4", union.Size())
}
intersection := s1.Intersection(s2)
if intersection.Size() != 2 {
t.Errorf("Intersection size = %d; want 2", intersection.Size())
}
}