【算法与数据结构基础】列表、栈、队列的原理及应用
背景
为什么要了解数据结构及算法?有个很恰当的比喻,如果把编程比作习武,会用哪种计算机语言相当于学会了招式,而学习算法则相当于在修炼内功。算法能帮我们优化程序占用的空间或消耗的时间,提高我们解决问题的效率。
1、列表
- C语言数组: C语言中列表(或叫数组)的定义方式为:int a[5],既指定了数据的类型,也指定了数组的长度。这对计算机是友好的,如此计算机只需要在内存中分配一个20 byte(即5个字节)长度的空间,并让a指向内存位置的首地址即可。
- python数组创建: python中列表的实现和C不同,一不需要指定数组长度,二不需要指定内容类型,一个python数组中可以放不同的内容,比如a = [1,2.5,‘abcd’]。我们知道不同类型的元素占内存空间大小是不一样的,那python怎么实现这种数组的创建呢?答案是存地址位置,虽然不同类型的值占内存大小不一样,但他们在内存中的地址位置是一样大的,python会通过开一部分内存来存内存地址,指向不同元素在内存中的位置。
- python数组append: 实现方式为先新开辟一个两倍于原数组的空间,把原数组中所有元素复制过来,然后在后面追加元素。平均时间复杂度为O(1)。
- python数组下标索引: 可以根据首地址位置加索引乘以单元素长度的计算直接找到对应元素,时间复杂度O(1)。
- python数组插入和删除: 为保证一定顺序,在中间插入和删除元素时,需要对其它元素的位置进行挪动。时间复杂度O(n)。
2、栈
- 定义: 只能再一端进行插入或删除操作的列表。
- 特点: 具有后进先出的特点,即LIFO(last in,first out)
- python中实现: python中的列表即可,进栈:append,出栈:pop,查看栈顶:li[-1]
- 应用: 检查括号匹配问题,像一般编译器都具有的功能,当代码中有一堆括号时,检查这些括号是否有正确匹配。思路:遇到左括号就进栈,遇右括号而且和栈顶的左括号相匹配,就让左括号出栈,最后栈空就表示能正确匹配,否则错误。
def stack_apply(st:str):
#用字典的方式来表示左右括号的对应关系
bracket = {'(':')', '[':']', '{':'}'}
li = []
for item in st:
#遇到左括号就进栈
if item in bracket.keys():
li.append(item)
#当栈空了还剩一个右括号,表示错误匹配,返回False
elif len(li)==0:
return False
#遇右括号而且和栈顶的左括号相匹配,就让左括号出栈
elif item == bracket[li[-1]]:
li.pop()
#遇到了右括号,但是和栈顶的左括号不匹配,直接返回False
else:
return False
#最后判断一下栈空了没有,空了就正确匹配,否则没有正确匹配
if len(li)==0:
return True
else:
return False
print(stack_apply('[]{([]])}'))
print(stack_apply('([)]'))
print(stack_apply('[])'))
print(stack_apply('{[]'))
print(stack_apply('{[()]}'))
3、队列
- 定义: 仅允许在列表的一端进行插入,另一端进行删除
- 性质: 先进先出,即FIFO(first in,first out)
- Python中的实现: collections.deque,进队列:append,出队列:popleft。
- 应用: linux中tail命令的实现:
from collections import deque
deque(open('test.txt','r'),5)
- 两个栈做队列: 思路:进队:1号栈进栈,出队:2号栈出栈,如果2号栈空,则把1号栈元素依次出栈进2号栈:
class queue_using_stack:
def __init__(self):
#初始化两个栈
self.stack1 = []
self.stack2 = []
#进队即1号栈进栈
def append(self,key):
self.stack1.append(key)
def pop(self):
#如果2号栈空,则先把1号栈里的元素依次出栈进2号栈
if not self.stack2:
while self.stack1:
self.stack2.append(self.stack1.pop())
#2号栈不空时,出队即2号栈出栈
return self.stack2.pop()
4、深度优先和广度优先
在讲解深度优先和广度优先之前,需要引入另一个栈和队列的应用问题,迷宫问题:
maze = [ #迷宫地图,1代表墙壁,0代表通路
[1,1,1,1,1,1,1,1,1,1],
[1,0,0,1,0,0,0,1,0,1],
[1,0,0,1,0,0,0,1,0,1],
[1,0,0,0,0,1,1,0,0,1],
[1,0,1,1,1,0,0,0,0,1],
[1,0,0,0,1,0,0,0,0,1],
[1,0,1,0,0,0,1,0,0,1],
[1,0,1,1,1,0,1,1,0,1],
[1,1,0,0,0,0,0,0,0,1],
[1,1,1,1,1,1,1,1,1,1]
]
我们用上述二维数组表示一个迷宫地图,其中1代表墙壁,0代表通路,尝试找到从起点(1,1)到终点(8.8)的可行路径。解决这个迷宫问题的两种思路,深度优先和广度优先,对应的就是栈和队列的应用。
- 深度优先: 思路:每一条可行路探索至无路可走时,返回到上一个岔路口,换条路探索,直至探索到终点:
#用一个数组表示四个行走方向
dirs = [
lambda x,y:(x-1,y), #左
lambda x,y:(x+1,y), #右
lambda x,y:(x,y-1), #上
lambda x,y:(x,y+1), #下
]
#x1,y1表示起点,x2,y2表示终点
def solve_maze_stack(x1, y1, x2, y2):
#用一个栈来存储行走路径,每走一步就进栈一个位置点
stack = []
#用数字2表示已经走过的路,把起点先标识为已走过
maze[x1][y1] = 2
#起点先进栈
stack.append((x1,y1))
while len(stack)>0:
#判断如果栈顶就是终点,表示走通了,打印栈里的元素即表示路径
if stack[-1] == (x2, y2):
print(stack)
return True
#遍历四个方向
for f in dirs:
#取出该方向的下一步位置
cur_point = maze[f(*stack[-1])[0]][f(*stack[-1])[1]]
#如果是没走过的通路
if cur_point==0:
#则标记为已走过并进栈
maze[f(*stack[-1])[0]][f(*stack[-1])[1]] = 2
stack.append(f(*stack[-1]))
break
#这个else是和for对应的,表示四个方向都无路可走了,就出栈最后一个位置节点
else:
stack.pop()
#栈空了仍未碰到终点,表示没有可行路,返回False
return False
solve_maze_stack(1,1,8,8)
这种思路即是深度优先,这样找出的路径不能保证是最短路径。
- 广度优先: 思路:用队列存储每个可行的岔路的最新位置点,一直探索下去,直至有个岔路先触达了终点,即找到了可行路径。
from collections import deque
def solve_maze_queue(x1, y1, x2, y2):
#创建一个队列,用来存每个岔路的最新位置点
q = deque()
#起点用2标记为已经走过
maze[x1][y1] = 2
#起点先进队,并用tuple中最后一个值记录上一步在trace数组中的index位置,方便回溯路径,起点用-1表示
q.append((x1,y1,-1))
#创建一个trace数组用来记录路径的
trace = []
#队不空时循环
while len(q)>0:
#出队队头位置点并记录为当前节点
cur_node = q.popleft()
#把当前节点记在路径trace里
trace.append(cur_node)
#判断如果走到了终点
if cur_node[:2] == (x2, y2):
#从trace的最后一个元素(即终点)开始
n = len(trace)-1
#遍历直至到起点(起点时n为-1)
while n>=0:
#打印路径位置点
print(trace[n])
#通过tuple中最后一个元素,追溯到上一步的位置点
n = trace[n][-1]
return True
#遍历四个方向
for f in dirs:
#获取到下一步位置点
next_node = f(*cur_node[:2])
#只要有路可走,就把每个可行路径的下个位置点入队,并把该位置点标记为2(已走过)
if maze[next_node[0]][next_node[1]]==0:
maze[next_node[0]][next_node[1]] = 2
q.append((next_node[0],next_node[1],len(trace)-1))
#队空了仍未到终点,表示没有通路,返回False
return False
solve_maze_queue(1,1,8,8)
广度优先搜索可以保证最终找到的路径一定是最短路径。
当然不仅仅是迷宫问题,基本所有问题用栈去解决即代表深度优先,用队列去解决即代表广度优先,比如二叉树的遍历,我们后面的文章会提到。
**内容整理自网络课程算法与数据结构LeetCode编程