【问题标题】:Sentinel node in doubly linked list双向链表中的哨兵节点
【发布时间】:2020-05-08 16:13:11
【问题描述】:

我正在努力实现我自己的双向链表。我决定在列表末尾使用哨兵节点,它将存储指向列表中最后一个节点的指针,最后一个节点将存储指向哨兵节点的指针。这是一个好方法还是应该以不同的方式使用哨兵节点?我的代码:

template <class T>
class MyList
{
public:
    MyList() : m_first(nullptr), m_last(nullptr), m_sentinel(new Node()) {}

    ~MyList()
    {
        delete m_sentinel;
    }

    void push_back(const T & data);
    void pop_back();
    void push_front(const T & data);
    void pop_front();

    class iterator;

    iterator begin()
    {
        if(m_first == nullptr) return iterator(m_sentinel);
        return iterator(m_first);

    }
    iterator end()
    {
        return iterator(m_sentinel);
    }
private:
    struct Node;

    Node * m_first;
    Node * m_last;
    Node * m_sentinel;
};

template <class T>
struct MyList<T>::Node
{
    Node() : m_data(nullptr), m_next(nullptr), m_prev(nullptr) {}
    Node(T * data, Node * next, Node * prev) : m_data(data), m_next(next), m_prev(prev) {}
    ~Node() { delete m_data; delete m_prev; }

    T * m_data;
    Node * m_next;
    Node * m_prev;
};

【问题讨论】:

  • 这不是我对为什么要将哨兵节点用于链表的理解。我的理解是你会使用一个循环链表。如果它不是循环的,则哨兵节点只是增加了不必要的复杂性。特别是因为您已经有了指向第一个和最后一个节点的指针。
  • 我可以看到最后一个节点的使用情况,以便利用快速追加到列表的后面。但是对于一个额外的哨兵节点,我没有看到它的实用程序。

标签: c++


【解决方案1】:

这是使用哨兵节点的一种方式,但问问自己:last 节点在 双重-链表中的特殊之处是什么?双向链表具有对称性;向前和向后看起来相同,模一些命名差异。在最后一个节点之后放置一个哨兵节点会破坏这种对称性,除非你也在第一个节点之前放置一个哨兵节点

所以让我们在第一个节点之前放置另一个哨兵节点。但是等等——为什么我们需要两个哨兵节点?最后一个后的哨兵对其“前一个”指针有要求,而第一个前的哨兵对其“下一个”指针有要求。这些要求并不矛盾;单个哨兵节点可以满足它们。如果使用单个哨兵节点,则会得到一个循环双向链表,哨兵节点标记开始和结束。

是时候将事情提升到另一个层次了。指向第一个和最后一个节点的指针的目的是什么?更重要的是,m_firstm_sentinel-&gt;m_next 有什么好处?好吧,后者确实有一个额外的间接级别。如果我们处理这个问题,MyList 将只需要跟踪哨兵节点。嗯……

template <class T>
class MyList
{
public:
    MyList() : m_sentinel() { m_sentinel.m_next = m_sentinel.m_prev = &m_sentinel; }

    ~MyList() { /* Empty for now, but see the text after the code. */ }

    /* Public API omitted */

private:
    struct Node {
        /* Definition omitted in this illustration. */
        /* Needing the definition here instead of later in the file is one drawback. */
    };

    Node * first() { return m_sentinel.m_next; }
    Node * last()  { return m_sentinel.m_prev; }

    Node m_sentinel;  // <-- not a pointer!
};

此版本的MyList 与您的大小相同(它由三个指针组成),但更好地利用了哨兵节点来简化向列表添加节点和从列表中删除节点。我会注意到有助于这项工作的一个细节是您的节点存储指向数据的指针,而不是直接将数据存储在节点内。因此,像往常一样,选择使用哪种实现归结为分析利弊。

如果你真的走这条路,你会在管理内存的方式上遇到一个奇怪的问题。您已让每个节点负责删除其他节点。这对于长列表可能会产生问题,因为您可能会耗尽堆栈空间(~Node() 在退出之前为前一个节点调用 ~Node(),因此您的调用堆栈随着节点数线性增长)。这也是非惯用的,因为节点不负责创建其他节点。根据经验,创建节点的对象应该负责确保它们被销毁。也就是说,没有delete 没有对应的new 在同一级别。将销毁节点的责任转移到~MyList(),在那里可以迭代地而不是递归地销毁节点。

【讨论】:

    猜你喜欢
    • 2021-10-11
    • 2020-01-22
    • 1970-01-01
    • 1970-01-01
    • 2017-11-22
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2014-07-27
    相关资源
    最近更新 更多