数据结构(二)栈和队列

一、栈

  1. 栈的定义中栈首先是线性表,然后只有一端(栈顶)可以进行操作,另一端固定(栈底)。
  2. 对栈的基本操作
InitStack(&S);//初始化栈
StackEmpty(S);//判断栈是否为空
Push(&S,x);//将元素压入堆栈(前提栈未满)
Pop(&S,x);//将元素从栈顶弹出(前提栈非空)
GetTop(S,&x);//返回当前栈顶元素
ClearStack(&S);//清空当前栈
bool InitStack(SqStack &s){
    s.top = -1;
    return true;
}
bool StackEmpty(SqStack s){
    if(s.top==-1)
        return true;
    return false;
}
bool Push(SqStack &s,int x){
    if(s.top!=MaxSize-1){
        s.data[++s.top] = x;
        return true;
    }
    return false;
}
bool Pop(SqStack &s,int &x){
    if(s.top==-1){
        return false;
    }
    else{
        x = s.data[s.top--];
        return true;
    }
    
}
bool GetTop(SqStack s,int &x){
    if(s.top == -1)
        return false;
    else{
        x = s.data[s.top--];
        return true;
    }
}
bool ClearStack(SqStack &s){
    s.top=-1;
    return true;
}

  1. 栈的顺序存储结构
# define MaxSize 50
typedef struct{
  Elemtype data[MaxSize];
  int top;//指示当前栈顶的位置
 }SqStack;

注意事项:

  • 栈顶指针:S.top=-1(初始)S.data[S.top]取栈顶元素
  • 进栈操作:栈不满时,栈顶指针先+1,再进行赋值
  • 出栈操作:栈非空时,先将栈顶值取出,然后再-1
  • 栈空条件:S.top=-1栈满条件:S.top=MaxSize-1栈长:S.top+1
    使用数组实现的堆栈受数组本身长度的影响,而且不同的堆栈在使用的时候判断栈空和栈满的条件不同,要根据实际情况具体分析.如果栈顶top被赋值为0,则赋值情况不同。
  1. 共享栈
    两个栈共享同一个物理空间,两个栈的栈尾设置在整个数据空间的两端,两个栈顶向共享空间的中间延伸。当两个栈的栈顶指针都指向栈顶元素,0号栈top=-1是为空,1号栈top=MaxSize时为空。当两个栈栈顶指针相差1时栈满,这时如果再进行操作会溢出,0号栈进栈是先加top再赋值,1号栈进栈是先减再赋值。
    共享栈可以有效的利用存储空间,两个栈的空间互相调节,只有在整个存储空间被占满时才发生上溢。
  2. 栈的链式存储结构
    通常使用单链表实现,没有栈溢出的情况,规定所有操作都是在表头进行的,这里规定链栈没有头节点,Lhead指向栈顶元素。
typedef struct Linknode{
   ElemType data;
   struct Linknode * next;
}*LiStack;

带头节点和不带头节点的链栈操作不同

  1. 对于n个不同元素进栈,出栈序列个数为1/(n+1)Cn/2n=1/(n+1)*(2n)!/n!*n!——卡特兰数
  2. 判断所给的操作序列是否为合法序列,合法序列即可以通过出栈和入栈操作得到的序列,这里给定的序列是有关IO的序列,I代表入栈操作,O代表出栈操作,初始状态和终止状态都为空
    思路:一开始想的是直接按照序列将IO顺序模拟出来,然后最后判断栈是否为空,实际上是没有真正理解合法序列的意思,就算序列是全I,也是一个合理的序列,栈肯定不为空,🤷‍♀️脑抽。
    有两种解法:思想上一致但是实现过程有些差别,就是对照的标准不一样。
    第一种是使用一个堆栈,按照正常堆栈的顺序进行操作,每次是I的时候就将I入栈,是O的时候出栈,如果这两个操作有一个返回的是false,则当前序列就是不合法的。
    第二种是直接判断给定的序列,不需要再使用一个堆栈,使用两个变量countI和countO来存储出栈和入栈的次数,如果当前出栈的次数大于入栈的次数则当前序列不合法,但是注意,扫描结束之后还是要判断是否相等,如果不相等仍然是非法序列
bool islegal(SqStack s,char* ch,int length){
    int i =0;
    while(i!=length){
        if(ch[i]=='I'){
            if(!Push(s,ch[i]))//刚才没有考虑的一点是这个操作是有返回值的要这里如果直接返回false就直接证明这个操作是不合法的
                return false;
        }
        else{
            if(!Pop(s, ch[i]))
                return false;
        }
    }
      if(StackEmpty(s))
        return true;
    else
        return false;
}

还有一种思路:将IO序列看成是一个+1/-1的序列,如果这个序列的任意子序列的前缀和不为0,则当前序列必定是非法的。(这种转换表示方法,把不能直接计算的东西看看是否能用可以计算的整数代替,进行求解)
6. 设计算法判断链表的n个字符是否中心对称
思路一:可以使用一个堆栈,将当前字符串的前n/2个字符入栈,然后将栈顶元素弹出与现在的从n/2(起始下标为0)开始的字符串进行大小比较有一个不一样的则不是中心对称
思路二:可以将当前链表逆置,然后将从头比较两个链表的对应位置元素是否相等(其实这个效率也不高)
思路三:将单链表中的全部元素都入栈,再次扫描单链表L并比较(这个使用两次扫描第一次入栈操作使用一把,第二次扫描比较实用一把,效率不高)

int dc(LinkList l,int n){
int i ;
char a[n/2[;
p = L-<next;
for (i =0;i<n/2;i++)
{
    a[i] = p->data;
    p =p->next;
}
i--;
if(n%2==1)
   p =p->next;
while(p!=nullptr&&a[i]==p->data){
i--;
p=p->next;
{
if(i==-1)
  return 1;
else
  return 0;
}
  1. 设有两个栈s1,s2都采用顺序栈的方式,并且共享一个存储 区域[0,…,maxsize-1],采用栈顶相向、迎面增长的存储方式。设计这两个栈的出栈和入栈操作
# define elemtp int
typedef struct {
    elemtp stack[MaxSize];
    int top[2];
}stk;
int push(stk s,int i,elemtp x){
    if(i<0||i>1)
        exit(0);//错误时使用exit退出
    if(s.top[1]-s.top[0]==1)
        return 0;//没有错误但是栈满时使用return 0
    switch (i) {//这里使用了一个switch比if更清晰
            //总是想不起来有switch这个东西存在
        case 0:
            s.stack[++s.top[0]]=x;
            return 1;
            break;
        default:
            s.stack[--s.top[1]]=x;
            return 1;
           
    }
}
elemtp pop(int i){
    if(i<0||i>1){
        exit(0);
    }
    switch (i) {
        case 0:
            if(s.top[0]==-1){
                return -1;
            }
            else{
                return s.stack[s.top[0]--];
            }
            break;
            
        default:
           if(s.top[1]==MaxSize){
                return -1;
            }
            else{
                return s.stack[s.top[1]++];
            }
    }
}


二、队列

  1. 队列一端插入另一端输出,FIFO
  2. 队列常见基本操作
InitQueue(&Q);//初始化队列构建一个空队列
QueueEmpty(&Q);//队列判空
EnQueue(&Q,x);//入队,若队列未满入队
DeQueue(&Q,&x);//出队,若队列非空,删除队头元素,使用x返回
GetHead(Q.&x);//读队头元素

虽然当使用数组实现的时候数组本身是可以直接随机访问的,但是现在数组的身份时队列,他就有了限制虽然是逻辑上的限制,是使用的人自己知道它自己实际应该有什么限制使用的时候应注意什么

  1. 队列的顺序存储(顺序队列
    分配一块连续的存储单元,只不过要注意一下指震荡 设置方法:front——队头指针;rear——队尾指针
    a、如果队头指针指向队头元素,队尾指针指向队尾元素的下一个位置
    b、如果队头指针指向队头元素的前一个位置,则队尾指针指向队尾元素
typedef struct {
    elemtp data[MaxSize];
    int front,rear;
}SqQueue;

注意事项:

  • 初始状态:Q.front==Q.rear=0;//不仅二者要相等而且要保证同时为0
  • 进队操作:队不满时,先送值到队尾元素,再将队尾指针+1
  • 出队操作:队不空时,先取队头元素之,再将队头指针-1
  • 如果只是单端的队列的话,当rear==MaxSize的时候不能判断的当前的队列已满,因为如果队首执行出队操作,当前队列中仍然有空闲的位置,只是单方向的rear后面的位置不能放置元素了
  1. 循环队列(解决了刚才顺序队列的缺点)
    将队列从逻辑上看成是一个环(不过是加了一个%,到了规定的表的长度的时候就恢复0在开始)
  • 队首指针进1:Q.front = (Q.front+1)%MaxSize;
  • 队尾指针进1:Q.rear = (Q.rear+1)%MaxSize;
  • 队列长度:(Q.rear+MaxSize-Q.front)%MaxSize;
  • 循环队列队空和队满的判定条件(3种方法)
1//牺牲一个单元来区分队空和队满
入队时也就是front从下标1开始指向,把下标0的位置空出来
队满条件:(Q.rear+1)%MaxSize == Q.front;
队空条件:Q.front==Q.rear;
队列中元素的个数:(Q.rear-Q.front+MaxSize)%MaxSize;                                                                                                     
2//类型中增设表示元素个数的数据成员
直接使用一个size来存储元素个数
队满条件:Q.size == MaxSize;
队空条件:Q.size == 0;
3//类型中增设tag数据成员,以区分是队满还是队空
tag=0:若因删除导致Q.rear==Q.front为队空
tag=1:若因插入导致Q.rear==Q.front为队满
  1. 队列的链式存储结构
    通常将队列的链式存储设计成带有头节点的单链表这样插入和删除操作就统一了
typedef struct {
    elemtp data;
    struct LinkList * next;
}LinkList;//链表节点
typedef struct {
    LinkList *first,*rear;
}LinkQueue;//链式队列

注意事项:

  • 队列判空:Q.rear==Q.front==null
  • 适合于数据变动比较大的情形,插入的时候使用尾插法,删除的时候从头节点的下一个开始删除
  • 链式队列的基本操作:
//初始化操作
void InitQueue(LinkQueue &Q){
    Q.front = Q.rear = (LinkList*)malloc(sizeof(LinkList));
    Q.front = Q.rear = nullptr;
}
//判空操作
bool isEmpty(LinkQueue &Q){
    if(Q.rear == Q.front)
        return true;
    else
        return false;
}

//入队
//在插入和删除的时候别忘了更新指针
bool EnQueue(LinkQueue &Q,elemtp x){
    LinkList * p=(LinkList*)malloc(sizeof(LinkList));
   //记住插入的是队尾
    p -> next = nullptr;
    p -> data = x;
    Q.rear ->next = p;
    Q.rear = p;
}

//出队
bool DeQueue(LinkQueue &Q,elemtp &x){
    if(Q.rear==Q.front)
        return false;
    else{
        LinkList *p = (LinkList*)malloc(sizeof(LinkList));
        //指向当前待删除节点
        p = Q.front->next;
        x = p->data;
        Q.front->next = p->next;//之前这里写的是Q.front=p->next;
        //这里考虑存在头节点的话Q.front应该是指向头节点的
        //就得是front的下一个才能指向当前删除节点的下一个节点
        
        //这里有一个问题没有考虑就是如果原队列只有一个节点的话
        //删除之后没有队列为空
        if(p==Q.rear)
            Q.front = Q.rear = nullptr;
        free(p);
        return true;
    }
}
  1. 双端队列
    允许两端都可以进行输入和输出的队列,队列的两端分别叫做前端和后端,只不过两端可以从左边输入从右边输出也可以从右边输入左边输出,甚至可以模拟栈(如果限定输入队列从哪个端口输入只能从该端口输出则当前的双端队列就变成共享栈)。
    输出受限的双端队列:限定一端可以输入输出,另一端只能输入。
    输入受限的双端队列:限定一端可以输入输出,另一端只能输入。
    仅从某端输入输出变成栈的时候可以使用Catalan公式。

三、栈和队列的应用

在进行栈或队列这种带有特殊逻辑的数据结构的使用的时候要分析当前问题的处理逻辑是否和这些数据结构的处理逻辑相同或相似

  1. 栈在括号匹配中的应用
    思想:一个由可匹配的各种括号组成的括号序列是否是匹配的,每次检测到当前括号的时候都急需下一个待匹配的括号出现,上一个就不管了,每次关注当前而且需要处理当前,即符合FILO
    如果是左括号入空栈,如果是右括号,将当前栈顶元素与右括号相对比,如果匹配,当前栈顶元素出栈,当前符号串元素后移一位继续判断;如果不匹配,直接退出返回false
  2. 栈在表达式求值中的应用
    思想:给出的都是中缀表达式,需要先把中缀表达式转换成没有括号的后缀表达式,然后再进行后缀表达式的计算。

关于后缀表达式:已经考虑表达式中的优先级,没有括号,只有操作数和运算符。

  • 中缀表达式转换成后缀表达式
    主要是优先级的处理问题!!!
    a、材料准备:一个符号栈、一个后缀表达式的空间(这个不是栈)、一个原始中缀表达式
    b、操作规则:
    (1)首先初始化符号栈(中缀表达式栈),压入#
    (2)读取中缀表达式如果是操作数直接入后缀表达式栈,如果是符号,则按照如下规则:
1)如果是左括号入符号栈,如果是右括号将当前符号栈中一直弹出到出现左括号的位置中间的全部符号都入后缀表达式
(2)如果是正常的符号,将当前符号的优先级与栈顶符号的优先级进行比对,如果当前符号的优先级大于栈顶符号的优先级,当前符号入栈
(3)如果是正常的符号,如果当前符号的优先级小于等于栈顶元素的优先级将符号栈中的元素弹出送入后缀表达式,然后继续和当前字符串进行比对
(4)读到字符串终止符之后,将当前符号栈中的元素依次弹出,略过左括号

ps:通过中缀表达式的分析直接将运算顺序放在表达式中,最前面的一定是优先级最高的

  • 后缀表达式求值
    准备好一个堆栈,从后缀表达式开始读入字符,如果是操作数入堆栈,如果是运算符,则从当前堆栈中弹出两个元素a/b,使用当前运算符进行运算b<op>a(按照正的顺序压入,弹出的时候第一个弹出的操作数实际上是在第二个运算位置),当所有地的表达式部分处理完毕之后栈顶元素就是最后的计算结果。

这里还可以把中缀表达式转换成一棵树,然后使用树的后序遍历直接得到后缀表达式。

使用栈这种存储结构可以保存之前元素的运行状态,可以保存之前状态对当前的影响,但是目前处理的还是最当下的任务,不过以前的任务也没有被忽略。

  1. 栈在递归中的应用
    递归问题的精髓在于能否把原始问题分解成规模较小但是解题思路完全相同的子问题来实现。
    适合用递归解决的问题:
    问题的定义是递归的;问题所涉及的数据结构是递归的;问题的解法满足递归的性质

递归调用的过程使用了递归工作栈,递归效率不高的原因是重复运算,且容易造成栈溢出。

  1. 队列在层次遍历中的应用
    有一大类问题需要逐层或逐行处理,这样的问题的思想均为在处理当前层的时候就对下一层或者下一行进行预处理,把处理顺序安排好,待当前层或当前行处理完毕,就可以处理下一层或下一行。使用队列是为了保存下一步的处理顺序
  2. 队列在计算机系统中的应用
  • 解决速度主机和外设速度不匹配的问题
    主机速度快,外设速度慢,设置一个缓冲区,主机向缓冲区内写数据,数据在缓冲区内使用队列存储,然后外设取数据的时候从队头取
  • 解决多用户引起的资源竞争问题
    将产生使用CPU请求的用户进程排序,每次从队首取一个进程使用当前CPU

四、特殊矩阵的压缩存储

在数据结构中研究的是如何使用最小的内存空间来存储同样的一组数据,把精力放在如何将矩阵更有效地存储在内存中,并能方便地提取矩阵中的元素

压缩存储:为多个值相同的元素使用一个存储空间进行存储,对于0元素不使用存储空间,为了节省空间

特殊矩阵:具有许多相同值的矩阵,而且这些矩阵相同值的分布呈现一定的规律性。

  1. 对称矩阵
    使用一半的存储空间存储上半部分或者下半部分,==注意取用的时候原始矩阵的二维下标和当前一维数组的下标匹配
  2. 三角矩阵
    和对称矩阵一样只不过多了一个元素存储上三角或者下三角的重复元素
  3. 三对角矩阵
    只有中间的斜着三条有元素存在,其余位置均为0

⚠️实际求对应位置关系的时候如果想不明白其实可以用待定系数法
设一维数组的下标为ai+bj+c,三个未知数,随便找3个直接能看出来对应关系的下标公式直接带入求出abc即可。

  1. 稀疏矩阵
    元素中非零元素在矩阵中的个数相对与矩阵中所有位置来说太小了比如100*100的矩阵只有100个元素有值,这个时候采用三元组存储矩阵
(i,j,data)//横坐标。纵坐标和数据

但是这样存储的矩阵失去了随机存储的特性
⚠️特殊矩阵的存储主要是看两个方面一个是特殊矩阵具体是什么,然后就是下标是从0开始还是从1开始

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值