스택 : 후입 - 선출
큐 : 선입 - 선출
우선순위 큐 : 우선순위가 높은 노드 선출
프로그래머가 데이터를 근거로 각 자료의 우선순위를 결정하여 출력을 이행할 수 있는 자료구조
배열기반 구현의 장/단점
구현 방법이 간단하나 한칸 씩 미는 작업이 반복되는 번거로움 과 새로운 데이터의 삽입위치를 찾기 위해 모든 데이터와 우선순위 비교가 이루어져야 한다.
(연결리스트기반 구현의 단점 역시 배열기반 구현의 단점과 유사하며 노드 의 수가 많아질 수 록 비례하여 성능이 저하된다.)
삽입
삽입되는 데이터는 최초에 우선순위가 가장 낮다고 가정하여 리프노드레벨 가장 우측에 추가되며 이후 부모노드와의 키값 대소비교를 통해 "최소힙 또는 최대힙의 조건을 만족할 때 까지" 위치를 조정하게 된다.
(*리프노드레벨 : 최하위 노드 레벨)
삭제
최우선순위 데이터(루트노드 데이터)를 삭제하고 말단 노드에 위치한 가장 작은 우선순위의 데이터로 그 자리를 대체한 후 이번엔 top → down 방식으로 루트노드 레벨 부터 차례로 자식노드와 키의 대소를 비교해 가며, 마찬가지 "힙의 조건을 만족할 때 까지" 위치를 조정하게 된다.
(*루트노드 : 최상위 노드)
이미 정렬되어있는 트리임을 가정함
구조체 정의(structure)
// structure definition
#define MAX_ELEMENT 100
// element : 데이터 혹은 자료의 우선순위를 의미(key)
typedef struct{
int key;
}element;
// heaptree : 힙 트리를 의미(완전 이진 트리)
typedef struct{
int heap_size;
element heap[MAX_ELEMENT];
}heaptree;
구조체 : 데이터를 담은 100개의 노드 트리가 정의되었다.
삽입(insert)
// 삽입 메서드 정의
void insert(heaptree *h, element newdata){
int i = ++(h->heap_size);
// 삽입 시 heap_size는 +1
// i == 2일 때 level2 와 level1 노드의 비교가 이뤄진다. 따라서 i !== 1의 조건이 붙는다
// 새데이터의 키값과 부모노드의 키값을 비교한다. i가 낮아질수록. 즉, 반복문이 거듭될수록 레벨을 낮아진다.
while(i != 1 && newdata.key > h->heap[i/2].key){
// 키값이 자식(new) > 부모 일 경우 부모노드의 키값이 한레벨 아래(자식노드의 본래 레벨)로 재조정된다.
h->heap[i] = h->heap[i/2];
// 레벨이 낮아진다.
i /= (double)2;
}
// 키값이 부모 > 자식(new)일 경우 반복문은 멈추게 되며 최종 비교 시의 i레벨에 새로운 데이터가 정착한다.
h->heap[i] = newdata;
}
삭제(delete)
import { element, heap } from 구조체정의 파일
// 삭제 메서드 정의
element delete_root_node(heaptree *h){
int parent=1, child=2;
// 삭제할 키값, 루트노드 자리를 대체할 리프노드의 키값을 저장할 변수 선언
element saveRootKey, saveLeafKey;
// 루트노드의 키값을 저장
saveRootKey = h->heap[1];
// 마지막 노드의 키값을 저장, 해당 노드는 없어지므로 후위연산을 해줌
saveLeafKey = h->heap[(h->heap_size)--];
// 최대 리프노드까지 비교해준다.
while(child <= h->heap_size){
/*
새로이 루트노드값이 된 saveLeafKey값과과 자식노드 값을 비교하는 과정에서 좌측-우측 자식노드가
수치상 정렬된 것이 아니기에 그 중 더 큰 값을 골라냄
*/
if(child < h->heap_size && h->heap[child].key < h->heap[child+1].key){
child++; // 비교대상을 우측 자식노드 선정
}
if(saveLeafKey > h->heap[child].key){
break; // 더이상의 비교가 필요하지 않음(반복문 탈출)
}
// 자식노드의 키값 > 부모노드의 키값 일 시 두 노드의 키값을 맞바꿈.
h->heap[parent] = h->heap[child];
parent = child;
child *= 2; // 한 레벨 내려간다.
}
// 더이상의 비교가 필요하지 않은 시점에서의 parent노드에 임시 루트노드(saveLeafKey)가 정착한다.
h->heap[parent] = last_item;
return delete_item;
}
완전이진트리의 일종으로 우선순위큐의 구현을 위해 고안 된 특수한 자료구조
데이터베이스의 데이터는 디스크에 위치한다. 다만 in memory DB(가령 redis, memcach)의 캐시데이터는 디스크에 존재하지 않는다.
이 디스크에 특정 위치에 있는 레코드를 찾기 위해서 인덱싱을 하는데 이 때 차수가 높아지게 되면(block level 1 - block level 2 - ... - block level n) 멀티 인덱싱이 필요하다. 이를 효과적으로 해결하는 방법이 m-way tree이다.
이진트리란 m=2 인 m-way 트리를 말한다. 여기서 m 은 루트노드의 자식노드 개수를 말한다.
인덱싱
검색의 기준이되는, 키값 역할의 인덱스(index)를 이용하여 레코드를 검색하는 방법이다. 예를 들어 대한민국 성인 남성을 키 순으로
[~170]
[170 ~ 180]
[180~ 190]
[190 ~]
으로 나눠서 쿼리한다고 하면 여기서 인덱스는 키(height)가 된다.
멀티인덱싱
2개 이상의 인덱스를 이용하여 쿼리하는 방법을 멀티인덱싱이라 부른다.
{
KCE: '#height <= :height' and '#sex = :sex', // 실제 검색
EAN: { '#height: height, #sex: sex' }, // 인덱스 설정(키, 성별)
EAV: { ':height': 180, ':sex': 'male' } // 인덱스 = value에서 value값
}
키와 성별을 인덱스로 설정해 키가 180보다 작은 남성을 검색한다.
m-way tree도 차수가 높아지면 효율이 급감하게 된다. 따라서 B트리 라는 일정 규칙이 있는 m-way tree를 규정하게 되는데
B트리의 규칙
루트 노드는 최소 2개 이상의 자식노드를 가진다.
루트 노드를 제외한 노드는 최소 [M/2]개의 자식노드를 가진다. (* [ ] : 소수점 버림 )
모든 리프노드들의 레벨은 같아야 한다. (하나의 리프노드만 삐죽 튀어나와있으면 안됨)
노드의 자료수가 K+1개이면 K개의 자료를 가져야 한다.
자료는 정렬상태로 저장된다.
입력자료가 중복되지 않는다.
Creation process가 bottom-up 형식이다
*bottom-up creation process
노드에 키를 주입하고 자리가 부족하여 남게되는 키를 위해 새로운 노드뭉치를 우측에 생성한다.
왼쪽(기존)노드의 최우측 키(중간값)를 부모노드로 승격시키고 왼쪽노드의 나머지 키들을 좌 / 우 노드로 구분한다.
간단히 "B+ 트리 = B트리 + 데이터의 연결리스트" 로 표현할 수 있다.
B+ 트리는 B트리의 특성 다시를 그대로 가지며 순차검색(sequential search)에 특장점을 갖는 형태로 발전한 자료구조이다.
검색
B트리에서의 검색방법과 매우 유사하다.
삽입
B트리에서의 삽입방법과 유사하지만 노드가 분리될 시 부모노드로 승격되는 중간값(키)이 새로 생성되는 노드뭉치의 최 좌측 노드에도 포함되어야 한다.
삭제
B트리에선 자료 검색의 Best Case가 루트노드에서 끝날 수 있지만 B+트리의 경우 반드시 리프노드레벨까지 검색이 이뤄져야 한다는 점이 단점이다.
multiple-way-tree → B tree → B+ tree
알고리즘이란 어떤 목적을 달성하기 위한 일련의 과정이라 볼 수 있고 이 때 그 과정의 소요시간은 세 가지 측면에서 바라볼 수 있다.
시간복잡도란?
알고리즘 수행을 위해 프로세스가 처리하는 연산의 수와 관련된 함수로 input 개수 n에 해당하는 연산 수를 계수, 상수항 등을 제외하여, 결과에 지대한 영향을 미치는 부분만 점근적으로(Asymptotically) 남겨 표현한 것이 바로 이 시간복잡도이다.
알고리즘의 효율 가늠
알고리즘의 효율을 가늠할 때 실행시간을 기준으로 하면 앞선 소요시간의 측면 1,2에 지대한 영향을 받게 되는데 이는 외부 요인으로(Outer factor) 여겨지며 실질적으로는 연산의 수와 관련된 함수인 시간복잡도를 효율 가늠의 기준으로 세우는 것이 바람직하다.
시간 복잡도의 세가지 표현방법
앞서 힙을 다루며 삽입과 삭제의 원리와 그것의 알고리즘을 살펴보았다. 이를 그대로 이용하여
(최대힙을 기준으로 정렬할 경우)
우선 버블소트와 동작방식이 비슷한 Selection sort의 알고리즘을 먼저 살펴보자
Selection sort
#include <stdio.h>
#define MAX_SIZE 100
// method declaration
void selection(int * ptr, int size);
int main(){
int list[] = {2,9,7,8,3,6};
int size = sizeof(list) / sizeof(int);
selection(list, size);
for(int i = 0 ; i < size ; i++){
printf("%d ", list[i]);
}
return 0;
}
// method definition
void selection(int * ptr, int size){
int left=0, right=size-1, largest, temp, position;
for(int i = 0 ; i < size-1; i++){
// set the value of 'largest', 'temp'(will be used in exchanging process)
largest = ptr[0], temp = ptr[right];
for(int j = 1 ; j < right ; j++){
if(ptr[j] > largest){
largest = ptr[j];
position = j;
}
}
ptr[right] = largest; // sorting(ascending way)
ptr[position] = temp;
// last position's element fills the new largest value's pre-position
right--; // to fill the array's last index, last index -1, last index -2 .....
}
}
Bubble sort
void bubble(int list[], int size){
int temp;
int right = size;
while(right != 1){
for(int i = 1 ; i < right ; i++){
temp = list[i];
if(list[i] < list[i-1]){
// change each one's position(back and forth)
list[i] = list[i-1];
list[i-1] = temp;
}
}
right--; // last index, last index-1, last index-2...
}
}
int main(){
int list[] = {5,3,2,7,6,1};
bubble(list, sizeof list / sizeof(int));
for(int i = 0 ; i < sizeof list / sizeof(int) ; i++){
printf("%d ", list[i]);
}
return 0;
}