A type declaration introduces a new name for a type:
type int = integer; array3 = array[1..3] of integer; matrix = array of array of integer; var i: int; a: array3; m: matrix;
A type declaration conveniently lets us avoid have to type the name
of a long type such as array of array of integer
over
and over again.
Also, type declarations are sometimes necessary if we wish to pass certain parameter types to functions, as we will see in the next section.
We will often want to pass an array to a function. We discussed this topic briefly last week, but will now take a closer look at it.
Suppose that we want to pass a static array or multidimensional array to a function. Surprisingly, the following declarations will fail to compile:
function sum(a: array[1..5] of integer): integer; // DOES NOT COMPILE function sum(a: array of array of integer): integer; // DOES NOT COMPILE
An array parameter to a function or procedure in Pascal must be either of a named type, or must be an open array.
To use a named type, simply declare a type name as described in the previous section:
type array5 = array[1..5] of integer; matrix = array of array of integer;
And now you can declare functions taking arrays of these types:
function sum(a: array5): integer; function sum(a: matrix): integer;
Alternately, a function or procedure parameter can have an array type with no bounds:
function sum(a: array of integer): integer;
This looks like a dynamic array type, but in this context this is an open array parameter. You can pass either a static or dynamic array to a function that expects an open array.
Just like dynamic arrays, open arrays are always indexed starting from 0, even if their source array has a different indexing base! For example:
procedure first(a: array of integer); begin writeln('low = ', low(a), ', high = ', high(a), ', first = ', a[0]); end; var a: array[1..5] of integer = (2, 4, 6, 8, 10); begin first(a); ...
This program will print
low = 0, high = 4, first = 2
This behavior might seem odd, but arguably it is useful, because you can write a function that operates on an open array without worrying about the indexing base of the array that is passed. For example:
function sum(a: array of integer): integer; var s: integer = 0; i: integer; begin for i := 0 to high(a) do s := s + a[i]; exit(s); end; var b: array[7..10] of integer = (1, 2, 3, 4); begin writeln(sum(b)); end.
This program prints 10. The function sum
works fine on
the array b
, even though b
is indexed from
7.
As we discussed in the previous lecture, Pascal passes arguments
either by value, which is the default, or by reference,
when a parameter is preceded with the var
keyword. When
an array is passed by value, the entire array is copied.
Consider again the sum
function from the previous
section. Its declaration looks like this:
function sum(a: array of integer): integer;
In this declaration, should we write var
before the
parameter name a
? This is a bit of a dilemma. If we
don't write var
, the array is copied on every call. This
is wasteful and is potentially expensive if the input array is large.
But writing var
seems odd, since the function does not
need to modify the input array.
We can resolve the dilemma by using the const
keyword:
function sum(const a: array of integer): integer;
When we pass an array with const
, it is not copied, and
the function cannot modify the array. That is exactly what we want in
this situation.
As a rule of thumb, when we pass an array to a function in Pascal,
we will almost always use either the var
keyword (if we
want the function to modify the array) or the const
keyword (if the function will only read the array). Without these
keywords, the array will be copied on every call, which is a behavior
we will want only rarely.
Last week we learned how to sort an array using the bubble sort algorithm. After sorting an array a of length n, all elements are in order:
a[0] ≤ a[1] ≤ a[2] ≤ … ≤ a[n - 1]
We may wish to search for an integer (sometimes called the “key”) in a sorted array. Here is a naive sequential search:
function contains(const a: array of integer; key: integer): boolean; var v: integer; begin for v in a do if v = key then exit(true); exit(false); end;
Unfortunately this can be inefficient if the input array is large. We can use an algorithm called binary search to find the key much more efficiently.
Binary search is related to the number-guessing game that we saw in an earlier lecture. Recall that in that game, one player thinks of a number N, say between 1 and 1,000,000. If we are trying to guess it, we can first ask "Is N greater than, less than, or equal to 500,000?" If we learn that N is greater, then we now know that N is between 500,001 and 1,000,000. In our next guess we can ask to compare N to 750,000. By always guessing at the midpoint of the unknown interval, we can divide the interval in half at each step and find N in a small number of guesses.
Similarly, suppose that we are searching for the key in a sorted array of 1,000,000 elements from a[0] through a[999,999]. We can first compare the key to a[500,000]. If it is not equal, then we know which half of the array contains the key. We can repeat this process until the key is found.
To implement this in Pascal code, we will use two integer variables lo and hi that keep track of the current unknown range. Initially lo = -1 and hi = length(a). At all times, we know that all array elements with indices <= lo are less than the key, and all elements with indices >= hi are greater than the key. That means that the key, if it exists in the array, must be in the range a[lo + 1] … a[hi – 1]. As the binary search progresses, lo and hi move toward each other. If eventually hi = lo + 1, then the unknown range is empty. This means that the key must not exist in the array, and we can return false.
Here is a binary search in Pascal:
function contains(const a: array of integer; key: integer): boolean; var lo, hi, mid: integer; begin lo := -1; hi := length(a); while hi – lo > 1 do begin mid := (lo + hi) div 2; if a[mid] = key then exit(true); if a[mid] < key then lo := mid; else // a[mid] > key hi := mid; end; exit(false); // not found end;
Suppose that the input array has 1,000,000,000 elements. How many times might this loop iterate?
After the first iteration, the unknown interval (hi – lo) has approximately 500,000,000 elements. After two iterations, it has 250,000,000 elements, and so on. After k iterations it has 1,000,000,000 / 2k elements. We reach the end of the loop when 1,000,000,000 / 2k = 1, so k = log2(1,000,000,000).
We learned in the first lecture that 230 = 1 G ≈ 1,000,000,000. So log2(1,000,000,000) ≈ 30. This means that the loop will iterate only about 30 times, which will take only a few microseconds at most.
We would now like to consider how long our programs or functions take to run as a function of their input size.
As a first example, consider this function, which adds the integers from 1 to n:
function sum(n: integer): integer; var s: integer = 0; i: integer; begin for i := 1 to n do s += i; exit(s); end;
Here the input size is n. The function will run quickly when n is small, but as n increases the function's running time will certainly increase. How can we quantify this?
Here is the same function again, with comments indicating how many times each line in the function will run:
function sum(n: integer): integer; var s: integer = 0; // 1 i: integer; begin for i := 1 to n do s += i; // n exit(s); // 1 end;
Suppose that the first commented statement above runs in time a, the second runs in time b and the third runs in time c, where a, b, and c are constants representing numbers of nanoseconds. Then the total running time is b n + (a + c).
In theoretical computer science we don't care at all about the constant factors in an expression such as this. They are dependent on the particular machine on which a program runs, and may not even be so easy to determine. We also don't care about the constant term (a + c). That's because as n grows, the constant term is dominated by the linear term b n, which becomes larger by an arbitrary factor. To be more precise, limn→∞ [b n / (a + c)] = ∞.
So we write:
running time = b n + (a + c) = O(n)
Here, the O is sometimes pronounced "big O". We could give a formal mathematical definition of big O, but informally it looks like the form of an expression after discarding all constant factors and lower-order terms which are dominated by the highest-order term. For example:
3 n + 4 = O(n)
5 n2 + 6 n + 4 = O(n2)
2 n + log(n) = O(n)
2n + 5 n4 + n = O(2n)
To know which lower-order terms we can discard we must know which functions grow faster than others. Here is a table of common growth rates in order from smallest to largest:
notation |
name |
---|---|
O(1) |
constant |
O(log n) |
logarithmic |
O(n) |
linear |
O(n log n) |
log linear |
O(n2) |
quadratic |
O(n3) |
cubic |
O(nk) |
polynomial |
O(kn) |
exponential |
O(n!) |
factorial |
Let's consider the running time of various algorithms that we have already studied in this course.
First let's look at primality testing using trial division, which we saw a couple of lectures ago:
function isPrime(n: integer): boolean; var i: integer; begin for i := 2 to trunc(sqrt(n)) do if n mod i = 0 then exit(false); exit(true); end;
Notice that for this algorithm some values of n are harder than others. For example, if n is even then the function will exit immediately, even if n is very large. On the other hand, if n is prime then the function will need to iterate all the way up to sqrt(n) in order to determine its primality.
In general we are interested in determining an algorithm's best-case and worst-case running times as the input size n increases. For some algorithms these will be the same, and for others they will be different. The best-case running time is a lower bound on how long it might take the algorithm to run, and the worst-case running time is an upper bound.
Let's annotate the function above with the number of times each statement might run:
function isPrime(n: integer): boolean; var i: integer; begin for i := 2 to trunc(sqrt(n)) do if n mod i = 0 then // min = 1, max = sqrt(n) - 1 exit(false); // 1 exit(true); // 1 end;
The statement 'if n mod i = 0
' might run only once, or
as many as sqrt(n) – 1 times. This function runs in O(1) in the
best case, and in O(sqrt(n)) in the worst case.
Now let's consider a bubble sort:
procedure bubbleSort(var a: array of integer); var i, j: integer; begin for i := high(a) - 1 downto 0 do for j := 0 to i do if a[j] > a[j + 1] then swap(a[j], a[j + 1]); end;
Analyzing this procedure's running time takes a bit more thought. Let
N be the length of the input array a; this is the input size. On the
first iteration of the outer for loop, i = high(a) – 1 = N – 2.
So j iterates from 0 to N – 2, a total of (N – 1) different
values. That means that the if
statement runs (N – 1)
times.
On the second iteration of the outer loop, i = N – 3. So j
iterates from 0 to N – 3, a total of (N – 2) different values,
and the if
statement runs (N – 2) times.
And so on. On the last outer loop iteration, the if
statement runs only once. The total number of times that the if
statement runs is
(N – 1) + (N – 2) + … + 2 + 1 = N (N – 1) / 2 = N2 / 2 – N / 2 = O(N2).
In the worst case, the if
condition always evaluates
to true and we call swap every time, so we perform O(N2)
swaps. Note that the time for any individual swap is constant. So the
total running time is
O(N2) (ifs) + O(N2) (swaps) = O(N2).
In the best case, the if
condition never evaluates to
true and we perform no swaps. Then the running time is
O(N2) (ifs) + 0 (swaps) = O(N2).
So bubble sort runs in O(N2) in both the best and worst cases. This doesn't mean that the function's running time is constant for any given N – in the worst case it will take longer. But asymptotically the best and worst cases are the same. In other words, they differ only by a constant factor.
By the way, an experienced programmer would not need to perform algebra to determine the running time of the procedure above. They could glance at it and immediately know that it is O(N2). That's because it has a double loop that fits this important pattern:
1 + 2 + 3 + … + N = O(N2)
In general, any double loop in which the inner loop first iterates to 1, then to 2, and so on up to N will be quadratic. Also, adding or subtracting a constant from the loop bounds will not generally affect the running time. For example, if the procedure above if we replace the line
for j := 0 to i do
with
for j := 1 to i do
or
for j := 0 to (i - 1) do
then the running time will still certainly be O(N2).
Finally let's determine the running time of a binary search for a value in a sorted array. (We just learned this algorithm; see the section "binary search", above). In the best case, the binary search immediately finds the value that we are seeking. So the best-case running time is O(1). As we observed above, in the worst case the binary search must perform log2(N) iterations, where N is the size of the input array. So the worst-case time is O(log2 N).
Actually we don't need to write the base 2 in O(log2 N). That's because O(log2 N) = O(logkN) for any k > 0. To see why, recall that
logab · logbc = logac for any a, b, c > 0
Thus, for example,
log210 · log10N = log2N
So we see that log10N and log2N differ by only a constant factor. The same is true for any two logarithmic bases. Because big-O ignores constant factors, we can omit the base and just write
O(log2N) = O(log N)
Let's summarize the best-case and worst-case running time for these algorithms:
algorithm |
best case |
worst case |
---|---|---|
primality testing |
O(1) |
O(sqrt(N)) |
bubble sort |
O(N2) |
O(N2) |
binary search for element |
O(1) |
O(log N) |
We leaned about the div and mod operators in our first lecture. Up to now we have only been using them with positive values. Let's now investigate the value of (a div b) when a is negative.
Here's a program to print out (i div 3) and (i mod 3) for various values of i:
var i: integer; begin writeln(' i (i div 3) (i mod 3)'); writeln('----------------------------'); for i := 6 downto -6 do writeln(i:2, (i div 3):8, (i mod 3):8); end.
The program prints this output:
i (i div 3) (i mod 3) ---------------------------- 6 2 0 5 1 2 4 1 1 3 1 0 2 0 2 1 0 1 0 0 0 -1 0 -1 -2 0 -2 -3 -1 0 -4 -1 -1 -5 -1 -2 -6 -2 0
Notice that if i is negative, then (i mod 3) is either negative or zero. Also notice the symmetry about i = 0. Specifically, (-i div 3) = -(i div 3) and (-i mod 3) = -(i mod 3).
In some programs it is inconvenient that mod can return a negative
value. Consider this example. We can represent an hour of the day
using an integer between 0 and 23. For example, the integer 14
represents 14:00. We'd like to write a function addHours
that adds a positive or negative number of hours to an hour of the
day. For example,
addHours(14, 2) = 16
addHours(23, 2) = 1 (i.e 23:00 plus 2 hours = 1:00)
addHours(1, -2) = 23 (i.e. 1:00 minus 2 hours = 23:00)
This implementation is wrong:
function addHours(h: integer; d: integer): integer; begin exit((h + d) mod 24); end;
It will return the correct answer in the first two examples above. But it will return addHours(1, -2) = -1, since -1 mod 24 = -1.
We want to ensure that the value we return is in the range 0..23. The easiest way to do that is to simply add 24 if the mod operator returns a negative value:
function addHours(h: integer; d: integer): integer; var i: integer; begin i := (h + d) mod 24; if i < 0 then i := i + 24; exit(i); end;
This is a useful trick to know, and we will use it from time to time.
In an earlier lecture we learned how to split a number into decimal digits from right to left. (See the section "breaking a number into digits" in the notes for lecture 3.)
Sometime we would instead like to iterate over digits from left to right. This is not much more difficult. Suppose that N = 247,638 and we want to split it into digits. We first need to calculate the highest power of 10 that is less than or equal to N. Call this value d. Now, in a loop, we can repeatedly call (N div d) to retrieve a digit, then (N mod d) to remove that digit from N. Then we divide d by 10 and proceed with the next digit.
Here is a Pascal function that implements this idea to add all the digits in a number, working from left to right:
function digitSum(n: integer): integer; var d: integer; sum: integer = 0; begin // Compute the highest power of 10 that is <= n. d := 1; while d * 10 <= n do d := d * 10; // Fetch digits from n. while d > 0 do begin sum := sum + (n div d); n := n mod d; d := d div 10; end; exit(sum); end;