내부 기수 정렬


200

긴 글입니다. 저를 참아주세요. 삶은 기수 정렬 알고리즘이 있습니까?


예비

나는 정렬하고 싶은 문자 "A", "C", "G"및 "T"(예, 당신은 짐작했습니다 : DNA ) 만 사용하는 작은 고정 길이 문자열이 많이 있습니다 .

현재 STL 의 모든 일반적인 구현에서 introsort 를 사용 std::sort하는을 사용합니다 . 이것은 꽤 잘 작동합니다. 그러나 기수 정렬이 내 문제 세트에 완벽하게 부합하고 실제로 훨씬 더 잘 작동한다고 확신합니다 .

세부

나는 매우 순진한 구현 으로이 가정을 테스트했으며 비교적 작은 입력 (약 10,000 정도)에 대해서는 이것이 사실입니다 (최소한 두 배 이상 빠름). 그러나 문제 크기가 커지면 런타임이 크게 저하됩니다 ( N > 5,000,000).

기수 정렬은 전체 데이터를 복사해야합니다 (실제로 순진한 구현에서는 두 번 이상). 이것은 ~ 4GiB를 메인 메모리에 넣어 성능을 저하시키는 것을 의미합니다. 그렇지 않은 경우에도 문제 크기가 실제로 더 커지기 때문에이 많은 메모리를 사용할 여유가 없습니다.

사용 사례

이상적으로이 알고리즘은 DNA 및 DNA5 (추가 와일드 카드 문자 "N"허용) 또는 IUPAC 모호성 코드 (16 개의 다른 값)가있는 DNA의 경우 2에서 100 사이의 모든 문자열 길이에서 작동해야합니다 . 그러나 이러한 모든 경우를 다룰 수는 없다는 점을 알고 있으므로 속도 향상에 만족합니다. 코드는 어떤 알고리즘을 디스패치할지 동적으로 결정할 수 있습니다.

연구

불행하게도 기수 정렬에 대한 Wikipedia 기사 는 쓸모가 없습니다. 전체 변형에 대한 섹션은 완전 쓰레기입니다. 기수 정렬NIST-DADS 섹션 은 존재하지 않는 옆에 있습니다. 알고리즘“MSL”을 설명하는 Efficient Adaptive In-Place Radix Sorting 이라는 유망한 소리의 논문이 있습니다. 불행하게도이 논문 역시 실망 스럽다.

특히 다음과 같은 것들이 있습니다.

첫째, 알고리즘에는 몇 가지 실수가 포함되어 있으며 설명 할 수없는 부분이 많습니다. 특히, 재귀 호출을 자세히 설명하지 않습니다 (현재 쉬프트 및 마스크 값을 계산하기 위해 포인터를 늘리거나 줄이는 것으로 가정합니다). 또한, 기능 사용 dest_groupdest_address정의를 제공하지 않고 있습니다. 나는 이것을 효율적으로 구현하는 방법을 보지 못했습니다 (즉, O (1); 적어도 dest_address사소하지는 않습니다).

마지막으로,이 알고리즘은 배열 인덱스를 입력 배열 내부의 요소와 교체하여 적절한 위치에 도달합니다. 이것은 분명히 숫자 배열에서만 작동합니다. 문자열에 사용해야합니다. 물론, 나는 강한 타이핑을 망쳐 놓고 메모리가 내 인덱스가 아닌 인덱스를 저장할 수 있다고 가정 할 수 있습니다. 그러나 이것은 32 비트 정수로 가정하여 문자열을 32 비트 메모리로 압축 할 수있는 한 작동합니다. 16 자에 불과합니다 (16> log (5,000,000) 인 순간 무시).

저자 중 한 사람의 다른 논문은 전혀 정확한 설명을 제공하지는 않지만 MSL의 런타임을 하위 선형으로 제공하여 잘못 전개됩니다.

요약 : DNA 문자열에서 작동하는 작업 기준 구현 또는 작업 가능한 기수 정렬에 대한 의사 코드 / 설명을 찾을 희망이 있습니까?


65
그것은 훌륭하게 쓰여진 질문입니다.
JustinT

1
작은 고정 길이 줄은 얼마나 작습니까?
EvilTeach

1
@EvilTeach : 사용 사례를 추가했습니다.
Konrad Rudolph

2
@ Stephan : 이것은 모두 훌륭합니다. 그러나 복사 / 캐시 미스의 경우 지연이 발생합니다. 기억의 경우 나는 물리적 한계에 부딪쳤다. 이것은 협상이 불가능합니다. 디스크에 데이터의 일부를 저장하는 모든 멋진 기술은 현재 빠른 정렬 솔루션보다 속도가 느립니다.
Konrad Rudolph

2
반면에 dsimcha의 솔루션 은 일부 입력의 퀵 정렬보다 확실히 빠릅니다 . 이동 횟수가 많고 캐시 지역이 적을 수 있지만 실제 환경에서는 여전히 좋습니다. 또한 수행해야 할 스왑 수를 줄이기 위해 솔루션을 약간 조정했습니다.
Konrad Rudolph

답변:


61

자, 여기 DNA를위한 MSD 기수 정렬의 간단한 구현이 있습니다. D 언어로 작성되었습니다. 왜냐하면 제가 가장 많이 사용하는 언어이므로 어리석은 실수를 할 가능성이 가장 적지 만 다른 언어로 쉽게 번역 될 수 있습니다. 제자리에 있지만 2 * seq.length어레이를 통과 해야 합니다.

void radixSort(string[] seqs, size_t base = 0) {
    if(seqs.length == 0)
        return;

    size_t TPos = seqs.length, APos = 0;
    size_t i = 0;
    while(i < TPos) {
        if(seqs[i][base] == 'A') {
             swap(seqs[i], seqs[APos++]);
             i++;
        }
        else if(seqs[i][base] == 'T') {
            swap(seqs[i], seqs[--TPos]);
        } else i++;
    }

    i = APos;
    size_t CPos = APos;
    while(i < TPos) {
        if(seqs[i][base] == 'C') {
            swap(seqs[i], seqs[CPos++]);
        }
        i++;
    }
    if(base < seqs[0].length - 1) {
        radixSort(seqs[0..APos], base + 1);
        radixSort(seqs[APos..CPos], base + 1);
        radixSort(seqs[CPos..TPos], base + 1);
        radixSort(seqs[TPos..seqs.length], base + 1);
   }
}

분명히 이것은 일반적인 것이 아니라 DNA와 관련이 있지만 빠르지 않아야합니다.

편집하다:

이 코드가 실제로 작동하는지 궁금해서 자신의 생체 정보 코드가 실행되기를 기다리는 동안 테스트 / 디버깅했습니다. 위의 버전은 실제로 테스트되어 작동합니다. 각각 5 개의 염기로 구성된 천만 개의 시퀀스에 대해 최적화 된 도입부보다 약 3 배 빠릅니다.


9
2x 패스 접근 방식으로 살 수 있다면 기수 -N까지 확장됩니다. pass 1 = 그냥지나 가서 각 N 자리수를 세십시오. 그런 다음 배열을 분할하는 경우 각 숫자가 시작되는 위치를 알려줍니다. 패스 2는 어레이의 적절한 위치로 교체합니다.
Jason S

(예 : N = 4의 경우 90000 A, 80000 G, 100 C, 100000 T가있는 경우 APos 대신 사용되는 누적 합 = [0, 90000, 170000, 170100]으로 초기화 된 배열을 만듭니다. 각 자릿수의 다음 요소를 바꾸어야하는 커서로 CPos 등.)
Jason S

나는 확실히 이진 표현이 문자열 표현 사이의 관계는 따로 필요에 따라 많은 메모리로 적어도 4 배를 사용, 될 것입니다 아니에요 무엇을
스테판 EGGERMONT

더 긴 시퀀스의 속도는 어떻습니까? 당신은 5의 길이만큼 다른 사람이없는
스테판 EGGERMONT

4
이 기수 정렬은 잘 알려진 기수 정렬 변형 인 미국 국기 정렬의 특수한 경우 인 것 같습니다.
Edward KMETT

21

나는 기수 정렬을 본 적이 없으며 기수 정렬의 특성상 임시 배열이 메모리에 들어가는 한 소외 정렬보다 훨씬 빠르다는 것을 의심합니다.

이유:

정렬은 입력 배열에서 선형 읽기를 수행하지만 모든 쓰기는 거의 임의적입니다. 특정 N 이상에서 이것은 쓰기 당 캐시 미스로 귀결됩니다. 이 캐시 미스로 인해 알고리즘 속도가 느려집니다. 제자리에 있는지 여부는이 효과를 변경하지 않습니다.

이것이 귀하의 질문에 직접 대답하지는 않지만 정렬이 병목 현상 인 경우 전처리 단계정렬 알고리즘 근처를 살펴보고 싶을 수도 있습니다 (소프트 힙의 wiki 페이지가 시작될 수 있습니다).

캐시 로컬 성이 향상 될 수 있습니다. 그러면 교과서에서 벗어난 기수 정렬이 더 잘 수행됩니다. 쓰기는 여전히 거의 임의적이지만 적어도 동일한 메모리 청크 주위에 클러스터링되어 캐시 적중률이 증가합니다.

그래도 실제로 작동하는지는 알 수 없습니다.

Btw : DNA 문자열 만 다루는 경우 : 문자를 두 비트로 압축하고 데이터를 많이 포장 할 수 있습니다. 이렇게하면 기본 표현에 비해 메모리 요구 사항이 4 배 줄어 듭니다. 어드레싱은 더 복잡해 지지만, CPU의 ALU는 모든 캐시 미스 기간 동안 많은 시간을 소비합니다.


2
두 가지 좋은 점; 정렬은 저에게 새로운 개념입니다, 나는 그것에 대해 읽어야 할 것입니다. 캐시 미스는 내 꿈을 괴롭히는 또 다른 고려 사항입니다. ;-) 이것에 대해보아야 할 것입니다.
Konrad Rudolph

그것은 나에게도 새로운 것이지만 (두 달), 일단 개념을 얻은 후에는 성능 향상 기회를보기 시작합니다.
Nils Pipenbrinck

기수가 매우 크지 않으면 쓰기는 거의 임의적 이지 않습니다. 예를 들어, 한 번에 하나의 문자를 정렬한다고 가정하면 (기수 -4 정렬) 모든 쓰기는 4 개의 선형 확장 버킷 중 하나에 이루어집니다. 이것은 캐시와 프리 페치 친화적입니다. 물론, 더 큰 기수를 사용하고 싶을 수도 있고, 어떤 포인터에서는 캐시와 프리 페치 친 화성 및 기수 크기 사이의 균형을 맞 춥니 다. 소프트웨어 프리 페치 또는 "실제"버킷으로 주기적으로 플러시되는 버킷의 스크래치 영역을 사용하여 손익 분기점을 더 큰 방사로 밀어 낼 수 있습니다.
BeeOnRope

8

시퀀스를 비트 단위로 인코딩하여 메모리 요구 사항을 확실히 제거 할 수 있습니다. 16 개의 상태 또는 4 비트 인 "ACGT"를 사용하여 길이 2에 대한 순열을보고 있습니다. 길이 3의 경우 64 개 상태이며 6 비트로 인코딩 할 수 있습니다. 따라서 시퀀스의 각 문자에 대해 2 비트 또는 16 문자에 대해 약 32 비트처럼 보입니다.

유효한 '단어'의 수를 줄이는 방법이 있다면 추가 압축이 가능할 수 있습니다.

따라서 길이가 3 인 시퀀스의 경우 크기가 uint32 또는 uint64 인 64 개의 버킷을 만들 수 있습니다. 그것들을 0으로 초기화하십시오. 매우 큰 3 개의 문자 시퀀스 목록을 반복하고 위와 같이 인코딩하십시오. 이것을 첨자로 사용하고 해당 버킷을 늘리십시오.
모든 시퀀스가 ​​처리 될 때까지이 과정을 반복하십시오.

다음으로 목록을 재생성하십시오.

64 개의 버킷을 순서대로 반복하여 해당 버킷에서 발견 된 수에 대해 해당 버킷으로 표시되는 시퀀스의 많은 인스턴스를 생성합니다.
모든 버킷이 반복되면 정렬 된 배열이 있습니다.

4의 시퀀스는 2 비트를 추가하므로 256 개의 버킷이 있습니다. 5의 시퀀스는 2 비트를 추가하므로 1024 개의 버킷이 있습니다.

어느 시점에서 버킷 수가 한계에 도달합니다. 파일에서 시퀀스를 메모리에 보관하지 않고 읽는 경우 버킷에 더 많은 메모리를 사용할 수 있습니다.

버킷이 작업 세트에 맞을 가능성이 있기 때문에 현장에서 정렬하는 것보다 이것이 더 빠를 것이라고 생각합니다.

기술을 보여주는 해킹입니다.

#include <iostream>
#include <iomanip>

#include <math.h>

using namespace std;

const int width = 3;
const int bucketCount = exp(width * log(4)) + 1;
      int *bucket = NULL;

const char charMap[4] = {'A', 'C', 'G', 'T'};

void setup
(
    void
)
{
    bucket = new int[bucketCount];
    memset(bucket, '\0', bucketCount * sizeof(bucket[0]));
}

void teardown
(
    void
)
{
    delete[] bucket;
}

void show
(
    int encoded
)
{
    int z;
    int y;
    int j;
    for (z = width - 1; z >= 0; z--)
    {
        int n = 1;
        for (y = 0; y < z; y++)
            n *= 4;

        j = encoded % n;
        encoded -= j;
        encoded /= n;
        cout << charMap[encoded];
        encoded = j;
    }

    cout << endl;
}

int main(void)
{
    // Sort this sequence
    const char *testSequence = "CAGCCCAAAGGGTTTAGACTTGGTGCGCAGCAGTTAAGATTGTTT";

    size_t testSequenceLength = strlen(testSequence);

    setup();


    // load the sequences into the buckets
    size_t z;
    for (z = 0; z < testSequenceLength; z += width)
    {
        int encoding = 0;

        size_t y;
        for (y = 0; y < width; y++)
        {
            encoding *= 4;

            switch (*(testSequence + z + y))
            {
                case 'A' : encoding += 0; break;
                case 'C' : encoding += 1; break;
                case 'G' : encoding += 2; break;
                case 'T' : encoding += 3; break;
                default  : abort();
            };
        }

        bucket[encoding]++;
    }

    /* show the sorted sequences */ 
    for (z = 0; z < bucketCount; z++)
    {
        while (bucket[z] > 0)
        {
            show(z);
            bucket[z]--;
        }
    }

    teardown();

    return 0;
}

해시 할 수있는 이유를 비교해보십시오.
wowest

1
젠장. 성능은 일반적으로 모든 DNA 처리에서 문제가됩니다.
EvilTeach

6

데이터 세트가 너무 크면 디스크 기반 버퍼 방식이 가장 좋을 것이라고 생각합니다.

sort(List<string> elements, int prefix)
    if (elements.Count < THRESHOLD)
         return InMemoryRadixSort(elements, prefix)
    else
         return DiskBackedRadixSort(elements, prefix)

DiskBackedRadixSort(elements, prefix)
    DiskBackedBuffer<string>[] buckets
    foreach (element in elements)
        buckets[element.MSB(prefix)].Add(element);

    List<string> ret
    foreach (bucket in buckets)
        ret.Add(sort(bucket, prefix + 1))

    return ret

예를 들어 문자열이 다음과 같은 경우 더 많은 수의 버킷으로 그룹화하는 방법을 실험합니다.

GATTACA

첫 번째 MSB 호출은 GATT에 대한 버킷 (총 버킷 256 개)을 반환하여 디스크 기반 버퍼의 분기를 줄입니다. 이는 성능을 향상시킬 수도 있고 향상시키지 않을 수도 있으므로 실험 해보십시오.


일부 응용 프로그램에는 메모리 매핑 파일을 사용합니다. 그러나 일반적으로 머신은 명시적인 디스크 백업을 요구하지 않을 정도로 RAM을 거의 제공하지 않는다는 가정하에 작업합니다 (물론 스왑은 여전히 ​​발생 함). 그러나 우리는 이미 자동 디스크 백업 어레이를위한 메커니즘을 개발하고 있습니다
Konrad Rudolph

6

나는 사지로 나가서 heap / heapsort 구현으로 전환 할 것을 제안합니다 . 이 제안에는 몇 가지 가정이 있습니다.

  1. 데이터 읽기를 제어합니다
  2. 정렬 된 데이터를 '시작'하자마자 정렬 된 데이터로 의미있는 작업을 수행 할 수 있습니다.

힙 / 힙 정렬의 장점은 데이터를 읽는 동안 힙을 빌드 할 수 있으며 힙을 빌드 한 순간에 결과를 얻을 수 있다는 것입니다.

물러서 자 운이 좋으면 데이터를 비동기 적으로 읽을 수있는 경우 (즉, 일종의 읽기 요청을 게시하고 일부 데이터가 준비되면 알림을받을 수 있음) 대기하는 동안 힙 청크를 빌드 할 수 있습니다. 디스크에서도 데이터를 가져올 수 있습니다. 종종이 접근 방식은 데이터를 가져 오는 데 걸리는 시간보다 정렬 비용의 절반의 비용을 대부분 묻을 수 있습니다.

데이터를 읽은 후에는 첫 번째 요소를 이미 사용할 수 있습니다. 데이터를 보내는 위치에 따라 이것은 좋을 수 있습니다. 다른 비동기식 리더 나 병렬 '이벤트'모델 또는 UI로 보내면 청크와 청크를 보낼 수 있습니다.

즉, 데이터를 읽는 방법을 제어 할 수없고 동 기적으로 읽히고 완전히 쓰여질 때까지 정렬 된 데이터를 사용하지 않으면이 모든 것을 무시하십시오. :(

Wikipedia 기사를 참조하십시오.


1
좋은 제안. 그러나 이미 시도했지만 내 경우에는 힙을 유지 관리하는 오버 헤드가 벡터의 데이터를 누적하고 모든 데이터가 도착하면 정렬하는 것보다 큽니다.
Konrad Rudolph


4

성능 측면에서보다 일반적인 문자열 비교 정렬 알고리즘을 살펴볼 수 있습니다.

현재는 모든 현의 모든 요소를 ​​만지고 있지만 더 잘할 수 있습니다!

특히 버스트 정렬 은이 경우에 매우 적합합니다. 보너스로, burstsort는 시도를 기반으로하기 때문에 DNA / RNA에 사용되는 작은 알파벳 크기에 대해 엄청나게 잘 작동합니다. 왜냐하면 어떤 종류의 삼항 검색 노드, 해시 또는 기타 트라이 노드 압축 체계를 구축 할 필요가 없기 때문입니다. 트라이 구현. 시도는 접미사 배열과 같은 최종 목표에도 유용 할 수 있습니다.

버스트 소트의 적절한 범용 구현은 소스 포지에서 http://sourceforge.net/projects/burstsort/ 에서 사용할 수 있지만 제자리에 있지는 않습니다.

비교 목적으로 C-burstsort 구현은 http://www.cs.mu.oz.au/~rsinha/papers/SinhaRingZobel-2006.pdf 에서 다루었으며 일부 일반적인 작업에서는 퀵 정렬 및 기수 정렬보다 4-5 배 빠릅니다.


버스트 정렬을 확실히 살펴 봐야합니다. 현재로서는 트라이가 어떻게 제자리에 구축 될 수 있는지는 알 수 없습니다. 일반적으로 접미사 배열은 실제 응용 분야에서 우수한 성능 특성으로 인해 생물 정보학에서 접미사 트리를 대체하지 않고 대체하여 시도합니다.
Konrad Rudolph

4

Drs의 대규모 게놈 염기 서열 처리 를 살펴보고 싶을 것 입니다. 카사하라와 모리시타.

4 개의 뉴클레오티드 문자 A, C, G 및 T로 구성된 문자열은 훨씬 빠른 처리를 위해 정수로 특수하게 인코딩 될 수 있습니다 . 기수 정렬은이 책에서 논의 된 많은 알고리즘 중 하나입니다. 이 질문에 대한 답변을 채택하고 성능이 크게 향상 될 수 있어야합니다.


이 책에 제시된 기수 정렬은 제자리에 있지 않으므로이 용도로는 사용할 수 없습니다. 끈 다짐에 관해서는 (물론) 이미하고 있습니다. 라이브러리에서 일반 문자열처럼 처리 할 수 ​​있기 때문에 (아래 게시) 최종 솔루션 (이것은 표시되지 않음)이 표시되지 않지만 RADIX사용 된 값은 물론 더 큰 값으로 조정할 수 있습니다.
Konrad Rudolph

3

trie를 사용해보십시오 . 데이터 정렬은 단순히 데이터 세트를 반복하고 삽입하는 것입니다. 구조는 자연스럽게 정렬되어 있으며 B- 트리와 비슷하다고 생각할 수 있습니다 (비교를 제외하고 항상 포인터 간접 지정을 사용함).

캐싱 동작은 모든 내부 노드를 선호하므로이를 개선하지는 못할 것입니다. 그러나 트리의 분기 요소를 피할 수도 있습니다 (모든 노드가 단일 캐시 라인에 적합하고 힙과 유사한 트리 노드를 레벨 순서 순회를 나타내는 연속 배열로 할당). 시도는 디지털 구조이기 때문에 (길이 k의 요소에 대한 O (k) 삽입 / 찾기 / 삭제) 기수 정렬에 대해 경쟁적인 성능을 가져야합니다.


trie는 순진한 구현과 같은 문제가 있습니다. O (n) 추가 메모리가 필요합니다.
Konrad Rudolph

3

나는 것 burstsort 문자열의 포장 비트 표현을. Burstsort는 기수보다 훨씬 나은 지역성을 가지고 있다고 주장하며 기존 시도 대신 버스트 시도로 추가 공간 사용을 줄였습니다. 원본 용지의 치수가 측정되었습니다.


2

기수 정렬은 캐시를 인식하지 않으며 큰 세트에 대한 가장 빠른 정렬 알고리즘이 아닙니다. 당신은 볼 수 있습니다 :

정렬 배열에 저장하기 전에 압축을 사용하고 DNA의 각 문자를 2 비트로 인코딩 할 수도 있습니다.


청구서 :이 qsort함수가 std::sortC ++에서 제공 하는 함수 보다 어떤 이점을 가지고 있는지 설명 할 수 있습니까? 특히, 후자는 현대 라이브러리에서 고도로 정교한 소개를 구현하고 비교 작업을 인라인합니다. 나는 대부분의 경우 O (n)에서 수행한다는 주장을 사지 않습니다. 일반적인 경우에는 사용할 수없는 정도의 내부 검사가 필요하기 때문에 (적어도 많은 오버 헤드가 없는 것은 아닙니다 ).
Konrad Rudolph

나는 C ++을 사용하지 않지만 테스트에서 인라인 QSORT는 stdlib의 qsort보다 3 배 빠를 수 있습니다. ti7qsort는 정수에 대해 가장 빠른 정렬입니다 (인라인 QSORT보다 빠름). 작은 고정 크기 데이터를 정렬하는 데 사용할 수도 있습니다. 데이터로 테스트를 수행해야합니다.
청구

1

dsimcha의 MSB 기수 정렬은 멋지게 보이지만 Nils는 캐시 위치가 큰 문제 크기에서 당신을 죽이는 것이라는 관찰로 문제의 핵심에 더 가까워집니다.

나는 매우 간단한 접근법을 제안합니다.

  1. m기수 정렬이 효율적인 가장 큰 크기 를 경험적으로 추정하십시오 .
  2. m입력을 모두 소진 할 때까지 한 번 에 요소 블록을 읽고 기수 정렬하여 메모리가 충분한 경우 메모리 버퍼에, 그렇지 않으면 파일에 기록합니다.
  3. 정렬 된 결과 블록을 병합 합니다.

Mergesort는 내가 알고있는 가장 캐시 친화적 인 정렬 알고리즘입니다. "배열 A 또는 B에서 다음 항목을 읽은 다음 출력 버퍼에 항목을 씁니다." 테이프 드라이브 에서 효율적으로 실행됩니다 . 항목 2n을 정렬 하는 데 공간 이 필요 n하지만, 내기에는 훨씬 개선 된 캐시 위치가 중요하지 않을 것입니다. 제자리가 아닌 기수 정렬을 사용하는 경우 어쨌든 추가 공간이 필요합니다.

마지막으로 mergesort는 재귀없이 구현할 수 있으며 실제로 이렇게하면 실제 선형 메모리 액세스 패턴이 명확 해집니다.


1

문제를 해결 한 것 같지만 기록에 따르면 실행 가능한 기수 정렬의 한 버전은 "American Flag Sort"인 것으로 보입니다. 여기에 설명되어 있습니다 : Engineering Radix Sort . 일반적인 아이디어는 각 문자에 대해 2 패스를 수행하는 것입니다. 먼저 각 문자 수를 계산하여 입력 배열을 빈으로 세분화 할 수 있습니다. 그런 다음 각 요소를 올바른 빈으로 바꿔 다시 통과하십시오. 다음 문자 위치에서 각 빈을 재귀 적으로 정렬하십시오.


실제로 사용하는 솔루션은 플래그 정렬 알고리즘과 매우 밀접한 관련이 있습니다. 관련된 차이점이 있는지 모르겠습니다.
Konrad Rudolph

2
: 정렬하지만, apperently의 내가 코딩 무엇을하는 미국 국기 들어 본 적이 coliru.stacked-crooked.com/a/94eb75fbecc39066은 그것은 현재 성능이 뛰어나는 것 std::sort, 그리고 나는 아직도 더 빨리 갈 수 디지타이저 multidigit 특정이야,하지만 내 테스트 스위트가 가진 메모리입니다 문제 (알고리즘이 아닌 테스트 스위트 자체)
Mooing Duck

@ KonradRudolph : 깃발 정렬과 다른 기수 정렬의 큰 차이점은 계산 패스입니다. 모든 기수 정렬은 매우 밀접한 관련이 있지만, 나는 당신이 기수 정렬이라고 생각하지 않습니다.
Mooing Duck

@MooingDuck : 샘플에서 영감을 얻었습니다. 독립적 인 구현에 갇히고 귀하의 도움으로 다시 돌아올 수있었습니다. 감사! 한 가지 가능한 최적화-아직 가치가 있는지 여부를 알기에 충분히 얻지 못했습니다. 교환하려는 위치의 요소가 이미 필요한 위치에 있으면 건너 뛰고 그 중 하나를 진행할 수 있습니다 그렇지 않습니다. 이를 감지하려면 물론 추가 논리와 가능한 추가 스토리지가 필요하지만 스왑은 비교에 비해 비싸므로 수행 할 가치가 있습니다.
500-내부 서버 오류

1

먼저, 문제의 코딩에 대해 생각하십시오. 문자열을 제거하고 이진 표현으로 대체하십시오. 첫 번째 바이트를 사용하여 길이 + 인코딩을 나타냅니다. 또는 4 바이트 경계에서 고정 길이 표현을 사용하십시오. 그러면 기수 정렬이 훨씬 쉬워집니다. 기수 정렬의 경우 가장 중요한 것은 내부 루프의 핫스팟에서 예외 처리를하지 않는 것입니다.

네, 4 차 문제에 대해 조금 더 생각했습니다. 이를 위해 Judy tree 와 같은 솔루션을 원합니다 . 다음 솔루션은 가변 길이 문자열을 처리 할 수 ​​있습니다. 고정 길이의 경우 길이 비트를 제거하면 실제로 더 쉽습니다.

16 개의 포인터 블록을 할당하십시오. 블록이 항상 정렬되므로 포인터의 최하위 비트를 재사용 할 수 있습니다. 특별한 저장소 할당자를 원할 수도 있습니다 (큰 저장소를 더 작은 블록으로 나눔). 여러 종류의 블록이 있습니다 :

  • 7 개의 길이의 가변 길이 문자열로 인코딩합니다. 채워지면 다음과 같이 대체합니다.
  • 위치는 다음 두 문자를 인코딩하며 다음 블록으로 16 개의 포인터를 가지며 다음으로 끝납니다.
  • 문자열의 마지막 세 문자의 비트 맵 인코딩

각 종류의 블록에 대해 LSB에 다른 정보를 저장해야합니다. 가변 길이 문자열이 있으므로 문자열 끝도 저장해야하며 마지막 블록 유형은 가장 긴 문자열에만 사용할 수 있습니다. 구조에 더 깊이 들어가면 7 개의 길이 비트를 적게 교체해야합니다.

이렇게하면 정렬 된 문자열을 상당히 빠르고 매우 메모리 효율적으로 저장할 수 있습니다. 그것은 마치 trie 처럼 행동 할 것 입니다. 이 작업을 수행하려면 충분한 단위 테스트를 작성하십시오. 모든 블록 전환의 적용 범위를 원합니다. 두 번째 종류의 블록으로 시작하고 싶습니다.

더 나은 성능을 위해 다른 블록 유형과 더 큰 크기의 블록을 추가 할 수 있습니다. 블록의 크기가 항상 동일하고 충분히 크면 포인터에 더 적은 비트를 사용할 수 있습니다. 16 개의 포인터 크기로 이미 32 비트 주소 공간에 사용 가능한 바이트가 있습니다. 흥미로운 블록 유형에 대한 Judy 트리 설명서를 살펴보십시오. 기본적으로 공간 (및 런타임) 트레이드 오프에 대한 코드 및 엔지니어링 시간을 추가합니다.

처음 네 문자에 대해 256 너비의 직접 기수로 시작하고 싶을 것입니다. 그것은 적절한 공간 / 시간 균형을 제공합니다. 이 구현에서는 간단한 trie보다 메모리 오버 헤드가 훨씬 적습니다. 대략 3 배 작습니다 (측정하지 않았습니다). O (n log n) 퀵 정렬과 비교할 때 알 수 있듯이 상수가 충분히 낮은 경우 O (n)은 문제가되지 않습니다.

복식 취급에 관심이 있습니까? 짧은 시퀀스로,있을 것입니다. 카운트를 처리하기 위해 블록을 조정하는 것은 까다 롭지 만 매우 공간 효율적일 수 있습니다.


비트 팩 표현을 사용하면 기수 정렬이 더 쉬워지는 방법을 알 수 없습니다. 그건 그렇고, 내가 사용하는 프레임 워크는 실제로 비트 팩 표현을 사용할 가능성을 제공하지만 인터페이스 사용자에게는 완전히 투명합니다.
Konrad Rudolph 2016 년

스톱워치를 볼 때 :)
Stephan Eggermont

Judy 나무를 확실히 볼 것입니다. Vanilla는 기본적으로 요소보다 적은 패스로 일반 MSD 기수 정렬처럼 동작하지만 추가 저장 공간이 필요하기 때문에 실제로 테이블에 많은 것을 가져 오지 않습니다.
Konrad Rudolph
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.