class: left, middle, title-slide # CSCI-UA 102 ## Data Structures
## Lists (Part 3) .author[ Instructor: Uday Kusupati
] .license[ Copyright 2020 Joanna Klukowska. Unless noted otherwise all content is released under a
[Creative Commons Attribution-ShareAlike 4.0 International License](https://creativecommons.org/licenses/by-sa/4.0/)] --- layout:true template: default name: section class: inverse, middle, center --- layout:true template: default name: poll class: inverse, full-height, center, middle --- layout:true template: default name: breakout class: breakout, middle --- layout:true template:default name:slide class: slide .bottom-left[© Joanna Klukowska. CC-BY-SA.] --- template: section ## Removing From a Linked List --- ## Removing From a Linked List In order to remove an element from a list, we first need to determine if it is actually stored in the list. -- As with adding to the list, we will need to consider a few possible scenarios: - removing an element that is stored in the first node - removing an element that is stored in the last node - removing an element that is store in a node somewhere in between -- The final algorithm has to work if we call it on an empty list, one-elements list or a list with many nodes. --- name: remove-front ## Removing from the Front of a List In these steps we will assume that there is at least one node, so that we have something to remove. --- template: remove-front .center[
] --- template: remove-front .center[
] .center80[ `head = ???` ] --- template: remove-front .center[
] .center80[ `head = head.next` ] --- template: remove-front .center[
] .center80[ `head = head.next` Do we need to do anything to get rid of the actual node that is no longer part of the list? ] --- template: remove-front .center[
] .center80[ `head = head.next` Do we need to do anything to get rid of the actual node that is no longer part of the list? - this depends on the programming language - in Java, as long as there are no more references to a given object, the Java garbage collection will take care of releasing the memory (i.e., marking it available for reuse) ] --- template: remove-front .center[
] .center80[ `head = head.next` Would this work, if we had a list with one element? ] -- .center80[ - almost, - if we removed the very last node, `head` is now set to `null`, but `tail` might still be pointing to the removed node ] -- .center80[ ``` if (head == null ) tail = null; ``` ] --- name: remove-back ## Removing the Last Node In these steps we will assume that there is at least one node, so that we have something to remove. --- template: remove-back .center[
] --- template: remove-back .center[
] .center80[ In order to remove the last node, we need to have a reference to the node right before. ] --- template: remove-back .center[
] .center80[ In order to remove the last node, we need to have a reference to the node right before. That's because we need to change where the `next` reference in that _one before_ node points to: `current.next = ???` ] --- template: remove-back .center[
] .center80[ In order to remove the last node, we need to have a reference to the node right before. That's because we need to change where the `next` reference in that _one before_ node points to: `current.next = ???` ] --- template: remove-back .center[
] .center80[ In order to remove the last node, we need to have a reference to the node right before. That's because we need to change where the `next` reference in that _one before_ node points to: `current.next = null` ] -- .center80[ Is this all we need to do? Or are there more steps needed? ] --- template: remove-back .center[
] .center80[ In order to remove the last node, we need to have a reference to the node right before. That's because we need to change where the `next` reference in that _one before_ node points to: `current.next = null` ] .center80[ Is this all we need to do? Or are there more steps needed? - the `tail` reference is no longer pointing to the last node, we need to fix this ] --- template: remove-back .center[
] .center80[ In order to remove the last node, we need to have a reference to the node right before. That's because we need to change where the `next` reference in that _one before_ node points to: `current.next = null` ] .center80[ Is this all we need to do? Or are there more steps needed? - the `tail` reference is no longer pointing to the last node, we need to fix this `tail = current; ` ] --- template: remove-back .center[
] .center80[ In order to remove the last node, we need to have a reference to the node right before. That's because we need to change where the `next` reference in that _one before_ node points to: `current.next = null` ] .center80[ Is this all we need to do? Or are there more steps needed? - the `tail` reference is no longer pointing to the last node, we need to fix this `tail = current; ` ] --- template: remove-back .center[
] .center80[ Would this work, if we had a list with one element? ] -- .center80[ - no, because with one node, there is no node before the one that we are removing ] -- .center80[ - if the list has only one element, we need to set both `head` and `tail` to `null` ``` if (head.next == null ) { head = null; tail = null; } ``` ] --- name: remove-middle ## Removing a Node from Within a List In these steps we will assume that there is at least one node, so that we have something to remove. --- template: remove-middle .center[
] --- template: remove-middle .center[
] .center80[ Once again, in order to remove a node, we need to have a reference to the node right before. ] --- template: remove-middle .center[
] .center80[ Once again, in order to remove a node, we need to have a reference to the node right before. That's because we need to change where the `next` reference in that _one before_ node points to: `current.next = ???` ] -- .center80[ We want to have `current.next` store a memory address of the node after the one that we are trying to delete. Where is that address stored? ] --- template: remove-middle .center[
] .center80[ Once again, in order to remove a node, we need to have a reference to the node right before. That's because we need to change where the `next` reference in that _one before_ node points to: `current.next = ???` ] .center80[ We want to have `current.next` store a memory address of the node after the one that we are trying to delete. Where is that address stored? `current.next = current.next.next;` ] --- template: remove-middle .center[
] .center80[ Is this all we need to do? - pretty much - we won't worry about the case of a list with one element, since then it is really removing the first/last node ] --- template: remove-middle .center[
] .center80[ Java garbage collector will eventually reclaim the memory allocated for the node that was deleted. ] --- template:section # Performance Analysis --- ## What is the performance of the operations that we discussed? Assume we have __N__ elements in the list.
_Note that asymptotic analysis only makes sense when N is very large and as it is growing to infinity._ -- - add at a specified position -- __O(N)__ -- - adding to the front/back __O(1)__ -- - remove from a specified position -- __O(N)__ -- - remove from the front __O(1)__ -- - remove from the back -- __O(N)__ -- - find the position/index of an element -- __O(N)__ -- - get the number of elements in the list -- __O(1)__ -- - empty the list -- __O(1)__ --- template:section # Examples and Things to Think About --- ## Implementing `size()` and `clear()` Provide the implementation for these two methods from our list interface: ```java // returns the current number of elements in the list public int size(); // removes all elements from the list, so it is once again empty public void clear(); ``` ----- Now, let's assume that for some unknown reason the `LinkedList` class implementation does not have a variable that keeps track of the size of the list (this is not a wise decision, but we can do that for the sake of an exercise). Rewrite your `size()` method so that it can determine the number of elements in the list without using the `size` data field. --- ## Iterating through the list We talked about iterating through the list till the very end (or past the end) and about stopping at the last node. Write the code that would iterate through the list and stop at the second to last node. Make sure that your code works without crashing if the list is empty or if it has only one element. In such cases the iteration should never happen and `current` should be set to `null`. What application might this kind of iteration have? --- ## Implementing `get()` The only remaining unimplemented method from our list interface is the `get()` method: ```java // retrieves and returns an item from position pos // throws NoSuchElementException if pos < 0 or pos >= size public E get( int pos) throws NoSuchElementException; ``` Implement it. In what way is it similar to the `find()` method? --- ## Alternative `find()` The `find()` in our interface returns the index of the element. There is often similar methods called `contains` that determines if the element is in the list or not and returns `true` or `false` accordingly. Implement such a method. --- ## Putting it all together for `remove()` We talked about different scenarios for removing an element from a list. Use these code fragments and ideas to actually implement the `remove()` method that was defined in our interface: ```Java // removes and returns an element at position pos, shifts elements starting // at pos+1 by one to the left (lower position values) // throws NoSuchElementException if pos < 0 or pos >= size public E remove(int pos) throws NoSuchElementException; ``` Assume that there is a `size` data field that keeps track of the exact number of elements in the list. And, of course, the `LinkedList` class has data fields that store references to the first node and to the last node: `head` and `tail`. Make sure that your function works when the list is empty, when it has only one element, and when it has many elements. --- ## Adding Numbers Imagine that we are working on a program that uses short linked lists to represent positive integers. In this representation, the 1's digit is stored in the first node, the 10's digit is stored in the second node, and so on. So a numbers 1,673 would be represented by `3 -> 7 -> 6 -> 1` Write a function that given two such numbers, calculates and returns their sum (as a linked list). You do not need to implement a list itself, but you won't be really be able to test your add function without at least a minimal class to represent these numbers. --- ## `ArrayList
` as an Example of Array Based Implementation of a List Instead of creating our own implementation that is based on an array, we will look at the existing one implemented in Java: `ArrayList
` class. -- - What makes this approach different from a linked lists approach? - the data is stored in an array instead of in nodes -- - `ArrayList
` = an array (that can store any type of data) + operations that make everything easy -- .center80[ .box[ Where did the pirate store his loot? - In an ArrayList, because ArrayLists can grow! .right[Dana L., 2018] ]] -- - So how do they grow? And can we make it efficient? --- template: section # Growing the Array --- name: growing-array ## Growing the Array Adding to the end of an array is O(1), except for when we need to add to a completely full array when it is O(N). -- .center[
] --- template: growing-array .center[
] - creating a new array and copying the data is fairly easy, but how much more room should we allocate? - if it is too large, then we have an array that is much too big for our data - if it is too little, then we might need to do this again, and again, and again --- template: growing-array ```java public boolean add(E element) { if (size >= array.length) { // make a bigger array and copy over the elements E[] bigger = (E[]) new Object[array.length * 2]; System.arraycopy(array, 0, bigger, 0, array.length); array = bigger; } array[size] = element; size++; return true; } ``` -- - where does the O(N) time come from in the above function -- - `arraycopy()` is linear, it runs in time proportional to the size of the array - but it is in practice faster than executing: ```java for (int i = 0; i < size; i++) bigger[i] = array[i]; ``` -- - so what is the performance of this function overall (not just sometimes O(1) and sometimes O(N))? --- ## Growing the Array - Amortized Analysis If the initial size of the array is N, then - .small[adding first element takes O(1)] -- - .small[adding second element takes O(1)] -- - .small[...] -- - .small[adding $N^{th}$ element takes O(1)] -- - .small[adding $N+1^{st}$ element takes O(N)] -- Intuitively, we have N \* O(1) + 1 \* O(N) to add N+1 elements -> this is still a constant time for each operation (almost). -- In __amortized analysis__ we are interested in the performance of a given operation on average - when we repeat the operation many time and calculate how much time it took divided by the number of repeats. (See [Amortized Analysis](https://en.wikipedia.org/wiki/Amortized_analysis) on Wikipedia for more formal details.) -- .center80[.yellowbox[ The add at the end (append) operation for an ArrayList has an amortized time performance of O(1) as long as the array size in increased by a multiplicative factor, not an additive factor. ]] -- - this means that we need to multiply the current size of an array by a constant to get the size of the bigger array: if N starts as 10, and the multiplicative constant is 2, the sizes will be: 10, 20, 40, 80, 160, ... -- - __not__ add some constant to the current size: if N starts at 10, and the additive constant is 2, the sizes would be: 10, 12, 14, 16, 18, ... --- template:section # Adding and Removing --- name: adding ## Adding at an Arbitrary Position If we want to add an element to an arbitrary position in the list, we will need to shift the values that are there to the higher indexes to make room for it. -- .center[
] --- template: adding .center[
] This operation is O(N) because we need to shift the elements by one position to the right (higher indexes). --- name: removing ## Removing from an Arbitrary Position If we want to remove an element from an arbitrary position in the list, we will need to shift the values in the higher indexes to fill in the _whole_ that remains after removing an element. -- .center[
] --- template: removing .center[
] This operation is O(N) because we need to shift the elements by one position to the left (lower indexes). --- template: section # Comparing `ArrayList
` Objects for Equality --- ## `equals` method specification The `equals` method in the `ArrayList
` class is inherited from the `AbstractList
` class. ---- -- `public boolean equals​(Object o)` Compares the specified object with this list for equality. Returns true if and only if the specified object is also a list, both lists have the same size, and all corresponding pairs of elements in the two lists are equal. (Two elements `e1` and `e2` are equal if (`e1==null ? e2==null : e1.equals(e2))`.) In other words, two lists are defined to be equal if they contain the same elements in the same order. __Implementation Requirements__: This implementation first checks if the specified object is this list. If so, it returns `true`; if not, it checks if the specified object is a list. If not, it returns `false`; if so, it iterates over both lists, comparing corresponding pairs of elements. If any comparison returns `false`, this method returns `false`. If either iterator runs out of elements before the other it returns `false` (as the lists are of unequal length); otherwise it returns `true` when the iterations complete. __Parameters__: `o` - the object to be compared for equality with this list __Returns__: `true` if the specified object is equal to this list -- .box[ Note that this specification allows us to use `equals` to compare different implementations of a list, for example an `ArrayList` instance with a `LinkedList` instance. The only requirement is that they contain the same elements in the same order. ] --- ## `equals` method implementation This is the code from `AbstractList
` class. ```java public boolean equals(Object o) { if (o == this) //check if the specified object is this list return true; if (!(o instanceof List)) //check if the specified object is a list return false; //iterate over both lists, comparing corresponding pairs of elements ListIterator
e1 = listIterator(); ListIterator> e2 = ((List>) o).listIterator(); while (e1.hasNext() && e2.hasNext()) { E o1 = e1.next(); Object o2 = e2.next(); if (!(o1==null ? o2==null : o1.equals(o2))) return false; } return !(e1.hasNext() || e2.hasNext()); } ``` --- ## `equals` method alternative implementation This is an alternative implementation of the same method. ```java public boolean equals(Object o) { if (o == this) //check if the specified object is this list return true; if (!(o instanceof List)) //check if the specified object is a list return false; //check if lists are of the same length if (this.size() != ((List>) o).size() ) { return false; } //iterate over both lists, comparing corresponding pairs of elements for (int i = 0; i < this.size(); i++ ) { E o1 = this.get(i); Object o2 = ((List>) o).get(i); if (!(o1==null ? o2==null : o1.equals(o2))) return false; } return true; } ``` --