Notes 8

Arrays, Searching, Recursion and Strings

Arrays

Introduction

Arrays are the first structured type that we have seen. It is defined as a contigious sequence of elements of the same size. The type of these elements is know as the base type. Being of the same size means they are of the same type. The objects can be of any type as long as they are all the same. The size of an array is fixed at compile time. Some languages, like C and BASIC, give the illusion of variable sized arrays but in reality, the values are copied from the old array to a new larger one. The old one is not extended. Since the elements are contigious, we can directly calculate the address of any elements given three pieces of information.

Addressing

We need the size of the elements, the address of the first element and the number of the element in the array. The expression for doing this is:
      initial_address + (element#) * size_of_base_type
For example, if we had an array of integers (each two bytes long) that started at address 15, the location in memory (the address) of element 10 is
        15 + (10) * 2  =  35
Languages like Pascal number the elements from 1 so the above has to be changed to use element#-1. This can be extended to work for languages (like Pascal) that allow ranges of array indices. You can convert to 1..N by subtracting the beginning of the range from the index, this gives a value from 0.... Subtracting the beginning of the range from the end gives the last element relative to 0. The expression for this is:
   initial_address + (element# - initial_element#) * size_of_base_type
Let's do the same calculation as above, only the array is declared from 5 to 38. Now the location of element 10 is
        15 + (10 - 5) * 2  =  25

Multi-dimensional Arrays

Multi-dimensional arrays are stored as sequences of one dimensional arrays. An NxM array has N rows and M columns. Either each row is stored (row major) or each column (column major). The calculation above is extended again to first find the row we want by changing element# to row# and size_of_object to size_of_row. This gives us the initial address of the row we want and we use exactly the first version of the expression to calculate the column address. First the row address
   initial_address + (row# - initial_row#) * size_of_row
Then find the column within that row
   row_address + (element# - initial_element#) * size_of_base_type
Arrays are characterized by fast random access. Any element on the array can be found as fast as any other. They are however, limited to the size originally allocated and all the elements must be the same size. It is also difficult to insert or delete elements
 

Searching


To find something in an array, we have to look at each element in the array and see if it is the one we want. Here I will first go over some different means to repeat things. That is, there are several techniques for looping in a program. If we have an array of 5 elements called foo that looks like this:
 

0
1
2
3
4
a
b
c
d
e
And we are searching for the element that has a 'c' in it, we could do the following 5 statements.
if(foo[0] == 'c') print "found it"
if(foo[1] == 'c') print "found it"
if(foo[2] == 'c') print "found it"
if(foo[3] == 'c') print "found it"
if(foo[4] == 'c') print "found it"
Notice that the only difference between these statements is the number in the square brackets. We would like to not have to repeat this over and over, esppecially if the array has hundreds of elements. There are three basic kinds of loops, the for, while and until loops. Here is a search routine that uses a for loop.
for(i=0;i<5;i++) {
   if(foo[i] == 'c') print "found it"
}
There are three parts to the for statement. The first part is the initialization. Here it sets i to 0. The second part is the termination check. The loop ends when this expression is false. Here, if i is greater than or equal to 5, the loop ends. Lastly is the increment. Here, we add 1 to i each time to do the loop again. In this loop, i will get the values 0,1,2,3,4 and each time it will check the value against the letter 'c'. This loop is equivalent to the 5 statements above.

The while loop is best used when the loop conditions aren't a range of numbers.
 

i=0
while(i<5) {
   if(foo[i] == 'c') print "found it"
   i=i+1
}


This is pretty much the same as  the other loop. But it can be written as

i=0
while(i<5 and foo[i] != 'c')  i=i+1
if(foo[i] == 'c') print "found it"


The until loop is like the while loop except that the check is doen at the bottom of the loop so the loop always runs at least once.

i=0
do {
   if(foo[i] == 'c') print "found it"
} until (i >= 5)


These are all linear searches. That is, we look at each element in turn until we find what we are looking for. Also, all these loops look at all elements, even after the find the one we are looking for. This is good if there are multiple matches but some times we want to quit when we find it. So we use the break statement. This causes the loop to end immediatly.
for(i=0;i<5;i++) {
   if(foo[i] == 'c') {
      print "found it"
      break
   }
}

Recursion


But notice that the data in the array is ordered. That is, it is in alphabetical order. We can take advantage of that and make the searching faster. We can do a binary search. This method involves breaking the problem in half repeatedly until we find the one we are looking for.
First a little about functions. A function or subroutine is a small program that is usually part of a larger program. It is best if the function is written to do just one thing. Often a function returns a value. In the main program, the function is executed when it is called. Functions also can take arguments.
The definition of a function looks like
foo(x,y) {
    if(x>y) return(x)
    return(y)
}
foo is the function name. The x and y are called the formal parameters. The return statement is used to send a value back to the main program. The function is called from the main program by using its name.
For example
                   x=foo(2,3)
When it used like this, the program that makes up the function is executed. If it executed a return statement, then the value in the return statement is assigned to the variable x.  The numbers 2 and 3 are called the actual parameters of the function.Within the function, that formal parameter x is set to 2 and the parameter y is set to 3. Then the code is executed. In this case, 2 is not greater than 3. So the return(y) line is executed. In this case, that is the same as return(3). Then the 3 is copied to the variable x.

Now lets look at a small function that calculates the factorial of a number. The factorial is  the multiplication of all the integers starting with the number and going down to 1. So, the the factorial of 4 is 4 * 3 * 2 * 1 = 24. This is written as 4!. Another way to think of the problem is to notice that n! = n*(n-1)!. Also that 1! = 1.
One method to calculate this is

fact(n) {
    if(n <= 1) return(1)  // 1!
    x=fact(n-1) //  (n-1)!
    return(n*x)  // n* (n-1)!
}

To understand how this works, you need to know how the system processes function calls. When the function starts, a block of memory is allocated called a stack frame. This is where we store the values of the formal parameters and any other variables that are used in the function. In this case, we have x.  So this block can be written as a list of  2 values. At the start, this is (4,?). The question mark is because x is undefined at the start.
So lets step through the execution of z=fact(4).
 
 
Code Line/ Notes stack frame
fact(4)  (4,?)
if(n <= 1) return(1)    False  (4,?)
x=fact(n-1)  fact(3)  new block started  (4,?)
   fact(3)  (4,?) (3,?)
   if(n <= 1) return(1)    False  (4,?) (3,?)
   x=fact(n-1)  fact(2) new block started  (4,?)(3,?)(2,?)
      if(n <= 1) return(1)    False  (4,?)(3,?)(2,?)
      x=fact(n-1)  fact(1) new block started  (4,?)(3,?)(2,?)(1,?)
         if(n <= 1) return(1)    True (4,?)(3,?)(2,?)(1,?)
      x=1 (4,?)(3,?)(2,1)
      return(n*x)     return(2*1) (4,?)(3,?)(2,1)
   x=2 (4,?)(3,2)
   return(n*x)   return(3*2) (4,?)(3,2)
x=6 (4,6)
return(n*x)   return(4*6) (4,6)
z=24

function bsearch(A, start, end, target)
{
        mid = (start + end) / 2  // if there is  a fraction, chop it. 2.5 becomes 2
        if(A[mid] == target) return(mid)   // we found it the first time
        if(A[mid] < target) {
           z=bsearch(A,start,mid-1,target)   // look in the left half
           return(z)
        }
        else {
           z=bsearch(A,mid+1,end,target)   // look in the right half
           return(z)
        }
}

The key thing that makes this work is that the array is sorted. If we look at a particular element in  the array, say element 2 and we are looking for 'b', we first check if foo[2] is equal to 'b'. It isn't. But foo[2] is 'c'. And since 'b' is less than 'c', we know that if 'b' is in the array at all, it must be to the left of 'c'. So we search the left half of the array from 0 to (2-1) or 1. Then we start over again. This time, start = 0 and end = 1. Mid = (1+0)/2 = 0. foo[0] != 'b'. So we call the bsearch routine again on the right half of the array we are looking at. This time, start = 1, end = 1. So mid = (1+1)/2 = 1. Since foo[1] = 'b', we are done. There are several levels of returns and the calling program gets a 1 back.

This technique is called recursion. It means that the program calls itself.
 

Strings

Introduction

A string is a sequence of characters. There is an order. In English it is left to right. They are really defined by a set of operations: How can we represent this in an array? Two choices are a null at end and a count in the beginning. Also set bit 7 in last byte high, initialize all locations to null

Tradeoffs

These have to do with the null-terminated array version of strings string is a type made up of an array[MAXSTR] of char.

1 procedure writestring(str)
// print the contents of str
2 i = 0
3 while (str[i] != 0) do
4 print(str[i]); i = i + 1
5 end while
6 end writestring

1 function length(str)
// returns the number of non-null characters in str
2 i = 0
3 while (str[i] != 0) do
4 i = i + 1
5 end while
6 return(i)
7 end length

1 function findchar(c,str)
// returns the index to the character c in the
// string str or -1 if it is not found
2 i = 0
3 while (str[i] != 0) and (str[i] != c) do i = i + 1 endwhile
4 if (str[i] = 0) then
5 return(-1) // didn't find it
6 else
7 return(i) // found it
8 end findchar

Write a routine using the count idea.
1 procedure writestring(str)
// print the contents of str
3 for(i=1;i<= str[0];i++)
4 print(str[i]);
6 end writestring

1 function length(str)
/* returns the number of characters in str */
2 return(str[0])
3 end length
 

.