📚퀵 정렬

📄기본 개념

특정 데이터를 기준으로 배열을 두 개의 부분배열로 분할하고, 각 부분배열에 대해 퀵정렬을 순환적으로 적용하는 방식이다.


📄피벗

배열을 두 개의 부분배열로 분할할 때 기준이 되는 특정 데이터를 말한다.

보통 주어진 배열의 첫 번째 데이터를 피벗으로 지정한다.


📄원리

피벗이 제자리를 잡도록 하는 정렬 방식이다.

퀵 정렬을 수행하면 왼쪽 부분배열의 값들은 피벗보다 작고 오른쪽 부분배열의 값들은 피벗보다 크다.

피벗 위치를 분할함수로 결정하고 왼쪽 부분배열과 오른쪽 부분배열에 대해 퀵 정렬을 순환적으로 처리한다.


📄퀵 정렬 알고리듬

1
2
3
4
5
6
7
8
9
10
11
12
13
14
QuickSort(array[], n)
{
	if(n > 1)
	{
		// 피벗 기준으로 두 개의 부분배열로 분할
		// 피벗은 제자리를 잡은 피벗의 위치(인덱스)를 표시
		pivot = Partition(array[0,...,n-1], n);

		// 왼쪽 부분배열에 대한 퀵 정렬 순환 호출
		QuickSort(array[0,...,pivot-1], pivot);
		// 오른쪽 부분배열에 대한 퀵 정렬 순환 호출
		QuickSort(array[pivot+1,...,n-1], n-pivot-1)
	}
}


📄부분배열 분할 과정

피벗이 되는 배열의 첫 번째 원소를 제외하고 배열의 양 끝에서부터 중앙 쪽으로 동시에 검색하면서 각각 피벗보다 큰 데이터와 작은 데이터를 찾는다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
int Partition(array[], n)
{
	left = 1, right = n -1;

	while(left < right)
	{
		while(left < n && array[left] < array[0])
		{
			left++;
		}
		while(right > 0 && array[right] >= array[0])
		{
			right--;
		}

		if(left < right)
		{
			array[left] array[right] 위치 교환
		}
		else
		{
			피벗과 array[right] 위치 교환
		}
	}

	return right;
}


📄성능과 특징

  • 분할 함수 Partition()의 성능: Θ(n)

피벗과의 비교 횟수

각 데이터는 피벗과 1회 또는 많아야 2회씩 비교(비교 횟수는 데이터 크기에 비례)


  • 퀵 정렬 QuickSort()의 성능

퀵 정렬의 수행시간은 분할되는 두 부분배열의 크기에 따라 달라진다.

1
2
3
4
5
6
QuickSort(A[], n)
{
	pivot = Partition(A[0...n-1], n);		// Θ(n)
	QuickSort(A[0..pivot-1], pivot);		// T(nL)
	QuickSort(A[pivot+1..n-1], n-pivot-1);	// T(nR)
}

T(n) = T(nL) + T(nR) + Θ(n)

배열이 어떤 크기의 두 부분배열로 분할되는냐에 따라 Partition() 함수 호출 횟수가 달라지고, 퀵 정렬 전체 성능 T(n)을 결정한다. 퀵 정렬 시간 복잡도는 최악의 경우, 최선의 경우, 평균적인 경우로 구분해서 살펴봐야 한다.


  • 최악의 경우

배열이 항상 0:n-1 또는 n-1:0으로 분할되는 경우 -> 배열이 이미 정렬되어 있는 경우

피벗만 제자리를 잡고 나머지 모든 원소가 하나의 부분배열이 되는 경우이다.

피벗이 항상 부분배열의 최솟값 도는 최댓값이 되는 경우이다.

입력 데이터가 정렬된 경우와 피벗을 배열의 첫 번째 원소로 정한 경우 최악의 경우가 발생할 수 있다.

T(n) = T(n-1) + T(0) + Θ(n) (n > 1), T(1) = 1

T(n) = T(n-1) + Θ(n)

T(n) = O(n2)


  • 최선의 경우

배열이 항상 n/2:n/2로 분할되는 경우

피벗을 중심으로 항상 동일한 크기의 두 부분배열로 분할되는 경우

피벗이 항상 배열의 중간값이 되는 경우

T(n) = T(n/2) + T(n/2) + Θ(n)

T(n) = 2T(n/2) + Θ(n)

T(n) = O(nlogn)


  • 퀵 정렬의 평균 수행시간

부분배열의 모든 분할 비율에 따른 수행시간의 평균

피벗은 동일한 확률을 가지고 분할 후 배열의 어느 곳에나 위치 가능

T(n) = O(nlogn)


퀵 정렬은 피벗 선택의 임의성만 보장되면 평균 수행시간을 보장한다.

최악의 수행시간이 일어나는 경우는 피벗을 배열의 첫 번째 원소로 지정하는 경우와 배열이 정렬된 경우 일어난다. 주어진 배열이 정렬되어 있는지 아닌지 알 수 없기 때문에 우리는 피벗을 조절하여 최악의 수행시간이 일어나는 경우를 피할 수 있다.

배열에서 임의의 값을 선택한 후, 배열의 첫 번째 원소와 서로 교환하고 정렬을 수행하는 방식이다.


  • 제자리 정렬 알고리듬

입력 배열 이외에 추가적인 저장 공간을 상수 개만 사용한다.


  • 안정적이지 않은 정렬 알고리듬

  • 분할정복 방법이 적용된 알고리듬

분할: 피벗을 기준으로 주어진 배열을 두 부분배열로 분할 -> 두 부분배열의 크기는 일정하지 않다.

정복: 두 부분배열에 대해 퀵 정렬을 순환적으로 적용하여 각 부분배열을 정렬한다.

결합: 필요 없음



📚합병 정렬

📄기본 개념

주어진 배열을 동일한 크기의 두 부분배열로 분할하고, 각 부분배열에 순환적으로 합병 정렬을 적용하여 정렬시킨 후, 정렬된 두 부분배열을 합병하여 하나의 정렬된 배열을 만든다.


📄알고리듬

  • 합병 정렬 알고리듬
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
MergeSort(A[], n)
{
	if(n > 1)
	{
		// 배열 A[] 중앙 인덱스
		Mid = n/2;
		// 왼쪽 부분배열의 순환 호출 -> 크기 n/2인 정렬된 배열 B[] 반환
		B[0..Mid-1] = MergeSort(A[0..Mid-1], Mid);
		// 오른쪽 부분배열의 순환 호출 -> 크기 n/2인 정렬된 배열 C[] 반환
		C[0..n-Mid-1] = MergeSort(A[Mid..n-1], n-Mid);
		// 정렬된 두 부분배열 B[]와 C[]의 합병: A[] = B[] + C[]
		A[0..n-1] = Merge(B[0..Mid-1], C[0..n-Mid-1], Mid, n-Mid);
	}

	return(A);
}
  • 합병 함수
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
Merge(B[], C[], n, m)
{
	i = j = k = 0;
	while(i < n && j < m)
	{
		// 부분배열 B[]와 C[] 비교해서 작은 데이터 선택
		if(B[i] <= C[j])
		{
			A[k++] = B[i++];
		}
		else
		{
			A[k++] = C[j++];
		}
	}

	// 부분배열 B[]에 남아 있는 모든 데이터 A[]로 이동
	for(; i < n; i++)
	{
		A[k++] = B[i];
	}
	// 부분배열 C[]에 남아 있는 모든 데이터 A[]로 이동
	for(; j < m; j++)
	{
		A[k++] = C[j];
	}

	return(A[0..n+m+1])
}


📄성능과 특징

  • 합병 함수 Merge()의 수행시간: Θ(n)

두 부분배열 B[]와 C[]간의 비교 횟수(데이터 크기에 비례)


  • 합병 정렬 MergeSort()의 수행시간: O(nlogn)

T(n) = T(n/2) + T(n/2) + Θ(n) (n > 1), T(1) = 0

T(n) = 2T(n/2) + Θ(n)

T(n) = O(nlogn)


  • 안정적인 정렬 알고리듬

합병 과정에서 동일한 두 데이터에 대해 항상 왼쪽 데이터를 먼저 선택


  • 제자리 정렬 알고리듬이 아님

입력 크기 n만큼의 추가적인 공간을 요구


  • 분할정복 방법이 적용된 알고리듬

분할: 주어진 배열을 동일한 크기의 2개의 부분배열로 분할

정복: 각 부분배열에 대해 합병 정렬을 순환적으로 적용하여 정렬

결합: 정렬된 두 부분배열을 합병하여 하나의 정렬된 배열을 만듦


  • 비순환적 방식으로도 구현 가능

반복문을 사용한 비순환적 방식으로 구현 가능하다. 분할 과정 없이 단순하게 합병하는 과정만으로 정렬한다.



Leave a comment