정렬(Sort)
정렬 알고리즘
정렬 알고리즘은 크게 2가지로 나뉠 수 있다.
간단하지만 느린 알고리즘 , 조금 더 복잡하지만 빠른 알고리즘
- Simple , Slow(간단하지만 느림)
- Selection Sort(선택 정렬)
- Bubble Sort(버블 정렬)
- Insertion Sort(삽입 정렬)
- Fast(위 알고리즘보다 복잡하지만 빠름)
- Merge Sort(병합 정렬)
- Quick Sokt(빠른 정렬)
- Heap Sort(힙 정렬)
Simple , Slow
1. Selection Sort(선택 정렬)
선택정렬의 아이디어는 다음과 같다
- 배열 중 가장 큰 값을 찾는다
- 가장 큰 값과 마지막 Index와 Swap
- 반복이 한 번 끝날때마다 맨 마지막은 Index은 정렬이 완료 따라서 체크 할 필요가 없다.
위 과정을 반복
O(n^2) 알고리즘이며 항상 모든 값을 확인해야 하므로 시간복잡도는 항상 같다.
ex)
[29, 10, 14, 37, 13]
- 배열 중 가장 큰 값을 찾는다. Max = 37
- 가장 큰 값과 마지막 Index를 Swap
Max = 37 , Index = 13
-> Swap
[29, 19, 14, 13, 37]
위 과정 반복
의사코드
Selection Sort 구현
#include <iostream>
#define N 5
using namespace std;
int main(){
int arr[N] = {24, 120, 64, 37, 13};
int max = 0;
int Last = N-1;
int k = 0;
int cnt = 0;
for(int i=0; i<N-1; i++){
max = arr[0];
k=i;
for(int j=0; j<=Last; j++){
if(max <= arr[j]){
max = arr[j];
k = j;
}
}
int temp = arr[Last];
arr[Last] = arr[k];
arr[k] = temp;
Last --;
}
for(int i=0; i<N; i++)
cout << arr[i] << " " ;
return 0;
}
2. Bubble Sort(버블 정렬)
Selection Sort와 아이디어는 비슷하며 물고기를 몰아서 그물로 잡는거와 비슷하다. (큰 물고기는 그물을 빠져나갈 수 없음)
- 배열의 현재값과 그 다음 값을 비교하여 더 큰 값을 찾는다.
- 큰 값을 더 뒤 Index로 Swap
- 한 사이클 반복이 끝날때마다 맨 마지막 Index는 정렬이 완료
위 과정 반복
O(n^2) 알고리즘이며 항상 모든 값을 확인해야 하므로 시간복잡도는 항상 같다.
ex)
[29, 10, 14, 37, 13]
- arr[0] 과 arr[1] 중 Max값 비교 Max = 29
- 더 큰 값의 Index를 더 뒤로 Swap
[10,29,14,37,13]
- arr[1] 과 arr[2] 중 Max값 비교 Max = 29
- 더 큰 값의 Index를 더 뒤로 Swap
[10,14,29,37,13]
위 과정 반복…
Bubble Sort 구현
#include <iostream>
#define N 5
using namespace std;
int main(){
int arr[N] = {10,29,14,37,13};
int len = N-1;
int max = 0;
for(int i=0; i<N-1; i++){
for(int j=0; j<=len-1; j++){
max = arr[j]; // 앞 Index값 삽입
if(max>arr[j+1]){ // 앞 Index값이 더 크면 Swap
int temp = arr[j]; //Swap
arr[j] = arr[j+1];
arr[j+1] = temp;
}
}
len --; // 맨 마지막 정렬은 완료되었으니 1개 축소
}
for(int i=0; i<N; i++)
cout << arr[i] << " " ;
return 0;
}
3. Insertion Sort(삽입 정렬)
뒤에서부터는 체크해야함 그 이유는 어차피 앞에서부터 확인 해서 들어갈 자리를 확인하더라도 뒤에서 부터 한 자리씩 Shift하는 과정이 필요함
Insert하기 전 Index까지는 이미 정렬이 되었다고 가정
- Insert하고 싶은 값을 미리 temp변수에 저장
- Insert값 이전 Index부터 값을 확인 후 temp보다 더 크 한 칸씩 Shift
- temp 보다 더 작은 값을 만나거나 첫 Index라면 그 자리에 temp 값을 Insert
위 과정을 반복
최악의 경우 O(n^2)의 수행시간
최선의 경우 O(n-1)의 수행시간
최악의 경우를 제외하고 Selection Sort나 Bubble Sort 보다 수행시간이 짧다.
ex)
[29, 10, 14, 37, 13]
#N 1일 때는 이미 정렬 되었다고 가정 1번Index(10)부터 시작
- 임시 변수temp에 10의 값을 저장
- arr[0] (현재29) 이 temp(insert)값보다 더 크다면 한 칸 Shift
- temp 보다 더 작은 값을 만나거나 첫 Index라면 그 자리에 temp 값을 Insert
[10,29,14,37,13]
- 임시 변수 temp에 14의 값을 저장
- arr[1] (현재29)이 temp(insert)값보다 더 크다면 한 칸 Shift
- temp 보다 더 작은 값을 만나거나 첫 Index라면 그 자리에 temp 값을 Insert
[10,14,29,37,13]
위 과정을 반복
Insertion Sort 구현
#include <iostream>
#define N 5
using namespace std;
int main(){
int idx=0;
int arr[N] = {29, 10, 14, 37, 13};
int temp = 0;
for(int i=1; i<N; i++){
temp = arr[i];
idx = i; //현재 i의 값을 저장
while(arr[idx] <= arr[idx-1]){
// 제일 처음은 정렬이 되었다고 생각하고 그 다음부터 작은값이 나올때까지 한자리씩 Swap
arr[idx] = arr[idx-1];
arr[idx-1] = temp;
idx --;
}
}
for(int i=0; i<N; i++)
cout << arr[i] << " " ;
return 0;
}
Fast (분할정복법)
1. Merge Sort(합병 정렬)
- 분할 : 해결하고자 하는 문제를 작은 크기의 동일한 문제들로 분할
- 정복 : 각각의 작은 문제를 순환적으로 해결
- 합병 : 작은 문제의 해를 합하여(Merge) 원래 문제에 해를 구함
- 데이터가 저장된 배열을 절반으로 나눔
- 각각을 순환 정렬
- 정렬된 두 개의 배열을 합쳐 전체를 정렬!
ex)
[12,24,63,12,51,2,125,32]
- 데이터를 절반으로 나누고 순환하여 정렬
[12,12,24,63] , [2,32,51,125] ↓ ↓ [12,24], [12,63] , [2,51] , [32,125] ↓ ↓ ↓ ↓ [12],[24],[63],[12] ,[51],[2],[125],[32]
- 합병 후 정렬
[2,12,12,24,32,51,63,125]
정렬된 두 배열을 합쳐야 하므로 추가적인 배열을 이용하여 합병해야 한다
- 1번 배열은 i idx
- 2번 배열은 j idx
- 둘 중 더작은값을 새로운 배열의 맨 처음에 삽입
- 한 쪽 배열의 index가 끝나면 나머지 배열의 값을 전부 추가 배열의 삽입
Mergesort(int Arr[], int left ,int right , int new)
- left,right 의 중간 지점 계산
- left정렬
- right정렬
- left , right합병
O(nlogn)의 수행시간
Merge Sort 구현
#include <iostream>
#include <algorithm>
#define SIZE 8
using namespace std;
void Merge(int arr[], int start, int mid, int end){
int i = start;
int j = mid+1;
int idx = start;
int len = SIZE;
int *temp = new int[len];
while(i<=mid && j<=end){ // 둘 중 하나라도 끝날때까지
if(arr[i] <= arr[j])
temp[idx++] = arr[i++];
else
temp[idx++] = arr[j++];
} //while
while(i<=mid)
temp[idx++] = arr[i++]; // 앞쪽 데이터가 남아있다면
while(j<=end)
temp[idx++] = arr[j++]; // 뒤쪽 데이터가 남아있다면
for(int k=start; k<=end; k++)
arr[k] = temp[k];
delete[] temp;
}
void Merge_Sort(int arr[], int start,int end){
if(start < end){ //시작보다 끝이 더 길어야 함 그게 아니라면 길이가 1개
int mid = (start+end)/2; //시작과 끝의 중간지점
Merge_Sort(arr, start, mid); //시작과 중간을 정렬
Merge_Sort(arr, mid+1, end); // 중간과 끝을 정렬
Merge(arr,start ,mid , end); // 합병 정렬
}
}
int main(){
int arr[] = {12,24,63,12,51,2,125,32};
int start = 0;
int end = SIZE-1;
Merge_Sort(arr,start,end);
for(int i=0; i<=end; i++)
cout << arr[i] << " ";
cout << endl;
return 0;
}
마지막 Merge하는 부분이 ..상당히 어려웠다.
2.Quick Sort(빠른정렬)
- 분할 : 조건을 이용하여 두 부분으로 분할
- 정복 : 각각의 문제를 순환적으로 해결
- 합병 : 합병과정은 없음
Pivot(기준)을 이용하여
Pivot보다 작은 수<- Pivot -> Pivot보다 큰 수
두 부분으로 분할 후 정렬
Merge Sort와는 다르게 합병하는 과정이 없다.
ex)
[12,24,63,12,51,2,125,32]
맨 마지막 값을 기준으로 정렬
[12,24,12,2] <- [32] -> [63,51,125]
왼쪽데이터와 오른쪽데이터 정렬
[2,12,12,24,32,51,63,125]
최악의 경우를 제외하고는 O(nlogn)의 수행속도
최악의 경우는 이미 배열이 정렬된 경우이고 O(n^2)의 수행속도
최악을 제외하고 이름처럼 가장 빠른 정렬 알고리즘이며 <algorithm>의 sort()함수가 QuickSort이다.
의사코드
# Pivot을 맨 처음 또는 맨 마지막을 설정하는건 별로 좋지 못한 방법이다. 따라서 중간값 또는 랜덤값을 설정하자
구현
#include <iostream>
#include <ctime>
using namespace std;
int Quick_partition(int arr[], int start, int end){
srand(time(NULL));
//int p_idx = rand()%end;
int p = arr[end]; // Pivot을 랜덤값으로 지정
int i = start-1; //배열의 밖을 지정
int j = start; // 시작 부분 지정
while(j<end)
{
if(arr[j]>=p)
j = j+1; // 기준보다 크면 그냥 넘어감
else{ //기준보다 작다면 값을 스왑
i = i+1;
int temp = arr[j];
arr[j] = arr[i];
arr[i] = temp;
j = j+1;
}
}
int temp = arr[i+1]; //마지막 기준값을 위치에 맞게 재배치 이후 Index를 return
arr[i+1] = p;
arr[end] = temp;
return (i+1);
}
void Quick_Sort(int arr[], int start, int end){
if(start<end){
int pivot = Quick_partition(arr, start, end);
Quick_Sort(arr,start, pivot-1);
Quick_Sort(arr, pivot+1, end);
}
}
int main(){
int arr[] = {12,24,63,12,51,2,125,32};
int size = 8;
int start = 0;
int end = size-1;
Quick_Sort(arr,start, end);
for(int i=0; i<=end; i++)
cout << arr[i] << " ";
cout << "\n";
return 0;
}
3.Heap Sort(힙정렬)
이진 Heap이라는 자료구조를 이용하여 정렬하는 방법으로 시간복잡도가 O(Nlog2N)으로 상당히 빠른속도의 정렬이 가능하며 추가배열이 필요하지 않아 저장공간을 적게 차지한다는 장점이 있다.
Heap ?
Heap이 되기위해서는 2가지 조건이 필요하다.
- Complete binary tree
- Heap property를 만족
첫 번째로는 Complete binary tree이다.
Complete binary tree(계층적관계)
- Full binary tree : 모든 Level의 Node가 꽉 차있는 형태
- Complete binary tree : 마지막 Level을 제외하고 모든 Node가 다 있으며 마지막 Level에서 오른쪽부터 노드가 없을 수 있다.
# Root node: Tree의 제일 윗 부분
# Leaf node: 자식이 없는 마지막 level Node
# binary tree(이진트리) : 각각의 Node가 최대 2명의 자식을 가질 수 있다.
# Full binary tree는 Complete binary tree조건도 만족
두 번째로는 Heap property만족이다
Heap property(힙의 특성) 다음 중 하나를 만족해야 한다.
- Max Heap Property
- 부모노드는 자식노드보다 크거나 같다
- Min Heap Property
- 부모노드는 자식노드보다 작거나 같다
Heap은 Complete binary tree 이면서 Max Heap property 또는 Min Heap property를 만족하면 된다.
Heap의 1차원 배열로 표현
다음과 같은 조건으로 힙을 1차원 배열로 표현을 할 수 있다.
- 루트 노드 : Arr[1]
- Arr[i]의 부모 노드 : Arr[i/2]
- Arr[i]의 왼쪽 자식 : Arr[2*i]
- Arr[i]의 오른쪽 자식 : Arr[(2*i)+1]
- 정렬할 데이터를 Complete binary tree로 생각
-> 아직 Heap이 아님 따라서 Heap특성을 만족시켜야 함# 현재 예제에서는 MAX특성을 이용 MIN또한 구현 가능
- MAX_HEAPIPY()을 이용하여 Heap특성을 만족시킴
두 가지 방법이 존재- Recursive Version
- Base case : Arr[i] 의 자식 노드가 없을 때
- Recursive Case : i 의 자식노드 중에서 최댓값 K를 찾고 Arr[i] 부모노드와 Arr[K] 최대 자식노드를 Swap MAX_HEAPIPY(Arr, K)
- Iteractive Version
While -> Arr[i]의 자식노드가 있는동안
i의 자식노드의 Max 값을 찾아 -> K 삽입
Arr[i] 부모노드와 Arr[K] 최대 자식노드를 Swap
- Recursive Version
MAX_HEAPIPY 구현
int Arr = [12,24,63,14,51,2,125,32]
입력받은 배열
void HEAP_BUILD(int Arr[], int sz)
// size/2 -> Root MAX_HEAPIPY
- 힙 구조를 만들기 위해서 자식노드가 존재하는 1번째 노드부터 루트까지 반복해주는 HEAP_BUILD함수를 만든다.
void MAX_HEAPIPY(int arr[], int N, int sz)
//재귀적으로 구현
- 자식노드가 없을때까지 자식노드 중 큰값을 찾아서 스왑을해줌
위 2가지 과정이 끝나면 MAX_HEAP 구조가 완성된다,
A = [125 51 63 32 24 2 12 14]
Heap_Sort(int arr[], int sz)
// 루트는 항상 최대값이므로 루트와 마지막을 교환 이후 마지막값을 정렬이 되었으므로 마지막을 제외하고 루트를 MAX_HEAPIPY
- 마지막으로 MAX_HEAP의 루트와 마지막인덱스를 교환하고 마지막 인덱스를 제외 -> 다시 MAX_HEAPIPY 반복!
A = [2 12 14 24 32 51 63 125]
// 결과값
MAX_HEAP_SORT 구현
#include <iostream>
#include <vector>
using namespace std;
void MAX_HEAPIPY(int arr[], int N, int sz)
{
int left = (N*2) +1;
int right = left +1;
int max = 0;
if(left>=sz){ //자식이 없는경우
;
}
else{
if(right>=sz){
right = 0;
max = left;
} //우측 노드가 없는경우
else{
max = arr[left]<=arr[right]?right:left;
}
if(arr[N]<= arr[max]){
int temp = arr[max];
arr[max] = arr[N];
arr[N] = temp;
MAX_HEAPIPY(arr,max,sz); //반복
}
}
}
void HEAP_BUILD(int arr[], int sz){
int len = sz;
for(int i=(len/2)-1; i>=0; i--){
MAX_HEAPIPY(arr,i,sz); //i는 원하는 노드
}
}
void Heap_Sort(int arr[], int sz)
{
if(sz==1){
cout << "ROOT\n";
}
else{
int len = sz-1;
int temp = arr[0];
arr[0] = arr[len];
arr[len] = temp;
MAX_HEAPIPY(arr,0,len);
Heap_Sort(arr, len);
}
}
int main(){
int A[] = {12,24,63,14,51,2,125,32};
int len = sizeof(A)/sizeof(int);
HEAP_BUILD(A,len);
for(int i=0; i<len; i++){
cout << A[i] << " " ;
}
cout << "\n";
Heap_Sort(A,len);
for(int i=0; i<len; i++){
cout << A[i] << " " ;
}
cout << "\n";
return 0;
}
==========================================================
검색(Serach)
트리(Tree) 와 이진트리(Tree)
용어
용어
- 루트(Root)
- 트리의 가장 위쪽에 있는 노드
- 부모-자식 관계
- 하나의 노드와 그 노드의 연결된 바로 아래 노드
- 형제관계
- 같은 레벨의 있는 노드
- 루트를 제외한 모든 노드들은 유일한 부모 노드를 가진다.
- 리프(leaf)노드
- 자식이 없는 노드
- 조상-자손 관계
- 부모-자식 관계를 확장한 것
- 부트리
- 전체 트리의 일부또한 트리이다.
- 레벨
- 루트-> Level1
- 높이
- 트리의 높이는 서로다른 Level의 개수
트리
- 계층적인 구조를 표현
- 조직도
- 폴더와 하위폴더
- 가계도
- 기본적인 성질
- 노드가 N개인 트리는 항상 N-1개의 링크(link)를 가진다.
- 트리에서 루트에서 어떤 노드로 가는 경로는 유일하다. 또한 임의의 두 노드간의 경로도 유일하다
이진트리
- 이진 트리에서 각 노드는 최대 2개의 자식 을 가진다.
-
각각의 자식 노드는 자신이 부모의 왼쪽 자신인지 오른쪽 자신인지가 결정된다.
- ex
- Expresion Tree
- (X+Y) * ((a+b) / c)
- Huffman Code(허프만 코드)
- 어떤 데이터를 인코딩하는 알고리즘(파일압축과 관련)
- Full and Complete Binary Trees
- 링크(HEAP_SORT)
- Expresion Tree
- 이진트리의 구조
- 연결구조(Linked Structure)
- 각 노드에 하나의 데이터 필드와 왼쪽자시(left), 오른쪽(right), 그리고 부모노드(p)의 주소를 저장
- 이진트리의 표현
- 연결리스트로 구현한다.
- 데이터필드 1개와 링크필드3개 필요
- 루트노드의 주소는 따로 보관
- 이진트리 알고리즘
순회(Traversal)
순회 : 이진 트리의 모든 노드를 방문하는 일
#연결리스트는 선형적인 구조라 하나의 방법밖에 존재하지 않지만 이진 트리는 다양한 방법이 존재
중순위(Inorder)순회
이진트리의 중순위(Inorder)순회
-
먼저 TL(트리의 왼쪽노드)을 inorder로 순회
-
Root(트리의 루트)를 순회
-
마지막으로 TR(트리의 오른쪽노드)을 inorder로 순회
TL -> ROOT -> TR
Recursive하게 구현
선순위(Preorder)순회
이진트리의 선순위(Preorder)순회
-
Root(트리의 루트)를 순회
-
먼저 TL(트리의 왼쪽노드)을preorder 순회
-
마지막으로 TR(트리의 오른쪽노드)을 preorder순회
ROOT -> TL -> TR Recursive하게 구현
후순위(Postorder)순회
이진트리의 후순위(Postorder)순회
-
먼저 TL(트리의 왼쪽노드)을 postorder 순회
-
TR(트리의 오른쪽노드)을 postorder 순회
-
마지막으로 Root(트리의 루트)를 순회
TL -> TR -> ROOT Recursive하게 구현
레벨오더(Level_order)순회
이진트리의 레벨오더
-
레벨 순으로 방문, 동일 레벨에서는 TL -> TR
-
큐(Queue)를 이용하여 구현
검색트리
검색트리 (Dynamic Set, Dictionary, Search Structure)
Dinamic Set을 트리의 형태로 구현
일반적으로 SEARCH, INSERT, DELETE 연산은 트리의 높이에 비례하는 시간복잡도를 가짐
이진검색트리 ,레드-블랙 트리, B-트리 등이 존재
- 여러 개의 키(Key)를 저장
- 다음과 같은 연산들을 지원하는 자료구조
- INSERT - 새로운 키 삽입
- SEARCH - 키 탐색
- DELETE - 키 삭제 ex) 심볼 테이블
자료구조 | 정렬여부 | 삽입(insert) | 탐색(serach) | 삭제(delete) |
---|---|---|---|---|
배열 | 정렬(O) | O(logn) | O(n) | O(n) |
정렬(X) | O(1) | O(n) | O(1) | |
연결리스트 | 정렬(O) | O(n) | O(n) | O(1) |
정렬(X) | O(1) | O(n) | O(1) |
#정렬된 혹은 정렬되지 않은 배열/연결리스트를 사용하여 INSERT , SERACH, DELETE를 할 경우 적어도 하나 이상은 O(n)의 시간복잡도를 가진다.
- 이를 해결하기 위한 다양한 방법
- 이진탐색트리 , 레드-블랙 트리, AVL-트리등의 트리 기반 구조
- Direct Address Table, 해쉬 테이블 등
이진검색트리(BST)
이진검색트리(BST)
- 이진 트리
- 각 노드에 하나의 키(Key)를 저장
- 각 노드 V에 대해서 그 노드의 왼쪽 부트리(subtree)에 있는 키들은 Key[V]보다 작거나 같고, 오른쪽 부트리에 있는 값은 크거나 같다.