【问题标题】:Quick sort not working as intended with linked list快速排序无法按预期使用链表
【发布时间】:2021-09-15 22:09:00
【问题描述】:

前言:

我不关心我的特定快速排序算法的可行性。这不是问题所在。我很好奇为什么我已经编写的程序的行为方式如此,而不是为什么我应该使用 STL 或其他什么。这是出于学习目的。 (例如,我知道选择枢轴,因为头部不是最好的情况——但这不是我要问的)

问题:

我有一个由Node*s 组成的链表。我专门为它写了一个快速排序算法。它的工作原理是获取给定的链表,并根据选择的pivot 递归地将其拆分为子列表。枢轴始终是列表的head(前面),而子列表leftright 是分别包含小于和大于/等于枢轴的值的列表。我几乎把它弄下来了,但我不知道如何正确地 add_link 枢轴。在以前的迭代中,我总是将枢轴添加_链接到正确的子列表,但这会导致无限循环,因此在比较值时我最终完全跳过了枢轴。结果列表已排序,但缺少用于枢轴的每个值。我应该如何解决这个问题?

这是一个最小的可重现示例:

#include <iostream>
struct Link {
    Link(int d=int(), Link* n=nullptr) {
        data = d;
        next = n;
    }
    int         data;
    Link* next;
};

struct LinkList {
  Link* sentinel;
  size_t      size;

 LinkList(Link* h=new Link, size_t s=0) {
    size = s;
    sentinel = h;
  }

  ~LinkList() {
    Link* current = sentinel;
    while (current != 0) {
      Link* next = current->next;
      delete current;
      current = next;
    }
    sentinel = 0;
  }
};

// Prototypes

Link* sort_q(Link* sentinel);
void  divide(Link* sentinel, Link* cmp, Link*& lower, Link*& greater);
Link* concatenate(Link* lower, Link* greater);

// helpful function calls quick sort
void sort(LinkList& l) {
    l.sentinel = sort_q(l.sentinel);
}


// returns false if there sentinel = null; true if add_link was successful
bool add_link(Link* sentinel, Link* add_link, Link*& end) {
    if (sentinel == nullptr) {
        return false;
    }
    Link* curr = sentinel;
    for (; curr->next; curr = curr->next) {}
    curr->next = add_link;
    end = curr->next;
    return true;
}

Link* sort_q(Link* sentinel) {

    Link* lower = nullptr;
    Link* greater = nullptr;
    Link* cmp = sentinel;

  // base case LinkList = null or sentinel->null
    if (sentinel == nullptr || sentinel->next == nullptr) {
        return sentinel;
    }

    divide(sentinel, cmp, lower, greater);

    lower = sort_q(lower);

    greater = sort_q(greater);

    return concatenate(lower, greater);
}

void divide(Link* sentinel, Link* cmp, Link*& lower, Link*& greater) {
    lower = new Link, greater = new Link;
  // lend is pointer to end of lower subLinkList
  // rend is pointer to end of greater subLinkList
    Link* lend = nullptr, * rend = nullptr;

  // loop through LinkList until end
    while (sentinel != nullptr) {
        if (sentinel == cmp) {
            sentinel = sentinel->next; continue;
        }
        if (sentinel->data < cmp->data) {
            // break current link
            Link* tmp = sentinel;
            sentinel = sentinel->next;
            tmp->next = nullptr;
      // if subLinkList is not empty, add_link current Link to subLinkList and update end pointer
            if (add_link(lend, tmp, lend))
                continue;
        // otherwise, "add_link" current Link to empty subLinkList and update end pointer manually
            lower->next = tmp;
            lend = lower->next;
        }
        else {
            // break current link
            Link* tmp = sentinel;
            sentinel = sentinel->next;
            tmp->next = nullptr;
            // if subLinkList is not empty, add_link current Link to subLinkList and update end pointer
            if (add_link(rend, tmp, rend))
                continue;
            // otherwise, "add_link" current Link to empty subLinkList and update end pointer manually
            greater->next = tmp;
            rend = greater->next;
        }
    }
    // remove dummy Link(s)
    if (lower->next)
        lower = lower->next;
    else
        lower = cmp;
    if (greater->next)
        greater = greater->next;
    else
        greater = cmp;
    // unlink cmp
    cmp->next = nullptr;
}

// connected subLinkLists
Link* concatenate(Link* lower, Link* greater) {
    Link* sentinel;

    sentinel = lower;

    while (lower->next != nullptr) {
        lower = lower->next;
    }

    lower->next = greater;

    return sentinel;
}

void print(LinkList &l) {
  for (Link* n = l.sentinel; n != NULL; n = n->next) {
    std::cout << n->data << '\n';
  }
}

int main() {
  // set up linked LinkList 8->4->5->11->7->5->3->9->null
  Link* sentinel = new Link(8 , new Link(4, new Link(5, new Link(11, new Link(7, new Link(5, new Link(3, new Link(9))))))));
  LinkList l(sentinel,5);

  sort(l);
  print(l);

  return 0;
}

我想要的输出是

3
4 // pivot missing from output
5
5
7
8 // pivot missing from output
9
11

但它输出

3
5
5
7
9
11

编辑#1:

我尝试在连接之间添加枢轴,但这也不起作用。它产生不同的结果,但并不完全相同。像这样修改sort_q()partition()

Link* qsort(Link* sentinel) {
   // ... other stuff before modification
   return concatenate(lower, cmp); // changed greater argument to pass cmp which is now cmp->next = greater once finishing partition()
}

void partition(Link* sentinel, Link*& cmp, Link*& lower, Link*& greater) {
    // .. other stuff before modifications

    // remove dummy Link
    if (lower->next)
        lower = lower->next;

    if (greater->next)
        greater = greater->next;

    cmp->next = greater; // cmp points to greater (greater) sublist
}

输出变成

3
4
5
7
0
8
11
0

【问题讨论】:

  • 我们是否可以假设您已经使用调试器逐行浏览了代码,并观察了发生的情况?
  • 您的枢轴节点需要位于中间。当您调用 concatenate 时,您会跳过枢轴节点
  • 有多种解决方法。我想你可以将pivot-&gt;next 设置为right 然后concatenate(left, pivot)
  • 你不想读这个,但我还是放弃了它:我看不出连接阶段的意义。你不应该需要它。
  • 顺便说一句,问题的布局很好。回答它所需的一切都在这里,除了提供编译器之外,我们几乎不需要干预来运行它并观看乐趣。

标签: c++ linked-list quicksort


【解决方案1】:

您假设可以同时获得一个较小的列表和一个较大的列表,并且在调用 divide 之前,您已经确保列表中至少有 2 个项目,所以这很好。

您只需要在除法结束时处理所有情况。确保 cmp 出现在两个列表之一中。几乎和以前一样,但您需要处理较低和较大列表都非空的情况。

// remove dummy node(s)
    cmp->next = nullPtr;
    if (lower->next && greater->next) {
        // we have two lists. put cmp on greater
        lower = lower->next;
        cmp->next = greater->next;
        greater = cmp;
    }
    else if (lower->next) {
        // only a lower list, use cmp on greater
        lower = lower->next;
        greater = cmp;
    }
    else if (greater->next) {
        // only a greater list, use cmp as lower.
        greater = greater->next;
        lower = cmp;
    }
    

看到上面,处理所有3种情况,可以简化为:

// remove dummy node(s)
    if (lower->next) {
        // we have lower node, so put cmp on greater
        lower = lower->next;
        cmp->next = greater->next;
        greater = cmp;
    }
    else if (greater->next) {
        // only a greater list, use cmp as lower.
        greater = greater->next;
        lower = cmp;
        cmp->next = nullPtr;
    }
    

然后使用concatenate(lower,greater)。虽然它可以优化为让divide 连接列表并只返回哨兵,但这有点重写。

编辑:把它放在一起消除你的内存泄漏,应该是这样的(注意,我没有编译或测试过)

void divide(Node* sentinel, Node* cmp, Node*& lower, Node*& greater) {
    lower = nullptr, greater = nullptr;
  // lend is pointer to end of lower sublist
  // rend is pointer to end of greater sublist
    Node* lend = nullptr, * rend = nullptr;

  // loop through list until end
    while (sentinel != nullptr) {
        if (sentinel == cmp) {
            sentinel = sentinel->next; continue;
        }
        if (sentinel->data < cmp->data) {
            // break current link
            Node* tmp = sentinel;
            sentinel = sentinel->next;
            tmp->next = nullptr;
      // if sublist is not empty, append current node to sublist and update end pointer
            if (append(lend, tmp, lend))
                continue;
        // otherwise, "append" current node to empty sublist and update end pointer manually
            lend = lower = tmp;
        }
        else {
            // break current link
            Node* tmp = sentinel;
            sentinel = sentinel->next;
            tmp->next = nullptr;
            // if sublist is not empty, append current node to sublist and update end pointer
            if (append(rend, tmp, rend))
                continue;
            // otherwise, "append" current node to empty sublist and update end pointer manually
            rend = greater = tmp;
        }
    }
    // insert cmp node
    if (lower) {
        // we have lower node, so put cmp on greater
        cmp->next = greater;
        greater = cmp;
    }
    else if (greater) {
        // only a greater list, use cmp as lower.
        lower = cmp;
        cmp->next = nullptr;
    }
}

【讨论】:

  • 啊,非常感谢!我真的很感激你花时间来解决它。这让事情变得更加清晰。
  • 还有最后一个问题,你知道我为什么会出现内存泄漏吗?根据 valgrind 的说法,这是由于分配了新的 leftright 地址,但我不确定如何弥补这一点。
  • 当然,是的。您应该在最后删除它们,或者可能重写以避免分配它们
  • 我想我不确定如何在涉及递归的情况下释放它们。我什么时候可以释放它们?
  • 你应该尝试重写而不分配它们。将它们初始化为null 和刚刚设置的left = tmp 而不是left-&gt;next = tmp,并适当更改if 语句
【解决方案2】:

我看到的主要问题是您有时(但并非总是)将枢轴添加到partition() 中的“左”或“右”列表中。这种不一致可能违反了partition() 的预期功能。但是,此功能没有记录。所以我会修改我的评估,说主要问题是你没有记录你的功能。您的项目越大,由于缺乏文档而遇到问题的可能性就越大。

文档!

我希望看到 partition() 记录说它将列表分为三部分:左、右和枢轴。

/// Partitions the list starting at `head` into three pieces. Data less than
/// the given partition's data is put in the `left` list, while the remaining
/// data (except `*partition` itself) is put in the `right` list. The third
/// piece consists of `*partition`, which is assumed to be in the given list.
void partition(Node* head, Node* pivot, Node*& left, Node*& right) {

另一种选择可能是立即将枢轴放在这些列表之一中。但是,这些列表将在调用partition() 后立即进行排序,并且不需要在该排序中包含*partition。左排序,右排序,然后连接左、分区和右。除了因为在下一次递归调用中要排序的节点更少而更快之外,将分区节点排除在您的列表之外将保证您的列表随着每次递归调用而变短。

不过,这是您的决定,因为您是设计师。重要的是决定你想要什么功能,记录下来,然后遵循这个决定。

强制一致性!

一旦您指定了功能,就更容易验证您的代码是否尊重您所做的任何设计决策。您的 sort_q() 函数无法解释上述规范,因为它没有将分区添加到列表中。可能有比下面更简单的方法来做到这一点,但是下面的代码几乎不需要重写代码就可以达到目的。 (sort_q()到此结束。)

    // ** Return left -> pivot -> right, not just left -> right **
    return concatenate(left, concatenate(pivot, right));
}

此外,您的partition() 函数通过(有时)将分区节点添加到您的列表之一而违反了此规范。别那样做。请将您的链接设置为 null。

    // remove dummy node(s)
    if (left->next)
        left = left->next;
    else
        left = nullptr;   // <-- null, not pivot
    if (right->next)
        right = right->next;
    else
        right = nullptr;   // <-- null, not pivot

嗯...我之前避免重写您的代码,但这种特殊的结构让我觉得不必要的复杂。如果某个指针不为空,请将其分配给某物。否则(当该指针为空时),分配空而不是指针(它为空,所以同样的事情)。那是比较罗嗦的。只需分配指针。

    // remove dummy node(s)
    left = left->next;
    right = right->next;
    // *** Memory leak, as we removed the nodes without deleting them ***

哦,内存泄漏是一种痛苦。特别是因为您实际上并不需要虚拟节点。但这会涉及到一个单独的问题,所以现在我只想指出,如果您已将 leftright 初始化为 nullptr,您的代码将非常接近工作。 (您的提示是,如果 left 初始化为 nullptr,那么在调用 append() 时,当且仅当 left 为空时,lend 为空。)

最后一点:如果您到目前为止都遵循了这一切,那么您可能在某个时候遇到了分段错误。您错过了concatenate() 的检查(可能是您开始使用虚拟节点的原因?)

// connected sublists
Node* concatenate(Node* left, Node* right) {
    if ( left == nullptr )   // <-- Add this check
        return right;        // <-- It is very easy to concatenate to nothing.
    Node* head = left;

这应该解决直接的问题。不过,还有其他地方需要改进,所以请继续努力。 (你的代码的其他部分我会称之为“不必要的复杂”,尽管不如 if 语句变得那么严重 - 也不那么明显。)

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2023-03-06
    • 2012-03-14
    • 2021-07-14
    • 1970-01-01
    • 2015-02-10
    • 2018-11-06
    相关资源
    最近更新 更多