29. Destructive Functions on Linked Lists


Destructive and nondestructive functions

The list functions that we have written so far have been nondestructive, meaning that they do not alter a list. Now we turn to destructive algorithms, which do alter lists, either by changing the numbers in the list cells or by changing the list structure by altering the pointers.

You almost certainly want to avoid memory sharing when you are using destructive functions, since otherwise changing one list might implicitly change another one that shares cells with the one you change.


Example: insert a value into a sorted linked list

Imagine that you have a linked list L that is in nondescending order. For example, list [2, 4, 4, 5, 9] is in nondescending order, but [2, 4, 3] is not. You would like to insert a value into that list, changing the list, and preserving the fact that it is in nondescending order. For example, if L is [2, 4, 4, 5, 9] before inserting 7, then L is [2, 4, 4, 5, 7, 9] after inserting 7.

When thinking about destructive functions, it is best to look at linked list diagrams rather than only looking at conceptual lists. Starting with linked list

and inserting 7 leads to the following linked list.

Notice that one list cell has been created to hold 7.

This function needs to be able to change pointers in L so that it can insert a new cell. So L needs to be passed by reference. There are three cases to consider to insert x into L.

  1. If L is an empty list, just change L to [x] = cons(x, NULL).

  2. If L is not empty and its head is greater than or equal to x, then x belongs at the beginning of the list; change L to cons(x,L). For example, if x is 1 and L is [2, 4, 6, 8], change L to be [1, 2, 4, 6, 8].

  3. If L is not empty and its head is less than x then insert x into the tail of L. The change in pointers in the tail of L will affect L.

Notice that the first two cases can both be accomplished by statement
  L = cons(x, L);
since, in case 1, L is NULL. Coding those ideas in C++ yields the following definition of insert.
  // insert(x,L) inserts x into list L, changing L.
  //
  // L must be in ascending order when insert is called,
  // and x is inserted in the correct place so that
  // L is sorted after the insertion.

  void insert(const int x, List& L)
  {
    if(L == NULL || x <= L->head)
    {
      L = cons(x, L);
    }
    else
    {
      insert(x, L->tail);
    }
  }
Notice that the definition of insert is tail-recursive. An optimizing compiler will change it into a loop for you.


Pointer variables

Would it have been acceptable to replace line

      insert(x, L->tail);
in the definition of insert by line
      insert(x, tail(L));
No, it would not. The problem is that the second parameter of insert uses call-by-reference. That means the actual thing that is passed must be a variable. L->tail is a variable, but tail(L) is not.


Example: Remove a value from a list

Suppose that remove(x, L) is intended to be a destructive function that removes the first occurrence of x from list L, or does nothing if x does not occur in L. For example, if L is [2, 4, 2, 4, 5] and you perform remove(4, L), then L is changed to [2, 2, 4, 5]. The cases are as follows.

  1. If L is empty, do nothing, since x obviously does not occur in L.

  2. If L is nonempty and the head of L is equal to x, then set L to its tail. However, the cell that contains x is presumably no longer needed. (We are not using memory sharing, so this cell only belongs to one list, L.) So it should be deleted. Look at how it needs to work when L is [2, 4, 6] and x is 2.

  3. If L is not empty and its head is not equal to x, then remove x from the tail of L.

Expressing that in C++ yields the following definition of remove. In the second case, we need to be careful manipulating pointers to avoid cutting the branch out from under ourselves.
  void remove(const int x, List& L)
  {
    if(L != NULL))
    {
      if(L->head == x)
      {
        List p = L;
        L = L->tail;
        delete p;
      }
      else
      {
        remove(x, L->tail))
      }
    }
  }


Example: Modify the items in a list

Suppose that you want doubleAll(L) to modify the items in list L by doubling each. For example, if L is [1, 3, 5], then after doing doubleAll(L), L will contain [2, 6, 10]. The cases are simple.

  1. If L is empty there is nothing to do.

  2. if L is not empty then double its head and call doubleAll on its tail.

Notice that this function does not need to change the pointer stored in L. It only changes what is stored in the list cells. so we do not use call by reference.
  void doubleAll(List L)
  {
    if(L != NULL)
    {
      L->head = 2 * L->head;
      doubleAll(L->tail);
    }
  }


Exercises

  1. Write a C++ definition of function removeAll(x, L), which removes all occurrences of x from list L. Answer

  2. Write a C++ definition of function addToEnd(x, L), which adds x to the end of list L. Answer

  3. If L has length n, how much time does it take to perform addToEnd(x, L)? Use big-Θ notation to express the answer. Answer

  4. Write a C++ definition of function addToStart(x, L) that adds x to the beginning of list L. If L has length n, how long does it take to perform addToStart(x, L)? Answer

  5. Write a C++ definition of function destroy(L), which deletes every cell in list L. Answer

  6. One way to sort a linked list L is to start with an empty list R and successively add each member of L to R, using function insert defined above. Write a function that sorts a linked list into nondescending order using this idea. Answer

  7. Is the definition of doubleAll tail-recursive? Answer