Book/Foundations Of Algorithms

CH8. More Computational Complexity: The Searching Problem

S0LL 2024. 12. 10. 15:33

8.1 Lower Bounds for Searching Only by Comparisons of Keys

 

이 섹션에서는 키 비교만을 사용하여 배열에서 특정 키를 검색할 때 소모되는 최악의 경우 비교 횟수의 하한을 다룹니다.

특히, Binary Search Sequential Search의 의사결정 트리(Decision Tree)를 사용하여 검색 과정의 효율성을 분석합니다.

 

의사결정 트리 (Decision Tree)

정의:

의사결정 트리는 키 를 배열에서 검색하는 과정에서 발생하는 모든 비교를 나타냅니다.

 내부 노드는 키 비교를 나타냅니다.

 리프 노드는 검색 결과를 나타냅니다 (성공적으로 검색된 위치 또는 실패).

예시:

Figure 8.1: 정렬된 배열에서 이진 탐색(Binary Search)의 의사결정 트리를 나타냅니다.

Figure 8.2: 순차 탐색(Sequential Search)의 의사결정 트리를 나타냅니다.

검색 알고리즘과 의사결정 트리의 관계

 

검색 알고리즘이 개의 키를 처리한다고 가정할 때:

1. 개의 키를 검색하는 모든 알고리즘에는 유효한 의사결정 트리가 존재합니다.

2. 최악의 경우 비교 횟수는 트리의 최대 깊이 (트리의 루트에서 리프까지의 가장 긴 경로)로 표현됩니다.

 

8.1.1 Lower Bounds for Worst-Case Behavior

 

Lemma 8.1

 

Lemma 8.2

 

Theorem 8.1

 

 

8.1.2 평균 성능에 대한 하한 (Lower Bounds for Average-Case Behavior)

 

1. 정의와 개념

거의 완전 이진 트리(Nearly Complete Binary Tree):

한 이진 트리가 깊이가  d - 1 까지 채워져 있으면 “거의 완전 이진 트리”라고 합니다.

예를 들어, Figure 8.3(a)는 완전 이진 트리, Figure 8.3(b)는 거의 완전 이진 트리입니다.

Binary Search의 결정 트리(Decision Tree)는 거의 완전 이진 트리로 나타낼 수 있습니다.

 

 

2. Lemma 8.3

 

내용:

Binary Search의 결정 트리는 “거의 완전 이진 트리”입니다.

 

증명:

기초: 노드가 1개일 때, 이는 명백히 “거의 완전 이진 트리”입니다.

귀납 가정: 노드가  k 개일 때, 이진 트리가 “거의 완전 이진 트리”라고 가정합니다.

귀납 증명:

노드가  n 개일 때:

 n 이 홀수일 경우, Binary Search는  n - 1 /2로 두 서브트리를 나눕니다. 이 두 서브트리는 구조적으로 동일하며, 귀납 가정에 의해 거의 완전합니다.

 n 이 짝수일 경우에도, 좌우 서브트리 중 하나가  n/2 , 나머지가  (n/2) - 1 이며, 두 서브트리도 귀납 가정에 의해 거의 완전합니다.

따라서 결정 트리는 “거의 완전 이진 트리”입니다.

 

 

 

3. Algorithm 8.1: Binary Search 평균 시간 분석

 

기본 동작:

 x   S[{mid}] 의 비교.

 

분석:

 

 

 

4. Lemma 8.4

 

내용:

 TND 가 최소( minTND(n) )가 되려면 결정 트리가 “거의 완전 이진 트리”여야 합니다.

 

증명:

 TND 가 최소가 아닌 경우, 트리의 마지막 레벨에서 노드를 제거하고 위로 이동시켜  TND 를 줄일 수 있습니다.

따라서  minTND(n) 를 가지려면 트리가 “거의 완전 이진 트리”여야 합니다.

 

 

 

5. Lemma 8.5

 

내용:

평균 시간 복잡도는  \text{minTND(n)} / n 입니다.

 

증명:

Binary Search의 평균 시간은  TND / n 에서 도출됩니다. Lemma 8.3과 8.4에 의해 이 결과를 얻을 수 있습니다.

 

 

6. Lemma 8.6

 

내용:

결정 트리에서 각 키  s_i 는 최소 한 번 비교되어야 하므로, 평균 시간 복잡도의 하한은  {minTND(n)} / n 입니다.

 

 

 

 

 

8.2 Interpolation Search

 

기본 개념

Interpolation Search는 **키 값(key value)**이 균등하게 분포되어 있다고 가정할 때 Binary Search보다 효율적으로 동작할 수 있는 탐색 알고리즘입니다.

Binary Search는 중간(mid) 위치를 항상 배열의 가운데로 설정하지만, Interpolation Search는 키 값의 분포에 따라 예상 위치를 계산해 중간값을 동적으로 결정합니다.

이를 통해 데이터가 균일하게 분포된 경우 Binary Search보다 적은 비교 횟수로 원하는 값을 찾을 수 있습니다.

 

 

 

 

 

 

8.3 Searching in Trees

 

이 부분은 트리 구조를 활용한 탐색에 대해 설명합니다. Binary Search(이진 탐색)와 Interpolation Search(보간 탐색)는 효율적이지만, 동적 데이터 추가나 삭제가 자주 이루어지는 경우에는 적합하지 않을 수 있습니다. 트리 구조는 이러한 상황에서 효율적인 해결책을 제공합니다.

 

8.3.1 Binary Search Trees (이진 탐색 트리)

 

1. 정적 탐색 vs 동적 탐색

정적 탐색: 데이터가 추가되거나 삭제되지 않고 고정된 경우. 이진 탐색(Binary Search)이 적합.

동적 탐색: 데이터가 자주 추가되고 삭제되는 경우. 이진 탐색 트리는 데이터 추가 및 삭제가 용이.

 

2. 이진 탐색 트리 구조

트리의 노드는 데이터 값을 저장하고, 왼쪽 자식은 더 작은 값, 오른쪽 자식은 더 큰 값을 저장.

In-order Traversal(중위 순회):

왼쪽 서브트리를 탐색 → 루트 방문 → 오른쪽 서브트리 탐색.

이 방법을 통해 정렬된 순서로 데이터에 접근 가능.

 

3. 이진 탐색 트리와 이진 탐색의 관계

이진 탐색 트리에서 탐색 과정은 이진 탐색(Binary Search)와 동일.

예를 들어, 트리 구조에서 특정 값을 찾기 위해 키 값과 비교를 반복하며 좌우 서브트리로 이동.

 

4. 문제점

트리가 불균형(Skewed Tree) 상태가 되면 성능이 선형 시간 O(n)으로 저하될 수 있음.

예: 값이 정렬된 순서로 추가될 경우 트리가 리스트처럼 한쪽으로 치우침.

 

5. Theorem 8.3

평균적으로, 이진 탐색 트리를 사용하는 탐색 시간은 A(n) ≈ 1.38 \log n.

트리가 무작위로 구성된다고 가정하면, 효율적인 탐색 성능을 보장.

 

 

8.3.2 B-Trees (B-트리)

 

B-트리는 대규모 데이터베이스 및 외부 저장소 검색에서 주로 사용되는 트리입니다.

 

1. 특징

B-트리는 항상 균형을 유지하며, 모든 리프 노드가 같은 깊이에 있음.

데이터를 추가하거나 삭제해도 트리의 균형이 깨지지 않음.

 

2. 3-2 트리 (3-2 Tree)

특성:

각 노드는 1개 또는 2개의 키를 포함.

키는 항상 정렬된 상태로 저장됨.

각 노드의 자식은 키 값에 따라 구분됨.

모든 리프 노드는 같은 깊이에 있음.

 

3. 추가 동작

키를 추가할 때:

1. 노드가 가득 차면, 가운데 키를 부모로 올림.

2. 리프 노드에 새 키를 추가하거나, 부모 노드로 분리.

 

4. 장점

항상 균형이 유지되므로 탐색, 삽입, 삭제 모두 O(\log n) 시간 복잡도를 가짐.

B-트리는 대규모 데이터 처리에 적합하며, 대부분의 현대 데이터베이스 시스템에서 활용됨.

 

 

  Figure 8.5: 이진 탐색 트리의 구성 및 탐색 방법.

Figure 8.6: 스큐드(Skewed) 트리 예시. 키가 정렬된 순서로 삽입될 경우.

Figure 8.7: 3-2 트리에서 데이터 추가 예제.

(a): 리프 노드에 새로운 키 추가.

(b): 리프 노드가 가득 찬 경우 부모로 키 이동.

(c): 루트 노드가 가득 찬 경우 새로운 루트 생성.

 

이렇게 B-트리는 데이터베이스 시스템에서 효율적으로 활용될 수 있는 매우 유용한 트리 구조를 설명합니다.

 

 

 

8.4 Hashing

 

Hashing 개요

 

Hashing은 키를 특정 값으로 변환하여 데이터를 효율적으로 저장하고 검색하는 기법입니다.

문제 상황: 1부터 100까지의 키와 약 100개의 데이터를 다룰 때, 배열에 데이터를 저장하면 키가 곧 인덱스가 되어 검색이 빠르지만, 메모리 낭비가 발생할 수 있습니다.

해결 방법: 키를 특정 범위(예: 0~99)로 변환하는 해시 함수를 사용해 메모리를 절약하면서도 효율적인 검색을 가능하게 합니다.

 

Hash Function

 

해시 함수  h(key) = key mod 100  는 키를 100으로 나눈 나머지를 반환합니다.

이 함수는 키를 특정 인덱스(0~99)로 변환하여 배열  S[i]  에 저장합니다.

조건: 키가 유일한 인덱스로 매핑되지 않을 가능성(충돌)이 존재합니다.

 

Collision (충돌)

충돌이란 두 키가 동일한 해시 값으로 변환되는 경우를 뜻합니다.

예를 들어, 키가 100개인 경우 충돌이 없는 확률은 매우 낮습니다 ( 9.3 * 10^{-43} ).

 

충돌 해결 방법: Open Hashing (열린 해싱)

각 해시 값에 버킷(bucket) 을 생성하고, 해당 해시 값에 매핑된 키들을 연결 리스트 형태로 저장합니다.

그림 8.8에서는 동일한 해시 값을 가지는 키들이 하나의 버킷에 저장된 예를 보여줍니다.

 

버킷과 검색

버킷 개수와 키 수: 버킷이 많을수록 충돌 확률이 줄어들지만, 메모리 낭비가 증가합니다. 일반적으로 키 수와 동일한 버킷을 사용하는 것이 적절합니다.

순차 검색: 충돌이 발생하면 연결 리스트를 순회해야 하므로 검색 시간이 증가할 수 있습니다.

 

 

 

Table 8.1

 

테이블 8.1은  k = \log n  또는  k = 2 \log n 일 때, 충돌 확률을 보여줍니다.

예를 들어,  n = 1024 일 때  k = \log n 에 대해 충돌 확률은  0.00027 로 매우 낮습니다.

 

결론

Hashing은 키가 균등하게 분포된 경우 Binary Search보다 훨씬 빠릅니다.

그러나 최악의 경우를 대비하여 해시 함수를 신중히 설계해야 합니다.

테이블을 참조하여 현실적인 키와 버킷 수에 따라 해싱 전략을 선택할 수 있습니다

 

 

 

8.5.1 Finding the Largest Key

1. 문제 정의:

주어진 배열  S 에서 가장 큰 키  \text{large} 를 찾습니다.

입력:  n , 배열  S  (크기  n ).

출력:  \text{large}  (가장 큰 값).

2. 알고리즘 (Algorithm 8.2):

void find_largest (int n, const keytype S[], keytype& large) {
    index i;
    large = S[1]; // 초기화
    for (i = 2; i <= n; ++i) { 
        if (S[i] > large)
            large = S[i];
    }
}

 

배열의 각 값을 순회하며 현재까지 발견된 가장 큰 값과 비교해 업데이트합니다.

비교 연산 수:  T(n) = n - 1 .

 

3. 이론적 최적성:

직관적으로, 배열의 모든 값을 비교해야 가장 큰 값을 찾을 수 있습니다.

Theorem 8.7: 어떤 결정적 알고리즘이라도 모든 입력에 대해  n-1 번 이상의 비교를 수행해야 가장 큰 키를 찾을 수 있습니다.

증명:  n-1 번보다 적게 비교하면 두 키를 비교하지 않아 잘못된 결과를 낼 수 있는 입력이 존재.

 

 

8.5.2 Finding Both the Smallest and Largest Keys

1. 문제 정의:

배열  S 에서 동시에 가장 큰 값  \text{large} 와 가장 작은 값  \text{small} 을 찾습니다.

2. 알고리즘 (Algorithm 8.3):

void find_both (int n, const keytype S[], keytype& small, keytype& large) {
    index i;
    small = S[1];
    large = S[1];
    for (i = 2; i <= n; ++i) {
        if (S[i] < small)
            small = S[i];
        else if (S[i] > large)
            large = S[i];
    }
}

 

각 배열 요소를 순회하며  \text{small}   \text{large} 를 업데이트.

비교 연산 수:  W(n) = 2(n-1) .

 

3. 향상된 방법:

한 번의 비교로 두 값을 동시에 판단하는 방식.

Algorithm 8.4:

두 개씩 쌍을 이루어 비교.  n/2 번의 비교로  \text{small}   \text{large}  후보군 생성.

추가  n/2 번의 비교로 최종 값 선택.

최적 비교 수:  3n/2 - 2  (짝수일 때).

 

Adversary Argument (대항 논증)

1. 목적:

알고리즘의 최악의 성능에 대해 하한선을 증명.

대항자가 알고리즘이 최대한 많은 비교를 수행하도록 설계된 입력을 동적으로 만듦.

2. 예제:

 n = 5 일 때,  8 번의 비교가 필요함을 대항자가 증명.

 

 

8.5.4 Finding the k-th Smallest Key

 

문제 설명

배열  S 에서  k -번째로 작은 키를 찾는 문제를 다룹니다.

단순히 배열을 정렬하고  k -번째 값을 반환하는 방법은  O(n \log n)  시간이 걸립니다.

더 적은 비교로 해결하기 위해 분할 및 정복(divide and conquer) 방법을 사용합니다.

 

알고리즘 설명 (Algorithm 8.5)

Partition Procedure: 배열을 분할합니다.  pivotpoint 는 기준 값입니다.

 pivotpoint 를 기준으로, 작은 값들은 왼쪽, 큰 값들은 오른쪽으로 배치합니다.

재귀 호출:

1. 만약  k = pivotpoint ,  S[pivotpoint] 를 반환합니다.

2.  k < pivotpoint : 왼쪽 부분 배열에서 다시 찾습니다.

3.  k > pivotpoint : 오른쪽 부분 배열에서 다시 찾습니다.

 

시간 복잡도

최악의 경우: 모든 재귀 호출이  n-1  길이로 이어질 때,  O(n^2)  시간이 걸립니다.

평균의 경우: 대부분  n/2 로 분할되므로  O(n) 에 가깝습니다.

 

 

Median of Medians (Algorithm 8.6)

 

개선 방법

분할 과정에서  pivot 을 임의로 선택하지 않고, Median of Medians 방법을 사용합니다.

배열을 5개씩 나눠서 각 그룹의 중간값을 찾습니다.

이 중간값들의 중간값을 구합니다(즉, “중간값들의 중간값”).

이를 기준으로  pivot 을 설정합니다.

 

알고리즘 수행 방식

1. 배열을  n/5  그룹으로 나눕니다.

2. 각 그룹의 중간값을 계산합니다.

3. 중간값들의 배열에서 다시 k번째 중간값을 찾습니다.

4. 이 값을 기준으로 분할합니다.

 

시간 복잡도

최악의 경우에도  O(n) 을 보장합니다.

이유: 매 재귀 호출마다 배열 크기가  n/5 로 감소하기 때문입니다.

 

 

Probabilistic Selection Algorithm (Algorithm 8.7)

 

확률적 접근법

Sherwood 알고리즘: 분할 기준인  pivot 을 랜덤하게 선택합니다.

이 방식은 평균적으로  O(n)  시간에 동작합니다.

최악의 경우는  O(n^2) 이지만, 랜덤 선택으로 인해 발생 확률이 매우 낮습니다.

 

시간 복잡도 분석

기대값 계산을 통해  O(n) 임을 보입니다.

재귀적으로 기대값을 계산하며, 반복적으로 모든 가능성을 고려합니다.

'Book > Foundations Of Algorithms' 카테고리의 다른 글

CH7. Introduction to Computational Complexity: The Sorting Problem  (1) 2024.12.05
CH2_Exercise  (0) 2024.10.16
CH1.Exercise  (1) 2024.10.14