4. Unconstrained Multi-dimensional Arrays

(A Java Specific Feature)



1. Introduction

We have observed several times that a multi-dimensional array object can be viewed as a particular example of a single dimensional array of compound objects, where this compound object is itself an array (that may also be multi-dimensional). As a result the properties and attributes of 1-dimenional arrays - instantiation at declaration, number of elements in the array, assignment of one array to another (as used in the GIANT_TERNARY Constructor of the previous lecture) - all have counterparts within a multi-dimensional array context.

The example application developed in the last lecture utilised a constrained multi-dimensional array. Its bounds (length of each dimension) were fixed when the array was declared - 3 symbols represented by patterns within 9-by-9 char arrays.

In COMP101 it was shown that the number of elements in a 1-dimensional array can be set when the application program is executed, in accordance with values specified by the user. A facility of this type is a common feature of many high-level programming languages, notable exceptions being FORTRAN and Pascal. Given our interpretation of multi-dimensional structures as 1-dimensional arrays of compound Objects, we would expect that the `bounds' of a multi-dimensional array Object can also be fixed when a program is executed. This, of course, is indeed the case. There is, however, an important feature of the Java implementation of unconstrained multi-dimensional arrays that makes these rather different from the analogous structures in languages such as, Ada, Algol68, etc.

Question: Why is the the word bounds in the paragraph above, written in inverted quotes?

Answer: Because, in Java, a k-dimensional array does not, necessarily, have exactly k `bounds' associated with it.

Suppose, for example we consider a 2-dimensional array,

int [ ] [ ] xy;

If we take as input some positive integer value, read into a variable, n say, then we could set specific bounds on xy by, e.g.

xy = new int [n] [ ];OR
xy = new int [n] [n];OR
xy = new int [n-1] [n-2];etc

The last two examples result in both dimensions being constrained; and the first with exactly one dimension being constrained.

If we examine the completely constrained cases more closely, we see that:

int [n] [n]

comprises n 1-dimensional arrays each having n elements.

int [n-1] [n-2]

comprises n-1 1-dimensional arrays each having n-2 elements.

What about the first case?

All we can say of

int [n] [ ]

is that it defines n 1-dimensional arrays each of whose lengths is currently unconstrained.

Similarly,

int [n] [n-1] [ ]

defines n 2-dimensional arrays, each of which consists of n-1 rows whose exact length(s) have yet to be constrained.

and,

int [n] [ ] [ ]

comprises n arbitrary and unconstrained 2-dimensional arrays.


So, what's the point?

The point is: there is no requirement, in Java, that when the n 1-dimensional arrays

xy[1][], xy[2][], ..., xy[n][]

do have their lengths constrained, for every single one of these to have the same length.

In other words, for a given value of n, 10 say, we could instantiate the lengths of each of the 10 1-dimensional arrays in `patterns' such as either of those in the figure below:

Figure 4.1: `2-dimensional' arrays: Possible configurations in Java.

In Figure 4.1(a), each odd numbered row contains exactly half as many elements as each even indexed row. Given an integer value, m, as the length of the even indexed arrays, we could instantiate such an allocation by


xy = new int[n][];
for (int i=0; i< n; i++)
   if (i%2==0)
     xy[i] = new int [m];
   else
     xy[i] = new int [m/2];

Figure 4.2: Java realisation of Array Strucure in Figure 4.1(a)

In Figure 4.1(b), the number of elements in xy[i] for i < 9 is exactly one more than the number of elements in xy[i+1], a structure that could be instantiated by


xy = new int[n][];
for (int i=0; i < n; i++)
  xy[i] = new int [n-i];

Figure 4.3: Java realisation of Array Structure in Figure 4.1(b)

Such forms are not restricted to the 2-dimensional cases. For example a 3-dimensional analogue of the organisation in Figure 4.1(b), could be realised by


xyz = new int [n][][];
for (int i=0; i < n; i++)
  {
  xyz[i] = new int [n-i][];
  for (int j=0; j < n-i; j++)
    xyz[i][j] = new int [n-i-j];
  };

Figure 4.4: A more complex example of a `Pyramid' Structure in 3-dimensions

In this example, the n `planes' of the array xyz, are organised so that for each k < n, the 2-dimemnsional array xyz[k] is instantiated in the form of Figure 4.1(b) using the value n-k (instead of 10).

Notice, that the order in which individual dimensions are constrained is important when organising such structures: the mechanisms below although appearing to have equivalent behaviour to the implementation in Figure 4.4 will not work:


xyz = new int [n][][];
for (int i=0; i < n; i++)
  {
  for (int j=0; j < n-i; j++)
    {
    xyz[i] = new int [n-i][];
    xyz[i][j] = new int [n-i-j];
    };
  };

int [][][] xyz = new int [n][][];
for (int i=0; i < n; i++)
  {
  for (int j=0; j < n-i; j++)
    {
    xyz[i][] = new int [][n-i-j];
    };
    xyz[i] = new int [n-i];
   };
Incorrect Form of Figure 4.4 (i)Incorrect Form of Figure 4.4 (ii)

Figure 4.5: Two Erroneous Realisations of the Structure Defined in Figure 4.4

The example in (ii), which attempts to constrain the third dimensions before the lengths of the second dimension arrays have been fixed will not even compile. The example in (i), although compiling correctly, will not result in the required form being created: the statement

xyz[i] = new int [n-i][];

will be executed n-i times (in Figure 4.4, this statement is carried out before the for loop whose counter variable is j is entered) and, as a result, only the arrays xyz[i][n-i-1] will be fully constrained (each will have length 1); the arrays of the form xyz[i][j] for any value of j other than n-i-1 will not have had space allocated and attempts to refer to elements within such will result in a run-time exception.

The examples above illustrate that a multi-dimensional array Object may be constrained to have an extremely intricate `irregular' structure, albeit one that can always be analysed in terms of the `1-dimensional array of compound Objects' abstraction that has been promoted. Such a facility is not common in other high-level languages supporting unconstrained multi-dimensional arrays, the `object-orientated' language C++ allows this, but in, for example, Ada the language syntax compels programs to constrain the bounds of all dimensions simultaneously, thus in the 2-dimensional case, each `row' of the array must contain exactly the same number of `columns'. In order to achieve a similar functionality to the Java mechanism a more complicated data structure would have to be defined.


2. Undirected Graphs

The capability to have arrays of varying sizes within the context of a multi-dimensional array Object may seem to be a feature for which there is little in the way of direct usefulness. While this might, arguably, be the case for the more eccentric organisational structures illustrated in Figure 4.1(a), it turns out that there is a very natural application for the structure depicted in Figure 4.1(b): efficient storage and handling of undirected graphs.

[Note on Terminology: The word `graph' is heavily used within Computer Science and Mathematics and its meaning is not identical over all these fields. You may have encountered the term `graph' previously in phrases such as `graph of a function' wherein a `graph' is a pictorial (i.e. `graphical') representation showing the relationship between the arguments supplied to a function and the value of the function; or, again, the term `graph' is sometimes applied to histograms (`bar-charts') in statistics. The definition of the term graph that is commonly understood within Computer Science (and areas of Mathematics such as Discrete Mathematics - as some of you will have seen on MATH183) is the one that we will introduce below. It is safe to assume within Computer Science, that unless it is explicitly stated to the contrary, the term `graph' always indicates a structure of this type.]

Definition:

An undirected graph, G(V,E), is defined by two (finite) sets:

  • V called the vertices (or `nodes') of G.
  • E called the edges of G.

The edges, E(G) of an undirected graph G(V,E) are a set of (unordered) pairs of distinct vertices from V(G).

The number of vertices in V(G) is called the order of G(V,E)

The number of edges in E(G) is called the size of G(V,E)

(Thus we may refer to a graph G(V,E) of order 10 and size 20 to describe one which has 10 vertices and 20 edges

Definition of an Undirected Graph

The Road Map Example that we presented in the second lecture can be seen as an undirected graph G(V,E) in which

V(G) = { A,B,C,D,E,F }

E(G) = { {A,B}, {A,C}, {A,F}, {B,D}, {C,D}, {C,E}, {C,F}, {D,F}, {E,F} }

The order of this graph is 6

The size of this graph is 9

The Road Map Example of Lecture 2 Interpreted as an Undirected Graph of Order 6 and Size 9

Graphs, in both the undirected form defined above, and the directed form, wherein the edges are a set of ordered pairs of distinct vertices (so that an edge (A,C) is distinct from an edge (C,A)), are an extremely important structure within most (if not all) areas of Computer Science. Although the terms by which these are referred to may differ, you will encounter graphs throughout the remainder of the degree course. Thus, within, e.g.

The study of algorithms operating on graphs is a major sub-area within the general field of algorithmics, as most of you will see later in the module Algorithmic Foundations (COMP108). Within the other areas selected, graph abstractions provide a useful model to present complex relationships in a clear `visual' form, e.g. analytic design methods such as Nassi-Schneidermann Charts, Control Flow Diagrams, and the more sophisticated approaches such as JSD, Entity-Relation Diagrams that you will encounter later, are in essence graph-theoretic models. Similarly, graphs are a natural view of the organisation of sub-components within a hardware design (whether at a gate or more complex sub-system design).

The pervasive nature of graphs as a modelling basis within Computer Science provides some motivation for developing a simple Java packgage that can store and provide some basic operations on graph instances.

We describe the components of such for undirected graphs, as a natural application illustrating the use of unconstrained multi-dimensional arrays in Java.

3. A Basic Undirected Graph Object in Java

3.1. Requirements

Develop a package Graph that will provide the following capabilities:

  1. Creation and Storage of an undirected graph G(V,E) in which the order (number of vertices) is specified by the application.
  2. Allows the edges in a given instance to be associated with arbitrary Objects, e.g. Integer (as would be appropriate with the Road Map example where such values could represent distances) or Boolean (for which values would indicate the presence or absence of edges).
  3. Provides methods to:
    1. Set the Object value that is used to represent the absence of an edge between vertices.
    2. Return the value of the Object indicating the absence of edges.
    3. Return the number of vertices (order) in an instance.
    4. Return the number of edges (size) in an instance.
    5. Test if there is an edge present between two specified vertices in an instance of a Graph.
    6. Obtain the value of an Object associated with a given pair of vertices (returning the value provided in (1) (which might be the null value) if no edge exists between the two vertices).
    7. Change the value of an Object instance representing an edge between two specified vertices in an instance of Graph. Note that this should provide a facility that allows edges to be removed, i.e. the current non-edge Object is a legitimate parameter value.
    8. Convert the Graph structure into a String that when printed describes the internal structure of the Graph.
These, of course, are a very minimal set of requirements, however, they provide a sufficient basis upon which to construct more sophisticated graph algorithms.

It should be noted that we have chosen to allow the datum associated with an edge to be an Object of arbitrary type, so ensuring that a plethora of different constructors and methods does not have to be written, i.e. one for each type required.

Closer inspection of the structures described, suggests the Class Diagram indicated in Figure 4.6 below,

Figure 4.6: Class Diagram for Graph Class

3.2. Analysis

Whilst a 2-dimensional array is a `natural' representation for graphs, if we look at the form suggested to implement the Road Map example, in Lecture 2, we see that it has one rather obvious weakness: it uses (more than) twice as many elements than are necessary to record all of the information pertinent to the graph structure, i.e.

This is not something particular to the example used but is a behaviour that will arise in any undirected graph form, i.e. the graph structures that are typically used do not have edges joining a vertex to itself; since the graph is undirected there is no distinction between describing an edge in the form {v,w} and the form {w,v}.

Thus a less wasteful structure for storing undirected graph structures would be one in which:

a) No elements are used to record information concerning `edges' of the form {v,v}
b)Exactly one of the edges {v,w} and {w,v} has information concerning it stored in the structure

Desiderata for `efficient' Undirected Graph Storage.

Of course the second of these requirements ought to be dealt with (within the Class definition itself) in a consistent fashion. There is, however, no need for applications that might use the Graph class to be aware of what this is. Indeed, in keeping with the principle of data hiding, making the mechanism used visible to users (or, worse, requiring that applications were required to supply references to edges in a `consistent' manner) would be poor design style.

Fortunately, there is a simple approach that the Class methods accessing the graph representation can use to identify a reference to an edge {v,w} as equivalent to a reference supplied in the form {w,v}: since (a) obviates the need to deal with storing `edges' {v,v}, and that the access to edge information will be via an array element - g[v][w] - hence, with integer indices, it can be arranged (internally within the Class definition) that accesses are predicated on the assumption v < w.

We can now examine the realisation of the Graph class in more detail.

Graph Class Fields

The fields

private int order

private int size

private Object NonEdge=null

in the Graph Class Diagram will record the number of vertices, number of edges, and the value that indicates the absence of an edge between two vertices. The last of these being instantiated to to a default setting of the null Object.

The core of the undirected graph representation in which all of the edge data for the instance will be maintained is the 2-dimensional array of Objects

private Object [][] matrix

In keeping with the requirement that undirected graphs of arbitrary order may be instantiated, this is in an unconstrained format.

In order to appreciate exactly how an undirected n-vertex graph is held within this array, we turn to the implementation of

The Constructor Graph(int n)

The integer parameter, n, defines the number of vertices in the graph instance. In modelling an undirected n-vertex graph using the 2-dimensional array matrix [][] the total number of cells used will be exactly (n*(n-1))/2. How do these correspond to the possible edges in an n-vertex undirected graph? Suppose, we look at the specific case of n=8. Here we have to provide for the edges:

{1,2}{1,3}{1,4}{1,5}{1,6}{1,7}{1,8}
{2,3}{2,4}{2,5}{2,6}{2,7}{2,8}
{3,4}{3,5}{3,6}{3,7}{3,8}
{4,5}{4,6}{4,7}{4,8}
{5,6}{5,7}{5,8}
{6,7}{6,8}
{7,8}

So we have a row for each vertex (except the highest indexed vertex) and within the row for vertex number k, a cell for each vertex whose index is strictly larger than k, i.e. the k'th row contains exactly n-k columns - one for each vertex except those whose index is less than or equal to k. Hence,


public Graph(int n)
    {
    order = n; size=0;
    matrix = new Object[n-1][];       // the number of `rows' for n vertices
    //
    for (int i=0; i < n-1; i++)
      matrix[i] = new Object[n-i-1];  // the number of `columns' for each row.
    //
    for (int i=0; i < n-1; i++)
      for (int j=0; j < n-i-1; j++)
        matrix[i][j] = NonEdge;        // assumes no edges are present.
     }

Figure 4.7: The Constructor Graph(int n)

Now, suppose that we wish to access the element of this array that corresponds to an edge between two distinct vertices v and w, where, assuming the convention mentioned above, we have

1 <= v < w <= n

Given the, rather irritating, feature of Java that the first element of a length k array is indexed by 0 and its last by k-1, we cannot simply inspect the element

matrix [v][w]

Certainly the data relevant to a vertex with index v is all contained within the 1-dimensional array of Objects

matrix [v-1][ ]

within this array,

The edge {v,v+1} is stored in element 0

The edge {v,v+2} is stored in element 1

The edge {v,v+3} is stored in element 2

...

The edge {v,order} is stored in element order-v-1

From which we see that the edge {v,w} can be accessed in the element

matrix [v-1] [w-v-1]

It is worth stressing, again, that this translation from a pair of distinct vertex labels - v and w - into a specific element of a 2-dimensional array, i.e. matrix [v-1][w-v-1] ought to be hidden from (and inaccessible to) applications using the Graph class: such applications will only want to recover information concerning an edge by supplying two distinct vertex labels.

We can now address the realisation of the Class methods. We note from the Class Diagram that these are all Instance Methods

Graph Class Methods

The methods

public int OrderOf()

public int SizeOf()

just provide access to the values of these data fields in an Instance, and so are realised by,


public int OrderOf()
  {
  return order;
  };
//
public int SizeOf()
  {
  return size;
  }

Figure 4.8: Methods to obtain the order and size of an Instance

The method

public Object NoEdge()

that is used to supply the current setting of the value used to indicate the absence of an edge between two vertices (held in the field NonEdge), is a little bit more involved: its setting might be to the null reference, and an error might result if this were used in the invoking application. It is, therefore, necessary to check if this is the case and, should it be so, to return the literal String "null".


public Object NoEdge()
  {
  if (!(NonEdge==null))           // We have to test this, in order to
    return NonEdge;               // avoid a NullPointerException error
  else                            // which would occur in this
    return "null";                // particular case.
  }

Figure 4.9: Obtaing the current `Non-edge' setting

The method

public boolean IsEdge(int v, int w)

is used to test whether the Graph instance contains an edge joining vertices v and w. When this is invoked the parameters will be values between 1 and order (and should be distinct). As has been stressed earlier, the realisation of the class is responsible for relating these values to the correct cell of the 2-dimensional array, using the approach descirbed earlier.


public boolean IsEdge( int v, int w)
  {
  if (v < w)
    return !(matrix[v-1][w-v-1]==NonEdge); // Data concerning vertex v
                                           // w.r.t `higher' numbered
                                           // vertices is in row `v-1'
  else
    return !(matrix[w-1][v-w-1]==NonEdge);
  }

Figure 4.10: Boolean test for presence (true)/absence (false) of an edge {v,w}

In order to permit applications to choose a different value to represent the absence of an edge the method

public void SetNonEdgeValue (Object no_edge)

is used. Again care has be taken should an application wish to restore the default null setting by supplying this as a literal String. In addition, the array elements that held the former NonEdge value have to updated to hold the new value set.


public void SetNonEdgeValue ( Object no_edge )
  {
  if ( (no_edge.toString()).equalsIgnoreCase("null") ) // Can't pass null
    {                                      // reference directly and
    for (int i=1; i < order; i++)            // so we use the String encoding 
      for (int j=i+1; j < order+1; j++)      // to indicate this setting.
        if (!IsEdge(i,j))
          matrix[i-1][j-i-1] = null;
    NonEdge=null;                          // Do AFTER updating matrix[][]!
    }
  else
    {
    for (int i=1; i < order; i++)            // Same procedure but where a
      for (int j=i+1; j < order+1; j++)      // non-null setting is being used.
        if (!IsEdge(i,j))
          matrix[i-1][j-i-1] = no_edge;
    NonEdge = no_edge;
    };
  }

Figure 4.11: Changing the non-edge indicator

The methods described so far have largely been concerned with customising various class fields to application-specific setting. The next two methods are likely to be the two most heavily employed within any application employing the Graph class

public void SetEdge (Object edge_val, int v, int w)

public Object GetEdge (int v, int w)

The former sets the value of the edge {v,w} to the Object edge_val. The latter returns the current Object associated with the edge {v,w}.

Since the current non-edge indicator is a valid setting of edge_val the SetEdge method provides a mechanism for deleting an edge from the Graph instance. This method must also ensure that the size field (recording the number of edges in the instance) is correctly maintained after this method has completed.



public void SetEdge ( Object edge_val, int v, int w)
  {
  if (v==w) 
    matrix[v-1][w-v-1] = null;           // This is a bit clumsy. Will force an
                                         // ArrayOutofBoundsException, 
                                         // which is what's needed.
  else if (v < w) 
    {
    if (!IsEdge(v,w))                    // If there's no edge {v,w}
      {
      if ( !(edge_val.equals(NonEdge)) ) // and we're about to add it then
        {
        size = size+1;                   // the number of edges increases by 1.
        };
      }
    else                                // If there IS an edge {v,w}
      {
      if ( edge_val.equals(NonEdge) )   // and we're about to remove it then
        {
        size=size-1;                    // the number of edges decreases by 1.
        };
      };
    matrix[v-1][w-v-1] = new Object();  // Probably not needed (a precaution).
    matrix[v-1][w-v-1] = edge_val;      // Store the new value.
    }
  else
    this.SetEdge(edge_val,w,v);         // Can't expect applications to ensure
                                        // {v,w} are supplied with v < w so arrange
                                        // this properly.
                                        // Note that if v=w is NOT caught then
                                        // this Method will recurse indefinitely.
                                        // (Don't have to worry about this
                                        // in IsEdge(v,w)
                                        // because an Array bound exception
                                        // will be raised
                                        // by the run-time system if v=w).
  }
//******************************************************************//
// Obtain the value of the Object associated with edge {v,w}        //
//******************************************************************//
public Object GetEdge (int v, int w)
  {
  if (v==w) 
    return matrix[v-1][w-v-1];      // See similar remarks re. SetEdge
  else if (v < w)
    if (IsEdge(v,w))               // if there's a `real' edge then
      return matrix[v-1][w-v-1];   // just return the associated Object.
    else                           // If there's not a `real' edge
      if (NonEdge==null)           // have to make sure a NullPointerException
                                   // isn't generated.
        return "null";
      else
        return NonEdge.toString(); // Returns the current NonEdge object value.
  else
    return this.GetEdge(w,v);
  }

Figure 4.12: Setting and Obtaining the current status of an edge {v,w}

Finally, we have the method

public String toString()

that will translate the current graph form into a String suitable for printing.


//*********************************************************************//
// Convert the Graph structure to a String form that is suitable for   //
// output. The edges {v,w} are stored in a String in which newline     //
// characters separate the different vertex details.                   //
// The printing form is as order-1 distinct lines, the k'th line       //
// containing exactly one fewer item than the (k-1)'st line. Hence     //
//                                                                     //
// Line 1: Status of edges {1,2} {1,3} {1,4} ... {1,order}             //
//      2:                 {2,3} {2,4} {2,5} ... {2,order}             //
//              ...                                                    //
//      k:                 {k,k+1} {k,k+2}   ... {k,order}             //
//              ...                                                    //
//    order-1              {order-1,order}                             //
//*********************************************************************//
public String toString()
  {
  String res=new String();
  for (int i=1; i < order; i++)
    {
    for (int j=i+1; j < order+1; j++)
      {
      res = res+GetEdge(i,j).toString()+" ";
      };
      res = res+"\n";
    };
  return res;
  }

Figure 4.13: Creating a suitable printing form of a Graph Instance.


The complete Graph Class Implementation is given below


//
// COMP102
// Example 6:  Undirected Graph Class
//              
// Paul E. Dunne 25/10/99
//
public class Graph
  {
  //
  // Fields
  //
  private Object [][] matrix;     // This will record the definition
                                  // of the Graph structure.
                                  //
                                  // N.B. Declaration as 2-D array of Object
                                  // so that the graph edges may be associated
                                  // with both primitive types (via Wrappers) and
                                  // Compound types. Typical instantiations might
                                  // be as Boolean (matrix[v][w] is TRUE if and
                                  // only if there is an edge between vertex v and
                                  // vertex w); Integer (so that the values stored
                                  // represent the `distance' separating two
                                  // vertices); String (so that edges are
                                  // associated with `text')
                                  //
  private int order;              // The number of vertices in this Graph Instance.
  private int size;               // The number of edges in this Graph Instance.
                                  // This value is dynamically updated when
                                  // the effect of relevant Instance methods
                                  // results in a change in the size of the Graph.
                                  //
  private Object NonEdge = null;  // It is useful to have a distinctive value that
                                  // can indicate if no edge is present between 
                                  // {v,w} in the Graph. Here we give a default
                                  // setting to the null Object. In specific
                                  // instantiations, however, this could be set
                                  // appropriately depending on the application
                                  // used, e.g. as the Boolean FALSE if using the
                                  // basic Boolean representation above; as the
                                  // largest (smallest) possible Integer where
                                  // edges are associated with `distances'; as the
                                  // empty String ("") in a labelling context, etc.
  //*****************************************************//
  // Constructor Definition                              //
  //*****************************************************//
  // The single Constructor provided, allocates storage  //
  // for an n-vertex Graph.                              //
  // The 2-dimensional array matrix[][] is configured    //
  // so that it comprises n-1 rows, the k'th row (k < n) //
  // containing exactly one fewer column than the        //
  // (k-1)'st                                            //
  //*****************************************************//
  // Access and modification to this array can ONLY BE   //
  // performed via the Instance methods defined below    //
  // cf. Presence of private modifier in the field       //
  // definition of matrix[][]                            //
  //*****************************************************//
  public Graph(int n)
    {
    order = n; size=0;
    matrix = new Object[n-1][];       // the number of `rows' for n vertices
    //
    for (int i=0; i < n-1; i++)
      matrix[i] = new Object[n-i-1];  // the number of `columns' for each row.
    //
    for (int i=0; i < n-1; i++)
      for (int j=0; j < n-i-1; j++)
        matrix[i][j] = NonEdge;        // assumes no edges are present.
     }
  //*********************************************************************//
  //                    Instance Methods                                 //
  //*********************************************************************//
  // The following methods provide access to the settings of `simple'    //
  // Class fields.                                                       //
  //*********************************************************************//
  //  public int OrderOf() : Provides access to value of the order field //
  //                         i.e. the number of vertices in this Graph.  //
  //*********************************************************************//
  //  public int SizeOf()  : Access to the field holding the number of   //
  //                         edges in this Graph.                        //
  //*********************************************************************//
  //  public Object NoEdge(): Access the current setting of the Non-edge //
  //                          Object value.                              //
  //*********************************************************************//
  public int OrderOf()
    {
    return order;
    };
  //
  public int SizeOf()
   {
   return size;
   }
  //
  public Object NoEdge()
    {
    if (!(NonEdge==null))           // We have to test this, in order to
      return NonEdge;               // avoid a NullPointerException error
    else                            // which would occur in this
      return "null";                // particular case.
    }
  //*********************************************************************//
  // Given two (distinct) vertices (v and w) with 1 <= v,w <= order,     //
  // return true if this graph contains an edge connecting them.         //
  //*********************************************************************//
  // N.B. Applications using the Graph Class do not have to be aware of  //
  // the storage as a 2-dimensional array. In this, and other methods,   //
  // the values v and w are transalated to give the correct `cell' of    //
  // the 2-dimensional array structure indicating the current state of   //
  // the `edge' {v,w}.                                                   //
  //*********************************************************************//
  public boolean IsEdge( int v, int w)
    {
    if (v < w)
      return !(matrix[v-1][w-v-1]==NonEdge); // Data concerning vertex v
                                             // w.r.t `higher' numbered
                                             // vertices is in row `v-1'
    else
      return !(matrix[w-1][v-w-1]==NonEdge);
    }
  //*********************************************************************//
  // Change the setting of the Object used to indicate that an edge is   //
  // NOT present between two vertices updating the Graph representation  //
  // so that the new Object value is stored.                             //
  //*********************************************************************//
  public void SetNonEdgeValue ( Object no_edge )
    {
    if ( (no_edge.toString()).equalsIgnoreCase("null") ) // Can't pass null
      {                                      // reference directly and
      for (int i=1; i < order; i++)            // so we use the String encoding 
        for (int j=i+1; j < order+1; j++)      // to indicate this setting.
          if (!IsEdge(i,j))
            matrix[i-1][j-i-1] = null;
      NonEdge=null;                          // Do AFTER updating matrix[][]!
      }
    else
      {
      for (int i=1; i < order; i++)            // Same procedure but where a
        for (int j=i+1; j < order+1; j++)      // non-null setting is being used.
          if (!IsEdge(i,j))
            matrix[i-1][j-i-1] = no_edge;
      NonEdge = no_edge;
      };
    }
  //*********************************************************************//
  // The main Instance method for defining a particular graph structure. //
  //*********************************************************************//
  // This allows edges to be added to the Graph and to be removed:       //
  //  To add an edge {v,w} supply an edge setting that is different from //
  // the current NonEdge setting.                                        //
  // To remove an edge {v,w} invoke the method with edge_val instantiated//
  // to the current NonEdge Object value.                                //
  //*********************************************************************//
  public void SetEdge ( Object edge_val, int v, int w)
   {
   if (v==w) 
     matrix[v-1][w-v-1] = null;           // This is a bit clumsy. Will force an
                                          // ArrayOutofBoundsException, 
                                          // which is what's needed.
   else if (v < w) 
     {
     if (!IsEdge(v,w))                    // If there's no edge {v,w}
       {
       if ( !(edge_val.equals(NonEdge)) ) // and we're about to add it then
         {
         size = size+1;                   // the number of edges increases by 1.
         };
       }
     else                                // If there IS an edge {v,w}
       {
       if ( edge_val.equals(NonEdge) )   // and we're about to remove it then
         {
         size=size-1;                    // the number of edges decreases by 1.
         };
       };
     matrix[v-1][w-v-1] = new Object();  // Probably not needed (a precaution).
     matrix[v-1][w-v-1] = edge_val;      // Store the new value.
     }
   else
     this.SetEdge(edge_val,w,v);         // Can't expect applications to ensure
                                         // {v,w} are supplied with v < w so arrange
                                         // this properly.
                                         // Note that if v=w is NOT caught then
                                         // this Method will recurse indefinitely.
                                         // (Don't have to worry about this
                                         // in IsEdge(v,w)
                                         // because an Array bound exception
                                         // will be raised
                                         // by the run-time system if v=w).
   }
  //******************************************************************//
  // Obtain the value of the Object associated with edge {v,w}        //
  //******************************************************************//
  public Object GetEdge (int v, int w)
    {
   if (v==w) 
     return matrix[v-1][w-v-1];      // See similar remarks re. SetEdge
    else if (v < w)
      if (IsEdge(v,w))               // if there's a `real' edge then
        return matrix[v-1][w-v-1];   // just return the associated Object.
      else                           // If there's not a `real' edge
        if (NonEdge==null)           // have to make sure a NullPointerException
                                     // isn't generated.
          return "null";
        else
          return NonEdge.toString(); // Returns the current NonEdge object value.
    else
      return this.GetEdge(w,v);
    }
  //*********************************************************************//
  // Convert the Graph structure to a String form that is suitable for   //
  // output. The edges {v,w} are stored in a String in which newline     //
  // characters separate the different vertex details.                   //
  // The printing form is as order-1 distinct lines, the k'th line       //
  // containing exactly one fewer item than the (k-1)'st line. Hence     //
  //                                                                     //
  // Line 1: Status of edges {1,2} {1,3} {1,4} ... {1,order}             //
  //      2:                 {2,3} {2,4} {2,5} ... {2,order}             //
  //              ...                                                    //
  //      k:                 {k,k+1} {k,k+2}   ... {k,order}             //
  //              ...                                                    //
  //    order-1              {order-1,order}                             //
  //*********************************************************************//
  public String toString()
    {
    String res=new String();
    for (int i=1; i < order; i++)
      {
      for (int j=i+1; j < order+1; j++)
        {
        res = res+GetEdge(i,j).toString()+" ";
        };
        res = res+"\n";
      };
    return res;
    }
  }

Figure 4.14 The Undirected Graph Class

4. A Simple Illustration of the Graph Class in Use

The Java program presented below provides a simple example of the Graph class in practice.

This reads in the integer value, n, defining the number of vertices and uses Integer as the basic edge Object, (recall that the primitive types are not recognised as Objects in Java, so we must use the Wrapper class Integer to store these). The Integer 0 is used as the non-edge Object, and we use the Integer 1 to indicate the presence of an edge between two vertices.

All that the program below does is to read in a sequence of (n*(n-1)/2) integer values (recall that this is the number of possible edges in an n-vertex undirected graph), interpreting positive values as indicating edges and other values as indicating no edge.

The program then computes the graph complement - the graph that results by adding edges which are missing and removing edges which were present.


//
// COMP102 
// Example 7:  Simple Application of the Graph Class
//
// Paul E. Dunne  28/10/99
//
import java.io.*;
import Graph;                 // The Graph Class Definition.
import PED_NumberIO;          // Ignore this (it's my own solution to
                              // to an irritating I/O problem.
public class SimpleGraphApp
  {
  public static InputStreamReader input = new InputStreamReader(System.in);
  public static BufferedReader   keyboardInput = new BufferedReader(input);
  static Graph G;
  static final Integer Present = new Integer(1);      // Object to indcate an edge
  static final Integer Absent  = new Integer(0);      // Object to indicate no edge.
                                                      // N.B. Use of wrapper.
  static int n;                                       // Will contain the number
                                                      // of vertices (G's order)
  static int edge;
  //
  public static void main(String[] args) throws IOException
    {
    System.out.println("Number of vertices in the Graph?:");
    n= new Integer(keyboardInput.readLine()).intValue();
    G = new Graph(n);                                // Set up the n-vertex Graph
    G.SetNonEdgeValue(Absent);                       // and the non-edge indicator
                                                     // to be used.
    for (int v=1; v < G.OrderOf(); v++)
      for (int w=v+1; w < G.OrderOf()+1; w++)
        {
        edge =PED_NumberIO.get_int(keyboardInput);
        if (edge > 0)
          G.SetEdge(Present,v,w);
        else
          G.SetEdge(Absent,v,w);
        };
    System.out.println(G.toString());
    //
    // Now compute the Complement of G
    //
    for (int v=1; v < G.OrderOf(); v++)
      for (int w=v+1; w < G.OrderOf()+1; w++)
        if (G.IsEdge(v,w))
          G.SetEdge(Absent,v,w);
        else
          G.SetEdge(Present,v,w);
    System.out.println(G.toString());
    }
  }

Figure 4.15 A Simple Application of the Graph Class

If we run this program using the input,

10
2 3 4 5 6 0 8 0 0
0 4 0 6 0 8 0 10
4 0 0 0 8 9 0
0 0 7 0 0 10
6 0 8 0 0
0 8 9 0
8 0 10
9 10
10

this will produce the output

111110100
01010101
1000110
001001
10100
0110
101
11
1
000001011
10101010
0111001
110110
01011
1001
010
00
0

5. Summary

  1. The instantiation of unconstrained multi-dimensional arrays within Java can be performed in a similar fashion to the mechanism employed for 1-dimensional arrays. The Java realisation, however, allows the `sub-array' structures to have differing lengths.
  2. Although applications exploiting this are infrequent there are structures for which this capability can be used to yield more efficient (in terms of storage used) realisations: one of the most important of these is in modelling undirected graphs
  3. Graph structures occur throughout Computer Science, and their use as a modelling abstraction will be apparent in all areas studied during the three years of the degree.
  4. Graphs are a very important abstract data type in their own right and the study of algorithms involving graphs a major sub-area within the field of algorithmics.
  5. The specific realisation of graph as an ADT given in this lecture is far from being the only one possible. For a number of special types of graph there are drawbacks with the form used. Some alternative methods will be discussed to illustrate examples of using dynamic data structures which are to be considered over the next few lectures.


(Notes prepared and maintained by Paul E. Dunne, October 1999)