Это длинный текст. Пожалуйста, потерпите меня. Сварился вопрос: Есть ли работоспособный алгоритм сортировки по основанию на месте?
У меня есть огромное количество строк малая фиксированная длина, в которых используются только буквы «A», «C», «G» и «T» (да, вы уже догадались: ДНК), которые я хочу отсортировать.
На данный момент я использую std::sort, который использует интросорт во всех распространенных реализациях STL. Это неплохо работает. Тем не менее, я убежден, что радиксная сортировка идеально подходит для моего набора задач и на практике должен работать с много лучше.
Я проверил это предположение с помощью очень наивной реализации, и для относительно небольших входных данных (порядка 10 000) это было правдой (ну, по крайней мере, более чем в два раза быстрее). Однако время выполнения ужасно ухудшается, когда размер проблемы становится больше (N> 5 000 000).
Причина очевидна: для поразрядной сортировки требуется копирование всех данных (на самом деле, более одного раза в моей наивной реализации). Это означает, что я поместил ~ 4 ГиБ в свою основную память, что явно снижает производительность. Даже если бы этого не произошло, я не могу позволить себе использовать такой объем памяти, поскольку размеры проблем фактически становятся еще больше.
В идеале этот алгоритм должен работать с любой длиной строки от 2 до 100, как для ДНК, так и для ДНК 5 (что позволяет использовать дополнительный подстановочный знак «N») или даже ДНК с ИЮПАКкоды неоднозначности (что дает 16 различных значений). Однако я понимаю, что все эти случаи не могут быть рассмотрены, поэтому я доволен любым улучшением скорости, которое я получаю. Код может динамически решать, какой алгоритм выполнять.
К сожалению, Статья в Википедии о радиксной сортировке бесполезен. Раздел про локальный вариант - полная чушь. Раздел NIST-DADS о радиксной сортировке практически не существует. Есть многообещающая статья под названием Эффективная адаптивная сортировка по основанию на месте, в которой описывается алгоритм «MSL». К сожалению, и эта статья неутешительна.
В частности, есть следующие моменты.
Во-первых, алгоритм содержит несколько ошибок и многое оставляет необъяснимым. В частности, он не детализирует вызов рекурсии (я просто предполагаю, что он увеличивает или уменьшает некоторый указатель для вычисления текущего сдвига и значений маски). Кроме того, он использует функции dest_group и dest_address, не давая определений. Я не понимаю, как это реализовать эффективно (то есть в O (1); по крайней мере, dest_address нетривиален).
И последнее, но не менее важное: алгоритм достигает на месте, меняя местами индексы массива с элементами внутри входного массива. Очевидно, это работает только с числовыми массивами. Мне нужно использовать его на струнах. Конечно, я мог бы просто испортить строгую типизацию и предположить, что память выдержит сохранение индекса там, где ей не место. Но это работает только до тех пор, пока я могу втиснуть свои строки в 32 бита памяти (при условии, что 32-битные целые числа). Это всего лишь 16 символов (давайте пока проигнорируем, что 16> log (5 000 000)).
Другая статья одного из авторов вообще не дает точного описания, но дает время выполнения MSL как сублинейную, что совершенно неверно.
Подвести итог: Есть ли надежда найти рабочую эталонную реализацию или, по крайней мере, хороший псевдокод / описание работающей локальной сортировки по основанию, которая работает со строками ДНК?
насколько малы маленькие струны фиксированной длины?
@EvilTeach: Я добавил варианты использования.
Каковы требования к выходу? Вы хотите просто создать отсортированный список? или они помещаются в базу данных для сопоставления? или же...?
@ Джейсон: Мне просто нужен список. Постобработка кардинально отличается. Одним из вариантов использования является создание массива суффиксов, такого как таблица поиска (с использованием k-мер вместо суффиксов). Текущая конструкция с быстрой сортировкой превосходит все обычные методы линейного времени.
Вопрос может быть не в порядке. На месте может быть так же плохо, как копирование, если нужно сделать много ходов.
@ Стефан: все в порядке. Но в случае промахов копирования / кеширования у меня просто задержка. В случае с памятью я достиг физического предела. Это просто не подлежит обсуждению. Все эти причудливые методы хранения частей данных на диске определенно медленнее, чем текущее решение для быстрой сортировки.
(продолжение) Решение dsimcha, с другой стороны, определенно Быстрее, чем quicksort для некоторых входных данных. Количество ходов может быть большим, а расположение кеша - небольшим, но в реальном мире все равно хорошо. Я также немного изменил решение, чтобы уменьшить количество замен, которые мне нужно выполнить.
@PeterMortensen На будущее я благодарен за исправления и ссылки для добавления контекста. Однако я не ценю особенно правки, которые являются чисто стилистическими («в порядке» против «в порядке», «то есть» против «то есть»).
Я понимаю, что это старый вопрос, но мне интересно, что вы пытались сделать. Я преподаю курс структур данных. У вас еще есть набор данных, с которым можно поиграть? Насколько велики были струны? А вы можете описать, для чего сортируете строки? Мне кажется, что то, что вы тогда делали с отсортированными данными, является важной недостающей частью этого вопроса. Например, если ваша цель состояла в том, чтобы иметь отсортированный набор последовательностей, которые вы затем могли бы попытаться объединить, я думаю, что потенциально создание дерева может быть намного быстрее, чем сортировка списка без изменений, но это всего лишь предположение.
@Dov Это было давным-давно, но я работал с секвенированием следующего поколения Illumina, считыванием (32 основания) и сортировкой, чтобы построить индекс поиска с постоянным временем (индекс q-грамм). Три, конечно, были бы возможны, но использование q-граммного индекса здесь оказалось эффективным на практике (Eland и т. д.). Если я правильно помню, тогда индекс будет запрошен графическим процессором - но, как я уже сказал, это было давно для проекта, над которым я больше не работаю.





Что ж, вот простая реализация сортировки по основанию 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);
}
}
Очевидно, что это специфично для ДНК, а не является общим, но это должно быть быстро.
Мне стало любопытно, действительно ли этот код работает, поэтому я протестировал / отладил его, ожидая запуска моего собственного биоинформатического кода. Приведенная выше версия сейчас фактически протестирована и работает. Для 10 миллионов последовательностей по 5 баз в каждой это примерно в 3 раза быстрее, чем при оптимизированной внутренней сортировке.
Если вы можете жить с подходом с 2-кратным проходом, он распространяется на radix-N: проход 1 = просто пройдите и посчитайте, сколько есть каждой из N цифр. Затем, если вы разбиваете массив, это говорит вам, где начинается каждая цифра. При проходе 2 выполняется замена на соответствующую позицию в массиве.
(например, для N = 4, если есть 90000 A, 80000 G, 100 C, 100000 T, тогда создайте массив, инициализированный кумулятивными суммами = [0, 90000, 170000, 170100], который используется вместо ваших APos, CPos и т. Д. В качестве курсора, на который должен быть заменен следующий элемент для каждой цифры.)
Я не уверен, какова будет связь между двоичным представлением и этим строковым представлением, помимо использования как минимум в 4 раза больше памяти, чем необходимо.
Как скорость с более длинными последовательностями? Не хватает разных длиной 5
Эта сортировка по основанию счисления выглядит как частный случай сортировки по американскому флагу - хорошо известного варианта сортировки по месту.
Совершенно не связано, но какую IDE вы используете для D? Пытался найти хороший ...
CodeBlocks. Это отстой (в основном это просто редактор плюс базовая автоматизация сборки), и я искал что-то получше, но пока нет хороших IDE для D2 (новейшая версия языка), а D имеет достаточно функций уровня языка, чтобы исключите шаблонный код, для которого вам не нужна хорошая IDE, как для некоторых других языков.
Этот код больше похож на быструю сортировку с предопределенной точкой поворота. Предложение Джейсона С. кажется намного проще распространить на более длинные префиксы.
Если у вас такой большой набор данных, я бы подумал, что лучше всего использовать дисковый буфер:
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 сегментов), таким образом вы сделаете меньше ветвей дискового буфера. Это может улучшить производительность, а может и не улучшить, поэтому поэкспериментируйте.
Мы используем файлы с отображением памяти для некоторых приложений. Однако в целом мы работаем, исходя из предположения, что машина предоставляет лишь достаточно оперативной памяти, чтобы не требовать явной поддержки диска (конечно, свопинг все еще имеет место). Но мы уже разрабатываем механизм для автоматических дисковых массивов.
Я никогда не видел сортировки по основанию на месте, и, судя по природе сортировки по основанию, я сомневаюсь, что она намного быстрее, чем сортировка вне места, если временный массив помещается в память.
Причина:
Сортировка выполняет линейное чтение входного массива, но все записи будут почти случайными. Начиная с определенного N и выше, это сводится к пропуску кеш-памяти при каждой записи. Этот промах в кеше замедляет работу вашего алгоритма. Если он на месте или нет, этот эффект не изменится.
Я знаю, что это не ответит на ваш вопрос напрямую, но если сортировка является узким местом, вы можете взглянуть на алгоритмы рядом с сортировкой как на шаг предварительной обработки (wiki-страница в soft-heap может помочь вам начать).
Это могло бы дать очень хороший прирост локальности кеша. Тогда лучше будет справиться сортировка по неуместному основанию из учебника. Записи по-прежнему будут почти случайными, но, по крайней мере, они будут сгруппированы вокруг одних и тех же блоков памяти и, как таковые, увеличат коэффициент попадания в кеш.
Я понятия не имею, сработает ли это на практике.
Кстати: если вы имеете дело только со строками ДНК: вы можете сжать символ на два бита и довольно много упаковать данные. Это сократит потребность в памяти в четыре раза по сравнению с обычным представлением. Адресация становится более сложной, но у ALU вашего процессора в любом случае есть много времени, чтобы потратить его во время всех промахов кеша.
Два хороших момента; ближняя сортировка - это новая для меня концепция, я должен прочитать об этом. Промахи в кэше - еще одна причина, о которой я мечтаю. ;-) Надо будет посмотреть об этом.
Для меня это тоже ново (пару месяцев), но как только у вас появится концепция, вы начнете видеть возможности повышения производительности.
Записи далеки от почти случайный, если ваша система счисления не очень большая. Например, если вы сортируете по одному символу за раз (сортировка по основанию 4), все записи будут выполняться в одну из 4 линейно растущих корзин. Это удобно как для кеширования, так и для предварительной выборки. Конечно, вы можете захотеть использовать более крупную систему счисления, и при определенном указателе вы столкнетесь с компромиссом между удобством использования кеша и предварительной выборки и размером системы счисления. Вы можете сдвинуть точку безубыточности в сторону больших радиусов, используя предварительную выборку программного обеспечения или временную область для ваших корзин с периодической промывкой до «настоящих» корзин.
Я собираюсь рискнуть и предложить вам перейти на реализацию heap / heapsort. Это предложение содержит некоторые предположения:
Прелесть сортировки кучи / кучи заключается в том, что вы можете создавать кучу во время чтения данных, и вы можете начать получать результаты в тот момент, когда вы создали кучу.
Вернемся назад. Если вам так повезло, что вы можете читать данные асинхронно (то есть вы можете отправить какой-то запрос на чтение и получить уведомление, когда некоторые данные будут готовы), а затем вы можете построить кусок кучи, пока вы ждете следующий блок данных - даже с диска. Часто этот подход может похоронить большую часть стоимости половины вашей сортировки за время, потраченное на получение данных.
Как только вы прочитали данные, первый элемент уже доступен. В зависимости от того, куда вы отправляете данные, это может быть здорово. Если вы отправляете его другому асинхронному считывателю, или какой-либо параллельной модели «событий» или пользовательскому интерфейсу, вы можете отправлять фрагменты и фрагменты по мере продвижения.
Тем не менее, если у вас нет контроля над тем, как данные читаются, и они читаются синхронно, и вы не можете использовать отсортированные данные, пока они не будут полностью записаны, игнорируйте все это. :(
См. Статьи в Википедии:
Хорошее предложение. Однако я уже пробовал это, и в моем конкретном случае накладные расходы на поддержание кучи больше, чем просто накопление данных в векторе и сортировка после получения всех данных.
Сначала подумайте о коде вашей проблемы. Избавьтесь от строк, замените их двоичным представлением. Используйте первый байт, чтобы указать длину + кодировку. В качестве альтернативы используйте представление фиксированной длины на четырехбайтовой границе. Тогда сортировка по основанию системы счисления становится намного проще. Для поразрядной сортировки самое важное - не иметь обработки исключений в горячей точке внутреннего цикла.
Хорошо, я подумал немного больше о проблеме с четырьмя числами. Для этого вам нужно решение, подобное Джуди дерево. Следующее решение может обрабатывать строки переменной длины; для фиксированной длины просто удалите биты длины, что на самом деле упрощает задачу.
Выделяем блоки по 16 указателей. Младший бит указателей можно использовать повторно, так как ваши блоки всегда будут выровнены. Вам может понадобиться специальный распределитель памяти для него (разбиение большого хранилища на более мелкие блоки). Есть несколько видов блоков:
Для каждого типа блока вам необходимо хранить различную информацию в младших разрядах. Поскольку у вас есть строки переменной длины, вам также необходимо хранить конец строки, а последний тип блока может использоваться только для самых длинных строк. 7 битов длины следует заменять на меньшее по мере того, как вы углубляетесь в структуру.
Это дает вам достаточно быстрое и очень эффективное по памяти хранение отсортированных строк. Он будет вести себя как три. Чтобы это работало, обязательно создайте достаточно модульных тестов. Вам нужно охватить все переходы блоков. Вы хотите начать только со вторым типом блоков.
Для еще большей производительности вы можете добавить различные типы блоков и больший размер блока. Если блоки всегда одного и того же размера и достаточно велики, вы можете использовать еще меньше бит для указателей. При размере блока в 16 указателей у вас уже есть свободный байт в 32-битном адресном пространстве. Взгляните на документацию по дереву Джуди, чтобы найти интересные типы блоков. По сути, вы добавляете код и время разработки в обмен на пространство (и время выполнения).
Вероятно, вы захотите начать с 256-кратного прямого основания для первых четырех символов. Это обеспечивает достойный компромисс между пространством и временем. В этой реализации вы получаете гораздо меньше накладных расходов на память, чем с простым деревом; он примерно в три раза меньше (не измерял). O (n) не проблема, если константа достаточно мала, как вы заметили при сравнении с быстрой сортировкой O (n log n).
Вы заинтересованы в работе с дублями? С короткими последовательностями они будут. Адаптировать блоки для обработки подсчетов сложно, но это может быть очень компактно.
Я не понимаю, как в моем случае становится проще сортировка по основанию счисления, если я использую битовое представление. Кстати, фреймворк, который я использую, на самом деле предоставляет возможность использования битового представления, но это совершенно прозрачно для меня как пользователя интерфейса.
Не когда смотришь на секундомер :)
Я обязательно посмотрю на деревья Джуди. Ванильные попытки на самом деле не приносят много пользы, потому что они ведут себя в основном как обычная сортировка по основанию MSD с меньшим количеством проходов по элементам, но требуют дополнительного хранилища.
Вы, конечно, можете отказаться от требований к памяти, кодируя последовательность в битах. Вы смотрите на перестановки, поэтому для длины 2 с «ACGT» это 16 состояний или 4 бита. Для длины 3 это 64 состояния, которые можно закодировать в 6 бит. Таким образом, это выглядит как 2 бита для каждой буквы в последовательности или примерно 32 бита для 16 символов, как вы сказали.
Если есть способ уменьшить количество допустимых «слов», возможно дальнейшее сжатие.
Таким образом, для последовательностей длиной 3 можно создать 64 сегмента, возможно, размером uint32 или uint64. Инициализируйте их обнулением. Просмотрите ваш очень большой список из 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;
}
Зачем сравнивать, когда можно хешировать, а?
Чертовски верно. Производительность обычно является проблемой при любой обработке ДНК.
Вы можете попробовать использовать три. Сортировка данных - это просто итерация по набору данных и его вставка; структура сортируется естественным образом, и вы можете думать о ней как о B-дереве (за исключением того, что вместо сравнения вы всегда используете косвенные ссылки указателя).
Поведение кэширования будет благоприятствовать всем внутренним узлам, поэтому вы, вероятно, не улучшите его; но вы также можете поиграть с коэффициентом ветвления вашего дерева (убедитесь, что каждый узел помещается в одну строку кэша, выделите узлы дерева, аналогичные куче, в виде непрерывного массива, который представляет обход порядка уровней). Поскольку попытки также являются цифровыми структурами (O (k) insert / find / delete для элементов длины k), у вас должна быть конкурентоспособная производительность для сортировки по основанию.
У trie та же проблема, что и у моей наивной реализации: для него требуется O (n) дополнительной памяти, что просто слишком много.
Сортировка по основанию MSB в dsimcha выглядит неплохо, но Нильс ближе к сути проблемы обнаруживает, что локальность кеша убивает вас при больших размерах проблем.
Предлагаю очень простой подход:
m наибольшего размера, для которого эффективна сортировка по основанию.m за один раз, отсортируйте их по основанию и запишите их (в буфер памяти, если у вас достаточно памяти, но в противном случае в файл), пока вы не исчерпаете свой ввод.Сортировка слиянием - это наиболее удобный для кеша алгоритм сортировки, о котором я знаю: «Прочтите следующий элемент из массива A или B, затем запишите элемент в выходной буфер». Он эффективно работает на ленточные накопители. Для сортировки элементов 2n требуется пространство n, но я держу пари, что значительно улучшенная локализация кеша, которую вы увидите, сделает это неважным - и если вы использовали сортировку без места, вам нужно это дополнительное пространство в любом случае.
Наконец, обратите внимание, что сортировка слиянием может быть реализована без рекурсии, и, по сути, это проясняет истинный линейный шаблон доступа к памяти.
Похоже, вы решили проблему, но для протокола, похоже, что одной из версий работоспособной сортировки по основанию на месте является "Сортировка по американскому флагу". Это описано здесь: Инженерная сортировка по основанию Radix. Общая идея состоит в том, чтобы выполнить 2 прохода для каждого символа - сначала посчитайте, сколько из них у вас есть, чтобы вы могли разделить входной массив на ячейки. Затем пройдите еще раз, переставляя каждый элемент в правильную корзину. Теперь рекурсивно отсортируйте каждую ячейку по позиции следующего символа.
На самом деле, решение, которое я использую, очень близко связано с алгоритмом сортировки флагов. Я не знаю, есть ли какое-нибудь различие.
Никогда не слышал о сортировке по американскому флагу, но, похоже, это то, что я закодировал: coliru.stacked-crooked.com/a/94eb75fbecc39066 В настоящее время он превосходит std::sort, и я уверен, что многозначный дигитайзер мог бы работать еще быстрее, но у моего набора тестов проблемы с памятью (не алгоритм, набор тестов сам)
@KonradRudolph: Большое различие между сортировкой по флагу и другими сортировками по системе счисления - это проход подсчета. Вы правы в том, что все виды Radix очень тесно связаны, но я бы не стал считать вашу сортировку Flag.
@MooingDuck: Я просто вдохновился вашим примером - я застрял в своей собственной независимой реализации, а ваша помогла мне вернуться на правильный путь. Спасибо! Одна возможная оптимизация - я не продвинулся здесь достаточно далеко, чтобы увидеть, стоит ли она еще: если элемент в позиции, которую вы меняете местами, уже оказался там, где он должен быть, вы можете пропустить это и перейти к тому, который нет. Обнаружение этого, конечно, потребует дополнительной логики и, возможно, дополнительного хранилища, но, поскольку свопы дороги по сравнению со сравнениями, это, возможно, стоит сделать.
Я бы взрывная сортировка представлял упакованные биты строк. Утверждается, что Burstsort имеет гораздо лучшую локальность, чем сортировка по основанию, уменьшая использование дополнительного пространства с помощью пакетных попыток вместо классических попыток. На оригинальной бумаге есть размеры.
Radix-Sort не учитывает кеширование и не является самым быстрым алгоритмом сортировки для больших наборов. Вы можете посмотреть:
Вы также можете использовать сжатие и кодировать каждую букву вашей ДНК в 2 бита перед сохранением в массиве сортировки.
Билл: не могли бы вы объяснить, какие преимущества эта функция qsort имеет по сравнению с функцией std::sort, предоставляемой C++? В частности, последний реализует сложный интросорт в современных библиотеках и встраивает операцию сравнения. Я не верю утверждению, что он выполняется за O (n) в большинстве случаев, поскольку для этого потребуется степень самоанализа, недоступная в общем случае (по крайней мере, без накладных расходов много).
Я не использую C++, но в моих тестах встроенный QSORT может быть в 3 раза быстрее, чем qsort в stdlib. Ti7qsort - это самая быстрая сортировка для целых чисел (быстрее, чем встроенный QSORT). Вы также можете использовать его для сортировки небольших данных фиксированного размера. Вы должны провести тесты со своими данными.
С точки зрения производительности вы можете захотеть взглянуть на более общие алгоритмы сортировки сравнения строк.
В настоящее время вы касаетесь каждого элемента каждой струны, но вы можете добиться большего!
В частности, для этого случая очень хорошо подходит пакетная сортировка. В качестве бонуса, поскольку пакетная сортировка основана на попытках, она работает смехотворно хорошо для небольших размеров алфавита, используемых в ДНК / РНК, поскольку вам не нужно встраивать какой-либо тройной узел поиска, хэш или другую схему сжатия узла дерева. три реализации. Попытки также могут быть полезны для вашей конечной цели, подобной массиву суффиксов.
Приличная реализация пакетной сортировки общего назначения доступна в кузнице исходного кода по адресу http://sourceforge.net/projects/burstsort/, но ее нет на месте.
В целях сравнения, реализация C-burstsort, охваченная тестами http://www.cs.mu.oz.au/~rsinha/papers/SinhaRingZobel-2006.pdf, в 4-5 раз быстрее, чем сортировка по принципу быстрой сортировки и сортировки по основанию для некоторых типичных рабочих нагрузок.
Мне обязательно нужно взглянуть на сортировку пакетов - хотя на данный момент я не понимаю, как можно построить дерево на месте. В целом массивы суффиксов почти полностью заменяют суффиксные деревья (и, следовательно, попытки) в биоинформатике из-за превосходных характеристик производительности в практических приложениях.
Вы захотите взглянуть на Крупномасштабная обработка последовательности генома от Drs. Касахара и Моришита.
Строки, состоящие из четырех нуклеотидных букв A, C, G и T, могут быть специально закодированы в целые числа для более быстрой обработки много. Радиксная сортировка - один из многих алгоритмов, обсуждаемых в книге; вы сможете адаптировать принятый ответ на этот вопрос и увидеть значительное улучшение производительности.
Сортировка по основанию, представленная в этой книге, не соответствует требованиям, поэтому ее нельзя использовать для этой цели. Что касается сжатия строк, я (конечно) уже этим занимаюсь. Мое (более или менее) окончательное решение (опубликованное ниже) не показывает этого, потому что библиотека позволяет мне обращаться с ними как с обычными строками, но используемое значение RADIX, конечно, можно (и есть) адаптировать к более крупным значениям.
«Сортировка Radix без лишнего места» - это документ, посвященный вашей проблеме.
Выглядит многообещающе, хотя проблема уже решена. Тем не менее, это входит в мою справочную библиотеку.
Хотя принятый ответ полностью отвечает описанию проблемы, я дошел до этого места, тщетно пытаясь найти алгоритм для разделения встроенного массива на N частей. Я сам написал одну, вот она.
Предупреждение: это нестабильный алгоритм разбиения, поэтому для многоуровневого разбиения необходимо переразбивать каждый результирующий раздел, а не весь массив. Преимущество в том, что он встроенный.
Способ, которым это помогает с поставленным вопросом, заключается в том, что вы можете многократно разбивать встроенные разделы на основе буквы строки, а затем сортировать разделы, когда они достаточно малы, с помощью алгоритма по вашему выбору.
function partitionInPlace(input, partitionFunction, numPartitions, startIndex=0, endIndex=-1) {
if (endIndex===-1) endIndex=input.length;
const starts = Array.from({ length: numPartitions + 1 }, () => 0);
for (let i = startIndex; i < endIndex; i++) {
const val = input[i];
const partByte = partitionFunction(val);
starts[partByte]++;
}
let prev = startIndex;
for (let i = 0; i < numPartitions; i++) {
const p = prev;
prev += starts[i];
starts[i] = p;
}
const indexes = [...starts];
starts[numPartitions] = prev;
let bucket = 0;
while (bucket < numPartitions) {
const start = starts[bucket];
const end = starts[bucket + 1];
if (end - start < 1) {
bucket++;
continue;
}
let index = indexes[bucket];
if (index === end) {
bucket++;
continue;
}
let val = input[index];
let destBucket = partitionFunction(val);
if (destBucket === bucket) {
indexes[bucket] = index + 1;
continue;
}
let dest;
do {
dest = indexes[destBucket] - 1;
let destVal;
let destValBucket = destBucket;
while (destValBucket === destBucket) {
dest++;
destVal = input[dest];
destValBucket = partitionFunction(destVal);
}
input[dest] = val;
indexes[destBucket] = dest + 1;
val = destVal;
destBucket = destValBucket;
} while (dest !== index)
}
return starts;
}
Это прекрасно написанный вопрос.