CS240
Mike Lam, Professor
Linked Lists
Retrospective
● Arrays are great
– O(1) access time to any element
– Amortized O(1) insertion and removal
– Referential arrays allow arbitrary-sized objects
● There are still disadvantages
– Occasional O(n) worst-case costs
– Requires large chunks of reserved memory
– Insertion/removal in the middle is expensive
Retrospective
● Goal: Do less work when inserting and removing in themiddle of our lists
2 3 5 8
Retrospective
● Goal: Do less work when inserting and removing in themiddle of our lists
● Let's "pull apart" the array
2 3 5 8
2 3 5 8
Retrospective
● Goal: Do less work when inserting and removing in themiddle of our lists
● Let's "pull apart" the array
● And add links between all the items
2 3 5 8
2 3 5 8
2 3 5 8
Linked Lists
● This is a "linked list"
● Every item has a "next" pointer/reference
– Last item has a NULL "next" pointer
● Add and remove items by manipulating the pointers
● Keep external pointers to the beginning ("head") andend ("tail") of the list
2 3 5 8
Singly-Linked Lists
● Singly-linked list:
"a" "b" "c" NULL
head tail
single linkper node
Singly-Linked Lists
● Node:
typedef struct linknode {
data_t data;
struct linknode *next;
} linknode_t;
● Linked list:
typedef struct {
linknode_t *head;
linknode_t *tail;
size_t size;
} linklist_t;
Singly-Linked Lists
● Creating new linked-list nodes:
linknode_t* malloc_node(data_t value)
{
linknode_t *new_node = (linknode_t*)malloc(sizeof(linknode_t));
// TODO: check for null
new_node->value = value;
new_node->next = NULL;
return new_node;
}
Singly-Linked Lists
● Inserting at the head:
linknode_t *newest = malloc_node(x);
newest->next = head;
head = newest;
Singly-Linked Lists
● Inserting at the tail:
linknode_t *newest = malloc_node(x);
newest->next = NULL;
tail->next = newest;
tail = newest;
Singly-Linked Lists
● Removing from the head:
if (head == NULL)
ERROR!
old_head = head;
head = head->next;
free(old_head);
Singly-Linked Lists
● Removing from the tail:
if (tail == NULL)
ERROR
old_tail = tail;
tail = ???
● Problem: Can't access previous node
Singly-Linked Lists
● Removing from the tail:
if (tail == NULL)
ERROR
old_tail = tail;
tail = ???
● Problem: Can't access previous node
– Solution: Track previous nodes as well● (doubly-linked lists)
Challenge
● Given a singly-linked list called "data", write asnippet of code that will print out all of thevalues in the list
Singly-Linked Lists
● Insert: O(1)
– if you have a reference to the location
– O(n) if the new location is index-based or the listneeds to be sorted
● Delete: O(1)
– if you have a reference to the item
– O(n) if you have to look for it
● Indexed access or search: O(n)
Linked Stack
● Consider stack implementation using a singly-linked list
Linked Stack
● Consider stack implementation using a singly-linked list
– Insert and remove at the head
– Push, pop, and top are O(1)
Linked Queue
● Consider queue implementation using a singly-linked list
Linked Queue
● Consider queue implementation using a singly-linked list
– Insert at tail, remove from head● Can't remove from the tail!
– Enqueue, dequeue, and first are O(1)
Looking ahead
● What if we kept two pointers?
– "next" and "prev"
– This is a "doubly-linked list"
● What if tail.next pointed to the head?
– This is a "circularly-linked list"
● What if we kept multiple pointers to placesfurther down the list?
– This is a "skip list"
Doubly-Linked Lists
● Two references: prev and next
– To predecessor and successor nodes
● Allows insert and remove at both ends
– Can now implement stacks, queues, and deques
– “Deque” = “double-ended queue”
Circularly-Linked Lists
● Keep a single node reference
● Useful for round-robin scheduling
– New operation: rotate()
● Can be used to implement regular list
– No need to track both head and tail
– head = tail.next
Sentinels
● Placeholder (“fake”) nodes at head and/or tail
2head tail
head tailEmpty list:
After append(2):
2 3head tailAfter append(3):
2 3 5head tailAfter append(5):
Sentinels
● Simplifies logic of insertion and removal
void list_append(list_t *a, x){ linknode_t *new = malloc_node(x); new->prev = a->tail; new->next = NULL; if (list->head == NULL) { a->head = new; } else { a->tail->next = new; } a->tail = new;}
void list_append(list_t *a, x){ linknode_t *new = malloc_node(x); new->prev = a->tail->prev; new->next = a->tail; a->tail->prev->next = new; a->tail->prev = new;}
2 3 5head tail
head tailEmpty list:
Populated list:
Deques
● Double-ended queue
● Two sets of insert/remove methods:
– addfirst and removefirst
– addlast and removelast
● Implementation using doubly-linked list w/sentinels
Tradeoffs
● Advantages of linked lists
– Worst-case O(1) bounds● No amortized bounds
– O(1) insertions and removals at arbitrary positions● No need to shift elements● This is a HUGE advantage!
Tradeoffs
● Advantages of arrays
– O(1) access to elements by index
– Proportionally fewer actual operations● Calculation and dereference vs. memory allocation and
reference re-arranging
– Proportionally less memory usage● Both arrays and linked lists can be referential● Arrays require at most 2n space overhead, while linked
lists are at least 2n (or 3n for doubly-linked lists)