3.1 数据结构定义
数据结构是指相互之间存在着一种或多种关系的数据元素的集合和该集合中数据元素之间的关系组成。简单来说,数据结构就是设计数据以何种方式组织并存储再计算机中。比如:列表、集合与字典等都是一种数据结构。
数据结构按照其逻辑结构可分为线性结构、树结构、图结构。
- 线性结构:数据结构中的元素存在一对一的相互关系
- 树结构:数据结构中的元素存在一对多的相互关系
- 图结构:数据结构中的元素存在多对多的相互关系
数组和列表有两点不同:1)数组的元素类型要相同;2)数组长度固定
3.2 栈
栈是一个数据集合,可以理解为只能在一端进行插入或删除操作的列表
栈的特点:后进先出
栈的基本操作:
- 进栈(压栈):push
- 出栈:pop
- 取栈顶:gettop
# 栈
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
stack = Stack()
stack.push(1)
stack.push(2)
stack.push(3)
print(stack.pop())
括号匹配问题:给一个字符串,其中包含小括号、中括号、大括号,求该字符串中的括号是否匹配。
# 栈
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
# stack = Stack()
# stack.push(1)
# stack.push(2)
# stack.push(3)
# print(stack.pop())
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
print(brace_match('[{([])}]'))
print(brace_match('[{([)}]'))
3.3 队列
队列(Queue)是一个数据集合,仅允许在列表的一端进行插入,另一端进行删除。
进行插入的一端称为队尾(rear),插入动作称为进队或入队;
进行删除的一端称为队头(front),删除动作成为出队
队列的性质:先进先出
队列的实现方式--环形队列:当队尾指针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:
# 随意设置个报错
raise IndexError('Queue is full.')
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
q = Queue(5)
for i in range(4):
q.push(i)
print(q.is_filled()) # True
print(q.pop()) # 结果为0
3.4 python的队列内置模块
# 内置模块
from collections import deque
q = deque()
q.append(1) # 队尾进队
print(q.popleft()) # 队首出队
# 用于双向队列
# q.appendleft(1) # 队首进队
# q.pop() # 队尾出队
q1 = deque([1, 2, 3]) # 创建了一个队列,并且有了三个元素
q1.append(4)
print(q1.popleft())
q2 = deque([1, 2, 3, 4], 5) # 5表示队列的size
q2.append(5) # q2已经满了,再进去一个元素5按道理会报错,但是在内置模块里,它会自动出队一个元素1,再入队元素5
print(q2.popleft()) # 出队元素为1
3.5 栈和队列的应用--迷宫问题
给一个二维列表,表示迷宫(0表示通道,1表示围墙)。给出算法,求一条走出迷宫的路径。
思考:
- 利用栈--深度优先搜索:回溯法,从一个节点开始,任意找下一个能走的点,当找不到能走的点时,退回上一个点寻找是否有其他方向的点。使用栈存储当前路径。(路径不一定最短)
# 用栈解决迷宫问题
maze = [
[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]
]
# x,y四个方向,上:x-1,y;下:x+1,y;左:x,y-1;右:x,y+1
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 maze_path(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 # 有路径就返回True
# curNode[0]是x,curNode[1]是y
# 搜索四个方向
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
maze_path(1, 1, 8, 8)
- 利用队列--广度优先搜索:从一个节点开始,寻找所有接下来能继续走的点,继续不断寻找,直到找到出口。使用队列存储当权正在考虑的节点。(路径最短)
# 用队列解决迷宫问题
from collections import deque
maze = [
[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]
]
# x,y四个方向,上:x-1,y;下:x+1,y;左:x,y-1;右:x,y+1
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):
# path是很多路径的,我们要找到是最少路径的
curNode = path[-1]
realpath = [] # 记录最少的路径
while curNode[2] != -1: # 当位置不是最起始节点
realpath.append(curNode[0:2]) # 就添加进去
curNode = path[curNode[2]] # 再换下一个节点
realpath.append(curNode[0:2]) # 最后加上起点
realpath.reverse() # 倒序
for node in realpath:
print(node)
def maze_path_queue(x1, y1, x2, y2):
queue = deque()
queue.append((x1, y1, -1)) # 起点的位置是-1
path = [] # 来记pop出来的节点
# 队不空就循环,队空就表示没有路了
while len(queue) > 0:
curNode = queue.pop() # 把当前节点存在curNode
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:
# nextNode是由curNode带进来的,所以要记住带它来的那个节点的位置,为了后面能找到回来的路径
queue.append((nextNode[0], nextNode[1], len(path) - 1))
maze[nextNode[0]][nextNode[1]] = 2 # 表示已经走过
else:
print('没有路')
return False
maze_path_queue(1, 1, 8, 8)
3.6 链表
链表是由一系列节点组成的元素集合。每个节点包含两部分,数据域item和指向下一个节点的指针next。通过节点之间的相互连接,最终串联成一个链表。
# 链表
class Node:
def __init__(self, item):
self.item = item
self.next = None
a = Node(1)
b = Node(2)
c = Node(3)
a.next = b
b.next = c
print(a.next.item) # b
print(a.next.next.item) # c
3.6.1 创建链表
- 头插法
- 尾插法
def create_linklist_head(li):
head = Node(li[0]) # 头节点
for element in li[1:]:
node = Node(element) # 创建了一个新节点
node.next = head
head = node
return head
# 尾插法
def create_linklist_tail(li):
head = Node(li[0])
tail = head
for element in li[1:]:
node = Node(element)
tail.next = node
tail = node
return head
def print_linklist(lk):
while lk:
print(lk.item, end=' ')
lk = lk.next
print()
lk = create_linklist_head([1, 2, 3])
# print(lk.item)
# print(lk.next.item)
print_linklist(lk) # 倒序的
lk1 = create_linklist_tail([1, 2, 3, 4, 5, 6])
print_linklist(lk1)
遍历:
3.6.2 链表的插入和删除
假设插入一个p,语句为:
p.next = curNode.next
curNode.next = p
假设删除一个p,语句为:
p = curNode.next
curNode.next = curNode.next.next
del p
3.6.3 双链表
双链表的每个节点有两个指针:一个指向后一个节点,另一个指向前一个节点。
class Node(object):
def __init__(self, item):
self.item = item
self.next = None
self.prior = None
双链表的插入:
p.next = curNode.next
curNode.next.prior = p
p.prior = curNode
curNode.next = p
双链表的删除:
p = curNode.next
curNode.next = p.next、
p.next.prior = curNode
del p
3.7 总结
顺序表(列表/数组)与链表对比
- 按元素查找:顺序表和链表都是O(n)
- 按下标查找:顺序表快O(1),链表O(n)
- 在某元素后插入:顺序表O(n),链表O(1)
- 删除某元素:顺序表O(n),链表O(1)
链表的内存可以更灵活的分配而且链表的链式存储的数据结构对树和图的结构有很大的启发性。
3.8 哈希表
哈希表一个通过哈希函数来计算数据存储位置的数据结构,通常支持如下操作:
insert(key, value):插入键值对(key, value)
get(key):如果存在键为key的键值对则返回其value,否则返回空值
delete(key):删除键为key的键值对
直接寻址表:
缺点:
- 当域U很大时,需要消耗大量内存,很不实际;
- 如果域U很大而实际出现的key很少,则大量空间被浪费;
- 无法处理关键字不是数字的情况。
对直接寻址表做改造->>>就成了哈希表
哈希(Hashing):
- 构建大小为m的寻址表T;
- key为k的元素放在h(k)位置上;
- h(k)是一个函数,其将域U映射到T[0, 1, ..., m-1]。
哈希表(Hash Table,又称为散列表),是一种线性表的存储结构。哈希表由一个直接寻址表和一个哈希函数组成。哈希函数h(k)将元素关键字k作为自变量,返回元素的存储下表。
假设有一个长度为7的哈希表,哈希函数h(k) = k%7。元素集合{14,22, 3, 5}的存储方式如图:
但是会出现一个新的问题:哈希冲突!
由于哈希表的大小是有限的,而要存储的值得总数量是无限的,因此对于任何哈希函数,都会出现两个不同元素映射到同一个位置上的情况,这种情况叫做哈希冲突。
比如h(k) = k % 7,h(0) = h(7)=h(14)=...
1.解决哈希冲突的方法--开放寻址法
开放寻址法:如果哈希函数返回的位置已经有值,则可以向后探查新的位置来存储这个值。
- 线性探查:如果i被占用,则探查i+1, i+2, ....
- 二次探查:如果位置i被占用,则用探查i+,i-, i+, i-, ....
- 二度哈希:有n个哈希函数,当使用第1个哈希函数h1发生冲突时,则尝试使用h2, h3, ...
2.解决哈希冲突的方法--拉链法
拉链法:哈希表每个位置都连接一个链表,当冲突发生时,冲突的元素将被加到该位置链表的最后。
常见的哈希函数:
- 除法哈希法:h(k) = k% m
- 乘法哈希法:h(k) = floor(m*(A*key%1))
- 全域哈希法:mod p) mod m a,b=1, 2, ..., p-1
# 拉链法
class linklist:
class Node:
def __init__(self, item=None):
self.item = item
self.next = None
# 迭代器类,为了避免遍历链表总是要循环去遍历
class linklistIterator:
def __init__(self, node):
self.node = node
def __next__(self):
if self.node:
cur_node = self.node
self.node = cur_node.next
return cur_node.item
else:
raise StopIteration
def __iter__(self):
return self
# iterable表示列表
def __init__(self, iterable=None):
self.head = None
self.tail = None
if iterable:
self.extend(iterable)
# 尾插法
def append(self, obj):
s = linklist.Node(obj)
if not self.head:
self.head = s
self.tail = s
else:
self.tail.next = s
self.tail = s
def extend(self, iterable):
for obj in iterable:
self.append(obj)
def find(self, obj):
for n in self:
if n == obj:
return True
else:
return False
# 返回列表迭代器
def __iter__(self):
return self.linklistIterator(self.head)
# 返回字符串
def __repr__(self):
return "<<" + ",".join(map(str, self)) + ">>"
# lk = linklist([1, 2, 3, 4, 5])
# print(lk) # <<1,2,3,4,5>> 直接打印链表
# for element in lk:
# print(element)
# 类似集合,不允许重复
class HashTable:
def __init__(self, size=101):
self.size = size
self.T = [linklist() for i in range(self.size)] # 空列表
def h(self, k):
return k % self.size
def insert(self, k):
i = self.h(k)
if self.find(k):
print("Duplicated Insert.")
else:
self.T[i].append(k)
def find(self, k):
i = self.h(k)
return self.T[i].find(k)
ht = HashTable()
ht.insert(0)
ht.insert(1)
ht.insert(3)
ht.insert(102)
print(','.join(map(str, ht.T)))
print(ht.find(4))
哈希表的应用--集合和字典
字典与集合都是通过哈希表来实现的。例如:a = {'name': 'Alice', 'age': 18, 'gender': 'Man'}
使用哈希表存储字典,通过哈希函数将字典的键映射为下标。假设h('name') = 3, h('age') = 1, h('gender') = 4,则哈希表存储为[None, 18, None, 'Alice', 'Man']
如果发生哈希冲突,则通过拉链法或开放寻址法解决。
哈希表的应用--md5算法
MD5(Message-Digest Algorithm 5)曾经是密码学中常用的哈希函数,可以把任意长度的数据映射为128位的哈希值,其曾经包含如下特征:
- 同样的消息,其MD5值必定相同;
- 可以快速计算出任意给定消息的MD5值;
- 除非暴力的枚举所有可能的消息,否则不可能从哈希值反推出消息本身;
- 两条消息之间即使只有微小的差别,其对应的MD5值也应该是完全不同、完全不相关的;
- 不能在有意义的时间内人工的构造两个不同的消息,使其具有相同的MD5值。、
哈希表的应用--SHA2算法(和MD5类似的性质)