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
- 설명
- 노드가 하나인 목록에서는 그 노드가 중간입니다.
후속 질문
한 번의 순회로 목록의 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_로 지정합니다.
자주 묻는 질문3
빠른 포인터와 느린 포인터는 왜 연결 리스트의 중간을 찾을까요?
둘 다 머리에서 시작하며, 각 라운드마다 빠른 포인터는 노드를 두 개 이동하고 느린 포인터는 한 개 이동합니다. k라운드 후 빠른 포인터는 2k 위치에 있고 느린 포인터는 k 위치에 있어, 이동 거리는 정확히 절반입니다. 따라서 빠른 포인터가 리스트의 끝에 도달하면 느린 포인터는 리스트의 중간에 있습니다.
연결 리스트의 중간을 찾는 시간 및 공간 복잡도는 얼마인가요?
세 가지 접근 방식 모두 O(n) 시간이 걸립니다. 리스트의 중간을 찾으려면 리스트의 절반 정도 또는 그 이상을 순회해야 하기 때문입니다. 값을 복사하면 O(n)의 추가 메모리를 사용합니다. 먼저 개수를 세는 방식과 빠른 포인터 및 느린 포인터 방식은 모두 O(1)을 사용하며, 포인터 방식은 한 번만 순회하면 됩니다.
두 번째 노드 대신 첫 번째 중간 노드를 어떻게 반환하나요?
빠른 포인터가 한 라운드 더 일찍 멈추도록 중지 규칙을 변경하세요. next[fast] != -1 및 next[next[fast]] != -1인 동안 반복합니다. 노드가 6개일 때 느린 포인터는 3 대신 위치 2에서 멈춥니다. 카운팅 방식에서는 count / 2 대신 (count - 1) / 2단계 이동합니다.
비슷한 문제
같은 아이디어를 쓰는 문제입니다. 두세 개를 풀면 패턴이 몸에 익습니다.
def middleNode(values, next):
# 여기에 코드를 작성하세요테스트 #1
테스트 #2
테스트 #3
인자들
values = [4, 9, 2, 7, 5] next = [3, -1, 1, 4, 2]
예상 출력
5