Где-то чуть меньше года назад во время поиска работы, после окончания курсов в Иннополисе один из потенциальных работодателей дал вот такое задание.
Есть 100 млн. чисел, каждое из которых от 0 до 1млрд.
Нужно отсортировать по возрастанию.
В самом начале программа случайно их заполняет, а потом сортирует.
Подвох был в том, что туже самую таску давали и другим выпускникам нашего курса. Задание дали на дом на 1 день, после скайп интервью.
Сразу стало понятно, что реализация сортировки на уровне 11 класса, тут не прокатит.
Двигаясь от простого к сложному, реализовал сначало обычную быструю сортировку
/**
* Классический алгоритм быстрой сортировки
*/
public class QuickSort extends AbstractQuickSort {
public void sort(int[] a) {
sort(a, 0, a.length - 1);
}
private void sort(int[] a, int lo, int hi) {
if(hi <= lo) return;
// Находим средний элемент
int j = partition(a, lo, hi);
// Рекусивное вызов левой / правой подчасти
sort(a, lo, j - 1);
sort(a, j + 1, hi);
}
}
Затем алгоритм распараллелил, при помощи ForkJoinPool
/**
* Классический алгоритм быстрой сортировки с применением fork join для распаралеливания вычеслений
*/
public class ParallelQuickSort extends AbstractQuickSort {
public void sort(int[] a) {
ForkJoinPool.commonPool().invoke(new SortAction(a, 0, a.length - 1));
}
/**
* Реализация ForkJoinTask для рекурсивной сортировки частей массива
*/
private class SortAction extends RecursiveAction{
private final int bubbleBlock = 16;
int[] a;
int lo;
int hi;
SortAction(int[] a, int lo, int hi) {
this.a = a;
this.lo = lo;
this.hi = hi;
}
@Override
protected void compute() {
if(hi <= lo) return;
if ((hi - lo) > bubbleBlock) {
// Находим средний элемент
int j = partition(a, lo, hi);
// Рекусивное вызов левой / правой подчасти
invokeAll(new SortAction(a, lo, j - 1), new SortAction(a, j + 1, hi));
}else{
// Для маленького массива применим пызырьковую сортировку
bubbleSort(a, lo, hi + 1);
}
}
/**
* Сортировка пузырьком, для ускорении сортировки маленьких подблоков
*/
private void bubbleSort(int[] a, int lo, int hi){
for (int i = lo; i < hi; i++) {
for (int j = i; j < hi; j++) {
if (a[i] > a[j]) {
int tmp = a[i];
a[i] = a[j];
a[j] = tmp;
}
}
}
}
}
}
Для проверки качества решения, сравню полученные алгоритмы с сортировкой Stream API. Представлено время в секундах. i7-7700 3.6GHz, 16Гб, 4 ядра
Алгоритм | Моя быстрая сортировка | Stream API |
---|---|---|
Обычный | 11,64 | 10,2 |
Параллельный | 5,02 | 3,9 |
Неудивительно, что моё решение менее производительно, по сравнению с нативной реализацией в Stream API. Главное — порядок тот же, свою задачу я выполнил, получил прирост в скорости после распараллеливания.
Интервьюер дал положительную обратную связь. Однако в ту компанию я так и не устроился, так как меня перехватили в другой компании, где я собеседовался в то же время.
1) Ссылка на гит
2) Книга где взял базовый алгоритм
Update 1
Ребята в статье речь прежде всего идет про внедрение ForkJoinPool, а не про саму быструю сортировку.
Update 2
Для любителей сортировки подсчетом, время выделения в куче памяти 4Гб составляет около 13 секунд. Это даже без учета сaмой сортировки, что уже хуже любого из представленных вариантов.