快速排序无法按预期使用链表

Quick sort not working as intended with linked list

前言:

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

问题:

我有一个由Node*组成的链表。我专门为它写了一个快速排序算法。它的工作原理是采用给定的链表,并根据选定的 pivot 递归地将其拆分为子列表。枢轴始终是列表的 head(前面),子列表 leftright 分别是包含小于和大于枢轴 than/equal 的值的列表.我几乎把它弄下来了,但我不知道如何正确 add_link 枢轴。在之前的迭代中,我总是 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

您假设您可以同时获得更小和更大的列表,并且您已经确保在调用 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 连接列表和 return 哨兵,但这更像是重写。

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

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

我看到的主要问题是您有时,但不总是,将枢轴添加到 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。左排序,右排序,然后连接左、分区和右。除了更快,因为在下一个递归调用中要排序的节点更少,将分区节点保留在列表之外将使 gua运行tee 您的列表变得更短每次递归调用。

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

强制一致性!

一旦您指定了功能,就可以更轻松地验证您的代码是否尊重您所做的任何设计决策。您的 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

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

    // 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 为空。)

最后一点:如果到目前为止您已经完成了所有这些操作,那么您可能 运行 在某个时候遇到了分段错误。您缺少检查 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 语句变得那么严重 - 也不太明显。)