一、结构体
数据结构中结构体很重要,所以先了解结构体相关知识点。
struct LNode{
int data;
char name[20];
};
//struct是定义结构体的关键字,LNode是结构体的名称
//如为上个结构体创建一个对象——struct LNode n;
//这里创建对象必须加上结构体关键字struct。n就是对象了,可直接使用
struct LNode{
int data;
char name[20];
}n;
//这是在定义的时候创建了对象n
typedef struct LNode{
int data;
struct LNode *next;//此处还不能用别名来代替使用,因为代码到此处别名还没有定义
}node;
//typedef 是定义别名的关键字,node为struct LNode的别名。创建对象就是 node n; 无需struct LNode n;
//注意加了typedef关键字,后面的node就是别名,不加typedef就是在定义的时候创建的node对象了
二、递归
递归在树、图遍历中用的很多,理解递归对帮助树图相关代码的理解有很大帮助。
1、递归实现求1+2+…+n。
int sum(int n){ //sum(n)是返回1+2+...+n的结果
int s;
if(n == 1){
s = 1;
}else{
s = sum(n-1) + n;
}
return s;
}
理解递归函数我认为应把握好整体和局部的关系,整体就是整个递归函数它起到什么样的作用,局部是我们怎样去实现这个递归,首先搞清楚这个函数它的作用,如上述代码,sum(n)是返回1+2+…+n的结果,我们就把它当成这样一个作用的函数去用,第一步,我们得考虑到n=1的特殊情况,它是递归的出口,它到n=1就不在递归,开始逐步返回结果。第二步,就要调用这个递归函数了,这种类型题目都是将最后一步和前面n-1步的递归函数结合,如s = n+sum(n-1); sum(n-1)是从1加到n-1的结果,这是这个递归函数的作用,不要一步步往递归里深究,会晕的,我们只要知道它就是这个作用,我们直接拿来用即可。
2、递归求任意个整数的和。
int sum(int a[],int n){ //a是包含n个整数的整形数组,下标从0到n-1
if(n==1){//递归出口,当只有一个数它的整数和就是a[0]
return a[0];
}else{
return sum(a,n-1)+a[n-1];//sum(a,n-1)表示数组中前n-1个数和
}
}
和上题类似,先搞清sum(a,n)的作用是返回数组a中前n个数的和,下标从0到n-1;再确定出口n=1的情况,最后将a[n-1]和前面n-1个数相加的结果sum(a,n-1)加起来即可。
3、求n个数中的最大值
int max(int a[],int n){//作用:返回n个数中的最大值
if(n==1){//递归出口
return a[0];//只有一个数最大值只能是它
}else{
if(a[n-1]>max(a,n-1)){//将最后一个数和前面n-1个数中的最大值比较
return a[n-1];
}else{
return max(a,n-1);
}
}
}
4、一个很经典的递归题,15级楼梯,一步最多三级,爬上楼梯可以有多少种走法。
分析:
①先定义一个函数 fun(n) 返回n级楼梯共有多少种走法;
②第一步有三种走法:
走一级,还剩下n-1级楼梯要走,剩下的就有fun(n-1)种走法;
走两级,还剩下n-2级楼梯要走,剩下的就有fun(n-2)种走法;
走三级,还剩下n-3级楼梯要走,剩下的就有fun(n-3)种走法;
所以n级楼梯的走法 fun(n)=fun(n-1)+fun(n-2)+fun(n-3);n>3
③根据以上分析可定义一个递归函数fun(n)并且我们可以很快的算出fun(1) = 1,fun(2) = 2,fun(3) = 4,剩下的就可以通过公式一步步推导出来了。实现代码如下:
int fun(int n){ //返回n级楼梯共有多少种走法
if(n==1){return 1;}//出口
if(n==2){return 2;}//出口
if(n==3){return 4;}//出口
return fun(n-1)+fun(n-2)+fun(n-3);
}
以上递归方法类型差不多,分析也一样的,再来看看树中的递归
void PreOrder(BiTree T){ //先序遍历以T为根节点的树
if(T!=NULL){//出口被隐藏了,T==NULL就不往下递归了
visit(T);//先序遍历第一步肯定是访问根结点
PreOrder(T->lchild);//遍历左子树
PreOrder(T->rchild);//遍历右子树
}
}
以上就是递归方面的题目,做的多接触的多也就慢慢理解了。
三、链表
链表的基础知识不过多介绍,主要说有关链表的编程题如何去做。
在链表的一些编程题中,首先要分析题目,可在草稿纸上画个链表,分析需要几个指针,指针的初始位置应该指向哪,各个指针都有什么作用,怎么实现,搞清这些再开始做题。
1、比如一个简单的从带头结点链表L中查找并删除所有值为x的结点
首先画出该链表
其次分析要用到哪些指针,既然是查找结点肯定是要整个遍历的,就需要一个指针P从头到尾来遍历查找值为x的结点,P初始位置放在头结点后面,因为头结点中没有信息的,P应指向第一个存储数据的结点上;当P从第一个有效结点往后遍历,直到找到值为x的结点时,如何删除该结点呢?
这就需要另一个指针Pro,它要指向P指针前一个结点,这样才方便实现删除结点X,P要始终保持在Pro指向的结点后面。Pro的初始位置就和L指向的位置相同,刚好在P指针前一个位置。
Pro->next = P->next;//执行删除
free(P);//释放结点空间
Pro、P 指针的初始位置如下:
完整代码:
void delete_X(LNode *L,int x){ //从链表L中删除值为x的结点
LNode *Pro,*P;//定义需要用到的指针
Pro = L;//定义Pro的初始位置
P = Pro->next;//定义P的初始位置
while(P!=NULL){//P指向的结点不为空就一直遍历下去
if(P->data == x){//找到则删除该结点
Pro->next = P->next;
free(P);
P = Pro->next;//重新定位P,继续查找
}else{//否则整体后移一位
Pro = P;
p = Pro->next;
}
}
}
2、删除带头结点单链表L中值最小的结点
先看看需要哪些指针,找最小的结点肯定得有一个指针 p 一直往后找,还要一个指针min指向当前找过的这些数中最小的那个结点。刚开始p和min都指向第一个结点(非头结点),min指在这表示初始状态下默认第一个结点为最小值结点,p则是从第一个结点开始往下找,找到了比min还小的,min指针就得指向那个更小的结点了,然后p继续找下去,直到结束后min指定的结点就是最小值结点了。
但是如何删除呢?min结点最后会指向最小值结点,将其删除则需要在min指针前面加一个指针pmin,pmin需要一直保持在min指针前面。而min指针变动是根据p指针来的,p指针找到了个更小的结点,min就直接指向 p当前指向的结点,此时pmin指针就不能保持在min之前了,又得需要一个指针pro一直保持在p之前就可以了。p找到了位置,min到p那边去,pmin就到pro那边去就可以了。
初始位置如下:
实现代码如下:
void delete_min(LNode *L){
LNode *pro,*p,*pmin,*min;//定义需要的指针
pro = pmin = L;//确定位置
p = min = pro->next;//确定位置
while(p!=NULL){//只要p不为空则一直找
if(p->data<min->data){//找到了更小的结点,min、pmin就过去
min = p;
pmin = pro;
}
pro = p;
p = p->next;
}
//结束之后min指的就是最小的结点,pmin在min前面
pmin->next = min->next;
free(min);
}
3、采用直接插入排序对带头结点链表L升序排序
直接插入排序是将第一个数看成是一个有序的表,然后依次将后面的数向有序表中插入,直到所有数都插到有序表中。
在链表中实现的基本思想是把链表从第一个结点之后断开,分成L和L2两个链表。
L中是只有一个结点的有序链表,将L2中的结点依次取下来插入到链表L中完成排序。
需要指针p,p是在L链表中寻找结点要插入的位置,升序就得满足插入的结点值大于p指针指向的结点值,小于p指针指向结点后继结点的值,注意结点可能插入到两端,故p指针应指向头结点L处,因为可能插入第一个位置;还需要指针qro、q,qro是指向要向L链表中插入的结点,也就是L2链表中的第一个结点,得将qro指向的结点取出来,所以还要指针q来指向下一个要插入的结点,也就是L2链表中的第二个结点,不然取出结点之后就不能继续取了。
初始位置如下:
//实现代码:
void sort(LNode *L){//对链表L采用直接插入排序进行升序排序
LNode *p,*qro,*q;//定义结点
p = L->next;//定义初始位置
qro = p->next;//定义初始位置
p->next = NULL;//将链表断开,分为两个链表
while(qro!=NULL){//循环插入,直到qro为空也就是全部插完,没有结点了
q = qro->next;//定义q的位置
p = L;//p一直在动,每次都将其位置重置到开头
while(p->next!=NULL&&p->next->data<qro->data){//寻找qro插入的位置,注意是p->next,不能是p
p = p->next;
}
qro->next = p->next;
p->next = qro;
qro = q;//插入之后将qro位置重置到剩下结点的第一个结点
}
}
四、树
树方面的编程题主要都是基于树的遍历来的,树的遍历包含前中后序,以及递归非递归的实现外加层次遍历共七种,这些遍历代码都很重要,很多题目都可以通过这七种遍历算法基础上修改就可以解出来。
1、先序遍历——递归方式
void PreOrder(BiTree T){
if(T!=NULL){
visit(T);//访问结点
PreOrder(T->lchild);//遍历左子树
PreOrder(T->rchild);//遍历右子树
}
}
2、中序遍历——递归方式
void InOrder(BiTree T){
if(T!=NULL){
PreOrder(T->lchild);//遍历左子树
visit(T);//访问结点
PreOrder(T->rchild);//遍历右子树
}
}
3、后序遍历——递归方式
void PostOrder(BiTree T){
if(T!=NULL){
PreOrder(T->lchild);//遍历左子树
PreOrder(T->rchild);//遍历右子树
visit(T);//访问结点
}
}
4、先序遍历——非递归方式
void PreOrder(BiTree T){
BiTree *p = T;
SeqStack S;//定义一个栈
while(p||!S.Empty_Stack()){//指针p和栈S只要一个不为空继续遍历
if(P){
visit(p);
S.Push_Stack(p);//p指针进栈
p = p->lchild;//访问左子树
}else{
S.Pop_Stack(p);//栈中元素出栈并将其赋值给p
p = p->rchild;//访问右子树
}
}
}
5、中序遍历——非递归方式
void InOrder(BiTree T){
BiTree *p = T;
SeqStack S;//定义一个栈
while(p||!S.Empty_Stack()){//指针p和栈S只要一个不为空继续遍历
if(P){
S.Push_Stack(p);//p指针进栈
p = p->lchild;//访问左子树
}else{
S.Pop_Stack(p);//栈中元素出栈并将其赋值给p
visit(p);
p = p->rchild;//访问右子树
}
}
}
6、后序遍历——非递归方式
void PostOrder(BiTree T){
SeqStack s1;//s1栈存放结点指针
SeqStack s2;//s2栈存放标志flag,记录该结点有没有被访问过
BiTree *p =T;
int flag;
while(p||!s1.Empty_Stack()){
if(p){
flag = 0;
s1.Push_Stack(p);//当前p指针第一次进栈
s2.Push_Stack(flag);//标志flag进栈,和指针保持同步
p = p->lchild;
}else{
s1.Pop_Stack(p);//栈中元素出栈并将其赋值给p
s2.Pop_Stack(flag);//栈中元素出栈并将其赋值给flag
if(flag == 0){//表示对应的p指针才进过一次栈
flag = 1;
s1.Push_Stack(p);//当前p指针第二次进栈
s2.Push_Stack(flag);//标志flag进栈,和指针保持同步
p = p->rchild;
}else{
visit(p);
p = NULL;//p必须要置空
}
}
}
}
7、层次遍历——队列实现方式
void LevelOrder(BiTree T){
InitQueue(Q);//初始化队列Q
BiTree p;
EnQueue(Q,T);//根节点T入队列Q
while(!IsEmpty(Q)){
DeQueue(Q,p);//对头元素出队并赋值给p
visit(p);
if(p->lchild!=NULL){
EnQueue(Q,p->lchild);
}
if(p->rchild!=NULL){
EnQueue(Q,p->rchild);
}
}
}
以上算法重在理解,画个树,按照程序步骤一步步模拟走下去,慢慢的就理解了,特别要注意,非递归实现树的先序和中序,每个结点只有一次进栈和一次出栈,而非递归实现树的后续结点要入两次栈,出两次栈。
现在来根据上述的遍历算法做一些题
1、计算二叉树结点个数
这个只用在访问结点处来计数就可以了
如递归方式下:
int count = 0;
void PreOrder(BiTree T){
if(T!=NULL){
//visit(T);//访问结点
count++;//count需全局变量,不会受递归影响
PreOrder(T->lchild);//遍历左子树
PreOrder(T->rchild);//遍历右子树
}
}
2、计算二叉树高度
二叉树高度是左右子树的最大高度加一,而求左右子树的高度又和求整个二叉树高度一致,因此可以用递归实现
int Height(BiTree T){
if(T == NULL) return 0;
else{
return Max(Height(T->lchild),Height(T->rchild))+1;
}
}
也可以通过层次遍历来得到树的高度,算法比较复杂,解释如下:
采用层次遍历的算法,设置变量level记录当前结点所在的层数,设置变量last指向当前层的最右结点,每次层次遍历出队时与last指针比较,若两者相等,则结束当前层的遍历,层数加一,并让last指向下一层的最右结点,直到遍历完成。level值即为二叉树高度。可在纸上按如下代码走一遍流程,加深理解。当求某层结点数和树的宽度也是采用类似的思想。
int Btdepth(BiTree T){
if(!T){
return 0;
}
int front = -1,rear = -1;//初始化队头队尾指针
int last = 0,level = 0;//last指向下一层第一个结点位置
BiTree Q[Maxsize];//设置足够大的队列存储二叉树结点指针
Q[++rear] = T;//根节点入队
BiTree p;
while(front<rear){//队列不为空,则循环
p = Q[++front];//出队
if(p->lchild){
Q[++rear] = p->lchild;//左孩子入队
}
if(p->rchild){
Q[++rear] = p->rchild;//右孩子入队
}
if(frnot == rear){//处理该层最右结点
level++;//层数加一
last = rear;//last指向下层
}
}
return level;
}
3、依次访问二叉树中最长路径上的各个结点,如果有多个则访问最左侧的那条。
利用计算树的高度的算法来解决此题,每次我们都访问最高的子树。
int Height(BiTree T){//计算根节点为T的树的高度
if(T == NULL) return 0;
else{
return Max(Height(T->lchild),Height(T->rchild))+1;
}
}
void longPath(BiTree T){
if(T!=NULL){
visit(T);
if(Height(T->lchild)<Height(T->rchild)){//左子树高度小于右子树高度
longPath(T->rchild);//对右子树进行递归遍历
}else{
longPath(T->lchild);//对左子树进行递归遍历
}
}
}
4、将二叉树的叶子结点按从左到右连成一个单链表,表头指针为head,连接时用叶子结点的右指针域来存放单链表指针。
我们所用的先中后序遍历对叶子结点的访问顺序都是从左到右的,它们之间相对顺序不变,可利用非递归先序和中序稍作修改即可,定义一个head头指针和一个rear尾结点指针,在遍历时对每个结点判断是否为叶子结点(左右子树为空),依次将所有叶子结点串联起来。
BiTree *PreOrder_link(BiTree T){
BiTree *p = T,*head = NULL,*rear;
SeqStack S;//定义一个栈
while(p||!S.Empty_Stack()){//指针p和栈S只要一个不为空继续遍历
if(P){
//visit(p);将访问p结点改为如下代码即可
if(p->lchild==NULL&&p->rchild == NULL){//如果是叶子结点
if(head == NULL){//如果是第一个叶子结点
head = rear = p;
}else{
rear->rchild = p;
rear = p;
}
}
S.Push_Stack(p);//p指针进栈
p = p->lchild;//访问左子树
}else{
S.Pop_Stack(p);//栈中元素出栈并将其赋值给p
p = p->rchild;//访问右子树
}
}
//最后将尾指针rchild域置空并返还头指针
rear->rchild = NULL;
return head;
}
5、在二叉树中打印值为x结点的所有祖先结点,结点值为x的只有一个。
首先我们要了解在采用非递归后序遍历中,最后访问根结点,访问到某结点时,栈中所有元素均为该结点的祖先,依次出栈打印即可。我们只需在非递归后序遍历算法基础上修改即可。
void PostOrder(BiTree T,int x){
SeqStack s1;//s1栈存放结点指针
SeqStack s2;//s2栈存放标志flag,记录该结点有没有被访问过
BiTree *p =T;
int flag;
while(p||!s1.Empty_Stack()){
if(p){
flag = 0;
s1.Push_Stack(p);//当前p指针第一次进栈
s2.Push_Stack(flag);//标志flag进栈,和指针保持同步
p = p->lchild;
}else{
s1.Pop_Stack(p);//栈中元素出栈并将其赋值给p
s2.Pop_Stack(flag);//栈中元素出栈并将其赋值给flag
if(flag == 0){//表示对应的p指针才进过一次栈
flag = 1;
s1.Push_Stack(p);//当前p指针第二次进栈
s2.Push_Stack(flag);//标志flag进栈,和指针保持同步
p = p->rchild;
}else{
//visit(p);将访问结点代码修改如下
if(p->data == x){//当访问到值为x时,此时栈中的元素都是其祖先结点
while(!s1.Empty_Stack()){//打印栈中所有元素
s1.Pop_Stack(p);//栈中元素出栈并将其赋值给p
visit(p);
}
}
p = NULL;//p必须要置空
}
}
}
}
这题还可衍生为更复杂的一题,求两个结点p,q的最近公共祖先结点,同样是用到了上述性质,用一个栈存储访问到结点p时栈中的所有元素(即p的所有祖先),另一个栈存储访问到结点q时栈中的所有元素(即q的所有祖先),将两个栈进行比对就可以得出结果。
typedef struct{//定义栈的结构体,struct后的结构体名称可省略
BiTree t;
int flag;
//上述存放结点指针的栈需配与之对应的flag标记栈。
//而在此题需要两个结点指针栈,则得配两个flag栈,太过笨重,因此将结点指针和flag标记定义一个结构体便于使用。
}stack;//stack为结构体别名
stack s[Maxsize],s1[Maxsize];//定义足够大的栈
BiTree Ancestor(BiTree T,BiTree *p,BiTree *q){
int top1,top = 0;//初始化栈指针
BiTree *bt = T;
while(bt!=NULL||top>0){//top>0就是指栈不为空
while(bt!=NULL&&bt!=p&&bt!=q){
while(bt!=NULL){
s[++top].t = bt;
s[top].flag = 0;
bt = bt->lchild;
}
}
while(top!=0&&s[top].flag == 1){
//假定p在q左侧,遇到p时,栈中元素均为p的祖先
if(s[top].t == p){
for(i = 1;i<=top;i++){//将栈s的元素转入辅助栈s1保存
s1[i] = s[i];
}
top1 = top;//同时保存栈顶位置
}
if(s[top].t == q){
for(i = top;i>0;i--){//两个栈中的元素进行匹配
for(j = top1;j>0;j--){
if(s1[j].t == s[i].t){//相同则找到最近公共祖先
return s[i].t;
}
}
}
top--;//退栈
}
}
if(top!= 0){
s[top].flag = 1;
bt = s[top].t->rchild;
}
}
return NULL;//到此处都没有返回结点,则表明没有公共祖先
}
五、图
图的遍历算法基本上都基于邻接表的存储结构,这里所有图的相关算法也是基于邻接表存储结构来的。图的遍历主要包含深度优先遍历和广度优先遍历,深度优先遍历又可用递归方法实现和非递归方法的实现。
首先了解图的结构体
#define MaxVertexNum 100 //图中顶点数目的最大值
typedef struct ArcNode{ //边表结点的结构体
int data; //存储的顶点
struct ArcNode *next;//指向下一条弧的指针
}ArcNode;
typedef struct VNode{//顶点表结点的结构体
int data;//顶点信息
ArcNode *first;//指向第一条依附在该顶点的弧的指针
}VNode;
typedef struct{
VNode adjlist[MaxVertexNum];//邻接表,就是顺序存储的顶点表
int vexnum,arcnum;//图的顶点数和边数
}Graph;//图的结构体
1、广度优先遍历
bool visited[MaxVertexNum];//访问标记数组
void BFS(Graph G,int v){//从顶点v出发,广度优先遍历图G,算法借助一个辅助队列
ArcNode *p;//定义一个边表结点指针
int w;
visit(v);//访问顶点v
visited[v] = true;//标记该顶点已经访问过了,下次不会被重复访问
Enqueue(Q,v);//顶点v入队
while(!isEmpty(Q)){
Dequeue(Q,v);//顶点v出队
for(p =G.adjlist[v].first;p;p = p->next ){
//p初始化为顶点v后面的第一个边表;单独p相当于p!=NULL
w = p->data;
if(!visited[w]){//边表上所示的顶点没被访问过就对其访问并标记
visit[w];
visited[w] = true;
Enqueue(Q,w);
}
}
}
}
void BFSTraverse(Graph G){
for(i= 0;i<G.vexnum;i++){
visited[i] = false;//初始化标记数组
}
InitQueue(Q);//初始化辅助队列Q
for(i= 0;i<G.vexnum;i++){
if(!visited[i]){
BFS(G,i);
}
}
}
2、深度优先遍历——递归
bool visited[MaxVertexNum];//访问标记数组
void DFS(Graph G,int v){//从顶点v出发,采用递归思想,深度优先遍历图G
ArcNode *p;
int w;
visit(v);
visited[v]=true;
for(p =G.adjlist[v].first;p;p = p->next ){
w = p->data;
if(!visited[w]){
DFS(G,w);
}
}
}
void DFSTraverse(Graph G){
for(i= 0;i<G.vexnum;i++){
visited[i] = false;//初始化标记数组
}
for(i= 0;i<G.vexnum;i++){
if(!visited[i]){
DFS(G,i);
}
}
}
3、深度优先遍历——非递归
bool visited[MaxVertexNum];//访问标记数组
void DFS_Non_RC(Graph G,int v){//从顶点v出发,深度优先遍历图G,一次遍历一个连通分量的所有顶点
ArcNode *p;
int w;
InitStack(S);//初始化栈
Push(S,v);//进栈
visited[v]=true;
while(!IsEmpty(S)){
k = Pop(S);//出栈
visit(k);
for(p =G.adjlist[v].first;p;p = p->next ){
w = p->data;
if(!visited[w]){
Push(S,w);
visited[w] = true;
}
}
}
}
void DFSTraverse(Graph G){
for(i= 0;i<G.vexnum;i++){
visited[i] = false;//初始化标记数组
}
for(i= 0;i<G.vexnum;i++){
if(!visited[i]){
DFS_Non_RC(G,i);
}
}
}
注意:对图的遍历和从某个顶点出发遍历图是两码事。如果是一个连通图,那么从某个顶点出发遍历图可以一次性遍历完所有的结点,也就完成了对图的遍历;如果一个图有n个连通分量,那么对整个图的遍历就需要从n个属于不同连通分量的顶点出发遍历图,也就是说需要调用n次从单个结点出发遍历的函数,如以上的DFS、BFS。
有关图的编程题:
1、分别采用基于深度优先遍历和广度优先遍历判断有向图中是否存在由顶点 V(i) 到 V(j) 的路径(i != j)。
此题只需使用从单个顶点 i 出发遍历图,如果在遍历过程中出现顶点 j 则说明存在路径,否则不存在。
//深度遍历的实现
for(i= 0;i<G.vexnum;i++){//先初始化标记数组,不能放到递归中
visited[i] = false;
}
bool DFS(Graph G,int i,int j){//从顶点 i 出发,采用递归思想,深度优先遍历图G
ArcNode *p;
int w;
//visit(v);
if(i == j){
return true;
}
visited[i]=true;
for(p =G.adjlist[i].first;p;p = p->next ){
w = p->data;
if(!visited[w]){
DFS(G,w,j);
}
}
}
//广度遍历的实现
bool BFS(Graph G,int i,int j){//从顶点i出发,广度优先遍历图G,算法借助一个辅助队列
ArcNode *p;//定义一个边表结点指针
for(i= 0;i<G.vexnum;i++){//先初始化标记数组
visited[i] = false;
}
int w;
visit(i);
visited[i] = true;//标记该顶点已经访问过了,下次不会被重复访问
Enqueue(Q,i);//顶点i入队
while(!isEmpty(Q)){
Dequeue(Q,i);//顶点i出队
for(p =G.adjlist[i].first;p;p = p->next ){
//p初始化为顶点i后面的第一个边表;单独p相当于p!=NULL
w = p->data;
if(!visited[w]){//边表上所示的顶点没被访问过就对其访问并标记
//visit[w];
if(w == j){//在此进行判断
return true;
}
visited[w] = true;
Enqueue(Q,w);
}
}
}
return false;
}
2、判别有向图中是否存在有向环,当有向环存在时,输出构成环的顶点。(C语言源码看此文章)
这题算是图中很经典的一题,首先我们知道构成环的顶点必有一个出度和一个入度,我们由此出发,计算出每个顶点的出度和入度,将出度或者入度为0的顶点入栈并标记,因为它们必然不是构成环的顶点。将栈中元素依次出栈,当一个顶点出栈时相当于我们从图中删除了这个顶点,此时相邻顶点的出度和入度就会发生变化,再将出度或入度为0的顶点进栈并标记。循环此操作直到栈空为止,就会将不是构成环的顶点全部标记出来,我们可以定义一个计数器记录标记顶点的个数,当这个个数小于总的顶点数时,那么剩余的顶点数必然构成了一个环,只需打印出没被标记的顶点就是构成环的顶点了。
如图,首先顶点5出度为0,顶点 6 入度为0,符合条件入栈;顶点5出栈,没有新增符合条件的顶点,顶点6出栈,也没有新增符合条件的顶点,此时栈空;剩下的顶点1、2、3、4必然构成一个环。
注意:单独的判断图中是否存在环只用出度为0或者入度为0就可以了,一般都是用入度为0开始判断,采用的就是拓扑排序,思路与上述一样,先将入度为0顶点入栈,然后依次出栈,并将入度变成0的顶点入栈,直到栈空,用count记录进入栈中的元素,count小于总顶点数则表明还有顶点剩下,会构成一个环;等于总顶点数则无环;这也是存在环的有向图中没有拓扑排序。
但是还要打印出构成环的顶点我感觉光入度为0或者出度为0就不够了,得入度为0且出度为0两个条件都要满足。
如上图,用入度为0的条件来模拟一遍流程
首先只有顶点 6 入度为0,入栈并标记;然后顶点 6 出栈,相当于从图中删除顶点 6 ,导致顶点 5 入度由 2 变成 1 ,并不符合入度为0的条件无法入栈标记,所以最终被标记的只有顶点 6 ,打印剩下的顶点1、2、3、4、5,多了个5。
但是此图可以用出度为0的条件达到正确的结果
顶点 5 出度为0,入栈并标记;顶点 5 出栈相当于从图中删除顶点 5,导致顶点 6 出度为0,顶点 6 入栈并标记,最后顶点 6 出栈结束。剩下未标记的有1、2、3、4,刚好是构成环的顶点。
这个图又和之前的图恰恰相反,用入度为0的条件可以打印出构成环的顶点,但用出度为0就不行。
所以我认为要正确打印出构成环的顶点需要入度为0和出度为0条件都成立。
代码如下:
void FindInDegree( Graph G, int indegree[]){ //计算每个顶点的入度
int i;
ArcNode *p;
for (i=0;i<G.vexnum;i++)
indegree[i]=0;//初始化入度
for (i=0;i<G.vexnum;i++)
{
p=G.adjlist[i].first;
while(p){
indegree[p->data]++;
p=p->next;
}
}
}
void FindOutDegree( Graph G, int outdegree[]){ //计算每个顶点的出度
int i;
ArcNode *p;
for (i=0;i<G.vexnum;i++)
outdegree[i]=0;//初始化出度
for (i=0;i<G.vexnum;i++)
{
p=G.adjlist[i].first;
while(p){
indegree[i]++;
p=p->next;
}
}
}
void findCircle(Graph G){
int indegree[MaxVertexNum],outdegree[MaxVertexNum];
SeqStack S;//定义一个栈
int i,j,count = 0;//count记录入栈的顶点数目
ArcNode *p;
FindInDegree(G,indegree);//计算每个顶点的入度
FindOutDegree(G,outdegree);//计算每个顶点的出度
for(i = 0;i<G.vexnum;i++){//初始化标记数组
visited[i] = false;
}
for(i = 0;i<G.vexnum;i++){
if(indegree[i]==0||outdegree[i]==0){//将图中所有顶点出度或者入度为0的入栈
Push(S,i);//入栈
visited[i] = true;//标记
}
}
while(!IsEmpty(S)){//栈不为空
i = Pop(S);
count++;
for(p = G.adjlist[i].first;p;p = p->next){//计算入度的变化
j= p->data;
indegree[j]--;
}
for(j = 0;j<G.vexnum;j++){//计算出度的变化就要对整个图的邻接表进行遍历了
for(p = G.adjlist[j].first;p;p = p->next){
if(p->data == i){
outdegree[j]--;
}
}
}
for(j = 0;j<G.vexnum;j++){
if((indegree[j]==0||outdegree[j]==0)&&!visited[j]){
//将出度或者入度为0 并且没有入栈的顶点入栈;也就是将出度或入度变为0的顶点入栈
Push(S,j);
}
}
}
if(count<G.vexnum){
printf("存在有向环");
for(j = 0;j<G.vexnum;j++){
if(visited[j] == false){
printf("%5d",j);//打印没有被标记的顶点
}
}
}else{
printf("G中无环");
}
}
3、邻接表生成逆邻接表
在邻接表中顶点表结点 0 后面跟着边表结点 1 和 2 ,那么在逆邻接表中顶点表结点 1 和 2 后面就要跟着 边表结点 0了。创建逆邻接表采用头插法更高效。
Graph *Reverse(Graph G)
{
int i,k;
ArcNode *p1,*p2;
Graph rG;//创建逆邻接表
for(i=0; i<=G.vexnum; i++)
{
p1=G.adjlist[i].first;
while(p1)
{
k=p1->data;
p2->data=i;
p2->next=rG.adjlist[k].first;//采用头插法
rG.adjlist[k].first=p2;
p1=p1->next;
}
}
return rG;
}
六、总结
对于链表这方面的编程题主要还是先进行分析再做题,题目做的多了,慢慢也就掌握了做题的技巧。树、图这方面的编程题难度大,首先要掌握基本的遍历,再去练习各种题,在做题过程中掌握各种解题技巧,在这方面编程题的考试中多多少少都能写出一点代码出来,不至于毫无头绪。
本文至此结束,如果有什么错误或者疑问还请指出!