All entries for Wednesday 10 July 2019
July 10, 2019
Datastructures – Linked lists part 3
Follow-up to Datastructures – Linked lists part 2 from Research Software Engineering at Warwick
This time we're presenting the final piece of the puzzle for linked lists - adding items. Adding items is very, very similar to removing them. To add an item "I" between two other items (lets call them "A" and "B", with A <-> B initially) you just have to set it up so that A's next pointer points to I, B's prev pointer also points to I and that I's prev pointer goes to A and I's next pointer goes to B. You do then have to take care if either A or B don't exist (i.e. you're adding at the start or the end of the list) but basically the idea is always the same. When you don't have a standard that you have to follow interface design in programming is often as much an art as a science and there are several ways of implementing a useful interface to do this. You can write a routine "insert_between(I,A,B)" that adds an item between two specified items (although this the has to deal with what should happen if you try to insert between two items that are not actually linked to each other and also requires the developer to provide redundant information. If this is going to work then A must be linked to B so if you know A you know B and vice versa). To add at the start of the list you specify "A" to be NULL, and to add at the end of the list you can specify "B" to be NULL. This isn't seen very often in the wild but it works perfectly well. You could also write "insert_after(I,A)" and "insert_before(I,B)" routines that would allow you to insert item I either "after A" or "before B", again specifying appropriate NULL items for A or B to add to the start or the end of the list. The most common approach in the wild at the moment is that used by the C++ Standard Type Library containers. Have a routine "insert(I,B)" that inserts the item before a specified item B, a routine "insert_after(I,A) to insert an item after the specified item, a routine "push_back(I)" that adds a new element to the end of your list and a "push_front(I)" routine to add the new item at the start of the list. While I'm not sure that I'd pick them from scratch I'll stick to the STL
void insert(llitem* head, llitem* new, llitem *before) { new->next = before; if (before->prev) { before->prev->next = new; new->prev = before->prev; } else { head = new; } before->prev = new; } void insert_after(llitem* new, llitem *after) { /*head is not used here because inserting after an item cannot touch the head*/ new->prev = after; if (after->next) { after->next->prev = new; new->next = after->prev; } after->next = new; } void push_front(llitem* head, llitem* new) { if (head){ head->prev = new; } head = new; } void push_back(llitem* head, llitem* new) { llitem * current = head; if (!current) { head = new; } else { /*spin on until you find an item that has no next element. That element is the end of the list*/ while(current->next) current = current->next; current->next = new; new->prev = current; } }
SUBROUTINE insert(head, new, before) TYPE(llitem), POINTER, INTENT(INOUT) :: head !Head of linked list TYPE(llitem), POINTER, INTENT(INOUT) :: new !Item to be inserted TYPE(llitem), POINTER, INTENT(INOUT) :: before !Item to insert before new%next => before IF (ASSOCIATED(before%prev)) THEN before%prev%next => new new%prev => before%prev ELSE head => new END IF before%prev => new END SUBROUTINE insert SUBROUTINE insert_after(new, after) !No head pointer because inserting after an item can't touch the head TYPE(llitem), POINTER, INTENT(INOUT) :: new !Item to be inserted TYPE(llitem), POINTER, INTENT(INOUT) :: after !Item to insert after new%prev => after IF (ASSOCIATED(after%next)) THEN after%next%prev => new new%next => after%next END IF after%next => new END SUBROUTINE insert_after SUBROUTINE push_front(head, new) !No head pointer because inserting after an item can't touch the head TYPE(llitem), POINTER, INTENT(INOUT) :: head !Head of the linked list TYPE(llitem), POINTER, INTENT(INOUT) :: new !Item to be added to the list IF (ASSOCIATED(head)) THEN head%prev => new END IF head => new END SUBROUTINE push_front SUBROUTINE push_front(head, new) !No head pointer because inserting after an item can't touch the head TYPE(llitem), POINTER, INTENT(INOUT) :: head !Head of the linked list TYPE(llitem), POINTER, INTENT(INOUT) :: new !Item to be added to the list TYPE(llitem), POINTER :: current current => head IF (.NOT. ASSOCIATED(current)) THEN head => new ELSE DO WHILE(ASSOCIATED(current%next)) current=>current%next END DO current%next=>new new%prev => current END IF END SUBROUTINE push_front
Once again a diagram will probably make things clearer.
There is one thing that you can spot easily from the implementations : in order to add to the end of the list you have to cycle through all of the intermediate items to get to the end. This isn't actually a major problem since for a lot of the uses of linked lists you can freely choose to add to the start of the list which happens immediately by just updating the head. However if you want to be able to add to the end of a linked list then it is common to actually store an equivalent of the "head" item that marks where the end of the list is. This item is generally called the "tail" item. I haven't described anything about using tails because it doesn't add anything to the fundamental nature of the linked list while making all of the implementation a little bit messier. Holding the tail also introduces a debugging risk because it is (technically) redundant and can be inconsistent with the actual last element that you would reach by iterating through the list (usually only because of errors in your implementation). The addition of multiple sources of information about something always complicates matters and you have to test very carefully to ensure that they can't become inconsistent.
This series has basically described how to implement a very simple linked list (if you wanted to). There are many limitations and weaknesses of this implementation (not being certain to nullify pointers on items when adding them, not having a tail item and also the fact that each item can only be in a single linked list at any point (usually you relax that requirement by having each llitem have a pointer to your real data rather than holding the data within the llitem structure itself. You do have to be careful to make sure that items are fully deleted when the last reference to them is released though!)) but it does, mostly, work. The final part of this series on linked lists is going to go through why you might not want to use them. Not just this limited implementation but when linked lists are not suitable datastructures and how you can create hybrids of linked lists and arrays that can perform much better.