Lecture 4: Notes

for...in

The for…in statement loops over successive values of an array:

var
  a: array[1..100] of integer;
  sum: integer = 0;

for v in a do
  sum := sum + v;

The preceding for loop is equivalent to

for i := low(a) to high(i) do
  sum := sum + a[i];

records

A record is a compound object that is composed of a set of fields. Each distinct record type defines the names and types of the fields that it contains. For example:

type
  book = record
    title: string;
    author: string;
    pages: integer;
  end;

You can access a record's fields using the '.' operator. For example:

var
  b: book;

begin
  b.title := 'war and peace';
  b.author := 'leo tolstoy';
  b.pages := 1440;
  
  writeln('title = ', b.title);

You can initialize a variable of any record type as you declare it:

var
  b: book = (title: 'war and peace'; author: 'leo tolstoy'; pages: 1440);

random cards

Here is a program that uses a record type to represent a playing card:

card = record
    rank: 2 .. 14;  // 11 = jack, 12 = queen, 13 = king, 14 = ace
    suit: suit;
  end;

The program uses an array of card to hold a hand of cards. It draws a random 5-card hand and checks whether it is a flush, i.e. whether all suits in the hand are identical.

{$r+}

uses math, sysutils;

type 
  suit = (club, diamond, heart, spade);

  card = record
    rank: 2 .. 14;  // 11 = jack, 12 = queen, 13 = king, 14 = ace
    suit: suit;
  end;

  hand = array of card;

function describe(c: card): string;
var
  ranks: array[11..14] of char = ('J', 'Q', 'K', 'A');
  suits: array[suit] of string = ('♣', '♦', '♥', '♠');
  s: string;

begin
  if c.rank <= 10 then
    s := intToStr(c.rank)
  else s := ranks[c.rank];
  describe := s + suits[c.suit];
end;

function randomCard(): card;
var 
  c: card;
begin
  c.rank := random(13) + 2;
  c.suit := suit(random(4));
  randomCard := c;
end;

function isIn(c: card; h: hand): boolean;
var
  i: integer;
begin
  for i := low(h) to high(h) do
    if (c.rank = h[i].rank) and (c.suit = h[i].suit)
      then exit(true);
  exit(false);
end;

function draw(n: integer): hand;
var
  h: hand;
  i: integer;
  c: card;
begin
  setLength(h, 0);
  for i := 1 to n do
    begin
      repeat
        c := randomCard();
      until not isIn(c, h);
      setLength(h, i);
      h[i - 1] := c;
    end;
  draw := h;
end;

function isFlush(h: hand): boolean;
var
  i: integer;
begin
  for i := low(h) to high(h) - 1 do
    if h[i].suit <> h[i + 1].suit then
      exit(false);
  exit(true);
end;

var
  h: hand;
  i: integer;

begin
  randomize;
  h := draw(5);
  for i := 0 to high(h) do
    write(describe(h[i]), '  ');
  writeln;
  writeln(isFlush(h));
end.

files

In general, files hold either text or binary data. In a text file, all information is stored as Unicode characters. For example, an integer in a text file is stored as a series of characters representing decimal digits. In a binary file, an integer is stored as a series of bytes, each of which holds 8 consecutive bits of the integer’s binary representation.

text files

The text type represents a text file:

var
  novel: text;

Call the assign function to assign a filename to a file. You can then call reset to open the file for reading, or rewrite or append to open the file for writing. Be warned that rewrite will truncate the file, i.e. erase all its existing contents, if it already exists! Call close to close the file when you are done reading or writing.

begin
  assign(novel, great_american);
  rewrite(novel);
  writeln(novel, It was a dark and stormy night.);
  writeln(novel, The wind howled through the treetops, and there was no moon in sight.);
  close(novel);

The read, readln, write and writeln functions work with text files just as they do for standard input and output.

binary files

The file type represents a file on disk containing a particular type of binary data:

var
  f: file of integer;
  g: file of real;

Often a file will hold a compound type:

type
  time = record
    year, month, day: integer;
    hour, minute, second: integer;
  end;

var
  timestamps: file of time;

Note, however, that a file cannot contain data of any of these types:

If you want to store character data in a binary file, you must use a fixed-size character array, e.g. an array[1..100] of char.

The assign, reset, rewrite, append and close functions work with binary files just like with text files. The read and write functions also work with binary files, but you can only read or write values of the binary file’s element type. Furthermore, readln and writeln don’t work with binary files since there are no newlines in such files.

writing to and reading from a binary file

Here is a program that simulates a random walk and writes a series of points to a binary file.

{$mode delphi}

type
  point = record
    x: integer;
    y: integer;
  end;

var
  f: file of point;
  n, i: integer;
  p: point;

begin
  randomize;
  write('iterations? ');
  readln(n);

  assign(f, 'walk_data');
  rewrite(f);
  
  p.x := 0;
  p.y := 0;
  
  for i := 1 to n do
    begin
      write(f, p);
      case random(4) of
        0: p.x := p.x - 1;
        1: p.x := p.x + 1;
        2: p.y := p.y - 1;
        3: p.y := p.y + 1;
      end;
    end;
  
  close(f);
end.

Here is a complementary program that reads the data written by the previous program, and prints out some statistics about the random walk.

{$mode delphi}

uses math;

type
  point = record
    x: integer;
    y: integer;
  end;

var
  f: file of point;
  time: integer = 0;
  p: point;
  distance: integer;
  furthest: integer = 0;
  furthest_time: integer = 0;

begin
  assign(f, 'walk_data');
  reset(f);
  
  while not eof(f) do
    begin
      read(f, p);
      time := time + 1;
      if (p.x = 0) and (p.y = 0) then
        writeln('hit origin at time ', time);
      distance := abs(p.x) + abs(p.y);
      if distance > furthest then
        begin
          furthest := distance;
          furthest_time := time;
        end;
    end;
  close(f);
  
  writeln('total time = ', time);
  writeln('furthest distance = ', furthest, ' at time ', furthest_time);
end.

analyzing a program’s running time

We are generally interested in characterizing a program or function’s running time as a function of its input size n.

Consider the following function, which returns the largest column sum in an (n x n) matrix.

uses math;

type matrix = array of array of integer;

// Return the largest column sum in an (n x n) matrix.
function largestColumnSum(a: matrix; n: integer): integer;
var 
  sum, largest, row, col: integer;

begin
  largest := - MaxInt;              // c1

  for col := 0 to n - 1 do
    begin
      sum := 0;                     // c2
      for row := 0 to n - 1 do
        sum := sum + a[row, col];   // c3
      largest := max(largest, sum); // c4
    end;

  exit(largest);                    // c5
end;

For the statements labelled c1 through c5, let c1, …c5 be the time costs of running each of these statements once. We assume that the ci are constant.

For a given value of n,

So the total running time of these lines is c3n2 + (c2 + c4)n + (c1 + c5). This equals an2 + bn + d for constants a, b, d.

Other statements in this function (e.g. the for loops themselves) will also each execute either n2 times, n times or 1 time. When we include their overhead, the constants a, b and d may change, but the function’s running time remains of the form an2 + bn + d.

asymptotic notation

In computer science, we generally ignore the constant factors and lower-order terms in a running time such as an2 + bn + d, and write it instead using big-O notation: an2 + bn + d = O(n^2). This indicates the order of growth of the running time, also known as the time complexity of the function or program.

f(n) = O(g(n)) means that f(n) is eventually bounded by some constant factor times g(n). We say that g(n) is an asymptotic upper bound for f(n).

More formally:

Definition. f(n) = O(g(n)) if there exist constants c, n0 > 0 such that 0 ≤ f(n) ≤ cg(n) whenever nn0 .

For given functions f and g, it can be awkward to show that f(n) = O(g(n)) using the definition above. It is often much easier to work with limits, so the following theorem is useful.

Definition. A function f is eventually positive if there exists some n0 such that f(n) > 0 whenever n > n0 .

Theorem. If functions f and g are eventually positive, then f(n) = O(g(n)) iff `underset(n→oo)("lim sup ") f(n)/g(n) < oo`.

For any function h, if `lim_(n→oo) h(n) = k`, then `underset(n→oo)("lim sup ") h(n)` must equal k as well. Combining this observation with the preceding theorem, we see that we can prove that f(n) = O(g(n)) by showing that f(n) / g(n) converges to a constant.

For example, let us show that an2 + bn + d = O(n2) as stated above. We have

`lim_(n→oo)((a n^2 + b n + d) / n^2) = lim_(n→oo)(a + b / n + d / n^2) = a + lim_(n→oo)(b / n + d / n^2) = a`

common time complexities

The functions below are in increasing order of asymptotic complexity.

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

factoring integers

The Fundamental Theorem of Arithmetic states that each integer has a unique factorization as a product of prime numbers.

The simplest algorithm for factoring an integer is called trial division, in which we attempt to divide an integer n by successive integers i < n. Here is a program that performs naive trial division:

{$mode delphi}

var
  n: integer;
  i: integer;

begin
  write('number to factor: ');
  readln(n);
  
  i := 2;
  while i < n do
    if n mod i = 0 then
      begin
        write(i, ' ');
        n := n div i;
      end
    else
      i := i + 1;
    
  writeln(n);
end.

This program works, but runs in time O(n) which is very inefficient for large n. We can make a couple of small changes to the program to make it more efficient:

{$mode delphi}

var
  n: integer;
  i: integer;

begin
  write('number to factor: ');
  readln(n);
  
  i := 2;
  while i <= sqrt(n) do
    if n mod i = 0 then
      begin
        write(i, ' ');
        n := n div i;
      end
    else
      if i = 2 then
        i := 3
      else i := i + 2;
    
  writeln(n);
end.

The most important change here is that we loop only while i `sqrt(n)`. To see that this is sound, consider the following loop invariant: No prime factors of n are less than i.

The invariant is certainly true at the beginning of the loop, since there are no primes less than 2. In each loop iteration, if i | n then we replace n by n / i, which must also have no prime factors less than i. If i does not divide n, then we advance i to the next odd integer, and n still has no prime factors that are less than i.

So the loop preserves the invariant. When the loop terminates, i > `sqrt(n)`. Since n is not divisible by any j < i, it must also not be divisible by any ji > `sqrt(n)`, for then n would be divisible by n / j < `sqrt(n)`, a contradiction. So the final value of n must be prime.

This modified algorithm runs in time O(`sqrt(n)`). The modification to advance i only through odd integers makes the algorithm run twice as fast (which does not change its time complexity).