Рекурсивный союз: как он работает на самом деле?

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

Это так; пример union в лекции выглядит следующим образом:

def union(other:BTSet) :BTSet = ((left union right) union other) incl element

Question1: Совершенно откровенно, даже прочитав соответствующие FAQ и другие темы форума, я до сих пор не понимаю, как и почему эта функция работает. В реализации объединения абсолютно нет "действия", кроме добавления (вызов incl) элемента в голове node, он просто называет себя снова и снова. Я был бы очень признателен за некоторые объяснения...

Question2: В форуме курса есть много сообщений, в которых говорится, что это решение неэффективно и что оно недостаточно. Видя, как я не понимаю, как это работает, я не совсем понимаю, почему это недостаточно.

ПОЖАЛУЙСТА, ОБРАТИТЕ ВНИМАНИЕ, что я никоим образом не требую спойлера для решения задания. Я более чем готов "выполнять работу за класс", но я просто не понимаю, что я должен здесь делать. Я не верю, что инструкции и рекомендации, приведенные в курсе, достаточны для того, чтобы обернуться вокруг причуд функционального программирования, поэтому я приветствую любые комментарии/ответы, которые касаются того, как правильно думать, а не как правильно кодировать.

Ответ 1

  A
 / \  union  D
B   C

((B union C) union D) incl A
  ^^^^^^^^^......................................assume it works

(  B             )
(    \   union D ) incl A
(     C          )

(((0 union C) union D) incl B) incl A
   ^^^^^^^^^.....................................just C

(((C union D) incl B) incl A
   ^^^^^^^^^.....................................expand

((((0 union 0) union D) incl C) incl B) incl A
    ^^^^^^^^^....................................just 0

(((0 union D) incl C) incl B) incl A
   ^^^^^^^^^.....................................just D

((D incl C) incl B) incl A
^^^^^^^^^^^^^^^^^^^^^^^^^^.......................all incl now

Просто запишите его шаг за шагом. Теперь вы видите, что объединение сводится к кучке включенных операторов, применяемых к правым аргументам.

Ответ 2

Я понимаю, что incl вставляет элемент в существующий набор? Если да, то там, где происходит вся настоящая работа.

Определение объединения - это множество, включающее все в любом наборе ввода. Если два набора сохранены как двоичные деревья, если вы берете союзы первого набора с ветвями второго, единственным элементом в том, что может отсутствовать результат, является элемент в корневом каталоге node второго дерева, поэтому, если вы вставляете этот элемент, у вас есть объединение обоих наборов ввода.

Это просто очень неэффективный способ вставки каждого элемента из обоих наборов в новый набор, который начинается пустым. Предположительно дубликаты отбрасываются incl, поэтому результатом является объединение двух входов.


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

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

Чтобы взять объединение двух множеств {1,2} и {2,3}, начнем с разложения первого множества на элемент 1 и подмножества {} и {2}. Мы рекурсивно используем объединение {}, {2} и {2,3} с использованием того же процесса, затем вставляем 1 в результат.

На каждом шаге проблема сводится от одной операции объединения к двум совместным операциям на меньших входах; стандартный алгоритм разделения и покоя. При достижении объединения одноточечного множества {x} и пустого множества {} объединение тривиально {x}, которое затем возвращается обратно в цепочку.

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

Ответ 3

Поэтому, основываясь на всех ответах выше, я думаю, что настоящая рабочая лошадка incl, а рекурсивный способ вызова union - это просто пройти через все элементы в наборах.

Я придумал следующую реализацию объединения, это лучше?

def union(other:BTSet) :BTSet = right union (left union (other incl element))

Ответ 4

  2
 / \  union  4
1   3

((1 union 3) union 4) incl 2
  ^^^^^^^^^......................................assume it works

(((E union E) union 3 incl 1) union 4) incl 2
   ^^^^^^^^^.....................................still E

(E union E) union 3 incl 1 = E union 3 incl 1 = 3 incl 1

Следующее поддерево должно быть 3, включая 1

(  3             ) 
(    \   union D ) incl 2
(      1         )


(((1 union E) union 4) incl 3) incl 2
   ^^^^^^^^^.......................................expand

(((( (E union E) union E) incl 1) union 4) incl 3) incl 2
      ^^^^^^^^^^^^^^^^^^^^^^^^^^..................still 1

((1 union 4) incl 3) incl 2
   ^^^^^^^^......................................continue

((((E union E) union 4) incl 1) incl 3) incl 2
   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^..........expand 1 union 4

((4 incl 1) incl 3) incl 2
  ^^^^^^^^^^^^^^^^^^^^^^^^^............Final union result 

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

Ответ 5

Вы не можете понять рекурсивные алгоритмы, если не посмотрите на базовый случай. На самом деле, зачастую ключ к пониманию заключается в понимании базового случая. Так как базовый случай не показан (возможно, потому, что вы его не заметили, в первую очередь) отсутствует понимание.

Ответ 6

Я делаю тот же курс, и приведенная выше реализация union оказалась крайне неэффективной.

Я придумал следующее не очень функциональное решение для создания объединения наборов бинарных деревьев, которое является более эффективным:

def union(that: BTSet): BTSet = {
  var result:BTSet = this
  that.foreach(element => result = result.incl(element))
  result
}