Сортировка двусвязного списка с сортировкой слияния

Я нашел этот код в Интернете, и он был для массивов, я хочу изменить его для дважды связанного списка (вместо индекса мы должны использовать указатель), пожалуйста, помогите мне, как я могу изменить метод слияния (у меня есть изменил метод сортировки сам) также это не моя домашняя работа, мне нравится работать со связанным списком!!

public class MergeSort {

private DoublyLinkedList LocalDoublyLinkedList;

public MergeSort(DoublyLinkedList list) {
    LocalDoublyLinkedList = list;

}

public void sort() {

    if (LocalDoublyLinkedList.size() <= 1) {
        return;
    }
    DoublyLinkedList listOne = new DoublyLinkedList();
    DoublyLinkedList listTwo = new DoublyLinkedList();
    for (int x = 0; x < (LocalDoublyLinkedList.size() / 2); x++) {
        listOne.add(x, LocalDoublyLinkedList.getValue(x));
}
for (int x = (LocalDoublyLinkedList.size() / 2) + 1; x < LocalDoublyLinkedList.size`(); x++) {`
    listTwo.add(x, LocalDoublyLinkedList.getValue(x));
}
//Split the DoublyLinkedList again
    MergeSort sort1 = new MergeSort(listOne);
    MergeSort sort2 = new MergeSort(listTwo);
    sort1.sort();
    sort2.sort();

    merge(listOne, listTwo);
}

private void merge(DoublyLinkedList a, DoublyLinkedList b) {
    int x = 0;
    int y = 0;
    int z = 0;
    while (x < first.length && y < second.length) {
        if (first[x] < second[y]) {
            a[z] = first[x];
            x++;
        } else {
            a[z] = second[y];
            y++;
        }
        z++;
    }
//copy remaining elements to the tail of a[];
    for (int i = x; i < first.length; i++) {
        a[z] = first[i];
        z++;
    }
    for (int i = y; i < second.length; i++) {
        a[z] = second[i];
        z++;
    }
}
}

Ответ 1

Сортировка сортировки требует частого разделения списка. Не повторяется ли в середине LinkedList довольно дорогостоящая операция, которую вы можете выполнить на нем (ну, не сортируя ее)? Я мог видеть, что шаг слияния работает очень хорошо (вы повторяете вперед по двум связанным спискам), но я не уверен, что эта реализация стоит проблем без операции разделения O (1).

Followup

Как мне указывалось, операция разделения O (n) на самом деле не усложняет задачу, когда вы уже выполняете O (n) вещи во время фазы слияния. Тем не менее, вы все равно столкнетесь с трудностями при выполнении итераций, как вы делаете (не используя Iterator, а вместо этого используя get на List со слабыми характеристиками произвольного доступа).

Мне было скучно при отладке какой-то другой проблемы, поэтому написал вам то, что я считаю достойной реализацией Java этого алгоритма. Я следовал за псевдокодом Wikipedia дословно и посыпался некоторыми обобщающими и печатными заявлениями. Если у вас есть какие-либо вопросы или проблемы, просто спросите.

import java.util.List;
import java.util.LinkedList;

/**
 * This class implements the mergesort operation, trying to stay
 * as close as possible to the implementation described on the
 * Wikipedia page for the algorithm. It is meant to work well
 * even on lists with non-constant random-access performance (i.e.
 * LinkedList), but assumes that {@code size()} and {@code get(0)}
 * are both constant-time.
 *
 * @author jasonmp85
 * @see <a href="http://en.wikipedia.org/wiki/Merge_sort">Merge sort</a>
 */
public class MergeSort {
    /**
     * Keeps track of the call depth for printing purposes
     */
    private static int depth = 0;

    /**
     * Creates a list of 10 random Longs and sorts it
     * using {@link #sort(List)}.
     *
     * Prints out the original list and the result.
     *
     */
    public static void main(String[] args) {
        LinkedList<Long> list = new LinkedList<Long>();

        for(int i = 0; i < 10; i++) {
            list.add((long)(Math.random() * 100));
        }

        System.out.println("ORIGINAL LIST\n" + 
                           "=================\n" +
                           list + "\n");

        List<Long> sorted = sort(list);

        System.out.println("\nFINAL LIST\n" +
                           "=================\n" +
                           sorted + "\n");
    }

    /**
     * Performs a merge sort of the items in {@code list} and returns a
     * new List.
     *
     * Does not make any calls to {@code List.get()} or {@code List.set()}.
     * 
     * Prints out the steps, indented based on call depth.
     *
     * @param list the list to sort
     */
    public static <T extends Comparable<T>> List<T> sort(List<T> list) {
        depth++;
        String tabs = getTabs();

        System.out.println(tabs + "Sorting: " + list);

        if(list.size() <= 1) {
            depth--;
            return list;
        }

        List<T> left   = new LinkedList<T>();
        List<T> right  = new LinkedList<T>();
        List<T> result = new LinkedList<T>();

        int middle = list.size() / 2;

        int added = 0;
        for(T item: list) {
            if(added++ < middle)
                left.add(item);
            else
                right.add(item);
        }

        left = sort(left);
        right = sort(right);

        result = merge(left, right);

        System.out.println(tabs + "Sorted to: " + result);

        depth--;
        return result;
    }

    /**
     * Performs the oh-so-important merge step. Merges {@code left}
     * and {@code right} into a new list, which is returned.
     *
     * @param left the left list
     * @param right the right list
     * @return a sorted version of the two lists' items
     */
    private static <T extends Comparable<T>> List<T> merge(List<T> left,
                                                           List<T> right) {
        String tabs = getTabs();
        System.out.println(tabs + "Merging: " + left + " & " + right);

        List<T> result = new LinkedList<T>();
        while(left.size() > 0 && right.size() > 0) {
            if(left.get(0).compareTo(right.get(0)) < 0)
                result.add(left.remove(0));
            else
                result.add(right.remove(0));
        }

        if(left.size() > 0)
            result.addAll(left);
        else
            result.addAll(right);

        return result;
    }

    /**
     * Returns a number of tabs based on the current call depth.
     *
     */
    private static String getTabs() {
        StringBuffer sb = new StringBuffer("");
        for(int i = 0; i < depth; i++)
            sb.append('\t');
        return sb.toString();
    }
}

Для запуска

  • Сохраните код в файл с именем MergeSort.java
  • Запустить javac MergeSort.java
  • Выполнить java MergeSort
  • Marvel
  • При необходимости запустите javadoc -private MergeSort.java, чтобы создать документацию. Откройте файл index.html, который он создает.

Ответ 2

Это зависит от того, что DoublyLinkedList - это конкретный пользовательский тип или просто псевдоним для связанного типа списка?

В первом случае вы должны иметь индексированные методы get/set и/или итератор, определенные в нем, что делает задачу простой.

В последнем случае почему бы не использовать стандартный java.util.LinkedList?

В терминах интерфейса List операция может быть реализована следующим образом:

<T> List<T> merge(List<T> first, List<T> second, List<T> merged) {
  if (first.isEmpty())
    merged.adAll(second);
  else if (second.isEmpty())
    merged.adAll(first);
  else {
    Iterator<T> firstIter = first.iterator();
    Iterator<T> secondIter = second.iterator();
    T firstElem = firstIter.next();
    T secondElem = secondIter.next();

    do {
      if (firstElem < secondElem) {
        merged.add(firstElem);
        firstElem = firstIter.hasNext() ? firstIter.next() : null;
      } else {
        merged.add(secondElem);
        secondElem = secondIter.hasNext() ? secondIter.next() : null;
      }
    } while (firstIter.hasNext() && secondIter.hasNext());
    //copy remaining elements to the tail of merged
    if (firstElem != null)
      merged.add(firstElem);
    if (secondElem != null)
      merged.add(secondElem);
    while (firstIter.hasNext()) {
      merged.add(firstIter.next());
    }
    while (secondIter.hasNext()) {
      merged.add(secondIter.next());
    }
  }
}

Эта реализация немного утомительна, чем при использовании массивов, главным образом, поскольку итераторы "потребляются" операцией next, поэтому необходимо вести учет текущего элемента в каждом списке. С get код будет проще, очень похож на решение массива, однако для больших списков он будет медленнее, как отметил @sepp2k.

Еще несколько заметок:

  • Традиция Java - использовать имена переменных нижнего регистра, следовательно localDoublyLinkedList
  • Java не имеет указателей, только ссылок.

Ответ 3

Я столкнулся с этой проблемой вчера. Вот несколько мыслей.

Сортировка DoublyLinkedList отличается от сортировки Array, поскольку вы не можете делать ссылки на основе индексов для любого произвольного элемента в Списке. Вместо этого вам нужно запомнить элементы во время каждого рекурсивного шага, а затем передать их функции слияния. Для каждого этапа рекурсии вам нужно запомнить только первый элемент из каждой половины списка. Если вы не помните эти элементы, вы быстро получите индексы, но это приведет к проблеме, которая в вашей merge -функции вам нужно пройти весь список с помощью for -loops, чтобы найти элементы для объединения. Это, в свою очередь, означает, что вы получаете сложность O(n^2).

Еще один важный момент - это шаг рекурсии в список и деление списка на две половины. Вы можете сделать этот шаг в рекурсивной части, используя for -loops. В отличие от merge -part на этом шаге for -loops приведет только к сложности O(log(n) * n/2), и это все еще ниже общей сложности O(n*log(n)). Вот почему:

  • Вам всегда нужно найти первый элемент каждой половины части списка.

  • На первом этапе рекурсии вам нужно передать элемент first и элемент в позиции n/2. Для этого требуется n/2 шагов.

  • На каждом следующем шаге вам нужно найти средний элемент для каждой из двух половин списка, который дает нам n/4, чтобы найти элемент в первой половине и n/4 в другой половине. В общей сложности это n/2.

  • В каждом последующем рекурсивном шаге количество двойников списка и длины разделяются на два:

    • 4 * n/8 на третьей глубине рекурсии

    • 8 * n/16 на 4-й глубине рекурсии и т.д.

  • Глубина рекурсии log(n), и на каждом шаге мы выполняем шаги n/2. Это равно O(log(n)*n/2)

Наконец, вот какой код:

public DoublyLinkedList mergesort(DoublyLinkedList in, int numOfElements) {
    in.first = mergesort(in.first, numOfElements);      
    return in;
}

слияние:

public ListElement mergesort(ListElement first, int length) {
    if(length > 1) {
        ListElement second = first;
        for(int i=0; i<length/2; i++) {
            second = second.next;
        }
        first = mergesort(first, length/2);
        second = mergesort(second, (length+1)/2);
        return merge(first, second, length);
    } else {
        return first;
    }
}

и слить:

public ListElement merge(ListElement first, ListElement second, int length) {
    ListElement result = first.prev; //remember the beginning of the new list will begin after its merged
    int right = 0;
    for(int i=0; i<length; i++) {
        if(first.getKey() <= second.getKey()) {
            if(first.next == second) break; //end of first list and all items in the second list are already sorted, thus break
            first = first.next;
        } else {
            if(right==(length+1)/2) 
                break; //we have merged all elements of the right list into the first list, thus break
            if(second == result) result = result.prev; //special case that we are mergin the last element then the result element moves one step back.
            ListElement nextSecond = second.next;
            //remove second
            second.prev.next = second.next;
            second.next.prev = second.prev;
            //insert second behind first.prev
            second.prev = first.prev;
            first.prev.next = second;
            //insert second before first
            second.next = first; 
            first.prev = second;
            //move on to the next item in the second list
            second = nextSecond;
            right++;
        }
    }
    return result.next; //return the beginning of the merged list
}

Максимальный объем используемой памяти также довольно низкий (не включая сам список). Исправьте меня, если я ошибаюсь, но он должен быть меньше 400 байт (на 32-битной). Это будет 12 байт за звонок на mergeSort раз глубина рекурсии log (n) плюс 20 байтов для переменных слияния, таким образом: 12 * log (n) +20 байтов.

P.S. Код проверен на 1 миллион предметов (занимает 1200 мс). Кроме того, DoublyLinkedList - это контейнер, в котором хранится первый ListElement списка.

Update: Я ответил на аналогичный вопрос о Quicksort, используя те же структуры данных, однако по сравнению с этой реализацией Mergesort он работает намного медленнее. Ниже приведены некоторые обновленные сроки для ссылки:

Сортировка слиянием:

1.000.000 Items:  466ms
8.300.000 Items: 5144ms

Quicksort:

1.000.000 Items:  696ms  
8.300.000 Items: 8131ms

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

Ответ 4

Прежде всего, вы НЕ должны использовать индексы при работе со связанными списками. Сделайте это так:

while (i < in.size/2){
  listOne.addLast( in.remove(in.first()) );
  i++
}
while(!in.isEmptly){
  listTwo.addLast( in.remove(in.first()) );
}

И для слияния

merge(a, b, out){
  while(!a.empty && !b.empty){
    if(a.first() >= b.first())
      out.addLast( a.remove(a.first()) );
    else
     out.addLast( b.remove(b.first()) );

  //remember to take care of the remaining elements 
  while(!a.empty)
    out.addLast( a.remove(a.first()) );
  while(!b.empty)
    out.addLast( b.remove(b.first()) );
}

Таким образом, все равно будет O (n log n)

Ответ 5

Другая идея - создать массив со всеми элементами списка, отсортировать массив и затем снова вставить элементы в список.

Pro: очень просто реализовать, быстрее, если плохая реализация списка mergesort (возможно, также быстрее, чем хорошие реализации)

Contra: использует дополнительное пространство (O (n))