반응형

기수 정렬이란?

 

출처 :   https://herong.tistory.com/m/190?category=818669   ​

 

기수 정렬 (Radix Sort) 자리수를 비교하며 정렬을 진행하는 알고리즘 이다.

 

기수 정렬의 종류

1. LSD (Least Significant Digit) 방식의 정렬

  • 가장 작은 자릿수부터 정렬을 진행 (숫자로 치면 일의 자리수부터)
  • 가장 작은 자릿수부터 큰 자릿수까지 비교해야 한다는 단점이 있지만, 코드 구현이 MSD에 비해 간결하다

2. MSD (Most Significant Digit) 방식의 정렬

  • 가장 큰 자릿수부터 정렬을 진행
  • LSD와 비교했을 때 정렬 상태 확인등의 추가 작업이 필요하지만, 중간에 정렬이 완료될 수 있다는 장점이 있다

 

기수 정렬의 과정

기수 정렬의 과정을 배열 [134,224,232,122] 을 이용해

LSD방식과 MSD방식으로 각각 설명하고자 한다.

 

1. LSD 방식

 

<STEP 1>

출처 :   https://herong.tistory.com/m/190?category=818669
  • 일의 자릿수부터 확인한다
  • 숫자를 나타내는 버킷에 일의 자릿수를 확인하며 저장한다 (2는 버킷2에 / 4는 버킷4에)
  • 꺼낼 때는 먼저 저장된 값부터 꺼내게 된다 (FIFO)

 

<STEP 2>

출처 :   https://herong.tistory.com/m/190?category=818669
  • 그 다음 자리수인 십의 자리를 기준으로 버킷에 저장한다
  • 위의 과정과 동일하게 진행한다

 

<STEP 3>

출처 :   https://herong.tistory.com/m/190?category=818669
  • 그 다음 자리수인 백의 자리를 기준으로 버킷에 저장한다
  • 마지막 자리수까지 검사를 마친 경우 오름차순으로 정렬이 완료된다

 

 

2. MSD 방식

 

<STEP 1>

출처 :   https://herong.tistory.com/m/190?category=818669
  • 가장 큰 자릿수 (여기서는 백의 자리)부터 확인한다
  • 각 숫자를 나타내는 버킷에 백의 자릿수를 확인하며 저장한다
  • 꺼낼 때는 먼저 저장된 값부터 꺼내게 된다 (FIFO)

 

<STEP 2>

출처 :   https://herong.tistory.com/m/190?category=818669
  • 두번째 과정부터는 STEP 1로 분류된 그룹끼리 정렬을 수행한다
    (134,122) , (224,232)

 

<STEP 3>

출처 :   https://herong.tistory.com/m/190?category=818669  ​
  • 두번째 그룹 (224,232)의 경우 자리수를 비교하지 않아도 이전에 정렬이 완료된 상태이므로
    더 이상 정렬을 진행하지 않아도 된다

 

기수 정렬의 특징

  • 정렬 순서상 앞과 뒤의 판단을 위한 비교 연산을 하지 않는다
  • 정렬 대상의 모든 길이가 동일해야 가장 효율적이다
    좋은 예시) [123,456,789,961], [music, clock, green, cream]
    나쁜 예시) [4562,12,651,-1123],[blue, yellow, red]
  • 정렬 대상의 자리수를 기준으로 '버킷'이라는 공간에 '큐(Queue)' 방식으로 저장 후 다시 꺼낸다
  • 같은 수의 순서가 섞이지 않는 안정 정렬이다 (MSD 방식은 불안정 정렬)

 

시간복잡도

  • 키의 수를 , 키의 최대 길이를 라고 했을 때, 시간복잡도는  이다.

 

공간 복잡도

  • LSD 방식의 경우 키의 수를 , 버켓의 종류 수를 라고 했을 때, 공간복잡도는  이다.
  • MSD 방식의 경우 키의 수를 , 버켓의 종류 수를 , 키의 최대 길이를 라고 했을 때, 공간복잡도는  이다.

 


코드로 구현한 기수 정렬

 

c++

void Radix_Sort()                // 기수정렬 함수 !
{
    int Radix = 1;                // 최대 자릿수까지 계산을 하기 위한 변수
    while (1)                    // 최대 자릿수가 몇 자리인지 알아내기 위한 반복문 !
    {
        if (Radix >= Max_Value) break;    // Max_Value는 입력과 동시에 구해놓은 배열에서의 최댓값 !
        Radix = Radix * 10;        
    }
    for (int i = 1; i < Radix; i = i * 10)    // 1의 자리부터 10씩 곱하면서 최대자릿수 까지 반복 !
    {
        for (int j = 0; j < MAX; j++)    // 모든 배열을 다 탐색하면서
        {
            int K;
            if (Arr[j] < i) K = 0;        // 만약 현재 배열의 값이 현재 찾는 자릿수보다 작으면 0 !
            else K = (Arr[j] / i) % 10;    // 그게 아니라면 위에서 말한 공식 적용 !
            Q[K].push(Arr[j]);        // Queue배열에 해당 값을 순차적으로 저장 !
        }

        int Idx = 0;
        for (int j = 0; j < 10; j++)    // 0부터 9까지 Queue에 저장된 값들을 순차적으로 빼내기 위한 반복문.
        {
            while (Q[j].empty() == 0)    // 해당 Index번호의 Queue가 빌 때 까지 반복
            {
                Arr[Idx] = Q[j].front();    // 하나씩 빼면서 배열에 다시 저장.
                Q[j].pop();        
                Idx++;
            }
        }
    }
}

 

코드 출처

 

Java

package Sort;

import java.util.Arrays;
// 기수 정렬 알고리즘 구현
public class radix {
    // 배열에서 최대값을 얻기 위한 메서드
    static int getMax(int[] data) {
        int mx = data[0];
        for(int i=1; i<data.length; i++) {
            if(data[i] > mx) {
                mx = data[i];
            }
        }
        return mx;
    }
    // exp 변수의 수에 따라 숫자를 정렬
    static void countSort(int[] data, int exp) {
        int[] output = new int[data.length];
        int[] count = new int[10];
        Arrays.fill(count, 0);
        
        // count 값 count배열에 저장
        for(int i=0; i<data.length; i++) {
            count[(data[i]/exp)%10]++;
        }
        // count 값이 포함시켜 count배열에 저장
        for(int i=1; i<10; i++) {
            count[i] += count[i-1];
        }
        // output배열 빌드
        for(int i=data.length-1; i>=0; i--) {
            output[count[(data[i]/exp)%10]-1] = data[i];
            count[(data[i]/exp)%10]--;
        }
        // output 배열에 저장된 값을 data 배열에 저장
        for(int i=0; i<data.length; i++) {
            data[i] = output[i];
        }
    }
    static void radixsort(int[] data) {
        // 최대값 찾기
        int m = getMax(data);
        for(int exp=1; m/exp>0; exp*=10) {
            countSort(data, exp);
        }
    }

    public static void main(String[] args) {
        // TODO Auto-generated method stub
        int [] data = {4, 54, 2, 8, 63, 7, 55, 56};
        // 기수 정렬 전
        System.out.println("# 기수 정렬 전");
        for(int i=0; i<data.length; i++) {
            System.out.print(data[i]+" ");
        }
        System.out.println();
        
        radixsort(data);
        // 기수 정렬 후
        System.out.println("# 기수 정렬 후");
        for(int i=0; i<data.length; i++) {
            System.out.print(data[i]+" ");
        }
    }

}

 

코드 출처

 

Python

def countingSort(arr, digit):
    n = len(arr)
  
    # 배열의 크기에 맞는 output 배열을 생성하고 10개의 0을 가진 count란 배열을 생성한다. 
    output = [0] * (n)
    count = [0] * (10)
    
    #digit, 자릿수에 맞는 count에 += 1을 한다. 
    for i in range(0, n):
        index = int(arr[i]/digit) 
        count[ (index)%10 ] += 1
 
    # count 배열을 수정해 digit으로 잡은 포지션을 설정한다.  
    for i in range(1,10):
        count[i] += count[i-1]  
        print(i, count[i])
    # 결과 배열, output을 설정한다. 설정된 count 배열에 맞는 부분에 arr원소를 담는다.   
    i = n - 1
    while i >= 0:
        index = int(arr[i]/digit)
        output[ count[ (index)%10 ] - 1] = arr[i]
        count[ (index)%10 ] -= 1
        i -= 1

    #arr를 결과물에 다시 재할당한다.  
    for i in range(0,len(arr)): 
        arr[i] = output[i]
 
# Method to do Radix Sort
def radixSort(arr):
    # arr 배열중에서 maxValue를 잡아서 어느 digit, 자릿수까지 반복하면 될지를 정한다. 
    maxValue = max(arr)  
    #자릿수마다 countingSorting을 시작한다. 
    digit = 1
    while int(maxValue/digit) > 0: 
        countingSort(arr,digit)
        digit *= 10
 
arr = [ 170, 45, 75, 90, 802, 24, 2, 66]
#arr = [4, 2, 1, 5, 7, 2]
radixSort(arr)
 
for i in range(len(arr)):
    print(arr[i], end=" ")

 

코드 출처

 


 

기수 정렬의 장단점

 

장점

  • 키의 길이 가 작다면 의 시간으로 정렬을 할 수 있기 때문에 굉장히 빠른 속도로 정렬을 할 수 있다
  • 카운팅 정렬과 같이 비교 연산 없이 정렬을 수행 할 수 있다

단점

  • 추가적으로 버킷을 사용하기 때문에 데이터 크기에 비례한 메모리가 필요하다
  • 버킷의 종류 (2진수,10진수,알파벳) 등이 고정적이지 않다
  • 정렬 방법의 특수성 때문에, 부동소수점 실수처럼 특수한 비교 연산이 필요한 데이터에는 적용하기 힘들다
반응형
반응형

힙 정렬이란?

힙 정렬(Heap Sort)이란 최대 힙 트리나 최소 힙 트리를 구성해 정렬을 하는 방법으로서,

내림차순 정렬을 위해서는 최대 힙을 구성하고

오름차순 정렬을 위해서는 최소 힙을 구성한다.

 

힙 정렬은 1964년 J.W.J 윌리엄스에 의해 발명되었고, 같은 해 R.W. 플로이드가 제자리 정렬을 할 수 있는 개선판을 출판하였고 이 방법이 바로 트리정렬 알고리즘을 이용한 방식이다.

 

힙 정렬 시뮬레이션

아래의 그림은 힙 정렬을 시뮬레이션한 그림이다.

추가로 힙 정렬을 숫자와 함께 보고 싶다면 아래의 사이트를 가보는 것도 추천한다.

 

힙 정렬 과정

힙 정렬 과정을 알기 위해선 힙 자료구조에 대한 선행 공부가 필수적이다.

만약 힙에 대한 자료구조를 모른다면 [자료구조] 힙 포스팅 한번 보고오도록 하자.

이제 힙 자료구조를 안다고 가정하고 과정을 설명해보고자 한다.

 

  1. n개의 노드에 대한 완전 이진 트리를 구성한다. 이때 루트 노드부터 부모노드, 왼쪽 자식노드, 오른쪽 자식노드 순으로 구성한다.
  2. 최대 힙을 구성한다. 최대 힙이란 부모노드가 자식노드보다 큰 트리를 말하는데,
    단말 노드를 자식노드로 가진 부모노드부터 구성하며 아래부터 루트까지 올라오며 순차적으로 만들어 갈 수 있다.
  3. 가장 큰 수(루트에 위치)를 가장 끝의 노드와 교환한다.
  4. 2와 3을 반복한다.

 

위의 알고리즘을 이제 시뮬레이션을 통하여 확인해보자.

[6, 5, 3, 1, 8, 7, 2, 4] 의 배열을 오름차순으로 정렬하는 문제가 주어졌다고 가정하자.

맨 위에서 설명한 것과 같이 오름차순 정렬은 최대 힙을 구성하여 정렬을 수행한다.

  1. 먼저 이진 탐색트리를 만들며 최대 힙을 구현한다.
  2. 그 후 최대 힙의 최대 루트 노드를 배열의 마지막 자리 원소로 고정하고 트리에서 마지막 노드와 자리를 바꾼다.
  3. 이 과정을 모든 자리가 고정될 때까지 최대 힙을 만들고 루트 노드를 배열의 마지막 자리 원소로 만드는 과정을 반복하며 정렬을 수행한다.

 

힙 정렬 시간 복잡도

힙 구현 포스팅에서도 알아봤듯이, 최대 힙으로 만드는 과정(heapify)의 시간 복잡도는  이다.

이 heapify의 과정이 n개의 원소를 다 정렬할 때까지 반복되므로 최종 힙 정렬의 시간 복잡도는  이 된다.

 

힙 정렬 구현 코드

// c++ 로 구현한 힙 정렬
#include <iostream>

using namespace std;

// i 노드가 가장 큰 노드인 힙트리를 만들기 위한 함수
// 힙 사이즈는 n
void heapify(int arr[], int n, int i)
{
    int largest = i; // 루트를 가장 큰 노드로 초기 설정
    int l = 2 * i + 1; // left
    int r = 2 * i + 2; // right

    // 왼쪽 자식 노드가 가장 큰 노드보다 클 때
    if (l < n && arr[l] > arr[largest])
        largest = l;

    // 오른쪽 자식 노드가 가장 큰 노드보다 클때
    if (r < n && arr[r] > arr[largest])
        largest = r;

    // 만약, 가장 큰 노드가 루트가 아니면 스왑
    if (largest != i) {
        swap(arr[i], arr[largest]);

        // 재귀적으로 서브트리를 힙화 한다.
        heapify(arr, n, largest);
    }
}

// 힙 정렬 함수
void heapSort(int arr[], int n)
{
    // 힙을 구성 (배열 재 정렬)
    for (int i = n / 2 - 1; i >= 0; i--)
        heapify(arr, n, i);

    // 하나씩 힙에서 원소를 추출
    for (int i = n - 1; i > 0; i--) {
        // 루트노드를 끝 노드로 이동
        swap(arr[0], arr[i]);

        // 줄어든 힙에서 최대 힙 구성
        heapify(arr, i, 0);
    }
}

// n 크기의 배열을 출력하는 코드
void printArray(int arr[], int n) {
    for (int i = 0; i < n; ++i)
        cout << arr[i] << " ";
    cout << "\n";
}

int main() {
    int arr[] = { 12, 11, 13, 5, 6, 7 };
    int n = sizeof(arr) / sizeof(arr[0]);

    heapSort(arr, n);

    cout << "정렬된 배열은 : \n";
    printArray(arr, n);
}

 

 

힙 정렬 특징 정리

  • 정렬을 위한 추가적인 메모리가 필요하지 않다. (제자리 정렬 가능)
  • 최선, 평균, 최악의 경우의 모두 heapify 과정이 필요하기 때문에 nlogn 을 보장한다.
  • 데이터의 순서를 보장하지 못하는 불안정(unstable)정렬이다.
  • 힙정렬과 퀵정렬을 비교해보면 똑같은 nlogn 이지만 컴퓨터의 하드웨어 구조상 퀵정렬이 실제로는 더 빠르다고 한다. 이유는 퀵 정렬의 경우는 대개 원소들끼리 근접한 메모리 영역에 붙어 있는 배열을 사용하기 때문에 일반적으로 캐시 친화적이지만
    힙정렬의 원소들은 좀 더 흩어져 있는 경우가 많아서 캐시 친화도가 떨어지는 문제가 있고
    힙정렬은 일반적으로 포인터 연산을 많이 사용하기 때문에 거기에 걸리는 오버헤드도 무시할 수는 없는 수준이기 때문이다.

나올 수 있는 면접 질문

  • 힙 정렬의 과정을 말씀해 주세요.
  • 힙 정렬의 시간복잡도는 어떻게 되나요? 왜 그러한 시간복잡도가 나오나요?
  • 힙 정렬과 퀵 정렬 어떤게 더 빠를까요?

참고

반응형
반응형

퀵정렬이란?

퀵정렬(Quick Sort)은 퀵이라는 이름답게 평균 속도 O(N*logN)을 자랑하는 매우 빠른 정렬 알고리즘이다.

퀵정렬은 분할정복(Divide & Conquer) 알고리즘의 하나로, 다른 원소와의 비교만으로 정렬을 수행하는 비교 정렬에 속하는 알고리즘이다.

 

퀵정렬의 전체적인 정렬 과정

퀵정렬은 임의의 pivot 값을 기준으로

pivot 의 좌측에는 pivot 보다 작은값을 두고

우측에는 pivot 보다 큰 값을 두는 방식으로 정렬을 진행한다.

 

전체적인 정렬 과정은 다음과 같다.

1. 리스트 안의 임의의 요소 하나를 선택한다. 이를 피봇(pivot) 이라고 하자.

2. pivot을 기준으로 pivot의 왼쪽에는 pivot보다 작은 원소를, pivot의 오른쪽에는 pivot보다 큰 원소를 배치한다.

3. pivot을 기준으로 분할된 왼쪽 리스트와 오른쪽 리스트에 대해 다시 1,2의 과정을 반복한다.

4. 이렇게 순환 과정을 통해 분할된 각 리스트의 크기가 0이나 1이 되면 수행을 종료한다.

 

퀵정렬의 자세한 정렬과정

이를 그림과 함께 좀 더 자세한 과정으로 살펴보자.

1. 피봇을 선택한다.

2. left는 왼쪽에서 오른쪽으로 가면서 피봇보다 큰 수를 찾는다.

3. right는 오른쪽에서 왼쪽으로 가면서 피봇보다 작은 수를 찾는다.

4. 찾은 지점에서 left와 right를 교환한다.

5. 위의 2,3,4과정 left와 right가 교차할 때 까지 반복한다.

6. left와 right가 교차하면 피봇(pivot)과 right를 교환한다.

7. 이렇게 되면 피봇의 왼쪽에는 피봇보다 작은수가, 피봇의 오른쪽에는 피봇보다 큰 수가 위치한다.

8. 피봇을 기준으로 왼쪽과 오른쪽 리스트 두개로 나눠 위의 과정을 반복 수행한다.

9. 이렇게 순환 과정을 통해 분할된 리스트의 크기가 0이나 1이 되면 수행을 종료한다.

 

퀵 정렬의 구현 코드

  • Python 코드
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_sort(arr, start, end):
    if start < end:
        pivot = partition(arr, start, end)
        quick_sort(arr, start, pivot - 1)
        quick_sort(arr, pivot + 1, end)
    return arr

 

퀵 정렬의 장점 및 단점

  • 장점
    • 속도가 빠르다 (평균 속도가 O(NlogN) 이며 O(NlogN)의 속도를 가지는 다른 정렬 알고리즘과 비교했을 때도 가장 빠르다.)
    • 추가 메모리 공간이 필요하지 않다 (O(logN) 만큼의 메모리를 필요로 한다.)
  • 단점
    • 최악의 경우 시간 복잡도는 O(N^2) 이다.

 

면접에 나올 수 있는 질문

  • 퀵 정렬이 최악의 시간복잡도를 가지는 경우를 제시해보세요.
  • 퀵 정렬의 소스코드에서 단 두 부분만을 바꿔 내림차순으로 바꿔보세요.

 

참고자료

반응형
반응형

병합정렬(합병정렬) 이란?

비교 기반의 정렬로, 안정 정렬에 속하며 분할 정복(Divide and Conquer) 알고리즘 중 하나이다.

 

※ 분할 정복(Divde and Conquer) 이란?

: 그대로 해결할 수 없는 문제를 작은 문제로 분할하여 해결한 후, 결과를 모아 원래의 문제를 해결하는 전략.

 

병합정렬의 전체적인 과정

그래서, 병합정렬의 전체적인 과정은 다음과 같이 요약할 수 있다.

  1. 정렬되지 않은 리스트를 각각 1개의 원소만 포함하는 n개의 부분리스트로 분할한다.
  2. 분할된 부분리스트들을 병합(Merge)하며 정렬한다.
    그럼 다시 결국 1개의 리스트가 될 것이고 이 리스트는 정렬된 상태일 것이다.

 

병합정렬의 세부 과정

  1. 분할(Divide) : 정렬되지 않은 리스트를 절반으로 분할하여 비슷한 크기의 두 리스트로 만든다.
    모든 리스트의 길이가 1이 될 때 까지 반복해서 분할 한다.
  2. 정복(Conquer) : 쪼개진 각 부분 리스트들을 정렬하며 합병해나간다.
  3. 결합(Combine) : 최종적으로 다시 하나의 정렬된 리스트로 결합한다.

 

이 과정을 그림으로 한번 살펴보자.

참고 : https://gmlwjd9405.github.io/2018/05/08/algorithm-merge-sort.html

 

병합정렬 코드

​그럼, 파이썬으로 나타낸 코드와 함께 살펴보자.

def merge_sort(list):
	# 길이가 1이면(보다 작으면) 정렬된 것으로 봄.(정렬필요X)
	if len(list) <= 1:
    	return list
    
    # 두개의 분할 리스트로 쪼갬
    mid = len(list) // 2
    left = list[:mid]
    right = list[mid:]
    
    # 쪼갠 각각의 리스트에 대해 길이가 1이 될 때 까지 반복 수행(재귀)
    left = merge_sort(left)
    right = merge_sort(right)
    
    # 쪼갠 리스트들을 정렬해가며 병합하는 함수 호출
    return merge(left, right)
def merge(left, right):
	# 정렬될 요소를 넣을 새 리스트
    result = []
    
    # 왼쪽, 오른쪽 리스트 모두 빌 때 까지 반복
    while len(left) > 0 or len(right) > 0:
        if len(left) > 0 and len(right) > 0:
        	# 첫 번째 값의 크기를 비교하여 작은 값을 꺼내 새 리스트에 삽입
            if left[0] <= right[0]:
                result.append(left[0])
                left = left[1:]
            else:
                result.append(right[0])
                right = right[1:]
        
        # 한 리스트만 비었다면 남은 리스트의 값을 넣음
        elif len(left) > 0:
            result.append(left[0])
            left = left[1:]
        elif len(right) > 0:
            result.append(right[0])
            right = right[1:]
    
    # 정렬된 하나의 리스트 반환
    return result

 

 

병합정렬의 장단점

  • 장점
    • 안정 정렬(Stable Sort)로, 입력데이터가 무엇이든 시간복잡도는 O(nlogn)이다.
      그래서, 항상 동일한 시간이 소요되므로 어떤 경우에도 좋은 성능을 보장한다.
      -> 데이터의 분포에 영향을 덜 받는다.
  • 단점
    • 제자리 정렬이 아니기 때문에, 별도의 추가적인 메모리가 필요하다.
    • 데이터가 많은 경우에는 이동 횟수가 많으므로 큰 시간적 낭비를 초래한다.

 

시간복잡도

출처 :&nbsp;https://gmlwjd9405.github.io/2018/05/08/algorithm-merge-sort.html

 

그럼 위 그림과 함께 어떻게 O(nlogn) 이라는 시간복잡도가 나왔는지 알아보자.

  • 분할 단계 : 개수가 1 이 될 때 까지 쪼개기만 하므로 연산수행 X
  • 병합 단계 :
    • 병합 단계의 깊이
      • 데이터의 개수 N이 2의 거듭제곱이라고 할 때,
        N=8 (2^3) 이라면, 8 -> 4 -> 2 ->1 개로 줄어 단계가 3임을 알 수 있다.
        N=16 (2^4) 이라면, 16 -> 8 -> 4 -> 2 -> 1 개로 줄어들어 4단계이다.

        즉, n=의 경우, 깊이 k=임을 알 수 있다.
    • 비교 연산
      • 마찬가지로, N이 8 이라고 할 때,
        크기가 1인 부분 배열 2개를 합병하는데 최대 2번의 비교 연산이 필요하며
        쌍이 4개 이므로 2 x 4 = 8번의 비교 연산이 필요하다.

      • 그 다음 단계에서는, 크기가 2인 부분배열 2개를 합병하는데 최대 4번의 비교연산이 필요하며
        쌍이 2개 이므로 4 x 2 = 8번의 비교 연산이 필요하다.

        즉, 각각의 병합 단계마다 최대 N번의 비교 연산을 수행한다.
    • 따라서 병합 단계의 깊이 x 각 단계의 비교 연산= 이다.
    • 이를 빅오 표기법으로 표기하면 O() 이다.

참고 자료

반응형
반응형

삽입 정렬이란?

 

Swfung8,&nbsp;CC BY-SA 3.0, via Wikimedia Commons​

삽입 정렬(Insertion Sort)은 자료 배열의 모든 요소를 앞에서부터 차례대로 비교하면서 자리를 교환(swap)하는

정렬 방법이다.

 

삽입 정렬의 과정

 

1. 현재 타겟이 되는 숫자와 이전 위치에 있는 원소들을 비교한다. (첫번째 타겟은 두번째 원소부터 시작)
2. 타겟이 되는 숫자가 이전 위치에 있던 원소보다 작다면 위치를 서로 교환한다.
3. 그 다음 타겟을 찾아 위와 같은 방법으로 반복한다.

 

 

위와 같은 삽입 정렬의 과정을 토대로 삽입 정렬의 전체적인 과정에 대해서 설명하자면 다음과 같다.

 

Muhamed Al-Humaid at Danish Wikipedia , Public domain, via Wikimedia Commons ​

 

(a) : 첫번째 타겟은 두번째 원소부터 시작하므로 7이다.
7 이전 위치에 있는 원소인 3과 비교했을 때 3이 더 작으므로 위치를 스왑하지 않는다.

 

(b) : 두번째 타겟인 2와 이전 위치의 원소인  [7,3] 을 순서대로 비교하며,
타겟보다 원소가 클 경우 원소를 뒤로 미루고 마지막 원소 위치에 타겟을 넣는다.

 

(c) : 세번째 타겟인 5와 이전 위치의 원소를 순서대로 비교한다.
5보다 작은 원소가 나오면 비교를 중단한다.

 

(d) : 네번째 타겟인 1과 이전 위치의 원소를 순서대로 비교한다.

 

(e) : 다섯번째 타겟인 4와 이전 위치의 원소를 순서대로 비교한다.

 

(f) : 삽입 정렬을 통한 오름차순 정렬 완료

 

 

삽입 정렬의 특징

  • 데이터를 비교하면서 찾기 때문에 비교 정렬 이다.
  • 정렬할 대상이 되는 데이터 이외에는 추가적인 공간을 필요로 하지 않기 때문에 제자리 정렬 (in-place sort) 이다.
  • 정렬을 하기 전에서 같은 key값을 가진 원소의 순서가 정렬 후에도 유지되는 안정 정렬 이다.

 

시간복잡도

  • 최악의 경우 (역순으로 정렬된 경우) 선택정렬과 마찬가지로  이다.
  • 정렬이 거의 되어있는 경우는 한번씩만 비교하므로

 

Q: 삽입 정렬이 가장 효율적인 정렬 알고리즘이 되는 경우는?

A: 거의 정렬이 되어있는 배열에 자료를 하나씩 삽입/제거 하는 경우,
삽입 정렬이 정렬 알고리즘 중 제일 효율적인 알고리즘이 될 수 있다.

 

 

공간 복잡도

  • 주어진 배열 안에서만 정렬이 이루어지는 제자리 정렬 이기 떄문에 공간복잡도는  이다.

 


 

코드로 구현한 삽입 정렬

 

C++

#include <iterator>

template<typename biIter>
void insertion_sort(biIter begin, biIter end)
{
    biIter bond = begin;
    for (++bond; bond!=end; ++bond)
    {
      typename std::iterator_traits<biIter>::value_type key = *bond;
      biIter ins = bond;
      biIter pre = ins;
      for (--pre; ins != begin && *pre > key; *ins-- = *pre--);
      *ins = key;
    }
}

 

 

Java

void insertionSort(int[] arr)
{

   for(int index = 1 ; index < arr.length ; index++){

      int temp = arr[index];
      int aux = index - 1;

      while( (aux >= 0) && ( arr[aux] > temp ) ) {

         arr[aux+1] = arr[aux];
         aux--;
      }
      arr[aux + 1] = temp;
   }
}

 

 

Python

def insert_sort(x):
    for i in range(1, len(x)):
        j = i - 1
        key = x[i]
        while x[j] > key and j >= 0:
            x[j+1] = x[j]
            j = j - 1
        x[j+1] = key
    return x

 


 

삽입 정렬의 장단점

 

장점

  • 알고리즘이 단순하다.
  • 추가적인 메모리 소비가 작다.
  • 거의 정렬 된 경우 매우 효율적이고 최선의 경우 의 시간복잡도를 갖는다.
  • 안정 정렬이 가능하다.

 

단점

  • 비교적 많은 수들의 이동을 포함하며 비교할 수가 많고 크기가 클 경우에 적합하지 않다.
  • 최악의 경우 의 시간복잡도를 갖는다.
  • 데이터의 상태에 따라서 성능 편차가 매우 크다.
반응형
반응형

버블 정렬이란 (거품 정렬, bubble sort)

버블 정렬은 서로 인접한 두 원소의 크기를 비교하여 정렬하는 방법이다.

구현은 간단하나 시간복잡도가 으로 상당히 비효율적이다.

 

 

버블 정렬 예시 및 시간복잡도 증명

간단한 예시를 통해 버블 정렬 과정을 나타내면 다음과 같다.

위 그림에서 확인할 수 있듯이, 5개의 요소를 거품 정렬을 적용해서 정렬하면

비교연산이 4회 + 3회 + 2회 + 1회 = 10회 소요되는 것을 확인할 수 있고,

요소의 개수를 N개로 일반화하면 (N-1) + (N-2) + (N-3) + ... + 2 + 1 가 된다.

 

이를 공차가 1인 등차수열 공식에 대입해보면,

결과적으로 N*(N+1) / 2 임을 알 수 있다.

 

이를 시간복잡도로 표현해보면  임을 확인할 수 있다.

 

버블 정렬 예제 코드 (python)

버블 정렬을 파이썬 코드로 작성하면 다음과 같다.

def bubble_sort(arr):
    for i in range(len(arr)-1, 0, -1):
        for j in range(i):
            if arr[j] > arr[j+1]:
                arr[j], arr[j+1] = arr[j+1], arr[j]
    
    return arr

test = [3,7,1,4,5,6,2,9,8]
print(bubble_sort(test))    # [1, 2, 3, 4, 5, 6, 7, 8, 9] 

 

버블 정렬 성능 개선 방법과 예제 코드(python)

만약 주어진 배열이 어느정도 정렬이 된 상태거나 정렬 진행중에 정렬이 완료된 상태일 경우,

기존 버블 정렬은 불필요한 연산을 진행하게 된다.

 

즉, 더 이상 정렬할 필요가 없어질 때 정렬을 멈추게 한다면

불필요한 연산을 줄임으로써 성능 개선을 할 수 있다.

 

버블 정렬 성능 개선 코드를 파이썬 코드로 작성하면 다음과 같다.

# <성능 개선 방법>
# 단계별로 정렬을 진행 중, 
# 만약 교체가 발생하지 않았다면 정렬이 완료된 것으로 간주하고 멈춘다.
def bubble_sort(array):
    for i in range(len(array)-1, 0, -1):
        quit = True
        for j in range(i):
            if array[j] > array[j+1]:
                array[j], array[j+1] = array[j+1], array[j]
                quit = False
        if quit:
            break
    
    return array

test = [3,7,1,4,5,6,2,9,8]
print(bubble_sort(test))    # [1, 2, 3, 4, 5, 6, 7, 8, 9] 

 

버블정렬 장단점

  • 장점
    • 이해하기 쉬우며, 동시에 구현도 간단하다.
  • 단점
    • 시간복잡도가  이기 때문에 비효율적이다.
반응형
반응형

선택 정렬

선택 정렬이란 제자리 정렬 알고리즘 중 한가지 방법이다.

보통 오름차순으로 정렬되는 선택 정렬은 다음과 같은 과정으로 이루어져 있다.

  1. 가장 작은 원소 값을 찾는다.
  2. 그 값을 맨 앞에 있는 값과 교체를 한다.
  3. 맨 처음 위치를 뺀 나머지 배열을 같은 방법으로 교체한다.
  4. 하나의 원소만 남을 때까지 위의 과정을 반복한다.

 

선택 정렬 예시

 

예시를 통해 좀 더 자세히 살펴보자.

위 그림과 같이, [5, 3, 8, 1, 2, 7] 로 구성된 배열이 있다고 하자.

  1. 앞에서부터 탐색하여, 가장 작은 원소 1을 찾은 후 맨 앞의 값 5와 교체한다.
  2. 1의 위치를 확정짓고 [3,8,5,2,7] 중 가장 작은 수 2를 맨 앞의 값 3과 교체한다.
  3. 같은 방법으로 [8,5,3,7] 중 가장 작은 수 3과 맨 앞의 값 8을 교체한다.
    ....
    마지막 원소까지 같은 방법으로 진행하면
    최종적으로 [1, 2, 3, 5, 7, 8] 이라는 정렬된 배열이 완성된다.

 

선택정렬 구현 및 시간복잡도 및 공간복잡도

void selectionSort(int[] arr) {
    int indexMin, temp;
    for (int i = 0; i < arr.length-1; i++) { // 1.
        indexMin = i;
        for (int j = i + 1; j < arr.length; j++) { // 2.
            if (arr[j] < arr[indexMin]) { // 3.
                indexMin = j;
            } 
        } // 4.swap(arr[indexMin], arr[i]) 
        temp = arr[indexMin]; 
        arr[indexMin] = arr[i];
        arr[i] = temp; 
    } 
    System.out.println(Arrays.toString(arr)); 
}

이 위의 코드처럼, 선택정렬은 데이터가 n 개일 때

첫번째 회전에서의 비교 횟수 : 1 ~ n-1 => n-1개

두번째 회전에서의 비교 횟수 : 2 ~ n-1 => n-2개

 

결국, n(n-1)/2개의 시간이 걸리게 되고

n개의 원소를 가진 배열을 정렬하는데 O(n^2) 만큼의 시간이 걸린다.

 

TIP

최선, 평균, 최악의 경우 시간복잡도는 O(n^2)로 동일하다.

선택정렬은 주어진 배열 안에서 교환을 통하여 정렬되는 제자리 정렬 이므로,

공간복잡도는 O(n) 이다.

 

선택 정렬의 장단점

장점

  1. 알고리즘이 단순하다.
  2. 정렬을 위한 비교 횟수는 많지만, 거품 정렬에 비해서 교환하는 횟수가 적기 때문에
    많은 교환이 일어나야 하는 자료에서는 효율적이다.
  3. 다른 메모리 공간이 필요하지 않는다.

단점

  1. 시간복잡도가 O(n^2)이므로 비효율적이다.
반응형
반응형

트라이란?

문자열 집합에서 어느 한 개의 문자열을 탐색하는 알고리즘은 무엇이 있을까?

 

최대 길이가 m인 문자열 n개의 집합이 있다고 가정해 보자.

 

가장 간단하지만 시간복잡도가 높은 방식은 무작정 순회를 돌며 찾는 방식인 brute force이다.

최대 길이가 m인 문자열 n개의 집합에서, 원하는 문자열이 있는지 찾는다면 비교횟수는 O(mn)이 된다.

 

이를 좀더 개선하는 방식으로 이진 탐색을 활용하면 좀더 낮은 시간복잡도를 갖게 된다.

이진 탐색을 사용하면 O(mlogn)으로 단축 시킬 수 있지만,

정렬 과정 자체에 O(nmlogn)의 시간이 걸려서

이 또한 비효율적인 방식이라고 볼 수 있다.

 

이를 모두 개선하기 위한 문자열 집합 검색법이 바로 트라이(Trie)라는 자료구조이다.

 

이를 트라이 알고리즘이 아닌 자료구조라는 말을 사용하는 이유는

트라이가 갖는 트리 종류를 보고 부르는 것인데 이는 잠시 후에 알아보도록 하고 트라이의 기원부터 살펴보도록 하자.

 

트라이의 개념은 1959년 René de la Briandais가 처음 설명하였고 처음에는 tri라고 불렸다가 tree의 이름과 헷갈려서

나중에 에드워드 프레드킨이 trie(트라이)라는 이름으로 명명하였다.

 

트라이 자료구조

이제 트라이가 왜 자료구조인지, 그 트라이 자료구조에 대해 아래 그림과 함께 살펴보도록 하자.

다음과 같은 문자열 집합이 있다고 해보자.

{"A", "to", "tea", "ted", "ten", "i", "in", "inn"} 

 

이 문자열을 트리형태로 한글자씩 따서 그 깊이만큼 만들면 위 그림과 같은 트리구조를 형성한다.

트라이 자료구조를 만들 때

삽입은 O(m)의 시간복잡도로 주어진 문자열을 트라이 자료구조로 만들 수 있다.

또한 검색의 경우에도 O(m)의 시간복잡도로 검색이 가능하다.

 

이러한 삽입과 검색의 연산을 파이썬 구현으로 확인해 보자.

class Node(object):
    def __init__(self, key, data=None):
        self.key = key
        self.data = data
        self.children = {}


class Trie(object):
    def __init__(self):
        self.head = Node(None)

    # 삽입 : head노드 부터 시작하여 문자열의 길이 만큼 돌며 자리를 찾는다.
    # 시간복잡도 O(M), M : string의 길이
    def insert(self, string):
        cur_node = self.head
        for char in string:
            # 자식노드에 해당 문자가 없으면 해당 문자로 자식노드를 만들어준다.
            if char not in cur_node.children:
                cur_node.children[char] = Node(char)
            cur_node = cur_node.children[char]
        # 마지막 노드까지 온 후 데이터를 string으로 해준다.
        cur_node.data = string

    # 검색
    # 시간복잡도 O(M), M : string의 길이
    def search(self, string):
        cur_node = self.head
        for char in string:
            if char in cur_node.children:
                cur_node = cur_node.children[char]
            else:
                return False
        # 탐색이 끝난 후
        # 해당 노드의 data가 있다면 문자가 포함되어 있다는 뜻
        if cur_node.data != None:
             return True

트라이 특징

앞서 말한대로 트라이는 문자길이 m을 갖는 문자열을 O(m)의 시간으로 트라이 자료구조로 만들 수 있고,

탐색할 때도 마찬가지로 O(m)의 시간으로 탐색이 가능하다.

 

하지만 트라이는 이러한 자료구조를 구성하는 공간복잡도가 엄청나게 많이 필요하게 된다.

문자열이라면 a~z 총 26개의 포인터 배열을 1 depth마다 갖게 될 수도 있기 때문이다.

 

이러한 특성 때문에 트라이의 공간복잡도는 0(포인터크기 * 포인터 배열 개수 * 트라이에 존재하는 총 노드의 개수)가 된다.

 

이를 해결하기 위해 비트 트라이, 트라이 압축, 외부 기억장치 트라이등의 다양한 구현전략이 등장하게 되었다.

이러한 구현전략에 좀 더 자세히 알고 싶다면 여기에서 한번 확인하길 바란다.

 

하지만 일반적인 알고리즘 풀이에서는 이러한 구현전략들을 사용하기가 어렵기 때문에,

트라이를 사용할 때는 꼭 문자열 집합중 문자열 1개의 최대길이가 몇까지 가능한지를 확인하고

그 길이가 작다면 트라이를 사용하는 것을 추천하는 바이다.

 

트라이 사용

트라이는 자동완성, 정렬, 전문 검색등에서 사용이 가능하다.

  • 자동완성 : 주어진 접두사를 가진 리스트를 반환하는데 활용할 수 있다.
  • 정렬 : 키 집합으로 하위노드가 정렬된 트라이를 만들면 집합 전체를 사전순으로 정렬할 수 있다.
  • 전문 검색 : 트라이의 특수한 형태로써 접미사 트리(suffix tree)라고 불리는 것은 빠른 전문(full-text) 검색에 활용하기 위해 모든 접미사를 색인하는데 이용할 수 있다.

 

트라이 관련 문제와 관련 주제

문자열 관련 알고리즘은 많은데 문자열 알고리즘 - 나무위키사이트에 궁금하다면 들어 가보는 것도 좋을 것 같다.

반응형

'CS > 자료구조' 카테고리의 다른 글

[자료구조] 해시 테이블  (0) 2023.02.16
[자료구조] 레드 블랙 트리  (0) 2023.02.16
[자료구조] B-Tree & B+Tree  (0) 2023.02.16
[자료구조] 이진탐색트리  (0) 2023.02.16
[자료구조] 힙(Heap) 에 대하여  (0) 2023.02.16

+ Recent posts