【问题标题】:sorting a doubly linked list with merge sort使用归并排序对双向链表进行排序
【发布时间】:2011-02-25 16:18:52
【问题描述】:

我在互联网上找到了这段代码,它是用于数组的,我想将它更改为双向链表(我们应该使用指针而不是索引)请你帮我看看如何更改合并方法(我有我自己改变了排序方法)这也不是我的家庭作业,我喜欢使用链表!

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

【问题讨论】:

    标签: java algorithm sorting linked-list mergesort


    【解决方案1】:

    合并排序需要经常拆分列表。迭代到 LinkedList 的中间不是您可以对其执行的最昂贵的操作(好吧,没有排序)?我可以看到合并步骤工作得很好(您正在向前迭代两个链表),但我不确定如果没有 O(1) 拆分操作,这个实现是否值得麻烦。

    跟进

    正如向我指出的那样,当您已经在进行 O(n) 操作时,O(n) 拆分操作并不会真正增加复杂性合并阶段。尽管如此,在进行迭代时,您仍然会遇到麻烦(不使用Iterator,而是在随机访问特性较差的List 上使用get)。

    我在调试其他问题时感到很无聊,所以给你写了我认为是该算法的一个不错的 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. 惊奇
    5. (可选)运行javadoc -private MergeSort.java 以创建文档。打开它创建的 index.html 文件。

    【讨论】:

    • 拆分操作确实很昂贵,但请注意,整体复杂度仍然是最佳的。递推关系为T(N) = 2T(N/2)+1.5N,很容易证明T(N) = O(N log N)
    【解决方案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 没有指针,只有引用。

    【讨论】:

    • 提及索引的 get/set 方法而不提及它们对于链表来说是 O(n) 对我来说似乎有点危险。编写排序算法时绝对不应该使用 get 和 set。
    【解决方案3】:

    我昨天遇到了这个问题。以下是一些想法。

    DoublyLinkedList 进行排序不同于对Array 进行排序,因为您不能对列表中的任意项进行基于索引的引用。相反,您需要在每个递归步骤中记住这些项目,然后将它们传递给合并函数。对于每个递归步骤,您只需要记住每个列表一半的第一项。如果您不记得这些项目,您很快就会得到索引,但这会导致您在merge-function 中使用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在第三递归深度

      • 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 字节。

    附:在 100 万个项目上测试的代码(需要 1200 毫秒)。此外,DoublyLinkedList 是一个容器,用于存储列表中的第一个 ListElement

    更新: 我已经使用相同的数据结构回答了关于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】:

        另一个想法是用列表的所有元素创建一个数组,对数组进行排序,然后再次将元素插入到列表中。

        优点:实现非常简单,如果列表合并排序的实现不佳,则速度更快(可能也比好的实现更快)

        Contra:使用一些额外的空间 (O(n))

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2012-03-07
          • 2016-06-19
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多