(03) Linked Lists. Impl. Stacks With Linked Lists - Copy
(03) Linked Lists. Impl. Stacks With Linked Lists - Copy
implementing stacks
and queues with it
The “Node” class, and the Singly
Linked List
public class public class LinkedList
1 next Node {
{ Node front;
6
A single node looks like int val; …..
this. Node next; }
}
front
1 null A linked list with just one
element Why is the front so important, and
4 kept separately?
The “Node” class, and the Singly
Linked List
What is an array? When we do, for instance: int[] arr = new int[100] . There are 100 integers
stored in there, but there is only one variable that gives us access to them: ‘arr’ (Which stores
the reference (address) of arr[0]).
arr
21
It’s all stored in the same place in
17 memory (consecutively). So, to
access the third element, I just do:
42
arr[2]. (i.e., jump two spots forward
91 from arr).
68
The figure shows that the node object contains two references now, one to the next node,
and another to its student st.
We could also make a ‘next’ as a private member variable inside the Student class, doing
away with the need off the node class. But that is bad design, as the Student class should
represent a Student, and should be independent of how it will be implemented. ( Think about it,
if you want to represent a student, then there is no concept of ‘next’ student ).
public class However, this would be an okay design if there is some next
Student person, that (in a natural way) belongs to the class person. Such as
{ if they are standing in a row … or … if the next person is the boss
String name; of this person etc.
//etc.
Student next;
Adding a node at the end of the Singly Linked
List
front
1 1 3 8 null
4 1
What if we want to add a node, say, to the end of this linked list. Well (we will see the details
later), we (1) just make a ‘new’ node, and then (2) make the ‘next’ of the last node (the ‘8’
node above), to point to this new node.
front
1 1 3 8
If we start from front, and go till null
4 1
(traverse the list), you will see that now
our list has one more item: 23
2 null
3
Node myNewNode = new
Node();
myNewNode.val = 23;
Adding a node at the end of the Singly Linked
List
front
1 1 3 8
If we start from front, and go till null
4 1
(traverse the list), you will see that now
our list has one more item: 23
2 null
3
addToLast(Node nd)
{
nd.next = null;
else {
while(curr.next != null) curr =
curr.next;
curr.next = nd;
}
Adding the first node in the linked list
When the list is empty, front == null. So, check this condition, and then do:
front = nd;
nd.next = null;
front
front = 2 null
null 3
Inserting after a given Class LinkedList
{
node private Node front;
public void addAtLast(Node
nd)
public void insertAfter(Node
r) …
}
1 1 3 8 1 null
front 4 1
nd
r I have a reference for the node with ‘3’
2 ?? in it, and I want to insert the new node,
1 N, just after it
1 1 3 8 1 null
front
4 1
Step1
Step2 The order of the steps is important
2 (It couldn’t have been the other
1 way round). Let’s see why.
Inserting after a given node
r
1 1 3 8 1 null
front 4 1
Step1
2 nd.next =
1 nd r.next
1 1 3 8 1 null r.next =
front 4 1 nd
Step1
Step2
2
1 nd
2 r.next =
1 nd nd
If you do it in the reverse order it would not work. For example, if you first do r.next = nd
… then look at the figure above. You lost all reference to the node with ‘8’ in it. You have
no way of accessing it, as you did not save the reference (address) of that node.
If you save it first, then indeed you can do it in the reverse order ( there is no point of it
though, other than to show you an example).
Node temp = r.next; //so we are first saving the address of that ‘8’ node
r.next = nd; //And now we can do it in the reverse order … but what’s the point! It is just to show you
another way.
nd.next = temp;
Inserting after a given node
Note that insertion after a node is quite efficient (it is O(1) ), however we are assuming that
we already have a reference to the node ‘r’. If we don’t, we have to do it the long way:
Traverse the list from the start, until you reach the point where we need to insert the new
node.
In an array, inserting into the middle of a long array is never efficient. We would need to
shift many elements.
(1) (2
)
21 16 14 23 62 91 21 16 14 23 62 91 91
21 16 14 23 62 91
4 4
4 5 5
5 (3 (4
) )
21 16 14 23 62 62 91 21 16 14 23 45 62 91
4
5
Advantage and disadvantages compared to
arrays
Class Node Class LinkedList
front { {
1 1 3 8 null int val; Node front;
4 1 Node …..
next; }
}
Remember, we need to add and remove from the same end. So, we can do it
at the ‘front’.
A correct implementation of Push
1 1 3 8 null 1 1 3 8 null
front
4 1 4 1
6 Step 1 6 Step 2
4 front
n 4
n
d nd.next =
d
front;
front = nd;
You can of course do it in some other sequence, just make sure you don’t lose reference to
something you would need later. So, this is the way we can implement “push”
An implementation of pop
1 1 3 8 null 1 1 3 8 null
front 4 1 front 4 1
retVal = front.val; //storing it in retVal, as I need to return this value to the calling function (I am
popping from a stack)
front = front.next; //I have deleted the first node. (What happens if there was only one node?)
return retVal;
If I would have not saved it in ‘retVal’, then after ‘front = front.next’, I would have lost all
contact (reference) to the node which stores 14, and I am supposed to pop and return that
value.
Q: Does this code still work if the linked list contains only 1 item, and I
am popping??
Q: How would you handle pop if the stack is empty (i.e., if front
== null)?
Easy to implement a stack, if you already
have a LinkedList class
public class Stack<T>
{
LinkedList<T> list;
public T pop()
{
//I am skipping steps for exceptions etc.
list.removeFirst();
}
public T peek()
{
//suppose list doesn’t provide this.
//how would you do it using push and pop?
}
}
An implementation of pop
1 1 3 8 null
front 4 1
So, what happens to the node we ‘deleted’, the node with ‘14’? Well, we didn’t
actually delete it, we just made it no longer part of the sequence if we start from
‘front’ and keep going forward.
If there are no references to that node from anywhere else in the code, the Java
Garbage Collector would clean it up, and return the memory to the OS .
By the way C++ does not have a garbage collector. The programmer has to write the
code such as ‘delete(var)’ to return the memory to the OS. Disadvantage: You have to
be more careful when programming to de-allocate whatever memory you have
stopped using. Advantage: There is no garbage collector program which will halt your
program every now and then.
A tailed linked list: Works well for the queue
ADT
tail
Class Node Class TLinkedList
{ {
front int val; Node front;
Node Node tail;
1 1 3 8 null next; void enqueue(int)…
4 1 } int dequeue() ….
}
In a queue, we add and remove from opposite ends (It is First-in-First-out, FIFO). So,
a tailed linked list seems like a good way of implementing a queue.
tail
retVal = front.val;
tail front = front.next;
front return retVal;
}
1 3 8 null But wait. Is this code correct? What happens
1 if there is only one node in the linked list
(see next slide)
front 1 1 3 8 null
4 1
2
public void addLast(int
tail
3 val)
n
d {
Node nd = new
Node(val);
front 1 1 3 8
nd.next = null;
4 1 tail.next = nd;
2 ??
tail = tail.next;
3 }
n
d Remember, with a non-linked list,
adding to the tail needed a
tail
traversal, and hence it was taking
O(n) time. With a ‘tail’ we can now
do it in O(1) time.
front 1 1 3 8 2 Null
4 1 3n
d
Enqueuing (Again, we need to handle some
special cases)
Note, we need extra checks to make it work when we are enqueuing
the first item.
Null
front public void addLast(int public void addLast(int
tail Null val)
val)
{ {
Node nd = new Node nd = new
2 Node(val); Node(val);
3 nd.next = null; nd.next = null;
n
d tail.next = nd;
tail = tail.next; if(tail == Null)
There is no tail, so how {
}
can we access its ‘next’ ? front = tail = nd;
(null.next is an error). }
else
{
tail.next = nd;
tail = tail.next;
}
Easy to implement a stack, if you already
have a LinkedList class
public class Queue<T>
{
LinkedList<T> list;
public T dequeue()
{
list.removeFirst();
}
public T peek()
{
//You can easily implement the peek() inside the linked
list class, using
// ‘return front.val’ (assuming front is not null; else throw
an exception)
}
LinkedList in Java API
As you can imagine, the LinkedList is very useful, in a lot of places, so Java has a pre-built
class of LinkedList.
Just as we did with stacks, let’s read some online documentation of this class in Java.
https://docs.oracle.com/javase/8/docs/api/java/util/LinkedLi
st.html
There are many more. Such as removeFirst, removeLast, afterFirst, afterLast etc.
Using these Linked Lists
Our own linked list - We should make it a
generic class
It can be a linked list of anything.
Notice the toString() function and it’s importance.
Let’s see some code It is what is used by the System.out.println to print
…. objects.
MyLinkedList.jav
a
Node.java
Note we implemented a generic class.
Other than that it just has the functions that we talked about: addFirst, addLast,
removeFirst (It doesn’t have a removeLast, because that cannot be implemented in
O(1) time. Although we can add the O(n) algorithm for it i we wanted (The code is
for a tailed singly list).
Also note, that in the LinkedList class, there is a private member variable size, that
keeps track of the size of the LinkedList. So that we have an O(1) implementation for
getSize().
By the way, how would we remove the end
efficiently?
tail By ‘efficiently’, I mean O(1), and not O(n).
(For some problems O(n) might be very good,
but not for this one).
front 1 1 3 8 null
4 1
In our tailed linked list, we can add to the tail easily (We already did when implementing
dequeue.
But note there is no efficient way to remove the tail, because we need it to then look like
the following: tail So, we need to change the ‘next’ of the 2nd last node above
(the node storing ‘3’), to null.
The problem is, we don’t have quick access to a 2nd last node in
a singly linked list. We have quick access to the tail, but we can
front 1 1 3 null
only go forward from the tail. We also have quick access to
4 1 ‘front’, but we will have to traverse the entire list before getting
to the 2nd last node (So, it would take O(n) time).
You might think that having two tails: References to the last, as
well as the 2nd last node, might help. But it doesn’t. Why? (Try
deleting twice).
Deletion of the last element is not efficient in
a singly linked list (tailed or non-tailed)
tail
int removeLast() //With a tailed singly-
linked list
{
front 1 1 3 8 null Node curr = front;
4 1 if(curr == null) removeFirst(); //i.e., just
pop
else
{
while(curr.next != tail) curr =
curr.next();
Of course, it is not efficient to implement a queue with a linked-list without tail (Because
we need to access both ends of the linked list, and in O(1) time if we want to be efficient).
We have already seen that.
In the removeFirst function, it is returning the node that is deleted (rather than just the
value of the node, the way we returned). So, it first saves the reference of this node in a
variable, before changing the links (and effectively deleting it).
Doubly Linked List in Java
It says at the
top:
null
null 1 1 2 3
6 1 4
A (tailed) linked list of these nodes (i.e., a tailed doubly linked list),
looks like above
front tail
When there is only one node in this liked list, ‘front’ = ‘tail’, and they
reference the only node in the list. Also, both the next and the ‘prev’
1 null of this node, are null (see the figure on the left).
null
6
Note, when there is no node in the list, we have front = null, as well
as prev = null
(tailed) Doubly Linked List –
removing Last item (the general
case)
front tai
l
null
null 1 1 2 3
6 1 4
Note, we can now efficiently remove the last item (the tail) from a
(doubly) linked list.
public int removeLast()
{
int retVal = tail.val; Where would this code throw an exception if
there is only one node in the linked list?
Node nd = tail.prev; //It is the node storing ‘2’ in the
figure above Also, if there is only one node in the linked
nd.next = null; list, what else would you have to change
tail = nd; other than ‘tail’?
return retVal;
}
(tailed) Doubly Linked List – removing last
item (the general case)
(0) (1) Node nd =
front tai front tail.prev; tai
l l
null null
null 1 1 2 3 null 1 1 2 3
6 1 4 6 1 4
nd
We will give one quick example of it, and skip the rest of the details: When you delete a node nd, you
need to change the ‘next’ of the node before ‘nd’, and you need to change the ‘prev’ of the node after
nd.
front tai
l
(2)A singly linked list with tail can, in addition to above, add items at the tail in O(1) time
(3)A doubly linked list with tail can, in addition to above, remove items at the tail in O(1)
time
If one decides to implement Stack using Linked List, then (1) above is good enough. But
treat the front of the list as the ‘top’ of the stack, if we want the push/pop operations to
be O(1)
If one decides to implement the queue using Linked List, then (2) above is good enough.
But we should add at the tail, and remove from the front, if we want to enqueue and
dequeue in O(1) time.
Java Linked List is a tailed doubly linked list, so that it can also remove from the tail in
O(1) time. (But a doubly linked list needs another ‘prev’ reference variable.
None of the linked list can return the element stored at an arbitrary index in O(1) time.
Other varieties
There are other varieties too. What we have seen are:
Another variety is, where the ‘next’ reference of the tail goes back to the head. Of
course, you can make the following varieties now.
3. Circular linked list (Tailed and not tailed).
4. Circular doubly linked list
At any rate, the main varieties are singly and doubly linked lists (tailed and non-tailed). It’s
not about what you can make, as one can make any crazy variety ( You can make any crazy
variety – say there is a third reference, ‘next’, ‘prev’, and ‘fourAgo’, the last one pointing to the node 4
spots earlier). It is about what is useful for some given application ( Although crazy varieties
have a great use in quizzes and homeworks).