栈和队列
栈和队列的基本概念
1. 栈的基本概念
1.1 栈的定义
栈是一种只能在一端进行插入或者删除操作的线性表。其中允许进行插入或删除操作的一端称为栈顶(TOP)。栈顶由一个叫作栈顶指针来表示,栈的另外一端称之为栈底,栈底是固定不变的。栈的插入和删除操作一般可以称之为入栈出栈。
1.2 栈的特点
主要特点为先进先出(FILO)。
1.3 栈的存储结构
可以用顺序表和链表来存储栈,分为顺序栈和链栈。
1.4 栈的数学性质
当n个元素以某种顺序进栈时,并且可以在任意时刻出栈,所获得的排列组合数目恰好满足函数Catalan():
2. 队列的基本概念
2.1 队列的定义
队列简称队,只允许一端插入另一端删除的线性表。可插入的一端称之为队尾(rear),可删除的一端称之为队头(front)。插入元素称为进队,删除元素称为出队。
2.2 队列的特点
队列的特点为先进先出(FIFO)。
2.3 队列的存储结构
可用顺序表和链表来存储队列,分为顺序队和链队。
栈和队列的存储结构与算法
下面主要介绍上述提到的顺序栈、顺序队、链栈和链队。
顺序栈
顺序栈主要有两个特殊状态和两个操作,分别为栈空、栈满和进栈、出栈。
#define MaxSize 1024
/*
* the sequence queue
**/
typedef struct
{
int data[MaxSize];
int front;
int rear;
}SeqQueue;
/*
* the sequence queue's operator
**/
/*
* initQueue()
* initializate the sequence queue
**/
void initQueue(SeqQueue &qu)
{
qu.front = qu.rear = 0;
}
/*
* isEmpty()
* emptiness(judge the sequence queue)
**/
int isEmpty(SeqQueue qu)
{
if(qu.front == qu.rear) return 1;
else return 0;
}
/*
* push()
* Enter the sequence queue
**/
int push(SeqQueue &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;
}
/*
* pop()
* out of the sequence queue
**/
int pop(SeqQueue &qu,int x)
{
if(qu.front == qu.rear) return 0;
qu.front = (qu.front+1) % MaxSize;
x = qu.data[qu.front];
return 1;
};
这里其实是按照标准操作来写的顺序栈,完全可以简化为我们常用的数组加个top指向栈顶就可以达到一样的作用。
int SeqStack[MaxSize]; int top = -1; // 初始化 顺序栈
SeqStack[++top] = x; // 进栈操作
x = SeqStack[top--]; // 出栈操作 !注意! 这里不能换成--top哈,道理很简单,自己领会。
链栈
链栈对应顺序栈来讲,同样有两个状态和两个操作。
#define MaxSize 1024
/*
* the link stack
**/
typedef struct LStackNode
{
int data; // data store the node's data field (default type int)
struct LStackNode *next; // point to successor node
}LStackNode;
/*
1. the link stack's operator
**/
/*
2. initStack()
3. initializate the Stack
**/
void initStack(LStackNode* &lst)
{
lst = (LStackNode*)malloc(sizeof(LStackNode));
lst->next = NULL;
}
/*
4. isEmpty()
5. emptiness(judge the Stack)
**/
int isEmpty(LStackNode *lst)
{
if(lst->next == NULL) return 1;
else return 0;
}
/*
6. push()
7. Enter the stack
**/
int push(LStackNode *lst,int x)
{
LStackNode *p;
p = (LStackNode*)malloc(sizeof(LStackNode));
p->next = NULL;
/* the insert of rear */
p->data = x;
p->next = lst->next;
lst->next = p;
}
/*
8. pop()
9. out of the stack
**/
int pop(LStackNode *lst,int &x)
{
LStackNode *p;
if(lst->next == NULL) return 0;
/* the delete of link list */
p = lst->next;
x = p->data;
lst->next = p->next;
free(p);
return 1;
}
这里涉及到一些链表的头插法,可以复习下前面有讲过的单链表的操作,结合代码比较容易理解,注解中有说明哪里用到了头插法。
顺序队列
普通的顺序队列在经过一系列的进队出队操作之后可能会出现”假溢出“,即队内没有元素也不允许元素进队。
这里主要说明下循环队列,循环队列即将顺序队整成一环,这样头指针和尾指针就可以一直走下去,不会出现“假溢出”。
具体操作过程如下图所示:
①由空队列进队两个元素,尾指针往后移动两格,front指向开头 0,rear指向 2.
②接着在①的基础上进队四个元素,出队三个元素,front指向 3,rear指向 6 .
③在②的基础上再进队两个元素,出队四个元素,front指向 7, rear指向 0.
循环队列也是有四个要素即两个状态和两个操作,队空和队满,进队和出队。
#define MaxSize 1024
/*
* the sequence queue
**/
typedef struct
{
int data[MaxSize];
int front;
int rear;
}SeqQueue;
/*
* the sequence queue's operator
**/
/*
* initQueue()
* initializate the sequence queue
**/
void initQueue(SeqQueue &qu)
{
qu.front = qu.rear = 0;
}
/*
* isEmpty()
* emptiness(judge the sequence queue)
**/
int isEmpty(SeqQueue qu)
{
if(qu.front == qu.rear) return 1;
else return 0;
}
/*
* push()
* Enter the sequence queue
**/
int push(SeqQueue &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;
}
/*
* pop()
* out of the sequence queue
**/
int pop(SeqQueue &qu,int x)
{
if(qu.front == qu.rear) return 0;
qu.front = (qu.front+1) % MaxSize;
x = qu.data[qu.front];
return 1;
};
链队
链队顾名思义利用链式存储的队列,这里以单链表实现为例子,链队即不存在队满上溢(除开内存满)。
链队也四要素,两个操作和两个特殊状态。
出队入队操作可如下图所示:
#define MaxSize 1024
/*
* the link queue
**/
typedef struct LQNode
{
int data; // data store the node's data field (default type int)
struct LQNode *next; // point to successor node
}LQNode;
typedef struct
{
LQNode *front;
LQNode *rear;
}LinkQueue;
/*
* the link queue's operator
**/
/*
* initQueue()
* initializate the link queue
**/
void initQueue(LinkQueue &linkqueue)
{
linkqueue = (LinkQueue*)malloc(sizeof(LinkQueue));
linkqueue->front = linkqueue->rear = NULL;
}
/*
* isEmpty()
* emptiness(judge the link queue)
**/
int isEmpty(LinkQueue linkqueue)
{
if(linkqueue->rear == NULL || linkqueue->front == NULL) return 1;
else return 0;
}
/*
* enQueue()
* Enter the link queue
**/
void enQueue(LinkQueue *linkqueue,int x)
{
LQNode *p;
p = (LQNode*)malloc(sizeof(LQNode));
p->data = x;
if(linkqueue->rear == NULL)
{
linkqueue->rear = linkqueue->front = p;
}
else
{
linkqueue->rear->next = p; // use the insert of rear
linkqueue->rear = p;
}
}
/*
* deQueue()
* out of the link queue
**/
int deQueue(LinkQueue *linkqueue,int &x)
{
LQNode *p;
if(linkqueue->rear == NULL) // the linkqueue is empty ,so cant deQueue()
{
return 0;
}
else
{
p = linkqueue->front;
}
if(linkqueue->front == linkqueue->rear) // if the linkqueue only have one LQNode,then it need be processed specially
{
linkqueue->front = linkqueue->rear = NULL;
}
else
{
linkqueue->front = linkqueue->front->next;
}
x = p->data;
free(p);
return 1;
};
利用链表存储队列,需要明白的就是,出队和进队操作,因为这两个操作涉及到对链表的删除插入,理解清楚链表相关操作的话,这部分也比较容易理解。
共享栈和双端队列
这边多提一嘴,共享栈和双端队列,都是基于前面所说的栈和队列的改良版。
但个人感觉不太常用,所以大致了解下就好啦!
共享栈
顾名思义:共享栈共享什么呢,当然是共享存储空间,即栈底分别位于存储空间的两端,栈顶在存储空间中,当两栈顶相遇时就发生了上溢。
双端队列
双端队列呢,也很容易理解,即两端都可以进行入队出队操作。这边就不再进行延申啦!感兴趣的同学可以去了解下,它的进队出队操作的实现代码。
常见问题(主要关于栈,经典问题需要掌握)
提问:括号配对问题,利用栈来判断一个表达式中的括号是否合法配对,即不出现多余的括号,也没有配对错误的括号(例如" )("),那么这样一个关于栈的操作的函数怎么去编写呢?
【分析】:
首先,已知有一个表达式需要我们对其进行判别,那么我们根据括号匹配的规则来,可以知道只要一对括号匹配成功就可以将这对括号去除掉,因为他们已经不影响后面的括号配对。这里呢,就是利用栈的特性,先进后出,把表达式中的左括号"(“一个一个提出来,然后将其对后面的括号进行配对操作,如果匹配那么”(“出栈,不配对就接着找下一个”(",最后栈为空则说明括号配对成功,反之则失败。
还是直接上代码比较容易理解:
int match(char exp[],int n) // exp 为存储括号的字符数组 n 为括号数
{
char SeqStack[MaxSize];
int top = -1; // 顺序栈的定义相较来说还是比较容易
for (int i = 0; i < n ; ++i)
{
if(exp[i] == '(') SeqStack[++top] = '('; // 将左括号存入栈中
if(exp[i] == ')')
{
if(top == -1) return 0; // 如果exp中出现 右括号 而栈为空 则说明 匹配失败
else --top; // 不为空 则说明匹配成功一对括号
}
}
if(top == -1) return 1; // 如果到最后 栈为空 则说明 括号都配对成功
else return 0; // 否则 匹配失败
}
提问:后缀表达式问题,编写函数求后缀式的值?
【分析】:
首先,这里先来了解下什么是后缀式。通俗的理解就是运算符位于操作符之后,这也是为了计算机方便处理算术表达式。我们平时常用的表达式时中缀式,当然也有前缀式。
后缀表达式运算规则:从左向右扫描,遇到数字压栈,遇到操作符,弹出栈顶的两个元素,先弹出的元素在右边,后弹出来的在左边,进行计算后,将结果压栈,再往后扫描,直到扫描结束,输出栈顶元素,即为最终结果。
前缀表达式运算规则:从右向左扫描,遇到数字压栈,遇到操作符,弹出栈顶的两个元素,先弹出的元素在左边,后弹出来的在右边,进行计算后,将结果压栈,再往前扫描,直到扫描结束,输出栈顶元素,即为最终结果。
举个例子就容易理解
例如:中缀式:(a+b+c*d)/e
转化成前缀式为 /++ab*cde
转换成后缀式为abcd*++e/
代码:
int Op(int a,char op,int b) // 运算函数 根据运算符来计算a 和 b
{
if(op == '+') return a+b;
if(op == '-') return a-b;
if(op == '*') return a*b;
if(op == '/') // 涉及到除法操作 就需要分类考虑下除数 为零的情况即可
{
if(b == 0)
{
cout << "the Divisor cant be zero!\n";
return 0;
}
else return a/b;
}
}
int cal_suffix_exp(char exp[])
{
int a,b,c;
int SeqStack[MaxSize];
int top = -1;
char op;
for(int i = 0; exp[i] != '\0'; ++i)
{
if(exp[i] >= '0' && exp[i] <= '9') // 这里默认操作数 都是个位
{
SeqStack[++top] = exp[i] - '0'; // 字符型传换成整形的常规操作
}
else
{
op = exp[i]; // 如果不是操作数 那就是运算符
b = SeqStack[top--];
a = SeqStack[top--]; // 这里就是前面说到的 从栈内连续提取两个数
c = Op(a,op,b);
SeqStack[++top] = c; // 将提取出来的两个数 计算后的结果重新存入栈中
}
}
return SeqStack[top]; // 最后栈中只剩栈顶元素 即为最后的结果
}