目录
5.栈
5.1 栈的基本操作
5.2 各种栈
(1).顺序栈
i.普通顺序栈
ii.共享栈
iii.关于销毁
(2).链栈
6.队列
6.1 队列的基本操作
6.2 各种队列
(1).循环队列
i.代码
ii.另外一种写法
(2).链式队列
i.代码
(3).双端队列
7.栈在括号匹配中的应用
i.主要思路
ii.代码
8.栈在表达式求值中的应用
(1).中缀表达式
(2).前缀表达式
(3).后缀表达式
i.中缀转后缀,
ii.后缀的运算(手算)
iii.中缀转前缀
iiii.前缀的运算(手算)
iiiii.中缀转后缀(机算)
9.栈在递归中的应用
10.队列的应用(以后会学到)
5.栈
栈(Stack)是一种只允许在一端进行插入和删除的线性表。
特点:先入栈的元素,后出栈。(LIFO:Lost in,First out)
空栈:没有数据元素的栈。
栈顶:允许进行插入与删除的一端。
栈底:不可进行操作的一端,除非栈底也是栈顶。
出栈序列的种类数:
n 指栈中的元素个数
5.1 栈的基本操作
*
InitStack(&S):初始化栈,创造一个空栈,分配内存。
DestoryStack(&S):销毁栈,释放栈占用的内存空间。
*
Push(&S, x):进栈,若S栈未满,则加入x 并使之成为新栈顶。——————————增
Pop(&S, &x):出栈,若S栈非空,则弹出栈顶元素,并将之返回。--------------------------删
*
GetTop(S, &x):读取栈顶元素,若S栈非空,则用x返回栈顶元素。(不删除元素)----查
*
StackEmpty(S):判断一个栈是否为空。为空,则返回true;反之,返回false.
5.2 各种栈
(1).顺序栈
i.普通顺序栈
顺序储存——数组
#define MaxSize 10 //
//顺序栈
class SqStack //sequence stack
{
public:
int data[MaxSize]; //静态数组存放栈元素
int top; //栈顶指针,储存栈顶的数组下标
};
//初始化
void InitStack(SqStack& S)
{
S.top = -1; //初始化栈顶指针
}
//判断栈是否为空
bool StackEmpty(SqStack S)
{
if (S.top == -1)
return true;
else
return false;
}
//进栈
bool Push(SqStack& S, int x)
{
if (S.top == MaxSize - 1)
return false; //栈已满,报错
S.data[++S.top] = x; //此代码等同于下文两行代码
//S.top = S.top + 1;
//S.data[S.top] = x;
return true;
}
//出栈
bool Pop(SqStack& S, int& x)
{
if (S.top == -1)
return false; //栈为空,报错
x = S.data[S.top--];//与下文两行代码相同
//x = S.data[S.top];
//S.top--; //将栈顶减一,即为删除了
return true;
}
//读取(几乎与出栈相同)
bool GetTop(SqStack& S, int& x)
{
if (S.top == -1)
return false; //栈为空,报错
x = S.data[S.top];
return true;
}
对于top 指针,它还可以指向储存数据的下一位,此时,判断空的依据就是 top == 0,进出栈时++、--的顺序也要发生改变,栈满条件:top == MaxSize。
ii.共享栈
两个栈共享同一片内存(也是顺序储存方式),用于节省内存,一般两个栈分别起始于这片内存的两端,因此,栈满条件就变成了top_0 + 1 == top_1。
#define MaxSize 10
//共享栈
class ShStack
{
public:
int data[MaxSize];
int top_0; //0号栈顶指针
int top_1; //1号
};
//初始化
void InitStack(ShStack& S)
{
S.top_0 = -1;
S.top_1 = MaxSize;
}
//其他操作
iii.关于销毁
销毁主要分为两步:
· 逻辑上的清空
· 内存的回收
第一步,就是对 top 的处理;第二步,因为储存用的是数组,数组在运行结束之后会由系统自动回收,所以第二步并没有具体的步骤。
(2).链栈
用链式储存结构进行储存。
在上一节中(数据结构(其二)--线性表(其一))单链表的建立,其中的头插法就是一种对链栈的处理。
主要函数的代码(可运行)
#include<iostream>
using namespace std;
#define MaxSize 10
//链栈
class LinkNode
{
public:
int data;
LinkNode* next;
};
using LiStack = LinkNode*;
//初始化(带头结点)
void InitLink(LiStack& S)
{
S = new LinkNode();
if (S == nullptr)
cout << "内存分配失败" << endl;
S->data = 0;
S->next = nullptr;
cout << "初始化成功" << endl; //检验代码是否可行
}
//初始化(不带头结点)
void InitLink1(LiStack& S)
{
S = nullptr;
}
//判断空栈(带头结点)
bool Empty(LiStack S)
{
if (S->next == nullptr)
return true;
return false;
}
//判断空栈(不带头结点)
bool Empty1(LiStack S)
{
if (S == nullptr)
return true;
return false;
}
//判断栈满
bool Full(LiStack S)
{
cout << "满不了一点" << endl;
return true;
}
//进栈(带头结点)
bool PushLink(LiStack& S, int x)
{
LinkNode* p = new LinkNode();
if (p == nullptr)
return false;
p->data = x;
p->next = S->next;
S->next = p;
return true;
}
//进栈(不带头结点)
bool PushLink1(LiStack& S, int x)
{
LinkNode* p = new LinkNode();
if (p == nullptr)
return false;
p->data = x;
p->next = S;
S = p;
return true;
}
//出栈(带头结点)
bool PopLink(LiStack& S, int& x)
{
if (S->next == nullptr)
return false; //栈为空
LinkNode* p = S->next;
x = p->data;
S->next = p->next;
delete p;
return true;
}
//出栈(不带头结点)
bool PopLink1(LiStack& S, int& x)
{
if (S->next == nullptr)
return false; //栈为空
LinkNode* p = S;
x = p->data;
S = p->next;
delete p;
return true;
}
//打印(带头结点)
void PrintLNode(LiStack S)
{
LinkNode* p = S->next;
int i = 1;
while (p != nullptr)
{
cout << "第" << i << "个元素:" << p->data << endl;
i++;
p = p->next;
}
cout << "___________________________" << endl;
}
//打印(不带头结点)
void PrintLNode1(LiStack S)
{
LinkNode* p = S;
int i = 1;
while (p != nullptr)
{
cout << "第" << i << "个元素:" << p->data << endl;
i++;
p = p->next;
}
cout << "___________________________" << endl;
}
//获取栈顶元素(带头结点)
int GetTop(LiStack S)
{
int a = -1;
if (S->next == nullptr)
return -999; //指示空栈
a = S->next->data;
return a;
}
//获取栈顶元素(不带头结点)
int GetTop1(LiStack S)
{
int a = -1;
if (S == nullptr)
return -999; //指示空栈
a = S->data;
return a;
}
//测验
void test01()
{
LiStack S;
InitLink(S);
int a; //接收出栈元素
//进栈
PushLink(S, 1);
PushLink(S, 2);
PushLink(S, 3); //逻辑上,这个是栈顶元素
PrintLNode(S);
//出栈
PopLink(S, a);
cout << "出栈元素a:" << a << endl;
PrintLNode(S);
//获取
cout << "获取栈顶:" << GetTop(S) << endl;
}
int main()
{
test01();
system("pause");
return 0;
}
6.队列
队列(Queue)是一种只允许在一端插入、在另一端删除的线性表。
特点:先入队的元素,也会先出队列。(FIFO)
队头:允许进行删除的一端,称为队头。
队尾:允许进行插入的一端,称为队尾。
6.1 队列的基本操作
*
InitQueue(&Q):初始化。
DestroyQueue(&Q):销毁队列,释放内存。
*
EnQueue(&Q, x):入队,若队列未满,则加入x,成为新的队尾。
DeQueue(&Q, &x):出队,若队列非空,则删除队头,并用x返回。
*
GetHead(Q, &x):读取队头元素,将队头元素用x 传递出来。
*
QueueEmpty(Q):判断队列是否为空。
6.2 各种队列
(1).循环队列
数组实现。
队头指针指向 data[0];
队尾指针指向存有数据的下一位,队尾指向的永远是空的。(借此,进行队列是否已满的检测)
之所以,称为循环,是因为在入队操作的逻辑中,使用了取余的思想(将无限的数据域控制在有限的范围中)。
i.代码
#include<iostream>
using namespace std;
#define MaxSize 10
//队列——顺序
class SqQueue
{
public:
int data[MaxSize];
int front, rear; //队头(front)与队尾指针,储存对应下标
//int size; //指示长度,此变量依据实际情况添加,有此变量,则可以充分利用内存,无需再牺牲一个单元内存来指示是否已满
//int tag; //指示最近一次的操作是删除(0)还是插入(1),也是可以充分利用内存,只有删除(tag==0)时,会使队列为空,只有插入(tag==1)时,会使队列已满,借此再根据两个指针的位置关系判别
};
//初始化
void InitQueue(SqQueue& Q)
{
Q.front = Q.rear = 0;
//Q.size = 0; //若添加了size,之后的函数都要发生一定的变化,不过,本身的逻辑并不会发生多大的变化
//Q.tag = 0; //无需与size同时存在
}
//判断空
bool QueueEmpty(SqQueue Q)
{
if (Q.front == Q.rear)
return true;
return false;
}
//判断满
bool QueueFull(SqQueue Q)
{
if ((Q.rear + 1) % MaxSize == Q.front) //由此,会牺牲一个单元内存,该内存中不会存储数据
return true;
return false;
}
//入队
bool EnQueue(SqQueue& Q, int x)
{
if (QueueFull(Q))
return false;
Q.data[Q.rear] = x;
Q.rear = (Q.rear + 1) % MaxSize; //队列指针后移的独特运算方法,太妙了,因为有了模运算,所以其后移也可以做到循环
return true;
}
//出队
bool DeQueue(SqQueue& Q, int& x)
{
if (QueueEmpty(Q))
return false;
x = Q.data[Q.front];
Q.front = (Q.front + 1) % MaxSize; //因为是数组存储,所以不需要额外的释放操作
return true;
}
//获取元素
bool GetHead(SqQueue Q, int& x)
{
if (QueueEmpty(Q))
return false;
x = Q.data[Q.front];
return true;
}
//获取元素个数
int GetLength(SqQueue Q)
{
return((Q.rear + MaxSize - Q.front) % MaxSize); //队列的长度计算公式
}
//打印全队列
bool PrintQueue(SqQueue Q)
{
if (QueueEmpty(Q))
return false;
int i = Q.front;
int cont = 1;
while (i != Q.rear)
{
cout << "第" << cont << "个数是:" << Q.data[i] << endl;
i++;
cont++;
}
cout << "当前元素个数:" << GetLength(Q) << endl;
cout << "-------------------" << endl;
return true;
}
//测验
void test01()
{
SqQueue q;
InitQueue(q);
cout << "当前元素个数:" << GetLength(q) << endl;
//入队
EnQueue(q, 1);
EnQueue(q, 2);
EnQueue(q, 3);
PrintQueue(q);
//出队
int a;
DeQueue(q, a);
cout << "出队元素:" << a << endl;
PrintQueue(q);
}
int main()
{
test01();
system("pause");
return 0;
}
ii.另外一种写法
rear 指向data 的最后一位元素,而非其下一位了。
那么,其队列的各个函数就要发生一些变化。
空的判断标准:(Q.rear + 1) % MaxSize == Q.front。
初始化:rear = MaxSize - 1;即零的前一位,如此方便插入。
插入:先移动rear 指针,再进行数据的赋值。
删除:也是类似。
Q.rear = (Q.rear + 1) % MaxSize;
Q.data[Q.rear] = x;
满的判断方法:
方法一,牺牲一个存储单元,使得front的前一个位置不能存放数据,则当(Q.rear + 2) % MaxSize == Q.front;时,即为满。
方法二,引入一个记录的变量,如size,tag等。
(2).链式队列
i.代码
值得注意:
· 链式队列的类的定义,有两个
#include<iostream>
using namespace std;
class LinkNode
{
public:
int data;
LinkNode* next;
};
//链式队列——记录一个链表只需要保存他的头指针即可
class LinkQueue
{
public:
LinkNode* front, * rear;
};
//初始化(带头结点)
void InitQueue(LinkQueue& Q)
{
Q.front = Q.rear = new LinkNode();
Q.front->next = nullptr;
}
//初始化(不带头结点)
void InitQueue1(LinkQueue& Q)
{
Q.front = nullptr;
Q.rear = nullptr;
}
//判空(带头结点)
bool QueueEmpty(LinkQueue& Q)
{
if (Q.front == Q.rear)
return true;
return false;
}
//判空(不带头结点)
bool QueueEmpty1(LinkQueue& Q)
{
if (Q.front == Q.rear && Q.front == nullptr)
return true;
return false;
}
//入队(带头结点)(只能尾插)
bool EnQueue(LinkQueue& Q, int x)
{
LinkNode* p = new LinkNode();
p->data = x;
p->next = nullptr;
Q.rear->next = p;
Q.rear = p;
return true;
}
//入队(不带头结点)(只能尾插)
bool EnQueue1(LinkQueue& Q, int x)
{
LinkNode* p = new LinkNode();
p->data = x;
p->next = nullptr;
if (Q.front == nullptr) //空队列时,无头结点,直接称为第一个结点
{
Q.front = p;
Q.rear = p;
}
else
{
Q.rear->next = p;
Q.rear = p;
}
return true;
}
//出队(带头结点)
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) //如果队头结点就是队尾结点,那么还需要更新rear 指针
Q.rear = Q.front;
free(p);
return true;
}
//出队(不带头结点)
bool DeQueue1(LinkQueue& Q, int& x)
{
if (Q.front == Q.rear)
return false;
LinkNode* p = Q.front;
x = p->data;
Q.front = p->next;
if (Q.rear == p)
{
Q.front = nullptr;
Q.rear = nullptr;
}
free(p);
return true;
}
//链式队列不会有队满情况
//打印全队(带头结点)
bool PrintLQueue(LinkQueue Q)
{
if (QueueEmpty(Q))
return false;
LinkNode* p = Q.front->next;
int i = 1;
while (1)
{
cout << "第" << i << "个元素:" << p->data << endl;
if (p == Q.rear)
break;
p = p->next;
i++;
}
}
//打印全队(不带头结点)
bool PrintLQueue1(LinkQueue Q)
{
if (QueueEmpty1(Q))
return false;
LinkNode* p = Q.front;
int i = 1;
while (1)
{
cout << "第" << i << "个元素:" << p->data << endl;
if (p == Q.rear)
break;
p = p->next;
i++;
}
}
void test04()
{
LinkQueue Q;
InitQueue1(Q);
//入队
EnQueue1(Q, 1);
EnQueue1(Q, 22);
EnQueue1(Q, 333);
PrintLQueue1(Q);
//出队
int a = 0;
DeQueue1(Q, a);
cout << "出队元素:" << a << endl;
PrintLQueue1(Q);
}
int main()
{
test04();
system("pause");
return 0;
}
(3).双端队列
只允许在从线性表的两端进行插入和删除。
双端队列也会由此衍生一些变种,如下
以上类型队列,在考研中,常考,给定输入的元素顺序,求合法的出队序列
一般,首先,有出队的第一个元素判断队列中已有的元素,再根据具体队列的插入删除性质来判断是否合法。
7.栈在括号匹配中的应用
用以处理复杂式子中的各种括号的配对{ [ ( ) ] }
i.主要思路
遇到左括号就入栈,遇到右括号就弹出栈顶,并与之进行匹配,成功则继续,失败则直接结束。
ii.代码
#include<iostream>
using namespace std;
#define MaxSize 10 //
//顺序栈
class SqStack //sequence stack
{
public:
char data[MaxSize]; //静态数组存放栈元素
int top; //栈顶指针,储存栈顶的数组下标
};
//初始化
void InitStack(SqStack& S)
{
S.top = -1; //初始化栈顶指针
}
//判断栈是否为空
bool StackEmpty(SqStack S)
{
if (S.top == -1)
return true;
else
return false;
}
//进栈
bool Push(SqStack& S, char x)
{
if (S.top == MaxSize - 1)
return false; //栈已满,报错
S.data[++S.top] = x; //此代码等同于下文两行代码
//S.top = S.top + 1;
//S.data[S.top] = x;
return true;
}
//出栈
bool Pop(SqStack& S, char& x)
{
if (S.top == -1)
return false; //栈为空,报错
x = S.data[S.top--];//与下文两行代码相同
//x = S.data[S.top];
//S.top--; //将栈顶减一,即为删除了
return true;
}
//括号匹配
bool bracketCheck(char str[], int length) //length是传入数组的长度
{
SqStack S;
InitStack(S);
for (int i = 0; i < length; i++)
{
if (str[i] == '(' || str[i] == '[' || str[i] == '{')
{
Push(S, str[i]); //将扫描的左半边括号存入栈中
}
else
{
if (StackEmpty(S)) //如果是空栈,则匹配失败
return false;
char topElem; //接收栈顶元素
Pop(S, topElem);
if (str[i] == ')' && topElem != '(')
return false;
if (str[i] == '[' && topElem != ']')
return false;
if (str[i] == '{' && topElem != '}')
return false;
}
}
return StackEmpty(S); //最终,栈为空则说明匹配成功
8.栈在表达式求值中的应用
(1).中缀表达式
日常最常见的(最习惯的)就是中缀表达式,如下
(1+(3+4)*8)-1
意为,加减乘除在式子之中。
(2).前缀表达式
前缀表达式,就是加减乘除排列在前面,如
+ a b
(3).后缀表达式
后缀表达式(逆波兰表达式)(此表达式应用更加广泛),即加减乘除在数字之后排列,如
a b + 就是 a+b,a b的配列顺序不可发生改变
i.中缀转后缀,
a + b - c => a b + c -
每一个子式,只包括两个数和一个运算符,子式还可以与其他数字、运算符组合构成新的子式,
其中a b +,就可以看作一个子式,其与c -构成了又一个式子
*
对于复杂的式子,如A + B - ( C * D + E) + F
遵循左优先原则,先转换A + B,再向右依次转换,得到A B + C D * E + - F +
ii.后缀的运算(手算)
如上文中的A B + C D * E + - F +,从左往右找,到第一个运算符,则弹出此处之前的两个数(A B),进行对应的运算,再将所得的数存入其中,继续从左往右遍历,以此类推,得出最终结果。
iii.中缀转前缀
遵循右优先原则,如
A + B * (C - D) - E / F
按照原则,可转换为
+ A - * B - C D / E F
实际的转换结果可有很多形式,这些转换的原则,只是为了统一格式。
iiii.前缀的运算(手算)
与后缀类似,只要从右往左遍历,其他雷同。(注意加减乘除的操作数顺序)
iiiii.中缀转后缀(机算)
机算的基本原则(只有运算符、界限符会入栈、出栈,操作数会直接加入后缀表达式):
· 遇到操作数,直接加入后缀表达式
· 遇到界限符,遇到“(” 入栈,遇到 “)”则依次弹出栈内运算符,并加入后缀表达式,直到弹出“)”为止,在没遇到“)”之前,按正常的逻辑来,该弹的弹,该入的入,但是,所谓的逻辑只是在括号内的,也就是说,在“(”之前入栈的不遵循括号内的逻辑,如,括号外有个*,括号内遇到了-,此时并不会把括号外的那个* 弹出。
· 遇到运算符,依次弹出栈中优先级高于或等于当前运算符的所有运算符,并加入后缀表达式,
· 处理完所有字符之后,将栈中剩余的运算符依次弹出,若栈空则无需此操作
9.栈在递归中的应用
在递归函数的调用中,其底层逻辑就是栈。
10.队列的应用(以后会学到)
· 树的层次遍历
· 图的广度优先遍历
· 在操作系统的应用——多进程调用系统资源时,系统对这些进程的处理原则就是队列的思想,先来先服务(FCFS)