栈与队列及部分题目

零。        全文概述

        本文将介绍栈和队列的基本知识,如何模拟栈和队列;以及部分题目。由于本次考试不允许使用stl。因此本文将不介绍栈和队列的stl方法,具体解题也只会用到模拟的栈和队列

        

壹。        栈与队列的介绍——有限制的线性表

        栈和队列相比于线性表其实并没有新的内容,只是此时对这个线性表能进行的操作有所限制而已。

        a.        栈           

1.        特点

        i        仅可以在栈顶进行插入or删除操作

                        

        ii.         遵循后进先出原则(LIFO)

                        所以对出栈顺序是有要求的

                eg:

                

        iii.        主要实现的操作:入栈 出栈

2.        应用:

        如果问题求解过程符合/需要“后进先出”——则要考虑栈

        

        i.        生活中的例子:        子弹上膛 电池

        ii.        编程中:                  函数调用/递归调用 进制转换 括号匹配 表达式求值(见后文)

        b.        队列   

1.        特点:

        i.        头删尾插:只能在表的一端进行插入操作,一端进行删除操作

        ii.        遵循先进先出原则(FIFO)

        iii.        主要实现的操作:与栈一样 就是入队和出队

2.        应用

        

                    如果问题求解过程符合/需要“先进先出”——则要考虑队列(如各种排队问题)

        

        i.        生活中的例子:        排队 任务分配

        ii.        编程中:                  业务排队

贰.        模拟栈        

        

        我们知道,栈和队列其实也是线性表,只是是有限制的线性表;因此只用直接按照学习线性表的方法进行模拟,并且再对其操作进行一些限制即可

        a.        用数组进行模拟(最简单 最通用)

去用数组模拟,并实现最基本的弹入 弹出 查询 判断是否为空

#include <cstdio>
#include <iostream>

using namespace std;

const int N = 100010;

//因为栈只用对栈顶进行操作,所以只需要一个tt指针去指向栈顶 
int skt[N],tt;
//此时相当于初始化tt为0——所以tt为0时表示栈是空的 

//弹入操作:
//相当于是两步:1.先让tt+1 让栈顶往上移动一位 	2.再skt【tt】=x 即可让x进入到栈顶 
void push(int x)
{
	skt[++tt]=x;
}

//弹出操作:直接让tt减一(相当于把栈顶指针往下移动了一位 弹出了一个值) 
void pop()
{
	tt--;
}

//判断是否为空 就看栈顶指针是否为0即可 
int empty()
{
	if(tt)	return 0;
	else	return 1;
}

//查询栈顶元素——所以只需要直接取栈顶指针的值即可 
int query()
{
	return skt[tt];
}

int main()
{
	int n;
	scanf("%d",&n);		
//对于用字符去判断操作类型的题目,有两种处理方式:
//1。用一个char数组+scanf可以刚好用空格跳过字符;随后可以用op【0】去判断
//2。 用string字符串去读入 
	
	while(n--)
	{
		char op[5];
		
		scanf("%s",op);
		
		if(op[0]=='p'&&op[1]=='u')
		{
			int x;
			cin>>x;
			push(x);
		}	
		
		if(op[0]=='p'&&op[1]=='o')	pop();	
		
		if(op[0]=='q')	printf("%d\n",query());
		
		if(op[0]=='e')
		{
			if(empty())	cout<<"YES"<<endl;
			else	cout<<"NO"<<endl;
		}
		
		
	}
	
	return 0;
}

       此时相当于把一个数组去横过来看作一个栈,且由于栈只能对栈顶操作,因此我们也只需要有一个指针tt指向栈顶即可

        可以看到,此时数组相当于一个栈,而tt指针所指的及以下部分即是我们可以用的;因此要实现入栈 出栈操作,也只需要把tt指针向上移动or向下移动即可。

        图解:

         要明确。用数组模拟栈时:关键在于要始终让tt指针指向栈顶的位置,而一切的操作也都是对tt指针做的(因为栈要求只能操作栈顶)

                       而虽然数组空间开的很大,但其实我们实际用到的栈的范围就是数组空间的0-tt(不包括0位置)

        用数组模拟栈是十分简单高效的,并且通过函数可以让操作清晰明了;因此后续的题目操作都采用数组模拟的栈

        b.         链栈模拟

        此时的链表相当于一个受限的单链表。在链表一章中我们有说过识别一个链表的唯一标识就是链表头节点head了。在链表模拟栈中,我们让每个链表的头节点都指向栈顶。

        因为插入和删除都只在栈顶进行,让链表的头节点直接指向栈顶也便于我们进行各种操作。

        由图示可以看出,链表模拟和数组模拟其实是一样的。也都是只有一个指针指向栈顶,并在栈顶进行各种操作 

#include <cstdio>
#include <iostream>

using namespace std;

 
struct sNode
{
	int data;		//存放数据 
	sNode * next;	//存放指向下一个位置的指针 
};

typedef sNode* stack; 	//后文的stack相当于就是结构体指针 指向链表所链的下一个位置 

//初始化的时候 相当于构造一个空栈 并让栈顶指针指向NULL 
void init( stack &s )
{	
	s = new sNode;
	s = NULL;	//在链表模拟中,当栈顶指针指向NULL时,我们认为栈是空的 
	return ;
}

//把值为x的插入到栈顶
void push( stack &s , int x ) 
{
	
	stack p = new sNode;//生成新顶点p
	p->data = x;  //他的值就应该是传进来的新的值	
	p->next = s;  //他下一个链接着的应该就是曾经的栈顶s
	
	s = p;		  //此时栈顶应该变化为p 
	
	return ; 
} 

//取栈顶元素 
int gettop( stack &s )
{
	if( s!=NULL )	return s->data;	//如果栈非空,则直接返回栈顶(指针)所指的那个值 
	else			return -1;	//若栈为空,则报错 
}  

//删除栈顶元素 
int pop( stack &s )
{
	if( s==NULL)	return -1;
	else
	{
		stack temp = s;	//用一个临时指针指向原先栈顶s;再将他delete 
		s = s->next;	//让栈顶指针往下移动一位即可 
		delete temp;
	} 
} 

//判断栈是否为空(为空则返回1,否则返回0) 
int empty( stack &s )
{
	if( s==NULL)	return 1;
	else			return 0;
} 



int main()
{
	//初始化 
	stack s;
	init(s); 
	
	//赋值
	push(s,3);
	push(s,4);
	push(s,5);
	
	//判断是否为空
	if(empty(s))	cout<<"为空"<<endl;
	else			cout<<"不为空"<<endl;
	
	//不断输出栈顶元素直到为空
	while(!empty(s))//栈不为空
	{
		int x = gettop(s);
		cout<<x<<" ";
		pop(s);
	} 
	
	
	return 0; 
}

                                

叁。        模拟队列

        类比模拟栈去理解记忆,此时也是给出两种方式模拟,一种是数组模拟,另一种是链表模拟

        a.        用数组进行模拟

#include <cstdio>
#include <iostream>

using namespace std;
const int N = 100010;

//对队列进行初始化 
int hh=0,tt=-1;//让队头一开始为0,队尾一开始为-1 	
int q[N];

//队尾插入 
void push(int x)
{
	q[++tt]=x;
	//也相当于是两步操作:1.tt+1即让队尾往后面挪动一个位置 2.把x赋值给这个新挪动出来的位置 
}

//队头删除 
void pop()
{
	hh++;	//队头的指针是hh,让hh++就是删除一个队头元素 
}

int query()
{
	return q[hh];				//直接去取队头元素即可 
}

bool empty()
{
	if(hh<=tt)	return false;	//只要队尾大于等于队头 就说明这个队列里面是有东西的 
	else		return true;
}


int main()
{
	int n;
	scanf("%d",&n);
	
	while(n--)
	{
		string s;
		cin>>s;
		
		if(s[0]=='e')
		{
			if(empty())	cout<<"YES"<<endl;
			else	cout<<"NO"<<endl;
		}
		
		if(s[0]=='q')	printf("%d\n",query());	
		
		if(s[0]=='p'&&s[1]=='o')	pop();
		
		if(s[0]=='p'&&s[1]=='u')
		{	
			int x;
			scanf("%d",&x);
			push(x);
				
		}
	}
	
	return 0;
}

        与栈去类比理解:此时同样也是一个数组和指针。不同之处在于栈的时候我们只能对栈顶进行操作,因此我们只需要一个tt指针指向栈顶即可;但是在队列中,我们对队首(hh)和队尾(tt)都要进行操作,因此需要两个指针分别指向队首和队列

        要注意的是:

                1.队首指针hh初始化赋值为0,队尾指针tt初始化赋值为-1。当tt<hh时我们认为队列是空的;而当tt>=hh,即队尾指针大于等于队首指针时,我们认为队列中有元素。

                2.由1可知,虽然队列的数组也开了很大,但其实真正有用的只有hh到tt的这部分(左右两边都要取到)

                3.此时对两个指针hh和tt都是有限制的,hh队首只能删除,tt队尾只能加入

图解:

        

        用数组模拟队列也是很简单方便的,因此后面我们都采取数组模拟队列

肆。        栈的例题

        

        a.        进制转换

           

        题目:把输入的十进制数转化成二进制数

        分析:

            

        对于把十进制数转换成二进制数,由进制之间转换的规则我们可以知道,只需要对其不断除2(作除法),再取余数,最后倒着将余数输出即可

        

        11转换成二进制的图解

        

        转换成代码:在第二步的操作中,我们发现是要将结果倒着输出,即LIFO——因此可以考虑使用栈  

        所以,只需要模拟出我们不断进行除法和取余数的操作(直到商为0退出循环),同时将得到的余数都放到栈中

        代码实现:

#include <iostream>
#include <cstdio>

using namespace std;

const int N = 100010;

//初始化 
int skt[N],tt;

//弹入操作
void push(int x)
{
	skt[++tt]=x;
}

//弹出操作
void pop()
{
	tt--;
}

//判断是否为空
int empty()
{
	if(tt)	return 0;
	else	return 1;
}

//查询栈顶元素 
int query()
{
	return skt[tt];
}

void Convert(int num,int n)
{

    if (num == 0) 
	{  //特殊情况特判,当输入是0时,输出也是0
        push(0);
    } 
	else 
	{
        while (num) //只有最后数字为0时,才会退出循环 
		{
            push(num % n);//取余数并放入栈中 
            num /= n;	  //做除法 
        }
    }
    
}
int main()
{
    int num;
    cin>>num;
	Convert( num , 2);
    
    // 把栈中的所有数都输出直到栈为空 
    while (!empty()) 
	{
        printf("%d", query());
        pop();
    }
    printf("\n");
    return 0;
}

本题的过程不算复杂,主要想借助本题说明一下

1.识别栈的使用:当出现需要LIFO的数据时应该考虑到使用栈去存储

2.由于数据结构考试不允许直接使用stl,因此我们只能使用数组模拟的栈

        使用方法如下:

        首先在程序的最前面加上数组模拟栈的相关代码(即初始化+各种操作)

const int N = 100010;

//初始化 
int skt[N],tt;

//弹入操作
void push(int x)
{
	skt[++tt]=x;
}

//弹出操作
void pop()
{
	tt--;
}

//判断是否为空
int empty()
{
	if(tt)	return 0;
	else	return 1;
}

//查询栈顶元素 
int query()
{
	return skt[tt];
}

        随后如果要使用栈,就直接调用skt数组(栈)以及各个操作函数即可

        b.        括号匹配

        

        题目:

              

        分析:

          此题要求我们进行括号的匹配,我们判断括号匹配时应该是在看到右括号时进行,在遇到某个右括号时去遍历过的左边找是否有与他匹配的括号。并且此时有多种括号,而正确的括号匹配中相邻的括号应该是一样的。综上,我们遇到一个新的右括号只用判断与他相邻的左括号是否匹配即可。

        这种匹配规则是可以利用栈去实现的:即在遇到左括号时无脑将其入栈,遇到右括号时就去栈顶找栈顶的左括号是否与其相同(此时栈顶的左括号就是相邻最近的左括号);相同则出栈栈顶括号(相当于两个括号成功匹配相消了),不相同则匹配失败。要注意最后遍历完后,要额外判断一下栈是否为空(为空才表示所有的左括号都被消耗了)

       

        画图模拟:

        

        代码实现:

#include <iostream>
#include <stack>
#include <string>

using namespace std;

const int N = 100010;

//初始化 
char skt[N];
int tt;

//弹入操作
void push(char x)
{
	skt[++tt]=x;
}

//弹出操作
void pop()
{
	tt--;
}

//判断是否为空
int empty()
{
	if(tt)	return 0;
	else	return 1;
}

//查询栈顶元素 
char query()
{
	return skt[tt];
}

//用一个函数去实现判断右括号与左括号是否匹配 
bool isMatching(char opening, char closing) {
    if (opening == '(' && closing == ')')
        return true;
    if (opening == '[' && closing == ']')
        return true;
    if (opening == '{' && closing == '}')
        return true;
    if (opening == '<' && closing == '>')
        return true;
    return false;
}

bool isBalanced(string str) {
    
    for (char c : str)	
	//一种遍历的方法——即每次循环都输入一个字符c(这个字符c来自字符串str)直到字符串为空 
	{
        if (c == '(' || c == '[' || c == '{' || c == '<') //左括号的情况
		{
            push(c);
        } 
		else		//右括号的情况
		{
            if (empty()||!isMatching(query(), c))	//如果匹配失败or栈此时为空 则直接返回失败,并跳出循环 
			{
                return false;
            } 
			else //如果匹配成功 则弹出一个栈顶元素
			{
                pop();
            }
        }
    }
    return empty();//循环完了额外判定下栈是否为空
}

int main() 
{
    string str;
    cin >> str;
    if (isBalanced(str)) {
        cout << "yes" << endl;
    } else {
        cout << "no" << endl;
    }
    return 0;
}

        注意:

        1.要注意此时放入栈内的元素应该是字符类型,所以在初始化skt【】数组时类型应该是char才对

        2.此时的匹配过程是要经过多次判断的,要实现许多功能,所以可以充分利用函数去分离这些功能

        c.        表达式求值(数据结构作业02 -- 四则运算表达式计算)

        题目:

数据结构作业02 -- 四则运算表达式计算

  

表达式的求值原理操作 以及 前中后缀表达式 放到学习完树之后再进行介绍

因此本道题只给出算法和代码实现      

        算法流程:

E1:设立运算符栈和操作数栈;

E2:开始运算符#入栈,向表达式串尾添加结束运算符#;

E3:逐词读入表达式,并处理:

    E31:若读入为操作数,则入栈;

    E32:若读入为运算符,则与栈顶运算符相比较:

        E321:若栈顶运算符优先级高于读入运算符:
                     弹出栈顶运算符和两个操作数,计算并将结果入栈,执行步骤E32;

        E322:若栈顶运算符优先级低于读入运算符:
                     则将读入运算符入栈,执行步骤E3;

        E323:若栈顶运算符优先级等于读入运算符:
                     若为#,计算结束,若为括号,则弹出运算符,执行步骤E3。

E4:检查栈状态,得到计算结果;

同样的也是按照规则进行匹配,只是此时的匹配规则更加复杂,同时要进行运算

        代码实现:

#include<iostream>
#include<cstring>
#include<stack>
#include<unordered_map>
using namespace std;
//num里面存放着运算结果以及运算的数
//op里面存放着操作符
stack<int> num;
stack<char> op;

//eval()计算目前的表达式的值
void eval()
{
    //注意因为用的是栈,所以先去出来的是b,其次是a,防止加减法出现负数
    auto b = num.top(); num.pop();
    auto a = num.top(); num.pop();
    auto c = op.top(); op.pop();
    int x;
    if(c == '+') x = a + b;
    else if(c == '-') x = a - b;
    else if(c == '*') x = a * b;
    else if(c=='/')
	{
		if(b==0)	
		{
			cout<<"error"<<endl;
	
		}
		else	x=a/b;
		
	}
    num.push(x);
}


int bracket(string str){//检查括号匹配情况
	int mark=0;
	for(int i=0;i<str.length();i++){
		if(str[i]=='(')  mark++;
		if(str[i]==')')  mark--;
	}
	return mark;
}


int main()
{
	unordered_map<char,int> pr{{'+',1},{'-',1},{'*',2},{'/',2}};
	qi:;
	while(1)
	{
    	
    string str;
    cin>>str;
    
    if(str[0]=='-')
    {
    	cout<<"error"<<endl;
    	continue;
    }
    
    int tmp = bracket(str);
    if(tmp!=0)
	{
		cout<<"error"<<endl;
		continue;
	}
    
	for(int i = 0;i < str.size();i ++)
    {
        auto c = str[i];
        //倘若这个字符是数字 且 连续出现数字,那么就需要将数字字符组合起来形成一个数
        if(isdigit(c))
        {
            int x = 0, j = i;
            while(isdigit(str[j]) && j < str.size())
                x = x * 10 + str[j++] - '0';
            i = j - 1;//更新i的值
            num.push(x);
        }
        
        //倘若是左括号,那么就要入操作符
        else if(c == '(')   op.push(c);
        //若是右操作符,那么直到做操作符里所有的数,都要进行一系列的运算
        else if(c == ')')
        {
            while(op.top() != '(')  eval();
            op.pop();//且将左括号删除
        }
        
        //若操作的是+-/*操作符,则根据栈头与目前操作符比较大小,若栈头操作符大于等于目前操作符则进行计算
        //且将当前运算符入栈
        else
        {
        	int tmp = i+1;
        	if(str[tmp]=='+' || str[tmp]=='-' || str[tmp]=='/' || str[tmp]=='*' )
        	{
        		cout<<"error"<<endl;
        		goto qi;
        	}
            while(op.size() && pr[op.top()] >= pr[c])   eval();
            op.push(c);
        }
    }
    //将剩余的运算符进行计算
    while(op.size())    eval();
    cout<<num.top()<<endl;
    
	}
    return 0;
}

               

        d.        栈的操作问题

        

        题目:

        栈的操作问题

        

        

        代码实现:

        

#include<stdio.h>
#include<string.h>
#include<stdlib.h>
#include<malloc.h>
#include<math.h>

const int N = 100010;

//初始化 
int skt[N],tt;// 定义一个数组skt,用于模拟栈

int nu[100001]; // 存放出栈序列
int head; // 入栈序列的当前值

//弹入操作
void push(int x)
{
	skt[++tt]=x;
}

//弹出操作
void pop()
{
	tt--;
}

//判断是否为空
int empty()
{
	if(tt)	return 0;
	else	return 1;
}

//查询栈顶元素 
int query()
{
	return skt[tt];
}



int main()
{
    int n;//长度 

    while(~scanf("%d",&n))	// 循环读入输入
	{ 
        head=1; // 初始化入栈序列的当前值
        tt = 0;

		// 读入出栈序列
        for( int i=0 ; i<n ; i++)	scanf("%d",&nu[i]);

        for( int i=0 ; i<n ; i++)	//遍历出栈序列
		{ 
            int t=nu[i]; // 获取当前出栈序列的值
            
            tt++; // 栈顶指针加1,模拟入栈操作
            while(skt[tt-1]<t)	 // 如果栈顶元素小于当前出栈序列的值
			{
                skt[tt]=head; // 将当前入栈序列的值入栈
                head++; 	  // 入栈序列的当前值加1
                tt++; 		  // 栈顶指针加1,模拟入栈操作
                printf("P");  // 输出入栈操作
            }
            
            tt--; // 栈顶指针减1,模拟出栈操作
            if(skt[tt]==t){ // 如果栈顶元素等于当前出栈序列的值
                printf("Q"); // 输出出栈操作
                tt--; // 栈顶指针减1,模拟出栈操作
            }
            else if(skt[tt]>t){ // 如果栈顶元素大于当前出栈序列的值
                printf(" error"); // 输出错误信息
                break; // 跳出循环
            }
        }
        memset(skt,0,sizeof(skt)); // 清空栈数组
        memset(nu,0,sizeof(nu)); // 清空出栈序列数组
        printf("\n"); // 输出换行符
    }
    return 0;
}

         

        head和两个数组的作用:

                我们的入栈序列的顺序一定是1 2 3 4......n。所以用一个head数组从1到n去表示入栈的序列。

                nu【】数组的作用是存储出栈序列。

                skt【】数组则是模拟栈,实现入栈出栈操作

        循环的操作:

                先取出栈序列中的一个值t想办法让其实现出栈操作。这里一定要注意到的是入栈序列是确定的1 2 3 4 n ; 且入栈序列具有单调性。所以我们可以将当前栈与在进入出栈操作的值t进行比较,有三种情况

                先入栈

                1.如果当前操作元素t>栈顶元素:这时候不满足出栈要求;又由于入栈序列是单调递增的,因此此时我们进行while循环不断让其入栈(P),直到操作元素t>=栈顶元素(如果一开始就有操作元素t>=栈顶元素。则不用进行此操作了)

                再出栈,此时

                2.如果当前操作元素t==栈顶元素:这时候满足了出栈要求,所以去进行出栈操作(Q),同时这个出栈序列nu【i】就操作完毕了,可以进入大的for循环的下一项去分析nu【i+1】

                3.如果当前操作元素t<栈顶元素:又由于入栈序列一定是单调递增的,所以此时无论如何都不可能再满足出栈序列了,因此报错error

        memet的操作:用于将数组清0

                1.头文件:#include<string.h>

                2.操作:memset(a,0,sizeof(a))——第一个参数是要进行清零操作的数组;第二个参数是0,表示要去赋值每一字节为0(结果就是全为0);第三个参数表示要操作的长度,一般是整个数组长度,所以用sizeof(数组a)去得

伍。        队列例题

        a.         银行排队

        题目:

        银行排队      

         

  

    代码实现

       

#include <iostream>
#include <vector>

using namespace std;

// 定义客户信息的结构体
struct Client {
    int arrival; // 到达时间
    int duration; // 办理业务所需时间
};

int main() 
{
    int m, total, arrival, duration;

    // 处理多组输入
    while (cin >> m >> total) 
	{
        vector<int> windows(m, 0); // 记录窗口下次空闲的时间
        vector<Client> clients; // 存储客户信息
        double totalWait = 0; // 总的等待时间

        // 读取客户到达和办理业务所需时间
        for (int i = 0; i < total; i++ ) 
		{
            cin >> arrival >> duration;
            clients.push_back(Client{arrival, duration});
        }

        // 遍历每个客户,分配窗口并计算等待时间
        for (const auto &client : clients) 
		{
            int early = 0; // 可以最早服务客户的窗口索引

            //通过循环所有的窗口一遍,找到最先空闲的窗口
            for (int i = 1; i < m; i++ ) 
			{
                if (windows[i] < windows[early]) 	early = i;//若有更早空闲的窗口,则更新
            }
            
            // 客户在窗口的可用时间到达(有窗口正在空闲),直接办理业务
            if (client.arrival >= windows[early]) 
			{
                windows[early] = client.arrival + client.duration;
            } 
			else// 没有窗口正在等待,客户需要等待窗口
            { 
                totalWait += windows[early] - client.arrival;
                windows[early] += client.duration;
            }
        }

        // 计算平均等待时间
        double ans = totalWait / total;
        printf("%.2f\n",ans); 
    }
    return 0;
}

        注:本题采用的是vector模拟

        算法思路如下:

  1. 为每个窗口设置一个变量来跟踪其下一个可用时间。
  2. 对于每个客户,找到下一个可用时间最早的窗口。如果有多个窗口同时可用,选择序号最小的窗口。
  3. 更新该窗口的下一个可用时间为当前时间加上办理业务所需的时间。
  4. 计算客户的等待时间为窗口的下一个可用时间减去客户到达的时间。
  5. 累计所有客户的等待时间,并除以客户总数求平均值。

        

        按照题目要求去把每个客户的情况都遍历一遍。(对于每个客户,都先去遍历一遍所有窗口找到最先结束的窗口)

        注意:

                由于我们在使用栈和队列时使用的都是数组模拟,因此其实操作会更加灵活。比如可以在队列的任意一端进行删除 插入操作

              

      

         

  • 27
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值