c notes iitkgp

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 a 1 ,a 2 ,...,a n constitute the input. Some function f(a 1 ,a 2 ,...,a n ) 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

Upload: iamdivya24

Post on 30-Nov-2015

97 views

Category:

Documents


0 download

DESCRIPTION

full notes for help regarding c..

TRANSCRIPT

Page 1: C NOTES IITKGP

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

Page 2: C NOTES IITKGP

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

Page 3: C NOTES IITKGP

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.

Page 4: C NOTES IITKGP

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:

o 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

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-

Page 5: C NOTES IITKGP

processor memory provides the desired storage space. External memory is

classified into two categories:

o 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

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.

Page 6: C NOTES IITKGP

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

Page 7: C NOTES IITKGP

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-decode-

execute 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.

Page 8: C NOTES IITKGP

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?

Page 9: C NOTES IITKGP

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!

Page 10: C NOTES IITKGP

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.

Page 11: C NOTES IITKGP

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;

Page 12: C NOTES IITKGP

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;

Page 13: C NOTES IITKGP

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

Page 14: C NOTES IITKGP

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

Page 15: C NOTES IITKGP

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 Bit

size Minimum value Maximum value

char 8 -27=-128 27-1=127

short int 16 -215=-32768 215-1=32767

int 32 -231=-2147483648 231-1=2147483647

long int 32 -231=-2147483648 231-1=2147483647

long long int 64 -263=-9223372036854775808

263-1=9223372036854775807

unsigned char 8 0 28-1=255

unsigned short int 16 0 216-1=65535

unsigned int 32 0 232-1=4294967295

unsigned long int 32 0 232-1=4294967295

unsigned long long int 64 0 264-1=18446744073709551615

Notes

Page 16: C NOTES IITKGP

• 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 32-

bit 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

Page 17: C NOTES IITKGP

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 Decimal Hex Binary Character

32 20 00100000 SPACE 80 50 01010000 P

33 21 00100001 ! 81 51 01010001 Q

34 22 00100010 " 82 52 01010010 R

35 23 00100011 # 83 53 01010011 S

36 24 00100100 $ 84 54 01010100 T

37 25 00100101 % 85 55 01010101 U

38 26 00100110 & 86 56 01010110 V

39 27 00100111 ' 87 57 01010111 W

40 28 00101000 ( 88 58 01011000 X

41 29 00101001 ) 89 59 01011001 Y

42 2a 00101010 * 90 5a 01011010 Z

43 2b 00101011 + 91 5b 01011011 [

44 2c 00101100 , 92 5c 01011100 \

45 2d 00101101 - 93 5d 01011101 ]

46 2e 00101110 . 94 5e 01011110 ^

47 2f 00101111 / 95 5f 01011111 _

48 30 00110000 0 96 60 01100000 `

49 31 00110001 1 97 61 01100001 a

50 32 00110010 2 98 62 01100010 b

51 33 00110011 3 99 63 01100011 c

52 34 00110100 4 100 64 01100100 d

53 35 00110101 5 101 65 01100101 e

54 36 00110110 6 102 66 01100110 f

55 37 00110111 7 103 67 01100111 g

56 38 00111000 8 104 68 01101000 h

57 39 00111001 9 105 69 01101001 i

58 3a 00111010 : 106 6a 01101010 j

59 3b 00111011 ; 107 6b 01101011 k

60 3c 00111100 < 108 6c 01101100 l

61 3d 00111101 = 109 6d 01101101 m

62 3e 00111110 > 110 6e 01101110 n

63 3f 00111111 ? 111 6f 01101111 o

64 40 01000000 @ 112 70 01110000 p

65 41 01000001 A 113 71 01110001 q

66 42 01000010 B 114 72 01110010 r

Page 18: C NOTES IITKGP

67 43 01000011 C 115 73 01110011 s

68 44 01000100 D 116 74 01110100 t

69 45 01000101 E 117 75 01110101 u

70 46 01000110 F 118 76 01110110 v

71 47 01000111 G 119 77 01110111 w

72 48 01001000 H 120 78 01111000 x

73 49 01001001 I 121 79 01111001 y

74 4a 01001010 J 122 7a 01111010 z

75 4b 01001011 K 123 7b 01111011 {

76 4c 01001100 L 124 7c 01111100 |

77 4d 01001101 M 125 7d 01111101 }

78 4e 01001110 N 126 7e 01111110 ~

79 4f 01001111 O 127 7f 01111111 DELETE

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

Page 19: C NOTES IITKGP

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

L (or l) long

LL (or ll) long long

Page 20: C NOTES IITKGP

U (or u) unsigned

UL (or ul) 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' '@' ' '

Page 21: C NOTES IITKGP

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.)

Page 22: C NOTES IITKGP

• 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 sweet-sounding-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.

Page 23: C NOTES IITKGP

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;

Page 24: C NOTES IITKGP

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 PI 3.1415926535 #define PERFECT1 6 #define PERFECT2 28 #define 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.

Page 25: C NOTES IITKGP

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.

Typecasting command 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 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

Page 26: C NOTES IITKGP

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

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

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

Divide by 2 0 0

Divide by 2 0 0

57 = (00111001)2

Page 27: C NOTES IITKGP

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.

Page 28: C NOTES IITKGP

Signed integer 0 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 1 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-1bt-

2...b0)2= 2t-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|.

Page 29: C NOTES IITKGP

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-1bt-

2...b0)2 with each bi = 1 - ai, and therefore the 2's complement of n is n' = 1+(bt-1bt-

2...b0)2 = 1+(2t-1)-n = 2t-n. In order that n' fits in t-bits we then require 0<=n'<=2t-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:

Page 30: C NOTES IITKGP

Decimal Signed

magnitude

1's

complement

2's

complement

+127 01111111 01111111 01111111

+126 01111110 01111110 01111110

+125 01111101 01111101 01111101

... ... ... ...

+3 00000011 00000011 00000011

+2 00000010 00000010 00000010

+1 00000001 00000001 00000001

0 00000000

or 10000000

00000000 or

11111111 00000000

-1 10000001 11111110 11111111

-2 10000010 11111101 11111110

-3 10000011 11111100 11111101

... ... ... ...

-126 01111110 10000001 10000010

-127 01111111 10000000 10000001

-128 No rep No rep 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

Page 31: C NOTES IITKGP

n Remainder

413657

Divide by 16 25853 9

Divide by 16 1615 13

Divide by 16 100 15

Divide by 16 6 4

Divide by 16 0 6

413657 = 0x64fd9

Example: Octal representation

n Remainder

413657

Divide by 8 51707 1

Divide by 8 6463 3

Divide by 8 807 7

Divide by 8 100 7

Divide by 8 12 4

Divide by 8 1 4

Divide by 8 0 1

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:

Page 32: C NOTES IITKGP

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.

Integral

part

Remain

der

172

Divide by 2

86 0

Divide by 2

43 0

Divide by 2

21 1

Divide by 2

10 1

Divide by 2

5 0

Divide by 2

2 1

Divide by 2

1 0

Divide by 2

0 1

172 = (10101100)2

Fractional

part

Integral

part

0.93

Multiply by 2

0.86 1

Multiply by 2

0.72 1

Multiply by 2

0.44 1

Multiply by 2

0.88 0

Multiply by 2

0.76 1

Multiply by 2

0.52 1

Multiply by 2

0.04 1

Multiply by 2

0.08 0

Multiply by 2

0.16 0

Multiply by 2

0.32 0

Page 33: C NOTES IITKGP

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 30 29 ... 24 23 22 21 ... 1 0

S E7 E6 ... E1 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[(E

7E6...E

1E0)2-127]

The biggest real number that this representation stores corresponds to

0 11111110 1111111 11111111 11111111

Page 34: C NOTES IITKGP

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 1.00000000000000000000000 x 2-126 = 2-126,

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

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

0 00000000 0000000 00000000 00000001

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 Interpretation

0 1111 1111 0000000 00000000 00000000 +Inf

1 1111 1111 0000000 00000000 00000000 -Inf

0 1111 1111 Any nonzero 23-bit value NaN

1 1111 1111 Any nonzero 23-bit value NaN

Page 35: C NOTES IITKGP

0 0000 0000 0000000 00000000 00000000 +0

1 0000 0000 0000000 00000000 00000000 -0

0 0111 1111 0000000 00000000 00000000 +1.0

1 0111 1111 0000000 00000000 00000000 -1.0

0 1000 0000 0000000 00000000 00000000 +2.0

1 1000 0000 0000000 00000000 00000000 -2.0

0 1000 0000 1000000 00000000 00000000 +3.0

1 1000 0000 1000000 00000000 00000000 -3.0

0 1111 1110 1111111 11111111 11111111 2255 - 2231

0 0000 0001 0000000 00000000 00000000 2-126

0 0000 0000 1111111 11111111 11111111 2-126 - 2-149

0 0000 0000 0000000 00000000 00000001 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

Page 36: C NOTES IITKGP

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.

Page 37: C NOTES IITKGP

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.

Page 38: C NOTES IITKGP

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

Page 39: C NOTES IITKGP

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-by-

one 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; /* Two integer variables are declared and

initialized */

x = y + 5; /* The value 15 of y is fetched and added to 5.

The sum 20 is stored in the memory location for x. */

y = x; /* The value stored in x, i.e., 20 is fetched and stored

back in y. */

Page 40: C NOTES IITKGP

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; /* Two integer variables are declared and

initialized */

y = x; /* The value stored in x, i.e., 43 is fetched and stored

back in y. */

x = y + 5; /* 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 (pre-

assignment) 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]; /* Declare float variables and

arrays */

char d, e[4]; /* Declare character variables

and arrays */

Page 41: C NOTES IITKGP

c[0] = a + b; /* c[0] is assigned 2.3456 + 6.5432, i.e.,

8.8888 */

c[1] = a - c[0]; /* c[1] is assigned 2.3456 - 8.8888, i.e., -

6.5432 */

c[2] = b - c[0]; /* c[2] is assigned 6.5432 - 8.8888, i.e., -

2.3456 */

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

-8.8888 */

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.

Page 42: C NOTES IITKGP

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 /* constant */

-3.21 /* constant */

'a' /* constant */

x /* variable */

-x[0] /* unary negation on a variable */

x + 5 /* addition of two subexpressions */

(x + 5) /* parenthesized expression */

(x) + (((5))) /* another parenthesized expression */

y[78] / (x + 5) /* more complex expression */

y[78] / x + 5 /* another complex expression */

y / (x = 5) /* expression involving assignment */

Page 43: C NOTES IITKGP

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 Description

- unary

negation

Applicable for integers and real numbers. Does not make

enough sense for unsigned operands.

+ (binary)

addition Applicable for integers and real numbers.

- (binary)

subtraction Applicable for integers and real numbers.

* (binary)

multiplication Applicable for integers and real numbers.

/ (binary)

division

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.

% (binary)

remainder Applicable only for integer operands.

Examples: Here are examples of integer arithmetic:

55 + 21 evaluates to 76.

55 - 21 evaluates to 34.

55 * 21 evaluates to 1155.

55 / 21 evaluates to 2.

55 % 21 evaluates to 13.

Here are some examples of floating point arithmetic:

Page 44: C NOTES IITKGP

55.0 + 21.0 evaluates to 76.0.

55.0 - 21.0 evaluates to 34.0.

55.0 * 21.0 evaluates to 1155.0.

55.0 / 21.0 evaluates to 2.6190476 (approximately).

55.0 % 21.0 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 Example

& AND

a = 237 1 1 1 0 1 1 0 1

b = 174 1 0 1 0 1 1 1 0

a & b is 172 1 0 1 0 1 1 0 0

| OR

a = 237 1 1 1 0 1 1 0 1

b = 174 1 0 1 0 1 1 1 0

a | b is 239 1 1 1 0 1 1 1 1

^ EXOR

a = 237 1 1 1 0 1 1 0 1

b = 174 1 0 1 0 1 1 1 0

a ^ b is 67 0 1 0 0 0 0 1 1

~ Complement a = 237 1 1 1 0 1 1 0 1

~a is 18 0 0 0 1 0 0 1 0

>> Right-shift a = 237 1 1 1 0 1 1 0 1

a >> 2 is 59 0 0 1 1 1 0 1 1

<< Left-shift b = 174 1 0 1 0 1 1 1 0

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

Some shorthand notations

Page 45: C NOTES IITKGP

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 + 10.43; is equivalent to a += 10.43;

a = a % 43; is equivalent to a %= 43;

c = c * (a + b - c); is equivalent to c *= a + b - c;

a = a >> 3; is equivalent to a >>= 3;

b = b ^ (a << 3); is equivalent to b ^= (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 pre-

decrement 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.

Page 46: C NOTES IITKGP

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.

Page 47: C NOTES IITKGP

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 right

* / % binary left

+ - binary left

<< >> binary left

& binary left

| ^ binary left

= += -= *= etc. binary right

Course home

Page 48: C NOTES IITKGP

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 Read an integer in decimal.

%o Read an integer in octal.

Page 49: C NOTES IITKGP

%x,%X Read an integer in hexadecimal.

%i

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.

%u Read an unsigned integer in decimal.

%hd,%hi,%ho,%hu,%hx,%hX Read a short integer.

%ld,%li,%lo,%lu,%lx,%lX Read a long integer.

%Ld,%Li,%Lo,%Lu,%Lx,%LX

Read a long long integer. This is not an ANSI C feature,

but works well in Linux. Replacing L by ll (i.e., using

%lld, %lli, etc.) continues to work in Linux and may be

better ported to other architectures. Some compilers also

support %q (quad).

%f Read a float.

%e Read a float in the scientific (exponential) notation.

%lf,%le Read a double.

%Lf,%Le Read a long double.

%c Read a single character.

%s Read a string of characters.

Example

int a;

unsigned long b;

float x, y;

char c, s[64];

scanf("%d",&a); /* Read the integer a in decimal */

scanf("%x",&b); /* Read the integer b in hexadecimal */

scanf("%f",&x); /* Read the floating point number x in decimal

notation */

scanf("%e",&y); /* Read the floating point number y in the

scientific notation */

scanf(" %c",&c); /* Read the character c */

scanf(" %s",s); /* Read a string and store in s */

/* For reading strings the ampersand (&) is not

needed */

Suppose that the user enters the following values:

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

Page 50: C NOTES IITKGP

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:

Page 51: C NOTES IITKGP

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 Print an integer in decimal.

%o Print an integer in octal.

%x Print an integer in hexadecimal. Use the digits

0,1,...,9,a,b,c,d,e,f.

%X Same as %x except that the digits 0,1,...,9,A,B,C,D,E,F

are used.

%u Print an unsigned integer in decimal.

%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 Print a long long integer. (Not in ANSI C. ll may be

used in place of L. Some compilers support %q.)

%f Print a float in decimal.

%e Print a float in the scientific (exponential) notation.

%E Same as %e except that E is used to denote the exponent.

%a Print a float in hexadecimal. Digits

0,1,...,9,a,b,c,d,e,f are used and the exponent

indicator is p.

%A Same as %a except that A,B,C,D,E,F and P are used.

%lf,%le,%lE,%la,%lA Print a double.

%Lf,%Le,%LE,%La,%LA Print a long double.

%c Print a single character.

Page 52: C NOTES IITKGP

%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 = %d = 0x%x\n", a, a);

printf("b = %d = 0x%x\n", b, b);

printf("x = %f = %e\n", x, x);

printf("y = %f = %e\n", y, y);

printf("c = '%c' = %d\n", c, c);

printf("s = %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 = 123 = 0x7b

b = 291 = 0x123

x = -123.456001 = -1.234560e+02

y = 0.000001 = 1.230000e-06

c = 'a' = 97

s = 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;

Page 53: C NOTES IITKGP

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 new-

line 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; /* Declare FILE pointers */

ifp = fopen("foo.in","r"); /* Open the file "foo.in" in read

mode */

ofp1 = fopen("bar1.out","w"); /* Open the file "bar1.out" in write

mode */

ofp2 = fopen("bar2.out","a"); /* Open the file "bar2.out" in append

mode */

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

Page 54: C NOTES IITKGP

"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

Page 55: C NOTES IITKGP

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;

Page 56: C NOTES IITKGP

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

Page 57: C NOTES IITKGP

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 Description

%nd,%ni,

%nu,%nld,

%nLd, etc.

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.

%-nd,%-ni,

%-nu, etc.

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.

%no,%-no Same as %nd and %-nd, except that the integer is printed in octal.

%nx,%-nx,

%nX,%-nX Same as %nd and %-nd, except that the integer is printed in

hexadecimal.

%n.mf,%n.mlf,

%n.mLf

Print a right-justified real number (in the decimal notation) with a total

of n characters (including the decimal pointer and the sign) and with m

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.

%ns 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.

%-ns This is the same as %ns except that the output is left-justified, i.e., extra

spaces, if any, are printed after the string.

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 }

Page 58: C NOTES IITKGP

{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

+--------------------------------------------------------------------

-----+

Page 59: C NOTES IITKGP

| 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

Page 60: C NOTES IITKGP

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:

0 if x = 0,

y = x if x > 0,

-x 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:

0 if n = 0,

Fn = 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.

Page 61: C NOTES IITKGP

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 False

x != y True

y % x == 10 True

600 < x * y False

600 <= x * y True

'B' > 'A' True

x / 0.3 == 50 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:

Page 62: C NOTES IITKGP

0 False

1 True

6 - 2 * 3 False

(6 - 2) * 3 True

0.0075 True

0e10 False

'A' True

'\0' False

x = 0 False

x = 1 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 True if and only if

AND C1 && C2 Both C1 and C2 are true

OR C1 || C2 Either C1 or C2 or both are true

NOT !C C is false

Example:

(7*7 < 50) && (50 < 8*8) True

(7*7 < 50) && (8*8 < 50) False

(7*7 < 50) || (8*8 < 50) True

!(8*8 < 50) True

('A' > 'B') || ('a' > 'b') False

('A' > 'B') || ('A' < 'B') True

('A' < 'B') && !('a' > 'b') True

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

< <= > >= Binary Left

Page 63: C NOTES IITKGP

== != Binary Left

&& Binary Left

|| Binary 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:

Page 64: C NOTES IITKGP

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);

Page 65: C NOTES IITKGP

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:

Page 66: C NOTES IITKGP

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.

Page 67: C NOTES IITKGP

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

Page 68: C NOTES IITKGP

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:

Page 69: C NOTES IITKGP

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

Page 70: C NOTES IITKGP

}

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' : printf("Thanks\n"); break;

case 'F' : printf("Merci\n"); break;

case 'G' : printf("Danke\n"); break;

case 'H' : printf("Shukriya\n"); break;

case 'I' : printf("Grazie\n"); break;

case 'J' : printf("Arigato\n"); break;

case 'K' : printf("Dhanyabaadagaru\n"); break;

default : 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

Page 71: C NOTES IITKGP

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 : sum += 1;

case 2 : sum += 2;

case 3 : sum += 3;

case 4 : sum += 4;

case 5 : sum += 5;

case 6 : sum += 6;

case 7 : sum += 7;

case 8 : sum += 8;

case 9 : sum += 9;

case 10 : sum += 10;

break;

default : printf("n = %d is not in the desired range...\n", n);

}

Course home

Page 72: C NOTES IITKGP

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 Set Description

N {1,2,3,4,...} The set of natural numbers (i.e., positive

integers)

N0 {0,1,2,3,...} The set of non-negative integers

Z {...,-3,-2,-

1,0,1,2,3,...} The set of integers

Page 73: C NOTES IITKGP

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).

Page 74: C NOTES IITKGP

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:

Page 75: C NOTES IITKGP

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

Page 76: C NOTES IITKGP

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

Page 77: C NOTES IITKGP

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:

Page 78: C NOTES IITKGP

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

Page 79: C NOTES IITKGP

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;

Page 80: C NOTES IITKGP

}

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+2

2+...+n

2.

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.

Page 81: C NOTES IITKGP

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

Page 82: C NOTES IITKGP

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; /* Incremet i */

H += 1.0/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.

Page 83: C NOTES IITKGP

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; /* Previous-to-previous values */

r1 = b; u1 = 0; v1 = 1; /* Previous values */

/* Extended gcd loop */

while (r1 > 0) {

/* Compute values for the current iteration */

q = r2 / r1; /* Compute the next quotient */

r = r2 - q * r1; /* Compute the next remainder */

u = u2 - q * u1; /* Identically compute the next u value */

v = v2 - q * v1; /* Identically 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), (1)

u2 * a + v2 * b = r2, (2)

u1 * a + v1 * b = r1. (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

Page 84: C NOTES IITKGP

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 u v u2*a+v2*b

Before loop 78 21 1 0 0 1 - - - - 78

1 78

21

21

15

1

0

0

1

0

1

1

-3

3

3

15

15

1

1

-3

-3

78

21

2 21

15

15

6

0

1

1

-1

1

-3

3

4

1

1

6

6

-1

-1

4

4

21

15

3 15

6

6

3

1

-1

-1

3

-3

4

4

-11

2

2

3

3

3

3

-11

-11

15

6

4 6

3

3

0

-1

3

3

-7

4

-11

-11

26

2

2

0

0

-7

-7

26

26

6

3

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].

Page 85: C NOTES IITKGP

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) { /* Attempt to bubble till the value of i

*/

for (j=0; j<=i; ++j) { /* Run j from 0 to the current upper

bound i */

if (A[j] > A[j+1]) { /* Two consecutive elements are in the

opposite order */

/* Swap A[j] and A[j+1] */

t = A[j]; /* Store A[j] in a temporary variable t

*/

A[j] = A[j+1]; /* Change A[j] to A[j+1] */

A[j+1] = t; /* Change A[j+1] to the old value of

A[j] stored in t */

}

}

}

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];

Page 86: C NOTES IITKGP

}

}

/* 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.

Page 87: C NOTES IITKGP

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 do-

while 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;

Page 88: C NOTES IITKGP

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 ()

{

Page 89: C NOTES IITKGP

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

Page 90: C NOTES IITKGP

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 if n = 0, f(n) = 2m - 1 if n = m > 0, 2m 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:

g(n) =

0 if n = 0, 3m - 2 if n = 2m - 1 for some m > 0, 3m - 1 if n = 2m for some m > 0, 3m if n = -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;

Page 91: C NOTES IITKGP

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

Page 92: C NOTES IITKGP

}

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

Page 93: C NOTES IITKGP

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

Page 94: C NOTES IITKGP

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 | +----+ 22 | 53 | 44 +--- 243 | 900 | 729 +----- 2466 | 17100 | 14796 +------- 24729 | 230400

Page 95: C NOTES IITKGP

| 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, /* Number of digits to the right of the decimal point */ grp[8], /* 2-digit groups */ sgn, /* Sign of n */ i; /* An index */ 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(".");

Page 96: C NOTES IITKGP

/* 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 built-

in 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.

Page 97: C NOTES IITKGP

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

Page 98: C NOTES IITKGP

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 -c -o foo1.o foo1.c cc -c -o foo2.o foo2.c cc -c -o bar1.o bar1.c cc -c -o bar2.o bar2.c cc -c -o bar3.o 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

Page 99: C NOTES IITKGP

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 foorams[8] = { 0xf00ba000, 0xf002ba00, 0xf0046ba0, 0xf008acba, 0xba0f0000, 0xba01f000, 0xba035f00, 0xba079bf0 }; ... /* Function prototypes. */ /* These functions are external to the user's programs. */

Page 100: C NOTES IITKGP

/* 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 <foobar/foo1.h> #include <foobar/foo2.h> #include <foobar/bar1.h> #include <foobar/bar2.h> #include <foobar/bar3.h>

or #include "/tmp/foobar/include/foo1.h" #include "/tmp/foobar/include/foo2.h" #include "/tmp/foobar/include/bar1.h" #include "/tmp/foobar/include/bar2.h" #include "/tmp/foobar/include/bar3.h"

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?

Page 101: C NOTES IITKGP

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 asin (double x); double acos (double x); double atan (double x); double 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);

Page 102: C NOTES IITKGP

The standard hyperbolic trigonometric functions.

In addition to the above functions, math.h also defines the following useful constants:

#define M_E 2.7182818284590452354 /* e */ #define M_LOG2E 1.4426950408889634074 /* log_2 e */ #define M_LOG10E 0.43429448190325182765 /* log_10 e */ #define M_LN2 0.69314718055994530942 /* log_e 2 */ #define M_LN10 2.30258509299404568402 /* log_e 10 */ #define M_PI 3.14159265358979323846 /* pi */ #define M_PI_2 1.57079632679489661923 /* pi/2 */ #define M_PI_4 0.78539816339744830962 /* pi/4 */ #define M_1_PI 0.31830988618379067154 /* 1/pi */ #define M_2_PI 0.63661977236758134308 /* 2/pi */ #define M_2_SQRTPI 1.12837916709551257390 /* 2/sqrt(pi) */ #define M_SQRT2 1.41421356237309504880 /* sqrt(2) */ #define M_SQRT1_2 0.70710678118654752440 /* 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, form-

feed, 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).

Page 103: C NOTES IITKGP

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);

Page 104: C NOTES IITKGP

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.

Page 105: C NOTES IITKGP

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 if n = 0, F(n) = 1 if n = 1, F(n-1) + F(n-2) 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)); }

Page 106: C NOTES IITKGP

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); /* Error condition */ if (n == 0) return (1); /* Basis case */ return(n * factorial(n-1)); /* 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

Page 107: C NOTES IITKGP

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; /* Base case: an array of size 1 is sorted */ mid = (i + j) / 2; /* Compute the mid index */ mergeSort(i,mid); /* Recursively sort the left half */ mergeSort(mid+1,j); /* Recursively sort the right half */ merge(i,mid,j); /* 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];

Page 108: C NOTES IITKGP

++k2; } else if (k2 > j2) { /* Right half is exhausted */ /* Copy from the left half */ tmpArray[k] = A[k1]; ++k1; } else if (A[k1] < A[k2]) { /* Left pointer points to a smaller value */ /* Copy from the left half */ tmpArray[k] = A[k1]; ++k1; } else { /* Right pointer points to a smaller value */ /* Copy from the right half */ tmpArray[k] = A[k2]; ++k2; } ++k; /* 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 size : 20 Array before sorting : 21 74 40 94 78 75 58 91 11 7 86 77 76 20 45 56 94 32 90 51 Array after sorting : 7 11 20 21 32 40 45 51 56 58 74 75 76 77 78 86 90 91 94 94

Page 109: C NOTES IITKGP

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;

Page 110: C NOTES IITKGP

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); /* Recursive call on the left part */ if (rsize > 0) quickSort(j-rsize+1,j); /* Recursive call on the 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); }

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.

Page 111: C NOTES IITKGP

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 if n = 0 or 1, Sn-1 + Sn-2 + 1 if n >= 2.

Define the sequence

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

It follows that

Tn = 1 if n = 0 or 1, Tn-1 + Tn-2 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 )

Page 112: C NOTES IITKGP

{ 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

Page 113: C NOTES IITKGP

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

Page 114: C NOTES IITKGP

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.

Page 115: C NOTES IITKGP

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

0

1

1

1

2

1

3

1

4

1

5

1

6

1

7

1

8

1

9

2

0

2

1

2

2

2

3

2

4

2

5

2

6

2

7

2

8

2

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 \

Page 116: C NOTES IITKGP

0 0

0 1 2 3 4 5 6 7 8 9

1

0

1

1

1

2

1

3

1

4

1

5

1

6

1

7

1

8

1

9

2

0

2

1

2

2

2

3

2

4

2

5

2

6

2

7

2

8

2

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 run-

time 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

Page 117: C NOTES IITKGP

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; /* Let p point to the 0-th location of the array A */ p = &A[0]; /* Let p point to the 0-th location of the array A */ p = &A[1]; /* Let p point to the 1-st location of the array A */ p = &A[i]; /* Let p point to the i-th location of the array A */

Page 118: C NOTES IITKGP

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; /* Let p point to the 0-th location of the array A */ p++; /* Now p points to the 1-st location of A */ p = p + 6; /* Now p points to the 7-th location of A */ p += 2; /* Now p points to the 9-th location of A */ --p; /* Now p points to the 8-th location of A */ p -= 5; /* Now p points to the 3-rd location of A */ p -= 5; /* Now p points to the (-2)-nd location of 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 */ }

Page 119: C NOTES IITKGP

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.

Page 120: C NOTES IITKGP

• 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 1 1 1 1 2 3 4 5 6 4 9 16 25 36 8 27 64 125 216

We can write the entries in the row-major order and represent the resulting flattened data

as a one-dimensional array:

Page 121: C NOTES IITKGP

1 1 1 1 1 2 3 4 5 6 4 9 16 25 36 8 27 64 125 216

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 one-

dimensional memory.

2-dimensional arrays can be defined like one-dimensional arrays, but with two square-

bracketed 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, 1, 1, 1 }, /* The zeroth row */ { 2, 3, 4, 5, 6 }, /* The first row */ { 4, 9, 16, 15, 25 }, /* The second row */ { 8, 27, 64, 125, 216 } /* The 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",

Page 122: C NOTES IITKGP

"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.

Page 123: C NOTES IITKGP

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]; /* A is a statically allocated array */ int (*B)[MAXCOL]; /* B is a pointer to an array of MAXCOL integers */ int *C[MAXROW]; /* C is an array of MAXROW int pointers */ int **D; /* D is a pointer to an int pointer */

Page 124: C NOTES IITKGP

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

Page 125: C NOTES IITKGP

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

allocation.

Course home

Page 126: C NOTES IITKGP

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.

Page 127: C NOTES IITKGP

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:

Page 128: C NOTES IITKGP

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;

Page 129: C NOTES IITKGP

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-

Page 130: C NOTES IITKGP

>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);

Page 131: C NOTES IITKGP

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; };

Page 132: C NOTES IITKGP

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.

Page 133: C NOTES IITKGP

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.

Page 134: C NOTES IITKGP

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.

Page 135: C NOTES IITKGP

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 16-

character 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];

Page 136: C NOTES IITKGP

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

Page 137: C NOTES IITKGP

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

Page 138: C NOTES IITKGP

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:

Page 139: C NOTES IITKGP

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.

Page 140: C NOTES IITKGP

• 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 ice-

creams. But my job is to teach you programming, not how to manufacture tasty ice-

creams.

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

Page 141: C NOTES IITKGP

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)); /* Create the first node */ head->data = 3; /* Set data for the first node */ 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 */

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));

Page 142: C NOTES IITKGP

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.

Page 143: C NOTES IITKGP

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 one-

dimensional 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 A[ROWSIZE][COLSIZE]; int (*B)[COLSIZE]; int *C[ROWSIZE]; int **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:

Page 144: C NOTES IITKGP

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 */

Page 145: C NOTES IITKGP

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 A[ROWSIZE][COLSIZE]; int (*B)[COLSIZE]; int *C[ROWSIZE]; int **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)

Page 146: C NOTES IITKGP

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

Page 147: C NOTES IITKGP

B[0] = 0 : i=0 | 0 4 8 12 16 | B[1] = 20 : i=1 | 20 24 28 32 36 | B[2] = 40 : i=2 | 40 44 48 52 56 | B[3] = 60 : i=3 | 60 64 68 72 76 | +-------------------------------+ Array C sizeof(*C) = 4 j=0 j=1 j=2 j=3 j=4 +-------------------------------+ C[0] = 1004 : i=0 | 1004 1008 1012 1016 1020 | C[1] = 1028 : i=1 | 1028 1032 1036 1040 1044 | C[2] = 1052 : i=2 | 1052 1056 1060 1064 1068 | C[3] = 1076 : i=3 | 1076 1080 1084 1088 1092 | +-------------------------------+ Array D sizeof(*D) = 4 j=0 j=1 j=2 j=3 j=4 +-------------------------------+ D[0] = 24 : i=0 | 24 28 32 36 40 | D[1] = 48 : i=1 | 48 52 56 60 64 | D[2] = 72 : i=2 | 72 76 80 84 88 | D[3] = 96 : i=3 | 96 100 104 108 112 | +-------------------------------+

Course home

Page 148: C NOTES IITKGP

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?

Page 149: C NOTES IITKGP

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).

Page 150: C NOTES IITKGP

• 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.

Page 151: C NOTES IITKGP

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.

Page 152: C NOTES IITKGP

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

Page 153: C NOTES IITKGP

• 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) {

Page 154: C NOTES IITKGP

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

Page 155: C NOTES IITKGP

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;

Page 156: C NOTES IITKGP

} 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 : "); print(L); printf("\n"); L = insert(L,'b',0); printf("Current list is : "); print(L); printf("\n"); L = delete(L,5); printf("Current list is : "); print(L); printf("\n"); L = insert(L,'c',1); printf("Current list is : "); print(L); printf("\n"); L = insert(L,'b',3); printf("Current list is : "); print(L); printf("\n"); L = delete(L,2); printf("Current list is : "); print(L); printf("\n"); L = insert(L,'z',8); printf("Current list is : "); print(L); printf("\n"); L = delete(L,2); printf("Current list is : "); print(L); printf("\n"); printf("Element at position 1 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

Page 157: C NOTES IITKGP

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);

Page 158: C NOTES IITKGP

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;

Page 159: C NOTES IITKGP

} }

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

Page 160: C NOTES IITKGP

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.

Page 161: C NOTES IITKGP

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);

Page 162: C NOTES IITKGP

} 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));

Page 163: C NOTES IITKGP

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 ()

Page 164: C NOTES IITKGP

{ 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)) {

Page 165: C NOTES IITKGP

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 First-

In-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 well-

defined 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);

Page 166: C NOTES IITKGP

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

Page 167: C NOTES IITKGP

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 wrap-

around 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 ) {

Page 168: C NOTES IITKGP

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");

Page 169: C NOTES IITKGP

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;

Page 170: C NOTES IITKGP

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 )

Page 171: C NOTES IITKGP

{ 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

Page 172: C NOTES IITKGP

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

Page 173: C NOTES IITKGP

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 application-

specific. 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

Page 174: C NOTES IITKGP

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 300n

2 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.

Page 175: C NOTES IITKGP

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.

Page 176: C NOTES IITKGP

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.

Page 177: C NOTES IITKGP

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.

Page 178: C NOTES IITKGP

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,

Page 179: C NOTES IITKGP

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;

}

Page 180: C NOTES IITKGP

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:

Page 181: C NOTES IITKGP

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.

Page 182: C NOTES IITKGP

• 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 + (k+1) + k x (2 + 1 + 1) +

1

(Init) (Loop condn) (No of iter) (ops in loop body)

(last comparison)

= 5k + 4.

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) {

Page 183: C NOTES IITKGP

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

Page 184: C NOTES IITKGP

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 Straßen'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];

}

Page 185: C NOTES IITKGP

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

Page 186: C NOTES IITKGP

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

Page 187: C NOTES IITKGP

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); /* Partition with respect to A[0] */

quickSort(A,i); /* Recursively sort the left half

excluding the pivot */

quickSort(&A[i+1],n-i-1); /* 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

Page 188: C NOTES IITKGP

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.

Page 189: C NOTES IITKGP

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

Page 190: C NOTES IITKGP

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

Page 191: C NOTES IITKGP

• 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 (n-

1) 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:

Page 192: C NOTES IITKGP

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

Page 193: C NOTES IITKGP

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 H

3 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-12

t-1 + at-22t-2 + ... + a12

1 + 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.

Page 194: C NOTES IITKGP

7. Let x,y,z be unsigned integers. Find the values of x,y,z after the following

statements are executed. 8. x = 5;

9. z = 12;

10. x *= x;

11. x += z * z;

12. y = x << 1;

13. z = 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.

Page 195: C NOTES IITKGP

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. ax + by = c

22. 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-02-

2004 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. Iteration 1: new entry = 84, max = 84, min = 84

28. Iteration 2: new entry = 87, max = 87, min = 84

29. Iteration 3: new entry = 72, max = 87, min = 72

30. Iteration 4: new entry = 53, max = 87, min = 53

31. Iteration 5: new entry = 93, max = 93, min = 53

32. ...

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).

Page 196: C NOTES IITKGP

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+b

2=c

2. 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 counter-

intuitive 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]

Page 197: C NOTES IITKGP

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. n1 < n2 < ... < nk and

55. a/b = 1/n1 + 1/n2 + ... + 1/nk.

Page 198: C NOTES IITKGP

(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., 2

n = 2005...

(Hint: Take log.)

Course home

Page 199: C NOTES IITKGP

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 H

3 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

Page 200: C NOTES IITKGP

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:

o One or both of the circles is/are undefined.

o The two circles touch (i.e., meet at exactly one point).

o The two circles intersect (at two points).

o 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 + F2

2 + ... + Fn

2 = 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 n-

th harmonic number.

5. [M] Find the flaw in the following proof:

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;

Page 201: C NOTES IITKGP

8. 9. n = 0; 10. x = 1 + rand() % 9; 11. y = 1 + rand() % 9; 12. while (n < 10) { 13. t = 1 + rand() % 9; 14. x *= t; 15. y /= t; 16. ++n; 17. } 18. 19. b. #define NITER 100 20. 21. double x, s, t; 22. int i; 23. 24. i = 0; s = t = 1; 25. do { 26. ++i; 27. t /= (double)i; 28. s += t; 29. } while (i < NITER); 30. 31. c. #define NITER 10 32. int i; 33. double A[NITER] = {1.0/2, 1.0/3, 1.0/5, 1.0/7, 1.0/11, 34. 1.0/13, 1.0/17, 1.0/19, 1.0/23, 1.0/29}; 35. 36. for (i=1; i<NITER; ++i) A[i] += A[i-1];

37. [HM] Consider the following puzzle given in pseudocode: 38. Let n be an odd positive integer. 39. Initialize C to the collection of integers 1,2,...,2n. 40. while (C contains two or more elements) { 41. Randomly choose two elements from the collection, call

them a and b. 42. Remove these elements from C. 43. Add the absolute difference |a-b| to C. 44. }

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. unsigned int foo1 ( unsigned int n ) 47. { 48. unsigned int t = 0; 49. 50. while (n > 0) { 51. if (n % 2 == 1) ++t; 52. n = n / 2; 53. } 54. return t; 55. }

Page 202: C NOTES IITKGP

56. 57. b. unsigned int foo2 ( unsigned int n ) 58. { 59. unsigned int t = 0; 60. 61. while (n > 0) { 62. if (n & 1) ++t; 63. n >>= 1; 64. } 65. return t; 66. } 67. 68. c. double foo3 ( double a , unsigned int n ) 69. { 70. double s, t; 71. 72. s = 0; 73. t = 1; 74. while (n > 0) { 75. s += t; 76. t *= a; 77. --n; 78. } 79. return s; 80. } 81. 82. d. double foo4 ( float A[] , int n ) 83. { 84. float s, t; 85. 86. s = t = 0; 87. for (i=0; i<n; ++i) { 88. s += A[i]; 89. t += A[i] * A[i]; 90. } 91. return (t/n)-(s/n)*(s/n); 92. } 93. 94. e. int foo5 ( unsigned int n ) 95. { 96. if (n == 0) return 0; 97. return 3*n*(n-1) + foo5(n-1) + 1; 98. } 99. 100. f. int foo6 ( char A[] , unsigned int n ) 101. { 102. int t; 103. 104. if (n == 0) return 0; 105. t = foo6(A,n-1); 106. if ( ((A[n-1]>='a') && (A[n-1]<='z')) || 107. ((A[n-1]>='A') && (A[n-1]<='Z')) || 108. ((A[n-1]>='0') && (A[n-1]<='9')) ) 109. ++t; 110. return t; 111. } 112.

Page 203: C NOTES IITKGP

113. g. int foo7 ( unsigned int a , unsigned int b ) 114. { 115. if ((a == 0) || (b == 0)) return 0; 116. return a * b / bar7(a,b); 117. } 118. 119. int bar7 ( unsigned int a , unsigned int b ) 120. { 121. if (b == 0) return a; 122. return bar7(b,a%b); 123. } 124. 125. h. int foo8 ( unsigned int n ) 126. [ 127. if (n == 0) return 0; 128. if (n & 1) return -1; 129. return 1 + bar8(n-1); 130. } 131. 132. int bar8 ( int n ) 133. { 134. if (!(n & 1)) return -2; 135. return 2 + foo8(n-1); 136. }

137. [HM] Prove that the following function correctly computes the number of

trailing 0's in the decimal representation of n! (factorial n). 138. int bar ( unsigned int n ) 139. { 140. int t = 0; 141. 142. while (n > 0) { 143. n /= 5; 144. t += n; 145. } 146. return t; 147. }

148. For k in N we have 149. a2k = (ak)2, and 150. 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,

Page 204: C NOTES IITKGP

2 Gn-1 + Gn-2 + Gn-3

if n = 2, if n >= 3.

. 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.

154. Consider the sequence of integers given by: 155. a1 = 1, 156. a2 = 1, 157. 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. Consider three sequences of integers defined recursively as follows: 159. a0 = 0 160. a1 = 1 161. an = a[n/3] - 2bn-2 + cn for n >= 2 162. 163. b0 = -1 164. b1 = 0 165. b2 = 1 166. bn = n - an-1 + cn-2 - bn-3 for n >= 3 167. 168. c0 = 1 169. 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.

Page 205: C NOTES IITKGP

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. a0 = 1. 172. an = an-1 + bn for n >= 1. 173. 174. b0 = 2. 175. bn = bn-1 + cn for n >= 1. 176. 177. c0 = -3. 178. 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. a0 x (a1 x (a2 x a3)) 182. a0 x ((a1 x a2) x a3) 183. (a0 x a1) x (a2 x a3) 184. (a0 x (a1 x a2)) x a3 185. ((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. C0 = 1, b. C1 = 1, and c. Cn = C0Cn-1 + C1Cn-2 + ... + Cn-2C1 + Cn-1C0 for n>=2.

(Hint: Classify a multiplication sequence based on the last multiplication.)

Page 206: C NOTES IITKGP

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.

186. [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 : 1 2 3 4 5 6 7 8 9 10 p(i) : 5 3 6 4 10 7 9 1 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.)

Page 207: C NOTES IITKGP

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

Page 208: C NOTES IITKGP

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 2 3 4 5

6 7 8 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

Page 209: C NOTES IITKGP

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. s(0,0) = 1, 197. s(n,0) = 0 for n > 0, 198. s(n,k) = 0 for k > n, 199. s(n,k) = (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. S(0,0) = 1, 202. S(n,0) = 0 for n > 0, 203. S(n,k) = 0 for k > n, 204. S(n,k) = 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 Runs Number of runs Number of descents 123 123 1 0 132 13,2 2 1 213 2,13 2 1 231 23,1 2 1 312 3,12 2 1 321 3,2,1 3 2

Page 210: C NOTES IITKGP

<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

Page 211: C NOTES IITKGP

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 H

3 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).

Page 212: C NOTES IITKGP

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 - 2b

2 = 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 + 2b

2 = 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 - b

2 = 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.

Page 213: C NOTES IITKGP

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 + cw

2 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.

Page 214: C NOTES IITKGP

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. Node Children c. a b,c,d d. b e,-,f e. c -,-,g f. d -,-,- g. e -,h,- h. f -,-,- i. g -,-,- j. 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 */

Page 215: C NOTES IITKGP

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. typedef struct { 28. char *data; 29. int allocSize; 30. } semiCompactString;

Rewrite the functions of the previous exercise for semiCompactString's.

Page 216: C NOTES IITKGP

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 million-

by-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.

Page 217: C NOTES IITKGP

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

Page 218: C NOTES IITKGP

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

Page 219: C NOTES IITKGP

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 H

3 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. S = init();

9. /* Initialize S to the empty set */

10.

11. isEmpty(S);

12. /* Return true if and only if S is the empty set */

13.

14. isSingleton(S);

Page 220: C NOTES IITKGP

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.

Page 221: C NOTES IITKGP

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. The empty tuple () is a nested list.

87. If A0,A1,...,An-1 are nested lists or integers for some n >= 1,

88. 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),()))

Page 222: C NOTES IITKGP

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,...,Vn-

1,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 sub-

list (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,...,Xn

en, with each ei being a non-negative integer and with the

Page 223: C NOTES IITKGP

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,...,Xn

en 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. S = init();

95. S = push(S,1);

96. S = push(S,2);

97. print top(S);

98. S = pop(S);

Page 224: C NOTES IITKGP

99. S = push(S,3);

100. S = push(S,4);

101. print top(S);

102. S = pop(S);

103. print top(S);

104. S = pop(S);

105. S = push(S,5);

106. print top(S);

107. S = pop(S);

108. print top(S);

109. 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: (), ((())), ()()(), ()(()(()(()()))). Non-

examples: ((), (()(()))), )()(.

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.

Page 225: C NOTES IITKGP

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.

Page 226: C NOTES IITKGP

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

Page 227: C NOTES IITKGP

c. Linked lists

d. Doubly linked lists

Course home

Page 228: C NOTES IITKGP

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 H

3 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: 2. (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) = adn

d + ad-1nd-1 + ... + a1n + a0

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. int F ( int n , int *Fprev ) 10. { 11. int Fn_1, Fn_2; 12. 13. if (n == 0) { 14. *Fprev = 1; 15. return (0);

Page 229: C NOTES IITKGP

16. } 17. if (n == 1) { 18. *Fprev = 0; 19. return (1); 20. } 21. Fn_1 = F(n-1,&Fn_2); 22. *Fprev = Fn_1; 23. return (Fn_1+Fn_2); 24. }

25. The following function recursively determines whether a given string is a

palindrome. Determine its time complexity. 26. int isPalindrome ( char A[] , int n ) 27. { 28. if (n <= 1) return 1; 29. if (A[0] != A[n-1]) return 0; 30. return isPalindrome(&A[1],n-2); 31. }

32. Determine the time complexity of the following iterative function: 33. int f ( int A[SIZE][SIZE] , int n ) 34. { 35. int i, j, sum = 0; 36. 37. for (i=0; i<n; ++i) { 38. if (i % 2 == 0) 39. for (j=0; j<=i; j=j+1) sum = sum + A[i][j]; 40. else 41. for (j=n-1; j>=i; j=j-1) sum = sum - A[i][j]; 42. } 43. }

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. T(1) = 1, c. T(n) = 2T(n-1) + 1 for n >= 2.

d. Prove that T(n) = 2n - 1 for all n >= 1.

Page 230: C NOTES IITKGP

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.

Page 231: C NOTES IITKGP

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