lc_1
lc_1
Interview Guide
Learning Data Structures and Algorithms
with LeetCode
Li Yin1
1
https://liyinscience.com
ii
iv
Contents
0 Preface 1
I Introduction 7
v
vi CONTENTS
5 Algorithm Analysis 43
5.1 Time Complexity . . . . . . . . . . . . . . . . . . . . . . . . . 43
5.1.1 Solve Recurrence Function . . . . . . . . . . . . . . . . 44
5.1.2 Time Complexity . . . . . . . . . . . . . . . . . . . . . 44
5.2 Space Complexity . . . . . . . . . . . . . . . . . . . . . . . . . 46
5.2.1 Summary . . . . . . . . . . . . . . . . . . . . . . . . . 46
5.2.2 More Examples . . . . . . . . . . . . . . . . . . . . . . 46
5.3 Amortized Analysis . . . . . . . . . . . . . . . . . . . . . . . . 47
xi
xii LIST OF FIGURES
5.2 The cheat sheet for time and space complexity with recur-
rence function. If T(n) = T(n-1)+T(n-2)+...+T(1)+O(n-1)
= 3n . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 46
xv
xvi LIST OF TABLES
Preface
1
2 0. PREFACE
Preface
Interview is the intermediate stage in our life between the long schooling and
the real-world employment and contribution. It is a process that literally
everyone must go through and trust me it is definitely not an easy and
short one. Graduating with a Computer science or engineering degree?
Dreaming of getting a job as a software engineer in game-playing companies
like Google, Facebook, Amazon, Microsoft, Oracle, LinkedIn, and on and
on? While, unluckily, for this type of jobs, the interview process could be
the most challengingable among all the interviews because they do ”coding
interview", with the interviewer scrutinizing every punch of your typing,
while at the same time you need to express whatever that is on your mind
to help them get easier to understand what is going on with you. Without
passing the ”coding interview" you do not even get a chance to discuss about
your passion about the company, your passion about the job, or your passion
about your life. They just donot care before you can approve that you are
a competitive ”coder".
Normally, how would you prepare for your interview? You Google ”How
to prepare for the coding interview for A company?" and figured out that
you need Introduction to Algorithms, Cracking the Coding Interview as the
basic knowledge base. Then there is the online coding practice website,
LeetCode, LintCode. Also, you need to pick a programming language, the
inconsistency of all these books and resources that they might use pseu-
docode or used Java, while you just personally prefer Python. Because why
not Python? It is the most popular and easy to use programming language
and it would constantly gain more popularity due to the rise of deep learn-
ing, machine learning, artificial intelligence. Now, you have a plan in your
mind. You pull out your first or second year college textbook Introduc-
tion to Algorithms from bookshelf and start to read this 1000-pages massive
book to refresh your mind about the basic algorithms, divide and conquer,
dynamic programming, greedy algorithm... After this, you go through the
3
4 0. PREFACE
coding interview, or you do both of them at the same time. After that,
you would search online to get another book that used Python to handle
the common algorithms (do a research of this book). What a tedious, high-
intensive, stressful and long process to just prepare for an interview. You
would think after this, you are done with the interview, but for software
engineers, it is not uncommon to change jobs every two or three years, then
you need to start the whole process again unless you are a good recorder
that documented your code and everything.
While, good news, this book which is written by a person who has gone
through the whole suffering stage of interview preparation, can ease your
interview and provide one-stop information and preparation for your ”coding
interview". We integrate seamlessly of the online resource about interview
process, the algorithm concepts from Introduction to Algorithms, the real
interviewing problems from LeetCode, the combination of the algorithms
and Python language, plus various of concise and hacking algorithms that
can solve common interviewing problems with the best complexity which
are categorized by the author in order to make the whole picture clearer
and the process easier (Section 4).
We do not simply putting all the contents together, the arrangement of
chapter order, the amount of content of each chapter, the connection of each
chapter, the explanation of the mindset are based on the LeetCode statistics
and real interviewing need.
Therefore, the purpose of this book is two-fold: to answer your questions
about interview process and prepare you fully for the ”coding interview"
part.
The following diagram based on the connection between different algo-
rithms also represents how we organize our contents:
Structure of book
In this book, we first introduce the full interview process to a job, offer
a preparation guidance, and current available and helpful online resources
to help you prepare; including informational websites, communities, and
interview real mocking resource. Second, in our book, each algorithm or
data structure chapter is customized based on their frequency, and difficulty
appearing in the real coding interviews using statistics from LeetCode, the
5
pointers, prefex sum, kadane algorihtm and so on for different type of array
problems such as subarray, subsequence, sum and so on.
The last Part ?? will offer solutions to all the exercises used in this
book.
However, we have extra outline at the end that would have another
outline follow the leetcode problems. This, way it would be quicker if you
already read the book, and just want to review the questions with certain
order.
All the code will be shown in a Jupyter Notebook, and can be downloaded
or forked from the github directory.
A reference and simpler content can be found here: http://interactivepython.org/runestone/st
Acknowledgements
• A special word of thanks goes to Professor Don Knuth1 (for TEX) and
Leslie Lamport2 (for LATEX).
• I’m deeply indebted my parents, colleagues and friends for their sup-
port and encouragement.
Amber Jain
http://amberj.devio.us/
1
http://www-cs-faculty.stanford.edu/~uno/
2
http://www.lamport.org/
3
http://gummi.midnightcoding.org/
4
http://projects.gnome.org/latexila/
Part I
Introduction
7
1
9
10 1. CODING INTERVIEW AND RESOURCES
of the coding interviews when you are doing it: either the online with screen
sharing or the on-site with whiteboard.
Interviewing.io Use the website interviewing.io, you can have real mock-
ing interviews given by software engineers working in top tech company.
This can greatly help you overcome the fear, tension. Also, if you do well
in the practice interviews, you can get real interviewing opportunities from
their partnership companies.
Interviewing is a skill that you can get better at. The steps mentioned
above can be rehearsed over and over again until you have fully internalized
them and following those steps become second nature to you. A good way
to practice is to find a friend to partner with and the both of you can take
turns to interview each other.
A great resource for practicing mock coding interviews would be in-
terviewing.io. interviewing.io provides free, anonymous practice technical
interviews with Google and Facebook engineers, which can lead to real jobs
and internships. By virtue of being anonymous during the interview, the
inclusive interview process is de-biased and low risk. At the end of the inter-
view, both interviewer and interviewees can provide feedback to each other
for the purpose of improvement. Doing well in your mock interviews will
unlock the jobs page and allow candidates to book interviews (also anony-
mously) with top companies like Uber, Lyft, Quora, Asana and more. For
those who are totally new to technical interviews, you can even view a demo
interview on the site (requires sign in). Read more about them here.
Aline Lerner, the CEO and co-founder of interviewing.io and her team
are passionate about revolutionizing the technical interview process and
helping candidates to improve their skills at interviewing. She has also
published a number of technical interview-related articles on the interview-
ing.io blog. interviewing.io is still in beta now but I recommend signing up
as early as possible to increase the likelihood of getting an invite.
Communities
If you understand Chinese, there is a good community 3 that we share infor-
mation with either interviews, career advice and job packages comparison.
Coding
Geekforgeeks
For example, For example, the maximum size of input n is 100K, or 105
(1K = 1, 000), and your algorithm is of order O(n2 ). Your common sense
told you that (100K)2 is an extremely big number, it is 101 0. So, you will try
to devise a faster (and correct) algorithm to solve the problem, say of order
O(n log2 n). Now 105 log2 105 is just 1.7 × 106 . Since computer nowadays
are quite fast and can process up to order 1M, or 106 (1M = 1, 000, 000)
operations in seconds, your common sense told you that this one likely able
to pass the time limit.
1.3 LeetCode
LeetCode is a website where you can practice on real interviewing questions
used by tech companies such as Facebook, Amazon, Google, and so on.
17
18 2. DATA STRUCTURES AND ALGORITHMS
Greedy (Part V). In Section 2.1, we will briefly introduce and compare
these four problem solving paradigms to gain a global picture of the
spirit of algorithms.
1. Complete Search
3. Dynamic Programming
4. Greedy Algorithm
2.1. FOUR PROBLEM SOLVING PARADIGMS 19
Figure 2.2: State Transfer Tree Structure for LIS, each path represents a
possible solution. Each arrow represents an move: find an element in the
following elements that’s larger than the current node.
1. Model the problem as a directed graph, where each node is the ele-
ments of the array, and an edge µ to v means node v > µ. The problem
now because finding the longest path from any node to any node in
this directed graph.
2. Model the problem as a tree. The tree starts from empty root node, at
each level i, the tree has n-i possible children: nums[i+1], nums[i+2],
..., nums[n-1]. There will only be an edge if the child’s value is larger
than its parent. Or we can model the tree as a multi-choice tree: for
2.3. COMPLEXITY ANALYSIS 21
The above example is to show us, how learning and practice using the
data structures and four problem solving paradigms can help us making
smarter decision about problem modeling and problem solving.
2. Best Case: in the best scenario, the data is arranged in a way, that
your algorithms run least amount of time. When we are analyzing the
complexity of algorithms, to think of the best case can help us come
up with a very reasonable lower bound, Ω.
3. Worst Case: in the worst scenario, its the opposite. This gave us a
way to measure the upper bound of the complexity, which is denoted
as O
server, we need to response the request of users, then decrease the response
time if especially useful here. Normally we want to decrease the time com-
plexity by sacrificing more space if the extra space is not a problem for the
physical machine. But in some cases, decrease the time complexity is more
important and needed, thus we need might go for alternative algorithms
that uses less space but might with more time complexity.
Therefore, to complete the story, for input with data size n, if we can
get time complexity O(n), which is called liner time. This is a very good
performance. If we do better, in some cases, you can get O(1), which means
constant complexity, however, we only get constant complexity in either
time or space, not both. There are complexity denoted as O(nd ), we call
power order, and for O(dn ), we call this exponentially complexity. There
are also the case of O(lgn), logarithm of n, which sometimes come together
with n as O(nlgn). By seeing more complexity algorithms in Part III, ??
and ?? we can have more sense of the complexity analysis.
Also, we provide the average and worst time and space complexity for
the some classical data structure’s operations (shown in Fig. 2.5) and of
24 2. DATA STRUCTURES AND ALGORITHMS
Fundamental Algorithm
Design and Analysis
27
29
Figure 3.1: Iteration vs recursion: in recursion, the line denotes the top-
down process and the dashed line is the bottom-up process.
31
32 3. ITERATION AND RECURSION
3.2 Recursion
Overflow problem
3.3 Summary
A conditional statement decides the termination of recursion while a control
variable’s value decide the termination of the iteration statement (except in
the case of a while loop). Infinite recursion can lead to system crash whereas,
infinite iteration consumes CPU cycles. Recursion repeatedly invokes the
mechanism, and consequently the overhead, of method calls. This can be
expensive in both processor time and memory space while iteration doesn’t.
Recursion makes code smaller while iteration makes it longer.
34 3. ITERATION AND RECURSION
4
In this chapter, we learn our the first and the most fundamental paradigm
of the four we introduced – Divide and Conquer. Recursion in most pro-
gramming languages are a recursive function that calls itself and return till
it hits its base cases. Divide-Conquer and Recursion are inseparable with
each other, as shown in Fig 4.1, with divide and conquer, a problem is
break down to two subproblems, recursively, till we reach to the base cases
35
36 4. DIVIDE AND CONQUER
where there solutions is trivial to solve. The relation between the problems
and its subproblems can be represented with Recurrence Function such as
T (n) = 2 ∗ T (n/2) + f (n), where for the original problem of size n, it was
break into 2 subproblems each has a size n/2, and f(n) is the cost function
denotes the time complexity it takes to merge the result of the two sub-
problems back to the solution of the current problem of size n. Therefore,
recurrence function and a tree structure is a nature way for us to either
visualize the process or deduct the time complexity.
F (n) = Comb(F (0, n/b), F (n/b + 1, 2n/b), ..., F (n − n/b, n)) (4.2)
Or in Recursion calls,
Example 1: Merge Sort The concept can be quite dry, let us look at
a simple example of merge sort. Given an array, [2, 5,1,8,9], the task is to
sort the array to [1, 2, 5, 8, 9]. To apply divide and conquer, we first divide
it into two halves: [2, 5, 1], [8, 9], sort each half and with return result [1,
2, 5], [8, 9], and now we just need to merge the two parts. The process can
be represented as the following:
1 d e f d i v i d e _ c o n q u e r (A, s , e ) :
2 # b a s e c a s e , can not be d i v i d e d f a r t h e r
3 i f s == e :
4 r e t u r n A[ s ]
5 # d i v i d e i n t o n / 2 , n/2 from middle p o s i t i o n
6 m = ( s+e ) // 2
7
8 # co nqu er
9 s 1 = d i v i d e _ c o n q u e r (A, s , m)
10 s 2 = d i v i d e _ c o n q u e r (A, m+1 , e )
11
12 # combine
13 r e t u r n combine ( s1 , s 2 )
The above is the classical Fibonacci Sequence, to get the fibonacci number
at position n, we first need to know the answer for subproblems f(n-1) and
f(n-2), we can solve it easily using recursion function:
1 def f i b (n) :
2 i f n <= 1 :
3 return n
4 r e t u r n f i b ( n−1) + f i b ( n−2)
38 4. DIVIDE AND CONQUER
The above recursion function has recursion tree shown in Fig 4.2b. And we
also draw the recursion tree of recursion function call for merge sort and
shown in Fig 4.2a. We notice that we call f(2) multiple times for fibonacci
but in the merge sort, each call is unique and wont be called more than
once. The recurrence function of merge sort is T (n) = 2 ∗ T (n/2) + n, and
for fibonacci sequence it is T (n) = T (n − 1) + T (n − 2) + 1.
In the following book, we use divide and conquer to refer to the first category
of dividing a problem into non-overlapping subproblems as Eq. 4.1. Follow
this, we give the official definition of Divide-and-conquer.
3. Combine: combine the solution from each subproblem into the solu-
tion to the current problem.
4.3. MORE EXAMPLES 39
For example , g i v e n t h e a r r a y [ − 2 , 1 , − 3 , 4 , − 1 , 2 , 1 , − 5 , 4 ] ,
t h e c o n t i g u o u s s u b a r r a y [ 4 , − 1 , 2 , 1 ] has t h e l a r g e s t sum =
6.
Solution: divide and conquer. T (n) = max(T (lef t), T (right), T (cross)),
max is for merging and the T(cross) is for the case that the po-
tential subarray across the mid point. For the complexity, T (n) =
2T (n/2) + n, if we use the master method, it would give us O(nlgn).
We write the following Python code
1 d e f maxSubArray ( s e l f , nums ) :
2 """
3 : type nums : L i s t [ i n t ]
4 : rtype : int
5 """
6 d e f getCrossMax ( low , mid , h i g h ) :
7 left_sum , right_sum =0 ,0
8 left_max , right_max = −maxint , −maxint
9 l e f t _ i , r i g h t _ j =−1,−1
10 f o r i i n xrange ( mid , low −1,−1) : #[ )
11 left_sum+=nums [ i ]
12 i f left_sum>left_max :
13 left_max= left_sum
14 left_i = i
15 f o r j i n xrange ( mid+1, h i g h +1) :
16 right_sum+=nums [ j ]
17 i f right_sum>right_max :
18 right_max= right_sum
19 right_j = j
20 r e t u r n ( l e f t _ i , r i g h t _ j , left_max+right_max )
21
22 d e f maxSubarray ( low , h i g h ) :
23 i f low==h i g h :
24 r e t u r n ( low , high , nums [ low ] )
25 mid = ( low+h i g h ) //2
26 r s l t =[]
27 #l e f t _ l o w , l e f t _ h i g h , left_sum = maxSubarray (
low , mid ) #[low , mid ]
28 r s l t . append ( maxSubarray ( low , mid ) ) #[ low , mid ]
29 #right_low , r i g h t _ h i g h , right_sum = maxSubarray (
mid+1 , h i g h ) #[mid+1 , h i g h ]
30 r s l t . append ( maxSubarray ( mid+1 , h i g h ) )
31 #cross_low , c r o s s _ h i g h , cross_sum = getCrossMax (
low , mid , h i g h )
32 r s l t . append ( getCrossMax ( low , mid , h i g h ) )
33 r e t u r n max( r s l t , key=lambda x : x [ 2 ] )
34 r e t u r n maxSubarray ( 0 , l e n ( nums ) −1) [ 2 ]
Also, we does not necessarily to use divide and conquer, we can be more
creative and try harder to make the time complexity goes to O(n). We
can convert this problem to best time to buy and sell stock problem.[0,
-2, -1, -4, 0, -1, 1, 2, -3, 1], => O(n), then we use prefix_sum, the
difference is we set prefix_sum to 0 when it is smaller than 0, O(n)
4.3. MORE EXAMPLES 41
Algorithm Analysis
43
44 5. ALGORITHM ANALYSIS
get to the leaves. The computational complexity for this recursion would be
the sum of all layers’s costs. And we assume T (1) = 1.
Figure 5.1: The process to construct a recursive tree for T (n) = 3T (bn/4c)+
cn2
3 2 3 3
T (n) = cn2 + cn + ( )2 cn2 + ... + ( )log4 n−1 cn2 + Θ(nlog4 3 )
16 16 16
log4 n−1
3 i 2
= ( ) cn + Θ(nlog4 3 )
X
i=0
16
∞
3 (5.1)
( )i cn2 + Θ(nlog4 3 )
X
<
i=0
16
1
= cn2 + Θ(nlog4 3 )
1 − (3/16)
= O(n2 ).
Master Method
The master method is probably the easiest way to come up with the compu-
tational complexity analysis. It is a theorem that are proved by researchers,
and we just need to learn how to use them. The master theorem goes:
For Eq. 14.2, let a ≥ 1, b > 1, we first compute nlogb a ,
46 5. ALGORITHM ANALYSIS
1. If f (n) = O(nlogb a− for constant > 0, then we get T (n) = Θ(nlogb a ).
3. If f (n) = Ω(nlogb a+ for constant > 0, and if af (n/b) ≤ cf (n) for
constant c < 1 and all sufficiently large n, then we get T (n) = Θ(f (n)).
5.2.1 Summary
For your convenience, we prove a table that shows the frequent used recur-
rence equations’ time complexity.
Figure 5.2: The cheat sheet for time and space complexity with recurrence
function. If T(n) = T(n-1)+T(n-2)+...+T(1)+O(n-1) = 3n
49
51
In the programming, data structures are used to store data and make
operations on them so that we can conduct different algorithms on them in
order to solve real-world problems and meet certain efficiency. The compar-
ison between varies of data structures are highly dependable on the context
of the problem we are facing. Being familiar with data structures is a must
for us to understand and implement algorithms following this part. The
concepts of data structures and the real data types and/or built-in modules
in Python goes hand in hand for the real understanding. Thus, in our book,
we insist on learning the concepts, real implementation with basic built-in
data types: list/ dict/ string/ together. On this base, we learn built-in mod-
ules which implements these data structures for us directly and with good
efficiency.
On high level, data structures can be categories as two main types: Liner
(Chapter 6) and Non-liner (include: Heap and Graph in Chapter 7 and
Chapter ??). Before we move ahead to learn these data structures, it is
essential for us to understand how normally data structures are categorized
based on specific characters:
• Mutable vs Immutable In the sense of if modification of the items
of the data structures is allowed, there are mutable and immutable
data structures.
• Static vs Dynamic Moreover, we can categorize the data structures
as static data structures and dynamic data structures according to if
we can change the size of the created data structures. In static data
structure the size of the structure is fixed since its creation. While, in
dynamic data structure, the size of the structure is not fixed and can be
modified through operations such as Insertion and Append. Dynamic
data structures are designed to facilitate change of data structures in
the run time.
The implementation of different data structures can vary as the pro-
gramming languages. To make the contents more compact and make the
reference more convenient, in this part, we combine data strutures from the
programming literature with corresponding data structures (either built-in
or external modules) come from Python. Due to this understanding, for
each data structure, the contents are organized as:
• firstly we will introduce the concept of the data structures including
definition, pros, and cons;
• secondly, the common basic operations with concepts and time com-
plexity: Access, Search, Insertion, Deletion.
• lastly, to complete the picture, we introduce Python’s data structures
(either built-in or external) with their methods and corresponding op-
erations.
52
• Introducing the built-in data structures include list, string, and tu-
ple which are arrays but each come with different features and popular
module as a complement;
• Understanding the concept of the linked list, either single linked list
or the doubly linked list, and the Python implementation of each data
structure.
6.1 Array
An array is container that holds a fixed size of sequence of items stored at
contiguous memory locations and each item is identified by array index
or key. The Array representation is shown in Fig. 6.1. Since using contiguous
memory locations, once we know the physical position of the first element,
an offset related to data types can be used to access any other items in the
array with O(1). Because of these items are physically stored contiguous
one after the other, it makes array the most efficient data structure to store
and access the items. Specifically, array is designed ans used for fast random
access of data.
Static Array VS Dynamic Array There are two types of array: static
array and dynamic array. They are different in the matter of fixing size or
53
54 6. LINEAR DATA STRUCTURE
not. In the static array, once we declared the size of the array, we are not
allowed to insert or delete any item at any position of the array. This is
due to the inefficiency of doing so, which can lead to O(n) time and space
complexity. For dynamic array, the fixed size restriction is removed but with
high price to allow it to be dynamic. However, the flip side of the coin is that
if the memory size of the array is beyond the memory size of your computer,
it could be impossible to fit the entire array in, and then we would retrieve
to other data structures that would not require the physical contiguity, such
as linked list, trees, heap, and graph.
Commonly, arrays are used to implement mathematical vectors and ma-
trices. Also, arrays are the basic units implementing other data structures,
such as hashtables, heaps, queues, stacks. We will see from other remaining
contents of this part that how array-based Python data structures are used
to implement the other data structures. On the LeetCode, these two data
structures are involved into 25% of LeetCode Problems. To note that it is
not necessarily for array data structure to have the same data type in the
real implementation of responding programming language as Python.
• Access: it takes O(1) time to access one item in the array given the
index;
• Search and Iteration: O(n) time for array to iterate all the elements
in the array. Similarly to search an item by value through iteration
takes O(n) time too.
In Python, there is no strictly defined built-in data types that are static
array. But it does have three there are built-in Array-like data structures:
List, Tuple, String, and Range. These data structures are different in the
sense of mutability, static or dynamic. More details of these data structures
in Python will be given in the next section. module array which is of same
6.1. ARRAY 55
data types just as the definition of Array here is also implemented the same
as in C++ with its array data structure. However, array is not as widely
used as of these three Items in list are actually not consecutive in memory
because it is mutable object. Memory speaking, list is not as efficient as
module Array and Strings.
1
can modify item after its creation
2
56 6. LINEAR DATA STRUCTURE
Negative indexing and slicing For indexing and slicing, we can pass by
negative integer as index and stride. The negative means backward, such
as -1 refers to the last item, -2 to the second last item and so on. For the
slicing
We list other characters and operations for each of these sequence data
types:
Use List as Static Array Therefore, list is better used with fixed size,
and no operation that incur items shifting such as pop(0) and insert(0, v),
or operation that incurs size growth such as append(). Because, it pre-alloc
a fixed size and once the size larger than this size, a new larger array is
made and everything inside the old array is copied over, then the old array
is marked for deletion. For example, we new a fixed size list, and we can do
slicing, looping, indexing.
1 lst1 = [3]∗5 # new a l i s t s i z e 5 with 3 a s i n i t i a l i z a t i o n
2 l s t 2 = [ 4 f o r i in range (5) ]
3 f o r idx , v i n enumerate ( l s t 1 ) :
4 l s t 1 [ i d x ] += 1
1 # SEARCHING
2 p r i n t ( l s t . i n d e x ( 4 ) ) #f i n d 4 , and r e t u r n t h e i n d e x
3 # output
4 # 3
Use List as Dynamic Array When the input size is reasonable, list can
be used dynamically. Now, Table 6.3 shows us the common List Methods,
and they will be used as list.methodName().
1 # INSERTION
2 l s t . i n s e r t ( 0 , 1 ) # i n s e r t an e l e m e n t a t i n d e x 0 , and s i n c e i t i s
empty l s t . i n s e r t ( 1 , 1 ) has t h e same e f f e c t
3 print ( l s t )
4
5 l s t 2 . i n s e r t (2 , 3)
6 print ( lst2 )
7 # output
8 # [1]
9 # [2 , 2 , 3 , 2 , 2]
10 # APPEND
11 f o r i in range (2 , 5) :
12 l s t . append ( i )
13 print ( l s t )
14 # output
15 # [1 , 2 , 3 , 4]
The time complexity of different built-in method for list can be found at
wiki.python.org/moin/TimeComplexity.
If you have a lot of numeric arrays you want to work with then it is worth
using the NumPy library which is an extensive array handling library often
used by software engineers to do linear algebra related tasks.
String
String are similar to static array and it follows the restriction that it only
stores one type of data: characters represented using ASCII or Unicode 3 .
String is more compact compared with storing the characters in list. In all,
string is immutable and static, meaning we can not modify its elements or
extend its size once its created.
String is one of the most fundalmental built-in data types, this makes
managing its common methods shown in Table 6.4 and 6.5 necessary. Use
boolean methods to check whether characters are lower case, upper case, or
title case, can help us to sort our data appropriately, as well as provide us
with the opportunity to standardize data we collect by checking and then
modifying strings as needed.
Following this, we give some examples showing how to use these func-
tions.
Just as we can join strings together, we can also split strings up using
the str.split() method. This method separates the string by whitespace if
no other parameter is given.
1 print ( balloon . s p l i t () )
2 #Ouput
60 6. LINEAR DATA STRUCTURE
We can also use str.split() to remove certain parts of an original string. For
example, let’s remove the letter ’a’ from the string:
1 print ( balloon . s p l i t ( "a" ) )
2 #Ouput
3 [ ' S ' , 'mmy h ' , ' s ' , ' b ' , ' l l o o n . ' ]
Now the letter a has been removed and the strings have been separated
where each instance of the letter a had been, with whitespace retained.
The str.replace() method can take an original string and return an up-
dated string with some replacement.
Let’s say that the balloon that Sammy had is lost. Since Sammy no
longer has this balloon, we will change the substring "has" from the original
string balloon to "had" in a new string:
1 p r i n t ( b a l l o o n . r e p l a c e ( " has " , " had " ) )
2 #Ouput
3 Sammy had a b a l l o o n .
Using the string methods str.join(), str.split(), and str.replace() will provide
you with greater control to manipulate strings in Python.
Related Useful Functions Function ord() would get the int value (ASCII)
of the char. And in case you want to convert back after playing with the
number, function chr() does the trick.
1 p r i n t ( ord ( 'A ' ) )# Given a s t r i n g o f l e n g t h one , r e t u r n an i n t e g e r
r e p r e s e n t i n g t h e Unicode code p o i n t o f t h e c h a r a c t e r when
t h e argument i s a u n i c o d e o b j e c t ,
2 p r i n t ( chr (65) )
Tuple
A tuple is a sequence of immutable Python objects, which is to say the
values of tuples can not be changed once its assigned. Also, as an immutable
objects, they are hashable, and thus be used as keys to dictionaries. Like
string and lists, tuple indices start at 0, and they can be indexed, sliced,
concatenated and so on. Tuples only offer two additional methods shown in
Table 6.6.
Since, tuples are quite similiar to lists, both of them are used in similar
situations as well. However, there are certain advantages of implementing a
tuple over a list. Below listed are some of the main advantages:
• We generally use tuple for heterogeneous (different) datatypes and list
for homogeneous (similar) datatypes.
6.1. ARRAY 61
• Since tuple are immutable, iterating through tuple is faster than with
list. So there is a slight performance boost.
• Tuples that contain immutable elements can be used as key for a dic-
tionary. With list, this is not possible.
New and Initialize Tuple Tuple can be created in two different syntax:
(1) putting different comma-separated values in a pair of parentheses; (2)
creating a tuple using built-in function tuple(), if the argument to tuple() is
a sequence then this creates a tuple of elements of that sequences. See the
Python snippet:
1 ' ' ' new a t u p l e ' ' '
2
3 # c r e a t with ( )
4 tup = ( ) # c r e a t e s an empty t u p l e
5 tup1 = ( ' c r a c k ' , ' l e e t c o d e ' , 2 0 1 8 , 2 0 1 9 )
6 tup2 = ( ' c r a c k ' , ) # when o n l y has one element , put comma behind
, s o t h a t i t wont be t r a n s l a t e d a s s t r i n g
7
8 # c r e a t with t u p l e ( )
9 tup3 = t u p l e ( ) # new an empty t u p l e
10 tup4 = t u p l e ( " l e e t c o d e " ) # t h e s e q u e n c e i s p a s s e d a s a t u p l e o f
elements
11 tup5 = t u p l e ( [ ' c r a c k ' , ' l e e t c o d e ' , 2 0 1 8 , 2 0 1 9 ] ) # same a s t u p l e 1
12 p r i n t ( ' tup1 : ' , tup1 , ' \ ntup2 : ' , tup2 , ' \ ntup3 : ' , tup3 , ' \
ntup4 : ' , tup4 , ' \ ntup5 : ' , tup5 )
6.1.2 Bonus
Circular Array The corresponding problems include:
1. 503. Next Greater Element II
1 Example 1 :
2
3 I nput : [ " a1 9 2 3 1 " , " g1 a c t c a r " , " zo4 4 7 " , " ab1 o f f key
dog " , " a8 a c t zoo " ]
4 Output : [ " g1 a c t c a r " , " a8 a c t zoo " , " ab1 o f f key dog " , " a1 9
2 3 1 " , " zo4 4 7 " ]
5
6
7
8 Note :
9
10 0 <= l o g s . l e n g t h <= 100
11 3 <= l o g s [ i ] . l e n g t h <= 100
12 l o g s [ i ] i s g u a r a n t e e d t o have an i d e n t i f i e r , and a word
a f t e r the i d e n t i f i e r .
2. Extra memory space for a pointer is required with each element of the
list.
The composing unit of linked list is called nodes. There are two types of
linked lists based on its ability to iterate items in different directions: Singly
Linked List wherein a node has only one pointer to link the successive node,
and Doubly Linked List wherein a node has one extra pointer to link back
to its predecessor.
We will detail these two sub data structures of linked list in the following
sections.
Fig. 6.2 shows the structure of a singly linked list. As we can see, a
singly linked is a linear data structure with only one pointer between two
successive nodes, and can only be traversed in a single direction, that is, we
can go from the first node to the last node, but can not do it in backforward
direction.
6.2. LINKED LIST 65
1. if the node is head, directly repoint the head to the next node
1 #. . .
2 def delete ( s e l f , val ) :
3 c u r r e n t = s e l f . head
4 prev = s e l f . head
5 while current :
6 i f c u r r e n t . v a l == v a l :
7 # i f t h e node i s head
8 i f c u r r e n t == s e l f . head :
9 s e l f . head = c u r r e n t . next
10 # rewire
11 else :
12 prev . next = c u r r e n t . next
13 s e l f . s i z e −= 1
14 prev = c u r r e n t
15 c u r r e n t = c u r r e n t . next
Sometimes, we will be asked to delete a List node, this deleting process does
not need the head and the traversal to find the value. We simply need to
change the value of this node to the value of the next node, and connect this
node to the next node’s node
1 d e f deleteByNode ( s e l f , node ) :
2 node . v a l = node . next . v a l
3 node . next = node . next . next
Search and iteration: in order to traverse the list and not to expose the
users to the node class by usig node.val to get the contents of the node, we
need to implement a method iter() that returns a generator gives out the
contents of the list.
1 # ...
2 def i t e r ( s e l f ) :
3 c u r r e n t = s e l f . head
4 while current :
5 val = current . val
6 c u r r e n t = c u r r e n t . next
7 yield val
Now, the linked list iteration looks just like a normal like iteration. Search
operation can now built upon the iteration and the process is the same as
linear search:
1 # ...
2 def search ( s e l f , val ) :
3 f o r value in s e l f . i t e r () :
4 i f v a l u e == v a l :
5 r e t u r n True
6 return False
6.2. LINKED LIST 67
Clear: in some cases, we need to clear all the nodes of the list, this is a
quite simple process. All we need to do to set the head to None
1 def clear ( s e l f ) :
2 s e l f . head = None
3 self . size = 0
On the basis of Singly linked list, doubly linked list (dll) contains an
extra pointer in the node structure which is typically called prev (short
for previous) and points back to its predecessor in the list. Because of the
prev pointer, a DLL can traverse in both forward and backward direction.
Also, compared with SLL, some operations such as deletion is more efficient
because we do not need to track the previous node in the traversal process.
1 # Node o f a doubly l i n k e d l i s t
2 c l a s s Node :
3 d e f __init__ ( s e l f , v a l , prev = None , next = None ) :
4 s e l f . val = val
5 s e l f . prev = prev # r e f e r e n c e t o p r e v i o u s node i n DLL
6 s e l f . next = next # r e f e r e n c e t o next node i n DLL
Append: The only difference is to link the nodes when adding and relink-
ing when deleting.
1 # linking
2 # replace line 3 , line 9
3 node = Node ( v a l )
4 c u r r e n t . next , node . prev = node , c u r r e n t
68 6. LINEAR DATA STRUCTURE
Deletion: compared with sll, we do not need to track the previous node.
1 #. . .
2 def delete ( s e l f , val ) :
3 c u r r e n t = s e l f . head
4 #prev = s e l f . head
5 while current :
6 i f c u r r e n t . v a l == v a l :
7 # i f t h e node i s head
8 i f c u r r e n t == s e l f . head :
9 c u r r e n t . prev = None #s e t t h e prev
10 s e l f . head = c u r r e n t . next
11 # rewire
12 else :
13 #prev . next = c u r r e n t . next
14 c u r r e n t . prev . next , c u r r e n t . next . prev =
c u r r e n t . next , c u r r e n t . prev
15
16 s e l f . s i z e −= 1
17 #prev = c u r r e n t
18 c u r r e n t = c u r r e n t . next
All the remaining operations such as Search, Iteration, and Clear are exactly
the same as in sll.
6.2.3 Bonus
Tail Pointer For both singly and doubly linked list, if we add another tail
pointer to its class, which points at the last node in the list, can simplify
some operations of the linked list from O(n) to O(1).
1 d e f i t e r a t e C i r c u l a r L i s t ( head ) :
2 i f not head :
3 return
4 c u r = head
5 w h i l e c u r . next != head :
6 c u r = c u r . next
7 return
Dummy Node Dummy node is a node that does not hold any value – an
empty Node use None as value, but is in the list to provide an extra node
at the front and/or read of the list. It is used as a way to reduce/remove
special cases in coding so that we can simply the coding complexity.
6.1 83. Remove Duplicates from Sorted List (easy). Given a sorted
linked list, delete all duplicates such that each element appear only
once.
Example 1 :
I nput : 1−>1−>2
Output : 1−>2
Example 2 :
I nput : 1−>1−>2−>3−>3
Output : 1−>2−>3
Solution 2: Recursion.
1 d e f r e c u r s i v e ( node ) :
2 i f node . next i s None :
3 r e t u r n node
4
5 next = r e c u r s i v e ( node . next )
6 i f next . v a l == node . v a l :
7 # d e l e t e next
8 node . next = node . next . next
9 r e t u r n node
1. 237. Delete Node in a Linked List (easy, delete only given current
node)
6. Sort List
7. Reorder List
Fast-slow pointers:
Stacks and queue are dynamic arrays with restrictions on deleting ele-
ments. Stack data structure can be visualized a stack of plates, we would
always put a plate on top of the pile, and get one from the top of it too.
This is stated as Last in, first out (LIFO). Queue data structures are
like real-life queue in the cashier out line, it follows the rule ’first come, first
served’, which can be officiallized as first in, first out (FIFO).
72 6. LINEAR DATA STRUCTURE
Stack The implementation for stack is simplily adding and deleting ele-
ment from the end.
1 # stack
2 s = []
3 s . append ( 3 )
4 s . append ( 4 )
5 s . append ( 5 )
6 s . pop ( )
Queue For queue, we can append at the last, and pop from the first index
always. Or we can insert at the first index, and use pop the last element.
6.3. STACK AND QUEUE 73
1 # queue
2 # 1 : u s e append and pop
3 q = []
4 q . append ( 3 )
5 q . append ( 4 )
6 q . append ( 5 )
7 q . pop ( 0 )
Stack and Singly Linked List with top pointer Because in stack, we
only need to add or delete item from the rear, using one pointer pointing at
the rear item, and the linked list’s next is connected to the second toppest
item, in a direction from the top to the bottom.
1 # s t a c k with l i n k e d l i s t
2 ' ' ' a<−b<−c<−top ' ' '
3 c l a s s Stack :
4 d e f __init__ ( s e l f ) :
5 s e l f . top = None
6 self . size = 0
7
8 # push
9 d e f push ( s e l f , v a l ) :
10 node = Node ( v a l )
11 i f s e l f . top : # c o n n e c t top and node
12 node . next = s e l f . top
13 # r e s e t t h e top p o i n t e r
14 s e l f . top = node
15 s e l f . s i z e += 1
16
17 d e f pop ( s e l f ) :
18 i f s e l f . top :
19 v a l = s e l f . top . v a l
20 i f s e l f . top . next :
21 s e l f . top = s e l f . top . next # r e s e t top
22 else :
23 s e l f . top = None
24 s e l f . s i z e −= 1
25 return val
26
27 e l s e : # no e l e m e n t t o pop
28 r e t u r n None
74 6. LINEAR DATA STRUCTURE
Queue and Singly Linked List with Two Pointers For queue, we need
to access the item from each side, therefore we use two pointers pointing at
the head and the tail of the singly linked list. And the linking direction is
from the head to the tail.
1 # queue with l i n k e d l i s t
2 ' ' ' head−>a−>b−> t a i l ' ' '
3 c l a s s Queue :
4 d e f __init__ ( s e l f ) :
5 s e l f . head = None
6 s e l f . t a i l = None
7 self . size = 0
8
9 # push
10 d e f enqueue ( s e l f , v a l ) :
11 node = Node ( v a l )
12 i f s e l f . head and s e l f . t a i l : # c o n n e c t top and node
13 s e l f . t a i l . next = node
14 s e l f . t a i l = node
15 else :
16 s e l f . head = s e l f . t a i l = node
17
18 s e l f . s i z e += 1
19
20 d e f dequeue ( s e l f ) :
21 i f s e l f . head :
22 v a l = s e l f . head . v a l
23 i f s e l f . head . next :
24 s e l f . head = s e l f . head . next # r e s e t top
25 else :
26 s e l f . head = None
27 s e l f . t a i l = None
28 s e l f . s i z e −= 1
29 return val
30
31 e l s e : # no e l e m e n t t o pop
32 r e t u r n None
Also, Python provide two built-in modules: Deque and Queue for such
purpose. We will detail them in the next section.
Table 6.8: Datatypes in Queue Module, maxsize is an integer that sets the
upperbound limit on the number of items that can be places in the queue.
Insertion will block once this size has been reached, until queue items are
consumed. If maxsize is less than or equal to zero, the queue size is infinite.
Class Data Structure
class queue.Queue(maxsize=0) Constructor for a FIFO queue.
class queue.LifoQueue(maxsize=0) Constructor for a LIFO queue.
class queue.PriorityQueue(maxsize=0) Constructor for a priority queue.
Table 6.9: Methods for Queue’s three classes, here we focus on single-thread
background.
Class Data Structure
Queue.put(item[, block[, timeout]]) Put item into the queue.
Queue.get([block[, timeout]]) Remove and return an item from the
queue.
Queue.qsize() Return the approximate size of the
queue.
Queue.empty() Return True if the queue is empty,
False otherwise.
Queue.full() Return True if the queue is full, False
otherwise.
Now, using Queue() and LifoQueue() to implement queue and stack re-
spectively is straightforward:
1 # python 3
2 import queue
3 # implementing queue
4 q = queue . Queue ( )
5 f o r i in range (3 , 6) :
6.3. STACK AND QUEUE 77
6 q . put ( i )
1 import queue
2 # imp lementing s t a c k
3 s = queue . LifoQueue ( )
4
5 f o r i in range (3 , 6) :
6 s . put ( i )
The process of the monotone decresing stack is shown in Fig. 6.5. Some-
times, we can relax the strict monotonic condition, and can allow the stack
or queue have repeat value.
To get the feature of the monotonic queue, with [5, 3, 1, 2, 4] as example,
if it is increasing:
78 6. LINEAR DATA STRUCTURE
• Popping out to get smaller/larger item to the right: when we pop one
element out, for the kicked out item, such as in step of 2, increasing
stack, 3 forced 5 to be popped out, for 5, 3 is the first smaller item
to the right. Therefore, if one item is popped out, for this item, the
current item that is about to be push in is 1) for increasing stack,
the nearest smaller item to its right, 2) for decreasing stack, the
nearest larger item to its right. In this case, we get [3,1, -1, -1,
-1], and [-1, 4, 2, 4, -1] respectively.
10 f i r s t L a r g e r T o L e f t [ i ] = A[ s t a c k [ − 1 ] ]
11 s t a c k . append ( i )
12 return firstLargerToLeft , firstLargerToRight , stack
For the above problem, If we do it with brute force, then use one for loop
to point at the current element, and another embedding for loop to look
for the first element that is larger than current, which gives us O(n2 ) time
complexity. If we think about the BCR, and try to trade space for efficiency,
and use monotonic queue instead, we gain O(n) linear time and O(n) space
complexity.
Monotone stack is especially useful in the problem of subarray where we
need to find smaller/larger item to left/right side of an item in the array.
To better understand the features and applications of monotone stack, let
us look at some examples. First, we recommend the audience to practice
on these obvious applications shown in LeetCode Problem Section before
moving to the examples:
There is one problem that is pretty interesting:
Window p o s i t i o n Max
−−−−−−−−−−−−−−− −−−−−
[ 1 3 −1] −3 5 3 6 7 3
1 [ 3 −1 −3] 5 3 6 7 3
1 3 [ −1 −3 5 ] 3 6 7 5
1 3 −1 [ −3 5 3 ] 6 7 5
1 3 −1 −3 [ 5 3 6] 7 6
1 3 −1 −3 5 [ 3 6 7] 7
Analysis: In the process of moving the window, any item that is smaller
than its predecessor will not affect the max result anymore, therefore, we
can use decrese stack to remove any trough. If the window size is the same
as of the array, then the maximum value is the first element in the stack
6.3. STACK AND QUEUE 81
(bottom). With the sliding window, we record the max each iteration when
the window size is the same as k. At each iteration, if need to remove the
out of window item from the stack. For example of [5, 3, 1, 2, 4] with k =
3, we get [5, 3, 4]. At step 3, we get 5, at step 4, we remove 5 friom the
stack, and we get 3. At step 5, we remove 3 if it is in the stack, and we get
4. With the monotone stack, we decrease the time complexity from O(kn)
to O(n).
1 import c o l l e c t i o n s
2
3 d e f maxSlidingWindow ( s e l f , nums , k ) :
4 ds = c o l l e c t i o n s . deque ( )
5 ans = [ ]
6 f o r i i n r a n g e ( l e n ( nums ) ) :
7 w h i l e ds and nums [ i ] >= nums [ ds [ − 1 ] ] : i n d i c e s . pop ( )
8 ds . append ( i )
9 i f i >= k − 1 : ans . append ( nums [ ds [ 0 ] ] ) #append t h e
c u r r e n t maximum
10 i f i − k + 1 == ds [ 0 ] : ds . p o p l e f t ( ) #i f t h e f i r s t a l s o
t h e maximum number i s out o f window , pop i t out
11 r e t u r n ans
I nput : [ 3 , 1 , 2 , 4 ]
Output : 17
Explanation : Subarrays are [ 3 ] , [ 1 ] , [ 2 ] , [ 4 ] , [ 3 , 1 ] ,
[1 ,2] , [2 ,4] , [3 ,1 ,2] , [1 ,2 ,4] , [3 ,1 ,2 ,4].
Minimums a r e 3 , 1 , 2 , 4 , 1 , 1 , 2 , 1 , 1 , 1 . Sum i s 1 7 .
if there is duplicate such as [3, 1, 4, 1], for the first 1, we need [3,1],
[1], [1,4], [1, 4,1] with subarries, and for the second 1, we need [4,1],
[1] instead. Therefore, we set the right length to find the >= item.
Now, the problem in converted to the first smaller item on the left side
and the first smaller or equal item on the right side. From the feature
we draw above, we need to use increasing stack, as we know, from the
pushing in, we find the first smaller item, and from the popping out,
for the popped out item, the current item is the first smaller item on
the right side. The code is as:
1 d e f sumSubarrayMins ( s e l f , A) :
2 n , mod = l e n (A) , 10∗∗9 + 7
3 l e f t , s1 = [ 1 ] ∗ n , [ ]
4 r i g h t = [ n−i f o r i i n r a n g e ( n ) ]
5 f o r i in range (n) : # f i n d f i r s t s m a l l e r to the l e f t
from p u s h i n g i n
6 w h i l e s 1 and A[ s 1 [ − 1 ] ] > A[ i ] : # can be e q u a l
7 i n d e x = s 1 . pop ( )
8 r i g h t [ i n d e x ] = i −i n d e x # k i c k e d out
9 i f s1 :
10 l e f t [ i ] = i −s 1 [ −1]
11 else :
12 l e f t [ i ] = i +1
13 s 1 . append ( i )
14 r e t u r n sum ( a ∗ l ∗ r f o r a , l , r i n z i p (A, l e f t , r i g h t )
) % mod
6.3.5 Bonus
Circular Linked List and Circular Queue The circular queue is a
linear data structure in which the operation are performed based on FIFO
principle and the last position is connected back to the the first position to
make a circle. It is also called “Ring Buffer”. Circular Queue can be either
6.3. STACK AND QUEUE 83
Analysis: This is a typical buffer problem. If the size is larger than the
buffer, then we squeeze out the easilest data. Thus, a queue can be used to
save the t and each time, squeeze any time not in the range of [t-3000, t]:
1 c l a s s RecentCounter :
2
3 d e f __init__ ( s e l f ) :
4 s e l f . ans = c o l l e c t i o n s . deque ( )
5
84 6. LINEAR DATA STRUCTURE
6 def ping ( s e l f , t ) :
7 """
8 : type t : i n t
9 : rtype : int
10 """
11 s e l f . ans . append ( t )
12 w h i l e s e l f . ans [ 0 ] < t −3000:
13 s e l f . ans . p o p l e f t ( )
14 r e t u r n l e n ( s e l f . ans )
Monotone Queue
Obvious applications:
search for an element in a hash map as O(1). In this section, we will examine
the hashing design and analysis mechanism.
First, let us frame the hashing problem: given a universe U of keys (or
items) with size n, with a hash table denoted by T [0...m − 1], in which each
position, or slot, corresponds to a key in the hash table. Fig. 6.6 shows an
example of hash table. When two keys have the same hash value produced
by hash function, it is called collision. Because α > 1, there must be at
least two keys that have the same hash value; avoiding collisions altogether
seems impossible. Therefore, in reality, a well-designed hashing mechanism
should include: (1) a hash function which minimizes the number of collisions
and (2) a efficient collision resolution if it occurs.
• Hash Map: hash map is a data structures that stores items as (key,
value) pair. And “key” are hashed using hash table into an index to
access the value.
• Hash Set: different to hash map, in a hash set, only keys are stored
and it has no duplicate keys. Set usually represents the mathematical
notion of a set, which is used to test membership, computing standard
86 6. LINEAR DATA STRUCTURE
Interpreting Keys For a given key, if the keys are not natural numbers,
such as any string, or tuple, they need to be firstly interpreted as natural
integers N = {0, 1, 2, ...}. This interpretation relation(function) needs to be
one to one; given two distinct keys, they should never be interpreted as the
same natural number. And we denote it as a interpret function k = f (key),
where key is a input key and k is the interpreted natural number. For string
or character, one possible way is to express them in suitable radix notation.
we might translate “pt” as the pair of decimal integers (112, 116) with their
ASCII character; then we express it as a radix128 integer, then the number
we get is (112 × 128) + 116 = 14452. This is usually called polynomial
rolling hashing, to generalize, k = s[0] + s[1] ∗ p + s[2] ∗ p2 + ... + s[n −
1] ∗ pn−1 , where the string has length n, and p is a chosen prime number
which is roughly equal to the number of characters in the input alphabet.
For instance, if the input is composed of only lowercase letters of English
alphabet, p=31 is a good choice. If the input may contain both uppercase
and lowercase letters, then p=53 is a possible choice.
should produce the same key value (f (s, m) = f (t, m), if s = t). And, we
try our best to minimize the collision to make it unlikely for two distinct
keys to have the same value. Therefore our expectation for average collision
times for the same slot will be α = m n
, which is called loading factor and
is a critical statistics for design hashing and analyze its performance. The
relation is denoted in Fig. 6.7. Besides, a good hash function satisfied the
condition of simple uniform hashing: each key is equally likely to be mapped
to any of the m slots. There are generally four methods:
1. The Direct addressing method, f (k, m) = k, and m = n. Direct
addressing can be impractical when n is beyond the memory size of a
computer. Also, it is just a waste of spaces when m << n.
Because the keys share a common factor c = 2 with the bucket number
4, then when we apply the division, it became (key/c)/(m/c); both the
quotient(also a multiple of the bucket size) and the remainder(modulo
or bucket number) can be written as multiple of the common factor.
So, the range of the slot index will be decrease to m/c. The real
loading factor increase to cα. Using a prime number is a easy way to
avoid this since a prime number has no factors other than 1 and itself.
ever, when we are deleting a key, we can not simply delete the key and value
and mark it as empty. If we did, we might be unable to retrieve any key dur-
ing whose insertion we had probed slot i because we stop probing whenever
empty slot is found.
Let us see with an example: Assume hash(x) = hash(y) = hash(z) = i.
And assume x was inserted first, then y and then z. In open addressing:
table[i] = x, table[i+1] = y, table[i+2] = z. Now, assume you want to delete
x, and set it back to NULL. When later you will search for z, you will find
that hash(z) = i and table[i] = NULL, and you will return a wrong answer:
z is not in the table.
To overcome this, you need to set table[i] with a special marker indicating
to the search function to keep looking at index i+1, because there might be
element there which its hash is also i
Perfect Hashing
6.4.3 Implementation
In this section, we practice on the learned concepts and methods by imple-
menting hash set and hash map.
Hash Set Design a HashSet without using any built-in hash table libraries.
To be specific, your design should include these functions: (705. Design
HashSet)
add ( v a l u e ) : I n s e r t a v a l u e i n t o t h e HashSet .
c o n t a i n s ( v a l u e ) : Return whether t h e v a l u e e x i s t s i n t h e HashSet
o r not .
remove ( v a l u e ) : Remove a v a l u e i n t h e HashSet . I f t h e v a l u e d o e s
not e x i s t i n t h e HashSet , do n o t h i n g .
For example:
MyHashSet h a s h S e t = new MyHashSet ( ) ;
h a s h S e t . add ( 1 ) ;
h a s h S e t . add ( 2 ) ;
hashSet . c o n t a i n s ( 1 ) ; // r e t u r n s t r u e
hashSet . c o n t a i n s ( 3 ) ; // r e t u r n s f a l s e ( not found )
h a s h S e t . add ( 2 ) ;
hashSet . c o n t a i n s ( 2 ) ; // r e t u r n s t r u e
h a s h S e t . remove ( 2 ) ;
hashSet . c o n t a i n s ( 2 ) ; // r e t u r n s f a l s e ( a l r e a d y removed )
Note: Note: (1) All values will be in the range of [0, 1000000]. (2) The
number of operations will be in the range of [1, 10000].
1 c l a s s MyHashSet :
2
3 d e f _h( s e l f , k , i ) :
4 r e t u r n ( k+i ) % 10001
90 6. LINEAR DATA STRUCTURE
5
6 d e f __init__ ( s e l f ) :
7 """
8 I n i t i a l i z e your data s t r u c t u r e h e r e .
9 """
10 s e l f . s l o t s = [ None ] ∗ 1 0 0 0 1
11 s e l f . s i z e = 10001
12
13 d e f add ( s e l f , key : ' i n t ' ) −> ' None ' :
14 i = 0
15 while i < s e l f . s i z e :
16 k = s e l f . _h( key , i )
17 i f s e l f . s l o t s [ k ] == key :
18 return
19 e l i f not s e l f . s l o t s [ k ] o r s e l f . s l o t s [ k ] == −1:
20 s e l f . s l o t s [ k ] = key
21 return
22 i += 1
23 # double s i z e
24 s e l f . s l o t s = s e l f . s l o t s + [ None ] ∗ s e l f . s i z e
25 s e l f . s i z e ∗= 2
26 r e t u r n s e l f . add ( key )
27
28
29 d e f remove ( s e l f , key : ' i n t ' ) −> ' None ' :
30 i = 0
31 while i < s e l f . s i z e :
32 k = s e l f . _h( key , i )
33 i f s e l f . s l o t s [ k ] == key :
34 s e l f . s l o t s [ k ] = −1
35 return
36 e l i f s e l f . s l o t s [ k ] == None :
37 return
38 i += 1
39 return
40
41 d e f c o n t a i n s ( s e l f , key : ' i n t ' ) −> ' b o o l ' :
42 """
43 Returns t r u e i f t h i s s e t c o n t a i n s t h e s p e c i f i e d e l e m e n t
44 """
45 i = 0
46 while i < s e l f . s i z e :
47 k = s e l f . _h( key , i )
48 i f s e l f . s l o t s [ k ] == key :
49 r e t u r n True
50 e l i f s e l f . s l o t s [ k ] == None :
51 return False
52 i += 1
53 return False
Hash Map Design a HashMap without using any built-in hash table li-
braries. To be specific, your design should include these functions: (706.
6.4. HASH TABLE 91
• put(key, value) : Insert a (key, value) pair into the HashMap. If the
value already exists in the HashMap, update the value.
Example:
hashMap = MyHashMap ( )
hashMap . put ( 1 , 1 ) ;
hashMap . put ( 2 , 2 ) ;
hashMap . g e t ( 1 ) ; // r e t u r n s 1
hashMap . g e t ( 3 ) ; // r e t u r n s −1 ( not found )
hashMap . put ( 2 , 1 ) ; // update the e x i s t i n g value
hashMap . g e t ( 2 ) ; // r e t u r n s 1
hashMap . remove ( 2 ) ; // remove t h e mapping f o r 2
hashMap . g e t ( 2 ) ; // r e t u r n s −1 ( not found )
1 c l a s s MyHashMap :
2 d e f _h( s e l f , k , i ) :
3 r e t u r n ( k+i ) % 10001 # [ 0 , 1 0 0 0 1 ]
4 d e f __init__ ( s e l f ) :
5 """
6 I n i t i a l i z e your data s t r u c t u r e h e r e .
7 """
8 s e l f . s i z e = 10002
9 s e l f . s l o t s = [ None ] ∗ s e l f . s i z e
10
11
12 d e f put ( s e l f , key : ' i n t ' , v a l u e : ' i n t ' ) −> ' None ' :
13 """
14 v a l u e w i l l always be non−n e g a t i v e .
15 """
16 i = 0
17 while i < s e l f . s i z e :
18 k = s e l f . _h( key , i )
19 i f not s e l f . s l o t s [ k ] o r s e l f . s l o t s [ k ] [ 0 ] i n [ key ,
−1]:
20 s e l f . s l o t s [ k ] = ( key , v a l u e )
21 return
22 i += 1
23 # d o u b l e s i z e and t r y a g a i n
24 s e l f . s l o t s = s e l f . s l o t s + [ None ] ∗ s e l f . s i z e
25 s e l f . s i z e ∗= 2
26 r e t u r n s e l f . put ( key , v a l u e )
27
28
29 d e f g e t ( s e l f , key : ' i n t ' ) −> ' i n t ' :
30 """
92 6. LINEAR DATA STRUCTURE
Python 2.X VS Python 3.X In Python 2X, we can use slice to access
keys() or items() of the dictionary. However, in Python 3.X, the same syn-
tax will give us TypeError: ’dict_keys’ object does not support indexing.
6.4. HASH TABLE 93
Instead, we need to use function list() to convert it to list and then slice it.
For example:
1 # Python 2 . x
2 d i c t . keys ( ) [ 0 ]
3
4 # Python 3 . x
5 l i s t ( d i c t . keys ( ) ) [ 0 ]
dict Data Type If we want to put string in set, it should be like this:
1 >>> a = s e t ( ' a a r d v a r k ' )
2 >>>
3 { 'd ' , 'v ' , 'a ' , ' r ' , 'k '}
4 >>> b = { ' a a r d v a r k ' }# o r s e t ( [ ' a a r d v a r k ' ] ) , c o n v e r t a l i s t o f
s t r i n g s to s e t
5 >>> b
6 { ' aardvark ' }
7 #o r put a t u p l e i n t h e s e t
8 a =s e t ( [ t u p l e ] ) o r { ( t u p l e ) }
Compare also the difference between and set() with a single word argument.
Collection Module
OrderedDict Standard dictionaries are unordered, which means that any
time you loop through a dictionary, you will go through every key, but you
are not guaranteed to get them in any particular order. The OrderedDict
from the collections module is a special type of dictionary that keeps track
of the order in which its keys were inserted. Iterating the keys of an ordered-
Dict has predictable behavior. This can simplify testing and debugging by
making all the code deterministic.
The defaultdict class from the collections module simplifies this process by
pre-assigning a default value when a key does not present. For different value
type it has different default value, for example, for int, it is 0 as the default
value. A defaultdict works exactly like a normal dict, but it is initialized
94 6. LINEAR DATA STRUCTURE
1 from c o l l e c t i o n s import d e f a u l t d i c t
2 dict = d e f a u l t d i c t ( int ) # default value f o r int i s 0
3 d i c t [ ' c o u n t e r ' ] += 1
There include: Time Complexity for Operations Search, Insert, Delete:
O(1).
Counter
10
11 I t i s p o s s i b l e t o u s e both o f t h e s e r u l e s a t t h e same time .
12
13 Given a l i s t o f e m a i l s , we send one e m a i l t o each a d d r e s s i n t h e
l i s t . How many d i f f e r e n t a d d r e s s e s a c t u a l l y r e c e i v e m a i l s ?
14
15 Example 1 :
16
17 I n p u t : [ " t e s t . e m a i l+a l e x @ l e e t c o d e . com " , " t e s t . e . m a i l+bob .
c a t h y @ l e e t c o d e . com " , " t e s t e m a i l+d a v i d @ l e e . t c o d e . com " ]
18 Output : 2
19 E x p l a n a t i o n : " t e s t e m a i l @ l e e t c o d e . com " and " t e s t e m a i l @ l e e . t c o d e .
com " a c t u a l l y r e c e i v e m a i l s
20
21 Note :
22 1 <= e m a i l s [ i ] . l e n g t h <= 100
23 1 <= e m a i l s . l e n g t h <= 100
24 Each e m a i l s [ i ] c o n t a i n s e x a c t l y one '@' c h a r a c t e r .
Answer: Use hashmap simply Set of tuple to save the corresponding sending
exmail address: local name and domain name:
1 class Solution :
2 d e f numUniqueEmails ( s e l f , e m a i l s ) :
3 """
4 : type e m a i l s : L i s t [ s t r ]
5 : rtype : int
6 """
7 i f not e m a i l s :
8 return 0
9 num = 0
10 handledEmails = s e t ( )
11 f o r email in emails :
12 local_name , domain_name = e m a i l . s p l i t ( '@ ' )
13 local_name = local_name . s p l i t ( '+ ' ) [ 0 ]
14 local_name = local_name . r e p l a c e ( ' . ' , ' ' )
15 h a n d l e d E m a i l s . add ( ( local_name , domain_name ) )
16 return l e n ( handledEmails )
96 6. LINEAR DATA STRUCTURE
7
7.1 Graphs
Graph is a widely used data structure to model real-world problems. Graph
is a collection of vertices and edges (which connects two vertices). The
weights of edges refers to the information with edges. In this section, we
only introduce graph representations in Python and graph types in
the following two subsections. We explore the important graph complete
search methodologies: Searching Graphs in Chapter 10 and Shortest Path
algorithms in Chapter ??.
We use G to denote the graph, V and E to refer its collections of vertices
and edges, respectively. Before move to the main contents, there are some
Python matrices skills we will cover here.
97
98 7. GRAPHS AND TREES
11 # [[0 , 0 , 0] , [0 , 0 , 2] , [0 , 0 , 0]]
However, we can not declare it in the following way, because we end up with
some copies of the same inner lists, thus modifying one element in the inner
lists will end up changing all of the them in the corresponding positions.
1 # wrong d e c l a r a t i o n
2 ma tri x4 = [ [ 0 ] ∗ c o l s ] ∗ rows
3 ma tri x4 [ 1 ] [ 2 ] = 2
4 # output by p r i n t i n g
5 # [[0 , 0 , 2] , [0 , 0 , 2] , [0 , 0 , 2]]
Accessing Rows:
1 # a c c e s s i n g row
2 f o r row i n ma tri x1 :
3 p r i n t ( row )
4 # output
5 # [0 , 0 , 0]
6 # [0 , 0 , 2]
7 # [0 , 0 , 0]
Accessing Cols: this is usually a lot slower than accessing each row due
to the fact that each row is a pointer while each col we need to obtain from
each row. The speed is cols times slower than accessing rows.
1 # accessing col
2 f o r i in range ( c o l s ) :
3 c o l = [ row [ i ] f o r row i n ma tri x1 ]
4 print ( col )
5 # output
6 # [0 , 0 , 0]
7 # [0 , 0 , 0]
8 # [0 , 2 , 0]
There’s also a handy ’idiom’ for transposing a nested list, turning ’columns’
into ’rows’:
1 t r a n s p o s e d M a t r i x 1 = l i s t ( z i p ( ∗ ma tri x1 ) )
2 print ( transposedMatrix1 )
3 # output
4 # [ ( 0 , 0 , 0) , (0 , 0 , 0) , (0 , 2 , 0) ]
If we need weights for each edge, use dictionary from the default dictionary
to represent:
1 graph= c o l l e c t i o n s . d e f a u l t d i c t ( d i c t )
2 f o r (num , den ) , v a l i n z i p ( e q u a t i o n s , v a l u e s ) :
3 graph [ num ] [ num ] = graph [ den ] [ den ] = 1 . 0
4 graph [ num ] [ den ] = v a l
5 graph [ den ] [ num ] = 1 / v a l
6 #d e f a u l t d i c t (< type ' d i c t ' > , {u ' a ' : {u ' a ' : 1 . 0 , u ' b ' : 2 . 0 } , u ' c ' :
{u ' c ' : 1 . 0 , u ' b ' : 0 . 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 } , u ' b ' : {u ' a ' : 0 . 5 , u ' c
': 3.0 , u 'b ': 1.0}})
7.2 Tree
Trees are well-known as a non-linear data structure. They organize data
hierarchically other than in the linear way.
In the first section, we will introduce the definition and common prop-
erties of tree (Section ??). There are different types of trees devised for
different purpose. The most widely used are binary tree and binary search
tree which are also the most popular tree problems you encounter in the real
interviews. At least 80% of chance you will be asked to solve a binary tree
or binary search tree related problem in a real coding interview especially
7.2. TREE 101
for new graduates which has no real industrial experience and pretty much
had no chance before to put the major related knowledge into practice yet.
Therefore, in the second section, we talk about the binary tree (Section 7.3)
which is the most common type of trees used to solve real-world problems.
Then in the third section, we introduce searching trees (Section ??): we
can see how such trees help us to store, and find the information we need.
Mainly two types of searching trees are included: binary search tree which
only has two branches and the trie which might have multiple and varied
number of branches for each node. The last two section is the bonus section,
with the more advanced tree structures Trie (Section 7.6) and Segment Tree
(Section ??) for string pattern match and array queries. Compared with
the pervious sections, these two structures are not necessaries but Trie does
help improve the time complexity of string pattern matching.
Also, to be noticed, this chapter serves as an introduction, the summary
and commonly seen problems related to tree will be detailed in Part ??.
1. Root->Leaf Path: the starting and ending node of the path is the root
and leaf node respectively;
2. Root->Any Path: the starting and ending node of the path is the root
and any node (Root, inner, leaf node) respectively;
3. Any->Any Path: the starting and ending node of the path is both any
node (Root, inner, leaf node) respectively.
7.2.2 Properties
The property of a tree starts from the property of a node (shown in Fig 7.2).
1. The depth (or level) of a node is the number of edges from the node
to the tree’s root node. And it can be obtained from up-down level-
by-level traversal.
102 7. GRAPHS AND TREES
2. The height of a node is the number of edges on the longest path from
the node to a leaf. A leaf node will have a height of 0.
3. The descendant of a node is any node that is reachable by repeated
proceeding from parent to child starting from this node. They are also
known as subchild.
4. The ancestor of a node is any node that is reachable by repeated
proceeding from child to parent starting from this node.
5. The degree of a node is the number of its children. A leaf is necessarily
degreed zero.
Properties of a tree:
1. The height(or depth) of a tree would be the height of its root node,
or equivalently, the depth of its deepest node.
2. The diameter (or width) of a tree is the number of nodes (or edges)
on the longest path between any two leaf nodes.
Forest is a set of n >= 0 disjoint treees.
topmost node in the tree. The left and right child recursively point to
smaller “subtrees" on either side.
In Python, a tree node is implemented as:
1 c l a s s TreeNode :
2 d e f __init__ ( s e l f , v a l ) :
3 s e l f . val = val
4 s e l f . l e f t = None
5 s e l f . r i g h t = None
• To construct a tree from a list which use “None" to denote for empty
node, which can be really useful to automatize the tree construction.
We can use the following code:
1 d e f g e n e r a t e B i n a r y T r e e ( nums , i d x ) :
2 i f i d x >= l e n ( nums ) :
3 r e t u r n None
4 node = TreeNode ( nums [ i d x ] )
5 node . l e f t = g e n e r a t e B i n a r y T r e e ( nums , i d x ∗2+1)
6 node . r i g h t = g e n e r a t e B i n a r y T r e e ( nums , i d x ∗2+2)
7 r e t u r n node
8
9 nums = [ 1 , 2 , 3 , 4 , 5 , None , 6 ]
10 r o o t = g e n e r a t e B i n a r y T r e e ( nums , 0 )
To show the nodes at each level, we use LevelOrder function to print out
the tree:
1 def LevelOrder ( root ) :
2 q = [ root ]
3 while q :
4 new_q = [ ]
5 for n in q :
6 i f n i s not None :
7 p r i n t ( n . v a l , end= ' , ' )
8 if n. left :
9 new_q . append ( n . l e f t )
10 i f n. right :
11 new_q . append ( n . r i g h t )
12 q = new_q
13 p r i n t ( ' \n ' )
14 LevelOrder ( root )
15 # output
16 # 1,
17
18 # 2 ,3 ,
19
20 # 4 , 5 , None , 6 ,
104 7. GRAPHS AND TREES
Example 1 :
Input : r o o t = [ 3 , 5 , 1 , 6 , 2 , 0 , 8 , n u l l , n u l l , 7 , 4 ] , p = 5 , q = 1
Output : 3
E x p l a n a t i o n : The LCA o f o f nodes 5 and 1 i s 3 .
Example 2 :
Input : r o o t = [ 3 , 5 , 1 , 6 , 2 , 0 , 8 , n u l l , n u l l , 7 , 4 ] , p = 5 , q = 4
Output : 5
E x p l a n a t i o n : The LCA o f nodes 5 and 4 i s 5 , s i n c e a node
can be a d e s c e n d a n t o f i t s e l f
a c c o r d i n g t o t h e LCA d e f i n i t i o n .
Solution: Divide and Conquer. There are two cases for LCA: 1)
two nodes each found in different subtree, like example 1. 2) two nodes
are in the same subtree like example 2. If we compare the current node
with the p and q, if it equals to any of them, return current node in
the tree traversal. Therefore in example 1, at node 3, the left return
as node 5, and the right return as node 1, thus node 3 is the LCA.
In example 2, at node 5, it returns 5, thus for node 3, the right tree
would have None as return, thus it makes the only valid return as the
final LCA. The time complexity is O(n).
1 d e f lowestCommonAncestor ( s e l f , r o o t , p , q ) :
2 """
3 : type r o o t : TreeNode
4 : type p : TreeNode
5 : type q : TreeNode
6 : r t y p e : TreeNode
7 """
8 i f not r o o t :
9 r e t u r n None
10 i f r o o t == p o r r o o t == q :
11 r e t u r n r o o t # found one v a l i d node ( c a s e 1 : s t o p a t
5 , 1 , case 2 : stop at 5)
7.4. BINARY SEARCH TREE 105
12 l e f t = s e l f . lowestCommonAncestor ( r o o t . l e f t , p , q )
13 r i g h t = s e l f . lowestCommonAncestor ( r o o t . r i g h t , p , q )
14 i f l e f t i s not None and r i g h t i s not None : # p , q i n
the subtree
15 return root
16 i f any ( [ l e f t , r i g h t ] ) i s not None :
17 r e t u r n l e f t i f l e f t i s not None e l s e r i g h t
18 r e t u r n None
Types of Binary Tree There are four common types of Binary Tree:
1) Full Binary Tree, 2) Complete Binary Tree, 3) Perfect Binary Tree, 4)
Balanced Binary Tree.
Full Binary Tree A binary tree is full if every node has 0 or 2 children.
We can also say that a full binary tree is a binary tree in which all nodes
except leaves have two children. In full binary tree, the number of leaves
and the number of all other non-leaf nodes has relation: L = Non-L + 1.
Complete Binary Tree A Binary Tree is complete Binary Tree if all
levels are completely filled except possibly the last level and the last level
has all keys as left as possible.
Perfect Binary Tree A Binary tree is Perfect Binary Tree in which all
internal nodes have two children and all leaves are at the same level.
Balanced Binary Tree A binary tree is balanced if the height of the
tree is O(Log n) where n is the number of nodes. For Example, AVL
tree maintains O(Log n) height by making sure that the difference between
heights of left and right subtrees is 1.
A degenerate (or pathological) tree A Tree where every internal
node has one child. Such trees are performance-wise same as linked list.
then adding a “3” duplicate key to this tree will result in:
3
/ \
7.4. BINARY SEARCH TREE 107
2 4
\
3
Operations
When looking for a key in a tree (or a place to insert a new key), we tra-
verse the tree from root to leaf, making comparisons to keys stored in the
nodes of the tree and deciding, on the basis of the comparison, to continue
searching in the left or right subtrees. On average, this means that each
comparison allows the operations to skip about half of the tree, so that each
SEARCH, INSERT or DELETE takes time proportional to the logarithm of
the number of items stored in the tree. This is much better than the linear
time required to find items by key in an (unsorted) array, but slower than
the corresponding operations on hash tables.
In order to build a BST, we need to INSERT a series of elements in the
tree organized by the searching tree property, and in order to INSERT, we
need to SEARCH the position to INSERT this element. Thus, we introduce
these operations in the order of SEARCH, INSERT and GENERATE.
Figure 7.4: The lightly shaded nodes indicate the simple path from the root
down to the position where the item is inserted. The dashed line indicates
the link in the tree that is added to insert the item.
Also, we can write it in an iterative way, which helps us save the heap space:
1 # i t e r a t i v e searching
2 d e f i t e r a t i v e _ s e a r c h ( r o o t , key ) :
3 w h i l e r o o t i s not None and r o o t . v a l != key :
4 i f r o o t . v a l < key :
5 root = root . right
6 else :
7 root = root . l e f t
8 return root
7 r o o t . r i g h t = i n s e r t i o n ( r o o t . r i g h t , key )
8 else :
9 r o o t . l e f t = i n s e r t i o n ( r o o t . l e f t , key )
10 return root
The above code needs return value and reassign the value for the right
and left every time, we can use the following code which might looks more
complex with the if condition but works faster and only assign element at
the end.
1 # recursive insertion
2 def i n s e r t i o n ( root , val ) :
3 i f r o o t i s None :
4 r o o t = TreeNode ( v a l )
5 return
6 i f val > root . val :
7 i f r o o t . r i g h t i s None :
8 r o o t . r i g h t = TreeNode ( v a l )
9 else :
10 i n s e rt i o n ( root . right , val )
11 else :
12 i f r o o t . l e f t i s None :
13 r o o t . l e f t = TreeNode ( v a l )
14 else :
15 i n s e rt i o n ( root . l e f t , val )
We can search the node iteratively and save the previous node. The while
loop would stop when hit at an empty node. There will be three cases in
the case of the previous node.
1 . The p r e v i o u s node i s None , which means t h e t r e e i s empty , s o
we a s s i g n a r o o t node with t h e v a l u e
2 . The p r e v i o u s node has a v a l u e l a r g e r than t h e key , means we
need t o put key a s l e f t c h i l d .
3 . The p r e v i o u s node has a v a l u e s m a l l e r than t h e key , means we
need t o put key a s r i g h t c h i l d .
1 # iterative insertion
2 d e f i t e r a t i v e I n s e r t i o n ( r o o t , key ) :
3 pre_node = None
4 node = r o o t
5 w h i l e node i s not None :
6 pre_node = node
7 i f key < node . v a l :
8 node = node . l e f t
9 else :
10 node = node . r i g h t
11 # we r e a c h e d t o t h e l e a f node which i s pre_node
12 i f pre_node i s None :
13 r o o t = TreeNode ( key )
14 e l i f pre_node . v a l > key :
15 pre_node . l e f t = TreeNode ( key )
16 else :
17 pre_node . r i g h t = TreeNode ( key )
18 return root
110 7. GRAPHS AND TREES
BST Generation First, let us declare a node as BST which is the root
node. Given a list, we just need to call INSERT for each element. The time
complexity can be O(n logn ).
1 datas = [ 8 , 3 , 10 , 1 , 6 , 14 , 4 , 7 , 13]
2 BST = None
3 f o r key i n d a t a s :
4 BST = i t e r a t i v e I n s e r t i o n (BST, key )
5 p r i n t ( L e v e l O r d e r (BST) )
6 # output
7 # [ 8 , 3 , 10 , 1 , 6 , 14 , 4 , 7 , 13]
50 50
/ \ delete (20) / \
30 70 −−−−−−−−−> 30 70
/ \ / \ \ / \
20 40 60 80 40 60 80
50 50
/ \ delete (30) / \
30 70 −−−−−−−−−> 40 70
\ / \ / \
40 60 80 60 80
50 60
/ \ delete (50) / \
40 70 −−−−−−−−−> 40 70
/ \ \
60 80 80
The i m p o r t a n t t h i n g t o n o t e i s , i n o r d e r s u c c e s s o r i s needed o n l y
when r i g h t c h i l d i s not empty . In t h i s p a r t i c u l a r c a s e ,
i n o r d e r s u c c e s s o r can be o b t a i n e d by f i n d i n g t h e minimum
v a l u e i n r i g h t c h i l d o f t h e node .
7.4. BINARY SEARCH TREE 111
Features of BST
Minimum and Maximum The operation is similar to search, to find
the minimum, we always traverse on the left subtree. For the maximum, we
just need to replace the “left” with “right” in the key word. Here the time
complexity is the same O(lgn).
1 # recursive
2 d e f get_minimum ( r o o t ) :
3 i f r o o t i s None :
4 r e t u r n None
5 i f r o o t . l e f t i s None : # a l e a f o r node has no l e f t s u b t r e e
6 return root
7 i f root . l e f t :
8 r e t u r n get_minimum ( r o o t . l e f t )
9
10 # iterative
11 d e f iterative_get_minimum ( r o o t ) :
12 w h i l e r o o t . l e f t i s not None :
13 root = root . l e f t
14 return root
7 w h i l e p i s not None :
8 i f n == p . l e f t :# i f c u r r e n t node i s t h e l e f t c h i l d node ,
then we found t h e s u c c e s s o r , p
9 return p
10 n = p
11 p = p . parent
12 return p
However, if it happens that your tree node has no parent defined, which
means you can not traverse back its parents. We only have one option. Use
the inorder tree traversal, and find the element right after the node.
For t h e r i g h t s u b t r e e o f t h e node :
1 ) I f i t i s not None , then t h e s u c c e s s o r i s t h e minimum node i n
t h e r i g h t s u b t r e e . e . g . f o r node 1 2 , s u c c e s s o r ( 1 2 ) = 13 = min
(12. right )
2 ) I f i t i s None , then t h e s u c c e s s o r i s one o f i t s a n c e s t o r s . We
t r a v e r s e down from t h e r o o t t i l l we f i n d c u r r e n t node , t h e
node i n advance o f c u r r e n t node i s t h e s u c c e s s o r . e . g .
s u c c e s s o r ( 2 ) =5
1 def S uc ce s so rI n or de r ( root , n) :
2 # Step 1 o f t h e above a l g o r i t h m
3 i f n . r i g h t i s not None :
4 r e t u r n get_minimum ( n . r i g h t )
5 # Step 2 o f t h e above a l g o r i t h m
6 s u c c = None
7 w h i l e r o o t i s not None :
8
9 i f n . val > root . val :
10 root = root . right
11 e l i f n . val < root . val :
12 succ = root
13 root = root . l e f t
14 e l s e : # we found t h e node , no need t o t r a v e r s e
15 break
16 return succ
6 p = n . parent
7 w h i l e p i s not None :
8 i f n == p . r i g h t :# i f c u r r e n t node i s t h e r i g h t node , p a r e n t
i s smaller
9 return p
10 n = p
11 p = p . parent
12 return p
The worst case to find the successor or the predecessor of a BST is to search
the height of the tree: include the one of the subtrees of the current node,
and go back to all the parents and greatparents of this code, which makes
it the height of the tree. The expected time complexity is O(lgn). And the
worst is when the tree line up and has no branch, which makes it O(n).
1 d e f lowestCommonAncestor ( s e l f , r o o t , p , q ) :
2 """
3 : type r o o t : TreeNode
4 : type p : TreeNode
5 : type q : TreeNode
6 : r t y p e : TreeNode
7 """
8 s = min ( p . v a l , q . v a l )
9 b = max( p . v a l , q . v a l )
10 d e f LCA( node ) :
11 i f not node :
12 r e t u r n None
13 i f node . v a l >b :
14 r e t u r n LCA( node . l e f t )
15 i f node . v a l <s :
16 r e t u r n LCA( node . r i g h t )
17 # c u r r e n t node [ s , b ] , then t h i s i s LCA
18 r e t u r n node
19
20 r e t u r n LCA( r o o t )
In order traverse can be used to sorting. e.g. 230. Kth Smallest Element
in a BST
Now we put a table here to summarize the space and time complexity
for each operation.
4. If the parent node is in range [i, j], then we separate this range at the
middle position m = (i + j)2, the left child takes range [i, m], and the
right child take the interval of [m+1, j].
Because in each step of building the segment tree, the interval is divided
into two halves, so the height of the segment tree will be log N . And there
will be totally N leaves and N-1 number of internal nodes, which makes the
total number of nodes in segment tree to be 2N − 1 and make the segment
tree a full binary tree.
Here, we use the Range Sum Query (RSQ) problem to demonstrate how
segment tree works:
Given nums = [ 1 , 3 , 5 ]
sumRange ( 0 , 2 ) −> 9
update ( 1 , 2 )
sumRange ( 0 , 2 ) −> 8
Note:
Build Segment Tree. Because the leaves of the tree is a single ele-
ment, we can use divide and conquer to build the tree recursively. For
a given node, we first build and return its left and right child(including
calculating its sum) in advance in the ‘divide‘ step, and in the ‘con-
quer’ step, we calculate this node’s sum using its left and right child’s
sum, and set its left and right child. Because there are totally 2n − 1
nodes, which makes the time and space complexity O(n).
1 d e f _buildSegmentTree ( s e l f , nums , s , e ) : #s t a r t i n d e x and
end i n d e x
2 if s > e:
3 r e t u r n None
7.5. SEGMENT TREE 117
4 i f s == e :
5 r e t u r n s e l f . TreeNode ( nums [ s ] )
6
7 m = ( s + e ) //2
8 # divide
9 l e f t = s e l f . _buildSegmentTree ( nums , s , m)
10 r i g h t = s e l f . _buildSegmentTree ( nums , m+1, e )
11
12 # co nqu er
13 node = s e l f . TreeNode ( l e f t . v a l + r i g h t . v a l )
14 node . l e f t = l e f t
15 node . r i g h t = r i g h t
16 r e t u r n node
Range Sum Query. Each query range [i, j], will be a combination
of ranges of one or multiple ranges. For instance, as in the segment
tree shown in Fig 7.5, for range [2, 4], it will be combination of [2, 3]
and [4, 4]. The process is similar to the updating, we starts from the
root, and get its middle index m: 1) if [i, j] is the same as [s, e] that i
== s and j == e, then return the value, 2) if the interval [i, j] is within
range [s, m] that j <=m , then we just search it in the left branch. 3)
if [i, j] in within range [m+1, e] that i>m, then we search for the right
branch. 4) else, we search both branch and the left branch has target
[i, m], and the right side has target [m+1, j], the return value should
be the sum of both sides. The time complexity is still O(log n).
1 d e f _rangeQuery ( s e l f , r o o t , i , j , s , e ) :
2 i f s > e or i > j :
3 return 0
4 i f s == i and j == e :
5 r e t u r n r o o t . v a l i f r o o t i s not None e l s e 0
6
118 7. GRAPHS AND TREES
7 m = ( s + e ) //2
8
9 i f j <= m:
10 r e t u r n s e l f . _rangeQuery ( r o o t . l e f t , i , j , s , m)
11 e l i f i > m:
12 r e t u r n s e l f . _rangeQuery ( r o o t . r i g h t , i , j , m+1, e )
13 else :
14 r e t u r n s e l f . _rangeQuery ( r o o t . l e f t , i , m, s , m) +
s e l f . _rangeQuery ( r o o t . r i g h t , m+1 , j , m+1 , e)
Segment tree can be used here to lower the complexity of each query to
O(logn).
Now, we define a trie Node: first it would have a bool variable to denote
if it is the end of the word and a children which is a list of of 26 children
TrieNodes.
1 c l a s s TrieNode :
2 # T r i e node c l a s s
3 d e f __init__ ( s e l f ) :
4 s e l f . c h i l d r e n = [ None ] ∗ 2 6
5 # isEndOfWord i s True i f node r e p r e s e n t t h e end o f t h e
word
6 s e l f . isEndOfWord = F a l s e
Compact Trie If we assign only one letter per edge, we are not taking
full advantage of the trie’s tree structure. It is more useful to consider
compact or compressed tries, tries where we remove the one letter per edge
constraint, and contract non-branching paths by concatenating the letters on
these paths. In this way, every node branches out, and every node traversed
represents a choice between two different words. The compressed trie that
corresponds to our example trie is also shown in Figure 7.6.
| | is the alphlbetical size, and N is the total number of nodes in the trie
P
Note: You may assume that all inputs are consist of lowercase letters
a-z. All inputs are guaranteed to be non-empty strings.
1 d e f i n s e r t ( s e l f , word ) :
2 """
3 I n s e r t s a word i n t o t h e t r i e .
4 : type word : s t r
5 : rtype : void
6 """
7 node = s e l f . r o o t #s t a r t from t h e r o o t node
8 f o r c i n word :
9 l o c = ord ( c )−ord ( ' a ' )
10 i f node . c h i l d r e n [ l o c ] i s None : # c h a r d o e s not
e x i s t , new one
11 node . c h i l d r e n [ l o c ] = s e l f . TrieNode ( )
12 # move t o t h e next node
13 node = node . c h i l d r e n [ l o c ]
14 # s e t the f l a g to true
15 node . is_word = True
1 d e f s t a r t W i t h ( s e l f , word ) :
2 node = s e l f . r o o t
3 f o r c i n word :
4 l o c = ord ( c )−ord ( ' a ' )
5 # c a s e 1 : not a l l l e t t e r s matched
6 i f node . c h i l d r e n [ l o c ] i s None :
7 return False
8 node = node . c h i l d r e n [ l o c ]
9 # case 2
10 r e t u r n True
Now complete the given Trie class with TrieNode and __init__ func-
tion.
1 c l a s s Trie :
2 c l a s s TrieNode :
122 7. GRAPHS AND TREES
3 d e f __init__ ( s e l f ) :
4 s e l f . is_word = F a l s e
5 s e l f . c h i l d r e n = [ None ] ∗ 26 #t h e o r d e r o f t h e
node r e p r e s e n t s a c h a r
6
7 d e f __init__ ( s e l f ) :
8 """
9 I n i t i a l i z e your data s t r u c t u r e h e r e .
10 """
11 s e l f . r o o t = s e l f . TrieNode ( ) # r o o t has v a l u e None
7.1 336. Palindrome Pairs (hard). Given a list of unique words, find
all pairs of distinct indices (i, j) in the given list, so that the concate-
nation of the two words, i.e. words[i] + words[j] is a palindrome.
1 Example 1 :
2
3 Input : [ " abcd " , " dcba " , " l l s " , " s " , " s s s l l " ]
4 Output : [ [ 0 , 1 ] , [ 1 , 0 ] , [ 3 , 2 ] , [ 2 , 4 ] ]
5 E x p l a n a t i o n : The p a l i n d r o m e s a r e [ " dcbaabcd " , " abcddcba " , "
s l l s " ," l l s s s s l l "]
6
7 Example 2 :
8
9 Input : [ " bat " , " tab " , " c a t " ]
10 Output : [ [ 0 , 1 ] , [ 1 , 0 ] ]
11 E x p l a n a t i o n : The p a l i n d r o m e s a r e [ " b a t t a b " , " t a b b a t " ]
There are several other data structures, like balanced trees and hash
tables, which give us the possibility to search for a word in a dataset of
strings. Then why do we need trie? Although hash table has O(1) time
complexity for looking for a key, it is not efficient in the following operations
:
8.1 Heap
Heap is a tree based data structures that satisfies heap property but im-
plemented as an array data structure. There are two kinds of heaps: max-
heaps and min-heaps. In both kinds, the values in the nodes satisfy a
heap property. For max-heap, the property states as for each subtree rooted
at x, items on all of its children subtrees of x are smaller than x. Normally,
heap is based on binary tree, which makes it a binary heap. Fig. 8.1 show a
binary max-heap and how it looks like in a binary tree data structure. In the
following content, we default our heap is a binary heap. Thus, the largest
element in a max-heap is stored at the root. For a heap of n elements the
height is log n).
As we can see we can implement heap as an array due to the fact that
the tree is complete. A complete binary tree is one in which each level must
be fully filled before starting to fill the next level. Array-based heap is more
space efficient compared with tree based due to the non-existence of the
child pointers for each node. To make the math easy, we iterate node in the
tree starting from root in the order of level by level and from left to right
with beginning index as 1 (shown in Fig. 8.1). According to such assigning
rule, the node in the tree is mapped and saved in the array by the assigned
127
128 8. HEAP AND PRIORITY QUEUE
Figure 8.1: Max-heap be visualized with binary tree structure on the left,
and implemnted with Array on the right.
index (shown in Fig. 8.1). In heap, we can traverse the imaginary binary
tree in two directions: root-to-leaf and leaf-to-root. Given a parent node
with p as index, the left child of can be found in position 2p in the array.
Similarly, the right child of the parent is at position 2p+1 in the list. To find
the parent of any node in the tree, we can simply use bp/2c. In Python3,
use integer division n//2. Note: we can start index with 0 as used in heapq
library introduced later in this section. Given a node x, the left and right
child will be 2∗x+1, 2∗x+2, and the parent node will have index (x−1)//2.
The common application of heap data structure include:
5 d e f __str__ ( s e l f ) :
6 out = ' '
7 f o r i in range (1 , s e l f . s i z e + 1) :
8 out += s t r ( s e l f . heap [ i ] ) + ' '
9 r e t u r n out
Pop with Sinking When we pop an item, we first save the root node’s
value in order to get either the maximum or minimum item in the heap.
Then we simply use the last item to fill in the root node. After this, we
need to traverse from the root node root-to-leaf, and compare each parent
with left and right child. In a min-heap, if the parent is larger than its
smaller child node, we swap the parent with the smaller child. This process
is called sinking. Same as the insert, O(log n).
1 d e f _sink ( s e l f , i n d e x ) : # e n f o r c e min−heap , r o o t −to−l e a f
2 while 2 ∗ index < s e l f . s i z e :
3 l i = 2 ∗ index
4 ri = li + 1
5 mi = l i i f s e l f . heap [ l i ] < s e l f . heap [ r i ] e l s e r i
6 i f s e l f . heap [ i n d e x ] > s e l f . heap [ mi ] :
7 # swap i n d e x with mi
8 s e l f . heap [ i n d e x ] , s e l f . heap [ mi ] = s e l f . heap [ mi ] ,
s e l f . heap [ i n d e x ]
9 i n d e x = mi
10 d e f pop ( s e l f ) :
11 v a l = s e l f . heap [ 1 ]
12 s e l f . heap [ 1 ] = s e l f . heap . pop ( )
13 s e l f . s i z e −= 1
14 s e l f . _sink ( i n d e x = 1 )
15 return val
130 8. HEAP AND PRIORITY QUEUE
1
https://www.geeksforgeeks.org/time-complexity-of-building-a-heap/
8.1. HEAP 131
functions like merge(), nlargest(), nsmallest() that we can use. Check out
https://docs.python.org/3.0/library/heapq.html for more detials.
Now, let us try to heapify the same examplary list as used in the last
section, [21, 1, 45, 78, 3, 5], we use need to call the function heapify(). The
time complexity of heapify is O(n)
1 ' ' ' implementing with heapq ' ' '
2 from heapq import heappush , heappop , h e a p i f y
3 h = [ 2 1 , 1 , 45 , 78 , 3 , 5 ]
4 heapify (h) # inplace
5 p r i n t ( ' h e a p i f y with heapq : ' , h )
1. CPU Scheduling
Implement with heapq Library The core function is the ones used to
implement the heap: heapify(), push(), and pop(). Here we demonstrate
how to use function nlargest() and nsmallest() if getting the first n largest
or smallest is what we need, we do not need to heapify() the list as we needed
in the heap and pop out the smallest. The step of heapify is built in these
two functions.
1 ' ' ' u s e heapq t o g e t n l a r g e s t and n s m a l l e s t ' ' '
2 l i 1 = [ 2 1 , 1 , 45 , 78 , 3 , 5 ]
3 # u s i n g n l a r g e s t t o p r i n t 3 l a r g e s t numbers
8.2. PRIORITY QUEUE 133
We can also pass by any data type that supports ’<’ comparison oepra-
tor, such as tuple, it will use the first item in the tuple as the key.
If we want to give the number with larger value as higher priority, a
simple hack is to pass by negative value. Another more professional way is
to pass by a customized object and rewrite the comparison operator: < and
== in the class with __lt__() and __eq__(). In the following code, we
show how to use higher value as higher priority.
1 c l a s s Job ( o b j e c t ) :
2 d e f __init__ ( s e l f , p r i o r i t y , d e s c r i p t i o n ) :
3 self . priority = priority
4 self . description = description
5 p r i n t ( 'New j o b : ' , d e s c r i p t i o n )
6 return
7 # d e f __cmp__( s e l f , o t h e r ) :
8 # r e t u r n cmp( s e l f . p r i o r i t y , o t h e r . p r i o r i t y )
9 ' ' ' c u s t o m i z e t h e comparison o p e r a t o r s ' ' '
10 d e f __lt__( s e l f , o t h e r ) : # <
11 try :
134 8. HEAP AND PRIORITY QUEUE
Let us take these knowledge into practice with a LeetCode Problem: 347.
Top K Frequent Elements (medium). Given a non-empty array of integers,
return the k most frequent elements.
Example 1 :
Input : nums = [ 1 , 1 , 1 , 2 , 2 , 3 ] , k = 2
Output : [ 1 , 2 ]
Example 2 :
Input : nums = [ 1 ] , k = 1
Output : [ 1 ]
frequent elements. The second step can be down with sorting, or using heap
we learned.
Solution 1: Use Counter(). Counter() has a function most_common(k)
that will return the top k most frequent items. However, its complexity will
be O(n log n).
1 from c o l l e c t i o n s import Counter
2 d e f topKFrequent ( s e l f , nums , k ) :
3 r e t u r n [ x f o r x , _ i n Counter ( nums ) . most_common ( k ) ]
8.3 Bonus
Complete Search
137
139
141
142 9. LINEAR DATA STRUCTURES-BASED SEARCH
1 d e f l i n e a r S e a r c h (A, t ) : #A i s t h e a r r a y , and t i s t h e t a r g e t
2 f o r i , v i n enumerate (A) :
3 i f A[ i ] == t :
4 return i
5 r e t u r n −1
1. Find Exact target, find the first position that value>= target, find
the last position that value<= target.(this is called lower_bound, and
upper_bound.
Find the Exact Target This is the most basic application of binary
search. We can set two pointers, l and r. Each time we compute the middle
position, and check if it is equal to the target. If it is, return the position;
if it is smaller than the target, move to the left half, otherwise, move to the
right half. The Python code is given:
1 def standard_binary_search ( l s t , t a r g e t ) :
2 l , r = 0 , len ( l s t ) − 1
3 w h i l e l <= r :
9.2. BINARY SEARCH 143
4 mid = l + ( r − l ) // 2
5 i f l s t [ mid ] == t a r g e t :
6 r e t u r n mid
7 e l i f l s t [ mid ] < t a r g e t :
8 l = mid + 1
9 else :
10 r = mid − 1
11 r e t u r n −1 # t a r g e t i s not found
From the example, we can see that multiple duplicates of the target exist,
it can possibly return any one of them. And for the case when the target
does not exist, it simply returns -1. In reality, we might need to find a
position where we can potentially insert the target to keep the sorted array
sorted. There are two cases: (1) the first position that we can insert, which
is the first position that has value>= target (2) and the last position we can
insert, which is the first position that has value > target. For example, if we
try to insert 3 in a, and 1 in b, the first position should be 1 and 1 in each
array, and the last position is 1 and 6 instead. For these two cases, we have
a Python built-in Module bisect which offers two methods: bisect_left()
and bisect_right() for these two cases respectively.
Find the First Position that value >= target This way the target
position separates the array into two halves: value < target, target_position,
value>= target. In order to meet the purpose, we make sure that if value
< target, we move to the right side, else, move to the left side.
1 # b i s e c t _ l e f t , no l o n g e r need t o check t h e mid element ,
2 # i t s e p a r a t e t h e l i s t i n t o two h a l f s : v a l u e < t a r g e t , mid ,
v a l u e >= t a r g e t
3 def bisect_left_raw ( lst , target ) :
4 l , r = 0 , l e n ( l s t )−1
5 w h i l e l <= r :
6 mid = l + ( r−l ) //2
7 i f l s t [ mid ] < t a r g e t : # move t o t h e r i g h t h a l f i f t h e
value < target , t i l l
8 l = mid + 1 #[ mid+1 , r i g h t ]
9 e l s e :# move t o t h e l e f t h a l f i s v a l u e >= t a r g e t
10 r = mid − 1 #[ l e f t , mid −1]
11 r e t u r n l # t h e f i n a l p o s i t i o n i s where
Find the First Position that value > target This way the target posi-
tion separates the array into two halves: value <= target, target_position,
144 9. LINEAR DATA STRUCTURES-BASED SEARCH
Bonus For the last two cases, if we return the position as l-1, then we get
the last position that value < target, and the last position value <= target.
Python Built-in Module bisect This module provides support for main-
taining a list in sorted order without having to sort the list after each in-
sertion. It offers six methods as shown in Table 9.1. However, only two are
most commonly used: bisect_left and bisect_right. Let’s see come exam-
plary code:
9.2. BINARY SEARCH 145
1 from b i s e c t import b i s e c t _ l e f t , b i s e c t _ r i g h t , b i s e c t
2 print ( " bisect l e f t : find 3 in a : " , b i s e c t _ l e f t (a , 3 ) , ' find 1 in
b : ' , b i s e c t _ l e f t ( b , 1 ) ) # lower_bound , t h e f i r s t p o s i t i o n
t h a t v a l u e>= t a r g e t
3 p r i n t ( " b i s e c t r i g h t : f i n d 3 in a : " , b i s e c t _ r i g h t ( a , 3) , ' f i n d 1
i n b : ' , b i s e c t _ r i g h t ( b , 1 ) ) # upper_bound , t h e l a s t
p o s i t i o n t h a t v a l u e <= t a r g e t
I n p u t : nums = [ 3 , 4 , 5 , 6 , 7 , 0 , 1 , 2 ] , t a r g e t = 0
Output : 5
Example 2 :
I n p u t : nums = [ 4 , 5 , 6 , 7 , 0 , 1 , 2 ] , t a r g e t = 3
Output : −1
In the rotated sorted array, the array is not purely monotonic. Instead,
there is one drop in the array because of the rotation, where it cuts the array
into two parts. Suppose we are starting with a standard binary search with
example 1, at first, we will check index 3, then we need to move to the right
side? Assuming we compare our middle item with the left item,
i f nums [ mid ] > nums [ l ] : # t h e l e f t h a l f i s s o r t e d
e l i f nums [ mid ] < nums [ l ] : # t h e r i g h t h a l f i s s o r t e d
e l s e : # f o r c a s e l i k e [ 1 , 3 ] , move t o t h e r i g h t h a l f
For a standard binary search, we simply need to compare the target with the
middle item to decide which way to go. In this case, we can use objection.
Check which side is sorted, because no matter where the left, right and the
middle index is, there is always one side that is sorted. So if the left side is
sorted, and the value is in the range of the [left, mid], then we move to the
left part, else we object the left side, and move to the right side instead.
146 9. LINEAR DATA STRUCTURES-BASED SEARCH
e l s e : # f o r case l i k e [ 1 , 3 ] , or [ 1 , 3 , 1 , 1 , 1 ] or [ 3 , 1 , 2 ,
3 , 3 , 3]
o n l y l++
1. get the result search range [l, r] which is the initial value for l and r
pointers.
For example:
9.1 441. Arranging Coins (easy). You have a total of n coins that you
want to form in a staircase shape, where every k-th row must have
exactly k coins. Given n, find the total number of full staircase rows
that can be formed. n is a non-negative integer and fits within the
range of a 32-bit signed integer.
Example 1 :
n = 5
Because t h e 3 rd row i s i n c o m p l e t e , we r e t u r n 2 .
1 def arrangeCoins ( s e l f , n) :
2 d e f i s V a l i d ( row ) :
3 r e t u r n ( row ∗ ( row+1) ) //2 <= n
4 # we need t o f i n d t h e l a s t p o s i t i o n t h a t i s v a l i d (<=)
5 def bisect_right () :
6 l , r = 1, n
7 w h i l e l <= r :
8 mid = l + ( r−l ) // 2
9 i f i s V a l i d ( mid ) : # r e p l a c e d compared with t h e
standard binary search
10 l = mid + 1
11 else :
12 r = mid − 1
13 r e t u r n l −1
14 return bisect_right ()
9.2 278. First Bad Version. You are a product manager and currently
leading a team to develop a new product. Unfortunately, the latest
version of your product fails the quality check. Since each version is
developed based on the previous version, all the versions after a bad
version are also bad.
Suppose you have n versions [1, 2, ..., n] and you want to find out the
first bad one, which causes all the following ones to be bad.
Solution: we keep doing binary search until we have searched all pos-
sible areas.
1 class Solution ( object ) :
2 def firstBadVersion ( s e l f , n) :
3 """
4 : type n : i n t
5 : rtype : int
6 """
7 l , r =0 ,n−1
8 l a s t = −1
9 w h i l e l<=r :
10 mid = l +(r−l ) //2
11 i f i s B a d V e r s i o n ( mid+1) : #move t o t h e l e f t , mid
i s index , s
12 r=mid−1
13 l a s t = mid+1 #t o t r a c k t h e l a s t bad one
14 else :
15 l=mid−1
16 return l a s t
9.2. BINARY SEARCH 149
I nput : [ 1 , 3 , 5 , 6 ] , 5
Output : 2
Example 2 :
I nput : [ 1 , 3 , 5 , 6 ] , 2
Output : 1
Example 3 :
I nput : [ 1 , 3 , 5 , 6 ] , 7
Output : 4
Example 4 :
I nput : [ 1 , 3 , 5 , 6 ] , 0
Output : 0
1 # inclusive version
2 d e f s e a r c h I n s e r t ( s e l f , nums , t a r g e t ) :
3 l = 0
150 9. LINEAR DATA STRUCTURES-BASED SEARCH
4 r = l e n ( nums )−1
5 w h i l e l <= r :
6 m = ( l+r ) //2
7 i f t a r g e t > nums [m ] : #s e a r c h t h e r i g h t h a l f
8 l = m+1
9 e l i f t a r g e t < nums [m] : # s e a r c h f o r t h e l e f t h a l f
10 r = m−1
11 else :
12 return m
13 return l
[
[1 , 3 , 5 , 7] ,
[ 1 0 , 11 , 16 , 2 0 ] ,
[ 2 3 , 30 , 34 , 50]
]
Given t a r g e t = 3 , r e t u r n t r u e .
15 w h i l e s r<=e r :
16 mid = s r +( er−s r ) //2
17 i f t a r g e t >matrix [ mid ] [ − 1 ] : #go t o t h e r i g h t
side
18 s r=mid+1
19 e l i f t a r g e t < matrix [ mid ] [ 0 ] : #go t h e t h e l e f t
side
20 e r = mid−1
21 e l s e : #v a l u e might be i n t h i s row
22 #s e a r c h i n t h i s row
23 l c , r c = 0 , c o l −1
24 w h i l e l c <=r c :
25 midc = l c +( rc−l c ) //2
26 i f matrix [ mid ] [ midc]== t a r g e t :
27 r e t u r n True
28 e l i f t a r g e t <matrix [ mid ] [ midc ] : #go t o
left
29 r c=midc−1
30 else :
31 l c=midc+1
32 return False
33 return False
2. 153. Find Minimum in Rotated Sorted Array (medium) The key here
is to compare the mid with left side, if mid-1 has a larger value, then
that is the minimum
2. Opposite-directional: One at the start and the other at the end, they
move close to each other and meet in the middle, (-> <-).
In order to use two pointers, most times the data structure needs to be
ordered in some way, and decrease the time complexity from O(n2 ) or O(n3 )
of two/three nested for/while loops to O(n) of just one loop with two pointers
and search each item just one time. In some cases, the time complexity is
highly dependable on the data and the criteria we set.
As shown in Fig. 9.2, the pointer i and j can decide: a pair or a subarray
(with all elements starts from i and end at j). We can either do search related
with a pair or a subarray. For the case of subarray, the algorithm is called
sliding window algorithm. As we can see, two pointers and sliding window
algorithm can be used to solve K sum (Section ??), most of the subarray
(Section ??), and string pattern match problems (Section ??).
Two pointer algorithm is less of a talk and more of problem attached. We
will explain this type of algorithm in virtue of both the leetcode problems
and definition of algorihtms. To understand two pointers techniques, better
to use examples, here we use two examples: use slow-faster pointer to find
9.3. TWO-POINTER SEARCH 153
the median and Floyd’s fast-slow pointer algorithm for loop detection in an
array/linked list and two pointers to get two sum.
Input : [ 1 , 2 , 3 , 4 , 5 ]
Output : Node 3 from t h i s l i s t ( S e r i a l i z a t i o n : [ 3 , 4 , 5 ] )
Example 2 ( even l e n g t h ) :
Input : [ 1 , 2 , 3 , 4 , 5 , 6 ]
Output : Node 4 from t h i s l i s t ( S e r i a l i z a t i o n : [ 4 , 5 , 6 ] )
We place two pointers simultaneously at the head node, each one moves at
different paces, the slow pointer moves one step and the fast moves two steps
instead. When the fast pointer reached the end, the slow pointer will stop at
154 9. LINEAR DATA STRUCTURES-BASED SEARCH
the middle. For the loop, we only need to check on the faster pointer, make
sure fast pointer and fast.next is not None, so that we can successfuly visit
the fast.next.next. When the length is odd, fast pointer will point at the
end node, because fast.next is None, when its even, fast pointer will point
at None node, it terimates because fast is None.
1 d e f middleNode ( s e l f , head ) :
2 slow , f a s t = head , head
3 w h i l e f a s t and f a s t . next :
4 f a s t = f a s t . next . next
5 s l o w = s l o w . next
6 return slow
In order to know the starting node of the cycle. Here, we set the distance
of the starting node of the cycle from the head is x, and y is the distance from
the start node to the slow and fast pointer’s node, and z is the remaining
distance from the meeting point to the start node.
Now, let’s try to device the algorithm. Both slow and fast pointer starts
at position 0, the node index they travel each step is: [0,1,2,3,...,k] and
[0,2,4,6,...,2k] for slow and fast pointer respectively. Therefore, the total
distance traveled by the slow pointer is half of the distance travelled by the
fat pointer. From the above figure, we have the distance travelled by slow
pointer to be ds = x+y, and for the fast pointer df = x+y+z+y = x+2y+z.
With the relation 2 ∗ ds = df . We will eventually get x = z. Therefore, by
moving slow pointer to the start of the linked list after the meeting point,
and making both slow and fast pointer to move one node at a time, they
will meet at the starting node of the cycle. (LeetCode problem: 142. Linked
List Cycle II (medium)).
1 d e f d e t e c t C y c l e ( s e l f , head ) :
2 s l o w = f a s t = head
3 bCycle = F a l s e
9.3. TWO-POINTER SEARCH 155
4 w h i l e f a s t and f a s t . next :
5 s l o w = s l o w . next
6 f a s t = f a s t . next . next
7 i f s l o w == f a s t : # a c y c l e i s found
8 bCycle = True
9 break
10
11 i f not bCycle :
12 r e t u r n None
13 # r e s e t t h e s l o w p o i n t e r t o f i n d t h e s t a r t i n g node
14 s l o w = head
15 w h i l e f a s t and s l o w != f a s t :
16 s l o w = s l o w . next
17 f a s t = f a s t . next
18 return slow
In order to remove the cycle as shown in Fig. 9.5, the starting node is when
slow and fast intersect, the last fast node before they meet. For the example,
we need to set -4 node’s next node to None. Therefore, we modify the above
code to stop at the last fast node instead:
1 # r e s e t t h e s l o w p o i n t e r t o f i n d t h e s t a r t i n g node
2 s l o w = head
3 w h i l e f a s t and s l o w . next != f a s t . next :
156 9. LINEAR DATA STRUCTURES-BASED SEARCH
4 s l o w = s l o w . next
5 f a s t = f a s t . next
6 f a s t . next = None
Due to the fact that the array is sorted which means in the array [s,s1
..., e1, e], the sum of any two integer is in range of [s+s1, e1+e]. By placing
two pointers each start from s and e, we started the search space from the
middle of the possible range. [s+s1, s+e, e1+e]. Compare the target t with
the sum of the two pointers v1 and v2 :
1. t == v1 + v2 : found
1 d e f twoSum ( s e l f , numbers , t a r g e t ) :
2 #u s e two p o i n t e r s
3 n = l e n ( numbers )
4 i , j = 0 , n−1
5 while i < j :
9.3. TWO-POINTER SEARCH 157
Sliding Window Algorithm with Sum In this part, we list two exam-
ples that we use flexible sliding window algorithms to solve subarray problem
with sum condition.
Given an array of n positive integers and a positive integer s, find the
minimal length of a contiguous subarray of which the sum >= s. If there isn’t
one, return 0 instead. (LeetCode Problem: 209. Minimum Size Subarray
Sum (medium)).
Example :
Input : s = 7 , nums = [ 2 , 3 , 1 , 2 , 4 , 3 ]
Output : 2
E x p l a n a t i o n : t h e s u b a r r a y [ 4 , 3 ] has t h e minimal l e n g t h under t h e
problem c o n s t r a i n t .
9.3. TWO-POINTER SEARCH 159
As we have shown in Fig. 9.7, the prefix sum is the subarray starts with the
first item in the array, we know that the sum of the subarray is monotoni-
cally increasing as the size of the subarray increase. Therefore, we place a
’window’ with left and right as i and j at the first item first. The steps are
as follows:
1. Get the optimal subarray starts from current i, 0: Then we first move
the j pointer to include enough items that sum[0:j+1]>=s, this is the
process of getting the optimial subarray that starts with 0. And as-
sume j stops at e0
2. Get the optimal subarray ends with current j, e0 : we shrink the window
size by moving the i pointer forward so that we can get the optimal
subarray that ends with current j and the optimal subarray starts from
s0 .
3 n = l e n ( nums )
4 i = j = 0
5 acc = 0 # acc i s the s t a t e
6 while j < n :
7 a c c += nums [ j ]# i n c r e a s e t h e window s i z e
8 w h i l e a c c >= s :# s h r i n k t h e window t o g e t t h e o p t i m a l
result
9 ans = min ( ans , j −i +1)
10 a c c −= nums [ i ]
11 i += 1
12 j +=1
13 r e t u r n ans i f ans != f l o a t ( ' i n f ' ) e l s e 0
Example 2 :
window in a way to get rid of this char in the current window by moving
the i pointer one step after this char.
1 def lengthOfLongestSubstring ( s e l f , s ) :
2 i f not s :
3 return 0
4 n = len ( s )
5 state = set ()
6 i = j = 0
7 ans = − f l o a t ( ' i n f ' )
8 while j < n :
9 i f s [ j ] not i n s t a t e :
10 s t a t e . add ( s [ j ] )
11 ans = max( ans , j −i )
12 else :
13 # s h r i n k t h e window : g e t t h i s c h a r out o f t h e window
14 w h i l e s [ i ] != s [ j ] : # f i n d t h e c h a r
15 s t a t e . remove ( s [ i ] )
16 i += 1
17 # skip t h i s char
18 i += 1
19 j += 1
20 r e t u r n ans i f ans != − f l o a t ( ' i n f ' ) e l s e 0
Now, let us see another example with string ang given a pattern to match.
Given a string S and a string T, find the minimum window in S which will
contain all the characters in T in complexity O(n). (LeetCode Problem: 76.
Minimum Window Substring (hard))
Example :
I n p u t : S = "ADOBECODEBANC" , T = "ABC"
Output : "BANC"
In this problem, the desirable window is one that has all characters
from T. The solution is pretty intuitive. We keep expanding the window by
moving the right pointer. When the window has all the desired characters,
we contract (if possible) and save the smallest window till now. The only
difference compared with the above problem is the definition of desirable:
we need to compare the state of current window with the required state in
T. They can be handled as a hashmap with character as key and frequency
of characters as value.
1 d e f minWindow ( s e l f , s , t ) :
2 d i c t _ t = Counter ( t )
3 s t a t e = Counter ( )
4 required = len ( dict_t )
5
6 # l e f t and r i g h t p o i n t e r
7 i , j = 0, 0
8
9 formed = 0
10 ans = f l o a t ( " i n f " ) , None # min l e n , and s t a r t pos
162 9. LINEAR DATA STRUCTURES-BASED SEARCH
11
12 while j < len ( s ) :
13 char = s [ j ]
14 # record current state
15 i f char in dict_t :
16 s t a t e [ c h a r ] += 1
17 i f s t a t e [ c h a r ] == d i c t _ t [ c h a r ] :
18 formed += 1
19
20 # Try and c o n t r a c t t h e window t i l l t h e p o i n t where i t
c e a s e s t o be ' d e s i r a b l e ' .
21 # bPrint = False
22 w h i l e i<=j and formed == r e q u i r e d :
23 # i f not b P r i n t :
24 # p r i n t ( ' found : ' , s [ i : j +1] , i , j )
25 # b P r i n t = True
26 char = s [ i ]
27 i f j −i +1 < ans [ 0 ] :
28 ans = j − i + 1 , i
29 # change t h e s t a t e
30 i f char in dict_t :
31 s t a t e [ c h a r ] −= 1
32 i f s t a t e [ c h a r ] == d i c t _ t [ c h a r ] −1:
33 formed −= 1
34
35 # Move t h e l e f t p o i n t e r ahead ,
36 i += 1
37
38 # Keep expanding t h e window
39 j += 1
40 # i f bPrint :
41 # p r i n t ( ' move t o : ' , s [ i : j +1] , i , j )
42 r e t u r n " " i f ans [ 0 ] == f l o a t ( " i n f " ) e l s e s [ ans [ 1 ] : ans [ 1 ] +
ans [ 0 ] ]
Input : A = [ 1 , 0 , 1 , 0 , 1 ] , S = 2
Output : 4
9.3. TWO-POINTER SEARCH 163
Explanation :
The 4 s u b a r r a y s a r e b o l d e d below :
[1 ,0 ,1 ,0 ,1]
[1 ,0 ,1 ,0 ,1]
[1 ,0 ,1 ,0 ,1]
[1 ,0 ,1 ,0 ,1]
• Substring: for an array with char as value, it requires the state of each
subarray does not related to the order of the characters (anagram-like
state) so that we can have the sliding window property.
2. Handle the right pointer and record the state of the current window;
4. Up till here, the state is not desirable. Move the right pointer in order
to find a desirable window;
165
166 10. ELEMENTARY GRAPH-BASED SEARCH
The breadth first search (BFS) and the depth first search (DFS) are the
two algorithms used for traversing and searching a node in a graph or a
tree. They can also be used to find out whether a node is reachable from a
given node or not. In Fig. 10.1 shows the BFS and DFS traverse ordering.
Starting from a given vertex u, BFS will traverse all of its adjacency nodes
vi and print out them, and then continue to the adjacency nodes of vi , while
the DFS will traverse all of its adjacency nodes, but in a recursively way,
which it recursively traverse the adjacency nodes of the current node untill
reaching to a node that has no outgoing nodes. The implementation of BFS
and DFS will be detailed in Section 10.1.1 and 10.1.3. Both BFS and DFS
has a time complexity of O(V + E) with adjacency list and O(V 2 ) with
adjacency matrix.
1. WHITE: (False, -1), which is the initial state of each vertex in the G,
and has not been visited yet.
2. BLACK: (True, 1), which markes that the node is fully visited and
complete, in DFS, it means returned from the recursive call.
A baisc DFS and BFS implementation will only need state 1) and 3).
While in some advanced extension of DFS and BFS, state 2) might be
needed, e.g.
Searching is an universal approach in problem solving. With searching, it
literally search in the solution space and find the solutions. In the following
section, we will implement BFS and DFS, with example shown in Fig. 10.2
and with the following network strcture in Python code:
1 adjacency_matrix = { 1 : [ 2 , 3 ] , 2 : [ 4 , 5 ] ,
2 3: [5] , 4: [6] , 5: [6] ,
3 6: [7] , 7: [ ] }
First In First Out order one by one; the other is called Level by Level BFS,
in this method, it saves all same-level vertices and visit them all at once.
Node by Node BFS Using a Queue here fits to save the next "root"
nodes. The Python implementation is given:
1 d e f b f s _ i t e r a t i v e ( graph , s t a r t ) :
2 queue , path = [ s t a r t ] , [ ]
3 visited = set ([ start ])
4
5 w h i l e queue :
6 p r i n t ( queue )
7 v e r t e x = queue . pop ( 0 ) #FIFO
8 path . append ( v e r t e x )
9 f o r n e i g h b o r i n graph [ v e r t e x ] :
10 i f not n e i g h b o r i n v i s i t e d :
11 queue . append ( n e i g h b o r )
12 v i s i t e d . add ( n e i g h b o r )
13 r e t u r n path
14 p r i n t ( b f s _ i t e r a t i v e ( adjacency_matrix , 1 ) )
15 #[ 1 , 2 , 3 , 4 , 5 , 6 , 7 ]
When and How When Bidirectional search can find the shortest path
successfully if all the paths are assigned uniform costs.
How When the graph from each side is not balanced: each node has
various branch factors. We take two queues: sourceQueue for BFS in forward
direction from source to target and targetQueue which is used to do the
BFS from the target towards the source in backward direction. We try to
alternate between the two queues: sourceQueue and targetQueue; basically
in every iteration we choose the smaller queue for the next iteration for
the processing which effectively helps in alternating between the two queues
only when the swapping between the two queues is profitable.
This helps in the following way: As we go deeper into a graph the number
of edges can grow exponentially. Since we are approaching in a balanced way,
selecting the queue which has smaller number of nodes for the next iteration,
we are avoiding processing a large number of edges and nodes by trying to
having the intersection point somewhere in the middle.
Since we are processing both the target and source queue we are not
going to much depth from any direction, either in forward direction (i.e,
while processing source queue) or in backward direction (i.e, target queue
which searches from target to source in backward manner).
Implementation
16 # [1 , 2 , 4 , 6 , 7 , 5 , 3]
Iterative DFS In the BFS, we use a Queue to save the nodes in each
level. If we use the same way to put the nodes in a data structure, the Stack
is used to implement the iterative version of depth first search. Let’s see
how depth first search works with respect to the following graph:
1 d e f d f s _ i t e r a t i v e ( graph , s t a r t ) :
2 s t a c k , path = [ s t a r t ] , [ ]
3
4 while stack :
5 v e r t e x = s t a c k . pop ( )
6
7 path . append ( v e r t e x )
8 f o r n e i g h b o r i n graph [ v e r t e x ] :
9 i f n e i g h b o r i n path :
10 continue
11 s t a c k . append ( n e i g h b o r )
12
13 r e t u r n path
14
15
16 adjacency_matrix = { 1 : [ 2 , 3 ] , 2 : [ 4 , 5 ] ,
17 3: [5] , 4: [6] , 5: [6] ,
18 6: [7] , 7: [ ] }
19
20 p r i n t ( d f s _ i t e r a t i v e ( adjacency_matrix , 1 ) )
21 # [1 , 3 , 5 , 6 , 7 , 2 , 4]
10.1.4 Backtracking
Backtracking is a variant of Depth-first search, and is a general algorithm for
finding all (or some) solutions to some computational problems, that incre-
mentally builds candidates to the solutions. As soon as it determines that a
candidate cannot possibly lead to a valid complete solution, it abandons this
partial candidate and “backtracks” (return to the upper level) and reset to
that level’s state so that the search process can continue to explore the next
choice. Backtracking is all about choices and consequences, this is why back-
tracking is the most common algorithm for solving constraint satisfaction
problem (CSP)1 , such as Eight Queens puzzle, Map Coloring problem, Su-
doku, Crosswords, and many other logic puzzles. (N-Queens : permutations
with backtracking Soduko : counting with backtracking Scheduling: subsets
with backtrackinghttps://www.cs.princeton.edu/~rs/AlgsDS07/24CombinatorialSearch.
pdf)
Combination
Combinations refer to the combination of n things taken k at a time without
repetition, the math formula Cnk . Combination does not care about the or-
dering between chosen elements, such that [a, b] and [b, a] are considered as
the same solution, thus only one of them can exist. Some useful formulation
with combinations include Cnk = Cnn−k .
10.1. GRAPH TRAVERSAL 173
From the above result we can see it is a DFS traversal. What if we just
want to gain result of a single layer at Cnk (as shown in the Fig. 10.4)?
The change is trivial; we can set the return level k to the level we want and
collect the result only at the end condition.
1 # r e p l a c e t h e 2nd−6th l i n e o f code above with t h e f o l l o w i n g
174 10. ELEMENTARY GRAPH-BASED SEARCH
2 i f d == k :
3 ans . append ( c u r r )
4 return
To generate the power set, backtracking is NOT the only solution, if you are
interested right now, check out Section ??.
Time Complexity
Permutation
Permutations refer to the permutation of n things taken k at a time without
repetition, the math formula is Akn . Compared with combination, [a, b] and
[b, a] would be considered as different solution. The relation of the number
of combination and permutation solution can be described in formula: Cnk =
Akn
k! , where k! = k ∗ (k − 1)... ∗ 1. So A ≥ C.
Python Template
1 # t e m p l a t e f o r Ank , d==k , n==n
2 d e f permute ( nums , n , k ) :
3 """
4 : type n : i n t
5 : type k : i n t
6 : rtype : List [ List [ int ] ]
7 """
8 ans = [ ]
9 d e f A_n_k( d , k , used , s , c u r r ) : #d c o n t r o l s t h e d e g r e e ( depth ) ,
k i s c o n t r o l s t h e depth o f t h e s t a t e t r a n s f e r
10 n o n l o c a l ans
11 i f d==k :
12 ans . append ( c u r r )
13 return
14 f o r i in range (n) :
15 i f used [ i ] :
16 continue
17 used [ i ] = True
18 c u r r . append ( nums [ i ] )
19 A_n_k( d+1, k , used [ : ] , i +1 , c u r r [ : ] )
20 c u r r . pop ( )
21 used [ i ] = F a l s e
22 A_n_k( 0 , k , [ F a l s e ] ∗ n , 0 , [ ] )
23 r e t u r n ans
There is another way with brute force, each time we have the option choose
this element or not to choose this element. Include this content.
To see the application of this, to go chapter array with the subset
Given a, possibly, partially filled grid of size ‘n’, completely fill the grid
with number between 1 and ‘n’. Goal is defined for verifying the solution.
Once the goal is reached, searching terminates. A fully filled grid is a solution
if:
3. Each sub-grid (if any) has unique numbers form 1 to ‘n’ or empty
spaces.
Termination conditions
Typically, backtracking algorithms have termination conditions other
than reaching goal. These help with failures in solving the problem and
special cases of the problem itself.
1. There are no empty spots left to fill and the candidate still doesn’t
qualify as a the solution.
2. There are no empty spots to begin with, i.e., the grid is already fully
filled.
Step-by-step algorithm
Here’s how our code will “guess” at each step, all the way to the final
solution:
2. Select a spot and place a number, between 1 and ‘n’, in it and validate
the candidate grid.
1. Each of the digits 1-9 must occur exactly once in each row.
2. Each of the digits 1-9 must occur exactly once in each column.
3. Each of the the digits 1-9 must occur exactly once in each of the
9 3x3 sub-boxes of the grid.
48 #b a c k t r a c k , and c l e a r s t a t e s
49 board [ i ] [ j ] = ' . '
50 row_state [ i ] &= ~(1 << ( v ) )
51 c o l _ s t a t e [ j ] &= ~(1 << ( v ) )
52 g r i d _ s t a t e [ g r i d _ i n d e x ] &= ~(1 << ( v ) )
53 # no p o s s i b l e d i g i t t o t r y
54 return False
55
56 helper (0)
10.1.5 Summary
BFS and DFS are two of the most universal algorithms for solving practical
problems. Each suits better than the other to specific type of problems.
• For BFS, it suits problems that ask the shortest paths( unweighted )
from a certain source node to a certain destination, whether it is single
sourced or all-pairs. Or in some cases, the questions requires us only
traverse the graph for a certain steps (levels). Because BFS is iterative
and can traverse the nodes level by level.
• Use either BFS or DFS when we just need to check correctness (whether
we can reach a given state to another.
Similarly, the recursive code for the InOrder Traversal and PostTraversal:
1 d e f InOrder ( r o o t ) :
2 i f r o o t i s None :
3 return [ ]
4 res = [ ]
5 l e f t = InOrder ( r o o t . l e f t )
6 #p r i n t ( r o o t . v a l , end = ' , ' )
7 r i g h t = InOrder ( r o o t . r i g h t )
8 r e s = l e f t + [ r o o t . v a l ]+ r i g h t
9 return res
10
11 d e f PostOrder ( r o o t ) :
12 i f r o o t i s None :
180 10. ELEMENTARY GRAPH-BASED SEARCH
13 return [ ]
14 res = [ ]
15 l e f t = PostOrder ( r o o t . l e f t )
16 #p r i n t ( r o o t . v a l , end = ' , ' )
17 r i g h t = PostOrder ( r o o t . r i g h t )
18 res = l e f t + right + [ root . val ]
19 return res
20 p r i n t ( InOrder ( r o o t ) )
21 p r i n t ( PostOrder ( r o o t ) )
22 # output
23 #[ 4 , 2 , 5 , 1 , 3 ]
24 #[ 4 , 5 , 2 , 3 , 1 ]
6 while stack :
7 tmp = s t a c k . pop ( )
8 r e s . append ( tmp . v a l )
9 i f tmp . l e f t :
10 s t a c k . append ( tmp . l e f t )
11 i f tmp . r i g h t :
12 s t a c k . append ( tmp . r i g h t )
13 return res [:: −1]
InOrder Iterative. In the inorder, we need to print out all the left subtree
first, and then the root, followed by the right. The process is as follows:
1 1 ) C r e a t e an empty s t a c k S .
2 2 ) I n i t i a l i z e c u r r e n t node a s r o o t
3 3 ) Push t h e c u r r e n t node t o S and s e t c u r r e n t = c u r r e n t −> l e f t
u n t i l c u r r e n t i s NULL
4 4 ) I f c u r r e n t i s NULL and s t a c k i s not empty then
5 a ) Pop t h e top item from s t a c k .
6 b ) P r i n t t h e popped item , s e t c u r r e n t = popped_item−>r i g h t
7 c ) Go t o s t e p 3 .
8 5 ) I f c u r r e n t i s NULL and s t a c k i s empty then we a r e done .
which is the same as merge sort. If the divide cost is only O(1).
10.2.4 Exercise
938. Range Sum of BST (Medium)
Given the root node of a binary search tree, return the sum of values
of all nodes with value between L and R (inclusive).
The binary search tree is guaranteed to have unique values.
10.3. EXERCISE 183
1 Example 1 :
2
3 I n p u t : r o o t = [ 1 0 , 5 , 1 5 , 3 , 7 , n u l l , 1 8 ] , L = 7 , R = 15
4 Output : 32
5
6 Example 2 :
7
8 I n p u t : r o o t = [ 1 0 , 5 , 1 5 , 3 , 7 , 1 3 , 1 8 , 1 , n u l l , 6 ] , L = 6 , R = 10
9 Output : 23
10.3 Exercise
10.3.1 Backtracking
77. Combinations
1 Given two i n t e g e r s n and k , r e t u r n a l l p o s s i b l e c o m b i n a t i o n s o f
k numbers out o f 1 . . . n .
2
3 Example :
4
5 Input : n = 4 , k = 2
6 Output :
7 [
8 [2 ,4] ,
9 [3 ,4] ,
10 [2 ,3] ,
11 [1 ,2] ,
12 [1 ,3] ,
13 [1 ,4] ,
14 ]
2
3 A mapping o f d i g i t t o l e t t e r s ( j u s t l i k e on t h e t e l e p h o n e
b u t t o n s ) i s g i v e n below .
4
5 Input : D i g i t s t r i n g " 2 3 "
6 Output : [ " ad " , " ae " , " a f " , " bd " , " be " , " b f " , " cd " , " c e " , " c f " ] .
7
8 Note :
9 Although t h e above answer i s i n l e x i c o g r a p h i c a l o r d e r , your
answer c o u l d be i n any o r d e r you want .
11
This chapter is built upon on the last chapter 10. Basically the advanced
application of DFS and BFS.
The first several sections, we talk about the advanced applications of
DFS, including Connected Components in Graph (Section 11.1), Topological
Sort(), Minimum Spanning Tree (MST) ??.
185
186 11. ADVANCED GRAPH-BASED SEARCH
X X X X
X O O X
X X O X
X O X X
A f t e r r u n n i n g your f u n c t i o n , t h e board s h o u l d be :
X X X X
X X X X
X X X X
X O X X
and then we just need another round to iterate the matrix to flip all
the remaining ’O’ and flip the ’-1’ back to ’O’.
1 d e f s o l v e ( s e l f , board ) :
2 i f not board :
3 return
4 rows , c o l s = l e n ( board ) , l e n ( board [ 0 ] )
5 i f rows == 1 o r c o l s == 1 :
6 return
7 i f rows == 2 and c o l s == 2 :
8 return
9 moves = [ ( 0 , −1) , ( 0 , 1 ) , ( −1 , 0 ) , ( 1 , 0 ) ]
10 # f i n d a l l c o n n e c t e d components t o t h e edge 0 , and mark
them a s −1,
11 # then f l i p a l l 0 s i n t h e o t h e r p a r t s
12 # change t h e −1 t o 0 s
13 d e f d f s ( x , y ) : # ( x , y ) i s t h e edge 0 s
14 f o r dx , dy i n moves :
15 nx = x + dx
16 ny = y + dy
17 i f nx < 0 o r nx >= rows o r ny < 0 o r ny >= c o l s
:
18 continue
19 i f board [ nx ] [ ny ] == 'O ' :
20 board [ nx ] [ ny ] = '−1 '
21 d f s ( nx , ny )
22 return
23 # f i r s t and l a s t c o l
24 f o r i i n r a n g e ( rows ) :
25 i f board [ i ] [ 0 ] == 'O ' :
26 board [ i ] [ 0 ] = '−1 '
27 d f s ( i , 0)
28 i f board [ i ] [ − 1 ] == 'O ' :
29 board [ i ] [ − 1 ] = '−1 '
30 d f s ( i , c o l s −1)
31 # # f i r s t and l a s t row
32 f o r j in range ( c o l s ) :
33 i f board [ 0 ] [ j ] == 'O ' :
34 board [ 0 ] [ j ] = '−1 '
35 dfs (0 , j )
36 i f board [ rows − 1 ] [ j ] == 'O ' :
37 board [ rows − 1 ] [ j ] = '−1 '
38 d f s ( rows −1, j )
39 f o r i i n r a n g e ( rows ) :
40 f o r j in range ( c o l s ) :
41 i f board [ i ] [ j ] == 'O ' :
42 board [ i ] [ j ] = 'X '
43 e l i f board [ i ] [ j ] == '−1 ' :
44 board [ i ] [ j ] = 'O '
45 else :
46 pass
I nput : n = 5 and e d g e s = [ [ 0 , 1 ] , [ 1 , 2 ] , [ 3 , 4 ] ]
0 3
| |
1 −−− 2 4
Output : 2
Example 2 :
I nput : n = 5 and e d g e s = [ [ 0 , 1 ] , [ 1 , 2 ] , [ 2 , 3 ] , [ 3 , 4 ] ]
0 4
| |
1 −−− 2 −−− 3
Output : 1
Solution: Use DFS. First, if given n node, and have edges, it will
have n components.
for n in v e r t i c e s :
i f n not v i s i t e d :
DFS( n ) # t h i s i s a component t r a v e r s e i t s c o n n e c t e d
components and mark them a s v i s i t e d .
Before we start the main part, it is easier if we can convert the edge list
into undirected graph using adjacencly list. Because it is undirected,
one edge we need to add two directions in the adjancency list.
1 d e f countComponents ( s e l f , n , e d g e s ) :
2 """
3 : type n : i n t
4 : type e d g e s : L i s t [ L i s t [ i n t ] ]
5 : rtype : int
6 """
7 i f not e d g e s :
8 return n
9 def dfs ( i ) :
10 for n in g [ i ] :
11 i f not v i s i t e d [ n ] :
12 v i s i t e d [ n ] = True
13 dfs (n)
14 return
15 # convert edges into a adjacency l i s t
16 g = [ [ ] f o r i in range (n) ]
17 f o r i , j in edges :
18 g [ i ] . append ( j )
19 g [ j ] . append ( i )
190 11. ADVANCED GRAPH-BASED SEARCH
20
21 # f i n d components
22 v i s i t e d = [ False ]∗ n
23 ans = 0
24 f o r i in range (n) :
25 i f not v i s i t e d [ i ] :
26 v i s i t e d [ i ] = True
27 dfs ( i )
28 ans += 1
29 r e t u r n ans
16 # A r e c u r s i v e f u n c t i o n t h a t u s e s v i s i t e d [ ] and p a r e n t t o
detect
17 # c y c l e i n subgraph r e a c h a b l e from v e r t e x v .
18 def i s C y c l i c U t i l ( s e l f , v , v i s i t e d , parent ) :
19
20 #Mark t h e c u r r e n t node a s v i s i t e d
21 v i s i t e d [ v]= True
22
23 #Recur f o r a l l t h e v e r t i c e s a d j a c e n t t o t h i s v e r t e x
24 f o r i i n s e l f . graph [ v ] :
25 # I f t h e node i s not v i s i t e d then r e c u r s e on i t
26 if v i s i t e d [ i ]== F a l s e :
27 i f ( s e l f . isCyclicUtil ( i , visited , v) ) :
28 r e t u r n True
29 # I f an a d j a c e n t v e r t e x i s v i s i t e d and not p a r e n t o f
current vertex ,
30 # then t h e r e i s a c y c l e
31 elif p a r e n t != i :
32 r e t u r n True
33
34 return False
35
36
37 #Returns t r u e i f t h e graph c o n t a i n s a c y c l e , e l s e f a l s e .
38 def isCyclic ( s e l f ) :
39 # Mark a l l t h e v e r t i c e s a s not v i s i t e d
40 v i s i t e d =[ F a l s e ] ∗ ( s e l f .V)
41 # Call the r e c u r s i v e helper f u n c t i o n to detect c y c l e in
different
42 #DFS t r e e s
43 f o r i i n r a n g e ( s e l f .V) :
44 i f v i s i t e d [ i ] ==F a l s e : #Don ' t r e c u r f o r u i f i t i s
already v i s i t e d
45 i f ( s e l f . i s C y c l i c U t i l ( i , v i s i t e d , −1) )== True :
46 r e t u r n True
47
48 return Fals
9 s e l f . graph = d e f a u l t d i c t ( l i s t )
10 s e l f .V = v e r t i c e s
11
12 d e f addEdge ( s e l f , u , v ) :
13 s e l f . graph [ u ] . append ( v )
14
15 def i s C y c l i c U t i l ( s e l f , v , visited , stack ) :
16 s t a c k . append ( v )
17 i f v i s i t e d [ v ] == GRAY:
18 print ( ' re v i s i t i n g : ' , v)
19 r e t u r n True
20
21 v i s i t e d [ v ] = GRAY # v i s i t i n g
22
23 f o r n e i g h b o u r i n s e l f . graph [ v ] :
24 i f v i s i t e d [ n e i g h b o u r ] != BLACK:
25 i f s e l f . i s C y c l i c U t i l ( neighbour , v i s i t e d , s t a c k ) ==
True :
26 r e t u r n True
27 s t a c k . remove ( v )
28 v i s i t e d [ v ] = BLACK
29 return False
30
31 # Returns t r u e i f graph i s c y c l i c e l s e f a l s e
32 def isCyclic ( s e l f ) :
33 v i s i t e d = [WHITE] ∗ s e l f .V
34
35 #r e c S t a c k = [ F a l s e ] ∗ s e l f .V
36 f o r node i n r a n g e ( s e l f .V) :
37 stack = [ ]
38 i f v i s i t e d [ node ] != BLACK:# v i s i t i t i f i t s not
completed
39 i f s e l f . i s C y c l i c U t i l ( node , v i s i t e d , s t a c k ) == True :
40 print ( stack )
41 r e t u r n True
42 return False
43
If we put the edge 2->0 and 3->0, we will get the cycle, plus the nodes in
the stack can tell us what is the cycle.
195
197
This part covers two important and advanced paradigm of the four king-
doms: dynamic programming (Chapter 12) and greedy algorithms (Chap-
ter 13).
As we discussed in Divide-and-conquer (Chapter 4), dynamic program-
ming is used for handling situations where in divide and conquer the sub-
problems overlaps. While, dynamic programming is not a panacea for this
problems, it typically applies to certain type of optimization problems which
we will explain in details in that chapter. Dynamic programming can be able
to decrease the exponential-time complexity into polynomial-time.
Greedy algorithms, just follows Dynamic Programming applies to similar
optimization problems with further improvement in efficiency due to making
each choice locally optimal.
198
12
Dynamic Programming
199
200 12. DYNAMIC PROGRAMMING
The Figure 12.1 record the two different methods, we can use memoization
and tabulation for short. Momoization and tabulation yield the same asymp-
totic time complexity, however the tabulation approach often has much bet-
ter constant factors, since it has less overhead for procedure calls. Usually,
202 12. DYNAMIC PROGRAMMING
I nput : [ 1 0 , 9 , 2 , 5 , 3 , 7 , 1 0 1 , 1 8 ]
204 12. DYNAMIC PROGRAMMING
Output : 4
E x p l a n a t i o n : The l o n g e s t i n c r e a s i n g s u b s e q u e n c e i s
[ 2 , 3 , 7 , 1 0 1 ] , t h e r e f o r e the length i s 4 .
Note: (1) There may be more than one LIS combination, it is only
necessary for you to return the length. (2) Your algorithm should run
in O(n2 ) complexity.
Follow up: Could you improve it to O(n log n) time complexity?
Figure 12.4: State Transfer Tree Structure for LIS, each path represents a
possible solution. Each arrow represents an move: find an element in the
following elements that’s larger than the current node.
ing node as shown in Fig 12.4, we can see subproblem which starts at
3, [3, 7, 101, 18] is recomputed multiple times, another one in the sub-
problem starts at 2, [2, 5, 3, 7, 101, 18]. Therefore if we can memoize
the subproblem’s solution, we can avoid recomputation in the DFS so-
lution. Unluckily, BFS can not be optimized using memoization even
if we figured out the overlap. Now, we give the code of DFS+Memo
and DFS.
1 # DFS + Memo
2 d e f r e c u r s i v e ( s e l f , nums ) :
3 """
4 : type nums : L i s t [ i n t ]
5 : rtype : int
6 """
7 # helper returns l o n g e s t i n c r e a s i n g subsequence f o r
nums [ i d x : ]
8 n = l e n ( nums )
9 i f not nums : r e t u r n 0
10 c a c h e = {}
11 def helper ( idx ) :
12 i f i d x == n−1: r e t u r n 1
13 i f idx in cache : return cache [ idx ]
14 # N o t i c e a t l e a s t t h e answer s h o u l d be 1
15 ans = 1
16 f o r i i n r a n g e ( i d x +1, n ) :
17 i f nums [ i d x ] < nums [ i ] :
18 ans = max( ans , 1 + h e l p e r ( i ) )
19 c a c h e [ i d x ] = ans
20 r e t u r n ans
21 r e t u r n max( h e l p e r ( i ) f o r i i n r a n g e ( n ) )
1 # BFS
2 d e f l e n g t h O f L I S ( s e l f , nums ) :
3 """
4 : type nums : L i s t [ i n t ]
5 : rtype : int
6 """
7 i f not nums :
8 return 0
9 l e n s = [ 1 ] ∗ l e n ( nums )
10 q = [ i f o r i i n r a n g e ( l e n ( nums ) ) ] # s t a r t pos can
be any number i n nums
11 #l e n s [ 0 ] = 1
12 while q :
13 new_q = s e t ( )
14 f o r idx in q :
15 # s e a r c h f o r number t h a t i s l a r g e r t h a t
current
16 f o r j i n r a n g e ( i d x +1 , l e n ( nums ) ) :
17 i f nums [ j ] > nums [ i d x ] :
18 l e n s [ j ] = max( l e n s [ j ] , l e n s [ i d x ]+1)
19 new_q . add ( j )
20 q = l i s t ( new_q )
206 12. DYNAMIC PROGRAMMING
21 r e t u r n max( l e n s )
10 max_before = 0
11 f o r j in range ( i ) :
12 i f nums [ i ] > nums [ j ] :
13 max_before = max( max_before , LIS [ j +1])
14 LIS [ i +1] = max_before+1
15 r e t u r n max( LIS )
1. State: Define what it means of each state, states the total/the maxi-
mum/minimum solution at position index; This requires us to know
how to divide problem into subproblems.
2. State Transfer Function: derive the function that how we can get cur-
rent state by using result from previous computed state. This requires
us to know how to construct the solution of a larger problem from
solved smaller problems.
4. Answer: which state or the max/min of all the state is the final result
needed.
Dos Dynamic programming fits for the optimizing the following problems
which are either exponential or polynomial complexity :
2. When the input dataset is a set while not an array or string or matrix,
90% chance we will not use DP.
2. Try to draw a tree or graph structure, and see if we can solve the
problem using BFS and DFS. This can help us identify the optimal
substructure and the overlapping.
3. After we identify that dynamic programming applies, we use the Sec 12.2.2
to implement the algorithm.
some programmers actually feel that the bottom-up version is more intuitive.
The decision on using which DP style is in your hand. To help you decide
which style that you should take when presented with a DP solution, we
present the trade-off comparison between top-down and bottom-up DP in
Table 12.6.
12 r e t u r n minSum
In the above solution, the state is a recursive tree, and the DFS traverse
all the elements in the tree. To reformulate this problem as dynamic pro-
gramming, if we use f [x][y] marks the minimum path sum start from (x, y),
then we have this relation f [x][y] = A[x][y] + min(f [x + 1][y], f [x + 1][y + 1],
which gives us a function T (n) = 2 ∗ T (n − 1). We still have O(2n ) time
complexity and still encounter LTE error.
1 d e f minimumTotal ( t r i a n g l e ) :
2 def divideConquer (x , y ) :
3 i f x == l e n ( t r i a n g l e ) :
4 return 0
5 r e t u r n t r i a n g l e [ x ] [ y]+min ( d i v i d e C o n q u e r ( x+1, y ) ,
d i v i d e C o n q u e r ( x+1, y+1) )
6 return divideConquer (0 , 0)
1. It saves the heap space from the implementation of the recursive func-
tion.
212 12. DYNAMIC PROGRAMMING
For the iterative, we have two ways: Bottom-up and top-down. This is
compared with your order to fill in the dynamic table. If we use our previous
defined relation function f [x][y] = A[x][y] + min(f [x + 1][y], f [x + 1][y + 1],
we need to know the result from the larger index so that we can fill in value
at the smaller index. Thus, we need to initialize the result for the largest
indices. And we reversely fill in the dynamic table, this is called top-down
method, from big index to small. Visually we propagate the information
from the end to the front. The final result
On the other side, if we fill in the table from small to larger index, we need
to rewrite the relation function to f [x][y] = A[x][y] + min(f [x − 1][y], f [x −
1][y − 1], this function feedforward the information from the beginning to
the end. So we need to initialize the result at (0,0), and the edge of the
triangle. following the increasing order, to get value for the larger index, it
is bottom-up method.
1 #bottom−up
2 from s y s import maxsize
3 d e f minimumTotal ( t r i a n g l e ) :
4 f = [ [ 0 f o r i i n r a n g e ( j +1) ] f o r j i n r a n g e ( l e n ( t r i a n g l e ) ) ]
# i n i t i a l i z e d to 0 f o r f ( )
5 n = len ( triangle )
6 #i n i t i a l i z e t h e f i r s t p o i n t , bottom
7 f [ 0 ] [ 0 ] = triangle [ 0 ] [ 0 ]
8 #i n i t i a l t h e l e f t c o l and t h e r i g h t c o l o f t h e t r i a n g l e
9 f o r x in range (1 , n) :
10 f [ x ] [ 0 ] = f [ x −1][0] + triangle [ x ] [ 0 ]
11 f [ x ] [ x ] = f [ x − 1 ] [ x −1] + t r i a n g l e [ x ] [ x ]
12 f o r x in range (1 , n) :
13 f o r y in range (1 , x ) :
14 f [ x ] [ y ] = t r i a n g l e [ x ] [ y ] + min ( f [ x − 1 ] [ y ] , f [ x − 1 ] [ y
−1])
15 r e t u r n min ( f [ − 1 ] )
Or we have top down and save the space by reusing triangle matrix. The
code is even simpler and we can have O(1) space complexity.
1 # top−down and no e x t r a s p a c e
2 d e f minimumTotal ( t r i a n g l e ) :
3 n = len ( triangle )
4 # from s m a l l i n d e x t o l a r g e i n d e x
5 f o r x i n r a n g e ( n−2 , −1, −1) :
6 f o r y i n r a n g e ( x , −1, −1) :
7 t r i a n g l e [ x ] [ y ] = t r i a n g l e [ x ] [ y ] + min ( t r i a n g l e [ x + 1 ] [
y ] , t r i a n g l e [ x + 1 ] [ y +1]) #g e t r e s u l t f o r l a r g e r s t a t e from
smaller state
8 return t r i a n g l e [ 0 ] [ 0 ]
Another way that we can get the sum between i, j in O(1) time with formula
sum(i, j) = sum(0, j) − sum(0, i) by using O(n) space to save the sum from
0 to current index.
solution: Divide and Conquer To further improve the efficiency, we use
divide and conquer, where we divide one array into two halves: the maxi-
mum subarray might located on the left size, or the right side, or some in
the left side and some in the right size, which crossed the bound. T (n) =
max(T (lef t), T (right), T (cross)), max is for merging and the T(cross) is
for the case that the potential subarray across the mid point. For the com-
plexity, T (n) = 2T (n/2) + n, if we use the master method, it would give us
O(nlgn). With this solution, we use O(lgn) space for the recursive function
stack space.
1 d e f maxSubArray ( s e l f , nums ) :
2 """
3 : type nums : L i s t [ i n t ]
4 : rtype : int
5 """
6 d e f getCrossMax ( low , mid , h i g h ) :
7 left_sum , right_sum =0 ,0
8 left_max , right_max = −maxint , −maxint
9 l e f t _ i , r i g h t _ j =−1,−1
10 f o r i i n xrange ( mid , low −1,−1) : #[ )
11 left_sum+=nums [ i ]
12 i f left_sum>left_max :
13 left_max= left_sum
14 left_i = i
15 f o r j i n xrange ( mid+1, h i g h +1) :
16 right_sum+=nums [ j ]
17 i f right_sum>right_max :
18 right_max= right_sum
19 right_j = j
20 r e t u r n ( l e f t _ i , r i g h t _ j , left_max+right_max )
21
22 d e f maxSubarray ( low , h i g h ) :
23 i f low==h i g h :
24 r e t u r n ( low , high , nums [ low ] )
25 mid = ( low+h i g h ) //2
26 r s l t =[]
27 #l e f t _ l o w , l e f t _ h i g h , left_sum = maxSubarray ( low , mid
) #[low , mid ]
28 r s l t . append ( maxSubarray ( low , mid ) ) #[ low , mid ]
29 #right_low , r i g h t _ h i g h , right_sum = maxSubarray ( mid+1 ,
h i g h ) #[mid+1 , h i g h ]
30 r s l t . append ( maxSubarray ( mid+1 , h i g h ) )
31 #cross_low , c r o s s _ h i g h , cross_sum = getCrossMax ( low ,
mid , h i g h )
32 r s l t . append ( getCrossMax ( low , mid , h i g h ) )
33 r e t u r n max( r s l t , key=lambda x : x [ 2 ] )
34 r e t u r n maxSubarray ( 0 , l e n ( nums ) −1) [ 2 ]
the maximum subarray value till j, the state till i we can get the result from
previous state i − 1, the value of current state depends on the larger one
between f [i − 1] plus the current element and the current element, which is
represented as f [i] = max(f [i − 1] + nums[i], nums[i]). This would gave us
O(n) time complexity and O(n + 1) space complexity. The initialization is
f [0] = 0, and the answer is max(f ).
1 from s y s import maxsize
2 d e f maximumSubarray ( nums ) :
3 i f not nums :
4 return 0
5 f = [− maxsize ] ∗ ( l e n ( nums ) +1)
6 f o r i , v i n enumerate ( nums ) :
7 f [ i +1] = max( f [ i ]+nums [ i ] , nums [ i ] ) #u s e f [ i +1] b e c a u s e
we have n+1 s p a c e
8 r e t u r n max( f )
However, here since we only need to track f [i] and f [i + 1], and keep
current maximum value, so that we do not need to use any space.
1 from s y s import maxsize
2 d e f maximumSubarray ( nums ) :
3 i f not nums :
4 return 0
5 f = 0
6 maxValue = −maxsize
7 f o r i , v i n enumerate ( nums ) :
8 f = max( f+nums [ i ] , nums [ i ] ) #u s e f [ i +1] b e c a u s e we have
n+1 s p a c e
9 maxValue = max( maxValue , f )
10 r e t u r n maxValue
The algorithm can also be easily modified to keep track of the starting
and ending indices of the maximum subarray (when max_so_far changes) as
216 12. DYNAMIC PROGRAMMING
well as the case where we want to allow zero-length subarrays (with implicit
sum 0) if all elements are negative.
Because of the way this algorithm uses optimal substructures (the max-
imum subarray ending at each position is calculated in a simple way from
a related but smaller and overlapping subproblem: the maximum subarray
ending at the previous position) this algorithm can be viewed as a sim-
ple/trivial example of dynamic programming.
Prefix Sum to get BCR convert this problem to best time to buy and sell
stock problem. [0, −2, −1, −4, 0, −1, 1, 2, −3, 1], which is to find the maxi-
mum benefit, => O(n), use prefix_sum, the difference is we set prefix_sum
to 0 when it is smaller than 0, O(n). Or we can try two pointers.
1 from s y s import maxint
2 d e f maxSubArray ( s e l f , nums ) :
3 """
4 : type nums : L i s t [ i n t ]
5 : rtype : int
6 """
7 max_so_far = −maxint − 1
8 prefix_sum= 0
9 f o r i i n r a n g e ( 0 , l e n ( nums ) ) :
10 prefix_sum+= nums [ i ]
11 i f ( max_so_far < prefix_sum ) :
12 max_so_far = prefix_sum
13
14 i f prefix_sum< 0 :
15 prefix_sum= 0
16 r e t u r n max_so_far
Solution: here we not only need to count all the solutions, we need to
record all the solutions. Before using dynamic prgramming, we can use DFS,
and we need a function to see if a splitted substring is palindrome or not.
The time complexity for this is T (n) = T (n−1)+T (n−2)+...+T (1)+O(n),
which gave out the complexity as O(3n ). This is also called backtracking
algorithm. The running time is 152 ms.
1 def partition ( s e l f , s ) :
2 """
3 : type s : s t r
12.3. PROBLEMS CAN BE OPTIMIZED USING DP 217
18 h e l p e r ( i +1 , path +[ s [ s t a r t : i + 1 ] ] , r e s )
19 helper (0 , [ ] , res )
20 return res
This is actually the example that if we want to print out all the solutions, we
need to use DFS and backtracking. It is hard to use dynamic programming
and save time.
Two-Choice State
801. Minimum Swaps To Make Sequences Increasing
1 We have two i n t e g e r s e q u e n c e s A and B o f t h e same non−z e r o
length .
2
3 We a r e a l l o w e d t o swap e l e m e n t s A[ i ] and B [ i ] . Note t h a t both
e l e m e n t s a r e i n t h e same i n d e x p o s i t i o n i n t h e i r r e s p e c t i v e
sequences .
4
5 At t h e end o f some number o f swaps , A and B a r e both s t r i c t l y
increasing . (A s e q u e n c e i s s t r i c t l y i n c r e a s i n g i f and o n l y
i f A [ 0 ] < A [ 1 ] < A [ 2 ] < . . . < A[A. l e n g t h − 1 ] . )
6
7 Given A and B, r e t u r n t h e minimum number o f swaps t o make both
sequences s t r i c t l y increasing . I t i s guaranteed that the
g i v e n i n p u t always makes i t p o s s i b l e .
8
9 Example :
10 Input : A = [ 1 , 3 , 5 , 4 ] , B = [ 1 , 2 , 3 , 7 ]
11 Output : 1
12 Explanation :
13 Swap A [ 3 ] and B [ 3 ] . Then t h e s e q u e n c e s a r e :
14 A = [ 1 , 3 , 5 , 7 ] and B = [ 1 , 2 , 3 , 4 ]
15 which a r e both s t r i c t l y i n c r e a s i n g .
16
17 Note :
18
19 A, B a r e a r r a y s with t h e same l e n g t h , and t h a t l e n g t h w i l l
be i n t h e r a n g e [ 1 , 1 0 0 0 ] .
20 A[ i ] , B [ i ] a r e i n t e g e r v a l u e s i n t h e r a n g e [ 0 , 2 0 0 0 ] .
12.3. PROBLEMS CAN BE OPTIMIZED USING DP 219
Simple DFS. The brute force solution is to generate all the valid sequence
and find the minimum swaps needed. Because each element can either be
swapped or not, thus make the time complexity O(2n ). If we need to swap
current index i is only dependent on four elements at two state, (A[i], B[i],
A[i-1], B[i-1]), at state i and i-1 respectively. At first, supposedly for each
path, we keep the last visited element a and b for element picked for A and
B respectively. Then
1 d e f minSwap ( s e l f , A, B) :
2 i f not A o r not B :
3 return 0
4
5 d e f d f s ( a , b , i ) : #t h e l a s t e l e m e n t o f t h e s t a t e
6 i f i == l e n (A) :
7 return 0
8 i f i == 0 :
9 # not swap
10 count = min ( d f s (A[ i ] , B [ i ] , i +1) , d f s (B [ i ] , A[ i ] , i
+1)+1)
11 r e t u r n count
12 count = s y s . maxsize
13
14 i f A[ i ]>a and B [ i ]>b : #not swap
15 count = min ( d f s (A[ i ] , B [ i ] , i +1) , count )
16 i f A[ i ]>b and B [ i ]>a :#swap
17 count = min ( d f s (B [ i ] , A[ i ] , i +1)+1, count )
18 r e t u r n count
19
20 return dfs ( [ ] , [ ] , 0)
DFS with single State Memo is not working. Now, to avoid overlap-
ping, [5,4], [3,7] because for the DFS there subproblem is in reversed order
compared with normal dynamic programming. Simply using the index to
identify the state will not work and end up with wrong answer.
DFS with muliple choiced memo. For this problem, it has two
potential choice, swap or keep. The right way is to distinguish different
state with additional variable. Here we use swapped to represent if the
current level we make the decision of swap or not.
1 d e f minSwap ( s e l f , A, B) :
2 i f not A o r not B :
3 return 0
4
5 d e f d f s ( a , b , i , memo, swapped ) : #t h e l a s t e l e m e n t o f t h e
state
6 i f i == l e n (A) :
7 return 0
8 i f ( swapped , i ) not i n memo :
9 i f i == 0 :
10 # not swap
11 memo [ ( swapped , i ) ] = min ( d f s (A[ i ] , B [ i ] , i +1,
memo, F a l s e ) , d f s (B [ i ] , A[ i ] , i +1, memo, True ) +1)
220 12. DYNAMIC PROGRAMMING
12 r e t u r n memo [ ( swapped , i ) ]
13 count = s y s . maxsize
14
15 i f A[ i ]>a and B [ i ]>b : #not swap
16 count = min ( count , d f s (A[ i ] , B [ i ] , i +1, memo,
False ) )
17 i f A[ i ]>b and B [ i ]>a : #swap
18 count = min ( count , d f s (B [ i ] , A[ i ] , i +1, memo,
True ) +1)
19 memo [ ( swapped , i ) ] = count
20
21 r e t u r n memo [ ( swapped , i ) ]
22
23 return dfs ( [ ] , [ ] , 0 , {} , False )
2. Use mathematical induction to find the constants and show that the
solution works.
T (n) ≤ 2(cbn/2clg(bn/2c)) + n
≤ cnlg(n/2) + n
≤ cnlgn − cnlg2 + n (12.3)
≤ cnlgn − cn + n
≤ cnlgn, ifc ≥ 1,
2. We can make a guess to prove the loose upper and lower bounds on
the recurrence and then reduce the range of uncertainty.
Also, for our guess, if we directly substitute we can not always prove the
induction. Consider the recurrence:
A reasonable and logical guess for this recurrenc is T (n) = O(n), so that we
obtain
12.5 Exercises
1. Coordinate Type 63. Unique Paths II (medium).
A robot is located at the top-left corner of a m x n grid (marked ’Start’
in the diagram below).
The robot can only move either down or right at any point in time. The
robot is trying to reach the bottom-right corner of the grid (marked
’Finish’ in the diagram below).
Now consider if some obstacles are added to the grids. How many
unique paths would there be?
An obstacle and empty space is marked as 1 and 0 respectively in the
grid.
Note: m and n will be at most 100.
Example 1:
1 Input :
2 [
3 [0 ,0 ,0] ,
4 [0 ,1 ,0] ,
5 [0 ,0 ,0]
6 ]
7 Output : 2
Explanation: There is one obstacle in the middle of the 3x3 grid above.
There are two ways to reach the bottom-right corner:
1 1 . Right −> Right −> Down −> Down
2 2 . Down −> Down −> Right −> Right
Sequence Type
1 d e f rob ( s e l f , nums ) :
2 """
3 : type nums : L i s t [ i n t ]
4 : rtype : int
5 """
6
7 i f not nums :
8 return 0
9 i f l e n ( nums ) ==1:
10 r e t u r n nums [ 0 ]
11 d e f r o b b e r 1 ( nums ) :
12 dp = [ 0 ] ∗ ( 2 )
13 dp [ 0 ] =0
14 dp [ 1 ] =nums [ 0 ] #i f l e n i s 1
15 f o r i i n r a n g e ( 2 , l e n ( nums ) +1) : #i f l e n g i s
2 . . . . , i n d e x i s i −1
16 dp [ i %2]=max( dp [ ( i −2)%2]+nums [ i −1] , dp [ ( i −1)
%2])
17 r e t u r n dp [ l e n ( nums ) %2]
18
19 r e t u r n max( r o b b e r 1 ( nums [ : − 1 ] ) , r o b b e r 1 ( nums [ 1 : ] ) )
9 r e t u r n min ( c o s t s [ 0 ] )
10
11 minCost = [ [ 0 f o r c o l i n r a n g e ( 3 ) ] f o r row i n r a n g e
( l e n ( c o s t s ) +1) ]
12 minCost [ 0 ] = [ 0 , 0 , 0 ]
13 minCost [ 1 ] = [ c o s t f o r c o s t i n c o s t s [ 0 ] ]
14 c o l o r S e t=s e t ( [ 1 , 2 , 0 ] )
15 f o r i i n r a n g e ( 2 , l e n ( c o s t s ) +1) :
16 f o r c in range (3) :
17 #p r e v i o u s c o l o r
18 p r e s = l i s t ( c o l o r S e t −s e t ( [ c ] ) )
19 print ( pres )
20 minCost [ i ] [ c ] = min ( [ minCost [ i − 1 ] [ pre_cor ]
f o r pre_cor i n p r e s ] )+c o s t s [ i − 1 ] [ c ]
21 r e t u r n min ( minCost [ − 1 ] )
There is a fence with n posts, each post can be painted with one of
the k colors.
You have to paint all the posts such that no more than two adjacent
fence posts have the same color.
Return the total number of ways you can paint the fence.
Note: n and k are non-negative integers. for three posts, the same
color, the first two need to be different
1 d e f numWays( s e l f , n , k ) :
2 """
3 : type n : i n t
4 : type k : i n t
5 : rtype : int
6 """
7 i f n==0 o r k==0:
8 return 0
9 i f n==1:
10 return k
11
12 count = [ [ 0 f o r c o l i n r a n g e ( k ) ] f o r row i n r a n g e ( n
+1) ]
13 same = k
14 d i f f = k ∗ ( k−1)
15 f o r i i n r a n g e ( 3 , n+1) :
16 pre_diff = d i f f
17 d i f f = ( same+ d i f f ) ∗ ( k−1)
18 same = p r e _ d i f f
19 r e t u r n ( same+ d i f f )
Explanation:
As shown below, there are 3 ways you can generate "rabbit" from S.
(The caret symbol ^ means the chosen letters)
1 rabbbit
2 ^^^^ ^^
226 12. DYNAMIC PROGRAMMING
3 rabbbit
4 ^^ ^^^^
5 rabbbit
6 ^^^ ^^^
Example 2:
1 Input : s 1 = " aabcc " , s 2 = " dbbca " , s 3 = " aadbbbaccc "
2 Output : f a l s e
Splitting Type DP
Example 2:
228 12. DYNAMIC PROGRAMMING
1 Input : [ −2 ,0 , −1]
2 Output : 0
3 E x p l a n a t i o n : The r e s u l t cannot be 2 , b e c a u s e [ −2 , −1] i s not
a subarray .
Example 2:
1 I nput : [ 1 , 2 , 3 , 4 , 5 ]
2 Output : 4
3 E x p l a n a t i o n : Buy on day 1 ( p r i c e = 1 ) and s e l l on day 5 (
p r i c e = 5 ) , p r o f i t = 5−1 = 4 .
4 Note t h a t you cannot buy on day 1 , buy on day
2 and s e l l them l a t e r , a s you a r e
5 e n g a g i n g m u l t i p l e t r a n s a c t i o n s a t t h e same
time . You must s e l l b e f o r e buying a g a i n .
230 12. DYNAMIC PROGRAMMING
Example 3:
1 Input : [ 7 , 6 , 4 , 3 , 1 ]
2 Output : 0
3 E x p l a n a t i o n : In t h i s c a s e , no t r a n s a c t i o n i s done , i . e . max
profit = 0.
1 I nput : [ 2 , 4 , 1 ] , k = 2
2 Output : 2
3 E x p l a n a t i o n : Buy on day 1 ( p r i c e = 2 ) and s e l l on day 2 (
p r i c e = 4 ) , p r o f i t = 4−2 = 2 .
Example 2:
1 I nput : [ 3 , 2 , 6 , 5 , 0 , 3 ] , k = 2
2 Output : 7
3 E x p l a n a t i o n : Buy on day 2 ( p r i c e = 2 ) and s e l l on day 3 (
p r i c e = 6 ) , p r o f i t = 6−2 = 4 .
4 Then buy on day 5 ( p r i c e = 0 ) and s e l l on day
6 ( p r i c e = 3 ) , p r o f i t = 3−0 = 3 .
Note:
1 1 <= k <= n <= 1 0 , 0 0 0 .
2 Elements o f t h e g i v e n a r r a y w i l l be i n r a n g e [ − 1 0 , 0 0 0 ,
10 ,000].
3 The answer with t h e c a l c u l a t i o n e r r o r l e s s than 10−5
w i l l be a c c e p t e d .
Challenge
O(n x m) memory is acceptable, can you do it in O(m) memory? Note
Hint: Similar to the backpack I, difference is dp[j] we want the value
maximum, not to maximize the volume. So we just replace f[i-A[i]]+A[i]
with f[i-A[i]]+V[i].
232 12. DYNAMIC PROGRAMMING
12.6 Summary
Steps of Solving Dynamic Programming Problems
We read through the problems, most of them are using array or string
data structures. We search for key words: ”min/max number", ”Yes/No" in
”subsequence/" type of problems. After this process, we made sure that we
are going to solve this problem with dynamic programming. Then, we use
the following steps to solve it:
1. .
2. New storage( a list) f to store the answer, where fi denotes the answer
for the array that starts from 0 and end with i. (Typically, one extra
space is needed) This steps implicitly tells us the way we do divide
and conquer: we first start with dividing the sequence S into S(1,n)
and a0 . We reason the relation between these elements.
4. We initialize the storage and we figure out where in the storage is the
final answer (f[-1], max(f), min(f), f[0]).
Greedy Algorithms
From the catalog: Greedy Algorithm listed on LeetCode, 90% of the prob-
lems are medium and hard difficulty. Luckily, we can see the frequency
is extremely low for each single problem available on LeetCode. Even the
most popular coding instruction book Cracking the Coding Interview does
not even mention Greedy Algorithm. So, we do not need to worry about
failing the interview due to this type of questions, because it is the most un-
likely type of questions in the interview. Probably your intervivewer would
not like to try it out himself or herself.
But still, to complete the blueprint of this book, we are still going to
learn the concepts of greedy algorithms, and try to solve the problems on
the LeetCode as practice.
235
236 13. GREEDY ALGORITHMS
3. Show that if we make the greedy choice, then only one subproblem
remains.
2.
Two key ingredients that can help us identify if a greedy algorithm will solve
a specific problem: Greedy-choice property and optimal substructure.
Figure 13.1: Screenshot of Greedy Catalog, showing the frequency and dif-
ficulty of this type in the real coding interview
238 13. GREEDY ALGORITHMS
Part VI
239
14
241
242 14. SORTING AND SELECTION ALGORITHMS
next book need the data to be in sorted order to carry on the next
step.
• O(n log n) (Section 14.2) Sorting: merge sort, quick sort, and Quick
Select;
14.1. O(N 2 ) SORTING 243
Implementation Using extra space to save the sorted sublist is the easy
start. We use the first for loop to indicate the item to be inserted, and
the second for loop to find the right position and do insertion in the sorted
sublist.
1 def insertionSort (a) :
2 i f not a o r l e n ( a ) == 1 :
3 return a
4 n = len (a)
5 s l = [ a [ 0 ] ] # sorted l i s t
6 f o r i i n r a n g e ( 1 , n ) : # i t e m s t o be i n s e r t e d i n t o t h e s o r t e d
7 j = 0
8 while j < len ( s l ) :
9 if a[ i ] > sl [ j ]:
10 j += 1
11 else :
12 sl . insert (j , a[ i ])
13 break
14 i f j == l e n ( s l ) : # not i n s e r t e d y e t
15 sl . insert (j , a[ i ])
16 return s l
4 n = len (a)
5 f o r i i n r a n g e ( 1 , n ) : # i t e m s t o be i n s e r t e d i n t o t h e s o r t e d
6 t = a[ i ]
7 j = i − 1
8 w h i l e j >= 0 and t < a [ j ] : # keep comparing i f t a r g e t i s
s t i l l smaller
9 a [ j +1] = a [ j ] # s h i f t c u r r e n t item backward
10 j −= 1
11 a [ j +1] = t # a [ j ] <= t , i n s e r t t a t t h e l o c a t i o n j +1
12 return a
One Pass Given an array of size n, in a single pass, there are n − 1 pairs
waiting for comparison and with potential n − 1 times of swap operations.
For example, when the array is: [9, 10, 2, 8, 9, 3, 7], to sort them in ascending
order. When comparing a pair (Ai , Ai+1 ), if Ai > Ai+1 , we swap these two
items. One pass for the bubble sort is shown in Fig. 14.2, and we can clearly
see after one pass, the largest item will be in place. This is what “bubble”
246 14. SORTING AND SELECTION ALGORITHMS
means in the name that each pass, the largest item in the valid window
bubble up to the end of the array.
Next Pass Therefore, in the next pass, the last item will no longer needed
to be compared. For pass i, it places the current i-th largest items in position
in range of [n − i − 1, n). the last i items will be sorted. We say, in the first
pass, where i = 0, the valid window is [0, n), and to be generalize the valid
window for i-th pass is [0, n − i).
In the code, we use a[j] > a[j + 1], because when there the pair has equal
values, we do not need to swap them. The advantage of doing so is (1) to
save unnecessary swaps and (2) keep the original order of items with same
keys. This makes bubble sort a stable sort. As we see in bubble sort, there
is no extra space needed to sort, this makes it in-place sort.
The above implementation runs O(n ) even if the array is sorted. We can
2
optimize the inner for loop by stopping the whole program if no swap is
detected in a pass. This can achieve better solution as good ad O(n) if the
input is nearly sorted.
Selection Sort
In the bubble sort, each pass we get the largest element in the valid window
in place by a series of swapping operations. Selection sort makes a slight
optimization through selecting the largest item in the current valid window
and swap it directly with the item at its corresponding right position. This
avoids the constantly swapping operations in the bubble sort. Each pass we
14.1. O(N 2 ) SORTING 247
find the largest element and switch it with the last element. Then the next
pass has one less element to loop through. The process is shown in Fig 14.3.
Like bubble sort, selection sort is in-place. Given an array [9, 10, 2, 8, 9,
3, 9], there exists equal keys 9. At the first pass, the very last 9 is swapped
to the position 1 with key 10, and it becomes the second among its equals.
Unlike bubble sort, this mechanism makes selection sort unstable.
In this section, we will introduce the most general and commonly used
sorting algorithms that has O(n log n) that matches our approved lower
bound. Merge Sort and Quick Sort both utilize the Divide-and-conquer
method. Heap Sort on the other hand uses the max/min heap data struc-
tures we have learned before to get the same upper bound performance.
the last steps and merge the two lists together and return the merged result
to the last function call. Merging is the process of taking two smaller sorted
lists and combining them together into a single, sorted, new list. Figure 14.4
shows our familiar example list as it is being split by mergeSort. Figure 14.5
shows the simple lists, now sorted, as they are merged back together. The
Python code:
1 d e f mergeSort ( nums , r e v e r s e = F a l s e ) :
2 i f not nums o r l e n ( nums ) == 1 :
3 r e t u r n nums
4 # d i v i d e i n t o two h a l v e s
5 mid = l e n ( nums ) // 2
6 l e f t = mergeSort ( nums [ : mid ] , r e v e r s e )
7 r i g h t = mergeSort ( nums [ mid : ] , r e v e r s e )
250 14. SORTING AND SELECTION ALGORITHMS
8
9 # merge
10 tmp = [ ]
11 i , j = 0, 0
12 w h i l e i < l e n ( l e f t ) and j < l e n ( r i g h t ) :
13 i f compare ( l e f t [ i ] , r i g h t [ j ] , r e v e r s e ) :
14 tmp . append ( l e f t [ i ] )
15 i += 1
16 else :
17 tmp . append ( r i g h t [ j ] )
18 j += 1
19 # d e a l with t h e l e f t o v e r e l e m e n t s
20 while i < len ( l e f t ) :
21 tmp . append ( l e f t [ i ] )
22 i += 1
23 while j < len ( right ) :
24 tmp . append ( r i g h t [ j ] )
25 j += 1
26 r e t u r n tmp
As we can see from the code and the merge sort process, each time we divide
the list, we made a copy of both halves: nums[:mid] and nums[mid:], and
when we are merging the list, we use a temporary space to save the merged
result and return it to its last recursive call, thus merge sort is not in-place,
and it takes extra space which is O(n) to implement.
Because each divide we need to take O(n) time to merge the two lists
back to a list. The complexity function can be deducted as follows:
14.2.2 HeapSort
Implementing heapsort using the heapq:
1 >>> d e f h e a p s o r t ( i t e r a b l e ) :
2 ... h = []
3 ... f o r value in i t e r a b l e :
4 ... heappush ( h , v a l u e )
5 ... r e t u r n [ heappop ( h ) f o r i i n r a n g e ( l e n ( h ) ) ]
6 ...
7 >>> h e a p s o r t ( [ 1 , 3 , 5 , 7 , 9 , 2 , 4 , 6 , 8 , 0 ] )
8 [0 , 1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 9]
is possible that the list may not be divided in half. When this happens, we
will see that performance is diminished.
A quick sort first selects a value, which is called the pivot value. Although
there are many different ways to choose the pivot value, we will simply
use the first item in the list. The role of the pivot value is to assist with
splitting the list. The actual position where the pivot value belongs in the
final sorted list, commonly called the split point, will be used to divide the
list for subsequent calls to the quick sort.
Figure ?? shows that 54 will serve as our first pivot value. Since we have
looked at this example a few times already, we know that 54 will eventually
end up in the position currently holding 31. The partition process will
happen next. It will find the split point and at the same time move other
items to the appropriate side of the list, either less than or greater than the
pivot value. Python code:
1 def quickSort ( a l i s t ) :
2 q u i c k S o r t H e l p e r ( a l i s t , 0 , l e n ( a l i s t ) −1)
3
4 def quickSortHelper ( a l i s t , f i r s t , l a s t ) :
5 i f f i r s t <l a s t :
6
7 splitpoint = partition ( alist , first , last )
8
9 q u i c k S o r t H e l p e r ( a l i s t , f i r s t , s p l i t p o i n t −1)
10 q u i c k S o r t H e l p e r ( a l i s t , s p l i t p o i n t +1, l a s t )
11
12 a l i s t = [54 ,26 ,93 ,17 ,77 ,31 ,44 ,55 ,20 ,100]
13 quickSort ( a l i s t )
14 print ( a l i s t )
Hoare Partition
In Hoare partition, we first pick a pivot, either randomly or left or right end,
then we put two indexs at both side of the array, we compared each side’s
element, if a condition is not satisfied, then stop, we do a switch.
1 d e f H o a r e P a r t i t i o n ( a r r , low , h i g h ) :
2 p i v o t=a r r [ h i g h ]
3 i=low
4 j=high −1
5 w h i l e True :
6 w h i l e a r r [ i ] <= p i v o t and i < j :
7 i += 1
8 w h i l e a r r [ j ] >= p i v o t and i < j :
9 j −= 1
10 i f i == j :
11 i f a r r [ i ] <= p i v o t :
12 i += 1
13 arr [ i ] , arr [ high ] = arr [ high ] , arr [ i ]
14 return i
252 14. SORTING AND SELECTION ALGORITHMS
15 else :
16 arr [ i ] , arr [ j ] = arr [ j ] , arr [ i ]
Lomuto’s Partition
For lomuto parition, we first pick the pivot from one side, and the other side
is going to be the index for swapping, use index j. Then we iterative index
i from j to the pivot side, if the element does not belong to the right side (if
pivot starts from left, then the swapping from the last element reversely go to
the first element) or left side(if the pivot starts from the rightmost element,
then the swapping from the first element to the second last element on the
right side), we do a swap. At last, we swap the last j with the pivot position.
14.2. O(N LOG N ) SORTING 253
1 def lumo_partition ( a l i s t , f i r s t , l a s t ) :
2 pivotvalue = a l i s t [ f i r s t ]
3 j = l a s t #p l a c e f o r swapping , where j i s t h e s m a l l e r e l e m e n t
on t h e r i g h t s i d e
4 f o r i i n r a n g e ( l a s t , f i r s t , −1) :
5 i f a l i s t [ i ] >= p i v o t v a l u e :
6 alist [ i ] , alist [ j ] = alist [ j ] , alist [ i ]
7 j −=1 #swap p l a c e move
8 #swap t h e p i v o t with t h e l a s t e l e m e n t
9 alist [ j ] , alist [ first ] = alist [ first ] , alist [ j ]
10 return j
Quick Select
Quickselect is a selection algorithm to find the k-th smallest element in an
unordered list. It is related to the quick sort sorting algorithm.
The algorithm is similar to QuickSort. The difference is, instead of
recurring for both sides (after finding pivot), it recurs only for the part
that contains the k-th smallest element. The logic is simple, if index of
partitioned element is more than k, then we recur for left part. If index
is same as k, we have found the k-th smallest element and we return. If
index is less than k, then we recur for right part. This reduces the expected
complexity from O(n log n) to O(n), with a worst case of O(n2 ).
1 def quickSelectHelper ( a l i s t , f i r s t , last , k) :
2 i f f i r s t <l a s t :
3 s p l i t p o i n t = lumo_partition ( a l i s t , f i r s t , l a s t )
4
5 i f k == s p l i t p o i n t :
6 return a l i s t [ k ]
7 e l i f k < s p l i t p o i n t : # f i n d them on t h e l e f t s i d e
8 r e t u r n q u i c k S e l e c t H e l p e r ( a l i s t , f i r s t , s p l i t p o i n t −1,
k)
9 e l s e : # f i n d i t on t h e r i g h t s i d e
10 r e t u r n q u i c k S e l e c t H e l p e r ( a l i s t , s p l i t p o i n t +1 , l a s t , k )
11 return a l i s t [ f i r s t ]
12
254 14. SORTING AND SELECTION ALGORITHMS
13 def quickSelect ( a l i s t , k) :
14 i f k > len ( a l i s t ) :
15 r e t u r n None
16 r e t u r n q u i c k S e l e c t H e l p e r ( a l i s t , 0 , l e n ( a l i s t ) −1, k−1)
Observing the count array after this step showing in the below table, key 7
will be the order (C6 , C7 ], which is (6, 7], that is only the 7-th position at
index 6. For key 2, it is in the range of (2, 4], that is 3-th and 4-th position.
Index : 0 1 2 3 4 5 6 7 8 9
Count : 0 2 4 4 5 6 6 7 7 7
In order to correct this and be stable, we loop over each key in the input
data in reverse order instead.
Properties
256 14. SORTING AND SELECTION ALGORITHMS
Python code:
To implement the code with Python, we need to know how to get the each
digit from the least significant to the most significant digit. We know if we
divide an integer by 10, and the reminder would be the least significant digit.
Then we divide this integer by 10, and get the reminder of this value of 10,
then we would have the digit for the least significant digit. The Python code
is shown as follows:
1 a = 178
2 w h i l e a >0:
3 d i g i t = a%10
4 a /= 10
22 f o r i in range (1 ,10) :
23 count [ i ] += count [ i −1]
24
25 # B u i l d t h e output a r r a y
26 i = n−1
27 w h i l e i >=0:
28 i n d e x = ( a r r [ i ] / exp1 )
29 output [ count [ ( i n d e x )%10 ] − 1 ] = a r r [ i ]
30 count [ ( i n d e x )%10 ] −= 1
31 i −= 1
32
33 # Copying t h e output a r r a y t o a r r [ ] ,
34 # s o t h a t a r r now c o n t a i n s s o r t e d numbers
35 i = 0
36 f o r i in range (0 , len ( arr ) ) :
37 a r r [ i ] = output [ i ]
38
39 # Method t o do Radix S o r t
40 def radixSort ( arr ) :
41
42 # Find t h e maximum number t o know number o f d i g i t s
43 max1 = max( a r r )
44
45 # Do c o u n t i n g s o r t f o r e v e r y d i g i t . Note t h a t i n s t e a d
46 # o f p a s s i n g d i g i t number , exp i s p a s s e d . exp i s 10^ i
47 # where i i s c u r r e n t d i g i t number
48 exp = 1
49 w h i l e max1/ exp > 0 :
50 c o u n t i n g S o r t ( a r r , exp )
51 exp ∗= 10
52
53 # D r i v e r code t o t e s t above
54 arr = [ 170 , 45 , 75 , 90 , 802 , 24 , 2 , 66]
55 radixSort ( arr )
56
57 f o r i in range ( len ( arr ) ) :
58 print ( arr [ i ] ) ,
59
60 # This code i s c o n t r i b u t e d by Mohit Kumra
at the first bit or digit of a number (or first letter of a string) to determine
which subtree the value belongs in. You then repeat the procedure recur-
sively using the next character or digit to determine which of the subtree’s
children it belongs in, and so on.
Use Counting Sort Counting sort is one of the most efficient sorting
algorithm for lexicographical ordered sorting.
1 def countSort ( arr ) :
2
3 # The output c h a r a c t e r a r r a y t h a t w i l l have s o r t e d a r r
4 output = [ 0 f o r i i n r a n g e ( 2 5 6 ) ]
5
6 # C r e a t e a count a r r a y t o s t o r e count o f i n i d i v i d u l
7 # c h a r a c t e r s and i n i t i a l i z e count a r r a y a s 0
8 count = [ 0 f o r i i n r a n g e ( 2 5 6 ) ]
9
10 # For s t o r i n g t h e r e s u l t i n g answer s i n c e t h e
11 # s t r i n g i s immutable
12 ans = [ " " f o r _ i n a r r ]
13
14 # S t o r e count o f each c h a r a c t e r
15 for i in arr :
16 count [ ord ( i ) ] += 1
17
18 # Change count [ i ] s o t h a t count [ i ] now c o n t a i n s a c t u a l
19 # p o s i t i o n o f t h i s c h a r a c t e r i n output a r r a y
20 f o r i in range (256) :
21 count [ i ] += count [ i −1]
22
23 # B u i l d t h e output c h a r a c t e r a r r a y
24 f o r i in range ( len ( arr ) ) :
25 output [ count [ ord ( a r r [ i ] ) ] −1] = a r r [ i ]
26 count [ ord ( a r r [ i ] ) ] −= 1
27
260 14. SORTING AND SELECTION ALGORITHMS
• Parameter key: the value of key will be any function that allows to
sort on the basis of the value returned from this function.
The difference of these two methods are: sort() basically works with the
list itself. It modifies the original list in place. The return value is N one;
sorted() works on any iterable that may include list, string, tuple, dict and
so on. It returns another list and doesn’t modify the original input.
Let us first see some examples, using them and then focusing on how we
can customize the comparison function through “key”.
14.6. PYTHON BUILT-IN SORT 261
Basic Examples
First, try sort a given list of integers/strings in-place:
1 l s t = [4 , 5 , 8 , 1 , 2 , 7]
2 l s t . sort ()
3 print ( l s t )
4 # output
5 # [1 , 2 , 4 , 5 , 7 , 8]
6 l s t . s o r t ( r e v e r s e=True )
7 # [8 , 7 , 5 , 4 , 2 , 1]
Note: For lists, list.sort() is faster than sorted() because it doesn’t have to
create a copy. For any other iterable, we have no choice but to apply sorted()
instead.
We want to compare them only with the first element in each tuple.
1 d e f getKey ( item ) :
2 r e t u r n item [ 0 ]
3 s o r t e d ( l s t , key = getKey )
4 #[ ( 1 , 8 , 2 ) , ( 1 , 7 , 1 0 ) , ( 1 , 7 , 1 ) , ( 3 , 2 , 9 ) , ( 6 , 3 , 1 0 ) , ( 1 1 ,
1 , 5) , (32 , 18 , 9) ]
Therefore, the comparison treates tuples such as (1, 8, 2), (1, 7, 10), (1, 7,
1) has the same value, and the sorted order following their original order in
the lst. For simplicity, the same thing can be done with lambda function.
1 s o r t e d ( l s t , key = lambda x : x [ 0 ] )
This can be translated like this: for each element (x) in mylist, return index
0 of that element, then sort all of the elements of the original list ’mylist’
by the sorted order of the list calculated by the lambda function.
262 14. SORTING AND SELECTION ALGORITHMS
1 s t u d e n t _ o b j e c t s = [ Student ( ' john ' , 'A ' , 1 5 ) , Student ( ' j a n e ' , 'B '
, 1 2 ) , Student ( ' dave ' , 'B ' , 1 0 ) ]
2 s o r t e d ( s t u d e n t _ o b j e c t s , key=lambda x : x . age ) # s o r t by age
3 [ ( ' dave ' , 'B ' , 1 0 ) , ( ' j a n e ' , 'B ' , 1 2 ) , ( ' john ' , 'A ' , 1 5 ) ]
The operator module functions allow multiple levels of sorting. For example,
to sort by grade then by age:
1 >>> s o r t e d ( s t u d e n t _ t u p l e s , key=i t e m g e t t e r ( 1 , 2 ) )
2 [ ( ' john ' , 'A ' , 1 5 ) , ( ' dave ' , 'B ' , 1 0 ) , ( ' j a n e ' , 'B ' , 1 2 ) ]
3
4 >>> s o r t e d ( s t u d e n t _ o b j e c t s , key=a t t r g e t t e r ( ' g r a d e ' , ' age ' ) )
5 [ ( ' john ' , 'A ' , 1 5 ) , ( ' dave ' , 'B ' , 1 0 ) , ( ' j a n e ' , 'B ' , 1 2 ) ]
1
https://www.toptal.com/developers/sorting-algorithms
264 14. SORTING AND SELECTION ALGORITHMS
Example 2 :
Input : −1−>5−>3−>4−>0
Output : −1−>0−>3−>4−>5
Example 2 :
Input : [ [ 1 , 4 ] , [ 4 , 5 ] ]
Output : [ [ 1 , 5 ] ]
E x p l a n a t i o n : I n t e r v a l s [ 1 , 4 ] and [ 4 , 5 ] a r e c o n s i d e r e d
overlapping .
14.3 Valid Anagram (242, easy). Given two strings s and t , write a
function to determine if t is an anagram of s.
Example 1 :
Input : s = " anagram " , t = " nagaram "
Output : t r u e
Example 2 :
Input : s = " r a t " , t = " c a r "
Output : f a l s e
Note: You may assume the string contains only lowercase alphabets.
14.8. LEETCODE PROBLEMS 265
Follow up: What if the inputs contain unicode characters? How would
you adapt your solution to such case?
14.5 Sort Colors (leetcode: 75). Given an array with n objects colored
red, white or blue, sort them so that objects of the same color are
adjacent, with the colors in the order red, white and blue. Here, we
will use the integers 0, 1, and 2 to represent the color red, white, and
blue respectively. Note: You are not suppose to use the library’s sort
function for this problem.
14.6 148. Sort List (sort linked list using merge sort or quick
sort).
Solutions
28 break
29 i f not bAdd :
30 #no o v e r l a p , push t o t h e heap
31 heappush ( h , i )
32 return h
3. Solution: there could have so many ways to do it, the most easy one
is to sort the letters in each string and see if it is the same. Or we can
have an array of 26, and save the count of each letter, and check each
letter in the other one string.
1 d e f isAnagram ( s e l f , s , t ) :
2 """
3 : type s : s t r
4 : type t : s t r
5 : rtype : bool
6 """
7 r e t u r n ' ' . j o i n ( s o r t e d ( l i s t ( s ) ) ) == ' ' . j o i n ( s o r t e d (
list (t)))
For the follow up, use a hash table instead of a fixed size counter.
Imagine allocating a large size array to fit the entire range of unicode
characters, which could go up to more than 1 million. A hash table is
a more generic solution and could adapt to any range of characters.
Bit Manipulation
x « y Returns x with the bits shifted to the left by y places (and new bits
on the right-hand-side are zeros). This is the same as multiplying x by 2y .
x » y Returns x with the bits shifted to the right by y places. This is the
same as dividing x by 2y , same result as the // operator. This right shift is
also called arithmetic right shift, it fills in the new bits with the value of the
sign bit.
271
272 15. BIT MANIPULATION
x & y "Bitwise and". Each bit of the output is 1 if the corresponding bit
of x AND of y is 1, otherwise it’s 0. It has the following property:
1 # keep 1 o r 0 t h e same a s o r i g i n a l
2 1 & 1 = 1
3 0 & 1 = 0
4 # s e t t o 0 with & 0
5 1 & 0 = 0
6 0 & 0 = 0
x ∧ y "Bitwise exclusive or". Each bit of the output is the same as the
corresponding bit in x if that bit in y is 0, and it’s the complement of the
bit in x if that bit in y is 1. It has the following basic properties:
1 # t o g g l e 1 o r 0 with ^ 1
2 1 ^ 1 = 0
3 0 ^ 1 = 1
4
5 # keep 1 o r 0 with ^ 0
6 1 ^ 0 = 1
7 0 ^ 0 = 0
Logical right shift The logical right shift is different to the above right
shift after shifting it puts a 0 in the most significant bit. It is indicated with
a >>> operator n Java. However, in Python, there is no such operator, but
we can implement one easily using bitstring module padding with zeros
using >>= operator.
1 >>> a = BitArray ( i n t =−1000, l e n g t h =32)
2 >>> a . i n t
3 −1000
4 >>> a >>= 3
5 >>> a . i n t
6 536870787
However, bin() doesn’t return binary bits that applies the two’s complement
rule. For example, for the negative value:
1 a1 = b i n ( −88)
2 # output
3 # −0b1011000
int(x, base = 10) The int() method takes either a string x to return an
integer with its corresponding base. The common base are: 2, 10, 16 (hex).
1 b = i n t ( ' 01011000 ' , 2 )
2 c = i n t ( ' 88 ' , 1 0 )
3 print (b , c )
4 # output
5 # 88 88
chr() The chr() method takes a single parameter of integer and return a
character (a string) whose Unicode code point is the integer. If the integer
i is outside the range, ValueError will be raised.
1 d = chr (88)
2 print (d)
3 # output
4 # X
274 15. BIT MANIPULATION
ord() The ord() method takes a string representing one Unicode character
and return an integer representing the Unicode code point of that character.
1 e = ord ( ' a ' )
2 print ( e )
3 # output
4 # 97
i=0
This is helpful if we just need two’s complement result instead of getting the
binary representation.
276 15. BIT MANIPULATION
Get ith Bit In order to do this, we use the property of AND operator
either 0 or 1 and with 1, the output is the same as original, while if it is and
with 0, they others are set with 0s.
1 # f o r n b i t , i i n r a n g e [ 0 , n−1]
2 def get_bit (x , i ) :
3 mask = 1 << i
4 i f x & mask :
5 return 1
6 return 0
7 print ( get_bit (5 ,1) )
8 # output
9 # 0
Else, we can use left shift by i on x, and use AND with a single 1.
1 def get_bit2 (x , i ) :
2 r e t u r n x >> i & 1
3 print ( get_bit2 (5 ,1) )
4 # output
5 # 0
Toggle ith Bit Toggling means to turn bit to 1 if it was 0 and to turn it to
0 if it was one. We will be using ’XOR’ operator here due to its properties.
1 x = x ^ mask
Clear Bits In some cases, we need to clear a range of bits and set them
to 0, our base mask need to put 1s at all those positions, Before we solve
this problem, we need to know a property of binary subtraction. Check if
you can find out the property in the examples below,
15.4. USEFUL COMBINED BIT OPERATIONS 277
The property is, the difference between a binary number n and 1 is all
the bits on the right of the rightmost 1 are flipped including the rightmost
1. Using this amazing property, we can create our mask as:
1 # b a s e mask
2 i = 5
3 mask = 1 << i
4 mask = mask −1
5 p r i n t ( b i n ( mask ) )
6 # output
7 # 0 b11111
With this base mask, we can clear bits: (1) All bits from the most significant
bit till i (leftmost till ith bit) by using the above mask. (2) All bits from
the lest significant bit to the ith bit by using ∼ mask as mask. The Python
code is as follows:
1 # i i −1 i −2 . . . 2 1 0 , keep t h e s e p o s i t i o n s
2 def c l e a r _ b i t s _ l e f t _ r i g h t ( val , i ) :
3 p r i n t ( ' val ' , bin ( val ) )
4 mask = ( 1 << i ) −1
5 p r i n t ( ' mask ' , b i n ( mask ) )
6 r e t u r n b i n ( v a l & ( mask ) )
1 # i i −1 i −2 . . . 2 1 0 , e r a s e t h e s e p o s i t i o n s
2 def c l e a r _ b i t s _ r i g h t _ l e f t ( val , i ) :
3 p r i n t ( ' val ' , bin ( val ) )
4 mask = ( 1 << i ) −1
5 p r i n t ( ' mask ' , b i n (~ mask ) )
6 r e t u r n b i n ( v a l & (~ mask ) )
Get the lowest set bit Suppose we are given ’0010,1100’, we need to get
the lowest set bit and return ’0000,0100’. And for 1100, we get 0100. If we
try to do an AND between 5 and its two’s complement as shown in Eq. 15.2
and 15.4, we would see only the right most 1 bit is kept and all the others
are cleared to 0. This can be done using expression x&(−x), −x is the two’s
complement of x.
278 15. BIT MANIPULATION
Clear the lowest set bit In many situations we want to strip off the
lowest set bit for example in Binary Indexed tree data structure, counting
number of set bit in a number. We use the following operations:
1 def strip_last_set_bit ( val ) :
2 p r i n t ( bin ( val ) )
3 return bin ( val & ( val − 1) )
4 print ( strip_last_set_bit (5) )
5 # output
6 # 0 b101
7 # 0 b100
15.5 Applications
Recording States Some algorithms like Combination, Permutation, Graph
Traversal require us to record states of the input array. Instead of using an
array of the same size, we can use a single integer, each bit’s location indi-
cates the state of one element with same index in the array. For example,
we want to record the state of an array with length 8. We can do it like
follows:
1 used = 0
2 f o r i in range (8) :
3 i f used &(1<< i ) : # check s t a t e a t i
4 continue
5 used = used | (1<< i ) # s e t s t a t e a t i used
6 p r i n t ( b i n ( used ) )
I nput : [ 2 , 2 , 1 ]
Output : 1
Example 2 :
I nput : [ 4 , 1 , 2 , 1 , 2 ]
Output : 4
Output : 28
E x p l a n a t i o n : The maximum r e s u l t i s 5 \^ 25 = 2 8 .
Solution 1: Build the Max bit by bit. First, let’s convert these
integers into binary representation by hand.
3 0000 , 0011
10 0000 , 1011
5 0000 , 0101
25 0001 , 1001
2 0000 , 0010
8 0000 , 1000
If we only look at the highest position i where there is one one and
all others zero. Then we know the maximum XOR m has 1 at that
bit. Now, we look at two bits: i, i-1. The possible maximum XOR
for this is append 0 or 1 at the end of m, we have possible max 11,
because for XOR, if we do XOR of m with others, mXORa = b, if b
exists in these possible two sets, then max is possible and it become
m << 1 + 1. We can carry on this process, the following process is
showed as follows: answer1̂ is the possible max,
15.5. APPLICATIONS 281
1 d e f findMaximumXOR ( s e l f , nums ) :
2 """
3 : type nums : L i s t [ i n t ]
4 : rtype : int
5 """
6 answer = 0
7 f o r i in range (32) [ : : − 1 ] :
8 answer <<= 1 # m u l t i p l e i t by two
9 p r e f i x e s = {num >> i f o r num i n nums} # s h i f t r i g h t
f o r n , d i v i d e /2^ i , g e t t h e f i r s t (32− i ) b i t s
10 answer += any ( ( answer +1) ^ p i n p r e f i x e s f o r p i n
prefixes )
11 r e t u r n answer
With Mask
I nput : 00000010100101000001111010011100
Output : 00111001011110000010100101000000
E x p l a n a t i o n : The i n p u t b i n a r y s t r i n g
00000010100101000001111010011100 r e p r e s e n t s t h e u n s i g n e d
i n t e g e r 4 3 2 6 1 5 9 6 , s o r e t u r n 964176192 which i t s b i n a r y
r e p r e s e n t a t i o n i s 00111001011110000010100101000000.
Example 2 :
282 15. BIT MANIPULATION
Input : 11111111111111111111111111111101
Output : 10111111111111111111111111111111
E x p l a n a t i o n : The i n p u t b i n a r y s t r i n g
11111111111111111111111111111101 r e p r e s e n t s t h e u n s i g n e d
i n t e g e r 4 2 9 4 9 6 7 2 9 3 , s o r e t u r n 3221225471 which i t s
binary representation i s
10101111110010110010011101101001.
Solution: Get Bit and Set bit with mask. We first get bits from
the most significant position to the least significant position. And get
the bit at that position with mask, and set the bit in our ’ans’ with a
mask indicates the position of (31-i):
1 # @param n , an i n t e g e r
2 # @return an i n t e g e r
3 def reverseBits ( s e l f , n) :
4 ans = 0
5 f o r i i n r a n g e ( 3 2 ) [ : : − 1 ] : #from h i g h t o low
6 mask = 1 << i
7 set_mask = 1 << (31− i )
8 i f ( mask & n ) != 0 : #g e t b i t
9 #s e t b i t
10 ans |= set_mask
11 r e t u r n ans
Input : [ 5 , 7 ]
Output : 4
Example 2 :
Input : [ 0 , 1 ]
Output : 0
7 r e t u r n count
8 p r i n t ( bitswaprequired (12 , 7) )
2. 389. Find the Difference (easy). Given two strings s and t which
consist of only lowercase letters. String t is generated by random
shuffling string s and then add one more letter at a random position.
Find the letter that was added in t.
Example :
Input :
s = " abcd "
t = " abcde "
Output :
e
Explanation :
' e ' i s t h e l e t t e r t h a t was added .
In this chapter, we will specifically talk math related problems. Normally, for
the problems appearing in this section, they can be solved using our learned
programming methodology. However, it might not inefficient (we will get
LTE error on the LeetCode) due to the fact that we are ignoring their math
properties which might help us boost the efficiency. Thus, learning some of
the most related math knowledge can make our life easier.
16.1 Numbers
16.1.1 Prime Numbers
A prime number is an integer greater than 1, which is only divisible by 1
and itself. First few prime numbers are : 2 3 5 7 11 13 17 19 23 ...
Some interesting facts about Prime numbers:
2. 2, 3 are only two consecutive natural numbers which are prime too.
285
286 16. MATH AND PROBABILITY PROBLEMS
There are actually a lot of space for us to optimize the algorithm. First,
√
instead of checking till n, we can check till n because a larger factor of n
must be a multiple of smaller factor that has been already checked. Also,
because even numbers bigger than 2 are not prime, so the step we can set
it to 2. The algorithm can be improved further by use feature 3 that all
primes are of the form 6k ± 1, with the exception of 2 and 3. Together with
feature 4 which implicitly states that every non-prime integer is divisible by
a prime number smaller than itself. So a more efficient method is to test if n
is divisible by 2 or 3, then to check through all the numbers of form 6k ± 1.
1 def isPrime (n) :
2 # corner cases
3 i f n <= 1 :
4 return False
5 i f n<= 3 :
6 r e t u r n True
7
8 i f n % 2 == 0 o r n % 3 == 0 :
9 return False
10
11 f o r i i n r a n g e ( 5 , i n t ( n ∗ ∗ 0 . 5 ) +1, 6 ) : # 6k+1 o r 6k−1, s t e p
6 , up t i l l s q r t ( n ) , when i =5 , check 5 and 7 , ( k−1 , k+1)
12 i f n%i == 0 o r n%( i +2)==0:
13 return False
14 r e t u r n True
15 r e t u r n True
16.1. NUMBERS 287
4 ugly . s o r t ( )
5 d e f nthUglyNumber ( s e l f , n ) :
6 """
7 : type n : i n t
8 : rtype : int
9 """
10 r e t u r n s e l f . u g l y [ n−1]
The second way is only generate the nth ugly number, with
1 class Solution :
2 n = 1690
3 ugly = [ 1 ]
4 i2 = i3 = i5 = 0
5 f o r i i n r a n g e ( n−1) :
6 u2 , u3 , u5 = 2 ∗ u g l y [ i 2 ] , 3 ∗ u g l y [ i 3 ] , 5 ∗ u g l y [ i 5 ]
7 umin = min ( u2 , u3 , u5 )
8 u g l y . append ( umin )
9 i f umin == u2 :
10 i 2 += 1
11 i f umin == u3 :
12 i 3 += 1
13 i f umin == u5 :
14 i 5 += 1
15
16 d e f nthUglyNumber ( s e l f , n ) :
17 """
18 : type n : i n t
19 : rtype : int
20 """
21 r e t u r n s e l f . u g l y [ n−1]
16.1.3 Combinatorics
1. 611. Valid Triangle Number
Follow up: Could you optimize your algorithm to use only O(k) extra
space? Solution: Generate from Index 0 to K.
1 d e f getRow ( s e l f , rowIndex ) :
2 i f rowIndex == 0 :
3 return [ 1 ]
4 # f i r s t , n = rowIndex +1, i f n i s even ,
5 ans = [ 1 ]
290 16. MATH AND PROBABILITY PROBLEMS
6 f o r i i n r a n g e ( rowIndex ) :
7 tmp = [ 1 ] ∗ ( i +2)
8 f o r j i n r a n g e ( 1 , i +1) :
9 tmp [ j ] = ans [ j −1]+ ans [ j ]
10 ans = tmp
11 r e t u r n ans
Triangle Counting
Analysis: The first solution is to get all digits [1,2], and generate all the
permutation [[1,2],[2,1]], and generate the integer again, and then sort gen-
erated integers, so that we can pick the next one that is larger. But the time
complexity is O(n!).
Now, let us think about more examples to find the rule here:
1 435798 − >435879
2 1432−>2134
If we start from the last digit, we look to its left, find the cloest digit that
has smaller value, we then switch this digit, if we cant find such digit, then
we search the second last digit. If none is found, then we can not find one.
Like 21. return -1. This process is we get the first larger number to the
right.
1 [ 5 , 5 , 7 , 8 , −1, −1]
2 [ 2 , −1, −1, −1]
For the reminding digits, we do a sorting and put them back to those digit
to get the smallest value
16.1. NUMBERS 291
1 class Solution :
2 def getDigits ( s e l f , n) :
3 digits = []
4 while n :
5 d i g i t s . append ( n%10) # t h e l e a s t i m p o r t a n t p o s i t i o n
6 n = i n t (n/10)
7 return d i g i t s
8 d e f g e t S m a l l e s t L a r g e r E l e m e n t ( s e l f , nums ) :
9 i f not nums :
10 return [ ]
11 r s t = [ −1]∗ l e n ( nums )
12
13 f o r i , v i n enumerate ( nums ) :
14 smallestLargerNum = s y s . maxsize
15 i n d e x = −1
16 f o r j i n r a n g e ( i +1 , l e n ( nums ) ) :
17 i f nums [ j ]>v and smallestLargerNum > nums [ j ] :
18 index = j
19 smallestLargerNum = nums [ j ]
20 i f smallestLargerNum < s y s . maxsize :
21 r s t [ i ] = index
22 return rst
23
24
25 def nextGreaterElement ( s e l f , n) :
26 """
27 : type n : i n t
28 : rtype : int
29 """
30 i f n==0:
31 r e t u r n −1
32
33 d i g i t s = s e l f . getDigits (n)
34 digits = digits [:: −1]
35 # print ( digits )
36
37 r s t = s e l f . getSmallestLargerElement ( d i g i t s )
38 # print ( rst )
39 stop_index = −1
40
41 # switch
42 f o r i i n r a n g e ( l e n ( r s t ) −1, −1, −1) :
43 i f r s t [ i ]!= −1: #s w i t c h
44 print ( ' switch ' )
45 stop_index = i
46 digits [ i ] , digits [ rst [ i ] ] = digits [ rst [ i ]] ,
digits [ i ]
47 break
48 i f stop_index == −1:
49 r e t u r n −1
50
51 # print ( digits )
52
53 # s o r t from stop_index+1 t o t h e end
292 16. MATH AND PROBABILITY PROBLEMS
54 d i g i t s [ stop_index + 1 : ] = s o r t e d ( d i g i t s [ stop_index + 1 : ] )
55 print ( digits )
56
57 #c o n v e r t t h e d i g i t i a l i z e d answer t o i n t e g e r
58 nums = 0
59 digit = 1
60 for i in d i g i t s [ : : − 1 ] :
61 nums+=d i g i t ∗ i
62 d i g i t ∗=10
63 i f nums >2147483647:
64 r e t u r n −1
65
66
67 r e t u r n nums
Special case is when one number is zero, the GCD is the value of the other.
gcd(a, 0) = a.
The basic algorithm is: we get all divisors of each number, and then find
the largest common value. Now, let’s see how to we advance this algorithm.
We can reformulate the last example as:
1 36 = 2 ∗ 2 ∗ 3 ∗ 3
2 60 = 2 ∗ 2 ∗ 3 ∗ 5
3 GCD = 2 ∗ 2 ∗ 3
4 = 12
1. gcd(a, 0) = a
2. gcd(a, a) = a,
16.2. INTERSECTION OF NUMBERS 293
Based on the above features, we can use Euclidean Algorithm to gain GCD:
1 def euclid (a , b) :
2 w h i l e a != b :
3 # r e p l a c e l a r g e r number by i t s d i f f e r e n c e with t h e
s m a l l e r number
4 if a > b:
5 a = a − b
6 else :
7 b = b − a
8 return a
9
10 p r i n t ( e u c l i d (36 , 60) )
The only problem with the Euclidean Algorithm is that it can take several
subtraction steps to find the GCD if one of the given numbers is much bigger
than the other. A more efficient algorithm is to replace the subtraction
with remainder operation. The algorithm would stops when reaching a zero
reminder and now the algorithm never requires more steps than five times
the number of digits (base 10) of the smaller integer.
The recursive version code:
1 def euclidRemainder ( a , b) :
2 i f a == 0 :
3 return b
4 r e t u r n gcd ( b%a , a )
a×b
lcm(a, b) = (16.1)
gcd(a, b)
294 16. MATH AND PROBABILITY PROBLEMS
Long Multiplication
16.2 29. Divide Two Integers (medium) Given two integers dividend
and divisor, divide two integers without using multiplication, division
and mod operator. Return the quotient after dividing dividend by
divisor. The integer division should truncate toward zero.
1 Example 1 :
2
3 Input : d i v i d e n d = 1 0 , d i v i s o r = 3
4 Output : 3
5
6 Example 2 :
7
8 Input : d i v i d e n d = 7 , d i v i s o r = −3
9 Output : −2
16.4. PROBABILITY THEORY 295
Analysis: we can get the sign of the result first, and then convert the
dividend and divisor into its absolute value. Also, we better handle
the bound condition that the divisor is larger than the vidivend, we
get 0 directly. The code is given:
1 def d i v i d e ( s e l f , dividend , d i v i s o r ) :
2 d e f d i v i d e ( dd ) : # t h e l a s t p o s i t i o n t h a t d i v i s o r ∗ v a l <
dd
3 s , r = 0, 0
4 f o r i in range (9) :
5 tmp = s + d i v i s o r
6 i f tmp <= dd :
7 s = tmp
8 else :
9 r e t u r n s t r ( i ) , s t r ( dd−s )
10 r e t u r n s t r ( 9 ) , s t r ( dd−s )
11
12 i f d i v i d e n d == 0 :
13 return 0
14 s i g n = −1
15 i f ( d i v i d e n d >0 and d i v i s o r >0 ) o r ( d i v i d e n d < 0 and
d i v i s o r < 0) :
16 sign = 1
17 d i v i d e n d = abs ( d i v i d e n d )
18 d i v i s o r = abs ( d i v i s o r )
19 i f d i v i s o r > dividend :
20 return 0
21 ans , did , dr = [ ] , s t r ( d i v i d e n d ) , s t r ( d i v i s o r )
22 n = l e n ( dr )
23 p r e = d i d [ : n−1]
24 f o r i i n r a n g e ( n−1 , l e n ( d i d ) ) :
25 dd = p r e+d i d [ i ]
26 dd = i n t ( dd )
27 v , p r e = d i v i d e ( dd )
28 ans . append ( v )
29
30 ans = i n t ( ' ' . j o i n ( ans ) ) ∗ s i g n
31
32 i f ans > (1<<31) −1:
33 ans = (1<<31)−1
34 r e t u r n ans
In programming tasks, such problems are either solvable with some closed-
form formula or one has no choice than to enumerate the complete search
space.
296 16. MATH AND PROBABILITY PROBLEMS
16.6 Geometry
In this section, we will discuss coordinate related problems.
939. Minimum Area Rectangle(Medium)
Given a set of points in the xy-plane, determine the minimum area of a
rectangle formed from these points, with sides parallel to the x and y axes.
If there isn’t any rectangle, return 0.
1 Example 1 :
2
3 Input : [ [ 1 , 1 ] , [ 1 , 3 ] , [ 3 , 1 ] , [ 3 , 3 ] , [ 2 , 2 ] ]
4 Output : 4
5
6 Example 2 :
7
8 Input : [ [ 1 , 1 ] , [ 1 , 3 ] , [ 3 , 1 ] , [ 3 , 3 ] , [ 4 , 1 ] , [ 4 , 3 ] ]
9 Output : 2
21 combine ( p o i n t s , i +1 , c u r r +[ p o i n t s [ i ] ] , ans )
22 return
23
24 ans =[ s y s . maxsize ]
25 combine ( p o i n t s , 0 , [ ] , ans )
26 r e t u r n ans [ 0 ] i f ans [ 0 ] != s y s . maxsize e l s e 0
Traverse linked list using two pointers. Move one pointer by one and
other pointer by two. If these pointers meet at some node then there is a
loop. If pointers do not meet then linked list doesn’t have loop. Once you
detect a cycle, think about finding the starting point.
1 d e f d e t e c t C y c l e ( s e l f , A) :
2 #f i n d t h e " i n t e r s e c t i o n "
3 p_f=p_s=A
4 w h i l e ( p_f and p_s and p_f . next ) :
5 p_f = p_f . next . next
16.8. EXERCISE 299
16.8 Exercise
16.8.1 Number
313. Super Ugly Number
1 Super u g l y numbers a r e p o s i t i v e numbers whose a l l prime f a c t o r s
are in t h e g i v e n prime l i s t p r i m e s o f s i z e k . For example ,
[1 , 2 , 4 , 7 , 8 , 13 , 14 , 16 , 19 , 26 , 28 , 32] i s the sequence
of the f i r s t 12 s u p e r u g l y numbers g i v e n p r i m e s = [ 2 , 7 , 1 3 ,
19] of size 4.
2
3 Note :
4 ( 1 ) 1 i s a s u p e r u g l y number f o r any g i v e n p r i m e s .
5 ( 2 ) The g i v e n numbers i n p r i m e s a r e i n a s c e n d i n g o r d e r .
6 ( 3 ) 0 < k <= 1 0 0 , 0 < n <= 1 0 6 , 0 < p r i m e s [ i ] < 1 0 0 0 .
7 ( 4 ) The nth s u p e r u g l y number i s g u a r a n t e e d t o f i t i n a 32− b i t
signed integer .
1 d e f nthSuperUglyNumber ( s e l f , n , p r i m e s ) :
2 """
3 : type n : i n t
4 : type p r i m e s : L i s t [ i n t ]
5 : rtype : int
6 """
7 nums = [ 1 ]
8 i d e x s = [ 0 ] ∗ l e n ( p r i m e s ) #f i r s t i s t h e c u r r e n t i d e x
9 f o r i i n r a n g e ( n−1) :
10 min_v = maxsize
11 min_j = [ ]
12 f o r j , i d e x i n enumerate ( i d e x s ) :
13 v = nums [ i d e x ] ∗ p r i m e s [ j ]
14 i f v<min_v :
15 min_v = v
16 min_j=[ j ]
17 e l i f v==min_v :
18 min_j . append ( j ) #we can g e t m u t i p l e j i f
there i s a t i e
300 16. MATH AND PROBABILITY PROBLEMS
Appendix
301
17
303
304 17. PYTHON KNOWLEDGE BASE
Python Data Types Python contains 12 built-in data types. These in-
clude four scalar data types( int, float, complex and bool), four sequence
types(string, list, tuple and range), one mapping type(dict) and two set
types(set and frozenset). All the four scalar data types together with
string, tuple, range and fronzenset are immutable, and the others are mu-
table. Each of these can be manipulated using:
• Operators
• Functions
• Data-type methods
306 17. PYTHON KNOWLEDGE BASE
We can also write a .py file ourselves and import them. We provide reference
to some of the popular and useful built-in modules that is not covered in
Part III in Python in Section 17.9 of this chapter, they are:
• Re
get / # f i r s t subpackage
__init__ . py
i n f o . py
p o i n t s . py
t r a n s a c t i o n s . py
create / # s e c o n d subpackage
__init__ . py
a p i . py
p l a t f o r m . py
When we import any package, python interpreter searches for sub direc-
tories / packages.
Library is collection of various packages. There is no difference between
package and python library conceptually. Have a look at requests/requests
library. We use it as a package.
This code takes advantage of the mutability of a single list object to gather
your data together and then allocate a single result string to put your data
in. That cuts down on the total number of objects allocated by almost half.
Another pitfall related to mutability is the following scenario:
1 d e f my_function ( param = [ ] ) :
2 param . append ( " t h i n g " )
3 r e t u r n param
4
5 my_function ( ) # r e t u r n s [ " t h i n g " ]
6 my_function ( ) # r e t u r n s [ " t h i n g " , " t h i n g " ]
What you might think would happen is that by giving an empty list as a
default value to param, a new empty list is allocated each time the function
is called and no list is passed in. But what actually happens is that every
call that uses the default list will be using the same list. This is because
Python (a) only evaluates functions definitions once, (b) evaluates default
arguments as part of the function definition, and (c) allocates one mutable
list for every call of that function.
Do not put a mutable object as the default value of a function parameter.
Immutable types are perfectly safe. If you want to get the intended effect,
do this instead:
308 17. PYTHON KNOWLEDGE BASE
1 d e f my_function2 ( param=None ) :
2 i f param i s None :
3 param = [ ]
4 param . append ( " t h i n g " )
5 r e t u r n param
6 Conclusion
There are various compound operators that follows the order: vari-
able_name (arithemetic operator) = variable or data type. Such as a += 5
that adds to the variable and later assigns the same. It is equivalent to a =
a + 5.
• Boolean False
Identity operators Identity operators are used to check if two values (or
variables) are located on the same part of the memory. Two variables that
are equal does not imply that they are identical as we have shown in the
last section.
17.3 Function
17.3.1 Python Built-in Functions
Check out here https://docs.python.org/3/library/functions.html.
17.3. FUNCTION 311
Built-in Data Types We have functions like int(), float(), str(), tuple(),
list(), set(), dict(), bool(), chr(), ord(). These functions can be used for
intialization, and also used for type conversion between different data types.
Map
Map applies a function to all the items in an input_list. Here is the
blueprint:
1 map( function_to_apply , l i s t _ o f _ i n p u t s )
Most of the times we want to pass all the list elements to a function
one-by-one and then collect the output. For instance:
1 items = [ 1 , 2 , 3 , 4 , 5]
2 squared = [ ]
3 f o r i in items :
4 s q u a r e d . append ( i ∗ ∗ 2 )
Map allows us to implement this in a much simpler and nicer way. Here you
go:
1 items = [ 1 , 2 , 3 , 4 , 5]
2 s q u a r e d = l i s t (map( lambda x : x ∗ ∗ 2 , i t e m s ) )
312 17. PYTHON KNOWLEDGE BASE
Most of the times we use lambdas with map so I did the same. Instead of a
list of inputs we can even have a list of functions! Here we use x(i) to call
the function, where x is replaced with each function in funcs, and i is the
input to the function.
1 def multiply (x) :
2 r e t u r n ( x∗x )
3 d e f add ( x ) :
4 r e t u r n ( x+x )
5
6 f u n c s = [ m u l t i p l y , add ]
7 f o r i in range (5) :
8 v a l u e = l i s t (map( lambda x : x ( i ) , f u n c s ) )
9 print ( value )
10
11 # Output :
12 # [0 , 0]
13 # [1 , 2]
14 # [4 , 4]
15 # [9 , 6]
16 # [16 , 8]
Filter
As the name suggests, filter creates a list of elements for which a function
returns true. Here is a short and concise example:
1 n um b er _ li s t = r a n g e ( −5 , 5 )
2 l e s s _ t h a n _ z e r o = l i s t ( f i l t e r ( lambda x : x < 0 , n um b er _ li s t ) )
3 pr int ( less_than_zero )
4
5 # Output : [ −5 , −4, −3, −2, −1]
The filter resembles a for loop but it is a builtin function and faster.
Note: If map and filter do not appear beautiful to you then you can read
about list/dict/tuple comprehensions.
Reduce
Reduce is a really useful function for performing some computation on a list
and returning the result. It applies a rolling computation to sequential pairs
of values in a list. For example, if you wanted to compute the product of a
list of integers.
So the normal way you might go about doing this task in python is using
a basic for loop:
1 product = 1
2 l i s t = [1 , 2 , 3 , 4]
3 f o r num i n l i s t :
4 p r o d u c t = p r o d u c t ∗ num
5
6 # p r o d u c t = 24
17.4. CLASS 313
17.4 Class
17.4.1 Special Methods
From [1]. http://www.informit.com/articles/article.aspx?p=453682&
seqNum=6 All the built-in data types implement a collection of special object
methods. The names of special methods are always preceded and followed
by double underscores (__). These methods are automatically triggered by
the interpreter as a program executes. For example, the operation x + y
is mapped to an internal method, x.__add__(y), and an indexing opera-
tion, x[k], is mapped to x.__getitem__(k). The behavior of each data type
depends entirely on the set of special methods that it implements.
User-defined classes can define new objects that behave like the built-
in types simply by supplying an appropriate subset of the special methods
described in this section. In addition, built-in types such as lists and dictio-
naries can be specialized (via inheritance) by redefining some of the special
methods. In this book, we only list the essential ones so that it speeds up
our interview preparation.
Table 17.6: Special Methods for Object Creation, Destruction, and Repre-
sentation
Method Description
*__init__(self Called to initialize a new instance
[,*args [,**kwargs]])
__del__(self) Called to destroy an instance
*__repr__(self) Creates a full string representation of an object
__str__(self) Creates an informal string representation
__cmp__(self,other) Compares two objects and returns negative, zero, or positive
__hash__(self) Computes a 32-bit hash index
hline Returns 0 or 1 for truth-value testing
__nonzero__(self)
__unicode__(self) Creates a Unicode string representation
If we have no __repr__(), the output for the following test cases are:
1 8766662474223
2 <__main__ . Student o b j e c t a t 0 x 7 f 9 2 5 c d 7 9 e f 0 >
Table 17.7: Special Methods for Object Creation, Destruction, and Repre-
sentation
Method Description
__lt__(self,other) self < other
__le__(self,other) self <= other
__gt__(self,other) self > other
__ge__(self,other) self >= other
__eq__(self,other) self == other
__ne__(self,other) self != other
17.4.3 Inheritance
From the above outputs, we can see that the colors1 list is the same but
in the second case, it is changed although we are assigning value to colors2.
The result can be either wanted or not wanted. In python, to assign one
list to other directly is similar to a pointer in C++, which both point to
the same physical address. In the first case, colors2 is reassigned a new list,
which has an new address, so now colors2 points to the address of this new
list instead, which leaves the values of colors2 untouched at all. We can
visualize this process as follows: However, we often need to do copy and
(a) The copy process for code 1 (b) The copy process for code 2
leave the original list or string unchanged. Because there are a variety of
list, from one dimensional, two-dimensional to multi-dimensional.
4 print ( list2 )
5 [ 'a ' , 'x ' , ' c ' , 'd ' ]
6 print ( list1 )
7 [ 'a ' , 'b ' , ' c ' , 'd ' ]
But as soon as a list contains sublists, we have the same difficulty, i.e.
just pointers to the sublists.
1 l s t 1 = [ ' a ' , ' b ' , [ ' ab ' , ' ba ' ] ]
2 lst2 = lst1 [ : ]
If you assign a new value to the 0th Element of one of the two lists, there
will be no side effect. Problems arise, if you change one of the elements of
the sublist.
1 >>> l s t 1 = [ ' a ' , ' b ' , [ ' ab ' , ' ba ' ] ]
2 >>> l s t 2 = l s t 1 [ : ]
3 >>> l s t 2 [ 0 ] = ' c '
4 >>> l s t 2 [ 2 ] [ 1 ] = ' d '
5 >>> p r i n t ( l s t 1 )
6 [ ' a ' , ' b ' , [ ' ab ' , ' d ' ] ]
5 l s t 2 = deepcopy ( l s t 1 )
6
7 lst2 [ 2 ] [ 1 ] = "d"
8 lst2 [0] = "c" ;
9
10 print lst2
11 print lst1
If we save this script under the name of deep_copy.py and if we call the
script with“python deep_copy.p”, we will receive the following output:
1 $ python deep_copy . py
2 [ ' c ' , ' b ' , [ ' ab ' , ' d ' ] ]
3 [ ' a ' , ' b ' , [ ' ab ' , ' ba ' ] ]
17.7 Loops
The for loop can often be needed in algorithms we have two choices: for and
while. So to learn the basic grammar to do for loop easily could help us be
more efficienct in programming.
Usually for loop is used to iterate over a sequence or matrix data. For
example, the following grammar works for either string or list.
318 17. PYTHON KNOWLEDGE BASE
Sometimes, we want to iterate two lists jointly at the same time, which
requires they both have the same length. We can use zip to join them
together, and all the others for loop works just as the above. For example:
1 a , b = [1 , 2 , 3 , 4 , 5] , [5 , 4 , 3 , 2 , 1]
2 f o r idx , (num_a , num_b) i n enumerate ( z i p ( a , b ) ) :
3 p r i n t ( idx , num_a , num_b)
5. Reversing
17.9. SUPPLEMENTAL PYTHON TOOLS 319
1 # 1 . r e v e r s i n g s t r i n g s or l i s t
2 a = ' crackingleetcode '
3 b = [1 ,2 ,3 ,4 ,5]
4 print (a [:: −1] , a [:: −1])
5 # 2 . i t e r a t e o v e r each c h a r o f t h e s t r i n g o r l i s t
c o n t e n t s i n r e v e r s e o r d e r e f f i c i e n t l y , h e r e we u s e z i p
to
6 f o r char , num i n z i p ( r e v e r s e d ( a ) , r e v e r s e d ( b ) ) :
7 p r i n t ( char , num)
8 #3 . r e v e r s e each d i g i t i n an i n t e g e r o r f l o a t number
9 num = 123456789
10 p r i n t ( i n t ( s t r (num) [ : : − 1 ] ) )
11
1 def index ( a , x ) :
2 ' Locate the l e f t m o s t value e x a c t l y equal to x '
3 i = bisect_left (a , x)
4 i f i != l e n ( a ) and a [ i ] == x :
5 return i
6 r a i s e ValueError
7
8 def find_lt (a , x) :
9 ' Find r i g h t m o s t v a l u e l e s s than x '
10 i = bisect_left (a , x)
11 if i :
12 r e t u r n a [ i −1]
13 r a i s e ValueError
14
15 def find_le (a , x) :
16 ' Find r i g h t m o s t v a l u e l e s s than o r e q u a l t o x '
17 i = bisect_right (a , x)
18 if i :
320 17. PYTHON KNOWLEDGE BASE
19 r e t u r n a [ i −1]
20 r a i s e ValueError
21
22 def find_gt (a , x ) :
23 ' Find l e f t m o s t v a l u e g r e a t e r than x '
24 i = bisect_right (a , x)
25 i f i != l e n ( a ) :
26 return a [ i ]
27 r a i s e ValueError
28
29 def find_ge ( a , x ) :
30 ' Find l e f t m o s t item g r e a t e r than o r e q u a l t o x '
31 i = bisect_left (a , x)
32 i f i != l e n ( a ) :
33 return a [ i ]
34 r a i s e ValueError
17.9.3 collections
collections is a module in Python that implements specialized container
data types alternative to Python’s general purpose built-in containers: dict,
list, set, and tuple. The including container type is summarized in Ta-
ble 17.8. Most of them we have learned in Part III, therefore, in the table
we simply put the reference in the table. Before we use them, we need to
import each data type as:
1 from c o l l e c t i o n s import deque , Counter , OrderedDict , d e f a u l t d i c t
, namedtuple
321