Recursion and Linked Lists.

Download Report

Transcript Recursion and Linked Lists.

CS503: Fifth Lecture, Fall 2008
Recursion and Linked Lists.
Michael Barnathan
Lab 1 Grades
Review: Lab 1
• There were a number of mistakes here.
• Most weren’t algorithmic, but were with Java.
–
–
–
–
–
Modifying the EmployeeLoader class.
Not invoking nextEmployee() properly.
Putting main() inside of the Employee constructor.
All sorts of bizarre Vector manipulation problems.
Very large try / catch blocks.
• Code in a try block executes slower. Make them as small as you can get away
with. If you exit after an exception, use “return”.
– Inverted compareTo() return values.
• Should be -1 is this < rhs, 1 if this > rhs, 0 if this == rhs.
– Summing the 20% and computing the ratio threw many off.
• If you don’t know Java well, it is imperative that you see me during
office hours or by appointment ASAP so we can work on it. Because
the theory is about to get much harder.
Java Issues and Tutoring
• If you are having trouble with Java, you can:
– See me during office hours.
– Set an appointment up (if office hours don’t work)
– Learn it on your own.
• Try setting yourself short (~30 minute) daily programming
exercises. They’ll help you familiarize yourself with Java
tremendously.
• The Deitel and Deitel book on Java is fairly good. So is the text for
the course (though more advanced than D&D).
– Seek tutoring.
• The department offers Java tutoring at 11:30 AM on Wednesdays
in the Wireless Lab.
– Retake CS176/501B next semester.
• It was previously taught in C++; now it’s taught in Java.
Minor conventions…
• Instead of saying “var = var + 5”, you can just write “var += 5”. It
runs slightly faster too.
• Same thing with -=, *=, /=, and others…
• Try blocks should contain as little code as possible (exception:
enclose loops in a try block; don’t enclose the try block in a loop).
– The reason is simple: checking for exceptions has overhead. Code in a
try block runs slower.
• If you catch an exception, your program execution will continue
from the catch.
– Make sure your program’s state will be consistent.
• (e.g., if you just caught a FileNotFoundException, make sure you don’t try to
read from the file… return in the catch block).
– Don’t use this as a glorified goto. Exceptions are not intended to be
used as flow control.
Calling nextEmployee()
• Remember, only static methods are called with a
class name:
– Employee e = EmployeeLoader.nextEmployee();
• All others are called through an object:
– EmployeeLoader el = new
EmployeeLoader(“Employee.csv”);
– Employee next_emp = el.nextEmployee();
• nextEmployee() is not static, so it is called using
an object: el.
Calling nextEmployee() in a loop.
•
•
•
nextEmployee() returns the next employee in the file and advances the file. It has side effects.
If you call it but don’t store the result, the employee you just read is gone.
So if you do this:
while (el.nextEmployee() != null)
v.add(el.nextEmployee());
•
You’ll only read every other employee.
–
Also, you may end up adding a null at the end.
• The correct thing to do in these situations is store the result:
Employee nextemp;
while ((nextemp = el.nextEmployee()) != null)
v.add(nextemp);
If you don’t like the idea of chaining operations like that, you can also write:
Employee nextemp = el.nextEmployee();
while (nextemp != null) {
v.add(nextemp);
nextemp = el.nextEmployee();
}
Vectors
• Three steps involved with the vector:
– Initialize:
• Vector<Employee> v = new Vector<Employee>();
– Add:
while ((nextemp = el.nextEmployee()) != null)
v.add(nextemp);
– Retrieve:
for (int i = 0; i < v.size(); i++)
salarytotal += v.get(i).salary;
• Common mistakes:
– Initializing the Vector in a loop: while (…) { v = new Vector(); v.add(nextemp); }
• Each time you initialize it with new, you empty the vector out.
– Changing types: double d = v.get(i); (put Employees in, get Employees out).
– Trying to get element v.size(): remember, arrays run from 0 to size-1.
compareTo
• To sort in Java, your class needs to implement interface Comparable.
• Comparable is a generic interface. You can specify what to compare it to.
– In this case, we want to compare Employees with other Employees.
– So implement Comparable<Employee>.
• Comparable makes us write compareTo(Employee other).
• Remember, compareTo always returns:
– -1 if this < other
– 1 if this > other
– 0 otherwise.
• If you write it any other way, you are going to alter the sorting order.
– Using 1 if this < other and -1 if this > other will sort in descending order.
– Java usually sorts in ascending order.
– The instructions I gave you assumed that the data was sorted in ascending
order. If you modified the sorting order, you’ll need to modify the instructions.
Finding the top 20%
• Compute array size * 0.80, call it top20start.
• Initialize counters totalsum and topsum to 0.
• Run through the array (backwards or forwards, it
doesn’t matter) and:
– Sum up all elements in totalsum.
– if (index > top20start), add v.get(index) to topsum.
• Now we know how much the total income is and what
proportion of it the top 20% makes.
• We do not average here. We simply divide:
– fraction_of_wealth = topsum / totalsum.
– Question: if this assignment were out of 30 points, how
would you find your grade out of 100? Same principle.
Writing good constructors: this
• Every nonstatic method has a special variable called “this”.
• “this” refers to the current object; the one the method is invoked
on.
• You can also disambiguate variable and field names using this:
– Employee(String name, String title, String city, double salary) {
•
•
•
•
this.name = name;
this.title = title;
this.city = city;
this.salary = salary;
– }
• Constructors have it too. this, like super, can call one constructor
within another:
– Employee() { this(“”, “”, “”, 0.00); }
Lab 1 Answer
• The highest 20% of wages (>$138,000) constituted approximately 30% of the
total.
– So salary does not obey the 80/20 rule.
• 80% of income is held by 69.23% of the population.
– That is, everyone making above $86,752 per year (good pay for the 30th percentile!)
100%
Histogram
350
120.00%
300
100.00%
80%
80.00%
200
60%
60.00%
150
40.00%
100
20.00%
50
0
$138,000
$86,752
70%
50%
Frequency
Cumulative %
0.00%
1
30934.71186
61868.42373
92802.13559
123735.8475
154669.5593
185603.2712
216536.9831
247470.6949
Frequency
250
90%
Bin
“Sigmoid” (S shaped) curve.
40%
30.77%
30.12%
30%
20%
10%
0%
$0
$50,000
$100,000
$150,000
Wealth Controlled (for salary>x)
$200,000
$250,000
Salary Percentile
$300,000
Thoughts
• If salaries don’t follow the 80/20 rule but wealth does, it
must have something to do with expenses.
– Profit = Revenue - Cost, after all.
– It would be interesting to compare wealth to household size,
home value, etc.
• Most salaries in this dataset fell between $75,000 and
$150,000.
– Few people made more.
– Few people made less.
• The histogram looks like a bell (also called Gaussian or
normal) curve, but with a skew towards higher salaries.
– The skew may indicate a high cost of living, which shifts all
salaries upwards.
Sorting Behaviors
• The program using Collections.sort executed in approximately 0.6 seconds
on average.
• Bubble Sort -> approximately 5.4 seconds.
• Insertion Sort -> 3.5 seconds.
• Selection Sort -> 4.3 seconds.
• Some found that selection sort was faster (in which case the times were
always very close) but most found insertion sort faster.
– This could be due to caching behavior.
– Insertion sort is the faster sort in theory.
• But insertion, selection, and bubble sort were all pretty close.
– They only differed by a constant factor.
• Merge sort blew them out of the water.
– It has a better asymptotic complexity.
• This is why we focus on asymptotic speedups when optimizing programs.
Lab Review
• Questions?
• As you can see, not asking questions can land
you in messy situations!
Here’s what we’ll be learning:
• Theory:
–
–
–
–
Recursion.
Recursive data structures.
The “Divide and Conquer” paradigm.
Memoization (as in “memo” + “ization” – there’s no “r”).
• Data Structures:
– Linked Lists.
• We are going to keep coming back to recursion
throughout the semester.
– But it should be easier for you each time we cover it.
– We’ll stop covering it when you’re sufficiently familiar with it.
Recursion
• A function is recursive if it calls itself as a step in solving a
problem.
– Why would we want to do this?
• A data structure is recursive if it can be defined in terms of
a smaller version of itself.
• Recursion is used when the problem can be broken down
into smaller instances of the same problem.
– Or “subproblems”.
– These subproblems are more easily solved.
• Often by breaking them into even smaller subproblems.
• Of course, at some point, we have to stop.
• The solutions to these subproblems are then merged into a
solution for the whole problem.
Recursive Structures: Example.
• Without using a loop, how would I print an nelement array?
– Output the first element.
– Print the remaining n-1 elements.
• A size-n array can be recursively defined as a
single element and an array of size n-1.
• This is called tail recursion. (“1 and the rest”)
Recursion: Why?
• Oftentimes, a problem can be solved by reducing it to a smaller
version of itself.
• If your solution is a function, you can call that function inside of
itself to solve the smaller problems.
• There are two components of a recursive solution:
– The reduction: this generates a solution to the larger problem through
solution of the smaller problems.
– The base case: this solves the problem outright when it’s “small
enough”.
• The key:
– You won’t be able to follow what is going on in every recursive call at
once; don’t think of it this way.
– Instead, think of a recursive function as a reduction procedure for
reducing a problem to a smaller instance of itself. Only when the
problem is very small do you attempt a direct solution.
Example:
•
Using a loop, print all of the numbers from 1 to n.
void printTo(int n) {
for (int i =1; i <= n; i++)
System.out.println(i);
}
•
Now do it using recursion.
– Stop and think before coding. Never rush into a recursive algorithm.
– Problem: Print every number from 1 to n.
– How can we break this problem up?
•
•
Print every number from 1 to n-1, then print n.
“Print n” is easy: System.out.println(n);
– How can we print every number from 1 to n-1?
– How about calling our solution with n-1?
•
This is going to call it with n-2…
–
–
•
This is going to call it with n-3...
» …
And print n-3.
And print n-2.
– And print n-1.
•
•
And print n.
And then we’re done! Right?
Where does it end!?
When does it end!?
• This is a fundamental question when writing
any recursive algorithm: where do we stop?
• The easiest place to stop is when n < 1.
– What do we do when n < 1?
– Well, we already outputted all of the numbers
from 1 to n. That was the goal.
– So we do nothing. We “return;”.
So here it is.
void printTo(int n) {
if (n < 1)
//Base case.
return;
else {
//Recursive step: 1 to n is 1 to n-1 and n.
printTo(n-1);
//Print 1 to n-1.
System.out.println(n);
//Print n.
}
}
• Question: What if we printed n before calling printTo?
• Don’t try to trace each call. Think about what this is doing.
The Reduction:
• We reduced the problem of printing 1 .. n to the problem of
printing 1 .. n-1 and printing n.
– A smaller instance of the same problem.
– So we solved it using the same function.
• Because we took one element off the end at a time, this is
called tail recursion.
• When it became “small enough” (n < 1), we solved it
directly.
– By stopping, since the output was already correct.
– If we kept going, we’d print 0, -1, …, which would be incorrect.
• That’s how recursion works.
– But this is a simple example.
• What’s the complexity of this algorithm?
Splitting into pieces.
• What we just saw was a “one piece” problem.
– We reduced the problem to one smaller problem.
• n -> (n-1).
– These are the easiest to solve.
– These solutions are usually linear.
• What if we split the problem into two smaller
problems at each step?
– Say we wanted to find the nth number in the
Fibonacci series.
Recursive Fibonacci:
• The Fibonacci series is a series in which each
term is the sum of the two previous terms.
– Recursive definition.
• The first two terms are 1.
– Base case (without this, we’d have a problem).
• It looks like this:
– 1, 1, 2, 3, 5, 8, 13, 21, 34, …
• And here’s its function:
– F(n) = F(n-1) + F(n-2)
– F(1) = F(2) = 1
Fibonacci: two-piece recursion.
• In order to find the nth Fibonacci number, we
need to simply add the n-1th and n-2th
Fibonacci numbers.
• Ok, so here’s a Java function fib(int n):
int fib(int n) {
}
• What would we write for the base case?
Fibonacci base case
int fib(int n) {
if (n <= 2)
return 1;
}
• That was simple. The recursive part?
Fibonacci base case
int fib(int n) {
if (n <= 2)
return 1;
return fib(n-1) + fib(n-2);
}
• Ok, that wasn’t too bad.
Solving multi-piece recursion.
• Often you get a direct solution from the recursive
call in one-piece recursion.
• But when you split into more than one piece, you
must often merge the solutions.
– In the case of Fibonacci, we did this by adding.
– Sometimes it will be more complex than this.
• Recursion usually looks like this:
– Call with smaller problems.
– Stop at the base case.
– Merge the subproblems as we go back up.
Divide and Conquer
• The practice of splitting algorithms up into
manageable pieces, solving the pieces, and
merging the solutions is called the “divide and
conquer” algorithm paradigm.
• It is a “top down” approach, in that you start
with something big and split it into smaller
pieces.
• Recursion isn’t necessary for these algorithms,
but it is often useful.
Memoization
•
•
•
•
What if we wanted to analyze fib?
Well, there’s one call to fib(n)…
Which makes two calls: fib(n-1) and fib(n-2)…
Which makes four calls: fib(n-2), fib(n-3),
fib(n-3), and fib(n-4)…
• Which makes eight…
• …
• Uh oh.
Why is it exponential?
• So this is O(2^n). Not good.
• And yet if you were to do it in a loop, you
could do it in linear time.
• But there’s something wrong with the way
we’re calling that causes an exponential result.
– There’s a lot of work being repeated.
• We repeat n-2 twice, n-3 four times, n-4 eight…
• In fact, this is the reason why it’s exponential!
• Fortunately, we can reduce this.
Memoization
• “Memoization” (no “r”) is the practice of caching
subproblem solutions in a table.
• (Come to think of it, they could have left the “r” in and
it would still be an accurate term).
• So when we find fib(5), we store the result in a table.
The next time it gets called, we just return the table
value instead of recomputing.
– So we save one call. What’s the big deal?
– fib(5) calls fib(4) and fib(3), fib(4) calls fib(3) and fib(2),
fib(3) calls…
– You actually just saved an exponential number of calls by
preventing fib(5) from running again.
Implementing Memoization
•
•
Use a member array for the table and wrap the actual work in a private function.
The fib() function looks up the answer in the table and calls that function if it’s not found.
class Fibonacci {
private static int[] fibresults = new int[n+1];
//Or use a Vector for dynamic sizing.
public int fib(int n) {
if (fibresults[n] <= 0)
//Not in table.
fibresults[n] = fib_r(n); //Fill it in.
return fibresults[n];
}
private int fib_r(int n) {
if (n <= 2)
return 1;
return fib(n-1) + fib(n-2);
}
}
//This does the real work.
//Base case.
//Note that we call “fib”, not “fib_r”.
The Gain
• When we store the results of that extra work,
this algorithm becomes linear.
• Finding F(50) without memoization takes 1
minute and 15 seconds on rockhopper.
– Look for F(60) and it will take hours.
• Finding F(50) with memoization takes 0.137s.
• The space cost of storing the table was linear.
– Because we’re storing for one variable, n.
Optimal Solution to Fibonacci
• The Fibonacci series has a closed form.
• That means we can find F(n) in constant time:
• F(n) = (phi^n - (-1/phi)^n) / sqrt(5).
• Phi is the Golden Ratio, approx. 1.618.
• Lesson: It pays to research the problem you’re
solving. You may save yourself a lot of work.
Recursion: Questions?
• This is an important topic to understand.
• Better to ask questions now, before we
introduce math to analyze these algorithms.
– Which will happen in 3 or 4 lectures.
– It’s important to intuitively understand a topic
before formally defining it using mathematics.
Linked Lists
• We said that arrays were:
– Contiguous.
– Homogenous.
– Random access.
• What if we drop the contiguousness?
• That is, adjacent elements in the list are no
longer adjacent in memory.
• It turns out that you lose random access, but
gain some other properties in return.
Linked Lists
• A linked list is simply a collection of elements in which
each points to the next element.
• For example:
1
2
3
• This is accomplished by storing a reference to the next
node in each node:
class Node<DataType> {
public DataType data;
public Node<DataType> next;
}
Variations
• Doubly linked lists contain pointers to the next and
previous nodes. The Java “LinkedList” class is doubly-linked.
– This class has a similar interface to Vector.
1
2
3
• Circularly linked lists are linked lists in which the last
element points back to the first:
– Seldom used, usually for “one every x” problems.
– To traverse one of these, stop when the next element is equal to
where you started.
1
0
2
3
CRUD: Linked Lists.
•
•
•
•
Insertion:
Access:
Updating an element:
Deleting an element:
• Search:
• Merge:
• Let’s start with access and insertion.
?
?
?
?
?
?
Node Access
• Elements are no longer contiguous in memory.
• We can no longer jump to the ith element.
• Now we have to start at the beginning and
follow the reference to the next node i times.
• Therefore, access is linear.
• This is called sequential access.
– Because every node must be visited in sequence.
Access element 3:
1
2
3
Node Insertion
• To insert into a list, all we need to do is change
the “next” pointer of the node before it and
point the new node to the one after.
• This is a constant-time operation (provided
we’re already in position to insert).
• Example:
Insert “5” after “1”:
1
5
2
3
Node1.next = new Node(5, Node1.next);
Merging Two Lists
• Linked lists have the unique property of
permitting merges to be carried out in
constant time (if you store the last node).
• In an array, you’d need to allocate a large array
and copy each of the two arrays to it.
• In a list, you simply change the pointer before
the target and the last pointer in the 2nd list.
• Example: 1
2
3
1
2
Merge at “2”:
6
5
4
6
5
3
4
CRUD: Linked Lists.
•
•
•
•
Insertion:
Access:
Updating an element:
Deleting an element:
• Search:
• Merge:
O(1)
O(N)
O(1)
?
O(N).
O(1).
• Binary search will not work on sequential access data structures.
– Moving around the array to find the middle is O(n), so we may as well
use linear search.
• Updating a node is as simple as changing its value.
• That leaves deletion.
Deletion
•
•
•
•
•
Insertion in reverse.
Easier to do in a doubly-linked list.
Store the next node from the target.
Remove the target node.
Set previous node’s next to the stored next.
Delete “5”:
1
5
2
3
CRUD: Linked Lists.
•
•
•
•
Insertion:
Access:
Updating an element:
Deleting an element:
• Search:
• Merge:
O(1)
O(N)
O(1)
O(1)
O(N).
O(1).
• Dynamically sized by nature.
– Just stick a new node at the end.
• Modifications are fast, but node access is the killer.
– And you need to access the nodes before performing other operations on
them.
• Three main uses:
– When search/access is not very important (e.g. logs, backups).
– When you’re merging and deleting a lot.
– When you need to iterate through the list sequentially anyway.
Les Adieux, L’Absence, Le Retour
• That was our first lecture on recursion.
• There will be others - it’s an important topic.
• The lesson:
– Self-similarity is found everywhere in nature:
trees, landscapes, rivers, and even organs exhibit
it. Recursion is not a primary construct for arriving
at solutions, but a method for analyzing these
natural patterns.
• Next class: Linked Lists 2, Stacks, and Queues.
• Begin thinking about project topics.