五、图

五、图

图是比较复杂的数据结构,它由顶点和顶点之间的弧组成。任何两个顶点之间都可能存在弧,利用计算机存储图的完整信息,就要求能存储图的类型(有向或无向,带权或无权)、顶点个数、弧的条数及每个顶点的信息、每对顶点之间是否存在弧或弧的信息。根据这些信息,可以完整描述图,继而对图进行操作。本章介绍两种图的存储结构,它们既有共性,又有特性。共性部分作为基类在 AGraph 类中,特性部分作为派生类分别在 MatrixGraph 和 AdjListGraph 中。

实现

//图抽象类的定义
template<typename VerT>class AGraph
{//定义顶点结构模板的图抽象类
private:
  int LocateVex(VerT u)const;  //返回顶点 u 的序号,不存在则返回 -1
  void DFS(int i, void(*visit) (VerT), bool Visited[])
  {//从顶点[i]出发递归深度优先遍历图
    Visited[i] = true;  //设置访问标志为 true (已访问)
    visit(GetVex(i));  //访问顶点[i]
    for(int j = FirstAdjVex(i); j >= 0; j = NextAdjVex(i, j))
    //从顶点[i]的第一个邻接顶点[j]开始
      if (!Visited[j])  //邻接顶点[j]尚未被访问
        DFS(j, visit, Visited);
  }
protected:
  int ArcNum;  //图的弧数
  bool Weight;  //true:带权;false:无权
  bool Directed;  //true:有向;false:无向
public:
  void CreateFromFile(char* FileName);  //由数据文件创建图
  bool PutVex(VerT v, VerT value);  //对顶点 v 赋新值 value
  virtual int FirstAdjVex(int i)const=0;
  //返回顶点[i]的第一个邻接顶点的序号,不存在则返回 -1
  virtual int NextAdjVex(int i, int j)const=0;
  //返回顶点[i](相对于顶点[j])的下一个邻接顶点的序号,不存在则返回 -1
  void InsertVex(VerT v);  //增添新顶点 v (不增添与顶点的相关弧)
  bool DeleteVex(VerT v);  //删除顶点 v 及相关弧
  bool InsertArc(VerT v, VerT w);//增添弧<v,w>,若是无向的,则还增加对称弧<w,v>
  bool DeleteArc(VerT v, VerT w);//删除弧<v,w>,若是无向的,则还删除对称弧<w,v>
  void Display()const;  //显示图的存储数据
  void DFSTraverse(void(*visit) (VerT))
  {//从顶点[0]起,深度优先递归遍历图
    int i;
    bool *Visited = new bool[GetVexNumOrArcNum(0)];
    for(i = 0; i < GetVexNumOrArcNum(0); i++)
      Visited[i] = false;  //未被访问
    for(i = 0; i < GetVexNumOrArcNum(0); i++)
      if (!Visited[i])
        DFS(i, visit, Visited);
    cout << endl;
    delete[] Visited;
  }
  void BFSTraverse(void(*visit) (VerT))
  {//从顶点[0]起,广度优先递归遍历图
    int i, j, k;
    bool *Visited = new bool[GetVexNumOrArcNum(0)];
    queue<int> q;  //存储顶点序号
    for(i = 0; i < GetVexNumOrArcNum(0); i++)
      Visited[i] = false;  //未被访问
    for(i = 0; i < GetVexNumOrArcNum(0); i++)
      if (!Visited[i])
      {
        Visited[i] = true;
        visit(GetVex(i));
        q.push(i);
        while(!q.empty())
        {
          j = q.front();
          q.pop();
          for(k = FirstAdjVex(j); k >= 0; k = NextAdjVex(j, k))
            if (!Visited[k])
            {
              Visited[k] = true;
              visit(GetVex(k));
              q.push(k);
            }
        }
      }
    cout << endl;
    delete[] Visited;
  }
  virtual int GetVexNumOrArcNum(int i)const=0; //返回图的顶点数(i=0)或弧数(i=1)
  bool GetDirectedOrWeight(int i)const
  {//返回图的有向性(i = 0)或带权性(i = 1)
    if (i == 0)
      return Directed;
    else
      return Weight;
  }
  virtual VerT GetVex(int i)const=0;  //返回顶点[i]的值
};

//对图的两个顶点相等的宏定义和定义权值类型
#define EQ(a, b) (a.name == b.name)
typedef int VRType;  //定义定点关系(权值)类型为整型

1. 图的邻接矩阵存储结构

由于图的顶点和弧在结构上完全没有关系,因此图类的模板就有两种:顶点结构 VerT 模板和弧结构 ArcT 模板。

实现

//图的邻接矩阵类
template<typename VerT, typename ArcT>class MatrixGraph: public AGraph<VerT>
{//带模板并继承 AGraph<VerT> 的邻接矩阵类
protected:
  int VexNum;  //图的顶点数
  VerT vexs[MAX_VERTEX_NUM];  //顶点向量
  ArcT arcs[MAX_VERTEX_NUM][MAX_VERTEX_NUM];  //邻接矩阵,存储弧信息
  int LocateVex(VerT u)const
  {//返回与 u 具有相同关键字的顶点序号,不存在则返回-1
    int i;
    for(i = 0; i < VexNum; ++i)
      if EQ(u, vexs[i])
        return i;
    return -1;
  }
public:
  MatrixGraph()
  {//构造函数,构造一个空图
    VexNum = ArcNum = 0;
  }
  void DestroyGraph()
  {//销毁图
    for(int i = VexNum - 1; i >= 0; i--)
      DeleteVex(vexs[i]);
  }
  void CreateFromFile(char* FileName)
  {//由数据文件创建图
    int i, j, k;
    VRType w = 0;  //初始设置为无权图
    VerT v1, v2;
    ifstream fin(FileName);
    fin >> boolalpha >> Directed;  //输入有向性
    fin >> boolalpha >> Weight;  //输入带权性
    if (Weight)  //带权图
      w = INFINITY;
    fin >>VexNum;
    for(i = 0; i < VexNum; ++i)
      InputVertexFromFile(fin, vexs[i]);  //输入顶点信息
    fin >> ArcNum;
    for(i = 0; i < VexNum; ++i)
      for(j = 0; j <VexNum; ++j)
        arcs[i][j].adj = w;  //不邻接
    if (Weight)  //带权图
      for(i = 0; i < VexNum; ++i)  //给对角线元素赋值 0
        arcs[i][i].adj = 0;
    if (!Weight)   //无权图
      w = 1;
    for(k = 0; k < ArcNum; ++k)
    {
      InputVertexFromFile(fin, v1);  //输入弧尾名称
      InputVertexFromFile(fin, v2);  //输入弧头名称
      if (Weight)  //带权图
        fin >> w;
      i = LocateVex(v1);  //弧尾序号
      j = LocateVex(v2);  //弧头序号
      arcs[i][j].adj = w;  //权值或 1 (表示有弧)
      InputArcFromFile(fin, arcs[i][j]);  //输入弧除权值外的信息
      if (!Directed)  //无向
        arcs[j][i] = arcs[i][j];  //对称单元信息相同
    }
    fin.close();
  }
  bool PutVex(VerT v, VerT value)
  {//对顶点 v 赋新值 value
    int k = LocateVex(v);
    if (k < 0)
      return false;
    vexs[k] = value;
    return true;
  }
  void InsertVex(VerT v)
  {//在图中增添新顶点 v (不增添弧)
    int i;
    VRType j = 0;  //初始设为无权图
    if (Weight)  //带权图
      j = INFINITY;
    vexs[VexNum] = v;
    for(i = 0; i < VexNum; i++)
      arcs[VexNum][i].adj = arcs[i][VexNum].adj = j;
    arcs[VexNum][VexNum].adj = 0;  //对角元素
    VexNum++; 
  }
  bool DeleteVex(VerT v)
  {//删除图中顶点 v 及相关的弧
    int i, j, k;
    k = LocateVex(v);
    if (k < 0)
      return false;
    for(i = 0; i < VexNum; i++)
      DeleteArc(v, vexs[i]);  //删除由顶点 v 发出的所有弧
    if (Directed)  //有向
      for(i = 0; i < VexNum; i++)
        DeleteArc(vexs[i], v);  //删除发向顶点 v 的所有弧
    for(j = k+1; j < VexNum; j++)
      vexs[j-1] = vexs[j];  //[k]后面的顶点向量依次前移
    for(i = 0; i < VexNum; i++)
      for(j = k+1; j < VexNum; j++)
        arcs[i][j-1] = arcs[i][j];  //移动待删除顶点之右的矩阵元素
    for(i = 0; i < VexNum; i++)
      for(j = k+1; j < VexNum; j++)
        arcs[j-1][i] = arcs[j][i];  //移动待删除顶点之下的矩阵元素
    VexNum--;
    return true;
  }
  bool InsertArc(VerT v, VerT w)
  {//增添弧<v,w>,若是无向的,则还增添对称弧<w,v>
    int i, j;
    i = LocateVex(v);
    j = LocateVex(w);
    if (i < 0 || j < 0)
      return false;
    ArcNum++;
    if (Weight)  //带权图
      cin >> arcs[i][j].adj;
    else  //无权图
      arcs[i][j].adj = 1;
    InputArc(arcs[i][j]);  //输入弧的其他信息
    if (!Directed)  //无向
      arcs[j][i] = arcs[i][j];
    return true;
  }
  bool DeleteArc(VerT v, VerT w)
  {//删除弧<v,w>,若是无向的,则还删除对称弧<w,v>
    int i, k;
    VRType j = 0;  //初始设为无权图
    if (Weight)  //带权图
      j = INFINITY;
    i = LocateVex(v);
    k = LocateVex(w);
    if (i < 0 || k < 0 || i == k)
      return false;
    if (arcs[i][k].adj != j)  //有弧<v,w>
    {
      arcs[i][k].adj = j;  //删除弧<v,w>
      if (!Directed)  //无向
        arcs[k][i] = arcs[i][k];
      ArcNum--;
    }
    return true;
  }
  int FirstAdjVex(int i)const
  {//返回顶点[i]的第一个邻接顶点的序号,不存在则返回 -1
    VRType j = 0;
    if (Weight)  //带权图
      j = INFINITY;
    for(int k = 0; k < VexNum; k++)
      if (i != k && arcs[i][k].adj != j)
        return k;
    return -1;  //没有邻接顶点
  }
  int NextAdjVex(int i, int j)const
  {//返回顶点[i](相对于顶点[j])的下一个邻接顶点的序号,不存在则返回 -1
    VRType m = 0;
    if (Weight)  //带权图
      m = INFINITY;
    for(int k = j+1; k < VexNum; k++)
      if (i != k && arcs[i][k].adj != m)
        return k;
    return -1;
  } 
  VerT GetVex(int i)const
  {//返回顶点[i]的值
    if (i >= VexNum || i < 0)
      exit(1);
    return vexs[i];
  }
  ArcT GetArc(int i, int j)const
  {//返回弧<i,j>的信息
    return arcs[i][j];
  }
  void Display()const
  {//输出邻接矩阵存储结构的图
    int i, j;
    string s1 = "无向", s2 = "无权图";
    if (Directed)
      s1 = "有向";
    if (Weight)
      s2 = "带权图";
    cout << VexNum << "个顶点" << ArcNum << "条弧的" << s1 << s2 
         << "。顶点依次是:";
    for(i = 0; i <VexNum; ++i)
      Visit(GetVex(i));
    cout << endl << "arcs.adj" << endl;
    for(i = 0; i < VexNum; i++)
    {
      for(j = 0; j < VexNum; j++)
        if (arcs[i][j].adj == INFINITY)
          cout << setw(2) << "#" << " ";  //用 # 代替无穷,表示不连通
        else
          cout << setw(2) << arcs[i][j].adj << " ";
      cout << endl;
    }
    for(i = 0; i < VexNum; i++)
      if (Directed)  //有向
      {
        for(j = 0; j < VexNum; j++)
          if (i != j && (!Weight && arcs[i][j].adj || 
          Weight && arcs[i][j].adj != INFINITY))  
          //非对角线元素,同时无权且 .adj 的值不为 0 或带权且 .adj 的值不为无穷(#)
            OutputArc(Directed, vexs[i], vexs[j], arcs[i][j]);
            //输出弧的信息
      }//加括号为避免 if-else 对配错
      else  //无向,输出上三角
        for(j = i+1; j < VexNum; j++)
          if (!Weight && arcs[i][j].adj || Weight && arcs[i][j].adj!=INFINITY)
          OutputArc(Directed, vexs[i], vexs[j], arcs[i][j]);  //输出弧的信息
  }
  int GetVexNumOrArcNum(int i)const
  {//返回图的顶点数(i = 0)或弧数(i = 1)
    if (i == 0)
      return VexNum;
    else
      return ArcNum;
  }
};

图的邻接矩阵存储结构用二维矩阵存储两点之间的弧。无论两顶点之间是否有弧,存储空间都已经预留。这很适合存储稠密图,即弧多的图。对于弧少的图,这种存储结构空间浪费很大。这种情况最好采用邻接表存储结构,或用稀疏矩阵代替二维矩阵。

2. 图的邻接表存储结构

图的邻接表存储结构的顶点信息采用顺序存储结构,每个顶点的邻接顶点采用链式存储结构。为了实现高效编程,其中的顺序存储结构用了 STL 的 vector 向量类实现,链式存储结构用 STL 的 list 表类实现。

实现

//图的邻接表类
template<typename VerT, typename ArcT>class Articul;  //声明带模板的关节点类
template<typename VerT, typename ArcT>class AdjListGraph: public AGraph<VerT>
{//带模板并继承 AGraph<VerT> 的邻接表类
  friend Articul<struct V, struct A>;
protected:
  struct ArcTNode  //弧结点的结构
  {
    int adjvex;  //邻接顶点的序号
    ArcT arc;  //弧信息
  };
  struct VerTNode  //顶点的结构
  {
    VerT vex;  //顶点信息
    list<ArcTNode> arcs;  //list 链表类存储该顶点所有出弧的信息
  };
  vector<VerTNode> vexs;  //vector 向量类存储所有顶点
  int LocateVex(VerT u)const
  {//返回与 u 有相同关键字的顶点在图中的序号,不存在则返回 -1
    for(int i = 0; i < vexs.size(); i++)
      if EQ(u, vexs[i].vex)
        return i;
    return -1;
  }
public:
  AdjListGraph()
  {//构造函数
    ArcNum = 0;
  }
  VerT GetVex(int i)const
  {//i 是图中某个顶点的序号,返回顶点[i]的信息
    if (i >= vexs.size() || i < 0)
      exit(1);
    return vexs[i].vex;
  }
  void CreateFromFile(char* FileName)
  {//由数据文件创建图
    int i, j, k;
    VerT v1, v2;
    ArcTNode e;
    VerTNode v;
    ifstream fin(FileName);
    fin >> boolalpha >> Directed;
    fin >> boolalpha >> Weight;
    fin >> j;  //输入顶点数
    for(i = 0; i < j; i++)
    {
      InputVertexFromFile(fin, v.vex);
      vexs.push_back(v);  //加到末尾
    }
    fin >> ArcNum;  //输入弧数
    for(k = 0; k < ArcNum; ++k)
    {
      InputVertexNameFromFile(fin, v1);  //输入弧尾
      InputVertexNameFromFile(fin, v2);  //输入弧头
      i = LocateVex(v1);  //弧尾的序号
      j = LocateVex(v2);  //弧头的序号
      e.adjvex = j;
      if (Weight)  //带权图
        fin >> e.arc.weight;
      InputArcFromFile(fin, e.arc);
      vexs[i].arcs.push_front(e);  //加入表头
      if (!Directed)  //无向
      {
        e.adjvex = i;
        vexs[j].arcs.push_front(e);  //加入表头
      }
    }
    fin.close();
  }
  bool PutVex(VerT v, VerT value)
  {//对顶点 v 赋新值 value
    int k = LocateVex(v);
    if (k < 0)
      return false;
    vexs[k].vex = value;
    return true;
  }
  void InsertVex(VerT v)
  {//在图中增添新顶点 v (不增添弧)
    VerTNode vv;
    vv.key = v;
    vexs.push_back(vv);
  }
  bool DeleteVex(VerT v)
  {//删除图中顶点 v 及其相关的弧
    int i, k;
    list<ArcTNode>::iterator p;
    k = LocateVex(v);
    ArcNum -= vexs[k].arcs.size(); 
    vexs.erase(vexs.begin() + k);  //删除 vexs[k]
    for(i = 0; i < vexs.size(); i++)
    {
      for(p = vexs[i].arcs.begin(); p != vexs[i].arcs.end(); p++)
        if (p->adjvex == k)
        {
          vexs[i].arcs.erase(p);  //删除指向 k 的弧
          if (Directed)  //有向图
            ArcNum--;
          break;
        }
      for(p = vexs[i].arcs.begin(); p != vexs[i].arcs.end(); p++)
        if (p->adjvex > k)
          p->adjvex--;  //序号减 1
    }
    return true;
  }
  bool InsertArc(VerT v, VerT w)
  {//增添弧<v,w>,若是无向的,则还增添对称弧<w,v>
    ArcTNode e;
    int i, j;
    string s1 = "边", s2 = "——";  //无向的情况
    i = LocateVex(v);
    j = LocateVex(w);
    if (i < 0 || j < 0)
      return false;
    ArcNum++;
    e.adjvex = j;
    InputArc(Directed, Weight, v, w, e.arc);  //输入弧的信息
    vexs[i].arcs.push_front(e);  
    if (!Directed)
    {
      e.adjvex = i;
      vexs[j] = arcs.push_front(e);
    }
    return true;
  }
  bool DeleteArc(VerT v, VerT w)
  {//删除弧<v,w>,若是无向的,则还删除对称弧<w,v>
    list<ArcTNode>::iterator p;
    int i, j;
    i = LocateVex(v);
    j = LocateVex(w);
    if (i < 0 || j < 0 || i == j)
      return false;
    for(p = vexs[i].arcs.begin(); p != vexs[i].arcs.end(); p++)
      if (p->adjvex == j)
      {
        vexs[i].arcs.erase(p);  //删除该结点
        ArcNum--;
        break;
      }
    if (!Directed)  //无向
      for(p = vexs[j].arcs.begin(); p != vexs[j].arcs.end(); p++)
        if (p->adjvex == i)
        {
          vexs[j].arcs.erase(p);  //删除该结点
          break;
        }
    return true;
  }
  int FirstAdjVex(int i)const
  {//返回顶点[i]的第一个邻接顶点,不存在则返回 -1
    list<ArcTNode>::const_iterator p = vexs[i].arcs.begin();  //只读指针
    if (p != vexs[i].arcs.end())
      return p->adjvex;
    else
      return -1;
  }
  int NextAdjVex(int i, int j)const
  {//返回顶点[i](相对于顶点[j])的下一个邻接顶点的序号,不存在则返回 -1
    list<ArcTNode>::const_iterator p;
    for(p = vexs[i].arcs.begin(); p != vexs[i].arcs.end(); p++)
      if (p->adjvex == j)
      {
        p++;
        break;
      }
    if (p == vexs[i].arcs.end())
      return -1;
    else   //p->adjvex == j
      return p->adjvex;
  }
  void Display()const
  {//输出邻接表存储结构的图
    int i;
    list<ArcTNode>::const_iterator p;
    string s1 = "无向", s2 = "无权图";
    if (Directed)
      s1 = "有向";
    if (Weight)
      s2 = "带权图";
    cout << vexs.size() << "个顶点" << ArcNum << "条弧的" << s1 << s2 
         << "。顶点依次是:";
    for(i = 0; i < vexs.size(); i++)
    {
      for(p = vexs[i].arcs.begin(); p != vexs[i].arcs.end(); p++)
        if (Directed || i < p->adjvex)  //有向或无向两次中的一次,输出弧
          OutputArc(Directed, Weight, vexs[i].vex,vexs[p->adjvex].vex,p->arc);
      cout << endl;
    }
  }
  int GetVexNumOrArcNum(int i)const
  {//返回图的顶点数(i = 0)或弧数(i = 1)
    if (i == 0)
      return vexs.size();
    else
      return ArcNum;
  }
};

3. 图的深度优先遍历和广度优先遍历

对图的搜索就是对图中顶点的遍历,也就是根据某种规则对图中的每个顶点访问一次且仅访问一次。为了不重复访问所有的顶点,需要为顶点向量设立一个访问标志数组 Visited[] ,并置其初值为 false(未被访问)。遍历时只访问那些未被访问过的顶点,且在访问后,将其访问标志的值改为 true 。当所有的顶点访问标志都为 true 时,则已完成遍历。遍历一般从图的第一个顶点开始,接下来并不是按照顶点的存储顺序遍历,而是按照顶点的邻接关系确定遍历的顺序。有两个搜索原则:深度优先遍历广度优先遍历

深度优先遍历算法的主要思想是:在访问一个顶点(假设为 A )之后,不是依照顶点的存储顺序访问下一个顶点(假设为 B ),而是先依次访问顶点 A 的所有邻接顶点。并且访问了顶点 A 的第一个邻接顶点(假设为 C )之后,又去先访问顶点 C 的所有邻接顶点。所以,对顶点 B (假设它与顶点 A 不连通)的访问是在对与 A 连通的所有顶点的访问之后进行的。这类算法用递归比较方便。

广度优先遍历算法的主要思想是:先访问图的第一个顶点,然后依次访问这个顶点的所有邻接顶点,再依次访问这些邻接顶点的所有邻接顶点。这需要建立一个先进先出的队列,依次将访问过的顶点入队。当前一个顶点的所有邻接顶点都被访问过了,就出队一个顶点,在访问这个顶点的所有邻接顶点并将这些邻接顶点入队,直至所有顶点都被访问过了。

所谓的第一个邻接顶点、第二个邻接顶点不是由图的拓扑关系(逻辑结构)决定的,它取决于图的存储(物理)结构。若存储结构不同,则它的某个顶点的第一个邻接顶点、第二个邻接顶点也可能不同。这会造成同一个图的同一种搜索得出的结果不同。当然,对任一种图的存储结构,如果顶点的输入顺序不同,搜索的顺序也不同。

4. 图的应用

4.1 无向图的连通分量和生成树

具有 n 个顶点的无向连通图至少有 n-1 条边,如果只有 n-1 条边,则不会形成环,这样的图称为 “生成树” 。连通图可通过遍历构造生成树,非连通图的每个连通分量可构造一棵生成树,整个非连通图构造为生成森林。

推荐阅读
数据结构之无向图的连通分量和生成树

4.2 最小生成树

推荐阅读
Prim 算法(1)
最小生成树-Prim算法和Kruskal算法
最小生成树(普利姆算法、克鲁斯卡尔算法)

4.3 关节点和重连通分量

推荐阅读
重连通分量 (Biconnected Component)
8.4 图的连通性—关节点和重连通分量
连通图关节点求解详解--tarjan算法,重连通分量的求解--栈

4.4 拓扑排序和关键路径

详解图的应用(最小生成树、拓扑排序、关键路径、最短路径)
拓扑排序和关键路径

4.5 最短路径

彻底弄懂最短路径问题
最短路径
单源最短路径(4):总结

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,457评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,837评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,696评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,183评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,057评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,105评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,520评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,211评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,482评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,574评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,353评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,213评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,576评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,897评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,174评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,489评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,683评论 2 335

推荐阅读更多精彩内容

  • 图是由顶点的有穷非空集合和顶点之间边的集合组成,通常表示为:G(V,E),其中,G表示一个图,V是图G中顶点的集合...
    开心糖果的夏天阅读 783评论 0 9
  • 概念 定义 图是一种较线性表和树更为复杂的数据结构相较于线性表的一对一(每个结点只有一个前驱后驱)和树的一对多(层...
    MrDTree阅读 1,107评论 0 2
  • 第一章 绪论 什么是数据结构? 数据结构的定义:数据结构是相互之间存在一种或多种特定关系的数据元素的集合。 第二章...
    SeanCheney阅读 5,712评论 0 19
  • https://zh.visualgo.net/graphds 浅谈图形结构https://zh.visualgo...
    狼之独步阅读 4,075评论 0 0
  • 图是一种比线性表和树更复杂的数据结构,在图中,结点之间的关系是任意的,任意两个数据元素之间都可能相关。图是一种多对...
    Alent阅读 2,263评论 1 22