CSE-250 Fall 2022 - Section B - Linked Lists and Iterators

Linked Lists and Iterators

CSE-250 Fall 2022 - Section B

Sept 21, 2022

Textbook: Ch. 7

Seq[T]

Array[T]
Pros: $O(1)$ apply, update
Cons: $O(n)$ remove, insert, append
ArrayBuffer[T]
Pros: $O(1)$ apply, update, Amortized $O(1)$ append
Cons: $O(n)$ remove, insert
List[T] (linked list)
Pros: ???
Cons: ???

mutable.List[T] : mutable.Seq[T]


    class SinglyLinkedList[T] extends Seq[T]
    {
      var head: Option[SinglyLinkedListNode[T]] = None

      /* ... */
    }
  

    class SinglyLinkedListNode(
      var value: T, 
      var next: Option[SinglyLinkedListNode[T]] = None
    )   
  

The mutable.Seq ADT

length: Int
Count the number of elements in the seq.
apply(idx: Int): A
Get the element (of type A) at position idx.
insert(idx: Int, elem: A): Unit
Insert an element at position idx with value elem.
remove(idx: Int): A
Remove the element at position idx and return the removed value.

mutable.List[T] : mutable.Seq[T]

Implementing length.


    def length: Int =
    {
      var i = 0
      var current = head
      while(current.isDefined){ i += 1; curr = curr.get.next }
      return i
    }
  

Complexity: $O(n)$

mutable.List[T] : mutable.Seq[T]

Idea: Keep track of the length


    class SinglyLinkedList[T] extends Seq[T]
    {
      var head: Option[SinglyLinkedListNode[T]] = None
      var length = 0

      /* ... */
    }
  

Complexity: $O(1)$

mutable.List[T] : mutable.Seq[T]

apply(2).

mutable.List[T] : mutable.Seq[T]

Implementing apply.


    def apply(idx: Int): T =
    {
      var current = head
      for(i <- 0 until idx){ 
        if(current.isEmpty) { throw IndexOutOfBoundsException(idx) }
        current = current.get.next 
      }
      if(current.isEmpty) { throw IndexOutOfBoundsException(idx) }
      return current
    }
  

Complexity: $O(n)$ (or $\Theta(\texttt{idx})$)

mutable.List[T] : mutable.Seq[T]

insert(1, "D").

mutable.List[T] : mutable.Seq[T]

Implementing insert.


    def insert(idx: Int, value: T): Unit =
    {
      if(idx == 0){ 
        head = Some( new SinglyLinkedListNode(value, head) )
      } else {
        var current = head
        for(i <- 0 until idx){ 
          if(curr.isEmpty) { throw IndexOutOfBoundsException(idx) }
          curr = curr.get.next 
        }
        curr.next = Some( new SinglyLinkedListNode(value, curr.next) )
      }
      length += 1
    }
  

Complexity: $O(n)$ (or $\Theta(\texttt{idx})$)

Let's use apply()

mutable.List[T] : mutable.Seq[T]


    def sum(list: List[Int]): Unit =
    {
      val total: Int = 0
      for(i <- 0 until list.length){ total += list(i) }
      return total
    }
  

What is the complexity?

mutable.List[T] : mutable.Seq[T]

$\sum_{i = 0}^{n-1} T_{apply}(i)$ $=\sum_{i = 0}^{n-1} i$

$$=\sum_{i = 0}^{n-1} \frac{(n-1)(n-1+1)}{2} = \frac{n^2 - n}{2} = \Theta(n^2)$$

Can we do better?

mutable.List[T] : mutable.Seq[T]


    def sum(list: List[Int]): Unit =
    {
      val total: Int = 0
      val current = list.head
      while(current.isDefined){ 
        total += current.get.value
        current = current.get.next
      }
      return total
    }
  

What is the complexity? $\sum_{i = 0}^{n-1} \Theta(1) = (n-1+1)\cdot \Theta(1) = \Theta(n)$

Access-by-Reference vs -by-Index

Why does this work?

What is the expensive part of apply?

Access-by-Reference vs -by-Index

Index → Value: $\Theta(idx)$
(access by index)

SinglyLinkedListNode → Value: $\Theta(1)$
(access by reference)

Iterator[T]

hasNext: Boolean
Returns true if there are more items to retrieve.
next:T
Returns the next item to retrieve.

An iterator is:

  • A reference to an element of the collection
  • A way to get to the next1 element of the collection.

1: For some definition of next.

ListIterator[T] : Iterator[T]


    class ListIterator[T](
      var current: Option[SinglyLinkedListNode[T]]
    )
    {
      def hasNext: Boolean = current.isDefined
      def next: T =
      {
        val ret = current.get.value
        current = current.get.next
        return ret
      }
    }
  

... back to LinkedLists

How about positional operations:
insertAfter(pos: SinglyLinkedListNode[T], value: T)

mutable.List[T] : mutable.Seq[T]

insertAfter(pos, "D")

mutable.List[T] : mutable.Seq[T]

Implementing a positional insertAfter


    def insertAfter(pos: SinglyLinkedListNode[T], value: T) =
    {
      pos.next = Some(
        new SinglyLinkedListNode(value, pos.next)
      )
      length += 1
    }
  

What is the complexity? $\Theta(1)$

mutable.List[T] : mutable.Seq[T]

How would you implement a positional remove?


    def remove(pos: SinglyLinkedListNode[T]): T =
    {
      val prev = ??? /* Problem: Need element **before** pos. */
      prev.next = pos.next
      length -= 1
      return pos.get.value
    }
  

Idea: Add a "backwards" pointer.

mutable.List[T] : mutable.Seq[T]


    class DoublyLinkedList[T] extends Seq[T]
    {
      var head: Option[DoublyLinkedListNode[T]] = None
      var last: Option[DoublyLinkedListNode[T]] = None
      var length = 0 

      /* ... */
    }
  

    class DoublyLinkedListNode[T](
      var value: T, 
      var next: Option[DoublyLinkedListNode[T]] = None
      var prev: Option[DoublyLinkedListNode[T]] = None
    )   
  

mutable.List[T] : mutable.Seq[T]

How would you implement a positional insertAfter?


    def insertAfter(pos: DoublyLinkedListNode[T], value: T) =
    {
      val newNode = new DoublyLinkedListNode(value, prev = Some(pos))
      if(pos.next.isDefined){  pos.next.prev = Some(newNode)
                               newNode.next = pos.next }
      else {                   last = newNode
                               newNode.next = None }
      pos.next = Some(newNode)
      length += 1
    }
  

mutable.List[T] : mutable.Seq[T]

How would you implement a positional remove?


    def remove(pos: DoublyLinkedListNode[T]): T =
    {
      if(pos.prev.isDefined){ pos.prev.next = pos.next }
      else                  { head = pos.next }

      if(pos.next.isDefined){ pos.next.prev = pos.prev }
      else                  { tail = pos.prev }

      length -= 1
      return pos.get.value
    }
  
Operation Array[T] ArrayBuffer[T] List[T] (Index) List[T] (Ref)
apply(i) $\Theta(1)$ $\Theta(1)$ $\Theta(i)$, $O(n)$ $\Theta(1)$
update(i, value) $\Theta(1)$ $\Theta(1)$ $\Theta(i)$, $O(n)$ $\Theta(1)$
insert(i, value) $\Theta(n)$ $O(n)$, ... $\Theta(i)$, $O(n)$ $\Theta(1)$
remove(i, value) $\Theta(n)$ $\Theta(n-i)$, $O(n)$ $\Theta(i)$, $O(n)$ $\Theta(1)$
append(i) $\Theta(n)$ $O(n)$, Amortized $\Theta(1)$ $\Theta(i)$, $O(n)$ $\Theta(1)$