数据结构
- 数据结构是指相互之间存在着一种或多种关系的数据元素的集合和该集合中数据元素之间的关系组成。
- 简单来说,数据结构就是设计数据以何种方式组织并存储在计算机当中
- 比如:列表,集合,字典等都是一种数据结构
- N.Wirth: “程序=数据结构+算法”
数据结构的分类
- 数据结构按照其逻辑结构可分为线性结构,树结构,图结构
- 线性结构:数据结构中的元素存在一对一的相互关系
- 树结构:数据结构中的元素存在一对多的相互关系
- 图结构:数据结构中的元素存在多对多的相互关系
线性结构–列表
列表中元素存储方式:顺序存储,一块连续的内存
列表的基本操作:按照下表查找,插入元素,删除元素
这些操作的时间复杂度: 查找O(1), 插入O(n), 删除O(n)
python的列表是如何实现的?
列表查找:如果一个列表的首地址为100,要想找到a[2],那么直接找100+2*4的地址所储存的数据
32位机器上:一个整数占4个字节,一个地址占4个字节:地址大小都是固定的
python上数组与列表有两点不同:
- 数组元素类型要相同
- 数组长度固定
python上列表:
python列表中存的是地址而不是值。然后其中的地址指向所储存的数值. 如下图左侧为数组,在其他语言称数组,每个地址直接储存对应的值,而对于python来说,这些地址上储存的仍然是地址,这些地址再映射到对应的数据上面,如下图右侧所示。在python中称为列表。
插入和删除不仅需要对当前地址进行操作,而且还要对列表当中的所有元素的地址进行更改,所以时间复杂度是O(n).
栈
- 栈(Stack)是一个数据集合,可以理解为只能在一段进行插入或删除操作的列表。
- 栈的特点:后进先出LIFO(last-in,first-out)
- 栈的概念:栈顶,栈底
- 栈的基本操作:
- 进栈(压栈):push
- 出栈:pop
- 取栈顶:gettop
使用一般的列表结构即可实现栈
- 进栈:li.append
- 出栈:li.pop
- 取栈顶:li[-1]
class Stack:
def __init__(self):
self.stack = []
def push(self, element):
self.stack.append(element)
def pop(self):
return self.stack.pop()
def get_top(self):
if len(self.stack) > 0:
return self.stack[-1]
else:
return None
def is_empty(self):
return len(self.stack) == 0
栈的应用–括号匹配问题
括号匹配问题:给一个字符串,其中包含小括号,中括号,大括号,求该字符串中的括号是否匹配
例如
- ()()[]{} 匹配
- ([{()}]) 匹配
- []( 不匹配
- [(]) 不匹配
()[][{}]
方法:如果遇到左括号,则让左括号进栈,如果遇到右括号则出栈,如果栈顶和右括号相互匹配(遇到的是右小括号,栈顶是左小括号则匹配)则把左括号出栈,如果到最后括号匹配结束,且栈是空的,就证明这些括号匹配
def brace_match(s):
match = {'}':'{', ']':'[', '(':')'}
stack = Stack()
for ch in s:
if ch in {'(', '[', '{'}:
stack.push(ch):
else: # ch in } ] )
if stack.is_empty():
return False
elif stack.get_top() == match[ch]:
stack.pop()
else: # stack.get_top() != match[ch]:
return False
if stack.is_empty():
return True:
else:
return False
队列
- 队列(Queue)是一个数据集合,进允许在列表的一端进行插入,另一端进行删除
- 进行插入的一端称为队尾(rear),插入动作为进队或入队
- 进行删除的一端称为队头(front),删除动作称为出队
- 队列的性质:先进先出(first-in, first-out)
队列实现方法–环形队列
用rear = (rear+1)%12
front = (front+1)%12
- 环形队列: 当队尾指针front == Maxsize + 1时,在前进一个位置自动到0
- 对首指针前进1: front = (front + 1) % MaxSize
- 队尾指针前进1: rear = (rear + 1) % MaxSize
- 队空条件: rear == front
- 队满条件: (rear + 1) % MaxSize == front
队列的实现
class Queue:
def __init__(self, size=100):
self.queue = [0 for _ in range(size)]
self.size = size
self.rear = 0 # 队尾指针
self.front = 0 # 队首指针
def push(self, element):
if not self.is_filled():
self.rear = (self.rear + 1) % self.size
self.queue[self.rear] = element
else:
return IndexError("Queue is filled")
def pop(self):
if not self.is_empty():
self.front = (self.front + 1) % self.size
return self.queue[self.front]
else:
raise IndexError("Queue is empty.")
# 判断队空
def is_empty(self):
return self.rear == self.front:
# 判断队满
def is_filled(self):
return (self.rear + 1) % self.size == self.front
队列内置模块
双向队列
- 双向队列的两段都支持进队和出队的操作
- 双向队列的基本操作
- 队首进队
- 队首出队
- 队尾进队
- 队尾出队
队列内置模块使用方法
from collections import deque
q = deque([1,2,3], ) # 如果队满了,前面的自动出队
q.append(1) # 队尾进队(从右边进队)
q.popleft() # 队首出队
# 用于双向队列
q.appendleft(2) # 队首进队
q.pop() # 队尾出队
# 实现linux tail 命令,打印文件的最后n行
def tail(n):
with open('test.txt', 'r') as f:
q = deque(f, n)
return q # 队列如果满了,前面的自动出队,所以tail中的n正好是队列中的n
迷宫问题
栈–深度优先搜索
- 回溯法
- 思路: 从一个节点开始,任意找下一个能走的点,当找不到能走的点是,退回上一个点寻找是否有其他方向的点
- 使用栈储存当前路径
如这个图中,按照上,右,下,左的顺序依次寻找是否可以继续走,绿色线走到了不能继续走的位置,回溯到蓝色线的起点继续向右走。以此类推,最后寻找到红色线的迷宫出路。
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)
]
def maze_path(x1, y1, x2, y2):
# (x1, y1) 迷宫起点位置。
# (x2, y2) 迷宫终点位置
stack = []
stack.append((x1, y1))
while(len(stack) > 0):
curNode = stack[-1]
if curNode[0] == x2 and curNode[1] == y2:
# 走到终点
for p in stack:
print(p)
return True
# x, y 四个方向: x-1,y; x+1,y; x,y-1; xy+1
# 如果想要控制优先方向,则可以在lambda表达式中进行修改
for dir in dirs:
nextNode = dir(curNode[0], curNode[1])
# 如果下一个节点能走
if maze[nextNode[0]][nextNode[1]] == 0:
stack.append(nextNode)
maze[nextNode[0]][nextNode[1]] = 2 # 2 表示该点已经走过
break
else: # 四个方向找不到出路
maze[nextNode[0]][nextNode[1]] = 2
stack.pop()
else:
print("没有路")
return False
队列–广度优先搜索
广度优先搜索寻找最短路径
- 思路: 从下一个节点开始,寻找所有接下来能继续走的点,继续不断寻找,直到找到出口。
- 使用队列存储当前正在考虑的点
右边的方框是队列首先是1进队列,为起点,再将2进队同时1出队,队列中存储着当前正在考虑的点。下一时刻,3进队,2出队。再下一时刻,3出队,有两条路径,分别为4和5,所以将4,5进队,以此类推直到找到终点。所以可以通过这个方法可以发现,一个点的进队的同时,也会有一个点出队,而这两个点是一条路径的前后两点的关系。因此当最后到达终点的时候,通过反向查找,判断是哪个点出队导致终点进队,从而找出整条路径。
如上图,第一个列表保存各个节点,第二个列表保存该节点是由哪个节点出队导致的。如位置4就代表是由数字7是由数字数列的4号位置由来的,也就是数字5. 数字7是由数字5出队列而来。
数字: 1 2 3 4 5 6 7
位置: -1 0 1 2 2 3 4
from collections import deque
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)
]
def print_r(path):
curNode = path[-1]
realpath = []
while curNode[2] != -1:
realpath.append(curNode[0:2])
curNode = path[curNode[2]]
realpath.append(curNode[0:2]) # 把起点放入realpath
realpath.reverse()
for node in realpath:
print(node)
def maze_path_queue(x1, y1, x2, y2):
queue = deque()
# x1和y1代表的当前队列的节点坐标,第三个位置在初始位置为-1是代表该节点是由哪个节点由来的
queue.append((x1, y1, -1))
path = []
while len(queue) > 0:
curNode = queue.popleft() # 队首出队
path.append(curNode)
if curNode[0] == x2 and curNode[1] == y2:
# 找到终点
print_r(path)
return True
# 接下来就是将当前节点的后面的四个节点推入队列
for dir in dirs:
nextNode = dir(curNode[0], curNode[1])
if maze[nextNode[0]][nextNode[1]] == 0:
queue.append((nextNode[0], nextNode[1], len(path)-1)) # 后续节点进队,寻找哪个节点带他进队的,并且记录下那个节点的位置
maze[nextNode[0]][nextNode[1]] = 2 # 标记为已经走过
else:
print("没有路")
return False