[英]Sentinel node in doubly linked list
我正在努力實現我自己的雙向鏈表。 我決定在列表末尾使用哨兵節點,它將存儲指向列表中最后一個節點的指針,最后一個節點將存儲指向哨兵節點的指針。 這是一個好方法還是應該以不同的方式使用哨兵節點? 我的代碼:
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;
};
這是使用哨兵節點的一種方式,但要問自己:是什么讓最后一個節點在雙向鏈表中特別? 雙向鏈表具有對稱性; 向前和向后看起來相同,模一些命名差異。 在最后一個節點之后放置一個哨兵節點會破壞這種對稱性,除非您還在第一個節點之前放置一個哨兵節點。
所以讓我們在第一個節點之前放置另一個哨兵節點。 但是等等——為什么我們需要兩個哨兵節點? 最后一個后的哨兵對其“前一個”指針有要求,而第一個前的哨兵對其“下一個”指針有要求。 這些要求並不矛盾; 單個哨兵節點可以滿足它們。 如果使用單個哨兵節點,則會得到一個循環雙向鏈表,哨兵節點標記開始和結束。
是時候將事情提升到另一個層次了。 指向第一個和最后一個節點的指針的目的是什么? 更重要的是, m_first 比m_sentinel->m_next
m_first
什么好處? 好吧,后者確實有一個額外的間接級別。 如果我們處理這個問題, 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
與您的大小相同(它由三個指針組成),但更好地利用了哨兵節點來簡化向列表添加節點和從列表中刪除節點。 我會注意到有助於這項工作的一個細節是您的節點存儲指向數據的指針,而不是直接將數據存儲在節點內。 因此,像往常一樣,選擇使用哪種實現歸結為分析利弊。
如果您執行 go 這條路線,您將在管理 memory 時遇到奇怪的問題。 您已讓每個節點負責刪除其他節點。 這對於長列表可能是有問題的,因為您可能會耗盡堆棧空間(在退出之前~Node()
~Node()
,因此您的調用堆棧隨着節點數線性增長)。 這也是非慣用的,因為節點不負責創建其他節點。 根據經驗,創建節點的 object 應該負責確保它們被銷毀。 也就是說,在同一級別沒有相應的new
的delete
。 將銷毀節點的責任轉移到~MyList()
節點可以迭代地處理,而不是遞歸地處理。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.