Главная / Курсы / Golang / Хеш-таблицы
# Глава 6. Хеш-таблицы ## Понятие хеш-таблицы Очень полезной структурой данных является хеш-таблица. Хеш-таблица оперирует парами «ключ — значение», обеспечивая быстрый поиск, добавление и удаление значений по ключу. Средняя амортизационная стоимость операций оценивается как *O(1)*. Это означает, что среднее время выполнения одной операции поиска, добавления или удаления не зависит от размера входных данных и постоянно. Хеш-таблицы используются, например, для реализации in-memory кеша. Это особая структура данных в веб-приложениях, кеширующая информацию между пользовательскими приложениями и базой данных. Такой прием позволяет ускорить процесс обработки информации. Помимо этого, хеш-таблицы используются для реализации частотных словарей, индексации данных и вообще для задач, где требуется сопоставить ключ некоторому значению. Go предоставляет встроенный тип `map`, реализующий хеш-таблицу. Тип `map` — ссылочный тип. При создании переменной этого типа вы получаете ссылку на хеш-таблицу. Создание `map` с ключом типа `string` и значением типа `int`: ```go {.example_for_playground .example_for_playground_001} var catsWeights map[string]int ``` Мы объявили `map`, но не инициализировали ее: она равна `nil`. Делать так можно, но это не имеет особого смысла. При попытке заполнения `map` данными произойдет ошибка. Чтобы избежать таких проблем, при создании `map` используют встроенную функцию `make`: ```go {.example_for_playground .example_for_playground_002} catsWeights := make(map[string]int) ``` Использовать несколько типов для одного ключа или значения нельзя. Типы ключей должны быть проверяемыми на равенство, чтобы можно было различать два ключа между собой. ## Заполнение хеш-таблицы данными `catsWeights` — пустая хеш-таблица. Выведем ее через функцию `fmt.Println`: ```go {.example_for_playground .example_for_playground_003} catsWeights := make(map[string]int) fmt.Println(catsWeights) ``` ``` map[] ``` Заполним эту хеш-таблицу данными: ```go {.example_for_playground .example_for_playground_004} catsWeights["Barsik"] = 10 catsWeights["Busa"] = 2 fmt.Println(catsWeights) ``` Так выглядит заполнение хеш-таблицы данными в момент создания: ```go {.example_for_playground .example_for_playground_005} catsWeights := map[string]int{ "Barsik": 10, "Busa": 2, } ``` ## Работа с элементами хеш-таблицы Так выглядит обращение к элементу `map` по ее ключу: ```go {.example_for_playground .example_for_playground_006} fmt.Println(catsWeights["Busa"]) ``` Если вы попытаетесь узнать значение несуществующего элемента, то получите значение типа по умолчанию: ```go {.example_for_playground .example_for_playground_007} fmt.Println(catsWeights["Sharik"]) ``` ``` 0 ``` Функция `piDigit()` по индексу возвращает очередной знак числа Пи (3.1415926535...) после десятичной точки, начиная с нулевого знака и заканчивая девятым включительно. {.task_text} С использованием `piDigit()` реализуйте тело функции `countDigits()`. Она принимает количество цифр числа Пи после десятичной точки (`0 <= n <= 9`) и возвращает `map` с количеством раз, которое встретилась каждая цифра. Цифры, не встретившиеся ни разу, в хеш-таблицу включать не нужно. {.task_text} Например, для `countDigits(5)` результат следующий: `map[1:2 4:1 5:1 9:1]`. Таким образом, для последовательности цифр `14159` цифра `1` встречается `2` раза; `4`, `5`, `9` - по одному разу. Другие цифры не встречаются. {.task_text} ```go {.task_source #golang_chapter_0060_task_0010} package main import ( "fmt" "math" "strconv" ) func piDigit(index int) int { const maxLen = 10 if index < 0 || index >= maxLen { return -1 } const integerPartLen = 2 piStr := strconv.FormatFloat(math.Pi, 'f', maxLen+integerPartLen, 64) res, _ := strconv.Atoi(string(piStr[index+integerPartLen])) return res } func countDigits(n int) map[int]int { // ваш код здесь } func main() { fmt.Println(countDigits(10)) } ``` Добавляйте в цикле единицу значению `map`, ключом которой является очередная цифра числа Пи. {.task_hint} ```go {.task_answer} package main import ( "fmt" "math" "strconv" ) func piDigit(index int) int { const maxLen = 10 if index < 0 || index >= maxLen { return -1 } const integerPartLen = 2 piStr := strconv.FormatFloat(math.Pi, 'f', maxLen+integerPartLen, 64) res, _ := strconv.Atoi(string(piStr[index+integerPartLen])) return res } func countDigits(n int) map[int]int { res := make(map[int]int) for i := 0; i < n; i++ { res[piDigit(i)]++ } return res } func main() { fmt.Println(countDigits(10)) } ``` Так выглядит удаление элемента из `map`: ```go {.example_for_playground .example_for_playground_008} delete(catsWeights, "Busa") ``` Встроенная функция `delete` удаляет элемент по ключу. Если ключ не найден, функция ничего не делает. Узнать длину `map` позволяет встроенная функция `len()`: ```go fmt.Println(len(catsWeights)) ``` ## Перебор элементов хеш-таблицы Чтобы перебрать все пары «ключ-значение», воспользуйтесь циклом и ключевым словом `range`: ```go {.example_for_playground .example_for_playground_009} for key, val := range catsWeights { fmt.Println(key, ":", val) } ``` Порядок, в котором располагаются элементы в `map`, **не определен** и изменяется от одного запуска программы к другому. Это сделано намеренно, поскольку из-за особенностей реализации нельзя гарантировать, что в разных версиях языка порядок окажется одним и тем же. Решение, при котором порядок элементов `map` не определен даже от запуска к запуску, позволяет писать более надежные программы. Если нужен конкретный порядок элементов, то их придется отсортировать самостоятельно. Например, строковые ключи можно отсортировать функцией `Strings` пакета `sort`. Функция `calculateConsts()` возвращает `map`, в которой наименованиям констант соотнесены их значения. Значение каждой константы содержит некоторое количество цифр после десятичной точки. Оно генерируется псевдослучайным образом в зависимости от входного аргумента `seed`. {.task_text} Реализуйте тело функции `chooseConsts()`, которая принимает на вход эту хеш-таблицу и удаляет из нее те константы, которые содержат после десятичной точки менее 5 цифр. Константы в хеш-таблице представлены строками. Длину строки в байтах позволяет узнать встроенная функция `len()`. {.task_text} ```go {.task_source #golang_chapter_0060_task_0020} package main import ( "fmt" "math" "math/rand" "strconv" "strings" ) func randLenStr(val float64, pseudoRandGen *rand.Rand) string { // дробная часть чисел не длиннее 10 знаков const prec = 10 // значение в интервале [1, 11) fractionalPartLen := pseudoRandGen.Intn(prec) + 1 strVal := strconv.FormatFloat(val, 'f', prec, 64) integerPartLen := strings.IndexRune(strVal, '.') + 1 return strVal[:integerPartLen + fractionalPartLen] } func calculateConsts(seed int64) map[string]string { // генератор псевдослучайных чисел gen := rand.New(rand.NewSource(seed)) return map[string]string{ "E": randLenStr(math.E, gen), "Pi": randLenStr(math.Pi, gen), "Sqrt2": randLenStr(math.Sqrt2, gen), "SqrtE": randLenStr(math.SqrtE, gen), "SqrtPi": randLenStr(math.SqrtPi, gen), "Ln2": randLenStr(math.Ln2, gen), "Ln10": randLenStr(math.Ln10, gen), } } func chooseConsts(consts map[string]string) { // ваш код здесь } func main() { // аргумент можно менять для отладки consts := calculateConsts(42) fmt.Println(consts) chooseConsts(consts) fmt.Println(consts) } ``` Чтобы узнать количество цифр после десятичной точки, воспользуйтесь функцией `IndexRune()` пакета `strings`. Эта функция позволяет определить индекс точки в строке. Затем вычислите длину подстроки после точки. В этом вам поможет встроенная функция `len()`. {.task_hint} ```go {.task_answer} package main import ( "fmt" "math" "math/rand" "strconv" "strings" ) func randLenStr(val float64, pseudoRandGen *rand.Rand) string { // дробная часть чисел не длиннее 10 знаков const prec = 10 // значение в интервале [1, 11) fractionalPartLen := pseudoRandGen.Intn(prec) + 1 strVal := strconv.FormatFloat(val, 'f', prec, 64) integerPartLen := strings.IndexRune(strVal, '.') + 1 return strVal[:integerPartLen + fractionalPartLen] } func calculateConsts(seed int64) map[string]string { // генератор псевдослучайных чисел gen := rand.New(rand.NewSource(seed)) return map[string]string{ "E": randLenStr(math.E, gen), "Pi": randLenStr(math.Pi, gen), "Sqrt2": randLenStr(math.Sqrt2, gen), "SqrtE": randLenStr(math.SqrtE, gen), "SqrtPi": randLenStr(math.SqrtPi, gen), "Ln2": randLenStr(math.Ln2, gen), "Ln10": randLenStr(math.Ln10, gen), } } func chooseConsts(consts map[string]string) { const fractionalPartMinLen = 5 var keysToDelete []string for key, val := range consts { pointIndex := strings.IndexRune(val, '.') if pointIndex == -1 || len(val[pointIndex + 1:]) < fractionalPartMinLen { keysToDelete = append(keysToDelete, key) } } for _, key := range keysToDelete { delete(consts, key) } } func main() { // аргумент можно менять для отладки consts := calculateConsts(42) fmt.Println(consts) chooseConsts(consts) fmt.Println(consts) } ``` ## Проверка наличия элемента в хеш-таблице Когда вы обращаетесь к элементу `map`, то всегда получаете значение. Не важно, содержится элемент в хеш-таблице или просто имеет значение по умолчанию. В большинстве случаев это нормально, однако иногда нужно точно знать, содержит ли хеш-таблица данный элемент. На самом деле, когда вы обращаетесь к элементу `map` по ключу, то возвращается не только значение, но и флаг. Он равен `true`, если элемент содержится в `map` и `false` — в противном случае. Часто его присваивают переменной с именем `ok`: ```go {.example_for_playground .example_for_playground_010} catsWeights := make(map[string]int) catsWeights["Barsik"] = 10 catsWeights["Busa"] = 2 if catsWeight, ok := catsWeights["Sharik"]; !ok { fmt.Println("no key Sharik in the catsWeights") fmt.Println("catsWeight =", catsWeight) } ``` Если переменная `ok` не нужна, то объявлять ее не следует: ```go {.example_for_playground .example_for_playground_011} catsWeight := catsWeights["Sharik"] fmt.Println("catsWeight =", catsWeight) ``` Как и срезы, хеш-таблицы нельзя сравнивать друг с другом. Хеш-таблицу можно сравнить только со значением `nil`. Напишите тело функции `isEqual`, которая проверяет, содержат ли две хеш-таблицы типа `map[string]int` одинаковые ключи и связанные с ними значения. Функция `isEqual` возвращает `true`, если это так, и `false` — в противном случае. {.task_text} ```go {.task_source #golang_chapter_0060_task_0030} package main import "fmt" func isEqual(x, y map[string]int) bool { // ваш код здесь } func main() { catsWeights := map[string]int{ "Barsik": 10, "Busa": 2, "Murzik": 0, } dogsWeights := map[string]int{ "Barsik": 10, "Busa": 2, "Sharik": 0, } fmt.Println(isEqual(catsWeights, dogsWeights)) } ``` Проверьте длины передаваемых хеш-таблиц. Если длины не равны, то возвращаем `false`. В противном случае организуйте цикл по элементам `map` и сравните их. Учтите: когда вы обращаетесь к элементу `map`, то элемент может как содержать значение по умолчанию, так и вовсе отсутствовать в `map`. В обоих случаях вы получите значение по умолчанию. Две эти ситуации необходимо различать. {.task_hint} ```go {.task_answer} package main import "fmt" func isEqual(x, y map[string]int) bool { if len(x) != len(y) { return false } for key, xVal := range x { if yVal, ok := y[key]; !ok || yVal != xVal { return false } } return true } func main() { catsWeights := map[string]int{ "Barsik": 10, "Busa": 2, "Murzik": 0, } dogsWeights := map[string]int{ "Barsik": 10, "Busa": 2, "Sharik": 0, } fmt.Println(isEqual(catsWeights, dogsWeights)) } ``` Начиная с версии Go 1.12, хеш-таблицы выводятся в консоль в отсортированном по ключам виде. Таким образом, можно было бы сравнить две хеш-таблицы как строки: ```go {.example_for_playground .example_for_playground_012} catsWeights := map[string]int{ "Barsik": 10, "Busa": 2, "Murzik": 0, } dogsWeights := map[string]int{ "Barsik": 10, "Busa": 2, "Sharik": 0, } fmt.Println(fmt.Sprint(catsWeights) == fmt.Sprint(dogsWeights)) ``` Мы не рекомендуем так делать в реальных проектах, потому что это ведет к потере в производительности. Однако такой прием может быть полезен при отладке. ## Реализация множеств Во многих современных языках, например в C++ и Python, существуют множества — коллекции уникальных элементов. В Go множества отсутствуют, однако вместо них можно использовать `map`. Хеш-таблица хранит пары: уникальные ключи и привязанные к ним значения. Для решения реальных задач значения нужны не всегда. Зачастую достаточно хранить только ключи без значений. Если создать `map` с ключом требуемого типа и значением типа пустой структуры `struct{}`, то мы получим хеш-таблицу, имитирующую множество. В качестве значения всегда указывается `struct{}{}`. Пустая структура, во-первых, не занимает места. Во-вторых, она подчеркивает намерение: нам нужны ключи без значений. Попробуйте реализовать эту идею в задаче подсчета уникальных слов в тексте. В переменной `s` типа `string` содержится некоторый текст. Допишите функцию `countUniqWords()`, которая возвращает количество уникальных слов в тексте. Словом будем считать любую последовательность символов, отделенную пробелами. {.task_text} ```go {.task_source #golang_chapter_0060_task_0040} package main import ( "fmt" "strings" ) func countUniqWords(s string) int { // ваш код здесь } func main() { s := "Go Rust Go C++ Lisp Lisp" fmt.Println(countUniqWords(s)) } ``` Создайте хеш-таблицу типа `map[string]struct{}`. Добавляйте в цикле каждое слово в качестве ключа хеш-таблицы, а в качестве его значения — `struct{}{}`. Таким образом, вы получите хеш-таблицу с уникальными словами. {.task_hint} ```go {.task_answer} package main import ( "fmt" "strings" ) func countUniqWords(s string) int { if len(s) == 0 { return 0 } uniqWords := make(map[string]struct{}) for _, word := range strings.Split(s, " ") { uniqWords[word] = struct{}{} } return len(uniqWords) } func main() { s := "Go Rust Go C++ Lisp Lisp" fmt.Println(countUniqWords(s)) } ``` ## Невозможность получения адреса значения элемента `map` Получить адрес значения элемента хеш-таблицы невозможно. Если вы попытаетесь сделать это, то получите ошибку компиляции: ```go {.example_for_playground .example_for_playground_013} catsWeights := make(map[string]int) catsWeights["Barsik"] = 10 fmt.Println(&catsWeights["Barsik"]) ``` ``` invalid operation: cannot take address of catsWeights["Barsik"] (map index expression of type int) ``` Так происходит, потому что значения элементов `map` не адресуемы. Отображение периодически расширяется. Если бы этого не происходило, то количество хранимых элементов превысило бы тот объем памяти, который отведен под `map`. Решение о том, когда пора увеличивать размер таблицы, принимается на основании `loadFactor`. Коэффициент заполняемости таблицы `loadFactor` — это отношение количества хранимых элементов «ключ — значение» к размеру таблицы. Когда он достигает пограничного значения, то хеш-таблица расширяется, а индексы элементов пересчитываются и перераспределяются. Если бы можно было получить адрес значения элемента `map`, то после перераспределения он бы устарел и оказался недопустимым. Получить адрес самой хеш-таблицы мы можем: ```go {.example_for_playground .example_for_playground_014} catsWeights := make(map[string]int) catsWeights["Barsik"] = 10 fmt.Printf("%p\n", &catsWeights) ``` ## Резюме 1. В Go хеш-таблицы реализованы через тип `map`. Переменная типа `map` — это ссылка на хеш-таблицу. 2. Хеш-таблицы содержат пары «ключ — значение» с уникальными ключами. 3. При попытке узнать значение несуществующего элемента хеш-таблицы через `[]` вы получите значение типа по умолчанию. 4. При обращении к элементу `map` через `[]` возвращается не только значение по ключу, но и флаг. Флаг позволяет понять, содержится ли искомый ключ в `map`. 5. Встроенная функция `delete` позволяет удалить элемент из `map`, `len` — узнать длину `map`, ключевое слово `range` — перебрать элементы `map`. 6. Порядок элементов `map` не определен. 7. В Go отсутствует тип `set` (множество уникальных элементов). На практике он заменяется хеш-таблицей со значениями типа `struct{}`. 8. Получить адрес значения элемента `map` нельзя.

Следующие главы находятся в разработке

Отправка...
Наша группа в telegram. Здесь можно задавать вопросы и общаться.
Задонатить. Если вам нравится курс, вы можете поддержать развитие площадки!