《大话数据结构》三

第4章 栈与队列

栈是限定仅在表尾进行插入和删除操作的线性表。
队列是只允许在一端进行插入操作、而在另一端进行删除操作的线性表。

栈的定义

栈(stack)是限定仅在表尾进行插入和删除操作的线性表。

我们把允许插入和删除的一端称为栈顶(top),另一端称为栈底(bottom),不含任何数据元素的栈称为空栈。栈又称为后进先出(Last In First Out)的线性表,简称 LIFO 结构。

栈的插入操作,叫做进栈,也称压栈、入栈。
栈的删除操作,叫做出栈,也有的叫弹栈。

进栈出栈的变化形式

元素数量多个,出栈次序会有很多种可能。

栈的抽象数据类型

1
2
3
4
5
6
7
8
9
10
11
12
13
ADT 栈
Data
同线性表。元素具有相同的类型,相邻元素具有前驱和后继关系。
Operation
InitStack(*S):初始化操作,建立一个空栈S。
DestroyStack(*S):若栈存在,则销毁它。
ClearStack(*S):将栈清空。
StackEmpty(S):若栈为空,返回 true ,否则返回 false
GetTop(S,*e):若栈存在且非空,用e返回S的栈顶元素。
Push(*S,e):若栈S存在,插入新元素e到栈S中并成为栈顶元素。
Pop(*S,e):删除栈S中的栈顶元素,并用e返回其值。
StackLength(S):返回栈S的元素个数。
endADT

栈的顺序存储结构及实现

栈的顺序存储结构

栈的结构定义

1
2
3
4
5
6
typedef init SElemType;
typedef struct
{
SElemType data[MAXSIZE]
int top; /* 用于栈顶指针 */
}SqStack;

栈的顺序存储结构——进栈操作

进栈操作 push,其代码如下:

1
2
3
4
5
6
7
8
9
10
Status Push (SqStack *S, SElemType e)
{
if (S->top == MAXSIZE - 1) /* 栈满 */
{
return ERROR;
}
S->top++; /* 栈顶指针增加一 */
S->data[S->top]=e; /* 将新插入元素赋值给栈顶空间 */
return OK;
}

栈的顺序存储结构——出栈操作

出栈操作 pop,其代码如下:

1
2
3
4
5
6
7
8
Status Pop (SqStack *S, SElemType *e)
{
if(S->top==-1)
return ERROR;
*e=S->data[S->top]; /* 将要删除的栈顶元素赋值给e */
S->top--; /* 栈顶指针减一 */
return OK;
}

进栈和出栈没有涉及到任何循环语句,因此时间复杂度均是 O(1)。

两栈共享空间

使用这样的数据结构,通常都是当两个栈的空间需求有相反关系时,也就是一个栈增长时另一个栈在缩短的情况。

栈的链式存储结构及实现

栈的链式存储结构

栈的链式存储结构,简称为链栈。
链栈的结构代码如下:

1
2
3
4
5
6
7
8
9
10
11
typedef struct StackNode
{
SElemType data;
struct StackNode *next;
}StackNode, *LinkStackPtr;

typedef struct LinkStack
{
LinkStackPtr top;
int count;
}

栈的链式存储结构——进栈操作

1
2
3
4
5
6
7
8
9
10
/* 插入元素 e 为新的栈顶元素 */
Status Push (LinkStack *S, SElemType e)
{
LinkStackPtr s = (LinkStackPtr)malloc(sizeof(StackNode));
s->data = e;
s->next = s->top; /* 把当前的栈顶元素赋值给新结点的直接后继 */
S->top = s; /* 将新的结点s赋值给栈顶指针 */
S->count++;
return OK;
}

栈的链式存储结构——出栈操作

假设变量 p 用来存储要删除的栈顶结点,将栈顶指针下移一位,最后释放 p 即可。

1
2
3
4
5
6
7
8
9
10
11
12
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;
}

链栈的进栈 push 和出栈 pop 操作没有任何循环操作,时间复杂度均为O(1)。
如果栈的使用过程中元素变化不可预料,有时很小,有时非常大,那么最好是用链栈,反之,如果它的变化在可控范围内,建议使用顺序栈会更好一些。

栈的作用

栈的引入简化了程序设计的问题,划分了不同关注层次,使得思考范围缩小,更加聚焦于我们要解决的问题核心。

栈的应用——递归

斐波那切数列实现

1
2
3
4
5
6
7
8
9
10
11
12
13
int Fbi (int i)
{
if (i < 2)
return i == 0 ? 0 : 1;
return Fbi(i-1) + Fbi(i-2);
}
int main()
{
int i;
for (int i = 0;i < 40; i++)
printf("%d ", Fbi(i));
return 0;
}

递归定义

在高级语言中,调用自己和其他函数并没有本质的不同。我们把一个直接调用自己或通过一系列的调用语句间接地调用自己的函数,称做递归函数。
每个递归定义必须至少有一个条件,满足递归不再进行,即不再引用自身而是返回值退出。

栈的应用——四则运算表达式求值

  1. 将中缀表达式转化为后缀表达式(栈用来进出运算的符号)。

  2. 将后缀表达式进行运算得出结果(栈用来进出运算的数字)。

队列的定义

队列(queue)是只允许在一端进行插入操作,而在另一端进行删除操作的线性表。

队列是一种先进先出(First In First Out)的线性表,简称FIFO。允许插入的一端称为队尾,允许删除的一端称为队头。

队列的抽象数据类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
ADT 队列(Queue)

Data
同线性表。元素具有相同的类型,相邻元素具有前缀和后继关系。
Operation
InitQueue(*Q):初始化操作,建立一个空队列Q。
DestroyQueue(*Q):若队列Q存在,则销毁它。
ClearQueue(*Q):将队列Q清空。
QueueEmpty(*Q):若队列Q为空,返回 true,否则返回 false
GetHead(Q,*e):若队列Q存在且非空,用e返回队列Q的队头元素。
EnQueue(*Q,e):若队列Q存在,插入新元素e到队列Q中并成为队尾元素。
DeQueue(*Q,*e):删除队列Q中队头元素,并用e返回其值。
QueueLength(Q):返回队列Q的元素个数。
endADT

循环队列

循环队列定义

我们把队列的这种头尾相接的顺序存储结构称为循环队列。

队列的链式存储结构及实现

队列的链式存储结构,其实就是线性表的单链表,只不过它只能尾进头出而已,我们把它简称为链队列。
链队列的结构为:

1
2
3
4
5
6
7
8
9
10
11
12
typedef int QElemType;

typedef struct QNode /* 节点结构 */
{
QElemType data;
struct QNode *next;
}QNode,*QueuePtr;

typedef struct /* 队列的链表结构 */
{
QueuePtr front,rear; /* 队头、队尾指针 */
}LinkQueue;

队列的链式存储结构——入队操作

1
2
3
4
5
6
7
8
9
10
11
Status EnQueue(LinkQueue *Q, QElemType 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 OK;
}

队列的链式存储结构——出队操作

1
2
3
4
5
6
7
8
9
10
11
12
13
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;
}

在可以确定队列长度最大值的情况下,建议用循环队列,如果你无法预估队列的长度时,则用链队列。

最近的文章

【iOS 开发】优化 iOS 安装包大小

最近优化了一下我负责的两个App ipa 包大小,记录一下优化方法。 App1:优化前:37.8MB 优化后:15.7MB 移除重复图片,多余图片,压缩大的图片,去掉用不到的类。包大小变为35.1MB,减少了2.7MB。 把环信含音视频的 SDK 换成不含音视频的 SDK。包大小变为28MB,减少 …

iOS开发 阅读全文
更早的文章

【iOS 开发】使用环信实现聊天遇到的一些坑

使用环信实现聊天的过程遇到了一些坑,记录一下避免自己和其他人踩坑或者尽快出坑。 问题1 聊天页面环信工具栏向上偏移问题描述:在聊天页面,点击输入框弹起键盘,点击IQKeyboardManager带的完成按钮或者点击空白页面收起键盘,反复操作多次后,会出现页面向上偏移,环信UI的工具栏移动到了页面最上 …

iOS 开发, 环信 阅读全文