ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 파이썬, 빠른 정렬(퀵 소트, Quick Sort)
    Python/파이썬 자료구조 알고리듬 2020. 10. 28. 18:37
    반응형

    호어 선생님이 만드신 빠른 정렬~!

    분할 정복 알고리듬을 이용합니다. 
    (머지 소트도 분할 정복이죠.)

    불안정 정렬입니다. 
    ([A1, A2]를 'A를 기준'으로 '숫자를 무시'하고 정렬했을 때 
    A1, A2의 순서가 바뀔 수 있습니다.)

    [A, B, A, C, A]를 정렬하면 [A, A, A, B, C]가 되는데.. 
    A, A, A 간 순서가 어떻게 되든 무슨 상관이냐 라고 생각할 수 있는데..

    [A1, B2, A2, C1, A3]의 경우
    두 번째 숫자를 기준으로 정렬한 뒤, 
    [A1, C1, B2, A2, A3]를
    첫 번째 문자를 기준으로 한 번 더 정렬해서,
    [A1, A2, A3, B2, C1]를
    만들어야 할 경우도 있습니다. 

    (스프레드 시트 작업에서 많이 씁니다. )

    불안정 정렬로는 이런 작업이 어렵습니다. ~!

     

    이 분이 영국 남자 호어 선생님

     

    설명을 위해 직접 그림을 그리려다가...
    그냥 구글링 했습니다. 

    그림을 잘 그려놓으셨습니다. 
    한 번에 이해가... ~!

    좋아요 클릭해드렸습니다. 

    https://coderkoo.tistory.com/7 

     

    퀵소트(quick sort) 알고리즘

    퀵소트(quick sort) 알고리즘 정렬 알고리즘 중 평균적으로 O(NlogN)으로 알려져 있는 Quick sort에 대해 알아보자. 1. 기본 아이디어 기본적으로 O(N^2)으로 정렬하는 알고리즘(Ex : 버블정렬)은 바꾸는 기�

    coderkoo.tistory.com

     

    자료를 수집하러 위키백과에 들어갔다가
    퀵 소트를 구현한 파이썬 코드를 봤습니다.
    파이썬스럽고 '재미있는' 코드라는 생각이 들었습니다.

    그런데 위키백과의 의사(pseudo) 코드를 보니 이 코드랑 똑같네요!
    제 가방끈이 짧아서 원 알고리듬도 못 알아본 것이었... ㅠ,.ㅠ

    # https://ko.wikipedia.org/wiki/%ED%80%B5_%EC%A0%95%EB%A0%AC#%ED%8C%8C%EC%9D%B4%EC%8D%AC
    
    def quick_sort1(array):
        if len(array) <= 1:
            return array
        pivot = array[len(array) // 2]
        less, more, equal = [], [], []
        for each in array:
            if each < pivot:
                less.append(each)
            elif each > pivot:
                more.append(each)
            else:
                equal.append(each)
        return quick_sort1(less) + equal + quick_sort1(more)
    
    
    print(quick_sort1([51, 20, 72, 63, 32, 75, 2, 88, 16, 29]))
    
    """
    [2, 16, 20, 29, 32, 51, 63, 72, 75, 88]
    """

    메모리를 절약하도록 수정해 보았습니다.
    (파이썬 GC가 알아서 처리하려나...)

    def quick_sort2(array):
        length = len(array)
        if length <= 1:
            return array
        pivot = array[length // 2]
        less, more, equal = [], [], []
        for each in array:
            if each < pivot:
                less.append(each)
            elif each > pivot:
                more.append(each)
            else:
                equal.append(each)
        del array  #
        return quick_sort2(less) + equal + quick_sort2(more)

     

    리스트를 만들고, append 하는 데도 비용이 발생합니다. 
    리스트를 하나만 유지하고, 요소의 순서를 바꿔줍니다. 
    재귀 함수에는 배열의 '참조'와
    배열에서 처리해야 할 부분의 시작과 끝의 '위치'만
    넘겨줍니다.
     

    이렇게 코딩하면 더 빠를 줄 알았지만...

    파이썬에서 배열은 기본적으로 참조(by reference)입니다.   

    pivot의 위치를 시작, 끝, 랜덤으로 지정하는 경우도 많습니다.

    # https://ko.wikipedia.org/wiki/%ED%80%B5_%EC%A0%95%EB%A0%AC#%ED%8C%8C%EC%9D%B4%EC%8D%AC
    
    def partition(arr, start, end):
        pivot = arr[start]
        left = start + 1
        right = end
        done = False
        while not done:
            while left <= right and arr[left] <= pivot:
                left += 1
            while left <= right and pivot <= arr[right]:
                right -= 1
            if right < left:
                done = True
            else:
                arr[left], arr[right] = arr[right], arr[left]
        arr[start], arr[right] = arr[right], arr[start]
        return right
    
    
    def quick_sort3(arr, start, end):
        if start < end:
            pivot = partition(arr, start, end)
            quick_sort3(arr, start, pivot - 1)
            quick_sort3(arr, pivot + 1, end)
        return arr
    
    
    print(quick_sort3([51, 20, 72, 63, 32, 75, 2, 88, 16, 29], 0, 9))

     

    조금 더 파이썬스럽게 중첩함수(함수 내 함수)를 이용한다면... 

    def quick_sort3(arr):
        def partition(start, end):
            pivot = arr[start]
            left = start + 1
            right = end
            done = False
            while not done:
                while left <= right and arr[left] <= pivot:
                    left += 1
                while left <= right and pivot <= arr[right]:
                    right -= 1
                if right < left:
                    done = True
                else:
                    arr[left], arr[right] = arr[right], arr[left]
            arr[start], arr[right] = arr[right], arr[start]
            return right
    
        def quick_sort(start, end):
            if start < end:
                pivot = partition(start, end)
                quick_sort(start, pivot - 1)
                quick_sort(pivot + 1, end)
    
        quick_sort(0, len(arr) - 1)
        return arr

     

    2가지 경우만 이해해 봅시다.

     p
    [6, 3, 1, (L)9, (R)2]

     

    Left는 pivot 다음 요소부터 'pivot보다 작으면 진행' 
    pivot보다 큰 요소(9)를 발견하면 3에 멈춥니다.

    Right는 마지막 요소부터 'pivot보다 크면 진행'
    pivot보다 작은 요소(2)를 발견하면 4에 멈춥니다. 

     

    멈추면 피봇 보다 큰 것 작은 것이 하나씩 나왔으니까 
    둘(right와 left)을 교환합니다.

     p
    [6, 3, 1, 2, 9]

     

    left와 right가 만날 때까지 위 과정을 반복합니다.
    (위 예시는 반복할 것이 없습니다.)

     

    마지막으로 start(=0)와 right(=3)을 교환하고 pivot = right.  

              p
    [2, 3, 1, 6, 9]

    퀵 소트의 조건에 맞게 정리되었습니다. 

     

    만약에 한 방향으로만 진행된다면... 

     p
    [6, 3, 1, 4, 5]

    left=4이고 right=4가 됩니다.  

     

    마지막 과정인 right와 pivot을 교환하는 과정을 거치면 다음과 같죠...

                 p
    [5, 3, 1, 4, 6]

    마찬가지로 퀵 소트의 조건에 맞게 정리되었습니다. 

     

    시간 테스트를 해 보았습니다. 

    from random import randint
    import time
    
    
    def check_time(f, *args):
        start_time = time.time()
        f(*args)
        end_time = time.time()
        print(f"{f.__name__}: {end_time - start_time}")
    
    
    nums = [randint(0, 100000) for _ in range(100000)]
    
    check_time(quick_sort1, nums[:])
    check_time(quick_sort2, nums[:])
    check_time(quick_sort3, nums[:])

    파이썬은 함수를 인자(매개변수 f)로 사용할 수 있습니다. 
    함수의 이름을 알기 위해 f.__name__을 사용했습니다. 
    quick_sort4 때문에 가변인자를 받기 위해 *args를 사용했습니다. 
    파이썬의 리스트는 참조입니다.
    리스트를 복사하기 위해 nums[:]를 사용했습니다. 
    nums.copy() 도 됩니다. 

    실행할 때 마다 약간씩 달라집니다만...
    경향성은 파악할 수 있습니다. 

    quick_sort1: 0.2385568618774414
    quick_sort2: 0.240553617477417
    quick_sort3: 0.29744744300842285

    제 예상과 달리, 파이썬에서는 세 번째 코드가 느렸습니다. 1, 2번 코드는 비슷했습니다. 엎치락 뒷치락...

     

    반응형
Designed by Tistory.