-
priority queue 는 우선순위 큐로, 큐인데 우선순위가 있는 아이템들을 저장하는 큐임.
-
주의할 점! 일반적인 큐의 경우 먼저 들어간 것이 먼저 나오는 선입선출이었다면, 우선 순위 큐의 경우 우선 순위가 높은 아이템이 먼저 나오게 됨.
-
priority queue 는 가장 흔한 자료구조임. 스택이나 선입선출 큐도 priority queue 로 구현이 가능함. 왜냐면, 우선순위를 어떻게 주느냐에 따라서 꺼내는 것이 맨 처음에 들어온 것일 수도 잇고 ,맨 마지막에 들어온 것으로 만들 수도 있으니까. 즉 priority queue 가 좀 더 일반적인 개념이라고 생각하면 됨.
-
주로 시뮬레이션 돌릴 때 많이 쓰임. 이벤트가 발생한 시간을 우선순위로 만들면, 시간 순에 따라 이벤트를 동작하게끔 만들 수 잇어서. 또 운영체제가 일이 많을 때 그 일들을 스케줄하는 것은 이 우선순위 큐에 따라서 스케줄 하게 됨. 사실 당연한 말.. 네트워크에 트래픽이 많이 몰려있을 때 중요한, 우선순위가 높은 것부터 내보내야 함.
-
delete(q)는 가장 우선 순위가 높은 아이템을 지우고, 중요한 것은 그 아이템의 값을 리턴하게 됨!! (리턴값이 있다는 것이 중요함)
-
우선순위 큐는 크게 두 가지 종류로 나눌 수 있음. 하나는 minimum priority queue 다른 하나는 maximum priority queue. minimum 은 말 그대로 우선순위가 가장 작은 것부터 뽑아내겠다는 거고 maximum 은 우선순위가 가장 큰 것부터 뽑아낸다는 것.
-
우선순위 큐는 크게 세 가지로 구현이 가능함. 하나는 배열로 구현, 다른 하나는 연결 리스트, 마지막으로는 힙으로 구현 가능한데, 여기서 힙이 가장 효율적임. 왜냐면 Insertion operation 과 deletion Operation 모두 시간 복잡도가 O(logn) 이기 때문임.
-
그럼 heap 이라는 자료구조는 무엇이냐? 우선 heap = complete binary tree 임. 즉 터미널 노드 전까지는 모든 자식 노드가 두 개여야 하고, 터미널 노드는 왼쪽부터 꽉 채워져 있어야 함.
-
그리고 나서 heap 은 Max heap 과 min heap 으로 나뉨. max 힙은 부모 노드의 키값이 자식 노드의 키값보다 크거나 같은 경우의 complete binary tree 를 의미하고, min 힙은 부모 노드의 키값이 자식 노드의 키값보다 작거나 같은 complete binary tree 를 의미함. 이때 binary search tree 와 힙은 완전 다름에 주의해야 함. 이진 탐색 트리는 좌우를 기준으로 크기가 나뉘었음. heap 은 부모 자식 관계!
-
힙은 complete binary tree 이기 때문에 힙의 높이는 O(log2n)임. 터미널 노드를 제외한 모든 노드에서 노드의 개수는, 힙의 레벨을 i 라고 할 때, 2^i-1 이라는 것도 알아두자. (레벨은 높이랑은 또 다른 얘기인 듯..? 그래서 뿌리 노드는 레벨 1 부터 시작하고 높이는 0 인 것 같아..? )
-
이때, heap 의 경우 항상! complete 트리라고 했다. 그말인 즉슨, heap 을 구현할 때 배열을 사용하면 아주 편리하다. complete 이거나 full 이라는 보장만 있다면 배열로 구현하는 것이 매우 편리하다.
-
먼저 각각의 노드에 배열의 인덱스를 붙인다. 뿌리노드가 1이고 왼쪽 자식에서 오른쪽 자식 순으로 레벨을 하나씩 키워가면 인덱싱을 해 준다.
-
그렇게 되면, 부모 노드 인덱스 = 자식 노드 인덱스 / 2 가 되고, 왼쪽 자식 노드 인덱스는 부모 노드 인덱스 * 2, 오른쪽 자식 노드 인덱스는 부모 노드 인덱스 * 2 + 1 이 된다.
-
Max heap 에서의 Insertion 절차.
- 새로운 노드는 힙에 있는 맨 마지막 노드의 바로 오른쪽에 삽입이 된다.
- 그럼 그 새로운 노드는 자기가 있는 곳에서부터 필요하다면 뿌리 노드까지 계속해서 비교와 자리바꿈을 반복한다.
- 그렇게 되면 어차피 최대 시간 복잡도 해 봐야, 트리의 높이이기 때문에 시간복잡도는 O(log2n) 이 나온다.
- 만약 부모의 키값이 자식의 키값보다 크거나 같다면 해당 작업을 중단한다.
-
이걸 쉐도 코드로 구현을 해 보면, 우선 인자로 넘겨줘야 하는 것은 heap 이랑 새로 추가할 노드의 키값.
- heap 에 노드 하나를 추가하므로, 힙 사이즈 1 늘려주고
- 1 늘어난 힙 사이즈의 인덱스를 i 에 넣어준다 .
- A[i] 즉 새로운 배열 속 노드에 원하던 키값을 대입해서 자리를 만들어 준다.
- 거슬러 올라가는 작업을 하면 되는데, i 가 1 일때는 뿌리 노드이기 때문에 부모노드가 없어서 하면 안되고, 부모 노드가 크거나 같으면 조건을 만족하는 것이니 부모노드가 자식 노드보다 더 작을 때까지만 반복한다. 그리고 A[i] 와 A[parent(i)] 의 위치를 서로 바꿔준 다음에 (근데 사실 여기서, 부모 노드를 자식 노드에 다운받는 것만 하면, 어차피 위로 올라가게 되어 있으니 굳이 자식 노드를 부모 노드에 올려줄 필요가 없음) i 에 2를 나눈 몫을 저장하면 됨. (부모 인덱스) 그리고 그렇게 와일문을 다 하고 빠져 나오면, 해당하는 인덱스를 가진 배열 요소에 넣고 싶던 키값을 넣어주면 끝!
-
Deletion in Max heap
- 가장 큰 키 값을 가진 노드를 삭제하는 것.
- 절차
- 뿌리 노드를 먼저 제거한다.
- 맨 마지막 노드를 뿌리 노드 자리에 옮겨 놓고
- 부모 노드 ≥ 자식 노드 조건을 만족할 때까지 부모 자식 관계를 바꿔준다. (필요하다면 terminal 노드까지 반복 )
- 삭제도 마찬가지로 시간복잡도 O(log2(n))
- 쉐도 코드
- 삭제하고 싶은 키값은 A[1] 이니까, 넣어주고 (여기서 주의할 점은, 힙을 만드는 배열에서 뿌리노드의 인덱스값은 0이 아니라 1이라는 것! ) A[1] 에는 heap의 사이즈를 인덱스로 가진는 A[heapsize] 를 넣어준다. 즉 자리를 옮겨주고, heap size 는 1을 뺀 값을 저장하면 된다. 이때 주의할 점은 인덱스가 1 부터 시작하기 때문에 맨 마지막 노드의 인덱스가 heapsize - 1 이 아니라 heapsize 그 자체라는 것!
- i 가 1 일 때는 뿌리 노드인데, 이 경우는 어떻게..?

쨋든 i 가 2부터 라고 하면, i 즉 인덱스가 heap size 보다 작거나 같을 때까지만 탐색을 하는데, 만약 i 가 heap size 보다 그냥 작고, (맨 마지막 노드가 아니고) 오른쪽 자식이 왼쪽 자식보다 더 크다면 오른쪽 자식의 인덱스를 largest 에 저장하고, 왼쪽 자식의 값이 더 크다면 왼쪽 자식의 인덱스를 largest 에 저장한다. 만약 i 가 heapsize 그 자체인 경우에도, largest 에 i 값 그 자체를 저장하게 된다. (비교 대상이 없으므로) ? 부모가 더 크거나 같으면 아닌지..?ㅇㅇ 맞음) 만약 부모가 자식 중 더 큰 값 보다 더 크다면, 반복문을 빠져 나오고, 그렇지 않다면, largest 를 인덱스로 갖는 놈과 부모의 자리를 바꾼 다음에, i 의 값을 largest 의 자식의 인덱스로 바꿔준다. 즉 곱하기 2 를(맞는지..? ㅇㅇ 맞음) 해준다.
-
참고로 max heap 만들기 전에 항상 Init 을 해주자. heapsize 를 0으로 정해주면 끝이다.
-
이렇게 햇을 때, max heap 에 삽입하는데 걸리는 시간 복잡도는 O(nlog2n)이다. 왜냐면, ? 이유를 모르겠.. 근데 이를 더 줄여서 log(n)으로 만드는 방법이 있다.
-
Building Max heap (교과서엔 없지만 중요함! 이걸 이용하면, O(nlogn)을 O(n)으로 줄일 수 있어서
- 배열이 주어지면, 먼저 그냥 모든 엘리멘트들을 힙에 넣어준다.
- 그렇게 되면 터미널 노드에 있는 모든 노드들(n/2 내림한 거 + 1 인덱스부터 n 인덱스까지)이 heap 속성을 만족하게 된다. 자식 노드가 아예 없기 때문에. 따라서 터미널 노드 하나 위의 노드들부터 조건을 만족시키면 된다.
- 따라서 맨 아래 노드서부터 차례대로 노드들이 속성을 만족하게끔 이동시키면 된다. 아래에서부터 차곡차곡 조건을 만족시키면서 올라가기 때문에 아래에서부터 위로 올라가는 실행 순서가 i의 자식 노드들이 모두 힙 조건을 만족한다는 것을 보장한다. 따라서 아래에서 위로 올라가는 순서가 매우 중요함.
- 시간 복잡도
- 하나의 서브트리에서 엘리멘트를 움직이는 동작은 총 O(h)번 일어난다. h는 서브트리의 높이! 따라서 O(log2m) (m은 서브트리에 있는 노드의 총 개수) 따라서 우리는 각 노드의 서브트리들에서 생긴 이동을 모두 합해주면 된다 .
- 그럼 시그마 h 가 0부터 log2n의 내림 까지 (트리의 높이) 더해주는데, 해당 높이에 존재하는 노드의 개수 곱하기 그 노드가 있는 곳의 트리의 높이의 서브트리에서 일어나는 이동의 수 를 더해주면 된다. 그렇게 되면 결과가 O(n)이 된다.
- 힙의 적용 예시로, heap sort 가 있다.
- 힙을 이용한 분류 알고리즘이다.
- 먼저 맥스 힙에 n개의 요소들을 정렬해서 넣어준다. (O(nlog2n)) ?왜..?
- 다음으로 맥스 힙에 있는 루트 노드를 제거한 다음에 그 값을 배열에 집어 넣는 것을 n 번 반복한다. O(nlog2n) ?왜..?
- 따라서 1번과 2번을 합친 시간 복잡도는 O(nlog2n 이다.) 근데 이때 만약 1번에서 맥스 힙을 만들 때, build max heap 을 사용하면, 시간 복잡도는 O(n) + O(nlog2n) 이 된다. 여전히 시간 복잡도는 O(nlog2n)이지만 훨씬 빠르다.
- 힙 분류 알고리즘이 유용할 때: 전체의 분류된 데이터가 필요한 게 아니라, 몇 개의 분류된 데이터가 필요할 때 유용하다. (가장 큰 데이터 몇 개)
- 코드로 구현을 하면,
- 먼저 init 해주고
- i=0부터 n까지, (? 왜 1부터가 아닌지.>?) 맥스힙에 요소를 추가해서 맥스힙을 만들어주고,
- i 가 n-1부터 0까지 delete 함수 호출해서 그 결과를 배열에 저장해 주면 된다.
- 힙의 적용 예시 중 다른 것으로 이산 이벤트 시뮬레이션이 있다. 이때 이산 이벤트 시뮬레이션의 경우, 일의 발생에 의해서 수행이 된다. 즉 이벤트의 발생을 기준으로 시뮬레이션을 돌리게 된다. 우리가 그 전에 했던 은행 시뮬레이션의 경우에는 이산 시간 시뮬레이션이다. (시간의 흐름에 따라 이벤트를 발생시킴으로써 수행된다.) 이산 시간 시뮬레이션과 달리 이산 이벤트 시뮬레이션은 시간을 일부러 1씩 증가시키거나 하지 않는다.
- 모든 시간의 진행은 이벤트 발생 시에만 일어난다. 이벤트들은 우선순위 큐에 저장이 되어 있고, 이벤트 시간에 기반해서 진행이 된다. 예를 들어 아이스크림 가게 시뮬레이션이 있다.
- 손님이 아이스크림 가게를 방문했다. 만약 앉을 수 있는 자리가 없다면, 그들은 그냥 떠나버릴 것이다. 우리의 목표는 얼마나 많은 의자가 있어야지 이익을 최대화할 수 있을까를 계산하는 것이다.
- element 구조체의 필드는 다음의 네 가지이다.
- 손님 그룹의 id (팀마다 다름)(팀을 식별하기 위함)
- 이벤트의 타입
- 도착 : 손님이 아이스크림 가게에 도착을 했다!
- 만약 이 이벤트ㅔ서 손님의 수가 남아있는 의자의 수보다 작다면,(?같은 경우도 손님 받아야 하는 것 x..?) 손님을 받는다. 그리고 남아있는 의자의 수에서 손님의 수를 빼준다. 만약 남아있는 의자의 수가 손님의 수와 같거나 작다면, 손님은 주문하지 않고 바로 떠남 이벤트를 실행한다.
- 주문 : 손님이 아이스크림을 주문했다!
- 손님의 수만ㅋ늠 주문을 받는다. 그리고 잠시 있다가 떠남 이벤트를 실행한다.
- 떠남 : 손님이 아이스크림 가게를 떠났다.
- 남아있는 의자의 수를 떠난 그룹의 손님 수만큼 증가시킨다.
- 이벤트가 언제 일어났는지를 저장하는 “키” (얘를 기준으로 우선순위에서 꺼냄. 즉 얘가 키 노드)
- 그 그룹에 있는 손님의 수
- 이때 이벤트 타입에 있는 “랜덤” 변수는 다음과 같다.
- 도착
- 게스트들의 도착 시간(키값)
- 하나의 그룹에 속한 손님의 수
- 주문
- 도착한 뒤에 주문하기까지 걸리는 시간(대기시간 등 포함)
- 손님들이 주문할 아이스크림의 개수
- 떠남
- 떠나기 전에 사람들이 샵에 머무는 시간(아이스크림 먹고 수다 떠는..)
- 주의할 것~: 시간이 짧은 것이 먼저 진행되어야 하기 때문에, (먼저 온 손님이 먼저 가야하니) min 힙이 사용된다라는 것!
- 예시
- id 0 번인 5명으로 구성된 0번 팀이 “도착” 이벤트를 가지고, 도착 시간은 1을 가지고 힙에 들어옴. 그 다음으로 2명으로 구성된 id 1번 팀이 “도착” 이벤트를 가지고 도착 시간은 3을 가지고 힙에 들어옴. 처음에 이용 가능한 의자는 총 10개임.
- 키값에 의해서, 키값이 더 작은 것이 먼저 힙을 빠져 나오게 됨. 즉 0번 팀이 먼저 와서 키값이 1이니 얘를 먼저 뺌. 그럼 남아있는 자리수와 구성원의 수를 비교함. 0번 팀은 총 5명인데 남은 의자는 10개 즉 수용할 수 있음. 그럼 이벤트 타입을 도착에서 “주문”으로 바꿔주고 order 하는데 걸리는 시간 (인당 1 총 5명이니 5) 을 기존의 키값에 더해줌. 그리고 나서 남은 의자의 개수가 줄었으니, 10개에서 손님의 수만큼인 5를 빼줘야 함.
- 그렇게 키값이 6이 되고 타입이 “주문” 이 된 채로 다시 힙에 들어감. 그럼 이번에는 1번 팀의 키값이 3이니 더 작음 그래서 1번 팀을 빼내옴. 마찬가지로 남아 있는 의자수랑 손님 수 비교, 손님 수가 더 작으니 수용해서 타입을 “주문” 으로 바꾸고, 키값을 (한 사람당 걸리는 주문 시간이 1이니까, 두 명이니까 +2)(3+2 =) 5로 바꾼 다음에 남은 의자 수가 5개에서 2명 자리 줄어든 3개 가 됨. 그리고 얘도 다시 힙에 넣음
- 그렇게 되면 키값이 또 1번 팀이 더 작음. 그러니까 또 빼옴. 그렇게 되면, 이번에는 남은 의자수랑 비교할 필요 없이 바로 타입을 “떠남” 으로 바꾸고, 떠날 때까지 걸리는 시간(랜덤)을 기존의 키값에 더해줌 (5 + 랜덤수 (ex.7)) = 12 가 됨. 그리고 나서 다시 힙에 넣음!! 에 주의! 떠남으로 바뀌는 순간 가는 게 아니라, 떠남으로 바뀌고 대기하다가 떠남.
- 또 키값 비교. 이번에는 0번팀이 더 작음. 그래서 0번 팀을 추출함. 이번에는 남은 의자 수 비교하지 말고 바로 타입 “떠남” 으로 만들어 버리고, key 값에 랜덤한 수를 더함. (6 + 8) = 14. 이렇게 타입이랑 키값만 바꾸고 다시 바로 힙으로 들어가야 함. 의자수 늘리면 안됨. 떠남 이벤트가 끝난 다음에 의자수 늘려야 함!
- 그럼 이제 다시 1번 팀의 키값이 더 작음. 따라서 1번 팀 추출. 이제는 떠나야 할 시간이 왓음. 따라서 남은 의자수에 1번 팀의 팀원 수를 더해줌.
- 힙에 남은 게 0번 팀 하나니까 추출. 마찬가지로 이제 떠나야 할 시간. 남은 의자 수에 5명을 더해줌.
- 코드
- element 구조체와, HeapType 구조체를 선언해 줌.
- 힙타입 구조체에는, element 요소를 저장하는 배열 heap[] 이 선언되어 있고, int 형 변수 heap_size 가 정의되어 있음. 즉 HeapType 을 들고 다니면, 힙 배열과 사이즈를 알 수 있음.
- 변수들
-
Huffman code