Как я могу проанализировать или улучшить мой простой алгоритм сжатия племянницы, основанный на коде Морзе?

У моей 8-летней племянницы вчера был урок по коду Морзе в школе, и ее назначение состояло в том, чтобы преобразовать различные фразы в код Морзе. Одна из фраз включала ее возраст, и вместо того, чтобы писать ---.., она написала 3-2., потому что (по ее словам), "это меньше написано именно так". Этот рудиментарный "алгоритм сжатия" вызвал мое любопытство, поэтому я написал немного кода для его реализации.

Однако мы сделали несколько изменений на этом пути. Я указал ей, что если вы написали только .....-----, нет никакого способа сказать, означал ли автор 50 или eeeeettttt. На самом деле между каждой буквой каждого слова и каждым словом происходит пауза, поэтому это не проблема, но наша схема не имела этого. Я вытащил какую-нибудь графическую бумагу и предложил заполнить код Морзе для каждого символа другим символом, чтобы облегчить кодирование и устранить неоднозначность из схемы. Мне понравилось использовать +, потому что "никто никогда не записывает их в предложениях". (Ой, я недавно закончил с математикой, но достаточно честный.)

Поскольку некоторые из нас пишут с помощью +, и все мы используем дефисы и периоды/точки, что противоречило бы с нашим стандартным определением кода Морзе, эти символы заменяются на p, h и d, соответственно. Конечно, это приводит нас к проблеме того, что делать с символами, которые не определены в нашем расширенном коде Морзе. Моя племянница хотела просто игнорировать их, так, что мы сделали. Для удобства хранения текстового сообщения с учетом регистра, буквы верхнего регистра не опускаются ниже в коде; они просто переносятся как есть и дополняются с помощью +.

Резюме алгоритма:

  • Коды Морзе имеют право на 5 символов с +
  • Мы расширили код Морзе, чтобы подставить p для +, d для . и h для -.
  • Символы, которые не определены в нашем "расширенном" морском коде, передаются без изменений.
  • Замены символов заменяются, если не встречается только один последовательный символ, и в этом случае число опущено.

Потенциальные ловушки:

  • Моя схема заполнения, вероятно, снижает эффективность сжатия.
  • Сжатие может быть улучшено за счет использования блоков размером более 5 символов.
  • Если бы моя племянница или я знали что-нибудь об алгоритмах сжатия, мы, вероятно, могли бы использовать это, чтобы сделать их успешными.
  • Это явно не подходит для производства, но поскольку для таких целей существует множество эффективных алгоритмов сжатия, я пока игнорирую эту проблему.
  • ???

Пример:

В нашем алгоритме "Hello, World" переводится на

H++++.++++.-..+.-..+---++,+++++++++W++++---++.-.++.-..+-..++

и сжимается до

H4+.4+.-2.+.-2.+3-2+,9+W4+3-2+.-.2+.-2.+-2.2+

Вот код Python, который я сбросил вместе:

#!/usr/bin/python3

import itertools
import string

class MorseString(str):
    def __init__(self, string):
        # or, pad values during iteration but this seems neater
        self._char_morse_map = {"a":".-+++", "b":"-...+", "c":"-.-.+", "d":"-..++", \
                                "e":".++++", "f":"..-.+", "g":"--.++", "h":"....+", \
                                "i":"..+++", "j":".---+", "k":"-.-++", "l":".-..+", \
                                "m":"--+++", "n":"-.+++", "o":"---++", "p":".--.+", \
                                "q":"--.-+", "r":".-.++", "s":"...++", "t":"-++++", \
                                "u":"..-++", "v":"...-+", "w":".--++", "x":"-..-+", \
                                "y":"-.--+", "z":"--..+", "1":".----", "2":"..---", \
                                "3":"...--", "4":"....-", "5":".....", "6":"-....", \
                                "7":"--...", "8":"---..", "9":"----.", "0":"-----",
                                " ":"+++++", ".":"d++++", "+":"p++++", "-":"h++++"}

        self._morse_char_map = dict()
        for letter, code in self._char_morse_map.items():
            self._morse_char_map[code] = letter

        self._string = string

        # convert the string to "Morse code". Could also use c.lower()
        self._morse_string = "".join([self._char_morse_map.get(c, c.ljust(5, "+")) for c in self._string])

    def compress(self):
        def grouper(n, k):
            return str(n) + k if n > 1 else k

        # could just use lambda
        return "".join([grouper(len(list(g)), k) for k, g in itertools.groupby(self._morse_string)])

    def decompress(self):
        i = 0
        start = 0
        chars = list()
        sc = self.compress()
        while i < len(sc):
            curr = sc[i]
            i += 1
            if not(curr in string.digits):
                num = 1 if start + 1 == i else int(sc[start:i-1])
                chars.append("".join(curr * num))
                start = i

        code = "".join(chars)
        chars = list()

        for i in range(0, len(code), 5):
            piece = "".join(code[i:i+5])
            chars.append(self._morse_char_map.get(piece, piece[0]))

        return "".join(chars)


def main():
    s0 = "Hello, World"
    ms0 = MorseString(s0)
    print(ms0._morse_string)
    print(ms0.compress())
    assert(s0 == ms0.decompress())

    s1 = "Hello  2 world."
    ms1 = MorseString(s1)
    assert(s1 == ms1.decompress())

    s2 = "The quick brown fox jumped over the lazy dog."
    ms2 = MorseString(s2)
    assert(s2 == ms2.decompress())

    s3 = "abc  -.+"
    ms3 = MorseString(s3)
    ms3
    assert(s3 == ms3.decompress())

if __name__ == "__main__":
    main()

Каковы некоторые простые методы, которые: а) улучшат наш алгоритм и б) относительно легко объяснить моей 8-летней племяннице? Хотя последний момент явно субъективен, я все же стараюсь побаловать свое любопытство как можно больше.

Я приветствую любые улучшения кода, так как он не структурирован ужасно хорошо (я довольно уверен, что он структурирован довольно плохо, на самом деле, но это быстро и грязно), хотя это строго для меня, так как я не получил моя племянница использует Python (YET).

Update

Вот обновленная версия кода, которая пытается включить модификацию пользователя1884905 в алгоритм и улучшения Karl для самого кода.

import itertools
import string

_char_morse_map = {"a":".-", "b":"-...", "c":"-.-.", "d":"-..", \
                   "e":".", "f":"..-.", "g":"--.", "h":"....", \
                   "i":"..", "j":".---", "k":"-.-", "l":".-..", \
                   "m":"--", "n":"-.", "o":"---", "p":".--.", \
                   "q":"--.-", "r":".-.", "s":"...", "t":"-", \
                   "u":"..-", "v":"...-", "w":".--", "x":"-..-", \
                   "y":"-.--", "z":"--..", "1":".----", "2":"..---", \
                   "3":"...--", "4":"....-", "5":".....", "6":"-....", \
                   "7":"--...", "8":"---..", "9":"----.", "0":"-----",
                   " ":"", ".":"d", "+":"p", "-":"h"}

_morse_char_map = {
    code: letter
    for letter, code in _char_morse_map.items()
}


def encode(s):
    return "".join(_char_morse_map.get(c, c) + "+" for c in s)

def decode(encoded):
    return "".join(decode_gen(encoded))

def decode_gen(encoded):
    word = ""
    for c in encoded:
        if c != "+":
            word += c
        else:
            yield _morse_char_map.get(word, word) if word != "" else " "
            word = ""

def compress(s):
    def grouper(n, k):
        return str(n) + k if n > 1 else k

    return "".join(grouper(len(list(g)), k) for k, g in itertools.groupby(s))

def decompress(compressed):
    return "".join(decompress_gen(compressed))

def decompress_gen(compressed):
    digits = ""
    for c in compressed:
        if c in string.digits:
            digits += c
        else:
            number = int(digits) if digits else 1
            yield "".join(c * number)
            digits = ""

Ответ 1

Я бы не стал заниматься этим; как есть, вы заново создаете сопоставления для каждой строки. Этого все еще можно избежать с помощью класса, но на самом деле проще просто настроить эти сопоставления, а затем написать простую функцию для кодирования строки. "Строка кода Морзе" на самом деле принципиально не отличается от простой строки, а привязка функций сжатия и декомпрессии к ней на самом деле не имеет никакого смысла. Просто напишите кучу функций; беспокоиться о ООП, когда у вас есть действительно значимая абстракция.

Как написано, ваша функция декомпрессии не имеет смысла; сжатие не является частью распаковки, и вы комбинировали декомпрессию кода Морзе с декодированием обратно к простой строке в той же функции. Это беспорядок.


self._morse_char_map = dict()
for letter, code in self._char_morse_map.items():
    self._morse_char_map[code] = letter

Это более аккуратно написано с пониманием dict:

self._morse_char_map = {
    code: letter
    for letter, code in self._char_morse_map.items()
}

"".join([...])

Квадратные скобки здесь не нужны; просто подайте выражение генератора на join вместо этого и воспользуйтесь специальным синтаксическим правилом.


chars = list()

Это более аккуратно написано chars = [], но попробуйте улучшить это на более высоком уровне...


while i < len(sc):
    curr = sc[i]
    i += 1
    if not(curr in string.digits):
        num = 1 if start + 1 == i else int(sc[start:i-1])
        chars.append("".join(curr * num))
        start = i

Метод: вместо того, чтобы устанавливать пустой список и повторно накапливать вещи на ''.join вместе, напишите генератор и пусть вместо этого будет передан результат. Это становится легче, если вы правильно разделили логику на свою собственную функцию:

def decompress(compressed):
    return ''.join(decompress_gen(compressed))


def decompress_gen(compressed):
    start = 0
    i = 0
    while i < len(compressed):
        curr = compressed[i]
        i += 1
        if not(curr in string.digits):
            num = 1 if start + 1 == i else int(compressed[start:i-1])
            yield "".join(curr * num)
            start = i

Теперь, очевидно, мы действительно хотим просто перебрать символы compressed с помощью цикла for; ручное увеличение индекса выглядит очень ужасно. Чтобы сделать эту работу, нам нужно посмотреть на данные персонажа за раз и вспомнить любые части числа, которые мы уже видели. Мы могли бы сделать арифметику по мере того, как мы идем, но сохраним использование int, вместо этого создавая буфер символов, которые являются частью count:

def decompress_gen(compressed):
    number_digits = ''
    for char in compressed:
        if char in string.digits:
            number_digits += char
        else:
            number = int(number_digits) if number_digits else 1
            yield "".join(char * number)
            number_digits = ''

chars = list()

for i in range(0, len(code), 5):
    piece = "".join(code[i:i+5])
    chars.append(self._morse_char_map.get(piece, piece[0]))

return "".join(chars)

В этот момент code - строка, поэтому ''.join не требуется при создании piece. Но опять же, мы могли бы просто использовать генераторы (ну, генераторные выражения) здесь:

return ''.join(
    self._morse_char_map.get(piece, piece[0])
    for piece in (
        code[i: i + 5]
        for i in range(0, len(code), 5)
    )        
)

Ответ 2

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

Это приведет к тому, что все числа 0-9 будут длиннее одного символа, но сделайте несколько обычных букв короче.

Например, a, e, i и t станут .-+, .+, ..+ и -+ вместо .-+++, .++++, ..+++ и -++++. (И space можно обозначить + вместо +++++)

Таким образом, ваш пример Hello, World станет следующим:

H+.+.-..+.-..+---+,++W+---+.-.+.-..+-..+

вместо

H++++.++++.-..+.-..+---++,+++++++++W++++---++.-.++.-..+-..++

и сжимайте до

H+.+.-2.+.-2.+3-+,2+W+3-+.-.+.-2.+-2.+

вместо

H4+.4+.-2.+.-2.+3-2+,9+W4+3-2+.-.2+.-2.+-2.2+

Понимая это рассуждение, кодировка Хаффмана будет казаться естественным следующим шагом, где основная идея состоит в том, чтобы заставить наиболее распространенные символы занимать как можно меньше места.

Edit:

См. также это изображение википедии таблица дихотоматического поиска, показывающая наиболее часто встречающиеся символы, близкие к вершине дерева.

Morse code tree