# 3.1.2. Quick Sort

In 

# 3.1.2. Quick Sort

  • Pivot strategy

    1. Divide
    • Select a pivot element, and then divide the array into two subarrays such that ....
    1. Conquer
    • sort each subarray recursively.
    1. Combine
    • do nothing.

    image

  • A simple implementation

    // Sort a list from A[left] to A[right].
    // Should be optimized for higher efficiency!!!
    
    void **quick_sort**(item_type *A, int left, int right) { 
      int pivot;
      if (right - left > 0) {
        //divide
        pivot = partition(A, left, right);
        //conquer
        quick_sort(A, left, pivot - 1);
        quick_sort(A, pivot + 1, right); 
      }
    }
    #define SWAP(a, b) { 
      item_type tmp; 
      tmp = a; 
      a = b; 
      b = tmp; 
    }
    
    int partition(item_type *A, int left, int right) { 
      int i, pivot;
      pivot = left;
      for (i = left; i < right; i++) {
        if (A[i] < A[right]) {
          SWAP(A[i], A[pivot]);
          pivot++;
          //How is the pivot element chosen in this function?  
        } 
      }
      SWAP(A[right], A[pivot]);
      return(pivot);
    }

# Cost Analysis

  • Cost

    • T(n) = T(m_1) + T(m_2) + cn (m_1 + m_2 = n-1) if n>1
    • T(1) = 1
  • Analysis |Quick Sort ||| |---|---|---| |Divide| Conquer| Combine| |O(n)|T(m_1)+T(m_2)|O(1)|

  • Worst-case time complexity

    • 매 단계에서 선택한 pivot element가 가장 크거나 가장 작을 경우,
    • T(n) = T(0) + T(n-1) + cn, T(1) = 1 then T(n) = O(n^2)
    • Skewed vs well-balanced trees
  • Average-case time complexity

    • T(n) = \sum_{p=1}^n {T(p-1) + T(n-p)} + cn
    • T(0) = 1 \rightarrow
    • \therefore T(n) = O(n log n)

# 직관적인 시간 복잡도 추정

  • T(n) = T(m_1) + T(m_2) + cn (m_1 + m_2 = n-1) if n>1
  • T(1) = 1

image
image

# Average Case Time Complexity

# 첫 번째 사실

  • n \leq 0, \forall n \in \mathbb{Z}, T_{ave}(n)n 개의 원소를 가지는 배열을 퀵 정렬 방법을 사용하여 정렬하는데 걸리는 평균 수행시간이라고 하자. 그러먼 어떤 양의 정수 b와 c에 대해 다음과 같은 재귀 관계 존재

  • T_{ave} (n) \geq cn + \frac {1}{n} \sum_{p=1}^{n} \{ T_{ave} (p-1) + T_{ave} (n-p) \} \\= cn + \frac{2}{n} {\sum_{p=0}^{n-1} {T_{ave}(p)}} \forall n \geq 2

  • T_{ave} (1) \leq b

  • T_{ave} (0) \leq b

  • Cost_{ave} = \sum_{p=1}^n {P_r (p) \cdot Cost(p)} = \frac {1}{n} \sum_{p=1}^n {... + ...}

# 두 번째 사실

  • k=2(b+c) 라 할 때, 2보다 같거나 큰 모든 정수 n 에 대하여 T_{ave} (n) \leq kn \log_e n 과 같은 관계 존재

  • 증명: 위의 부등식을 수학적 귀납법을 사용하여 증명하자.

    1. n=2

      • 첫 번째 사실로부터 다음과 같은 관계 성립 \\ T_{ave}(2) \leq 2c + T_{ave} (0) + T_{ave} (1) \leq 2(b+c) \leq k \cdot 2 \ log_e 2
      • \therefore 따라서 두 번째 사실 성립
    2. 3보다 같거나 큰 임의의 n 이 given

      • Assume that : m<n 인 모든 m 에 대하여 두 번째 사실 성립한다고 가정하자.

      • 그러면 첫 번째 사실과 이 과정을 사용하여 다음과 같은 관계 유도 가능

      • T_{ave} (n) \leq cn + \frac {2} {n} \sum_{m=0}^{n-1} {T_{ave} (m)} \\ = cn + \frac 2 m \{ T_{ave} (0)+T_{ave} (1) \} + \frac {2} {n} \sum_{m=2}^{n-1} {T_{ave} (m)} \\ \leq cn + \frac {4b} n + \frac {2k} n \sum_{m=2}^{n-1} {m log_e m}

      • 그러므로 T_{ave} (n) \leq cn + \frac {2} {n} \sum_{p=0}^{n-1} {T_{ve} (p)} \forall n \geq 2

  • 함수 $x \log_e x$가 $x$에 대하여 아래로 볼록한 함수이어서 m log_e m \leq \int_m^{m+1} x \log_e x dx 라는 사실을 이용하면 다음과 같은 관계식을 얻는다.

    • T_{ave} (n)= cn + \frac {4b}n + \frac {2k}n \int_2^n x log_e x dx \\ \leq cn + \frac {4b}{n} + \frac{2k}{n} \{ \frac{n^2 log_e n}{2} - \frac {n^2} 4 \} \\= knlog_e n + \{ cn + \frac{4b} n - \frac {kn} 2\}

    $\int_2n x log_e x dx =[\frac 1 2 x2 log_e x - \frac {x2} 4]_2n $

    = (\frac {n^2} 2) log_e n - \frac {n^2} 4 - (2log_e 2 - 1) \leq \frac {n^2} {2} {log_e n} - \frac {n^2} {4}

  • 이 때, $ cn + \frac{4b} n - \frac 2 = (c-\frac k 2 )n + \frac {4b} n = b(\frac 4 n -n)$ 과 같고 이 값은 2보다 같거나 큰 n에 대해 항상 0보다 같거나 작으므로 T_{ave} (n) \leq kn log_e n 이 되어 3보다 같거나 큰 임의의 n에 대해서도 두 번째 사실이 성립한다. 따라서 2보다 같거나 큰 모든 정수 n에 대해 다음과 같은 두 번째 사실이 성립한다.

# Anther Implementation

void quicksort (element list[ ], int left, int right){
// list[left], …, list[right]까지 오름차순으로 정렬.
// list[left].key를 중추 키(pivot key)로 선정
// list[left].key ≤ list[right + 1].key 라고 가정
    int pivot, i, j;
    element temp;
    if (left < right) {
        i = left; j = right + 1;
        pivot = list[left].key;
        do {
        // pivot을 중심으로 왼쪽과 오른쪽 리스트 생성
        // 왼쪽 리스트: pivot보다 적은 키들을 저장, 오른쪽은 반대
            do // 왼쪽부터 pivot보다 큰 키를 검색
                i++;
            while (list[i].key < pivot);
            do // 오른쪽부터 pivot보다 작은 키를 검색
                j--;
            while (list[j].key > pivot);
            if ( i < j ) // 각 리스트의 속성을 만족하도록 데이터 교환
                SWAP( list[i], list[j], temp );
        } while ( i < j );
        SWAP( list[left], list[j], temp );
        quicksort( list, left, j – 1 ); // 왼쪽 리스트를 다시 정렬
        quicksort( list, j + 1, right ); // 오른쪽 리스트를 다시 정렬
    }
}

# Improving the Performance of Quick Sort

  • How can you select a “good” pivot element?

    • Choose a random element in the list.
    • Choose the median of the first, middle, and final elements in the list.
    • Choose the median of the entire elements in the list. (bad idea)
    • Etc.
  • Program 7.4. improved quicksort

    • Choosing the median of the first, middle, and final elements as the partitioning element and cutting off the recursion for small subfiles can significantly improve the performance of quicksort.

    • This implementation partitions on the median of the first, middle, and final elements in the array (otherwise leaving these elements out of the partitioning process).

    • Files of size 11 or smaller are ignored during partitioning; then, insertion from is used to finish the sort.

      #define M
      
      void quicksort(ITEM[] a, int l, int r)
      {
          if (r - l <= M)
              return;
          exch(a, (l + r) / 2, r - 1);
          compExch(a, l, r - 1);
          compExch(a, l, r);
          compExch(a, r - 1, r);
          int i = partition(a, l + 1, r - 1);
          quicksort(a, l, i - 1);
          quicksort(a, i + 1, r);
      }
      
      void sort(ITEM a[], int l, int r)
      {
          quicksort(a, l, r);
          insertion(a, l, r);
      }
  • How can you minimize the bookkeeping cost involved in the recursive calls?

    • Much of the pushing and popping of the frame stack is unnecessary.

    • Lists of size smaller than M are ignored during quick sort, then do a single sorting pass at the end.

    • Avoid making the recursive call on the larger subrange.  The depth of recursion <= O(log n)

      quicksortTRO(E, first, last)
      {
        int first1, last1, first2, last2;
        first2 = first;
        last2 = last;
        while (last2 - first2 > 1)
        {
      
            pivotElement = E[first];
            pivot = pivotElement.key;
            int splitPoint = partition(E, pivot, first2, last2);
            E[splitPoint] = pivotElement;
            if (splitPoint < (first2 + last2) / 2)
            {
                first1 = first2;
                last1 = splitPoint - 1;
                first2 = splitPoint + 1;
                last2 = last2;
            }
            else
            {
                first1 = splitPoint + 1;
                last1 = last2;
                first2 = first2;
                last2 = splitPoint - 1;
            }
            quicksortTro(E, first1, last1); 
            // continue loop for fist2, last2.
        }
        return;
      }

# Example: Quick Sort

  • By courtesy of David R. Musser

  • Average-case:O(n log n)

  • Worst-case: O(n^2)

    image

# Quicksort: Implementation 2 [K. Loudon]

#include <stdlib.h>
#include <string.h>
#include "sort.h"

static int compare_int(const void *int1, const void *int2){
    // Compare two integers (used during median-of-
    // three partitioning
    if (*(const int *)int1 > *(const int *)int2)
        return 1;

    else if (*(const int *)int1 < *(const int *)int2)
        return -1;

    else
        return 0;
}

static int partition(void *data, int esize, int i, int k,int (*compare)(const void *key1, const void *key2)){
    char *a = data;
    void *pval, *temp;
    int r[3];
    /*  Allocate storage for the partition value and swapping. */
    if ((pval = malloc(esize)) == NULL)
        return -1;
    if ((temp = malloc(esize)) == NULL)
    {
        free(pval);
        return -1;
    }
    /* Use the median-of-three method to find the partition value.  */
    r[0] = (rand() % (k - i + 1)) + i;
    r[1] = (rand() % (k - i + 1)) + i;
    r[2] = (rand() % (k - i + 1)) + i;
    issort(r, 3, sizeof(int), compare_int);
    memcpy(pval, &a[r[1] * esize], esize);
    /* Create two partitions around the partition   value.  */
    i--;
    k++;
    while (1)    {
        /* Move left until an element is found in the wrong partition. */
        *do { k--; }
        while (compare(&a[k * esize], pval) > 0);
        /* Move right until an element is found in the wrong partition. */
        do{
            i++;
        }while (compare(&a[i * esize], pval) < 0);

        if (i >= k){
            break;
        }
        /* Stop partitioning when the left and right counters cross. */
        else{
            /* Swap the elements now under the left and   right counters.  */
            memcpy(temp, &a[i * esize], esize);
            memcpy(&a[i * esize], &a[k * esize], esize);
            memcpy(&a[k * esize], temp, esize);
        }
    }

    /* Free the storage allocated for
     partitioning. */

    free(pval);
    free(temp);
    /* Return the position dividing the two partitions. */

    return k;
}

int qksort(void *data, int size, int esize, int i, int k, int (*compare)(const void *key1, const void *key2)){
    int j;
    /* Stop the recursion when it is not possible
     to partition further. */
    if (i < k)
    {
        // Determine where to partition the elements
        if ((j = partition(data, esize, i, k,
                           compare)) < 0)
            return -1;
        // Recursively sort the left partition
        if (qksort(data, size, esize, i, j, compare)< 0)
          return -1;

        // Recursively sort the right partition
        if (qksort(data, size, esize, j + 1, k, compare) < 0)
            return -1;
    }

    return 0;
}