今天看图的广度优先遍历的时候,发现用到了队列,补一下链队列的知识,参考《大话数据结构》的P118~120,自己写了一个简单的测试例子便于理解。
理解起来并不难,用的是单链表结构。front指向链表的头结点(是虚结点,它的next指向第一个节点),rear指向链表的尾节点。
下面举个简单的例子,实现链队列的创建,入队和出队操作。
第一个程序调试了很久,编译没有问题,运行总是崩溃。是对内存分配没有考虑全面,先把错误的程序放上来思考一下。
1 #include <iostream>
2 #include <string>
3 #include <stdlib.h>
4 using namespace std; 5 #define OVERFLOW 0
6
7 //链队列的结点结构
8 typedef struct QNode 9 { 10 char data; 11 struct QNode *next;//定义指向下一个结点的指针next
12 }QNode; 13
14 //队列的链式存储结构
15 typedef struct
16 { 17 QNode *front,*rear;//定义指向队头结点和队尾结点的指针
18 }LinkQueue; 19
20 //入队操作,在链表尾部插入结点
21 LinkQueue *EnQueue(LinkQueue *Q,char e) 22 { 23 Q=new LinkQueue;//或者Q=(LinkQueue*)malloc(sizeof(LinkQueue));
24 Q->front=Q->rear=NULL; 25 QNode *s;//定义一个指向结点的指针s
26 s=new QNode;//或者s=(QNode *)malloc(sizeof(QNode));
27 if(!s) exit(OVERFLOW);//如果分配结点内存失败,直接结束程序 28 //exit函数的头文件是stdlib.h。 29 //exit的声明为void exit(int value); 30 //exit的作用是,退出程序,并将参数value的值返回给主调进程。 31 //exit(a); 等效于在main函数中return a; 32
33 //给s指向的新结点填内容
34 s->data=e; 35 s->next=NULL; 36 Q->rear->next=s;//使rear指向结点的next指向新节点s
37 Q->rear=s;//使rear指向新节点s
38 cout<<"ok"<<" "; 39 return Q;//返回指向该链队列的地址
40 } 41
42 //出队操作,把头结点的next指向的元素出队,返回出队的元素
43 char DeQueue(LinkQueue *Q) 44 { 45 char e; 46 QNode *s;//定义一个指向结点的指针s
47 if(Q->front==Q->rear) return 0;//如果链队列为空,返回0,也可以定义其他char类型的标志
48 s=Q->front->next;//把要删除的结点的地址暂存给p
49 e=s->data;//把要删除的结点的data给e,需要返回该元素
50 Q->front->next=s->next;//把要删除的结点的下一个结点的地址给头结点的next
51 if(Q->rear==s) 52 Q->rear=Q->front;//如果要删除的结点是队尾结点,说明删除该结点后链队列为空,rear和front同时指向头结点
53 delete s;//或者frees);
54 return e;//返回出队元素
55 } 56
57 int main() 58 { 59 LinkQueue *p=NULL; 60 p=EnQueue(p,'A');//插入A
61 cout<<p->rear->data<<endl; 62 }
运行到第36行时候崩溃
通过思考,是因为没有让Q指向的链结构LinkQueue中的front和rear指针指向new出来的QNode。
修改后的程序可以正常运行,代码和解释如下(VS2012测试通过):
1 #include <iostream>
2 #include <string>
3 #include <stdlib.h>
4 using namespace std; 5
6 #define OVERFLOW 0
7
8 //链队列的结点结构
9 typedef struct QNode 10 { 11 char data; 12 struct QNode *next;//定义指向下一个结点的指针next
13 }QNode; 14
15 //队列的链式存储结构,可以看到只占8个字节(sizeof(LinkQueue)==8)
16 typedef struct
17 { 18 QNode *front,*rear;//定义指向队头结点和队尾结点的指针
19 }LinkQueue; 20
21 //链队列的初始化,申请内存
22 LinkQueue *InitQueue(LinkQueue *Q) 23 { 24 Q=new LinkQueue;//或者Q=(LinkQueue*)malloc(sizeof(LinkQueue));
25 Q->front=Q->rear=new QNode; 26 //初始化为空队列(front==rear表示空队列),并且同时指向头结点,如果只是置为相等,不指向new出来的QNode,程序会崩溃
27 return Q; 28 } 29 //为什么new一个头结点,头结点的next指向链队列的第一个节点,可以思考一下如果没有头结点会怎样? 30
31 //入队操作,在链表尾部插入结点
32 void EnQueue(LinkQueue *Q,char e) 33 { 34 QNode *s;//定义一个指向结点的指针s
35 s=new QNode;//或者s=(QNode *)malloc(sizeof(QNode));
36 if(!s) exit(OVERFLOW);//如果分配结点内存失败,直接结束程序 37 //exit函数的头文件是stdlib.h。 38 //exit的声明为void exit(int value); 39 //exit的作用是,退出程序,并将参数value的值返回给主调进程。 40 //exit(a); 等效于在main函数中return a; 41
42 //给s指向的新结点填内容
43 s->data=e; 44 s->next=NULL; 45 Q->rear->next=s;//使rear指向结点的next指向新节点s
46 Q->rear=s;//使rear指向新节点s
47 cout<<"ok"<<" "; 48 } 49
50 //出队操作,把头结点的next指向的元素出队,返回出队的元素
51 char DeQueue(LinkQueue *Q) 52 { 53 char e; 54 QNode *s;//定义一个指向结点的指针s
55 if(Q->front==Q->rear) 56 { 57 cout<<"error"<<" "; 58 return 0;//如果链队列为空,返回0,也可以定义其他char类型的标志
59 } 60 s=Q->front->next;//把要删除的结点的地址暂存给p
61 e=s->data;//把要删除的结点的data给e,需要返回该元素
62 Q->front->next=s->next;//把要删除的结点的下一个结点的地址给头结点的next
63 if(Q->rear==s) 64 { 65 Q->rear=Q->front;//如果要删除的结点是队尾结点,说明删除该结点后链队列为空,rear和front同时指向头结点
66 } 67 delete s;//或者free(s);
68 cout<<"ok"<<" "<<e<<endl;//输出出队元素
69 return e;//返回出队元素
70 } 71
72 int main() 73 { 74 LinkQueue *p=NULL; 75 p=InitQueue(p);//如果缺少这一步程序就崩溃,一开始没有注意到这个问题,花了一个多小时 76
77 //入队
78 cout<<"in:"<<endl; 79 EnQueue(p,'A');//插入A
80 cout<<p->rear->data<<endl;//输出A
81 EnQueue(p,'B');//插入B
82 cout<<p->rear->data<<endl;//输出B
83 EnQueue(p,'C');//插入C
84 cout<<p->rear->data<<endl;//输出C
85 EnQueue(p,'D');//插入D
86 cout<<p->rear->data<<endl;//输出D 87
88 //出队
89 cout<<"out:"<<endl; 90 DeQueue(p);//输出A
91 DeQueue(p);//输出B
92 DeQueue(p);//输出C
93 DeQueue(p);//输出D
94 DeQueue(p);//没有元素了,出队失败
95 }
运行结果:
补充:比较循环队列和链队列
时间上:
基本操作入队和出队等都是常数时间,O(1)。
同样是O(1),也有细微差异。循环队列事先申请好空间,使用期间不释放。链队列每次申请和释放节点会存在时间开销。
空间上:
循环队列必须有一个固定的长度,如果存储的元素个数比长度小很多,造成空间浪费的问题。
链队列只需要一个指针域,代码中有讲到占8个字节(rear和front两个指针变量分别占4个字节)。所以空间上链队列更灵活。
总结:
在可以确定队列长度最大值的情况下,建议用循环队列。如果无法预估队列长度,用链队列。