クイックソート


クイックソートの概要

  • クイックソート
    各グループにピボットを設定し、そのピボットを基準値以上/以下の2つのグループに分けてソートします.

  • 平均時間複雑度の計算
    図中の一般例のように、必要なループ呼び出し深さ(分割レベル)lognのため、要素の個数はNであるため、平均時間複雑度はO(Nlogn)である.したがって、非常に高速なソートアルゴリズムとしてよく使用されています.しかし,距離の遠いデータ交換(swap)のため,ソートは不安定である.

  • クイックソートの利点
    (1)最悪の条件以外は,通常非常に速いアルゴリズムである.

  • クイックソートの欠点
    (1)交換したリストを不安定に並べ替える
    (2)リストがソートされている場合、ソートに時間がかかる
  • 最悪の場合は
  • です

  • 最悪の場合の時間的複雑さ
    例に示すように、配列が整列し、ピボットが最初の要素(最小値)である場合、左側の要素は1つに分割され、右側の要素の個数は毎回n-1つに分割されます.したがって,ループ呼び出し深さ(分割レベル)はn−1を必要とし,要素の個数はnであるため,時間複雑度はO(n^2)である.

  • 整理する
  • 平均時間複雑度:O(nlogn)
  • 時間の複雑度が最も低い:O(n^2)
  • 空間複雑度:O(n)
  • 分割プロセス(partition)

  • a[pl]>=x plを右に移動し、成立した要素が見つかるまで

  • prを左に移動して、成立した要素a[pr]<=xが見つかるまで


  • 上記の要素(swap)を同じ位置で停止して交換する


  • 繰り返しスキャンとスワップ後の交点


  • plとprが交差すると,パケットプロセスが終了し,配列は2つのグループに分けられる.

  • 征服過程.
    分割プロセスを繰り返します.
  • 高速Sortアルゴリズムコード
  • #include <stdio.h>
    #include <stdlib.h>
    
    #define swap(type, x, y) do { type t = x; x = y; y = t;} while(0) 
    
    void quick_sort(int a[], int left, int right) {
    	int pl = left; // left pivot
    	int pr = right; // right pivot 
    	int x = a[(pl + pr) / 2]; // 중앙 요소 값
    
    	/* 분할(divide) */
    	do {
    		// 왼쪽피봇값과 오른쪽 피봇값을 서로 교환해야함(작은 값은 왼쪽으로 큰 값은 오른쪽으로)
    		while (a[pl] < x) pl++; // 왼쪽 피봇값이 중앙 요소값보다 작을때
    		while (a[pr] > x) pr--; // 오른쪽 피봇값이 중앙 요소값보다 클때
    		if (pl <= pr) { // pl이 pr보다 작을경우나 같을 경우 둘다 이동시켜야함(같을 경우도 값 교환됨)
    			swap(int, a[pl], a[pr]); // 교환
    			pl++; 
    			pr--;
    		}
    	} while (pl <= pr); // pl과 pr 교차시 종료(두 그룹으로 분할 마침)
    	
    	/* 정복(conquer) */
    	if (left < pr) quick(a, left, pr);  // pr이 right 값이 됨
    	if (right > pl) quick(a, pl, right); // pl이 left 값이 됨
    }
    要素の数が1のグループはグループ化する必要はありません.従って、アルゴリズムの再帰呼び出し部分からleftplが見られる.これは,要素数が2の場合のみグループ化するためである.
  • 校正コール(征服)プロセス
  • prがa[0]の右側(左は、
  • plがa[n-1]の左側にある場合(right>pl)、右側パケット
  • 非再帰的実装
    スタックにプッシュする値は、グループ(配列)の最初の要素と最後の要素のインデックスです.グループが完了すると、左側のグループに対応する範囲インデックスと右側のグループに対応する範囲インデックスにスクロールします.次にスタックでpop範囲の操作を繰り返します.
  • 高速ソート非再帰アルゴリズム
  • /*--- 퀵 정렬 (비재귀 버전) ---*/
    /* 분할 범위(인덱스 양끝)를 Push, Pop */
    void quick_sort(int a[], int left, int right)
    {
    	// 그룹의 첫 요소, 끝 요소 인덱스
    	IntStack lstack, rstack;
    
    	// 스택 초기화 
    	Initialize(&lstack, right - left + 1); 
    	Initialize(&rstack, right - left + 1); 
    	
    	Push(&lstack, left); Print(&lstack); // 맨 앞 인덱스
    	Push(&rstack, right);  Print(&rstack); // 맨 뒤 인덱스
    
    	while (!IsEmpty(&lstack)) { // 두 그룹으로 분할 마치면 종료(함수 호출 완료하면 Pop 되므로)
    		// Pop하면 분할 시작
    		int pl = (Pop(&lstack, &left), left); // Pop후 왼쪽 스택에 left 대입
    		int pr = (Pop(&rstack, &right), right); // Pop후 오른쪽 스택에 rigjht 대입
    		int x = a[(left + right) / 2]; // 중앙값
    
    		/* 분할(partition) */
    		do { 
    			while (a[pl] < x) pl++;
    			while (a[pr] > x) pr--;
    			if (pl <= pr) {
    				swap(int, a[pl], a[pr]);
    				pl++;
    				pr--;
    			}
    		} while (pl <= pr); // pl과 pr 교차시 종료(두 그룹으로 분할 마침)
    
    		/* 스택의 최대 용량 줄이는 코드*/
    		if (pr - left < right - pl) { // 오른쪽 요소의 개수가 더 많으면
    		// 오른쪽 그룹 Push 먼저(요소가 작은 왼쪽 그룹 Pop 먼저도서 최대 스택 용량 줄어듬)
    			swap(int, pl, left); // left는 pl, pl는 left
    			swap(int, pr, right); // right는 pr, pr는 right
    		}
    
    		/* 그룹 분할 완료후 Push */
    		if (left < pr) { // 왼쪽 그룹 푸쉬
    			Push(&lstack, left); // 맨 첫 요소 
    			Push(&rstack, pr);   // 맨 끝 요소 
    		}
    		if (right > pl) { // 오른쪽 그룹 푸쉬
    			Push(&lstack, pl);	// 맨 첫 요소 
    			Push(&rstack, right); // 맨 끝 요소 
    		}
    	}
    	Terminate(&lstack); 
    	Terminate(&rstack);
    }
  • IntStack.h
  • /* int형 스택 IntStack(헤더) */
    #ifndef ___IntStack
    #define ___IntStack
    
    /*--- 스택을 구현하는 구조체 ---*/
    typedef struct {
    	int max;		/* 스택 용량 */
    	int ptr;		/* 스택 포인터 */
    	int* stk;		/* 스택의 첫 요소에 대한 포인터 */
    } IntStack;
    
    /*--- 스택 초기화 ---*/
    int Initialize(IntStack* s, int max);
    
    /*--- 스택에 데이터를 푸시---*/
    int Push(IntStack* s, int x);
    
    /*--- 스택에서 데이터를 팝 ---*/
    int Pop(IntStack* s, int* x);
    
    /*--- 스택에서 데이터를 피크 ---*/
    int Peek(const IntStack* s, int* x);
    
    /*--- 스택 비우기 ---*/
    void Clear(IntStack* s);
    
    /*--- 스택의 최대 용량 ---*/
    int Capacity(const IntStack* s);
    
    /*--- 스택의 데이터 개수 ---*/
    int Size(const IntStack* s);
    
    /*--- 스택이 비어 있나요? ---*/
    int IsEmpty(const IntStack* s);
    
    /*--- 스택이 가득 찼나요? ---*/
    int IsFull(const IntStack* s);
    
    /*--- 스택에서 검색 ---*/
    int Search(const IntStack* s, int x);
    
    /*--- 모든 데이터 출력 ---*/
    void Print(const IntStack* s);
    
    /*--- 스택 종료 ---*/
    void Terminate(IntStack* s);
    #endif
  • IntStack.c
  • /* int형 스택 IntStack (소스) */
    #include <stdio.h>
    #include <stdlib.h>
    #include "IntStack.h"
    
    /*--- 스택 초기화 ---*/
    int Initialize(IntStack* s, int max)
    {
    	s->ptr = 0;
    	if ((s->stk = calloc(max, sizeof(int))) == NULL) {
    		s->max = 0;								/* 배열의 생성에 실패 */
    		return -1;
    	}
    	s->max = max;
    	return 0;
    }
    
    /*--- 스택에 데이터를 푸시---*/
    int Push(IntStack* s, int x)
    {
    	if (s->ptr >= s->max)						/* 스택이 가득 참 */
    		return -1;
    	s->stk[s->ptr++] = x;
    	return 0;
    }
    
    /*--- 스택에서 데이터를 팝 ---*/
    int Pop(IntStack* s, int* x)
    {
    	if (s->ptr <= 0)							/* 스택이 비어 있음 */
    		return -1;
    	*x = s->stk[--s->ptr];
    	return 0;
    }
    
    /*--- 스택에서 데이터를 피크 ---*/
    int Peek(const IntStack* s, int* x)
    {
    	if (s->ptr <= 0)							/* 스택이 비어 있음 */
    		return -1;
    	*x = s->stk[s->ptr - 1];
    	return 0;
    }
    
    /*--- 스택 비우기 ---*/
    void Clear(IntStack* s)
    {
    	s->ptr = 0;
    }
    
    /*--- 스택 용량 ---*/
    int Capacity(const IntStack* s)
    {
    	return s->max;
    }
    
    /*--- 스택에 쌓여 있는 데이터 수 ---*/
    int Size(const IntStack* s)
    {
    	return s->ptr;
    }
    
    /*--- 스택이 비어 있는가? ---*/
    int IsEmpty(const IntStack* s)
    {
    	return s->ptr <= 0;
    }
    
    /*--- 스택은 가득 찼는가? ---*/
    int IsFull(const IntStack* s)
    {
    	return s->ptr >= s->max;
    }
    
    /*--- 스택에서 검색 ---*/
    int Search(const IntStack* s, int x)
    {
    	int i;
    
    	for (i = s->ptr - 1; i >= 0; i--)	/* 꼭대기(top) → 바닥(bottom)으로 선형 검색 */
    		if (s->stk[i] == x)
    			return i;		/* 검색 성공 */
    	return -1;				/* 검색 실패 */
    }
    
    /*--- 모든 데이터 표시 ---*/
    void Print(const IntStack* s)
    {
    	int i;
    
    	for (i = 0; i < s->ptr; i++)		/* 바닥(bottom) → 꼭대기(top)로 스캔 */
    		printf("%d ", s->stk[i]);
    	putchar('\n');
    }
    
    /*--- 스택 종료 ---*/
    void Terminate(IntStack* s)
    {
    	if (s->stk != NULL)
    		free(s->stk);		/* 배열을 삭제 */
    	s->max = s->ptr = 0;
    }
    
    スタック容量の削減
    上のアルゴリズムでは,配列の個数をスタックの容量に初期化する.ただし、なるべくメモリを使わないほうがいいので、改善してみましょう.上のアルゴリズムは常に左のグループをプッシュし、スタックの一番後ろのプッシュデータは遅くポップアップされます.従って、先に入ったグループは、その後分割される.これにより、プッシュ順に従ってデータの数を制御することができる.
  • 要素数の多いグループから
  • をプッシュする.
  • 要素数の少ないグループから
  • をプッシュする.
    要素の数が多いグループをバッファリングする場合は、要素の数が比較的少ないグループを分割してから、要素の数が多いグループを分割し、スタック容量を削減します.

  • 入力値


  • 要素の多いグループを先にプッシュ

  • より少ない要素のグループを先にプッシュ
  • これは、エレメント数の少ないグループで、エレメント数の多いグループが分割および征服されると、スタック内のデータの最大容量が増大するためです.上記の例では、配列サイズは8で、スタックサイズは2のみです.このように容量を減らすと、スタック上の最大容量はlognより小さくなります.popは分割より先であるため,分割過程(深さ)logn回より小さい.スタック容量を減らすには、リフレッシュ前に次のコードを追加します.
    /* 추가한 코드 */
    if (pr - left < right - pl) { // 오른쪽 요소의 개수가 더 많으면
    		// 오른쪽 그룹 Push 먼저(요소가 작은 왼쪽 그룹 Pop 먼저도서 최대 스택 용량 줄어듬)
    			swap(int, pl, left); // left는 pl, pl는 left
    			swap(int, pr, right); // right는 pr, pr는 right
    		}
            
    /* 그룹 분할 완료후 Push */
    if (left < pr) { // 왼쪽 그룹 푸쉬
    			Push(&lstack, left); // 맨 첫 요소 
    			Push(&rstack, pr);   // 맨 끝 요소 
    		}
    if (right > pl) { // 오른쪽 그룹 푸쉬
    			Push(&lstack, pl);	// 맨 첫 요소 
    			Push(&rstack, right); // 맨 끝 요소 
    		}
    追加されたコードは、右側の要素が左側の要素より多い場合、plとleft、pr、rigtのコードを交換します.そうすると、要素の多い部分から、まずはPushです.
    高速ソートの改善

    左揃え値8としてピボットを選択すると、ピボット値(8)のみのグループと残りのグループに分けられます.1つの要素と残りの要素に分割する分割は一方に偏り、分割プロセスが増加するため、迅速なソートは期待できません.そこで、以下の方法を参考にしてください.
    1.高速ソートを改善し、3つの中値を得る
    分割する配列に3つ以上の要素がある場合は、中心値の要素の1つを軸として3つの要素を選択します.分割する配列の最初の、中間の、および終了する要素をソートし、中間および終了で2番目の要素を交換します.端点の2番目の要素の値(a[right-1])を選択することにより、分割するターゲット範囲をa[left+1]~a[right-2]に縮小する.
    2.挿入ソートを使用したクイックソートの改善
    クイックソートは、要素数の少ない配列では処理が速くなく、挿入ソートは配列サイズが小さい場合に効率的です.配列サイズが10個未満の場合は、挿入ソートを使用します.再帰呼び出しの深さを低減します.
  • 左側カーソルplの開始位置左側=左側+1
  • 右カーソルprの開始位置右=右-2
  • これにより、パケットのサイズが一方に偏ることを回避し、スキャンする要素を3つ減らして、より速い速度でソートすることができます.
  • コード
  • int sort3elem(int x[], int a, int b, int c) {
    	if (x[b] < x[a]) swap(int, x[b], x[a]);
    	if (x[c] < x[b]) swap(int, x[c], x[b]);
    	if (x[b] < x[a]) swap(int, x[b], x[a]);
    	return b;
    }
    void insertion(int a[], int n) {
    	int i, j;
    
    	for (i = 1; i < n; i++) { // 전 값과 비교하기 때문에 1인덱스부터 시작
    		int tmp = a[i]; // 삽입할 값 저장
    		for (j = i; j > 0 && a[j - 1] > tmp; j--) // 삽입할 값의 앞만 검사
    			a[j] = a[j - 1]; // 뒤로 한칸씩 옮김
    		// memmove(&a[j], &a[j - 1], sizeof(int)); // memory move 위치 이동 함수
    		a[j] = tmp; // 알맞은 위치에 삽입
    	}
    }
    void quick(int a[], int left, int right) { // 배열 분할 함수
    	int pl = left; // left pivot
    	int pr = right; // right pivot 
    	int x; // 피봇 변수
    	int m = sort3elem(a, pl, (pl + pr) / 2, pr); // 한쪽으로 치우쳐 지는 분할 해결
    	x = a[m]; // 중앙요소 피봇값 대입
    	swap(int, a[m], a[right - 1]); // 중앙요소 값과 끝에서 두번째 값과 위치 바꿈
    
    	/* 스캔할 값 3개 줄임 */
    	pl++; // left = left + 1
    	pr -= 2; // right = right - 2
    
    	if (right - left < 9)
    		insertion(&a[left], right - left + 1); // 처음배열 left부터
    	else {
    		/* 분할(divide) */
    		do {
    			// 왼쪽피봇값과 오른쪽 피봇값을 서로 교환해야함(작은 값은 왼쪽으로 큰 값은 오른쪽으로)
    			while (a[pl] < x) pl++; // 왼쪽 피봇값이 중앙 요소값보다 작을때
    			while (a[pr] > x) pr--; // 오른쪽 피봇값이 중앙 요소값보다 클때
    			if (pl <= pr) { // pl이 pr보다 작을경우나 같을 경우 둘다 이동시켜야함(같을 경우도 값 교환됨)
    				swap(int, a[pl], a[pr]); // 교환
    				pl++;
    				pr--;
    			}
    		} while (pl <= pr);
    
    		/* 작은 요소개수 그룹부터 분할준비 */
    		if (pr - left < right - pl) { // 오른쪽 요소의 개수가 더 많으면
    			// 오른쪽 그룹 함수 호출 먼저
    			swap(int, pl, left); // left는 pl, pl는 left
    			swap(int, pr, right); // right는 pr, pr는 right
    		}
    
    		/* 정복 */
    		if (left < pr) quick(a, left, pr);  // pr이 right 값이 됨
    		if (right > pl) quick(a, pl, right); // pl이 left 값이 됨
    	}
    }
    標準ライブラリqsort関数
    int,double型,構造体式配列,すべての資料型の配列に適している.ただし、関数名はクイックソートから来ますが、内部では常にクイックソートアルゴリズムが使用されるわけではありません.
  • ヘッダー:#include
  • フォーマット:void qsort(void*base,size tnmemb,size t size,int(*比較)(const void*,const void*)
  • base:ソートする配列
  • nmemb:要素数
  • size:貝熱元素サイズ
  • compar:比較関数、comparに保存されている比較関数を使用して
  • をソート
    手動で比較関数を作成する必要があります
    1.最初の引数の値が小さい場合は-1を返します.
    2.最初の引数が2番目の引数と同じ値を指す場合は、0を返します.
    3.最初の引数がより大きい値を指す場合は、1を返します.
  • サンプルコード
  • #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    
    typedef struct {
    	char name[10];	/* 이름 */
    	int height;		/* 키 */
    	int weight;		/* 몸무게 */
    } Person;
    
    /*--- int형 비교 함수(오름차순 정렬에 사용) ---*/
    int int_cmp(const int* a, const int* b)
    {
    	return *a < *b ? -1 : *a > * b ? 1 : 0;
    }
    /*--- Person형 비교 함수(이름 오름차순 정렬) ---*/
    int npcmp(const Person* x, const Person* y)
    {
    	return strcmp(x->name, y->name);
    }
    
    /*--- Person형 비교 함수(키 오름차순 정렬) ---*/
    int hpcmp(const Person* x, const Person* y)
    {
    	return x->height < y->height ? -1 : x->height > y->height ? 1 : 0;
    }
    
    /*--- Person형 비교 함수(몸무게 내림차순 정렬) ---*/
    int wpcmp(const Person* x, const Person* y)
    {
    	return x->weight < y->weight ? 1 : x->weight > y->weight ? -1 : 0;
    }
    
    /*--- 사람 no명의 데이터를 표시 ---*/
    void print_person(const Person x[], int no)
    {
    	int i;
    	for (i = 0; i < no; i++)
    		printf("%-10s %dcm %dkg\n", x[i].name, x[i].height, x[i].weight);
    }
    
    int main()
    {
    	Person x[] = {
    		{ "sunmi", 170, 52 },
    		{ "yoobin", 180, 70 },
    		{ "sohee", 172, 63 },
    		{ "jina", 165, 50 },
    	};
    
    	int nx = sizeof(x) / sizeof(x[0]); 		/* 배열 x의 요소 개수 */
    
    	puts("정렬 전");
    	print_person(x, nx);
    
    	/* 이름 오름차순으로 정렬 */
    	qsort(x, nx, sizeof(Person), (int(*)(const void*, const void*)) npcmp);
    	puts("\n이름 오름차순으로 정렬 후");
    	print_person(x, nx);
    
    	/* 키 오름차순으로 정렬 */
    	qsort(x, nx, sizeof(Person), (int(*)(const void*, const void*)) hpcmp);
    	puts("\n키 오름차순으로 정렬 후");
    	print_person(x, nx);
    
    	/* 몸무게 내림차순으로 정렬 */
    	qsort(x, nx, sizeof(Person), (int(*)(const void*, const void*)) wpcmp);
    	puts("\n몸무게 내림차순으로 정렬 후");
    
    	print_person(x, nx);
    
    	return 0;
    }