Сложность реализации дерева Хаффмана в Haskell

Я пытаюсь выучить Haskell, но мне это очень сложно, а онлайн-ресурсов не так много. Кажется, у меня есть серьезное непонимание того, как должны выглядеть рекурсивные вызовы, и я был бы признателен за указание в правильном направлении. Я пытаюсь взять дерево и вернуть каждый конечный узел с хранящимся там символом, а также путь, по которому туда добраться. (Таким образом, ввод (Fork (Leaf x) (Leaf y)) будет иметь вывод [(x,[False]),(y,[True])] ). Мой код выглядит так:

data htree a = Leaf a | Fork (htree a) (htree a) deriving (Show, Eq)

encode :: htree a -> [(a, [Bool])]
encode (Leaf a) = [(a, ????)]

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


person napqueen6078    schedule 24.10.2019    source источник
comment
Вы выбрали довольно нетривиальный пример, чтобы понять, как работает рекурсия. Попробуйте для начала пройтись по спискам.   -  person Fyodor Soikin    schedule 24.10.2019
comment
На самом деле я довольно хорошо знаю, как перемещаться по спискам, и написал много функций со списками. Я понимаю основы такого мышления, я просто не могу применить его ни к чему, что расстраивает.   -  person napqueen6078    schedule 24.10.2019
comment
there aren't a lot of online resources Какой тип ресурса вы считаете наиболее полезным? Многие люди рекомендуют Learn you a Haskell. Это бесплатно онлайн. Вот глава о рекурсии: learnyouahaskell.com/recursion   -  person Garrison    schedule 24.10.2019


Ответы (2)


Рассмотрим Fork. У него есть два поддерева, и каждое из этих поддеревьев имеет некоторую кодировку.

Допустим, кодировка левого поддерева:

[(x, pathToX), (y, pathToY)]

И допустим, что правая кодировка поддерева:

[(a, pathToA), (b, pathToB)]

Теперь вы видите, какой должна быть кодировка всего форка? Это должно быть так:

[(a, True : pathToA), (b, True : pathToB), (x, False : pathToX), (y, False : pathToY)]

Согласны ли вы с этим? Если нет, то подумайте. Может быть, поработать над некоторыми небольшими примерами. Пока вы не согласитесь, что это так.

Видишь, что я там сделал? Я добавил False к каждому пути в левом поддереве, а затем добавил True к каждому пути в правом поддереве.

Давайте запишем это в синтаксисе Haskell:

encode (Fork left right) = prependToEach False (encode left) ++ prependToEach True (encode right)

Теперь вы, возможно, заметили, что здесь я сжульничал: я использую функцию prependToEach, которой не существует. Ну ничего, давайте определимся!

prependToEach x list = map (prepend x) list

Видеть? Добавление чего-либо к каждому элементу списка — это просто отображение функции добавления одного элемента в список.

Но я, конечно, опять схалтурил: такой функции, как prepend, пока нет. Так пусть будет один!

prepend x (a, path) = (a, x : path)

И вот! Теперь осталось только определить базовый вариант: каким должен быть путь Leaf? Что ж, согласно приведенному вами примеру, каждый Leaf будет иметь пустой путь, отражающий тот факт, что вам не нужно делать никаких ходов, чтобы перейти от этого листа к тому же листу:

encode (Leaf a) = [(a, [])]

А теперь, собирая все вместе:

encode :: HTree a -> [(a, [Bool])]
encode (Leaf a) = [(a, [])]
encode (Fork left right) = prependToEach False (encode left) ++ prependToEach True (encode right)
    where
    prependToEach x list = map (prepend x) list
    prepend x (a, path) = (a, x : path)

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

encode :: HTree a -> [(a, [Bool])]
encode (Leaf a) = [(a, [])]
encode (Fork left right) = [(x, False : p) | (x, p) <- encode left] ++ [(x, True : p) | (x, p) <- encode right]

P.S. обратите внимание, что тип не может называться htree, потому что имена типов в Haskell должны быть написаны с заглавной буквы. Вы можете заметить, что в последнем фрагменте я переименовал его в HTree.

person Fyodor Soikin    schedule 24.10.2019
comment
Данг, это ОЧЕНЬ информативно. Большое спасибо! У меня есть вопрос, если вы не возражаете ответить: почему в базовом случае есть внешние скобки, а в рекурсивном - нет? Это может показаться непонятным вопросом, но мне все еще трудно понять определенный синтаксис Haskell (буквально на днях вы помогли мне с кодом, который я правильно продумал, но не смог правильно понять синтаксис, и я хотелось бы избежать таких ситуаций в будущем). Еще раз спасибо! - person napqueen6078; 24.10.2019
comment
И базовый, и рекурсивный случаи возвращают список. В базовом случае список строится с использованием синтаксиса квадратных скобок. В рекурсивном случае список создается путем объединения двух других списков с помощью оператора ++. - person Fyodor Soikin; 24.10.2019

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

Причина в том, что (список из N элементов) ++ (список из 1 элемента) должен создать совершенно новый список из N+1 элементов. Таким образом, добавление только нескольких элементов за раз может быть медленным.

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

Вот пример функции encode, использующей этот метод:

data HTree a = Leaf a | Fork (HTree a) (HTree a) deriving (Show, Eq)

encode :: HTree a -> [(a, [Bool])]
encode tree = go [] tree [] where
  go :: [Bool] -> HTree a -> [(a, [Bool])] -> [(a, [Bool])]
  go path (Leaf leaf) = ((leaf, path):)
  go path (Fork left right) = go (False:path) left . go (True:path) right

Обратите внимание, что вам на самом деле не нужны подписи типов, я просто включил их для ясности (и, вероятно, это хорошая практика), но удалил только 3 строки:

encode tree = go [] tree [] where
  go path (Leaf leaf) = ((leaf, path):)
  go path (Fork left right) = go (False:path) left . go (True:path) right

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

person Clinton    schedule 24.10.2019
comment
Объединение списков действительно требует перераспределения всего списка в языках с аппликативным порядком вычислений, таких как Scala, F#, Ocaml и почти во всех других языках. Однако в Haskell из-за обычного порядка вычисления конкатенация не выполняет перераспределение до тех пор, пока вы не форсируете результат, и даже в этом случае перераспределение происходит только один раз для всего результата, а не один раз для каждой конкатенации. - person Fyodor Soikin; 25.10.2019