目录
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)