Всем доброго времени суток. Для начинающего программиста (коим я и являюсь) реализация базовых структур данных, по типу бинарных деревьев и связных списков, довольно легка. Чего не скажешь о хэш картах. В этой статье мы и разберём пример её реализации.

Об устройстве хэш карты

В данном примере мы будем рассматривать хэш-карту на базе функции хеширования Пирсона. Данная функция имеет на выходе число типа uint8, как следствие мы можем использовать 256 ячеек памяти для хранения результата на базе ключа. Однако есть такой недостаток хэш-функций как коллизии. Тут в дело вступают связные списки. Условимся, что наша структура хэш-карты будет иметь в себе массив длинною 256, а каждый из элементов будет являться связным списком.

Разберёмся на примере

Начнём с хэш-функции.
Краткими словами эта функция получающая на вход любое количество байт и возвращающая при этом 1 байт. Её реализация базируется на наборе инструкций (в данном случае сдвиг элемента на номер символа в таблицы ASCII), а так же на 256-байтной таблицы поиска, содержащей перестановку значений от 0 до 255.

var table[256]int = func()[256]int {
  var data [256]int
  rand.Seed(time.Now().Unix())
  for index, _ := range(data) { data[index] = index }
  for index, _ := range(data) {
    rv := rand.Intn(256)
    data[index], data[rv] = data[rv], data[index]
  }
  return data
}()

В начале мы создаём массив из 256 элементов, каждый из которых имеет значение, равное своему индексу, а затем перемешиваем эти элементы в рандомном порядке.

func hash8(message string) int {
  // using global table
  var hash int = len(message)%256

  for _, value := range(message) {
    hash = table[(hash + int(value))%256]
  }

  return hash
}

А вот и реализация алгоритма :)
Рассмотрим её поподробнее. Получая на вход строку, мы декларируем переменную hash равную (длина строки) mod 256, она же и будет служить результатом хэш-функции. Далее итерируя по строке мы присваиваем hash значение равное значение элемента таблицы поиска по индексу (hash + номер в ASCII) mod 256.

Разобравшись с хэш-функцией рассмотрим реализацию самой хэш-карты.

type Node struct {
  key   string
  value string
  next  *Node
}

type List struct {
  head 	*Node
}

type Map struct {
  map_list  [256]*List
  len       int64
}

Как и писалось выше хэш-карта базируется на массиве длинною 256 элементов. Каждый элемент это начало/голова связного списка List, состоящего из узлов Node, которые в свою очередь имеют переменные ключ key и значение value. Помимо того Map имеет переменную len, которая нужна для подсчёта длины карты.

Теперь перейдём к методам. Для начала надо реализовать метода работы связного списка:
1. Вставка элемента
2. Удаление элемента
3. Вывод элементов (в частность ключей)

Спойлер

Ниже мы не будем детально рассматривать реализацию методов связного списка...
Об это как-нибудь будет пост.

func (l *List) add(key, val string, m *Map) {
  new_data := &Node{value: val, key: key}
  if l.head == nil {
    l.head = new_data
    m.len++
  } else {
    current := l.head
    for current.next != nil {
      if key == current.key {
        current.value = val
        return
      }
      current = current.next
    }
    if key == current.key {
      current.value = val
      return
    }
    current.next = new_data
    m.len++
  }
}

Для вставки элемента мы проходим по связному списку, при нахождении такого же ключа, как и входного, изменяем значение, в ином случае добавляем в конец новый элемент и увеличиваем счётчик длины на 1.

func (l *List) delete(key string, m *Map) {
  current := l.head
  if current.key == key {
    l.head = l.head.next
    m.len--
    return
  }
  for current.next != nil {
    if current.next.key == key {
      current.next = current.next.next
      m.len--
      return
    }
    current = current.next
  }
  panic("Key is missing!!")
}

Для удаления элемента там необходимо пройтись по списку и найти нужный ключ и удалить этот элемент (заменив его на следующий), попутно уменьшив счётчик длины. В случае отсутствия ключа возвращаем ошибку.

func (l *List) print() []string {
  var result []string
  current := l.head
  if current != nil {
    result = append(result, current.key)
  }
  for current.next != nil {
    result = append(result, current.next.key)
    current = current.next
  }

  return result
}

Для получения ключей списка мы проходим по нему и собираем все ключи в массив result, а затем возвращаем его.

Следующим шагом необходимо реализовать методы самой структуры Map:
1. Вставка
2. Удаление
3. Получение значения по ключу
4. Получение всех ключей

func (m *Map) insert(key, val string) {
  if m.map_list[hash8(key)] == nil {
    m.map_list[hash8(key)] = &List{}
  }
  m.map_list[hash8(key)].add(key, val, m)
}

Для вставки элемента мы проверяем наличие данных в массиве по индексу результата хэш-функции, в случае, если его нет создаём экземпляр структуры List. Иначе у нас возникает коллизия. Для этого обращаемся к элементу (который является экземпляром структуры List) и вызываем у него ранее описанный метод add.

func (m *Map) delete(key string) {
  elem := m.map_list[hash8(key)]
  if elem == nil { panic("Map is nil!!") }
  if elem.head.key == key && elem.head.next == nil {
    m.map_list[hash8(key)]  = nil
    m.len--
    fmt.Println("+")
  } else if elem.head.next != nil {
    elem.delete(key, m)
  } else {
    panic("Key is missing!!")
  }
}

Для реализации удаления элемента проверяем наличие элемента в массиве по индексу результата хэш-функции:

  • Если элемент пустой, то возвращаем ошибку.

  • Если есть элемент, но следующего за ним нет, то присваиваем элементу массива nil.

  • Если есть элемент и последующий за ним, то вызываем метод delete у элемента массива (который имеет тип *List).

  • В ином случае возвращаем ошибку о том, что ключ отсутствует.

func (m *Map) get_value(key string) string {
  if m.map_list[hash8(key)] == nil { panic("Map is nil!!") }

  current := m.map_list[hash8(key)].head
  if current != nil && current.key == key { return current.value }
  for current.next != nil {
    if current.next.key == key { return  current.next.value }
    current = current.next
  }
  panic("Key is missing!!")
}

Чтобы получить значение по ключу, необходимо получить связный список из массива на основе результата хэш-функции. Далее пройтись по связному списку, сравнивая входной и текущий ключ.

func (m *Map) keys() []string {
  var answer [] string
  for _, data := range(m.map_list) {
    if data != nil { answer = append(answer, data.print()...)}
  }

  return answer
}

Для выгрузки всех ключей мы проходим по всему массиву. Если в элементе есть данные, то дополняем слайс answer результативным слайсом функции print у структуры List.

Итоги

Мы разобрались, как реализовать простую хэш-карту на основе 8-битной хэш-функции Пирсона с помощью языка Go. Рассмотрели краевые случаи всех описанных методов, а так же возникновение коллизии. Сложность данной структуры (как на чтение, так и на запись) я бы оценил в наилучшем случает, как O(1), в наихудшем же случае она доходит до O(n), при возникновении коллизии и формировании связного списка длинною > 1.

Исходный код

Комментарии (2)


  1. flx0
    19.09.2022 11:56
    +4

    Некорректно здесь говорить про то что сложнось O(1) в лучшем случае. О большое - это про то как ведет себя функция при сколь угодно больших значениях n. Как нетрудно видеть, после добавления 256 элементов в ваш хешмап, он гарантированно начнет расти линейно.


  1. wataru
    19.09.2022 12:15
    +2

    "Начнём с хэш-функции… функция получающая на вход любое количество байт и возвращающая при этом 1 байт"


    Некорректное утверждение. То, что у вас хеш в 8 бит (что ужасно плохо и мало!), вообще никак не относится к определению хеш функции.


    Ваш алгоритм тасовки массива при генерации table генерирует перестановки не равновероятно.


    Ни слова про открытую/закрытую адресацию. Ничего про fill factor, про перехеширование...


    Кстати, ваша таблица работает не за O(1), а за линию.


    В общем, ужасная "хеш-таблица" и посредственная статья. В википедии и то лучше написано.