一、栈的定义
1、栈是限定仅在表尾进行插入和删除操作的线性表。把允许插入和删除的一端称为栈顶,另一端称为栈底。不含任何数据元素的栈称为空栈。栈又称为后进先出的线性表,简称LIFO结构。
2、栈的插入操作叫做进栈,也称压栈、入栈;栈的删除操作叫做出栈,也有的叫做弹栈。
二、进栈出栈变化形式
1、栈对线性表的插入和删除的位置进行了限制,并没有对元素进出的时间进行限制。即在不是所有元素都进栈的情况下,事先进去的元素也可以出栈,只要保证是栈顶元素出栈就可以。
三、栈的顺序存储结构及实现
1、栈的结构定义,如下
#define OK 1 #define ERROR 0 #define TRUE 1 #define FALSE 0 #define MAXSIZE 20 /* 存储空间初始分配量 */ typedef int Status; typedef int SElemType; /* SElemType类型根据实际情况而定,这里假设为int */ /* 顺序栈结构 */ typedef struct { SElemType data[MAXSIZE]; int top; /* 用于栈顶指针 */ }SqStack;
2、栈的不同情况示意图,如下
3、进栈操作:进栈示意图如下
实现代码如下:
/* 插入元素e为新的栈顶元素 */ Status Push(SqStack *S, SElemType e) { if(S->top == MAXSIZE -1) /* 栈满 */ { return ERROR; } S->top++; /* 栈顶指针增加1 */ S->data[S->top] = e; /* 将新插入元素赋值给栈顶空间 */ return OK; }
4、出栈操作:代码如下
/* 若栈不空,则删除S的栈顶元素,用e返回其值,并返回OK;否则返回ERROR */ Status Pop(SqStack *S, SElemType *e) { if(S->top == -1) { return ERROR; } *e = S->data[S->top]; /* 将要删除的栈顶元素赋值给e */ S->top--; /* 栈顶指针减1 */ return OK; }
5、顺序栈的进栈和出栈操作时间复杂度:两者没有涉及到任何循环语句,因此时间复杂度均是O(1)。
四、两栈共享空间(顺序栈)
1、两个相同类型的栈用一个数组来存储它们,数组有两个端点,两个栈有两个栈底,让一个栈的栈底为数组的始端,即下标为0处,另一个栈的栈底为数组的末端,即下标为数组长度n-1处。如下图。
2、两栈共享空间的结构的代码如下:
#define OK 1 #define ERROR 0 #define TRUE 1 #define FALSE 0 #define MAXSIZE 20 /* 存储空间初始分配量 */ typedef int Status; typedef int SElemType; /* SElemType类型根据实际情况而定,这里假设为int */ /* 两栈共享空间结构 */ typedef struct { SElemType data[MAXSIZE]; int top1; /* 栈1栈顶指针 */ int top2; /* 栈2栈顶指针 */ }SqDoubleStack;
3、进栈操作:代码如下
/* 插入元素e为新的栈顶元素 */ Status Push(SqDoubleStack *S, SElemType e, int stackNumber) { if (S->top1+1==S->top2) /* 栈已满,不能再push新元素了 */ { return ERROR; } if (stackNumber == 1) /* 栈1有元素进栈 */ { S->data[++S->top1] = e; /* 若是栈1则先top1+1后给数组元素赋值 */ } else if (stackNumber == 2) /* 栈2有元素进栈 */ { S->data[--S->top2] = e; /* 若是栈2则先top2-1后给数组元素赋值 */ } return OK; }
4、出栈操作:代码如下
Status Pop(SqDoubleStack *S,SElemType *e,int stackNumber) { if (stackNumber == 1) { if (S->top1 == -1) { return ERROR; /* 说明栈1已经是空栈 */ } *e = S->data[S->top1--]; /* 将栈1的栈顶元素出栈 */ } else if (stackNumber == 2) { if (S->top2 == MAXSIZE) { return ERROR; /* 说明栈2已经是空栈 */ } *e = S->data[S->top2++]; /* 将栈2的栈顶元素出栈 */ } return OK; }
5、两栈共享空间这样的数据结构通常是用在当两个栈的空间需求有相反关系时。
五、栈的链式存储结构及实现
1、栈的链式存储结构简称链栈。
2、由于单链表有头指针,而栈顶指针也是必须的,所以比较好的做法是把栈顶放在单链表的头部;另外由于已有了栈顶在头部了,所以通常对于链栈来说是不需要头结点的。如下图。
3、对于空栈来说,链表原定义是头指针指向空,那么链栈的空其实就是top=NULL的时候。
4、链栈的结构代码如下:
#define OK 1 #define ERROR 0 #define TRUE 1 #define FALSE 0 #define MAXSIZE 20 /* 存储空间初始分配量 */ typedef int Status; typedef int SElemType; /* SElemType类型根据实际情况而定,这里假设为int */ /* 链栈结构 */ struct StackNode { SElemType data; struct StackNode *next; }; typedef struct StackNode *LinkStackPtr; typedef struct { LinkStackPtr top; int count; }LinkStack;
5、进栈操作:进栈操作示意图如下
实现代码:
/* 插入元素e为新的栈顶元素 */ Status Push(LinkStack *S, SElemType e) { LinkStackPtr lsptr = (LinkStackPtr)malloc(sizeof(struct StackNode)); lsptr->data = e; lsptr->next = S->top; /* 把当前的栈顶元素赋值给新结点的直接后继,见图中① */ S->top = lsptr; /* 将新的结点s赋值给栈顶指针,见图中② */ S->count++; return OK; }
6、出栈操作:删除示意图如下
判断链栈是否为空实现代码:
/* 若栈S为空栈,则返回TRUE,否则返回FALSE */ Status StackEmpty(LinkStack S) { if (S.count == 0) { return TRUE; } else { return FALSE; } }
出栈代码:
/* 若栈不空,则删除S的栈顶元素,用e返回其值,并返回OK;否则返回ERROR */ Status Pop(LinkStack *S, SElemType *e) { LinkStackPtr p; if(StackEmpty(*S)) { return ERROR; } *e = S->top->data; p = S->top; /* 将栈顶结点赋值给p,见图中③ */ S->top=S->top->next; /* 使得栈顶指针下移一位,指向后一结点,见图中④ */ free(p); /* 释放结点p */ S->count--; return OK; }
7、链栈的进栈和出栈时间复杂度:时间复杂度均为O(1)。
六、栈的作用
1、栈的应用——递归:编译器使用栈来实现递归。简答的就是在前行阶段,对于每一层递归,函数的局部变量、参数值以及返回地址都被压入栈中。在退回阶段,位于栈顶的局部变量、参数值和返回地址被弹出,用于返回调用层次中执行代码段其余部分,也就是恢复了调用的状态。
2、栈的应用——四则运算表达式求值
2.1 后缀(逆波兰)表示法定义:把平时所用的标准四则运算表达式“9 + (3 - 1) * 3 + 10 / 2”叫做中缀表达式,因为所有的运算符号都在两数字的中间;而“9 3 1 - 3 * + 10 2 / +”这样的表达式称为后缀表达式,因为所有的符号都是在要运算数字的后面出现。
2.2 后缀表达式计算规则:从左到右遍历表达式的每个数字和符号,遇到是数字就进栈,遇到是符号就将处于栈顶两个数字出栈,进行运算,运算结果进栈,一直到最终获得结果。
2.3 中缀表达式转后缀表达式规则:从左到右遍历中缀表达式的每个数字和符号,若是数字就输出,即成为后缀表达式的一部分;若是符号,则判断其与栈顶符号的优先级,是右括号或优先级不高于栈顶符号则栈顶元素依次出栈并输出,并将当前符号进栈,一直到最终输出后缀表达式为止。
七、队列的定义
1、队列是只允许在一端进行插入操作,而在另一端进行删除操作的线性表。
2、队列是一种先进先出的线性表,简称FIFO。允许插入的一端为队尾,允许删除的一端称为对头。如队列q=(a1,a2,.......,an)的示意图如下:
八、循环队列
1、队列顺序存储:假设队列有n个元素,则顺序存储的队列需要建立一个大于n的数组,并把队列的所有元素存储在数组的前n个单元,数组下标为0的一端即是对头;然后入队操作就是在队尾加一个元素,不需要移动任何元素,所以时间复杂度是O(1);但是出队是在对头,即下标为0的位置,所以后面的所有元素得向前移动,以保证下标为0的位置不为空,时间复杂度为O(n)。
2、在1的情况下,如果不限制队列的元素必须存储在数组的前n个单元这一条件,则出队的性能就会大大增加,即对头不需要一定在下标为0的位置。如下图
3、在2的情况下为了避免当只有一个元素时,对头和队尾重合使处理变麻烦,所以用front指针指向对头元素,rear指针指向队尾元素的下一个位置。当front等于rear时,就是空队列。如下图
4、如下图所示,当入队a5时,rear指针都移动到数组之外去了,且如果接着入队的话,就会产生数组越界的错误,可实际上队列在下标为0和1的地方还是空闲的,把该种现象叫做“假溢出”。
5、循环队列定义:解决假溢出的办法就是后面满了,就再从头开始,也就是头尾想接的循环,把队列的这种头尾想接的顺序存储结构称为循环队列。
6、采用循环队列,如下图,此时font等于rear,是队列满时,但是在没采用循环队列的3的情况下,front等于rear是空队列的情况,此时采取的办法是当front=rear时依然是定义的空队列,而当数组中还有一个空闲空间时就定义是队列满时。如下图左边部分就认为是队列满,而不允许出现下图右边部分的情况。
7、采用6中定义队列满时,上图左边部分是一种满时情况,此时rear比front小,它们相差1,如下图也是一种满时情况,此时rear比front大,它们相差整整一圈。所以如果队列的最大尺寸为QueueSize,那么队列满的条件是(rear + 1) % QueueSize = front。
8、采用6中定义队列满时,如上图当rear大于front,此时队列的长度计算方式是:rear-front。如6中的图的左边部分所示,当rear小于front,此时队列长度分为两段,分别是QueueSize-front和0+rear,即rear-front+QueueSize,当图中rear不是指向1,而是指向0,这里的两段的计算方式也适合,所以通用的计算队列长度公式为:(rear - front + QueueSize) % QueueSize。
9、循环队列的顺序存储结构代码如下:
#define OK 1 #define ERROR 0 #define TRUE 1 #define FALSE 0 #define MAXSIZE 20 /* 存储空间初始分配量 */ typedef int Status; typedef int QElemType; /* QElemType类型根据实际情况而定,这里假设为int */ /* 循环队列的顺序存储结构 */ typedef struct { QElemType data[MAXSIZE]; int front; /* 头指针 */ int rear; /* 尾指针,若队列不空,指向队列尾元素的下一个位置 */ }SqQueue;
10、循环队列的初始化代码:
/* 初始化一个空队列Q */ Status InitQueue(SqQueue *Q) { Q->front = 0; Q->rear = 0; return OK; }
11、循环队列求队列长度代码:
/* 返回Q的元素个数,也就是队列的当前长度 */ int QueueLength(SqQueue Q) { return (Q.rear - Q.front + MAXSIZE) % MAXSIZE; }
12、循环队列的入队列操作:
/* 若队列未满,则插入元素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; }
13、循环队列的出队列操作:
/* 若队列不空,则删除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; }
九、队列的链式存储结构及实现
1、将队列的链式存储结构简称为链队列,为了操作方便,将对头指针指向链队列的头结点,而队尾指针指向终端结点,当front和rear都指向头结点时就是空队列,如下图。
2、链队列的结构代码:
#define OK 1 #define ERROR 0 #define TRUE 1 #define FALSE 0 #define MAXSIZE 20 /* 存储空间初始分配量 */ typedef int Status; typedef int QElemType; /* QElemType类型根据实际情况而定,这里假设为int */ /* 结点结构 */ struct QNode { QElemType data; struct QNode *next; }; typedef struct QNode *QueuePtr; /* 队列的链表结构 */ typedef struct { QueuePtr front, rear; /* 队头、队尾指针 */ }LinkQueue;
3、入队操作:插入示意图如下
实现代码:
/* 插入元素e为Q的新的队尾元素 */ Status EnQueue(LinkQueue *Q, QElemType e) { QueuePtr s = (QueuePtr)malloc(sizeof(struct QNode)); if(!s) /* 存储分配失败 */ { exit(OVERFLOW); } s->data = e; s->next = NULL; Q->rear->next = s; /* 把拥有元素e的新结点s赋值给原队尾结点的后继,见图中① */ Q->rear = s; /* 把当前的s设置为队尾结点,rear指向s,见图中② */ return OK; }
4、出队操作:出队操作时就是头结点的后继结点出队,将头结点的后继改为它后面的结点,若链表除头结点外只剩一个元素时,需要将rear指向头结点,如下图。
实现代码:
/* 若队列不空,删除Q的队头元素,用e返回其值,并返回OK,否则返回ERROR */ Status DeQueue(LinkQueue *Q, QElemType *e) { QueuePtr p; if(Q->front == Q->rear) { return ERROR; } p = Q->front->next; /* 将欲删除的队头结点暂存给p,见图中① */ *e = p->data; /* 将欲删除的队头结点的值赋值给e */ Q->front->next = p->next; /* 将原队头结点的后继p->next赋值给头结点后继,见图中② */ if(Q->rear == p) /* 若队头就是队尾,则删除后将rear指向头结点,见图中③ */ { Q->rear = Q->front; } free(p); return OK; }