A user cannot distinguish <#66317#><#34032#>sort<#34032#><#66317#> and <#66318#><#34033#>quick-sort<#34033#><#66318#>. Both
consume a list of numbers; both produce a list that consists of the same
numbers but is arranged in ascending order. To an observer, the functions
are completely equivalent. This raises the question which of the two a
programmer should provide. More generally, if we can develop a function
using structural recursion and an equivalent one using generative
recursion, what should we do?
To understand this choice better, let's discuss another classical example of
generative recursion from mathematics: the problem of finding the greatest
common divisor of two positive natural numbers. All such numbers have at least one divisor in common: 1. On
occasion, this is also the only common divisor. For example, 2 and 3 have
only 1 as common divisor because 2 and 3, respectively, are the only other
divisors. Then again, 6 and 25 are both numbers with several divisors:
- 6 is evenly divisible by 1, 2, 3, and 6;
- 25 is evenly divisible by 1, 5, and 25.
Still, the greatest common divisior of 25 and 6 is 1. In contrast,
18 and 24 have many common divisors:
- 18 is evenly divisible by 1, 2, 3, 6, and 9;
- 24 is evenly divisible by 1, 2, 3, 4, 6, 8, and 12.
The greatest common divisor is 6.
Following the design recipe, we start with a contract and a purpose statement:
<#71473#>;; <#66319#><#34044#>gcd<#34044#> <#34045#>:<#34045#> <#34046#>N<#34046#><#34047#>[<#34047#><#34048#>;SPMgt;=<#34048#> <#34049#>1]<#34049#> <#34050#>N<#34050#><#34051#>[<#34051#><#34052#>;SPMgt;=<#34052#> <#34053#>1]<#34053#> <#34054#><#34054#><#34055#>-;SPMgt;<#34055#><#34056#><#34056#> <#34057#>N<#34057#><#66319#><#71473#>
<#71474#>;; to find the greatest common divisior of <#66320#><#34058#>n<#34058#><#66320#> and <#66321#><#34059#>m<#34059#><#66321#><#71474#>
<#34060#>(define<#34060#> <#34061#>(gcd<#34061#> <#34062#>n<#34062#> <#34063#>m)<#34063#> <#34064#>...)<#34064#>
The contract specifies the precise inputs: natural numbers that are greater
or equal to <#66322#><#34068#>1<#34068#><#66322#> (not <#66323#><#34069#>0<#34069#><#66323#>).
Now we need to make a decision whether we want to pursue a design based on
structural or on generative recursion. Since the answer is by no means
obvious, we develop both. For the structural version, we must consider
which input the function should process: <#66324#><#34070#>n<#34070#><#66324#>, <#66325#><#34071#>m<#34071#><#66325#>, or both.
A monent's consideration suggests that what we really need is a function
that starts with the smaller of the two and outputs the first
number smaller or equal to this input that evenly divides both <#66326#><#34072#>n<#34072#><#66326#>
and <#66327#><#34073#>m<#34073#><#66327#>.
We use <#66328#><#34074#>local<#34074#><#66328#> to define an appropriate auxiliary function:
<#71475#>;; <#66329#><#34079#>gcd-structural<#34079#> <#34080#>:<#34080#> <#34081#>N<#34081#><#34082#>[<#34082#><#34083#>;SPMgt;=<#34083#> <#34084#>1]<#34084#> <#34085#>N<#34085#><#34086#>[<#34086#><#34087#>;SPMgt;=<#34087#> <#34088#>1]<#34088#> <#34089#><#34089#><#34090#>-;SPMgt;<#34090#><#34091#><#34091#> <#34092#>N<#34092#><#66329#><#71475#>
<#71476#>;; to find the greatest common divisior of <#66330#><#34093#>n<#34093#><#66330#> and <#66331#><#34094#>m<#34094#><#66331#><#71476#>
<#71477#>;; <#34095#>structural<#34095#> recursion using data definition of <#66332#><#34096#>N<#34096#><#34097#>[<#34097#><#34098#>;SPMgt;=<#34098#> <#34099#>1]<#34099#><#66332#><#71477#>
<#34100#>(d<#34100#><#34101#>efine<#34101#> <#34102#>(gcd-structural<#34102#> <#34103#>n<#34103#> <#34104#>m)<#34104#>
<#34105#>(l<#34105#><#34106#>ocal<#34106#> <#34107#>((d<#34107#><#34108#>efine<#34108#> <#34109#>(first-divisior-;SPMlt;=<#34109#> <#34110#>i)<#34110#>
<#34111#>(c<#34111#><#34112#>ond<#34112#>
<#34113#>[<#34113#><#34114#>(=<#34114#> <#34115#>i<#34115#> <#34116#>1)<#34116#> <#34117#>1]<#34117#>
<#34118#>[<#34118#><#34119#>else<#34119#> <#34120#>(c<#34120#><#34121#>ond<#34121#>
<#34122#>[<#34122#><#34123#>(and<#34123#> <#34124#>(=<#34124#> <#34125#>(remainder<#34125#> <#34126#>n<#34126#> <#34127#>i)<#34127#> <#34128#>0)<#34128#>
<#34129#>(=<#34129#> <#34130#>(remainder<#34130#> <#34131#>m<#34131#> <#34132#>i)<#34132#> <#34133#>0))<#34133#>
<#34134#>i]<#34134#>
<#34135#>[<#34135#><#34136#>else<#34136#> <#34137#>(first-divisior-;SPMlt;=<#34137#> <#34138#>(-<#34138#> <#34139#>i<#34139#> <#34140#>1))]<#34140#><#34141#>)]<#34141#><#34142#>)))<#34142#>
<#34143#>(first-divisior-;SPMlt;=<#34143#> <#34144#>(min<#34144#> <#34145#>m<#34145#> <#34146#>n))))<#34146#>
The conditions ``evenly divisible'' have been encoded as <#66333#><#34150#>(=<#34150#><#34151#> <#34151#><#34152#>(remainder<#34152#>\ <#34153#>n<#34153#>\ <#34154#>i)<#34154#>\ <#34155#>0)<#34155#><#66333#> and <#66334#><#34156#>(=<#34156#>\ <#34157#>(remainder<#34157#>\ <#34158#>m<#34158#>\ <#34159#>i)<#34159#>\ <#34160#>0)<#34160#><#66334#>. The two ensure that
<#66335#><#34161#>i<#34161#><#66335#> divides <#66336#><#34162#>n<#34162#><#66336#> and <#66337#><#34163#>m<#34163#><#66337#> without a remainder. Testing
<#66338#><#34164#>gcd-structural<#34164#><#66338#> with the examples shows that it finds the expected
answers.
Although the design of <#66339#><#34165#>gcd-structural<#34165#><#66339#> is rather straightforward,
it is also naive. It simply tests every number whether it divides
both <#66340#><#34166#>n<#34166#><#66340#> and <#66341#><#34167#>m<#34167#><#66341#> evenly and returns the first such number.
For small natural numbers, this process works just fine. Consider the
following example, however:
<#34172#>(gcd-structural<#34172#> <#34173#>101135853<#34173#> <#34174#>45014640)<#34174#>
The result is <#66342#><#34178#>177<#34178#><#66342#> and to get there <#66343#><#34179#>gcd-structural<#34179#><#66343#> had to
compare 101135676, that is, 101135853 - 177, numbers. This is a large effort
and even reasonably fast computers spend several minutes on this task.
<#34182#>Exercise 26.3.1<#34182#>
Enter the definition of <#66344#><#34184#>gcd-structural<#34184#><#66344#> into the
<#34185#>Definitions<#34185#> window and evaluate <#66345#><#34186#>(time<#34186#>\ <#34187#>(gcd-structural<#34187#><#34188#> <#34188#><#34189#>101135853<#34189#>\ <#34190#>45014640))<#34190#><#66345#> in the <#34191#>Interactions<#34191#> window.
<#34192#>Hint:<#34192#> Once <#66346#><#34193#>gcd-structural<#34193#><#66346#> is tested, switch to the MzScheme
language (without debugging). It runs programs faster than the lower
language levels but offers less protection. Add
<#34198#>(require-library<#34198#> <#34199#>``macro.ss'')<#34199#>
to the top of the <#34203#>Definitions<#34203#> window. Have some reading
handy!~ Solution<#66347#><#66347#>
Since mathematicians recognized the inefficiency of the ``structural
algorithm'' a long time ago, they studied the problem of finding divisiors
in more depth. The essential insight is that for two natural numbers
<#66348#><#34211#>larger<#34211#><#66348#> and <#66349#><#34212#>smaller<#34212#><#66349#>, their greatest common divisor is
equal to the greatest common divisior of <#66350#><#34213#>smaller<#34213#><#66350#> and the
<#66351#><#34214#>remainder<#34214#><#66351#> of <#66352#><#34215#>larger<#34215#><#66352#> divided into <#66353#><#34216#>smaller<#34216#><#66353#>. Here
is how we can put this insight into equational form:
<#34221#>(gcd<#34221#> <#34222#>larger<#34222#> <#34223#>smaller)<#34223#>
<#34224#>=<#34224#> <#34225#>(gcd<#34225#> <#34226#>smaller<#34226#> <#34227#>(remainder<#34227#> <#34228#>larger<#34228#> <#34229#>smaller))<#34229#>
Since <#66354#><#34233#>(remainder<#34233#>\ <#34234#>larger<#34234#>\ <#34235#>smaller)<#34235#><#66354#> is smaller than both
<#66355#><#34236#>larger<#34236#><#66355#> and <#66356#><#34237#>smaller<#34237#><#66356#>, the right-hand side use of
<#66357#><#34238#>gcd<#34238#><#66357#> consumes <#66358#><#34239#>smaller<#34239#><#66358#> first.
Here is how this insight applies to our small example:
- The given numbers are <#66359#><#34241#>18<#34241#><#66359#> and <#66360#><#34242#>24<#34242#><#66360#>.
- According to the mathematicians' insight, they have the same greatest
common divisor as <#66361#><#34243#>18<#34243#><#66361#> and <#66362#><#34244#>6<#34244#><#66362#>.
- And these two have the same greatest common divisor as <#66363#><#34245#>6<#34245#><#66363#> and
<#66364#><#34246#>0<#34246#><#66364#>.
And here we seem stuck because <#66365#><#34248#>0<#34248#><#66365#> is nothing expected. But,
<#66366#><#34249#>0<#34249#><#66366#> can be evenly divided by every number, so we have
found our answer: <#66367#><#34250#>6<#34250#><#66367#>.
Working through the example not only explains the idea but also suggests
how to discover the case with a trivial solution. When the smaller of the
two numbers is <#66368#><#34251#>0<#34251#><#66368#>, the result is the larger number. Putting
everything together, we get the following definition:
<#71478#>;; <#66369#><#34256#>gcd-generative<#34256#> <#34257#>:<#34257#> <#34258#>N<#34258#><#34259#>[<#34259#><#34260#>;SPMgt;=<#34260#> <#34261#>1]<#34261#> <#34262#>N<#34262#><#34263#>[<#34263#><#34264#>;SPMgt;=1]<#34264#> <#34265#><#34265#><#34266#>-;SPMgt;<#34266#><#34267#><#34267#> <#34268#>N<#34268#><#66369#><#71478#>
<#71479#>;; to find the greatest common divisior of <#66370#><#34269#>n<#34269#><#66370#> and <#66371#><#34270#>m<#34270#><#66371#><#71479#>
<#71480#>;; <#34271#>generative<#34271#> recursion: <#66372#><#34272#>(gcd<#34272#> <#34273#>n<#34273#> <#34274#>m)<#34274#><#66372#> = <#66373#><#34275#>(gcd<#34275#> <#34276#>n<#34276#> <#34277#>(remainder<#34277#> <#34278#>m<#34278#> <#34279#>n))<#34279#><#66373#> if <#66374#><#34280#>(;SPMlt;=<#34280#> <#34281#>m<#34281#> <#34282#>n)<#34282#><#66374#><#71480#>
<#34283#>(d<#34283#><#34284#>efine<#34284#> <#34285#>(gcd-generative<#34285#> <#34286#>n<#34286#> <#34287#>m)<#34287#>
<#34288#>(l<#34288#><#34289#>ocal<#34289#> <#34290#>((d<#34290#><#34291#>efine<#34291#> <#34292#>(clever-gcd<#34292#> <#34293#>larger<#34293#> <#34294#>smaller)<#34294#>
<#34295#>(c<#34295#><#34296#>ond<#34296#>
<#34297#>[<#34297#><#34298#>(=<#34298#> <#34299#>smaller<#34299#> <#34300#>0)<#34300#> <#34301#>larger]<#34301#>
<#34302#>[<#34302#><#34303#>else<#34303#> <#34304#>(clever-gcd<#34304#> <#34305#>smaller<#34305#> <#34306#>(remainder<#34306#> <#34307#>larger<#34307#> <#34308#>smaller))]<#34308#><#34309#>)))<#34309#>
<#34310#>(clever-gcd<#34310#> <#34311#>(max<#34311#> <#34312#>m<#34312#> <#34313#>n)<#34313#> <#34314#>(min<#34314#> <#34315#>m<#34315#> <#34316#>n))))<#34316#>
The <#66375#><#34320#>local<#34320#><#66375#> definition introduces the workhorse of the function:
<#66376#><#34321#>clever-gcd<#34321#><#66376#>, a function based on generative recursion. Its first
line discovers the trivially solvable case by comparing <#66377#><#34322#>smaller<#34322#><#66377#> to
<#66378#><#34323#>0<#34323#><#66378#> and produces the matching solution. The generative step uses
<#66379#><#34324#>smaller<#34324#><#66379#> as the new first argument and <#66380#><#34325#>(remainder<#34325#>\ <#34326#>larger<#34326#><#34327#> <#34327#><#34328#>smaller)<#34328#><#66380#> as the new second argument to <#66381#><#34329#>clever-gcd<#34329#><#66381#>, exploiting the
above equation.
If we now use <#66382#><#34330#>gcd-generative<#34330#><#66382#> with our complex example from above:
<#34335#>(gcd-generative<#34335#> <#34336#>101135853<#34336#> <#34337#>45014640)<#34337#>
we see that the response is nerly instantaneous. A hand-evaluation shows
that <#66383#><#34341#>clever-gcd<#34341#><#66383#> recurs only nine times before it produces the
solution: <#66384#><#34342#>177<#34342#><#66384#>. In short, the use of generative recursion has
helped find us a much faster solution to our problem.
<#34345#>Exercise 26.3.2<#34345#>
Formulate informal answers to the four key questions for
<#66385#><#34347#>gcd-generative<#34347#><#66385#>.~ Solution<#66386#><#66386#>
<#34353#>Exercise 26.3.3<#34353#>
Enter <#66387#><#34355#>gcd-generative<#34355#><#66387#> into the <#34356#>Definitions<#34356#> window and
evaluate <#66388#><#34357#>(time<#34357#>\ <#34358#>(gcd-generative<#34358#>\ <#34359#>101135853<#34359#>\ <#34360#>45014640))<#34360#><#66388#> in the
<#34361#>Interactions<#34361#> window.
Evaluate <#66389#><#34362#>(clever-gcd<#34362#>\ <#34363#>101135853<#34363#>\ <#34364#>45014640)<#34364#><#66389#> by hand. Show only those lines
that introduce a new recursive call to
<#66390#><#34365#>clever-gcd<#34365#><#66390#>.~ Solution<#66391#><#66391#>
<#34371#>Exercise 26.3.4<#34371#>
Formulate a termination argument for
<#66392#><#34373#>gcd-generative<#34373#><#66392#>.~ Solution<#66393#><#66393#>
Considering the above example, it is tempting to develop functions using
generative recursion. After all, they produce answers faster! This
judgement is too rash for three reasons. First, even a well-designed
algorithm isn't always faster than an equivalent structurally recursive
function. For example, <#66394#><#34381#>quick-sort<#34381#><#66394#> wins only for large lists; for
small ones, the standard <#66395#><#34382#>sort<#34382#><#66395#> function is faster. Worse, a badly
designed algorithm can wreck havoc on the performance of a program. Second,
it is typically easier to design a function using the recipe for structural
recursion. Conversely, designing an algorithm requires an idea of how to
generate new, smaller problems, a step that often requires deep
mathematical insight. Finally, people who read functions can easily
understand structurally recursive functions, even without much
documentation. To understand an algorithm, the generative step must be
well-explained, and even with a good explanation, it may still be difficult
to grasp the idea.
Experience shows that most functions in a program employ structural
recursion; only a few exploit generative recursion. When we encounter a
situation where a design could use either the recipe for structural or
generative recursion, the best approach is often to start with a structural
version. If it turns out to be too slow, the alternative design using
generative recursion should be explored. If it is chosen, it is important
to document the problem generation with good examples and to give a good
termination argument.
<#34385#>Exercise 26.3.5<#34385#>
Evaluate
<#34391#>(quick-sort<#34391#> <#34392#>(list<#34392#> <#34393#>10<#34393#> <#34394#>6<#34394#> <#34395#>8<#34395#> <#34396#>9<#34396#> <#34397#>14<#34397#> <#34398#>12<#34398#> <#34399#>3<#34399#> <#34400#>11<#34400#> <#34401#>14<#34401#> <#34402#>16<#34402#> <#34403#>2))<#34403#>
by hand. Show only those lines that introduce a new recursive call to
<#66396#><#34407#>quick-sort<#34407#><#66396#>. How many recursive applications of <#66397#><#34408#>quick-sort<#34408#><#66397#>
are required? How many recursive applications of <#66398#><#34409#>append<#34409#><#66398#>? Suggest a
general rule for a list of length <#66399#><#34410#>N<#34410#><#66399#>.
Evaluate
<#34415#>(quick-sort<#34415#> <#34416#>(list<#34416#> <#34417#>1<#34417#> <#34418#>2<#34418#> <#34419#>3<#34419#> <#34420#>4<#34420#> <#34421#>5<#34421#> <#34422#>6<#34422#> <#34423#>7<#34423#> <#34424#>8<#34424#> <#34425#>9<#34425#> <#34426#>10<#34426#> <#34427#>11<#34427#> <#34428#>12<#34428#> <#34429#>13<#34429#> <#34430#>14)<#34430#>
by hand. How many recursive applications of <#66400#><#34434#>quick-sort<#34434#><#66400#> are
required? How many recursive applications of <#66401#><#34435#>append<#34435#><#66401#>? Does this
contradict the first part of the exercise?~ Solution<#66402#><#66402#>
<#34441#>Exercise 26.3.6<#34441#>
Enter <#66403#><#34443#>sort<#34443#><#66403#> and <#66404#><#34444#>quick-sort<#34444#><#66404#> into the <#34445#>Definitions<#34445#>
window. Test the functions and then explore how fast each works on various
lists. The experiment should confirm the claim that the plain <#66405#><#34446#>sort<#34446#><#66405#>
function wins (in many comparisons) over <#66406#><#34447#>quick-sort<#34447#><#66406#> for short
lists and vice versa. Determine the cross-over point. Then build a
<#66407#><#34448#>sort-quick-sort<#34448#><#66407#> function that behaves like <#66408#><#34449#>quick-sort<#34449#><#66408#> for
large lists and switches over to the plain <#66409#><#34450#>sort<#34450#><#66409#> function for lists
below the cross-over point.
<#34451#>Hints:<#34451#> (1) Use the ideas of exercise~#exquicksorttime1#34452> to create
test cases. (2) Develop <#66410#><#34453#>create-tests<#34453#><#66410#>, a function that creates
large test cases randomly. Then evaluate
<#34458#>(define<#34458#> <#34459#>test-case<#34459#> <#34460#>(create-tests<#34460#> <#34461#>10000))<#34461#>
<#34462#>(collect-garbage)<#34462#>
<#34463#>(time<#34463#> <#34464#>(sort<#34464#> <#34465#>test-case))<#34465#>
<#34466#>(collect-garbage)<#34466#>
<#34467#>(time<#34467#> <#34468#>(quick-sort<#34468#> <#34469#>test-case))<#34469#>
The uses of <#66411#><#34473#>collect-garbage<#34473#><#66411#> helps DrScheme deal with large
lists.~ Solution<#66412#><#66412#>