DS4 栈 实验习题【DS C++】

DS4 栈 实验习题【DS C++】

前言

昨天做数据结构实验,发挥挺拉的,尤其是最后一问求波兰式和逆波兰式课下想了很久才有思路。然后代码实现的时候还是遇到了不少bug,疯狂debug到第二天1点多。。。

话不多说,来看一下这些折磨人的小妖精吧。

本次的代码都放在我的GitHub仓库上,需要的可以去看看

代码库链接

A. 前驱后继(线性结构)

题目描述

在双向链表中,A有一个指针指向了后继节点B,同时,B又有一个指向前驱节点A的指针。这样不仅能从链表头节点的位置遍历整个链表所有节点,也能从链表尾节点开始遍历所有节点。

对于给定的一列数据,按照给定的顺序建立双向链表,按照关键字找到相应节点,输出此节点的前驱节点关键字及后继节点关键字。

输入

第一行两个正整数n(代表节点个数),m(代表要找的关键字的个数)。

接下来输入n个整数为关键字key(数据保证关键字在数列中没有重复)。

接下来有m个要查找的关键字,每个占一行。

输出

对给定的每个关键字,输出此关键字前驱节点关键字和后继节点关键字。如果给定的关键字没有前驱或者后继,则不输出。给定关键字为每个输出占一行。

输入样例

10 3
1 2 3 4 5 6 7 8 9 0
3
1
0

输出样例

2 4
2
9

编码思路

基本上和单链表差不多,加多一个pre指针,指向前置节点。

Code(C++)

#include<iostream>

using namespace std;

// 定义双向链表节点
class ListNode
{
public:
	int val;
	ListNode* next, *pre;
public:
	ListNode() {};
	ListNode(int v) : val(v), next(nullptr), pre(nullptr){}
};

// 定义双向链表
class LinkList
{
private:
	// 头节点和尾节点
	ListNode* head, * tail;
	int len;
public:
	// 构造函数,初始化链表
	// 传入数组
	LinkList(int len, int* arr)
	{
		this->len = len;
		// 先构造头节点
		head = new ListNode(arr[0]);
		ListNode* node = head->next;
		ListNode* pre = head;
		// 遍历数组,和单链表过程差不多,但是需要加一个加节点前置指针指向pre
		for (int i = 1; i < len; ++i)
		{
			node = new ListNode(arr[i]);
			pre->next = node;
			node->pre = pre;
			node = node->next;
			pre = pre->next;
		}
		// 尾节点
		tail = node;
	}
	// 找到Key节点,然后输出前置和后置
	void getKey(int key)
	{
		ListNode* node = head;
		// 这里其实可以使用双指针去遍历,降低到O(n/2)
		for (int i = 0; i < len; ++i)
		{
			if (node->val == key)
			{
				break;
			}
			node = node->next;
		}
		if (node != nullptr)
		{
			// 先判断是不是前置和后置指针都有
			if (node->pre != nullptr && node->next != nullptr)
			{
				cout << node->pre->val << " " << node->next->val;
			}
			else if (node->pre != nullptr)
			{
				cout << node->pre->val;
			}
			else
			{
				cout << node->next->val;
			}
			cout << endl;
		}
	}
};

int main(int argc, char** argv)
{
	int n;
	cin >> n;
	int m;
	cin >> m;
	int* arr = new int[n];
	for (int i = 0; i < n; ++i)
	{
		cin >> arr[i];
	}
	LinkList* list = new LinkList(n, arr);
	for (int i = 0; i < m; ++i)
	{
		int key;
		cin >> key;
		list->getKey(key);
	}

	return 0;
}

代码中其实有可以优化的地方,比如说构造的时候也可以双向构造,不过影响不大,主要是找到Key的时候,可以采用双指针去遍历,有效降低时间复杂度,提高性能。

B. DS堆栈–逆序输出(STL栈使用)

题目描述

C++中已经自带堆栈对象stack,无需编写堆栈操作的具体实现代码。

本题目主要帮助大家熟悉stack对象的使用,然后实现字符串的逆序输出

输入一个字符串,按字符按输入顺序压入堆栈,然后根据堆栈后进先出的特点,做逆序输出
在这里插入图片描述

输入

第一行输入t,表示有t个测试实例
第二起,每一行输入一个字符串,注意字符串不要包含空格

字符串的输入可以考虑一下代码:

#include

int main()

{ string str;

Int len;

cin>>str; //把输入的字符串保存在变量str中

len = str.length() //获取输入字符串的长度

}

输出

每行逆序输出每一个字符串

输入样例

2
abcdef
aabbcc

输出样例

fedcba
ccbbaa

编码思路

直接按题目来就行了。

Code(C++)

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

using namespace std;

int main(int argc, char** argv)
{
	int t;
	cin >> t;
	while (t--)
	{
		string str;
		cin >> str;
		stack<char> chstk;
		// 直接遍历字符串
		for (char& ch : str)
		{
			// 压栈
			chstk.push(ch);
		}
		// 然后不断出栈
		while (!chstk.empty())
		{
			cout << chstk.top();
			chstk.pop();
		}
		cout << endl;
	}

	return 0;
}

C. DS堆栈–行编辑

题目描述

使用C++的STL堆栈对象,编写程序实现行编辑功能。行编辑功能是:当输入#字符,则执行退格操作;如果无字符可退就不操作,不会报错

本程序默认不会显示#字符,所以连续输入多个#表示连续执行多次退格操作

每输入一行字符打回车则表示字符串结束

注意:必须使用堆栈实现,而且结果必须是正序输出

输入

第一行输入一个整数t,表示有t行字符串要输入
第二行起输入一行字符串,共输入t行

输出

每行输出最终处理后的结果,如果一行输入的字符串经过处理后没有字符输出,则直接输出NULL

输入样例

4
chinaa#
sb#zb#u
##shen###zhen###
chi##a#####

输出样例

china
szu
sz
NULL

编码思路

我们用栈存储字符,识别到#的时候,判断栈是否为空,不空则栈顶出栈。

Code(C++)

#include<iostream>
#include<stack>
#include<string>
using namespace std;

/// <summary>
/// 行编辑函数
/// </summary>
/// <param name="s"></param>
void lineEdit(string s)
{
	// 使用两个栈,一个用于存储,一个用于输出
	stack<char> stk1, stk2;
	for (char& ch : s)
	{
		// 识别到不是 # 则压栈 
		if (ch != '#')
		{
			stk1.push(ch);
		}
		// 判断栈是否空,不空则出栈
		else if (!stk1.empty())
		{
			stk1.pop();
		}
	}
	// 如果遍历完,栈1为空,则直接输出,并跳出
	if (stk1.empty())
	{
		cout << "NULL" << endl;
		return;
	}
	// 否则,则需要使用另外一个栈将原栈的元素顺序倒置,然后才输出
	while (!stk1.empty())
	{
		stk2.push(stk1.top());
		stk1.pop();
	}
	while (!stk2.empty())
	{
		cout << stk2.top();
		stk2.pop();
	}
	cout << endl;
}

int main(int argc, char** argv)
{
	int t;
	cin >> t;
	while (t--)
	{
		string s;
		cin >> s;
		lineEdit(s);
	}

	return 0;
}

D. DS堆栈–括号匹配

题目描述

处理表达式过程中需要对括号匹配进行检验,括号匹配包括三种:“(”和“)”,“[”和“]”,“{”和“}”。例如表达式中包含括号如下:

(	)	[	(	)	(	[	]	)	]	{	}
1	2	3	4	5	6	7	8	9	10	11	12

从上例可以看出第1和第2个括号匹配,第3和第10个括号匹配,4和5匹配,6和9匹配,7和8匹配,11和12匹配。从中可以看到括号嵌套的的情况是比较复杂的,使用堆栈可以很方便的处理这种括号匹配检验,可以遵循以下规则:

1、 当接收第1个左括号,表示新的一组匹配检查开始;随后如果连续接收到左括号,则不断进堆栈。

2、 当接受第1个右括号,则和最新进栈的左括号进行匹配,表示嵌套中1组括号已经匹配消除

3、 若到最后,括号不能完全匹配,则说明输入的表达式有错

建议使用C++自带的stack对象来实现

在这里插入图片描述

输入

第一行输入一个t,表示下面将有t组测试数据。接下来的t行的每行输入一个表达式,表达式只考虑英文半角状态输入,无需考虑中文全角输入

输出

对于每一行的表达式,检查括号是否匹配,匹配则输入ok,不匹配则输出error

输入样例
2
(a+b)[4*5+(-6)]
[5*8]/{(a+b)-6
输出样例
ok
error
提示

算法流程

1、初始化,i=0,建立堆栈,栈为空
2、输入表达式,建立指针指向表达式的头部
3、读入表达式的第i个字符
4、如果第i个字符是左括号,入栈
5、如果第i个字符是右括号,检查栈顶元素是否匹配
A.如果匹配,弹出栈顶元素
B.如果不匹配,报错退出
6、i++,指向下一个字符,是否已经表达式末尾
A. 未到末尾,重复步骤3
B. 已到达末尾
a. 堆栈为空,输出ok
b. 堆栈不为空,输出error

编码思路

其实提示,写得蛮清楚的了,按照流程来即可。

Code(C++)

#include<iostream>
#include<string>
#include<stack>
using namespace std;

// 括号匹配函数
void ParMatch(string s)
{
	stack<char> stk;
	// 用于记录最终是否成功匹配
	bool flag = true;
	for (char& ch : s)
	{
		// 左括号入栈
		if (ch == '(' || ch == '[' || ch == '{')
		{
			stk.push(ch);
		}
		// 右括号出栈,匹配
		else if (!stk.empty() && 
				(ch == '}'  && stk.top() == '{' 
				|| ch == ']' && stk.top() == '[' 
				|| ch == ')' && stk.top() == '('))
		{
			stk.pop();
		}
		// 匹配不成功 直接退出
		else if (ch == '}' || ch == ')' || ch == ']')
		{
			flag = false;
			break;
		}
	}
	// 判断栈是否为空,以及是否匹配成功
	if (!stk.empty() || flag == false)
	{
		cout << "error" << endl;
	}
	else
	{
		cout << "ok" << endl;
	}
}

int main(int argc, char** argv)
{
	int t;
	cin >> t;
	while (t--)
	{
		string s;
		cin >> s;
		ParMatch(s);
	}
	return 0;
}

E. DS堆栈–迷宫求解

题目描述

给出一个N*N的迷宫矩阵示意图,从起点[0,0]出发,寻找路径到达终点[N-1, N-1]

要求使用堆栈对象来实现,具体算法参考课本3.2.4节51页

输入

第一行输入t,表示有t个迷宫

第二行输入n,表示第一个迷宫有n行n列

第三行起,输入迷宫每一行的每个方格的状态,0表示可通过,1表示不可通过

输入n行

以此类推输入下一个迷宫

输出

逐个输出迷宫的路径

如果迷宫不存在路径,则输出no path并回车

如果迷宫存在路径,将路径中每个方格的x和y坐标输出,从起点到终点,每输出四个方格就换行,最终以单词END结尾,具体格式参考示范数据

输出的代码参考如下:

//path是保存路径的堆栈,堆栈中每个元素都包含x坐标和y坐标,用属性xp和yp表示

//path1是一个临时堆栈,把path的数据倒序输出到path1,使得路径按正序输出

​ if (!path.empty()) //找到路径

​ { //…若干代码,实现path的数据导入path1

​ i=0; //以下是输出路径的代码

​ while (!path1.empty())

​ { cpos = path1.top();

​ if ( (++i)%4 == 0 )

​ cout<<’[’<<cpos.xp<<’,’<<cpos.yp<<’]’<<"–"<<endl;

​ else

​ cout<<’[’<<cpos.xp<<’,’<<cpos.yp<<’]’<<"–";

​ path1.pop();

​ }

​ cout<<“END”<<endl;

​ }

​ else

​ cout<<“no path”<<endl; //找不到路径输出no path

输入样例
2
8
0 0 0 1 1 1 1 1
1 0 0 0 1 0 0 1
1 0 0 0 1 0 0 0
1 1 0 0 0 0 0 1
0 0 1 1 0 1 1 0
0 0 0 0 0 0 1 1
1 1 1 1 1 0 0 1
0 0 0 0 1 0 0 0
7
0 0 0 1 1 1 1
1 0 0 1 0 0 1
1 0 0 1 0 0 0
1 1 0 0 0 0 1
0 0 1 1 0 1 0
1 0 0 0 0 1 0
0 0 0 0 1 1 0

输出样例
[0,0]--[0,1]--[0,2]--[1,2]--
[1,3]--[2,3]--[3,3]--[3,4]--
[4,4]--[5,4]--[5,5]--[6,5]--
[6,6]--[7,6]--[7,7]--END
no path

编码思路

这里采用DFS还是BFS都可以,推荐使用DFS,函数递归使用程序自身的栈,如果我们能够到达终点,则开始递归将坐标存进栈,注意,我们和题目直接给出的思路不同,题目需要在搜索的过程中将坐标压栈入栈,而我们的思路则可以减少不必要的压栈和出栈操作,性能提高。

注意:因为OJ的原因,即使一个样例有多条路径可选,但只有oj内部答案的样例才算通过,所以根据样例我们可以猜测向右的优先级高于向下。以此类推。

Code(C++)

#include<iostream>
#include<stack>
#include<utility>
using namespace std;

class Solution
{
public:
	// 路径栈,存储路径的坐标
	stack<pair<int, int>> path;
	const int dx[4] = { 0, 1, 0, -1 };
	const int dy[4] = { 1, 0, -1, 0 };

	// 寻找路径函数
	stack<pair<int, int>> findpath(int n,int** arr)
	{
		// 先判断起始点是否可以通过
		if (arr[0][0] == 1)
		{
			return {};
		}
		bool flag = true;
		// 调用dfs函数搜索路径
		dfs(arr, 0, 0, n, flag);
		return path;
	}
	/// <summary>
	/// DFS搜索路径
	/// </summary>
	/// <param name="arr">迷宫</param>
	/// <param name="x">当前x坐标</param>
	/// <param name="y">当前y坐标</param>
	/// <param name="n">迷宫尺寸</param>
	/// <param name="flag">是否到达终点标识符</param>
	void dfs(int** arr, int x, int y, int n, bool& flag)
	{
		// 如果到达终点
		if (x == n - 1 && y == n - 1)
		{
			// 压栈
			path.push({x, y});
			// 标识符
			flag = false;
			return;
		}
		// 避免出现下一个位置直接回退,造成不必要的时间浪费
		arr[x][y] = -1;
		for (int i = 0; i < 4 && flag; ++i)
		{
			// 下一坐标计算
			int nx = x + dx[i];
			int ny = y + dy[i];
			// 判断下一坐标是否可走
			if (nx >= 0 && nx < n && ny >= 0 && ny < n && arr[nx][ny] == 0)
			{
				dfs(arr, nx, ny, n, flag);
			}
		}
		// 找到路径,递归回来直接压栈
		if (!flag)
		{
			path.push({ x, y });
		}

	}
};

void display(stack<pair<int, int>> path)
{
	if (!path.empty())	//找到路径
	{	
		int i = 0;
		while (!path.empty())
		{
			auto cops = path.top();
			cout << '[' << cops.first << "," << cops.second << "]--";
			if ((++i) % 4 == 0)
			{
				cout << endl;
			}
			path.pop();
		}
		cout << "END" << endl;
	}
	else
	{
		cout << "no path" << endl; //找不到路径输出no path
	}
}


int main(int argc, char** argv)
{
	int t;
	cin >> t;
	while (t--)
	{
		int n;
		cin >> n;
		int** arr = new int*[n];
		for (int i = 0; i < n; ++i)
		{
			arr[i] = new int[n];
			for (int j = 0; j < n; ++j)
			{
				cin >> arr[i][j];
			}
		}
		Solution sol;
		auto path = sol.findpath(n, arr);
		display(path);
	}


	return 0;
}

F. DS栈—波兰式,逆波兰式

题目描述

表达式有三种表示方法,分别为:

前缀表示(波兰式):运算符+操作数1+操作数2

中缀表示:操作数1+运算符+操作数2

后缀表示(逆波兰式):操作数1+操作数2+运算符

例如:a +b * (c -d ) - e/f

波兰式:-+a*b-cd/ef (运算符在操作数的前面,用递归计算波兰式)

中缀式:a+b*c-d-e/f

逆波兰式:abcd-*+ef/- (运算符在操作数的后面,用栈计算逆波兰式)

​ 中缀表示就是原表达式去掉扣号。

根据表达式求波兰式、逆波兰式都是教材第三章表达式求值的思想。

求波兰式,需要操作数栈(注意不是计算结果入栈,有计算式入栈),运算符栈。区别在于从后往前扫描表达式,‘(’ 换成’)’,’('换成‘)’。栈顶运算符优先级>新读入运算符优先级出栈,教材第三章表3.1中的相同运算符优先级>(从左往右计算)改为<,例如栈顶为‘+‘,新读入的为‘+’,则栈顶优先级<新读入的优先级。

求逆波兰式,只需要运算符栈。操作数直接输出,操作符按表3.1优先级顺序出栈,输出。

​ 输入表达式,求其波兰式和逆波兰式。

输入

测试次数

每组测试数据一行,一个合法表达式

输出

对每组测试数据,输出两行

第一行,表达式的波兰表示

第二行,表达式的逆波兰表示

不同组测试数据间以空行分隔。

输入样例
2
4+2*3-10/5
12+3*5+(2+10)*5 

输出样例
- + 4 * 2 3 / 10 5
4 2 3 * + 10 5 / -

+ + 12 * 3 5 * + 2 10 5
12 3 5 * + 2 10 + 5 * +

编码思路

  1. 存储输入数据采用字符串
  2. 波兰式,我们需要两个栈,一个用于存储操作符,一个用于存储操作数(其实最终还是需要存储操作符)
  3. 波兰式采用逆序遍历,这是波兰式,也就是前缀式的原因,需要将操作符先输出,所以最终需要将操作符栈的元素出栈压栈给操作数栈。
  4. 波兰式,对于遍历到的操作符,如果优先级低于栈顶,需要将栈顶出栈再入栈,保证最终的输出符合前缀式。
  5. 对于括号,括号内的优先级是最高的,但是最终输出并不需要括号,所以操作数并不需要怎么管,括号内的操作符在遍历完括号操作符(一对)之后则需要将括号内的操作符出栈。
  6. 逆波兰式,采取正向遍历即可,而且不需要使用操作数栈,直接输出,而操作符栈则需要存储。
  7. 由于遍历的顺序不同,所以需要对于运算符的优先级采取不同的判断。
  8. 注意字符串的拼接细节,由于C++的特性,不能将字符串常量直接和char拼接。

Code(C++)

#include<iostream>
#include<string>
#include<stack>
#include<sstream>
using namespace std;

class Solution
{
private:
	stack<string> nums;		// 存储操作数, 实际上会存储操作符的
	stack<char> op;			// 存储操作符

	// 逆向遍历时判断操作符优先级
	bool opcmpRight(char op1, char op2)
	{
		// + - 的时候,只需要判断栈顶是否同级,逆序,同级新的优先级更高
		if (op1 == '+' || op1 == '-')
		{
			return op2 == '+' || op2 == '-';
		}
		// * / 直接入栈
		return true;
	}

	// 正向遍历时判断操作符优先级
	bool opcmpLeft(char op1, char op2)
	{
		// 正向的时候,同级新的优先级会低
		if (op1 == '*' || op1 == '/')
		{
			return op2 != '*' && op2 != '/';
		}
		return false;
	}
public:
	// 求解波兰式
	string poland(string s)
	{
		int len = s.length();
		// 逆序遍历字符串
		for (int i = len - 1; i >= 0; --i)
		{
			// 数字存栈
			if (s[i] >= '0' && s[i] <= '9')
			{
				// 字符串拼接
				string num = "";
				num += s[i];
				// 判断前置是否是数字
				while (i - 1 >= 0 && s[i - 1] >= '0' && s[i - 1] <= '9')
				{
					num = s[--i] + num;
				}
				// 将数字入栈
				nums.push(num);
			}
			// 操作符
			// 判断栈是否空,或者有右括号
			else if (op.empty() || s[i] == ')' || op.top() == ')')
			{
				op.push(s[i]);
			}
			// 识别到左括号,此时需要将括号内的操作符输出
			else if (s[i] == '(')
			{
				// 操作数栈存储括号内的操作符
				while (!op.empty() && op.top() != ')')
				{
					string ops = "";
					ops += op.top();
					nums.push(ops);
					op.pop();
				}
				// 右括号出栈
				op.pop();
			}
			// 优先级高于栈顶,无脑入栈
			else if (opcmpRight(s[i], op.top()))
			{
				op.push(s[i]);
			}
			// 优先级低于栈顶
			else
			{
				// 操作符出栈并存入操作数栈
				while (!op.empty() && !opcmpRight(s[i], op.top()))
				{
					string ops = "";
					ops += op.top();
					nums.push(ops);
					op.pop();
				}
				// 压栈
				op.push(s[i]);
			}
		}
		// 将最终操作符栈压入操作数栈中
		while (!op.empty())
		{
			string ops = "";
			ops += op.top();
			nums.push(ops);
			op.pop();
		}
		// 得出答案
		string ans = "";
		while (!nums.empty())
		{
			ans += nums.top() + (nums.size() > 1 ? " " : "");
			nums.pop();
		}
		return ans;
	}

	// 求解逆波兰式
	string antiPoland(string s)
	{
		int len = s.size();
		string ans = "";
		// 正向遍历
		for (int i = 0; i < len; ++i)
		{
			// 数字直接加入答案
			if (s[i] >= '0' && s[i] <= '9')
			{
				ans += s[i];
				while (i + 1 < len && s[i + 1] >= '0' && s[i + 1] <= '9')
				{
					ans += s[++i];
				}
				ans += " ";
			}
			// 操作符的情况则需要和波兰式类似存栈
			// 栈空,左括号
			else if (op.empty() || s[i] == '(' || op.top() == '(')
			{
				op.push(s[i]);
			}
			// 右括号则需要输出括号内的操作符
			else if (s[i] == ')')
			{
				while (!op.empty() && op.top() != '(')
				{
					ans += op.top();
					ans += " ";
					op.pop();
				}
				op.pop();
			}
			// 判断优先级
			else if (opcmpLeft(s[i], op.top()))
			{
				op.push(s[i]);
			}
			else
			{
				while (!op.empty() && !opcmpLeft(s[i], op.top()))
				{
					ans += op.top();
					ans += " ";
					op.pop();
				}
				op.push(s[i]);
			}
		}
		// 将栈中输出到答案中
		while (!op.empty())
		{
			ans += op.top();
			ans += op.size() > 1 ? " " : "";
			op.pop();
		}
		return ans;
	}
};

int main(int argc, char** argv)
{
	int t;
	cin >> t;
	while (t--)
	{
		Solution sol;
		string s;
		cin >> s;
		cout << sol.poland(s) << endl;
		cout << sol.antiPoland(s) << endl << endl;
	}

	return 0;
}

后话

关于波兰式的问题,有些题目可能会有更多的操作符以及变量,其实后者只需要在比较优先级函数更改即可,前者在遍历的时候加一些语句也可以实现。

有机会的话,会专门写一篇细谈波兰之类的栈的blog。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值