The compiler will provide a default copy constructor, destructor, and operator= if you don’t define them. This is fine as long as there are no pointer variables in the private data that use dynamic memory allocation. The image library that I have made available uses dynamic memory allocation. So, when you are writing your new image class, you will need to implement these operations. The copy constructor and destructor should be straightforward. (You can use the functions in ImageLib!) The only tricky part is in operator=, it is important that you check for self-assignment. Consider this example for an arbitrary class:
const someClass & operator=(const
someClass &input) {
if (this
!= &input) { // necessary since
deallocation will
// destroy
input if they are the same!
// deallocate memory for “this” here
// then copy input into “this”
}
return *this;
}
Note that the copy constructor must also take a const & parameter, otherwise we will not be able to construct from a constant copy of object.
Usually, these three methods should be written first (if you are using dynamic memory), since they are called automatically in some cases. Otherwise, this may cause your code to crash.
Recursion
You should have been introduced to recursion in your previous courses. Here is a quick recap:
Recursion is a fundamental technique for writing programs. The basic idea is break the problem into easier problems of the same general type. Note that there are two distinct parts to recursive functions:
The factorial function is a common function that can illustrate recursion. The factorial function is:
F(n) = n * (n – 1) * (n – 2) * … * 2 * 1 (for any integer n > 0)
F(1) = 1
We can write a simple (non-recursive) function to compute factorial:
// Computes the factorial of n, if n
> 0. Otherwise, returns 1.
int factorial(int n) {
int result = 1;
for (int i = 1; i <=
n; i++)
result =
result * i;
return result;
}
Note that we can also write: F(n) = n * F(n-1) (for n >
1). Since this defines the function in terms of a simpler version of the
problem, we can use recursion to compute factorial:
// Computes the factorial of n, if n
> 0. Otherwise, returns 1.
int
factorial(int n) {
if (n <= 1) return 1;
else return n *
factorial(n – 1);
}
Let’s compare the two versions:
The recursive version is simpler, because the computer is doing more of the work. The recursive version may require more memory (if the compiler is not smart enough to optimize the code) and it may take slightly longer (owing to function calling overhead), but these are not overriding factors. If you write your code using an efficient algorithm and, after it is working properly, find that the speed is not acceptable, then you can worry about optimization.
Recursion is similar to induction in that you assume that simpler problem (the recursive call) is solved correctly and then you use this assumption to solve the complete problem. One of the most difficult aspects of recursion is accepting that the recursive call will do the right thing. Here is a checklist of conditions that must be true for the recursion to work. If they are all true, then you should feel confident that you function will work correctly
Let’s go back to our recursive factorial function and see if it meets these criteria.
There is a classic problem called “the towers of
This problem seems difficult at first glance (and it is difficult to solve without recursion), but it can be solved very elegantly using recursion. First, let’s look at how the problem can be broken down into simpler problems of a similar nature. A simpler problem would be to move fewer disks than in the original problem. What if we moved half of the disks? What would be our next step? We have nowhere to go from there, because the new problem can’t be solved like the old problem (there is no spare tower to work with). How about moving all but one of the disks? At that point, we could move the largest disk. We can also move the other disks after moving the largest disk, since the pole with the largest disk can be used as a spare tower. This leads to the following procedure: move all but the largest disk onto the spare tower (using a recursive call), move the largest disk onto goal tower, and then move all of the other disks onto the goal tower (using another recursive call). Believe it or not, with a simple base case, this solves the entire problem!
To solve the problem, we will assume that we already have classes corresponding to towers and disks with the following interface:
disk:
No methods needed.
tower:
disk removeTopDisk()
bool addDiskToTop(disk diskAdded) // Returns false if new disk is too big to be added
//
Recursive algorithm that solves the towers of
// Pre-conditions: Source
has at least n disks on it.
// Destination
and spare are either empty or they hold only
// disks that are
larger than the top n disks on source.
//
Post-conditions: The top n disks on
source have been moved to destination.
// Disks that
start on destination or spare are not moved.
void hanoi(int n, tower &source,
tower &destination, tower &spare)
{
if (n > 0)
{
disk
diskToMove = source.removeTopDisk();
destination.addDiskToTop(diskToMove);
}
}
Believe it or not, that solves the entire problem!
A java applet that demonstrates the solution can be found at:
http://www.cut-the-knot.org/recurrence/hanoi.shtml
Trace the algorithm for n = 4:
Move
disk from a to c
Move
disk from a to b
Move
disk from c to b
Move
disk from a to c
Move
disk from b to a
Move
disk from b to c
Move
disk from a to c
Move
disk from a to b
Move
disk from c to b
Move
disk from c to a
Move
disk from b to a
Move
disk from c to b
Move
disk from a to c
Move
disk from a to b
Move
disk from c to b
How do we know that the pre-conditions are never violated? We need induction to prove this, so you’ll have to trust me for now.
An interesting question is: given n, how many moves does it
take to solve the towers of
moves(n) = moves(n – 1) + 1 + moves(n – 1) = 2 * moves(n – 1) + 1
moves(1) = 1
For example: moves(3) = 2 * moves(2) + 1 = 2 * (2 * moves(1) + 1) + 1 = 2 * (2 * 1 + 1) + 1 = 7
It can be observed (and proven using induction) that moves(n) = 2n – 1.
One season on an episode of Survivor, the community challenge involved an intelligence test where the teams played a game against each other called 21 flags. At the start of the game, there are 21 flags in a designated location. The two teams take turns removing 1, 2, or 3 flags from the game. The winning team is the team to remove the last flag. Interestingly, if the first team knows the optimal strategy, they can guarantee that they win the game. The solution can be seen using recursive (and logical) reasoning.
First, let’s think of the base case. At what position can you guarantee that you can win the game? If there are 1, 2, or 3 flags left, you win. Now, how can you put the other team in a position such that they must leave you with 1, 2, or 3 flags. The only case that guarantees this is if they have 4 flags during their turn. If you can leave them with exactly 4 flags, then you win. From here, we need to devise a way to leave the opponent with exactly four flags. The key is to see that this is exactly like trying to leave the opponent with 0 flags (winning the game). If they have exactly 8 flags on their turn, then you can leave them with exactly 4 flags on their next turn, and then win. This same situation applies at each step – you want to leave you opponent with a number of flags that is a multiple of 4 every time. If you go first, you remove 1 flag, leaving the opponent with 20. No matter how many flags your opponents takes, you leave them with 16, then 8, then 4, then 0, and you win no matter what they do.
Fractals
Fractals are
(according to Wikipedia quoting Mandelbrot) “a rough or fragmented geometric
shape that can be split into parts, each of which is (at least approximately) a
reduced-size copy of the whole.” Fractal patterns can be drawn relatively
easily using recursion. An example is a ruler pattern (which is a simple
fractal). The inches (on the bottom) are divided into halves, which have a
smaller tick mark. The halves are recursively divided into quarters and then
again into eighths, with successively smaller tick marks.
If we assume that we
have a method to draw a segment somewhere, we can write a function to draw a
snowflake fractal. Let’s assume that someone has given us a method:
void drawSegment(x1, y1, x2,
y2); // (x1, y1) and (x2, y2)
are the coordinates of the endpoints.
Here is our
snowflake method:
void drawSnowflake(float
centerx, float centery, float size) {
if
(size < MIN_SIZE) return;
//
draw “plus”
drawSegment(centerx,
centery – size/2, centerx, centery + size/2);
drawSegment(centerx
– size/2, centery, centerx + size/2, centery);
//
recursively draw smaller snowflakes
drawSnowflake(centerx,
centery – size/4, size/2);
drawSnowflake(centerx,
centery + size/4, size/2);
drawSnowflake(centerx
– size/4, centery, size/2);
drawSnowflake(centerx
+ size/4, centery, size/2);
}
Notice that each
snowflake is a copy of the largest snowflake, except that it is smaller,
shifted, and has one less level of “flake.” You will be programming an image
fractal in your next assignment. Here are some alternative patterns:
Carrano, Chapter 2:
4, 7, 9, 10, 11, 13, 18