Review: Fundamentals of Computer Science 1: Class Website
Class-based Program Design
Read lecture notes before coming to class!
Java is a versatile and widely-used programming language known for its strong typing and object-oriented features. In this introduction, we'll explore the basics of Java, including atomic data types, compound data, and some essential programming concepts.
Java supports various primitive data types, which represent simple atomic data. Here are some examples:
// Primitive Types
int distance = 10; // An integer representing distance
double coffeePrice = 4.25; // A double representing the price of a coffee
String greeting = "hello"; // A string representing a greeting
char initial = 'A'; // A character representing an initial
boolean isRaining = true; // A boolean representing a weather condition
In Java, it's important to note the type of each variable. Unlike some dynamically-typed languages, Java checks data types at compile time, reducing the risk of type errors during runtime.
Compound data types are structures that can hold multiple values. In Java, we often use classes to define such
structures. Let's explore an example involving a Book
and its Author
.
class Author {
String name;
int birthYear;
Author(String name, int birthYear) {
this.name = name;
this.birthYear = birthYear;
}
}
class Book {
String title;
Author author;
double price;
Book(String title, Author author, double price) {
this.title = title;
this.author = author;
this.price = price;
}
}
class ExamplesBook {
Author jkRowling = new Author("J.K. Rowling", 1965);
Book harryPotter = new Book("Harry Potter and the Philosopher's Stone", jkRowling, 19.99);
ExamplesBook() {}
}
In this example, Author
is a class representing an author's name and birth year, while
Book
is a class representing a book's title, its author, and its price. Instances of these classes
(jkRowling
and harryPotter
) are created using constructors, which initialize the
objects' attributes.
Java is a strongly-typed language, meaning that the type of a variable is checked at compile time. This ensures that type mismatches are caught early in the development process. For instance, you cannot add a string to a number directly:
int num = 5;
String text = " apples";
String result = num + text; // This will compile and concatenate the number and string.
However, adding a number to a string results in string concatenation rather than arithmetic addition.
Java uses infix notation for operations, which means operators are placed between the operands. For example:
int sum = 3 + 4; // Addition
int product = 5 * 6; // Multiplication
boolean isEqual = (7 == 7); // Equality check
Java classes are the blueprint for creating objects. They encapsulate data (attributes) and behavior (methods).
Here’s an example with a Person
class:
class Person {
String name;
int age;
// Constructor
Person(String name, int age) {
this.name = name;
this.age = age;
}
// Method to display person's info
void displayInfo() {
System.out.println("Name: " + name + ", Age: " + age);
}
}
class ExamplesPerson {
Person alice = new Person("Alice", 30);
Person bob = new Person("Bob", 25);
ExamplesPerson() {
alice.displayInfo();
bob.displayInfo();
}
}
In this example, the Person
class has attributes for a person's name and age, a constructor to
initialize these attributes, and a method to display the person's information. Instances of the
Person
class (alice
and bob
) are created and their information is
displayed.
Java is a powerful language with a strong typing system that helps catch errors at compile time. Its object-oriented nature allows for the creation of complex data structures through classes and objects. By understanding the basics of atomic and compound data, infix notation, and class structures, you can start building robust Java applications XDD
In programming, unions allow for a variable to be one of several types. In Racket, union data types are defined in comments, but in Java, we use interfaces to represent unions. Here are some examples to illustrate this concept.
(define-struct invader (loc color bullets size))
;; An Invader is a (make-invader Posn String PosInt PosInt)
(define invader1 (make-invader (make-posn 60 120) "green" 30 3))
#;(define (invader-temp1 inv)
(posn-temp1 (invader-loc inv))
(invader-color inv)
(invader-bullets inv)
(invader-size inv))
(define-struct ship (loc color speed))
;; A Spaceship is a (make-ship Posn String PosInt)
;; interp.: loc is a position on the Cartesian plane
;; speed is measured in miles/hour
;; A GamePiece is one of:
;; - Spaceship
;; - Invader
;; NOTE: The Union data type is only in comments in DrRacket. In Java, however, this is not in comments.
interface IGamePiece {}
// Represents an Invader in the game
class Spaceship implements IGamePiece {
Location loc;
String color;
int speed;
Spaceship(Location loc, String color, int speed){
this.loc = loc;
this.color = color;
this.speed = speed;
}
}
// Represents an Invader in the game
class Invader implements IGamePiece {
Location loc;
String color;
int bullets;
int size;
Invader(Location loc, String color, int bullets, int size){
this.loc = loc;
this.color = color;
this.bullets = bullets;
this.size = size;
}
}
// A class to represent a location on the Cartesian plane
class Location {
int x;
int y;
Location(int x, int y){
this.x = x;
this.y = y;
}
}
class Examples {
// You can't do the line below because it doesn't know what loc1 is yet
// Spaceship ship = new Spaceship(this.loc1, "blue", 55);
Location loc1 = new Location(30, 40);
Location loc2 = new Location(60, 80);
// Represents two IGamePieces: Spaceships and Invaders
IGamePiece ship = new Spaceship(this.loc1, "blue", 55);
IGamePiece invader1 = new Invader(this.loc2, "pink", 30, 3);
// You can still do this, but it's bad practice
// Spaceship ship = new Spaceship(new Location(30, 40), "blue", 55);
}
>IGamePiece is the type at compile time, while
Spaceship
and Invader
are
the types at runtime. Use the this
keyword to remove ambiguity. In Java, unions are represented by
interfaces, which allows for more general operations.
;; A Person is a (make-person String Person Person)
(define-struct person (name mom dad))
(define alice (make-person "Alice"
(make-person "Sally"
(make-person "Abby"
...))))
; this issue with this definition is that you cannot stop--you need a base case
; for referential data
;; An;; An AT is one of:
;; - #false
;; - (make-person String AT AT)
interface IAT{}
// Class to represent an unknown in an ancestor tree
class Unknown implements IAT {
Unknown() {}
}
// Class to represent a person in an ancestor tree
class Person implements IAT {
String name;
int yob;
IAT mom;
IAT dad;
Person(String name, int yob, IAT mom, IAT dad) {
this.name = name;
this.yob = yob;
this.mom = mom;
this.dad = dad;
}
}
In this example, IAT
is an interface representing a node in an ancestor tree. The
Unknown
class represents an unknown person, while the Person
class represents a person
with a name, year of birth (yob), and references to their mother (mom
) and father
(dad
), both of which are also of type IAT
.
Understanding unions in Java involves using interfaces to allow a variable to represent one of several types. This concept is powerful for creating flexible and extensible data structures, such as game pieces in a video game or nodes in an ancestor tree. By using interfaces, Java allows for the implementation of union types that can lead to more generalized and maintainable code.
In Java, compound data types are used to represent more complex structures that can contain multiple pieces of data. These are typically implemented using classes. Classes can be either concrete or abstract. Let's explore these concepts in detail.
A concrete class is a class that can be instantiated, meaning you can create objects from it. Concrete classes define attributes and methods that represent the state and behavior of the objects.
Here is an example representing Books
import tester.*;
class Book {
String name;
String author;
double price;
int year;
Book(String name, String author, double price, int year) {
this.name = name;
this.author = author;
this.price = price;
this.year = year;
}
double getPrice() {
return this.price;
}
// to return the discounted price of this book given the discount rate
double discount(double rate) {
return this.price - (rate * this.price);
}
// to return a new book with the same author and name as this book,
// but with price discounted at the given rate
Book discountedBook(double rate) {
return new Book(this.name, this.author, this.discount(rate), this.year);
}
}
/*
* Represent a bunch of books, and computing
* - their total price
* - how many books we have
* - all the books published before the given year
* - a sorted list of books
*/
/*
* A list of books is one of
* empty
* (cons book list-of-books)
*/
// DYNAMIC DISPATCH: deciding which method definition to invoke (in which class)
// based on the information available at runtime of the object that's invoking
// the method
interface ILoBook {
// to compute the total price of all books in this list of books
double totalPrice();
// to count how many books are in this list of books
int count();
// to return a list of all the books in this list of books
// published before the given year
ILoBook allBooksBefore(int year);
// to construct a list of books that contains the same books as
// this list of books, but sorted increasing by price
ILoBook sortByPrice();
ILoBook insert(Book b);
}
class MtLoBook implements ILoBook {
MtLoBook() {
// nothing to do!
}
public double totalPrice() {
return 0;
}
public int count() {
return 0;
}
public ILoBook allBooksBefore(int year) {
return this;
// return new MtLoBook();
}
public ILoBook sortByPrice() {
return this;
// return new MtLoBook();
}
public ILoBook insert(Book b) {
return new ConsLoBook(b, this);
}
}
class ConsLoBook implements ILoBook {
Book first;
ILoBook rest;
ConsLoBook(Book first, ILoBook rest) {
this.first = first;
this.rest = rest;
}
public double totalPrice() {
/* Template:
* FIELDS:
* this.first -- Book
* this.rest -- ILoBook
* METHODS:
* this.totalPrice() -- double
* this.count() -- int
* this.allBooksBefore(int) -- ILoBook
* this.sortByPrice() -- ILoBook
* METHODS OF FIELDS:
* this.first.getPrice() -- double ****
* this.first.discount(double) -- double
* this.first.discountedBook(double) -- Book
* this.rest.totalPrice() -- double *****
* this.rest.count() -- int
* this.rest.allBooksBefore(int) -- ILoBook
* this.rest.sortByPrice() -- ILoBook
*/
return this.first.getPrice() + this.rest.totalPrice();
}
public int count() {
return 1 + this.rest.count();
}
public ILoBook allBooksBefore(int year) {
if (this.first.year < year) {
return new ConsLoBook(
this.first,
this.rest.allBooksBefore(year));
} else {
return this.rest.allBooksBefore(year);
}
}
public ILoBook sortByPrice() {
/*
* Assuming we have:
* ILoBook insert(Book) -- on cons? on ilobook?
* boolean isCheaper(Book) -- on Book
*/
return this.rest.sortByPrice().insert(this.first);
}
// to insert the given book into this sorted list of books
public ILoBook insert(Book b) {
if (b.getPrice() < this.first.getPrice()) {
return new ConsLoBook(b, this);
} else {
return new ConsLoBook(this.first, this.rest.insert(b));
}
}
}
class ExamplesBooks {
Book htdp = new Book("HtDP", "MF", 0.0, 2014);
Book hp = new Book("HP & the search for more money", "JKR", 9000.00, 2015);
Book gatsby = new Book("The Great Gatsby", "FSF", 15.99, 1930);
ILoBook mtList = new MtLoBook();
ILoBook twoBooks = new ConsLoBook(this.htdp,
new ConsLoBook(this.hp,
this.mtList));
ILoBook threeBooks = new ConsLoBook(this.gatsby, this.twoBooks);
}
An abstract class is a class that cannot be instantiated on its own and is meant to be subclassed. Abstract classes can contain abstract methods, which are methods without a body that must be implemented by subclasses. Abstract classes provide a template for other classes to extend.
Here is a great example of complex data struture in java - with a combinations of interfaces, abstract classes, inheritance, etc.
/*
+-----------------------------------------+
| +------------------------------------+ |
| | | |
v v | |
+----------------------------+ | |
| IShape | | |
+----------------------------+ | |
| double area() | | |
| boolean distTo0() | | |
| IShape grow(int) | | |
| boolean biggerThan(IShape) | | |
| boolean contains(CartPt) | | |
+----------------------------+ | |
| | |
/ \ | |
--- | |
| | |
--------------------------------------------- | |
| | | |
+-----------------------------------+ +----------------------------+ | |
| abstract AShape | | Combo | | |
+-----------------------------------+ +----------------------------+ | |
+--| CartPt loc | | IShape top |-+ |
| | String color | | IShape bot |---+
| +-----------------------------------+ +----------------------------+
| | abstract double area() | | double area() |
| | boolean distTo0() | | boolean distTo0() |
| | abstract IShape grow(int) | | IShape grow(int) |
| | boolean biggerThan(IShape) | | boolean biggerThan(IShape) |
| | abstract boolean contains(CartPt) | | boolean contains(CartPt) |
| +-----------------------------------+ +----------------------------+
| |
| / \
| ---
| |
| --------------------------------
| | |
| +--------------------------+ +--------------------------+
| | Circle | | Rect |
| +--------------------------+ +--------------------------+
| | int radius | | int width |
| +--------------------------+ | int height |
| | double area() | +--------------------------+
| | boolean distTo0() | | double area() |
| | IShape grow(int) | | IShape grow(int) |
| | boolean contains(CartPt) | | boolean contains(CartPt) |
| +--------------------------+ +--------------------------+
| / \
| ---
| |
| +-----------------------------+
| | Square |
| +-----------------------------+
| +-----------------------------+
| | IShape grow(int) |
| +-----------------------------+
|
+-------+
|
v
+-----------------------+
| CartPt |
+-----------------------+
| int x |
| int y |
+-----------------------+
| double distTo0() |
| double distTo(CartPt) |
+-----------------------+
*/
interface IShape {
// to compute the area of this shape
public double area();
// to compute the distance form this shape to the origin
public double distTo0();
// to increase the size of this shape by the given increment
public IShape grow(int inc);
// is the area of this shape is bigger than the area of the given shape?
public boolean biggerThan(IShape that);
// does this shape (including the boundary) contain the given point?
public boolean contains(CartPt pt);
}
// to represent a geometric shape
abstract class AShape implements IShape {
CartPt loc;
String color;
AShape(CartPt loc, String color) {
this.loc = loc;
this.color = color;
}
// to compute the area of this shape
public abstract double area();
// to compute the distance form this shape to the origin
public double distTo0(){
return this.loc.distTo0();
}
// to increase the size of this shape by the given increment
public abstract IShape grow(int inc);
// is the area of this shape is bigger than the area of the given shape?
public boolean biggerThan(IShape that){
return this.area() >= that.area();
}
// does this shape (including the boundary) contain the given point?
public abstract boolean contains(CartPt pt);
}
// to represent a circle
class Circle extends AShape {
int radius;
Circle(CartPt center, int radius, String color) {
super(center, color);
this.radius = radius;
}
/* TEMPLATE
Fields:
... this.loc ... -- CartPt
... this.rad ... -- int
... this.color ... -- String
Methods:
... this.area() ... -- double
... this.distTo0() ... -- double
... this.grow(int) ... -- IShape
... this.biggerThan(IShape) ... -- boolean
... this.contains(CartPt) ... -- boolean
Methods for fields:
... this.loc.distTo0() ... -- double
... this.loc.distTo(CartPt) ... -- double
*/
// to compute the area of this shape
public double area(){
return Math.PI * this.radius * this.radius;
}
// to compute the distance form this shape to the origin
public double distTo0(){
return this.loc.distTo0() - this.radius;
}
// to increase the size of this shape by the given increment
public IShape grow(int inc){
return new Circle(this.loc, this.radius + inc, this.color);
}
// does this shape (including the boundary) contain the given point?
public boolean contains(CartPt pt){
return this.loc.distTo(pt) <= this.radius;
}
}
// to represent a rectangle
class Rect extends AShape {
int width;
int height;
Rect(CartPt nw, int width, int height, String color) {
super(nw, color);
this.width = width;
this.height = height;
}
/* TEMPLATE
FIELDS
... this.loc ... -- CartPt
... this.width ... -- int
... this.height ... -- int
... this.color ... -- String
METHODS
... this.area() ... -- double
... this.distTo0() ... -- double
... this.grow(int) ... -- IShape
... this.biggerThan(IShape) ... -- boolean
... this.contains(CartPt) ... -- boolean
METHODS FOR FIELDS:
... this.loc.distTo0() ... -- double
... this.loc.distTo(CartPt) ... -- double
*/
// to compute the area of this shape
public double area(){
return this.width * this.height;
}
// to increase the size of this shape by the given increment
public IShape grow(int inc){
return new Rect(this.loc, this.width + inc, this.height + inc,
this.color);
}
// does this shape (including the boundary) contain the given point?
public boolean contains(CartPt pt){
return (this.loc.x <= pt.x) && (pt.x <= this.loc.x + this.width) &&
(this.loc.y <= pt.y) && (pt.y <= this.loc.y + this.height);
}
}
//to represent a square
class Square extends Rect {
Square(CartPt nw, int size, String color) {
super(nw, size, size, color);
}
/* TEMPLATE
FIELDS
... this.loc ... -- CartPt
... this.width ... -- int
... this.height ... -- int
... this.color ... -- String
METHODS
... this.area() ... -- double
... this.distTo0() ... -- double
... this.grow(int) ... -- IShape
... this.biggerThan(IShape) ... -- boolean
... this.contains(CartPt) ... -- boolean
METHODS FOR FIELDS:
... this.loc.distTo0() ... -- double
... this.loc.distTo(CartPt) ... -- double
*/
// to increase the size of this shape by the given increment
public IShape grow(int inc){
return new Square(this.loc, this.width + inc, this.color);
}
}
// to represent a shape that combines two existing shapes
class Combo implements IShape {
IShape top;
IShape bot;
Combo(IShape top, IShape bot) {
this.top = top;
this.bot = bot;
}
/* TEMPLATE
FIELDS
... this.top ... -- IShape
... this.bot ... -- IShape
METHODS
... this.area() ... -- double
... this.distTo0() ... -- double
... this.grow(int) ... -- IShape
... this.biggerThan(IShape) ... -- boolean
... this.contains(CartPt) ... -- boolean
METHODS FOR FIELDS:
... this.top.area() ... -- double
... this.top.distTo0() ... -- double
... this.top.grow(int) ... -- IShape
... this.top.biggerThan(IShape) ... -- boolean
... this.top.contains(CartPt) ... -- boolean
... this.bot.area() ... -- double
... this.bot.distTo0() ... -- double
... this.bot.grow(int) ... -- IShape
... this.bot.biggerThan(IShape) ... -- boolean
... this.bot.contains(CartPt) ... -- boolean
*/
// to compute the area of this shape
public double area() {
return this.top.area() + this.bot.area();
}
// to compute the distance form this shape to the origin
public double distTo0(){
return Math.min(this.top.distTo0(), this.bot.distTo0());
}
// to increase the size of this shape by the given increment
public IShape grow(int inc) {
return new Combo(this.top.grow(inc), this.bot.grow(inc));
}
// is the area of this shape is bigger than the area of the given shape?
public boolean biggerThan(IShape that){
return this.area() >= that.area();
}
// does this shape (including the boundary) contain the given point?
public boolean contains(CartPt pt) {
return this.top.contains(pt) || this.bot.contains(pt);
}
}
// to represent a Cartesian point
class CartPt {
int x;
int y;
CartPt(int x, int y) {
this.x = x;
this.y = y;
}
/* TEMPLATE
FIELDS
... this.x ... -- int
... this.y ... -- int
METHODS
... this.distTo0() ... -- double
... this.distTo(CartPt) ... -- double
*/
// to compute the distance form this point to the origin
public double distTo0(){
return Math.sqrt(this.x * this.x + this.y * this.y);
}
// to compute the distance form this point to the given point
public double distTo(CartPt pt){
return Math.sqrt((this.x - pt.x) * (this.x - pt.x) +
(this.y - pt.y) * (this.y - pt.y));
}
}
class ExamplesShapes {
ExamplesShapes() {}
CartPt pt1 = new CartPt(0, 0);
CartPt pt2 = new CartPt(3, 4);
CartPt pt3 = new CartPt(7, 1);
IShape c1 = new Circle(new CartPt(50, 50), 10, "red");
IShape c2 = new Circle(new CartPt(50, 50), 30, "red");
IShape c3 = new Circle(new CartPt(30, 100), 30, "blue");
IShape s1 = new Square(new CartPt(50, 50), 30, "red");
IShape s2 = new Square(new CartPt(50, 50), 50, "red");
IShape s3 = new Square(new CartPt(20, 40), 10, "green");
IShape r1 = new Rect(new CartPt(50, 50), 30, 20, "red");
IShape r2 = new Rect(new CartPt(50, 50), 50, 40, "red");
IShape r3 = new Rect(new CartPt(20, 40), 10, 20, "green");
IShape cb1 = new Combo(this.r1, this.c1);
IShape cb2 = new Combo(this.r2, this.r3);
IShape cb3 = new Combo(this.cb1, this.cb2);
// test the method distTo0 in the class CartPt
boolean testDistTo0(Tester t) {
return
t.checkInexact(this.pt1.distTo0(), 0.0, 0.001) &&
t.checkInexact(this.pt2.distTo0(), 5.0, 0.001);
}
// test the method distTo in the class CartPt
boolean testDistTo(Tester t) {
return
t.checkInexact(this.pt1.distTo(this.pt2), 5.0, 0.001) &&
t.checkInexact(this.pt2.distTo(this.pt3), 5.0, 0.001);
}
// test the method area in the shape classes
boolean testShapeArea(Tester t) {
return
t.checkInexact(this.c1.area(), 314.15, 0.01) &&
t.checkInexact(this.s1.area(), 900.0, 0.01) &&
t.checkInexact(this.r1.area(), 600.0, 0.01) &&
t.checkInexact(this.cb1.area(), 914.15926, 0.01) &&
t.checkInexact(this.cb2.area(), 2200.0, 0.01) &&
t.checkInexact(this.cb3.area(), 3114.15926, 0.01);
}
// test the method distTo0 in the shape classes
boolean testShapeDistTo0(Tester t) {
return
t.checkInexact(this.c1.distTo0(), 60.71, 0.01) &&
t.checkInexact(this.c3.distTo0(), 74.40, 0.01) &&
t.checkInexact(this.s1.distTo0(), 70.71, 0.01) &&
t.checkInexact(this.s3.distTo0(), 44.72, 0.01) &&
t.checkInexact(this.r1.distTo0(), 70.71, 0.01) &&
t.checkInexact(this.r3.distTo0(), 44.72, 0.01) &&
t.checkInexact(this.cb1.distTo0(), 60.71, 0.01) &&
t.checkInexact(this.cb2.distTo0(), 44.72, 0.01) &&
t.checkInexact(this.cb3.distTo0(), 44.72, 0.01);
}
// test the method grow in the shape classes
boolean testShapeGrow(Tester t) {
return
t.checkExpect(this.c1.grow(20), this.c2) &&
t.checkExpect(this.s1.grow(20), this.s2) &&
t.checkExpect(this.r1.grow(20), this.r2) &&
t.checkExpect(this.cb1.grow(20), new Combo(this.r2, this.c2));
}
// test the method biggerThan in the shape classes
boolean testShapeBiggerThan(Tester t) {
return
t.checkExpect(this.c1.biggerThan(this.c2), false) &&
t.checkExpect(this.c2.biggerThan(this.c1), true) &&
t.checkExpect(this.c1.biggerThan(this.s1), false) &&
t.checkExpect(this.c1.biggerThan(this.s3), true) &&
t.checkExpect(this.c1.biggerThan(this.cb1), false) &&
t.checkExpect(this.s1.biggerThan(this.s2), false) &&
t.checkExpect(this.s2.biggerThan(this.s1), true) &&
t.checkExpect(this.s1.biggerThan(this.c1), true) &&
t.checkExpect(this.s3.biggerThan(this.c1), false) &&
t.checkExpect(this.s2.biggerThan(this.cb1), true) &&
t.checkExpect(this.r1.biggerThan(this.r2), false) &&
t.checkExpect(this.r2.biggerThan(this.r1), true) &&
t.checkExpect(this.r1.biggerThan(this.c1), true) &&
t.checkExpect(this.r3.biggerThan(this.s1), false) &&
t.checkExpect(this.r2.biggerThan(this.cb1), true) &&
t.checkExpect(this.r3.biggerThan(this.cb1), false) &&
t.checkExpect(this.cb2.biggerThan(this.r1), true) &&
t.checkExpect(this.cb1.biggerThan(this.r2), false) &&
t.checkExpect(this.cb1.biggerThan(this.c1), true) &&
t.checkExpect(this.cb1.biggerThan(this.c3), false) &&
t.checkExpect(this.cb1.biggerThan(this.s2), false) &&
t.checkExpect(this.cb2.biggerThan(this.s1), true) &&
t.checkExpect(this.cb1.biggerThan(this.cb3), false) &&
t.checkExpect(this.cb2.biggerThan(this.cb1), true);
}
// test the method contains in the shape classes
boolean testShapeContains(Tester t) {
return
t.checkExpect(this.c1.contains(new CartPt(100, 100)), false) &&
t.checkExpect(this.c2.contains(new CartPt(40, 60)), true) &&
t.checkExpect(this.s1.contains(new CartPt(100, 100)), false) &&
t.checkExpect(this.s2.contains(new CartPt(55, 60)), true) &&
t.checkExpect(this.r1.contains(new CartPt(100, 100)), false) &&
t.checkExpect(this.r2.contains(new CartPt(55, 60)), true) &&
t.checkExpect(this.cb1.contains(new CartPt(100, 100)), false) &&
t.checkExpect(this.cb2.contains(new CartPt(55, 60)), true);
}
}
In this way, the abstract class Shape
provides a template, ensuring that all shapes
have a color and can calculate their area, while each specific shape can implement the details of how its area
is calculated.
The extends
keyword means that a class inherits everything from another class. For
example, if Circle
, Square
, and Rectangle
extend AShape
, they inherit all its properties and
methods. Because AShape
implements the IShape
interface, these
subclasses also implement IShape
"is-a relationship!!".
You cannot create objects directly from an abstract class like AShape
. Abstract classes
are templates meant to be extended by other classes. When you create a subclass, you use the super
command to send parameters to the constructor of the abstract class.
Abstract classes provide a structure that subclasses must follow. You can also extend non-abstract classes, but extending abstract classes ensures that certain methods and properties are included in all subclasses.
Subclasses can override methods from their abstract superclass to provide specific behavior. The program always uses the method defined in the most specific (or local) class.
Using abstract classes and interfaces makes your code more flexible and easier to manage. It allows you to add new types without changing existing code, which is very helpful in large projects.
Should a combination shape be an instance of AShape
?
No, because a combination shape might not have specific attributes like location or color, which AShape
requires. Instead, it should implement relevant interfaces or extend other classes
that suit its unique properties.
The dot product is an operation that takes two equal-length sequences of numbers (usually coordinate vectors)
and returns a single number. For example, given two lists, (1, 2, 3)
and (5, 6)
, the dot product calculation should return 5*1 + 6*2 + 0*3 = 5 + 12 + 0 = 17
. The concept of double dynamic dispatch allows us to
determine the type of the list (whether it is a ConsLoInt
or MtLoInt
) and call the appropriate method.
interface ILoInt {
int dotProduct(ILoInt that);
// Helper for dot product
int dotProductHelp(int firstOfOriginalList, ILoInt restOfOriginalList);
}
class ConsLoInt implements ILoInt {
int first;
ILoInt rest;
ConsLoInt(int f, ILoInt r) {
this.first = f;
this.rest = r;
}
public int dotProduct(ILoInt that) {
return that.dotProductHelp(this.first, this.rest);
}
public int dotProductHelp(int firstOfOriginalList, ILoInt restOfOriginalList) {
return this.first * firstOfOriginalList + this.rest.dotProduct(restOfOriginalList);
}
}
class MtLoInt implements ILoInt {
public int dotProduct(ILoInt that) {
return 0;
}
public int dotProductHelp(int firstOfOriginalList, ILoInt restOfOriginalList) {
return 0;
}
}
class Examples {
ILoInt mt = new MtLoInt();
ILoInt list1 = new ConsLoInt(4, new ConsLoInt(5, this.mt));
ILoInt list2 = new ConsLoInt(1, new ConsLoInt(2, new ConsLoInt(3, this.mt)));
boolean testDot(Tester t) {
return t.checkExpect(list1.dotProduct(list1), 16 + 25)
&& t.checkExpect(this.list1.dotProduct(this.list2), 4 + 10);
}
}
List abstractions provide ways to operate on lists using functions. Here are some common list abstractions:
[X -> Boolean] [List-of X] -> [List-of X]
[X -> Y] [List-of X] -> [List-of Y]
[X Y -> Y] Y [List-of X] -> Y
[X -> Boolean] [List-of X] -> Boolean
[X -> Boolean] [List-of X] -> Boolean
Currently, we can't implement Foldr
or Map
as they require
knowing the return type Y
.
Using the filter
abstraction, we can create function objects to override methods and
filter a list of paintings based on certain criteria.
// In ConsLoPainting
public ILoPainting filter(IPaintingPredicate pred) {
if(pred.apply(this.first)) {
return new ConsLoPainting(this.first, this.rest.filter(pred));
} else {
return this.rest.filter(pred);
}
}
// In IPaintingPredicate
class BySomeArtist implements IPaintingPredicate {
String artistName;
BySomeArtist(String artistName) {
this.artistName = artistName;
}
// Is the given painting painted by this.artistName?
public boolean apply(Painting p) {
return p.checkArtistName(this.artistName);
}
}
class BeforeSomeYear implements IPaintingPredicate {
int year;
BeforeSomeYear(int year) {
this.year = year;
}
public boolean apply(Painting p) {
return p.year < this.year;
}
}
// Example usage
this.list3.filter(new BySomeArtist("Monet")); // returns a filtered list
// In ILoPainting
boolean ormap(IPaintingPredicate pred);
// In MtLoPainting
boolean ormap(IPaintingPredicate pred) {
return false;
}
// In ConsLoPainting
boolean ormap(IPaintingPredicate pred) {
return pred.apply(this.first) || this.rest.ormap(pred);
}
// In ILoPainting
boolean andmap(IPaintingPredicate pred);
// In MtLoPainting
boolean andmap(IPaintingPredicate pred) {
return true;
}
// In ConsLoPainting
boolean andmap(IPaintingPredicate pred) {
return pred.apply(this.first) && this.rest.andmap(pred);
}
A higher-order predicate is a predicate that takes another predicate as an argument. This allows for more complex filtering and sorting operations.
// In IPaintingPredicate
class AndPredicate implements IPaintingPredicate {
IPaintingPredicate left;
IPaintingPredicate right;
AndPredicate(IPaintingPredicate left, IPaintingPredicate right) {
this.left = left;
this.right = right;
}
/*
* FIELDS:
* this.left ... IPaintingPredicate
* this.right ... IPaintingPredicate
* METHODS:
* this.apply(Painting) ... Boolean
* METHODS FOR FIELDS:
* this.left.apply(Painting) ... Boolean
* this.right.apply(Painting) ... Boolean
*/
// Does the painting pass both predicates?
public boolean apply(Painting p) {
return this.left.apply(p) && this.right.apply(p);
}
}
// Example usage
this.list3.filter(new AndPredicate(new BySomeArtist("Monet"), new BeforeSomeYear(1900)));
These predicates can be nested and used for complex filtering and sorting operations, making your code more flexible and powerful.
There are genral two visitor approches
"The main difference is where the functionality is implemented: either in IList or in IListVisitor. The choice of one over the other will often depend on whether you can modify IList to add more functionality or you cannot modify it but still want to add more functionality." -- Professor Razzaq
A quick reminder/ note on the following higher-order functions:
IList<T> -> IList<T>
: Takes a Predicate<T>
object.IList<T> -> IList<U>
: Takes a Function<T,U>
object.IList<T> -> U
: Takes an initial U value, and a BiFunction<T,U,U>
object.Example of IList
with the direct approch
import java.util.function.*;
import tester.Tester;
//represents a generic list
interface IList<T> {
//filter this list using the given predicate
IList<T> filter(Predicate<T> pred);
//map a given function onto every member of this list and return a list of the results
<U> IList<U> map(Function<T, U> converter);
//combine the items in this list using the given function
<U> U fold(BiFunction<T,U,U> converter, U initial);
}
//represents a generic empty list
class MtList<T> implements IList<T> {
MtList() {}
/* fields:
*
* methods:
* this.filter(Predicate<T>) ... IList<T>
* this.map(Function<T, U>) ... <U> IList<U>
* this.fold(BiFunction<T,U,U>, U) ... <U> U
* methods for fields:
*/
//filter this empty list using the given predicate
public IList<T> filter(Predicate<T> pred) {
return new MtList<T>();
}
//map a given function onto every member of this list and return a list of the results
public <U> IList<U> map(Function<T, U> converter) {
return new MtList<U>();
}
//combine the items in this list using the given function
public <U> U fold(BiFunction<T,U,U> converter, U initial) {
return initial;
}
}
//represents a generic non-empty list
class ConsList<T> implements IList<T> {
T first;
IList<T> rest;
ConsList(T first, IList<T> rest) {
this.first = first;
this.rest = rest;
}
/* fields:
* this.first ... T
* this.rest ... IList<T>
* methods:
* this.filter(Predicate<T>) ... IList<T>
* this.map(Function<T, U>) ... <U> IList<U>
* this.fold(BiFunction<T,U,U>, U) ... <U> U
* methods for fields:
* this.rest.filter(Predicate<T>) ... IList<T>
* this.rest.map(Function<T, U>) ... <U> IList<U>
* this.rest.fold(BiFunction<T,U,U>, U) ... <U> U
*/
//filter this non-empty list using the given predicate
public IList<T> filter(Predicate<T> pred) {
if (pred.test(this.first)) {
return new ConsList<T>(this.first, this.rest.filter(pred));
}
else {
return this.rest.filter(pred);
}
}
//map a given function onto every member of this list and return a list of the results
public <U> IList<U> map(Function<T, U> converter) {
return new ConsList<U>(converter.apply(this.first), this.rest.map(converter));
}
//combine the items in this list using the given function
public <U> U fold(BiFunction<T,U,U> converter, U initial) {
return converter.apply(this.first, this.rest.fold(converter,initial));
}
}
// rep a pred that cjeck str start w a
class StingStartWJ implements Predicate {
public boolean test(String t) {
return t.substring(0,1).equals("a");
}
}
// rep a bifunc that check str end w/ er
class EndWEr implements BiFunction {
@Override
public Integer apply(String t, Integer u) {
return t.substring(t.length() - 2).equals("er") ?
u + 1 : u;
}
}
// rep a func that make str start w first 2 letters
class First2 implements Function {
@Override
public String apply(String t) {
return t.substring(0,2);
}
}
class ExamplesLists{
void testFilter(Tester t) {
months.filter((mon) -> mon.substring(0,1).equals("a")) -> "April"
}
void testFold(Tester t) {
t.checkExpect(months.fold((cur,acc) ->
cur.substring(cur.length() - 2).equals("er") ?
acc + 1 : acc, 0)
, 4)
&& t.checkExpect(months.fold(new EndWEr(), 0), 4);
}
void testMap(Tester t) {
(months.map((cur) -> cur.substring(0,2))) -> list of all months with only 2 letters
}
}
Below is a great graph map for different implementation for this approch (please overlook my handwriting)
Here are examples of higher-order visitor function
// visitor interface for lists
interface IListVisitor<T, R> {
// visits an empty list
R visit(MtList<T> mt);
// visits a non-empty list
R visit(ConsList<T> cons);
}
// visitor that filters the list based on a predicate
class FilterVisitor<T> implements IListVisitor {
Predicate<T> pred;
FilterVisitor(Predicate<T> pred) {
this.pred = pred;
}
// returns the empty list when visiting an empty list
public IList<T> visit(MtList<T> mt) {
return mt;
}
// returns a filtered non-empty list based on the predicate
public IList<T> visit(ConsList<T> cons) {
if (this.pred.test(cons.first)) {
return new ConsList<T>(cons.first, cons.rest.accept(this));
} else {
return cons.rest.accept(this);
}
}
}
// visitor that maps a function onto every element in the list
class MapVisitor<T, U> implements IListVisitor {
Function<T, U> converter;
MapVisitor(Function<T, U> converter) {
this.converter = converter;
}
// returns an empty list when visiting an empty list
public IList<U> visit(MtList<T> mt) {
return new MtList<U>();
}
// returns a mapped non-empty list based on the converter function
public IList<U> visit(ConsList<T> cons) {
return new ConsList<U>(this.converter.apply(cons.first),
cons.rest.accept(this));
}
}
// visitor that folds the list into a single value based on a function and initial value
class FoldRVisitor<T, U> implements IListVisitor<T, U> {
BiFunction<T, U, U> converter;
U initial;
FoldRVisitor(BiFunction<T, U, U> converter, U initial) {
this.converter = converter;
this.initial = initial;
}
// returns the initial value when visiting an empty list
public U visit(MtList<T> mt) {
return initial;
}
// returns the folded value when visiting a non-empty list
public U visit(ConsList<T> cons) {
return this.converter.apply(cons.first, cons.rest.accept(this));
}
}
// visitor that appends a list to another list
class AppendVisitor<T> implements IListVisitor {
IList<T> aList;
AppendVisitor(IList<T> aList) {
this.aList = aList;
}
// returns the list to append when visiting an empty list
public IList<T> visit(MtList<T> mt) {
return aList;
}
// returns a new list that is the result of appending the list to a non-empty list
@Override
public IList<T> visit(ConsList<T> cons) {
return new ConsList<T>(cons.first, cons.rest.accept(this));
}
}
When updating a person's information, it's often better to modify the existing person object rather than creating a new one with the updated value. This approach ensures that any references to the person in other lists or directly will reflect the changes.
class Person {
String name;
int phone;
Person(String name, int phone) {
this.name = name;
this.phone = phone;
}
// Returns true if the given person has the same name and phone number as this person
boolean samePerson(Person that) {
return this.name.equals(that.name) && this.phone == that.phone;
}
// Returns true if this person has the same name as the given String
boolean sameName(String name) {
return this.name.equals(name);
}
// Returns the phone number of this person if they have the same name as the given String
int phoneOf(String name) {
if (this.name.equals(name)) {
return this.phone;
} else {
throw new RuntimeException("The given name does not match this person's name");
}
}
// EFFECT: changes this person's phone number to the given one
void changePhone(int newNum) {
this.phone = newNum;
}
}
interface ILoPerson {
// Returns true if this list contains a person with the given name
boolean contains(String name);
// Gets the phone number of the person with the given name
int findPhoneNum(String name);
// EFFECT: changes the phone number of the person with the given name
void changeNum(String name, int newNum);
}
class MtLoPerson implements ILoPerson {
public boolean contains(String name) {
return false;
}
public int findPhoneNum(String name) {
return -1;
}
public void changeNum(String name, int newNum) {}
}
class ConsLoPerson implements ILoPerson {
Person first;
ILoPerson rest;
ConsLoPerson(Person first, ILoPerson rest) {
this.first = first;
this.rest = rest;
}
public boolean contains(String name) {
return this.first.sameName(name) || this.rest.contains(name);
}
public int findPhoneNum(String name) {
if (this.first.sameName(name)) {
return this.first.phoneOf(name);
} else {
return this.rest.findPhoneNum(name);
}
}
public void changeNum(String name, int newNum) {
if (this.first.sameName(name)) {
this.first.changePhone(newNum);
} else {
this.rest.changeNum(name, newNum);
}
}
}
class Examples {
ILoPerson friends;
ILoPerson family;
Person frank;
void initData() {
frank = new Person("Frank", 7294);
friends = new ConsLoPerson(frank, new MtLoPerson());
family = new ConsLoPerson(frank, new MtLoPerson());
}
void testChangePhoneNum(Tester t) {
this.initData();
t.checkExpect(this.friends.findPhoneNum("Frank"), 7294);
t.checkExpect(this.family.findPhoneNum("Frank"), 7294);
t.checkExpect(this.frank.phone, 7294);
this.friends.changeNum("Frank", 4927);
t.checkExpect(this.friends.findPhoneNum("Frank"), 4927);
t.checkExpect(this.family.findPhoneNum("Frank"), 4927);
t.checkExpect(this.frank.phone, 4927);
}
}
Aliasing occurs when multiple variables refer to the same object. Here are some examples to illustrate this concept:
Person alice1 = new Person("Alice", 67890);
Person alice2 = new Person("Alice", 67890);
Person alice3 = alice1; // Alias for alice1
alice1.samePerson(alice2); // -> false, different objects with the same values
alice1.samePerson(alice3); // -> true, same object
alice1.phone = 09676; // Change phone number of alice1
alice1.samePerson(alice2); // -> false, still different objects
alice1.samePerson(alice3); // -> true, still the same object
There are generally two kinds of equality:
Examples:
Person alice1 = new Person("Alice", 67890);
Person alice2 = new Person("Alice", 67890);
Person alice3 = alice1; // Alias for alice1
alice1.equals(alice2); // -> false, different objects with the same values (intensional equality)
alice1.equals(alice3); // -> true, same object (intensional equality)
class Counter {
int val;
Counter() {
this(0);
}
Counter(int initialVal) {
this.val = initialVal;
}
int get() {
int ans = this.val;
this.val = this.val + 1;
return ans;
}
}
class ExamplesCounter {
boolean testCounter(Tester t) {
Counter c1 = new Counter();
Counter c2 = new Counter(5);
Counter c3 = c1;
return t.checkExpect(c1.get(), 0) // Test 1
&& t.checkExpect(c2.get(), 5) // Test 2
&& t.checkExpect(c3.get(), 1) // Test 3
&& t.checkExpect(c1.get() == c1.get(), false) // Test 4
&& t.checkExpect(c2.get() == c1.get(), true) // Test 5
&& t.checkExpect(c2.get() == c1.get(), true) // Test 6
&& t.checkExpect(c1.get() == c1.get(), false) // Test 7
&& t.checkExpect(c2.get() == c1.get(), false);// Test 8
}
}
When abstracting over types in Java, you can use the Void
type if a method should not return
anything. In the method, you return null
. This is different from the primitive void
type.
interface IList<T> {
// EFFECT: modifies the first object in this list that passes the predicate
void modify(IPred<T> whichOne, IFunc whatToDo);
}
class MtList<T> implements IList<T> {
public T find(IPred<T> whichOne) {
return null;
}
// EFFECT: modifies the first object in this list that passes the predicate
public void modify(IPred<T> whichOne, IFunc whatToDo) {}
}
class ConsList<T> implements IList<T> {
T first;
IList<T> rest;
ConsList(T first, IList<T> rest) {
this.first = first;
this.rest = rest;
}
public T find(IPred<T> whichOne) {
if (whichOne.apply(this.first)) {
return this.first;
} else {
return this.rest.find(whichOne);
}
}
// EFFECT: modifies the first object in this list that passes the predicate
public void modify(IPred<T> whichOne, IFunc whatToDo) {
if (whichOne.apply(this.first)) {
whatToDo.apply(this.first);
} else {
this.rest.modify(whichOne, whatToDo);
}
}
}
class ChangePhone implements IFunc {
int num;
ChangePhone(int num) {
this.num = num;
}
public Void apply(Person x) {
x.phone = this.num;
return null;
}
}
In the example above, ChangePhone
implements IFunc
and modifies the phone number of a
Person
object. This is a common pattern when you need to apply changes to objects in a list.
Loops are a fundamental concept in programming that allow you to execute a block of code multiple times. Java supports several types of loops:
Every while
loop can be converted into a counted for
loop, and every counted
for
loop can be transformed into a for-each loop.
The for-each loop is used to iterate over elements in a collection, such as an array or an ArrayList.
... setup ...
for (T item : list) {
... body ...
}
... use the results ...
Example:
ArrayList strings = new ArrayList<>(Arrays.asList("one", "two", "three"));
for (String s : strings) {
System.out.println(s);
}
A counted for loop is used when you know in advance how many times you want to execute a statement or a block of statements.
for (initialization; termination condition; update statement) {
... body ...
}
Explanation:
Example:
for (int i = 0; i < 10; i++) {
System.out.println(i);
}
This example finds the index of the string that comes first alphabetically in an ArrayList:
// In ArrayListUtils
int findIndexOfMinItem(ArrayList strings) {
if (strings.size() == 0) {
throw new RuntimeException("No min of an empty list");
}
int currentMin = 0;
for (int i = 0; i < strings.size(); i++) {
if (strings.get(currentMin).compareTo(strings.get(i)) > 0) {
currentMin = i;
}
}
return currentMin;
}
Nested for loops are used when you need to perform multiple iterations within another iteration. This is often used for multi-dimensional arrays or creating combinations.
Example: Get every combination of suits and values to create a deck of cards:
// In ArrayListUtils class
ArrayList makeDeck() {
ArrayList cards = new ArrayList();
ArrayList suits = new ArrayList(Arrays.asList("hearts", "diamonds", "spades", "clubs"));
ArrayList values = new ArrayList(Arrays.asList("ace", "two", "three", "four"));
for (int i = 0; i < values.size(); i++) {
for (int j = 0; j < suits.size(); j++) {
cards.add(new Card(values.get(i), suits.get(j)));
}
}
return cards;
}
// Outside of ArrayListUtils class
class Card {
String value;
String suit;
Card(String value, String suit) {
this.value = value;
this.suit = suit;
}
}
A while loop repeatedly executes a block of statements while a specified condition is true. It is used when you do not know in advance how many times the loop should run.
... initialize ...
while (termination condition) {
... body ...
... update ...
}
Example:
// Count to 10 using a while loop
int count = 0;
while (count < 10) {
System.out.println(count);
count++;
}
While loops are useful when the termination condition is not based on a simple counter and may depend on more complex logic or input from the user.
Selection Sort is a simple comparison-based sorting algorithm. The algorithm divides the input list into two parts: the sublist of items already sorted, which is built up from left to right at the front of the list, and the sublist of items remaining to be sorted that occupy the rest of the list.
// In ILoInt
ILoInt nsort();
ILoInt insert(int n);
// In Mt
ILoInt nsort() {
return this;
}
ILoInt insert(int n) {
return new ConsLoInt(n, this);
}
// In Cons
ILoInt nsort() {
return this.rest.nsort().insert(this.first);
}
ILoInt insert(int n) {
if (this.first < n) {
return new ConsLoInt(this.first, this.rest.insert(n));
} else {
return new ConsLoInt(n, this);
}
}
Insertion Sort is a simple sorting algorithm that builds the final sorted array one item at a time. It is much less efficient on large lists than more advanced algorithms such as quicksort, heapsort, or merge sort.
// In ILoInt
ILoInt nsort();
ILoInt insert(int n);
// In Mt
ILoInt nsort() {
return this;
}
ILoInt insert(int n) {
return new ConsLoInt(n, this);
}
// In Cons
ILoInt nsort() {
return this.rest.nsort().insert(this.first);
}
ILoInt insert(int n) {
if (this.first < n) {
return new ConsLoInt(this.first, this.rest.insert(n));
} else {
return new ConsLoInt(n, this);
}
}
Quicksort is a highly efficient sorting algorithm and is based on partitioning of the array of data into smaller arrays. A large array is partitioned into two arrays, one of which holds values smaller than the specified value, say pivot, based on which the partition is made, and another array holds values greater than the pivot value.
For a more detailed explanation and live analysis, please refer here, where I did a full presentation on Quicksort
while (low <= high) {
while (list.get(low) <= pivot) {
low++;
}
while (list.get(high) > pivot) {
high--;
}
if (low < high) {
swap(list, low, high);
}
}
Merge Sort is an efficient, stable, comparison-based, divide and conquer sorting algorithm. Most implementations produce a stable sort, which means that the implementation preserves the input order of equal elements in the sorted output.
Example:
Consider the list: 4 7 6 5 3 0 1
Sort pairs of numbers:
47 56 03 1
Merge:
4 5 6 7 0 1 3
0 1 3 4 5 6 7
Note: You don’t have to compare every element to every other element if each sublist is already sorted. This can also be done in place by advancing low and high pointers.
Priority queues are abstract data types similar to regular queues or stacks, but with an added priority for each element. The element with the highest priority is served before the elements with lower priority.
A max heap is a special tree-based data structure that satisfies the following properties:
Upheap: When adding a new element, place it in the next available position to maintain structural integrity, then swap it up to maintain heap order.
Downheap: When removing the root element, replace it with the last element in the heap, then swap it down to maintain heap order.
// Array-based representation of a heap
leftChild(i) = 2 * i + 1
rightChild(i) = 2 * i + 2
parent(i) = (i - 1) / 2 (integer math)
Example of adding an element:
Index 0 1 2 3 4 5 6 7 8 9 10
Priority 70 65 50 30 60 40 20 10 20 15 50
// Add 65 and swap with its parent until heap property is restored
Index 0 1 2 3 4 5 6 7 8 9 10
Priority 70 65 50 30 60 40 20 10 20 15 50
Example of removing an element:
Index 0 1 2 3 4 5 6 7 8 9
Priority 60 50 50 30 15 40 20 10 20 70
// Remove 70 and replace with last element, then downheap
Index 0 1 2 3 4 5 6 7 8 9
Priority 60 50 50 30 15 40 20 10 20 70
Heapsort is a comparison-based sorting algorithm that uses a binary heap data structure. It divides its input into a sorted and an unsorted region, and iteratively shrinks the unsorted region by extracting the largest element and moving it to the sorted region.
To build a heap from an ArrayList:
For each i from 1 to the size of the ArrayList,
upheap(i)
To upheap at index idx:
Let parentIdx = floor((idx - 1) / 2)
If the item at index idx is greater than the item at index parentIdx:
Swap items at indices idx and parentIdx
upheap(parentIdx)
To remove the maximum item:
Swap the item at index 0 with the item at the index of the last heap item
downheap(0)
To downheap at index idx:
Let leftIdx = 2 * idx + 1
Let rightIdx = 2 * idx + 2
If the item at index idx is less than either of the items at leftIdx or rightIdx:
Let biggestIdx = index of the largest of the items at leftIdx or rightIdx
Swap items at indices idx and biggestIdx
downheap(biggestIdx)
A Deque (Double-Ended Queue) is a linear collection that supports element insertion and removal at both ends. This implementation includes nodes, a sentinel node, and operations for manipulating the deque.
abstract class ANode<T> {
ANode<T> next;
ANode<T> prev;
// Constructor with next and prev nodes
ANode(ANode<T> next, ANode<T> prev) {
this.next = next;
this.prev = prev;
}
// Constructor with no arguments (used for Sentinel)
ANode() {
this.next = this;
this.prev = this;
}
}
The ANode
class serves as a base class for the sentinel and data nodes. It maintains references to
the next and previous nodes, facilitating bidirectional traversal.
class Node<T> extends ANode<T> {
T data;
// Constructor with just data (used for creating new nodes)
Node(T data) {
super();
this.data = data;
}
// Constructor with data, next, and prev nodes
Node(T data, ANode<T> next, ANode<T> prev) {
super(next, prev);
if (next == null || prev == null) {
throw new IllegalArgumentException("Next and prev nodes cannot be null");
}
this.data = data;
this.next.prev = this;
this.prev.next = this;
}
}
The Node
class extends ANode
and adds a data field. It provides
constructors for creating nodes with or without specified next and previous nodes.
class Sentinel<T> extends ANode<T> {
// Default constructor for sentinel node
Sentinel() {
super();
}
}
The Sentinel
class represents a special node that does not hold data. It serves as a
marker for the start and end of the deque, making boundary conditions simpler to handle.
class Deque<T> {
Sentinel<T> header;
// Constructor with sentinel node
Deque(Sentinel<T> header) {
this.header = header;
}
// Default constructor
Deque() {
this.header = new Sentinel<T>();
}
// Counts the number of nodes in the Deque, not including the header node
int size() {
int count = 0;
ANode<T> current = this.header.next;
while (current != this.header) {
count++;
current = current.next;
}
return count;
}
// Effect: consumes a value of type T and inserts it at the front of the list
void addAtHead(T val) {
new Node<>(val, this.header.next, this.header);
}
// Effect: consumes a value of type T and inserts it at the back of the list
void addAtTail(T val) {
new Node<>(val, this.header, this.header.prev);
}
// Returns the removed note
// Effect: removes the node at the head of the linked list and returns that node
T removeFromHead() {
if (this.header.prev == this.header
|| this.header.next == this.header) {
throw new RuntimeException("Cannot remove from empty deque");
}
Node<T> firstNode = (Node<T>) this.header.next;
this.header.next = firstNode.next;
firstNode.next.prev = this.header;
return firstNode.data;
}
// Returns the removed note
// Effect: removes the node at the tail of the linked list and returns that node
T removeFromTail() {
if (this.header.prev == this.header
|| this.header.next == this.header) {
throw new RuntimeException("Cannot remove from empty deque");
}
Node<T> lastNode = (Node<T>) this.header.prev;
this.header.prev = lastNode.prev;
lastNode.prev.next = this.header;
return lastNode.data;
}
// Finds the first node within this deque that satisfies the given predicate
ANode<T> find(Predicate<T> pred) {
ANode<T> current = this.header.next;
while (current != this.header) {
Node<T> node = (Node<T>) current;
if (pred.test(node.data)) {
return current;
}
current = current.next;
}
return this.header;
}
// Appends a given Deque<T> to the end of this Deque<T>
void append(Deque<T> dq) {
int size = dq.size();
for (int i = 0; i < size; i++) {
this.addAtTail(dq.removeFromHead());
}
}
// Reverses the order of the elements in the deque
void reverse() {
Deque<T> d = new Deque<T>();
for (int i = size() - 1; i >= 0; i--) {
T last = this.removeFromTail();
d.addAtTail(last);
}
this.append(d);
}
}
The Deque
class manages the sentinel node and provides various methods to manipulate
the deque:
Lovely reminder: these examples are accessing a field of a field. They are meant to showcase the functionality! Please add helpers to rely on dynamic dispatch in case of an exam! (they should not take too long if you understand the structure of a deque)
Assuming we have an ArrayList
called alist
, we often want to
iterate over its elements and perform some operations. This can be done using a for-each loop:
for (T t : alist) {
... do something ...
}
In English, this is what we want:
while (there are more items in alist) {
T t = get next item;
... do something ...
// We need to keep track of the state
}
Remember: Always call hasNext
before you try to get the next item!
This is already implemented in Java. See the documentation for Iterator
and Iterable
interfaces.
interface Iterator<T> {
// Are there any more items to process?
boolean hasNext();
// Gets the next item to process
// Usually advances to the item following the one returned
T next();
}
interface Iterable<T> {
Iterator<T> iterator();
}
class ArrayList<T> implements Iterable<T> {
...
Iterator<T> iterator() {
// And the ArrayListIterator implements Iterator
return new ArrayListIterator<T>(this);
}
}
class ArrayListIterator<T> implements Iterator<T> {
ArrayList<T> items;
int currentIndex;
ArrayListIterator(ArrayList<T> items) {
this.items = items;
this.currentIndex = 0;
}
// Are there any more items to process?
public boolean hasNext() {
return this.currentIndex < this.items.size();
}
// Gets the next item to process
// EFFECT: Add 1 to the current index
public T next() {
T result = this.items.get(this.currentIndex);
this.currentIndex++;
return result;
// OR (this might work)
// return this.items.get(this.currentIndex++);
}
}
In the Examples
class, we can use the iterator to simulate what a for-each loop does behind the
scenes:
// In Examples class
Iterator<T> arrayListIter = new ArrayListIterator<T>(alist);
while (arrayListIter.hasNext()) {
T t = arrayListIter.next();
... body ...
}
For linked lists, we have to use extends
when it's an interface.
interface IList<T> extends Iterable<T> {
// Is this list a cons?
boolean isCons();
}
class MtList<T> implements IList<T> {
// Returns an Iterator especially for this empty list
public Iterator<T> iterator() {
return new IListIterator<T>(this);
}
// Is this list a cons?
public boolean isCons() {
return false;
}
}
class ConsList<T> implements IList<T> {
T first;
IList<T> rest;
ConsList(T first, IList<T> rest) {
this.first = first;
this.rest = rest;
}
// Returns an Iterator especially for this non-empty list
public Iterator<T> iterator() {
return new IListIterator<T>(this);
}
// Is this list a cons?
public boolean isCons() {
return true;
}
}
class IListIterator<T> implements Iterator<T> {
IList<T> items;
IListIterator(IList<T> items) {
this.items = items;
}
// Are there any more items to process?
public boolean hasNext() {
return this.items.isCons();
}
// Gets the next item to process
// EFFECT: Queues the next
public T next() {
ConsList<T> cons = (ConsList<T>) this.items;
T answer = cons.first;
items = cons.rest;
return answer;
}
}
We can now use for-each loops with our custom ILists:
class Examples {
IList ints = new ConsList<>(2, new ConsList<>(1, new MtList<>()));
IList strs = new ConsList<>("fundies", new ConsList<>("two", new MtList<>()));
int result;
String str;
int count() {
result = 0;
for (Integer i : ints2) {
result += i;
}
return result;
}
// checkExpect(result, 3) -> true
String count2() {
str = "";
for (String i : strs) {
str += i;
}
return str;
}
// checkExpect(strs, "fundiestwo") -> true
}
Higher-order iterators are iterators that operate on other iterators. They can modify the behavior of the
underlying iterator, allowing for more flexible and reusable code. In this explanation, we will explore two
higher-order iterators: EveryOtherIter
and TakeN
.
The EveryOtherIter
class represents an iterator that returns every other item from a
given sequence. It skips every second item in the source iterator.
class EveryOtherIter<T> implements Iterator<T> {
Iterator<T> source;
EveryOtherIter(Iterator<T> source) {
this.source = source;
}
public boolean hasNext() {
// this sequence has a next item if the source does
return this.source.hasNext();
}
public T next() {
if (!this.hasNext()) {
throw new NoSuchElementException("there are no more items!");
}
T answer = this.source.next(); // gets the answer, and advances the source
// We need to have the source "skip" the next value
if (this.source.hasNext()) {
this.source.next(); // get the next value and ignore it
}
return answer;
}
public void remove() {
// We can remove an item if our source can remove the item
this.source.remove(); // so just delegate to the source
}
}
The EveryOtherIter
class takes an existing iterator as its source and overrides the
hasNext
and next
methods to return every other item:
class ExamplesEveryOther {
ArrayList strs = new ArrayList<>(Arrays.asList("bird", "cat", "dog"));
EveryOtherIter eo = new EveryOtherIter<>(this.strs.iterator());
void testEveryOtherIter(Tester t) {
t.checkExpect(this.eo.hasNext(), true);
t.checkExpect(this.eo.next(), "bird");
t.checkExpect(this.eo.hasNext(), true);
t.checkExpect(this.eo.next(), "dog");
t.checkExpect(this.eo.hasNext(), false);
}
}
The TakeN
class represents an iterator that returns only the first n
items from a given sequence.
class TakeN<T> implements Iterator<T> {
Iterator<T> source;
int howMany;
int countSoFar;
TakeN(Iterator<T> source, int n) {
this.source = source;
this.howMany = n;
this.countSoFar = 0;
}
public boolean hasNext() {
return (this.countSoFar < this.howMany) && this.source.hasNext();
}
public T next() {
if (!this.hasNext()) {
throw new NoSuchElementException("there are no more items!");
}
this.countSoFar++;
return this.source.next();
}
public void remove() {
// We can remove an item if our source can remove the item
this.source.remove(); // so just delegate to the source
}
}
The TakeN
class takes an existing iterator as its source and a number n
. It overrides the hasNext
and next
methods
to return only the first n
items:
n
.class ExamplesTakeN {
ArrayList nums = new ArrayList<>(Arrays.asList(1, 2, 3, 4, 5));
TakeN tn = new TakeN<>(this.nums.iterator(), 3);
void testTakeN(Tester t) {
t.checkExpect(this.tn.hasNext(), true);
t.checkExpect(this.tn.next(), 1);
t.checkExpect(this.tn.hasNext(), true);
t.checkExpect(this.tn.next(), 2);
t.checkExpect(this.tn.hasNext(), true);
t.checkExpect(this.tn.next(), 3);
t.checkExpect(this.tn.hasNext(), false);
}
}
Higher-order iterators like EveryOtherIter
and TakeN
allow for
flexible and reusable iteration patterns. By implementing custom iterators, we can extend the functionality of
existing collections and create more complex iteration behaviors.
Algorithm analysis involves calculating the complexity of algorithms, focusing on time complexity and space complexity. Common tools for this are the RAM model of computation and asymptotic analysis of worst-case complexity.
An algorithm's input determines the number of steps it takes to run. For a given input size n
:
For example, if an algorithm takes 2n
steps in the worst case, the worst-case function for time
complexity is T(n) = 2n
.
Big O notation describes the limiting behavior of a function as its input tends towards infinity. It simplifies the comparison of algorithm efficiencies by focusing on the dominant term and ignoring constants.
f(n) = O(g(n))
means f(n)
is at
most c * g(n)
for some constant c
and sufficiently large n
.f(n) = Ω(g(n))
means f(n)
is at least c * g(n)
for some constant c
and sufficiently large n
.f(n) = Θ(g(n))
means f(n)
is both O(g(n))
and Ω(g(n))
.Big O notation creates classes of functions that describe their growth rates. Common classes include:
O(1)
: Constant timeO(log n)
: Logarithmic time (e.g., binary search)O(n)
: Linear timeO(n log n)
: Superlinear time (e.g., mergesort)O(n²)
: Quadratic timeO(2^n)
: Exponential timeO(n!)
: Factorial timeThe following table shows the growth rates of common classes:
n | log n | n | n log n | n² | 2^n | n! |
---|---|---|---|---|---|---|
10 | 0.003 μs | 0.01 μs | 0.033 μs | 0.1 μs | 1 μs | 3.63 ms |
20 | 0.004 μs | 0.02 μs | 0.086 μs | 0.4 μs | 1 ms | 77.1 years |
30 | 0.005 μs | 0.03 μs | 0.147 μs | 0.9 μs | 1 sec | 8.4 × 10^15 years |
40 | 0.005 μs | 0.04 μs | 0.213 μs | 1.6 μs | 18.3 min | - |
50 | 0.006 μs | 0.05 μs | 0.282 μs | 2.5 μs | 13 days | - |
Donald Knuth (June–July 1998). "Teach Calculus with Big O" (PDF). Notices of the American Mathematical Society. 45 (6): 687. Archived (PDF) from the original on 2021-10-14. Retrieved 2021-09-05. (Unabridged version Archived 2008-05-13 at the Wayback Machine)
Here are some examples to illustrate different time complexities in Java:
public class ConstantTime {
public int getFirstElement(int[] arr) {
return arr[0];
}
}
public class LinearTime {
public int sumArray(int[] arr) {
int sum = 0;
for (int num : arr) {
sum += num;
}
return sum;
}
}
public class QuadraticTime {
public void printPairs(int[] arr) {
for (int i = 0; i < arr.length; i++) {
for (int j = 0; j < arr.length; j++) {
System.out.println(arr[i] + ", " + arr[j]);
}
}
}
}
public class ExponentialTime {
public int fibonacci(int n) {
if (n <= 1) {
return n;
}
return fibonacci(n - 1) + fibonacci(n - 2);
}
}
Binary Search is a search algorithm that finds the position of a target value within a sorted array. It works by repeatedly dividing the search interval in half:
mid = ⌊(low + high) / 2⌋
mid - 1
.mid + 1
.Binary Search is a search algorithm that finds the position of a target value within a sorted array. It works by repeatedly dividing the search interval in half:
public class BinarySearch {
public static int binarySearch(int[] arr, int target) {
int left = 0;
int right = arr.length - 1;
while (left <= right) {
int mid = left + (right - left) / 2;
if (arr[mid] == target) {
return mid;
} else if (arr[mid] < target) {
left = mid + 1;
} else {
right = mid - 1;
}
}
return -1; // target not found
}
class Examples {
int[] arr = {1, 2, 3, 4, 5, 6, 7, 8, 9};
int target = 5;
int result = binarySearch(arr, target);
// Index of target:
result
}
}
Binary Search has a time complexity of O(log n)
. It is very efficient for searching in large
sorted arrays because it reduces the search space by half in each step.
Breadth-First Search (BFS) is a search algorithm that explores all the nodes at the present depth level before moving on to nodes at the next depth level:
Breadth-First Search (BFS) is a search algorithm that explores all the nodes at the present depth level before moving on to nodes at the next depth level:
import java.util.*;
public class BreadthFirstSearch {
class Graph {
private int V; // Number of vertices
private LinkedList adj[]; // Adjacency lists
Graph(int v) {
V = v;
adj = new LinkedList[v];
for (int i = 0; i < v; ++i) {
adj[i] = new LinkedList();
}
}
void addEdge(int v, int w) {
adj[v].add(w);
}
void BFS(int s) {
boolean visited[] = new boolean[V];
LinkedList queue = new LinkedList();
visited[s] = true;
queue.add(s);
while (queue.size() != 0) {
s = queue.poll();
System.out.print(s + " ");
Iterator i = adj[s].listIterator();
while (i.hasNext()) {
int n = i.next();
if (!visited[n]) {
visited[n] = true;
queue.add(n);
}
}
}
}
}
class Examples {
Graph g = new Graph(4);
g.addEdge(0, 1);
g.addEdge(0, 2);
g.addEdge(1, 2);
g.addEdge(2, 0);
g.addEdge(2, 3);
g.addEdge(3, 3);
// Breadth First Traversal starting from vertex 2:
g.BFS(2);
}
}
Breadth-First Search (BFS) has a time complexity of O(V + E)
, where V
is the number
of vertices and E
is the number of edges. It is useful for finding the shortest path in an
unweighted graph.
Depth-First Search (DFS) is a search algorithm that explores as far as possible along each branch before backtracking:
Depth-First Search (DFS) is a search algorithm that explores as far as possible along each branch before backtracking:
import java.util.*;
public class DepthFirstSearch {
class Graph {
private int V; // Number of vertices
private LinkedList adj[]; // Adjacency lists
Graph(int v) {
V = v;
adj = new LinkedList[v];
for (int i = 0; i < v; ++i) {
adj[i] = new LinkedList();
}
}
void addEdge(int v, int w) {
adj[v].add(w);
}
void DFSUtil(int v, boolean visited[]) {
visited[v] = true;
System.out.print(v + " ");
Iterator i = adj[v].listIterator();
while (i.hasNext()) {
int n = i.next();
if (!visited[n]) {
DFSUtil(n, visited);
}
}
}
void DFS(int v) {
boolean visited[] = new boolean[V];
DFSUtil(v, visited);
}
}
class Examples {
Graph g = new Graph(4);
g.addEdge(0, 1);
g.addEdge(0, 2);
g.addEdge(1, 2);
g.addEdge(2, 0);
g.addEdge(2, 3);
g.addEdge(3, 3);
// Depth First Traversal starting from vertex 2:
g.DFS(2);
}
}
Depth-First Search (DFS) has a time complexity of O(V + E)
, where V
is the number of
vertices and E
is the number of edges. It is useful for exploring all the nodes in a graph and can
be used to detect cycles.
[1] Knuth, Donald (1998). Sorting and Searching. The Art of Computer Programming. Vol. 3 (2nd ed.). Reading, MA: Addison-Wesley Professional.
Finding the shortest path between two vertices in a graph is a common problem in computer science. The algorithm discussed here calculates the cost of the cheapest path as well as the actual path between the source and target vertices.
The algorithm works by using a priority queue to explore the shortest path in a weighted graph:
import java.util.*;
class Vertex {
String name;
List outEdges = new ArrayList<>();
Vertex(String name) {
this.name = name;
}
void addEdge(Vertex to, int weight) {
outEdges.add(new Edge(this, to, weight));
}
}
class Edge {
Vertex from;
Vertex to;
int weight;
Edge(Vertex from, Vertex to, int weight) {
this.from = from;
this.to = to;
this.weight = weight;
}
}
class Graph {
// Find the cost of the cheapest path between the two given vertices
int shortestPathCost(Vertex source, Vertex target) {
ArrayList unvisited = new ArrayList<>();
HashMap distances = new HashMap<>();
HashMap predecessors = new HashMap<>();
unvisited.add(source);
distances.put(source, 0);
while (unvisited.size() > 0) {
Vertex v = unvisited.remove(0);
for (Edge e : v.outEdges) {
if (distances.get(e.to) == null || distances.get(e.to) > distances.get(v) + e.weight) {
distances.put(e.to, distances.get(v) + e.weight);
predecessors.put(e.to, v);
unvisited.add(e.to);
}
}
}
return distances.getOrDefault(target, -1);
}
// Find the cheapest path between the two given vertices
ArrayList shortestPath(Vertex source, Vertex target) {
ArrayList unvisited = new ArrayList<>();
HashMap distances = new HashMap<>();
HashMap predecessors = new HashMap<>();
unvisited.add(source);
distances.put(source, 0);
while (unvisited.size() > 0) {
Vertex v = unvisited.remove(0);
for (Edge e : v.outEdges) {
if (distances.get(e.to) == null || distances.get(e.to) > distances.get(v) + e.weight) {
distances.put(e.to, distances.get(v) + e.weight);
predecessors.put(e.to, v);
unvisited.add(e.to);
}
}
}
ArrayList path = new ArrayList<>();
Vertex step = target;
if (predecessors.get(step) == null) {
return path;
}
path.add(step);
while (step != source) {
step = predecessors.get(step);
path.add(0, step);
}
return path;
}
}
public class Examples {
Vertex v1 = new Vertex("A");
Vertex v2 = new Vertex("B");
Vertex v3 = new Vertex("C");
Vertex v4 = new Vertex("D");
v1.addEdge(v2, 1);
v2.addEdge(v3, 2);
v3.addEdge(v4, 1);
v1.addEdge(v4, 4);
Graph g = new Graph();
System.out.println("Shortest path cost from A to D: " + g.shortestPathCost(v1, v4));
ArrayList path = g.shortestPath(v1, v4);
System.out.print("Path: ");
for (Vertex v : path) {
System.out.print(v.name + " ");
}
}
This shortest path algorithm, similar to Dijkstra's algorithm, has a time complexity of O(V^2)
due to the use of a simple unvisited list. This can be improved to O(V log V + E)
by using a priority queue (such as a min-heap) instead of a list for the
unvisited vertices. It efficiently finds the shortest path in graphs with non-negative edge weights.
Finding the Minimum Spanning Tree (MST) of a graph is a fundamental problem in computer science. The MST connects all vertices in a weighted graph such that the total edge weight is minimized, and no cycles are formed. We will explore three different algorithms to solve this problem: Borůvka’s, Prim’s, and Kruskal’s algorithms.
Borůvka’s algorithm starts with each vertex as a separate component and repeatedly adds the shortest edge from each component to another component until all components are connected.
import java.util.*;
class Vertex {
String name;
List outEdges = new ArrayList<>();
Vertex(String name) {
this.name = name;
}
void addEdge(Vertex to, int weight) {
outEdges.add(new Edge(this, to, weight));
}
}
class Edge implements Comparable {
Vertex from;
Vertex to;
int weight;
Edge(Vertex from, Vertex to, int weight) {
this.from = from;
this.to = to;
this.weight = weight;
}
public int compareTo(Edge other) {
return this.weight - other.weight;
}
}
class Graph {
List vertices = new ArrayList<>();
List edges = new ArrayList<>();
void addVertex(Vertex v) {
vertices.add(v);
}
void addEdge(Vertex from, Vertex to, int weight) {
Edge edge = new Edge(from, to, weight);
from.addEdge(to, weight);
edges.add(edge);
}
List boruvka() {
List mst = new ArrayList<>();
UnionFind uf = new UnionFind(vertices.size());
while (mst.size() < vertices.size() - 1) {
Edge[] cheapest = new Edge[vertices.size()];
for (Edge e : edges) {
int set1 = uf.find(vertices.indexOf(e.from));
int set2 = uf.find(vertices.indexOf(e.to));
if (set1 != set2) {
if (cheapest[set1] == null || e.weight < cheapest[set1].weight) {
cheapest[set1] = e;
}
if (cheapest[set2] == null || e.weight < cheapest[set2].weight) {
cheapest[set2] = e;
}
}
}
for (Edge e : cheapest) {
if (e != null) {
int set1 = uf.find(vertices.indexOf(e.from));
int set2 = uf.find(vertices.indexOf(e.to));
if (set1 != set2) {
mst.add(e);
uf.union(set1, set2);
}
}
}
}
return mst;
}
}
class UnionFind {
int[] parent;
int[] rank;
UnionFind(int size) {
parent = new int[size];
rank = new int[size];
for (int i = 0; i < size; i++) {
parent[i] = i;
rank[i] = 0;
}
}
int find(int i) {
if (parent[i] != i) {
parent[i] = find(parent[i]);
}
return parent[i];
}
void union(int i, int j) {
int root1 = find(i);
int root2 = find(j);
if (root1 != root2) {
if (rank[root1] > rank[root2]) {
parent[root2] = root1;
} else if (rank[root1] < rank[root2]) {
parent[root1] = root2;
} else {
parent[root2] = root1;
rank[root1]++;
}
}
}
}
public class Examples {
Graph g = new Graph();
Vertex a = new Vertex("A");
Vertex b = new Vertex("B");
Vertex c = new Vertex("C");
Vertex d = new Vertex("D");
Vertex e = new Vertex("E");
Vertex f = new Vertex("F");
g.addVertex(a);
g.addVertex(b);
g.addVertex(c);
g.addVertex(d);
g.addVertex(e);
g.addVertex(f);
g.addEdge(a, b, 30);
g.addEdge(a, e, 50);
g.addEdge(b, c, 40);
g.addEdge(b, e, 35);
g.addEdge(c, d, 25);
g.addEdge(c, e, 15);
g.addEdge(d, f, 50);
g.addEdge(e, f, 50);
List mst = g.boruvka();
for (Edge e : mst) {
System.out.println(e.from.name + " - " + e.to.name + " : " + e.weight);
}
}
Borůvka’s algorithm has a time complexity of O(E log V)
. It repeatedly adds the cheapest edge from
each component until all components are connected, making it efficient for parallel processing.
Prim’s algorithm grows a single tree starting from an arbitrary vertex. It repeatedly adds the cheapest edge that connects a vertex in the tree to a vertex outside the tree until all vertices are connected.
import java.util.*;
class PrimGraph {
List vertices = new ArrayList<>();
List edges = new ArrayList<>();
void addVertex(Vertex v) {
vertices.add(v);
}
void addEdge(Vertex from, Vertex to, int weight) {
Edge edge = new Edge(from, to, weight);
from.addEdge(to, weight);
to.addEdge(from, weight); // Since the graph is undirected
edges.add(edge);
}
List prim(Vertex start) {
List mst = new ArrayList<>();
PriorityQueue pq = new PriorityQueue<>(Comparator.comparingInt(e -> e.weight));
Set inMST = new HashSet<>();
inMST.add(start);
pq.addAll(start.outEdges);
while (!pq.isEmpty() && inMST.size() < vertices.size()) {
Edge edge = pq.poll();
if (inMST.contains(edge.to)) continue;
mst.add(edge);
inMST.add(edge.to);
for (Edge e : edge.to.outEdges) {
if (!inMST.contains(e.to)) {
pq.add(e);
}
}
}
return mst;
}
}
public class PrimExamples {
PrimGraph g = new PrimGraph();
Vertex a = new Vertex("A");
Vertex b = new Vertex("B");
Vertex c = new Vertex("C");
Vertex d = new Vertex("D");
Vertex e = new Vertex("E");
Vertex f = new Vertex("F");
g.addVertex(a);
g.addVertex(b);
g.addVertex(c);
g.addVertex(d);
g.addVertex(e);
g.addVertex(f);
g.addEdge(a, b, 30);
g.addEdge(a, e, 50);
g.addEdge(b, c, 40);
g.addEdge(b, e, 35);
g.addEdge(c, d, 25);
g.addEdge(c, e, 15);
g.addEdge(d, f, 50);
g.addEdge(e, f, 50);
List mst = g.prim(a);
for (Edge e : mst) {
System.out.println(e.from.name + " - " + e.to.name + " : " + e.weight);
}
}
Prim’s algorithm has a time complexity of O(E log V)
when using a priority queue. It is efficient
for dense graphs and is widely used due to its simplicity and effectiveness.
Kruskal’s algorithm sorts all edges in the graph by their weight and adds the smallest edge to the MST, provided it does not form a cycle, until all vertices are connected.
import java.util.*;
class KruskalGraph {
List vertices = new ArrayList<>();
List edges = new ArrayList<>();
void addVertex(Vertex v) {
vertices.add(v);
}
void addEdge(Vertex from, Vertex to, int weight) {
Edge edge = new Edge(from, to, weight);
edges.add(edge);
}
List kruskal() {
List mst = new ArrayList<>();
UnionFind uf = new UnionFind(vertices.size());
Collections.sort(edges);
for (Edge e : edges) {
int set1 = uf.find(vertices.indexOf(e.from));
int set2 = uf.find(vertices.indexOf(e.to));
if (set1 != set2) {
mst.add(e);
uf.union(set1, set2);
}
}
return mst;
}
}
public class KruskalExamples {
KruskalGraph g = new KruskalGraph();
Vertex a = new Vertex("A");
Vertex b = new Vertex("B");
Vertex c = new Vertex("C");
Vertex d = new Vertex("D");
Vertex e = new Vertex("E");
Vertex f = new Vertex("F");
g.addVertex(a);
g.addVertex(b);
g.addVertex(c);
g.addVertex(d);
g.addVertex(e);
g.addVertex(f);
g.addEdge(a, b, 30);
g.addEdge(a, e, 50);
g.addEdge(b, c, 40);
g.addEdge(b, e, 35);
g.addEdge(c, d, 25);
g.addEdge(c, e, 15);
g.addEdge(d, f, 50);
g.addEdge(e, f, 50);
List mst = g.kruskal();
for (Edge e : mst) {
System.out.println(e.from.name + " - " + e.to.name + " : " + e.weight);
}
}
Kruskal’s algorithm has a time complexity of O(E log E)
due to the sorting of edges. It is
particularly efficient for sparse graphs and is known for its simplicity in implementation.
[1] Chin, F.; Houck, D. (1978), "Algorithms for updating minimal spanning trees", Journal of Computer and System Sciences, 16 (3): 333–344, doi:10.1016/0022-0000(78)90022-3.
JUnit is a widely used testing framework for Java that helps developers write and run repeatable tests. It is an essential tool for ensuring code quality and reliability. JUnit allows for the creation of automated tests that can be executed frequently to verify that the code behaves as expected.
JUnit follows a few simple steps to test code:
@Test
to mark
methods as test cases. Additional annotations like @Before
, @After
, @BeforeClass
, and @AfterClass
can
be used to set up and tear down resources needed for the tests.assertEquals
, assertTrue
, and assertFalse
to check that the code produces the
expected results.Here is an example of a simple Java class with JUnit tests:
import static org.junit.jupiter.api.Assertions.*;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
class Calculator {
public int add(int a, int b) {
return a + b;
}
public int subtract(int a, int b) {
return a - b;
}
public int multiply(int a, int b) {
return a * b;
}
public int divide(int a, int b) {
if (b == 0) {
throw new IllegalArgumentException("Division by zero");
}
return a / b;
}
}
public class CalculatorTest {
private Calculator calculator;
@BeforeEach
void setUp() {
calculator = new Calculator();
}
@Test
void testAdd() {
assertEquals(5, calculator.add(2, 3));
assertEquals(-1, calculator.add(-2, 1));
assertEquals(0, calculator.add(-2, 2));
}
@Test
void testSubtract() {
assertEquals(1, calculator.subtract(3, 2));
assertEquals(-3, calculator.subtract(-2, 1));
assertEquals(-4, calculator.subtract(-2, 2));
}
@Test
void testMultiply() {
assertEquals(6, calculator.multiply(2, 3));
assertEquals(-2, calculator.multiply(-2, 1));
assertEquals(-4, calculator.multiply(-2, 2));
}
@Test
void testDivide() {
assertEquals(2, calculator.divide(6, 3));
assertEquals(-2, calculator.divide(-4, 2));
assertEquals(0, calculator.divide(0, 1));
}
@Test
void testDivideByZero() {
Exception exception = assertThrows(IllegalArgumentException.class, () -> {
calculator.divide(1, 0);
});
assertEquals("Division by zero", exception.getMessage());
}
}
JUnit testing provides several benefits:
Let's review the different programming languages we have seen so far and their characteristics:
JavaScript is a versatile, dynamically typed, object-oriented language commonly used in web development. Here are some key features:
42 // Returns 42
false // Returns false
"world" + 10 // Returns "world10"
50 + "hello" // Returns "50hello"
"good" + true // Returns "goodtrue"
"good" + false // Returns "goodfalse"
[4, 5, 6] // Creates an Array
[4, 5, 6].length // 3
function g(x) { return x * 3; }
g(3) // 9
var obj = { "y": 7, "b": "world" }
obj.y // 7
obj.b // "world"
function DogMaker(name) { this.name = name; this.sound = "bark"; }
var buddy = new DogMaker("buddy");
buddy.name // "buddy"
buddy.sound // "bark"
var biggerBuddy = { name: "biggerBuddy", __proto__: buddy }
biggerBuddy.sound // "bark"
buddy.sound = "woof"
biggerBuddy.sound // "woof"
biggerBuddy.favoriteToy = "ball"
buddy.favoriteToy // undefined
Object-oriented programming involves creating objects that contain both data and methods. Key features of object-oriented languages include:
this
keyword (refers to the current object)In JavaScript, we can create an object-oriented system. Here's an example:
The Point class represents a point in a 2D space with x and y coordinates.
class Point {
constructor(x, y) {
this.x = x;
this.y = y;
}
// Method to calculate the distance from the origin
distance() {
return Math.sqrt(this.x * this.x + this.y * this.y);
}
}
// Creating instances of Point
const point1 = new Point(5, 10);
const point2 = new Point(15, 20);
// Accessing properties of the objects
console.log(point1.x); // 5
console.log(point2.y); // 20
// Calculating distance from the origin
console.log(point1.distance()); // 11.18
console.log(point2.distance()); // 25
The Rectangle class represents a rectangle with width and height.
class Rectangle {
constructor(width, height) {
this.width = width;
this.height = height;
}
// Method to calculate the area of the rectangle
area() {
return this.width * this.height;
}
}
// Creating instances of Rectangle
const rect1 = new Rectangle(5, 10);
const rect2 = new Rectangle(15, 20);
// Calculating the area of the rectangles
console.log(rect1.area()); // 50
console.log(rect2.area()); // 300
// Example of creating and using Point objects
const examplePoint1 = new Point(3, 4);
const examplePoint2 = new Point(6, 8);
console.log(`Point1 Distance: ${examplePoint1.distance()}`); // 5
console.log(`Point2 Distance: ${examplePoint2.distance()}`); // 10
// Example of creating and using Rectangle objects
const exampleRect1 = new Rectangle(2, 3);
const exampleRect2 = new Rectangle(4, 5);
console.log(`Rectangle1 Area: ${exampleRect1.area()}`); // 6
console.log(`Rectangle2 Area: ${exampleRect2.area()}`); // 20
When transitioning to OOD, remember the following key points: