Advertisement
sp2danny

Linked List Tutorial in C using Sentry

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