栈与队列
栈的定义
- 栈:栈是限定仅在表尾进行插入和删除操作的线性表。
我们把允许插入和删除的一端称为 栈顶,另一端称为 栈底,不含任何数据元素的栈称为 空栈。栈又称为后进先出的 线性表,简称 LIFO 结构。
栈的特殊之处在于它限制了这个线性表的插入和删除位置,它始终只在栈顶进行。
-
栈的插入操作,叫作进栈,也称压栈、入栈。进栈
-
栈的删除操作,叫作出栈,也有的叫作弹栈。出栈
栈的应用
栈的引入简化了程序设计的问题,划分了不同关注层次,使得思考范围缩小,更加聚焦于我们要解决的问题核心。反之,像数组等,因为要分散精力去考虑数组的下标增减等细节问题,反而掩盖了问题的本质。
可以直接使用 Stack 的 push 和 pop 方法,非常方便。
constexpr auto MAXSIZE = 20;
typedef int T; //这里的T依照代码的实际情况而定
栈的顺序存储结构
class orderstarck //普通栈存取
{
public:
/*
* 栈的结构定义
*/
typedef struct {
T data[MAXSIZE];
int top; //用于栈顶指针
}SqStarck;
/*
* 入栈操作
* 插入元素e为新的栈顶元素
*/
bool Push(SqStarck* s, T e) {
if (s->top == MAXSIZE - 1)
return false;
s->top++; //栈顶指针增加1
s->data[s->top] = e; //将新插入元素赋值给栈顶空间
return true;
}
/*
* 出栈操作
*
*/
bool Pop(SqStarck* s, T* e) {
if (s->top == -1)
return false;
*e = s->data[s->top]; //将要删除的栈顶元素赋值给e
s->top--; //栈顶指针减一
return true;
}
private:
};
两栈共享空间
class towstarck //两栈共享空间
{
/*
* top1与top2在进出栈时,加加减减不同
* 进栈是先变为在赋值,出栈是先赋值在变位
*/
public:
/*
* 两栈共享空间
*/
typedef struct {
T data[MAXSIZE];
int top1; //栈1栈顶指针
int top2; //栈2栈顶指针
}SqDoubleStack;
//插入新的栈顶元素
bool Push(SqDoubleStack* s, T e, int number) {
if (s->top1 + 1 == s->top2) //栈已满,不能在添加新元素
return false;
if (number == 1) //栈1有元素进栈
s->data[++s->top1] = e; //若栈1则先top1+1后给数组元素赋值
else if (number == 2) //栈1有元素进栈
s->data[--s->top2] = e; //若栈2则先top2-1后给数组元素赋值
return true;
}
//若栈不空,删除s的栈顶元素,用e返回其值
bool Pop(SqDoubleStack* s, T* e, int number) {
if (number == 1)
{
if (s->top1 == -1)
return false; //栈1为空
*e = s->data[s->top1--]; //栈1的栈顶元素出栈
}
else if (number == 2)
{
if (s->top2 == -1)
return false; //栈2为空
*e = s->data[s->top2++]; //将栈2的栈顶元素出栈
}
return true;
}
private:
};
链式存储结构
class ChainStack
{
public:
typedef struct Node {
T data;
struct Node* next;
}Node,*LinkStackChain;
typedef struct LinkStack {
LinkStackChain top;
int count;
};
bool Push(LinkStack *s,T e) {
LinkStackChain l = (LinkStackChain)malloc(sizeof(Node));
/*
* if(l)这结构避免了一个问题: malloc 如果可用内存不足,对的调用可能会返回 null
* 代码取消引用可能的 null 指针。 如果该指针的值无效,则结果是未定义的。 若要解决此问题,请在使用之前验证指针。
* if(l)即验证指针
*/
if (l)
{
l->data = e;
l->next = s->top; //当前栈顶元素赋值给新结点的直接后继
s->top = l; //将新的结点l赋值给栈顶指针
s->count++;
}
return true;
}
bool Pop(LinkStack *s,T *e) {
LinkStackChain p;
if (false) //判断栈是否为空,这里StackEmpty还需要自己定义
return false;
*e = s->top->data;
p = s->top; //栈顶赋值给结点
s->top = s->top->next; //栈顶指针下移一位
free(p); //释放结点p
s->count--;
return true;
}
private:
};
栈的应用——递归
在高级语言中,调用自己和其它函数没有本质的不同。我们把一个直接用自己或通过一系列的调用语句间接地调用自己的函数,称作递归函数。
每个递归函数必须至少有一个条件,满足时递归不再执行,即不再引用自身而是返回值退出。
递归和迭代的区别是:
- 迭代使用的是循环结构,递归使用的是选择结构。
- 递归能使程序的结构更清晰、更简洁、更容易让人理解,从而减少读懂代码的时间。但是大量的递归调用会建立函数的副本,会耗费大量的时间和内存。
- 迭代则不需要反复调用函数和占用额外的内存。因此我们应该视不同情况选择不同的代码实现方式。
在前行阶段,对于每一层递归,函数的局部变量、参数值以及返回地址都被压入栈中。在退回阶段,位于栈顶的局部变量、参数值和返回地址被弹出,用于返回调用层次中执行代码的其余部分,也就是恢复了调用的状态。
int &FibonacciSequence(int *a) {
for (int i = 1; i < MAXSIZE; i++)
{
if (i==1)
a[i] = a[0] + 1;
else
a[i] = a[i - 1] + a[i - 2];
//cout << a[i]<<endl;
}
return *a;
}
int Fbi(int i) {
if (i < 2)
return i == 0 ? 0 : 1;
return Fbi(i - 1) + Fbi(i - 2);
}
int main(){
//* 递归 *斐波那契数列实现
int a[MAXSIZE] = {0};
FibonacciSequence(a);
for (int i = 0; i < MAXSIZE; i++)
cout << Fbi(i)<<"\t"<< a[i] << endl;
}
四运算表达式(后续补上)
队列定义
- 队列:队列是只允许在一端进行插入操作、而在另一端进行删除操作的线性表。
**队列是一种先进先出的线性表,简称 FIFO。允许插入的一端称为队尾,允许删除的一端称为队头。**假设队列是 q = (a1, a2, ……, an),那么 a1 就是队头元素,而 an 就是队尾元素。我们在删除时,总是从 a1 开始;插入时,列在最后。
队列同样是线性表,队列也有类似线性表的各种操作,不同的就是插入数据只能在队尾进行,删除数据只能在队头进行。
循环队列
解决假溢出的办法就是后面满了,就再从头开始,也就是头尾相接的循环。我们把队列的这种头尾相接的顺序存储结构称为循环队列。
队列的链式存储结构及实现
队列的链式存储结构,其实就是线性表的单链表,只不过它只能尾进头出而已,我们把它简称为链队列。
typedef struct {
T data[MAXSIZE];
int front;
int rear;
}CirQueue;
//初始化一个空队列
bool emptyQueue(CirQueue *Q) {
Q->front = 0;
Q->rear = 0;
return true;
}
//返回Q的元素个数,即队列当前长度
int QueueLength(CirQueue Q) {
return (Q.rear - Q.front + MAXSIZE) %MAXSIZE;
}
队列的链式存储结构——入队操作
- 入队操作时,其实就是在链表尾部插入结点
///队列未满时,插入元素e为Q新的队尾元素
bool EnQueue(CirQueue *Q,T e) {
if ((Q->rear + 1) % MAXSIZE == Q->front) //队满判断
return false;
Q->data[Q->rear] = e; //赋值给队尾
Q->rear = (Q->rear + 1) % MAXSIZE; //在最后则转到数组头部
return true;
}
队列的链式存储结构——出队操作
- 出队操作时,就是头结点的后继结点出队,将头结点的后继改为它后面的结点,若链表除头结点外只剩一个元素时,则需将 rear 指向头结点。
/// 若队列不空,删除Q中队头元素
bool DeQueue(CirQueue* Q, T *e) {
if (Q->rear == Q->front) //队空判断
return false;
*e = Q->data[Q->front]; //队头元素赋值给e
Q->front = (Q->front + 1) % MAXSIZE; //front指针向后移一位置,若最后则转移到数组头部
return true;
}
循环队列与链队列的对比
对比:时间上,基本操作都是常数时间,都为 O(1) 的,不过循环队列是事先申请好空间,使用期间不释放,而对于链队列,每次申请和释放结点也会存在一些时间开销,如果入队出队频繁,则两者还是有细微差异。空间上,循环队列必须有一个固定的长度,所以就有了存储元素个数和空间浪费的问题。而链队列不存在这个问题,尽管它需要一个指针域,会产生一些空间上的开销,但也可以接收。所以在空间上,链队列更加灵活。
class ChainQueue{public: typedef int T; typedef struct Qnode{ //结点结构 T data; struct Qnode* next; }Qnode,*QueuePtr; typedef struct { //队列的链表结构 QueuePtr front, rear; //对头,队尾指针 }LinkQueue; //入队 bool EnQueue(LinkQueue *Q,T e) { QueuePtr s = (QueuePtr)malloc(sizeof(Qnode)); if (!s) //存储分配失败 exit(OVERFLOW); s->data = e; s->next = NULL; Q->rear->next = s; //把拥有元素e新节点s赋值给队尾结点的后继 Q->rear = s; //当前s设置为队尾结点,rear指向s return true; } //出队 bool DeQueue(LinkQueue* Q, T *e) { QueuePtr s ; if (Q->front == Q->rear) return false; s = Q->front->next; //将要删除的队头结点暂存给p *e = s->data; //将要删除的队头结点赋值给e Q->front->next = s->next;//将原队投结点后继s->next赋值给头节点后继 if (Q->rear == s) //若队头是队尾,则删去后将rear指向头节点 Q->rear = Q->front; free(s); return true; }private:};
总的来说,在可以确定队列长度最大值的情况下,建议用循环队列,如果你无法预估队列的长度时,则用链队列。