C#-数据结构-图的存储结构

6.2 图的存储结构 

图是一种复杂的数据结构,顶点之间是多对多的关系,即任意两个顶点之间 都可能存在联系。所以,无法以顶点在存储区的位置关系来表示顶点之间的联系, 即顺序存储结构不能完全存储图的信息,但可以用数组来存储图的顶点信息。要 存储顶点之间的联系必须用链式存储结构或者二维数组。图的存储结构有多种, 这里只介绍两种基本的存储结构:邻接矩阵和邻接表。 

6.2.1 邻接矩阵 

邻接矩阵(Adjacency Matrix)是用两个数组来表示图,一个数组是一维数 组,存储图中顶点的信息,一个数组是二维数组,即矩阵,存储顶点之间相邻的 信息,也就是边(或弧)的信息,这是邻接矩阵名称的由来。 假设图G=(V,E)中有n个顶点,即V={v0,v1,…,vn-1},用矩阵A[i][j]表示边 (或弧)的信息。矩阵A[i][j]是一个n×n的矩阵,矩阵的元素为: 

若 G 是网,则邻接矩阵可定义为

其中,wij 表示边(vi,vj)或弧<vi,vj>上的权值;∞表示一个计算机允许的大于 所有边上权值的数。 图6.1(a)、图6.2(a)、图6.2(b)的图的邻接矩阵如图6.8(a)、6.8(b)、6.8(c) 所示。 
⎟ 

从图的邻接矩阵表示法可以看出这种表示法的特点是: 

(1)无向图或无向网的邻接矩阵一定是一个对称矩阵。因此,在具体存放 邻接矩阵时只需存放上(或下)三角矩阵的元素即可。

(2)可以很方便地查找图中任一顶点的度。对于无向图或无向网而言,顶 点 vi 的度就是邻接矩阵中第 i 行或第 i 列中非 0 或非∞的元素的个数。对于有 向图或有向网而言,顶点 vi 的入度是邻接矩阵中第 i 列中非 0 或非∞的元素的 个数,顶点 vi 的出度是邻接矩阵中第 i 行中非 0 或非∞的元素的个数。

(3)可以很方便地查找图中任一条边或弧的权值,只要 A[i][j]为 0 或∞, 就说明顶点 vi 和 vj 之间不存在边或弧。但是,要确定图中有多少条边或弧,则 必须按行、按列对每个元素进行检测,所花费的时间代价是很大的。这是用邻接 矩阵存储图的局限性。 

下面以无向图的邻接矩阵类的实现来说明图的邻接矩阵表示的类的实现。 无向图邻接矩阵类 GraphAdjMatrix<T>中有三个成员字段,一个是 Node<T> 类型的一维数组 nodes,存放图中的顶点信息;一个是整型的二维数组 matirx, 表示图的邻接矩阵,存放边的信息;一个是整数 numEdges,表示图中边的数目。 因为图的邻接矩阵存储结构对于确定图中边或弧的数目要花费很大的时间代价, 所以设了这个字段。 

无向图邻接矩阵类 GraphAdjMatrix<T>的实现如下所示

public class GraphAdjMatrixs<T> : IGraph<T>
{
    private Node<T>[] nodes;       //顶点数组 
    private int numEdges;          //边的数目
    private int[,] matrix;        //邻接矩阵数组 

    //构造器 
    public GraphAdjMatrixs(int n)
    {
        nodes = new Node<T>[n];
        matrix = new int[n, n];
        numEdges = 0;
    }

    //获取索引为index的顶点的信息 
    public Node<T> GetNode(int index)
    {
        return nodes[index];
    }

    //设置索引为index的顶点的信息   
    public void SetNode(int index, Node<T> v)
    {
        nodes[index] = v;
    }

    //边的数目属性         
    public int NumEdges
    {
        get
        {
            return numEdges;
        }
        set
        {
            numEdges = value;
        }
    }
    //获取matrix[index1, index2]的值  
    public int GetMatrix(int index1, int index2)
    {
        return matrix[index1, index2];
    }
    //设置matrix[index1, index2]的值  
    public void SetMatrix(int index1, int index2) 
    {
        matrix[index1, index2] = 1; 
    }
    //获取顶点的数目    
    public int GetNumOfVertex()
    { 
        return nodes.Length;
    }

    //获取边的数目      
    public int GetNumOfEdge()
    { 
        return numEdges; 
    }

    //判断v是否是图的顶点 
    public bool IsNode(Node<T> v)
    {            
        //遍历顶点数组 
        foreach (Node<T> nd in nodes)             
        {                 
            //如果顶点nd与v相等,则v是图的顶点,返回true
            if (v.Equals(nd))                 
            {                     
                return true;                 
            }             
        } 
        return false; 
    }

    //获取顶点v在顶点数组中的索引   
    public int GetIndex(Node<T> v)
    { 
        int i = -1;                          
        //遍历顶点数组    
        for (i = 0; i < nodes.Length; ++i)
        {
            //如果顶点v与nodes[i]相等,则v是图的顶点,返回索引值i。  
            if (nodes[i].Equals(v)) 
            { 
                return i; 
            } 
        } 
        return i;
    }

    //在顶点v1和v2之间添加权值为v的边 

    public void SetEdge(Node<T> v1, Node<T> v2,int v)
    {             
        //v1或v2不是图的顶点   
        if (!IsNode(v1) || !IsNode(v2))
        {
            Debug.WriteLine("Node is not belong to Graph!"); return;
        }
        //不是无向图     
        if (v != 1)
        { 
            Debug.WriteLine("Weight is not right!"); return;
        }
        //矩阵是对称矩阵  
        matrix[GetIndex(v1), GetIndex(v2)] = v; 
        matrix[GetIndex(v2), GetIndex(v1)] = v;
        ++numEdges; 
    }

    //删除顶点v1和v2之间的边   
    public void DelEdge(Node<T> v1, Node<T> v2)
    {
        //v1或v2不是图的顶点      
        if (!IsNode(v1) || !IsNode(v2))
        {
            Debug.WriteLine("Node is not belong to Graph!"); return;
        }
        //顶点v1与v2之间存在边   
        if (matrix[GetIndex(v1), GetIndex(v2)] == 1)
        {
            //矩阵是对称矩阵  
            matrix[GetIndex(v1), GetIndex(v2)] = 0; 
            matrix[GetIndex(v2), GetIndex(v1)] = 0;
            --numEdges;
        }
    }

    //判断顶点v1与v2之间是否存在边   
    public bool IsEdge(Node<T> v1, Node<T> v2)

    {
        //v1或v2不是图的顶点 

        if (!IsNode(v1) || !IsNode(v2))
        {
            Debug.WriteLine("Node is not belong to Graph!"); 
            return false;
        }

        //顶点v1与v2之间存在边            
        if (matrix[GetIndex(v1), GetIndex(v2)] == 1)
        {
            return true;
        }
        else  //不存在边  
        { 
            return false; 
        } 
    }
}

无向图邻接矩阵类 GraphAdjMatrix<T>除了实现了接口 IGraph<T>中的方法 外,本身还有两个成员方法,一个是 IsNode,功能是判断一个顶点是否是无向 图的顶点,因为我们对不是图中的顶点进行处理是毫无意义的;一个是 GetIndex,功能是得到图的某个顶点在 nodes 数组中的序号,因为 matrix 数组 的下标是整数而不是顶点类型。 由于无向图邻接矩阵类 GraphAdjMatrix<T>中的成员方法的实现比较简单, 这里就不一一进行说明

6.2.2 邻接表

邻接表(Adjacency List)是图的一种顺序存储与链式存储相结合的存储结构, 类似于树的孩子链表表示法。顺序存储指的是图中的顶点信息用一个顶点数组来 存储,一个顶点数组元素是一个顶点结点,顶点结点有两个域,一个是数据域 data,存放与顶点相关的信息,一个是引用域 firstAdj,存放该顶点的邻接表的第 一个结点的地址。顶点的邻接表是把所有邻接于某顶点的顶点构成的一个表,它 是采用链式存储结构。所以,我们说邻接表是图的一种顺序存储与链式存储相结 合的存储结构。其中,邻接表中的每个结点实际上保存的是与该顶点相关的边或 弧的信息,它有两个域,一个是邻接顶点域 adjvex,存放邻接顶点的信息,实际 上就是邻接顶点在顶点数组中的序号;一个是引用域 next,存放下一个邻接顶点 的结点的地址。

顶点结点和邻接表结点的结构如图 6.9 所示

而对于网的邻接表结点还需要存储边上的信息(如权值),所以结点应增设一个域 info。网的邻接表结点的结构如图 6.10 所示。 

图 6.1(a)的邻接表如图 6.11 所示。 

 

若无向图中有 n 个顶点和 e 条边,则它的邻接表需 n 个顶点结点和 2e 个邻
接表结点,在边稀疏的情况下,用邻接表存储图比用邻接矩阵节 省存储空间,当与边相关的信息较多时更是如此。 

在无向图的邻接表中,顶点 vi 的度恰为第 i 个邻接表中的结点数;而在有向 图中,第 i 的邻接表中的结点数只是顶点 vi 的出度,为求入度,必须遍历整个邻 接表。在所有邻接表中其邻接顶点域的值为 i 的结点的个数是顶点 vi 的入度。有 时,为了便于确定顶点的入度或者以顶点 vi 为头的弧,可以建立一个有向图的 逆邻接表,即对每个顶点 vi 建立一个以 vi 为头的弧的邻接表。图 6.12 是图 6.1(b) 的邻接表和逆邻接表。

在建立邻接表或逆邻接表时,若输入的顶点信息即为顶点的编号,则建立邻 接表的时间复杂度为 O(n+e),否则,需要查找才能得到顶点在图中的位置,则 时间复杂度为 O(n*e)

在邻接表上很容易找到任一顶点的第一个邻接点和下一个邻接点。但要判定 任意两个顶点(vi 和 v j)之间是否有边或弧相连,则需查找第 i 个或 j 个邻接表, 因此,不如邻接矩阵方便。 

下面以无向图邻接表类的实现来说明图的邻接表类的实现。 无向图邻接表的邻接表结点类 adjListNode<T>有两个成员字段,一个是 adjvex,存储邻接顶点的信息,类型是整型;一个是 next,存储下一个邻接表结 点的地址,类型是 adjListNode<T>。adjListNode<T>的实现如下所示

public class adjListNode<T>
{
    private int adjvex;                //邻接顶点 
    private adjListNode<T> next;      //下一个邻接表结点 

    //邻接顶点属性 
    public int Adjvex
    {
        get
        {
            return adjvex;
        }
        set
        {
            adjvex = value;
        }
    }

    //下一个邻接表结点属性 
    public adjListNode<T> Next
    {
        get
        {
            return next;
        }
        set
        {
            next = value;
        }
    }

    //构造器 
    public adjListNode(int vex)
    {
        adjvex = vex; next = null;
    }
}

无向图邻接表的顶点结点类 VexNode<T>有两个成员字段,一个 data,它存 储图的顶点本身的信息,类型是 Node<T>;一个是 firstAdj,存储顶点的邻接表的 第 1 个结点的地址,类型是 adjListNode<T>。VexNode<T>的实现如下所示。 

public class VexNode<T>
{
    private Node<T> data;             //图的顶点
    private adjListNode<T> firstAdj;  //邻接表的第1个结点 

    //图的顶点属性 
    public Node<T> Data
    {
        get
        {
            return data;
        }
        set
        {
            data = value;
        }
    }

    //邻接表的第1个结点属性 
    public adjListNode<T> FirstAdj
    {
        get
        {
            return firstAdj;
        }
        set
        {
            firstAdj = value;
        }
    }

    //构造器 
    public VexNode()
    {
        data = null; firstAdj = null;
    }

    //构造器 
    public VexNode(Node<T> nd)
    {
        data = nd; firstAdj = null;
    }

    //构造器 
    public VexNode(Node<T> nd, adjListNode<T> alNode)
    {
        data = nd; firstAdj = alNode;
    }
}

无向图邻接表类 GraphAdjList<T>有一个成员字段 adjList,表示邻接表数组, 数组元素的类型是 VexNode<T>。GraphAdjList<T>实现了接口 IGraph<T>中的方 法。与无向图邻接矩阵类 GraphAdjMatrix<T>一样,GraphAdjList<T>实现了两个 成员方法 IsNode 和 GetIndex。功能与 GraphAdjMatrix<T>一样。无向图邻接表 类 GraphAdjList<T>的实现如下所示。 

public class GraphAdjList<T> : IGraph<T>
{
    //邻接表数组 
    private VexNode<T>[] adjList;

    //索引器 
    public VexNode<T> this[int index]
    {
        get
        {
            return adjList[index];
        }
        set
        {
            adjList[index] = value;
        }
    }

    //构造器 
    public GraphAdjList(Node<T>[] nodes)
    {
        adjList = new VexNode<T>[nodes.Length]; for (int i = 0; i < nodes.Length; ++i)
        {
            adjList[i].Data = nodes[i]; adjList[i].FirstAdj = null;
        }
    }

    //获取顶点的数目 
    public int GetNumOfVertex()
    {
        return adjList.Length;
    }

    //获取边的数目 
    public int GetNumOfEdge()
    {
        int i = 0;

        //遍历邻接表数组 
        foreach (VexNode<T> nd in adjList)
        {
            adjListNode<T> p = nd.FirstAdj; while (p != null)
            {
                ++i; p = p.Next
                }
        }

        return i / 2;
    }

    //判断v是否是图的顶点 
    public bool IsNode(Node<T> v)
    {
        //遍历邻接表数组 
        foreach (VexNode<T> nd in adjList)
        {
            //如果v等于nd的data,则v是图中的顶点,返回true 
            if (v.Equals(nd.Data))
            {
                return true;
            }
        }

        return false;
    }

    //获取顶点v在邻接表数组中的索引 
    public int GetIndex(Node<T> v)
    {
        int i = -1;

        //遍历邻接表数组 
        for (i = 0; i < adjList.Length; ++i)
        {
            //邻接表数组第i项的data值等于v,则顶点v的索引为i 
            if (adjList[i].Data.Equals(v))
            {
                return i;
            }
        }
        return i;
    }

    //在顶点v1和v2之间添加权值为v的边 
    public void SetEdge(Node<T> v1, Node<T> v2, int v)
    {
        //v1或v2不是图的顶点或者v1和v2之间存在边 
        if (!IsNode(v1) || !IsNode(v2) || IsEdge(v1, v2))
        {
            Debug.WriteLine("Node is not belong to Graph!"); return;
        }

        //权值不对           
        if(v != 1) 
        {
            Debug.WriteLine("Weight is not right!"); return;
        }

        //处理顶点v1的邻接表 
        adjListNode<T> p = new adjListNode<T>(GetIndex(v2));

        //顶点v1没有邻接顶点 
        if (adjList[GetIndex(v1)].FirstAdj == null)
        {
            adjList[GetIndex(v1)].FirstAdj = p;
        }
        //顶点v1有邻接顶点 
        else
        {
            p.Next = adjList[GetIndex(v1)].FirstAdj; adjList[GetIndex(v1)].FirstAdj = p;
        }

        //处理顶点v2的邻接表 
        p = new adjListNode<T>(GetIndex(v1));

        //顶点v2没有邻接顶点 
        if (adjList[GetIndex(v2)].FirstAdj == null)
        {
            adjList[GetIndex(v2)].FirstAdj = p;
        }
        //顶点v1有邻接顶点 
        else
        {
            p.Next = adjList[GetIndex(v2)].FirstAdj; adjList[GetIndex(v2)].FirstAdj = p;
        }
    }

    //删除顶点v1和v2之间的边 
    public void DelEdge(Node<T> v1, Node<T> v2)
    {
        //v1或v2不是图的顶点 
        if (!IsNode(v1) || !IsNode(v2))
        {
            Debug.WriteLine("Node is not belong to Graph!"); return;
        }

        //顶点v1与v2之间有边 
        if (IsEdge(v1, v2))
        {
            //处理顶点v1的邻接表中的顶点v2的邻接表结点 
            adjListNode<T> p = adjList[GetIndex(v1)].FirstAdj; adjListNode<T> pre = null;

            while (p != null)
            {
                if (p.Adjvex != GetIndex(v2))
                {
                    pre = p; p = p.Next;
                }
            }

            pre.Next = p.Next;

            //处理顶点v2的邻接表中的顶点v1的邻接表结点 
            p = adjList[GetIndex(v2)].FirstAdj; pre = null;

            while (p != null)
            {
                if (p.Adjvex != GetIndex(v1))
                {
                    pre = p; p = p.Next;
                }
            }

            pre.Next = p.Next;
        }
    }

    //判断v1和v2之间是否存在边 
    public bool IsEdge(Node<T> v1, Node<T> v2)
    {
        //v1或v2不是图的顶点 
        if (!IsNode(v1) || !IsNode(v2))
        {
            Debug.WriteLine("Node is not belong to Graph!"); return false;
        }

        adjListNode<T> p = adjList[GetIndex(v1)].FirstAdj; while (p != null)
        {
            if (p.Adjvex == GetIndex(v2))
            {
                return true;
            }

            p = p.Next;
        }

        return false;
    }
}

下面对成员方法进行说明: 

1、GetNumOfVertex() 算法思路:求无向图的顶点数比较简单,直接返回 adjList 数组的长度就 可以了。 算法实现如下:

public int GetNumOfVertex()  
{            
      return adjList.Length;  
}

2、GetNumOfEdge() 算法思路:求无向图的边数比求顶点数要复杂一些,需要求出所有顶点的 邻接表的结点的个数,然后除以 2。 算法实现如下: 

public int GetNumOfEdge()
{
    int i = 0;
    foreach (VexNode<T> nd in adjList)
    {
        adjListNode<T> p = nd.FirstAdj;

        while (p != null) { ++i; }
    }

    return i / 2;
}

3、SetEdge(Node<T> v1, Node<T> v2, int v) 

算法思路:首先判断顶点 v1 和 v2 是否是图的顶点和 v1 和 v2 是否存在边。 如果 v1 和 v2 不是图的顶点和 v1 和 v2 存在边,不作处理。然后,判断 v 的值是 否为1,为1不作处理。否则,先分配一个邻接表结点,其adjvex域是v2在adjList 数组中的索引号,然后把该结点插入到顶点 v1 的邻接表的表头;然后再分配一 个邻接表结点,其 adjvex 域是 v1 在 adjList 数组中的索引号,然后把该结点插 入到顶点 v2 的邻接表的表头。

本算法是把邻接表结点插入到顶点邻接表的表头,当然,也可以插入到邻 接表的表尾,或者按照某种要求插入,只是对插入这个操作而言,在表的头部插 入是简单的,而本书在后面关于图的处理,如图的深度优先遍历和广度优先遍 历等,对图的顶点没有特殊要求,所以采用了在邻接表的头部插入结点。如果对 图的顶点有特殊要求,则需要按照一定的要求进行插入,需要修改这里的代码。 算法实现如下:

public void SetEdge(Node<T> v1, Node<T> v2, int v)
{
    if (!IsNode(v1) || !IsNode(v2) || IsEdge(v1, v2))
    {
        Debug.WriteLine("Node is not belong to Graph!"); return;
    }

    if (v != 1)
    {
        Debug.WriteLine("Weight is not right!"); return;
    }

    adjListNode<T> p = new adjListNode<T>(GetIndex(v2)); if (adjList[GetIndex(v1)].FirstAdj == null)
    {
        adjList[GetIndex(v1)].FirstAdj = p;
    }
    else
    {
        p.Next = adjList[GetIndex(v1)].FirstAdj; adjList[GetIndex(v1)].FirstAdj = p;
    }

    p = new adjListNode<T>(GetIndex(v1)); if (adjList[GetIndex(v2)].FirstAdj == null)
    {
        adjList[GetIndex(v2)].FirstAdj = p;
    }
    else
    {
        p.Next = adjList[GetIndex(v2)].FirstAdj; adjList[GetIndex(v2)].FirstAdj = p;
    }
}

4、DelEdge(Node<T> v1, Node<T> v2) 

算法思路:首先判断顶点 v1 和 v2 是否是图的顶点以及 v1 和 v2 是否存在 边。如果 v1 和 v2 不是图的顶点或 v1 和 v2 不存在边,不作处理。否则,先在顶 点 v1 的邻接表中删除 adjVex 的值等于顶点 v2 在 adjList 数组中的序号结点, 然后删除顶点 v2 的邻接表中 adjVex 的值等于顶点 v1 在 adjList 数组中的序号 结点。

算法实现如下: 

public void DelEdge(Node<T> v1, Node<T> v2)
{
    if (!IsNode(v1) || !IsNode(v2))
    {
        Console.WriteLine("Node is not belong to Graph!"); return;
    }

    if (IsEdge(v1, v2))
    {
        adjListNode<T> p = adjList[GetIndex(v1)].FirstAdj; adjListNode<T> pre = null;

        while (p != null)
        {
            if (p.Adjvex != GetIndex(v2))
            { pre = p; p = p.Next; }
        }

        pre.Next = p.Next;

        p = adjList[GetIndex(v2)].FirstAdj; pre = null;

        while (p != null)
        {
            if (p.Adjvex != GetIndex(v1))
            {
                pre = p; p = p.Next;
            }
        }

        pre.Next = p.Next;
    }
}

5、IsEdge(Node<T> v1, Node<T> v2)

算法思路:首先判断顶点 v1 和 v2 是否是图的顶点。如果 v1 和 v2 不是图的 顶点,不作处理。否则,在顶点 v1(或 v2)的邻接表中查找是否存在 adjVex 的值等于 v2(或 v1)在 adjList 中的序号的结点,如果存在,则返回 true,否 则返回 false。

算法实现如下

public bool IsEdge(Node<T> v1, Node<T> v2)
{
    if (!IsNode(v1) || !IsNode(v2))
    {
        Console.WriteLine("Node is not belong to Graph!"); return false;
    }

    adjListNode<T> p = adjList[GetIndex(v1)].FirstAdj; while (p != null)
    {
        if (p.Adjvex == GetIndex(v2))
        {
            return true;
        }

        p = p.Next;

    }

    return false;
}

 

 

  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
C#版本数据结构,用C#的同志们有福啦 本书节选: 第1章 绪论 数据是外部世界信息的计算机化,是计算机加工处理的对象。运用计算机处 理数据时,必须解决四个方面的问题:一是如何在计算机中方便、高效地表示和 组织数据;二是如何在计算机存储器(内存和外存)中存储数据;三是如何对存 储在计算机中的数据进行操作,可以有哪些操作,如何实现这些操作以及如何对 同一问题的不同操作方法进行评价;四是必须理解每种数据结构的性能特征,以 便选择一个适合于某个特定问题的数据结构。这些问题就是数据结构这门课程所 要研究的主要问题。本章首先说明学习数据结构的必要性和本书的目的,然后解 释数据结构及其有关概念,接着讨论算法的相关知识,最后简单介绍本书所要用 到的相关数学知识和C#知识。 1.1 数据结构 1.1.1 学习数据结构的必要性 我们知道,虽然每个人都懂得英语的语法与基本类型,但是对于同样的题目, 每个人写出的作文,水平却高低不一。程序设计也和写英语作文一样,虽然程序 员都懂得语言的语法与语义,但是对于同样的问题,程序员写出来的程序不一样。 有的人写出来的程序效率很高,有的人却用复杂的方法来解决一个简单的问题。 当然,程序设计水平的提高仅仅靠看几本程序设计书是不行的。只有多思索、 多练习,才能提高自己的程序设计水平;否则,书看得再多,提高也不大。记得 刚学程序设计时,常听人说程序设计水平要想提高,最重要的是多看别人写的程 序,多去思考问题。从别人写的程序中,我们可以发现效率更高的解决方法;从 思考问题的过程中,我们可以了解解决问题的方法常常不只一个。运用先前解决 问题的经验,来解决更复杂更深入的问题,是提高程序设计水平的最有效途径。 数据结构正是前人在思索问题的过程中所想出的解决方法。一般而言,在学 习程序设计一段时间后,学习“数据结构”便能让你的程序设计水平上一个台阶。 如果只学会了程序设计的语法和语义,那么你只能解决程序设计三分之一的问 题,而且运用的方法并不是最有效的。但如果学会了数据结构的概念,就能在程 序设计上,运用最有效的方法来解决绝大多数的问题。 《数据结构》这门课程的目的有三个。第一个是讲授常用的数据结构,这些 数据结构形成了程序员基本数据结构工具箱(toolkit)。对于许多常见的问题,工 具箱里的数据结构是理想的选择。就像.NET Framework 中Windows应用程序开 发中的工具箱,程序员可以直接拿来或经过少许的修改就可以使用,非常方便。 第二个是讲授常用的算法,这和数据结构一样,是人们在长期实践过程中的总结, 程序员可以直接拿来或经过少许的修改就可以使用。可以通过算法训练来提高程 序设计水平。第三个目的是通过程序设计的技能训练促进程序员综合能力的提 高。 1.1.2 基本概念和术语 在本小节中,将对一些常用的概念和术语进行介绍,这些概念和术语在以后 的章节中会多次出现。 1、数据(Data) 数据是外部世界信息的载体,它能够被计算机识别、存储和加工处理,是计 算机程序加工的原料。计算机程序处理各种各样的数据,可以是数值数据,如整 数、实数或复数;也可以是非数值数据,如字符、文字、形、图像、声音等。 2、数据元素(Data Element)和数据项(Data Item) 数据结构C#语言版) 1.1 数据结构2 数据元素是数据的基本单位,在计算机程序中通常被作为一个整体进行考虑 和处理。数据元素有时也被称为元素、结点、顶点、记录等。一个数据元素可由 若干个数据项(Data Item)组成。数据项是不可分割的、含有独立意义的最小数据 单位,数据项有时也称为字段(Field)或域(Domain)。例如,在数据库信息处理系 统中,数据表中的一条记录就是一个数据元素。这条记录中的学生学号、姓名、 性别、籍贯、出生年月、成绩等字段就是数据项。数据项分为两种,一种叫做初 等项,如学生的性别、籍贯等,在处理时不能再进行分割;另一种叫做组合项, 如学生的成绩,它可以再分为数学、物理、化学等更小的项。 3、数据对象(Data Object) 数据对象是性质相同的数据元素的集合,是数据的一个子集。例如,整数数 据对象是{0,±1,±2,±3,…},字符数据对象是{a,b,c,…}。 4、数据类型(Data Type) 数据类型是高级程序设计语言中的概念,是数据的取值范围和对数据进行操 作的总和。数据类型规定了程序中对象的特性。程序中的每个变量、常量或表达 式的结果都应该属于某种确定的数据类型。例如,C#语言中的字符串类型(String, 经常写为string)。一 个String表示一个恒定不变的字符序列集合,所有的字符序 列集合构成String的取值范围。我们可以对String进行求长度、复制、连接两个 字符串等操作。 数据类型可分为两类:一类是非结构的原子类型,如C#语言中的基本类型 (整型、实型、字符型等);另一类是结构类型,它的成分可以由多个结构类型 组成,并可以分解。结构类型的成分可以是非结构的,也可以是结构的。例如, C#语言中数组的成分可以是整型等基本类型,也可以是数组等结构类型。 5、数据结构(Data Structure) 数据结构是相互之间存在一种或多种特定关系的数据元素的集合。在任何问 题中,数据元素之间都不是孤立的,而是存在着一定的关系,这种关系称为结构 (Structure)。根据数据元素之间关系的不同特性,通常有4类基本数据结构: (1) 集合(Set):如1.1(a)所示,该结构中的数据元素除了存在“同属于一个集 合”的关系外,不存在任何其它关系。 (2) 线性结构(Linear Structure):如1.1(b)所示,该结构中的数据元素存在着一 对一的关系。 (3) 树形结构(Tree Structure):如1.1(c)所示,该结构中的数据元素存在着一对 多的关系。 (4) 状结构(Graphic Structure):如1.1(d)所示,该结构中的数据元素存在着 多对多的关系。 (a) 集合 (b) 线性结构 (c) 树形结构 (d)状结构 1.1 4 类基本数据结构关系 由于集合中的元素的关系极为松散,可用其它数据结构来表示,所以本书不 做专门介绍。关于集合的概念在1.3.1小节中有介绍。 数据结构的形式化定义为: 数据结构C#语言版) 1.1 数据结构3 数据结构(Data Structure)简记为DS,是一个二元组, DS = (D,R) 其中:D是数据元素的有限集合, R是数据元素之间关系的有限集合。 下面通过例题来进一步理解后3类数据结构。 【例1-1】 学生信息表(如表1.1所示.)是一个线性的数据结构,表中的每 一行是一个记录(在数据库信息处理系统中,表中的一个数据元素称为一个记 录)。一条记录由学号、姓名、行政班级、性别和出生年月等数据项组成。表中 数据元素之间的关系是一对一的关系。 表 1.1 学生信息表 学号 姓名 行政班级 性别 出生年月 040303001 雷洪 软件04103 男 1986.12 040303002 李春 软件04103 女 1987.3 040303003 周刚 软件04103 男 1986.9 【例1-2】 家族关系是典型的树形结构,1.2是一个三代的家族关系。在 中,爷爷、儿子、女儿、孙子、孙女或外孙女是一个结点(在树形结构中,数 据元素称为结点),他们之间是一对多的关系。其中,爷爷有两个儿子和一个女 儿,这是一对三的关系;一个儿子有两个儿子(爷爷的孙子),这是一对二的关 系;另一个儿子有一个儿子(爷爷的孙子)和一个女儿(爷爷的孙女),这是一 对二的关系;女儿有三个女儿(爷爷的外孙女),这是一对三的关系。树形结构 具有严格的层次关系,爷爷在树形结构的最上层,中间层是儿子和女儿,最下层 是孙子、孙女和外孙女。不能把这种关系倒过来,因为绝对不会先有儿子或女儿 再有爷爷,也不会先有孙子或孙女再有儿子、先有外孙女再有女儿。 外孙女 爷爷 儿子 儿子 女儿 孙子 孙子 孙子 孙女 外孙女 外孙女 1.2 家族关系 【例1-3】 1.3是四个城市的公路交通,这是一个典型的状结构。在 中,每个城市是一个顶点(在状结构中,数据元素称为顶点),它们之间是 多对多的关系。成都与都江堰、雅安直接通公路,都江堰与成都、青城山直接通 公路,青城山与都江堰、成都及雅安直接通公路,雅安与成都、青城山直接通公 路。这些公路构成了一个公路交通网,所以,又把状结构称为网状结构(Network Structure) 数据结构C#语言版) 1.2 算法4 成都 都江堰 青城山 雅安 1.3 四城市交通 从数据类型和数据结构的概念可知,二者的关系非常密切。数据类型可以看 作是简单的数据结构。数据的取值范围可以看作是数据元素的有限集合,而对数 据进行操作的集合可以看作是数据元素之间关系的集合。 数据结构包括数据的逻辑结构和物理结构。上述数据结构的定义就是数据的 逻辑结构(Logic Structure),数据的逻辑结构是从具体问题抽象出来的数学模型, 是为了讨论问题的方便,与数据在计算机中的具体存储没有关系。然而,我们讨 论数据结构的目的是为了在计算机中实现对它的操作,因此还需要研究在计算机 中如何表示和存储数据结构,即数据的物理结构(Physical Structure)。数据的物理 结构又称为存储结构(Storage Structure),是数据在计算机中的表示(又叫映像) 和存储,包括数据元素的表示和存储以及数据元素之间关系的表示和存储。 数据的存储结构包括顺序存储结构和链式存储结构两种。顺序存储结构 (Sequence Storage Structure)是通过数据元素在计算机存储器中的相对位置来表 示出数据元素的逻辑关系,一般把逻辑上相邻的数据元素存储在物理位置相邻的 存储单元中。在C#语言中用数组来实现顺序存储结构。因为数组所分配的存储 空间是连续的,所以数组天生就具有实现数据顺序存储结构的能力。链式存储结 构(Linked Storage Structure)对逻辑上相邻的数据元素不要求其存储位置必须相 邻。链式存储结构中的数据元素称为结点(Node),在结点中附设地址域(Address Domain)来存储与该结点相邻的结点的地址来实现结点间的逻辑关系。这个地址 称为引用(Reference),这个地址域称为引用域(Reference Domain)。 从20世纪60年代末到70年代初,出现了大型程序,软件也相对独立,人 们越来越重视数据结构,认为程序设计的实质是确定数据结构,加上设计一个好 的算法,这就是人们常说的“程序=数据结构+算法”。下一节谈谈算法的问题。 1.2 算法 从上节我们知道,算法与数据结构和程序的关系非常密切。进行程序设计时, 先确定相应的数据结构,然后再根据数据结构和问题的需要设计相应的算法。由 于篇幅所限,下面只从算法的特性、算法的评价标准和算法的时间复杂度等三个 方面进行介绍。 1.2.1 算法的特性 算法(Algorithm)是对某一特定类型的问题的求解步骤的一种描述,是指令的 有限序列。其中的每条指令表示一个或多个操作。一个算法应该具备以下5个特 性: 1、有穷性(Finity):一个算法总是在执行有穷步之后结束,即算法的执行时间是 有限的。 2、确定性(Unambiguousness):算法的每一个步骤都必须有确切的含义,即无二 义,并且对于相同的输入只能有相同的输出。 3、输入(Input):一个算法具有零个或多个输入。它即是在算法开始之前给出的 数据结构C#语言版) 1.2 算法5 量。这些输入是某数据结构中的数据对象。 4、 输出(Output):一个算法具有一个或多个输出,并且这些输出与输入之间存 在着某种特定的关系。 5、 能行性(realizability):算法中的每一步都可以通过已经实现的基本运算的有 限次运行来实现。 算法的含义与程序非常相似,但二者有区别。一个程序不一定满足有穷性。 例如操作系统,只要整个系统不遭破坏,它将永远不会停止。还有,一个程序只 能用计算机语言来描述,也就是说,程序中的指令必须是机器可执行的,而算法 不一定用计算机语言来描述,自然语言、框、伪代码都可以描述算法。 在本书中我们尽可能采用C#语言来描述和实现算法,使读者能够阅读或上 机执行,以便更好地理解算法。 1.2.2 算法的评价标准 对于一个特定的问题,采用的数据结构不同,其设计的算法一般也不同,即 使在同一种数据结构下,也可以采用不同的算法。那么,对于解决同一问题的不 同算法,选择哪一种算法比较合适,以及如何对现有的算法进行改进,从而设计 出更适合于数据结构的算法,这就是算法评价的问题。评价一个算法优劣的主要 标准如下: 1、正确性(Correctness)。算法的执行结果应当满足预先规定的功能和性能的要求, 这是评价一个算法的最重要也是最基本的标准。算法的正确性还包括对于输入、 输出处理的明确而无歧义的描述。 2、可读性(Readability)。算法主要是为了人阅读和交流,其次才是机器的执行。 所以,一个算法应当思路清晰、层次分明、简单明了、易读易懂。即使算法已转 变成机器可执行的程序,也需要考虑人能较好地阅读理解。同时,一个可读性强 的算法也有助于对算法中隐藏错误的排除和算法的移植。 3、健壮性(Robustness)。一个算法应该具有很强的容错能力,当输入不合法的数 据时,算法应当能做适当的处理,使得不至于引起严重的后果。健壮性要求表明 算法要全面细致地考虑所有可能出现的边界情况和异常情况,并对这些边界情况 和异常情况做出妥善的处理,尽可能使算法没有意外的情况发生。 4、运行时间(Running Time)。运行时间是指算法在计算机上运行所花费的时间, 它等于算法中每条语句执行时间的总和。对于同一个问题如果有多个算法可供选 择,应尽可能选择执行时间短的算法。一般来说,执行时间越短,性能越好。 5、占用空间(Storage Space)。占用空间是指算法在计算机上存储所占用的存储空 间,包括存储算法本身所占用的存储空间、算法的输入及输出数据所占用的存储 空间和算法在运行过程中临时占用的存储空间。算法占用的存储空间是指算法执 行过程中所需要的最大存储空间,对于一个问题如果有多个算法可供选择,应尽 可能选择存储量需求低的算法。实际上,算法的时间效率和空间效率经常是一对 矛盾,相互抵触。我们要根据问题的实际需要进行灵活的处理,有时需要牺牲空 间来换取时间,有时需要牺牲时间来换取空间。 通常把算法在运行过程中临时占用的存储空间的大小叫算法的空间复杂度 (Space Complexity)。算法的空间复杂度比较容易计算,它主要包括局部变量所占 用的存储空间和系统为实现递归所使用的堆栈占用的存储空间。 如果算法是用计算机语言来描述的,还要看程序代码量的大小。对于同一个 问题,在用上面5条标准评价的结果相同的情况下,代码量越少越好。实际上, 代码量越大,占用的存储空间会越多,程序的运行时间也可能越长,出错的可能 数据结构C#语言版) 1.2 算法6 性也越大,阅读起来也越麻烦。 在以上标准中,本书主要考虑程序的运行时间,也考虑执行程序所占用的空 间。影响程序运行时间的因素很多,包括算法本身、输入的数据以及运行程序的 计算机系统等。计算机的性能由以下因素决定: 1、硬件条件。包括所使用的处理器的类型和速度(比如,使用双核处理器还是 单核处理器)、可使用的内存(缓存和RAM)以及可使用的外存等。 2、实现算法所使用的计算机语言。实现算法的语言级别越高,其执行效率相对 越低。 3、所使用的语言的编译器/解释器。一般而言,编译的执行效率高于解释,但解 释具有更大的灵活性。 4、所使用的操作系统软件。操作系统的功能主要是管理计算机系统的软件和硬 件资源,为计算机用户方便使用计算机提供一个接口。各种语言处理程序如编译 程序、解释程序等和应用程序都在操作系统的控制下运行。 1.2.3 算法的时间复杂度 一个算法的时间复杂度(Time Complexity)是指该算法的运行时间与问题规 模的对应关系。一个算法是由控制结构和原操作构成的,其执行的时间取决于二 者的综合效果。为了便于比较同一问题的不同算法,通常把算法中基本操作重复 执行的次数(频度)作为算法的时间复杂度。算法中的基本操作一般是指算法中 最深层循环内的语句,因此,算法中基本操作语句的频度是问题规模n的某个函 数f(n),记作:T(n)=O(f(n))。其中“O”表示随问题规模n的增大,算法执行时 间的增长率和f(n)的增长率相同,或者说,用“O”符号表示数量级的概念。例 如,如 )1n(n 2 1 )n(T −= ,则 )1n(n 2 1 −的数量级与n2 相同,所以T(n)=O(n2 )。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值