인덱스 바이너리

마지막 업데이트: 2022년 5월 11일 | 0개 댓글
  • 네이버 블로그 공유하기
  • 네이버 밴드에 공유하기
  • 페이스북 공유하기
  • 트위터 공유하기
  • 카카오스토리 공유하기
이번 강좌는 MongoDB의 Index에 관한 내용입니다.

인덱스 바이너리

리트코드에서 다음과 같은 알고리즘 문제를 풀게 되었다.

35. Search Insert Position
Given a sorted array and a target value, return the index if the target is found. If not, return the index where it would be if it were inserted 인덱스 바이너리 in order.

오름차순으로 정렬된 배열과 타겟값이 주어진다. 배열 내에서 타겟값과 동일한 요소의 인덱스를 찾아야 한다. 타겟값과 일치하는 요소가 없는 경우에는 정렬 순서에 맞추어 타겟값을 배열 안에 넣을 때, 타겟값의 인덱스를 구하는 문제다.

단순하게 생각해본다면 배열에 반복문을 사용해 앞에서부터 값을 타겟과 비교해보면 될 것이다. 이를 코드로 구현해보면 다음과 같다.

위 방법을 사용하면 원하는 값을 찾아낼 수 있다. 이 경우, 최대 배열의 개수만큼 연산이 이루어지므로 시간복잡도는 O(N)이 된다. 시간복잡도를 줄일 수 있는 다른 방법은 없을까?

* 이진 검색(Binary Search)을 사용한 풀이법

이진 검색을 사용하면 시간복잡도를 줄일 수 있다. 먼저 이진 검색에 대해서 위키백과는 다음과 같이 인덱스 바이너리 설명하고 있다.

이진 검색 알고리즘(Binary Search Algorithm)
이진 검색 알고리즘은 오름차순으로 정렬된 리스트에서 특정한 값의 위치를 찾는 알고리즘이다. 처음 중간의 값을 임의의 값으로 선택하여, 그 값과 찾고자 하는 값의 크고 작음을 비교하는 방식을 채택하고 있다. 처음 선택한 중앙값이 만약 찾는 값보다 크면 그 값은 새로운 최댓값이 되며, 인덱스 바이너리 작으면 그 값은 새로운 최솟값이 된다.

오름차순으로 정렬된 배열의 중간값과 타겟 값을 비교해서 중간값이 크면 전체를 훑을 필요 없이 배열의 처음부터 중간값 이전까지에서만 타겟을 확인하면 된다. 반대로, 타겟값이 크면 중간값 + 1 부터 배열의 마지막까지 범위에서만 타겟값을 찾으면 된다.

이진검색을 이용해 효과적으로 솔루션을 찾아낸 케이스가 있어 이를 아래와 같이 정리해보았다.

위 경우에서는 이진 검색을 반복할수록 검색 범위가 2/N으로 줄어든다는 점을 확인할 수 있다. 즉 시간복잡도를 계산하면 O(log n)이 되어 기존 방법보다 훨씬 효율적이라는 점을 확인할 수 있다.

인덱스 바이너리

순차 탐색(sequential search) : 리스트 안에 있는 특정한 데이터를 찾기 위해 앞에서부터 데이터를 하나씩 확인하는 방법
이진 탐색(binary search) : 정렬되어 있는 리스트에서 탐색 범위를 절반씩 좁혀가며 데이터를 탐색하는 방법
이진 탐색은 시작점, 끝점, 중간점이 존재

ex)
정렬되어 있는 리스트가 있다고 가정하고 4인 원소를 찾는 예시
[0, 2, 4, 6, 8, 12, 14, 16, 18] 시작점 0(index) 끝점 9 중간점 4 으로 설정하고, 중간점과 찾고자하는 원소값이 작다면 오른쪽 범위는 볼 필요가 없다.
[0, 2, 4, 6] 이렇게 탐색범위는 총 4개 줄어드는데, 시작점은 0 중간점은 1 끝점은 3이 인덱스 바이너리 된다. 이렇게 해도 원하는 4를 못찾았는데 이번에는 중간점에 있는 2보다 4가 더 작으므로 오른쪽만 본다
[4, 6] 시작점 0 끝점 1 중간점 0 -> 4를 찾았다. 총 3번의 step으로 원하는 값을 찾을 수 있다.

시간복잡도는 탐색 범위를 2로 나누는 것과 동일하므로 연산 횟수는 log2N에 비례하므로, O(logN)을 보장

# binary search with recursive
def binary_search_resursive(arr, target, start, end):
if start > end:
return None
mid = (start+end) // 2

# 찾은 경우 중간점 인덱스 반환
if arr[mid] == target:
return mid

# 중간점의 값보다 찾고자 하는 값이 작은 경우 왼쪽 확인
elif arr[mid] > target:
return binary_search_resursive(arr, target, start, mid-1)

# 중간점의 값보다 찾고자 하는 값이 큰 경우 오른쪽 확인
else:
return binary_search_resursive(arr, target, mid+1, end)

# binary search with for
def binary_search_for(arr, target, start, end):
while start mid = (start + end) // 2

# 찾은 경우 중간점 인덱스 반환
if arr[mid] == target:
return mid

# 중간점 값보다 찾고자 하는 값이 작은 경우 왼쪽 확인
elif arr[mid] > target:
end = mid - 1

# 중간점 값보다 찾고자 하는 값이 큰 경우 오른쪽 확인
else:
start = mid + 1

return None # 못찾았다면 None 반환

# 원하는 원소 n 과 target list 가 몇개 인지 입력 받기
n, target = list(map(int, input().split()))

# 전체 원소 입력 받기
arr = list(map(int, input().split()))

result = binary_search_resursive(arr, target, 0, n-1)

"""
10 7
1 3 5 7 9 11 13 15 17 19
4
"""
if 인덱스 바이너리 result == None:
print('원소 없음')
else:
print(result+1)

# python binary search libary
from bisect import bisect_left, bisect_right

print(bisect_left(a,x)) # 정렬된 순서를 유지하면서 배열 a에 x를 삽입할 가장 왼쪽 인덱스 반환 -> 2
print(bisect_right(a,x)) # 정렬된 순서를 유지하면서 배열 a에 x를 삽입할 가장 오른쪽 인덱스 반환 -> 4

# 특정 범위에 속하는 데이터 개수 구하기

# 값이 [left_value, right_value]인 데이터의 개수 반환하는 함수
def count_by_range(a, left_value, right_value):
right_index = bisect_right(a, right_value)
left_index = bisect_left(a, left_value)
return right_index - left_index

a = [1, 2, 3, 3, 3, 3, 4, 4, 8, 인덱스 바이너리 9]

# 값이 4인 데이터 개수 출력
print(count_by_range(a, 4, 4)) # 2

# 값이 [-1, 3] 범위에 있는 데이터 개수 출력
print(count_by_range(a, -1, 3)) # 6

파라메트릭 서치(parametric search)
- 최적화 문제를 결정 문제(yes or no) 로 바꾸어 해결하는 기법
ex) 특정한 조건을 만족하는 가장 알맞은 값을 빠르게 찾는 최적화 문제
일반적으로 코딩 테스트에서는 이진 탐색을 이용하여 해결 가능

[Java] 이분 탐색(Binary Search)

공부를 목적으로 진행하는 포스팅으로 만약 틀린 부분이 있거나 미흡한 점이 있으면 피드백 부탁드리겠습니다.

이분 탐색 혹은 이진 탐색이라 불리는 이 알고리즘은 간단하면서 굉장히 효율적인 알고리즘입니다. 이 알고리즘을 수행하기 위해서는 기본적으로 정렬이 되어있어야 합니다. 정렬된 자료구조 안에서 특정 값을 찾을 때 절반씩 나누어 값을 찾는다는 것이 핵심적인 아이디어입니다.

이분 탐색은 탐색을 진행할 때마다 탐색 범위를 반으로 줄입니다. 분할 정복(Divide Conquer)알고리즘과 유사한데 이분 탐색은 분할 정복 알고리즘의 한 예입니다.

만약 탐색 범위가 더이상 나눠지지 않는 1이 될 때의 탐색 횟수를 T라고 하고, 정렬된 배열의 길이가 N인 자료구조에서 이분 탐색을 했을 경우의 시간 복잡도를 표로 보여드리겠습니다.

탐색 횟수 범위
0 N
1 N/2 => N/2^1
2 N/4 => N/2^2
3 N/8 => N/2^3
T N/? => N/2^T

빅오 표기법 기준으로 최악의 경우를 T라 가정한다면 N/2^T = 1 이므로, T = log2 ​(N)임을 보입니다.

컴퓨터는 이진수 시스템을 사용하기 때문에, 로그는 밑을 대부분 2로 사용합니다. 즉, lon2N 표기법이 logN으로 쓰입니다. 그러나 로그의 밑이 변할 때, loga(N)과 logb(N)은 오로지 상수 승수에 따라서만 달라지며 이것은 빅오 표기법에서 버림 합니다. 그러므로 O(log N)은 로그의 밑과 상관없이 로그 시간 알고리즘에 대한 표준 표기법이 됩니다.

즉, 이분 탐색 알고리즘은 log2(N) => logN 시간 복잡도를 가진다고 할 수 있습니다.

이분 탐색 유형의 문제를 풀이할 때는 대게 3가지 변수를 사용합니다. (변수명은 취향 차이입니다.)

Mid = (Strat + End) / 2

또한, 대표적으로 3가지 아이디어를 기억하시면 됩니다. (오름차순 기준)

1) 찾고자 하는 값이 배열[Mid]의 값보다 큰 경우, Start 값을 증가시킵니다.

2) 찾고자 하는 값이 배열[Mid]의 값보다 작은 경우, End 값을 감소시킵니다.

3) 찾고자 하는 값이 배열[Mid]에 위치한 경우, Mid를 인덱스 바이너리 반환합니다.

간단하게 예를 들어 설명해보겠습니다.

인덱스 바이너리
index 0 1 2 3 4
배열 값 1 3 5 7 9

이런 배열이 주어졌다고 가정하고, 찾는 값은 7이라고 하겠습니다. 하지만 배열 안에 어떤 값들이 들어 있는지 모릅니다. 그렇다면 7을 찾기 위해 배열의 처음부터 끝까지 반복문을 통해서 탐색해야 합니다. 만약 배열의 길이가 1억 이상이고 찾고자 하는 7이 배열의 마지막 인덱스에 위치해 있으면 7을 찾기 위하여 배열의 마지막까지 쓸데 없는 탐색을 해야 할 것입니다.

index 0 1 2 3 4
배열 값 1 3 5 7 9

Start와 End는 파란색 Mid는 빨간색으로 표현하겠습니다. (0+4) / 2 = 2 = Mid입니다.

Mid가 위치한 값은 5이고 찾고자 하는 값은 7이므로 Start의 값을 증가시키겠습니다. (Start = Mid + 1)

인덱스 바이너리
index 0 1 2 3 4
배열 값 1 3 5 7 9

Start = 3, End = 4 , Mid = (3 + 4) / 2 = 3 (나머지 버림)

Mid가 위치한 값은 7로, 찾고자 하는 7을 찾을 수 있었습니다. 이분 탐색 알고리즘을 사용하지 않았다면 0~3까지 탐색해야 했지만, 사용함으로써 단 2회 인덱스 바이너리 만에 원하는 값을 찾을 수 있었습니다. 주어진 예제가 길이가 작아 차이가 없어 보일 수 있지만 길이가 크다면 효과는 강력합니다.

문제 유형마다(최대, 최소를 구하는 문제 등) 코딩 방법이 달라지겠지만 큰 틀은 벗어나지 않습니다. 문제를 풀 때 주의할 점은 Mid를 잡는 기준을 잘 생각해야 합니다. 보통 배열의 Index를 잡긴 하지만 난이도가 있는 문제를 풀 때는 꼭 Index만이 Mid의 기준이 아닙니다.

몇 가지 문제를 보여드리겠습니다.

첫째 줄에 자연수 N(1 ≤ N ≤ 100,000)이 주어진다. 다음 줄에는 N개의 정수 A[1], A[2], …, A[N]이 주어진다. 다음 줄에는 M(1 ≤ M ≤ 100,000)이 주어진다. 다음 줄에는 M개의 수들이 주어지는데, 이 수들

대표적인 이분 탐색 문제입니다. 난이도도 그리 높지 않고 쉽게 연습할 수 있는 문제입니다.

사실 Java에는 이분 탐색을 사용할 수 있는 라이브러리가 존재합니다. 바로 Arrays.binarySearch()입니다.

하지만 이 라이브러리는 우리가 자주 봐왔던 구현 방식이 조금 달라서 주의가 필요합니다.

간단하게 설명하자면, 파라미터는 (배열, 찾고자 하는 값) 이렇게 받습니다.

반환 값은 찾고자 하는 값이 존재하면 그 값의 인덱스를 반환하고, 존재 하지 않으면 그 값을 끼워 넣어 1부터 시작하는 인덱스를 음수로 변경하여 반환합니다.

아래 코드는 똑같은 문제를 Arrays.binarySearch() 메소드를 사용하여 풀이했습니다.

이 메소드를 적절히 활용한다면 괜찮겠지만 직접 구현을 해봐서 나만의 것으로 만드는게 더 인덱스 바이너리 중요합니다.

n명이 입국심사를 위해 줄을 서서 기다리고 있습니다. 각 입국심사대에 있는 심사관마다 심사하는데 걸리는 시간은 다릅니다. 처음에 모든 심사대는 비어있습니다. 한 심사대에서는 동시에 한

위 문제는 Mid 기준을 Index가 아닌 다른 기준으로 설정했을 때 유형입니다.

인덱스가 아닌 주어진 시간을 Mid로 잡아 문제를 풀이하였습니다.

출발지점부터 distance만큼 떨어진 곳에 도착지점이 있습니다. 그리고 그사이에는 바위들이 놓여있습니다. 바위 중 몇 개를 제거하려고 합니다. 예를 들어, 도착지점이 25만큼 떨어져 있고, 바위가

위 문제 또한 Mid의 기준을 본인이 설정해야 합니다. 사실 이분 탐색 문제는 출제자가 이미 Mid의 기준을 생각하고 출제하는 듯합니다. 그 의도를 파악하는 것이 쉽지 않을뿐..

이분 탐색 알고리즘은 효율성 문제에서 많이 출제되지만 간단한 코드와는 다르게 난이도가 있는 문제들이 많습니다. 유형을 반복해서 풀어보고 경험을 쌓는 것이 중요해 보입니다.

[MongoDB] 강좌 6편 Index 설정

이번 강좌는 MongoDB의 Index에 관한 내용입니다.

Index란?

Index는 MongoDB에서 데이터 쿼리를 더욱 효율적으로 할 수 있게 해줍니다. 인덱스가 없이는, MongoDB는 collection scan – 컬렉션의 데이터를 하나하나 조회 – 인덱스 바이너리 방식으로 스캔을 하게 됩니다. 만약 document의 갯수가 매우 많다면, 많은 만큼 속도가 느려지겠죠? 이 부분을 향상시키기 위하여 인덱스를 사용하면 더 적은 횟수의 조회로 원하는 데이터를 찾을 수 있습니다.

Document의 필드(들) 에 index 를 걸면, 데이터의 설정한 키 값을 가지고 document들을 가르키는 포인터값으로 이뤄진 B-Tree를 만듭니다. 여기서 B-Tree는 Balanced Binary search Tree 인데요, B-Tree 에서 Binary Search를 통하여 쿼리 속도를 매우 빠르게 향상 시킬 수 있습니다. Balanced Binary Tree / Binary Search 키워드에 대해선 자료구조를 공부하신 분들이라면 익숙하겠지만 그렇지 않은 분들을 위해 한번 간단하게 원리를 설명해보겠습니다.

만약에 숫자가 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15 가 있을 때 이 중에서 14를 찾는다고 한다면, Index가 없을때는 1부터 14까지 쭉 조회를 하여 14를 찾아냅니다.

하지만 Index가 있을땐 다음과 같은 B-Tree를 만듭니다. ( B-Tree가 만들어지는 과정은 생략하겠습니다. )

Untitled-7

이 트리를 간단하게 설명해드리자면, 특정 값보다 작은 값은 왼쪽에, 큰 값은 오른쪽에 위치하는 규칙을 가지고 있는데요.

이 트리를 사용하여 14를 찾을땐 8 → 12 → 14 이렇게 3번의 조회만에 14를 찾을 수 있게됩니다.

bintree-traverse

그리고, 순회 알고리즘을 사용하여 가장 낮은값부터 가장 큰 값 까지 매우 효율적으로 정렬도 할 수 있습니다.

자, 이제 원리를 이해하였다면 MongoDB에서 인덱스를 사용하는 방법을 배워보겠습니다.

Index의 종류

기본 인덱스 _id

모든 MongoDB의 컬렉션은 기본적으로 _id 필드에 인덱스가 존재합니다. 만약에 컬렉션을 만들 때 _id 필드를 따로 지정하지 않으면 mongod드라이버가 자동으로 _id 필드 값을 ObjectId로 설정해줍니다.

_id 인덱스는 unique(유일)하고 이는 MongoDB 클라이언트가 같은 _id 를 가진 문서를 중복적으로 추가하는 것을 방지합니다.

Single(단일) 필드 인덱스

MongoDB 드라이버가 지정하는 _id 인덱스 외에도, 사용자가 지정 할 수 있는 단일 필드 인덱스가 있습니다.

다음 이미지 처럼 score 값으로 정렬 할 수 있지요.

Diagram of an index on the ``score`` field (ascending).

Compound (복합) 필드 인덱스

두개 이상의 필드를 사용하는 인덱스를 복합 인덱스라고 부릅니다. 다음 이미지와 같이 첫번째 필드 (userid)는 오름차순으로, 두번째 필드 (score)는 내림차순으로 정렬 해야 하는 상황이 있을때 사용합니다.

Diagram of a compound index on the ``userid`` field (ascending) and the ``score`` field (descending). The index sorts first by the ``userid`` field and then by the ``score`` field.

Multikey 인덱스

Diagram of a multikey index on the ``addr.zip`` field. The ``addr`` field contains an array of address documents. The address documents contain the ``zip`` field.

필드 타입이 배열인 필드에 인덱스를 적용 할 때는 Multikey 인덱스가 사용됩니다. 이 인덱스를 통하여 배열에 특정 값이 포함되어 있는 document를 효율적으로 스캔 할 수 있습니다.

Geospatial(공간적) Index

지도의 좌표와 같은 데이터를 효율적으로 쿼리하기 위해서 (예: 특정 좌표 반경 x 에 해당되는 데이터를 찾을 때) 사용되는 인덱스입니다. 자세한 사항은 매뉴얼을 참고해주세요.

Text 인덱스

텍스트 관련 데이터를 효율적으로 쿼리하기 위한 인덱스입니다. 자세한 사항은 매뉴얼 Text Indexes 을 참고하세요.

해쉬 (hashed) 인덱스

이 인덱스를 사용하면 B Tree가아닌 Hash 자료구조를 사용합니다. Hash는 검색 효율이 B Tree보다 좋지만, 정렬을 하지 않습니다.

인덱스 생성

인덱스를 생성 할 땐, 다음과 같은 createIndex() 메소드를 사용합니다. 파라미터는 인덱스를 적용할 필드를 전달합니다. 값을 1로하면 오름차순으로, -1로 하면 내림차순으로 정렬합니다.

다양한 예제를 통하여 인덱스 생성을 배워보도록 하겠습니다.

예제1 단일 필드 인덱스 생성

score 필드에 인덱스를 걸어줍니다.

이 인덱스는 다음과 같은 쿼리를 할 때 효율적으로 실행하게 해줍니다.

예제2 복합 필드 인덱스 생성

이렇게 여러 필드를 넣어 인덱스를 생성하면 age를 오름차순으로 정렬한 상태에서 score 는 내림차순으로 정렬합니다.

인덱스 속성

인덱스에 속성을 추가 할 땐 createIndex() 메소드의 두번째 인자에 속성값을 document 타입으로 전달해주면 됩니다.

인덱스에 적용 할 수 있는 4가지 속성이 있는데요, 한번 이에 대해 알아보고, 예제를 통해 사용법을 배워보도록 하겠습니다.

Unique (유일함) 속성

_id 필드처럼 컬렉션에 단 한개의 값만 존재 할 수 있는 속성입니다.

예제3 email 인덱스에 unique 속성 적용

unique 속성은 다음처럼 복합 인덱스에도 적용 할 수 있습니다.

예제4 firstName 과 lastName 복합인덱스에 unique 속성 적용

Partial (부분적) 속성

partial 속성은 document의 조건을 정하여 일부 document에만 인덱스를 적용 할 때 사용됩니다.

partial 속성을 사용하면, 필요한 부분에만 인덱싱을 사용하여 저장공간도 아끼고 속도를 더 높일수 있습니다.

예제5 visitors 값이 1000 보다 높은 document에만 name 필드에 인덱스 적용

예제6 TLL 속성

이 인덱스 속성은 Date 타입, 혹은 Date 배열 타입의 필드에 적용 할 수 있는 속성입니다. 이 속성을 사용하여 document를 expire(만료) 시킬 수 있습니다. 즉, 추가하고 특정 시간이 지나면, document 를 컬렉션에서 제거합니다.

예제: notifiedDate 가 현재 시각과 1시간 이상 차이나면 제거

document가 만료되어 제거 될 때, 시간이 아주 정확하지는 않습니다. 만료되는 document를 제거하는 thread는 매 60초마다 실행됩니다. 이점 유의해주세요.

인덱스 바이너리

Full binary tree

단말 노드가 아닌 모든 노드가 2 개의 자식을 가진 트리

Perfect binary tree

모든 단말 노드의 깊이가 같은 full binary tree

Complete binary tree

Perfect binary tree 에서 끝 부분을 제외하고 다른 것이 남아 있는 트리이다 . Perfect binary 인덱스 바이너리 tree 의 각 노드에 부모에서 자식으로 , 외쪽에서 오른쪽으로 번호를 매겼을 때 perfect binary tree 는 아니지만 그 번호가 연속되어 있는 경우이다 .

Balanced binary tree

모든 단말 노드의 깊이 차이가 많아야 1 인 tree 이다 . Balanced binary tree 는 예측 가능한 깊이 (predictable depth) 를 가진다 . 노드 개수를 n 이라고 하면 길이는 log 2 n 이 된다 .

Order of tree traversal

In-order: 왼쪽 자식 -> 나 -> 오른쪽 자식

Pre-order: 나 -> 왼쪽 자식 -> 오른쪽 자식

Post-order: 왼쪽 자식 -> 오른쪽 자식 -> 나

Level-order: Root -> depth 1 인 노드들 -> depth 2 인 노드들 인덱스 바이너리 -> .

Binary tree 는 배열 (array) 로 표현할 경우 따로 포인터를 가지고 있지 않아도 부모 , 자식에 대한 indexing 이 가능하다는 장점이 있다 .

Root 를 0 부터 시작하느냐 1 부터 시작하느냐에 따라 다른데 ,

( 위 그림에서 처럼 ) root 가 0 부터 시작하면 , parent index = (my index – 1) / 2, left child index = my index * 2 + 1, right child index = my index * 2 + 2 가 된다 .


0 개 댓글

답장을 남겨주세요