Chapter 3: Implementation of Algorithms to Solve Problems
Think of looking up a name in a printed telephone directory. Nobody starts at page one. You open somewhere near the middle, see whether the name you want falls before or after that page, and throw away the half that cannot contain it. Repeat, and a directory of a hundred thousand names surrenders in about seventeen looks. The method is so natural that most people invent it without being taught.
That method is an algorithm — a method of solution set out as a finite sequence of exact steps. It is also one of the most instructive failures in the history of computing. Donald Knuth, who traced its published history, records that the idea appeared in print as early as 1946, and yet well over a decade passed before a published version was correct for arrays of every size. The idea was obvious. The details were not. Programmers who understood the method perfectly still lost a name that sat at the very first position, or ran forever on a directory with a single entry, or slipped one place past the end of the list.
The lesson is the one this chapter is built on. Knowing roughly what to do is not the same as knowing exactly what to do, in what order, and for every possible input. Getting from the first to the second is the whole craft of algorithm design.
Diagram
Fig. 3.0-1 – a sorted list of eleven boxed names drawn as a horizontal strip; the middle box shaded and labelled “look here first”; a curly brace under the left half labelled “discarded in one step”; below it, the same strip redrawn with only the right half remaining and its own middle box shaded, showing the second look; caption strip on the right reading “each look halves what is left”
Each of these questions is answered somewhere in this chapter. Watch for them as you read.
3.1 Algorithms and Algorithmic Problem Solving
Every program you have written so far began, whether you noticed it or not, with a decision about method. Before a single line of Java was typed, you had already settled what the program would be given, what it had to produce, and roughly how it would get from one to the other. The typing came afterwards.
This chapter takes that hidden first stage and brings it into the open. The habit it builds is simple to state and slow to acquire: work out the method completely, in ordinary language, before you commit it to a programming language. A method written out this way is what we have been calling an algorithm.
The reason for the discipline is practical. A mistake in method that survives into code is expensive to find, because the compiler will not help you — a program can compile perfectly, run without complaint, and be wrong. A mistake in method caught on paper costs a pencil stroke.
3.1.1 What an Algorithm Is
Consider a task from a school office: given the marks a student has scored in five subjects, produce the total and the average. Written out as a method, it looks like this.
1. Read the five marks.
2. Add them to get the total.
3. Divide the total by 5 to get the average.
4. Display the total and the average.
5. Stop.
Four things are worth noticing about those steps, because they are the four things that make the difference between a method and a vague intention. The steps are finite in number. Each one is exact — there is no step that says “handle the marks somehow”. The steps are in an order, and the order matters, since you cannot divide a total you have not computed. And the method ends.
Notice also what the steps do not say. They do not say whether the marks are read from a keyboard, a file or a printed register. They do not say whether the average is stored in a variable called avg or average, or whether the language used is Java or any other. An algorithm fixes the method, not the machinery.
Read that definition slowly, because every phrase in it is doing work. Finite sequence rules out a method that goes on for ever. Well-defined rules out a step that two careful readers could carry out differently. Any valid input rules out a method that happens to work on the one example you tried. And terminates rules out a method that is still running when the examination hall closes.
3.1.2 Where the Word Came From
The word is not an English one, and it is not really a word at all — it is a person’s name, worn down by nine centuries of use.
Muhammad ibn Musa al-Khwarizmi was a Persian mathematician and astronomer who worked in Baghdad in the early ninth century. He wrote a book explaining the Hindu–Arabic numerals and the methods of calculating with them — the place-value system with a symbol for zero that the whole world now uses. He wrote a second book on solving linear and quadratic equations, and from a phrase in its title, al-jabr, English gets the word “algebra”.
When European scholars translated his work into Latin in the twelfth century, they rendered his name as Algoritmi. The English scholar Adelard of Bath was among the translators who carried this material westward. For several hundred years afterwards, “algorism” meant simply calculating with the new numerals rather than with counters or Roman numerals. Only much later did the word broaden into its present sense: any finite, exact, step-by-step method for solving a problem.
The history is not decoration. Al-Khwarizmi’s contribution was precisely the modern one — he did not merely solve particular equations, he set down general procedures that anyone could follow to solve any equation of that form, without needing to be clever. That is what an algorithm still is.
3.1.3 Properties of a Well-formed Algorithm
The definition in 3.1.1 packs several requirements into one sentence. It helps to separate them out, because when an algorithm goes wrong it is almost always because one specific requirement has been broken.
Finiteness means the algorithm stops after a finite number of steps, for every valid input. It is not enough that it stops on the input you tested. A method that halves a quantity again and again, waiting for it to reach exactly zero, will halt on some inputs and run for ever on others.
Definiteness means every step has exactly one meaning. A step such as “adjust the value suitably” fails this test; a step such as “subtract 1 from the value” passes it. Definiteness is what makes an algorithm mechanical, and being mechanical is the point — a machine cannot use judgement.
Every algorithm has zero or more input values, supplied to it before or during execution, and at least one output value, which is its whole reason for existing. An algorithm that computes something and displays nothing has done no useful work.
Effectiveness means every step is simple enough to be actually carried out, in finite time, with pencil and paper if necessary. “Divide 22 by 7” is effective. “Write down the exact decimal value of \(\pi\)” is not, because the writing never finishes.
| Property | What it demands | Symptom when violated |
|---|---|---|
| Finiteness | Stops after a finite number of steps, on every valid input | The program hangs; the loop never ends |
| Definiteness | Every step has exactly one possible meaning | Two people implement the same step differently |
| Input | Zero or more values supplied to the algorithm | The method has nothing to work on |
| Output | At least one value produced as a result | The program computes but reports nothing |
| Effectiveness | Every step can actually be carried out in finite time | A step is stated but cannot be performed |
Finiteness is the property students break most often, and almost always in the same way: a repetition whose stopping condition never becomes true. Suppose a method is meant to keep subtracting 3 from a number until the number is exactly 0. For 12 it terminates. For 13 it never does — the value passes straight through 0 to \(-2\), and the condition it is waiting for is gone for ever. The flow in Fig. 3.1-1 shows the shape of that failure.
Diagram
Fig. 3.1-1 – flow: start → read value N → check “is N equal to 0?” → if no, N ← N − 3 → arrow returning to the check → if yes, display “finished” → stop; label the return arrow “for N = 13 this path is taken for ever” and mark the two branches “yes” and “no”
The repair is not to test more inputs but to state a condition that must eventually become true: keep subtracting while the value is greater than 0. Now the value strictly decreases at every pass and cannot avoid crossing the boundary, so the method halts for every starting number.
3.1.4 What Algorithms Give You, and What They Do Not
Writing the method down before coding it buys you four things.
It separates thinking from typing. Deciding what to do and deciding how to say it in Java are different kinds of work, and doing them at the same time is why students stare at a half-written program without knowing what is wrong.
It is independent of language. The same algorithm can be implemented in Java by one person and in another language by someone else, and it remains the same algorithm. This is why algorithms outlive the languages they were first coded in.
It can be checked before it is coded. You can trace an algorithm on paper with a sample input and see what it produces, which is the subject of section 3.4.
And it can be discussed. Two people can argue about a method written in ordinary steps far more easily than about two hundred lines of code.
Against this, three honest limitations.
Writing an algorithm takes time, and for a task of three or four steps that time is not repaid. Nobody writes an algorithm to swap two variables.
Repetition and branching are clumsy to express on paper. Code has braces and indentation to show exactly where a loop ends; a written algorithm has to say endwhile and hope the reader is paying attention. This is why the conventions in section 3.2.2 matter so much.
And for a genuinely large problem, a single flat list of steps becomes unreadable long before it becomes complete. Large problems are broken into smaller named methods, each with its own small algorithm — which is exactly what you did in Class XI when you first wrote a method instead of one enormous main.
There is one further limitation worth stating plainly, because it is easy to forget. An algorithm can be perfectly finite, perfectly definite and perfectly effective, and still solve the wrong problem. Correctness is always correctness with respect to a specification. If the specification was misread, no amount of care in the steps will rescue it.
3.2 Ways of Representing an Algorithm
An algorithm is a method, and a method has to be written down somehow before anyone else can read it. There is no single official notation. What matters is that the reader can carry out your steps without guessing, and that you can see at a glance where a repetition begins and ends.
Three forms are in common use: the step-and-description form, pseudocode, and the flowchart. They are not three different algorithms. They are three ways of writing the same one, and it is worth seeing a single method in all three so that the differences stand out.
The method we shall use throughout this section is deliberately small. A city bus charges a flat fare of ₹10 for the first 5 kilometres of a journey, and ₹4 for every kilometre after that. Given the distance travelled, compute the fare.
3.2.1 The Step-and-Description Form
This is the form you will see most often in Computer Science answer scripts, and it has two parts.
First comes a short preamble in square brackets, naming the quantities the algorithm works with and saying what each one holds. This is not decoration; a reader who does not know what DIST stands for cannot check your steps.
Then come the steps themselves, numbered in Roman or ordinary numerals. Each step opens with its purpose in square brackets — a one-line description of what this step achieves — followed by the actions that achieve it.
[DIST is the distance of the journey in kilometres and FARE is the amount payable.
The fare is ₹10 for the first 5 km and ₹4 for every kilometre beyond that.]
I. [Read the journey distance]
Read DIST
II. [Compute the fare according to the distance]
if (DIST <= 5) then
FARE ← 10
else
FARE ← 10 + 4 × (DIST − 5)
endif
III. [Report the result]
Display("Fare = ", FARE)
IV. Stop
The bracketed descriptions are what make this form readable. Strip them away and you are left with a small block of near-code that a reader has to decipher line by line. Keep them, and the shape of the method — read, decide, report — is visible in a single glance.
Notice the symbol ←. It means “gets the value of”, and it is the standard way of writing assignment in an algorithm. Writing FARE = 10 invites confusion with the mathematical claim that FARE and 10 are equal; the arrow says clearly that a value is being placed into a named store.
3.2.2 Pseudocode Conventions Used in This Book
Pseudocode is a way of writing an algorithm that borrows the structure of a programming language — assignment, conditions, loops, indentation — while keeping the vocabulary of ordinary English. It obeys no compiler, so no punctuation is compulsory; but it must be consistent, or it stops being readable.
The conventions in Table 3.2 are used for every algorithm in this book. Learn them once and you will not have to decode a fresh notation in each section.
| Notation | Meaning |
|---|---|
X ← expression |
Assignment: evaluate the expression and store the result in X |
Read X or Accept X |
Take a value from the user and store it in X |
Display(...) |
Show the listed values to the user |
if (condition) then ... else ... endif |
Selection: carry out one of two alternatives |
Repeat while (condition) ... endwhile |
Repetition: test first, repeat as long as the condition holds |
Repeat for I = a, a+1, ..., b ... endfor |
Repetition a known number of times, I taking each value in turn |
[ ... ] |
A comment: the purpose of a step, or a note about the variables |
Stop |
End of the algorithm |
Return |
End of a named sub-algorithm, giving control back to the caller |
Two rules matter more than the rest.
Indent the body of every selection and every repetition, and close it explicitly with endif, endwhile or endfor. On paper there are no braces, so the closing word is the only thing telling your reader where the repeated part stops. An algorithm whose loop has no visible end is a genuine ambiguity, and by the standard of section 3.1.3 it fails definiteness.
Use Repeat while when you do not know in advance how many passes are needed, and Repeat for when you do. The bus fare method needs neither, since it makes one decision and stops.
Here is the same method in pseudocode, without the step-and-description scaffolding.
Read DIST
if (DIST <= 5) then
FARE ← 10
else
FARE ← 10 + 4 × (DIST − 5)
endif
Display("Fare = ", FARE)
Stop
3.2.3 Flowcharts
A flowchart is a diagram of an algorithm, in which each step is a labelled shape and arrows show the order in which the steps are carried out. The shape itself carries meaning, so a reader can tell a decision from a calculation without reading a word. The five standard symbols are shown in Fig. 3.2-1.
Diagram
Fig. 3.2-1 – the five flowchart symbols drawn in a vertical column, each with its name to the right: an oval labelled “Terminal – Start or Stop”; a parallelogram labelled “Input / Output – Read or Display”; a rectangle labelled “Process – calculation or assignment”; a diamond labelled “Decision – a condition with two exits, Yes and No”; a plain arrow labelled “Flow line – order of execution”
Read the symbols this way. Every flowchart begins and ends with an oval. A parallelogram is where data enters or leaves. A rectangle changes the value of something. A diamond asks a question and has exactly two outgoing arrows, one marked Yes and one marked No. The arrows, not the position on the page, decide the order.
Fig. 3.2-2 is the bus fare method as a flowchart. Compare it with the pseudocode above, line for shape: the Read becomes a parallelogram, the if becomes a diamond, each of the two fare calculations becomes a rectangle, and the two branches rejoin before the result is displayed.
Diagram
Fig. 3.2-2 – flowchart of the bus fare method; oval “Start” at the top; parallelogram “Read DIST”; diamond “DIST <= 5 ?” with the Yes branch on the left leading to a rectangle “FARE ← 10” and the No branch on the right leading to a rectangle “FARE ← 10 + 4 × (DIST − 5)”; both branches joined by arrows into a parallelogram “Display FARE”; oval “Stop” at the bottom
The rejoining is worth dwelling on. Both branches lead to the same display step, because the fare is reported once, whichever way it was worked out. A flowchart in which the two arms of a decision never come back together is usually a sign that the same step has been drawn twice by mistake.
Flowcharts are excellent for small methods and for explaining control flow to someone seeing it for the first time. They stop being useful quickly. A method of forty steps with three nested repetitions produces a diagram that spills across pages and is harder to follow than the pseudocode it came from. For anything beyond a page, write the steps.
3.2.4 Algorithm, Program and Language Independence
A program is an algorithm expressed in a programming language, in a form a computer can be made to execute. The relationship is one of expression, not of identity — the algorithm is the method, the program is one particular way of saying it.
That distinction explains the single most useful property of an algorithm: it is language independent. The bus fare method above says nothing about Java. Rendered in Java it would need a class, a main method, a declared type for DIST, and a semicolon after every statement. Rendered in another language it would need a different set of ceremonies. The method underneath — read, compare against 5, compute one of two amounts, display — does not change, and two students who write those different programs have implemented the same algorithm.
| Feature | Algorithm | Program |
|---|---|---|
| Nature | A method of solution, written for a human reader | An implementation of that method, written for a machine |
| Language | Ordinary language with a fixed notation; no compiler | A specific programming language, such as Java |
| Rules of form | Conventions only; no error is possible in the notation itself | Strict syntax; a missing semicolon prevents compilation |
| Execution | Cannot be executed; it is carried out by hand or implemented first | Executed directly after compilation |
| Detail required | Types, declarations and class structure are left out | Every type, declaration and structure must be stated |
| Checking | Verified by reasoning and by tracing on paper | Verified by compiling, running and testing on inputs |
There is one direction of travel that is not symmetric. Every program embodies some algorithm, even a badly written one. But not every algorithm needs to become a program — it may be carried out by a person, or used only to compare two possible approaches before either is coded.
3.3 Designing an Algorithm
Knowing what an algorithm is does not tell you how to produce one. Faced with a fresh problem, most students begin in the middle — they start writing steps before they have decided what the steps are meant to achieve, and then patch the result until it looks plausible.
There is a better order of work, and it is the order this section follows. Read the problem until you can state exactly what goes in and what must come out. Decide the shape of the method from the three building blocks available to you. Choose your variables and say what each one holds. Only then write the steps.
The problem we shall build is this. A class of N students has appeared in a test. Given each student’s marks, find how many students scored at least the qualifying mark of 35, and report that count together with the percentage of the class it represents.
3.3.1 Reading the Problem: Input, Output and Constraints
Three questions have to be answered before anything is written, and answering them badly is the commonest cause of a correct-looking method that solves the wrong problem.
What is the input? Not “the marks”, but precisely: one integer N giving the number of students, followed by N marks. Notice that N is itself an input, and that it arrives before the marks — you cannot read the marks until you know how many there are.
What is the output? Two values: a count of qualifying students, and a percentage. Saying this now prevents the common slip of computing the count and forgetting the percentage entirely.
What constraints apply? A class size must be positive. Marks in a test lie in a fixed range. Constraints matter because they tell you which inputs your method must cope with and which it may refuse.
| Aspect | Statement |
|---|---|
| Input | An integer N, the number of students; then N marks, one per student |
| Output | The number of students scoring 35 or more, and that number as a percentage of N |
| Constraints | N must be greater than 0; marks are whole numbers |
| Behaviour on invalid input | If N is not positive, report the fact and stop without computing anything |
The last row is the one students leave out. A specification that says nothing about bad input leaves the decision to whoever writes the code, and it will be made carelessly. Here the decision is deliberate: with no students there is nothing to count and no percentage to compute, since dividing by zero is not defined.
3.3.2 Building Steps from Sequence, Selection and Repetition
Whatever the problem, the steps of an algorithm are assembled from just three ways of arranging work. This is not a rule of thumb; it is a result about computation, and it is why every programming language you will meet offers the same three facilities under different names.
Sequence is one step after another, each carried out exactly once, in written order. Selection is a choice between alternatives, governed by a condition — one arm is carried out and the other is not. Repetition is a group of steps carried out again and again while some condition holds. Fig. 3.3-1 shows the three shapes side by side.
Diagram
Fig. 3.3-1 – three small flow fragments drawn side by side and labelled; left, “Sequence”: three stacked rectangles joined by downward arrows; centre, “Selection”: a diamond with a Yes branch to one rectangle and a No branch to another, both arrows rejoining below; right, “Repetition”: a diamond labelled “condition true?” with a Yes branch into a rectangle whose arrow loops back up to the diamond, and a No branch leaving downwards
Designing the method now becomes a question of arrangement. The outer shape of our problem is a sequence of three things: read the class size, examine every student, report the findings. The middle one of those must happen once per student, so it is a repetition. Inside that repetition, each student either qualifies or does not, so there is a selection. The structure is visible before a single detail is filled in.
Read N
Repeat once for each student
if this student qualifies then record the fact
Report the count and the percentage
That skeleton is worth writing down on rough paper for any problem. It is much easier to notice that a step is missing from four lines than from forty.
Filling in the detail needs one further idea. To know at the end how many students qualified, something must remember how many have qualified so far. A variable used for this purpose is a counter: it is set to zero before the repetition begins, and increased by one each time the event it counts occurs.
The placement of that initialisation is not a detail. Set the counter to zero before the repetition and it accumulates across all N students. Set it to zero inside the repetition and it is wiped clean on every pass, so the final value can never exceed one. The step is in the right place only if it happens once, and the repetition is exactly the region where “once” is not guaranteed.
The argument under Why it works is the useful part of the box, and it is the part students skip. It is a statement about what remains true every time round the loop — that COUNT holds the number of qualifiers among the students seen so far. A claim of that kind, true before the loop and preserved by each pass, is what turns “it seems to work” into “it must work”.
3.3.3 Choosing and Describing the Variables
Every variable in an algorithm should have exactly one job, and its name should say what that job is. COUNT holds a count; PCT holds a percentage. A variable named X holds whatever the reader can guess, which is the problem.
Three habits keep the variable list honest.
Give a variable one purpose only. Reusing MARK later in the same method to hold a percentage saves nothing and costs a reader their bearings.
Do not introduce a variable you never read back. A variable that is assigned and then ignored is either a leftover from an earlier attempt or a step you meant to write and did not; both are worth noticing.
Introduce a variable at the point where it first has a meaning. PCT appears only in step IV, because before the count is complete there is no percentage to speak of.
The variable description table sets all of this out for the reader: every variable the method uses, the kind of value it holds, and its purpose. It is written alongside the algorithm, not afterwards from memory, and it is a required part of the documentation for any program you submit.
| Variable | Type | Purpose |
|---|---|---|
| N | Integer | Number of students in the class |
| MARK | Integer | Marks of the student currently being examined |
| COUNT | Integer | Number of students who have qualified so far |
| I | Integer | Loop counter, running from 1 to N |
| PCT | Real | Percentage of the class that qualified |
Notice that PCT is the only real quantity in the table. Everything else counts things, and things are counted in whole numbers. Deciding this at the design stage saves you from discovering it later, when the program reports that 3 students out of 8 are 0 per cent of the class.
3.3.4 Guidelines for Writing Clear Steps
The rules below are not style preferences. Each one prevents a specific failure that appears again and again in written algorithms.
Say what a step is for
Give every step its bracketed purpose. If you cannot state in one line what a step achieves, the step is doing more than one thing and should be split.
Keep each step unambiguous
“Adjust the total” and “process the array” are not steps. Write the operation itself: COUNT ← COUNT + 1. The test is simple — could two readers carry this out differently? If so, rewrite it.
Make the order of operations explicit
In step IV the percentage is written COUNT × 100 / N rather than COUNT / N × 100. On paper the two look equal, and in exact arithmetic they are. In a program they are not: with 3 qualifiers out of 8, dividing first gives zero and multiplying afterwards leaves it zero. Multiply first and the answer is 37.5. Write the form you actually want, and do not rely on the reader to rearrange it kindly.
Close every branch and every loop
Every if needs its endif and every repetition its endwhile or endfor. An unclosed branch leaves the reader guessing which steps are inside it, and a method that can be read two ways has failed the test of definiteness from section 3.1.3.
Use no more variables than the method needs
Extra variables do not make an algorithm safer; they make it harder to check, because every one of them is a value that has to be kept correct. Before adding a variable, ask what question it answers that no existing variable answers.
Make sure the repetition can end
Every loop must contain something that moves it towards its stopping condition. In step III the counter I advances on every pass, so after N passes the repetition is over. When a loop is controlled by a condition rather than a count, check explicitly that the condition can become false — this is the finiteness requirement, and it is not automatic.
Deal with the awkward input in the algorithm, not afterwards
Step I rejects a class size of zero or less. It is tempting to leave such checks out of the written method and add them when coding, but an input the algorithm never considered is an input nobody has thought about.
3.4 Checking an Algorithm: The Dry Run
An algorithm on paper cannot be run. There is no compiler to reject it and no machine to execute it, which is exactly why a written method can look convincing and still be wrong. Something has to take the place of the computer, and that something is you.
A dry run is the act of carrying out an algorithm by hand on a chosen input, writing down the value of every variable after every step, and comparing the result the method produces with the result the specification demands. It is slow, it is unglamorous, and it finds more errors per hour than any other technique available to a student.
3.4.1 Tracing Step by Step
The discipline of a dry run lies in the record. Working the steps in your head proves nothing, because the head quietly corrects small mistakes as it goes; the point of writing every value down is that the paper does not.
Set the trace out as a table. Give one column to each variable that changes, one row to each pass of the repetition, and a final column for anything the algorithm displays. Then work strictly by what the steps say, not by what you meant them to say.
Take the qualifying-count algorithm of section 3.3.2 and run it on a class of five, with marks 41, 28, 35, 60 and 12. By hand, three of those five reach 35, so the method should report a count of 3 and a percentage of 60.
| Pass (I) | MARK read | Is MARK ≥ 35? | COUNT after the pass | Displayed |
|---|---|---|---|---|
| — (before the loop) | — | — | 0 | — |
| 1 | 41 | Yes | 1 | — |
| 2 | 28 | No | 1 | — |
| 3 | 35 | Yes | 2 | — |
| 4 | 60 | Yes | 3 | — |
| 5 | 12 | No | 3 | — |
| — (after the loop) | — | — | 3 | Qualified = 3 |
| — | — | — | 3 | Percentage = 60 |
Result: the method reports a count of 3 and a percentage of 60, which agrees with the hand answer. The trace also shows the counter behaving as the correctness argument claimed — after each pass, COUNT holds the number of qualifiers among the students examined so far, and never anything else.
Three habits make a trace trustworthy.
Include a row for the state before the loop begins. Errors of initialisation are invisible if the table starts at the first pass, and initialisation is where a large share of them live.
Record the value after every pass, even when nothing changed. Pass 2 above changes nothing, and writing the unchanged 1 is what confirms you actually considered it.
Never write the value you expect. Write the value the step produces. A trace that agrees with your expectations at every point has usually been copied from your expectations.
3.4.2 Boundary Values and Special Cases
If a trace can only ever test one input, the inputs you choose matter enormously. Testing 5, then 6, then 7 tells you almost nothing beyond what 5 told you. The inputs that find errors are the ones sitting at the edges of the method’s behaviour.
A boundary value is an input lying exactly at the point where the behaviour of an algorithm changes: the smallest value it accepts, the largest, the value at which a condition switches from false to true, or a case where a repetition runs the fewest possible number of times.
Table 3.6 lists the boundary cases worth trying for the qualifying-count problem, and what each one is designed to catch.
| Case to try | Input | What must happen | Error it exposes |
|---|---|---|---|
| Smallest valid class | N = 1, mark 50 | Count 1, percentage 100 | A loop written to run N − 1 times |
| Invalid class size | N = 0 | Message displayed, nothing computed | Division by zero |
| Mark exactly at the qualifying value | N = 1, mark 35 | Count 1, percentage 100 | The wrong comparison operator |
| Mark just below it | N = 1, mark 34 | Count 0, percentage 0 | The same, in the other direction |
| Nobody qualifies | N = 4, marks 10, 20, 30, 12 | Count 0, percentage 0 | A counter never initialised |
| Everyone qualifies | N = 4, marks 40, 50, 60, 90 | Count 4, percentage 100 | A loop that stops one pass early |
The third case is worth working through, because it is the one that separates a method that happens to be right from one that is right on purpose. The specification says a student qualifies on scoring at least 35, so a mark of exactly 35 must be counted.
| Pass (I) | MARK read | Is MARK ≥ 35? | COUNT after the pass | Displayed |
|---|---|---|---|---|
| — (before the loop) | — | — | 0 | — |
| 1 | 35 | Yes | 1 | — |
| — (after the loop) | — | — | 1 | Qualified = 1 |
| — | — | — | 1 | Percentage = 100 |
Result: the student is counted, as the specification requires. Had step III been written if (MARK > 35), this same trace would end with a count of 0 and a percentage of 0 — and the earlier trace, on 41, 28, 35, 60, 12, would have reported 2 instead of 3. One character decides it.
A mistake of this kind, where a method is correct except at the very edge of a range, is an off-by-one error. It comes in a small number of recognisable shapes: > written for ≥, a loop running from 1 to N − 1 when it should reach N, an array index one past the last element. None of them shows up when you test a comfortable input in the middle of the range, which is precisely why comfortable inputs are the wrong ones to test.
There is a further class of special cases to think about: inputs that are valid but unusual. An empty list, a list with one element, a list in which every value is identical, a list already in the order you were asked to produce. Methods that assume “there will be at least two of them” tend to fail on exactly these.
3.4.3 Why an Algorithm Terminates
A trace that runs for ever cannot be completed on paper, and a trace that you stop after six passes tells you nothing about the seventh. Termination therefore has to be argued, not observed.
The argument has a standard shape. Find some quantity that changes on every pass of the repetition, show that it moves steadily in one direction, and show that the stopping condition must be reached once it has moved far enough. In the qualifying-count algorithm that quantity is the counter I: it increases by one every pass, and the repetition ends when it has passed N. Since N is fixed before the loop begins, the end is guaranteed.
Now consider a repetition written like this, intended to visit every second position from 0 up to N.
I ← 0
Repeat while (I ≠ N)
Display(I)
I ← I + 2
endwhile
For N = 8 it displays 0, 2, 4, 6 and stops. For N = 7 it displays 0, 2, 4, 6 and then 8, 10, 12, and never stops, because I steps straight over 7 and the equality it is waiting for never occurs. The quantity does move steadily, but the stopping condition is a single point that the movement can miss.
The repair is to test a direction rather than a point.
I ← 0
Repeat while (I < N)
Display(I)
I ← I + 2
endwhile
Now the condition is not a target to be hit but a region to be left, and I cannot increase for ever without leaving it. As a working rule: when a loop variable advances by more than one, or by an amount you compute, never control the loop with ≠ or =.
Termination can also depend on the input rather than on the method. A loop that keeps reading values until the user enters a sentinel value — a special value such as −1, agreed in advance to mean “no more data” — ends only if that value is eventually supplied. Such a method is finite for every valid input, which is what the definition of an algorithm requires; but the specification must say what the sentinel is, or the user has no way of stopping it.
3.5 Algorithms for Number Problems
The problems in this section share a family resemblance. Each takes one or two numbers, works on them arithmetically, and reports something the numbers do not display on their face — the nature of the roots of an equation, the largest divisor two numbers have in common, whether a number has any divisors at all, what its digits add up to.
They are worth studying together because the techniques recur. Guarding against an input the formula cannot handle, replacing a pair of values by a smaller pair that has the same answer, searching for a divisor without searching further than necessary, peeling a number apart one digit at a time — these appear again in problems that look nothing like these.
3.5.1 Roots of a Quadratic Equation
A quadratic equation is written \(ax^2 + bx + c = 0\), and its roots are given by the familiar formula. What makes it an interesting algorithm rather than a single line of arithmetic is that the formula behaves in three different ways, and the method has to decide which one applies before it computes anything.
The deciding quantity is the discriminant, \(d = b^2 - 4ac\). If \(d\) is negative, the square root cannot be taken among the real numbers and the roots are imaginary. If \(d\) is zero, the two roots coincide and there is really only one value, \(-b/2a\). If \(d\) is positive, there are two distinct real roots. Fig. 3.5-1 shows what the three cases look like as curves.
Diagram
Fig. 3.5-1 – three small graphs side by side, each showing a parabola against a horizontal x-axis; left graph labelled “d > 0 : two real distinct roots” with the curve cutting the axis at two marked points; centre graph labelled “d = 0 : two equal roots” with the curve touching the axis at one marked point; right graph labelled “d < 0 : roots are imaginary” with the curve lying entirely above the axis and not meeting it
One constraint has to be stated before any of this is safe. If \(a\) is zero the equation is not quadratic at all, and worse, the formula divides by \(2a\) — that is, by zero. A method that silently accepts \(a = 0\) is not incomplete, it is wrong, so the check belongs in step I.
Two details in step III repay attention.
The brackets around \(2 \times A\) are not optional. Written as −B / 2 × A, the expression divides by 2 and then multiplies by A, which is a different number entirely. On paper the notation \(\frac{-b}{2a}\) makes the grouping obvious; in a linear expression you must supply it yourself.
The nested selection tests D < 0 first and splits the remaining possibilities afterwards. Any of the three orders would work, but each branch must be reached under exactly one condition. A method that tests D < 0, then D > 0, then assumes the rest is D = 0 is correct; a method that tests D <= 0 and then D = 0 inside it is a trap waiting to be sprung.
| Step | Quantity computed | Value |
|---|---|---|
| I | A, B, C read; A ≠ 0 so the method continues | 1, −5, 6 |
| II | D ← B × B − 4 × A × C | 25 − 24 = 1 |
| III | D < 0 ? | No |
| III | D = 0 ? | No |
| III | R1 ← (5 + 1) / 2 | 3 |
| III | R2 ← (5 − 1) / 2 | 2 |
| III | Displayed | Roots are real and distinct : 3, 2 |
Result: the roots 3 and 2 are produced, which is right — \(x^2 - 5x + 6\) factorises as \((x-3)(x-2)\). Substituting either value back into the equation is a check you can make in seconds, and it is worth making.
3.5.2 Greatest Common Divisor by Euclid’s Method
The greatest common divisor of two positive integers is the largest number that divides both of them exactly. The obvious method is to try every number from the smaller of the two downwards until one divides both; for 84 and 36 that means testing 36, 35, 34 and so on down to 12. It works, and for large numbers it is hopeless.
Euclid’s method replaces the obvious search with an observation. Any number that divides both A and B also divides the remainder when A is divided by B — and conversely. So the pair \((A, B)\) and the pair \((B, A \bmod B)\) have exactly the same set of common divisors, and therefore the same greatest one. The second pair is smaller. Keep replacing, and the numbers shrink rapidly until one of them becomes zero; the other is then the answer, because every number divides 0 and the largest divisor of A is A itself.
Here A mod B means the remainder left when A is divided by B, the operation written % in Java.
| Feature | Repeated subtraction | Division method |
|---|---|---|
| Operation per pass | Subtract the smaller from the larger | Replace the pair by (B, A mod B) |
| Passes for 84 and 36 | 4 | 2 |
| Passes for 1000 and 3 | Over 300 | 2 |
| Stopping condition | The two values become equal | The second value becomes 0 |
| Arithmetic needed | Subtraction and comparison only | Integer remainder |
| Behaviour on very unequal numbers | Very slow; subtracts 3 from 1000 again and again | Unaffected; one division does the work of many subtractions |
The final row is the reason the division method is the one worth learning. Both are correct. Only one of them stays usable when the numbers are far apart.
Translating this method into Java is direct, because the three assignments of step III map one-to-one onto Java statements. The only real decision is where the input comes from; here it is read from the keyboard using Scanner.
| Pass | a at start |
b at start |
r = a % b |
a after |
b after |
|---|---|---|---|---|---|
| 1 | 84 | 36 | 12 | 36 | 12 |
| 2 | 36 | 12 | 0 | 12 | 0 |
| — | 12 | 0 | — | loop ends | — |
Result: the program prints GCD of 84 and 36 = 12. The trace shows the pair shrinking while the answer stays fixed: 84 and 36 have greatest common divisor 12, and so do 36 and 12, and so do 12 and 0.
3.5.3 Testing for a Prime and Finding Prime Pairs
A prime number is a whole number greater than 1 whose only exact divisors are 1 and itself. Testing for primality is therefore a search: look for a divisor, and report success only if the search fails.
The question worth asking is how far the search must go. Testing every number from 2 up to N − 1 certainly works, but it does far more work than necessary. Suppose N has some divisor D other than 1 and itself, so that \(N = D \times E\). If both D and E were larger than \(\sqrt{N}\), their product would be larger than N, which it is not. So at least one of the two is no larger than \(\sqrt{N}\) — and a search that reaches \(\sqrt{N}\) cannot miss it.
That single observation reduces the work on a number near 10 000 from about ten thousand divisions to about a hundred. It is a good example of a saving that comes from thinking about the problem rather than from writing faster code.
The condition is written D × D <= N rather than D <= √N deliberately. Comparing whole numbers avoids taking a square root at all, and so avoids any question of rounding at the boundary. For N = 49 the loop tests D = 2, 3, 4, 5, 6 and 7; at D = 7 the condition \(7 \times 7 \le 49\) still holds, the remainder is 0, and 49 is correctly reported composite. Had the loop stopped one step earlier, it would have declared 49 prime.
With a primality test in hand, a second problem becomes easy. Given an even number N below 100, display every pair of primes that adds up to N. The direct method needs no array at all: for each candidate P from 2 upwards, ask whether P and N − P are both prime.
Taking P only as far as N / 2 avoids reporting the same pair twice with its members swapped.
| P | Is P prime? | N − P | Is N − P prime? | Displayed |
|---|---|---|---|---|
| 2 | Yes | 16 | No | — |
| 3 | Yes | 15 | No | — |
| 4 | No | 14 | not tested | — |
| 5 | Yes | 13 | Yes | 5 + 13 = 18 |
| 6 | No | 12 | not tested | — |
| 7 | Yes | 11 | Yes | 7 + 11 = 18 |
| 8 | No | 10 | not tested | — |
| 9 | No | 9 | not tested | — |
Result: the two pairs 5 + 13 and 7 + 11 are displayed. Note the column headed not tested — the second test is only reached when the first succeeds, which is why an and between two conditions is worth more than it looks.
3.5.4 Extracting and Using the Digits of a Number
A great many problems ask for something about the digits of a number rather than about its value — the sum of the digits, the number reversed, whether it reads the same both ways, how many digits it has. None of these can be answered without first getting at the digits, and the technique for that is worth learning once and reusing everywhere.
Two operations do all the work. The remainder on division by 10 gives the last digit; integer division by 10 removes it. Applying them alternately peels the number apart from the right.
For 4073: the remainder on division by 10 is 3, and dividing by 10 leaves 407. Repeat, and the digits emerge as 3, 7, 0, 4 — in reverse order, which is a fact worth remembering rather than fighting.
| Pass | T at start | R = T mod 10 | S after | T after |
|---|---|---|---|---|
| 1 | 4073 | 3 | 3 | 407 |
| 2 | 407 | 7 | 10 | 40 |
| 3 | 40 | 0 | 10 | 4 |
| 4 | 4 | 4 | 14 | 0 |
| — | 0 | — | 14 | loop ends |
Result: the sum 14 is displayed, which agrees with \(4 + 0 + 7 + 3\). Pass 3 is the one to look at — the digit 0 contributes nothing to the sum, but the pass still has to happen, because it is what removes the 0 and exposes the 4 behind it.
Two remarks about this method, both of which matter when you code it.
The division must be integer division. In Java, t / 10 on an int discards the fractional part automatically, which is what the method wants. Store t in a double and the loop never terminates, because dividing a positive real number by 10 makes it small but never zero.
The working copy matters. If the original value is needed later — to display it, or to compare against a result built from its digits — destroy a copy and keep the original, exactly as a and b did in the GCD program.
The same peeling gives more than a sum. Adding one line builds the number reversed:
REV ← REV × 10 + R
Each new digit is appended at the right of what has been built so far, and since the digits arrive right-to-left, the result is the original number written backwards. Starting REV at 0 and running this alongside the digit sum turns 4073 into 3704 with no extra loop. That one line is the whole basis of reversing a number, checking whether a number is a palindrome, and — with 10 replaced by another base — writing a number in a different number system.
3.6 Algorithms on Single Dimensional Arrays
Everything in the previous section worked on one or two numbers at a time. Most real problems arrive as a collection — the marks of a whole class, the readings from a meter over a month, the prices in a stock register — and a collection has to be held somewhere before it can be examined as a whole.
You met arrays in Class XI, so a brief recall is enough. An array holds a fixed number of values of the same type under a single name, and the individual values are reached by position. Positions are numbered from 0, so an array of N elements has indices 0 to N − 1, and its last element is at index N − 1, never at N. Reaching any element costs the same as reaching any other, whatever its position and however large the array.
Diagram
Fig. 3.6-1 – a single dimensional array drawn as a horizontal row of five boxes containing the values 34, 12, 78, 5, 61; beneath each box its index 0, 1, 2, 3, 4; a bracket over the whole row labelled “A.length is 5”; an arrow to the last box labelled “last element is A[4], not A[5]”
That last point deserves the emphasis. The gap between “five elements” and “last index 4” is the single most productive source of errors in array code, and every algorithm in this section has been written with the boundary stated explicitly rather than left to be worked out later.
Six problems follow: finding the largest and smallest values, searching in two different ways, sorting by two different techniques, and combining two sorted arrays into one. They are the standard repertoire, and between them they demonstrate almost every idea you need for handling collections.
3.6.1 Finding the Maximum and the Minimum
The method is the one you would use with a printed list and a pencil. Look at the first value, and provisionally call it both the largest and the smallest you have seen. Then work down the list, and whenever a value beats the current champion, replace it.
The provisional answer is the interesting part. It must be an actual element of the array, and the natural choice is the first one, A[0]. Starting the search from index 1 then costs nothing, since element 0 has already been accounted for.
| I | A[I] | A[I] > MAX ? | A[I] < MIN ? | MAX after | MIN after |
|---|---|---|---|---|---|
| — (after step I) | — | — | — | 34 | 34 |
| 1 | 12 | No | Yes | 34 | 12 |
| 2 | 78 | Yes | No | 78 | 12 |
| 3 | 5 | No | Yes | 78 | 5 |
| 4 | 61 | No | No | 78 | 5 |
Result: maximum 78 and minimum 5, which is right. Notice pass 4: 61 is a large value but not large enough to displace 78, and the pass correctly changes nothing.
3.6.2 Linear Search
Searching asks a different question from the one above: not what the extreme values are, but whether a particular value is present, and if so where.
Linear search examines the elements one after another from the beginning until the value is found or the array runs out. It makes no assumption whatever about the order of the data, which is its great strength and the reason it can never be dispensed with.
Reporting the result needs care. “Found” and “not found” is less useful than the position, and a position of 0 is a perfectly good answer, so 0 cannot be used to mean failure. The usual convention is to report −1, which is not a valid index and so cannot be mistaken for one.
The early exit is worth a comment. Without it the search still gives the right answer, but it keeps comparing after the answer is known, and if the value occurs more than once it reports the last occurrence rather than the first. Leaving as soon as the question is settled is both faster and more precisely specified.
3.6.3 Binary Search
Linear search treats the array as an unordered heap. When the data is already sorted, that is a waste of information, and binary search is the method that uses it.
The idea is the one from the telephone directory. Look at the middle element of the region still under consideration. If it is the value you want, you are finished. If it is too small, the value — if present at all — must lie in the upper half, so the lower half can be discarded entirely, without examining a single element in it. If it is too large, discard the upper half. Repeat on what is left.
Each step halves the region. An array of 1000 elements is reduced to 500, then 250, then 125, and is exhausted after about ten steps. Linear search on the same array may take a thousand.
The region under consideration is held by two indices, LOW and HIGH. Everything outside the range LOW to HIGH has been ruled out. The middle position is \((\text{LOW} + \text{HIGH}) / 2\) using integer division, so when the region has an even number of elements the method takes the lower of the two middle ones — which one it takes does not matter, only that it takes one of them.
Diagram
Fig. 3.6-2 – three stacked rows, each showing the same ten-element array 5, 12, 19, 26, 33, 47, 58, 61, 74, 90 with indices 0 to 9 beneath; row 1 labelled “LOW = 0, HIGH = 9” with the cell at index 4 shaded and marked “MID, value 33, too small” and indices 0 to 4 crossed out; row 2 labelled “LOW = 5, HIGH = 9” with the cell at index 7 shaded and marked “MID, value 61, too large” and indices 7 to 9 crossed out; row 3 labelled “LOW = 5, HIGH = 6” with the cell at index 5 shaded and marked “MID, value 47, found”
| Feature | Linear search | Binary search |
|---|---|---|
| Requirement on the data | None; any order will do | The array must be sorted |
| How the region shrinks | By one element per comparison | By half per comparison |
| Comparisons on 1000 elements, worst case | 1000 | About 10 |
| Best case | 1 comparison, when the value is first | 1 comparison, when the value is at the middle |
| Cost of an unsuccessful search | The whole array is examined | About the same as a successful one |
| When it is the right choice | Small arrays, or data that is not sorted | Large sorted arrays, searched repeatedly |
The table hides a trap. Binary search is faster only if the data is already sorted; sorting an array purely in order to search it once is more work than simply searching it linearly. The technique pays when one sorted array is searched many times.
| Pass | low |
high |
mid |
a[mid] |
Comparison with 47 | Action |
|---|---|---|---|---|---|---|
| 1 | 0 | 9 | 4 | 33 | too small | low becomes 5 |
| 2 | 5 | 9 | 7 | 61 | too large | high becomes 6 |
| 3 | 5 | 6 | 5 | 47 | equal | pos becomes 5, loop ends |
Result: the program prints 47 found at index 5. Three comparisons settled a ten-element array; a linear search would have needed six.
An unsuccessful search is worth following too. Looking for 50 in the same array, the region narrows to LOW = 5, HIGH = 6, then to LOW = 6, HIGH = 6, then to LOW = 6, HIGH = 5 — at which point LOW has passed HIGH, the region is empty, and the loop ends with pos still −1. Nothing goes wrong, and nothing is examined twice.
3.6.4 Bubble Sort
Sorting arranges the elements of an array into order — here, ascending order. Bubble sort does it by comparing neighbours: it walks along the array, and wherever two adjacent elements are in the wrong order, it exchanges them.
One walk along the array is called a pass. Watch what a single pass achieves. The largest value, wherever it starts, is bigger than its right-hand neighbour at every comparison, so it is exchanged forward again and again and finishes at the end of the array. After one pass, the last position holds the largest element, and it never has to move again.
That is the whole method. Each pass places one more element correctly at the end, so the region still needing attention shrinks by one every time, and N − 1 passes are enough — once all but one element are in place, the one left over has nowhere else to be.
Diagram
Fig. 3.6-3 – one pass of bubble sort over the array 29, 13, 42, 7, 21 drawn as four stacked rows of five boxes; in each row the pair of adjacent boxes being compared is outlined and marked with the comparison, and an arrow marked “exchange” or “no change” leads to the next row; the final row shows 13, 29, 7, 21, 42 with the last box shaded and labelled “42 is now in its final position”
The upper limit of the inner repetition, N − 1 − I, is what stops the method from re-examining the tail that is already settled. Writing N − 2 there instead would still sort correctly, but it would compare pairs whose order was decided passes ago.
| Pass | Comparisons made (left element in bold order of J) | Array after the pass |
|---|---|---|
| 1 | 29 with 13 exchange; 29 with 42 keep; 42 with 7 exchange; 42 with 21 exchange | 13, 29, 7, 21, 42 |
| 2 | 13 with 29 keep; 29 with 7 exchange; 29 with 21 exchange | 13, 7, 21, 29, 42 |
| 3 | 13 with 7 exchange; 13 with 21 keep | 7, 13, 21, 29, 42 |
| 4 | 7 with 13 keep | 7, 13, 21, 29, 42 |
Result: the array is sorted as 7, 13, 21, 29, 42. Two things are visible in the trace. The tail fills up from the right — 42 is fixed after pass 1, 29 after pass 2, 21 after pass 3. And pass 4 changes nothing, because the array was already in order by then; bubble sort as written has no way of noticing that and makes the pass anyway.
int t = a[j];
a[j] = a[j + 1];
a[j + 1] = t;The temporary variable is not clutter — it is the only place the first value exists between the two assignments.
3.6.5 Selection Sort
Selection sort reaches the same destination by a different route. Instead of exchanging neighbours repeatedly, it finds the right element for each position directly.
Consider position 0. The element that belongs there is the smallest in the whole array, so find it — by the search of section 3.6.1, restricted to the region from 0 onwards — and exchange it into position 0. Position 0 is now correct and never needs touching again. Now consider position 1, the smallest of what remains, and so on. After N − 1 positions have been filled this way, the last position holds the only element left.
Notice the difference in effort. Bubble sort may exchange on every comparison; selection sort compares just as much but exchanges at most once per position.
| I (position being filled) | Region scanned | Smallest found, at index | Exchange | Array after |
|---|---|---|---|---|
| 0 | 46, 8, 31, 19, 4 | 4, at index 4 | 46 with 4 | 4, 8, 31, 19, 46 |
| 1 | 8, 31, 19, 46 | 8, at index 1 | none needed | 4, 8, 31, 19, 46 |
| 2 | 31, 19, 46 | 19, at index 3 | 31 with 19 | 4, 8, 19, 31, 46 |
| 3 | 31, 46 | 31, at index 3 | none needed | 4, 8, 19, 31, 46 |
Result: the array is sorted as 4, 8, 19, 31, 46, using two exchanges in all. The rows where no exchange was needed are worth noticing — the test POS ≠ I in step III is what prevents an element being pointlessly exchanged with itself.
| Feature | Bubble sort | Selection sort |
|---|---|---|
| Basic operation | Compare and exchange adjacent elements | Find the smallest of the remaining region and place it |
| Where the sorted part grows | From the right-hand end | From the left-hand end |
| Comparisons | About half of N × N | About half of N × N |
| Exchanges | One for every out-of-order pair; many | At most one per position; at most N − 1 |
| Data movement | Values shuffle a step at a time | Values move directly to their place |
| Behaviour on data already sorted | Still makes every pass; no exchanges | Still scans every region; no exchanges |
Neither technique is a clear winner on comparisons, because both examine every pair of positions. Selection sort wins on movement, which matters when the elements being moved are large. Both are studied here as designs rather than as production tools; bubble, selection and insertion are the three standard techniques, and all three are treated in detail when arrays are studied in their own right.
3.6.6 Merging Two Sorted Arrays
Two arrays are each already in ascending order, and a single sorted array containing all their elements is wanted. Sorting the combined collection from scratch would work and would throw away everything you already know.
The efficient method uses the ordering. The smallest element overall must be the first element of one array or the first of the other — there is nowhere else for it to be. Compare those two, copy the smaller into the result, and move past it in the array it came from. Repeat: at every stage the smallest element not yet copied is at the front of one of the two remaining stretches.
Eventually one array is used up. The other is already sorted, and everything left in it is larger than everything copied so far, so it can simply be copied across in order.
Only one of the two repetitions in step III can actually run, since step II ends the moment either array is exhausted. Writing both, rather than testing which case applies, keeps the method short and makes it impossible to get the test the wrong way round.
| Step | A[I] | B[J] | Which is copied | C so far |
|---|---|---|---|---|
| 1 | 3 | 5 | 3, from A | 3 |
| 2 | 9 | 5 | 5, from B | 3, 5 |
| 3 | 9 | 9 | 9, from A | 3, 5, 9 |
| 4 | 14 | 9 | 9, from B | 3, 5, 9, 9 |
| 5 | 14 | 25 | 14, from A | 3, 5, 9, 9, 14 |
| 6 | 20 | 25 | 20, from A | 3, 5, 9, 9, 14, 20 |
| 7 | A exhausted | 25 | 25, by step III | 3, 5, 9, 9, 14, 20, 25 |
Result: C holds 3, 5, 9, 9, 14, 20, 25 — seven elements, as it must, since 4 + 3 = 7. Step 3 is the one to study: the two front elements are equal, and the condition A[I] <= B[J] sends the one from A first. Both copies happen and neither value is lost, which is exactly what a strict < would have got wrong if it had been paired with a careless else.
3.7 Algorithms on Double Dimensional Arrays
Some data is naturally a table rather than a list: the marks of every student in every subject, the daily rainfall of each month, a seating plan, a distance chart between towns. Squeezing a table into a single row of values loses the very thing that made it a table — the fact that each value has a row and a column.
A double dimensional array stores such data directly. It is an array whose elements are themselves arrays, so a value is identified by two indices: A[I][J] is the element in row I and column J. Both index ranges begin at 0, so an array of M rows and N columns has rows 0 to M − 1 and columns 0 to N − 1.
Diagram
Fig. 3.7-1 – a grid of three rows and four columns drawn as boxes; column indices 0, 1, 2, 3 written above the grid and row indices 0, 1, 2 written to the left; the box in row 1, column 2 shaded and labelled “A[1][2]”; a note below the grid reading “3 rows × 4 columns = 12 elements; last element is A[2][3]”
Almost every method in this section is a nested repetition: an outer one that moves from row to row and an inner one that moves along the columns of the current row. Getting comfortable with which loop controls which index is most of the work.
3.7.1 Traversing a Matrix
To visit every element of a table, two loops are needed. Which of them is on the outside decides the order in which the elements are visited, and that order matters more often than students expect.
In row-wise traversal the outer repetition runs over rows and the inner one over columns, so the elements are visited left to right along row 0, then along row 1, and so on — the order in which you would read a printed table. In column-wise traversal the loops are exchanged, and the elements are visited down column 0, then down column 1.
| Feature | Row-wise traversal | Column-wise traversal |
|---|---|---|
| Outer repetition runs over | Rows, I from 0 to M − 1 | Columns, J from 0 to N − 1 |
| Inner repetition runs over | Columns, J from 0 to N − 1 | Rows, I from 0 to M − 1 |
| Order of visiting | A[0][0], A[0][1], … then row 1 | A[0][0], A[1][0], … then column 1 |
| Natural use | Totals or sorting within each row | Totals or comparisons within each column |
| Element accessed | A[I][J] in both cases | A[I][J] in both cases |
The last row is the point of the table. The two traversals differ only in the order of the loops, never in how an element is written. A student who has just exchanged the loops and then also exchanged the indices has changed the shape of the array being read, not the order of reading it, and the result is usually an error about the array bounds.
When the order does not matter — when every element is to be examined and nothing depends on the sequence — either traversal will do. Finding the largest element of a table is such a problem, and its method is the one from section 3.6.1 with one extra loop wrapped round it, plus two variables to remember where the largest was found.
The three assignments inside the if must stay together. Updating MAX without updating MR and MC leaves the position describing an element that is no longer the largest, and the error is invisible until the position is printed.
| I | J | A[I][J] | Larger than MAX? | MAX | MR | MC |
|---|---|---|---|---|---|---|
| — | — | — | — | 8 | 0 | 0 |
| 0 | 0 | 8 | No (equal) | 8 | 0 | 0 |
| 0 | 1 | 3 | No | 8 | 0 | 0 |
| 0 | 2 | 17 | Yes | 17 | 0 | 2 |
| 1 | 0 | 12 | No | 17 | 0 | 2 |
| 1 | 1 | 6 | No | 17 | 0 | 2 |
| 1 | 2 | 25 | Yes | 25 | 1 | 2 |
| 2 | 0 | 9 | No | 25 | 1 | 2 |
| 2 | 1 | 14 | No | 25 | 1 | 2 |
| 2 | 2 | 2 | No | 25 | 1 | 2 |
Result: the largest element is 25, at row 1 and column 2. The first row of the trace is A[0][0] being compared with itself, which changes nothing and costs nothing; starting the outer loop at 0 rather than complicating the method to skip one element is the simpler choice.
3.7.2 Sorting Each Row of a Matrix
“Sort the matrix” can mean two quite different things, and the specification has to say which. Sorting all M × N values together produces a single ordered sequence laid back into the table. Sorting each row separately leaves every value in the row it started in and merely rearranges it within that row. This subsection does the second.
The method needs no new sorting technique. Take the bubble sort of section 3.6.4 and apply it, unchanged, to each row in turn. A row of a double dimensional array behaves exactly like a single dimensional array of N elements, so the inner machinery is identical — only the array being sorted changes on each outer pass.
Three levels of nesting is as deep as anything in this chapter goes, and it is worth reading the structure aloud once: for each row, make N − 1 passes, and in each pass compare the adjacent pairs. If you can say that, you can write it.
Notice that the sorting happens in the array itself. After the loops have run, the original arrangement no longer exists anywhere, which is why the matrix is printed before the sorting begins. Any method that changes an array in place destroys what it started with, and if the original is needed later it must be copied first.
3.7.3 Working with the Diagonals
When a table has as many rows as columns it is called a square matrix, and only then do its diagonals exist. Two of them do.
The left diagonal, also called the main diagonal, runs from the top left corner to the bottom right. Its elements are those whose row and column indices are equal: A[0][0], A[1][1], A[2][2] and so on, which is to say A[I][I].
The right diagonal runs from the top right corner to the bottom left. Here the two indices are not equal but add up to a constant: in an N × N matrix, the element in row I lies in column N − 1 − I, so its elements are A[I][N − 1 − I].
Diagram
Fig. 3.7-2 – a 3 × 3 grid holding the values 5, 2, 9 in the first row, 4, 7, 1 in the second and 6, 3, 8 in the third; row indices 0, 1, 2 on the left and column indices 0, 1, 2 above; the cells 5, 7, 8 shaded in one tint and labelled “left diagonal, A[I][I]”; the cells 9, 7, 6 shaded in a second tint and labelled “right diagonal, A[I][N−1−I]”; the centre cell 7 marked “belongs to both”
Once the index pattern is seen, the algorithm collapses. A diagonal has exactly N elements, one from each row, so a single repetition reaches all of them — no nesting is needed, and using nested loops here would visit N × N cells to make use of 2N of them.
| I | A[I][I] | LSUM after | Column N − 1 − I | A[I][N−1−I] | RSUM after |
|---|---|---|---|---|---|
| — | — | 0 | — | — | 0 |
| 0 | 5 | 5 | 2 | 9 | 9 |
| 1 | 7 | 12 | 1 | 7 | 16 |
| 2 | 8 | 20 | 0 | 6 | 22 |
Result: the left diagonal sums to 20 and the right to 22. The element 7 at the centre is added to both sums, because it genuinely lies on both diagonals — which happens in every square matrix of odd size, and never in one of even size. If a specification asks for the sum of all diagonal elements, that shared element must be counted once, not twice, and the method needs an explicit correction.
One guard belongs in any diagonal method: check that the matrix really is square. Asking for A[I][I] in a table of 3 rows and 5 columns runs off nothing, but asking for it in a table of 5 rows and 3 columns reaches column 3 of a row that has only columns 0 to 2, and the program stops with an error about the array index. A diagonal of a non-square table is not a hard problem — it is a meaningless one, and the method should say so and stop.
3.8 Algorithms on Strings
A string is a sequence of characters treated as one value — a name, a sentence, a line read from the keyboard. Like an array, it is indexed from 0, so the characters of a sentence of L characters occupy positions 0 to L − 1, and the space between two words is a character occupying a position of its own.
Diagram
Fig. 3.8-1 – the sentence “an ox” drawn as five boxed characters a, n, space, o, x with the space box shaded and labelled “the separator is a character too”; index numbers 0, 1, 2, 3, 4 written beneath the boxes; a bracket over positions 0 to 1 labelled “word 1” and over positions 3 to 4 labelled “word 2”
The operations used in this section are the ones you met in Class XI. Table 3.14 recalls them, with the exact result each produces, because most string errors are really errors about what a method returns.
| Operation | Returns | Note |
|---|---|---|
length() |
The number of characters, as an int |
The last valid position is length() - 1 |
charAt(i) |
The character at position i, as a char |
i must lie from 0 to length() - 1 |
substring(i) |
The part from position i to the end |
A new string; the original is unchanged |
substring(i, j) |
The part from position i up to but not including j |
The second index is one past the last character wanted |
indexOf(x) |
The first position at which x occurs, or −1 |
−1 is used because 0 is a valid position |
equals(x) |
true if the two strings hold the same characters |
Compares contents, not identity |
One property of strings shapes every algorithm here. A string in Java is immutable: no operation changes an existing string, and every operation that appears to modify one in fact produces a new string. Writing WORD ← WORD + CH does not append a character to WORD; it builds a fresh string from the old contents and the new character, and points WORD at that. The method still works, and for sentences of ordinary length the cost is of no consequence — it is the reason a separate class exists for text that is manipulated heavily, which is studied alongside strings in their own right.
3.8.1 Extracting Words from a Sentence
Almost every string problem set at this level begins with the same requirement: get at the individual words. Once you can do that reliably, counting them, measuring them, comparing them and rearranging them are small additions.
The reliable method is to scan the sentence one character at a time, collecting characters into a store as you go. A character that is not a space extends the word being collected. A space means the word just collected is complete — so it can be used, and the store emptied ready for the next one.
That leaves one difficulty, and it is the difficulty in every version of this problem. The last word of a sentence is not followed by a space, so the “word is complete” case never fires for it, and the word is silently lost. There are two repairs. Either handle the leftover word after the repetition ends, or append a single space to the sentence before starting, so that every word without exception ends in the ordinary way. The second is shorter and has one path through the code instead of two, so it is the one used here.
The inner test length(WORD) > 0 does more than it appears to. Two consecutive spaces would otherwise end a word of no characters, count it, and display a blank line. With the test in place, extra spaces between words — and a sentence that begins with a space — are handled correctly without any further code.
| I | CH | Is CH a space? | WORD after | Displayed | COUNT |
|---|---|---|---|---|---|
| — | — | — | “” | — | 0 |
| 0 | a | No | “a” | — | 0 |
| 1 | n | No | “an” | — | 0 |
| 2 | space | Yes, and WORD is not empty | “” | an 2 | 1 |
| 3 | o | No | “o” | — | 1 |
| 4 | x | No | “ox” | — | 1 |
| 5 | space (the appended one) | Yes, and WORD is not empty | “” | ox 2 | 2 |
Result: both words are listed and the count is 2. Row 5 is the whole reason for appending the space: without it the loop would end after row 4 with “ox” still sitting unused in WORD, and the sentence would appear to contain one word.
Problems of this kind frequently ask you to compare a collected word against a given one — to delete it, count it, or replace it. That comparison has a trap of its own.
if (word.equals("the"))
{
count = count + 1;
}Use equals for strings and == only for primitive values such as int and char. Comparing ch == ' ' in the program above is correct precisely because ch is a char.
3.8.2 Rearranging and Transforming a Sentence
Extracting the words was the hard part. Once each word arrives, complete, at a known moment, what you do with it is a matter of choice — display it, measure it, test it, or store it towards a result.
Reversing the order of the words in a sentence shows this clearly. The scanning is unchanged, character for character. Only the action taken when a word is completed is different: instead of displaying the word, place it in front of everything built so far.
Why in front? Because the words arrive in the order they appear in the sentence, and the result needs them in the opposite order. Putting each new arrival at the head of the result reverses the sequence without any array, any second pass, or any counting.
The test for an empty result is not fussiness. Without it, the very first word would be joined to nothing by a space, and the finished sentence would carry a stray space at its end. Small blemishes of this kind are the difference between output that matches a specification and output that nearly matches it.
| Word completed | REV before | REV after |
|---|---|---|
| the | “” | “the” |
| cat | “the” | “cat the” |
| sat | “cat the” | “sat cat the” |
Result: the sentence displayed is sat cat the. The trace is short because the character-level scanning has already been established; once you trust the collecting mechanism, a trace at the level of whole words is enough to check the new part of the method.
The same skeleton answers a whole family of questions. Display only the words longer than four characters, and the change is a test before the display. Display the words in the order they came but each spelt backwards, and the change is in how WORD is built — add each character at the front rather than the end. Count how many words begin with a vowel, and the change is a test on the first character of WORD. In every case the scanning stays as it is, and only the moment of completion is rewritten. Recognising that one skeleton, rather than memorising a separate method for each question, is what this section is for.
3.9 From Algorithm to Java Program
Everything so far has produced methods on paper. This section closes the gap: it takes a method that has been designed, argued and traced, and turns it into a program that compiles, runs and can be trusted.
The translation is largely mechanical, and that is exactly the point of having done the design work first. But three decisions are made here and nowhere else — the type of every variable, where the input comes from, and what the output looks like — and each of them can turn a correct method into an incorrect program.
3.9.1 Translating Each Step into Code
Every form used in the pseudocode of this book has a direct Java counterpart. Table 3.16 gives them together, so that translation becomes a matter of substitution rather than invention.
| Pseudocode form | Java form |
|---|---|
X ← expression |
x = expression; |
Read X |
x = in.nextInt(); with a Scanner named in |
Display(...) |
System.out.println(...); |
if (c) then ... endif |
if (c) { ... } |
if (c) then ... else ... endif |
if (c) { ... } else { ... } |
Repeat while (c) ... endwhile |
while (c) { ... } |
Repeat for I = a, ..., b |
for (int i = a; i <= b; i++) { ... } |
Exit the repetition |
break; |
Stop |
The end of main, or return; to leave it early |
A[I] and A[I][J] |
a[i] and a[i][j] |
Two things in the table are not substitutions but decisions.
The first is the type of every variable. Pseudocode says S ← 0 and leaves it there; Java demands to know whether S counts things, in which case it is an int, or measures something divisible, in which case it is a double. The variable description table you wrote at design time already records this, which is one of the reasons for writing it.
The second is where the input comes from. A method that says Read N says nothing about a keyboard. In a program the value must be obtained from somewhere, and the choice — read with Scanner, or fixed in the program — should be stated plainly rather than left for a reader to discover.
Here is a complete method carried through both stages. A school library charges no fine on a book returned on time. A book returned late is charged ₹2 for each of the first five overdue days, and ₹5 for every day beyond the fifth. Given the number of days each of N returned books was overdue, report how many were late and what the total fine comes to.
The expression 10 + 5 × (D − 5) deserves one sentence. The first five days are charged in full at ₹2 each, which is ₹10, and only the days beyond the fifth are charged at ₹5. Writing 5 × D for a long delay would silently recharge the first five days at the higher rate — a specification error, not a coding one, and no compiler will find it.
Now the translation. Each step becomes the Java form from Table 3.16, the types come from the variable description, and the input is read from the keyboard.
The prompts printed before each nextInt are not part of the method, and the algorithm quite properly says nothing about them. They are there because a program that waits silently for input looks to the user like a program that has hung.
3.9.2 Testing on Seen and Unseen Inputs
A program that produces the right answer on the one input you designed with has demonstrated very little. The input you designed with is the input your method was shaped around, and it will be the input your method handles best.
Useful testing has two halves. The seen input is the one given with the specification — the example whose answer is already known, so that any disagreement is unarguable. The unseen inputs are the ones you choose afterwards, and they should be chosen for the same reasons that guided the boundary values of section 3.4.2: the smallest permitted input, the value where a rule changes, the value just past it, an input where nothing happens at all, and an input the program is meant to refuse.
The essential discipline is to work out the expected output by hand before running the program. A result computed after the event is not a prediction; it is an excuse, and it will agree with whatever the program did.
| Test | Input | Expected output | What it checks |
|---|---|---|---|
| Seen input | n = 4; days 0, 3, 8, 5 | Late 3, total 41 | The specification’s own example |
| Smallest input, nothing late | n = 1; day 0 | Late 0, total 0 | The loop runs once; a punctual book costs nothing |
| Exactly at the rule change | n = 1; day 5 | Late 1, total 10 | ₹2 a day still applies on the fifth day |
| Just past the rule change | n = 1; day 6 | Late 1, total 15 | ₹10 for five days plus ₹5 for the sixth |
| Every book late | n = 2; days 1, 1 | Late 2, total 4 | The counter accumulates across passes |
| Refused input | n = 0 | Message; nothing computed | The guard in step I |
The third and fourth rows are the pair that earns its place. A program that charges ₹5 a day from the fifth day onwards instead of from the sixth gives 25 for day 5 and 20 for day 6 — visibly wrong to anyone who has worked the numbers out first, and completely invisible to anyone who has not.
Notice also what the fourth row is really testing. It is not the code; it is whether the code and the specification agree about the phrase “beyond the fifth”. Most disputes about a program’s correctness turn out, on inspection, to be disputes about what it was supposed to do.
3.9.3 Finding and Correcting Errors
Errors come in three kinds, and knowing which kind you are looking at tells you where to look.
A syntax error breaks the rules of the Java language — a missing semicolon, an unmatched brace, a misspelt keyword. The compiler refuses to produce a class file and names the line. These are the cheapest errors to have, because they cannot escape into a finished program.
A runtime error appears while the program is running and stops it. An index outside the bounds of an array, an attempt to divide an integer by zero, a Scanner asked for a number when the user typed a word — the program halts and reports what happened. The message names the fault, and it is worth reading rather than glancing at.
A logical error is the dangerous one. The program compiles, runs to completion, and produces the wrong answer. Nothing reports it, because nothing has been violated except your intention.
| Feature | Syntax error | Runtime error | Logical error |
|---|---|---|---|
| When it is discovered | At compilation | While the program runs | Only when the output is checked |
| What happens | No class file is produced | The program stops part-way | The program finishes normally |
| Who reports it | The compiler, with a line number | The runtime system, with a message | Nobody |
| Typical cause | Missing semicolon or brace, misspelt name | Index out of bounds, division by zero | Wrong condition, wrong formula, wrong order of steps |
| How it is found | Read the compiler message | Read the message and the line it names | Compare the output with the expected result |
| How it is fixed | Correct the form of the statement | Guard the operation or correct the index | Re-examine the method, then the code |
The cycle a program goes through is shown in Fig. 3.9-1. It is worth noticing how many of the arrows point backwards.
Diagram
Fig. 3.9-1 – flow: design the algorithm → dry run on paper → write the Java code → compile → decision “any syntax errors?” with Yes returning to “write the Java code” and No continuing → run on the seen input → decision “output as expected?” with Yes continuing and No branching to “locate the error” which returns to “dry run on paper” → run on unseen and boundary inputs → decision “all correct?” with No returning to “locate the error” and Yes leading to “program accepted”; label the backward arrows “correct and repeat”
Locating a logical error is a search, and it responds to the same discipline as any other search: halve the region you are looking in. Display the value of a key variable half-way through the method. If it is already wrong there, the fault is before that point; if it is right, the fault is after it. Two or three such probes will usually corner an error that could not be found by staring at the whole program.
Two habits shorten the hunt considerably.
Re-dry-run the failing input, on paper, following the code exactly as written rather than as intended. This is how you find that the loop runs one pass too few, or that a variable is reset where it should not be.
Check the boundaries first. If the program is right on 3 and wrong on 5, and 5 is where a rule changes, the fault is almost certainly in the condition that separates the two rules — not in the arithmetic on either side of it.
When a program finally passes every test, the last step is to read it once more against the algorithm it came from. Programs drift during debugging — a condition is loosened to make one test pass, a line is moved to silence an error — and a program that no longer matches its documented method is a program whose correctness argument has quietly expired.
3.10 Summary
What an algorithm is
- An algorithm is a finite sequence of well-defined steps which, for any valid input, terminates after a finite number of steps and produces the required output.
- Five properties are demanded of it: finiteness, definiteness, zero or more inputs, at least one output, and effectiveness. A set of instructions failing any one of them is not an algorithm.
- Finiteness is the property most often broken, and almost always in the same way — a repetition whose stopping condition can never become true.
- The word comes from the name of Muhammad ibn Musa al-Khwarizmi, rendered as Algoritmi by twelfth-century Latin translators; it first meant calculating with the Hindu–Arabic numerals and later widened to mean any exact step-by-step method.
Representing an algorithm
- The step-and-description form pairs each step with a bracketed statement of its purpose, and is preceded by a description of the quantities used.
- Pseudocode borrows the structure of a programming language while keeping ordinary vocabulary. This book uses
←for assignment,Read,Display,if ... then ... else ... endif,Repeat while ... endwhile,Repeat for ... endfor,StopandReturn. - Every selection and repetition must be indented and closed explicitly, because on paper there are no braces to show where a block ends.
- A flowchart draws the method with five standard symbols — oval for start and stop, parallelogram for input and output, rectangle for a process, diamond for a decision with two exits, and arrows for the order of execution. It suits small methods and becomes unreadable for large ones.
- A program is an algorithm expressed in a programming language. The algorithm fixes the method and is language independent; the program adds types, declarations and syntax, and can be executed.
Designing and checking
- Design begins by stating the input, the output, the constraints, and what must happen when the input is invalid — not by writing steps.
- Every algorithm is assembled from three arrangements only: sequence, selection and repetition.
- A counter or an accumulator is initialised once, before the repetition that updates it. Initialising it inside the repetition destroys the running total on every pass.
- The variable description table records every variable, its type and its purpose, and is written alongside the method rather than afterwards.
- A dry run is the manual execution of an algorithm on a chosen input, recording every variable’s value after every step. It can show that a method is wrong; no number of dry runs can show that it is right.
- Correctness comes from an argument about what stays true after every pass of a repetition. The dry run catches the places where that argument was applied carelessly.
- Boundary values — the smallest input, the value where a rule changes, the value just past it — find errors that comfortable inputs never reveal. An error of this kind is called an off-by-one error.
- Termination must be argued: identify a quantity that moves steadily towards the stopping condition. A loop whose variable advances by more than one must be controlled by
<or>, never by=or≠.
Number problems
- The roots of a quadratic equation are decided by the discriminant \(d = b^2 - 4ac\): negative gives imaginary roots, zero gives two equal roots \(-b/2a\), positive gives two distinct real roots. The method must first reject \(a = 0\), which is both not quadratic and a division by zero.
- Euclid’s division method finds the greatest common divisor by replacing the pair \((A, B)\) with \((B, A \bmod B)\) until the second becomes zero, at which point the first is the answer. The replacement is valid because both pairs have the same common divisors.
- A search for a divisor need go no further than the square root of the number, because if \(N = D \times E\) then at least one of the two factors is no larger than \(\sqrt{N}\). The condition is written
D × D <= Nto avoid taking a square root at all. - The digits of a number are obtained by repeatedly taking the remainder on division by 10 and then dividing by 10. The digits emerge from the right, and
REV ← REV × 10 + Rbuilds the number reversed as they arrive.
Arrays
- An array of N elements has indices 0 to N − 1, and its last element is at index N − 1.
- Searching for the largest or smallest value starts with the first element as the provisional answer, never with zero — a provisional answer must be a real candidate.
- Linear search examines elements in turn, needs no ordering, and costs up to N comparisons. It reports −1 for absence, because 0 is a valid position.
- Binary search requires a sorted array and halves the region under consideration at every step, settling a thousand elements in about ten comparisons. Its mistakes live in the boundaries:
HIGHmust start at N − 1, the middle element must be excluded from the surviving half, and the loop must continue whileLOW <= HIGH. - Bubble sort exchanges adjacent elements that are out of order; each pass carries the largest remaining element to the end, and N − 1 passes suffice.
- Selection sort finds the smallest element of the unsorted region and places it directly. It makes the same number of comparisons as bubble sort but at most N − 1 exchanges.
- Exchanging two values requires a temporary store; assigning one to the other first destroys the value being moved.
- Two sorted arrays are merged by repeatedly copying the smaller of the two front elements, then copying whatever remains of the array that is not exhausted. Every element is examined and copied once.
Double dimensional arrays and strings
- A double dimensional array is indexed by row and column, both from 0. Row-wise and column-wise traversal differ only in which loop is on the outside, never in how an element is written.
- Sorting each row of a matrix applies an ordinary sorting technique to each row in turn, with the row index held fixed; sorting one row cannot disturb another.
- In a square matrix the left diagonal is the set of elements A[I][I] and the right diagonal the set A[I][N − 1 − I]. A single repetition reaches both. When N is odd the centre element lies on both diagonals.
- A string is indexed from 0 and is immutable — every operation that appears to change one in fact builds a new string.
- Words are extracted by scanning character by character, collecting non-space characters and treating a space as the end of a word. Appending one space to the sentence ensures the last word is completed like every other; testing that the collected word is not empty makes repeated spaces harmless.
- The same scanning skeleton answers a whole family of questions; only the action taken at the moment a word is completed changes. Placing each completed word in front of the result reverses the order of the words in one pass.
- Strings are compared with
equals, which tests the characters, not with==, which tests whether two names refer to the same object.
From algorithm to program
- Translation is largely substitution, but three decisions are made only at this stage: the type of every variable, where the input comes from, and the form of the output.
- A variable that must survive from one pass to the next is declared outside the loop; one that belongs to a single pass is declared inside it.
- Testing uses the seen input given with the specification and unseen inputs chosen at the boundaries. The expected result is worked out by hand before the program is run, or the test proves nothing.
- Errors are of three kinds. A syntax error is caught by the compiler and no class file is produced. A runtime error stops the program while it runs and is reported. A logical error is reported by nobody — the program finishes and the answer is wrong.
- A logical error is located by halving the search: display a key value part-way through, and decide from it whether the fault lies before or after that point.
- After debugging, the program is read once more against the algorithm it came from, because a program that no longer matches its documented method has lost the argument that justified believing it correct.
3.11 Key Terms
- algorithm — a finite sequence of well-defined steps which, for any valid input, terminates after a finite number of steps and produces the required output.
- binary search — a search on a sorted array that examines the middle element of the region still under consideration and discards the half in which the value cannot lie.
- boundary value — an input lying exactly where the behaviour of a method changes, such as the smallest permitted value or the value at which a condition switches.
- bubble sort — a sorting technique that repeatedly compares adjacent elements and exchanges them when they are out of order, each pass placing one more element at the end.
- column-wise traversal — visiting the elements of a double dimensional array column by column, with the outer repetition running over the columns.
- counter — a variable set to zero before a repetition and increased by one each time the event it counts occurs.
- definiteness — the requirement that every step of an algorithm has exactly one possible meaning.
- discriminant — the quantity \(b^2 - 4ac\), whose sign decides whether the roots of a quadratic equation are imaginary, equal or real and distinct.
- double dimensional array — an array whose elements are identified by two indices, a row and a column, both numbered from 0.
- dry run — the manual execution of an algorithm on a chosen input, recording the value of every variable after every step.
- effectiveness — the requirement that every step of an algorithm can actually be carried out in a finite time.
- finiteness — the requirement that an algorithm stops after a finite number of steps for every valid input.
- flowchart — a diagram of an algorithm in which each step is a labelled shape and arrows show the order of execution.
- GCD — greatest common divisor; the largest number that divides two given numbers exactly.
- immutable — describing a value that cannot be changed once created; every operation on a Java
Stringproduces a new string rather than altering the existing one. - input — the values, zero or more, supplied to an algorithm before or during its execution.
- left diagonal — in a square matrix, the elements whose row and column indices are equal, running from the top left corner to the bottom right.
- linear search — a search that examines the elements of an array one after another from the beginning until the value is found or the array is exhausted.
- logical error — a fault that leaves the program compiling and running normally but producing a wrong result; nothing reports it.
- off-by-one error — a fault in which a method is correct except at the extreme of a range, typically from
>written for>=or from a loop that runs one pass too few or too many. - output — the result an algorithm produces; every algorithm must produce at least one.
- pass — one complete walk of a repetition over the region of data it is examining.
- program — an algorithm expressed in a programming language, in a form that can be compiled and executed.
- pseudocode — a way of writing an algorithm that borrows the structure of a programming language while keeping ordinary vocabulary, and obeys no compiler.
- repetition — an arrangement in which a group of steps is carried out again and again while a condition holds.
- right diagonal — in a square matrix of size N, the elements in row I and column N − 1 − I, running from the top right corner to the bottom left.
- row-wise traversal — visiting the elements of a double dimensional array row by row, with the outer repetition running over the rows.
- runtime error — a fault that appears while the program is running and stops it, such as an index outside the bounds of an array or a division by zero.
- selection — an arrangement in which a condition decides which one of two alternatives is carried out.
- selection sort — a sorting technique that finds the smallest element of the unsorted region and exchanges it into the next position to be filled.
- sentinel value — a special value agreed in advance to mean that no more data will follow, used to end a repetition that reads input.
- sequence — an arrangement in which steps are carried out one after another, each exactly once, in written order.
- square matrix — a double dimensional array having as many rows as columns; only such an array has diagonals.
- syntax error — a breach of the rules of the language, caught by the compiler, which then produces no class file.
- variable description table — a table listing every variable a method uses, with its type and its purpose, written alongside the method as part of its documentation.
3.12 Points to Ponder
A dry run can show that a method is wrong but can never show that it is right. Why, then, do most programmers spend far more time tracing than proving? Is that a failure of discipline, or is there something a trace gives you that an argument does not?
Binary search beats linear search only when the array is already sorted, and sorting an array costs far more than searching it once. Under what circumstances does the faster method actually save anything — and how would you decide, before writing any code, which side of that line your problem falls on?
Bubble sort and selection sort make the same number of comparisons, and selection sort moves the data less. If neither is clearly better on every count, on what grounds would you ever choose one over the other? What would you need to know about the data first?
An algorithm is said to be language independent, yet every algorithm in this chapter used an assignment arrow, counted array positions from 0, and relied on division that discards the fractional part. Those conventions came from programming languages. How independent is an algorithm really, and does it matter?
A program that passes every test you can devise is not the same as a program that is correct. What would it take to close that gap completely? Is it ever closed in practice, and if not, what makes us willing to use programs at all?
The chapter insisted that awkward inputs be dealt with in the algorithm rather than patched in while coding. The finished program is the same either way. So what exactly is lost when the check is added later?
3.13 Objective Questions
- Which property of an algorithm is broken by a repetition whose stopping condition can never become true?
- Definiteness
- Finiteness
- Effectiveness
- Output [1 mark]
- In an array of N elements, the index of the last element is:
- N
- N + 1
- N − 1
- 1 [1 mark]
- Binary search may be applied only when the array is:
- of even size
- sorted
- made up of integers
- free of duplicate values [1 mark]
- After the first pass of a bubble sort in ascending order, which element is certainly in its final position?
- The smallest, at the beginning
- The largest, at the end
- The middle element
- None; no element is settled until the last pass [1 mark]
- A fault that allows a program to compile and run to completion but produces a wrong answer is called:
- a syntax error
- a runtime error
- a logical error
- a compilation error [1 mark]
Fill in the blanks. For two
intoperands in Java, the value of17 % 10is ______ and the value of17 / 10is ______. [1 mark]State whether true or false: in a square matrix of odd order, the centre element lies on both the left diagonal and the right diagonal. [1 mark]
Write the full form of GCD. [1 mark]
Match each flowchart symbol in Column A with its use in Column B.
| Column A | Column B |
|---|---|
| a. Oval | i. A calculation or an assignment |
| b. Parallelogram | ii. A condition with two exits |
| c. Rectangle | iii. The start or the stop of the method |
| d. Diamond | iv. Input from, or output to, the user |
- Assertion (A): A dry run that produces the expected output proves that the algorithm is correct. Reason (R): A dry run carries out the algorithm exactly as written, on one particular input. Based on the above statements, choose the appropriate option:
- Both Assertion (A) and Reason (R) are true and Reason (R) is the correct explanation of Assertion (A).
- Both Assertion (A) and Reason (R) are true and Reason (R) is not the correct explanation of Assertion (A).
- Assertion (A) is true but Reason (R) is false.
- Assertion (A) is false but Reason (R) is true. [1 mark]
- Assertion (A): Binary search may report that a value is absent from an array in which it is actually present. Reason (R): Binary search discards half of the region under consideration without examining any element in it. Based on the above statements, choose the appropriate option:
- Both Assertion (A) and Reason (R) are true and Reason (R) is the correct explanation of Assertion (A).
- Both Assertion (A) and Reason (R) are true and Reason (R) is not the correct explanation of Assertion (A).
- Assertion (A) is true but Reason (R) is false.
- Assertion (A) is false but Reason (R) is true. [1 mark]
3.14 Check Your Concepts
Define an algorithm. State any two of the properties that every algorithm must satisfy. [2 marks]
What is a dry run? State one thing a dry run can establish about an algorithm and one thing it cannot. [2 marks]
A search method reports −1 when the value sought is not present. Give the reason why 0 cannot be used for this purpose. [2 marks]
Differentiate between an algorithm and a program, giving any three points of difference. [3 marks]
Differentiate between linear search and binary search on the basis of (i) what the method requires of the data, (ii) the worst-case number of comparisons on an array of N elements, and (iii) how the region still under examination shrinks. [3 marks]
An algorithm sets a variable to 1 and then repeatedly adds 4 to it, stopping when the variable becomes exactly 20. Explain why this algorithm does not terminate, and state the correction needed. [3 marks]
Write an algorithm that accepts a positive integer and displays both the sum of its digits and the number written in reverse, using a single repetition. Include the description of every variable used. [4 marks]
The values 52, 7, 38, 15 and 26 are to be arranged in ascending order using the selection sort technique. Show the contents of the array after each pass, and state how many exchanges are made altogether. [4 marks]
While finding the largest value in an array, a student initialises the provisional maximum to 0. Explain, with a suitable example, why this is wrong, and state the correct initialisation. [4 marks]
Write an algorithm that accepts N integers into a single dimensional array and displays the second largest value among them. Your answer must include the algorithm, a statement of why it works, and the number of comparisons it makes in terms of N. Assume the array holds at least two distinct values. [6 marks]
Compare bubble sort and selection sort with respect to the number of comparisons made, the number of exchanges made, the end of the array at which the sorted region grows, and their behaviour on data that is already in order. On that evidence, say which of the two you would prefer for arranging a large array of long names, and give your reason. [6 marks]
3.15 Output & Debugging
- Write the exact output of the following fragment, and state in one line what it computes.
int n = 4;
int f = 1;
for (int i = 1; i <= n; i++)
{
f = f * i;
System.out.println(i + " " + f);
}[3 marks]
- The following fragment is meant to display the sum of the elements of the array. Identify the error, state what happens when the fragment is run, and rewrite the faulty line correctly.
int a[] = {5, 9, 2, 7};
int s = 0;
for (int i = 0; i <= a.length; i++)
{
s = s + a[i];
}
System.out.println(s);[3 marks]
- Write the output of the following fragment, and state what it computes for a general positive value of
x.
int x = 27;
int c = 0;
while (x > 0)
{
c = c + x % 10;
x = x / 10;
}
System.out.println(c);[2 marks]
- State what the following fragment prints. Explain why it prints that, and give the single change that would make it print
matched.
String w = "";
w = w + 'y';
w = w + 'e';
w = w + 's';
if (w == "yes")
{
System.out.println("matched");
}
else
{
System.out.println("not matched");
}[3 marks]
- How many times is the statement
c = c + 1;executed? Write the value printed.
int c = 0;
for (int i = 1; i <= 4; i++)
{
for (int j = 1; j <= 4 - i; j++)
{
c = c + 1;
}
}
System.out.println(c);[2 marks]
3.16 Programming Practice
Write a single method
boolean isPrime(int n)that returnstruewhennis a prime number andfalseotherwise. The method must not examine any trial divisor larger than the square root ofn, and must give the correct answer forn = 1andn = 2. Submit the method only, together with a variable description table for the variables it uses. [4 marks]Two single dimensional arrays hold M and N integers respectively, each already arranged in ascending order. Write a program that merges them into a third array of M + N elements which is also in ascending order, and displays the merged array. The two input arrays may be fixed in the program; state clearly which values you have used. Submit the algorithm, the complete class, and a variable description table.
Sample input: first array 4, 11, 19; second array 2, 11, 15, 30. The merged array must contain all seven values, with 11 appearing twice. [6 marks]
- Write a program that accepts a sentence terminated by a full stop and displays every word of more than four characters on a line of its own, followed by the total number of such words. Words are separated by one or more blank spaces, and the program must work correctly when the sentence contains extra spaces between words. Submit the algorithm, the complete class, and a variable description table.
Sample input: A crowded platform at the railway station. [6 marks]
- Write a program that accepts the order M of a square matrix, where M must be greater than 2 and less than 10, and then accepts M × M integers into the matrix. Display an appropriate message and stop if M is outside the permitted range. Otherwise display the matrix in the form of a square, followed by the sum of the elements of its left diagonal and the sum of the elements of its right diagonal, each on a line of its own. Submit the complete class and a variable description table.
Sample input: M = 3, with the values 5, 2, 9, 4, 7, 1, 6, 3, 8 entered row by row. [6 marks]