Как закодировать строку с помощью алгоритма Хаффмана

Михаил Попов    17.12.2015 23:02    Алгоритмы     нет комментариев

Коды Хаффмана

Сегодня рассмотрим задачу по кодированию строки по алгоритму Хаффмана и ее декодированию. Также в ходе решения задачи будет рассмотрен тест алгоритма, позволяющий оценить его корректность. Условие задачи:

По данной непустой строке $%s$% длины не более $%10^4$%, состоящей из строчных букв латинского алфавита, постройте оптимальный беспрефиксный код. В первой строке выведите количество различных букв $%k$%, встречающихся в строке, и размер получившейся закодированной строки. В следующих $%k$% строках запишите коды букв в формате $%"letter: code"$%. В последней строке выведите закодированную строку.
Sample Input:
abacabad

Sample Output:
4 14
a: 0
b: 10
c: 110
d: 111
01001100100111

Алгоритм Хаффмана основан на использовании информации о частотах вхождения символов в строку. В рассмотренном ниже примере будет показано:

  • кодирование символов на основании частоты
  • получение кодированной строки
  • декорирование строки
  • тестирование алгоритма

Для алгоритма со временем работы $%O(nlogn)$% будем использовать очередь с приоритетами. Кому лень читать все что написано ниже, можно просто посмотреть визуализацию. Наже приведен алгоритм кодирования на Python:

import heapq  # модуль для работы с мин. кучей из стандартной библиотеки Python
from collections import Counter  # словарь в котором для каждого объекта поддерживается счетчик
from collections import namedtuple

# добавим классы для хранения информации о структуре дерева
# воспользуемся функцией namedtuple из стандартной библиотеки
class Node(namedtuple("Node", ["left", "right"])):  # класс для ветвей дерева - внутренних узлов; у них есть потомки
    def walk(self, code, acc):
        # чтобы обойти дерево нам нужно:
        self.left.walk(code, acc + "0")  # пойти в левого потомка, добавив к префиксу "0"
        self.right.walk(code, acc + "1")  # затем пойти в правого потомка, добавив к префиксу "1"

class Leaf(namedtuple("Leaf", ["char"])):  # класс для листьев дерева, у него нет потомков, но есть значение символа
    def walk(self, code, acc):
        # потомков у листа нет, по этому для значения мы запишем построенный код для данного символа
        code[self.char] = acc or "0"  # если строка длиной 1 то acc = "", для этого случая установим значение acc = "0"

def huffman_encode(s):  # функция кодирования строки в коды Хаффмана
    h = []  # инициализируем очередь с приоритетами
    for ch, freq in Counter(s).items(): # постоим очередь с помощью цикла, добавив счетчик, уникальный для всех листьев
        h.append((freq, len(h), Leaf(ch)))  # очередь будет представлена частотой символа, счетчиком и самим символом
    heapq.heapify(h)  # построим очередь с приоритетами
    count = len(h) # инициализируем значение счетчика длиной очереди
    while len(h) > 1:  # пока в очереди есть хотя бы 2 элемента
        freq1, _count1, left = heapq.heappop(h)  # вытащим элемент с минимальной частотой - левый узел
        freq2, _count2, right = heapq.heappop(h)  # вытащим следующий элемент с минимальной частотой - правый узел
        # поместим в очередь новый элемент, у которого частота равна суме частот вытащенных элементов
        heapq.heappush(h, (freq1 + freq2, count, Node(left, right))) # добавим новый внутренний узел у которого
                                                                     # потомки left и right соответственно
        count += 1  # инкрементируем значение счетчика при добавлении нового элемента дерева
    code = {}  # инициализируем словарь кодов символов
    if h:  # если строка пустая, то очередь будет пустая и обходить нечего
        [(_freq, _count, root)] = h  # в очереди 1 элемент, приоритет которого не важен, а сам элемент - корень дерева
        root.walk(code, "")  # обойдем дерева от корня и заполним словарь для получения кодирования Хаффмана
    return code  # возвращаем словарь символов и соответствующих им кодов

def main():
    s = input()  # читаем строку длиной  до 10**4
    code = huffman_encode(s)  # кодируем строку
    encoded = "".join(code[ch] for ch in s)  # отобразим закодированную версию, отобразив каждый символ
                                             # в соответствующий код и конкатенируем результат
    print(len(code), len(encoded))  # выведем число символов и длину закодированной строки
    for ch in sorted(code): # обойдем символы в словаре в алфавитном порядке с помощью функции sorted()
        print("{}: {}".format(ch, code[ch]))  # выведем символ и соответствующий ему код
    print(encoded)  # выведем закодированную строку

if __name__ == "__main__":
    main()

Теперь можем выполнить проверку нашего алгоритма, для этого напишем функцию декодирования строки и функцию тестирования. В функции тестирования будем генерировать строку произвольных символов произвольной длины.


def huffman_decode(encoded, code):  # функция декодирования исходной строки по кодам Хаффмана
    sx =[]  # инициализируем массив символов раскодированной строки
    enc_ch = ""  # инициализируем значение закодированного символа
    for ch in encoded:  # обойдем закодированную строку по символам
        enc_ch += ch  # добавим текущий символ к строке закодированного символа
        for dec_ch in code:  # постараемся найти закодированный символ в словаре кодов
            if code.get(dec_ch) == enc_ch:  # если закодированный символ найден,
                sx.append(dec_ch)  # добавим значение раскодированного символа к массиву раскодированной строки
                enc_ch = ""  # обнулим значение закодированного символа
                break
    return "".join(sx)  # вернем значение раскодированной строки


def test(n_iter=100):  # добавим тест для проверки алгоритма
    import random  # нам понадобится генератор случайных чисел
    import string  # модуль для работы со строками, чтобы получить значения символов по их коду

    # сгененрируем строку из ascii-символов
    for i in range(n_iter):  # тест включает краевые случаи с пустой строкой и строкой из 1 символа
        length = random.randint(0, 32)  # сгеренируем код символа
        s = "".join(random.choice(string.ascii_letters) for _ in range(length)) # получим символ по коду и добавим к строке
        code = huffman_encode(s)  # выполним кодирование строки
        encoded = "".join(code[ch] for ch in s)  # получим закодированную строку
        assert huffman_decode(encoded, code) == s  # раскодируем строку и сравним ее с исходной строкой

P.S. Еще, если вы не заметили, я сделал пагинацию страничек моего блога по 5 статей на страничку, так как статей уже довольно много и они начинают ощутимо по времени грузиться. Также добавил немного снега в преддверии новогодних праздников. Надеюсь вам нравится.

Курс по алгоритмам пройден Наибольшая невозрастающая подпоследовательность

0     0

blog comments powered by Disqus