계수 정렬 알고리즘의 단계 안내

비교를 바탕으로 하는 모든 알고리즘이 n개 요소의 수조를 정렬하는 시간 복잡도는 O(nlogn)이지만 일부 가설이 검증되면 선형 시간으로 실행되는 정렬 알고리즘이 존재한다.
계수 정렬은 바로 이러한 알고리즘 중의 하나로, 이 글은 그것의 실현에 차근차근 지침을 제공했다.
이 게시물은 최초로 나의 개인 홈페이지에 게재되었다website.너는 여기서 알고리즘을 직관적으로 해석한 슬라이드를 찾을 수 있다.
계수 정렬은 선결 조건을 적용해야 한다.n개의 입력 요소 중 하나는 유한한 범위 내의 정수 또는 이 범위 내의 정수 키가 있어야 한다.
현재 우리는 범위가 [0,k]라고 가정한다.

생각


그 기본 사상은 모든 키가 입력 수조에 나타나는 횟수(예를 들어 그 주파수)는 원소가 정렬 출력 수조에 있는 위치를 확정하는 데 사용할 수 있다는 것이다.
이게 어떻게 가능하지?
입력 요소의 키가 c, c<=k와 같다고 가정하십시오. 입력 그룹이 m개의 키예를 들면 더 잘 이해하는 데 도움이 된다.[1,1,2,4,3,0,1,2,3,1]로 입력하면 다섯 개의 키가 2보다 작다.따라서 앞의 2개는 출력 [0,1,1,1,1,2,2,3,4]의 위치 5에 들어간다.

계산 주파수


모든 키의 주파수는 크기가 k+1과 같은 부기수 그룹을 사용하여 계산하고 0으로 초기화할 수 있습니다.입력 수조의 키가 [0,k] 범위 내에 있기 때문에 우리는 이를 교체하여 부기 수조의 인덱스로 사용할 수 있다.
교체 과정에서, 우리는 키 인덱스로 된 부기 요소를 추가했다.마지막으로 부기수 그룹의 모든 요소는 키의 주파수와 해당하는 인덱스를 저장합니다.

구성 정렬 출력


만약 추가 값이 없는 정수 키만 있다면, 정렬 출력을 구축하는 것은 매우 간단할 것이다.우리는 bookkeping 그룹을 교체할 수 있으며, 키의 출현 횟수마다 정렬된 출력을 채울 수 있다.
void CountingSort(vector<int>& keys) {

    int max_key = *max_element(keys.begin(), keys.end());

    vector<int> bookkeeping(max_key + 1);

    for (int key : keys) bookkeeping[key]++;

    for (int i = 0, k = 0; i <= max_key; ++i) {
        for (int j = 0; j < bookkeeping[i]; ++j) {
            keys[k++] = i;
        }
    }
}
입력 요소는 키 값이 맞는 일반적인 상황보다 더 복잡합니다.정렬 출력에서 각 입력 요소의 위치를 미리 계산할 필요가 있습니다.
가장 간단한 방법은 다른 진열을 세우는 것이다.우리는 부기수 그룹을 사용해서 하나의 그룹을 만들 수 있으며, 이 그룹은 다음에 나타날 키가 정렬 출력에 있는 위치를 알려줄 것이다.
우리는 이 그룹을 nxt index라고 할 수 있다. 왜냐하면 i번째 요소는 다음 입력 요소가 정렬된 출력의 위치를 표시하기 때문이다.다음 색인 배열 작업을 구성하려면 다음 두 가지를 고려할 수 있습니다.
  • 키가 i인 첫 번째 원소의 출력 위치는 키가 에 대응한다
  • 인덱스가 [0, i-1] 범위 내의 부기 서브 그룹은 키를 포함한다.
    따라서 다음 인덱스 그룹의 i번째 요소는 부기 그룹에서 인덱스 i-1까지의 누적과 대응한다.부기와 다음 색인 그룹의 크기가 어떻게 같은지 주의하십시오.
    마지막 단계는 다음 색인 그룹을 사용하여 정렬된 출력을 구축하는 것입니다.

    next 인덱스로 정렬 출력 만들기


    현재, 우리는 입력 그룹을 두루 훑어보고, 모든 키를 다음 인덱스 그룹의 인덱스로 사용해서 출력 그룹의 위치를 얻을 수 있습니다.
    현재 요소가 출력 위치에 배치되면 키 인덱스의 다음 인덱스 요소가 증가합니다.이것은 항상 변하지 않습니다. 즉, nxt 인덱스의 i번째 요소는 다음 입력 요소가 정렬 출력에서 키 i의 위치를 나타냅니다.
    일단 이 교체가 완성되면, 우리는 정렬된 출력 그룹을 입력 그룹으로 복사할 수 있다.
    vector<pair<int, string>> CountingSort(vector<pair<int, string>>& items) {
    
        int max_key = max_element(items.begin(), items.end(), 
                      [](auto const& x, auto const& y) {
                          return x.first < y.first; 
                        })->first;
    
        vector<int> bookkeeping(max_key+1, 0);
    
        //counter[i] corresponds to the number of entries with key equal to i
        for (const auto& item : items) {
            bookkeeping[item.first]++;
        }
    
        //nextIndex[i] corresponds to the number of entries with key less than i
        vector<int> next_index(max_key+1, 0);
        for (int i = 1; i < next_index.size(); ++i) {
            next_index[i] = next_index[i-1] + bookkeeping[i-1];
        }
    
        vector<pair<int, string>> output(items.size());
        for (const auto& item : items) output[next_index[item.first]++] = item;
    
        return output;
    }
    

    공간 최적화


    우리는 단독 넥스트 인덱스 그룹을 만드는 것이 아니라 메모리 공간을 절약하기 위해 부기 그룹을 직접 사용할 수 있다.이 점을 이해하기 위해 생각해 보자.
  • 키가 i인 마지막 원소의 위치는 키가 <=i인 원소의 수량
  • 에 대응한다
    2️. 인덱스 [0, i] 범위 내의 부기 서브 그룹은 키 <=i 요소의 주파수를 포함합니다
    따라서 인덱스 i에 대한 부기수 그룹의 누적과 정렬 출력의 마지막 입력 요소에 대응하는 위치 (1 추가).부기수 그룹의 누적과
    다음에, 우리는 출력 그룹의 위치를 얻기 위해, 모든 키를 부기 그룹의 인덱스로 뒤로 교체해서 입력할 수 있다.
    현재 요소를 출력 위치에 두기 전에 인덱스 키를 누르면 부기가 감소합니다.이렇게 하면 다음 인덱스의 i번째 요소는 정렬된 출력에서 마지막 입력 요소의 위치를 나타냅니다(1).
    vector<pair<int, string>> CountingSort(vector<pair<int, string>>& items) {
    
        int max_key = max_element(items.begin(), items.end(), 
                      [](auto const& x, auto const& y) {
                        return x.first < y.first; 
                        })->first;
    
        vector<int> bookkeeping(max_key + 1, 0);
    
        //count keys frequency
        for (const auto& item : items) {
            bookkeeping[item.first]++;
        }
    
        //at the end each element is the index of the last element with that key
        std::partial_sum(bookkeeping.begin(), bookkeeping.end(), bookkeeping.begin());
    
        vector<pair<int, string>> output(items.size());
    
        //build sorted output iterating backward
        for (auto it = items.crbegin(); it != items.crend(); ++it) {
            output[--bookkeeping[it->first]] = *it;
        }
    
        return output;
    }
    

    범용 범위까지 확장


    우리는 항상 모든 키가 정수이고 0에서 어떤 최대치 k 사이에 포함된다고 가정한다.
    사실 이것은 전제가 아니다.우리는 계수 정렬을 임의의 정수 범위의 키에 적용할 수 있다.비결은 최소 원소를 찾아 최소 원소의 주파수를 0 인덱스에 저장하는 것이다.

    등록 정보


    계수 정렬의 중요한 특징은 안정적인 것이다. 같은 키를 가진 요소는 출력 그룹에서 입력 그룹과 같은 순서로 나타난다.
    점근 분석에 대해 우리는 다음과 같이 생각한다.
  • 교체 입력 수조와 기장 수조
  • 의 시간 복잡도는 O(n+k)
  • 메모리 부기수 그룹의 공간 복잡도는 O(k)이다.
  • 일반적으로 정렬할 항목의 수량은 이 항목들이 사용할 수 있는 키의 수량과 점진적인 차이가 없다.이러한 상황에서 k는 O(n)로 변하고 전체 알고리즘의 시간 복잡도는 O(n)이다.어쨌든 이것은 항상 유효하지 않다(즉 입력 = [10,1,2,4,3,0,1,2,3,1]).

    파이썬 구현


    def countingSort(input):
    
        maxKey= max(input, key=lambda item:item[0])[0]
    
        bookeepingLength = maxKey+1
        bookeeping = [0] * bookeepingLength
    
        # Count keys frequency
        for item in input: 
            bookeeping[item[0]] += 1
    
        # at the end each element is the index 
        # of the last element with that key
        for i in range(1, bookeepingLength):
            bookeeping[i] += bookeeping[i-1] 
    
        output = [0] * len(input)
    
        # build sorted output iterating backward
        i = len(input) - 1
        while i >= 0:
            item = input[i]
            bookeeping[item[0]] -= 1
            position = bookeeping[item[0]]
            output[position] = item
            i -= 1
    
        return output
    

    C# 구현


    class CountingSort {
    
            private static int[] PartialSum(int[] input)
            {
                for (int i = 1; i < input.Count(); i++)
                {
                    input[i] = input[i] + input[i - 1];
                }
    
                return input;
            }
    
            public static (int, string)[] CountingSort((int, string)[] items)
            {
                int max_key = items.Max(t => t.Item1);
                var bookkeeping = new int[max_key + 1];
    
                //count keys frequency
                foreach (var item in items) {
                    bookkeeping[item.Item1]++;
                }
    
                //at the end each element is the index of the last element with that key
                bookkeeping = PartialSum(bookkeeping);
    
                var output = new (int, string)[items.Length];
    
                //build sorted output iterating backward
                for (int i = items.Length - 1; i >= 0; i--) {
                    output[--bookkeeping[items[i].Item1]] = items[i];
                }
    
                return output;
            }
    }
    

    결론


    계수 정렬은 기능이 강하고 매우 효율적인 알고리즘이다.그것의 기본 사상은 매우 간단하지만, 실현하기에는 매우 까다로울 수 있으니 주의해야 한다.
    이 글은 계수 정렬 알고리즘을 실현하는 단계별 지침을 제공했다.
    이 알고리즘은 다양한 프로그래밍 언어(C++, C#, Python)에서 내 GitHub 저장소에서 얻을 수 있다.
    만약 당신이 이 글을 좋아한다면, 나를 따라 매일 더 많은 관련 내용을 얻으세요!

    좋은 웹페이지 즐겨찾기