Vdoc - Pub The Complete Coding Interview Guide in Java
Vdoc - Pub The Complete Coding Interview Guide in Java
1. Start something
2. It’s time to shine online
8. Summary
11. Chapter 2:
12. What Interviews at Big Companies Look Like
1. Interviews at Google
2. Interviews at Amazon
3. Interviews at Microsoft
4. Interviews at Facebook
5. Interviews at Crossover
6. Summary
13. Chapter 3:
14. Common Non-Technical Questions and How To Answer Them
15. Chapter 4:
16. How to Handle Failures
17. Chapter 5:
18. How to Approach a Coding Challenge
1. Technical quiz
2. Coding challenge
1. The problems specific to coding challenges are
meant to be difficult
2. Tackling a coding challenge problem
3. Summary
1. Technical requirements
2. Understanding OOP concepts
1. What is an object?
2. What is a class?
3. What is abstraction?
4. What is encapsulation?
5. What is inheritance?
6. What is polymorphism?
7. What is association?
8. What is aggregation?
9. What is composition?
1. What is S?
2. What is L?
3. What is I?
4. What is D?
5. Coding challenges
1. Example 1: Jukebox
2. Example 2: Vending machine
3. Example 3: Deck of cards
4. Example 4: Parking lot
5. Example 5: Online reader system
6. Example 6: Hash table
7. Example 7: File system
8. Example 8: Tuple
9. Example 9: Cinema with a movie ticket
booking system
6. Summary
22. Chapter 7:
23. Big O Analysis of Algorithms
1. Analogy
2. Big O complexity time
3. The best case, worst case, and expected case
4. Big O examples
1. Example 1 – O(1)
2. Example 2 – O(n), linear time algorithms
3. Example 3 – O(n), dropping the constants
4. Example 6 – different steps are summed or
multiplied
5. Example 7 – log n runtimes
6. Example 9 – in-order traversal of a binary tree
7. Example 10 – n may vary
8. Example 11 – memoization
9. Example 13 – identifying O(1) loops
10. Example 14 – looping half of the array
11. Example 15 – reducing Big O expressions
12. Example 16 – looping with O(log n)
13. Example 17 – string comparison
14. Example 18 – factorial Big O
15. Example 19 – using n notation with caution
16. Example 21 – the number of iteration counts in
Big O
17. Example 22 – digits
18. Example 23 – sorting
24. Chapter 8:
25. Recursion and Dynamic Programming
1. Technical requirements
2. Recursion in a nutshell
4. Coding challenges
1. Technical requirements
2. Bit manipulation in a nutshell
3. Coding challenges
4. Summary
28. Section 3: Algorithms and Data Structures
29. Chapter 10:
30. Arrays and Strings
1. Technical requirements
2. Arrays and strings in a nutshell
3. Coding challenges
4. Summary
5. Summary
1. Technical requirements
2. Stacks in a nutshell
3. Queues in a nutshell
4. Coding challenges
1. Technical requirements
2. Trees in a nutshell
1. General tree
2. Binary Search Tree
3. Balanced and unbalanced binary trees
4. Complete binary tree
5. Full binary tree
6. Perfect binary tree
7. Binary Heaps
3. Graphs in a nutshell
1. Adjacency matrix
2. Adjacency list
3. Graph traversal
4. Coding challenges
5. Advanced topics
6. Summary
1. Technical requirements
2. Sorting algorithms
1. Heap Sort
2. Merge Sort
3. Quick Sort
4. Bucket Sort
5. Radix Sort
3. Searching algorithms
4. Coding challenges
5. Summary
1. Technical requirements
2. Tips and suggestions
3. Coding challenges
4. Summary
1. Technical Requirements
2. Java concurrency (multithreading) in a nutshell
3. Questions and coding challenges
3. Summary
46. Chapter 18:
47. Unit Testing
1. Technical Requirements
2. Unit testing in a nutshell
3. Questions and coding challenges
4. Summary
1. Scalability in a nutshell
2. Questions and coding challenges
4. Summary
Landmarks
1. Cover
2. Table of Contents
BIRMINGHAM—MUMBAI
Livery Place
35 Livery Street
Birmingham
B3 2PB, UK.
ISBN 978-1-83921-206-2
www.packt.com
Packt.com
Subscribe to our online digital library for full access to
over 7,000 books and videos, as well as industry leading
tools to help you plan your personal development and
advance your career. For more information, please visit
our website.
Why subscribe?
Spend less time learning and more time coding with practical
eBooks and videos from over 4,000 industry professionals
Improve your learning with Skill Plans designed especially for you
Contributors
KNOW YOURSELF 4
START SOMETHING 11
LINKEDIN RESUME 21
SUMMARY 25
Chapter 2:
INTERVIEWS AT GOOGLE 28
INTERVIEWS AT AMAZON 28
INTERVIEWS AT MICROSOFT 29
INTERVIEWS AT FACEBOOK 29
INTERVIEWS AT CROSSOVER 30
SUMMARY 31
Chapter 3:
SUMMARY 40
Chapter 4:
FAILURE IS AN OPTION 42
SUMMARY 45
Chapter 5:
TECHNICAL QUIZ 48
CODING CHALLENGE 50
SUMMARY 59
Section 2: Concepts
Chapter 6:
Object-Oriented Programming
TECHNICAL REQUIREMENTS 64
WHAT IS AN OBJECT? 65
WHAT IS A CLASS? 66
WHAT IS ABSTRACTION? 67
WHAT IS ENCAPSULATION? 70
WHAT IS INHERITANCE? 73
WHAT IS POLYMORPHISM? 75
WHAT IS ASSOCIATION? 79
WHAT IS AGGREGATION? 81
WHAT IS COMPOSITION? 83
WHAT IS S? 86
WHAT IS L? 93
WHAT IS I? 98
WHAT IS D? 102
POPULAR QUESTIONS PERTAINING TO OOP,
SOLID, AND GOF DESIGN PATTERNS 105
EXAMPLE 1: JUKEBOX 123
EXAMPLE 8: TUPLE 149
SUMMARY 150
Chapter 7:
ANALOGY 152
EXAMPLE 1 – O(1) 155
EXAMPLE 11 – MEMOIZATION 166
EXAMPLE 22 – DIGITS 175
EXAMPLE 23 – SORTING 176
SUMMARY 178
Chapter 8:
TECHNICAL REQUIREMENTS 180
RECURSION IN A NUTSHELL 180
CODING CHALLENGES 185
Bit Manipulation
TECHNICAL REQUIREMENTS 236
BITWISE OPERATORS 237
CODING CHALLENGES 242
SUMMARY 285
Section 3: Algorithms and Data
Structures
Chapter 10:
TECHNICAL REQUIREMENTS 289
CODING CHALLENGES 290
SUMMARY 376
Chapter 11:
TECHNICAL REQUIREMENTS 378
MAPS IN A NUTSHELL 379
CODING CHALLENGES 380
SUMMARY 423
Chapter 12:
TECHNICAL REQUIREMENTS 426
STACKS IN A NUTSHELL 426
QUEUES IN A NUTSHELL 429
CODING CHALLENGES 432
SUMMARY 465
Chapter 13:
TECHNICAL REQUIREMENTS 467
TREES IN A NUTSHELL 468
GENERAL TREE 469
BINARY HEAPS 479
GRAPHS IN A NUTSHELL 481
ADJACENCY MATRIX 482
ADJACENCY LIST 483
GRAPH TRAVERSAL 484
CODING CHALLENGES 486
ADVANCED TOPICS 556
SUMMARY 557
Chapter 14:
TECHNICAL REQUIREMENTS 559
HEAP SORT 561
MERGE SORT 564
QUICK SORT 567
BUCKET SORT 570
RADIX SORT 575
CODING CHALLENGES 580
SUMMARY 627
Chapter 15:
TECHNICAL REQUIREMENTS 630
CODING CHALLENGES 631
SUMMARY 667
Section 4: Bonus – Concurrency and
Functional Programming
Chapter 16:
Concurrency
TECHNICAL REQUIREMENTS 672
Functional-Style Programming
CODING CHALLENGE 21 –
STRING::VALUEOF 707
SUMMARY 707
Chapter 18:
Unit Testing
SUMMARY 724
Chapter 19:
System Scalability
SCALABILITY IN A NUTSHELL 726
SUMMARY 739
Other Books You May Enjoy
Collect the best techniques for solving a wide range of Java coding
problems.
https://static.packt-
cdn.com/downloads/9781839212062_ColorImages.pdf
Conventions used
There are a number of text conventions used throughout
this book.
rectangle.draw();
circle.draw();
triangle.draw();
rectangle.draw();
circle.draw();
Get in touch
Feedback from our readers is always welcome.
General feedback: If you have questions about any
aspect of this book, mention the book title in the subject
of your message and email us at
customercare@packtpub.com.
Reviews
Please leave a review. Once you have read and used this
book, why not leave a review on the site that you
purchased it from? Potential readers can then see and
use your unbiased opinion to make purchase decisions,
we at Packt can understand what you think about our
products, and our authors can see your feedback on their
book. Thank you!
Since you bought this book, you want to invest some time
and money in a Java software development career.
Mainly, you want to become part of the amazing Java
ecosystem! You already feel the power and the energy
that comes from focusing on working with Java,
therefore, even if you haven’t yet actively thought about
it, you’ve already started to prepare yourself for a Java
interview.
Ideally, in the long term, you must focus on what you like
to do the most! This way, you maximize your chances of
becoming a top Java developer. But, doing what you like
the most should be considered in the context of what the
IT market offers (in both the short term, and most
importantly, the long term). Some Java technologies are
widely covered by job offers, while others may require a
lot of time to find a job or must make some really
unpleasant trade-offs (for example, relocation). It is
strongly advisable to periodically consult and participate
(every vote counts) in the most relevant Java surveys
conducted by websites such as blogs.oracle.com, snyk.io,
jaxenter.com, codeburst.io, jetbrains.com, and
dzone.com. Having a wide range of companies to choose
from statistically maximizes the chances of finding the
right company for you. This is half of the problem, while
the other half is to prepare yourself to make sure that the
company with the job you want will want you.
Important note
Take your time to read several surveys from the last 2-3
years from important websites such as blogs.oracle.com,
snyk.io, jaxenter.com, codeburst.io, jetbrains.com, and
dzone.com. Primarily, you can search on Google for java
technologies survey 2019 or similar combinations of
keywords. Also, don’t neglect the financial part, so make
sure to search for java salaries survey 2019 as well.
This way, you can quickly filter the technologies that are
most required by the market. Learning popular
technologies maximizes your chances of getting a job in
the near future.
Start something
For a student or a recent graduate, it is pretty hard to
decide where to start from in order to gain experience
and write a resume. You are aware that you should start
something, but you cannot decide what that something
should be. Well, that something should be code. Before
you have any formal work, get involved in school
projects, internships, programming, volunteering work,
and any kind of practical experience.
Be enthusiastic (show people that you enjoy your work, but don’t
exaggerate).
Take the chance to prove your speaking skills (this opens you the
door to technical conferences).
Promote your work (add links and hints for more videos, source
code, and so on).
Important note
Personal websites
Don’t list all Java flavors: Don’t add a list such as Spring MVC,
Spring Data, Spring Data REST, Spring Security, and so on. Just
say Spring. Or, if you are Java EE guy, then don’t add a list of JPA,
EJB, JSF, JAX-RX, JSON-B, JSON-P, JASPIC, and so on. Just say
Java EE, Jakarta EE. Or, if you see them listed that way in the job
description, then you can add them between brackets. For
example: Spring (MVC, Data including Data REST, Security) or
Java EE (JPA, EJB, JSF, JAX-RX, JSON-B, JSON-B, JASPIC).
LinkedIn resume
Most likely, your LinkedIn profile will be the first stop
for recruiters. Moreover, a significant number of e-job
platforms require your LinkedIn account whenever you
try to apply for a job. There are even cases where this
account is mandatory.
LinkedIn is a social network dedicated to tracking
professional connections. Essentially, LinkedIn is an
online resume on steroids. On LinkedIn, you can create
job alerts, and colleagues, customers, and friends can
endorse you or your work, which can be quite valuable.
Important note
See if you can apply directly via the company website (by
bypassing the placement agency, you can speed up the process and
the company can hire you directly without paying commission to
the placement agency).
You have the chance to find out more about the company history,
vision, projects, culture, and so on.
You can find out contacts of relevant people at the company (for
example, you can find a phone number for details and support).
Not selling yourself well: The interviewer must see your value.
Nobody can communicate your value to them better than you can.
Tell them about a problem that you had (at a previous company, in
a certain project, and so on) and explain how you solved it with
your team or independently. Employers want people who are
excellent team players but are capable of working independently
as well. Follow the Situation|Action|Result (SAR) approach. Start
by describing the situation. Continue by explaining the actions you
took, and finally, describe the result.
Summary
This chapter summarized the best practices that should
be followed to obtain a job in the Java ecosystem. We
talked about choosing a proper job and our eligibility,
getting experience, working on resumes, and so on. Most
of this advice was addressed to students or people who
have just graduated. Of course, do not consider these
pieces of advice as an exhaustive list or a list that should
be applied integrally. These practices will help you pick
up the fruits that you consider appealing and allow you
to add your own touch to the process.
Amazon
Microsoft
Crossover
Interviews at Google
The Google interview starts with a technical phone
screen (technical questions and coding challenges).
There will be 4-5 people involved in these technical
phone screens. One of the phone screens will be non-
technical. At this moment, feel free to ask anything you
want.
Interviews at Amazon
The Amazon interview starts with a technical phone
screen conducted by a team from Amazon. If some
interviewers are not convinced after this phone screen,
then it is possible that they will ask for another one to
clarify the issues.
Interviews at Microso
The Microsoft interview starts with several technical
phone screens or they might require you to travel to one
of their working branches. You will have 4-5 technical
interviews with different teams.
If you did not get any feedback after a week, then you
should trigger a friendly follow-up e-mail to Microsoft
contacts. Sometimes, it takes just a day until they
provide a decision, but it can take a week, a month, or
even more.
Interviews at Facebook
The Facebook interview starts with several technical and
non-technical phone screens involving questions
(technical and non-technical) and coding challenges.
Commonly, the interviews are conducted by a team of
software engineers and hiring managers.
Interviews at Crossover
Crossover is a remote company. They recruit remotely
via their platform and have an exclusive on-site interview
process. Their on-site interview adheres to the following
roadmap:
Summary
In this chapter, we had an overview of how interviews are
conducted in several leading IT companies. Most IT
companies follow the same practices presented in this
chapter, with their own different combinations and
flavors.
Common Non-Technical
Questions and How To
Answer Them
In this chapter, we will tackle the main aspects of the
non-technical interview questions. This part of the
interview is commonly carried out by a hiring manager
or even an HR person. To prepare for this interview
means getting familiar with the following questions:
Here are some tips that will help you with this question
(pay attention to how this question is interleaved with
the previous one – if the working style of this company
relates nicely to the style of your current or ex-company,
then most likely, the same reasons for leaving that job
will apply to avoiding this job as well):
Important note
Accepting or rejecting an
offer
Accepting an offer is quite simple. You need to inform
the company that you accept the offer and discuss details
such as the starting date (especially if you need to work a
notice period at your current workplace), paperwork,
reallocation (if it is the case), and so on.
Declining an offer is a bit more of a delicate situation. It
must be done in a way that allows you to remain in good
relations with everyone. The company has invested time
and resources in the interview, and so you have to
decline their offer politely. You may also, after a while,
consider applying to the company again. For example,
you can say something like I want to thank you for the
offer. I was impressed with your company and I
enjoyed the interview process, but I’ve decided it’s not
the right choice for me right now. Thank you again, and
maybe someday we will meet again.
Failure is an option
In movies, we often hear the expression “failure is not an
option.” But those are just movies! An interview always
ends with an offer or a rejection, and so failure is an
option. It is our task to mitigate failures.
Summary
This chapter provided a brief overview of an important
aspect that we must tackle wisely during a job search—
failures. They are a part of life, and we must know how to
handle them in a healthy and professional way. Don’t get
too emotional, and try to have a professional, cold,
realistic, and objective approach of each failure.
Technical quiz
Coding challenge
Technical quiz
The technical quiz can take on a question-answer format
with the technical interviewer, or it can be an on-site
quiz. Commonly, it contains 20-40 questions and takes
less than an hour.
When the technical interviewer conducts the process,
you will have to provide free answers and the duration
may vary (for example, between 30-45 minutes). It is
important to be crystal clear, concise, and always on
topic.
Important note
Coding challenge
The coding challenge is the climax of any technical
interview. This is the moment where you can show all
your coding skills. It’s time to demonstrate that you can
do this job. Having working and clean code can help you
make a great impression. A great impression may fill in
the gaps that you left open during any other stage of the
interview.
The solution is not obvious: Since they are fairly complex, the
solutions to these problems are not obvious. Don’t expect to find a
solution immediately! Almost nobody does! These questions are
specially designed to see how you handle a situation where you
cannot immediately see the solution. This is why you may have
couple of hours to solve it (most commonly, between 1 and 3
hours).
Important note
Keep talking!
Ask the proper questions: As long as you know and respect the
limits, you can ask questions that can save you time. For example,
it is OK to ask, I can’t remember – what is the default MySQL
port, 3308 or 3306? However, don’t exaggerate with these
questions!
Important note
Avoid running the application after each line of code.
Instead, run the application after each logical block of
code. Make the corrections and run it again. Take
advantage of the IDE debugging tool.
Important note
Now, let’s take a look at the general steps that are meant
to provide a methodological and logical approach to
solving a problem.
Important note
Build an example
Important note
Important note
Important note
Don’t relax! You have won the current battle, but not the
war! Often, the interviewer will want to see your code
working for corner cases or special cases as well. Usually,
such special cases involve dummy values, boundaries
values, improper inputs, actions that force exceptions,
and so on. If your code is not robust and it fails these
attempts, then the interviewer will think that this is
exactly how you’ll code the production applications as
well. On the other hand, if your code works, then the
interviewer will be totally impressed.
Important note
Ask for guidance: This should be your last resort, but in a crisis,
you must apply desperate solutions. You can ask something such
as, I am confused about this aspect because… (and explain; try to
justify your confusion). Can you please give me a tip about what I
am missing here?
Summary
In this chapter, we talked about the process of tackling a
coding challenge problem. Besides the steps we
enumerated earlier – understand the problem, build an
example, decide and explain the algorithm(s), code the
skeleton, and code and test the solution – there is one
more step that will become the objective of the chapters
that follow: practice a lot of problems! In the next
chapter, we will start with the fundamental concepts of
programming.
Section 2: Concepts
This section covers questions regarding concepts.
Providing excellent knowledge in this area is a great
indicator that you have the fundamental skills required,
which means you have a solid and healthy technical
foundation to answer questions at the interview stage.
Companies look for such people as possible candidates
that can be trained to solve very specific and complex
tasks.
Object-Oriented
Programming
This chapter covers the most popular questions and
problems relating to Object-Oriented Programming
(OOP) that are encountered at Java interviews.
OOP concepts
SOLID principles
GOF design patterns
Coding challenges
Technical requirements
You can find all the codes present in this chapter on
GitHub. Please visit the following
link:https://github.com/PacktPublishing/The-
Complete-Coding-Interview-Guide-in-
Java/tree/master/Chapter06
Object
Class
Abstraction
Encapsulation
Inheritance
Polymorphism
Association
Aggregation
Composition
Important note
The correct answers to these questions are a combination
of technical knowledge and real-world analogies or
examples. Avoid cold answers with super-technical
details and no examples (for example, don't talk about
the internal representation of an object). Pay attention to
what you're saying because the interviewer may extract
questions directly from your answers. If your answer has
mentioned a notion in passing, then the next question
may refer to that notion. In other words, don't add to
your answer any aspects that you are unfamiliar with.
What is an object?
The key points that you should encapsulate in your
answer are the following:
What is a class?
The key points that you should encapsulate in your
answer are the following:
What is abstraction?
The key points that you should encapsulate in your
answer are the following:
this.carType = carType;
@Override
@Override
@Override
@Override
@Override
return this.carType;
+ electricCar.getCarType() + "\n");
electricCar.speedUp();
electricCar.turnLeft();
electricCar.slowDown();
What is encapsulation?
The key points that you should encapsulate in your
answer are the following:
System.out.println("Sleep ...");
energy++;
hungry++;
System.out.println("Play ...");
mood++;
energy--;
meow();
System.out.println("Feed ...");
hungry--;
mood++;
meow();
System.out.println("Meow!");
return mood;
}
public int getHungry() {
return hungry;
return energy;
cat.feed();
cat.play();
cat.feed();
cat.sleep();
Mood: 53
Hungry: 49
What is inheritance?
The key points that you should encapsulate in your
answer are the following:
this.name = name;
super(name);
this.team = team;
What is polymorphism?
The key points that you should encapsulate in your
answer are the following:
+ color);
triangle.draw();
triangle.draw("red");
triangle.draw(10, "blue");
@Override
@Override
@Override
triangle.draw();
rectangle.draw();
circle.draw();
Important note
There are people who consider polymorphism as the
most important concept in OOP. Moreover, there are
voices that consider runtime polymorphism as the only
genuine polymorphism, while compile-time
polymorphism is not actually a form of polymorphism.
During an interview, initiating such a debate is not
recommended. It is better to act as a mediator and
present both sides of the coin. We will discuss soon how
to tackle such situations.
What is association?
The key points that you should encapsulate in your
answer are the following:
this.name = name;
this.city = city;
this.zip = zip;
Two aggregated objects have their own life cycle, but one of the
objects is the owner of the HAS-A relationship.
Important note
this.type = type;
this.size = size;
this.weight = weight;
this.name = name;
this.racket = racket;
racket);
What is composition?
The key points that you should encapsulate in your
answer are the following:
this.type = type;
this.horsepower = horsepower;
}
// getters and setters omitted for brevity
this.name = name;
this.engine=engine;
return engine.getHorsepower();
return name;
System.out.println("Horsepower: " +
car.getHorsepower());
Horsepower: 300
What is S?
The key points that you should encapsulate in your
answer are the following:
S stands for One class should have one, and only one,
responsibility.
this.width = width;
this.height = height;
}
this.width = width;
this.height = height;
This class also follows the SRP, hence our job is done.
The complete application is named
SingleResponsabilityPrinciple. Moving on, let's talk
about the second SOLID principle, the Open Closed
Principle.
What is O?
The key points that you should encapsulate in your
answer are the following:
O sustains the fact that our classes should not contain constraints
that will require other developers to modify our classes in order to
accomplish their job – other developers should only extend our
classes to accomplish their job.
this.shapes = shapes;
int sum = 0;
if (shape.getClass().equals(Circle.class)) {
.getRadius(), 2);
} else
if(shape.getClass().equals(Rectangle.class)) {
* ((Rectangle) shape).getWidth();
return sum;
}
}
Since each shape has its own formula for area, we require
an if-else (or switch) structure to determine the type of
shape. Furthermore, if we want to add a new shape (for
example, a triangle), we have to modify the
AreaCalculator class to add a new if case. This means
that the preceding code breaks the OCP. Fixing this code
to observe the OCP imposes several modifications in all
classes. Hence, be aware that fixing code that doesn't
follow the OCP can be quite tricky, even in the case of a
simple example.
this.width = width;
this.height = height;
this.radius = radius;
@Override
this.shapes = shapes;
}
int sum = 0;
sum += shape.area();
return sum;
What is L?
The key points that you should encapsulate in your
answer are the following:
super(name);
@Override
@Override
tournament");
}
The VipMember class is roughly the same as
PremiumMember, so we can skip it and focus on the
FreeMember class. The FreeMember class can join
tournaments, but cannot organize tournaments. This is
an issue that we need to tackle in the
organizeTournament() method. We can throw an
exception with a meaningful message or we can display a
message as follows:
super(name);
@Override
...");
@Override
tournaments");
}
);
member.organizeTournament();
this.name = name;
this.name = name;
@Override
);
member.joinTournament();
);
member.organizeTournament();
What is I?
The key points that you should encapsulate in your
answer are the following:
I stands for the Interface Segregation Principle (ISP).
I splits an interface into two or more interfaces until clients are not
forced to implement methods that they will not use.
this.www = www;
@Override
+ www);
}
@Override
@Override
WwwPingConnection www
www.connect();
this.www = www;
}
@Override
+ www);
@Override
What is D?
The key points that you should encapsulate in your
answer are the following:
this.dbName = dbName;
}
public String get() {
System.out.println("Connecting to "
+ postgresql.get());
this.dbName = dbName;
@Override
System.out.println("Connecting to " +
jdbcUrl.get());
If further details are required, then you can list the main
rules that govern method overriding:
Important note
...
@Override
throws CloneNotSupportedException {
return clone;
Important note
Important note
Important note
@Override
}
@Override
@Override
@Override
@Override
return Math.random()*60d /
Math.pow(Math.random(), 3);
}
}
Important note
System.out.println("Moving a vehicle");
Now, let's call these two static methods from the main()
method:
Moving a vehicle
Moving a car
Important note
Important note
Whenever you can cite or mention famous references, do
so.
Coding challenges
Next, we will tackle several coding challenges regarding
object-oriented programming. For each problem, we will
follow Figure 5.2 from Chapter 5, How to Approach a
Coding Challenge. Mainly, we will start by asking the
interviewer a question such as What are the design
constraints? Commonly, coding challenges that orbit
OOD are expressed by the interviewer in a general way.
This is done intentionally to make you ask details about
design constraints.
Once we have a clear picture of the constraints, we can
try an example (which can be a sketch, a step-by-step
runtime visualization, a bullet list, and suchlike). Then,
we figure out the algorithm(s)/solution(s), and finally,
we provide the design skeleton.
Example 1: Jukebox
Amazon, Google
this.cdPlayer = cdPlayer;
@Override
private CD cd;
this.playlist = playlist;
this.cds = cds;
this.songs = songs;
this.song = song;
this.songs = songs;
The User, CD, and Song classes are skipped for now,
but you can find them all in the complete application
named Jukebox. This kind of problem can be
implemented in a wide variety of ways, so feel free to try
your own designs as well.
...
...
}
The vending machine needs an internal inventory to
track the items and status of the coins. We can shape this
generically as follows:
}
Finally, the vending machine can be shaped to
implement the Selector interface and provide a bunch
of private methods used to accomplish the internal tasks:
= new Inventory<>();
= new Inventory<>();
public VendingMachine() {
initMachine();
System.out.println("Initializing the
this.suit = suit;
this.value = value;
super(suit, value);
this.cards = cards;
public StandardPack() {
super.setCards(build());
@Override
return cards;
this.cards = pack.getCards();
@Override
this.licensePlate = licensePlate;
this.spotsNeeded = spotsNeeded;
this.type = type;
this.name = name;
}
this.name = name;
this.floors = floors;
this.name = name;
this.totalSpots = totalSpots;
String label) {
this.parkingFloor = parkingFloor;
this.label = label;
this.id = id;
this.parkingLot = parkingLot;
@Override
@Override
@Override
books.putIfAbsent(book.getIsbn(), book);
return books.get(isbn);
this.reader = reader;
refreshReader();
this.book = book;
refreshBook();
page = book.fetchPage(++pageNumber);
refreshPage();
page = book.fetchPage(--pageNumber);
refreshPage();
public OnlineReaderSystem() {
this.reader = reader;
displayer.displayReader(reader);
this.reader = readerManager.find(email);
if (this.reader != null) {
displayer.displayReader(reader);
displayer.displayBook(book);
this.book = library.find(isbn);
if (this.book != null) {
displayer.displayBook(book);
displayer.nextPage();
displayer.previousPage();
library.addBook(book);
if (!book.equals(this.book)) {
return library.remove(book);
}
return false;
readerManager.addReader(reader);
if (!reader.equals(this.reader)) {
return readerManager.remove(reader);
return false;
return reader;
return book;
K key;
V value;
HashEntry(K k, V v) {
this.key = k;
this.value = v;
this.next = null;
...
= new HashEntry[SIZE];
...
if (entries[hash] == null) {
entries[hash] = hashEntry;
currentEntry = currentEntry.next;
currentEntry.next = hashEntry;
if (entries[hash] != null) {
if (currentEntry.key.equals(key)) {
currentEntry = currentEntry.next;
return null;
}
Finally, we add a dummy hash function (in reality, we
use hash functions such as Murmur 3 –
https://en.wikipedia.org/wiki/MurmurHash):
So far so good! I suggest you try your own designs for the
preceding 10 problems as well. Do not consider that the
solutions presented are the only ones that are correct.
Practice as much as you can by varying the context of the
problem and challenge yourself with other problems as
well.
Summary
This chapter covered the most popular questions about
OOP fundamentals and 10 design coding challenges that
are very popular in interviews. In the first part, we began
with OOP concepts (object, class, abstraction,
encapsulation, inheritance, polymorphism, association,
aggregation, and composition), continued with the
SOLID principles, and finished with an amalgam of
questions combining OOP Concepts, SOLID principles,
and design pattern knowledge. In the second part, we
tackled 10 carefully crafted design coding challenges,
including designing a jukebox, a vending machine, and
the famous hash table.
Analogy
Big O examples
Analogy
Imagine a scenario where you've found one of your
favorite movies on the internet. You can order it or
download it. Since you want to see it as soon as possible,
which is the best way to proceed? If you order it, then it
will take a day to arrive. If you download it, then it will
take half a day to download. So, it is faster to download
it. That's the way to go!
But wait! Just when you get ready to download it, you
spot the Lord of the Rings Master Collection at a great
price, and so you think about downloading it as well.
Only this time, the download will take 2 days. However,
if you place an order, then it will still only take a single
day. So, placing an order is faster!
As you can see, not all O times perform the same. O(n!),
O(2n), and O(n2) are considered horrible and we
should strive to write algorithms that perform outside
this area. O(n log n) is better than O(n!) but is still bad.
O(n) is considered fair, while O(log n) and O(1) are
good.
Big O examples
We will try to determine Big O for different snippets of
code exactly as you will see at interviews, and we will go
through several relevant lessons that need to be learned.
In other words, let's adopt a learning-by-example
approach.
Drop constants
Example 1 – O(1)
Consider the following three snippets of code and
compute Big O for each of them:
// snippet 1
return 23;
System.out.println(a[i]);
}
In order to determine the Big O value for this snippet of
code, we have to answer the following question: how
many times does this for loop iterate? The answer is
a.length times. We cannot say exactly how much time
this means, but we can say that the time will grow
linearly with the size of the given array (which represents
the input). So, this snippet of code will have an
O(a.length) time and is known as linear time. It is
denoted as O(n).
System.out.println("Current element:");
System.out.println(a[i]);
System.out.println(a[i] + 1);
Important note
Keep in mind that Big O doesn't depend on the number
of code lines. It depends on the runtime rate of increase,
which is not modified by constant-time operations.
The first code snippet uses a single loop, but it has two if
statements, while the second code snippet uses two
loops, but it has one if statement per loop.
Important note
Important note
Important note
In the first snippet, we have two for loops that loop the
same array, a (we have the same input for both loops),
and so Big O can be expressed as O(n), where n refers to
a. In the second code snippet, we also have two for
loops, but they loop different arrays (we have two inputs,
a and b). This time, Big O is not O(n)! What does n refer
to – a or b? Let's say that n refers to a. If we increase the
size of b, then O(n) doesn't reflect the runtime rate of
increase. Therefore, Big O is the sum of these two
runtimes (the runtime of a plus the runtime of b). This
means that Big O must refer to both runtimes. For this,
we can use two variables that refer to a and to b. So, Big
O is expressed as O(a + b). This time, if we increase the
size of a and/or b, then O(a + b) captures the runtime
rate increase.
Important note
compare 17 to 17
return
Now, let's express Big O for this pseudo-code. We can
observe that the algorithm consists of a continuous half-
life of the array until only one element remains. So, the
total runtime is dependent on how many steps we need
in order to find a certain number in the array.
Important note
int fibonacci(int k) {
if (k <= 1) {
return k;
printInOrder(node.left);
printInOrder(node.right);
void printFibonacci(int k) {
int fibonacci(int k) {
if (k <= 1) {
return k;
Example 11 – memoization
What is Big O for the following snippet of code?
void printFibonacci(int k) {
}
}
if (k <= 1) {
return k;
return cache[k];
+ fibonacci(k - 1, cache);
return cache[k];
Calling fibonacci(0):
Result of fibonacci(0) is 0
Calling fibonacci(1):
Result of fibonacci(1) is 1
Calling fibonacci(2):
fibonacci(0)
fibonacci(1)
Result of fibonacci(2) is 1
Calling fibonacci(3):
fibonacci(1)
Result of fibonacci(3) is 2
Calling fibonacci(4):
Result of fibonacci(4) is 3
Calling fibonacci(5):
Result of fibonacci(5) is 5
Calling fibonacci(6):
fibonacci(4) is fetched from cache[4] as: 3
Result of fibonacci(6) is 8
since we
eliminate constants.
For the first snippet, the inner loop doesn't work and it is run n
times by the outer loop, and so n*n = n2, results in O(n2).
For the second snippet, the inner loop does roughly n/2 work and
it is run n times by the outer loop, so n*n/2 = n2/2 = n2 * 1/2,
which results in (after removing the constants) O(n2).
System.out.println(a[i] + a[j]);
// O(1)
System.out.println(a[i]);
}
O(n + p)
O(n + log n)
Important note
So, Big O for the inner loop is O(log n). To compute the
total Big O, we consider that the outer loop is executed n
times, and within that loop, another loop is executed log
n times. So, the total Big O result is O(n)* O (log n) = O(n
log n).
String[] sortArrayOfString(String[] a) {
return a;
Now, let's focus on the for loop and see the wrong
answer that is commonly given by candidates. We
already know that sorting a single string gives us O(n log
n). Doing this for each string means O(n) * (n log n) =
O(n*n log n) = O(n2 log n). Next, we sort the array itself,
which is also given as O(n log n). Putting all of the results
together, the total Big O value is O(n2 log n) + O(n log n)
= O(n2 log n + n log n), which is O(n2 log n) since n log
n is a non-dominant term. However, is this correct? The
short answer is no! But why not?! There are two major
mistakes that we've done: we've used n to represent two
things (the size of the array and the length of the string)
and we assumed that comparing String requires a
constant time as is the case for fixed-width integers.
if (num >= 1) {
} else {
return 1;
int count = 0;
int sum = y;
sum += y;
count++;
return count;
int sqrt(int n) {
if (guess * guess == n) {
return guess;
return -1;
Example 22 – digits
The following snippet of code sum up the digits of an
integer. What is Big O?
int sumDigits(int n) {
int result = 0;
while (n > 0) {
result += n % 10;
n /= 10;
return result;
mergesort(y);
for (int i : x) {
if (binarySearch(y, i) >= 0) {
return true;
return false;
Summary
In this chapter, we covered one of the most predominant
topics in an interview, Big O. Sometimes, you'll have to
determine Big O for a given code, while other times,
you'll have to determine it for your own code. In other
words, there is little chance of bypassing Big O in an
interview. No matter how hard you train, Big O always
remains a hard topic that can put even the best
developers in trouble. Fortunately, the cases covered
here are the most popular in interviews and they
represent perfect templates for a lot of derived problems.
Recursion in a nutshell
Coding challenges
Technical requirements
You will find all the code presented in this chapter on
GitHub at https://github.com/PacktPublishing/The-
Complete-Coding-Interview-Guide-in-
Java/tree/master/Chapter08.
Recursion in a nutshell
A method that calls itself directly/indirectly is called
recursion. This method is known as a recursive method.
The famous Fibonacci numbers problem can be
implemented recursively, as follows:
int fibonacci(int k) {
// base case
if (k <= 1) {
return k;
// recursive call
Important Note
Dynamic Programming in a
nutshell
When we talk about optimizing recursion, we talk about
Dynamic Programming. This means that solving
recursive problems can be done using plain recursive
algorithms or Dynamic Programming.
if (k <= 1) {
return k;
int fibonacci(int k) {
if (k <= 1) {
return k;
} else if (cache[k] > 0) {
return cache[k];
+ fibonacci(k - 1, cache);
return cache[k];
Important note
Important note
if (k <= 1) {
return k;
int first = 1;
int second = 0;
int result = 0;
second = first;
first = result;
return result;
Here, we can see that the robot can go from one cell (m,
n) to an adjacent cell, which can be (m-1, n) or (m, n-1).
For example, if the robot is placed at (5, 5), then it can go
to (4, 5) or (5, 4). Furthermore, from (4, 5), it can go to
(3, 5) or (4, 4), while from (5, 4), it can go to (5, 3) or (4,
4).
if (m < 0 || n < 0) {
return false;
if (maze[m][n]) {
return false;
return true;
return false;
}
A move means to slide the upper disk from one rod to another rod.
For n=3: Let's get some help from the following diagram:
Move the top n - 1 disks from the origin to the intermediate, using
the target as an intermediate.
Move the top n - 1 disks from the intermediate to the target, using
the origin as an intermediate.
if (n <= 0) {
return;
if (n == 1) {
return;
Let's consider n=15 and k=3. So, there are 15 men and
every third man will be eliminated from the circle until
only one remains. Let's visualize this via the following
diagram (this is very useful for figuring out the pattern of
killings):
Figure 8.6 – Josephus for n=15 and k=3
if (n == 1) {
return 1;
} else {
return (josephus(n - 1, k) + k - 1) % n + 1;
If you find this approach quite tricky, then you can try an
iterative approach based on a queue. First, fill up the
queue with n men. Next, loop the queue and, for each
man, retrieve and remove the head of this queue
(poll()). If the retrieved man is not the kth, then insert
this man back in the queue (add()). If this is the kth
man, then break the loop and repeat this process until
the queue's size is 1. The code for this is as follows:
circle.add(i);
while (circle.size() != 1) {
if (i == k) {
System.out.println("Eliminated: "
+ eliminated);
break;
}
circle.add(eliminated);
+ circle.peek());
...
a[i][j] = -a[i][j];
currentColorSpot++;
computeColorSpot(i - 1, j, cols,
rows, a, color);
computeColorSpot(i, j - 1, cols,
rows, a, color);
computeColorSpot(i, j + 1, cols,
rows, a, color);
int biggestColorSpot = 0;
int color = 0;
if (a[i][j] > 0) {
currentColorSpot = 0;
computeColorSpot(i, j, cols,
rows, a, a[i][j]);
biggestColorSpot = currentColorSpot;
}
}
if (cache[amount][position] > 0) {
return cache[amount][position];
return 1;
int count = 0;
count += calculateChangeMemoization(remaining,
cache[amount][position] = count;
return count;
Let's focus on the solution and start from the first row:
row 0. We can build a tower on this row in any column;
therefore, we can say the following:
Figure 8.9(b): Part 1 of the logic to build the towers
So, we start from the first row and build the first tower
on (0,0). We go to the second row and try to build the
second tower so that we don't share the column or
diagonal with the first tower. We go to the third row and
try to build the third tower so that we don't share the
column or diagonal with the first two towers. We follow
the same logic for the fourth and fifth towers. This is our
solution. Now, we repeat this logic – we build the first
tower at (0,1) and continue building until we find the
second solution. Next, we build the first tower at (0, 2),
(0, 3) and finally at (0,4) while we repeat the process. We
can write this recursive algorithm as follows:
Set<Integer[]> solutions) {
if (row == GRID_SIZE) {
solutions.add(columns.clone());
} else {
columns[row] = col;
currentRow++) {
if (currentColumn == nextColumn) {
return false;
}
int columnsDistance
= Math.abs(currentColumn - nextColumn);
if (columnsDistance == rowsDistance) {
return false;
return true;
Adobe, Microsoft
if (value == middleIndex) {
return middleIndex;
Math.min(middleIndex - 1, value));
if (leftIndex >= 0) {
return leftIndex;
value), endIndex);
System.out.format("%2s", elevations[i][j]);
System.out.println();
}
Now, let's sketch a 5x5 grid and view an input and its
output. The following image shows the initial grid in the
form of a 3D model, along with a possible path and the
solved grid:
prevElevation = currentElevation;
elevations[i][j] = 0;
computePath(prevElevation,i,j-1,
rows,cols,elevations);
computePath(prevElevation,i-1,
j,rows,cols,elevations);
computePath(prevElevation,i,j+1,
rows,cols,elevations);
computePath(prevElevation,i+1,j,
rows,cols,elevations);
Each box is strictly larger than the box above it in terms of their
width and height.
// Memoization
@Override
return Integer.compare(b2.getWidth(),
b1.getWidth());
});
// place each box as the base (bottom box) and
int highest = 0;
return highest;
// Memoization
return cache[base];
int highest = 0;
cache[base] = highest;
return highest;
P(c1) = c1
int n = str.length();
if (n == 0) {
permutations.add(prefix);
} else {
permutations.addAll(permute(prefix +
str.charAt(i),
return permutations;
}
This code will work fine. Because we use a Set (not a
List), we respect the requirement stating that the
returned list of permutations should not contain
duplicates. However, we do generate duplicates. For
example, if the given string is aaa, then we generate six
identical permutations, even if there is only one. The
only difference is that they are not added to the result
since a Set doesn't accept duplicates. This is far from
being efficient.
String str) {
characters.compute(c, count);
return characters;
P(a=3,b=2,c=2) = [a + P(a=2,b=2,c=2)] + [b +
P(a=3,b=1,c=1)] + [c + P(a=3,b=2,c=1)]
P(a=4,b=1,c=1) = [a + P(a=3,b=1,c=1)] + [b +
P(a=4,b=0,c=1)] + [c + P(a=4,b=1,c=0)]
P(a=4,b=2,c=1) = [a + P(a=3,b=2,c=1)] + [b +
P(a=4,b=1,c=1)] + [c + P(a=4,b=2,c=0)]
P(a=2,b=2,c=2) = [a + P(a=1,b=2,c=2)] + [b +
P(a=2,b=1,c=2)] + [c + P(a=2,b=2,c=1)]
P(a=3,b=1,c=1) = ...
if (strlength == 0) {
permutations.add(prefix);
} else {
if (count > 0) {
permutations.addAll(permute(prefix + c,
strlength - 1, characters));
characters.put(c, count);
return permutations;
We start from a corner of the chessboard: This way, the knight can
initially go in only two directions instead of eight.
We compute the circular path using two arrays: We can move from
(r, c) to (r + ROW[i],c + COL[i]) with i in [0, 7]:
COL[] = {1,2,2,1,-1,-2,-2,-1,1};
ROW[] = {2,1,-1,-2,-2,-1,1,2,2};
= {1,2,2,1,-1,-2,-2,-1,1};
= {2,1,-1,-2,-2,-1,1,2,2};
visited[r][c] = cell;
// we have a solution
if (cell >= n * n) {
print(visited);
visited[r][c] = 0;
return;
if (isValid(newR, newC)
&& visited[newR][newC] == 0) {
visited[r][c] = 0;
...
}
{{{}}},{{}{}},{{}}{},{}{{}},{}{}{}
return results;
return;
results.add(String.valueOf(str));
} else {
str[index] = '{';
results);
str[index] = '}';
results);
}
}
if (n == 0) {
return 1;
} else if (n < 0) {
return 0;
return cache[n];
RECURSIVE APPROACH
Let's try to find a solution via recursion. If we add the
subset arr[0]=3, then we have to find the subset for s =
s-arr[0] = 7-3 = 4. Finding a subset for s=4 is a sub-
problem that can be solved based on the same logic,
which means we can add arr[1]=2 in the subset, and the
next sub-problem will consist of finding the subset for s
= s-arr[1] = 4-2 = 2.
/* Recursive approach */
if (currentSum == givenSum) {
if (subset[i] == 1) {
subset[index] = 1;
currentSum += arr[index];
findSumRecursive(arr, index + 1,
currentSum -= arr[index];
subset[index] = 0;
findSumRecursive(arr, index + 1,
...
...
1. While the element of the current row (i) is greater than the value
of the current column (j), we just copy the preceding value (i-1, j),
in the current (i, j) cell.
2. If the element of the current row (i) is smaller than or equal to the
value of the current column (j), then we look to the (i-1, j) cell and
do the following:
b. If cell (i-1, j) is F, then we fill up the (i, j) cell with the value at (i-
1, j-element_at_this_row).
1. Start from the right-bottom cell, which is T (let's say that this cell
is at (i, j)).
a. If the cell above this one, (i-1, j), is F, then write down the
element at this row (this element is part of the subset) and go to
cell (i-1, j-element_at_this_row).
b. While the cell above this one, (i-1, j), is T, we go up the cell (i-1,
j).
c. Repeat this from step 1a until the entire subset is written down.
Next, we apply step 1b, so we land in cell (3, 7). The cell
above (3, 7) has the value F, so we apply step 1a. First, we
write down the element at row 3, which is 6. Then, we go
to cell (3-1, 7-6) = (2, 1). So far, the subset is {2, 6}.
The cell above (2, 1) has the value F, so we apply step 1a.
First, we write down the element at row 2, which is 1.
Then, we go to cell (2-1, 1-1) = (1, 0). Above cell (1,0), we
have only T, so we stop. The current and final subset is
{2, 6, 1}. Obviously, 2+6+1 = 9.
boolean[][] matrix
matrix[0][i] = false;
matrix[i][0] = true;
matrix[i][j] = matrix[i][j]
return matrix[arr.length][givenSum];
if (index == str.length()) {
return true;
canBreak = canBreak
|| dictionary.contains(str.substring(index, i + 1))
return canBreak;
table[0] = true;
if (dictionary.contains(str.substring(i, j))) {
table[j] = true;
return table[str.length()];
Trie-based solution
// characters 'a'-'z'
public Trie() {
}
// Trie node
private Node() {
this.leaf = false;
};
node.leaf = true;
table[0] = true;
if (table[i]) {
if (node == null) {
break;
table[j + 1] = true;
}
}
return table[str.length()];
th is is a f a m o u s problem
th is is a famous problem
this is a f a m o u s problem
Summary
In this chapter, we covered one of the most popular
topics in interviews: recursion and Dynamic
Programming. Mastering this topic requires a lot of
practice. Fortunately, this chapter provided a
comprehensive set of problems that covered the most
common recursive patterns. From permutations to grid-
based problems, from classical problems such as Tower
of Hanoi to tricky problems such as generating curly
braces, this chapter has covered a wide range of recursive
cases.
Bit Manipulation
This chapter covers the most important aspects of bit
manipulation that you should know about when it forms
part of a technical interview. Such problems are often
encountered in interviews and they are not easy. The
human mind was not designed to manipulate bits;
computers were designed for that. This means that
manipulating bits is quite hard and extremely prone to
mistakes. Hence, it is advisable to always double-check
every bit operation.
You must understand the theory of bits very well (for example, bit
operators)
Coding challenges
Technical requirements
All the code present in this chapter can be found on
GitHub at https://github.com/PacktPublishing/The-
Complete-Coding-Interview-Guide-in-
Java/tree/master/Chapter09.
Bit manipulation in a
nutshell
In Java, we can manipulate bits of the following data
types: byte (8-bit), short (16-bit), int (32-bit), long
(64-bit), and char (16-bit).
All the positions to the left of 110011 are actually filled with zeros,
up to 32 bits in total.
// 110011
System.out.println("Binary: " +
Integer.toBinaryString(51));
System.out.println("Decimal: "
Bitwise operators
Manipulating bits involves several operators. These
operators are as follows:
Important note
Important note
Important note
Coding challenges
In the next 25 coding challenges, we will exploit different
aspects of bit manipulations. Since these kinds of
problems are really brain-teasing, they are preferred in
interviews. Understanding a snippet of code that
manipulates bits is not an easy task, so take your time
and dissect each problem and snippet of code. This is the
only way to obtain some patterns and templates in order
to solve these kinds of problems.
The following figure contains a set of four bit-mask that
are important to have in your toolbelt:
if (result == 0) {
return '0';
return '1';
How about clearing the bits between the given k and the
LSB? Let me show you the code:
Again, use a paper and a pen and take it step by step. The
complete application is called ClearBits.
1. Sum all the bits of the current column (the first column is the
column of LSB).
2. Convert the result into binary (for example, via successive
divisions by 2).
3. Keep the rightmost bit as the result.
4. Carry the remains bits into the remaining columns (one bit per
column).
5. Go to the next column and repeat from step 1.
Now, let's see this step by step (the bold sections are
carried):
int and;
int t;
and = q & p;
xor = q ^ p;
while (and != 0) {
t = xor ^ and;
xor = t;
return xor;
1. Multiply every bit of the second binary number by every bit of the
first binary number, starting from the rightmost column (column
0).
2. Sum up the results.
So, 5 * 1 = 5.
So far, this is not such a big deal, but let's continue with 5
* 2; that is, with 101 * 10. If we think that 5 * 2 = 5 * 0 +
10 * 1, then this means that 101 * 10 = 101 * 0 + 1010 * 1.
So, we left shifted 5 by one position and we right shifted
2 by one position.
int result = 0;
while (p != 0) {
if ((p & 1) != 0) {
result = result + q;
return result;
while (p != 0) {
q = q ^ p;
p = borrow << 1;
return q;
Do subtraction, 10 - 10 = 0.
Do subtraction, 011 - 10 = 1.
There are no more bits to process from the dividend, so we can say
that 11/2 has the quotient 101 (which is 5) and that the remainder
is 1.
...
q = Math.abs(q);
p = Math.abs(p);
long t = 0;
long quotient = 0;
if (halfdown <= q) {
t = t + p << i;
If the next bit is 1, then check whether the previous bit was 1
if (~n == 0) {
return Integer.SIZE; // 32
int currentSequence = 0;
int longestSequence = 0;
boolean flag = true;
while (n != 0) {
if ((n & 1) == 1) {
currentSequence++;
flag = false;
? 0 : flag
? 0 : ++currentSequence;
flag = true;
longestSequence = Math.max(
currentSequence, longestSequence);
n >>>= 1;
return longestSequence;
int copyn = n;
int zeros = 0;
while ((copyn != 0) && ((copyn & 1) == 0)) {
zeros++;
int ones=0;
ones++;
n = n | (1 << marker);
int copyn = n;
int zeros = 0;
int ones = 0;
// count trailing 0s
zeros++;
ones++;
return -1;
n = n | (1 << marker);
return n;
int count = 0;
int xor = q ^ p;
while (xor != 0) {
return count;
a = 1, 1 * 4 = 4
a = 2, 2 * 4 = 8
a = 3, 3 * 4 = 12
a = 4, 4 * 4 = 16
1. We take the odd bits and shift them to the right by one position.
2. We take the even bits and shift them to the left by one position.
We can take the odd bits via the AND[&] operator and a
bit-mask that contains bits of 1 in the odd positions:
10101010101010101010101010101010. Let's see this in
action:
Figure 9.35 – Swapping odd and even bits (1)
We can take the even bits via the AND[&] operator and a
bit-mask that contains bits of 1 in the even positions:
1010101010101010101010101010101. Let's see this in
action:
int moveToOddPositions
For the right rotation, the code will look as follows (you
should be able to follow this solution with no issues):
int n = arr.length;
int result = 0;
int nr;
int sumBits;
sumBits = 0;
nr = (1 << i);
sumBits++;
}
// the sum not multiple of 3 are the
if ((sumBits % 3) == 0) {
return result;
int oneAppearance = 0;
int twoAppearances = 0;
twoAppearances = twoAppearances
return oneAppearance;
int nr = arr[i];
if (bitArr.get(nr)) {
} else {
bitArr.set(nr);
}
The complete application is called FindDuplicates.
So, if we take any set bit (for example, the rightmost bit)
of the result of XOR[^] and divide the elements of the
array into two sets, then one set will contain elements
with the same bit set and the other set will contain
elements with the same bit not set. In other words, we
divide the elements into two sets by comparing the
rightmost set bit of XOR[^] with the bit at the same
position in each element. By doing so, we will get p in
one set and q in the other set.
Done!
xor ^= arr[i];
// get the rightmost set bit (you can use any other set
bit)
int p = 0;
int q = 0;
p = p ^ arr[i];
} else {
q = q ^ arr[i];
}
System.out.println("The numbers are: " + p + " and " +
q);
20 = 000 = {}
21 = 001 = {a}
22 = 010 = {b}
23 = 011 = {a, b}
24 = 100 = {c}
25 = 101 = {a, c}
26 = 110 = {b, c}
27 = 111 = {a, b, c}
subset.add(set[j]);
subsets.add(subset);
return subsets;
int count = 0;
if (!isPowerOfTwo(n)) {
return -1;
while (n != 0) {
n = n >> 1;
++count;
return count;
Summary
Since this chapter is a comprehensive resource for bit
manipulation, then if you got this far, you've seriously
boosted your bit manipulation skills. We covered the
main theoretical aspects and solved 25 coding challenges
in order to help you learn patterns and templates for
solving bit manipulation problems.
Coding challenges
Technical requirements
All the code present in this chapter can be found on
GitHub at https://github.com/PacktPublishing/The-
Complete-Coding-Interview-Guide-in-
Java/tree/master/Chapter10.
Coding challenges
In the following 29 coding challenges, we'll tackle a set of
popular problems encountered in Java technical
interviews done by medium to large companies
(including Google, Amazon, Flipkart, Adobe, and
Microsoft). Besides these 29 coding challenges
(discussed in this book), you may like to check out the
following non-exhaustive list of strings and arrays coding
challenges that you can find in my other book, Java
Coding Problems
(https://www.amazon.com/gp/product/1789801419/),
published by Packt as well:
Applying indentation
Transforming strings
Sorting an array
Reversing an array
...
char ch = str.charAt(i);
if (!Character.isWhitespace(ch)) {
return false;
} else {
System.out.println("The given string
return false;
return true;
Next, we peek at the first letter from the given string and
we compute the subtraction between its ASCII code and
97 (the ASCII code of a). Let's denote this with s. Now,
we create another bit mask by left shifting 1 by s
positions. This will result in a bit mask that has the MSB
of 1 followed by s bits of 0 (1000...). Next, we can apply
the AND[&] operator between the bit mask of unique
characters (which is initially 0000...) and this bit mask
(1000...). The result will be 0000... since 0 & 1 = 0. This
is the expected result since this is the first processed
letter, so there are no letters being flipped in the bit mask
of unique characters.
...
int marker = 0;
return false;
return true;
int countWhitespaces = 0;
if (Character.isWhitespace(str[i])) {
countWhitespaces++;
if (countWhitespaces > 0) {
+ countWhitespaces * 2];
int index = 0;
if (Character.isWhitespace(str[i])) {
encodedStr[index] = '0';
encodedStr[index + 1] = '2';
encodedStr[index + 2] = '%';
index = index + 3;
} else {
encodedStr[index] = str[i];
index++;
return encodedStr;
return str;
return false;
return false;
int is = 0;
int il = 0;
if (shorter.charAt(is) != longer.charAt(il)) {
if (marker) {
return false;
marker = true;
if (shorter.length() == longer.length()) {
is++;
}
} else {
is++;
il++;
return true;
If the current character and the next character are the same, then
we increment a counter.
In the end, after processing all the characters from the given
string, we compare the length of the result with the length of the
given string and we return the shortest string.
In terms of code, we have the following:
int count = 0;
count++;
if (!Character.isWhitespace(str.charAt(i))) {
result.append(str.charAt(i))
.append(count);
count = 0;
}
} else {
result.append(str.charAt(i));
count = 0;
? str : result.toString();
String.valueOf(Integer.MAX_VALUE).length());
char ch = str.charAt(i);
if (Character.isDigit(ch)) {
temp.append(ch);
} else {
if (temp.length() > 0) {
result.add(Integer.parseInt(temp.toString()));
temp.delete(0, temp.length());
return result;
int cp = str.codePointAt(i);
if (i < str.length()-1
result.add(cp);
result.add(str.codePointAt(i+1));
i++;
return result;
int cp = str.codePointAt(i);
// the constant 2 means a suroggate pair
if (Character.charCount(cp) == 2) {
result.add(cp);
result.add(str.codePointAt(i+1));
i++;
return result;
+ Pattern.quote(str2) + ".*");
m[j][i] = m[i][j];
m[i][j] = temp;
transpose(m);
m[j][i] = m[k][i];
m[k][i] = temp;
return true;
// rotate counterclockwise
return true;
Tip
With a little trick and some work, we can keep the space
complexity set to O(1). The trick consists of using the
first row and column of the matrix to mark the zeros
found in the rest of the matrix. For example, if we find a
zero at cell (i, j) with i≠0 and j≠0, then we set M[i][0] =
0 and M[0][j] = 0. Once we've done that for the entire
matrix, we can loop the first column (column 0) and
propagate each zero that's found on the row. After that,
we can loop the first row (row 0) and propagate each
zero that's found on the column.
But how about the potential initial zeros of the first row
and column? Of course, we have to tackle this aspect as
well, so we start by flagging whether the first
row/column contains at least one 0:
if (m[0][j] == 0) {
firstRowHasZeros = true;
break;
if (m[i][0] == 0) {
firstColumnHasZeros = true;
break;
if (m[i][j] == 0) {
m[i][0] = 0;
m[0][j] = 0;
if (m[i][0] == 0) {
setRowOfZero(m, i);
}
}
if (m[0][j] == 0) {
setColumnOfZero(m, j);
if (firstRowHasZeros) {
setRowOfZero(m, 0);
if (firstColumnHasZeros) {
setColumnOfZero(m, 0);
m[r][j] = 0;
m[i][c] = 0;
int value;
int backLink;
this.value = value;
this.backLink = backLink;
initializeSlots();
...
...
throws OverflowException {
node.value = value;
node.backLink = top;
backLinks[stack] = free;
throws OverflowException {
nextFreeSlot = theArray[free].backLink;
size++;
return free;
...
throws UnderflowException {
if (top == -1) {
backLinks[stack] = node.backLink;
freeSlot(top);
return node;
theArray[index].backLink = nextFreeSlot;
nextFreeSlot = index;
size--;
The first zone is assigned to the first stack and lies at the left-hand
side of the array endpoint (while we push into this stack, it grows
in the right direction).
The second zone is assigned to the second stack and lies at the
right-hand side of the array endpoint (while we push into this
stack, it grows in the left direction).
The third zone is assigned to the third stack and lies in the middle
of the array (while we push into this stack, it may grow in any
direction).
l = 10, r = 10 → STOP
return Collections.emptyList();
java.util.Arrays.sort(m);
int l = 0;
int r = m.length - 1;
while (l < r) {
if (sum == k) {
l++;
r--;
l++;
r--;
return result;
a: {1, 2, 32, 46} b: {-4, 5, 15, 18, 20} c: {3} d: {6, 8} e: {-2,
-1, 0}
b. Replace the Binary Min Heap's root with the next element from
the array that the element was extracted from (if the array doesn't
have any more elements, then replace the root element with
infinite; for example, with Integer.MAX_VALUE).
int data;
int heapIndex;
int currentIndex;
int currentIndex) {
this.data = data;
this.heapIndex = heapIndex;
this.currentIndex = currentIndex;
int len = 0;
for (int i = 0; i < arrs.length; i++) {
len += arrs[i].length;
// perform merging
heapify(heap, 0, k);
result[i] = heap[0].data;
heap[0].currentIndex++;
heap[0].data = Integer.MAX_VALUE;
} else {
heap[0].data = subarray[heap[0].currentIndex];
return result;
Now, let's consider that q={ 2, 6, 9, 10, 11, 65, 67} and p=
{ 1, 5, 17, 18, 25, 28, 39, 77, 88}, and let's apply the
previous steps.
Here, you can see that the new qPointer and new
pPointer respect step 1 of our algorithm since
q[qPointer-1], which is 11, is less than p[pPointer], which
is, 18; and p[pPointer-1], which is 17, is less than
q[qPointer], which is 65. With this, we found the perfect
qPointer to be 5.
swap(q, p);
int qPointerMin = 0;
int qPointer;
int pPointer;
qPointerMin = qPointer + 1;
qPointerMax = qPointer - 1;
int maxLeft = 0;
}
// if the length of 'q' + 'p' arrays is odd,
if ((q.length + p.length) % 2 == 1) {
return maxLeft;
int minRight = 0;
minRight = p[pPointer];
minRight = q[qPointer];
return -1;
If the given matrix contains only one row, then cells with 1's in
them will be the maximum size of the square sub-matrix.
Therefore, the maximum size is 1.
If the given matrix contains only one column, then cells with 1's in
them will be the maximum size of the square sub-matrix.
Therefore, the maximum size is 1.
int maxSubMatrixSize = 1;
subMatrix[0][i] = matrix[0][i];
subMatrix[i][0] = matrix[i][0];
if (matrix[i][j] == 1) {
Math.min(subMatrix[i][j - 1],
subMatrix[i - 1][j])) + 1;
maxSubMatrixSize = Math.max(
maxSubMatrixSize, subMatrix[i][j]);
}
}
return maxSubMatrixSize;
int maxArea = 0;
return maxArea;
int maxArea = 0;
while (i < j) {
} else {
return maxArea;
int left = 0;
int right = m.length - 1;
if (m[middle] == x) {
return middle;
} else {
} else {
left = middle + 1; // search in the right-half
return -1;
a. If the current interval does not overlap with the interval from
the top of the stack, then push it into the stack.
b. If the current interval overlaps with the interval from the top of
the stack and the end of the current interval is greater than that of
the stack top, then update the top of the stack with the end of the
current interval.
4. At the end, the stack contains the merged intervals.
// Step 1
java.util.Arrays.sort(intervals,
new Comparator<Interval>() {
});
// Step 3a
if (stackOfIntervals.empty() || interval.start
> stackOfIntervals.peek().end) {
stackOfIntervals.push(interval);
// Step 3b
stackOfIntervals.peek().end = interval.end;
// Step 1
java.util.Arrays.sort(intervals,
new Comparator<Interval>() {
});
int index = 0;
// Step 2a
<= intervals[i].end) {
<= intervals[i].end) {
index--;
// Step 2b
} else {
intervals[index] = intervals[i];
}
index++;
If the sum of fuel ≥ the sum of distances, then the tour can be
completed.
int start = 0;
if (sumRemainingFuel >= 0) {
sumRemainingFuel += remainingFuel;
} else {
sumRemainingFuel = remainingFuel;
start = i;
totalFuel += remainingFuel;
if (totalFuel >= 0) {
return start;
} else {
return -1;
// start point 1
// no solution, return -1
// start point 2
If the minimum is smaller than the height of the current bar, then
the current bar cannot hold water on top of it.
If the minimum is greater than the height of the current bar, then
the current bar can hold an amount of water equal to the
difference between the minimum and the height of the current bar
on top of it.
int n = bars.length - 1;
int water = 0;
left[0] = Integer.MIN_VALUE;
return water;
// to 0 and bars.length-1
int left = 0;
int water = 0;
left++;
} else {
right--;
return water;
You are allowed to buy and sell the stock only once.
You are allowed to buy and sell the stock only twice.
You are allowed to buy and sell the stock unlimited times.
You are allowed to buy and sell the stock only k times (k is given).
int result = 0;
}
return result;
right[prices.length - 1] = 0;
int result = 0;
return result;
int[] prices) {
int result = 0;
if (diff > 0) {
result += diff;
return result;
}
1. temp[p] = Math.max(result[p - 1]
temp[p] = Math.max(result[p - 1]
return result[k];
.boxed()
.collect(Collectors.toSet());
int longestSequence = 1;
if (!sequenceSet.contains(elem - 1)) {
int sequenceLength = 1;
while (sequenceSet.contains(elem +
sequenceLength)) {
sequenceLength++;
longestSequence = Math.max(
longestSequence, sequenceLength);
}
return longestSequence;
(10+10+10+3) = 33
(5+5+10+10+3) = 33
(5+5+5+5+10+3) = 33
(5+5+5+5+5+5+3) = 33
(3+3+3+3+3+3+3+3+3+3+3) = 33
(3+3+3+3+3+3+5+5+5) = 33
(3+3+3+3+3+3+5+10) = 33
table[0] = 1;
return table[n];
if (arr[i] == arr[j]) {
return true;
return false;
java.util.Arrays.sort(arr);
return true;
prev = arr[i];
return false;
if (set.contains(arr[i])) {
return true;
}
set.add(arr[i]);
return false;
1. We iterate over the given array and for each arr[i], we do the
following:
if (arr[Math.abs(arr[i])] > 0) {
arr[Math.abs(arr[i])] = -arr[Math.abs(arr[i])];
} else if (arr[Math.abs(arr[i])] == 0) {
arr[Math.abs(arr[i])] = -(arr.length-1);
} else {
return true;
}
}
return false;
For the following five coding challenges, you can find the
solutions in the code bundled with this book. Take your
time and challenge yourself to come up with a solution
before checking the bundled code.
Summary
The goal of this chapter was to help you master various
coding challenges involving strings and/or arrays.
Hopefully, the coding challenges in this chapter have
provided various techniques and skills that will be very
useful in tons of coding challenges that fall under this
category. Don't forget that you can enrich your skills
even more via the book Java Coding Problems
(https://www.amazon.com/gp/product/1789801419/),
which is published by Packt as well. Java Coding
Problems comes with 35+ strings and arrays problems
that were not tackled in this book.
In the next chapter, we will discuss linked lists and maps.
Chapter 11:
Maps in a nutshell
Coding challenges
Technical requirements
All of the code files in this chapter are available on
GitHub and can be accessed at
https://github.com/PacktPublishing/The-Complete-
Coding-Interview-Guide-in-
Java/tree/master/Chapter11.
However, before going into the coding challenges, let's
first learn about linked lists and maps.
The code bundle for this book comes with the following
applications (each application exposes the
insertFirst(), insertLast(), insertAt(), delete(),
deleteByIndex(), and print() methods):
Maps in a nutshell
Imagine that you are looking for a word in a dictionary.
The word itself is unique and can be considered a key.
The meaning of this word can be considered the value.
Therefore, the word and its meaning form a key-value
pair. Similarly, in computing, a key-value pair
accommodates a piece of data in which the value can be
found by searching with the key. In other words, we
know the key and we can use it to find the value.
The most common methods to work with a map are get(), put(),
and remove().
Sorting a Map
Copying a HashMap
private V value;
this.key = key;
this.value = value;
= new MyEntry[DEFAULT_CAPACITY];
Next, we can focus on working with this array to act as a
map for the client. Putting an entry into the map can
only be done if the entry's key is unique across the map.
If the given key exists, then we just update its value. In
addition to this, we can add an entry as long as we
haven't exceeded the map capacity. The typical approach
in such a case is to double the size of the map. The code
based on these statements is as follows:
if (entries[i].getKey().equals(key)) {
entries[i].setValue(value);
success = false;
if (success) {
checkCapacity();
if (size == entries.length) {
if (entries[i] != null) {
if (entries[i].getKey().equals(key)) {
return entries[i].getValue();
return null;
}
Finally, we need to remove an entry using the key.
Removing an element from an array involves shifting the
remaining elements by one position. After the elements
are shifted, the penultimate and last elements are equal.
You can avoid memory leaks by nullifying the last
element of the array. It is a common mistake to forget
this step:
if (entries[i].getKey().equals(key)) {
entries[i] = null;
size--;
condenseArray(i);
int i;
}
The production implementation of a map is much more
complicated than the one exposed here (for example, a
map uses buckets). However, most probably, you won't
need to know more than this implementation in an
interview. Nevertheless, it is a good idea to mention this
to the interviewer. That way, you can show them you
understand the complexity of the problem and that you
are aware of it.
set.add(entries[i].getKey());
return set;
}
To return a collection of values, we loop the map and add
the values, one by one, to a List. We use a List since
values can contain duplicates:
list.add(entries[i].getValue());
return list;
map.put(nuts[i], i);
if (map.containsKey(bolt)) {
nuts[i] = bolts[i];
} else {
if (dataSet.contains(currentNode.data)) {
prevNode.next = currentNode.next;
if (currentNode == tail) {
tail = prevNode;
size--;
} else {
dataSet.add(currentNode.data);
prevNode = currentNode;
currentNode = currentNode.next;
1. The current node, which starts from the head of the linked list and
traverses the linked list, node by node, until it reaches the tail (for
example, in the preceding diagram, the current node is the second
node).
2. The runner node, which starts from the same place as the current
node, that is, the head of the linked list.
Additionally, the runner node iterates through the linked
list and checks whether the data of each node is equal to
the data of the current node. While the runner code
iterates through the linked list, the current node's
position remains fixed.
if (runnerNode.next.data == currentNode.data) {
if (runnerNode.next == tail) {
tail = runnerNode;
runnerNode.next = runnerNode.next.next;
size--;
} else {
runnerNode = runnerNode.next;
}
}
currentNode = currentNode.next;
head = currentNode;
tail = currentNode;
if (currentNode.data < n) {
currentNode.next = head;
head = currentNode;
} else {
tail = currentNode;
currentNode = nextNode;
tail.next = null;
if (firstRunner == null) {
throw new IllegalArgumentException(
firstRunner = firstRunner.next;
firstRunner = firstRunner.next;
secondRunner = secondRunner.next;
return secondRunner.data;
slowRunner = slowRunner.next;
fastRunner = fastRunner.next.next;
break;
return;
slowRunner = head;
slowRunner = slowRunner.next;
fastRunner = fastRunner.next;
}
As a quick note, don't expect that FR can jump over SR,
so they will not meet. This scenario is not possible.
Imagine that FR has jumped over SR and it is at node a,
then SR must be at node a-1. This means that, at the
previous step, FR was at node a-2 and SR was at node
(a-1)-1=a-2; therefore, they have collided.
So, when FR has reached the end of the linked list and
SR has reached the fourth node (the middle of the linked
list), the stack contains the values of 2, 1, and 4. Next, we
can continue to move SR at a rate of 1 node until the end
of the linked list. At each move, we pop a value from the
stack, and we compare it with the current node value. If
we find a mismatch, then the linked list is not a
palindrome. In the code, we have the following:
// the first half of the linked list is added into the stack
firstHalf.push(slowRunner.data);
slowRunner = slowRunner.next;
fastRunner = fastRunner.next.next;
}
if (fastRunner != null) {
slowRunner = slowRunner.next;
if (top != slowRunner.data) {
return false;
slowRunner = slowRunner.next;
return true;
return null;
if (node1 != null) {
value += node1.data;
if (node2 != null) {
value += node2.data;
resultNode.next = more;
return resultNode;
The main issue is that the lists are not of the same size. If
their sizes were equal, we could traverse both of them,
node by node, from head to tail until they collide (until
node_list_1.next= node_list_2.next). If we could skip
the nodes with values of 2 and 1, our lists will be the
same size (refer to the next diagram; since the first list is
longer than the second list, we should start iterating
from the node marked virtual head):
int s1 = linkedListSize(currentNode1);
int s2 = linkedListSize(currentNode2);
currentNode1 = currentNode1.next;
} else {
currentNode2 = currentNode2.next;
if (currentNode1 == currentNode2) {
return currentNode1.data;
currentNode1 = currentNode1.next;
currentNode2 = currentNode2.next;
return -1;
But we can fix the links, right? Well, we can explicitly set
n1.next to point to n2, and set n2.next to point to n3:
n1.next = n2
n2.next = n3
Now it should be good! We can swap two consecutive
nodes. However, when we swap a pair of nodes, we also
break the links between two consecutive pairs of nodes.
The following diagram illustrates this issue (we swap and
fix the links for the n1-n2 pair and the n3-n4 pair):
return;
node1 = node2;
node2 = auxNode;
node1.next = node2;
node2.next = node3;
if (prevPair == null) {
head = node1;
} else {
prevPair.next = node1;
if (currentNode.next == null) {
tail = currentNode;
}
// prepare the prevNode of the current pair
prevPair = node2;
currentNode = node3;
Since the head of list1 is less than the head of list2 (4 <
5), it becomes the head of the merged list. We said that
list1 will point to the last node of the merged list;
therefore, the next node to compare should be list1.next
(the node with value 7) and list2 (the node with value 5).
The following diagram reveals the result of this
comparison:
head = list1;
} else {
head = list2;
list2 = list1;
list1 = head;
list1.next = list2;
list2 = auxNode;
}
list1 = list1.next;
if (list1.next == null) {
list1.next = list2;
if (currentNode.c == currentNode.next.c
currentNode.next = middleNode;
currentNode.next = middleNode;
} else {
currentNode = currentNode.next;
// step 1
// step 2
// step 3
currentNode.next = null;
// step 4
nextNode.next = head;
head = nextNode;
// step 1
currentNode = currentNode.next;
}
// step 2
currentNode.next.next = head;
// step 3
head = currentNode.next;
// step 4
currentNode.next = null;
a. The next node (initially null) becomes the node next to the
current node (initially the head).
b. The node next to the current node (initially the head) becomes
the previous node (initially null).
d. The current node becomes the next node (the node from step
2a).
if (head != null) {
int counter = 0;
next = current.next;
current.next = prev;
prev = current;
current = next;
counter++;
if (next != null) {
return prev;
currentNode.prev = currentNode.next;
currentNode.next = prev;
// update the previous node before moving to the next
node
prevNode = currentNode;
currentNode = currentNode.prev;
if (prevNode != null) {
head = prevNode;
Fast eviction of data: When the cache is full (it has reached its
allocated bounds), the cache should empower an efficient
algorithm to evict an entry.
But how does the doubly linked list help us to track the
recently used entries? The secret relies on the following
points:
When we need to evict an entry, we evict the tail of the linked list
(so, the tail of the linked list holds the least recently used value).
}
private final Map<Integer, Node> hashmap;
public LRUCache() {
if (node != null) {
removeNode(node);
addNode(node);
return node.value;
return -1;
if (node != null) {
node.value = value;
removeNode(node);
addNode(node);
} else {
newNode.prev = null;
newNode.next = null;
newNode.value = value;
newNode.key = key;
hashmap.remove(tail.key);
removeNode(tail);
addNode(newNode);
} else {
addNode(newNode);
}
hashmap.put(key, newNode);
node.next = head;
node.prev = null;
if (head != null) {
head.prev = node;
head = node;
if (tail == null) {
tail = head;
if (node.prev != null) {
node.prev.next = node.next;
} else {
head = node.next;
if (node.next != null) {
node.next.prev = node.prev;
} else {
tail = node.prev;
Summary
This chapter brought your attention to the most common
problems involving linked lists and maps. Among these
problems, the ones that involve singly linked lists are
preferred; therefore, this chapter was primarily focused
on this category of coding challenges.
Stacks in a nutshell
Queues in a nutshell
Coding challenges
Technical requirements
All the code files presented in this chapter are available
on GitHub at https://github.com/PacktPublishing/The-
Complete-Coding-Interview-Guide-in-
Java/tree/master/Chapter12.
Stacks in a nutshell
A stack is a linear data structure that uses the Last-In-
First-Out (LIFO) principle. Think of a stack of plates
that needs to be washed. You take the first plate from the
top (which was the last one to be added) and you wash it.
Afterward, you take the next plate from the top and so
on. This is exactly what a real-life stack is (for example, a
stack of plates, a stack of books, a stack of CDs, and so
on).
E peek(): Returns (but doesn't remove) the top element from the
stack
MyStack() {
Object[].class.getComponentType(),
DEFAULT_CAPACITY);
public E pop() {}
public E peek() {}
if (isFull()) {
ensureCapacity();
stack[top++] = e;
public E pop() {
// if the stack is empty then just throw an exception
if (isEmpty()) {
E e = stack[--top];
stack[top] = null;
return e;
public E peek() {
if (isEmpty()) {
Queues in a nutshell
A queue is a linear data structure that uses the First-In-
First-Out (FIFO) principle. Think of people standing in
a queue to buy stuff. You can also imagine ants that are
walking in a queue formation.
MyQueue() {
Object[].class.getComponentType(),
DEFAULT_CAPACITY);
front = 0;
rear = -1;
capacity = DEFAULT_CAPACITY;
public E dequeue() {}
public E peek() {}
if (isFull()) {
ensureCapacity();
queue[rear] = e;
count++;
capacity = newSize;
public E dequeue() {
if (isEmpty()) {
E e = queue[front];
queue[front] = null;
count--;
return e;
}
Peeking an element from a queue means returning the
next element from the beginning of the underlying array
without removing it from the array:
public E peek() {
if (isEmpty()) {
return queue[front];
Coding challenges
In the next 11 coding challenges, we will cover the most
popular problems involving stacks and queues that have
appeared in interviews in the past few years in a wide
range of companies that hire Java developers. One of the
most common problems, Implementing three stacks
with one array, was covered in Chapter 10, Arrays and
Strings.
1. Loop the string from left to right and push each character into the
stack.
2. Loop the stack and pop the characters one by one. Each popped
character is put back into the string.
stack.push(c);
chars[i] = stack.pop();
1. For each character of the given string, take one of the following
decisions:
i. Check the top of stack, and if it is {, pop and move it to the next
character.
switch (bracesStr.charAt(i)) {
case '{':
stackBraces.push(bracesStr.charAt(i));
break;
case '}':
return false;
stackBraces.pop();
break;
default:
return false;
return stackBraces.empty();
= new LinkedList<>();
if (stacks.isEmpty() || stacks.getLast().size()
>= STACK_SIZE) {
stack.push(value);
stacks.add(stack);
} else {
stacks.getLast().push(value);
removeStackIfEmpty();
return value;
if (stacks.getLast().isEmpty()) {
stacks.removeLast();
shift(stackIndex);
removeStackIfEmpty();
return value;
currentStack.push(nextStack.remove(0));
For day 2, the price is 34. Since 34 is less than the price of the
prior day (55), the stock span of day 2 is also 1.
For day 3, the price is 22. Since 22 is less than the price of the
prior day (34), the stock span of day 3 is also 1. Days 7 and 8 fall
under the same scenario.
For day 4, the price is 23. Since 23 is greater than the price of the
prior day (22), but is less than the price of day 2, the stock span is
2. Day 9 is similar to day 4.
For day 5, the price is 27. Since this price is greater than the prices
of days 3 and 4 but less than the price of day 2, the stock span is 3.
For day 6, the price is 88. This is the biggest price so far, so the
stock span is 6.
For day 10, the price is 100. This is the biggest price so far, so the
stock span is 10.
1. The first day has a stock span of 1 and an index of 0 – we push this
index into the stack (let's denote it as dayStack; therefore,
dayStack.push(0)).
2. We loop the remaining days (day 2 has index 1, day 3 has index 2,
and so on) and do the following:
Let's see how this algorithm works for our test case:
1. The first day has a stock span of 1 and an index of 0 – we push this
index into the stack, dayStack.push(0).
2. For the second day, stockPrices[1]=34 and
stockPrices[0]=55. Since 34 < 55, the stock span of day 2 is i -
dayStack.peek() = 1 - 0 = 1. We push in stack 1,
dayStack.push(1).
3. For the third day, stockPrices[2]=22 and stockPrices[1]=34.
Since 22 < 34, the stock span of day 3 is 2 - 1 = 1. We push in stack
1, dayStack.push(2).
4. For the fourth day, stockPrices[3]=23 and stockPrices[2]=22.
Since 23 > 22 and the stack is not empty, we pop the top, so we
pop the value 2. Since 23 < 34 (stockPrices[1]), the stock span of
day 4 is 3 - 1 = 2. We push in stack 3, dayStack.push(3).
5. For the fifth day, stockPrices[4]=27 and stockPrices[3]=23.
Since 27 > 23 and the stack is not empty, we pop the top, so we
pop the value 3. Next, 27 < 34 (remember that we popped the
value 2 in the previous step, so the next top has the value 1), and
the stock span of day 5 is 4 - 1 = 3. We push in stack 4,
dayStack.push(4).
6. For the sixth day, stockPrices[5]=88 and stockPrices[4]=27.
Since 88 > 27 and the stack is not empty, we pop the top, so we
pop the value 4. Next, 88 > 34 and the stack is not empty, so we
pop the value 1. Next, 88 > 55 and the stack is not empty, so we
pop the value 0. Next, the stack is empty and the stock span of day
6 is 5 + 1 = 6.
dayStack.push(0);
while (!dayStack.empty()
dayStack.pop();
if (dayStack.empty()) {
spanResult[i] = i + 1;
} else {
dayStack.push(i);
return spanResult;
Stack<Integer> stackOfMin;
public MyStack() {
stackOfMin.push(value);
return super.push(value);
@Override
if (value == min()) {
stackOfMin.pop();
return value;
return Integer.MAX_VALUE;
} else {
return stackOfMin.peek();
The given value multiplied by 2 should not exceed the int data
type domain.
if (stack.empty()) {
stack.push(value);
min = value;
stack.push(value);
} else {
stack.push(r - min);
min = value;
}
if (stack.empty()) {
stack.pop();
return min;
public MyQueueViaStack() {
stackEnqueue.push(e);
public E dequeue() {
reverseStackEnqueue();
return stackDequeue.pop();
public E peek() {
reverseStackEnqueue();
return stackDequeue.peek();
if (stackDequeue.isEmpty()) {
while (!stackEnqueue.isEmpty()) {
stackDequeue.push(stackEnqueue.pop());
private E peek;
public MyStackViaQueue() {
if (!queue1.isEmpty()) {
if (peek != null) {
queue1.add(peek);
queue1.add(e);
} else {
if (peek != null) {
queue2.add(peek);
queue2.add(e);
size++;
peek = null;
}
public E pop() {
if (size() == 0) {
if (peek != null) {
E e = peek;
peek = null;
size--;
return e;
E e;
if (!queue1.isEmpty()) {
e = switchQueue(queue1, queue2);
} else {
e = switchQueue(queue2, queue1);
size--;
return e;
public E peek() {
if (size() == 0) {
if (peek == null) {
if (!queue1.isEmpty()) {
} else {
return peek;
return size;
to.add(from.poll());
}
}
1. Repeat steps 1a, 1b, and 1c as long as the current bar is smaller
than the top of the stack and the stack is not empty:
e. If this area is bigger than the previous one, then we store this
one.
2. Push the index of the current bar into the stack.
3. Repeat from step 1 until every bar is processed.
int maxArea = 0;
int barHeight;
if (bar == histogram.length) {
} else {
barHeight = histogram[bar];
while (!stack.empty()
stack.push(bar);
return maxArea;
a. While the given k is greater than 0, the stack is not empty and
the top element in the stack is greater than the currently traversed
digit:
ii. Decrement k by 1.
int i = 0;
stack.pop();
k--;
stack.push(nr.charAt(i));
i++;
while (k > 0) {
stack.pop();
k--;
}
System.out.println("The number is (as a printed stack; "
}
So far, we know how to decide if a move from the current
cell to another cell (from the eight possible movements)
is valid or not. Furthermore, we have to define an
algorithm to determine a movement pattern. We know
that from a cell (r, c), we can move in eight directions in
neighboring cells. So, the most convenient algorithm
consists of trying to move from the current cell into all
the valid neighbors, as follows:
int r, c;
this.r = r;
this.c = c;
int m = matrix.length;
int n = matrix[0].length;
int island = 0;
island++;
return island;
flagged[i][j] = true;
while (!queue.isEmpty()) {
int r = queue.peek().r;
int c = queue.peek().c;
queue.poll();
b. If the popped cell is the destination cell (that is, it is on the last
column), then simply return its distance (the distance from the
destination cell to the source cell on the first column).
c. If the popped cell is not the destination then, for each of the four
adjacent cells of this cell, enqueue each valid cell (safe and
unvisited) into the queue with distance (+1) and mark it as visited.
if (board[r1][0] == 1) {
visited[r1][0] = true;
while (!queue.isEmpty()) {
queue.poll();
// if destination is found then return minimum
distance
if (cIdx == N - 1) {
cIdx + COL_4[k])) {
return -1;
Trees in a nutshell
Graphs in a nutshell
Coding challenges
Technical requirements
All the code present in this chapter can be found on
GitHub at https://github.com/PacktPublishing/The-
Complete-Coding-Interview-Guide-in-
Java/tree/master/Chapter13.
Trees in a nutshell
A tree is a non-linear data structure that organizes data
hierarchically in nodes and cannot contain cycles. A tree
has a specific terminology that may vary slightly, but
commonly, the following notions are adopted:
Typically, any tree can have a root. The nodes of the tree
can respect a certain order (or not), can store any type of
data, and may have links to their parents.
this.element = element;
this.left = null;
this.right = null;
}
public Node(Node left, Node right, T element) {
this.element = element;
this.left = left;
this.right = right;
// operations
1. Pop the first node from the queue as the current node.
2. Visit the current node.
3. If the current node has a left node, then enqueue that left node.
4. If the current node has a right node, then enqueue that right node.
5. Repeat from step 1 until the queue is empty.
queue.add(node);
while (!queue.isEmpty()) {
// Step 1
// Step 2
// Step 3
if (current.left != null) {
queue.add(current.left);
}
// Step 4
if (current.right != null) {
queue.add(current.right);
if (node != null) {
printPreOrder(node.left);
printPreOrder(node.right);
printInOrder(node.left);
printInOrder(node.right);
if (node != null) {
printPostOrder(node.left);
printPostOrder(node.right);
RED-BLACK TREE
A Red-Black tree is a self-balancing BST where each
node is under the incident of the following rules:
Every path from a node to a NULL node has the same number of
black nodes
AVL TREE
An AVL tree (named after their inventors, Adelson-
Velsky and Landis) is a self - balancing BST that respects
the following rules:
So, in a perfect binary tree, all the leaf nodes are at the
same level. This means that the last level contains the
maximum number of nodes. These kinds of tree are
pretty rare in interviews.
Important note
Binary Heaps
In a nutshell, a Binary Heap is a complete binary tree
that has a heap property. When the elements are in
ascending order (the heap property says that the element
of each node is greater than or equal to the element of its
parent), we have a Min Binary Heap (the minimum
element is the root element), while when they are in
descending order (the heap property says that the
element of each node is less than or equal to the element
of its parent), we have a Max Binary Heap (the maximum
element is the root element).
public MaxHeap() {
capacity = DEFAULT_CAPACITY;
Comparable[].class.getComponentType(),DEFAULT_
CAPACITY);
// operations
Important note
Graphs in a nutshell
A graph is a data structure that's used to represent a
collection of nodes that can be connected with edges. For
example, a graph can be used to represent a network of
members on a social media platform, so it is a great data
structure for representing real-life connections. A tree
(as detailed in the previous section) is a particular type of
graph. In other words, a tree is a graph without cycles. In
graph terms, a graph without cycles is called an acyclic
graph.
Adjacency matrix
An adjacency matrix is represented by a boolean two-
dimensional array (or an integer two-dimensional array
that contains only 0s and 1s) of size n x n, where n is the
number of vertices. If we denote this two-dimensional
array as a matrix, then matrix[i][j] is true (or 1) if there
is an edge from vertex i to vertex j; otherwise, it is false
(or 0). The following diagram shows an example of an
adjacency matrix for an undirected graph:
Figure 13.13 – An adjacency matrix for an undirected
graph
public Graph() {
// operations
}
Another approach we can use to represent a graph in a
computer is the adjacency list.
Adjacency list
An adjacency list is an array of lists whose size is equal to
the number of vertices in the graph. Every vertex is
stored in this array and it stores a list of adjacent
vertices. In other words, the list at index i of the array
contains the adjacent vertices of the vertex stored in the
array at index i. The following diagram shows an
example of an adjacency list for an undirected graph:
public Graph() {
// operations
Graph traversal
The two most common ways to traverse a graph are via
Depth-first Search (DFS) and Breadth-first
Search (BFS). Let's have a rundown of each. BFS is
mainly used for graphs.
1. Start from the current node (the given node) and push the current
node into Stack.
2. While Stack is not empty, do the following:
In the code bundled with this book, you can find a graph
implementation based on the adjacency matrix called
GraphAdjacencyMatrixTraversal. You can also find one
based on the adjacency list called
GraphAdjacencyListTraversal. Both applications
contain BFS and DFS implementations.
Coding challenges
Now that we have had a brief overview of trees and
graphs, it is time to challenge ourselves with the 25 most
popular coding problems encountered in interviews
about these topics.
visited.add(from);
queue.add(from);
while (!queue.isEmpty()) {
T element = queue.poll();
List<T> adjacents = adjacencyList.get(element);
if (adjacents != null) {
for (T t : adjacents) {
visited.add(t);
queue.add(t);
if (t.equals(to)) {
return true;
return false;
return null;
}
int middle = (start + end) / 2;
nodeCount++;
return node;
currentLevelOfNodes.add(root);
currentLevelOfElements.add(root.element);
while (!currentLevelOfNodes.isEmpty()) {
allLevels.add(currentLevelOfElements);
if (parent.left != null) {
currentLevelOfNodes.add(parent.left);
currentLevelOfElements.add(parent.left.element);
if (parent.right != null) {
currentLevelOfNodes.add(parent.right);
currentLevelOfElements.add(parent.right.element);
return allLevels;
}
The complete application is called
ListPerBinaryTreeLevel.
if (p == null) {
return false;
return true;
return true;
if (p == null || q == null) {
return false;
long t1 = Duration.between(current.element.
plusMinutes(current.time), element).toMinutes();
long t2 = Duration.between(current.element,
element.plusMinutes(time)).toMinutes();
// overlapping found
this.time = time;
this.element = element;
this.left = null;
this.right = null;
this.time = time;
this.element = element;
this.left = left;
this.right = right;
}
if (element == null) {
if (current == null) {
long t1 = Duration.between(current.element.
plusMinutes(current.time), element).toMinutes();
long t2 = Duration.between(current.element,
element.plusMinutes(time)).toMinutes();
return current;
}
if (element.compareTo(current.element) < 0) {
} else {
return current;
printInOrder(root);
if (node != null) {
printInOrder(node.left);
printInOrder(node.right);
return isBalanced(root);
if (root == null) {
return true;
return false;
} else {
if (root == null) {
return 0;
if (root == null) {
return 0;
if (leftHeight == Integer.MIN_VALUE) {
}
int rightHeight = checkHeight(root.right);
if (rightHeight == Integer.MIN_VALUE) {
} else {
We know that the BST property says that for each node,
n, of a BST, the left descendants of n ≤ n < right
descendants of n. This means that the first two binary
trees shown in the previous diagram are valid BST, while
the last one is not a valid BST. Now, adding the elements
of the middle and the last binary tree to the array will
result in an array of [40, 40]. This means we cannot
validate or invalidate a BST based on this array since we
cannot distinguish between the trees. So, in conclusion,
you should rely on this simple algorithm if the given
binary tree doesn't accept duplicates.
T minElement, T maxElement) {
if (node == null) {
return true;
node.element.compareTo(minElement) <= 0)
return false;
if (!isBinarySearchTree(node.left, minElement,
node.element)
|| !isBinarySearchTree(node.right,
node.element, maxElement)) {
return false;
return true;
Node inOrderSuccessor(Node n) {
System.out.println("\n\nIn-Order:");
node = inOrderSuccessor(node);
if (node == null) {
return null;
// case (a)
if (node.right != null) {
return findLeftmostNode(node.right);
// case (b)
node = node.parent;
return node.parent;
return stack;
visited.add(currentElement);
List<T> adjacents =
adjacencyList.get(currentElement);
if (adjacents != null) {
for (T t : adjacents) {
visited.add(t);
}
stack.push(currentElement);
return null;
return root;
return left;
return right;
return root;
return root;
} else {
b. If the popped cell is the target cell, then return its distance.
c. If the popped cell is not the target cell, then mark this cell as
visited and enqueue each of the eight possible movements into the
queue by increasing the distance by 1.
queue.add(startCell);
while (!queue.isEmpty()) {
int r = cell.r;
int c = cell.c;
return distance;
if (!visited.contains(cell)) {
visited.add(cell);
// with +1 distance
int rt = r + ROW[i];
int ct = c + COL[i];
return false;
return true;
if (root == null) {
return;
queue.add(root);
int level = 0;
while (!queue.isEmpty()) {
level++;
position--;
if (node.left != null) {
queue.add(node.left);
if (node.right != null) {
queue.add(node.right);
// level done
System.out.println();
So, a node that is part of the max path is put into one of
the following four cases:
3 only has the left child, -5, so 3 is added to max(-5, 0), 3+max(-5,
0)=3.
50 is added to the maximum of the left (39) and the right (69) sub-
trees, so 39+69+50=158 (this is the max path sum).
The following code reveals the implementation of this
algorithm:
maxPathSum(root);
return max;
if (root == null) {
return 0;
RECURSION-BASED SOLUTION
One solution to this problem is to use recursion and
hashing (if you are not familiar with the concept of
hashing, then please read Chapter 6, Object-Oriented
Programming, the Hash table problem). In Java, we can
use hashing via the built-in HashMap implementation,
so there is no need to write a hashing implementation
from scratch. But how is this HashMap useful? What
should we store in an entry (key-value pair) of this map?
// map of diagonals
printDiagonal(root, 0, map);
System.out.println(map.get(i));
if (node == null) {
return;
if (!map.containsKey(diagonal)) {
}
map.get(diagonal).add(node.element);
ITERATIVE-BASED SOLUTION
Solving this problem can be done iteratively as well. This
time, we can employ Level-Order traversal and enqueue
the nodes of a diagonal using a Queue. The main
pseudocode for this solution can be written as follows:
(first diagonal)
Print A
(next diagonal)
(let's denote it as B)
queue.add(root);
root = root.right;
queue.add(dummy);
while (queue.size() != 1) {
if (front != dummy) {
node = node.right;
} else {
queue.add(dummy);
System.out.println();
private T element;
this.element = element;
this.left = left;
this.right = right;
this.count = 1;
}
}
if (current == null) {
if (element.compareTo(current.element) == 0)
{
current.count++;
return current;
...
if (node == null) {
return null;
if (element.compareTo(node.element) < 0) {
if (element.compareTo(node.element) == 0) {
if (node.count > 1) {
node.count--;
return node;
...
// step 1
return true;
}
// step 2
return false;
// step 3
if (!treeOne.element.equals(treeTwo.element)) {
return false;
// steps 4, 5, 6 and 7
|| isIsomorphic(treeOne.left, treeTwo.left)
if (root == null) {
return;
queue.add(root);
Node currentNode;
while (!queue.isEmpty()) {
int i = 0;
i++;
currentNode = queue.poll();
if (i == size) {
System.out.print(currentNode.element + " ");
if (currentNode.left != null) {
queue.add(currentNode.left);
if (currentNode.right != null) {
queue.add(currentNode.right);
kthLargest(root, k);
}
private int c;
return;
kthLargest(root.right, k);
c++;
if (c == k) {
System.out.println(root.element);
kthLargest(root.left, k);
if (root == null) {
return null;
}
Node node = new Node(root.element);
node.left = mirrorTreeInTree(root.right);
node.right = mirrorTreeInTree(root.left);
return node;
if (node == null) {
return;
Node auxNode;
mirrorTreeInPlace(node.left);
mirrorTreeInPlace(node.right);
auxNode = node.left;
node.left = node.right;
node.right = auxNode;
Odd levels should be printed from left to right and even levels
from right to left.
Odd levels should be printed from right to left and even levels
from left to right.
RECURSIVE APPROACH
Let's try to implement the spiral order traversal from the
left-hand side of the preceding diagram. Notice that the
odd levels should be printed from left to right, while the
even levels should be printed in reverse order. Basically,
we need to adjust the well-known Level-Order traversal
by flipping the direction of the even levels. This means
that we can use a boolean variable to alternate the
printing order. So, if the boolean variable is true (or 1),
then we print the current level from left to right;
otherwise, we print it from right to left. At each iteration
(level), we flip the boolean value.
if (root == null) {
return;
int level = 1;
// there is nothing to do
};
}
if (root == null) {
return false;
if (level == 1) {
return true;
if (flip) {
} else {
ITERATIVE APPROACH
Let's try to implement the spiral order traversal from the
right-hand side of the given diagram. We'll do this via an
iterative approach this time. Mainly, we can use two
stacks (Stack) or a double ended queue (Deque). Let's
learn how we can do this via two stacks.
if (node == null) {
return;
rl.push(node);
while (!rl.empty()) {
rl.pop();
if (temp.right != null) {
lr.push(temp.right);
if (temp.left != null) {
lr.push(temp.left);
while (!lr.empty()) {
if (temp.left != null) {
rl.push(temp.left);
if (temp.right != null) {
rl.push(temp.right);
The nodes at distance 1 from a leaf node are 3, 11, 7, and 45.
The nodes at distance 2 from a leaf node are 11, 47, and 40.
if (node == null) {
return;
return;
pathToLeaf.add(node);
pathToLeaf.remove(node);
So, for sum=74, we can find the pair (6, 68). If sum=89,
then the pair is (43, 46). If sum=99, then the pair is (50,
49). The nodes that form the pair can be from the same
sub-tree or different sub-trees and can include the root
and leaf nodes as well.
// base case
if (node == null) {
return false;
return true;
if (set.contains(sum - node.element)) {
return true;
} else {
set.add(node.element);
}
if (minNode != null) {
fio.push(minNode);
minNode = minNode.left;
if (maxNode != null) {
rio.push(maxNode);
maxNode = maxNode.right;
} else {
if (fio.peek() == rio.peek()) {
break;
return true;
minNode = fio.pop();
minNode = minNode.right;
} else {
maxNode = rio.pop();
maxNode = maxNode.left;
return false;
if (root == null) {
return;
if (!map.containsKey(dist)) {
map.put(dist, 0);
// or in functional-style
/*
*/
int p = (maxHeap.length - 2) / 2;
while (p >= 0) {
int smallest = p;
// compare maxHeap[p] with its left and
smallest = left;
smallest = right;
if (smallest != p) {
swap(maxHeap, p, smallest);
/* Helper methods */
heap[i] = heap[j];
heap[j] = aux;
result = (leftNode.element.equals(rightNode.element))
&& isSymmetricRecursive(leftNode.left,
rightNode.right)
&& isSymmetricRecursive(leftNode.right,
rightNode.left);
return result;
queue.offer(root.left);
queue.offer(root.right);
while (!queue.isEmpty()) {
result = true;
|| left.element != right.element) {
result = false;
break;
} else {
queue.offer(left.left);
queue.offer(right.right);
queue.offer(left.right);
queue.offer(right.left);
return result;
1. Create the Min Binary Heap from the array of rope lengths (O(log
n)).
2. Poll the root of the Min Binary Heap, which will give us the
smallest rope (O(log n)).
3. Poll the root again, which will give us the second smallest rope
(O(log n)).
4. Connect two ropes (sum up their lengths) and put the result back
into the Min Binary Heap.
5. Repeat from step 2 until there is a single rope left (the result of
connecting all the ropes).
if (ropeLength == null) {
return -1;
add(ropeLength[i]);
int totalLength = 0;
int l1 = poll();
int l2 = poll();
add(l1 + l2);
return totalLength;
Advanced topics
Right from the start, you should know that the following
topics are rarely encountered in technical interviews.
First, let me enumerate these topics as a non-exhaustive
list:
Dijkstra's algorithm
Interval trees
B-trees
Bipartite graph
Graph coloring
Regular expressions
A*
Summary
This was one of the tough chapters of this book and a
must-read for any technical interview. Trees and graphs
are such wide, wonderful, and challenging topics that
entire books have been dedicated to them. However,
when you have to prepare for an interview, you don't
have the time to study tons of books and deep dive into
every topic. This is exactly where the magic of this
chapter comes into the picture: this chapter (just like the
entire book) is totally focused on the fact that you must
achieve your goal: ace a technical interview.
Sorting algorithms
Searching algorithms
Coding challenges
Technical requirements
You can find all the code files for this chapter on GitHub
at https://github.com/PacktPublishing/The-Complete-
Coding-Interview-Guide-in-
Java/tree/master/Chapter14.
Sorting algorithms
Considering the sorting algorithms from the perspective
of a person preparing for an interview reveals two main
categories: a category containing a lot of relatively simple
sorting algorithms that don’t occur in interviews, such as
Bubble Sort, Insertion Sort, Counting Sort, and so on,
and a category containing Heap Sort, Merge Sort, Quick
Sort, Bucket Sort, and Radix Sort. These represent the
top five sorting algorithms that occur in technical
interviews.
Bubble Sort
Pancake Sort
Exchange Sort
Selection Sort
Shell Sort
Insertion Sort
Counting Sort
Merge Sort
Heap Sort
Bucket Sort
Cocktail Sort
Cycle Sort
Quick Sort
Quick Sort with a Comparator
Radix Sort
Heap Sort
If you are not familiar with the heap concept, then
consider reading the Binary Heaps section of Chapter
13, Trees and Graphs.
int n = arr.length;
buildHeap(arr, n);
while (n > 1) {
swap(arr, 0, n - 1);
n--;
heapify(arr, n, 0);
heapify(arr, n, i);
int left = i * 2 + 1;
int right = i * 2 + 2;
int greater;
greater = left;
} else {
greater = i;
greater = right;
}
if (greater != i) {
swap(arr, i, greater);
heapify(arr, n, greater);
arr[x] = arr[y];
arr[y] = temp;
Merge Sort
Now, let’s discuss the Merge Sort algorithm. The time
complexity cases are as follows: best case O(n log n),
average case O(n log n), worst case O(n log n). The space
complexity may vary, depending on the chosen data
structures (it can be O(n)).
if (arr.length > 1) {
sort(left);
sort(right);
return left;
return right;
int t1 = 0;
int t2 = 0;
result[i] = left[t1];
t1++;
} else {
result[i] = right[t2];
t2++;
Quick Sort
Quick Sort is another recursive sorting algorithm based
on the famous divide and conquer strategy. The time
complexity cases are as follows: best case O(n log n),
average case O(n log n), worst case O(n2). The space
complexity is O(log n) or O(n).
The worst case scenario (O(n2)) takes place when all the
elements of the given array are smaller than the chosen
pivot or larger than the chosen pivot. Choosing the pivot
element can be done in at least four ways, as follows:
end
end
pivot = array[right]
m = left
for i = m to right-1
m=m+1
end
end
return m
end
To sort the entire array, we call sort(array, 0,
array.length-1). Let’s see its implementation:
sort(arr, m + 1, right);
int m = left;
swap(arr, i, m++);
return m;
Bucket Sort
Bucket Sort (or Bin Sort) is another sorting technique
that’s encountered in interviews. It is commonly used in
computer science and useful when the elements are
uniformly distributed over a range. The time complexity
cases are as follows: the best and average cases O(n+k),
where O(k) is the time for creating the bucket (this will
be O(1) for a linked list or hash table), while O(n) is the
time needed to put the elements of the given array into
the bucket (this will also be O(1) for a linked list or hash
table). The worst case is O(n2). The space complexity is
O(n+k).
sort(array)
end
/* Scatter-Sort-Gather approach */
buckets[hash(e, hashes)].add(e);
Collections.sort(bucket);
int p = 0;
arr[p++] = j;
sort(array)
end
/* Scatter-Gather approach */
max = arr[i];
bucket[i] = 0;
bucket[arr[i]]++;
int p = 0;
arr[p++] = i;
}
Radix Sort
Radix Sort is a sorting algorithm that works very well for
integers. In Radix Sort, we sort the elements by grouping
the individual digits by their positions in the numbers.
Next, we sort the elements by sorting the digits at each
significant position. Commonly, this is done via Counting
Sort (the Counting Sort algorithm is detailed in the book
Java Coding Problems
(www.packtpub.com/programming/java-coding-
problems), published by Packt, but you can find an
implementation of it in the application called
SortArraysIn14Ways). Mainly, sorting the digits can be
done via any stable sorting algorithm.
max = arr[i];
int exp = 1;
exp *= radix;
buckets[i] = 0;
int bucket;
buckets[bucket]++;
}
out[--buckets[bucket]] = arr[i];
Searching algorithms
The main searching algorithm that occurs in interviews
as a standalone problem or part of another problem is
the Binary Search algorithm. The best case time
complexity is O(1), while the average and worst case is
O(log n). The worst case auxiliary space complexity of
Binary Search is O(1) for the iterative implementation
and O(log n) for the recursive implementation due to the
call stack.
compare 17 to 17
return
The iterative implementation is listed here:
int left = 0;
if (p == arr[mid]) {
return mid;
right = mid - 1;
left = mid + 1;
return -1;
Coding challenges
So far, we’ve covered the most popular sorting and
searching algorithms that are encountered in technical
interviews. It is advised that you practice these
algorithms since they may occur as standalone problems
that require the pseudocode or the implementation.
Let’s see this test case step by step (let’s denote the index
of the last element from p with pIdx and the index of the
last element from q with qIdx). In the previous diagram,
pIdx=2 (corresponding to element 8) and qIdx=1
(corresponding to element 4).
Step 1: We compare the last element from p (the
element at index pIdx) with the last element from q (the
element at index qIdx), so we compare 8 with 4. Since 8
> 4, we copy 8 to the end of p. Since both arrays are
sorted, 8 is the maximum of these arrays, so it must go to
the last position (index) in p. It will occupy an empty slot
in p (remember that p is large enough to fit q at its end).
We decrease pIdx by 1.
// merge p and q
p[mIdx] = p[pIdx];
pIdx--;
} else {
p[mIdx] = q[qIdx];
qIdx--;
mIdx--;
String[] words = {
};
Since anagrams contain exactly the same characters, this
means that if we sort them, then they will be identical
(for example, sorting "slat", "salt" and "last" result in
"alst"). So, we can say that two strings (words) are
anagrams by comparing their sorted versions. In other
words, all we need is a sorting algorithm. The most
convenient way to do this is to rely on Java's built-in
sorting algorithm, which is Dual-Pivot Quicksort for
primitives and TimSort for objects.
void sort(Object[] a)
Arrays.sort(wordToChar);
return String.valueOf(wordToChar);
return
sortStringChars(sl).compareTo(sortStringChars(s2));
if (result.containsKey(sortedWord)) {
result.get(sortedWord).add(word);
} else {
anagrams.add(word);
result.put(sortedWord, anagrams);
System.out.println(result.values());
}
If n is the number of strings (words) and each string
(word) has a maximum of m characters, then the time
complexity of the preceding two approaches is O(nm log
m).
wordToChar[word.charAt(j) - 'a']++;
}
String computedWord = String.valueOf(wordToChar);
if (result.containsKey(computedWord)) {
result.get(computedWord).add(word);
} else {
anagrams.add(word);
result.put(computedWord, anagrams);
System.out.println(result.values());
this.arr = arr.clone();
return -1;
return arr[index];
int index = 1;
while (sl.peekAt(index) != -1
index *= 2;
int mid;
right = mid - 1;
left = mid + 1;
} else {
return mid;
return -1;
Dividing the given linked list like this can be done via the
Fast Runner/Slow Runner approach. This approach was
detailed in Chapter 11, Linked Lists and Maps, in the The
Fast Runner/Slow Runner approach section. Mainly,
when the Fast Runner (FR) reaches the end of the
given linked list, the Slow Runner (SR) points to the
middle of this list, so we can split the list in two. The
code for this is listed here:
fastRunner = fastRunner.next;
if (fastRunner != null) {
slowRunner = slowRunner.next;
fastRunner = fastRunner.next;
sourceNode, slowRunner.next};
slowRunner.next = null;
return headsOfSublists;
// sort the given linked list via the Merge Sort algorithm
head = sort(head);
return head;
head1 = sort(head1);
head2 = sort(head2);
if (head1 == null) {
return head2;
return head1;
Node merged;
merged = head1;
} else {
merged = head2;
return merged;
return -1;
if (stringsArr[mid].isEmpty()) {
return -1;
&& !stringsArr[rightMid].isEmpty()) {
mid = rightMid;
break;
&& !stringsArr[leftMid].isEmpty()) {
mid = leftMid;
break;
rightMid++;
leftMid--;
if (str.equals(stringsArr[mid])) {
return mid;
} else {
return;
while (!sorted) {
// Step 1
lastElement = queue.poll();
extraQueue.add(lastElement);
} else { // Step 2
queue.add(queue.poll());
count++;
if (count != queueSize) {
continue;
// Step 4
if (extraQueue.size() == queueSize) {
sorted = true;
// Step 3
queue.add(extraQueue.poll());
lastElement = queue.peek();
count = 0;
int sortIndex) {
// dequeue
queue.poll();
if (flag) {
queue.add(minElement);
flag = true;
minElement = currentElement;
} else {
queue.add(currentElement);
queue.add(minElement);
while (!stack.isEmpty()) {
int t = stack.pop();
stack.push(auxStack.pop());
auxStack.push(t);
// Step 2
while (!auxStack.isEmpty()) {
stack.push(auxStack.pop());
Done! The given stack has been sorted. Let’s see the
code:
if (stack.isEmpty()) {
return;
sortedInsert(stack, top);
stack.push(element);
return;
sortedInsert(stack, element);
stack.push(top);
}
The runtime of this code is O(n2) with an auxiliary space
of O(n) for the recursion call stack (n is the number of
elements in the given stack). The complete application is
called SortStackInPlace.
int left = 0;
if (element == midElement) {
return true;
right = mid - 1;
} else {
left = mid + 1;
return false;
If the end of a row is less than p, then p must be below that row.
int row = 0;
if (matrix[row][col] == element) {
return true;
} else {
row++;
return false;
Note
if (arr == null) {
return -1;
int left = 0;
if (arr[middle] == 0) {
left = middle + 1;
} else {
right = middle - 1;
if (arr[left] == 1) {
return left;
}
return -1;
marker = arr[i];
return maxDiff;
this.element = element;
this.left = null;
this.right = null;
}
if (root == null) {
} else {
insert(root, element);
if (node.left != null) {
insert(node.left, element);
} else {
node.leftTreeSize++;
} else {
if (node.right != null) {
insert(node.right, element);
} else {
if (element == node.element) {
return node.leftTreeSize;
if (node.left == null) {
return -1;
} else {
} else {
? -1 : getRank(node.right, element);
if (rightTreeRank == -1) {
return -1;
} else {
if (i != maxFoundIndex) {
swap(arr, i, maxFoundIndex);
? arr[left] : Integer.MIN_VALUE;
? arr[middle] : Integer.MIN_VALUE;
? arr[right] : Integer.MIN_VALUE;
Math.max(middleElement, rightElement));
if (leftElement == maxElement) {
return left;
} else if (middleElement == maxElement) {
return middle;
} else {
return right;
a. While the stack is not empty and the top element is greater than
or equal to arr[i], we pop from the stack.
c. If the stack is not empty, then the nearest smaller value to arr[i]
is the top element of the stack. We can peek and print this
element.
stack.pop();
if (stack.empty()) {
System.out.print("_, ");
} else {
stack.push(arr[i]);
1. Count and store the frequency of each element from the first array
in a map.
2. For each element of the second array, check if the current element
from the second array is present in the map or not.
b. Remove the current element from the map so that, in the end,
the map will contain only the elements that are present in the first
array but are not present in the second array.
3. Append the elements from the map to the end of the first array
(these are already sorted since we used a TreeSet).
frequencyMap.put(firstArr[i],
frequencyMap.get(firstArr[i]) + 1);
int index = 0;
firstArr[index++] = secondArr[i];
frequencyMap.remove(secondArr[i]);
firstArr[index++] = entry.getKey();
Well, this was the last problem in this chapter. Now, it’s
time to summarize our work!
Summary
This was a comprehensive chapter that covered sorting
and searching algorithms. You saw the implementations
of Merge Sort, Quick Sort, Radix Sort, Heap Sort, Bucket
Sort, and Binary Search. Moreover, in the code bundled
with this book, there’s an application called
SortArraysIn14Ways that contains the implementations
of 14 sorting algorithms.
Coding challenges
Technical requirements
All the code files present in this chapter are available on
GitHub at https://github.com/PacktPublishing/The-
Complete-Coding-Interview-Guide-in-
Java/tree/master/Chapter15.
Tips and suggestions
When you get a brain-teaser problem, the most
important aspect is to not panic. Read the problem
several times and write down your conclusions in a
systematic approach. It is mandatory to clearly identify
what input, output, and constraints it should obey.
Let's try a simple example. Two fathers and two sons sit
down and eat eggs. They eat exactly three eggs; each
person has an egg. How is this possible?
How about thinking like this: each person has an egg and
they (four people) eat exactly three eggs, so it doesn't say
that each person eats an egg; they only have an egg.
Maybe one of them shares their egg with another person.
Hmmm, this doesn't seem too logical!
System.out.println("fizzbuzz");
System.out.println("fizz");
System.out.println("buzz");
} else {
// Step 1
n -= 1000;
}
// Step 2
n = n % 100;
// Step 3
n = n % 10;
// Step 4
return roman;
At the first pass, we open every door (we visit each door,
#1, #2, #3, #4, ..., #100):
At the second pass, we only visit the even doors (#2, #4,
#6, #8, #10, #12 …), so the even doors are closed and the
odd ones are opened:
Figure 15.4 – The even doors are closed and the odd ones
are opened (step 2)
At the third pass, we only visit doors #3, #6, #9, #12, ….
This time, we close door #3, which we opened on our
first visit, open door #6, which was closed on our second
visit, and so on and forth:
So, at the last visit (the 100th visit), the opened doors are
all perfect squares, while the rest of the doors are closed.
Obviously, even if we observe this, we don't have the
necessary time in an interview to traverse 100 visits. But
maybe we don't even need to do all 100 visits to observe
this result. Let's assume that we do only 15 steps and we
try to see what's happening to a certain door. For
example, the following image reveals the state of door
#12 over 15 steps:
// 0 - closed door
// 1 - opened door
doors[i] = 0;
if ((j + 1) % (i + 1) == 0) {
if (doors[j] == 0) {
doors[j] = 1;
} else {
doors[j] = 0;
return doors;
Solution: Let's denote the teams as T1, T2, T3, T4, T5,
T6, T7, and T8. If T1 plays with T2...T8, they will play 7
matches. Since each team must play with the other teams
twice, we have 8*7=56 matches. If, at each match, a team
can win a point, then we have 56 points that are
distributed between 8 teams.
int count5 = 0;
int count7 = 0;
list.add(1);
int m = min(min(list.get(count3) * 3,
list.add(m);
if (m == list.get(count3) * 3) {
count3++;
if (m == list.get(count5) * 5) {
count5++;
if (m == list.get(count7) * 7) {
count7++;
}
We can provide an implementation via three queues as
well. The steps of this algorithm are as follows:
// base cases
if (n == 0 || n == 1) {
return 1;
return 0;
int count = 0;
return count;
return 0;
int n = digits.length;
count[0] = 1;
count[1] = 1;
count[i] = 0;
return count[n];
0 <= B <= 9
0 <= C <= 9
int q = i * 4;
String m = String.valueOf(p);
.reverse().toString();
p = Integer.parseInt(m);
q = Integer.parseInt(n);
if (p == q) {
break;
return false;
}
return false;
return true;
if (lenA == 0 || lenB == 0) {
return "0";
int idx1 = 0;
int idx2 = 0;
int carry = 0;
idx2 = 0;
idx2++;
// store carry
if (carry > 0) {
idx1++;
int i = c.length - 1;
i--;
if (i == -1) {
return "0";
while (i >= 0) {
result += (c[i--]);
return result;
From examples 1 and 4, we can see that if the digits of the given
number are in descending order, then it is impossible to find a
greater number. Every swap will lead to a smaller number.
From example 2, we can see that if the digits of the given number
are in ascending order, then the next greater number that has the
same digits can be obtained by swapping the last two digits.
int currentDigit;
currentDigit = arr[i];
min = i;
break;
if (min == -1) {
} else {
// Steps 2 and 3: Swap 'min' with 'len-1'
System.out.print(i);
start++;
end--;
arr[j] = aux;
Important note
int t = n;
while (n > 0) {
int k = n % 10;
if (k != 0 && t % k != 0) {
return false;
n /= 10;
return true;
Case 5: If the number of given tiles can be arranged along with the
chocolate bar's width, then there is a single cut. We'll return 1.
Case 6: If the number of given tiles can be arranged along with the
chocolate bar's height, then there is a single cut. We'll return 1.
return -1;
}
// case 1
return -1;
// case 4
return 0;
// cases 5 and 6
return 1;
// case 7
if (nTiles % i == 0) {
int a = i;
int b = nTiles / i;
return 2;
}
// cases 2 and 3
return -1;
So, the angle between the hour and the minute hand is
the abs((h*30o + m*0.5o) - m*6o). If the returned result
is greater than 180o, then we have to return (360o -
result) since the problem requires us to calculate the
shorter angle between the hour and the minute hand.
Clock 1, 10:10:
Clock 2, 9:40:
Clock 3, 4:40:
1. Square every element in the input array (O(n)). This means that
we can write a2 = b2 + c2 as a = b + c.
2. Sort the given array in ascending order (O(n log n)).
3. If a = b + c, then a is always the largest value between a, b, and c.
So, we fix a so that it becomes the last element of this sorted array.
4. Fix b so that it becomes the first element of this sorted array.
5. Fix c so that it becomes the element right before element a.
6. So far, b<a and c<a. To find the Pythagorean triplets, execute a
loop that increases b from 1 to n and decreases c from n to 1. The
loop stops when b and c meet:
// Step1
// Step 2
Arrays.sort(arr);
// Steps 3, 4, and 5
int b = 0;
int c = i - 1;
// Step 6
while (b < c) {
// Step 6c
+ Math.sqrt(arr[i]));
b++;
c--;
// Steps 6a and 6b
b++;
} else {
c--;
1. This is the initial state. The elevator is on the ground floor and five
people are ready to take it. Let's consider that the minimum time
is 0 (so, 0 units of time).
2. In the elevator, we take the people who are going to the 4th floor
and the one person who is going to the 2nd floor. Remember that
we can take a maximum of three people at a time. So far, the
minimum time is 0.
3. The elevator goes up and stops at the 2nd floor. One person gets
off. Since each floor represents a unit of time, we have a minimum
time of 2.
4. The elevator goes up and stops at the 4th floor. The remaining two
people get off. The minimum time becomes equal to 4.
5. At this step, the elevator is empty. It must go down to the ground
floor to pick up more people. Since it goes down four floors, the
minimum time becomes 8.
6. We pick up the remaining two people. The minimum time remains
as 8.
7. The elevator goes up and stops at the 1st floor. One person gets off.
The minimum time becomes 9.
8. The elevator goes up and stops at the 2nd floor. One person gets
off. The minimum time becomes 10.
9. At this step, the elevator is empty. It must go down to the ground
floor. Since it goes down two floors, the minimum time becomes
12.
int aux;
aux = floors[i];
floors[i] = floors[j];
floors[j] = aux;
}
int time = 0;
time += (2 * floors[i]);
return time;
Sectors
Nearest elevator
https://github.com/topics/elevator-simulation
https://austingwalters.com/everyday-algorithms-elevator-
allocation/.
Summary
In this chapter, we covered the most popular problems
that fit into the mathematics and puzzles categories.
While many companies avoid such problems, there are
still major players such as Google and Amazon that rely
on these kinds of problems in their interviews.
Concurrency
Developing single-threaded Java applications is rarely
feasible. Therefore, most of your projects will be
multithreaded (that is, they will run in a multithreaded
environment). This means that, sooner or later, you'll
have to tackle certain multithreading problems. In other
words, at some point, you'll have to get your hands dirty
with code that manipulates Java threads directly or via
dedicated APIs.
Technical Requirements
The codes used in this chapter can be found on GitHub
on: https://github.com/PacktPublishing/The-Complete-
Coding-Interview-Guide-in-Java/tree/master/Chapter16
Java concurrency
(multithreading) in a nutshell
Our computers can run multiple programs or
applications at the same time (for example, we can listen
to music on a media player and navigate the internet at
the same time). A process is an executing instance of a
program or application (for example, by double-clicking
on the NetBeans icon on your computer, you start a
process that will run the NetBeans program).
Additionally, a thread is a lightweight subprocess that
represents the smallest executable unit of work of a
process. A Java thread has relatively low overhead, and it
shares common memory space with other threads. A
process can have multiple threads with one main thread.
Important note
Important note
Important note
The NEW state: A thread that is created but not started (this is
the state until the Thread#start() method is invoked).
The WAITING state: A thread, t1, that waits (without having set
an explicit timeout period) for another thread, t2, to finish its job
is in the WAITING state.
In the code bundle for this book, you can find a simple
example of causing a deadlock named Deadlock.
In the code bundle for this book, you can find simple
examples of using Executor and
ThreadPoolExecutor in the application named
ExecutorAndExecutorService.
void run()
In the code bundle for this book, you can find simple
examples of using Runnable and Callable in the
application named RunnableAndCallable.
Coding challenge 7 – Starvation
Problem: Explain what thread starvation is.
Notice that the volatile variables are not a good fit for
read-modify-write scenarios. For such scenarios, we will
rely on atomic variables (for example, AtomicBoolean,
AtomicInteger, and AtomicReference).
Note
If the data buffer is not empty, then the consumer consumes one
product (by removing it from the data buffer).
As long as the data buffer is not empty, the producer waits.
THE PRODUCER
If the queue is not empty, then the producer waits until
the consumer finishes. To do this, the producer relies on
the wait() method, as follows:
synchronized (queue) {
while (!queue.isEmpty()) {
queue.wait();
}
On the other hand, if the queue is empty, then the
producer enqueues one product and notifies the
consumer thread via notify(), as follows:
synchronized (queue) {
Thread.sleep(rnd.nextInt(MAX_PROD_TIME_MS));
queue.add(product);
queue.notify();
THE CONSUMER
If the queue is empty, then the consumer waits until the
producer finishes. For this, the producer relies on the
wait() method, as follows:
synchronized (queue) {
while (queue.isEmpty()) {
queue.wait();
}
On the other hand, if the queue is not empty, then the
consumer dequeues one product and notifies the
producer thread via notify(), as follows:
synchronized (queue) {
if (product != null) {
Thread.sleep(rnd.nextInt(MAX_CONS_TIME_MS));
queue.notify();
THE PRODUCER
The producer waits for the consumer to be available via
hasWaitingConsumer():
while (queue.hasWaitingConsumer()) {
String product = "product-" + rnd.nextInt(1000);
Thread.sleep(rnd.nextInt(MAX_PROD_TIME_MS));
queue.add(product);
THE CONSUMER
The consumer uses the poll() method with a timeout to
extract the product:
MAX_PROD_TIME_MS * 2,
TimeUnit.MILLISECONDS);
if (product != null) {
Thread.sleep(rnd.nextInt(MAX_CONS_TIME_MS));
Functional-Style
Programming
As you probably know, Java is not a purely functional
programming language like Haskell, but starting with
version 8, Java has added some functional-style support.
The effort of adding this support was a success and
functional-style code was widely adopted by developers
and companies. Functional-style programming sustains
code that is more understandable, maintainable, and
testable. However, writing Java code in the functional
style requires serious knowledge of lambdas, the stream
API, Optional, functional interfaces, and so on. All
these functional programming topics can be interview
topics as well and, in this chapter, we will cover some of
the hot questions that are mandatory to know for passing
a regular Java interview. Our agenda contains the
following topics:
Java functional-style
programming in a nutshell
As usual, this section is meant to highlight and refresh
the main concepts of our topic and to provide a
comprehensive resource for answering the fundamental
questions that may occur in a technical interview.
Key concepts of functional-style
programming
So, the key concepts of functional programming include
the following:
Pure functions
Higher-order functions
PURE FUNCTIONS
A pure function is a function whose execution has no
side effects and the return value depends only on its
input parameters. The following Java method is a pure
function:
return x + y;
}
If a method uses member variables or mutates the states
of a member variable, then it is not a pure function.
HIGHER-ORDER FUNCTIONS
A higher-order function takes one or more functions as
parameters and/or returns another function as a result.
Java emulates higher-order functions via lambda
expressions. In other words, in Java, a higher-order
function is a method that gets one (or more) lambda
expressions as arguments and/or returns another
lambda expression.
return x.compareTo(y);
});
No state
No side effects
Immutable variables
NO SIDE EFFECTS
By no side effects, we should understand that a function
cannot change (mutate) any state outside of the function
(outside of its functional scope). State outside of a
function includes the following:
IMMUTABLE VARIABLES
Functional programming encourages and sustains the
usage of immutable variables. Relying on immutable
variables helps us to avoid side effects in a much easier
and more intuitive way.
long result = 1;
for (; n > 0; n--) {
result *= n;
return result;
}
Alternatively, looping can be achieved via the Java
Stream API, which is functionally inspired:
return LongStream.rangeClosed(1, n)
On the left of the arrow, there are the parameters of this lambda
that are used in the lambda body. In this example, these are the
parameters of the FilenameFilter.accept(File folder, String
fileName) method.
The arrow that sits between the list of parameters and the body of
a lambda acts as a separator.
@Override
};
@FunctionalInterface
.map(Integer::parseInt)
.collect(Collectors.toList());
.flatMap(List::stream)
.collect(Collectors.toList());
Arrays.asList("Gac", "Cantaloupe"),
melonLists.stream()
.map(Collection::stream) // Stream<Stream<String>>
.distinct();
.flatMap(Collection::stream) // Stream<String>
.distinct()
.collect(Collectors.toList());
List<Integer> ints
.filter(i -> i != 0)
.collect(Collectors.toList());
addresses.stream()
.sorted()
.collect(Collectors.toList());
@FunctionalInterface
return Arrays.stream(arr);
}
2. Second, we can use Stream#of():
return Stream.of(arr);
}
3. The last technique is via List#stream():
return Arrays.asList(arr).stream();
}
2. Secondly, by using IntStream#of():
return IntStream.of(arr);
return Arrays.stream(segments)
.sum();
}
Since Polygon has a single abstract method, it is a
functional interface as well. So, it can be annotated with
@FunctionalInterface.
Spliterators.spliteratorUnknownSize(
your_Iterator, your_Properties);
In the book Java Coding Problems
(https://www.amazon.com/gp/product/B07Y9BPV4W/)
, you can find more details on this topic, including a
complete guide for writing a custom Spliterator.
Summary
In this chapter, we've covered several hot topics
regarding functional-style programming in Java. While
this topic is quite extensive, with many books dedicated
to it, the questions covered here should be enough to
pass a regular Java interview that covers the main
features of the Java 8 language.
Unit Testing
As a developer (or software engineer), you must have
skills in the testing field as well. For example, developers
are responsible for writing the unit tests of their code
(for example, using JUnit or TestNG). Most probably, a
pull request that doesn't contain unit tests as well won't
be accepted.
Let's begin!
Technical Requirements
The codes used in this chapter can be found on GitHub
on: https://github.com/PacktPublishing/The-Complete-
Coding-Interview-Guide-in-Java/tree/master/Chapter18
@Test
// Arrange
// Act
// Assert
assertEquals(6, sum);
@Test
try {
theStream.findAny().get();
fail("Expected a NoSuchElementException to be
thrown");
@Test(expected = NoSuchElementException.class)
theStream.findAny().get();
@Rule
@Test
throws NoSuchElementException {
thrown.expect(NoSuchElementException.class);
theStream.findAny().get();
Via this approach, you can test the value of the exception
message. These examples have been grouped into an
application called junit4/TestingExceptions.
@Test
theStream.findAny().get();
});
@Test
Throwable ex = assertThrows(
NoSuchElementException.class, () -> {
theStream.findAny().get();
});
@RunWith(Suite.class)
@Suite.SuiteClasses({
TestConnect.class,
TestHeartbeat.class,
TestDisconnect.class
})
@RunWith(JUnitPlatform.class)
@SelectPackages({
"coding.challenge.connection.test",
"coding.challenge.login.test"
})
@RunWith(JUnitPlatform.class)
@SuiteDisplayName("TEST CONNECTION")
@SelectClasses({
TestConnect.class,
TestHeartbeat.class,
TestDisconnect.class
})
@Test
public void
givenFolderWhenGetAbsolutePathThenSuccess() {
assumeThat(File.separatorChar, is('/'));
assertThat(new File(".").getAbsolutePath(),
is("C:/SBPBP/GitHub/Chapter18/junit4"));
@Test
public void
givenFolderWhenGetAbsolutePathThenSuccess() {
assumingThat(File.separatorChar == '/',
() -> {
assertThat(new File(".").getAbsolutePath(),
is("C:/SBPBP/GitHub/Chapter18/junit5"));
});
// run these assertions always, just like normal test
assertTrue(true);
1: @TestFactory
2: Stream<DynamicTest> dynamicTestsExample() {
3:
5:
7:
13: dynamicTests.add(dynamicTest);
14: }
15:
17: }
The static members are not allowed in inner classes, which means
that the @BeforeAll and @AfterAll methods cannot be used in
nested tests.
@RunWith(JUnitPlatform.class)
= Logger.getLogger(NestedTest.class.getName());
@Test
void test1() {
@Nested
class A {
@BeforeEach
void beforeEach() {
@AfterEach
void afterEach() {
@Test
void test2() {
Summary
In this chapter, we covered several hot questions and
coding challenges about unit testing via JUnit4 and
JUnit5. It is important to not neglect this topic. Most
likely, in the last part of an interview for a Java developer
or software engineer position, you'll get several questions
related to testing. Moreover, those questions will be
related to unit testing and JUnit.
System Scalability
Scalability is, for sure, one of the most critical demands
for the success of a web application. An application's
capacity to scale depends on the whole system
architecture, and building a project while having
scalability in mind is the best way to go. You'll be very
thankful later when the success of the business may
require the application to be highly scalable due to heavy
loads of traffic.
Scalability in a nutshell
Scalability in a nutshell
The most predictable yet important question your
interviewer will ask you is: What is scalability?
Scalability is the capability and ability of a process
(system, network, application) to cope with an increase
in workload (by workload, we understand anything that
pushes the system to the limit, such as traffic, storage
capacity, a maximum number of transactions, and so on)
when adding resources (typically hardware). Scalability
can be expressed as the ratio between the increase in
system performance and the rise in resources used.
Moreover, scalability also means the ability to add extra
resources without affecting/modifying the structure of
the main nodes.
Choose hosting and tools: Scaling is not only about the code!
The infrastructure counts a lot as well. Today, many cloud players
(for example, Amazon) provide autoscaling and dedicated tools
(Docker, Kubernetes, and so on).
Test and monitor: Testing and monitoring your code will help
you to discover issues as soon as possible.
If a node fails, then it affects only its users (other nodes continue
to work).
Having a linear and theoretically infinite scalability, the
SN architecture is quite popular. Google is one of the
major players that relies on SN.
Let's consider a path such as Ana -> Bob -> Carla -> Dan
-> Elvira, where each person has 100 friends. A
unidirectional BFS will traverse 100 million (1004)
nodes. A bidirectional BFS will traverse only 20,000
nodes (2 x 1002).
How do you assign a unique identifier (ID) for each given URL?
How do you track the stats of each answer (the total number of
views, voting, and so on)?
How do you design user features such as upload, search, view, and
share files/photos?
How do you tackle the social graph (see Coding challenge 13)?
How do you detect the best matches for the already typed string?
How do you tackle a case when the user is typing too fast?
How do you guarantee that the crawler is not stuck on the same
domain forever?
Summary
This is the last chapter of this book. We've just covered a
bunch of problems that fit into the scalability topic.
Anghel Leonard
ISBN: 9781789801415
Solve strings and number problems using the latest Java APIs
Nick Samoylov
ISBN: 9781789957051
Gain insights into data structures and understand how they are
used in Java
Find out what streams are and how they can help in data
processing