Sắp xếp Radix tại chỗ


200

Đây là một văn bản dài. Xin vui lòng chịu với tôi. Đun sôi xuống, câu hỏi là: Có một thuật toán sắp xếp radix tại chỗ khả thi ?


Sơ bộ

Tôi đã có một số lượng lớn các chuỗi có độ dài cố định nhỏ chỉ sử dụng các chữ cái CẦN A, BẠC, VĂN, VĂN và VĂN (vâng, bạn đã đoán ra: DNA ) mà tôi muốn sắp xếp.

Hiện tại, tôi sử dụng sử std::sortdụng introort trong tất cả các triển khai STL phổ biến . Điều này hoạt động khá tốt. Tuy nhiên, tôi tin rằng loại radix phù hợp hoàn hảo với vấn đề của tôi và sẽ hoạt động tốt hơn nhiều trong thực tế.

Chi tiết

Tôi đã thử nghiệm giả định này với một triển khai rất ngây thơ và đối với các đầu vào tương đối nhỏ (trên 10.000), điều này là đúng (tốt, ít nhất là nhanh hơn gấp đôi). Tuy nhiên, thời gian chạy xuống cấp rất nhiều khi kích thước sự cố trở nên lớn hơn ( N > 5.000.000).

Lý do rất rõ ràng: sắp xếp radix yêu cầu sao chép toàn bộ dữ liệu (thực tế hơn một lần trong quá trình thực hiện ngây thơ của tôi). Điều này có nghĩa là tôi đã đặt ~ 4 GiB vào bộ nhớ chính của mình, điều này rõ ràng sẽ giết chết hiệu suất. Ngay cả nếu không, tôi không thể đủ khả năng sử dụng bộ nhớ này vì kích thước của vấn đề thực sự còn lớn hơn.

Trường hợp sử dụng

Lý tưởng nhất là thuật toán này sẽ hoạt động với bất kỳ độ dài chuỗi nào trong khoảng từ 2 đến 100, đối với DNA cũng như DNA5 (cho phép thêm một ký tự đại diện ký tự gợi Niết), hoặc thậm chí DNA với mã mơ hồ IUPAC (dẫn đến 16 giá trị riêng biệt). Tuy nhiên, tôi nhận ra rằng tất cả các trường hợp này không thể được bảo hiểm, vì vậy tôi hài lòng với bất kỳ cải thiện tốc độ nào tôi nhận được. Mã có thể quyết định động để gửi thuật toán nào.

Nghiên cứu

Thật không may, bài viết Wikipedia về radix sort là vô dụng. Phần về một biến thể tại chỗ là rác hoàn toàn. Phần NIST-DADS về sắp xếp cơ số nằm cạnh không tồn tại. Có một bài báo đầy hứa hẹn được gọi là Phân loại Radix thích ứng tại chỗ hiệu quả , mô tả thuật toán này MSLiến. Thật không may, bài báo này, quá thất vọng.

Đặc biệt, có những điều sau đây.

Đầu tiên, thuật toán chứa một số lỗi và không giải thích được nhiều. Cụ thể, nó không nêu chi tiết cuộc gọi đệ quy (tôi chỉ đơn giản giả định rằng nó tăng hoặc giảm một số con trỏ để tính giá trị dịch chuyển và mặt nạ hiện tại). Ngoài ra, nó sử dụng các chức năng dest_groupdest_addresskhông đưa ra định nghĩa. Tôi không thấy cách thực hiện những điều này một cách hiệu quả (nghĩa là trong O (1); ít nhất dest_addresslà không tầm thường).

Cuối cùng nhưng không kém phần quan trọng, thuật toán đạt được vị trí tại chỗ bằng cách hoán đổi các chỉ số mảng với các phần tử bên trong mảng đầu vào. Điều này rõ ràng chỉ hoạt động trên các mảng số. Tôi cần sử dụng nó trên chuỗi. Tất nhiên, tôi chỉ có thể gõ phím mạnh và tiếp tục giả định rằng bộ nhớ sẽ chấp nhận việc tôi lưu trữ một chỉ mục nơi nó không thuộc về. Nhưng điều này chỉ hoạt động miễn là tôi có thể ép các chuỗi của mình vào 32 bit bộ nhớ (giả sử số nguyên 32 bit). Đó chỉ là 16 ký tự (hãy bỏ qua cho đến lúc 16> log (5.000.000)).

Một bài báo khác của một trong những tác giả không đưa ra mô tả chính xác nào cả, nhưng nó cho thời gian chạy của MSL là tuyến tính phụ, điều này hoàn toàn sai.

Tóm tắt lại : Có bất kỳ hy vọng tìm thấy một triển khai tham chiếu làm việc hoặc ít nhất là một mã giả / mô tả tốt về một loại cơ số hoạt động tại chỗ hoạt động trên các chuỗi DNA?


65
Đó là một câu hỏi được viết xuất sắc.
JustinT

1
Làm thế nào nhỏ là các chuỗi chiều dài cố định nhỏ?
EvilTeach

1
@EvilTeach: Tôi đã thêm các trường hợp sử dụng.
Konrad Rudolph

2
@Stephan: tất cả đều ổn và tốt. Nhưng trong trường hợp sao chép / cache bị lỗi, tôi chỉ bị trễ. Trong trường hợp bộ nhớ, tôi đạt đến giới hạn phyical. Điều này chỉ đơn giản là không thể chối bỏ. Tất cả những kỹ thuật ưa thích để lưu trữ các phần của dữ liệu trên đĩa chắc chắn chậm hơn so với giải pháp quicksort hiện tại.
Konrad Rudolph

2
Mặt khác, giải pháp của dsimcha chắc chắn là nhanh hơn so với quicksort đối với một số đầu vào. Số lượng di chuyển có thể cao và bộ nhớ cache cục bộ nhỏ nhưng trong thế giới thực, nó vẫn tốt. Tôi cũng đã điều chỉnh một chút giải pháp để giảm số lần hoán đổi mà tôi cần thực hiện.
Konrad Rudolph

Câu trả lời:


61

Chà, đây là một triển khai đơn giản của một loại cơ chế MSD cho DNA. Nó được viết bằng D vì đó là ngôn ngữ mà tôi sử dụng nhiều nhất và do đó ít có khả năng mắc lỗi ngớ ngẩn nhất, nhưng nó có thể dễ dàng được dịch sang một số ngôn ngữ khác. Nó ở vị trí nhưng yêu cầu 2 * seq.lengthđi qua mảng.

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);
   }
}

Rõ ràng, đây là loại đặc trưng cho DNA, trái ngược với việc nói chung, nhưng nó phải nhanh.

Biên tập:

Tôi tò mò liệu mã này có thực sự hoạt động hay không, vì vậy tôi đã kiểm tra / gỡ lỗi nó trong khi chờ mã tin sinh học của riêng tôi chạy. Phiên bản trên bây giờ thực sự đã được thử nghiệm và hoạt động. Đối với 10 triệu chuỗi gồm 5 cơ sở, mỗi chuỗi nhanh hơn khoảng 3 lần so với một hướng nội được tối ưu hóa.


9
Nếu bạn có thể sống với cách tiếp cận vượt qua 2 lần, điều này sẽ mở rộng sang radix-N: pass 1 = chỉ cần đi qua và đếm xem có bao nhiêu chữ số của mỗi chữ số N. Sau đó, nếu bạn đang phân vùng mảng, nó sẽ cho bạn biết mỗi chữ số bắt đầu từ đâu. Pass 2 không hoán đổi đến vị trí thích hợp trong mảng.
Jason S

(ví dụ: N = 4, nếu có 90000 A, 80000 G, 100 C, 100000 T, sau đó tạo một mảng được khởi tạo thành tổng tích lũy = [0, 90000, 170000, 170100] được sử dụng thay cho APos của bạn, CPos, v.v. làm con trỏ cho vị trí phần tử tiếp theo cho mỗi chữ số được hoán đổi.)
Jason S

Tôi không chắc mối quan hệ giữa biểu diễn nhị phân và biểu diễn chuỗi này sẽ là gì, ngoài việc sử dụng ít nhất 4 lần bộ nhớ cần thiết
Stephan Eggermont

Làm thế nào là tốc độ với trình tự dài hơn? Bạn không có đủ những cái khác nhau với chiều dài 5
Stephan Eggermont

4
Loại radix này có vẻ là một trường hợp đặc biệt của loại American Flag - một biến thể sắp xếp radix nổi tiếng tại chỗ.
Edward KMett

21

Tôi chưa bao giờ thấy một loại radix tại chỗ, và từ bản chất của loại radix, tôi nghi ngờ rằng nó nhanh hơn nhiều so với một loại sắp xếp không đúng chỗ, miễn là mảng tạm thời phù hợp với bộ nhớ.

Lý do:

Việc sắp xếp thực hiện đọc tuyến tính trên mảng đầu vào, nhưng tất cả các ghi sẽ gần như ngẫu nhiên. Từ một N nhất định trở lên, điều này sẽ biến thành một lỗi nhớ cache trên mỗi lần ghi. Lỗi bộ nhớ cache này là những gì làm chậm thuật toán của bạn. Nếu nó tại chỗ hoặc không sẽ không thay đổi hiệu ứng này.

Tôi biết rằng điều này sẽ không trả lời trực tiếp câu hỏi của bạn, nhưng nếu sắp xếp là một nút cổ chai, bạn có thể muốn xem các thuật toán sắp xếp gần như là một bước tiền xử lý (trang wiki trên heap mềm có thể giúp bạn bắt đầu).

Điều đó có thể cung cấp một tăng cục bộ bộ nhớ cache rất tốt đẹp. Sau đó, một loại văn bản sắp xếp ra khỏi sách văn bản sẽ hoạt động tốt hơn. Việc ghi vẫn sẽ gần như ngẫu nhiên nhưng ít nhất chúng sẽ tập hợp xung quanh cùng một khối bộ nhớ và do đó làm tăng tỷ lệ nhấn bộ đệm.

Tôi không biết nếu nó hoạt động trong thực tế mặc dù.

Btw: Nếu bạn chỉ xử lý chuỗi DNA: Bạn có thể nén một char thành hai bit và đóng gói dữ liệu của bạn khá nhiều. Điều này sẽ cắt giảm yêu cầu bộ nhớ theo yếu tố bốn trên một đại diện ngây thơ. Địa chỉ trở nên phức tạp hơn, nhưng ALU của CPU của bạn có rất nhiều thời gian để sử dụng trong tất cả các lỗi nhớ cache.


2
Hai điểm tốt; sắp xếp gần là một khái niệm mới với tôi, tôi sẽ phải đọc về điều đó. Bộ nhớ cache là một sự cân nhắc khác ám ảnh những giấc mơ của tôi. ;-) Tôi sẽ phải xem về điều này.
Konrad Rudolph

Nó cũng mới đối với tôi (một vài tháng), nhưng một khi bạn có khái niệm, bạn bắt đầu thấy các cơ hội cải thiện hiệu suất.
Nils Pipenbrinck

Việc viết rất xa gần như ngẫu nhiên trừ khi cơ số của bạn rất lớn. Ví dụ: giả sử bạn sắp xếp một ký tự tại một thời điểm (loại radix-4), tất cả các lần ghi sẽ thuộc về một trong 4 nhóm tăng trưởng tuyến tính. Đây là cả bộ nhớ cache và tìm nạp trước thân thiện. Tất nhiên, bạn có thể muốn sử dụng một cơ số lớn hơn và tại một số con trỏ bạn đã đánh đổi giữa bộ đệm và tính thân thiện với tìm nạp trước và kích thước cơ số. Bạn có thể đẩy điểm hòa vốn về phía các radice lớn hơn bằng cách sử dụng phần mềm tìm nạp trước hoặc khu vực cào cho các thùng của bạn với việc xả định kỳ vào các thùng "thực".
BeeOnRope

8

Bạn chắc chắn có thể loại bỏ các yêu cầu bộ nhớ bằng cách mã hóa chuỗi theo bit. Bạn đang xem xét hoán vị vì vậy, trong chiều dài 2, với "ACGT" là 16 trạng thái, hoặc 4 bit. Đối với độ dài 3, đó là 64 trạng thái, có thể được mã hóa thành 6 bit. Vì vậy, nó trông giống như 2 bit cho mỗi chữ cái trong chuỗi, hoặc khoảng 32 bit cho 16 ký tự như bạn đã nói.

Nếu có một cách để giảm số lượng 'từ' hợp lệ, có thể nén thêm.

Vì vậy, đối với các chuỗi có độ dài 3, người ta có thể tạo ra 64 thùng, có thể có kích thước uint32 hoặc uint64. Khởi tạo chúng về không. Lặp lại qua danh sách rất lớn của bạn về 3 chuỗi char và mã hóa chúng như trên. Sử dụng cái này như là một chỉ mục và tăng thùng đó.
Lặp lại điều này cho đến khi tất cả các chuỗi của bạn đã được xử lý.

Tiếp theo, tạo lại danh sách của bạn.

Lặp lại qua 64 nhóm theo thứ tự, đối với số lượng tìm thấy trong nhóm đó, tạo ra nhiều trường hợp của chuỗi được biểu thị bởi nhóm đó.
khi tất cả các nhóm đã được lặp lại, bạn có mảng được sắp xếp của bạn.

Một chuỗi gồm 4, thêm 2 bit, do đó sẽ có 256 thùng. Một chuỗi gồm 5, thêm 2 bit, do đó sẽ có 1024 thùng.

Tại một số điểm, số lượng xô sẽ đạt đến giới hạn của bạn. Nếu bạn đọc các chuỗi từ một tệp, thay vì giữ chúng trong bộ nhớ, sẽ có nhiều bộ nhớ hơn cho các thùng.

Tôi nghĩ rằng điều này sẽ nhanh hơn việc thực hiện sắp xếp tại chỗ vì các thùng có thể phù hợp với bộ công việc của bạn.

Đây là một bản hack cho thấy kỹ thuật

#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;
}

Tại sao so sánh khi bạn có thể băm eh?
đáng kinh ngạc nhất

1
Chết tiệt. Hiệu suất nói chung là một vấn đề với bất kỳ xử lý DNA.
EvilTeach

6

Nếu tập dữ liệu của bạn quá lớn, thì tôi nghĩ rằng cách tiếp cận bộ đệm dựa trên đĩa sẽ là tốt nhất:

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

Tôi cũng sẽ thử nghiệm nhóm vào một số lượng lớn hơn các thùng, ví dụ, nếu chuỗi của bạn là:

GATTACA

cuộc gọi MSB đầu tiên sẽ trả về nhóm cho GATT (256 tổng số nhóm), theo cách đó bạn tạo ra ít nhánh hơn của bộ đệm dựa trên đĩa. Điều này có thể hoặc không thể cải thiện hiệu suất, vì vậy hãy thử nghiệm với nó.


Chúng tôi sử dụng các tập tin ánh xạ bộ nhớ cho một số ứng dụng. Tuy nhiên, nói chung, chúng tôi làm việc theo giả định rằng máy chỉ cung cấp đủ RAM để không yêu cầu sao lưu đĩa rõ ràng (tất nhiên, việc hoán đổi vẫn diễn ra). Nhưng chúng tôi đã phát triển một cơ chế cho các mảng được hỗ trợ đĩa tự động
Konrad Rudolph

6

Tôi sẽ đi ra ngoài trên một chi và đề nghị bạn chuyển sang thực hiện heapsort . Đề xuất này đi kèm với một số giả định:

  1. Bạn kiểm soát việc đọc dữ liệu
  2. Bạn có thể làm điều gì đó có ý nghĩa với dữ liệu được sắp xếp ngay khi bạn 'bắt đầu' sắp xếp nó.

Cái hay của heap-sort là bạn có thể tạo heap trong khi bạn đọc dữ liệu và bạn có thể bắt đầu nhận được kết quả ngay khi bạn tạo được heap.

Hãy lùi lại. Nếu bạn may mắn đến mức bạn có thể đọc dữ liệu không đồng bộ (nghĩa là bạn có thể đăng một số loại yêu cầu đọc và được thông báo khi một số dữ liệu đã sẵn sàng), và sau đó bạn có thể xây dựng một đống heap trong khi chờ đợi đoạn dữ liệu tiếp theo sẽ đến - ngay cả từ đĩa. Thông thường, phương pháp này có thể chôn vùi phần lớn chi phí của một nửa số sắp xếp của bạn sau thời gian nhận dữ liệu.

Khi bạn đã đọc dữ liệu, phần tử đầu tiên đã có sẵn. Tùy thuộc vào nơi bạn đang gửi dữ liệu, điều này có thể là tuyệt vời. Nếu bạn đang gửi nó đến một trình đọc không đồng bộ khác, hoặc một số mô hình 'sự kiện' hoặc giao diện người dùng song song, bạn có thể gửi các khối và khối khi bạn đi.

Điều đó nói rằng - nếu bạn không kiểm soát được cách đọc dữ liệu và nó được đọc đồng bộ và bạn không sử dụng dữ liệu được sắp xếp cho đến khi nó được viết hoàn toàn - hãy bỏ qua tất cả điều này. :

Xem các bài viết Wikipedia:


1
Gợi ý tốt. Tuy nhiên, tôi đã thử điều này và trong trường hợp cụ thể của tôi, chi phí duy trì một đống lớn hơn là chỉ tích lũy dữ liệu trong một vectơ và sắp xếp một khi tất cả dữ liệu đã đến.
Konrad Rudolph


4

Hiệu suất-khôn ngoan bạn có thể muốn xem xét một thuật toán sắp xếp so sánh chuỗi tổng quát hơn.

Hiện tại bạn kết thúc việc chạm vào mọi yếu tố của mỗi chuỗi, nhưng bạn có thể làm tốt hơn!

Đặc biệt, một loại nổ là rất phù hợp cho trường hợp này. Như một phần thưởng, vì blastsort dựa trên các lần thử, nó hoạt động rất tốt cho các kích thước bảng chữ cái nhỏ được sử dụng trong DNA / RNA, vì bạn không cần phải xây dựng bất kỳ loại nút tìm kiếm ternary, băm hoặc sơ đồ nén nút trie nào khác vào trie thực hiện. Các lần thử cũng có thể hữu ích cho mục tiêu cuối cùng giống như hậu tố của bạn.

Một triển khai mục đích chung khá tốt của blastsort có sẵn trên giả mạo nguồn tại http://sourceforge.net/projects/burstsort/ - nhưng nó không được thực hiện.

Đối với mục đích so sánh, việc triển khai C-blastsort được đề cập tại http://www.cs.mu.oz.au/~rsinha/ con / SinhaRingZobel-2006.pdf điểm chuẩn nhanh hơn 4-5 lần so với quicksort và radix sắp xếp cho một số khối lượng công việc điển hình.


Tôi chắc chắn sẽ phải xem xét loại nổ - mặc dù tại thời điểm này tôi không thấy cách thức bộ ba có thể được xây dựng tại chỗ. Trong các mảng hậu tố nói chung có tất cả trừ các cây hậu tố (và do đó, cố gắng) trong tin sinh học vì các đặc tính hiệu suất vượt trội trong các ứng dụng thực tế.
Konrad Rudolph

4

Bạn sẽ muốn xem qua Xử lý trình tự bộ gen quy mô lớn của các tiến sĩ. Kasahara và Morishita.

Các chuỗi bao gồm bốn chữ cái nucleotide A, C, G và T có thể được mã hóa đặc biệt thành Số nguyên để xử lý nhanh hơn nhiều . Radix sort nằm trong số nhiều thuật toán được thảo luận trong cuốn sách; bạn sẽ có thể điều chỉnh câu trả lời được chấp nhận cho câu hỏi này và thấy sự cải thiện hiệu suất lớn.


Loại radix được trình bày trong cuốn sách này không đúng chỗ nên không thể sử dụng cho mục đích này. Đối với việc nén chuỗi, tôi (tất nhiên) đã làm điều này. Giải pháp cuối cùng của tôi (ít nhiều) (được đăng dưới đây) không cho thấy điều này bởi vì thư viện cho phép tôi coi chúng như các chuỗi bình thường - nhưng tất nhiên RADIXgiá trị được sử dụng có thể (và) được điều chỉnh theo các giá trị lớn hơn.
Konrad Rudolph

3

Bạn có thể thử sử dụng một trie . Sắp xếp dữ liệu chỉ đơn giản là lặp qua bộ dữ liệu và chèn nó; cấu trúc được sắp xếp một cách tự nhiên và bạn có thể nghĩ nó giống với B-Tree (ngoại trừ thay vì so sánh, bạn luôn luôn sử dụng các chỉ dẫn con trỏ).

Hành vi bộ đệm sẽ ưu tiên tất cả các nút nội bộ, vì vậy bạn có thể sẽ không cải thiện điều đó; nhưng bạn cũng có thể sử dụng hệ số phân nhánh của bộ ba của mình (đảm bảo rằng mọi nút đều khớp với một dòng bộ đệm duy nhất, phân bổ các nút trie tương tự như một đống, như một mảng liền kề đại diện cho một giao dịch theo cấp bậc). Vì các lần thử cũng là các cấu trúc kỹ thuật số (O (k) chèn / tìm / xóa đối với các phần tử có độ dài k), nên bạn có hiệu suất cạnh tranh với loại sắp xếp cơ số.


Bộ ba có cùng một vấn đề với việc triển khai ngây thơ của tôi: nó yêu cầu bộ nhớ bổ sung O (n) đơn giản là quá nhiều.
Konrad Rudolph

3

Tôi sẽ phá vỡ một đại diện bit-bit của chuỗi. Burstsort được tuyên bố là có địa phương tốt hơn nhiều so với các loại cơ số, giữ cho việc sử dụng không gian thêm xuống với các lần thử thay cho các lần thử cổ điển. Giấy gốc có số đo.


2

Radix-Sort không có ý thức bộ đệm và không phải là thuật toán sắp xếp nhanh nhất cho các tập lớn. Bạn có thể nhìn vào:

Bạn cũng có thể sử dụng nén và mã hóa từng chữ cái DNA của mình thành 2 bit trước khi lưu trữ vào mảng sắp xếp.


bill: bạn có thể giải thích những lợi thế của qsortchức năng này so với std::sortchức năng được cung cấp bởi C ++ không? Đặc biệt, cái sau thực hiện một hướng nội rất tinh vi trong các thư viện hiện đại và nội tuyến hoạt động so sánh. Tôi không mua khiếu nại mà nó thực hiện trong O (n) trong hầu hết các trường hợp, vì điều này sẽ yêu cầu một mức độ nội tâm không có sẵn trong trường hợp chung (ít nhất là không phải không có nhiều chi phí).
Konrad Rudolph

Tôi không sử dụng c ++, nhưng trong các thử nghiệm của tôi, QSORT nội tuyến có thể nhanh hơn 3 lần so với qsort trong stdlib. Ti7qsort là loại nhanh nhất cho số nguyên (nhanh hơn QSORT nội tuyến). Bạn cũng có thể sử dụng nó để sắp xếp dữ liệu kích thước cố định nhỏ. Bạn phải làm các bài kiểm tra với dữ liệu của bạn.
hóa đơn

1

Loại radix MSB của dsimcha trông rất đẹp, nhưng Nils lại gần trung tâm của vấn đề hơn với quan sát rằng địa phương bộ đệm là thứ giết chết bạn ở kích cỡ vấn đề lớn.

Tôi đề nghị một cách tiếp cận rất đơn giản:

  1. Ước tính thực nghiệm kích thước lớn nhất m mà một loại cơ số có hiệu quả.
  2. Đọc các khối của m phần tử tại một thời điểm, sắp xếp chúng và viết chúng ra (vào bộ nhớ đệm nếu bạn có đủ bộ nhớ, nhưng ngược lại với tệp), cho đến khi bạn cạn kiệt đầu vào.
  3. Sáp nhập các khối sắp xếp kết quả.

Mergesort là thuật toán sắp xếp thân thiện với bộ nhớ cache nhất mà tôi biết: "Đọc mục tiếp theo từ mảng A hoặc B, sau đó viết một mục vào bộ đệm đầu ra." Nó chạy hiệu quả trên các ổ đĩa băng . Nó đòi hỏi 2nkhông gian để sắp xếpn các mục, nhưng tôi cá là địa phương bộ nhớ cache được cải thiện nhiều mà bạn sẽ thấy sẽ không quan trọng - và nếu bạn đang sử dụng một loại cơ số không tại chỗ, dù sao bạn cũng cần thêm không gian đó.

Cuối cùng xin lưu ý rằng sáp nhập có thể được thực hiện mà không cần đệ quy, và trên thực tế, làm theo cách này sẽ làm rõ mẫu truy cập bộ nhớ tuyến tính thực sự.


1

Có vẻ như bạn đã giải quyết được vấn đề, nhưng đối với hồ sơ, có vẻ như một phiên bản của loại radix tại chỗ khả thi là "Sắp xếp cờ Mỹ". Nó được mô tả ở đây: Engineering Radix Sort . Ý tưởng chung là thực hiện 2 lần cho mỗi ký tự - đầu tiên là đếm số lượng mỗi bạn có, để bạn có thể chia mảng đầu vào thành các thùng. Sau đó đi qua một lần nữa, hoán đổi từng yếu tố vào thùng chính xác. Bây giờ sắp xếp đệ quy từng thùng trên vị trí ký tự tiếp theo.


Trên thực tế, giải pháp tôi sử dụng có liên quan rất chặt chẽ với thuật toán Sắp xếp cờ. Tôi không biết nếu có bất kỳ sự phân biệt có liên quan.
Konrad Rudolph

2
Chưa bao giờ nghe về Sắp xếp cờ Mỹ, nhưng rõ ràng đó là những gì tôi đã mã hóa: coliru.stacked-crooking.com/a/94eb75fbecc39066 Hiện tại nó vượt trội hơn std::sortvà tôi chắc chắn rằng một bộ số hóa nhiều chữ số có thể chạy nhanh hơn, nhưng bộ thử nghiệm của tôi có bộ nhớ các vấn đề (không phải là thuật toán, bộ thử nghiệm)
Mooing Duck

@KonradRudolph: Sự khác biệt lớn giữa sắp xếp Cờ và các loại cơ số khác là đường chuyền đếm. Bạn đúng rằng tất cả các loại cơ số có liên quan rất chặt chẽ với nhau, nhưng tôi sẽ không coi bạn là một loại Cờ.
Vịt Mooing

@MooingDuck: Chỉ cần lấy một số cảm hứng từ mẫu của bạn ở đó - Tôi đã bị mắc kẹt trong việc thực hiện độc lập của riêng mình và bạn đã giúp tôi trở lại đúng hướng. Cảm ơn! Một tối ưu hóa có thể - Tôi chưa đủ xa để xem liệu nó có đáng hay không: Nếu yếu tố ở vị trí bạn đang trao đổi sẽ xảy ra ở nơi cần đến, bạn có thể muốn bỏ qua điều đó và tiến tới không phải. Tất nhiên, việc phát hiện điều này sẽ đòi hỏi thêm logic, và cũng có thể lưu trữ thêm, nhưng vì các giao dịch hoán đổi là đắt so với so sánh, nên có thể đáng làm.
500 - Lỗi máy chủ nội bộ

1

Đầu tiên, hãy nghĩ về mã hóa vấn đề của bạn. Loại bỏ các chuỗi, thay thế chúng bằng một đại diện nhị phân. Sử dụng byte đầu tiên để chỉ độ dài + mã hóa. Hoặc, sử dụng biểu diễn độ dài cố định ở ranh giới bốn byte. Sau đó, loại radix trở nên dễ dàng hơn nhiều. Đối với một loại cơ số, điều quan trọng nhất là không có xử lý ngoại lệ tại điểm nóng của vòng lặp bên trong.

OK, tôi đã nghĩ thêm một chút về vấn đề 4 nhân. Bạn muốn một giải pháp như cây Judy cho việc này. Giải pháp tiếp theo có thể xử lý các chuỗi có độ dài thay đổi; đối với chiều dài cố định chỉ cần loại bỏ các bit độ dài, điều đó thực sự làm cho nó dễ dàng hơn.

Phân bổ khối 16 con trỏ. Các bit con trỏ ít quan trọng nhất có thể được sử dụng lại, vì các khối của bạn sẽ luôn được căn chỉnh. Bạn có thể muốn một bộ cấp phát lưu trữ đặc biệt cho nó (chia lưu trữ lớn thành các khối nhỏ hơn). Có một số loại khối khác nhau:

  • Mã hóa với 7 bit độ dài của chuỗi có độ dài thay đổi. Khi chúng đầy, bạn thay thế chúng bằng cách:
  • Vị trí mã hóa hai ký tự tiếp theo, bạn có 16 con trỏ tới các khối tiếp theo, kết thúc bằng:
  • Mã hóa bitmap của ba ký tự cuối cùng của chuỗi.

Đối với mỗi loại khối, bạn cần lưu trữ thông tin khác nhau trong LSB. Khi bạn có các chuỗi có độ dài thay đổi, bạn cũng cần lưu trữ chuỗi cuối và loại khối cuối cùng chỉ có thể được sử dụng cho các chuỗi dài nhất. 7 bit chiều dài nên được thay thế bằng ít hơn khi bạn tiến sâu hơn vào cấu trúc.

Điều này cung cấp cho bạn một bộ lưu trữ hợp lý rất nhanh và rất hiệu quả bộ nhớ của các chuỗi được sắp xếp. Nó sẽ hành xử hơi giống như một trie . Để làm việc này, hãy đảm bảo xây dựng đủ các bài kiểm tra đơn vị. Bạn muốn bảo hiểm của tất cả các chuyển tiếp khối. Bạn muốn bắt đầu chỉ với loại khối thứ hai.

Để có hiệu suất cao hơn nữa, bạn có thể muốn thêm các loại khối khác nhau và kích thước khối lớn hơn. Nếu các khối luôn có cùng kích thước và đủ lớn, bạn có thể sử dụng ít bit hơn cho các con trỏ. Với kích thước khối 16 con trỏ, bạn đã có một byte miễn phí trong không gian địa chỉ 32 bit. Hãy xem tài liệu về cây Judy cho các loại khối thú vị. Về cơ bản, bạn thêm mã và thời gian kỹ thuật để đánh đổi không gian (và thời gian chạy)

Bạn có thể muốn bắt đầu với một cơ số trực tiếp rộng 256 cho bốn ký tự đầu tiên. Điều đó cung cấp một sự đánh đổi không gian / thời gian đàng hoàng. Trong triển khai này, bạn nhận được ít bộ nhớ hơn nhiều so với một bộ ba đơn giản; nó nhỏ hơn khoảng ba lần (tôi chưa đo). O (n) không có vấn đề gì nếu hằng số đủ thấp, như bạn nhận thấy khi so sánh với quicksort O (n log n).

Bạn có quan tâm đến việc xử lý gấp đôi? Với trình tự ngắn, sẽ có. Điều chỉnh các khối để xử lý số lượng là khó khăn, nhưng nó có thể rất hiệu quả về không gian.


Tôi không thấy cách sắp xếp radix trở nên dễ dàng hơn trong trường hợp của tôi nếu tôi sử dụng một đại diện đóng gói bit. Nhân tiện, khung công tác tôi sử dụng thực sự cung cấp khả năng sử dụng một đại diện được đóng gói bit nhưng điều này hoàn toàn minh bạch đối với tôi với tư cách là người dùng giao diện.
Konrad Rudolph

Không phải khi bạn nhìn vào đồng hồ bấm giờ của bạn :)
Stephan Eggermont

Tôi chắc chắn sẽ nhìn vào cây Judy. Vanilla cố gắng không thực sự mang lại nhiều thứ cho bàn vì chúng hoạt động cơ bản giống như một loại cơ chế MSD bình thường với ít lần vượt qua các yếu tố hơn nhưng cần thêm dung lượng lưu trữ.
Konrad Rudolph
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.