Data Structures and Algorithms using Python
Copyright © 2023 by Rance Necaise
Table Of Contents
Data Structures and Algorithms using Python
Copyright © 2023
by Rance Necaise

4.6 Implementing the MultiArray ADT

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.

Constructor

The constructor will require 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. A sample instance of the MultiArray class is illustrated in Figure 4.6.1.

Figure 4.6.1: A sample MultiArray object for the 2-D array from Figure 4.5.1
Special Topic
Variable Length Arguments

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.

assert len(dimensions) > 1, "The array must have 2 or more dimensions."
self._dims = dimensions    

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 1-D array is created using the Array class defined earlier in the chapter.

size = 1
for d in dimensions :
  assert d > 0, "Dimensions must be > 0."
  size *= d      
self._elements = Array(size)

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 4.4 for computing the element offsets. The actual computation and initialization is performed by the _computeFactors helper method, which is left as an exercise.

self._factors = Array(len(dimensions))
self._computeFactors()

The complete constructor is shown below:

class MultiArray :
  def __init__(self, *dimensions) :          
    assert len(dimensions) > 1, \
          "The array must have 2 or more dimensions."
    self._dims = dimensions    

    size = 1
    for d in dimensions :
      assert d > 0, "Dimensions must be > 0."
      size *= d      

    self._elements = Array(size)
    self._factors = Array(len(dimensions))
    self._computeFactors()

Dimensionality and Lengths

In the multi-dimensional version of the array, there is no single length value. Instead, 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, is used:

def length(self, dim) :                      
  assert dim >= 1 and dim < len(self._dims), "Dimension component out of range."
  return self._dims[dim - 1]                

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 numDims method returns the dimensionality of the array, which can be obtained from the number of elements in the _dims tuple

def numDims(self) :
  return len(self._dims)

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 4.3, when a multi-component subscript is specified (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. Here is the definition of the __getitem__ method:

def __getitem__(self, ndxTuple) :
  assert len(ndxTuple) == self.numDims(), "Invalid # of array subscripts."
  index = self._computeIndex(ndxTuple)
  assert index is not None, "Array subscript out of range."
  return self._elements[index]

The __setitem__ operator method can be implemented 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:

def __setitem__(self, ndxTuple, value) :
  assert len(ndxTuple) == self.numDims(), "Invalid # of array subscripts."
  index = self._computeIndex(ndxTuple)
  assert index is not None, "Array subscript out of range."
  self._elements[index] = value

Clearing the elements of the array to a specific value is as simple as calling the clear method on the _elements array, the 1-D storage array:

def clear(self, value) :
  self._elements.clear(value)

Computing the Offset

The _computeIndex helper method implements equation X, which computes the offset within the 1-D storage array.

def _computeIndex(self, idx) :                
  offset = 0
  for j in range(len(idx)) :
    if idx[j] < 0 or idx[j] >= self._dims[j] :
      return None
    else :
      offset += idx[j] * self._factors[j]        
  return offset                                  

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; otherwise, 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 information can be provided to the programmer as to the exact element access operation that caused the error. The implementation of the Multiarray class is provided below

Program Listing
Program: multiarray.py
  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.    
  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 or 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.     ......
Page last modified on August 25, 2023, at 03:28 PM