처음부터 차근차근

[알고리즘] Quick 정렬 본문

CS/알고리즘

[알고리즘] Quick 정렬

HangJu_95 2024. 1. 2. 16:50
728x90

QuickSort

  • 퀵 정렬은 불안정 정렬에 속하며, 다른 원소와의 비교만으로 정렬을 수행하는 비교 정렬에 속합니다.
  • 분할 정복 알고리즘의 하나로, 평균적으로 매우 빠른 수행 속도를 자랑하는 정렬 방법입니다.
    • 합병 정렬(merge sort)과 달리 퀵 정렬은 list를 비균등하게 분할합니다.
  • 분할 정복(divide and conquer) 방법
    • 문제를 작은 2개의 문제로 분리하고 각각을 해결한 다음, 결과를 모아서 원래의 문제를 해결하는 전략입니다.
    • 분할 정복 방법은 대개 순환 호출을 이용하여 구현합니다.
  • 하나의 리스트를 피벗(pivot)을 기준으로 두 개의 비균등한 크기로 분할하고 분할된 부분 리스트를 정렬한 다음, 두 개의 정렬된 부분 리스트를 합하여 전체가 정렬된 리스트가 되게 하는 방법이다.
  • 퀵 정렬은 다음의 단계들로 이루어진다.
    • 분할(Divide): 입력 배열을 피벗을 기준으로 비균등하게 2개의 부분 배열(피벗을 중심으로 왼쪽: 피벗보다 작은 요소들, 오른쪽: 피벗보다 큰 요소들)로 분할한다.
    • 정복(Conquer)부분 배열을 정렬한다. 부분 배열의 크기가 충분히 작지 않으면 순환 호출 을 이용하여 다시 분할 정복 방법을 적용한다.
    • 결합(Combine): 정렬된 부분 배열들을 하나의 배열에 합병한다.
    • 순환 호출이 한번 진행될 때마다 최소한 하나의 원소(피벗)는 최종적으로 위치가 정해지므로, 이 알고리즘은 반드시 끝난다는 것을 보장할 수 있다.
  • Quick 정렬 과정 설명
    1. 리스트 안에 있는 한 요소를 선택한다. 이렇게 고른 원소를 pivot이라고 한다.
    2. pivot을 기준으로 pivot보다 작은 요소들은 모두 pivot의 왼쪽으로 옮겨지고 큰 요소는 모두 오른쪽으로 옮겨진다.
    3. 피벗을 제외한 왼쪽 리스트와 오른쪽 리스트를 다시 정렬한다.
      • 분할된 부분 리스트에 대하여 순환 호출을 이용하여 정렬을 반복합니다.
      • 부분 리스트에서도 다시 피벗을 정하고 피벗을 기준으로 2개의 부분 리스트로 나누는 과정을 반복합니다.
    4. 부분 리스트들이 더 이상 분할이 불가능 할 때까지 반복한다.
      • 리스트의 크기가 0이나 1이 될 때까지 반복합니다.

Quicksort 과정

QuickSort 알고리즘 예제

https://gmlwjd9405.github.io/2018/05/10/algorithm-quick-sort.html

  • 배열에 5, 3, 8, 4, 9, 1, 6, 2, 7이 저장되어 있다고 가정하고 자료를 오름차순으로 정렬해 보자.
  • 퀵 정렬에서 피벗을 기준으로 두 개의 리스트로 나누는 과정(c언어 코드의 partition 함수의 내용)
  • 피벗 값을 입력 리스트의 첫 번째 데이터로 하자. (다른 임의의 값이어도 상관없다.)
  • 2개의 인덱스 변수(low, high)를 이용해서 리스트를 두 개의 부분 리스트로 나눈다.
  • 1회전: 피벗이 5인 경우,
    • low는 왼쪽에서 오른쪽으로 탐색해가다가 피벗보다 큰 데이터(8)을 찾으면 멈춘다.
    • high는 오른쪽에서 왼쪽으로 탐색해가다가 피벗보다 작은 데이터(2)를 찾으면 멈춘다.
    • low와 high가 가리키는 두 데이터를 서로 교환한다.
    • 이 탐색-교환 과정은 low와 high가 엇갈릴 때까지 반복한다.
  • 2회전: 피벗(1회전의 왼쪽 부분리스트의 첫 번째 데이터)이 1인 경우,
    • 위와 동일한 방법으로 반복한다.
  •  3회전: 피벗(1회전의 오른쪽 부분리스트의 첫 번째 데이터)이 9인 경우,
    • 위와 동일한 방법으로 반복한다.

예제 출처 : https://gmlwjd9405.github.io/2018/05/10/algorithm-quick-sort.html

QuickSort Typescript 예제 코드

  quicksort(start: number, end: number): number[] {
    const startTime = Date.now();
    const answer = this.quicksortMethod(this.arr, start, end);
    const endTime = Date.now();
    console.log(endTime - startTime, "ms");
    return answer as number[];
  }

  private partition(part: number[], start: number, end: number): number {
    let pivot = part[end]; // 정렬할 리스트의 가장 오른쪽 데이터를 피벗으로 선택
    let i = start - 1; // 왼쪽에 넣을 인덱스를 위한 변수
    // 배열의 start값부터 진행
    for (let j = start; j < end; j++) {
      // pivot보다 값이 작거나 같은 경우
      if (part[j] <= pivot) {
        // 인덱스 값 1 추가
        i += 1;
        // 해당하는 인덱스와 part[j]의 위치를 변경
        [part[i], part[j]] = [part[j], part[i]];
      }
    }
    // 최종 i보다 1 큰 값과 pivot의 위치를 변경한다.
    [part[i + 1], part[end]] = [part[end], part[i + 1]];
    return i + 1;
  }

  private quicksortMethod(
    arr: number[],
    start: number,
    end: number
  ): number[] | null {
    if (start >= end) { // 정렬할려는 데이터의 범위가 1 이하이면
      return null;
    }
    let p = this.partition(arr, start, end);
    this.quicksortMethod(arr, start, p - 1);
    this.quicksortMethod(arr, p + 1, end);
    return arr;
  }

Quicksort 알고리즘 특징

  • 장점
    • 속도가 빠르다.
      • 시간 복잡도가 O(nlog₂n)를 가지는 다른 정렬 알고리즘과 비교했을 때도 가장 빠르다.
    • 추가 메모리 공간을 필요로 하지 않는다.
      • 퀵 정렬은 O(log n)만큼의 메모리를 필요로 한다.
  •  단점
    • 정렬된 리스트에 대해서는 퀵 정렬의 불균형 분할에 의해 오히려 수행시간이 더 많이 걸린다.
    • 퀵 정렬의 불균형 분할을 방지하기 위하여 피벗을 선택할 때 더욱 리스트를 균등하게 분할할 수 있는 데이터를 선택한다.
      • EX) 리스트 내의 몇 개의 데이터 중에서 크기순으로 중간 값(medium)을 피벗으로 선택한다.

Quicksort의 시간복잡도

최선의 경우 : nlog₂n

  • 순환 호출의 깊이
    • 레코드의 개수 n이 2의 거듭제곱이라고 가정(n=2^k)했을 때, n=2^3의 경우, 2^3 -> 2^2 -> 2^1 -> 2^0 순으로 줄어들어 순환 호출의 깊이가 3임을 알 수 있다. 이것을 일반화하면 n=2^k의 경우, k(k=log₂n)임을 알 수 있다.
    • k=log₂n
  • 각 순환 호출 단계의 비교 연산
    • 각 순환 호출에서는 전체 리스트의 대부분의 레코드를 비교해야 하므로 평균 n번 정도의 비교가 이루어진다.
    • 평균 n번
  •  순환 호출의 깊이 * 각 순환 호출 단계의 비교 연산 = nlog₂n

최악의 경우 : n^2

  • 리스트가 계속 불균형하게 나누어지는 경우 (특히, 이미 정렬된 리스트에 대하여 퀵 정렬을 실행하는 경우)
  • 순환 호출의 깊이
    • 레코드의 개수 n이 2의 거듭제곱이라고 가정(n=2^k)했을 때, 순환 호출의 깊이는 n임을 알 수 있다.
    • n
  • 각 순환 호출 단계의 비교 연산
    • 각 순환 호출에서는 전체 리스트의 대부분의 레코드를 비교해야 하므로 평균 n번 정도의 비교가 이루어진다.
    • 평균 n번
  • 순환 호출의 깊이 * 각 순환 호출 단계의 비교 연산 = n^2

참고

 

[알고리즘] 퀵 정렬(quick sort)이란 - Heee's Development Blog

Step by step goes a long way.

gmlwjd9405.github.io

'CS > 알고리즘' 카테고리의 다른 글

[알고리즘] Heapsort  (1) 2024.01.02
[알고리즘] MergeSort  (0) 2024.01.02
[알고리즘] 버블 정렬, 선택 정렬, 삽입 정렬  (0) 2023.12.27
[알고리즘] Backtracking, N-Queen 문제  (0) 2023.11.16
[알고리즘] BFS  (1) 2023.11.16