Задать вопрос
@zlodiak

Можно ли так описать хэш-таблицу?

Готовлюсь сдавать экзамен и пробую описать в общих чертах как работает словарь на основе хэш-таблицы. Оцените пожалуйста, можно ли так рассказать о ней:

словари и множества организованы на основе хэш-таблицы(ХТ).

ХТ это разреженный массив. примерно треть состоит из пустых значений. По мере изменения размера он время от времени перестраивается в новый участок памяти, изменяя количество своих элементов. Поэтому изменять словарь в цикле плохая идея(Нужно сначала прогнать цикл, а затем внести изменения).

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

Таким образом можно запросить значение по ключу: arr[key]. Поиск происходит очень быстро потому что не приходится перебирать все значения(пусть даже двоичным поиском) как например в случае со списком.


Я упустил что-нибудь важное? Как можно дополнить и что стоит почитать?
  • Вопрос задан
  • 227 просмотров
Подписаться 2 Простой 1 комментарий
Решения вопроса 1
@deliro
1. ХТ пуста от 2/3 своего размера до 1/3. 1/3 - это уже она вот-вот реаллоцируется.
2. Про циклы - фигня. В питоне нет ни одного способа добавить элементы кучей так, чтобы на самом деле они добавились не в цикле на уровне Си. Даже dict comprehensions добавляют элементы последовательно.
3. Хэш от ключа - это встроенная функция hash(). Для конкретной ХТ берется остаток от деления хэша на размер ХТ. На самом деле, берётся хэш по битовой маске (размер_ХТ - 1) [Например hash(obj) & 2**16 - 1]. Но для степеней двойки эти операции равноценны.
4. Ты совсем забыл момент с разрешением коллизий (это когда хэши двух разных ключей совпадают). В питоновых словарях это самый интересный момент. И именно из-за него удаленные данные из словаря не удаляются физически до следующей реаллокации.
5. "Очень быстро" - это как?

UPD.

На текущий момент реализация словаря в питоне поменялась. В 3.6 версии сделали все словари по умолчанию ordered и заодно уменьшили размер словаря в байтах на 20-25%. Вот реализация актуальных словарей на питоне (в оригинале, она, конечно на Си):

Раскрыть
import array
import collections
import itertools

# Placeholder constants
FREE = -1
DUMMY = -2


class Dict(collections.MutableMapping):
    "Space efficient dictionary with fast iteration and cheap resizes."

    @staticmethod
    def _gen_probes(hashvalue, mask):
        "Same sequence of probes used in the current dictionary design"
        PERTURB_SHIFT = 5
        if hashvalue < 0:
            hashvalue = -hashvalue
        i = hashvalue & mask
        yield i
        perturb = hashvalue
        while True:
            i = (5 * i + perturb + 1) & 0xFFFFFFFFFFFFFFFF
            yield i & mask
            perturb >>= PERTURB_SHIFT

    def _lookup(self, key, hashvalue):
        "Same lookup logic as currently used in real dicts"
        assert self.filled < len(self.indices)  # At least one open slot
        freeslot = None
        for i in self._gen_probes(hashvalue, len(self.indices) - 1):
            index = self.indices[i]
            if index == FREE:
                return (FREE, i) if freeslot is None else (DUMMY, freeslot)
            elif index == DUMMY:
                if freeslot is None:
                    freeslot = i
            elif (
                self.keylist[index] is key
                or self.hashlist[index] == hashvalue
                and self.keylist[index] == key
            ):
                return (index, i)

    @staticmethod
    def _make_index(n):
        "New sequence of indices using the smallest possible datatype"
        if n <= 2 ** 7:
            return array.array("b", [FREE]) * n  # signed char
        if n <= 2 ** 15:
            return array.array("h", [FREE]) * n  # signed short
        if n <= 2 ** 31:
            return array.array("l", [FREE]) * n  # signed long
        return [FREE] * n  # python integers

    def _resize(self, n):
        """Reindex the existing hash/key/value entries.
           Entries do not get moved, they only get new indices.
           No calls are made to hash() or __eq__().

        """
        n = 2 ** n.bit_length()  # round-up to power-of-two
        self.indices = self._make_index(n)
        for index, hashvalue in enumerate(self.hashlist):
            for i in Dict._gen_probes(hashvalue, n - 1):
                if self.indices[i] == FREE:
                    break
            self.indices[i] = index
        self.filled = self.used

    def clear(self):
        self.indices = self._make_index(8)
        self.hashlist = []
        self.keylist = []
        self.valuelist = []
        self.used = 0
        self.filled = 0  # used + dummies

    def __getitem__(self, key):
        hashvalue = hash(key)
        index, i = self._lookup(key, hashvalue)
        if index < 0:
            raise KeyError(key)
        return self.valuelist[index]

    def __setitem__(self, key, value):
        hashvalue = hash(key)
        index, i = self._lookup(key, hashvalue)
        if index < 0:
            self.indices[i] = self.used
            self.hashlist.append(hashvalue)
            self.keylist.append(key)
            self.valuelist.append(value)
            self.used += 1
            if index == FREE:
                self.filled += 1
                if self.filled * 3 > len(self.indices) * 2:
                    self._resize(4 * len(self))
        else:
            self.valuelist[index] = value

    def __delitem__(self, key):
        hashvalue = hash(key)
        index, i = self._lookup(key, hashvalue)
        if index < 0:
            raise KeyError(key)
        self.indices[i] = DUMMY
        self.used -= 1
        # If needed, swap with the lastmost entry to avoid leaving a "hole"
        if index != self.used:
            lasthash = self.hashlist[-1]
            lastkey = self.keylist[-1]
            lastvalue = self.valuelist[-1]
            lastindex, j = self._lookup(lastkey, lasthash)
            assert lastindex >= 0 and i != j
            self.indices[j] = index
            self.hashlist[index] = lasthash
            self.keylist[index] = lastkey
            self.valuelist[index] = lastvalue
        # Remove the lastmost entry
        self.hashlist.pop()
        self.keylist.pop()
        self.valuelist.pop()

    def __init__(self, *args, **kwds):
        if not hasattr(self, "keylist"):
            self.clear()
        self.update(*args, **kwds)

    def __len__(self):
        return self.used

    def __iter__(self):
        return iter(self.keylist)

    def iterkeys(self):
        return iter(self.keylist)

    def keys(self):
        return list(self.keylist)

    def itervalues(self):
        return iter(self.valuelist)

    def values(self):
        return list(self.valuelist)

    def iteritems(self):
        return itertools.izip(self.keylist, self.valuelist)

    def items(self):
        return zip(self.keylist, self.valuelist)

    def __contains__(self, key):
        index, i = self._lookup(key, hash(key))
        return index >= 0

    def get(self, key, default=None):
        index, i = self._lookup(key, hash(key))
        return self.valuelist[index] if index >= 0 else default

    def popitem(self):
        if not self.keylist:
            raise KeyError("popitem(): dictionary is empty")
        key = self.keylist[-1]
        value = self.valuelist[-1]
        del self[key]
        return key, value

    def __repr__(self):
        return "Dict(%r)" % self.items()

    def show_structure(self):
        "Diagnostic method.  Not part of the API."
        print("=" * 50)
        print(self)
        print("Indices:", self.indices)
        for i, row in enumerate(zip(self.hashlist, self.keylist, self.valuelist)):
            print(i, row)
        print("-" * 50)


if __name__ == "__main__":
    d = Dict([("timmy", "red"), ("barry", "green"), ("guido", "blue")])
    d.show_structure()

Описание (возможно, понадобится VPN из-за выходок РКН)
Ответ написан
Пригласить эксперта
Ваш ответ на вопрос

Войдите, чтобы написать ответ

Похожие вопросы