Uploaded by aliang06

CS61BL Summer 2021 Slides:Notes

advertisement
implements vs extends
When would you use one over the other? Here are some notes about each (non-exhaustive
list):
Interfaces (implements)
● Defines rules (methods) to be followed, doesn’t say how
● All variables and methods are public by default (Any variables are also static and final)
● Can define default methods
● One class can implement multiple interfaces
Classes (extends)
● Can define variables, methods, and constructors
● State-holding variables and methods are inherited when the class is being extended
● Can leave methods abstract
● One class can extend only one other class
When should we use them?
Then what’s the difference between classes (that can be abstract) and interfaces
(that can use default methods)?
●
Classes can pass on state-holding variables. Interfaces only have static + final
variables, so the only variables one can work with in an interface belong to a
class.
●
One class can implement many interfaces, but it can only extend at most one
class. Choose wisely!
●
Class inheritance is useful when your classes are really similar, and end up
sharing a lot of implementation. Interface inheritance is useful when you have
totally different ways of supporting the same behavior.
Interface vs. Implementation Inheritance
Interface Inheritance (a.k.a. what):
●
Allows you to generalize code in a powerful, simple way.
Implementation Inheritance (a.k.a. how):
●
Allows code-reuse: Subclasses can rely on superclasses or interfaces.
○ Gives another dimension of control to subclass designers: Can
decide whether or not to override default implementations.
Important: In both cases, we specify “is-a” relationships, not “has-a”.
●
●
Good: Dog implements Animal, LinkedListDeque implements Deque,
RotatingLinkedListDeque extends LinkedListDeque.
Bad: Cat implements Claw, Set implements SLList.
Overriding versus
Overloading
Method Overriding
If a “subclass” has a method with the exact same signature as in the
“superclass”, we say the subclass overrides the method.
public interface Deque<T> {
public void addLast(T item);
...
public class ArrayDeque<T> implements Deque<T>{
...
public void addLast(T item) {
...
ArrayDeque overrides addLast(T)
Method Overriding
public class Animal {
public void makeNoise() {
System.out.print(“hi!”);
}
}
public class Pig extends Animal {
public void makeNoise() {
System.out.print(“oink”);
}
}
A Pig might always makeNoise
a little differently than your
average animal.
makeNoise is overriden.
Method Overriding
public class Animal {
public void makeNoise() {
System.out.print(“hi!”);
}
}
But maybe Dogs say “hi” to
everyone, but when greeted with
another Dog, they react differently.
Now, is Dog overriding makeNoise?
public class Pig extends Animal {
public void makeNoise() {
System.out.print(“oink”);
}
}
public class Dog
extends Animal
{
public void makeNoise(Dog friend) {
System.out.print(“woof!”);
}
}
Method Overriding vs. Overloading
Methods with the same name but different signatures are overloaded.
● This applies to both from a subclass to a superclass, as well within one class.
public class Dog extends Animal {
public void makeNoise(Dog x)
}
makeNoise is overloaded
{…}
public class Math {
public int
abs(int a) {...}
public double abs(double a) {...}
}
abs is overloaded
Note: Adding the @Override Annotation
In 61BL, we’ll always mark every overriding method with the @Override annotation.
● Example: Mark ArrayDeque.java’s overriding methods with @Override.
● Even if you don’t write @Override, subclass still overrides the method if it’s written
correctly. It’s just an optional reminder that you’re overriding.
Why use @Override?
● Main reason: Protects against typos.
○ If you say @Override, but the method isn’t actually overriding anything, compile
error.
○ e.g. public void addLats(T item)
● Reminds programmer that method definition came from somewhere higher up in the
inheritance hierarchy.
public class ArrayDeque<T> implements Deque<T>{
...
@Override
public void addLast(T item) {
...
ArrayDeque
Deque
LinkedListDeque
Static vs Dynamic Types
and Casting
Is-A
Recall: Inheritance lets us define hierarchies.
●
●
●
A dog “is-a” canine.
A canine “is-a” carnivore.
A carnivore “is-an” animal.
animal
omnivore
herbivore
carnivore
feline
canine
dog
Is-A
So could we declare a variable like below:
canine A = new Dog();
animal
Or could a method with the signature
public void feed(carnivore c) {...}
omnivore
herbivore
carnivore
be called on a Dog instance?
canine
dog
feline
The Magic Box Analogy
I like to imagine Java as a big factory. When you declare a variable, you’re
setting aside a magic memory box. The boxes only hold what you say it can
hold.
●
If I have a box that can hold Fruit, it can certainly hold an Apple, right? This
is the beauty of inheritance.
Once the Java compiler puts the value inside the box, it forgets exactly what’s
in there. It just reads the label on the box–which is whatever you declared it
as.
●
●
This “label” that the compiler knows is the static type of a variable.
The type of whatever is actually inside the box is the dynamic type.
Static Type vs. Dynamic Type
Every variable in Java has a “compile-time type”, a.k.a. “static type”.
● This is the type specified at declaration. Never changes!
● This is the type checked during compilation.
Variables also have a “run-time type”, a.k.a. “dynamic type”.
● This is the type specified at instantiation (e.g. when using new).
● Equal to the type of the object being pointed at.
● This is the type we start with during runtime.
public static void main(String[] args) {
LivingThing lt1;
lt1 = new Fox();
Animal a1 = (Animal) lt1;
Fox h1 = new Fox();
lt1 = new Squid();
}
Static Type
Dynamic Type
LivingThing
lt1
LivingThing
null
Static Type vs. Dynamic Type
Every variable in Java has a “compile-time type”, a.k.a. “static type”.
● This is the type specified at declaration. Never changes!
● This is the type checked during compilation.
Variables also have a “run-time type”, a.k.a. “dynamic type”.
● This is the type specified at instantiation (e.g. when using new).
● Equal to the type of the object being pointed at.
● This is the type we start with during runtime.
public static void main(String[] args) {
LivingThing lt1;
lt1 = new Fox();
Animal a1 = (Animal) lt1;
Fox h1 = new Fox();
lt1 = new Squid();
}
Static Type
Dynamic Type
LivingThing
lt1
LivingThing
Fox
Static Type vs. Dynamic Type
Every variable in Java has a “compile-time type”, a.k.a. “static type”.
● This is the type specified at declaration. Never changes!
● This is the type checked during compilation.
Variables also have a “run-time type”, a.k.a. “dynamic type”.
● This is the type specified at instantiation (e.g. when using new).
● Equal to the type of the object being pointed at.
● This is the type we start with during runtime.
public static void main(String[] args) {
LivingThing lt1;
lt1 = new Fox();
Animal a1 = (Animal) lt1;
Fox h1 = new Fox();
lt1 = new Squid();
}
Static Type
Dynamic Type
LivingThing
lt1
LivingThing
Fox
Animal
Fox
Animal
a1
Static Type vs. Dynamic Type
Every variable in Java has a “compile-time type”, a.k.a. “static type”.
● This is the type specified at declaration. Never changes!
● This is the type checked during compilation.
Variables also have a “run-time type”, a.k.a. “dynamic type”.
● This is the type specified at instantiation (e.g. when using new).
● Equal to the type of the object being pointed at.
● This is the type we start with during runtime.
public static void main(String[] args) {
LivingThing lt1;
lt1 = new Fox();
Animal a1 = (Animal) lt1;
Fox h1 = new Fox();
lt1 = new Squid();
}
Static Type
Dynamic Type
LivingThing
lt1
LivingThing
Fox
Animal
Fox
Fox
Fox
Animal
a1
Fox
h1
Static Type vs. Dynamic Type
Every variable in Java has a “compile-time type”, a.k.a. “static type”.
● This is the type specified at declaration. Never changes!
● This is the type checked during compilation.
Variables also have a “run-time type”, a.k.a. “dynamic type”.
● This is the type specified at instantiation (e.g. when using new).
● Equal to the type of the object being pointed at.
● This is the type we start with during runtime.
public static void main(String[] args) {
LivingThing lt1;
lt1 = new Fox();
Animal a1 = (Animal) lt1;
Fox h1 = new Fox();
lt1 = new Squid();
}
Static Type
Dynamic Type
LivingThing
lt1
LivingThing
Squid
Animal
Fox
Fox
Fox
Animal
a1
Fox
h1
An interesting question...
What if we tried doing something like Animal a = new Car();?
Compile time checking
The Java compiler is like the diligent factory inspector, making sure that we only
put items in boxes that they can fit in.
●
The compiler also catches syntax errors or undefined references
When we make an assignment, var = value, the compiler asks, “Can a memory
box of var’s type hold something of value’s type?”.
Similarly, if we were to call a method, var1.myMethod(var2), the compiler asks,
“Can an item of var1’s type call a method with the name myMethod that could take
in an item of var2’s type?”. If it can, then we’ll give that method our seal of approval
and write it into our records.
Dynamic Method Selection For Overridden Methods
public class Animal {
public void makeNoise() {
System.out.print(“hi!”);
}
}
public class Pig extends Animal {
public void makeNoise() {
System.out.print(“oink”);
}
}
Animal p = new Pig();
p.makeNoise()
What happens here? Let’s ask…
● What is the static type of p?
● What is the dynamic type of p?
● Which type does the compiler see?
● What is the signature that the
compiler gives the seal of approval
to?
● What method gets run at runtime?
Dynamic Method Selection For Overridden Methods
Suppose we call a method of an object using a variable with:
●
●
compile-time type X
run-time type Y
Then if Y overrides the method in X, Y’s method is used instead.
●
This is known as “dynamic method selection”. Well, at least, at Berkeley!
Dynamic Method Selection Puzzle
Suppose we have classes defined below. Try to predict the results.
public interface Animal {
default void greet(Animal a) {
public class Dog implements Animal {
print("hello animal"); }
void sniff(Animal a) {
default void sniff(Animal a) {
print("dog sniff animal"); }
print("sniff animal"); }
default void flatter(Animal a) { void flatter(Dog a) {
print("u r cool dog"); }
print("u r cool animal"); }
}
}
Animal a = new Dog();
Dog d = new Dog();
a.greet(d);
a.sniff(d);
d.flatter(d);
a.flatter(d);
Dynamic Method Selection Puzzle
Suppose we have classes defined below. Try to predict the results.
public interface Animal {
default void greet(Animal a) {
public class Dog implements Animal {
print("hello animal"); }
void sniff(Animal a) {
default void sniff(Animal a) {
print("dog sniff animal"); }
print("sniff animal"); }
default void flatter(Animal a) { void flatter(Dog a) {
print("u r cool dog"); }
print("u r cool animal"); }
}
}
Animal a = new Dog();
Dog d = new Dog();
a.greet(d);
// "hello animal"
a.sniff(d);
d.flatter(d);
a.flatter(d);
Dynamic Method Selection Puzzle
Suppose we have classes defined below. Try to predict the results.
public interface Animal {
default void greet(Animal a) {
public class Dog implements Animal {
print("hello animal"); }
void sniff(Animal a) {
default void sniff(Animal a) {
print("dog sniff animal"); }
print("sniff animal"); }
default void flatter(Animal a) { void flatter(Dog a) {
print("u r cool dog"); }
print("u r cool animal"); }
}
}
Animal a = new Dog();
Dog d = new Dog();
a.greet(d);
// "hello animal"
a.sniff(d);
// "dog sniff animal"
d.flatter(d);
a.flatter(d);
Dynamic Method Selection Puzzle
Suppose we have classes defined below. Try to predict the results.
public interface Animal {
default void greet(Animal a) {
public class Dog implements Animal {
print("hello animal"); }
void sniff(Animal a) {
default void sniff(Animal a) {
print("dog sniff animal"); }
print("sniff animal"); }
default void flatter(Animal a) { void flatter(Dog a) {
print("u r cool dog"); }
print("u r cool animal"); }
}
}
Animal a = new Dog();
Dog d = new Dog();
a.greet(d);
// "hello animal"
a.sniff(d);
// "dog sniff animal"
d.flatter(d); // "u r cool dog"
a.flatter(d);
Dynamic Method Selection Puzzle
Suppose we have classes defined below. Try to predict the results.
public interface Animal {
default void greet(Animal a) {
public class Dog implements Animal {
print("hello animal"); }
void sniff(Animal a) {
default void sniff(Animal a) {
print("dog sniff animal"); }
print("sniff animal"); }
default void flatter(Animal a) { void flatter(Dog a) {
print("u r cool dog"); }
print("u r cool animal"); }
}
}
Animal a = new Dog();
Dog d = new Dog();
a.greet(d);
// "hello animal"
flatter is
a.sniff(d);
// "dog sniff animal"
overloaded, not d.flatter(d); // "u r cool dog"
overridden!
a.flatter(d); // “u r cool animal”
The Method Selection Algorithm
Consider the function call var1.func(var2), where var1 has static
type class1, and var2 has static type class2.
At compile time, the compiler verifies that class1 has a method named func
that can handle class2. It then records the signature of this method.
●
Note: If there are multiple methods that can handle class2, the compiler
records the “most specific” one. For example, if class2=Dog, and class1
has func(Dog) and func(Animal), it will record func(Dog).
At runtime, if var1’s dynamic type overrides the recorded signature, use the
overridden method. Otherwise, use class1’s version of the method.
Casting
Let’s say Dog also has a method, Bark(), that Animal does not have. Do the
two lines below work?
Animal A = new Dog()
A.bark()
Casting
Let’s say Dog also has a method, Bark(), that Animal does not have. Do the
two lines below work?
X
Animal A = new Dog()
A.bark()
Compiler says no! >:(
Casting
Java has a special syntax for specifying the compile-time type of any expression.
●
●
Put desired type in parenthesis before the expression.
Examples:
○
Compile-time type Animal:
○
Compile-time type Dog:
Animal fido = new Dog();
(Dog) fido;
Tells compiler to pretend it sees a particular type. Believe us, O might compiler!
Before and after: casting makeover
Fruit f = new Apple();
Apple a = f;
f.appleJuice();
Fruit f = new Apple();
Apple a = (Apple) f;
((Apple) f).appleJuice();
Lecture 3
CS 61BL Summer 2021
●
Announcements
July 8 2021
●
●
●
●
Midterm 1 today 6-8PM.
Alternate exams have been
scheduled via email.
Gitlet released!
No quiz tomorrow
Lab 9 tomorrow
Partner repo submissions
only
Last week recap
Last week...
●
●
●
●
Linked lists were our first deep dive into data structures.
○ Introduced in lecture, lab5 and lab6
○ Solidified in Proj1
We expanded from Linked Lists to consider the concept of a “list” in general.
○ Interfaces in lab7
○ Collections: list, set, map
○ Interface inheritance
Inheritance and dynamic method selection
○ Lab8
Discussion of error handling
○ What to do when something goes wrong or we don’t support some functionality?
○ Exception handling.
○ Example: Iterators and the NoSuchElementException
We’re ready to take on bigger projects!
Software Engineering
Motivation for Today
In some ways, we have misled you about what programming entails.
● 61A: Fill in the function.
● 61B(L): Implement the class according to our spec.
Always working at a “small scale” introduces habits that will cause you great
pain later.
In this lecture, we’ll try to give you a sense of how to deal with the “large scale”
● Projects 2 and 3 will give you a chance to encounter the issues yourself.
● We hope these lectures help with projects 2 and 3.
Credits
This lecture is a based on Josh Hug’s Spring 2019 SWE lectures.
His lectures were heavily inspired by “A Philosophy of Software Design” by
John Ousterhout.
● It’s cheap and very good!
● https://books.google.com/books?id=pD6-swEACAAJ&dq=philosophy+soft
ware+design&hl=en&sa=X&ved=0ahUKEwj9sZDmisvhAhXN6Z4KHcY6AYo
Q6AEIKjAA
● https://www.amazon.com/Philosophy-Software-Design-John-Ousterhout/
dp/1732102201
Complexity Defined
The Power of Software
Unlike other engineering disciplines, software is effectively unconstrained by
the laws of physics.
● Programming is an act of almost pure creativity!
The greatest limitation we face in building systems is being able to understand
what we’re building! Very unlike other disciplines, e.g.
● Chemical engineers have to worry about temperature.
● Material scientists have to worry about how brittle a material is.
● Civil engineers have to worry about the strength of concrete.
Complexity, the Enemy
Our greatest limitation is simply understanding the system we’re trying to
build!
As real programs are worked on, they gain more features and complexity.
● Over time, it becomes more difficult for programmers to understand all
the relevant pieces as they make future modifications.
Tools like IntelliJ, JUnit tests, the IntelliJ debugger, the visualizer all make it
easier to deal with complexity.
● But our most important goal is to keep our software simple.
Dealing with Complexity
There are two approaches to managing complexity:
● Making code simpler and more obvious.
○
●
Eliminating special cases, e.g. sentinel nodes.
Encapsulation into modules.
○
In a modular design, creators of one “module” can use other modules without knowing
how they work.
The Nature of Complexity
What is complexity exactly? Ousterhout defines it as:
● “Complexity is anything related to the structure of a software system that
makes it hard to understand and modify the system.”
Takes many forms:
● Understanding how the code works.
● The amount of time it takes to make small improvements.
● Finding what needs to be modified to make an improvement.
● Difficult to fix one bug without introducing another.
“If a software system is hard to understand and modify, then it is complicated.
If it is easy to understand and modify, then it is simple”.
Complexity
Note: Our usage of the term
“complex” in these lecture is not
synonymous with “large and
sophisticated”.
● It is possible for even short
programs to be complex.
Example from a former CS10
student.
What makes this complex?
Complexity and Importance
Complexity also depends on how often a piece of a system is modified.
● A system may have a few pieces that are highly complex, but if nobody
ever looks at that code, then the overall impact is minimal.
Ousterhout’s book gives a crude mathematical formulation:
● C = sum(c_p * t_p) for each part p.
○
○
c_p is the complexity of part p.
t_p is the time spent working on part p.
Symptoms and Causes of
Complexity
Symptoms of Complexity
Ousterhout describes three symptoms of complexity:
● Change amplification: A simple change requires modification in many
places.
● Cognitive load: How much you need to know in order to make a change.
○
●
Note: This is not the same as number of lines of code. Often MORE lines of code actually
makes code simpler, because it is more narrative.
Unknown unknowns: The worst type of complexity. This occurs when it’s
not even clear what you need to know in order to make modifications!
○
Common in large code bases.
Obvious Systems
In a good design, a system is ideally obvious.
In an obvious system, to make a change a developer can:
● Quickly understand how existing code works.
● Come up with a proposed change without doing too much thinking.
● Have a high confidence that the change should actually work, despite not
reading much code.
Complexity Comes Slowly
Every software system starts out beautiful, pure, and clean.
As they are built upon, they slowly twist into uglier and uglier shapes. This is
almost inevitable in real systems.
● Each complexity introduced is no big deal, but: “Complexity comes about
because hundreds or thousands of small dependences and obscurities
build up over time.”
● “Eventually, there are so many of these small issues that every possible
change is affected by several of them.”
● This incremental process is part of what makes controlling complexity so
hard, thus Ousterhout recommends a zero tolerance philosophy.
Strategic vs. Tactical
Programming
Tactical Programming
Much (or all) of the programming that you have done, Ousterhout would
describe as “tactical”.
● “Your main focus is to get something working, such as a new feature or
bug fix.” (or to get an autograder test to pass)
Tactical Programming
Much (or all) of the programming that you have done, Ousterhout would
describe as “tactical”.
● “Your main focus is to get something working, such as a new feature or
bug fix.” (or to get an autograder test to pass)
This may seem like a silly criticism. Clearly working code is good.
Tactical Programming
The problem with tactical programming:
● You don’t spend problem thinking about overall design.
● As a result, you introduce tons of little complexities, e.g. making two
copies of a method that do something similar.
● Each individual complexity seems reasonable, but eventually you start to
feel the weight.
○
○
Refactoring would fix the problem, but it would also take time, so you end up introducing
even more complexity to deal with the original ones.
Projects 2 and 3 will give you a chance to feel this!
The end result is misery.
Strategic Programming
“The first step towards becoming a good software designer is to realize that
working code isn’t enough.”
● “The most important thing is the long term structure of the system.”
● Adding complexities to achieve short term time gains is unacceptable.
Strategic programming requires lots of time investment.
● And as novice programmers, it’ll seem quite mysterious and hard. On
projects 2 and 3, try to plan ahead, but realize your system is very likely
going to be horrible looking when you’re done.
Suggestions for Strategic Programming
For each new class/task:
● Rather than implementing the first idea, try coming up with (and possibly
even partially implementing) a few different ideas.
● When you feel like you have found something that feels clean, then fully
implement that idea.
● In real systems: Try to imagine how things might need to be changed in
the future, and make sure your design can handle such changes.
Strategic Programming is Very Hard
No matter how careful you try to be, there will be mistakes in your design.
● Avoid the temptation to patch around these mistakes. Instead, fix the
design.
○
○
●
Example: Don’t add a bunch of special cases! Instead, make sure the system gracefully
handles the cases you didn’t think about.
Specific example: Adding sentinel nodes to SLLists.
Indeed, it is often impossible to design large software systems entirely in
advance.
Tactical vs. Strategic Programming Case Study
As a startup, Facebook embraced tactical programming.
● “Move fast and break things.”
● Common for new engineers to push changes to the live site within their
first week.
○
○
●
Facebook was very successful, but its codebase was a mess:
○
●
Very rapid development process in the short term.
Felt empowering!
“incomprehensible, unstable, few comments or tests, and painful to work with.”
Eventually, motto became “Move fast with stable infra.”
Note: Arguably Facebook’s general attitude has done great harm.
Tactical vs. Strategic Programming Case Study
By contrast Google and VMware are known as highly strategic organizations.
● “Both companies placed a heavy emphasis on high quality code and good
design.”
● “Both companies built sophisticated products that solved complex
problems with reliable software systems.”
● “The companies’ strong technical cultures became well known in Silicon
Valley. Few other companies could compete with them to hire the top
technical talent.”
Real world projects and companies succeed with either approach!
● … but Ousterhout says it’s probably more fun to work somewhere with a
nice code base.
Modular Design
Inspired partially by “A Philosophy of Software Design” chapters 4 and 5.
Hiding Complexity
One powerful tool for managing complexity is to design your system so that
programmer is only thinking about some of the complexity at once.
● By using helper methods and helper classes, you can hide complexity.
Modular Design
In an ideal world, each system would be broken down into modules, where
every module is totally independent.
● Here, “module” is an informal term referring to a class, a package, or
other unit of code.
● Not possible for modules to be entirely independent, because code from
each module has to call other modules.
○
e.g. need to know signature of methods to call them.
In modular design, our goal is to minimize dependencies between modules.
Interface vs. Implementation
As we’ve seen, there is an important distinction between Interface and
Implementation.
● Deque is an interface.
● LinkedListDeque, ArrayDeque, TreeDeque, etc. are implementations.
Deque
LinkedListDeque
ArrayDeque
TreeDeque
Interface vs. Implementation
Ousterhout: “The best modules are those whose interfaces are much simpler
than their implementation.” Why?
● A simple interface minimizes the complexity the module can cause
elsewhere. If you only have a getNext() method, that’s all someone can
do.
● If a module’s interface is simple, we can change an implementation of that
module without affecting the interface.
○
Silly example: If List had an arraySize method, this would mean you’d be stuck only
being able to build array based lists.
Interface
A Java interface has both a formal and an informal part:
● Formal: The list of method signatures.
● Informal: Rules for using the interface that are not enforced by the
compiler.
○
○
○
○
Example: If your code requires methods to be called in a particular order to work
properly, that is an informal part of the interface.
Example: If your add method throws an exception on null inputs, that is an informal part
of the interface.
Example: Runtime for a specific method, e.g. add in ArrayList.
Can only be specified in comments.
Be wary of any informal rules of your modules as you build projects 2 and 3.
Information Hiding
Ousterhout: “The best modules are those that provide powerful functionality
yet have simple interfaces. I use the term deep to describe such modules.”
The most important way to make your modules deep is to practice
“information hiding”.
● Embed knowledge and design decision in the module itself, without
exposing them to the outside world.
Reduces complexity in two ways:
● Simplifies interface.
● Makes it easier to modify the system.
Information Leakage
The opposite of information hiding is information leakage.
● Occurs when design decision is reflected in multiple modules.
○
○
Any change to one requires a change to all.
Information is embodied in two places, i.e. it has “leaked”.
Ousterhout:
● “Information leakage is one of the most important red flags in software
design.”
● “One of the best skills you can learn as a software designer is a high level
of sensitivity to information leakage.”
Summary and Suggestions
Some suggestions as you embark on future large scale projects:
● Build classes that provide functionality needed in many places in your
code.
● Create “deep modules”, e.g. classes with simple interfaces that do
complicated things.
● Be strategic, not tactical.
● Most importantly: Hide information from yourself when unneeded!
Hydration is key
AFAB: Drink ~ 11 cups a day
AMAB: Drink ~ 16 cups a day
Sources: cnn and harvard
TLDR: you are probably not drinking enough water.
Extra: Teamwork
Teamwork
This class is team based with teams of 2-3 students.
Two main reasons:
● Get practice working on a team.
● Learn from your partner(s) and come up with creative solutions together.
Ancillary reason: Also reduces programming workload per person. This course
could potentially be made a solo experience but we have decided against that.
Slides again adapted from Josh Hug. Some material for this section of lecture
drawn from www.teamingxdesign.com.
Teamwork
In the real world, some tasks are much too large to be handled by a single
person.
It’s important to learn how to work well in groups now! This is a real world
skill.
Group Intelligence
In the famous “Evidence for a Collective Intelligence Factor in the Performance
of Human Groups”, Woolley et. al investigated the success of teams of
humans on various tasks.
They found that performance on a wide variety of tasks is correlated, i.e.
groups that do well on any specific task tend to do very well on the others.
● This suggests that groups do have “group intelligence”
Group Intelligence
In the famous “Evidence for a Collective Intelligence Factor in the Performance
of Human Groups”, Woolley et. al investigated the success of teams of
humans on various tasks.
Studying individual group members, Woolley et. al found that:
● Collective intelligence is not significantly correlated with average or max
intelligence of each group.
Group Intelligence
In the famous “Evidence for a Collective Intelligence Factor in the Performance
of Human Groups”, Woolley et. al investigated the success of teams of
humans on various tasks.
Studying individual group members, Woolley et. al found that:
● Instead, collective intelligence was correlated with three things:
Group Intelligence
In the famous “Evidence for a Collective Intelligence Factor in the Performance
of Human Groups”, Woolley et. al investigated the success of teams of
humans on various tasks.
Studying individual group members, Woolley et. al found that:
● Instead, collective intelligence was correlated with three things:
○
Average social sensitivity of group members as measured using the “Reading the Mind in
the Eyes Test” (this is really interesting).
Group Intelligence
In the famous “Evidence for a Collective Intelligence Factor in the Performance
of Human Groups”, Woolley et. al investigated the success of teams of
humans on various tasks.
Studying individual group members, Woolley et. al found that:
● Instead, collective intelligence was correlated with three things:
○
○
Average social sensitivity of group members as measured using the “Reading the Mind in
the Eyes Test” (this is really interesting).
How equally distributed the group was in conversational turn-taking, e.g. groups where
one person dominated did poorly.
Group Intelligence
In the famous “Evidence for a Collective Intelligence Factor in the Performance
of Human Groups”, Woolley et. al investigated the success of teams of
humans on various tasks.
Studying individual group members, Woolley et. al found that:
● Instead, collective intelligence was correlated with three things:
○
○
○
Average social sensitivity of group members as measured using the “Reading the Mind in
the Eyes Test” (this is really interesting).
How equally distributed the group was in conversational turn-taking, e.g. groups where
one person dominated did poorly.
Percentage of females in the group (paper suggests this is due to correlation with greater
social sensitivity).
Teamwork
So, your success as a team isn’t about just letting the “smartest” person do all
the work.
Instead, it’s about effectively collaborating and communicating to build your
group intelligence.
●
●
●
●
Treat each other with respect.
Be open and honest with each other.
Make sure to set clear expectations.
… and if those expectations are not met, confront this fact head on.
Reflexivity
Important part of teamwork is “reflexivity”.
● “A group’s ability to collectively reflect upon team objectives, strategies,
and processes, and to adapt to them accordingly.”
● Recommended that you “cultivate a collaborative environment in which
giving and receiving feedback on an ongoing basis is seen as a
mechanism for reflection and learning.”
○
It’s OK and even expected for you and your partner to be a bit unevenly matched in terms
of programming ability.
You might find this description of best practices for team feedback useful,
though it’s targeted more towards larger team projects.
● Some key ideas from this document follow.
Feedback is Hard: Negativity
Most of us have received feedback from someone which felt judgmental or in
bad faith.
● Thus, we’re afraid to give even constructive negative feedback for fear
that our feedback will be misconstrued as an attack.
● And we’re conditioned to watch out for negative feedback that is
ill-intentioned.
Feedback is Hard: Can Seem Like a Waste of Time
Feedback also can feel like a waste of time:
● You may find it a pointless exercise to reflect on one other during the
project. What does that have to do with a programming class?
● In the real world, the same thing happens. Your team has limited time to
figure out “what” to do, so why stop and waste time reflecting on “how”
you’re working together?
Feedback is Hard: Coming Up With Feedback is Tough
Feedback can simply be difficult to produce.
● You may build incredible technical skills, but learning to provide useful
feedback is hard!
● Without confidence in your ability to provide feedback, you may wait until
you are forced to do so by annual reviews or other structured time to
provide it.
○
If you feel like your partnership could be better, try to talk about it without waiting. Both
parties will probably feel better if you share how you are feeling.
Gitlet Introduction
Some Advice
•
Gitlet is the best
•
Start early!
•
Complete lab 12
•
Read the spec
Gitlet Introduction
java gitlet.Main init
git init
Working Directory
Gitlet Repository
Staging Area
.
Commits
Working Directory
Gitlet Repository
.
Staged for addition
Staged for removal
Staging Area
Commits
Working Directory
Gitlet Repository
.
Staged for addition
Staged for removal
Staging Area
Commits
Commit 0
Metadata
null
Working Directory
Gitlet Repository
.
Staged for addition
Staged for removal
Staging Area
Commits
Commit 0
Metadata
null
message = “initial commit”
timestamp = 00:00:00 UTC, Thursday, 1 January 1970
Working Directory
Gitlet Repository
.
Staged for addition
Staged for removal
Staging Area
Commits
Commit 0
Metadata
null
Working Directory
Gitlet Repository
.
Staged for addition
Master
Commit 0
Metadata
null
HEAD
Staged for removal
Staging Area
Commits
nano Hello.txt
Working Directory
Gitlet Repository
.
Staged for addition
Master
Commit 0
Metadata
null
HEAD
Staged for removal
Staging Area
Commits
java gitlet.Main add Hello.txt
git add Hello.txt
Working Directory
Gitlet Repository
.
Staged for addition
Master
HEAD
Staged for removal
Staging Area
Commits
Commit 0
Metadata
null
Blobs
Working Directory
Gitlet Repository
.
Staged for addition
Master
Staged for removal
Staging Area
Commits
HEAD
Commit 0
Metadata
null
Blobs
Blob 0
“Hello”
Working Directory
Gitlet Repository
.
Staged for addition
Staged for removal
Staging Area
Hello.txt → Blob 0
Master
HEAD
Commits
Commit 0
Metadata
null
Blob 0
“Hello”
Blobs
java gitlet.Main commit “Created Hello.txt”
git commit -m “Created Hello.txt”
Working Directory
Gitlet Repository
.
Staged for addition
Staged for removal
Staging Area
Hello.txt → Blob 0
Master
HEAD
Commits
Commit 0
Metadata
null
Blob 0
“Hello”
Blobs
Working Directory
Gitlet Repository
.
Staged for addition
Staged for removal
Staging Area
Hello.txt → Blob 0
Master
Commits
HEAD
Commit 0
Commit 1
Metadata
Metadata
null
null
Blobs
Blob 0
“Hello”
Working Directory
Gitlet Repository
.
Staged for addition
Staged for removal
Staging Area
Hello.txt → Blob 0
Master
Commits
HEAD
Commit 0
Commit 1
Metadata
Metadata
null
null
Blob 0
“Hello”
message = “Created Hello.txt”
timestamp = 17:10:00 PST, Monday, 13 April 2020
Blobs
Working Directory
Gitlet Repository
.
Staged for addition
Master
Staged for removal
Commits
HEAD
Commit 0
Commit 1
Metadata
Metadata
null
Staging Area
Hello.txt
Blobs
Blob 0
“Hello”
Working Directory
Gitlet Repository
.
Staged for addition
Master
Commit 1
Metadata
Metadata
Blob 0
“Hello”
Staging Area
Commits
HEAD
Commit 0
null
Staged for removal
Hello.txt
Blobs
Working Directory
Gitlet Repository
.
Staged for addition
Master
Staged for removal
HEAD
Commit 0
Commit 1
Metadata
Metadata
null
Staging Area
Commits
Hello.txt
Blobs
Blob 0
“Hello”
nano World.txt
java gitlet.Main add World.txt
java gitlet.Main add Hello.txt
java gitlet.Main commit “Created World.txt and
tried to add Hello.txt”
Working Directory
Gitlet Repository
.
Staged for addition
Master
Staged for removal
Commit 1
Metadata
Metadata
null
Commits
HEAD
Commit 0
Staging Area
Hello.txt
Blobs
Blob 0
“Hello”
Working Directory
Gitlet Repository
.
Staged for addition
Master
Staged for removal
HEAD
Commit 0
Commit 1
Metadata
Metadata
null
Blob 0
“Hello”
Staging Area
Commits
Hello.txt
Blobs
Working Directory
Gitlet Repository
.
Staged for addition
Staged for removal
Staging Area
World.txt → Blob 1
Master
Commit 0
Commit 1
Metadata
Metadata
null
Commits
HEAD
Hello.txt
Blob 0
Blob 1
“Hello”
“World”
Blobs
Working Directory
Gitlet Repository
.
Staged for addition
Staged for removal
HEAD
Master
Commit 0
Commit 1
Commit 2
Metadata
Metadata
Metadata
null
Hello.txt
Hello.txt
Blob 0
Blob 1
“Hello”
“World”
Staging Area
Commits
World.txt
Blobs
nano Hello.txt
nano World.txt
java gitlet.Main add World.txt
java gitlet.Main add Hello.txt
java gitlet.Main commit “Modified World.txt
and Hello.txt”
Working Directory
Gitlet Repository
.
Staged for addition
Staged for removal
HEAD
Master
Commit 0
Commit 1
Commit 2
Metadata
Metadata
Metadata
null
Hello.txt
Hello.txt
Blob 0
Blob 1
“Hello”
“World”
Staging Area
Commits
World.txt
Blobs
Working Directory
Gitlet Repository
.
Staged for addition
Staging Area
Staged for removal
Hello.txt → Blob 2
World.txt → Blob 3
Commit 0
Commit 1
Commit 2
Metadata
Metadata
Metadata
null
Hello.txt
Hello.txt
Commits
HEAD
Master
World.txt
Blobs
Blob 0
Blob 1
Blob 2
Blob 3
“Hello”
“World”
“Hello there”
“World!!!!!”
Working Directory
Gitlet Repository
.
Staged for addition
Staging Area
Staged for removal
HEAD Commits
Master
Commit 0
Commit 1
Commit 2
Commit 3
Metadata
Metadata
Metadata
Metadata
null
Hello.txt
Hello.txt
World.txt
Hello.txt
World.txt
Blob 0
Blob 1
Blob 2
Blob 3
“Hello”
“World”
“Hello there”
“World!!!!!”
Blobs
java gitlet.Main init
java gitlet.Main log
//Read from.gitlet all the files you need to run
….
//Write onto disk (.gitlet folder) all the new objects you created / modified
Diving Into the
Technical Details
Working Directory
Gitlet Repository
.
Staged for addition
Staging Area
Staged for removal
HEAD Commits
Master
Commit 0
Commit 1
Commit 2
Commit 3
Metadata
Metadata
Metadata
Metadata
null
Hello.txt
Hello.txt
World.txt
Hello.txt
World.txt
Blobs
Blob 0
Blob 1
Blob 2
Blob 3
“Hello”
“World”
“Hello there”
“World!!!!!”
Working Directory
Gitlet Repository
.
Staged for addition
Staging Area
Staged for removal
HEAD Commits
Master
Commit 0
Commit 1
Commit 2
Commit 3
Metadata
Metadata
Metadata
Metadata
null
Hello.txt
Hello.txt
World.txt
Hello.txt
World.txt
Blob 0
Blob 1
Blob 2
Blob 3
“Hello”
“World”
“Hello there”
“World!!!!!”
Blobs
HEAD Commits
Master
Commit 0
Commit 1
Commit 2
Commit 3
Metadata
Metadata
Metadata
Metadata
null
Hello.txt
Hello.txt
World.txt
Hello.txt
World.txt
Blob 0
Blob 1
Blob 2
Blob 3
“Hello”
“World”
“Hello there”
“World!!!!!”
HEAD Commits
Master
Commit 0
Commit 1
Commit 2
Commit 3
Metadata
Metadata
Metadata
Metadata
null
Hello.txt
Hello.txt
World.txt
Hello.txt
Blobs
World.txt
Blob 0
Blob 1
Blob 2
Blob 3
“Hello”
“World”
“Hello there”
“World!!!!!”
Blobs
HEAD
Master
Commit 3 Commit 3
Hello.txt
Blob 0
Commit 3
Metadata
Hello.txt
World.txt
Blob 0
Blob 1
Commit 2
null
Metadata
Commit 1
Metadata
Commit 2
Commit 1
Commit 0
Commit 0
Metadata
Hello.txt
World.txt
Blob 2
Blob 3
Blob 0
Blob 1
Blob 2
Blob 3
“Hello”
“World”
“Hello there”
“World!!!!!”
commit 0
commit 1
Commits
commit 2
commit ?
commit 3
Blobs
commit 0
commit 1
commit 2
commit ?
commit 3
SHA1(
) = 8e95382d558549303e6
HEAD
Master
5e6194cbf 5e6194cbf
Hello.txt
a699ba6
5e6194cbf
Metadata
Hello.txt
World.txt
a699ba6
9639c673b
71b40c70
null
Metadata
9639c673b
Metadata
71b40c70
9639c673b
f3af0eb43
f3af0eb43
Commits
Metadata
Hello.txt
World.txt
0e93cac
0e84daf
a699ba6
9639c673b
0e93cac
0e84daf
“Hello”
“World”
“Hello there”
“World!!!!!”
Blobs
HEAD
Master
5e6194cbf 5e6194cbf
a699ba6
Hello.txt
a699ba6
9639c673b
5e6194cbf
Metadata
Hello.txt
World.txt
a699ba6
9639c673b
0e93cac
71b40c70
null
Metadata
9639c673b
Metadata
71b40c70
9639c673b
f3af0eb43
f3af0eb43
Commits
Metadata
Hello.txt
World.txt
0e93cac
0e84daf
0e84daf
001001010
111001010
111011010
111001111
byte[ ]
byte[ ]
byte[ ]
byte[ ]
Blobs
Lecture 4
CS 61BL Summer 2021
●
●
●
Announcements
7/12/21
●
●
●
●
Midterm grades out
Midterm regrade requests open
tomorrow 7/13, close in a week.
Grades tab in Beacon is out. Some
assignments (extra credit practice
exam, surveys, midterm) have yet
to be imported.
Lab Tues-Friday (Tues is a
Gradescope assignment, Friday is a
Proj2 workday)
Gitlet Checkpoint Friday
Quiz 5 and 6 this Wednesday and
Friday
Incident report form and shoutout
form both on front page of site.
Asymptotic Analysis
61B(L): Writing Efficient Programs
“An engineer will do for a dime what any fool will do for a dollar.”
Efficiency comes in two flavors:
●
Programming cost (course to date. Will also revisit later).
○
○
How long does it take to develop your programs?
How easy is it to read, modify, and maintain your code?
■ More important than you might think!
■ Majority of cost is in maintenance, not development!
61B(L): Writing Efficient Programs
“An engineer will do for a dime what any fool will do for a dollar.”
Efficiency comes in two flavors:
●
Programming cost (course to date. Will also revisit later).
○
○
●
How long does it take to develop your programs?
How easy is it to read, modify, and maintain your code?
■ More important than you might think!
■ Majority of cost is in maintenance, not development!
Execution cost (from today until end of course).
○
○
How much time does your program take to execute?
How much memory does your program require?
Example of Algorithm Cost
Objective: Determine if a sorted array contains any duplicates.
●
Given sorted array A, are there indices i and j where A[i] = A[j]?
-3
-1
2
4
4
8
10
12
Silly algorithm: Consider every possible pair, returning true if any match.
●
Are (-3, -1) the same? Are (-3, 2) the same? ...
Better algorithm?
Example of Algorithm Cost
Objective: Determine if a sorted array contains any duplicates.
●
Given sorted array A, are there indices i and j where A[i] = A[j]?
-3
-1
2
4
4
8
10
12
Silly algorithm: Consider every possible pair, returning true if any match.
●
Are (-3, -1) the same? Are (-3, 2) the same? ...
Better algorithm?
●
Today’s goal: Introduce formal
technique for comparing
algorithmic efficiency.
For each number A[i], look at A[i+1], and return true the first time you
see a match. If you run out of items, return false.
Intuitive Runtime Characterizations
How Do I Runtime Characterization?
Our goal is to somehow characterize the runtimes of the functions below.
● Characterization should be simple and mathematically rigorous.
● Characterization should demonstrate superiority of dup2 over dup1.
public static boolean dup1(int[] A) {
for (int i = 0; i < A.length; i += 1) {
for (int j = i + 1; j < A.length; j += 1) {
dup2
if (A[i] == A[j]) {
public static boolean dup2(int[] A) {
return true;
for (int i = 0; i < A.length - 1; i += 1) {
}
if (A[i] == A[i + 1]) {
}
return true;
}
}
return false;
}
}
return false;
dup1
}
Techniques for Measuring Computational Cost
Technique 1: Measure execution time in seconds using a client program.
● Tools:
○
○
○
Physical stopwatch.
Unix has a built in time command that measures execution time.
Princeton Standard library has a Stopwatch class.
public static void main(String[] args) {
int N = Integer.parseInt(args[0]);
int[] A = makeArray(N);
// record times for each function call
dup1(A);
dup2(A);
}
Time Measurements for dup1 and dup2
N
dup1
dup2
10000
0.08
0.08
50000
0.32
0.08
100000
1.00
0.08
200000
8.26
0.1
400000
15.4
0.1
seconds
N
Time to complete (in seconds)
Techniques for Measuring Computational Cost
Technique 1: Measure execution time in seconds using a client program.
● Good: Easy to measure, meaning is obvious.
● Bad: May require large amounts of computation time. Result varies with
machine, compiler, input data, etc.
public static void main(String[] args) {
int N = Integer.parseInt(args[0]);
int[] A = makeArray(N);
// record times for each function call
dup1(A);
dup2(A);
}
Techniques for Measuring Computational Cost
Technique 2A: Count possible operations for an array of size N = 10,000.
● Good: Machine independent. Input dependence captured in model.
● Bad: Tedious to compute. Array size was arbitrary. Doesn’t tell you actual
time.
operation
count, N=10000
for (int i = 0; i < A.length; i += 1) {
for (int j = i+1; j < A.length; j += 1) {
if (A[i] == A[j]) {
return true;
}
}
}
return false;
i=0
1
j=i+1
1 to 10000
less than (<)
2 to 50,015,001
increment (+=1)
0 to 50,005,000
equals (==)
1 to 49,995,000
array accesses
2 to 99,990,000
The counts are tricky to compute. Work not shown.
best case
worst case
Techniques for Measuring Computational Cost
Technique 2B: Count possible operations in terms of input array size N.
● Good: Machine independent. Input dependence captured in model. Tells you
how algorithm scales.
● Bad: Even more tedious to compute. Doesn’t tell you actual time.
for (int i = 0; i < A.length; i += 1) {
for (int j = i + 1; j < A.length; j += 1)
{
if (A[i] == A[j]) {
return true;
}
}
}
return false;
operation
symbolic count
count, N=10000
i=0
1
1
j=i+1
1 to N
1 to 10000
less than (<)
2 to (N2+3N+2)/2
2 to 50,015,001
increment (+=1)
0 to (N2+N)/2
0 to 50,005,000
equals (==)
1 to (N2-N)/2
1 to 49,995,000
array accesses
2 to N2-N
2 to 99,990,000
Techniques for Measuring Computational Cost
Your turn: Try to come up with rough estimates for the symbolic and
exact counts for at least one of the operations.
● Tip: Don’t worry about being off by one. Just try to predict the rough
magnitudes of each.
for (int i = 0; i < A.length - 1; i += 1){
if (A[i] == A[i + 1]) {
return true;
}
}
return false;
operation
sym.
count
count,
N=10000
i=0
1
1
less than (<)
increment (+=1)
equals (==)
array accesses
Techniques for Measuring Computational Cost
Your turn: Try to come up with rough estimates for the symbolic and
exact counts for at least one of the operations.
for (int i = 0; i < A.length - 1; i += 1) {
if (A[i] == A[i + 1]) {
return true;
}
}
return false;
Especially observant folks may notice we didn’t count
everything, e.g. “- 1” and “+ 1” operations. We’ll see
why this omission is not a problem very shortly.
operation
symbolic
count
count,
N=10000
i=0
1
1
less than (<)
0 to N
0 to 10000
increment (+=1)
0 to N - 1
0 to 9999
equals (==)
1 to N - 1
1 to 9999
array accesses
2 to 2N - 2
2 to 19998
If you did this exercise but were off by one, that’s fine. The exact numbers aren’t that important.
Comparing Algorithms
Which algorithm is better? Why?
dup1
dup2
operation
symbolic count
count, N=10000
i=0
1
1
j=i+1
1 to N
1 to 10000
less than (<)
2 to (N2+3N+2)/2
2 to 50,015,001
increment (+=1)
0 to (N2+N)/2
0 to 50,005,000
equals (==)
1 to (N2-N)/2
1 to 49,995,000
array accesses
2 to N2-N
2 to 99,990,000
operation
symbolic
count
count,
N=10000
i=0
1
1
less than (<)
0 to N
0 to 10000
increment (+=1)
0 to N - 1
0 to 9999
equals (==)
1 to N - 1
1 to 9999
array accesses
2 to 2N - 2
2 to 19998
Comparing Algorithms
Which algorithm is better? dup2. Why?
● Fewer operations to do the same work [e.g. 50,015,001 vs. 10000 operations].
● Better answer: Algorithm scales better in the worst case. (N2+3N+2)/2 vs. N.
● Even better answer: Parabolas (N2) grow faster than lines (N).
dup1
dup2
operation
symbolic count
count, N=10000
i=0
1
1
j=i+1
1 to N
1 to 10000
less than (<)
2 to (N2+3N+2)/2
2 to 50,015,001
increment (+=1)
0 to (N2+N)/2
0 to 50,005,000
equals (==)
1 to (N2-N)/2
1 to 49,995,000
array accesses
2 to N2-N
2 to 99,990,000
operation
symbolic
count
count,
N=10000
i=0
1
1
less than (<)
0 to N
0 to 10000
increment (+=1)
0 to N - 1
0 to 9999
equals (==)
1 to N - 1
1 to 9999
array accesses
2 to 2N - 2
2 to 19998
Asymptotic Behavior
In most cases, we care only about asymptotic behavior, i.e. what happens
for very large N.
● Simulation of billions of interacting particles.
● Social network with billions of users.
● Logging of billions of transactions.
● Encoding of billions of bytes of video data.
Algorithms which scale well (e.g. look like lines) have better asymptotic runtime
behavior than algorithms that scale relatively poorly (e.g. look like parabolas).
Parabolas vs. Lines
Suppose we have two algorithms that zerpify a collection of N items.
● zerp1 takes 2N2 operations.
● zerp2 takes 500N operations.
For small N, zerp1 might be faster, but as dataset size grows, the parabolic
algorithm is going to fall farther and farther behind (in time it takes to
complete).
zerp2
zerp1
Scaling Across Many Domains
We’ll informally refer to the “shape” of a runtime function as its
order of growth (will formalize soon).
● Effect is dramatic! Often determines whether a problem can be solved at all.
(from Algorithm Design: Tardos, Kleinberg)
Duplicate Finding
Our goal is to somehow characterize the runtimes of the functions below.
Characterization should be simple and mathematically rigorous.
Characterization should demonstrate superiority of dup2 over dup1.
dup1: parabolic, a.k.a. quadratic
dup2: linear
operation
symbolic count
operation
i=0
1
symbolic
count
j=i+1
1 to N
i=0
1
less than (<)
2 to (N2+3N+2)/2
less than (<)
0 to N
increment (+=1)
0 to (N2+N)/2
increment (+=1)
0 to N - 1
equals (==)
1 to (N2-N)/2
equals (==)
1 to N - 1
array accesses
2 to N2-N
array accesses
2 to 2N - 2
Worst Case
Order of Growth
Duplicate Finding
Our goal is to somehow characterize the runtimes of the functions below.
● Characterization should be simple and mathematically rigorous.
operation
count
i=0
1
j=i+1
1 to N
operation
count
i=0
1
less than (<)
0 to N
increment (+=1)
0 to N - 1
equals (==)
1 to N - 1
array accesses
2 to 2N - 2
2
less than (<)
2 to (N +3N+2)/2
increment (+=1)
0 to (N2+N)/2
2
equals (==)
1 to (N -N)/2
array accesses
2 to N2-N
Let’s be more careful about what we mean when we say the left function is “like” a
parabola, and the right function is “like” a line.
Intuitive Simplification 1: Consider Only the Worst Case
Simplification 1: Consider only the worst case.
for (int i = 0; i < A.length; i += 1) {
for (int j = i+1; j < A.length; j += 1) {
if (A[i] == A[j]) {
return true;
}
}
}
return false;
operation
count
i=0
1
j=i+1
1 to N
less than (<)
2 to (N2+3N+2)/2
increment (+=1)
0 to (N2+N)/2
equals (==)
1 to (N2-N)/2
array accesses
2 to N2-N
Intuitive Simplification 1: Consider Only the Worst Case
Simplification 1: Consider only the worst case.
● Justification: When comparing algorithms, we often care only about the
worst case [but we will see exceptions in this course].
for (int i = 0; i < A.length; i += 1) {
for (int j = i+1; j < A.length; j += 1) {
if (A[i] == A[j]) {
return true;
}
}
}
return false;
We’re effectively focusing on the case where there are no
duplicates, because this is where there is a performance
difference.
operation
worst case count
i=0
1
j=i+1
N
less than (<)
(N2+3N+2)/2
increment (+=1)
(N2+N)/2
equals (==)
(N2-N)/2
array accesses
N2-N
Intuitive Order of Growth Identification:
Consider the algorithm below. What do you expect will be the
order of growth of the runtime for the algorithm?
A.
B.
C.
D.
N
N2
N3
N6
[linear]
[quadratic]
[cubic]
[sextic]
operation
count
less than (<)
100N2 + 3N
greater than (>)
2N3 + 1
and (&&)
5,000
In other words, if we plotted total runtime vs. N, what shape would we expect?
Intuitive Order of Growth Identification
Consider the algorithm below. What do you expect will be the
order of growth of the runtime for the algorithm?
A.
B.
C.
D.
N [linear]
N2 [quadratic]
N3 [cubic]
N6 [sextic]
operation
count
less than (<)
100N2 + 3N
greater than (>)
2N3 + 1
and (&&)
5,000
Argument:
● Suppose < takes α nanoseconds, > takes β nanoseconds, and && takes γ
nanoseconds.
Extremely
2
3
important point.
● Total time is α(100N + 3N) + β(2N + 1) + 5000γ nanoseconds.
Make sure you
● For very large N, the 2βN3 term is much larger than the others.
understand it!
Intuitive Simplification 2: Restrict Attention to One Operation
Simplification 2: Pick some representative operation to act as a proxy
for the overall runtime.
There are other good choices.
● Good choice: increment.
● Bad choice: assignment of j = i + 1.
operation
worst case count
We call our choice the “cost model”.
for (int i = 0; i < A.length; i += 1) {
for (int j = i+1; j < A.length; j += 1) {
if (A[i] == A[j]) {
return true;
}
}
}
return false;
i=0
1
j=i+1
N
less than (<)
(N2+3N+2)/2
increment (+=1)
(N2+N)/2
equals (==)
(N2-N)/2
array accesses
N2-N
cost model = increment
Intuitive Simplification 3: Eliminate low order terms.
Simplification 3: Ignore lower order terms.
for (int i = 0; i < A.length; i += 1) {
for (int j = i+1; j < A.length; j += 1) {
if (A[i] == A[j]) {
return true;
}
}
}
return false;
operation
worst case
increment (+=1)
(N2+N)/2
Intuitive Simplification 4: Eliminate multiplicative constants.
Simplification 4: Ignore multiplicative constants.
● Why? It has no real meaning. We already threw away information when
we chose a single proxy operation.
for (int i = 0; i < A.length; i += 1) {
for (int j = i+1; j < A.length; j += 1) {
if (A[i] == A[j]) {
return true;
}
}
}
return false;
operation
worst case
increment (+=1)
N2/2
Simplification Summary
Simplifications:
1. Only consider the worst case.
2. Pick a representative operation (a.k.a. the cost model).
3. Ignore lower order terms.
4. Ignore multiplicative constants.
operation
count
i=0
1
j=i+1
1 to N
less than (<)
2 to (N2+3N+2)/2
These three simplifications are OK because we only
care about the “order of growth” of the runtime.
2
increment (+=1)
0 to (N +N)/2
equals (==)
1 to (N2-N)/2
array accesses
2 to N2-N
operation
worst case o.o.g.
increment (+=1)
N2
Worst case order of growth of runtime: N2
Big-Theta
Formalizing Order of Growth
Given a function R(N), we can apply our last two simplifications
(ignore lower order terms and multiplicative constants) to yield the order of
growth.
● Example: R(N) = 3N3 + N2
● Order of growth: N3
Let’s move to a more formal notation called Big-Theta.
● The math might seem daunting at first.
● … but the idea is exactly the same! Using “Big-Theta” instead of “order of
growth” does not change the way we analyze code at all.
Order of Growth Exercise
Consider the functions below.
● Informally, what is the “shape” of each function for very large N?
●
In other words, what is the order of growth of each function?
function
order of growth
N3 + 3N4
1/N + N3
1/N + 5
NeN + N
40 sin(N) + 4N2
Order of Growth Exercise
Consider the functions below.
● Informally, what is the “shape” of each function for very large N?
●
In other words, what is the order of growth of each function?
function
order of growth
N3 + 3N4
N4
1/N + N3
N3
1/N + 5
1
NeN + N
NeN
40 sin(N) + 4N2
N2
Big-Theta
Suppose we have a function R(N) with order of growth f(N).
● In “Big-Theta” notation we write this as R(N) ∈ Θ(f(N)).
● Examples:
○
○
○
○
○
N3 + 3N4 ∈ Θ(N4)
1/N + N3 ∈ Θ(N3)
1/N + 5 ∈ Θ(1)
NeN + N ∈ Θ(NeN)
40 sin(N) + 4N2 ∈ Θ(N2)
function R(N)
order of growth
N3 + 3N4
N4
1/N + N3
N3
1/N + 5
1
NeN + N
NeN
40 sin(N) + 4N2
N2
Big-Theta: Formal Definition (Visualization)
means there exist positive constants k1 and k2 such that:
for all values of N greater than some N0.
i.e. very large N
2
2
Example: 40 sin(N) + 4N ∈ Θ(N )
● R(N) = 40 sin(N) + 4N2
● f(N) = N2
● k1 = 3
● k2 = 5
Big-Theta: Intuitive Definition
means that R(N) will always take f(N) time. In other
words, every time we execute the R(N) program, we are
guaranteed it will take f(N) time.
Big-Theta: Intuitive Definition
means that R(N) will always take f(N) time. In other
words, every time we execute the R(N) program, we are
guaranteed it will take f(N) time.
for (int i = 0; i < N; i += 1) {
System.out.println(i);
}
The function to the left always takes N time to run, so we
can say it is runs in Θ(N)
Big-Theta and Runtime Analysis
Using Big-Theta doesn’t change anything about runtime analysis
(no need to find k1 or k2 or anything like that).
● The only difference is that we use the Θ symbol anywhere we would have
said “order of growth”.
operation
worst case count
i=0
1
j=i+1
Θ(N)
operation
worst case count
increment (+=1)
Θ(N2)
2
less than (<)
Θ(N )
increment (+=1)
Θ(N2)
equals (==)
Θ(N2)
array accesses
Θ(N2)
Big O Notation
Worst case runtime: Θ(N2)
Big Theta
We used Big Theta to describe the order of growth of a function.
function R(N)
order of growth
N3 + 3N4
Θ(N4)
1/N + N3
Θ(N3)
1/N + 5
Θ(1)
NeN + N
Θ(NeN)
40 sin(N) + 4N2
Θ(N2)
We also used Big Theta to describe the rate of growth of the runtime
of a piece of code.
Big O
Whereas Big Theta can informally be thought of as something
like “equals”, Big O can be thought of as “less than or equal”.
Example, the following are all true:
● N3 + 3N4 ∈ Θ(N4)
● N3 + 3N4 ∈ O(N4)
● N3 + 3N4 ∈ O(N6)
● N3 + 3N4 ∈ O(N!)
● N3 + 3N4 ∈ O(NN!)
Big O
Whereas Big Theta can informally be thought of as something
like “equals”, Big O can be thought of as “less than or equal”.
Example, the following are all true:
● N3 + 3N4 ∈ Θ(N4)
● N3 + 3N4 ∈ O(N4)
Note that this is the tightest big O bound
● N3 + 3N4 ∈ O(N6)
● N3 + 3N4 ∈ O(N!)
● N3 + 3N4 ∈ O(NN!)
Big O: Formal Definition (Visualization)
means there exists a positive constant k2 such that:
for all values of N greater than some N0.
i.e. very large N
Big O: Intuitive Definition
means that the function R(N) will never be worse, or
slower, than this bound.
A function is “bad” if it
takes a long time to run
Big O: Intuitive Definition
means that the function R(N) will never be worse, or
slower, than this bound.
N is A.length
dup2
for (int i = 0; i < A.length - 1; i += 1) {
if (A[i] == A[i + 1]) {
return true;
}
}
return false;
What is the runtime of dup2, with
a tight big O bound?
Big O: Intuitive Definition
means that the function R(N) will never be worse, or
slower, than this bound.
N is A.length
dup2
for (int i = 0; i < A.length - 1; i += 1) {
if (A[i] == A[i + 1]) {
return true;
}
}
return false;
Big Omega Notation
What is the runtime of dup2, with
a tight big O bound?
Answer: O(N)
Big Omega
Whereas Big O can informally be thought of as something
like “less than or equal”, Big Omega Ω can be thought of as “greater
than or equal”.
Example, the following are all true:
● N3 + 3N4 ∈ Ω(N4)
● N3 + 3N4 ∈ Ω(N3)
● N3 + 3N4 ∈ Ω(N2)
● N3 + 3N4 ∈ Ω(N)
● N3 + 3N4 ∈ Ω(1)
Are all functions Ω(1)?
Big Omega
Whereas Big O can informally be thought of as something
like “less than or equal”, Big Omega Ω can be thought of as “greater
than or equal”.
Example, the following are all true:
● N3 + 3N4 ∈ Ω(N4)
Note that this is the tightest big Ω bound
● N3 + 3N4 ∈ Ω(N3)
● N3 + 3N4 ∈ Ω(N2)
● N3 + 3N4 ∈ Ω(N)
● N3 + 3N4 ∈ Ω(1)
Are all functions Ω(1)?
Big Omega: Formal Definition
means there exists a positive constant k1 such that:
for all values of N greater than some N0.
i.e. very large N
Big Omega: Intuitive Definition
means that the function R(N) will never be better, or
faster, than this bound.
Big Omega: Intuitive Definition
means that the function R(N) will never be better, or
faster, than this bound.
N is A.length
dup2
for (int i = 0; i < A.length - 1; i += 1) {
if (A[i] == A[i + 1]) {
return true;
}
}
return false;
What is the runtime of dup2, with
a tight big Ω bound?
Big Omega: Intuitive Definition
means that the function R(N) will never be better, or
faster, than this bound.
N is A.length
dup2
for (int i = 0; i < A.length - 1; i += 1) {
if (A[i] == A[i + 1]) {
return true;
}
}
return false;
What is the runtime of dup2, with
a tight big Ω bound?
Answer: Ω(1)
Summary: Big Theta vs. Big O vs. Big Omega
Big Theta
Θ(f(N))
Big O
O(f(N))
Big Omega
Ω(f(N))
Informal meaning:
Family
Family Members
Order of growth is
f(N).
Θ(N2)
N2/2
2N2
N2 + 38N + N
Order of growth is
less than or equal
to f(N).
O(N2)
N2/2
2N2
lg(N)
Order of growth is
greater than or
equal to f(N).
Ω(N2)
N2/2
2N2
8N4
iPad Demo (if time permits)
Problem 1 from this discussion
Citations
Slides revamped from Professor Hug’s Spring 2019 CS 61B lecture
An introduction to Trees & Traversals
What is a tree?
Trees naturally represent recursively defined, hierarchical objects with more
than one recursive subpart for each instance
A tree consists of:
●
●
A set of nodes.
A set of edges that connect those nodes.
○
Constraint: There is exactly one path between any two nodes.
Rooted Trees
A rooted tree is a tree where we’ve chosen one node as the “root”.
●
●
Every node N except the root has exactly one parent, defined as the first
node on the path from N to the root.
A node with no child is called a leaf.
A
For each of these:
B
● A is the root.
● B is a child of A. (and C of B)
C
● A is a parent of B. (and B of C)
A
A
B
C
C
B
C
Tree traversal orderings: level
Level Order
●
Visit top-to-bottom, left-to-right (like reading in English):
D
B
A
F
E
C
G
Tree traversal orderings: level
Level Order
●
Visit top-to-bottom, left-to-right (like reading in English): DBFACEG
D
B
A
F
C
E
G
Tree traversal orderings: depth first
Depth First Traversals
●
●
●
3 types: Preorder, Inorder, Postorder
Basic (rough) idea: Traverse “deep nodes” (e.g. A) before shallow ones (e.g.
F).
Note: Traversing a node is different than “visiting” a node. See next slide.
D
B
A
F
E
C
G
Depth first: preorder
Preorder: “Visit” a node, then traverse its children:
preOrder(BSTNode x) {
if (x == null) return;
print(x.key)
preOrder(x.left)
preOrder(x.right)
}
D
B
A
F
C
E
G
Depth first: preorder
Preorder: “Visit” a node, then traverse its children:
preOrder(BSTNode x) {
if (x == null) return;
print(x.key)
preOrder(x.left)
preOrder(x.right)
}
D B A C F E G
D
B
A
F
E
C
G
Depth first: inorder
Inorder traversal: Traverse left child, visit, then traverse right child:
inOrder(BSTNode x) {
if (x == null) return;
inOrder(x.left)
print(x.key)
inOrder(x.right)
}
D
B
A
F
C
E
G
Depth first: inorder
Inorder traversal: Traverse left child, visit, then traverse right child:
inOrder(BSTNode x) {
if (x == null) return;
inOrder(x.left)
print(x.key)
inOrder(x.right)
}
A B C DE F G
D
B
A
F
E
C
G
Depth first: postorder
Postorder traversal: Traverse left, traverse right, then visit:
postOrder(BSTNode x) {
if (x == null) return;
postOrder(x.left)
postOrder(x.right)
print(x.key)
}
D
B
A
F
C
E
G
Depth first: postorder
Postorder traversal: Traverse left, traverse right, then visit: ACBEGFD
postOrder(BSTNode x) {
if (x == null) return;
postOrder(x.left)
postOrder(x.right)
print(x.key)
}
D
B
F
A
C
E
G
A Useful Visual Trick (for Humans, Not Algorithms)
●
●
●
Preorder traversal: We trace a path around the graph, from the top going
counter-clockwise. “Visit” every time we pass the LEFT of a node.
Inorder traversal: “Visit” when you cross the bottom of a node.
Postorder traversal: “Visit” when you
cross the right a node.
1
2
Example: Post-Order Traversal
●
478529631
4
3
6
5
7
8
9
What Good Are All These Traversals?
Example: Preorder Traversal for printing directory listing:
sc2APM
directOverlay
directIO
DXHookD3D11.cs
directO.suo
Injector.cs
Where do we go from here?
●
●
●
BFS: breadth first search
Graphs
Graph algorithms
notes
directO.sln
python
printAPM.py
Disjoint Sets
What problems are we curious about?
●
●
●
●
Dynamic Connectivity and the Disjoint Sets Problem
Quick Find
Quick Union
Weighted Quick Union
Meta-goals of the Coming Lectures
Data Structure Refinement:
Next couple of weeks: Deriving classic solutions to interesting problems, with
an emphasis on how sets, maps, and priority queues are implemented.
Today: Deriving the “Disjoint Sets” data structure for solving the “Dynamic
Connectivity” problem. We will see:
How a data structure design can evolve from basic to sophisticated.
How our choice of underlying abstraction can affect asymptotic runtime (using
our formal Big-Theta notation) and code complexity.
The Disjoint Sets Data Structure
The Disjoint Sets data structure has two operations:
●
connect(x, y): Connects x and y.
●
isConnected(x, y): Returns true if x and y are connected. Connections can
be transitive, i.e. they don’t need to be direct.
Example:
●
●
●
●
●
●
●
connect(China, Vietnam)
connect(China, Mongolia)
isConnected(Vietnam, Mongolia)? true
connect(USA, Canada)
isConnected(USA, Mongolia)? false
connect(China, USA)
isConnected(USA, Mongolia)? true
Hype
rloop
The Disjoint Sets Data Structure
The Disjoint Sets data structure has two operations:
●
connect(x, y): Connects x and y.
isConnected(x, y): Returns true if x and y are connected. Connections can
be transitive, i.e. they don’t need to be direct.
Useful for many purposes, e.g.:
●
●
Percolation theory:
○
●
Computational chemistry.
Implementation of other algorithms:
○
Kruskal’s algorithm.
Hype
rloop
Disjoint Sets on Integers
To keep things simple, we’re going to:
●
Force all items to be integers instead of arbitrary data (e.g. 8 instead of
USA).
●
Declare the number of items in advance, everything is disconnected at
start.
ds = DisjointSets(7)
ds.connect(0, 1)
ds.connect(1, 2)
ds.connect(0, 4)
ds.connect(3, 5)
ds.isConnected(2, 4): true
ds.isConnected(3, 0): false
0
4
1
2
3
5
6
Disjoint Sets on Integers
To keep things simple, we’re going to:
●
Force all items to be integers instead of arbitrary data (e.g. 8 instead of
USA).
●
Declare the number of items in advance, everything is disconnected at
start.
0
1
2
3
ds = DisjointSets(7)
ds.connect(0, 1)
ds.connect(1, 2)
ds.connect(0, 4)
ds.connect(3, 5)
ds.isConnected(2, 4): true
ds.isConnected(3, 0): false
ds.connect(4, 2)
4
5
6
Disjoint Sets on Integers
To keep things simple, we’re going to:
●
Force all items to be integers instead of arbitrary data (e.g. 8 instead of
USA).
●
Declare the number of items in advance, everything is disconnected at
start.
ds = DisjointSets(7)
ds.connect(0, 1)
ds.connect(1, 2)
ds.connect(0, 4)
ds.connect(3, 5)
ds.isConnected(2, 4): true
ds.isConnected(3, 0): false
ds.connect(4, 2)
ds.connect(4, 6)
0
1
2
4
3
5
6
Disjoint Sets on Integers
To keep things simple, we’re going to:
●
Force all items to be integers instead of arbitrary data (e.g. 8 instead of
USA).
●
Declare the number of items in advance, everything is disconnected at
start. ds = DisjointSets(7)
ds.connect(0, 1)
ds.connect(1, 2)
ds.connect(0, 4)
ds.connect(3, 5)
ds.isConnected(2, 4): true
ds.isConnected(3, 0): false
ds.connect(4, 2)
ds.connect(4, 6)
ds.connect(3, 6)
0
4
1
2
3
5
6
Disjoint Sets on Integers
●
●
Force all items to be integers instead of arbitrary data (e.g. 8 instead of
USA).
Declare the number of items in advance, everything is disconnected at
start.
ds = DisjointSets(7)
ds.connect(0, 1)
ds.connect(1, 2)
ds.connect(0, 4)
ds.connect(3, 5)
ds.isConnected(2, 4): true
ds.isConnected(3, 0): false
ds.connect(4, 2)
ds.connect(4, 6)
ds.connect(3, 6)
ds.isConnected(3, 0): true
The Disjoint Sets Interface
0
1
2
4
3
5
6
getBack()
connect(int p, int q)
deleteBack()
isConnected(int
p, int q)
get(int
i)
public interface DisjointSets {
/** Connects two items P and Q. */
void connect(int p, int q);
/** Checks to see if two items are connected. */
boolean isConnected(int p, int q);
}
Goal: Design an efficient DisjointSets implementation.
● Number of elements N can be huge.
● Number of method calls M can be huge.
● Calls to methods may be interspersed (e.g. can’t assume it’s only connect
operations followed by only isConnected operations).
The Naive Approach
Naive approach:
●
●
Connecting two things: Record every single connecting line in some data
structure.
Checking connectedness: Do some sort of (??) iteration over the lines to
see if one thing can be reached from the other.
0
4
1
2
3
6
5
A Better Approach: Connected Components
Rather than manually writing out every single connecting line, only record
the sets that each item belongs to.
{0}, {1}, {2}, {3}, {4}, {5}, {6}
{0, 1}, {2}, {3}, {4}, {5}, {6}
{0, 1, 2}, {3}, {4}, {5}, {6}
{0, 1, 2, 4}, {3}, {5}, {6}
{0, 1, 2, 4}, {3, 5}, {6}
connect(0, 1)
connect(1, 2)
connect(0, 4)
connect(3, 5)
isConnected(2, 4): true
isConnected(3, 0): false
{0, 1, 2, 4}, {3, 5}, {6}
connect(4, 2)
{0, 1, 2, 4, 6}, {3, 5}
connect(4, 6)
{0, 1, 2, 3, 4, 5, 6}
connect(3, 6)
isConnected(3, 0): true
A Better Approach: Connected Components
For each item, its connected component is the set of all items that are
connected to that item.
●
●
Naive approach: Record every single connecting line somehow.
Better approach: Model connectedness in terms of sets.
How things are connected isn’t something we need to know.
Only need to keep track of which connected component each item belongs to.
○
○
0
4
1
2
3
5
{ 0, 1, 2, 4 }, {3, 5}, {6}
Up next: We’ll consider how to do track set membership in Java.
Quick Find
6
Challenge: Pick Data Structures to Support Tracking of Sets
Before connect(2, 3) operation:
0
1
2
4
3
After connect(2, 3) operation:
0
6
1
2
4
5
{ 0, 1, 2, 4 }, {3, 5}, {6}
3
6
5
{ 0, 1, 2, 4, 3, 5}, {6}
Assume elements are numbered from 0 to N-1.
Challenge: Pick Data Structures to Support Tracking of Sets
Before connect(2, 3) operation:
0
4
1
2
3
After connect(2, 3) operation:
6
5
{ 0, 1, 2, 4 }, {3, 5}, {6}
0
1
2
4
5
{ 0, 1, 2, 4, 3, 5}, {6}
Map<Integer, Integer> -- first number represents set and
second represents item
● Slow because you have to iterate to find which set
something belongs to.
Assume elements are numbered from 0 to N-1.
3
6
Challenge: Pick Data Structures to Support Tracking of Sets
Before connect(2, 3) operation:
0
1
2
4
3
After connect(2, 3) operation:
0
6
1
2
4
5
{ 0, 1, 2, 4 }, {3, 5}, {6}
3
6
5
{ 0, 1, 2, 4, 3, 5}, {6}
Map<Integer, Integer> -- first number represents the item, and
the second is the set number.
● More or less what we get to shortly, but less efficient for
reasons I will explain.
Assume elements are numbered from 0 to N-1.
Challenge: Pick Data Structures to Support Tracking of Sets
Before connect(2, 3) operation:
0
4
1
2
3
After connect(2, 3) operation:
6
5
0
4
1
2
3
5
{ 0, 1, 2, 4 }, {3, 5}, {6}
{ 0, 1, 2, 4, 3, 5}, {6}
Idea #1: List of sets of integers, e.g. [{0, 1, 2, 4}, {3, 5}, {6}]
● In Java: List<Set<Integer>>.
● Very intuitive idea.
6
Challenge: Pick Data Structures to Support Tracking of Sets
If nothing is connected:
0
1
2
3
4
5
6
Idea #1: List of sets of integers, e.g. [{0}, {1}, {2}, {3}, {4}, {5}, {6}]
● In Java: List<Set<Integer>>.
● Very intuitive idea.
● Requires iterating through all the sets to find anything. Complicated and slow!
○ Worst case: If nothing is connected, then isConnected(5, 6) requires
iterating through N-1 sets to find 5, then N sets to find 6. Overall runtime
of Θ(N).
Performance Summary
Implementation
constructor
connect
isConnected
ListOfSetsDS
Θ(N)
O(N)
O(N)
Constructor’s runtime has order of
growth N no matter what, so Θ(N).
Worst case is Θ(N), but other
cases may be better. We’ll say
O(N) since O means “less than
or equal”.
ListOfSetsDS is complicated and slow.
● Operations are linear when number of connections are small.
○ Have to iterate over all sets.
● Important point: By deciding to use a List of Sets, we have doomed
ourselves to complexity and bad performance.
My Approach: Just Use a Array of Integers
Before connect(2, 3) operation:
0
1
2
After connect(2, 3) operation:
3
4
6
{ 0, 1, 2, 4 }, {3, 5}, {6}
4 4 4 5 4 5 6
0
1
2
3
1
2
3
4
5
int[] id
0
4
5
6
5
{ 0, 1, 2, 4, 3, 5}, {6}
int[] id
6
5 5 5 5 5 5 6
0
1
2
3
4
5
6
Idea #2: list of integers where ith entry gives set number (a.k.a. “id”) of item i.
●
connect(p, q): Change entries that equal id[p] to id[q]
QuickFindDS
public class QuickFindDS implements DisjointSets {
private int[] id;
Very fast: Two array accesses: Θ(1)
public boolean isConnected(int p, int q) {
return id[p] == id[q];
}
Relatively slow: N+2 to 2N+2 array accesses: Θ(N)
public void connect(int p, int q) {
int pid = id[p];
public QuickFindDS(int N) {
int qid = id[q];
id = new int[N];
for (int i = 0; i < id.length; i++) {
for (int i = 0; i < N; i++)
if (id[i] == pid) {
id[i] = i;
id[i] = qid;
}
}
}
}...
Performance Summary
Implementation
constructor
connect
isConnected
ListOfSetsDS
Θ(N)
O(N)
O(N)
QuickFindDS
Θ(N)
Θ(N)
Θ(1)
QuickFindDS is too slow for practical use: Connecting two items takes N time.
● Instead, let’s try something more radical.
Quick Union
Improving the Connect Operation
Approach zero: Represent everything as boxes and lines. Overly complicated.
0
1
2
3
6
??? in Java instance variables.
4
5
ListOfSets: Represent everything as connected components. Represented
connected components as list of sets of integers.
{ 0, 1, 2, 4 }, {3, 5}, {6}
[{0, 1, 2, 4}, {3, 5}, {6}]
List<Set<Integer>>
QuickFind: Represent everything as connected components. Represented
connected components as a list of integers, where value = id.
{ 0, 1, 2, 4 }, {3, 5}, {6}
[2, 2, 2, 3, 2, 3, 6]
int[]
Improving the Connect Operation
QuickFind: Represent everything as connected components. Represented
connected components as a list of integers where value = id.
● Bad feature: Connecting two sets is slow!
{ 0, 1, 2, 4 }, {3, 5}, {6}
[2, 2, 2, 3, 2, 3, 6]
int[]
Next approach (QuickUnion): We will still represent everything as connected
components, and we will still represent connected components as a list of
integers. However, values will be chosen so that connect is fast.
Improving the Connect Operation
Hard question: How could we change our set representation so that combining
two sets into their union requires changing one value?
0
1
2
3
4
0
6
{ 0, 1, 2, 4 }, {3, 5}, {6}
1
2
3
3
4
5
6
5
{ 0, 1, 2, 4, 3, 5}, {6}
0 0 0 3 0 3 6
0
2
4
5
id
1
3 3 3 3 3 3 6
id
6
0
1
2
3
4
5
6
Improving the Connect Operation
Hard question: How could we change our set representation so that combining
two sets into their union requires changing one value?
●
Idea: Assign each item a parent (instead of an id). Results in a tree-like
shape.
parent
-1
0
1
-1
0
3
-1
0
1
2
3
4
5
6
○
An innocuous sounding, seemingly arbitrary solution.
○
Unlocks a pretty amazing universe of math that we won’t discuss.
Note: The optional textbook
has an item’s parent as itself
instead of -1 for root items.
0
0 is the “root” of
this set.
6
3
1
5
4
{0, 1, 2, 4}
2
{3, 5}
{6}
Improving the Connect Operation
connect(5, 2)
●
How should we change the parent list to handle this connect operation?
○
If you’re not sure where to start, consider: why can’t we just set id[5] to 2?
parent
-1
0
1
-1
0
3
-1
0
1
2
3
4
5
6
0
0 is the “root” of
this set.
4
6
3
1
5
2
{0, 1, 2, 4}
{3, 5}
{6}
Improving the Connect Operation (Your Answer)
connect(5, 2)
●
One possibility, set id[3] = 2
●
Set id[3] = 0
parent
-1
0
1
2
0
2
-1
0
1
2
3
4
5
6
0
0 is the “root” of
this set.
4
2
{0, 1, 2, 4}
6
3
1
5
{3, 5}
{6}
Improving the Connect Operation
connect(5, 2)
●
Find root(5). // returns 3
●
Find root(2). // returns 0
●
Set root(5)’s value equal to root(2).
parent
-1
0
1
-1
0
3
-1
0
1
2
3
4
5
6
0
0 is the “root” of
this set.
4
6
3
1
5
2
{0, 1, 2, 4}
{3, 5}
{6}
Improving
the
Connect
Operation
connect(5, 2)
●
●
●
Find root(5). // returns 3
Find root(2). // returns 0
Set root(5)’s value equal to root(2).
parent
-1
0
1
0
0
3
-1
0
1
2
3
4
5
6
0
0 is the “root” of
this set.
4
2
{0, 1, 2, 4}
6
3
1
5
{3, 5}
{6}
Set Union Using Rooted-Tree Representation
connect(5, 2)
●
Make root(5) into a child of root(2).
parent
-1
0
1
0
0
3
-1
0
1
2
3
4
5
6
What are the potential performance issues with this approach?
●
Compared to QuickFind, we have to climb up a tree.
0
0 is the “root” of
this set.
4
6
3
1
5
2
{0, 1, 2, 4,
3, 5}
{6}
Set Union Using Rooted-Tree Representation
connect(5, 2)
●
Make root(5) into a child of root(2).
parent
-1
0
1
0
0
3
-1
0
1
2
3
4
5
6
What are the potential performance issues with this approach?
●
Tree can get too tall! root(x) becomes expensive.
0
0 is the “root” of
this set.
4
5
2
{0, 1, 2, 4,
6
3
1
3, 5}
{6}
The Worst Case
If we always connect the first item’s tree below the second item’s tree, we can
end up with a tree of height M after M operations:
0
●
●
●
●
connect(4, 3)
connect(3, 2)
connect(2, 1)
connect(1, 0)
1
2
3
4
For N items, what’s the worst case runtime…
For connect(p, q)?
● For isConnected(p, q)?
●
The Worst Case
If we always connect the first item’s tree below the second item’s tree, we can
end up with a tree of height M after M operations:
●
●
●
●
connect(4, 3)
connect(3, 2)
connect(2, 1)
connect(1, 0)
0
1
2
3
For N items, what’s the worst case runtime…
● For connect(p, q)? Θ(N)
● For isConnected(p, q)? Θ(N)
4
QuickUnionDS
public class QuickUnionDS implements DisjointSets {
private int[] parent;
public QuickUnionDS(int N) {
parent = new int[N];
for (int i = 0; i < N; i++)
{ parent[i] = -1; }
For N items, this means a worst case runtime of Θ(N).
}
private int find(int p) {
int r = p;
while (parent[r] >= 0)
{ r = parent[r]; }
return r;
}
Here the find operation is the same as
the “root(x)” idea we had in earlier slides.
public boolean isConnected(int p, int q) {
return find(p) == find(q);
}
{
int i = find(p);
int j = find(q);
parent[i] = j;
}
Performance Summary
Implementation
Constructor
connect
isConnected
ListOfSetsDS
Θ(N)
O(N)
O(N)
QuickFindDS
Θ(N)
Θ(N)
Θ(1)
QuickUnionDS
Θ(N)
O(N)
O(N)
Using O because runtime can
be between constant and linear.
QuickFindDS defect: QuickFindDS is too slow: Connecting takes Θ(N) time.
QuickUnion defect: Trees can get tall. Results in potentially even worse
performance than QuickFind if tree is imbalanced.
● Observation: Things would be fine if we just kept our tree balanced.
Weighted Quick Union
A Choice of Two Roots
Suppose we are trying to connect(2, 5). We have two choices:
A. Make 5’s root into a child of 2’s root.
B. Make 2’s root into a child of 5’s root.
0
1
Which is the better choice?
4
Height: 2
3
2
5
0
1
4
+
2
3
3
0
5
4
5
Height: 3
1
2
A Choice of Two Roots
Suppose we are trying to connect(2, 5). We have two choices:
A. Make 5’s root into a child of 2’s root.
B. Make 2’s root into a child of 5’s root.
0
1
Which is the better choice?
4
Height: 2
3
2
5
0
1
+
4
3
3
0
5
2
5
4
Height: 3
1
2
A Choice of Two Roots
Suppose we are trying to connect(2, 5). We have two choices:
A. Make 5’s root into a child of 2’s root.
B. Make 2’s root into a child of 5’s root.
0
1
Which is the better choice?
4
0
3
2
2
5
4
3
+
0
1
5
Height: 2
3
4
5
Height: 3
1
2
Improvement #1: Weighted QuickUnion
Modify quick-union to avoid tall trees.
●
●
Track tree size (number of elements).
New rule: Always link root of smaller tree to larger tree.
New rule: If we call connect(3, 8), which entry (or entries) of parent[] changes?
A.
B.
C.
D.
0
parent[3]
parent[0]
parent[8]
parent[6]
parent
1
2
3
4
Note: The rule I picked is based on weight,
not height. We’ll talk about why soon.
-1
0
0
0
0
0
0
6
6
8
0
1
2
3
4
5
6
7
8
9
Implementing WeightedQuickUnion
Minimal changes needed:
●
●
●
Use parent[] array as before.
isConnected(int p, int q) requires no changes.
connect(int p, int q) needs to somehow keep track of sizes.
○
○
See the Disjoint Sets lab for the full details.
Two common approaches:
■ Use values other than -1 in parent array for root nodes to track size.
■
parent
Create a separate size array.
-6
0
0
0
0
0
-4
6
6
8
0
1
2
3
4
5
6
7
8
9
size
10
1 1 1 1 1 4 1 2 1
0
1
2
3
4
5
6
7
8
9
6
5
7
8
9
Weighted Quick Union Performance
Let’s consider the worst case where the tree height grows as fast as possible.
0
1
N
H
1
0
2
1
Weighted Quick Union Performance
Let’s consider the worst case where the tree height grows as fast as possible.
0
2
1
3
N
H
1
0
2
1
Weighted Quick Union Performance
Let’s consider the worst case where the tree height grows as fast as possible.
0
N
H
1
0
Weighted Quick Union Performance
Let’s consider the worst case where the tree height grows as fast as possible.
0
1
2
3
N
H
1
0
2
1
4
2
Weighted Quick Union Performance
Let’s consider the worst case where the tree height grows as fast as possible.
4
0
1
2
N
H
1
0
2
1
4
2
5
3
Weighted Quick Union Performance
Let’s consider the worst case where the tree height grows as fast as possible.
4
0
1
2
3
5
N
H
1
0
2
1
4
2
6
7
Weighted Quick Union Performance
Let’s consider the worst case where the tree height grows as fast as possible.
4
0
1
2
5
3
N
H
1
0
2
1
4
2
6
7
Weighted Quick Union Performance
Let’s consider the worst case where the tree height grows as fast as possible.
0
1
2
4
3
5
6
7
N
H
1
0
2
1
4
2
8
3
Weighted Quick Union Performance
Let’s consider the worst case where the tree height grows as fast as possible.
●
N
H
1
0
2
1
4
2
8
3
16
4
Worst case tree height is Θ(log N).
0
1
2
4
3
5
8
6
7
9
10
12
11
13
14
15
Performance Summary
Implementation
Constructor
connect
isConnected
ListOfSetsDS
Θ(N)
O(N)
O(N)
QuickFindDS
Θ(N)
Θ(N)
Θ(1)
QuickUnionDS
Θ(N)
O(N)
O(N)
WeightedQuickUnionDS
Θ(N)
O(log N)
O(log N)
QuickUnion’s runtimes are O(H), and WeightedQuickUnionDS height is given
by H = O(log N). Therefore connect and isConnected are both O(log N).
By tweaking QuickUnionDS we’ve achieved logarithmic time performance.
●
Fast enough for all practical problems.
Why Weights Instead of Heights?
We used the number of items in a tree to decide upon the root.
●
Why not use the height of the tree?
○
Worst case performance for HeightedQuickUnionDS is asymptotically the same! Both are
Θ(log(N)).
○
Resulting code is more complicated with no performance gain.
0
1
2
3
1
6
4
5
+
7
2
0
3
8
9
(Briefly) Path Compression
4
5
6
7
8
9
What We’ve Achieved
Implementation
Constructor
connect
isConnected
ListOfSetsDS
Θ(N)
O(N)
O(N)
WeightedQuickUnionDS
Θ(N)
O(log N)
O(log N)
Performing M operations on a DisjointSet object with N elements:
●
●
●
For our naive implementation, runtime is O(MN).
For our best implementation, runtime is O(N + M log N).
For N = 109 and M = 109, difference is 30 years vs. 6 seconds.
○
●
Key point: Good data structure unlocks solutions to problems that could otherwise not
be solved!
Good enough for all practical uses, but could we theoretically do better?
Suppose we have a ListOfSetsDS implementation of Disjoint Sets.
Suppose that it has 1000 items, i.e. N = 1000.
Suppose we perform a total of 150 connect operations and 212 isConnected
operations.
●
M = 150 + 212 = 362 operations
So when we say O(NM), we’re saying it’ll take no more than 1000 * 362 units of
time (in some arbitrary unit of time).
●
This is a bit informal. O is really about asymptotics, i.e. behavior for very
large N and M, not specific N and Ms that we pick.
170 Spoiler: Path Compression: A Clever Idea
Below is the topology of the worst case if we use WeightedQuickUnion.
●
Clever idea: When we do isConnected(15, 10), tie all nodes seen to the root.
○
Additional cost is insignificant (same order of growth).
0
1
5
11
2
6
12
7
8
13
3
9
4
10
14
15
Path Compression: Theoretical Performance (Bonus)
Path compression results in a union/connected operations that are very very close to
amortized constant time.
●
●
●
M operations on N nodes is O(N + M lg* N).
A tighter bound: O(N + M α(N)), where α is the inverse Ackermann function.
The inverse ackermann function is less than 5 for all practical inputs!
N
○ See “Efficiency of a Good But Not Linear Set Union Algorithm.”
○ Written by Bob Tarjan while at UC Berkeley in 1975.
1
0
15
11
5
1
12
6
2
7
8
3
9
10
4
α(N)
0
...
1
...
2
...
3
...
4
5
13
14
170 Spoiler: Path Compression: A Clever Idea
Below is the topology of the worst case if we use WeightedQuickUnion
●
Clever idea: When we do isConnected(15, 10), tie all nodes seen to the root.
○
Additional cost is insignificant (same order of growth).
0
1
5
11
6
12
2
7
8
13
3
9
4
10
14
15
Path Compression: Another Clever Idea
Below is the topology of the worst case if we use WeightedQuickUnion
● Clever idea: When we do isConnected(15, 10), tie all nodes seen to the root.
○ Additional cost is insignificant (same order of growth).
0
15
11
5
1
12
6
13
2
7
8
14
3
9
10
4
Path Compression: Another Clever Idea
Draw the tree after we call isConnected(14, 13).
0
15
11
5
1
12
6
2
7
8
13
3
10
4
3
10
4
9
14
Path Compression: Another Clever Idea
Draw the tree after we call isConnected(14, 13).
0
15
11
5
1
12
6
13
2
7
8
14
9
Path Compression: Another Clever Idea
Draw the tree after we call isConnected(14, 13).
0
15
11
5
1
12
6
2
7
8
13
3
10
4
3
10
4
9
14
Path Compression: Another Clever Idea
Draw the tree after we call isConnected(14, 13).
0
15
11
5
12
1
13
6
7
14
8
2
9
170 Spoiler: Path Compression: A Clever Idea
Path compression results in a union/connected operations that are very very
close to amortized constant time (amortized constant means “constant on
average”).
●
●
M operations on N nodes is O(N + M lg* N) - you will see this in CS170.
N
lg* N
lg* is less than 5 for any realistic input.
1
0
0
15
11
5
12
1
6
2
7
8
3
4
10
9
2
13
2
1
4
2
16
3
65536
4
265536
5
16
14
Path Compression: Theoretical Performance (Bonus)
Path compression results in a union/connected operations that are very very close to
amortized constant time.
●
●
●
M operations on N nodes is O(N + M lg* N).
A tighter bound: O(N + M α(N)), where α is the inverse Ackermann function.
The inverse ackermann function is less than 5 for all practical inputs!
N
○ See “Efficiency of a Good But Not Linear Set Union Algorithm.”
○ Written by Bob Tarjan while at UC Berkeley in 1975.
1
0
15
11
5
1
12
6
2
7
8
3
9
10
4
α(N)
0
...
1
...
2
...
3
...
4
5
13
14
A Summary of Our Iterative Design Process
And we’re done! The end result of our iterative design process is the standard way
disjoint sets are implemented today - quick union and path compression.
The ideas that made our implementation efficient:
●
Represent sets as connected components (don’t track individual connections).
○
ListOfSetsDS: Store connected components as a List of Sets (slow, complicated).
○
QuickFindDS: Store connected components as set ids.
○
QuickUnionDS: Store connected components as parent ids.
■
WeightedQuickUnionDS: Also track the size of each set, and use size to decide on new
tree root.
●
WeightedQuickUnionWithPathCompressionDS: On calls to connect and
isConnected, set parent id to the root for all items seen.
Performance Summary
Implementation
Runtime
ListOfSetsDS
O(NM)
QuickFindDS
Θ(NM)
QuickUnionDS
O(NM)
WeightedQuickUnionDS
O(N + M log N)
WeightedQuickUnionDSWithPathCompression
O(N + M α(N))
Runtimes are given assuming:
● We have a DisjointSets object of size N.
● We perform M operations, where an operation is defined as either a call to
connected or isConnected.
Citations
Hilf Sp20 and Hug Fa20 slides :)
The proof of the inverse ackermann runtime for disjoint sets is given here:
http://www.uni-trier.de/fileadmin/fb4/prof/INF/DEA/Uebungen_LVA-Ankuendi
gungen/ws07/KAuD/effi.pdf
as originally proved by Tarjan here at UC Berkeley in 1975.
Are you getting enough sleep?
●
●
●
●
●
●
1 in 3 adults do not get enough sleep.
This leads to long term diseases and lower cognitive function, while tired.
Teen (13–18 years old) should be getting 8–10 hours per 24 hours
Adult (18–60 years old) should be getting 7 or more hours per night
It is important to maintain consistency in a bed time (even on the
weekends) to ensure good sleep quality.
Source: https://www.cdc.gov/sleep/features/getting-enough-sleep.html
Lecture 5
CS61BL Summer 2021
-
GitBugs for debugging
-
Announcements
Monday, July 19th
-
See resources/GitBugs guide
Gitlet due this Friday
Redemption quiz 5 today
Redemption tutoring for quiz
6 today
Redemption quiz 6 tomorrow
Quiz 7 on Wednesday, Quiz 8
on Friday
Lab as normal
Binary Search
Our goal:
-
You have a group of items with some natural ordering (e.g. numbers or
letters). In other words, the items are comparable.
You want to be able to quickly add items to the structure. You also want
to be able to quickly check if your structure contains a given item.
Idea 1: An ordered list, starting at the front
A
B
D
L
M
X
Z
Contains(M)
Contains(G)
Insert(Y)
Idea 1: An ordered list, starting at the front
A
Contains() is in O(n)
Insert() is also in O(n).
Can we do better?
B
D
L
M
X
Z
Idea 2: An ordered list, starting at the middle
A
B
D
L
M
X
Z
Contains(M)
Contains(G)
Insert(Y)
Idea 2: An ordered list, starting at the middle
A
B
D
L
M
X
Z
Because we start at the middle, we can cut out half of the list.
However, O(n/2) is the same as O(n)! So we’re still linear… Can we continue improving?
Trees
Binary Search Trees
-
Last week, we learned what a tree is, in general
Now, let’s take those ideas and look at a specific type of tree: a binary
search tree!
We’ll apply the idea we saw in our first improvement: narrow down your
search and continually cut down the amount of items we have to look at.
Idea 3: A tree!
A
B
D
L
M
X
Z
L
B
X
D
A
M
Z
Idea 3: A tree!
Contains(M)
L
Contains(G)
B
A
X
D
M
Insert(Y)
Z
We’ll formally analyze the runtime of
these operations in a moment.
Binary Search Trees
Invariants:
○ Left child’s value must be less than root’s value
○ Right child’s value must be greater than root’s value
Insert(Item):
○ Start at root. If your item is greater than current node’s item, go right. If it’s
less, go left. Keep doing this until you hit the end!
Contains(Item):
○ Start at root. If your item is greater than current node’s item go right. If it’s
less, go left. Keep going until you find your item that you’re looking for. If you
reach a leaf and haven’t found it, its not present.
Let’s look at some examples
●
●
●
●
Examples
X
2
0
1
1
0
4
2
0
2
0
3
2
0
1
1
1
0
2
0
6
3
1
2
8
5
9
7
Question for the chat: are these both valid trees?
0
1
0
3
1
2
6
3
5
4
Binary Search Trees
● What does inserts runtime depend on?
○ Answers:
● What does contains runtime depend on?
○ Answers:
● Best case? Worst case? Average case?
○ What do these trees look like?
2
4
5
6
Question for the chat: which one would you prefer?
0
1
0
3
1
2
6
3
2
4
6
5
5
4
Tree Height
0
1
-
-
Insert and contains both depend on the height of
the tree. If both trees have N items, how do their
heights compare?
For the spindly tree, we see that each layer has a
single item. Then with N items, we have N layers in
our tree.
3
2
6
5
4
0
1
3
2
4
5
6
Tree Height
For the bushy tree:
- Node 0 is the root a tree with N items. Node 1 is
the root of a subtree with roughly N/2 items. Node
2 is the same.
- For each node, its rooting a subtree that is half the
size of the subtree at the layer before.
- We start with N, and each layer we divide by 2.
Then a node at layer k is the root of a subtree of
size N/2ᵏ.
- How many layers are there until we reach a leaf?
- In other words, how many times do we have to
divide N by 2 until we reach 1 (a leaf is a root of a
subtree with only 1 element)
- N/2ᵏ = 1
- N = 2ᵏ
- k = log₂(N)
Tree Height
-
0
1
3
2
6
5
4
0
1
2
3
4
6
5
0
1
In conclusion, an extremely spindly tree would
have a height bounded by O(N)
A perfectly bushy tree would have a height
bounded by O(logN)
3
2
6
How does this affect the best/worst case analysis of our
methods like insert() and contains()?
5
4
0
1
3
2
4
5
6
Best case analysis: contains()
0
Which of the following describes the best
case?
1
A)
3
B)
2
6
1
5
3
4
C)
0
D)
2
4
5
6
The best case is when the tree only
has one element, and we get Θ(1)
runtime no matter what.
The best case is when the element
we’re looking for is the root, and we
get Θ(1) runtime
The best case is when the element
is not present in the tree, and we
get Θ(1) runtime.
The best case is when the element
is not present in the tree, and we
get Θ((log(N)) runtime for the tree
on the right, and Θ(N) for the tree
on the left.
Worst case analysis: contains()
0
What describes the worst case?
1
A)
3
B)
2
0
6
1
5
4
3
C)
2
4
5
6
The worst case is when the element
is not present, and we have to look
at every node in the tree to see if its
the one we want. This is Θ(n) for
both trees.
The worst case is when there are a
LOT of elements in the tree, so it
takes longer to look through them.
The worst case is when the element
is not present, and we have to
traverse the entire height of the
tree. This is Θ(N) for the left tree,
and Θ(logN) for the right.
..
.
... oops
What’s the fundamental issue?
● Binary trees are “lazy” about where to insert data
○ The shape of the tree is highly dependent on the order
of insertions.
● Why don’t we just randomize before we insert?
○ Get Θ(log(n)) height!! Great! Right?
○ We often do not have all the data before starting the
program.
● How can we do better?
Self Balancing Growth
B-Trees
● Solution attempt number one:
○ Overstuff the nodes! (to a certain extent)
○ The tree can never be imbalanced
7
2
5
7
10
5
8
12
7
3
2
10
3
8
10
3
12 13
2
5
8
12 13 15
B-Trees
B-Trees are trees that can have more than one item per node (and more
than two children per node). They are most popular in two specific contexts:
Order refers to the maximum number of children
● Small order (3 or 4):
a single node can have.
○ Used as a conceptually simple balanced search tree (as today).
● Very large order (say thousands)
○ Used in practice for databases and filesystems (i.e. systems with
very large records).
■ See CS186
Note: Since the small order tree isn’t practically useful (as we’ll see in a few
slides), people usually only use the name B-tree to refer to the very large
order case.
2-3 Trees
● A B-Tree of order 3 is called a 2-3 tree
● The name comes from the invariant that a single node can
have 2 or 3 children
○ Each node can have 1-2 entries
● Every non-leaf node must have one more child than it has
entries
The origin of "B-tree" has never been explained by the authors. As we shall
see, "balanced," "broad," or "bushy" might apply. Others suggest that the
"B" stands for Boeing. Because of his contributions, however, it seems
appropriate to think of B-trees as "Bayer"-trees.
- Douglas Corner (The Ubiquitous B-Tree)
2-3 Tree Structure
20
10 1018
1
3
17
40
55
26
19
22
39
41
66
2-3 Tree Operations: get
● V get(K k):
○ At each node, check for a key matching k.
○ If not found, move down to the appropriate child by comparing k
against the keys in the node.
○ Continue until k is found, or at a leaf not containing k.
get(3)
20
10 10 18
1
3
17
40
55
26
19
22
39
41
66
2-3 Tree Operations: get
● V get(K k):
○ At each node, check for a key matching k.
○ If not found, move down to the appropriate child by comparing k
against the keys in the node.
○ Continue until k is found, or at a leaf not containing k.
get(3)
20
10 10 18
1
3
17
40
55
26
19
22
39
41
66
2-3 Tree Operations: get
● V get(K k):
○ At each node, check for a key matching k.
○ If not found, move down to the appropriate child by
comparing k against the keys in the node.
○ Continue until k is found, or at a leaf not containing k.
get(3)
20
10 10 18
1
3
17
40
55
26
19
22
39
41
66
2-3 Tree Operations: get
● V get(K k):
○ At each node, check for a key matching k.
○ If not found, move down to the appropriate child by comparing k
against the keys in the node.
○ Continue until k is found, or at a leaf not containing k.
get(3)
20
10 10 18
1
3
17
40
55
26
19
22
39
41
66
2-3 Tree Operations: get
● V get(K k):
○ At each node, check for a key matching k.
○ If not found, move down to the appropriate child by comparing k
against the keys in the node.
○ Continue until k is found, or at a leaf not containing k.
get(3)
20
10 10 18
1
3
17
40
55
26
19
22
39
41
66
2-3 Tree Operations: add
● add(K k, V v): (inserting key value pair)
○ Walk down the tree like we’re doing get(k).
○ When reaching a leaf, add the key to its ordered position.
○ If the resulting leaf node has 3 entries, “split” the node by sending the
middle element up.
○ Repeat if this creates more 3-entry-nodes.
insert(8)
20
10 10 18
1
3
17
40
55
26
19
22
39
41
66
2-3 Tree Operations: add
● add(K k, V v): (inserting key value pair)
○ Walk down the tree like we’re doing get(k).
○ When reaching a leaf, add the key to its ordered position.
○ If the resulting leaf node has 3 entries, “split” the node by sending the
middle element up.
○ Repeat if this creates more 3-entry-nodes.
insert(8)
20
10 10 18
1
3
17
40
55
26
19
22
39
41
66
2-3 Tree Operations: add
● add(K k, V v): (inserting key value pair)
○ Walk down the tree like we’re doing get(k).
○ When reaching a leaf, add the key to its ordered position.
○ If the resulting leaf node has 3 entries, “split” the node by sending the
middle element up.
○ Repeat if this creates more 3-entry-nodes.
insert(8)
20
10 10 18
1
3
17
40
55
26
19
22
39
41
66
2-3 Tree Operations: add
● add(K k, V v): (inserting key value pair)
○ Walk down the tree like we’re doing get(k).
○ When reaching a leaf, add the key to its ordered position.
○ If the resulting leaf node has 3 entries, “split” the node by sending the
middle element up.
○ Repeat if this creates more 3-entry-nodes.
insert(8)
20
10 10 18
1
3
17
40
55
26
19
22
39
41
66
2-3 Tree Operations: add
● add(K k, V v): (inserting key value pair)
○ Walk down the tree like we’re doing get(k).
○ When reaching a leaf, add the key to its ordered position.
○ If the resulting leaf node has 3 entries, “split” the node by sending the
middle element up.
○ Repeat if this creates more 3-entry-nodes.
insert(8)
20
40
We’ve reached a leaf
node. Add the item here!
10 10 18
1
3
17
55
26
19
22
39
41
66
2-3 Tree Operations: add
● add(K k, V v): (inserting key value pair)
○ Walk down the tree like we’re doing get(k).
○ When reaching a leaf, add the key to its ordered position.
○ If the resulting leaf node has 3 entries, “split” the node by sending the
middle element up.
○ Repeat if this creates more 3-entry-nodes.
insert(8)
20
10 10 18
1
3
8
17
40
55
26
19
22
39
41
66
2-3 Tree Operations: add
● add(K k, V v): (inserting key value pair)
○ Walk down the tree like we’re doing get(k).
○ When reaching a leaf, add the key to its ordered position.
○ If the resulting leaf node has 3 entries, “split” the node by
sending the middle element up.
○ Repeat if this creates more 3-entry-nodes.
insert(8)
20
40
This node has too many
items. Let’s split the node!
10 10 18
1
8
3
17
55
26
19
22
39
41
66
2-3 Tree Operations: add
● add(K k, V v): (inserting key value pair)
○ Walk down the tree like we’re doing get(k).
○ When reaching a leaf, add the key to its ordered position.
○ If the resulting leaf node has 3 entries, “split” the node by
sending the middle element up.
○ Repeat if this creates more 3-entry-nodes.
insert(8)
20
3
1
10 10 18
8
17
40
55
26
19
22
39
41
66
2-3 Tree Operations: add
● add(K k, V v): (inserting key value pair)
○ Walk down the tree like we’re doing get(k).
○ When reaching a leaf, add the key to its ordered position.
○ If the resulting leaf node has 3 entries, “split” the node by sending the
middle element up.
○ Repeat if this creates more 3-entry-nodes.
insert(8)
20
40
This node has too many
items. Let’s split the node!
3
10 10 18
8
1
17
55
26
19
22
39
41
66
2-3 Tree Operations: add
● add(K k, V v): (inserting key value pair)
○ Walk down the tree like we’re doing get(k).
○ When reaching a leaf, add the key to its ordered position.
○ If the resulting leaf node has 3 entries, “split” the node by sending the
middle element up.
○ Repeat if this creates more 3-entry-nodes.
insert(8)
10
3
1
20
18
8
17
40
55
26
19
22
39
41
66
2-3 Tree Operations: add
● add(K k, V v): (inserting key value pair)
○ Walk down the tree like we’re doing get(k).
○ When reaching a leaf, add the key to its ordered position.
○ If the resulting leaf node has 3 entries, “split” the node by sending the
middle element up.
○ Repeat if this creates more 3-entry-nodes.
insert(8)
10
20
40
This node has too many
items. Let’s split the node!
3
18
8
1
17
55
26
19
22
39
41
66
2-3 Tree Operations: add
insert(8)
20
Balanced! :-)
10
3
1
40
18
8
17
55
26
19
22
39
41
66
Perfect Balance
Observation: 2-3 Trees have perfect balance.
● If we split the root, every node gets pushed down by exactly one level.
● If we split a leaf node or internal node, the height doesn’t change.
● Height increases only if the root is split
We will never get unbalanced trees that
look like the ones below! We don’t have
time to go over why, but I suggest you
try creating some imbalanced trees on
your own and see what they end up
looking like.
7
2
1
10 13
5
8
12
G
15 17
AB
G
X
B
C
A
X
C
2-3 Tree Runtimes
● Because of this balance, the height H of a 2-3 Tree is between
log3(N) and log2(N)
● Get: Traverse down the tree to find item. Worst case: Θ(H) or Θ(log
N)
● Insert: Traverse down the tree and put the item in the correct leaf.
Max number of times we split nodes per insert is ~H, so worst case
is still Θ(2H) or Θ(log N)
● Runtimes are pretty good!
7 13
10
3
Max 2 items per node.
Max 3 non-null children per node.
2
5
8
11
16 20
15
17
21
But there’s a slight problem...
●
Though the runtimes are good, 2-3 Trees can be a real pain to implement
○
●
The remove() operation (omitted) can be hard to code
“Beautiful algorithms are, unfortunately, not always the most useful” Knuth
Lets look at some other alternatives.
BST Structure and
Tree Rotation
BSTs
Suppose we have a BST with the numbers 1, 2, 3. There are five possible BSTs.
● The specific BST you get is based on the insertion order.
● More generally, for N items, there are Catalan(N) different BSTs.
1
1
3
3
2
2
3
1
1
3
2
3
2
2
1
Given any BST, it is possible to move to a different configuration using
“rotation”.
Tree Rotation Definition
rotateLeft(G): Let x be the right child of G. Make G the new left child of x.
● Preserves search tree property. No change to semantics of tree.
● Can think of as temporarily merging G and P, then sending G down
and left.
G
C
P
A
k
B
P
GP
j
r
l
G
C
A
k
B
j
C
r
l
For this example rotateLeft(G) increased height of the tree!
r
A
k
j
B
l
Your Turn
rotateRight(P): Let x be the left child of P. Make P the new right child of x.
● Can think of as temporarily merging G and P, then sending P down
and right.
P
G
r
C
k
A
j
l
B
Your Turn
rotateRight(P): Let x be the left child of P. Make P the new right child of x.
● Can think of as temporarily merging G and P, then sending P down
and right.
● Note: k was G’s right child. Now it is P’s left child.
P
G
G
r
C
A
k
j
C
P
A
l
k
B
j
r
l
B
For this example rotateRight(P) decreased height of tree!
Rotation for Balance
Rotation:
● Can shorten (or lengthen) a tree.
● Preserves search tree property.
D
B
rotateRight(D)
B
D
>D
<B
<B
rotateLeft(B)
> B and < D
> B and < D
>D
Rotation for Balance
Rotation:
● Can shorten (or lengthen) a tree.
● Preserves search tree property.
D
B
rotateRight(D)
B
D
>D
<B
<B
> B and < D
rotateLeft(B)
> B and < D
Can use rotation to balance a BST: Demo
● Rotation allows balancing of a BST in O(N) moves.
>D
Rotation for Balance
Rotation:
● Can shorten (or lengthen) a tree.
● Preserves search tree property.
D
B
rotateRight(D)
B
D
>D
<B
<B
> B and < D
rotateLeft(B)
Red-Black Trees
> B and < D
>D
Search Trees
There are many types of search trees:
● Binary search trees: Can balance using rotation, but we have no
algorithm for doing so (yet).
● 2-3 trees: Balanced by construction, i.e. no rotations required, but very
hard to implement.
Let’s try something clever, but strange.
Our goal: Build a BST that is structurally identical to a 2-3 tree.
● Since 2-3 trees are balanced, so will our special BSTs.
Red-Black trees
● We can use red-black trees to help us more easily implement this
idea of 2-3 and 2-3-4 trees.
● Generally, red-black trees are binary search trees that color edges
red or black in order to provide some form of organization to the
nodes.
○
○
Red: the nodes connected by this edge in our red-black tree are in the same node
in the corresponding B-Tree
Black: regular node
● Red-black trees have a correspondence with 2-3-4 trees, while
left-leaning red-black trees have a 1-1 correspondence with 2-3 trees.
○
We will be discussing left-leaning red black trees for the remainder of this lecture.
Representing a 2-3 Tree as a BST
A 2-3 tree with only 2-nodes is trivial.
● BST is exactly the same!
m
m
o
e
b
g
n
o
e
p
b
g
n
p
What do we do about 3-nodes?
m
b
e
????
o
df
g
n
p
Dealing with 3-Nodes
Possibility 1: Create dummy “glue” nodes.
m
m
o
o
df
d
b
e
g
n
f
n
p
b
e
g
Result is inelegant. Wasted link. Code will be ugly.
df
d
f
p
Dealing with 3-Nodes
Possibility 2: Create “glue” links with the smaller item off to the left.
m
m
b
e
f
o
df
g
n
p
o
d
b
n
g
e
Idea is commonly used in practice (e.g. java.util.TreeSet).
f
df
d
For convenience, we’ll mark glue links as “red”.
Left-Leaning Red Black Binary Search Tree (LLRB)
A BST with left glue links that represents a 2-3 tree is often called a “Left
Leaning Red Black Binary Search Tree” or LLRB.
●
●
●
LLRBs are normal BSTs!
There is a 1-1 correspondence between an LLRB and an equivalent 2-3 tree.
The red is just a convenient fiction. Red links don’t “do” anything special.
p
Left-Leaning Red Black Binary Search Tree (LLRB)
Draw the LLRB corresponding to the 2-3 tree shown below.
uw
as
v
xy
Left-Leaning Red Black Binary Search Tree (LLRB)
Draw the LLRB corresponding to the 2-3 tree shown below.
w
uw
u
as
v
y
xy
s
a
v
x
Left-Leaning Red Black Binary Search Tree (LLRB)
Draw the LLRB corresponding to the 2-3 tree shown below.
w
uw
u
as
y
xy
v
s
v
x
a
Searching an LLRB tree for a
key is easier than a 2-3 tree.
● Treat it exactly like any
BST.
LLRB Height
Suppose we have a 2-3 tree of height H.
● What is the maximum height of the corresponding LLRB?
L
DE
B
A
P
G
C
F
J
H
I
SU
N
K
M O
QR
T
VW
LLRB Height
Suppose we have a 2-3 tree of height H.
● What is the maximum height of the corresponding LLRB?
○
Total height is H (black) + H + 1 (red) = 2H + 1.
Worst case would be if these
were both 3 nodes.
L
DE
B
A
P
G
C
F
J
H
I
SU
N
K
M O
QR
T
VW
Left-Leaning Red Black Tree (LLRB) Properties
Some handy LLRB properties:
● No node has two red links [otherwise it’d be analogous to a 4 node, which
are disallowed in 2-3 trees].
● Every path from root to null reference has same number of black links
[because 2-3 trees have the same number of links to every leaf]. LLRBs
are therefore balanced.
Left-Leaning Red Black Tree (LLRB) Properties
Some handy LLRB properties:
● No node has two red links [otherwise it’d be analogous to a 4 node, which
are disallowed in 2-3 trees].
● Every path from root to null reference has same number of black links
[because 2-3 trees have the same number of links to every leaf]. LLRBs
are therefore balanced.
G
C
G
G
X
B
B
A
A
Invalid, B has two red links.
B
X
C
Invalid, not
black balanced.
A
G
X
C
Invalid, not
black balanced.
B
A
X
C
Valid
Left-Leaning Red Black Tree (LLRB) Properties
One last important question: Where do LLRBs come from?
● Typically would not make sense to build a 2-3 tree, then convert. Even
more complex.
● Instead, it turns out we implement an LLRB insert as follows:
○
○
Insert as usual into a BST.
Use zero or more rotations to maintain the 1-1 mapping.
Maintaining 1-1
Correspondence Through
Rotations
The 1-1 Mapping
There exists a 1-1 mapping between:
● 2-3 Tree
● LLRB
Implementation of an LLRB is based on maintaining this 1-1 correspondence.
● When performing LLRB operations, pretend like you’re a 2-3 tree.
● Preservation of the correspondence will involve tree rotations.
Design Task #1: Insertion Color
Should we use a red or black link when inserting?
S
(E)
d
ad
E
S
ad
d(E
S
)
E
LLRB World
add(E)
S
ES
World 2-3
Design Task #1: Insertion Color
Should we use a red or black link when inserting?
S
(E)
d
ad
E
S
ad
d(E
S
)
E
LLRB World
S
World 2-3
add(E)
ES
Use red! In 2-3 trees new
values are ALWAYS added to
an existing leaf node (at first).
Design Task #2: Insertion on the Right
Suppose we have leaf E, and insert S with a red link. What is the problem below,
and what do we do about it?
B
A
B
add(S)
E
A
E
S
LLRB World
B
A
B
add(S)
A
E
ES
World 2-3
Design Task #2: Insertion on the Right
Suppose we have leaf E, and insert S with a red link. What is the problem below,
and what do we do about it: Red right links aren’t allowed, so rotateLeft(E).
B
A
B
add(S)
E
rotateLeft(E)
A
E
A
S
LLRB World
B
A
World 2-3
B
add(S)
E
A
B
ES
S
E
New Rule: Representation of Temporary 4-Nodes
We will represent temporary 4-nodes as BST nodes with two red links.
● This state is only temporary (more soon), so temporary violation of
“left leaning” is OK.
B
B
A
Represents
temporary 4 nodes.
Temporarily violates
“no red right links”.
add(Z)
S
A
S
E
E
Z
LLRB World
B
B
add(Z)
A
Temporarily violates
“no 4 nodes”.
A
ES
ESZ
World 2-3
Design Task #3: Double Insertion on the Left
Suppose we have the LLRB below and insert E. We end up with the wrong
representation for our temporary 4 node. What should we do so that the
temporary 4 node has 2 red children (one left, one right) as expected?
B
B
add(E)
A
Z
A
Z
S
S
E
LLRB World
B
A
World 2-3
B
add(E)
SZ
A
ESZ
Design Task #3: Double Insertion on the Left
What should we do so that the temporary 4 node has 2 red children (one left,
one right) as expected: Rotate Z right.
B
B
B
add(E)
A
Z
A
rotateRight(Z)
Z
S
A
S
S
E
Z
E
LLRB World
B
B
add(E)
A
A
SZ
ESZ
World 2-3
Design Task #4: Splitting Temporary 4-Nodes
Suppose we have the LLRB below which includes a temporary 4 node. What
should we do next?
● Try to figure this one out! It’s a particularly interesting puzzle.
G
B
A
X
C
Hint: Ask yourself “What Would
2-3 Tree Do?” WW23TD?
LLRB World
G
World 2-3
ABC
BG
split(A/B/C)
X
A
C
X
Design Task #4: Splitting Temporary 4-Nodes
Suppose we have the LLRB below which includes a temporary 4 node. What
should we do next?
● Flip the colors of all edges touching B.
G
G
flip(B)
B
A
X
B
C
A
X
C
Note: This doesn’t change the
BST structure/shape!
LLRB World
G
ABC
World 2-3
BG
split(A/B/C)
X
A
C
X
… and That’s It!
Congratulations, you just invented the red-black BST.
● When inserting: Use a red link.
● If there is a right leaning “3-node”, we have a Left Leaning Violation.
○
●
If there are two consecutive left links, we have an Incorrect 4 Node
Violation.
○
●
Rotate left the appropriate node to fix.
Rotate right the appropriate node to fix.
If there are any nodes with two red children, we have a Temporary 4 Node.
○
Color flip the node to emulate the split operation.
One last detail: Cascading operations.
● It is possible that a rotation or flip operation will cause an additional
violation that needs fixing.
Optional Exercises
Cascading Balance Example
Inserting Z gives us a temporary 4 node.
● Color flip yields an invalid tree. Why? What’s next?
B
B
A
add(Z)
S
B
A
flip(S)
S
E
E
A
S
Z
E
Z
LLRB World
B
A
World 2-3
B
add(Z)
ES
A
BS
split(E/S/Z)
ESZ
A
E
Z
Cascading Balance Example
Inserting Z gives us a temporary 4 node.
● Color flip yields an invalid tree. Why? What’s next?
● We have a right leaning 3-node (B-S). We can fix with rotateLeft(b).
B
S
rotateLeft(B)
A
S
E
B
Z
A
Z
E
LLRB World
BS
World 2-3
A
E
Z
Insertion of 7 through 1
To get an intuitive understanding of why all this works, try inserting the 7, 6,
5, 4, 3, 2, 1, into an initially empty LLRB.
● You should end up with a perfectly balanced BST!
To check your work, see this demo.
4
2
1
6
3
5
7
LLRB Runtime and
Implementation
LLRB Runtime
The runtime analysis for LLRBs is simple if you trust the 2-3 tree runtime.
● LLRB tree has height O(log N).
● Contains is trivially O(log N).
● Insert is O(log N).
○
○
O(log N) to add the new node.
O(log N) rotation and color flip operations per insert.
We will not discuss LLRB delete.
● Not too terrible really, but it’s just not interesting enough to cover. See
optional textbook if you’re curious (though they gloss over it, too).
LLRB Implementation
●
Using what we have learned, turning a BST into an LLRB requires
additional code to handle the rotations and color flips.
This is exactly what you have done in Lab!
○
●
One other note on red links vs. red nodes:
In this lecture we used the idea of a red link which glued nodes together.
However in lab we used red nodes and no colored links.
In implementation it is relatively easy to add coloring to each node, and relatively more
complicated to add coloring to the links.
As such we will typically implement the version with red nodes.
We can think of all of the following trees as equivalent.
○
○
○
○
○
S
S
BS
B
A
E
2-3 Tree
Z
A
B
Z
E
LLRB w/ red links
A
Z
E
LLRB w/ red nodes
Multidimensional Data
Yet Another Type of Map/Set
We’ve touched on two fairly general implementations of sets and maps (we
still have not discussed how Hash Tables work, that will be next week!):
● Hash Table: Requires that keys can be hashed.
● Search Tree: Requires that keys can be compared.
Both of these implementations require that data is one-dimensional.
Today, we’ll discuss one very important special case: Multi-dimensional keys.
One-dimensional Data
So far our data has been one dimensional. Some examples:
● Integers: 1 is smaller than 3; 5 is bigger than 2
● Doubles: 1.0 is smaller than 3.0; 5.0 is bigger than 2.0
● Strings: “apple” is “smaller” than “cat”, “dog” is bigger than “banana”
There is a single
axis, and there is a
clear interpretation
of which of two
given elements is
bigger.
Two-dimensional Data
What if our data lies in two dimensions?
● (Integer, Integer) Points: e.g. (1,2) or (0,4)
● (Double, Double) Points: e.g. (1.0, 2.0) or (0.0, 4.0)
● (String, String) tuples: e.g. (“apple”, “cat”) or (“dog”, “banana”)
There are now two
axes that our data
lies on, the x-axis
and the y-axis.
Two-dimensional Data
Out of the points (1,2) and (0,4) which is “bigger”? Why?
Two-dimensional Data
Out of the points (1,2) and (0,4) which is “bigger”? Why?
We could impose some
kind of relative ordering on
these points, but inherently
by saying one point is
larger than the other we
are compressing the data
into one-dimension.
For multidimensional data
the question of which is
items are “bigger” is a
somewhat flawed question.
Two-dimensional Data
Out of the points (1,2) and (0,4) which is “bigger”? Why?
We could impose some
kind of relative ordering on
these points, but inherently
by saying one point is
larger than the other we
are compressing the data
into one-dimension.
For multidimensional data
the question of which is
items are “bigger” is a
somewhat flawed question.
(0, 4)
(1, 2)
(1, 2)
X-Based Tree
(0, 4)
Y-Based Tree
Suppose we chose the X-based tree, what
happens if we want to look up a point
based on it’s Y-value? It could be anywhere!
k-dimensional Data
What if our data lies in k dimensions?
● (Integer, Integer, . . . ,Integer) Points: e.g. (1,2,...,0) or (0,4,...,8)
● (Double, Double, . . . , Double) Points: e.g. (1.0,2.0,...,0.0) or (0.0,4.0,...,8.0)
● (String, String, . . . ,String) tuples: e.g. (“apple”, “cat”,...”monkey”) or (“dog”,
“banana”,...,”orange”)
With k-dimensions we have the same problems!
● In this class we will only deal with one-dimensional data and
two-dimensional data, but it can be useful to consider the generalization
of this to higher k-dimensional data.
k-d Trees
Binary Search Trees
BSTs had some nice properties:
● The ability to compare one-dimensional data is leveraged organize items.
● This organization can lead to logarithmic operations for checking if an item
exists in the tree or adding an element to the tree.
○
To guarantee this we need some form of balancing.
We’ll exploit similar properties to organize spatial (multidimensional) data. This
will allow us to:
● Have similarly fast add and contains operations
○
●
Again we need to have some form of guarantee that our tree is balanced.
Define new operations like nearest which make more sense for spatial data.
○
○
More on nearest later.
Nearest will be one of the key reasons we will study and implement k-d trees.
Key Insight
The multidimensional data that we have is comprised of elements which can be
compared on one dimension! We saw this before:
(0, 4)
(1, 2)
(1, 2)
X-Based Tree
(0, 4)
Y-Based Tree
Instead of choosing to use strictly an X-based approach or a Y-based approach,
we can combine both of them.
k-d Tree: An X and Y Based Tree
Instead of choosing to use strictly an X-based approach or a Y-based approach,
we can combine both of them.
●
●
We switch between comparing on X values and comparing on Y values.
At each level we apply the same binary search tree principal for the
dimension we are comparing on.
k-d Tree: An X and Y Based Tree
It can be helpful to visualize the data both as a tree, and also spatially on the
coordinate plane.
●
●
●
The red lines correspond to when we split the space into two spaces by x.
The blue lines correspond to when we split the space into two spaces by y.
As we proceed further down the tree spaces get even further divided into
pieces.
Exercise: Where to look
If we have the following k-d tree, which nodes should we consider when looking
for the point (2,3)? Can we skip any like with a BST?
Exercise: Where to look
If we have the following k-d tree, which nodes should we consider when looking
for the point (2,3)? Can we skip any like with a BST?
Yes! We can skip the nodes above which have been struck out. We should
exclude them for the same ways that we will exclude nodes in a BST.
k-d Trees: Operations
k-d Trees Insertion / Construction
Let’s see an example of how to build a k-d tree.
●
k-d tree insertion demo.
(4, 5)
(1, 5)
C
E
(3, 3)
A
F
(4, 4)
D
(2, 3)
B
(4, 2)
K-d Trees and Nearest Neighbor
k-d trees support an efficient nearest method.
● Optimization: Do not explore subspaces that can’t possible have a better
answer than your current best.
Example: Find the nearest point to (0, 7).
● Answer is (1, 5).
● k-d tree nearest demo.
(4, 5)
(1, 5)
(0, 7)
C
E
(3, 3)
A
F
(4, 4)
D
(2, 3)
B
(4, 2)
Nearest Pseudocode
nearest(Node n, Point goal, Node best):
● If n is null, return best
● If n.distance(goal) < best.distance(goal), best = n
● If goal < n (according to n’s comparator):
○
●
●
goodSide = n.”left”Child
badSide = n.”right”Child
goodSide = n.”right”Child
badSide = n.”left”Child
best = nearest(goodSide, goal, best)
If bad side could still have something useful
○
●
■
■
else:
■
■
Nearest is a helper method that returns
whichever is closer to goal out of the
following two choices:
1. best
2. all items in the subtree starting at n
best = nearest(badSide, goal, best)
return best
This is our pruning rule.
Inefficient Nearest Pseudocode
nearest(Node n, Point goal, Node best):
● If n is null, return best
● If n.distance(goal) < best.distance(goal), best = n
● best = nearest(n.leftChild, goal, best)
● best = nearest(n.rightChild, goal, best)
● return best
Here we do no pruning so
we will search the whole
tree.
Consider implementing this inefficient version first.
● Easy to implement.
● Once you’ve verified this is working, you can implement the more efficient
version on the previous slide.
○
The key difference is that you would have to add pruning.
Today’s PSA
● ALWAYS backup your files.
● Why?
○ Theft
○ Water Accident (maybe don’t watch netflix in the bathtub)
○ rm -rf ~ (Do Not try this at home)
○ Computer cracks in half/corrupted/etc
● There are many methods to do this, some free and some paid
○ Google Drive/GitHub/Bitbucket (and other free file storage)
○ Physical Hard Drive (one time purchase)
○ Online backup (subscription based)
Lecture 6: Heaps
and Graphs
CS 61BL Summer 2021
●
BYOW released today
○
○
●
●
Announcements
Monday, July 26
●
●
●
Do Lab 16 (tomorrow’s lab)
before starting
Two AG phases, see spec
Midterm 2 today
No redemption quiz or
tutoring for quiz 8
Quiz 9 on Wednesday, Quiz
10 on Friday
Lab as normal
DO NOT discuss the exam on
Ed until we release you.
○
This would be considered
academic dishonesty. See the
syllabus for more
Priority Queue
The Priority Queue Interface
/** (Min) Priority Queue: Allowing tracking and removal of the
* smallest item in a priority queue. */
public interface MinPQ<Item> {
/** Adds the item to the priority queue. */
public void add(Item x);
/** Returns the smallest item in the priority queue. */
public Item getSmallest();
/** Removes the smallest item from the priority queue. */
public Item removeSmallest();
/** Returns the size of the priority queue. */
public int size();
}
Useful if you want to keep track of the “smallest”, “largest”, “best” etc. seen so far.
Usage Example: Unharmonious Texts
Suppose you want to monitor the text messages of the citizens to make sure
that they are not having any unharmonious conversations.
Each day, you create a list of the M messages that seem most unharmonious
using the HarmoniousnessComparator.
Naive approach: Create a list of all messages sent for the entire day. Sort it
using your comparator. Return the M messages that are largest.
Naive Implementation: Store and Sort
public List<String> unharmoniousTexts(Sniffer sniffer, int M) {
ArrayList<String> allMessages = new ArrayList<String>();
for (Timer timer = new Timer(); timer.hours() < 24; ) {
allMessages.add(sniffer.getNextMessage());
}
Comparator<String> cmptr = new HarmoniousnessComparator();
Collections.sort(allMessages, cmptr, Collections.reverseOrder());
return allMessages.sublist(0, M);
}
Potentially uses a huge amount of memory Θ(N), where N is number of texts.
● Goal: Do this in Θ(M) memory using a MinPQ.
MinPQ<String> unharmoniousTexts = new HeapMinPQ<Transaction>(cmptr);
Better Implementation: Track the M Best
public List<String> unharmoniousTexts(Sniffer sniffer, int M) {
Comparator<String> cmptr = new HarmoniousnessComparator();
MinPQ<String> unharmoniousTexts = new HeapMinPQ<Transaction>(cmptr);
for (Timer timer = new Timer(); timer.hours() < 24; ) {
unharmoniousTexts.add(sniffer.getNextMessage());
if (unharmoniousTexts.size() > M)
{ unharmoniousTexts.removeSmallest(); }
}
ArrayList<String> textlist = new ArrayList<String>();
while (unharmoniousTexts.size() > 0) {
textlist.add(unharmoniousTexts.removeSmallest());
}
return textlist;
}
Can track top M transactions using only M memory. API for MinPQ also makes code
very simple (don’t need to do explicit comparisons).
How Would We Implement a MinPQ?
Some possibilities:
● Ordered Array
● Bushy BST: Maintaining bushiness is annoying. Handling duplicate priorities is
awkward.
● HashTable: No good! Items go into random places.
Ordered Array
Bushy BST
Hash Table
add
Θ(N)
Θ(log N)
Θ(1)
getSmallest
Θ(1)
Θ(log N)
Θ(N)
removeSmallest
Θ(N)
Θ(log N)
Θ(N)
Caveats
Dups tough
Worst Case Θ(·) Runtimes
Heap
Heaps
Introducing the Heap
BSTs would work, but need to be kept bushy and duplicates are awkward.
Binary min-heap: Binary tree that is complete and obeys min-heap property.
●
●
Min-heap: Every node is less than or equal to both of its children.
Complete: Missing items only at the bottom level (if any), all nodes are as far
left as possible.
0
5
8
5
1
8
0
0
0
6
2
8
5
1
8
6
8
0
1
8
Incomplete
5
2
8
1
4
6
Lacks min-heap property
Heap Comprehension Test
Which of these are min heaps?
8
8
8
8
8
8
4
5
2
6
7
0
0
8
7
5
3
1
9
Heap Comprehension Test Solutions
8
8
8
8
8
4
5
8
2
6
7
0
8
Incomplete
0
7
3
5
1
9
Lacks min-heap property
What Good Are Heaps?
Heaps lend themselves very naturally to implementation of a priority queue.
Hopefully easy question:
●
How would you support getSmallest()?
0
5
8
1
8
6
How Do We Add To A Heap?
1
Challenge: Come up with an algorithm for add(x).
●
How would we insert 3?
5
1
6
Runtime must be logarithmic.
7
6
5
7
8
?
Bonus: Come up with an algorithm for removeSmallest().
Solution: See https://goo.gl/wBKdFQ for an animated demo.
3
Heap Operations Summary
Given a heap, how do we implement PQ operations?
●
●
●
getSmallest() - return the item in the root node.
add(x) - place the new employee in the last position, and promote as high
as possible.
removeSmallest() - assassinate the president (of the company), promote
the rightmost person in the company to president. Then demote repeatedly,
always taking the ‘better’ successor.
See https://goo.gl/wBKdFQ for an animated demo.
Remaining question: How would we do all this in Java?
Heap Representations
How should we represent the Heap?
Approach 1:
●
Similar to what we did for BSTs
class Node {
int value; // e.g. 0
Node left;
Node right;
...
0
5
8
1
6
8
How should we represent the Heap?
Approach 1:
●
Similar to what we did for BSTs
class Node {
int value; // e.g. 0
Node left;
Node right;
...
0
5
8
1
8
6
This works but doesn’t leverage the complete nature of the tree...
How should we represent the Heap?
Approach 2: Store keys in an array. Don’t store structure anywhere.
- To interpret array: Simply assume tree is complete.Obviously only works
for “complete” trees.
public class Heap<Key> {
Key[] keys;
...
0
1
3
7
e
2
4 g
b
a
k
d
8
f
9
5 p
j
10
m
11
Key[] keys
v
6
r
12
y
k
e
v
b
g
p
y
a
d
f
j
m r
0
1
2
3
4
5
6
7
8
9
10 11 12 13
x
13
A Deep Look at Approach 2
w
Challenge: Write the parent(k) method for approach 2.
x
public void swim(int k) {
if (keys[parent(k)] ≻ keys[k]) {
swap(k, parent(k));
swim(parent(k));
}
}
0
1
3
7
a
d
8
2
f
9
5 p
j
10
Key[] keys
z
y
w
x
y
z
0
1
2
3
Key[] keys
k
e
4 g
b
x
m
11
v
6
r
12
x
13
y
k
e
v
b
g
p
y
a
d
f
j
m r
0
1
2
3
4
5
6
7
8
9
10 11 12 13
public class Tree<Key> {
Key[] keys;
...
x
A Deep Look at Approach 2
w
Challenge: Write the parent(k) method for approach 2.
x
z
y
public void swim(int k) {
if (keys[parent(k)] ≻ keys[k]) {
Key[] keys w x y z
swap(k, parent(k));
0 1 2 3
swim(parent(k));
public int parent(int k) {
}
return (k - 1) / 2;
}
}
0
1
3
7
e
2
4 g
b
a
Key[] keys
k
d
8
f
9
v
5 p
j
10
m
11
6
r
12
k
e
v
b
g
p
y
a
d
f
j
m r
x
0
1
2
3
4
5
6
7
8
9
10 11 12 13
public class Tree<Key> {
Key[] keys;
...
y
x
13
Slight improvement: Leaving One Empty Spot
Approach 2b: Store keys in an array. Offset everything by 1 spot.
● Same as 2, but leave spot 0 empty.
● Makes computation of children/parents “nicer”.
○
○
○
leftChild(k) = k*2
rightChild(k) = k*2 + 1
parent(k) = k/2
1
2
4
8
a
e
3
5 g
b
d
9
Key[] keys
k
f
10
6 p
j
11
m
12
v
7
r
13
x
14
y
-
k
e
v
b
g
p
y
a
d
f
j
m r
x
0
1
2
3
4
5
6
7
8
9
10 11 12 13 14
Heap Implementation of a Priority Queue
Ordered Array
Bushy BST
Hash Table
Heap
add
Θ(N)
Θ(log N)
Θ(1)
Θ(log N)
getSmallest
Θ(1)
Θ(log N)
Θ(N)
Θ(1)
removeSmallest
Θ(N)
Θ(log N)
Θ(N)
Θ(log N)
Graphs
Graph Definition
A graph consists of:
● A set of vertices.
● A set of zero or more edges, each of which connects two vertices.
Green structures below are graphs.
● Note, all trees are graphs!
Graph Definition
A simple graph is a graph with:
● No edges that connect a vertex to itself, i.e. no “loops”.
● No two edges that connect the same vertices, i.e. no “parallel edges”.
Green graph below is simple, pink graphs are not.
Graph Definition
A simple graph is a graph with:
●
●
No edges that connect a vertex to itself, i.e. no “loops”.
No two edges that connect the same vertices, i.e. no “parallel edges”.
In 61BL, unless otherwise explicitly stated, all graphs will be simple.
●
In other words, when we say “graph”, we mean “simple graph.”
Graph Terminology
●
●
●
●
Graph:
○ Set of vertices, a.k.a. nodes.
○ Set of edges: Pairs of vertices.
○ Vertices with an edge between are
adjacent.
A path is a sequence of vertices connected by
edges.
○ A simple path is a path without repeated
vertices.
A cycle is a path whose first and last vertices are
the same.
○ A graph with a cycle is ‘cyclic’.
Two vertices are connected if there is a path
between them. If all vertices are connected, we
say the graph is connected.
Figure from Algorithms 4th Edition
Graph Types
Undirected
Directed
b
Acyclic:
a
d
a
d
c
c
b
Cyclic:
a
e
b
b
d
c
a
d
c
Graph Example: The Paris Metro
This schematic map of the Paris Metro is a graph:
●
●
●
●
Undirected
Connected
Cyclic (not a tree!)
Vertex-labeled (each has a color).
Directed Graph Example
Edge captures ‘is-a-type-of’ relationship. Example: descent is-a-type-of movement.
Depth-First Traversal
Not a tree!
● Two paths from
group_action
to event.
s-t Connectivity
Let’s solve a classic graph problem called the s-t connectivity problem.
●
Given source vertex s and a target vertex t, is there a path between s and
t?
3
Requires us to traverse the graph somehow.
0
s
1
6
4
2
7
5
t
8
s-t Connectivity
Let’s solve a classic graph problem called the s-t connectivity problem.
●
Given source vertex s and a target vertex t, is there a path between s and
t?
3
Requires us to traverse the graph somehow.
0
s
Come up for an algorithm for connected(s, t)
1
6
4
2
5
7
t
8
s-t Connectivity
One possible recursive algorithm for connected(s, t).
●
●
●
Does s == t? If so, return true.
Otherwise, if connected(v, t) for any neighbor v of s, return true.
Return false.
3
What is wrong with the algorithm above?
0
s
1
6
4
7
5
2
t
8
s-t Connectivity
One possible recursive algorithm for connected(s, t).
● Does s == t? If so, return true.
● Otherwise, if connected(v, t) for any neighbor v of s, return true.
● Return false.
What is wrong with it? Can get caught in an infinite loop. Example:
● connected(0, 7):
3
○
○
●
Does 0 == 7? No, so...
if (connected(1, 7)) return true;
connected(1, 7):
○
○
Does 1 == 7? No, so…
If (connected(0, 7)) … ← Infinite loop.
0
s
1
6
4
2
5
7
t
8
s-t Connectivity
One possible recursive algorithm for connected(s, t).
●
●
●
Does s == t? If so, return true.
Otherwise, if connected(v, t) for any neighbor v of s, return true.
Return false.
3
What is wrong with it? Can get caught in an infinite loop.
●
How do we fix it?
0
s
1
6
4
7
5
2
t
8
s-t Connectivity
One possible recursive algorithm for connected(s, t).
●
●
●
●
Mark s.
Does s == t? If so, return true.
Otherwise, if connected(v, t) for any unmarked neighbor v of s, return
true.
3
Return false.
Basic idea is same as before, but visit each
vertex at most once.
0
s
1
2
●
Marking nodes prevents multiple visits.
●
Demo: Recursive s-t connectivity.
6
4
5
7
t
8
Depth First Traversal
This idea of exploring a neighbor’s entire subgraph before moving on to the
next neighbor is known as Depth First Traversal.
●
●
Example: Explore 1’s subgraph completely before using the edge 0-3.
Called “depth first” because you go as deep as possible.
s
0
1
3
2
4
8
7
t
5
6
Depth First Traversal
This idea of exploring a neighbor’s entire subgraph before moving on to the
next neighbor is known as Depth First Traversal.
●
●
Example: Explore 1’s subgraph completely before using the edge 0-3.
Called “depth first” because you go as deep as possible.
s
1
2
5
6
0
3
4
8
7
t
Entirely possible for 1’s subgraph to include 3!
● It’s still depth first, since we’re not using the
edge 0-3 until the subgraph is explored.
From: https://xkcd.com/761/
The Power of Depth First Search
DFS is a very powerful technique that can be used for many types of graph
problems.
Another example:
●
Let’s discuss an algorithm that computes a path to every vertex.
●
Let’s call this algorithm DepthFirstPaths.
●
Demo: DepthFirstPaths.
Tree vs. Graph Traversals
Tree Traversals
D
There are many tree traversals:
●
●
●
●
Preorder: DBACFEG
Inorder: ABCDEFG
Postorder: ACBEGFD
Level order: DBFACEG
B
A
F
C
E
G
Graph Traversals
D
There are many tree traversals:
●
●
●
●
Preorder: DBACFEG
Inorder: ABCDEFG
Postorder: ACBEGFD
Level order: DBFACEG
B
F
A
E
C
G
What we just did in DepthFirstPaths is called “DFS Preorder.”
●
●
DFS Preorder: Action is before DFS calls to neighbors.
○ Our action was setting edgeTo.
○ Example: edgeTo[1] was set before DFS calls to
neighbors 2 and 4.
One valid DFS preorder for this graph: 012543678
○ Equivalent to the order of dfs calls.
3
0
s
1
6
4
7
5
2
8
Graph Traversals
D
There are many tree traversals:
●
●
●
●
Preorder: DBACFEG
Inorder: ABCDEFG
Postorder: ACBEGFD
Level order: DBFACEG
B
F
A
E
C
G
Could also do actions in DFS Postorder.
●
●
●
●
DFS Postorder: Action is after DFS calls to neighbors.
Example: dfs(s):
○ mark(s)
○ For each unmarked neighbor n of s, dfs(n)
○ print(s)
Results for dfs(0) would be: 347685210
Equivalent to the order of dfs returns.
3
0
s
1
6
4
2
5
8
7
Graph Traversals
D
Just as there are many tree traversals:
●
●
●
●
Preorder: DBACFEG
Inorder: ABCDEFG
Postorder: ACBEGFD
Level order: DBFACEG
B
F
A
E
C
So too are there many graph traversals, given some source:
● DFS Preorder: 012543678 (dfs calls).
● DFS Postorder: 347685210 (dfs returns).
0
s
G
3
1
6
4
7
5
2
8
Graph Traversals
D
Just as there are many tree traversals:
●
●
●
●
Preorder: DBACFEG
Inorder: ABCDEFG
Postorder: ACBEGFD
Level order: DBFACEG
B
A
So too are there many graph traversals, given some source:
● DFS Preorder: 012543678 (dfs calls).
● DFS Postorder: 347685210 (dfs returns).
0
● BFS order: Act in order of distance from s.
s
○ BFS stands for “breadth first search”.
○ Analogous to “level order”. Search is wide, not deep.
○ 0 1 24 53 68 7
F
E
C
G
3
1
6
4
2
5
8
7
Shortest Paths Challenge
2
Goal: Given the graph above, find the shortest path
from s to all other vertices.
s
0
1
●
Give a general algorithm.
●
Hint: You’ll need to somehow visit vertices in BFS
order.
●
Hint #2: You’ll need to use some kind of data
structure.
●
Hint #3: Don’t use recursion.
5
3
4
6
7
BFS Answer
Breadth First Search.
● Initialize a queue with a starting vertex s and mark that vertex.
○
○
●
A queue is a list that has two operations: enqueue (a.k.a. addLast) and dequeue (a.k.a.
removeFirst).
Let’s call this the queue our fringe.
Repeat until queue is empty:
○
○
A queue is the opposite of a stack. Stack
Remove vertex v from the front of the queue.
has push (addFirst) and pop (removeFirst).
For each unmarked neighbor n of v:
■ Mark n.
■ Set edgeTo[n] = v (and/or distTo[n] = distTo[v] + 1).
■ Add n to end of queue.
Demo: Breadth First Paths
Do this if you want to track distance value.
Citations
Slides adapted from Josh Hug’s Spring 2021 iteration of CS 61B
Lecture 7
CS61BL Summer 2021
●
●
Announcements
●
Monday, August 2nd 2021
●
●
●
Phase 1 Partner Review form
will be released on ed at the
end of lecture.
Midterm 2 Grades released,
congrats!
Regrade requests open today
at 6PM, close next week.
Redemption quiz 9 today
Redemption tutoring for quiz
10 today
Week 6 Survey due
tomorrow!
Agenda
-
Dijkstra’s
A*
MSTs
Agenda
-
Dijkstra’s
A*
MSTS
Note that we won’t be going over sorting
today for the sake of time, but we will
link comprehensive videos in the lab
specs and it is in scope for the final.
Finding the shortest path
Suppose we’re trying to get from s to t. Can we use BFS?
s
t
The reason the places are in Chinese is
because Professor Hug took this diagram from
his Chinese language version of 61B.
Breadth First Search for Mapping Applications
BFS yields the wrong route from s to t.
● BFS yields a route of length ~190 instead of ~110.
● We need an algorithm that takes into account edge distances, also known
as “edge weights”!
s
80
10
s
20
20
40
40
40
40
t
80
10
40
40
70
Correct Result
t
70
BFS Result
Dijkstra’s Algorithm
Problem: Single Source Single Target Shortest Paths
Goal: Find the shortest path from source vertex s to some target vertex t.
3
11
1
3
2
s
5
0
1
2
4
1
1
2
15
6
5
4
1
5
t
Challenge: Try to find the shortest path from town 0 to town 5.
● Each edge has a number representing the length of that road in miles.
Problem: Single Source Single Target Shortest Paths
Goal: Find the shortest paths from source vertex s to some target vertex t.
3
Best path from 0 to 5 is
● 0 -> 2 -> 4 -> 5.
● Total length is 9 miles.
11
1
3
2
s
5
0
1
2
4
1
1
2
6
5
4
15
The path 0 -> 2 -> 5 only
involves three towns, but total
length is 16 miles.
1
5
Problem: Single Source Single Target Shortest Paths
Goal: Find the shortest paths from source vertex s to some target vertex t.
3
11
1
2
3
2
s
5
0
1
4
1
1
2
15
6
5
4
1
v
0
1
2
3
4
5
6
distTo[]
0.0
2.0
5.0
9.0
-
5
Observation: Solution will always be a path with no cycles (assuming
non-negative weights).
edgeTo[]
0→1
1→4
4→5
-
Problem: Single Source Shortest Paths
Goal: Find the shortest paths from source vertex s to every other vertex.
3
11
1
3
2
s
5
0
1
2
4
1
1
2
6
5
4
15
1
5
Challenge: Try to write out the solution for this graph.
● You should notice something interesting.
Problem: Single Source Shortest Paths
Goal: Find the shortest paths from source vertex s to every other vertex.
3
11
1
2
3
2
s
5
0
1
4
1
1
2
15
6
5
4
1
v
0
1
2
3
4
5
6
distTo[]
0.0
2.0
1.0
11.0
5.0
9.0
10.0
5
Observation: Solution will always be a tree.
● Can think of as the union of the shortest paths to all vertices.
edgeTo[]
0→1
0→1
6→3
1→4
4→5
4→6
SPT Edge Count
If G is a connected edge-weighted graph with V vertices and E edges, how many
edges are in the Shortest Paths Tree (SPT) of G? (assume every vertex is
reachable)
3
1
6
s
4
0
2
5
SPT Edge Count
If G is a connected edge-weighted graph with V vertices and E edges, how many
edges are in the Shortest Paths Tree (SPT) of G? (assume every vertex is
reachable)
3
V: 7
Number of edges in SPT
is 6
1
6
s
4
0
2
5
Always V-1:
● For each vertex,
there is exactly one
input edge (except
source).
Finding a Shortest Paths Tree (By Hand)
What is the shortest paths tree for the graph below? Note: Source is A.
B
2
5
s
2
A
1
1
D
5
C
Finding a Shortest Paths Tree (By Hand)
What is the shortest paths tree for the graph below? Note: Source is A.
● Annotation in magenta shows the total distance from the source.
2
B
0
s
2
5
2
A
1
1
D
5
C
1
4
Creating an Algorithm
Let’s create an algorithm for finding the shortest paths tree.
Will start with an incorrect algorithm and then successively improve it.
● Algorithm begins in state below. All vertices unmarked. All distances
∞
infinite. No edges in the SPT.
B
s
2
5
∞
2
A
∞
1
D
1
5
C
∞
Finding a Shortest Paths Tree Algorithmically (Incorrect)
Bad algorithm #1: Perform a depth first search. When you visit v:
● For each edge from v to w, if w is not already part of SPT, add the edge.
5
dfs(A):
dfs(B):
∞
5
Add A->B to SPT
Add B->D to SPT
B
Add A->C to SPT
0
s
2
5
3
A
∞
1
5
1
1
C
0
2
5
3
A
1
1
1
7
D
5
C
1
∞
D
5
B
∞1
s
3
A
5
C
2
5
0
s
D
1
dfs(C):
B already in SPT.
D already in SPT.
B
A already in SPT.
7
Finding a Shortest Paths Tree Algorithmically (Incorrect)
Bad algorithm #2: Perform a depth first search. When you visit v:
●
For each edge from v to w, add edge to the SPT if that edge yields shorter
distance.
We’ll call this
5
dfs(A):
A->B is 5, < than ∞
A->C is 1, < than ∞
0
s
dfs(B):
B->D is 5 + 2 = 7, better than ∞.
B->A is 5 + 3 = 8, worse than 0.
∞
B
2
5
3
A
∞
1
D
1
5
1
dfs(C):
C->B is 1 + 1 = 2, better than 5.
C->D is 1 + 5 = 6, better than 7.
7
D
5
2
3
A
∞
C
5
0
s
1
B
∞1
process “edge
relaxation”.
2
3
A
52
C
B
5
0
s
5
7
1
6
1
D
1
5
Improvements:
● Use better edges if found.
C
1
Finding a Shortest Paths Tree Algorithmically
Dijkstra’s Algorithm: Perform a best first search (closest first). When you visit v:
● For each edge from v to w, relax that edge.
5
A has lowest dist, so dijkstras(A):
C has lowest dist, so dijkstras(C):
∞
5 2
C->B is 1 + 1 = 2, better than 5.
A->B is 5, < than ∞
A->C is 1, < than ∞
0
s
C->D is 1 + 5 = 6, better than ∞.
B
2
5
3
A
∞
1
1
5
1
1
0
3
A
1
1
1
D
6
4
1
D
5
C
6
C
2
5
∞
5
B
∞1
B has lowest dist, so dijkstras(B):
B->A is 2 + 3 = 5, worse than 0.
B->D is 2 + 2 = 4, better than 6.
3
A
2
C
s
s
2
5
0
D
B
Improvements:
● Use better edges if found.
● Traverse “best first”.
Dijkstra’s Algorithm Demo
Insert all vertices into fringe Priority Queue (PQ), storing vertices in order of
distance from source.
Repeat: Remove (closest) vertex v from fringe, and relax all edges pointing
from v.
3
Dijkstra’s Algorithm Demo Link
∞
11
∞
1
0
s
3
2
5
0
1
∞
∞
1
15
6
5
4
∞
2
1
2
4
1
∞
5
Dijkstra’s Pseudocode and
Runtime
Dijkstra’s Algorithm Pseudocode
Dijkstra’s:
● PQ.add(source, 0)
● For other vertices v, PQ.add(v, infinity)
● While PQ is not empty:
○
○
p = PQ.removeSmallest()
Relax all edges from p
Relaxing an edge p → q with weight w:
● If distTo[p] + w < distTo[q]:
○
○
○
distTo[q] = distTo[p] + w
edgeTo[q] = p
PQ.changePriority(q, distTo[q])
Key invariants:
● edgeTo[v] is the best known
predecessor of v.
● distTo[v] is the best known total
distance from source to v.
● PQ contains all unvisited vertices
in order of distTo.
Important properties:
● Always visits vertices in order of
total distance from source.
● Relaxation always fails on edges
to visited (white) vertices.
Guaranteed Optimality
Dijkstra’s Algorithm:
● Visit vertices in order of best-known distance from source. On visit, relax
every edge from the visited vertex.
Dijkstra’s is guaranteed to return a correct result if all edges are non-negative.
Guaranteed Optimality
Dijkstra’s Algorithm:
● Visit vertices in order of best-known distance from source. On visit, relax
every edge from the visited vertex.
Dijkstra’s is guaranteed to return a correct result if all edges are non-negative.
Proof sketch: Assume all edges have non-negative weights.
● At start, distTo[source] = 0, which is optimal.
● After relaxing all edges from source, let vertex v1 be the vertex with
minimum weight, i.e. that is closest to the source. Claim: distTo[v1] is
optimal, and thus future relaxations will fail. Why?
○
○
distTo[p]
≥ distTo[v1] for all p, therefore
distTo[p] + w ≥ distTo[v1]
Can use induction to prove that this holds for all vertices after dequeuing.
Negative Edges
Dijkstra’s Algorithm:
● Visit vertices in order of best-known distance from source. On visit,
relax every edge from the visited vertex.
Dijkstra’s can fail if graph has negative weight edges. Why?
● Relaxation of already visited vertices can succeed.
14
101
34
1
82
33
-67
Negative Edges
Dijkstra’s Algorithm:
● Visit vertices in order of best-known distance from source. On visit,
relax every edge from the visited vertex.
Dijkstra’s can fail if graph has negative weight edges. Why?
● Relaxation of already visited vertices can succeed.
14
34
1
101
34
82
33
-67
Even though vertex 34 has greater distTo at
the time of its visit, it is still able to modify the
distTo of a visited (white) vertex.
Dijkstra’s Algorithm Runtime
Priority Queue operation count, assuming binary heap based PQ:
● add: V, each costing O(log V) time.
○
●
●
Note that we since each priority is initially infinity, each insertion really takes O(1) time
removeSmallest: V, each costing O(log V) time.
changePriority: E, each costing O(log V) time.
Overall runtime: O(V*log(V) + V*log(V) + E*logV).
● Assuming E > V, this is just O(E log V) for a connected graph.
# Operations
Cost per operation
Total cost
PQ add
V
O(log V)
O(V log V)
PQ removeSmallest
V
O(log V)
O(V log V)
PQ changePriority
E
O(log V)
O(E log V)
A* Algorithm
Single Target Dijkstra’s
Is this a good algorithm for a navigation application?
● Will it find the shortest path?
● Will it be efficient?
The Problem with Dijkstra’s
Dijkstra’s will explore every place within nearly two thousand miles of Denver
before it locates NYC.
The Problem with Dijkstra’s
Dijkstra’s will explore every place within nearly two thousand miles of Denver
before it locates NYC.
Why am I in Portland
if I’m trying to find
shortest path to New
York...
The Problem with Dijkstra’s
We have only a single target in mind, so we need a different algorithm. How
can we do better?
How can we do Better?
Explore eastwards first?
Introducing A*
Compared to Dijkstra’s which only considers d(source, v).
Big idea:
● Visit vertices in order of d(Denver, v) + h(v, goal), where h(v, goal) is a
guess at the distance from v to our goal NYC.
● In other words, look at some location v if:
○ We know already know the fastest way to
reach v.
○ AND we suspect that v is also the fastest way to
NYC taking into account the time to get to v.
Example: Henderson is farther than Englewood, but
probably overall better for getting to NYC.
We set this as the priority of
a vertex in the fringe in A*
A* Demo, with s = 0, goal = 6.
Insert all vertices into fringe PQ, storing vertices in order of d(source, v) + h(v, goal).
Repeat: Remove best vertex v from PQ, and relax all edges pointing from v.
3
A* Demo Link
∞
11
∞
1
#
0
1
2
3
4
5
6
h(v, goal)
0
1
s 0
3
15
2
Heuristic h(v, goal)
5
estimates that
∞
distance from 2 to 6
is 15.
0
3
2
5
1
∞
∞
1
15
4
1
∞
5
6
5
4
∞
2
1
2
A* Heuristic Example
How do we get our estimate?
●
●
Estimate is an arbitrary heuristic h(v, goal).
Doesn’t have to be perfect!
For the map to the right, what could we use?
A* Heuristic Example
How do we get our estimate?
● Estimate is an arbitrary heuristic h(v, goal).
● Doesn’t have to be perfect!
For the map to the right, what could we use?
● As-the-crow-flies distance to NYC.
/** h(v, goal) DOES NOT CHANGE as algorithm runs. */
public double h(v, goal) {
return computeLineDistance(v.latLong, goal.latLong);
}
A* vs. Dijkstra’s Algorithm
http://qiao.github.io/PathFinding.js/visual/
Note, if edge weights are all equal (as here),
Dijkstra’s algorithm is just breadth first search.
This is a good tool for understanding distinction
between order in which nodes are visited by the
algorithm vs. the order in which they appear on the
shortest path.
● Unless you’re really lucky, vastly more nodes are
visited than exist on the shortest path.
Minimum Spanning Trees
A new type of problem
We’ve already got a handful of graph-related party tricks: we can traverse in
depth first order and breadth first order, find a topological ordering, and find
the shortest path from one vertex to another in a weighted graph.
Let’s now discuss a totally different type of graph problem: finding a minimum
spanning tree.
Spanning Trees
Given an undirected graph, a spanning tree T is a subgraph of G, where T:
● Is connected.
These two properties
make it a tree.
● Is acyclic.
● Includes all of the vertices. This makes it spanning.
Example:
● Spanning tree is the black edges and vertices.
A minimum spanning tree is a spanning tree of minimum total edge weight.
● Example: Directly connecting buildings by power lines.
Spanning Trees
Which are valid spanning trees?
A)
B)
C)
Which are valid spanning trees?
Not a spanning
tree! Has two
disconnected
components.
Not a spanning
tree! Has a cycle.
Yes, this is a
valid spanning
tree!
MST Applications
●
●
●
●
●
●
Design of networks, water supply networks, electrical grids.
Subroutine in algorithms for other problems (e.g. traveling salesman
problem)
Handwriting and mathematical expression recognition
Image segmentation
Describe financial markets
And many more!
MST
Find the MST for the graph.
B
s
3
A
C
2
2
D
2
MST
Find the MST for the graph.
B
s
A
C
2
2
3
D
2
A Useful Tool for Finding the MST: Cut Property
●
●
A cut is an assignment of a graph’s nodes to two non-empty sets.
A crossing edge is an edge which connects a node from one set to a node
from the other set.
Cut property: Given any cut, minimum weight crossing edge is in the MST.
Question for you:
●
For rest of today, we’ll assume edge weights are unique.
Which edge is the minimum weight edge crossing the cut {2, 3, 5, 6}?
0-7
2-3
1-7
0-2
5-7
1-3
1-5
2-7
4-5
1-2
4-7
0-4
6-2
3-6
6-0
6-4
0.16
0.17
0.19
0.26
0.28
0.29
0.32
0.34
0.35
0.36
0.37
0.38
0.40
0.52
0.58
0.93
Question for you:
Which edge is the minimum weight edge crossing the cut {2, 3, 5, 6}?
0-2 must be
part of the MST!
0-7
2-3
1-7
0-2
5-7
1-3
1-5
2-7
4-5
1-2
4-7
0-4
6-2
3-6
6-0
6-4
0.16
0.17
0.19
0.26
0.28
0.29
0.32
0.34
0.35
0.36
0.37
0.38
0.40
0.52
0.58
0.93
Cut Property Proof
Suppose that for some cut, the minimum crossing edge e is not in the MST.
● Adding e to the MST creates a cycle.
● Some other edge f that is in the MST must also be a crossing edge in order for both
sides of the cut to be connected.
● Removing f and adding e is a lower weight spanning tree.
● Contradiction!
Generic MST Finding Algorithm
Start with no edges in the MST.
● Find a cut that has no crossing edges already in the MST.
● Add smallest crossing edge to the MST.
● Repeat until V-1 edges are in the MST.
This should work, but we need some way of finding a cut with no crossing
edges!
● Random isn’t a very good idea.
Prim’s Algorithm
Prim’s Algorithm
Start from some arbitrary start node, add this to your MST under construction.
● Repeatedly add shortest edge that connects a node in the MST to one not yet
included
● Repeat until all vertices are included.
Let’s walk through a demo that shows how
Prim’s works conceptually.
Prim’s Algorithm Demo
Prim’s Algorithm
A
5
1
4
B
6
0
C
7
D
3
F
E
2
Start with any node.
Add that node to the set of nodes in
the MST.
While there are still nodes not in
the MST:
Add the lightest edge that
leads to a node that is
unvisited.
Add the new node to the set of
nodes in the MST.
Prim’s Algorithm Demo
Prim’s Algorithm
A
5
1
4
B
6
0
C
3
7
D
F
E
2
Start with any node.
Add that node to the set of nodes in
the MST.
While there are still nodes not in
the MST:
Add the lightest edge that
leads to a node that is
unvisited.
Add the new node to the set of
nodes in the MST.
Prim’s Algorithm Demo
Prim’s Algorithm
A
5
1
4
B
6
0
C
7
D
3
F
E
2
Start with any node.
Add that node to the set of nodes in
the MST.
While there are still nodes not in
the MST:
Add the lightest edge that
connects a visited node to an
unvisited one (sprawl outward)
Add the new node to the set of
nodes in the MST.
Prim’s Algorithm Demo
Prim’s Algorithm
A
5
1
4
B
6
0
C
3
7
D
F
E
2
Start with any node.
Add that node to the set of nodes in
the MST.
While there are still nodes not in
the MST:
Add the lightest edge that
connects a visited node to an
unvisited one (sprawl outward)
Add the new node to the set of
nodes in the MST.
Prim’s Algorithm Demo
Prim’s Algorithm
A
5
1
4
B
6
0
C
7
D
3
F
E
2
Start with any node.
Add that node to the set of nodes in
the MST.
While there are still nodes not in
the MST:
Add the lightest edge that
connects a visited node to an
unvisited one (sprawl outward)
Add the new node to the set of
nodes in the MST.
Prim’s Algorithm Demo
Prim’s Algorithm
A
5
1
4
B
6
0
C
3
7
D
F
E
2
Start with any node.
Add that node to the set of nodes in
the MST.
While there are still nodes not in
the MST:
Add the lightest edge that
connects a visited node to an
unvisited one (sprawl outward)
Add the new node to the set of
nodes in the MST.
Prim’s Algorithm Demo
Prim’s Algorithm
A
5
1
4
B
6
0
C
7
D
3
F
E
2
Start with any node.
Add that node to the set of nodes in
the MST.
While there are still nodes not in
the MST:
Add the lightest edge that
connects a visited node to an
unvisited one (sprawl outward)
Add the new node to the set of
nodes in the MST.
Prim’s Algorithm Demo
Prim’s Algorithm
A
5
1
4
B
6
0
C
3
7
D
F
E
2
Start with any node.
Add that node to the set of nodes in
the MST.
While there are still nodes not in
the MST:
Add the lightest edge that
connects a visited node to an
unvisited one (sprawl outward)
Add the new node to the set of
nodes in the MST.
Prim’s Algorithm Demo
Prim’s Algorithm
A
5
1
4
B
6
0
C
7
D
3
F
E
2
Start with any node.
Add that node to the set of nodes in
the MST.
While there are still nodes not in
the MST:
Add the lightest edge that
connects a visited node to an
unvisited one (sprawl outward)
Add the new node to the set of
nodes in the MST.
Prim’s Algorithm Demo
Prim’s Algorithm
A
5
1
4
B
6
0
C
3
7
D
F
E
2
Start with any node.
Add that node to the set of nodes in
the MST.
While there are still nodes not in
the MST:
Add the lightest edge that
connects a visited node to an
unvisited one (sprawl outward)
Add the new node to the set of
nodes in the MST.
Prim’s Algorithm Demo
Prim’s Algorithm
A
5
1
4
B
6
0
C
7
D
3
F
E
2
Start with any node.
Add that node to the set of nodes in
the MST.
While there are still nodes not in
the MST:
Add the lightest edge that
connects a visited node to an
unvisited one (sprawl outward)
Add the new node to the set of
nodes in the MST.
Prim’s Algorithm Demo
Prim’s Algorithm
A
5
1
4
B
6
0
C
3
7
D
F
E
2
Start with any node.
Add that node to the set of nodes in
the MST.
While there are still nodes not in
the MST:
Add the lightest edge that
connects a visited node to an
unvisited one (sprawl outward)
Add the new node to the set of
nodes in the MST.
Prim’s Algorithm Demo
Prim’s Algorithm
A
5
1
4
B
6
0
C
7
D
3
F
E
2
Start with any node.
Add that node to the set of nodes in
the MST.
While there are still nodes not in
the MST:
Add the lightest edge that
connects a visited node to an
unvisited one (sprawl outward)
Add the new node to the set of
nodes in the MST.
Prim’s Algorithm Demo
Prim’s Algorithm
A
5
1
4
B
6
0
C
3
7
D
F
E
2
Start with any node.
Add that node to the set of nodes in
the MST.
While there are still nodes not in
the MST:
Add the lightest edge that
connects a visited node to an
unvisited one (sprawl outward)
Add the new node to the set of
nodes in the MST.
Prim’s Algorithm Demo
Prim’s Algorithm
A
5
1
4
B
6
0
C
7
D
3
F
E
2
Start with any node.
Add that node to the set of nodes in
the MST.
While there are still nodes not in
the MST:
Add the lightest edge that
connects a visited node to an
unvisited one (sprawl outward)
Add the new node to the set of
nodes in the MST.
Prim’s Algorithm Demo
Prim’s Algorithm
A
5
1
4
B
6
0
C
3
7
D
F
E
2
Start with any node.
Add that node to the set of nodes in
the MST.
While there are still nodes not in
the MST:
Add the lightest edge that
connects a visited node to an
unvisited one (sprawl outward)
Add the new node to the set of
nodes in the MST.
Prim’s Algorithm Demo
Prim’s Algorithm
A
5
1
4
B
6
0
C
7
D
3
F
E
2
Start with any node.
Add that node to the set of nodes in
the MST.
While there are still nodes not in
the MST:
Add the lightest edge that
connects a visited node to an
unvisited one (sprawl outward)
Add the new node to the set of
nodes in the MST.
Prim’s Algorithm Demo
Prim’s Algorithm
A
5
1
4
B
6
0
C
3
7
D
F
E
2
Start with any node.
Add that node to the set of nodes in
the MST.
While there are still nodes not in
the MST:
Add the lightest edge that
connects a visited node to an
unvisited one (sprawl outward)
Add the new node to the set of
nodes in the MST.
Prim’s Algorithm Demo
Prim’s Algorithm
A
5
1
4
B
6
0
C
7
D
3
F
E
2
Start with any node.
Add that node to the set of nodes in
the MST.
While there are still nodes not in
the MST:
Add the lightest edge that
connects a visited node to an
unvisited one (sprawl outward)
Add the new node to the set of
nodes in the MST.
Prim’s Correctness and the Cut Property
Now that we’ve gotten a feel for how Prim’s works, we can see that Prim’s algorithm is
an implementation of the generic MST solving algorithm we came up with earlier!
Start with no edges in the MST.
●
Find a cut that has no crossing edges already in the MST.
●
Add smallest crossing edge to the MST.
●
Repeat until V-1 edges are in the MST.
Remember, the problem with the generic algorithm was figuring out how to find that
cut.
Prim’s Correctness and the Cut Property
A
5
1
4
B
6
0
C
7
D
3
F
E
2
Instead of randomly selecting a cut,
we methodically build outward from
some start vertex. The two sides of
the cut are the vertices already
included, and those yet to be
included.
This way, we always have a cut that
has no crossing edges already in the
MST (because all the edges that are in
the MST are in one side of the cut).
Prim’s Algorithm Implementation
So, we walked over a conceptual example of how Prim’s works. But how do we actually
do it? The natural implementation of the conceptual version of Prim’s algorithm is highly
inefficient.
● Example: Iterating over purple edges shown is unnecessary and slow.
3
Can use some cleverness and a PQ to speed things up.
11
1
3
2
s
5
0
1
2
4
1
1
2
15
6
3
4
1
5
Prim’s vs. Dijkstra’s
While they solve totally different problems, Prim’s and Dijkstra’s algorithms are nearly
identical in implementation. Dijkstra’s considers “distance from the source”, and Prim’s
considers “distance from the tree.”
Visit order:
● Dijkstra’s algorithm visits vertices in order of distance from the source.
● Prim’s algorithm visits vertices in order of distance from the MST under construction.
Relaxation:
● Relaxation in Dijkstra’s considers an edge better based on distance to source.
● Relaxation in Prim’s considers an edge better based on distance to tree.
Prim’s vs Dijkstra’s
Realistic Implementation Demo (Link)
● Very similar to Dijkstra’s!
Prim’s Runtime
Exactly like Dijkstra’s runtime:
● Insertions: V, each costing O(log V) time.
● Pop min from queue: V, each costing O(log V) time.
● UpdatePriority: E, each costing O(log V) time.
Overall runtime, assuming E > V, we have O(E log V) runtime.
Note: this should be a reasonable assumption as we typically run MST algorithms on connected graphs.
Operation
Number of Times
Time per Operation
Total Time
Insert
V
O(log V)
O(V log V)
Delete minimum
V
O(log V)
O(V log V)
Update priority
E
O(log V)
O(E log V)
Kruskal’s Algorithm
Kruskal’s Algorithm
The awesome thing about data structures and algorithms is that there are
often many ways to solve the same problem.
Now, let’s see another approach: Kruskal’s algorithm.
High-level overview
Prim’s algorithm approaches finding an MST by choosing a start vertex, and
slowly crawling out, incorporating more and more vertices into the MST by
always picking the next edge that connects the incorporated set into the
unincorporated.
Kruskal’s will work by sorting all the edges from lightest to heaviest. Then, we’ll
repeatedly add the next edge that doesn’t cause a cycle in the MST.
Kruskal’s Algorithm
Initially, the MST is empty.
● Consider edges in increasing order of weight.
● Add edge to MST unless doing so creates a cycle.
● Repeat until V-1 edges.
Kruskal’s Algorithm Demo
A
5
1
4
3
B
F
Kruskal’s Algorithm
6
0
C
7
D
E
2
While there are still nodes not in
the MST:
Add the lightest edge that
doesn’t create a loop.
Add the new node to the set of
nodes in the MST.
Kruskal’s Algorithm Demo
A
5
1
4
B
3
F
Kruskal’s Algorithm
6
0
C
7
D
E
2
While there are still nodes not in
the MST:
Add the lightest edge that
doesn’t create a loop.
Add the endpoints of that edge
to the set of nodes in the MST.
Kruskal’s Algorithm Demo
A
5
1
4
3
B
F
Kruskal’s Algorithm
6
0
C
7
D
E
2
While there are still nodes not in
the MST:
Add the lightest edge that
doesn’t create a loop.
Add the endpoints of that edge
to the set of nodes in the MST.
Kruskal’s Algorithm Demo
A
5
1
4
B
3
F
Kruskal’s Algorithm
6
0
C
7
D
E
2
While there are still nodes not in
the MST:
Add the lightest edge that
doesn’t create a loop.
Add the endpoints of that edge
to the set of nodes in the MST.
Kruskal’s Algorithm Demo
A
5
1
4
3
B
F
Kruskal’s Algorithm
6
0
C
7
D
E
2
While there are still nodes not in
the MST:
Add the lightest edge that
doesn’t create a loop.
Add the endpoints of that edge
to the set of nodes in the MST.
Kruskal’s Algorithm Demo
A
5
1
4
B
3
F
Kruskal’s Algorithm
6
0
C
7
D
E
2
While there are still nodes not in
the MST:
Add the lightest edge that
doesn’t create a loop.
Add the endpoints of that edge
to the set of nodes in the MST.
Kruskal’s Algorithm Demo
A
5
1
4
3
B
F
Kruskal’s Algorithm
6
0
C
7
D
E
2
While there are still nodes not in
the MST:
Add the lightest edge that
doesn’t create a loop.
Add the endpoints of that edge
to the set of nodes in the MST.
Kruskal’s Algorithm Demo
A
5
1
4
B
3
F
Kruskal’s Algorithm
6
0
C
7
D
E
2
While there are still nodes not in
the MST:
Add the lightest edge that
doesn’t create a loop.
Add the endpoints of that edge
to the set of nodes in the MST.
Kruskal’s Algorithm Demo
A
5
1
4
3
B
F
Kruskal’s Algorithm
6
0
C
7
D
E
2
While there are still nodes not in
the MST:
Add the lightest edge that
doesn’t create a loop.
Add the endpoints of that edge
to the set of nodes in the MST.
Kruskal’s Algorithm Demo
A
5
1
4
B
3
F
Kruskal’s Algorithm
6
0
C
7
D
E
2
While there are still nodes not in
the MST:
Add the lightest edge that
doesn’t create a loop.
Add the endpoints of that edge
to the set of nodes in the MST.
Kruskal’s Algorithm Demo
A
5
1
4
3
B
F
Kruskal’s Algorithm
6
0
C
7
D
E
2
While there are still nodes not in
the MST:
Add the lightest edge that
doesn’t create a loop.
Add the endpoints of that edge
to the set of nodes in the MST.
Kruskal’s Algorithm Demo
A
5
1
4
B
3
F
Kruskal’s Algorithm
6
0
C
7
D
E
2
While there are still nodes not in
the MST:
Add the lightest edge that
doesn’t create a loop.
Add the endpoints of that edge
to the set of nodes in the MST.
Kruskal’s Algorithm Demo
A
5
1
4
3
B
F
Kruskal’s Algorithm
6
0
C
7
D
E
2
While there are still nodes not in
the MST:
Add the lightest edge that
doesn’t create a loop.
Add the endpoints of that edge
to the set of nodes in the MST.
Kruskal’s Algorithm Demo
A
5
1
4
B
3
F
Kruskal’s Algorithm
6
0
C
7
D
E
2
While there are still nodes not in
the MST:
Add the lightest edge that
doesn’t create a loop.
Add the endpoints of that edge
to the set of nodes in the MST.
Kruskal’s Algorithm Demo
A
5
1
4
3
B
F
Kruskal’s Algorithm
6
0
C
7
D
E
2
While there are still nodes not in
the MST:
Add the lightest edge that
doesn’t create a loop.
Add the endpoints of that edge
to the set of nodes in the MST.
Kruskal’s Algorithm Demo
A
5
1
4
B
3
F
Kruskal’s Algorithm
6
0
C
7
D
E
2
While there are still nodes not in
the MST:
Add the lightest edge that
doesn’t create a loop.
Add the endpoints of that edge
to the set of nodes in the MST.
Kruskal’s Algorithm Demo
A
5
1
4
3
B
F
Kruskal’s Algorithm
6
0
C
7
D
E
2
While there are still nodes not in
the MST:
Add the lightest edge that
doesn’t create a loop.
Add the endpoints of that edge
to the set of nodes in the MST.
Kruskal’s Algorithm Demo
A
5
1
4
B
3
F
Kruskal’s Algorithm
6
0
C
7
D
E
2
While there are still nodes not in
the MST:
Add the lightest edge that
doesn’t create a loop.
Add the endpoints of that edge
to the set of nodes in the MST.
Kruskal’s Algorithm Demo
A
5
1
4
B
3
F
Kruskal’s Algorithm
6
0
C
7
D
E
2
While there are still nodes not in
the MST:
Add the lightest edge that
doesn’t create a loop.
Add the endpoints of that edge
to the set of nodes in the MST.
Kruskal’s Correctness and the Cut Property
Why does Kruskal’s work? Special case of generic MST algorithm.
● Suppose we add edge e = v->w.
● Side 1 of cut is all vertices connected to v, side 2 is everything else.
● No crossing edge is black (since we don’t allow cycles).
● No crossing edge has lower weight (consider in increasing order).
Realistic Kruskal’s
Just as we saw with Prim’s, the conceptual version we went through is simple
enough to think about (and probably what you’d want to do if asked to run
Kruskal’s on an exam).
However, how do we implement Kruskal’s in reality?
●
●
Like Prim’s, we can use a priority queue for easily getting the next lightest
edge (or we could sort the list and iterate down the list).
But what about cycle detection?
Design question:
Let’s say we’re considering adding the next lightest edge e, that connects
vertices v and w. In code, how do we determine if adding e creates a loop?
Design question:
Let’s say we’re considering adding the next lightest edge e, that connects
vertices v and w. In code, how do we determine if adding e creates a loop?
●
Throwback to Disjoint Sets! We can just the disjoint sets data structure
and start with each edge in its own set. Whenever we add an edge (a, b),
we connect(a, b). To check if an e would form a cycle, just see if v and w
are already connected with isConnected()!
Kruskal’s Algorithm Realistic Implementation Demo (Link)
Prim’s vs. Kruskal’s
Step by step visualization for you to play around with (not shown in lecture)
Kruskal’s Runtime
For all intents and purposes, we can consider the runtime of Kruskal’s to be O(ElogV) (same as Prim’s)
Fun fact: This is possible because of “bottom-up
heapification”.
Operation
Insert
Number of Times
Time per Operation
Total Time
E
O(log E)
O(E)
Delete minimum
O(E)
O(log E)
O(E log E)
union
O(V)
O(log* V)
O(V log* V)
isConnected
O(E)
O(log* V)
O(E log*V)
Note: If we use a pre-sorted list of edges (instead of a PQ), then we can simply iterate through the list in
O(E) time, so the second row is no longe relevant.
Though we are assuming E>V (like we can in most scenarios w connected graphs), we also know E is limited to
at most V² . Thus, E*logE is the same as E logV² = E 2log(V) = E log(V)
170 Spoiler: Faster Compare-Based MST Algorithms
year
worst case
discovered by
1975
E log log V
Yao
1984
E log* V
Fredman-Tarjan
1986
E log (log* V)
Gabow-Galil-Spencer-Tarjan
1997
E α(V) log α(V)
Chazelle
2000
E α(V)
Chazelle
2002
optimal (link)
Pettie-Ramachandran
???
E ???
???
(Slide Courtesy of Kevin Wayne, Princeton University)
PSA: joy pottery particles purple
Your passwords protect your online identity.
Don’t reuse your passwords: https://xkcd.com/792/
Some password schemes are dumb: https://xkcd.com/936/
XKCD was not entirely correct about Correct Horse Battery Stapler:
https://security.stackexchange.com/questions/62832/is-the-oft-cited-xkcd-scheme-no-longer-good-advice
XKCD Password generator: https://preshing.com/20110811/xkcd-password-generator/
Download