## 1.2 The Importance of Developing Efficient Algorithms
Previously we mentioned that, regardless of how fast computers become or how cheap memory gets, efficiency will always remain an important consideration. Next we show why this is so by comparing two algorithms for the same problem.
### 1.2.1 Sequential Search Versus Binary Search
Earlier we mentioned that the approach used to find a name in the phone book is a modified binary search, and it is usually much faster than a sequential search. Next we compare algorithms for the two approaches to show how much faster the binary search is.
We have already written an algorithm that does a sequential search—namely, [Algorithm 1.1](LiB0008.html#27). An algorithm for doing a binary search of an array that is sorted in nondecreasing order is similar to thumbing back and forth in a phone book. That is, given that we are searching for *x*, the algorithm first compares *x* with the middle item of the array. If they are equal, the algorithm is done. If *x* is smaller than the middle item, then *x* must be in the first half of the array (if it is present at all), and the algorithm repeats the searching procedure on the first half of the array. (That is, *x* is compared with the middle item of the first half of the array. If they are equal, the algorithm is done, etc.) If *x* is larger than the middle item of the array, the search is repeated on the second half of the array. This procedure is repeated until *x* is found or it is determined that *x* is not in the array. An algorithm for this method follows.
Algorithm 1.5: Binary Search**![Start example](https://box.kancloud.cn/e95df9d604ab2d89febe370ae4d88fb1_1x1.gif)**- Problem: Determine whether *x* is in the sorted array *S* of *n* keys.
- Inputs: positive integer *n*, sorted (nondecreasing order) array of keys *S* indexed from 1 to *n*, a key *x.*
- Outputs: *location*, the location of *x* in *S* (0 if *x* is not in *S*).
```
void binsearch (int n,
const keytype S[],
keytype x,
index& location)
{
index low, high, mid;
low = 1; high = n;
location = 0;
while (low < = high && location = = 0){
mid = ∟(low + high)/2⌋;
if (x = = S[mid])
location = mid;
else if (x < S[ mid ])
high = mid - 1;
else
low = mid + 1;
}
}
```
**![End example](https://box.kancloud.cn/e95df9d604ab2d89febe370ae4d88fb1_1x1.gif)**
Let's compare the work done by Sequential Search and Binary Search. For focus we will determine the number of comparisons done by each algorithm. If the array *S* contains 32 items and *x* is not in the array, [Algorithm 1.1](LiB0008.html#27) (Sequential Search) compares *x* with all 32 items before determining that *x* is not in the array. In general, Sequential Search does *n* comparisons to determine that *x* is not in an array of size *n.* It should be clear that this is the largest number of comparisons Sequential Search ever makes when searching an array of size *n.* That is, if *x* is in the array, the number of comparisons is no greater than *n.*
Next consider [Algorithm 1.5](#ch01ex10) (Binary Search). There are two comparisons of *x* with *S*\[*mid*\] in each pass through the while loop (except when *x* is found). In an efficient assembler language implementation of the algorithm, *x* would be compared with *S*\[*mid*\] only once in each pass, the result of that comparison would set the condition code, and the appropriate branch would take place based on the value of the condition code. This means that there would he only one comparison of *x* with *S*\[*mid*\] in each pass through the while loop. We will assume the algorithm is implemented in this manner. With this assumption, [Figure 1.1](#ch01fig01) shows that the algorithm does six comparisons when *x* is larger than all the items in an array of size 32. Notice that 6 = lg 32 + 1. By "lg" we mean log2. The log2 is encountered so often in analysis of algorithms that we reserve the special symbol lg for it. You should convince yourself that this is the largest number of comparisons Binary Search ever does. That is, if *x* is in the array, or if *x* is smaller than all the array items, or if *x* is between two array items, the number of comparisons is no greater than when *x* is larger than all the array items.
[![Click To expand](https://box.kancloud.cn/2a950cc7ee5d79061a1f50723ba1b8bb_350x50.jpg)](fig1-1_0.jpg)
Figure 1.1: The array items that Binary Search compares with *x* when *x* is larger than all the items in an array of size 32. The items are numbered according to the order in which they are compared.
Suppose we double the size of the array so that it contains 64 items. Binary Search does only one comparison more because the first comparison cuts the array in half, resulting in a subarray of size 32 that is searched. Therefore, when *x* is larger than all the items in an array of size 64, Binary Search does seven comparisons. Notice that 7 = lg 64 + 1. In general, each time we double the size of the array we add only one comparison. Therefore, if *n* is a power of 2 and *x* is larger than all the items in an array of size *n*, the number of comparisons done by Binary Search is lg *n* + 1.
[Table 1.1](#ch01table01) compares the number of comparisons done by Sequential Search and Binary Search for various values of *n*, when *x* is larger than all the items in the array. When the array contains around 4 billion items (about the number of people in the world), Binary Search does only 33 comparisons, whereas Sequential Search compares *x* with all 4 billion items. Even if the computer was capable of completing one pass through the while loop in a nanosecond (one billionth of a second), Sequential Search would take 4 seconds to determine that *x* is not in the array, whereas Binary Search would make that determination almost instantaneously. This difference would be significant in an online application or if we needed to search for many items.
Table 1.1: The number of comparisons done by Sequential Search and Binary Search when *x* is larger than all the array itemsArray Size
Number of Comparisons by Sequential Search
Number of Comparisons by Binary Search
128
128
8
1,024
1,024
11
1,048,576
1,048,576
21
4, 294, 967, 296
4, 294, 967, 296
33
For convenience, we considered only arrays whose sizes were powers of 2 in the previous discussion of Binary Search. In [Chapter 2](LiB0014.html#141) we will return to Binary Search as an example of the divide-and-conquer approach, which is the focus of that chapter. At that time we will consider arrays whose sizes can be any positive integer.
As impressive as the searching example is, it is not absolutely compelling because Sequential Search still gets the job done in an amount of time tolerable to a human life span. Next we will look at an inferior algorithm that does not get the job done in a tolerable amount of time.
### 1.2.2 The Fibonacci Sequence
The algorithms discussed here compute the *n*th term of the Fibonacci sequence, which is defined recursively as follows:
![](https://box.kancloud.cn/f48e804b01fae69ba0bbffee7aca3d7e_255x75.jpg)
Computing the first few terms, we have
![](https://box.kancloud.cn/02c1a3db584a5cafb31f99c027e448a9_237x102.jpg)
There are various applications of the Fibonacci sequence in computer science and mathematics: Because the Fibonacci sequence is defined recursively, we obtain the following recursive algorithm from the definition.
Algorithm 1.6: nth Fibonacci Term (Recursive)**![Start example](https://box.kancloud.cn/e95df9d604ab2d89febe370ae4d88fb1_1x1.gif)**- Problem: Determine the *n*th term in the Fibonacci sequence.
- Inputs: a nonnegative integer *n.*
- Outputs: *fib*, the *n*th term of the Fibonacci sequence.
```
int fib (int n)
{
if (n <= 1)
return n;
else
return fib (n - 1) + fib (n-2);
}
```
**![End example](https://box.kancloud.cn/e95df9d604ab2d89febe370ae4d88fb1_1x1.gif)**
By "nonnegative integer" we mean an integer that is greater than or equal to 0, whereas by "positive integer" we mean an integer that is strictly greater than 0. We specify the input to the algorithm in this manner to make it clear what values the input can take. However, for the sake of avoiding clutter, we declare *n* simply as an integer in the expression of the algorithm. We will follow this convention throughout the text.
Although the algorithm was easy to create and is understandable, it is extremely inefficient. [Figure 1.2](#ch01fig02) shows the recursion tree corresponding to the algorithm when computing *fib*(5). The children of a node in the tree contain the recursive calls made by the call at the node. For example, to obtain *fib*(5) at the to! level we need *fib*(4) and *fib*(3); then to obtain *fib*(3) we need *fib*(2) and *fib*(1), and so on. As the tree shows, the function is inefficient because values are computed over and over again. For example, *fib*(2) is computed three times.
[![Click To expand](https://box.kancloud.cn/d3d9d69571a28941892841d7f12035f4_350x260.jpg)](fig1-2_0.jpg)
Figure 1.2: The recursion tree corresponding to [Algorithm 1.6](#ch01ex11) when computing the fifth Fibonacci term.
How inefficient is this algorithm? The tree in [Figure 1.2](#ch01fig02) shows that the algorithm computes the following numbers of terms to determine *fib(n)* for 0 ≤ *n* ≤ 6:
*n*
Number of Terms Computed
0
1
1
1
2
3
3
5
4
9
5
15
6
25
The first six values can be obtained by counting the nodes in the subtree rooted at *fib(n)* for 1 ≤ *n* ≤ 5, whereas the number of terms for *fib(6)* is the sum of the nodes in the trees rooted at *fib*(5) and *fib*(4) plus the one node at the root. These numbers do not suggest a simple expression like the one obtained for Binary Search. Notice, however, that in the case of the first seven values, the number of terms in the tree more than doubles every time it increases by 2. For example, there are nine terms in the tree when *n* = 4 and 25 terms when *n* = 6. Let's call *T(n)* the number of terms in the recursion tree for *n.* If the number of terms more than doubled every time *n* increased by 2, we would have the following for *m* a positive power of 2:
![](https://box.kancloud.cn/6a22b0718e607d9eb25d80f1eae8e110_211x245.jpg)
Because *T*(0) = 1, this would mean *T(n)* > 2*n*/2. We use induction to show that this is true for *n* ≥ 2 even if *n* is not a power of 2. The inequality does not hold for *n* = 1 because *T*(1) = 1, which is less than 21/2. Induction is reviewed in [Section A.3](LiB0095.html#1289) in [Appendix A](LiB0093.html#1281).
Theorem 1.1**![Start example](https://box.kancloud.cn/e95df9d604ab2d89febe370ae4d88fb1_1x1.gif)**If *T* (*n*) is the number of terms in the recursion tree corresponding to [Algorithm 1.6](#ch01ex11), then, for *n* ≥ 2,
![](https://box.kancloud.cn/f2ad82b24865551f3096771e08a37b1b_103x24.jpg)
Proof: The proof is by induction on *n*.
Induction base: We need two base cases because the induction step assumes the results of two previous cases. For *n* = 2 and *n* = 3, the recursion in [Figure 1.2](#ch01fig02) shows that
![](https://box.kancloud.cn/82ed013155059ceca4bf28693555f633_200x55.jpg)
Induction hypothesis: One of way to make the induction hypothesis is to assume that the statement is true for all *m < n.* Then, is the induction step, show that this implies that the statement must be true for *n.* This technique is used in this proof. Suppose for all *m* such that 2 ≤ *m < n.*
![](https://box.kancloud.cn/ef78ab9e1b1a1dfb21309fdab38b5d41_115x26.jpg)
Induction step: We must show that *T*(*n*) < 2*n*/2. The value of *T*(*n*) is the sum of *T*(*n* - 1) and *T*(*n* - 2) plus the one node at the root. Therefore,
![](https://box.kancloud.cn/77308a71c9673bd8f27feca7c05341bf_400x61.jpg)
**![End example](https://box.kancloud.cn/e95df9d604ab2d89febe370ae4d88fb1_1x1.gif)**
We established that the number of terms computed by [Algorithm 1.6](#ch01ex11) to determine the *n*th Fibonacci term is greater than 2*n*\\2. We will return to this result to show how inefficient the algorithm is. But first let's develop an efficient algorithm for computing the *n*th Fibonacci term. Recall that the problem with the recursive algorithm is that the same value is computed over and over. As [Figure 1.2](#ch01fig02) shows, *fib*(2) is computed three times in determining *fib*(5). If when computing a value, we save it in an array, then whenever we need it later we do not need to recompute it. The following iterative algorithm uses this strategy.
Algorithm 1.7: *n*th Fibonacci Term (Iterative)**![Start example](https://box.kancloud.cn/e95df9d604ab2d89febe370ae4d88fb1_1x1.gif)**- Problem: Determine the *n*th term in the Fibonacci sequence.
- Inputs: a nonnegative integer *n.*
- Outputs : *fib*2, the *n*th term in the Fibonacci sequence.
```
int fib 2 (int n)
{
index i;
int f[0 .. n];
f[ 0 ] = 0;
if (n < 0)
f[ 1 ] = 1;
for (i = 2; i<= n; i++)
f[ i ] = f[i - 1] + f [i -2 ];
}
return f[ n ];
}
```
**![End example](https://box.kancloud.cn/e95df9d604ab2d89febe370ae4d88fb1_1x1.gif)**
[Algorithm 1.7](#ch01ex13) can be written without using the array *f* because only the two most recent terms are needed in each iteration of the loop. However, it is more clearly illustrated using the array.
To determine *fib*2(*n*), the previous algorithm computes every one of the first *n* + 1 terms just once. So it computes *n* + 1 terms to determine the *n*th Fibonacci term. Recall that [Algorithm 1.6](#ch01ex11) computes more than 2*n*/2 terms to determine the nth Fibonacci term. [Table 1.2](#ch01table02) compares these expressions for various values of *n.* The execution times are based on the simplifying assumption that one term can be computed in 10-9 second. The table shows the time it would take [Algorithm 1.7](#ch01ex13) to compute the nth term on a hypothetical computer that could compute each term in a nanosecond, and it shows a lower bound on the time it would take to execute [Algorithm 1.7](#ch01ex13). By the time *n* is 80, [Algorithm 1.6](#ch01ex11) takes at least 18 minutes. When *n* is 120, it takes more than 36 years, an amount of time intolerable compared with a human life span. Even if we could build a computer one billion times as fast, [Algorithm 1.6](#ch01ex11) would take over 40,000 years to compute the 200th term. This result can be obtained by dividing the time for the 200th term by one billion. We see that regardless of how fast computers become, [Algorithm 1.6](#ch01ex11) will still take an intolerable amount of time unless *n* is small. On the other hand, [Algorithm 1.7](#ch01ex13) computes the *n*th Fibonacci term almost instantaneously. This comparison shows why the efficiency of an algorithm remains an important consideration regardless of how fast computers become.
Table 1.2: A comparison of [Algorithms 1.6](#ch01ex11) and [1.7](#ch01ex13)*n*
*n* + 1
2*n*/2
Execution Time Using [Algorithm 1.7](#ch01ex13)
Lower Bound on Execution Time Using [Algorithm 1.6](#ch01ex11)
40
41
1,048,576
41 ns\[[\*](#ftn.ch01table02fnt01)\]
1048 μ*s*\[[†](#ftn.ch01table02fnt02)\]
60
61
1\.l × 1O9
61 ns
1 s
80
81
1\.1 × 1012
81 ns
18 min
100
101
1\.1 × 1015
101 ns
13 days
120
121
1\.2 × 1018
121 ns
36 years
160
161
1\.2 × 1024
161 ns
3\.8 × 107 years
200
201
1\.3 × 1030
201 ns
4 × 1013 years
\[[\*](#ch01table02fnt01)\]1 ns = 10-9 second.
\[[†](#ch01table02fnt02)\]1 μs = 10−6 second.
[Algorithm 1.6](#ch01ex11) is a divide-and-conquer algorithm. Recall that the divide-and-conquer approach produced a very efficient algorithm ([Algorithm 1.5](#ch01ex10): Binary Search) for the problem of searching a sorted array. As shown in [Chapter 2](LiB0014.html#141), the divide-and-conquer approach leads to very efficient algorithms for some problems, but very inefficient algorithms for other problems. Our efficient algorithm for computing the *n*th Fibonacci term ([Algorithm 1.7](#ch01ex13)) is an example of the *dynamic programming approach*, which is the focus of [Chapter 3](LiB0024.html#252). We see that choosing the best approach can be essential.
We showed that [Algorithm 1.6](#ch01ex11) computes at least an exponentially large number of terms, but could it be even worse? The answer is no. Using the techniques in [Appendix B](LiB0102.html#1369), it is possible to obtain an exact formula for the number of terms, and the formula is exponential in *n.* See [Examples B.5](LiB0103.html#1384) and [B.9](LiB0103.html#1394) in [Appendix B](LiB0102.html#1369) for further discussion of the Fibonacci sequence.
- Table of Contents
- BackCover
- Foundations of Algorithms Using C++ Pseudocode, Third Edition
- Preface
- Chapter Contents
- Pedagogy
- Course Outlines
- Acknowledgments
- Errors
- Chapter 1: Algorithms - Efficiency, Analysis, and Order
- 1.2 The Importance of Developing Efficient Algorithms
- 1.3 Analysis of Algorithms
- 1.4 Order
- 1.5 Outline of This Book
- Exercises
- Chapter 2: Divide-and-Conquer
- 2.1 Binary Search
- 2.2 Mergesort
- 2.3 The Divide-and-Conquer Approach
- 2.4 Quicksort (Partition Exchange Sort)
- 2.5 Strassen's Matrix Multiplication Algorithm
- 2.6 Arithmetic with Large Integers
- 2.7 Determining Thresholds
- 2.8 When Not to Use Divide-and-Conquer
- Exercises
- Chapter 3: Dynamic Programming
- 3.1 The Binomial Coefficient
- 3.2 Floyd's Algorithm for Shortest Paths
- 3.3 Dynamic Programming and Optimization Problems
- 3.4 Chained Matrix Multiplication
- 3.5 Optimal Binary Search Trees
- 3.6 The Traveling Salesperson Problem
- Exercises
- Chapter 4: The Greedy Approach
- 4.1 Minimum Spanning Trees
- 4.2 Dijkstra's Algorithm for Single-Source Shortest Paths
- 4.3 Scheduling
- 4.4 Huffman Code
- 4.5 The Greedy Approach versus Dynamic Programming: The Knapsack Problem
- Exercises
- Chapter 5: Backtracking
- 5.2 The n-Queens Problem
- 5.3 Using a Monte Carlo Algorithm to Estimate the Efficiency of a Backtracking Algorithm
- 5.4 The Sum-of-Subsets Problem
- 5.5 Graph Coloring
- 5.6 The Hamiltonian Circuits Problem
- 5.7 The 0-1 Knapsack Problem
- Exercises
- Chapter 6: Branch-and-Bound
- 6.1 Illustrating Branch-and-Bound with the 0 - 1 Knapsack problem
- 6.2 The Traveling Salesperson Problem
- 6.3 Abductive Inference (Diagnosis)
- Exercises
- Chapter 7: Introduction to Computational Complexity - The Sorting Problem
- 7.2 Insertion Sort and Selection Sort
- 7.3 Lower Bounds for Algorithms that Remove at Most One Inversion per Comparison
- 7.4 Mergesort Revisited
- 7.5 Quicksort Revisited
- 7.6 Heapsort
- 7.6.1 Heaps and Basic Heap Routines
- 7.6.2 An Implementation of Heapsort
- 7.7 Comparison of Mergesort, Quicksort, and Heapsort
- 7.8 Lower Bounds for Sorting Only by Comparison of Keys
- 7.8.1 Decision Trees for Sorting Algorithms
- 7.8.2 Lower Bounds for Worst-Case Behavior
- 7.8.3 Lower Bounds for Average-Case Behavior
- 7.9 Sorting by Distribution (Radix Sort)
- Exercises
- Chapter 8: More Computational Complexity - The Searching Problem
- 8.1 Lower Bounds for Searching Only by Comparisons of Keys
- 8.2 Interpolation Search
- 8.3 Searching in Trees
- 8.4 Hashing
- 8.5 The Selection Problem: Introduction to Adversary Arguments
- Exercises
- Chapter 9: Computational Complexity and Interactability - An Introduction to the Theory of NP
- 9.2 Input Size Revisited
- 9.3 The Three General Problem Categories
- 9.4 The Theory of NP
- 9.5 Handling NP-Hard Problems
- Exercises
- Chapter 10: Number-Theoretic Algorithms
- 10.1 Number Theory Review
- 10.2 Computing the Greatest Common Divisor
- 10.3 Modular Arithmetic Review
- 10.4 Solving Modular Linear Equations
- 10.5 Computing Modular Powers
- 10.6 Finding Large Prime Numbers
- 10.7 The RSA Public-Key Cryptosystem
- Exercises
- Chapter 11: Introduction to Parallel Algorithms
- 11.1 Parallel Architectures
- 11.2 The PRAM Model
- Exercises
- Appendix A: Review of Necessary Mathematics
- A.2 Functions
- A.3 Mathematical Induction
- A.4 Theorems and Lemmas
- A.5 Logarithms
- A.6 Sets
- A.7 Permutations and Combinations
- A.8 Probability
- Exercises
- Appendix B: Solving Recurrence Equations - With Applications to Analysis of Recursive Algorithms
- B.2 Solving Recurrences Using the Characteristic Equation
- B.3 Solving Recurrences by Substitution
- B.4 Extending Results for n, a Power of a Positive Constant b, to n in General
- B.5 Proofs of Theorems
- Exercises
- Appendix C: Data Structures for Disjoint Sets
- References
- Index
- Index_B
- Index_C
- Index_D
- Index_E
- Index_F
- Index_G
- Index_H
- Index_I
- Index_J
- Index_K
- Index_L
- Index_M
- Index_N
- Index_O
- Index_P
- Index_Q
- Index_R
- Index_S
- Index_T
- Index_U
- Index_V
- Index_W-X
- Index_Y
- Index_Z
- List of Figures
- List of Tables
- List of Algorithms, Examples, and Theorems
- List of Sidebars