【数据结构】从0开始数据结构学习-限定性线性表—栈与队列

本节为栈与队列内容,回到总目录:点击此处

栈 LIFO(后进先出)

–将线性表的插入和删除运算限制只在表的一端进行 栈顶|栈底–

数据元素:可以是任意类型的数据,但必须属于同一个数据对象
关系:栈中数据元素之间是线性关系
基本操作:1. InitStack() 2.ClearStack() 3.IsEmpty() 4.IsFull()

  		   5. Push(S,X)	  3.Pop(S,x)		   7.GetTop(S,x)

栈在计算机中主要有两种基本的存储结构:顺序存储结构(顺序栈)|链式存储结构(链栈)

顺序栈

用一组地址连续的存储单元依次存放自栈底到栈顶的数据元素,同时由于栈的操作的特殊性,还必须附设一个位置指针top(栈顶指针)。 通常以 top = -1 来表示空栈

#define Stack_Size 50
typedef struct
{
    StackElementType elem[Stack_Size];
    int top; //栈顶元素的下标,top = -1 表示空栈
}SeqStack;

img

顺序栈的操作

·初始化栈

void InitStack(SeqStack *S)
{
    S -> top = -1;
}

·进栈

int Push(SeqStack * S, StackElementType x)
{
    if(S -> top == Stack_Size) return false;
    S -> top ++;
    S -> elem[S -> top] = x;
    return true;
}

·出栈

int Pop(SeqStack *S, StackElementType *x)
{
    if(S -> top == -1)
    {
        return false;
    }
    else
    {
        *x = S -> elem[S -> top];
        S -> top --; //修改栈顶指针
        return true;
    }
}

·获取栈顶元素

int GetTop(SeqStack *S, StackElementType *x)
{
    if (S -> top == -1)  return false;
    else 
    {
        *x = S -> elem[S -> top];
        return true;
    }
}
链栈

用链表表示的栈为链栈,一般使用带头指针的单链表,链表的头指针一般就是栈顶指针。
top -> NULL 则表示该栈为空栈
注:在栈使用完毕后应该记得回收空间

typedef struct Node
{
    StackElementType data;
    struct node * next;
}LinkStackNode;
typedef LinkStackNode *LinkStack;
链栈的操作

·进栈

int Push(LinkStack top, StackElementType x)
{
    LinkStackNode * temp;
    temp = (LinkStackNode *)malloc(sizeof(LinkStackNode));
    if (temp == NULL) return Flase;
    temp -> data = x;
    temp -> next = top -> next;
    top -> next = temp;
    return true;
}

·出栈

int Pop(LinkStack top , StackElementType *x)
{
    LinkStackNode * temp;
    temp = top -> next;
    if(temp == NULL) return false //说明栈为空
    top -> next = temp -> next;
    *x = temp -> data;
    free(temp);
    return true;
}
多栈共享技术

·在顺序栈道共享技术中,最常用的是两个栈的共享技术,也就是双端栈。两个栈的栈底分别放在一维数组的两端(0,m - 1),由于两个栈顶是动态变化的,这样可以形成互补,使得每个栈可用的最大空间与实际使用情况有关,这样利用率更高。

#define M 100
typedef struct
{
    StackElementType Stack[M]; //栈区
    StackElementType top[2];  //top[0],top[1]栈顶指示器
}DqStack;
//双端栈的初始化
void InitStack(DqStack *S)
{
    S -> top[0] = -1;
    S -> top[1] = M;
}
//双端栈的进栈操作
int Push(DqStack *S, StackElementType x, int i)
{
    //将元素x压入i号堆栈
    if(S -> top[0] + 1 == S -> top[1]) return false //栈已满
    switch(i)
    {
        case 0: //0号栈
            S -> top[0] ++;
            S -> Stack[S -> top[0]] = x;
            break;
        case 1: //1号栈
            S -> top[1] --;
            S -> Stack[S -> top[1]] = x;
            break;
        default:
            return false;
    }
    return true;
}
//双端栈的出栈操作
int Pop(DqStack * S,StackElementType *x, int i)
{
    switch(i)
    {
        case 0:
            if(S -> top[0] == -1) return false;
            * x = S -> Stack[S -> top[0]];
            S -> top[0] --;
            break;
        case 1:
            if(S -> top[1] == M) return false;
            * x = S -> Stack[S -> top[1]];
            S -> top[1] ++;
            break;
        default:
            return false;
    }
    return true;
}

img

·链栈实现多栈运算,多栈运算//同时使用两个以上的栈实现操作/

#define M 10
typedef struct node
{
    StackElementType data;
    struct node * next;
}LinkStackNode,* LinkStack;
LinkStack top[M]; //用于指示M个链栈道栈顶指针。

img

栈的应用举例
括号匹配问题
void BracketMatch(char *str)
{
    Stack S;
    int i;
    char ch;
    InitStack(&S);
    for(i = 0; str[i] != '\0' ; i++)
    {
        switch(str[i])
        {
            case '(':
            case '[':
            case '{':
                push(&S,str[i]);
                break;
            case ')':
            case ']':
            case '}':
                if(IsEmpty(&S))
                {
                    cout << "右括号多余"; return;
                }
                else
                {
                    GetTop(&S,&ch); //获取栈顶元素
                    if(Match(ch,str[i])) //判断是否括号可以匹配。
                        Pop(&S,&ch);
                    else
                        cout<<"对应的左右括号不同类"; return;
                }
        }
    }
    if (IsEmpty(S))
        cout << "括号匹配";
    else
        cout << "左括号多余!";
}
表达式求值

任何一个表达式都是由运算对象(Operand)、运算符(Operator)、界限符(Delimiter)组成 ||
为了正确处理表达式,使用栈来实现正确的指令序列是一个重要的技术

【算法思想】 A/B↑C+D*E#
1、规定运算符的优先级表
2、设置两个栈:OVS(运算数栈),OPTR(运算符栈)
3、自左向右,进行处理:
遇到运算数则进入OVS栈,遇到运算符则与OPTR栈的栈顶运算符进行优先度比较:
·如果当前运算符优先度大于OPTR栈顶运算符优先级,则当前运算符进OPTR栈。
·如果当前运算符有限度小于OPTR栈顶运算符优先级,则OPTR退栈一次,得到栈顶运算符θ,连续退OVS栈两次,得到运算数a、b,执行a、b的θ操作,得到结果T(i),将T(i)进OVS栈。

img

int ExpEvaluation()
{
    InitStack(&OPTR); //初始化两个栈
    InitStack(&OVS);
    Push(&OPTR,'#');  //便于操作先将#压入栈底
    cout<<"输入一个表达式";
    ch = getchar();
    while(ch != '#' || GetTop(OPTR) != '#') //表示表达式结束
    {
        if(!In(ch,OPSet))  //不是操作符,是操作数,进OVS栈
        {
            n = GetNumber(ch); //字符转化
            push(&OVS,n); //运算数压入OVS
            ch = getchar();
        }
        else
            switch(Compare(ch,GetTop(OPTR)))  //比较运算符的优先级
            {
                case '>': //若输入的运算符比栈顶的优先级高
                    Push(&OPTR,ch);  //则压入栈
                    ch = getchar();
                    break;
                case '=':
                case '<':
                    Pop(&OPTR,&op);  //OPTR退栈一次
                    Pop(&OVS,&b);  //OVS退栈两次
                    Pop(&OVS,&a);  //取引用表示直接取他们的值
                    v = Execute(a,op,b);  //执行a、b的θ操作
                    Push(&OVS,v);  //并把获得的操作结果重新压回OVS栈
                    break;
            }
    }
}
栈与递归的实现

典例:汉诺塔问题:

void hanoi(int n, char x, char y, char z)
{
	if(n == 1) move(x,1,z); //编号为1的圆盘从x移动到z    
	else
    {
        hanoi(n-1,x,z,y);  //将x上编号为1~n-1的圆盘移动到y,z为辅助
        move(x,n,z); //将第n个圆盘从x移动到z
        hanoi(n-1,y,x,z); //将y上编号为1~n-1的圆盘移动到z,x为辅助
    }
}

递归函数调用时,按照“后调用先返回”的原则处理调用过程。
系统将整个程序运行时所需的数据空间安排在一个栈中,每当调用一个函数时,就为它在栈顶分配一个存储区,而每当从一个函数退出时,就释放它的存储区。//故此,当前正在运行的函数的数据区必须在栈顶
为保证递归函数正确执行,系统需设立一个 递归工作栈 -->>用作整个递归函数运行期间使用的数据存储区
工作记录 -->>每层递归所需信息构成的(其中包括所有的实在参数、局部变量和上一层返回地址)
活动记录 -->> 当前执行层的工作记录必为递归工作栈栈顶的工作记录
当前环境指针 -->>指示活动记录的栈顶指针

递归的特性:
1.递归算法实际上是 分治 将复杂问题化为简单问题的求解方法
2.递归算法的效率较低

递归算法的转化方法:
1.简单递归问题的转换,对于尾递归和单向递归的算法----->可采用循环结构的算法替代
2.基于栈的方法:将递归中隐含的栈机制,转换为有用户直接控制的显式的栈 (利用栈来保存数据,栈的后进先出的特性吻合递归算法的执行过程)

1.单向递归:
斐波那契数列
if(n == 1 || n == 0) return n;
else
{
x = 0, y = 1;
for(i = 2; i <= n;i++)
{
z = y; y = x + y; x = z;
}
}
2.尾递归 ||阶乘

if(n == 1) return 1;
return n*fac(n-1);

----->

int fac = 1;
for(i = 1;i <= n; i++) fac = fac *i;
return fac;

  • 对斐波那契数列的优化(记忆化搜索|动态规划)

    因为每次计算的中间结果都一定,不会发生变化,可以用数组存起来,从而得到优化。

    int memo[MAX_N + 1];
    int fib(int n)
    {
        if(n <= 1) return n;
        if(memo[n] != 0) return memo[n];
        return memo[n] = fib(n-1) + fib(n-2);
    }
    
队列

先进先出(FIFO) 队尾rear ,队头 front
队头指针永远指向第一个元素,队尾指针永远指向最后一个元素

队列的独特用途:1.离散事件的模拟(模拟事件发生的先后顺序,如CPU芯片中的指令译码队列)
2.操作系统中的作业调度(一个CPU执行多个作业)

抽象数据类型定义

ADT Queue
{
    数据元素:可以是任意类型的数据,但必须属同一个数据对象
    结构关系:队列中的数据元素之间是线性的
    基本操作:
    InitQueue(Q) :将队列Q初始化为一个空队列
    IsEmpty(Q) :对一个已存在的队列Q,判断是否为空。
    IsFull(Q):对一个已存在的队列Q,判断是否满了。
    EnterQueue(Q,x):在队列Q队尾插入x
    DeleteQueue(Q,x):在队列Q的队头元素出队,并用x带回其值
    GetHead(Q,x):取队列Q的队头元素,不出队并用x带回其值
    ClearQueue(Q):将队列Q置为空队列
}ADT Queue;
队列的表示和实现
链队列

img

typedef struct Node
{
    QueueElementType data;
    struct Node * next;
}LinkQueueNode;

typedef struct
{
    LinkQueueNode *front;
    LinkQueueNode *rear;
}LinkQueue;

链队的操作:
·链队的初始化

int InitQueue(LinkQueue *Q)
{
    Q -> front = (LinkQueueNode *)malloc(sizeof(LinkQueueNode));
    if(Q -> front != NULL)
    {
        Q -> rear = Q -> front;
        Q -> front -> next = NULL;
        return true;
    }
    else return false; //溢出
}

·链队列入队操作
//带头节点!
//空的链队列队的队头指针和队尾指针均指向头节点!

int EnterQueue(LinkQueue *Q; QueueElementType x)
{
    LinkQueueNode * NewNode;
    NewNode = (LinkQueueNode *)malloc(sizeof(LinkQueueNode));
    if(NewNode != NULL)
    {
        NewNode -> data = x;
        NewNode -> next = NULL;
        Q -> rear -> next = NewNode;
        Q -> rear = NewNode; //队尾指针始终指向最后一个元素
        return true;
    }
    else return false //溢出
}

·链队列出队操作算法

int DeleteQueue(LinkQueueNode *Q; QueueElementType *x)
{
    LinkQueueNode *p;
    if(Q -> front == Q -> rear) return false;
    p = Q -> front -> next;  //指向队头元素
    Q -> front -> next = p -> next; //删除队头元素
    if(Q -> rear == p) //表示如果队列中只有一个元素p,则p出队后成为空队
    {
        Q -> rear = Q -> front;
    }
    *x = p -> data;
    free(p);
    return true;
}
循环队列 // 顺序队列

如果是顺序队列,很容易出现的问题就是 “假溢出”

其解决方法:设置更大的队列的元素个数
修改出队算法,每次出队,队列中的剩余元素都向前移动
修改入队,增加判断条件,当出现假溢出时,队列中的元素向对头移动,然后完成入队操作
采用循环队列

初始化队列时,令 front = rear = 0;入队时,直接将新元素送入尾指针rear所指的单元,然后尾部指针+1;出队时,直接取队头指针front所指的元素,然后头指针+1。
//当rear = Maxsize的时候 认为队满
//但不一定真的满了,队头可能存在很多空间,但无法使用,这种现象称为 假溢出
—>一个巧妙的方法是将顺序队列的数组看成一个环状的空间,即规定最后一个单元的后继为第一个单元,形象地称之为循环队列。

img

img

循环队列的定义

#define MAXSIZE 50
typedef struct
{
    QueueElementType element[MAXSIZE];
    int front;
    int rear;
    //int count;   /*可选增加的计数器*/
}SeqQueue;

新问题:循环队列中,队满和队空都有可能存在front = rear;

解决方法:
① 设置一个计时器,用于记录队列中元素个数(队列长度)
队满:count > 0 && rear == front;
队空:count == 0;
②加设标志位,判断是否有过出入队
tag == 1 表示入队 && front == rear
tag == 0 表示出队 && front == rear
③少用一个存储单元,这样rear就永远追不上front
队满:front == (rear + 1) % MaxSize
队空:rear == front

循环队列的初始化操作

void InitQueue(SeqQueue *Q)
{
    Q -> front = Q -> rear = 0;
}

循环队列的入队操作

int EnterQueue(SeqQueue * Q, QueueElenmentType x)
{
    if ((Q -> rear + 1) % MaxSize == Q -> front)
    {
        return false; //队满
    }
    Q -> element[Q -> rear] = x;
    Q -> rear = (Q -> rear + 1) % MaxSize; //尾指针向后移动一位,取模运算!
    return true;
}

循环队列的出队操作

int DeleteQueue(SeqQueue *Q, QueueElement *x)
{
    if (Q -> front == Q -> rear) return false; //队空
    * x = Q -> element[Q -> front];
    Q -> front = (Q -> front + 1) % MaxSize;
    return true;
} 
队列的应用举例
杨辉三角形

//最大行数一定小于循环队列的MaxSize值
img

void YangHuiTriangle()
{
    SeqQueue Q;
    InitQueue(&Q);
    EnterQueue(&Q,1) //第一行元素入队
    for(n = 2;n <= N; n ++) //产生第n行元素并入队,同时打印第n-1行的元素
    {
        EnterQueue(&Q,1); //第n行第一个元素入队
        for(i = 1; i <= n - 2; i ++) //利用n - 1行元素去产生第n行中间的n-2个元素并入队
        {
            DeleteQueue (&Q,&temp);
            Print(temp); //打印n-1行的元素
            GetHead(Q,&x);
            temp = temp + x; //利用n-1行元素产生第n行元素
            EnterQueue(&Q,temp);
        }
        DeleteQueue (&Q, &x);
        cout << x; //打印n-1行的最后一个元素
        EnterQueue(&Q,1); //第n行的最后一个元素入队
    }
    while (!IsEmpty(Q)) //打印最后一行元素
    {
        DeleteQueue (&Q,&x);
        cout << x;
    }
}
键盘输入循环缓冲区问题

队列的特性保证的输入的字符先输入、先保存、先处理的要求,

#include <cstdio>
#include <conio>
#include <queue>
main()
{
    char ch1,ch2;
    SeqQueue Q;
    int f;
    InitQueue (&Q);
    for(;;)
    {
        for(;;)
        {
            cout << "A";
            if(kbhit())
            {
                ch1 = getchar();
                if (ch1 == ';' || ch1 == ',') break;
                f = EnterQueue (&Q,ch1);
                if (f == FALSE)
                {
                    cout << "队列已满";
                    break;
                }
            }
        }
        while (!IsEmpty(Q))
        {
            DeleteQueue(&Q,&ch2);
            putchar(ch2);
        }
        if (ch == ';') break;
    }
}
◾栈与队列 总结与反思

·LIFO~FIFO
顺序和链式两种存储方式:
·对进栈操作来说,顺序栈受到事先开辟的栈区容量的限制,可能产生上溢。
·循环队列是顺序队列,为了缓解假溢出,可以看作是一个首尾相接的环
·链队列的操作实现与单链表类似,只不过链队中除了头指针,还外设一个尾指针[并通常封装在一个结构体里]

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值