The fringe of a binary tree are the values in left-to-right order. For example, the fringe of the following tree:

Similar documents
You ve encountered other ways of signalling errors. For example, if you lookup an unbound key in a hashtable, Java (and Scala) produce nulls:

Finally, all fields are public by default, so you can easily write simple functions, such as this one, without writing getters:

Lecture 2. 1 Immutability. 2 Case Classes

The Art of Recursion: Problem Set 10

Computer Science 210 Data Structures Siena College Fall Topic Notes: Trees

Lecture #11: Immutable and Mutable Data. Last modified: Sun Feb 19 17:03: CS61A: Lecture #11 1

Lecture 7: Primitive Recursion is Turing Computable. Michael Beeson

Lecture 23: Priority Queues 10:00 AM, Mar 19, 2018

CS Introduction to Data Structures How to Parse Arithmetic Expressions

Lecture 1 Contracts : Principles of Imperative Computation (Fall 2018) Frank Pfenning

Lecture 1 Contracts. 1 A Mysterious Program : Principles of Imperative Computation (Spring 2018) Frank Pfenning

Intro. Scheme Basics. scm> 5 5. scm>

Project Compiler. CS031 TA Help Session November 28, 2011

Induction and Semantics in Dafny

CISC-124. Casting. // this would fail because we can t assign a double value to an int // variable

Notes from a Short Introductory Lecture on Scala (Based on Programming in Scala, 2nd Ed.)

CS1 Lecture 5 Jan. 26, 2018

Compilers and computer architecture: Semantic analysis

Applicative, traversable, foldable

Lecture #12: Immutable and Mutable Data. Last modified: Mon Feb 22 16:33: CS61A: Lecture #12 1

Lecture 8: Iterators and More Mutation

16 Multiple Inheritance and Extending ADTs

Lecture 21: The Many Hats of Scala: OOP 10:00 AM, Mar 14, 2018

Lab 10: Sockets 12:00 PM, Apr 4, 2018

Thread Safety. Review. Today o Confinement o Threadsafe datatypes Required reading. Concurrency Wrapper Collections

Test 1: CPS 100. Owen Astrachan. October 1, 2004

COMP 250 Fall recurrences 2 Oct. 13, 2017

CS103 Handout 29 Winter 2018 February 9, 2018 Inductive Proofwriting Checklist

8 Understanding Subtyping

Applicative, traversable, foldable

Compilers and computer architecture: A realistic compiler to MIPS

COMP-520 GoLite Tutorial

Student Number: Lab day:

Lecture 4. The Java Collections Framework

CS558 Programming Languages

Type Checking in COOL (II) Lecture 10

tree nonlinear Examples

Checked and Unchecked Exceptions in Java

Implementing Continuations

Sorting: Given a list A with n elements possessing a total order, return a list with the same elements in non-decreasing order.

CompSci 220. Programming Methodology 12: Functional Data Structures

HOW TO WRITE RECURSIVE FUNCTIONS ON YOUR OWN

CS61B Lecture #20: Trees. Last modified: Mon Oct 8 21:21: CS61B: Lecture #20 1

CSE332: Data Abstractions Lecture 23: Programming with Locks and Critical Sections. Tyler Robison Summer 2010

Lecture 23: Priority Queues, Part 2 10:00 AM, Mar 19, 2018

CS 151. Linked Lists, Recursively Implemented. Wednesday, October 3, 12

CSCI Lab 9 Implementing and Using a Binary Search Tree (BST)

Functions CHAPTER 5. FIGURE 1. Concrete syntax for the P 2 subset of Python. (In addition to that of P 1.)

APCS Semester #1 Final Exam Practice Problems

Lecture 4 Searching Arrays

Topic 14. The BinaryTree ADT

CSC 1052 Algorithms & Data Structures II: Linked Lists Revisited

Lecture 10 Notes Linked Lists

Code Reuse: Inheritance

CS162 Week 1. Kyle Dewey. Friday, January 10, 14

15-780: Problem Set #3

Name Section Number. CS210 Exam #4 *** PLEASE TURN OFF ALL CELL PHONES*** Practice

CS61B Lecture #20: Trees. Last modified: Wed Oct 12 12:49: CS61B: Lecture #20 1

11 Parameterized Classes

CMPSCI 187 / Spring 2015 Implementing Sets Using Linked Lists

COMPUTER SCIENCE TRIPOS

CSE 143 SAMPLE MIDTERM

Object-Oriented Design Lecture 16 CS 3500 Fall 2010 (Pucella) Friday, Nov 12, 2010

CS1 Lecture 5 Jan. 25, 2019

CSE341: Programming Languages Lecture 9 Function-Closure Idioms. Dan Grossman Winter 2013

/633 Introduction to Algorithms Lecturer: Michael Dinitz Topic: Sorting lower bound and Linear-time sorting Date: 9/19/17

Trees. Chapter 6. strings. 3 Both position and Enumerator are similar in concept to C++ iterators, although the details are quite different.

Variables and Data Representation

CSC148H Week 3. Sadia Sharmin. May 24, /20

CSE143X: Computer Programming I & II Programming Assignment #10 due: Friday, 12/8/17, 11:00 pm

Red-Black trees are usually described as obeying the following rules :

INF 212 ANALYSIS OF PROG. LANGS FUNCTION COMPOSITION. Instructors: Crista Lopes Copyright Instructors.

Basic Data Structures

CSIS 10B Lab 2 Bags and Stacks

What is an Iterator? An iterator is an abstract data type that allows us to iterate through the elements of a collection one by one

Traversing Trees with Iterators

Traversing Trees with Iterators

CS61B Lecture #20. Today: Trees. Readings for Today: Data Structures, Chapter 5. Readings for Next Topic: Data Structures, Chapter 6

CS558 Programming Languages

Week 5 Tutorial Structural Induction

Classes Classes 2 / 36

Non-numeric types, boolean types, arithmetic. operators. Comp Sci 1570 Introduction to C++ Non-numeric types. const. Reserved words.

Lecture 10 Notes Linked Lists

8 Hiding Implementation Details

CS1622. Semantic Analysis. The Compiler So Far. Lecture 15 Semantic Analysis. How to build symbol tables How to use them to find

CS558 Programming Languages

APT Session 6: Compilers

A Sophomoric Introduction to Shared-Memory Parallelism and Concurrency Lecture 5 Programming with Locks and Critical Sections

From last time. What is the effect on R0 (in terms of an equivalent multiplication) of each of the following ARM instructions? ADD R0, R0, R0,LSL #3

Binary Search Trees. Contents. Steven J. Zeil. July 11, Definition: Binary Search Trees The Binary Search Tree ADT...

This is a set of practice questions for the final for CS16. The actual exam will consist of problems that are quite similar to those you have

Lecture: Functional Programming

Basic Data Structures 1 / 24

15 Multiple Inheritance and Traits

COMP 161 Lecture Notes 16 Analyzing Search and Sort

Lecture 15 Binary Search Trees

Assertions and Exceptions Lecture 11 Fall 2005

CS 3410 Ch 7 Recursion

/633 Introduction to Algorithms Lecturer: Michael Dinitz Topic: Priority Queues / Heaps Date: 9/27/17

CSCI 3155: Homework Assignment 3

Transcription:

Lecture 13 The Same Fringe Problem Given a binary tree: sealed trait BinTree [+A] case class Node [A]( lhs : BinTree [A], rhs : BinTree [A]) extends BinTree [A] case class Leaf [A]( x: A) extends BinTree [A] The fringe of a binary tree are the values in left-to-right order. For example, the fringe of the following tree: val t1 = Node ( Leaf (10), Node ( Leaf (20), Leaf (40))) is 10, 20, and 40, in that order. The same-fringe problem is to write a function to determine if two trees have the same fringe. For example the following tree: val t2 = Node ( Node ( Leaf (10), Leaf (20)), Leaf (40)) Has the same fringe as t1. We can write a simple recursive function to calculate the fringe of a tree, as shown in fig. 23.1, so a simple solution to the same-fringe problem is: fringe ( t1) == fringe ( t2) But, this is not a very efficient solution: If the trees are very large, we ll spend a lot of time appending lists, and If the fringes are different, we ll generate the entire fringe instead of terminating as soon as a difference is detected. An Imperative Solution A typical Java solution to this problem, which is straightforward to reproduce in Scala, is to use an iterator. You ve seen simple iterators for lists and arrays before. You can also write an iterator that iterates over the fringe of a tree. The key idea is this: before we descend into the left-hand side of a node to generate its fringe, we need to store the right-hand side of the node in a variable so that we don t forget to generate its fringe. Since the tree may be arbitrarily deep, we may need to remember a stack of nodes. Think! What would happen if we used a queue of nodes? An iterator that uses this idea is shown in fig. 23.2 along with a solution to the same-fringe problem that doesn t have the two issues we identified above. An unusual feature of this solution is that it uses two iterators simultaneously in a single loop. What s interesting about this problem is that the solution seems to be fundamentally imperative. In particular, each invocaation of.next returns the next value, which means that the iterators must use mutable state internally. Is it possible to solve this problem without state? def fringe [A]( t: BinTree [A ]): List [A] = t match { case Leaf (x) => List (x) case Node (lhs, rhs ) => fringe ( lhs ) ++ fringe ( rhs ) Figure 23.1: Recursively calculating the fringe of a binary tree. 113

class FringeIterator [A]( tree : BinTree [A]) extends Iterator [A] { private val stack = collection. mutable. Stack ( tree ) def hasnext = stack. isempty def next () = { val top = stack. pop () top match { case Leaf (x) => x case Node (lhs, rhs ) => { stack. push ( rhs ); stack. push ( lhs ); next () def samefringe [A]( t1: BinTree [A], t2: BinTree [A ]): Boolean = { val iter1 = new FringeIterator ( t1) val iter2 = new FringeIterator ( t2) while ( iter1. hasnext && iter2. hasnext ) { val x = iter1. next val y = iter2. next if (x!= y) { return false return! iter1. hasnext &&! iter2. hasnext Figure 23.2: An imperative solution. def g (): Int = { println (" Evaluating 10 def f1(x: Int ) = { println (" Evaluating x + 10 f1(g ()) g") f1") Figure 23.3: Which line prints first? Think! Try to solve the problem before proceeding further. By-Name Arguments When you apply a function in Scala (and in most other programming languages), the argument of the function is first reduced to a value and then that value is passed to the function. For example, in f(2 + 3), the expression 2 + 3 is first reduced to the value 5 and then f(5) is applied. Think! Is the statement above true? Can you think of a way to experimentally validate the claim that arguments are reduced to values before they are passed to functions? Consider the two functions in fig. 23.3 and the expression f1(g()). If the argument is evaluated first, then Evaluating g will print before evaluating f. Conversely, if the expression g() is passed to f1 unevaluated and only evaluated when it is needed, then we d expect the lines to print in the opposite order. If you try this out, you ll find that the former is true, thus validating the claim. However, Scala allows you to change the order of evaluation. In the following function, the => annotation means that the argument is only evaluated when it is needed: def f2(x: => Int ) = { println (" Evaluating f2") x + 10 114

Therefore, the expression f2(g()) prints Evaluating f2 before it prints Evaluating g, since the value of g() is not needed until after Evaluating f2 is printed. The following function ignores its argument: def f3(x: => Int ) = { println (" Evaluating 10 f3") Therefore, f3(g()) does not evaluate g() at all, so Evaluating g is not printed. In fact, you can even write f3(throw new Exception("bad")) and the exception will not be thrown. By-name arguments have several applications and are used extensively in the Scala standard library. For example, Scala maps have a.get method that optionally returns a value stored in a map: val m = Map ("X" -> 1 ) val v = m. get ( key ) // produces Some (1) if key is "X", otherwise None val v2 = m. get ("Z") // produces None Suppose we were using maps in a program and that we wanted to return a default value 0 instead of None. We could write: m. get ( key ) match { case Some (v) => v case None => 0 Alternatively, we could simply write m.getorelse(key, 0). It turns out that the second argument of this method is a by-name argument. Therefore, we can write m.getorelse(key, f()) and be assured that f will not be applied unless it is needed. It is safe to write, even if f() is an expensive computation that should only be run if key is not in the map. We could even write m.getorelse(key, throw new Exception(...)) to throw a custom exception if key is not found. Functional Generators The reason that iterators cannot be functional is evident in the type for iterators. The Iterator<T> interface is part of the Java standard library and it corresponds to the following Scala trait: trait Iterator [T] { def hasnext (): Boolean def next (): T def remove (): Unit // not relevant for our discussion Recall that a key property of functional programs is that a function (or method) always produces the same result when applies to the same arguments. In this interface, the next() method takes zero arguments. Therefore, if an implementation of Iterator[T] were written functionally, it could only ever produce one result. To allow functional programming to work, we re going to have to work with a new kind of iterator, which we ll call a generator, where the next method produces the next value and a new generator that generates the rest of the collection: trait Generator [T] { def hasnext (): Boolean def next (): (T, Generator [T]) An important detail of iterators that isn t obvious from the type, is that the next method will throw an exception if hasnext produces false. Instead of throwing an exception, we can make this behavior manifest in the type, by eliminating hasnext method and changing the type of next to optionally produce an element and a generator. trait Generator [T] { def next (): Option [(T, Generator [T ])] We can now write some a functional generator. For example, here is a generator that produces three numbers: val agen = new Generator [ Int ] { def next () = Some ((1, new Generator [ Int ] { def next () = Some ((2, new Generator [ Int ] { def next () = Some ((3, new Generator [ Int ] { def next () = None 115

)) )) Here is a simple function that prints all the elements generated by any generator, including the one above: def printall [T]( gen : Generator [T ]): Unit = gen. next () match { case None => () case Some ((x, rest )) => println (x); printall ( rest ) )) Combining Generators You probably agree that the definition of agen is too unwieldy. To make generators easier to write, we are going to define three generator building functions: 1. The function zero[t]() produces a generator that generates zero values of type T: def zero [T ](): Generator [T] = new Generator [T] { def next () = None 2. The function one(x) produces a generator that only generates the value x: def one [T]( x: T): Generator [T] = new Generator [T] { def next () = Some ((x, zero [T ]())) 3. The function append(gen1, gen2) takes two generators as arguments and first generates the values of gen1 and then the values of gen2. Furthermore, to avoid generating values we don t need, the arguments are passed by-name: def append [T]( gen1 : => Generator [T], gen2 : => Generator [T ]): Generator [T] = new Generator [T] { def next () = gen1. next match { case None => gen2. next case Some ((x, gen1rest )) => Some ((x, append ( gen1rest, gen2 ))) To make append easier to use, we can modify the Generator[T] trait to include an operator that invokes append: trait Generator [T] { def next (): Option [(T, Generator [T ])] // as before def ++( other : => Generator [T ]): Generator [T] = append ( this, other ) Using these functions, we can write a generator that generates the fringe of a binary tree: def fringe [T]( t: BinTree [T ]): Generator [T] = t match { case Leaf (x) => one (x) case Node (lhs, rhs ) => fringe ( lhs ) ++ fringe ( rhs ) Notice that it is very similar to the function that generates the fringe as a list. However, it behaves quite differently. Since the append (++) operation evaluates its arguments only when needed, it doesn t immediately visit all the leaves of the tree, which was the problem with the original function. The following function takes two generators and produces true if the generate exactly the same values: def samegen [T]( gen1 : Generator [T], gen2 : Generator [T ]): Boolean = ( gen1. next, gen2. next ) match { case ( None, None ) => true case ( Some ((x, rest1 )), Some ((y, rest2 ))) => x == y && samegen ( rest1, rest2 ) case _ => false If the supplied generators only generate values on need, the samegen function will abort early and produce false if (1) it encounters two values x!= y or (2) one generator runs out of values before the other. Notice that samegen has the exactly the same structure as the obvious recursive function that checks if two lists are equal. Finally, we can write the samefringe function that we wanted as follows: def samefringe [T]( t1: BinTree [T], t2: BinTree [T]) = samegen ( fringe ( t1), fringe ( t2 )) Think! How would you verify that this version of samefringe truly aborts early and doesn t incorrectly traverse both trees when it isn t necessary? 116

More Generators It is staightforward to define other handy generator-building functions. For example, it is easy to define functions that are analogous to map, filter, and fold for lists. You can even write a function to flatten a generator-generator into a simple generator: def flatten [T]( gen : Generator [ Generator [T ]]): Generator [T] = new Generator [T] { def next () = gen. next match { case None => None case Some ((x, rest )) => (x ++ flatten ( rest )). next 117

118