360 likes | 648 Views
탐색 (1). 용어 정리 리스트 : 하나 이상의 필드로 된 레코드의 집합 키 (Key) : 레코드를 구분하기 위해서 사용되는 필드 순차 탐색 (Sequential Search) 레코드 리스트를 왼편에서 오른편 또는 오른편에서 왼편으로 레코드를 검사하는 것. 정렬의 필요성. 정렬의 두 가지 중요한 사용법 (1) 탐색에서 보조로 사용 (2) 리스트의 엔트리를 비교하는 방법으로 사용 정렬 방법 (1) 내부 방법 (internal methods)
E N D
탐색 (1) • 용어 정리 • 리스트 : 하나 이상의 필드로 된 레코드의 집합 • 키(Key) : 레코드를 구분하기 위해서 사용되는 필드 • 순차 탐색 (Sequential Search) • 레코드 리스트를 왼편에서 오른편 또는 오른편에서 왼편으로 레코드를 검사하는 것
정렬의 필요성 • 정렬의 두 가지 중요한 사용법 (1) 탐색에서 보조로 사용 (2) 리스트의 엔트리를 비교하는 방법으로 사용 • 정렬 방법 (1) 내부 방법 (internal methods) • 정렬할 리스트가 작아서 전체적인 정렬이 메인 메모리 상에서 실행될 수 있을 때 사용 (2) 외부 방법 (external methods) • 큰 리스트에 사용
삽입 정렬 (1) • 새로운 레코드를 i개의 정렬된 레코드 리스트에 끼워 넣어 크기가 i+1인 정렬된 결과 레코드 리스트를 만든다. void insert(element e, element a[], int i) {/* e를 정렬된 리스트 a[1:i]에 삽입하여 결과 리스트 a[1:i+1]도 정렬되게 한다. 배열 a는 적어도 i+2 원소를 포함할 공간을 가지고 있어야 한다. */ a[0] = e; while(e.key <a[i].key) { a[i+1] = a[i]; i--; } a[i+1] = e; } 정렬된 리스트로 삽입
삽입 정렬 (2) • insert(e, a, i)는 최악의 경우 삽입 전 i+1번 비교해야 함 • insert의 복잡도 : O(i) • insertionSort의 복잡도 • i = j-1 = 1,2,...,n-1일 때 insert를 호출 • 복잡도 : O( ) = O(n2) void insertionSort(element a[], int n) {/* a[1:n]을 비감소 키 순서대로 정렬 */ int j; for(j = 2; j <= n; j++) { element temp = a[j]; insert(temp, a, j-1); } } 삽입 정렬
j [1] [2] [3] [4] [5] _ 5 4 3 2 1 2 45 3 2 1 3 345 2 1 4 2345 1 5 12345 삽입 정렬 (3) • 삽입 정렬의 최악의 예 • n = 5, 입력 키 순서 : 5, 4, 3, 2, 1 • 입력 리스트가 역순이므로 새로운 레코드가 정렬된 부분에 삽입될 때마다 전체 정렬된 부분이 오른쪽으로 이동
j [1] [2] [3] [4] [5] _ 2 3 4 5 1 2 23 4 5 1 3 234 5 1 4 2345 1 5 12345 삽입 정렬 (4) • n = 5, 입력 키 순서가 2, 3, 4, 5, 1일 경우 • j = 2, 3, 4에 대한 시간 : O(1) • j = 5에 대한 시간 : O(n) • insertionSort는 안정적 • 작은 n(예를 들어, n≤30)에 대해 가장 빠른 정렬 방법
삽입 정렬 (5) • 삽입 정렬의 변형 • 이원 삽입 정렬 • insert(프로그램 7.4)에서 사용된 순차 탐색 기법 대신 이원 탐색을 사용 • 삽입 정렬에서 수행하는 비교 횟수를 줄인다 • 레코드 이동 횟수는 변하지 않음 • 연결 삽입 정렬 • 리스트의 원소들을 배열 대신 연결 리스트로 표현 • 링크 필드만 조정하면 되므로 레코드 이동 횟수가 0 • insert에서 사용한 순차 탐색은 그대로 사용
퀵 정렬 (1) • 평균 성능이 가장 좋은 정렬 방법 • 퀵 정렬(Quick Sort) 단계 (1) 정렬할 레코드 중 피벗(pivot:중추) 레코드를 선택 (2) 정렬할 레코드들을 다시 정돈 • 피벗의 왼쪽 : 피벗의 키보다 작거나 같은 레코드들을 위치 시킴 • 피벗의 오른쪽 : 피벗의 키보다 크거나 같은 레코드들을 위치 시킴 (3) 퀵 정렬을 순환적으로 사용 • 피벗의 왼쪽과 오른쪽의 레코드들은 서로 독립적으로 정렬된다
퀵 정렬 (2) void quickSort(element a[], int left, int right) {/* a[left:right]를 비감소 키 순서대로 정렬한다. a[left].key는 피벗으로 임의로 선정한다. a[left].key≤ a[right+1].key라고 가정한다. */ int pivot, i, j; element temp; if(left < right) { i = left; j = right +1; pivot = a[left].key; do{ /* 서브리스트를 왼쪽에서 오른쪽으로 탐색하면서 left와 right 경계가 교차되거나 만날 때까지 out-of-order 원소들을 swap한다 */ do i++; while(a[i].key < pivot); do j--; while(a[j].keky > pivot); if(i < j) SWAP(a[i], a[j],temp); } while(i < j); SWAP(a[left], a[j],temp); quickSort(a, left, j-1); quickSort(a, j+1, right); } } 퀵 정렬
퀵 정렬 (3) • 키 (26, 5, 37, 1, 61, 11, 59, 15, 48, 19)를 가진 10개의 레코드로 된 리스트를 퀵 정렬을 사용하여 정렬 R1 R2 R3 R4 R5 R6 R7 R8 R9 R10 left right [26 5 37 1 61 11 59 15 48 19] 1 10 [11 5 19 1 15] 26 [59 61 48 37] 1 5 [ 1 5] 11 [19 15] 26 [59 61 48 37 1 2 1 5 11 [19 15] 26 [59 61 48 37] 4 5 1 5 11 15 19 26 [59 61 48 37] 7 10 1 5 11 15 19 26 [48 37] 59 [61] 7 8 1 5 11 15 19 26 37 48 59 [61] 10 10 1 5 11 15 19 26 37 48 59 61 quickSort를 호출할 때마다의 리스트 상태 대괄호는 계속해서 정렬할 서브리스트를 나타냄
퀵 정렬 (4) • quickSort의 분석 • 최악의 경우 복잡도 : O(n2) • 한 레코드의 위치가 정확히 정해질 때마다 그 레코드의 왼쪽과 오른쪽의 서브리스트 크기가 같을 경우 • 크기가 n/2인 두 개의 서브리스트를 정렬하는 작업과 같다 • 크기가 n인 리스트에서 한 레코드를 위치시키는 데 필요한 시간 : O(n) T(n) ≤ cn + 2T(n/2) 어떤 상수 c에 대해서 ≤ cn + 2(cn/2 + 2T(n/4)) ≤ 2cn + 4T(n/4) . . . ≤ cnlog2n + nT(1) = O(nlog2n) • 평균 연산 시간 : O(nlogn)
퀵 정렬 (7) • 퀵 정렬과 스택 공간 • 퀵 정렬은 순환(recursion)을 구현하기 위해 스택 공간 필요 • 리스트가 균등하게 나뉘는 경우 • 최대 순환 깊이는 log n이 되어 스택 공간으로 O(log n) 필요 • 최악의 경우 • 순환의 각 단계에서, 크기가 n-1인 왼쪽 서브 리스트와 크기가 0인 오른쪽 서브리스트로 리스트가 나뉘는 경우 • 순환 깊이는 n이 되어 스택 공간으로 O(n) 필요 • 보다 작은 크기의 서브리스트를 먼저 정렬하여 스택 공간을 점근적으로 감소시킬 수 있다.
합병 정렬 (1) • 합병(Merging) • 두개의 정렬된 리스트를 하나의 정렬된 리스트로 합병 void merge(element initList[], element mergedList[], int i, int m, int n) { /* initList[1:m]과 initList[m+1:n]는 정렬된 리스트. 이들은 정렬된 리스트 mergedList[i:n]으로 합병된다.*/ int j,k,t; j = m+1; k = i; while( i <= m && j <= n) { if (initList[i].key <= initList[j].key) mergeList[k++] = initList[i++]; else mergeList[k++] = initList[j++]; } if (i > m) /* mergedList[k:n] = initList[j:n]*/ for(t = j; t <= n; t++) mergeList[t] = initList[t]; else /* mergedList[k:n] = initList[i:m] */ for(t = i; t <= m; t++) mergeListpk+t-i] = initList[t]; } 합병시간 : O(n-l+1) n-l+1 : 합병될 레코드 수
합병 정렬 (2) • 반복 합병 정렬(Iterative Merge Sort) • 입력 리스트를 길이가 1인 n개의 정렬된 서브리스트로 간주 • 반복 합병 정렬 단계 • 첫번째 합병 단계 : 리스트들을 쌍으로 합병하여 크기가 2인 n/2개의 리스트를 얻는다. • n이 홀수이면 리스트 하나는 크기가 1 • 두번째 합병 단계 : n/2개의 리스트를 다시 쌍으로 합병하여 n/4개의 리스트를 얻는다. • 합병 단계는 하나의 서브 리스트가 남을 때까지 계속된다. • 한번 합병할 때마다 서브 리스트의 수는 반으로 줄어든다.
합병 정렬 (3) • 반복 합병 정렬의 예 • 입력 리스트 : (26, 5, 77, 1, 61, 11, 59, 15, 48, 19) 26 5 77 1 61 11 59 15 48 19 5 26 1 77 11 61 15 59 19 48 1 5 26 77 11 15 59 61 19 48 1 5 11 15 26 59 61 77 19 48 1 5 11 15 19 26 48 59 61 77 각 단계에서 합병되는 서브리스트를 합병 트리로 나타낸 것
합병 정렬 (4) • 합병 정렬은 여러 개의 합병 단계(pass)로 구현된다. void mergePass(element initList[], element resultList[], int n, int s) { /* 길이가 s인 서브리스트의 인접 쌍들이 // initList에서부터 resultList로 합병된다. n은 initList에 있는 레코드 수이다. for(i = 1; i <= n-2*s+1; i+= 2*s) merge(initList, mergedList, i, i+s-1, i+2*s-1); if((i+s-1)<n) merge(initList, mergedList, i, i+s-1, n); else for(j=i; j <= n; j++) mergedList[j] = initList[j]; } 합병 패스
합병 정렬 (5) • mergeSort 분석 • i번째 패스 : 크기가 2i-1인 리스트를 합병 • 총 단계가 데이타에 적용된다. • 합병의 각 단계에 걸리는 시간 : O(n) • 총 연산 시간 : O(nlogn) void mergeSort(element a[], int n) { int s = 1; /* 현재 segment 크기 */ element extra[MAX_SIZE]; while (s<n) { mergePass(a, extra, n, s); s *= 2; mergePass(extra , a, n , s); s *= 2; } }
합병 정렬 (6) • 순환 합병 정렬 (Recursive Merge Sort) • 정렬할 리스트를 거의 똑같이 두 개로 나눈다. • left 서브리스트와 right 서브리스트 • 서브리스트들은 순환적으로 정렬된다. • 정렬된 서브리스트들은 합병된다.
합병 정렬 (7) • 순환적 합병 정렬의 서브 리스트 분할 • 입력 리스트 : (26, 5, 77, 1, 61, 11, 59, 15, 49, 19) 26 5 77 1 61 11 59 15 48 19 5 26 11 59 5 26 77 11 15 59 19 48 1 61 1 5 26 61 77 11 15 19 48 59 1 5 11 15 19 26 48 59 61 77
합병 정렬 (7) • 순환적 합병 정렬의 서브 리스트 분할 • 두 개의 서브리스트를 만든다 • left ~ , +1~ right • 정수 배열 link[1:n]을 사용 • 함수 merge가 사용될 때 일어나는 레코드 복사를 제거하기 위하여 정수 포인터를 각 레코드에 연관 시키기 위함 • list[i] - 정렬된 서브리스트에서 레코드 i다음에 있는 레코드 • 이 배열의 사용으로 레코드 복사는 링크 변경으로 대체되고 정렬 함수의 실행 시간은 레코드 크기 s에 독립적이 됨 • 필요한 추가 공간 : O(n) ∴ 반복적 합병 정렬은 O(snlogn) 시간과 O(sn) 추가 공간 필요 • 최종 체인이 결정하는 정렬 순서로 레코드들을 물리적으로 재정돈 해야하는 후처리 필요
합병 정렬 (8) • rMergeSort의 분석 • 합병 정렬의 순환 버전 • 안정적 • 연산 시간 : O(nlogn) • 순환 합병 정렬 구현 int rmergeSort(element a[], int link[], int left, int right) {// 정렬할 리스트는 a[left:right]. 초기에 link[i]는 모든 i에 대해 0이다. // rmergeSort는 정렬된 체인의 첫번째 원소의 인덱스를 반환한다. if(left >= right) return left; int mid = (left + right)/2; return listMerge(a, link, rmergeSort(a, link, left, mid), //왼쪽 반을 정렬 rmergeSort(a, link, mid+1, right)); // 오른쪽 반을 정렬 } 순환 합병 정렬
합병 정렬 (9) int listMerge(element a[], int link[], int start1, int start2) { // start1과 start2에서 시작하는 정렬된 체인이 합병된다. // link[0]가 임시 헤더로 사용된다. 합병된 체인의 시작을 반환한다. int last1, last2, lastResult=0; // 결과 체인의 마지막 레코드 for(last1 =start1, last2 = start2; last1 && last2; ) if(a[last1] <= a[last2]) { link[lastResult] = last1; lastResult = last1; last1 = link[last1]; } else { link[lastResult] = last2; lastResult = last2; last2 = link[last2]; } // 나머지 레코드들을 결과 체인에 첨가 if(last1 == 0) link[lastResult] = last2; else link[lastResult] = last1; return link[0]; } 정렬된 체인의 합병
합병 정렬 (10) • 합병 정렬의 변형 • 자연 합병 정렬 (Natural Merge Sort) • 입력 리스트 내에 이미 존재하고 있는 순서를 고려 • 이미 정렬되어 있는 레코드의 서브리스트를 식별할 수 있도록 초기 패스를 만들어야 함 26 1 61 15 48 19 5 26 11 59 5 26 77 1 11 59 61 15 19 48 1 5 11 26 59 61 77 15 19 48 1 5 11 15 19 26 48 59 61 77 자연 합병 정렬
히프 정렬 (1) • 합병 정렬의 문제점 • 정렬한 레코드 수에 비례하여 저장 공간이 추가로 필요 • 히프 정렬(heap sort) • 일정한 양의 저장 공간만 추가로 필요 • 최악의 경우 연산 시간과 평균 연산 시간 : O(nlogn) • 합병 정렬보단 약간 느림 • 최대-히프 구조 사용 • 최대-히프와 연관된 삭제, 삽입 함수 : O(nlogn) 정렬 방법 • adjust 함수 사용 – 최대 히프를 재조정
히프 정렬 (3) • 정렬 과정 • adjust를 반복적으로 호출 최대 히프 구조를 만든다. • 히프의 첫번째 레코드와 마지막 레코드를 교환한다. • 최대 키를 가진 레코드가 정렬된 배열의 정확한 위치에 자리잡게 됨 • 히프의 크기를 줄인 후 히프를 재조정한다. • 전체 연산 시간 : O(nlogn) void heapSort(element a[], int n) { int i, j; element temp; for(i=n/2; i>0; i--) adjust(a,i,n); for(i=n-1; i>0;i--) { SWAP(a[1], a[i+1],temp); adjust(a, 1, i); } }
히프 정렬 (4) • 이진 트리로 변환된 배열 • 입력 리스트 : (26, 5, 77, 1, 61, 11, 59, 15, 48, 19) [1] [1] 77 26 [2] [3] 61 59 [2] [3] 5 77 [4] [5] 19 48 11 26 [4] [5] 61 1 11 59 [6] [7] [6] [7] 5 15 1 19 15 48 [8] [9] [10] [8] [9] [10] (a) 입력 리스트를 이진 트리로 변환한 모습 (b) 초기 히프 형태로 구성한 것 (heapSort 코드의 첫번째 for 루프를 거친 후의 최대 히프 모습)
히프 정렬 (5) [1] [1] 61 59 48 59 48 26 [2] [2] [3] [3] 19 19 15 11 26 15 11 1 [4] [5] [4] [5] [6] [7] [6] [7] 5 1 5 (b) 히프크기=8 Sorted = [61,77] (a) 히프크기=9 Sorted = [77] [8] [8] [9] [1] 26 [1] 48 19 11 [2] [3] 19 26 [2] [3] 5 15 1 [4] [5] 5 15 11 1 [4] [5] [6] [6] [7] (d) 히프크기=6 Sorted = [48,59,61,77] (c) 히프크기=7 Sorted = [59,61,77]
히프 정렬 (6) [1] [1] 19 15 [2] 15 11 [2] 5 11 [3] [3] 5 (f) 히프크기=5 Sorted = [19,26,48,59,61,77] 1 1 (e) 히프크기=5 Sorted = [26,48,59,61,77] [4] [5] [4] [1] 11 (g) 히프크기=3 Sorted = [15,19,26,48,59,61,77] 5 1 [2] [3]
여러 키에 의한 정렬 (3) • LSD(least-significant-digit-first) 정렬 • 최소 유효 숫자 우선 정렬 • 카드 숫자 값(키 K2)에 따라 13개의 파일을 만듦 • 3들을 2들 위에, king들을 queen들 위에, ace들을 king들 위에 올려놓음 • 카드 뭉치를 거꾸로 놓고 안정된 정렬 방법을 이용하여 무늬(K1)에 따라 4개의 파일로 만듦 • 4개의 파일들은 각각 키 K2에 따라 정렬되게 함 • 4개의 파일을 합침 • LSD가 MSD보다 더 단순 • 생성된 파일과 서브 파일을 독립적으로 정렬할 필요가 없으므로 오버헤드가 적게 든다.
여러 키에 의한 정렬 (4) • 기수 (radix) 정렬 • 어떤 기수 r을 이용하여 정렬 키를 몇 개의 숫자로 분해 • r = 10 : 키를 십진수로 분할 • r = 2 : 키를 이진수로 분할 • 기수-r 정렬에서는 r개의 빈(bin)이 필요 • (why?) 정렬되어야 하는 레코드가 R1,...,Rn일 때, 레코드의 키는 기수-r을 이용하여 분할 0~(r-1) 사이의 d개의 숫자를 가진 키가 된다. • 각 빈의 레코드는 빈의 첫 레코드를 가리키는 포인터와 마지막 레코드를 가리키는 포인터를 통해 체인으로 연결되며, 체인은 큐처럼 동작
여러 키에 의한 정렬 (5) int radixSort(element a[], int link[], int d, int r, int n) { int front[r], rear[r]; int i, bin, current, first, last; first = 1; for(i = 1; i < n; i++) link[i] = i+1; link[n] = 0; for(i=d-1; i >=0; i--) { for(bin = 0; bin < r; bin++) front[bin] = 0; for(current = first; current; current = link[current]) { bin = digit[a[current], i, r); if(front[bin] == 0) front[bin] = current; else link[rear[bin]] = current; rear[bin] = current; } for(bin++; bin < r; bin++) if(front[bin]) {link[last] = front[bin]; last = rear[bin]; } link[last] = 0; } return first; } LSD 기수 정렬
여러 키에 의한 정렬 (6) • radixSort의 분석 : • 전체 연산 시간 : O(d(n+r)) • d 패스를 처리하면서 각 패스의 연산 시간은 O(n+r)임 • d 값은 기수 r의 선택과 가장 큰 키에 의해 정해짐 • r의 선택을 달리하면 연산 시간이 달라진다.
여러 키에 의한 정렬 (7) • 기수 정렬의 예 • 범위가 [0, 999]인 십진수를 정렬 (d=3, r=10) a[1] a[2] a[3] a[4] a[5] a[6] a[7] a[8] a[9] a[10] 179 208 306 93 859 984 55 9 271 33 (a) 초기 입력 e[0] e[1] e[2] e[3] e[4] e[5] e[6] e[7] e[8] e[9] 9 33 859 271 93 984 55 306 208 179 f[0] f[1] f[2] f[3] f[4] f[5] f[6] f[7] f[8] f[9] 179 93 33 984 55 306 208 179 859 9 (b) 첫번째-패스 큐들 (First-pass queues)과 결과 체인
여러 키에 의한 정렬 (8) e[0] e[1] e[2] e[3] e[4] e[5] e[6] e[7] e[8] e[9] 9 208 859 179 306 33 55 271 984 93 f[0] f[1] f[2] f[3] f[4] f[5] f[6] f[7] f[8] f[9] 306 208 9 33 55 859 271 179 984 93 (c) 두 번째-패스 큐들 (Second-pass queues)과 결과 체인
여러 키에 의한 정렬 (9) e[0] e[1] e[2] e[3] e[4] e[5] e[6] e[7] e[8] e[9] 93 55 271 33 179 208 306 859 984 9 f[0] f[1] f[2] f[3] f[4] f[5] f[6] f[7] f[8] f[9] 9 33 55 93 179 208 271 306 859 984 (d) 세 번째-패스 큐들 (Third-pass queues)과 결과 체인