目录
栈和队列的概念
栈的基本概念
1 定义:栈是一种只能在一端进行插入或删除操作的线性表。操作端称为栈顶 (TOP),相应另一端即表底是固定不变的栈底。栈的插入和删除称为入栈和出栈。
由此可知栈具有线性表的一切特性:有限、有序、同型以及可空。
2 特点:栈最主要的特点是先进后出(FILO)。
3 存储结构:栈的本质是在操作上稍加限制的线性表,因此栈分为顺序栈和链式栈。栈顶由栈顶指针指示,对于顺序栈,其为一个记录栈顶数组下标的整型变量;对于链式栈,其为指向栈顶结点的指针。
注意区分顺序栈的栈顶指针和链栈的栈顶指针,类似于静态链表和普通链表的区别。
4 数学性质:当 n 个元素以某种顺序进栈,并且可在任意时刻出栈(在满足先进后出的前提下)时,所获得的元素排列的数目 N 恰好满足函数 Catalan() 的计算,即
队列的基本概念
1 定义:队列是一种只能在一端进行插入,在另一端进行删除的线性表。插入端为队尾 (Rear),删除端为队头 (Front)。队列的插入和删除称为进队和出队。
2 特点:队列最主要的特点是先进先出(FIFO)。
3 存储结构:队列的本质也是在操作上稍加限制的线性表,因此队列分为顺序队和链队。进队的元素进队后就成为新的队尾元素,出队的元素出队后,后继元素就成为新的队头元素。
栈和队列的存储结构、算法与应用
结构体定义
1 顺序栈
typedef struct
{
int data[maxsize];
int top;
}SqStack;
数组存放栈中元素,top 为栈顶指针,SqStack 为顺序栈类型定义。
2 链栈
typedef struct
{
int data;
struce LNode *next;
}LNode;
定义了数据域 data ,指针域 next (不是 *next),链栈结点 LNode。其实也能看出这里定义的是一个单向链表。
3 顺序队列
typedef struct
{
int data[maxsize];
int front;
int rear;
}SqQueue;
定义了队首指针 front,队尾指针 rear,顺序队类型 SqQueue。
4 链队定义
链队的定义是分两部分的,一部分是定义结点的结构体,一部分是定义队首队尾指针的结构体。
(1)链队结点类型定义
typedef struct QNode
{
int data;
struct QNode *next;
}QNode;
(2)链队类型定义
typedef struct
{
QNode *front;
QNode *rear;
}LiQNode;
顺序栈
1 顺序栈(Sequence stack)的要素
(1)两个状态
1)栈空状态:St.Top(St.Top) == -1。当栈是空的时候,栈顶指针不指向有效的空间,可以理解为指向栈底的下方(数组下标为0的空间是数组的第一个空间,也是要存储元素的),所以用 -1 来表示栈为空。
注意 St.Top == 0 和 1 的区别,栈顶指针初始状态的不同会导致后面的入栈出栈的也要发生相应的改变。
2)栈满状态:St.Top == maxsize - 1,maxsize 是栈的最大长度。因为顺序栈本质是线性表(即数组),数组下标是从 0 开始计数的,所以栈顶下标为数组长度 -1。
3)非法状态(上溢和下溢)
栈满后继续入栈会造成上溢,栈空后继续出栈会造成下溢。
(2)两个操作
附加)建栈:只需将栈顶指针置为 -1 即可。
void initStack (SqStack &St)
{
if (St.Top == -1) return 1; // 建栈成功返回 1,失败返回 0
else return 0;
}
1)元素 x 的入栈操作:++(St.Top); St.data[St.Top] = x;。因为规定了 Top 为 -1 时栈空,因此元素入栈是先上移指针,再压入元素。当然反过来进行也是可以的,本质上没有区别。入栈的两条语句可以简化为代码段中所示。
int Push (SqStack &St, int x)
{
if (St.Top = maxsize - 1) return 0; // 栈满入栈失败返回 0
St.data[++(St.Top)] = x; // 先上移指针,再压入元素
return 1;
}
2)元素 x 的出栈操作:x = St.data[St.Top]; --(St.Top);。出栈不同于进栈,出栈时必须先下移指针,再弹出元素。不能颠倒,否则会丢失栈顶的元素。出栈的两条语句可以简化为代码段中所示。
int pop (SqStack &St, int &x)
{
if (St.Top = -1) return 0; // 栈为空出栈失败返回 0
St.data[--(St.Top)]; // 必须先下移指针,再弹出元素
return 1;
}
很多有关数组的数据结构的元素扩充或减少都都需要先检查数组的状态。
(3)补充
考研里面,栈仅仅是一个工具,力求简洁和方便,因此上述的定义方法可以完成不用,而采用如下写法
int stack[maxsize]; // 两句话定义加初始化一个栈
int top = -1; // 栈顶指针和元素的类型视题目而定
stack[++top] = x; // 一句实现进栈
x = stack[top--]; // 一句实现出栈
同样的,判断栈满和栈空的语句也是视题目而定。比如入栈元素不多,但栈足够大就无需考虑栈满的情况,写算法时要加上自己的解释。对于其他很多基础工具也可以采用这样的一条思路,尽量简化掉一些不必要的语句。当然并不是越简洁越好,有时候为了保证算法的完整性某些步骤还是需要保留下来的。
a++ 和 ++a 的选择:
如果是内建数据类型,两者的效率是没有区别的。
如果是自定义数据类型,两者就会有效率上的差别了,++a 的效率更高。
在 a++ 和 ++a 等效的情况下,后者的执行效率是要高于前者的,所以多采用 ++a。
应用
下面用两个例子来让大家栈的应用场景。
1 设计一个算法,判断一个表达式中的左右小括号是否配对,表达式从字符数组 exp[n] 中读取。
这一例的关键问题在于,怎么处理多个“左括号”和多个“右括号”的配对?假如先扫描出了两个左括号,当扫描出一个右括号时该和哪一个匹配?你当然可以用两个边路来记录左右括号的个数,但是看不出表达式是否出错了。因为配对不仅需要左右括号的个数相等,同时要正确的配对,不能一个右括号配对一个左括号。栈就是解决此类问题的很方便的工具。即在解决问题的过程中出现了一个子问题,但凭现有条件不能解决它,需要记下,等待以后出现可以解决它的条件后再返回来解决。栈具有“记忆”的功能,这是它的FILO特性所延伸出来的一种特性。
int match (char exp[],int n)
{
char stack[maxsize];
int top=-1;
int i;
for (i = 0; i < n; i++) {
if (exp[i] == '(') // 如果遇到“(”,则入栈等待以后处理
stack[++top] = '('; // 一句话完成入栈操作
if (exp[i] == ')') { // 如果遇到“)”
if (top == -1) return 0; // 栈空,则不匹配
else --top; // 弹出一个“)”用于配对
}
}
if (top == -1) return 1; // 栈空(所有括号都被处理掉),则说明配对完全
else return 0; // 否则括号不匹配
}
2 设计一个算法求后缀式的数值,后缀式存在字符数组 exp[] 中并以“ \0 ”作为结束符,假设后缀式中的数都是个位数。
后缀式的求值要用栈来解决。对于一个后缀式,当从左往右扫描到一个数值的时候,具体怎么运算,此时还不知道,需要扫描到后边的运算符才知道,因此必须先存起来。所以要用到栈。后缀式和前缀式都只有唯一的一种运算次序,而中缀式却不一定,如 a + b + c。后缀式和前缀式是由中缀式按某一种运算次序而生成的,因此对于一个中缀式可能有多种后缀式或者前缀式。
int op (int a, char Op, int b)
{
if (Op == '+') return a + b;
if (Op == '-') return a - b;
if (Op == '*') return a * b;
if (Op == '/') {
if (b == 0) { // 分母为 0 输出错误
printf("ERROR!"); // 小细节不能忽视
return 0;
}
else return a / b;
int com (char exp[]) // 后缀式计算表达式
{
int i, a, b, c; // a,b 为操作数,c保存结果
/*注意要用 int 类型来定义初始化并定义栈,因
为运算过程中可能产生多位的数字所以用整型*/
int stack[maxsize];
int top = -1;
char Op; // Op 用来取运算符
for (i = 0; exp[i] != '\0'; ++i) {
if (exp[i] >= '0' && exp[i] <= '9') // 碰到数字入栈
stack[++top] = exp[i] - '0'; // 字符型转换为整型
else { // 碰到运算符
Op = exp[i]; // 取运算符
b = stack[top--]; // 取第二个操作数(后入栈的)
a = stack[top--]; // 取第一个操作数
c = op (a, Op, b); // 运算,结果存于 c
stack[++top] = c; // 运算结果入栈
}
}
return stack[top];
}
这段代码好多啊码的我好累啊,以后长代码都只讲一下思路好了,真的太长了,读者自己去写一下吧。。。
顺带提一下字符型和整型的转换,就当做是复习了。
如果定义一个整型变量 a,执行 a = '5';,则此时 a 里边保存的是 5 的 ASCII 码值而不是 5。如何将'5'这个字符代表的真正意义,即5这个整数保存于 a 中,只需执行 a ='5' - '0';即可。
同理,如果把一个整型数字(假设为a)转化为对应的字符型数字(ASCII码值)存储在字符变量(假设为b)中,只需执行 b = a + '0';即可。
这两种转化只适用于 0 ~ 9 这 10 个数字。
中缀表达式如何转化为后缀表达式
从头到尾读取中缀表达式的每个对象,对不同对象按不同的情况处理
① 运算数:直接输出;
② 左括号:压入堆栈;
③ 右括号:将栈顶的运算符弹出并输出,直到遇到左括号(出栈,不输出)
④ 运算符:
若优先级大于栈顶运算符时,则把它压栈;
若优先级小于等于栈顶运算符时,将栈顶运算符弹出并输出;再比较新的栈顶运算符,直到该运算符大于栈顶运算符优先级为止,然后将该运算符压栈;
⑤ 若各对象处理完毕,则把堆栈中存留的运算符一并输出。
链栈
1 链栈(Linked stack)的要素
(1)两个状态
1)栈空状态:ls -> next == NULL。判断栈空的语句和单链表的一样。
2)栈满状态:链表的内存是动态分配的,因此不存在栈满的状态。
(2)两个操作
附加)建栈:
void initStack (LNode *&ls)
{
ls = (LNode*)malloc(sizeof(LNode)); // 制造一个头结点
ls -> next = NULL; // 众所周知,必不可少的一步
}
1)元素(指针 p 所指)进栈操作:p -> next = ls -> next; ls -> next = p;。可以看出这是头插法。
void push (LNode *ls, int x)
{
LNode *p;
p = (LNode*)malloc(sizeof(LNode));
p -> next = NULL;
/*头插法*/
p -> data = x;
p -> next = ls -> next;
ls -> next = p;
}
2)元素 x 出栈操作(将其值保存在 x 中):p = Cs -> next; x = p -> data; Cs -> next = p -> next; free(p);。可以看出这是单链表的删除操作。
void push (LNode *ls, int x)
{
LNode *p;
if (ls -> next == NULL) return 0;
/*单链表的删除操作*/
p = ls -> next;
x = p -> next;
ls -> next = p -> next;
free(p);
return 1;
}
3)用不带头结点的单链表存储链栈,设计初始化栈、判断栈是否为空、进栈和出栈等相应的算法
void initStackl (LNode *&ls) // 栈初始化
{
Cs = NULL;
}
int isEmptyl (LNode *ls) // 判断栈是否为空
{
if(Cs == NULL); return 1;
else return 0;
}
void pushl (LNode *&ls,int x) // 进栈
{
LNode *p;
p = (LNode*)malloc(sizeof(LNode))
p -> next = NULL;
p -> data = x;
/*插入/入栈*/
p -> next = ls;
ls = p;
}
int popl (LNode *&ls,int &x) // 出栈
{
LNode *p;
if(ls == NULL) return 0;
p = ls;
/*删除/出栈*/
x = p -> data;
ls = p -> next;
free(p);
return 1;
}
顺序队
1 循环队列
(1)假溢出:顺序队中,元素的进队出队分别会导致队尾指针(rear)和队首指针(front)的后移,在经过一定次数之后,两者都会指向数组末端的 maxsize - 1 处。此时队列虽然是空的,但是无法让元素入队,这就是所谓的“假溢出”。
(2)为了解决假溢出的问题,将数组的首尾相连形成一个环便成了循环队列。
2 循环队列(Circular queue)的要素
(1)两个状态
1)队空状态:qu.rear == qu.front。
2)队满状态:(qu.rear + 1) % maxsize == qu.front。
循环队列存在一个问题:队列初始空状态两指针指向同一个位置,队列满状态时两指针仍然指向一个位置,因此指针相等时队列是空是满都有可能。因此另外设一个判断语句来判断队列的状态。
(2)两个操作
1)元素 x 进队操作:qu.rear = (qu.rear + 1) % maxsize; qu.data[qu.rear] = x;。
2)元素 x 出队操作:qu.front = (qu.front + 1) % maxsize; x = qu.data[qu.front];。
3) 队列的初始化、判空、进队和出队算法
void initQueue (SqQueue &qu)
{
qu.front = qu.rear = 0;
}
int isQueueEmpty (SqQueue qu)
{
if (qu.front == qu.rear) return 1;
else return 0;
}
int enQueue (SqQueue &qu, int x)
{
if ((qu.rear + 1) % maxsize == qu.front) return 0; // 队满不能入队
qu.rear = (qu.rear + 1) % maxsize; // 先动指针
qu.data[qu.rear] = x; // 再入元素
return 1;
}
int deQueue (SqQueue &qu, int &x)
{
if (qu.front == qu.rear) return 0; // 队空不能出队
qu.front = (qu.front + 1) % maxsize; // 先动指针
x = qu.data[qu.front]; // 再出元素
return 1;
}
同样的,以上这些函数在书写程序题目的时候并不实用,需要在题目中提取其中有用的操作。
链队
1 链队(Linked queue)的要素
(1)两个状态
1)队空状态:lqu -> rear == NULL 或者 lqu -> front == NULL。
2)队满状态:不存在(内存无限大时)。
(2)两个操作
1)p 指向元素进队操作:lqu -> rear -> next = p; lqu -> rear = p;。
2)p 所指元素出队操作: lqu -> front = p -> next; x = p -> data; free(p);。
链队的代码就不贴了,和单链表的其实没有太大的区别。哦对了提一下初始化链队和判断队空的算法
void initQueue (LiQueue *&lqu)
{
lqu = (LiQueue*)malloc(sizeof(LiQueue));
lqu -> front = lqu -> rear = NULL;
}
int isQueueEmpty (LiQueue *lqu)
{
if (lqu -> rear == NULL || lqu -> front == NULL) return 1;
else return 0;
}
要注意链队的结点和队首队尾指针的结构体是不同的,因此初始化的时候用的是 LiQueue 来定义 rear 和 front。