Vous êtes sur la page 1sur 231

Ye CS13002 Programming and Data Structures

Spring semester

Introduction
What is a digital computer?
A computer is a machine that can perform computation. It is difficult to give a precise definition of computation. Intuitively, a computation involves the following three components:

Input: The user gives a set of input data. Processing: The input data is processed by a well-defined and finite sequence of steps. Output: Some data available from the processing step are output to the user.

Usually, computations are carried out to solve some meaningful and useful problems. One supplies some input instances for the problem, which are then analyzed in order to obtain the answers for the instances.

Types of problems
1. Functional problems A set of arguments a1,a2,...,an constitute the input. Some function f(a1,a2,...,an) of the arguments is calculated and output to the user. 2. Decision problems These form a special class of functional problems whose outputs are "yes" and "no" (or "true" and "false", or "1" and "0", etc). 3. Search problems Given an input object, one tries to locate some particular configuration pertaining to the object and outputs the located configuration, or "failure" if no configuration can be located. 4. Optimization problems

Given an object, a configuration and a criterion for goodness, one finds and reports the configuration pertaining to the object, that is best with respect to the goodness criterion. If no such configuration is found, "failure" is to be reported.

Specific examples
1. Polynomial root finding Category: Functional problem Input: A polynomial with real coefficients Output: One (or all) real roots of the input polynomial Processing: Usually, one involves a numerical method (like the Newton-Raphson method) for computing the real roots of a polynomial. 2. Matrix inversion Category: Functional problem Input: A square matrix with rational entries Output: The inverse of the input matrix if it is invertible, or "failure" Processing: Gaussian elimination is a widely used method for matrix inversion. Other techniques may also be conceived of. 3. Primality testing Category: Decision problem Input: A positive integer Output: The decision whether the input integer is prime or not Processing: For checking the primality of n, it is an obvious strategy to divide n by integers between 2 and square root of n. If a divisor of n is found, n is declared "composite" ("no"), else n is declared "prime" ("yes"). This obvious strategy is, however, very slow. More practical primality testing algorithms are available. The first known (theoretically) fast algorithm is due to three Indians (Agarwal, Kayal and Saxena) from IIT Kanpur. 4. Traveling salesman problem (TSP) Category: Optimization problem Input: A set of cities, the cost of traveling between each pair of cities, and the criterion of cost minimization Output: A route through all the cities with each city visited only once and with the total cost of travel as small as possible

Processing: Since the total number of feasible routes for n cities is n!, a finite quantity, checking all routes to find the minimum is definitely a strategy to solve the TSP. However, n! grows very rapidly with n, and this brute-force search is impractical. We do not know efficient solutions for the TSP. One may, however, plan to remain happy with a suboptimal solution in which the total cost is not the smallest possible, but close to it. 5. Weather prediction Category: Functional problem Input: Records of weather for previous days and years. Possibly also data from satellites. Output: Expected weather of Kharagpur for tomorrow Processing: One statistically processes and analyzes the available data and makes an educated extrapolating guess for tomorrow's weather. 6. Web browsing Category: Functional problem Input: A URL (abbreviation for "Uniform Resource Locator" which is colloquially termed as "Internet site") Output: Display (audio and visual) of the file at the given URL Processing: Depending on the type of the file at the URL, one or more specific programs are run and the desired output is generated. For example, a web browser can render an HTML page, images in some formats etc. For displaying a movie, a separate software (or its plug-in) need be employed. 7. Chess : Can I win? Category: Search problem Input: A configuration of the standard 8x8 chess board and the player ("white" or "black") who is going to move next Output: A winning move for the next player, if existent, or "failure" Processing: In general, finding a winning chess move from a given state is a very difficult problem. The trouble is that one may have to explore an infinite number of possibilities. Even when the total possibilities are finite in number, that number is so big that one cannot expect to complete exploration of all of these possibilities in a reasonable time. A more practical strategy is to investigate all possible board sequences involving a small number of moves starting from the given configuration and to identify the best sequence under some criterion and finally prescribe the first move in the best sequence.

A computer is a device that can solve these and similar problems. A digital computer accepts, processes and outputs data in digitized forms (as opposed to analog forms).

A computer is a fundamental discovery of human mind. It does not tend to mimic other natural phenomena (except perhaps our brain). A computer can solve many problems. This is in sharp contrast with most other engineering gadgets. Computers are programmable, i.e., one can solve one's own problems by a computer.

The basic components of a digital computer


In order that a digital computer can solve problems, it should be equipped with the following components:

Input devices These are the devices using which the user provides input instances. In a programmable computer, input devices are also used to input programs. Examples: keyboard, mouse.

Output devices These devices notify the user about the outputs of a computation. Example: screen, printer.

Processing unit The central processing unit (CPU) is the brain of the computing device and performs the basic processing steps. A CPU typically consists of: An arithmetic and logical unit (ALU): This provides the basic operational units of the CPU. It is made up of units (like adders, multipliers) that perform arithmetic operations on integers and real numbers, and of units that perform logical operations (logical and bitwise AND, OR etc.). o A control unit: This unit is responsible for controlling flow of data and instructions. o General purpose registers: A CPU usually consists of a finite number of memory cells that work as scratch locations for storing intermediate results and values. External memory
o

The amount of memory (registers) resident in the CPU is typically very small and is inadequate to accommodate programs and data even of small sizes. Out-of-the-

processor memory provides the desired storage space. External memory is classified into two categories: Main (or primary) memory: This is a high-speed memory that stays close to the CPU. Programs are first loaded in the main memory and then executed. Usually main memory is volatile, i.e., its contents are lost after power-down. o Secondary memory: This is relatively inexpensive, bigger and low-speed memory. It is normally meant for off-line storage, i.e., storage of programs and data for future processing. One requires secondary storage to be permanent, i.e., its contents should last even after shut-down. Examples of secondary storage include floppy disks, hard disks and CDROM disks. Buses
o

A bus is a set of wires that connect the above components. Buses are responsible for movement of data from input devices, to output devices and from/to CPU and memory. The interconnection diagram for a simple computer is shown in the figure below. This architecture is commonly called the John von Neumann architecture after its discoverer who was the first to give a concrete idea of stored program computers. Surprisingly enough, the idea of computation (together with a rich theory behind it) was proposed several decades earlier than the first real computer is manufactured. John von Neumann proposed the first usable draft of a working computer.

Figure : The John von Neumann architecture

How does a program run in a computer?


The inputs, the intermediate values and the instructions defining the processing stage reside in the (main) memory. In order to separate data from instructions the memory is divided into two parts:

Data area The data area stores the variables needed for the processing stage. The values stored in the data area can be read, written and modified by the CPU. The data

area is often divided into two parts: a stack part and a heap part. The stack part typically holds all statically allocated memory (global and local variables), whereas the heap part is used to allocate dynamic memory to programs during run-time.

Instruction area The instruction area stores a sequence of instructions that define the steps of the program. Under the control of a clock, the computer carries out a fetch-decodeexecute cycle in which instructions are fetched one-by-one from the instruction area to the CPU, decoded in the control unit and executed in the ALU. The CPU understands only a specific set of instructions. The instructions stored in memory must conform to this specification.

The fetch-decode-execute cycle works as follows: 1. For starting the execution of a program, a sequence of machine instructions is copied to the instruction area of the memory. Also some global variables and input parameters are copied to the data area of the memory. 2. A particular control register, called the program counter (PC), is loaded with the address of the first instruction of the program. 3. The CPU fetches the instruction from that location in the memory that is currently stored in the PC register. 4. The instruction is decoded in the control unit of the CPU. 5. The instruction may require one or more operands. An operand may be either a data or a memory address. A data may be either a constant (also called an immediate operand) or a value stored in the data area of the memory or a value stored in a register. Similarly, an address may be either immediate or a resident of the main memory or available in a register. 6. An immediate operand is available from the instruction itself. The content of a register is also available at the time of the execution of the instruction. Finally, a variable value is fetched from the data part of the main memory. 7. If the instruction is a data movement operation, the corresponding movement is performed. For example, a "load" instruction copies the data fetched from memory to a register, whereas a "save" instruction sends a value from a register to the data area of the memory. 8. If the instruction is an arithmetic or logical instruction, it is executed in the ALU after all the operands are available in the CPU (in its registers). The output from the ALU is stored back in a register. 9. If the instruction is a jump instruction, the instruction must contain a memory address to jump to. The program counter (PC) is loaded with this address. A jump may be conditional, i.e., the PC is loaded with the new address if and only if some condition(s) is/are true. 10. If the instruction is not a jump instruction, the address stored in the PC is incremented by one.

11. If the end of the program is not reached, the CPU goes to Step 3 and continues its fetch-decode-execute cycle.

Figure : Execution of a program

Why need one program?


The electronic speed possessed by computers for processing data is really fabulous. Can you imagine a human prodigy manually multiplying two thousand digit integers flawlessly in an hour? A computer can perform that multiplication so fast that you even do not perceive that it has taken any time at all. It is wise to exploit this instrument to the best of our benefit. Why not, right?

However, there are many programs already written by professionals and amateurs. Why need we bother about writing programs ourselves? If we have to find roots of a polynomial or invert/multiply matrices or check primality of natural numbers, we can use standard mathematical packages and libraries. If we want to do web browsing, it is not a practical strategy that everyone writes his/her own browser. It is reported that playing chess with the computer could be a really exciting experience, even to world champions like Kasparov. Why should we write our own chess programs then? Thanks to the free (and open-source) software movement, many useful programs are now available in the public domain (often free of cost). Still, we have to write programs ourselves! Here are some compelling reasons:

There are so many problems to solve! Simple counting arguments suggest that computers can solve infinitely many problems. Given that the age of the universe and the human population are finite, we cannot expect every problem to be solved by others. In other words, each of us is expected to encounter problems which are so private that nobody else has even bothered to solve them, let alone making the source-codes or executables freely available for our consumption. Sometimes programs are available for solving some of our problems, but these programs are either too costly to satisfy our budget or so privately solved by others that they don't want to share their programs with us. If we plan to harness the electronic speed of computers, there seems to be no alternative way other than writing the programs ourselves. A stupendous example is provided by the proof of the four color conjecture, a curious mathematical problem that states that, given the map of any country, one can always color the states of the country using only four colors in such a way that no two states that share some boundary receive the same color. That five colors are sufficient was known long back, but the four color conjecture remained unsolved for quite a long time. Mathematicians reduced the problem to checking a list of configurations. But the list was so huge that nobody could even think of hand-calculating for all these instances. A computer program helped them explore all these possibilities. The four color conjecture finally came out to be true. Conservatives raised a huge hue and cry about such filthy methods of mathematical problem solving. But a problem solved happens to be a problem solved. Let the cynic cry! Computers can aid you solving many problems of various flavors ranging from mundane to practical to esoteric to deeply theoretical. Moreover, anybody may benefit from programming computers, irrespective of his/her area of study. It's just your own sweet will whether you plan to exploit this powerful servant.

Hey, we can write better programs than them!

Yes, we often can. Available programs may be too general and we can solve instances of our interest by specific programs much more efficiently than the general jack-of-all-trades stuff. Moreover, you may occasionally come up with brand-new algorithms that hold the promise of outperforming all previously known algorithms. You would then desire to program your algorithms to see how they perform in reality. Designing algorithms is (usually) a more difficult task than programming the algorithms, but the two may often go hand-in-hand before you jump to a practical conclusion.

How can one program?


Given a problem at hand, you tell the computer how to solve it and the machine does it. Unfortunately, telling the computer your processing steps is not that easy. Computers can be communicated with only in the language that they understand and are quite stubborn about that. You have to specify the exact way in which the fetch-decode-execute cycle is to be carried out so that your problem is solved. The CPU of a computer supports a primitive set of instructions (typically, data movement, arithmetic, logical and jump instructions). Writing a program using these instructions (called assembly instructions) has two major drawbacks:

The assembly language is so low-level that writing a program in this language is a very formidable task. One ends up with unmanageably huge codes that are very error-prone and extremely difficult to debug and update. The assembly language varies from machines to machines. The assembly codes suitable for one machine need not be understood by another machine. Moreover, different machines support different types of assembly instructions and there is no direct translation of instructions of one machine to those of another. For example, the ALU of Computer A may support integer multiplication, whereas that of Computer B does not. You have to translate each single multiplication instruction for Computer A to your own routine (say, involving additions and shifts) for doing multiplication in Computer B.

A high-level language helps you make your communication with computers more abstract and simpler and also widely machine-independent. You then require computer programs that convert your high-level description to the assembly-level descriptions of individual machines, one program for each kind of CPU. Such a translator is called a compiler. Therefore, your problem solving with computers involves the following three steps: 1. Write the program in a high-level language You should use a text editor to key in your program. In the laboratory we instruct you to use the emacs editor. You should also save your program in a (named) file.

We are going to teach you the high-level language known as C. 2. Compile your program You need a compiler to do that. In the lab you should use the C compiler cc (a.k.a. gcc).
cc myprog.c

If your program compiles successfully, a file named a.out (an abbreviation of "assembler output") is created. This file stores the machine instructions that can be understood by the particular computer where you invoked the compiler. If compilation fails, you should check your source code. The reason of the failure is that you made one or more mistakes in specifying your idea. Compilers are very stubborn about the syntax of your code. Even a single mistake may let the compiler churn out many angry messages. 3. Run the machine executable file This is done by typing
./a.out

(and then hitting the return/enter button) at the command prompt.

Your first C programs

The file intro1.c This program takes no input, but outputs the string "Hello, world!" in a line.
#include <stdio.h> main () { printf("Hello, world!\n"); }

The file intro2.c This program accepts an integer as input and outputs the same integer.
#include <stdio.h> main () { int n;

scanf("%d",&n); printf("%d\n",n); }

The file intro3.c This program takes an integer n as input and outputs its square n2.
#include <stdio.h> main () { int n; scanf("%d",&n); printf("%d\n",n*n); }

The file intro4.c This program takes an integer n as input and is intended to compute its reciprocal 1/n.
#include <stdio.h> main () { int n; scanf("%d",&n); printf("%d\n",1/n); }

Unfortunately, the program does not print the desired output. For input 0, it prints "Floating exception". (Except for really esoteric situations, division by 0 is a serious mathematical crime, so this was your punishment!) For input 1 it outputs 1, whereas for input -1 it outputs -1. For any other integer you input, the output is 0. That's too bad! But the accident is illustrating. Though your program compiled gracefully and ran without hiccups, it didn't perform what you intended. This is because you made few mistakes in specifying your desire.

The file intro5.c A corrected version of the reciprocal printer is as follows:


#include <stdio.h> main () { int n;

scanf("%d",&n); printf("%f\n",1.0/n); }

For input 67 it prints 0.014925, for input -32 it prints -0.031250. That's good work! However, it reports 1.0/0 as "Inf" (infinity). Mathematicians may now turn very angry because you didn't get the punishment you deserved.

Course home

CS13002 Programming and Data Structures

Spring semester

Variables and simple data types


The first abstraction a high-level language (like C) offers is a way of structuring data. A machine's memory is a flat list of memory cells, each of a fixed size. The abstraction mechanism gives special interpretation to collections of cells. Think of a collection of blank papers glued (or stapled) together. A piece of blank paper is a piece of paper, after all. However, when you see the neatly bound object, you leap up in joy and assert, "Oh, that's my note book!" This is abstraction. Papers remain papers and their significance in a note book is in no way diminished. A special meaning of the collection is a thing that is rendered by the abstraction. There is another point here -- usage convenience. You would love to take class notes in a note book instead of in loose sheets. A note book is abstract in yet another sense. You call it a note book irrespective of the size and color of the papers, of whether there are built-in lines on the papers, of what material is used to manufacture the papers, etc. The basic unit for storage of digital data is called a bit. It is an object that can assume one of the two possible values "0" and "1". Depending on how one is going to implement a bit, the values "0" and "1" are defined. If a capacitor stands for a bit, you may call its state "0" if the charge stored in it is less than 0.5 Volt, else you call its state "1". For a switch, "1" may mean "on" and "0" then means "off". Let us leave these implementation details to material scientists and VLSI designers. For us it is sufficient to assume that a computer comes with a memory having a huge number of built-in bits. A single bit is too small a unit to be adequately useful. A collection of bits is what a practical unit for a computer's operation is. A byte (also called an octet) is a collection of eight bits. Bigger units are also often used. In many of today's computers data are transfered and processed in chunks of 32 bits (4 bytes). Such an operational unit is often called a word. Machines supporting 64-bit words are also coming up and are expected to replace 32-bit machines in near future.

Basic data types


Bytes (in fact, bits too) are abstractions. Still, they are pretty raw. We need to assign special meanings to collections of bits in order that we can use those collections to solve our problems. For example, a matrix inversion routine deals with matrices each of whose elements is a real (or rational or complex) number. We then somehow have to map memory contents to numbers, to matrices, to pairs of real numbers (complex numbers), and so on. Luckily enough, a programmer does not have to do this mapping himself/herself. The C compiler already provides the abstractions you require. It is the headache of the compiler how it would map your abstract entities to memory cells in your

machine. You, in your turn, must understand the abstraction level which is provided to you for writing programs in C. For the time being, we will look at the basic data types supported by C. We will later see how these individual data types can be glued together to form more structured data types. Back to our note book example. A paper is already an abstraction, it's not any collection of electrons, protons and neutrons. So let us first understand what a paper is and what we can do with a piece of paper. We will later investigate how we can manufacture note books from papers, and racks from note books, and book-shelfs from racks, drawers, locks, keys and covers.

Integer data types


Integers are whole numbers that can assume both positive and negative values, i.e., elements of the set:
{ ..., -3, -2, -1, -, 1, 2, 3, ... }

This set is infinite, both the ellipses extending ad infinitum. C's built-in integer data types do not assume all possible integral values, but values between a minimum bound and a maximum bound. This is a pragmatic and historical definition of integers in C. The reason for these bounds is that C uses a fixed amount of memory for each individual integer. If that size is 32 bits, then only 232 integers can be represented, since each bit has only two possible states. Integer data type
char short int int long int long long int unsigned char unsigned short int unsigned int unsigned long int

Bit size 8 16 32 32 64 8 16 32 32

Minimum value -27=-128 -215=-32768 -231=-2147483648 -231=-2147483648 -263=9223372036854775808 0 0 0 0

Maximum value 27-1=127 215-1=32767 231-1=2147483647 231-1=2147483647 2631=9223372036854775807 28-1=255 216-1=65535 232-1=4294967295 232-1=4294967295 2641=18446744073709551615

unsigned long long int 64 0

Notes

The term int may be omitted in the long and short versions. For example, long int can also be written as long, unsigned long long int also as unsigned long long. ANSI C prescribes the exact size of int (and unsigned int) to be either 16 bytes or 32 bytes, that is, an int is either a short int or a long int. Implementers decide which size they should select. Most modern compilers of today support 32bit int. The long long data type and its unsigned variant are not part of ANSI C specification. However, many compilers (including gcc) support these data types.

Float data types


Like integers, C provides representations of real numbers and those representations are finite. Depending on the size of the representation, C's real numbers have got different names. Real data type Bit size float 32 double 64 long double 128

Character data types


We need a way to express our thoughts in writing. This has been traditionally achieved by using an alphabet of symbols with each symbol representing a sound or a word or some punctuation or special mark. The computer also needs to communicate its findings to the user in the form of something written. Since the outputs are meant for human readers, it is advisable that the computer somehow translates its bit-wise world to a human-readable script. The Roman script (mistakenly also called the English script) is a natural candidate for the representation. The Roman alphabet consists of the lower-case letters (a to z), the upper case letters (A to Z), the numerals (0 through 9) and some punctuation symbols (period, comma, quotes etc.). In addition, computer developers planned for inclusion of some more control symbols (hash, caret, underscore etc.). Each such symbol is called a character. In order to promote interoperability between different computers, some standard encoding scheme is adopted for the computer character set. This encoding is known as ASCII (abbreviation for American Standard Code for Information Interchange). In this scheme each character is assigned a unique integer value between 32 and 127. Since eight-bit units (bytes) are very common in a computer's internal data representation, the code of a character is represented by an 8-bit unit. Since an 8-bit unit can hold a total of 28=256 values and the computer character set is much smaller than that, some values of this 8-bit unit do not correspond to visible characters. These values are often used for representing invisible control characters (like line feed, alarm, tab etc.) and extended

Roman letters (inflected letters like , , ). Some values are reserved for possible future use. The ASCII encoding of the printable characters is summarized in the following table. Decimal Hex Binary Character 32 20 00100000 SPACE 33 21 00100001 ! 34 22 00100010 " 35 23 00100011 # 36 24 00100100 $ 37 25 00100101 % 38 26 00100110 & 39 27 00100111 ' 40 28 00101000 ( 41 29 00101001 ) 42 2a 00101010 * 43 2b 00101011 + 44 2c 00101100 , 45 2d 00101101 46 2e 00101110 . 47 2f 00101111 / 48 30 00110000 0 49 31 00110001 1 2 50 32 00110010 51 33 00110011 3 52 34 00110100 4 53 35 00110101 5 54 36 00110110 6 7 55 37 00110111 56 38 00111000 8 57 39 00111001 9 58 3a 00111010 : 59 3b 00111011 ; 60 3c 00111100 < 61 3d 00111101 = 62 3e 00111110 > 63 3f 00111111 ? 64 40 01000000 @ 65 41 01000001 A 66 42 01000010 B Decimal Hex Binary Character 80 50 01010000 P 81 51 01010001 Q 82 52 01010010 R 83 53 01010011 S 84 54 01010100 T 85 55 01010101 U 86 56 01010110 V 87 57 01010111 W 88 58 01011000 X 89 59 01011001 Y 90 5a 01011010 Z 91 5b 01011011 [ 92 5c 01011100 \ 93 5d 01011101 ] 94 5e 01011110 ^ 95 5f 01011111 _ 96 60 01100000 ` 97 61 01100001 a 98 62 01100010 b 99 63 01100011 c 100 64 01100100 d 101 65 01100101 e 102 66 01100110 f 103 67 01100111 g 104 68 01101000 h 105 69 01101001 i 106 6a 01101010 j 107 6b 01101011 k 108 6c 01101100 l 109 6d 01101101 m 110 6e 01101110 n 111 6f 01101111 o 112 70 01110000 p 113 71 01110001 q 114 72 01110010 r

67 68 69 70 71 72 73 74 75 76 77 78 79

43 01000011 C 115 73 01110011 s 44 01000100 D 116 74 01110100 t 45 01000101 E 117 75 01110101 u 46 01000110 F 118 76 01110110 v 47 01000111 G 119 77 01110111 w 48 01001000 H 120 78 01111000 x 49 01001001 I 121 79 01111001 y 4a 01001010 J 122 7a 01111010 z 4b 01001011 K 123 7b 01111011 { 4c 01001100 L 124 7c 01111100 | 4d 01001101 M 125 7d 01111101 } 4e 01001110 N 126 7e 01111110 ~ O 127 7f 01111111 DELETE 4f 01001111 Table : The ASCII values of the printable characters

C data types are necessary to represent characters. As told earlier, an eight-bit value suffices. The following two built-in data types are used for characters.
char unsigned char

Well, I mentioned earlier that these are integer data types. I continue to say so. These are both integer and character data types. If you want to interpret a char value as a character, you see the character it represents. If you want to view it as an integer, you see the ASCII value of that character. For example, the upper case A has an ASCII value of 65. An eight-bit value representing the character A automatically represents the integer 65, because to the computer A is recognized by its ASCII code, not by its shape, geometry or sound!

Pointer data types


Pointers are addresses in memory. In order that the user can directly manipulate memory addresses, C provides an abstraction of addresses. The memory location where a data item resides can be accessed by a pointer to that particular data type. C uses the special character * to declare pointer data types. A pointer to a double data is of data type double *. A pointer to an unsigned long int data is of type unsigned long int *. A character pointer has the data type char *. We will study pointers more elaborately later in this course.

Constants
Having defined data types is not sufficient. We need to work with specific instances of data of different types. Thus we are not much interested in defining an abstract class of

objects called integers. We need specific instances like 2, or -496, or +1234567890. We should not feel extravagantly elated just after being able to define an abstract entity called a house. We need one to live in. Specific instances of data may be constants, i.e., values that do not change during the execution of programs. For example, the mathematical pi remains constant throughout every program, and expectedly throughout our life-time too. Similarly, when we wrote 1.0/n to compute reciprocals, we used the constant 1.0. Constants are written much in the same way as they are written conventionally.

Integer constants
An integer constant is a non-empty sequence of decimal numbers preceded optionally by a sign (+ or -). However, the common practice of using commas to separate groups of three (or five) digits is not allowed in C. Nor are spaces or any character other than numerals allowed. Here are some valid integer constants:
332 -3002 +15 -00001020304

And here are some examples that C compilers do not accept:


3 332 2,334 - 456 2-34 12ab56cd

You can also express an integer in base 16, i.e., an integer in the hexadecimal (abbreviated hex) notation. In that case you must write either 0x or 0X before the integer. Hexadecimal representation requires 16 digits 0,1,...,15. In order to resolve ambiguities the digits 10,11,12,13,14,15 are respectively denoted by a,b,c,d,e,f (or by A,B,C,D,E,F). Here are some valid hexadecimal integer constants:
0x12ab56cd -0X123456 0xABCD1234 +0XaBCd12

Since different integer data types use different amounts of memory and represent different ranges of integers, it is often convenient to declare the intended data type explicitly. The following suffixes can be used for that: Suffix Data type long L (or l) long long LL (or ll)

U (or u) UL (or ul)

unsigned unsigned long

ULL (or ull) unsigned long long

Here are some specific examples:


4000000000UL 123U -0x7FFFFFFFl 0x123456789abcdef0ULL

Real constants
Real constants can be specified by the usual notation comprising an optional sign, a decimal point and a sequence of digits. Like integers no other characters are allowed. Here are some specific examples:
1.23456 1. .1 -0.12345 +.4560

And here are some non-examples (invalid real constants):


. - 1.23 1 234.56 1,234.56 1.234.56

Real numbers are sometimes written in the scientific notation (like 3.45x1067). The following expressions are valid for writing a real number in this fashion:
3.45e67 +3.45e67 -3.45e-67 .00345e-32 1e-15

You can also use E in place of e in this notation.

Character constants
Character constants are single printable symbols enclosed within single quotes. Here are some examples:
'A' '7' '@' ' '

There are some special characters that require you to write more than one printable characters within the quotes. Here is a list of some of them: Constant Character ASCII value '\0' Null 0 '\b' Backspace 8 '\t' Tab 9 '\n' New line 13 '\'' Quote 39 '\\' Backslash 92 Since characters are identified with integers in the range -127 to 128 (or in the range 0 to 255), you can use integer constants in the prescribed range to denote characters. The particular sequence '\xuv' (synonymous with 0xuv) lets you write a character in the hex notation. (Here u and v are two hex digits.) For example, '\x2b' is the integer 43 in decimal notation and stands for the character '+'.

Pointer constants
Well, there are no pointer constants actually. It is dangerous to work with constant addresses. You may anyway use an integer as a constant address. But doing that lets the compiler issue you a warning message. Finally, when you run the program and try to access memory at a constant address, you are highly likely to encounter a frustrating mishap known as "Segmentation fault". That's a deadly enemy. Try to avoid it as and when you can! Incidentally, there is a pointer constant that is used widely. This is called NULL. A NULL pointer points to nowhere.

Variables
Constants are not always sufficient to reflect reality. Though I am a constant human being and your constant PDS teacher, I am not a constant teacher for you or this classroom. Your or V1's teacher changes with time, though at any particular instant it assumes a constant value. A variable data is used to portray this scenario. A variable is specified by a name given to a collection of memory locations. Named variables are useful from two considerations:

Variables bind particular areas in the memory. You can access an area by a name and not by its explicit address. This abstraction simplifies a programmer's life dramatically. (If you want to tell a story to your friend about your pet, you would like to use its name instead of holding the constant object all the time in front of your friend's bored eyes.)

Names promote parameterized computation. You change the value of a variable and obtain a different output. For example, the polynomial 2a2+3a-4 evaluates to different values, as you plug in different values for the variable a. Of course, the particular name a is symbolic here and can be replaced by any other name (b,c etc.), but the formal naming of the parameter allows you to write (and work with) the function symbolically.

Naming conventions
C does not allow any sequence of characters as the name of a variable. This kind of practice is not uncommon while naming human beings too. However, C's naming conventions are somewhat different from human conventions. To C, a legal name is any name prescribed by its rules. There is no question of aesthetics or meaning or sweetsounding-ness. You would probably not name your (would-be) baby as "123abc". C also does not allow this name. However, C allows the name "abc123". One usually does not see a human being with this name. But then, have you heard of "Louis XVI"? Well, you may rack your brain for naming your baby. Here are C's straightforward rules.

Any sequence of alphabetic characters (lower-case a to z and upper-case A to Z) and numerals (0 through 9) and underscore (_) can be a valid name, provided that: o The name does not start with a numeral. o The name does not coincide with one of C's reserved words (like double, unsigned, for). These words have special meanings to the compilers and so are not allowed for your variables. o The name does not coincide with the same name of another entity (declared in the same scope). o The name does not contain any character other than those mentioned above. C's naming scheme is case-sensitive, i.e., teacher, Teacher, TEACHER, TeAcHeR are all different names. C does not impose any restriction on what name goes to what type of data. The name fraction can be given to an int variable and the name submerged can be given to a float variable. There is no restriction on the minimum length (number of characters) of a name, as long as the name is not the empty string. Some compilers impose a restriction on the maximum length of a name. Names bigger than this length are truncated to the maximum allowed leftmost part. This may lead to unwelcome collisions in different names. However, this upper limit is usually quite large, much larger than common names that we give to variables.

In C, names are given to other entities also, like functions, constants. In every case the above naming conventions must be adhered to.

Declaring variables
For declaring one or more variables of a given data type do the following:

First write the data type of the variable. Then put a space (or any other white character). Then write your comma-separated list of variable names. At the end put a semi-colon.

Here are some specific examples:


int m, n, armadillo; int platypus; float hi, goodMorning; unsigned char _u_the_charcoal;

You may also declare pointers simultaneously with other variables. All you have to do is to put an asterisk (*) before the name of each pointer.
long int counter, *pointer, *p, c; float *fptr, fval; double decker; double *standard;

Here counter and c are variables of type long int, whereas pointer and p are pointers to data of type long int. Similarly, decker is a double variable, whereas standard is a pointer to a double data.

Initializing variables
Once you declare a variable, the compiler allocates the requisite amount of memory to be accessed by the name of the variable. C does not make any attempt to fill that memory with any particular value. You have to do it explicitly. An uninitialized memory may contain any value (but it must contain some value) that may depend on several funny things like how long the computer slept after the previous shutdown, how much you have browsed the web before running your program, or may be even how much dust has accumulated on the case of your computer. We will discuss in the next chapter how variables can be assigned specific values. For the time being, let us investigate the possibility that a variable can be initialized to a constant value at the time of its declaration. For achieving that you should put an equality sign immediately after the name followed by a constant value before closing the declaration by a comma or semicolon.
int dint = 0, hint, mint = -32; char *Romeo, *Juliet = NULL; float gloat = 2e-3, throat = 3.1623, coat;

Here the variable dint is initialized to 0, mint to -32, whereas hint is not initialized. The char pointer Romeo is not initialized, whereas Juliet is initialized to the NULL pointer. Notice that uninitialized (and unassigned) variables may cause enough sufferings to a programmer. Take sufficient care!

Names of constants
So far we have used immediate constants that are defined and used in place. In order to reuse the same immediate constant at a different point in the program, the value must again be explicitly specified. C provides facilities to name constant values (like variables). Here we discuss two ways of doing it. Constant variables Variables defined as above are read/write variables, i.e., one can both read their contents and store values in them. Constant variables are read-only variables and can be declared by adding the reserved word const before the data type.
const double pi = 3.1415926535; const unsigned short int perfect1 = 6, perfect2 = 28, perfect3 = 496;

These declarations allocate space and initialize the variables like variable variables, but don't allow the user to alter the value of PI, perfect1 etc. at a later time during the execution of the program.
#define'd constants

These are not variables. These are called macros. If you #define a value against a name and use that name elsewhere in your program, the name is literally substituted by the C preprocessor, before your code is compiled. Macros do not reside in the memory, but are expanded well before any allocation attempt is initiated.
#define #define #define #define PI 3.1415926535 PERFECT1 6 PERFECT2 28 PERFECT3 496

Look at the differences with previous declarations. First, only one macro can be defined in a single line. Second, you do not need the semicolon or the equality sign for defining a macro.

Parameterized macros can also be defined, but unless you fully understand what a macro means and how parameters are handled in macros, don't use them. Just a wise tip, I believe! You can live without them.

Typecasting
An integer can naturally be identified with a real number. The converse is not immediate. However, we can adopt some convention regarding conversion of a real number to an integer. Two obvious candidates are truncation and rounding. C opts for truncation. In order to convert a value <val> of any type to a value of <another_type> use the following directive:
(<another_type>)<val>

Here <val> may be a constant or a value stored in a named variable. In the examples below we assume that piTo4 is a double variable that stores the value 97.4090910340. Output value (int)9.8696044011 The truncated integer 9 (int)-9.8696044011 The truncated integer -9 (float)9 The floating-point value 9.000000 (int)piTo4 The integer 97 (char)piTo4 The integer 97, or equivalently the character 'a' (int *)piTo4 An integer pointer that points to the memory location 97. (double)piTo4 The same value stored in piTo4 Typecasting command Typecasting also applies to expressions and values returned by functions.

Representation of numbers in memory


Binary representation
Computer's world is binary. Each computation involves manipulating a series of bits each realized by some mechanism that can have two possible states denoted "0" and "1". If that is the case, integers, characters, floating point numbers need also be represented by bits. Here is how this representation can be performed. For us, on the other hand, it is customary to have 10 digits in our two hands and consequently 10 digits in a number system. The decimal system is natural. Not really, it is just the convention. From our childhood we have been taught to use base ten representations to such an extent that it is difficult to conceive of alternatives, in fact to even think that any natural number greater than 1 can be a legal base for number

representation. (There also exists an "exponentially big" unary representation of numbers that uses only one digit better called a "symbol" now.) Binary expansion of integers Let's first take the case of non-negative integers. In order to convert such an integer n from the decimal representation to the binary representation, one keeps on dividing n by 2 and remembering the intermediate remainders obtained. When n becomes 0, we have to write the remainders in the reverse sequence as they are generated. That's the original n in binary. n Remainder 57 Divide by 2 28 1 0 Divide by 2 14 Divide by 2 7 0 Divide by 2 3 1 Divide by 2 1 1 Divide by 2 0 1 57 = (111001)2 For computers, we usually also specify a size t of the binary representation. For example, suppose we want to represent 57 as an unsigned char, i.e., as an 8-bit value. The above algorithm works fine, but we have to

either insert the requisite number of leading zero bits, or repeat the "divide by 2" step exactly t times without ever looking at whether the quotient has become 0. n Remainder 57 Divide by 2 28 1 Divide by 2 14 0 Divide by 2 7 0 Divide by 2 3 1 Divide by 2 1 1 Divide by 2 0 1 0 Divide by 2 0 Divide by 2 0 0 57 = (00111001)2

What if the given n is too big to fit in a t-bit place? Now also you can "divide by 2" exactly t times and read the t remainders backward. That will give you the least significant t bits of n. The remaining more significant bits will simply be ignored. n Remainder 657 Divide by 2 328 1 Divide by 2 164 0 Divide by 2 82 0 Divide by 2 41 0 Divide by 2 20 1 Divide by 2 10 0 Divide by 2 5 0 Divide by 2 2 1 657 = (...10010001)2 Signed magnitude representation of integers Now we add provision for sign. Here is how this is conventionally done. In a t-bit signed representation of n:

The most significant (leftmost) bit is reserved for the sign. "0" means positive, "1" means negative. The remaining t-1 bits store the (t-1)-bit representation of the magnitude (absolute value) of n (i.e., of |n|).

Example: The 7-bit binary representation of 57 is (0111001)2.


The 8-bit signed magnitude representation of 57 is (00111001)2. The 8-bit signed magnitude representation of -57 is (10111001)2.

Back to decimal Given an integer in unsigned or signed representation, its magnitude and sign can be determined. For the sign, the most significant bit is consulted. For the magnitude, a sum of appropriate powers of 2 is calculated. Let the magnitude be stored in l bits. The bits are numbered 0,1,...,l-1 from right to left. The i-th position (from the right) corresponds to the power 2i. One simply adds the powers of 2 corresponding to those positions that hold 1 bits in the binary representation.

Signed integer

0 1 1 1 0 0 1

Position Sign 6 5 4 3 2 1 0 Contribution + 0 25 24 23 0 0 20 +(25+24+23+20) = +(32+16+8+1) = +57

Signed integer

0 0 1 0 0 0 1

Position Sign 6 5 4 3 2 1 0 Contribution - 0 0 24 0 0 0 20 -(24+20) = -(16+1) = -17

Unsigned integer 1 0 0 1 0 0 0 1 Position 7 6 5 4 3 2 1 0 Contribution 27 0 0 24 0 0 0 20 27+24+20 = 128+16+1 = 145 Notes:


The t-bit unsigned representation can accommodate integers in the range 0 to 2t-1. The t-bit signed magnitude representation can accommodate integers in the range -(2t-1-1) to +(2t-1-1). In the signed magnitude representation 0 has two renderings: +0 = 0000...0 and -0=1000...0.

1's complement representation


1's complement of a t-bit sequence (at-1at-2...a0)2 is the t-bit sequence (bt-1bt-2...b0)2, where for each i we have bi = 1 - ai, i.e., bi is the bit-wise complement of ai. Here (bt-1btt 2...b0)2= 2 -1-(at-1at-2...a0)2. The t-bit 1's complement representation of an integer n is a t-bit signed representation with the following properties:

The most significant (leftmost) bit is the sign bit, 0 if n is positive, 1 if n is negative. The remaining t-1 bits are used to stand for the absolute value |n|. o If n is positive, these t-1 bits hold the (t-1)-bit binary representation of |n|. o If n is negative, these t-1 bits hold the (t-1)-bit 1's complement of |n|.

Example: The 7-bit binary representation of 57 is (0111001)2. The 7-bit 1's complement of 57 is (1000110)2.

The 1's complement representation of +57 is (00111001)2. The 1's complement representation of -57 is (11000110)2.

Notes:

The t-bit 1's complement representation can accommodate integers in the range (2t-1-1) to +(2t-1-1). 0 has two representations: +0 = (0000...0)2 and -0 = (1111...1)2.

2's complement representation


The t-bit 2's complement of a positive integer n is 1 plus the t-bit 1's complement of n. Thus one first complements each bit in the t-bit binary expansion of n, and then adds 1 to this complemented number. If n = (at-1at-2...a0)2, then its t-bit 1's complement is (bt-1bt2...b0)2 with each bi = 1 - ai, and therefore the 2's complement of n is n' = 1+(bt-1btt t t 2...b0)2 = 1+(2 -1)-n = 2 -n. In order that n' fits in t-bits we then require 0<=n'<=2 -1, i.e., 1<=n<=2t. The t-bit 2's complement representation of an integer n is a t-bit signed representation with the following properties:

The most significant (leftmost) bit is the sign bit, 0 if n is positive, 1 if n is negative. The remaining t-1 bits are used to stand for the absolute value |n|. o If n is positive, these t-1 bits hold the (t-1)-bit binary representation of |n|. o If n is negative, these t-1 bits hold the (t-1)-bit 2's complement of |n|.

Example: The 7-bit binary representation of 57 is (0111001)2. The 7-bit 1's complement of 57 is (1000110)2, so the 7-bit 2's complement of 57 is (1000111)2.

The 2's complement representation of +57 is (00111001)2. The 2's complement representation of -57 is (11000111)2.

Notes:

The t-bit 2's complement representation can accommodate integers in the range 2t-1 to +(2t-1-1). 0 has only one representation: (0000...0)2. The 2's complement representation simplifies implementation of arithmetic (in hardware).

Example: The different 8-bit representations of signed integers are summarized in the following table:

Decimal +127 +126 +125


...

Signed 1's 2's magnitude complement complement 01111111 01111110 01111101


...

01111111 01111110 01111101


...

01111111 01111110 01111101


...

+3 +2 +1 0 -1 -2 -3
...

00000011 00000010 00000001 00000000 or 10000000 10000001 10000010 10000011


...

00000011 00000010 00000001 00000000 or 11111111 11111110 11111101 11111100


...

00000011 00000010 00000001 00000000 11111111 11111110 11111101


...

-126 -127 -128

01111110 01111111 No rep

10000001 10000000 No rep

10000010 10000001 10000000

Hexadecimal and octal representations


Similar to binary (base 2) representation, one can have representations of integers in any base B>=2. In computer science two popular bases are 16 and 8. The representation of an integer in base 16 is called the hexadecimal representation, whereas that in base 8 is called the octal representation of the integer. For any base B, the base B representation of n can be obtained by successively dividing n by B until the quotient becomes zero. One then writes the remainders in the reverse sequence as they are generated. Since division by B leaves remainders in the range 0,1,...,B-1, one requires these many digits for the base B representation. If B=8, the natural (octal) digits are 0,1,...,7. For B=16, we have a problem; we now require 16 digits 0,1,...,15. Now it is difficult to distinguish, for example, between 13 as a digit and 13 as the digit 1 followed by the digit 3. We use the symbols a,b,c,d,e,f (also in upper case) to stand for the hexadecimal digits 10,11,12,13,14,15. Example: Hexadecimal representation

n 413657 Divide by 16 25853 Divide by 16 Divide by 16 Divide by 16 Divide by 16 1615 100 6 0

Remainder

9 13 15 4 6

413657 = 0x64fd9 Example: Octal representation n 413657 Divide by 8 51707 Divide by 8 Divide by 8 Divide by 8 Divide by 8 Divide by 8 Divide by 8 6463 807 100 12 1 0 1 3 7 7 4 4 1 Remainder

413657 = (1447731)8 Since 16 and 8 are powers of two, the hexadecimal and octal representations of an integer can also be computed from its binary representation. For the hexadecimal representation, one generates groups of successive 4 bits starting from the right of the binary representation. One may have to add a requisite number of leading 0 bits in order to make the leftmost group contain 4 bits. One 4 bit integer corresponds to an integer in the range 0,1,...,15, i.e., to a hexadecimal digit. For the octal representation, grouping should be made three bits at a time. Example: The binary representation of 413657 is (1100100111111011001)2. Arranging this bit-sequence in groups of 4 gives:
110 0100 1111 1101 1001

Thus 413657 = 0x64fd9, as calculated above. The grouping with three bits per group is:

1 100 100 111 111 011 001

Thus 413657 = (1447731)8.

IEEE floating point standard


Now it's time for representing real numbers in binary. Let us first review our decimal intuition. Think of the real number:
n = 172.93 = 1.7293 x 102 = 0.17293 x 103

By successive division by 2 we can represent the integer part 172 of n in binary. For the fractional part 0.93 we use repeated multiplication by two in order to get the bits after the binary point. After each multiplication, the integer part of the product generates the next bit in the representation. We then replace the old fractional part by the fractional part of the product. Fractional part 0.93 Multiply by 2 0 0 1 1 0 1 0 1 Multiply by 2 Multiply by 2 Multiply by 2 Multiply by 2 Multiply by 2 Multiply by 2 Multiply by 2 Multiply by 2 Multiply by 2 0.86 0.72 0.44 0.88 0.76 0.52 0.04 0.08 0.16 0.32 1 1 1 0 1 1 1 0 0 0 Integral part

Integral part 172 Divide by 2 Divide by 2 Divide by 2 Divide by 2 Divide by 2 Divide by 2 Divide by 2 Divide by 2 86 43 21 10 5 2 1 0

Remain der

172 = (10101100)2

0.93 = (0.1110111000... )2 172.93 = (10101100.1110111000...)2 = (1.01011001110111000...)2 x 27 = (0.1010110 01110111000...)2 x 28 It turns out that the decimal fraction 0.93 does not have a terminating binary expansion. So we have to approximate the binary expansion (after the binary point) by truncating the series after a predefined number of bits. Truncating after ten bits gives the approximate value of n to be:
= = = = (1.0101100111)2 x 27 (20 + 2-2 + 2-4 + 2-5 + 2-8 + 2-9 + 2-10) x 27 27 + 25 + 23 + 22 + 2-1 + 2-2 + 2-3 128 + 32 + 8 + 4 + 0.5 + 0.25 + 0.125 172.875

This example illustrates how to store approximate representations of real numbers using a fixed amount of bits. If we write the expansion in the normal form with only one 1 bit (and nothing else) to the left of the binary point, then it is sufficient to store only the fractional part (0101100111 in our example) and the exponent of 2 (7 in the example). This is precisely what is done by the IEEE 754 floating-point format. This is a 32-bit representation of signed floating point numbers. The 32 bits are used as follows: 31 S 30 E7 29 E6
... ...

24 E1

23

22

21

...

E0 M22 M21 ... M1 M0

The meanings of the different parts are as follows:


S is the sign bit, 0 represents positive, and 1 negative. The eight bits E7E6...E1E0 represent the exponent. For usual numbers it is allowed to lie in the range 1 to 254. The rightmost 23 bits M22M21...M1M0 represent the mantissa (also called significand). It is allowed to take any of the 223 values between 0 and 223-1.

Normal numbers The normal number that this 32-bit value stores is interpreted as:
(-1)S x (1.M22M21...M1M0)2 x 2[(E7E6...E1E0)2-127]

The biggest real number that this representation stores corresponds to


0 11111110 1111111 11111111 11111111

which is approximately 2128, i.e., 3.403 x 1038. The smallest positive value that this format can store corresponds to
0 00000001 0000000 00000000 00000000

which is i.e., nearly 1.175 x 10-38. Denormal numbers The IEEE standard also supports a denormal form. Now all the exponent bits E7E6...E1E0 must be 0. The 32-bit value is now interpreted as the number:
(-1)S x (0.M22M21...M1M0)2 x 2-126 1.00000000000000000000000 x 2-126 = 2-126,

The maximum positive value that can be represented by the denormal form corresponds to
0 00000000 1111111 11111111 11111111

which is
0.11111111111111111111111 x 2-126 = 2-126 - 2-149.

This is obtained by subtracting 1 from the least significant bit position of the smallest positive integer representable by a normal number. Denormal numbers therefore correspond to a gradual underflow from normal numbers. The minimum positive value that can be represented by the denormal form corresponds to which is 2-149, i.e., nearly 1.401 x 10-45. Special numbers Recall that the exponent bits were not allowed to take the value 1111 1111 (255 in decimal). This value corresponds to some special numbers. These numbers together with some other special ones are listed in the following table. 32-bit value 0 1111 1111 0000000 00000000 00000000 1 1111 1111 0000000 00000000 00000000 0 1111 1111 Any nonzero 23-bit value 1 1111 1111 Any nonzero 23-bit value Interpretation +Inf -Inf NaN NaN
0 00000000 0000000 00000000 00000001

0 0000 0000 0000000 00000000 00000000 1 0000 0000 0000000 00000000 00000000 0 0111 1111 0000000 00000000 00000000 1 0111 1111 0000000 00000000 00000000 0 1000 0000 0000000 00000000 00000000 1 1000 0000 0000000 00000000 00000000 0 1000 0000 1000000 00000000 00000000 1 1000 0000 1000000 00000000 00000000 0 1111 1110 1111111 11111111 11111111 0 0000 0001 0000000 00000000 00000000 0 0000 0000 1111111 11111111 11111111 0 0000 0000 0000000 00000000 00000001

+0 -0 +1.0 -1.0 +2.0 -2.0 +3.0 -3.0 2255 - 2231 2-126 2-126 - 2-149 2-149

Introduction to arrays
Arrays are our first example of structured data. Think of a book with pages numbered 1,2,...,400. The book is a single entity, has its individual name, author(s), publisher, bla bla bla, but the contents of its different pages are (normally) different. Moreover, Page 251 of the book refers to a particular page of the book. To sum up, individual pages retain their identities and still we have a special handy bound structure treated as a single entity. That's again abstraction, but this course is mostly about that. Now imagine that you plan to sum 400 integers. Where will you store the individual integers? Thanks to your ability to declare variables, you can certainly do that. Declare 400 variables with 400 different names, initialize them individually and finally add each variable separately to an accumulating sum. That's gigantic code just for a small task. Arrays are there to help you. Like your book you now have a single name for an entire collection of 400 integers. Declaration is small. Codes for initialization and addition also become shorter, because you can now access the different elements of the collection by a unique index. There are built-in C constructs that allow you do parameterized (i.e., indexed) tasks repetitively.

Declaring arrays
Simple! Just as you did for individual data items, write the data type, then a (legal) name and immediately after that the size of the array within square brackets. For example, the declaration

int intHerd[400];

creates an array of name intHerd that is capable of storing 400 int data. A more stylistic way to do the same is illustrated now.
#define HERD_SIZE 400 int intHerd[HERD_SIZE];

Here are two other arrays, the first containing 123 float data, the second 1024 unsigned char data.
float flock[123]; unsigned char crowd[1024];

You can intersperse declaration of arrays with those of simple variables and pointers.
unsigned long man, society[100], woman, *ptr;

This creates space for two unsigned long variables man and woman, an array called society with hundred unsigned long data, and also a pointer named ptr to an unsigned long data. Note that all individual elements of a single array must be of the same type. You cannot declare an array some of whose elements are integers, the rest floating-point numbers. Such heterogeneous collections can be defined by other means that we will introduce later.

Accessing individual array elements


Once an array A of size s is declared, its individual elements are accessed as A[0],A[1],...,A[s-1]. It is very important to note that: Array indexing in C is zero-based. This means that the "first" element of A is named as A[0] (not A[1]), the "second" as A[1], and so on. The last element is A[s-1]. Each element A[i] is of data type as provided in the declaration. For example, if the declaration goes as:
int A[32];

each of the elements A[0],A[1],...,A[31] is a variable of type int. You can do on each A[i] whatever you are allowed to do on a single int variable. C does not provide automatic range checking.

If an array A of size s is declared, the element A[i] belongs to the array (more correctly, to the memory locations allocated to A) if and only if 0 <= i <= s-1. However, you can use A[i] for other values of i. No compilation errors (nor warnings) are generated for that. Now when you run the program, the executable attempts to access a part of the memory that is not allocated to your array, nor perhaps to (the data area allocated to) your program at all. You simply do not know what resides in that part of the memory. Moreover, illegal memory access may lead to the deadly "segmentation fault". C is too cruel at certain points. Beware of that!

Initializing arrays
Arrays can be initialized during declaration. For that you have to specify constant values for its elements. The list of initializing values should be enclosed in curly braces. For the declaration
int A[5] = { 51, 29, 0, -34, 67 }; A[0] is initialized to 51, A[1] to 29, A[2] to 0, A[3] to -34 and A[4] to 67. Similarly, for

the declaration
char C[8] = { 'a', 'b', 'h', 'i', 'j', 'i', 't', '\0' }; C[0] gets the value 'a', C[1] the value 'b', and so on. The last (7th) location receives

the null character. Such null-terminated character arrays are also called strings. Strings can be initialized in an alternative way. The last declaration is equivalent to:
char C[8] = "abhijit";

Now see that the trailing null character is missing here. C automatically puts it at the end. Note also that for individual characters, C uses single quotes, whereas for strings, it uses double quotes. If you do not mention sufficiently many initial values to populate an entire array, C uses your incomplete list to initialize the array locations at the lower end (starting from 0). The remaining locations are initialized to zero. For example, the initialization
int A[5] = { 51, 29 };

is equivalent to
int A[5] = { 51, 29, 0, 0, 0 };

If you specify an initialization list, you may omit the size of the array. In that case, the array will be allocated exactly as much space as is necessary to accommodate the initialization list. You must, however, provide the square brackets to indicate that you are declaring an array; the size may be missing between them.

int A[] = { 51, 29 };

creates an array A of size 2 with A[0] holding the value 51 and A[1] the value 29. This declaration is equivalent to
int A[2] = { 51, 29 };

but not to
int A[5] = { 51, 29 };

There are a lot more things that pertain to arrays. You may declare multi-dimensional arrays, you may often interchange arrays with pointers, and so on. But it's now too early for these classified topics. Wait until your experience with C ripens.

Course home

CS13002 Programming and Data Structures

Spring semester

Assignments
Assignments and imperative programming
Initialization during declaration helps one store constant values in memory allocated to variables. Later one typically does a sequence of the following:

Read the values stored in variables. Do some operations on these values. Store the result back in some variable.

This three-stage process is effected by an assignment operation. A generic assignment operation looks like:
variable = expression;

Here expression consists of variables and constants combined using arithmetic and logical operators. The equality sign (=) is the assignment operator. To the left of this operator resides the name of a variable. All the variables present in expression are loaded to the CPU. The ALU then evaluates the expression on these values. The final result is stored in the location allocated to variable. The semicolon at the end is mandatory and denotes that the particular statement is over. It is a statement delimiter, not a statement separator.

Animation example : expression evaluation


A C program typically consists of a sequence of statements. They are executed one-byone from top to bottom (unless some explicit jump instruction or function call is encountered). This sequential execution of statements gives C a distinctive imperative flavor. This means that the sequence in which statements are executed decides the final values stored in variables. Let us illustrate this using an example:
int x = 43, y = 15; initialized */ x = y + 5; y = x; /* Two integer variables are declared and

/* The value 15 of y is fetched and added to 5. The sum 20 is stored in the memory location for x. */ /* The value stored in x, i.e., 20 is fetched and stored back in y. */

After these statements are executed both the memory locations for x and y store the integer value 20. Let us now switch the two assignment operations.
int x = 43, y = 15; initialized */ y = x; x = y + 5; /* Two integer variables are declared and

/* The value stored in x, i.e., 43 is fetched and stored back in y. */ /* The value 43 of y is fetched and added to 5. The sum 48 is stored in the memory location for x. */

For this sequence, x stores the value 48 and y the value 43, after the two assignment statements are executed. The right side of an assignment operation may contain multiple occurrences of the same variable. For each such occurrence the same value stored in the variable is substituted. Moreover, the variable in the left side of the assignment operator may appear in the right side too. In that case, each occurrence in the right side refers to the older (preassignment) value of the variable. After the expression is evaluated, the value of the variable is updated by the result of the evaluation. For example, consider the following:
int x = 5; x = x + (x * x);

The value 5 stored in x is substituted for each occurrence of x in the right side, i.e., the expression 5 + (5 * 5) is evaluated. The result is 30 and is stored back to x. Thus, this assignment operation causes the value of x to change from 5 to 30. The equality sign in the assignment statement is not a mathematical equality, i.e., the above statement does not refer to the equation x = x + x2 (which happens to have a single root, namely x = 0). It similarly makes sense to write
z = z + 2;

to imply an assignment (increment the value of z by 2). Mathematically, it makes little sense, since no numbers you know seem to satisfy the equation z = z + 2. (But I know some of them!) Notice that in C there is a different way for checking equality of two expressions. The single equality sign is not that. Floating point numbers, characters and array locations may also be used in assignment operations.
float a = 2.3456, b = 6.5432, c[5]; arrays */ char d, e[4]; and arrays */ /* Declare float variables and /* Declare character variables

c[0] = a 8.8888 */ c[1] = a 6.5432 */ c[2] = b 2.3456 */ a = c[1] -8.8888 */

+ b; - c[0]; - c[0]; + c[2];

/* c[0] is assigned 2.3456 + 6.5432, i.e., /* c[1] is assigned 2.3456 - 8.8888, i.e., /* c[2] is assigned 6.5432 - 8.8888, i.e., /* a is assigned (-6.5432) + (-2.3456), i.e.,

d = 'A' - 1; /* d is assigned the character ('@') one less than 'A' in the ASCII chart */ e[0] = d + 1; /* e[0] is assigned the character next to '@', i.e., 'A' */ e[1] = e[0] + 1; /* e[1] is assigned the character next to 'A', i.e., 'B' */ e[2] = e[0] + 2; /* e[2] is assigned the character second next to 'A', i.e., 'C' */ e[3] = e[2] + 1; /* e[3] is assigned the character next to 'C', i.e., 'D' */

An assignment does an implicit type conversion, if its left side turns out to be of a different data type than the type of the expression evaluated.
float a = 7.89, b = 3.21; int c; c = a + b;

Here the right side involves the floating point operation 7.89 + 3.21. The result is the floating point value 11.1. The assignment plans to store this result in an integer variable. The value 11.1 is first truncated and subsequently the integer value 11 is stored in c. One can explicitly mention this typecasting command as:
float a = 7.89, b = 3.21; int c; c = (int)(a + b);

The parentheses around the expression a + b implies that the typecasting is to be done after the evaluation of the expression. The following variant has a different effect:
float a = 7.89, b = 3.21; int c; c = (int)a + b;

Here a is first converted to 7 and then added to 3.21. The resulting value (10.21) is truncated and stored in c. That is, now c is assigned the value 10. In C, an assignment operation also returns a value. It is precisely the value that is assigned. This value can again be used in an expression.

int a, b, c; c = (a = 8) + (b = 13);

Here a is assigned the value 8 and b the value 13. The values (8 and 13) returned by these assignments are then added and the sum 21 is stored in c. The assignment of c also returns a value, i.e., 21. Here we have ignored this value. Assignment is right associative. For example,
a = b = c = 0;

is equivalent to
a = (b = (c = 0));

Here c is first assigned the value 0. This value is returned to assign b, i.e., b also gets the value 0. The value returned from this second assignment is then assigned to a. Thus after this statement all of a, b and c are assigned the value 0.

Built-in operators
Now that we know how to assign values to variables, what remains is a discussion on how expressions can be generated. Here are the rules:

A constant is an expression. A (defined) variable is an expression. If E is an expression, then so also is (E). If E is an expression and op a unary operator defined in C, then op E is again an expression. If E1 and E2 are expressions and op is a binary operator defined in C, then E1 op E2 is again an expression. If V is a variable and E is an expression, then V = E is also an expression.

These rules do not exhaust all possibilities for generating expressions, but form a handy set to start with. Examples:
53 -3.21 'a' x -x[0] x + 5 (x + 5) (x) + (((5))) y[78] / (x + 5) y[78] / x + 5 y / (x = 5) /* /* /* /* /* /* /* /* /* /* /* constant */ constant */ constant */ variable */ unary negation on a variable */ addition of two subexpressions */ parenthesized expression */ another parenthesized expression */ more complex expression */ another complex expression */ expression involving assignment */

1 + 32.5 / 'a'

/* expression involving different data types */

Non-examples:
5 3 /* space is not an operator and integer constants may not contain spaces */ y *+ 5 /* *+ is not a defined operator */ x (+ 5) /* badly placed parentheses */ x = 5; /* semi-colons are not allowed in expressions */

We now list the basic operators defined in C and the interpretations of these operators.

Arithmetic operators
Arithmetic operators include negation, addition, subtraction, multiplication and division. The result of the operation depends on which type of data the arithmetic operator operates on. The following table summarizes the relevant information. Operator + * Meaning unary negation (binary) addition (binary) subtraction Description Applicable for integers and real numbers. Does not make enough sense for unsigned operands. Applicable for integers and real numbers. Applicable for integers and real numbers.

(binary) Applicable for integers and real numbers. multiplication For integers division means "quotient", whereas for real numbers division means "real division". If both the operands are integers, the integer quotient is calculated, whereas if (one or both) the operands are real numbers, real division is carried out. Applicable only for integer operands.

(binary) division

(binary) remainder

Examples: Here are examples of integer arithmetic:


55 55 55 55 55 + * / % 21 21 21 21 21 evaluates evaluates evaluates evaluates evaluates to to to to to 76. 34. 1155. 2. 13.

Here are some examples of floating point arithmetic:

55.0 55.0 55.0 55.0 55.0

+ * / %

21.0 21.0 21.0 21.0 21.0

evaluates to 76.0. evaluates to 34.0. evaluates to 1155.0. evaluates to 2.6190476 (approximately). is not defined.

Note: C does not provide a built-in exponentiation operator.

Bitwise operators
Bitwise operations apply to unsigned integer operands and work on each individual bit. Bitwise operations on signed integers give results that depend on the compiler used, and so are not recommended in good programs. The following table summarizes the bitwise operations. For illustration we use two unsigned char operands a and b. We assume that a stores the value 237 = (11101101)2 and that b stores the value 174 = (10101110)2. Operator Meaning
a = 237 &

Example 1 1 1 0 1 1 0 1 1 0 1 0 1 1 1 0

AND

b = 174

a & b is 172 1 0 1 0 1 1 0 0 a = 237 |

1 1 1 0 1 1 0 1 1 0 1 0 1 1 1 0

OR

b = 174

a | b is 239 1 1 1 0 1 1 1 1 a = 237 ^

1 1 1 0 1 1 0 1 1 0 1 0 1 1 1 0 0 1 0 0 0 0 1 1 1 1 1 0 1 1 0 1 0 0 0 1 0 0 1 0 1 1 1 0 1 1 0 1

EXOR

b = 174 a ^ b is 67 a = 237

Complement

~a is 18 a = 237

>>

Right-shift

a >> 2 is 59 0 0 1 1 1 0 1 1 b = 174

<<

Left-shift

1 0 1 0 1 1 1 0

b << 1 is 92 0 1 0 1 1 1 0 0

Some shorthand notations

C provides some shorthand notations for some particular kinds of operations. For example, if the variable to be assigned is the first operand in the expression on the right side, then this variable may be omitted in the expression and the operator comes before the equality sign. More precisely, the assignment
var = var op expression;

is equivalent to
var op= expression;

Here the operator op can be any binary operator described above, namely, +,,*,/,%,&,|,^,>>,<<. Some specific examples are:
a a c a b = = = = = a a c a b + 10.43; % 43; * (a + b - c); >> 3; ^ (a << 3); is is is is is equivalent equivalent equivalent equivalent equivalent to to to to to a a c a b += 10.43; %= 43; *= a + b - c; >>= 3; ^= (a << 3);

A special case of this can be shortened further: increment/decrement by 1.


a = a + 1; is equivalent to a += 1; which is also equivalent to ++a; b = b - 1; is equivalent to b -= 1; which is also equivalent to --b;

These increment/decrement operators (++ and --) are called pre-increment and predecrement operators. C also provides post-increment and post-decrement operators. These operators are same (++ and --) but are written after the variable being incremented/decremented. The isolated statements
a++; b--;

are respectively equivalent to


++a; --b;

However, there is a subtle difference between the two. Recall that every assignment returns a value. The increment (or decrement) expressions ++a and a++ are also assignment expressions. Both stand for "increment the value of a by 1". But then which value of a is returned by this expression? We have the following rules:

For a++ the older value of a is returned and then the value of a is incremented. This is why it is called the post-increment operation. For ++a the value of a is first incremented and this new (incremented) value of a is returned. This is why it is called the pre-increment operation.

A similar argument holds for the decrement operations. The following examples illustrate the differences:
a = 43; b = 15; c = (++a) * (--b);

Here a is first incremented and the value 44 is returned. Also b is decremented and the value 14 is returned. Then these two values are multiplied and the product 44*14 = 616 is assigned to c.
a = 43; b = 15; c = (++a) * (b--);

Now a is first incremented and the value 44 is returned. But the value of b is first returned (15) and then decremented. Thus c gets the value 44*15 = 660. Similarly, after the execution of the following statements
a = 43; b = 15; c = (a++) * (b--); a, b and c respectively hold the values 44, 14 and 43*15 = 645.

Precedence of operators
An explicitly parenthesized arithmetic (and/or logical) expression clearly indicates the sequence of operations to be performed on its arguments. However, it is quite common that we do not write all the parentheses in such expressions. Instead, we use some rules of precedence and associativity, that make the sequence clear. For example, the expression
a + b * c

conventionally stands for


a + (b * c)

and not for


(a + b) * c

The reason is that the multiplication operator has higher precedence than the addition operator. This means that * attracts the common operand b more forcibly than + does. As a result, b becomes an operand for * and not for +. Note that in general these two expressions evaluate to different values. For example, 40 + (15 * 7) equals 145, whereas (40 + 15) * 7 evaluates to 385. It is, therefore, necessary that when we write 40 + 15 * 7, we precisely understand which way we plan to resolve the ambiguity.

In order to explain another source of ambiguity, let us look at the expression


a - b - c

Now the common operand b belongs to two same operators (subtraction). They have the same precedence. Now we can evaluate this as
(a - b) - c

or as
a - (b - c)

Again the two expressions may evaluate to different values. For example, (40 - 15) - 7 is 18, whereas 40 - (15 - 7) is 32. The convention is that the first interpretation is correct. In other words, the subtraction operator is left-associative. C is no exception to these conventional interpretations. You need not fully parenthesize a composite expression. C applies the standard precedence rules for evaluating the expression. The following table describes the precedence and associativity rules for all the arithmetic and bitwise operators introduced so far. The table lists operators from higher to lower precedences, i.e., operators at later rows have lower precedences than operators at earlier rows. Operator(s)
++ * + << & | = += -= ^ / >> -~ %

Type

Associativity

unary non-associative unary binary binary binary binary binary right left left left left left right Course home

*= etc. binary

CS13002 Programming and Data Structures

Spring semester

Input/Output
This is yet another imperative feature of C. Reading values from the user and printing values to the terminal impart a sequential flavor to the program. If you print a variable and then do some computation, you get some output. Instead, if you do the computations first and then print the same variable, you may get a different output. It is very essential that you understand the precise flow of execution of a C program. Well, so far you have encountered only flat sequences of statements executed one-by-one from top to bottom. Things start getting complicated as you encounter jump instructions (conditionals, loops and function calls). For effective computation you need these jump instructions. Imperative programming may be a complete mess, unless you understand the control flow thoroughly.

Standard input/output
This is the direct method of communicating with the user, namely, reading from and writing to the terminal. Here are the basic primitives for doing these. scanf Read from the terminal. printf Write to the terminal.

Scanning values
The usage procedure for scanf is as follows:
scanf(control string, &var1, &var2, ...);

The primitive scanf waits for the user to enter a value by the keyboard. After the user writes a value and hits the enter button, the value goes to the memory location allocated to the variable specified. So scanf is another way of assigning values to variables. The control string specifies the data type that is to be read from the terminal. Here is a list of the most used formats:
%d %o

Read an integer in decimal. Read an integer in octal.

%x,%X

Read an integer in hexadecimal. Read an integer in decimal/octal/hex. If the integer starts with 0x or 0X, treat it as a hexadecimal integer, else if it starts with 0, treat it as an octal integer, otherwise treat it as a decimal integer. Read an unsigned integer in decimal.

%i

%u

%hd,%hi,%ho,%hu,%hx,%hX Read a short integer. %ld,%li,%lo,%lu,%lx,%lX Read a long integer.

Read a long long integer. This is not an ANSI C feature, but works well in Linux. Replacing L by ll (i.e., using %Ld,%Li,%Lo,%Lu,%Lx,%LX %lld, %lli, etc.) continues to work in Linux and may be better ported to other architectures. Some compilers also support %q (quad).
%f %e %lf,%le %Lf,%Le %c %s

Read a float. Read a float in the scientific (exponential) notation. Read a double. Read a long double. Read a single character. Read a string of characters.

Example
int a; unsigned long b; float x, y; char c, s[64]; scanf("%d",&a); /* scanf("%x",&b); /* scanf("%f",&x); /* notation */ scanf("%e",&y); /* scientific notation */ scanf(" %c",&c); /* scanf(" %s",s); /* /* needed */ Read the integer a in decimal */ Read the integer b in hexadecimal */ Read the floating point number x in decimal Read the floating point number y in the Read the character c */ Read a string and store in s */ For reading strings the ampersand (&) is not

Suppose that the user enters the following values:


123 123 -123.456 1.23e-6 a Hey! I am your instructor.

Most of the readings go as expected. a receives the decimal value 123, b receives 0x123 (which is 291 in decimal), x and y respectively receive -123.456 = -1.23456e2 and 1.23e-6 = 0.00000123. Also c obtains the value 'a' (whose ASCII value is 97). However, a problem comes with the string s: it receives the value "Hey!" only. The rest of the input is lost! The situation is actually worse: the rest is not lost. The computer remembers this part and supplies this to the next scanf, if any is executed. Why does it occur? The reason is: scanf stops reading as soon as it encounters a white character (space, tab, new line, etc.). You have to do something more complicated in order to read strings with spaces. Note also that the scanning of c requires a space before the %c. This is for consuming the space following the value of y given by the user. The same applies to the reading of s. Reading characters and strings is often too painful in C. Here are the basic rules:
scanf stops reading as soon as it encounters a white character. The trailing white

character remains in the input stream. Leading white characters are ignored, when numbers are read. White characters are characters and so are not ignored, when characters and strings are read.

You can read more than one variables in a single scanf. The six scanf's for the last example can be combined as:
scanf("%d %x %f %e %c %s", &a, &b, &x, &y, &c, s);

Spaces are ignored before numbers. So the statement


scanf("%d%x%f%e %c %s", &a, &b, &x, &y, &c, s);

has the same effect. You may use other separators instead of space. For example, the following statement
scanf("%d,%x,%f,%e,%c,%s", &a, &b, &x, &y, &c, s);

requires you to enter the input values as:


123,123,-123.456,1.23e-6,a,Hey! I am your instructor.

In all these examples, the string s is assigned the value "Hey!". Use the fgets primitive (see below) to repair this. It is also a queer thing to use & in every argument except strings. This is because scanf is a function. In C, every function call is of the type call-by-value. In order to see the desired effects (assignments of the arguments by the values given by the user), we need to pass addresses of the variables. A string (character array) is, however, already an address (a pointer), so we don't require an extra &. All these concepts will gradually be clear, as you understand more and more of the idiosyncracies of C. For the time being just rehearse and memorize the following two lines:

You need ampersands for all things, Unless you are scanning strings.

Printing values
Printing is remarkably neater than scanning. No ampersands. And printf prints precisely what you ask it to do. The basic syntax is very similar to scanf.
printf(control string, arg1, arg2, ...);

This directive causes the program to print the values of the arguments arg1, arg2, ... to the terminal following the format specified in the control string. The control string may contain (almost) any sequence of characters with special escape sequences (starting with percents) that determine how to print the arguments. The argumnets, on the other hand, specify what to print. Here is a list of the basic escape sequences:
%d,%i %o %x

Print an integer in decimal. Print an integer in octal. Print an integer in hexadecimal. Use the digits 0,1,...,9,a,b,c,d,e,f. Same as %x except that the digits 0,1,...,9,A,B,C,D,E,F are used. Print an unsigned integer in decimal.

%X %u

%hd,%hi,%ho,%hu,%hx,%hX Print a short integer. %ld,%li,%lo,%lu,%lx,%lX Print a long integer. %Ld,%Li,%Lo,%Lu,%Lx,%LX %f %e %E

Print a long long integer. (Not in ANSI C. ll may be used in place of L. Some compilers support %q.) Print a float in decimal. Print a float in the scientific (exponential) notation. Same as %e except that E is used to denote the exponent. Print a float in hexadecimal. Digits 0,1,...,9,a,b,c,d,e,f are used and the exponent indicator is p. Same as %a except that A,B,C,D,E,F and P are used. Print a double. Print a long double. Print a single character.

%a

%A %lf,%le,%lE,%la,%lA %Lf,%Le,%LE,%La,%LA %c

%s %% \"

Print a string of characters. Print a literal %. Print a literal double quote ".

Example: Suppose you want to print the scanned values from the notorious scanf example.
int a; unsigned long b; float x, y; char c, s[64]; scanf("%d %x %f %e %c %s", &a, &b, &x, &y, &c, s); printf("a printf("b printf("x printf("y printf("c printf("s = = = = = = %d = 0x%x\n", a, a); %d = 0x%x\n", b, b); %f = %e\n", x, x); %f = %e\n", y, y); '%c' = %d\n", c, c); %s\n", s);

If you supply the inputs


123 123 -123.456 1.23e-6 a Hey! I am your instructor.

the printf statements print the following lines:


a b x y c s = = = = = = 123 = 0x7b 291 = 0x123 -123.456001 = -1.234560e+02 0.000001 = 1.230000e-06 'a' = 97 Hey!

Once again you may combine several printf's in a single statement. For example, the same output is produced by the following:
printf("a = %d = 0x%x\nb = %d = 0x%x\n", a, a, b, b); printf("x = %f = %e\ny = %f = %e\n", x, x, y, y); printf("c = '%c' = %d\ns = %s\n", c, c, s);

Here look at the dual meaning of characters. When viewed as a character, it looks like a; when viewed as an integer, it looks like 97. During printf no values are assigned. So printf can legally handle printing values of expressions. Thus an argument of printf can be any valid expression. For example, the following snippet
int a = -3, b = 5;

printf("expression1 = %d, and ", a / (a + b)); printf("expression2 = %f.\n", (float)a / (float)(a + b)); printf("That's all!\n");

prints
expression1 = -1, and expression2 = -1.500000. That's all!

There is a funny thing about printf. It indeed returns a value, namely, the number of characters printed. Here is an example:
int a = -3, b = 5; int n; n = printf("expression1 = %d, and ", a / (a + b)); n += printf("expression2 = %f.\n", (float)a / (float)(a + b)); n += printf("That's all!\n"); printf("Total number of characters printed before this line = %d\n", n);

The output is
expression1 = -1, and expression2 = -1.500000. That's all! Total number of characters printed before this line = 59

How come? You can see only 57 printed characters. Yep! You forgot to count the newline characters at the end of the first two lines.

File input/output
So far you have seen examples of I/O from/to the terminal. This is a special case of what is called file I/O. You can read from or write to any file using built-in functions that have call syntaxes very similar to the standard I/O calls. In order to use a file you must first open a file pointer or a file descriptor. The fopen call can be used for that. Here are the three basic ways of opening a file descriptor.
FILE *ifp, *ofp1, *ofp2; ifp = fopen("foo.in","r"); mode */ ofp1 = fopen("bar1.out","w"); mode */ ofp2 = fopen("bar2.out","a"); mode */ /* Declare FILE pointers */ /* Open the file "foo.in" in read /* Open the file "bar1.out" in write /* Open the file "bar2.out" in append

Once the file pointers are opened, they can be used for reading from or writing to the named files. For the last example, the file "foo.in" is opened in the "read" mode, i.e., you can read from the file "foo.in". The file "bar1.out", on the other hand, is opened in the

"write" mode. The file, if existent, is rewritten, else a new file in the name "bar1.out" is opened. You can write whatever you like to this file. Finally, the file "bar2.out" is opened in the append mode. You can write to the file "bar2.out". However, writing starts at the end. This means that if a file with the name "bar2.out" already exists, then its content is left unaltered, but now you get the facility to write to this file starting from the end of the file. If "bar2.out" didn't exist, one new file is created with this name and you can now start writing to it. Reading from and writing to a file can be effected only via the FILE pointers opened. The fopen call simply associates a file name and an access mode with a FILE pointer. If ifp is a FILE pointer opened in the read mode, you can read from it using the directive:
fscanf(ifp, control string, &var1, &var2, ...);

Here control string and the arguments are to be used exactly in the same way as explained in connection with scanf. Similarly, if ofp is a FILE pointer opened in the write or append mode, one can use the following call for writing to the file:
fprintf(ofp, control string, expr1, expr2, ...);

Like printf, the control string specifies how to print and the arguments expr1, expr2, ... indicate what to print. When your program starts execution, three FILE pointers are opened by default. The standard input stdin is opened in the read mode for scanning values from the terminal. The standard output stdout and standard error stderr descriptors are opened in the append mode. Both are meant for writing to the terminal. With special shell commands one can separate out the two output streams. In Unix-like platforms almost everything under the sun is treated as a file. Hard disk files look like files, but the terminal is also a file and can be read from and written to. In fact, the call
scanf(control string, &var1, &var2, ...);

is equivalent to the call


fscanf(stdin, control string, &var1, &var2, ...);

Similarly, the call


printf(control string, expr1, expr2, ...);

is equivalent to the call

fprintf(stdout, control string, expr1, expr2, ...);

There are a lot of other things that you can do using FILE pointers. We won't go into the details here. We only mention a new call to do something useful: reading a string with spaces. The call goes like this:
fgets(str, n, ifp);

Here str is a character array, n a positive integer, and ifp a FILE pointer opened in the read mode. The call reads an entire line from the FILE pointer ifp and stores the line with a trailing NULL character ('\0') in the string str. If the line in the input file is bigger than n characters, then only n-1 characters are read and stored in str together with the trailing NULL character. The array str should be large enough to accommodate n characters. Using a smaller array may corrupt memory and/or raise segmenation faults. But what about reading an entire line from the terminal, as our original problem was? You still wonder how! That's damn easy:
fgets(str, n, stdin);

Period! Nay, semi-colon; Once you are through working with a FILE pointer fp and do no longer require it, you may explicitly close the pointer using the call:
fclose(fp);

When your program terminates, all opened pointers (including the standard ones) are closed. Doing it explicitly is a matter of good programming etiquette and is on esoteric situations needed for your survival. Every system imposes a restriction on the maximum number of FILE pointers that can be opened simultaneously. This upper bound is compiler-dependent and is usually not very high. If this value is 16, and you need to access 25 files, and if we assume you do not need to access all these 25 files simultaneously, it is advantageous to close unused FILE pointers. These closed descriptors may be reassigned in a subsequent fopen call.

String input/output
Now I/O from/to a string. The concepts are similar. Use the sscanf and sprintf calls.
sscanf(str, control string, &var1, &var2, ...); sprintf(str, control string, expr1, expr2, ...);

Example: Here is a simple sscanf example:


char str[] = "53 -123.456 @"; int a; float b; char c;

sscanf(str,"%d %f %c", &a, &b, &c); printf("a = %d\nb = %f\nc = %d\n", a, b, c);

This snippet generates the output:


a = 53 b = -123.456001 c = 64

Example: Here is a simple sprintf example:


char str[128]; sprintf(str, "%lu %e\n", 521lu << 9 , 521.0 * 512.0); fprintf(stdout, "%s", str);

The output is
266752 2.667520e+05

Example: Now here is a deeply illustrating example:


int a = -3, b = 5; char str[128], *cptr; cptr = str; cptr += sprintf(cptr,"expression1 = %d, and ", a / (a + b)); cptr += sprintf(cptr,"expression2 = %f.\n", (float)a / (float)(a + b)); cptr += sprintf(cptr,"That's all!\n"); printf("%s", str);

You get the output:


expression1 = -1, and expression2 = -1.500000. That's all!

Don't ask us to explain now what this code does. Let us wait till you mature as a C programmer in order to understand, assimilate and eventually appreciate the big idiosyncracies of C, its pointer arithmetic, its arrays, bla bla bla. There is no hurry indeed. Oh, didn't I mention that like printf, both fprintf and sprintf return the number of characters printed? Furthermore, each of scanf, fscanf and sscanf returns an integer value. Read your system's manual if you have to know what this return value stands for.

Formatted input/output
You can control the format of printed output using special directives. Using these extra directives helps you, for example, to generate nicely aligned lines. All you have to do is

to insert a number between the % and the subsequent type specifier (d,x,f,s, etc.). The following table summarizes some of these options. Here n and m are assumed to be positive integer values. Format
%nd,%ni, %nu,%nld, %nLd, etc.

Description Print an integer in the decimal notation using at least n characters. If the decimal representation of the integer is of length l < n (including the sign for negative integers), then n - l spaces are printed and then the integer is printed. If l >= n, then this directive is similar to the simple %d. This is similar to %nd except that the extra spaces, if any, are printed after the integer. In short, %nd yields right-justified output, whereas %nd yields left-justified output. Same as %nd and %-nd, except that the integer is printed in octal. Same as %nd and %-nd, except that the integer is printed in hexadecimal. Print a right-justified real number (in the decimal notation) with a total

%-nd,%-ni, %-nu, etc. %no,%-no %nx,%-nx, %nX,%-nX

%n.mf,%n.mlf, of n characters (including the decimal pointer and the sign) and with m %n.mLf characters to the right of the decimal point. If the float value cannot be

printed in the recommended space, then %n.mf prints as %f does.


%-n.mf,%n.mlf, %-n.mLf

Same as %n.mf, except that the printing is left-justified. Print a right-justified string using a total of n characters. If the original string is bigger than or equal to the recommended number n, then %ns prints as does %s. This is the same as %ns except that the output is left-justified, i.e., extra spaces, if any, are printed after the string.

%ns

%-ns

Example: For the following formatted print statements


printf("{%2d} {%3d} {%4d} {%-2d} {%-3d} {%-4d}\n", 123, 234, 345, 456, 567, 678); printf("{%2x} {%3x} {%4x} {%-2x} {%-3x} {%-4x}\n", 123, 234, 345, 456, 567, 678); printf("{%2s} {%3s} {%4s} {%-2s} {%-3s} {%-4s}\n", "abc", "bcd", "cde", "def", "efg", "fgh"); printf("{%4.2f} {%5.2f} {%6.2f} {%-5.2f} {%-6.2f} {%-7.2f}\n", 1.2345, 2.3456, 3.4567, -4.5678, -5.6789, -6.7890);

the output looks like:


{123} {234} { 345} {456} {567} {678 }

{7b} { ea} { 159} {1c8} {237} {2a6 } {abc} {bcd} { cde} {def} {efg} {fgh } {1.23} { 2.35} { 3.46} {-4.57} {-5.68 } {-6.79

Example: io1.c The program


#include <stdio.h> main () { char name1[64] = "Abhijit Das", name2[64] = "Chittaranjan Mandal", name3[64] = "Sandeep Sen"; char dept1[4] = "CSE", dept2[4] = "SIT", dept3[4] = "CSE"; int room1 = 123, room2 = 6, room3 = 301; float height1 = 1.7781, height2 = 1.7399, height3 = 1.7412; int lucky1[2] = { 561, 1729 }, lucky2[2] = { 28, 496 }, lucky3[2] = { -1073741789, 104729}; printf(" %10s %20s %s", "Name", "Department", "Room No"); printf(" Height Lucky numbers\n"); printf(" +------------------------------------------------------------------------+\n"); printf(" | %-20s", name1); printf("%7s ",dept1); printf(" %-2d",room1); printf("%9.2f",height1); printf(" %11d and %-7d|", lucky1[0], lucky1[1]); printf("\n"); printf(" | %-20s", name2); printf("%7s ",dept2); printf(" %-2d",room2); printf("%9.2f",height2); printf(" %11d and %-7d|", lucky2[0], lucky2[1]); printf("\n"); printf(" | %-20s", name3); printf("%7s ",dept3); printf(" %-3d",room3); printf("%9.2f",height3); printf(" %11d and %-7d|", lucky3[0], lucky3[1]); printf("\n"); printf(" +------------------------------------------------------------------------+\n"); }

The output
Name Department Room No Height Lucky numbers +------------------------------------------------------------------------+

| Abhijit Das CSE 123 1.78 561 and 1729 | | Chittaranjan Mandal SIT 6 1.74 28 and 496 | | Sandeep Sen CSE 301 1.74 -1073741789 and 104729 | +------------------------------------------------------------------------+

Course home

CS13002 Programming and Data Structures

Spring semester

Conditions and branching


Now we will break our impasse of monolithically executing statements after statements from top to bottom. We add jumps inside the program. We still continue with the basic top-to-bottom flow, but will now allow leaving out some sections conditionally. Think about mathematical definitions like the following. Suppose we want to assign to y the absolute value of an integer (or real number) x. Mathematically, we can express this idea as:
y = 0 x -x if x = 0, if x > 0, if x < 0.

From a programmer's point of view this means that if x = 0, we can blindly assign to y the constant 0. If x is non-zero but positive, we can simply copy x to y. Finally, if x is negative, we have to take the unary minus of it and assign that negated value to y. In other words, depending on the value of x we have to do different things. At a particular time, x can have only one value and exactly one of the three possibilities need be executed. However, at different times x may have different values, and so our program should be able to handle all possibilities. This exemplifies what is called a selective structure in a program. As another example, let us define the famous Fibonacci numbers:
Fn = 0 if n = 0, 1 if n = 1, Fn-1 + Fn-2 if n >= 2.

Now you are again in a similar situation. Depending on the value of n, you have three options. If n = 0 or 1, you immediately know what the corresponding Fibonacci number is. If n is bigger than 1, you compute the two previous Fibonacci numbers and add them up. How to compute the previous two numbers? Once again you check which of the three given conditions hold. And then repeat. Right now we will not study repetitive structures, but only mention that the possibility of repetition is dictated by the value of n. If your program has to work in such a conditional world, you need two constructs:

A way to specify conditions (like x < 0, or n >= 2). A way to selectively choose different blocks of statements depending on the outcomes of the condition checks.

Logical conditions
Let us first look at the rendering of logical conditions in C. A logical condition evaluates to a Boolean value, i.e., either "true" or "false". For example, if the variable x stores the value 15, then the logical condition x > 10 is true, whereas the logical condition x > 100 is false.

Comparing two variables


The usual mathematical relations comparing two expressions E1 and E2 can be implemented in C as the following table illustrates: Relational operator
== != < <= > >=

Usage

Condition is true iff

E1 == E2 E1 and E2 evaluate to the same value E1 != E2 E1 and E2 evaluate to different values E1 < E2 E1 evaluates to a value smaller than E2 E1 <= E2 E1 evaluates to a value smaller than or equal to E2 E1 > E2 E1 evaluates to a value larger than E2 E1 >= E2 E1 evaluates to a value larger than or equal to E2

The equality checker is == and not the single =. Recall that = is the assignment operator. In a place where a logical condition is expected, an assignment of the form E1 = E2 makes sense and could be a potential source of bugs. Example: Let x and y be integer variables holding the values 15 and 40 at a certain point in time. At that time, the following truth values hold:
x == y x != y y % x == 10 600 < x * y 600 <= x * y 'B' > 'A' x / 0.3 == 50 False True True False True True False (due to floating point errors)

What is Boolean value in C?


A funny thing about C is that it does not support any Boolean data type. Instead it uses any value (integer, floating point, character, etc.) as a Boolean value. Any non-zero value of an expression evaluates to "true", and the zero value evaluates to "false". In fact, C allows expressions as logical conditions. Example:

0 1 6 - 2 * 3 (6 - 2) * 3 0.0075 0e10 'A' '\0' x = 0 x = 1

False True False True True False True False False True

The last two examples point out the potential danger of mistakenly writing = in place of ==. Recall that an assignment returns a value, which is the value that is assigned.

Logical operators
Logical operators are used to combine multiple logical conditions. In the following table C, C1 and C2 are assumed to be logical conditions (including expressions). Logical operator Syntax AND OR NOT Example:
(7*7 < 50) && (50 < 8*8) (7*7 < 50) && (8*8 < 50) (7*7 < 50) || (8*8 < 50) !(8*8 < 50) ('A' > 'B') || ('a' > 'b') ('A' > 'B') || ('A' < 'B') ('A' < 'B') && !('a' > 'b') True False True True False True True

True if and only if

C1 && C2 Both C1 and C2 are true C1 || C2 Either C1 or C2 or both are true !C C is false

Notice that here is yet another source of logical bug. Using a single & and | in order to denote a logical operator actually means letting the program perform a bit-wise operation and possibly ending up in a logically incorrect answer. Let us now review the question of precedence and associativity of relational and logical operators. The following table summarizes the relevant details with precedence decreasing downwards. Operator(s)
! < <= >

Type Associativity Unary Right Left

>= Binary

==

!=

Binary Binary Binary

Left Left Left

&& ||

Example:
x <= y && y <= z || a >= b is equivalent to ((x <= y) && (y <= z)) || (a >= b). C1 && C2 && C3 is equivalent to (C1 && C2) && C3. a > b > c is equivalent to (a > b) > c.

Let us now see how we can use conditions to write selective structures in C.

The if statement
Imagine a situation like this:
Do PDS lab; Have snacks; If it does not rain, play soccer; Solve Maths assignment; Enjoy dinner;

In this example, playing soccer is dependent on rain. If it is not rainy, play soccer, else skip it and continue with your remaining pending work. A situation like this is described in the following figure:

This kind of structure can be rendered in C as follows:


if (Condition) { Block 1 }

Here "block" means a sequence of statements. If the block consists of a single statement, the braces may be omitted. Suppose you scan an integer x from the user and then replace it with its absolute value. if x is bigger than or equal to 0, there is nothing to do. If x is negative, replace it by -x.
scanf("%d",&x);

if (x < 0) x = -x;

Animation example : one-way branching

The if-else statement


Now suppose you are adamant to play something after your PDS lab.
Do PDS lab; Have snacks; If it does not rain, play soccer, otherwise play table tennis; Solve Maths assignment; Enjoy dinner;

This is an example of a situation depicted in the figure below:

This kind of structure can be rendered in C as follows:


if (Condition) { Block 1 } else { Block 2 }

If a block consists of a single statement, the corresponding braces may be omitted. Suppose you scan an integer x from the user and assign to y the absolute value of x. if x is bigger than or equal to 0, then simply copy x to y. If x is negative, copy -x to y.

scanf("%d",&x); if (x >= 0) y = x; else y = -x;

Animation example : two-way branching

Interactive animation : two-way branching

Consider the following special form of the if-else statement:


if (C) v = E1; else v = E2;

Here depending upon the condition C, the variable v is assigned the value of either the expression E1 or the expression E2. This can be alternatively described as:
v = (C) ? E1 : E2;

Here is an explicit example. Suppose we want to compute the larger of two numbers x and y and store the result in z. We can write:
z = (x >= y) ? x : y;

Nested if statements
A block of an if or if-else statement may itself contain one or more if and/or if-else statements. Suppose that we want to compute the absolute value |xy| of the product of two integers x and y and store the value in z. Here is a possible way of doing it:
if (x >= 0) { z = x; if (y >= 0) z *= y; else z *= -y; } else { z = -x; if (y >= 0) z *= y; else z *= -y; }

This can also be implemented as:


if (x >= 0) z = x; else z = -x; if (y >= 0) z *= y; else z *= -y;

Here is a third way of doing the same:


if ( ((x >= 0)&&(y >= 0)) || ((x < 0)&&(y < 0)) ) z = x * y; else z = -x * y;

Animation example : nested branching

Interactive animation : nested branching

Interactive animation : max of three elements

Multi-way branching
Now think of your evening schedule like the following:
Do PDS lab; Have snacks; If it does not rain, play soccer, otherwise if the common room is free, play table tennis, otherwise if your friend is available, play Scrabble; otherwise play guitar; Solve Maths assignment; Enjoy dinner;

This is generalized in the following figure:

Repeated if-else statements


A structure of the last figure can be translated into C as:
if (Condition 1) { Block 1 } else if (Condition 2) { Block 2 } else if ... ... } else if (Condition n) { Block n } else { Block n+1

Here is a possible implementation of the assignment y = |x|:


scanf("%d",&x); if (x == 0) y = 0; else if (x > 0) y = x; else y = -x;

Animation example : three-way branching The switch statement


If the multi-way branching is dependent on the value of a single expression, one can use the switch statement. For example, assume that in the above figure Condition i stands for (E == vali), where E is an expression and vali are possible values of the expression for i=1,2,...,n. One can write this as:
switch (E) { case val1 : Block 1 break; case val2 : Block 2 break; ... case valn : Block n break; default: Block n+1 }

Suppose you plan to write a multilingual software which prompts a thanking message based on the language. Here is an implementation:
char lang; ... switch (lang) case 'E' : case 'F' : case 'G' : case 'H' : case 'I' : case 'J' : case 'K' : default : }

{ printf("Thanks\n"); break; printf("Merci\n"); break; printf("Danke\n"); break; printf("Shukriya\n"); break; printf("Grazie\n"); break; printf("Arigato\n"); break; printf("Dhanyabaadagaru\n"); break; printf("Thanks\n");

The switch statement has a queer behavior that necessitates the use of the break statements. It keeps on checking if the value of the top expression matches the case

values. Once a match is found, further comparisons are disabled and all following statements before the closing brace are executed one by one.

Animation example : switch


In order to avoid this difficulty, you are required to put additional break statements as and when required. This statement causes the program to leave the switch area without proceeding further down the area.

Animation example : switch with break

Interactive animation : switch with break

There are, however, situations where this odd behavior of switch can be exploited. Let us look at an artificial example. Suppose you want to compute the sum
n + (n+1) + ... + 10

for n in the range 0<=n<=10. For other values of n, an error message need be printed. The following snippet does this.
sum = 0; switch (n) case 0 case 1 case 2 case 3 case 4 case 5 case 6 case 7 case 8 case 9 case 10 { : : : : : : : : : : :

sum += 1; sum += 2; sum += 3; sum += 4; sum += 5; sum += 6; sum += 7; sum += 8; sum += 9; sum += 10; break; default : printf("n = %d is not in the desired range...\n", n);

Course home

CS13002 Programming and Data Structures

Spring semester

Loops and iteration


This is the first time we are going to make an attempt to move backward in a program. Loops make this backward movement feasible in a controlled manner. This control is imparted by logical conditions. Many problem-solving strategies involve repetitive steps. For example, suppose we want to compute the sum of n numbers. A specific instance of this problem is the computation of the harmonic number:
Hn = 1/1 + 1/2 + 1/3 + ... + 1/n.

A reasonable strategy to solve this problem is to start with a sum initialized to zero and then let each 1/i be added to the sum. When all of the n summands are added, the number accumulated in the sum is the desired output. Here the repetitive part is the addition of the next number to the current sum. So there should be a way to identify the "next" number. Moreover, when there are no more (next) numbers, the repetitive addition should stop. Here is a high-level description of this process:
Initialize sum to 0. for each i in the set {1,2,...,n} add 1/i to sum. Report the accumulated sum as the output value.

Here i identifies the "next" number and when all of the n possible values of i have been tried out, the repetitive addition process should stop.

Mathematical induction
Let us formalize the above notion of repetitive calculation. The following sets occur frequently in the study of computer science. So let us designate them by some special symbols. Symbol N N0 Z Set
{1,2,3,4,...} {0,1,2,3,...} {...,-3,-2,1,0,1,2,3,...}

Description The set of natural numbers (i.e., positive integers) The set of non-negative integers The set of integers

Theorem: [Principle of mathematical induction] Let S be a subset of N with the following properties: (1) 1 is in S. (2) Whenever n is in S, so also is n+1. Then S = N. A similar theorem holds for the set N0: Theorem: [Principle of mathematical induction] Let S be a subset of N0 with the following properties: (1) 0 is in S. (2) Whenever n is in S, so also is n+1. Then S = N0. The principle of mathematical induction can be stated in various equivalent forms. The following is one such equivalent statement. Theorem: [Principle of strong mathematical induction] Let S be a subset of N with the following properties: (1) 1 is in S. (2) Whenever 1,2,...,n are in S, so also is n+1. Then S = N. For N0 this can be stated as: Theorem: [Principle of strong mathematical induction] Let S be a subset of N0 with the following properties: (1) 0 is in S. (2) Whenever 0,1,2,...,n are in S, so also is n+1. Then S = N0. Let us now see how we can exploit these principles for designing iterative algorithms. First look at the the computation of harmonic numbers. For the sake of simplicity let us also define:
H0 = 0.

Let S denote the set of all non-negative integers for which Hn can be computed. I will show that S = N0. First, H0 is equal to the constant 0 and so can be computed; so 0 is in S. Now suppose that Hn can be computed for some n>=0, i.e., n is in S. It is easy to see that
Hn+1 = Hn + 1/(n+1).

But then since 1/(n+1) is also computable, adding this quantity to Hn gives us a way to compute Hn+1, i.e., n+1 is in S too. Hence by the principle of mathematical induction S = N0. This argument not only establishes that Hn can be computed for all non-negative n, but also suggests a way to compute Hn. Here is the algorithm:
Set i = 0 and H0 = 0. For i = 1,2,3,...,n in that order compute Hi = Hi-1 + 1/i.

We will shortly see how this English description can be translated to a C program. For the time being let us concentrate on some other examples. The greatest common divisor gcd(a,b) of two non-negative integers (not both 0) is defined as the largest natural number of which both a and b are integral multiples. The standard gcd algorithm taught in schools is based on successive Euclidean division. Let us try to render it as a sequence of repetitive computations. For the sake of simplicity, we assume that whenever we write gcd(a,b) we mean a>=b. The first step is to show that gcd(a,b) is computable for all a>=1 and for all b in the range 0<=b<=a. We proceed by induction on a. If a=1, we must have b=0 or b=1. Now both gcd(1,0) and gcd(1,1) equal the constant 1. For the inductive step assume that gcd(a',b') is computable whenever a'<a, and that we want to compute gcd(a,b). We use the following theorem: Theorem: [Euclidean gcd theorem] Let a,b be positive integers and r = a rem b. Then gcd(a,b) = gcd(b,r). Let us now come back to the inductive step. If a is an integral multiple of b, we have r=0, and so by the theorem gcd(a,b)=gcd(b,0)=b. If a is not an integral multiple of b, we cannot have a=b, i.e., now a>b. By the induction hypothesis gcd(b,r) is computable. Also the remainder r is computable from a and b. Therefore, gcd(a,b) is also computable. This proof also leads to the following iterative algorithm:
As long as b is not equal to 0 do the following: Compute the remainder r = a rem b. Replace a by b and b by r. Report a as the desired gcd.

Recursive definitions
A sequence an for all n in N (or N0) can often be inductively (also called recursively) defined. For example, the sequence of harmonic numbers is defined as:

H0 = 0, Hn = Hn-1 + (1/n) for n>=1.

The sequence an = n2 can be recursively defined as:


a0 = 0, an = an-1 + 2n - 1 for n>=1.

Also recall the definition of Fibonacci numbers:


F0 = 0, F1 = 1, Fn = Fn-1 + Fn-2 for n>=2.

In all these cases the recursive definition provides a way to compute an element in the sequence from one or more elements that occur earlier in the sequence. The basis cases specify the terminal conditions, where constant values are to be used for the initial elements of the sequence. More complicated entities may also be defined recursively. For example, suppose we want to obtain the list of all permutations of the natural numbers 1,2,...,n. If n=1, the list consists of a single element. For n>=2, we inductively compute the list of permutations of 1,2,...,n-1. Then for each permutation in the list we insert n in any one of the n allowed positions. One can easily check that in this way we can generate all the permutations of 1,2,...,n with each such permutation generated exactly once. Induction proves to be a useful methodology for attacking problems. In the first place, it is procedural, i.e., it often leads to good algorithms for computing recursively defined objects. Second, the idea of reduction of a problem into one or more smaller problems and then combining the solutions of the subproblems to obtain the solution of the original problem turns out to be extremely useful for designing algorithms. We will come back again to a discussion of recursion in connection with functions.

Loops
It is high time now that we concentrate on the realization of repetitive structures in C. In the C terminology these are called loops.

Pre-test and post-test loops


Loops can be broadly classified in two categories based on the location where the condition for looping back is checked. In a pre-test loop the condition for entering the body of the loop is checked at the beginning (top) of the loop. If the condition is satisfied, execution enters the body and proceeds sequentially. After the entire body is executed, control comes back unconditionally to the top of the loop. Meanwhile, the body might have changed the

world in a way so as to affect the continuation condition. If it is still satisfied, the loop is entered once more, and the body is again executed and control again comes back to the top of the loop. If the condition at the top is not satisfied, the loop body is ignored and the control of execution goes to the area after the end of the loop.

Figure: Pre-test loop In a post-test loop, on the other hand, the control of execution enters the loop body unconditionally. After the entire body is executed, the loop condition is checked. If it is satisfied, control goes back to the top of the loop, the body is again executed and the continuation condition is again checked. This process is repeated until the continuation

condition becomes false. In that case, control leaves the loop and proceeds further down the code.

Figure: Post-test loop It is important to note that for a post-test loop the loop body is executed at least once, since control enters the body unconditionally. On the other hand, in a pre-test loop the loop body need not be executed at all. If the continuation condition is false initially, the entire loop is ignored.

while loops
The pre-test loop of the above figure can be rendered in C as follows:

while (the continuation condition is true) { execute loop body; }

If the loop body consists of a single statement, the braces may be omitted. Example: As a specific example of a while loop, let us implement the gcd algorithm using repeated division.
while (b > 0) { r = a % b; /* Compute the next remainder */ a = b; /* Replace a by b */ b = r; /* Replace b by r */ } printf("gcd = %d\n",a);

Animation example : while loop Interactive animation : while loop Interactive animation : gcd Interactive animation : Euclidean gcd

Example: Here is how the n-th harmonic number may be computed using a while loop. Note that Hi can be generated from Hi-1 and i. So it is not necessary to store all the values H0,H1,...,Hi-1. Remembering only the previous harmonic number suffices.
float i, H; unsigned int n; ... i = 0; H = 0; while (i < n) { ++i; /* Increment i */ H += 1.0/i; /* Update the harmonic number accordingly */ } printf("H(%d) = %f\n", n, H);

Example: Finally, let us look at the computation of Fibonacci numbers using while loops.
i = 1; /* Initialize i to 1 */ F = 1; /* Initialize Fi */ p1 = 0; /* Initialize Fi-1 */ while (i < n) { ++i; /* Increment i */ p2 = p1; /* The old Fi-1 now becomes Fi-2 */ p1 = F; /* The old Fi now becomes Fi-1 */ F = p1 + p2; /* Compute Fi from Fi-1 and Fi-2 */ } printf("F(%d) = %d", n, F);

do-while loops

The do-while loop of C is a post-test loop. It has the following syntax:


do { execute loop body; } while (continuation condition is true);

Example: Harmonic numbers can be computed using the do-while loop as:
float i, H; unsigned int n; ... i = 0; H = 0; do { ++i; /* Increment i */ H += 1.0/i; /* Update the harmonic number accordingly */ } while (i < n); printf("H(%d) = %f\n", n, H);

Example: The following loop computes the gcd of two integers a,b with 1<=b<=a.
do { r = a % b; /* Compute the next remainder */ a = b; /* Replace a by b */ b = r; /* Replace b by r */ } while (b > 0); printf("gcd = %d\n",a);

Example: Finally, here is an implementation of the computation of Fibonacci numbers Fi for i>=2.
i = 1; /* Initialize i to 1 */ F = 1; /* Initialize Fi */ p1 = 0; /* Initialize Fi-1 */ do { ++i; /* Increment i */ p2 = p1; /* The old Fi-1 now becomes Fi-2 */ p1 = F; /* The old Fi now becomes Fi-1 */ F = p1 + p2; /* Compute Fi from Fi-1 and Fi-2 */ } while (i < n); printf("F(%d) = %d", n, F);

Animation example : do-while loop Interactive animation : do-while loop for loops
These are pre-test loops and have the following syntax:
for ( initialize loop; continuation condition ; loop increment ) { execute loop body;

Here the loop initialization step consists of a set of book-keeping task that need be carried out before entering the loop, and the loop increment step refers to a set of tasks carried out at the end of the loop body just before the continuation condition is checked. The for loop can be equivalently described in terms of the following while loop:
initialize loop; while (continuation condition is true) { execute loop body; loop increment; }

Example: One can compute gcds using for loops as follows:


for ( ; b > 0 ; ) { r = a % b; /* Compute the next remainder */ a = b; /* Replace a by b */ b = r; /* Replace b by r */ }

Here the initialization and increment steps are empty. Example: Computation of harmonic numbers using for loops is quite simple:
H = 0; for (i=1; i<=n; ++i) H += 1.0/i; printf("H(%d) = %f\n", n, H);

Example: If more than one statements need be executed during the initialization or increment step, they should be separated by commas, since semi-colons indicate separation of the three parts of the loop control area. The Fibonacci computation may have the following form with for loops. We assume that n>=2.
for ( i = 2, p1 = 1, p2 = 0; i <= n; ++i , p2 = p1 , p1 = F ) F = p1 + p2; /* Compute Fi from Fi-1 and Fi-2 */ printf("F(%d) = %d", n, F);

Example: The following animation demonstrates an iterative computation of the sequence 12+22+...+n2.

Animation example : for loop Interactive animation : for loop Interactive animation : computation of Fibonacci numbers
Example: The following animation iteratively assigns values to the elements of an array. The i-th location receives the value (i+1)2.

Animation example : for loop with arrays


Example: [Linear search] Given an array of n elements A[0],A[1],...,A[n-1] and an element x, we want to find if x is present in the array, i.e., if x = A[i] for some i. This is the famous search problem. One obvious strategy is to compare x successively with A[0],A[1],A[2],... The algorithm stops as soon as a match is found or the array gets exhausted.

Animation example : linear search Interactive animation : linear search


The code for linear search is given below:
found = 0; for (i=0; (!found)&&(i<n); ++i) { if (A[i] == x) { printf("Match found\n"); found = 1; } } if (!found) printf("No match found\n");

Example: [Binary search] Now assume that the array A is sorted, i.e., A[0] <= A[1] <= ... <= A[n-1]. First we compare the element x with the array element A[(n-1)/2]. If x > A[(n-1)/2], then x cannot be found in the first half of the array. On the other hand, if x <= A[(n-1)/2], there is no need to search x in the second half of the array. Thus by a single comparison we discard one half of the array. We then repeat the process, every time discarding half of the remaining part of the array. Eventually, we reduce the search to a single array element and check whether x equals this element.

Animation example : binary search Interactive animation : binary search


Here is how we can translate the binary search algorithm in C:
L = 0; R = n-1; while (L < R) { M = (L + R) / 2; if (x > A[M]) L = M+1; else R = M; } if (A[L] == x) printf("Match found\n"); else printf("Match not found\n");

Loop invariants

For verifying the correctness of loops one often uses the concept of loop invariance. A loop invariant refers to a statement that is true at all instants when the loop condition is checked. It may be expressed in terms of one or more variables controlling the flow of the loop. Example: Consider the while loop implementation of the computation of Hn.
i = 0; H = 0; while (i < n) { ++i; H += 1.0/i; }

/* Incremet i */ /* Update the harmonic number accordingly */

Here the loop invariant is the statement "H stores the value Hi for all i=0,1,2,...,n". The correctness of this statement can be proved using induction on i. It is initially true (H0=0). Now suppose it is true for a particular i < n. As the loop body is executed the value of i changes to i+1 and that of H changes to Hi+1/(i+1)=Hi+1. The loop terminates when i equals n. By the loop invariance property H then stores the value Hn. This is precisely what we wanted to compute. Example: Next look at the do-while implementation of Fibonacci computation.
i = 1; /* Initialize i to 1 */ F = 1; /* Initialize Fi */ p1 = 0; /* Initialize Fi-1 */ do { ++i; /* Increment i */ p2 = p1; /* The old Fi-1 now becomes Fi-2 */ p1 = F; /* The old Fi now becomes Fi-1 */ F = p1 + p2; /* Compute Fi from Fi-1 and Fi-2 */ } while (i < n);

It is easy to check that an invariant for this loop is the statement "F holds the value Fi and p1 the value Fi-1" and is true for i=1,2,...,n. The loop terminates for i=n, and in that case F stores Fn as desired. Example: Here is a new example. Suppose we want to compute the maximum of n positive integers stored in an array A.
max = A[0]; for (i=1; i<n; ++i) { if (A[i] > max) max = A[i]; }

A loop invariant here is "max stores the maximum value among the integers A[0],A[1],...,A[i-1]" and is true for all i=1,2,...,n. Example: Let us now come to a really non-trivial example of loop invariance.

Theorem: Let a,b be positive integers and d = gcd(a,b). Then there exist integers u,v with the property that ua + vb = d. Computation of the gcd d along with the multipliers u,v is called the extended gcd computation. The following algorithm does that.
/* Initialize */ r2 = a; u2 = 1; v2 = 0; r1 = b; u1 = 0; v1 = 1; /* Extended gcd loop */ while (r1 > 0) { /* Compute values for q = r2 / r1; /* r = r2 - q * r1; /* u = u2 - q * u1; /* v = v2 - q * v1; /* /* Previous-to-previous values */ /* Previous values */

the current Compute the Compute the Identically Identically

iteration */ next quotient */ next remainder */ compute the next u value */ compute the next v value */

/* Prepare for the next iteration */ r2 = r1; u2 = u1; v2 = v1; /* Let the previous-to-previous values be the previous values */ r1 = r; u1 = u; v2 = v; /* Let the previous values be the current values */ } printf("gcd(a,b) = %d = (%d) * a + (%d) * b\n", r2, u2, v2);

Interactive animation : extended Euclidean gcd


It is not at all clear that this algorithm works correctly. The correctness can be established from the following invariance property: Claim: Whenever the continuation condition for the above loop is checked, we have:
gcd(r2,r1) = gcd(a,b), u2 * a + v2 * b = r2, u1 * a + v1 * b = r1. (1) (2) (3)

Proof The three conditions are obviously true at the beginning of the first iteration; this is how the values are initialized. Now suppose that the relations hold for certain iteration with r1 > 0. The loop body is then executed. First, the quotient q and the remainder r of Euclidean division of r2 by r1 is computed. By Euclid's gcd theorem
gcd(r1,r) = gcd(r2,r1) = gcd(a,b).

Moreover,
u = u2 - q * u1, and v = v2 - q * v1,

and so

= = = =

u * a + v * b (u2 - q * u1) * a + (v2 - q * v1) * b (u2 * a + v2 * b) - q * (u1 * a + v1 * b) r2 - q * r1 r.

Thus the three equations (1)-(3) continue to be satisfied for the new r,u,v values. QED The loop terminates when r1 becomes 0. In that case gcd(a,b) = gcd(r2,r1) = gcd(r2,0) = r2 = u2 * a + v2 * b, and, therefore, r2,u2,v2 constitute a desired set of values for the extended gcd. Let us look at the trace of the values stored in different variables for a sample run with a=78 and b=21. Iteration No r2 r1 u2 u1 v2 v1 q r Before loop 78 21 1 1 2 3 4 78 21 1 21 15 0 21 15 0 15 6 1 0 0 1 0 0 1 1 u v -3 -3 4 4 u2*a+v2*b 78 78 21 21 15 15 6 6 3

1 3 15 1 -3 3 15 1 3 4 1 6 -1 1 6 -1

1 1 -1 -3

15 6 1 -1 -3 4 2 3 6 3 -1 3 4 -11 2 3 6 3

3 -11 3 -11

3 -1 3 4 -11 2 0 -7 26 0 3 -7 -11 26 2 0 -7 26

gcd(78,21) = 3 = (3) * 78 + (-11) * 21

Nested loops
One or more loops can be nested inside another loop. In that case the inner loops usually have continuation conditions dependent on variables different from the variable(s) governing the continuation of the outer loop. The programmer should be sufficiently careful so as not to do something silly inside the inner loops, that affects the behavior of the outer loop. Example: [Bubble sort] Suppose you have an array A of n elements (say, integers). They are stored in the array locations A[0],A[1],...,A[n-1]. We want to rearrange these integers in such a way that after the rearrangement we have
A[0] <= A[1] <= A[2] <= ... <= A[n-1].

Such an array is called a sorted array and the process of making the array sorted is refered to as sorting. Sorting is a basic and fundamental computational problem. There are several algorithms proposed for sorting. For the time being, let us look at an algorithm known as bubble sort.

Animation example : bubble sort Interactive animation : bubble sort


The bubble sort algorithm can be implemented using a singly nested loop as follows:
for (i=n-2; i>=0; --i) { */ for (j=0; j<=i; ++j) { bound i */ if (A[j] > A[j+1]) { opposite order */ t = A[j]; */ A[j] = A[j+1]; A[j+1] = t; A[j] stored in t */ } } } /* Change A[j] to A[j+1] */ /* Change A[j+1] to the old value of /* Run j from 0 to the current upper /* Two consecutive elements are in the /* Swap A[j] and A[j+1] */ /* Store A[j] in a temporary variable t /* Attempt to bubble till the value of i

Example: [Selection sort] The working of the selection sort is somewhat similar to that of bubble sort. Here the outer loop runs over i ranging from n-1 down to 1. For a given i, the largest element in the subarray A[0],A[1],...,A[i] is found out and is swapped with the element A[i]. Thus during the first iteration of the outer loop A[n-1] receives the largest element in the array, in the second iteration A[n-2] receives the second largest element, and so on.

Animation example : selection sort Interactive animation : selection sort


The code for selection sort follows:
for (i=n-1; i>=1; --i) { /* First find the maximum element of A[0],A[1],...,A[i] */ /* Initialize maximum entry to be the leftmost one */ maxidx = 0; max = A[0]; /* Now search for a potentially bigger maximum */ for (j=1; j<=i; ++j) { if (A[j] > max) { /* An element bigger that the current maximum is located */ /* Adjust the maximum entry */ maxidx = j; max = A[j];

} } /* Swap A[i] with the maximum element */ A[maxidx] = A[i]; /* Store the last element at index maxidx */ A[i] = max; /* Store the maximum at the last index */ }

Example: [Insertion sort] Here is yet another sorting algorithm that uses nested loops. Here the outer loop runs over a variable i ranging from 1 to n-1. For a particular i, the portion A[0],A[1],...,A[i-1] is already sorted. Then the element A[i] is considered and is inserted at the appropriate position in the sorted list A[0],A[1],...,A[i-1]. That will make a bigger sorted list A[0],A[1],...,A[i]. When the loop body finishes execution with i=n, the entire array is sorted.

Animation example : insertion sort Interactive animation : insertion sort


Here is the code for insertion sort.
for (i=1; i<n; ++i) { /* Consider A[i] */ /* Search for the correct insertion location of A[i] */ t = A[i]; /* Store A[i] in a temporary variable */ j = 0; /* Initialize search location */ while (t > A[j]) ++j; /* Skip smaller entries */ /* Here j holds the desired insertion location */ /* Shift forward the remaining entries each by one location */ for (k=i-1; k>=j; --k) A[k+1] = A[k]; /* Finally insert the old A[i] at the j-th location */ A[j] = t; }

Flow control inside loops


The continuation condition dictates whether the loop body is to be repeated or skipped. There are some constructs by which this natural flow can be altered.

The break statement


A loop may be forcibly broken from inside irrespective of whether the continuation condition is satisfied or not. This is achieved by the break statement. Example: Let us write the gcd algorithm with explicit break statement. Here we make the loop an infinite one. The check whether b equals 0 is carried out inside the loop. If the check succeeds, the loop is broken explicitly by a break statement.

while (1) { if (b == 0) break; r = a % b; a = b; b = r; } printf("gcd = %d\n", a);

Note that any loop can be implemented as an infinite loop with an explicit break. The dowhile loop
do { execute loop body; } while (continuation condition is true);

is equivalent to
do { execute loop body; if (continuation condition is false) break; } while (1);

and also to
while (1) { execute loop body; if (continuation condition is false) break; }

Interactive animation : for loop with break


In case of nested loops, a break statement causes the innermost loop (in which the statement is executed) to be broken. As a toy example, suppose that we want to compute the sum of gcds of all pairs (a,b) with 1<=a<=b<=20. Here is an implementation with explicit break statements.
/* Initialize sum */ sum = 0; for (i=1; i<=20; ++i) { for (j=i; j<=20; ++j) { /* Now we plan to compute gcd(j,i) */ /* But we must not disturb the loop variables */ /* So we copy j and i to temporary variables a and b and change those copies */ a = j; b = i; /* The Euclidean gcd loop */ while (1) { if (b == 0) break; /* gcd computation is over, so break the while loop */ r = a % b; a = b;

b = r; } /* When the while loop is broken, a contains gcd(j,i). Add it to the accumulating sum. */ sum += a; } } printf("The desired sum = %d\n", sum);

Next follows a more obfuscating implementation of the same algorithm. Here all the loops are broken with explicit break statements. Moreover, the break statements occur in the middle of the loops. Finally, the gcd loop is rewritten so that we can break as soon as we find a zero remainder. In that case, b holds the desired gcd.
sum = 0; /* Initialize sum to 0 */ i = 0; /* Initialize the outer loop variable */ while (1 != 0) { /* This condition is always true */ j = ++i; /* Increment i and assign the incremented value to j */ if (j == 21) break; /* Break the outermost loop */ while (3.1416 > 0) { /* This condition is always true */ a = j; b = i; /* Copy j and i to temporary variables */ while ('A') { /* This condition is again always true, since 'A' = 65 */ r = a % b; /* Compute next remainder */ if (!r) break; /* Break the innermost loop */ a = b; /* Adjust a and b and */ b = r; /* prepare for the next iteration */ } /* End of innermost loop */ sum += b; /* Add gcd(j,i) to the accumulating sum */ if (j == 20) break; /* Break the intermediate loop */ ++j; /* Prepare for the next value of j */ } /* End of intermediate loop */ } /* End of outermost loop */ printf("The desired sum = %d\n", sum);

Well then, is it a good style to write programs this way? Certainly no! This makes your code quite unreadable to (and hence unusable by) others. Even if some code is meant for your personal consumption only, debugging it may cause you enough headache, in particular, when you are already pretty tired or hungry and plan to finish the day's programming as early as possible. Programming is fun anyway. For the kick you may at your leisure time make attempts to write and/or understand obfuscated codes. So then, what does the following program print (as a function of n)?
#include <stdio.h> main () {

unsigned int n, i, j, s; printf("Enter a positive integer : "); scanf("%d",&n); s = 0x00000041 ^ (unsigned int)'A'; while (i = --n) while (j = --i) while (--j) ++s; printf("s = %d\n", s); }

The continue statement


The continue statement also affects the normal execution of a loop. It does not cause the loop to terminate, but throws the control to the top of the loop ignoring the remaining part of the loop body for the current iteration. Example: Suppose we want to print the integers 1,2,...,100 neatly with 10 integers printed in a line. Here is how this can be done:
for (i=1; i<=100; ++i) { printf("%4d",i); if (i%10 != 0) continue; printf("\n"); }

Here if i is a multiple of 10, the new line character is printed. Otherwise the continue statement lets the control flow reach the top of the loop, i.e., to the loop increment area where the variable i is incremented. The same effect can be realized by the following while loop:
i = 0; while (i < 100) { ++i; printf("%4d",i); if (i%10 != 0) continue; printf("\n"); }

Interactive animation : for loop with continue


Course home

CS13002 Programming and Data Structures

Spring semester

Functions and recursion


Imagine a hotel with infinitely many rooms 0,1,2,... On a rainy night all the rooms numbered 1,2,3,... are occupied by tenants. Room number 0 is used as the reception, it being opposite to the entrance. Every tenant was enjoying TV and waiting for a sumptuous dinner being prepared in the adjacent restaurant. All of a sudden a bus carrying another infinite number of passengers arrives in front of the hotel's entrance. The chauffeur meets the manager and requests him to give rooms to all the passengers. It was a stormy night and there were no other hotels in the vicinity. So the manager devises a plan. He first relocates the existing tenants, so that the tenant at room no m goes to room no 2m-1 for all m=1,2,3,... He numbers the new guests -1,-2,3,... and allocates the room 2m for passenger -m. He then writes a small computer program that notifies people of the new room allotment. He uses the following function:
0 f(n) = 2m - 1 2m if n = 0, if n = m > 0, if n = -m for some m > 0.

Everybody seems happy at this. Only the boarder of room no 224,036,583-1 (the largest known prime number of today, a 7235733-digit number) raises an objection indicating that he has to move too many rooms ahead. He insists that the current occupant of room number n should not be asked to shift by more than n/2 rooms. The manager complies and comes up with a second function:
0 3m - 2 g(n) = 3m - 1 3m if if if if n n n n = = = = 0, 2m - 1 for some m > 0, 2m for some m > 0, -m for some m > 0.

The manager allegedly wrote a C program. His initial program looked like:
#include <stdio.h> int f ( int n ) { if (n == 0) return (0); else if (n > 0) return (2*n-1); else return (-2*n); } int main () { int n;

while (1) { printf("Input n : "); scanf("%d",&n); printf("Room number for %d is %d.\n", n, f(n)); } }

After the request of Mr. Mersenne the XLI, the manager changed his program to:
#include <stdio.h> int g ( int n ) { int m; if (n == 0) return (0); else if (n < 0) return (-3*n); else { m = (n + 1)/2; if (n % 2 == 0) return (3*m-1); else return(3*m-2); } } int main () { int n; while (1) { printf("Input n : "); scanf("%d",&n); printf("Room number for %d is %d.\n", n, g(n)); } }

There is a technical problem here. Though the hotel in the story has infinitely many rooms and the bus carried infinitely many passengers, C's integers are limited in size (32 or 64 bits). But then since this is a story, we may take liberty to imagine about a fabulous C compiler that supports integers of any size!

Translating mathematical functions in C


The above example illustrates how we can write functions in C. A function is expected to carry out a specific job depending on the argument values passed to it. After the job is accomplished, the function returns some value to the caller. In the above example, the function f (or g) accepts as an argument the number of the tenant, computes the room number of the tenant and returns this value to the place where it is called. The basic syntax of writing a function goes like this:
return_type function_name ( list_of_arguments ) { function body

The argument list should be a comma-separated list of type-name pairs, where type is any valid data type and name is any legal formal name of a variable. Argument values can be accessed inside the function body using these names. The return type of a function should again be a valid data type (like int, float, char *). A function may even choose to return no explicit values in which case its return type is to be mentioned as void. The function body starts with a declaration of local variables. These variables together with the function arguments are accessible only in the function body and not outside it. After the declarations one writes the C statements that compute the specific task that the function is meant for. The function returns a value using the statement:
return (return_value);

The parentheses around return_value are optional. In case a function is expected to return nothing (i.e., void), the return statement looks like:
return;

The return statement not only returns a value (possibly void) to the caller, but also returns the control back to the place where it is called. In case no explicit return statements are present in the function body, control goes back to the caller after the entire body of the function is executed. Calling a function uses the following syntax:
function_name ( argument_values )

Here argument values are provided as a comma-separated list of expressions. The formal names of the function arguments have absolutely nothing to do with the expressions passed during the call. However, the number of arguments and their respective types in a function call must match with those that are declared in the function header. In some cases data of different types are implicitly typecast when passed to functions, but it is advisable that you do not rely too much on C's automatic typecasting mechanism. That may lead to unwelcome run-time errors. Functions may call other functions in the function body. In fact, a function call can be treated as an expression. It is like referring to a+b as add(a,b). Just because your keyboard does not support enough symbols, you have to call your functions by special names. A function is regarded as an isolated entity that can perform a specific job. Therefore, if that specific job is to be carried out several times (possibly) with different argument

values, functions prove to be useful. Functions also add to the legibility and modularity of programs, thereby enhancing simpler debugging. It is a bad practice to write long monolithic programs. We encourage you to break up the monolithic structure into logically coherent parts and implement each part as a function. Functions (like loops) provide a way in which the standard sequential top-to-bottom flow of control is disturbed. This is the reason why functions may pose some difficulty to an inexperienced programmer. But the benefits they provide far outweigh one's efforts to master them. Guess what, you too must master them! Example: If a program requires computation of several gcd's, it is advisable to write a function and call it with appropriate parameters as and when a gcd is to be calculated.
#include <stdio.h> int gcd ( int a , int b ) { int r; /* Check for errors : gcd(0,0) is undefined */ if ((a==0) && (b==0)) return (0); /* Make the arguments non-negative */ if (a < 0) a = -a; if (b < 0) b = -b; /* Special case : gcd(a,0) = a */ if (b == 0) return (a); /* The Euclidean gcd loop */ while (1) { r = a % b; if (r == 0) return (b); a = b; b = r; } } int main () { int i, j, s; s = 0; for (i=1; i<=20; ++i) { for (j=i; j<=20; ++j) { s += gcd(j,i); } } printf("The desired sum = %d\n", s); }

Example: In all the previous examples we have made the function call at only one place. One may replace this call by an explicit code carried out in the function. However, if the

same function is called multiple times, inserting an equivalent code at all call locations increases the size of the code and calls for separate maintenance of the different copies. This is your first tangible benefit of using functions. Think of a situation when a committee of n members need be formed. The committee must have a core team consisting of at least two members and no more than one-third of the entire committee. In how many ways the core committee may be selected? Here is a program that computes this number: function1.c
#include <stdio.h> int factorial ( int n ) { int prod = 1, i; for (i=2; i<=n; ++i) prod *= i; return(prod); } int binomial ( int n , int r ) { return(factorial(n)/(factorial(r)*factorial(n-r))); } int main () { int n, i, s = 0; printf("Total number of members : "); scanf("%d",&n); for (i=2; i<=n/3; ++i) s += binomial(n,i); printf("Total number of ways = %d\n", s); }

Example: Here is a more complicated example. Suppose we want to print the square root of an integer truncated after the third digit following the decimal point. We use the standard algorithm taught in the school. The algorithm finds successive digits in the square root. The following example illustrates a typical computation of a square root:
153 | 12.369 1 | +----+ | 53 | 44 +--| 900 | 729 +----| 17100 | 14796 +------| 230400

22 243 2466 24729

| 222561 +--------7839

Here is the complete source code: function2.c


#include <stdio.h> int nextDigit ( int r , int s , int grp ) /* Here r is whatever remains, s is the sqrt found so far, and grp is the next two digits to be considered. */ { int d = 0; /* Keep on searching for the next digit in the square root */ while ((20*s+d)*d <= 100*r+grp) ++d; /* Here d is just one bigger than the correct digit */ return(d-1); } void printSqrt ( int n ) { int s, /* Square root found so far */ r, /* Whatever remains */ d, /* next digit */ nl, /* Number of digits to the left of the decimal point */ nr = 3, point */ grp[8], sgn, i; /* 2-digit groups */ /* Sign of n */ /* An index */ /* Number of digits to the right of the decimal

if (n < 0) { sgn = 1; n = -n; } else sgn = 0; if (n == 0) { nl = 1; grp[0] = 0; } else { nl = 0; while (n != 0) { grp[nl] = n % 100; /* Save next 2-digit group */ n /= 100; ++nl; } } /* Initialize */ s = 0; r = 0; /* First print the digits to the left of the decimal point */ for (i=nl-1; i>=0; --i) { d = nextDigit(r,s,grp[i]); printf("%d",d); r = (100 * r + grp[i]) - (20 * s + d) * d; s = 10 * s + d; } /* Print the decimal point */ printf(".");

/* Print digits after the decimal point */ for (i=0; i<nr; ++i) { d = nextDigit(r,s,0); printf("%d",d); r = 100 * r - (20 * s + d) * d; s = 10 * s + d; } /* Square root of negative numbers should be imaginary */ if (sgn) printf("i"); printf("\n"); } int main () { int n; printf("Enter an integer : "); scanf("%d",&n); printSqrt(n); }

Example: C provides many built-in functions. For example, the main function is a builtin function and must be present in any executable program. It returns an int value. It may also accept arguments. Here is the complete prototype of main:
int main ( int argc , char *argv[] ) { ... }

One cannot call the main function from any other function in a program. If that is the case, who calls it and who uses its return value? The external world! When you run your program from a shell (possibly by typing ./a.out), you can pass (command-line) arguments to main. Moreover, when the program terminates, the return value of main is returned to the shell. You may choose to use the value for doing something useful. Think of a call like this:
./a.out -5 3.1416 foo.txt

When the main function starts execution, its argc parameter receives the value 4, because the total number of arguments including ./a.out is 4. The other argument argv is actually an array of arrays of characters. argv[0] gets the string "./a.out", argv[1] the string "-5", argv[2] the string "3.1416", and argv[3] the string "foo.txt". You can process these values from inside the main function. For example, you may supply a file name, some initial values, etc. via command-line arguments.

Some other built-in C functions include printf and scanf. A queer thing about these functions is that they support variable number of parameters. You can also write functions with this property, but we won't discuss it here.

Function prototypes
As long as a function is defined earlier (in the program) than it is called, there seems to be no problem. However, if the C compiler meets a function call before seeing its definition, it assumes that the function returns an int. Eventually the compiler must encounter the actual definition of the function. If the compiler then discovers that the function returns a value of type other than int, it issues a mild warning message. Compilation then proceeds successfully. However, when you run this program, you may find awkward run-time errors. That happens because the run-time system typecasts data of another type to int. That may create troubles in esoteric situations. The way out is to always define a function earlier than it is called. Unfortunately, there is a situation where this cannot be done, namely, when a function egg() calls a function chicken() and the function chicken() also calls egg(). Which function will then be defined earlier? The most graceful way to tackle this problem is to define the prototype of a function towards the beginning of your program. The prototype only mentions the return type and parameter types. The body may be (and must be) defined somewhere else, even after it is called. A function prototype looks like the first line of the function followed by a semicolon (instead of its body surrounded by curly braces).
return_type function_name ( argument_list );

For example, the gcd, nextDigit and printSqrt functions defined above have the following prototypes:
int gcd ( int a , int b ); int nextDigit ( int r , int s , int grp ); void printSqrt ( int n );

During a prototype declaration the names of the variables play no roles. It is the body that is expected to make use of them, and the prototype has no body at all. So these names may be blissfully omitted. That is, it is legal to write:
int gcd ( int , int ); int nextDigit ( int , int , int ); void printSqrt ( int );

When you actually define the function, its header must faithfully match with the prototype found earlier.

Archiving C functions

Function prototypes are also useful during packaging of C functions in libraries. We explain the concept with an example. Assume that you are writing a set of useful tools to be used by foobarnautic scientists and engineers. The subject deals with two topics: foomatics and bargodics. You plan to write your foomatic functions in two files foo1.c and foo2.c, the first containing the basic tools and the second some advanced tools. For bargodics too you plan to write two C sources bar1.c and bar2.c. Later you realize that some bargodic topics are so advanced that they may better be called esoteric and should be placed in a third file bar3.c. All these five files have C functions, each meant for doing some specific job, like computing fooctorials, barnomial coefficients etc. However, none of these files should have a main function. A future user of your library will write the main function in her program, call your foobarnautic functions from her program and finally compile and run her program to unveil foobarnautic mysteries. You first write the would-be useful functions in five files as mentioned above. You then compile each such file to an object file (not an executable file, since no file has a main).
cc cc cc cc cc -c -c -c -c -c -o -o -o -o -o foo1.o foo2.o bar1.o bar2.o bar3.o foo1.c foo2.c bar1.c bar2.c bar3.c

Five object files foo1.o, foo2.o, bar1.o, bar2.o and bar3.o are obtained after successful compilation. You then join these object files to an archive (library):
ar rc libfoobar.a foo1.o foo2.o bar1.o bar2.o bar3.o

The archive command (ar) creates the library libfoobar.a. You may optionally run the following utility on this archive in order to add some book-keeping information in the archive:
ranlib libfoobar.a

Now your library is ready. Copy it to a system directory if you have write permission there, else store it somewhere else, say, in the directory /tmp/foobar/lib. Now when the future user plans to use your library, she simply compiles her program fooexplore.c (with main) as:
cc fooexplore.c -lfoobar

if the library libfoobar.a resides in a system directory. If not, she should specifically mention the directory of the library and compile her program as:
cc fooexplore.c -L/tmp/foobar/lib -lfoobar

But... Something goes wrong, may be terribly wrong. Her compilation attempt issues a hell lot of warning messages. In fact, cc may even refuse to compile fooexplore.c. That was your fault, not the user's. You have missed to do some vital things. As soon as the frustrated programmer rings you up, you realize your fault. Now do the remaining things. Create header files foo1.h, foo2.h, bar1.h, bar2.h and bar3.h. These files should contain only the following:

All new type definitions that you used in your library. All global variables and constants you used in your library. Prototypes of all functions defined in the library.

For example, foo1.h may look like:


/********************************************************************** **** * foo1.h : Header file for basic foomatic utilities * * Created by : 04FB1331 Foolan Barik * * Last updated : January 08, 2005 * * Copyright 2005 by the Dept of Foobarnautic Engg, IIT Kharagpur, India * *********************************************************************** ***/ /* Prevent accidental multiple #inclusion of this header file */ #ifndef _FOO1_H #define _FOO1_H /* New type definitions */ typedef unsigned long int fooint; typedef long double fooreal; typedef unsigned char foochar; ... /* Macros */ #define _FOO_BAR_TRUE 1 #define _FOO_BAR_FALSE 0 #define _FOO_BAR_PI 3.141592653589793238462643383 ... /* Global constants static const fooint { 0xf00ba000, 0xba0f0000, ... */ foorams[8] = 0xf002ba00, 0xf0046ba0, 0xf008acba, 0xba01f000, 0xba035f00, 0xba079bf0 };

/* Function prototypes. */ /* These functions are external to the user's programs. */

/* So use the extern keyword. */ extern fooint fooctorial ( fooint ) ; extern fooreal fooquation ( fooreal , fooint * , foochar ) ; ... #endif

If the source foo1.c contains these definitions, remove them from the C file and instead #include "foo1.h" towards the beginning of foo1.c. Do not define any function in a header file. Do the above for all sources. Recompile your library, copy the new libfoobar.a once again to an appropriate directory. Then choose a suitable directory for putting the headers. If you have permission to write in the system's include directory (usually /usr/include), create a directory foobar under this directory and copy your five header files to this new directory. If you do not have permission to write in /usr/include, create the directory /tmp/foobar/include and copy the header files there. Call back the user and notify her of these new developments. The user then adds the following lines to her source code fooexplore.c.
#include #include #include #include #include <foobar/foo1.h> <foobar/foo2.h> <foobar/bar1.h> <foobar/bar2.h> <foobar/bar3.h> "/tmp/foobar/include/foo1.h" "/tmp/foobar/include/foo2.h" "/tmp/foobar/include/bar1.h" "/tmp/foobar/include/bar2.h" "/tmp/foobar/include/bar3.h"

or
#include #include #include #include #include

depending on where you put the header files. Eureka! Her program now compiles and churns out unthinkably sublime foobarnautic data. She immediately rings you up again. You are afraid if any other thing went wrong. But you receive by your bewildered ears that she is thanking you profusely and inviting you for a tasty dinner in a posh downtown restaurant!

Built-in libraries
Well, you don't always have to write libraries. You can use libraries written by others. Think of the square root printer we have designed earlier. If you have to write every such basic function yourself, when will you write programs that solve your own problems?

Fortunately, many libraries are available in the standard C developer's distribution. Here we describe some of the most useful ones.

The math library


One useful library is the C math library. In order to use the library you should include the header file <math.h>. Do it after you include <stdio.h>. But that's not all. This inclusion makes accessible to your program only the function prototypes and some constant declarations. In order to link the function definitions you should also use the -lm flag during compilation time.
cc myMathHeavyProg.c -lm

Once you are given a library, the designer of the library should also specify in a document how to use the library, i.e., what new data types and constants are defined in it and what each function defined in the archive does. Here follows a high-level description of some useful mathematical functions defined in the C math library.
double sqrt (double x);

Returns the square root of the real number x.


double pow (double x, double y); Returns the real number xy. double floor (double x);

Returns the largest integer smaller than or equal to x.


double ceil (double x);

Returns the smallest integer larger than or equal to x.


double fabs (double x);

Returns the absolute value |x| of x.


double exp (double x);

Returns ex, where e = 2.7182818284... = 1+(1/1!)+(1/2!)+(1/3!)+... is the famous number you encountered in your calculus course.
double log (double x);

Returns the natural logarithm of x, i.e., the real logarithm of x to the base e.
double log10 (double x);

Returns the real logarithm of x to the base 10.


double sin (double x); double cos (double x); double tan (double x);

The standard trigonometric functions. The argument should be specified in radians.


double double double double asin (double x); acos (double x); atan (double x); atan2 (double x, double y);

The inverse trigonometric functions. acos returns a value in the range [0,pi], asin and atan in the range [-pi/2,+pi/2], and atan2 in the range [-pi,+pi].
double sinh (double x); double cosh (double x); double tanh (double x);

The standard hyperbolic trigonometric functions. In addition to the above functions, math.h also defines the following useful constants:
#define #define #define #define #define #define #define #define #define #define #define #define #define M_E M_LOG2E M_LOG10E M_LN2 M_LN10 M_PI M_PI_2 M_PI_4 M_1_PI M_2_PI M_2_SQRTPI M_SQRT2 M_SQRT1_2 2.7182818284590452354 1.4426950408889634074 0.43429448190325182765 0.69314718055994530942 2.30258509299404568402 3.14159265358979323846 1.57079632679489661923 0.78539816339744830962 0.31830988618379067154 0.63661977236758134308 1.12837916709551257390 1.41421356237309504880 0.70710678118654752440 /* /* /* /* /* /* /* /* /* /* /* /* /* e */ log_2 e */ log_10 e */ log_e 2 */ log_e 10 */ pi */ pi/2 */ pi/4 */ 1/pi */ 2/pi */ 2/sqrt(pi) */ sqrt(2) */ 1/sqrt(2) */

The ctype library


Include the header <ctype.h> in order to access several character-related functions. You don't have to link any special library during compilation time. Many of these functions return Boolean values (true and false). However, C does not have a default Boolean data type. Here the Boolean value is returned as an integer (int) with the convention that 0 means "false" and any non-zero value means "true".
int isalpha (int c);

Returns true if and only if c is an alphabetic character ('A'-'Z' and 'a'-'z').


int isupper (int c);

Returns true if and only if c is an upper-case alphabetic character ('A'-'Z');


int islower (int c);

Returns true if and only if c is a lower-case alphabetic character ('a'-'z');


int isdigit (int c);

Returns true if and only if c is a decimal digit ('0'-'9');


int isxdigit (int c);

Returns true if and only if c is a hexadecimal digit ('0'-'9', 'A'-'F' and 'a'-'f').
int isalnum (int c);

Returns true if and only if c is an alphanumeric character ('A'-'Z', 'a'-'z' and '0'-'9').
int isspace (int c);

Returns true if and only if c is a white space character (space, tab, new-line, formfeed, carriage-return).
int isprint (int c);

Returns true if and only if c is a printable character (0x20-0x7e).


int ispunct (int c);

Returns true if and only if c is a printable character other than space, letter and digit.
int isgraph (int c);

Returns true if and only if c is a graphical character (0x21-0x7e).

int iscntrl (int c);

Returns true if and only if c is a control character (0x00-0x1f and 0x7f).


int tolower (int c);

If c is an upper-case letter, the corresponding lower-case letter is returned. Otherwise, c itself is returned.
int toupper (int c);

If c is a lower-case letter, the corresponding upper-case letter is returned. Otherwise, c itself is returned.

The stdlib library


The standard library may be included by including the header <stdlib.h>. No separate libraries need be linked during compilation time.
int atoi (const char *s);

Returns the integer corresponding to the string s. For example, the string "243" corresponds to the integer 243.
long atol (const char *s);

Returns the long integer corresponding to the string s. For example, the string "243576809" corresponds to the integer 243576809L.
double atof (const char *s);

Returns the floating point number corresponding to the string s. For example, the string "243576.809" corresponds to the floating-point number 2.43576809e05 (in the scientific notation).
int rand ();

Returns a random integer between 0 and RAND_MAX. In our lab RAND_MAX is 231-1.
void srand (unsigned int s);

Seed the random number generator by the integer s. A natural seed is the current system time. The following statement does this.
srand((unsigned int)time(NULL));

In order to use the time function, you should #include <time.h>.


int abs (int n);

Returns the absolute value |n|of the integer n.


long labs (long n);

Returns the absolute value |n|of the long integer n.


int system (const char *s);

Passes the string argument s to be executed by the shell. For example, system("clear"); clears the screen.

Passing parameters
In C all parameters are passed by value. This means that for a call like
u = fooquation(x+y*z,&n,c);

the arguments are first evaluated and subsequently the values are copied to the formal parameters defined in the function header:
long double fooquation ( long double x , unsigned long *p , unsigned char c ) { long double w; ... return(w); }

During the call, the formal parameter x gets the value of the expression x+y*z, the pointer p gets the address of n and the formal parameter c obtains the value stored in the variable c during the time of the call. The formal arguments x,p,c are treated in fooquation as local variables. Any change in these values is not reflected outside the function. Thus the variables x,y,z,c in the caller function are unaffected by whatever fooquation does with the formal arguments x,p,c. The caller variable n is an exception. We didn't pass n straightaway to fooquation, we instead passed its address. So fooquation receives a copy of this address in its formal argument p. If the function modifies the pointer p, this does not change the address of n in the caller. However, fooquation may wish to write to the address passed to p. This modifies n, but not &n. If you want to modify the value of some variable in a function, pass to the function a pointer to the variable. Here is a failed attempt to swap the values of two variables:
void badswap ( int a , int b ) { int t; t = a; a = b; b = t; } int main () { int m = 51, n = 23; printf("m = %d, n = %d.\n", m, n); badswap(m,n); printf("m = %d, n = %d.\n", m, n); }

The program prints:


m = 51, n = 23. m = 51, n = 23.

The call of badswap produces no effect on m and n. In order to produce the desired effect, use the following strategy:
void swap ( int *ap , int *bp ) { int t; t = *ap; *ap = *bp; *bp = t; } int main () { int m = 51, n = 23; printf("m = %d, n = %d.\n", m, n); swap(&m,&n); printf("m = %d, n = %d.\n", m, n); }

This time the program prints:


m = 51, n = 23. m = 23, n = 51.

Animation example : parameter passing in C


Now what should you do if you want to change a pointer? The answer is simple: pass a pointer to the pointer. How? We will give an answer to this new question later. Hold your patience.

Recursive functions
Recall that certain functions are defined recursively, i.e., in terms of itself. For example, consider the function F that maps n to the n-th Fibonacci number, i.e., F(n) = Fn. (In fact, every sequence is a function in a natural way.) We then have:
0 F(n) = 1 F(n-1) + F(n-2) if n = 0, if n = 1, if n >= 2.

It is then tempting to write F as follows:


int F ( int n ) { if (n == 0) return (0); if (n == 1) return (1); return (F(n-1)+F(n-2)); }

Does it work? The potential problem is: if F calls F itself with different parameter values, what would be the formal argument n for F. Every new invocation of F is expected to erase the old value of n. That would lead to error. In the above example, when F(n-1) returns, we have to make a second invocation F(n-2). Now if by this time the value of n has changed, we expect to get incorrect results. So what is the way out? The answer is: there is no way out. In fact, there has not been any problem at all. The above function perfectly works. Older languages like FORTRAN (designed in the 50's) used to face a problem, and there was again no way out. You cannot call a function from itself. That is, recursion was strictly prohibited. C A R Hoare first proposed a way to work around with this problem. He introduced the concept of nests which we nowadays refer to as stacks. Every time a function is called, its formal parameters and local variables are pushed to the top of the call stack. In this way different invocations refer to different memory locations for accessing variables of the same name. When a function returns, its local data are popped out of the stack and control returns to the caller function for which variables reside in the current top of the stack. The first high-level language that supported recursion was ALGOL. Most languages designed after that (late sixties onward) support recursion. C is no exception. In fact, the latest version of FORTRAN (FORTRAN 90) also supports recursion.

Animation example : recursive computation of Fibonacci numbers Interactive animation : recursive computation of Fibonacci numbers
Here is another example: recursive computation of the factorial function.
int factorial ( int n ) { if (n < 0) return (-1); if (n == 0) return (1); return(n * factorial(n-1)); }

/* Error condition */ /* Basis case */ /* Recursive call */

Animation example : recursive computation of the factorial function Interactive animation : recursive computation of the factorial function
Example: [Merge sort] This is a very interesting recursive sorting technique. The array to be sorted is first divided in two halves of nearly equal sizes. Each half is then recursively sorted. Two

sorted subarrays are then merged to form the final sorted list. Recursion stops when the array is of size 1. Such an array is already sorted. The correctness of this algorithm can be established by the principle of strong mathematical induction. The base case (arrays of size 1) is obvious. For the inductive step, it suffices to prove that the merging routine correctly merges two sorted arrays. We leave out the details here.

Animation example : merge sort Interactive animation : merge sort


Here is a recursive implementation of the merge sort algorithm. For simplicity, we work with a global array, so that we do not have to bother about passing the array as a function argument.
#include <stdio.h> #include <stdlib.h> #include <time.h> #define MAXSIZE 1000 int A[MAXSIZE]; /* Function prototypes */ void mergeSort ( int, int ); void merge ( int, int, int ); void printArray ( int ); void mergeSort ( int i , int j ) /* i and j are the leftmost and rightmost indices of the current part of the array being sorted. */ { int mid; if (i == j) return; sorted */ mid = (i + j) / 2; mergeSort(i,mid); mergeSort(mid+1,j); merge(i,mid,j); } /* Base case: an array of size 1 is /* /* /* /* Compute the mid index */ Recursively sort the left half */ Recursively sort the right half */ Merge the two sorted subarrays */

void merge ( int i1, int j1, int j2 ) { int i2, k1, k2, k; int tmpArray[MAXSIZE]; i2 = j1 + 1; k1 = i1; k2 = i2; k = 0; while ((k1 <= j1) || (k2 <= j2)) { if (k1 > j1) { /* Left half is exhausted */ /* Copy from the right half */ tmpArray[k] = A[k2];

++k2; } else if (k2 > j2) { tmpArray[k] = A[k1]; ++k1; } else if (A[k1] < A[k2]) { smaller value */ tmpArray[k] = A[k1]; ++k1; } else { smaller value */ tmpArray[k] = A[k2]; ++k2; } ++k; }

/* Right half is exhausted */ /* Copy from the left half */ /* Left pointer points to a /* Copy from the left half */ /* Right pointer points to a /* Copy from the right half */

/* Advance pointer for writing */

/* Copy temporary array back to the original array */ --k; while (k >= 0) { A[i1+k] = tmpArray[k]; --k; } } void printArray ( int s ) { int i; for (i=0; i<s; ++i) printf("%3d",A[i]); printf("\n"); } int main () { int s, i; srand((unsigned int)time(NULL)); printf("Array size : "); scanf("%d",&s); for (i=0; i<s; ++i) A[i] = 1 + rand() % 99; printf("Array before sorting : "); printArray(s); mergeSort(0,s-1); printf("Array after sorting : "); printArray(s); }

Here is a sample run of the program:


Array Array 56 94 32 Array 86 90 91 size : 20 before sorting : 90 51 after sorting : 94 94 21 74 40 94 78 75 58 91 11 7 86 77 76 20 45

7 11 20 21 32 40 45 51 56 58 74 75 76 77 78

Example: [Quick sort] Another recursive sorting technique is called the quick sort. For random arrays quick sort turns out to be one of the practically fastest sorting algorithms. Invented by C A R Hoare, this algorithm demonstrates the necessity for the facility of recursion in a high-level language. Inspired by this (and other) needs, Hoare himself wrote a commercial compiler for the language ALGOL 60. Here we describe a simple version of the quick sort algorithm that employs auxiliary storage for partitioning the array. The idea is to choose a pivot, typically the first element of the array. All the remaining elements are partitioned into two collections, the first containing those array elements that are less than the pivot and the second containing the elements not less than the pivot. Then the original array is replaced by the smaller part followed by the pivot followed by the larger part. With this partitioning the pivot is now in the correct position. The smaller and larger parts are then recursively sorted by the quick sort algorithm. Once again the correctness of this algorithm can be rigorously established using the principle of strong mathematical induction.

Animation example : quick sort with extra storage


The complete implementation of the quick sort algorithm can be found here.
#include <stdio.h> #include <stdlib.h> #include <time.h> #define MAXSIZE 1000 int A[MAXSIZE]; void quickSort ( int i , int j ) { int pivot; int leftArray[MAXSIZE], rightArray[MAXSIZE]; int lsize, rsize; int k, idx; if (i == j) return; pivot = A[i]; k = i; lsize = rsize = 0; /* Separate out the left and right parts */ while (k < j) { ++k; if (A[k] < pivot) leftArray[lsize++] = A[k]; else rightArray[rsize++] = A[k]; } /* Copy back the left part, the pivot and the right part to the original array */ k = i;

for (idx=0; idx<lsize; ++idx) A[k++] = leftArray[idx]; A[k++] = pivot; for (idx=0; idx<rsize; ++idx) A[k++] = rightArray[idx]; if (lsize > 0) quickSort(i,i+lsize-1); left part */ if (rsize > 0) quickSort(j-rsize+1,j); right part */ } void printArray ( int s ) { int i; for (i=0; i<s; ++i) printf("%3d",A[i]); printf("\n"); } int main () { int s, i; srand((unsigned int)time(NULL)); printf("Array size : "); scanf("%d",&s); for (i=0; i<s; ++i) A[i] = 1 + rand() % 99; printf("Array before sorting : "); printArray(s); quickSort(0,s-1); printf("Array after sorting : "); printArray(s); } /* Recursive call on the /* Recursive call on the

The partitioning of the array can be done in-place, i.e., without using extra storage. We won't go to the details here. The following animation implements quick sort with in-place partitioning.

Animation example : in-place quick sort

Recursion or iteration?
The divide-and-conquer algorithms like merge sort and quick sort give rise to a new genre of algorithm design and analysis techniques. Until recursion could be realized, implementing these algorithms was really non-trivial. However, recursion is not really an unadulterated boon. To exemplify this issue, let us compare the performances of the iterative version of the Fibonacci number generation function and of the recursive version described above. Computation of Fn by the iterative version (using simple loops) requires n-1 additions and some additional overheads proportional to n.

But what about the recursive version? Let Sn denote the number of additions performed by the iterative method for the computation of Fn. We evidently have:
Sn = 0 Sn-1 + Sn-2 + 1 if n = 0 or 1, if n >= 2.

Define the sequence


Tn = Sn + 1 for all n = 0,1,2,...

It follows that
Tn = 1 Tn-1 + Tn-2 if n = 0 or 1, if n >= 2.

Thus T0 = F1 and T1 = F2. By induction we then have Tn = Fn+1, i.e.,


Sn = Fn+1 - 1.

If n = 25, we have Sn = 121392, whereas for n = 50, we have Sn = 20365011073. Compare these figures with the very small numbers (respectively 24 and 49) of additions performed by the iterative method. The reason for this poor performance of the recursive algorithm is that many Fi are computed multiple times. For example, Fn computes both Fn-1 and Fn-2, whereas Fn-1 also computes Fn-2. It is absolutely unnecessary to recompute the same value again and again. But unless we do something, we cannot eliminate this massive amount of multiple computations. It is, therefore, often advisable to replace recursion by iteration. If some function makes only one recursive call and does nothing after the recursive call returns (except perhaps forwarding the value returned by the recursive call), then one calls this recursion a tail recursion. Tail recursions are easy to replace by loops: since no additional tasks are left after the call, no book-keeping need be performed, i.e., there is no harm if we simply replace the local variables and function arguments by the new values pertaining to the recursive call. This leads to an iterative version with the loop continuation condition dictated by the function arguments. The factorial and Fibonacci routines that we presented earlier are not tail-recursive. The factorial routine performs a multiplication after the recursive call returns, and so it feels the necessity to store the formal parameter n. With the following implementation this need is eliminated. Here we pass to the recursive function an accumulating product.
int facrec ( int n , int prod ) { if (n < 0) return (-1); if (n == 0) return (prod); return (facrec(n-1,n*prod)); } int factorial ( int n )

{ return (facrec(n,1)); }

The straightforward iterative version of this is the following:


int faciter ( int n ) { int prod; if (n < 0) return (-1); prod = 1; /* Corresponds to facrec(n,1) */ while (n > 0) { /* Corresponds to the sequence of recursive calls */ prod *= n; /* Second argument in the recursive call */ n = n - 1; /* Change the formal parameter */ } return (prod); }

For the Fibonacci number generator the following strategy reduces the overhead of recursion to something proportional to n. This function returns both Fn and Fn-1. But since a function cannot straightaway return two values simultaneously, the returning of Fn-1 is effected by pointers. Since the computation of Fn requires only two previous values, the efficient (linear) behavior is restored by the following recursive implementation.
int F ( int n , int *Fprev ) { int Fn_1, Fn_2; if (n == 0) { *Fprev = 1; return (0); } if (n == 1) { *Fprev = 0; return (1); } Fn_1 = F(n-1,&Fn_2); *Fprev = Fn_1; return (Fn_1+Fn_2); }

This function is not tail-recursive, but that does not matter much. In the base case, it computes (F0,F1). From these values it computes (F1,F2), and from these the values (F2,F3), and so on. Eventually, we get (Fn-1,Fn) which contains the desired number Fn.

Interactive animation : fast recursive computation of Fibonacci numbers


In general, it is not an easy matter to replace recursion by iteration (or more ambitiously by tail-recursion). Whenever the replacement idea is intuitive and straightforward, one may go for it. After all, recursion has some overheads. In most cases, however, we have

to look more deeply into the structure of the problem in order to devise a suitable iterative substitute. Memoization and dynamic programming techniques often help. But these topics are too advanced to be dealt with in this introductory course.

Now here is again an obfuscated code for you. Determine what the following function computes. Express the return value as a function of the input integer n. Assume that n >= 0.
int foo ( int n ) { int s = 0; while (n--) s += 1 + foo(n); return s; }

Course home

CS13002 Programming and Data Structures

Spring semester

Arrays
We have already discussed how one can define and initialize arrays and access individual cells of an array. In this chapter we introduce some advanced techniques related to handling of arrays.

Passing arrays to functions


We have seen how individual values (variables and pointers) can be passed to functions. Now let us see how we can pass an entire array to a function. Suppose an array is defined as:
#define MAXSIZE 100 int myarr[MAXSIZE];

In order to pass the array myarr to a function foonction one may define the function as:
int foonction ( int A[MAXSIZE] , int size ) { ... }

This function takes two arguments, the first is an array of size MAXSIZE, and the second an integer argument named size. Here this second argument is meant for passing the actual size of the array. Your array can hold 100 integers. However, at a certain point of time you may be using only 32 locations (0 through 31) of the array. The other unused locations also hold some values. If they are not initialized, they contain unpredictable values. You do not want these garbage values to be interpreted by your function as important ones. So you specify the actual size to be 32. The function call should go like this:
foonction(myarr,32);

Inside the function the array location myarr[i] can be accessed (read or written) as A[i]. It is very important to note that: When you pass an array to a function, all changes you make in the array locations are visible from outside.

In this example setting A[5] to 20 inside the function also changes myarr[5] to 20. This apparently contradicts the pass-by-value call mechanism of C. But the actual scenario is not so. When you pass an array, the entire array is not copied element-by-element. What is copied is the address of the first (I mean, the zeroth) location of the array. That is indeed a pointer. This dual meaning of an array will be dealt with at length later in this chapter. You don't have to specify the length of the array in the function declaration. This is again because the array is not copied element-wise. Only the starting address of the array is passed. The function call does not allocate memory for the elements of the array. Therefore, it does not matter how big the array is. However, it is necessary to mention that the argument that is passed is an array and not an element of the constituent data type. The following declaration is adequate and admissible:
int foonction ( int A[] , int size ) { ... }

Animation example : passing arrays to functions

Interactive animation : passing arrays to functions

Strings
In C a string is defined to be a null-terminated character array. The null character ('\0') is used to indicate the end of the string. Like any other arrays, C does not impose range checking of array indices for strings. Declaration of an array allocates a fixed space for it. You need not use the entire space. Instead you can store your data in the initial portion of the array. It is, therefore, necessary to put a boundary of the actual data. This is the reason why we passed the size parameter to foonction above. Strings handle it differently, namely by putting an explicit marker at the end of the actual data. Here is an example of a string: I I T K h a r a g p u r , 7 2 1 3 0 2 \ 0

0 1 2 3 4 5 6 7 8

9 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9

Here we use an array of size 30. The string "IIT Kharagpur, 721302" is stored in the first 21 locations. This is followed by the null character. A total of 22 characters is needed to represent this string of length 21. Whatever follows after this null character is irrelevant for defining the string. If we set the element at location 6 to '\0', the array looks like: I I T K h \ r a g p u r , 7 2 1 3 0 2 \

0 0 1 2 3 4 5 6 7 8

0 9 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9

Considered as a string this stands for "IIT Kh". Recall that C allows you to read from and write to the locations at indices 30,31,... of this array. These are memory locations not allocated to the array, since its size is 30. Writing beyond the allocated space is expected to corrupt memory or even raise fatal runtime errors (Segmentation faults). In particular, if you do not put the null character at the end of the string, C keeps on searching for it and may go out of the legal boundary and create troubles. C offers some built-in functions for working with strings. They assume (null-terminated) strings as input and create (null-terminated) strings. You do not have to append the null character explicitly. For example, the statement
strcpy(A,"IIT Kharagpur"); copies the string "IIT Kharagpur" to the character array A and also appends the required

null character at the end of it. In order to use these string functions you should #include <string.h>. No additional libraries need be linked during compilation time. The math library was quite different. Well, mathematics and mathematicians are traditionally known to be different from the rest of the lot!
int strlen (const char s[]);

Returns the length (the number of characters before the first null character) of the string s.
int strcmp (const char s[], const char t[]); Compares strings s and t. Returns 0 if the two strings are identical, a negative value if s is lexicographically smaller than t (i.e., if s comes before t in the standard dictionary order), and a positive value if s is lexicographically larger than t. int strncmp (const char s[], const char t[], size_t n); Compares the prefixes of strings s and t of length n. Returns 0, a negative value or a positive value according as whether the prefix of s is equal to, lexicographically smaller than or lexicographically larger than the prefix of t. If a string (s or t) is already of length l < n, then the first l characters of the string (i.e., the entire string) is considered for the comparison. The decision of strncmp

is based on the relative placement of the prefixes according to dictionary rules. For example, the string "IIT" comes before "MIT", "UIUC" and "IITian", but comes after "IIIT", "BITS" and "I" in the standard dictionary order. Note that string comparison is done in a case-sensitive manner. 'A' has the ASCII value (65) less than that for 'a' (95) and so 'A' comes before 'a' in the lexicographic order. It is more correct to say that comparison is done with respect to ASCII

values, whereas ASCII values are assigned to characters based broadly on the dictionary order. Case-sensitivity is inherent in ASCII. You have to live with it.
char *strcpy (char s[], const char t[]); Copies the string t to the string s. char *strncpy (char s[], const char t[] , size_t n); Copies the prefix of t of size n to s. Again if t is of size l < n, then only l characters are copied to s. In all these cases a trailing null character is also copied to s. char *strcat (char s[], const char t[]); Appends the string t and then the null character at the end of s. The string s (a

pointer, see below) is returned.


char *strncat (char s[], const char t[], size_t n); Appends the first n characters of t and then the null character at the end of s. If t is of length l < n, then only l characters of t are appended to s. The string s is

returned.
int *strchr (const char s[], int c);

Returns the pointer to the first occurrence of the integer c (treated as a character under the ASCII encoding). If the character c does not occur in s, the NULL pointer is returned.
int *strrchr (const char s[], int c);

Returns the pointer to the last occurrence of the integer c (treated as a character under the ASCII encoding). If the character c does not occur in s, the NULL pointer is returned.

Arrays and pointers


The double entendre of arrays constitutes a confusing and yet beautiful feature of C. An array is an array, if it is viewed so. One can access elements by the usual square bracket notation (like A[i]). In addition, an array A is also a pointer. You can assign pointers of similar types to A and do pointer arithmetic in order to navigate through the elements of the array. Consider an array of integers and an int pointer:
#define MAXSIZE 10 int A[MAXSIZE], *p;

The following are legal assignments for the pointer p:


p = A; */ p = &A[0]; */ p = &A[1]; */ p = &A[i]; */ /* Let p point to the i-th location of the array A /* Let p point to the 1-st location of the array A /* Let p point to the 0-th location of the array A /* Let p point to the 0-th location of the array A

Whenever p is assigned the value &A[i], the value *p refers to the array element A[i], and so also does p[0]. Pointers can be incremented and decremented by integral values. After the assignment p = &A[i]; the increment p++ (or ++p) lets p one element down the array, whereas the decrement p-- (or --p) lets p move by one element up the array. (Here "up" means one index less, and "down" means one index more.) Similarly, incrementing or decrementing p by an integer value n lets p move forward or backward in the array by n locations. Consider the following sequence of pointer arithmetic:
p = A; */ p++; p = p + 6; p += 2; --p; p -= 5; p -= 5; /* /* /* /* /* /* Now Now Now Now Now Now p p p p p p points points points points points points to to to to to to the the the the the the 1-st location of 7-th location of 9-th location of 8-th location of 3-rd location of (-2)-nd location A */ A */ A */ A */ A */ of A */ /* Let p point to the 0-th location of the array A

Oops! What is a negative location in an array? Like always, C is pretty liberal in not securing its array boundaries. As you may jump ahead of the position with the largest legal index, you are also allowed to jump before the opening index (0). Though C allows you to do so, your run-time memory management system may be unhappy with your unhealthy intrusion and may cause your program to have a premature termination (with the error message "Segmentation fault"). It is the programmer's duty to insure that his/her pointers do not roam around in prohibited areas. Here is an example of a function that computes the sum of the elements of an array. The naive method for doing so is:
int fooddition1 ( int A[] , int size ) { int i; int sum = 0; for (i=0; i<size; ++i) sum += A[i]; return (sum); }

The second method uses pointers:


int fooddition2 ( int A[] , int size ) { int i, *p; int sum = 0; p = A; /* Let p point to the 0-th location of A */ for (i=0; i<size; ++i) { sum += *p; /* Add to sum the element pointed to by p */ ++p; /* Let p point to the next location in A */ }

return (sum); }

Here is a third method that uses pointers in a subtler way:


int fooddition3 ( int A[] , int size ) { int i, *p; int sum = 0; p = A; for (i=0; i<size; ++i) sum += *(p + i); return (sum); }

Some key points need be highlighted now.

Pointers are addresses in memory. If that is so, it apparently does not matter whether it is a pointer to an int or a char or a double etc. But the pointer arithmetic brings out the difference. Different data types require different amounts of space. For example, an int typically requires 4 bytes, a char only one byte, a double eight bytes, and so on. Now when you talk about the pointer p+i, the actual memory address depends on how much (in bytes) one needs to advance p in order to generate the i-th address. The amount of advance is dependent on the data type that the pointer points to. For int pointers, p+i refers to 4i bytes ahead in memory. For char pointers, this is just i bytes ahead of p. Finally, for double pointers, p+i is 8i bytes ahead of p. The notation p+i is an abstraction that hides the details of organization of data in the memory. You don't have to remember how much space each data type requires. C will automatically advance your pointers by appropriate amounts. Arrays and pointers are almost the same, but not identical. You can assign addresses to pointers. But you are not allowed to do the same on arrays. An array can only be declared, but cannot be assigned. Only the elements of an array can be assigned values. For example, if we declare:
int A[MAXSIZE];

the following are not legal assignments:


A = &(A[2]); ++A;

However, statements like


p = A + i; sum += *(A + i);

are permitted, because they do not involve assignment of A.

In a function declaration (or prototype) where an array need be passed, we can pass a pointer instead. We have mentioned earlier that passing arrays to a function does not copy the array element-by-element. It only passes the address of the (0-th entry of the) array. We can substitute that by an explicit pointer. Here is how we can rewrite the fooddition routine.
int fooddition4 ( int *A , int size ) { int i = 0, sum = 0; while (i < size) { sum += *A; ++A; ++i; } return (sum); } int main () { int A[5] = {3, 5, 7, 11, 13}; int s; /* Compute the sum of all five elements of A */ s = fooddition4(A,5); /* Compute the sum of the first through third elements of A */ s = fooddition4(&A[1],3); }

The formal parameter A in fooddition4 is a pointer. It can be incremented (like ++A;). On the other hand, A refers to an array in main and so an increment like ++A; is not allowed in main.

Multi-dimensional arrays
One-dimensional arrays are quite able to represent many natural collections. There are some other natural collections that may better be conceptualized as 2-dimensional data. The first example is a matrix. What else can be a more natural 2-dimensional data other than a matrix whose entries are natural numbers? So think of the following 4x5 matrix:
1 2 4 8 1 3 9 27 1 1 1 4 5 6 16 25 36 64 125 216

We can write the entries in the row-major order and represent the resulting flattened data as a one-dimensional array:

1 1 1 64 125 216

16

25

36

27

As long as the column dimension of the matrix is known, the original matrix can be recovered easily from this 1-D array. More precisely, consider an m-by-n matrix (a matrix with m rows and n columns). It contains a total of mn elements. Let us number the rows 0,1,...,m-1 from top to bottom and the columns 0,1,...,n-1 from left to right. The entry at position (i,j) then maps to the (ni+j)-th entry of the one-dimensional array. On the other hand, the k-th entry of the one-dimensional array corresponds to the (i,j)-th element of the matrix, where i = k / n and j = k % n. One-dimensional arrays suffice. Still, it is convenient and intuitive to visualize matrices as two-dimensional arrays. C provides constructs to define and work with such arrays. Of course, the memory of a computer is typically treated as a one-dimensional list of memory cells. Any two-dimensional structure has to be flattened using a strategy like that mentioned above. C handles this for you. In other words, the abstraction relieves you from the task of doing the index arithmetic explicitly. You refer to the (i,j)-th element as the (i,j)-th element. C translates it into the appropriate address in the onedimensional memory. 2-dimensional arrays can be defined like one-dimensional arrays, but with two squarebracketed dimensions. For example, the declaration
int matrix[20][10];

allocates memory for a 20x10 array of int variables. The first index (20) indicates the number of rows allocated, whereas the second indicates the number of columns allocated. Here is another example:
#define MAXROW 50 #define MAXCOL 50 float M[MAXROW][MAXCOL];

Elements of a 2-D array can be initialized to constant values using nested curly braces:
int mat[4][5] = { { 1, 1, { 2, 3, { 4, 9, { 8, 27, };

1, 1, 1 4, 5, 6 16, 15, 25 64, 125, 216

}, }, }, }

/* /* /* /*

The The The The

zeroth row */ first row */ second row */ third row */

Rows of a 2-D array of characters can be initialized to constant strings.


char address[4][100] = { "Department of Foobarnautic Engineering", "Indian Institute of Technology", "Kharagpur 721302",

"India" };

For a 2-D array A the (i,j)-th element is treated as a variable and can be accessed by the name A[i][j]. Both the row numbering and the column numbering start from 0. For example, the (1,3)-th element of mat is accessed as mat[1][3] and, if initialized as above, stores the int value 5.

Animation example : in-place transpose of a matrix


2-D arrays can be passed to functions using a syntax similar to the declaration of 2-D arrays:
#define ROWDIM 10 #define COLDIM 12 int fooray ( int A[ROWDIM][COLDIM], int r , int c ) { ... }

Here the actual row and column dimensions of the used part of the array A are passed via the parameters r and c. It is not mandatory to specify the row dimension ROWDIM. But the column dimension COLDIM must be specified, since 2-D to 1-D mapping in memory requires the column dimension. Thus the declaration
int fooray ( int A[][COLDIM], int r , int c ) { ... }

is allowed, whereas the declarations


int fooray ( int A[][], int r , int c ) { ... }

and
int fooray ( int A[ROWDIM][], int r , int c ) { ... }

are not allowed.

Like 1-D arrays, 2-D arrays are not copied element-by-element to functions. A pointer is only passed. This implies that changes made to the array elements inside the function are visible outside the function. Indeed 2-D arrays are pointers too. However, these pointers are rather distinct in nature from those pointers that represent 1-D arrays. The situation is quite clumsy and confusing.
#define MAXROW 4 #define MAXCOL 5 int barsum ( int A[][MAXCOL] , int r , int c ) { int i, j, s; int (*p)[MAXCOL]; s = 0; p = A; for (i=0; i<r; ++i) for (j=0; j<c; ++j) s += p[i][j]; return s; }

The array A[][MAXCOL] can be assigned to the pointer p that should be declared as:
int (*p)[MAXCOL];

This declaration means that p is a pointer to an array of MAXCOL integers. The parentheses surrounding *p are absolutely necessary for this. The declaration
int *p[MAXCOL];

won't work in this context. The reason is that the array indicator [] has higher precedence than the pointer indicator *. Therefore, the last declaration is equivalent to
int *(p[MAXCOL]);

and means that p is an array of MAXCOL int pointers. This does not match the type of A. In addition, this does not match the dimension of A. There are four ways in which a 2-D array may be declared.
#define MAXROW 4 #define MAXCOL 5 int A[MAXROW][MAXCOL]; int (*B)[MAXCOL]; integers */ int *C[MAXROW]; int **D; /* A is a statically allocated array */ /* B is a pointer to an array of MAXCOL /* C is an array of MAXROW int pointers */ /* D is a pointer to an int pointer */

All these are essentially different in terms of memory management. Except the first array A, the three other arrays support dynamic memory allocation. When properly allocated memory, each of these can be used to represent a MAXROW-by-MAXCOL array. Moreover, in all the four cases the (i,j)-th entry of the array is accessed as Array_name[i][j]. The first two (A and B) are pointers to arrays, whereas the last two (C and D) are arrays of pointers. The following figure elaborates this difference.

Figure: Two-dimensional arrays

We will discuss more about two-dimensional arrays in the chapter on dynamic memory allocation.

Course home

CS13002 Programming and Data Structures

Spring semester

Structures
Now it is time to combine heterogeneous data to form a named collection. For example, think of a student's record that might comprise a name, a roll number, a height and a CGPA. A name and a roll number are strings, a height (in cms, rounded to the nearest integer) is an integer and a CGPA is a floating point value. A structure can be used to combine these different types of data into a single item. Moreover, each constituent field in the composite data is made individually accessible. What we benefit from using structures is a convenient and logical way of looking at and arranging data. That's the basic motivation behind every abstraction.

Defining structures
Structures can be defined by the struct keyword. For example, a student's record can be defined as:
#define MAXLEN 100 struct stud { char name[MAXLEN]; char roll[MAXLEN]; int height; float cgpa; };

This declaration gives a user-defined data of type struct stud that has four members: two character arrays of names name and roll, an integer named height and a floating point value named cgpa. The struct declaration only defines a data type but no instances of data of this type. In order to declare specific instances of structure data, one should employ the usual variable declaration procedure.
struct stud thatStudent, FBStudents[60], *studPointer;

This declaration defines a structure with the name thatStudent, an array FBStudents consisting of 60 student records and a pointer studPointer to a student record. A single instance of struct stud is depicted in the following figure.

Figure : Example of a simple structure A second example is provided by complex numbers which can be represented as pairs of real numbers. One can use the following structure:
struct comp { double real; double imag; };

Here the two fields of the structure are of the same data type and so one can in fact use a double array of size 2 to represent a complex number. However, the above definition enhances readability and highlights the logical (mathematical) structure of a complex number. One then uses the declaration
struct comp z, z1, z2;

to obtain specific instances of complex numbers.

Type definitions
The typedef declarations are used to rename data types in C. For example, if one plans to work with unsigned long long int variables, but plans not to write that big a name, one may define the following short-cut:

typedef unsigned long long int ull;

After this definition, the data type unsigned long long int can be called also as ull, i.e., one can declare variables as:
ull n, array[100], *ptr;

One can also typedef pointers and arrays:


typedef ull *ullPointer; typedef ull ullArray[128];

Here we assume that unsigned long long int is already typedef'd as ull. We use this definition to typedef two other data types. First, ullPointer is defined to be a pointer to an unsigned long long int, whereas ullArray is defined to be an array of 128 unsigned long long int data. One can instantiate data of these types in the usual way:
ullPointer p;

defines a pointer variable p, whereas


ullArray A;

defines an array A of 128 unsigned long long int data. In an analogous way, one can use typedef's to give short single-word names to structure data types. For example, the declarations
typedef struct stud student; typedef struct comp complex;

give names student and complex to the user-defined data types struct stud and struct comp. The following variable declarations are legal after these typedef's:
student thatStudent, FBStudents[60], *studPointer; complex z, z1, z2;

One can combine a struct declaration and a subsequent typedef as follows:


typedef struct stud { char name[MAXLEN]; char roll[MAXLEN]; int height; float cgpa; } student; typedef struct { double real;

double imag; } complex;

Since the new struct data type is now given an explicit name (like student or complex), it is not necessary to give any tag after the keyword struct. This is what we have done for the complex data type. Notice, however, that using a tag after struct is not prohibited (see the definition of student) and is indeed essential in a particular situation that we will describe towards the end of this chapter.

Initializing structures
Structures can be initialized much in the same way as arrays can be -- by a curly-braced comma-separated list of initializing constant values for the individual members. For example, the above student record can be initialized as:
struct stud thatStudent = { "Foolan Barik", "03FB1331", 175, 9.81 };

or with the typedef'd name as:


student thatStudent = { "Foolan Barik", "03FB1331", 175, 9.81 };

Initializing values populate the members of the variable in the same order as they appear in the struct declaration. For the above example, the string name receives the value "Foolan Barik", roll the value "03FB1331", height the value 175 and cgpa the value 9.81.

Accessing members of structures


Accessing individual members of a structure is different from what is done with arrays. Now one should write the name of a structure variable followed by a dot (.) and then by the formal name given to the member. For example, if thatStudent is initialized as above, thatStudent.name refers to the string "Foolan Barik", thatStudent.roll refers to the string "03FB1331", thatStudent.height refers to the integer value 175 and thatStudent.cgpa to the floating point value 9.81. If we have an array of structures, one first uses square brackets to refer to an element of the array and then uses dot and a member name to access the corresponding member of the structure, For example, FBStudents[5].height refers to the height field of the element at index 5 in the array FBStudents. If we have a pointer to a structure, we first dereference the pointer in order to obtain the structure and then write dot and the member name. For example, if studPointer is a pointer to a struct stud, the notation (*studPointer).roll refers to the string holding the roll number of the student whose records are pointed to by the pointer studPointer. There is an alternative way of writing the same thing: studPointer-

>roll. The dereferencing * and the dot . are combined to the symbol -> which C

designers deemed to be an intuitive and natural notation. Example: The following function computes the average CGPA of the students of the Department of Foobarnautic Engineering:
float avCGPA ( struct stud FBStudents[] , int n ) { float sum = 0; int i; for (i=0; i<n; ++i) sum += FBStudents[i].cgpa; return (sum/(float)n); }

Here is how you can do the same with pointers:


float avCGPA2 ( struct stud FBStudents[] , int n ) { float sum = 0; int i; struct stud *p; p = FBStudents; for (i=0; i<n; ++i) { sum += p->cgpa; ++p; } return (sum/(float)n); }

Passing structures to functions


Syntactically, structures are treated analogously as normal (built-in) variables. Passing structure variables to and from functions follows the call-by-value mechanism explained earlier. Example: Let us write a function that accepts two complex structures as arguments and returns the structure representing the product of these two arguments.
#include <stdio.h> struct comp { float real; float imag; }; struct comp cmul ( struct comp z1 , struct comp z2 ) { struct comp z; z.real = (z1.real) * (z2.real) - (z1.imag) * (z2.imag);

z.imag = (z1.real) * (z2.imag) + (z1.imag) * (z2.real); return z; } int main () { struct comp a = {1.1,2.2}, b = {2.4,-3.6}, c; c = cmul(a,b); printf("product = (%f)+i(%f)\n", c.real, c.imag); }

This program outputs:


product = (10.560000)+i(1.320000)

Animation example : passing structures to functions


What requires explanation is what is meant by call-by-value in connection with structure arguments. A structure requires some amount of memory to accommodate all the defining members. This size (in bytes) can be accessed by the sizeof call, like:
printf("Size of struct stud = %d\n", sizeof(struct stud));

In fact, the sizeof statement can be used for any data type, built-in or user-defined. For example, sizeof(int) typically returns 4, sizeof(double) returns 8, sizeof(char) returns 1 and so on. For structures, the value returned by the sizeof statement is dependent on the compiler and the architecture of the underlying machine. When a structure is passed to a function, the corresponding sizeof() bytes are copied to the formal argument of the function. For example, in my machine sizeof(struct stud) is 208. This includes locations to store the arrays name and roll, the integer height and the floating point number cgpa. When a struct stud variable is passed to a function, these 208 bytes are copied to the argument. This, in particular, implies that changes in the members of the argument are not visible outside the function. This also includes changes in the arrays name and roll. When a struct stud value is returned from a function and assigned to a variable in the caller function, 208 bytes are copied from the returned value to the variable. Let us now define a structure with pointers:
struct stud2 { char *name; char *roll; int height; float cgpa; };

Figure : Example of a structure with pointers Now sizeof(struct stud2) is 16. This is what is needed to store two pointers, one integer and one floating point number. These pointers may point to arrays (or may be allocated memory dynamically), but the memory for these arrays lies outside the structure variable. When we pass a struct stud2 variable to a function, only 16 bytes are copied. That includes the pointers name and roll, but not the arrays which they point to. Any change in the arrays pointed to by these pointers is now visible to the caller function. Arrays and pointers are similar, but not the same thing!

Structures with self-referencing pointers


A structure with pointer(s) to structure(s) of the same type turns out to be very useful for representing many interesting objects. The following figure illustrates how such structures form the basic building block (a node) for representing a list and a tree. We will see later how such objects can be dynamically created and maintained. For the time being, let us focus on how a structure representing a node in a list or tree can be defined.

Figure : Example of structures with self-referencing pointers (a) List structure (b) Tree structure First consider a node in a list. Let us assume that we are dealing with a list of integers. In order to create the linked structure of the above figure, we need a node to contain a pointer to another node of the same type. In practice, a node may contain data other than an integer and a pointer. For simplicity here we restrict the members of a node to only these two fields.

struct _listnode { int data; struct _listnode *next; };

One can also use type definitions:


typedef struct _listnode { int data; struct _listnode *next; } listnode;

An important thing to note here is that the formal tag after the struct keyword (_listnode in the last example) was absolutely necessary for these declarations, even when the new structure is typedef'd. There is nothing other than this formal name that can specify the type of the pointer next. It is only after the part inside curly braces can be defined properly, when the typedef makes sense. After these definitions we can use individual variables and pointers. The declaration
listnode mynode, *head;

defines a structure mynode of type listnode and a pointer head to a structure of this type. A node in a (binary) tree consists of two pointers, the first for pointing to the left child and the second for pointing to the right child.
typedef struct _treenode { int data; struct _treenode *left; struct _treenode *right; } treenode;

After this definition one can declare individual nodes like:


treenode thatNode, leaf[100];

One can declare pointers to nodes in the usual way:


treenode *root;

or by using other type definitions:


typedef treenode *tnptr; tnptr root;

We will shortly use such linked structures with dynamic memory allocation for realizing several useful (abstract) objects.

Unions
Suppose we want to make a list of nodes. Each node in the list may be one of two possible types: a data node and a control node. Suppose further that a data node stores an int, whereas a control node stores a control information that can be specified by a 16character string. A structure like the following can be used:
struct foonode { int data; char control[16]; } thisNode, fooArray[1024];

The problem with this is that irrespective of whether a node is a control node or a data node, the structure requires space for both the data and the control string. A data node does not use the control string at all, and similarly a control node does not require the data. That leads to unnecessary waste of space. In order to reduce the space requirement of each node, we should use a union instead of a struct.
union barnode { int data; char control[16]; } thisNode, barArray[1024];

In this case the compiler reserves the space that is sufficient to store the biggest of the individual members. For example, the int member requires 4 bytes, whereas the control string requires 16 bytes. For the struct foonode the compiler uses 20 bytes of memory. For the union barnode, on the other hand, a memory of only 16 bytes is allocated. That memory (more correctly, a part of it) can be used as an integer variable or as a character string. In other words, the members of a union occupy overlapping space. When we say thatNode.data or barArray[51].data, the content of the memory is interpreted as an integer, whereas thatNode.control or barArray[51].control refers to a character string. This may seem confusing initially, because it is not clear what data is actually stored in the memory. Interpreting a character string as an integer need not always make sense, and vice versa. The information regarding what kind of data a union stores is to be maintained externally, i.e., outside the union. One possibility is to use unions in conjunction with structures.
#define DATA_NODE 0 #define CONTROL_NODE 1 struct foobarnode { int what; /* can be either DATA_NODE or CONTROL_NODE */ union { int data; char control[16]; } info; } thatNode, foobarArray[1024];

This structure stores the type of the node and then the union of an integer and a character string. Depending on the value of what, the programmer is to interpret the type of the node. If what is set to DATA_NODE, one should use the union info as an integer data and access this as thatNode.info.data or as foobarArray[131].info.data. On the other hand, if what is set to CONTROL_NODE, one should use the union as a character string that can be accessed as thatNode.info.control or as foobarArray[131].info.control. Here is another example, in which a node contains a union of three different kinds of data.
#include <stdio.h> typedef struct _foostruct { int intArray[512]; double dblArray[128]; char chrArray[1024]; struct _foostruct *next; } foostruct; typedef struct _barstruct { int type; union { int intArray[512]; double dblArray[128]; char chrArray[1024]; } data; struct _barstruct *next; } barstruct; int main () { printf("sizeof(foostruct) = %d\n", sizeof(foostruct)); printf("sizeof(barstruct) = %d\n", sizeof(barstruct)); }

In my machine, this program outputs:


sizeof(foostruct) = 4100 sizeof(barstruct) = 2056

Look at the space saving effected by using the union. Note also that the next pointer should be there in every node irrespective of its type. That is why this pointer should be declared outside the union.

Course home

CS13002 Programming and Data Structures

Spring semester

Pointers and dynamic memory allocation


All variables, arrays, structures and unions that we worked with so far are statically allocated, meaning that whenever an appropriate scope is entered (e.g. a function is invoked) an amount of memory dependent on the data types and sizes is allocated from the stack area of the memory. When the program goes out of the scope (e.g. when a function returns), this memory is returned back to the stack. There is an alternative way of allocating memory, more precisely, from the heap part of the memory. In this case, the user makes specific calls to capture some amount of memory and continues to hold that memory unless it is explicitly (i.e., by distinguished calls) returned back to the heap. Such memory is said to be dynamically allocated. In order to exemplify the usefulness of dynamic memory allocation, suppose that there are two types of foomatic collections: the first type refers to an array of ten integers, whereas the second type refers to an array of ten million integers. A foomatic chain is made from a combination of one million collections of first type and few (say, ten) collections of the second type. Such a chain demands a total memory capable of holding 110 million integers. Assuming that an integer is of size 32 bits, this amounts to a memory of 440 Megabytes. A modern personal computer usually has enough memory to accommodate this data. It is a foomatic convention to treat both types of collection uniformly, i.e., our plan is to represent both by a single data type. Think of the difference between you and me. I am the instructor (synonymously the president) of the class, whereas students are only listeners (synonymous with citizens). A president is the most important person in a society, he requires microphones, computers, bla bla bla. Still, both the president and each citizen are of the same data type called human. So a foollection is a foomatic human capable of representing a collection of either type. If we plan to handle it using a structure with an array (or union), we must prepare for the bigger collections. The definition goes like this:
typedef struct { int type; int data[10000000]; } foollection;

Now irrespective of what a foollection data actually stores, it requires memory for ten million and one integers. (Think of each of you being given a PA system and a computer in the class.) A foomatic chain then requires over 40,000 Gigabytes of memory. This is

sheer waste of space, since only 440 Megabytes suffice. Moreover, no personal computer I have heard of comes with so much memory including hard disks. What is the way out? Let us plan to redefine foollection in the following way:
typedef struct { int type; int *data; } foollection;

I have replaced the static array by a pointer. We will soon see that a pointer can be allocated memory from the heap and that the amount of memory to be allocated to each pointer can be specified during the execution of the program. Thus the data pointer in a foollection variable is assigned exactly as much memory as is needed. (It is as if when I come to the classroom, the run-time system gives me a PA system and a computer, whereas a student is given only a comfortable chair.) Now each collection requires, in addition to the actual data array, the space for an int variable and for a pointer, typically demanding 4 bytes each. So a foomatic chain requires a space overhead of slightly more than 8 Megabytes, i.e., a chain with all foomatic abstractions now fits in a memory of size less than 450 Megabytes. My computer has this much space. Let me illustrate another situation where dynamic memory allocation proves to be extremely useful. Look at lists and trees made up of structures with self-referencing pointers:

Figure: Dynamic lists A static array can implement such lists, but has two disadvantages:

The size of a static array is fixed during declaration, i.e., a static array can handle lists of a bounded size. Even if my machine has more memory than yours, I cannot leverage this superiority of my computer with static arrays. On the other extreme, irrespective of the actual size of the collection, a static array necessarily consumes the entire space for the biggest supportable collection.

The linked structure can be incorporated in the framework of an array, but that requires (often awful) calculations to find the locations of the next objects. If pointers with dynamically assigned memory are used, accessing objects following the links becomes much easier.

So there is a big bunch of reasons why we should jump for dynamic memory management. Do it. But listen to the standard good advice from me. Dynamic memory allocation gives a programmer too much control of memory. Inexperienced programmers do not know how to effectively exploit that control. There remains every chance that everything gets repeatedly goofed up and the programmer, tired of fighting with segmentation faults for weeks, eventually gives up and joins the ice-cream industry. If you excel in this new job, I won't mind, even given that I am not a particular fan of icecreams. But my job is to teach you programming, not how to manufacture tasty icecreams.

One-dimensional dynamic memory


The built-in function malloc allocates a one-dimensional array to a pointer. You have to specify the total amount of memory (in bytes) that you would like to allocate to the pointer.
#define SIZE1 25 #define SIZE2 36 int *p; long double *q; p = (int *)malloc(SIZE1 * sizeof(int)); q = (long double *)malloc(SIZE2 * sizeof(long double));

The first call of malloc allocates to p a (dynamic) array capable of storing SIZE1 integers. The second call allocates an array of SIZE2 long double data to the pointer q. In addition to the size of each array, we need to specify the sizeof (size in bytes of) the underlying data type. malloc allocates memory in bytes and reads the amount of bytes needed from its sole argument. If you demand more memory than is currently available in your system, malloc returns the NULL pointer. So checking the allocated pointer for NULLity is the way how one can check if the allocation request has been successfully processed by the memory management system.
malloc allocates raw memory from some place in the heap. No attempts are made to

initialize that memory. It is the programmer's duty to initialize and then use the values stored at the locations of a dynamic array.

Animation example : 1-D dynamic memory

Example: Let us now write a function that allocates an appropriate amount of memory to a foollection structure based on the type of the collection it is going to represent.
foollection initfc ( int type ) { foollection fc; /* Set type of the collection */ fc.type = type; /* Allocate memory for the data pointer */ if (type == 1) fc.data = (int *)malloc(10*sizeof(int)); else if (type == 2) fc.data = (int *)malloc(10000000*sizeof(int)); else fc.data = NULL; /* Check for error conditions */ if (fc.data == NULL) fprintf(stderr, "Error: insufficient memory or unknown type.\n"); return fc; }

Example: Let us now create a linked list of 4 nodes holding the integer values 3,5,7,9 from start to end. For simplicity we do not check for error conditions.
typedef struct _node { int data; struct _node *next; } node; node *head, *p; int i; head = (node *)malloc(sizeof(node)); head->data = 3; */ p = head; /* Next p will navigate down the list */ for (i=1; i<=3; ++i) { p->next = (node *)malloc(sizeof(node)); /* Allocate the next node */ p = p->next; /* Advance p by one node */ p->data = 2*i+3; /* Set data */ } p->next = NULL; /* Terminate the list by NULL */ /* Create the first node */ /* Set data for the first node

An important thing to notice here is that we are always allocating memory to p->next and not to p itself. For example, first consider the allocation of head and subsequently an allocation of p assigned to head->next.
head = (node *)malloc(sizeof(node));

p = head->next; p = (node *)malloc(sizeof(node));

After the first assignment of p, both this pointer and the next pointer of *head point to the same location. However, they continue to remain different pointers. Therefore, the subsequent memory allocation of p changes p, whereas head->next remains unaffected. For maintaining the list structure we, on the other hand, want head->next to be allocated memory. So allocating the running pointer p is an error. One should allocate p->next with p assigned to head (not to head->next). Now p and head point to the same node and, therefore, both p->next and head->next refer to the same pointer -- the one to which we like to allocate memory in the subsequent step. This example illustrates that the first node is to be treated separately from subsequent nodes. This is the reason why we often maintain a dummy node at the head and start the actual data list from the next node. We will see many examples of this convention later in this course. There are two other ways by which memory can be allocated to pointers. The calloc call takes two arguments, a number n of cells and a size s of a data, and returns an allocated array capable of storing n objects each of size s. Moreover, the allocated memory is initialized to zero. If the allocation request fails, the NULL pointer is returned.
#define FOO_CHAIN_SIZE 1000000 typedef struct { int type; int *data; } foollection; foollection *foochain; foochain = (foollection *)calloc(FOO_CHAIN_SIZE,sizeof(foollection));

This call creates an array of one million foollection structures (or NULL if the machine cannot provide the requested amount of memory). Each structure in the array is initialized to zero, i.e., each foochain[i].type is set to 0 and each foochain[i].data is set to NULL. The realloc call reallocates memory to a pointer. It is essentially used to change the amount of memory allocated to some pointer. If the new size s' of the memory is larger than the older size s, then s bytes are copied from the old memory to the new memory. The remaining s'-s bytes are left uninitialized. On the contrary, if s'<s, then only s' bytes are copied. If the reallocation request fails, the original pointer remains unchanged and the NULL pointer is returned.

As an example, suppose that we want to change the size of the dynamic array pointed to by foochain from one million to two millions, but without altering the data currently stored in the array. We can use the following call:
#define NEW_SIZE 2000000 foochain = realloc(foochain, NEW_SIZE * sizeof(foollection)); if (foochain == NULL) fprintf(stderr, "Error: unable to reallocate storage.\n");

Memory allocated by malloc, calloc or realloc can be returned to the heap by the free system call. It takes an allocated pointer as argument. For example, the foochain pointer can be deallocated memory by the call:
free(foochain);

When a program terminates, all allocated memory (static and dynamic) is returned to the system. There is no necessity to free memory explicitly. However, since memory is a bounded resource, allocating it several times, say, inside a loop, may eventually let the system run out of memory. So it is a good programming practice to free memory that will no longer be used in the program.

Two-dimensional dynamic memory


Allocating two-dimensional memory is fundamentally similar to allocating onedimensional memory. One uses the same calls (malloc, etc.) described in the previous section. One should only be careful about the allocation sizes and the return types. Recall that we have four ways of declaring two-dimensional arrays. These are summarized below:
#define ROWSIZE 100 #define COLSIZE 200 int int int int A[ROWSIZE][COLSIZE]; (*B)[COLSIZE]; *C[ROWSIZE]; **D;

The first array A is fully static. It cannot be allocated or deallocated memory dynamically. As the definition of A is encountered, the required amount of space is allocated to A from the stack area of the memory. When the definition of A expires (i.e., the scope of A ends, say, due to return from a function or exit from a block), the static memory is returned back to the stack. Each of the three other arrays (B,C,D) has a dynamic component in it. Let us study them case-by-case.
B is a pointer to an array of COLSIZE integers. So it can be allocated ROWSIZE rows in the

following way:

B = (int (*)[COLSIZE])malloc(ROWSIZE * sizeof(int[COLSIZE]));

The same can be achieved in a more readable way as follows:


typedef int matrow[COLSIZE]; B = (matrow *)malloc(ROWSIZE * sizeof(matrow)); C is a static array of ROWSIZE int pointers. Therefore, C itself cannot be allocated or deallocated memory. The individual rows of C should be allocated memory. int i; for (i=0; i<ROWSIZE; ++i) C[i] = (int *)malloc(COLSIZE * sizeof(int)); D is dynamic in both directions. First, it should be allocated memory to store ROWSIZE int pointers each meant for a row of the 2-D array. Each row pointer, in turn, should be allocated memory for COLSIZE int data. int i; D = (int **)malloc(ROWSIZE * sizeof(int *)); for (i=0; i<ROWSIZE; ++i) D[i] = (int *)malloc(COLSIZE * sizeof(int));

The last two pointers C,D allow rows of different sizes, since each row is allocated memory individually. That's all! It may be somewhat confusing to understand the differences among these four cases. Things become clearer once you realize what type of pointer each of A,B,C,D is.

Animation example : 2-D dynamic memory


Though the internal organizations of these arrays are quite different in the memory, their access mechanism is the same in the sense that the same notation Array_name[i][j] refers to the i,j-th entry in each of the four arrays. In order to promote this uniformity, the C compiler has to be quite fussy about the types of these arrays. Typecasting among these four types is often a crime that may result in mild warnings to failure of compilation to segmentation faults. Take sufficient care. Beware of the ice-cream industry! The freeing mechanism is also different for the four arrays.
int i; /* A is a static array and cannot be free'd */ /* B is a single pointer */

free(B); /* C is a static array of pointers each to be free'd individually */ for (i=0; i<ROWSIZE; ++i) free(C[i]); /* Free each row */ /* D is a pointer to pointers. Each of these pointers is to be free'd */ for (i=0; i<ROWSIZE; ++i) free(D[i]); /* Free each row */ free(D); /* Free the row top */

I think it suffices to learn to work with only the completely static (A) and the completely dynamic (D) versions of 2-D arrays. They are my personal favorites and any-time recommendations. Still, if you care, here follows a program that shows you the internal organizations of each memory cell and each row header for these four kinds of arrays. The addresses are displayed as byte distances relative to the header of the entire matrix.
#include <stdio.h> #define ROWSIZE 4 #define COLSIZE 5 int int int int A[ROWSIZE][COLSIZE]; (*B)[COLSIZE]; *C[ROWSIZE]; **D;

int main () { int i, j; printf("\nArray A\n"); printf("sizeof(*A) = %d\n",sizeof(*A)); printf(" j=0 j=1 j=2 j=3 j=4\n"); printf(" +------------------------------+\n"); for (i=0; i<ROWSIZE; ++i) { printf("A[%d] = %4d : i=%d |", i, (int)A[i]-(int)A, i); for (j=0; j<COLSIZE; ++j) printf("%6d", (int)(&A[i][j])-(int)A); printf(" |\n"); } printf(" +------------------------------+\n"); printf("\nArray B\n"); B = (int (*)[COLSIZE])malloc(ROWSIZE * sizeof(int[COLSIZE])); printf("sizeof(*B) = %d\n",sizeof(*B)); printf(" j=0 j=1 j=2 j=3 j=4\n"); printf(" +------------------------------+\n"); for (i=0; i<ROWSIZE; ++i) { printf("B[%d] = %4d : i=%d |", i, (int)B[i]-(int)B, i); for (j=0; j<COLSIZE; ++j)

printf("%6d", (int)(&B[i][j])-(int)B); printf(" |\n"); } printf(" +\n"); +-------------------------------

printf("\nArray C\n"); for (i=0; i<ROWSIZE; ++i) C[i] = (int *)malloc(COLSIZE * sizeof(int)); printf("sizeof(*C) = %d\n",sizeof(*C)); printf(" j=0 j=1 j=2 j=3 j=4\n"); printf(" +------------------------------+\n"); for (i=0; i<ROWSIZE; ++i) { printf("C[%d] = %4d : i=%d |", i, (int)C[i]-(int)C, i); for (j=0; j<COLSIZE; ++j) printf("%6d", (int)(&C[i][j])-(int)C); printf(" |\n"); } printf(" +------------------------------+\n"); printf("\nArray D\n"); D = (int **)malloc(ROWSIZE * sizeof(int *)); for (i=0; i<ROWSIZE; ++i) D[i] = (int *)malloc(COLSIZE * sizeof(int)); printf("sizeof(*D) = %d\n",sizeof(*D)); printf(" j=0 j=1 j=2 j=3 j=4\n"); printf(" +------------------------------+\n"); for (i=0; i<ROWSIZE; ++i) { printf("D[%d] = %4d : i=%d |", i, (int)D[i]-(int)D, i); for (j=0; j<COLSIZE; ++j) printf("%6d", (int)(&D[i][j])-(int)D); printf(" |\n"); } printf(" +------------------------------+\n"); }

A sample output of this program executed in my machine follows:


Array A sizeof(*A) = 20 A[0] A[1] A[2] A[3] = = = = 0 20 40 60 : : : : i=0 i=1 i=2 i=3 j=0 j=1 j=2 j=3 j=4 +-------------------------------+ | 0 4 8 12 16 | | 20 24 28 32 36 | | 40 44 48 52 56 | | 60 64 68 72 76 | +-------------------------------+

Array B sizeof(*B) = 20 j=0 j=1 j=2 j=3 j=4 +-------------------------------+

B[0] B[1] B[2] B[3]

= = = =

0 20 40 60

: : : :

i=0 i=1 i=2 i=3

| 0 4 8 12 16 | | 20 24 28 32 36 | | 40 44 48 52 56 | | 60 64 68 72 76 | +-------------------------------+

Array C sizeof(*C) = 4 C[0] C[1] C[2] C[3] = = = = 1004 1028 1052 1076 : : : : i=0 i=1 i=2 i=3 j=0 j=1 j=2 j=3 j=4 +-------------------------------+ | 1004 1008 1012 1016 1020 | | 1028 1032 1036 1040 1044 | | 1052 1056 1060 1064 1068 | | 1076 1080 1084 1088 1092 | +-------------------------------+

Array D sizeof(*D) = 4 D[0] D[1] D[2] D[3] = = = = 24 48 72 96 : : : : i=0 i=1 i=2 i=3 j=0 j=1 j=2 j=3 j=4 +-------------------------------+ | 24 28 32 36 40 | | 48 52 56 60 64 | | 72 76 80 84 88 | | 96 100 104 108 112 | +-------------------------------+

Course home

CS13002 Programming and Data Structures

Spring semester

Abstract data types


You are now a master C programmer. You know most of the essential features of C. So, given a problem, you plan to jump to write the code. Yes, I see that you have mentally written the #include line. Please wait. Solving a problem is a completely different game. Writing the final code is only a tiny part of it. I admit that even when all the algorithms are fully specified, writing a good program is not always a joke, in particular, when the program is pretty huge and involves cooperation of many programmers. Think of the Linux operating system which was developed by thousands of free-lance programmers all over the globe. The system works pretty harmoniously and reportedly with much less bugs than software from commercial giants like Microsoft. First notice that your code may be understood and augmented by third parties in your absence. Even if you flood your code with documentation, its readability is not ensured. An important thing you require is a design document. That is not at the programming level, but at a more abstract level. Data abstraction is the first step. A problem is a problem of its own nature. It deals with input and output in specified formats not related to any computer program. For example, a weather forecast system reads gigantic databases and outputs some prediction. Where is C coming in the picture in this behavioral description? One can use any other computer language, perhaps assembly languages, or even hand calculations, to arrive at the solution. Assume that you are taught the natural language English pretty well. You are also given a plot. Your problem is to write an attractive detective story in English. Is it a trivial matter? I think it is not quite so, at least for most of us. You have to carefully plan about your characters, the location, the sequence, the suspense, and what not. Each such planning step involves many things that have nothing to do with English. The murderer is to be modeled as a human being, an abstract data, together with a set of behaviors, a set of abstract procedures. There is no English till this point. A language is necessary only when you want to give a specific concrete form to these abstract things. Still, you cannot perhaps be a Conan Doyle or Christie, neither in plot design nor in expressions. Well, they are geniuses. However, if you plan carefully and master English reasonably well to arrive at a decent and sleek production, who knows, you may be the script-writer for the next Bollywood blockbuster?

What is an abstract data type?


An abstract data type (ADT) is an object with a generic description independent of implementation details. This description includes a specification of the components from which the object is made and also the behavioral details of the object. Instances of abstract objects include mathematical objects (like numbers, polynomials, integrals, vectors), physical objects (like pulleys, floating bodies, missiles), animate objects (dogs, Pterodactyls, Indians) and objects (like poverty, honesty, inflation) that are abstract even in the natural language sense. You do not see C in Pterodactyls. Only when you want to simulate a flying Pterodactyl, you would think of using a graphics package in tandem with a computer language. Similarly, inflation is an abstract concept. When you want to model it and want to predict it for the next 10 years, you would think of writing an extrapolation program in C. Specifying only the components of an object does not suffice. Depending on the problem you are going to solve, you should also identify the properties and behaviors of the object and perhaps additionally the pattern of interaction of the object with other objects of same and/or different types. Thus in order to define an ADT we need to specify:

The components of an object of the ADT. A set of procedures that provide the behavioral description of objects belonging to the ADT.

There may be thousands of ways in which a given ADT can be implemented, even when the coding language remains constant. Any such implementation must comply with the content-wise and behavioral description of the ADT. Examples

Integers: An integer is an abstract data type having the standard mathematical meaning. In order that integers may be useful, we also need to specify operations (arithmetic operations, gcd, square root etc.) and relations (ordering, congruence etc.) on integers. Real numbers: There are mathematically rigorous ways of defining real numbers (Dedekind cuts, completion of rational numbers, etc). To avoid these mathematical details, let us plan to represent real numbers by decimal expansions (not necessarily terminating). Real numbers satisfy standard arithmetic and other operations and the usual ordering. Complex numbers: A complex number may be mathematically treated as an ordered pair of real numbers. An understanding of real numbers is then sufficient to represent complex numbers. However, the complex arithmetic is markedly different from the real arithmetic. Polynomials with real (or complex or integer or rational) coefficients with the standard arithmetic. Matrices with real (or complex or integer or rational) entries with the standard matrix arithmetic (which may include dimension, rank, nullity, etc).

Sets are unordered collections of elements. We may restrict our study to sets of real (or complex) numbers and talk about union, intersection, complement and other standard operations on sets. A multiset is an unordered collection of elements (say, numbers), where each element is allowed to have multiple occurrences. For example, an aquarium is a multiset of fish types. One can add or delete fishes to or from an aquarium. A book is an ADT with attributes like name, author(s), ISBN, number of pages, subject, etc. You may think of relations like comparison of difficulty levels of two books.

How to implement an abstract data type?


It is now and only now when you think about writing C codes. Carefully investigate the specification of the ADT and possible target applications where this ADT is going to be used. Plan for suitable C constructs to provide the appropriate functionality with good performance. Try to exploit your experience with C. But fully understand what you are going to implement, the limitations, the expected performance figures, the ease of code maintenance, and a lot of related issues. After all, you have to market your product. Examples

Integers: Oh, my! C provides so many integer variables and still I have to write my integers. Yep! You may have to. For most common-place applications C's built-in integer data types are sufficient. But not always. Suppose my target application is designing a cryptosystem, where one deals with very big integers, like those of bit-sizes one to several thousand bits. Our C's maximum integer length is 64 bits. That is grossly inadequate to address the cryptosystem designer's problem. ANSI standards dictate use of integers of length at most 32 bits, which are even poorer for cryptography, but at the minimum portable across platforms. At any rate, you need your customized integer data types. A common strategy is to break big integers into pieces and store each piece in a built-in data type. To an inexperienced user breaking with respect to the decimal representation seems easy and intuitive. But computer's world is binary. So breaking with respect to the binary representation is much more efficient in terms of space and running time. So we plan to use an array of unsigned long variables to store the bits of a big integer. Each such variable is a 32-bit word and is capable of storing 32 bits of a big integer. Therefore, if we plan to work with integers of size no larger than 10,000 bits, we require an array of size no more than 313 unsigned long variables. The zeroth location of the array holds the least significant 32 bits of a big integer, the first location the next 32 bits, and so on. Since all integers are not necessarily of size 10,000 bits, it is also necessary to store the actual word-size of a big integer. Finally, if we also plan to allow negative integers, we should also reserve a location for storing the sign information. So here is a possible implementation of the big integer data type.

typedef struct { unsigned long words[313]; unsigned int wordSize; unsigned char sign; } bigint;

This sounds okay, but has an efficiency problem. When you pass a bigint data to a function, the entire words array is copied element-by-element. That leads to unreasonable overheads during parameter passing. We can instead use an array of 315 unsigned long variables and use its 313-th and 314-th locations to store the size and sign information. The first 313 locations (at indexes 0 through 312) represent the magnitude of the integer as before.
#define SIZEIDX 313 #define SIGNIDX 314 typedef unsigned long goodbigint[315];

Now goodbigint is a simple array and so passing it to a function means only a pointer is passed. Quite efficient, right? These big integers are big enough for cryptographic applications, but cannot represent integers bigger than big, for example, integers of bit-size millions to billions. Whenever we use static arrays, we have to put an upper limit on the size. If we have to deal with integers of arbitrary sizes (as long as memory permits), we have no option other than using dynamic memory and allocate the exact amount of memory needed to store a very big integer. But then since the maximum index of the dynamic array is not fixed, we have to store the size and sign information at the beginning of the array. Thus the magnitude of the very big integer is stored starting from the second array index. This leads to somewhat clumsy translation between word indices and array indices.
#define SIZEIDX 0 #define SIGNIDX 1 typedef unsigned long *verybigint;

A better strategy is to use a structure with a dynamic words pointer.


typedef struct { unsigned long *words; unsigned int size; unsigned char sign; } goodverybigint;

So you have to pay a hell lot of attention, when implementation issues come. Good solutions come from experience and innovativeness. Being able to define integers for a variety of applications is not enough. We need to do arithmetic (add, subtract, multiply etc.) on these integers. It is beyond the scope of this elementary course to go into the details of these arithmetic routines.

It suffices here only to highlight the difference between abstract specifications and application-specific implementations. Both are important.

Real numbers: Again C provides built-in implementations of real numbers: float, double and long double. If one has to use floating point numbers of higher precision, one has to go for private floating point data types and write arithmetic routines for these new data types. These are again topics too advanced for this course. Complex numbers: If we are happy with real numbers of double precision, the most natural way to define a complex number is the following:
typedef struct { double real; double imag; } complex;

Let us also illustrate the implementation of some arithmetic routines on complex numbers:
complex cadd ( complex z1 , complex z2 ) { complex z; z.real = z1.real + z2.real; z.imag = z1.imag + z2.imag; return z; } complex cmul ( complex z1 , comple z2 ) { complex z; z.real = z1.real * z2.real - z1.imag * z2.imag; z.imag = z1.real * z2.imag + z1.imag * z2.real; return z; } complex conj ( complex z1 ) { complex z; z.real = z1.real; z.imag = -z1.imag; return z; } void cprn ( complex z ) { printf("(%lf) + i(%lf)", z.real, z.imag); }

Matrices: Suppose we want to work with matrices having complex entries and suppose that the complex ADT has been defined as above. We may define matrices of bounded sizes as:
#define MAXROW 10 #define MAXCOL 15

typedef struct { int rowdim; int coldim; complex entry[MAXROW][MAXCOL]; } matrix;

Let us now implement some basic arithmetic operations on these matrices.


matrix msetid ( int n ) { matrix C; int i, j; if ((n > MAXROW) || (n > MAXCOL)) { fprintf(stderr, "msetid: Matrix too big\n"); C.rowdim = C.coldim = 0; return C; } C.rowdim = C.coldim = n; for (i = 0; i < C.rowdim; ++i) { for (j = 0; j < C.coldim; ++j) { A.entry[i][j].real = (i == j) ? 1 : 0; A.entry[i][j].imag = 0; } } return C; } matrix madd ( matrix A , matrix B ) { matrix C; int i, j; if ((A.rowdim != B.rowdim) || (A.coldim != B.coldim)) { fprintf(stderr, "madd: Matrices of incompatible dimensions\n"); C.rowdim = C.coldim = 0; return C; } C.rowdim = A.rowdim; C.coldim = A.coldim; for (i = 0; i < C.rowdim; ++i) for (j = 0; j < C.coldim; ++j) C.entry[i][j] = cadd(A.entry[i][j],B.entry[i][j]); return C; } matrix mmul ( matrix A , matrix B ) { matrix C; int i, j, k; complex z; if (A.coldim != B.rowdim) {

fprintf(stderr, "mmul: Matrices of incompatible dimensions\n"); C.rowdim = C.coldim = 0; return C; } C.rowdim = A.rowdim; C.coldim = B.coldim; for (i = 0; i < A.rowdim; ++i) { for (j = 0; j < B.coldim; ++j) { C.entry[i][j].real = 0; C.entry[i][j].imag = 0; for (k = 0; k < A.coldim; ++k) { z = cmul(A.entry[i][k], B.entry[k][j]); C.entry[i][j] = cadd(C.entry[i][j],z); } } } return C; }

A complete example : the ordered list ADT


Let us now define a new ADT which has not been encountered earlier in your math courses. We call this ADT the ordered list. It is a list of elements, say characters, in which elements are ordered, i.e., there is a zeroth element, a first element, a second element, and so on, and in which repetitions of elements are allowed. For an ordered list L, let us plan to have the following functionality:
L = init();

Initialize L to an empty list. L = insert(L,ch,pos); Insert the character ch at position pos in the list L and return the modified list. Report error if pos is not a valid position in L. delete(L,pos); Delete the character at position pos in the list L. Report error if pos is not a valid position in L. isPresent(L,ch); Check if the character ch is present in the list L. If no match is found, return -1, else return the index of the leftmost match.
getElement(L,pos);

Return the character at position pos in the list L. Report error if pos is not a valid position in L.
print(L);

Print the list elements from start to end. We will provide two complete implementations of this ADT. We assume that the element positions are indexed starting from 0.

Implementation using static arrays

Let us restrict the number of elements in the ordered list to be <= 100. One can then use an array of characters of this size. Moreover, one needs to maintain the current size of the list. Thus the list data type can be defined as:
#define MAXLEN 100 typedef struct { int len; char element[MAXLEN]; } olist;

Let us now implement all the associated functions one by one.


olist init () { olist L; L.len = 0; return L; } olist insert ( olist L , char ch , int pos ) { int i; if ((pos < 0) || (pos > L.len)) { fprintf(stderr, "insert: Invalid index %d\n", pos); return L; } if (L.len == MAXLEN) { fprintf(stderr, "insert: List already full\n"); return L; } for (i = L.len; i > pos; --i) L.element[i] = L.element[i-1]; L.element[pos] = ch; ++L.len; return L; } olist delete ( olist L , int pos ) { int i; if ((pos < 0) || (pos >= L.len)) { fprintf(stderr, "delete: Invalid index %d\n", pos); return L; } for (i = pos; i <= L.len - 2; ++i) L.element[i] = L.element[i+1]; --L.len; return L; } int isPresent ( olist L , char ch ) { int i; for (i = 0; i < L.len; ++i) if (L.element[i] == ch) return i; return -1;

} char getElement ( olist L , int pos ) { if ((pos < 0) || (pos >= L.len)) { fprintf(stderr, "getElement: Invalid index %d\n", pos); return '\0'; } return L.element[pos]; } void print ( olist L ) { int i; for (i = 0; i < L.len; ++i) printf("%c", L.element[i]); }

Here is a possible main() function with these calls.


int main () { olist L; L = init(); L = insert(L,'a',0); printf("Current list is : "); L = insert(L,'b',0); printf("Current list is : "); L = delete(L,5); printf("Current list is : "); L = insert(L,'c',1); printf("Current list is : "); L = insert(L,'b',3); printf("Current list is : "); L = delete(L,2); printf("Current list is : "); L = insert(L,'z',8); printf("Current list is : "); L = delete(L,2); printf("Current list is : "); printf("Element at position 1 }

print(L); printf("\n"); print(L); printf("\n"); print(L); printf("\n"); print(L); printf("\n"); print(L); printf("\n"); print(L); printf("\n"); print(L); printf("\n"); print(L); printf("\n"); is %c\n", getElement(L,1));

Here is the complete program.

Animation example : Implementation of the ordered list ADT with static memory Implementation using linked lists

Let us now see an implementation based on dynamic linked lists. We use the same prototypes for function calls. But we define the basic data type olist in a separate manner. For the sake of ease of writing the functions, we maintain a dummy node at the beginning of the linked list.
typedef struct _node { char element; struct _node *next; } node; typedef node *olist; olist init () { olist L; /* Create the dummy node */ L = (node *)malloc(sizeof(node)); L -> element = '\0'; L -> next = NULL; return L; } olist insert ( olist L , char ch , int pos ) { int i; node *p, *n; if (pos < 0) { fprintf(stderr, "insert: Invalid index %d\n", pos); return L; } p = L; i = 0; while (i < pos) { p = p -> next; if (p == NULL) { fprintf(stderr, "insert: Invalid index %d\n", pos); return L; } ++i; } n = (node *)malloc(sizeof(node)); n -> element = ch; n -> next = p -> next; p -> next = n; return L; } olist delete ( olist L , int pos ) { int i; node *p; if (pos < 0) { fprintf(stderr, "delete: Invalid index %d\n", pos);

return L; } p = L; i = 0; while ((i < pos) && (p -> next != NULL)) { p = p -> next; ++i; } if (p -> next == NULL) { fprintf(stderr, "delete: Invalid index %d\n", pos); return L; } p -> next = p -> next -> next; return L; } int isPresent ( olist L , char ch ) { int i; node *p; i = 0; p = L -> next; while (p != NULL) { if (p -> element == ch) return i; p = p -> next; ++i; } return -1; } char getElement ( olist L , int pos ) { int i; node *p; i = 0; p = L -> next; while ((i < pos) && (p != NULL)) { p = p -> next; ++i; } if (p == NULL) { fprintf(stderr, "getElement: Invalid index %d\n", pos); return '\0'; } return p -> element; } void print ( olist L ) { node *p; p = L -> next; while (p != NULL) { printf("%c", p -> element); p = p -> next;

} }

The main() function of the static array implementation can be used without any change under this implementation. Here is the complete program.

Animation example : Implementation of the ordered list ADT with dynamic memory
This exemplifies that the abstract properties and functional behaviors are independent of the actual implementation, or stated in another way, our two implementations of the ordered list ADT correctly and consistently tally with the abstract specification. And why should we stop here? There could be thousand other ways in which the same ADT can be implemented, and in all these cases the function prototypes may be so chosen that the same main() function will work. This is the precise difference between an abstract specification and particular implementations.

Course home

CS13002 Programming and Data Structures

Spring semester

Stacks and queues


Stacks and queues are special kinds of ordered lists in which insertion and deletion are restricted only to some specific positions. They are very important tools for solving many useful computational problems. Since we have already implemented ordered lists in the most general form, we can use these to implement stacks and queues. However, because of the special insertion and deletion patterns for stacks and queues, the ADT functions can be written to be much more efficient than the general functions. Given the importance of these new ADTs, it is worthwhile to devote time to these special implementations.

The stack ADT and its applications


A stack is an ordered list of elements in which elements are always inserted and deleted at one end, say the beginning. In the terminology of stacks, this end is called the top of the stack, whereas the other end is called the bottom of the stack. Also the insertion operation is called push and the deletion operation is called pop. The element at the top of a stack is frequently referred, so we highlight this special form of getElement. A stack ADT can be specified by the following basic operations. Once again we assume that we are maintaining a stack of characters. In practice, the data type for each element of a stack can be of any data type. Characters are chosen as place-holders for simplicity.
S = init();

Initialize S to an empty stack.


isEmpty(S);

Returns "true" if and only if the stack S is empty, i.e., contains no elements.
isFull(S);

Returns "true" if and only if the stack S has a bounded size and holds the maximum number of elements it can.
top(S);

Return the element at the top of the stack S, or error if the stack is empty.
S = push(S,ch);

Push the character ch at the top of the stack S.


S = pop(S);

Pop an element from the top of the stack S.


print(S);

Print the elements of the stack S from top to bottom. An element popped out of the stack is always the last element to have been pushed in. Therefore, a stack is often called a Last-In-First-Out or a LIFO list.

Applications of stacks Stacks are used in a variety of applications. While some of these applications are "natural", most other are essentially "pedantic". Here is a list anyway.

For processing nested structures, like checking for balanced parentheses, evaluation of postfix expressions. For handling function calls and, in particular, recursion. For searching in special data structures (depth-first search in graphs and trees), for example, for implementing backtracking.

Animation example : Use of stacks to evaluate postfix expressions Interactive animation : Use of stacks to evaluate postfix expressions

Implementations of the stack ADT


A stack is specified by the ordered collection representing the content of the stack together with the choice of the end of the collection to be treated as the top. The top should be so chosen that pushing and popping can be made as far efficient as possible.

Using static arrays


Static arrays can realize stacks of a maximum possible size. If we assume that the stack elements are stored in the array starting from the index 0, it is convenient to take the top as the maximum index of an element in the array. Of course, the other choice, i.e., the other boundary 0, can in principle be treated as the top, but insertions and deletions at the location 0 call for too many relocations of array elements. So our original choice is definitely better.
#define MAXLEN 100 typedef struct { char element[MAXLEN]; int top; } stack; stack init () { stack S; S.top = -1; return S; } int isEmpty ( stack S ) { return (S.top == -1);

} int isFull ( stack S ) { return (S.top == MAXLEN - 1); } char top ( stack S ) { if (isEmpty(S)) { fprintf(stderr, "top: Empty stack\n"); return '\0'; } return S.element[S.top]; } stack push ( stack S , char ch ) { if (isFull(S)) { fprintf(stderr, "push: Full stack\n"); return S; } ++S.top; S.element[S.top] = ch; return S; } stack pop ( stack S ) { if (isEmpty(S)) { fprintf(stderr, "pop: Empty stack\n"); return S; } --S.top; return S; } void print ( stack S ) { int i; for (i = S.top; i >= 0; --i) printf("%c",S.element[i]); }

Here is a possible main() function calling these routines:


int main () { stack S; S = init(); printf("Current stack : "); print(S); printf(" with top = %c.\n", top(S)); S = push(S,'d'); printf("Current stack : "); print(S); printf(" with top = %c.\n", top(S)); S = push(S,'f'); printf("Current stack : "); print(S); printf(" with top = %c.\n", top(S));

S = push(S,'a'); printf("Current stack : "); print(S); printf(" with top = %c.\n", top(S)); S = pop(S); printf("Current stack : "); print(S); printf(" with top = %c.\n", top(S)); S = push(S,'x'); printf("Current stack : "); print(S); printf(" with top = %c.\n", top(S)); S = pop(S); printf("Current stack : "); print(S); printf(" with top = %c.\n", top(S)); S = pop(S); printf("Current stack : "); print(S); printf(" with top = %c.\n", top(S)); S = pop(S); printf("Current stack : "); print(S); printf(" with top = %c.\n", top(S)); S = pop(S); printf("Current stack : "); print(S); printf(" with top = %c.\n", top(S)); }

Here is the complete program. The output of the program is given below:
top: Empty stack Current stack : with top = . Current stack : d with top = d. Current stack : fd with top = f. Current stack : afd with top = a. Current stack : fd with top = f. Current stack : xfd with top = x. Current stack : fd with top = f. Current stack : d with top = d. top: Empty stack Current stack : with top = . pop: Empty stack top: Empty stack Current stack : with top = .

Animation example : Implementation of stacks with static memory Using dynamic linked lists
As we have seen earlier, it is no big deal to create and maintain a dynamic list of elements. The only consideration now is to decide whether the beginning or the end of the list is to be treated as the top of the stack. Deletion becomes costly, if we choose the end of the list as the top. Choosing the beginning as the top makes the implementations of both push and pop easy. So we stick to this convention. As usual, we maintain a dummy node at the top (beginning) for simplifying certain operations. The ADT functions are implemented below:
typedef struct _node { char element; struct _node *next; } node; typedef node *stack; stack init ()

{ stack S; /* Create the dummy node */ S = (node *)malloc(sizeof(node)); S -> element = '\0'; S -> next = NULL; return S; } int isEmpty ( stack S ) { return (S -> next == NULL); } int isFull ( stack S ) { /* With dynamic memory the stack never gets full. However, a new allocation request may fail because of memory limitations. That may better be checked immediately after each malloc statement is executed. For simplicity we avoid this check in this implementation. */ return 0; } char top ( stack S ) { if (isEmpty(S)) { fprintf(stderr, "top: Empty stack\n"); return '\0'; } return S -> next -> element; } stack push ( stack S , char ch ) { node *T; if (isFull(S)) { fprintf(stderr, "push: Full stack\n"); return S; } /* Copy the new element in the dummy node */ S -> element = ch; /* Create a new dummy node */ T = (node *)malloc(sizeof(node)); T -> element = '\0'; T -> next = S; return T; } stack pop ( stack S ) { if (isEmpty(S)) {

fprintf(stderr, "pop: Empty stack\n"); return S; } /* Treat the stack top as the new dummy node */ S -> next -> element = '\0'; return S -> next; } void print ( stack S ) { node *T; T = S -> next; while (T != NULL) { printf("%c", T -> element); T = T -> next; } }

These new functions are compatible with the main() function of the implementation using arrays. The complete program is here.

Animation example : Implementation of stacks with dynamic linked lists

The queue ADT and its applications


A queue is like a "natural" queue of elements. It is an ordered list in which all insertions occur at one end called the back or rear of the queue, whereas all deletions occur at the other end called the front or head of the queue. In the popular terminology, insertion and deletion in a queue are respectively called the enqueue and the dequeue operations. The element dequeued from a queue is always the first to have been enqueued among the elements currently present in the queue. In view of this, a queue is often called a FirstIn-First-Out or a FIFO list. The following functions specify the operations on the queue ADT. We are going to maintain a queue of characters. In practice, each element of a queue can be of any welldefined data type.
Q = init();

Initialize the queue Q to the empty queue.


isEmpty(Q);

Returns "true" if and only if the queue Q is empty.


isFull(Q);

Returns "true" if and only if the queue Q is full, provided that we impose a limit on the maximum size of the queue.
front(Q);

Returns the element at the front of the queue Q or error if the queue is empty.
Q = enqueue(Q,ch);

Inserts the element ch at the back of the queue Q. Insertion request in a full queue should lead to failure together with some appropriate error messages.
Q = dequeue(Q);

Delete one element from the front of the queue Q. A dequeue attempt from an empty queue should lead to failure and appropriate error messages.
print(Q);

Print the elements of the queue Q from front to back. Applications of queues

For implementing any "natural" FIFO service, like telephone enquiries, reservation requests, traffic flow, etc. For implementing any "computational" FIFO service, for instance, to access some resources. Examples: printer queues, disk queues, etc. For searching in special data structures (breadth-first search in graphs and trees). For handling scheduling of processes in a multitasking operating system.

Animation example : Use of queues for round-robin scheduling

Implementations of the queue ADT


Continuing with our standard practice followed so far, we are going to provide two implementations of the queue ADT, the first using static memory, the second using dynamic memory. The implementations aim at optimizing both the insertion and deletion operations.

Using static arrays


Recall that in our implementation of the "ordered list" ADT we always let the list start from the array index 0. This calls for relocation of elements of the list in the supporting array after certain operations (usually deletion). Now we plan to exploit the specific insertion and deletion patterns in queues to avoid these costly relocations. We maintain two indices to represent the front and the back of the queue. During an enqueue operation, the back index is incremented and the new element is written in this location. For a dequeue operation, on the other hand, the front is simply advanced by one position. It then follows that the entire queue now moves down the array and the back index may hit the right end of the array, even when the size of the queue is smaller than the capacity of the array. In order to avoid waste of space, we allow our queue to wrap at the end. This means that after the back pointer reaches the end of the array and needs to proceed further down the line, it comes back to the zeroth index, provided that there is space at the beginning of the array to accommodate new elements. Thus, the array is now treated as a circular one with index MAXLEN treated as 0, MAXLEN + 1 as 1, and so on. That is, index calculation is done

modulo MAXLEN. We still don't have to maintain the total queue size. As soon as the back index attempts to collide with the front index modulo MAXLEN, the array is considered to be full. There is just one more problem to solve. A little thought reveals that under this wraparound technology, there is no difference between a full queue and an empty queue with respect to arithmetic modulo MAXLEN. This problem can be tackled if we allow the queue to grow to a maximum size of MAXLEN - 1. This means we are going to lose one available space, but that loss is inconsequential. Now the condition for full array is that the front index is two locations ahead of the back modulo MAXLEN, whereas the empty array is characterized by that the front index is just one position ahead of the back again modulo MAXLEN. An implementation of the queue ADT under these design principles is now given.
#define MAXLEN 100 typedef struct { char element[MAXLEN]; int front; int back; } queue; queue init () { queue Q; Q.front = 0; Q.back = MAXLEN - 1; return Q; } int isEmpty ( queue Q ) { return (Q.front == (Q.back + 1) % MAXLEN); } int isFull ( queue Q ) { return (Q.front == (Q.back + 2) % MAXLEN); } char front ( queue Q ) { if (isEmpty(Q)) { fprintf(stderr,"front: Queue is empty\n"); return '\0'; } return Q.element[Q.front]; } queue enqueue ( queue Q , char ch ) {

if (isFull(Q)) { fprintf(stderr,"enqueue: Queue is full\n"); return Q; } ++Q.back; if (Q.back == MAXLEN) Q.back = 0; Q.element[Q.back] = ch; return Q; } queue dequeue ( queue Q ) { if (isEmpty(Q)) { fprintf(stderr,"dequeue: Queue is empty\n"); return Q; } ++Q.front; if (Q.front == MAXLEN) Q.front = 0; return Q; } void print ( queue Q ) { int i; if (isEmpty(Q)) return; i = Q.front; while (1) { printf("%c", Q.element[i]); if (i == Q.back) break; if (++i == MAXLEN) i = 0; } }

Here is a sample main() for these functions.


int main () { queue Q; Q = init(); printf("Current queue : "); print(Q); printf("\n"); Q = enqueue(Q,'h'); printf("Current queue : "); print(Q); printf("\n"); Q = enqueue(Q,'w'); printf("Current queue : "); print(Q); printf("\n"); Q = enqueue(Q,'r'); printf("Current queue : "); print(Q); printf("\n"); Q = dequeue(Q); printf("Current queue : "); print(Q); printf("\n"); Q = dequeue(Q); printf("Current queue : "); print(Q); printf("\n"); Q = enqueue(Q,'c'); printf("Current queue : "); print(Q); printf("\n"); Q = dequeue(Q); printf("Current queue : "); print(Q); printf("\n");

Q = dequeue(Q); printf("Current queue : "); print(Q); printf("\n"); Q = dequeue(Q); printf("Current queue : "); print(Q); printf("\n"); }

Finally, this is the output of the complete program.


Current queue : Current queue : h Current queue : hw Current queue : hwr Current queue : wr Current queue : r Current queue : rc Current queue : c Current queue : dequeue: Queue is empty Current queue :

Animation example : Implementation of queues with static memory Using dynamic linked lists
Linked lists can be used for implementing queues. We plan to maintain a dummy node at the beginning and two pointers, the first pointing to this dummy node and the second pointing to the last element. Both insertion and deletion are easy at the beginning. Insertion is easy at the end, but deletion is difficult at the end, since we have to move the pointer at the end one step back and there is no way other than traversing the entire list in order to trace the new end. So the natural choice is to take the beginning of the linked list as the front of the queue and the end of the list as the back of the queue. The corresponding implementation is detailed below:
typedef struct _node { char element; struct _node *next; } node; typedef struct { node *front; node *back; } queue; queue init () { queue Q; /* Create the dummy node */ Q.front = (node *)malloc(sizeof(node)); Q.front -> element = ' '; Q.front -> next = NULL; Q.back = Q.front;

return Q; } int isEmpty ( queue Q ) { return (Q.front == Q.back); } int isFull ( queue Q ) { return 0; } char front ( queue Q ) { if (isEmpty(Q)) { fprintf(stderr,"front: Queue is empty\n"); return '\0'; } return Q.front -> element; } queue enqueue ( queue Q , char ch ) { node *C; if (isFull(Q)) { fprintf(stderr,"enqueue: Queue is full\n"); return Q; } /* Create new node */ C = (node *)malloc(sizeof(node)); C -> element = ch; C -> next = NULL; /* Adjust the back of queue */ Q.back -> next = C; Q.back = C; return Q; } queue dequeue ( queue Q ) { if (isEmpty(Q)) { fprintf(stderr,"dequeue: Queue is empty\n"); return Q; } /* Make the front of the queue the new dummy node */ Q.front = Q.front -> next; Q.front -> element = '\0'; return Q; } void print ( queue Q )

{ node *G; G = Q.front -> next; while (G != NULL) { printf("%c", G -> element); G = G -> next; } }

And here is the program with a main() identical to that for the array implementation.

Animation example : Implementation of queues with dynamic linked lists


Course home

CS13002 Programming and Data Structures

Spring semester

Performance analysis of programs


There may exist many algorithms to solve a given problem. Moreover, the same algorithm may be implemented in a variety of ways. It is now time to analyze the relative merits and demerits of different algorithms and of different implementations. We are shortly going to introduce a set of theoretical tools that make this analysis methodical. In order to solve a problem, it is not sufficient to design an algorithm and provide a correct (bug-free) implementation of the algorithm. One should also understand how good his/her program is. Here "goodness" is measured by relative performance of a program compared to other algorithms and implementations. This is where the true "computer science" starts. Being able to program does not qualify one to be a computer scientist or engineer. A good programmer is definitely a need for every modern society and it requires time and patience to master the art of good programming. Computer science goes beyond that by providing formal treatment of programs and algorithms. Consider that an airplane pilot is not required to know any aerospace engineering and an aerospace engineer need not know how to run an airplane. We need both pilots and aerospace engineers for our society. In short, when you write (and perhaps sell) your programs or when you buy others' programs, you should exercise your ability to compare the apparent goodness of available programs. The task is not always easy. Here are some guidelines for you.

Resource usage of a program


You may feel tempted to treat a small and compact program as good. That is usually not a criterion for goodness. Most users execute precompiled binaries. It does not matter how the source code looked like. What is more important is how efficiently a program solves a problem. Efficiency, in turn, is measured by the resource usage of a program (or algorithm). The two most important general resources are:

Running time Space requirement

Here running time refers to the amount of time taken by a program for a given input. The time is expected to vary according to what input the program processes. For example, a program that does matrix multiplication should take more running time for bigger matrices. It is, therefore, customary to view the running time as a function of the input. In order to simplify matters, we assume that the size of the input is the most important thing (instead of the actual input). For instance, a standard matrix multiplication routine performs a number of operations (additions and multiplications of elements) that is

dependent only on the dimensions of the input matrices and not on the actual elements of the matrices. A standard practice is to measure the size of the input by the number of bits needed to represent the input. However, this simple convention is sometimes violated. For example, when an array of integers need be sorted, it is customary to take the size of the array (number of integers in the array) as the input size. Under the assumption that each integer is represented by a word of a fixed size (e.g. 32 bits), the bit-size of the input is actually a constant multiple of the array size, and so this new definition of input size is not much of a deviation from the standard convention. (We will soon see that constant multipliers are neglected in the theory.) However, when the integers to be sorted may be arbitrarily large (multiple precision integers), this naive negligence of the sizes of the operands is certainly not a good idea. One should in this case consider the actual sizes (in bits or words) of each individual element of the array. Poorer measures of input size are also sometimes adopted. An nxn matrix contains n2 elements and even if each element is of fixed size (like int or float), one requires a number of bits proportional to n2 for the complete specification of the matrix. However, when we plan to multiply two nxn matrices or invert an nxn matrix, it is seemigly more human to treat n (instead of n2) as the input size parameter. In other words, the running time is expressed as a function of n and not as a function of the technically correct input size n2 (though they essentially imply the same thing). Space (memory) requirement of a program is the second important criterion for measuring the performance of a program. Obviously, a program requires space to store (and subsequently process) the input. What matters is the additional amount of memory (static and dynamic) used by the program. This extra storage is again expressed as a function of the input size. It is often advisable to reduce the space requirement of a program. Earlier, semiconductor memory happenned to be a costly resource and so programs requiring smaller amount of extra memory are prefered to programs having larger space requirements. Nowadays, the price of semiconductor memory has gone down quite dramatically. Still, a machine comes with only a finite amount of memory. This means that a program having smaller space requirement can handle bigger inputs given any limited amount of memory. The essential argument in favor of reducing the space requirement of a program continues to make sense today and will do so in any foreseeable future. Some other measures of performance of a program may be conceived of, like ease of use (the user interface), features, security, etc. These requirements are usually applicationspecific. We will not study them in a general forum like this chapter. Palatable and logical as it sounds, it is difficult to express the running time and space usage of a program as functions of the input size. The biggest difficulty is to choose a unit of time (or space). Since machines vary widely in speed and memory management issues, the same program running on one machine may have markedly different

performance figures on another machine. A supercomputer is expected to multiply two given matrices much faster than a PC. There is no standard machine for calibrating the relative performances of different programs. Comparison results on a particular machine need not tally with the results on a different machine. Since different machines have different CPU and memory organizations and support widely varying instruction sets, results from one machine cannot, in general, be used to predict program behaviors on another machine. That's a serious limitation of machine-dependent performance figures. There is a hell lot of other factors that lead to variations in the running time (and space usage) of a program, even when the program runs on the same machine. These variations are caused by off-line factors (like the compiler used to compile the program), and also by many run-time factors (like the current load of the CPU, the current memory usage, availability of cache memory and swap memory). To sum up, neither seconds (or its fractions like microseconds) nor machine cycles turn out to be a faithful measure of the running time of a program. Similarly, memory usage cannot be straightaway measured in bits or words. We must invent some kind of abstractions. The idea is to get rid of the dependence on the run-time environment (including hardware). A second benefit is that we don't have to bother about specific implementations. An argument in the algorithmic level would help us solve our problem of performance measurement. The abstraction is based on the measurement of time by numbers. Such a number signifies the count of basic operations performed by the program (or algorithm). A definition of what is basic will lead to controversies. In order to avoid that, we will adopt our own conventions. An arithmetic operation (addition, subtraction, multiplication, division, comparison, etc. of integers or floating point numbers) will be treated as a basic operation. So will be a logical and a bitwise operation (AND, OR, shift etc.). We will count (in terms of the input size) how many such basic operations are performed by the program. That number will signify the abstract running time of the program. Usually, the actual running times of different programs are closely proportional to these counts. The constant of proportionality depends on the factors mentioned above and vary from machine to machine (and perhaps from time to time on the same machine). We will simply ignore these constants. This practice will bring under the same umbrella two programs doing n2 and 300n2 basic operations respectively for solving the same problem. Turning blind to constants of proportionality is not always a healthy issue. Nonetheless, the solace is that these abstract measures have stood the taste of time in numerous theoretical and practical situations. The space requirement of a program can be quantified along similar lines, namely, by the number of basic units (integers or floating-point numbers or characters) used by a program. Again we ignore proportionality constants and do not talk about the requirement of memory in terms of bits.

We will call the above abstract running time and space usage of a program to be its time complexity and space complexity respectively. In the rest of this chapter, we will concentrate mostly on time complexity.

The order notation


The order notation captures the idea of time and space complexities in precise mathematical terms. Let us start with the following important definition: Let f and g be two positive real-valued functions on the set N of natural numbers. We call g(n) to be of the order of f(n) if there exist a positive real constant c and a natural number n0 such that g(n) <= cf(n) for all n >= n0. In this case we write g(n) = O(f(n)) and also say that g(n) is big-Oh of f(n). The following figure illustrates the big-Oh notation. In this example, we take c=2.

Figure: Explaining the big-Oh notation Examples

Take f(n) = n and g(n) = 2n + 3. For n >= 3 we have 2n + 3 <= 3n. Thus taking the constants c = 3 and n0 = 3 shows that 2n + 3 = O(n). Conversely, for any n >= 1 we have n <= 2n + 3, i.e., n = O(2n + 3) too. Since 100n <= n2 for all n >= 100, we have 100n = O(n2). Now I will show that n2 is not of the order of 100n. Assume otherwise, i.e., n2 = O(100n), i.e., there exist constants c and n0 such that n2 <= 100cn for all n >= n0. This implies that n <= 100c for all n >= n0. This is clearly absurd, since c is a finite positive real number. The last two examples can be easily generalized. Let f(n) be a polynomial in n of degree d. It can be easily shown that f(n) = O(nd). In other words, the highest degree term in a polynomial determines its order. That is intuitively clear, since as n becomes sufficiently large, the largest degree term dominates over other terms.

Now let g(n) be another polynomial in n of degree e. Assume that d <= e. Then it can be shown that f(n) = O(g(n)). If d = e, then g(n) = O(f(n)) too. However, if d < e, then g(n) is not O(f(n)). Any function that is O(nd) for some positive integral constant d is said to be of polynomial order. A function which is O(n) is said to be of linear order. We can analogously define functions of quadratic order (O(n2) functions), cubic order (O(n3) functions), and so on.

A distinguished case of polynomial order O(nd) corresponds to the value d = 0. A function f(n) of this order is an O(1) function. For all sufficiently big n, f(n) is by definition bounded from above by a constant value and so is said to have constant order. I will now show n = O(2n). We prove by induction on n that n <= 2n for all n >= 1. This is certainly true for n = 1. So assume that n >= 2 and that n 1 <= 2n-1. We also have 1 <= 2n-1. Adding the two inequalities gives n <= 2n. The converse of the last order relation is not true, i.e., 2n is not of the order of n. We prove this by contradiction. Assume that 2n = O(n), i.e., 2n <= cn for all n >= n0. Simple calculus shows that the function 2x / x on a real variable x tends to infinity as x tends to infinity. In particular, c cannot be a bounded constant in this case. A function which is O(an) for some real constant a > 1 is said to be of exponential order. It can be shown that for any a > 1 and d >= 1 we have nd = O(an), but an is not of the order of nd. In other words, any polynomial function grows more slowly than a (truly) exponential function.

A similar comparison holds between logarithmic and polynomial functions. For any positive integers d and e, the function (log n)d is O(ne), but ne is not O((log n)d). Functions of polynomial, exponential and logarithmic orders are most widely used for analyzing algorithms.

We now explain how the order notation is employed to characterize the time and space complexities of a program. We count the number of basic operations performed by an algorithm and express that count as having the order of a simple function. For example, if an algorithm performs 2n2 - 3n + 1 operations on an input of size n, we say that the algorithm runs in O(n2) time, i.e., in quadratic time, or that it is a quadratic time algorithm. Any algorithm that runs in polynomial time is said to be a polynomial-time algorithm. An algorithm that does not run in polynomial time, but in exponential time, is called an exponential-time algorithm. An exponential function (like 2n) grows so rapidly (compared to polynomial functions) with the input n that exponential-time algorithms are usually much slower compared to polynomial-time algorithms, even when the input is not too big. By an efficient solution of a problem, one typically means devising an algorithm for that problem, that runs in some polynomial time O(nd) with d as small as possible.

Examples We now analyze the complexities of some popular algorithms discussed earlier in the notes.

Computation of factorials In this case we express the running-time as a function of the integer n whose factorial is to be computed. Let us first look at the following iterative algorithm:
int factorialIter ( int n ) { int prod, i; if (n <= 1) return 1; prod = 1; for (i=2; i<=n; ++i) prod *= i; return prod; }

The function first compares n with 1. If n is indeed less than or equal to 1, the constant value 1 is returned. Thus for n = 0,1 the algorithm does only one basic operation (comparison). Here we neglect the cost of returning a value. If n > 2, then prod is first initialized to 1. Then the loop starts. The loop contains an initialization of i, exactly n-1 increments of i and exactly n comparisons of i with n. Inside the function body the variable prod is multiplied by i. The loop is executed n-1 times. This accounts for a total of n-1 multiplications. Thus the total number of basic operations done by this iterative function is
1 + 1 + 1 + (n-1) + n + (n-1) = 3n + 1.

Since 3n + 1 is O(n), it follows that the above algorithm runs in linear time. Next consider the following recursive function for computing factorials:
int factorialRec ( int n ) { if (n <= 1) return 1; return n * factorialRec(n-1); }

Let T(n) denote the running time of this recursive algorithm for the input n. If n = 0,1, then T(n) = 1, since computation in these cases involves only a single comparison. If n >= 2, then in addition to this comparison, factorialRec is called on input n-1 and then the return value is multiplied by n. To sum up, we have:
T(0) = 1, T(1) = 1,

T(n) = 1 + T(n-1) + 1 = T(n-1) + 2 for n >= 2.

This is not a closed-form expression for T(n). A formula for T(n) can be derived by repeatedly using the last relation until the argument becomes too small (0 or 1) so that the constant value 1 can be substitued for it.
T(n) = T(n-1) + 2 = (T(n-2) + 2) + 2 = T(n-2) + 4 = T(n-3) + 6 ... = T(1) + 2(n-1) = 1 + 2(n-1) = 2n - 1.

Therefore,
T(0) = 1, T(n) = 2n - 1 for n >= 1.

It follows that the recursive function also runs in linear time. Note that both the iterative and recursive versions run in O(n) time. But the actual running times are respectively 3n + 1 and 2n - 1. It may appear to the reader that the recursive function is faster (since 2 is smaller than 3). But in the analysis, we have neglected the cost of function calls and returns. The iterative version makes no recursive calls, whereas the recursive version makes n-1 recursive calls. It depends on the compiler and the run-time system whether n-1 recursive calls is slower or faster than the overhead associated with the loop in the iterative version. Still, we should feel happy to end the story by rephrasing the fact that both the two versions are equally efficient -- as efficient as an O(n) function.

Computation of Fibonacci numbers With Fibonacci numbers, the iterative and recursive versions exhibit marked difference in running times. We start with the iterative version.
int fibIter ( int n ) { int i, p1, p2, F; if (n <= 1) return n; i = 1; F = 1; p1 = 0; while (i < n) { ++i; p2 = p1; p1 = F; F = p1 + p2; } return F; }

The function initially makes a comparison and if n = 0,1 the value n is returned. For n >= 2, it proceeds further down. First, three variables (i,F,p1) are initialized. The subsequent while loop is executed exactly n-1 times. The body of the loop involves four basic operations (one increment, two copies and one addition). Moreover, the loop continuation condition is checked n times. So the number of basic operations performed by this iterative algorithm is
1 + 3 + 4(n-1) + n = 5n.

In particular, fibIter runs in linear time. Let us now investigate the recursive version:
int fibRec ( int n ) { if (n <= 1) return n; return fibRec(n-1) + fibRec(n-2); }

Let T(n) denote the running time of this recursive function on input n. Simple investigation of the function shows that:
T(0) = 1, T(1) = 1, T(n) = T(n-1) + T(n-2) + 2 for n >= 2.

Now it is somewhat complicated to find a closed-form formula for T(n). We instead give an upper bound and a lower bound on T(n). To that effect let us first introduce a new function S(n) as:
S(n) = T(n) + 2 for all n.

We then have:
S(0) = 3, S(1) = 3, S(n) = S(n-1) + S(n-2) for n >= 2.

Denote by F(n) the n-th Fibonacci number and use induction on n. S(0) <= F(4) and S(1) <= F(5). Moreover, S(n) = S(n-1) + S(n-2) <= F(n+3) + F(n+2) = F(n+4). A lower bound on S(n) can be derived by induction on n as: S(0) >= F(3) and S(1) >= F(4). Moreover, S(n) = S(n-1) + S(n-2) >= F(n+2) + F(n+1) = F(n+3). It follows that:
F(n+3) - 2 <= T(n) <= F(n+4) - 2 for all n >= 0.

The next question is to find a closed form formula for the Fibonacci numbers. We will not do it here, but present the well-known result:

F(n) = [1/sqrt(5)][((1+sqrt(5))/2)n - ((1-sqrt(5))/2)n].

The number r = (1+sqrt(5))/2 = 1.61803... is called the golden ratio. Also (1-sqrt(5))/2 = -0.61803... is the negative of the reciprocal of the golden ratio and has absolute value less than 1. The powers [(1-sqrt(5))/2]n become very small for large values of n and so
F(n) is approximately equal to [1/sqrt(5)]rn.

For all sufficiently large n, we then have


[1/sqrt(5)]rn+3 - 2 <= T(n) <= [1/sqrt(5)]rn+4 - 2

The first inequality shows that T(n) cannot have polynomial order, whereas the second inequality shows that T(n) is of exponential order. To sum up, recursion helped us convert a polynomial-time (in fact, linear) algorithm to a truly exponential algorithm. This teaches you two lessons. First, use recursion judiciously. Second, different algorithms (or implementations) for the same problem may have widely different complexities. Performance analysis of programs is really important then!

Linear search We are given an array A of n integers and another integer x. The task is to locate the existence of x in A. Here n is taken to be the input size. We assume that A is not sorted, i.e., we will do linear search in the array. Here is the code:
int linSearch ( int A[] , int n , int x ) { int i; for (i=0; i<n; ++i) if (A[i] == x) return 1; return 0; }

The time complexity of the above function depends on whether x is present in A and if so at which location. Clearly, the worst case (longest running time) occurs when x is not present in the array and the last statement (return 0;) is executed. In this case the loop requires one initialization of i, n increments of i and n+1 comparisons of i with n. Inside the loop body there is a single comparison which fails in all of the n iterations of the loop in the worst-case scenario. Thus the total time needed by this function is:
1 + n + (n+1) + n = 3n + 2.

This is O(n), i.e., the linear search is a linear time algorithm.

Binary search In order to curtail the running time of linear search, one uses the binary search algorithm. This requires the array A to be sorted a priori. We do not compute the running time for sorting now, but look at the running time of binary search in a sorted array.
int binSearch ( int A[] , int n , int x ) { int L, R, M; L = 0; R = n-1; while (L < R) { M = (L + R) / 2; if (x > A[M]) L = M+1; else R = M; } return (A[L] == x); }

For simplicity assume that the array size n is a power of 2, i.e., n = 2k for some integer k >= 0. Initially, the boundaries L and R are adjusted to the leftmost and rightmost indices of the entire array. After each iteration of the while loop the central index M of the current search window is computed. Depending on the result of comparison of x with A[M], the boundaries (L,R) is changed either to (L,M) or to (M+1,R). In either case, the size of the search window (i.e., the subarray delimited by L and R) is reduced to half. Thus after k iterations of the while loop the search window reduces to a subarray of size 1, and L and R become equal. After the loop terminates, a comparison is made between x and an array element. So the number of basic operations done by this algorithm equals:
2 1 (Init) (Loop condn) (last comparison) = 5k + 4. (No of iter) (ops in loop body) + (k+1) + k x (2 + 1 + 1) +

But k = log2n, so the running time of binary search is O(log n), i.e., logarithmic. This is far better than the linear running time of the linear search algorithm.

Bubble sort It is interesting to look at the running times of different sorting algorithms. Let us start with a non-recursive sorting algorithm. Here is the code that bubble sorts an array of size n.
void bubbleSort ( int A[] , int n ) { for (i=n-2; i>=0; --i) {

for (j=0; j<=i; ++j) { if (A[j] > A[j+1]) { t = A[j]; A[j] = A[j+1]; A[j+1] = t; } } } }

This is an example of a nested for loop. The outer loop runs over i for the values n-2,n-3,...,0 and for a value of i the inner loop is executed i+1 times. This means that the inner loop is executed a total number of
(n-1) + (n-2) + ... + 2 + 1 = n(n-1)/2

times. Each iteration of the inner loop involves a comparison and conditionally a set of three assignment operations. Thus the inner loop performs at most
4 x n(n-1)/2 = 2n(n-1)

basic operations. This quantity is O(n2). We should also add the costs associated with the maintenance of the loops. The outer loop requires O(n) time, whereas for each i the inner loop requires O(i) time. The n-1 iterations of the outer loop then leads to a total of O((n-1) + (n-2) + ... + 1), i.e., O(n2), basic operations for maintaining all of the inner loops. To sum up, we conclude that the bubble sort algorithm runs in O(n2) time.

Matrix multiplication Here is the straightforward code for multiplying two n x n matrices. We take n as the input size parameter.
/* Multiply two n x n matrices A and B and store the product in C */ void matMul ( int C[SIZE][SIZE] , int A[SIZE][SIZE] , int B[SIZE][SIZE] , int n ) { int i, j, k; for (i=0; i<n; ++i) { for (j=0; j<n; ++j) { C[i][j] = 0; for (k=0; k<n; ++k) C[i][j] += A[i][k] * B[k][j]; } } }

This is another example of nested loops with an additional level of nesting (compared to bubble sort). The outermost and the intermediate loops run independently over the values of i and j in the range 0,1,...,n-1. For each of

these n2 possible values of i,j, the element C[i][j] is first initialized and then the innermost loop on k is executed exactly n times. Each iteration in the innermost loop involves one multiplication and one addition. Therefore, for each i,j the innermost loop takes O(n) running time. This is also the cost associated with maintaining the loop on k. Thus each execution of the body of the intermediate loop takes a total of O(n) time and this body is executed n2 times leading to a total running time of O(n3). It is easy to argue that the cost for maintaining the loop on i is O(n) and that for maintaining all of the n executions of the intermediate loop is O(n2). So two n x n matrices can be multiplied in O(n3) time. Can we make any better than that? The answer is: yes. There are algorithms that multiply two n x n matrices in time O(nw) time, where w < 3. One example is Straen's algorithm that takes time O(nlog2(7)), i.e., O(n2.807...). The best known matrix multiplication algorithm is due to Coppersmith and Winograd. Their algorithm has a running time of O(n2.376). It is clear that for setting the value of all C[i][j]'s one must perform at least n2 basic operations. It is still an open question whether O(n2) running time suffices for matrix multiplication.

Stack ADT operations Look at the two implementations of the stack ADT detailed earlier. It is easy to argue that each function (except print) performs only a constant number of operations irrespective of the current size of the stack and so has a running time of O(1). This is the reason why we planned to write seperate routines for the stack and queue ADTs instead of using the routines for the ordered list ADT. Insertion or deletion in the ordered list ADT may require O(n) time, where n is the current size of the list.

Partitioning in quick sort This example illustrates the space complexity of a program (or function). We concentrate only on the partitioning stage of the quick sort algorithm. The following function takes the first element of the array as the pivot and returns the last index of the smaller half of the array. The pivot is stored at this index.
int partition1 ( int A[] , int n ) { int *L, *R, lIdx, rIdx, i, pivot; L = (int *)malloc((n-1) * sizeof(int)); R = (int *)malloc((n-1) * sizeof(int)); pivot = A[0]; lIdx = rIdx = 0; for (i=1; i<n; ++i) { if (A[i] <= pivot) L[lIdx++] = A[i]; else R[rIdx++] = A[i]; }

for (i=0; i<lIdx; ++i) A[i] = L[i]; A[lIdx] = pivot; for (i=0; i<rIdx; ++i) A[lIdx + 1 + i] = R[i]; free(L); free(R); return lIdx; }

Here we collect elements of A[] smaller than or equal to the pivot in the array L and those that are larger than the pivot in the array R. We allocate memory for these additional arrays. Since the sizes of L and R are not known a priori, we have to prepare for the maximum possible size (n-1) for both. In addition, we use a constant number (six) of variables. The total additional space requirement for this function is therefore
2(n-1) + 6 = 2n + 4,

which is O(n). Let us plan to reduce this space requirement. A possible first approach is to store L and R in a single array LR of size n-1. Though each of L and R may be individually as big as having a size of n-1, the total size of these two arrays must be n-1. We store elements of L from the beginning and those of R from the end of LR. The following code snippet incorporates this strategy:
int partition2 ( int A[] , int n ) { int *LR, lIdx, rIdx, i, pivot; LR = (int *)malloc((n-1) * sizeof(int)); pivot = A[0]; lIdx = 0; rIdx = n-1; for (i=1; i<n; ++i) { if (A[i] <= pivot) LR[lIdx++] = A[i]; else LR[rIdx--] = A[i]; } for (i=0; i<lIdx; ++i) A[i] = LR[i]; A[lIdx] = pivot; for (i=rIdx+1; i<n; ++i) A[i] = LR[i]; free(LR); return lIdx; }

The total amount of extra memory used by this function is


(n-1) + 5 = n + 4,

which, though about half of the space requirement for partition1, is still O(n). We want to reduce the space complexity further. Using one or more additional arrays will always incur O(n) space overhead. So we would avoid using any such

extra array, but partition A in A itself. This is called in-place partitioning. The function partition3 below implements in-place partitioning. It works as follows. It maintains the loop invariant that at all time the array A is maintained as a concatenation LUR of three regions. The leftmost region L contains elements smaller than or equal the pivot. The rightmost region R contains elements bigger than the pivot. The intermediate region U consists of yet unprocessed elements. Initially, U is the entire array A (or A without the first element which is taken to be the pivot), and finally U should be empty. The region U is delimited by two indices lIdx and rIdx indicating respectively the first and last indices of U. During each iteration, the element at lIdx is compared with the pivot, and depending on the comparison result this element is made part of L or R.
int partition3 ( int A[] , int n ) { int lIdx, rIdx, pivot, t; pivot = A[0]; lIdx = 1; rIdx = n-1; while (lIdx <= rIdx) { if (A[lIdx] <= pivot) { /* The region L grows */ ++lIdx; } else { /* Exchange A[lIdx] with the element at the U-R boundary. */ t = A[lIdx]; A[lIdx] = A[rIdx]; A[rIdx] = t; /* The region R grows */ --rIdx; } } /* Place the pivot A[0] in the correct place by exchanging it with the last element of L */ A[0] = A[rIdx]; A[rIdx] = pivot; return rIdx; }

The function partition3 uses only four extra variables and so its space complexity is O(1). That is a solid improvement over the earlier versions. It is easy to check that the time complexity of each of these three partition routines is O(n).

Worst-case versus average complexity

Our basic aim is to provide complexity figures (perhaps in the O notation) in terms of the input size, and not as a function of any particular input. So far we have counted the maximum possible number of basic operations that need be executed by a program or function. As an example, consider the linear search algorithm. If the element x happens to be the first element in the array, the function linSearch returns after performing only few operations. The farther x can be located down the array, the bigger is the number of operations. Maximum possible effort is required, when x is not at all present in the array. We argued that this maximum value is O(n). We call this the worst-case complexity of linear search. There are situations where the worst-case complexity is not a good picture of the practical situation. On an average, a program may perform much better than what it does in the worst case. Average complexity refers to the complexity (time or space) of a program (or function) that pertains to a random input. It turns out that average complexities for some programs are markedly better than their worst-case complexities. There are even examples where the worst-case complexity is exponential, whereas the average complexity is a (low-degree) polynomial. Such an algorithm may take a huge amount of time in certain esoteric situations, but for most inputs we expect the program to terminate soon. We provide a concrete example now: the quick sort algorithm. By partition we mean a partition function for an array of n integers with respect to the first element of the array as the pivot. One may use any one of the three implementations discussed above.
void quickSort ( int A[] , int n ) { int i; if (n <= 1) return; i = partition(A,n); quickSort(A,i); excluding the pivot */ quickSort(&A[i+1],n-i-1); }

/* Partition with respect to A[0] */ /* Recursively sort the left half /* Recursively sort the right half */

Let T(n) denote the running time of quickSort for an array of n integers. The running time of the partition function is O(n). It then follows that:
T(n) <= T(i) + T(n-i-1) + cn + d

for some constants c and d and for some i depending on the input array A. The presence of i on the right side makes the analysis of the running time somewhat difficult. We cannot treat i as a constant for all recursive invocations. Still, some general assumptions lead to easily derivable closed-form formulas for T(n). An algorithm like quick sort (or merge sort) is called a divide-and-conquer algorithm. The idea is to break the input into two or more parts, recursively solve the problem on each part and subsequently combine the solutions for the different parts. For the quick

sort algorithm the first step (breaking the array into two subarrays) is the partition problem, whereas the combining stage after the return of the recursive calls involves doing nothing. For the merge sort, on the other hand, breaking the array is trivial -- just break it in two nearly equal halves. Combining the solutions involves the non-trivial merging process. It follows intuitively that the smaller the size of each subproblem is, the easier it is to solve each subproblem. For any superlinear function f(n) the sum
f(k) + f(n-k-1) + g(n)

(with g(n) a linear function) is large when the breaking of n into k,n-k-1 is very skew, i.e., when one of the parts is very small and the other nearly equal to n. For example, take f(n) = n2. Consider the function of a real variable x:
y = x2 + (n-x-1)2 + g(n)

Differentiation shows that the minimum value of y is attained at x = n/2 approximately. The value of y increases as we move more and more away from this point in either direction. So T(n) is maximized when i = 0 or n-1 in all recursive calls, for example, when the input array is already sorted either in the increasing or in the decreasing order. This situation yields the worst-case complexity of quick sort:
T(n) <= = <= = <= <= <= = T(n-1) + T(0) + cn + d T(n-1) + cn + d + 1 (T(n-2) + c(n-1) + d + 1) + cn + d + 1 T(n-2) + c[n + (n-1)] + 2d + 2 T(n-3) + c[n + (n-1) + (n-2)] + 3d + 3 ... T(0) + c[n + (n-1) + (n-2) + ... + 1] + nd + n cn(n-1)/2 + nd + n + 1,

which is O(n2), i.e., the worst-case time complexity of quick sort is quadratic. But what about its average complexity? Or a better question is how to characterize an average case here. The basic idea of partitioning is to choose a pivot and subsequently break the array in two halves, the lesser mortals stay on one side, the greater mortals on the other. A randomly chosen pivot is expected to be somewhere near the middle of the eventual sorted sequence. If the input array A is assumed to be random, its first element A[0] is expected to be at a random location in the sorted sequence. If we assume that all the possible locations are equally likely, it is easy to check that the expected location of the pivot is near the middle of the sorted sequence. Thus the average case behavior of quick sort corresponds to
i = n-i-1 = n/2 approximately.

We than have:
T(n) <= 2T(n/2) + cn + d.

For simplicity let us assume that n is a power of 2, i.e., n = 2t for some positive integer t. But then
T(n) = <= <= = <= <= <= = = T(2t) 2T(2t-1) + c2t + d 2(2T(2t-2) + c2t-1 + d) + c2t + d 22T(2t-2) + c(2t+2t) + (2+1)d 23T(2t-3) + c(2t+2t+2t) + (22+2+1)d ... 2tT(20) + ct2t + (2t-1+2t-2+...+2+1)d 2t + ct2t + (2t-1)d cnlog2n + n(d+1) - d.

The first term in the last expression dominates over the other terms and consequently the average complexity of quick sort is O(nlog n). Recall that bubble sort has a time complexity of O(n2). The situation does not improve even if we assume an average scenario, since we anyway have to make O(n2) comparisons in the nested loop. Insertion and selection sorts attain the same complexity figure. With quick sort, the worst-case complexity is equally poor. But in practice a random array tends to follow the average behavior more closely than the worst-case behavior. That is reasonable improvement over quadratic time. The quick sort algorithm turns out to be one of the practically fastest general-purpose comparison-based sorting algorithm. We will soon see that even the worst-case complexity of merge sort is O(nlog n). It is an interesting theoretical result that a comparison-based sorting algorithm cannot run in time faster than O(nlog n). Both quick sort and merge sort achieve this lower bound, the first on an average, the second always. Historically, this realization provided a massive impetus to promote and exploit recursion. Tony Hoare invented quick sort and popularized recursion. We cannot think of a modern compiler without this facility. Also, do you see the significance of the coinage divide-and-conquer? We illustrated above that recursion made the poly-time Fibonacci routine exponentially slower. That's the darker side of recursion. Quick sort and merge sort highlight the brighter side. When it is your time to make a decision to accept or avoid recursion, what will you do? Analyze the complexity and then decide.

How to compute the complexity of a program?


The final question is then how to derive the complexity of a program. So far you have seen many examples. But what is a standard procedure for deriving those divine functions

next to the big-Oh? Frankly speaking, there is none. (This is similar to the situation that there is no general procedure for integrating a function.) However, some common patterns can be identified and prescription solutions can be made available for those patterns. (For integration too, we have method of substitution, integration by parts, and some such standard rules. They work fine only in presence of definite patterns.) The theory is deep and involved and well beyond the scope of this introductory course. We will again take help of examples to illustrate the salient points. First consider a non-recursive function. The function is a simple top-to-bottom set of instructions with loops embedded at some places in the sequence. One has to carefully study the behavior of the loops and add up the total overhead associated with each loop. The final complexity of the function is the sum of the complexities of each individual instruction (including loops). The counting process is not always straighforward. There is a deadly branch of mathematics, called combinatorics, that deals with counting principles. We have already deduced the time complexity of several non-recursive functions. Let us now focus our attention to recursive functions. As we have done in connection with quickSort, we write the running time of an invocation of a recursive function by T(n), where n denotes the size of the input. If n is of a particular form (for example, if n has a small value), then no recursive calls are made. Some fixed computation is done instead and the result is returned. In this case the techniques for non-recursive functions need be employed. Finally, assume that the function makes recursive calls on inputs of sizes n1,n2,...,nk for some k>=1. Typically each ni is smaller than n. These calls take respective times T(n1),T(n2),...,T(nk). We add these times. Furthermore, we compute the time taken by the function without the recursive calls. Let us denote this time by g(n). We then have:
T(n) = T(n1) + T(n2) + ... + T(nk) + g(n).

Such an equation is called a recurrence relation. There are tools by which we can solve recurrence relations of some particular types. This is again part of the deadly combinatorics. We will not go to the details, but only mention that a recurrence relation for T(n) together with a set of initial conditions (e.g. T(n) for some small values of n) may determine a closed-form formula for T(n) which can be expressed by the Big O notation. It is often not necessary to compute an exact formula for T(n). Proving a lower and an upper bound may help us determine the order of T(n). Recall how we have analyzed the complexity of the recursive Fibonacci function. We end this section with two other examples of complexity analysis of recursive functions. Examples

Computing determinants The following function computes the determinant of an n x n matrix using the expand-at-the-first-row method. It recursively computes n determinants of (n1) x (n-1) sub-matrices and then does some simple manipulation of these determinant values.
int determinant ( int A[SIZE][SIZE] , int n ) { int B[SIZE][SIZE], i, j, k, l, s; if (n == 1) return A[0][0]; s = 0; for (j=0; j<n; ++j) { for (i=1; i<n; ++i) { for (l=k=0; k<n; ++k) if (k != j) B[i-1][l++] = A[i][k]; } if (j % 2 == 0) s += A[0][j] * determinant(B,n-1); else s -= A[0][j] * determinant(B,n-1); } }

I claim that this algorithm is an extremely poor choice for computing determinants. If T(n) denotes the running of the above function, we clearly have:
T(1) = 1, and T(n) >= n T(n-1) for n >= 2.

Multiple substitution of the second inequality then implies that:


T(n) >= n T(n-1) >= n(n-1) T(n-2) >= n(n-1)(n-2) T(n-3) ... >= n(n-1)(n-2)...2 T(1) = n!

How big is n! (factorial n)? Since i >= 2 for i = 2,3,...,n, it follows that n! >= 2n-1. Thus the running-time of the above function is at least exponential. Polynomial-time algorithms exist for computing determinants. One may use elementary row operations in order to reduce the given matrix to a triangular matrix having the same determinant. For a triangular matrix, the determinant is the product of the elements on the main diagonal. We urge the students to exploit this idea in order to design an O(n3) algorithm for computing determinants.

Merge sort The merge sort algorithm on an array of size n is depicted below:

void mergeSort ( int A[] , int n ) { if (n <= 1) return; mergeSort(A,n/2); mergeSort(&A[n/2],n-(n/2)); merge(A,0,n/2-1,n/2,n-1); }

For simplicity, assume that n = 2t for some t. The merge step on two arrays of size n/2 can be easily seen to be doable in O(n) time. It then follows that:
T(1) = 1, and T(n) <= 2 T(n/2) + cn + d

for some constants c and d. As in the average case of quick sort, one can deduce the running time of merge sort to be O(nlog n).

Course home

CS13002 Programming and Data Structures

Spring semester

Exercise set I
Note: Students are encouraged to solve as many problems from this set as possible. Some of these will be solved during the lectures, if time permits. We have made attempts to classify the problems based on the difficulty level of solving them. An unmarked exercise is of low to moderate difficulty. Harder problems are marked by H, H2 and H3 meaning "just hard", "quite hard" and "very hard" respectively. Exercises marked by M have mathematical flavor (as opposed to computational). One requires elementary knowledge of number theory or algebra or geometry or combinatorics in order to solve these mathematical exercises. 1. Assume that the CPU of a particular computer has three general-purpose registers A,B,C. Assume also that m,n,t are integer values stored in the machine's memory. Write assembly instructions for performing the following assignments. Use an assembly language similar to that discussed in the examples given in the notes. Assume that all operations are integer operations. a. m = m + n - 1; b. t = (m+5)*(n+5); c. n = (m+5)/(n+5)+(n+5)/(m+5); 2. [M] Let n be a non-negative integer less than 2t. Let (at-1at-2...a1a0)2 be the t-bit binary expansion of n obtained by the repeated divide-by-2 procedure described in the notes. Prove that:
3. n = at-12t-1 + at-22t-2 + ... + a121 + a0.

4. Write the 8-bit 2's complement representations of the following integers: a. 123 b. -123 c. -7 d. 63 5. Find the 32-bit floating point representation of the following real numbers (under the IEEE 754 format): a. 123 b. -123 c. 0.1 d. 0.2 e. 0.25 f. -543.21 6. [H2M] Let x be a proper fraction, i.e., a real number in the range 0<=x<1. Prove that x has a terminating binary expansion if and only if it is of the form a/2k for some integers a,k with 0<=a<2k.

7. Let x,y,z be unsigned integers. Find the values of x,y,z after the following statements are executed.
8. 9. 10. 11. 12. 13. x z x x y z = 5; = 12; *= x; += z * z; = x << 1; = y % z;

14. Assume that m and n are (signed) integer variables and that x and y are floating point variables. Write logical conditions that evaluate to "true" if and only if: a. x+y is an integer. b. m lies strictly between x and y. c. m equals the integer part of x. d. x is positive with integer part at least 3 and with fractional part less than 0.3. e. m and n have the same parity (i.e., are both odd or both even). f. m is a perfect square. 15. Write a program to solve the following problems: a. Show that -29 and 31 are roots of the polynomial x3 + x2 - 905x 2697. What is its third root? b. Show that -2931 is a root of the polynomial x3 + 2871x2 - 174961x + 2634969. c. The three roots of the polynomial x3 + x2 - 74034x + 5294016 are integers. Find them. d. The three roots of the polynomial x3 + x2 - 28033x - 1815937 are again integers. Find them. 16. Read five positive real numbers a, b, c, d and e from the user and compute their arithmetic mean, geometric mean, harmonic mean and standard deviation. 17. Input four integers a, b, c and d with b and d positive. a. Output the rational numbers (not their float equivalents) (a/b)+(c/d), (a/b)(c/d) and (a/b)*(c/d). b. Output the rational numbers (a/b)+(c/d), (a/b)-(c/d) and (a/b)*(c/d) in lowest terms, that is, in the form m/n with n>0 and gcd(m,n)=1. 18. Input four integers a, b, c and d with a or b (or both) non-zero and with c or d (or both) non-zero. a. Output the complex numbers (a+ib)/(c+id) and (c+id)/(a+ib) in the form (r/s)+i(u/v) with r,s,u,v integers and s,v>0. b. Output the complex numbers (a+ib)/(c+id) and (c+id)/(a+ib) in the form (r/s)+i(u/v) with r/s and u/v in lowest terms, that is, with s,v>0 and with gcd(r,s)=gcd(u,v)=1. 19. Let m and n be 32-bit unsigned integers. Use bit operations to assign to m the following functions of n: a. 1 if n is odd, 0 if n is even. b. 1 if n is divisible by 4, 0 otherwise. c. 2n (Assume that n<=31). d. n rotated by k positions to the left for some integer k>=0. e. n rotated by k positions to the right for some integer k>=0.

20. Write a program that does the following: Scan six real numbers a,b,c,d,e,f and compute the point of intersection of the straight lines:
21. 22. ax + by = c dx + ey = f

Your program should specifically handle the case that the two given lines are parallel. 23. Write a program to determine the roots of a quadratic equation. Figure out a way to handle the case when the the roots are not real. 24. Write a program that scans a string and checks if it represents a valid date in the format DD-MM-YYYY. (Example: 29-02-2005 is not a valid date, but 29-022004 is valid.) 25. An ant is sitting at the left end of a rope of length 10 cm. At t=0 the ant starts moving along the rope to reach the other end of the rope. The ant has a speed of 1 cm per second. After every second the rope stretches instantaneously and uniformly (along its length) by 10 cm with the left end fixed at the point from where the ant started its journey. Suppose that the ant's legs provide it sufficient friction in order to withstand the stretching of the rope. Write a program to demonstrate that the ant will be able to reach the right end of the rope. Your program should also calculate how many seconds the ant would take to achieve this goal. You may assume that the length of the ant is negligible (i.e., zero). Note: The ant would reach the right end of the rope, even if its initial length and stretching per second were 1 km (or even a billion kilometers) instead of 10 cm. But for these dimensions the ant would take such an unbelievably large time that your program will not give you the confirmation in your life-time. Moreover, you will require more precision than what double can provide. Try to solve this puzzle mathematically. 26. Randomly generate a sequence of integers between -5 and +99 and output the maximum and minimum values generated so far. Exit, if a negative integer is generated. You must not store the sequence generated (say using an array), but update the maximum and minimum values on the fly, as soon as a new entry is generated. A sample run is given below:
27. 28. 29. 30. 31. 32. Iteration Iteration Iteration Iteration Iteration ... 1: 2: 3: 4: 5: new new new new new entry entry entry entry entry = = = = = 84, 87, 72, 53, 93, max max max max max = = = = = 84, 87, 87, 87, 93, min min min min min = = = = = 84 84 72 53 53

33. It is known that the harmonic number Hn converges to k + ln n as n tends to infinity. Here ln is the natural logarithm and k is a constant known as Euler's constant. In this exercise you are asked to compute an approximate value for Euler's constant. Generate the values of Hn and ln n successively for n=1,2,3,..., and compute the difference kn = Hn - ln n. Stop when kn-kn-1 is less than a specific error bound (like 10-8).

Note: It is not known whether Euler's constant is rational or not. It has, however, been shown that if Euler's constant is rational, its denominator must have more than 10,000 decimal digits. 34. Write a program that, given a positive integer n, determines the integer t with the property that 2t-1<=n<2t. This integer t is called the bit-length of n. 35. A Pythagorean triple is a triple (a,b,c) of positive integers with the property that a2+b2=c2. Write a program that scans a positive integer value k and outputs all Pythagorean triples (a,b,c) with 0<a<=b<c<=k. 36. Consider the function
37. f(a,b) = (a2+b2)/(ab-1)

of two positive integers a,b with ab>1. a. Write a program that scans a positive integer k and prints the three values a,b,f(a,b) if and only if 0<a<=b<=k and f(a,b) is an integer. b. [H3M] Do you see a surprising fact about these f(a,b) values? Can you prove your hunch? 38. Given a number in decimal write a program to print the reverse of the number. For example, the reverse of 3481 is 1843. 39. Write a program that does the following: Read a decimal integer and print the ternary (base 3) representation of the integer. 40. Write a program that does the following: Read a string of 0's and 1's and print the decimal equivalent of the string treated as an integer in the binary representation. 41. [H] Write a program that does the following: Read a string of 0's, 1's and 2's and print the decimal equivalent of the string treated as an integer in the ternary representation. 42. Write a program that, given a positive number x (not necessarily integral) and an integer k, computes the kth root of x using the bisection method. Supply an accuracy for your answer. 43. Generate a random sequence of birthdays and store the birthdays in an array. As soon as a match is found, report that. Also report how many birthdays were generated to get the match. Note: It is surprising to see that you usually require a very small number of people (around 25) in order to have a match in their birthdays. However counterintuitive it might sound, it is a mathematical truth, commonly known as the birthday paradox. In short it says that if you draw (with replacement) about sqrt(n) samples from a pool of n objects, there is about 50/50 chance that you get a repetition. If you draw 2 sqrt(n) samples, you can be almost certain that there will be at least one repetition. 44. Write a program that, given an array A[] of integers, finds out the largest and second largest elements of the array. 45. Write a program that, given an array A[] of signed integers, finds out a subsequence i,i+1,...,j such that the sum
46. A[i] + A[i+1] + ... + A[j]

is maximum over all such subsequences. Note that the problem is trivial if all numbers are positive -- your algorithm should work when the numbers may have different signs. 47. [H2] Can you write a program that solves the problem of the last exercise using roughly n operations? 48. Read an English sentence from the terminal. Count and print the number of occurrences of the alphabetic letters (a through z) in it. Also print the total number of distinct alphabetic letters in the sentence. Make no distinction between upper and lower case letters, i.e., 'a' is treated the same as 'A', 'b' the same as 'B' and so on. Neglect non-alphabetic characters (digits, spaces, punctuation symbols etc.). 49. Input two strings a and b from the user and check if b is a substring of a. If b is a substring of a, then your program should also print the leftmost position of the leftmost match of b in a. 50. Write a program that scans a positive integer and checks if the integer is a perfect number (i.e., a number which is equal to the sum of all its proper integral divisors, e.g., 6 = 1+2+3). 51. Write a program that reads a positive integer n and lists all primes between 1 and n. Use the sieve of Eratosthenes described below: Use an array of n cells indexed 1 through n. Since C starts indexing from 0, one may, for the ease of referencing, use an array of n+1 cells (rather than n). Initially all the array cells are unmarked. During the process one marks the cells with composite indices. An unmarked cell holds the value 0, a marked cell holds 1. Henceforth, let us abbreviate "marking the cell at index i" as "marking i". Any positive integral multiple of a positive integer k, other than k itself, is called a proper multiple of k. Starting with k=2, mark all proper multiples of 2 between 1 and n. Then look at the smallest integer >2 that has not been marked. This is k=3 and must be a prime. Mark all the proper multiples of 3 and then look at the next unmarked integer -- this is k=5. Then mark the proper multiples of 5 and so on. The process need continue as long as k<=sqrt(n), since every composite integer m, 1<m<=n, must have a prime divisor <=sqrt(n). After the loop described in the last paragraph terminates, report the indices of the unmarked cells in your array. These are precisely all the primes in the range 1,2,...,n. Now adjust the bound n in order to detect the millionth prime. 52. [HH] Repeat the above problem where a cell is marked at most once. In the previous description, cell 6 will will get marked when we consider 2 as well as 3 etc. 53. [HM] Write a program that, given two integers a,b with 0<a<b, finds integers n1,n2,...,nk with the properties that:
54. 55. n1 < n2 < ... < nk and a/b = 1/n1 + 1/n2 + ... + 1/nk.

(Hint: You may use the following idea. If a/b is already of the form 1/n, we are done. Otherwise, find the integer n such that 1/n<a/b<1/(n-1). Print n, replace a/b by (a/b)-(1/n) and repeat. Prove that this gives a strictly increasing sequence of printed values (n) and that the process terminates after finitely many steps.) 56. Write a program that, given a set of n points in the plane (specified by their x and y coordinates), determines the smallest circle that encloses all these points. (Hint: The smallest circle must either pass through three given points or have two given points at the opposite ends of a diameter.) 57. In this exercise you are asked to compute approximate values of pi. a. Write the infinite series for 1/(1+x2). b. Integrate (between 0 and x) both 1/(1+x2) and the infinite series for it, put the value x = 1/sqrt(3) and write pi as an infinite series. c. Truncate the series after n terms and evaluate the truncated series to get an approximate value of pi. Use the values n=10i for i=1,2,...,6. d. Write the infinite series for 1/sqrt(1-x2). e. Integrate (between 0 and x) both 1/sqrt(1-x2) and the infinite series for it, put the value x = 1/2 and write pi as an infinite series. f. Truncate the series after n terms and evaluate the truncated series to get an approximate value of pi. Use the values n=10i for i=1,2,...,6. 58. [H] Write a program to determine the smallest positive integer n with the following property. Let
59. n = akak-1...a1a0

be the decimal representation of n with ak>0. Look at the integer:


n' = a0akak-1...a2a1

(the cyclic right shift of n). The desired property of n is that n' must be a proper integral multiple of n. 60. [H] Write a program to find the smallest positive integer n with the property that the decimal expansion of 2n starts with the four digits 2005, i.e., 2n = 2005... (Hint: Take log.)

Course home

CS13002 Programming and Data Structures

Spring semester

Exercise set II
Note: Students are encouraged to solve as many problems from this set as possible. Some of these will be solved during the lectures, if time permits. We have made attempts to classify the problems based on the difficulty level of solving them. An unmarked exercise is of low to moderate difficulty. Harder problems are marked by H, H2 and H3 meaning "just hard", "quite hard" and "very hard" respectively. Exercises marked by M have mathematical flavor (as opposed to computational). One requires elementary knowledge of number theory or algebra or geometry or combinatorics in order to solve these mathematical exercises. 1. Write functions to compute the following: a. The area of a circle whose diameter is supplied as an argument. b. The volume of a 3-dimensional sphere whose surface area is given as an argument. c. The area of an ellipse for which the lengths of the major and minor axes are given as arguments. d. Given the coordinates of three distinct points in the x-y plane, the radius of the circle circumscribing the three points. Your function should return -1 if the three given points are collinear. e. Given a positive integer n, the sum of squares of all (positive) proper divisors of n. f. Given integers n>=0 and b>1, the expansion of n in base b. (Example: (987654321)_10 = (4,38,92,23,114)_123.) g. Given n and an array of n positive floating point numbers, the geometric mean of the elements of the array. 2. Write functions to perform the following tasks: o Check if a positive integer (provided as parameter) is prime. o Check if a positive integer (provided as parameter) is composite. o Return the sum S7(n) of the 7-ary digits of a positive integer n (supplied as parameter). Use the above functions to find out the smallest positive integer i for which S7(pi) is composite, where pi is the i-th prime. Also print the prime pi. Note: 1 is neither prime nor composite. The sequence of primes is denoted by
p1=2, p2=3, p3=5, p4=7, p5=11, ...

As an illustrative example for this exercise consider the 31-st prime p31 = 127 that expands in base 7 as

127 = 2 x 72 + 4 x 7 + 1,

i.e., the 7-ary expansion of 127 is 241 and therefore


S7(127) = 2 + 4 + 1 = 7,

which is prime. 3. Write a function that, given two points (x1,y1) and (x2,y2) in the plane, returns the distance between the points. Write another function that computes the radius of the circle
x2 + y2 + ax + by + c

defined by the triple (a,b,c). Note that for some values of (a,b,c) this radius is not defined. In that case your function should return some negative value. Input two triples (a1,b1,c1) and (a2,b2,c2) so as to define two circles. Use the above two functions to determine which of the following cases occurs: One or both of the circles is/are undefined. The two circles touch (i.e., meet at exactly one point). The two circles intersect (at two points). The two circles do not intersect at all. 4. [M] Use the principle of mathematical induction to prove the following assertions: . x2n+1+y2n+1 is divisible by x+y for all n in N0. a. 1/sqrt(1) + 1/sqrt(2) + ... + 1/sqrt(n) > 2(sqrt(n+1) - 1) for all n in N. b. F12 + F22 + ... + Fn2 = FnFn+1 for all n in N0, where Fn denotes the n-the Fibonacci number. c. H1 + H2 + ... + Hn = (n+1)Hn - n for all n in N0, where Hn denotes the nth harmonic number. 5. [M] Find the flaw in the following proof:
o o o o

Theorem: All horses are of the same color. Proof Let there be n horses. We proceed by induction on n. If n=1, there is nothing to prove. So assume that n>1 and that the theorem holds for any group of n-1 horses. From the given n horses discard one, say the first one. Then all the remaining n-1 horses are of the same color by the induction hypothesis. Now put the first horse back and discard another, say the last one. Then the first n-1 horses have the same color again by the induction hypothesis. So all the n horses must have the same color as the ones that were not discarded either time. QED 6. Find a loop invariant for each of the following loops:
7. a. int n, x, y, t;

8. 9. 10. 11. 12. 13. 14. 15. 16. 17. 18. 19. b. 20. 21. 22. 23. 24. 25. 26. 27. 28. 29. 30. 31. c. 32. 33. 34. 35. 36. 38. 39. 40. 41. them 42. 43. 44.

n = 0; x = 1 + rand() % 9; y = 1 + rand() % 9; while (n < 10) { t = 1 + rand() % 9; x *= t; y /= t; ++n; } #define NITER 100 double x, s, t; int i; i = 0; s = t = 1; do { ++i; t /= (double)i; s += t; } while (i < NITER); #define NITER 10 int i; double A[NITER] = {1.0/2, 1.0/3, 1.0/5, 1.0/7, 1.0/11, 1.0/13, 1.0/17, 1.0/19, 1.0/23, 1.0/29}; for (i=1; i<NITER; ++i) A[i] += A[i-1]; Let n be an odd positive integer. Initialize C to the collection of integers 1,2,...,2n. while (C contains two or more elements) { Randomly choose two elements from the collection, call a and b. Remove these elements from C. Add the absolute difference |a-b| to C. }

37. [HM] Consider the following puzzle given in pseudocode:

For every iteration of the loop, two elements are removed and one element is added, so the size of the collection reduces by 1. After 2n-1 iterations the collection contains a single integer, call it t, and the loop terminates. Prove that t is odd. (Hint: Try to locate a delicate loop invariant.) 45. Determine what each of the following foomatic functions computes:
46. a. 47. 48. 49. 50. 51. 52. 53. 54. 55. unsigned int foo1 ( unsigned int n ) { unsigned int t = 0; while (n > 0) { if (n % 2 == 1) ++t; n = n / 2; } return t; }

56. 57. 58. 59. 60. 61. 62. 63. 64. 65. 66. 67. 68. 69. 70. 71. 72. 73. 74. 75. 76. 77. 78. 79. 80. 81. 82. 83. 84. 85. 86. 87. 88. 89. 90. 91. 92. 93. 94. 95. 96. 97. 98. 99. 100. 101. 102. 103. 104. 105. 106. 107. 108. 109. 110. 111. 112.

b.

unsigned int foo2 ( unsigned int n ) { unsigned int t = 0; while (n > 0) { if (n & 1) ++t; n >>= 1; } return t; }

c.

double foo3 ( double a , unsigned int n ) { double s, t; s = 0; t = 1; while (n > 0) { s += t; t *= a; --n; } return s; }

d.

double foo4 ( float A[] , int n ) { float s, t; s = t = 0; for (i=0; i<n; ++i) { s += A[i]; t += A[i] * A[i]; } return (t/n)-(s/n)*(s/n); }

e.

int foo5 ( unsigned int n ) { if (n == 0) return 0; return 3*n*(n-1) + foo5(n-1) + 1; } int foo6 ( char A[] , unsigned int n ) { int t; if (n == 0) return 0; t = foo6(A,n-1); if ( ((A[n-1]>='a') && (A[n-1]<='z')) || ((A[n-1]>='A') && (A[n-1]<='Z')) || ((A[n-1]>='0') && (A[n-1]<='9')) ) ++t; return t; }

f.

113. g. 114. 115. 116. 117. 118. 119. 120. 121. 122. 123. 124. 125. h. 126. 127. 128. 129. 130. 131. 132. 133. 134. 135. 136.

int foo7 ( unsigned int a , unsigned int b ) { if ((a == 0) || (b == 0)) return 0; return a * b / bar7(a,b); } int bar7 ( unsigned int a , unsigned int b ) { if (b == 0) return a; return bar7(b,a%b); } int foo8 ( unsigned int n ) [ if (n == 0) return 0; if (n & 1) return -1; return 1 + bar8(n-1); } int bar8 ( int n ) { if (!(n & 1)) return -2; return 2 + foo8(n-1); }

137. [HM] Prove that the following function correctly computes the number of trailing 0's in the decimal representation of n! (factorial n).
138. 139. 140. 141. 142. 143. 144. 145. 146. 147. int bar ( unsigned int n ) { int t = 0; while (n > 0) { n /= 5; t += n; } return t; }

148.
149. 150.

For k in N we have
a2k = (ak)2, and a2k+1 = (ak)2 x a.

Use this observation to write a recursive function that, given a real number a and a non-negative integer n, computes the power an. 151. Write a recursive function that computes the binomial coefficient C(n,r) using the inductive definition:
152. C(n,r) = C(n-1,r) + C(n-1,r-1)

for suitable values of n and r. Supply appropriate boundary conditions. 153. Define a sequence Gn as:
Gn = 0 1 if n = 0, if n = 1,

2 Gn-1 + Gn-2 + Gn-3

if n = 2, if n >= 3.

154.
155. 156. 157.

. Write an iterative function for the computation of Gn for a given n. a. Write a recursive function for the computation of Gn for a given n. b. [H] Write an efficient recursive function for the computation of Gn for a given n. Here efficiency means recursive invocation of the function for no more than n times. Consider the sequence of integers given by:
a1 = 1, a2 = 1, an = 6an-2 - an-1 for n >= 3.

. Write a recursive function to compute a20. a. Write an iterative function to compute a20. b. Suppose that a mathematician tells you that c. an = (2n+1 + (-3)n-1)/5 for all n>=1. Use this formula to compute a20. Compare the timings of these three approaches for computing a20. In order to measure time, use the built-in function clock() defined in <time.h>. 158.
159. 160. 161. 162. 163. 164. 165. 166. 167. 168. 169.

Consider three sequences of integers defined recursively as follows:


a0 = 0 a1 = 1 an = a[n/3] - 2bn-2 + cn b0 b1 b2 bn = = = = for n >= 2

-1 0 1 n - an-1 + cn-2 - bn-3

for n >= 3

c0 = 1 cn = bn - 3c[n/2] + 5

for n >= 1

Here for a real number x the notation [x] stands for the largest integer less than or equal to x. For example, [3]=3, [3.1416]=3, [0.1416]=0, [-1.1416]=-2, [3]=-3, [5/3]=1, [-5/3]=-2, etc. For this exercise you need consider x>=0 only, in which case [x] can be viewed as the integral part of x. . Write three mutually recursive functions for computing an, bn and cn. a. Compute a25. Count the total number of times ai, bi and ci are computed for i=0,...,25 (that is, the corresponding functions are called with argument i) during the computation of a25. b. Compute b25. Count the total number of times ai, bi and ci are computed for i=0,...,25 during the computation of b25. c. Compute c25. Count the total number of times ai, bi and ci are computed for i=0,...,25 during the computation of c25.

d. Write an iterative version of the mutually recursive procedure. Maintain three arrays a, b and c each of size 26. Use the boundary conditions (values for a0, a1, b0 etc.) to initialize. Then use the recursive definition to update the a, b and c values "simultaneously". In this method if some value (say, ai) is once computed, it is stored in the appropriate array location for all subsequent uses. This saves the time for recalculating the same value again and again. e. Compute the values a25, b25 and c25 using the iterative procedure. 170. [M] What is wrong in the following mutually recursive definition of three sequences an, bn and cn?
171. 172. 173. 174. 175. 176. 177. 178. a0 = 1. an = an-1 + bn for n >= 1. b0 = 2. bn = bn-1 + cn for n >= 1. c0 = -3. cn = cn-1 + an for n >= 1.

179. Two frogs are sitting at the bottom of a flight of 10 steps and debating in how many ways then can jump up the stairs. They can jump one, two or three steps at once. For example, they can cover the 10 steps by jumping (3,3,3,1) or (2,3,2,1,2) or other suitable combinations of steps. Their mathematics is not very strong and they approach you for help in order to find out the total number of possibilities they have to reach the top. Please provide them with a general solution (not only for 10 but for general n steps) in the form of a C function. Note that the order of the steps is important here, i.e., (3,3,3,1) is treated distinct from (1,3,3,3) for example. 180. Suppose we want to compute the product a0 x a1 x ... x an of n+1 numbers. Since multiplication is associative, we can insert parentheses in any order in order to completely specify the sequence of multiplications. For example, for n=3 we can parenthesize a0 x a1 x a2 x a3 in the following five ways:
181. 182. 183. 184. 185. a0 x (a1 x (a2 x a3)) a0 x ((a1 x a2) x a3) (a0 x a1) x (a2 x a3) (a0 x (a1 x a2)) x a3 ((a0 x a1) x a2) x a3

The number of ways in which n+1 numbers can be multiplied is denoted by Cn and is called the n-th Catalan number. . [HM] Show that Catalan numbers can be recursively defined as follows:
a. b. c. C0 = 1, C1 = 1, and Cn = C0Cn-1 + C1Cn-2 + ... + Cn-2C1 + Cn-1C0 for n>=2.

(Hint: Classify a multiplication sequence based on the last multiplication.)

186.

d. Write an iterative function to compute Cn for a given n. (Remark: The computation of Cn requires all the previous values C0,C1,...,Cn-1. So you are required to store Catalan numbers in an array.) e. Write a recursive function to compute Cn. f. [H] Write an efficient recursive function to compute Cn. Here efficiency means that each Ci is to be computed only once in the entire sequence of recursive calls. [HM] In this exercise we work with permutations of 1,2,...,n. . Write a recursive function that prints all permutations of 1,2,...,n with each permutation printed only once. a. [H2] Write an iterative function that prints all permutations of 1,2,...,n with each permutation printed only once. b. A permutation p = a1,a2,...,an of 1,2,...,n can be treated as a function
c. p : {1,2,...,n} --> {1,2,...,n}

with p(i)=ai for all i=1,2,...,n. If p(b1)=b2, p(b2)=b3, ..., p(bk-1)=bk and p(bk)=b1, we say that (b1,b2,...,bk) is a cycle of length k in p. A permutation can be written as a collection of pairwise disjoint cycles. For example, consider the permutation p of 1,2,...,10:
i : p(i) : 1 5 2 3 3 6 4 5 4 10 6 7 7 9 8 1 9 10 2 8

The cycle decomposition of this p is (1,5,10,8)(2,3,6,7,9)(4). Write a function that, given a positive integer n and an array holding a permutation p of 1,2,...,n, prints the cycle decomposition of p. d. Let p be a permutation of 1,2,...,n. If p(i)=i, then i is called a fixed point of p. A permutation p without any fixed point is called a derangement. Write a function that, upon input n, computes the number of derangements of 1,2,...,n. 187. [H] Tower of Hanoi: There are three pegs A,B,C. Initially, Peg A contains n disks (with holes at their centers). The disks have radiuses 1,2,3,...,n and are arranged in Peg A in increasing sizes from top to bottom, i.e., the disk of radius 1 is at the top, the disk of radius 2 is just below it, ..., the disk of radius n is at the bottom. Your task is to move the disks from Peg A to Peg B in such a way that you are never allowed to move a larger disk on the top of a smaller disk. You may use Peg C as an auxiliary location for the transfer. Write a recursive function by which you can perform this transfer. Your function should print all disk movements. (Hint: First move the top n-1 disks from Peg A to Peg C using Peg B as an auxiliary location. Then move the largest disk from Peg A to Peg B. Finally, move the n-1 disks from Peg C to Peg B using Peg A as an auxiliary location.)

188. In this exercise you are asked to build a function library on polynomial arithmetic. Assume that polynomials with real coefficients need only be considered. . First chalk out a way to represent a polynomial in an array. You may restrict the degree of a polynomial to be less than some bound, say 100. a. Write functions to perform the following operations on polynomials. All the input and output polynomials should be passed as arguments to the functions. Each function is allowed to return nothing or an integer value. Your functions should allow the possibility to store the output in one of the input polynomials. Initialization of a polynomial to the zero polynomial. Addition of two polynomials. Difference of two polynomials. Multiplication of two polynomials. Evaluation of a polynomial at an integer point. Derivative of a polynomial. Integral of a polynomial. Fix the constant of integration using two real values a,b, where b specifies the value that the output polynomial would assume if evaluated at a. Scanning of a polynomial. Printing of a polynomial. 189. The built-in random number generator rand() returns an integer value between 0 and RAND_MAX. You may assume that all these values are equally likely (uniform distribution). Use this built-in random number generator to generate the following: . A signed random integer between -RAND_MAX and RAND_MAX. a. A random integer between 0 and 999. b. A random integer between 100 and 999. c. A random integer between -999 and 999. d. A random floating point number between 0 and 1. e. [HM] On input n (a positive integer) and p (a real number between 0 and 1), a random integer t between 0 and n with probability
f. C(n,t)pt(1-p)n-t,

where C(n,t) stands for the binomial coefficient. g. [H2M] A random floating point number t between 0 and 1 following the continuous probability density function
p(t) = 4t 4 - 4t if 0 <= t <= 1/2, if 1/2 < t <= 1.

h. [H3M] A random non-negative floating point number t with the continuous probability density function
i. e-t for all t >= 0.

190. [H] Write an efficient program to sort a file of integers. The input file is a large piece of data (say 100,000 integers), whereas the maximum size of an array

that can be used inside the program is 1000 (one thousand) integers. Note that you are not allowed to read from and write to the same file simultaneously. Generate the input file of numbers using the built-in random number generator rand(). 191. [M] Formally establish the correctness of the sorting algorithms discussed in the notes (bubble, insertion, selection, merge and quick sort). (Hint: Use induction on the length of the array.) 192. Counting sort: Assume that you want to sort an array A of n integers each known to be in the range 0,1,...,99. Use an array B of size 100 to count how many times each k in the range 0,1,...,99 occurs in A. Then use these counts to rewrite the array A in the sorted order. Your program should use only a number of operations proportional to the size n of A. 193. Odd-even merging: This exercise explores a recursive method of merging two sorted arrays A = (a0,a1,...,an-1) and B = (b0,b1,...,bn-1). For simplicity assume that n is a power of 2. If n=1, then comparing a0 with b0 suffices. So assume that n>1. Recursively merge the sorted subarrays Aodd = (a1,a3,a5,...) and Bodd = (b1,b3,b5,...). Also recursively merge the subarrays Aeven = (a0,a2,a4,...) and Beven = (b0,b2,b4,...). Call the resulting sorted arrays X = (x0,x1,...,xn-1) and Y = (y0,y1,...,yn-1) respectively. . Argue that X and Y can be merged by comparing xi with yi+1 for i=0,1,...,n-2. a. Write a recursive function implementing this odd-even merging step. 194. Write a program for printing the elements of a two-dimensional array (not necessarily square) in each of the following orders: . To-and-fro row-major order. a. Diagonal-major order. b. Spiral order. Notice that the diagonal-major order makes enough sense for square matrices. For general mxn matrices, take the length of each diagonal to be m and treat the elements as organized in a wrap-around fashion. For example, consider the 4x5 matrix: 1 6 2 7 3 8 4 5

9 10

11 12 13 14 15 16 17 18 19 20 The listing of its elements in the to-and-fro row-major order is:


1 2 3 4 5 10 9 8 7 6 11 12 13 14 15 20 19 18 17 16

The listing of the elements in the diagonal-major order is:


1 7 13 19 2 8 14 20 3 9 15 16 4 10 11 17 5 6 12 18

The listing of the elements in the spiral order is:


1 2 3 4 5 10 15 20 19 18 17 16 11 6 7 8 9 14 13 12

195. Stirling numbers s(n,k) of the first kind are non-negative integers defined recursively as:
196. 197. 198. 199. s(0,0) s(n,0) s(n,k) s(n,k) = = = = 1, 0 for n > 0, 0 for k > n, (n-1)s(n-1,k) + s(n-1,k-1) for n > 0 and 0 < k <= n.

. Write a recursive function to compute s(n,k). a. Write an iterative function to compute s(n,k). You should better maintain a two-dimensional array and compute the values s(n,k) in a particular order of the pair (n,k). 200. Stirling numbers S(n,k) of the second kind are non-negative integers defined recursively as:
201. 202. 203. 204. S(0,0) S(n,0) S(n,k) S(n,k) = = = = 1, 0 for n > 0, 0 for k > n, k S(n-1,k) + S(n-1,k-1) for n > 0 and 0 < k <= n.

. Write a recursive function to compute S(n,k). a. Write an iterative function to compute S(n,k). 205. A run in a permutation is a maximal monotonic increasing sequence of adjacent elements in the permutation. For example, the runs in
206. 257183496

are
257, 18, 349, 6.

Every run (except the last) is followed by a descent (also called a fall). For example, in the above permutation the descents are 71, 83 and 96. If a permutation has exactly k+1 runs, then it has exactly k descents, and conversely. Let us denote by the notation
<n,k>

the number of permutations of 1,...,n with exactly k descents. The numbers <n,k> are called Eulerian numbers. All permutations of 1,2,3 and the runs in each permutation are shown below. This list gives us the values of <3,k>.
Permutation descents 123 132 213 231 312 321 Runs 123 13,2 2,13 23,1 3,12 3,2,1 Number of runs 1 2 2 2 2 3 Number of 0 1 1 1 1 2

<3,0> = 1 <3,1> = 4 <3,2> = 1

It is known that the Eulerian numbers satisfy the following recurrence relation:
<n,0> = 1. <n,k> = 0, if k >= n. <n,k> = (k+1)<n-1,k> + (n-k)<n-1,k-1>, otherwise.

. Write a recursive function to compute <n,k>. a. Write an iterative function to compute <n,k>. 207. In this exercise you are asked to build a function library on matrix arithmetic. Consider matrices (not necessarily square) with real entries. . First chalk out a way to represent a matrix in a two-dimensional array. You may restrict the dimension of a matrix to be less than some bound, say 20. a. Write functions to perform the following operations on matrices. All the input and output matrices should be passed as arguments to the functions. Each function is allowed to return nothing or an integer value. You should also check that the dimensions of the input matrices are consistent for the operation. Your functions should allow the provision for the output matrix being the same as one of the input matrices. Initialization of a matrix to the zero matrix of a given dimension. Initialization of a matrix to the (square) identity matrix of a given dimension. Addition of two matrices. Difference of two matrices. Multiplication of two matrices. Inverse of a (square) matrix. Rank of a matrix. Scanning of a matrix. Printing of a matrix. 208. Use your library of the previous exercise to solve a square system of linear equations. If the system is underdefined or inconsistent, your program should report failure. 209. [M] A square matrix A = (aij) is called symmetric if aij = aji for all indices i,j. A is called skew-symmetric if aij = -aji for all indices i,j with i != j. Write a function that, given a square matrix A, computes a symmetric matrix B and a skew-symmetric matrix C satisfying A = B + C.

Course home

CS13002 Programming and Data Structures

Spring semester

Exercise set III


Note: Students are encouraged to solve as many problems from this set as possible. Some of these will be solved during the lectures, if time permits. We have made attempts to classify the problems based on the difficulty level of solving them. An unmarked exercise is of low to moderate difficulty. Harder problems are marked by H, H2 and H3 meaning "just hard", "quite hard" and "very hard" respectively. Exercises marked by M have mathematical flavor (as opposed to computational). One requires elementary knowledge of number theory or algebra or geometry or combinatorics in order to solve these mathematical exercises. 1. Consider the data type complex discussed in the notes. Write a function that takes an array of complex numbers as input and sorts the array with respect to the absolute values of the elements of the array. 2. Write a function that calls the arithmetic routines on the complex data type in order to compute the two complex square roots of a quadratic equation with complex coefficients. 3. Define a structure to represent an (ordered) pair of integers. For two pairs (a,b) and (c,d) we say (a,b) < (c,d) if and only if either a < c or a = c and b < d. Write a function that sorts an array of integer pairs with respect to this ordering (called lexicographic ordering). 4. A rational number is defined by a pair of integers (a,b) with b > 0 and is interpreted to stand for a/b. A rational number a/b is said to be in the reduced form if gcd(a,b)=1. a. Define a structure to represent a rational number. b. Write a function that returns the rational number 0/1. This function can be used to initialize a rational number variable. c. Write a function that, given a rational number, returns its reduced form. d. Write a function that, upon input of two rational numbers, returns the sum of the two input numbers in the reduced form. e. Repeat the previous part for subtraction, multiplication and division of rational numbers. 5. Consider the following subset of complex numbers:
6. Q(i) = { x + iy | x and y are rational numbers and i = sqrt(1) }. a. Define a structure to represent an element of Q(i). Use the rational

structure of the previous exercise for this definition. b. By invoking the arithmetic routines on rational numbers implemented in the previous exercise, implement the arithmetic (addition, subtraction, multiplication and inverse) in Q(i).

7. Define a structure representing a book and having the following fields: Name, list of authors, publisher, year of publication and ISBN number. Write functions to do the following tasks on an array of books: a. Find all books published in a given year. b. Find all books published between two given years. c. Find all books from a given publisher. d. Find all books from a given author. e. Sort the books by their ISBN numbers. f. Sort the books by their names. g. Sort the books by their first authors. 8. Consider the following set of real numbers:
9. A = { a + b sqrt(2) | a,b are integers }.

a. Define a structure to represent an element of A. b. Write a function that, upon input of two elements of A, returns the sum of the input elements. c. Repeat the last part for subtraction and multiplication of elements of A. d. It is known that the element a + b sqrt(2) has an inverse in the set A if and only if a2 - 2b2 = 1 or -1. Write a function that, given an invertible element of A, returns the inverse of the element. If the input to the function is not invertible, the function should return 0 after prompting an error message. 10. Consider the following set of complex numbers:
11. B = { a + b sqrt(-2) | a,b are integers }.

a. Define a structure to represent an element of B. b. Write a function that, upon input of two elements of B, returns the sum of the input elements. c. Repeat the last part for subtraction and multiplication of elements of B. d. [M] It is known that the element a + b sqrt(-2) has an inverse in the set B if and only if a2 + 2b2 = 1. Prove that the only invertible elements of B are 1 and -1. 12. Consider the following set of real numbers:
13. C = { a + by | a,b are integers }.

where y = [1 + sqrt(5)] / 2. a. Define a structure to represent an element of C. b. Write a function that, upon input of two elements of C, returns the sum of the input elements. c. Repeat the last part for subtraction and multiplication of elements of C. d. It is known that the element a + by has an inverse in the set C if and only if a2 + ab - b2 = 1 or -1. Write a function that, given an invertible element of C, returns the inverse of the element. If the input to the function is not invertible, the function should return 0 after prompting an error message. 14. [HM] Consider the following set of real numbers:
15. D = { a + bw + cw2 | a,b,c are integers }.

where w is the real cube root of 2.

a. Define a structure to represent an element of D. b. Write a function that, upon input of two elements of D, returns the sum of the input elements. c. Repeat the last part for subtraction and multiplication of elements of D. d. [H2M] Define the norm of an element t = a + bw + cw2 of D as
e. N(t) = (a + bw + cw2)(a + bw' + cw'2)(a + bw'' + cw'' ),
2

where w',w'' are the two complex cube roots of 2. It is known that t is invertible in D if and only if N(t) is 1 or -1. Write a function that, upon input of an element t of D, determines whether t is invertible in D. Note: In modern algebra, the sets A,B,C,D of the above exercises are examples of number rings. These rings constitute some central objects of study in algebraic number theory. 16. A circle in the X-Y plane is specified by three real numbers a,b,c. The real numbers may be interpreted in two possible ways. The first possibility is that (a,b) represents the center and c the radius of the circle. In the second representation, we refer to the equation of the circle as:
17. X2 + Y2 + aX + bY + c = 0.

So a structure holding three double variables together with a flag indicating the particular interpretation suffices to store a circle. a. Write a function that converts a circle structure from the first to the second representation. b. Write a function that converts a circle structure from the second to the first representation. c. Write a function that, upon input a circle and two real numbers x,y, checks whether the point (x,y) lies inside, on or outside the circle. Note that the input circle may be of any of the two representations. d. Write a function that, upon input two circles each with any representation, determines whether the circles touch, intersect or do not intersect. e. Write a function that, upon input a circle in any representation, returns the side of a square that has the same area as the input circle. 18. A rectangle in the X-Y plane can be specified by eight real numbers representing the coordinates of its four corners. Define a structure to represent a rectangle using eight double variables. Notice that here we do not assume the sides of a rectangle to be necessarily parallel to the X and Y axes. Notice also that by a rectangle we mean only the boundary (not including the region inside). a. Write a function that, upon input a structure of the above kind, determines whether the structure represents a valid rectangle. b. Write a function that, upon input a valid rectangle, determines the area of the rectangle.

c. [HM] Write a function that, upon input a valid rectangle and two real numbers x,y, determines whether the point (x,y) lies inside, on or outside the rectangle. d. [H2M] Write a function that, upon input two valid rectangles, determines whether the two rectangles touch, intersect or do not intersect. 19. In a doubly linked list, each node is given two pointers, the first pointing to the next element in the list and the second to the previous element in the list. The next pointer of the last node and the previous pointer of the first node should be NULL. a. Draw a doubly linked list on four nodes. b. Define a structure with self-referencing pointers to represent a node in a doubly linked list. 20. Use a sequence of memory allocation calls in order to create a linked list of one hundred complex numbers, where for each k=1,2,...,100 the k-th number in the list is k2 + i(-1)kk2. 21. Create a doubly linked list of the 100 complex numbers of the previous exercise. 22. In a ternary tree each node has three children (each possibly empty). a. Draw a ternary tree having the following nodes:
b. c. d. e. f. g. h. i. j. Node a b c d e f g h Children b,c,d e,-,f -,-,g -,-,-,h,-,-,-,-,-,-,-

Here - (dash) denotes an empty child. k. Define a structure data type with self-referencing pointers to represent a node in a ternary tree. 23. Use a sequence of memory allocation calls to create the ternary tree of the last exercise. 24. Consider the following type definition:
25. typedef char *compactString;

The idea is to dynamically store (null-terminated) character strings in compactString arrays so that each array is allocated the exact amount of memory necessary to store a string. For example, the string "AbCdEf" is of length 6 and requires 7 characters (including the trailing null character) for storage. So a compactString storing this string should be allocated exactly 7 bytes of memory. Implement functions for doing the following tasks on compactString arrays.
/* Initialize a compactString to the empty string. */ compactString initEmpty (); /* Reverse the compactString s and store in the compactString t */

void reverse ( compactString t , compactString s ) ; /* Append the character c to the compactString s */ void append ( compactString s , char c ) ; /* Insert the character c at the beginning of the compactString s */ void prepend ( compactString s , char c ) ; /* Delete and return the last character of the compactString s */ char delEnd ( compactString s ) ; /* Delete and return the first character of the compactString s */ char delStart ( compactString s ) ; /* Save to the compactString t the prefix of the compactString s of length n */ void prefix ( compactString t , compactString s , unsigned int n ) ; /* Save to the compactString t the suffix of the compactString s of length n */ void suffix ( compactString t , compactString s , unsigned int n ) ; /* Concatenate the compactString's s1 and s2 and store in the compactString t */ void concatenate ( compactString t , compactString s1 , compactString s2 ) ;

You should use dynamic memory management in order to ensure compact representations of strings. Notice also that in the above prototypes, you should allow the target string t to be the same as an input argument. For example, a prefix of s may be stored in s itself. You should free unused allocated memory. 26. Memory allocation and reallocation on compactString's of the last exercise need be carried out even when the size of the string changes by 1. In order to reduce this overhead, let us plan to dynamically maintain the size allocated to each array to be a power of 2. Whenever a string requires n bytes for storage, we actually allocate 2t bytes, where 2t-1 < n <= 2t. In that case many operations require no reallocation of memory. However, we need to maintain the actual amount of memory allocated to a string. Define the following data type:
27. 28. 29. 30. typedef struct { char *data; int allocSize; } semiCompactString;

Rewrite the functions of the previous exercise for semiCompactString's.

31. A (univariate) polynomial is specified by an array of its coefficients. Since one cannot check during program execution the size of a static or dynamic array, one should additionally maintain the degree of a polynomial. a. Define a structure to represent a polynomial with integer coefficients. The coefficient array is to be maintained dynamically so that the exact amount of memory needed to store the coefficient array is only allocated. b. Write a function that, given a polynomial p (in this representation) and an integer a, returns the value p(a). c. Write functions to implement arithmetic routines (addition, subtraction and multiplication) on polynomials. Each routine should be stingy enough to (re)allocate to the output polynomial only the space just required to store the coefficient array. 32. You are given a network of sensor nodes deployed in a battlefield. Each node is specified by its id (an integer) and its location of deployment (two integer or floating point numbers indicating the X and Y coordinates of the node with respect to some fixed reference point). A sensor node can communicate with another provided that they are physically located within 100 meters of one another. In that case, the two nodes are called neighbors. You are given a text file whose first line stores the number n of nodes in the sensor network. In lines 2 through n+1 individual nodes are specified by three numbers (id and coordinates). For simplicity, assume that the node ids are 0,1,2,...,n-1. Read the data from the file and store in a dynamic two-dimensional array the list of neighbors of each node. The i-th row should store all the neighbors of node i in sorted order (of id) and should be allocated exactly the amount of memory needed to accommodate this list of neighbors. 33. A matrix is said to be sparse if each row of it contains only few non-zero entries. Such sparse matrices occur in many situations. For example, a complicated machine may have one million components, but each component interacts with at most one hundred other components. So the interaction matrix, though millionby-million in size, has at most one hundred non-zero entries in each row and may be rightfully dubbed sparse. In order to store a sparse matrix, it suffices to store for each row only the column indices where non-zero entries occur and also the corresponding entries. This reduces the space overhead associate with the storage. For the example in the last paragraph, a complete million-by-million array requires space for storing one trillion (1012) entries, whereas a sparse representation is capable of storing the same matrix in a space for only one hundred million index-entry pairs. a. Define a suitable dynamic two-dimensional array type to represent a sparse matrix. b. Write a function that computes the transpose of a sparse matrix (under this representation). c. Write a function that adds two sparse matrices.

d. Write a function that multiplies two sparse matrices. Note that the transpose At of a sparse matrix A is not necessarily sparse. Some rows of At may be quite dense. However, if the non-zero elements of A occur at randomly chosen columns, then At is also sparse with high probability. The product of two sparse matrices is expected to be much less sparse than the arguments. 34. We generally deal with complex numbers of the form a + ib, where a and b are floating point numbers. However, in the special case when both a and b are integers, it may be desirable to store a and b as integers. Define a structure holding the real and imaginary parts of a complex number together with a flag. Depending on the value of the flag, the two parts of the complex number are to be interpreted. If the flag has the zero value, the parts are treated as floating point numbers. For any non-zero value of the flag, the parts are treated as integers. Your structure should contain a union for the storage of the two parts. Write a routine to initialize a complex number to the zero value. The initialization routine should also accept a value for the flag as an argument and set the real and imaginary parts in the union accordingly. Write the arithmetic routines on these complex numbers. Each argument in each such routine may be of any type (pair of floating point numbers or of integers). Your program should output an integer pair as the output complex number if both the input arguments are integer pairs. If one or both of the input arguments is/are floating point pair(s), the output should also be a floating point pair. 35. Write a program to solve the following problem. You are given a text file. Your problem is to adjust the spaces in each line in such a way that the resulting text is justified (both at the left and at the right). Here is our proposal of an algorithm that you should use in order to perform text justification. o First read the input file and store the lines in a two-dimensional character array with each line stored in a single row of the array. o The text is assumed to be divided into paragraphs. Two consecutive paragraphs are separated by a blank line. o The last line in a paragraph is not to be justified. Also a blank line is not to be justified. o Finally suppose that you have a non-blank line which is not the last line of a paragraph. If the length of this line is already larger than the target width, then do not perform any processing of this line. Otherwise, increase the sizes of the inter-word gaps so that the resulting line has a width equal to the target width. Assume that len denotes the initial length of the line

and that the line initially contains nsp number of inter-word spaces. You have to add a total of
o extra = target_width - len

number of additional spaces to the line. In order that the insertion leads to (aesthetically) good-looking paragraphs, it is necessary to distribute the extra new spaces more or less uniformly among the nsp inter-word gaps. Let
q = extra / nsp (integer division).

First insert q additional spaces in each of the nsp gaps. If extra is not an integral multiple of nsp, this still leaves us with
r = extra - q * nsp

spaces to be inserted. If the line has an odd number in the current paragraph, add another single space in each of the first r gaps. On the other hand, if the line has an even number in the current paragraph, add a single space in each of the last r gaps.

Course home

CS13002 Programming and Data Structures

Spring semester

Exercise set IV
Note: Students are encouraged to solve as many problems from this set as possible. Some of these will be solved during the lectures, if time permits. We have made attempts to classify the problems based on the difficulty level of solving them. An unmarked exercise is of low to moderate difficulty. Harder problems are marked by H, H2 and H3 meaning "just hard", "quite hard" and "very hard" respectively. Exercises marked by M have mathematical flavor (as opposed to computational). One requires elementary knowledge of number theory or algebra or geometry or combinatorics in order to solve these mathematical exercises. 1. Rewrite the dynamic linked list implementations of the ordered list, stack and queue ADTs incorporating the feature that whenever a node is deleted, the memory allocated to that node is freed. 2. Implement the ordered list, stack and queue ADTs with dynamic linked lists but without using the dummy nodes at the beginning of the lists. 3. Dynamic arrays may be used to provide a third implementation of ordered list, stack and queue ADTs. Here the array holding the list is to be allocated memory dynamically depending on the size of the list. Implement the ADTs using dynamic arrays. 4. Implement the ordered list ADT using doubly linked lists. (Recall from Exercise set III that in a doubly linked list each node maintains two pointers, one pointing to the next node in the list, the other to the previous node in the list.) 5. Write a function that takes as arguments two sorted linked lists and outputs a sorted linked list obtained by merging the two input lists. 6. Think of the ordered list ADT modified using the following strategy. Whenever an element is located using the isPresent() operation, that particular element is deleted from the current position and reinserted at the beginning of the list. The motivation behind this relocation is that in many situations an element accessed in a list is expected with high probability to be accessed several times in the future. So keeping the element near the beginning of the list reduces average search time. Modify the ordered list ADT implementations to incorporate this modification. 7. Consider the ADT set that represents a collection of integers. The ADT should support standard set operations:
8. 9. 10. 11. 12. 13. 14. S = init(); /* Initialize S to the empty set */ isEmpty(S); /* Return true if and only if S is the empty set */ isSingleton(S);

15. /* Return true if and only if S contains only one element */ 16. 17. isMember(S,a); 18. /* Return true if and only if a is a member of the set S */ 19. 20. S = addElement(S,a); 21. /* Add the element a to the set S. If a is already in S, 22. there will be no change, else a new element is to be inserted. */ 23. 24. S = delElement(S,a); 25. /* Remove the element a from the set S. No change if a is not 26. a member of S. */ 27. 28. S = union(U,V); 29. /* Assign to S the union of the sets U and V */ 30. 31. S = intersection(U,V); 32. /* Assign to S the intersection of the sets U and V */ 33. 34. S = difference(U,V); 35. /* Assign to S the set difference U - V */ 36. 37. S = symmDiff(U,V); 38. /* Assign to S the symmetric difference (U - V) union (V U) */ 39. 40. print(S); 41. /* Print the elements of the set S */ 42. 43. printSorted(S); 44. /* Print the elements of the set S in the sorted order. */

a. Implement the set ADT using static arrays. b. Implement the set ADT using dynamic arrays. c. Implement the set ADT using linked lists. 45. A multiset is like a set with the exception that each member of the set may be present multiple times. For example, an aquarium is a multiset specified by the different species of fish it contains and by the number of fish in the aquarium belonging to each such species. For this exercise, concentrate on multisets of integers (because integers are less fishy). The multiset ADT should support the following operations.
46. S = init(); 47. /* Initialize S to the empty multiset */ 48. 49. isMember(S,a); 50. /* Return true if and only if a is a member of the multiset S */ 51. 52. count(S,a); 53. /* Return the number of occurrences of a in the multiset S */ 54.

55. S = addElement(S,a,n); 56. /* Add n occurrences of a to the multiset S */ 57. 58. S = delElement(S,a,n); 59. /* Delete n occurrences of a from the multiset S. If S contains 60. less than n occurrences of a, only those many that are present 61. in S need be deleted. */ 62. 63. S = union(U,V); 64. /* Assign to S the union of the multisets U and V. If U and V 65. respectively contain m and n occurrences of a, then their 66. union would contain m+n occurrences of a. */ 67. 68. S = intersection(U,V); 69. /* Assign to S the difference of the multisets U and V. If U and V 70. respectively contain m and n occurrences of a, then their 71. intersection would contain min(m,n) occurrences of a. */ 72. 73. S = difference(U,V); 74. /* Assign to S the difference U - V. If U and V respectively 75. contain m and n occurrences of a, then their difference would 76. contain max(m-n,0) occurrences of a. */ 77. 78. print(S); 79. /* Print all the elements of S with positive multiplicities. Also 80. print the corresponding multiplicities. */ 81. 82. printSorted(S); 83. /* Same as print(S) except that the elements are printed in the 84. sorted order. */

a. Implement the multiset ADT using static arrays. b. Implement the multiset ADT using dynamic arrays. c. Implement the multiset ADT using linked lists. 85. [H] A nested ordered list of integers is recursively defined as follows:
86. 87. 88. The empty tuple () is a nested list. If A0,A1,...,An-1 are nested lists or integers for some n >= 1, then (A0,A1,...,An-1) is again a nested list.

Here are some examples:


() (3,4,5) ((),3,(),(4),5) (3,(4),((5),(),(6,7,(8))),((9),()))

A nested list should support the following functions:


L = init(); /* Initialize the nested list L to the empty list */ isEmpty(L); /* Returns true if and only if L is the empty list */ L = insertInt(U,a,k); /* If U = (U0,U1,...,Um-1) is a list and a an integer, then assign to L the nested list (U0,U1,...,Uk-1,a,Uk,...,Um-1). Report error if k > m. */ L = insertList(U,V,k); /* If U = (U0,U1,...,Um-1) is a list and V a nested list, then assign to L the nested list (U0,U1,...,Uk-1,V,Uk,...,Um-1). Report error if k > m. */ L = join(U,V); /* If U = (U0,U1,...,Um-1) and V = (V0,V1,...,Vn-1) are nested lists, assign to L the nested list (U0,U1,...,Um-1,V0,V1,...,Vn-1). */ L = joinAt(U,V,k); /* If U = (U0,U1,...,Um-1) and V = (V0,V1,...,Vn-1) are nested lists, assign to L the nested list (U0,U1,...,Uk-1,V0,V1,...,Vn1,Uk,...,Um-1). Report error if k > m. */ L = delete(U,k); /* If U = (U0,U1,...,Um-1) is a nested list, assign to L the nested list (U0,U1,...,Uk-1,Uk+1,...,Um-1). Report error if k >= m. */ L = sublist(U,k,l); /* If U = (U0,U1,...,Um-1) is a nested list, return the sublist (Uk,...,Ul). Report error for improper indices k,l. */ print(L); /* Print the nested list L as a fully parenthesized expression. */

89. Implement the (univariate) polynomial ADT with all standard arithmetic operations on polynomials. First mention the prototypes of the ADT functions and then implement. Use dynamic memory management to implement the list of coefficients. 90. A multivariate polynomial in n variables X1,...,Xn is a finite sum of terms of the form aX1e1,...,Xnen, with each ei being a non-negative integer and with the

coefficient a being an integer. Arithmetic operations on a multivariate polynomial are carried out following standard rules. a. Write the ADT functions for polynomials. Include standard arithmetic operations and partial derivatives. b. Assume that the number of variables is small, like 2 or 3. Implement the ADT using static multi-dimensional arrays to store the coefficients. c. Also implement the ADT using dynamic multi-dimensional arrays to store the coefficients. d. [H] Each term in a multivariate polynomial is identified by the coefficient and the exponents. For example, the term aX1e1,...,Xnen is determined by the tuple (a,e1,...,en). So a structure capable of storing n+1 fields suffices to store a term, and a polynomial is an array of such structures. Use this strategy to implement the polynomial ADT. You should think of a way to order the exponent tuples (e1,...,en) and store the terms sorted under this ordering. e. [H2] An n-variate polynomial can be thought of as a univariate polynomial whose coefficients are (n-1)-variate polynomials. This recursive definition provides us with yet another handle for implementing the polynomial ADT. Use a nested linked list structure for a concrete recursive realization of the multivariate polynomial ADT. 91. Suppose you want to implement two stacks in a single array. Two possibilities are outlined here: Odd-even strategy: Stack 1 uses locations 0,2,4,... of the array, whereas Stack 2 uses the array locations 1,3,5,... Colliding strategy: The two stacks start from the two ends of the array and grow in opposite directions (towards one another). Implement both the strategies. Write two sets of initialize, push and pop functions. 92. Write a function that uses the stack ADT calls in order to reverse a character string. 93. [M] Suppose that you have a stack and push to the stack the integers 1,2,...,n in that sequence. In between these push operations you also invoke some pop operations in such a way that a pop request is never sent to an empty stack. Immediately before each pop operation you also print the top of the stack. After all of the integers 1,2,...,n are pushed, the elements remaining in the stack are printed and popped resulting in an eventually empty stack. The printed integers form a permutation of the integers 1,2,...,n. An example is given below for n = 5:
94. 95. 96. 97. 98. S = init(); S = push(S,1); S = push(S,2); print top(S); S = pop(S);

99. 100. 101. 102. 103. 104. 105. 106. 107. 108. 109.

S = push(S,3); S = push(S,4); print top(S); S = pop(S); print top(S); S = pop(S); S = push(S,5); print top(S); S = pop(S); print top(S); S = pop(S);

This sequence prints the permutation:


2,4,3,5,1

Prove or disprove: All permutations of 1,2,...,n can be generated by a suitable sequence of such push and pop operations. 110. Use stack ADT calls to recognize strings with balanced parentheses. Examples of such strings: (), ((())), ()()(), ()(()(()(()()))). Nonexamples: ((), (()(()))), )()(. 111. Use stack ADT calls to recognize strings with balanced parentheses and square brackets. Examples of such strings: (), ([()]), []()[], ()[()(()[()()])]. Non-examples: (], (()[)()), ([], [()[[]]]), ]()[. 112. [HM] The usual infix notation for writing arithmetic expressions requires parentheses in order to specify the exact sequence of operations. For example, 2+3x4 refers to 2+(3x4). If we want to do the addition first and then the multiplication, we must put explicit parentheses like (2+3)x4. The postfix notation for 2+(3x4) is 2 3 4 x + and that for (2+3)x4 is 2 3 + 4 x. In the postfix notation we do not require parentheses and still the meaning (i.e., the exact sequence of operations) is uniquely determined by the expression. In the rest of this exercise you are asked to prove this property of the postfix notation. The result continues to hold for any mix of binary, unary, ternary, ... operators. In this exercise, assume for the sake of simplicity that all operators are binary. An arithmetic expression contains operands and operators. A token is either an operator or an operand. Prove the following assertions: a. An arithmetic expression (involving binary operations only) contains an odd number of tokens. b. A postfix expression starts with an operand and ends with an operator provided that it is of length bigger than 1. c. A postfix expression with (exactly) 2m+1 tokens has m operators and m+1 operands. d. For any postfix expression and any position in the expression, the number of operands to the left of the position is strictly larger than the number of operators to the left of the same position. e. No proper suffix of a valid postfix expression is again a valid postfix expression.

f. Let op be the last token (an operator) in a postfix expression with more than one tokens. Then the expression looks like arg1 arg2 op, where arg1 and arg2 are the two arguments for op and are again valid postfix expressions. The arguments arg1 and arg2 can be uniquely identified from the original postfix expression. 113. Write a function that takes a fully parenthesized arithmetic expression in the infix notation as the input and outputs the value of the expression. You may restrict only to binary operations (+,-,*,/,%). You may also assume that all operands are integers. In order to evaluate a parenthesized arithmetic expression in the infix notation, one may use a stack. A token in such an expression is either an operand (an integer) or an operator (+,- etc.) or the left parenthesis or the right parenthesis. One starts with an empty stack and reads the input string from left to right. Once a token other than the right parenthesis is read from the input, the token is pushed to the stack. When a right parenthesis is encountered, pop operations are performed until a left parenthesis is popped out. The tokens (excluding the parentheses) that are popped out form a simple (sub)expression (either a single integer or two integers separated by an operation). Evaluate that sub-expression and push the value back to the stack. When the entire input is read, the stack should contain a single integer which is the value of the input expression. 114. Suppose you have the stock prices p1,...,pn of a company for n consecutive days. The span of day i is the maximum number of consecutive days (starting at and including day i) over which the stock price pi remains maximum. For example, for the stock prices 5,4,3,3,4,2,6,3 on 8 days, the respective spans are 6,5,2,1,2,1,2,1. Use stack ADT calls to compute the span of each day. 115. Suppose that you have an mxn maze of rooms. Each adjacent pair of rooms has a door that allows passage between the rooms. At some point of time some of the doors are locked, the rest are open. A mouse sits at room number (s,t) and there is fabulous food for the mouse at room number (u,v). Your task is to determine whether there exists a route for the mouse from room (s,t) to room (u,v) through the open doors. The idea is to start a search at room no (s,t), then investigate rooms (s1,t1),...,(sk,tk) that can be reached from (s,t) and then those rooms that can be reached from each (si,ti), and so on. There is no need to revisit a room during the search. Maintain an mxn array of flags in order to keep track of the rooms that are visited. a. [Depth-first search] Use a stack to implement the search. Initially push (s,t) to the empty stack. Subsequently, as long as the stack is not empty, consider the room (x,y) at the top of the stack. If (x,y) has a yet unvisited neighboring room, push that room at the top of the stack. If (x,y) does not have an unvisited neighboring room, pop (x,y) out of the stack. If during these operations, the desired room (u,v) ever appears at (the top of) the stack, then a route from (s,t) to (u,v) is detected. If the search completes (i.e., the stack becomes empty) without ever having (u,v) in the stack, then there is no (s,t)-(u,v) path.

b. [Breadth-first search] Implement the search using a queue. Maintain a queue of rooms to search from. Initially enqueue (s,t) to an empty queue. Subsequently, as long as the queue is not empty, look at the room (x,y) at the front of the queue. If (x,y) = (u,v), then report success and return. Else dequeue (x,y) from the front and enqueue all unvisited neighboring rooms at the back of the queue. If the search stops (i.e., the queue becomes empty) without ever having (u,v) at the front of the queue, report failure. c. [Random walk] Also implement a memoryless version of the search. Your program does not have to remember what rooms have already been visited by the mouse. The mouse would instead randomly select one of the open doors for going to an adjacent room (which may be visited earlier). If there is no (s,t)-(u,v) path, then whatever random sequence of movements the mouse makes, it can never reach the room (u,v). On the other hand, if there exists an (s,t)-(u,v) path, the mouse would eventually reach the desired room (u,v) with high probability. However, in this case there remains a chance that the room selection of the mouse is so bad that it misses a desired path for ever. The probability that this awkward incident happens decreases considerably with the number of moves. Since your program has to run for a finite time, you cannot obviously wait for an indefinitely long exploration by the mouse. Assume instead that the mouse dies of hunger and exhaustion, after it makes million room changes without ever reaching the food at room (u,v). d. Prepare some configurations of the rooms for which there actually exist (s,t)-(u,v) path(s). Run the above three algorithms on these configurations and compare the numbers of room changes that the mouse makes under the three different strategies in order to reach room (u,v). 116. Use queue ADT calls to implement round-robin scheduling as exemplified in this animation. 117. Write a function making suitable stack and queue ADT calls to solve each of the following problems: a. Given a string, check if it is of the form w#w, where w is a string with alphanumeric characters only. b. Given a string, check if it is of the form ww, where w is a string with alphanumeric characters only. c. Given a string, check if it is of the form w#wr, where w is a string with alphanumeric characters only, and where wr stands for the reverse of the string w. d. Given a string, check if it is of the form wwr, where w is a string with alphanumeric characters only. e. Given a string, check if it is a palindrome. 118. A double-ended queue is a queue with the exception that it supports insertion and deletion at both the ends. Each insert/delete operation must specify the end at which the operation is to be performed. Implement initialization, insertion and deletion functions on a double-ended queue using: a. Static arrays b. Dynamic arrays

c. Linked lists d. Doubly linked lists

Course home

CS13002 Programming and Data Structures

Spring semester

Exercise set V
Note: Students are encouraged to solve as many problems from this set as possible. Some of these will be solved during the lectures, if time permits. We have made attempts to classify the problems based on the difficulty level of solving them. An unmarked exercise is of low to moderate difficulty. Harder problems are marked by H, H2 and H3 meaning "just hard", "quite hard" and "very hard" respectively. Exercises marked by M have mathematical flavor (as opposed to computational). One requires elementary knowledge of number theory or algebra or geometry or combinatorics in order to solve these mathematical exercises. 1. [M] Arrange the following functions in the increasing order of their rates of growth:
(sqrt(2))n, 2sqrt(n), n2log n, n(log n)2, (nlog n)2, nlog n, nsqrt(n), nn, (log n)n. 3. Let f(n) be the polynomial 4. f(n) = adnd + ad-1nd-1 + ... + a1n + a0 2.

with ad > 0. Prove that f(n) = O(nd) and nd = O(f(n)). (Note that some of the coefficients ai may be negative.) 5. Let f(n),g(n),f1(n),g1(n) be positive real-valued functions of natural numbers. Prove the following assertions: a. If f(n) = O(f1(n)) and g(n) = O(g1(n)), then f(n) + g(n) = O(f1(n) + g1(n)). b. If f(n) = O(f1(n)) and g(n) = O(g1(n)), then f(n)g(n) = O(f1(n)g1(n)). c. f(n) + g(n) = O(max(f(n),g(n))). 6. Establish that the worst-case running times of insertion sort and of selection sort on an array of n elements are O(n2). 7. [M] Denote U(n) = S(n) / 3, where S(n) is as defined in the derivation of the running time of the recursive Fibonacci function. Find a closed form formula for U(n) and hence for T(n). 8. Deduce that the following function recursively computes Fibonacci numbers in linear time.
9. 10. 11. 12. 13. 14. 15. int F ( int n , int *Fprev ) { int Fn_1, Fn_2; if (n == 0) { *Fprev = 1; return (0);

16. 17. 18. 19. 20. 21. 22. 23. 24.

} if (n == 1) { *Fprev = 0; return (1); } Fn_1 = F(n-1,&Fn_2); *Fprev = Fn_1; return (Fn_1+Fn_2); }

25. The following function recursively determines whether a given string is a palindrome. Determine its time complexity.
26. 27. 28. 29. 30. 31. 33. 34. 35. 36. 37. 38. 39. 40. 41. 42. 43. int isPalindrome ( char A[] , int n ) { if (n <= 1) return 1; if (A[0] != A[n-1]) return 0; return isPalindrome(&A[1],n-2); } int f ( int A[SIZE][SIZE] , int n ) { int i, j, sum = 0; for (i=0; i<n; ++i) { if (i % 2 == 0) for (j=0; j<=i; j=j+1) sum = sum + A[i][j]; else for (j=n-1; j>=i; j=j-1) sum = sum - A[i][j]; } }

32. Determine the time complexity of the following iterative function:

44. [H] Write a function that accepts a positive integer n and prints all permutations of 1,2,3,...,n. Assume that printing a single integer is a basic operation and establish the time complexity of your function. 45. Establish that merging two sorted arrays each of size n/2 can be done in O(n) time. 46. Establish that merging two sorted linked lists each of size n/2 can be done in O(n) time. 47. [H] Write the sorting routines (bubble, insertion, selection, quick and merge sorts) for linked lists. Each routine should have the same time complexity as the corresponding routine on arrays. 48. Consider the Tower of Hanoi problem of Exercise set II. Solve the problem using the algorithm that first recursively moves the top n-1 disks from Peg A to Peg C using Peg B as an auxiliary location, then moves the largest disk from Peg A to Peg B, and finally moves the n-1 disks from Peg C to Peg B using Peg A as an auxiliary location. Let T(n) denote the number of disk movements done by the algorithm for n disks. a. Show that T(n) satisfies the following recurrence:
b. c. T(1) = 1, T(n) = 2T(n-1) + 1 for n >= 2. d. Prove that T(n) = 2n - 1 for all n >= 1.

e. [HM] Argue that any algorithm that solves the Tower of Hanoi problem must make at least 2n - 1 disk movements. (Hint: Consider the instance when the largest disk is removed from Peg A.) 49. Compare the running times of the insertion and deletion functions in our implementations of the ordered list, stack and queue ADTs. Express the running time in terms of the current size n (number of elements) of the list (or stack or queue). 50. [H2] In this exercise we plan to compute the binomial coefficient C(n,k). Several algorithms are proposed to that effect. These algorithms vary widely in their time complexities ranging from polynomial to truly exponential. a. We know that binomial coefficients satisfy the recurrence relation:
b. C(n,k) = C(n-1,k) + C(n-1,k-1)

for suitable values of n,k. Write a recursive function that straightaway uses this recurrence relation. Use suitable boundary conditions so that recursion eventually terminates. c. Deduce that the running time of the recursive algorithm of Part a) is exponential and not polynomial in n. For computing the running time, take k <= n. d. Use a two-dimensional auxiliary array to keep track of the pairs (m,j) for which C(m,j) has already been computed. If the value is available, replace a recursive call for computing C(m,j) by reading the value from the auxiliary array. This technique is known as memoization. e. Deduce that the running time of the recursive routine with memoization is polynomial in n. f. Write an iterative routine that generates the Pascal triangle in the following order: C(0,0), C(1,0), C(1,1), C(2,0), C(2,1), C(2,2), ... till the value of C(n,k) is computed. The top-down algorithm of Part a) recomputes many C(m,j) values multiple times. The bottom-up technique of this part is an example of dynamic programming. g. Deduce that the iterative algorithm of Part e) runs in time polynomial in n. h. Use the formula
i. C(n,k) = n(n-1)...(n-k+1) / k!

to compute the value of C(n,k). j. Argue that the running time of the algorithm of Part g) is polynomial in n. k. Compare the space complexities of the above four algorithms. 51. Suppose we want to compute the transpose of a matrix A and store the result in A itself. We do not assume A to be necessarily a square matrix. a. Write a function that takes an m x n matrix A as input, computes in a local matrix B the transpose of A and finally copies B back to A. What is the space complexity of this function? b. [H] Write a function that computes At in A using only a constant amount of additional storage.

52. Write a function that takes a square matrix A as input and computes in A itself the matrix A - At using only O(1) additional storage. (Hint: The matrix A - At is anti-symmetric, i.e., its (j,i)-th element is the negative of its (i,j)-th element.) 53. Let A be an n x n matrix. a. Write a function that converts A to row-reduced echelon form in O(n3) time using elementary row operations only. b. Write a function that computes the determinant of A in O(n3) time. 54. Write a function that computes the rank of an n x n matrix in O(n3) time. 55. Write a function that inverts an n x n matrix in O(n3) time. 56. Write a function that, given a square system
57. Ax = b

of linear equations, determines a solution for x, provided that the system is solvable. Your function should run in a time polynomial in the size (number of variables or equations) of the system. Your function should handle underdetermined (but consistent) systems, i.e., systems that have multiple solutions. 58. In this exercise a sparse matrix denotes a square matrix having only few (constant numbers of) non-zero elements in each row. a. Define a data type for storing a sparse matrix. b. Write a function that adds two n x n sparse matrices in O(n) time. c. [H] Write a function that multiplies two n x n sparse matrices in O(n2) time. Notice that the complexities of addition and multiplication of dense (non-sparse) n x n matrices are O(n2) and O(n3) respectively. The sparse representation brings down these complexity figures.

Course home

Vous aimerez peut-être aussi