Diameter of Binary Tree
Du erhältst einen Binärbaum, der im Array tree in Ebenenreihenfolge gespeichert ist. Die Wurzel befindet sich am Index 0, die Kinder des Knotens am Index i befinden sich bei 2*i+1 (links) und 2*i+2 (rechts), -1 kennzeichnet eine leere Stelle, und das Array kann mit zusätzlichen Einträgen -1 enden. Gib den Durchmesser des Baums zurück: die Anzahl der Kanten auf dem längsten Pfad zwischen zwei beliebigen Knoten. Der Pfad kann durch die Wurzel verlaufen oder innerhalb eines Teilbaums bleiben.
Funktion
- treeinteger-array
- der Binärbaum in Ebenenreihenfolge, wobei -1 eine leere Stelle kennzeichnet
- Gibt zurückinteger
- die Anzahl der Kanten auf dem längsten Pfad zwischen zwei Knoten
Einschränkungen
1 ≤ tree.length ≤ 32767- Jedes
tree[i]ist-1oder ein Wert mit0 ≤ tree[i] ≤ 1000. tree[0]ist niemals-1, daher hat der Baum mindestens einen Knoten.- Das Array kann nach dem letzten Knoten noch zusätzliche
-1-Einträge enthalten. - Beide Kinder einer leeren Stelle sind ebenfalls leer, und die Tiefe beträgt höchstens
14.
Beispiele
- Eingabe
- tree = [8, 3, 6, 1, 4, -1, -1, -1, -1, 7]
- Ausgabe
- 4
- Erklärung
- Der Pfad
7,4,3,8,6(Indizes9,4,1,0,2) enthält fünf Knoten, die durch vier Kanten verbunden sind. Er biegt an der Wurzel ab: drei Kanten nach unten auf der linken Seite und eine nach unten auf der rechten Seite.
- Eingabe
- tree = [2, 5, -1, 1, 9, -1, -1, 3, -1, -1, 4]
- Ausgabe
- 4
- Erklärung
- Der Pfad
3,1,5,9,4hat vier Kanten und biegt beim5am Index1ab. Die Wurzel hat kein rechtes Kind, daher umfasst ein Pfad durch die Wurzel nur die drei Kanten auf ihrer linken Seite.
- Eingabe
- tree = [6, -1, -1]
- Ausgabe
- 0
- Erklärung
- Ein einzelner Knoten hat keine Kanten. Der längste Pfad besteht nur aus dem Knoten und hat die Länge
0.
+12 versteckte Tests beim Einreichen
Weiterführende Frage
Wie würdest du den Pfad selbst zurückgeben, also die Knotenwerte von einem Ende des Durchmessers zum anderen?
Tipps
Öffne sie nacheinander. Jeder verrät ein bisschen mehr.
Jeder Pfad in einem Baum hat einen höchsten Knoten, an dem er vom Aufwärts- zum Abwärtsgehen wechselt. Wenn du diesen Knoten kennen würdest, wie lang könnte der Pfad durch ihn sein?
Ein Pfad, der am Knoten
iabbiegt, führt hinunter in den linken Teilbaum und hinunter in den rechten. Im besten Fall entspricht seine Länge der Höhe des linken Kindes plus der Höhe des rechten Kindes, wobei eine Höhe die Knoten auf dem längsten nach unten führenden Pfad zählt und eine leere Stelle die Höhe0hat.Berechne die Höhen von unten nach oben in einem Post-Order-Durchlauf: Die Höhe eines Knotens ist
1 + max(left, right). Während du an einem Knotenleftundrightvorliegen hast, aktualisiere die Antwort mitleft + right.
Lösung
Der längste Pfad muss nicht durch die Wurzel verlaufen, daher reicht es nicht aus, die beiden Seiten der Wurzel zu messen. Jeder Pfad hat einen höchsten Knoten, an dem er von aufwärts zu abwärts wechselt, und der längste Pfad, der an einem Knoten abbiegt, ist die Höhe seines linken Teilbaums plus die Höhe seines rechten Teilbaums. Ein einziger Postorder-Durchlauf berechnet alle Höhen von unten nach oben und prüft dabei jeden möglichen Wendepunkt, in O(n).
Miss jedes Knotenpaar
Korrekt, wird aber bei den größten Tests nicht fertig
Idee
Zuerst geht es darum, wie du dich im Array bewegst. Der Knoten am Index i hat sein linkes Kind an 2*i+1 und sein rechtes Kind an 2*i+2, sein Elternknoten befindet sich also an (i-1)/2, abgerundet. Eine Position ist nur dann tatsächlich belegt, wenn ihr Index innerhalb des Arrays liegt und der dortige Wert nicht -1 ist. In [8, 3, 6, 1, 4, -1, -1, -1, -1, 7] hat die 7 am Index 9 ihren Elternknoten am Index 4, und diese 4 hat ihren Elternknoten am Index 1.
Der Durchmesser ist der größte Abstand zwischen zwei Knoten, du kannst also jedes Paar messen. Um den Abstand zwischen den Indizes a und b zu ermitteln, gehst du Schritt für Schritt in Richtung Wurzel, bis sie sich treffen, und zwar immer vom größeren Index aus. Ein größerer Index liegt niemals auf einer höheren Ebene, daher überschreitet dieser Schritt niemals den Treffpunkt. Die Anzahl der Schritte entspricht der Anzahl der Kanten. Für 9 und 2: Die 9 geht zum 4 und dann zum 1, die 2 geht zum 0, und die 1 geht zum 0. Vier Schritte.
Das ist korrekt, aber langsam. Der größte Test ist ein vollständiger Baum mit 16383 Knoten. Dabei entstehen etwa 1.3 × 10^8 Paare, und jedes Paar erfordert bis zu 26 Schritte. Milliarden von Schritten für eine einzige Antwort liegen weit über dem Zeitlimit.
Algorithmus
- Sammle die Indizes aller echten Knoten.
- Setze für jedes Paar
(a, b)edges = 0und wiederhole, bisa == b: Ersetze den größeren Index durch seinen Elternknoten und addiere1zuedges. - Behalte den größten Wert für
edges, den du siehst, und gib ihn zurück.
def diameterOfBinaryTree(tree):
nodes = [i for i, value in enumerate(tree) if value != -1]
best = 0
for x in range(len(nodes)):
for y in range(x + 1, len(nodes)):
a, b = nodes[x], nodes[y]
edges = 0
while a != b:
# A larger index is never higher up, so climb from it.
if a > b:
a = (a - 1) // 2
else:
b = (b - 1) // 2
edges += 1
best = max(best, edges)
return bestMiss beide Höhen an jedem Knoten
Idee
Betrachte den längsten Pfad von seinem höchsten Knoten aus, also dem Knoten, an dem es nicht mehr nach oben, sondern nach unten geht. Von dort geht er so weit wie möglich die linke Seite hinunter und so weit wie möglich die rechte Seite hinunter. Sei height(c) die Anzahl der Knoten auf dem längsten absteigenden Pfad von c, wobei für eine leere Stelle 0 gilt. Dann hat der längste Pfad, der sich am Knoten i wendet, height(2*i+1) + height(2*i+2) Kanten, eine Kante für jeden dieser Knoten.
Probiere also jeden Knoten als Wendepunkt aus und merke dir das beste Ergebnis. Im zweiten Beispiel hat die 5 am Index 1 links die Höhe 2 (1, 3) und rechts ebenfalls 2 (9, 4), also ergibt sich ein Pfad mit vier Kanten. Die Wurzel hat links die Höhe 3 und rechts 0, was nur drei ergibt.
Jeder Aufruf von height durchläuft einen ganzen Teilbaum, und ein Knoten wird für jeden seiner Vorfahren erneut durchlaufen. Der Aufwand beträgt also O(n·h). Bei h ≤ 14 ist das hier schnell genug, aber bei einem kettenförmigen Zeigerbaum kann h bis auf n anwachsen, und dieselbe Idee kostet dann O(n²). Die wiederholten Aufrufe von height sind der unnötige Aufwand, den der letzte Ansatz beseitigt.
Algorithmus
- Schreibe
height(i):0für einen leeren Platz, andernfalls1 + max(height(2*i+1), height(2*i+2)). - Berechne für jeden echten Knoten
iheight(2*i+1) + height(2*i+2). - Gib die größte dieser Summen zurück.
def diameterOfBinaryTree(tree):
n = len(tree)
def height(i):
# Nodes on the longest downward path from i; an empty spot has 0.
if i >= n or tree[i] == -1:
return 0
return 1 + max(height(2 * i + 1), height(2 * i + 2))
best = 0
for i in range(n):
if tree[i] != -1:
# The longest path that turns at node i goes down both sides.
best = max(best, height(2 * i + 1) + height(2 * i + 2))
return bestEin Postorder-Durchlauf über die Höhen
Idee
Die Höhe eines Knotens hängt nur von den Höhen seiner beiden Kinder ab, und genau diese beiden Zahlen benötigt auch die Prüfung auf den Wendepunkt. Berechne sie also einmal von unten nach oben. Bei einer Postorder-Traversierung werden beide Kinder vor ihrem Elternknoten verarbeitet. An jedem Knoten hast du dann left und right: Aktualisiere die Antwort mit left + right und gib 1 + max(left, right) an den Elternknoten weiter.
Im ersten Beispiel gibt das Blatt 7 den Wert 1 zurück, der darüberliegende Knoten 4 gibt 2 zurück und der Knoten 3 gibt 3 zurück, da sein anderes Kind 1 die Höhe 1 hat. Der Knoten 6 gibt 1 zurück. An der Wurzel ist left + right = 3 + 1 = 4 die Antwort. Jeder andere Knoten erreicht höchstens den Wert 3, nämlich mit 1 + 2 = 3.
Jeder Knoten wird einmal besucht, daher beträgt die Laufzeit O(n). Die Rekursion ist so tief wie der Baum, also O(h), mit etwa einem Stackframe pro Ebene. Die Antwort wird in einer Variablen außerhalb der Rekursion gespeichert, denn was ein Aufruf zurückgibt (eine Höhe), ist nicht das, was du am Ende möchtest (eine Pfadlänge).
Algorithmus
- Setze
best = 0und schreibeheight(i). Gib bei einer leeren Stelle0zurück. - Berechne
left = height(2*i+1)undright = height(2*i+2). - Setze
bestauf den größeren Wert vonbestundleft + right. - Gib
1 + max(left, right)zurück. - Rufe
height(0)auf und gibbestzurück.
def diameterOfBinaryTree(tree):
n = len(tree)
best = 0
def height(i):
# Returns the height of node i and updates best on the way back up.
nonlocal best
if i >= n or tree[i] == -1:
return 0
left = height(2 * i + 1)
right = height(2 * i + 2)
best = max(best, left + right) # the longest path that turns at node i
return 1 + max(left, right)
height(0)
return best
Stolperfallen und Grenzfälle
Die meisten falschen Antworten zählen das Falsche oder messen am falschen Knoten.
- Knoten statt Kanten zählen. Der Pfad
7,4,3,8,6hat fünf Knoten und die Länge4, und ein einzelner Knoten hat den Durchmesser0. - Nur über die Wurzel messen. Im zweiten Beispiel hat der beste Pfad durch die Wurzel drei Kanten, und die Antwort ist vier, mit einem Knick bei Index
1. - Den Durchmesser aus dem rekursiven Aufruf zurückgeben. Der übergeordnete Knoten benötigt die Höhen seiner Kindknoten, um längere Pfade zu bilden; der Durchmesser gehört in eine separate Variable.
- Zwei Höhenkonventionen vermischen. Bei Höhen, die Knoten zählen, und
0für eine leere Stelle ergibtleft + rightbereits die Anzahl der Kanten. Bei Höhen, die Kanten zählen, braucht man-1für eine leere Stelle undleft + right + 2. Die Hälfte der einen und die Hälfte der anderen Konvention führt zu einem Fehler von eins oder zwei. - Über das Ende hinaus lesen. Ein Blatt nahe dem Ende des Arrays kann Kindindizes haben, die über den letzten Eintrag hinausgehen. Behandle einen Index jenseits des Endes als leere Stelle.
- Den Offset in Lua und R verwechseln, wo Arrays bei 1 beginnen. Behalte für die
2*i+1-Berechnung die Knotenindizes bei 0 und liestree[i + 1].
Häufige Fragen4
Wie hoch ist die Zeitkomplexität des Durchmessers eines binären Baums?
Die Lösung in Postorder besucht jeden Knoten einmal und benötigt daher O(n) Zeit sowie O(h) zusätzlichen Speicherplatz für die Rekursion, wobei h die Höhe ist. Die Höhen separat an jedem Knoten zu berechnen, kostet O(n·h), was bei einem baumförmigen Gebilde wie einer Kette zu O(n²) wird.
Verläuft der Durchmesser eines Binärbaums immer durch die Wurzel?
Nein. Der längste Pfad kann vollständig innerhalb eines Teilbaums liegen, zum Beispiel wenn die Wurzel einen kurzen Zweig und auf der anderen Seite einen tiefen, stark verzweigten Teilbaum hat. Deshalb prüfst du left + right an jedem Knoten und nicht nur an der Wurzel.
Wird der Durchmesser in Knoten oder in Kanten gezählt?
Hier wird in Kanten gezählt, also in den Verbindungen zwischen aufeinanderfolgenden Knoten auf dem Pfad. Daher hat ein einzelner Knoten den Durchmesser 0 und zwei verbundene Knoten den Durchmesser 1. In manchen Büchern wird stattdessen die Anzahl der Knoten gezählt, wodurch sich ein Wert ergibt, der um eins größer ist. Prüfe, wonach eine Aufgabe fragt, bevor du 1 addierst oder abziehst.
Wie ermittelt man den Durchmesser eines Binärbaums ohne Rekursion?
Besuche die Knoten in einer Reihenfolge, in der jedes Kind vor seinem Elternknoten kommt. Eine Möglichkeit: Lege die Wurzel auf einen Stack, nimm Knoten herunter und füge sie einer Liste hinzu, während du ihre Kinder auf den Stack legst, und durchlaufe diese Liste anschließend rückwärts. Speichere die Höhe jedes Knotens in einem Array, lies an jedem Knoten die Höhen der beiden Kinder aus und aktualisiere die Antwort mit ihrer Summe. Die Laufzeit bleibt O(n).
Ähnliche Aufgaben
Aufgaben mit denselben Ideen. Wer zwei oder drei davon löst, behält das Muster.
Python
def diameterOfBinaryTree(tree):
# Schreibe hier CodeFall 1
Fall 2
Fall 3
Eingabe
tree = [8, 3, 6, 1, 4, -1, -1, -1, -1, 7]
Erwartet
4