Gamio M. Top Java Challenges. Cracking the Coding Interview...2020
Gamio M. Top Java Challenges. Cracking the Coding Interview...2020
Moises Gamio
Founder, codersite.dev
Top Java Challenges
Cracking the Coding Interview
by Moises Gamio
Copyright © 2020 Moises Gamio. All rights reserved.
It cannot reproduce any part of this book, stored in a retrieval system, or
transmitted in any form or by any means, electronic, mechanical,
photocopying, recording, or otherwise, without the publisher's express
written permission.
Once you have read and used this book, please leave a review on the site that
you purchased it. By doing so, you can help me improve the next editions of
this book. Thanks, and I hope you enjoy using the text in your job interview!
If you find any error in the text, code, or suggestions, please let me know by
emailing me at [email protected].
Linked Lists 21
2.0 Fundamentals 21
2.1 Implement a Linked List 22
2.2 Reverse a Linked List 24
v
5.0 Fundamentals 44
5.1 Bubble Sort 44
5.2 Insertion Sort 47
5.3 Quick Sort 48
5.4 Binary Search 50
Hash Table 59
7.0 Fundamentals 59
7.1 Design a Hash Table 60
7.2 Find the Most Frequent Elements in an Array 63
7.3 Nuts and Bolts 64
Trees 66
8.0 Fundamentals 66
8.1 Binary Search Tree 67
8.2 Tree Traversal 70
Graphs 74
9.0 Fundamentals 74
9.1 Depth-First Search (DFS) 75
9.2 Breadth-First Search (BFS) 81
Coding Challenges 86
10.0 Fundamentals 86
10.1 Optimize Online Purchases 86
10.2 Tic Tac Toe 94
Big O Notation 114
Time and Space Complexity 114
Order of Growth of Common Algorithms 114
How to Find the Time Complexity of an Algorithm 117
Why Big O Notation ignores Constants? 118
I am a software engineer who faced real interviews as a candidate for startups and big
companies. Throughout the years, I have sourced factual questions that have been tried,
tested, and commented on step by step and are now part of this book! I hope you find them
practical and valuable in your career search.
Yes, your CV matters. The job market is tough, and big firms and tech startups receive many
applicants every day, so the need to filter the best one becomes a daunting process. The
interview by phone and the code challenge to be solved at home will lead you to the last and
most decisive stage of the recruiting process, where the interviewer evaluates your problem
solving skills and knowledge of efficient data structures and algorithms.
Usually, the interviewer poses a complex problem to solve in a limited time. You need to
provide rationales about design choices made in your proposed solution in terms of resource
and time efficiency.
Therefore, knowing the most appropriate data structures and algorithms to solve common
problems frequently used in the recruiting process can be decisive. Whether your knowledge
of efficient data structures and algorithms is scarce or buried in your memory since you
learned about them for the first time, this book can help you. It includes the most common
questions, and their respective solutions, that you can find in a real interview. Recall, the
more prepared you are, the more points you accumulate objectively concerning other
candidates.
This book includes typical questions based on real interviews that you must know and
practice. All solved and explained in detail.
Typical questions include String manipulation, arrays, variables swapping, linked list,
refactoring, recursion, sorting, searching, stacks, queues, trees, graphs, optimization, and
games. In these questions, you can see how to choose the proper data structure for their
optimal implementation.
Also included are questions where you can design an algorithm based on the test
development-driven approach, which is exceptionally essential for today's companies.
The more prepared and confident you are, the better the chances of negotiating your next
salary.
Moises Gamio
Software Engineer, Senior Java Developer
• Design a sequence of steps algorithm should do, defining the data structures and data
types.
• Is the algorithm fast enough and fit in memory? Here you must refactor the algorithm,
possibly changing the data structures and the sequence of steps. Big O Notation is your
helper in this task.
Clean Code
Clean code can be read and enhanced by a developer other than its original author.
If you want to be a better programmer, you must follow these recommendations.
Clean code has Intention-Revealing names
Names reveal intent. Someone who reads your code must understand the purpose of your
Imagine that we don't have the //ingredients comment in e2fResult variable. Then,
further in any part of our code, when we try to process this variable, we have the following
sentence:
e2f = e2fResult[i];
And we don't know what does e2f means! Well, someone suggests asking the person
responsible for this code. But that guy is not at the office. Well, send it an email, and he is on
holiday!
But if, instead, we adopt names that reveal intent from the beginning, we could avoid these
catastrophic scenarios.
ingredient = ingredients[i];
Throughout this book, you will see how important these practices are to write efficient and
readable algorithms.
1.0 Fundamentals
Arrays
An array is an object that stores a fixed number of elements of the same data type. It uses a
contiguous memory location to store the elements. Its numerical index accesses each
element.
If you ask the Array, give me the element at index 4, the computer locates that element's cell
in a single step.
That happens because the computer finds the memory address where the Array begins -
1000 in the figure above - and adds 4, so the element will be located at memory address
1004.
Arrays are a linear data structure because the elements are arranged sequentially and accessed
randomly.
One of the limitations of the Array is that adding or deleting data takes a lot of time.
In a one-dimensional array, you retrieve a single value when accessing each index.
//declares an array of integers
int[] arrayOfInts;
arrayOfInts[0] = 100;
arrayOfInts[3] = 200;
In a multidimensional array, you retrieve an array when accessing each index. A two
dimensional array is known as a matrix.
Arrays allow us to have one variable store multiple values.
For example, we define an array of products 2-D, whose elements per row represents:
productId, price, and grams.
double[][] arrayOfProducts = new double[4][3];
arrayOfProducts[2][0] = 2;
arrayOfProducts[2][1] = 1.29;
arrayOfProducts[2][2] = 240;
10! U1 R1
arrayOfProductslO] —- -------
arrayOfProducts[l] —j------- ' _ |
Once an array is created, it cannot change its size. For dynamic arrays, we can use a List.
An ArrayList is a resizable array implementation of the List interface.
List<String> listOfCities = new ArrayList<>();
listOfCities.add("New York");
listOfCities.add("Berlin");
listOfCities.add("Paris");
listOfCities.add("Berlin");
Strings
A sequence of character data is called a string and is implemented by the String class.
The java compiler creates and places a new string instance in the string constant pool. This
avoids to create two instances with the same value.
2. By new keyword
String s = new String("hello world");
The java compiler creates a new string object in the heap memory. The variable s refers to
the object.
myString + "chars."
The previous concatenation creates a third String object in memory. That is because Strings
are immutable, they cannot change once it is created. Use a mutable StringBuilder class if
you want to manipulate the contents of the string on the fly.
StringBuilder stringOnTheFly = new StringBuilder();
stringOnTheFly.append(myString).append("chars.");
String, StringBuffer and StringBuilder implements the CharSequence interface that is used to
represent a sequence of characters.
Solution
We choose an array — holds values of a single type - as our data structure because the
algorithm receives a small amount of data, which is predictable and is read it randomly (its
numerical index accesses each element).
Firstly, convert the text to be reversed to a character array. Then, calculate the length of the
string.
Secondly, swap the position of array elements using a loop. Don't use additional memory,
which means avoiding unnecessary objects or variables (space complexity). Swapping does it
in place by transposing values using a temporary variable. Then, swap the first element with
the last, the second element with the penultimate, and so on. Moreover, we only need to
iterate until half of the Array.
Finally, it returns the new character array as a String. Listing 1.1 shows the algorithm.
Example:
When idx = 0:
chars = {a, b, c, 2, 1, 3, 2}
chars[idx] = a
chars[arrayLength-1-idx] = 2
When idx = 1:
chars = {2, b, c, 2, 1, 3, a}
chars[idx] = b
chars[arrayLength-1-idx] = 3
When idx = 2:
chars = {2, 3, c, 2, 1, b, a}
chars[idx] = c
chars[arrayLength-1-idx] = 1
When idx = 3:
chars = {2, 3, 1, 2, c, b, a}
idx is not less than arrayLength/2
end
Tests
@Test
public void reverseText_useCases() {
assertEquals("abc2132", StringUtils.reverse("2312cba"));
assertEquals("ba", StringUtils.reverse("ab"));
assertEquals("c a1", StringUtils.reverse("1a c"));
}
During the interview, it is common to receive additional questions about your code. For
instance, what happens if we pass a null argument.
First, we need to define our test case
if (text == null)
throw new RuntimeException("text is not initialized");
Moreover, the interviewer wants our algorithm to reverse only those characters that occupy
an odd position inside the Array.
Again, we define our assumption using a test case.
@Test
public void reverseOdssText() {
assertEquals("ub32tca192", StringUtils.reverseOdds("2b12cta39u"));
}
The % operator is used to detect these locations. Under the loop for sentence, we need to
add the following conditional sentence:
if ((idx+1) % 2 != 0) {
Solution
We assume the charset is ASCII, but you should always ask interviewers if you are unsure.
Originally based on the English alphabet, ASCII encodes 128 specified characters into
seven-bit integers. Ninety-five of the encoded characters are printable: these include the
digits 0 to 9, lowercase letters a to z uppercase letters A to Z, and punctuation symbols.
Firstly, create a Boolean array to store the occurrence of every character.
Secondly, iterate the string of characters, use the java charAt method to return the numerical
representation for every character. Check this value in the Boolean Array. If it exists, then
the string does not have unique values. Otherwise, store this value in the Boolean Array as
its first occurrence.
Finally, if all characters had only one occurrence in the Boolean Array, the string has all
unique characters. Listing 1.2 shows the algorithm.
Tests
public class AreUniqueCharsTest {
@Test
public void is_not_UniqueChars() {
assertFalse(StringUtils.areUniqueChars("29s2"));
assertFalse(StringUtils.areUniqueChars("1903aio9p"));
}
@Test
public void is_UniqueChars() {
assertTrue(StringUtils.areUniqueChars("29s13"));
assertTrue(StringUtils.areUniqueChars("2813450769"));
}
}
Solution
We can loop through each character and check it against another one on the opposite side. If
one of these checks fails, then the text is not Palindrome. Listing 1.3 shows the algorithm.
Tests
public class IsPalindromeTest {
@Test
public void is_not_palindrome() {
assertFalse(StringUtils.isPalindrome("2f1"));
assertFalse(StringUtils.isPalindrome("-101"));
}
@Test
public void is_palindrome() {
assertTrue(StringUtils.isPalindrome("2f1f2"));
assertTrue(StringUtils. isPalindrome( ''-101-''));
assertTrue(StringUtils.isPalindrome("9"));
assertTrue(StringUtils.isPalindrome("99"));
assertTrue(StringUtils.isPalindrome("madam"));
}
}
Solution
Firstly, we must split the version number into its components. Since the input is in the form
of strings, there is a need to divide them, given the dot delimiter that separates them to
convert them to int arrays.
Secondly, we must iterate the arrays while it has not achieved at least one of the lengths of
version 1 :10.1.2
Solution
Maybe your first idea is to iterate through the list. You compare every item with the other
ones. If a duplicate is detected, then it is removed. Or maybe you traverse the list and store
the first occurrence of each item in a new list and ignore all the next occurrences of that
item. Those solutions are called brute force algorithms because they use straightforward
methods of solving a problem, but sometimes what the interviewer expects is to reuse the
libraries included in the JDK, which improves efficiency.
We use the Set interface from the Collections library. By definition, Set does not allow
duplicates. Then, use the sort method to order its items.
A Comparable interface sorts lists of custom objects in natural ordering. List of Objects that
already implement Comparable (e.g., String) can be sorted automatically by Collections.sort.
LinkedHashSet (Collection list) is used to initialize a HashSet with the list items, removing the
duplicates. Listing 1.5 shows this implementation.
Tests
@Test
public void givenIntegersThenRemovedDuplicates() {
List<Integer> input = new ArrayList<>();
input.add(Integer.valueOf(3));
input.add(Integer.valueOf(3));
input.add(Integer.valueOf(4));
input.add(Integer.valueOf(1));
input.add(Integer.valueOf(7));
input.add(Integer.valueOf(1));
input.add(Integer.valueOf(2));
input.add(Integer.valueOf(1));
List<Integer> result = ListUtils.removeDuplicatesAndOrder(input);
assertEquals("[1, 2, 3, 4, 7]", result.toString());
}
Solution
We build two for loops, an outer one deals with one layer of the matrix per iteration, and an
inner one deals with the rotation of the elements of the layers. We rotate the elements in n/2
cycles. We swap the elements with the corresponding cell in the matrix in every square cycle
by using a temporary variable. Listing 1.6 shows the algorithm.
21 16 11 6’ 1 31 11 6 1 21 16 11 6 1
22 7 8 9 2 22 17 12 7 2 22 17 12 7 2
23 12 13 14 3 23 18 13 8 3 23 18 13 8 3
24 17 18 19 4 24 19 14 9 4 24 19 14 9 4
25 20 15 5o 5 25 20 15 10 5 25 20 15 10 5
/* layers */
for (int i = 0; i < n / 2; i++) {
/* elements */
for (int j = i; j < n - i - 1; j++){
//Swap elements in clockwise direction
//temp = top-left
int temp =matrix[i][j];
//top-left <- bottom-left
matrix[i][j] = matrix[n - 1 - j][i];
//bottom-left <- bottom-right
matrix[n - 1 - j][i] = matrix[n - 1 - i][n - 1 - j];
//bottom-right <- top-right
matrix[n - 1 - i][n - 1 - j] = matrix[j][n - 1 - i];
//top-right <- top-left
matrix[j][n - 1 - i] = temp;
}
}
}
}
Tests
@Test
public void rotate5x5() {
int[][] matrix = new int[][]{
{1, 2, 3, 4, 5},
{6, 7, 8, 9, 10},
{11, 12, 13, 14, 15},
{16, 17, 18, 19, 20},
{21, 22, 23, 24, 25}};
MatrixUtils.rotate(matrix);
assertArrayEquals(new int[]{21, 16, 11, 6, 1}, matrix[0]);
assertArrayEquals(new int[]{22, 17, 12, 7, 2}, matrix[1]);
}
• A compartment is represented as a pair of pipes that may or may not have items
between them (‘|’ = ascii decimal 124).
Example
S = ‘|**|*|*’
startIndices = [1,1]
endIndices = [5,6]
The string has a total of 2 closed compartments, one with 2 items and one with 1 item. For
the first pair of indices, (1,5), the substring is ‘|**|*’. There are 2 items in a compartment.
For the second pair of indices, (1,6), the substring is ‘|**|*|’ and there are 2 + 1 = 3 items
Solution
To determine the number of items in closed compartments, we need to build the substrings
from the two indices startIndices and endIndices.
We evaluate every character from the substring. All strings start with a ‘|’ character. We
define a numOfAsterisk variable to count items inside a compartment.
We define a wasFirstPipeFound variable to initialize our numOfAsterisk variable the first time a
‘|’ character is found, and we accumulate all items since subsequent ‘|’ characters.
import java.util.ArrayList;
import java.util.List;
public class Container {
public static List<Integer> numberOfItems(String s,
List<Integer> startIndices, List<Integer> endIndices) {
if (startIndices.size()<1 || startIndices.size()>100000)
throw new RuntimeException("wrong size in startIndices");
if (endIndices.size()<1 || endIndices.size()>100000)
throw new RuntimeException("wrong size in endIndices");
if (start<1 || start>100000)
throw new RuntimeException("wrong value at startIndices");
if (end<1 || end>100000)
throw new RuntimeException("wrong value at endIndices");
int numOfAsterisk = 0;
boolean wasFirstPipeFound = false;
int numOfAsteriskAccumulated = 0;
for (char c : s.substring(start-1, end).toCharArray()) {
if (c == '|') {
if (wasFirstPipeFound == true) {
numOfAsteriskAccumulated +=numOfAsterisk;
numOfAsterisk = 0;
} else {
wasFirstPipeFound = true;
numOfAsterisk = 0;
}
} else if (c == '*') {
numOfAsterisk +=1;
} else {
throw new RuntimeException("wrong character");
}
}
numberOfItemsInClosedCompartments.add(numOfAsteriskAccumulated);
}
return numberOfItemsInClosedCompartments;
}
}
Test
@Test
public void test_numberOfItems() {
assertEquals(new ArrayList<Integer>(Arrays.asList(2,3)),
Container.numberOfItems("|**|*|*",
new ArrayList<Integer>(Arrays.asList(1,1)),
new ArrayList<Integer>(Arrays.asList(5,6))
));
}
The customer must buy shoes for 4 dollars since there is only one option. This leaves 6
dollars to spend on the other 3 items. Combinations of prices paid for jeans, skirts, and tops
respectively that add up to 6 dollars or less are [2,2,2], [2,2,1], [3,2,1], [2,3,1]. There are 4
ways the customer can purchase all 4 items.
Function description
Create a function that returns an integer which represents the number of options present to
buy the four items.
The function must have 5 parameters:
int[]priceOfJeans: An integer array, which contains the prices of the pairs of jeans available.
int[] priceOfShoes: An integer array, which contains the prices of the pairs of shoes available.
int[] priceOfSkirts: An integer array, which contains the prices of the skirts available.
int[] priceOfTops: An integer array, which contains the prices of the tops available.
int dollars: the total number of dollars available to shop with.
Constraints
Solution
To find how many ways the customer can purchase all four items, we can iterate the four
arrays, combine all its products, and validate that customer cannot spend more money than
the budgeted amount. The for-each construct helps our code be elegant and readable and
there is no use of the index.
This algorithm works fine when array size and values are small. But based on the constraints,
imagine you are processing a priceOfShoe value of 100000 at the location priceOfShoes[101], and
at that moment, the sum of priceOfJean + priceOfShoe is greater than 10 dollars. Therefore, it
does not make sense to continue processing the following possible 1000000000 items of
pricesOfSkits[] and other 1000000000 items ofpriceOfTops[].
To skip this particular iteration, we use the “continue” statement and proceed with the next
iteration in the loop. The next priceOfShoe at the location priceOfShoes[102], for example.
Listing 1.8 shows an optimized solution.
validate(priceOfJeans, "jeans");
validate(priceOfShoes, "shoes");
validate(priceOfSkirts, "skirts");
validate(priceOfTops, "tops");
int numberOfOptions = 0;
for (int priceOfJean : priceOfJeans) {
if (priceOfJean >= dollars)
continue;
for (int priceOfShoe : priceOfShoes) {
if (priceOfJean + priceOfShoe >= dollars)
continue;
for (int priceOfSkirt : priceOfSkirts) {
if (priceOfJean + priceOfShoe + priceOfSkirt >= dollars)
continue;
for (int priceOfTop : priceOfTops) {
if (priceOfJean + priceOfShoe + priceOfSkirt + priceOfTop <= dollars)
numberOfOptions +=1;
}
Tests
@Test
public void test_shoppingOptions() {
int[] priceOfJeans = {2, 3};
int[] priceOfShoes = {4};
int[] priceOfSkirts = {2, 3};
int[] priceOfTops = {1, 2};
assertEquals(4, ShoppingOptions.getNumberOfOptions(priceOfJeans, priceOfShoes,
priceOfSkirts, priceOfTops, 10));
}
@Test
public void test_shoppingOptionsBigPrices() {
int[] priceOfJeans = {2, 10000, 3};
int[] priceOfShoes = {2000002, 4};
int[] priceOfSkirts = {2, 3000000, 3};
int[] priceOfTops = {1, 2};
assertEquals(4, ShoppingOptions.getNumberOfOptions(priceOfJeans, priceOfShoes,
priceOfSkirts, priceOfTops, 10));
}
A function or method should be small, making it easier to read and understand. We have
moved all validations to a private method.
What happens when our possible choosing prices are located at the end of the arrays?. One
possible solution could be to sort the arrays before iterating and find the right combinations
of prices.
2.0 Fundamentals
A linked list is a linear data structure that represents a sequence of nodes. Unlike arrays,
linked lists store items at a not contiguous location in the computer's memory. It connects
items using pointers.
Connected data that dispersed is throughout memory are known as nodes. In a linked list, a
node embeds data items. Because there are many similar nodes in a list, using a separate class
called Node makes sense, distinct from the linked list itself.
Each Node object contains a reference (usually called next or link) to the next Node in the
list. This reference is a pointer to the next Node's memory address. A Head is a special node
that is used to denote the beginning of a linked list. A linked list representation is shown in
the following figure.
Head
Node
A 1442 B 1841
data link
Each Node consists of two memory cells. The first cell holds the actual data, while the
second cell serves as a link indicating where the next Node begins in memory. The final
Node's link contains null since the linked list ends there.
In the figure above, we say that "B" follows "A," not that "B" is in the second position.
A linked list's data can be spread throughout the computer's memory, which is a potential
advantage over the Array. An array, by contrast, needs to find an entire block of contiguous
cells to store its data, which can get increasingly difficult as the array size grows. For this
reason, Linked Lists utilize memory more effectively.
When each Node only points to the next Node, we have a singly linked list. We have a
doubly-linked list when each Node points to the next Node and the previous Node.
Unlike an array, a linked list doesn't provide constant time to access the nth element. We
have to iterate n-1 elements to obtain the nth element. But we can insert, remove, and update
nodes in constant time from the beginning of a linked list.
Solution
Create a linked list class
We can represent a LinkedList as a class with its Node as a separate class. The LinkedList
class will have a reference to the Node type.
Adding a node
We can add a new node in three ways:
• If the head node is null (Empty Linked List), make the new node the head.
• If the head node is not null, find the last node. Make the last node => next as the new
node.
@Override
public String toString() {
StringJoiner stringJoiner = new StringJoiner(" -> ", "[", "]");
Node currentNode = head;
while (currentNode != null) {
stringJoiner.add(currentNode.data.toString());
currentNode = currentNode.next;
}
return stringJoiner.toString();
}
Tests
@Test
public void addNodes() {
LinkedList<String> linkedList = new LinkedList<>();
linkedList.add("s1");
linkedList.add("s2");
Solution
To reverse a linked list, we implement an iterative method:
1. Initialize three-pointers: prev as NULL, current as head, and next as NULL.
2. Iterate through the linked list. Inside the loop, do the following:
// Before changing next of current, store next node
next = current->next
current = next
Let's see what happens at the reverse method in the first iteration of a LinkedList:
current = next the next iteration will process the new current = s2
Tests
@Test
public void reverseLinkedList() {
LinkedList<String> linkedList = new LinkedList<>();
linkedList.add("s1");
linkedList.add("s2");
linkedList.add("s3");
linkedList.add("s4");
linkedList.reverse();
assertEquals("[s4 -> s3 -> s2 -> s1]",
linkedList.toString());
}
@Test
public void reverseIntegersLinkedList() {
LinkedList<Integer> linkedList = new LinkedList<>();
linkedList.add(new Integer(l));
linkedList.add(new Integer(2));
linkedList.add(new Integer(3));
linkedList.add(new Integer(5));
linkedList.reverse();
assertEquals("[5 -> 3 -> 2 -> 1]",
linkedList.toString());
}
3.0 Fundamentals
Puzzles are usually asked to see how you go about solving a tricky problem. You should
know basic math concepts to excel in coding interviews.
Useful methods included in the Math Class:
abs(x): Returns the absolute value of x.
Example: abs(-1) = 1;
round(x): Returns the value of x rounded to its nearest integer.
Example: round(4.4) = 4
ceil(x): Returns the value of x rounded up to its nearest integer.
Example: ceil(4,4) = 5.0
floor(x): Returns the value of x rounded down to its nearest integer.
Example: floor(4.4) = 4.0
max(x): Returns the number with the highest value.
Example: max(6,7) = 7
pow(x,y): Returns the value of x to the power of y.
Example: pow(2,5) = 32.0
random(): Returns a random number between 0 and 1.
Example: random() => 0.45544344999209374
Other usefulformulas:
Sum of first n positive integers:
n(n+1)/2
Distance between two points P(x1,y1) and Q(x2,y2):
d(P, Q) = V (x2 - x1)2 + (y2 — y1)2
Solution
It looks like an easy program, but believe me, most experienced programmers during the
interview forget to use the operator "%," which returns the division remainder. If it is 0, the
number is even; otherwise, it is odd. Listing 3.1 shows the algorithm.
return sum;
}
}
Our code runs properly, but as Software Engineers, we need to care about code efficiency.
The following algorithm runs faster than the previous one because it only loops the N/2
times.
public class NumberUtils {
public static int sumOfEvenNumbers(int N) {
int sum = 0;
int number = 2;
while (number <= N) {
sum += number;
//increase number by 2, which in definition
//is the next even number
number+=2;
}
return sum;
}
}
Tests
@Test
public void sumOfEvenNumbers_test() {
assertEquals(42, NumberUtils.sumOfEvenNumbers(12));
assertEquals(110, NumberUtils.sumOfEvenNumbers(21));
}
Solution
We use the % operator to verify if a year is divisible by 4, 100, or 400. Listing 3.2 shows the
algorithm.
This algorithm had a real application in the Y2K problem, where COBOL programs in a
Bank used to store four-digit years with only the final two digits, so you could not
distinguish 2000 from 1900. In addition to calculating a leap year:
For years represented from 50 until 99, add 19 at begin, resulting in 1950 until 1999.
For years represented from 00 until 49, add 20 at begin, resulting in 2000 until 2049.
The Bank assumed that before 2049 all systems would be migrated to modern programming
languages (digital transformation), but that is another story.
Tests
@Test
public void isLeapYear() {
assertTrue(DateUtils.isLeapYear(400));
assertTrue(DateUtils.isLeapYear(2000));
assertTrue(DateUtils.isLeapYear(2020));
}
@Test
public void is_notLeapYear() {
assertFalse(DateUtils.isLeapYear(401));
assertFalse(DateUtils.isLeapYear(2018));
}
Solution
Maybe the first idea that comes to our minds could be to iterate from the given number and
decrease one by one, and in every iteration to check if every new number contains one digit
less than the previous number. If the answer is True, then the previous one is the smallest
number with the same number of digits as the original number. Listing 3.3 shows the first
algorithm for positive numbers.
The smallest number is a power of 10, where the exponent is: number of digits — 1
Second solution:
If we want to include negative numbers, we must consider the smallest number with the
same number of digits and the same sign. Here the solution:
public class NumberUtils {
public static int smallest(int N) {
int numberOfDigits = (int) String.valueOf(Math.abs(N)).length();
if (N >= 0) {
if (numberOfDigits == 1) {
return 0;
} else {
return (int) Math.pow(10, numberOfDigits - 1);
}
} else
return 1 - (int) Math.pow(10, numberOfDigits);
}
}
The main idea in Analysis of Algorithms is always to improve the algorithm performance by
reducing the number of steps and comparisons. The simpler and more intuitive an algorithm
is, the more useful and efficient it will be.
Tests
@Test
public void test_right_smallest_values() {
assertTrue(NumberUtils.smallest(4751) == 1000);
assertTrue(NumberUtils.smallest(189) == 100);
assertTrue(NumberUtils.smallest(37) == 10);
assertTrue(NumberUtils.smallest(1) == 0);
assertTrue(NumberUtils.smallest(0) == 0);
assertTrue(NumberUtils.smallest(-1) == -9);
assertTrue(NumberUtils.smallest(-38) == -99);
}
@Test
public void test_wrong_smallest_values() {
assertFalse(NumberUtils.smallest(8) == 1);
assertFalse(NumberUtils.smallest(2891) == 2000);
}
Solution
It looks like a simple algorithm but is "hard" for some programmers because they try to
follow the following reasoning:
if (theNumber is divisible by 3) then
print "Fizz"
else if (theNumber is divisible by 5) then
print "Buzz"
else /* theNumber is not divisible by 3 or 5 */
print theNumber
end if
But where do we print "Fizz-Buzz" in this algorithm? The interviewer expects that you think
for yourself and made good use of conditional without duplication. Realizing that a number
divisible by 3 and 5 is also divisible by 3*5 is the key to a FizzBuzz solution. Listing 3.4
shows the algorithm.
Exponentiation involves two numbers: the base b and the exponent or power n.
Exponentiation corresponds to repeated multiplication of the base n times. For instance, in
the following expression: 35 = 243, we say that 243 is the 5th power of 3. Therefore, 243 is
the correct power of 3.
Solution
We do the inverse operation to verify if a number is a valid power nth of another number
(base).
Divide the given number by the base and evaluate if it provides a 0 remainder, then we iterate
this operation until we find a value of 1. Otherwise, if the rest is not 0, then the given
number is not a valid power nth of the base. Listing 3.5 shows a function, which returns true
when a given number is the right power of a base.
@Override
public Boolean apply(Integer number, Integer base) {
return isNumberAValidPowerOfBase(number, base);
}
Tests
@Test
public void testOfWrongReturnValues() {
assertFalse(isNumberAValidPowerOfBase.apply(6, 2));
assertFalse(isNumberAValidPowerOfBase.apply(16, 5));
assertFalse(isNumberAValidPowerOfBase.apply(14, 7));
}
@Test
public void testOfValidReturnValues() {
assertT rue(isNumberAValidPowerOfBase.apply(243, 3));
assertT rue(isNumberAValidPowerOfBase.apply(16, 4));
assertT rue(isNumberAValidPowerOfBase.apply(125, 5));
}
Solution
A simple solution is to iterate decreasingly through all numbers from the half of the given
if (number == 2)
return true;
Tests
@Test
public void notPrimeNumbers() {
assertFalse(MathUtils.isPrimeNumber(-1));
assertFalse(MathUtils.isPrimeNumber(625));
assertFalse(MathUtils.isPrimeNumber(4));
assertFalse(MathUtils.isPrimeNumber(100));
}
@Test
public void primeNumbers() {
assertTrue(MathUtils.isPrimeNumber(2));
assertTrue(MathUtils.isPrimeNumber(3));
assertTrue(MathUtils.isPrimeNumber(5));
assertTrue(MathUtils.isPrimeNumber(7));
assertTrue(MathUtils.isPrimeNumber(73));
}
Solution
Each point has coordinates (x,y), so we can calculate the distance with the hypotenuse.
Tests
@Test
public void given_twoPoints_return_distance() {
Point point1 = new Point(2, 3);
Point point2 = new Point(5, 7);
assertEquals(5, point1.distance(point2), 0);
}
Solution
An immutable class is a class whose instances cannot be modified. Its information is fixed
for the lifetime of the object without changes.
To make a class immutable, we follow these rules:
• Don't include a mutators method that could modify the object's state.
• Ensure exclusive access to any mutable components. Don't make references to those
objects. Make defensive copies.
Immutable objects are thread-safe; they require no synchronization. We use a BigDecimal data
type for our Class because it provides operations on numbers for arithmetic, rounding and
can handle large floating-point numbers with great precision. Listing 3.8 shows an
immutable Class.
import java.math.BigDecimal;
public final class Money {
private static final String DOLAR = "USD";
private static final String EURO = "EUR";
private static int ROUNDING_MODE = BigDecimal.ROUND_HALF_EVEN;
private static int DECIMALS = 2;
private BigDecimal amount;
private String currency;
public Money() {
}
//Currency converter
public Money multiply(BigDecimal factor) {
return Money.valueOf(
rounded(this.amount.multiply(factor)),
this.currency.equals(DOLAR) ? EURO : DOLAR);
}
//round to 2 decimals
private BigDecimal rounded(BigDecimal amount) {
return amount.setScale(DECIMALS, ROUNDING_MODE);
}
String class produces immutable objects, so we can trust any client who passes it in the
arguments. But this is not the case for BigDecimal, which can be extended and manipulated
for some untrusted clients, e.g., to expand its toString() method.
java.math
Class BigDecimal
java.lang. Object
java.lang.Number
java.math. BigDecimal
Serializable, Comparable<BigDecimal>
To protect our Class from untrusted clients, we can create copies of these arguments. The
following code shows the multiply method modified.
Tests:
@Test
public void convert_EURO_to_DOLLAR() {
final Money moneyInEuros = Money.valueOf(new BigDecimal("67.89"), "EUR");
final Money moneyInDollar =
moneyInEuros.multiply(new BigDecimal("1.454706142288997"));
assertEquals(new BigDecimal("98.76"), moneyInDollar.getAmount());
}
@Test
public void convert_DOLLAR_to_EURO() {
final Money moneyInDollar = Money.valueOf(new BigDecimal("98.76"), "USD");
final Money moneyInEuros =
moneyInDollar.multiplysecure(new BigDecimal("0.6874240583232078"));
assertEquals(new BigDecimal("67.89"), moneyInEuros.getAmount());
}
Solution
We need to find the total number of products in this range [X.. Y].
Example:
Given X=6 and Y=20
The function should return 3
These integers are 6=2*3, 12=3*4, and 20=4*5.
Tests
@Test
public void test_right_products() {
assertTrue(NumberUtils.validProducts(6, 20) == 3);
assertTrue(NumberUtils.validProducts(1000, 1130) == 2);
}
@Test
public void test_wrong_products() {
assertFalse(NumberUtils.validProducts(21, 29) == 1);
}
Solution
We order the parts, then calculates the time between the two consecutive parts of small sizes
until we arrive at the last part. Listing 3.10 shows the algorithm.
int accumulatedTime = 0;
for (int idx = 0; idx < arrayOfSizes.length - 1; idx++) {
accumulatedTime += (arrayOfSizes[idx] + arrayOfSizes[idx + 1]);
//once assembled, we carry the current time to the next element
//so in the next iteration, the first of the next two parts
//it will already include the total time required
//to assemble the two previous parts
arrayOfSizes[idx + 1] = arrayOfSizes[idx] + arrayOfSizes[idx + 1];
}
return accumulatedTime;
}
}
Tests
@Test
public void test_right_values() {
assertT rue(AssembleParts.minimumTime(4,
new ArrayList<Integer>(Arrays.asList(8, 4, 6, 12))) == 58);
assertT rue(AssembleParts.minimumTime(5,
new ArrayList<Integer>(Arrays.asList(3, 7, 2, 10, 5))) == 59);
}
4.0 Fundamentals
A method or function that calls itself is called recursion. A recursive function is defined in
terms of itself. We always include a base case to finish the recursive calls.
Each time a function calls itself, its arguments are stored on the Stack before the new
arguments take effect. Each call creates new local variables. Thus, each call has its copy of
arguments and local variables.
That is one reason sometimes we don’t need to use recursion in the Production
environment; for example, when we pass a big integer, they can overflow the Stack and crash
any application. But in other cases, we can efficiently solve problems.
Solution
• We define the base case: returns 1 when N <= 1 to stop the recursion
The following figure shows in the first half how a succession of recursive calls executes until
factorial(1) - the base case - returns 1, which stops the recursion. The second half shows the
values calculated and returned from each recursive call to its caller.
The following Listing shows the iterative version (and more efficient).
public class Factoriallterative {
public static int factorial(int N) {
int result = 1;
for (int i = 2; i <= N; i++)
result *= i;
return result;
}
}
Tests
@Test
public void test_right_results() {
assertTrue(FactorialRecursive.factorial(1) == 1);
assertTrue(FactorialRecursive.factorial(0) == 1);
assertTrue(FactorialRecursive.factorial(3) == 6);
assertTrue(FactorialRecursive.factorial(6) == 720);
}
@Test
public void test_wrong_results() {
assertFalse(FactorialRecursive.factorial(3) == 5);
assertFalse(FactorialRecursive.factorial(4) == 10);
}
Solution
We define two base cases:
fibonacci(0) = 0 and fibonacci(1) = 1
We calculate the ith Fibonacci number recursively:
fibonacci(N) = fibonacci(N-1) + fibonacci(N-2)
if (N == 1)
return 1;
Tests
public class FibonacciTest {
@Test
public void test_fibonacciSeries() {
assertTrue(MathUtils.fibonacci(3) == 2);
assertTrue(MathUtils.fibonacci(7) == 13);
assertTrue(MathUtils.fibonacci(10) == 55);
}
}
5.0 Fundamentals
Searching refers to finding an item in a collection that meets some specified criterion.
Sorting refers to rearranging all the items in a collection into increasing or decreasing order.
Sorting algorithms are essential to improve the efficiency of other algorithms that require
input data previously sorted.
Applications of sorting:
• Element uniqueness - An algorithm would sort the numbers in a list and check adjacent
pairs to detect duplicate items.
• Frequency distribution - An algorithm would sort a list of items and count from left to
right.
• Merge lists - An algorithm would compare elements from both sorted lists and add the
smaller ones to every iteration’s new merged list.
Solution
The algorithm uses a Boolean variable to track whether it has found a swap in its most
recent pass through the Array; as long as the variable is true, the algorithm loops through the
Array, looking for adjacent pairs of elements that are out of order and swap them. The time
complexity, in the worst case it requires O(n2) comparisons. Listing 5.1 shows the algorithm.
boolean numbersSwapped;
do {
numbersSwapped = false;
for (int i = 0; i < numbers.length - 1; i++) {
if (numbers[i] > numbers[i + 1]) {
int aux = numbers[i + 1];
numbers[i + 1] = numbers[i];
numbers[i] = aux;
numbersSwapped = true;
}
}
} while (numbersSwapped);
return numbers;
}
}
Example:
First pass-through:
{6, 4, 9, 5} -> {4, 6, 9, 5} swap because of 6 > 4
{4, 6, 9, 5} -> {4, 6, 9, 5}
{4, 6, 9, 5} -> {4, 6, 5, 9} swap because of 9 > 5
NumbersSwapped=true
Second pass-through:
{4, 6, 5, 9} -> {4, 6, 5, 9}
{4, 6, 5, 9} -> {4, 5, 6, 9} swap because of 6 >5
{4, 5, 6, 9} -> {4, 5, 6, 9}
NumbersSwapped=true
Third pass-through:
{4, 5, 6, 9} -> {4, 5, 6, 9}
{4, 5, 6, 9} -> {4, 5, 6, 9}
{4, 5, 6, 9} -> {4, 5, 6, 9}
NumbersSwapped=false
In the first pass through, we have to make three comparisons -> {6, 5, 4, 9}
In our second passthrough, we have to make only two comparisons because we didn't need
to compare the final two numbers -> {5, 4, 6, 9}
In our third pass through, we made just one comparison -> {4, 5, 6, 9}
In summarize:
N # steps N2
4 12 16
5 20 25
10 90 100
Therefore, in Big O Notation, we could say that the Bubble Sort algorithm has O(N2)
efficiency.
Tests
@Test
public void sortingArrays() {
final int[] numbers = {6, 4, 9, 5};
final int[] expected = {4, 5, 6, 9};
int[] numbersSorted = sorting.bubbleSort(numbers);
assertArrayEquals(expected, numbersSorted);
}
@Test
public void sortManyElementArray() {
final int[] array = {7, 9, 1, 4, 9, 12, 4, 13, 9};
final int[] expected = {1, 4, 4, 7, 9, 9, 9, 12, 13};
sorting.bubbleSort(array);
assertArrayEquals(expected, array);
}
Solution
• Iterates over all the elements and start at index i=1.
• Compare the current element (key) with all its preceding elements.
• If the key element is smaller than its predecessors, swap them. Move elements that are
greater than the key, one position ahead of their current position.
This algorithm takes a quadratic running time O(n2). Listing 5.2 shows the algorithm.
Example:
numbers = {13, 12, 14, 6, 7}
When i = 1. Since 13 is greater than 12, move 13 and insert 12 before 13
12, 13, 14, 6, 7
When i = 2., 14 will remain at its position as all previous elements are smaller than 14
12, 13, 14, 6, 7
When i = 3., 6 will move to the beginning, and all other elements from 12 to 14 will move
@Test
public void sortingArray() {
final int[] numbers = {7, 9, 1, 4, 9, 12, 4, 13, -2, 9};
final int[] expected = {-2, 1, 4, 4, 7, 9, 9, 9, 12, 13};
sorting.insertSort(numbers);
assertArrayEquals(expected, numbers);
}
Solution
Given an array, we execute the following in-place steps:
1. We pick from the Array, the last element as our pivot.
2. We execute a partition operation, where we put all smaller elements before the pivot and
put all greater elements after the pivot. After this reorder of elements, the pivot is in its
final and correct position.
3. Apply the above steps recursively to every sub-array of elements.
Tests
@Test
public void test_quickSort() {
final int[] numbers = {13, 12, 14, 6, 7};
final int[] expected = {6, 7, 12, 13, 14};
sorting.quickSort(numbers, 0, numbers.length - 1);
assertArrayEquals(expected, numbers);
}
@Test
public void sortingArray() {
final int[] numbers = {7, 9, 1, 4, 9, 12, 4, 13, -2, 9};
final int[] expected = {-2, 1, 4, 4, 7, 9, 9, 9, 12, 13};
sorting.quickSort(numbers, 0, numbers.length - 1);
assertArrayEquals(expected, numbers);
}
0 12 3 4 mln=5 max =6 7 8 9
Found 21 3 7 9 13 18 21 41 52 81 j 97
Solution
Search the sorted Array by repeatedly dividing the search interval in half. If the element X is
less than the item in the middle of the interval, narrow the interval to the lower half.
Otherwise, narrow it to the upper half. Repeatedly check until the value is found or the
interval is empty. Figure 5.4 shows the iteration when we search for 21. Time complexity is
O (log n). If we pass an array of 4 billion elements, it takes at most 32 comparisons.
int min = 0;
int max = array.length - 1;
while (min <= max) {
int mid = (min + max) / 2;
if (target.compareTo(array[mid]) < 0) {
max = mid - 1;
} else if (target.compareTo(array[mid]) > 0) {
min = mid + 1;
} else {
return true;
}
}
return false;
}
}
@Test
public void binarySearch_target_Found() {
assertTrue(BinarySearch.search( "cal",
new String[]{"ada", "cal", "fda"}));
assertTrue(BinarySearch.search(21,
new Integer[]{1, 2, 3, 4, 5, 21}));
assertTrue(BinarySearch.search(21,
new Integer[]{3, 7, 9, 13, 18, 21, 41, 52, 81, 97}));
}
Solution
We can join the two lists into a new list and apply a sort algorithm such as bubble sort,
insertion, or quicksort. What we are going to do is implement a new algorithm maintaining
the same NlogN performance.
• We define a new List to add all elements from the other two lists in a sorted way.
• We compare elements from both lists and add the smaller one to the new list in every
iteration. Before passing to the next iteration, we increment in one the index of the list,
which contains the smaller element.
• If there is a list that still contains elements, we add them directly to the new list.
return mergedSortedList;
}
}
Tests
@Test
public void mergeSortedLists() {
List<Integer> sList1 = Arrays.asList(1,1,2,5,8);
List<Integer> sList2 = Arrays.asList(3,4,6);
assertEquals("[1, 1, 2, 3, 4, 5, 6, 8]",
SortedList.merge_sorted(sList1,sList2).toString());
}
@Test
public void mergeSortedLists2() {
List<Integer> sList1 = Arrays.asList(2,4,5);
List<Integer> sList2 = Arrays.asList(1,3,6);
assertEquals("[1, 2, 3, 4, 5, 6]",
SortedList.merge_sorted(sList1,sList2).toString());
}
6.0 Fundamentals
Stack
A Stack is an abstract data type, which includes a collection of objects that follow the last-in,
first-out (LIFO) principle, i.e., the element inserted at last is the first element to come out of
the list. A real-world example of a stack is the Stack of trays at a cafeteria.
Stacks have the following constraints:
• It can read only the last element of a stack, which is called the top. The peek operation
returns the value stored at the top of a stack without removing it from the stack.
You can implement a Stack using an Array or a Singly Linked List.
Stacks are helpful to handle temporary data as part of various algorithms, for example:
• Undo mechanism on text editor is based on Stack, i.e., cancel recent edit operations. The
editor keeps text changes in a Stack.
• Web browsers store URLs of visited websites on a stack. When a user visits a new
website, its URL is "pushed" on the Stack. When the user presses the "back" button, the
previous URL is "pop" from the Stack.
• It can read only the element at the front of a Queue, called a front.
\ back front
O 00 B
empty queue enqueue enqueue dequeue
• Call centers, theaters, and other similar services process customer requests using the
FIFO principle.
Solution
We use a stack data structure to ensure two delimiting symbols match up correctly (right
pair). Why Stack? Because insertion and deletion of items take place at one end called the
top of the Stack. The JDK includes the Java.util.Stack data structure. The push method adds
an item to the top of this Stack. The pop method removes the item at the top of this Stack.
• Iterate every character from the given expression. If it is an opening symbol, push that
symbol onto the Stack. If it is a closing symbol, pop an element from the Stack (the last
opening symbol added) and check that they are the right pair.
Tests
@Test
public void incorrect_expressions() {
assertFalse(delimiterMatching.apply(null));
assertFalse(delimiterMatching.apply(""));
assertFalse(delimiterMatching.apply("("));
assertFalse(delimiterMatching.apply("(()a]"));
}
@Test
public void correct_expressions() {
assertTrue(delimiterMatching.apply("()"));
assertTrue(delimiterMatching.apply("([])"));
assertTrue(delimiterMatching.apply("{{([])}}"));
assertTrue(delimiterMatching.apply("{{a([b])}c}dd"));
assertTrue(delimiterMatching.apply("(w*(x+y)/z-(p/(r-q)))"));
}
Solution
Stacks and Queues are abstract in their definition. That means, for example, in the case of
Queues, we can implement its behavior using two stacks.
Then we create two stacks of Java.uiLStack, inbox, and outbox. The add method pushes new
elements onto the inbox. And the peek method will do the following:
• If the outbox is empty, refill it by popping each element from the inbox and pushing it
onto the outbox.
• Pop and return the top element from the outbox.
INPUT
add l Queue: peek
import java.util.Stack;
public class QueueViaStacks<T> {
Stack<T> inbox;
Stack<T> outbox;
public QueueViaStacks() {
inbox = new Stack<>();
outbox = new Stack<>();
}
public T peek() {
if (outbox.isEmpty()) {
while (!inbox.isEmpty()) {
//Filled in inverse order
outbox.push(inbox.pop());
}
}
return outbox.pop();
}
}
Tests
public class QueueViaStacksTest {
QueueViaStacks<Integer> queueViaStacks;
@Before
public void Before() {
queueViaStacks = new QueueViaStacks<>();
}
@Test
public void pop_firstElement() {
queueViaStacks.add(4);
queueViaStacks.add(2);
queueViaStacks.add(9);
assertEquals(new Integer(4), queueViaStacks.peek());
}
}
Solution
• Convert the text to an array of characters.
import java.util.Stack;
public class StringUtils {
public static String reverse(String text) {
char[] charsArray = text.toCharArray();
Stack<Character> stack = new Stack<Character>();
return String.valueOf(charsArray);
}
}
Tests
@Test
public void reverseText_success() {
assertEquals("efac", StringUtils.reverse("cafe"));
assertEquals("2312cba", StringUtils.reverse("abc2132"));
}
7.0 Fundamentals
A hash table is a data structure that is very fast for insertion and searching. In Big O
Notation takes a constant time: O(1).
A hash table is built using an array of paired values. Each pair is comprised of a key and a
value, which are paired to indicate some significant association, for example:
students = { "1573297" => "Jan",
"8174321" => "Brian",
"8119214" => "James" }
In this example, the 1573297 is the key, and Jan is the value. They indicate that Passport
Number 1573297 identifies Jan.
For a seven-digit numeric range, we need an array's size of 10,000,000. If we decide to store
only a few thousand passport numbers, the array will be almost empty.
We need to squeeze our input key range into the array index range. To locate 1573297 in a
fast way, we use a hash function (hashing) to convert 1573297 (key) to an index number
within the size of our Array. Its value is stored at the index of the key after the key has been
hashed.
For example, the following figure shows our hash table after using a hash function.
Key Range:
[1 ... 9,999,999]
An algorithm hashes the key, turns it into an array index number, and jumps directly to the
index with that number to get the associated value.
But sometimes, there is a risk that different keys map to the same hashed array index. That is
called a collision; we obtain a key hash to an already filled position.
There are two solutions to deal with collisions: Chaining, where each entry in the hash array
points to its own linked list. The other one is open addressing, where only one array stores
all items; when we want to insert a new hashed key, and the slot is already filled, then the
algorithm looks for another empty slot to insert the new item. This kind of search is called
the probe sequence.
Most of the programming languages already implement a strategy for dealing with collisions
and choosing a hash function.
Solution
Imagine that we want to identify warehouses as keys composed of 3 characters.
We realize that exists a collision for the keys "DAB" and "BAD".
To implement chaining, we need to create a hash table entry that behaves as a linked list. And
we define an array that holds all entries.
We need to define our hash function. We will do it easily for testing purposes. We map
letters to numbers. (A = 1, B = 2, C = 3, D = 4, E = 5). Then take the product of the digits.
Example:
Key: ”DAB”
Mapping: D = 4, A = 1, B = 2
Product:
4* 1 *2= 8
First, we define our assumption.
@Test
public void hashTest() {
assertEquals(8, hashTable.hashTheKey("DAB"));
assertEquals(15, hashTable.hashTheKey("ACE"));
}
The put method creates a new hashed index by calling the hashTheKey method and creates a
new entry in the linked list at the hashed index. If the entry was already filled, then create the
entry at the end of the linked list.
Before implementingput and get methods, we define our assumption.
@Test
public void getTest() {
hashTable.put("DAB", "Atlanta");
hashTable.put("ABC", "Chicago");
hashTable.put("CAD", "Dallas");
hashTable.put("BAD", "Detroit");
assertEquals("Atlanta", hashTable.get("DAB"));
}
}
return null;
}
Solution
Firstly, create a class that implements the Map interface and which permits null values.
Secondly, iterate the input array and store elements and their frequency as key-value pairs.
Then, traverse the Map and inverse the order with the maximum frequency on top.
Finally, traverse the previous Map, and build a list of the elements with maximum
frequencies. Listing 7.2 shows the algorithm.
Tests
@Test
public void test_right_values() {
assertArrayEquals(new int[]{2, 3},
ArrayUtils.mostFrecuent(new int[]{3, 2, 0, 3, 1, 2}));
assertArrayEquals(new int[]{5},
ArrayUtils.mostFrecuent(new int[]{3, 5, 0, 5, 5, 1, 2}));
assertArrayEquals(new int[]{7},
ArrayUtils.mostFrecuent(new int[]{7}));
}
bolts = 'x',
Solution
• The method receives two characters array, nuts[] and bolts[].
• Iterate the bolts array and check if the HashMap already contains each bolt as a key.
• If a bolt at i location in the bolts array is already included in the HashMap, replace the
nuts[i] with the content from the bolts[i] to indicate we found a match.
Tests
@Test
public void matchNultsAndBoltsTest() {
char nuts[] = {'$', '%', '&', 'x', '@'};
char bolts[] = {'%', '@', 'x', '$', '&'};
NutsAndBolts.match(nuts, bolts);
assertArrayEquals(nuts, bolts);
}
8.0 Fundamentals
Tree
A tree is a data structure that consists of nodes connected by edges.
Tree structures are non-linear data structures. They allow us to implement algorithms much
faster than when using linear data structures.
Binary Tree
A binary tree can have at the most two children: a left node and a right node. Every node
contains two elements: a key used to identify the data stored by the node and a value that is
collected in the node. The following figure shows the binary tree terminology
• The value of the left Node must be lesser than the value of its parent.
Solution
We define a Product Class which will be the data contained in a Node.
Listing 8.1.1 shows how we create a Product Class.
Listing 8.1.2 shows how we create a NodeP Class to store a list of Products. Moreover, this
Class allows us to have two NodeP attributes to hold the left and right nodes.
if (root == null)
root = newNode;
else {
NodeP current = root;
NodeP parent;
while (true) {
parent = current;
if (gtin.compareTo(current.getGtin()) < 0) {
current = current.getLeft();
if (current == null) {
parent.setLeft(newNode);
return;
}
} else if (gtin.compareTo(current.getGtin()) > 0) {
current = current.getRight();
if (current == null) {
parent.setRight(newNode);
return;
}
} else
return; //already exists
}
}
return;
}
}
Listing 8.1.4 shows a find method, which iterates through all nodes until a GTIN is found.
This algorithm reduces the search space to N/2 because the binary search tree is always
ordered.
while (!current.getGtin().equals(gtin)) {
if (gtin.compareTo(current.getGtin()) < 0) {
current = current.getLeft();
} else {
current = current.getRight();
}
if (current == null) //not found in children
return null;
}
return current;
}
Tests
@Test
public void test_findNode() {
tree.insert("04000345706564",
new ArrayList<>(Arrays.asList(product1)));
tree.insert("07611400983416",
new ArrayList<>(Arrays.asList(product2)));
tree.insert("07611400989104",
new ArrayList<>(Arrays.asList(product3, product4)));
tree.insert("07611400989111",
new ArrayList<>(Arrays.asList(product5)));
tree.insert("07611400990292",
new ArrayList<>(Arrays.asList(product6, product7, product8)));
assertEquals(null, tree.find("07611400983324"));
tree.insert("07611400983324", new ArrayList<>(Arrays.asList(product9)));
assertTrue(tree.find("07611400983324") != null);
assertEquals("07611400983324", tree.find("07611400983324").getGtin());
}
This Binary Search Tree works well when the data is inserted in random order. But when the
values to be inserted are already ordered, a binary tree becomes unbalanced. With an
unbalanced tree, we cannot find data quickly.
One approach to solving unbalanced trees is the red-black tree technique, a binary search
tree with some unique features.
Assuming that we already have a balanced tree, listing 8.1.5 shows us how fast in terms of
comparisons could be a binary search tree, which depends on a number N of elements. For
instance, to find a product by GTIN in 1 billion products, the algorithm needs only 30
comparisons.
int acumElements = 0;
int comparisons = 0;
for (int level = 0; level <= N / 2; level++) {
int power = (int) Math.pow(2, level);
acumElements += power;
if (acumElements >= N) {
comparisons = ++level;
break;
}
}
System.out.println("comparisons -> " + comparisons);
return comparisons;
}
}
Tests
@Test
public void whenNelements_return_NroComparisons() {
assertTrue(treePerformance.comparisons(15) <= 4);
assertTrue(treePerformance.comparisons(31) <= 5);
assertTrue(treePerformance.comparisons(1000) <= 10);
assertTrue(treePerformance.comparisons(1000000000) <= 30);
}
• In-order Traversal: We visit the tree in this order: Left, Root, Right.
• Pre-order Traversal: We visit the tree in this order: Root, Left, Right.
• Post-order Traversal: We visit the tree in this order: Left, Right, Root.
For this solution, we traverse the tree in order traversal, where the output will produce the
key values in ascending order.
We create a recursive inOrderTraversal method that receives the root node as a parameter and
does the following instructions:
import java.util.StringJoiner;
public class TreeADT {
class Product {
int productId;
String name;
Product(int productId, String name) {
this.productId = productId;
this.name = name;
}
public String getName() {
return name;
}
}
class NodeP {
private String gtin;
private Product data;
private NodeP left;
private NodeP right;
public NodeP(String gtin, Product data) {
this.gtin = gtin;
this.data = data;
}
//setters and getters omitted
}
if (root == null)
root = newNode;
else {
NodeP current = root;
NodeP parent;
while (true) {
parent = current;
if (gtin.compareTo(current.getGtin()) < 0) {
current = current.getLeft();
if (current == null) {
parent.setLeft(newNode);
return;
}
} else if (gtin.compareTo(current.getGtin()) > 0) {
current = current.getRight();
if (current == null) {
parent.setRight(newNode);
return;
}
} else {
current.setData(product);
return; //already exists
}
}
}
return;
}
Tests
import java.util.StringJoiner;
public class TreeADTTest {
private TreeADT tree;
@Test
public void test_traverseTree() {
tree.insert("0510414", 12, "apple");
tree.insert("0301712", 14, "cherry");
tree.insert("0814101", 13, "mango");
tree.insert("0401312", 19, "orange");
tree.insert("0911741", 11, "banana");
tree.insert("0617411", 16, "grape");
tree.insert("0210417", 18, "kiwi");
StringJoiner stringJoiner = new StringJoiner(" -> ", "[", "]");
assertEquals("[kiwi -> cherry -> orange -> apple -> grape -> mango -> banana]",
tree.inOrderTraversal(tree.getRoot( ), stringJoiner));
}
}
9.0 Fundamentals
A Graph is a non-linear data structure consisting of nodes (vertices) and edges. Its shape
depends on the physical or abstract problem we are trying to solve. For example, if nodes
represent cities, the routes which connect cities may be defined by no-directed edges. But
if nodes represent tasks to complete a project, then their edges must be directed to indicate
which task must be completed before another.
Terminology
A Graph shows only the relationships between the vertices and the edges, and the most
important here is which edges are connected to which vertex. We can also say that a Graph
models connections between objects.
Adjacency
When a single edge connects two vertices, then they are adjacent or neighbors. In the figure
above, the vertices represented by Berlin and Leipzig are adjacent, but the cities Berlin and
Dresden are not.
Path
A Path is a sequence of edges. The figure above shows a path from Berlin to Munchen that
passes through cities Leipzig and Nurnberg. Then the path is Berlin, Leipzig, Nurnberg,
Munchen.
Connected Graphs
• Bremen
12
Hannover
Dormunt
10
• Frankfurt
• Stuttgart
Solution
Depth-first search (DFS) is an algorithm for traversing the Graph. The algorithm starts at
the root node (selecting some arbitrary city as the root node) and explores as far as possible
along each path. Figure 9.1.1 shows a sequence of steps if we choose Berlin as the root node.
Implementing the algorithm
Model the problem
We need an Object which supports any data included in the Node. We called it vertex.
Inside this vertex, we define a boolean variable to avoid cycles in searching cities, so we will
mark each Node when visiting it. Listing 9.1.1 shows a Vertex Class implementation.
We have two approaches to model how the vertices are connected (edges): the adjacency
matrix and the adjacency list. For this algorithm, we are going to implement the adjacency
matrix.
The adjacency matrix
In a graph of N vertices, we create a two-dimensional array of NxN. An edge between two
vertices (cities) indicates a connection (two adjacent nodes) represented by 1. No
connections are represented by 0.
For instance, the table above says Leipzig is adjacent to Berlin, Dresden, and Nurnberg, for
example.
Create and initialize an abstract data type
We create an abstract data type called Graph to define the behavior of our data structure.
We need a stack data structure so we can remember the visited vertices. A stack follows the
last-in, first-out (LIFO) principle, i.e., the city inserted at last is the first city to come out of
the stack.
We define an arrayOfVertex[] array to store new Vertices(cities) added to the Graph.
public Graph() {
arrayOfVertex = new Vertex[MAX_VERTEX];
mapOfVertex = new ConcurrentHashMap<>();
numOfVertices = 0;
matrixOfAdjVertex = new int[MAX_VERTEX][MAX_VERTEX];
stack = new Stack<>();
//initialize matrix
for (int i = 0; i < MAX_VERTEX; i++) {
for (int j = 0; j < MAX_VERTEX; j++) {
matrixOfAdjVertex[i][j] = 0;
}
}
}
}
The numOfVertices variable determines the location (index) of the new City in the
arrayOfVertex[].
Adding an edge
We add two entries to matrixOfAdjVertex , because the two cities are connected in both
directions.
The point here is that we need to define the topology of our Graph, adding Vertices(cities)
and edges that connect them.
The algorithm
Our dfs() method receives the City name as its argument. Then we locate the index of this
City in our HashMap, and it is marked as visited and push it onto the Stack.
We iterate the stack items until it is empty. And this is what we do in every iteration:
1. We retrieve the vertex from the top of the Stack (peek).
2. We try to retrieve at least one unvisited neighbor for this vertex.
3. If one vertex is found, it is marked as visited and pushes onto the Stack.
4. If one vertex is not found, we pop the Stack.
If Berlin were our entry city, then the first adjacent City will be Leipzig, marked as visited
and pushed into the Stack. We read (peek) Leipzig from the Stack in the next iteration and
look for its neighbors. Therefore, following these iterations, we arrive at Munchen as the last
city in this path. That is the in-depth essence of the algorithm. To explore as far as possible
along each branch before continuing with a new one.
while (!stack.isEmpty()) {
int adjVertex = getAdjVertex(stack.peek());
if (adjVertex != -1) {
arrayOfVertex[adjVertex].setVisited(true);
System.out.print(
arrayOfVertex[adjVertex].getName() + " ");
stack.push(adjVertex);
} else {
stack.pop();
}
}
}
Tests
public class GraphTest {
Graph graph;
@Before
public void setup() {
graph = new Graph();
}
@Test
public void test_addVertex() {
Vertex city = new Vertex("Berlin");
graph.addVertex(city);
assertTrue(graph.getMapOfVertex().size() == 1);
}
@Test
public void test_addEdge() {
String city1 = "Berlin";
String city2 = "Leipzig";
Vertex v1 = new Vertex(city1);
Vertex v2 = new Vertex(city2);
graph.addVertex(v1); //location 0
graph.addVertex(v2); //location 1
graph.addEdge(city1, city2);
assertTrue(graph.getMatrixOfAdjVertex()[0][1] == 1);
assertTrue(graph.getMatrixOfAdjVertex()[1][0] == 1);
}
Output:
Berlin Leipzig Dresden Nurnberg Munchen Rostock Magdeburg Hannover Dortmund
Frankfurt Stuttgart Bremen
We can change the entry city (Hannover) and see different traversing paths.
Output:
Hannover Dortmund Frankfurt Stuttgart Magdeburg Berlin Leipzig Dresden Nurnberg
Munchen Rostock Bremen
Solution
In the breadth-first search, the algorithm stays as close as possible to the starting point. It
visits all the vertices adjacent to the starting vertex. The algorithm is implemented using a
queue.
Figure 9.2.1 shows a sequence of steps if we choose Berlin as the root node. The numbers
indicate the order in which the vertices are visited.
The breath-first search algorithm first finds all the vertices that are one edge away from the
starting vertex, then all the vertices that are two edges away, three edges away, and so on. It
is useful to answer questions like what is the shortest path from Berlin to another city like
Munchen?
We traverse cities that are one edge away from Berlin (first level): Rostock, Magdeburg, and
Leipzig. Then we traverse cities that are two edges away from Berlin (second level):
Hannover, Nurnberg, and Dresden. Then we traverse cities that are three edges away from
Berlin (third level): Bremen, Dortmund, and Munchen. That's the idea. We already found
Munchen before traversing another possible path: Berlin, Magdeburg, Hannover, Dortmund,
Frankfurt, Stuttgart, Munchen, which corresponds to the sixth level.
The algorithm
Our bfs() method receives the City name as its argument. Then we locate the index of this
City in our HashMap, and it is marked as visited, and add it onto the queue.
We iterate the queue items until it is empty. And this is what we do in every iteration:
1. We retrieve and remove the head Vertex of this queue (remove).
2. We iterate in an inner loop through all neighbors (adjacent) of this head Vertex until all
they were visited. Every adjacent vertex is marked as visited and added to the queue.
3. When the previous iteration cannot find more adjacent vertices, then we retrieve and
remove the new head Vertex.
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
Tests
public class GraphTest {
Graph graph;
@Before
public void setup() {
graph = new Graph(15);
}
@Test
public void test_addVertex() {
Vertex city = new Vertex("Berlin");
graph.addVertex(city);
assertTrue(graph.getMapOfVertex().size() == 1);
}
@Test
public void test_addEdge() {
String city1 = "Berlin";
String city2 = "Leipzig";
Vertex v1 = new Vertex(city1);
Vertex v2 = new Vertex(city2);
graph.addVertex(v1); //location 0
graph.addVertex(v2); //location 1
graph.addEdge(city1, city2);
assertTrue(graph.getAdjList()[0].get(0) == 1);
assertTrue(graph.getAdjList()[1].get(0) == 0);
}
Output:
Berlin Leipzig Rostock Magdeburg Dresden Hannover Nurnberg Dortmund Bremen
Munchen Frankfurt Stuttgart
10.0 Fundamentals
There are two ways to receive a coding challenge from a recruiter. First, you receive a
description of the coding challenge via email that you need to solve at home. Second, you
need to solve the coding challenge in front of other developers at the recruiter's office.
For both ways, keep in mind a few things for making a good impression on the recruitment
process:
• Host your final code on a website like Github with a clear README file and clear
commit messages
• Include test cases for your code. It shows that you care about maintainability.
• Apply SOLID principles, which tell you how to arrange your functions into classes and
how those classes should be interrelated.
Solution
Imagine that we have a budget of 4 US$ and we want to buy the most valuable snacks from
table 10.1.1
But who decides if a product is more valuable than another one? Well, this depends on every
business. It could be an estimation based on quantitative or qualitative analysis. We choose a
quantitative approach for this solution based on which product gives us more grams per dollar
invested.
We use the Red-Green Refactor technique to implement our algorithm, which is the basis of
test-drive-development (TDD). In every assumption, we will write a test and see if it fails.
Then, we write the code that implements only that test and sees if it succeeded, then we can
refactor the code to make it better. Then we continue with another assumption and repeat
the previous steps until the algorithm is successfully implemented for all tests.
To generalize the concept of "the most valuable product," we assign a value to every
product. Our algorithm receives two parameters: an array 2-D, which includes [product-
id][price][value], and the budget.
BasketOptimized basketOptimized;
@Before
public void setup() {
basketOptimized = new BasketOptimized();
}
double[][] mostValueableProducts =
basketOptimized.fill(myProducts, 4);
assertEquals(590d,
Arrays.stream(mostValueableProducts).
mapToDouble(arr -> arr[2]).sum(), 0);
}
}
The first time, it should fail because the fill method doesn't exist. Then we need to create an
easy implementation to pass the test: the sum of the values must be equal to 590 because this
represents all selected products whose prices sum less than or equal to 4.
Now, we proceed to implement the fill method.
Assumption #2 - Given an array of products not ordered by value, return the most
valuable products
@Test
public void given_productsNotOrderedByValue_return_mostValuables() {
double[][] mostValuableProducts =
basketOptimized.fill(myProducts, 4);
assertEquals(590d,
Arrays.stream(mostValuableProducts).
mapToDouble(arr -> arr[2]).sum(), 0);
}
We realize we need to order the Array by value because we want the most valuable products,
so it is time to refactor our algorithm. What we need to do is to sort our input array.
public double[][] fill(double[][] myProducts, double budget) {
Arrays.sort(myProducts, Collections.reverseOrder(
Comparator.comparingDouble(a -> a[2])));
int len = myProducts.length;
double[][] mostValueableProducts = new double[len][3];
double sum = 0;
for (int idx = 0; idx < len; idx++) {
sum = sum + myProducts[idx][1]; //price
if (sum <= budget) {
mostValueableProducts[idx][0] =
myProducts[idx][0]; //id
mostValueableProducts[idx][1] =
myProducts[idx][1]; //price
mostValueableProducts[idx][2] =
myProducts[idx][2]; //value
}
}
return mostValueableProducts;
}
Then we can see that our two first test cases were successful.
Assumption #3 - Given an array of products, we need to obtain the most valuable
products from all possible combinations of the products
The test expects a result of 590, which corresponds to the final price of 3,73US$
(0.98+0.98+0.48+1.29). Once the algorithm sort by value the input array, we obtain the
following new input array:
[1.0, 0.98, 230.0]
[5.0, 0.98, 230.0]
[7.0, 0.48, 75.0]
[4.0, 1.29, 55.0]
[2.0, 0.51, 30.0]
[3.0, 0.49, 28.0]
[6.0, 4.86, 14.0]
But here we realize another combination of products that give us the most valuable
products: 230+230+75+30+28 = 593, which corresponds to the final price of 3,44US$.
Then we need to refactor our code to calculate all combinations (subsets) and return the
most valuable products under a budget of 4 US$.
The subsets can be represented by the binary options from 0 to 7 (the array size).
Bitwise operators allow us to manipulate the bits within an integer. For example, the int
value for 33 in binary is 00100001, where each position represents a power of two, starting
with 20 at the rightmost bit.
int numIterations = (int) Math.pow(2, myProducts.length);
So, for seven products, we have 128 iterations. And for every iteration, we build an inner
loop to decide which products to include in a subset of products.
Let see an example:
When int idx = 33
We need to build a subset with those products, which pass the following criteria:
if ((idx & (int) Math.pow(2, idx2)) == 0) {
00100001 0 1 0001 1
00100001 1 2 0010 0
00100001 2 4 0100 0
00100001 3 8 1000 0
00100001 4 16 00010000 0
00100001 5 32 00100000 1
00100001 6 64 01000000 0
That means that a new subset will include those products located at indexes 1, 2, 3, 4, and 6
from our Array of products. And we need to ask if we can afford to buy these products
under our limited budget.
if (subSet.length > 0 && sumPrice <= budget) {
We build a HashMap to store all combinations and the sum of its values. Finally, we return
the first element of the HashMap, ordered by value.
combinations.entrySet()
.stream()
.sorted(Map.Entry.comparingByValue(Comparator.reverseOrder()))
.forEachOrdered(x -> reverseSortedMap
.put(x.getKey(), x.getValue()));
Tests
BasketOptimized basketOptimized;
@Before
public void setup() {
basketOptimized = new BasketOptimized();
}
@Test
public void given_productsOrderedByValue_return_mostValuables() {
double[][] mostValuableProducts =
basketOptimized.fill(myProducts, 4);
assertEquals(590d,
Arrays.stream(mostValuableProducts).
mapToDouble(arr -> arr[2]).sum(), 0);
}
@Test
public void given_productsNotOrderedByValue_return_mostValuables() {
double[][] mostValuableProducts =
basketOptimized.fill(myProducts, 4);
assertEquals(590d,
Arrays.stream(mostValuableProducts).
mapToDouble(arr -> arr[2]).sum(), 0);
}
double[][] mostValuableProducts =
basketOptimized.fill(myProducts, 4);
assertEquals(593d,
Arrays.stream(mostValuableProducts).
mapToDouble(arr -> arr[2]).sum(), 0);
}
Solution
We learn from Object-Oriented Design and SOLID principles that we need to delegate
responsibilities to different components. For this game, we identify the following classes:
Board — set size, get Winner, draw?
Player — (Human, IA)
Utils — to load configuration files.
App — the main Class that assembly and controls our different components.
Test case #1: Define the size of the board
Based on the size of the Board, we need to initialize a bi-dimensional array to store the
symbols after every move. Listing 10.2.1 shows one assumption about the setSize method.
@Before
public void setUp() {
board = new Board();
}
@Test
public void whenSizeThenSetupBoardSize() throws Exception {
board.setSize(10);
assertEquals(10, board.getBoard().length);
}
}
Listing 10.2.2 shows an initial implementation of Board Class and the setSize method.
TDD allows us to design, build, and test the smallest methods first and assemble them later.
And the most important is that we can refactor it without breaking the rest of the test cases.
Test case #2: Enter a symbol based on valid coordinates
Once the size is set up, we need to accept valid coordinates and check if that location is still
available.
@Test
public void whenCoordinatesAreNotBusyThenPutSymbol() throws Exception {
board.setSize(3);
board.putSymbol(1, 2, "X");
board.putSymbol(2, 3, "O");
assertEquals("O", board.getBoard()[1][2]);
}
If we set the size to 4, that means that for the player, the lower limit is 1, and the upper limit
is 4.
Test case #3: After every move, print the board
Every time a player enters valid coordinates, then the Board is updated and printed.
@Test
public void whenBoardIsNotNullThenPrintIsPossible() {
board.setSize(10);
board.putSymbol(1, 2, "X");
board.putSymbol(2, 3, "O");
board.print();
}
Here you can even delegate the print of the Board to another component, e.g.,
Console.print. In this way, when you want to print in XML o HTML format, Console Class
will be responsible for implementing the new methods. There should never be more than one reason
for a class to change.
Test Case #4: Returns a winner
@Test
public void horizontalLineFilledThenWinnerX() {
board.setSize(3);
board.putSymbol(1, 1, "X");
board.putSymbol(1, 2, "O");
board.putSymbol(2, 1, "X");
board.putSymbol(2, 3, "O");
board.putSymbol(3, 1, "X");
board.print();
assertEquals("X", board.getWinner());
}
Then we iterate every horizontal line and check if it is filled with the same symbol. Listing
10.2.6 shows the implementation of the getWinner method.
We need the same logic for all the ways to win: vertical, diagonal, so we need to abstract
every case in sub methods. Listing 10.2.7 shows the new getWinner method after
refactoring.
if (board == null)
throw new RuntimeException("Board is not initialized");
Now let see the Utils Class, which loads the symbols and the size of the Board.
Test case #5: Load file
We need a method that, given a Filename, retrieves the content of that File. Listing 10.2.8
shows the test case.
@Before
public void setUp() {
utils = new Utils();
}
@Test
public void whenFileExistsThenReturnContent() throws Exception {
String content = utils.loadFile("symbols.txt");
assertNotNull(content);
}
}
Assumes that our configuration files are located under the project name. Listing 10.2.9
shows a loadFile implementation.
import java.nio.file.Files;
import java.nio.file.Paths;
@Test
public void whenGetBoardSizeIs4ThenReturnSize() throws Exception {
String content = utils.loadFile("board.txt");
assertEquals(4, utils.getBoardSize(content));
}
if (content == null)
throw new RuntimeException("Invalid setting for the board");
return Integer.valueOf(content).intValue();
}
board.txt
4
@Test
public void whenSymbolsIsThreeThenReturnValues() throws Exception {
String content = utils.loadFile("symbols.txt");
String[] symbols = utils.getSymbols(content);
assertArrayEquals(new String[]{"X","O","A"}, symbols);
}
symbols.txt
X,O,A
return symbols;
}
@Test
public void inputFromConsoleThenReturnInput() throws Exception {
Input input = utils.getInputFromConsole("2,3");
assertEquals(2, input.getX());
assertEquals(3, input.getY());
}
Pattern p = Pattern.compile("[0-9]*\\.?,[0-9]+");
Matcher matcher = p.matcher(inputFromConsole);
if (!matcher.find())
throw new RuntimeException("Invalid input from console");
@Before
public void setUp() {
humanPlayer = new Human();
iaPlayer = new IA();
board = new Board();
board.setSize(3);
}
@Test
public void whenSetSymbolThenReturnSameSymbol() throws Exception {
humanPlayer.setSymbol("A");
assertEquals("A", humanPlayer.getSymbol());
}
@Test
public void whenIAPlaysThenReturnInput() throws Exception {
iaPlayer.setSymbol("A");
board.putSymbol(1, 1, "X");
board.putSymbol(2, 2, "O");
board.print();
Input input = iaPlayer.play(board.getBoard());
assertEquals(1, input.getX());
assertEquals(2, input.getY());
}
Now that we have all components ready, it is time to build the main Class. Listing 10.2.19
shows how App Class assembly all components.
int idx = 0;
boolean stillPlaying = true;
while (stillPlaying) {
Player player = playersIterator.get(idx++);
if (player instanceof IA) {
System.out.println("Player " + player.getSymbol() +
" enter your coordinates in Format x,y: ");
Input input = ((IA) player).play(board.getBoard());
board.putSymbol(input.getX(), input.getY(), player.getSymbol());
} else {
boolean coordinateOk = false;
while (!coordinateOk) {
System.out.println("Player " + player.getSymbol() +
" enter your coordinates in Format x,y: ");
try {
Input input = utils.getInputFromConsole(scanner.nextLine());
board.putSymbol(input.getX(), input.getY(), player.getSymbol());
coordinateOk = true;
} catch (RuntimeException ex) {
System.out.println(ex.getMessage());
}
}
}
board.print();
String winner = board.getWinner();
if (winner != null) {
stillPlaying = false;
if (winner.equals(DRAW_)) {
System.out.println(DRAW_);
} else {
System.out.println("Player " + winner + " is the Winner!");
}
}
if (idx == 3) //to control the turn of every player
idx = 0;
}
}
}
DEMO:
Welcome to TIC TAC TOE 2.0!
You are playing in a board 4x4
Player A enter your coordinates in Format x,y:
O| |X|O
A| | |
A|A|X|
A|O|X|O
@Before
public void setUp() {
board = new Board();
}
@Rule
public ExpectedException thrownException = ExpectedException.none();
@Test
public void whenSizeThenSetupBoardSize() throws Exception {
board.setSize(10);
assertEquals(10, board.getBoard().length);
}
@Test
public void whenPutCharacterWrongCoordinateThenThrowError() throws Exception {
board.setSize(4);
thrownException.expect(RuntimeException.class);
board.putSymbol(1, 5, "X");
}
@Test
public void whenCoordinateIsValidThenPutCharacterIsOk() throws Exception {
board.setSize(3);
board.putSymbol(1, 2, "X");
assertEquals("X", board.getBoard()[0][1]);
}
@Test
public void whenCoordinatesAreBusyThenPutCharacterThrowError() throws Exception {
board.setSize(3);
board.putSymbol(1, 2, "X");
thrownException.expect(RuntimeException.class);
board.putSymbol(1, 2, "O");
}
@Test
public void whenCoordinatesAreNotBusyThenPutSymbol() throws Exception {
board.setSize(3);
board.putSymbol(1, 2, "X");
board.putSymbol(2, 3, "O");
assertEquals("O", board.getBoard()[1][2]);
}
@Test
public void whenBoardIsNotNullThenPrintIsPossible() {
board.setSize(10);
board.print();
}
@Test
public void horizontalLineIsNotSameSymbolThenReturnNull() {
board.setSize(3);
board.putSymbol(2, 2, "X");
board.putSymbol(1, 2, "O");
board.putSymbol(1, 3, "X");
board.putSymbol(2, 1, "O");
board.putSymbol(3, 2, "X");
board.print();
assertEquals(null, board.getWinner());
}
@Test
public void horizontalLineFilledThenWinnerX() {
board.setSize(3);
board.putSymbol(1, 1, "X");
board.putSymbol(1, 2, "O");
board.putSymbol(2, 1, "X");
board.putSymbol(2, 3, "O");
board.putSymbol(3, 1, "X");
board.print();
assertEquals("X", board.getWinner());
}
@Test
public void horizontalLineFilledThenWinnerO() {
board.setSize(4);
board.putSymbol(1, 1, "X");
board.putSymbol(1, 4, "O");
board.putSymbol(2, 1, "X");
board.putSymbol(2, 4, "O");
board.putSymbol(3, 1, "X");
board.putSymbol(3, 4, "O");
board.putSymbol(3, 3, "X");
board.putSymbol(4, 4, "O");
board.print();
assertEquals("O", board.getWinner());
}
@Test
public void diagonalLineSameSymbolThenReturnWinnerX() {
board.setSize(3);
board.putSymbol(1, 1, "X");
board.putSymbol(1, 2, "O");
board.putSymbol(2, 2, "X");
board.putSymbol(2, 1, "O");
board.putSymbol(3, 3, "X");
board.print();
assertEquals("X", board.getWinner());
}
@Test
public void diagonalLineSameSymbolThenReturnWinnerO() {
board.setSize(3);
board.putSymbol(1, 1, "X");
board.putSymbol(1, 3, "O");
board.putSymbol(3, 2, "X");
board.putSymbol(2, 2, "O");
board.putSymbol(3, 3, "X");
board.putSymbol(3, 1, "O");
board.print();
assertEquals("O", board.getWinner());
}
@Test
public void diagonalLineSameSymbolThenReturnWinnerX_2() {
board.setSize(5);
board.putSymbol(1, 1, "O");
board.putSymbol(1, 5, "X");
board.putSymbol(3, 2, "O");
board.putSymbol(2, 4, "X");
board.putSymbol(3, 4, "O");
board.putSymbol(3, 3, "X");
board.putSymbol(1, 2, "O");
board.putSymbol(4, 2, "X");
board.putSymbol(5, 3, "O");
board.putSymbol(5, 1, "X");
board.print();
assertEquals("X", board.getWinner());
}
111 Cracking the Coding Interview
@Test
public void whenIsDrawThenReturnTrue() {
board.setSize(3);
board.putSymbol(1, 1, "X");
board.putSymbol(1, 2, "O");
board.putSymbol(3, 2, "X");
board.putSymbol(2, 2, "X");
board.putSymbol(3, 3, "O");
board.putSymbol(2, 1, "O");
board.putSymbol(1, 3, "X");
board.putSymbol(3, 1, "O");
board.putSymbol(2, 3, "X");
board.print();
assertEquals("DRAW!", board.getWinner());
}
}
import org.junit.rules.ExpectedException;
import static org.junit.Assert.*;
@Before
public void setUp() {
utils = new Utils();
}
@Rule
public ExpectedException thrownException = ExpectedException.none();
@Test
public void whenFileNotExistsThenThrowError() throws Exception {
thrownException.expect(RuntimeException.class);
utils.loadFile("board2.txt");
}
@Test
public void whenFileExistsThenReturnContent() throws Exception {
String content = utils.loadFile("symbols.txt");
assertNotNull(content);
}
@Test
public void whenGetBoardSizeIs11ThenThrowError() throws Exception {
thrownException.expect(RuntimeException.class);
utils.getBoardSize("11");
}
@Test
public void whenGetSymbolsIsNotThreeThenThrowError() throws Exception {
thrownException.expect(RuntimeException .class);
utils.getSymbols("A,B,C,D");
}
@Test
public void whenSymbolsIsThreeThenReturnValues() throws Exception {
String content = utils.loadFile("symbols.txt");
String[] symbols = utils.getSymbols(content);
assertArrayEquals(new String[]{"X","O","A"}, symbols);
}
@Test
public void whenInputFromConsoleIsWrongThrowException() throws Exception {
thrownException.expect(RuntimeException.class);
utils.getInputFromConsole("");
}
@Test
public void inputFromConsoleIsWrongFormatThrowException() throws Exception {
thrownException.expect(RuntimeException.class);
utils.getInputFromConsole("10");
}
@Test
public void inputFromConsoleThenReturnInput() throws Exception {
Input input = utils.getInputFromConsole("2,3");
assertEquals(2, input.getX());
assertEquals(3, input.getY());
}
}
Big O Notation is a mathematical notation that helps us analyze how complex an algorithm
is in terms of time and space. When we build an application for one user or millions of users,
it matters.
We usually implement different algorithms to solve one problem and measure how efficient
is one respect to the other ones.
• Best case or Big Omega £2(n): Usually, the algorithm executes independently of the
input size in one step.
• Average case or Big Theta 0(n): When the input size is random.
• Worst-case or Big O Notation O(n): Gives us an upper bound on the runtime for any
input. It gives us a kind of guarantee that the algorithm will never take any longer with a
new input size.
The order of growth is related to how the runtime of an algorithm increases when the input
size increases without limit and tells us how efficient the algorithm is. We can compare the
relative performance of alternative algorithms.
Big O Notations examples:
O(1) - Constant
It does not matter if the input contains 1000 or 1 million items. The code always executes in
one step.
public class BigONotation {
public void constant(List<String> list, String item) {
list.add(item);
}
}
@Test
public void test_constantTime() {
List<String> list = new ArrayList<>(Arrays.asList("one", "two", "three"));
bigONotation.constant(list, "four");
}
In a best-case scenario, an add method takes O(1) time. The worst-case scenario takes O(n).
O(N) - Linear
Our algorithm runs in O(N) time if the number of steps depends on the number of items
included in the input.
@Test
public void test_linearTime() {
final int[] numbers = {1, 2, 4, 6, 1, 6};
assertTrue(bigONotation.sum(numbers) == 20);
}
O(N2) - Quadratic
When we have two loops nested in our code, we say it runs in quadratic time O(N2). For
@Test
public void test_quadraticTime() {
bigONotation.initializeBoard(3);
}
O(N3) - Cubic
We say that our algorithm runs in Cubic time when the code includes at the most three
nested loops. For example, given N integers, how many triples sum to precisely zero? One
approach (not the best) is to use three nested loops.
@Test
public void test_countThreeSum() {
final int[] numbers = {30, -40, -20, -10, 40, 0, 10, 5};
assertTrue(bigONotation.countThreeSum(numbers) == 4);
}
O(LogN) — Logarithmic
This kind of algorithm produces a growth curve that peaks at the beginning and slowly
flattens out as the size of the input increase.
Log28 = 3
return sum;
}
First, we split the code into individual operations and then compute how many times it is
being executed, as is shown in the following table.
Figure A.2 O(N) isfaster than O(N2) for all amounts of data
Now, if we compare O(100N) with O(N2), we can see that O(N2) is faster than O(100N) for
some amounts of data, as shown in Figure A.3.
Figure A.3 O(N2) isfaster than O(100N) for some amounts of data
But after a point, O(100N) becomes faster and remains faster for all increasing amounts of
data from that point onward. And that is the reason why Big O Notation ignores constants.
Because of this, O(100N) is written as O(N).