【问题标题】:Optimal Quicksort for Single Linked List单链表的最优快速排序
【发布时间】:2013-01-26 04:31:21
【问题描述】:

我正在努力实现一个快速排序功能来对单链表进行排序。我必须使用什么算法来完成这个?对于链表,每次比较都需要 O(N) 的最坏情况,而不是数组通常的 O(1)。那么最坏情况下的复杂度是多少呢?

总而言之,我需要对快速排序算法进行哪些修改才能获得最佳排序算法,以及算法的最坏情况复杂度是多少?

谢谢!

我在下面有一个实现:

public static SingleLinkedList quickSort(SingleLinkedList list, SLNode first, SLNode last)
{
    if (first != null && last != null)
    {
        SLNode p = partition(list, first, last) ;
        quickSort(list,first,p) ;
        quickSort(list,p.succ, last) ;
    }
    return list ;
}

public static SLLNode partition(SinlgleLinkedList list, SLNode first, SLNode last)
{

    SLNode p = first ;
    SLNode ptr = p.succ ;

    while (ptr!=null)
    {
        if (ptr.data.compareToIgnoreCase(p.data)<0)
        {
            String pivot = p.data ;
            p.data =  ptr.data ;
            ptr.data = p.succ.data ;
            p.succ.data = pivot ;
            p = p.succ ;
        }
        ptr = ptr.succ ;
    }
    return p ;
}

【问题讨论】:

  • 基本上,不要使用带有链表样式数据结构的快速排序。你想要合并排序。对于随机访问为 O(n) 的任何数据结构,快速排序总是表现不佳。
  • 我特别想为学习目的实现快速排序。
  • 你可以使用 qsort (x:xs) = qsort (filter (&lt; x) xs) ++ [x] ++ qsort (filter (&gt;= x) xs) 算法虽然 some consider that it is not "true" quicksort 因为分区没有到位。
  • 不要在节点之间移动数据,而是重新排列节点。分区时,从现有节点构建两个单独的列表,然后加入它们。
  • @Yuushi 请看下面的代码。小心,快速排序不需要一般的随机访问,所以它在链表上的性能很好。

标签: algorithm sorting linked-list quicksort


【解决方案1】:

Mergesort 对于链表实现起来更自然,但是您可以很好地进行快速排序。下面是我在几个应用程序中使用的 C 语言中的一个。

您不能有效地使用列表进行快速排序,这是一个普遍的误解。这不是真的,尽管需要仔细实施。

为了回答您的问题,列表的快速排序算法与数组的基本相同。选择一个枢轴(下面的代码使用列表的头部),围绕枢轴划分为两个列表,然后递归地对这些列表进行排序并将结果附加到中间的枢轴。有点不明显的是,如果您在排序结果的尾部添加要按原样附加的列表的参数,则可以在不额外传递列表的情况下完成追加操作。在基本情况下,添加此列表不需要任何工作。

事实证明,如果比较便宜,则归并排序往往会运行得更快一些,因为快速排序会花费更多时间摆弄指针。但是,如果比较成本很高,那么快速排序通常会运行得更快,因为它需要的比较少。

如果NODE *list是初始列表的头部,那么你可以用它排序

qs(list, NULL, &list);

这是排序代码。请注意,其中一部分是对已排序列表的优化。如果这些情况不常见,则可以删除此优化。

void qs(NODE * hd, NODE * tl, NODE ** rtn)
{
    int nlo, nhi;
    NODE *lo, *hi, *q, *p;

    /* Invariant:  Return head sorted with `tl' appended. */
    while (hd != NULL) {

        nlo = nhi = 0;
        lo = hi = NULL;
        q = hd;
        p = hd->next;

        /* Start optimization for O(n) behavior on sorted and reverse-of-sorted lists */
        while (p != NULL && LEQ(p, hd)) {
            hd->next = hi;
            hi = hd;
            ++nhi;
            hd = p;
            p = p->next;
        }

        /* If entire list was ascending, we're done. */
        if (p == NULL) {
            *rtn = hd;
            hd->next = hi;
            q->next = tl;
            return;
        }
        /* End optimization.  Can be deleted if desired. */

        /* Partition and count sizes. */
        while (p != NULL) {
            q = p->next;
            if (LEQ(p, hd)) {
                p->next = lo;
                lo = p;
                ++nlo;
            } else {
                p->next = hi;
                hi = p;
                ++nhi;
            }
            p = q;
        }

        /* Recur to establish invariant for sublists of hd, 
           choosing shortest list first to limit stack. */
        if (nlo < nhi) {
            qs(lo, hd, rtn);
            rtn = &hd->next;
            hd = hi;        /* Eliminated tail-recursive call. */
        } else {
            qs(hi, tl, &hd->next);
            tl = hd;
            hd = lo;        /* Eliminated tail-recursive call. */
        }
    }
    /* Base case of recurrence. Invariant is easy here. */
    *rtn = tl;
}

【讨论】:

  • 您可以通过交换关于枢轴的整个元素子列表而不是单个元素来提高效率,因为在链接列表中交换子列表将是 O(1)。不过,只有当您的瓶颈在于内存写入时才真正相关。
  • @Andork 谢谢。好主意。这段代码非常漂亮(尤其是没有排序列表的特殊情况),我会
【解决方案2】:

这是一个java实现。它使用头部作为支点。这可以通过在附加右子列表之前避免扫描左子列表来进一步改进,但它确实有效。这也是 O(nLogn)。

public class QuickSortLinkedList {

 public ListNode sortList(ListNode head) {

        //Base Case
        if(head == null || head.next == null)
            return head;
        //Partition Strategy
        //Chose first element as pivot and move all elements smaller than the pivot at the end of LL
        //So the order will be pivot, elements smaller than or equal to pivot, elements larger than pivot
        //Example: 9,13,10,6,9,8,11 => 9,13,10,9,11,6,8  and the method will return a pointer to 11
        ListNode partitionedElement = partition(head);

        //The elements to the right of pivot were all smaller than pivot after partioned
        //Example: LeftPartition  = 6->8->null
        ListNode leftPartition = partitionedElement.next;

        //The elements to the left of pivot were all large , so they go in right partition
        //Example: rightPartition = 9->13->10->9->11->null
        ListNode rightPartition = head;
        partitionedElement.next = null;

        //But there can be edge cases
        //Example: 3,5,3,4,5-null => after partition , list is unchanged and last element 5 is returned
        //in this case leftPartition: 3->null and rightPartition 5,3,4,5-null
        if(leftPartition == null){
            leftPartition = head;
            rightPartition = head.next;
            head.next =null;
        }

        //Now Recursively sort
        rightPartition = sortList(rightPartition);
        leftPartition = sortList(leftPartition);

        //After sorting append rightPartition to leftPartition
        ListNode iterator = leftPartition;
        while(iterator.next!=null)
            iterator = iterator.next;
        iterator.next = rightPartition;

        return leftPartition;
    }

    private ListNode partition(ListNode head){
     //Base case
     if(head.next.next == null){

         if(head.next.val>head.val)
            return head.next;
         else
         return head;
     } 

    else{    
        ListNode i = head.next;
        ListNode pivot = head;
        ListNode lastElementSwapped = (pivot.next.val>=pivot.val)?pivot.next:pivot;

        while(i!=null && i.next !=null){

            if(i.next.val >= pivot.val){
                if(i.next == lastElementSwapped.next){
                    lastElementSwapped = lastElementSwapped.next;
                }
                else{
                    ListNode temp = lastElementSwapped.next;
                    lastElementSwapped.next = i.next;
                    i.next = i.next.next;
                    lastElementSwapped = lastElementSwapped.next;
                    lastElementSwapped.next = temp;
                }
            }

            i = i.next;

        }
        return lastElementSwapped;
    }

  }
}

【讨论】:

    【解决方案3】:

    您可以使用快速排序而不会丢失 O(n*log(n)) 的预期行为。诀窍很简单——将节点放入数组中,对节点数组进行排序,以正确的顺序重新链接它们。

    【讨论】:

    • 为什么是-1?这是最好的方法,因为快速排序链表作为一个结构根本没有多大意义......
    • 假设我们想直接使用快速排序对链表进行排序,而不是先将其转换为其他东西,然后对其进行排序,然后再将其转换回来。这个问题在技术上可能存在漏洞,但这就是为什么这是一个假设,而不是事实。
    • 优雅的解决方案,不会失去任何渐近性能
    【解决方案4】:

    你可以使用这个算法:

    • 选择头节点作为主节点。
    • 左分区从枢轴节点开始,将保持其尾部
    • 右分区开始为空
    • 对于每个节点:将其添加到左侧或右侧分区列表中,将其 next 引用设置为该分区的当前头,并将其设为新头。
    • 由于枢轴元素是左分区的尾节点,请将其下一个引用设置为 null,以便真正终止该列表。
    • 对左右分区应用递归,返回其可能修改的头引用。
    • 将两个已排序的子列表绑定在一起。由于左分区的尾节点保证保留在那里并且是枢轴节点,因此将其next 引用设置为第二个已排序分区的头。
    • 返回排序列表的头部。

    遇到基数时递归停止,即当列表中的节点少于 2 个时。

    平均时间复杂度为 O(nlogn)。最差的时间复杂度是 O(n²)。例如,当列表已经排序时,它将遭受这种时间复杂度。

    这是一个用quick_sort方法在Python中实现的单链表:

    class Node:
        def __init__(self, data, nxt=None):
            self.data = data
            self.next = nxt
    
        def __iter__(self):
            curr = self
            while curr:
                node = curr
                curr = curr.next
                yield node
    
        def values(self):
            return (node.data for node in self)
    
        def partition(self):
            nodes = iter(self) # All nodes headed by this node
            next(nodes)  # Skip self
            left = self   # Left partition always has pivot node as its tail
            pivotvalue = self.data
            right = None
            for curr in nodes:  # Remaining nodes
                # Prefix the current node to the relevant partition
                if curr.data < pivotvalue:
                    curr.next = left
                    left = curr
                else:
                    curr.next = right
                    right = curr
            self.next = None  # Terminate the left partition
            return left, right 
            
        def quick_sort(self):
            if not self.next:  # Base case: one node only
                return self
            left, right = self.partition()
            # Left partition has at least one node (the pivot node, which remains its tail)
            left = left.quick_sort()
            # Right partition could be empty 
            if right:
                right = right.quick_sort()
            self.next = right  # Chain the two sorted partitions
            return left
    
        def is_sorted(self):
            values = self.values()
            prev = next(values)
            for data in values:
                if data < prev:
                    return False
                prev = data
            return True
    
    
    class LinkedList:
        def __init__(self, *values):
            self.head = None
            self.prefix(*values)
        
        def prefix(self, *values):
            for data in reversed(values):
                self.head = Node(data, self.head)
                
        def values(self):
            if self.head:
                return self.head.values()
    
        def quick_sort(self):
            self.head = self.head and self.head.quick_sort()
    
        def is_sorted(self):
            return self.head is not None and self.head.is_sorted()
    

    这里有一些代码可以用 20 个值的随机列表反复测试这个实现:

    from random import shuffle
    
    values = list(range(20))
    for _ in range(100):
        shuffle(values)
        linkedlist = LinkedList(*values)
        print("Shuffled:", *linkedlist.values())
        linkedlist.quick_sort()
        print("Sorted:", *linkedlist.values())
        if not linkedlist.is_sorted():  # Test for failure
            raise ValueError("not sorted!")
    

    【讨论】: