这是本人根据王道考研数据结构课程整理的笔记,希望对您有帮助。
3.1 栈
3.1.1 栈的基本概念
栈:栈(stack)是只允许在一端进行插入或删除操作的线性表。
(后进先出, LIFO, Last In First Out)
栈的基本操作
InitStack(&S)
:初始化栈。构造一个空栈
S
S
S,分配内存空间。
DestroyStack(&S)
:销毁栈。销毁栈,并释放栈
S
S
S所占用的内存空间。
Push(&S,x)
:进栈。若栈
S
S
S未满,则将
x
x
x加入成为新栈顶。
PoP(&S,&x)
:出栈。若栈
S
S
S非空,则弹出栈顶元素,并用
x
x
x返回。
GetTop(S,&x)
:读栈顶元素。若栈
S
S
S非空,则用
x
x
x返回栈顶元素。
StackEmpty(S)
:判断一个栈
S
S
S是否为空。若
S
S
S为空,则返回true,否则返回false。
总结:
3.1.2 栈的顺序存储实现
顺序栈的定义
#define MaxSize 10
typedef struct
{
ElemType data[MaxSize];
int top; //栈顶指针(从0开始)
}SqStack;
初始化操作
void InitStack(SqStack &S)
{
S.top = -1;
}
void testStack()
{
SqStack S;
InitStack(S);
//...
}
判断栈空
bool StackEmpty(SqStack S)
{
if(S.top == -1)
return true;
else
return false;
}
进栈操作
bool Push(SqStack &S, ElemType x)
{
if(S.top == MaxSize - 1)
return false;
S.top = S.top + 1;
S.data[S.top] = x;
return true;
}
S.top = S.top + 1; S.data[S.top] = x;
等价于
S.data[++S.top] = x;
出栈操作
bool Pop(SqStack &S, ElemType &x)
{
if(S.top == -1)
return false;
x = S.data[S.top];
S.top = S.top - 1;
return true;
}
x = S.data[S.top]; S.top = S.top - 1;
等价于
x = S.data[S.top--];
读栈顶元素
bool GetTop(SqStack S, ElemType &x)
{
if(S.top == -1)
return false;
x = S.data[S.top];
return true;
}
顺序栈的缺点:栈的大小不可变
共享栈:为了提高内存空间的利用率
#define MaxSize 10
typedef struct
{
ElemType data[MaxSize];
int top0; //0号栈栈顶指针
int top1; //1号栈栈顶指针
}ShStack;
void InitStack(ShStack &S)
{
S.top0 = -1;
S.top1 = MaxSize;
}
总结:
3.1.2 栈的链式存储实现
链栈的定义
typedef struct LinkNode
{
ElemType data;
struct Linknode *next;
} LinkNode, *LinkStack;
(以下部分是自己写的,不保证正确)
链栈的初始化
//【不带头结点】
bool InitLinkStack(LinkStack &S)
{
S = NULL; //空表,暂时还没有任何结点
return true;
}
//【带头结点】
bool InitLinkStack(LinkStack &S)
{
S = (LinkNode *)malloc(sizeof(Linknode));
if (S == NULL)
return false;
S->next = NULL;
return true;
}
链栈的进栈
bool Push(Linknode &S, ElemType x)
{
if(S == NULL) //不带头结点
{
S = (LinkNode *)malloc(sizeof(LinkNode));
S->next == NULL;
}
Linknode r = (LinkNode *)malloc(sizeof(LinkNode));
if(r == NULL) //内存不足
return false;
r->data = x;
r->next = S->next;
S->next = r;
return true;
}
链栈的出栈
bool Pop(LinkStack &S, ElemType &x)
{
if(S == NULL)
return false;
if(S->next == NULL)
return false;
LinkStack r = S;
x = S->next->data;
S->next = S->next->next;
free(r);
return true;
}
获取栈顶元素
bool GetTop(LinkStack S, ElemType &x)
{
if(S == NULL)
return false;
if(S->next == NULL)
return false;
x = S->next->data;
return true;
}
判断栈空
bool StackEmpty(LinkStack S)
{
return (S == NULL || S->next == NULL);
}
判断栈满
(链栈会满吗?黑人问号???)
总结:
3.2 队列
3.2.1 队列的基本概念
队列:队列是只允许在一端进行插入,在另一端删除的线性表。
(先进先出, FIFO, First In First Out)
队列的基本操作
InitQueue(&Q)
:初始化队列。构造一个空队列
Q
Q
Q,分配内存空间。
DestroyQueue(&Q)
:销毁队列。销毁队列,并释放队列
Q
Q
Q所占用的内存空间。
EnQueue(&Q,x)
:入队。若队列
Q
Q
Q未满,将
x
x
x加入成为新队尾。
DeQueue(&Q,&x)
:出队。若队列
Q
Q
Q非空,则删除队头元素,并用
x
x
x返回。
GetHead(Q,&x)
:读队头元素。若队列
Q
Q
Q非空,则用
x
x
x返回队头元素。
QueueEmpty(Q)
:判断一个队列
Q
Q
Q是否为空。若
Q
Q
Q为空,则返回true,否则返回false。
总结:
3.2.2 队列的顺序存储实现
队列的顺序实现
#define MaxSize 10
typedef struct
{
ElemType data[MaxSize];
int front,rear; //队头指针和队尾指针。
} SqQueue;
- 从rear入队,从front出队
- rear指向队尾元素后一个位置
初始化队列
void InitQueue(SqQueue &Q)
{
Q.rear = Q.front = 0;
}
判断队列是否为空
bool QueueEmpty(SqQueue Q)
{
if(Q.rear = Q.front)
return true;
else
return false;
}
入队操作(循环队列)
bool EnQueue(SqQueue &Q, ElemType x)
{
if((Q.rear+1) % MaxSize == Q.front) //判断队满
return false;
Q.data[Q.rear] = x;
Q.rear = (Q.rear + 1) % MaxSize;
return true;
}
这张图是队满的示意图。
这里牺牲一个存储单元,是因为如果全都存储了数据,那么rear和front就会重合,被认为是空队列。
队列元素个数:
(rear + MaxSize - front) % MaxSize
如果不想浪费这个存储单元,可以考虑如下方案:
#define MaxSize 10 typedef struct { ElemType data[MaxSize]; int front,rear; //队头指针和队尾指针。从rear入队,从front出队。 int size; //队列当前长度 } SqQueue;
当插入成功时,size++;当删除成功时,size–。因此可以根据size的大小来判断是否队满。
出队操作(循环队列)
bool DeQueue(SqQueue &Q, ElemType &x)
{
if(Q.rear == Q.front) //判断队空
return false;
x = Q.data[Q.front];
Q.front = (Q.front + 1) % MaxSize;
return true;
}
总结:
3.2.3 队列的链式存储实现
typedef struct LinkNode
{
ElemType data;
struct LinkNode *next;
}LinkNode;
typedef struct
{
LinkNode *front, *rear;
}LinkQueue;
初始化队列
//【带头结点】
void InitQueue(LinkQueue &Q)
{
//初始时front、rear都指向头结点
Q.front = Q.rear = (LinkNode *)malloc(sizeof(LinkNode));
Q.front->next = NULL;
}
//【不带头结点】
void InitQueue(LinkQueue &Q)
{
Q.front = NULL;
Q.rear = NULL;
}
判断队列是否为空
//【带头结点】
bool IsEmpty(LinkQueue Q)
{
if(Q.front == Q.rear)
return true;
else
return false;
}
//【不带头结点】
bool IsEmpty(LinkQueue Q)
{
if(Q.front == NULL) //【or】if(Q.rear == NULL)
return true;
else
return false;
}
入队
//【带头结点】
void EnQueue(LinkQueue &Q, ElemType 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, ElemType 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; //新结点插入到rear之后
Q.rear = s; //修改表尾指针
}
}
出队
//【带头结点】
bool DeQueue(LinkQueue &Q, ElemType &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)
Q.rear = Q.front;
free(p);
return true;
}
//【不带头结点】
bool DeQueue(LinkQueue &Q, ElemType &x)
{
if(Q.front == Q.rear)
return false;
LinkNode *p = Q.front;
x = p->data;
Q.front->next = p->next;
if(Q.rear == p)
Q.front = NULL;
Q.rear = NULL;
free(p);
return true;
}
队列满的条件
一般不会满,除非内存不足。
总结:
3.2.4 双端队列
双端队列:双端队列是只允许从两端插入、两端删除的线性表。
输入受限的双端队列:只允许从一端插入、两端删除的线性表。
输出受限的双端队列:只允许从两端插入、一端删除的线性表。
总结:
3.3 栈和队列的应用
3.3.0 C++中stack类的用法
stack常用操作
stack<int> q; //以int型为例
int x;
q.push(x); //将x压入栈顶
q.top(); //返回栈顶的元素
q.pop(); //删除栈顶的元素
q.size(); //返回栈中元素的个数
q.empty(); //检查栈是否为空,若为空返回true,否则返回false
例子:
#include<iostream>
#include<stack>
using namespace std;
int main()
{
stack<int> q;
q.push(1);
q.push(2);
q.push(3);
q.push(4);
q.push(5);
cout<<"q.size "<<q.size()<<endl;
cout<<"q.top "<<q.top()<<endl; //输出栈顶元素
q.pop(); //删除栈顶元素
cout<<"q.size "<<q.size()<<endl;
cout<<"q.top "<<q.top()<<endl;
return 0;
}
q.size 5
q.top 5
q.size 4
q.top 4
3.3.1 栈在括号匹配中的应用
3.3.2 栈在表达式求值中的应用
中缀表达式 | 后缀表达式 | 前缀表达式 |
---|---|---|
a + b | a b + | + a b |
a + b - c | a b + c - | - + a b c |
a + b - c * d | a b + c d * - | - + a b * c d |
将中缀表达式转为后缀表达式
“左优先”原则:只要左边的运算符能先计算,就优先算左边的。
机算步骤如下:
- 若遇到操作数。直接加入后缀表达式。
- 若遇到界限符。遇到
(
直接入栈;遇到)
则依次弹出栈内的运算符并加入后缀表达式,直到弹出(
为止。注意:(
不加入后缀表达式。 - 若遇到运算符。依次弹出栈中优先级高于或等于当前运算符的所有运算符,并加入后缀表达式,若碰到
(
或栈空则停止。之后再把当前运算符入栈。 - 按上述方法处理完所有字符后,将栈中剩余运算符依次弹出,并加入后缀表达式。
用栈实现后缀表达式的计算
- 从左往右扫描下一个元素,直到处理完所有元素。
- 若扫描到操作数则压入栈,并回到1;否则执行3。
- 若扫描到运算符,则弹出两个栈顶元素,执行相应计算,运算结果压回栈顶,回到1。
(前缀表达式类似,只是从右往左扫描)
用栈实现中缀表达式的计算
- 初始化两个栈,操作数栈和运算符栈
- 若扫描到操作数,压入操作数栈
- 若扫描到运算符或界限符,则按照“中缀转后缀”相同的逻辑压入运算符栈(期间也会弹出运算符,每当弹出一个运算符时,就需要再弹出两个操作数栈的栈顶元素并执行相应运算,运算结果再压回操作数栈)
3.3.3 栈在递归中的应用
函数调用的特点:最后被调用的函数最先执行结束(LIFO)
- 递归调用时,函数调用栈可称为“递归工作栈”
- 每进入一层递归,就将递归调用所需信息压入栈顶
- 每退出一层递归,就从栈顶弹出相应信息
- 缺点:太多层递归可能会导致栈溢出
3.3.4 队列的应用
树的层次遍历
图的广度优先遍历
操作系统
3.4 稀疏矩阵的压缩存储
法1:
法2: