連結串列
連結串列被用於許多庫/應用程式,這是有充分理由的。以下是它相對於其他容器的優點,間接來自 cplusplus.com 上的參考頁面
- 在容器中的任何位置高效地插入和刪除元素(恆定時間)。
- 以正向順序迭代元素(線性時間)。
- 在容器內甚至在不同容器之間高效地移動元素和元素塊(恆定時間)。
以下內容僅適用於雙向連結串列,我稍後會解釋
參考資料中沒有解釋的是它是如何實現的。你*為什麼*會想知道這些?對我來說,僅僅是好奇心。對其他人來說,也許他們可能會建立自己的連結串列容器型別。無論如何,這是*有人*最終會用到的知識,希望如此。
設計
連結串列通常被描述為以某種方式連線在一起的線性節點列表。在 C/++ 中,你通常會有一個結構,其中包含資料和指向下一個結構容器的指標,該容器包含資料和指向下一個結構容器的指標……依此類推。連結串列的主要優點是它不以連續的方式包含資料,而是一種靈活的方式。這允許快速插入和更好的整體迭代。連結串列通常甚至用作其他容器(例如佇列或堆疊容器)的基礎。
連結串列有幾種變體。實際上,“連結串列”一詞並不真正指實現(因此,是一個抽象術語),而只是指容器的資料的儲存方式(透過引用連結)。連結串列最常見的實現可能是雙向連結串列。雙向連結串列是一個包含節點的列表,這些節點包含對列表中前一個*和*下一個連結的引用。這允許快速節點刪除、更快地獲取尾部節點以及更靈活的迭代。
以雙向連結串列的靈活性為代價,它通常需要使用更多的記憶體。在大型列表中,考慮一個節點的大小是正常大小的兩倍。這會嚴重影響應用程式。如果您沒有理由向後迭代,則可以認為雙向連結串列效率低下,僅僅是因為設計。 std::list 是一個雙向連結串列。因此,如果我需要單向連結串列,我會自己實現一個。單向連結串列只能向前迭代,僅僅因為它所持有的節點僅包含對列表中下一個節點的引用,而不包含對前一個節點的引用,但其優點是少一個引用。
另一種不太常用的連結串列型別是迴圈連結串列。所有這些都是一個單向/雙向連結串列,其中尾部節點向前迭代到起始節點(也可能反之亦然)。這沒有太多用途,因為它通常在迭代列表時存在問題,但例如,一個列表會迭代節點直到收到給定的訊號。此外,它實際上是一種與連結串列一起使用的技術……並不總是在實現中使用,儘管特殊的列表實現可以比其他實現更好地處理迴圈連結串列:TODO:新增示例...
在嵌入式系統中,使用連結串列可能很昂貴。每個引用的儲存可能非常繁重,以至於不希望將連結串列與僅儲存一個數據位置的節點一起使用。相反,他們通常使用所謂的展開連結串列。在展開連結串列中,節點像往常一樣儲存對下一個和前一個的引用,但每個節點都儲存資料陣列。當您迭代每個節點時,您線性地迭代陣列中的資料,然後移動到下一個節點。這樣,我們可以在一箇中擁有三到四個資料節點,同時減少節點總數(從而節省記憶體)。但是,這通常使用連續的記憶體來儲存節點,因此很難移動陣列中的節點。TODO:給出一個例子。
維基百科有很棒的圖片:http://en.wikipedia.org/wiki/Linked_list#Linear_and_circular_lists
實現
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
|
template <typename T>
struct SNode //Singly-linked list node
{
SNode () : _next(0) {}
SNode (T data) : _data(data), _next(0) {}
SNode (T data, Node<T>* next) : _data(data), _next(next){}
SNode (Node<T>* next) : _next(next) {}
T data;
Node<T>* next; //Only the next reference.
}
template <typename T>
struct Node : public SNode<T>
{
Node<T>* prev; //This is the only difference in structure!
}
|
這可能是最基本的列表結構形式。但是,這並沒有顯示列表在任何方面都是有用的。我們可以透過增加實現的複雜性來大大簡化介面。 std::list 使用父類,同時在內部使用抽象方法(迭代器)控制節點,以訪問節點。我認為 std::list 提供的介面很好,所以我將建立一個類似於它的東西
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166
|
#include <iostream>
template <typename T>
class List;
template <class TNode>
class Iterator
{
/* Helper class to provide pointer like facilities around a node */
friend class List<typename TNode::value_type>;
TNode* pNode; //The node oriented with this instance of iterator.
Iterator(TNode* _pNode) : pNode(_pNode) {}
public:
void operator++(){ pNode = pNode->_next; }
void operator++(int){ pNode = pNode->_next; }
bool operator!=(Iterator<TNode> rval){ return !(pNode == rval.pNode); }
bool operator==(Iterator<TNode> rval){ return (pNode == rval.pNode); }
typename TNode::value_type operator*(){ return pNode->_data; }
Iterator<TNode> operator+(int _i)
{
Iterator<TNode> iter = *this;
for (int i = 0; i < _i; ++i)
{
if (iter.pNode) //If there's something to move onto...
++iter;
else
break;
}
return iter; //Return regardless of whether its valid...
}
};
template <typename T>
class Node
{
friend class List<T>;
friend class Iterator<Node<T> >;
Node () : _next(0) {}
Node (T data) : _data(data), _next(0) {}
Node (T data, Node<T>* next) : _data(data), _next(next){}
Node (Node<T>* next) : _next(next) {}
T _data;
Node<T>* _next;
public:
typedef T value_type;
};
template <typename T>
class List
{
Node<T>* first;
public:
typedef Iterator<Node<T> > iterator;
typedef T value_type;
List () : first(0) {}
~List()
{
if (first)
{
Node<T> *iter = first;
while (iter != 0)
{
Node<T>* tmp = iter;
iter = iter->_next;
delete tmp;
}
}
}
void push_back(T data)
{
if (first)
{
Node<T> *iter = first;
for (; iter->_next != 0; iter = iter->_next); //Iterate until we reach the end of our linked list.
iter->_next = new Node<T>(data);
}
else
first = new Node<T>(data);
};
void push_front(T data)
{
if (first)
{
Node<T> * tmp = new Node<T>(data);
tmp->_next = first;
first = tmp;
}
else
first = new Node<T>(data);
}
iterator begin(){ return iterator(first); }
iterator end(){ return iterator(0); }
bool erase(iterator& _iNode) //True for success, vice versa
{
/* This is rather inneffecient. Maybe a better way to do this? */
/* Even then, it's *still* more effecient than a contiguous container */
if (_iNode.pNode == first)
{
first = first->_next;
delete _iNode.pNode;
return true;
}
else
{
for (Node<T>* iter = first; iter->_next; iter = iter->_next)
{
if (iter->_next == _iNode.pNode) //Find our node.
{
iter->_next = _iNode.pNode->_next;
delete _iNode.pNode;
return true;
}
}
}
return false;
}
};
int main(void)
{
List<int> list;
list.push_back(3);
list.push_back(4);
list.push_front(2);
list.push_front(1);
/*Print all elements*/
for (List<int>::iterator iter = list.begin();
iter != list.end(); ++iter)
{
std::cout << (*iter) << std::endl;
}
/*Delete second element and reprint*/
List<int>::iterator tmp = list.begin() + 1;
list.erase(tmp);
for (List<int>::iterator iter = list.begin();
iter != list.end(); ++iter)
{
std::cout << (*iter) << std::endl;
}
/*Now delete first node and print again*/
tmp = list.begin();
list.erase(tmp);
for (List<int>::iterator iter = list.begin();
iter != list.end(); ++iter)
{
std::cout << (*iter) << std::endl;
}
std::cin.ignore();
//List object takes care of deletion for us.
return 0;
}
|
就功能而言,這是一個巨大的改進。我們現在為我們的節點提供了(基本的)記憶體管理,以及易於使用的迭代器來迭代我們的節點,而沒有指標的危險。你還想要什麼?
上面是單向連結串列的快速實現。如果你檢視程式碼,它相對簡單明瞭(透過邏輯)。沒有的是已經被註釋掉了。
在上面,我們可以為我們的特定需求更改和定製*很多*東西。例如,(*)類的迭代器可以同時包含前一個節點和當前節點,以幫助更有效地進行刪除,但會犧牲記憶體和迭代時間。例如,如果您有一個很大的列表並且一直移動並且必須重新迭代列表,那麼由於重新分配多個節點引用的額外常量,這可能不是很有效。如果您不斷地刪除和/或交換列表中的元素,那麼這將非常有效,因為您需要更改將被交換或刪除的元素的先前節點所持有的下一個引用。
還有一種方法可以透過一種稱為 XOR 連結的雙向連結串列來降低記憶體成本,該方法使用 XOR 加密指標來縮小使用的記憶體大小。TODO:提供示例。