(This page has no text content)
Data Structures And Algorithms Made Easy -To All My Readers By Narasimha Karumanchi
Copyright© 2017 by CareerMonk.com All rights reserved. Designed by Narasimha Karumanchi Copyright© 2017 CareerMonk Publications. All rights reserved. All rights reserved. No part of this book may be reproduced in any form or by any electronic or mechanical means, including information storage and retrieval systems, without written permission from the publisher or author.
Acknowledgements Mother and Father, it is impossible to thank you adequately for everything you have done, from loving me unconditionally to raising me in a stable household, where your persistent efforts and traditional values taught your children to celebrate and embrace life. I could not have asked for better parents or role-models. You showed me that anything is possible with faith, hard work and determination. This book would not have been possible without the help of many people. I would like to express my gratitude to all of the people who provided support, talked things over, read, wrote, offered comments, allowed me to quote their remarks and assisted in the editing, proofreading and design. In particular, I would like to thank the following individuals: ▪ Mohan Mullapudi, IIT Bombay, Architect, dataRPM Pvt. Ltd. ▪ Navin Kumar Jaiswal, Senior Consultant, Juniper Networks Inc. ▪ A. Vamshi Krishna, IIT Kanpur, Mentor Graphics Inc. ▪ Cathy Reed, BA, MA, Copy Editor –Narasimha Karumanchi M-Tech, IIT Bombay Founder, CareerMonk.com
Preface Dear Reader, Please hold on! I know many people typically do not read the Preface of a book. But I strongly recommend that you read this particular Preface. It is not the main objective of this book to present you with the theorems and proofs on data structures and algorithms. I have followed a pattern of improving the problem solutions with different complexities (for each problem, you will find multiple solutions with different, and reduced, complexities). Basically, it’s an enumeration of possible solutions. With this approach, even if you get a new question, it will show you a way to think about the possible solutions. You will find this book useful for interview preparation, competitive exams preparation, and campus interview preparations. As a job seeker, if you read the complete book, I am sure you will be able to challenge the interviewers. If you read it as an instructor, it will help you to deliver lectures with an approach that is easy to follow, and as a result your students will appreciate the fact that they have opted for Computer Science / Information Technology as their degree. This book is also useful for Engineering degree students and Masters degree students during their academic preparations. In all the chapters you will see that there is more emphasis on problems and their analysis rather than on theory. In each chapter, you will first read about the basic required theory, which is then followed by a section on problem sets. In total, there are approximately 700 algorithmic problems, all with solutions. If you read the book as a student preparing for competitive exams for Computer Science / Information Technology, the content covers all the required topics in full detail. While writing this book, my main focus was to help students who are preparing for these exams. In all the chapters you will see more emphasis on problems and analysis rather than on theory. In each chapter, you will first see the basic required theory followed by various problems. For many problems, multiple solutions are provided with different levels of complexity. We start with the brute force solution and slowly move toward the best solution possible for that problem. For each problem, we endeavor to understand how much time the algorithm takes and how much memory the algorithm uses.
It is recommended that the reader does at least one complete reading of this book to gain a full understanding of all the topics that are covered. Then, in subsequent readings you can skip directly to any chapter to refer to a specific topic. Even though many readings have been done for the purpose of correcting errors, there could still be some minor typos in the book. If any are found, they will be updated at www.CareerMonk.com. You can monitor this site for any corrections and also for new problems and solutions. Also, please provide your valuable suggestions at: Info@CareerMonk.com. I wish you all the best and I am confident that you will find this book useful. –Narasimha Karumanchi M-Tech, I IT Bombay Founder, CareerMonk.com
Other Books by Narasimha Karumanchi IT Interview Questions Data Structures and Algorithms for GATE Data Structures and Aigorithms Made Easy in Java Coding Interview Questions Peeling Design Patterns Elements of Computer Networking Data Structures and Algorithmic Thinking with Python
Table of Contents 1. Introduction 1.1 Variables 1.2 Data Types 1.3 Data Structures 1.4 Abstract Data Types (ADTs) 1.5 What is an Algorithm? 1.6 Why the Analysis of Algorithms? 1.7 Goal of the Analysis of Algorithms 1.8 What is Running Time Analysis? 1.9 How to Compare Algorithms 1.10 What is Rate of Growth? 1.11 Commonly Used Rates of Growth 1.12 Types of Analysis 1.13 Asymptotic Notation 1.14 Big-O Notation [Upper Bounding Function] 1.15 Omega-Q Notation [Lower Bounding Function] 1.16 Theta-Θ Notation [Order Function] 1.17 Important Notes 1.18 Why is it called Asymptotic Analysis? 1.19 Guidelines for Asymptotic Analysis 1.20 Simplyfying properties of asymptotic notations 1.21 Commonly used Logarithms and Summations 1.22 Master Theorem for Divide and Conquer Recurrences 1.23 Divide and Conquer Master Theorem: Problems & Solutions 1.24 Master Theorem for Subtract and Conquer Recurrences 1.25 Variant of Subtraction and Conquer Master Theorem 1.26 Method of Guessing and Confirming
1.27 Amortized Analysis 1.28 Algorithms Analysis: Problems & Solutions 2. Recursion and Backtracking 2.1 Introduction 2.2 What is Recursion? 2.3 Why Recursion? 2.4 Format of a Recursive Function 2.5 Recursion and Memory (Visualization) 2.6 Recursion versus Iteration 2.7 Notes on Recursion 2.8 Example Algorithms of Recursion 2.9 Recursion: Problems & Solutions 2.10 What is Backtracking? 2.11 Example Algorithms of Backtracking 2.12 Backtracking: Problems & Solutions 3. Linked Lists 3.1 What is a Linked List? 3.2 Linked Lists ADT 3.3 Why Linked Lists? 3.4 Arrays Overview 3.5 Comparison of Linked Lists with Arrays & Dynamic Arrays 3.6 Singly Linked Lists 3.7 Doubly Linked Lists 3.8 Circular Linked Lists 3.9 A Memory-efficient Doubly Linked List 3.10 Unrolled Linked Lists 3.11 Skip Lists 3.12 Linked Lists: Problems & Solutions 4. Stacks 4.1 What is a Stack? 4.2 How Stacks are used 4.3 Stack ADT
4.4 Applications 4.5 Implementation 4.6 Comparison of Implementations 4.7 Stacks: Problems & Solutions 5. Queues 5.1 What is a Queue? 5.2 How are Queues Used? 5.3 Queue ADT 5.4 Exceptions 5.5 Applications 5.6 Implementation 5.7 Queues: Problems & Solutions 6. Trees 6.1 What is a Tree? 6.2 Glossary 6.3 Binary Trees 6.4 Types of Binary Trees 6.5 Properties of Binary Trees 6.6 Binary Tree Traversals 6.7 Generic Trees (N-ary Trees) 6.8 Threaded Binary Tree Traversals (Stack or Queue-less Traversals) 6.9 Expression Trees 6.10 XOR Trees 6.11 Binary Search Trees (BSTs) 6.12 Balanced Binary Search Trees 6.13 AVL (Adelson-Velskii and Landis) Trees 6.14 Other Variations on Trees 7. Priority Queues and Heaps 7.1 What is a Priority Queue? 7.2 Priority Queue ADT 7.3 Priority Queue Applications 7.4 Priority Queue Implementations
7.5 Heaps and Binary Heaps 7.6 Binary Heaps 7.7 Heapsort 7.8 Priority Queues [Heaps]: Problems & Solutions 8. Disjoint Sets ADT 8.1 Introduction 8.2 Equivalence Relations and Equivalence Classes 8.3 Disjoint Sets ADT 8.4 Applications 8.5 Tradeoffs in Implementing Disjoint Sets ADT 8.8 Fast UNION Implementation (Slow FIND) 8.9 Fast UNION Implementations (Quick FIND) 8.10 Summary 8.11 Disjoint Sets: Problems & Solutions 9. Graph Algorithms 9.1 Introduction 9.2 Glossary 9.3 Applications of Graphs 9.4 Graph Representation 9.5 Graph Traversals 9.6 Topological Sort 9.7 Shortest Path Algorithms 9.8 Minimal Spanning Tree 9.9 Graph Algorithms: Problems & Solutions 10. Sorting 10.1 What is Sorting? 10.2 Why is Sorting Necessary? 10.3 Classification of Sorting Algorithms 10.4 Other Classifications 10.5 Bubble Sort 10.6 Selection Sort 10.7 Insertion Sort
10.8 Shell Sort 10.9 Merge Sort 10.10 Heap Sort 10.11 Quick Sort 10.12 Tree Sort 10.13 Comparison of Sorting Algorithms 10.14 Linear Sorting Algorithms 10.15 Counting Sort 10.16 Bucket Sort (or Bin Sort) 10.17 Radix Sort 10.18 Topological Sort 10.19 External Sorting 10.20 Sorting: Problems & Solutions 11. Searching 11.1 What is Searching? 11.2 Why do we need Searching? 11.3 Types of Searching 11.4 Unordered Linear Search 11.5 Sorted/Ordered Linear Search 11.6 Binary Search 11.7 Interpolation Search 11.8 Comparing Basic Searching Algorithms 11.9 Symbol Tables and Hashing 11.10 String Searching Algorithms 11.11 Searching: Problems & Solutions 12. Selection Algorithms [Medians] 12.1 What are Selection Algorithms? 12.2 Selection by Sorting 12.3 Partition-based Selection Algorithm 12.4 Linear Selection Algorithm - Median of Medians Algorithm 12.5 Finding the K Smallest Elements in Sorted Order 12.6 Selection Algorithms: Problems & Solutions
13. Symbol Tables 13.1 Introduction 13.2 What are Symbol Tables? 13.3 Symbol Table Implementations 13.4 Comparison Table of Symbols for Implementations 14. Hashing 14.1 What is Hashing? 14.2 Why Hashing? 14.3 HashTable ADT 14.4 Understanding Hashing 14.5 Components of Hashing 14.6 Hash Table 14.7 Hash Function 14.8 Load Factor 14.9 Collisions 14.10 Collision Resolution Techniques 14.11 Separate Chaining 14.12 Open Addressing 14.13 Comparison of Collision Resolution Techniques 14.14 How Hashing Gets O(1) Complexity? 14.15 Hashing Techniques 14.16 Problems for which Hash Tables are not suitable 14.17 Bloom Filters 14.18 Hashing: Problems & Solutions 15. String Algorithms 15.1 Introduction 15.2 String Matching Algorithms 15.3 Brute Force Method 15.4 Rabin-Karp String Matching Algorithm 15.5 String Matching with Finite Automata 15.6 KMP Algorithm 15.7 Boyer-Moore Algorithm
15.8 Data Structures for Storing Strings 15.9 Hash Tables for Strings 15.10 Binary Search Trees for Strings 15.11 Tries 15.12 Ternary Search Trees 15.13 Comparing BSTs, Tries and TSTs 15.14 Suffix Trees 15.15 String Algorithms: Problems & Solutions 16. Algorithms Design Techniques 16.1 Introduction 16.2 Classification 16.3 Classification by Implementation Method 16.4 Classification by Design Method 16.5 Other Classifications 17. Greedy Algorithms 17.1 Introduction 17.2 Greedy Strategy 17.3 Elements of Greedy Algorithms 17.4 Does Greedy Always Work? 17.5 Advantages and Disadvantages of Greedy Method 17.6 Greedy Applications 17.7 Understanding Greedy Technique 17.8 Greedy Algorithms: Problems & Solutions 18. Divide and Conquer Algorithms 18.1 Introduction 18.2 What is the Divide and Conquer Strategy? 18.3 Does Divide and Conquer Always Work? 18.4 Divide and Conquer Visualization 18.5 Understanding Divide and Conquer 18.6 Advantages of Divide and Conquer 18.7 Disadvantages of Divide and Conquer 18.8 Master Theorem
18.9 Divide and Conquer Applications 18.10 Divide and Conquer: Problems & Solutions 19. Dynamic Programming 19.1 Introduction 19.2 What is Dynamic Programming Strategy? 19.3 Properties of Dynamic Programming Strategy 19.4 Can Dynamic Programming Solve All Problems? 19.5 Dynamic Programming Approaches 19.6 Examples of Dynamic Programming Algorithms 19.7 Understanding Dynamic Programming 19.8 Longest Common Subsequence 19.9 Dynamic Programming: Problems & Solutions 20. Complexity Classes 20.1 Introduction 20.2 Polynomial/Exponential Time 20.3 What is a Decision Problem? 20.4 Decision Procedure 20.5 What is a Complexity Class? 20.6 Types of Complexity Classes 20.7 Reductions 20.8 Complexity Classes: Problems & Solutions 21. Miscellaneous Concepts 21.1 Introduction 21.2 Hacks on Bit-wise Programming 21.3 Other Programming Questions References
The objective of this chapter is to explain the importance of the analysis of algorithms, their notations, relationships and solving as many problems as possible. Let us first focus on understanding the basic elements of algorithms, the importance of algorithm analysis, and then slowly move toward the other topics as mentioned above. After completing this chapter, you should be able to find the complexity of any given algorithm (especially recursive functions). 1.1 Variables Before going to the definition of variables, let us relate them to old mathematical equations. All of us have solved many mathematical equations since childhood. As an example, consider the below equation:
We don’t have to worry about the use of this equation. The important thing that we need to understand is that the equation has names (x and y), which hold values (data). That means the names (x and y) are placeholders for representing data. Similarly, in computer science programming we need something for holding data, and variables is the way to do that. 1.2 Data Types In the above-mentioned equation, the variables x and y can take any values such as integral numbers (10, 20), real numbers (0.23, 5.5), or just 0 and 1. To solve the equation, we need to relate them to the kind of values they can take, and data type is the name used in computer science programming for this purpose. A data type in a programming language is a set of data with predefined values. Examples of data types are: integer, floating point, unit number, character, string, etc. Computer memory is all filled with zeros and ones. If we have a problem and we want to code it, it’s very difficult to provide the solution in terms of zeros and ones. To help users, programming languages and compilers provide us with data types. For example, integer takes 2 bytes (actual value depends on compiler), float takes 4 bytes, etc. This says that in memory we are combining 2 bytes (16 bits) and calling it an integer. Similarly, combining 4 bytes (32 bits) and calling it a float. A data type reduces the coding effort. At the top level, there are two types of data types: • System-defined data types (also called Primitive data types) • User-defined data types System-defined data types (Primitive data types) Data types that are defined by system are called primitive data types. The primitive data types provided by many programming languages are: int, float, char, double, bool, etc. The number of bits allocated for each primitive data type depends on the programming languages, the compiler and the operating system. For the same primitive data type, different languages may use different sizes. Depending on the size of the data types, the total available values (domain) will also change. For example, “int” may take 2 bytes or 4 bytes. If it takes 2 bytes (16 bits), then the total possible values are minus 32,768 to plus 32,767 (-215 to 215-1). If it takes 4 bytes (32 bits), then the possible values are between -2,147,483,648 and +2,147,483,647 (-231 to 231-1). The same is the case with other data types. User defined data types If the system-defined data types are not enough, then most programming languages allow the users
to define their own data types, called user – defined data types. Good examples of user defined data types are: structures in C/C + + and classes in Java. For example, in the snippet below, we are combining many system-defined data types and calling the user defined data type by the name “newType”. This gives more flexibility and comfort in dealing with computer memory. 1.3 Data Structures Based on the discussion above, once we have data in variables, we need some mechanism for manipulating that data to solve problems. Data structure is a particular way of storing and organizing data in a computer so that it can be used efficiently. A data structure is a special format for organizing and storing data. General data structure types include arrays, files, linked lists, stacks, queues, trees, graphs and so on. Depending on the organization of the elements, data structures are classified into two types: 1) Linear data structures: Elements are accessed in a sequential order but it is not compulsory to store all elements sequentially. Examples: Linked Lists, Stacks and Queues. 2) Non – linear data structures: Elements of this data structure are stored/accessed in a non-linear order. Examples: Trees and graphs. 1.4 Abstract Data Types (ADTs) Before defining abstract data types, let us consider the different view of system-defined data types. We all know that, by default, all primitive data types (int, float, etc.) support basic operations such as addition and subtraction. The system provides the implementations for the primitive data types. For user-defined data types we also need to define operations. The implementation for these operations can be done when we want to actually use them. That means, in general, user defined data types are defined along with their operations. To simplify the process of solving problems, we combine the data structures with their operations and we call this Abstract Data Types (ADTs). An ADT consists of two parts: 1. Declaration of data
2. Declaration of operations Commonly used ADTs include: Linked Lists, Stacks, Queues, Priority Queues, Binary Trees, Dictionaries, Disjoint Sets (Union and Find), Hash Tables, Graphs, and many others. For example, stack uses LIFO (Last-In-First-Out) mechanism while storing the data in data structures. The last element inserted into the stack is the first element that gets deleted. Common operations of it are: creating the stack, pushing an element onto the stack, popping an element from stack, finding the current top of the stack, finding number of elements in the stack, etc. While defining the ADTs do not worry about the implementation details. They come into the picture only when we want to use them. Different kinds of ADTs are suited to different kinds of applications, and some are highly specialized to specific tasks. By the end of this book, we will go through many of them and you will be in a position to relate the data structures to the kind of problems they solve. 1.5 What is an Algorithm? Let us consider the problem of preparing an omelette. To prepare an omelette, we follow the steps given below: 1) Get the frying pan. 2) Get the oil. a. Do we have oil? i. If yes, put it in the pan. ii. If no, do we want to buy oil? 1. If yes, then go out and buy. 2. If no, we can terminate. 3) Turn on the stove, etc... What we are doing is, for a given problem (preparing an omelette), we are providing a step-by- step procedure for solving it. The formal definition of an algorithm can be stated as: An algorithm is the step-by-step unambiguous instructions to solve a given problem. In the traditional study of algorithms, there are two main criteria for judging the merits of algorithms: correctness (does the algorithm give solution to the problem in a finite number of steps?) and efficiency (how much resources (in terms of memory and time) does it take to execute the). Note: We do not have to prove each step of the algorithm. 1.6 Why the Analysis of Algorithms?
To go from city “A” to city “B”, there can be many ways of accomplishing this: by flight, by bus, by train and also by bicycle. Depending on the availability and convenience, we choose the one that suits us. Similarly, in computer science, multiple algorithms are available for solving the same problem (for example, a sorting problem has many algorithms, like insertion sort, selection sort, quick sort and many more). Algorithm analysis helps us to determine which algorithm is most efficient in terms of time and space consumed. 1.7 Goal of the Analysis of Algorithms The goal of the analysis of algorithms is to compare algorithms (or solutions) mainly in terms of running time but also in terms of other factors (e.g., memory, developer effort, etc.) 1.8 What is Running Time Analysis? It is the process of determining how processing time increases as the size of the problem (input size) increases. Input size is the number of elements in the input, and depending on the problem type, the input may be of different types. The following are the common types of inputs. • Size of an array • Polynomial degree • Number of elements in a matrix • Number of bits in the binary representation of the input • Vertices and edges in a graph. 1.9 How to Compare Algorithms To compare algorithms, let us define a few objective measures: Execution times? Not a good measure as execution times are specific to a particular computer. Number of statements executed? Not a good measure, since the number of statements varies with the programming language as well as the style of the individual programmer. Ideal solution? Let us assume that we express the running time of a given algorithm as a function of the input size n (i.e., f(n)) and compare these different functions corresponding to running times. This kind of comparison is independent of machine time, programming style, etc. 1.10 What is Rate of Growth? The rate at which the running time increases as a function of input is called rate of growth. Let us
Comments 0
Loading comments...
Reply to Comment
Edit Comment