wiki:forward-list
Differences
This shows you the differences between two versions of the page.
| Both sides previous revisionPrevious revision | |||
| wiki:forward-list [August 14, 2026 at 14:55] – Ivan Janevski | wiki:forward-list [August 14, 2026 at 15:10] (current) – external edit 127.0.0.1 | ||
|---|---|---|---|
| Line 17: | Line 17: | ||
| | Insert/ | | Insert/ | ||
| | Insert after a given node | O(1) | | | Insert after a given node | O(1) | | ||
| + | | Remove after a given node | O(1) | | ||
| | Access by index | O(n) | | | Access by index | O(n) | | ||
| | Append to tail | O(n) | | | Append to tail | O(n) | | ||
| | Search | O(n) | | | Search | O(n) | | ||
| + | | Reverse | O(n) | | ||
| ## Structure | ## Structure | ||
| - | (TODO) write prose | + | A generic forward list separates the node type from the payload type by storing an opaque `void*` in each node, rather than embedding a fixed value type directly. The list itself is just a pointer to the head node; a `list_t` wrapper adds a tail pointer purely as an O(1) append optimization for this implementation, |
| - | (TODO) draw standalone latex figure here of 1 -> 2 -> 3 -> 4 | + | |
| + | \documentclass[border=5pt]{standalone} | ||
| + | \usepackage{tikz} | ||
| + | \usetikzlibrary{arrows.meta} | ||
| + | \begin{document} | ||
| + | \begin{tikzpicture}[node/ | ||
| + | \foreach \i/\x in {1/ | ||
| + | \node[node] | ||
| + | } | ||
| + | \foreach \i/\j in {1/2,2/3,3/4} { | ||
| + | \draw[thick, | ||
| + | } | ||
| + | \draw[thick, | ||
| + | \end{tikzpicture} | ||
| + | \end{document} | ||
| ```c | ```c | ||
| Line 39: | Line 55: | ||
| ## Operations | ## Operations | ||
| - | (TODO): Write prose for each operation | ||
| - | Write standalone latex figure for each illustration | ||
| - | Write code for each opreation | ||
| ### Create | ### Create | ||
| - | list_create | + | The list starts as a zeroed `list_t`: `ln_head` and `ln_tail` both NULL. `calloc()` is a convenient way to get this for free when the list itself is heap-allocated. |
| + | |||
| + | ```c | ||
| + | list_t *list_create(void) { | ||
| + | return | ||
| + | } | ||
| + | ``` | ||
| + | |||
| + | \documentclass[border=5pt]{standalone} | ||
| + | \usepackage{tikz} | ||
| + | \usetikzlibrary{arrows.meta} | ||
| + | \begin{document} | ||
| + | \begin{tikzpicture}[> | ||
| + | \node (head) at (0,0) {head}; | ||
| + | \node (tail) at (2.4,0) {tail}; | ||
| + | \node[draw, | ||
| + | \draw[thick, | ||
| + | \draw[thick, | ||
| + | \end{tikzpicture} | ||
| + | \end{document} | ||
| ### Destroy | ### Destroy | ||
| - | Free memory with free | + | Freeing the list means walking from `ln_head` to NULL, freeing each node, then freeing the list struct itself. The payload pointed to by `ln_data` is the caller' |
| + | |||
| + | ```c | ||
| + | void list_destroy(list_t *list) { | ||
| + | list_node_t *cur = list-> | ||
| + | while (cur != NULL) { | ||
| + | list_node_t *next = cur-> | ||
| + | | ||
| + | cur = next; | ||
| + | } | ||
| + | free(list); | ||
| + | } | ||
| + | ``` | ||
| + | |||
| + | \documentclass[border=5pt]{standalone} | ||
| + | \usepackage{tikz} | ||
| + | \usetikzlibrary{arrows.meta} | ||
| + | \begin{document} | ||
| + | \begin{tikzpicture}[node/ | ||
| + | \node[node] (n1) at (0,0) {1}; | ||
| + | \node[node] (n2) at (1.6,0) {2}; | ||
| + | \node[node] (n3) at (3.2,0) {3}; | ||
| + | \draw[thick, | ||
| + | \draw[thick, | ||
| + | \draw[thick, | ||
| + | |||
| + | \node[node, | ||
| + | \node[node, | ||
| + | \node[node, | ||
| + | \foreach \n in {m1,m2,m3} { | ||
| + | \draw[gray, | ||
| + | \draw[gray, | ||
| + | } | ||
| + | \node[gray] at (1.6,-2.9) {free()' | ||
| + | \end{tikzpicture} | ||
| + | \end{document} | ||
| ### Append | ### Append | ||
| + | Because `ln_tail` is tracked, appending is O(1): the new node is linked after the current tail, and `ln_tail` is advanced. Without a tail pointer this would require an O(n) traversal, as noted above. | ||
| + | |||
| + | ```c | ||
| + | void list_append(list_t *list, void *data) { | ||
| + | list_node_t *node = malloc(sizeof(list_node_t)); | ||
| + | node-> | ||
| + | node-> | ||
| + | if (list-> | ||
| + | list-> | ||
| + | } else { | ||
| + | list-> | ||
| + | list-> | ||
| + | } | ||
| + | } | ||
| + | ``` | ||
| + | |||
| + | \documentclass[border=5pt]{standalone} | ||
| + | \usepackage{tikz} | ||
| + | \usetikzlibrary{arrows.meta} | ||
| + | \begin{document} | ||
| + | \begin{tikzpicture}[node/ | ||
| + | \node[node] (n1) at (0,0) {1}; | ||
| + | \node[node] (n2) at (1.6,0) {2}; | ||
| + | \node[node] (n3) at (3.2,0) {3}; | ||
| + | \node[node, | ||
| + | \draw[thick, | ||
| + | \draw[thick, | ||
| + | \draw[teal, | ||
| + | \draw[thick, | ||
| + | \node at (3.2,0.9) {tail (before)}; | ||
| + | \draw[thick, | ||
| + | \node[teal] at (4.8,0.9) {tail (after)}; | ||
| + | \draw[teal, | ||
| + | \end{tikzpicture} | ||
| + | \end{document} | ||
| + | |||
| ### Prepend | ### Prepend | ||
| - | ### Sort | + | Prepending is O(1) regardless of whether a tail pointer exists, since it only ever touches the head. |
| + | |||
| + | ```c | ||
| + | void list_prepend(list_t *list, void *data) { | ||
| + | list_node_t *node = malloc(sizeof(list_node_t)); | ||
| + | node-> | ||
| + | node-> | ||
| + | list-> | ||
| + | if (list-> | ||
| + | list-> | ||
| + | } | ||
| + | } | ||
| + | ``` | ||
| + | |||
| + | \documentclass[border=5pt]{standalone} | ||
| + | \usepackage{tikz} | ||
| + | \usetikzlibrary{arrows.meta} | ||
| + | \begin{document} | ||
| + | \begin{tikzpicture}[node/ | ||
| + | \node[node, | ||
| + | \node[node] (n2) at (1.6,0) {2}; | ||
| + | \node[node] (n3) at (3.2,0) {3}; | ||
| + | \draw[teal, | ||
| + | \draw[thick, | ||
| + | \draw[thick, | ||
| + | \node[teal] at (0,0.9) {head (after)}; | ||
| + | \draw[teal, | ||
| + | \node at (1.6,0.9) {head (before)}; | ||
| + | \draw[thick, | ||
| + | \end{tikzpicture} | ||
| + | \end{document} | ||
| + | |||
| + | ### Insert after | ||
| + | Inserting after a given node is O(1) because it only touches two links: the new node's `ln_next` and the given node's `ln_next`. This is the operation a forward list is actually good at, and the reason algorithms hold onto a node pointer rather than an index when they can. If the given node happens to be the tail, `ln_tail` must be advanced too. | ||
| + | |||
| + | ```c | ||
| + | void list_insert_after(list_t *list, list_node_t *node, void *data) { | ||
| + | list_node_t *n = malloc(sizeof(list_node_t)); | ||
| + | n-> | ||
| + | n-> | ||
| + | node-> | ||
| + | if (list-> | ||
| + | list-> | ||
| + | } | ||
| + | } | ||
| + | ``` | ||
| + | |||
| + | \documentclass[border=5pt]{standalone} | ||
| + | \usepackage{tikz} | ||
| + | \usetikzlibrary{arrows.meta} | ||
| + | \begin{document} | ||
| + | \begin{tikzpicture}[node/ | ||
| + | \node[node] (n1) at (0,0) {1}; | ||
| + | \node[node] (n2) at (1.6,0) {2}; | ||
| + | \node[node] (n3) at (3.2,0) {3}; | ||
| + | \node[node, | ||
| + | \draw[thick, | ||
| + | \draw[gray, | ||
| + | \draw[teal, | ||
| + | \draw[teal, | ||
| + | \draw[thick, | ||
| + | \end{tikzpicture} | ||
| + | \end{document} | ||
| + | |||
| + | ### Remove after | ||
| + | The mirror of insert-after: | ||
| + | |||
| + | ```c | ||
| + | void list_remove_after(list_t *list, list_node_t *node) { | ||
| + | list_node_t *doomed = node-> | ||
| + | if (doomed == NULL) return; | ||
| + | node-> | ||
| + | if (list-> | ||
| + | list-> | ||
| + | } | ||
| + | free(doomed); | ||
| + | } | ||
| + | ``` | ||
| + | |||
| + | \documentclass[border=5pt]{standalone} | ||
| + | \usepackage{tikz} | ||
| + | \usetikzlibrary{arrows.meta} | ||
| + | \begin{document} | ||
| + | \begin{tikzpicture}[node/ | ||
| + | \node[node] (n1) at (0,0) {1}; | ||
| + | \node[node] (n2) at (1.6,0) {2}; | ||
| + | \node[node, | ||
| + | \node[node] (n4) at (4.8,0) {4}; | ||
| + | \draw[thick, | ||
| + | \draw[gray, | ||
| + | \draw[gray, | ||
| + | \draw[thick, | ||
| + | \draw[thick, | ||
| + | \node[gray] at (3.2,-0.9) {free()' | ||
| + | \end{tikzpicture} | ||
| + | \end{document} | ||
| ### Search | ### Search | ||
| - | ### ... most common oprations... | + | A linear scan comparing `ln_data` against a target using a caller-supplied comparator, since the list is generic over the payload type. |
| + | |||
| + | ```c | ||
| + | list_node_t *list_search(list_t *list, void *key, int (*cmp)(void *, void *)) { | ||
| + | list_node_t *cur = list-> | ||
| + | while (cur != NULL) { | ||
| + | if (cmp(cur-> | ||
| + | cur = cur-> | ||
| + | } | ||
| + | return NULL; | ||
| + | } | ||
| + | ``` | ||
| + | |||
| + | \documentclass[border=5pt]{standalone} | ||
| + | \usepackage{tikz} | ||
| + | \usetikzlibrary{arrows.meta} | ||
| + | \begin{document} | ||
| + | \begin{tikzpicture}[node/ | ||
| + | \node[node] (n1) at (0,0) {7}; | ||
| + | \node[node] (n2) at (1.6,0) {3}; | ||
| + | \node[node, | ||
| + | \node[node] (n4) at (4.8,0) {2}; | ||
| + | \draw[thick, | ||
| + | \draw[thick, | ||
| + | \draw[thick, | ||
| + | \draw[thick, | ||
| + | \node[orange] at (3.2,-0.9) {key = 9, found}; | ||
| + | \end{tikzpicture} | ||
| + | \end{document} | ||
| + | |||
| + | ### Length | ||
| + | Since there is no size field, computing the length costs an O(n) traversal. A forward list that needs frequent length queries should cache a count in `list_t` and maintain it in every insert/ | ||
| + | |||
| + | ```c | ||
| + | size_t list_length(list_t *list) { | ||
| + | size_t n = 0; | ||
| + | for (list_node_t *cur = list-> | ||
| + | n++; | ||
| + | } | ||
| + | return n; | ||
| + | } | ||
| + | ``` | ||
| + | |||
| + | ### Reverse | ||
| + | Reversal is done in place with the classic three-pointer walk: `prev`, `cur`, and a saved `next` so the forward link isn't lost the moment it's overwritten. Each node's `ln_next` is flipped to point backward, and `prev` and `cur` both advance one step. The old head ends up as the new tail and vice versa, so both `ln_head` and `ln_tail` need updating. | ||
| + | |||
| + | ```c | ||
| + | void list_reverse(list_t *list) { | ||
| + | list_node_t *prev = NULL; | ||
| + | list_node_t *cur = list-> | ||
| + | list-> | ||
| + | while (cur != NULL) { | ||
| + | list_node_t *next = cur-> | ||
| + | cur-> | ||
| + | prev = cur; | ||
| + | cur = next; | ||
| + | } | ||
| + | list-> | ||
| + | } | ||
| + | ``` | ||
| + | |||
| + | \documentclass[border=5pt]{standalone} | ||
| + | \usepackage{tikz} | ||
| + | \usetikzlibrary{arrows.meta} | ||
| + | \begin{document} | ||
| + | \begin{tikzpicture}[node/ | ||
| + | \node[node] (n1) at (0,0) {1}; | ||
| + | \node[node] (n2) at (1.6,0) {2}; | ||
| + | \node[node] (n3) at (3.2,0) {3}; | ||
| + | \node[node] (n4) at (4.8,0) {4}; | ||
| + | \draw[thick, | ||
| + | \draw[thick, | ||
| + | \draw[thick, | ||
| + | \draw[thick, | ||
| + | |||
| + | \node[node, | ||
| + | \node[node, | ||
| + | \node[node, | ||
| + | \node[node, | ||
| + | \draw[teal, | ||
| + | \draw[teal, | ||
| + | \draw[teal, | ||
| + | \draw[teal, | ||
| + | \end{tikzpicture} | ||
| + | \end{document} | ||
| + | |||
| + | ### Sort | ||
| + | In-place sorting a singly linked list is naturally suited to merge sort: splitting the list in half only requires forward pointers (advance a slow and a fast pointer to find the midpoint), and merging two sorted forward lists only requires relinking `ln_next` pointers, no random access. Quicksort' | ||
| + | |||
| + | ```c | ||
| + | static list_node_t *split(list_node_t *head) { | ||
| + | list_node_t *slow = head, *fast = head-> | ||
| + | while (fast != NULL && fast-> | ||
| + | slow = slow-> | ||
| + | fast = fast-> | ||
| + | } | ||
| + | list_node_t *mid = slow-> | ||
| + | slow-> | ||
| + | return mid; | ||
| + | } | ||
| + | |||
| + | static list_node_t *merge(list_node_t *a, list_node_t *b, int (*cmp)(void *, void *)) { | ||
| + | list_node_t head = {0}; | ||
| + | list_node_t *tail = & | ||
| + | while (a != NULL && b != NULL) { | ||
| + | if (cmp(a-> | ||
| + | tail-> | ||
| + | a = a-> | ||
| + | } else { | ||
| + | tail-> | ||
| + | b = b-> | ||
| + | } | ||
| + | tail = tail-> | ||
| + | } | ||
| + | tail-> | ||
| + | return head.ln_next; | ||
| + | } | ||
| + | |||
| + | static list_node_t *merge_sort(list_node_t *head, int (*cmp)(void *, void *)) { | ||
| + | if (head == NULL || head-> | ||
| + | list_node_t *mid = split(head); | ||
| + | return merge(merge_sort(head, | ||
| + | } | ||
| + | |||
| + | void list_sort(list_t *list, int (*cmp)(void *, void *)) { | ||
| + | list-> | ||
| + | list_node_t *cur = list-> | ||
| + | while (cur != NULL && cur-> | ||
| + | cur = cur-> | ||
| + | } | ||
| + | list-> | ||
| + | } | ||
| + | ``` | ||
| + | |||
| + | \documentclass[border=5pt]{standalone} | ||
| + | \usepackage{tikz} | ||
| + | \usetikzlibrary{arrows.meta} | ||
| + | \begin{document} | ||
| + | \begin{tikzpicture}[node/ | ||
| + | \node[node] (b1) at (0,0) {3}; | ||
| + | \node[node] (b2) at (1.6,0) {1}; | ||
| + | \node[node] (b3) at (3.2,0) {4}; | ||
| + | \node[node] (b4) at (4.8,0) {2}; | ||
| + | \draw[thick, | ||
| + | \draw[thick, | ||
| + | \draw[thick, | ||
| + | \draw[thick, | ||
| + | \node at (-1.1,0) {before}; | ||
| + | |||
| + | \node[node, | ||
| + | \node[node, | ||
| + | \node[node, | ||
| + | \node[node, | ||
| + | \draw[teal, | ||
| + | \draw[teal, | ||
| + | \draw[teal, | ||
| + | \draw[teal, | ||
| + | \node at (-1.1,-2) {after}; | ||
| + | \end{tikzpicture} | ||
| + | \end{document} | ||
wiki/forward-list.md · Last modified: (external edit)
