Symmetric Tree
배열 tree에 레벨 순서로 저장된 이진 트리가 주어집니다. 루트는 인덱스 0에 있고, 인덱스 i에 있는 노드의 자식은 2*i+1(왼쪽)과 2*i+2(오른쪽)에 있으며, -1은 빈 자리를 나타내고, 배열 끝에는 여분의 -1 항목이 있을 수 있습니다. 루트를 지나는 수직선을 기준으로 트리가 자기 자신의 거울상이라면 true를 반환하고, 그렇지 않으면 false를 반환하세요. 모양과 값이 모두 일치해야 합니다.
함수
- treeinteger-array
- 빈 자리는 -1로 표시한 레벨 순서 이진 트리
- 반환값boolean
- 트리가 자기 자신과 대칭이면 true, 그렇지 않으면 false
제약 조건
1 ≤ tree.length ≤ 32767- 각
tree[i]는-1이거나0 ≤ tree[i] ≤ 1000을 만족하는 값입니다. tree[0]은 절대-1이 아니므로, 트리에는 노드가 하나 이상 있습니다.- 배열은 마지막 노드 뒤에 추가적인
-1항목이 있을 수 있습니다. - 빈 자리의 두 자식도 모두 비어 있으며, 깊이는 최대
14입니다.
예제
- 입력
- tree = [1, 2, 2, 3, 4, 4, 3]
- 출력
- true
- 설명
- 트리를 가운데에서 접으세요. 인덱스
1과2에 있는 두 개의2가 만나고, 인덱스3과6에 있는 바깥쪽3이 만나며,4와5에 있는 안쪽4가 만납니다.
- 입력
- tree = [1, 2, 2, -1, 3, -1, 3]
- 출력
- false
- 설명
- 두
3은 모두 부모 노드의 오른쪽에 매달려 있습니다. 거울 대칭에서는 왼쪽2의 오른쪽 자식(인덱스4)이 오른쪽2의 왼쪽 자식(인덱스5)을 마주해야 하지만, 인덱스5는 비어 있습니다.
- 입력
- tree = [4, 6, 6, 5, -1, -1, 9]
- 출력
- false
- 설명
- 모양은 거울상입니다. 인덱스
3은 인덱스6을 마주 보고 있으며 둘 다 노드를 가지고 있습니다. 값은5와9로 다르므로 트리는 대칭이 아닙니다.
제출 시 숨은 테스트 +16개
후속 질문
모양은 서로 대칭이지만 일부 값이 다르다면, 트리를 대칭으로 만들기 위해 최소 몇 개의 노드 값을 바꿔야 할까요?
힌트
하나씩 열어 보세요. 열 때마다 조금 더 알려 줍니다.
루트의 왼쪽 자식은 어떤 노드와 일치해야 하나요? 그리고 그 노드의 왼쪽 자식은 어떤 노드와 일치해야 하나요?
한 번에 두 위치를 비교합니다. 두 위치가 모두 비어 있거나 같은 값을 가지며 자식 노드가 서로 교차할 때 서로 대칭입니다. 즉, 한쪽의 왼쪽 자식은 다른 쪽의 오른쪽 자식과 대칭이고, 한쪽의 오른쪽 자식은 다른 쪽의 왼쪽 자식과 대칭입니다.
인덱스 쌍을 스택에 넣고,
(1, 2)부터 시작합니다. 쌍을 꺼내 다음과 같이 처리합니다. 두 위치가 모두 비어 있으면 건너뛰고, 한쪽만 비어 있거나 값이 다르면 실패하며, 그렇지 않으면(2*a+1, 2*b+2)와(2*a+2, 2*b+1)을 넣습니다.
풀이
대칭성은 쌍의 속성입니다. 모든 노드에는 루트 반대편의 대칭 위치에 짝이 있으며, 왼쪽 자식의 짝은 오른쪽 자식입니다. 따라서 노드를 자기 자신의 자식과 비교하지 않습니다. 트리의 두 절반을 동시에 서로 반대 방향으로 따라가면서 각 쌍의 구조와 값을 비교하고, 서로 일치하지 않는 첫 번째 쌍에서 멈춥니다.
각 수준을 그 역순과 비교하세요
핵심 아이디어
먼저 배열에서 이동하는 방법을 살펴보겠습니다. 인덱스 i에 있는 노드의 왼쪽 자식은 2*i+1에, 오른쪽 자식은 2*i+2에 있습니다. 자식은 해당 인덱스가 배열 안에 있고 그 위치의 값이 -1이 아닐 때만 실제 노드입니다. [1, 2, 2, 3, 4, 4, 3]에서 루트 1의 자식은 인덱스 1과 2에 있고, 인덱스 1의 2의 자식은 3과 4에 있습니다.
이제 한 번에 트리의 한 레벨씩 살펴보겠습니다. 좌우 대칭 이미지는 왼쪽에서 오른쪽으로 읽을 때와 오른쪽에서 왼쪽으로 읽을 때 동일하므로, 빈 위치도 포함해 적은 각 레벨은 양방향으로 읽었을 때 같아야 합니다. 첫 번째 예시에서 루트 아래의 레벨은 2 2와 3 4 4 3입니다. 두 번째 예시에서는 2 2 다음에 -1 3 -1 3이 나오는데, 이를 뒤집으면 3 -1 3 -1이므로 답은 false입니다.
빈 위치도 해당 레벨에 남겨 두어야 합니다. 빈 위치를 제외하면 두 번째 예시의 마지막 레벨은 3 3이 되어 조건을 통과합니다. 해당 레벨에 있는 각 실제 노드의 자식 위치마다 항목을 하나씩 기록하고, 비어 있는 위치에는 -1을 기록합니다. 빈 위치의 자식도 비어 있으므로 아무것도 추가하지 않습니다. 각 노드는 한 번씩 방문하므로 시간 복잡도는 O(n)이며, 한 번에 한 레벨만 메모리에 유지하므로 가장 너비가 넓은 레벨의 너비를 w라 할 때 공간 복잡도는 O(w)입니다.
알고리즘
- 루트 인덱스
0을 담은 목록에서 시작합니다. - 목록의 각 인덱스에 대해 왼쪽에서 오른쪽으로 두 자식 위치를 모두 적습니다. 자식이 실제로 있으면 그 값을, 비어 있으면
-1을 적습니다. 다음 레벨에 사용할 실제 자식들을 모읍니다. - 자식 위치의 행이 역순 행과 다르면
false를 반환합니다. - 다음 레벨로 이동해 비어 있을 때까지 반복한 다음
true를 반환합니다.
def isSymmetric(tree):
n = len(tree)
level = [0] # the real nodes of one level, left to right
while level:
row = [] # the child spots under this level, -1 for an empty one
next_level = []
for i in level:
for child in (2 * i + 1, 2 * i + 2):
if child < n and tree[child] != -1:
row.append(tree[child])
next_level.append(child)
else:
row.append(-1)
if row != row[::-1]:
return False
level = next_level
return True대칭 쌍에 대한 재귀
핵심 아이디어
전체 레벨 대신 두 하위 트리를 비교합니다. 즉, 인덱스 1에서 시작하는 루트의 왼쪽 하위 트리와 인덱스 2에서 시작하는 오른쪽 하위 트리입니다. 두 위치가 모두 비어 있거나, 같은 값을 가지고 자식 노드가 서로 교차 대응하면 두 위치는 서로 대칭입니다. 한쪽의 왼쪽 자식은 다른 쪽의 오른쪽 자식과 대칭을 이루고(바깥쪽 쌍), 한쪽의 오른쪽 자식은 다른 쪽의 왼쪽 자식과 대칭을 이룹니다(안쪽 쌍).
첫 번째 예제에서 mirrors(1, 2)는 두 2를 비교한 다음, 바깥쪽 3들을 위해 mirrors(3, 6)을 호출하고 안쪽 4들을 위해 mirrors(4, 5)를 호출합니다. 두 호출은 각각 아래에 비어 있는 위치만 있는 것을 확인하고 true를 반환합니다. 두 번째 예제에서는 mirrors(4, 5)가 인덱스 4의 3과 인덱스 5의 빈 위치가 마주한 것을 확인하고 false를 반환합니다. 그러면 false가 최상위까지 거슬러 올라갑니다.
실제 노드 하나는 최대 하나의 쌍에만 속하므로 시간 복잡도는 O(n)입니다. 호출 스택의 깊이는 트리의 높이만큼인 O(h)이며, 여기서는 최대 14개 프레임입니다.
알고리즘
mirrors(a, b)를 작성하세요. 인덱스가 끝을 벗어났거나-1을 포함하고 있으면 해당 위치는 비어 있습니다. 두 위치가 모두 비어 있으면true를 반환하고, 한쪽만 비어 있으면false를 반환하세요.tree[a]와tree[b]가 다르면false를 반환하세요.- 그렇지 않으면
mirrors(2*a+1, 2*b+2)와mirrors(2*a+2, 2*b+1)의 결과를 반환하세요. mirrors(1, 2)를 반환하세요. 자식이 없는 루트는 두 위치가 모두 비어 있는 경우이므로true입니다.
def isSymmetric(tree):
n = len(tree)
def mirrors(a, b):
# Spots a and b must hold the same value, or both be empty.
empty_a = a >= n or tree[a] == -1
empty_b = b >= n or tree[b] == -1
if empty_a or empty_b:
return empty_a and empty_b
return (tree[a] == tree[b]
and mirrors(2 * a + 1, 2 * b + 2) # outer pair
and mirrors(2 * a + 2, 2 * b + 1)) # inner pair
return mirrors(1, 2)대칭 쌍의 명시적 스택
핵심 아이디어
재귀에 필요한 것은 단 하나입니다. 아직 확인하지 않은 쌍입니다. 그 쌍들을 직접 스택에 저장하면 호출이 사라집니다. (1, 2) 쌍부터 시작합니다. 쌍을 꺼냅니다. 두 위치가 모두 비어 있으면 그 아래에는 아무것도 없으므로 다음으로 넘어갑니다. 한쪽이 비어 있거나 값이 다르면 트리는 대칭이 아닙니다. 그렇지 않으면 바깥쪽 쌍 (2*a+1, 2*b+2)과 안쪽 쌍 (2*a+2, 2*b+1)을 넣습니다.
쌍을 확인하는 순서는 중요하지 않습니다. 모든 쌍이 일치해야만 트리가 대칭이기 때문입니다. 스택을 사용하면 깊이 우선 순서로 확인하고, 큐를 사용하면 레벨 순서로 확인하며 같은 방식으로 작동합니다. 세 번째 예제에서는 처음으로 잘못된 쌍인 (3, 6)에서 멈추며, 이 쌍에는 5와 9가 들어 있습니다.
꺼내기 한 번마다 쌍 하나를 처리하고 각 실제 노드는 최대 한 쌍에만 포함되므로 시간 복잡도는 O(n)입니다. 스택에는 현재 경로의 레벨마다 대기 중인 쌍이 대략 하나씩 저장되므로 공간 복잡도는 O(h)이며, 걱정해야 할 재귀 제한도 없습니다.
알고리즘
(1, 2)쌍을 스택에 넣습니다.(a, b)쌍을 꺼냅니다. 두 위치가 모두 비어 있으면(인덱스가 끝을 벗어났거나-1이면) 다음 쌍으로 넘어갑니다.- 한 위치만 비어 있거나
tree[a]가tree[b]와 다르면false를 반환합니다. (2*a+1, 2*b+2)와(2*a+2, 2*b+1)을 넣습니다.- 스택이 비면
true를 반환합니다.
def isSymmetric(tree):
n = len(tree)
stack = [(1, 2)] # pairs of spots that must mirror each other
while stack:
a, b = stack.pop()
empty_a = a >= n or tree[a] == -1
empty_b = b >= n or tree[b] == -1
if empty_a and empty_b:
continue
if empty_a or empty_b or tree[a] != tree[b]:
return False
stack.append((2 * a + 1, 2 * b + 2)) # outer pair
stack.append((2 * a + 2, 2 * b + 1)) # inner pair
return True
함정과 경계 사례
대부분의 오답은 잘못된 노드 쌍을 비교하거나, 빈 위치도 트리의 형태에 포함된다는 점을 잊습니다.
- 각 하위 트리를 따로 확인합니다. 왼쪽 하위 트리 자체가 대칭일 필요는 없습니다.
[1, 2, 2, 3, 4, 4, 3]에서 하위 트리2, 3, 4는 대칭이 아니지만 전체 트리는 대칭입니다. 왼쪽 하위 트리는 오른쪽 하위 트리를 거울처럼 비춰야 합니다. - 자식 노드의 쌍을 잘못 짝짓습니다. 한쪽의 왼쪽 자식은 다른 쪽의 오른쪽 자식과 마주합니다. 즉,
(2*a+1, 2*b+2)와(2*a+2, 2*b+1)이며,(2*a+1, 2*b+1)은 아닙니다. - 값만 비교합니다.
[1, 2, 2, -1, 3, -1, 3]에서 빈 위치를 제외하면 각 레벨이 양쪽에서 똑같이 읽히지만, 트리는 대칭이 아닙니다. 레벨 행에-1을 유지하거나 쌍을 검사할 때 비어 있는지 확인하세요. - 배열의 끝을 넘어 읽습니다. 배열의 끝을 벗어난 인덱스는 빈 위치입니다.
tree[a]를 읽기 전에a < n인지 확인하세요. 노드가 하나뿐인 트리에는 인덱스1이나2가 아예 없습니다. - 일치하는 쌍을 처음 찾았을 때 멈춥니다. 쌍 하나가 맞는다고 해서 아무것도 증명되지 않습니다. 모든 쌍을 확인한 뒤에만
true를 반환하세요. - 배열 인덱스가 1부터 시작하는 Lua와 R에서 오프셋을 혼동합니다.
2*i+1계산을 위해 노드 인덱스는 0부터 시작하도록 유지하고,tree[i + 1]을 읽으세요.
자주 묻는 질문4
대칭 트리의 시간 복잡도는 얼마인가요?
각 실제 노드는 하나의 대칭 쌍의 일부로 한 번씩 비교되므로 시간 복잡도는 O(n)입니다. 재귀 버전과 스택 버전은 현재 경로의 대기 중인 쌍을 저장하기 위해 O(h)의 추가 공간을 사용합니다. 레벨별 버전은 한 레벨을 메모리에 유지하며, 가장 너비가 넓은 레벨을 기준으로 O(w)의 공간을 사용합니다.
재귀 없이 이진 트리가 대칭인지 어떻게 확인하나요?
서로 대칭이어야 하는 노드 쌍을 스택이나 큐에 저장하고, 루트의 두 자식부터 시작하세요. 쌍을 하나 꺼내 불일치가 있으면 실패하고, 해당 노드의 자식 중 바깥쪽 쌍과 안쪽 쌍을 추가하세요. 불일치 없이 스택이 비면 트리는 대칭입니다.
대칭 트리와 서로 동일한 두 트리의 차이점은 무엇인가요?
왼쪽과 왼쪽, 오른쪽과 오른쪽을 비교했을 때 두 트리는 동일합니다. 왼쪽 하위 트리가 오른쪽 하위 트리의 거울상과 동일하면 트리는 대칭입니다. 따라서 비교 방식이 서로 교차하여 왼쪽과 오른쪽, 오른쪽과 왼쪽을 비교합니다. 자식 노드 쌍의 순서를 바꾸면 두 문제 모두 동일한 쌍 비교 코드로 해결할 수 있습니다.
노드가 하나뿐인 트리는 대칭인가요?
네. 단일 노드에는 비어 있는 자식 자리가 두 개 있으며, 비어 있는 두 자리는 서로 대칭입니다. 자식이 정확히 하나인 루트는 결코 대칭이 아닙니다. 그 자식이 비어 있는 자리와 마주하고 있기 때문입니다.
비슷한 문제
같은 아이디어를 쓰는 문제입니다. 두세 개를 풀면 패턴이 몸에 익습니다.
Python
def isSymmetric(tree):
# 여기에 코드를 작성하세요케이스 1
케이스 2
케이스 3
입력
tree = [1, 2, 2, 3, 4, 4, 3]
기대값
true