Как я могу проанализировать или улучшить мой простой алгоритм сжатия племянницы, основанный на коде Морзе?
У моей 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]()