Java 9 Data Structures And Algorithms: A Step By Guide To Algorithms
User Manual: Pdf
Open the PDF directly: View PDF .
Page Count: 340
Download | |
Open PDF In Browser | View PDF |
Java 9 Data Structures and Algorithms A step-by-step guide to data structures and algorithms Debasish Ray Chawdhuri BIRMINGHAM - MUMBAI Java 9 Data Structures and Algorithms Copyright © 2017 Packt Publishing All rights reserved. No part of this book may be reproduced, stored in a retrieval system, or transmitted in any form or by any means, without the prior written permission of the publisher, except in the case of brief quotations embedded in critical articles or reviews. Every effort has been made in the preparation of this book to ensure the accuracy of the information presented. However, the information contained in this book is sold without warranty, either express or implied. Neither the author, nor Packt Publishing, and its dealers and distributors will be held liable for any damages caused or alleged to be caused directly or indirectly by this book. Packt Publishing has endeavored to provide trademark information about all of the companies and products mentioned in this book by the appropriate use of capitals. However, Packt Publishing cannot guarantee the accuracy of this information. First published: April 2017 Production reference: 1250417 Published by Packt Publishing Ltd. Livery Place 35 Livery Street Birmingham B3 2PB, UK. ISBN 978-1-78588-934-9 www.packtpub.com Credits Author Debasish Ray Chawdhuri Reviewer Miroslav Wengner Commissioning Editor Kunal Parikh Acquisition Editor Chaitanya Nair Content Development Editor Nikhil Borkar Technical Editor Madhunikita Sunil Chindarkar Copy Editor Muktikant Garimella Project Coordinator Vaidehi Sawant Proofreader Safis Editing Indexer Mariammal Chettiyar Graphics Abhinash Sahu Production Coordinator Nilesh Mohite Cover Work Nilesh Mohite About the Author Debasish Ray Chawdhuri is an established Java developer and has been in the industry for the last 8 years. He has developed several systems, right from CRUD applications to programming languages and big data processing systems. He had provided the first implementation of extensible business reporting language specification, and a product around it, for the verification of company financial data for the Government of India while he was employed at Tata Consultancy Services Ltd. In Talentica Software Pvt. Ltd., he implemented a domain-specific programming language to easily implement complex data aggregation computation that would compile to Java bytecode. Currently, he is leading a team developing a new highperformance structured data storage framework to be processed by Spark. The framework is named Hungry Hippos and will be open sourced very soon. He also blogs at http://www.geekyarticles.com/ about Java and other computer sciencerelated topics. He has worked for Tata Consultancy Services Ltd., Oracle India Pvt. Ltd., and Talentica Software Pvt. Ltd. I would like to thank my dear wife, Anasua, for her continued support and encouragement, and for putting up with all my eccentricities while I spent all my time writing this book. I would also like to thank the publishing team for suggesting the idea of this book to me and providing all the necessary support for me to finish it. About the Reviewer Miroslav Wengner has been a passionate JVM enthusiast ever since he joined SUN Microsystems in 2002. He truly believes in distributed system design, concurrency, and parallel computing. One of Miro's biggest hobby is the development of autonomic systems. He is one of the coauthors of and main contributors to the open source Java IoT/Robotics framework Robo4J. Miro is currently working on the online energy trading platform for enmacc.de as a senior software developer. I would like to thank my family and my wife, Tanja, for big support during reviewing this book. www.PacktPub.com eBooks, discount offers, and more Did you know that Packt offers eBook versions of every book published, with PDF and ePub files available? You can upgrade to the eBook version at www.PacktPub. com and as a print book customer, you are entitled to a discount on the eBook copy. Get in touch with us at customercare@packtpub.com for more details. At www.PacktPub.com, you can also read a collection of free technical articles, sign up for a range of free newsletters and receive exclusive discounts and offers on Packt books and eBooks. https://www.packtpub.com/mapt Get the most in-demand software skills with Mapt. Mapt gives you full access to all Packt books and video courses, as well as industry-leading tools to help you plan your personal development and advance your career. Why subscribe? • Fully searchable across every book published by Packt • Copy and paste, print, and bookmark content • On demand and accessible via a web browser Customer Feedback Thanks for purchasing this Packt book. At Packt, quality is at the heart of our editorial process. To help us improve, please leave us an honest review on this book's Amazon page at https://www.amazon.com/dp/1785889346. If you'd like to join our team of regular reviewers, you can e-mail us at customerreviews@packtpub.com. We award our regular reviewers with free eBooks and videos in exchange for their valuable feedback. Help us be relentless in improving our products! Table of Contents Preface Chapter 1: Why Bother? – Basic vii 1 Asymptotic upper bound of a function Asymptotic upper bound of an algorithm Asymptotic lower bound of a function Asymptotic tight bound of a function 4 8 8 9 The performance of an algorithm Best case, worst case and the average case complexity Analysis of asymptotic complexity Optimization of our algorithm Fixing the problem with large powers Improving time complexity Summary Chapter 2: Cogs and Pulleys – Building Blocks Arrays Insertion of elements in an array Insertion of a new element and the process of appending it Linked list Appending at the end Insertion at the beginning Insertion at an arbitrary position Looking up an arbitrary element Removing an arbitrary element Iteration Doubly linked list Insertion at the beginning or at the end Insertion at an arbitrary location Removing the first element Removing an arbitrary element [i] 2 3 4 10 10 11 13 15 16 16 18 20 21 23 24 25 27 28 30 31 32 33 34 Table of Contents Removal of the last element Circular linked list Insertion Removal Rotation Summary 36 37 38 38 39 40 Chapter 3: Protocols – Abstract Data Types 41 Chapter 4: Detour – Functional Programming 57 Stack Fixed-sized stack using an array Variable-sized stack using a linked list Queue Fixed-sized queue using an array Variable-sized queue using a linked list Double ended queue Fixed-length double ended queue using an array Variable-sized double ended queue using a linked list Summary Recursive algorithms Lambda expressions in Java Functional interface Implementing a functional interface with lambda Functional data structures and monads Functional linked lists The forEach method for a linked list Map for a linked list Fold operation on a list Filter operation for a linked list Append on a linked list The flatMap method on a linked list The concept of a monad Option monad Try monad Analysis of the complexity of a recursive algorithm Performance of functional programming Summary Chapter 5: Efficient Searching – Binary Search and Sorting Search algorithms Binary search Complexity of the binary search algorithm [ ii ] 42 43 45 47 48 50 52 53 55 56 58 60 60 61 62 62 65 66 67 70 74 74 76 76 82 86 89 90 91 92 93 96 Table of Contents Sorting Selection sort Complexity of the selection sort algorithm 98 98 101 Insertion sort 102 Bubble sort 105 Complexity of insertion sort 104 Inversions Complexity of the bubble sort algorithm 106 107 A problem with recursive calls Tail recursive functions Non-tail single recursive functions Summary 108 109 112 114 Chapter 6: Efficient Sorting – quicksort and mergesort 115 Chapter 7: Concepts of Tree 135 quicksort Complexity of quicksort Random pivot selection in quicksort mergesort The complexity of mergesort Avoiding the copying of tempArray Complexity of any comparison-based sorting The stability of a sorting algorithm Summary A tree data structure The traversal of a tree 115 120 122 123 126 127 129 133 134 136 139 The depth-first traversal The breadth-first traversal 140 142 The tree abstract data type Binary tree Types of depth-first traversals Non-recursive depth-first search Summary Chapter 8: More About Search – Search Trees and Hash Tables Binary search tree Insertion in a binary search tree Invariant of a binary search tree Deletion of an element from a binary search tree Complexity of the binary search tree operations Self-balancing binary search tree AVL tree Complexity of search, insert, and delete in an AVL tree [ iii ] 143 145 147 149 153 155 156 158 160 162 167 168 171 176 Table of Contents Red-black tree Insertion Deletion The worst case of a red-black tree Hash tables Insertion 177 179 183 188 190 191 The complexity of insertion 193 Search 193 Complexity of the search 194 Choice of load factor Summary 194 194 Chapter 9: Advanced General Purpose Data Structures 195 Chapter 10: Concepts of Graph 225 Priority queue ADT Heap Insertion Removal of minimum elements Analysis of complexity Serialized representation Array-backed heap Linked heap Insertion Removal of the minimal elements Complexity of operations in ArrayHeap and LinkedHeap Binomial forest Why call it a binomial tree? Number of nodes The heap property Binomial forest Complexity of operations in a binomial forest Sorting using a priority queue In-place heap sort Summary What is a graph? The graph ADT Representation of a graph in memory Adjacency matrix Complexity of operations in a sparse adjacency matrix graph More space-efficient adjacency-matrix-based graph Complexity of operations in a dense adjacency-matrix-based graph [ iv ] 195 196 197 198 199 200 201 204 206 207 209 209 211 212 212 214 221 221 222 223 226 228 229 230 234 235 243 Table of Contents Adjacency list 244 Adjacency-list-based graph with dense storage for vertices 251 Complexity of operations in an adjacency-list-based graph Complexity of the operations of an adjacency-list-based graph with dense storage for vertices Traversal of a graph Complexity of traversals Cycle detection Complexity of the cycle detection algorithm Spanning tree and minimum spanning tree For any tree with vertices V and edges E, |V| = |E| + 1 Any connected undirected graph has a spanning tree Any undirected connected graph with the property |V| = |E| + 1 is a tree Cut property Minimum spanning tree is unique for a graph that has all the edges whose costs are different from one another Finding the minimum spanning tree Union find Complexity of operations in UnionFind Implementation of the minimum spanning tree algorithm Complexity of the minimum spanning tree algorithm Summary 250 258 259 264 264 267 267 268 269 269 269 271 272 273 277 277 279 280 Chapter 11: Reactive Programming 281 Index 315 What is reactive programming? Producer-consumer model Semaphore Compare and set Volatile field Thread-safe blocking queue Producer-consumer implementation Spinlock and busy wait Functional way of reactive programming Summary [v] 282 283 283 284 285 286 289 296 301 313 Preface Java has been one of the most popular programming languages for enterprise systems for decades now. One of the reasons for the popularity of Java is its platform independence, which lets one write and compile code on any system and run it on any other system, irrespective of the hardware and the operating system. Another reason for Java's popularity is that the language is standardized by a community of industry players. The latter enables Java to stay updated with the most recent programming ideas without being overloaded with too many useless features. Given the popularity of Java, there are plenty of developers actively involved in Java development. When it comes to learning algorithms, it is best to use the language that one is most comfortable with. This means that it makes a lot of sense to write an algorithm book, with the implementations written in Java. This book covers the most commonly used data structures and algorithms. It is meant for people who already know Java but are not familiar with algorithms. The book should serve as the first stepping stone towards learning the subject. What this book covers Chapter 1, Why Bother? – Basic, introduces the point of studying algorithms and data structures with examples. In doing so, it introduces you to the concept of asymptotic complexity, big O notation, and other notations. Chapter 2, Cogs and Pulleys – Building Blocks, introduces you to array and the different kinds of linked lists, and their advantages and disadvantages. These data structures will be used in later chapters for implementing abstract data structures. Chapter 3, Protocols – Abstract Data Types, introduces you to the concept of abstract data types and introduces stacks, queues, and double-ended queues. It also covers different implementations using the data structures described in the previous chapter. [ vii ] Preface Chapter 4, Detour – Functional Programming, introduces you to the functional programming ideas appropriate for a Java programmer. The chapter also introduces the lambda feature of Java, available from Java 8, and helps readers get used to the functional way of implementing algorithms. This chapter also introduces you to the concept of monads. Chapter 5, Efficient Searching – Binary Search and Sorting, introduces efficient searching using binary searches on a sorted list. It then goes on to describe basic algorithms used to obtain a sorted array so that binary searching can be done. Chapter 6, Efficient Sorting – Quicksort and Mergesort, introduces the two most popular and efficient sorting algorithms. The chapter also provides an analysis of why this is as optimal as a comparison-based sorting algorithm can ever be. Chapter 7, Concepts of Tree, introduces the concept of a tree. It especially introduces binary trees, and also covers different traversals of the tree: breadth-first and depth-first, and pre-order, post-order, and in-order traversal of binary tree. Chapter 8, More About Search – Search Trees and Hash Tables, covers search using balanced binary search trees, namely AVL, and red-black trees and hash-tables. Chapter 9, Advanced General Purpose Data Structures, introduces priority queues and their implementation with a heap and a binomial forest. At the end, the chapter introduces sorting with a priority queue. Chapter 10, Concepts of Graph, introduces the concepts of directed and undirected graphs. Then, it discusses the representation of a graph in memory. Depth-first and breadth-first traversals are covered, the concept of a minimum-spanning tree is introduced, and cycle detection is discussed. Chapter 11, Reactive Programming, introduces the reader to the concept of reactive programming in Java. This includes the implementation of an observable patternbased reactive programming framework and a functional API on top of it. Examples are shown to demonstrate the performance gain and ease of use of the reactive framework, compared with a traditional imperative style. What you need for this book To run the examples in this book, you need a computer with any modern popular operating system, such as some version of Windows, Linux, or Macintosh. You need to install Java 9 in your computer so that javac can be invoked from the command prompt. [ viii ] Preface Who this book is for This book is for Java developers who want to learn about data structures and algorithms. A basic knowledge of Java is assumed. Conventions In this book, you will find a number of text styles that distinguish between different kinds of information. Here are some examples of these styles and an explanation of their meaning. Code words in text, database table names, folder names, filenames, file extensions, pathnames, dummy URLs, user input, and Twitter handles are shown as follows: "We can include other contexts through the use of the include directive." A block of code is set as follows: public static void printAllElements(int[] anIntArray){ for(int i=0;i|g(x)| for all sufficiently large x, then f(x) = O(h(x)). °° For example, x3 = O(x4), because if x is sufficiently large, x4 > x3. Note that whenever there is an inequality on functions, we are only interested in what happens when x is large; we don't bother about what happens for small x. To summarize the above definition, you can drop constant multipliers (rule 2) and ignore lower order terms (rule 3). You can also overestimate (rule 4). You can also do all combinations for those because rules can be applied any number of times. We had to consider the absolute values of the function to cater to the case when values are negative, which never happens in running time, but we still have it for completeness. There is something about the sign = that is not usual. Just because f(x) = O(g(x)), it does not mean, O(g(x)) = f(x). In fact, the last one does not even mean anything. It is enough for all purposes to just know the preceding definition of the big O notation. You can read the following formal definition if you are interested. Otherwise you can skip the rest of this subsection. The preceding idea can be summarized in a formal way. We say the expression f(x) = O(g(x)) means that positive constants M and x0 exist, such that |f(x)| < M|g(x)| whenever x > x0. Remember that you just have to find one example of M and x0 that satisfy the condition, to make the assertion f(x) = O(g(x)). [5] Why Bother? – Basic For example, Figure 1 shows an example of a function T(x) = 100x2+2000x+200. This function is O(x2), with some x0 = 11 and M = 300. The graph of 300x2 overcomes the graph of T(x) at x=11 and then stays above T(x) up to infinity. Notice that the function 300x2 is lower than T(x) for smaller values of x, but that does not affect our conclusion. Figure 1. Asymptotic upper bound To see that it's the same thing as the previous four points, first think of x0 as the way to ensure that x is sufficiently large. I leave it up to you to prove the above four conditions from the formal definition. I will, however, show some examples of using the formal definition: • 5x2 = O(x2) because we can say, for example, x0 = 10 and M = 10 and thus f(x) < Mg(x) whenever x > x0, that is, 5x2 < 10x2 whenever x > 10. • It is also true that 5x2 = O(x3) because we can say, for example, x0 = 10 and M = 10 and thus f(x) < Mg(x) whenever x > x0, that is, 5x2 < 10x3 whenever x > 10. This highlights a point that if f(x) = O(g(x)), it is also true that f(x) = O(h(x)) if h(x) is some functions that grows at least as fast as f(x). [6] Chapter 1 • How about the function f(x) = 5x2 - 10x + 3? We can easily see that when x is sufficiently large, 5x2 will far surpass the term 10x. To prove my point, I can simply say x>5, 5x2 > 10x. Every time we increment x by one, the increment in 5x2 is 10x + 1 and the increment in 10x is just a constant, 10. 10x+1 > 10 for all positive x, so it is easy to see why 5x2 is always going to stay above 10x as x goes higher and higher. In general, any polynomial of the form anxn + an-1xn-1 + an-2xn-2 + … + a0 = O(xn). To show this, we will first see that a0 = O(1). This is true because we can have x0 = 1 and M = 2|a0|, and we will have |a0| < 2|a0| whenever x > 1. Now, let us assume it is true for some n. Thus, anxn + an-1xn-1 + an-2xn-2 + … + a0 = O(xn). What it means, of course, is that some Mn and x0 exist, such that |anxn + an-1xn-1 + anxn-2 + … + a0 | < Mnxn whenever x>x0. We can safely assume that x0 >2, because if it is 2 not so, we can simply add 2 to it to get a new x0 , which is at least 2. Now, |anxn + an-1xn-1 + an-2xn-2 + … + a0| < Mnxn implies |an+1xn+1 + anxn + an-1xn-1 + an-2xn-2 + … + a0| ≤ |an+1xn+1| + |anxn + an-1xn-1 + an-2xn-2 + … + a0| < |an+1xn+1| + Mnxn. This means |an+1xn+1| + Mnxn > |anxn + an-1xn-1 + an-2xn-2 + … + a0|. If we take Mn+1= |an+1| + Mn, we can see that Mn+1 xn+1 = |an+1| xn+1 + Mn xn+1 =|an+1 xn+1| + Mn xn+1> |an+1 xn+1| + Mn xn > |an+1 xn+1 + anxn + an-1xn-1 + an-2xn-2 + … + a0|. That is to say, |an+1 xn+1 + an-1xn-1 + an-2xn-2 + … + a0 |< Mn+1 xn+1 for all x > x0, that is, an+1 xn+1 + anxn + an-1xn-1 + an-2xn-2 + … + a0 = O(xn+1). Now, we have it true for n=0, that is, a0 = O(1). This means, by our last conclusion, a1x + a0 = O(x). This means, by the same logic, a2 x2 + a1x + a0 = O(x2), and so on. We can easily see that this means it is true for all polynomials of positive integral degrees. [7] Why Bother? – Basic Asymptotic upper bound of an algorithm Okay, so we figured out a way to sort of abstractly specify an upper bound on a function that has one argument. When we talk about the running time of a program, this argument has to contain information about the input. For example, in our algorithm, we can say, the execution time equals O(power). This scheme of specifying the input directly will work perfectly fine for all programs or algorithms solving the same problem because the input will be the same for all of them. However, we might want to use the same technique to measure the complexity of the problem itself: it is the complexity of the most efficient program or algorithm that can solve the problem. If we try to compare the complexity of different problems, though, we will hit a wall because different problems will have different inputs. We must specify the running time in terms of something that is common among all problems, and that something is the size of the input in bits or bytes. How many bits do we need to express the argument, power, when it's sufficiently large? Approximately log2 (power). So, in specifying the running time, our function needs to have an input that is of the size log2 (power) or lg (power). We have seen that the running time of our algorithm is proportional to the power, that is, constant times power, which is constant times 2 lg(power) = O(2x),where x= lg(power), which is the the size of the input. Asymptotic lower bound of a function Sometimes, we don't want to praise an algorithm, we want to shun it; for example, when the algorithm is written by someone we don't like or when some algorithm is really poorly performing. When we want to shun it for its horrible performance, we may want to talk about how badly it performs even for the best input. An a symptotic lower bound can be defined just like how greater-than-or-equal-to can be defined in terms of less-than-or-equal-to. A function f(x) = Ω(g(x)) if and only if g(x) = O(f(x)). The following list shows a few examples: • Since x3 = O(x3), x3 = Ω(x3) • Since x3 = O(5x3), 5x3 = Ω(x3) • Since x3 = O(5x3 - 25x2 + 1), 5x3 - 25x2 + 1 = Ω(x3) • Since x3 = O(x4), x4 = O(x3) Again, for those of you who are interested, we say the expression f(x) = Ω(g(x)) means there exist positive constants M and x0, such that |f(x)| > M|g(x)| whenever x > x0, which is the same as saying |g(x)| < (1/M)|f(x)| whenever x > x0 , that is, g(x) = O(f(x)). [8] Chapter 1 The preceding definition was introduced by Donald Knuth, which was a stronger and more practical definition to be used in computer science. Earlier, there was a different definition of the lower bound Ω that is more complicated to understand and covers a few more edge cases. We will not talk about edge cases here. While talking about how horrible an algorithm is, we can use an asymptotic lower bound of the best case to really make our point. However, even a criticism of the worst case of an algorithm is quite a valid argument. We can use an asymptotic lower bound of the worst case too for this purpose, when we don't want to find out an asymptotic tight bound. In general, the asymptotic lower bound can be used to show a minimum rate of growth of a function when the input is large enough in size. Asymptotic tight bound of a function There is another kind of bound that sort of means equality in terms of asymptotic complexity. A theta bound is specified as f(x) = Ͽ(g(x)) if and only if f(x) = O(g(x)) and f(x) = Ω(g(x)). Let's see some examples to understand this even better: • Since 5x3=O(x3) and also 5x3=Ω(x3), we have 5x3=Ͽ(x3) • Since 5x3 + 4x2=O(x3) and 5x3 + 4x2=Ω(x3), we have 5x3 + 4x2=O(x3) • However, even though 5x3 + 4x2 =O(x4), since it is not Ω(x4), it is also not Ͽ(x4) • Similarly, 5x3 + 4x2 is not Ͽ(x2) because it is not O(x2) In short, you can ignore constant multipliers and lower order terms while determining the tight bound, but you cannot choose a function which grows either faster or slower than the given function. The best way to check whether the bound is right is to check the O and the condition separately, and say it has a theta bound only if they are the same. Note that since the complexity of an algorithm depends on the particular input, in general, the tight bound is used when the complexity remains unchanged by the nature of the input. [9] Why Bother? – Basic In some cases, we try to find the average case complexity, especially when the upper bound really happens only in the case of an extremely pathological input. But since the average must be taken in accordance with the probability distribution of the input, it is not just dependent on the algorithm itself. The bounds themselves are just bounds for particular functions and not for algorithms. However, the total running time of an algorithm can be expressed as a grand function that changes it's formula as per the input, and that function may have different upper and lower bounds. There is no sense in talking about an asymptotic average bound because, as we discussed, the average case is not just dependent on the algorithm itself, but also on the probability distribution of the input. The average case is thus stated as a function that would be a probabilistic average running time for all inputs, and, in general, the asymptotic upper bound of that average function is reported. Optimization of our algorithm Before we dive into actually optimizing algorithms, we need to first correct our algorithm for large powers. We will use some tricks to do so, as described below. Fixing the problem with large powers Equipped with all the toolboxes of asymptotic analysis, we will start optimizing our algorithm. However, since we have already seen that our program does not work properly for even moderately large values of power, let's first fix that. There are two ways of fixing this; one is to actually give the amount of space it requires to store all the intermediate products, and the other is to do a trick to limit all the intermediate steps to be within the range of values that the long datatype can support. We will use binomial theorem to do this part. As a reminder, binomial theorem says (x+y)n = xn + nC1xn-1y + nC2xn-2y2 + nC3xn-3y3 + n C4xn-4y4 + … nCn-1x1yn-1 + yn for positive integral values of n. The important point here is that all the coefficients are integers. Suppose, r is the remainder when we divide a by b. This makes a = kb + r true for some positive integer k. This means r = a-kb, and rn = (a-kb)n. If we expand this using binomial theorem, we have rn = an - nC1 an-1.kb + nC2an-2.(kb)2 n C3an-3.(kb)3 + nC4an-4.(kb)4 + … nCn-1a1.(kb)n-1 ± (kb)n. Note that apart from the first term, all other terms have b as a factor. Which means that we can write rn = an + bM for some integer M. If we divide both sides by b now and take the remainder, we have rn % b = an % b, where % is the Java operator for finding the remainder. [ 10 ] Chapter 1 The idea now would be to take the remainder by the divisor every time we raise the power. This way, we will never have to store more than the range of the remainder: public static long computeRemainderCorrected(long base, long power, long divisor){ long baseRaisedToPower = 1; for(long i=1;i<=power;i++){ baseRaisedToPower *= base; baseRaisedToPower %= divisor; } return baseRaisedToPower; } This program obviously does not change the time complexity of the program; it just fixes the problem with large powers. The program also maintains a constant space complexity. Improving time complexity The current running time complexity is O(2x), where x is the size of the input as we have already computed. Can we do better than this? Let's see. What we need to compute is (basepower) % divisor. This is, of course, the same as (base2)power/2 % divisor. If we have an even power, we have reduced the number of operations by half. If we can keep doing this, we can raise the power of base by 2n in just n steps, which means our loop only has to run lg(power) times, and hence, the complexity is O(lg(2x)) = O(x), where x is the number of bits to store power. This is a substantial reduction in the number of steps to compute the value for large powers. However, there is a catch. What happens if the power is not divisible by 2? Well, then we can write (basepower)% divisor = (base ((basepower-1))%divisor = (base ((base2)power-1)%divisor, and power-1 is, of course, even and the computation can proceed. We will write up this code in a program. The idea is to start from the most significant bit and move towards less and less significant bits. If a bit with 1 has n bits after it, it represents multiplying the result by the base and then squaring n times after this bit. We accumulate this squaring by squaring for the subsequent steps. If we find a zero, we keep squaring for the sake of accumulating squaring for the earlier bits: public static long computeRemainderUsingEBS(long base, long power, long divisor){ long baseRaisedToPower = 1; long powerBitsReversed = 0; int numBits=0; [ 11 ] Why Bother? – Basic First reverse the bits of our power so that it is easier to access them from the least important side, which is more easily accessible. We also count the number of bits for later use: while(power>0){ powerBitsReversed <<= 1; powerBitsReversed += power & 1; power >>>= 1; numBits++; } Now we extract one bit at a time. Since we have already reversed the order of bit, the first one we get is the most significant one. Just to get an intuition on the order, the first bit we collect will eventually be squared the maximum number of times and hence will act like the most significant bit: while (numBits-->0){ if(powerBitsReversed%2==1){ baseRaisedToPower *= baseRaisedToPower * base; }else{ baseRaisedToPower *= baseRaisedToPower; } baseRaisedToPower %= divisor; powerBitsReversed>>>=1; } return baseRaisedToPower; } We test the performance of the algorithm; we compare the time taken for the same computation with the earlier and final algorithms with the following code: public static void main(String [] args){ System.out.println(computeRemainderUsingEBS(13, 10_000_000, 7)); long startTime = System.currentTimeMillis(); for(int i=0;i<1000;i++){ computeRemainderCorrected(13, 10_000_000, 7); } long endTime = System.currentTimeMillis(); System.out.println(endTime - startTime); startTime = System.currentTimeMillis(); for(int i=0;i<1000;i++){ computeRemainderUsingEBS(13, 10_000_000, 7); } endTime = System.currentTimeMillis(); System.out.println(endTime - startTime); } [ 12 ] Chapter 1 The first algorithm takes 130,190 milliseconds to complete all 1,000 times execution on my computer and the second one takes just 2 milliseconds to do the same. This clearly shows the tremendous gain in performance for a large power like 10 million. The algorithm for squaring the term repeatedly to achieve exponentiation like we did is called... well, exponentiation by squaring. This example should be able to motivate you to study algorithms for the sheer obvious advantage it can give in improving the performance of computer programs. Summary In this chapter, you saw how we can think about measuring the running time of and the memory required by an algorithm in seconds and bytes, respectively. Since this depends on the particular implementation, the programming platform, and the hardware, we need a notion of talking about running time in an abstract way. Asymptotic complexity is a measure of the growth of a function when the input is very large. We can use it to abstract our discussion on running time. This is not to say that a programmer should not spend any time to make a run a program twice as fast, but that comes only after the program is already running at the minimum asymptotic complexity. We also saw that the asymptotic complexity is not just a property of the problem at hand that we are trying to solve, but also a property of the particular way we are solving it, that is, the particular algorithm we are using. We also saw that two programs solving the same problem while running different algorithms with different asymptotic complexities can perform vastly differently for large inputs. This should be enough motivation to study algorithms explicitly. In the following chapters, we will study the most used algorithmic tricks and concepts required in daily use. We will start from the very easy ones that are also the building blocks for the more advanced techniques. This book is, of course, by no means comprehensive; the objective is to provide enough background to make you comfortable with the basic concepts and then you can read on. [ 13 ] Cogs and Pulleys – Building Blocks We discussed algorithms in the previous chapter, but the title of the book also includes the term "data structure." So what is a data structure? A data structure is an organization of data in memory that is generally optimized so it can be used by a particular algorithm. We have seen that an algorithm is a list of steps that leads to a desired outcome. In the case of a program, there is always some input and output. Both input and output contain data and hence must be organized in some way or another. Therefore, the input and output of an algorithm are data structures. In fact, all the intermediate states that an algorithm has to go through must also be stored in some form of a data structure. Data structures don't have any use without algorithms to manipulate them, and algorithms cannot work without data structures. It's because this is how they get input and emit output or store their intermediate states. There are a lot of ways in which data can be organized. Simpler data structures are also different types of variables. For example, int is a data structure that stores one 4-byte integer value. We can even have classes that store a set of specific types of values. However, we also need to think about how to store a collection of a large number of the same type of values. In this book, we will spend the rest of the time discussing a collection of values of the same type because how we store a collection determines which algorithm can work on them. Some of the most common ways of storing a collection of values have their own names; we will discuss them in this chapter. They are as follows: • Arrays • Linked lists • Doubly linked lists • Circular linked lists [ 15 ] Cogs and Pulleys – Building Blocks These are the basic building blocks that we will use to build more complex data structures. Even if we don't use them directly, we will use their concepts. Arrays If you are a Java programmer, you must have worked with arrays. Arrays are the basic storage mechanisms available for a sequence of data. The best thing about arrays is that the elements of an array are collocated sequentially and can be accessed completely and randomly with single instructions. The traversal of an array element by an element is very simple. Since any element can be accessed randomly, you just keep incrementing an index and keep accessing the element at this index. The following code shows both traversal and random access in an array: public static void printAllElements(int[] anIntArray){ for(int i=0;i =targetIndex;i--){ array[i+1]=array[i]; } array[targetIndex]=value; } } [ 17 ] Cogs and Pulleys – Building Blocks What would be the running time complexity of the preceding algorithm? For all our cases, we will only consider the worst case. When does an algorithm perform worst? To understand this, let's see what the most frequent operation in an algorithm is. It is of course the shift that happens in the loop. The number of shifts become maximum when startIndex is at the beginning of the array and targetIndex at the end or vice versa. This is when all but one element has to be shifted one by one. The running time in this case must be some constant times the number of elements of the array plus some other constant to account for the non-repeating operations. So it is T(n) = K(n-1)+C for some constants K and C, where n is the number of elements in the array and T(n) is the running time of the algorithm. This can be expressed as follows: T(n) = K(n-1)+C = Kn + (C-K) The following steps explain the expression: 1. As per rule 1 of the definition of big O, T(n) = O(Kn + (C-K)). 2. As per rule 3, T(n) = O(Kn). 3. We know |-(C-K)| < |Kn + (C-K)| is true for sufficiently large n. Therefore, as per rule 3, since T(n) = O(Kn + (C-K)), it means T(n) = O(Kn + (C-K) + (-(CK))), that is, T(n) = O(Kn). 4. And, finally, as per rule 2, T(n) = O(n). Now since the array is the major input in the algorithm, the size of the input is represented by n. So we will say, the running time of the algorithm is O(n), where n is the size of the input. Insertion of a new element and the process of appending it Now we move on to the process of insertion of a new element. Since arrays are fixed in size, insertion requires us to create a new array and copy all the earlier elements into it. The following figure explains the idea of an insertion made in a new array: Figure 2: Insertion of a new element into an array [ 18 ] Chapter 2 The following code does exactly that: public static int [] insertExtraElementAtIndex(int[] array, int index, int value){ int [] newArray = new int[array.length+1]; First, you copy all the elements before the targeted position as they are in the original array: for(int i=0;i implements Iterable , Visualizable { First, we create a Node class inside the LinkedList class, which will act as a wrapper for the elements and also hold the reference to the next node: protected static class Node { protected E value; protected Node next; public String toString(){ return value.toString(); } } int length = 0; Node [] lastModifiedNode; [ 20 ] Chapter 2 Then, we must have references for the first and last elements: Node first; Node last; Finally, we create a method called getNewNode() that creates a new empty node. We will need this if we want to use a different class for a node in any of the subclasses: protected Node getNewNode() { Node node = new Node<>(); lastModifiedNode = new Node[]{node}; return node; } } At this point, the unfinished class LinkedList will not be able to store any element; let's see how to do this, though. Notice that we have implemented the Iterable interface. This will allow us to loop through all the elements in an advanced for loop. Appending at the end Appending at the end is achieved by simply creating a link from the last element of the original linked list to the new element that is being appended and then reassigning the reference to the last element. The second step is required because the new element is the new last element. This is shown in the following figure: Figure 4: Appending at the end of a linked list [ 21 ] Cogs and Pulleys – Building Blocks There is a small difference when you append an element to a linked list that is empty to start with. At this point, the first and last references are null, and this case must be handled separately. The following figure explains this case: Figure 5: Appending to an empty linked list We will achieve this by using the following simple code as is. We return the node that has just been added. This is helpful to any class that is extending this class. We will do the same in all cases, and we will see the use of this while discussing doubly linked lists: public Node appendLast(E value) { Node node = getNewNode(); node.value = value; We try to update the reference of the current last node only if the list is not empty: if (last != null) last.next = node; Then, we must update the last reference as the new element is not going to be the last element: last = node; Finally, if the list is empty, the new element must also be the first new element and we must update the first reference accordingly, as shown in the preceding figure: if (first == null) { first = node; } length++; return node; } [ 22 ] Chapter 2 Notice that we also keep track of the current length of the list. This is not essential, but if we do this, we do not have to traverse the entire list just to count how many elements are in the list. Now, of course, there is this important question: what is the time complexity of appending to a linked list? Well, if we do it the way we have done it before—that is, by having a special reference to the last element—we don't need any loop, as we can see in the code. If the program does not have any loops, all operations would be onetime operations, hence everything is completed in constant time. You can verify that a constant function has this complexity: O(1). Compare this with what was appended at the end of an array. It required the creation of a new array and also had O(n) complexity, where n was the size of the array. Insertion at the beginning Inserting an element at the beginning of a list is very similar to appending it at the end. The only difference is that you need to update the first reference instead of the last reference: public Node appendFirst(E value) { Node node = getNewNode(); node.value = value; node.next = first; first = node; if (length == 0) last = node; length++; return node; } [ 23 ] Cogs and Pulleys – Building Blocks Insertion at an arbitrary position Insertion at an arbitrary position can be achieved in the same way we perform an insertion in the first element, except that we update the reference of the previous element instead of the first reference. There is, however, a catch; we need to find the position where we need to insert the element. There is no way to find it other than to start at the beginning and walk all the way to the correct position while counting each node we step on. Figure 6: Insertion of an arbitrary element into a linked list We can implement the idea as follows: public Node insert(int index, E value) { Node node = getNewNode(); First, we take care of the special cases: if (index < 0 || index > length) { throw new IllegalArgumentException("Invalid index for insertion"); } else if (index == length) { return appendLast(value); } else if (index == 0) { return appendFirst(value); } else { As mentioned earlier, we walk all the way to the desired position while counting the nodes, or in this case, counting the index in the opposite direction: Node result = first; while (index > 1) { index--; result = result.next; } [ 24 ] Chapter 2 Finally, we update the references: node.value = value; node.next = result.next; result.next = node; length++; return node; } } What is the complexity of this algorithm? There is a loop that must run as many times as the index. This algorithm seems to have a running time that is dependent on the value of the input and not just its size. In this case, we are only interested in the worst case. What is the worst case then? It is when we need to step on all the elements of the list, that is, when we have to insert the element at the end of the list, except for the last element. In this case, we must step on n-1 elements to get there and do some constant work. The number of steps would then be T(n) = C(n-1)+K for some constants C and K. So, T(n) = O(n). Looking up an arbitrary element Finding the value of an arbitrary element has two different cases. For the first and last element, it is simple. Since we have direct references to the first and last element, we just have to traverse that reference and read the value inside it. I leave this for you to see how it could be done. However, how do you read an arbitrary element? Since we only have forward references, we must start from the beginning and walk all the way, traversing references while counting steps until we reach the element we want. Let's see how we can do this: public E findAtIndex(int index) { We start from the first element: Node result = first; while (index >= 0) { if (result == null) { throw new NoSuchElementException(); } else if (index == 0) { [ 25 ] Cogs and Pulleys – Building Blocks When the index is 0, we would have finally reached the desired position, so we return: return result.value; } else { If we are not there yet, we must step onto the next element and keep counting: index--; result = result.next; } } return null; } Here too, we have a loop inside that has to run an index a number of times. The worst case is when you just need to remove one element but it is not the last one; the last one can be found directly. It is easy to see that just like you insert into an arbitrary position, this algorithm also has running time complexity of O(n). Figure 7: Removing an element in the beginning Removing an element in the beginning means simply updating the reference to the first element with that of the next element. Note that we do not update the reference in the element that has just been removed because the element, along with the reference, would be garbage-collected anyway: public Node removeFirst() { if (length == 0) { throw new NoSuchElementException(); } [ 26 ] Chapter 2 Assign the reference to the next element: Node origFirst = first; first = first.next; length--; If there are no more elements left, we must also update the last reference: if (length == 0) { last = null; } return origFirst; } Removing an arbitrary element Removing an arbitrary element is very similar to removing an element from the beginning, except that you update the reference held by the previous element instead of the special reference named first. The following figure shows this: Figure 8: Removing an arbitrary element Notice that only the link in the linked list is to be reassigned to the next element. The following code does what is shown in the preceding figure: protected Node removeAtIndex(int index) { if (index >= length || index < 0) { throw new NoSuchElementException(); } [ 27 ] Cogs and Pulleys – Building Blocks Of course, removing the first element is a special case: if (index == 0) { Node nodeRemoved = first; removeFirst(); return nodeRemoved; } First, find out the element just before the one that needs to be removed because this element would need its reference updated: Node justBeforeIt = first; while (--index > 0) { justBeforeIt = justBeforeIt.next; } Update the last reference if the last element is the one that is being removed: Node nodeRemoved = justBeforeIt.next; if (justBeforeIt.next == last) { last = justBeforeIt.next.next; } Update the reference held by the previous element: justBeforeIt.next = justBeforeIt.next.next; length--; return nodeRemoved; } It is very easy to see that the running time worst case complexity of this algorithm is O(n)—which is similar to finding an arbitrary element—because this is what needs to be done before removing it. The operation of the actual removal process itself requires only a constant number of steps. Iteration Since we are working in Java, we prefer to implement the Iterable interface. It lets us loop through the list in a simplified for loop syntax. For this purpose, we first have to create an iterator that will let us fetch the elements one by one: protected class ListIterator implements Iterator { protected Node nextNode = first; @Override [ 28 ] Chapter 2 public boolean hasNext() { return nextNode != null; } @Override public E next() { if (!hasNext()) { throw new IllegalStateException(); } Node nodeToReturn = nextNode; nextNode = nextNode.next; return nodeToReturn.value; } } The code is self-explanatory. Every time it is invoked, we move to the next element and return the current element's value. Now we implement the iterator method of the Iterable interface to make our list an iterable: @Override public Iterator iterator() { return new ListIterator(); } This enables us to use the following code: for(Integer x:linkedList){ System.out.println(x); } The preceding code assumes that the variable linkedList was LinkedList . Any list that extends this class will also get this property automatically. [ 29 ] Cogs and Pulleys – Building Blocks Doubly linked list Did you notice that there is no quick way to remove the element from the end of a linked list? This is because even if there is a quick way to find the last element, there is no quick way to find the element before it whose reference needs to be updated. We must walk all the way from the beginning to find the previous element. Well then, why not just have another reference to store the location of the last but one element? This is because after you remove the element, how would you update the reference otherwise? There would be no reference to the element right before that. What it looks like is that to achieve this, we have to store the reference of all the previous elements up to the beginning. The best way to do this would be to store the reference of the previous element in each of the elements or nodes along with the reference to the next element. Such a linked list is called a doubly linked list since the elements are linked both ways: Figure 9: Doubly linked list We will implement a doubly linked list by extending our original linked list because a lot of the operations would be similar. We can create the barebones class in the following manner: public class DoublyLinkedList extends LinkedList { We create a new Node class extending the original one and adding a reference for the previous node: protected static class DoublyLinkedNode extends Node { protected DoublyLinkedNode prev; } Of course, we need to override the getNode() method to use this node: @Override protected Node getNewNode() { return new DoublyLinkedNode (); } } [ 30 ] Chapter 2 Insertion at the beginning or at the end Insertion at the beginning is very similar to that of a singly linked list, except that we must now update the next node's reference for its previous node. The node being inserted does not have a previous node in this case, so nothing needs to be done: public Node appendFirst(E value) { Node node = super.appendFirst(value); if (first.next != null) ((DoublyLinkedNode ) first.next).prev = (DoublyLinkedNode ) first; return node; } Pictorially, it can be visualized as shown in the following figure: Figure 10: Insertion at the beginning of a doubly linked list Appending at the end is very similar and is given as follows: public Node appendLast(E value) { DoublyLinkedNode origLast = (DoublyLinkedNode ) this.last; Node node = super.appendLast(value); If the original list were empty, the original last reference would be null: if (origLast == null) { origLast = (DoublyLinkedNode ) first; } ((DoublyLinkedNode ) this.last).prev = origLast; return node; } [ 31 ] Cogs and Pulleys – Building Blocks The complexity of the insertion is the same as that of a singly linked list. In fact, all the operations on a doubly linked list have the same running time complexity as that of a singly linked list, except the process of removing the last element. We will thus refrain from stating it again until we discuss the removal of the last element. You should verify that the complexity stays the same as with a singly linked list in all other cases. Insertion at an arbitrary location As with everything else, this operation is very similar to the process of making an insertion at an arbitrary location of a singly linked list, except that you need to update the references for the previous node. Figure 11: Insertion at an arbitrary location of a doubly linked list The following code does this for us: public Node insert(int index, E value) { DoublyLinkedNode inserted = (DoublyLinkedNode ) super.insert(index, value); [ 32 ] Chapter 2 In the case of the first and last element, our overridden methods are invoked anyway. Therefore, there is no need to consider them again: if(index!=0 && index!=length) { if (inserted.next != null) { This part needs a little bit of explaining. In Figure 11, the node being inserted is 13. Its previous node should be 4, which was originally the previous node of the next node 3: inserted.prev = ((DoublyLinkedNode ) inserted.next).prev; The prev reference of the next node 3 must now hold the newly inserted node 13: ((DoublyLinkedNode ) inserted.next).prev = inserted; } } return inserted; } Removing the first element Removing the first element is almost the same as that for a singly linked list. The only additional step is to set the prev reference of the next node to null. The following code does this: public Node removeFirst() { super.removeFirst(); if (first != null) { ((DoublyLinkedNode ) first).prev = null; } return first; } [ 33 ] Cogs and Pulleys – Building Blocks The following figure shows what happens. Also, note that finding an element does not really need an update: Figure 12: Removal of the first element from a doubly linked list There can be an optimization to traverse backward from the last element to the first in case the index we are looking for is closer toward the end; however, it does not change the asymptotic complexity of the find operation. So we leave it at this stage. If interested, you would be able to easily figure out how to do this optimization. Removing an arbitrary element Just like other operations, removal is very similar to removal of elements in the case of a singly linked list, except that we need to update the prev reference: [ 34 ] Chapter 2 Figure 13: Removal of an arbitrary element from a doubly linked list The following code will help us achieve this: public Node removeAtIndex(int index) { if(index<0||index>=length){ throw new NoSuchElementException(); } This is a special case that needs extra attention. A doubly linked list really shines while removing the last element. We will discuss the removeLast() method in the next section: if(index==length-1){ return removeLast(); } [ 35 ] Cogs and Pulleys – Building Blocks The rest of the code is fairly easy to figure out: DoublyLinkedNode nodeRemoved = (DoublyLinkedNode ) super.removeAtIndex(index); if ((DoublyLinkedNode ) nodeRemoved.next != null) ((DoublyLinkedNode ) nodeRemoved.next).prev = nodeRemoved.prev; return nodeRemoved; } Removal of the last element This is where a doubly linked list really shines. This is the reason we got started with a doubly linked list. And it's not even a lot of code. Check this out: public Node removeLast() { Node origLast = last; if(last==null){ throw new IllegalStateException ("Removing element from an empty list"); } Just use the fact that we have access to the previous node's reference and we can update the last reference very easily: last = ((DoublyLinkedNode )last).prev; If the list is not empty after removal, set the next reference of the new last element to null. If the new list is empty instead, update the first element as well: if(last!=null){ last.next = null; } else{ first = null; } Don't forget to update the length: length--; return origLast; } [ 36 ] Chapter 2 We don't need a new figure to understand the update of the references as they are really similar to the removal process of the first element. The only difference from the singly linked list is that in the case of a singly linked list, we need to walk all the way to the end of the list to find the previous element of the list. However, in the case of a doubly linked list, we can update it in one step because we always have access to the previous node's reference. This drastically reduces the running time from O(n) in the case of a singly linked list to O(1) in the case of a doubly linked list. Circular linked list A circular linked list is an ordinary linked list, except that the last element holds the reference to the first element as its next element. This, of course, justifies its name. It would be useful when, for example, you are holding a list of players in a list and they play in turn in a round robin fashion. The implementation is simplified if you use a circular linked list and just keep rotating as the players complete their turn: Figure 14: A circular linked list [ 37 ] Cogs and Pulleys – Building Blocks The basic structure of a circular linked list is the same as that of a simple linked list; no more fields or methods are required: public class CircularLinkedList extends LinkedList { } Insertion This is the same as the insertion for a simple linked list, except that you assign the last references next to the first: @Override public Node appendFirst(E value) { Node newNode = super.appendFirst(value); last.next = first; return newNode; } From this, it is not hard to guess how it would be to append at the end: @Override public Node appendLast(E value) { Node newNode = super.appendLast(value); last.next = first; return newNode; } Insertion at any other index, of course, remains the same as that for a simple linked list; no more changes are required. This means the complexity of the insertion stays the same as with that for a simple linked list. Removal Removal also only changes when you remove the first or the last element. In any case, just updating the last element's next reference solves the purpose. The only place where we need to change this is when we remove the first element. This is because the same operation we used for a simple linked list does not update the previous element's next reference, which we need to do: @Override public Node removeFirst() { Node newNode = super.removeFirst(); last.next = first; return newNode; } Nothing else needs to be done in removal. [ 38 ] Chapter 2 Rotation What we are doing here is just bringing the next element of the first element to the first position. This is exactly what the name "rotation" would imply: public void rotate(){ last = first; first = first.next; } Figure 15: Rotation of a circular linked list [ 39 ] Cogs and Pulleys – Building Blocks Doing the same with a simple linked list would require no more than assigning one more reference. You should be able to figure out how to do this with a simple linked list. But this operation looks more natural for a circular linked list, as conceptually, there is no first element. The real power of a circular linked list is the iterator, which never ends. If the list is non-empty, the iterator will have hasNext(), which always returns true. This means you can simply keep calling the next() method on the iterator and keep processing the elements in a round robin fashion. The following code should make it clear what I mean: for(int i=0;i<30;i++){ System.out.print(" "+ linkedList.first); linkedList.rotate(); } Note that if you try to use the enhanced for loop with a circular linked list, you will run into an infinite loop. Summary We covered a few basic data structures and the algorithms for manipulating them. In addition to this, we also found out their running time complexities. To summarize this, an array provides you with the fastest random access there is with this time complexity: O(1). But arrays cannot change size; the only modification they allow is to change the value of an element. A linked list allows fast append at the end and insertion at the beginning at O(1) time. However, O(1) removal is only available for removing the first element. This is resolved by a doubly linked list that also allows O(1) removal from the end. A circular linked list holds a reference to the first element in the next reference of the last element. This makes the list a circular structure that allows one to loop indefinitely. In the upcoming chapters, we will discuss the abstraction of data structures called abstract data types. We will use the data structures we have seen in this chapter to implement the abstract data types, which in turn will be used in later chapters. [ 40 ] Protocols – Abstract Data Types In the last chapter, we saw a few basic data structures and some algorithms to manipulate them. However, sometimes we may want to hide the implementation details of a data structure and only want to know how they interact with other algorithms. We may want to specify a few operations that they must allow and forget about how they are achieved. This is not very different from abstraction of a part of a program in any large software application. For example, in Java, we create interfaces that only define the methods of an object that its class must implement, and then we use this interface type, being confident that they will be implemented properly. We do not want to think about how an implementation class would provide their implementation. Such interfaces of data structure are called abstract data types. To put this another way, an abstract data type (ADT) is a description of what a data structure should do for its user. It is a list of operations that any implementation must support and the complete description of what these operations are supposed to do. A few of these have very frequent usage and have names given to them. We will discuss a few of these here. In this chapter, you will learn about the following concepts: • The definition of some common ADTs and their operations • How to implement these ADTs using both simple arrays and the data structures you learned in the last chapter [ 41 ] Protocols – Abstract Data Types Stack A stack is a very commonly used ADT. It is so named because it resembles a stack of plates used in a restaurant. In such a stack, a plate that has been washed and put last would stay on top. This would be the first plate to be picked up when a plate is needed. The plate that went in first would be at the bottom of the stack and would be picked last. So, the last plate to be placed in the stack is the first plate to get out, we can also call this last in first out (LIFO). Similarly, a stack ADT has a protocol where the last value that is put in it must be returned on the first attempt to get a value out, and the value that went in first must come out last. The following figure will make it more clear: The operation of putting a new value in a stack is called push, and the operation of retrieving a value from a stack is called pop. The element that was pushed last must be popped first. The operation that allows one to see what the next pop will return is called peek. The peek operation returns the top element without modifying the stack. We expect all stack implementations to have all operations implemented in the time complexity of O(1). This is also part of our stack protocol. The stack ADT has the following operations: • Push: This adds an element at the top of the stack • Pop: This removes the element at the top of the stack • Peek: This checks the next value to be popped [ 42 ] Chapter 3 Since we know that ADTs are to data structures what interfaces are to classes, we will code an ADT as an interface. The following is our interface for a stack: public interface Stack { void push(E value); E pop(); E peek(); } Of course, we will not leave it at this. We will see how a stack can actually be implemented. To this end, we will see both a fixed-sized stack using an array to store it's data, and a growing stack using a linked list for storing data. We will start with the first. Fixed-sized stack using an array A fixed-sized stack uses a pre-allocated array to store values, that is when this stack has used up the entire array, it can no longer accept new values until the old ones are popped. This is not very different from an actual stack of plates, which most certainly has a maximum height that it can handle. As always, we start with the basic structure of the class, as follows: public class StackImplArray implements Stack { We need an array to store the elements, and we need to remember where the top of the stack is in that array. The top always marks the index of the element that will be popped next. When there are no more elements to be popped, it is set to -1. Why -1? Because this is the natural choice as it does not require any special handling when the first element is inserted: protected E[] array; int top=-1; public StackImplArray(int size){ array = (E[])new Object[size]; } } The push operation in a stack can be to simply put the value in the array right next to the current top and then set the top to the new position, as illustrated in the following code: @Override public void push(E value) { [ 43 ] Protocols – Abstract Data Types We first check whether the stack is already full or the current top is equal to the maximum index possible, like this: if(top == array.length-1){ throw new NoSpaceException("No more space in stack"); } Now, we set the top to the new position and put the value we need to store in there as follows: top++; array[top] = value; } The exception we used is a custom exception for this purpose. The code of the exception is simple as shown in the following code: public class NoSpaceException extends RuntimeException{ public NoSpaceException(String message) { super(message); } } The pop operation is just the opposite. We need to first take the value of the current top and then update the top to the new position, which is one less than the current position, as shown in the following code: @Override public E pop() { We first check whether the stack is already empty, in which case we return a special value, null. This is shown in the following code: if(top==-1){ return null; } Then we update the top and return the value at the current top as follows: top--; return array[top+1]; } [ 44 ] Chapter 3 The peek operation does not change the state of the stack, and hence is even simpler: @Override public E peek() { Just like the pop operation, we return null if the stack is empty: if(top==-1){ return null; } Otherwise, we return the top element, as follows: return array[top]; } It is in fact possible to have a stack without an upper limit backed up by an array. What we really need to do is that whenever we run out of space, we can resize the array. Array actually cannot be resized, so the operation would be to create a new array with a higher size (maybe twice as much as the original size), and copy all the old elements into this array. Since this involves copying all the n elements to the new array one by one, the complexity of this operation is O(n). Variable-sized stack using a linked list The problem with an array-based implementation is that since arrays are fixed in size, the stacks cannot grow beyond a fixed-size. To resolve this, we have to do what we did to fix the same problem for an array, that is, use a linked list instead. We start such an implementation with the following bare bone class. The linked list will store the values. Instead of assigning a new linked list to it, we do so using an overridable method getNewLinkedList(). This will be useful in the class that extends from this one: public class StackImplLinkedList implements Stack { protected LinkedList list = getNewLinkedList(); protected LinkedList getNewLinkedList(){ return new LinkedList<>(); } } [ 45 ] Protocols – Abstract Data Types To see which end of the linked list must be used as the top of the stack, we need to remember that our stack protocol expects the operations to be O(1), so we must choose an end that allows both insertion and removal in O(1) time. That end is of course the front of the list as we saw in the last chapter. This makes the following code for the push operation self-explanatory: @Override public void push(E value) { list.appendFirst(value); } Note that this time, we did not check whether the stack is full because this implementation of the stack is never full, it grows as it needs and the underlying linked list takes care of that. The pop operation, however, does need to check whether the stack is empty and return null at that point. The following code for the pop operation is also quite self-explanatory: @Override public E pop() { if(list.getLength()==0){ return null; } E value = list.getFirst(); list.removeFirst(); return value; } The peek operation is, of course, the same, except it does not remove the top element: @Override public E peek() { if(list.getLength()==0){ return null; } return list.getFirst(); } This concludes our linked list-based implementation of a stack. In the next section, we will check out another ADT called a queue. [ 46 ] Chapter 3 Queue What is the opposite of a stack? This may be a weird question. However, a stack follows LIFO, last in first out. The opposite of that is first-in-first-out (FIFO). So, in some sense, a FIFO ADT can be considered as the opposite of a stack. This is not very different from a queue of people waiting for a bus or at a doctor's clinic. The first person to show up gets the first chance to get onto the bus or to get to see the doctor. The second person gets the second chance. No wonder, such an abstract data type is called a queue. Appending to the end of a queue is called enqueuing and removing from it is called dequeuing. The contract is, of course, that the first value that is enqueued would be the first to be dequeued. The following figure illustrates this operation: The queue ADT has the following operations: • Enqueue: This adds an element at the back of the queue • Dequeue: This removes an element from the front of the queue • Peek: This checks the element that would be dequeued next The queue will be represented by the following interface: public interface Queue { void enqueue(E value); E dequeue(); E peek(); } [ 47 ] Protocols – Abstract Data Types Fixed-sized queue using an array Just like the stack, we have an array-based implementation of a queue. However, since a queue receives new values and removes old values from opposite sides, the body of the queue moves as it does. The following figure will illustrate this point: [ 48 ] Chapter 3 This means that after a sequence of a few such operations, the end of the queue will reach the end of the array, and there will be space left at the beginning of the array. At this point, we don't want to stop receiving new values as there is space left, so we roll over to the beginning of the array. That is to say, we continue adding the new values at the beginning of the array. To do all these manipulations, we must have separate variables storing the indexes of the beginning and the end of the queue. Also, since due to roll over, sometimes the end is smaller than the beginning, we store the length separately to avoid confusion. We start with the bare bone implementation of the class just as before. The start represents the index of the element that would be dequeued next and the end represents the position of the next value that would be enqueued. This is illustrated in the following code: public class QueueImplArray implements Queue { protected E[] array; protected int start=0; protected int end=0; protected int length=0; public QueueImplArray(int size){ array = (E[]) new Object[size]; } } The enqueue operation does not change the start position. The new value is put at the end position of the array and the end is incremented by one. The end, of course, needs to be rolled over in case it goes beyond the maximum index of the array, as shown in the following code: @Override public void enqueue(E value) { if(length>=array.length){ throw new NoSpaceException("No more space to add an element"); } array[end] = value; The modulo operator will make sure that the index goes to the beginning of the array when it hits the end of the array, as follows: end = (end+1) % array.length; length++; } [ 49 ] Protocols – Abstract Data Types The dequeue operation does not change the end position. We read from the start index and then increment the start index with rollover, as follows: @Override public E dequeue() { if(length<=0){ return null; } E value = array[start]; start = (start+1) % array.length; length--; return value; } The peek operation lets us see the element that would be dequeued next, without removing it. It is, of course, simpler. We just return the next element to be dequeued. This is shown in the following code: @Override public E peek() { if(length<=0){ return null; } return array[start]; } A queue backed up by an array can be resized in a similar manner as described for the case of a stack, and this too will be O(n), since we must copy all the old elements to the newly allocated array one by one. Variable-sized queue using a linked list Just like a stack, we want to implement a queue using a linked list. We need to remember that all operations must be O(1) in running time. If we enqueue by appending new elements at the beginning of the linked list, we will need to remove elements from the end of the list during dequeuing. This will not work as removal of an element from the end of a linked list is O(n). But appending at the end of a linked list is O(1) and so is removing from the beginning of the list. Hence, the end of the queue, where new elements are enqueued, would be the end of the list. And the start of the queue, where the elements are dequeued from, would be the beginning of the linked list. [ 50 ] Chapter 3 Given this, the implementation of a queue using a linked list is straightforward. Again, we create an instance of the list only using a getNewLinkedList() method, which can be overridden by a subclass to use a different linked list, as follows: public class QueueImplLinkedList implements Queue { protected LinkedList list = getNewLinkedList(); protected LinkedList getNewLinkedList(){ return new LinkedList<>(); } The enqueue operation simply appends at the end of the list as follows: @Override public void enqueue(E value) { list.appendLast(value); } The dequeue operation first checks if the list is empty so it can return null, and then it simply removes the first element from the list. It must also return the element that it just removed: @Override public E dequeue() { if(list.getLength()==0){ return null; } E value = list.getFirst(); list.removeFirst(); return value; } Just like the dequeue operation, the peek operation first needs to check whether the list is empty, in which case it has to return a null value, otherwise it simply returns the element at the beginning of the list that would be dequeued on the next dequeue operation, as shown in the following code: @Override public E peek() { if(list.getLength()==0){ return null; } return list.getFirst(); } } [ 51 ] Protocols – Abstract Data Types Double ended queue A double ended queue is a combination of a stack and a queue. The idea is that you are allowed to insert and remove elements at both ends of the queue. If you remove elements from the side you have inserted, it will behave like a stack. On the other hand, if you insert and remove on opposite ends, it will behave like a queue. You can mix these operations and use them in any order you like. The following figure shows a few operations to clarify this idea: A double ended queue has the following operations all with a complexity of O(n): • Push: This inserts an element at the beginning • Pop: This removes an element from the beginning • Inject: This inserts an element at the end • Eject: This removes an element from the end • Peek: This checks the first element • PeekLast: This checks the last element [ 52 ] Chapter 3 A double ended queue will be represented by the following interface: public interface DoubleEndedQueue extends Stack { void inject(E value); E eject(); E peekLast(); } Note that since a double ended queue has push and pop operations just like a stack and it preserves the same meaning, we create this interface extending the Stack interface. Fixed-length double ended queue using an array Since we have created the double ended queue as an extension of a stack, one would expect its implementation to extend a stack implementation as well. However, remember that a double ended queue is both a stack and a queue. The array-based implementation for a queue was more complex than that for a stack due to rollover of the indexes. We don't want to reprogram those, so we choose to extend a queue implementation instead of a stack implementation, as shown in the following code: public class DoubleEndedQueueImplArray extends QueueImplArray implements DoubleEndedQueue { We initialize the queue to the fixed length, as follows: public DoubleEndedQueueImplArray(int size) { super(size); } This is appended at the end of the double ended queue, which is the same as the enqueue operation of a queue: @Override public void inject(E value) { enqueue(value); } The eject operation is the removal of an element from the end of the double ended queue. We don't have an equivalent operation in a simple queue. So, we must code for it as follows: @Override public E eject() { if (length <= 0) { return null; } [ 53 ] Protocols – Abstract Data Types The end has to decrement by one with a provision for rollover. But if the end is already at zero, it will become negative, which will not work well with the modulo operator, because it will return a negative value. To always keep it positive, we add the length of the array to it. Note that it does not change the remainder when divided by the length of the array. This is shown in the following code: end = (end + array.length - 1) % array.length; E value = array[end]; length--; return value; } The peekLast operation simply needs to return the element that would have been returned by the eject operation without modifying anything, as shown in the following code: @Override public E peekLast() { if (length <= 0) { return null; } return array[(end + array.length - 1) % array.length]; } The push operation is the insertion of an element at the beginning of the double ended queue. There is no equivalent operation in a simple queue. Hence, we need to code for it as follows: @Override public void push(E value) { if (length >= array.length) { throw new NoSpaceException("No more space to add an element"); } This operation is very similar to updating the end index eject operation, as shown in the following code: start = (start + array.length - 1) % array.length; array[start] = value; length++; } [ 54 ] Chapter 3 The pop operation is the removal of the element at the beginning of the queue, which is the same as the dequeue operation of an ordinary queue. This is illustrated in the following code: @Override public E pop() { return dequeue(); } } Note that we don't write any code for the peek operation, which should return the element at the beginning of the double ended queue, as it is the same as the peek operation for a simple queue. The array-based implementation is, of course, fixed in size and cannot hold more elements than it's fixed size. Next, we develop a linked list-based implementation. Variable-sized double ended queue using a linked list We had earlier used a simple linked list to implement both a queue and a stack. However, remember again that all operations must be O(1). Now, we must both add and remove elements at both ends of the underlying linked list. We know that removal from the end of a singly linked list is O(n) and we cannot use it. So, we must use a doubly linked list instead. This time we do not have to worry about rollovers and so we will extend the linked list implementation of a stack, which is the natural choice. We will replace its singly linked list with a doubly linked list by overriding the getLinkedList() method, as follows: public class DoubleEndedQueueImplLinkedList extends StackImplLinkedList implements DoubleEndedQueue { @Override protected LinkedList getNewLinkedList() { return new DoublyLinkedList (); } The inject operation inserts a new element at the end of the list as shown in the following code: @Override public void inject(E value) { list.appendLast(value); } [ 55 ] Protocols – Abstract Data Types The eject operation must remove and return the last element of the list. This is illustrated in the following code: @Override public E eject() { if(list.getLength()==0){ return null; } E value = list.getLast(); list.removeLast(); return value; } Finally, the peekLast() method will just return the last element of the doubly linked list as follows: @Override public E peekLast() { if(list.getLength()==0){ return null; } return list.getLast(); } } We only had to implement the inject(), eject(), and peekLast() methods as the other methods are already implemented by the stack we extend. Summary In this chapter, we saw that an abstract data type or ADT is an abstraction of a data structure. It is a contract that an underlying data structure is supposed to adhere to. The contract involves different operations on the data structure and their specific behavior. We then saw a few simple ADTs as examples. These ADTs are, however, extremely useful as we will see in the course of this book when we encounter other algorithms. Abstraction allows different implementations of the structures. We will also see more ADTs in the course of this book and their implementations. In the next chapter, we will take a detour into a new area of algorithms called functional programming. Remember that an algorithm is a sequence of steps that may be followed to achieve a desired processing; it turns out that there is another way of looking at it, which we will explore in the next chapter. [ 56 ] Detour – Functional Programming In the beginning of this book, we saw that an algorithm is a sequence of steps to achieve a result. This way of solving a problem by following a sequence of instructions is called imperative programming. Each statement in the program can be thought of as an imperative sentence asking the computer to do something. However, this is not the only way of looking at it. Functional programming sees an algorithm as a composition of components rather than as a sequence of steps. A problem to solve is seen as a composition of smaller-sized problems. Instead of using a loop, we combine smaller versions of the same problem. Functional programming uses recursion as a basic component. A recursion is nothing but solving the same problem for a smaller size and then composing the result with something else to get the solution for the given size of the problem. This has a far-reaching implication in how easy it is to read and understand a program. This makes it very important to study. There are really two worlds in the programming paradigm. The imperative style of programming is favored by C-like languages, such as C, C++, and Java. On the purely functional side, there are languages such as Lisp, Haskell, and Scheme. Apart from these, some languages try to have the best of both worlds, such as Python or Scala. This is easier said than done; trying to mix both ideas in a language means you have features to support both, but to use them effectively is truly an art. So, if Java is imperative in nature, why are we talking about functional programming in this book? Well, as I pointed out, sometimes, it is better to mix both concepts and get the best of both worlds. Recently, the Java community has taken note of this fact and has introduced a feature lambda from Java version 8 to provide some level of functional programming support. So, our intention is not to program completely in functional style, as that is not the preferred programming style of Java, but we will do it just enough to make our programs more beautiful and to aid our understanding of algorithms. [ 57 ] Detour – Functional Programming This chapter will introduce this rather foreign concept to you and provide some basic tools that are commonly used in functional programming. You will learn the following concepts: • Recursive algorithms and the immutability of variables • Monads • Aggregations on monads • Java's support for functional programming, that is, lambda. Recursive algorithms As I have already pointed out, recursive algorithms are a different way of thinking about solving a problem. For example, say our problem is to write a program that, given a positive integer n, returns the sum of numbers from zero to n. The known imperative way of writing it is simple: public int sum_upto(int n){ int sum=0; for(int i=0;i<=n;i++){ sum+=i; } return sum; } The following would be the functional version of the problem: public int sum_upto_functional(int n){ return n==0?0:n+sum_upto_functional(n-1); } That's it–just a one-liner! This is probably nothing new to Java programmers, as they do understand recursive functions. However, an imperative programmer would use recursion only when nothing else worked. But this is a different way of thinking. How do we justify that it is equivalent to solving the problem for a smaller input and then composing it with something else? Well, we are certainly first computing the same function for an input that is smaller by one and then just adding n to it. There is one more thing to note here: in the imperative version, we are updating the variable called sum for each value of the loop variable i. However, in the functional version, we are not updating any variable. What do we achieve by that? When a variable is updated multiple times in a program, it is hard to understand or debug it because you need to keep track of the latest value. When this does not happen, it is far easier to understand what is happening. In fact, it makes it simpler in such a way that we can even prove the correctness of the program completely formally, which is rather hard to do for imperative programs where variables change values. [ 58 ] Chapter 4 Let's check out another example. This one is about choosing r objects from n objects, where the order does not matter. Let's have a set A of a finite amount of objects. Let the number of objects in this set be n. How many subsets B of this set A have exactly r elements? Of course, the maximum number of elements any subset of A can have is n; hence r ≤ n. We will call this function choose. So, we write this as choose(n,r). Now, the only subset that has an equal number of elements as A is A itself. So, choose(n,n) equals 1. How do we break this problem into subproblems of a similar nature but with smaller input? To find out how many subsets B have r elements, we first think of the set A as a combination of a subset C with n-1 elements and one particular element a. So, we can say A = C ⋃ {a}. Now, we consider two disjoint cases: the element a is a member of the subset B and when it is not a member of the subset B. When a is not a member of the subset B, B is also a subset of C. The number of such subsets B with exactly r elements is choose(n-1,r), since C has n-1 elements. On the other hand, when a is a member of the set B, then B can be thought of as a union of two sets – a set D that has all the elements of B except a, and the other is just {a}. So, B = D ⋃ {a}. Now, you can see that D is a subset of C that we defined. How many such subsets D are there of C? There are choose(n-1,r-1) subsets, since C has n-1 elements and D has r-1 elements. So, the number of such subsets B is choose(n-1,r-1). This means that the total number of sets B with or without a as an element is choose(n-1,r) + choose(n-1,r-1). If we put this in recursive calls, the r and n will get reduced until r equals zero or n equals r. We have already considered the case when n equals r, that is, choose(n,n). When r equals zero, it means B is the null set. Since there is only one null set, choose(n,0) equals 1. Now, we put this in code: public long choose(long n, long r){ if(n x+1; int y = sfi.modify(1); Take note of the parentheses and the arrow sign. The parentheses contain all the parameters. The types of parameters are not specified because they are already specified in the interface method. There can be zero or more parameters. There are two kinds of lambda syntax–one that has an expression as the body and one that has one or more steps as the body. These lambdas look a bit different from each other. A lambda that is implemented as a one liner looks like the one we just saw. This is called an expression syntax. The expression syntax can be used if the lambda expression is a one liner. For multi-line code, we use the block syntax as shown below: Thread t = new Thread(()->{for(int i=0;i<500;i++) System.out.println(i);}); One can use block syntax for functions that return a value as well, especially when using multiple lines of code. In that case, one just needs to use a return statement to return a value. [ 61 ] Detour – Functional Programming Since in a functional program all variables must not ever be reassigned, we should declare them final to avoid accidentally modifying them. However, since typing final for every variable clutters the code a bit, we avoid doing so. In a purely functional language, the variables are immutable by default. Even in a semifunctional language, such as Scala, it is so if it generally encourages the functional style. However, since Java mostly prefers an imperative style, the final keyword is necessary, causing a little clutter. Now that we know about lambda, we can start learning about functional data structures. Functional data structures and monads Functional data structures are data structures that follow the principle of immutability and inductive (or recursive) definition. Immutability means that any modification of the data structure would result in a new data structure, and any old reference to the original version would still have access to the original version. Inductive definition means that the definition of the structure is defined as a composition of smaller versions of the same data structure. Take, for example, our linked list. When we add an element to or remove an element from the beginning of the list, it will modify the linked list. That means any reference to the linked list will now hold a reference to the modified linked list. This doesn't conform to the principle of immutability. A functional linked list would make sure that the older references still held reference to an unmodified version. We will discuss how to do it in the next section. Functional linked lists To make a linked list that is immutable, we consider a linked list to be made of two parts: • A head containing the first element of the list • A tail containing another linked list containing the rest of the elements Note that we have now defined the linked list recursively, being true to our functional design. This recursion says that a linked list is: • Either an empty list • Or a set of two objects, as follows: °° A head containing one element of its element type °° A tail containing another linked list of the same type [ 62 ] Chapter 4 This version of the definition is the same as the previous simplified one, except that we have now specified how we represent where the list terminates. The list terminates where there are no more elements, that is, when the tail is an empty list. Let's put all these in code. First, we define a version according to the simplified definition: public class LinkedList { private E head; private LinkedList tail; private LinkedList(){ } private LinkedList(E head, LinkedList tail){ this.head = head; this.tail = tail; } public E head(){ return head; } public LinkedList tail(){ return tail; } This is the core of immutability for our linked list. Note that every time we add a new value to our linked list, we create a new linked list so that the old references still hold references to the unmodified list: public LinkedList add(E value){ return new LinkedList (value,this); } } The code is self-explanatory, now that we already know how we think about our linked list. But note that we have made the constructors private. We don't want people to create inconsistent versions of our linked lists, such as a null tail or something. We insist that everyone creates our linked list by first creating an empty linked list and then adding elements to it. So, we add the following EmptyList class and add() method: public static final class EmptyList extends LinkedList { @Override public E head() { [ 63 ] Detour – Functional Programming throw new NoValueException("head() invoked on empty list"); } @Override public LinkedList tail() { throw new NoValueException("tail() invoked on empty list"); } } public static LinkedList emptyList(){ return new EmptyList<>(); } Now we can use the linked list as follows: LinkedList linkedList = LinkedList. emptyList() .add(5).add(3).add(0); while(!(linkedList instanceof LinkedList.EmptyList)){ System.out.println(linkedList.head()); linkedList = linkedList.tail(); } But wait, did we just modify the linkedList variable in the while loop? Yes, but that does not comply with the principle of immutability. To solve this, let's see what we would mostly want to do with a list. In general, we would want to perform the following operations: • Do something for each element of the list. For example, print all the elements to the console. • Get a new list where each element is transformed using a function that is provided. • Compute a function of all the elements in the list. This is an aggregation of the elements. For example, find the sum of all the elements. • Create a new list containing only selected elements of the list. This is called filtering. We will deal with them one by one. At the end of the next section, you will be prepared to learn about monads as well. [ 64 ] Chapter 4 The forEach method for a linked list The forEach() method on a linked list would do something for each element of the list. This something would be passed as a lambda. For this purpose, we will first create a functional interface that consumes one parameter but does not return anything: @FunctionalInterface public interface OneArgumentStatement { void doSomething(E argument); } With this interface available, we will define the forEach() method for a list, as follows: public class LinkedList { … public static class EmptyList extends LinkedList { … @Override public void forEach(OneArgumentStatement processor) {} } … public void forEach(OneArgumentStatement processor){ processor.doSomething(head()); tail().forEach(processor); } } The ellipsis represent more code that we have already discussed and need not be repeated. The forEach() method simply processes the head and then recursively calls itself on the tail. Note again that, true to our philosophy of recursion, we have implemented the forEach() method using recursion. Of course, this will not work on an empty list because the head and tail are null. The empty list represents when the method needs to stop calling itself. We achieve this by overriding the forEach() method in the EmptyList class to not do anything. Now we can print all the elements using the following code: linkedList.forEach((x) -> {System.out.println(x);}); [ 65 ] Detour – Functional Programming We pass a lambda that, given any element x, calls System.out.println on x. But, if you see, this lambda just works as a delegation to the System.out.println method that already has the required form of the lambda. Java allows you to use a method as a lambda with the following syntax. The :: operator is used to tell the compiler that you are not looking for a field with that name; instead you are looking for a method with that name: linkedList.forEach(System.out::println); Note that this time we did not even modify the list while printing the elements, unlike last time, when we did it using a loop. Map for a linked list Now we move on to the next thing we want to do with a list, which is to create a new list where all the elements are transformed according to a lambda that is provided. What I mean is that we want to do the following: LinkedList tranformedList = linkedList.map((x)->x*2); We need to implement the map() method in a way that transformedList holds all the elements of linkedList multiplied by 2, in the same order. The following is the implementation of the map() method: public class LinkedList { … public static class EmptyList extends LinkedList { … @Override public LinkedList map(OneArgumentExpression transformer) { return LinkedList.emptyList(); } } … public LinkedList map(OneArgumentExpression transformer){ return new LinkedList<>(transformer.compute(head()), tail.map(transformer)); } } [ 66 ] Chapter 4 As usual, the method is defined recursively. The transformed list is just the head transformed followed by the tail transformed. We have also overridden the method in the EmptyList class to return an empty list because an empty list transformed is just another empty list of a possibly different type. With this implementation in place, we can do the following: LinkedList tranformedList = linkedList.map((x)->x*2); tranformedList.forEach(System.out::println); This should print a list with all the values multiplied by 2. You can even change the type of the elements by transformation, such as the following: LinkedList tranformedListString = linkedList.map((x)->"x*2 = "+(x*2)); tranformedListString.forEach(System.out::println); The tranformedListString list is a list of strings, and printing each element on the next line shows the strings obtained. Now we move on to the next thing we want to do with a list, which is to compute some function that uses all the values in the list. This is called an aggregation operation. But before looking at a general case, we will concentrate on a specific one, called a fold operation. Fold operation on a list A fold operation on a list is an aggregation operation that can be done element by element. For example, if we want to compute the sum of all the elements of a list, we can do it by taking each element of the list and adding it to a moving sum, so when we are done with processing all the elements, we will have the sum of all elements. There are two operations that suit this purpose: foldLeft and foldRight. The foldLeft operation aggregates the head first and moves on to the tail. The foldRight method aggregates the tail first and then moves on to the head. Let's start with foldLeft. But before doing anything, we need a functional interface that represents an expression of two parameters: @FunctionalInterface public interface TwoArgumentExpression { R compute(A lhs, B rhs); } [ 67 ] Detour – Functional Programming With this interface available, we define the foldLeft method in the following way: public class LinkedList { … … public static class EmptyList extends LinkedList { … @Override public R foldLeft(R initialValue, TwoArgumentExpression computer) { return initialValue; } } … public R foldLeft(R initialValue, TwoArgumentExpression computer){ R newInitialValue = computer.compute(initialValue, head()); return tail().foldLeft(newInitialValue, computer); } } We compute a new value from initialValue and the head using the lambda passed, and then we use this updated value to compute foldLeft on the tail. The empty list overrides this method to just return the initialValue itself because it just marks the end of the list. Now we can compute the sum of all elements as follows: int sum = linkedList.foldLeft(0,(a,b)->a+b); System.out.println(sum); We have passed 0 as the initial value and the lambda that sums up the values passed. This looks complicated until you get used to this idea, but once you get used to it, it is very simple. Let's see what is happening step by step; the list from head to tail is {0,3,5}: 1. In the first invocation, we pass the initial value 0. The computed newInitialValue is 0+0 = 0. Now, we pass this newInitialValue to the tail to foldLeft, which is {3,5}. 2. The {3,5} has a head 3 and tail {5}. 3 is added to the initialValue 0 to give a newInitialValue 0+3=3. Now, this new value 3 is passed to the tail {5} to foldLeft. [ 68 ] Chapter 4 3. The {5} has a head 5 and tail and empty list. 5 is added to the initialValue 3 to get 8. Now this 8 is passed as initialValue to the tail, which is an empty list. 4. The empty list, of course, just returns the initial value for a foldLeft operation. So it returns 8, and we get the sum. Instead of computing one value, we can even compute a list as a result. The following code reverses a list: LinkedList reversedList = linkedList.foldLeft(LinkedList.emptyList(),(l,b)->l.add(b) ); reversedList.forEach(System.out::println); We have simply passed an empty list as an initial operation, and then our operation simply adds a new element to the list. In the case of foldLeft, the head will be added before the tail, causing it to be placed more in the tail side in the newly constructed list. What if we want to process the right-most end (or away from the head) first and move to the left? This operation is called foldRight. This can be implemented in a very similar manner, as follows: public class LinkedList { … public static class EmptyList extends LinkedList { … @Override public R foldRight(TwoArgumentExpression computer, R initialValue) { return initialValue; } } … public R foldRight(TwoArgumentExpression computer, R initialValue){ R computedValue = tail().foldRight(computer, initialValue); return computer.compute(head(), computedValue); } } [ 69 ] Detour – Functional Programming We have switched the order of the arguments to make it intuitive that the initialValue is being combined from the right end of the list. The difference from foldLeft is that we compute the value on the tail first, calling a foldRight on it. Then we return the result of the computed value from the tail being combined with the head to get the result. In the case of computing a sum, it does not make any difference which fold you invoke because sum is commutative, that is, a+b always equals b+a. We can call the foldRight operation for the computation of sum in the following way, which will give the same sum: int sum2 = linkedList.foldRight((a,b)->a+b, 0); System.out.println(sum2); However, if we use an operator that is not commutative, we will get a different result. For example, if we try reversing the list with the foldRight method, it will give the same list instead of being reversed: LinkedList sameList = linkedList.foldRight((b,l)>l.add(b), LinkedList.emptyList()); sameList.forEach(System.out::println); The final thing we wanted to do with a list was filtering. You will learn it in the next subsection. Filter operation for a linked list Filter is an operation that takes a lambda as a condition and creates a new list that has only those elements that satisfy the condition. To demonstrate this, we will create a utility method that creates a list of a range of elements. First, we create a helper method that appends a range of numbers to the head of an existing list. This method can call itself recursively: private static LinkedList ofRange(int start, int end, LinkedList tailList){ if(start>=end){ return tailList; }else{ return ofRange(start+1, end, tailList).add(start); } } [ 70 ] Chapter 4 Then we use the helper method to generate a list of a range of numbers: public static LinkedList ofRange(int start, int end){ return ofRange(start,end, LinkedList.emptyList()); } This will let us create a list of a range of integers. The range includes the start and excludes the end. For example, the following code will create a list of numbers from 1 to 99 and then print the list: LinkedList rangeList = LinkedList.ofRange(1,100); rangeList.forEach(System.out::println); We now want to create a list of all even numbers, say. For that, we create a filter method in the LinkedList class: public class LinkedList { … public static class EmptyList extends LinkedList { … @Override public LinkedList filter(OneArgumentExpression selector) { return this; } } … public LinkedList filter(OneArgumentExpression selector){ if(selector.compute(head())){ return new LinkedList (head(), tail().filter(selector)); }else{ return tail().filter(selector); } } } [ 71 ] Detour – Functional Programming The filter() method checks whether the the condition is met. If yes, then it includes the head and calls the filter() method on the tail. If not, then it just calls the filter() method on the tail. The EmptyList of course needs to override this method to just return itself because all we need is an empty list. Now, we can do the following: LinkedList evenList = LinkedList.ofRange(1,100).filter((a)->a%2==0); evenList.forEach(System.out::println); This will print all the even numbers between 1 and 99. Let's go through some more examples in order to get used to all this stuff. How do we add all numbers from 1 to 100? The following code will do that: int sumOfRange = LinkedList.ofRange(1,101).foldLeft(0, (a,b)>a+b); System.out.println(sumOfRange); Note that we have used the range of (1,101) because the end number is not included in the generated linked list. How do we compute the factorial of a number using this? We define a factorial method as follows: public static BigInteger factorial(int x){ return LinkedList.ofRange(1,x+1) .map((a)->BigInteger.valueOf(a)) .foldLeft(BigInteger.valueOf(1),(a,b)->a.multiply(b)); } We have used Java's BigInteger class because factorials grow too fast and an int or a long cannot hold much. This code demonstrates how we converted the list of integers to a list of BigIntegers using the map method before multiplying them with the foldLeft method. We can now compute the factorial of 100 with the following code: System.out.println(factorial(100)); This example also demonstrates the idea that we can combine the methods we developed to solve more complicated problems. Once you get used to this, reading a functional program and understanding what it does is a lot simpler than doing the same for their imperative versions. We have even used one-character variable names. Actually, we could use meaningful names, and in some cases, we should. But here the program is so simple and the variables used are so close to where they are defined that it's not even necessary to name them descriptively. [ 72 ] Chapter 4 Let's say we want to repeat a string. Given an integer, n, and a string, we want the resultant string to be a repetition of the original string n number of times. For example, given an integer 5 and a string Hello, we want the output to be HelloHello HelloHello Hello. We can do this with the following function: public static String repeatString(final String seed, int count){ return LinkedList.ofRange(1,count+1) .map((a)->seed) .foldLeft("",(a,b)->a+b); } What we are doing here is first creating a list of length count and then replacing all its elements with the seed. This gives us a new list with all the elements equal to the seed. This can be folded to get the desired repeated string. This is easy to understand because it is very much like the sum method, except we are adding strings instead of integers, which causes repetition of the string. But we don't even need to do this. We can do this even without creating a new list with all the elements replaced. The following will do it: public static String repeatString2(final String seed, int count){ return LinkedList.ofRange(1,count+1) .foldLeft("",(a,b)->a+seed); } Here, we just ignore the integer in the list and add the seed instead. In the first iteration, a would be set to the initial value, which is an empty string. Every time, we just ignore the content and instead add the seed to this string. Note that in this case, variable a is of the String type and variable b is of the Integer type. So, we can do a lot of things using a linked list, using its special methods with lambda parameters. This is the power of functional programming. What we are doing with lambda, though, is that we are passing the implementation of interfaces as pluggable code. This is not a new concept in an object-oriented language. However, without the lambda syntax, it would take a lot of code to define an anonymous class to do the equivalent, which would clutter the code a lot, thus undermining the simplicity. What has changed though is the immutability, leading to chaining of methods and other concepts. We are not thinking about state while analyzing the programs; we are simply thinking of it as a chain of transformations. The variables are more like variables in algebra, where the value of x stays the same throughout a formula. [ 73 ] Detour – Functional Programming Append on a linked list We have completed all the things that were in the list of the things we wanted to do. There may be a few more. One important thing, for example, is append. This operation sticks one list to another. This can be done using the foldRight method that we have already defined: public LinkedList append(LinkedList rhs){ return this.foldRight((x,l)->l.add(x),rhs); } Now, we perform the following: LinkedList linkedList = LinkedList. emptyList().add(5).add(3).add(0); LinkedList linkedList2 = LinkedList. emptyList().add(6).add(8).add(9); linkedList.append(linkedList2).forEach(System.out::print); This will output 035986, which is the first list stuck in front of the second list. To understand how it works, first remember what a foldRight operation does. It starts with an initial value–in this case, the right hand side (RHS). Then it takes one element at a time from the tail end of the list and operates on that with the initial list using the provided operation. In our case, the operation simply adds an element to the head of the initial list. So, in the end, we get the entire list appended to the beginning of the RHS. There is one more thing that we want to do with a list, but we have not talked about it until now. This concept requires an understanding of the earlier concepts. This is called a flatMap operation, and we will explore it in the next subsection. The flatMap method on a linked list The flatMap operation is just like the map operation, except we expect the operation passed to return a list itself instead of a value. The job of the flatMap operation is to flatten the lists thus obtained and append them one after another. Take for example the following code: LinkedList funnyList =LinkedList.ofRange(1,10) .flatMap((x)->LinkedList.ofRange(0,x)); [ 74 ] Chapter 4 The operation passed returns a range of numbers starting from 0 to x-1. Since we started the flatMap on a list of numbers from 1 to 9, x will get values from 1 to 9. Our operation will then return a list containing 0,x-1 for each value of x. The job of the flatMap operation is to then flatten all these lists and stick them one after another. Take a look at the following line of code, where we print funnyList: funnyList.forEach(System.out::print); It will print 001012012301234012345012345601234567012345678 on the output. So, how do we implement the flatMap operation? Let's have a look: public class LinkedList { public static class EmptyList extends LinkedList { … @Override public LinkedList flatMap(OneArgumentExpression > transformer) { return LinkedList.emptyList(); } } … public LinkedList flatMap(OneArgumentExpression > transformer){ return transformer.compute(head()) append(tail().flatMap(transformer)); } } So what is happening here? First, we compute the list obtained by the head and the result of the flatMap operation on the tail. Then we append the result of the operation on the head of the list in front of the list obtained by flatMap on the tail. In case of an empty list, the flatMap operation just returns an empty list because there is nothing for the transformation to be called on. [ 75 ] Detour – Functional Programming The concept of a monad In the previous section, we saw quite a few operations for a linked list. A few of them, namely map and flatMap, are a common theme in many objects in functional programming. They have a meaning outside of the list. The map and flatMap methods, and a method to construct a monad from a value are what make such a wrapper object a monad. A monad is a common design pattern that is followed in functional programming. It is a sort of container, something that stores objects of some other class. It can contain one object directly as we will see; it can contain multiple objects as we have seen in the case of a linked list, it can contain objects that are only going to be available in the future after calling some function, and so on. There is a formal definition of monad, and different languages name its methods differently. We will only consider the way Java defines the methods. A monad must have two methods, called map() and flatMap(). The map() method accepts a lambda that works as a transformation for all the contents of the monad. The flatMap method also takes a method, but instead of returning the transformed value, it returns another monad. The flatMap() method then extracts the output from the monad and creates a transformed monad. We have already seen an example of a monad in the form of a linked list. But the general theme does not become clear until you have seen a few examples instead of just one. In the next section, we will see another kind of monad: an option monad. Option monad An option monad is a monad containing a single value. The whole point of this is to avoid handling null pointers in our code, which sort of masks the actual logic. The point of an option monad is to be able to hold a null value in a way that null checks are not required in every step. In some way, an option monad can be thought of as a list of zero or one objects. If it contains just zero objects, then it represents a null value. If it contains one object, then it works as the wrapper of that object. The map and flatMap methods then behave exactly like they would behave in the case of a one-argument list. The class that represents an empty option is called None. First, we create an abstract class for an option monad. Then, we create two inner classes called Some and None to represent an Option containing a value and one without a value, respectively. This is a more general pattern for developing a monad and can cater to the fact that the non-empty Option has to store a value. We could do this with a list as well. Let's first see our abstract class: public abstract class Option { public abstract E get(); public abstract Option map(OneArgumentExpression transformer); public abstract Option flatMap(OneArgumentExpression > transformer); [ 76 ] Chapter 4 public abstract void forEach(OneArgumentStatement statement); … } A static method optionOf returns the appropriate instance of the Option class: public static Option optionOf(X value){ if(value == null){ return new None<>(); }else{ return new Some<>(value); } } We now define the inner class, called None: public static class None extends Option { @Override public Option flatMap(OneArgumentExpression > transformer) { return new None<>(); } @Override public E get() { throw new NoValueException("get() invoked on None"); } @Override public Option map(OneArgumentExpression transformer) { return new None<>(); } @Override public void forEach(OneArgumentStatement statement) { } } We create another class, Some, to represent a non-empty list. We store the value as a single object in the class Some, and there is no recursive tail: public static class Some extends Option { E value; public Some(E value){ [ 77 ] Detour – Functional Programming this.value = value; } public E get(){ return value; } … } The map and flatMap methods are pretty intuitive. The map method accepts a transformer and returns a new Option where the value is transformed. The flatMap method does the same, except it expects the transformer to wrap the returned value inside another Option. This is useful when the transformer can sometimes return a null value, in which case the map method will return an inconsistent Option. Instead, the transformer should wrap it in an Option, for which we need to use a flatMap operation. Have a look at the following code: public static class Some extends Option { … public Option map(OneArgumentExpression transformer){ return Option.optionOf(transformer.compute(value)); } public Option flatMap(OneArgumentExpression > transformer){ return transformer.compute(value); } public void forEach(OneArgumentStatement statement){ statement.doSomething(value); } } To understand the usage of an Option monad, we will first create a JavaBean. A JavaBean is an object exclusively intended to store data. It is the equivalent of a structure in C. However, since encapsulation is a defining principle of Java, the members of the JavaBean are not accessed directly. They are instead accessed through special methods called getters and setters. However, our functional style dictates that the beans be immutable, so there won't be any setter methods. The following set of classes gives a few examples of JavaBeans: public class Country { private String name; private String countryCode; public Country(String countryCode, String name) { this.countryCode = countryCode; [ 78 ] Chapter 4 this.name = name; } public String getCountryCode() { return countryCode; } public String getName() { return name; } } public class City { private String name; private Country country; public City(Country country, String name) { this.country = country; this.name = name; } public Country getCountry() { return country; } public String getName() { return name; } } public class Address { private String street; private City city; public Address(City city, String street) { this.city = city; this.street = street; } public City getCity() { return city; } public String getStreet() { return street; [ 79 ] Detour – Functional Programming } } public class Person { private String name; private Address address; public Person(Address address, String name) { this.address = address; this.name = name; } public Address getAddress() { return address; } public String getName() { return name; } } There is not much to understand in these four classes. They are there to store a person's data. In Java, it is not very uncommon to hit a case where you will hit a very similar kind of object. Now, let's say, given a variable person of type Person, we want to print the name of the country he/she lives in. If the case is that any of the state variables can be null, the correct way to do it with all null checks would look like the following: if(person!=null && person.getAddress()!=null && person.getAddress().getCity()!=null && person.getAddress().getCity().getCountry()!=null){ System.out.println(person.getAddress().getCity().getCountry()); } This code would work, but let's face it–it's a whole bunch of null checks. We can get a hold of the address simply by using our Options class, as follows: String countryName = Option.optionOf(person) .map(Person::getAddress) .map(Address::getCity) .map(City::getCountry) .map(Country::getName).get(); [ 80 ] Chapter 4 Note that if we just print this address, there is a chance that we will print null. But it would not result in a null-pointer exception. If we don't want to print null, we need a forEach method just like the one in our linked list: public class Option { public static class None extends Option { … @Override public void forEach(OneArgumentStatement statement) { } } … public void forEach(OneArgumentStatement statement){ statement.doSomething(value); } } The forEach method just calls the lambda passed on the value it contains, and the None class overrides it to do nothing. Now, we can do the following: Option.optionOf(person) .map(Person::getAddress) .map(Address::getCity) .map(City::getCountry) .map(Country::getName) .forEach(System.out::println); This code will now not print anything in case of a null name in country. Now, what happens if the Person class itself is functionally aware and returns Options to avoid returning null values? This is where we need a flatMap. Let's make a new version of all the classes that were a part of the Person class. For brevity, I will only show the modifications in the Person class and show how it works. You can then check the modifications on the other classes. Here's the code: public class Person { private String name; private Address address; public Person(Address address, String name) { this.address = address; this.name = name; [ 81 ] Detour – Functional Programming } public Option getAddress() { return Option.optionOf(address); } public Option getName() { return Option.optionOf(name); } } Now, the code will be modified to use flatMap instead of map: Option.optionOf(person) .flatMap(Person::getAddress) .flatMap(Address::getCity) .flatMap(City::getCountry) .flatMap(Country::getName) .forEach(System.out::println); The code now fully uses the Option monad. Try monad Another monad we can discuss is the Try monad. The point of this monad is to make exception handing a lot more compact and avoid hiding the details of the actual program logic. The semantics of the map and flatMap methods are self-evident. Again, we create two subclasses, one for success and one for failure. The Success class holds the value that was computed, and the Failure class holds the exception that was thrown. As usual, Try is an abstract class here, containing one static method to return the appropriate subclass: public abstract class Try { public abstract Try map( OneArgumentExpressionWithException expression); public abstract Try flatMap( OneArgumentExpression > expression); public abstract E get(); public abstract void forEach( OneArgumentStatement statement); public abstract Try processException( [ 82 ] Chapter 4 OneArgumentStatement statement); … public static Try of( NoArgumentExpressionWithException expression) { try { return new Success<>(expression.evaluate()); } catch (Exception ex) { return new Failure<>(ex); } } … } We need a new NoArgumentExpressionWithException class and a OneArgumentExpressionWithException class that allows exceptions in its body. They are as follows: @FunctionalInterface public interface NoArgumentExpressionWithException { R evaluate() throws Exception; } @FunctionalInterface public interface OneArgumentExpressionWithException { R compute(A a) throws Exception; } The Success class stores the value of the expression passed to the of() method. Note that the of() method already executes the expression to extract the value. protected static class Success extends Try { protected E value; public Success(E value) { this.value = value; } The fact is that this is a class that represents the success of the earlier expression; the flatMap has to only handle exceptions in the following expression, which the following Try passed to it handles itself, so we can just return that Try instance itself: @Override public Try flatMap( OneArgumentExpression > expression) { return expression.compute(value); } [ 83 ] Detour – Functional Programming The map() method, however, has to execute the expression passed. If there is an exception, it returns a Failure; otherwise it returns a Success: @Override public Try map( OneArgumentExpressionWithException expression) { try { return new Success<>( expression.compute(value)); } catch (Exception ex) { return new Failure<>(ex); } } The get() method returns the value as expected: @Override public E get() { return value; } The forEach() method lets you run another piece of code on the value without returning anything: @Override public void forEach( OneArgumentStatement statement) { statement.doSomething(value); } This method does not do anything. The same method on the Failure class runs some code on the exception: @Override public Try processException( OneArgumentStatement statement) { return this; } } Now, let's look at the Failure class: protected static class Failure extends Try { protected Exception exception; public Failure(Exception exception) { this.exception = exception; } [ 84 ] Chapter 4 Here, in both the flatMap() and map() methods, we just change the type of Failure, but return one with the same exception: @Override public Try flatMap( OneArgumentExpression > expression) { return new Failure<>(exception); } @Override public Try map( OneArgumentExpressionWithException expression) { return new Failure<>(exception); } There is no value to be returned in the case of a Failure: @Override public E get() { throw new NoValueException("get method invoked on Failure"); } We don't do anything in the forEach() method because there is no value to be worked on, as follows: @Override public void forEach( OneArgumentStatement statement) { … } The following method runs some code on the exception contained in the Failure instance: @Override public Try processException( OneArgumentStatement statement) { statement.doSomething(exception); return this; } } [ 85 ] Detour – Functional Programming With this implementation of the Try monad, we can now go ahead and write some code that involves handing exceptions. The following code will print the first line of the file demo if it exists. Otherwise, it will print the exception. It will print any other exception as well: Try.of(() -> new FileInputStream("demo")) .map((in)->new InputStreamReader(in)) .map((in)->new BufferedReader(in)) .map((in)->in.readLine()) .processException(System.err::println) .forEach(System.out::println); Note how it removes the clutter in handling exceptions. You should, at this stage, be able to see what is going on. Each map() method, as usual, transforms a value obtained earlier, only, in this case, the code in the map() method may throw an exception and that would be gracefully contained. The first two map() methods create a BufferedReader in from a FileInputStream, while the final map() method reads a line from the Reader. With this example, I am concluding the monad section. The monadic design pattern is ubiquitous in functional programming and it's important to understand this concept. We will see a few more monads and some related ideas in the next chapter. Analysis of the complexity of a recursive algorithm Throughout the chapter, I have conveniently skipped over the complexity analysis of the algorithms I have discussed. This was to ensure that you grasp the concepts of functional programming before being distracted by something else. Now is the time to get back to it. Analyzing the complexity of a recursive algorithm involves first creating an equation. This is naturally the case because the function is defined in terms of itself for a smaller input, and the complexity is also expressed as a function of itself being calculated for a smaller input. For example, let's say we are trying to find the complexity of the foldLeft operation. The foldLeft operation is actually two operations, the first one being a fixed operation on the current initial value and the head of the list, and then a foldLeft operation on the tail. Suppose T(n) represents the time taken to run a foldLeft operation on a list of length n. Now, let's assume that the fixed operation takes a time A. Then, the definition of the foldLeft operation suggests that T(n) = A + T(n-1). Now, we would try to find a function that solves this equation. In this case, it is very simple: [ 86 ] Chapter 4 T(n) = A + T(n-1) => T(n) – T(n-1) = A This means T(n) is an arithmetic progression and thus can be represented as T(n) = An + C, where C is the initial starting point, or T(0). This means T(n) = O(n). We have already seen how the foldLeft operation works in linear time. Of course, we have assumed that the the operation involved is constant with time. A more complex operation will result in a different complexity. You are advised to try to compute the complexity of the other algorithms, which are not very different from this one. However, I will provide a few more of these. Earlier in this chapter, we implemented the choose function as follows: choose(n,r) = choose(n-1,r) + choose(n-1, r-1) If we assume that the time taken is given by the function T(n,r), then T(n,r) = T(n-1,r) + T(n-1,r-1) + C, where C is a constant. Now we can do the following: T(n,r) = T(n-1,r) + T(n-1,r-1) + C =>T(n,r) 1) + C T(n-1,r) = T(n-1,r- Similarly, T(n-1,r) - T(n-2,r) = T(n-2,r-1) + C, by simply having n-1 in place of n. By stacking such values, we have the following: T(n,r) - T(n-1,r) = T(n-1,r) - T(n-2,r) T(n-2,r) - T(n-3,r) … T(r+1,r) - T(r,r) = T(n-1,r-1) + C = T(n-2,r-1) + C = T(n-3,r-1) + C T(r,r-1) + C The preceding equation considers n-r such steps in total. If we sum both sides of the stack, we have the following: Of course, T(r,r) is constant time. Let's call it B. Hence, we have the following: [ 87 ] Detour – Functional Programming Note that we can apply the same formula to T(i,r-1) too. This will give us the following: This gives the the following after simplification: We can continue this way and we will eventually get an expression with multiple nested summations, as follows: Here A's and D's are also constants. When we are talking about asymptotic complexity, we need to assume that a variable is sufficiently large. In this case, there are two variables, with the condition that r is always less than or equal to n. So, first we consider the case where r is fixed and n is being increased and being made sufficiently large. In this case, there would be a total of r summations nested in one another. T(t,0) is a constant time. The summation has r depth, each having a maximum of (n-r) elements, so it is O((n-r)r). The other terms are O((n-r)r). Hence we can say the following: T(n,r) = O((n-r)r) = O(nr) The size of the input is of course not n; it is log n = u (say). Then, we have the complexity of computation of T(n,r) = O(2sr). Another interesting case would be when we increase both r and n while also increasing the difference between them. To do that, we may want a particular ratio between the two, we assume r/n= k , k<1 always. Then we can see the asymptotic growth of the function T(n, kn). But computing this requires calculus and is outside the scope of this book. [ 88 ] Chapter 4 This shows that even though the analysis of algorithms in functional form can be easier, the analysis of the time complexity can be fairly difficult. It is easy to understand why it is more difficult to compute the complexity of a functional algorithm. At the end of the day, computing complexity involves counting the number of steps required to do the computation. In the imperative style, steps of computation are direct, so it is easy to count them. On the other hand, a recursive style is a higher level of abstraction, and hence, counting the number of steps is harder. In the succeeding chapters, we will see more of these analyses. Performance of functional programming If we think about it, the whole point of functional programming is to have immutability and recursive definitions (or inductive definitions) of programs so that they can be analyzed easily. In general, adding additional constraints on your program would make it simpler to analyze but would reduce what you can do with it. Functional programming, of course, adds additional constraints on imperative programming in the form of immutability, that is, you are no longer allowed to reassign a variable. This is done so that the analysis of the program, that is, understanding how the program works, is now simpler. It is also simpler to prove theorems about the programs. However, we also lose some of the things that we could do without such restrictions. It turns out that any program can be rewritten in a functional style in a way to produce the same results. However, no guarantees are made about their performance or complexity in general. So, a functional version of a program can be a lot less efficient than its imperative counterpart. And indeed, in real life, we face many such scenarios. So, it is really a tradeoff between performance and simplicity. The general direction should then be that when working with a large input size, it is better to do away with restrictions in order to be able to optimize more. On the other hand, when the input sizes are small, it makes sense to stick to a functional style because the performance is probably not affected much by it. There are some cases, though, where the functional version has the same running time complexity as the imperative version. In such a case, a functional version might be preferred because of its simplicity. It should be noted that since Java does not provide any explicit way of garbage collection and really, it happens by chance or outside the control of the programmer, a functional programming style will fill up the heap very quickly because of being immutable and thus being thrown away right after being created. So, it will not be advisable to use them where performance is really a problem. [ 89 ] Detour – Functional Programming This would seem really contrary to the fact that many large data processing systems, such as Spark, use a functional programming style. However, these systems only have a specialized language that gives an appearance of a functional programming style; they get translated to an almost non-functional form before they are even executed. To elaborate a little more, a map method in a monad may not evaluate anything at all; instead, it may just create a new object that contains this operator. A general program can then analyze these structures and construct an imperative program that does the same work. This provides a simple interface to the person using the framework as well as keeping the resource usage under control. In the next chapter, we will explore some of these ideas. Summary In this chapter, we learned a new way of looking at algorithms. The functional style of writing a program can simplify the analysis of its correctness, that is, you can easily understand why the program produces correct output. We saw a few patterns in functional programming, especially monads. We also saw how Java provides support for the functional style of programming through the syntax called lambda, which has existed from version 9 of Java. Finally, we saw how to use lambda effectively for functional programming. Functional programs are, in general, easier to verify for correctness, but it is harder to compute their complexity. They generally perform either at the same speed as or slower than their imperative counterparts. It is a trade-off between development effort and computational efficiency. For smaller inputs, it is thus desirable to have a functional style of programming, whereas for processing large inputs, imperative style may be preferred. [ 90 ] Efficient Searching – Binary Search and Sorting What is searching? Searching is trying to locate a given value in a collection of values. For example, you are given an array of integers, and your problem is to check whether the integer 5 is in that array. This is a search problem. In addition to just deciding whether the element 5 is in the array, we may be interested in its location as well when it is found. This is also a search problem. Another interesting take on it would be to imagine a dictionary, that is, an array of values and associated values. For example, you have an array of names of students and their marks, as shown in the following table: Name Marks Tom 63 Harry 70 Merry 65 Aisha 85 Abdullah 72 … ... The list continues. Suppose, our system lets the student view his/her own marks. They would type their name and the system would show their marks. For simplicity, let's assume that there are no duplicate names. Now, we have to search for the name provided and return the corresponding values. This is, thus, another search problem. As we will see, search problems are quite ubiquitous in programming. [ 91 ] Efficient Searching – Binary Search and Sorting In this chapter, you will learn the following: • Search algorithms • Efficient searching in a sorted list • Some sorting algorithms Search algorithms Suppose you are given an array of values and you are required to check whether a particular value is in that array, what is the most natural way to find that element? Well, it seems that the natural way is to go through each element one by one and check whether they match the given value. If any one does, we have found that element and we can return the index; if not, we can return -1 at the end of processing all the elements to report that such an element could not be found. This is what we would call a linear search. The following demonstrates a linear search algorithm in an array: public static int linearSearch(E[] values, valueToLookup) { for (int i = 0; i < values.length; i++) { if (values[i].equals(valueToLookup)) { return i; } } return -1; } The function linearSearch takes an array of values and a value to search in it, and returns the index if the value is found. If not, it returns a special value, -1. The program simply goes through each element and checks whether the current element matches with the value to lookup; if it does, then it just returns the current index, otherwise it keeps looking. At the end of the array, it returns the special value, -1. Now the following piece of code should return -1 in the first case and the value 5 in the second case: Integer[] integers = new Integer[]{232,54,1,213,654,23, 6,72,21}; System.out.println(ArraySearcher.linearSearch(integers,5)); System.out.println(ArraySearcher.linearSearch(integers,23)); [ 92 ] Chapter 5 Now, if we want to solve the student-marks problem described in the introduction of this chapter, we just need to have the marks of the students stored in a different array in the same order, as follows: static String[] students = new String[]{"Tom","Harry", "Merry","Aisha", "Abdullah"}; static int[] marks = new int[]{63,70, 65, 85, 72}; Now we can write a function to search for a name: public static Integer marksForName(String name){ int index = linearSearch(students, name); if(index>=0){ return marks[index]; }else{ return null; } } First, we look for the name of the students in the list of students. If the name is found, the corresponding index would be assigned to the variable index and the value would be greater than or equal to zero. In such a case, we return the marks stored in the same index as of the marks array. If it is not found, we return null. To lookup the marks for Merry, for example, we call as shown here: System.out.println(marksForName("Merry")); We correctly obtain her marks, that are, 65. What is the complexity of linear search? We have a for loop that moves through each element of an array of length n (say); in the worst case, we would go through all the elements, so the worst case complexity is θ(n). Even on an average, we would be visiting half the elements before we hit the correct element, so the average case complexity is θ(n/2) = θ(n). Binary search Is a linear search the best we can do? Well, it turns out that if we are looking at an arbitrary array, this is what we have to do. After all, in an arbitrary array, there is no way to know whether an element is there without potentially looking at all of them. More specifically, we cannot say for sure that some element does not exist, without verifying all the elements. The reason is that the value of one element does not say anything about the values of the other elements. [ 93 ] Efficient Searching – Binary Search and Sorting But, what information can one element have about other elements in an array? One way to make elements have information about the other elements is to have a sorted array instead of just an arbitrary array. What is a sorted array? A sorted array is an array that has all the elements arranged in order of their values. When an array is sorted, every element contains the information that everything on the left is smaller than that particular element, and everything on the right is bigger (or the other way round if the order of the elements is opposite, but we will consider the arrays that are sorted in an increasing order from left to right). This information can, amazingly, make this search a lot faster. Here is what we do: • • • Check the middle element of the array. If it matches the element we are searching for, we are done. If the middle element is smaller than the value we are searching for, search on the subarray on the right of the current array. This is because everything on the left is even smaller. If the middle element is bigger than the value we are searching for, search only in the left sub-array. To avoid creating copies of the array while creating a sub-array, we just pass on the whole array, but we remember the start and end positions we are looking at. The start is included in the range and end is excluded. So, only elements on the right of the start position and the left of the end position are included in the subarray being searched. The following figure gives a visual understanding of binary search: Figure 1: Binary Search. An arrow representing moving one element to another during the search. [ 94 ] Chapter 5 But, before implementing this algorithm, we need to understand the concept of Comparable. Comparable is an interface in the Java standard library that looks like this: package java.lang; public interface Comparable { public int compareTo(T o); } Any class implementing this interface has to compare a different object with itself. It is required that the type parameter, T, must be instantiated with the same class that implements it, like this: public class Integer implements Comparable { public int compareTo(Integer o){ … } } The compareTo method intends to compare an object of the same type. If the current object (the one that the this reference refers to) is smaller than the object passed, compareTo must return a negative value. If the object passed is smaller, the method must return a positive value. Otherwise, if they are equal, it must return 0. The following conditions are required to be fulfilled by the compareTo method: • If a.compareTo(b) == 0, then a.equals(b) must be true • If a.compareTo(b) < 0 and b.compareTo(c) < 0, then a.compareTo(c) <0 • If a.compareTo(b) <0, then b.compareTo(a) > 0 • If b.equals(c) is true and a.compareTo(b) <0, then a.compareTo(c) <0 • If b.equals(c) is true and a.compareTo(b) >0, then a.compareTo(c) >0 Basically, the conditions are the same for a total order where equality is represented by the equals method. It basically generalizes the concept of the < and <= operators, which are there for numbers. Of course, the compareTo method for the Wrapper objects are implemented exactly as the < and <= operators are on the primitives inside them. Now, we write the search function to do the search, as per the preceding steps: private static , F extends E> int binarySearch( E[] sortedValues, F valueToSearch, int start, int end) { if(start>=end){ return -1; } [ 95 ] Efficient Searching – Binary Search and Sorting int midIndex = (end+start)/2; int comparison = sortedValues[midIndex].compareTo( valueToSearch); if(comparison==0){ return midIndex; }else if(comparison>0){ return binarySearch(sortedValues, valueToSearch, start, midIndex); }else{ return binarySearch(sortedValues, valueToSearch, midIndex+1, end); } } Note that in this case, we have mandated that the objects in the array must be comparable so that we can know if an object is greater than or less than another object. It does not matter exactly how this relationship is determined; the array must be sorted using the same comparison – that the comparison between two consecutive elements will make sure the element on the left is smaller than the one on the right, as provided by Comparable. The first if condition checks whether the array passed is empty, if so, obviously the element to be searched is not found and we return -1 to represent this. Then, we find the midIndex and recursively search for the element in either the left or the right subarray. Once we have this function, we create another wrapper function to run the search without having to mention the start and the end positions: public static , F extends E> int binarySearch( E[] sortedValues, F valueToSearch) { return binarySearch(sortedValues, valueToSearch, 0, sortedValues.length); } Complexity of the binary search algorithm In every step, we are partitioning the total array into two parts, barring the one element that we are comparing. In the worst case, that is, the case where the element searched is not present in the array, we will have to get down all the way to the point where we are dealing with an empty array, in which case we return -1 and stop recursing. We must have had an array of only one element in the previous step. For our analysis, we will consider this step as the last step. So, let's have a sorted array of n elements and T(.) is the time required for searching in the array. So, we have the following: T(n) = T((n-1)/2) + C, where C is a constant. [ 96 ] Chapter 5 In general, the two search branches in every step would be of different sizes, one potentially being of size one less than that of the other part. But we will ignore these small differences, which hardly matter for a large n. Hence, we will work with the following equation: T(n) = T(n/2) + C Now, let's assume that n is an integral power of 2 so that n = 2m for some integer m. So, we have this: T(2m) = T(2m-1) + C Now we take another function S(.), such that S(m) = T(2m) for all m. Then, we have: S(m) = S(m-1) + C This is the formula for an arithmetic progression. And hence, we have this: S(m) = mC + D, where D is also a constant. => T(2m) = mC + D => T(n) = C lg(n) + D So, we have the asymptotic complexity of T(n): T(n) = O(lg(n)) The function, T(n), grows only as fast as the logarithm of the size of the array passed, which is really slow. This makes binary search an extremely efficient algorithm. To sort of field test the algorithms, we run both linear and binary search algorithms on an array of a hundred million elements with the following code: int arraySize = 100000000; Long array[] = new Long[arraySize]; array[0] = (long)(Math.random()*100); for(int i=1;i T(n) – T(n-1) = Cn + D Similarly, T(n-1) - T(n-2) = C(n-1) + D and so on. If we stack these equations, we get the following: T(n) – T(n-1) = Cn + D T(n-1) – T(n-2) = C(n-1) + D T(n-2) – T(n-3) = C(n-2) + D T(n-3) – T(n-4) = C(n-3) + D … T(1) – T(0) = C(1) + D [ 101 ] Efficient Searching – Binary Search and Sorting Adding both sides, we get this: So, the selection sort has a complexity of θ(n2), where n is the size of the array being sorted. Now, we will see the next sorting algorithm, which is the insertion sort. Insertion sort In selection sort, we first selected a position and then found the element that should sit there. In the insertion sort, we do the opposite; we first select an element and then insert the element into position where it should sit. So, for every element, we first find out where it should be and then we insert the element in the right place. So, we first see how to insert an element into a sorted array. The idea is that we are given an array of sorted elements and we are supposed to insert another element in the correct position, so that the resulting array remains sorted. We will consider a simpler problem. We are given an array that is sorted except for the last element. Our job is to insert the last element in the correct position. The recursive way to achieve this insertion is as follows: • If the element to be inserted is bigger than the end element of the sorted array, it belongs in the end and the insertion is complete. • Otherwise, we swap the last element with the element to be inserted and recursively insert this element in the rest of the smaller array in front of it. We do it with the following function. The function takes an array and a position that represents this last position: public static > void insertElementSorted( E[] array, int valueIndex) { if (valueIndex > 0 && array[valueIndex].compareTo( array[valueIndex - 1]) < 0) { swap(array, valueIndex, valueIndex - 1); [ 102 ] Chapter 5 insertElementSorted(array, valueIndex - 1); } } If the last position or the valueIndex is 0, there is nothing to be done, as the element is already in the correct position, that is, 0. There is no array to the left of valueIndex in this case. If not, we compare the last element to the previous element. Since the array on the left is presumed to be sorted, the previous element is the largest element in the sorted part of the array. If the last element is bigger than even this one, there is nothing more to be done. If not, we swap the last element with the previous one and run the insertion recursively on the array with one less element. The last element has moved to the previous position and it must now be compared with the element before that, and so on. With the insertion function available for sorted arrays, we are now ready to write the algorithm for an insertion sort. In every step of the insertion sort, we consider a boundary in the array. Everything on the left of the boundary has already been sorted. Our job in the current step is to insert the element at the boundary index into the left sorted array, which we achieve using the insertElementSorted function. We implement this sort with the following simple strategy. In any step, we do the following: • We first sort the left-hand side of the boundary so that our assumption about it being sorted is achieved • Then we invoke the insertElementSorted function to insert the current boundary element in the sorted array Of course, when boundary is zero, it means that there is no array to be sorted and we simply return: public static > void insertionSort( E[] array, int boundary) { if(boundary==0){ return; } insertionSort(array, boundary-1); insertElementSorted(array, boundary); } [ 103 ] Efficient Searching – Binary Search and Sorting Complexity of insertion sort To compute the complexity of insertion sort, we must first compute it for the insertElementSorted function. Let the time taken for an array of effective length (that is, from zero to boundary-1), n be T(n). From there, we recursively call it with n-1. So, we have the following: T(n) = T(n-1) + C where C is a constant => T(n) = θ(n) Let's now assume that the time taken for sorting an array of n elements is S(n). Apart from the base case, it calls itself with one less argument and then calls the insertElementSorted function with an array of effective length n-1. Thus, we have this: S(n) = S(n-1) + T(n) + D where D is a constant. Again, when n is large, T(n) = θ(n); hence, it can be approximated by An where A is a constant. So, we have this: S(n) = S(n-1) + An + D => S(n) – S(n-1) = An + D, Since this is true for all n, we have: S(n) – S(n-1) = An + D S(n-1) – S(n-2) = A(n-1) + D S(n-2) – S(n-3) = A(n-2) + D … S(1) – S(0) = A + D Summing both sides, we get the following: Thus, insertion sort has the same asymptotic complexity as selection sort. [ 104 ] Chapter 5 Bubble sort Another interesting sorting algorithm is a bubble sort. Unlike the previous algorithms, this one works at a very local level. The strategy is as follows: • Scan through the array, searching pairs of consecutive elements that are ordered wrongly. Then find a j, such that array[j+1] < array[j]. • Whenever such a pair is found, swap them and continue searching until the end of the array and then back from the beginning again. • Stop when a scan through the entire array does not even find a single pair. The code that does this is as follows: public static > void bubbleSort( E[] array) { boolean sorted = false; while (!sorted) { sorted = true; for (int i = 0; i < array.length - 1; i++) { if (array[i].compareTo(array[i + 1]) > 0) { swap(array, i, i + 1); sorted = false; } } } } The flag, sorted, keeps track of whether any inverted pairs were found during a scan. Each iteration of the while loop is a scan through the entire array, the scan being done inside the for loop. In the for loop, we are, of course, checking each pair of elements, and if an inverted pair is found, we swap them. We stop when sorted is true, that is, when we have not found a single inverted pair in the entire array. To see that this algorithm will indeed sort the array, we have to check two things: • When there are no inverted pairs, the array is sorted. This justifies our stopping condition. [ 105 ] Efficient Searching – Binary Search and Sorting This is, of course, true because when there are no inverted pairs, we have that for all j< array.length-1, we have array[j+1]>=array[j]. This is the definition of an array being in an increasing order, that is, the array being sorted. • Irrespective of the input, the program will eventually reach the preceding condition after a finite number of steps. That is to say that we need the program to finish in a finite number of steps. To see this, we need to understand the concept of inversions. We will explore them in the next section. Inversions Inversion in an array is a pair of elements that are wrongly ordered. The pair may be close together or very far apart in the array. For example, take the following array: Integer[] array = new Integer[]{10, 5, 2, 3, 78, 53, 3}; How many inversions does the array have? Let us count: 10>5, 10>2, 10>3, 10<78, 10<53, 10>3 5>2, 5>3, 5<78, 5<53, 5>3 , 2<3, 2<78, 2<53, 2<3 , 3<78, 3<53, 3=3 , 78>53, 78>3 53>3 In this listing, every element is compared with the elements following it. There is an inversion when there is a greater-than sign, highlighted by bold characters. Counting the bold ones, we see there are 10 inversions. For any input array, there is a number of inversions. In a sorted array, the number of inversions would be zero. Now, think about what happens to the number of inversions when a swap is made. A swap interchanges a pair of consecutive elements, thus breaking one inversion (the swap happens only when there is an inversion between consecutive elements). To see this more clearly, consider the following case of a swap between j and j+1 indexes: …......., j, j+1, ….... [ 106 ] Chapter 5 Let's take the jth element first. Let it have x number of inversions with the left part of the array. Since these elements are on the left, all inversions of this type are with elements greater than the jth element. When the jth element moves to the (j+1)th position, they still remain to the left, and the only element added to the left of the jth element is the element it is swapped with. Hence, no changes to the number of inversion happens to the jth element, other than the one due to the (j+1)th element. The same logic can be applied to the inversions with it in the right part of the array, and also to both sides of the array for the (j+1)th element. Because of the swap, one inversion is broken between jth and (j+1)th elements. Hence, the number of inversions reduce by exactly one in each inversion. This means the number of swaps in bubble sort would be exactly equal to the number of inversions in the input array, which is finite. And since each scan through the array requires a swap in the previous scan, the total number of scans is at most one more that the number of swaps; this is finite too. This makes sure that the algorithm always finishes. Complexity of the bubble sort algorithm To understand the complexity of a bubble sort, we have to count the number of steps. Is the number of steps equal to the number of swaps? The answer is, not really. In case of asymptotic analysis, we must always count the step that happens a maximum number of times. In this case, that step is comparison. How many comparisons are there per scan of the array? n-1 of course. So, now the analysis of complexity is reduced to the number of scans we need to sort the array. Let's see what happens to the maximum element after the first scan. Let's say the maximum element is at the index j. So, it will be compared with the element at j+1. For simplicity, let's assume that all elements are different. Now, since it is the maximum element, the element at the j+1 position will be less than it, and hence it will be swapped. Now the maximum element is at the position, j+1, and is being compared with the element at position, j+2, and the same thing happens. It will continue until the maximum element is at the end of the array. If the elements are not unique, the same will happen to the rightmost maximum element. In the next cycle, the maximum element will already be at the end of the array, and we will hit the second maximum (or another maximum element) somewhere in the array. Now, since one maximum element is at the end of the array, we can think of the rest of the array apart from the last element. In this array, the current maximum is the maximum and it will reach the end of the current part of the array at the end of the current scan. [ 107 ] Efficient Searching – Binary Search and Sorting This shows that at the end of each scan, at least one element reaches the correct final position without altering the correct positions of the ones that got there before the scan, which means that at the end of n scans, all of the elements would be in the correct position and the array would be sorted. That is to say that after at most n scans, the bubble sort would be complete. In each of those scans, there are O(n) operations. So, the worst case complexity of bubble sort is O(n2). This is not the end of this analysis; we still have to show that there is a case that takes that many steps, and only then can we have a theta bound on the worst case. We take the case where all the elements are sorted in the opposite order, that is, they are in a decreasing order and are all distinct. In such a case, every element has an inversion with all the others. This means that each one of the n elements have an inversion with n-1 other elements, that is, n(n-1) inversions in total. But since each inversion would be counted twice, once from each of the elements that are members of the inversion, it is actually n(n-1)/2. Now, note that the maximum number of swaps that can be done in one scan is n-1, which will happen if every comparison results in a swap because there are n-1 comparisons per scan. So, we will need at least (n(n-1)/2)/ (n-1) = n/2 scans to complete all the swaps, each requiring n-1 comparisons. So, the complexity is at least n(n-1)/2 = Ω(n2). Of course then, the worst case is at least this much complex because the worst case is, by definition, the most complex case. So, the worst case is both O(n2) and Ω(n2), that is to say that it is θ(n2). A problem with recursive calls A problem with recursive calls is that they are expensive; a method invocation entails considerable overhead on the processor. It is, in general, better to avoid invoking methods if you want to improve performance to the last bit. On top of that, there is a limit to the depth of function calls that you can go to, before the program breaks. This is because a program has a stack to enable method invocation semantics, that actually gets a new element containing all variables and the position of the current instruction to the stack. This stack does not grow indefinitely, but instead is fixed in size; usually, it can hold a few thousand values, which means that if your method invocation is deeper than that, it will break and the program will exit with an error. This means that our insertion sort will break for an array containing more than a few thousand entries. On the other hand, it is generally easier to explain an algorithm in a functional form. To balance between these two aspects, we need to be able to convert to and from the recursive and non-recursive versions of the same algorithm. We will do this step by step from the simplest form to the more complicated form. [ 108 ] Chapter 5 Tail recursive functions A recursive function is called a tail recursive function if all the recursive calls to itself in the function are the last operations. I say it like there are multiple calls and all of them must be the last operations. How is that possible? I mean there can be different calls from different conditional branches of the code inside the function. However, whenever the function calls itself that must be the last operation in that conditional branch of the function. For example, take our binary search algorithm again: private static