数据结构之栈
1. 什么是栈?
栈是一种常见的数据结构,它遵循先进后出(LIFO)的原则。栈可以用来解决很多实际问题,比如函数调用、表达式求值、括号匹配等。
一般使用数组、链表实现栈
2. 特点
- 栈是一种线性数据结构,由一系列元素组成。
- 栈的插入和删除操作只能在栈的顶部进行。
- 栈的顶部元素是最后一个插入的元素,也是唯一可以访问的元素。
3. 操作
栈支持以下几种操作:
- Push:将元素插入到栈的顶部。
- Pop:将栈顶部的元素删除,并返回该元素的值。
- Peek:返回栈顶部元素的值,但不删除该元素。
- isEmpty:检查栈是否为空。
- size:返回栈中元素的个数。
4. 最佳实践
在使用栈的时候,有一些最佳实践可以帮助我们更好地利用它:
- 在使用栈之前,先确定栈的大小,以避免栈溢出的问题。
- 使用异常处理机制来处理栈溢出的情况。
- 在进行复杂的操作时,可以使用多个栈来简化问题。
5. 坑
在使用栈的过程中,有一些常见的坑需要注意:
- 忘记检查栈是否为空就进行 Pop 操作,可能会导致栈溢出异常。
- 没有正确处理运算符的优先级,可能会导致表达式求值结果错误。
- 在使用栈求解问题时,需要确保栈中元素的顺序和操作的顺序一致,否则可能会得到错误的结果。
6. 示例
6.1 计算表达式的值
1 def evaluate_expression(expression): 2 stack = [] # 使用数组/列表实现栈 3 operators = {'+': lambda x, y: x + y, '-': lambda x, y: x - y, '*': lambda x, y: x * y, '/': lambda x, y: x / y} 4 for char in expression.split(): 5 if char.isdigit(): 6 stack.append(int(char)) # 遇到数字就入栈 7 elif char in operators: # 遇到字符,请出栈,出2次即操作的2个数 8 operand2 = stack.pop() 9 operand1 = stack.pop() 10 result = operators[char](operand1, operand2) # 做运算操作 11 stack.append(result) # 将结果入栈 12 print(f"表达式{expression},栈:{stack}") # 表达式3 4 + 2 *,栈:[14] 13 return stack.pop() # 将结果出栈,最终列表只有1个元素即表达式结果 14 15 16 ''' 17 规律是: 18 1. 顺序从左向右; 19 2. 开头2个必须是数字,第3个是操作符,然后做运算,其结果与第4个数字(必须是数字)、第5个字符进行操作,依次类推 20 因此构造此表达是解题的关键 21 ''' 22 expression = "3 4 + 2 *" # 3和4做+操作,然后其结构7和2做乘法操作。 23 print(evaluate_expression(expression)) # 输出结果为 14
输出:
1 2 | 表达式 3 4 + 2 * ,栈:[ 14 ] 14 |
6.2 括号匹配
1 ''' 2 1. 题目: 栈是一个非常常用的数据结构,可以用来实现括号匹配。括号匹配是指检查一个字符串中的括号是否正确配对。 3 4 2. 实现思路 5 1. 遍历字符串中的每个字符。 6 2. 如果遇到左括号(如'('、'['、'{'),则将其压入栈中。 7 3. 如果遇到右括号(如')'、']'、'}'),则检查栈顶部的元素。 8 1. 如果栈为空或栈顶部的元素与当前右括号不匹配,则括号不匹配,返回 False。 9 2. 如果栈顶部的元素与当前右括号匹配,则将栈顶部的元素弹出。 10 4. 遍历完字符串后,检查栈是否为空。 11 1. 如果栈为空,则括号匹配,返回 True。 12 2. 如果栈不为空,则括号不匹配,返回 False。 13 14 ''' 15 16 17 def is_valid_parentheses(s): 18 if not (len(s) % 2 == 0 and len(s) > 0): # 判断是否可以数量上是否满足 19 return False 20 21 stack = [] # 列表实现栈 22 # 特点,匹配后半部分为key,前半部分为value(这样入栈只能是前半部分,总之栈里的元素与字典的key匹配,与字典的value相等)。 23 # 与栈顶元素比较是否匹配,匹配继续遍历,不匹配直接返回False 24 mapping = {')': '(', ']': '[', '}': '{'} 25 for char in s: 26 if char in mapping: 27 if not stack or stack.pop() != mapping[char]: 28 return False 29 else: 30 stack.append(char) 31 return not stack 32 33 34 s = "([{}])" 35 print(is_valid_parentheses(s)) # 输出结果为 True 36 37 s = "([{}]))" 38 print(is_valid_parentheses(s)) # 输出结果为 False
6.3 汉诺塔(Tower of Hanoi)
汉诺塔(Hanoi Tower)是一个经典的递归问题,它可以通过递归算法来解决。
在这个问题中,有三根柱子(通常称为A、B和C),其中一根柱子上有一组不同大小的圆盘,按照从大到小的顺序堆叠在柱子上。
任务是将所有的圆盘从一根柱子移动到另一根柱子,每次只能移动一个圆盘,并且大的圆盘不能放在小的圆盘上面。
规则:
- 一次只能移动一个盘子。
- 永远不能将一个较大的盘子放在一个较小的盘子之上。
python语言:注意声明hanoi函数、调用hanoi函数参数的顺序
def hanoi(n, source, auxiliary, target): if n == 1: print(f"移动盘子 1 从 {source} 到 {target}") return hanoi(n - 1, source, target, auxiliary) print(f"移动盘子 {n} 从 {source} 到 {target}") hanoi(n - 1, auxiliary, source, target) # 在这里,n 表示盘子的数量,source 是初始柱子,auxiliary 是辅助柱子,target 是目标柱子 n = 3 # 你可以根据需要修改盘子的数量 hanoi(n, 'A', 'B', 'C')
输出:
1 2 3 4 5 6 7 | 移动盘子 1 从 A 到 C 移动盘子 2 从 A 到 B 移动盘子 1 从 C 到 B 移动盘子 3 从 A 到 C 移动盘子 1 从 B 到 A 移动盘子 2 从 B 到 C 移动盘子 1 从 A 到 C |
Java语言
public class HanoiTower { public static void main(String[] args) { int numDiscs = 3; // 圆盘的数量 char source = 'A'; // 起始柱子 char auxiliary = 'B'; // 辅助柱子 char target = 'C'; // 目标柱子 solveHanoi(numDiscs, source, auxiliary, target); } public static void solveHanoi(int n, char source, char auxiliary, char target) { if (n == 1) { // 如果只有一个圆盘,直接将其从起始柱子移动到目标柱子 System.out.println("Move disk 1 from " + source + " to " + target); } else { // 递归地将 n-1 个圆盘从起始柱子移动到辅助柱子 solveHanoi(n - 1, source, target, auxiliary); // 移动剩下的一个圆盘到目标柱子 System.out.println("Move disk " + n + " from " + source + " to " + target); // 递归地将 n-1 个圆盘从辅助柱子移动到目标柱子 solveHanoi(n - 1, auxiliary, source, target); } } }
输出:
1 2 3 4 5 6 7 | Move disk 1 from A to C Move disk 2 from A to B Move disk 1 from C to B Move disk 3 from A to C Move disk 1 from B to A Move disk 2 from B to C Move disk 1 from A to C |
最佳实践和详解:
-
使用递归:递归是解决汉诺塔问题的最佳方法,因为它自然地反映了问题的分治性质。通过递归,你可以将复杂的问题分解成更小的子问题,然后逐步解决它们。
-
参数说明:在递归函数中,参数
n
表示要移动的圆盘数量,source
表示起始柱子,auxiliary
表示辅助柱子,target
表示目标柱子。 -
终止条件:递归函数中的终止条件是
n
等于1,即只有一个圆盘时直接移动到目标柱子。 -
递归调用:在递归步骤中,先将 n-1 个圆盘从起始柱子移动到辅助柱子,然后将一个圆盘从起始柱子移动到目标柱子,最后将 n-1 个圆盘从辅助柱子移动到目标柱子。这个过程保证了大圆盘永远不会放在小圆盘上面。
-
输出移动步骤:在每次移动圆盘时,输出移动的详细步骤,以便了解整个过程。
javascirpt语言
1 function hanoiTower(discs, source, auxiliary, target) { // 顺序是 源柱子、辅助柱子、目标柱子 2 if (discs === 1) { 3 console.log(`Move disc 1 from ${source} to ${target}`); 4 return; 5 } 6 7 hanoiTower(discs - 1, source, target, auxiliary); // 顺序是:源柱子、目标柱子、辅助柱子 8 console.log(`Move disc ${discs} from ${source} to ${target}`); 9 hanoiTower(discs - 1, auxiliary, source, target); // 顺序是:辅助柱子、源柱子、目标柱子 10 } 11 12 const numDiscs = 3; // 设置圆盘的数量 13 const sourcePeg = 'A'; // 源柱子 14 const auxiliaryPeg = 'B'; // 辅助柱子 15 const targetPeg = 'C'; // 目标柱子 16 17 hanoiTower(numDiscs, sourcePeg, auxiliaryPeg, targetPeg);
输出:
1 2 3 4 5 6 7 | Move disc 1 from A to C Move disc 2 from A to B Move disc 1 from C to B Move disc 3 from A to C Move disc 1 from B to A Move disc 2 from B to C Move disc 1 from A to C |
6.4 老鼠走迷宫
java语言:链表实现的栈
1. 链表实现栈
1 class Node 2 { 3 int x; // 坐标x 4 int y; // 坐标y 5 Node next; 6 public Node(int x,int y) 7 { 8 this.x=x; 9 this.y=y; 10 this.next=null; 11 } 12 } 13 14 /** 15 * 使用链表实现栈的功能 16 */ 17 public class TraceRecord 18 { 19 public Node first; 20 public Node last; 21 public boolean isEmpty() 22 { 23 return first==null; 24 } 25 public void insert(int x,int y) 26 { 27 Node newNode=new Node(x,y); 28 if(this.isEmpty()) 29 { 30 first=newNode; 31 last=newNode; 32 } 33 else 34 { 35 last.next=newNode; 36 last=newNode; 37 } 38 } 39 40 public void delete() 41 { 42 Node newNode; 43 if(this.isEmpty()) 44 { 45 System.out.print("[�����Ѿ�����]\n"); 46 return; 47 } 48 newNode=first; 49 while(newNode.next!=last) 50 newNode=newNode.next; 51 newNode.next=last.next; 52 last=newNode; 53 54 } 55 }
2. 老鼠走迷宫
1 // 老鼠走迷宫 2 3 import java.io.IOException; 4 5 public class MouseGo { 6 public static int ExitX = 8; //定义出口的X坐标在第8行 7 public static int ExitY = 10; //定义出口的Y坐标在第10列 8 //声明迷宫数组10 X 12 9 public static int[][] MAZE = {{1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1}, 10 {1, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1}, 11 {1, 1, 1, 0, 1, 1, 0, 0, 0, 0, 1, 1}, 12 {1, 1, 1, 0, 1, 1, 0, 1, 1, 0, 1, 1}, 13 {1, 1, 1, 0, 0, 0, 0, 1, 1, 0, 1, 1}, 14 {1, 1, 1, 0, 1, 1, 0, 1, 1, 0, 1, 1}, 15 {1, 1, 1, 0, 1, 1, 0, 1, 1, 0, 1, 1}, 16 {1, 1, 1, 1, 1, 1, 0, 1, 1, 0, 1, 1}, 17 {1, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1}, 18 {1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1}}; 19 20 public static void main(String args[]) throws IOException { 21 int i, j, x, y; 22 TraceRecord path = new TraceRecord(); 23 x = 1; // 1,1 坐标是入口 24 y = 1; 25 // 0. 打印迷宫 26 System.out.print("[迷宫的路径(0标记的部分)]\n"); 27 for (i = 0; i < 10; i++) { 28 for (j = 0; j < 12; j++) 29 System.out.print(MAZE[i][j]); 30 System.out.print("\n"); 31 } 32 // 从1,1到出口(8,10)尝试路径 33 while (x <= ExitX && y <= ExitY) { 34 MAZE[x][y] = 2; // 设置2表示可通行 35 if (MAZE[x - 1][y] == 0) // 向上,0表示可行 36 { 37 x -= 1; // 更新当前坐标 38 path.insert(x, y);// 当前位置入栈 39 } else if (MAZE[x + 1][y] == 0) // 向下,0表示可行 40 { 41 x += 1; 42 path.insert(x, y); 43 } else if (MAZE[x][y - 1] == 0) // 向左,0表示可行 44 { 45 y -= 1; 46 path.insert(x, y); 47 } else if (MAZE[x][y + 1] == 0) // 向右,0表示可行 48 { 49 y += 1; 50 path.insert(x, y); 51 } else if (chkExit(x, y, ExitX, ExitY) == 1) // 检查是否到底出口,到底直接退出 52 break; 53 else { 54 MAZE[x][y] = 2; // 走过的路径 55 path.delete(); // 删除节点 56 x = path.last.x; // 返回重试 57 y = path.last.y; 58 } 59 } 60 System.out.print("[老鼠走过的路径(2标记的部分)]\n"); 61 for (i = 0; i < 10; i++) { 62 for (j = 0; j < 12; j++) 63 System.out.print(MAZE[i][j]); 64 System.out.print("\n"); 65 } 66 } 67 68 // 检查是否到底出口 69 public static int chkExit(int x, int y, int ex, int ey) { 70 if (x == ex && y == ey) { 71 if (MAZE[x - 1][y] == 1 || MAZE[x + 1][y] == 1 || MAZE[x][y - 1] == 1 || MAZE[x][y + 1] == 2) 72 return 1; 73 if (MAZE[x - 1][y] == 1 || MAZE[x + 1][y] == 1 || MAZE[x][y - 1] == 2 || MAZE[x][y + 1] == 1) 74 return 1; 75 if (MAZE[x - 1][y] == 1 || MAZE[x + 1][y] == 2 || MAZE[x][y - 1] == 1 || MAZE[x][y + 1] == 1) 76 return 1; 77 if (MAZE[x - 1][y] == 2 || MAZE[x + 1][y] == 1 || MAZE[x][y - 1] == 1 || MAZE[x][y + 1] == 1) 78 return 1; 79 } 80 return 0; 81 } 82 }
输出:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | [迷宫的路径(0标记的部分)] 111111111111 100011111111 111011000011 111011011011 111000011011 111011011011 111011011011 111111011011 110000001001 111111111111 [老鼠走过的路径(2标记的部分)] 111111111111 122211111111 111211222211 111211211211 111222211211 111211011211 111211011211 111111011211 110000001221 111111111111 |
python语言:非栈
1 ''' 2 老鼠走迷宫是一个经典的计算机科学和算法问题,通常使用深度优先搜索(DFS)或广度优先搜索(BFS)等算法来解决。 3 下面我将使用Python来实现一个简单的老鼠走迷宫的解决方案,采用深度优先搜索算法。 4 首先,我们需要一个迷宫的表示方法。通常,我们可以使用二维数组来表示迷宫,其中0表示可以通过的路径,1表示墙,2表示老鼠的路径,3表示已经探索过但无法通行的路径。 5 ''' 6 # 模拟迷宫 7 maze = [ 8 [0, 1, 0, 0, 0], 9 [0, 1, 0, 1, 0], 10 [0, 0, 0, 1, 0], 11 [1, 1, 1, 1, 0], 12 [0, 0, 0, 0, 0] 13 ] 14 15 16 def solve_maze(maze, start, end): 17 rows = len(maze) 18 cols = len(maze[0]) 19 20 def is_valid(x, y): # 验证是否可行 21 # return 0 <= x < rows and 0 <= y < cols and maze[x][y] == 0 22 return rows > x >= 0 == maze[x][y] and 0 <= y < cols 23 24 def dfs(x, y): 25 if x == end[0] and y == end[1]: 26 return True # 已经到达终点 27 if is_valid(x, y): 28 maze[x][y] = 2 # 将当前位置标记为老鼠的路径 29 30 # 尝试向四个方向移动 31 if (dfs(x + 1, y) or 32 dfs(x - 1, y) or 33 dfs(x, y + 1) or 34 dfs(x, y - 1)): 35 return True 36 37 # 如果四个方向都无法通行,将当前位置标记为已探索过但无法通行 38 maze[x][y] = 3 39 return False 40 41 return False 42 43 if dfs(start[0], start[1]): 44 return maze # 返回包含老鼠路径的迷宫 45 else: 46 return None # 如果没有找到路径,返回None 47 48 49 # 测试 50 start = (0, 0) 51 end = (4, 4) 52 result = solve_maze(maze, start, end) 53 54 if result: 55 for row in result: 56 print(row) 57 else: 58 print("没有找到路径")
输出:
[2, 1, 2, 2, 2] [2, 1, 2, 1, 2] [2, 2, 2, 1, 2] [1, 1, 1, 1, 2] [0, 0, 0, 0, 0]
javascript语言:用深度优先搜索(DFS)算法来解决迷宫问题
函数接受一个迷宫矩阵作为输入,矩阵中包含了障碍物(1)、可通行路径(0)和已访问路径(2)。函数的目标是找到从迷宫的起点到终点的路径,并返回包含路径的迷宫矩阵,或者如果没有可行路径,则返回 "No solution found."。
1 function solveMaze(maze) { 2 const rows = maze.length; 3 const cols = maze[0].length; 4 const directions = [ 5 [-1, 0], // 上 6 [1, 0], // 下 7 [0, -1], // 左 8 [0, 1] // 右 9 ]; 10 11 function isSafe(x, y) { 12 return x >= 0 && x < rows && y >= 0 && y < cols && maze[x][y] === 0; 13 } 14 15 function dfs(x, y) { 16 maze[x][y] = 2; // 标记当前位置为已访问 17 if (x === rows - 1 && y === cols - 1) { 18 // console.log(x,rows-1) 19 // console.log(y,cols-1) 20 return true; // 找到了出口 21 } 22 23 24 for (const [dx, dy] of directions) { 25 const newX = x + dx; 26 const newY = y + dy; 27 28 if (isSafe(newX, newY)) { 29 if (dfs(newX, newY)) { 30 return true; // 如果从新位置找到出口,则返回true 31 } 32 } 33 } 34 35 maze[x][y] = 0; // 如果当前路径不通,则回溯 36 return false; 37 } 38 39 if (dfs(0, 0)) { // 从0,0开始 40 return maze; // 返回包含路径的迷宫 41 } else { 42 return "No solution found."; 43 } 44 } 45 46 const maze = [ 47 [0, 1, 0, 0, 0], 48 [0, 1, 0, 1, 0], 49 [0, 0, 0, 0, 0], 50 [0, 1, 1, 1, 0], 51 [0, 0, 0, 1, 0] 52 ]; 53 54 const solvedMaze = solveMaze(maze); 55 if (typeof solvedMaze === "string") { 56 console.log(solvedMaze); // 输出 "No solution found." 57 } else { 58 for (const row of solvedMaze) { 59 console.log(row.join(" ")); 60 } 61 }
输出结果:
1 2 3 4 5 | 2 1 2 2 2 2 1 2 1 2 2 2 2 0 2 0 1 1 1 2 0 0 0 1 2 |
解释:
-
solveMaze(maze)
函数接受迷宫矩阵maze
作为输入参数。 -
定义了
rows
和cols
变量来获取迷宫的行数和列数。 -
directions
数组定义了四个方向,用于在迷宫中移动,分别是上、下、左、右。每个方向都表示为一个二维数组[dx, dy]
,其中dx
表示在行上的移动,dy
表示在列上的移动。 -
isSafe(x, y)
函数用于检查给定的坐标(x, y)
是否是一个安全的位置,即不超出迷宫边界并且没有障碍物(值为0表示可通行)。 -
dfs(x, y)
是深度优先搜索函数,它从坐标(x, y)
开始搜索路径。首先,它检查是否已经到达了终点(rows - 1, cols - 1)
,如果是,则返回true
表示找到了出口。然后,它将当前位置标记为已访问(值为2)。接下来,它遍历四个方向,检查每个方向上的新坐标(newX, newY)
是否是一个安全的位置,如果是,则递归调用dfs
函数。如果在任何一个方向上找到了出口,函数会返回true
,表示已经找到通往出口的路径。如果四个方向都没有找到通往出口的路径,函数会将当前位置标记为可通行(值为0),然后返回false
,表示当前路径不通,需要回溯。 -
在函数的最后,如果
dfs(0, 0)
返回true
,则说明找到了通往出口的路径,函数返回包含路径的迷宫矩阵;否则,返回 "No solution found." 表示没有可行路径。 -
最后,代码检查返回值的类型,如果是字符串,则输出 "No solution found.",否则,遍历迷宫矩阵并将每一行以空格分隔后输出,显示找到的路径。
在给定的示例迷宫中,如果有可行路径,那么函数将返回包含路径的迷宫矩阵,否则会输出 "No solution found."。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本
· C#/.NET/.NET Core优秀项目和框架2025年2月简报
· Manus爆火,是硬核还是营销?
· 终于写完轮子一部分:tcp代理 了,记录一下
· 【杭电多校比赛记录】2025“钉耙编程”中国大学生算法设计春季联赛(1)