[非原创]树和图的遍历

简介: 备注:本文使用的范例代码来自《系统设计师教程》(王春森主编),本文性质属于我的对引用代码的注释和分析,因此并非原创。本文的部分先发表于中国编程论坛。   (一)用栈前序遍历树   对这篇文章的来源性说明:理论和代码来源,《系统设计师教程》(王春森主编),文章内容来自我对书中代码的分析和手工注释。

       备注:本文使用的范例代码来自《系统设计师教程》(王春森主编),本文性质属于我的对引用代码的注释和分析,因此并非原创。本文的部分先发表于中国编程论坛。

 

(一)用栈前序遍历树

 

对这篇文章的来源性说明:理论和代码来源,《系统设计师教程》(王春森主编),文章内容来自我对书中代码的分析和手工注释。
( 本文献给小师妹:littlehead(我是大好人))

名词:栈,遍历,前序遍历,树。

(1)准备:树的定义省略。但是由于数的定义属于递归型定义,即树的每个子节点又是一棵树,所以这个递归型的定义使对树的很多操作通常都可以用递归算法来实现。对数节点的定义,一个节点含有一个数据,这里以一个字符数据作为示范,假设一个节点最多含有M个子节点(M成为树的次数),则节点定义为:

typedef struct tnode
{
    char data;
    struct tnode *child[M];
} TNODE;

我们对用根节点指针来持有和处理一棵树:即:
TNODE *root;

前序遍历和后序遍历:遍历值得是依次输出所有节点数据。前序后序中的序指的是本节点和其子节点之间的输出顺序关系。对于普通树来说存在前序,后序两种遍历。对二叉树来说又有中序遍历。

(2)我们先给出前序遍历的伪码为:
void pre_order(TNODE *t, int m)  //m为数次数
{
    if(t!=NULL) 打印t;           //先输出本节点
    for(i=0;i<m;i++)
        pre_order(t->child[i],m); //前序输出所有子节点
}

/*--------------------------------
  root-> 1
           / | \
          2  3  4
         / \    |
        5   6   7
               / \
              8   9
----------------------------------*/

对于图1所示的树,则其前序遍历和后序遍历如下:

前序:1,2,5,6,3,4,7,8,9;
后序:5,6,2,3,8,9,7,4,1;

(3)采用栈来代替递归函数的方法如下:

方法:每出栈一个节点,打印该节点,然后则将其所有子节点逆序(从右到左)入栈。
初始栈状态:根节点入栈。栈中仅有根节点。栈顶指针top=1;
结束条件:栈为空。即栈顶指针top=0;

其相关代码如下 :
img_1c53668bcee393edac0d7b3b3daff1ae.gif img_405b18b4b6584ae338e0f6ecaf736533.gif Code_使用栈前序遍历树
/*使用栈前序遍历树*/
#define M 10 /*树的次/度:最大子节点数*/
#define SN 100 /*栈最大深度*/
typedef 
struct tnode
{
    
char data;
    
struct tnode *child[M];
}TNODE;

/*t-root; m-度*/
void pre_order(TNODE *t,int m)
{
    TNODE 
*s[SN];/*定义栈*/
    
int top=0,i;/*top-栈顶指针;*/
    
if(t==NULL)
        
return;
    s[top
++]=t;/*根节点入栈*/
    
    
while(top>0)    /*当栈不为空时:*/
    {
        t
=s[--top];/*取出栈顶节点并打印输出该节点*/
        printf(
"%c ",t->data);
        
/*将当前节点的子节点按逆序入栈*/
        
for(i=m-1;i>=0;i--)
        {
            
if(t->child[i])
                s[top
++]=t->child[i];
        }
    }
}
遍历时的栈中节点状态如图2所示(蓝色箭头表示栈顶,每一行表示栈在当前的一个状态)。
 
(二)用队列按层次遍历树

(1)按层次遍历树。层次,即相当于资源管理器的文件夹深度,见前文图1中的纵坐标。由于在示例树中的节点编号就是按照层次来给出的,所以按层次遍历这棵树的结果就是:1,2,3,4,5,6,7,8,9。
     由于我们在遍历过程中处于树的某个局部,因此我们需要引入一个辅助数据结构来帮助我们完成遍历。考虑到层次遍历的特点是,每一层次的所有子节点应该集中在一起,当访问某个节点时,其下一层次的子节点应该处于等待被访问的状态。因此我们应该引入队列作为辅助存储。
     方法如下:
Title      算法:每次取出队首节点,打印该节点,队首指针向后移动一格,然后将其所有子节点顺序进入列尾部,队尾指针相应向后移动。
     初始队列状态:根节点入队列,队列中仅有根节点。head=0,tail=1;
     结束条件:队列为空。即队列的两个指针重合,head==tail;
    
     队列我们同样用一个数组来模拟,并且需要两个变量head和tail来标识队列的有效区间,队列的特点是FIFO(先入先出),因此,子序列入队是顺序入队(和前文中用栈前序不同,由于栈的特点是FILO或者LIFO,所以子节点是逆序入栈的)。有效队列在数组中处于移动状态,在head,tail在移动到数组末尾时,重新指向数组前部,所以代码中这里有个索引自增后对队列长度取余的运算,因此实际上这里的队列在逻辑上是环形的,也就是数组的首尾相接,因此只有向队首或者向队尾两种移动方向的区别,而队列位于数组的什么位置并不重要(比如队列可能会一部分位于数组末端,一部分位于数组起始处,在内存视角上看不是连续的,但在逻辑上依然是连续的)。

     下面我们看相关的代码:
img_1c53668bcee393edac0d7b3b3daff1ae.gif img_405b18b4b6584ae338e0f6ecaf736533.gif Code_层次遍历
/* 用队列(queue)来层次遍历一颗普通树*/
#define M 10 /*树的次/度:最大子节点数*/
#define QN 100 /*队列最大长度*/
typedef 
struct tnode
{
    
char data;
    
struct tnode *child[M];
}TNODE;

int lev_order(TNODE *t, int m)
{
    TNODE 
*q[QN];/* queue */
    TNODE 
*p;
    
int head=0, tail=0, i;/*队列的头,尾索引*/
    q[tail
++]=t; /*根节点入列*/
    
while(head!=tail)/* 当队列不为空(首尾相遇时为空) */
    {
        p
=q[head];/*从队首出列一个节点*/
        head
=(head+1)%QN; /* head向后(队尾方向)移动一格 */
        printf(
"%c ",t->data);/*打印该节点*/
        
        
for(i=0;i<m;i++) /* 出列节点的所有子节点按顺序入列(进入队列尾部) */
        {
            
if(p->child[i])
            {
                
if((tail+1)%QN==head) /*如果队列已经满了,返回0表示失败 */
                    
return 0;
                q[tail]
=p->child[i]; /*子节点入列*/
                tail
=(tail+1)%QN;    /* tail向后移动一格 */
            }
        }
    }
    
return 1; /*遍历结束,返回1表示成功*/
}
(2)关于树的少许补充:
2.1 二叉查找树和堆的结构看起来一样,但是区别是,二叉查找树:左子<=根<=右子; 而堆是:子节点<=根。另外,堆在逻辑上是树形,但存储是用数组存储的(节点按层次连续存入数组)。
2.2 中序遍历二叉查找树,输出的一个有序结果。
2.3 用作图法按前序,后序遍历树的规律:
      假设我们把二叉树的一个节点提取出来,我们看出,从节点中心垂直向上定义为0度角,逆时针正向,则节点的左右子树分支分别为120度(左子树)和240(右子树)。则我们可以用作图法得到各种序遍历的节点输出角度:从根节点上方向左下方画一条线外围住所有节点直到返回根节点,当线条经历到节点的以下角度时输出该节点(如图3所示,前序输出为1,2,4,5,3,6):
      前序输出角度:60度;
      中序(仅针对二叉树):180度;
      后序:300度。
       
 
(三)图的遍历
       图是一种复杂数据结构,图需要存储节点与节点之间的关联关系,主要有矩阵和链表两种存储方法。这里采用链表,针对每个节点都存储一个链表,链表中表示的是所有和该节点有链接的其他节点号。因此我们需要一个指针数组来存储所有链表。代码如下:
img_1c53668bcee393edac0d7b3b3daff1ae.gif img_405b18b4b6584ae338e0f6ecaf736533.gif Code_图遍历
/*深度遍历和广度遍历图,图采用一组链表存储*/
#include 
<stdio.h>

#define N 30 /*最大节点数*/

typedef 
struct node
{
    
int vno;
    
struct node *next;
} edgeNode;

typedef edgeNode
* Graph[N];

/*建立一张图。每输入一条边,与之联系的节点插入到该节点链表的头部*/
int create_list(Graph g)
{
    
int vn, en, k, i, j;
    edgeNode 
*p;
    printf(
"\ninput count of nodes:\n");
    scanf(
"%d", &vn);
    printf(
"input count of edges:\n");
    scanf(
"%d",&en);

    
for(k=0; k<N; k++)
        g[k]
=NULL;
    
for(k=0; k<en; k++)
    {
        printf(
"input the edge[%d](i,j):\n", k);
        scanf(
"%d,%d",&i,&j);

        p
=(edgeNode*)malloc(sizeof(edgeNode));
        p
->vno=j;
        p
->next = g[i];
        g[i]
=p;

        p
=(edgeNode*)malloc(sizeof(edgeNode));
        p
->vno=i;
        p
->next = g[j];
        g[j]
=p;
    }
    
/*返回图中节点数*/
    
return vn;
}

/*打印输出所有节点链表*/
void print_list(Graph g)
{
    
int i;
    edgeNode 
*p=NULL;
    printf(
"\nGraph g:\n");
    
for(i=0; i<N; i++)
    {
        
if(g[i]==NULL)
            
continue;
        printf(
"g[%d]: ",i);
        p
=g[i]; /* p = head*/
        
while(p!=NULL)
        {
            printf(
"%d - ",p->vno);
            p 
= p->next;
        }
        printf(
"NULL\n");
    }
}

/*------------------------递归,深度遍历-------------------------*/
/*存储节点状态,是否被遍历*/
int visited[N];

void clearflags()
{
    
int i;
    
for(i=0; i<N; i++)
        visited[i]
=0;
}

/*深度优先遍历,采用递归函数*/
void dfs(Graph g, int i)
{
    edgeNode 
*t;
    printf(
"%4d,", i);
    visited[i] 
= 1;
    t
=g[i];
    
while(t!=NULL)
    {
        
if(visited[t->vno]==0)
            dfs(g,t
->vno);
        t
=t->next;
    }
}

/*------------------------队列,广度遍历------------------------*/

#define M (2*N)

int queue[M];

/*广度优先遍历*/
void bfs(Graph g, int s)
{
    
int i, v, w, head, tail;
    edgeNode 
*t;

    head
=tail=0;

    
/*访问该节点,并且该节点进队*/
    printf(
"%4d,", s);
    visited[s]
=1;
    queue[tail
++]=s;

    
while(head != tail)         /*当队列不为空时*/
    {
        v 
= queue[head];            /*队列首节点出列*/
        head 
= (head+1)%M;         /*head指针向右方移动*/
        
for(t=g[v]; t!=NULL; t=t->next)
        {
            w 
= t->vno;
            
if(visited[w]==0)
            {
                printf(
"%4d,", w);
                visited[w] 
= 1;
                
if((tail+1)%M == head)
                {
                    printf(
"queue's size is too small! !");
                    
return;
                }
                queue[tail] 
= w;    /*顶点w进队*/
                tail 
= (tail+1)%M;
            }
        }
    }
}

/*
   0---1
   |\ /|
   | 4 |
   |/ \|
   3---2   
 
*/
void main()
{
    Graph g;
    
int i,vn;
    vn 
= create_list(g);
    printf(
"g has %d nodes\n",vn);

    
/*深度遍历*/
    printf(
"\n dfs: ");
    clearflags();
    dfs(g,
0);

    
/*广度遍历*/
    printf(
"\n bfs: ");
    clearflags();
    bfs(g,
0);

    print_list(g);
    getch();
}
 
       针对下面这张图:
       
       程序的输出如下:

 dfs:    0,   4,   3,   2,   1,    (深度优先遍历)
 bfs:    0,   4,   3,   1,   2,    (广度优先遍历)
 
Graph g: (图g的所有节点链表)

g[0]: 4 - 3 - 1 - NULL
g[1]: 4 - 2 - 0 - NULL
g[2]: 4 - 3 - 1 - NULL
g[3]: 4 - 0 - 2 - NULL
g[4]: 3 - 2 - 1 - 0 - NULL
目录
相关文章
|
9月前
|
消息中间件 人工智能 NoSQL
[高并发架构] 挑战百万级Token吞吐:智能体来了(西南总部)深度解析AI调度官的流量削峰与分级治理策略
传统的 API Gateway 已经失效。我们需要一种专为 AI 设计的流量治理组件。 本文将基于 智能体来了(西南总部) 技术团队的实战经验,深度解析企业级 “AI 调度官” (AI Dispatcher) 的架构设计。我们将探讨如何利用 消息队列(MQ)削峰、优先级队列调度、以及基于语义的自适应限流,构建一个高可用的百万级 Token 吞吐系统。
|
Python
【金融量化】通道突破策略之布林带策略(Bollinger Band )、肯特纳通道策略(Keltner Channel)、唐奇安通道策略(Donchian)原理简介
本文介绍了三种金融量化分析中的通道突破策略:布林带策略(Bollinger Band)、肯特纳通道策略(Keltner Channel)和唐奇安通道策略(Donchian Channel),并提供了每种策略的原理和Python实现代码。
1859 2
|
Java 关系型数据库 MySQL
【高频】你了解乐观锁和悲观锁吗?
【高频】你了解乐观锁和悲观锁吗?
649 3
|
机器学习/深度学习 存储
深入理解SVM中的核函数及其应用
深入理解SVM中的核函数及其应用
1117 83
|
移动开发 安全 Java
Java Applet基础详解
Java Applet基础详解
933 0
|
编解码 Dart API
鸿蒙Flutter实战:06-使用ArkTs开发Flutter鸿蒙插件
本文介绍了如何开发一个 Flutter 鸿蒙插件,实现 Flutter 与鸿蒙的混合开发及双端消息通信。通过定义 `MethodChannel` 实现 Flutter 侧的 token 存取方法,并在鸿蒙侧编写 `EntryAbility` 和 `ForestPlugin`,使用鸿蒙的首选项 API 完成数据的读写操作。文章还提供了注意事项和参考资料,帮助开发者更好地理解和实现这一过程。
1008 0
|
Linux 网络安全 开发工具
SecureCRT 绝佳配色方案
SecureCRT 绝佳配色方案
2103 0
|
数据可视化 数据挖掘 DataX
Python 数据可视化的完整指南
Python 数据可视化在数据分析和科学研究中至关重要,它能帮助我们理解数据、发现规律并以直观方式呈现复杂信息。Python 提供了丰富的可视化库,如 Matplotlib、Seaborn、Plotly 和 Pandas 的绘图功能,使得图表生成简单高效。本文通过具体代码示例和案例,介绍了折线图、柱状图、饼图、散点图、箱形图、热力图和小提琴图等常用图表类型,并讲解了自定义样式和高级技巧,帮助读者更好地掌握 Python 数据可视化工具的应用。
1136 3
|
Unix Linux Windows
操作系统的演变与基本原理
本文旨在深入探讨操作系统的历史演变过程及其背后的设计原理。通过对不同时期典型操作系统的分析,本文揭示了它们如何响应技术挑战和社会需求的变化。此外,文章还将阐述操作系统的核心功能和关键技术,如进程管理、内存管理和文件系统,并探讨这些技术如何影响计算机系统的性能和可靠性。通过综合历史案例和技术分析,本文希望为读者提供一个全面而深入的理解,为什么操作系统是现代计算不可或缺的基石。
502 28
|
小程序 安全 数据建模
微信小程序 APP和SSL证书
微信小程序与第三方服务器通讯的域名必须满足以下条件: 1、一个已备案的域名,域名不能加端口; 2、域名必须配置ssl证书,也就是能通过https://访问; 3、HTTPS 服务器的 TLS 版本必须大于等于1.2,就是要支持TLS 1.2及以上版本; 4、微信小程序后台加上合法域名设置。
1308 82
微信小程序 APP和SSL证书

热门文章

最新文章