참조
이 글은 이것이 취업을 위한 코딩 테스트다 with 파이썬을 읽고 작성하였습니다.
투 포인터 알고리즘은 리스트에 순차적으로 접근해야 할 때 2개의 점의 위치를 기록하면서 처리하는 알고리즘을 의미합니다. 예를 들어서 한 반에 학생이 40명이 있을 때, 모든 학생을 번호순대로 일렬로 세운 뒤, 학생들을 순차적으로 지목해야 할 경우를 생각해보겠습니다. 2,3,4,5,6,7번 학생을 지목해야 할 때, 번호로 한명씩 부르기보다는 '2번부터 7번까지의 학생'이라고 부를 수도 있습니다. 이처럼 리스트에 담긴 데이터에 순차적으로 접근해야 할 때는 '시작점'과 '끝점' 2개의 점으로 접근할 데이터의 범위를 표현할 수 있습니다.
이런 투 포인터 알고리즘을 이용하여 '특정한 합을 가지는 부분 수열 찾기' 문제를 풀 수 있습니다. '특정한 합을 가지는 부분 연속 수열 찾기 문제'는 양의 정수로만 구성된 리스트가 주어졌을 때, 그 부분 연속 수열 중에서 '특정한 합'을 갖는 수열의 개수를 출력하는 문제입니다.
1,2,3,2,5를 차례대로 갖는 리스트가 주어졌 있다고 가정하겠습니다.
이 문제를 투 포인터 알고리즘을 이용하여 풀겠습니다. 투 포인터 알고리즘의 특징은 2개의 변수를 이용해 리스트 상의 위치를 기록한다는 점입니다. '특정한 합을 가지는 부분 연속 수열 찾기' 문제에서는 부분 연속 수열의 시작점(start)과 끝점(end)의 위치를 기록합니다. 특정한 부분합을 M이라고 할 때, 구체적 알고리즘은 아래와 같습니다.
위의 알고리즘을 통해 부분합이 5인 부분 연속 수열의 수를 계산하기 위한 구체적인 과정을 알아보겠습니다.
알고리즘에 따라서 초기 단계에서는 시작점과 끝점이 첫 번째 원소의 인덱스를 가리키도록 합니다. 이때 현재의 부분합은 1이므로 무시됩니다.
이전 단계에서의 부분합이 1이었기 때문에 end를 1 증가시킵니다. 현재 부분합은 3이므로 이번에도 마찬가지로 무시됩니다.
이전 단계에서의 부분합이 3이었기 때문에 end를 1 증가시킵니다. 현재 부분합은 6이므로 이번에도 마찬가지로 무시됩니다.
이전 단계에서의 부분합이 6이었기 때문에 start를 1 증가시킵니다. 현재 부분합이 5이므로 하나의 경우를 찾은 것이기에 카운트를 증가시킵니다.
이전 단계에서의 부분합이 5였기 때문에 start를 1 증가시킵니다. 현재 부분합이 3이므로 무시합니다.
이전 단계에서의 부분합이 3이었기 때문에 end를 1 증가시킵니다. 현재 부분합이 5이므로 하나의 경우를 찾은 것이기에 카운트를 증가시킵니다.
이전 단계에서의 부분합이 5였기 때문에 start를 1 증가시킵니다. 현재 부분합은 2이므로 무시합니다.
이전 단계에서의 부분합이 2였기 때문에 end를 1 증가시킵니다. 현재 부분합은 7이므로 무시됩니다.
이전 단계에서의 부분합이 7이었기 때문에 start를 1 증가시킵니다. 현재 부분합은 5이므로 하나의 경우를 찾은 것이기에 카운트를 증가시킵니다.
결과적으로 카운트된 경우의 수는 3입니다. 투 포인터 알고리즘은 구현 가능한 방식이 매우 많다는 특징이 있습니다. 아래의 코드에서는 시작점(start)을 반복문을 이용하여 증가시키며, 증가할 때마다 끝점(end)을 그것에 맞게 증가시키는 방식으로 구현하였습니다. 이 문제를 투 포인터 알고리즘으로 해결할 수 있는 이유는 기본적으로 시작점을 오른쪽으로 이동시키면 항상 합이 감소하고, 끝점을 오른쪽으로 이동시키면 항상 합이 증가하기 때문입니다. 만약 리스트 내 원소에 음수 데이터가 포함되어 있는 경우에는 투 포인터 알고리즘으로 문제를 해결할 수 없습니다.
n = 5 # 데이터의 개수
m = 5 # 찾고자 하는 부분 합 M
data = [1,2,3,2,5] # 전체 수열
# 카운트
count = 0
# 합을 계산하기 위한 변수
interval_sum = 0
# 끝점 변수
end = 0
# start를 차례대로 증가시키며 반복
for start in range(n):
# end를 가능한 만큼 이동시키기
while interval_sum < m and end < n:
interval_sum += data[end]
end += 1
# 부분합이 m일 때 카운트 증가
if interval_sum == m:
count += 1
interval_sum -= data[start]
print(count)
투 포인터 알고리즘은 '정렬되어 있는 두 리스트의 합집합' 같은 문제에 효과적으로 사용할 수 있습니다. 이 문제는 이미 정렬되어 있는 2개의 리스트가 입력으로 주어지고 이 두 리스트의 모든 원소를 합쳐서 정렬한 결과를 계산하는 것이 문제의 요구사항입니다.
이 문제를 풀기 위해 2개의 리스트 A, B가 주어졌을 때, 2개의 포인터를 이용하여 각 리스트에서 처리되지 않은 원소 중 가장 작은 원소를 가리키면 됩니다. 이 문제에서는 기본적으로 이미 정렬된 결과가 주어지므로 리스트 A와 B의 원소를 앞에서부터 확인하면 됩니다.
초기 단계에서는 두 리스트의 모든 원소가 들어갈 수 있는 크기의 결과 리스트를 하나 생성합니다. 또한 i가 리스트 A의 첫 번째 원소를 가리키도록 하고, j가 리스트 B의 첫 번째 원소를 가리키도록 합니다.
현재 i와 j가 가리키는 두 원소를 비교합니다. A[i]=1, B[j]=2입니다. A[i] < B[j]이므로 결과 리스트에 i가 가리키고 있는 원소를 담습니다. 이후에 i를 1 증가시킵니다.
현재 i와 j가 가리키는 두 원소를 비교합니다. A[i]=3, B[j]=2입니다. A[i] > B[j]이므로 결과 리스트에 j가 가리키고 있는 원소를 담습니다. 이후에 j를 1 증가시킵니다.
현재 i와 j가 가리키는 두 원소를 비교합니다. A[i]=3, B[j]=4입니다. A[i] < B[j]이므로 결과 리스트에 i가 가리키고 있는 원소를 담습니다. 이후에 i를 1 증가시킵니다.
현재 i와 j가 가리키는 두 원소를 비교합니다. A[i]=5, B[j]=4입니다. A[i] > B[j]이므로 결과 리스트에 j가 가리키고 있는 원소를 담습니다. 이후에 j를 1 증가시킵니다.
현재 i와 j가 가리키는 두 원소를 비교합니다. A[i]=5, B[j]=6입니다. A[i] < B[j]이므로 결과 리스트에 i가 가리키고 있는 원소를 담습니다. 이후에 i를 1 증가시킵니다.
이제 리스트 A에 있는 모든 원소가 결과 리스트에 담겼습니다. 남은 리스트 B에 있는 모든 원소를 겨로가 리스트에 담으면 됩니다.
결과적으로 정렬된 리스트 A와 B의 데이터 개수가 각각 N,M이라고 할 때, 이 알고리즘의 시간 복잡도는 O(N+M)입니다. 단순히 각 리스트의 모든 원소를 한 번씩만 순회하면 되기 때문입니다.
# 사전에 정렬된 리스트 A와 B 선언
n, m = 3, 4
a = [1,3,5]
b = [2,4,6,8]
# 리스트 A와 B의 모든 원소를 담을 수 있는 크기의 결과 리스트 초기화
result = [0] * (n + m)
# 인덱스 초기화
i = 0
j = 0
k = 0
# 모든 원소가 결과 리스트에 담길 때까지 반복
while i < n or j < m:
# 리스트 B의 모든 원소가 처리되었거나, 리스트 A의 원소가 더 작을 때
if j >= m or (i < n and a[i] <= b[j]):
# 리스트 A의 원소를 결과 리스트로 옮기기
result[k] = a[i]
# 리스트 A 인덱스 더하기
i += 1
# 리스트 A의 모든 원소가 처리되었거나, 리스트 B의 원소가 더 작을 때
else:
# 리스트 B의 원소를 결과 리스트로 옮기기
result[k] = b[j]
# 리스트 B 인덱스 더하기
j += 1
# 결과리스트 인덱스 더하기
k += 1
# 결과 리스트 출력
for i in result:
print(i, end=" ")
이 '정렬되어 있는 두 리스트의 합집합' 알고리즘의 경우 병합 정렬(Merge Sort)와 같은 일부 알고리즘에서 사용되고 있습니다.