1 of 40

Pre-announcements

  • Info session at Kresge Library at 6:30 PM tomorrow 1/31 if you want to help out with autonomous underwater vehicles.

  • No experience needed all are welcome.

2 of 40

CS61B: 2019

Lecture 4: Node Based Lists

  • From IntList to SLList
    • The private keyword
    • Nested classes
    • Recursive private helper methods
    • Caching
    • Sentinel nodes

3 of 40

From IntList to SLList

4 of 40

Last Time in 61B: Recursive Implementation of a List

While functional, “naked” linked lists like the one above are hard to use.

  • Users of this class are probably going to need to know references very well, and be able to think recursively. Let’s make our users’ lives easier.

public class IntList {

public int first;

public IntList rest;

public IntList(int f, IntList r) {

first = f;

rest = r;

}

...

5 of 40

Improvement #1: Rebranding and Culling

public class IntNode {

public int item;

public IntNode next;

public IntNode(int i, IntNode n) {

item = i;

next = n;

}

}

Not much of an improvement obviously, but this next weird trick will be more impressive.

IntNode is now dumb, has no methods. We will reintroduce functionality in the coming slides.

6 of 40

Improvement #2: Bureaucracy

public class IntNode {

public int item;

public IntNode next;

public IntNode(int i, IntNode n) {

item = i;

next = n;

}

}

IntList X = new IntList(10, null);

SLList Y = new SLList(10);

public class SLList {

public IntNode first;

public SLList(int x) {

first = new IntNode(x, null);

}

...

}

SLList is easier to instantiate (no need to specify null), but we will see more advantages to come.

Next: Let’s add addFirst and getFirst methods to SLList.

IntNode is now dumb, has no methods.

7 of 40

The Basic SLList and Helper IntNode Class

public class SLList {

public IntNode first;

public SLList(int x) {

first = new IntNode(x, null);

}

public void addFirst(int x) {

first = new IntNode(x, first);

}

public int getFirst() {

return first.item;

}

}

public class IntNode {

public int item;

public IntNode next;

public IntNode(int i, IntNode n) {

item = i;

next = n;

}

}

SLList L = new SLList(15);

L.addFirst(10);

L.addFirst(5);

int x = L.getFirst();

Example

usage:

8 of 40

SLLists vs. IntLists

While functional, “naked” linked lists like the IntList class are hard to use.

  • Users of IntList are need to know Java references well, and be able to think recursively.
  • SLList is much simpler to use. Simply use the provided methods.
  • Why not just add an addFirst method to the IntList class? Turns out there is no efficient way to do this. See exercises in lectureCode repository.

SLList L = new SLList(15);

L.addFirst(10);

L.addFirst(5);

int x = L.getFirst();

IntList L = new IntList(15, null);

L = new IntList(10, L);

L = new IntList(5, L);

int x = L.first;

9 of 40

Naked Linked Lists (IntList) vs. SLLists

L1

addFirst()

SLList class acts as a middle man between user and raw data structure.

first

getFirst()

5

10

15

L2

L1

item

next

5

10

15

first

rest

Naked recursion: Natural for IntList user to have variables that point to the middle of the IntList.

10 of 40

Public vs. Private

Nested Classes

11 of 40

The SLList So Far

public class SLList {

public IntNode first;

public SLList(int x) {

first = new IntNode(x, null);

}

public void addFirst(int x) {

first = new IntNode(x, first);

}

...

}

SLList L = new SLList(15);

L.addFirst(10);

L

addFirst()

first

getFirst()

item

next

10

15

12 of 40

A Potential SLList Danger

public class SLList {

public IntNode first;

public SLList(int x) {

first = new IntNode(x, null);

}

public void addFirst(int x) {

first = new IntNode(x, first);

}

...

}

SLList L = new SLList(15);

L.addFirst(10);

L.first.next.next = L.first.next;

L

addFirst()

first

getFirst()

item

next

10

15

Users of our class might be tempted to try to manipulate our secret IntNode directly in uncouth ways!

13 of 40

A Potential SLList Danger

public class SLList {

public IntNode first;

public SLList(int x) {

first = new IntNode(x, null);

}

public void addFirst(int x) {

first = new IntNode(x, first);

}

...

}

Users of our class might be tempted to try to manipulate our secret IntNode directly in uncouth ways!

SLList L = new SLList(15);

L.addFirst(10);

L.first.next.next = L.first.next;

L

addFirst()

first

getFirst()

item

next

10

15

14 of 40

Access Control

public class SLList {

public IntNode first;

public SLList(int x) {

first = new IntNode(x, null);

}

public void addFirst(int x) {

first = new IntNode(x, first);

}

...

}

We can prevent programmers from making such mistakes with the private keyword.

15 of 40

Improvement #3: Access Control

public class SLList {

private IntNode first;

public SLList(int x) {

first = new IntNode(x, null);

}

public void addFirst(int x) {

first = new IntNode(x, first);

}

...

}

Use the private keyword to prevent code in other classes from using members (or constructors) of a class.

SLList L = new SLList(15);

L.addFirst(10);

L.first.next.next = L.first.next;

jug ~/Dropbox/61b/lec/lists2

$ javac SLListUser.java

SLListUser.java:8: error: first has private access in SLList

L.first.next.next = L.first.next;

16 of 40

Why Restrict Access?

Hide implementation details from users of your class.

  • Less for user of class to understand.
  • Safe for you to change private methods (implementation).

Car analogy:

  • Public: Pedals, Steering Wheel Private: Fuel line, Rotary valve

  • Despite the term ‘access control’:
    • Nothing to do with protection against hackers, spies, and other evil entities.

17 of 40

Improvement #4: Nested Classes

Can combine two classes into one file pretty simply.

public class SLList {

public class IntNode {

public int item;

public IntNode next;

public IntNode(int i, IntNode n) {

item = i;

next = n;

}

}

private IntNode first;

public SLList(int x) {

first = new IntNode(x, null);

} ...

Nested class definition.

Could have made IntNode a private nested class if we wanted.

Instance variables, constructors, and methods of SLList typically go below nested class definition.

18 of 40

Why Nested Classes?

Nested Classes are useful when a class doesn’t stand on its own and is obviously subordinate to another class.

  • Make the nested class private if other classes should never use the nested class.

In my opinion, probably makes sense to make IntNode a nested private class.

  • Hard to imagine other classes having a need to manipulate IntNodes.

19 of 40

Static Nested Classes

If the nested class never uses any instance variables or methods of the outer class, declare it static.

  • Static classes cannot access outer class’s instance variables or methods.
  • Results in a minor savings of memory. See book for more details / exercise.

public class SLList {

private static class IntNode {

public int item;

public IntNode next;

public IntNode(int i, IntNode n) {

item = i;

next = n;

}

...

We can declare IntNode static, since it never uses any of SLList’s instance variables or methods.

Analogy: Static methods had no way to access “my” instance variables. Static classes cannot access “my” outer class’s instance variables.

Unimportant note: For private nested classes, access modifiers are irrelevant.

20 of 40

addLast() and size()

21 of 40

Adding More SLList Functionality

To motivate our remaining improvements, and to give more functionality to our SLList class, let’s add:

  • .addLast(int x)
  • .size()

Recommendation: Try writing them yourself before watching how I do it.

Methods

Non-Obvious Improvements

addFirst(int x)

#1

Rebranding: IntListIntNode

getFirst

#2

Bureaucracy: SLList

#3

Access Control: publicprivate

#4

Nested Class: Bringing IntNode into SLList

See study guide for starter code!

Answers not shown in slides. See sp19-lectureCode or video for answers.

22 of 40

Private Recursive Helper Methods

To implement a recursive method in a class that is not itself recursive (e.g. SLList):

  • Create a private recursive helper method.
  • Have the public method call the private recursive helper method.

public class SLList {

private int size(IntNode p) {

if (p.next == null) {

return 1;

}

return 1 + size(p.next);

}

public int size() {

return size(first);

}

}

23 of 40

Efficiency of Size: http://yellkey.com/design

How efficient is size?

  • Suppose size takes 2 seconds on a list of size 1,000.
  • How long will it take on a list of size 1,000,000?

  1. 0.002 seconds.
  2. 2 seconds.
  3. 2,000 seconds.
  4. 2,000,000 seconds.

public class SLList {

private int size(IntNode p) {

if (p.next == null) {

return 1;

}

return 1 + size(p.next);

}

public int size() {

return size(first);

}

}

24 of 40

Improvement #5: Fast size()

Your goal:

  • Modify SLList so that the execution time of size() is always fast (i.e. independent of the size of the list).

Also I have a gift giveaway: Accidentally some CDs from KALX 10 years ago. Come take one from the front if you want one.

private IntNode first;

public SLList(int x) {

first = new IntNode(x, null);

}

public void addFirst(int x) {

First = new IntNode(x, front);

}

private int size(IntNode p) {

if (p.next == null)

return 1;

return 1 + size(p.next);

}

public int size() {

return size(first);

}

25 of 40

Improvement #5: Fast size()

Solution: Maintain a special size variable that caches the size of the list.

  • Caching: putting aside data to speed up retrieval.

TANSTAAFL: There ain't no such thing as a free lunch.

  • But spreading the work over each add call is a net win in almost any circumstance.

26 of 40

Naked Linked Lists (IntList) vs. SLLists

x

addFirst()

SLList class acts as a middle man between user and the naked recursive data structure. Allows us to store meta information about entire list, e.g. size.

first

getFirst()

5

10

15

x

item

next

5

10

15

first

rest

3

size

size()

addLast()

27 of 40

Improvement #6a: Representing the Empty List

Benefits of SLList vs. IntList so far:

  • Faster size() method than would have been convenient for IntList.
  • User of an SLList never sees the IntList class.
    • Simpler to use.
    • More efficient addFirst method (see exercises).
    • Avoids errors (or malfeasance):

Another benefit we can gain:

  • Easy to represent the empty list. Represent the empty list by setting first to null. Let’s try!

15

10

28 of 40

How Would You Fix addLast?

Your goal:

  • Fix addLast so that we do not get a null pointer exception when we try to add to the back of an empty SLList:

public class SLList {

private IntNode first;

private int size;

public SLList() {

first = null;

size = 0;

}

public void addLast(int x) {

size += 1;

IntNode p = first;

while (p.next != null) {

p = p.next;

}

p.next = new IntNode(x, null);

} ...

SLList s1 = new SLList();

s1.addLast(5);

See study guide for starter code if you want to try on a computer.

29 of 40

One Solution

One possible solution:

  • Add a special case for the empty list.

But there are other ways...

public void addLast(int x) {

size += 1;

if (first == null) {

first = new IntNode(x, null);

return;

}

IntNode p = first;

while (p.next != null) {

p = p.next;

}

p.next = new IntNode(x, null);

}

30 of 40

Sentinel Nodes

31 of 40

Tip For Being a Good Programmer: Keep Code Simple

As a human programmer, you only have so much working memory.

  • You want to restrict the amount of complexity in your life!
  • Simple code is (usually) good code.
    • Special cases are not ‘simple’.

public void addLast(int x) {

size += 1;

if (first == null) {

first = new IntNode(x, null);

return;

}

IntNode p = first;

while (p.next != null) {

p = p.next;

}

p.next = new IntNode(x, null);

}

32 of 40

addLast’s Fundamental Problem

The fundamental problem:

  • The empty list has a null first. Can’t access first.next!

public void addLast(int x) {

size += 1;

if (first == null) {

first = new IntNode(x, null);

return;

}

IntNode p = first;

while (p.next != null) {

p = p.next;

}

p.next = new IntNode(x, null);

}

Our fix is a bit ugly:

  • Requires a special case.
  • More complex data structures will have many more special cases (gross!!)

How can we avoid special cases?

  • Make all SLLists (even empty) the “same”.

33 of 40

Improvement #6b: Representing the Empty List Using a Sentinel

Create a special node that is always there! Let’s call it a “sentinel node”.

0

addFirst()

first

getFirst()

item

next

??

size

size()

addLast()

addFirst()

first

getFirst()

item

next

??

5

15

10

3

size

size()

addLast()

The empty list is just the sentinel node.

A list with 3 numbers has a sentinel node and 3 nodes that contain real data.

Let’s try reimplementing SLList with a sentinel node.

34 of 40

Sentinel Node

The sentinel node is always there for you.

Notes:

  • I’ve renamed first to be sentinel.
  • sentinel is never null, always points to sentinel node.
  • Sentinel node’s item needs to be some integer, but doesn’t matter what value we pick.
  • Had to fix constructors and methods to be compatible with sentinel nodes.

0

addFirst()

sentinel

getFirst()

item

next

63

size

size()

addLast()

addFirst()

sentinel

getFirst()

item

next

63

5

15

10

3

size

size()

addLast()

35 of 40

addLast (with Sentinel Node)

Bottom line: Having a sentinel simplifies our addLast method.

  • No need for a special case to check if sentinel is null (since it is never null).

public void addLast(int x) {

size += 1;

if (sentinel == null) {

sentinel = new IntNode(x, null);

return;

}

IntNode p = sentinel;

while (p.next != null) {

p = p.next;

}

p.next = new IntNode(x, null);

}

0

addFirst()

sentinel

getFirst()

item

next

??

size

size()

addLast()

36 of 40

Invariants

An invariant is a condition that is guaranteed to be true during code execution (assuming there are no bugs in your code).

An SLList with a sentinel node has at least the following invariants:

  • The sentinel reference always points to a sentinel node.
  • The first node (if it exists), is always at sentinel.next.
  • The size variable is always the total number of items that have been added.

Invariants make it easier to reason about code:

  • Can assume they are true to simplify code (e.g. addLast doesn’t need to worry about nulls).
  • Must ensure that methods preserve invariants.

37 of 40

Summary

Methods

Non-Obvious Improvements

addFirst(int x)

#1

Rebranding: IntListIntNode

getFirst

#2

Bureaucracy: SLList

size

#3

Access Control: publicprivate

addLast(int x)

#4

Nested Class: Bringing IntNode into SLList

#5

Caching: Saving size as an int.

#6

Generalizing: Adding a sentinel node to allow representation of the empty list.

38 of 40

For Those Who Were a Bit Bewildered!

Don’t panic if it felt fast!

The LinkedListDeque class that you’ll build in project 1 (to be officially released Friday) will give you practice so that you can deeply understand the ideas from today’s lecture.

39 of 40

Old Deprecated Slides

40 of 40

Improvement #7: Helper Methods

Suppose we wanted to write a getBack() method.

  • Would be quite similar to insertBack()
  • Make sense to create a getBackNode() method that can be used by both getBack() and insertBack()