[코테] 이진 탐색

uuuu.jini·2022년 12월 3일
0

범위를 반씩 좁혀가는 탐색


> 순차 탐색

리스트 내에서 데이터를 매우 빠르게 탐색하는 이진 탐색 알고리즘에 대해 공부한다. 이진 탐색을 알아보기 전에 가장 기본 탐색 방법인 순차 탐색에 대해 먼저 이해해야 한다. 예제 문제에서 N개의 데이터가 있을 때, 그 데이터를 차례대로 하나씩 확인하여 어떠한 처리를 해준 경우가 많았는데 그 자체로도 이미 순차 탐색이라고 할 수 있다.

순차 탐색(Sequential Search) 이란 리스트 안에 있는 특정한 데이터를 찾기 위해 앞에서부터 데이터를 차례대로 확인하는 방법이다. 보통 정렬되지 않은 리스트에서 데이터를 찾아야 할 때 사용한다. 리스트 내에 데이터가 아무리 많아도 시간만 충분하다면 항상 원하는 원소(데이터)를 찾을 수 있다는 장점이 있다.

순차 탐색은 이름처럼 순차로 데이터를 탐색한다는 의미이다. 리스트의 데이터에 하나씩 방문하며 특정한 문자열과 같은지 검사하므로 구현도 간단하다. 순차 탐색은 정말 자주 사용되는데, 리스트에 특정 값의 원소가 있는지 체크할 때도 순차 탐색으로 원소를 확인하고, 리스트 자료형에서 특정한 값을 가지는 원소의 개수를 세는 count() 메서드를 이용할 때도 내부에서는 순차 탐색이 수행된다.

'''
순차 탐색 코드
'''

# 순차 탐색 소스코드 구현


def sequential_search(n, target, array):
    # 각 원소를 하나씩 확인하며
    for i in range(n):
        # 현재의 원소가 찾고자 하는 원소와 동일한 경우
        if array[i] == target:
            return i + 1  # 현재의 위치 반환 (인덱스는 0부터 시작하므로 1 더하기)


print("생성할 원소의 개수를 입력한 다음 한 칸 띄고 문자열을 입력하세요.")
input_data = input().split()
n = int(input_data[0])
target = input_data[1]

print("앞서 적은 원소 개수만큼 문자열을 입력하세요. 구분은 띄어쓰기 한 칸으로 합니다.")
array = input().split()

print(sequential_search(n, target, array))

소스코드를 실행하면 정상적으로 이름 문자열이 몇 번째 데이터인지 출력한다. 이처럼 순차 탐색은 데이터 정렬 여부와 상관없이 가장 앞에 있는 원소부터 하나씩 확인해야 한다는 점이 특징이다. 따라서 데이터의 개수가 N개일 때 최대 N번의 비교 연산이 필요하므로 순차 탐색의 최악의 경우 시간 복잡도는 O(N)이다.

> 이진 탐색: 반으로 쪼개면서 탐색하기

이진 탐색(Binary Search) 은 배열 내부의 데이터가 정렬되어 있어야만 사용이 가능하다. 데이터가 무작위일 때는 사용할 수 없지만, 이미 정렬되어 있다면 매우 빠르게 데이터를 찾을 수 있다는 특징이 있다. 이진 탐색은 탐색 범위를 절반씩 좁혀가며 데이터를 탐색하는 특징이 있다.

이진 탐색은 위치를 나타내는 변수 3개를 사용하는데 탐색하고자 하는 범위의 시작점, 끝점 그리고 중간점이다. 찾으려는 데이터와 중간점 위치에 있는 데이터를 반복적으로 비교해서 원하는 데이터를 찾는게 이진 탐색 과정이다.

시작점과 끝점을 확인한 다음 둘 사이에 중간점을 찾는다. 소수점인 경우 소수점 이하를 버린다. (if [0] & [9] -> [4.5] -> [4]

이진 탐색은 한 번 확인할 때마다 확인하는 원소의 개수가 절반씩 줄어든다는 점에서 시간 복잡도가 O(logN)이다. 절반씩 데이터를 줄어들도록 만든다는 점은 앞서 다룬 퀵 정렬과 공통점이 있다.

이진 탐색을 구현하는 방법에는 2가지가 있다. 그 중 하나는 재귀함수를 이용하는 방법이고, 다른 하나는 단순하게 반복문을 이용하는 방법이다.

- 재귀함수를 이용한 이진 탐색 소스코드

# 이진 탐색 소스코드 구현 (재귀함수)
def binary_search(array, target, start, end):
    if start > end:
        return None
    mid = (start + end) // 2
    # 찾은 경우 중간점 인덱스 반환
    if array[mid] == target:
        return mid
    # 중간점의 값보다 찾고자 하는 값이 작은 경우 왼쪽 확인
    elif array[mid] > target:
        return binary_search(array, target, start, mid-1)
    # 중간점의 값보다 찾고자 하는 값이 큰 경우 오른쪽 확인
    elif array[mid] < target:
        return binary_search(array, target, mid+1, end)


# n(원소의 개수)과 target(찾고자 하는 문자열)을 입력받기
n, target = list(map(int, input().split()))
# 전체 원소 입력받기
array = list(map(int, input().split()))

# 이진 탐색 수행 결과 출력
result = binary_search(array, target, 0, n-1)
if result == None:
    print("원소가 존재하지 않습니다.")
else:
    print(result + 1)

mid = (start + end) // 2는 중간점을 의미한다. 2로 나눈 몫만 구하기 위해 몫 연산자(//)를 사용한 것이다.

- 반복문을 사용한 이진 탐색 소스코드

# 이진 탐색 소스코드 구현(반복문)
def binary_search(array, target, start, end):
    while start <= end:
        mid = (start + end) // 2
        # 찾은 경우 인덱스 반환
        if array[mid] == target:
            return mid
        elif array[mid] > target:
            end = mid - 1
        else:
            start = mid + 1
    return None


# n(원소의 개수)과 target(찾고자 하는 문자열)을 입력받기
n, target = list(map(int, input().split()))
# 전체 원소 입력받기
array = list(map(int, input().split()))

# 이진 탐색 수행 결과 출력
result = binary_search(array, target, 0, n-1)
if result == None:
    print("원소가 존재하지 않습니다.")
else:
    print(result + 1)

코딩 테스트에서의 이진 탐색

제대로 이진 탐색 코드를 작성한 프로그래머는 10% 내외! 실제 구현 까다롭다. 외워야 한다. 높은 난이도의 문제에서는 이진 탐색 알고리즘이 다른 알고리즘과 함께 사용되기도 한다. 탐색 범위가 2,000만을 넘어가면 이진 탐색으로 문제를 접근하길 권한다.

> 트리 자료구조

이진 탐색은 전제 조건이 데이터 정렬이다. 동작하는 프로그램에서 데이터를 정렬해두는 경우가 많으므로 이진 탐색을 효과적으로 사용할 수 있다. 데이터베이스는 내부적으로 대용량 데이터 처리에 적합한 트리 자료구조를 이용하여 항상 데이터가 정렬되어 있다. 따라서 데이터베이스에서의 탐색은 이진 탐색과는 조금 다르지만, 이진 탐색과 유사한 방법을 이용해 탐색을 항상 빠르게 수행하도록 설계되어 있어서 데이터가 많아도 탐색하는 속도가 빠르다.

트리 자료구조 는 노드와 노드의 연결로 표현하며 여기에서 노드는 정보의 단위로서 어떠한 정보를 가지고 있는 개체로 이해할 수 있다. 트리 자료구조는 그래프 자료구조의 일종으로 데이터베이스 시스템이나 파일 시스템과 같은 곳에서 많은 양의 데이터를 관리하기 위한 목적으로 사용한다. 트리 자료구조는 몇 가지 주요한 특징이 있다.

  • 트리는 부모 노드와 자식 노드의 관계로 표현된다.
  • 트리의 최상단 노드를 루트 노드라고 한다.
  • 트리의 최하단 노드를 단말 노드라고 한다.
  • 트리에서 일부를 떼어내도 트리 구조이며 이를 서브 트리라 한다.
  • 트리는 파일 시스템과 같이 계층적이고 정렬된 데이터를 다루기에 적합하다.

정리하자면 큰 데이터를 처리하는 소프트웨어는 대부분 데이터를 트리 자료구조로 저장해서 이진 탐색과 같은 탐색 기법을 이용해 빠르게 탐색이 가능하다.

> 이진 탐색 트리

트리 자료구조 중에서 가장 간단한 형태가 이진 탐색 트리이다. 이진 탐색 트리란 이진 탐색이 동작 할 수 있도록 고안된, 효율적인 탐색이 가능한 자료구조이다.

이진 탐색 트리는 다음과 같은 특징을 가진다.

  • 부모 노드보다 왼쪽 자식 노드가 작다.
  • 부모 노드보다 오른쪽 자식 노드가 크다.

간단하게 표현하면 왼쪽 자식 노드 < 부모 노드 < 오른쪽 자식 노드가 성립해야지 이진 탐색 트리라고 할 수 있다.

이진 탐색 트리에 데이터를 넣고 빼는 방법은 알고리즘보다는 자료구조에 가까우며, 이진 탐색 트리 자료구조를 구현하도록 요구하는 문제는 출제 빈도가 낮다.

따라서 이진 탐색 트리가 미리 구현되어 있다고 가정하고 데이터를 조회하는 과정만 살펴본다. 찾는 원소가 37일 때 동작하는 과정을 본다.

  1. 이진 탐색은 루트 노드부터 방문한다. 루트 노드는 30이고 찾는 원소값은 37이다. 공식에 따라 부모 노드의 왼쪽 자식 노드는 30이하이므로 왼쪽에 있는 모든 노드는 확인할 필요가 없다. 따라서 오른쪽 노드를 방문한다.
  2. 오른쪽 자식 노드인 48이 이번에는 부모 노드이다. 48은 찾는 원소값인 37 보다 크다. 공식에 따라 부모 노드의 오른쪽 자식인 노드는 모두 48 이상이므로 확인할 필요가 없다. 따라서 왼쪽 노드를 방문한다.
  3. 현재 방문한 노드의 값인 37과 찾는 원소값인 37이 동일하다 따라서 탐색을 마친다.

이진 탐색 트리에서 데이터 조회는 동작 원리만 살펴보면 간단하게 느껴진다. 공식에 따라 루트 노드부터 왼쪽 자식 노드 혹은 오른쪽 자식 노드로 이동하며 반복적으로 방문한다. 자식 노드가 없을 때 까지 원소를 찾지 못했다면, 이진 탐색 트리에 원소가 없는 것이다.

- 빠르게 입력받기

이진 탐색 문제는 입력 데이터가 많거나, 탐색 범위가 매우 넓은 편이다. 예를 들어 데이터의 개수가 1,000만 개를 넘어가거나 탐색 범위의 크기가 1,000억 이상이라면 이진 탐색 알고리즘을 의심해보자. 이렇게 입력 데이터의 개수가 많은 문제에 input() 함수를 사용하면 동작 속도가 느려서 시간 초과로 오답 판정을 받을 수 있다. 이처럼 입력 데이터가 많은 문제는 sys 라이브러리의 readline() 함수를 이용하면 시간 초과를 피할 수 있다.

때로는 코딩 테스트 출제자가 아예 sys 라이브러리를 사용하기를 권고하는 문장을 문제에 적어놓기도 한다.

import sys
input_data = sys.stdin.readline().rstrip()

print(input_data)

sys 라이브러리를 사용할 때는 한 줄 입력받고 나서 rstrip() 함수를 꼭 호출해야 한다. 소스코드에 readline()으로 입력하면 입력 후 엔터가 줄ㄹ 바꿈 기호로 입력되는데, 이 공백 문자를 제거하려면 rstrip() 함수를 사용해야 한다.

실전 문제


1. 부품 찾기

전자 매장에는 부품이 N개 있다. 각 부품은 정수 형태의 고유한 번호가 있다. 어느 날 손님이 M개 종류의 부품을 대량으로 구매하겠다며 당일 날 견적서를 요청했다. 동빈이는 때를 놓치지 않고 손님이 문의한 부품 M개 종류를 모두 확인해서 견적서를 작성해야 한다. 이때 가게 안에 부품이 모두 있는지 확인하는 프로그램을 작성한다.

예를 들어 가게의 부품이 총 5개일 때 부품 번호가 다음과 같다고 하자.

N = 5
[8, 3, 7, 9, 2]

손님은 총 3개의 부품이 있는지 확인 요청했는데 부품 번호는 다음과 같다.

M = 3
[5, 7, 9]

이때 손님이 요청한 부품 번호의 순서대로 부품을 확인해 부품이 있으면 yes를, 없으면 no를 출력한다. 구분은 공백으로 한다.

import sys
input = sys.stdin.readline

N = int(input())
tools = list(map(int, input().split()))
M = int(input())
customers = list(map(int, input().split()))

tools.sort()


def find_tool(tools, customer, start, end):
    if start > end:
        return False
    mid = (start + end) // 2
    if tools[mid] == customer:
        return True
    elif tools[mid] < customer:
        return find_tool(tools, customer, mid + 1, end)
    else:
        return find_tool(tools, customer, start, mid - 1)


for customer in customers:
    print('yes' if find_tool(tools, customer, 0, N-1) else 'no', end=' ')

> 떡볶이 떡 만들기

여행 가신 부모님을 대신해서 떡집 일을 대신하기로 했다. 오늘은 떡볶이 떡을 만드는 날이다. 떡볶이 떡은 재밌게도 떡볶이 떡의 길이가 일정하지 않다. 대신에 한 봉지 안에 들어가는 떡의 총 길이는 절단기로 잘라서 맞춰준다.

절단기에 높이(H)를 지정하면 줄지어진 떡을 한번에 절단한다. 높이가 H보다 긴 떡은 H 위의 부분이 잘릴 것이고, 낮은 떡은 잘리지 않는다.

예를 들어 높이가 19, 14, 10, 17cm 인 떡이 나란히 있고 절단기 높이를 15cm로 지정하면 자른 뒤 떡의 높이는 15, 14, 10, 15cm 가 될 것이다. 잘린 떡의 길이는 차례대로 4, 0, 0, 2cm이다. 손님은 6cm 만큼의 길이를 가져간다.

손님이 왔을 때 요청한 총 길이가 M일 때 적어도 M만큼의 떡을 얻기 위해 절단기에 설정할 수 있는 높이의 최댓값을 구하는 프로그램을 작성하시오.

import sys
input = sys.stdin.readline

N, M = map(int, input().split())
rice_cakes = list(map(int, input().split()))
rice_cakes.sort()
max_rice_cake = rice_cakes[len(rice_cakes)-1]  # 최대 떡 길이


def find_max_cutting_rice_cake_cm(rice_cakes, cm, start, end, result):
    if start > end:
        return result
    mid_cm = (start + end) // 2
    cutting_rice_cakes = 0

    for rice_cake in rice_cakes:
        cutting_cm = rice_cake - mid_cm
        if cutting_cm > 0:
            cutting_rice_cakes += cutting_cm

    if cutting_rice_cakes >= cm:
        result = mid_cm
        return find_max_cutting_rice_cake_cm(rice_cakes, cm, mid_cm + 1, end, result)
    else:
        return find_max_cutting_rice_cake_cm(rice_cakes, cm, start, mid_cm - 1, result)


result = 0
print(find_max_cutting_rice_cake_cm(rice_cakes, M, 0, max_rice_cake, result))

풀긴 풀었는데 말이G

이 문제의 풀이 아이디어는 의외로 간단하다. 적절한 높이를 찾을 때까지 절단기의 높이 H를 반복적으로 조정하는 것이다. 그래서 현재 이 높이로 자르면 조건을 만족할 수 있는가?를 확인한 뒤에 조건의 만족여부에 따라 탐색 범위를 좁혀서 해결할 수 있다. 범위를 좁힐 때는 이진 탐색의 원리를 이용하여 절반씩 탐색 범위를 좁혀 나간다.

절단기의 높이 H는 0부터 가장 긴 떡의 길이 안에 있어야 떡을 자를 수 있다.

나 잘한듯!
(정정) 하긴 한듯 잘한건 아닌듯 ;

profile
멋쟁이 토마토

0개의 댓글