【问题标题】:Stable Quicksort With Linked List带链表的稳定快速排序
【发布时间】:2014-04-25 05:46:58
【问题描述】:

我正在尝试使用链表进行快速排序的稳定实现,但由于某种原因,这段代码崩溃了。它似乎在它崩溃之前到达了分区中while循环的第二次迭代。

基本上,我使用头部作为枢轴点,任何小于枢轴的东西都会进入新的链表,大于枢轴的任何东西也会进入新的链表。这被递归调用,然后在最后将它们组合起来。

    class Node
    {
    public:
        Node* next;
        int key;
        int key2;

        Node()
        {
            next = NULL;
        }
    };

    void fillRandom(Node*& root, int length)
    {
       if(length != 0){
            root->key = rand()%10;
            root->next = new Node;
            fillRandom(root->next, length-1);
        }
    }

    void partitionLL(Node*& lessThan, Node*& greaterThan, Node*& root)
    {
        Node* lessThanTemp = lessThan; Node* greaterThanTemp = greaterThan;
        Node* temp = root;

        while(temp->next != NULL)
        {
            if(temp->key > root->key){
                greaterThanTemp = temp;
                greaterThanTemp->next = new Node;
                greaterThanTemp = greaterThanTemp->next;
            }else{
                lessThanTemp = temp;
                lessThanTemp->next = new Node;
                lessThanTemp = lessThanTemp->next;
            }
            temp = temp->next;
        }
    }

    void quickSort(Node* root)
    {
        if(root->next != NULL){//i.e. theres only two nodes in the list
            Node* lessThan = new Node; Node* greaterThan = new Node;
            partitionLL(lessThan, greaterThan, root);
            quickSort(lessThan);
            quickSort(greaterThan);
        }else{
            return;
        }
    }

    int main()
    {
        int length = 15;
        Node* root = new Node;

        fillRandom(root, length);
        quickSort(root);
    }

【问题讨论】:

  • 这是学习如何使用调试器的最佳时机。如果您构建程序的调试版本,并在调试器中运行它,那么调试器将在崩溃的地方停止。然后调试器将让您检查并遍历函数调用堆栈(例如,如果它在库代码中停止),还允许您检查变量的值。
  • 调试器也可用于逐行执行代码,在这种情况下可能会更好,因为它可以让您准确地看到代码在做什么以及它为变量分配了什么值.尤其是使用您的 partionLL 函数,因为它(对我来说)似乎很可疑。
  • 最后一点:你实际上并没有排序任何东西,因为在你第一次从@调用quickSortroot->next指针将是NULL 987654327@函数。
  • 最后一件事让我相信你展示给我们的程序并不是真正崩溃的程序,因为这个程序不会做任何可能崩溃的事情:它调用quickSort,它立即返回,程序退出。真的是崩溃程序的Minimal, Complete, and Verifiable example吗?
  • 为了让事情看起来更好,我取出了我为链表赋值的部分。我将对其进行编辑。我相信该错误与 temp->next 在第一次运行后为空有关。

标签: c++ linked-list quicksort


【解决方案1】:

你的想法成立(建立两个链表)。它缺少的分离和重新连接代码。在处理链表时,首先要记住的是,在进行重新排列时曾经需要分配新节点很少。您使用给定的节点。这就是他们的目的。您的算法应该做的只是重新排列其中的指针。

以下正是这样做的。一旦分配了列表,就不需要新的节点。除此之外的算法与您的想法相同:

  • 使用头节点作为枢轴值。将其从列表中分离
  • 沿途将列表附加节点的其余部分枚举到两个列表之一中。注意从一个列表移动到另一个列表的代码将节点添加到它们各自列表的ends
  • 完成后,您将拥有两个列表和一个单独的枢轴节点。终止两个列表并递归。
  • 一旦递归返回寻找左侧列表的末尾。那是添加枢轴节点的位置,然后将右侧列表连接到该位置。

代码如下。我强烈建议在调试器中遍历它:

#include <iostream>
#include <iterator>
#include <algorithm>
#include <random>

struct Node
{
    Node* next;
    int key;
    int key2;

    Node( int key, int key2 )
        : key(key), key2(key2), next()
    {}
};

Node * createList(size_t N)
{
    std::random_device rd;
    std::mt19937 rng(rd());
    std::uniform_int_distribution<> dist(1,10);

    Node *root = nullptr;
    Node **pp = &root;
    for (int i=0; i<N; ++i)
    {
        *pp = new Node(dist(rng), i+1);
        pp = &(*pp)->next;
    }
    return root;
}

void freeList(Node *& root)
{
    while (root)
    {
        Node *tmp = root;
        root = tmp->next;
        delete tmp;
    }
}

void printList(const Node* root)
{
    for (;root;root = root->next)
        std::cout << root->key << '(' << root->key2 << ") ";
    std::cout << '\n';
}

// quicksort a linked list.
void quickSort(Node *&root)
{
    // trivial lists are just returned immediately
    if  (!root || !(root->next))
        return;

    Node *lhs = nullptr, **pplhs = &lhs;
    Node *rhs = nullptr, **pprhs = &rhs;
    Node *pvt = root;
    root = root->next;
    pvt->next = nullptr;

    while (root)
    {
        if (root->key < pvt->key)
        {
            *pplhs = root; // tack on lhs list end
            pplhs = &(*pplhs)->next;
        }
        else
        {
            *pprhs = root; // tack on rhs list end
            pprhs = &(*pprhs)->next;
        }
        root = root->next;
    }

    // terminate both list. note that the pivot is still
    //  unlinked, and will remain so until we merge
    *pplhs = *pprhs = nullptr;

    // invoke on sublists.
    quickSort(lhs);
    quickSort(rhs);

    // find end of lhs list, slip the pivot into  position, then 
    //  tack on the rhs list.
    while (*pplhs)
        pplhs = &(*pplhs)->next;
    *pplhs = pvt;
    pvt->next = rhs;

    // set final output
    root = lhs;
}

int main()
{
    Node *root = createList(20);
    printList(root);
    quickSort(root);
    printList(root);
    freeList(root);
    return 0;
}

输出(可变)

6(1) 7(2) 1(3) 10(4) 8(5) 10(6) 4(7) 7(8) 2(9) 9(10) 1(11) 8(12) 10(13) 8(14) 6(15) 9(16) 8(17) 2(18) 8(19) 9(20) 
1(3) 1(11) 2(9) 2(18) 4(7) 6(1) 6(15) 7(2) 7(8) 8(5) 8(12) 8(14) 8(17) 8(19) 9(10) 9(16) 9(20) 10(4) 10(6) 10(13) 

括号中的数字是原始列表中节点的原始顺序。注意,我特地选择了一个小的随机池来选择,这样可以体验到很多等键,从而证明排序是稳定的; like-keys 保留其原始列表顺序。例如,原始列表中有五个8 值。排序后他们的顺序是:

8(5) 8(12) 8(14) 8(17) 8(19)

这是有意的,并且通过确保当我们将项目移动到拆分列表时我们总是将它们添加到末端来实现。

无论如何,我希望它有所帮助。

【讨论】:

  • 哦,我明白了。这有很大帮助。我现在意识到我忘记合并它们了。但是,我特别询问了为什么我的崩溃,但事实证明我什至不需要该问题的代码只是逻辑。我对你在这里做的一些事情有点困惑。为什么ppplhs 是双指针?另外,为什么pplhs = &amp;lhs。最后,如果 Node 不为 NULL,while(root) 是否返回 true?无论如何,这确实有帮助。我也学到了一些东西。谢谢!
  • 我使用一个指向指针的指针来保存将接收下一个节点指针的指针的地址。它使诸如前向列表构建之类的事情变得更加容易。指向指针的指针类似于常规的单向指针,但它们持有的是一个额外的间接级别(它们持有一个指针的地址)。因此pplhs = &amp;lhs 表示“将lhs 指针的地址放入pplhs。关于C 中的while(root) 布尔评估为零或非零。空指针与零同义,因此while(root) 表示“而root 持有一个非空值”。
  • @amarucci13 如果您发现答案有帮助,您可以使用答案发布左上角的箭头将其勾选。如果您在 the 正确答案中找到它,您也可以选中该复选框。现在,在将其选为 最佳答案之前,我会先看看是否有更多可能更合适的答案。
  • 是的,我试过了,但没有足够的声誉哦,在这里,你需要 15 来投票。
  • @amarucci13 啊,真可惜。好吧,希望它有所帮助。花一些时间学习调试,然后在调试器中逐步调试代码(CodeBlocks 或 Netbeans 中的 gdb 集成,或用于 Windows、VS 等)。听起来很奇怪,调试 工作 代码是 非常有助于理解它的工作原理。无论如何,很高兴它有所帮助。