栈
限定在表尾进行插入和删除操作的线性表。把允许插入、删除元素的一端称为栈顶(top),另一端叫做栈底(bottom),不含任何元素的栈叫做空栈,栈又称为后进先出(Last in first out)的线性表,简称为LIFO结构
栈的ADT:
进栈(压栈):添加元素到线性表,栈顶指针top加一。栈顶指针指示当前栈顶元素的下标
出栈(弹栈):删除元素,top减一
两个栈共享空间
用于合并两个相同数据类型的栈,节约空间。使用数组实现两个栈共享空间的结构
数组两端为栈底,有两个栈顶指针,如果栈1满,那么后续进入栈1的元素其实存在栈2,这种结构一般用在两个栈空间上存在相反关系,即栈1增长而栈2缩短,就像买卖股票,有人买就有人卖,总的股票数量不变。如果两个栈都是同时增长,容易溢出。
栈的链式存储结构
简称链栈,其实就是将栈顶替代头结点,并且插入元素时,是将新结点放入栈顶,指向原栈顶结点,删除元素则是弹出栈顶元素,并且将top指向下一个结点。
链栈适合长度不定或变化较大的场景,比顺序栈的给定长度灵活一些,但也增加了内存开销(结点需要存储指针)
栈的作用
- 递归
相信大家都对递归有一定的了解,递归的基本组成是边界条件和分解步骤,边界条件判断递归什么时候停止,分解步骤则将原先的计算步骤分解为更小的部分(一般是调用自己),分解的过程,也就是调用方法的过程,就会生成一个栈,分解的深度越深,生成的栈越多,直到边界条件才会返回,栈又依次弹出,返回结果。
以求斐波那契数为例,常规迭代的代码如下:
void sumNormal(int total)
{
if (total < 2)
{
printf("%s ", "input invalid");
}
int a[total];
a[0] = 1;
printf("%d ", a[0]);
a[1] = 1;
printf("%d ", a[1]);
for (int i = 2; i < total; i++)
{
a[i] = a[i - 1] + a[i - 2];
printf("%d ", a[i]);
}
printf(" end");
}
int main()
{
sumNormal(40);
}
输出如下
1 1 2 3 5 8 13 21 34 55 89 144 233 377 610 987 1597 2584 4181 6765 10946 17711 28657 46368 75025 121393 196418 317811 514229 832040 1346269 2178309 3524578 5702887 9227465 14930352 24157817 39088169 63245986 102334155 end
如果使用递归:
int sumFib(int total)
{
if (total < 2)
{
// 边界条件
return 1;
}
else
{
// 分解
return sumFib(total - 1) + sumFib(total - 2);
}
}
int main()
{
// sumNormal(40);
for (int i = 0; i < 40; i++)
{
printf("%d ", sumFib(i));
}
}
输出相同,可以发现sumFib函数其实就是求前两位元素之和,即数列的下一项。那么代码中每次进行分解步骤,都会生成一个栈,也可以从函数输出时间看出,递归的方式虽然容易理解,但是时间、空间消耗较迭代方式大。当然,一般而言,递归的编程方式是以空间换取时间的,通常是消耗内存大而时间性能优化。
- 四则运算表达式求值
计算机可以对简单算式进行高效计算,那么如何让计算机计算四则运算表达式:
9+(3-1)×3+10÷2呢?这里我们使用栈的数据结构和后缀表达式(也叫逆波兰表达式)实现。
后缀表达式是什么。
常规的四则运算表达式,如上所述的9+(3-1)×3+10÷2叫做中缀表达式,运算符是写在两个数字之间的,而后缀表达式,是把运算对象写在前面,而运算符写在后面的表达方式,如a + b,后缀表达式就是a b +,如(a + b) * c,后缀表达式就是a b + c ,这种表达式不带括号,虽然不符合人的习惯,但结合栈,可以更好地被计算机“理解”。
至于后缀表达式具体是如何通过栈交给计算机计算结果,后面再说,先看看如何将普通的中缀表达式转换为后缀表达式
2.1 中缀表达式转后缀表达式
也使用栈进行生成
规则:输入中缀表达式,从左到右遍历,如果是数字就直接输出,如果是符号,判断其与栈顶元素的优先级,优先级不高于栈顶元素,则栈顶元素出栈并输出,当前符号进栈;如果符号是右括号,出栈直到左括号
具体过程:
输入参数:9+(3-1)×3+10÷2
遍历,元素9,输出:9
下一个元素+,此时空栈,直接进栈:
元素(,左括号直接进栈
元素3,输出9 3
元素-,进栈
元素1,输出:9 3 1
元素),右括号则将栈内元素依次出栈,直到左括号(括号不输出),输出:9 3 1 -
元素,优先级高于栈顶元素,进栈
元素3,输出:9 3 1 - 3
元素+,优先级最低,栈内元素全部输出:9 3 1 - 3 * +,此时空栈,然后再将元素+进栈
元素10,输出:9 3 1 - 3 * + 10
元素÷,优先级高于栈顶元素,进栈:
元素2,输出9 3 1 - 3 * + 10 2
元素遍历完毕,栈内元素出栈,输出:9 3 1 - 3 * + 10 2 / +
用代码表示这个过程:
// 输入中缀表达式,输出逆波兰表达式
void poland_sequence(char *input)
{
// 初始化栈
StackTest stackTest;
stackTest.top = -1;
// 出栈元素
char popEle;
for (int i = 0; i < MAXSIZE; i++)
{
char element = input[i];
if (element >= '0' && element <= '9')
{
// 数字,直接输出
printf("%c ", element);
}
else if ('#' == element)
{
// 到达最后,输出全部栈元素
popAllElement(&stackTest, popEle);
printf("\n output complete!");
}
else
{
//是符号
if (element == '(')
{
// 左括号,进栈
pushStack(&stackTest, element);
}
else if (element == ')')
{
// 出栈输出,直到遇到左括号
do
{
popStack(&stackTest, &popEle);
if (popEle != '(')
{
printf("%c ", popEle);
}
} while ('(' != popEle);
}
else if (element == '+' || element == '-')
{
// 判断栈顶元素,左括号则进栈,否则输出全部元素
getTopElement(&stackTest, &popEle);
if (popEle == '(')
{
// 当前元素进栈
pushStack(&stackTest, element);
}
else
{
popAllElement(&stackTest, popEle);
pushStack(&stackTest, element);
}
}
else
{
// 是乘、除,进栈
pushStack(&stackTest, element);
}
}
}
}
int main()
{
// 输入:(9 + (3 - 1) * 3 + 8 / 2)
char initial_sequence[MAXSIZE] = {'9', '+', '(', '3', '-', '1', ')', '*', '3', '+', '8', '/', '2', '#'};
// 输出:9 3 1 - 3 * + 8 2 / +
poland_sequence(initial_sequence);
return 1;
}
可以跟着代码配合流程计算一遍,主要是符号优先级的比较,左括号优先级最高的,直接进栈,乘除高于加减,另外右括号应该对栈元素依次出栈直到遇到左括号。
2.2 计算后缀表达式的值
这里计算后缀表达式的值也使用栈的数据结构实现,规则是:从左到右遍历,数字则进栈,符号则将栈顶两个元素出栈并计算,将计算结果入栈。
针对上述生成的后缀表达式:9 3 1 - 3 * + 10 2 / + 可以得出以下的运算过程:
数字9,3,1依次进栈
符号-,1出栈作为减数,3作为被减数,3 - 1 = 2,数字2进栈,这里注意顺序。之后数字3进栈:
符号*,3,2出栈计算,2 * 3 = 6,数字6进栈;符号+,6,9出栈计算,6 + 9 = 15,数字15进栈
数字10,2依次进栈;符号/,2,10出栈,这里也是2作为除数,10作为被除数,10 / 2 = 5,数字5进栈
最后是符号+,5,15出栈,5 + 15 = 20,计算完毕
这个过程可以尝试用代码实现,这里不再深究。通过这个例子,相信可以理解到栈的先进后出的特性,以及它在实际应用中的效果
队列
队列,字面意思理解就像是生活中排队时的队伍,新来的人都是在队伍尾部加入队列,而买完东西,先离开队列的总是队列开头的人
队列是一种先进先出的线性表,简称FIFO(First in first out),允许插入的一端叫做队尾,允许删除的一端叫做队头
队列的抽象数据模型:
队列的顺序存储结构
常规线性存储
与线性表类似,只不过新增、插入元素都只能在队尾操作,移除元素只能移除队头,并且后继元素需要全部前移一位:
可以发现这种方式对于移除元素非常不友好,出队列的性能低下
循环队列
为了避免移除元素时,频繁移动元素,怎么做可以在移除元素时不移动后续元素呢?
移动元素时是为了保持队头在下标0的位置,其实可以引入一个指针,指示队头的位置,移除元素时将指针后移一位即可。
这又带来了新的问题,如果再向队列中添加两个元素,那么就会造成存储空间有空闲(下标0的位置),而无法再添加元素的问题,即进行出队列操作后,队头的空闲内存似乎永远也使用不了。为此,再对存储方式进行一点改变,如果后面满了,就再从头开始,如果前面有空闲位置,就在前面进行插入。这样形成了头尾相接的队列,叫做循环队列。
我们使用front指示队头的位置,rear指示队列最后一个元素之后的第一个位置。
入队列时,添加到rear的位置并且rear后移
出队列时,front指示的元素出队列并且front后移
判断队列是否已满,由于空队列、满队列时都有front = rear,因此可以特意保留一个空位置
如上图所示的情况,就把队列看作已满,此时满足(rear+1)%QueueSize==front,另外,求队列的长度就是:(rear-front+QueueSize)%QueueSize
部分代码:
/* 循环队列的顺序存储结构 */
typedef struct
{
QElemType data[MAXSIZE];
int front; /* 头指针 */
int rear; /* 尾指针,若队列不空,指向队列尾元素的下一个位置 */
}SqQueue;
/* 若队列未满,则插入元素e为Q新的队尾元素 */
Status EnQueue(SqQueue *Q,QElemType e)
{
if ((Q->rear+1)%MAXSIZE == Q->front) /* 队列满的判断 */
return ERROR;
Q->data[Q->rear]=e; /* 将元素e赋值给队尾 */
Q->rear=(Q->rear+1)%MAXSIZE;/* rear指针向后移一位置, */
/* 若到最后则转到数组头部 */
return OK;
}
/* 若队列不空,则删除Q中队头元素,用e返回其值 */
Status DeQueue(SqQueue *Q,QElemType *e)
{
if (Q->front == Q->rear) /* 队列空的判断 */
return ERROR;
*e=Q->data[Q->front]; /* 将队头元素赋值给e */
Q->front=(Q->front+1)%MAXSIZE; /* front指针向后移一位置, */
/* 若到最后则转到数组头部 */
return OK;
}
队列的链式存储
对应线性表的单链表,不过只能在尾部添加元素,头部删除元素,简称链队列
以下是链队列的一些方法实现:
#include "stdio.h"
#include "stdlib.h"
#define OK 1
#define ERROR 0
typedef int Status;
// 链式队列结点
typedef struct
{
int data;
struct LinkedQueueNode *next;
} LinkedQueueNode, *NodePointer;
// 链式队列结构定义
typedef struct
{
NodePointer front, rear;
} LinkedQueue;
// 初始化一个空队列
Status initQueue(LinkedQueue *linkedQueue)
{
linkedQueue->front = linkedQueue->rear = (NodePointer)malloc(sizeof(LinkedQueueNode));
// 初始状态,只有一个结点
linkedQueue->front->next = NULL;
return OK;
}
// 入队列
Status enQueue(LinkedQueue *linkedQueue, int data)
{
NodePointer newNode = (NodePointer)malloc(sizeof(LinkedQueueNode));
// 因为要放在队尾,所以其next指向null
newNode->next = NULL;
newNode->data = data;
// 将原队尾指向新的这个结点
linkedQueue->rear->next = newNode;
linkedQueue->rear = newNode;
return OK;
}
// 出队列
Status deQueue(LinkedQueue *linkedQueue, int *data)
{
// 代表此时队列为空
if (linkedQueue->front == linkedQueue->rear)
{
return ERROR;
}
// 拿到第一个结点
NodePointer first = linkedQueue->front->next;
// 获取结点数据
*data = first->data;
// 头结点取代这个结点
linkedQueue->front->next = first->next;
// 如果rear也指向这个结点,那么这个结点出队后队列为空
if (linkedQueue->rear == first)
{
// 更新rear的指向
linkedQueue->rear = linkedQueue->front;
}
// 释放结点
free(first);
return OK;
}
// 查看队列元素,从队头开始遍历
Status printQueue(LinkedQueue *linkedQueue)
{
printf("content of this queue: ");
NodePointer currentNode = linkedQueue->front->next;
while (currentNode)
{
printf("%d ", currentNode->data);
currentNode = currentNode->next;
}
printf("\n");
return OK;
}
int main()
{
LinkedQueue linkedQueue;
// 初始化队列
initQueue(&linkedQueue);
// 数据入队列
enQueue(&linkedQueue, 1);
enQueue(&linkedQueue, 2);
enQueue(&linkedQueue, 3);
printQueue(&linkedQueue);
// 出队列
int deQueueData;
printf("dequeue status: %d \n", deQueue(&linkedQueue, &deQueueData));
printf("dequeue result: %d \n", deQueueData);
printQueue(&linkedQueue);
printf("-------------------\n");
printf("dequeue status: %d", deQueue(&linkedQueue, &deQueueData));
printf("\n");
printf("dequeue result: %d \n", deQueueData);
printQueue(&linkedQueue);
printf("-------------------\n");
printf("dequeue status: %d", deQueue(&linkedQueue, &deQueueData));
printf("\n");
printf("dequeue result: %d \n", deQueueData);
printQueue(&linkedQueue);
printf("-------------------\n");
printf("dequeue status: %d", deQueue(&linkedQueue, &deQueueData));
printf("\n");
printf("dequeue result: %d \n", deQueueData);
printQueue(&linkedQueue);
return OK;
}
输出结果:
content of this queue: 1 2 3
dequeue status: 1
dequeue result: 1
content of this queue: 2 3
-------------------
dequeue status: 1
dequeue result: 2
content of this queue: 3
-------------------
dequeue status: 1
dequeue result: 3
content of this queue:
-------------------
dequeue status: 0
dequeue result: 3
content of this queue: