смущает проблема с наибольшим диаметром двоичного дерева

#python #algorithm #binary-tree #depth-first-search

#python #алгоритм #двоичное дерево #поиск в глубину

Вопрос:

Не уверен, как это работает…

 class TreeNode:
  def __init__(self, val, left=None, right=None):
    self.val = val
    self.left, self.right = left, right

  def find_diameter(self, root):
    self.calculate_height(root)
    return self.treeDiameter

  def calculate_height(self, currentNode):
    if currentNode is None:
      return 0

    leftTreeDiameter = self.calculate_height(currentNode.left)
    rightTreeDiameter = self.calculate_height(currentNode.right)

    diameter = leftTreeDiameter   rightTreeDiameter   1
    self.treeDiameter = max(self.treeDiameter, diameter)

    return max(leftTreeDiameter, rightTreeDiameter)   1
 

Приведенный выше код работает для получения максимального диаметра двоичного дерева, но я не понимаю последнюю строку calculate_height . Зачем нам нужно возвращать max(leftTreeDiameter, rightTreeDiameter) 1

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

Единственное место, которое, кажется, добавляет что-либо, кроме 0, — это последняя строка кода, calculate_height потому что, хотя мы добавляем leftTreeDiameter rightTreeDiameter 1 , чтобы получить общий диаметр, это возможно только из-за return 0 и return max(leftTreeDiameter, rightTreeDiameter) 1 правильно?

Кроме того, я не понимаю, почему можно назначить self.calculate_height(currentNode.left) leftTreeDiameter . Я имею в виду, что я думал, что мне понадобится что-то вроде…

 def calculate_left_height(self, currentNode, height=0):
  if currentNode is None:
    return 0

  self.calculate_height(currentNode.left, height   1)
  
  return height
 

где мы просто добавляем 1 к высоте каждый раз. В этом случае вместо того, чтобы делать что-то вроде leftTreeDiameter = self.calculate_height(currentNode.left) , я просто передаю в качестве аргумента height 1 каждый раз, когда мы видим узел.

но если я сделаю это, мне понадобится отдельный метод только для вычисления правильной высоты, и в моем find_diameter методе потребуется рекурсивный вызов find_diameter с обоими root.left , а также с root.right .

Где моя логика неверна и как это calculate_height работает на самом деле. Я думаю, у меня возникли проблемы, пытаясь понять, как отслеживать стек?

Комментарии:

1. Что такое «диаметр» дерева?

2. не уверен, смогу ли я это хорошо объяснить, но на родительском узле, если вы перейдете к левому дочернему элементу, а затем от этого дочернего элемента переходите влево и влево и так далее и делайте то же самое с родительского узла, но справа считайте каждый раз, когда вы видите узел, и добавляйте 1 для родительского узла. Вот каков диаметр для этого вопроса. Если есть поддерево с большим диаметром, чем этот диаметр, это тот, который рассматривается. Имеет ли это смысл?

3. Возможно, ссылка на проблему была бы лучшей…

4. Извините, это платный онлайн-курс, поэтому я не могу просто предоставить ссылку, но я думаю, что @trincot прояснил это для меня

Ответ №1:

Имена, используемые в этом коде, сбивают с толку: leftTreeDiameter и rightTreeDiameter не диаметры, а высоты.

Во-вторых, функция calculate_height имеет побочные эффекты, что не очень приятно. С одной стороны, он возвращает высоту и одновременно присваивает диаметр. Это сбивает с толку. Многие программисты на Python предпочли бы, чтобы функция была чистой и просто возвращала что-то, ничего не изменяя. Или, альтернативно, функция может только изменять некоторое состояние и не возвращать его. Выполнение обоих может сбить с толку.

Кроме того, сбивает с толку то, что, хотя класс вызывается TreeNode , его find_diameter метод по-прежнему требует node в качестве аргумента. Это противоречит интуиции. Мы ожидаем, что метод будет использоваться в self качестве узла для действия, а не аргумента.

Но давайте просто переименуем переменные и добавим несколько комментариев:

 leftHeight = self.calculate_height(currentNode.left)
rightHeight = self.calculate_height(currentNode.right)

# What is the size of the longest path from leaf-to-leaf 
#   whose top node is the current node?
diameter = leftHeight   rightHeight   1
# Is this path longer than the longest path that we
#   had found so far? If so, take this one.
self.treeDiameter = max(self.treeDiameter, diameter)
# The height of the tree rooted at the current node
#   is the height of the highest childtree (either left or right), 
#   with one added to account for the current node
return max(leftHeight, rightHeight)   1
 

Это должно быть ясно, но поймите, что self в этом процессе всегда есть экземпляр, для которого find_diameter вызывается метод, и на самом деле он не играет роли фактического узла, поскольку корень передается в качестве аргумента. Таким образом, повторное присвоение self.treeDiameter всегда одному и тому же свойству. Это свойство создается не на каждом узле… только на узле, на котором вы вызываете find_diameter .

Я надеюсь, что вставленные комментарии прояснили, как работает этот алгоритм.

ПРИМЕЧАНИЕ: ваша собственная идея создания calculate_left_height этого не сделает: она никогда не изменяет значение height , которое она получает в качестве аргумента, и в конечном итоге возвращает его. Таким образом, он возвращает то же значение, которое он уже получил. Это, очевидно, мало что даст…

Комментарии:

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

2. Вы должны смотреть на это «снизу вверх». Итак, начните с самого глубокого уровня рекурсии, где высота равна 0. Затем вы возвращаетесь назад, … и возвращаетесь назад. Таким образом, вы всегда знаете высоту дочерних элементов, прежде чем определять высоту самого узла. Теперь, какова высота (дерева в) узле? Если у него есть короткое поддерево слева и высокое поддерево справа, то, безусловно, высота левого поддерева стала неактуальной. Затем общая высота полностью определяется правой стороной. Следовательно: макс.

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

4. Когда высота левого поддерева корня равна 2, а правого поддерева равна 1, тогда высота (!!!) корня определяется не правым поддеревом, а только левым поддеревом. Высота корня равна 2 1 = 3 (не 4). Другое дело — диаметр . Для этого вычисление отличается. Он суммирует высоты и добавляет 1 для самого корня: 2 1 1 =4. Там важны обе высоты, они добавляются. Пожалуйста, поймите, что вычисления для высоты и диаметра совершенно разные. Не путайте их.

5. Да, хотя это менее запутанно, если вы используете индукцию : предположим, что рекурсивный вызов возвращает правильный результат для двух дочерних элементов. Затем посмотрите, как это означает, что высота также правильно вычисляется для текущего узла и возвращается. Итак, … если базовый вариант рассчитан правильно (а это так: 0 — правильная высота для конечного узла), то по индукции все остальные высоты тоже вычисляются правильно.