视频地址:https://www.bilibili.com/video/BV1uA411N7c5
数据结构是指相互之间存在着一种或多种关系的数据元素的集合和该集合中数据元素之间的关系组成。简单来说,数据结构就是设计数据以何种方式组织并存储在计算机中。
比如:列表、集合与字典等都是一种数据结构。
N.Wirth: “程序 = 数据结构 + 算法”
数据结构按照其逻辑结构可以分为:①线性结构;②树结构;③图结构。
- 线性结构:数据结构中的元素存在 一对一 的相互关系
- 树结构:数据结构中的元素存在 一对多 的相互关系
- 图结构:数据结构中的元素存在 多对多 的相互关系
1. 列表/数组
列表(其他语言成为数组)是一种基本数据类型。
关于列表的问题:
- 列表中的元素是如何存储的?
- 列表的基本操作:按下标(索引)查找、插入元素、删除元素…这些操作的时间复杂度是多少?
- 查找: O ( 1 ) O(1) O(1)
- 插入: O ( n ) O(n) O(n)
- 删除: O ( n ) O(n) O(n)
append
: O ( 1 ) O(1) O(1)
扩展:Python的列表是如何实现的?
数组与列表有两点不同:
- 数组元素类型相同
- 数组长度固定
2. 栈(stack)
栈(Stack)是一个数据集合,可以理解为只能在一端进行插入或删除操作的列表。
- 栈的特点:先进后出,后进先出LIFO(Last-In, First-Out)
- 栈的概念:栈顶、栈底
- 栈的基本操作:
- 进栈(压栈):
push
- 出栈:
pop
- 取栈顶:
gettop
- 进栈(压栈):
2.1 栈的实现
使用一般的列表结构(list
)即可实现栈。
- 进栈:
ls.append
- 出栈:
ls.pop
- 取栈顶:
ls[-1]
2.2 栈的基本实现代码
class Stack:
def __init__(self):
self.stack = []
def push(self, elem):
self.stack.append(elem)
def pop(self):
if self.stack:
return self.stack.pop()
else:
raise "Empty Value"
def gettop(self):
if self.stack:
return self.stack[-1]
else:
return None
if __name__ == "__main__":
stack = Stack()
stack.push(1)
stack.push(2)
stack.push(3)
print(stack.pop())
print(stack.gettop())
2.3 栈的应用 —— 括号匹配问题
括号匹配问题:给一个字符串,其中包含小括号、中括号和大括号,求该字符串中的括号是否匹配。
例如:
()()[]{} 匹配
([{()}]) 匹配
[]( 不匹配
[(]) 不匹配
思路:有左括号先放到栈中,看下一个;如果是右括号,那么看和栈顶是否匹配,如果匹配,出栈。看完所有元素后,如果栈是空栈,那么说明是合法的,如果不为空,那么是非法的。
特殊情况:如果直接来了一个右括号,直接非法!
代码:
class Stack():
def __init__(self):
self.stack = []
def push(self, elem):
self.stack.append(elem)
def pop(self):
if self.stack:
return self.stack.pop()
else:
raise "Empty Value"
def gettop(self):
if self.stack:
return self.stack[-1]
else:
raise "Empty Value"
def is_empty(self):
return len(self.stack) == 0
def brace_match(s):
match_dict = {')': '(',
']': '[',
'}': '{'}
stack = Stack()
for char in s:
if char in "([{":
stack.push(char)
elif char in ")]}":
if stack.is_empty():
return False
elif stack.gettop() == match_dict[char]:
stack.pop()
else: # 不匹配
return False
else:
raise "Illegal Value"
# 看一下栈里是否有元素
if stack.is_empty():
return True
else:
return False
if __name__ == "__main__":
test_1 = "()()[]{}"
test_2 = "([{()}])"
test_3 = "[]("
test_4 = "[(])"
print(brace_match(test_1))
print(brace_match(test_2))
print(brace_match(test_3))
print(brace_match(test_4))
"""
True
True
False
False
"""
3. 队列(Queue)
队列(Queue)是一个数据集合,仅允许在列表的一端进行插入,另一端进行删除。
- 进行插入的一端称为队尾(rear),插入动作称为进队或入队
- 进行删除的一端称为对头(front),删除动作称为出队
- 队列的性质:先进先出FIFO(First-In, First-Out)
3.1 队列的实现
队列是否能用列表简单实现?为什么?
列表不好实现,出队3次以后,再入队就没法入队了。但如果把这个队列的头和尾连起来,这个问题就解决了!
3.2 队列的实现方式 —— 环形队列
Q:队满的时候为什么有一个位置是空的?
A:为了判断这个队是空队还是满队:
- 空队列:
front == rear
- 满队列:
front == rear + 1
Q:rear
进队到11
时,怎么变为0
?
A:使用%
-> rear = rear % len(queue)
-> rear %= 12
Q:那么front
呢?
A:也是一样的,也是front %= len(queue)
环形队列:当队尾指针front = MaxSize - 1
时,再前进一个位置就自动到0
。
- 队首指针前进1:
front = (front + 1) % MaxSize
- 队尾指针前进1:
rear = (rear + 1) % MaxSize
- 队空条件:
rear == front
- 队满条件:
(rear + 1) % MaxSize == front
MaxSize
为队列的长度
3.3 队列的代码实现
class Queue:
def __init__(self, size=100):
# 队列的长度需要固定
self.size = size # 队列的长度
self.queue = [0 for i in range(size)]
self.rear = 0 # 队尾(进队)
self.front = 0 # 队首(出队)
# 入队
def push(self, elem):
if not self.is_filled():
self.rear = (self.rear + 1) % self.size
self.queue[self.rear] = elem # 进队
else:
raise "Filled Queue Error"
# 出队
def pop(self):
if not self.is_empty():
self.front = (self.front + 1) % self.size
# 这里不删除旧的元素了,因为环形的,有值后会覆盖它!
return self.queue[self.front]
else:
raise "No Value Error"
# 判断队空
def is_empty(self):
if self.rear == self.front:
return True
else:
return False
# 判断队满
def is_filled(self):
if (self.rear + 1) % self.size == self.front:
return True
else:
return False
# 获取队尾
def getrear(self):
return self.queue[self.rear]
# 获取队首
def getfront(self):
return self.queue[(self.front + 1) % 12]
if __name__ == "__main__":
q = Queue(5)
for i in range(4):
q.push(i)
print(q.is_filled())
print(q.pop())
print(q.is_empty())
print(q.getfront())
print(q.getrear())
3.4 双向队列
双向队列的两端都支持进队和出队操作,其基本操作如下:
- 队首进队
- 队首出队
- 队尾进队
- 队尾出队
3.5 Python队列内置模块
使用方法from collections import deque
,具体操作如下:
- 创建队列:
queue = deque()
- 进队:
append()
- 出队:
popleft()
- 双向队列队首进队:
appendleft()
- 双向队列队尾出队:
pop()
注意:这里并不是
import queue
模块,因为queue
模块一般用在多线程、多进程中,用来保持线程安全的。如果我们想要解题,那么我们就使用from collections import deque
deque
: 全称dequeue
,de表示双向,即双向队列
deque
是从左到右队首和堆尾!
一般情况下,单向队列用的比较多
from collections import deque
q = deque()
# 单向队列
q.append(1) # 队尾进队
print(q.popleft()) # 队首出队
# print(q.popleft()) # IndexError: pop from an empty deque
# 双向队列
q.appendleft(2) # 队首进队
print(q.pop())
值得注意的是,deque()
在创建的时候是可以传入值的,如下:
from collections import deque
# 不传入值
q1 = deque()
q1.append(1)
q1.append((1, 2, 3))
q1.append([1, 2, 3])
print(q1) # deque([1, (1, 2, 3), [1, 2, 3]])
q1.popleft()
q1.popleft()
print(q1) # deque([[1, 2, 3]])
# 传入值
q2 = deque([1, 2, 3])
print(q2) # deque([1, 2, 3])
q2.append(4)
print(q2) # deque([1, 2, 3, 4])
q2.append([5, 6, 7]) # deque([1, 2, 3, 4, [5, 6, 7]])
print(q2)
# 传入值和最大值
q3 = deque([1, 2, 3], maxlen=5)
print(q3) # deque([1, 2, 3], maxlen=5)
q3.append(4)
q3.append(5)
print(q3) # deque([1, 2, 3, 4, 5], maxlen=5)
q3.append(6)
print(q3) # deque([2, 3, 4, 5, 6], maxlen=5) -> 自动将1pop掉了
3.6 使用deque
实现Linux的tail
命令
Linux的tail
命令即在terminal中输出文本文件的后五行。
from collections import deque
def tail(n):
with open('test.txt', 'r') as f:
q = deque(f, n)
return q
if __name__ == "__main__":
# print(tail(5)) # deque(['gdfsfdgh567u65g\n', 'dsfgfdgh456t24t\n', 'sdgfstg453rt234t2\n', 'gdfs344534y45\n', 'qqqfdu6574'], maxlen=5)
for line in tail(5):
print(line, end="")
"""
gdfsfdgh567u65g
dsfgfdgh456t24t
sdgfstg453rt234t2
gdfs344534y45
qqqfdu6574
"""
3.7 栈和队列的应用 —— 迷宫问题
给一个二维列表,表示迷宫(0表示通道,1表示围墙)。给出算法,求一条走出迷宫的路径。
maze
: 英[meɪz] 美[meɪz]
n. 迷宫; 纷繁复杂的规则; 复杂难懂的细节; 迷宫图;
vt. 使困惑; 使混乱; 迷失;
3.7.1 方法1:栈 —— 深度优先搜索
- 回溯法
- 思路:从一个节点开始,任意找下一个能走的点,当找不到能走的点时,退回上一个点寻找是否有其他方向的点。
- 使用栈存储当前路径。
def maze_path(maze, x1, y1, x2, y2):
# 创建栈
stack = []
# 将起点放到栈中
stack.append((x1, y1))
# 定义四种寻找方式
directions = [
lambda x, y: (x + 1, y), # 上
lambda x, y: (x - 1, y), # 下
lambda x, y: (x, y - 1), # 左
lambda x, y: (x, y + 1), # 右
]
# 当栈不为空,说明能继续找
while len(stack) > 0:
# 设定当前节点
curNode = stack[-1]
# 判断当前节点是否为终点
if curNode[0] == x2 and curNode[1] == y2:
print("Find Solution!")
for path in stack:
print(path, end="->")
return True
else: # 如果不是终点,按照四个方向依次寻找
for direction in directions:
# 得到下一个节点
nextNode = direction(curNode[0], curNode[1])
# 判断下一个节点是否为0
if maze[nextNode[0]][nextNode[1]] == 0: # 说明是下一个节点
# 压栈
stack.append(nextNode)
# 进行标记以防止回退
maze[nextNode[0]][nextNode[1]] = 2
# 已经找到下一个节点,停止该节点的搜索
break
else: # 如果不是,继续搜索下一个方向
pass
else: # 如果四个方向都没有找到下一个节点
stack.pop()
else: # 栈为空
print("No Solution!")
return False
if __name__ == "__main__":
maze = [
[1, 1, 1, 1, 1, 1, 1, 1, 1, 1], # 1
[1, 0, 0, 1, 0, 0, 0, 1, 0, 1], # 2
[1, 0, 0, 1, 0, 0, 0, 1, 0, 1], # 3
[1, 0, 0, 0, 0, 1, 1, 0, 0, 1], # 4
[1, 0, 1, 1, 1, 0, 0, 0, 0, 1], # 5
[1, 0, 0, 0, 1, 0, 0, 0, 0, 1], # 6
[1, 0, 1, 0, 0, 0, 1, 0, 0, 1], # 7
[1, 0, 1, 1, 1, 0, 1, 1, 0, 1], # 8
[1, 1, 0, 0, 0, 0, 0, 0, 0, 1], # 9
[1, 1, 1, 1, 1, 1, 1, 1, 1, 1], # 10
]
maze_path(maze, 1, 1, 8, 8)
"""
Find Solution!
(1, 1)->(2, 1)->(3, 1)->(4, 1)->(5, 1)->(5, 2)->(5, 3)->(6, 3)->(6, 4)->(6, 5)->(7, 5)->(8, 5)->(8, 6)->(8, 7)->(8, 8)->
"""
3.7.2 方法2:队列 —— 广度优先搜索
思路:从一个节点开始,寻找所有接下来能继续走的点,继续不断寻找,直到找到出口。
多种可能同时进行
- 使用队列存储当前正在考虑的节点。
from collections import deque
def print_path(path):
curNode = path[-1] # path的最后一个元素就是终点
real_path = [] # 存放真正的路径
# 找到其他节点
while curNode[2] != -1:
real_path.append((curNode[0], curNode[1]))
curNode = path[curNode[2]]
else:
real_path.append((curNode[0], curNode[1])) # 起点
real_path.reverse()
for node in real_path:
print(node, end="->")
def maze_path_queue(maze, x1, y1, x2, y2):
# 创建队列
queue = deque()
# 存放起点
queue.append((x1, y1, -1)) # x坐标,y坐标,哪个位置让它来的
path = [] # 存放出队的节点
# 定义四种寻找方式
directions = [
lambda x, y: (x + 1, y), # 上
lambda x, y: (x - 1, y), # 下
lambda x, y: (x, y - 1), # 左
lambda x, y: (x, y + 1), # 右
]
while len(queue) > 0:
curNode = queue.popleft() # 队首出栈
path.append(curNode)
# 判断curNode是否为终点
if curNode[0] == x2 and curNode[1] == y2:
print("Found Solution: ")
print_path(path)
return True
# 搜索四个方向
for direction in directions:
nextNode = direction(curNode[0], curNode[1])
# 判断nextNode是否能走通
if maze[nextNode[0]][nextNode[1]] == 0:
# 能走,进队,并记录哪个节点带它来的
queue.append((nextNode[0], nextNode[1], len(path) - 1))
# 标记为已走过
maze[nextNode[0]][nextNode[1]] = 2
else:
print("No Solution")
return False
if __name__ == "__main__":
maze = [
[1, 1, 1, 1, 1, 1, 1, 1, 1, 1], # 1
[1, 0, 0, 1, 0, 0, 0, 1, 0, 1], # 2
[1, 0, 0, 1, 0, 0, 0, 1, 0, 1], # 3
[1, 0, 0, 0, 0, 1, 1, 0, 0, 1], # 4
[1, 0, 1, 1, 1, 0, 0, 0, 0, 1], # 5
[1, 0, 0, 0, 1, 0, 0, 0, 0, 1], # 6
[1, 0, 1, 0, 0, 0, 1, 0, 0, 1], # 7
[1, 0, 1, 1, 1, 0, 1, 1, 0, 1], # 8
[1, 1, 0, 0, 0, 0, 0, 0, 0, 1], # 9
[1, 1, 1, 1, 1, 1, 1, 1, 1, 1], # 10
]
maze_path_queue(maze, 1, 1, 8, 8)
"""
Found Solution:
(1, 1)->(2, 1)->(3, 1)->(4, 1)->(5, 1)->(5, 2)->(5, 3)->(6, 3)->(6, 4)->(6, 5)->(7, 5)->(8, 5)->(8, 6)->(8, 7)->(8, 8)->
"""
4. 链表(Linked List)
链表是由一系列节点组成的元素集合。每个节点包含两部分,数据域item和指向下一个节点的指针next。通过节点之间的相互连接,最终串联成一个链表。
class Node():
def __init__(self, item):
self.item = item
self.next = None
定义链表
class Node:
def __init__(self, item):
self.item = item
self.next = None
if __name__ == "__main__":
# 创建节点
a = Node(1)
b = Node(2)
c = Node(3)
# 将节点连起来
a.next = b
b.next = c
# 通过a这个节点找到b和c
print(a.next.item) # 2
print(a.next.next.item) # 3
4.1 链表的创建与遍历
上面创建链表的方式是手动的,这样太慢了。创建链表的方式有:头插法和尾插法。
- 头插法:在head前面插入,插入完毕后,head指向新的头
- 尾插法:在tail后面插入,插入完毕后,tail指向新的尾
class Node():
def __init__(self, item):
self.item = item
self.next = None
def create_linkedlist_head(ls):
# 新建头节点
head = Node(ls[0])
for elem in ls[1:]:
node = Node(elem)
node.next = head
head = node
return head
def create_linkedlist_tail(ls):
head = Node(ls[0])
# 此时头节点和尾节点是同一指向
tail = head
for elem in ls[1:]:
# 为不同元素创建节点
node = Node(elem)
tail.next = node
tail = node
return head
def print_linklist(lk):
while lk:
print(lk.item, end=", ")
lk = lk.next
if __name__ == '__main__':
lk_head = create_linkedlist_head([1, 2, 3])
print_linklist(lk_head) # 3, 2, 1,
print("")
lk_tail = create_linkedlist_tail([1, 2, 3, 4, 5])
print_linklist(lk_tail) # 1, 2, 3, 4, 5,
4.2 链表节点的插入
数组插入需要挪动,再插入,这样的时间复杂度比较高( O ( n ) O(n) O(n)),而对于一个链表而言,先把需要插入的地方的链子解开,插入后连起来就行了。
p.next = curNode.next
curNode.next = p
4.3 链表节点的删除
p = curNode.next
curNode.next = curNode.next.next # 写成curNode.next = p.next也行
del p # 其实删不删都可以
5. 双链表
双链表的每个节点有两个指针:一个指向后一个节点,另一个指向前一个节点。
class Node():
def __init__(self, item=None):
self.item = item
self.next = None
self.prior = None
5.1 双链表节点的插入
p.next = curNode.next
curNode.next.prior = p
p.prior = curNode
curNode.next = p
5.2 双链表节点的删除
p = curNode.next
curNode.next = p.next
p.next.prior = curNode
del p
6. 链表 —— 复杂度分析
顺序表(列表/数组)与链表对比:
Item | 顺序表 | 链表 |
---|---|---|
按元素值查找 | O ( n ) O(n) O(n) | O ( n ) O(n) O(n) |
按下标查找 | O ( 1 ) O(1) O(1) | O ( n ) O(n) O(n) |
在某个元素后插入 | O ( n ) O(n) O(n) | O ( 1 ) O(1) O(1) |
删除某个元素 | O ( n ) O(n) O(n) | O ( 1 ) O(1) O(1) |
- 链表在插入和删除的操作上明显快与顺序表
- 链表的内存可以更灵活地分配:
- 试利用链表重新实现栈和队列
- 链表这种链式存储的数据结构对树和图的结构有很大的启发性
7. 哈希表
哈希表是一个通过哈希函数来计算数存储位置的数据结构,通常支持如下操作:
insert(key, value)
: 插入键值对(key, value)get(key)
: 如果存在键为key
的键值对则返回其value
,否则返回空值delete(key)
: 删除键为key
的键值对
哈希表其实就是一个
dict
7.1 直接寻址表
当关键字的全域 U U U 比较小时,直接寻址是一种简单而有效的方法。
U U U是一个集合,里面包含了所有可能出现的关键词
K K K是一个集合,里面包含了所有实际出现的关键词
T T T是一个列表,它和 U U U一一对应
直接寻址技术的缺点:
- 当域 U U U很大时,需要消耗大量内存,很不实际
- 如果域
U
U
U很大而实际出现的
key
很少,则大量空间被浪费 - 无法处理关键字不是数字的情况
Q:那我们应该怎么解决这些问题呢?
A:在直接寻址表上加一个哈希函数就形成了哈希表
7.2 哈希
直接寻址表:key
为k
的元素放到k
位置上
改进的直接寻址表:哈希(Hashing)
- 构建大小为 m m m的寻址表 T T T
key
为k
的元素放到 h ( k ) h(k) h(k)位置上- h ( k ) h(k) h(k)是一个函数,其将域 U U U映射到表 T [ 0 , 1 , . . . , m − 1 ] T[0, 1, ..., m-1] T[0,1,...,m−1]
7.3 哈希表
哈希表(Hash Table,又称为散列表),是一种线性表的存储结构。哈希表由一个直接寻址表和一个哈希函数组成。哈希函数 h ( k ) h(k) h(k)将元素关键字 k k k作为自变量,返回元素的存储下标。
假设有一个长度为7的哈希表,哈希函数$h(k) = k % 7 。元素集合 。元素集合 。元素集合{14, 22, 3, 5}$的存储方式如下图:
元素集合 { 14 , 22 , 3 , 5 } \{14, 22, 3, 5\} {14,22,3,5}是指key,没有value
h ( k ) = k % 7 ∈ [ 0 , 6 ] h(k) = k \% 7 \in [0, 6] h(k)=k%7∈[0,6]
Q: 如果有一个key=0
,那么上面的表中key=0
已经有key
了,怎么办?
A:这么情况称为哈希冲突
7.4 哈希冲突
由于哈希表的大小是有限的,而要存储的值的数量是无限的,因此对于任何哈希函数而言,都会出现两个不同元素映射到同一个位置上的情况,这种情况叫做哈希冲突。
比如 h ( k ) = k % 7 h(k) = k \% 7 h(k)=k%7, h ( 0 ) = h ( 7 ) = h ( 14 ) = . . . h(0) = h(7) = h(14) = ... h(0)=h(7)=h(14)=...
7.4.1 解决哈希冲突1 —— 开放寻址法
开放寻址法:如果哈希函数返回的位置已经有值,则可以向后探查新的位置来存储这个值。
- 线性探查:如果位置 i i i 被占用,则探查 i + 1 , i + 2 , . . . i + 1, i + 2, ... i+1,i+2,...
- 二次探查:如果位置 i i i 被占用,则探查 i + 1 2 , i − 1 2 , i + 2 1 , i − 2 2 , . . . i + 1^2, i - 1^2, i + 2^1, i - 2^2, ... i+12,i−12,i+21,i−22,...
- 二度哈希(Double Hash):有 n n n 个哈希函数,当使用第1个哈希函数 h 1 h_1 h1发生冲突时,则尝试使用 h 2 , h 3 , . . . h_2, h_3, ... h2,h3,...
7.4.2 解决哈希冲突2 —— 拉链法
拉链法:哈希表每个位置都连接一个链表,当冲突发生时,冲突的元素将被加到该位置链表的最后。
之前是每个位置存放一个元素,现在不是元素而是链表
7.5 常见的哈希函数
- 除法哈希法:
- h ( k ) = k % m h(k) = k \% m h(k)=k%m
- ->
lambda k: k % m
- 乘法哈希法:
- h ( k ) = f l o o r ( m × ( A × k e y % 1 ) ) h(k) = \rm{floor}(m\times (A \times \rm{key} \% 1)) h(k)=floor(m×(A×key%1))
- ->
lambda k: floor(m*(A*key%1))
%1
: 取小数部分
- 全域哈希法:
- h a , b ( k ) = ( ( a × k e y + b ) % p ) % m h_{a, b}(k) = ((a\times \rm{key} + b) \% p) \% m ha,b(k)=((a×key+b)%p)%m
- ->
lambda a, b, k: ((a * key + b) % p) % m
- a , b = 1 , 2 , . . . , p − 1 a, b = 1, 2, ..., p-1 a,b=1,2,...,p−1
7.6哈希表的应用 —— 集合与字典
字典与集合都是通过哈希表来实现的。
a = {'name': 'Alex', 'age': 18, 'gender': 'Male'}
使用哈希表存储字典,通过哈希函数将字典的键映射为下标。假设 h(‘name’) = 3, h(‘age’) = 1, h(‘gender’) = 4, 则哈希表存储为 [None, 18, None, ‘Alex’, ‘Man’]
如果发生哈希冲突,则通过拉链法或开放寻址法解决。
7.7 哈希表的应用 —— MD5算法
MD5(Message-Digest Algorithm 5)曾经是密码学中常用的哈希函数,可以把任意长度的数据映射为128位的哈希值,其曾经包含如下特征:
- 同样的消息,其MD5值必定相同;
- 可以快速计算出给定消息的MD5值;
- 除非暴力的枚举所有可能的消息,否则不可能从哈希值反推出消息本身;
- 两条消息之间即使只有微小的差别,其对应的MD5值也应该是完全不同、完全不相关区的;
- 不能在有意义的时间内人工构造两个不同的消息使其具有相同的MD5值。
Message-Digest: 消息摘要
哈希只有加密,没有解密
MD5值相同的两个文件,但二者也有可能是不同的,因为有哈希冲突的存在,而且128位的哈希值是有限的(虽然有限,但是人工制造两个哈希值相同的不同文件基本上是不可能的)
现在MD5已经被破解了,不再安全了,但仍然在大规模使用
7.7.1 应用举例 —— 文件的哈希值
算出文件的哈希值,若两个文件的哈希值相同,则可以认为这两个文件是相同的,因此:
- 用户可以利用它来验证下载的文件是否完整
- 云存储服务商可以利用它来判断用户要上传的文件是否已经存在于服务器上,从而实现秒传的功能,同时也可以避免存储过多相同的文件副本
7.8 哈希表的应用 —— SHA-2算法
历史上MD5和SHA-1曾经是使用最为广泛的 Cryptographic Hash Function,但是随着密码学的发展,这两个哈希函数的安全性相继受到了各种挑战。因此现在安全性较重要的场合推荐使用SHA-2等新的、更安全的哈希函数。
- SHA-2包含了一系列的哈希函数:SHA-224,SHA-256,SHA-384,SHA-512,SHA-512/224,SHA-512/256,其对应的哈希长度分别为224,256,384或512位。
- SHA-2具有和MD5类似的性质(参见MD5算法的特征)。
SHA: Secure Hash Algorithm
SHA-2目前还没有被破解,因此更加安全
7.8.1 应用举例 —— SHA-2算法
在比特币(Bitcoin)系统中,所有参与者需要共同解决如下问题:对于一个给定的字符串U
,给定的目标哈希值H
,需要计算出一个字符串V
,使得U+V
的哈希值与H
的差小于一个给定值D
。此时,只能通过暴力枚举V
来猜测。
首先计算出结果的人可以获得一定奖金,而某人首先计算成功的概率与其拥有的计算量成正比,所以其获得奖金的期望值与其拥有的计算量成正比。
8. 树与二叉树
树是一种数据结构,比如目录结构。树是一种可以递归定义的数据结构,是由 n n n个节点组成的集合:
- 如果 n = 0 n=0 n=0,那这是一颗空树;
- 如果 n > 0 n > 0 n>0,那存在1个节点作为树的根节点,其他节点可以分为 m m m个集合,每个集合本身又是一棵树。
8.1 一些概念
- 根节点:A
- 叶子节点:没有孩子节点的节点(到头了,没有分叉了),比如 B,C,P,Q,K,L,M,N
- 树的深度(高度):最深的
- 节点的度:指定节点对应的分叉数,E的度=2
- 树的度:哪个节点分的叉最多,那树的就是几 -> max(节点的度)=6
- 孩子节点/父节点:相对的概念
- 子树:EIJPQ是一颗以E为根节点的树
8.2 用树创建一个文件夹系统
# 树是由一堆节点构成,所以节点是树的核心
class Node:
def __init__(self, name, node_type='dir'):
# 链式存储的方式
self.name = name
self.type = node_type # "dir" or "file"
self.children = []
self.parent = None # 指向父节点
# 让Node可以被打印
def __repr__(self):
return self.name
class FileSystemTree:
def __init__(self):
self.root = Node("/") # 根节点: 这里仿照的是Linux系统的root
self.now = self.root # 在哪个文件夹中
def mkdir(self, name):
# name必须以/结尾
if name[-1] != "/":
name += "/"
# 创建Node
node = Node(name)
# 和现在的文件夹相连
self.now.children.append(node)
node.parent = self.now
# 展现当前目录下的所有目录
def ls(self):
print(self.now.children)
# return self.now.children
# change directory(只支持相对路径)
def cd(self, name):
if name in ["..", "../"]: # 返回上一级目录
self.now = self.now.parent
return True
# name必须以/结尾
if name[-1] != "/":
name += "/"
for child in self.now.children:
if child.name == name: # 如果有这个目录
self.now = child # 切换目录
return True
else:
raise ValueError("invalid directory")
if __name__ == "__main__":
n = Node('hello')
n2 = Node('world')
n.children.append(n2)
n2.parent = n
tree = FileSystemTree()
tree.mkdir("var/")
tree.mkdir("bin/")
tree.mkdir("usr/")
tree.ls() # [var/, bin/, usr/]
tree.cd("bin/")
tree.mkdir("python/")
tree.ls() # [python/]
tree.cd("../")
tree.ls() # [var/, bin/, usr/]
8.3 二叉树
二叉树的链式存储:将二叉树的节点定义为一个对象,节点之间通过类似链表的链接方式来连接。
8.3.1 节点定义:
class BiTreeNode:
def __init__(self, data):
self.data = data
self.lchild = None
self.rchild = None
二叉树的度是2
class BiTreeNode:
def __init__(self, data):
self.data = data
self.lchild = None # 左孩子节点
self.rchild = None # 右孩子节点
if __name__ == "__main__":
a = BiTreeNode("A")
b = BiTreeNode("B")
c = BiTreeNode("C")
d = BiTreeNode("D")
e = BiTreeNode("E")
f = BiTreeNode("F")
g = BiTreeNode("G")
e.lchild = a
e.rchild = g
a.rchild = c
c.lchild = b
c.rchild = d
g.lchild = f
root = e
print(root.lchild.rchild.data) # C
8.3.2 二叉树的遍历
二叉树的遍历方式:
- 前序遍历:EACBDGF —— 自己 -> 左 -> 右
- 中序遍历:ABCDEGF —— 左 -> 自己 -> 右
- 后序遍历:BDCAFGE —— 左 -> 右 -> 自己
- 层次遍历:EAGCFBD —— 每一层,从左到右
前序、中序、后序都是指遍历自己的位置
给出三者的任意两者都可以推出这棵树
中序遍历后一定是升序的!(有序!)
from collections import deque
class BiTreeNode:
def __init__(self, data):
self.data = data
self.lchild = None # 左孩子节点
self.rchild = None # 右孩子节点
def pre_order(root):
# 前序遍历: 自己 -> 左 -> 右
if root: # 如果root不为空
print(root.data, end=" ") # 自己
pre_order(root.lchild) # 左
pre_order(root.rchild) # 右
def in_order(root):
# 中序遍历: 左 -> 自己 -> 右
if root:
in_order(root.lchild)
print(root.data, end=" ")
in_order(root.rchild)
def post_order(root):
# 后序遍历: 左 -> 右 -> 自己
if root:
post_order(root.lchild)
post_order(root.rchild)
print(root.data, end=" ")
def level_order(root):
# 层次遍历
# 先创建队列,并添加根节点
queue = deque()
queue.append(root)
while len(queue) > 0: # 只要队列不空
node = queue.popleft() # 出队并且得到是哪个出队了
print(node.data, end=" ") # 打印出队的元素
if node.lchild: # 看一下出队的节点是否有左孩子
queue.append(node.lchild)
if node.rchild: # 看一下出队的节点是否有右孩子
queue.append(node.rchild)
if __name__ == "__main__":
a = BiTreeNode("A")
b = BiTreeNode("B")
c = BiTreeNode("C")
d = BiTreeNode("D")
e = BiTreeNode("E")
f = BiTreeNode("F")
g = BiTreeNode("G")
e.lchild = a
e.rchild = g
a.rchild = c
c.lchild = b
c.rchild = d
g.lchild = f
root = e
print(root.lchild.rchild.data) # C
pre_order(root) # E A C B D G F
print("")
in_order(root) # A B C D E F G
print("")
post_order(root) # B D C A F G E
print("")
level_order(root) # E A G C F B D
8.4 二叉搜索树(Binary Search Tree, BST)
二叉搜索树是一颗二叉树,且满足以下性质:设 x
是二叉树的一个节点,如果 y
是 x
左子树的任意一个节点,那么 y.key ≤ x.key
;如果 y
是 x
右子树的一个节点,那么 y.key ≥ x.key
。
简单理解:任意一个节点的左侧的值都比该节点小,右边都比该节点大
二叉搜索树的操作:
- 查询: O ( log n ) O(\log^n) O(logn)
- 插入: O ( log n ) O(\log^n) O(logn)
- 删除
import random
class BiTreeNode:
def __init__(self, data):
self.data = data
self.lchild = None
self.rchild = None
self.parent = None
class BinarySearchTree:
def __init__(self, ls=None):
self.root = None # 根节点
if ls:
for val in ls:
self.insert_no_rec(val) # 非递归的方法(更快)
# self.root = self.insert(self.root, val) # 递归的方法
def insert(self, node, val):
"""
node: 递归时遍历的所有节点
val: 插入的值
"""
if node is None: # 如果node为空(写成 if not node也可以)
node = BiTreeNode(val) # node为空则需要创建一个节点
elif val < node.data: # 说明插入的值比遍历的节点小 -> 左边
node.lchild = self.insert(node.lchild, val)
node.lchild.parent = node
elif val > node.data:
node.rchild = self.insert(node.rchild, val)
node.rchild.parent = node
else: # 相等的情况
pass
return node
def insert_no_rec(self, val):
p = self.root
if not p: # p是空的(空树的情况下)
self.root = BiTreeNode(val)
return
while True:
if val < p.data:
if p.lchild: # 左子树存在
p = p.lchild
else: # 左子树不存在
p.lchild = BiTreeNode(val)
p.lchild.parent = p
return
elif val > p.data:
if p.rchild:
p = p.rchild
else:
p.rchild = BiTreeNode(val)
p.rchild.parent = p
return
else:
return
def pre_order(self, root):
if root:
print(root.data, end=" ")
self.pre_order(root.lchild)
self.pre_order(root.rchild)
def in_order(self, root):
if root:
self.in_order(root.lchild)
print(root.data, end=" ")
self.in_order(root.rchild)
def post_order(self, root):
if root:
self.post_order(root.lchild)
self.post_order(root.rchild)
print(root.data, end=" ")
def query(self, node, val):
"""
递归版本需要node
"""
if not node: # 如果node为空(递归的终止条件)
return None
elif node.data > val:
return self.query(node.lchild, val)
elif node.data < val:
return self.query(node.rchild, val)
else:
return node
def query_no_rec(self, val):
p = self.root
while p:
if p.data > val:
p = p.lchild
elif p.data < val:
p = p.rchild
else:
return p
else:
return None
if __name__ == '__main__':
tree = BinarySearchTree([4, 6, 7, 9, 2, 1, 3, 5, 8])
tree.pre_order(tree.root) # 4 2 1 3 6 5 7 9 8
print("")
tree.in_order(tree.root) # 1 2 3 4 5 6 7 8 9
print("")
tree.post_order(tree.root) # 1 3 2 5 8 9 7 6 4
print("")
ls = list(range(500))
random.shuffle(ls)
tree = BinarySearchTree(ls)
tree.in_order(tree.root) # 0 1 2 3 4 5 ... 99 (排好序了!)
print()
ls = list(range(0, 500, 2))
random.shuffle(ls)
tree = BinarySearchTree(ls)
print(tree.query(tree.root, 4).data) # 4
print(tree.query_no_rec(3)) # None
8.4.1 二叉搜索树 —— 删除操作
- 如果要删除的节点是叶子节点:直接删除
- 如果要删除的节点只有一个孩子:将此节点的父亲与孩子连接,然后删除该节点。
如果要删除的节点是根节点,且这个根只有一个孩子节点,那么直接更新根就行。
- 如果要删除的节点有两个孩子:将其右子树的最小节点(该节点最多有一个右孩子)删除,并替换当前节点。
在右子树中,往左走到头就是最小节点
这部分的代码如下:
min_node = node.rchild
while min_node.lchild: # 如果min_node有左孩子
min_node = min_node.lchild # 让左孩子替换min_node
下面是整体的代码:
import random
class BiTreeNode:
def __init__(self, data):
self.data = data
self.lchild = None
self.rchild = None
self.parent = None
class BinarySearchTree:
def __init__(self, ls=None):
self.root = None # 根节点
if ls:
for val in ls:
self.insert_no_rec(val) # 非递归的方法(更快)
# self.root = self.insert(self.root, val) # 递归的方法
def insert(self, node, val):
"""
node: 递归时遍历的所有节点
val: 插入的值
"""
if node is None: # 如果node为空(写成 if not node也可以)
node = BiTreeNode(val) # node为空则需要创建一个节点
elif val < node.data: # 说明插入的值比遍历的节点小 -> 左边
node.lchild = self.insert(node.lchild, val)
node.lchild.parent = node
elif val > node.data:
node.rchild = self.insert(node.rchild, val)
node.rchild.parent = node
else: # 相等的情况
pass
return node
def insert_no_rec(self, val):
p = self.root
if not p: # p是空的(空树的情况下)
self.root = BiTreeNode(val)
return
while True:
if val < p.data:
if p.lchild: # 左子树存在
p = p.lchild
else: # 左子树不存在
p.lchild = BiTreeNode(val)
p.lchild.parent = p
return
elif val > p.data:
if p.rchild:
p = p.rchild
else:
p.rchild = BiTreeNode(val)
p.rchild.parent = p
return
else:
return
def pre_order(self, root):
if root:
print(root.data, end=" ")
self.pre_order(root.lchild)
self.pre_order(root.rchild)
def in_order(self, root):
if root:
self.in_order(root.lchild)
print(root.data, end=" ")
self.in_order(root.rchild)
def post_order(self, root):
if root:
self.post_order(root.lchild)
self.post_order(root.rchild)
print(root.data, end=" ")
def query(self, node, val):
"""
递归版本需要node
"""
if not node: # 如果node为空(递归的终止条件)
return None
elif node.data > val:
return self.query(node.lchild, val)
elif node.data < val:
return self.query(node.rchild, val)
else:
return node
def query_no_rec(self, val):
p = self.root
while p:
if p.data > val:
p = p.lchild
elif p.data < val:
p = p.rchild
else:
return p
else:
return None
def __remove_node_1(self, node):
# 情况1:node是叶子结点(没有孩子)
if not node.parent: # 如果节点的父节点为空(那就是root节点)
self.root = None # 删除根节点
if node == node.parent.lchild: # node是它父亲的左孩子
node.parent.lchild = None # 删除左孩子
else: # 右孩子
node.parent.rchild = None # 删除右孩子
def __remove_node_2_1(self, node):
# 情况2-1:node只有一个左孩子节点
if not node.parent: # 根节点
# 左孩子就是新的root
self.root = node.lchild
node.lchild.parent = None
elif node == node.parent.lchild: # 是左孩子
node.parent.lchild = node.lchild
node.lchild.parent = node.parent
else: # 是右孩子
node.parent.rchild = node.lchild # node只有左孩子,没有右孩子
node.lchild.parent = node.parent
def __remove_node_2_2(self, node):
# 情况2-2:node只有一个右孩子
if not node.parent: # 是根节点
self.root = node.rchild
elif node == node.parent.lchild: # 是左孩子
node.parent.lchild = node.rchild
node.rchild.parent = node.parent
else: # 是右孩子
node.parent.rchild = node.rchild
node.rchild.parent = node.parent
def delete(self, val):
if self.root: # 如果不是空树
node = self.query_no_rec(val) # 找到val对应的node
if not node: # 如果val对应的node不存在
raise ValueError(f"数值{val}不存在!")
if not node.lchild and not node.rchild: # node是叶子节点(没有孩子)
self.__remove_node_1(node)
elif not node.rchild: # 只有一个孩子(只有左孩子)-> 2-1
self.__remove_node_2_1(node)
elif not node.lchild: # 只有一个右孩子 -> 2-2
self.__remove_node_2_2(node)
else: # 两个孩子都有
# 找右子树最小的节点
min_node = node.rchild
# 一直找左孩子节点
while min_node.lchild:
min_node = min_node.lchild
# 直接替换即可
node.data = min_node.data
# 删除min_node -> 还需要判断
if min_node.rchild: # 只有右孩子节点
self.__remove_node_2_2(min_node)
else: # 是叶子节点
self.__remove_node_1(min_node)
if __name__ == '__main__':
tree = BinarySearchTree([4, 6, 7, 9, 2, 1, 3, 5, 8])
tree.pre_order(tree.root) # 4 2 1 3 6 5 7 9 8
print("")
tree.in_order(tree.root) # 1 2 3 4 5 6 7 8 9
print("")
tree.post_order(tree.root) # 1 3 2 5 8 9 7 6 4
print("")
ls = list(range(500))
random.shuffle(ls)
tree = BinarySearchTree(ls)
tree.in_order(tree.root) # 0 1 2 3 4 5 ... 99 (排好序了!)
print()
ls = list(range(0, 500, 2))
random.shuffle(ls)
tree = BinarySearchTree(ls)
print(tree.query(tree.root, 4).data) # 4
print(tree.query_no_rec(3)) # None
tree = BinarySearchTree([1, 4, 2, 5, 3, 8, 6, 9, 7])
tree.in_order(tree.root) # 1 2 3 4 5 6 7 8 9
print()
tree.delete(4)
tree.delete(1)
tree.delete(3)
tree.delete(8)
tree.in_order(tree.root) # 2 5 6 7 9
二叉搜索树的效率
平均情况下,二叉搜索树进行搜索的时间复杂度为 O ( lg n ) O(\lg^n) O(lgn)。
但是在最坏情况下,二叉搜索树可能非常偏斜(极端情况下退化到 O ( n ) O(n) O(n))。解决方案有:
- 随机化插入
- AVL树
8.5 AVL树
AVL树:AVL树是一棵自平衡的二叉搜索树。
AVL树具有以下性质:
- 根的左右子树的高度差的绝对值不能超过1
- 根的左右子树都是平衡二叉树
高度差:针对每个节点的左右孩子数之差(有孩子为1,没孩子为0)
balance factor,平衡因子:记录了左右子树的高度之差。对于10而言,左=0,右=1,所以balance factor = 0 - 1 = -1
平衡二叉树:简单理解为树的深度一致
8.6 AVL树 —— 插入
插入一个节点可能会破坏AVL树的平衡,可以通过旋转操作来进行修正。
可视化VAL
插入一个节点后,只有从插入节点到根节点的路径上的节点的平衡可能被改变。我们需要找出第一个破坏了平衡条件的节点,称之为K
。K
的两棵子树的高度差为2。
不平衡的情况有4种。
8.6.1 左旋
不平衡是由于对K的右孩子的右子树插入导致的:左旋。
8.6.2 右旋
不平衡是由于对K的左孩子的左子树插入导致的:右旋。
8.6.3 右旋-左旋
不平衡是由于对K的右孩子的左子树插入导致的:右旋-左旋。
8.6.4 左旋-右旋
不平衡是由于对K的左孩子的右子树插入导致的:左旋-右旋。
- 左左:右旋
- 右右:左旋
- 左右:左右
- 右左:右左
8.7 二叉搜索树扩展应用 —— B树
B树(B-Tree):B树是一棵自平衡的多路搜索树,常用于数据库的索引。
比17和35小的走左边,在范围内的走中间,大的走右边。