## Linked lists

- Pros and cons of arrays
- Benefits
- They are simple to understand
- They also keep things in a definite order, if we need that
- They allow
**random access**- “get the*n*th item” is easy

- When we ran out of space in the array implementation of
`Bag`

, what did we have to do?- Make a new, bigger array
- Copy the old contents into it
- Get rid of the old array (Java does this for us)

- What if the user had added 10,000 items to the
`Bag`

, then removed them all?- The array would stay huge
- Maybe this is good, in case they add 10,000 items again…
- …Or maybe this is a waste of space

- What if, unlike a
`Bag`

, we**cared**about the order of the items?- We
*would*have to shift all the items down one at a time if we removed one at the beginning

- We

- Benefits
- Rulers vs. Chains
- You can think of an array as a (really long) ruler
- It has regularly-spaced numbered markings on it
- You can tie an item on at each of these markings
- You know exactly where to go if you want “the item at the
*n*th marking” - But the ruler has a fixed size
- (Let’s assume you can’t
*cut*the ruler :P)

- (Let’s assume you can’t

- What if we instead used a chain?
- The chain can have as few or as many links as you want
- You can tie an item on each link
- Let’s say each link has a hole in it so you can hook and unhook the links
- That way, you can add or remove a link anywhere
- Adding or removing a link to the ends is really easy
- Adding or removing one in the middle is trickier, but still pretty easy
- You could even split one chain into two or more chains

- But the links are not numbered, so if you want to get the
*n*th item…**You have to start at one end and count***n*links

- You can think of an array as a (really long) ruler
- Linked lists
- A
**linked list**is a data structure where each item is held by a link in a chain - We call each link a
**node**- It’s just tradition.

- A node contains two things:
- The value it holds
**A link to the next node in the chain**

- To keep track of a whole linked list…
- We only need to hold onto the
**first node** - Just like a chain - you only have to hold one end, and the chain hangs from it

- We only need to hold onto the
- But if each node only links to the
*next*node…- Can you go backwards?
- No.
- There
*is*a variant of linked lists where each node keeps links in*both*directions - But that’s kind of a pain in the ass and not always worth it

- A

## Implementing a `Bag`

using a linked list in Java

- We’re gonna have a class called
`LinkedBag`

- And
*inside*that class, we can declare the`Node`

class!- This is a
**nested class** - We can either declare it as a
`static class`

or not - In this case we don’t need the extra features a non-static class has
- So we’ll just use
`private static class Node`

- It works just like any other class, but it can only be seen and used by
`LinkedBag`

- This is a
- Keeping track of the list and size
- We only need a reference to the
**head**of the list - the first link in the chain. - If the list is empty, then
`_head == null`

. - We’ll also keep track of the size so we don’t have to count nodes every time.

- We only need a reference to the
- Adding an item
- Where’s the easiest place to add a node?
- The beginning.

- There are two cases to consider: when
`_head == null`

and when`_head != null`

- But really, the two cases are the same:
- Make a new node which points to the old head (which might be
`null`

) - Make that node the new head
- Increase the size by 1

- Make a new node which points to the old head (which might be

- Where’s the easiest place to add a node?
- Removing an arbitrary item
- Again, where’s the easiest place to remove from? The beginning.
- Again, two cases to consider, but the
`_head == null`

case is easy: just return`null`

. - Otherwise:
- Get the item from the old head
- Make the old head’s next node the new head (which might be
`null`

) - Decrease the size by 1

- Looking for an item
- With the array-based
`Bag`

s, how did we find an item?- By
**iterating**over the array one item at a time. - Either we found it, or we got to the end of the array and didn’t find it.

- By
- We do the same thing here.
- We start at the head…
- And follow the links until we find the item or we run out of nodes.

- With the array-based

## Algorithm Analysis

- Every solution to a problem has two costs:
**time**and**space**- Time is how many
*steps we take to get the end* - Space is how many
*things we have to remember at once*- (We won’t really get into space analysis in this class)
- (But we can often trade time for space and vice versa)

- Time is how many
- Computers get faster every year…
- But algorithm analysis is about something deeper.
- Two solutions to the same problem can take
**drastically different amounts of time.** - And it’s not because of the computer they’re running on.

- An example:
**summing integers from 1 to n**- Say
`sum(n) = 1 + 2 + 3 + ... + (n-1) + n`

- So
`sum(1) == 1, sum(2) == 3, sum(3) == 6`

etc.

- So
- Three algorithms to do this:
- A.
`for(i = 1 to n) sum = sum + i`

- B.
`for(i = 1 to n) { for(j = 1 to i) sum = sum + 1 }`

- C.
`sum = n * (n+1) / 2`

- A.
- Intuitively, what order do these come in, from fastest to slowest?
- C, A, B.
- But
*why?* - It has to do with the
*number of repeated steps.*

- Let’s count the number of additions, multiplications, and divisions in each
- A.
`n`

additions, nothing else. Total =`n`

operations - C. 1 addition, 1 multiplication, 1 division. Total = 3 operations.
- B…
- This one is trickier.
- How many times does the outer loop (
`i`

) run? - How many times does the inner loop (
`j`

) run for each of those? - There’s 1 addition inside the inner loop.
- The first outer loop we do 1 iteration of the inner loop (1 addition).
- The second outer loop, we do 2 iterations of the inner loop (2 additions).
- The third, 3; the fourth, 4… hey, what’s this pattern?
- It’s
`n * (n + 1) / 2`

additions!- right? we’re adding 1 each time, so to get to
`sum`

, we have to do`sum`

additions.

- right? we’re adding 1 each time, so to get to
- In total, we do
`(n^2 + n) / 2`

operations.

- A.
- How well do these perform for various values of
`n`

?- A is a linear function of
`n`

. - C is a constant value (3) - it doesn’t depend on
`n`

at all. - B is a
*quadratic*function of`n`

. **As n grows larger, C always takes the same amount of time; A gets worse; and B gets***way*worse- Like,
*mathematically provably*worse.

- Like,

- A is a linear function of
**This is algorithm analysis:**focusing on the trends of time (or space)**as you increase the size of the input.**- Next years computers might be twice as fast, but a bad algorithm is always bad.

- Say

## Big-O notation (the letter, not the digit)

- If we have a polynomial…
- What does its graph look like?
- It kinda wiggles around at x = 0

- But in the
**long run…**- Doesn’t matter what order polynomial it is, it just
**goes up.** - The higher the order,
**the faster it goes up.** - Is there much of a difference between
`n^2`

and`n^2 + n`

?- Lower-order terms don’t matter.

- How about
`n^2`

and`2n^2`

?- Multiplicative constants don’t matter.

- Doesn’t matter what order polynomial it is, it just

- What does its graph look like?
- Big-O notation
- If you have an algorithm whose behavior is characterized by
`f(n)`

… - Then we say it’s
*bounded above*by a function`g(n)`

if:`g(n) >= f(n)`

“in the long run.”

- The proper formal definition of “in the long run” is
`c*g(n) >= f(n) for all n >= n_0`

`c`

is some positive constant, and`n_0`

is some positive integer.

- That
`n_0`

bit has to do with the sort of “break even” point some functions have- Consider
`n`

vs`n^2`

- It’s possible to have an
`n^2`

function that is*less than*`n`

… for a while. - But at some point, they
*intersect.*That intersection point is`n_0`

. - For every
`n >= n_0`

,`n^2`

is larger.

- Consider
- We say that
**an algorithm is O(something)**if “something” bounds its runtime from above.

- If you have an algorithm whose behavior is characterized by
- Complexities we already encountered
`O(1)`

is**constant time.**- No matter what the input is, it always takes the same amount of time.

`O(n)`

is**linear time.**- A singly-nested loop is
*usually*this.

- A singly-nested loop is
`O(n^2)`

is**quadratic time.**- A doubly-nested loop is
*often*this.

- A doubly-nested loop is

## Analyzing some things

- Sequential search through an array or linked list
- For a list of length
`n`

, what is the**worst case**?- We look through all
`n`

items and don’t find it. - Therefore, it’s linear time –
`O(n)`

.

- We look through all
- What about the
?*best*case- We find the item at the very beginning.
- Therefore, it’s constant time.
- We actually could write this best-case time as
`Ω(1)`

- We actually could write this best-case time as

- What about the
?*average*case- ??????
- Average what?
- To say what kind of average, we have to have a
**probability distribution**. - Let’s say it’s equally likely to find the item anywhere in the list.
- So we’d add up all the possibilities, and divide by
`n`

to get the average.- In this case, it’s
`((n^2 + n) / 2) / n`

=`n`

. It’s`O(n)`

again.

- In this case, it’s
- Intuitively, this makes sense - chances are, it’s gonna be somewhere in the middle.

- For a list of length
- Let’s analyze add(), remove(), contains(E), and remove(E) for the Array and Linked
`Bag`

s.- As it turns out,
*both*implementations are as follows:- add() is
`O(1)`

- remove() is
`O(1)`

- contains(E) is
`O(n)`

- remove(E) is
`O(n)`

- add() is

- As it turns out,