Как реализовать многопоточную сортировку MergeSort в Java

Большинство примеров сортировки слиянием, которые я нахожу, выполняются в одном потоке. Это в первую очередь сводит на нет некоторые преимущества использования алгоритма сортировки слиянием. Может ли кто-нибудь показать, как правильно написать алгоритм сортировки слиянием в Java с использованием многопоточности.

Решение должно использовать функции из последней версии java, если это применимо. Многие решения, уже присутствующие в Stackoverflow, используют простые потоки. Я ищу решение, демонстрирующее ForkJoin с RecursiveTask, который, по-видимому, является основным вариантом использования класса RecursiveTask.

Акцент должен быть сделан на демонстрации алгоритма с превосходными характеристиками производительности, включая временную и пространственную сложность, где это возможно.

ПРИМЕЧАНИЕ. Ни один из предложенных повторяющихся вопросов не применим, поскольку ни один из них не предлагает решения с использованием рекурсивной задачи, которая была именно тем, о чем спрашивал этот вопрос.


person Jeffrey Phillips Freeman    schedule 26.04.2018    source источник
comment
Вот ответ с использованием RecursiveAction (если вы не можете использовать Java 8): stackoverflow.com/a/2210249/14955   -  person Thilo    schedule 26.04.2018
comment
@Thilo Спасибо, мне нужна была демонстрация RecursiveTask, но эта ссылка может быть полезна другим.   -  person Jeffrey Phillips Freeman    schedule 26.04.2018
comment
Об этом утверждении в вопросе: Это сводит на нет преимущество использования алгоритма сортировки слиянием в первую очередь Это совсем не так. Цель сортировки слиянием - не быть многопоточным алгоритмом. Это отличный алгоритм сортировки также в однопоточной среде.   -  person Lii    schedule 26.04.2018
comment
@Lii Спасибо, отредактировал вопрос, чтобы исправить это.   -  person Jeffrey Phillips Freeman    schedule 26.04.2018
comment
@ mark-rotteveel это не дубликат, эти ссылки не соответствуют критериям, указанным в вопросе.   -  person Jeffrey Phillips Freeman    schedule 26.04.2018
comment
@Thilo Может быть, поможет удалить дубликат флага? Ни одна из этих ссылок не предоставляет решения RecursiveTask в соответствии с запрошенным вопросом.   -  person Jeffrey Phillips Freeman    schedule 26.04.2018
comment
@JeffreyPhillipsFreeman Вы сами проголосовали за то, чтобы закрыть эти другие вопросы как дубликаты ваших собственных, поэтому, по вашему мнению, они были дубликатами вашего вопроса. Я только изменил направление этих голосов на более старые. Ты не можешь получить свой пирог, и он тоже его съест ...   -  person Mark Rotteveel    schedule 27.04.2018
comment
@MarkRotteveel Не будь таким драматичным. Как я объяснил, флаг дублирования имел смысл только в одном направлении. Этот вопрос более конкретный, чем другой. Вы абсурдны.   -  person Jeffrey Phillips Freeman    schedule 27.04.2018
comment
Не я здесь абсурдный. Если другие вопросы были дубликатами ваших, то и ваш дубликат. Если это не так, то вам не следовало голосовать за то, чтобы закрыть их как дубликаты. Я только что принял это самопровозглашенное дублирование и отдал предпочтение более устоявшимся вопросам в качестве цели, поскольку это обычное правило при подаче дублированных голосов. Если вы теперь измените аргумент, что ваш вопрос является более конкретным, то вам не следовало голосовать за закрытие как дубликат этих более общих вопросов.   -  person Mark Rotteveel    schedule 27.04.2018
comment
@MarkRotteveel Очевидно, я здесь новенький и впервые использовал систему флагов. Насколько я понимаю, и, возможно, неверно, это односторонний флаг, указывающий, что какой-то вопрос дает ответ на текущий. Если это неправильный флаг, когда один вопрос просто более конкретен, чем другой, пусть будет так, поэтому я сделал неправильный выбор. Смысл голосования в том, что я ожидал, что другие, более опытные, поймут это, а не возьмут верх над тем, чтобы преподать урок. Все это кажется очень детским ответом.   -  person Jeffrey Phillips Freeman    schedule 28.04.2018
comment
Я бы не стал называть новую учетную запись старше двух лет, но ладно. Если вы хотите связать вопросы между собой, рассмотрите возможность публикации комментария на этот счет, не голосуйте за закрытие как дубликаты, если вы действительно не считаете их дубликатами. В любом случае вы можете подумать о том, чтобы пометить свой ответ для внимания модератора и переместить его в Многопоточная быстрая сортировка или сортировка слиянием (в котором уже есть ответ, использующий fork / join + рекурсивное действие).   -  person Mark Rotteveel    schedule 28.04.2018
comment
@MarkRotteveel все эти 2 года моя учетная запись в основном не использовалась. Но ваш правильный комментарий, возможно, был лучше. Я подумал, что процесс голосования решит это сам. Я не особо беспокоюсь о том, чтобы пригласить модератора. Просто кажется с твоей стороны очень по-детски.   -  person Jeffrey Phillips Freeman    schedule 28.04.2018


Ответы (1)


Наиболее удобной парадигмой многопоточности для сортировки слиянием является парадигма fork-join. Это предоставляется в Java 8 и новее. Следующий код демонстрирует сортировку слиянием с использованием fork-join.

import java.util.*;
import java.util.concurrent.*;

public class MergeSort<N extends Comparable<N>> extends RecursiveTask<List<N>> {
    private List<N> elements;

    public MergeSort(List<N> elements) {
        this.elements = new ArrayList<>(elements);
    }

    @Override
    protected List<N> compute() {
        if(this.elements.size() <= 1)
            return this.elements;
        else {
            final int pivot = this.elements.size() / 2;
            MergeSort<N> leftTask = new MergeSort<N>(this.elements.subList(0, pivot));
            MergeSort<N> rightTask = new MergeSort<N>(this.elements.subList(pivot, this.elements.size()));

            leftTask.fork();
            rightTask.fork();

            List<N> left = leftTask.join();
            List<N> right = rightTask.join();

            return merge(left, right);
        }
    }

    private List<N> merge(List<N> left, List<N> right) {
        List<N> sorted = new ArrayList<>();
        while(!left.isEmpty() || !right.isEmpty()) {
            if(left.isEmpty())
                sorted.add(right.remove(0));
            else if(right.isEmpty())
                sorted.add(left.remove(0));
            else {
                if( left.get(0).compareTo(right.get(0)) < 0 )
                    sorted.add(left.remove(0));
                else
                    sorted.add(right.remove(0));
            }
        }

        return sorted;
    }

    public static void main(String[] args) {
        ForkJoinPool forkJoinPool = ForkJoinPool.commonPool();
        List<Integer> result = forkJoinPool.invoke(new MergeSort<Integer>(Arrays.asList(7,2,9,10,1)));
        System.out.println("result: " + result);
    }
}

Следующий вариант кода, который гораздо менее прямолинеен, устраняет чрезмерное копирование ArrayList. Первоначальный несортированный список создается только один раз, и вызовы подсписка не должны выполнять какое-либо копирование. Раньше мы копировали список массивов при каждом ответвлении алгоритма. Кроме того, теперь при объединении списков вместо создания нового списка и копирования в него значений каждый раз мы повторно используем левый список и вставляем в него наши значения. Избегая дополнительного шага копирования, мы улучшаем производительность. Здесь мы используем LinkedList, потому что вставки довольно дешевы по сравнению с ArrayList. Мы также исключаем вызов remove, который также может быть дорогостоящим для ArrayList.

import java.util.*;
import java.util.concurrent.*;

public class MergeSort<N extends Comparable<N>> extends RecursiveTask<List<N>> {
    private List<N> elements;

    public MergeSort(List<N> elements) {
        this.elements = elements;
    }

    @Override
    protected List<N> compute() {
        if(this.elements.size() <= 1)
            return new LinkedList<>(this.elements);
        else {
            final int pivot = this.elements.size() / 2;
            MergeSort<N> leftTask = new MergeSort<N>(this.elements.subList(0, pivot));
            MergeSort<N> rightTask = new MergeSort<N>(this.elements.subList(pivot, this.elements.size()));

            leftTask.fork();
            rightTask.fork();

            List<N> left = leftTask.join();
            List<N> right = rightTask.join();

            return merge(left, right);
        }
    }

    private List<N> merge(List<N> left, List<N> right) {
        int leftIndex = 0;
        int rightIndex = 0;
        while(leftIndex < left.size() || rightIndex < right.size()) {
            if(leftIndex >= left.size())
                left.add(leftIndex++, right.get(rightIndex++));
            else if(rightIndex >= right.size())
                return left;
            else {
                if( left.get(leftIndex).compareTo(right.get(rightIndex)) < 0 )
                    leftIndex++;
                else
                    left.add(leftIndex++, right.get(rightIndex++));
            }
        }

        return left;
    }

    public static void main(String[] args) {
        ForkJoinPool forkJoinPool = ForkJoinPool.commonPool();
        List<Integer> result = forkJoinPool.invoke(new MergeSort<Integer>(Arrays.asList(7,2,9,-7,777777,10,1)));
        System.out.println("result: " + result);
    }
}

Мы также можем улучшить код еще на один шаг, используя итераторы вместо прямого вызова get при выполнении слияния. Причина этого в том, что попадание в LinkedList по индексу имеет низкую временную производительность (линейную), поэтому с помощью итератора мы устраняем замедление, вызванное внутренней итерацией связанного списка при каждом получении. Вызов next на итераторе - это постоянное время, в отличие от линейного времени для получения вызова. Следующий код изменен для использования вместо него итераторов.

import java.util.*;
import java.util.concurrent.*;

public class MergeSort<N extends Comparable<N>> extends RecursiveTask<List<N>> {
    private List<N> elements;

    public MergeSort(List<N> elements) {
        this.elements = elements;
    }

    @Override
    protected List<N> compute() {
        if(this.elements.size() <= 1)
            return new LinkedList<>(this.elements);
        else {
            final int pivot = this.elements.size() / 2;
            MergeSort<N> leftTask = new MergeSort<N>(this.elements.subList(0, pivot));
            MergeSort<N> rightTask = new MergeSort<N>(this.elements.subList(pivot, this.elements.size()));

            leftTask.fork();
            rightTask.fork();

            List<N> left = leftTask.join();
            List<N> right = rightTask.join();

            return merge(left, right);
        }
    }

    private List<N> merge(List<N> left, List<N> right) {
        ListIterator<N> leftIter = left.listIterator();
        ListIterator<N> rightIter = right.listIterator();
        while(leftIter.hasNext() || rightIter.hasNext()) {
            if(!leftIter.hasNext()) {
                leftIter.add(rightIter.next());
                rightIter.remove();
            }
            else if(!rightIter.hasNext())
                return left;
            else {
                N rightElement = rightIter.next();
                if( leftIter.next().compareTo(rightElement) < 0 )
                    rightIter.previous();
                else {
                    leftIter.previous();
                    leftIter.add(rightElement);
                }
            }
        }

        return left;
    }

    public static void main(String[] args) {
        ForkJoinPool forkJoinPool = ForkJoinPool.commonPool();
        List<Integer> result = forkJoinPool.invoke(new MergeSort<Integer>(Arrays.asList(7,2,9,-7,777777,10,1)));
        System.out.println("result: " + result);
    }
}

Наконец, самые сложные версии кода, эта итерация использует полностью локальную операцию. Создается только начальный ArrayList, и никакие дополнительные коллекции никогда не создаются. Таким образом, логику особенно сложно соблюдать (поэтому я оставил ее напоследок). Но он должен быть максимально приближен к идеальной реализации.

import java.util.*;
import java.util.concurrent.*;

public class MergeSort<N extends Comparable<N>> extends RecursiveTask<List<N>> {
    private List<N> elements;

    public MergeSort(List<N> elements) {
        this.elements = elements;
    }

    @Override
    protected List<N> compute() {
        if(this.elements.size() <= 1)
            return this.elements;
        else {
            final int pivot = this.elements.size() / 2;
            MergeSort<N> leftTask = new MergeSort<N>(this.elements.subList(0, pivot));
            MergeSort<N> rightTask = new MergeSort<N>(this.elements.subList(pivot, this.elements.size()));

            leftTask.fork();
            rightTask.fork();

            List<N> left = leftTask.join();
            List<N> right = rightTask.join();

            merge(left, right);
            return this.elements;
        }
    }

    private void merge(List<N> left, List<N> right) {
        int leftIndex = 0;
        int rightIndex = 0;
        while(leftIndex < left.size() ) {
            if(rightIndex == 0) {
                if( left.get(leftIndex).compareTo(right.get(rightIndex)) > 0 ) {
                    swap(left, leftIndex++, right, rightIndex++);
                } else {
                    leftIndex++;
                }
            } else {
                if(rightIndex >= right.size()) {
                    if(right.get(0).compareTo(left.get(left.size() - 1)) < 0 )
                        merge(left, right);
                    else
                        return;
                }
                else if( right.get(0).compareTo(right.get(rightIndex)) < 0 ) {
                    swap(left, leftIndex++, right, 0);
                } else {
                    swap(left, leftIndex++, right, rightIndex++);
                }
            }
        }

        if(rightIndex < right.size() && rightIndex != 0)
            merge(right.subList(0, rightIndex), right.subList(rightIndex, right.size()));
    }

    private void swap(List<N> left, int leftIndex, List<N> right, int rightIndex) {
        //N leftElement = left.get(leftIndex);
        left.set(leftIndex, right.set(rightIndex, left.get(leftIndex)));
    }

    public static void main(String[] args) {
        ForkJoinPool forkJoinPool = ForkJoinPool.commonPool();
        List<Integer> result = forkJoinPool.invoke(new MergeSort<Integer>(new ArrayList<>(Arrays.asList(5,9,8,7,6,1,2,3,4))));
        System.out.println("result: " + result);
    }
}
person Jeffrey Phillips Freeman    schedule 26.04.2018
comment
Вероятно, это можно переписать для сортировки на месте во внутреннем массиве вместо выделения новых списков массивов для каждого merge шага. - person Thilo; 26.04.2018
comment
@Thilo Хорошее замечание. Я попробую это сделать и добавлю к своему ответу, если получу хорошее решение. - person Jeffrey Phillips Freeman; 26.04.2018
comment
@Thilo Ну вот, я тоже добавил версию с копированием на месте. По крайней мере, насколько я могу приблизиться к одному, насколько я могу судить. - person Jeffrey Phillips Freeman; 26.04.2018
comment
Я понял, что мою текущую реализацию на месте можно улучшить, используя итератор вместо get (из-за использования связанного списка). Собираюсь реализовать это исправление и обновить свой ответ ... - person Jeffrey Phillips Freeman; 26.04.2018
comment
@Thilo Я обновил код копирования на месте, чтобы использовать итераторы. Теперь это даже эффективнее, чем раньше. - person Jeffrey Phillips Freeman; 26.04.2018
comment
Хорошо, так что его не полностью на месте на последнем. Собираюсь сделать это как отдельный пост, поскольку он больше не будет RecursiveTask. - person Jeffrey Phillips Freeman; 26.04.2018
comment
Итак, вчера вечером я понял, как по-настоящему сделать его на 100% готовым. Просто добавлен последний фрагмент кода, демонстрирующий это. Это отличается от первого предоставленного мной фрагмента, который был только частично на месте. @Thilo - person Jeffrey Phillips Freeman; 26.04.2018