알고리즘 공부할 때, 빈번하게 나오는 유형이 아니다보니 매번 공부를 미뤘다.
이젠, 시간에 쫓기지 않아도 되니 조금씩 다뤄볼 예정이다 !
이분탐색
이분 탐색이란 말그대로 정렬된 리스트에서 검색 범위를 줄여나가며 특정 값을 찾는 알고리즘이다.
왜 사용 ?
검색이 반복될 때마다, 범위를 절반씩 줄여나갈 수 있기 때문에 시간복잡도 O(logN)이다. 범위 절반씩 줄기 때문에, 당연하다.
투 포인터(Two Pointer)와 무슨 차이 ?
조금 깊게 생각해보면 차이를 확연히 느낄 수 있다.
이분 탐색 | 투 포인터 | |
시간복잡도 | O(logN) | O(N) |
조건 | 정렬된 리스트 | 상관없음 |
방식 | mid를 활용해서 범위를 절반씩 줄여나감 | start, end 사용해서 한 칸씩 이동 |
정렬되지 않은 리스트에서 특정한 값을 찾아야한다면, 투 포인터가 유리할수도 있기 때문에, 알맞은 상황에 쓰면 된다.
이분탐색 코드 구현
def binary_search(target, data): #찾으려는 값, 리스트
data.sort()
start = 0
end = len(data) - 1
while start <= end:
mid = (start + end) // 2
if data[mid] == target:
return mid
elif data[mid] < target:
start = mid + 1
else:
end = mid -1
return None
다음과 같은 로직으로 짜진다.
start와 end를 통해 mid를 구한 후,
우리가 찾고자 하는 값(tar)이 위치(중앙)의 값과 같으면, mid를 반환하고
그렇지 않다면, 투 포인터처럼 start와 end를 움직여준다 !
예제 풀어보기
https://www.acmicpc.net/problem/10816
10816번: 숫자 카드 2
첫째 줄에 상근이가 가지고 있는 숫자 카드의 개수 N(1 ≤ N ≤ 500,000)이 주어진다. 둘째 줄에는 숫자 카드에 적혀있는 정수가 주어진다. 숫자 카드에 적혀있는 수는 -10,000,000보다 크거나 같고, 10,
www.acmicpc.net
# 이분탐색 x => 시간초과 코드
n = int(input())
lst = list(map(int,input().split()))
m = int(input())
tar = list(map(int,input().split()))
ans = []
for i in tar:
if i in lst:
ans.append(lst.count(i))
else :
ans.append(0)
print(ans)
당연한 결과지만, O(N^2)이므로 시간초과가 났다. (반복문에서 n, count에서 n)
딕셔너리를 이용하면 해결된다..! but 나의 목적은 그게 아니기 때문에,,^^
이분탐색을 적용하려고 했으나, 어떻게 tar의 개수를 O(NlogN)으로 세는지 감이 잡히지 않았다.
이분탐색을 적용했으나, O(N^2)로 코드가 짜졌다 ㅋㅋㅋ ㅠㅠ
그래서 그냥 다른 분들의 코드를 보면서 익히려고 한다.
## 다른 분 풀이 참고
def binary(n, N, start, end):
if start > end:
return 0
m = (start+end)//2
if n == N[m]:
return N[start:end+1].count(n)
elif n < N[m]:
return binary(n, N, start, m-1)
else:
return binary(n, N, m+1, end)
target을 찾으면 start부터 end까지에서 count를 적용해준다..
나는 바보같이 이분 탐색을 적용하고 쓰질 못했다
로직을 파악하고, 코드를 다시 짜봤다
"""
실버 4 :
첫 시도 : 시간 초과 (이분 탐색을 적절히 사용 x)
결국, 다른 사람의 풀이를 참고하였음
"""
## 정답 전체 코드
import sys
input = sys.stdin.readline
def bs(lst, tar, start, end):
#start, end가 들어오면, lst에서 tar를 찾자
mid = (start+end)//2
if start > end: # 다 돌았는데, tar이 없는 것
return 0
if tar == lst[mid] : #찾으면
return lst[start:end+1].count(tar)
elif tar > lst[mid]:
return bs(lst, tar, mid+1, end)
else :
return bs(lst, tar, start, mid-1)
n = int(input())
lst = list(map(int, input().split()))
lst.sort()
m = int(input())
target = list(map(int, input().split()))
dic = {}
start,end = [0, len(lst)-1]
for i in lst:
if i not in dic:
dic[i] = bs(lst, i, start, end)
print(dic)
print(' '.join(str(dic[x]) if x in dic else '0' for x in target ))
내가 틀렸던 이유는 아래와 같다.
1. count를 모든 범위에 사용하여 O(N)만큼 소요
2. 딕셔너리를 사용하지 않음
## 시간 초과
for i in target:
if i not in lst: ## 이것도 사실 IN 보단, bs에 넣는게 시간은 더 효율적일듯
ans.append(0)
else:
ans.append(bs(lst, i, start, end))
처음엔 ans 스택을 만들어주고, tar 리스트를 반복문을 돌려 하나의 값씩 추가해주었다. 이분 탐색을 이용했음에도 불구하고 이것 또한 시간 초과가 일어났다.
그 이유는 많은 양의 값들을 리스트에 append하기 때문에 시간초과가 난 것 같다.
(솔직히 이해 안 됨. 스택에 append도 O(1)이고, 딕셔너리에 값을 넣는 것도 O(1) 아님??
어짜피 target 리스트를 반복 돌려서 스택에 추가하면 , 중복된 값도 없고
lst에서 딕셔너리에 있는지 확인 후 추가하면, 마찬가지로 중복된 값 없음
그럼 비슷해야하는거 아닌가..
)
==> 해결했다 !!!!!!!
정말 간단한 문제였다.
리스트의 in은 O(N)이고, 딕셔너리의 in은 해쉬를 사용하기 때문에, O(1)이었음
딕셔너리를 이용해 append하지 말고, 넣어주자 !
'PS' 카테고리의 다른 글
[소프티어] 징검다리 -level3 (1) | 2023.12.30 |
---|---|
백준 양 파이썬 / sol (0) | 2023.12.19 |
[프로그래머스] 숫자 변환하기 파이썬 (0) | 2023.08.26 |
배낭 문제(Knapsack Problem) (0) | 2023.04.09 |