본 시리즈는 프로그래밍 알고리즘의 개념을 정리하고 실습을 진행해보는 시리즈입니다.
탐색(Search) 알고리즘은 주어진 데이터 구조 내에서 원하는 데이터를 찾아내기 위한 일련의 과정입니다.
탐색 알고리즘은 크게 두 가지 범주로 나눌 수 있습니다:
이번 시리즈에서는 이러한 다양한 탐색 알고리즘을 순차적으로 다루면서 그 개념과 활용 방법을 소개합니다.
이번 포스팅에서는 배열 기반의 투 포인터(Two-Pointer)와 이진 탐색(Binary Search)을 중점적으로 설명하고, 이후 포스팅에서는 깊이 우선 탐색(DFS), 너비 우선 탐색(BFS), 백트래킹(Backtracking) 등의 보다 복잡한 탐색 기법들을 다룰 예정입니다.
Two-Pointer 알고리즘은 배열과 같은 선형 자료구조에서 두 개의 포인터를 이용해 원하는 조건을 만족하는 값을 찾는 알고리즘입니다.
Two-Pointer 알고리즘은 두 포인터를 배열의 서로 다른 위치에 두고 점차적으로 이동시키며 탐색하는 방식입니다. 포인터가 시작되는 위치에 따라 두 가지 일반적인 경우가 있습니다.
두 포인터를 배열의 양쪽 끝에 두고, 각각 서로를 향해 이동시키는 방식입니다.
이 방식은 주로 두 수의 합을 찾는 문제에서 사용됩니다.
두 포인터가 같은 방향에서 출발해 한쪽으로 이동하는 방식입니다.
이 방식은 주로 부분합이나 연속된 구간을 찾는 문제에서 사용됩니다.
예시 1: 두 수의 합 찾기 (양 끝에서 시작)
주어진 배열 [7, 15, 22, 25, 40, 50, 55, 65, 70, 75, 90, 99]
에서 두 수의 합이 100
이 되는 쌍을 찾아보겠습니다.
예시 2: 연속 부분 배열의 합 찾기 (같은 방향에서 시작)
정렬되지 않은 배열에서 연속된 부분 배열의 합이 특정 값을 만족하는 구간을 찾아보겠습니다.
[7, 2, 5, 1, 8, 6, 3]
에서 연속 부분 배열의 합이 16
이 되는 구간을 찾는 문제입니다.# 예시 1: 두 수의 합 찾기 (양 끝에서 시작)
def two_sum(arr, target):
i, j = 0, len(arr) - 1
while i < j:
total = arr[i] + arr[j]
if total == target:
return i, j
elif total < target:
i += 1
else:
j -= 1
return None
# 예시 2: 연속 부분 배열의 합 찾기 (같은 방향에서 시작)
def find_subarray_with_sum(arr, target):
i, j = 0, 0
current_sum = 0
while j < len(arr):
current_sum += arr[j]
while current_sum > target and i < j:
current_sum -= arr[i]
i += 1
if current_sum == target:
return i, j
j += 1
return None
# 테스트
arr1 = [7, 15, 22, 25, 40, 50, 55, 65, 70, 75, 90, 99]
target1 = 100
print(two_sum(arr1, target1)) # 출력: (3, 9)
arr2 = [7, 2, 5, 1, 8, 6, 3]
target2 = 16
print(find_subarray_with_sum(arr2, target2)) # 출력: (1, 4)
Python 테스트 결과 해석
예시 1: 두 수의 합 찾기
배열 [7, 15, 22, 25, 40, 50, 55, 65, 70, 75, 90, 99]
에서 두 수의 합이 100
이 되는 값을 찾으면, 포인터가 가리키는 값 arr[3] = 25
와 arr[9] = 75
를 더하면 100
이 됩니다.
3
과 9
의 값을 더한 합이 목표 값 100
과 일치함을 확인할 수 있습니다.예시 2: 연속 부분 배열의 합 찾기
배열 [7, 2, 5, 1, 8, 6, 3]
에서 연속된 부분 배열의 합이 16
이 되는 구간을 찾으면, 인덱스 1
에서 4
까지의 구간 [2, 5, 1, 8]
의 합이 16
이 됩니다.
1
부터 4
까지의 합이 16
이 되는 구간을 찾았음을 확인할 수 있습니다.// 예시 1: 두 수의 합 찾기 (양 끝에서 시작)
public class TwoPointerExample {
public static int[] twoSum(int[] arr, int target) {
int i = 0, j = arr.length - 1;
while (i < j) {
int sum = arr[i] + arr[j];
if (sum == target) {
return new int[]{i, j};
} else if (sum < target) {
i++;
} else {
j--;
}
}
return null; // 합을 찾지 못한 경우
}
// 예시 2: 연속 부분 배열의 합 찾기 (같은 방향에서 시작)
public static int[] findSubarrayWithSum(int[] arr, int target) {
int i = 0, currentSum = 0;
for (int j = 0; j < arr.length; j++) {
currentSum += arr[j];
while (currentSum > target && i < j) {
currentSum -= arr[i];
i++;
}
if (currentSum == target) {
return new int[]{i, j};
}
}
return null; // 합을 찾지 못한 경우
}
public static void main(String[] args) {
// 예시 1 테스트
int[] arr1 = {7, 15, 22, 25, 40, 50, 55, 65, 70, 75, 90, 99};
int target1 = 100;
int[] result1 = twoSum(arr1, target1);
if (result1 != null) {
System.out.println("Indices: " + result1[0] + ", " + result1[1]);
} else {
System.out.println("No pair found.");
}
// 예시 2 테스트
int[] arr2 = {7, 2, 5, 1, 8, 6, 3};
int target2 = 16;
int[] result2 = findSubarrayWithSum(arr2, target2);
if (result2 != null) {
System.out.println("Subarray found from index " + result2[0] + " to " + result2[1]);
} else {
System.out.println("No subarray found.");
}
}
}
Java 테스트 결과 해석
예시 1: 두 수의 합 찾기
배열 [7, 15, 22, 25, 40, 50, 55, 65, 70, 75, 90, 99]
에서 두 수의 합이 100
이 되는 쌍을 찾으면, 인덱스 3
의 값 25
와 인덱스 9
의 값 75
를 더한 값이 100
입니다.
3
과 9
의 두 수가 합이 100
이 되는 쌍임을 확인할 수 있습니다.예시 2: 연속 부분 배열의 합 찾기
배열 [7, 2, 5, 1, 8, 6, 3]
에서 연속된 부분 배열의 합이 16
이 되는 구간을 찾으면, 인덱스 1
부터 4
까지의 구간 [2, 5, 1, 8]
의 합이 16
입니다.
1
에서 4
까지의 연속 부분 배열의 합이 16
임을 확인할 수 있습니다.Two-Pointer 알고리즘의 시간 복잡도는 O(n)으로, 배열을 한 번만 순회하기 때문에 매우 효율적입니다.
이를 선형 탐색(Brute Force)와 비교하면, 선형 탐색의 경우는 모든 가능한 쌍을 검사해야 하므로 의 시간 복잡도를 가집니다.
n
개의 요소에 대해 각각 n
번씩 검사가 이루어집니다.반면, Two-Pointer 알고리즘은 배열이 정렬된 상태에서 포인터를 조정해가며 필요한 계산을 하므로 한 번의 순회(O(n))만으로 문제를 해결할 수 있습니다.
Binary Search(이진 탐색) 알고리즘은 정렬된 배열에서 특정 값을 찾기 위해 사용되는 효율적인 탐색 알고리즘입니다.
Binary Search는 다음과 같은 단계로 동작합니다:
(left + right) // 2
로 계산됩니다.(left + right)
에서 int
데이터 타입의 overflow
가 일어날수 있습니다. 이때는 left + (right - left) // 2
와 같은 방식으로 사용하시면 됩니다.right = mid - 1
로 갱신합니다.left = mid + 1
로 갱신합니다.이진 탐색은 배열이 정렬된 상태일 때만 사용할 수 있으며, 배열의 크기가 클수록 탐색 속도가 매우 빠릅니다.
주어진 배열 [7, 15, 22, 25, 40, 50, 55, 65, 70, 75, 90, 99]
에서 값 65를 찾는 과정을 살펴보겠습니다.
초기 상태:
5
, 중간값은 arr[5] = 50
)첫 번째 비교: arr[5] = 50
이므로, 목표 값 65
보다 작습니다. 따라서 left = mid + 1
로 갱신하여 탐색 범위를 좁힙니다.
8
, 중간값은 arr[8] = 70
)두 번째 비교: arr[8] = 70
이므로, 목표 값 65
보다 큽니다. 따라서 right = mid - 1
로 갱신하여 탐색 범위를 좁힙니다.
6
, 중간값은 arr[6] = 55
)세 번째 비교: arr[6] = 55
이므로, 목표 값 65보다 작습니다. 따라서 left = mid + 1
로 갱신합니다.
7
, 중간값은 arr[7] = 65
)결과: 중간값 arr[7] = 65
는 목표 값 65
와 일치하므로, 탐색이 성공적으로 종료됩니다.
Python 코드 예시
def binary_search(arr, target):
left, right = 0, len(arr) - 1
while left <= right:
mid = (left + right) // 2
if arr[mid] == target:
return mid
elif arr[mid] < target:
left = mid + 1
else:
right = mid - 1
return -1 # 값을 찾지 못한 경우
# 테스트
arr = [7, 15, 22, 25, 40, 50, 55, 65, 70, 75, 90, 99]
target = 65
result = binary_search(arr, target)
print(f"Target {target} found at index {result}") # 출력: Target 65 found at index 7
Java 코드 예시
public class BinarySearchExample {
public static int binarySearch(int[] arr, int target) {
int left = 0, right = arr.length - 1;
while (left <= right) {
int mid = (left + right) / 2;
if (arr[mid] == target) {
return mid;
} else if (arr[mid] < target) {
left = mid + 1;
} else {
right = mid - 1;
}
}
return -1; // 값을 찾지 못한 경우
}
public static void main(String[] args) {
int[] arr = {7, 15, 22, 25, 40, 50, 55, 65, 70, 75, 90, 99};
int target = 65;
int result = binarySearch(arr, target);
if (result != -1) {
System.out.println("Target " + target + " found at index " + result);
} else {
System.out.println("Target not found.");
}
}
}
Binary Search의 시간 복잡도는 O(log n)입니다.
이진 탐색은 다음과 같이 작동합니다:
선형 탐색이 O(n)의 복잡도를 가지는 것에 비해, 이진 탐색은 훨씬 더 빠르게 목표 값을 찾을 수 있습니다.
1,000,000
일 때 선형 탐색은 최악의 경우 1,000,000
번 비교가 필요하지만, 이진 탐색은 20번 미만의 비교로 목표 값을 찾을 수 있습니다.이번 포스팅에서는 배열 기반 탐색 알고리즘 중 Two-Pointer와 Binary Search에 대해 살펴보았습니다. 두 알고리즘 모두 효율적으로 탐색을 수행할 수 있는 방법으로, 배열에서 특정 값을 빠르게 찾는 데 중요한 역할을 합니다.
다음 포스팅에서는 그래프 및 트리 기반 탐색으로 깊이 우선 탐색(DFS), 너비 우선 탐색(BFS)을 다루고 이후에 백트래킹(Backtracking) 알고리즘에 대해 다룰 예정입니다.