[algorithm] 내부 기수 정렬

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


예비

나는 정렬하고 싶은 문자 “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 문자열에서 작동하는 작업 기준 구현 또는 작업 가능한 기수 정렬에 대한 의사 코드 / 설명을 찾을 희망이 있습니까?



답변

자, 여기 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 배 빠릅니다.


답변

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

이유:

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

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

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

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

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


답변

시퀀스를 비트 단위로 인코딩하여 메모리 요구 사항을 확실히 제거 할 수 있습니다. 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;
}


답변

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

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 개)을 반환하여 디스크 기반 버퍼의 분기를 줄입니다. 이는 성능을 향상시킬 수도 있고 향상시키지 않을 수도 있으므로 실험 해보십시오.


답변

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

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

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

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

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

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

Wikipedia 기사를 참조하십시오.


답변

추가 공간이없는 기수 정렬 “은 문제를 해결하는 논문입니다.


답변

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

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

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

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

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