数据结构-栈和队列
1.栈
1.1 栈的定义
栈是一种特殊的线性表。其特殊性在于限定插入和删除数据元素的操作只能在线性表的一端进行。如下所示:
结论:后进先出(Last In First Out),简称为LIFO线性表。
栈的基本运算有六种:
构造空栈:InitStack(S)、
判栈空: StackEmpty(S)、
判栈满: StackFull(S)、
进栈: Push(S,x)、可形象地理解为压入,这时栈中会多一个元素
退栈: Pop(S) 、 可形象地理解为弹出,弹出后栈中就无此元素了。
取栈顶元素:StackTop(S),不同与弹出,只是使用栈顶元素的值,该元素仍在栈顶不会改变。
由于栈也是线性表,因此线性表的存储结构对栈也适用,通常栈有顺序栈和链栈两种存储结构,这两种存储结构的不同,则使得实现栈的基本运算的算法也有所不同。
我们要了解的是,在顺序栈中有"上溢"和"下溢"的概念。顺序栈好比一个盒子,我们在里头放了一叠书,当我们要用书的话只能从第一本开始拿(你会把盒子翻过来吗?真聪明^^),那么当我们把书本放到这个栈中超过盒子的顶部时就放不下了(叠上去的不算,哼哼),这时就是"上溢","上溢"也就是栈顶指针指出栈的外面,显然是出错了。反之,当栈中已没有书时,我们再去拿,看看没书,把盒子拎起来看看盒底,还是没有,这就是"下溢"。"下溢"本身可以表示栈为空栈,因此可以用它来作为控制转移的条件。
链栈则没有上溢的限制,它就象是一条一头固定的链子,可以在活动的一头自由地增加链环(结点)而不会溢出,链栈不需要在头部附加头结点,因为栈都是在头部进行操作的,如果加了头结点,等于要在头结点之后的结点进行操作,反而使算法更复杂,所以只要有链表的头指针就可以了。
1.2 栈的顺序存储
使用c++的面向对象封装:
// Test.cpp : Defines the entry point for the console application. // #include "stdafx.h" #include <iostream> using namespace std; #define MAX 10 // MAXIMUM STACK CONTENT class stack { private: int arr[MAX]; int top; public: stack() { inItStack(); } /************************************************************************/ /* 初始化栈 */ /************************************************************************/ void inItStack() { top=-1; } /************************************************************************/ /* 入栈 */ /************************************************************************/ void push(int a) { top++; if(top < MAX) { arr[top]=a; } else { cout<<"STACK FULL!!"<<top; } } /************************************************************************/ /* 出栈 */ /************************************************************************/ int pop() { if(isEmpty()) { cout<<"STACK IS EMPTY "; return NULL; } else { int data=arr[top]; arr[top]=NULL; top--; return data; } } /************************************************************************/ /* 是否为空 */ /************************************************************************/ bool isEmpty() { if(top == -1) return true; else return false; } }; int main() { stack a; a.push(3); a.push(10); a.push(1); cout<<"Pop:"<<a.pop(); return 0; }结论:由于栈的插入和删除操作具有它的特殊性,所以用顺序存储结构表示的栈并不存在插入删除数据元素时需要移动的问题,但栈容量难以扩充的弱点仍就没有摆脱。
1.3 栈的链式存储
若是栈中元素的数目变化范围较大或不清楚栈元素的数目,就应该考虑使用链式存储结构。人们将用链式存储结构表示的栈称作"链栈"。链栈通常用一个无头结点的单链表表示。如图所示:
栈的操作是线性表操作的特例。
简单用c实现:
// Test.cpp : Defines the entry point for the console application. // #include "stdafx.h" #include <stdio.h> #include "stdlib.h" #include <iostream> using namespace std; //宏定义 #define TRUE 1 #define FALSE 0 #define OK 1 #define ERROR 0 #define INFEASIBLE -1 #define OVERFLOW -2 #define STACKEMPTY -3 #define LT(a,b) ((a)<(b)) #define N = 100 typedef int Status; typedef int ElemType; typedef struct LNode{ ElemType data; struct LNode *next; }LNode, *LinkList; typedef struct stack{ LinkList top; } STACK; /************************************************************************/ /* 接口: */ /************************************************************************/ void InitStack(STACK &S); void Push(STACK &S,ElemType e); void Pop(STACK &S, ElemType *e); ElemType GetTop(STACK S,ElemType *e); int StackEmpty(STACK S); /************************************************************************/ /* */ /************************************************************************/ void InitStack(STACK &S) { S.top=NULL; } /************************************************************************/ /* 入栈 */ /************************************************************************/ void Push(STACK &S,ElemType e) { LinkList p; p = (LinkList )malloc(sizeof(LNode)); if (!p) exit(OVERFLOW); p->data = e; p->next = S.top; S.top = p; } /************************************************************************/ /* 出栈 */ /************************************************************************/ void Pop(STACK &S, ElemType *e) { LinkList p; if(StackEmpty(S)) exit(STACKEMPTY); *e = S.top->data; p = S.top; S.top = p->next; free(p); } /************************************************************************/ /* 获取栈顶元素内容 */ /************************************************************************/ ElemType GetTop(STACK S, ElemType *e) { if(StackEmpty(S)) exit(STACKEMPTY); *e = S.top->data; } /************************************************************************/ /* 判断栈S是否空 */ /************************************************************************/ int StackEmpty(STACK S) { if(S.top==NULL) return TRUE; return FALSE; } void main() { STACK S; InitStack( S); Push(S, 3); Push(S, 4); ElemType e; Pop(S,&e); cout<<"Pop elem:"<<e; }
1.4 栈的应用
1) 数制转换
2)语法词法分析
3)表达式求值等
1.5 栈的递归和实现
汉诺塔的问题:
解决:
1)如果有一个盘子,直接从X移到Z即可。
2)如果有n个盘子要从X移到Z,Y作为辅助。问题可以转化为,先将上面n-1个从X移动到Y,Z作为辅助,然后将第n个从X移动到Z,最后将剩余的n-1个从Y移动到Z,X作为辅助。
完整实现代码,包括栈的实现:
// Test.cpp : Defines the entry point for the console application. // #include "stdafx.h" #include <stdio.h> #include "stdlib.h" #include <iostream> using namespace std; //宏定义 #define TRUE 1 #define FALSE 0 #define OK 1 #define ERROR 0 #define INFEASIBLE -1 #define OVERFLOW -2 #define STACKEMPTY -3 #define LT(a,b) ((a)<(b)) #define N = 100 typedef int Status; typedef int ElemType; typedef struct LNode{ ElemType data; struct LNode *next; }LNode, *LinkList; typedef struct stack{ LinkList top; } STACK; /************************************************************************/ /* 接口: */ /************************************************************************/ void InitStack(STACK &S); void Push(STACK &S,ElemType e); void Pop(STACK &S, ElemType *e); ElemType GetTop(STACK S,ElemType *e); int StackEmpty(STACK S); /************************************************************************/ /* */ /************************************************************************/ void InitStack(STACK &S) { S.top=NULL; } /************************************************************************/ /* 入栈 */ /************************************************************************/ void Push(STACK &S,ElemType e) { LinkList p; p = (LinkList )malloc(sizeof(LNode)); if (!p) exit(OVERFLOW); p->data = e; p->next = S.top; S.top = p; } /************************************************************************/ /* 出栈 */ /************************************************************************/ void Pop(STACK &S, ElemType *e) { LinkList p; if(StackEmpty(S)) exit(STACKEMPTY); *e = S.top->data; p = S.top; S.top = p->next; free(p); } /************************************************************************/ /* 获取栈顶元素内容 */ /************************************************************************/ ElemType GetTop(STACK S, ElemType *e) { if(StackEmpty(S)) exit(STACKEMPTY); *e = S.top->data; } void printStack(STACK S){ LinkList p; p = S.top; printf("栈: "); while (p) { printf("%d ", p->data); p = p->next; } } /************************************************************************/ /* 如果有一个盘子,直接从X移到Z即可。 如果有n个盘子要从X移到Z,Y作为辅助。问题可以转化为,先将上面n-1个从X移动到Y,Z作为辅助,然后将第n个从X移动到Z,最后将剩余的n-1个从Y移动到Z,X作为辅助。 */ /************************************************************************/ void move(STACK &Sa,STACK &Sb) { ElemType e; Pop(Sa,&e); Push(Sb, e); } void hanoi(int n,STACK &X,STACK &Y,STACK &Z) { if(n==1) return move(X, Z); //将圆盘1号直接移到z hanoi(n-1,X,Z,Y); //将x上的1大n-1圆盘移到y,z做辅助塔 move(X, Z); //将编号为n的圆盘移z hanoi(n-1,Y,X,Z); //将y上的1大n-1圆盘移到z,x做辅助塔 } /************************************************************************/ /* 判断栈S是否空 */ /************************************************************************/ int StackEmpty(STACK S) { if(S.top==NULL) return TRUE; return FALSE; } void main() { STACK Sx, Sy,Sz; InitStack( Sx); InitStack( Sy); InitStack( Sz); int i, n = 10; for (i = 10 ; i>=1 ;i--) { Push(Sx, i); } printStack(Sx); hanoi(n, Sx,Sy,Sz); printStack(Sz); }
1.队列
1.1 队列定义
队列(Queue)也是一种运算受限的线性表,它的运算限制与栈不同,是两头都有限制,插入只能在表的一端进行(只进不出),而删除只能在表的另一端进行(只出不进),允许删除的一端称为队尾(rear),允许插入的一端称为队头 (Front)
,队列的操作原则是先进先出的,所以队列又称作FIFO表(First In First Out)
队列的基本运算也有六种:
置空队 :InitQueue(Q)
判队空: QueueEmpty(Q)
判队满: QueueFull(Q)
入队 : EnQueue(Q,x)
出队 : DeQueue(Q)
取队头元素: QueueFront(Q),不同与出队,队头元素仍然保留。
队列也有顺序存储和链式存储两种存储结构,前者称顺序队列,后者为链队。
对于顺序队列,我们要理解"假上溢"的现象。
我们现实中的队列比如人群排队买票,队伍中的人是可以一边进去从另一头出来的,除非地方不够,总不会有"溢出"的现象,相似地,当队列中元素完全充满这个向量空间时,再入队自然就会上溢,如果队列中已没有元素,那么再要出队也会下溢。
那么"假上溢"就是怎么回事呢?
因为在这里,我们的队列是存储在一个向量空间里,在这一段连续的存储空间中,由一个队列头指针和一个尾指针表示这个队列,当头指针和尾指针指向同一个位置时,队列为空,也就是说,队列是由两个指针中间的元素构成的。在队列中,入队和出队并不是象现实中,元素一个个地向前移动,走完了就没有了,而是指针在移动,当出队操作时,头指针向前(即向量空间的尾部)增加一个位置,入队时,尾指针向前增加一个位置,在某种情况下,比如说进一个出一个,两个指针就不停地向前移动,直到队列所在向量空间的尾部,这时再入队的话,尾指针就要跑到向量空间外面去了,仅管这时整个向量空间是空的,队列也是空的,却产生了"上溢"现象,这就是假上溢。
为了克服这种现象造成的空间浪费,我们引入循环向量的概念,就好比是把向量空间弯起来,形成一个头尾相接的环形,这样,当存于其中的队列头尾指针移到向量空间的上界(尾部)时,再加1的操作(入队或出队)就使指针指向向量的下界,也就是从头开始。这时的队列就称循环队列。
通常我们应用的大都是循环队列。由于循环的原因,光看头尾指针重叠在一起我们并不能判断队列是空的还是满的,这时就需要处理一些边界条件,以区别队列是空还是满。方法至少有三种,一种是另设一个布尔变量来判断(就是请别人看着,是空还是满由他说了算),第二种是少用一个元素空间,当入队时,先测试入队后尾指针是不是会等于头指针,如果相等就算队已满,不许入队。第三种就是用一个计数器记录队列中的元素的总数,这样就可以随时知道队列的长度了,只要队列中的元素个数等于向量空间的长度,就是队满。
2.2 队列的顺序存储
顺序存储如图:
由于是顺序存储结构的存储空间是静态分配的,所以在添加数据的时,有可能没有剩余空间的情况。
解决这种“假溢出”情况,使用循环队列。在c语言中,不能用动态分配的一维数组来实现循环队列。若使用循环队列,必须设置最大队列长度,若无法估计最大长度,就使用链式队列。
c实现:
// Test.cpp : Defines the entry point for the console application. // #include "stdafx.h" #include <stdio.h> #include "stdlib.h" #include <iostream> using namespace std; //宏定义 #define TRUE 1 #define FALSE 0 #define OK 1 #define ERROR 0 #define INFEASIBLE -1 #define OVERFLOW -2 #define QUEUEEMPTY -3 #define MAX_QUEUE 10 //队列的最大数据元素数目 typedef int Status; typedef int ElemType; typedef struct queue{ ElemType elem[MAX_QUEUE] ; ///假设当数组只剩下一个单元时认为队满 int front; //队头指针 int rear; //队尾指针 }QUEUE; /************************************************************************/ /* 各项基本操作算法。 */ /************************************************************************/ void InitQueue(QUEUE *&Q); void EnQueue(QUEUE *Q,ElemType elem); void DeQueue(QUEUE *Q,ElemType *elem); int QueueEmpty(QUEUE Q); /************************************************************************/ /* 初始化 直接使用结构体指针变量,必须先分配内存地址,即地址的指针 */ /************************************************************************/ void InitQueue(QUEUE *&Q) { Q = (QUEUE *) malloc (sizeof(QUEUE)); Q->front = Q->rear = -1; } /************************************************************************/ /* 入队 */ /************************************************************************/ void EnQueue(QUEUE *Q, ElemType elem) { if((Q->rear+1)% MAX_QUEUE == Q->front) exit(OVERFLOW); Q->rear = (Q->rear + 1)%MAX_QUEUE; Q->elem[Q->rear] = elem; } /************************************************************************/ /* 出队 */ /************************************************************************/ void DeQueue(QUEUE *Q,ElemType *elem) { if (QueueEmpty(*Q)) exit(QUEUEEMPTY); Q->front = (Q->front+1) % MAX_QUEUE; *elem=Q->elem[Q->front]; } /************************************************************************/ /* 获取队头元素内容 */ /************************************************************************/ void GetFront(QUEUE Q,ElemType *elem) { if ( QueueEmpty(Q) ) exit(QUEUEEMPTY); *elem = Q.elem[ (Q.front+1) % MAX_QUEUE ]; } /************************************************************************/ /* 判断队列Q是否为空 */ /************************************************************************/ int QueueEmpty(QUEUE Q) { if(Q.front==Q.rear) return TRUE; else return FALSE; } void main() { QUEUE *Q; InitQueue( Q); EnQueue( Q, 1); EnQueue( Q, 2); ElemType e; DeQueue( Q,&e); cout<<"De queue:"<<e; }
注意:InitQueue(QUEUE *&Q) 传的是指针的地址。
2.3 链式队列:
// Test.cpp : Defines the entry point for the console application. // #include "stdafx.h" #include <stdio.h> #include "stdlib.h" #include <iostream> using namespace std; //宏定义 #define TRUE 1 #define FALSE 0 #define OK 1 #define ERROR 0 #define INFEASIBLE -1 #define OVERFLOW -2 #define QUEUEEMPTY -3 typedef int Status; typedef int ElemType; typedef struct LNode { //链式队列的结点结构 ElemType elem; //队列的数据元素类型 struct LNode *next; //指向后继结点的指针 }LNode, *LinkList; typedef struct queue{ //链式队列 LinkList front; //队头指针 LinkList rear; //队尾指针 }QUEUE; /************************************************************************/ /* 各项基本操作算法。 */ /************************************************************************/ void InitQueue(QUEUE *Q); void EnQueue(QUEUE *Q,ElemType elem); void DeQueue(QUEUE *Q,ElemType *elem); void GetFront(QUEUE Q,ElemType *elem) ; bool QueueEmpty(QUEUE Q); /************************************************************************/ /*初始化队列Q */ void InitQueue(QUEUE *Q) { Q->front = (LinkList)malloc(sizeof(LNode)); if (Q->front==NULL) exit(ERROR); Q->rear= Q->front; } /*入队 */ void EnQueue(QUEUE *Q,ElemType elem) { LinkList s; s = (LinkList)malloc(sizeof(LNode)); if(!s) exit(ERROR); s->elem = elem; s->next = NULL; Q->rear->next = s; Q->rear = s; } /*出队 */ void DeQueue(QUEUE *Q,ElemType *elem) { LinkList s; if(QueueEmpty(*Q)) exit(ERROR); *elem = Q->front->next->elem; s = Q->front->next; Q->front->next = s->next; free(s); } /*获取队头元素内容 */ void GetFront(QUEUE Q,ElemType *elem) { if(QueueEmpty(Q)) exit(ERROR); *elem = Q.front->next->elem; } /*判断队列Q是否为空 */ bool QueueEmpty(QUEUE Q) { if(Q.front == Q.rear) return TRUE; return FALSE; } void main() { QUEUE Q; InitQueue( &Q); EnQueue( &Q, 1); EnQueue( &Q, 2); ElemType e; DeQueue( &Q,&e); cout<<"De queue:"<<e; }
【举例1】银行排队
【举例2】模拟打印机缓冲区。
在主机将数据输出到打印机时,会出现主机速度与打印机的打印速度不匹配的问题。这时主机就要停下来等待打印机。显然,这样会降低主机的使用效率。为此人们设想了一种办法:为打印机设置一个打印数据缓冲区,当主机需要打印数据时,先将数据依次写入这个缓冲区,写满后主机转去做其他的事情,而打印机就从缓冲区中按照先进先出的原则依次读取数据并打印,这样做即保证了打印数据的正确性,又提高了主机的使用效率。由此可见,打印机缓冲区实际上就是一个队列结构。
【举例3】CPU分时系统
在一个带有多个终端的计算机系统中,同时有多个用户需要使用CPU运行各自的应用程序,它们分别通过各自的终端向操作系统提出使用CPU的请求,操作系统通常按照每个请求在时间上的先后顺序,将它们排成一个队列,每次把CPU分配给当前队首的请求用户,即将该用户的应用程序投入运行,当该程序运行完毕或用完规定的时间片后,操作系统再将CPU分配给新的队首请求用户,这样即可以满足每个用户的请求,又可以使CPU正常工作。