0% found this document useful (0 votes)
14 views

Java Algorithms Interview Challenger

The document is a book titled 'Java Interview Challenger' by Rafael Chinelato del Nero, aimed at helping readers master the fundamentals of data structures and algorithms for Java interviews. It covers various topics including interview processes, memory allocation, Big O notation, and data structures like arrays, strings, hashtables, linked lists, stacks, queues, graphs, and trees. The book was published on September 21, 2023, and is available for purchase on Leanpub.

Uploaded by

liza.shuba1999
Copyright
© © All Rights Reserved
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
14 views

Java Algorithms Interview Challenger

The document is a book titled 'Java Interview Challenger' by Rafael Chinelato del Nero, aimed at helping readers master the fundamentals of data structures and algorithms for Java interviews. It covers various topics including interview processes, memory allocation, Big O notation, and data structures like arrays, strings, hashtables, linked lists, stacks, queues, graphs, and trees. The book was published on September 21, 2023, and is available for purchase on Leanpub.

Uploaded by

liza.shuba1999
Copyright
© © All Rights Reserved
Available Formats
Download as PDF, TXT or read online on Scribd
You are on page 1/ 194

Java Interview Challenger

Ace Java Interviews by Mastering


Fundamentals of Data Structures and
Algorithms

Rafael Chinelato del Nero


This book is for sale at
http://leanpub.com/java_interview_challenger

This version was published on 2023-09-21

This is a Leanpub book. Leanpub empowers authors and


publishers with the Lean Publishing process. Lean Publishing is
the act of publishing an in-progress ebook using lightweight tools
and many iterations to get reader feedback, pivot until you have
the right book and build traction once you do.

© 2023 Rafael Chinelato del Nero

https://t.me/javalib
Contents

Java Interview Process . . . . . . . . . . . . . . . . . . . . . 1


Interview Mindset . . . . . . . . . . . . . . . . . . . . . . . 1
CV . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2
Market Yourself . . . . . . . . . . . . . . . . . . . . . . . . 2
Interview Modalities . . . . . . . . . . . . . . . . . . . . . 6
Take Home Project . . . . . . . . . . . . . . . . . . . . . . 7
Code Quality . . . . . . . . . . . . . . . . . . . . . . . . . . 7
Code Design . . . . . . . . . . . . . . . . . . . . . . . . . . 8
Algorithms Interview . . . . . . . . . . . . . . . . . . . . . 9
Summary . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11

Memory Allocation . . . . . . . . . . . . . . . . . . . . . . . 13
Data in Binary Number . . . . . . . . . . . . . . . . . . . . 13
Contiguous Memory Slots Allocation . . . . . . . . . . . . 14
Array Allocation . . . . . . . . . . . . . . . . . . . . . . . 15
Static Memory Allocation . . . . . . . . . . . . . . . . . . 17
Dynamic Memory Allocation . . . . . . . . . . . . . . . . 17
Summary . . . . . . . . . . . . . . . . . . . . . . . . . . . . 17

Big O Notation . . . . . . . . . . . . . . . . . . . . . . . . . . 19
Asymptotic Notations . . . . . . . . . . . . . . . . . . . . . 19
Big O Notation in Practice . . . . . . . . . . . . . . . . . . 20
Constant – O(1) . . . . . . . . . . . . . . . . . . . . . . . . 20
Accessing an Array by Index . . . . . . . . . . . . . . . . 22
Logarithmic – O(log n) . . . . . . . . . . . . . . . . . . . . 22
Linear – O(n) . . . . . . . . . . . . . . . . . . . . . . . . . 25

https://t.me/javalib
CONTENTS

Two Inputs – O(m + n) . . . . . . . . . . . . . . . . . . . . 26


Log-linear – O(n log n) . . . . . . . . . . . . . . . . . . . . 27
Quadratic – O(n²) . . . . . . . . . . . . . . . . . . . . . . . 28
Cubic – O(n ^ 3) . . . . . . . . . . . . . . . . . . . . . . . . 29
Exponential – O(c ^ n) . . . . . . . . . . . . . . . . . . . . 30
Brute-force Password Break . . . . . . . . . . . . . . . . . 31
Factorial O(n!) . . . . . . . . . . . . . . . . . . . . . . . . . 31
Summary . . . . . . . . . . . . . . . . . . . . . . . . . . . . 32

Array . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 35
Array Memory Allocation . . . . . . . . . . . . . . . . . . 35
Static Arrays . . . . . . . . . . . . . . . . . . . . . . . . . . 36
Insert an Element in the Middle of the Array . . . . . . . 37
Dynamic Arrays . . . . . . . . . . . . . . . . . . . . . . . . 38
Summary . . . . . . . . . . . . . . . . . . . . . . . . . . . . 39

String . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 41
Get a character from a String . . . . . . . . . . . . . . . . 43
Copy a String . . . . . . . . . . . . . . . . . . . . . . . . . 44
String Encodings What to Use? . . . . . . . . . . . . . . . 47
Summary . . . . . . . . . . . . . . . . . . . . . . . . . . . . 49

Hashtable . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 50
What is a Hash Function? . . . . . . . . . . . . . . . . . . 51
Hash function Collision . . . . . . . . . . . . . . . . . . . 52
Hash collision in practice with Java . . . . . . . . . . . . . 54
Optimizing Hash Collision in Java . . . . . . . . . . . . . 57
Optimizing Hash Collision in Java . . . . . . . . . . . . . 60
Summary . . . . . . . . . . . . . . . . . . . . . . . . . . . . 62

Linked List . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 64
Singly Linked List Structure . . . . . . . . . . . . . . . . . 64
Add Elements to Linked List . . . . . . . . . . . . . . . . . 66
Search Element from Linked List . . . . . . . . . . . . . . 68
Inserting Element in the middle of a Linked List . . . . . 70
Doubly Linked List . . . . . . . . . . . . . . . . . . . . . . 71

https://t.me/javalib
CONTENTS

Circular Linked List . . . . . . . . . . . . . . . . . . . . . . 74


Pros from Linked List . . . . . . . . . . . . . . . . . . . . . 75
Cons from Linked List . . . . . . . . . . . . . . . . . . . . 75
Summary . . . . . . . . . . . . . . . . . . . . . . . . . . . . 75

Stack . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 77
Inserting and Removing Elements from a Stack with Java 77
Stack inherits Vector . . . . . . . . . . . . . . . . . . . . . 79
Using Stack with Deque and ArrayDeque . . . . . . . . . 80
Time Complexity from Stack . . . . . . . . . . . . . . . . 82
Summary . . . . . . . . . . . . . . . . . . . . . . . . . . . . 83

Queue . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 85
What is Queue? . . . . . . . . . . . . . . . . . . . . . . . . 85
Inserting Elements at the Start and the End of the Queue 88
Deleting Elements at the Start and End of the Queue . . . 89
Big(O) Notation – Time Complexity of a Queue . . . . . 90
Delete the first element of the queue . . . . . . . . . . . . 92
Summary . . . . . . . . . . . . . . . . . . . . . . . . . . . . 94

Graph . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 95
Undirected Graph . . . . . . . . . . . . . . . . . . . . . . . 98
Directed Graph . . . . . . . . . . . . . . . . . . . . . . . . 99
Acyclic and Cyclic Graph . . . . . . . . . . . . . . . . . . 101
Summary . . . . . . . . . . . . . . . . . . . . . . . . . . . . 104

Tree . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 105
Binary Tree . . . . . . . . . . . . . . . . . . . . . . . . . . . 108
Ternary Tree . . . . . . . . . . . . . . . . . . . . . . . . . . 109
K-ary tree . . . . . . . . . . . . . . . . . . . . . . . . . . . 110
Perfect Binary Tree . . . . . . . . . . . . . . . . . . . . . . 111
Complete Binary Tree . . . . . . . . . . . . . . . . . . . . . 111
Full Binary Tree . . . . . . . . . . . . . . . . . . . . . . . . 112
Balanced Binary Tree . . . . . . . . . . . . . . . . . . . . . 113
Summary . . . . . . . . . . . . . . . . . . . . . . . . . . . . 117

https://t.me/javalib
CONTENTS

Recursion . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 118
Seeing LIFO/FILO in practice . . . . . . . . . . . . . . . . 120
Recursion Tree with Fibonacci . . . . . . . . . . . . . . . . 122
Big(O) Notation for Recursive Fibonacci . . . . . . . . . . 124
Summary . . . . . . . . . . . . . . . . . . . . . . . . . . . . 125

Logarithm . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 127
Logarithm Time Complexity in Binary Search . . . . . . 127
Summary . . . . . . . . . . . . . . . . . . . . . . . . . . . . 130

Bubble Sort . . . . . . . . . . . . . . . . . . . . . . . . . . . . 131


Optimized Bubble Sort . . . . . . . . . . . . . . . . . . . . 134
Big (O) Notation Complexity . . . . . . . . . . . . . . . . 135
Summary . . . . . . . . . . . . . . . . . . . . . . . . . . . . 136

Insertion Sort . . . . . . . . . . . . . . . . . . . . . . . . . . . 137


Big(O) Notation . . . . . . . . . . . . . . . . . . . . . . . . 140
Summary . . . . . . . . . . . . . . . . . . . . . . . . . . . . 140

Selection Sort . . . . . . . . . . . . . . . . . . . . . . . . . . . 142


Selection Sort By Comparing Element Lowest Value . . . 145
Big (O) Notation Complexity . . . . . . . . . . . . . . . . 146
Summary . . . . . . . . . . . . . . . . . . . . . . . . . . . . 146

Quicksort . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 148
Choosing the pivot with the Quicksort Algorithm . . . . 148
Creating Partitions with the Quicksort Algorithm . . . . 148
Creating Partition with Left and Right Pointers . . . . . . 151
Summary . . . . . . . . . . . . . . . . . . . . . . . . . . . . 152

Merge Sort . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 154


Merge Sort Code with Java . . . . . . . . . . . . . . . . . . 156
When to Use Merge Sort? . . . . . . . . . . . . . . . . . . 158
When to NOT Use Merge Sort? . . . . . . . . . . . . . . . 159
Pros and Cons of Merge Sort . . . . . . . . . . . . . . . . . 160
Summary . . . . . . . . . . . . . . . . . . . . . . . . . . . . 161

https://t.me/javalib
Depth-first-search . . . . . . . . . . . . . . . . . . . . . . . . 162
Preorder Traversal . . . . . . . . . . . . . . . . . . . . . . . 165
Recursive Preorder Traversal without Lambda . . . . . . 165
Recursive Preorder Traversal with Lambda . . . . . . . . 166
Preorder with Looping . . . . . . . . . . . . . . . . . . . . 167
Postorder Traversal . . . . . . . . . . . . . . . . . . . . . . 168
Recursive Postorder traversal without Lambdas . . . . . . 168
Recursive Postorder traversal with Lambda . . . . . . . . 169
Postorder traversal with looping . . . . . . . . . . . . . . 169
Inorder Traversal . . . . . . . . . . . . . . . . . . . . . . . 171
Recursive In-order Traversal . . . . . . . . . . . . . . . . . 171
In-order Traversal with Looping . . . . . . . . . . . . . . . 173
Big (O) Notation for Depth First Search . . . . . . . . . . 174
Summary . . . . . . . . . . . . . . . . . . . . . . . . . . . . 174

Breadth-First Search . . . . . . . . . . . . . . . . . . . . . . . 176


Breadth-First Search with a Tree . . . . . . . . . . . . . . 177
Breadth-First Search Traversal with Graph . . . . . . . . 180
Breadth-First Search Big(O) Notation . . . . . . . . . . . . 183
Summary . . . . . . . . . . . . . . . . . . . . . . . . . . . . 184

Next Steps . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 185


Fundamentals are the Key . . . . . . . . . . . . . . . . . . 185
Be Risk Taker . . . . . . . . . . . . . . . . . . . . . . . . . 186
Don’t Focus Only On Technical Skills . . . . . . . . . . . 186
Interview Mindset . . . . . . . . . . . . . . . . . . . . . . . 186
Conclusion . . . . . . . . . . . . . . . . . . . . . . . . . . . 187

https://t.me/javalib
Java Interview Process
Interviews are difficult and many times tricky, and if we don’t
know what the rules are to pass in an interview, we are very likely
to fail.
One important point to remember when doing interviews is that
they don’t necessarily test your skills as a software engineer. The
interviews have their gaps, and they could be better. For example,
if it’s been a while since we don’t practice algorithms and need to
know some data structures, we will likely fail the interview.
Therefore, understanding the rules of the game is very important.
Otherwise, we won’t pass on those interviews.
Being rejected in many interviews and feeling frustrated is a normal
feeling. I’ve been rejected in many interviews and felt frustrated,
but we need to remember all the time that interviews will not
define how good we are as software engineers. It will test some
skills, but the day-to-day work is completely different.
That’s why I created this chapter so you can understand the rules
of the interview game so you can pass on those interviews.

Interview Mindset
Don’t be intimidated by the amount of technologies you see in a job
spec because no one really knows all of those technologies in depth.
Instead, knowing what those technologies do is usually enough.
Also, see failing in an interview as a necessary step for your growth.
Failure is just a stepping stone to bring you success. The crucial
point is to learn why we failed, improve and try again.

https://t.me/javalib
Java Interview Process 2

The more we do interviews, the better we get at them. Keep that in


mind and never stop learning to pass in the interview.
The good news is that after reading this book you will have a
strong foundation to solve any kind of algorithm and a broad
understanding of systems design. You will also have some insights
to leverage your career!

CV
Your CV has to have relevant information regarding what the
market is asking for. Suppose you put in your CV that you know
Struts or any other obsolete technology. That won’t matter. That’s
why you must align your CV with the market’s requirements.
That doesn’t mean that you must lie on your CV. You need instead
to be honest because they will ask you about the concepts you
included in your CV during the interview. They might disqualify
you immediately if you don’t know the technology because the
interviewer will notice that.
Make sure to put the biggest highlights you had in your job
experience. On the top of your CV, include how many years
of experience you have and the results you got for previous
companies. Try to use numbers. For example, if you refactored
lots of code from a previous company, you can state on your CV
the following.

• Increased developer’s productivity by 25% by Refactoring


code and creating generic components.

If you think about your previous experiences, you will have plenty
of ideas about the results you brought.

https://t.me/javalib
Java Interview Process 3

Market Yourself
Your CV is a way to market yourself, but an even stronger way to
market yourself is to share your knowledge. You build trust with
people who never met you by sharing your knowledge.
You can also combine your knowledge sharing with your CV. You
can include links from your blog, Youtube channel, or talks you
gave.
Also, you can skip interviews entirely if you are well known to solve
a specific problem that companies need very much. If a person
needs the problem you solve to be solved, they will ask you to join
their company, and they will ask you how much you want to solve
this problem.

Having a blog
Having a blog is a great investment in yourself and your career.
That’s because you will be improving your communications skills
and will be filling out the knowledge gaps you have. The ultimate
way to learn anything is by sharing what you know.
If you decide to create a blog, make sure you are consistent. You
will also need to determine a focus. Meaning what problem you
will solve that will make you stand out for companies. Let’s see
some problems:

• Performance
• Bugs
• Security
• Data
• Cloud Resilience

You can also share your knowledge on something you are learning.
Let’s suppose you are learning AWS concepts; you can share this

https://t.me/javalib
Java Interview Process 4

knowledge since that will be the most powerful way for you to
really master this content.
Decide how often you will post a new article. You can post an
article per week, bi-weekly, or monthly. Once you decide on it, it’s
crucial to stick to it. To stick to it, create a list of 20 article titles,
and you will have articles for almost half a year if you decide to
develop weekly articles.
You can get started creating articles on:

• https://dev.to
• https://medium.com
• https://www.linkedin.com/1

Once you have some content, I recommend creating your own


WordPress blog with the following service:

• https://www.bluehost.com

Youtube Channel

If you prefer videos, create your Youtube channel and share what
you know. Having a focus on a problem that companies face is
also powerful because then the people who see your video and
understand you can help then you will probably be hired without
going through several interview steps.

Giving Talks

You have the option of also giving talks. Giving talks is a great way
for you to market yourself and get visibility. When you give a talk,
1 https://www.linkedin.com

https://t.me/javalib
Java Interview Process 5

even if you are not an expert on the subject, you will be perceived
as an expert.
If you want to start giving talks, start small. Give a talk to a friend
of yours, give a talk online, and then expand gradually.
Also, get involved in Java user groups from your city and help
them in some way. Gain influence and trust, and once you have
something to present, ask them to do a lighting talk of 5 or 15
minutes. That will be a powerful way for you to build up your
confidence, share your knowledge, and gain exposure.

Open Source Projects


Contributing to open-source projects is a powerful way to market
yourself and refine your software development skills. By doing so,
you will massively boost your CV and might even get hired by big
companies.
For example, Red Hat (IBM) hires developers according to their
contributions to their open-source projects. Getting started on the
open-source project world is hard, but you can start small.
Start by changing something very small, maybe a typo in the
documentation, fixing some unit tests, or doing something that
nobody else wants to do. Then, your chances of having a pull
request merges are much higher. The other benefit of starting with
documentation is that you will have a better understanding of the
project.
The more small pull requests you get approved and merged, the
more trust you gain in that project. Then, you are far more
likely to create feature pull requests and make more important
contributions.
You can take a look at the following open-source projects:

• https://github.com/eclipse/jnosql

https://t.me/javalib
Java Interview Process 6

• https://github.com/quarkusio/quarkus
• https://github.com/elastic/elasticsearch
• https://github.com/square/okhttp
• https://github.com/google/guava
• https://github.com/spring-projects/spring-boot
• https://github.com/jenkinsci/jenkins
• https://github.com/google/guice
• https://github.com/mockito/mockito

Create your pull request and get your software engineering skills
to a whole new level!

Interview Modalities
I’ve done many interviews throughout my career, and they were
very different from each other. When I was starting my career,
knowing Object-Oriented programming and basic sort algorithms
was enough
Obviously, as my career progressed, the interviews for higher levels
were more difficult. The interview for an intermediate developer
some time ago was enough to know Java EE, now called Jakarta
EE.

Nowadays, the market has gone almost entirely to the cloud,


and nearly all companies are asking for Microservices concepts.
Therefore, it’s crucial to know what a Microservice is and explain
that during the interview. We also need to know why tests are
important because that will also be asked.
Let’s see now each interview style in more detail. Before we explore
the interview styles, every interview will have the following:

• Screening call: They will ask questions about your past


experiences to check if you are a good candidate for the

https://t.me/javalib
Java Interview Process 7

opportunity. If your experience matches what they are


looking for, you will pass this one.
• Behavioral Interview: Checks if you behave in a suitable way
that aligns with the company’s values. They will usually see
if you complain about your previous company, if you work
well in a team if you are friendly, and if you communicate
clearly.
• Culture Fit: In this interview, it’s important for you to
check what are the company values, then you show those
values in this interview. In short, you need to be friendly,
communicate clearly, and show that you want to bring value
to the company with your work.

Take Home Project


Sometimes companies will opt to give you a take-home project.
They will access your skills to create a robust and performant
system using fundamental concepts of a web application usually.
In one interview I did, they wanted me to create a manual job
application with a Thread pool and a queue. Sometimes, they
will ask you to create something even if there are already several
frameworks that do the job.
Other takeaway tests will ask you to create a normal application
with the problem they are presenting, and your job is to create the
project in a way that is robust, performant, and has a good code
design.
They are expecting an application that is production-ready, which
means well tested, with logs, containerized ready to be deployed in
the cloud, well documented, and with different profiles to deploy
in multiple environments such as dev, stage, qa, and production.
Obviously, they are expecting you to solve the problem they
presented in an optimized way.

https://t.me/javalib
Java Interview Process 8

Code Quality
It’s crucial to know how to create good quality code, not reinvent
the wheel with technologies, and understand what you do with
software development.
One of the most important principles is to create cohesive code
(code that does ONE thing very well) and code with low coupling
(code that doesn’t have strong dependency).
Another crucial point is correctly naming projects, packages,
classes, methods, and variables. Avoid acronyms and use the name
conventions for the Java language.
Mastering paradigms like Object-Oriented Programming (OOP)
and Functional Programming will help develop good-quality code.
SOLID principles are powerful in guiding you to create high-quality
code. Therefore, it’s crucial to learn those concepts well.
The last component to help you create high-quality code is to
master the most essential design patterns. We can’t overuse design
patterns. We should use them only in suitable situations instead.
Otherwise, the code will only get more complicated.

Code Design
We need to have a basic understanding of code design to develop
systems and also to pass on interviews. :)
Sometimes, there will be interviews that will ask you how you
design your code and how many layers you use.
In Java, it’s common to have the controller layer (the one that has
the API endpoints), then the service that has business requirements
implementations, the repository that interacts with the database,
the data objects, and the database entities objects.

https://t.me/javalib
Java Interview Process 9

Design patterns and Domain-Driven Design will also help you to


create a good code design.
Knowing the frameworks you are working with and using their
features also helps you create good code. For example, if you are
using Spring, it’s better to stick with Spring nomenclatures to be
consistent with the framework.

Java Application Interview Style


In this interview, usually two senior developers and maybe one
manager will ask the following questions:

• Java features (Concurrency, Collection, Exception, OOP)


• Tests
• Microservices concepts
• SQL
• Database Model

To learn the Java core concepts, get the Java Challengers


book and stay sharp for the Java interview: https:
//leanpub.com/javachallengers
They will also ask about your previous experiences, so only tell
the experience from the three last companies you worked for.
Otherwise, it’s too much. Also, highlight your accomplishments
when you are talking about your experiences.
They also might ask you about your previous experiences and
model the system you were working with.

Algorithms Interview
This interview is nowadays very popular. A lot of companies
(small, large companies) are following the same process for inter-
views.

https://t.me/javalib
Java Interview Process 10

Similarly to the Java application interview, there will usually be


two senior developers interviewing you for a code challenge.
In the algorithm interview, you will be accessed the following:

• Your ability to understand the problem


• Your ability to communicate your thought process
• You must communicate your thought process while you solve
the problem
• Your ability to communicate clearly
• Your code quality
• Data Structures
• Big O Notation
• Algorithms techniques such as recursion or memoization

Systems Design Interview

In the Systems Design interview, you will be assessed in the ability


to design a system according to the requirements. This interview
is tricky if you don’t know the rules of the game. In this interview,
they will usually give you a vague problem, such as “Design
Instagram.” Then you must ask questions about where you can
reasonably design a system in 45 minutes.
Don’t ever start designing the system without asking questions.
You must assume things during this interview. So if they tell you
to design Instagram, you say, “I assume you just want the basic
features, such as the possibility to post photos and stories, right?”.
They will probably say yes, then you start it.
Make questions to the interviewer directing the system to be simple;
the simpler you can make the system, the better for you. They
might ask you to design any system. They might ask you, for
example, to design an internal system they have. Then you will

https://t.me/javalib
Java Interview Process 11

have to read and understand their problem and then design the
system explaining why of your choices.
In the Systems Design interview, it’s crucial that you have some
knowledge of how to create a system.

Summary
Interviews are annoying, but it’s necessary to know what are the
rules of those interviews. Otherwise, we will fail no matter how
good of software engineers we are.
A good motivation, though, is to think that by getting good with
data structures, algorithms, and Systems Design, we will become
better software engineers.
Therefore, let’s see what were the main points of this article:

• Your CV must be polished, but it is not the only way to market


yourself.
• Sharing your knowledge is a powerful way to market your-
self.
• You can use links from the knowledge you shared on your
CV.
• You can create your blog to market yourself.
• Decide the focus you want to explore to share your knowl-
edge.
• Decide how often you will create an article. Weekly, bi-
weekly, monthly.
• Share your knowledge on blogs, Youtube, or giving talks.
• Contribute with open source projects, create a PR for a
documentation typo, and anything else that is small.
• There are different interview modalities.
• Screening call only check if your experience is enough.

https://t.me/javalib
Java Interview Process 12

• Behavioral interviews focus on how friendly you are, how


good you communicate, and how well you work in a team.
• Culture fit interview evaluates if your values are similar to
the company’s values.
• Take home project. They will test your ability to create a
production-ready system.
• Code quality will see how well you can use the best program-
ming practices to make your code simple.
• Code design will check how you divide your code into layers.
In Java, it’s usually Controller, Service, Repository, Entity
data, and transfer data.
• Java Application interview will test your knowledge in the
Java language. A few concepts about Microservices and
databases as well.
• Algorithms, you will solve a code challenge, and you need
to know about data structures, Big O notation, and how to
communicate your thinking process.
• Systems Design, you will have a vague problem such as
“design Youtube” and you will have to ask questions assum-
ing that they want only the main features. Then you use
your expertise to design the system, considering what the
requirements are.

https://t.me/javalib
Memory Allocation
Every time we create a variable, invoke a method, or create an
instance memory allocation will happen in Java and any other
programming language. Data is stored in the form of bits, each
memory slot can hold 1 byte which is the same as 8 bits.
Let’s see the chart of how many bits each variable from Java uses:

Type Data Size Data Value Range


boolean 1 bit false, true
byte 1 byte -128 to 127
short 2 bytes 0 (‘\u0000’) to 65535 (‘\uffff’)
char 2 bytes -32768 to 32767
int 4 bytes -2147483648 to 2147483647
long 8 bytes 1-9223372036854775808 to
9223372036854775807
float 4 bytes 1.40239846e-45f to
3.40282347e+38f
double 8 bytes 4.94065645841246544e-324 to
1.79769313486231570e+308

Data in Binary Number


Computers only understand binary numbers, therefore, every data
stored in memory will be transformed into a binary number. A
binary number is a number with a base 2 which means that the
data must be represented by only 0 or 1.
The int number for example has 4 bytes which translates to 32 bits.

https://t.me/javalib
Memory Allocation 14

Let’s see how the decimal number 1 is represented by a binary


number:

1 public class BinaryRepresentation {


2
3 public static void main(String[] args) {
4 int number1 = 1;
5 int number10 = 10;
6
7 showBinaryNumber(number1);
8 showBinaryNumber(number10);
9 }
10
11 private static void showBinaryNumber(int number) {
12 var binaryNumber = String.format("%32s", Integer
13 .toBinaryString(number)).replace(\
14 ' ', '0');
15 System.out.println(binaryNumber);
16 }
17
18 }

Output: 00000000000000000000000000000001 00000000000000000000000000001010


The zeros on the left don’t make any impact in a binary number
but I added the zeros on the left to show you that an int value has
32 bits and that’s the space that will be taken in the memory slot.
Since creating a variable, and invoking a method are elementary
operations the computer is able to find a memory slot given the
memory address very quickly.

Contiguous Memory Slots Allocation


As mentioned before, each memory slot holds 1 byte. Let’s see in
the following diagram the representation of how a boolean, short,

https://t.me/javalib
Memory Allocation 15

int, and double types are stored in memory:

Figure 1. Memory Allocation

Array Allocation
An array also needs to have data allocated contiguously meaning
that if there is an array of boolean with 10 elements, these arrays
need to occupy 10 bytes, it can’t be broken. Otherwise, it would
impossible to have quick access to its elements.
Now, remember that the boolean type takes 1 bit but each memory
slot only stores 1 byte. The JVM (Java Memory Model) also avoids
word tearing which basically forces values not to be broken down
into the same memory slot. Also, word tearing should avoid
changing multiple fields in the same memory slot.
Therefore, each boolean value will hold 1 byte of space, other than
that, we will need 10 contiguous bytes to allocate an array of 10
boolean values. Internally the JVM will store more bytes for the
array. We don’t need to know this in detail but if you are curious
you can run your own tests with the library JOL (Java Object
Layout).

https://t.me/javalib
Memory Allocation 16

Since an array in Java is an object, there will be extra bytes space


because the JVM will allocate in memory called as object header.
In the header, there is the mark word memory allocation which
will store the Garbage Collector metadata, identity hashcode, and
locking information which takes around 8 bytes.
Klass is part of the header too and it’s used to store class metadata,
the JVM will use 4 bytes for that. Also, there is the extra space of
memory the JVM will allocate for the array length.
The JVM will allocate approximately 17 extra bytes when creating
an array object.
However, there is no need to know that in detail, the most important
thing you must remember is that arrays will take memory space
contiguously. You will learn more about arrays in the chapter about
Arrays.
Therefore, let’s see how an array of 10 elements would be repre-
sented NOT considering the extra space the JVM will create for the
array object:

Figure 2. Array Memory Allocation

https://t.me/javalib
Memory Allocation 17

Static Memory Allocation


Static memory is the stack memory in Java. The stack memory is
used to allocate space from local variables, and methods invoca-
tions in the LIFO (Last-in Last-out) style.
A very important point from the stack memory is that when a
method finishes its execution the variable and the method will be
removed from the stack. When using the stack memory there won’t
be problems with threads collisions because each thread has its own
stack.
Also, the access pattern from the stack is easy enough to allocate
and deallocate memory it is faster than the dynamic memory (heap
memory).

Dynamic Memory Allocation


The dynamic memory in Java is the heap memory. The heap
memory is used to store objects and the references of these objects
are stored in the stack memory.
The heap memory can be accessed globally in the application. It’s
also more complex than the stack memory. In the heap memory,
objects need to be collected non-used objects by the Garbage
Collector. Also, since the objects are shared in the application, the
heap memory is not thread-safe.

Summary
The main focus of this chapter is to show you how variables and
methods are stored in memory, not necessarily to show you how the

https://t.me/javalib
Memory Allocation 18

Java memory model works because that would be another whole


book.
Let’s see the key points of this chapter:

• Each memory slot holds 1 byte.


• 1 byte is the same as 8 bits. boolean stores 1 bit but will be
stored in 1 byte to avoid having different data in the same
memory slot.
• Every variable value behind the scenes becomes a binary
number so the computer can understand it.
• An array will occupy memory space contiguously, this means
data has to be stored from back to back.
• An array in Java will occupy more space in memory due to
the internal JVM configurations.
• The static memory in Java is the stack memory.
• The stack memory will keep methods and variables alive until
they are finished. It’s also thread-safe.
• The dynamic memory in Java is the heap memory.
• The heap memory in Java is more complex and it’s mainly
responsible to manage instances of objects.

https://t.me/javalib
Big O Notation
The Big(O) Notation is a fundamental concept to help us measure
the time complexity and space complexity of the algorithm. In
other words, it helps us measure how performant and how much
storage and computer resources an algorithm uses.
Also, in any coding interview, you will be required to know Big(O)
notation. That will help you to get your dream job since every
FAANG (Meta, Alphabet, Amazon, Apple, Netflix, Microsoft,
Google) company will ask you that, even other companies.
The Big(O) notation is not 100% accurate, instead, it’s an estimate
of how much time and space your algorithm will take. Another
rule is that the Big(O) notation will be calculated considering the
worst-case scenario.
Now that we have some understanding regarding Big(O) notation,
let’s see the following diagram from the fastest O(1) to the slowest
O(n!) time complexity:
Source from (https://www.bigocheatsheet.com)[https://www.bigocheatsheet.com]

Asymptotic Notations
The asymptotic notation is a defined pattern to measure the perfor-
mance and memory usage of an algorithm. Even though the Omega
and Theta notations are not very much used in coding interviews,
it’s important to know they exist.
Let’s see what are those asymptotic notations:

• Omega Notation “Ω”: best-case scenario where the time com-


plexity will be as optimal as possible based on the input.

https://t.me/javalib
Big O Notation 20

• Theta Notation “Θ”: average-case scenario where the time


complexity will be the average considering the input.
• Big-O Notation “O”: worst-case scenario and the most used
in coding interviews. It’s the most important operator to
learn because we can measure the worst-case scenario time
complexity of an algorithm.

Big O Notation in Practice


To see those notations in practice, let’s take the example of the
Bubble sort algorithm. If you don’t remember this algorithm, it
sorts the elements from an array using two loopings, checking
element by element. We will further explore the Bubble sort in
the next chapters.
The best-case scenario (Omega notation Ω) would be when the
array is already sorted. In this case, it would be necessary to run
through the array only once. Therefore the time complexity would
be Ω(n).
The average-case scenario (Theta notation Θ) would be when the
array has only a couple of elements unordered. In this case, it
wouldn’t be necessary to run through the whole algorithm. Even
though this is a bit better, the time complexity is still considered as
O(n ^ 2).
The worst-case scenario (Big O notation) would be when the array
is fully unsorted and the whole array has to be traversed. It would
be necessary to sort the whole array, therefore, the time complexity
would be O(n ^ 2).
The space complexity from the Bubble sort algorithm will be
always O(1) since we only change values in place from an array.
Changing values in place means that we don’t need to create a new
array, we only change array values.

https://t.me/javalib
Big O Notation 21

Constant – O(1)
In practice, we need to measure the Big(O) notation by checking
the number of an elementary operation. In the case where we are
creating an int number in memory, we are storing 8 bytes. If we
were to be very precise with the Big(O) time complexity we would
have to write O(8). But notice that this is a constant time. It doesn’t
depend on any external number. It’s also irrelevant, O(8) doesn’t
really change much performance. For this reason, we can consider
that O(8) is actually O(1).
When assigning a variable this will take the time complexity of
O(1) because it’s only one operation. Remember that the Big
(O) notation will not measure precisely the performance of the
algorithm. Therefore, the O(1) time complexity is an abstract way
to measure code performance. Keep in mind that O(1) is pretty fast
though. It’s doing only one light operation:
int number = 7; // O(1) time and space complexity If we have
a code with many operations and we are not using any kind of
external parameter that will change the time complexity, it will still
be considered as O(1). Let’s see the following code:

1 public class O1Example {


2
3 public static void main(String[] args) {
4 int num1 = 10;
5 int num2 = 10;
6
7 System.out.println(num1 + num2);
8 }
9
10 }

Notice that even though the code above would be considered some-
thing around O(3), the number 3 is irrelevant because it doesn’t

https://t.me/javalib
Big O Notation 22

make much of a difference. Also, it doesn’t matter how many


times we executed this algorithm it will have always a constant
time complexity. Therefore, the code above also has O(1) of time
complexity.
Also, notice that two values are being stored in two variables. Even
though this is actually O(2) as space complexity, we can consider it
as O(1) as well.

Accessing an Array by Index


Accessing an index of an array has also constant time complexity.
That’s because once the array is created in memory, we won’t need
to traverse the array, or do any other special operation. We just
need to access it and the time complexity for that is O(1) or constant
time:

1 int[] numbers = { 1, 2, 3};


2 System.out.println(numbers[1]); // O(1) here

Logarithmic – O(log n)
The classic algorithm that uses the O(log n) complexity is the
binary search. That’s because it’s not necessary to run through
the whole array. Instead, we get the number in the middle and
check if it’s lower or greater than the number to be found. Then
we break the array in two, we break it again until the number is
found. That’s only possible because a binary search must have the
array already sorted.
Let’s see the following diagram:

https://t.me/javalib
Big O Notation 23

Figure 3. Binary Search

As you can see in the above diagram, instead of traversing the


whole array we could break the array in two for three times and
that was enough to find the number. That’s exactly the number we
expect with the log complexity. That’s because 2 ^ 3 is the same
as the size of our array. In other words, 2 * 2 * 2 = 8. Therefore,
3 is the log complexity of our number.
For you to notice how efficient and fast is the time complexity of
log n, let’s think of an example. Suppose we pass an array from
the size of 1048576 which is equivalent to 2 ^ 20, if you guessed
that the number of iterations would be 20, you are right! In a linear
complexity, the number of iterations would be 1048576 which is
very slow.
Now that you understand better what is the log complexity, let’s
see the code from the binary search algorithm:

https://t.me/javalib
Big O Notation 24

1 public class BinarySearch {


2
3 public static int binarySearch(int[] array, int targe\
4 t) {
5 int middle = array.length / 2;
6
7 var leftPointer = 0;
8 var rightPointer = array.length - 1;
9
10 while (leftPointer <= rightPointer) {
11 if (array[middle] < target) {
12 leftPointer = middle + 1;
13 } else if (array[middle] > target) {
14 rightPointer = middle - 1;
15 } else {
16 return middle;
17 }
18
19 middle = (leftPointer + rightPointer) / 2;
20 }
21 return -1;
22 }
23
24 @Test
25 public void testCaseLog3() {
26 int [] array = {2, 3, 5, 7, 8, 10, 13, 16};
27 System.out.println(binarySearch(array, 2));
28 }
29
30 @Test
31 public void testCaseLog20() {
32 int [] array = new int[1048576];
33 int number = 0;
34 for (int i = 0; i < array.length; i++) {
35 array[i] = ++number;

https://t.me/javalib
Big O Notation 25

36 }
37 System.out.println(binarySearch(array, 1));
38 }
39 }

Output: testCaseLog3: Iterations count: 3 0


testCaseLog20: Iterations count: 20 0
Another algorithm that has the time complexity of log(n) is the
binary search tree. It’s similar to the binary search algorithm but
has the difference that it will check numbers from a graph.

Linear – O(n)
When we use looping we have a linear complexity. It doesn’t matter
if it’s the ‘for’, ‘while’, ‘foreach’, ‘do while’. Any of those loopings
will have a linear complexity.

1 Let’s see a simple example:


2
3 public void printNumbers(int n) {
4 for (int i = 0; i < n; i++) {
5 System.out.println(i);
6 }
7 }

Notice that the above code will print the number i that is incre-
mented in each iteration. Since we run through the looping n times,
we will have the time complexity of O(n).
Another important point is that if we have two loopings that
depend on the same input, in our case, n we will still have the time
complexity of O(n).
Let’s see how that translates in code:

https://t.me/javalib
Big O Notation 26

1 public static void printNumbers2Looping(int n) {


2 for (int i = 0; i < n; i++) { // O(n)
3 System.out.println(i);
4 }
5
6 for (int j = 0; j < n; j++) { // O(n)
7 System.out.println(j);
8 }
9 }

As you can see in the above code, at first we might think that
the time complexity is more than O(n) but it’s actually still O(n)
because both loopings are using n as the size number.

Two Inputs – O(m + n)


To measure the time complexity we have to pay attention to the
input numbers the algorithm is using. That will change the time
complexity. If a looping depends on two inputs to be executed, the
time complexity won’t be O(n), instead, it will be O(m + n) if we
use two variables as the looping lenght.
Let’s see how that works in practice:

1 public static void printNumbers(int m, int n) {


2 for (int i = 0; i < m; i++) {
3 System.out.println(i);
4 }
5
6 for (int i = 0; i < n; i++) {
7 System.out.println(i);
8 }
9 }

https://t.me/javalib
Big O Notation 27

As you can see in the above code, we have two inputs, m and n.
Also, it doesn’t matter if the size of n is greater than m or vice-
versa, the Big(O) notation will always be O(m + n) because m or n
might be any number. Therefore, it might dramatically impact the
time complexity of either m or n.

Log-linear – O(n log n)


Many efficient sorting algorithms such as Quicksort, Mergesort,
and Heapsort have the time complexity of O(n log n). The time
complexity O(n log n) is a bit slower than O(log n) and O(n).
The log-linear complexity is present in algorithms that use the
divide-and-conquer strategy. In the example of the Quicksort
algorithm, we need to traverse the array, find the pivot number,
divide the array into partitions, and swap elements until the array
is fully sorted. Notice that besides traversing the whole array with
the time complexity of O(n) we are also dividing the array into
partitions O(n log n).
The time complexity of O(n log n) is scalable, it can handle a
great number of elements effectively. If you notice, the Quicksort
algorithm that has the O(n log n) complexity is one of the most used
in programming languages because of its efficiency.
If you see the Java docs from the Arrays.sort method from the JDK
code, you will notice that the Quicksort algorithm is being used:

https://t.me/javalib
Big O Notation 28

1 public class Arrays {


2
3 /**
4 * Sorts the specified array into ascending numerical\
5 order.
6 *
7 * @implNote The sorting algorithm is a Dual-Pivot Qu\
8 icksort
9 * by Vladimir Yaroslavskiy, Jon Bentley, and Joshua \
10 Bloch. This algorithm
11 * offers O(n log(n)) performance on all data sets, a\
12 nd is typically
13 * faster than traditional (one-pivot) Quicksort impl\
14 ementations.
15 *
16 * @param a the array to be sorted
17 */
18 public static void sort(int[] a) { ... }
19
20 }

Quadratic – O(n²)
The exponential time complexity is present in low-performant
sorting algorithms such as bubble sort, Selection sort, and Insertion
sort.

https://t.me/javalib
Big O Notation 29

1 public class On2Example {


2
3 public static void main(String[] args) {
4 int[] numbers = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
5 int countOperations = countOperationsOn2(numbers);
6 System.out.println(countOperations);
7 }
8
9 public static int countOperationsOn2(int[] numbers) {
10 int countOperations = 0;
11 for (int i = 0; i < numbers.length; i++) {
12 for (int j = 0; j < numbers.length; j++) {
13 countOperations++;
14 }
15 }
16 return countOperations;
17 }
18
19 }

Output: 100
As you can see in the code above, the iteration depends on the size
of the array. We also have a for loop inside another one which
makes the time complexity be multiplied by 10 * 10.
Notice that the size of the array we are passing is 10. Therefore, the
countOperations variable will have a value of 100.

Cubic – O(n ^ 3)
The cubic complexity is similar to the quadratic one but instead of
having two nested loopings, we will have 3 nested loopings.
Let’s see how this will be represented in the code:

https://t.me/javalib
Big O Notation 30

1 public static int countOperationsOn3(int[] numbers) {


2 int countOperations = 0;
3 for (int i = 0; i < numbers.length; i++) {
4 for (int j = 0; j < numbers.length; j++) {
5 for (int k = 0; k < numbers.length; k++) {
6 countOperations++;
7 }
8 }
9 }
10 return countOperations;
11 }

Output: 1000
If we pass an array with the size of 10 to the above
countOperationsOn3 method, we will have an output of 1000.
That’s because 10 * 10 * 10 = 1000. Notice that the cubic
complexity will happen when we use 3 nested loopings. This time
complexity is very slow.

Exponential – O(c ^ n)
Exponential complexity is one of the worst ones. Also, to make the
notation clear, c stands for constant and n for the variable number.
Therefore, if we have the constant number of 10 ^ 5, we have 10
* 10 * 10 * 10 * 10 which corresponds to 100000.

Another important point to remember regarding the exponential


time complexity is that the exponent number, the power number is
the one that represents the exponential complexity:
constant ^ exponent number
One real-world example of an algorithm that uses exponential
time complexity is when we need to know the number of possible
combinations of something.

https://t.me/javalib
Big O Notation 31

Let’s suppose we want to know how many possible unique combi-


nations we can have with cake toppings or no toppings at all:

• Chocolate
• Strawberry
• Whipped Cream

To find this out we will have to use 2 as base ^ 3 which is equal to


8 combinations. Now let’s see some examples:

Toppings Combinations
1 1 - Plain cake
2 4 - Plain cake, chocolate, strawberry,
chocolate & strawberry
3 8–…

As you can see in the table above, the number grows exponentially.
If we want a cake with 10 toppings, then the amount of combina-
tions would be 1024, and so on!

Brute-force Password Break


To find out a password with brute force, suppose we can only use
numbers to make this example easy. Only numbers from 0 to 9 can
be input, also, suppose that the password has a length of 3 numbers.
The constant number will be 10 ^ 3 which is the same as 10 * 10 *
10 = 1000 possible combinations of numbers. Very easy password
to break. Now if it accepts lower-case letters and numbers then we
would have 26 letters + 10 numbers = 36 as a constant. That would
translate to 36 ^ 3 = 36 * 36 * 36 = 46656 possible combinations.
That’s why most websites require us to use special characters so
the number of possible combinations grows exponentially.

https://t.me/javalib
Big O Notation 32

Factorial O(n!)
The concept of factorial is simple. Suppose we have the factorial of
5! then this will equal 1 * 2 * 3 * 4 * 5 which results in 120.

A good example of an algorithm that has factorial time complexity


is the array permutation. In this algorithm, we need to check
how many permutations are possible given the array elements.
For example, if we have 3 elements A, B, and C, there will be 6
permutations. Let’s see how it works:
ABC, BAC, CAB, BCA, CAB, CBA – Notice that we have 6 possible
permutations, the same as 3!.
If we have 4 elements, we will have 4! which is the same as 24
permutations, if 5! 120 permutations, and so on.
Another algorithm is the traveling salesman, you can learn more
in the follwoing link (https://en.wikipedia.org/wiki/Travelling_-
salesman_problem)[https://en.wikipedia.org/wiki/Travelling_-
salesman_problem]. There is no need to fully understand this
algorithm, it’s quite complex but it’s good to know that it has the
factorial time complexity.

Summary
Measuring time complexity and space complexity is an essential
skill for every software engineer who wants to create high-quality
software to stand out. In this chapter, we learned:

• Best-case scenario of a time complexity can be described as


Omega Big-Ω notation
• Average-case scenario of a time complexity can be described
as Theta Big-Θ notation

https://t.me/javalib
Big O Notation 33

• Worst-case scenario of time complexity and the most impor-


tant that is the Big-O notation
• O(1): constant time. Examples of accessing a number from
an array. Calculating numbers…
• O(log n): Logarithmic time. Uses the divide and conquer
strategy. The binary search and tree binary search are good
examples of algorithms that have this time complexity. An
approximate real-world example would be to look at a word
in the dictionary.
• O(n) – Linear time: When we traverse the whole array
once, we have the O(n) time complexity. When we store
information in an array of n we will also have the O(n) for
space complexity. A real-world example could be reading a
book.
• O(N log N) – Log-linear: The Merge sort, Quick Sort,
Tim Sort, and Heap Sort are algorithms that have log-linear
complexity. Those algorithms use the divide and conquer
strategy making it more effective than O(n ^ 2).
• O(N ^ 2) – Quadratic: The Bubble Sort, Insertion Sort, and
Select Sort are some of the algorithms that have the quadratic
complexity. If there are two nested loopings traversing the
whole array each time, then we have O(n ^ 2) of time
complexity.
• O(N ^ 3) – Cubic: When we have 3 nested loopings and we
traverse the whole array on those loopings we will have the
cubic time complexity.
• O(c ^ n) – Exponential: The exponential complexity grows
very quickly. A good example of this time complexity is when
someone try to break a password. Suppose it’s a password
that supports only numbers and has 4 digits. That equivalates
to 10 ^ 4 = 10000 possible combinations. The greater the
exponent number the faster the number grows.
• O(n!) – Factorial: The factorial of 3! is the same as 1
* 2 * 3 = 5. It grows in a similar way to exponential

https://t.me/javalib
Big O Notation 34

complexity. The algorithms that have this time complexity


are array permutation and traveling salesman.

https://t.me/javalib
Array
The array data structure is probably the most used in every ap-
plication. If not directly used it’s indirectly used with ArrayList,
ArrayDeque, Vector, and other classes.
Simply put, an array is a data structure that stores multiple vari-
ables into it so that there is no need to create many variables with
different names.
Arrays in Java are always an object, therefore, they will occupy
space in the memory heap and it will create a reference for this
object.

Array Memory Allocation


The array is stored in the memory RAM and takes up space back
to back. Most memory RAM stores 1 byte (8 bits) in each space.
In Java, each primitive int number occupies 4 bytes in memory.
Therefore, let’s how this would work in the following memory
model if we create an array with 5 int elements:

https://t.me/javalib
Array 36

Figure 4. Array Memory Allocation

As you can see in the above diagram, when we are creating an


array we must pass the type of the elements and size from it. That’s
because when creating the array the memory allocation has to be
back to back.
That’s the reason why we can very quickly access an element from
an array. Since we know where the array starts, in the case of the
diagram above it’s in the memory address 5, the compiler makes a
simple calculation.
Considering the index we pass to the array, the size, and the type of
the element, we can easily calculate where the element is present in
memory. To get the second element from the array, for example, we
would add 4 bytes to the first element index and we would know
where the second element is allocated. For this reason, to access an
element in an array the time complexity will be always O(1).
To change an element in the array is also constant time O(1). That’s
because we can quickly access the variable index and assign a new
value to it.
To create an array the time complexity is O(n) because when
creating it, we will define the type and size of the array and the
required space in memory will be allocated.

https://t.me/javalib
Array 37

Static Arrays
As the name suggests, a static array is an array that can’t be
changed. We need to pass the type and size of the array and after
that, we can’t change the type or size of the array.
Let’s see in the following code how to create a static array with the
int type and show all the elements:

1 int[] array = {1, 2, 3, 4, 5}; // Create an array O(n)


2 for (int i = 0; i < array.length; i++) {
3 System.out.print(array[i] + " ");
4 }

Output: 1 2 3 4 5
Notice in the code above that we create an array of int values. Once
it’s created we can’t change either the type or the size of the array,
that’s what makes it static.
Then we access each element of the array by index and show
the values that will be 0 because those are the default values for
primitive int in Java.

Insert an Element in the Middle of


the Array
To insert an element in the middle of the array it will be necessary
to shift the elements. Therefore, the time complexity will be O(n).
Let’s imagine we want to put 3 in the middle of 2 and 4 in the
following array: { 1, 2, 4, 5 }. To do that, we will have to first create
a new array with the size of 5.

https://t.me/javalib
Array 38

Then, it will be necessary to move elements 4 and 5 to one position


ahead. Once this is done we can access index 2 and insert element
3.

Dynamic Arrays
There are classes in Java that make use of a dynamic array such as
ArrayList, Vector, and others. When we create an ArrayList, we
have a static array under the hood that starts as empty but after
adding the first element the size goes to 10. Then it doubles every
time it’s needed as you can see in the following code of the JVM:

1 public class ArrayList<E> extends AbstractList<E>


2 implements List<E>, RandomAccess, Cloneable, java\
3 .io.Serializable
4 {
5 private static final int DEFAULT_CAPACITY = 10;
6 transient Object[] elementData;
7 private int size;
8
9 private static final Object[] DEFAULTCAPACITY_EMPTY_E\
10 LEMENTDATA = {};
11
12 public ArrayList() {
13 this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTD\
14 ATA;
15 }
16
17 // This is the method that will double the array when\
18 ever it's needed
19 private Object[] grow(int minCapacity) {
20 int oldCapacity = elementData.length;
21 if (oldCapacity > 0 || elementData != DEFA\
22 ULTCAPACITY_EMPTY_ELEMENTDATA) {

https://t.me/javalib
Array 39

23 int newCapacity = ArraysSupport.newLength(old\


24 Capacity,
25 minCapacity - oldCapacity, /* minimum\
26 growth */
27 oldCapacity &gt;&gt; 1 /* p\
28 referred growth */);
29 return elementData = Arrays.copyOf(elementDat\
30 a, newCapacity);
31 } else {
32 return elementData = new Object[Math.max(DEFA\
33 ULT_CAPACITY, minCapacity)];
34 }
35 }
36
37 // Omitted other methods...
38
39 }

The elementData variable is the one that will store the data behind
the scenes for the ArrayList. Therefore, notice that the dynamic
array actually manipulates a static array to behave as dynamic.
When adding an element to an ArrayList, it will check if the size is
greater than 10 and if that is true the time complexity will be O(n).
That happens because since the array was created with the size of
10, it’s necessary to create a new array with the size of 20 copying
all the elements into the new one. To do so, we need to traverse the
whole array and copy element by element. Then we add the 11th
element to the array.
When the array behind the scenes is created with the size of 20 then
whenever we add an element we will have the time complexity of
O(1). Notice that the vast majority of the time when adding an
element to a dynamic array will be pretty fast, it will be O(1). Only
on the edge-case scenarios when the array size needs to be doubled
the time complexity will be O(n). This is also called amortized
complexity.

https://t.me/javalib
Array 40

Summary
• An array is allocated in memory from back to back.
• Accessing an array by index has the time complexity of O(1),
it’s pretty fast.
• Static array is the array that is created with a size and a type
pre-defined.
• Dynamic array is an adaptation of the static array that
automatically resizes it when necessary.
• A dynamic array will double its size when necessary.
• Adding an element to a dynamic array will be mostly O(1)
because there will be space more often.
• When adding an element to a dynamic array exceeds the size
of the static array under the hood, it will be necessary to
create a new array, copy the elements from the existing array,
then add the new element. Therefore, the time complexity
will be O(n).
• Adding an element in the middle of the array will have the
time complexity of O(n). That’s because it will be necessary
to shift all the elements from the right side to one position on
the right. Only then we will be able to insert the element by
index.
• To remove the first element from the array, it will be nec-
essary to shift all the elements from the right to the left.
Therefore, the time complexity is O(n).
• To remove an element from the array in the last position takes
O(1) complexity. That’s because we only need to remove the
last value and we have direct access to it.

https://t.me/javalib
String
The String data structure in Java and in other programming lan-
guages behind the scenes is an array of bytes. Bytes behind
the scenes are numbers that can be translated into bits or binary
numbers. Those numbers correspond to a character in an encoding
standard called ASCII (American Standard Code for Information
Exchange) as you can see in the following diagram.
** ASCII Codes **

Figure 5. Ascii Table

** Extended ASCII Codes **

https://t.me/javalib
String 42

Figure 6. Extended Ascii table

As you can see in the above diagram, each letter corresponds to a


number in the ASCII table. For example, the capital letter “A” is 65,
“B” is 66, “C” is 67 and the lowercase letter “a” is 97. It’s important
to know that each letter is represented by a number because this
concept will surely be used in many algorithms.
The ASCII table has 255 keyboard actions. Between them, there are
essential commands for the keyboard, capital and lowercase case
characters, special characters, and numbers.
Each ASCII character stores 1 byte in memory which is the same
as 8 bits.
Java doesn’t have a primitive type for String. It has actually a class.
Also, if you take a look inside the String class, you will notice that
actually the String class stores information in an array of bytes:

https://t.me/javalib
String 43

1 public final class String


2 implements java.io.Serializable, Comparable<String>, \
3 CharSequence,
4 Constable, ConstantDesc {
5
6 @Stable
7 private final byte[] value;
8
9 // Omitted fields and methods...
10 }

Characters and ASCII Encoding from a String Let’s then traverse


in a String to show in practice that each String character stores a
number from the ASCII table:

1 public class StringAscii {


2
3 public static void main(String[] args) {
4 String challenger = "ABCDabcd1234";
5
6 // O(n) time complexity - O(1) space complexity
7 for (int i = 0; i < challenger.length(); i++) {
8 char character = challenger.toCharArray()[i];
9 byte characterAsciiNumber = challenger.getBytes()[i\
10 ];
11 System.out.print(character + ":" + characterAsciiNu\
12 mber + " ");
13 }
14 }
15
16 }

Output: A:65 B:66 C:67 D:68 a:97 b:98 c:99 d:100 1:49 2:50 3:51 4:52

https://t.me/javalib
String 44

Get a character from a String


The time complexity to get a character from a String is O(1). That
happens because as mentioned before, a String is actually an array
of bytes behind the scenes.
If we want to retrieve the first character from a String, we can use
the following code:

1 public class GetCharacterString {


2
3 public static void main(String[] args) {
4 String challenger = "Never Stop Learning";
5 // O(1) time complexity
6 System.out.println(challenger.toCharArray()[0]);
7 }
8 }

Output: N

Copy a String
In programming languages like C++ a String is mutable. In other
programming languages such as Java, Python, C#, Kotlin, and
Golang a String is immutable. This means that those Strings will
not be changed in memory. Therefore, every time we concatenate
a String via code, another String will be created.
This means that whenever we concatenate a String, we will have
the time complexity of O(N). That happens because a new array of
bytes will be created in memory and then will copy the String array
into it.
Let’s see the following code example:

https://t.me/javalib
String 45

1 public class CopyStringComplexity {


2 public static void main(String[] args) {
3 String duke = "Awesome Duke ";
4 String juggy = "Awesome Juggy";
5
6 String dukeAndJuggy;
7
8 // O(n) time complexity
9 dukeAndJuggy = duke + juggy;
10 System.out.println(dukeAndJuggy);
11 }
12 }

Output: Awesome Duke Awesome Juggy


If we concatenate a String within a loop, then the time complexity
will be even worse. We will get the time complexity of O(n^2).
Let’s see the following code:

1 static void concatenateStringOn2Complexity(int n) {


2 String duke = "Awesome Duke ";
3 String juggy = "Awesome Juggy";
4
5 String dukeAndJuggy = "";
6
7 // O(n^2) time complexity
8 for (int i = 0; i < n; i++) {
9 dukeAndJuggy = duke + juggy;
10 }
11 System.out.println(dukeAndJuggy);
12 }

Code analysis:
Depending on the n parameter, there will be more concatenations.
Also, notice that there is a looping with n and within that loop

https://t.me/javalib
String 46

there will be another loop for n characters the String has to do


the concatenation. The above code is very slow. Fortunately,
Java provides a class that concatenates a String effectively and
significantly reduces the time complexity. Let’s further explore that
in the following section.
Effectively Copying String with StringBuilder and StringBuffer
The above code is ineffective to do many String concatenations.
When using the StringBuilder class though, the scenario changes.
This happens because the StringBuilder class will only create a new
array and really concatenate the String.
Let’s see how slow and inefficient normal String concatenation is
when we have a great number of concatenations:

1 public class ConcatenationComparison {


2
3 public static void main(String[] args) {
4 var nSize = 100000;
5 concatenateString(nSize);
6 concatenateStringBuilder(nSize);
7 }
8
9 static void concatenateString(int n) {
10 var timeMillis = System.currentTimeMillis();
11 String concatString = "";
12
13 // O(n2) time complexity
14 for (int i = 0; i < n; i++) {
15 concatString += i;
16 }
17
18 var processTime = System.currentTimeMillis() - timeMi\
19 llis;
20 System.out.println(processTime + " milliseconds");
21 }

https://t.me/javalib
String 47

1 static void concatenateStringBuilder(int n) {


2 var timeMillis = System.currentTimeMillis();
3 StringBuilder concatStringBuilder = new StringBuilder(\
4 );
5
6 // O(n) time complexity
7 for (int i = 0; i < n; i++) {
8 concatStringBuilder.append(i);
9 }
10
11 var processTime = System.currentTimeMillis() - timeMil\
12 lis;
13 System.out.println(processTime + " milliseconds");
14 }
15 }

Output (The output will slightly vary from time to time): 2820
milliseconds 5 milliseconds
As you can see in the code above the difference in time processing is
exponential. By using the StringBuilder to concatenate String when
doing the looping 100000 times, the StringBuilder will perform 564
times faster than normal concatenation.
That’s why it’s so important to understand what happens behind
the scenes with data structures and code, otherwise, we won’t be
able to understand why the performance is extremely better with
StringBuilder.
Another important class to know about is StringBuffer that has
does the same as StringBuilder but is thread-safe, meaning that will
avoid data collision when working in a multi-thread environment.
Time complexity is another crucial concept to understand to create
performant code.

https://t.me/javalib
Hashtable 51

example, it will transform into UTF-16 encoding by the hashCode


implementation.
Notice that the JDK will give us a high-quality hash function
meaning that we don’t need to reinvent the wheel by creating other
functions. The String, Integer, Double, Boolean, and many other
classes have their own implementation of hashCode.
The important thing to remember about the hash function is that it
has to be performant and has to return an integer code if possible
unique per each object.

What is a Hash Function?


Hash functions are used to create almost irreversible cryptogra-
phies where the input is almost impossible to retrieve. The most
popular one is SHA256. Also, in general, hash functions are mostly
used with Password verification, Compiler operations, algorithms
such as Robin Carp for pattern searching, and of course, Hash
Tables.
In Java, we have the famous method from the Object class called
hashCode which is responsible to generate the hash code.
A hash function might be extremely simple, it could be even a
constant value but that wouldn’t have any benefit. It could be
very simple also, for example, we can create a hash function for
an Object to sum the ASCII characters and return an int number:
Linked List 67

Code analysis:
In the code above we create a new node with the passed value, then
we set this value to the next pointer from the tail.
Notice that the tail (last element) at this moment holds the same
object reference from the head (first element). That’s the reason
the objects will be chained correctly.
Then, we add the element to the tail object since this is the last
object from the Linked List.

Add Element to Linked List Time Complexity

Notice in the code above that the add operation in a Linked List is
pretty simple. Therefore, the time complexity will be O(1) since we
only do a value assignment. There is no need to traverse the Linked
List.

Delete First Element from Linked List

Deleting the first element from a Linked List is also an easy


operation. To accomplish that we need to pass the next object
reference to the head instance variable. By doing that, we will be
erasing the first element from the chain and putting the second
element that contains the other objects from the chain:
Queue
The Queue data structure is very useful in algorithms. It’s very
used when traversing graphs for example. It’s also very efficient in
terms of performance to insert and remove the first or last elements.

What is Queue?
We can use an analogy of a real-world queue to explain what is a
queue in computer science. Let’s imagine a person that goes to a
bank queue to pay a bill. The first person to arrive in the queue is
the first person out or the first person to be served. The last person
arriving in the queue will be the last one to be served.
Let’s see the diagram:

https://t.me/javalib
Queue 86

Figure 12. FIFO

In Java, we already have an implementation of the data structure


queue. It’s the interface Queue. Let’s see in practice the same
example we’ve seen in the diagram above:

1 import java.util.LinkedList;
2 import java.util.Queue;
3
4 public class QueueExample {
5
6 public static void main(String[] args) {
7 Queue<String> peopleQueue = new LinkedList<>();
8 peopleQueue.add("Wolverine"); // First in & first out
9 peopleQueue.add("Juggernaut");
10 peopleQueue.add("Xavier");

https://t.me/javalib
Queue 87

11 peopleQueue.add("Beast"); // Last in & last out


12
13 var queueSize = peopleQueue.size();
14 for (int i = 0; i < queueSize; i++) {
15 System.out.print(peopleQueue.poll() + " ");
16 }
17 }
18
19 }

Output: Wolverine Juggernaut Xavier Beast


Notice in the code above that we are inserting data only at the
beginning of the queue. Then we use the poll method to remove
and return the inserted elements.
There is an important detail when using the implementation of
LinkedList. If we look into the internal implementation from
LinkedList, notice that the data is stored in the data structure of
a graph:

1 public class LinkedList<E>


2 extends AbstractSequentialList<E>
3 implements List<E>, Deque<E>, Cloneable, java.io.Seri\
4 alizable
5 {
6 transient int size = 0;
7
8 transient Node<E> first;
9 transient Node<E> last;
10
11 private static class Node<E> {
12 E item;
13 Node<E> next;
14 Node<E> prev;
15

https://t.me/javalib
Queue 88

16 Node(Node<E> prev, E element, Node<E> next) {


17 this.item = element;
18 this.next = next;
19 this.prev = prev;
20 }
21 }
22 // Omitted other code
23 }

Since LinkedList uses the structure of a graph we have great


performance when inserting or removing elements at the beginning
or the end of the List. However, it’s slow to search for a specific
element since the whole list has to be traversed object by object.
Notice also that LinkedList can’t be accessed via an index like an
array which is much faster, it’s an O(1) time complexity.

Inserting Elements at the Start and


the End of the Queue
To add elements at the end of the queue there is a ready-to-use
interface in Java called Deque. Deque is the short name for a
double-ended queue that enables us to insert and remove elements
from the start and the end of the queue.
Let’s see how we can use Deque to add an element at the first and
last position of the queue:

https://t.me/javalib
Queue 89

1 public class DequeAddFirstAndLast {


2 public static void main(String[] args) {
3 Deque<String> xmenQueue = new LinkedList<>();
4 xmenQueue.add("Wolverine");
5 xmenQueue.addFirst("Cyclops");
6 xmenQueue.addLast("Xavier");
7
8 showAndRemoveQueueElements(xmenQueue);
9 }
10
11 private static void showAndRemoveQueueElements(Deque<St\
12 ring> peopleQueue) {
13 var queueSize = peopleQueue.size();
14 for (int i = 0; i < queueSize; i++) {
15 System.out.print(peopleQueue.poll() + " ");
16 }
17 }
18 }

Output: Cyclops Wolverine Xavier

Deleting Elements at the Start and


End of the Queue
In the following code, we will add three elements to the queue
and then will use removeFirst method to remove the first element
“Wolverine” and the last element “Xavier”. Lastly, we will show
and remove the remaining element “Cyclops”:

https://t.me/javalib
Queue 90

1 import java.util.Deque;
2 import java.util.LinkedList;
3
4 public class DequeDeleteFirstAndLast {
5 public static void main(String[] args) {
6 Deque<String> xmenQueue = new LinkedList<>();
7 xmenQueue.add("Wolverine");
8 xmenQueue.add("Cyclops");
9 xmenQueue.add("Xavier");
10
11 System.out.println("Removing first: " + xmenQueue.rem\
12 oveFirst());
13 System.out.println("Removing last: " + xmenQueue.remo\
14 veLast());
15
16 showAndRemoveQueueElements(xmenQueue);
17 }
18
19 private static void showAndRemoveQueueElements(Deque<St\
20 ring> peopleQueue) {
21 var queueSize = peopleQueue.size();
22 for (int i = 0; i < queueSize; i++) {
23 System.out.print(peopleQueue.poll() + " ");
24 }
25 }
26 }

Output: Removing first: Wolverine Removing last: Xavier Cyclops

https://t.me/javalib
Queue 91

Big(O) Notation – Time Complexity


of a Queue

Insert an element at the beginning of the


queue

To insert an element at the beginning of the queue we only link a


new object into the first element of the LinkedList. Therefore, the
complexity is O(1). Take a look at the following code:

1 private void linkFirst(E e) {


2 final Node<E> f = first;
3 final Node<E> newNode = new Node<>(null, e, f);
4 first = newNode;
5 if (f == null)
6 last = newNode;
7 else
8 f.prev = newNode;
9 size++;
10 modCount++;
11 }

Insert an element at the end of the queue

This is only possible to accomplish with a double-ended queue. The


time complexity is O(1), the same as inserting an element at the
beginning of the queue. Also, the code is very similar as you can
see in the following code:

https://t.me/javalib
Queue 92

1 void linkLast(E e) {
2 final Node<E> l = last;
3 final Node<E> newNode = new Node<>(l, e, null);
4 last = newNode;
5 if (l == null)
6 first = newNode;
7 else
8 l.next = newNode;
9 size++;
10 modCount++;
11 }

Insert or delete an element in the middle of


the queue
The time complexity is O(n) because it’s necessary to traverse the
graph of objects until we can add an element in the wished position.

Find an element
The time complexity is O(n) and that’s because until the element is
found it’s necessary to traverse the graph, element by element.

Delete the first element of the


queue
Similarly to the insertion, the time complexity is O(1) because we
have direct access to the first element.
To explain the following code, we pass the first element as a
parameter, then we assign the data to new variables. Then we
assign null from the next element to help the garbage collector

https://t.me/javalib
Queue 93

collect the dead instance. In the first element instance variable we


put the reference of the next element:

1 private E unlinkFirst(Node<E> f) {
2 // assert f == first && f != null;
3 final E element = f.item;
4 final Node<E> next = f.next;
5 f.item = null;
6 f.next = null; // help GC
7 first = next;
8 if (next == null)
9 last = null;
10 else
11 next.prev = null;
12 size--;
13 modCount++;
14 return element;
15 }

Delete the last element of the queue It’s only possible to delete the
last element by using a double-linked list. Also, since a double-
linked list stores the last element into a separate variable, we have
direct access to it to perform the deletion. Therefore, the time
complexity is O(1).
To briefly explain the following code, we receive the last element by
parameter, variable “l”. Then we pass the previous element attached
to the last element to a new variable. We pass null to item and prev
from the last element to help the garbage collector collect those
dead instances. Finally, we pass the reference from prev to the last
element instance variable.

https://t.me/javalib
Queue 94

1 private E unlinkLast(Node<E> l) {
2 // assert l == last && l != null;
3 final E element = l.item;
4 final Node<E> prev = l.prev;
5 l.item = null;
6 l.prev = null; // help GC
7 last = prev;
8 if (prev == null)
9 first = null;
10 else
11 prev.next = null;
12 size--;
13 modCount++;
14 return element;
15 }

Summary
The queue data structure is a very important data structure to be
mastered and it’s used in the famous breadth-first search algorithm.
Let’s see the key points from the queue data structure:
It uses the FIFO (First-in First-out) structure. The same from a
real-world queue. To insert and delete at the beginning or at the
end of the queue the time complexity is O(1). To find an element
in a queue with a LinkedList the time complexity is O(n) because
it’s necessary to traverse all elements for the worst-case scenario.
To delete or insert an element in the middle of the queue the time
complexity is also O(n).

https://t.me/javalib
Graph
The graph data structure is a composition of nodes connected by
edges. Graphs are vastly used in the real world. One very simple
example is Facebook where a person is a friend of another person
and so on. Graphs can also represent routes from one place to
another.
A graph has nodes/vertices and is connected by the edges. To
exemplify those nomenclatures, let’s see the following image:

https://t.me/javalib
Graph 96

Figure 13. Node Nomenclature

https://t.me/javalib
Graph 97

Now that we see a node in a diagram, let’s see how we represent it


in code (also remember that the following Node class will be used
in the following examples):

1 import java.util.ArrayList;
2 import java.util.List;
3
4 public class Node {
5
6 Object value;
7 private List<Node> adjacentNodes = new ArrayList<>();
8
9 public Node(Object value) {
10 this.value = value;
11 }
12
13 public void addAdjacentNode(Node node) {
14 this.adjacentNodes.add(node);
15 }
16
17 public List<Node> getAdjacentNodes() {
18 return adjacentNodes;
19 }
20
21 public void showNodes() {
22 BreathFirstSearchPrintNodes.printNodes(this);
23 }
24
25 public Object getValue() {
26 return value;
27 }
28 }

Notice that a Node in essence contains a value and its adjacent


(neighbors) nodes which are represented by the adjacentNodes list.

https://t.me/javalib
Graph 98

In the constructor, we receive the value from the Node.


The addAjacentNode adds an adjacent node, also called a neighbor
node.
The showNodes method will traverse and show every value from
every Node by using the Breath-First-Search algorithm.
The getValue method returns the current value from the Node.

Undirected Graph
In the example of Facebook, a person is a friend of another one,
therefore, this connection is unidirectional. Let’s see how this can
be represented in the following image:
Notice in the graph above that Rafael is a friend of Bruno and
the connection is undirected. This means that Rafael has access
to Bruno’s profile and vice-versa.
Let’s see that in code in the following example. Let’s use the
same Node class as above but we will explore the connect method
instead:

1 import java.util.ArrayList;
2 import java.util.List;
3
4 public class Node {
5
6 Object value;
7 private List<Node> adjacentNodes = new ArrayList<>();
8
9 // Omitted addAdjacentNode, getAdjacentNodes, showNodes\
10 , and getValue methods...
11 public void connect(Node node) {
12 if (this == node) throw new IllegalArgumentException(\
13 "Can't connect node to itself");

https://t.me/javalib
Graph 99

14 this.adjacentNodes.add(node);
15 node.adjacentNodes.add(this);
16 }
17
18 }

Notice in the code above that we are connecting a node to each


other in the connect method. The current instance node adds the
node passed via parameter and the node passed by parameter adds
the current instance node to its adjacent nodes. Therefore, we have
a connection from both sides between nodes.
Now, let’s populate this Node object with the same elements and
print them from the above diagram:

1 public class UndirectedGraph {


2
3 public static void main(String[] args) {
4 Node rafaelRootNode = new Node("Rafael");
5 Node brunoNode = new Node("Bruno");
6 Node jamesNode = new Node("James Gosling");
7 Node dukeNode = new Node("Duke");
8 Node johnNode = new Node("John");
9
10 rafaelRootNode.connect(brunoNode);
11 rafaelRootNode.connect(johnNode);
12 rafaelRootNode.connect(dukeNode);
13 brunoNode.connect(jamesNode);
14
15 rafaelRootNode.showNodes();
16 }
17
18 }

Output: Visited nodes: Rafael | Bruno | John | Duke | James Gosling


|

https://t.me/javalib
Graph 100

Directed Graph
The graph data structure can be directional, which means that it
might connect to one node but the other node might not connect
back. A simple real-world example of that is when an airplane
goes somewhere. The airplane will go to another city, therefore,
it’s directional.
Another example is Twitter, a person can follow another one.
However, the other person doesn’t need to follow back.
Let’s see how that works in the example of Twitter:

Figure 14. Uncycled Graph

Notice in the image above that Rafael follows James Gosling, Duke,
John, and Bruno. However, James Gosling, Juggy, Duke, and John
don’t follow Rafael back. Rafael follows Bruno and Bruno follows
back Rafael. From the node of Rafael, it’s possible to traverse
through the whole graph.
Also, notice that the graph above doesn’t have cycles. Therefore,
it’s an acyclic graph.
Let’s see how to represent the above graph in code:

https://t.me/javalib
Graph 101

1 public class DirectedGraph {


2 public static void main(String[] args) {
3 Node rafaelRootNode = new Node("Rafael");
4 Node brunoNode = new Node("Bruno");
5 Node jamesNode = new Node("James Gosling");
6 Node juggyNode = new Node("Juggy");
7 Node dukeNode = new Node("Duke");
8 Node johnNode = new Node("John");
9
10 rafaelRootNode.addAdjacentNode(brunoNode);
11 brunoNode.addAdjacentNode(rafaelRootNode);
12 rafaelRootNode.addAdjacentNode(jamesNode);
13 rafaelRootNode.addAdjacentNode(johnNode);
14 rafaelRootNode.addAdjacentNode(dukeNode);
15 jamesNode.addAdjacentNode(juggyNode);
16
17 rafaelRootNode.showNodes();
18 }
19 }

Output: Visited nodes: Rafael | Bruno | James Gosling | John | Duke


| Juggy |

Acyclic and Cyclic Graph


A graph can be cyclic or not. This means that if there are edges
connecting the graph in a cyclic way then we have a cyclic graph.
Let’s see the representation from an acyclic graph:

https://t.me/javalib
Graph 102

Figure 15. Acyclic Graph

Representing the above acyclic graph in Java code, it’s similar to


the code example from the directed graph:

1 public class AcyclicGraph {


2
3 public static void main(String[] args) {
4 Node dukeRootNode = new Node("Duke");
5 Node mozillaNode = new Node("Mozilla");
6 Node mobyDockNode = new Node("Moby Dock");
7 Node juggyNode = new Node("Juggy");
8
9 dukeRootNode.addAdjacentNode(mozillaNode);
10 dukeRootNode.addAdjacentNode(mobyDockNode);
11 dukeRootNode.addAdjacentNode(juggyNode);
12 dukeRootNode.showNodes();
13 }
14 }

https://t.me/javalib
Graph 103

Output: Visited nodes: Duke | Mozilla | Moby Dock | Juggy |


When traversing through a cyclic graph, it’s necessary to check
if the node was already traversed. Otherwise, an infinite looping
would happen. Let’s see how a cyclic graph can be represented in
the following image:

Figure 16. Cyclic Graph

1 public class CyclicGraph {


2 public static void main(String[] args) {
3 Node dukeNode = new Node("Duke");
4 Node mobyDockNode = new Node("Moby Dock");
5 Node juggyNode = new Node("Juggy");
6
7 dukeNode.addAdjacentNode(mobyDockNode);
8 mobyDockNode.addAdjacentNode(juggyNode);
9 juggyNode.addAdjacentNode(dukeNode);
10

https://t.me/javalib
Graph 104

11 dukeNode.showNodes();
12 }
13 }

Output: Visited nodes: Duke | Moby Dock | Juggy |

Summary
The Graph data structure is highly important to be mastered by
every software developer. It’s vastly used behind the scenes by
frameworks, libraries, and technologies. That’s the reason why it’s
vastly asked in many companies during interviews.
To recap, let’s see the important points:

• Graphs contain nodes (vertices) and connections (edges).


Some real examples are friends connections from social me-
dia, and a vehicle going from one point to another.
• An adjacent or neighbor node is the direct relationship from
one node to the other one.
• A graph can be directed, which means that one node can
reach the other one but the other one can’t.
• A graph can be undirected, which means that the connection
with another node will be bidirectional.
• A graph can be acyclic, meaning that there won’t be cycles
between the nodes’ relationships.
• A graph can be cyclic, meaning that there will be cycles
between the nodes’ relationships.

https://t.me/javalib
Tree
The tree data structure is a type of graph. A tree has a root node (top
node) that will have a relationship with its child nodes. The path
that connects the root node to the child nodes is called a branch.
The leaf node is the node that doesn’t have any children and is not
the root node.
In algorithms, you will see a lot of the nomenclature height. Height
in trees is the number of nodes from the highest branch to the root
node. Another keyword is the depth of a tree which means the
count of nodes from a specific node to the root node.
To make those nomenclatures clear, let’s see them in a diagram:

Figure 17. Tree from Root Diagram

A real-world example is the hierarchy of a company. For example:

https://t.me/javalib
Tree 106

Figure 18. Tree from Root Real-World Diagram

To follow and run your own tests from the following


code examples, you can download the code in the
following link (https://github.com/rafadelnero/java-
algorithms/tree/main/src/main/java/fundamentals/tree)[https://github.com/rafadeln
algorithms/tree/main/src/main/java/fundamentals/tree]!
Let’s see a simple way to represent this data in Java code:

https://t.me/javalib
Tree 107

1 import java.util.LinkedList;
2 import java.util.List;
3
4 public class TreeNode {
5
6 private String value;
7 private List<TreeNode> childNodes;
8
9 public TreeNode(String value) {
10 this.value = value;
11 this.childNodes = new LinkedList<>();
12 }
13
14 public void addChild(TreeNode childNode) {
15 this.childNodes.add(childNode);
16 }
17
18 public void showTreeNodes() {
19 BreathFirstSearchPrintTreeNodes.printNodes(this);
20 }
21
22 public String getValue() {
23 return value;
24 }
25
26 public List<TreeNode> getChildNodes() {
27 return childNodes;
28 }
29 }

Notice that the code above is very similar to the graph data
structure. We are also using the Breadth-first search algorithm to
show the data from the tree. For now, don’t worry about it, just
keep in mind that this is a famous algorithm that will visit and
print each node. Now let’s populate the data from the company

https://t.me/javalib
Tree 108

hierarchy in the Tree data structure:

1 public class CompanyHierarchyTree {


2
3 public static void main(String[] args) {
4 TreeNode rootTreeNode = new TreeNode("CEO");
5 TreeNode vpNode = new TreeNode("Vice President");
6 TreeNode managerNode = new TreeNode("Manager");
7 TreeNode dev1Node = new TreeNode("Developer 1");
8 TreeNode dev2Node = new TreeNode("Developer 2");
9 TreeNode dev3Node = new TreeNode("Developer 3");
10 rootTreeNode.addChild(vpNode);
11 vpNode.addChild(managerNode);
12 managerNode.addChild(dev1Node);
13 managerNode.addChild(dev2Node);
14 managerNode.addChild(dev3Node);
15
16 rootTreeNode.showTreeNodes();
17 }
18
19 }

Output: Visited nodes: CEO | Vice President | Manager | Developer


1 | Developer 2 | Developer 3 |
Another example is the way file system on a computer. There is a
hierarchy of compartments within the computer to organize files.
For example, there is the root computer compartment and then
users, the Desktop, and finally your file. This is a classic example
of a tree data structure.
A tree can’t have cycles and each node can’t have more than one
parent.

https://t.me/javalib
Tree 109

Binary Tree
A binary tree is a tree that has up to 2 child nodes. Let’s see how
that works in a diagram:

Figure 19. Binary Tree Diagram

Notice that the diagram above is a binary tree because the nodes
have up to 2 children at max. Even though node 3 has only one
child that still makes the above diagram a binary tree.

https://t.me/javalib
Tree 110

Ternary Tree
A ternary tree is similar to the binary tree but instead of having up
to 2 child nodes, it has up to 3 child nodes. Let’s see how this is
represented in a diagram:

Figure 20. Ternary Tree Diagram

Notice in the diagram above that node 1 and node 2 has 3 child
nodes. Even though node 4 has only one child node this also doesn’t
matter, it’s still a ternary tree since the max number of child nodes
is 3.

K-ary tree
The “K” represents the max number of child nodes a tree can have.
For example, we can represent a binary tree as a 2-ary tree because

https://t.me/javalib
Tree 111

both mean that the tree can have up to 2 child nodes. Similarly, a
3-ary tree is the same as a ternary tree.

Perfect Binary Tree


A perfect binary tree has the same depth for every child node to
the leaf nodes. Let’s see how to represent it in a diagram:

Figure 21. Perfect Tree Diagram

Complete Binary Tree


A complete binary tree contains its nodes complete for the leftmost
nodes. Also, the interior nodes have to have two child nodes. Only
the leaf nodes that are not the leftmost are allowed to not have child
nodes.

https://t.me/javalib
Tree 112

Figure 22. Complete Binary Tree

Remember that if the binary tree is complete only for the rightmost
nodes, that wouldn’t be considered a complete binary tree.

Full Binary Tree


When a tree has either two or zero child nodes, it’s considered a
full binary tree. To illustrate a full binary tree, take a look at the
following diagram:

https://t.me/javalib
Tree 113

Figure 23. Full Binary Tree

https://t.me/javalib
Tree 114

Balanced Binary Tree


A balanced tree can’t have its subtrees heights with more than 1
level of difference between the left and right subtrees. Let’s see an
example to understand it more clearly:

Figure 24. Balanced Tree

Notice in the diagram above that the total height of this tree is 3.

https://t.me/javalib
Tree 115

The height of the left nodes 4 and 5 is 3. The height of node 3 is


2. Therefore, the difference between the node subtrees is 1, and for
this reason, is a balanced tree.
The following diagram is an example of a non-balanced binary tree.
That’s because the height from the left subtrees is higher than 1
compared to the right subtrees:

https://t.me/javalib
Tree 116

Figure 25. Non Balanced Tree

Note that in the diagram above node 6 has a height of 4. Node 3


which is on the right has a height of 2. Therefore, the difference in

https://t.me/javalib
Tree 117

the height of the nodes is higher than 1 and for this reason, the tree
above is not a balanced tree.

Summary
In this chapter, we saw essential concepts from the Tree data
structure. As you can see, a tree is a graph used in many systems
and the real world. Let’s see the key points from the Tree data
structure:

• Tree is a type of Graph but has specific structures and rules.


• Node is every element from the tree.
• Root node is the node from the top that will access the child
nodes.
• Leaf node is any child node.
• The height of a tree is how deep the child nodes go.
• Binary Tree can have up to two child nodes at max.
• Ternary Tree can have up to three child nodes.
• K-ary tree can have up to whatever number you want of child
nodes.
• Perfect Binary Tree has to have the same number of child
nodes for every parent.
• Complete Binary Tree means that the left-most nodes are
complete. There must be two child nodes on the left side of
the tree.
• Full Binary Tree when a tree has 0 or two child nodes. It can’t
have only one child node.
• Balanced Binary Tree can’t have more than 1 level of height
difference.

https://t.me/javalib
Recursion
Recursion is a programming fundamental that is highly used in
algorithms. Simply put, recursion is the ability of a method to call
itself. When a method calls itself it stacks up and uses the LIFO
(Last-in First-out) approach. It’s the same concept as a stack of
plates. Let’s see the following scenario:

Figure 26. Last-in First-out

To summarize the above figure, remember that the last element that
is inserted in the stack will be the first one to be removed. That’s
why LIFO.
To memorize and really understand the LIFO approach, just re-
member that when you stack up plates, you can’t get the plate at
the bottom. Instead, it’s much easier and more suitable to get the
first one at the top of the stack. The methods are stacked exactly in
the same way!
FILO (First-in Last-out) has the same result as LIFO. It basically
means that the first plate to go on the stack will be the last one to
go out.

https://t.me/javalib
Recursion 119

Figure 27. First-in Last-out

To summarize the above figure, the first plate that is inserted in the
stack will be the last one to be removed.
Another point to keep in mind is that what can be done with
recursion can also be done with loopings too and vice-versa.
Let’s see a simple Java example using this concept:

1 public class Recursion {


2
3 public static void main(String[] args) {
4 System.out.println(getNumber(0));
5 }
6
7 static int getNumber(int number) {
8 if (number > 5) { // #A
9 return number;
10 }
11
12 return getNumber(number + 1); // #B
13 }
14
15 }

Code analysis:

• #A: This “if” is the condition that is necessary to break


the recursion looping. This basically means that when the
number parameter is greater than 5, the recursion looping will
stop. This is also called the base case in recursion.

https://t.me/javalib
Recursion 120

• #B: Notice that the getNumber method is invoking itself and


it’s adding up 1 to the parameter. This means that every time
the method getNumber is invoked the number parameter will
be summed with 1. This method is invoked the first time with
0 as the number and it’s stacked until it’s 6 as you can see in
the following image:

Figure 28. Recursion Debug

Seeing LIFO/FILO in practice


When we sum the numbers the order that methods are invoked
recursively doesn’t really matter. However, if we want to print
numbers in order that changes completely. Let’s see LIFO/FILO
happening in practice:

https://t.me/javalib
Recursion 121

1 public class NumbersRecursion {


2
3 public static void main(String[] args) {
4 showNumbers(0);
5 }
6
7 static void showNumbers(int number) {
8 if (number == 5) {
9 return;
10 }
11 showNumbers(number + 1);
12 System.out.print(number + " ");
13 }
14 }

Output: 4 3 2 1 0
Notice that the number was printed in reverse order. That’s because
the method is recursively stacked as you can see in the debugging
images:
When all methods were invoked, notice that the variable number
is 5:

Figure 29. LIFO Debug Full Stack

When all methods were invoked and the methods are going out of
the stack the variable number is 3:

https://t.me/javalib
Recursion 122

Figure 30. LIFO Debug Half Stack

Therefore, it’s like the recursive call will intercept this method
invocation and wait until the last recursive method is called to be
the first out of the stack. That’s why the first number to be printed
is 4!

Recursion Tree with Fibonacci


The Fibonacci algorithm is a classic one every developer does
during their college studies. Solving the Fibonacci algorithm with
recursion though is not so efficient it uses a lot more memory and
it’s more complex.
If you forgot what is the Fibonacci algorithm, the goal is to basically
sum the previous number with the next one starting with 0 and 1.
For example, by starting the algorithm receiving 0 and 1 we would
have the following numbers:
0, 1, 2, 3, 5, 8, 13, 21, 34, 55…

A way to solve this problem in maths is to use the following


formula:
Fn = Fn-1 + Fn-2

Considering that maths is the basis of programming and it’s what


is behind it, we can replicate that in code by using recursion.
Logarithm
The logarithm time complexity is present in many algorithms and
it’s very effective. Therefore, it’s important to understand how it
works so we can know how performant the algorithm that has this
time complexity is.
In simple words, a logarithm is the number of times that a number
multiplies itself to get a certain result. Let’s take a look at the
mathematical function before seeing practical examples:

Figure 32. Logarithm Formula

If we substitute b (base) with 2, then the result will be the x


(exponential) number of 2 to result in x.
For example, in the context of algorithms let’s consider n = 16 and
we have to figure out what is the power number that the base 2
should have for the result of 16.
log 2 ^ y = 16 == log 2 ^ 4 = 16

The number 2 powered by 4 is the same as 16. Therefore, the


algorithm will have 4 iterations in its worst-case scenario.
Merge Sort 160

In summary, avoid using merge sort:

• When space is limited


• For very small arrays
• When stability is not required
• For highly unsorted arrays

Pros and Cons of Merge Sort


No sorting algorithm is perfect for all situations. Insted, we have
to pick the best algorithm for the spcific situation. Therefore, let’s
see the pros and cons from the merge sort.

Pros:

• Merge sort has a time complexity of O(n log n), which means
that it can sort large amounts of data efficiently.
• It is a stable sorting algorithm, meaning that it preserves the
relative order of equal elements in the input list.
• Merge sort is a divide-and-conquer algorithm, which makes
it easy to implement recursively and understand.
• Merge sort can also be used to efficiently merge two sorted
lists into a single sorted list.

Cons:

• Merge sort requires additional memory to store the tempo-


rary arrays used during the sorting process, which can be a
disadvantage when working with limited memory.

https://t.me/javalib
Merge Sort 161

• The algorithm is not efficient for small lists, as the overhead


of the recursive calls and array copying can outweigh the
benefits of the sorting algorithm itself.
• Merge sort is not an in-place sorting algorithm, meaning
that it requires additional memory to perform the sorting
operation. This can be a disadvantage when working with
large datasets or limited memory environments.

Summary
The merge sort is not an easy one to master. However, let’s see
some essential points of this algorithm so you can more easily
remember and master it.
The merge sort algorithm:

• Uses the divide-and-conquer strategy.


• Uses recursion.
• Is fast enough because it has the time complexity of O(n log
n).
• Is stable, meaning that it doesn’t change the order of elements
when they are equal.
• Is not space efficient. It uses an auxiliary array to sort
elements.
• Is efficient with large input arrays.
• Is efficient with partially sorted input arrays because it will
sort and merge only the unsorted part of the array.

https://t.me/javalib
Depth-first-search
The depth-first-search algorithm is used a lot in algorithms where
it’s necessary to traverse through nodes. This algorithm is likely
not to be used in day-to-day work directly. However, LinkedList
uses the concepts of graphs, so we use it indirectly all the time.
Before going through the DFS (Depth-First-Search) algorithm, en-
sure you fully understand the graph data structure, tree, stack data
structure, and recursion.
Let’s use the following graph to traverse using the
depth-first-search algorithm.

https://t.me/javalib
Depth-first-search 163

Figure 38. Graph Traverse

To represent the above graph we will use the following classes:

https://t.me/javalib
Depth-first-search 164

1 import java.util.ArrayList;
2 import java.util.List;
3
4 public class Node {
5
6 Object value;
7 private List<> adjacentNodes = new ArrayList<>();
8 private boolean visited;
9
10 public Node(Object value) {
11 this.value = value;
12 }
13
14 public void addAdjacentNode(Node node) {
15 this.adjacentNodes.add(node);
16 }
17
18 public List<> getAdjacentNodes() {
19 return adjacentNodes;
20 }
21
22
23 public Object getValue() {
24 visited = true;
25 return value;
26 }
27
28 public boolean isVisited() {
29 return visited;
30 }
31 }

Now let’s populate this graph with the same data from the diagram
above:

https://t.me/javalib
Depth-first-search 165

1 public class GraphMock {


2
3 public static Node createPreorderGraphMock() {
4 Node rootNode = new Node(1);
5 Node node2 = new Node(2);
6 Node node3 = new Node(3);
7 Node node4 = new Node(4);
8 Node node5 = new Node(5);
9 Node node6 = new Node(6);
10 Node node7 = new Node(7);
11
12 rootNode.addAdjacentNode(node2);
13 node2.addAdjacentNode(node3);
14 node3.addAdjacentNode(node4);
15 node4.addAdjacentNode(node5);
16
17 rootNode.addAdjacentNode(node6);
18 rootNode.addAdjacentNode(node7);
19
20 return rootNode;
21 }
22 }

Preorder Traversal
The preorder graph traversal means that we will start from the root,
then will traverse to the left and right subtrees.

Recursive Preorder Traversal


without Lambda
Let’s see first how to traverse recursively without the use of lambda:

https://t.me/javalib
Depth-first-search 166

1 public class DepthFirstSearchPreorder {


2
3 public static void main(String[] args) {
4 Node rootNode = GraphMock.createPreorderGraphMock();
5 dfsRecursive(rootNode);
6 }
7
8 public static void dfsRecursiveWithoutLambda(Node node) {
9 System.out.print(node.getValue() + " ");
10
11 for (Node eachNode : node.getAdjacentNodes()) {
12 if (!eachNode.isVisited()) {
13 dfsRecursiveWithoutLambda(eachNode);
14 }
15 }
16 }
17 }

Output: 1 2 3 4 5 6 7
Notice in the code above that we first ask if the node was not
visited and then invoke the dfsRecursiveWithoutLambda method
recursively. The methods are stacked into the memory heap. The
LIFO (Last-in First-Out) approach is used. This means that the last
invoked method will be the first to be fully executed.

Recursive Preorder Traversal with


Lambda
Let’s now traverse the data from the graph above using lambda:

https://t.me/javalib
Depth-first-search 167

1 import fundamentals.graph.Node;
2
3 public class DepthFirstSearchPreorder {
4
5 public static void main(String[] args) {
6 Node rootNode = GraphMock.createPreorderGraphMock();
7 dfsRecursive(rootNode);
8 }
9
10 public static void dfsRecursive(Node node) {
11 System.out.print(node.getValue() + " ");
12 node.getAdjacentNodes().stream()
13 .filter(n -> !n.isVisited())
14 .forEach(DepthFirstSearchPreorder::dfsRecursive);
15 }
16
17 }

Output: 1 2 3 4 5 6 7
As you can see in the code above, we are using the recursion
technique. We traverse through the adjacent (neighbor) nodes
filtering the nodes that were already visited. We are also invoking
the dfsRecursive method recursively which will basically stack up
the methods in a stack and will pop them out as soon as there isn’t
any other adjacent node to be traversed.

Preorder with Looping


Doing the depth-first-search algorithm without recursion is more
complex but still possible. Let’s see how is the code:
// Omitted class code public static void dfsNonRecursive(Node
node) { Stack<> stack = new Stack<>(); Node currentNode = node;
stack.push(currentNode);

https://t.me/javalib
Depth-first-search 168

while (!stack.isEmpty()) { currentNode = stack.pop(); if (!currentN-


ode.isVisited()) { for (int i = currentNode.getAdjacentNodes().size()
- 1; i >= 0; i—) { stack.push(currentNode.getAdjacentNodes().get(i));
}

1 System.out.print(currentNode.getValue() + " ");


2 }

} } Output: 1 2 3 4 5 6 7
Notice that the code above is not very intuitive. We have to iterate
the child elements of each node in the reverse order and then put
the elements into a stack. However, it’s important to show this
code to prove that what can be done recursively can also be done
iteratively.

Postorder Traversal
The postorder traversal will start from the leaf nodes in the left
subtree until all the left subtree nodes are visited. Then the same
will happen in the right subtree, and finally, the root will be printed.

Recursive Postorder traversal


without Lambdas
Let’s see first how to do that without lambdas:

https://t.me/javalib
Depth-first-search 169

1 public class DepthFirstSearchPostorder {


2
3 public static void main(String[] args) {
4 dfsRecursive(GraphMock.createGraphMock());
5 }
6
7 public static void dfsRecursive(Node node) {
8 for (Node eachNode : node.getAdjacentNodes()) {
9 if (!eachNode.isVisited()) {
10 dfsRecursive(eachNode);
11 }
12 }
13
14 System.out.print(node.getValue() + " ");
15 }
16 }

Output: 5 4 3 2 7 6 1

Recursive Postorder traversal with


Lambda
Now let’s see the postorder traversal with lambda:
// Omitted class code public static void dfsRecursiveWith-
Lambda(Node node) { node.getAdjacentNodes().stream() .filter(n ->
!n.isVisited()) .forEach(DepthFirstSearchPostorder::dfsRecursiveWithLambda);
System.out.print(node.getValue() + “ “); } Output: 5 4 3 2 7 6 1
Notice in the code above that the code from the preorder graph
traversal is very similar. We only changed the place where the node
is being printed which is after the recursive invocation.

https://t.me/javalib
Depth-first-search 170

Postorder traversal with looping


Now, to see the post-order traversal within a looping it’s more
suitable to use a Binary Tree as a data structure. If we use a
graph that can have more than two children the algorithm will be
unnecessarily complex for this example.
Therefore, let’s see how to do the post-order depth-first-search
algorithm without recursion:

1 public static void dfsNonRecursive(TreeNode root) {


2 Stack<TreeNode> stack = new Stack<>();
3 stack.push(root);
4
5 while (!stack.isEmpty()) {
6 TreeNode current = stack.peek();
7 var isLeaf = current.left == null && current.right \
8 == null;
9
10 if (isLeaf) {
11 TreeNode node = stack.pop();
12 System.out.print(node.value + " ");
13 } else {
14 if (current.right != null) {
15 stack.push(current.right);
16 current.right = null;
17 }
18 if (current.left != null) {
19 stack.push(current.left);
20 current.left = null;
21 }
22 }
23 }
24 }

Output: 5 4 3 2 7 6 1

https://t.me/javalib
Depth-first-search 171

Code analysis:
Notice in the code above that we will only show the node’s data
if it’s the leaf node. Which is exactly what we need. Another
important point is that we insert data into the stack first from a
right node and then from the left node.
The stack will push the inserted node as the first element of the
stack. Since in the postorder traversal we want to print first the
left subtree, that will work perfectly. Another crucial detail here is
that we are changing the state of the right and left nodes and that
must be done! If we don’t change the state of those nodes the nodes
coming before the leaf node will never be printed. That’s because
we only print leaf nodes!
If we don’t want to change the state of our nodes, the code would
be a little bit more complicated. But it’s not really necessary to
know all the types of algorithms with graphs. We need to know
the principles instead to be able to understand and solve common
algorithms we might face in an interview or even on day-to-day
tasks.

Inorder Traversal
The inorder traversal will first print the left subtree, then the root,
and finally the right subtree.

Recursive In-order Traversal


Similarly to the postorder traversal, it’s more suitable to use this
algorithm with a binary tree. By using recursion, the algorithm is
much simpler. Let’s see the code example with recursion first:

https://t.me/javalib
Depth-first-search 172

1 public class DepthFirstSearchInorder {


2
3 public static void main(String[] args) {
4 dfsRecursive(TreeMock.createTreeMock());
5 }
6
7 public static void dfsRecursive(TreeNode node) {
8 if (node != null) {
9 dfsRecursive(node.left);
10 System.out.print(node.value + " ");
11 dfsRecursive(node.right);
12 }
13 }
14
15 }

Output: 3 2 4 5 1 6 7
Code analysis:
In the code above, we first have to check if the node is different than
null to avoid a NullPointerException. Then we traverse the left
nodes first and then methods are stacked up recursively. Let’s see
in detail what happens when invoking those methods recursively.
We start from the root, which is node 1. Then we invoke the method
recursively to the left node, then we are in node 2. Then we invoke
again the method recursively with the left node which is 3 now.
But node 3 doesn’t have either left or right nodes, therefore, the
base condition from the recursive method is fulfilled because the
next left or right node will be null.
Now the methods will start being popped out from the stack. The
first one to be popped out is node 3. Now we go back to node 2.
In node 2 we will continue the method execution to print the value
of 2 and then will invoke the right node. The right node from 2
is node 4. Then another recursive call is made with node 4 firstly

https://t.me/javalib
Depth-first-search 173

trying to invoke the left node but this one will be null. Therefore,
the method execution will continue.
In node 4 we only have the right node. Therefore, the number 4 will
be printed and the right 5 is invoked. Node 5 doesn’t have a left or
right node. Therefore the method from node 5 will be popped out
of the stack and the number 5 will be printed.
Then, the method invocation will go back to the root node and
pretty much the same on the right side.
To summarize this process, let’s see the order in the nodes are
invoked and printed:

1 1 -> 2 -> 3 -> null -> prints 3 -> null -> prints 2 -> 4 \
2 -> null -> prints 4 -> prints 5 -> null -> 5 -> prints 1
3 -> prints 6 -> null -> 6 -> prints 7 -> null -> 7

In-order Traversal with Looping


As mentioned before, all that is made using recursion can also
be made with looping. Therefore, let’s see how to implement the
inorder traversal by using looping:

1 // Omitted class code


2 public static void dfsNonRecursive(TreeNode node) {
3 Stack<TreeNode> stack = new Stack<>();
4 TreeNode current = node;
5
6 while (current != null || !stack.isEmpty()) {
7 while (current != null) {
8 stack.push(current);
9 current = current.left;
10 }

https://t.me/javalib
Depth-first-search 174

11
12 TreeNode lastNode = stack.pop();
13 System.out.print(lastNode.value + " ");
14 current = lastNode.right;
15 }
16 }

Output: 3 2 4 5 1 6 7
Code analysis:
Notice that the code above has a similar idea to the recursive
algorithm. First, we populate the stack with the left nodes. Then,
we print the latest node from the left and search for the right node.
If the right node is null, then the previously inserted elements from
the stack will be popped out and printed.
Remember that in the in-order traversal, the left leaf nodes must be
printed first. That’s why the elements from the left are stacked up
first too.

Big (O) Notation for Depth First


Search
The Big (O) notation is very important to know as a programmer.
That’s because it’s possible to roughly know what is the perfor-
mance and how much space an algorithm uses.
Time Complexity = O(Vertices+Edges) Space Complexity = O(N)
– Recursive methods count as space because those methods will
be temporarily in the memory heap using memory. The recursive
methods are stacked up and will be removed from the stack
whenever they are fully executed. Therefore, it’s not the same as
the time complexity.

https://t.me/javalib
Depth-first-search 175

Summary
The depth-first-search algorithm will visit the depth nodes first
and then the nodes closer to the root. As we’ve seen we can use
this algorithm for graphs or tree data structures.
We can use the preorder, postorder, and in-order, algorithms to
traverse graphs. Notice also that it’s much easier to use the depth-
first-search algorithms with a binary tree instead of a graph that
might be cyclic or have more than two children nodes. Therefore,
let’s recap the main points:

• The depth-first search algorithm is useful to traverse nodes


in depth.
• Recursion makes the code much simpler when implementing
a depth-first search algorithm.
• Preorder Depth-First search will start from the root, then
will traverse to the left and right subtrees.
• Post-Order Depth-First search will start from the left sub-
tree, then the right subtree, and finally the root.
• In-order Depth-First search will start from the left subtree,
the root, and finally the right subtree.
• It’s easier to implement the depth-first search algorithm with
a binary tree instead of a cyclic graph.
• A graph can be cyclic, but a tree can not be cyclic.

https://t.me/javalib
Breadth-First Search
The Breadth-First search algorithm is a way to traverse through
graphs or trees so that the nodes are visited level by level. The
depth-first search algorithm, on the other hand, will traverse nodes
to the depth-first as its name suggests, in other words, branch by
branch.
The traversal starts from the root node, in the case of the following
diagram, from the top. Node 1 will be printed first (level 1), then
node 2 and node 3 (level 2), then node 4, node 5, and node 6
(level 3), and finally, node 7 and node 8 (level 4).

https://t.me/javalib
Breadth-First Search 177

Figure 39. Breath-first-search

As mentioned above, If we traverse the above graph using the


breath-first search algorithm we will have the output of: 1 2
3 4 5 6 7 8

https://t.me/javalib
Breadth-First Search 178

Breadth-First Search with a Tree


To understand the breadth-first search algorithm make sure you
mastered the tree data structure first.
Before traversing a tree, let’s see how we define a Binary Tree via
code:

1 public class TreeNode {


2 public int value;
3 public TreeNode left;
4 public TreeNode right;
5
6 public TreeNode(int value) {
7 this.value = value;
8 right = null;
9 left = null;
10 }
11 }

Let’s populate the above tree with the same data from the diagram
we’ve seen above:

1 public class TreeMock {


2 public static TreeNode createBfsMock() {
3 TreeNode rootTreeNode = new TreeNode(1);
4 TreeNode treeNode2 = new TreeNode(2);
5 TreeNode treeNode3 = new TreeNode(3);
6 TreeNode treeNode4 = new TreeNode(4);
7 TreeNode treeNode5 = new TreeNode(5);
8 TreeNode treeNode6 = new TreeNode(6);
9 TreeNode treeNode7 = new TreeNode(7);
10 TreeNode treeNode8 = new TreeNode(8);
11
12 rootTreeNode.left = treeNode2;

https://t.me/javalib
Breadth-First Search 179

13 rootTreeNode.right = treeNode3;
14
15 treeNode2.left = treeNode4;
16 treeNode2.right = treeNode5;
17 treeNode3.right = treeNode6;
18
19 treeNode5.left = treeNode7;
20 treeNode5.right = treeNode8;
21
22 return rootTreeNode;
23 }
24 }

In the following algorithm, we will use the data structure queue


that uses the FIFO (First-in first out) strategy to insert and
retrieve data. In other words, the first element that is in will be the
first one that will be out. It’s the same situation as a real-world
queue. The first person that is in the queue, will be the first one to
be served, or the first one out of the queue.
To use this data structure in Java we will use the Queue interface
and the implementation of LinkedList. We will send the root node
to the bfsForTree method as a parameter. Then will add the root
element to the queue. Finally, we will use a while looping asking if
the queue is not empty.
Inside the while looping, we remove the first inserted element by
using the poll method. Then, we print the currentNode and add all
the adjacent (neighbor) nodes to the queue. The looping goes on
until the queue is empty:

https://t.me/javalib
Breadth-First Search 180

1 import java.util.LinkedList;
2 import java.util.Queue;
3
4 public class BreathFirstSearch {
5
6 public static void main(String[] args) {
7 bfsForTree(TreeMock.createBfsMock());
8 }
9
10 public static void bfsForTree(TreeNode node) {
11 Queue<TreeNode> queue = new LinkedList<>();
12 queue.add(node);
13
14 while (!queue.isEmpty()) {
15 var currentNode = queue.poll();
16
17 if (currentNode != null) {
18 System.out.print(currentNode.value + " ");
19 queue.add(currentNode.left);
20 queue.add(currentNode.right);
21 }
22 }
23 }
24
25 }

Output: 1 2 3 4 5 6 7 8

Breadth-First Search Traversal with


Graph
Similarly to traversing a tree, we can also traverse a graph that has
cycles. The code doesn’t change too much to traverse a graph.

https://t.me/javalib
Breadth-First Search 181

Now let’s see in code how we describe a Graph and how to populate
it as the above diagram in code:

1 public class Node {


2
3 Object value;
4 private List<Node> adjacentNodes = new ArrayList<>();
5 private boolean visited;
6
7 public Node(Object value) {
8 this.value = value;
9 }
10
11 public void addAdjacentNode(Node node) {
12 this.adjacentNodes.add(node);
13 }
14
15 public Object getValue() {
16 visited = true;
17 return value;
18 }
19 // Omitted other methods...
20 }

Important detail: Notice that in the Node class we


have the getValue method that gets a value and also
assigns the visited flag with the value of true. That’s
because a Node from a Graph can be cyclic and this will
be the way we will control if a Node was visited or not
in our algorithm.

https://t.me/javalib
Breadth-First Search 182

1 public class GraphMock {


2
3 public static Node createBfsMock() {
4 Node rootNode = new Node(1);
5 Node node2 = new Node(2);
6 Node node3 = new Node(3);
7 Node node4 = new Node(4);
8 Node node5 = new Node(5);
9 Node node6 = new Node(6);
10 Node node7 = new Node(7);
11 Node node8 = new Node(8);
12
13 rootNode.addAdjacentNode(node2);
14 rootNode.addAdjacentNode(node3);
15 node2.addAdjacentNode(node4);
16 node2.addAdjacentNode(node5);
17 node4.addAdjacentNode(node2); // Makes this Graph Moc\
18 k Cyclic
19 node5.addAdjacentNode(node7);
20 node5.addAdjacentNode(node8);
21
22 rootNode.addAdjacentNode(node3);
23 node3.addAdjacentNode(node6);
24
25 return rootNode;
26 }
27 }

As commented in the code above, we are creating a cyclic Graph.


That’s because node2 adds the adjacent node4 and node4 adds the
adjacent node2.
Finally, let’s implement the breadth-first search algorithm for a
graph. Remember that the main difference between a graph and
a tree is that a graph can be cyclic.
Therefore, we need to ask if the node was already visited, if

https://t.me/javalib
Breadth-First Search 183

we don’t do that we would get an infinite looping because as


mentioned before, the graph we are using is cyclic.

1 import java.util.ArrayDeque;
2 import java.util.Queue;
3
4 public class BreathFirstSearch {
5
6 public static void main(String[] args) {
7 bfsForGraph(GraphMock.createBfsMock());
8 }
9
10 public static void bfsForGraph(Node node) {
11 Queue<Node> queue = new LinkedList<>();
12 queue.add(node);
13
14 while (!queue.isEmpty()) {
15 var currentNode = queue.poll();
16 if (!currentNode.isVisited()) {
17 System.out.print(currentNode.getValue() + " ");
18 queue.addAll(currentNode.getAdjacentNodes());
19 }
20 }
21 }
22
23 }

Output: 1 2 3 4 5 6 7 8

Breadth-First Search Big(O) Notation


Before checking out the complexity keep in mind that v = vertices
and e = edges.

https://t.me/javalib
Breadth-First Search 184

Time Complexity: When we traverse through a Graph, the com-


plexity will be O(v+e) because for every vertice that is inserted into
the queue the child nodes will be also inserted. The number of
child nodes is exactly the number of edges. Therefore, the time
complexity will be O of vertices + edges.
Space Complexity: O(v) because the data inserted into the queue
will be exactly the number of vertices from the graph or tree.
Important: To learn more and fully absorb this algorithm,
get the code (https://github.com/rafadelnero/java-
algorithms/tree/main/src/main/java/fundamentals/bfs)[https://github.com/rafadeln
algorithms/tree/main/src/main/java/fundamentals/bfs] and run
your own tests.

Summary
– The Breadth-first Search algorithm uses a queue FIFO (First-
in First-out) approach. – The nodes are visited level by level. –
There is no need to check if the node was visited when traversing
a tree since a tree can’t be cyclic. – When we traverse a graph, it’s
necessary to check if the node was already visited, otherwise, there
will be an infinite loop.

https://t.me/javalib
Next Steps
Now that you have a strong foundation with fundamentals, it’s
time to try interviews. Also, make sure you will practice algo-
rithms. Once you know the fundamentals it’s much easier to solve
them.
Those are the websites I recommend you to try out:
https://www.algoexpert.io https://leetcode.com https:
//www.hackerrank.com
Make sure your CV is aligned to what the market wants because
that will make the difference if they will call you for the interview
or not.
Don’t get intimidated by the amount of technologies you need to
study also, instead, try out the interview and see how it goes.

Fundamentals are the Key


You probably noticed that this book is focused on fundamentals.
That’s because if you know the fundamentals well enough, you will
learn other technologies much faster!
Notice that all programming languages use data structures and
algorithms behind the scenes. Therefore, you will learn other
programming languages much faster because you know the fun-
damentals well.
The same is valid with the fundamentals of systems design. Learn-
ing a high-level technology makes us have to learn a lot more.
That’s because if the fundamentals are not clear, we will have to
learn tools, not how that works exactly.

https://t.me/javalib
Next Steps 186

Therefore, the key to learn faster is to learn the fundamentals well


and if you followed this book, you now know a lot more!

Be Risk Taker
Paradoxically, taking risks is much safer than not taking risks at all.
Imagine a developer who stays in the same company for more than
10 years and only uses old technologies. Then, suddenly his boss
reaches out to him and get him fired.
Therefore, because the developer didn’t go out of his comfort zone,
now he has to face the market with skills that are obsolete and he
will be looking for jobs without a job which is not a good position.
If the developer had tried to look other companies and had some
interviews he would know what the market was asking. But
because he didn’t take any riks, now he has to do lots of interviews
without preparation.
Remember, always take calculated risks, don’t be the accomodated
developer. Be an action taker instead!

Don’t Focus Only On Technical Skills


To go beyond in your career, you need to focus on soft skills as well.
Yes, I know this is not something we developers like very much but
we have to understand that they are crucial for our career growth.
Skills such as communication, negotiation, and marketing are
crucial for us. I never liked studying those skills but when I noticed
how important it is for the career and even life in general, I read
several books to get better on those areas.
Then my career sky rocketed and to share that with more devel-
opers I created the Challenger Developer course to share with you
what you can do to get better on those skills!

https://t.me/javalib
Next Steps 187

Interview Mindset
To pass in interviews and don’t give up, you need to have a
calibrated mindset. You need to be resilient, be able to receive
feedback and not allow your ego take control of yourself.
Many developers get frustrated, feel they are not enough, or feel
they are not even worthy of being a software engineer after being
rejected in interviews.
If you feel like that or you simply want to go to the next level faster
I strongly suggest you to take the next step and have Challenger
Developer course so you empower yourself to get the best Java jobs
with high salary.
Remember, the best investment you can ever make is to invest
in yourself. Also, it’s the action taker the one who will make a
difference.

Conclusion
That’s all challenger, I am happy to see you finishing this book, I
know it’s not an easy book but it’s those books who will make you
go to the next level.
Also, adopt the mindset of never stop learning, remember, we
need to be growing constantly and this is fun! Just be careful to not
learn too much and never use the knowledge. Learn something so
can you do something. Otherwise, it’s a waste of your time.
Take your next step and do the Challenger Developer course so you
go to the next level in your career! https://javachallengers.com/
challenger-developer-course

https://t.me/javalib

You might also like