Sept 21, 2022
class SinglyLinkedList[T] extends Seq[T]
{
var head: Option[SinglyLinkedListNode[T]] = None
/* ... */
}
class SinglyLinkedListNode(
var value: T,
var next: Option[SinglyLinkedListNode[T]] = None
)
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)
Idea: Keep track of the length
class SinglyLinkedList[T] extends Seq[T]
{
var head: Option[SinglyLinkedListNode[T]] = None
var length = 0
/* ... */
}
Complexity: O(1)
apply(2).
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 Θ(idx))
insert(1, "D").
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 Θ(idx))
Let's use apply()
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?
=n−1∑i=0(n−1)(n−1+1)2=n2−n2=Θ(n2)
Can we do better?
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? ∑n−1i=0Θ(1)=(n−1+1)⋅Θ(1)=Θ(n)
Why does this work?
What is the expensive part of apply?
Index → Value: Θ(idx)
(access by index)
SinglyLinkedListNode → Value: Θ(1)
(access by reference)
An iterator is:
1: For some definition of next.
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)
insertAfter(pos, "D")
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? Θ(1)
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.
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
)
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
}
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) | Θ(1) | Θ(1) | Θ(i), O(n) | Θ(1) |
update(i, value) | Θ(1) | Θ(1) | Θ(i), O(n) | Θ(1) |
insert(i, value) | Θ(n) | O(n), ... | Θ(i), O(n) | Θ(1) |
remove(i, value) | Θ(n) | Θ(n−i), O(n) | Θ(i), O(n) | Θ(1) |
append(i) | Θ(n) | O(n), Amortized Θ(1) | Θ(i), O(n) | Θ(1) |