
This book is dedicated to Dr. Hamid R. Tizhoosh for inspiring me in my studies and to my mother, Min Kyoung Seo, for her kindness and support.
The motivation for writing this book was the lack of resources available about data structures and algorithms written in JavaScript. This was strange to me because today many of the job opportunities for software development require knowledge of JavaScript; it is the only language that can be used to write the entire stack, including the front-end, mobile (native and hybrid) platforms, and back-end. It is crucial for JavaScript developers to understand how data structures work and how to design algorithms to build applications.
Therefore, this book aims to teach data structure and algorithm concepts from computer science for JavaScript rather than for the more typical Java or C++. Because JavaScript follows the prototypal inheritance pattern, unlike Java and C++ (which follow the inheritance pattern), there are some changes in writing data structures in JavaScript. The classical inheritance pattern allows inheritance by creating a blueprint-like form that objects follow during inheritance. However, the prototypal inheritance pattern means copying the objects and changing their properties.
This book first covers fundamental mathematics for Big-O analysis and then lays out the basic JavaScript foundations, such as primitive objects and types. Then, this book covers implementations and algorithms for fundamental data structures such as linked lists, stacks, trees, heaps, and graphs. Finally, more advanced topics such as efficient string search algorithms, caching algorithms, and dynamic programming problems are explored in great detail.
Thank you, Phil Nash, for the valuable feedback that helped me improve the technical content of this book with clear explanations and concise code.
Special thanks to the Apress team. This includes James Markham, Nancy Chen, Jade Scard, and Chris Nelson. Finally, I want to thank Steve Anglin for reaching out to me to publish with Apress.

is a data engineer at Yelp and previously worked for the data platform engineering team at NVIDIA. He developed a deep interest in JavaScript during an internship at SMART Technologies (acquired by Foxconn), where he developed Node.js-based JavaScript APIs for serial port communication between electronic board drivers and a web application. Despite how relevant JavaScript is to the modern software engineering industry, currently no books besides this one teach algorithms and data structures using JavaScript. Sammie understands how difficult these computer science concepts are and aims to provide clear and concise explanations in this book.

is a developer evangelist for Twilio, serving developer communities in London and all over the world. He is a Ruby, JavaScript, and Swift developer; Google Developers Expert; blogger; speaker; and occasional brewer. He can be found hanging out at meetups and conferences, playing with new technologies and APIs, or writing open source code.
O(1) is holy.
—Hamid Tizhoosh
Before learning how to implement algorithms, you should understand how to analyze the effectiveness of them. This chapter will focus on the concept of Big-O notation for time and algorithmic space complexity analysis. By the end of this chapter, you will understand how to analyze an implementation of an algorithm with respect to both time (execution time) and space (memory consumed).
The Big-O notation measures the worst-case complexity of an algorithm. In Big-O notation, n represents the number of inputs. The question asked with Big-O is the following: “What will happen as n approaches infinity?”

Common Big-O complexities
The following sections illustrate these common time complexities with some simple examples.
O(1) does not change with respect to input space. Hence, O(1) is referred to as being constant time. An example of an O(1) algorithm is accessing an item in the array by its index. O(n) is linear time and applies to algorithms that must do n operations in the worst-case scenario.
Coefficient rule: If f(n) is O(g(n)), then kf(n) is O(g(n)), for any constant k > 0. The first rule is the coefficient rule, which eliminates coefficients not related to the input size, n. This is because as n approaches infinity, the other coefficient becomes negligible.
Sum rule: If f(n) is O(h(n)) and g(n) is O(p(n)), then f(n)+g(n) is O(h(n)+p(n)). The sum rule simply states that if a resultant time complexity is a sum of two different time complexities, the resultant Big-O notation is also the sum of two different Big-O notations.
Product rule: If f(n) is O(h(n)) and g(n) is O(p(n)), then f(n)g(n) is O(h(n)p(n)). Similarly, the product rule states that Big-O is multiplied when the time complexities are multiplied.
Transitive rule: If f(n) is O(g(n)) and g(n) is O(h(n)), then f(n) is O(h(n)). The transitive rule is a simple way to state that the same time complexity has the same Big-O.
Polynomial rule: If f(n) is a polynomial of degree k, then f(n) is O(nk). Intuitively, the polynomial rule states that polynomial time complexities have Big-O of the same polynomial degree.
Log of a power rule: log(nk) is O(log(n)) for any constant k > 0. With the log of a power rule, constants within a log function are also ignored in Big-O notation.
Special attention should be paid to the first three rules and the polynomial rule because they are the most commonly used. I’ll discuss each of those rules in the following sections.
If f(n) is O(g(n)), then kf(n) is O(g(n)), for any constant k > 0.
This means that both 5f(n) and f(n) have the same Big-O notation of O(f(n)).
This block has f(n) = 5n. This is because it runs from 0 to 5n. However, the first two examples both have a Big-O notation of O(n). Simply put, this is because if n is close to infinity or another large number, those four additional operations are meaningless. It is going to perform it n times. Any constants are negligible in Big-O notation.
Lastly, this block of code has f(n) = n+1. There is +1 from the last operation (count+=3). This still has a Big-O notation of O(n). This is because that 1 operation is not dependent on the input n. As n approaches infinity, it will become negligible.
If f(n) is O(h(n)) and g(n) is O(p(n)), then f(n)+g(n) is O(h(n)+p(n)).
It is important to remember to apply the coefficient rule after applying this rule.
In this example, line 4 has f(n) = n, and line 7 has f(n) = 5n. This results in 6n. However, when applying the coefficient rule, the final result is O(n) = n.
If f(n) is O(h(n)) and g(n) is O(p(n)), then f(n)g(n) is O(h(n)p(n)).
In this example, f(n) = 5n*n because line 7 runs 5n times for a total of n iterations. Therefore, this results in a total of 5n2 operations . Applying the coefficient rule, the result is that O(n)=n2.
The polynomial rule states that polynomial time complexities have a Big-O notation of the same polynomial degree.
If f(n) is a polynomial of degree k, then f(n) is O(nk).
In this example, f(n) = nˆ2 because line 4 runs n*n iterations.
This was a quick overview of the Big-O notation. There is more to come as you progress through the book.
Eliminating coefficients/constants (coefficient rule)
Adding up Big-Os (sum rule)
Multiplying Big-Os (product rule)
Determining the polynomial of the Big-O notation by looking at loops (polynomial rule)
Calculate the time complexities for each of the exercise code snippets.
O(n2)
There are two nested loops. Ignore the constants in front of n.
O(n3)
There are four nested loops, but the last loop runs only until 10.
O(1)
Constant complexity. The function runs from 0 to 1000. This does not depend on n.
O(n)
Linear complexity. The function runs from 0 to 10n. Constants are ignored in Big-O.
O(log2n)
Logarithmic complexity. For a given n, this will operate only log2n times because i is incremented by multiplying by 2 rather than adding 1 as in the other examples.
O(∞)
Infinite loop. This function will not end.
This chapter will briefly discuss some exceptions and cases of JavaScript’s syntax and behavior. As a dynamic and interpreted programming language, its syntax is different from that of traditional object-oriented programming languages. These concepts are fundamental to JavaScript and will help you to develop a better understanding of the process of designing algorithms in JavaScript.
The scope is what defines the access to JavaScript variables. In JavaScript, variables can belong to the global scope or to the local scope. Global variables are variables that belong in the global scope and are accessible from anywhere in the program.
However, this creates a global variable, and this is one of the worst practices in JavaScript. Avoid doing this at all costs. Always use var or let to declare variables. Finally, when declaring variables that won’t be modified, use const.
In JavaScript, var is one keyword used to declare variables. These variable declarations “float” all the way to the top. This is known as variable hoisting. Variables declared at the bottom of the script will not be the last thing executed in a JavaScript program during runtime.
The bottom variable declaration, which was at the last line in the function, is floated to the top, and logging the variable works.
The key thing to note about the var keyword is that the scope of the variable is the closest function scope. What does this mean?
In Java, this syntax would have thrown an error because the insideIf variable is generally available only in that if statement block and not outside it.
4 was printed, not the global value of 1, because it was redeclared and available in that scope.
In this example, nothing is logged to the console because the insideIf variable is available only inside the if statement block.
JavaScript has different data types than in traditional languages such as Java. Let’s explore how this impacts things such as equality comparison.
Here, node is some variable. If that variable is empty, null, or undefined, it will be evaluated as false.
false
0
Empty strings ('' and "")
NaN
undefined
null
true
Any number other than 0
Non-empty strings
Non-empty object
JavaScript is a scripting language, and variables are not assigned a type during declaration. Instead, types are interpreted as the code runs.
"5" == 5 returns true because "5" is coerced to a number before the comparison. On the other hand, "5" === 5 returns false because the type of "5" is a string, while 5 is a number.
Most strongly typed languages such as Java use isEquals() to check whether two objects are the same. You may be tempted to simply use the == operator to check whether two objects are the same in JavaScript.
Although these objects are equivalent (same properties and values), they are not equal. Namely, the variables have different addresses in memory.
This is why most JavaScript applications use utility libraries such as lodash1 or underscore,2 which have the isEqual(object1, object2) function to check two objects or values strictly. This occurs via implementation of some property-based equality checking where each property of the object is compared.
Although the two functions perform the same operation, the functions have different addresses in memory, and therefore the equality operator returns false. The primitive equality check operators, == and ===, can be used only for strings and numbers. To implement an equivalence check for objects, each property in the object needs to be checked.
JavaScript has a different variable declaration technique than most programming languages. var declares the variable within the function scope, let declares the variable in the block scope, and variables can be declared without any operator in the global scope; however, global scope should be avoided at all times. For type checking, typeof should be used to validate the expected type. Finally, for equality checks, use == to check the value, and use === to check for the type as well as the value. However, use these only on non-object types such as numbers, strings, and booleans.
This chapter will focus on JavaScript number operations, number representation, Number objects, common number algorithms, and random number generation. By the end of this chapter, you will understand how to work with numbers in JavaScript as well as how to implement prime factorization, which is fundamental for encryption.
These operators are universally used in other programming languages and are not specific to JavaScript.

The 32-bit floating-point number system

Figure 3-1 shows the following break down of the 32 bits:
sign = 0

This results in the following:
value = 1 x 2124-127 x 1.25 = 1 x 2-3 x 1.25 = 0.15625
With decimal fractions, this floating-point number system causes some rounding errors in JavaScript. For example, 0.1 and 0.2 cannot be represented precisely.
To really understand why 0.1 cannot be represented properly as a 32-bit floating-point number, you must understand binary. Representing many decimals in binary requires an infinite number of digits. This because binary numbers are represented by 2n where n is an integer.

Long division for 0.1
Luckily, there are some built-in properties of the Number object in JavaScript that help work around this.
Since JavaScript uses floating point to represent all numbers, integer division does not work.
Integer division in programming languages like Java simply evaluates division expressions to their quotient.
This function works by checking whether the difference between the two numbers are smaller than Number.EPSILON. Remember that Number.EPSILON is the smallest difference between two representable numbers. The difference between 0.1+0.2 and 0.3 will be smaller than Number.EPSILON.
Number.MAX_VALUE returns the largest floating-point number possible.
Number.MIN_SAFE_INTEGER returns the smallest integer.
Number.MIN_VALUE returns the smallest floating-point number possible.
Number.MIN_VALUE is equal to 5e-324. This is not a negative number since it is the smallest floating-point number possible and means that Number.MIN_VALUE is actually bigger than Number.MIN_- SAFE_INTEGER.
This is because this is similar to writing 0 - 1 == -1.
This evaluates to true because nothing can go smaller than -Infinity.
One of the most discussed algorithms involving numbers is for testing whether a number is a prime number. Let’s review this now.
Time Complexity: O(n)
The time complexity is O(n) because this algorithm checks all numbers from 0 to n.
This is an example of an algorithm that can be easily improved. Think about how this method iterates through 2 to n. Is it possible to find a pattern and make the algorithm faster? First, any multiple of 2s can be ignored, but there is more optimization possible.
Time Complexity: O(sqrt(n))
This improved solution cuts the time complexity down significantly.
Time Complexity: O(sqrt(n))
This algorithm works by printing any number that is divisible by i without a remainder. In the case that a prime number is passed into this function, it would be handled by printing whether n is greater than 2.
Math.random() returns a float between 0 and 1.
You may wonder how you get random integers or numbers greater than 1.
Given three numbers x, y, and p, compute (xˆy) % p. (This is modular exponentiation.)
Here, x is the base, y is exponent, and p is the modulus.
Modular exponentiation is a type of exponentiation performed over a modulus, which is useful in computer science and used in the field of public-key encryption algorithms.
This does exactly what the question asks. However, it cannot handle large exponents.
Remember that this is implemented with encryption algorithms. In strong cryptography, the base is often at least 256 bit (78 digits).
Consider this case, for example:
Base: 6x1077, Exponent: 27, Modulus: 497
In this case, (6x1077)27 is a very large number and cannot be stored in a 32-bit floating point.
There is another approach, which involves some math. One must observe the following mathematical property:
Using this mathematical property, you can iterate 1 to the exponent, recalculating each time by multiplying the current modulus value with the last.
Example: Base: 4, Exponent: 3, Modulus: 5
4ˆ3 % 5 = 64 % 5 = 4
value = (lastValue x base ) % modulus:
value = (1 x 4) % 5 = 4 % 5 = 4
value = (4 x 4) % 5 = 16 % 5 = 1
value = (1 x 4) % 5 = 4 % 5 = 4
Time Complexity: O(n)
The time complexity is O(n) where n is equal to the exponent value.
Print all primes less than n.
Time Complexity: O(nsqrt(n))
This is because isPrime (covered earlier in this chapter) with a time complexity of O(sqrt(n)) is run n times.
Check for a set of prime factors.
Let’s define ugly numbers as those whose only prime factors are 2, 3, or 5. The sequence 1, 2, 3, 4, 5, 6, 8, 9, 10, 12, 15, … shows the first 11 ugly numbers. By convention, 1 is included.
Time Complexity for maxDivide(number, divisor): O(logdivisor(number))
The time complexity of maxDivide is a logarithmic function which depends on divisor and the number. When testing primes of 2, 3, and 5, the logarithmic of 2 (log2 (n)) yields the highest time complexity.
Time Complexity for isUgly: O(log2(n))
Time Complexity for arrayNUglyNumbers: O(n(log2(n)))
The isUgly function is limited by the time complexity of maxDivide(number, 2). Hence, arrayNUglyNumbers has n times that time complexity.
Prime number validation and prime factorization are concepts used in various computer science applications such as encryption, as covered in Chapter 4. Finally, random number generation in JavaScript works via Math.random().
This chapter will focus on working with strings, the JavaScript String object, and the String object’s built-in functions. You will learn how to access, compare, decompose, and search strings for commonly used real-life purposes. In addition, the chapter will explore string encoding, decoding, encryption, and decryption. By the end of this chapter, you will understand how to effectively work with JavaScript strings and have a fundamental understanding of string encoding and encryption.
JavaScript’s native String primitive comes with various common string functions.
.charAt(index) takes an index (which starts at 0) and returns the character at that index location in the string.
This can be really useful for comparing strings when sorting algorithms, which is covered later in the book.
This is useful for when there are items listed in a string. The string can be turned into an array to easily iterate through them.
Regular expressions (regexes) are a set of characters that define a search pattern. Learning how to use regexes is a massive task of its own, but as a JavaScript developer, it is important you know the basics of regexes.
JavaScript also comes with the native object RegExp, which is used for regular expressions.
search(): Tests for matches in a string. This returns the index of the match.
match(): Tests for matches. This returns all the matches.
exec(): Tests for matches in a string. This returns the first match.
test(): Tests for matches in a string. This returns true or false.
Here are the basic regex rules:
^: Indicates the start of a string/line
\d: Finds any digit
[abc]: Finds any character between the brackets
[^abc]: Finds any character not between the brackets
[0-9]: Finds any digit between the brackets
[^0-9]: Finds any digit not between the brackets
(x|y): Finds any of the alternatives specified
Regexes are immensely helpful for checking the validity of user input in JavaScript. One common type of input check is to validate whether it has any numeric characters.
The following are five regexes that developers often use.
In web applications, web URLs often pass in parameters in the URL for routing or database query purposes.
Encoding is a general concept in computer science that represents characters in a specialized format for efficient transmission or storage.
All computer file types are encoded in specific structures.
This is a Base64-encoded PDF string. Data like this is often passed to the server when a PDF file is uploaded.
The btoa() function creates a Base64-encoded ASCII string from a string. Each character in the string is treated as a byte (8 bits: eight 0 and 1s).
Learn more about Base64 at https://en.wikipedia.org/wiki/Base64 .
The database creates a unique integer-based ID for the URL. In Figure 4-1, www.google.com has the entry 11231230 in the database.

Database entries
The integer ID is shortened into a string. When shortened with Base62 encoding, 11231230 will be VhU2.

Database entries after shortening

SSL warning

TSL process
The server sends its asymmetric public key to the browser.
The browser creates a symmetric key for the current session, which is encrypted by the server’s asymmetric public key.
The server decrypts the browser’s session via its private key and retrieves the session key.
Both systems have the session key and will use this to transmit data securely.
This is secure because only the browser and the server know the session key. If the browser was to connect to the same server the next day, a new session key would be created.
The SSL warning message is a sign that the browser and server may not be encrypting the data on that connection.
The most commonly used public-key encryption algorithm is the RSA algorithm.
RSA is an encryption algorithm based on the difficulty of factoring large integers. In RSA, two large prime numbers and a supplementary value are generated as the public key. Anyone can use the public key to encrypt a message, but only those with the prime factors can decode the message.
Key generation: The public key (shared) and private key (kept secret) are generated. The construction method of the keys generated should be also secret.
Encryption: The secret message can be encrypted via the public key.
Decryption: Only the private key can be used to decrypt the message.
The product of p and q is denoted as n.
The product of (p-1) and (q-1) is denoted as phi.
e is typically 3. Other values greater than 2 can be used.
d is a value such that (e × d) % phi = 1.

24096
Function | Usage |
|---|---|
charAt(index) | Accesses a single character at index |
substring(startIndex, endIndex) | Accesses part of string from startIndex to endIndex |
str1 > str2 | Returns true if str1 is lexicographically bigger than str2 |
indexOf(str, startIndex) | Index of the desired str starting at startIndex |
str.split(delimiter) | Breaks a string into an array with the specified delimiter |
str.replace(original,new) | Replaces original with new |
Regex Pattern | Usage |
|---|---|
/\d+/ | Any numeric characters |
/^\d+$/ | Only numeric characters |
/^[0-9]*.[0-9]*[1-9]+$/ | Float numeric characters |
/[a-zA-Z0-9]/ | Only alphanumeric characters |
This chapter will focus on working with JavaScript arrays. As a JavaScript developer, you will use the array often; it is the most commonly used data structure. Arrays in JavaScript come with a lot of built-in methods. In fact, there are various ways to do the same type of array operations for each use case. By the end of this chapter, you will understand how to work with arrays and be able to choose the right method for the situation.
For any data structure, developers are interested in time and space complexity associated with the four fundamental operations: access, insertion, deletion, and search. (For a review of Big-O notations, please refer to Chapter 1.)
The time complexity of this operation is O(1) in theory. It should be noted that, practically, this depends on the JavaScript engine that runs the code. This applies to all natively supported JavaScript objects.
The time complexity of .pop is O(1) similarly to .push.
Iteration is the process of accessing each of the items contained within a data structure. There are multiple ways to iterate through an array in JavaScript. They all have a time complexity of O(n) since the iteration is visiting n number of elements.
This prints the following: 0,1,2,3.
This printsall, cows, are, and big.
This prints out all, cows, are, and big.
Both print all, cows, are, and big.
The following sections discuss other commonly used helper functions for processing. In addition, working with arrays will be covered.
.from() takes O(n), where n is the size of the array. This is intuitive because copying the array requires copying all n elements of the array.
This helper function returns and changes the contents of an array by removing existing elements and/or adding new elements.
.splice() is, worst case, O(n). Similarly to copying, if the range specified is the whole array, each n item has to be removed.
Both the Math.max and Math.min functions take an unlimited number of parameters, so you can use the spread operator for the following operations.
All the code for the exercises can be found on GitHub.1
Problem: Given the array arr, find and return two indices of the array that add up to weight or return -1 if there is no combination that adds up to weight.
For example, in an array like [1,2,3,4,5], what numbers add up to 9?
The answers are 4 and 5, of course.
This solution iterates through an array looking to see whether a matching pair exists.
Two for loops over n elements of the array yields a high time complexity. However, no extra memory was created. Similar to how time complexity describes the time required relative to input size, n, to finish the algorithm, the space complexity describes the additional memory needed for implementation. The space complexity, O(1), is constant.
Time Complexity: O(n2)
Space Complexity: O(1)
Let’s think about how to do this in linear time of O(n).
What if any previously seen array elements were stored and could be checked easily?
Here, 4 and 5 are the combination, and their indices are [3,4]. How could it be determined that a solution exists when 5 is visited?
Time Complexity: O(n)
Space Complexity: O(n)
Storing into a hash table and looking an item up from a hash table is only O(1). Space complexity has increased to O(n) to store the visited array indices inside the hash table.
Let’s review what the .slice() function does.
Time Complexity: O(n)
Space Complexity : O(n)
The time complexity is O(n) because all n items in the array must be accessed. Space complexity is also O(n) to hold all n items when copying the array.
Recall that median in an even number of a set is the average of the two middle numbers. If the array is sorted, this is simple.
Here’s an example:
Now, you can iterate through both of the arrays and compare which is bigger to track the median. If the two arrays are the same size, the total size will be an even number.
This is because both two even numbers and two odd numbers add up to an even number. Please refer to Chapter 8 for more background.
Since both of the arrays are sorted, this function can be recursively called. Each time, it checks which median is greater.
If the second array’s median is greater, the first array is cut in half, and only the higher half is passed recursively.
If the first array’s median is greater, the second array is cut in half, and only the higher half is passed in as the first array for the next function call because the array2 parameter in the function must always be bigger than the array1 parameter. Finally, the size of the array represented as pos is required to check whether the size of the array is even or odd.
Here’s another example:
array1 = [1,2,3] and array2 = [4, 5, 6]
Here, the median of array1 is 2, and the median of array2 is 5. So, the median must be present within [2,3] and [4,5]. Since there are only four elements left, the median can be computed as follows:
Time Complexity: O(log2(n))
By cutting the array size by half each time, logarithmic time complexity is achieved.
In this example with three arrays, k=3.
To do this, simply iterate over each array and count instances of every element. However, do not track repeated ones (5 and 5.5 should be counted once in one array iteration). To do this, check whether the last element is the same before incrementing. This will work only if it is sorted.
Time Complexity: O(kn)
Space Complexity: O(n)
Here, n is longest array length, and k is the number of arrays.
Some parts of JavaScript can be written just like a functional programming language. Unlike imperative programming, JavaScript does not focus on the state of the program. It does not use loops, only function (method) calls. You can learn more about functional programming in JavaScript from Beginning Functional JavaScript by Anto Aravinth (Apress, 2017).
In this section, only three functional array methods in JavaScript will be explored: map, filter, and reduce. These methods do not change the original array contents.
The map function applies passed function transformation to every element in the array and returns a new array with those transformations applied.
The filter function returns only those elements of the array that meet a passed condition parameter. Again, this does not change the original array.
The reduce function combines all the elements in the array into one value using a passed transformation function parameter.

Multidimensional array

Jagged array

Three-by-three matrix

Three-by-three matrix of numbers
All the code for the exercises can be found on GitHub.2

Spiral print
Printing from left to right
Printing from top to bottom
Printing from right to left
Printing from bottom to top
Keeping a limit on these four operations
Top row
Bottom row
Left column
Right column
Time Complexity: O(mn)
Space Complexity: O(1)
Here, m is the number of rows, and n is the number of columns . Each item in the matrix is visited only once.
Given a matrix representing a tic-tac-toe board , determine whether someone won, whether it was a tie, or whether the game has not ended yet.3
Here are some examples.
Here it is as a matrix: [['O', 'X', '-'], ['-' ,'X', 'O'], ['O', 'X', '-']].
Here it is as a matrix: [['O','-','X'], ['-','O','-'], ['-','X','O']].

Finding a path
This will generate the proper matrix where each row is an array of the characters and the board is the array of those rows.

Console output
Time Complexity: O(mn)
Space Complexity: O(1)
Here, m is the row length , and n is the column length. Each element is visited only once.
Rotate a matrix to the left by 90 degrees .

Matrix counterclockwise rotation
The third column of the matrix becomes the first row of the result.
The second column of the matrix becomes the second row of the result.
The first column of the matrix becomes the third row of the result.
Time Complexity: O(mn)
Space Complexity: O(1)
Here, m is the row length, and n is the column length . Each element is visited only once. The space complexity is O(1) because the original array is modified instead of creating a new array.
Function | Usage |
|---|---|
push(element) | Adds an element to the end of the array |
pop() | Removes the last element of the array |
shift() | Removes the first element of the array |
slice(beginIndex, endIndex) | Returns a part of the array from beginIndex to endIndex |
splice(beginIndex, endIndex) | Returns a part of the array from beginIndex to endIndex and modifies the original array by removing those elements |
concat(arr) | Adds new elements (from arr) into the array at the end of array |
Function | Usage |
|---|---|
for (var prop in arr) | Iterates by the index of the array element |
for (var elem of arr) | Iterates by the value of the array element |
arr.forEach(fnc) | Applies the fnc value on each element |
Finally, recall that JavaScript utilizes jagged arrays, an array of arrays, to get multidimensional array behavior. With two-dimensional arrays, two-dimensional surfaces such as a tic-tac-toe board and maze can easily be represented.
JavaScript objects are what makes the JavaScript programming language so versatile. Before diving into data structures and algorithms, let’s review how JavaScript objects work. This chapter will focus on what JavaScript objects are, how they are declared, and how their properties can be changed. In addition, this chapter will cover how JavaScript classes are implemented using prototypal inheritance.
As shown in the previous code, the title property was dynamically added in line 7 to the JavaScript object. Similarly, functions in JavaScript classes are added this way by dynamically adding them to the object.
In most strongly typed languages such as Java, the methods of a class are defined at the same time as the class. However, in JavaScript, the function has to be added as a JavaScript Object property of that class.
This class dynamically adds the sayName function in the constructor. This pattern is known as prototypal inheritance .
Prototypal inheritance is the only method of inheritance in JavaScript. To add functions of a class, simply use the .prototype property and specify the name of function.
When you use the .prototype property, you are essentially dynamically extending the JavaScript Object property of the object. This is the standard because JavaScript is dynamic and classes can add new function members as needed later. This isn’t possible for compiled languages such as Java because they will throw an error on compilation. This unique property of JavaScript lets developers take advantage of the prototypical inheritance.
To reiterate, adding functions to a class dynamically is how JavaScript implements prototypical inheritance. Functions of a class are added either in the constructor or via .prototype.
In JavaScript, unlike other object-oriented programming languages, prototypical inheritance is the preferred method of inheritance. Prototypical inheritance works by adding new functions to a JavaScript class via .prototype. Private variables are explicitly declared in Java and C++. However, a private variable is not supported in JavaScript, and to mimic the functionality of a private variable, you need to create a variable that is scoped to the constructor function. Declaring a variable as part of that object in the constructor via this.variableName automatically makes that property public.
Add an exampleKey property to an empty JavaScript object in two different ways and set it to exampleValue.
Create two classes: Animal and Dog. The Animal class should take two parameters in the constructor (name and animalType). Set them as its public properties.
In addition, the Animal class should have two functions: sayName and sayAnimalType. sayName prints name, and sayAnimalType prints animalType initialized in the constructor.
Let’s first define the Animal class and the specified required functions.
For the Dog class to inherit this, define the Dog class and then copy its prototype, as shown in the following code block:
In any program, a variable takes up some memory. In low-level programming languages such as C, the programmer must allocate and deallocate memory manually. In contrast, the V8 JavaScript engine and other modern JavaScript engines have garbage collectors that delete unused variables for the programmer. Despite this memory management done by the JavaScript engine, however, there are common pitfalls that developers can fall into. This chapter will show some basic examples of these pitfalls and present techniques to help the garbage collector minimize the key JavaScript memory problems.
A memory leak is a failure in a program to release discarded memory, causing impaired performance and sometimes even failure. Memory leaks can happen when JavaScript engines’ garbage collectors do not free memory properly.
Follow the key principles outlined in this chapter to avoid memory leaks during JavaScript development.
You might expect the clickEvent() function to use 5KB of memory since it is only referencing bar1 from the foo object. However, the truth is that it is using 10KB of memory since it has to load the whole foo object into the function’s into scope to access the bar1 property.
If a variable pointing to a DOM element is declared outside of an event callback, then it is in memory and leaks DOM if the element is deleted.
The event listener on the one element will cause the two to disappear from the web page when clicked. However, even if the DOM is deleted in the HTML, reference to it will remain if used in an event callback. When the two element is no longer in use, this is a memory leak and should be avoided.
If an object is on the global window object, it is in memory. The window object is a global object in a browser and comes with various built-in methods such as alert() and setTimeout(). Any additional objects declared as a property of window will not be cleared because window is a required object for the browser to run. Remember that any global variable declared will be set as a property of the window object.
It is good to avoid global variables whenever possible. This will help save memory.
An object is cleared when all references are cleared. Always remember to limit the amount of scope the function pulls and pass the property of an object only into functions instead of the entire object. This is because the object’s memory footprint can be very large (e.g., an array of 100,000 integers for data visualization project); if only one of the object’s properties is needed, you should avoid using the entire object as a parameter.
Although memory in JavaScript is not allocated by the programmer, there are still numerous ways to mitigate memory leaks where applicable. If the object is in reference, it is in memory. Similarly, HTML DOM elements should not be referenced once deleted. Finally, only reference objects in a function that are needed. In many cases, it is more applicable to pass in a property of the object rather than the object itself. Also, be extremely mindful when declaring a global variable.
In this chapter, exercises are about identifying memory inefficiencies and optimizing a given piece of code.
Problem: An excessive amount of memory is used in printProperty because the entire object is brought into the printProperty function. To fix this, only the property being printed should be brought in as a parameter of the function.
Problem: Global variables are used where not necessary. Albeit small, the global variables RED, GREEN, and BLUE bloat the global scope and should be moved inside the redGreenBlueCount function.
Analyze and fix memory issues for the following code.
Problem: This is the “leaking DOM” issue discussed earlier in the chapter. When elements are removed, they are still referenced by the callback function. To address this, put the one and two variables into a callback’s scope and remove the event listener after.
Answer:
This chapter introduces the concept of recursion and recursive algorithms. First, the definition of recursion and fundamental rules for recursive algorithms will be explored. In addition, methods of analyzing efficiencies of recursive functions will be covered in detail using mathematical notations. Finally, the chapter exercises will help solidify this information.

Recursion illustrated
When recursive functions are implemented incorrectly, it causes fatal issues because the program will get stuck and not terminate. Infinite recursive calls result in stack overflow. Stack overflow is when the maximum number of call stacks of the program exceeds the limited amount of address space (memory).
For recursive functions to be implemented correctly, they must follow certain rules so that stack overflow is avoided. These rules are covered next.
In recursion, there must be a base case (also referred to as terminating case). Because recursive methods call themselves, they will never stop unless this base case is reached. Stack overflow from recursion is most likely the result of not having a proper base case. In the base case, there are no recursive function calls.
The base case for this function is when n is smaller or equal to 0. This is because the desired outcome was to stop counting at 0. If a negative number is given as the input, it will not print that number because of the base case. In addition to a base case, this recursive function also exhibits the divide-and-conquer method.
In computer science, the divide-and-conquermethod is when a problem is solved by solving all of its smaller components. With the countdown example, counting down from 2 can be solved by printing 2 and then counting down from 1. Here, counting down from 1 is the part solved by “dividing and conquering.” It is necessary to make the problem smaller to reach the base case. Otherwise, if the recursive call does not converge to a base case, a stack overflow occurs.
Let’s now examine a more complex recursive function known as the Fibonacci sequence .
1, 1, 2, 3, 5, 8, 13, 21 …
How might you program something to print the Nth term of the Fibonacci sequence?
A for loop can be used to keep track of the last two elements of the Fibonacci sequence, and its sum yields the Fibonacci number.
Now, how might this be done recursively?
Base case: The base case for the Fibonacci sequence is that the first element is 1.
Divide and conquer: By definition of the Fibonacci sequence, the Nth Fibonacci number is the sum of the (n-1)th and (n-2)th Fibonacci numbers. However, this implementation has a time complexity of O(2n), which is discussed in detail later in this chapter. We will explore a more efficient recursive algorithm for the Fibonacci sequence using tail recursion in the next section.
Time Complexity: O(n)
At most, this function executes n times because it’s decremented by n-1 each time with only single recursive call.
Space Complexity: O(n)
The space complexity is also O(n) because of the stack call used for this function . This will be further explained in the “Recursive Call Stack Memory” section later in this chapter.
To conclude the rules of recursion, let’s examine another example, which is more complex.

Pascal’s triangle
Base case: The base case for Pascal’s triangle is that the top element (row=1, col=1) is 1. Everything else is derived from this fact alone. Hence, when the column is 1, return 1, and when the row is 0, return 0.
This is the beauty of recursion! Look next at how short and elegant this code is.
In Chapter 1, Big-O analysis of recursive algorithms was not covered. This was because recursive algorithms are much harder to analyze. To perform Big-O analysis for recursive algorithms, its recurrence relations must be analyzed.
In algorithms implemented iteratively, Big-O analysis is much simpler because loops clearly define when to stop and how much to increment in each iteration. For analyzing recursive algorithms, recurrence relations are used. Recurrence relations consist of two-part analysis: Big-O for base case and Big-O for recursive case.
Base case:T (n) = O(1)
Recursive case:T (n) = T (n − 1) + T (n − 2) + O(1)
Now, this relation means that since T (n) = T (n − 1) + T (n − 2) + O(1), then (by replacing n with n−1), T (n − 1) = T (n − 2) + T (n − 3) + O(1). Replacing n−1 with n−2 yields T (n − 2) = T (n − 3) + T (n − 4) + O(1). Therefore, you can see that for every call, there are two more calls for each call. In other words, this has a time complexity of O(2n).
Calculating Big-O this way is difficult and prone to error. Thankfully, there is a concept known as the master theorem to help. The master theorem helps programmers easily analyze the time and space complexities of recursive algorithms.
Given a recurrence relation of the form T (n) = aT (n/b) + O(nc) where a >= 1 and b >=1, there are three cases.
a is the coefficient that is multiplied by the recursive call. b is the “logarithmic” term, which is the term that divides the n during the recursive call. Finally, c is the polynomial term on the nonrecursive component of the equation.
Case 1:c < logb(a) then T (n) = O(n(logb(a))).
For example, T (n) = 8T (n/2) + 1000n2
Identify a, b, c:a = 8, b = 2, c = 2
Evaluate:log2(8) = 3. c < 3 is satisfied.
Result:T (n) = O(n3)
Case 2:c = logb(a) then T (n) = O(nclog(n)).
For example, T (n) = 2T (n/2) + 10n.
Identify a, b, c:a = 2, b = 2, c = 1
Evaluate:log2(2) = 1. c = 1 is satisfied.
Result:T (n) = O(nclog(n)) = T (n) = O(n1log(n)) = T (n) = O(nlog(n))
Case 3:c > logb(a) then T (n) = O(f (n)).
For example, T (n) = 2T (n/2) + n2.
Identify a,b,c: a = 2, b = 2, c = 2
Evaluate:log2(2) = 1. c > 1 is satisfied.
Result:T (n) = f (n) = O(n2)
This section covered a lot about analyzing the time complexity of recursive algorithms. Space complexity analysis is just as important. The memory used by recursive function calls should also be noted and analyzed for space complexity analysis.
When a recursive function calls itself, that takes up memory, and this is really important in Big-O space complexity analysis.

Call stack in Developer Tools

Call stack memory
Recursive functions have an additional space complexity cost that comes from the recursive calls that need to be stored in the operating system’s memory stack. The stack is accumulated until the base case is solved. In fact, this is often why an iterative solution may be preferred over the recursive solution. In the worst case, if the base case is implemented incorrectly, the recursive function will cause the program to crash because of a stack overflow error that occurs when there are more than the allowed number of elements in the memory stack.
Recursion is a powerful tool to implement complex algorithms. Recall that all recursive functions consist of two parts: the base case and the divide-and-conquer method (solving subproblems).
Analyzing the Big-O of these recursive algorithms can be done empirically (not recommended) or by using the master theorem. Recall that the master theorem needs the recurrence relation in the following form: T (n) = aT (n/b) +O(nc). When using the master theorem, identify a, b, and c to determine which of the three cases of the master theorem it belongs to.
Finally, when implementing and analyzing recursive algorithms, consider the additional memory caused by the call stack of the recursive function calls. Each recursive call requires a place in the call stack at runtime; when the call stack accumulate n calls, then the space complexity of the function is O(n).
These exercises on recursion cover varying problems to help solidify the knowledge gained from this chapter. The focus should be to identify the correct base case first before solving the entire problem. You will find all the code for the exercises on GitHub.1
To do this, keep dividing the number by 2 and each time calculate the modulus (remainder) and division.
Time Complexity: O(log2(n))
Time complexity is logarithmic because the recursive call divides the n by 2, which makes the algorithm fast. For example, for n = 8, it executes only three times. For n=1024, it executes 10 times.
Space Complexity: O(log2(n))
This is a classical recursion problem and one that is pretty hard to solve. The premise of the problem is to swap elements of the array in every possible position.

Permutation of array recursion tree
Base case: beginIndex is equal to endIndex.
When this occurs, the function should print the current permutation.
Time Complexity: O(n!)
Space Complexity: O(n!)
There are n! permutations , and it creates n! call stacks.

Flatten a dictionary recursion tree
To do this, iterate over any property and recursively check it for child properties, passing in the concatenated string name.
Time Complexity: O(n)
Space Complexity: O(n)
Each property is visited only once and stored once per nproperties.
The idea behind this one is that with two indexes (one in front and one in back) you check at each step until the front and back meet.
Time Complexity: O(n)
Space Complexity: O(n)
Space complexity here is still O(n) because of the recursive call stack. Remember that the call stack remains part of memory even if it is not declaring a variable or being stored inside a data structure.
This chapter focuses on working with sets. The concepts of sets from both a mathematical definition and on the implementation level are described and explored. Common set operations, as well as their implementations, are covered in great detail. By end of this chapter, you will understand how to use JavaScript’s native Set object to utilize set operations.
Sets are one of the most fundamental data structures. The idea of a set is simple: it is a group of definite, distinct objects. In layman’s terms, in programming, a set is a group of unordered unique (no duplicate) elements. For example, a set of integers may be {1, 2, 3, 4}. Within this, its subsets are {}, {1}, {2}, {3}, {4}, {1, 2}, {1, 3}, {1, 4}, {2, 3}, {2, 4}, {3, 4}, {1, 2, 3}, {1, 2, 4}, {1, 3, 4}, and {2, 3, 4}. Sets are important for checking and adding a unique element in O(1) constant time. The reason that sets have constant time operations is that the implementations are based on that of hash tables (covered in Chapter 11).
The native Set object has only one property: size (integer). This property is the current number of elements within the set.
The set is a powerful data structure for performing uniqueness checks. This section will cover the following key operations: insertion, deletion, and contains.
Notice that adding the duplicate element does not work for a set. As discussed in the introduction, insertion into a set occurs in constant time.
Time Complexity: O(1)
This is useful for being able to delete items in constant time in contrast to arrays where it would take O(n) time to delete an item.
Time Complexity: O(1)
Time Complexity: O(1)
In addition to the natively supported set functions, other essential operations are available; they are explored in this section.
A set is a fundamental data structure to represent unordered unique elements. In this chapter, JavaScript’s native Set object was introduced. The Set object supports insertion, deletion, and contains check, which all have a time complexity of O(1). With these built-in methods, other fundamental set operations such as intersection, difference, union, and superset check are implemented. These will enable you to implement algorithms with fast uniqueness checks in future chapters.
Operation | Function Name | Description |
|---|---|---|
Insertion | Set.add | Native JavaScript. Adds the element to the set if it’s not already in the set. |
Deletion | Set.delete | Native JavaScript. Deletes the element from the set if it’s in the set. |
Contains | Set.has | Native JavaScript. Checks whether an element exists within in the set. |
Intersection (A∩B) | intersectSets | Returns a set with common elements of set A and set B. |
Union (A∪B) | unionSet | Returns a set with all elements of set A and set B. |
Difference (A-B) | differenceSet | Returns a set with all elements. |
Time Complexity: O(n)
Space Complexity: O(n)
In an array of length n, this function has to iterate through the entire array in the worst case and also store all those elements in the set.
Given two integer arrays with some of the same values, return one array that has all the unique elements from both of the original arrays.
Time Complexity: O(n + m )
Space Complexity: O(n + m)
The time and space complexity for this algorithm is O(n + m) where n is the length of arr1 and m is the length of arr2. This is because all elements inside both arrays need to be iterated through.
Searching data and sorting through data are fundamental algorithms. Searching refers to iterating over the data structure’s elements to retrieve some data. Sorting refers to putting the data structure’s elements in order. The searching and sorting algorithms are different for every data structure. This chapter focuses on searching and sorting for arrays. By the end of this chapter, you will understand how to use common sorting and searching algorithms for arrays.
As mentioned, searching is the task of looking for a specific element inside a data structure. When searching in an array, there are two main techniques depending on whether the array is sorted. In this section, you’ll learn about linear and binary searching. Linear searches are especially flexible because they can be used with both sorted and unsorted data. Binary searches are specifically used with sorted data. However, a linear search has a higher time complexity than a binary search.
Time Complexity: O(n)

Linear search
As another example, with an array of [1,2,3,4,5] and a search term of 3, it would take three iterations to complete (1, 2, 3). The reason why this algorithm has a Big-O of O(n) is that, in the worst-case scenario, the entire array needs to be iterated. For example, if the search term is 5, it takes five iterations (1, 2, 3, 4, 5). If 6 is the search term, it goes through the entire array (1, 2, 3, 4, 5) and then returns false because it was not found.
As noted previously, a linear search algorithm like this is great because it works whether or not the array is sorted. In a linear search algorithm, every element of the array is checked. So, you should use a linear search when the array is not sorted. If the array is sorted, you can do the search faster via a binary search.
Binary search is a searching algorithm that works on sorted data. Unlike the linear search algorithm, in which every element of the array is checked, binary searches can check the middle value to see whether the desired value is greater or smaller than it. If the desired value is smaller, this algorithm can search through the smaller parts, or it can search through the bigger parts if the desired value is bigger.

Binary search in the left half of the array

Binary search in the right half of the array
The binary search algorithm is fast but can be done only if the array is sorted. It checks the middle element if that is the element that is being searched for. If the search element is bigger than the middle element, the lower bound is set to the middle element plus one. If the search element is less than the middle element, the higher bound is set to the middle element minus one.
This way, the algorithm is continuously dividing the array into two sections: the lower half and the upper half. If the element is smaller than the middle element, it should look for it in the lower half; if the element is bigger than the middle element, it should look for it in the upper half.
Binary searches are used by humans without them even knowing. An example is a phone directory that is arranged from A to Z by last name.
If you are given the task of finding someone with the last name of Lezer, one would first go to the L section and open it halfway through. Lizar is on that page; this means that the lower section contains L + [a to i] and the upper section contains L + [i to z] last names. You would then check the middle of the lower section. Laar appears, so you would now check the upper section. This process repeats until Lezer is found.
Sorting is one of the most important topics in computer science; it is faster and easier to locate items in a sorted array than in an unsorted sorted array. You can use sorting algorithms to sort an array in memory for searching later in the program or to write to a file for later retrieval. In this section, different sorting techniques will be explored. We will start with the naive sorting algorithms and then explore efficient sorting algorithms. Efficient sorting algorithms have various trade-offs that should be considered during usage.

First run of the bubble sort

The rest of the bubble sort runs
Time Complexity: O(n2)
Space Complexity: O(1)
Bubble sort is the worst type of sort because it compares every pair possible, whereas other sorting algorithms take advantage of the presorted parts of the array. Because bubble sort uses nested loops, it has a time complexity of O(n2).

Selection sort
Time Complexity: O(n2)
The time complexity for selection sort is still O(n2) because of the nested for loop .

Insertion sort
Time Complexity: O(n2)
Space Complexity: O(1)
Again, this sorting algorithm has a quadratic time complexity of O(n2) like bubble and insertion sort because of the nested for loop.
Quicksort works by obtaining a pivot and partitioning the array around it (bigger elements on one side and smaller elements on the other side) until everything is sorted. The ideal pivot is the median of the array since it will partition the array evenly but getting the median of an unsorted array linear time to compute. Hence, a pivot is typically obtained by taking the median value of the first, middle, and last elements in the partition. This sort is a recursive one and uses the divide-and-conquer methodology to break the quadratic complexity barrier and get the time complexity down to O(nlog2(n)). However, with a pivot that partitions everything on one side, the time complexity is worse case: O(n2).

Quicksort
Time Complexity: O(nlog2(n)) on average, O(n2) for worst case
Space Complexity: O(log2(n))
One downside about a quicksort algorithm is that it could potentially be O(n2) if a bad pivot is always picked . A bad pivot is one that it does not partition the array evenly. The ideal pivot is the median element of the array. In addition, a quicksort algorithm takes a bigger space complexity of O(log2(n)) compared to other sorting algorithms because of the call stack in recursion.
Use a quicksort algorithm when the average performance should be optimal. This has to do with the fact that quicksort works better for the RAM cache.
Quickselect is a selection algorithm to find the kth smallest element in an unordered list. Quickselect uses the same approach as a quicksort algorithm. A pivot is chosen, and the array is partitioned. Instead of recursing both sides like quicksort, however, it recurses only the side for the element. This reduces the complexity from O(nlog2(n)) to O(n).
Time Complexity: O(n)

Mergesort
The merging function works by taking the two arrays (left and right) and merging them into one resultant array. The elements need to be compared as they get merged to preserve order.
Time Complexity: O(nlog2(n))
Space Complexity: O(n)
Mergesort has a large space complexity of O(n) because of the need to create n number of arrays to be merged later. Use mergesort when a stable sort is needed. A stable sort is one that’s guaranteed not to reorder elements with identical keys. Mergesort is guaranteed to be O(nlog2(n)). A disadvantage of mergesort is that it uses O(n) in space.

Count sort
Time Complexity: O(k+n)
Space Complexity: O(k)
Use count sort when you’re sorting integers with a limited range. This will be the fastest sort for this case.
JavaScript has a built-in sort() method for an array object, which sorts elements by ascending order. To use it, there is an optional parameter that you can pass in a comparator function.
In the previous example, notice that numbers starting with 1 came first (1, 12), then numbers starting with 2, and so forth. This is because no comparator function was passed and JavaScript converted the elements into a string and sorted it according to the alphabet.
The sort() function can be useful when you need a quick way to sort something without implementing it yourself.
There are two ways to search inside an array: linear search and binary search. Binary search is faster with O(log2(n)) time complexity, while linear search has O(n) time complexity. However, the binary search can be performed only on a sorted array.
Algorithm | Time Complexity | Space Complexity |
|---|---|---|
Quicksort | O(nlog2(n)) | O(nlog2(n)) |
Mergesort | O(nlog2(n)) | O(nlog2(n)) |
Bubble sort | O(n2) | O(n2) |
Insertion sort | O(n2) | O(n2) |
Selection sort | O(n2) | O(n2) |
Count sort | O(k + n) | O(k) |
Time Complexity: O(n)
This is essentially a linear search since it has to linearly check one by one the value for the square root.
Time Complexity: O(log2(n))
Bonus: Find a Square Root of a Float
Time Complexity: O(n2)
Space Complexity: O(1)
There is a lot of checking, and hence it takes quadratic time.
Time Complexity: O(n)
Space Complexity: O(n)
This algorithm cuts the time complexity to O(n) but takes O(n) space as well to store items into the store object.
Time Complexity: O(log2n)
Space Complexity: O(1)
Examples
As shown, there’s a lot of flexibility with these comparators, and they can be used for sorting without needing to implement a sort yourself.
Create a function that generates an object of words (as keys) and the number of times the words occur in a string ordered by highest to lowest occurrences.
Here’s some example input: practice makes perfect. get perfect by practice. just practice.
Time Complexity: O(nlog2(n))
Space Complexity: O(n)
Time complexity is limited by the sorting algorithm that the JavaScript engine uses. Most use either mergesort or quicksort, which are both O(nlog2(n)).
A hash table is a fixed-sized data structure in which the size is defined at the start. This chapter explains how hash tables work by focusing on hashing, the method of generating a unique key. By the end of this chapter, you will understand various hashing techniques and know how to implement a hash table from scratch.

Simple hash table overview
A hash table contains two main functions: put() and get() . put() is used for storing data into the hash table, while get() is used for retrieving data from the hash table. Both of these functions have a time complexity of O(1).
In a nutshell, a hash table is analogous to an array whose index is calculated with a hashing function to identify a space in memory uniquely.
Deterministic: Equal keys produce equal hash values.
Efficiency: It should be O(1) in time.
Uniform distribution: It makes the most use of the array.
The first technique for hashing is to use prime numbers. By using the modulus operator with prime numbers, a uniform distribution of the index can be guaranteed.

Hash table of size 11, with all empty elements

Hash table after inserting the value pairs
This is a problem because 7 already exists in the index of 7 and causes an index collision. With a perfect hashing function, there are no collisions. However, collision-free hashing is almost impossible in most cases. Therefore, strategies for handling collisions are needed for hash tables.
To work around occurring collisions, the probing hashing technique finds the next available index in the array. The linear probing technique resolves conflicts by finding the next available index via incremental trials, while quadratic probing uses quadratic functions to generate incremental trials.

Hash table 1 after using linear probing
However, now when the get(key) function is used, it has to start at the original hash result (7) and then iterate until 18 is found.
The main disadvantage of linear probing is it easily creates clusters, which are bad because they create more data to iterate through.

Linear probing (on top) and quadratic probing (on bottom)
Different: It needs to be different to distribute it better.
Efficiency: It should still be O(1) in time.
Nonzero: It should never evaluate to zero. Zero gives the initial hash value.
hash2(x) = R − (x % R)
i * hash2(x)
7, “hi”
20, “hello”
33, “sunny”
46, “weather”
59, “wow”
72, “forty”
85, “happy”
98, “sad”
This result is more uniformly distributed than the result from linear probing. It would be easier to see with a bigger array size and more elements.
Again, double-hashing results in a more uniformly distributed array than the result from linear probing. Both quadratic probing and double-hashing are great techniques to reduce the number of collisions in a hash table. There are collision resolution algorithms far more advanced than these techniques, but they are beyond the scope of this book.
A hash table is a fixed-sized data structure in which the size is defined at the start. Hash tables are implemented using a hash function to generate an index for the array. A good hash function is deterministic, efficient, and uniformly distributive. Hash collisions should be minimized with a good uniformly distributive hash function, but having some collisions is unavoidable. Hash collision-handling techniques include but are not limited to linear probing (incrementing the index by 1), quadratic probing (using a quadratic function to increment the index), and double-hashing (using multiple hash functions).
The next chapter explores stacks and queues, which are dynamically sized data structures.
This chapter covers stacks and queues; both are versatile data structures commonly used in the implementation of other, more complex data structures. You will learn what stacks and queues are, how and when they are used, and how to implement them. Finally, the exercises will help you to understand these concepts as well as when to apply stacks and queues to an algorithmic problem.

Stack, LIFO
In JavaScript, arrays have methods that define the stack class: pop and push (as discussed in Chapter 5). With this, a stack can be easily implemented.
Let’s first consider “peeking” at the most recently added element. This can be done simply by using the largest index of the array.
Time Complexity: O(1)
Time Complexity: O(1)
Time Complexity: O(1)
Accessing specific elements in a data structure is important. Here, let’s take a look at how to access an element based on order.
Time Complexity: O(n)
Search will be implemented in a similar way .
Time Complexity: O(n)

Queue, FIFO
In JavaScript, arrays have methods that define the queue class: shift() and push() (as discussed in Chapter 5). Recall that the shift() method on an array in JavaScript removes and returns the first element of the array. Adding to a queue is commonly known as enqueuing , and removing from a queue is known as dequeuing . shift() can be used for the dequeue, and .push() can be used for the enqueue.
Time Complexity: O(1)
Time Complexity: O(n)
Because the shift() implementation removes the element at zero indexes and then shifts remaining indexes down consecutively, all other elements in the array need to have their indexes altered, and this takes O(n). With a linked-list implementation, as covered in Chapter 13, this can be reduced to O(1) .
Time Complexity: O(n)
Time Complexity: O(n)
Access | Search | Peek | Insertion | Deletion | |
|---|---|---|---|---|---|
Queue | O(n) | O(n) | O(1) | O(1) | O(n)3 |
Stack | O(n) | O(n) | O(1) | O(1) | O(1) |
All the code for the exercises can be found on GitHub.4
Stack Using Queues
A queue can be made with two stacks. A queue is a data structure that returns the first-added element with the dequeue() method. A stack is a data structure that returns the last-added element via pop. In other words, a queue removes elements in the reverse direction of a stack.
For example, examine a stack array with [1,2,3,4,5].
To reverse the order, all of the elements could be pushed onto a second stack and pop that second stack. So, the second stack array will look like this: [5,4,3,2,1].
Queue Using Stacks
The cashier requires a customer name and order item for the order.
The customer who was served first is processed first.
addOrder(customer): Enqueues a customer object to be processed by deliverOrder()
deliverOrder(): Prints the name and order for the next customer to be processed
((())) is a valid parentheses set, while ((() and ))) are not. A stack can be used to check the validity of parentheses by storing the left parenthesis and using push and triggering pop when the right parenthesis is seen.
Time Complexity: O(n)
This algorithm processes a string character by character. Hence, its time complexity is O(n), where n is the length of the string.
Time Complexity: O(n2)
This algorithm involves a reshuffling of the elements between two stacks, which in the worst possible case takes O(n2), where n is the number of elements to be sorted.
This chapter will cover linked lists. A linked list is a data structure in which each node points to another node. Unlike arrays, which have a fixed size, a linked list is a dynamic data structure that can allocate and deallocate memory at runtime. By the end of this chapter, you will understand how to implement and work with linked lists.
There are two types of linked lists discussed in this chapter: singly and doubly linked lists. Let’s examine the singly linked list first.

Singly linked list
The start of the linked list is referred to as the head . This property defaults to null before inserting any element into the linked list.
Time Complexity: O(1)
This is a constant time operation; no loops or traversal is required.

Interior node removal from a singly linked list
Time Complexity: O(n)
In the worst case, the entire linked list must be traversed.
Time Complexity: O(n)
Like with the deletion operation, in the worst case, the entire linked list must be traversed.

Doubly linked list example with five nodes
Time Complexity: O(1)
Time Complexity: O(1)
Time Complexity: O(1)
Time Complexity: O(1)
Time Complexity: O(n)
Time Complexity: O(n)
Although the time complexity for search is the same as the singly linked list’s search, only the doubly linked list can search bidirectionally (using prev or next). This means that if given a reference to a doubly linked list node, doubly linked lists can perform a full search, but a singly linked list is limited to only its next pointers .
The linked list data structure works by each node having a next pointer (and previous, or prev, pointer if doubly linked) to a different node. Insertion for both singly and doubly linked lists has a constant time complexity of O(1). The time complexity of deleting from the head of the singly and doubly linked lists is O(1) as well. However, searching for an item in both singly and doubly linked list takes O(n) time. Doubly linked lists should be used over singly linked lists when bidirectional traversal/search is required. Furthermore, doubly linked lists allow you to pop from either the tail or the head of the linked list for a flexible and fast O(1) operation.
You can find all the code for the exercises on GitHub.2
Time Complexity: O(n)
Space Complexity: O(1)
To fully reverse a linked list, the entire N elements of the linked list must be traversed.
Time Complexity: O(n2)
Space Complexity: O(n)
Time Complexity: O(n)
Space Complexity: O(n)
Use of the JavaScript Object as a hash table to store and check for seen elements cuts it down to O(n) but O(n) in space as extra memory is required for the hash table.
Caching is the process of storing data into temporary memory so that it can be easily retrieved for later use if it is required again. As an example, a database system keeps data cached to avoid rereading the hard drive, and a web browser caches web pages (images and assets) to avoid redownloading the contents. Put simply, in caching, the goal is to maximize hits (an item is in the cache when requested) and minimize misses (an item is not in the cache when requested).
In this chapter, two caching techniques will be discussed: least frequently used (LFU) and least recently used (LRU) caching.
The concept of caching comes from the world of operating systems. You can read more about it in a lecture presentation1 by Jeff Zarnett from the University of Waterloo.
Temporal locality : A memory location that has been recently accessed is likely to be accessed again.
Spatial locality : A memory location near one that has recently been accessed is likely to be accessed again.
The optimal caching algorithm would be able to replace the part of the cache that will be used most distantly in the future with the new element to be inserted. This will require, for each item, calculating how many time in the future that item will be accessed. It should be obvious to you that this is impossible to implement because it requires looking into the future.
Least frequently used (LFU) caching is a caching algorithm used by the operating system to manage memory. The system tracks the number of times a block is referenced in memory. By design, when the cache exceeds its limit, the system deletes the item with the lowest reference frequency. The easiest implementation of the LFU cache is assigning a counter to every block loaded into the cache and incrementing a counter every time a reference is made to that block. When the cache exceeds its limit, the system searches for the block with the lowest counter and removes it from the cache.
Although LFU caching seems like an intuitive approach, it is not ideal when an item in memory is referenced repeatedly for a short amount of time and not accessed again. The frequency for that block is high because of its repeated reference, but this forces the system to delete other blocks that may be used more frequently outside the short block of time. In addition, new items in the system are susceptible to being deleted quickly because of their lower frequency of being accessed. Because of these issues, LFU is uncommon, but some hybrid systems utilize the core LFU concept. Examples of a such system are mobile keyboard apps. Suggested words appear on the keyboard apps, and it makes sense to implement this using LFU caching since the user likely uses the same words often. The frequency of a word would a great metric to see whether the word should exist in the cache.
Implementing set for the LFU has a few steps. There are two cases: insert the new item and replace an old item. When inserting a new item, a new node is created. If the cache is not full, it can be inserted into the freq’s doubly linked list of frequency 1. If the capacity is full, the tail item in the doubly linked list of frequency is deleted, and then the new node is inserted.
Least recently used (LRU) caching is a caching algorithm that removes the oldest (least recently used) items first, so the item replaced is the oldest accessed item. When an item in the cache is accessed, that item moves to the back (newest in the order) of the list. When a page not found in the cache is accessed, the front item (or oldest in the order) is removed, and the new item is put at the back (newest in the order) of the list.
The implementation of this algorithm requires keeping track of which node was used when. To accomplish this, the LRU cache is implemented using a doubly linked list and hash table.
A doubly linked list is needed to keep track of the head (the oldest data). A doubly linked list is required because of the most recently used requirement. Each time new data is inserted, the head moves up until the size is exceeded. Then the oldest data is evicted.

LRU cache
Algorithm | Comment |
|---|---|
Optimal | Impossible to implement |
Least frequently used | Bad for temporal locality |
Least recently used | Uses doubly-linked + hashmap |
A general tree data structure is composed of nodes with children nodes. The first/top node is called the root node . This chapter will explore many different types of trees such as binary trees, binary search trees, and self-balancing binary search trees. First, this chapter will cover what trees are and how they are structured. Then, it will cover methods of traversing the tree data structure in detail. Finally, you will learn about binary search trees and self-balancing binary search trees to understand how to store easily searchable data.

Generalized tree with any number of children

Binary tree
Traversal through an array is simple: you access the tree using the index and increment the index until the index reaches the size limit. With trees, the left and right pointers have to be followed in order to go through every element in the tree. There are various ways to do this, of course; the most popular traversal techniques are pre-order traversal, post-order traversal, in-order traversal, and level-order traversal.
All the code for tree traversals is available on GitHub.1

Pre-order traversal
Here is the result: [42, 41, 10, 40, 50, 45, 75].

In-order traversal
Here is the result of this traversal : [10, 41, 40, 42, 45, 50, 75].

Post-order traversal
Here is the result : [10, 40, 41, 45, 75, 50, 42].

Level-order traversal
Here is the result: [42, 41, 50, 10, 40, 45, 75].
If you know you need to explore the roots before inspecting any leaves, choose pre-order traversal because you will encounter all the roots before all of the leaves.
If you know you need to explore all the leaves before any nodes, choose post-order traversal because you don’t waste any time inspecting roots when searching for leaves.
If you know that the tree has an inherent sequence in the nodes and you want to flatten the tree into its original sequence, then you should use an in-order traversal. The tree would be flattened in the same way it was created. A pre-order or post-order traversal might not unwind the tree back into the sequence that was used to create it.
Time Complexity: O(n)
The time complexity of any of these traversals is the same because each traversal requires that all nodes are visited.
Binary search trees (BSTs) also have two children, left and right. However, in a binary search tree, the left child is smaller than the parent, and the right child is bigger than the parent. BSTs have this structure because this property enables for searching, inserting, and removing specific values with O(log2(n)) time complexity.

Binary search tree

Unbalanced binary search tree
Time Complexity (for balanced trees): O(log2(n))
Time Complexity (for unbalanced trees): O(n)
Time complexity is dependent on the height of the binary search tree.
Case 1: The node has no children.
This is the simplest case. If the node has no child, return null. That node has been deleted now.
Case 2: The node has one child.
If the node has one child, simply return the existing child. That child has now bubbled up and replaced the parent.
Case 3: The node has two children.
If the node has two children, either find the maximum of the left subtree or find the minimum of the right subtree to replace that node.
Time Complexity (for balanced tree): O(log2(n))
Time Complexity (for unbalanced trees): O(n)
Time complexity for deletion is also O(log2(n)) because at most that’s the height that will need to be traversed to find and delete the desired node.
Time Complexity (for balanced tree): O(log2(n))
Time Complexity (for unbalanced trees): O(n)
Note that all of the operations’ time complexities are equal to the height of the binary tree search. With unbalanced binary search trees, the time complexity is high. To address this, there are families of binary search trees that ensure the height is balanced. One example of such self-balancing trees is an AVL tree.
AVL trees rotate their children to maintain balance after insertion.

Rotate left before

Rotate left after

Rotate right before

Rotate right after
If an AVL tree is still unbalanced after one rotation, it has to rotate twice for full balance.

A situation where rotating right and then rotating left is appropriate

Rotate right first

Rotate left after

A situation where rotating left and then rotating right is appropriate

Rotate left first

Rotate right after
Time Complexity: O(nlog2(n))
Space Complexity: O(nlog2(n))
Space complexity is from the recursive call stacks in memory.
The time complexity and space complexity are both O(nlog2(n)) because AVL trees are balanced. The space complexity is from the recursive call stacks in memory.

AVL result

BST result
Clearly, this is a skewed binary search tree that is completely unbalanced. At this point, it looks like a linked list. Once the tree becomes completely unbalanced like this, it has a linear time complexity for deletion, insertion, and search instead of logarithmic time.
Operation | Best (If Balanced) | Worst (If Completely Unbalanced) |
|---|---|---|
Deletion | O(log2(n)) | O(n) |
Insertion | O(log2(n)) | O(n) |
Search | O(log2(n)) | O(n) |
You can find all the code for the exercises on GitHub.2
The logic for this one is actually fairly simple but hard to notice at first.

Lowest common ancestor, example 1

Lowest common ancestor, example 2
Time Complexity: O(log2(n))

Mirror trees
Their root node’s key must be the same.
The left subtree of root of a and the right subtree root of b are mirrors.
The right subtree of a and the left subtree of b are mirrors.
This chapter will introduce heaps. A heap is an important data structure that returns the highest or lowest element in O(1) time. This chapter will focus on explaining how heaps are implemented as well as how to work with them. One example is heap sort, which is a sorting algorithm based on heaps.
A heap is a type of tree-like data structure in which the parent is bigger than its children (if max-heap) or smaller than its children (if min-heap). This property of the heap makes it useful for sorting data.
Heaps, unlike other tree data structures, use an array to store data instead of having pointers to their children. A heap node’s children’s positions (indices) in the array can be calculated easily. This is because the parent-child relationship is easily defined with a heap.
There are many types of heaps that have different numbers of children. In this chapter, only binary heaps will be considered. Since a heap uses an array to store the data, the indices of the array define the order/height of each element. A binary heap can be built by placing the first array element as the root and then filling each left and right element in order.

Heap indices
There are two types of binary heaps: max-heap and min-heap. In max-heap, the root node has the highest value, and each node’s value is greater than its children. In min-heap, the root node has the lowest value, and each node’s value is smaller than its children.
Heaps can store any values of any type: strings, integer, and even custom classes. As covered in Chapters 3 and 4, strings and integer value comparisons are handled natively by JavaScript (e.g., 9 is greater than 1, z is greater than a). However, for custom classes, the developer needs to implement a way to compare two classes. This chapter will look at heaps that store integer values only.

Max-heap
Here is the array for the max-heap shown in Figure 16-2: [100, 19, 36, 17, 3, 25, 1, 2, 7].

Min-heap
Here is the array for the max-heap shown in Figure 16-3: [1, 2, 3, 17, 19, 36, 7, 25, 100].

Heap relationship
The size function is another helper that returns the size (number of elements) of the heap.
When elements are added or removed, the structure of the heap must remain (the node being greater than its children for a max-heap or smaller than its children for a min-heap).
This requires items to swap and “bubble up” to the top of the heap. Similar to bubbling up, some items need to “bubble down” to their rightful position in order to keep the structure of the heap. Percolation takes O(log2(n)) in time.
Insert 12 as the first node (Figure 16-5).

The min-heap root node
Insert a new 2 node (Figure 16-6).

The newest node is smaller than the parent
The 2 node bubbles up because it is smaller than 12 and hence should be on the top of the min-heap (Figure 16-7).

The smaller node has bubbled up to the parent position
Insert a new 23 node in the second child position (Figure 16-8).

The larger 23 node remains in place in the min-heap structure
Insert 4 in the heap, as in Figure 16-9.

The new node in the min-heap is smaller than the one above it
12 is swapped with 4 to maintain the min-heap structure (Figure 16-10).

The smaller 4 node has bubbled up to maintain the min-heap structure
Insert 13, as in Figure 16-11.

The newest and larger 13 node remains in place
Here is the array content for this heap: [2, 4, 23, 12, 13].
A max-heap implementation differs only in the comparators. For bubbling down, the max-heap node swaps with one of its children if the child is greater. Likewise, for bubbling up, the newest node swaps with its parent if its parent is smaller than the new node.
Insert the first node, which is 12 (Figure 16-12).

The first max-heap node
Insert a new 2 node (Figure 16-13).

The new smaller node remains in place in the max-heap structure
Insert 23, as in Figure 16-14.

The new child node is larger than the parent
The 23 node “bubbles” to the top to maintain max-heap structure (Figure 16-15).

The new larger node is swapped with the smaller 12
Insert 4, as in Figure 16-16.

The new node is larger than the one above it
To maintain the max-heap structure, 4 bubbles up, and 2 bubbles down (Figure 16-17).

The 4 and 2 nodes swap places
Insert 13, as in Figure 16-18.

The new node is larger than the one above it
Because of the max-heap structure, 13 and 4 swap positions (Figure 16-19).

Percolation restores the max-heap structure
Here is the array content for this heap: [23, 13, 12, 2, 4] .
Now that heap classes have been created, sorting with a heap is fairly straightforward. To get a sorted array, simply call .pop() on the heap until it is empty and store the stored popped objects. This is as known as a heap sort. Since percolation takes O(log2(n)), and sorting must pop n number of elements, the time complexity for a heap sort is O(nlog2(n)), like quicksort and mergesort.
In this section, we will first do an ascending sort implemented using a min-heap and then a descending sort implemented using a max-heap.

Min-heap sort after all items added

Min-heap sort: popping 2 out

Min-heap sort: popping 4 out

Min-heap sort: popping 12 out
The last node (where 13 used to be) is removed, and then 13 is placed on the top. Through the percolation process, 13 moves down to after the left child of 12 since it’s bigger than both 4 and 13.

Max-heap sort after all items are added

Max sort: popping 23 out

Max sort: popping 13 out

Max sort: popping 12 out
Node | Index |
|---|---|
(self) | N |
Parent | (N-1) / 2 |
Left child | (N*2) + 1 |
Right child | (N*2) + 2 |
Operation | Time Complexity |
|---|---|
Deletion (leads to “bubble down”) | O(log2(n)) |
Insertion (leads to “bubble up”) | O(log2(n)) |
Heap sort | O(n log2(n)) |
You can find all the code for the exercises on GitHub.1
Since this question is in this chapter, that’s already a big hint for approaching it. In theory, the solution is fairly simple. Have one min-heap and one max-heap, and then retrieving the median takes only O(1).
For example, let’s have a stream of the following integers: 12, 2, 23, 4, 13.
Time Complexity: O(klog2(n))
Here, n is the size of the array since each .pop costs O(log2(n)), which has to be done k times.
Space Complexity: O(n)
O(n) in memory is needed to store the heap array.
This chapter covers graphs. Graphs are a versatile way of representing connections between objects. In this chapter, you will learn graph basics, including fundamental terminology and graph types. The chapter will also cover working with these different graph types and methods of representing graphs in data structures that have already been explored. Finally, algorithms for traversing, searching, and sorting graphs are explored to solve problems such as finding the shortest path between two graph nodes.
Application | Item | Connection |
|---|---|---|
Web site | Web page | Links |
Map | Intersection | Road |
Circuit | Component | Wiring |
Social media | Person | “Friendship”/connection |
Telephone | Phone number | Landline |

Two examples of graphs
Vertex: A vertex is the node from which graphs are formed. In this chapter, a node will be noted as V for Big-O analysis. A vertex is represented using a circle, as shown in Figure 17-2.
Edge: An edge is the connection between nodes in a graph. Graphically, it is the “line” between the vertices. It will be noted as E for Big-O analysis. An edge is represented using a line, as shown in Figure 17-2.

Vertices and edges
Degree of vertex: The degree of a vertex refers to the number of edges on that vertex (node).
Sparse graph: A graph is considered sparse when only a small fraction of possible connections exist between vertices (see Figure 17-3).

Sparse graph
Dense graph: A graph is considered dense when there are a lot of connections between different vertices (see Figure 17-4).

Dense graph
Cyclical graph: A directed graph is considered cyclical if there is a path that travels from a vertex and back to itself. For example, in Figure 17-5, B can follow the edge to C and then D and then E and then to B again.

Graph with a cycle on B

Graph without a cycle
Weights: Weights are values on the edges. Weights can signify various things depending on the context. For example, weights on a directed graph can represent the distance required to get from node A to B, as shown in Figure 17-7.

Directed graph with weights

Undirected graph with weights

Graph (left), adjacency list (middle), and adjacency matrix (right)
So far, the concepts and definitions of graphs have been discussed. Now, let’s actually start implementing these ideas into code and learn how to add and remove edges and vertices.

The first undirected graph
Continuing with the same example, let’s implement the functions for removing edges and vertices for the graph class.

Vertex 5 removed

Vertex 1 removed

Edge between 2 and 3 removed

Directed graph
In this example, the E node can “travel” to the D node, and the D node can travel only to the C node.

Adding A to B

Adding B to C

Adding C to A
A graph can be traversed in multiple ways. The two most common approaches are breadth-first search and depth-first search. Similarly to how different tree traversal techniques were explored, this section will focus on these two traversal techniques and when to use each of them.

Level-order traversal for binary search tree

Breadth-first search graph
Similar to the level-order traversal for the tree data structure, a queue is needed for a BFS.
Time Complexity: O(V + E)
The time complexity is O(V + E), where V is the number of vertices and E is the number of edges. This is because the algorithm has to go through every edge and node to traverse the whole graph.

The earlier undirected graph example
Applying the BFS to the graph, the following is printed: 1, 2, 5, 3, 4.

Breadth-first search, part 1

Breadth-first search, part 2
Depth-first search (DFS) refers to a search algorithm in a graph that focuses on traversing deep into one connection before visiting the other connections.

Post-order traversal

Depth-first search graph
Notice how E is visited last. This is because the search visits all the nodes connected to C in depth before visiting E.
Similar to the pre-post, and in-order traversal for the tree data structure, recursion is used to go deep into a node until that path is exhausted.
Time Complexity: O(V + E)
The time complexity is O(V + E) where V is the number of vertices and E is the number of edges. This is because the algorithm has to go through every edge and node to traverse the whole graph. This is the same time complexity as the BFS algorithm.

The earlier graph example from Figure 17-20
Applying the DFS to the graph, the following is printed: 1, 2, 3, 4, 5.

Depth-first search, part 1

Depth-first search, part 2
Now that we have covered the basics of graphs and how to traverse them, we can discuss weighted edges and Dijkstra’s algorithm, which employs shortest path searches.
Recall that edges in a graph represent a connection between the vertices. If edges establish a connection, weight can be assigned to that connection. For example, for a graph that represents a map, the weights on the edges are distances.

Graph representation of five cities
The most important question for weighted edge graphs is, what is the shortest path from one node to another? There are series of shortest path algorithms for graphs. The one we discuss is Dijkstra’s algorithm.

Dijkstra stage 1: everything marked as infinity

Dijkstra stage 2: B and C processed

Dijkstra stage 3: all nodes now processed
Time Complexity: O(V2 + E)
The algorithm here is similar to the BFS algorithm but requires the _extractMin method , which is O(n) in time complexity. Because of this, the time complexity is O(V2 + E) because all neighbor vertices of the node currently being traversed have to be checked during the _extractMin method. This algorithm can be improved using a priority queue for the extract min, which would yield O(log2(V )) _extractMin and hence yield an overall time complexity of O(E + V) * O(log2(V)) = O(E log2(V)). This can be even more optimized by using a Fibonacci heap, which has constant time to compute _extractMin. However, for simplicity, neither a Fibonacci heap nor a priority queue was used for this demonstration.
For a directed graph, it can be important to know which node should be processed first for various applications. An example of this is a task scheduler where one task depends on a previous task being done. Another example is a JavaScript library dependency manager where it has to figure out which libraries to import before others. The topological sorting algorithm implements this. It is a modified DFS that uses a stack to record the order.

Topological sort
Time Complexity: O(V + E)
Space Complexity: O(V)
The topological sort algorithm is simply DFS with an extra stack. Therefore, the time complexity is the same as DFS. Topological sorting requires O(V) in space because it needs to store all the vertices in the stack. This algorithm is powerful for scheduling jobs from given dependencies.
This chapter discussed different types of graphs, their properties, and how to search and sort them. A graph, composed of vertices and connected via edges, can be represented as a data structure in many different ways. In this chapter, an adjacency list was used to represent the graph. If the graph is dense, it is better to use a matrix-based representation of a graph instead. In a graph’s edges, weights signify the importance (or the lack thereof) of the connected vertices. Moreover, by assigning weights to edges, Dijkstra’s shortest path algorithm was implemented. Finally, graphs are versatile data structures with various use cases and interesting algorithms.
Property | Description |
|---|---|
Dense | There are a lot of connections between different vertices. |
Sparse | Only a small fraction of possible connections exist between vertices. |
Cyclical | There is a path that takes vertices back to themselves. |
Uncyclical | There is a no path such that vertices can be taken back to themselves. |
Directed | Graphs have a direction between edges. |
Undirected | Graphs do not have a direction between edges. |
Algorithm | Description/Use Case | Time Complexity |
|---|---|---|
BFS | Traverses the graph by visiting neighbor nodes one level at a time | O(V + E ) |
DFS | Traverses the graph by going deep into each neighbor node one at a time | O(V + E ) |
Dijkstra | Finds the shortest path from one vertex to the rest of the other vertices | O(V2+ E ) |
Topological Sort | Sorts the directed graph; for job scheduling algorithms | O(V + E ) |
This chapter will cover more advanced string algorithms than the previous chapters have discussed. They should be easier to understand now that you have learned about some other data structures. Specifically, this chapter will focus on string searching algorithms.

Trie of Sammie, Simran, Sia, Sam
Time Complexity: O(W )
Space Complexity: O(N*M)
Time complexity is O(W) for all operations (insert, search, delete), where W is the length of the string being searched because each character in the string is checked. The space complexity is O(N*M), where N is the number of words inserted into the trie and M is the length of the longest character. Hence, a trie is an efficient data structure when there are multiple strings with common prefixes. For searching one specific string pattern in one specific string, a trie is not efficient because of the additional memory required to store the strings in the tree-like structure.
For a pattern search in a single target string, the Boyer–Moore algorithm and the Knuth–Morris–Pratt (KMP) algorithm are useful and are covered later in this chapter.

Find tool commonly seen in many applications

Brute-force pattern match iterations

Boyer–Moore Skipping Indices
Pattern | Bad Match Table |
|---|---|
jam | {j: 2, a: 1, m: 3} |
data | {d: 3, a: 2, t: 1} |
struct | {s: 5, t: 4, r: 3, u: 2, c: 1} |
roi | {r: 2, o: 1, i: 3} |
Best Case:
In the best case, all the characters in the pattern are the same, and this consistently produces shifts by T, where T is the length of the pattern. Hence, O(W/T) is the best time complexity where W is the string where the pattern is being searched. The space complexity is O(1) since only 1 value is stored into the bad match table.
Time Complexity: O(T/W)
Space Complexity: O(1)
Worst Case:
In the worst case, the string has the pattern at the end of the string, and the preceding part is all unique characters. An example of this is a string of abcdefgxyz and pattern of xyz. In this case, T*W string comparisons are done.
Time Complexity: O(T*W)
Space Complexity: O(T)
All the characters in the pattern and the string are the same. An example of such a case is the string bbbbbb and the pattern bbb. This case cannot use the skip mechanism to its fullest because the index will always be incremented by 1. Space complexity in this case is T because the pattern could have all unique characters.
Chapter 4 discussed the native String.prototype.indexOf function. A naive implementation of the String.prototype.indexOf function was included as an exercise for that chapter. A better (faster) implementation uses the Knuth–Morris–Pratt (KMP) string search algorithm. The following implementation of the KMP algorithm returns all indices where the pattern is present.
The KMP string searching algorithm searches for occurrences of the “word” W within an input “text,” which is T, by utilizing the observation that the occurrence of a mismatch contains sufficient information about where the next match could begin. This helps to skip re-examination of previously matched characters. A prefix array has to be built to indicate how many indices it has to backtrack to get the same prefix. For the string ababaca, the prefix building looks like the following:
array index 0 1 2 3 4 5 6
character a b a b a c a
prefix array 0
The character is b.
The previous prefix array value, prefix[0], is 0.
Compare index 0 to the current index: a (at index = 0) and b (at index = 1) mismatch.
Array index 0 1 2 3 4 5 6
Character a b a b a c a
Prefix array 0 0
The character is a.
The previous prefix array value, prefix[1], is 0.
Compare the index and to the current index: a (at index = 0) and a (at index = 2) match.
Array index 0 1 2 3 4 5 6
Character a b a b a c a
Prefix array 0 0 1
The character is b.
The previous prefix array value, prefix[2], is 1.
Compare index 1 and the current index: b (at index = 1) and b (at index = 3) match.
Array index 0 1 2 3 4 5 6
Character a b a b a c a
Prefix array 0 0 1 2
The character is a.
The previous prefix array value, prefix[3], is 2.
Compare index 2 and the current index: a (at index = 2) and a (at index = 4) match.
Array index 0 1 2 3 4 5 6
Character a b a b a c a
Prefix array 0 0 1 2 3
The character is c.
The previous prefix array value, prefix[4], is 3.
Compare index 3 and the current index: b (at index = 3) and c (at index = 5) mismatch.
Array index 0 1 2 3 4 5 6
Character a b a b a c a
Prefix array 0 0 1 2 3 0
The character is c.
The previous prefix array value, prefix[5], is 0.
Compare from index 0 and current index: a (at index = 0) and a (at index = 5) match.
Array index 0 1 2 3 4 5 6
Character a b a b a c a
Prefix array 0 0 1 2 3 0 1
Time Complexity: O(W)
Space Complexity: O(W )
Preprocessing a word of length W requires both O(W) time and space complexity.
Time Complexity: O(W + T )
Here, W is the “word” in the T (the main string being searched).
The Rabin–Karp algorithm is based on hashing to find the specified pattern in text. While KMP is optimized to skip redundant checks during the search, Rabin–Karp seeks to speed up the equality of the pattern of the substring via a hash function. To do this efficiently, the hash function must be O(1). Specifically for the Rabin-Karp search, the Rabin fingerprint hashing technique is used.
The Rabin fingerprint is calculated via the following equation: f(x) = m0 + m1x + … + mn-1 xn-1 where n is the number of characters being hashed and x is some prime number.
sa: f(x) = m0 + m1x = 115 + (97)*(101) = 9912
am: f(x) = m0 + m1x = 97 + (109)*(101) = 11106
me: f(x) = m0 + m1x = 109 + (101)*(101) = 10310
Preprocessing Time Complexity: O(W )
The preprocessing time complexity W is the length of the “word.”
Matching Time Complexity: O(W + T )
At most, this algorithm iterates through the sum of length T and length W, where T is the string being searched for.
The Rabin–Karp algorithm can be used for detecting plagiarism. With a source material, this algorithm can search through a paper submission for instances of phrases and sentences from the source material (and ignoring grammar details like punctuation by omitting punctuation characters during the preprocessing phase). This problem is impractical for single-search algorithms because of the large set of sought (input) phrases and sentences. The Rabin–Karp algorithm is also used in other string matching applications such as looking for a specific sequence in large DNA data.
Trie is great for multiple searches and prefix pattern matching.
Boyer–Moore, with assumption that the absence of a match at the end means no need to match the beginning, tries to match the last character of the pattern instead of the first; this allows large “jumps” (spaces between indexes) and works better when the text is larger.
The KMP algorithm searches for occurrences of the pattern in a string by observing that when a mismatch occurs, the pattern itself has sufficient information to determine the index in the string where the next match could begin. Hence, the KMP algorithm is better for small sets.
Algorithm | Preprocessing Time Complexity | Matching Time Complexity | Space Complexity |
|---|---|---|---|
Naive | None | O(W ∗ T ) | None |
Boyer–Moore | O(W + T ) | O(T /W ) best case O(W ∗ T) worst case | O(1) |
KMP | O(W ) | O(T ) | O(W ) |
Rabin–Karp | O(W ) | O(W + T ) | O(1) |
Dynamic programming involves breaking down problems into their subproblems. By solving for the optimal subproblems and saving those results into memory to access them whenever a repeated problem needs to be solved, the algorithmic complexity decreases significantly. Implementing dynamic programming algorithms requires higher-level thinking about the problem’s patterns. To explain dynamic programming, let’s re-examine the Fibonacci sequence that was discussed in Chapter 8. Then the chapter will cover the rules of dynamic programming and walk you through some examples to make the concepts more concrete.

Recursion tree for Fibonacci numbers
This is known as overlapping subproblems. Calculating the Fibonacci sequence for 6 requires calculating the Fibonacci sequence for 4 and 5. Hence, the Fibonacci sequence for 5 overlaps with the fourth Fibonacci sequence calculation. This problem also has an optimal substructure, which refers to the fact that the optimal solution to the problem contains optimal solutions to its subproblems.
With this, let’s now formalize what dynamic programming is.
Dynamic programming (DP) is the method of storing values that were already calculated and using those values to avoid any recalculations (typically in a recursive algorithm). This method can be applied only to those problems with overlapping subproblems and optimal substructure.
Similar to divide and conquer in recursion, DP combines solutions on subproblems. DP is used when solutions for subproblems are needed multiple times. It stores subproblem solutions typically in a hash table, an array, or a matrix, and this is referred to as memoization . DP is useful for solving problems in which there are many repeated subproblems.
An example of this can be seen with the Fibonacci sequence recursive method. It can be observed that some numbers such as 3 will be recalculated many times.
A hash table can be used to store results to avoid any recalculations. Doing this reduces the time complexity from O(2n) to just O(n), which is an immense change. Calculating O(2n) with a realistically large enough n can take literally years to compute.
An optimal substructure is when the optimal solution of a problem can be found by using the optimal solutions of its subproblems.
For example, the shortest path finding algorithms have optimal substructures. Consider finding the shortest path for traveling between cities by car. If the shortest route from Los Angeles to Vancouver passes through San Francisco and then Seattle, then the shortest route from San Francisco to Vancouver must pass through Seattle as well.
1 step, 1 step, 1 step, 1 step
1 step, 1 step, 2 steps
1 step, 3 steps
2 steps, 2 steps
Time Complexity: O(3n)
Time Complexity: O(n)
This shows the power of dynamic programing. It improves time complexity immensely.
This section will explore and solve some of the classical dynamic programming problems. The first one that will be explored is the knapsack problem.
Given n weights and the values of items, put these items in a knapsack of a given capacity, w, to get the maximum total value in the knapsack.
The item is included in the optimal subset.
The item is not included in the optimal set.
(excluding the Nth item): max value obtained with n-1 items
(including the Nth item): max value obtained with n-1 items minus the Nth item (can only work if the weight of the Nth item is smaller than W)
Time Complexity: O(2n)

Recursion tree for knapsack
Time Complexity: O(n*w )
Here, n is the number of items, and w is the capacity of the knapsack.
Space Complexity: O(n*w )
This algorithm requires an n times wcombination to store the cached results inside matrixDP.
The next DP question that will be studied is another classic.
Given two sequences, find the length of the longest subsequence where a subsequence is defined as a sequence that appears in relative order without necessarily being contiguous. For example, sam, sie, aie, and so forth, are subsequences of sammie. A string has 2n possible subsequences where n is the length of the string.
As a real-world example, let’s consider a generalized computer science problem that appears in main domains such as bioinformatics (DNA sequencing). This algorithm is also how the diff functionality (file comparison to output difference between files) is implemented in version control and operating systems.
Time Complexity: O(2n)

Recursion tree for longest common string length
Time Complexity: O(m * n)
Space Complexity: O(m * n)
Here, m is the length of str1, and n is the length of str2.
Given a value/money n and an unlimited supply of each coin of different values, S = {S1, S2, .. Sm}, of size M, how many ways can the change be made without considering the order of the coins?
Time Complexity: O(nm)
Space Complexity: O(n)
Here, m is the number of available types of coins, and n is the desired currency to convert into change.

Recursion tree for longest coin change
To account solve for this, a table (matrix) can be used to store already computed results.
Time Complexity: O(m * n)
Space Complexity: O(m * n)
Here, m is the number of available types of coins, and n is the desired currency to convert into change.
Given a string (str1) of length m and another string (str2) of length n, what is the minimum number of edits to convert str1 into str2?
Insert
Remove
Replace
Time Complexity: O(3m)
The time complexity of the naive solution is exponential, and the worst case is when no characters in the two strings match. This makes sense because each call has three calls (insert, remove, replace).

Recursion tree for edit distance
Time Complexity: O(m * n)
Space Complexity: O(m * n)
Here, m is the length of str1, and n is the length of str2.
Optimal substructure: The optimal solution to the problem contains optimal solutions to its subproblems.
Overlapping subproblems: The solutions for subproblems are needed multiple times.
To store the already computed solutions to a subproblem, a matrix or a hash table is typically used; this is because both provide O(1) lookup time. Doing this, the time complexity can be improved from exponential (e.g., O(2n )) to polynomial time (e.g., O(n2)).
Bit manipulation is an advanced topic that JavaScript developers typically do not need to know. Low-level programming languages such as C take advantage of these operators. However, you should learn a bit about bit manipulation if you want to implement high-performance server-side code.
Understanding bit manipulation requires some knowledge of digital logic. Any introductory course in discrete math or circuits would be helpful to understand these concepts.
& : AND
| : OR
~ : NOT
^ : XOR
<<: Left shift
>>: Right shift
>>>: Zero-fill right shift
Recall from Chapter 3 that all numbers are represented with 32 bits (meaning there are 32 1s and 0s). When converting from decimal numbers (base 10) to binary (base 2), it is important to keep this in mind.
In bitwise operations, the numbers are in binary representation. For example, 9 in binary is 1001, and 5 in binary is 101.
In this example, shifting divided the 9 by 2 (integer division). This is because, again, binary is a base 2 system.
In this example, shifting divided the 9 by 2 (integer division). This is because, again, binary is a base 2 system.
To have a better understanding of why these operations work, it is recommended to take an introductory digital logic course in school or online. In the end, everything consists of 1s and 0s because a transistor in a computer can have only two states: on and off.
This section will cover how to perform addition, subtraction, multiplication, division, and modulus using bitwise operators.
Adding binary numbers is no different from adding decimal numbers. The rule that children learn in the second grade is the same: add up two numbers and carry 1 to next digit if it exceeds 10.
Subtraction is the difference of two numbers. However, you can also think of it as adding a negative number. Here’s an example: 5 - 4 = 5 + (-4).
Operator | Operation | Use Case |
|---|---|---|
& | AND | 1 when both bits are 1 |
| | OR | 1 when either bit is 1 |
∼ | NOT | Inverts all the bits |
^ | XOR | 1 when only one of the bits is 1 |
<< | Left shift | Shifts to the left, and any excess bits are shifted off |
>> | Right shift | Shifts to the right, and any excess bits are shifted off |
>>> | Zero-fill right shift | Shifts to the right, and any excess bits are shifted off and the sign bit comes 0 |