class: left, middle, title-slide # CSCI-UA 102 ## Data Structures
## Java Programs (Under the Hood) .author[ Instructor: Uday Kusupati
] .license[ Copyright 2020 Joanna Klukowska. Unless noted otherwise all content is released under a
[Creative Commons Attribution-ShareAlike 4.0 International License](https://creativecommons.org/licenses/by-sa/4.0/)] --- layout:true template: default name: section class: inverse, middle, center --- layout:true template: default name: breakout class: breakout, middle --- layout:true template:default name:slide class: slide .bottom-left[© Joanna Klukowska. CC-BY-SA.] --- ## pollev.com/uday
--- ## Recap
--- template: slide ## Why do I need to read code - understanding existing code base - debugging skills - code review - learning new things - code interviews --- ## Process of reading a new codebase - __get a big picture__ - run the code if possible to see what it does, understand the purpose of the whole program and its general behavior -- - __understand the structure__ - look at the files, how are they organized, what do they contain where is the entry point for the program, if available, read the top level documentation for each file/class -- - __study the overall logic__ - look at the _big_ code blocks and functions, what is the flow of the program -- - __dive into details__ - start reading functions and code blocks more carefully, determine data structures that are used, figure out any parts that are unclear -- - __test-drive the program__ - once you understand more of the code, run the program and try to break it, what assumptions does it make about the _outside world behavior_, are there situations that it cannot handle -- - __analyze the performance__ - think through data structures and algorithms to analyze overall performance of the program, can things be improved or optimized -- - __study each piece__ - analyze each class and function, can they be used outside of the context of this program, how, what issues might come up if they are --- ## Things that may help - take notes - draw diagrams - read the available README files and documentation (both class level, function level and inline) - write questions about parts you do not understand or that are confusing - ask questions --- ## Memory - When a program is executing on a computer, it is given a pool of memory to work with. -- - The program does not need to _worry_ about any other program accessing that memory. (Stay tuned for the operating systems class to learn why/how.) -- - The program organizes its _things_ in two different memory areas: - stack - heap --- ## Stack __Stack__ is where all the local variables and temporary information for functions/methods are stored. - It is organized in a collection of memory blocks, called __stack frames__. Each block belongs to a function/method. The block of a function that is currently executing is on top. Right below it is a block of a function that called the currently executing function, etc. The block for `main` is always at the bottom of the stack. -- name:stack __Example__ --- template:stack .smaller[ .left-column2[ ```Java public class StackExample { public static void main (String [] args ) { foo () ; } public static void foo (){ bar( 15 ); bar( 8 ); } public static void bar ( int x ) { bat(x); } public static void bat ( int x ) { ... } } ``` ] .right-column2[ .center[
before the first method starts the stack is empty ]]] --- template:stack .smaller[ .left-column2[ ```Java public class StackExample { * public static void main (String [] args ) { foo () ; } public static void foo (){ bar( 15 ); bar( 8 ); } public static void bar ( int x ) { bat(x); } public static void bat ( int x ) { ... } } ``` ] .right-column2[ .center[
program starts: `main` is called ]]] --- template:stack .smaller[ .left-column2[ ```Java public class StackExample { * public static void main (String [] args ) { * foo () ; } * public static void foo (){ bar( 15 ); bar( 8 ); } public static void bar ( int x ) { bat(x); } public static void bat ( int x ) { ... } } ``` ] .right-column2[ .center[
`main` calls function `foo` ]]] --- template:stack .smaller[ .left-column2[ ```Java public class StackExample { * public static void main (String [] args ) { * foo () ; } * public static void foo (){ * bar( 15 ); bar( 8 ); } * public static void bar ( int x ) { bat(x); } public static void bat ( int x ) { ... } } ``` ] .right-column2[ .center[
`foo` calls function `bar` passing value of 15 to it ]]] --- template:stack .smaller[ .left-column2[ ```Java public class StackExample { * public static void main (String [] args ) { * foo () ; } * public static void foo (){ * bar( 15 ); bar( 8 ); } * public static void bar ( int x ) { * bat(x); } * public static void bat ( int x ) { ... } } ``` ] .right-column2[ .center[
`bar` calls function `bat` passing its parameter to `bat` ]]] --- template:stack .smaller[ .left-column2[ ```Java public class StackExample { * public static void main (String [] args ) { * foo () ; } * public static void foo (){ * bar( 15 ); bar( 8 ); } * public static void bar ( int x ) { bat(x); } public static void bat ( int x ) { ... } } ``` ] .right-column2[ .center[
`bat` finishes and its stack frame is removed from the stack ]]] --- template:stack .smaller[ .left-column2[ ```Java public class StackExample { * public static void main (String [] args ) { * foo () ; } * public static void foo (){ bar( 15 ); bar( 8 ); } public static void bar ( int x ) { bat(x); } public static void bat ( int x ) { ... } } ``` ] .right-column2[ .center[
`bar` finishes and its stack frame is removed from the stack ]]] --- template:stack .smaller[ .left-column2[ ```Java public class StackExample { * public static void main (String [] args ) { * foo () ; } * public static void foo (){ bar( 15 ); * bar( 8 ); } * public static void bar ( int x ) { bat(x); } public static void bat ( int x ) { ... } } ``` ] .right-column2[ .center[
`foo` calls function `bar` again with 8 as the parameter ]]] --- template:stack .smaller[ .left-column2[ ```Java public class StackExample { * public static void main (String [] args ) { * foo () ; } * public static void foo (){ bar( 15 ); * bar( 8 ); } * public static void bar ( int x ) { * bat(x); } * public static void bat ( int x ) { ... } } ``` ] .right-column2[ .center[
`bar` calls function `bat` passing its parameter to `bat` ]]] --- template:stack .smaller[ .left-column2[ ```Java public class StackExample { * public static void main (String [] args ) { * foo () ; } * public static void foo (){ bar( 15 ); * bar( 8 ); } * public static void bar ( int x ) { bat(x); } public static void bat ( int x ) { ... } } ``` ] .right-column2[ .center[
`bat` finishes and its stack frame is removed from the stack ]]] --- template:stack .smaller[ .left-column2[ ```Java public class StackExample { * public static void main (String [] args ) { * foo () ; } * public static void foo (){ bar( 15 ); bar( 8 ); } public static void bar ( int x ) { bat(x); } public static void bat ( int x ) { ... } } ``` ] .right-column2[ .center[
`bar` finishes and its stack frame is removed from the stack ]]] --- template:stack .smaller[ .left-column2[ ```Java public class StackExample { * public static void main (String [] args ) { foo () ; } public static void foo (){ bar( 15 ); bar( 8 ); } public static void bar ( int x ) { bat(x); } public static void bat ( int x ) { ... } } ``` ] .right-column2[ .center[
`foo` finishes and its stack frame is removed from the stack ]]] --- template:stack .smaller[ .left-column2[ ```Java public class StackExample { public static void main (String [] args ) { foo () ; } public static void foo (){ bar( 15 ); bar( 8 ); } public static void bar ( int x ) { bat(x); } public static void bat ( int x ) { ... } } ``` ] .right-column2[ .center[
`main` finishes and its stack frame is removed from the stack; the program ends ]]] --- name: stack_frame ## Stack Frame Content ### Very Simplified View Each stack frame contains information about local variables that the function creates: -- .left-column2[ - the function arguments - all locally created variables - the return value (if applicable) .smaller[ {{content}} ]] -- Consider this function: ```Java double charStats ( String str, char c ) { double ratio; int count = 0; for (int i = 0; i < str.length(); i++ ) { if (str.charAt(i) == c ) { count++; } } ratio = count / str.length(); return ratio; } ``` How many local variables are there? -- .right-column2[
.small[ The stack frame for `charStats` function should contain memory for five different local variables: - two parameters: `str` and `c` - the `ratio` variable - the `count` variable - the loop counter variable `i` ] ] --- name: heap ## Heap - Whenever the program uses the keyword `new` the memory for that object is allocated on the heap. - Heap is not as organized as the stack. The chunks of memory that are allocated to different arrays and objects can be _all over the place_. (Well, there is some logic in it, but we will not get into it and it is not relevant for our discussions.) -- .center[
] --- template:heap .center[
.smaller[ Again, we do not really care what exactly is the memory address stored in `c`. ]] --- template:heap .center[
.smaller[ And the memory addresses at which `c` and the actual `Circle` objects are locted, do not matter for us either. ]] --- name: array-heap ## Arrays and the Heap Arrays in Java are always stored on the heap in consecutive memory locations. Example: If our program tries to allocate an array of 10 integers, we will need 40 consecutive bytes of memory on the heap (because each `int` needs 4 bytes of memory on current computers). ```Java int [] myArray = new int[10]; ``` -- .center[
] --- template:array-heap .center[
] .center70[ - We'll assume that the memory address of the array is `100` (we'll use decimal numbers instead of hexadecimal numbers for addresses in this example to make things a bit easier ). - This means that the address of the first element is also `100`. ] --- template:array-heap .center[
] .center70[ - The address of an element at index 1 is exactly 4 bytes after the element at index 0 (because arrays are always allocated in consecutive memory locations). - Therefore the address of the element at index 1 is `104` (this is `100` + 1 * 4bytes ). ] --- template:array-heap .center[
] .center70[ - With a bit of arithmetic we can figure out the address of the element at index 5:
.center[ initial_address + index * size_of_int
] or .center[ `100` + 5 * 4 = `120`
] ] --- template:array-heap .center[
] .center[ - What do you think is the address of the last element? ] --- template:array-heap .center[
] --- template:section # Examples and Things to Think About --- ## Example 1: What Happens in Memory Assume that there is a class called `Circle`. It has a public data field called `radius`. It has a one parameter constructor that takes a radius of a circle as its argument and creates a `Circle` object with that radius. ```Java Circle c1 = new Circle (10); Circle c2 = new Circle (20); Circle c3 = c1; System.out.println(c1.radius + " " + c2.radius + " " + c3.radius); c1.radius = 30; System.out.println(c1.radius + " " + c2.radius + " " + c3.radius); c1 = c2; System.out.println(c1.radius + " " + c2.radius + " " + c3.radius); c2.radius = 5; c2 = c3; System.out.println(c1.radius + " " + c2.radius + " " + c3.radius); c1 = c2; c1.radius = 15; System.out.println(c1.radius + " " + c2.radius + " " + c3.radius); ``` What is the output of the above code? --- ## Example 2: What Happens in Memory ```Java Circle [] circles = new Circle[10]; for (int i = 0; i < 10; i++ ) { circles[i] = new Circle(i * 5); } ``` - How is the above array laid out in memory? - Assuming the code fragment is part of a `main` function, what is stored on the stack and what is stored on the heap? - Consider the following code fragment: ```Java circles[2] = circles[8]; circles[8].radius = 1; ``` - how does the memory image change? - what happens to the `Circle` object that used to be stored in `circles[2]`? - how many different `Circle` objects are accessible through the array? --- name:stack_frame_swap ## Think About: `swap` function Given the `swap` function .left-column2[ .smaller[ ```Java void swap ( double d1, double d2 ) { double tmp; tmp = d1; d1 = d2; d2 = tmp; } ``` consider the following code fragment: ```Java double num1 = 3.1415; double num2 = 2.1718; swap ( num1, num2 ); ``` ]] -- .right-column2[ the values of `d1` and `d2` and initialized to the arguments that are passed to the function:
] --- template:stack_frame_swap .right-column2[ the value stored in `d1` is copied to `tmp`:
] --- template:stack_frame_swap .right-column2[ the value stored in `d2` is copied to `d1`:
] --- template:stack_frame_swap name:stack_frame_swap_final .right-column2[ the value stored in `tmp` is copied to `d2`:
] --- template:stack_frame_swap_final __and the swap is completed!__ -- __or is it?__ .smaller[ If we execute ```Java System.out.println(num1 + " " + num2 ); ``` after the call to `swap` function what will the output be? ] --- class: left, middle, title-slide # CSCI-UA 102 ## Data Structures
## Data Structures and Algorithms
(Bird's Eye View) .author[ Instructor: Uday Kusupati
] .license[ Copyright 2020 Joanna Klukowska. Unless noted otherwise all content is released under a
[Creative Commons Attribution-ShareAlike 4.0 International License](https://creativecommons.org/licenses/by-sa/4.0/).] --- template: section # Phone-book Search ## (as an introduction to algorithm performance analysis) --- ## Remember Phone-books? - __Raise your hand if you have ever seen a phone-book.__ -- - __Raise your hand if you have ever used a phone-book.__ -- .center[
.small[
Tomasz Sienicki / [WikimediaCommons](https://commons.wikimedia.org/wiki/File:Telefonbog_ubt-1.JPG) / [CC BY](https://creativecommons.org/licenses/by/3.0)
]
] --- name:Jane-take1 ## Searching For Jane Wong (Take 1) .left-column2[ .pseudocode[ 1. open the phone-book on page one 1. if Jane Wong is on that page - get her number 1. otherwise - flip to the next page - go back to step 2 ] ] --
--- template: Jane-take1
--- template: Jane-take1
--- template: Jane-take1 name:Jane-take1-final
--- template:Jane-take1-final You are most likely going to tell me that this is not a very good algorithm, but let's think about it for a while. -- - Is the algorithm correct? (i.e., will it find Jane Wong in the phone book?) -- __YES__ -- - Is it efficient? (i.e., is this the fastest way of doing it?) -- __NO__ -- - Assuming that there are 1,000 pages in the phone book, how many page turns will it require? --
__can't know for sure, but ~800-900__ (or 1000 if Jane is not listed) -- - How about if there are 10,000 pages in the phone book? How many page turns will be needed? --
__again, can't know for sure, but ~8,000-9,000__ (or 10,000 if Jane is not listed) --- template:Jane-take1-final .important[ When the number of page turns is directly proportional to the total number of pages, we are using a __linear__ algorithm to search for Jane. ] -- More generally, .important[ When the number of operations is directly proportional to the input size (generally denoted as `N`), we are using a __linear__ algorithm. This is often described as an `O(N)` algorithm. ] --- ## Searching For Jane Wong (Take 2) .pseudocode[ 1. open the phone-book on page one 1. if Jane Wong is on that page - get her number 1. otherwise - __flip two pages__ - go back to step 2 ] -- - Is the algorithm correct? (i.e., will it find Jane Wong in the phone book?) -- __NO__ -- But we can fix it taking advantage of the fact that phone books are sorted. --- ## Searching For Jane Wong (Take 2.5) .pseudocode[ 1. open the phone-book on page one 1. if Jane Wong is on that page - get her number 1. otherwise - if the current page contains names _after_ Jane Wong - __go back one page__ - go back to step two - otherwise - __flip two pages__ - go back to step 2 ] -- - Is the algorithm correct? (i.e., will it find Jane Wong in the phone book?) -- __YES__ -- - Is it efficient? (i.e., is this the fastest way of doing it?) --
__NO__ (well, it is twice as fast as the _take 1_ algorithm, but we can do better) -- - The number of page turns required by this algorithm is still directly proportional to the number of pages, so it is still a __linear algorithm__. --- name:Jane-take3 ## Searching For Jane Wong (Take 3) This is the algorithm that most people would follow (well, approximately). .pseudocode[ 1. - 1. open the phone-book to the middle page {{content}} ] -- 1. if Jane Wong is on that page - get her number {{content}} -- 1. otherwise, if the page contains names _after_ Jane Wong - tear the phone book in half - throw out the second half (including the page you just looked at) - go back to step 1 {{content}} -- 1. otherwise, if the page contains names _before_ Jane Wong - tear the phone book in half - throw out the first half (including the page you just looked at) - go back to step 1 -- What is the missing first step? --- ## Searching For Jane Wong (Take 3) This is the algorithm that most people would follow (well, approximately). .pseudocode[ 1. if there is no phone-book left - Jane Wong is not listed, can't get her number 1. open the phone-book to the middle page 1. if Jane Wong is on that page - get her number 1. otherwise, if the page contains names _after_ Jane Wong - tear the phone book in half - throw out the second half (including the page you just looked at) - go back to step 1 1. otherwise, if the page contains names _before_ Jane Wong - tear the phone book in half - throw out the first half (including the page you just looked at) - go back to step 1 ] -- - Is the algorithm correct? (i.e., will it find Jane Wong in the phone book?) -- __YES__ -- - Is it efficient? (i.e., is this the fastest way of doing it?) -- __YES__ (although we won't prove it) -- - Assuming that there are 1,000 pages in the phone book, how many page turns will it require? --
__at most 10__ -- - How about if there are 10,000 pages in the phone book? How many page turns will be needed? --
__at most 14__ --- name:halving ## Power of Halving .left-column2[ The significant performance improvement in this algorithm comes from halving the number of pages that we need to look at after we examine each page. - in _take 1_ and _take 2.5_ the number of pages that we eliminated was 1 or 2 - in _take 3_ the number of pages that we eliminate is equal to the half of the remaining pages ] --
--- template:halving
.left-column2[ .important[ When the search space decreases by half after each comparison, the algorithm is said to be __logarithmic__. It is growing proportionately to the logarithm of the input size `N`. Such an algorithm is said to be `O(log N)`. ]]
--- name:compare-plots ## Comparing Performance of Algorithms --- template:compare-plots .center[
] --- template:compare-plots .center80[.center[
For a fixed value of `N=25`, the time that a logarithmic algorithm takes is much smaller than the time taken by a linear algorithm. ]] -- .center80[.center[ (This may not always be true for very small values of `N`, but it always becomes true as `N` grows.) ]] --- template:compare-plots .center80[.center[
As we double the `N = 50`, the time taken by a linear algorithm doubles, but the time taken by a logarithmic algorithm barely increases. ]] --- template: section # Working with Arrays --- template: slide ## An Array as The First Data Structure An __array__ is probably the first data structure that everybody learns. --- name: array-contiguous ## Constant Access Time to Elements Recall the array image from last class (with memory addresses indicated below): .center80[.center[
]] Because an array occupies contiguous memory locations, __the access to individual elements is instantaneous.__ -- For example, when we execute ``` System.out.println(array[3]); ``` we get the element at index `3` right away. -- It does not matter how big the array is. It also does not matter if we are trying to access an element at index `3` or at index `3000`, the access time is going to be the same. --- template: array-contiguous .important[ When an operation is independent of the number of elements in the data structure, it is said to be __constant__, which is denoted as `O(1)`. ] --- ## Searching in an Array For searching in an array, we can apply similar algorithms as for searching in a phone book: - If the data in our array is __not sorted__, then we will use a __linear search__, `O(N)`. - If the data in our array is __sorted__, then we can use a __binary search__, `O(log N)`. --- name: array-add ## Adding an Element to an Array Now, let's see how elements are added to an array. .center[ {{content}} ] --- template: array-add
Start with an empty 5-element array. --- template: array-add
Add 7 to it. --- template: array-add
Add 7 to it. --- template: array-add
Add 5 to it. --- template: array-add
Add 5 to it. --- template: array-add
Add 6, 2, and 9 to the array. --- template: array-add
Add 6, 2, and 9 to the array. --- template: array-add
Now, let's try to add 1. --- template: array-add
Now, let's try to add 1. Where should it go? --- template: array-add
__We need a bigger array!__ -- .center[ __That means that we need to copy the 5 existing elements to the bigger array, first,
and then add 1 to it. __ ] --- template: array-add
Copy 7 --- template: array-add
Copy 5 --- template: array-add
Copy 6 --- template: array-add
Copy 2 --- template: array-add
Copy 9 --- name: array-add-final template: array-add
__Finally, add the 1!__ --- template: array-add-final
.center[ .huge[Tedious, isn't it?!] ]
--- ## Adding an Element to an Array - The initial add operations were fast. In fact, they were performed in __constant time__. -- - But once the array is _full_ we need to do a lot of work in order to add another element: .pseudocode[ 1. create a new, larger array 1. for each element in the original array - copy it to the new array 1. add the new element to the new array ] -- This is a __linear time__ algorithm, and it may take a lot of time if the original array had many elements. --
.center[.large[Can we do better? ]]
--- name: alternative ## Alternative Memory Layout What if we could put elements anywhere in memory, instead of being restricted to contiguous locations? .center[ {{content}} ] --- template: alternative
But then, how do we know which element is at which _index_?
How do we know what comes before what? --- template: alternative
We need to have a way of somehow _connecting_ the elements? -- .center[ But how can we exactly accomplish these _arrows_ that tell us where the next element is? ] --- template:alternative
Remember that each value has a unique memory address (its location). --- template:alternative
We can use additional memory (a block right next two the actual element)
to keep track of the location of the next element. --- template:alternative
7 is the first element (just like it was in the array).
5 is the second element, so we store the address of 5, right next to the element 7. --- template:alternative
Using our _arrow abstraction_ we show that the new element right
next to 7 points to the memory location that stores 5. --- template:alternative
Then the memory location next to 5 needs to store the memory address of 6 (our next element). --- template:alternative
And we use an arrow to indicate that it points to the memory location that stores 6. --- template:alternative
We do the same for keeping track of where 2 and 9 are located. --- template:alternative
Finally, to indicate that 9 is the last element, the memory location
next to it is set to `0x0` (or `null`) to indicate that it does not point to anything. --- template:alternative
--- template:alternative
When we want to add 1 to our list of numbers, we simply place it in
an available memory location and ... --- template:alternative
When we want to add 1 to our list of numbers, we simply place it in
an available memory location and ...
connect it to the rest of the list by adjusting stored memory addresses. --- ## Alternative Memory Layout What if we could put elements anywhere in memory, instead of being restricted to contiguous locations? .center[
.big[This means that we __do not__ need to copy all the elements
to a new list when we try to add 1. ] ]