сортировка двусвязных списков с помощью сортировки слиянием - PullRequest
10 голосов
/ 30 мая 2010

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

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

Ответы [ 5 ]

6 голосов
/ 30 мая 2010

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

Followup

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

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

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

Для запуска

  1. Сохранить код в файл с именем MergeSort.java
  2. Пробег javac MergeSort.java
  3. Пробег java MergeSort
  4. Marvel
  5. При необходимости запустите javadoc -private MergeSort.java для создания документации. Откройте файл index.html, который он создает.
3 голосов
/ 20 мая 2011

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

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

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

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

  2. На первом шаге рекурсии вам нужно передать элемент first и элемент в позиции n/2. Это займет n/2 шагов, чтобы найти.

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

  4. На каждом последующем рекурсивном шаге количество частей списка удваивается, а длины делятся на два:

    • 4 * n/8 в 3-й глубине рекурсии

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

  5. Глубина рекурсии составляет 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-битной) Для mergeSort это будет 12 байт на вызов, умноженный на глубину рекурсии log (n) плюс 20 байт для переменных слияния, таким образом: 12 * log (n) +20 байт.

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

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

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

1.000.000 Items:  466ms
8.300.000 Items: 5144ms

Быстрая сортировка:

1.000.000 Items:  696ms  
8.300.000 Items: 8131ms

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

3 голосов
/ 30 мая 2010

Это зависит от того, что 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 нет указателей, есть только ссылки.
1 голос
/ 30 мая 2010

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

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)

0 голосов
/ 20 мая 2011

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

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

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

...