0% found this document useful (0 votes)
4 views28 pages

Unit 4

Chapter 3 focuses on sets and maps as abstract data types, emphasizing their importance in programming and the need to understand their implementations. It details the Set ADT, which stores unique values and supports various operations like union, intersection, and difference, along with a list-based implementation. The chapter also discusses the selection of appropriate data structures for implementing sets and provides examples of their usage in Python.

Uploaded by

shriyampatil
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
4 views28 pages

Unit 4

Chapter 3 focuses on sets and maps as abstract data types, emphasizing their importance in programming and the need to understand their implementations. It details the Set ADT, which stores unique values and supports various operations like union, intersection, and difference, along with a list-based implementation. The chapter also discusses the selection of appropriate data structures for implementing sets and provides examples of their usage in Python.

Uploaded by

shriyampatil
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
You are on page 1/ 28

CHAPTER 3

Sets and Maps

In the previous chapters, we studied several complex abstract data types that
required the use of a data structure for their implementation. In this chapter, we
continue exploring abstract data types with a focus on several common containers.
Two of these are provided by Python as part of the language itself: sets and
dictionaries. Nevertheless, it’s still important to understand how they work and
some of the common ways in which they are implemented.
Your experience in programming will likely not be limited to the Python lan-
guage. At some point in the future, you may use one if not several other common
programming languages. While some of these do provide a wide range of abstract
data types as part of the language itself or included in their standard library, oth-
ers, like C, do not. Thus, it’s important that you know how to implement a set or
dictionary ADT if necessary, when one is not available as part of the language.
Further, both the set and dictionary types provide excellent examples of ab-
stract data types that can be implemented using different data structures. As you
learned in Chapter 1, there may be multiple data structures and ways to organize
the data in those structures that are suitable for implementing an abstract data
type. Thus, it’s not uncommon for language libraries to provide multiple imple-
mentations of an abstract data type, which allows the programmer to choose the
best option for a given problem. Your ability to choose from among these various
implementations will depend not only on your knowledge of the abstract data type
itself, but also on understanding the pros and cons of the various implementations.

3.1 Sets
The Set ADT is a common container used in computer science. But unlike the
Bag ADT introduced in Chapter 1, a set stores unique values and represents the
same structure found in mathematics. It is commonly used when you need to store
a collection of unique values without regard to how they are stored or when you
need to perform various mathematical set operations on collections.

69
70 CHAPTER 3 Sets and Maps

3.1.1 The Set Abstract Data Type


The definition of the set abstract data type is provided here, followed by an im-
plementation using a list. In later chapters, we will provide and evaluate alternate
implementations for the Set ADT.

Define Set ADT

A set is a container that stores a collection of unique values over a given comparable
domain in which the stored values have no particular ordering.
 Set(): Creates a new set initialized to the empty set.

 length (): Returns the number of elements in the set, also known as the
cardinality. Accessed using the len() function.

 contains ( element ): Determines if the given value is an element of the set


and returns the appropriate boolean value. Accessed using the in operator.

 add( element ): Modifies the set by adding the given value or element to the
set if the element is not already a member. If the element is not unique, no
action is taken and the operation is skipped.

 remove( element ): Removes the given value from the set if the value is con-
tained in the set and raises an exception otherwise.

 equals ( setB ): Determines if the set is equal to another set and returns a
boolean value. For two sets, A and B, to be equal, both A and B must contain
the same number of elements and all elements in A must also be elements in
B. If both sets are empty, the sets are equal. Access with == or !=.

 isSubsetOf( setB ): Determines if the set is a subset of another set and re-
turns a boolean value. For set A to be a subset of B, all elements in A must
also be elements in B.

 union( setB ): Creates and returns a new set that is the union of this set and
setB. The new set created from the union of two sets, A and B, contains all
elements in A plus those elements in B that are not in A. Neither set A nor
set B is modified by this operation.

 intersect( setB ): Creates and returns a new set that is the intersection
of this set and setB. The intersection of sets A and B contains only those
elements that are in both A and B. Neither set A nor set B is modified by
this operation.

 difference( setB ): Creates and returns a new set that is the difference of
this set and setB. The set difference, A − B, contains only those elements that
are in A but not in B. Neither set A nor set B is modified by this operation.
3.1 Sets 71

 iterator (): Creates and returns an iterator that can be used to iterate over
the collection of items.

Example Use
To illustrate the use of the Set ADT, we create and use sets containing the courses
currently being taken by two students. In the following code segment, we create
two sets and add elements to each. The results are illustrated in Figure 3.1.

smith = Set()
smith.add( "CSCI-112" )
smith.add( "MATH-121" )
smith.add( "HIST-340" )
smith.add( "ECON-101" )

roberts = Set()
roberts.add( "POL-101" )
roberts.add( "ANTH-230" )
roberts.add( "CSCI-112" )
roberts.add( "ECON-101" )

smith set roberts set

“CSCI-112” “CSCI-112”
“MATH-121” “POL-101”
“ECON-101” “ECON-101”
“HIST-340” “ANTH-230”

Figure 3.1: Abstract view of the two sample sets.

Next, we determine if the two students are taking the exact same courses. If
not, then we want to know if they are taking any of the same courses. We can do
this by computing the intersection between the two sets.

if smith == roberts :
print( "Smith and Roberts are taking the same courses." )
else :
sameCourses = smith.intersection( roberts )
if sameCourses.isEmpty() :
print( "Smith and Roberts are not taking any of "\
+ "the same courses." )
else :
print( "Smith and Roberts are taking some of the "\
+ "same courses:" )
for course in sameCourses :
print( course )
72 CHAPTER 3 Sets and Maps

In this case, the two students are both taking CSCI-112 and ECON-101. Thus,
the results of executing the previous code segment will be

Smith and Roberts are taking some of the same courses:


CSCI-112 ECON-101

Suppose we want to know which courses Smith is taking that Roberts is not
taking. We can determine this using the set difference operation:

uniqueCourses = smith.difference( roberts )


for course in sameCourses :
print( course )

This example reinforces one of the advantages of working with an abstraction


by focusing on what functionality the ADT provides instead of how that function-
ality is implemented. By hiding the implementation details, we can use an ADT
independent of its implementation. In fact, the choice of implementation for the
Set ADT will have no effect on the instructions in our example program.

3.1.2 Selecting a Data Structure


To implement the Set ADT, we must select a data structure based on the same
criteria we used for the Bag ADT from Chapter 1. Since we are trying to replicate
the functionality of the set structure provided by Python, we don’t want to use that
structure. That leaves the array, list, and dictionary containers for consideration
in implementing the Set ADT. The storage requirements for the bag and set are
very similar with the difference being that a set cannot contain duplicates. The
dictionary would seem to be the ideal choice since it can store unique items, but it
would waste space in this case. Remember, the dictionary stores key/value pairs,
which requires two data fields per entry. We could store the individual items of the
set in the key fields and leave the value fields empty, but that would use twice the
amount of storage than necessary. This waste does not occur with an array or list.
An array could be used to implement the set, but a set can contain any number
of elements and by definition an array has a fixed size. To use the array structure,
we would have to manage the expansion of the array when necessary in the same
fashion as it’s done for the list. Since the list can grow as needed, it seems ideal
for storing the elements of a set just as it was for the bag and it does provide for
the complete functionality of the ADT. Since the list allows for duplicate values,
however, we must make sure as part of the implementation that no duplicates are
added to our set.

3.1.3 List-Based Implementation


Having selected the list structure, we can now implement the Set ADT as shown
in Listing 3.1. Some of the operations of the set are very similar to those of the
Bag ADT and are implemented in a similar fashion. Sample instances for the two
sets from Figure 3.1 are illustrated in Figure 3.2.
3.1 Sets 73

smith roberts
theElements  theElements 
 
Set Set
0  “CSCI-112”
“CSCI-112” 0  “POL-101”
“POL-101”

1  “MATH-121”
“MATH-121” 1  “ANTH-230”
“ANTH-230”

2  “HIST-340”
“HIST-340” 2  “CSCI-112”
“CSCI-112”

3  “ECON-101”
“ECON-101” 3  “ECON-101”
“ECON-101”

Figure 3.2: Two instances of the Set class implemented as a list.

Listing 3.1 The linearset.py module.

1 # Implementation of the Set ADT container using a Python list.


2 class Set :
3 # Creates an empty set instance.
4 def __init__( self ):
5 self._theElements = list()
6
7 # Returns the number of items in the set.
8 def __len__( self ):
9 return len( self._theElements )
10
11 # Determines if an element is in the set.
12 def __contains__( self, element ):
13 return element in self._theElements
14
15 # Adds a new unique element to the set.
16 def add( self, element ):
17 if element not in self :
18 self._theElements.append( element )
19
20 # Removes an element from the set.
21 def remove( self, element ):
22 assert element in self, "The element must be in the set."
23 self._theElements.remove( item )
24
25 # Determines if two sets are equal.
26 def __eq__( self, setB ):
27 if len( self ) != len( setB ) :
28 return False
29 else :
30 return self.isSubsetOf( setB )
31
(Listing Continued)
74 CHAPTER 3 Sets and Maps

Listing 3.1 Continued . . .


32 # Determines if this set is a subset of setB.
33 def isSubsetOf( self, setB ):
34 for element in self :
35 if element not in setB :
36 return False
37 return True
38
39 # Creates a new set from the union of this set and setB.
40 def union( self, setB ):
41 newSet = Set()
42 newSet._theElements.extend( self._theElements )
43 for element in setB :
44 if element not in self :
45 newSet._theElements.append( element )
46 return newSet
47
48 # Creates a new set from the intersection: self set and setB.
49 def interset( self, setB ):
50 ......
51
52 # Creates a new set from the difference: self set and setB.
53 def difference( self, setB ):
54 ......
55
56 # Returns an iterator for traversing the list of items.
57 def __iter__( self ):
58 return _SetIterator( self._theElements )

Adding Elements
As indicated earlier, we must ensure that duplicate values are not added to the set
since the list structure does not handle this for us. When implementing the add
method, shown in lines 16–18, we must first determine if the supplied element is
already in the list or not. If the element is not a duplicate, we can simply append
the value to the end of the list; if the element is a duplicate, we do nothing. The
reason for this is that the definition of the add() operation indicates no action
is taken when an attempt is made to add a duplicate value. This is known as a
noop, which is short for no operation and indicates no action is taken. Noops are
appropriate in some cases, which will be stated implicitly in the definition of an
abstract data type by indicating no action is to be taken when the precondition
fails as we did with the add() operation.

Comparing Two Sets


For the operations that require a second set as an argument, we can use the oper-
ations of the Set ADT itself to access and manipulate the data of the second set.
Consider the “equals” operation, implemented in lines 26–30 of Listing 3.1, which
determines if both sets contain the exact same elements. We first check to make
3.2 Maps 75

Avoid Reinventing the Wheel. Using operations provided by an ADT


to implement other methods of that same ADT allows you to take ad-

TIP
vantage of the abstraction and avoid “reinventing the wheel” by duplicating
code in several places.

sure the two sets contain the same number of elements; otherwise, they cannot be
equal. It would be inefficient to compare the individual elements since we already
know the two sets cannot be equal. After verifying the size of the lists, we can test
to see if the self set is a subset of setB by calling self.isSubsetOf(setB). This
is a valid test since two equal sets are subsets of each other and we already know
they are of the same size.
To determine if one set is the subset of another, we can iterate over the list
of elements in the self set and make sure each is contained in setB. If just one
element in the self set is not in setB, then it is not a subset. The implementation
of the isSubsetOf() method is shown in lines 33–37.

The Set Union


Some of the operations create and return a new set based on the original, but the
original is not modified. This is accomplished by creating a new set and populating
it with the appropriate data from the other sets. Consider the union() method,
shown in lines 40–46, which creates a new set from the self set and setB passed
as an argument to the method.
Creating a new set, populated with the unique elements of the other two sets,
requires three steps: (1) create a new set; (2) fill the newSet with the elements
from setB; and (3) iterate through the elements of the self set, during which each
element is added to the newSet if that element is not in setB. For the first step,
we simply create a new instance of the Set class. The second step is accomplished
with the use of the list extend() method. It directly copies the entire contents
of the list used to store the elements of the self set to the list used to store the
elements of the newSet. For the final step, we iterate through the elements of
setB and add those elements to the the newSet that are not in the self set. The
unique elements are added to the newSet by appending them to the list used to
store the elements of the newSet. The remaining operations of the Set ADT can
be implemented in a similar fashion and are left as exercises.

3.2 Maps
Searching for data items based on unique key values is a very common application
in computer science. An abstract data type that provides this type of search
capability is often referred to as a map or dictionary since it maps a key to
a corresponding value. Consider the problem of a university registrar having to
manage and process large volumes of data related to students. To keep track of the
information or records of data, the registrar assigns a unique student identification
76 CHAPTER 3 Sets and Maps

number to each individual student as illustrated in Figure 3.3. Later, when the
registrar needs to search for a student’s information, the identification number is
used. Using this keyed approach allows access to a specific student record. If
the names were used to identify the records instead, then what happens when
multiple students have the same name? Or, what happens if the name was entered
incorrectly when the record was initially created?

10210

Brown
John
10175
14 East Main St
Smith
Somewhere
John
10142 VA
14 East Main St
Roberts 99155
Somewhere
John
10015 VA
14 East Main St
Smith 99155
Somewhere
John
VA
14 East Main St
99155
Somewhere
VA
99155

Figure 3.3: Unique key/data pairs.

In this section, we define our own Map ADT and then provide an implementa-
tion using a list. In later chapters, we will implement and evaluate the map using
a variety of data structures. We use the term map to distinguish our ADT from
the dictionary provided by Python. The Python dictionary is implemented using a
hash table, which requires the key objects to contain the hash method for gen-
erating a hash code. This can limit the type of problems with which a dictionary
can be used. We define our Map ADT with the minimum requirement that the
keys are comparable, which will allow it to be used in a wider range of problems.
It’s not uncommon to provide multiple implementations of an ADT as is done with
many language libraries. We will explore the implementation details of Python’s
dictionary later in Chapter 11 when we discuss hash tables and the design of hash
functions.

3.2.1 The Map Abstract Data Type


The Map ADT provides a great example of an ADT that can be implemented using
one of many different data structures. Our definition of the Map ADT, which is
provided next, includes the minimum set of operations necessary for using and
managing a map.
3.2 Maps 77

Define Map ADT

A map is a container for storing a collection of data records in which each record
is associated with a unique key. The key components must be comparable.
 Map(): Creates a new empty map.

 length (): Returns the number of key/value pairs in the map.


 contains ( key ): Determines if the given key is in the map and returns True
if the key is found and False otherwise.
 add( key, value ): Adds a new key/value pair to the map if the key is not
already in the map or replaces the data associated with the key if the key is in
the map. Returns True if this is a new key and False if the data associated
with the existing key is replaced.
 remove( key ): Removes the key/value pair for the given key if it is in the
map and raises an exception otherwise.
 valueOf( key ): Returns the data record associated with the given key. The
key must exist in the map or an exception is raised.
 iterator (): Creates and returns an iterator that can be used to iterate over
the keys in the map.

3.2.2 List-Based Implementation


We indicated earlier that many different data structures can be used to implement
a map. Since we are trying to replicate the functionality of the dictionary provided
by Python, we don’t want to use that structure. That leaves the use of an array
or list. As with the Set ADT, both the array and list structures can be used, but
the list is a better choice since it does not have a fixed size like an array and it can
expand automatically as needed.
In the implementation of the Bag and Set ADTs, we used a single list to store
the individual elements. For the Map ADT, however, we must store both a key
component and the corresponding value component for each entry in the map.
We cannot simply add the component pairs to the list without some means of
maintaining their association.
One approach is to use two lists, one for the keys and one for the correspond-
ing values. Accessing and manipulating the components is very similar to that
used with the Bag and Set ADTs. The difference, however, is that the associa-
tion between the component pairs must always be maintained as new entries are
added and existing ones removed. To accomplish this, each key/value must be
stored in corresponding elements of the parallel lists and that association must be
maintained.
78 CHAPTER 3 Sets and Maps

Instead of using two lists to store the key/value entries in the map, we can use
a single list. The individual keys and corresponding values can both be saved in a
single object, with that object then stored in the list. A sample instance illustrating
the data organization required for this approach is shown in Figure 3.4.

Smith
Smith
entryList  John
John
14
14 East
East Main
Main St
St
Map Somewhere
Somewhere Roberts
Roberts
VA
VA Susan
Susan
99155
0  10015
10015  99155 231
231 Quarry
Quarry Rd
Rd
Nowhere
Nowhere
TX
TX
1  10142
10142  11333
11333

2  10210
10210 
Brown
Brown
Jessica
Jessica
3  10175
10175  Smith
Smith 231
231 Quarry
Quarry Rd
Rd
Jane
Jane Plains
Plains
MapEntry 81
81 Jefferson
Jefferson St
St TN
TN
East
East End
End 30101
30101
PA
PA
28541
28541

Figure 3.4: The Map ADT implemented using a single list.

The implementation of the Map ADT using a single list is provided in List-
ing 3.2. As we indicated earlier in Chapter 1, we want to avoid the use of tuples
when storing structured data since it’s better practice to use classes with named
fields. The MapEntry storage class, defined in lines 56–59, will be used to store the
individual key/value pairs. Note this storage class is defined to be private since it’s
only intended for use by the Map class that provides the single list implementation
of the Map ADT.

Listing 3.2 The linearmap.py module.

1 # Implementation of Map ADT using a single list.


2 class Map :
3 # Creates an empty map instance.
4 def __init__( self ):
5 self._entryList = list()
6
7 # Returns the number of entries in the map.
8 def __len__( self ):
9 return len( self._entryList )
10
11 # Determines if the map contains the given key.
12 def __contains__( self, key ):
13 ndx = self._findPosition( key )
14 return ndx is not None
15
16 # Adds a new entry to the map if the key does exist. Otherwise, the
3.2 Maps 79

17 # new value replaces the current value associated with the key.
18 def add( self, key, value ):
19 ndx = self._findPosition( key )
20 if ndx is not None : # if the key was found
21 self._entryList[ndx].value = value
22 return False
23 else : # otherwise add a new entry
24 entry = _MapEntry( key, value )
25 self._entryList.append( entry )
26 return True
27
28 # Returns the value associated with the key.
29 def valueOf( self, key ):
30 ndx = self._findPosition( key )
31 assert ndx is not None, "Invalid map key."
32 return self._entryList[ndx].value
33
34 # Removes the entry associated with the key.
35 def remove( self, key ):
36 ndx = self._findPosition( key )
37 assert ndx is not None, "Invalid map key."
38 self._entryList.pop( ndx )
39
40 # Returns an iterator for traversing the keys in the map.
41 def __iter__( self ):
42 return _MapIterator( self._entryList )
43
44 # Helper method used to find the index position of a category. If the
45 # key is not found, None is returned.
46 def _findPosition( self, key ):
47 # Iterate through each entry in the list.
48 for i in range( len(self) ) :
49 # Is the key stored in the ith entry?
50 if self._entryList[i].key == key :
51 return i
52 # When not found, return None.
53 return None
54
55 # Storage class for holding the key/value pairs.
56 class _MapEntry :
57 def __init__( self, key, value ):
58 self.key = key
59 self.value = value

Many of the methods require a search to determine if the map contains a given
key. In this implementation, the standard in operator cannot be used since the list
contains MapEntry objects and not simply key entries. Instead, we have to search
the list ourselves and examine the key field of each MapEntry object. Likewise, we
routinely have to locate within the list the position containing a specific key/value
entry. Since these operations will be needed in several methods, we can create a
helper method that combines the two searches and use it where needed.
The findPosition() helper method searches the list for the given key. If
the key is found, the index of its location is returned; otherwise, the function
80 CHAPTER 3 Sets and Maps

returns None to indicate the key is not contained in the map. When used by
the other methods, the value returned can be evaluated to determine both the
existence of the key and the location of the corresponding entry if the key is in
the map. By combining the two searches into a single operation, we eliminate the
need to first determine if the map contains the key and then searching again for
its location. Given the helper method, the implementation of the various methods
is straightforward. Implementation of the iterator method is left as an exercise.

3.3 Multi-Dimensional Arrays


In Chapter 2, we worked with one- and two-dimensional arrays, but arrays can be
larger than two dimensions. In fact, arrays can contain any number of dimensions
that may be needed for a given problem. A multi-dimensional array stores
a collection of data in which the individual elements are accessed with multi-
component subscripts: xi,j or yi,j,k . Figure 3.5 illustrates the abstract view of a
two- and three-dimensional array. As we saw earlier, a two-dimensional array is
typically viewed as a table or grid consisting of rows and columns. An individual
element is accessed by specifying two indices, one for the row and one for the
column. The three-dimensional array can be visualized as a box of tables where
each table is divided into rows and columns. Individual elements are accessed by
specifying the index of the table followed by the row and column indices. Larger
dimensions are used in the solutions for some problems, but they are more difficult
to visualize.

les
columns tab 1 2
0 1 2 3 4 0
0 0

1 1
rows

rows

2 2

3 3

0 1 2 3
columns

Figure 3.5: Sample multi-dimensional arrays: (left) a 2-D array viewed as a rectangular
table and (right) a 3-D array viewed as a box of tables.

Most high-level programming languages provide a convenient way to create and


manage multi-dimensional arrays while others require a more hands-on approach.
C++ and Java are two examples of languages that provide multi-dimensional arrays
as part of the language. Python, of course, does not directly support arrays of any
dimension. But that did not prevent us from defining and implementing abstract
data types in the previous chapter for one- and two-dimensional arrays. Likewise,
we can define an abstract data type for creating and using arrays of any dimension.
3.3 Multi-Dimensional Arrays 81

3.3.1 The MultiArray Abstract Data Type


To accommodate multi-dimensional arrays of two or more dimensions, we define
the MultiArray ADT and as with the earlier array abstract data types, we limit the
operations to those commonly provided by arrays in most programming languages
that provide the array structure.

Define MultiArray ADT

A multi-dimensional array consists of a collection of elements organized into mul-


tiple dimensions. Individual elements are referenced by specifying an n-tuple or
a subscript of multiple components, (i1 , i2 , . . . in ), one for each dimension of the
array. All indices of the n-tuple start at zero.
 MultiArray( d1 , d2 , . . . dn ): Creates a multi-dimensional array of elements or-
ganized into n-dimensions with each element initially set to None. The number
of dimensions, which is specified by the number of arguments, must be greater
than 1. The individual arguments, all of which must be greater than zero,
indicate the lengths of the corresponding array dimensions. The dimensions
are specified from highest to lowest, where d1 is the highest possible dimension
and dn is the lowest.

 dims(): Returns the number of dimensions in the multi-dimensional array.

 length( dim ): Returns the length of the given array dimension. The individ-
ual dimensions are numbered starting from 1, where 1 represents the first, or
highest, dimension possible in the array. Thus, in an array with three dimen-
sions, 1 indicates the number of tables in the box, 2 is the number of rows,
and 3 is the number of columns.

 clear( value ): Clears the array by setting each element to the given value.

 getitem ( i1 , i2 , . . . in ): Returns the value stored in the array at the element


position indicated by the n-tuple (i1 , i2 , . . . in ). All of the specified indices
must be given and they must be within the valid range of the corresponding
array dimensions. Accessed using the element operator: y = x[ 1, 2 ].

 setitem ( i1 , i2 , . . . in , value ): Modifies the contents of the specified array


element to contain the given value. The element is specified by the n-tuple
(i1 , i2 , . . . in ). All of the subscript components must be given and they must
be within the valid range of the corresponding array dimensions. Accessed
using the element operator: x[ 1, 2 ] = y.

3.3.2 Data Organization


Most computer architectures provide a mechanism at the hardware level for creat-
ing and using one-dimensional arrays. Programming languages need only provide
82 CHAPTER 3 Sets and Maps

appropriate syntax to make use of a 1-D array. Multi-dimensional arrays are not
handled at the hardware level. Instead, the programming language typically pro-
vides its own mechanism for creating and managing multi-dimensional arrays.
As we saw earlier, a one-dimensional array is composed of a group of sequential
elements stored in successive memory locations. The index used to reference a
particular element is simply the offset from the first element in the array. In most
programming languages, a multi-dimensional array is actually created and stored
in memory as a one-dimensional array. With this organization, a multi-dimensional
array is simply an abstract view of a physical one-dimensional data structure.

Array Storage
A one-dimensional array is commonly used to physically store arrays of higher
dimensions. Consider a two-dimensional array divided into a table of rows and
columns as illustrated in Figure 3.6. How can the individual elements of the table
be stored in the one-dimensional structure while maintaining direct access to the
individual table elements? There are two common approaches. The elements
can be stored in row-major order or column-major order . Most high-level
programming languages use row-major order, with FORTRAN being one of the
few languages that uses column-major ordering to store and manage 2-D arrays.

0 1 2 3 4

0 2 15
15 45 13
13 78
78

1 40 12
12 52 91
91 86
86

2 59 25
25 33 41
41 66

Figure 3.6: The abstract view of a sample 3 × 5 two-dimensional array.

In row-major order, the individual rows are stored sequentially, one at a time,
as illustrated in Figure 3.7. The first row of 5 elements are stored in the first 5
sequential elements of the 1-D array, the second row of 5 elements are stored in
the next five sequential elements, and so forth.
In column-major order, the 2-D array is stored sequentially, one entire column
at a time, as illustrated in Figure 3.8. The first column of 3 elements are stored in
the first 3 sequential elements of the 1-D array, followed by the 3 elements of the
second column, and so on.
For larger dimensions, a similar approach can be used. With a three-dimensional
array, the individual tables can be stored contiguously using either row-major or
column-major ordering. As the number of dimensions grow, all elements within
a single instance of each dimension are stored contiguously before the next in-
stance. For example, given a four-dimensional array, which can be thought of as
an array of boxes, all elements of an individual box (3-D array) are stored before
the next box.
3.3 Multi-Dimensional Arrays 83

0 1 2 3 4
Physical storage
0 2 15
15 45
45 13
13 78
78
of a 2-D array using
row-major order.
1 40
40 12
12 52
52 91
91 86
86

2 59
59 25
25 33
33 41
41 6

row 0 row 1 row 2

22 
15
15 
45
45 
13
13 
78
78 
40
40 
12
12 
52
52 91
91 86
86 59
59 25 33
33 41
41 66
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14

Figure 3.7: Physical storage of a sample 2-D array (top) in a 1-D array using row-major
order (bottom).

Index Computation
Since multi-dimensional arrays are created and managed by instructions in the
programming language, accessing an individual element must also be handled by
the language. When an individual element of a 2-D array is accessed, the compiler
must include additional instructions to calculate the offset of the specific element
within the 1-D array. Given a 2-D array of size m×n and using row-major ordering,
an equation can be derived to compute this offset.
To derive the formula, consider the 2-D array illustrated in Figure 3.7 and
observe the physical storage location within the 1-D array for the first element in
several of the rows. Element (0, 0) maps to position 0 since it is the first element
in both the abstract 2-D and physical 1-D arrays. The first entry of the second
row (1, 0) maps to position n since it follows the first n elements of the first row.
Likewise, element (2, 0) maps to position 2n since it follows the first 2n elements
in the first two rows. We could continue in the same fashion through all of the
rows, but you would soon notice the position for the first element of the ith row is

0 1 2 3 4
Physical storage
of a 2-D array using 0 2 15
15 45
45 13
13 78
78
column-major order.
1 40 12
12 52
52 91
91 86
86

2 59 25
25 33
33 41
41 6

column 0 column 1 column 2 column 3 column 4

22 
40
40 
59 
15
15 
12
12 
25 
45
45 
52
52 33
33 13
13 91
91 41 78 86
86 66
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14

Figure 3.8: Physical storage of a sample 2-D array (top) in a 1-D array using column-
major order (bottom).
84 CHAPTER 3 Sets and Maps

n ∗ i. Since the subscripts start from zero, the ith subscript not only represents a
specific row but also indicates the number of complete rows skipped to reach the
ith row.
Knowing the position of the first element of each row, the position for any
element within a 2-D array can be determined. Given an element (i, j) of a 2-D
array, the storage location of that element in the 1-D array is computed as

index2 (i, j) = i ∗ n + j (3.1)

The column index, j, is not only the offset within the given row but also the
number of elements that must be skipped in the ith row to reach the jth column.
To see this formula in action, again consider the 2-D array from Figure 3.7 and
assume we want to access element (2, 3). Finding the target element within the
1-D array requires skipping over the first 2 complete rows of elements:

2 15
15 45 13 78
78
i

40 12
12 52 91 86
86

59
59 25
25 33
33 41
41 66

and the first 3 elements within row 2:

59
59 25
25 33 41
41 6

Plugging the indices into the equation from above results in an index position of
13, which corresponds to the position of element (2, 3) within the 1-D array used
to physically store the 2-D array.
Similar equations can be derived for arrays of higher dimensions. Given a 3-D
array of size d1 × d2 × d3 , the 1-D array offset of element (i1 , i2 , i3 ) stored using
row-major order will be

index3 (i1 , i2 , i3 ) = i1 ∗ (d2 ∗ d3 ) + i2 ∗ d3 + i3 (3.2)

For each component (i) in the subscript, the equation computes the number of
elements that must be skipped within the corresponding dimension. For example,
the factor (d2 ∗ d3 ) indicates the number of elements in a single table of the cube.
When it’s multiplied by i1 we get the number of complete tables to skip and in turn
the number of elements to skip in order to arrive at the first element of table i1 .
3.3 Multi-Dimensional Arrays 85

i1

d2
d3

The remaining part of the equation (i2 ∗ d3 + i3 ) is equivalent to index2 (i2 , i3 ),


which indicates the number of elements to skip within the i1 table. As the number
of dimensions increase, additional products are added to the equation, one for each
new dimension. For example, the equation to compute the offset for a 4-D array is

index4 (i1 , i2 , i3 , i4 ) = i1 ∗ (d2 ∗ d3 ∗ d4 ) + i2 ∗ (d3 ∗ d4 ) + i3 ∗ d4 + i4 (3.3)

You may notice a pattern developing as the number of dimensions increase.


This pattern leads to a general equation for computing the 1-D array offset for
element (i1 , i2 , . . . , in ) within an n-dimensional array:

index(i1 , i2 , . . . , in ) = i1 ∗ f1 + i2 ∗ f2 + · · · + in−1 ∗ fn−1 + in ∗ 1 (3.4)

where the fj values are the factors representing the number of elements to be
skipped within the corresponding dimension and are computed using

n
Y
fn = 1 and fj = dk ∀0<j<n (3.5)
k=j+1

The size of a multi-dimensional array is fixed at the time it’s created and cannot
change during execution. Likewise, the several fj products used in the equation
above will not change once the size of the array is set. This can be used to our
advantage to reduce the number of multiplications required to compute the element
offsets. Instead of computing the products every time an element is accessed, we
can compute and store the factor values and simply plug them into the equation
when needed.

3.3.3 Variable-Length Arguments


The definition of the MultiArray ADT requires a variable-length argument for
the constructor and the two element access methods. The number of arguments
passed to each method is supposed to equal the number of dimensions in the
array. Python functions and methods can be defined to accept a variable number
of arguments, which is exactly what we need to implement the MultiArray ADT.
Consider the following function, which accepts any number of arguments (assumed
to be numerical in this example) and then prints how many arguments were passed
and the sum of those arguments:
86 CHAPTER 3 Sets and Maps

def func( *args ):


print "Number of arguments: ", len( args )
sum = 0
for value in args :
sum += value
print( "Sum of the arguments: ", sum )

When using the function, we can pass a variable number of arguments for each
invocation. For example, all of the following are valid function calls:

func( 12 )
func( 5, 8, 2 )
func( 18, -2, 50, 21, 6 )

which results in the following output:


Number of arguments: 1
Sum of the arguments: 12
Number of arguments: 3
Sum of the arguments: 15
Number of arguments: 5
Sum of the arguments: 93

The asterisk next to the argument name (*args) tells Python to accept any
number of arguments and to combine them into a tuple. The tuple is then passed
to the function and assigned to the formal argument marked with the asterisk.
Note the asterisk is only used in the argument list to indicate that the function
or method can accept any number of arguments. It is not part of the argument
name. The len() operation can be applied to the tuple to determine the number
of actual arguments passed to the function. The individual arguments, which
are elements in the tuple, can be accessed by using the subscript notation or by
iterating the collection.

3.3.4 Implementing the MultiArray


To implement the MultiArray ADT, the elements of the multi-dimensional array
can be stored in a single 1-D array in row-major order. Not only does this create
a fast and compact array structure, but it’s also the actual technique used by
most programming languages. A partial implementation of the MultiArray class
is provided in Listing 3.3.

Listing 3.3 The array.py module with the MultiArray class.

1 # Implementation of the MultiArray ADT using a 1-D array.


2 class MultiArray :
3 # Creates a multi-dimensional array.
4 def __init__( self, *dimensions ):
5 assert len(dimensions) > 1, "The array must have 2 or more dimensions."
6 # The variable argument tuple contains the dim sizes.
7 self._dims = dimensions
8
3.3 Multi-Dimensional Arrays 87

9 # Compute the total number of elements in the array.


10 size = 1
11 for d in dimensions :
12 assert d > 0, "Dimensions must be > 0."
13 size *= d
14
15 # Create the 1-D array to store the elements.
16 self._elements = Array( size )
17 # Create a 1-D array to store the equation factors.
18 self._factors = Array( len(dimensions) )
19 self._computeFactors()
20
21 # Returns the number of dimensions in the array.
22 def numDims( self ):
23 return len(self._dims)
24
25 # Returns the length of the given dimension.
26 def length( self, dim ):
27 assert dim >= 1 and dim < len(self._dims),\
28 "Dimension component out of range."
29 return self._dims[dim - 1]
30
31 # Clears the array by setting all elements to the given value.
32 def clear( self, value ):
33 self._elements.clear( value )
34
35 # Returns the contents of element (i_1, i_2, ..., i_n).
36 def __getitem__( self, ndxTuple ):
37 assert len(ndxTuple) == self.numDims(), "Invalid # of array subscripts."
38 index = self._computeIndex( ndxTuple )
39 assert index is not None, "Array subscript out of range."
40 return self._elements[index]
41
42 # Sets the contents of element (i_1, i_2, ..., i_n).
43 def __setitem__( self, ndxTuple, value ):
44 assert len(ndxTuple) == self.numDims(), "Invalid # of array subscripts."
45 index = self._computeIndex( ndxTuple )
46 assert index is not None, "Array subscript out of range."
47 self._elements[index] = value
48
49 # Computes the 1-D array offset for element (i_1, i_2, ... i_n)
50 # using the equation i_1 * f_1 + i_2 * f_2 + ... + i_n * f_n
51 def _computeIndex( self, idx ):
52 offset = 0
53 for j in range( len(idx) ):
54 # Make sure the index components are within the legal range.
55 if idx[j] < 0 || idx[j] >= self._dims[j] :
56 return None
57 else : # sum the product of i_j * f_j.
58 offset += idx[j] * self._factors[j]
59 return offset
60
61 # Computes the factor values used in the index equation.
62 def _computeFactors( self ):
63 ......
88 CHAPTER 3 Sets and Maps

Constructor
The constructor, which is shown in lines 4–19, defines three data fields: dims
stores the sizes of the individual dimensions; factors stores the factor values
used in the index equation; and elements is used to store the 1-D array used as
the physical storage for the multi-dimensional array.
The constructor is defined to accept a variable-length argument as required in
the ADT definition. The resulting tuple will contain the sizes of the individual
dimensions and is assigned to the dims field. The dimensionality of the array
must be verified at the beginning of the constructor as the MultiArray ADT is
meant for use with arrays of two dimensions or more.
The elements of the multi-dimensional array will be stored in a 1-D array. The
fixed size of the array can be computed as the product of the dimension lengths
by traversing over the tuple containing the variable-length argument. During the
traversal, the precondition requiring all dimension lengths be greater than zero is
also evaluated. The Array class defined earlier in the chapter is used to create the
storage array.
Finally, a 1-D array is created and assigned to the factors field. The size of
the array is equal to the number of dimensions in the multi-dimensional array. This
array will be initialized to the factor values used in Equation 3.4 for computing
the element offsets. The actual computation and initialization is performed by the
computeFactors() helper method, which is left as an exercise. A sample instance
of the MultiArray class is illustrated in Figure 3.9.

dims
 3 55
factors
 5 11
elements
 22 15
15 45
45 13
13 78
78 40 12
12 52
52 91
91 86
86 59
59 25
25 33
33 41
41 6

MultiArray

Figure 3.9: A sample MultiArray object for the 2-D array from Figure 3.6.

Dimensionality and Lengths


In the multi-dimensional version of the array, there is no single length value. In-
stead, each dimension of the array has an associated size. Python’s len() function
cannot be used for this task since we must specify a particular dimension to obtain
its size. Instead, the length() method, as shown in lines 26–29 of Listing 3.3,
is used. The method first verifies the given dimension index is between 1 and n,
which is the legal range specified in the ADT definition. The size of the requested
dimension is then returned using the appropriate value from the dims tuple. The
3.4 Application: Sales Reports 89

numDims() method returns the dimensionality of the array, which can be obtained
from the number of elements in the dims tuple.

Element Access
Access to individual elements within an n-D array requires an n-tuple or multi-
component subscript, one for each dimension. As indicated in Section 2.3.2, when
a multi-component subscript is specified (i.e., y = x[i,j]), Python automatically
stores the components in a tuple in the order listed within the brackets and passes
the tuple to the ndxTuple argument.
The contents of the ndxTuple are passed to the computeIndex() helper method
to compute the index offset within the 1-D storage array. The use of the helper
method reduces the need for duplicate code that otherwise would be required in
both element access methods. The setitem operator method can be imple-
mented in a similar fashion. The major difference is that this method requires a
second argument to receive the value to which an element is set and modifies the
indicated element with the new value instead of returning a value.

Computing the Offset


The computeIndex() helper method, shown in lines 51–59 of Listing 3.3, imple-
ments Equation 3.4, which computes the offset within the 1-D storage array. The
method must also verify the subscript components are within the legal range of the
dimension lengths. If they are valid, the offset is computed and returned; other-
wise, None is returned to flag an invalid array index. By returning None from the
helper method instead of raising an exception within the method, better informa-
tion can be provided to the programmer as to the exact element access operation
that caused the error.

3.4 Application: Sales Reports


LazyMart, Inc. is a small regional chain department store with locations in several
different cities and states. The company maintains a collection of sales records for
the various items sold and would like to generate several different types of reports
from this data. One such report, for example, is the yearly sales by store, as
illustrated in Figure 3.10 on the next page, while others could include total sales
across all stores for a specific month or a specific item.
The sales data of the current calendar year for all of LazyMart’s stores is
maintained as a collection of entries in a text file. For example, the following
illustrates the first several lines of a sample sales data text file:
8
100
5 11 85 45.23
1 4 26 128.93
1 8 75 39.77
:
90 CHAPTER 3 Sets and Maps

LazyMart Sales Report


Store #1

Item# Jan Feb Mar ... Nov Dec


1 1237.56 1543.23 1011.00 2101.88 2532.99
2 829.85 974.18 776.54 802.50 643.21
3 3100.00 3218.25 3005.34 2870.50 3287.25
4 1099.45 1573.75 1289.21 1100.00 1498.25

: : : : ... : :

99 704.00 821.30 798.00 532.00 699.50


100 881.25 401.00 375.00 732.00 500.00
­­­­­­­ ­­­­­­­ ­­­­­­­ ­­­­­­­ ­­­­­­­

Figure 3.10: A sample sales report

where the first line indicates the number of stores; the second line indicates the
number of individual items (both of which are integers); and the remaining lines
contain the sales data. Each line of the sales data consists of four pieces of in-
formation: the store number, the month number, the item number, and the sales
amount for the given item in the given store during the given month. For sim-
plicity, the store and item numbers will consist of consecutive integer values in the
range [1 . . . max], where max is the number of stores or items as extracted from
the first two lines of the file. The month is indicated by an integer in the range
[1 . . . 12] and the sales amount is a floating-point value.

Data Organization
While some reports, like the student report from Chapter 1, are easy to produce
by simply extracting the data and writing it to the report, others require that we
first organize the data in some meaningful way in order to extract the information
needed. That is definitely the case for this problem, where we may need to produce
many different reports from the same collection of data. The ideal structure for
storing the sales data is a 3-D array, as shown in Figure 3.11, in which one dimen-
sion represents the stores, another represents the items sold in the stores, and the
last dimension represents each of the 12 months in the calendar year. The 3-D
array can be viewed as a collection of spreadsheets, as illustrated in Figure 3.12.

res
sto...
7
0
0
:
items

100
0 ... 12
months

Figure 3.11: The sales data stored in a 3-D array.


3.4 Application: Sales Reports 91
7

.
..
es
3

or
2

st
1
0
0

.
5

..
items
6

10

: : : :

99

0 1 2 3 4 5 6 7 8 9 10 11
months

Figure 3.12: The sales data viewed as a collection of spreadsheets.

Each spreadsheet contains the sales for a specific store and is divided into rows and
columns where each row contains the sales for one item and the columns contain
the sales for each month.
Since the store, item, and month numbers are all composed of consecutive
integer values starting from 1, we can easily represent each by a unique index
that is one less than the given number. For example, the data for January will be
stored in column 0, the data for February will be stored in column 1, and so on.
Likewise, the data for item number 1 will be stored in row 0, the data for item
number 2 will be stored in row 1, and so on. We leave the actual extraction of
the data from a text file as an exercise. But for illustration purposes, we assume
this step has been completed resulting in the creation and initialization of the 3-D
array as shown here:

salesData = MultiArray( 8, 100, 12 )

Total Sales by Store


With the data loaded from the file and stored in a 3-D array, we can produce many
different types of reports or extract various information from the sales data. For
example, suppose we want to determine the total sales for a given store, which
includes the sales figures of all items sold in that store for all 12 months. The
following function computes this value:

# Compute the total sales of all items for all months in a given store.
def totalSalesByStore( salesData, store ):
# Subtract 1 from the store # since the array indices are 1 less
# than the given store #.
92 CHAPTER 3 Sets and Maps

s = store-1
# Accumulate the total sales for the given store.
total = 0.0

# Iterate over item.


for i in range( salesData.length(2) ):
# Iterate over each month of the i item.
for m in range( salesData.length(3) ):
total += salesData[s, i, m]

return total

Assuming our view of the data as a collection of spreadsheets, this requires travers-
ing over every element in the spreadsheet containing the data for the given store.
If store equals 1, this is equivalent to processing every element in the spreadsheet
shown at the front of Figure 3.12. Two nested loops are required since we must sum
the values from each row and column contained in the given store spreadsheet.
The number of rows (dimension number 2) and columns (dimension number 3) can
be obtained using the length() array method.

Total Sales by Month


Next, suppose we want to compute the total sales for a given month that includes
the sales figures of all items in all stores sold during that month. This value can
be computed using the following function:
# Compute the total sales of all items in all stores for a given month.
def totalSalesByMonth( salesData, month ):
# The month number must be offset by 1.
m = month - 1
# Accumulate the total sales for the given month.
total = 0.0

# Iterate over each store.


for s in range( salesData.length(1) ):
# Iterate over each item of the s store.
for i in range( salesData.length(2) ):
total += salesData[s, i, m]

return total

This time, the two nested loops have to iterate over every row of every spread-
sheet for the single column representing the given month. If we use this function
to compute the total sales for the month of January, the elements of the 3-D array
that will be accessed are shown by the shaded area in Figure 3.13(a).

Total Sales by Item


Another value that we can compute from the sales data in the 3-D array is the
total sales for a given item, which includes the sales figures for all 12 months and
from all 8 stores. This is computed by the following function:
3.4 Application: Sales Reports 93

# Compute the total sales of a single item in all stores over all months.
def totalSalesByItem( salesData, item ):
# The item number must be offset by 1.
m = item - 1

# Accumulate the total sales for the given month.


total = 0.0

# Iterate over each store.


for s in range( salesData.length(1) ):
# Iterate over each month of the s store.
for m in range( salesData.length(3) ):
total += salesData[s, i, m]

return total

The cells of the array that would be accessed when using this function to
compute the total sales for item number 5 are shown by the shaded area in Fig-
ure 3.13(b). Remember, the sales for each item are stored in a specific row of the
array and the index of that row is one less than the item number since the indices
start at 0.
7 7
.

.
..

..
es

3 es 3
or

or
2 2
st

st

1 1
0 0
0 0

1 1

2 2

3 3

4 4
.

.
5 5
..

..
items

items

6 6

7 7

8 8

9 9

10 10

: : : : : : : :

(a) 99 (b) 99

0 1 2 3 4 5 6 7 8 9 10 11 0 1 2 3 4 5 6 7 8 9 10 11
months months

Figure 3.13: The elements of the 3-D array that must be accessed to compute the total
sales: (a) for the month of January and (b) for item number 5.

Monthly Sales by Store


Finally, suppose we want to compute the total monthly sales for each of the 12
months at a given store. While the previous examples computed a single value,
this task requires the computation of 12 different totals, one for each month. We
can store the monthly totals in a 1-D array and return the structure, as is done in
the following function:
94 CHAPTER 3 Sets and Maps

# Compute the total sales per month for a given store. A 1-D array is
# returned that contains the totals for each month.

def totalSalesPerMonth( salesData, store ):


# The store number must be offset by 1.
s = store - 1

# The totals will be returned in a 1-D array.


totals = Array( 12 )

# Iterate over the sales of each month.


for m in range( salesData.length(3) ):
sum = 0.0

# Iterate over the sales of each item sold during the m month.
for i in range( salesData.length(2) ):
sum += salesData[s, i, m]

# Store the result in the corresponding month of the totals array.


totals[m] = sum

# Return the 1-D array.


return totals

Figure 3.14 illustrates the use of the 1-D array for storing the individual
monthly totals. The shaded area shows the elements of the 3-D array that are
accessed when computing the total sales for the month of April at store number 1.
The monthly total will be stored at index position 3 within the 1-D array since
that is the corresponding column in the 3-D array for the month of April.

store months
0 0 1 2 3 4 5 6 7 8 9 10 11

5
items

10

: : : :

99

totals
0 1 2 3 4 5 6 7 8 9 10 11

Figure 3.14: The elements the 3-D array that must be accessed to compute the monthly
sales for store number 1.
Exercises 95

Exercises
3.1 Complete the Set ADT by implementing intersect() and difference().

3.2 Modify the Set() constructor to accept an optional variable argument to


which a collection of initial values can be passed to initialize the set. The
prototype for the new constructor should look as follows:

def Set( self, *initElements = None )

It can then be used as shown here to create a set initialized with the given
values:

s = Set( 150, 75, 23, 86, 49 )

3.3 Add a new operation to the Set ADT to test for a proper subset. Given two
sets, A and B, A is a proper subset of B, if A is a subset of B and A does not
equal B.

3.4 Add the str() method to the Set implementation to allow a user to print
the contents of the set. The resulting string should look similar to that of a
list, except you are to use curly braces to surround the elements.

3.5 Add Python operator methods to the Set class that can be used to perform
similar operations to those already defined by named methods:

Operator Method Current Method


add (setB) union(setB)
mul (setB) interset(setB)
sub (setB) difference(setB)
lt (setB) isSubsetOf(setB)

3.6 Add a new operation keyArray() to the Map class that returns an array con-
taining all of the keys stored in the map. The array of keys should be in no
particular ordering.

3.7 Add Python operators to the Map class that can be used to perform similar
operations to those already defined by named methods:

Operator Method Current Method


setitem (key, value) add(key, value)
getitem (key) valueOf(key)

3.8 Design and implement the iterator class SetIterator for use with the Set
ADT implemented using a list.
96 CHAPTER 3 Sets and Maps

3.9 Design and implement the iterator class MapIterator for use with the Map
ADT implemented using a list.
3.10 Develop the index equation that computes the location within a 1-D array for
element (i, j) of a 2-D array stored in column-major order.
3.11 The 2-D array described in Chapter 2 is a simple rectan-
0
gular structure consisting of the same number of elements
1
in each row. Other layouts are possible and sometimes
2
required by problems in computer science. For example,
3
the lower triangular array shown on the right is organized
4
such that the rows are staggered with each successive row
0 1 2 3 4
consisting of one more element than the previous row.
(a) Derive an equation that computes the total number of elements in the
lower triangular table for a table of size m × n.
(b) Derive an index equation that maps an element of the lower triangular
table onto a one-dimensional array stored in row-major order.

3.12 Complete the implementation of the MultiArray class by implementing the


helper method computeFactors().

Programming Projects
3.1 In this chapter, we implemented the Set ADT using a list. Implement the Set
ADT using a bag created from the Bag class. In your opinion, which is the
better implementation? Explain your answer.
3.2 Define a new class named TriangleArray to implement the lower triangular
table described in Exercise 3.11.
3.3 Given a collection of items stored in a bag, design a linear time algorithm that
determines the number of unique items in the collection.
3.4 Write a function that extracts the sales data from a text file and builds the
3-D array used to produce the various reports in Section 3.4. Assume the data
file has the format as described in the chapter.
3.5 Write a menu-driven program that uses your function from the previous ques-
tion to extract the sales data and can produce any of the following reports:

(a) Each of the four types of reports described in the chapter.


(b) The sales for a single store similar to that shown in Section 3.4 with the
data sorted by total sales.
(c) The total sales for each store sorted by total sales from largest to smallest.
(d) The total sales for each item sorted by item number.

You might also like