Advertisement
Guest User

Linked List Tutorial C++

a guest
Jan 24th, 2015
590
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 11.85 KB | None | 0 0
  1.  
  2. This is a small tutorial showing how to design a linked list in C++ using a sentry node.
  3. It's intended for novices that have tried to create a linked list implementation themselves,
  4. and discovered that there are a lot of special cases needed to handle empty list, first node,
  5. last node, etc.
  6.  
  7. Let's first show intended use, since it affects design:
  8.  
  9. Here is a small example creating a list, filling it up with values, and printing them
  10.  
  11. #include <iostream>
  12. #include "LinkedList.h++"
  13.  
  14. int main()
  15. {
  16. List<int> lst;
  17.  
  18. // add items
  19. lst.PushBack( 3 );
  20. lst.PushBack( 5 );
  21. lst.PushBack( 7 );
  22.  
  23. // iterate and print
  24. auto iter = lst.Begin();
  25. while( iter != lst.End() )
  26. {
  27. std::cout << iter->data << " ";
  28. iter = iter->next;
  29. }
  30. std::cout << std::endl;
  31. }
  32.  
  33. A few things to note:
  34.  
  35. 1. The implementation is a template, this is not as complicated to write as
  36. you may think.
  37. 2. The use of the concept 'iterator'. As the tutorial unfold, it will become
  38. clear why this is advantageous.
  39.  
  40.  
  41. Let's take a look in the first few lines of LinkedList.h++
  42. For brevity, all error checking is omitted, and const considerations largely ignored
  43.  
  44. #pragma once
  45.  
  46. #include <cstddef>
  47.  
  48. template<typename T>
  49. class List
  50. {
  51. struct Node
  52. {
  53. Node* next;
  54. Node* prev;
  55. T data;
  56. };
  57.  
  58. struct Sentry // since T might not be default-constructible, we need a separate Sentry type
  59. {
  60. Node* next;
  61. Node* prev;
  62. };
  63.  
  64. Node* sentry; // this will be our sentry node. it will be of type Sentry, but 'poses' as a Node
  65. std::size_t size;
  66.  
  67. // helper function to link 2 nodes
  68. void Link( Node* n1, Node* n2 )
  69. {
  70. n1->next = n2;
  71. n2->prev = n1;
  72. }
  73.  
  74. public:
  75.  
  76. typedef Node* Iterator;
  77.  
  78.  
  79. You may notice that this is a doubly linked list, this is very useful even if you don't
  80. iterate backwards, it simplifies node insertion, node deletion, and last item access.
  81.  
  82. Second, there is no head item, but rather a sentry node. In this implementation, the list
  83. is never really empty, it always contains at least one item (the dummy 'sentry' node).
  84. This removes the need for ALL special cases, as you will see.
  85.  
  86. Effectively, sentry->next functions as a head pointer, and sentry->prev as a tail pointer.
  87.  
  88.  
  89. Let's have a look at a few more lines:
  90.  
  91. // this creates a new empty list
  92. List()
  93. {
  94. sentry = (Node*) new Sentry; // make a new sentry node
  95. Link( sentry, sentry ); // that points to itself
  96. size = 0;
  97. }
  98.  
  99. // returns true if list is empty
  100. bool Empty() const
  101. {
  102. return sentry->next == sentry; // when sentry points to itself, the list is empty
  103. }
  104.  
  105. // returns size of list
  106. std::size_t Size() const
  107. {
  108. return size;
  109. }
  110.  
  111. Please note that here is no copy-constructor, assignment operator etc. The List class is not
  112. really complete without those, but the point of this tutorial is to demonstrate benefits of
  113. using a sentry node, so I cut away some 'clutter'
  114.  
  115.  
  116. Let's see how Begin and End are implemented:
  117.  
  118. // pointer to first item of list, or end if empty
  119. Iterator Begin()
  120. {
  121. return sentry->next; // no special-cases needed when using sentry
  122. }
  123.  
  124. // pointer to end (one past last)
  125. Iterator End()
  126. {
  127. return sentry;
  128. }
  129.  
  130. As you can see, the sentry node also doubles as the 'one past last' marker.
  131.  
  132. If you are unfamiliar with the 'iterator' concept, the begin marker points to the first item,
  133. and the end marker points one past last item. This can be used for arrays too, like this:
  134.  
  135. constexpr int SZ = 15;
  136. int arr[SZ];
  137. int* iter = arr;
  138. int* end = arr+SZ;
  139. while( iter != end )
  140. {
  141. (*iter) = 3; // set value for all items to 3
  142. ++iter;
  143. }
  144.  
  145. With an array, the iterator type is an item pointer, item access is done with '*', and
  146. iterating to next item with '++'
  147.  
  148. With our list implementation, the iterator type is a Node*, item access is done with
  149. '->data' and iterating to next item is done with 'i=i->next'
  150.  
  151. With macros, these could be handled the same way. Then you could change mid-project if
  152. you are using an array or a list, without having to rewrite your algorithms.
  153.  
  154. It could look something like this:
  155.  
  156. // for array
  157. #define NXT(i) ++i
  158. #define ACC(i) *i
  159. // for list
  160. #define NXT(i) i=i->next
  161. #define ACC(i) i->data
  162.  
  163. But it's beyond the scope of this tutorial, so let's not delve further into that.
  164.  
  165. My apologies for the use of macros, but if you are capable of parsing the replacement
  166. templates, you don't qualify as a novice, or need this tutorial.
  167.  
  168. Note that iterators in the standard library's std::list, std::vector etc mimic
  169. C-style array iterators with '++' and '*'
  170.  
  171.  
  172. Lets see how adding items to the list is implemented:
  173.  
  174. // this inserts new data before 'here'
  175. Iterator Insert( Iterator here, const T& data )
  176. {
  177. Iterator item = new Node{0,0,data}; // create new item. use T's copy-constructor
  178. Link( here->prev, item ); // link in new node. item comes before here,
  179. Link( item, here ); // so in-between `here->prev´ and `here´
  180. size += 1; // update size
  181. return item;
  182. }
  183.  
  184. // adds new item last
  185. void PushBack( const T& d )
  186. {
  187. Insert( End(), d );
  188. }
  189.  
  190. // adds new item first
  191. void PushFront( const T& d )
  192. {
  193. Insert( Begin(), d );
  194. }
  195.  
  196. As you can see, the Insert function adds the new item *before* its marker node,
  197. this way you can easily insert nodes everywhere, even first and last,
  198. as the PushBack and PushFront functions demonstrate.
  199.  
  200.  
  201. Let's have a look at the erase implementation:
  202.  
  203. // erase one item. erasing end is harmless
  204. Iterator Erase( Iterator here )
  205. {
  206. if( here == sentry ) return sentry; // check for end
  207. Iterator nxt = here->next; // save next item for return value
  208. Link( here->prev, here->next ); // unlink item. no special cases needed when using sentry
  209. delete here; // delete item. this will call T's destructor
  210. size -= 1; // update size
  211. return nxt;
  212. }
  213.  
  214. A curious thing is that Erase returns the next item. This is for a reason:
  215.  
  216. This is how you iterate a list, while deleting some items that fulfils some criteria:
  217.  
  218. auto iter = lst.Begin();
  219. while( iter != lst.End() )
  220. {
  221. DoSomething( iter->data );
  222. if( SomeCriteria( iter->data ) )
  223. iter = lst.Erase( iter ); // this is why Erase returns an iterator
  224. else
  225. iter = iter->next;
  226. }
  227.  
  228. Also note that erasing end is NOT harmless on STL containers.
  229.  
  230.  
  231. A List is sometimes used as a stack. We have Push functions, let's have Pop too:
  232.  
  233. // remove last item. don't call on empty list
  234. void PopBack()
  235. {
  236. Erase( sentry->prev ); // sentry->prev points to last item
  237. }
  238.  
  239. // remove first item. don't call on empty list
  240. void PopFront()
  241. {
  242. Erase( sentry->next ); // sentry->next points to first item
  243. }
  244.  
  245. I chose to let the Pop functions return void, since T may not be lightweight
  246.  
  247.  
  248. We are nearing the end. Lets have a way to clean up after us.
  249.  
  250. // erase all items
  251. void Clear()
  252. {
  253. Iterator cur = sentry->next;
  254. while( cur != sentry )
  255. {
  256. Iterator nxt = cur->next; // we have to save the next pointer, since deletion invalidates access
  257. delete cur; // this will call T's destructor
  258. cur = nxt;
  259. }
  260. size = 0;
  261. // note that the list is left in a usable (but empty) state
  262. // while(!Empty()) Erase(Begin()); // would have been simpler
  263. }
  264.  
  265. // this destroys the list itself
  266. ~List()
  267. {
  268. Clear();
  269. delete (Sentry*) sentry;
  270. }
  271.  
  272.  
  273. A few more convenience functions:
  274.  
  275. // returns last item by reference
  276. T& Back()
  277. {
  278. return sentry->prev->data;
  279. }
  280.  
  281. // returns first item by reference
  282. T& Front()
  283. {
  284. return sentry->next->data;
  285. }
  286.  
  287. // non-range-checked index access
  288. T& operator [] ( std::size_t idx )
  289. {
  290. Iterator iter = Begin();
  291. while( idx-- ) iter = iter->next; // this loop is why index access in O(n)
  292. return iter->data;
  293. }
  294.  
  295.  
  296. Now lastly an integrity check for debugging purpose:
  297.  
  298. bool IntegrityCheck()
  299. {
  300. if( ! sentry ) return false; // not even allocated?
  301. Iterator prev = sentry;
  302. Iterator curr = sentry->next;
  303. std::size_t sz = 0;
  304. while( curr != sentry )
  305. {
  306. if( ! curr ) return false; // there should be no null pointers in the list
  307. if( sz >= size ) return false; // list is longer than what size suggest
  308. ++sz;
  309. if( curr->prev != prev ) return false; // back-pointer dont point to previous item
  310. prev = curr;
  311. curr = curr->next;
  312. }
  313. if( sz != size ) return false; // list is shorter than what size suggest
  314. if( sentry->prev != prev ) return false; // 'tail' pointer don't point to last item
  315.  
  316. return true;
  317. }
  318.  
  319. };
  320.  
  321. That concludes the content of LinkedList.h++, lets have a complete example:
  322.  
  323. #include <iostream>
  324. #include <cassert>
  325. #include <string>
  326.  
  327. #include "LinkedList.h++"
  328.  
  329. // lets create an adaptor, so we can use range-based for loops
  330. template<typename T> struct ll_iter {
  331. typename List<T>::Iterator i;
  332. ll_iter& operator++() { i=i->next; return *this; }
  333. T& operator*() { return i->data; }
  334. bool operator!=(ll_iter other) { return i != other.i; }
  335. };
  336. template<typename T> ll_iter<T> begin ( List<T>& l ) { return {l.Begin()}; }
  337. template<typename T> ll_iter<T> end ( List<T>& l ) { return {l.End()}; }
  338.  
  339. using namespace std;
  340.  
  341. string TestList()
  342. {
  343. List<int> list;
  344.  
  345. assert( list.IntegrityCheck() );
  346.  
  347. list.PushBack( 1 );
  348. list.PushBack( 2 );
  349. list.PushBack( 3 );
  350. list.PushBack( 4 );
  351.  
  352. // list should be 1, 2, 3, 4
  353.  
  354. assert( list.IntegrityCheck() );
  355.  
  356. auto iter = list.Begin();
  357. while( iter != list.End() )
  358. {
  359. iter->data *= 3; // triple values to 3, 6, 9, 12
  360. if( iter->data % 2 )
  361. iter = list.Erase( iter ); // remove odd, now 6, 12
  362. else
  363. iter = iter->next;
  364. }
  365.  
  366. assert( list.IntegrityCheck() );
  367.  
  368. string str;
  369.  
  370. for( auto&& val : list )
  371. {
  372. str += to_string( val ) + " ";
  373. }
  374.  
  375. return str; // this should return "6 12 "
  376. }
  377.  
  378. int main() { cout << TestList() << endl; }
  379.  
  380.  
  381.  
  382. For reference, the whole content of LinkedList.h++ can be accessed at http://codepad.org/78kxUIUZ
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement