Tài liệu hạn chế xem trước, để xem đầy đủ mời bạn chọn Tải xuống
1
/ 15 trang
THÔNG TIN TÀI LIỆU
Thông tin cơ bản
Định dạng
Số trang
15
Dung lượng
200,08 KB
Nội dung
840 Patterns and UML state. The Controller is the input part; it relays commands from the user to the Model. Normally each of the three interacting parts is realized as an object with responsibilities for its own tasks. The Model-View-Controller pattern is an example of a divide-and- conquer strategy: One big task is divided into three smaller tasks with well-defined responsibilities. Display 20.1 diagrams the Model-View-Controller pattern. As a very simple example, the Model might be a container class, such as a stack. The View might display the top of the stack. The Controller gives commands to push or pop data on the stack. The Model (the stack) notifies the View to display a new top-of- stack value whenever the stack contents change. Any application can be made to fit the Model-View-Controller pattern, but it is par- ticularly well suited to GUI (graphical user interface) design projects, where the View can indeed be a visualization of the state of the Model. (A GUI is simply a windowing interface of the form you find in most modern software applications, as opposed to the simple text I/O we have used in this book.) For example, the Model might be an object to represent your list of computer desktop object names. The View could then be a GUI object that produces a screen display of your desktop icons. The Controller relays commands to the Model (which is a desktop object) to add or delete names. The Model object notifies the View object when the screen needs to be updated. We have presented the Model-View-Controller pattern as if the user were the Con- troller, primarily to simplify the example. The Controller need not be under the direct control of the user, but could be some other kind of software or hardware component. Display 20.1 Model-View-Controller Pattern Controller ManipulateNotify action1( ) action2( ) . . . data1 data2 . . . Model update( ) View 20_CH20.fm Page 840 Monday, August 18, 2003 2:08 PM Patterns 841 Example A S ORTING P ATTERN The most efficient sorting algorithms all seem to follow a similar pattern. Expressed recursively, they divide the list of elements to be sorted into two smaller lists, recursively sort the two smaller lists, and then recombine the two sorted lists to obtain the final sorted list. In Display 20.2 this pattern is expressed as a template function to sort an array into increasing order using the < operator. Our Sorting pattern uses a divide-and-conquer strategy. It divides the entire collection of ele- ments to be sorted into two smaller collections, sorts the smaller collections by recursive calls, and then combines the two sorted collections to obtain the final sorted array. The following is the heart of our Sorting pattern: int splitPt = split(a, begin, end); sort(a, begin, splitPt); sort(a, splitPt, end); join(a, begin, splitPt, end); Although the pattern imposes some minimum requirements on the functions split and join, the pattern does not say exactly how the functions split and join are defined. Different defini- tions of split and join will yield different sorting algorithms. Array indexes are examples of iterators, and we will use the notation [begin, end) from Chapter 19 to comment the pattern and indeed to think about and derive the pattern. The function split rearranges the elements in the interval [begin, end) and then divides the interval at a split point, splitPt. The two smaller intervals [begin, splitPt) and [splitPt, end) are then sorted by a recursive call to the function sort. Note that the split function both rearranges the elements in the array interval [begin, end) and returns the index splitPt that divides the interval [begin, end). After the two smaller intervals are sorted, the function join then combines the two sorted intervals to obtain the final sorted version of the entire larger interval. The pattern says nothing about how the function split rearranges and divides the interval [begin, end). In a simple case, split might simply choose a value splitPt between begin and end and divide the interval into the points before splitPt and the points after splitPt, with no rearranging. We will see an example that realizes the Sorting pattern by defining split this way. On the other hand, the function split could do something more elaborate, such as moving all the “small” elements to the front of the array and all the “large” elements toward the end of the array. This would be a step on the way to fully sorting the values. We will also see an example that realizes the Sorting pattern in this second way. The simplest realization of this Sorting pattern is the mm mm ee ee rr rr gg gg ee ee ss ss oo oo rr rr tt tt realization given in Display 20.3. A test program is given in Display 20.4. The merge sort is an example in which the definition of split is very simple. It divides the array into two intervals with no rearranging of elements. The join function is more complicated. After the two subintervals are sorted, it merges the two sorted subintervals, copying elements from the array to a temporary array. The merging starts by comparing the smallest elements in each smaller sorted interval. The smaller of these two ele- merge sort 20_CH20.fm Page 841 Monday, August 18, 2003 2:08 PM 842 Patterns and UML Display 20.2 Divide-and-Conquer Sorting Pattern 1 //This is the file sortpattern.cpp. 2 template <class T> 3 int split(T a[], int begin, int end); 4 //Rearranges elements [begin, end) of array a into two intervals 5 //[begin, splitPt) and [splitPt, end), such that the Sorting pattern works. 6 //Returns splitPt. 7 template <class T> 8 void join(T a[], int begin, int splitPt, int end); 9 //Combines the elements in the two intervals [begin, split) and 10 //[splitPt, end) in such a way that the Sorting pattern works. 11 template <class T> 12 void sort(T a[], int begin, int end) 13 //Precondition: Interval [begin, end) of a has elements. 14 //Postcondition: The values in the interval [begin, end) have 15 //been rearranged so that a[0] <= a[1] <= <= a[(end - begin) - 1]. 16 { 17 if ((end - begin) > 1) 18 { 19 int splitPt = split(a, begin, end); 20 sort(a, begin, splitPt); 21 sort(a, splitPt, end); 22 join(a, begin, splitPt, end); 23 }//else sorting one (or fewer) elements, so do nothing. 24 } 25 template <class T> 26 void sort(T a[], int numberUsed) 27 //Precondition: numberUsed <= declared size of the array a. 28 //The array elements a[0] through a[numberUsed - 1] have values. 29 //Postcondition: The values of a[0] through a[numberUsed - 1] have 30 //been rearranged so that a[0] <= a[1] <= <= a[numberUsed - 1]. 31 { 32 sort(a, 0, numberUsed); 33 } Display 20.3 Merge Sort Realization of Sort Pattern (part 1 of 2) 1 //File mergesort.cpp: the merge sort realization of the Sorting pattern. 2 template <class T> 3 int split(T a[], int begin, int end) 4 { 5 return ((begin + end)/2); 6 } 20_CH20.fm Page 842 Monday, August 18, 2003 2:08 PM Patterns 843 Display 20.3 Merge Sort Realization of Sort Pattern (part 2 of 2) 7 template <class T> 8 void join(T a[], int begin, int splitPt, int end) 9 { 10 T *temp; 11 int intervalSize = (end - begin); 12 temp = new T[intervalSize]; 13 int nextLeft = begin; //index for first chunk 14 int nextRight = splitPt; //index for second chunk 15 int i = 0; //index for temp 16 //Merge till one side is exhausted: 17 while ((nextLeft < splitPt) && (nextRight < end)) 18 { 19 if (a[nextLeft] < a[nextRight]) 20 { 21 temp[i] = a[nextLeft]; 22 i++; nextLeft++; 23 } 24 else 25 { 26 temp[i] = a[nextRight]; 27 i++; nextRight++; 28 } 29 } 30 31 while (nextLeft < splitPt)//Copy rest of left chunk, if any. 32 { 33 temp[i] = a[nextLeft]; 34 i++; nextLeft++; 35 } 36 while (nextRight < end) //Copy rest of right chunk, if any. 37 { 38 temp[i] = a[nextRight]; 39 i++; nextRight++; 40 } 41 for (i = 0; i < intervalSize; i++) 42 a[begin + i] = temp[i]; 43 } 20_CH20.fm Page 843 Monday, August 18, 2003 2:08 PM 844 Patterns and UML Display 20.4 Demonstrating the Sorting Pattern 1 //Tests the Divide-and-Conquer Sorting pattern. 2 #include <iostream> 3 using std::cout; 4 using std::cin; 5 using std::endl; 6 #include "sortpattern.cpp" 7 #include "mergesort.cpp" 8 void fillArray(int a[], int size, int& numberUsed); 9 //Precondition: size is the declared size of the array a. 10 //Postcondition: numberUsed is the number of values stored in a. 11 //a[0] through a[numberUsed - 1] have been filled with 12 //nonnegative integers read from the keyboard. 13 14 int main( ) 15 { 16 cout << "This program sorts numbers from lowest to highest.\n"; 17 int sampleArray[10], numberUsed; 18 fillArray(sampleArray, 10, numberUsed); 19 sort(sampleArray, numberUsed); 20 cout << "In sorted order the numbers are:\n"; 21 for (int index = 0; index < numberUsed; index++) 22 cout << sampleArray[index] << " "; 23 cout << endl; 24 return 0; 25 } 26 void fillArray(int a[], int size, int& numberUsed) 27 < The rest of the definition of fillArray is given in Display 5.5. > S AMPLE D IALOGUE This program sorts numbers from lowest to highest. Enter up to 10 nonnegative whole numbers. Mark the end of the list with a negative number. 80 30 50 70 60 90 20 30 40 -1 In sorted order the numbers are: 20 30 30 40 50 60 70 80 90 20_CH20.fm Page 844 Monday, August 18, 2003 2:08 PM Patterns 845 ments is the smallest of all the elements in either subinterval, and so it is moved to the first posi- tion in the temporary array. The process is then repeated with the remaining elements in the two smaller sorted intervals to find the next smallest element, and so forth. There is a trade-off between the complexity of the functions split and join. You can make either of them simple at the expense of making the other more complicated. For the merge sort, split was simple and join was complicated. We next give a realization in which split is com- plicated and join is simple. Display 20.5 gives the qq qq uu uu ii ii cc cc kk kk ss ss oo oo rr rr tt tt realization of our Sorting pattern. If the line #include "mergesort.cpp" in Display 20.4 is replaced by the following: #include "quicksort.cpp" then the program will give the same input and output. The files mergesort.cpp and quick- sort.cpp give two different realizations of the same Sorting pattern. In the quick-sort realization, the definition of split is quite sophisticated. An arbitrary value in the array is chosen; this value is called the ss ss pp pp ll ll ii ii tt tt tt tt ii ii nn nn gg gg vv vv aa aa ll ll uu uu ee ee . In our realization we chose a[begin], as the splitting value, but any value will do equally well. The elements in the array are rearranged so that all those elements that are less than or equal to the splitting value are at the front of the array and all the values that are greater than the splitting value are at the other end of the array; the splitting value is placed so that it divides the entire array into these smaller and larger ele- ments. Note that the smaller elements are not sorted and the larger elements are not sorted, but all the elements before the splitting value are smaller than any of the elements after the splitting value. The smaller elements are sorted by a recursive call, the larger elements are sorted by another recursive call, and then these two sorted segments are combined with the join function. In this case the join function is as simple as it possibly could be: It does nothing. Since the sorted smaller elements all preceed the sorted larger elements, the entire array is sorted. (The quick-sort realization can be done without the use of a second temporary array (temp). However, that detail would only distract from the message of this example. In a real application, you may or may not, depending on details, want to consider the possibility of using the quick- sort realization without a temporary array.) ■ EFFICIENCY OF THE SORTING PATTERN Essentially any sorting algorithm can be realized using this Sorting pattern. However, the most efficient implementations are those for which the split function divides the array into two substantial chunks, such as half and half, or one-fourth and three- fourths. A realization of split that divides the array into one, or a very few, elements and the rest of the array will not be very efficient. quick sort splitting value 20_CH20.fm Page 845 Monday, August 18, 2003 2:08 PM 846 Patterns and UML Display 20.5 Quick-Sort Realization of Sorting Pattern (part 1 of 2) 1 //File quicksort.cpp: the quick-sort realization of the Sorting pattern. 2 #include <algorithm> 3 using std::swap; 4 template <class T> 5 int split(T a[], int begin, int end) 6 { 7 T *temp; 8 int size = (end - begin); 9 temp = new T[size]; 10 T splitV = a[begin]; 11 int up = 0; 12 int down = size - 1; 13 //Note that a[begin] = splitV is skipped. 14 for (int i = begin + 1; i < end; i++) 15 { 16 if (a[i] <= splitV) 17 { 18 temp[up] = a[i]; 19 up++; 20 } 21 else 22 { 23 temp[down] = a[i]; 24 down ; 25 } 26 } 27 //0 <= up = down < size 28 temp[up] = a[begin]; //Positions the split value, splitV. 29 //temp[i] <= splitV for i < up; temp[up] = splitV; temp[i] > splitV for 30 //i > up. So, temp[i] <= temp[j] for i in [0, up) and j in [up, end). 31 for (i = 0; i < size; i++) 32 a[begin + i] = temp[i]; 33 34 if (up > 0) 35 return (begin + up); 36 else 37 return (begin + 1); //Ensures that both pieces are nonempty. 38 } 20_CH20.fm Page 846 Monday, August 18, 2003 2:08 PM Patterns 847 Tip For example, the merge sort realization of split divides the array into two roughly equal parts, and a merge sort is indeed very efficient. It can be shown, although we will not do so here, that the merge sort runs in time O(N log N) and that any sorting algo- rithm (that meets some minimal and reasonable constraints) cannot be faster than O(N log N). So, a merge sort is, in some sense, optimal. The quick-sort realization of split divides the array into two portions that might be almost equal or might be very different in size depending on the choice of a splitting value. Since in extremely unfortunate cases the split might be very uneven, the worst- case running time for a quick sort is O(N 2 ), which is much slower than the O(N log N) we obtained for the merge sort. However, for an array that is filled with randomly cho- sen values, most splitting values will produce a division that is close enough to an even division. So, under a suitable definition of average case, the quick sort has an average- case running time that is O(N log N). In practice, the quick sort is one of the best- performing sorting algorithms. The selection sort algorithm, which we discussed in Chapter 5, divides the array into two pieces, one with a single element and one with the rest of the array interval (see Self-Test Exercise 2). Because of this uneven division, the selection sort has a worst-case, and even average-case, running time that is O(N 2 ). In practice, the selec- tion sort is not a very fast sorting algorithm, although it does have the virtue of simplic- ity. We do not have room to provide proof of these running times in this book, but you can find such results in almost any data structures or analysis of algorithms text. P RAGMATICS AND P ATTERNS You should not feel compelled to follow all the fine details of a pattern. Patterns are guides, not requirements. For example, we did the quick-sort implementation by exactly following the pat- tern in order to have a clean example. In practice, we would have taken some liberties. Notice that, with a quick sort, the join function does nothing. In practice, we would simply eliminate the calls to join. These calls incur overhead and accomplish nothing. Display 20.5 Quick-Sort Realization of Sorting Pattern (part 2 of 2) 39 template <class T> 40 void join(T a[], int begin, int splitPt, int end) 41 { 42 //Nothing to do. 43 } 44 20_CH20.fm Page 847 Monday, August 18, 2003 2:08 PM 848 Patterns and UML Self-Test Exercises The Divide-and-Conquer Sorting pattern must divide the interval being sorted into two smaller intervals. If there were cases in which the split function divided the interval into an empty interval and the rest, then one subinterval would be the same as the original interval being divided, and infinite recursion would result. In the quick-sort realization we avoided this infinite recursion with the following lines from the definition of split: if (up > 0) return (begin + up); else return (begin + 1); //Ensures that both pieces are nonempty. Without this extra adjustment, the function split could compute 0 as the value of up and so divide the interval into two intervals with one of the two being empty. That would produce infi- nite recursion. The way this is usually avoided with a quick-sort realization (and the way that pro- duces the nicest code) is to separate out the split point and divide only the remaining element, so that the array interval is divided into three pieces: the split point, the subinterval before the split point, and the subinterval after the split point. This guarantees that even if one subinterval is empty, the other is shorter than the interval being divided. Thus, infinite recursion is avoided. When these points are taken into consideration, you are likely to change the Sorting pattern to the following when you are designing the quick-sort realization: if ((end - begin) > 1) { int splitPt = split(a, begin, end); sort(a, begin, splitPt - 1); sort(a, splitPt + 1, end); }//else sorting one (or fewer) elements, so do nothing. Patterns are there to help you, not to provide an obstacle. Feel free to adjust them if need be. ■ PATTERN FORMALISM A well-developed body of techniques exists for using patterns. We will not go into the details here. The UML discussed in Section 20.2 is one formalism used to express pat- terns. The place of patterns and any specific formalisms for patterns within the software design process is not yet clear. However, it is clear that basic patterns—as well as certain pattern names, such as Model-View-Controller—have become standard and useful tools for software design. 1. Is a template function definition a pattern? 2. Give the contents of a file named selectionsort.cpp that will realize the selection sort algorithm (see Display 5.8) for the Divide-and-Conquer Sorting pattern given in Display 20.2. (This is the selection sort analog of what was done for the quick sort in Display 20.5.) 20_CH20.fm Page 848 Monday, August 18, 2003 2:08 PM UML 849 3. Which of the following would give the fastest running time when an array is sorted using the quick-sort algorithm: a fully sorted array, an array of random values, or an array sorted from largest to smallest (that is, sorted backwards)? Assume that all arrays are of the same size and have the same base type. UML One picture is worth a thousand words. Chinese proverb People do not think in C++ or in any other programming language. As a result, com- puter scientists have always sought to produce more human-oriented ways of represent- ing programs. One widely used representation is pseudocode, which is a mixture of a programming language such as C++ and a natural language such as English. To think about a programming problem without needing to worry about the syntax details of a language such as C++, you can simply relax the syntax rules and write in pseudocode. Pseudocode has become a standard tool used by programmers, but it is a linear and algebraic representation of programming. Computer scientists have long sought to give software design a graphical representation. To this end, a number of graphical represen- tation systems for program design have been proposed, used, and ultimately found to be wanting. Terms such as flowchart, structure diagram, and many other names of graphical program representations are today only recognized by those of the older gen- eration. Today’s candidate for a graphical representation formalism is the Unified Modeling Language (UML). The UML was designed to reflect and be used with the object-oriented programming philosophy. It is too early to say whether or not the UML will stand the test of time, but it is off to a good start. A number of companies have adopted the UML formalism for use in their software design projects. ■ HISTORY OF UML UML developed along with object-oriented programming. As the OOP philosophy became more and more commonly used, different groups developed their own graphi- cal or other representations for OOP design. In 1996 Grady Booch, Ivar Jacobson, and James Rumbaugh released an early version of UML. The UML was intended to bring together the various different graphical representation methods to produce a standard- ized graphical representation language for object-oriented design and documentation. Since that time the UML has been developed and revised in response to feedback from the OOP community. Today the UML standard is maintained and certified by the Object Management Group (OMG), a nonprofit organization that promotes the use of object-oriented techniques. 20.2 Unified Modeling Language (UML) 20_CH20.fm Page 849 Monday, August 18, 2003 2:08 PM . a programming language such as C++ and a natural language such as English. To think about a programming problem without needing to worry about the syntax details of a language such as C++, you can simply. same base type. UML One picture is worth a thousand words. Chinese proverb People do not think in C++ or in any other programming language. As a result, com- puter scientists have always sought