当前位置: 首页 > article >正文

数据结构-栈和队列的应用

目录

  • 前言
  • 一、栈的应用
  • 二、队列的应用(农夫过河问题)
    • 2.1 问题描述
    • 2.2 算法选择
    • 2.3 算法精化
    • 2.4 算法实现
    • 2.5 问题结果
  • 总结

前言

本篇文章使用两个例子说明栈和队列的应用,
对于迷宫问题,使用栈实现深度优先策略解决迷宫问题;
对于农夫过河问题,使用队列实现广度优先策略解决农夫过河问题。

一、栈的应用

二、队列的应用(农夫过河问题)

2.1 问题描述

一个农夫带着一只狼、一只羊和一颗白菜,身处河的南岸。农夫要把这些东西全部运到河的北岸。问题是农夫只有一条小船,船小到只能容下农夫和一件物品,当然,船只有农夫能撑。另外,因为狼能吃羊,而羊能吃白菜,所以,农夫不能留下羊和狼或者羊和白菜单独在河一边,自己离开。好在狼属于食肉动物,它不吃白菜。请问农夫该采取什么方案,才能将所有的东西安全运过河呢?

2.2 算法选择

求解这个问题的最简单的方法是逐步进行试探。每一步都在前一步选择基础上搜素下一步的所有可能的状态。用计算机实现上述系统搜索过程,可以采用两种不同的策略,一种是广度优先搜索(breadth first);另一种是深度优先搜索(depth first)。实现广度优先搜索的工具是队列;实现深度优先搜索的工具是栈。本节讨论队列的应用,所以重点介绍广度优先搜索策略。
广度优先搜索的思想就是,在搜索过程中,总是首先搜索下面一步的所有可能情况,然后再进一步考虑更后面的各种情况。要实现广度优先搜索,一般都采用队列作为辅助结构,把每一步所有可能达到的状态都列举出来,放在这个队列中,然后顺序取出来分别进行处理,在处理中又再把下一步的情况全部放在队列里。由于队列的操作原则是先进先出,所以,只有在前一步的所有情况都处理完后,才能开始后面一步各情况的处理。
以遍历二叉树为例,使用广度优先搜索策略,进行层序遍历。
在这里插入图片描述

图2.1 广度优先遍历二叉树

2.3 算法精化

要模拟农夫过河问题,首先需要选择一个对问题中每个角色的位置进行描述的方法。一个很方便的方法是用4位二进制数顺序分别表示农夫、狼、白菜和羊的位置。例如,用0表示农夫或某物品在河的南岸,1表示在河的北岸。因此,整数5(其二进制表示为0101)表示农夫和白菜在河的南岸,而狼和羊在北岸。这时,农夫不在,因此狼会把羊吃掉,所以是一种不安全的状态。问题的初始状态是整数0(其二进制为0000);而问题的终结状态是整数15(其二进制表示为1111)。
在这里插入图片描述

图2.2 二进制位置表示图

用整数location表示用上述方法描述的状态,可以用下面的4个函数从上述状态得到每个角色所在位置的代码。函数返回值为真(1)表示农夫或物品的位置在河的北岸,否则在南岸。

//农夫过河问题
//个体判断函数
/*	四位二进制数分别表示农夫,狼,白菜,羊的位置
	0 表示在南岸  1 表示在北岸
	初始状态 location = 0000(二进制) 表示农夫,狼,白菜,羊都位于河的南岸
	使用按位 & 操作,取出每个位置的信息,例如 location & 0x08 取出农夫的位置信息
*/

//取出农夫位置信息
int farmer(int location)
{
	return (0 != (location & 0x08));
}

//取出狼位置信息
int wolf(int location)
{
	return (0 != (location & 0x04));
}

//取出白菜位置信息
int cabbage(int location)
{
	return (0 != (location & 0x02));
}

//取出羊位置信息
int goat(int location)
{
	return (0 != (location & 0x01));
}

此外,还应该分析问题中的所有角色构成的状态,确定其中那些状态是安全的,哪些是不安全的。因为,单独留下白菜和羊或单独留下狼和羊在某一岸是不安全的,所以安全状态的判断可以使用下面的函数实现。

//安全状态的判断函数
/*
	不能单独留下狼和羊,例如 location = 0101 是一种不安全的状态
	不能单独留下白菜和羊,例如 location = 1100 是一种不安全的转态
	安全返回 1
	不安全返回 0
*/

int safe(int location)
{
	//判断羊和白菜是否单独
	if ((goat(location) == cabbage(location)) && (farmer(location) != goat(location)))
	{
		return 0;
	}

	//判断狼和羊是否单独
	if ((wolf(location) == goat(location)) && (farmer(location) != goat(location)))
	{
		return 0;
	}
	return 1;   //其他状态安全
}

2.4 算法实现

完成了上面的准备工作后,现在的问题变成:从初始状态二进制0000出发,寻找一种全部由安全状态构成的、能够实现的状态变迁序列(序列中的每状态都可以从前一状态通过农夫带东西划船过河到达),到达最终状态二进制1111.为避免不必要的重复,在序列中不应该出现重复的状态。
根据广度优先搜索的思想,算法中需要使用一个整数队列moveTo,把搜索过程中每一步所有可能到达的状态都保存起来。队列中的每个元素表示可以安全到达的中间状态。
另外,使用一个整数数组rooute记录已被访问过的各个状态,以及已被发现的能够到达这些状态的前驱状态。由于在这个问题中需要列举的所有状态(二进制0000~1111)一共16种,所以route数组只需要使用16个元素。route的每个元素初始化为-1,每当在队列中加入一个新的状态时,就把route中以该状态作下标的元素的值改为达到这个状态的前一转态的下标值。所以数组的第i个元素不仅记录了状态i是否已经被访问过,同时对于以及被访问过的状态,还保存了这个状态的前驱状态下标。算法结束后,可以利用route数组元素的值生成一个正确的路径。
在这里插入图片描述

图2.3 route数组

代码实现如下:

//农夫问题求解

/*
	从初始状态二进制0000出发,寻找一种全部由安全状态构成、能够实现的状态变迁序列
	(序列中的每个状态都可以从前一状态通过农夫带东西划船过河到达)
	到达最终的状态1111。为避免不必要的重复,在序列中不应该出现重复的状态

	整数队列moveTo:把搜索过程中每一步所有可能到达的状态都保存起来。队列中的每个元素表示
	可以安全到达的中间状态
	整数数组route:用于记录已被访问过的各个状态,以及已被发现的能够到达这些状态的前驱状态
	由于在这个问题中需要列举的状态(二进制0000~1111)一共16种,则数组长度大小为16
	每个数组的元素值初始化为-1
	每当在队列中加入一个新的状态时,就把route中以该状态做下标的元素的值改为达到这个状态的下标值
	数组的第i个元素不仅记录状态i是否已被访问过,同时对于已被访问过的状态,还保存了这个状态的前驱状态
	算法结束后,可以利用route数组元素的值生成一个正确的状态路径
*/

void farmerProblem()
{
	int movers, location, newlocation;
	int route[16];				//用于记录已考虑的状态路径
	struct SeqQueue moveTo;		//用于记录可以安全到达的中间状态
	initSeqQueue(&moveTo);		//初始化队列
	enSeqQueue(&moveTo, 0x00);	//初始状态二进制0000入队
	for (int i = 0; i < 16; i++)//初始化数组route
	{
		route[i] = -1;
	}
	route[0] = 0;
	while (!isEmptySeqQueue(&moveTo) && (route[15] == -1))
	{
		getQElemSeqQueue(&moveTo, &location);	//取出队头状态为当前状态
		QElemType e;
		deSeqQueue(&moveTo, &e);
		//循环值依次表示羊、白菜、狼、农夫的移动情况
		//movers的值用二进制表示依次为0001(羊)、0010(白菜)、0100(狼)、1000(农夫)
		for (movers = 1; movers <= 8; movers <<= 1)
		{
			if ((0 != (location & 0x08)) == (0 != (location & movers))) //农夫与移动的物品在同一岸
			{
				// 0x08 | movers 得到的结果表明农夫或物品应该在船上,物品无法单独过河
				// location ^ (0x08 | movers) 把坐船过河的农夫与物品的状态翻转
				newlocation = location ^ (0x08 | movers);			//计算新状态
				if (safe(newlocation) && route[newlocation] == -1)	//新状态安全且未处理
				{
					route[newlocation] = location;					//记录旧状态且作为新状态的前驱
					enSeqQueue(&moveTo, newlocation);				//将新状态入队
				}
			}
		}
	}

	if (route[15] != -1)
	{
		printf("The reverse path is :\n");
		for (location = 15; location >= 0; location = route[location])
		{
			printf("The location is : %d\n", location);
			if (location == 0)
			{
				exit(0);
			}
		}
	}
	else
	{
		printf("No solution\n");
	}
}

注意:这里实现队列的代码已省略,感兴趣可查看文章:https://blog.csdn.net/pyc68/article/details/145093486?spm=1001.2014.3001.5501

算法开始时,把初始状态为0(表明人和物都在南岸)放入队列中。while循环时,只要队列不为空便取出队头元素,for循环用于列举所有可以移动的角色(包括农夫本身,用movers表示),for循环的增量表达式里使用了左移位操作,循环值依次为1、2、4、8,分别表示羊、白菜、狼和农夫的移动情况。由于在每一次移动时,农夫都必须改变状态,所以只有农夫与被移动的东西在同一岸时,农夫才可以将其带走。当然,农夫可以什么都不带,单独过河。将变量movers与二进制0x08进行按位异或运算,所得结果为1,表明相应的农夫或物品应该在船上,再将原有状态与这个结果进行一次按位异或运算,得到移动后的一个新状态。最后检验新状态是否安全,是否未被处理过。如果成立,就确认这个状态转换可行(把这个状态放入队列中,并且修改route数组的值)

2.5 问题结果

图2.4标出了送入队列的各个状态(位置)和广度优先搜索的顺序编号。
通过图2.4得出一条从0000到达1111的路径:
0000-1001:农夫把羊从南岸带到北岸
1001-0001:农夫独自从北岸回到南岸
0001-1011:农夫把白菜从南岸带到北岸
1011-0010:农夫把羊从北岸带回南岸
0010-1110:农夫把狼从南岸带到北岸
1110-0110:农夫独自从北岸回到南岸
0110-1111:农夫把羊从南岸带到北岸

在这里插入图片描述

图2.4 广度优先搜索的结果和顺序图

总结

农夫过河问题完整代码:https://gitee.com/PYSpring/data-structure/tree/master/queue_code


http://www.kler.cn/a/542675.html

相关文章:

  • vite + axios 代理不起作用 404 无效
  • Visual Studio 使用 “Ctrl + /”键设置注释和取消注释
  • STM32_USART通用同步/异步收发器
  • 网络工程师 (31)VLAN
  • 示例代码:C# MQTTS双向认证(客户端)(服务器EMQX)
  • 华为Mate 70 Pro或推出全新版本
  • 亚马逊云科技Bedrock知识库自定义语义搜索配置教程
  • SpringBoot项目练习
  • HTML 简介
  • ASP.NET Core的贫血模型与充血模型
  • JavaScript系列(69)--安全编程技术详解
  • DeepSeek模型架构及优化内容
  • Leetcode 3448. Count Substrings Divisible By Last Digit
  • 更新无忧:用 Docker 数据卷确保 Open WebUI 数据持久化
  • java项目部署到linux读取properties中文乱码
  • 超全前端面试(全!全!全!!!)
  • LeetCode 每日一题 2025/2/3-2025/2/9
  • 2024.1.2版本Android Studio gradle下载超时问题处理
  • python基础入门:6.2JSON与CSV数据处理
  • SkyWalking 10.1.0 实战:从零构建全链路监控,解锁微服务性能优化新境界
  • qt QCommandLineOption 详解
  • 蓝桥杯算法日记|2.11二分算法
  • 【目标检测xml2json】label从VOC格式xml文件转COCO格式json文件
  • PostgreSQL 数据库压力测试指南
  • 普通用户授权docker使用权限
  • docker和docker compose版本太低问题的解决方案