As a developer, understanding data structures and algorithms is crucial for writing efficient and scalable code. Here is a comprehensive guide to help you learn and master these fundamental concepts:

**Introduction to Algorithms and Data Structures (DSA)**__:__

**Introduction to Algorithms and Data Structures (DSA)**are foundational concepts in computer science that play a crucial role in solving computational problems efficiently.**Data Structures and Algorithms**are organized and stored in formats to enable efficient data manipulation and retrieval. They provide a way to organize and store data so that operations can be performed efficiently. Some of the common data structures that every developer should know are Arrays, Linked List, Stack, Queue, Trees, Graphs, etc.**Data structures**are step-by-step procedures or formulas for solving specific problems. They are a sequence of well-defined, unambiguous instructions designed to perform a specific task or solve a particular problem. Some of the common algorithms that every developer should know are Searching Algorithms, Sorting Algorithms, Graph Algorithms, Dynamic Programming, Divide and Conquer, etc.**Algorithms**

Data Structure & Algorithms Guide for Developers

**Basic Data Structures**__:__

**Basic Data Structures**## Arrays:

**Learn how to create and manipulate arrays, including basic operations like insertion, deletion, and searching**

The most basic yet important data structure is the array. It is a linear data structure. An array is a collection of homogeneous data types where the elements are allocated contiguous memory. Because of the contiguous allocation of memory, any element of an array can be accessed in constant time. Each array element has a corresponding index number.

To learn more about arrays, refer to the article â€œ** Introduction to Arrays**â€œ.

Here are some topics about array which you must learn:

- Reverse Array â€“ Reverse an array means shifting the elements of an array in a reverse manner i.e., the last element becomes the first element, second last element becomes the second element, and so on.
- Rotation of Array â€“ Rotation of array means shifting the elements of an array in a circular manner i.e., in the case of right circular shift the last element becomes the first element, and all other element moves one point to the right.
- Rearranging an array â€“ Rearrangement of array elements suggests the changing of an initial order of elements following some conditions or operations.
- Range queries in the array â€“ Often you need to perform operations on a range of elements. These functions are known as range queries.
- Multidimensional array â€“ These are arrays having more than one dimension. The most used one is the 2-dimensional array, commonly known as a matrix.
- Kadaneâ€™s algorithm
- Dutch national flag algorithm

**Types of Arrays: **

You can imagine a 1d array as a row, where elements are stored one after another.**One-dimensional array (1-D Array):**2-D Multidimensional arrays can be considered as an array of arrays or as a matrix consisting of rows and columns.**Two-dimensional array (2-D Array or Matrix):**A 3-D Multidimensional array contains three dimensions, so it can be considered an array of two-dimensional arrays.**Three-dimensional array (3-D Array):**

## Linked Lists:

**Understand the concept of linked lists, including singly linked lists, doubly linked lists, and circular linked lists**

As the above data structures, the linked list is also a linear data structure. But Linked List is different from Array in its configuration. It is not allocated to contiguous memory locations. Instead, each node of the linked list is allocated to some random memory space and the previous node maintains a pointer that points to this node. So no direct memory access of any node is possible and it is also dynamic i.e., the size of the linked list can be adjusted at any time. To learn more about linked lists refer to the article â€œ** Introduction to Linked List**â€œ.

The topics which you must want to cover are:

- Singly Linked List â€“ In this, each node of the linked list points only to its next node.
- Circular Linked List â€“ This is the type of linked list where the last node points back to the head of the linked list.
- Doubly Linked List â€“ In this case, each node of the linked list holds two pointers, one point to the next node and the other points to the previous node.

## Stacks:

**Learn about the stack data structure and their applications**

** Stack** is a linear data structure which follows a particular order in which the operations are performed. The order may be LIFO(Last In First Out) or FILO(First In Last Out).

The reason why Stack is considered a complex data structure is that it uses other data structures for implementation, such as Arrays, Linked lists, etc. based on the characteristics and features of Stack data structure.

## Queues:

**Learn about the queue data structure and their applications**

** Queue** is a linear data structure which follows a particular order in which the operations are performed. The order may be FIFO (First In First Out).

The reason why Queue is considered a complex data structure is that it uses other data structures for implementation, such as Arrays, Linked lists, etc. based on the characteristics and features of Queue data structure.

## Trees:

**Understand the concepts of binary trees, binary search trees, AVL trees, and more**

After having the basics covered about the linear data structure, now it is time to take a step forward to learn about the non-linear data structures. The first non-linear data structure you should learn is the tree.

Tree data structure is similar to a tree we see in nature but it is upside down. It also has a root and leaves. The root is the first node of the tree and the leaves are the ones at the bottom-most level. The special characteristic of a tree is that there is only one path to go from any of its nodes to any other node.

Based on the maximum number of children of a node of the tree it can be â€“

- Binary tree â€“ This is a special type of tree where each node can have a maximum of 2 children.
- Ternary tree â€“ This is a special type of tree where each node can have a maximum of 3 children.
- N-ary tree â€“ In this type of tree, a node can have at most N children.

Based on the configuration of nodes there are also several classifications. Some of them are:

- Complete Binary Tree â€“ In this type of binary tree all the levels are filled except maybe for the last level. But the last level elements are filled as left as possible.
- Perfect Binary Tree â€“ A perfect binary tree has all the levels filled
- Binary Search Tree â€“ A binary search tree is a special type of binary tree where the smaller node is put to the left of a node and a higher value node is put to the right of a node
- Ternary Search Tree â€“ It is similar to a binary search tree, except for the fact that here one element can have at most 3 children.

## Graphs:

**Learn about graph representations, graph traversal algorithms (BFS, DFS), and graph algorithms (Dijkstra’s, Floyd-Warshall, etc.)**

Another important non-linear data structure is the graph. It is similar to the Tree data structure, with the difference that there is no particular root or leaf node, and it can be traversed in any order.

A ** Graph** is a non-linear data structure consisting of a finite set of vertices(or nodes) and a set of edges that connect a pair of nodes.

Each edge shows a connection between a pair of nodes. This data structure helps solve many real-life problems. Based on the orientation of the edges and the nodes there are various types of graphs.

Here are some must to know concepts of graphs:

- Types of graphs â€“ There are different types of graphs based on connectivity or weights of nodes.
- Introduction to BFS and DFS â€“ These are the algorithms for traversing through a graph
- Cycles in a graph â€“ Cycles are a series of connections following which we will be moving in a loop.
- Topological sorting in the graph
- Minimum Spanning tree in graph

**Advanced Data Structures**__:__

**Advanced Data Structures**## Heaps:

**Understand the concept of heaps and their applications, such as priority queues**

A Heap is a special ** Tree-based Data Structure** in which the tree is a complete binary tree.

**Types of heaps:**

Generally, heaps are of two types.

**Max-Heap**In this heap, the value of the root node must be the greatest among all its child nodes and the same thing must be done for its left and right sub-tree also.**:****Min-Heap**In this heap, the value of the root node must be the smallest among all its child nodes and the same thing must be done for its left ans right sub-tree also.**:**

## Hash Tables:

**Learn about hash functions, collision resolution techniques, and applications of hash tables**

** Hashing** refers to the process of generating a fixed-size output from an input of variable size using the mathematical formulas known as hash functions. This technique determines an index or location for the storage of an item in a data structure.

## Tries:

**Understand trie data structures and their applications, such as prefix matching and autocomplete**

Trie is a type of k-ary search tree used for storing and searching a specific key from a set. Using Trie, search complexities can be brought to optimal limit (key length).

A trie (derived from retrieval) is a multiway tree data structure used for storing strings over an alphabet. It is used to store a large amount of strings. The pattern matching can be done efficiently using tries.

- Trie Delete
- Trie data structure
- Displaying content of Trie
- Applications of Trie
- Auto-complete feature using Trie
- Minimum Word Break
- Sorting array of strings (or words) using Trie
- Pattern Searching using a Trie of all Suffixes

**Basic Algorithms**__:__

**Basic Algorithms**Basic algorithms are the fundamental building blocks of computer science and programming. They are essential for solving problems efficiently and are often used as subroutines in more complex algorithms.

## Sorting Algorithms:

**Learn about different sorting algorithms like bubble sort, selection sort, insertion sort, merge sort, quicksort, and their time complexity**

** Sorting Algorithm** is used to rearrange a given array or list elements according to a comparison operator on the elements. The comparison operator is used to decide the new order of element in the respective data structure.

Sorting algorithms are essential in computer science and programming, as they allow us to organize data in a meaningful way. Here’s an overview of some common sorting algorithms:

:**Bubble Sort**: Bubble sort repeatedly steps through the list, compares adjacent elements, and swaps them if they are in the wrong order.**Description**: O(n^2) in the worst case and O(n) in the best case (when the list is already sorted).**Time Complexity**

:**Selection Sort**: Selection sort divides the input list into two parts: the sublist of items already sorted and the sublist of items remaining to be sorted. It repeatedly selects the smallest (or largest) element from the unsorted sublist and swaps it with the first element of the unsorted sublist.**Description**: O(n^2) in all cases (worst, average, and best).**Time Complexity**

:**Insertion Sort**: Insertion sort builds the final sorted array one element at a time by repeatedly inserting the next element into the sorted part of the array.**Description**: O(n^2) in the worst case and O(n) in the best case (when the list is already sorted).**Time Complexity**

:**Merge Sort**: Merge sort is a divide-and-conquer algorithm that divides the input list into two halves, sorts each half recursively, and then merges the two sorted halves.**Description**: O(n log n) in all cases (worst, average, and best).**Time Complexity**

:**Quick Sort**: Quick sort is a divide-and-conquer algorithm that selects a pivot element and partitions the input list into two sublists: elements less than the pivot and elements greater than the pivot. It then recursively sorts the two sublists.**Description**: O(n^2) in the worst case and O(n log n) in the average and best cases.**Time Complexity**

:**Heap Sort**: Heap sort is a comparison-based sorting algorithm that builds a heap from the input list and repeatedly extracts the maximum (or minimum) element from the heap and rebuilds the heap.**Description**: O(n log n) in all cases (worst, average, and best).**Time Complexity**

:**Radix Sort**: Radix sort is a non-comparison-based sorting algorithm that sorts elements by their individual digits or characters. It sorts the input list by processing the digits or characters from the least significant digit to the most significant digit.**Description**: O(nk) where n is the number of elements in the input list and k is the number of digits or characters in the largest element.**Time Complexity**

## Searching Algorithms:

**Understand linear search, binary search, and their time complexity**

Searching algorithms are used to find a particular element or value within a collection of data. Here are two common searching algorithms:

:**Linear Search**: Linear search, also known as sequential search, checks each element in the list until the desired element is found or the end of the list is reached. It is the simplest and most intuitive searching algorithm.**Description**: O(n) in the worst case, where n is the number of elements in the list. This is because in the worst case, the algorithm may need to check every element in the list.**Time Complexity**

:**Binary Search**: Binary search is a more efficient searching algorithm that works on sorted lists. It repeatedly divides the list in half and checks whether the desired element is in the left or right half. It continues this process until the element is found or the list is empty.**Description**: O(log n) in the worst case, where n is the number of elements in the list. This is because the algorithm divides the list in half at each step, leading to a logarithmic time complexity.**Time Complexity**

Comparison:

:**Linear Search**- Works on both sorted and unsorted lists.
- Time complexity is O(n) in the worst case.
- Simple to implement.

:**Binary Search**- Works only on sorted lists.
- Time complexity is O(log n) in the worst case.
- More efficient than linear search for large lists.

## Recursion:

**Understand Recursion, how it works and solve problems about how Recursion can solve complex problems easily**

The process in which a ** function calls itself **directly or indirectly is called recursion and the corresponding function is called a recursive function. Using a recursive algorithm, certain problems can be solved quite easily. Recursion is one of the most important algorithms which uses the concept of code reusability and repeated usage of the same piece of code.

A recursive function solves a particular problem by calling itself for smaller subproblems and using those solutions to solve the original subproblem.

The point which makes Recursion one of the most used algorithms is that it forms the base for many other algorithms such as:

- Tree traversals
- Graph traversals
- Divide and Conquers Algorithms
- Backtracking algorithms
- Tower of Hanoi

## Backtracking:

**Learn Backtracking to explore all the possible combinations to solve a problem and track back whenever we reach a dead-end.**

Backtracking is a problem-solving algorithmic technique that involves finding a solution incrementally by trying ** different options** and

**them if they lead to a**

**undoing****. It is commonly used in situations where you need to explore multiple possibilities to solve a problem, like searching for a path in a maze or solving puzzles like Sudoku. When a dead end is reached, the algorithm backtracks to the previous decision point and explores a different path until a solution is found or all possibilities have been exhausted.**

**dead end**Backtracking is used to solve problems which require exploring all the combinations or states. Some of the common problems which can be easily solved using backtracking are:

- N-Queen Problem
- Solve Sudoku
- M-coloring problem
- Rat in a Maze
- The Knightâ€™s tour problem
- Permutation of given String
- Subset Sum problem
- Magnet Puzzle

**Advanced Algorithms:**

**Advanced Algorithms:**## Bitwise Algorithms:

The ** Bitwise Algorithms** is used to perform operations at the bit-level or to manipulate bits in different ways. The bitwise operations are found to be much faster and are sometimes used to improve the efficiency of a program.

**involve manipulating individual bits of binary representations of numbers to perform operations efficiently. These algorithms utilize bitwise operators like**

**Bitwise algorithms****, etc., to solve problems related to tasks such as setting, clearing, or toggling specific bits, checking if a number is even or odd, swapping values without using a temporary variable, and more.**

**AND, OR, XOR, shift operators**Some of the most common problems based on Bitwise Algorithms are:

- Binary representation of a given number
- Count set bits in an integer
- Add two bit strings
- Turn off the rightmost set bit
- Rotate bits of a number
- Compute modulus division by a power-of-2-number
- Find the Number Occurring Odd Number of Times
- Program to find whether a given number is power of 2

## Dynamic Programming:

**Understand the concept of dynamic programming and how it can be applied to solve complex problems efficiently**

Dynamic programming is a problem-solving technique used to solve problems by breaking them down into simpler subproblems. It is based on the principle of ** optimal substructure** (optimal solution to a problem can be constructed from the optimal solutions of its subproblems) and

**(solutions to the same subproblems are needed repeatedly).**

**overlapping subproblems**Dynamic programming is typically used to solve problems that can be divided into overlapping subproblems, such as those in the following categories:

: Problems where you need to find the best solution from a set of possible solutions. Examples include the shortest path problem, the longest common subsequence problem, and the knapsack problem.**Optimization Problems**: Problems where you need to count the number of ways to achieve a certain goal. Examples include the number of ways to make change for a given amount of money and the number of ways to arrange a set of objects.**Counting Problems**: Problems where you need to make a series of decisions to achieve a certain goal. Examples include the traveling salesman problem and the 0/1 knapsack problem.**Decision Problems**

Dynamic programming can be applied to solve these problems efficiently by storing the solutions to subproblems in a table and reusing them when needed. This allows for the elimination of redundant computations and leads to significant improvements in time and space complexity.

The steps involved in solving a problem using dynamic programming are as follows:

: Break down the problem into smaller subproblems that can be solved independently.**Identify the Subproblems**: Define a recurrence relation that expresses the solution to the original problem in terms of the solutions to its subproblems.**Define the Recurrence Relation**: Solve the subproblems using the recurrence relation and store the solutions in a table.**Solve the Subproblems**: Use the solutions to the subproblems to construct the solution to the original problem.**Build the Solution**: If necessary, optimize the solution by eliminating redundant computations or using space-saving techniques.**Optimize**

## Greedy Algorithms:

**Learn about greedy algorithms and their applications in optimization problems**

Greedy algorithms are a class of algorithms that make a series of choices, each of which is the best at the moment, with the hope that this will lead to the best overall solution. They do not always guarantee an optimal solution, but they are often used because they are simple to implement and can be very efficient.

Here are some key points about greedy algorithms:

: A greedy algorithm makes a series of choices, each of which is the best at the moment, with the hope that this will lead to the best overall solution. This is known as the greedy choice property.**Greedy Choice Property**: A problem exhibits optimal substructure if an optimal solution to the problem contains optimal solutions to its subproblems. Many problems that can be solved using greedy algorithms exhibit this property.**Optimal Substructure**:**Applications of Greedy Algorithms**: In graph theory, a minimum spanning tree is a subset of the edges of a connected, edge-weighted graph that connects all the vertices together, without any cycles and with the minimum possible total edge weight.**Minimum Spanning Tree**: In graph theory, the shortest path problem is the problem of finding a path between two vertices in a graph such that the sum of the weights of its constituent edges is minimized.**Shortest Path**: Huffman encoding is a method of lossless data compression that assigns variable-length codes to input characters, with shorter codes assigned to more frequent characters.**Huffman Encoding**

:**Characteristics of Greedy Algorithms**: A greedy algorithm makes a series of choices, each of which is the best at the moment, with the hope that this will lead to the best overall solution.**Greedy Choice Property**: A problem exhibits optimal substructure if an optimal solution to the problem contains optimal solutions to its subproblems.**Optimal Substructure**: Greedy algorithms do not always guarantee an optimal solution, but they are often used because they are simple to implement and can be very efficient.**Greedy Algorithms are not always optimal**

:**Examples of Greedy Algorithms**: Dijkstra’s algorithm is a graph search algorithm that finds the shortest path between two vertices in a graph with non-negative edge weights.**Dijkstra’s Algorithm**: Prim’s algorithm is a greedy algorithm that finds a minimum spanning tree for a weighted undirected graph.**Prim’s Algorithm**

## Divide and Conquer:

**Understand the divide-and-conquer paradigm and how it is used in algorithms like merge sort and quicksort**

Divide and conquer is a problem-solving paradigm that involves breaking a problem down into smaller subproblems, solving each subproblem independently, and then combining the solutions to the subproblems to solve the original problem. It is a powerful technique that is used in many algorithms, including merge sort and quicksort.

Here are the key steps involved in the divide-and-conquer paradigm:

: Break the problem down into smaller subproblems that are similar to the original problem but smaller in size.**Divide**: Solve each subproblem independently using the same divide-and-conquer approach.**Conquer**: Combine the solutions to the subproblems to solve the original problem.**Combine**

Merge Sort:

: Divide the unsorted list into two sublists of about half the size.**Divide**: Recursively sort each sublist.**Conquer**: Merge the two sorted sublists into a single sorted list.**Combine**

Quicksort:

: Choose a pivot element from the list and partition the list into two sublists: elements less than the pivot and elements greater than the pivot.**Divide**: Recursively sort each sublist.**Conquer**: Combine the sorted sublists and the pivot element to form a single sorted list.**Combine**

The divide-and-conquer paradigm is used in many algorithms because it can lead to efficient solutions for a wide range of problems. It is particularly useful for problems that can be divided into smaller subproblems that can be solved independently. By solving each subproblem independently and then combining the solutions, the divide-and-conquer approach can lead to a more efficient solution than solving the original problem directly.

**Algorithm Analysis**__:__

**Algorithm Analysis****Learn about the time complexity and space complexity of algorithms and how to analyze them using Big O notation**

Time complexity and space complexity are two important measures of the efficiency of an algorithm. They describe how the time and space requirements of an algorithm grow as the size of the input increases. Big O notation is a mathematical notation used to describe the upper bound on the growth rate of an algorithm’s time or space requirements.

:**Time Complexity**- Time complexity measures the amount of time an algorithm takes to run as a function of the size of the input.
- It is often expressed using Big O notation, which describes the upper bound on the growth rate of the algorithm’s time requirements.
- For example, an algorithm with a time complexity of O(n) takes linear time, meaning the time it takes to run increases linearly with the size of the input.
- Common time complexities include O(1) (constant time), O(log n) (logarithmic time), O(n) (linear time), O(n log n) (linearithmic time), O(n^2) (quadratic time), O(n^3) (cubic time), and more.

:**Space Complexity**- Space complexity measures the amount of memory an algorithm uses as a function of the size of the input.
- It is also expressed using Big O notation, which describes the upper bound on the growth rate of the algorithm’s space requirements.
- For example, an algorithm with a space complexity of O(n) uses linear space, meaning the amount of memory it uses increases linearly with the size of the input.
- Common space complexities include O(1) (constant space), O(log n) (logarithmic space), O(n) (linear space), O(n log n) (linearithmic space), O(n^2) (quadratic space), O(n^3) (cubic space), and more.

:**Analyzing Time and Space Complexity**- To analyze the time and space complexity of an algorithm, you can follow these steps:
- Identify the basic operations performed by the algorithm (e.g., comparisons, assignments, arithmetic operations).
- Determine the number of times each basic operation is performed as a function of the size of the input.
- Express the total number of basic operations as a mathematical function of the input size.
- Simplify the mathematical function and express it using Big O notation.

- For example, consider the following pseudocode for a simple algorithm that finds the maximum element in an array:
max = array[0]

for i = 1 to n - 1

if array[i] > max

max = array[i]

return max- The basic operations performed by the algorithm are comparisons and assignments.
- The number of comparisons is n – 1, and the number of assignments is 1.
- The total number of basic operations is 2n – 2.
- The time complexity of the algorithm is O(n), and the space complexity is O(1).

- To analyze the time and space complexity of an algorithm, you can follow these steps:

**Understand the concepts of best-case, worst-case, and average-case time complexity.**

The concepts of best-case, worst-case, and average-case time complexity are used to describe the performance of an algorithm under different scenarios. They help us understand how an algorithm behaves in different situations and provide insights into its efficiency.

:**Best-Case Time Complexity**- The best-case time complexity of an algorithm is the minimum amount of time it takes to run on any input of a given size.
- It represents the scenario where the algorithm performs optimally and takes the least amount of time to complete.
- Best-case time complexity is often denoted using Big O notation, where O(f(n)) represents the upper bound on the growth rate of the best-case running time as a function of the input size n.
- For example, an algorithm with a best-case time complexity of O(1) takes constant time, meaning it always completes in the same amount of time, regardless of the input size.

:**Worst-Case Time Complexity**- The worst-case time complexity of an algorithm is the maximum amount of time it takes to run on any input of a given size.
- It represents the scenario where the algorithm performs the least efficiently and takes the most amount of time to complete.
- Worst-case time complexity is often denoted using Big O notation, where O(f(n)) represents the upper bound on the growth rate of the worst-case running time as a function of the input size n.
- For example, an algorithm with a worst-case time complexity of O(n^2) takes quadratic time, meaning the time it takes to run increases quadratically with the input size.

:**Average-Case Time Complexity**- The average-case time complexity of an algorithm is the average amount of time it takes to run on all possible inputs of a given size.
- It represents the expected performance of the algorithm when running on random inputs.
- Average-case time complexity is often denoted using Big O notation, where O(f(n)) represents the upper bound on the growth rate of the average-case running time as a function of the input size n.
- For example, an algorithm with an average-case time complexity of O(n) takes linear time, meaning the time it takes to run increases linearly with the input size.

**Problem Solving**__:__

**Problem Solving**Practice solving algorithmic problems on platforms like GeeksForGeeks, LeetCode, HackerRank, etc. GeeksforGeeks is a popular platform that provides a wealth of resources for learning and practicing problem-solving in computer science and programming. Here’s how you can use GeeksforGeeks for problem-solving:

- Covers a wide range of topics, including data structures, algorithms, programming languages, databases, and more.
- Support for multiple programming languages. You can choose any language you’re comfortable with or want to learn.
- Detailed explanations, examples, and implementations for various data structures and algorithms.
- Includes various approach to solve a problem starting from Brute Force to the most optimal approach.
- Interview Preparation section for common coding interview questions, tips and guidance.
- Discussion forum where users can ask and answer questions and engage in discussions to learn from others, seek help, and share your knowledge.
- Provides an online IDE for coding practice and experiment with code snippets, run them, and check the output directly on the platform.

__Why are Data Structure & Algorithms important in software development?__

Data Structures and Algorithms are fundamental concepts in computer science and play a crucial role in software development for several reasons:

Aspect |
Importance |
---|---|

Efficiency |
Faster and more resource-efficient software. |

Resource Utilization |
Optimization of memory and processing power usage, especially crucial for applications on devices with limited resources. |

Scalability |
Ensures performance as software grows, handling larger datasets or user loads without significant performance degradation. |

Problem Solving |
Provides a systematic approach to problem-solving, breaking down complex problems into manageable components. |

Interviews and Assessments |
Important for success in technical interviews, coding assessments, and evaluations where algorithmic problem-solving is assessed. |

**Additional Resources**__:__

**Additional Resources**Enroll in online courses on platforms like GeeksforGeeks, Coursera, edX, and Udemy.**Online Courses:**Solve coding challenges on websites like GeeksforGeeks, LeetCode, HackerRank, and CodeSignal.**Practice:**Join online communities like GeeksforGeeks, Stack Overflow, Reddit, and GitHub to learn from others and share your knowledge.**Community:**Read books like “Introduction to Algorithms” by Cormen, Leiserson, Rivest, and Stein, and “Algorithms” by Robert Sedgewick and Kevin Wayne.**Books:**