Maximum Depth of Binary Tree
레벨 순서로 배열 tree에 저장된 이진 트리가 주어집니다. 루트는 인덱스 0에 있고, 인덱스 i에 있는 노드의 자식은 2*i+1(왼쪽)과 2*i+2(오른쪽)에 있으며, -1은 빈 위치를 나타내고 배열 끝에 추가 -1 항목이 있을 수 있습니다. 트리의 최대 깊이, 즉 루트에서 리프까지 이어지는 가장 긴 경로에 있는 노드의 수를 반환하세요.
함수
- treeinteger-array
- 빈 위치에는 -1을 사용하여 레벨 순서로 표현한 이진 트리
- 반환값integer
- 루트에서 리프까지 이어지는 가장 긴 경로의 노드 수
제약 조건
1 ≤ tree.length ≤ 32767- 각
tree[i]는-1이거나0 ≤ tree[i] ≤ 1000인 값입니다. tree[0]은 절대-1이 아니므로 트리에는 노드가 최소 하나 있습니다.- 배열은 마지막 노드 뒤에 추가적인
-1항목이 있을 수 있습니다. - 비어 있는 위치의 두 자식도 모두 비어 있으며, 깊이는 최대
14입니다.
예제
- 입력
- tree = [5, 8, 1, -1, 3, -1, -1, -1, -1, 6]
- 출력
- 4
- 설명
- 가장 긴 경로는
5,8,3,6(인덱스0,1,4,9)이며, 노드 4개를 포함합니다.1을 거치는 경로는 노드 2개 이후에 끝납니다.
- 입력
- tree = [7, -1, -1]
- 출력
- 1
- 설명
- 두 개의
-1항목은 루트의 비어 있는 자식 위치입니다. 루트만으로도 노드 하나로 이루어진 경로이므로 깊이는0이 아니라1입니다.
- 입력
- tree = [2, -1, 9, -1, -1, -1, 4]
- 출력
- 3
- 설명
- 루트
2에는 왼쪽 자식이 없습니다. 인덱스2에 있는 오른쪽 자식9에는 인덱스6에 있는4가 오른쪽 자식으로 있으며, 경로는 노드 3개로 이루어져 있습니다.
제출 시 숨은 테스트 +13개
후속 질문
가장 긴 루트-리프 경로의 길이뿐 아니라 그 경로에 있는 값들을 어떻게 반환할까요? 여러 경로의 길이가 같다면 어떤 경로를 반환할까요? 그리고 계약에 이를 어떻게 명시할까요?
힌트
하나씩 열어 보세요. 열 때마다 조금 더 알려 줍니다.
루트에 대해 생각해 보세요. 왼쪽 하위 트리의 깊이와 오른쪽 하위 트리의 깊이를 알고 있다면, 전체 트리의 깊이는 얼마일까요?
루트의 깊이에 두 하위 트리 깊이 중 더 큰 값을 더한 값이며, 비어 있는 위치의 깊이는
0입니다. 모든 노드에 동일한 규칙이 적용되므로, 각 노드의 깊이를 아는 순회로 답을 찾을 수 있습니다.루트의 깊이를 1로 하여, 노드 인덱스와 깊이로 이루어진 쌍을 스택에 넣습니다. 쌍을 꺼내고 지금까지 확인한 가장 큰 깊이를 기록한 다음, 배열 범위 안에 있고
-1이 아닌 각 자식 노드를 깊이에 1을 더해2*i+1및2*i+2에 넣습니다.
풀이
깊이는 가장 긴 가지 하나에 의해 결정되며, 모든 노드를 살펴보지 않고는 어떤 가지가 가장 긴지 알 수 없습니다. 따라서 각 노드에서 깊이를 파악하며 전체 순회를 해야 합니다. 재귀, 레벨별 너비 우선 탐색, 직접 스택을 사용하는 깊이 우선 탐색은 모두 한 번의 순회로 이 작업을 수행합니다. 이 방법들은 현재 위치를 추적하는 방식이 서로 다릅니다.
두 하위 트리에서의 재귀
핵심 아이디어
먼저 배열을 어떻게 탐색하는지 살펴보겠습니다. 인덱스 i에 있는 노드의 왼쪽 자식은 2*i+1, 오른쪽 자식은 2*i+2에 있습니다. 자식은 해당 인덱스가 배열 안에 있고 그곳의 값이 -1이 아닐 때만 실제로 존재합니다. [5, 8, 1, -1, 3, -1, -1, -1, -1, 6]에서 루트 5의 자식은 인덱스 1과 2에 있고, 인덱스 1의 8은 왼쪽 인덱스 3이 비어 있으며 오른쪽 인덱스 4에 3이 있습니다. 그리고 그 3의 아래쪽 인덱스 9에는 6이 있습니다.
이제 핵심 아이디어를 살펴보겠습니다. 어떤 노드를 지나는 가장 깊은 경로는 두 하위 트리 중 더 깊은 쪽으로 내려갑니다. 따라서 인덱스 i에 있는 하위 트리의 깊이는 해당 노드의 깊이 1에 2*i+1과 2*i+2에 있는 하위 트리 깊이 중 더 큰 값을 더한 것입니다. 비어 있는 위치의 깊이는 0이며, 여기서 재귀가 끝납니다. 리프의 깊이는 1 + max(0, 0) = 1이 되고, 값은 루트까지 거슬러 올라갑니다.
각 노드는 한 번씩 방문하므로 시간 복잡도는 O(n)입니다. 호출 스택에는 현재 경로의 각 레벨마다 프레임이 하나씩 쌓이므로, 깊이가 h일 때 O(h)의 공간을 사용하며 여기서는 최대 14입니다. 이 제한 덕분에 이 문제에서는 재귀를 안전하게 사용할 수 있습니다. 긴 사슬 모양의 포인터 기반 트리라면 같은 코드가 재귀 제한에 도달하게 되며, Python에서 그 제한은 1000개 프레임입니다.
알고리즘
depth(i)를 작성합니다.i가 배열의 끝을 지났거나tree[i]가-1이면0을 반환합니다.- 그렇지 않으면
1 + max(depth(2*i+1), depth(2*i+2))를 반환합니다. depth(0)을 반환합니다.
def maxDepth(tree):
def depth(i):
# An index past the end or a -1 is an empty spot: depth 0.
if i >= len(tree) or tree[i] == -1:
return 0
return 1 + max(depth(2 * i + 1), depth(2 * i + 2))
return depth(0)너비 우선 탐색, 레벨별로
핵심 아이디어
최대 깊이는 트리의 레벨 수이므로, 경로를 따라가는 대신 레벨을 셀 수 있습니다. 큐는 노드를 레벨 순서대로 방문합니다. 루트를 큐에 넣고 시작한 다음, 노드를 꺼낼 때마다 실제 자식 노드를 큐의 뒤에 추가합니다.
레벨을 세려면 큐를 묶음 단위로 처리합니다. 각 묶음을 처리하기 전에 큐에 노드가 몇 개 있는지 확인합니다. 묶음을 처리하는 동안 추가하는 자식 노드는 기존 노드 뒤에 들어가므로, 이 노드들은 정확히 한 레벨에 속합니다. 그 수만큼 노드를 꺼내고 자식 노드를 큐에 넣은 다음, 깊이에 1을 더합니다. 큐가 비면 깊이는 묶음의 개수입니다. 첫 번째 예시에서 묶음은 [5], [8, 1], [3], [6]이므로 답은 4입니다.
각 노드는 큐에 한 번 들어가고 한 번 나옵니다. 시간 복잡도는 O(n)입니다. 큐에는 한 번에 한 레벨의 노드가 들어가므로, 가장 너비가 넓은 레벨의 노드 수를 w라고 할 때 공간 복잡도는 O(w)입니다. 포화 트리에서는 마지막 레벨에 전체 노드의 약 절반이 들어갑니다. 깊이가 14일 때 16383개 중 8192개가 들어갑니다.
알고리즘
- 루트 인덱스
0을 큐에 넣고depth = 0으로 설정합니다. - 큐가 비어 있지 않은 동안
depth에1을 더하고 큐의 크기를 읽습니다. - 그 개수만큼 인덱스를 꺼냅니다. 각 인덱스에 대해 배열 범위 안에 있고
-1이 아닌 자식 인덱스2*i+1과2*i+2를 큐에 넣습니다. - 큐가 비면
depth를 반환합니다.
from collections import deque
def maxDepth(tree):
n = len(tree)
queue = deque([0])
depth = 0
while queue:
depth += 1
for _ in range(len(queue)): # exactly the nodes of this level
i = queue.popleft()
for child in (2 * i + 1, 2 * i + 2):
if child < n and tree[child] != -1:
queue.append(child)
return depth명시적 스택을 사용하는 깊이 우선 탐색
핵심 아이디어
재귀 호출을 하나도 하지 않고도 재귀 방식처럼 경로를 따라갈 수 있습니다. 직접 스택을 만들고 각 노드를 깊이와 함께 저장하세요. 노드가 얼마나 깊이 있는지 기억해 주는 다른 정보가 없기 때문입니다. (0, 1) 쌍으로 시작합니다. 루트의 깊이는 1입니다.
쌍을 하나 꺼내 깊이를 지금까지 확인한 최댓값과 비교한 다음, 실제 자식 노드마다 depth + 1을 붙여 스택에 넣습니다. 트리의 모든 노드는 도달 경로의 길이를 담아 정확히 한 번씩 스택에 들어가므로, 꺼낸 쌍 중 깊이가 가장 큰 값이 답입니다. 첫 번째 예시에서는 인덱스 9에 있는 6이 (9, 4)로 스택에 들어가며, 이보다 깊은 쌍은 없습니다.
시간 복잡도는 O(n)입니다. 스택에는 현재 경로를 따라 아직 처리되지 않은 형제 노드가 저장되며, 레벨당 하나 정도이므로 공간 복잡도는 O(h)입니다. 재귀 방식과 같지만 호출 스택이 넘칠 염려가 없습니다. 트리의 깊이가 깊을 수 있을 때 사용할 방법이며, 포인터 기반 트리에도 그대로 적용할 수 있습니다.
알고리즘
(0, 1)을 스택에 넣고best = 0으로 설정합니다.(i, depth)쌍을 꺼내고best를best와depth중 더 큰 값으로 설정합니다.- 배열 범위 안에 있고
-1이 아닌 각 자식 인덱스2*i+1및2*i+2에 대해depth + 1과 함께 스택에 넣습니다. - 스택이 빌 때까지 반복한 다음
best를 반환합니다.
def maxDepth(tree):
n = len(tree)
best = 0
stack = [(0, 1)] # (node index, depth of that node); the root is never empty
while stack:
i, depth = stack.pop()
best = max(best, depth)
for child in (2 * i + 1, 2 * i + 2):
if child < n and tree[child] != -1:
stack.append((child, depth + 1))
return best
함정과 경계 사례
이 문제에서 오답이 나오는 가장 흔한 이유는 하나씩 어긋나거나, 비어 있는 자리를 노드로 취급하기 때문입니다.
- 노드가 아니라 간선의 수를 세는 경우. 여기서 노드 하나의 깊이는
1입니다. 노드 하나에0을 반환하거나, 노드 4개로 이루어진 경로에3을 반환하면 1만큼 부족합니다. - 범위 검사를 생략하는 경우. 배열의 끝부분에 있는 리프의 자식 인덱스가 마지막 항목을 넘어설 수 있습니다. 배열이 마지막 노드 바로 뒤에서 끝날 수도 있기 때문입니다.
tree[child]를 읽기 전에child < n인지 확인하세요. - 배열의 길이로 깊이를 알아내는 경우. 배열 끝에
-1항목이 더 있을 수 있으므로, 실제 노드가 있는 가장 깊은 수준보다 배열의 길이가 더 깊은 수준에 해당할 수 있습니다. -1을 값으로 취급하는 경우.-1은 노드가 없음을 나타내므로, 스택에 넣거나, 큐에 추가하거나, 세면 안 됩니다.- 트리가 균형을 이룬다고 가정하는 경우. 답은 가장 긴 가지를 따릅니다. 오른쪽 자리가 모두 비어 있고 왼쪽으로만 이어진 노드 14개의 사슬이 그 예입니다.
- 너비 우선 탐색 방식에서 반복문 안에서 큐의 크기를 읽는 경우. 자식을 추가하면 크기가 바뀌므로, 해당 묶음 처리를 시작하기 전에 크기를 저장하세요.
- 배열이 1부터 시작하는 Lua와 R에서 오프셋을 혼동하는 경우.
2*i+1계산에는 노드 인덱스를 0부터 시작하는 방식으로 유지하고,tree[i + 1]을 읽으세요.
자주 묻는 질문4
이진 트리의 최대 깊이의 시간 복잡도는 무엇인가요?
모든 접근 방식은 각 노드를 한 번씩 방문하므로 시간 복잡도는 O(n)입니다. 깊이 우선 방식은 탐색 중인 경로를 위해 O(h)의 추가 공간을 사용하며, 여기서 h는 깊이입니다. 너비 우선 방식은 가장 너비가 넓은 레벨을 위해 O(w)의 공간을 사용하며, 완전한 트리에서는 노드의 약 절반에 해당할 수 있습니다.
이진 트리의 최대 깊이를 구할 때 DFS와 BFS 중 무엇을 사용해야 할까요?
둘 다 O(n) 시간 안에 올바른 답을 제공합니다. 깊이 우선 탐색은 코드가 더 짧고 깊이에 비례하는 메모리를 사용하므로 너비가 넓고 깊이가 얕은 트리에 적합합니다. 너비 우선 탐색은 레벨을 직접 세고 가장 너비가 넓은 레벨에 비례하는 메모리를 사용하므로 깊이가 깊고 너비가 좁은 트리에 적합합니다. 최소 깊이를 구할 때는 BFS가 유리한데, 처음 만나는 리프에서 탐색을 멈출 수 있기 때문입니다.
재귀 없이 이진 트리의 최대 깊이를 어떻게 구하나요?
노드와 깊이로 이루어진 명시적인 쌍 스택을 사용하세요. 루트를 깊이 1로 시작하고, 쌍을 꺼내 깊이를 기록한 다음 각 자식을 깊이에 1을 더해 넣으세요. 꺼낸 깊이 중 가장 큰 값이 답입니다. 한 번에 한 레벨씩 처리하는 큐를 사용해도 되며, 레벨마다 1씩 셉니다.
이진 트리의 깊이와 높이는 어떻게 다른가요?
노드의 깊이는 루트에서 해당 노드까지 내려가는 단계 수를 세고, 노드의 높이는 해당 노드에서 가장 깊은 리프까지 내려가는 단계 수를 셉니다. 트리의 최대 깊이와 루트의 높이는 같은 값입니다. 이 문제에서는 노드 수를 세므로 노드 하나의 깊이는 1입니다. 일부 책에서는 대신 간선 수를 세므로 값이 하나 작아집니다.
비슷한 문제
같은 아이디어를 쓰는 문제입니다. 두세 개를 풀면 패턴이 몸에 익습니다.
Python
def maxDepth(tree):
# 여기에 코드를 작성하세요케이스 1
케이스 2
케이스 3
입력
tree = [5, 8, 1, -1, 3, -1, -1, -1, -1, 6]
기대값
4