코딩테스트[파이썬]/이것이 코딩테스트다(이코테)

[정렬] - 퀵정렬

softmoca__ 2024. 1. 25. 23:19
목차

퀵정렬

- 병합 알고리즘과 더불어 대부분의 라이브러리의 근간이 되는 가장 많이 사용되는 알고리즘이다.

-  기준(피벗(Pivot)) 설정한 다음 큰 수와 작은 수를 교환한 후 리스트를 반으로 나누는 방식으로 동작한다

- 피벗을 설정하고 리스트를 분할하는 방법에 따라서 여러가지 방식으로 퀵 정렬을 구분하는데, 가장 대표적인 분할 방식인 호어 분할(Hoare Partition) 방식(리스트의 첫 데이터를 피벗으로 설정)을 기준으로 퀵 정렬을 이해해 보자.

 

피벗을 설정한 후 왼쪽에서부터 피벗보다 큰 데이터를 찾고, 오른쪽에서부터 피벗보다 작은 데이터를 찾는다

그 다음 큰 데이터와 작은 데이터의 위치를 서로 교환한다

==> 위 과정을 반복하면 피벗에 대하여 정렬이 수행

 

 

이해의 편의를 위해 크게 3개의 파트로 나누어 보자 ~

 

PART 1

STEP 0) 리스트의 첫 번째 데이터를 피벗으로 설정하므로 피벗은 5이다.

이후에 왼쪽에서부터 5보다 큰 데이터를 선택하므로 7이 선택되고, 오른쪽에서부터 5보다 작은 데이터를 선택하므로 4가 선택된다.

이제 이 두 데이터의 위치를 바꾼다.[7과 4 변경]

 

 

STEP 1) 그 다음 다시 피벗(5)보다 큰 데이터와 작은 데이터를 각각 찾는다. 찾은 뒤에는 두 값의 위치를 서로 변경하는데, 현재 9와 2가 선택되었으므로 이 두 데이터의 위치를 변경한다 [9와2 변경]

 

 

STEP 2) 그 다음 다시 피벗보다 큰 데이터와 작은 데이터를 찾는다.

단 , 현재 왼쪽에서부터 찾는 값과 오른쪽에서부터 찾는 값의 위치가 서로 엇갈린 것을 알 수 있다.

이렇게 두 값이 엇갈린 경우에는 '작은 데이터'와 '피벗'의 위치를 서로 변경하여 분할을 수행한다 -> [1과 5 변경]

 

 

STEP 3) 분할 완료 ! 이와 같이 피벗이 이동한 상태에서 왼쪽 리스트와 오른쪽 리스트를 살펴보자.

이제 5의 왼쪽에 있는 데이터는 모두 5보다 작고, 오른쪽에 있는 데이터는 모두 5보다 크다는 특징이 있다!

이렇게 피벗의 왼쪽에는 피벗보다 작은 데이터가 위치하고, 피벗의 오른쪽에는 피벗보다 큰 데이터가 위치히다록 작업을 분할(Divide) 또는 파티션(Partition)이라고 한다

 

이 상태에서 왼쪽 리스트오른쪽 리스트개별적으로 정렬시킬 것이다

왼쪽 리스트는 어떻게 정렬되어도 모든 데이터가 5보다 작고, 오른쪽 리스트는 어떻게 정렬되어도 모든 데이터가 5보다 크다

따라서 왼쪽 리스트와 오른쪽 리스트에서도 각각 피벗을 설정하여 동일한 방식으로 정렬을 수행하면 전체 리스트에 대하여 모두 정렬이 이루어질 것이다

 

 

 

PART 2

 

 

왼쪽 리스트에서는 위의 그림과 같이 정렬이 진행되며 구체적인 정렬 과정은 이전과 동일하다

 

 

 

 

 

PART 3

 

오른쪽 리스트에서는 위의 그림과 같이 정렬이 진행되며 구체적인 정렬 과정은 이전과 동일하다]

 

재귀함수와 동작 원리가 같다고 생각하면 된다

퀵 정렬은 재귀 함수 형태로 작성했을 때 구현이 매우 간결해진다

그렇다면 종료 조건은 언제 일까? ==>현재 리스트의 데이터 개수가 1개인 경우 동작을 멈춘다

리스트 원소가 1개라면 이미 정렬이 된 것과 같으며, 분할이 불가능하다

즉, 최종적으로 아래와 같은 과정을 거쳐 정렬이 되게 된다.

왼쪽의 필기는  1 4 2 0 3을 퀵정렬 한느 방법이다.

 

 

 

파이썬으로  직관적인 구현한 퀵정렬

array = [5, 7, 9, 0, 3, 1, 6, 2, 4, 8]

def quick_sort(array, start, end):
    if start >= end: # 원소가 1개인 경우 종료
        return
    pivot = start # 피벗은 첫 번째 원소
    left = start + 1
    right = end
    while(left <= right):
        # 피벗보다 큰 데이터를 찾을 때까지 반복 
        while(left <= end and array[left] <= array[pivot]):
            left += 1
        # 피벗보다 작은 데이터를 찾을 때까지 반복
        while(right > start and array[right] >= array[pivot]):
            right -= 1
        if(left > right): # 엇갈렸다면 작은 데이터와 피벗을 교체
            array[right], array[pivot] = array[pivot], array[right]
        else: # 엇갈리지 않았다면 작은 데이터와 큰 데이터를 교체
            array[left], array[right] = array[right], array[left]
    # 분할 이후 왼쪽 부분과 오른쪽 부분에서 각각 정렬 수행
    quick_sort(array, start, right - 1)
    quick_sort(array, right + 1, end)

quick_sort(array, 0, len(array) - 1)
print(array)

 

 

파이썬의 장점을 살려 구현한 퀵정렬

피벗과 데이터를 비교하는 연산 횟수가 증가하므로 시간이 쪼금 더 걸리지만 직관적이고 기억하기 쉽다.

array = [5, 7, 9, 0, 3, 1, 6, 2, 4, 8]

def quick_sort(array):
    # 리스트가 하나 이하의 원소만을 담고 있다면 종료
    if len(array) <= 1:
        return array

    pivot = array[0] # 피벗은 첫 번째 원소
    tail = array[1:] # 피벗을 제외한 리스트

    left_side = [x for x in tail if x <= pivot] # 분할된 왼쪽 부분
    right_side = [x for x in tail if x > pivot] # 분할된 오른쪽 부분

    # 분할 이후 왼쪽 부분과 오른쪽 부분에서 각각 정렬을 수행하고, 전체 리스트를 반환
    return quick_sort(left_side) + [pivot] + quick_sort(right_side)

print(quick_sort(array))

 

 

퀵 정렬의 시간 복잡도

선택정렬과 삽입정렬의 시간복잡도는 O(N^2)였다.

선택정렬과 삽입정렬은 최악의 경우에도 항상 시간복잡도 O(N^2)을 보장한다. 

퀵정렬의 평균시간복잡도는 O(NogN)이다.

앞서다루었던두정렬알고리즘에비해매우 빠른편이다.

최악의 경우시간복잡도는 O(N^2)이다.(무작위로 입력되는 빠르지만 이미 정렬되있을 경우 매우 느리다)

즉, 삽입정렬과 반대이다 !

+ 하지만 실제 라이브러리에는 추가적인 로직으로 인해 최악의경우에도 O(NlogN)이 보장되니 걱정 없이 사용하면 된다.

 

 

 

 

'코딩테스트[파이썬] > 이것이 코딩테스트다(이코테)' 카테고리의 다른 글

[정렬] -위에서 아래로  (0) 2024.01.26
[정렬] -계수정렬  (2) 2024.01.26
[정렬] - 삽입정렬  (0) 2024.01.25
[정렬] 선택정렬  (0) 2024.01.25
[DFS] -미로탈출  (1) 2024.01.25