-------------------------------------------------------------------------- COMP 731 - Data Struc & Algorithms - Lecture 15 - Thursday, August 3, 2000 -------------------------------------------------------------------------- Today: o Program abstraction / programming style - a solution to PS3 I was pleased that people managed to get the programs done and working. But I felt that the programs were, without exception, poorly abstracted ;-( So: the first part of today's lecture was spent discussing some general guidelines for the writing of better programs. Among the guidelines: 1. Make the structure of the program mirror the high-level ideas of the problem; find clear abstraction boundaries! This is the most difficult aspect of programming, the most important, and the most intangible and difficult to teach. You have to find the "good", "clean" abstraction boundaries. 2. Use the features of the language you are working in. In particular, when writing a C++ program, use classes to implement ADTs. I don't recommend use of inheritance for programs of the sort we do in this class -- but proper use of classes will result in nicer programs. 3. Avoid excessive use of global variables. Have your programs communicate through arguments/messages passing. 4. Choose your data structures carefully, weighing all the possibilities and options. 5. Comment code appropriately. 6. Make sure that one subroutine is doing ONE thing. This one thing should be clear enough to describe in a brief comment. This is of course part of (1). 7. Keep the code of any one routine short -- normally, less than one page. 8. Express ideas "directly" in the programming language, sticking to common conventions (eg., the C/C++ use of 0 for false, nonzero for true -- not, eg., the characters "t" and "f"). 9. Make sure you spend time on what matters. Use a profiler to see where execution time is being spent. For example, in program 2, I believe that most of the time is now being spent in on the pseudorandom number generator, so optimizing other parts of the program won't much impact performance. I now went over my solution to program 3. It's not perfect (eg., I should probably be using constructors instead of Makeset() and AddVerticies(), but hopefully it at least illustrates finding better abstractin boundaries. I omit void error (char* s) and int random(int n) (the program won't run until you add these routines). I wrote these, but you really should probably take these from a library, instead. _________________________________________________________________________________ /* * prog2.cpp * * Estimate F(n) for various values of n * Phillip Rogaway * COMP 731 - Term 1 of 2000 */ #include const nmax = 1000; // maximum size graph program can handle. Also max number of sets // // The standard "union/find data structure", implemented using union-by-rank (but // not bothering with collapsing-find). The elements in the universe have names 1..N. // Union allows arbitrary names. We also keep track of the number of disjoint // sets in the collection, and return this value as a member function. // class Sets { struct Pair { int parent; int rank; }; Pair L[nmax+1]; int N; // elements have names 1..N, where N<=nmax int num_disjoint_sets; // number of disjoint sets public: void Makesets(int n); // make n disjoint sets. (should be constructor) int Find(int x); // return the canonical name for the set containing x void Union(int x, int y) // union the sets containing x and y int NumDisjointSets() {return num_disjoint_sets;} // number of disjoint sets }; void Sets::Makesets(int n) { if (n<1 || n>nmax) error("Sets::Makesets() - Bad argument"); N = n; num_disjoint_sets = n; for (int i=1; i<=n; i++) { L[i].parent = i; L[i].rank = 0; } } int Sets::Find(int x) { if (x<1 || x>N) error("Sets::Find() - Bad argument"); while (L[x].parent!=x) x = L[x].parent; return x; } void Sets::Union(int x, int y) { if (x<1 || x>N || y<1 || y>N) error("Sets::Union() - Bad argument"); int a = Find(x); int b = Find(y); if (a==b) return; num_disjoint_sets--; if (L[a].rank > L[b].rank) {L[b].parent = a;} else if (L[b].rank < L[a].rank) {L[a].parent = b;} else {L[b].parent = a; L[a].rank++; } } // // A graph G=(V,E) is represented by n=|V|, m=|E|, and a compacted adjacency // matrix, which records, as a seqeunce of bits, Adj[1,2], Adj[1,3], ..., Adj[n-1,n]. // We also maintain the set of components of G, as a set of sets, s. // class Graph { int n; // number of verticies int m; // number of edges unsigned long A[nmax*(nmax-1)/2 / 32 + 1]; // compacted adjacency list Sets s; public: int NumEdges() {return m;} //returns the number of edges in the graph void AddVerticies(int N); //replace the graph by N isolated verticies int HasEdge(int i, int j); //return true iff {i,j} is an edge of the graph void AddEdge(int i, int j); //modify the graph by adding edge {i,j} void AddRandomEdge(); //modify graph by adding a random absent edge int NumComponents() {return s.NumDisjointSets();} //return # of components }; void Graph::AddVerticies(int N) { n = N; m = 0; s.Makesets(N); for (int i=0; i <= n*(n-1)/2/32; i++) { A[i] = 0; } } int Graph::HasEdge(int i, int j) { if (i<1 || i>n || j<1 || j>n) error("Graph::HasEdge - Bad arguments"); if (i>j) {int temp = i; i=j; j=temp;} if (i==j) return 0; int pos = (i-1)*n - i*(i-1)/2 + j-i-1; int high = pos >> 5; int low = pos & 0x1f; return ((A[high] & (1<n || j<1 || j>n) error("Graph::AddEdge - Bad arguments"); if (i==j) error("Graph::AddEdge - Attempt to add a self loop"); if (i>j) {int temp = i; i=j; j=temp;} int pos = (i-1)*n - i*(i-1)/2 + j-i-1; int high = pos >> 5; int low = pos & 0x1f; if ((A[high] & (1<1) G.AddRandomEdge(); nedges += G.NumEdges(); } return (double)nedges/ntimes; } /* * main */ void main (int argc, char** argv) { cout << "COMP 731 - Assignment 3" << endl << "Program to estimate F(n) values" << endl << endl; cout << "Estimate for F(4) = " << EstimateF(4) << endl; for (int n=10; n<=100; n+=10) { cout << "Estimate for F(" << n << ") = " << EstimateF(n) << endl; } for (n=200; n<=1000; n+=100) { cout << "Estimate for F(" << n << ") = " << EstimateF(n) << endl; } }