상수 시간(리스트의 길이와 무관 ) : O(1)
선형 시간(리스트의 길이에 비례) : O(n)
데이터 원소들을 순서를 지어 늘어놓는다. 번호가 붙여진 칸에 원소들을 채워넣는 방식이다.
저장 공간 : 연속한 위치
특정 원소 지칭 : 매우 간편 O(1)
이진 탐색이 선형 탐색보다 빠른 방법이나, 항상 이진탐색을 사용하지는 않는다. 그러려면 우선 배열을 정렬해야 하는데, 한 번만 탐색하고 말 거라면, 굳이 크기 순으로 늘어놓느라 시간을 소모하는 것보다 한번씩 다 뒤지는 것이 나을 때도 있다.
하나의 함수에서 자신을 다시 호출하여 작업을 수행하는 것으로 종결 조건이 매우 중요하다.
재귀적인 방법으로 많은 문제를 풀 수 있다. ex) 이진트리, 자연수의 합 구하기, 조합의 수, 하노이의 탑, 피보나치 순열
재귀함수(recursive version) VS 반복문(Iterative version)
둘 다 O(n)이지만 재귀함수는 함수호출때문에 효율성은 떨어진다.
재귀적 이진탐색
입출력 예
L = [2, 3, 5, 6, 9, 11, 15]
x = 6
l = 0
u = 6
L[3] == 6 이므로 3 을 리턴해야 합니다.
또 다른 예로,
L = [2, 5, 7, 9, 11]
x = 4
l = 0
u = 4
리스트 L 내에 4 의 원소가 존재하지 않으므로 -1 을 리턴해야 합니다.
if x not in L로 했다가 효율성 검사에서 다 실패했다.
이 코드는 L을 다 돌아야해서 절반씩 삭제해 탐색하는 이진탐색의 장점을 없앤다. x가 L에 없는 경우는 low가 up보다 커지는 경우이므로 l > u
얼마나 오래걸리느냐가 아닌 문제를 푸는데 얼마만큼의 자원을 요구하냐이다.
점근 표기법(asymptotic notation)의 하나로 어떤 함수의 증가 양상을 다른 함수와의 비교로 표현(알고리즘의 복잡도를 표현할 때 흔히 쓰임)
입력의 크기가 n일 때,
O(logn) - 입력의 크기의 로그에 비례하는 시간 소요
O(n) - 입력의 크기에 비례하는 시간 소요
계수는 그다지 중요하지 않음
✨보다 나은(낮은) 복잡도를 가지는 정렬 알고리즘
병합 정렬(merge sort) - O(nlogn)
정렬할 데이터를 반씩 나누어 각각을 정렬시킨다.
내부구현은 숨겨두고 밖에서 보이는 것들만 제공하는 것
Data
ex) 정수, 문자열, 레코드, ...
A set of operations (일련의 연산들)
ex) 삽입, 삭제, 순회, ...
ex) 정렬, 탐색, ...
각 원소들을 줄줄이 엮어서 관리하는 방식이다.
원소들이 링크 (link) 라고 부르는 고리로 연결되어 있어, 가운데에서 끊어 하나를 삭제하거나, 가운데를 끊고 그 자리에 다른 원소를 (원소들을) 삽입하는 것이 선형 배열보다 빠르다.
원소의 삽입/삭제가 빈번히 일어나는 응용에서는 연결 리스트가 많이 이용된다.
단점
1. 구조 표현에 소요되는 저장 공간 (메모리) 소요가 크다.
2. k 번째의 원소 를 찾아가는 데 시간이 오래 걸린다.
선형 배열에서는 데이터 원소들이 번호가 붙여진 칸들에 들어 있으므로 그 번호를 이용하면 되지만, 연결 리스트에서는 원소들이 고리로 연결되어 있으므로 특정 번째의 원소를 접근하려면 앞에서부터 하나씩 링크를 따라가야한다.
저장공간 : 임의의 위치
특정 원소 지칭 : 매우 간편 O(n)
def insertAt(self, pos, newNode):
pos는 newNode가 들어갈 위치로 (1 <= pos <= nodeCount +1)이다.
def insertAt(self, pos, newNode):
prev = self.getAt(pos-1)
newNode.next = prev.next
prev.next = newNode
self.nodeCount +=1
1,2과정의 순서를 바꿀 수 는 없다. prev.next의 값을 newNode로 먼저 바꿔버리면 원래 지정되어있던 값이 사라져서 newNode의 next값을 지정할 수 없기 때문이다.삽입하려는 위치가 맨 앞일 때
prev없음, head 조정 필요
삽입하려는 위치가 맨 끝일 때(맨 앞에서 굳이 pos로 다 찾아갈 필요 없다. prev를 tail로 잡고 삽입하면 된다.)
Tail 조정 필요
빈 리스트에 삽입할 때
위 두 조건을 잘 처리하면 된다.
💡 연결 리스트 원소 삽입의 복잡도
맨 앞에 삽입하는 경우 : O(1)
중간에 삽입하는 경우 : O(n)
맨 끝에 삽입하는 경우 : O(1)
def popAt(self, pos):
pos가 가리키는 위치의 (1 <= pos <= nodeCount)
prev.next를 curr.next로 조정한다.(prev는 pos-1 curr는 pos)
curr(삭제값)의 data를 꺼내 리턴
nodeCount -= 1
삭제하려는 node가 맨 앞의 것일 때
prev없음, head 조정 필요
삭제하려는 node가 맨 끝의 것일 때(tail을 가지고 tail 앞의 노드(prev)를 찾아낼 수 없어 처음부터 찾아와야함)
Tail 조정 필요
유일한 노드를 삭제할 때
-> 위 두 조건에 의해 처리되는가?!
💡 연결 리스트 원소 삭제의 복잡도
맨 앞에서 삭제하는 경우 : O(1)
중간에서 삭제하는 경우 : O(n)
맨 끝에서 삭제하는 경우 : O(n)
def concat(self, L):
연결 리스트 self의 뒤에 또다른 연결 리스트 L을 이어 붙임
어떤 노드를 주고 그 뒤에 삽입,삭제 하는 방식의 메서드 생성
insertAfter(prev, newNode)
popAfter(prev)
-> 맨 앞에 삽입, 삭제할 경우를 위해 맨 앞에 dummy node를 추가한 형태로 연결 리스트를 조금 변형 시킨다. 연결리스트가 0부터 시작한다.
def insertAfter(self, prev, newNode):
insertAt의 구현 : 이미 구현한 insertAfter()을 호출해 이용한다.
def insertAt(self, pos, newNode):
def popAfter(self, prev):
prev 의 다음 node를 삭제하고 그 node의 data를 리턴
💡 주의사항
첫번째 리스트의 tail을 두번째 리스트의 더미노드 뒷 노드에 이어줘야한다.