数据结构——第三章《栈、队列和数组》

第三章栈、队列和数组

3.1栈

3.1.1栈的基本概念
1,定义

线性表是具有相同数据类型的n(n20)个数据元素的有限序列,其中n为表长,当n=0时线性表是一个空表。

栈(Stack)是只允许在一端进行插入或删除操作的线性表。

术语
  • 栈顶:

允许插入和删除的一端

  • 栈底:

不允许插入和删除的一端

  • 空栈:

特性:后进先出(LIFO)

2,基本操作

InitList(&L):初始化表。构造一个空的线性表L,分配内存空间。
DestroyList(&L):销毁操作。销毁线性表,并释放线性表L所占用的内存空间

Listlnsert(&L,i,e):插入操作。在表L中的第i个位置上插入指定元素e。
ListDelete(&L,i&e):删除操作。删除表L中第1个位置的元素,并用e返回删除元素的值。

LocateElem(L.e):按值查找操作。在表L中查找具有给定关键字值的元素。
GetElem(L,i):按位查找操作。获取表L中第i个位置的元素的值。

其他常用操作:
Length(L):求表长。返回线性表L的长度,即L中数据元素的个数。
PrintList(L):输出操作。按前后顺序输出线性表L的所有元素值。
Empty(L):判空操作。若L为空表,则返回true,否则返回false。

3.1.2栈的顺序存储实现(top=-1,指向当前元素)

顺序存储,用静态数组实现,并需要记录栈顶指针。

//顺序栈的定义
#define MaxSize 10
typedef struct
{
    ElmType data[MaxSize];//静态数组存放栈中元素
    int top;//栈顶指针,指向此时栈顶元素的位置
}SqStack;
1,基本操作
1,创(初始化)
void InitStack(SqStack &S)
{
    S.top = -1;//初始化栈顶指针
}
2,增(进栈)

先判断栈是否满了

//新元素入栈
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;
}
3,删(出栈)

数据还残留在内存中,只是逻辑上被删除了

//出栈操作
bool Pop(SqStack &S, ElemType &x)
{
    if(S.top == -1)\\栈空,报错
        return false;
    x = S.data[S.top];
    S.top--;
    return true;
}
4,查(获取栈顶元素)
//读栈顶元素
bool GetTop(SqStack S, ElemType &x)
{
    if(S.top == -1)//栈空报错
        return false;
    x = S.data[s.top];
    retur true;
}
5,判空、判满
//判断栈空
bool StackEmpty(SqStack S)
{
    if(S.top == -1)
        return true;
    else
        return false;
}
2,两种实现

初始化时top=0和top=-1

前者代表栈顶指针指向下一个要插入的位置

后者指向当前元素的位置

3,共享栈

两个栈共享同一片内存空间,两个栈从两边往中间增长

初始化

0号栈栈顶指针初始时 top0=-1;

1号栈栈顶指针初始时 top1=MaxSize;

栈满条件

top0+ 1 == top1;

3.1.3栈的链式存储实现

跟单链表差不多,限制只能头插,删除也只能删除第一个元素

1,定义

用链式存储方式实现的栈

2,带头结点

两种实现方式
不带头结点(推荐)

3,重要基本操作

创(初始化)
增(进栈)
删(出栈)
查(获取栈顶元素)
如何判空、判满?

3.2队列

3.2.1队列的基本概念
1,定义

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

特点:

先进入队列的元素先出队(FIFO)。

术语:

队头,队尾,空队列

队头:允许删除的一端

队尾:允许插入的一瑞

2,基本操作

InitQueue(&Q):初始化队列,构造一个空队列Q。
DestroyQueue(&Q):销毁队列。销毁并释放队列Q所占用的内存空间

EnQueue(&Q,x):入队,若队列Q未满,将x加入,使之成为新的队尾。
DeQueue(&Q,&x):出队,若队列Q非空,删除队头元素,并用x返回。

GetHead(Q,&x):读队头元素,若队列Q非空,则将队头元素赋值给x。

其他常用操作:
QueueEmpty(Q):判队列空,若队列Q为空返回true,否则返回false。

3.2.2队列的顺序存储实现
1,队列的实现
#define MaxSize 10
typedef struct
{
    ElemType data[MaxSize]; //用静态数组存放队列元素
    int front,rear;//队头指针和队尾指针
}SqQueue;

分配一块连续的存储单元存放队列中的元素,并附设两个指针

队尾指针:指向队尾元素的后一个位置(下一个应该插入的位置)。

队头指针:指向队头元素。

2,基本操作

进队操作:队不满时,先送值到队尾元素,再将队尾指针加1
出队操作:队不空时,先取队头元素值,再将队头指针加1

1,初始化
//初始化队列
void InitQueu(SqQueue &Q)
{
    //初始化时,队头、队尾指针指向0
    Q.read = Q.front =0;
}
2,判空
bool QueueEmpty(SqQueue Q)
{
    if(Q.rear == Q.front0)
        return true;
    else
        return false;
}
3,增/删(入队/出队操作)

进队操作:队不满时,先送值到队尾元素,再将队尾指针加1
出队操作:队不空时,先队头元素值,再将队头指针加1

假溢出:这种溢出并不是真正的溢出,在data数组中依然存在可以存放元素的空位置

3,循环队列

把存储队列元素的表从逻辑上视为一个环,称为循环队列

初始时:Q.front=Q.rear=0
队首指针进1:Q.front=(Q.front+1)%MaxSize
队尾指针进1:Q.rear=(Q.rear+1)%MaxSize
队列长度:(Q.rear+MaxSize-Q.front)%MaxSize.

判断条件:

队空:Q.front=Q.rear

队满:

牺牲一个单元来区分队空和队满,即”队头指针在队尾指针的下一位置作为队满的标志”。

类型中增设表示元素个数的数据成员。

类型中增设tag数据成员,以区分是队满还是队空

3.2.3队列的链式存储实现
1,队列的实现
typedef struct LinkNode//链式队列节点
{
    ElemType data;
    struct LinkNode *next;
}LinkNode;
typedef struct{			//链式队列
    LinkNode *front, *rear;//队列的头和尾指针
}LinkQueue;
2,基本操作
1,创(初始化)
//初始化队列(带头节点)
void InitQueue(LinkQueue &Q)
{
    //初始时front、rear都指向头节点
    Q.front = Q.rear =(LinkNode *)malloc(sizeof(LinkNode));
    Q.front->next = NULL;
}

//判断队列是否为空
bool IsEmpty(LinkQueue Q)
{
    if(Q.front == Q.rear)
        return true;
    else
        return false;
}
//初始化队列(不带头节点)
void InitQueue(LinkQueue &Q)
{
    //初始化时front、rear都指向NULL
    Q.front = NULL;
    Q.front = NULL;
}

//判断队列是否为空(带头节点)
bool IsEmpty(LinkQueue Q)
{
    if(Q.front == NULL)
        return true;
    else 
        return false;
}
2,增(入队)
//新元素入队(带头结点)
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;//修改表尾指针
    }
}
3,删(出队)
//队头元素出队(带头节点)
bool DeQueu(LinkQueue &Q, ElemType &x)
{
    if(Q.front == Q.rear)//空队
        return false;
    LinkNode *p = Q.front->next;
    x = p->data;
    Q.front->next = p->next;//修改头节点的next指针
    if(Q.rear == p)//如果此次是最后一个节点出队
        Q.rear = Q.front;//修改rear指针的指向
    free(p);
    return true;
}

//队头元素出队(不带头节点)
bool DeQueu(LinkQueue &Q, ElemType &x)
{
    if(Q.front == Q.rear)//空队
        return false;
    LinkNode *p = Q.front;
    x = p->data;
    Q.front = p->next;//修改头节点的next指针
    if(Q.rear == p)//如果此次是最后一个节点出队
    {
        Q.rear = NULL;
        Q.front = NULL;
    }
    free(p);
    return true;
}
4,查(获取头元素)
5,判满

链式存储——一般不会队满,除非内存不足

3.2.4双端队列

栈:只允许从一端插入和删除的线性表

队列:只允许从一端插入、另一端删除的线性表

双端队列:只允许从两端插入、两端删除的线性表(若只使用其中一端的插入,删除操作,效果等同于栈)

输入受限的双端队列:允许从两端删除、从一端插入的队列
输出受限的双端队列:允许从两端插入、从一端删除的队列

考点:对输出序列合法性的判断(在栈中合法的输出序列,在双端队列中必定合法)

3.3栈和队列的应用

3.3.1栈在括号匹配中的应用

用栈实现括号匹配:

依次扫描所有字符,遇到左括号入栈,遇到右括号则弹出栈顶元素检查是否匹配。

匹配失败情况:①左括号单身②右括号单身③左右括号不匹配

3.3.2栈在表达式求值中的应用(上)
1,概念

运算符、操作数、界限符(DIY概念:左操作数/右操作数)

2,三种表达式

中缀表达式:运算符在操作数中间

后缀表达式(逆波兰式):运算符在操作数后面

前缀表达式(波兰式):运算符在操作数前面

3,后缀表达式考点
1,中缀转后缀(手算)

①确定中缀表达式中各个运算符的运算顺序

②选择下一个运算符,按照「左操作数右操作数运算符]的方式组合成一个新的操作数

③如果还有运算符没被处理,就继续②

“左优先”原则:只要左边的运算符能先计算,就优先算左边的(可保证运算顺序唯一)

2,后缀转中缀(手算)

从左往右扫描,每遇到一个运算符,就将<左操作数 右操作数 运算符>变为(左操作数 运算符 右操作数)的形式。

3,后缀表达式的计算(机算)

用栈实现后缀表达式的计算:

①从左往右扫描下一个元素,直到处理完所有元素

②若扫描到操作数则压入栈,并回到①;否则执行③(注意:先出栈的是“右操作数”)

③若扫描到运算符,则弹出两个栈顶元素,执行相应运算,运算结果压回栈顶,回到①

4,前缀表达式
1,中缀转前缀

中缀转前缀的手算方法:

① 确定中缀表达式中各个运算符的运算顺序

②选择下一个运算符,按照「运算符左操作数右操作数】的方式组合成一个新的操作数

③如果还有运算符没被处理,就继续②右边

“右优先”原则:只要右边的运算符能先计算,就优先算右边的

2,计算

用栈实现前缀表达式的计算:

①从右往左扫描下一个元素,直到处理完所有元素

②若扫描到操作数则压入栈,并回到①;否则执行③(注意:先出栈的是“左操作数”)

③若扫描到运算符,则弹出两个栈顶元素,执行相应运算,运算结果压回栈顶,回到①

3.3.3栈在表达式求值中的应用(下)
1,用栈实现中缀表达式转后缀表达式:

初始化一个栈,用于保存暂时还不能确定运算顺序的运算符。从左到右处理各个元素,直到末尾。可能遇到三种情况:

①遇到操作数。直接加入后缀表达式。

②遇到界限符。遇到“(”直接入栈;遇到“)”则依次弹出栈内运算符并加入后缀表达式,直到弹出“(”为止。注意:“(”不加入后缀表达式。

③遇到运算符。依次弹出栈中优先级高于或等于当前运算符的所有运算符,并加入后缀表达式,若碰到“(”或栈空则停止。之后再把当前运算符入栈。按上述方法处理完所有字符后,将栈中剩余运算符依次弹出,并加入后缀表达式。

2,用栈实现后缀表达式的计算:

①从左往右扫描下一个元素,直到处理完所有元素

②若扫描到操作数则压入栈,并回到①;否则执行③

③若扫描到运算符,则弹出两个栈顶元素,执行相应运算,运算结果压回栈顶,回到①

3,用栈实现中缀表达式的计算:

初始化两个栈,操作数栈和运算符栈

若扫描到操作数,压入操作数栈

若扫描到运算符或界限符,则按照“中缀转后缀”相同的逻辑压入运算符栈(期间也会弹出运算符,每当弹出一个运算符时,就需要再弹出两个操作数栈的栈顶元素并执行相应运算,运算结果再压回操作数栈)

3.3.4栈在递归中的应用

函数调用的特点:最后被调用的函数最先执行结束(LIFO)

函数调用时,需要用一个“函数调用栈”存储:

①调用返回地址②实参③局部变量

递归调用时,函数调用栈可称为“递归工作栈

”每进入一层递归,就将递归调用所需信息压入栈顶

每退出一层递归,就从栈顶弹出相应信息

缺点:效率低,太多层递归可能会导致栈溢出;可能包含很多重复计算。

3.3.5队列的应用

树的层次遍历

图的广度优先遍历

实现先来先服务管理调度策略

3.4特殊矩阵的压缩存储

1,对称矩阵
1,特点

若n阶方阵中任意一个元素aij都有ai,j = aj,i则该矩阵为对称矩阵

2,压缩存储策略:

只存储主对角线+下三角区(或主对角线+上三角区)

按行优先或者按列优先

2,三角矩阵
1,特点

下三角矩阵:除了主对角线和下三角区,其余的元素都相同

上三角矩阵:除了主对角线和上三角区,其余的元素都相同

2,压缩

按行优先/列优先规则依次存储非常量区域,并在最后一个位置存放常量c

3,三对角矩阵(带状矩阵)
1,特点

当li-jl>1时,有ai,j=0(1<= i, j<=n)

2,压缩

按行优先/列优先规则依次存储带状区域

4,稀疏矩阵
1,特点

非零元素远远少于矩阵元素的个数

2,策略
策略一:

顺序存储——三元组<行,列,值>

策略二:

链式存储——十字链表法

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值