[アルゴリズム]クイックソート(C)


この授業では、クイックソートについて説明します.

クイックソートアルゴリズムの概要


Charles Antony Richard Hoareが開発した並べ替えアルゴリズム
  • クイックソートは不安定ソートに属し、他の要素との比較のみでソートされる比較ソートに属する.
  • 分割征服アルゴリズムの1つであり、平均速度が非常に速いソート方法
  • マージソート(merge sort)とは異なり、クイックソートはリストを不均一に分割する.
  • 分割征服方法


    問題を二つの小さな問題に分けて、それぞれ解決し、結果を元の問題を解決する策略に集中する.
    分割征服法は、通常、ループコールを使用して実装される.

    コースの説明


    リスト
  • の要素を選択します.この均一な要素を軸心(pivot)と呼ぶ.
  • ピボットを基準として、ピボットより小さい要素はすべてピボットの左側に移動し、ピボットより大きい要素はすべてピボットの右側に移動します.
    (ピボット中心の左側:ピボットより小さい要素、右側:ピボットより大きい要素)
  • .
  • ピボット以外の左側のリストと右側のリストを並べ替えます.分割された部分リストに対して、ループコールを使用して並べ替えを繰り返します.
  • 部分リストで、再度ピボットを決定し、ピボットを基準として2つの部分リストに分割するプロセスを繰り返す.一部のリストが分割できなくなるまで繰り返します.リストサイズが0または1になるまで、
  • を繰り返します.

    高速ソートアルゴリズムの具体的な概念


  • 1つのリストをピボット(pivot)を基準に2つの不均一なサイズに分割し、分割された部分リストをソートし、2つのソートされた部分リストをマージしてリスト全体をソートする方法です.
    クイックソートは、次の手順で構成されます.
  • 分割:入力配列を軸心を基準とし、2つの部分配列(軸心を中心とし、左:軸心より小さい要素、右:軸心より大きい要素)に不均一に分割します.
  • 征服(Conquer):部分配列をソートします.部分配列のサイズが小さくない場合は,ループ呼び出しを用いて分割征服法を再適用する.
  • マージ:整列した配列の一部を1つの配列にマージします.
    ループ呼び出しを行うたびに、少なくとも1つの要素(ピボット)が最終的に位置を決定するので、アルゴリズムが必ず終了することを保証できます.
  • アルゴリズムの例

  • アレイに5、3、8、4、9、1、6、2、7が格納されていると仮定し、昇順にデータをソートします.
  • クイックソートでは、ピボットによって2つのリスト(C言語コードのパーティション関数の内容)
  • に分けられる.

  • ピボット値を入力リストの最初のデータとして使用します.(他の任意の値も構いません.)
    2つのインデックス変数(low,high)を使用して、リストを2つの部分リストに分けます.
    1回転:ピボットが5の場合、
  • lowを左から右にナビゲートし、ピボットより大きいデータ(8)を見つけたら停止します.
    highは右から左にナビゲートし、軸心より小さいデータ(2)を見つけたら停止します.
    lowとhighが指す2つのデータは互いに交換される.
    この探索−交換過程はlowとhighが交差するまで繰り返される.

  • 2回転:ピボット(1回転の左側のリストの最初のデータ)は1です.
    上記の方法を繰り返します.

  • 3回転:ピボット(1回転右リストの最初のデータ)は9です.
    上記の方法を繰り返します.
  • <コード>

    # include <stdio.h>
    # define MAX_SIZE 9
    # define SWAP(x, y, temp) ( (temp)=(x), (x)=(y), (y)=(temp) )
    
    // 1. 피벗을 기준으로 2개의 부분 리스트로 나눈다.
    // 2. 피벗보다 작은 값은 모두 왼쪽 부분 리스트로, 큰 값은 오른쪽 부분 리스트로 옮긴다.
    /* 2개의 비균등 배열 list[left...pivot-1]와 list[pivot+1...right]의 합병 과정 */
    /* (실제로 숫자들이 정렬되는 과정) */
    int partition(int list[], int left, int right) {
        int pivot, temp;
        int low, high;
    
        low = left;
        high = right + 1;
        pivot = list[left]; // 정렬할 리스트의 가장 왼쪽 데이터를 피벗으로 선택(임의의 값을 피벗으로 선택)
    
        /* low와 high가 교차할 때까지 반복(low<high) */
        do {
            /* list[low]가 피벗보다 작으면 계속 low를 증가 */
            do {
                low++; // low는 left+1 에서 시작
            } while (low <= right && list[low] < pivot);
    
            /* list[high]가 피벗보다 크면 계속 high를 감소 */
            do {
                high--; //high는 right 에서 시작
            } while (high >= left && list[high] > pivot);
    
            // 만약 low와 high가 교차하지 않았으면 list[low]를 list[high] 교환
            if (low < high) {
                SWAP(list[low], list[high], temp);
            }
        } while (low < high);
    
        // low와 high가 교차했으면 반복문을 빠져나와 list[left]와 list[high]를 교환
        SWAP(list[left], list[high], temp);
    
        // 피벗의 위치인 high를 반환
        return high;
    }
    
    // 퀵 정렬
    void quick_sort(int list[], int left, int right) {
    
        /* 정렬할 범위가 2개 이상의 데이터이면(리스트의 크기가 0이나 1이 아니면) */
        if (left < right) {
            // partition 함수를 호출하여 피벗을 기준으로 리스트를 비균등 분할 -분할(Divide)
            int q = partition(list, left, right); // q: 피벗의 위치
    
            // 피벗은 제외한 2개의 부분 리스트를 대상으로 순환 호출
            quick_sort(list, left, q - 1); // (left ~ 피벗 바로 앞) 앞쪽 부분 리스트 정렬 -정복(Conquer)
            quick_sort(list, q + 1, right); // (피벗 바로 뒤 ~ right) 뒤쪽 부분 리스트 정렬 -정복(Conquer)
        }
    
    }
    
    void main() {
        int i;
        int n = MAX_SIZE;
        int list[10] = { 5, 3, 8, 4, 9, 1, 6, 2, 7 };
    
        // 퀵 정렬 수행(left: 배열의 시작 = 0, right: 배열의 끝 = 8)
        quick_sort(list, 0, n - 1);
    
        // 정렬 결과 출력
        for (i = 0; i < n; i++) {
            printf("%d\n", list[i]);
        }
    }

    高速ソートアルゴリズムの特徴


    長所

  • は速度が速い.
    時間複雑度O(nloḡn)を有する他の並べ替えアルゴリズムと比較しても最も速い.
    余分なメモリ容量は必要ありません.
  • クイックソートにはO(logn)と同じメモリが必要です。


    短所


    並べ替えられたリストの場合、高速並べ替えの不均衡分割はかえって実行時間を増加させる.
    クイックソートの不均衡分割を防止するため、ピボットを選択する際に、リストをより均一に分割できるデータを選択する.
    EX)リスト内のいくつかのデータにおいて,中心値(medium)を大きさ順に選択する.

    高速ソートの時間的複雑さ


    最良の場合


    比較回数

    1.ループ呼び出しの深さ

  • 記録された個数nが2の繰返し二乗(n=2^k)であると仮定すると、n=2^3の場合、ループ呼び出しの深さが3であることを示す2^3->2^2->2^1->2^0の順に減少する.これを一般化すれば,n=2^kの場合はk(k=logヤングn)であることが分かる.

  • k=log₂n

  • 各ループ呼び出しステップの比較演算
    -各ループ呼び出しは、リスト全体のレコードの大部分を比較する必要があるため、平均n回の比較が行われる.
    -平均n回

  • ループ呼び出しの深さ*ループ呼び出しステップごとの比較演算=nloḡn

  • 移動回数
    ->比較回数が少ないので無視できます.

  • 最適な場合T(n)=O(nloḡn)
  • 最悪の場合


    リストがバランスを崩し続ける場合(特にソートされたリストのクイックソート)
    比較回数
  • ループ呼び出し深さ
    ->レコードの個数nが2の繰返し二乗(n=2^k)であると仮定すると,ループ呼び出しの深さはnとなる.
    ->n(ループ呼び出しの深さ)
  • 各ループ呼び出しステップの比較演算
    ->各ループ呼び出しは、リスト全体のレコードの大部分を比較する必要があるため、平均n回の比較を行います.
    ->平均n回
  • ループ呼び出しの深さ*ループ呼び出しステップ毎の比較演算=n^2
  • 移動回数
    ->比較回数が少ないので無視できます.
    ->最悪の場合、T(n)=O(n^2)
  • 平均

  • ->平均T(n)=O(nloḡn)
    ->O(nlog
    ->高速ソートは、不要なデータ移動と遠隔データ交換を削減するだけでなく、1回の決定されたピボットを後続の演算から除外することもできます.
  • 단순(구현 간단)하지만 비효율적인 방법
    1.삽입 정렬, 
    2.선택 정렬, 
    3.버블 정렬
    
    복잡하지만 효율적인 방법
    1. 퀵 정렬,
    2. 힙 정렬,
    3. 합병 정렬,
    4. 기수 정렬