Elementary List Processing
With arrays and structures, we save an item in memory and later refer to it by name (or by index); with linked lists, the manner in which we save information makes it more difficult to access but easier to rearrange.
Working with data that are organized in linked lists is called list processing.
1. Definition
**Definition 3.3 ** A linked list is either a null link or a link to a node that contains an item and a link to a linked list.
This definition is more restrictive than definition 3.2, but it corresponds more closely to the mental model that we have when we write list-processing code.
2. List Processing
2.1 Traverse
One of the common operations that we perform on lists is to traverse them: We scan through the items on the list sequentially, performing some operation on each.
For example, if x
is a pointer to the first node of a list, the final node has a null pointer, and visit
is a function that takes an item as an argument:
for (t = x; t != NULL; t = t->next) {
visit(t->item);
}
This loop is as ubiquitous in list-processing programs as is the corresponding for (int i = 0; i < N; i++)
in array-processing program.
2.2 List Reversal
Implementation in C :
#include <stdio.h>
#include <stdlib.h>
typedef struct node* Link;
struct node {
int item;
Link next;
};
Link reverse(Link head) {
Link r = NULL;
for (Link y = head; y != NULL;) {
// save y to tmp node
// The variable is allocated once, when the function is called
// (https://stackoverflow.com/questions/7959573/declaring-variables-inside-loops-good-practice-or-bad-practice)
Link tmp = y->next;
// y point to r(head of reverse list)
y->next = r;
// move r to y
r = y;
// move y to next
y = tmp;
}
// return the new head of reverse list
return r;
}
void print_list(Link head) {
printf("[");
for (Link x = head; x != NULL; x = x->next) {
printf("%d ", x->item);
}
printf("]");
}
void print_node(Link node) {
printf("{item: %d, next: %p", node->item, node->next);
}
void print_list_verbose(Link head) {
printf("[");
for (Link x = head; x != NULL; x = x->next) {
print_node(x);
printf(", ");
}
printf("]");
}
Link new_list(int len) {
Link head = malloc(sizeof(*head));
head->item = 0;
head->next = NULL;
Link x = head;
// create a list that item is from 0 to len-1
for (int i = 1; i < len; i++) {
Link node = malloc(sizeof(*node));
node->item = i;
// add to tail of the lists
node->next = x->next;
x->next = node;
// move x to next
x = node;
}
return head;
}
int main() {
Link list = new_list(10);
printf("Before: \n");
print_list(list);
printf("\n");
Link reverseLink = reverse(list);
printf("\nAfter: \n");
print_list(reverseLink);
printf("\n");
return 0;
}
Implementation in go:
package main
import (
"fmt"
"os"
"strconv"
"strings"
)
type Node struct {
item int
next *Node
}
func (n *Node) String() string {
var builder strings.Builder
builder.WriteString("[")
for x := n; x != nil; x = x.next {
builder.WriteString(fmt.Sprintf("%d ", x.item))
}
builder.WriteString("]")
return builder.String()
}
func (n *Node) Reverse() *Node {
var r *Node
for x := n; x != nil; {
t := x.next
x.next = r
r = x
x = t
}
return r
}
func NewLinkedList(len int) *Node {
head := &Node{}
x := head
for i := 1; i < len; i++ {
node := &Node{item: i}
node.next = x.next
x.next = node
x = x.next
}
return head
}
func main() {
if len(os.Args) < 2 {
fmt.Printf("Usage: ListReversal list_length")
}
len, _ := strconv.Atoi(os.Args[1])
list := NewLinkedList(len)
fmt.Println(list)
reverseList := list.Reverse()
fmt.Println(reverseList)
}
This function reverses the links in a list, returning a pointer to the final node. To accomplish this task, we need to maintain links to three consecutive nodes in the list.
2.3 List Insertion Sort
Clang implementation:
#include <stdio.h>
#include <stdlib.h>
typedef struct Node* Link;
struct Node {
int item;
Link next;
};
Link generate_random_list(int len) {
Link head = malloc(sizeof(*head));
// head is a dummy node
Link t = head;
for (int i = 0; i < len; i++) {
t->next = malloc(sizeof(*t));
// move top node to next
t = t->next;
t->next = NULL;
t->item = rand() % 100;
}
return head;
}
void print_list(Link head) {
printf("[");
// head is a dummy node
for (Link t = head->next; t != NULL; t = t->next) {
printf("%d ", t->item);
}
printf("]");
}
void print_node_verbose(Link node) {
printf("{Item: %d, Next: %p}", node->item, node->next);
}
Link insertion_sort(Link inHead) {
// dummy node
Link outHead = malloc(sizeof(*outHead));
outHead->next = NULL;
// traverse input list
for (Link x = inHead->next; x != NULL;) {
Link tmp = x->next;
Link y;
for (y = outHead; y->next != NULL; y = y->next) {
if (x->item < y->next->item) {
break;
}
}
x->next = y->next;
y->next = x;
x = tmp;
}
// inHead is no longer to use
// release memory
free(inHead);
return outHead;
}
int main(int argc, char* argv[]) {
if (argc < 2) {
printf("Usage: ListInsertion list_length");
return 1;
}
Link list = generate_random_list(atoi(argv[1]));
print_list(list);
printf("\n");
Link sortedList = insertion_sort(list);
print_list(sortedList);
return 0;
}
Golang implementation:
package main
import (
"fmt"
"math/rand"
"os"
"strconv"
"strings"
)
type Node struct {
item int
next *Node
}
func (n *Node) InsertionSort() *Node {
sortedHead := &Node{}
for x := n.next; x != nil; {
tmp := x.next
var y *Node
for y = sortedHead; y.next != nil; y = y.next {
if x.item < y.next.item {
break
}
}
x.next = y.next
y.next = x
x = tmp
}
return sortedHead
}
func (n Node) String() string {
var builder strings.Builder
builder.WriteString("[")
for x := n.next; x != nil; x = x.next {
builder.WriteString(fmt.Sprintf("%d ", x.item))
}
builder.WriteString("]")
return builder.String()
}
func NewRandomList(len int) *Node {
head := &Node{}
t := head
for i := 0; i < len; i++ {
n := &Node{item: rand.Intn(100)}
t.next = n
t = t.next
}
return head
}
func main() {
if len(os.Args) < 2 {
fmt.Println("Usage ListInsertionSort list_length")
}
len, _ := strconv.Atoi(os.Args[1])
list := NewRandomList(len)
fmt.Println(list)
sortedList := list.InsertionSort()
fmt.Println(sortedList)
}
We maintain a dummy node called a head node at the beginning of each list. We ignored the item field in a list’s head node, but maintain its link as the pointer to the node containing the first item in the list.
The program uses two lists:
- One to collect the random input in the first loop
- Another to collect the sorted output in the second loop
The primary reason to use the head node at the beginning becomes clear when we consider the process of adding the first node to the sorted list.
We have three options:
Duplicate the for loop that finds the smallest item and set up a one-node list.
Test whether the output list is empty every time that we wish to insert a node.
Use a dummy head node whose link points to the first node on the list.
The first option is inelegant and requires extra code; the second is also inelegant and requires extra time.
3. Head and tail conventions in linked lists
3.1 Circular, never empty
Operation | Code |
---|---|
fist insert | head->next = head; |
insert t after x | t->next = x->next; x->next = t; |
delete after x | x->next = x->next->next; |
traversal loop | t = head; do {... t = t->next; } while (t != head); |
test if one item | if (head->next) == head |
3.2 Head pointer, null tail
Operation | Code |
---|---|
initialize | head = NULL |
insert t after x | if (x == NULL){head = t; head->next = NULL;} else {t->next = x->next; x->next = t;} |
delete after x | t = x->next; x->next = t->next; |
traversal loop | for (t = head; t != NULL; t=t->next) |
test if empty | if (head == NULL) |
3.3 Dummy head node, null tail
Operation | Code |
---|---|
initialize | head = malloc(sizeof *head); head->next = NULL; |
insert t after x | t->next = x->next; x->next = t; |
delete after x | t = x->next; x->next = t->next; |
traversal loop | for (t = head->next; t != NULL; t = t->next) |
test if empty | if (head->next == NULL) |
3.4 Dummy head and tail nodes
Operation | Code |
---|---|
initialize | head = malloc(sizeof *head); z = malloc(sizeof *z) head->next = z; z->next = z; |
insert t after x | t->next = x->next; x->next = t; |
delete after x | t = x->next; x->next = t->next; |
traversal loop | for (t = head->next; t != z; t = t->next) |
test if empty | if (head->next == z) |