Middle of the Linked List
같은 길이의 두 배열에 저장된 단일 연결 리스트가 주어집니다. 노드 i는 값 values[i]를 가지며 노드 next[i]를 가리킵니다. -1은 리스트의 끝을 나타내고, 헤드는 노드 0입니다. 노드는 리스트 순서대로 저장되어 있지 않으므로 링크를 따라가세요.
중간 노드의 값을 반환하세요. 리스트의 노드 수가 짝수이면 중간 노드가 두 개입니다. 이때 두 번째 노드의 값을 반환하세요.
함수
- valuesinteger-array
- 각 노드가 보유한 값
- nextinteger-array
- 각 노드가 연결되는 노드의 인덱스, 또는 마지막 노드인 경우 -1
- 반환값integer
- 중간 노드의 값, 길이가 짝수일 때 두 번째 중간 노드
제약 조건
1 ≤ n ≤ 5000이며, 여기서n은values와next의 길이입니다.-104 ≤ values[i] ≤ 104next[i]는-1이거나0부터n-1까지의 노드 인덱스입니다.- 노드
0에서 시작하여 목록은 모든 노드를 정확히 한 번씩 방문한 다음-1에 도달합니다. 사이클은 없습니다.
예제
- 입력
- values = [4, 9, 2, 7, 5]next = [3, -1, 1, 4, 2]
- 출력
- 5
- 설명
- 노드
0에서 링크를 따라가면 노드0, 3, 4, 2, 1을 지나므로, 목록은4, 7, 5, 2, 9입니다. 다섯 개 중 세 번째는 노드4이며, 그 값은5입니다. 배열 자체의 가운데 항목인values[2] = 2는 다른 노드입니다.
- 입력
- values = [10, 20, 30, 40, 50, 60]next = [1, 2, 3, 4, 5, -1]
- 출력
- 40
- 설명
- 여기서는 노드가 순서대로 저장됩니다. 노드가 6개이면 가운데 노드가 두 개이며,
30과40중 두 번째 노드가 선택됩니다.
- 입력
- values = [8]next = [-1]
- 출력
- 8
- 설명
- 노드가 하나인 목록에서는 그 노드가 중간입니다.
제출 시 숨은 테스트 +13개
후속 질문
한 번의 순회로 목록의 3분의 1 지점에 있는 노드를 반환할 수 있나요? 각 포인터는 얼마나 빠르게 움직여야 하며, 어디에서 멈추면 될까요?
힌트
하나씩 열어 보세요. 열 때마다 조금 더 알려 줍니다.
목록의 끝에 도달하기 전까지는 목록의 길이를 알 수 없습니다. 두 명의 이동자가 맨 앞에서 출발해 그중 한 명이 다른 한 명보다 두 배 빠르게 움직이면 어떨까요?
더 빠른 포인터가 끝에 도달했을 때 느린 포인터는 거리의 절반만큼 이동했으므로 중간 노드에 위치합니다. 남은 유일한 세부 사항은 길이가 짝수일 때 두 번째 중간 노드에 도달하도록 언제 멈출지입니다.
slow와fast를 노드0에서 시작합니다.fast가-1이 아니고next[fast]가-1이 아닌 동안, slow는 한 링크, fast는 두 링크씩 이동합니다. 그런 다음values[slow]를 반환합니다.
풀이
배열에서 중간은 인덱스 n / 2에 있습니다. 연결 리스트에는 인덱스가 없습니다. 끝까지 따라가야 길이를 알 수 있고, 그때는 이미 중간을 지나친 뒤입니다. 리스트를 배열에 복사하거나, 먼저 개수를 센 다음 다시 따라갈 수도 있습니다. 간단한 해법은 서로 다른 속도로 리스트를 따라가는 포인터 두 개를 사용하여, 빠른 포인터가 끝에 도달했을 때 느린 포인터가 중간에 있도록 하는 것입니다.
값을 배열에 복사하기
핵심 아이디어
이 문제에서 포인터는 노드 인덱스입니다. 다음 노드로 이동하는 것은 node = next[node]이며, -1에 도달하면 끝을 벗어난 것입니다. 첫 번째 예제에서 노드 0부터 순회하면 0 → 3 → 4 → 2 → 1 → -1이 됩니다.
리스트의 문제는 특정 위치로 건너뛸 수 없다는 것입니다. 그러니 건너뛸 수 있도록 바꿔 봅시다. 리스트를 한 번 순회하면서 각 값을 지나칠 때마다 새 배열에 추가합니다. 이 배열에는 리스트 순서대로 값이 담기며, 첫 번째 예제에서는 [4, 7, 5, 2, 9]가 됩니다. 배열의 가운데는 정수 나눗셈을 사용한 인덱스 length / 2입니다.
이 인덱스만으로도 길이가 짝수일 때 두 번째 가운데 값을 구할 수 있습니다. 값이 6개라면 인덱스는 3이고, 이는 두 번째 예제에서 40인 네 번째 값입니다. 순회에는 O(n) 시간이 들고 복사에는 O(n)의 추가 메모리가 필요하지만, 다음 두 가지 접근 방식은 이를 피합니다.
알고리즘
- 빈 배열과
node = 0으로 시작합니다. node가-1이 아닌 동안values[node]를 추가하고next[node]로 이동합니다.- 내림한
length / 2인덱스의 항목을 반환합니다.
def middleNode(values, next):
in_order = []
node = 0
while node != -1:
in_order.append(values[node])
node = next[node]
return in_order[len(in_order) // 2]수를 센 다음 절반만큼 걸어가세요
핵심 아이디어
전체 복사본은 필요 없고 길이만 알면 됩니다. 리스트를 한 번 순회하며 노드 수를 세세요. 그런 다음 다시 헤드에서 시작해 length / 2만큼 내림하여 이동합니다. 멈춘 노드가 중간 노드입니다.
이만큼 이동하는 이유는 다음과 같습니다. k번 이동하면 헤드를 위치 0으로 세었을 때 위치 k에 있는 노드에 도달합니다. 5개 노드가 있는 리스트의 중간은 위치 2이고, 6개 노드가 있는 리스트의 두 번째 중간은 위치 3이며, 둘 다 length / 2입니다. 첫 번째 예시에서는 5를 센 다음 두 번 이동해 0 → 3 → 4로 가고, values[4] = 5를 읽습니다.
이제 메모리 사용량은 O(1)입니다. 그 대가로 리스트의 절반을 한 번 더 순회해야 하며, 총 이동 횟수는 1.5n입니다. 그래도 여전히 O(n)입니다.
알고리즘
- 노드
0에서-1까지 이동하며 노드의 개수를 셉니다. - 노드
0으로 돌아갑니다. count / 2를 내림한 횟수만큼 정확히node = next[node]로 이동합니다.values[node]를 반환합니다.
def middleNode(values, next):
length = 0
node = 0
while node != -1:
length += 1
node = next[node]
node = 0
for _ in range(length // 2):
node = next[node]
return values[node]빠른 포인터와 느린 포인터
핵심 아이디어
두 포인터를 헤드에 둡니다. 각 라운드마다 slow는 노드 하나를 이동하고 fast는 두 노드를 이동합니다. k라운드가 지나면 slow는 위치 k에 있고 fast는 위치 2k에 있으므로, slow는 항상 fast가 이동한 거리의 절반을 이동한 셈입니다. fast가 끝에 도달하면 slow는 가운데에 있으며, 길이를 알 필요가 없습니다.
중단 규칙에 따라 어느 쪽 가운데를 얻을지가 결정됩니다. fast가 실제 노드이고 그다음 노드도 있는 동안 계속 진행합니다. 즉, fast != -1이고 next[fast] != -1인 동안입니다. 길이가 홀수이면 fast는 마지막 노드에서 멈춥니다. 길이가 짝수이면 fast는 끝을 벗어나 -1이 되고, 이로 인해 slow가 한 번 더 이동하여 두 가운데 중 뒤쪽에 도달합니다. 두 번째 예시에서 slow는 0, 1, 2, 3으로 이동하고 fast는 0, 2, 4, -1로 이동하며, values[3]은 40입니다.
첫 번째 예시에서 slow는 노드 0, 3, 4를 방문하고 fast는 0, 4, 1을 방문합니다. 노드 1이 마지막 노드이므로 루프는 slow가 노드 4에 있을 때 멈추고 답은 5입니다. fast는 약 n번 이동하고 slow는 n / 2번 이동하므로, 메모리로 정수 두 개만 사용해 한 번 순회합니다.
알고리즘
slow = 0과fast = 0으로 설정합니다.fast != -1이고next[fast] != -1인 동안slow = next[slow]와fast = next[next[fast]]로 설정합니다.values[slow]를 반환합니다.
def middleNode(values, next):
slow = fast = 0
# Stop when fast is on the last node or has stepped past it.
while fast != -1 and next[fast] != -1:
slow = next[slow]
fast = next[next[fast]]
return values[slow]
함정과 경계 사례
루프는 짧으므로 실수는 루프의 시작 지점, 종료 지점, 반환값에서 발생합니다.
values[n / 2]를 반환하는 경우. 노드는 리스트 순서대로 저장되어 있지 않으므로 배열의 중간 항목은 대개 다른 노드입니다. 첫 번째 예에서는5대신2를 반환합니다.- 길이가 짝수일 때 첫 번째 중간 노드를 선택하는 경우.
next[fast]와next[next[fast]]가 모두 유효한 동안 실행되는 루프는 한 번 일찍 멈추어 두 번째 예에서40대신30을 반환합니다. fast != -1을 확인하기 전에next[fast]를 확인하는 경우. 길이가 짝수이면 fast가-1이 되고, 대부분의 언어에서는next[-1]을 읽을 때 오류가 발생합니다. Python에서는 대신 마지막 항목을 조용히 읽어 오므로 더 좋지 않습니다.- 세는 방식에서
count / 2 - 1만큼 이동하거나 올림하는 경우. head를 위치0으로 세고, 내림한count / 2만큼 정확히 이동하세요. - 노드의 값이 아니라 노드 인덱스를 반환하는 경우.
- 배열 인덱스가 1부터 시작하는 Lua와 R에서 오프셋을 잊는 경우. 노드 인덱스는 0부터 시작하도록 유지하고
next[node + 1]을 읽으세요. Ruby와 R에서는next라는 단어를 예약어로 사용하므로, 각 언어의 시작 코드에서는 매개변수 이름을next_로 지정합니다.
자주 묻는 질문4
빠른 포인터와 느린 포인터는 왜 연결 리스트의 중간을 찾을까요?
둘 다 머리에서 시작하며, 각 라운드마다 빠른 포인터는 노드를 두 개 이동하고 느린 포인터는 한 개 이동합니다. k라운드 후 빠른 포인터는 2k 위치에 있고 느린 포인터는 k 위치에 있어, 이동 거리는 정확히 절반입니다. 따라서 빠른 포인터가 리스트의 끝에 도달하면 느린 포인터는 리스트의 중간에 있습니다.
연결 리스트의 중간을 찾는 시간 및 공간 복잡도는 얼마인가요?
세 가지 접근 방식 모두 O(n) 시간이 걸립니다. 리스트의 중간을 찾으려면 리스트의 절반 정도 또는 그 이상을 순회해야 하기 때문입니다. 값을 복사하면 O(n)의 추가 메모리를 사용합니다. 먼저 개수를 세는 방식과 빠른 포인터 및 느린 포인터 방식은 모두 O(1)을 사용하며, 포인터 방식은 한 번만 순회하면 됩니다.
두 번째 노드 대신 첫 번째 중간 노드를 어떻게 반환하나요?
빠른 포인터가 한 라운드 더 일찍 멈추도록 중지 규칙을 변경하세요. next[fast] != -1 및 next[next[fast]] != -1인 동안 반복합니다. 노드가 6개일 때 느린 포인터는 3 대신 위치 2에서 멈춥니다. 카운팅 방식에서는 count / 2 대신 (count - 1) / 2단계 이동합니다.
빠른 포인터와 느린 포인터 기법은 또 어디에 사용되나요?
같은 두 가지 속도를 사용하면 연결 리스트에서 사이클을 감지할 수 있습니다. 루프에서는 빠른 포인터가 느린 포인터를 따라잡아 둘이 만납니다. 또한 사이클이 시작되는 위치를 찾고, 병합 정렬을 하거나 리스트가 양방향으로 동일하게 읽히는지 확인하기 위해 리스트를 반으로 나눌 수도 있습니다.
비슷한 문제
같은 아이디어를 쓰는 문제입니다. 두세 개를 풀면 패턴이 몸에 익습니다.
Python
def middleNode(values, next):
# 여기에 코드를 작성하세요케이스 1
케이스 2
케이스 3
입력
values = [4, 9, 2, 7, 5] next = [3, -1, 1, 4, 2]
기대값
5