1. 栈
栈,又名堆栈,它是一种运算受限的线性表。限定仅在表尾进行插入和删除操作的线性表。这一端被称为栈顶,相对地,把另一端称为栈底。向一个栈插入新元素又称作进栈、入栈或压栈,它是把新元素放到栈顶元素的上面,使之成为新的栈顶元素;从一个栈删除元素又称作出栈或退栈,它是把栈顶元素删除掉,使其相邻的元素成为新的栈顶元素。

1.1 栈的顺序存储结构
#include <stdio.h>
#include <malloc.h>
#define STACK_MAX_SIZE 10
typedef struct CharStack {
int top;
int data[STACK_MAX_SIZE];
} *CharStackPtr;
CharStackPtr charStackInit() {
CharStackPtr resultPtr = (CharStackPtr)malloc(sizeof(CharStack));
resultPtr->top = -1;
return resultPtr;
}
void outputStack(CharStackPtr paraStack) {
for (int i = 0; i <= paraStack->top; i ++) {
printf("%c ", paraStack->data[i]);
}
}
void push(CharStackPtr paraStackPtr, int paraValue) {
if (paraStackPtr->top >= STACK_MAX_SIZE - 1) {
printf("栈已存满");
return;
}
paraStackPtr->top ++;//上移top
paraStackPtr->data[paraStackPtr->top] = paraValue;//元素赋值
}
char pop(CharStackPtr paraStackPtr) {
if (paraStackPtr->top < 0) {
printf("无法删除");
return '\0';
}
paraStackPtr->top --;
return paraStackPtr->data[paraStackPtr->top + 1];
}
void pushPopTest() {
CharStackPtr tempStack = charStackInit();
outputStack(tempStack);
for (char ch = 'a'; ch < 'm'; ch ++) {
printf("添加%c.\r\n", ch);
push(tempStack, ch);
outputStack(tempStack);
}
for (int i = 0; i < 3; i ++) {
ch = pop(tempStack);
printf("删除%c.\r\n", ch);
outputStack(tempStack);
}
}
void main() {
pushPopTest();
}
1.2 栈的链式存储
栈的链式存储结构是通过线性表的链式存储结构实现的,操作起来和单链表形式差不多。链栈的操作与链表类似,入栈和出栈都在链表的表头进行。在定义链栈的结点时,定义一个存放数据的变量data,一个存放后继指针的变量*next,结点定义如下:
定义栈的时候,由于栈是在顶部进行元素的插入或删除操作,所以,需要一个栈顶指针top,因为是链栈,还需要一个计数变量count。链栈的结构定义如下。
#include<stdio.h>
#include<stdlib.h>
#include<time.h>//下方获取随机数的time需要的工具箱
#define INFINITY 99999
typedef struct node//数据节点结构体
{
int val;//数据
struct node* next;//指针
}pnode;
typedef struct seqstack//头结点结构体
{
int size;//记录栈的大小
pnode* top;//指向栈顶元素
}stack;//别名方便使用
#include<stdio.h>
#include<stdlib.h>
#include<time.h>
#define INFINITY 99999
typedef struct node
{
int val;//数据
struct node* next;//指针
}pnode;
typedef struct seqstack
{
int size;//记录栈的大小
pnode* top;//指向栈顶元素
}stack;
stack* initstack()//创建栈
{
stack* istack=(stack*)malloc(sizeof(stack));
if(istack!=NULL)
{
istack->top=NULL;
istack->size=0;
}
return istack;
}
int isempty(stack* istack)//判断栈为空
{
if(istack->top==NULL)
{
return 1;//栈为空
}
return 0;//栈不为空
}
int seqstack_top(stack* istack)//获取栈顶元素
{
if(isempty(istack)==0)//
{
return istack->top->val;
}
return INFINITY;//返回无穷大,不能返回-1,有可能栈的顶端元素就是-1
}
int seqstack_pop(stack* istack)//弹出栈顶元素
{
if(isempty(istack)==0)//栈不为空
{
int account=istack->top->val;
istack->top=istack->top->next;
return account;
}
return INFINITY;
}
void seqstack_push(stack* istack,int x)//压栈(入栈)
{
pnode* temp;
temp=(pnode*)malloc(sizeof(pnode));
temp->val=x;
temp->next=istack->top;
istack->top=temp;
istack->size++;
return;
}
void seqstack_destory(stack* istack)//销毁栈
{
if(isempty(istack)==0)
{
free(istack);
}
}
void seqstack_print(stack* istack)
{
pnode* temp=istack->top;
for(int i=1;i<=istack->size;i++)
{
printf("%d ",temp->val);
temp=temp->next;
if(i%5==0)
{
printf("\n");
}
}
printf("\n");
}
int main()
{
srand((unsigned)time(0));//以时间为种子获得随机数
stack* istack=initstack();
printf("请输入初始栈的容量\n");
int m;
scanf("%d",&m);
for(int i=0;i<m;i++)//将随机数压栈
{
seqstack_push(istack,rand()%1000);//取余1000是因为我想把获取的随机数控制在1000以内
}
//获取栈顶元素
printf("栈顶元素:%d\n",seqstack_top(istack));
printf("栈中的元素\n");
seqstack_print(istack);
printf("\n请输入压栈的数据\n");
int n;
scanf("%d",&n);
seqstack_push(istack,n);
printf("输出压栈后的栈中元素\n");
seqstack_print(istack);
printf("弹出栈顶的元素后的栈中元素\n");
seqstack_pop(istack);
seqstack_print(istack);
printf("销毁栈表");
seqstack_destory(istack);
return 0;
}
2.队列
2.1 队列的顺序存储
2.1.1 队列的顺序存储存在的缺陷
假设一个队列有n个元素,则顺序存储的队列需要建立一个大于n的数组,并把队列的所有元素存储在数组的前n个单元,数组下标为0的一端即为对头。
所谓的入队,就是在队尾追加一个元素,不需要移动任何元素,所以时间复杂度为O(1).
队列的出队是在对头,即下标为0的位置,也就意味着,队列中的所有位置都得向前移动,以保证下标为0的位置,即对头不为空。此时时间复杂度为O(n)。
可是有时候想想,为什么出队列时一定要全部移动呢?如果不限制队列的元素一定要存储在数组的前n个单元,出队的性能就会大大增加。也就是说,队头不需要一定在下标为0的位置。

但如果不全部移动的话,又会引入新的问题?那就是数组的 “假溢出”
为了避免当只有一个元素时,对头和队尾重合使得处理变得麻烦,所以引入两个指针,front指针指向对头元素,rear指针指向队尾元素的下一个元素。这样当front等于rear时,不是队列中有一个元素,而是表示空队列。
假设数组的长度为5,空队列及初始状态如左图所示,front与rear指针都指向下标为0的位置。当队列中有4个元素时,front指针不变,rear指针指向下标为4的位置。
此时出队两个元素,则front指针指向下标为2的位置,rear不变。再入队一个元素,front指针不变,此时rear指针移动到数组之外,这就产生了 “假溢出” 现象。

为了解决这种假溢出的现象。我们引入了循环队列
2、循环队列的定义
我们把队列的头尾相接的顺序存储结构称之为循环队列。
为了解决假溢出的现象,就是当队列满了,我们再从头开始存数据,这是时候头尾已经相连了,就形成了循环队列。
继续刚才的例子,将rear指针指向下标为0的位置,就不会导致rear指针指向不明的问题。

接着入队两个元素,会发现rear指针与front重合了

此时问题又来了,刚才说了,当rear等于front时,表示是空队列,现在当队列满时,rear也等于front。那么如何判断队列到底是空的还是满的了?
我们有两个判断方法:
办法一是设置标志变量flag,当front = rear ,且flag =0 为队列空,当front =rear ,且flag =1 时队列满。
办法二是当队列为空时,条件是front = rear ,当队列满时,我们修改其条件,保留一个元素的空间,也是就是说,当队列满时,数组里面还有一个空闲的单元。
例如下图就表示了队列已经满了

就不能再插入元素了。
由于队列是循环队列,rear可能比front大,也可能比front小,所以假设队列的最大尺寸为QueueSize, 队列满的判断条件改为(rear + 1)%QueueSize = front. 队列的长度为(rear - front + QueueSize)% QueueSize.
循环队列的引入解决了数据移动的时间损耗,使得本来插入和删除是O(n)的时间复杂度变成了O(1).
typedef struct{
int data[MAXSIZE]; //容量
int front; //前驱下标
int rear; //后继下标
}SqQueue;
/*初始化队列*/
void InitQueue(SqQueue* Q){
Q->front=0;
Q->real=0; //前驱和后继都指向0
}
/*入队操作*/
int EnQueue(SqQueue* Q,int e){
if(FullQueue(*Q))
return 0;
Q->data[Q->rear]=e;
Q->rear = (Q->rear+1)%MAXSIZE; //将rear指针向后移一位
/*若到最后则转到数组头部*/
}
/*出队操作*/
int DeQueue(SqQueue* Q){
int e;
if(Q->front ==Q->rear){
return 0;
}
e = Q->data[Q->front];
Q->front = (Q->front+1)%MAXSIZE;
return e;
}
2.2 队列的链式存储
// 链式队列结点
typedef struct LinkNode
{
int data;
struct linkNode *next;
}LinkNode;
// 链式队列
typedef struct
{
//队列的队头和队尾指针
LinkNode *front,*rear;
}LinkQueue;
//初始化队列(带头结点)
void InitQueue(LinkQueue &Q)
{
Q.front=Q.rear=(LinkNode*)malloc(sizeof(LinkNode));
Q.front->next = NULL;
}
//初始化队列(不带头结点)
void InitQueue(LinkQueue &Q)
{
// 初始时 front、rear 都指向NULL
Q.front = NULL;
Q.rear = NULL;
}
入队

//入队(带头结点)
void EnQueue(LinkQueue &Q,int x)
{
LinkNode *s = (LinkNode*)malloc(sizeof(LinkNode));
s->data = x;
s->next = NULL;
Q.rear->next = s; //新结点插入到rear之后
Q.rear = s; // 修改表尾指针
}
//入队 (不带头节点)
void EnQueue(LinkQueue &Q,int x)
{
LinkNode *s = (LinkNode*)malloc(sizeof(LinkNode));
s->data = x;
s->next = NULL;
//空队列中插入第一个元素
if(Q.front == NULL){
Q.front = s;
Q.rear = s; //第一个元素入队时需要特别处理
}else{
Q.rear->next = s;
Q.rear = s;
}
}
出队
//出队 (带头结点)
bool DeQueue(LinkQueue &Q,int &x)
{
if(Q.front == Q.rear){
return false;
}
LinkNode *p = Q.front->next;
x = p->data;
Q.front->next = p->next;
if(Q.rear == p){ //z最后一个结点出队
Q.rear = Q.front
}
free(p);
return true;
}
//出队(不带头结点)
bool DeQueue(LinkQueue &Q,int &x)
{
if(Q.front==NULL){
return false;
}
LinkNode *p = Q.front;
x = p->data;
Q.front = p->next;
if(Q.rear == p){ //最后一个结点出队
Q.front = NULL;
Q.rear = NULL;
}
free(p);
return true;
}
判空
//判断队列是否为空(带头结点)
bool IsEmpty(LinkQueue Q)
{
if(Q.front == Q.rear){
return true;
}else{
return false;
}
}
//判断队列是否为空(不带头结点)
bool IsEmpty(LinkQueue Q)
{
if(Q.front == NULL){
return true
}else{
return false;
}
}
3.应用
3.1 栈的应用
3.1.1 递归
3.1.2 逆波兰
括号匹配:
bool bracketMatching(char* paraString, int paraLength) {
CharStackPtr tempStack = charStackInit();
push(tempStack, '#');
char tempChar, tempPopedChar;
for (int i = 0; i < paraLength; i++) {
tempChar = paraString
switch (tempChar) {
case '(':
case '[':
case '{':
push(tempStack, tempChar);
break;
case ')':
tempPopedChar = pop(tempStack);
if (tempPopedChar != '(') {
return false;
}
break;
case ']':
tempPopedChar = pop(tempStack);
if (tempPopedChar != '[') {
return false;
}
break;
case '}':
tempPopedChar = pop(tempStack);
if (tempPopedChar != '{') {
return false;
}
break;
default:
break;
}
}
tempPopedChar = pop(tempStack);
if (tempPopedChar != '#') {
return false;
}
return true;
}
中序表达式转换成后序表达式
什么是中缀表达式?
例如a+b,运算符在两个操作数的中间。这是我们从小学开始学习数学就一直使用的表达式形式。
什么是后缀表达式?
例如a b + ,运算符在两个操作数的后面。后缀表达式虽然看起来奇怪,不利于人阅读,但利于计算机处理。转换为后缀表达式的好处是:
1、去除原来表达式中的括号,因为括号只指示运算顺序,不是实际参与计算的元素。
2、使得运算顺序有规律可寻,计算机能编写出代码完成计算。逆波兰算法的核心思想是将普通的中缀表达式转换为后缀表达式。
逆波兰算法的核心步骤就2个:
1、将中缀表达式转换为后缀表达式,例如输入的原始表达式是 3*(5+7) ,转换得到 3 5 7 + *
2、根据后缀表达式,按照特定的计算规则得到最终计算结果中缀表达式转换为后缀表达式:
1、你需要设定一个栈SOP,和一个线性表 L 。SOP用于临时存储运算符和左括号分界符( ,L用于存储后缀表达式。
2、遍历原始表达式中的每一个表达式元素:
(1)如果是操作数,则直接追加到 L中。只有 运算符 或者 分界符( 才可以存放到 栈SOP中
(2)如果是分界符
Ⅰ 如果是左括号 ( , 则 直接压入SOP,等待下一个最近的 右括号 与之配对。
Ⅱ 如果是右括号 ) ,则说明有一对括号已经配对(在表达式输入无误的情况下)。
不将它压栈,丢弃它,然后从SOP中出栈,得到元素e,将e依次追加到L里。一直循环,直到出栈元素e 是 左括号 ( ,同样丢弃他。
(3)如果是运算符(用op1表示)
Ⅰ 如果SOP栈顶元素(用op2表示) 不是运算符,则二者没有可比性,则直接将此运算符op1压栈。 例如栈顶是左括号 ( ,或者栈为空。
Ⅱ 如果SOP栈顶元素(用op2表示) 是运算符 ,则比较op1和 op2的优先级。如果op1 > op2 ,则直接将此运算符op1压栈。
如果不满足op1 > op2,则将op2出栈,并追加到L,再试图将op1压栈,如果如果依然不满足 op1>新的栈顶op2,继续将新的op2弹出追加到L ,直到op1可以压入栈中为止。
也就是说,如果在SOP栈中,有2个相邻的元素都是运算符,则他们必须满足:下层运算符的优先级一定小于上层元素的优先级,才能相邻。
3、最后,如果SOP中还有元素,则依次弹出追加到L后,就得到了后缀表达式。逆波兰式的计算:
1、从左到右扫描表达式,如果当前字符为数字,则入栈。
2、如果当前字符为运算符,则将栈顶两个元素出栈,作相应运算,结果再入栈。
3、最后当表达式扫描完后,栈里的就是计算结果了。下图是逆波兰式的转换和逆波兰式的计算。
1 + 2 * ( 3 + 4 ) - 5


3.1.3 共享栈
利用栈底位置相对不变的特性,可让两个顺序栈共享一个一维数组空间,将两个栈的栈底分别设置在共享空间的两端,两个栈顶向共享空间的中间延伸。
共享栈是为了更有效地利用存储空间,两个栈的空间相互调节,只有在整个存储空间被占满时才发生上溢。其存取数据的时间复杂度均为O(1),所以对存取效率没有什么影响。

栈空条件:栈0空为top0==-1;栈1空为top==MaxSize。
栈满条件:top0==top1-1。
元素x进栈操作:进栈=操作为top0++;data[top0]=x;进栈1操作为top1–;data[top1]=x。
出栈操作:出栈0操作为x=data[top0];top0–;出栈1操作为x=data[top1];top1++。
在上述设置中,data数组表示共享栈的存储空间,top0和top1分别为两个栈的栈顶指针,这样该共享栈通过data、top0和top1来标识,也可以将它们设计为一个结构体类型:
#include<stdio.h>
#define MaxSize 100
typedef int ElemType;
typedef struct
{
ElemType data[MaxSize];
int top0, top1;
}StackType;
void InitStack(StackType& s)
{
s.top0 = -1;
s.top1 = MaxSize;
}
bool EmptyStack(StackType s, int i)
{
if (i == 0) return s.top0 == -1;
else return s.top1 == MaxSize;
}
bool Push(StackType& s, int i, ElemType e)
{
if (i < 0 || i>1)
{
printf("栈号输入不对!");
return false;
}
if (s.top0 + 0 == s.top1) return false; //栈满
else
{
if (i == 0) s.data[++s.top0] = e;
else s.data[--s.top1] = e;
}
return true;
}
bool Pop(StackType& s, int i, ElemType& e)
{
if (i < 0 || i>1)
{
printf("栈号输入不对!");
return false;
}
if (i == 0)
{
if (s.top0 == -1) return false; //栈s1空
else e = s.data[s.top0--];
}
if (i == 1)
{
if (s.top1 == MaxSize) return false; //栈s2空
else e = s.data[s.top1++];
}
return true;
}
void Input(StackType& s)
{
int symbol = 1, stacknum;
ElemType num;
while (symbol)
{
printf("输入0(0号栈)或1(1号栈),或3(退出):\n");
scanf("%d", &num);
if (num == 3) break;
else if (num < 0 || num>1)
{
printf("输入错误!\n");
break;
}
if (num == 0 || num == 1)
{
printf("请输入元素值:\n");
scanf("%d", &stacknum);
if (Push(s, num, stacknum) == 0)
{
printf("栈已满,无法添加!\n");
break;
}
}
}
}
bool DispStack(StackType s, int stacknum)
{
int i;
if (s.top0 == -1 && s.top1 == MaxSize)
{
printf("栈为空\n");
return false;
}
if (stacknum == 0)
{
printf("栈0中的元素为:");
for (i = 0; i <= s.top0; i++)
printf("%d ", s.data[i]);
printf("\n");
printf("栈顶元素为:%d\n", s.data[s.top0]);
}
else if (stacknum == 1)
{
printf("栈1中的元素为:");
for (i = MaxSize - 1; i >= s.top1; i--)
printf("%d ", s.data[i]);
printf("\n");
printf("栈顶元素为:%d\n", s.data[s.top1]);
}
}
int main()
{
StackType s;
InitStack(s);
Input(s);
DispStack(s, 0);
DispStack(s, 1);
printf("共享栈中当前有:%d个元素", MaxSize - (s.top1 - s.top0) + 1);
return 0;
}
5221

被折叠的 条评论
为什么被折叠?



