数据结构与算法
文章目录
- 数据结构与算法
-
- 第四章、数据结构
-
- 一、线性的数据结构
-
- [1. 数据结构分类](#1. 数据结构分类)
- [2. 列表](#2. 列表)
-
- [2.1 列表/数组](#2.1 列表/数组)
- [3. 栈](#3. 栈)
-
- [3.1 定义](#3.1 定义)
- [3.2 栈的实现](#3.2 栈的实现)
- 代码实现
- [3.3 栈的应用 - 括号匹配问题](#3.3 栈的应用 - 括号匹配问题)
- [3.4 代码实现](#3.4 代码实现)
- [4. 队列](#4. 队列)
-
- [4.1 定义](#4.1 定义)
- [4.2 队列的实现](#4.2 队列的实现)
- [4.3 队列的实现 - 环形队列](#4.3 队列的实现 - 环形队列)
- [4.4 代码实现](#4.4 代码实现)
- [4.5 双向队列](#4.5 双向队列)
- [4.6 Python 队列内置模块](#4.6 Python 队列内置模块)
- [5. 栈和队列的应用 - 迷宫问题](#5. 栈和队列的应用 - 迷宫问题)
- [6. 链表](#6. 链表)
- [7. 哈希表](#7. 哈希表)
- 二、树状数据结构
-
- [1. 树](#1. 树)
-
- [1.1 树的概念](#1.1 树的概念)
- [1.2 树的实例:模拟文件系统](#1.2 树的实例:模拟文件系统)
- [2. 二叉树](#2. 二叉树)
- [3. 二叉搜索树](#3. 二叉搜索树)
-
- [3.1 二叉搜索树的概念](#3.1 二叉搜索树的概念)
- [3.2 二叉搜索树:插入](#3.2 二叉搜索树:插入)
- [3.3 二叉搜索树:查询](#3.3 二叉搜索树:查询)
- [3.4 二叉搜索树 - 删除操作](#3.4 二叉搜索树 - 删除操作)
- [3.5 二叉搜索树的效率](#3.5 二叉搜索树的效率)
- [4. AVL 树](#4. AVL 树)
-
- [4.1 AVL 树的概念](#4.1 AVL 树的概念)
- [4.2 AVL 树 - 插入](#4.2 AVL 树 - 插入)
-
- [4.2.1 AVL 插入 - 右旋](#4.2.1 AVL 插入 - 右旋)
- [4.2.2 AVL 插入 - 左旋](#4.2.2 AVL 插入 - 左旋)
- [4.2.3 AVL 插入 - 右旋-左旋](#4.2.3 AVL 插入 - 右旋-左旋)
- [4.2.4 AVL 插入 - 左旋-右旋](#4.2.4 AVL 插入 - 左旋-右旋)
- 代码实现
- [二叉搜索树的扩展应用 - B 树](#二叉搜索树的扩展应用 - B 树)
- 第五章、算法进阶
-
- [1. 贪心算法](#1. 贪心算法)
-
- [1.1 贪心算法 - 概念](#1.1 贪心算法 - 概念)
- [1.2 找零问题](#1.2 找零问题)
- [1.3 背包问题](#1.3 背包问题)
-
- [1.3.1 分数背包](#1.3.1 分数背包)
- [1.4 拼接数字问题](#1.4 拼接数字问题)
- [1.5 活动选择问题](#1.5 活动选择问题)
- [2. 动态规划](#2. 动态规划)
-
- [2.1 从斐波那契数列看动态规划](#2.1 从斐波那契数列看动态规划)
- [2.2 钢条切割问题](#2.2 钢条切割问题)
-
- [2.2.1 钢条切割问题 - 递推式](#2.2.1 钢条切割问题 - 递推式)
- [2.2.2 钢条切割问题 - 最优子结构](#2.2.2 钢条切割问题 - 最优子结构)
- [2.2.3 钢条切割问题 - 自顶向下递归实现](#2.2.3 钢条切割问题 - 自顶向下递归实现)
- [2.2.4 钢条切割问题 - 动态规划解法(自底向上实现)](#2.2.4 钢条切割问题 - 动态规划解法(自底向上实现))
- [2.2.5 钢条切割问题 - 重构解](#2.2.5 钢条切割问题 - 重构解)
- [2.3 动态规划问题关键特征](#2.3 动态规划问题关键特征)
- [2.4 最长公共子序列](#2.4 最长公共子序列)
- [3. 欧几里得算法](#3. 欧几里得算法)
- [4. RSA 加密算法简介](#4. RSA 加密算法简介)
-
- [4.1 密码与加密](#4.1 密码与加密)
- [4.2 RSA 加密算法](#4.2 RSA 加密算法)
- [4.3 RSA 加密算法过程](#4.3 RSA 加密算法过程)
第四章、数据结构
一、线性的数据结构
- 数据结构是指相互之间存在着一种或多种关系的数据元素的集合和该集合中数据元素之间的关系组成
- 简单来说,数据结构就是设计数据以何种方式组织并存储在计算机中
- 比如:列表、集合与字典都是一种数据结构
- N.Wirth:"程序 = 数据结构 + 算法"
1. 数据结构分类
- 数据结构按照其逻辑结构可分为线性结构\树结构\图结构
- 线性结构:数据结构中的元素存在一对一的相互关系
- 树结构:数据结构中的元素存在一对多的相互关系
- 图结构:数据结构中的元素存在多对多的相互关系
2. 列表
2.1 列表/数组
数组和列表有两点不一样:
- 数组元素类型要相同
- 数组长度固定
- 列表(其他语言称数组)是一种基本数据类型
- 关于列表的问题:
- 列表中的元素是如何存储的?
- 列表的基本操作:按下标查找、插入元素、删除元素...
- 这些操作的时间复杂度是多少?
- 扩展:Python 的列表是如何实现的?
- 会指向另一个存储了数据的地址(不是单纯存储在列表中)
ps:插入和删除对于列表来说都是 O(n) 的复杂度
3. 栈
3.1 定义
- 栈(Stack)是一个数据集合,可以理解为只能在一端进行插入或者删除操作的列表
- 栈的特点:后进先出 LIFO(Last-in,First-out)
- 栈的概念:栈顶、栈底
- 栈的基本操作:
- 进栈(压栈):push
- 出栈:pop
- 取栈顶:gettop

3.2 栈的实现
- 使用一般的列表结构即可实现栈
- 进栈 :
li.append
- 出栈 :
li.pop
- 取栈顶 :
li[-1]
- 进栈 :
代码实现
python
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
if __name__ == '__main__':
stack = Stack()
stack.push(1)
stack.push(2)
stack.push(3)
print(stack.pop())
3.3 栈的应用 - 括号匹配问题
- 括号匹配问题:给一个字符串,其中包括小括号、中括号、大括号,求该字符串中的括号是否匹配
- 例如:
- ()()[]{} 匹配
- ([{()}]) 匹配
- ([] 不匹配
-
(\]) 不匹配
python
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_yop() != match[ch]
return False
if stack.is_empty():
return True
else:
return False
if __name__ == '__main__':
print(brace_match('[{}{()}({[]})]'))
print(brace_match('{}{(])[]][)(){}{}{'))
4. 队列
4.1 定义
- 队列(Queue)是一个数据集合,仅允许在列表的一段进行插入,另一端进行删除
- 进行插入的一段称为队尾(rear),插入动作称为进队或入队
- 进行删除的一端称为队头(front),删除动作称为出队
- 队列的性质:先进先出(First-in,First-out)

4.2 队列的实现
- 队列能否用列表简单实现?为什么?

这样的话前面的空间会被浪费

4.3 队列的实现 - 环形队列
- 环形队列:当队首指针 front == Maxsize + 1时,再前进一个位置就自动到0
- 队首指针前进1:front = (front + 1) % MaxSize
- 队尾指针前进1:rear = (rear + 1) % MaxSize
- 队空条件:rear == front
- 队满条件:(rear + 1) % MaxSize == front
4.4 代码实现
python
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 filled")
def pop(self):
if not self.is_empty():
self.front = (self.front + 1) % self.front
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
if __name__ == '__main__':
q = Queue(5)
for i in range(4):
q.push(i)
print(q.pop())
q.push(4)
4.5 双向队列
双向队列
- 双向队列的两端都支持进队和出队操作
- 双向队列的基本操作:
- 队首进队
- 队首出队
- 队尾进队
- 队尾出队

4.6 Python 队列内置模块
- 使用方法:
from collections import deque
- 创建队列:
queue = deque()
- 进队:
append()
- 出队:
popleft()
- 双向队列队首进队:
appendleft()
- 双向队列队尾出队:
pop()
- 创建队列:
python
from collections import deque
q = deque([1, 2, 3, 4, 5], 5) # 创建一个队列【第一个参数是队列,第二个参数是最大长度(队满了前面的自动出队)】
q.append(6) # 队尾进队
print(q.popleft()) # 2 # 队首出队
# 用于双向队列
q.appendleft(1) # 队首进队
q.pop() # 队尾出队
代码实现
-
打印一个文件后五行的内容
-
txt 文件
txt
123
1234
12345
asas
xdewd
asefqrgrt
dwefwef
sdad
efwef
saa
- py 文件
python
from collections import deque
def tail(n):
with open('test.txt', 'r') as f:
q = deque(f, n)
return q
for line in tail(5):
print(line, end='') # 打印文件后五行的内容
5. 栈和队列的应用 - 迷宫问题
- 给一个二维列表,表示迷宫(0表示通道,1表示围墙)。给出算法,求一条走出迷宫的路径
- 可以通过栈和队列两种方法来做
5.1 栈 - 深度优先搜索
- 回溯法
- 思路:从一个节点开始,任意找下一个能走的点,当找不到能走的点时,退回上一个点寻找是否有其他方向的点
- 使用栈存储当前路径
代码实现
python
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]
]
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); (x, y+1)
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
if __name__ == '__main__':
maze_path(1, 1, 8, 8)
5.2 队列 - 广度优先搜索
- 思路:从一个节点开始,寻找所有接下来能继续走的点,继续不断寻找,直到找到出口
- 使用队列存储当前正在考虑的节点

代码实现
python
from collections import deque
# 定义迷宫,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]
]
# 定义四个方向的移动函数,分别是上、下、左、右
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):
"""
打印从起点到终点的路径
:param path: 路径列表,每个元素是一个元组 (x, y, parent_index)
"""
curNode = path[-1] # path的最后一个位置就是终点
realpath = []
# 从终点开始回溯到起点
while curNode[2] != -1:
realpath.append(curNode[0:2]) # 记录当前节点的坐标
curNode = path[curNode[2]] # 根据parent_index找到上一个节点
realpath.append(curNode[0:2]) # 把起点放进去
realpath.reverse() # 反转路径,使其从起点到终点
for node in realpath:
print(node)
def maze_path_queue(x1, y1, x2, y2):
"""
使用队列解决迷宫问题,找到从起点到终点的路径
:param x1: 起点的x坐标
:param y1: 起点的y坐标
:param x2: 终点的x坐标
:param y2: 终点的y坐标
:return: True if a path is found, False otherwise
"""
queue = deque()
queue.append((x1, y1, -1)) # 将起点加入队列,-1表示起点没有父节点
path = []
while len(queue) > 0:
curNode = queue.popleft() # 取出队列中的第一个节点
path.append(curNode) # 将当前节点加入路径
if curNode[0] == x2 and curNode[1] == y2:
# 到达终点,打印路径并返回True
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
if __name__ == '__main__':
maze_path_queue(1, 1, 8, 8) # 调用函数,从(1,1)走到(8,8)
6. 链表
- 链表是由一系列节点组成的元素集合。每个节点包含两部分,数据域 item 和指向下一个节点的指针 next。通过节点之间的相互连接,最终串联成一个链表
python
class Node(object):
def __init__(self, item):
self.item = item
self.next = None

python
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) # 2
print(a.next.next.item) # 3
6.1 创建链表
- 头插法

- 尾插法

代码实现
python
class Node:
def __init__(self, item):
self.item = item
self.next = None
def create_linklist_head(li): # 头插法
head = Node(li[0])
for element in li[1:]:
node = Node(element) # 在element这里创建一个新节点
node.next = head # 将行节点的下一个节点变为head
head = node # 将这个节点变为head
return head
def create_linklist_tail(li): # 尾插法
head = Node(li[0])
tail = head
for element in li[1:]:
node = Node(element) # 在element这里创建一个新节点
tail.next = node
tail = node
return head
def print_linklist(lk):
while lk:
print(lk.item, end=',')
lk = lk.next
if __name__ == '__main__':
lk1 = create_linklist_head([1, 2, 3])
lk2 = create_linklist_head([1, 2, 3, 6, 8])
print_linklist(lk1)
print()
print_linklist(lk2)
6.2 链表节点的插入和删除
插入
p.next = curNode.next
curNode.next = p


删除
p = curNode.next
curNode.next = curNode.next.next
del p


6.3 双链表
-
双链表的每个节点有两个指针:一个指向后一个节点,另一个指向前一个节点
-
如何创建双链表?
pythonclass Node(object): def __init__(self, item=None): self.item = item self.next = None self.prior = None
- 原版

- 升级版(双链表)

6.3.1 双链表节点的插入
p.next = curNode.next
curNode.next.prior = p
p.prior = curNode
curNode.next = p





6.3.2 双链表节点的删除
p = curNode.next
curNode.next = p.next
p.next.prior = curNode
del p




总结
链表 - 复杂度分析
-
顺序表(列表 / 数组)与链表
-
按元素值查找
- 顺序表 O(n)
- 链表 O(n)
-
按下标查找
- 顺序表 O(1)
- 链表 O(n)
-
在某元素后插入
- 顺序表 O(n)
- 链表 O(1)
-
删除某元素
- 顺序表 O(n)
- 链表 O(1)
-
-
链表在插入和删除的操作上明显快于顺序表
-
链表的内存可以更灵活的分配
- 试利用链表重新实现栈和队列
-
链表这种链式存储的数据结构对树和图的结构有很大的启发性
7. 哈希表
- 哈希表一个通过哈希函数来计算数据存储位置的数据结构,通常支持如下操作:
insert(key, value)
:插入键值对 (key, value)get(key)
:如果存在键为 key 的键值对则返回其 value,否则返回空值delete(key)
:删除键为 key 的键值对
直接寻址表
-
当关键字的全域 U 比较小时,直接寻址是一种简单而有效的方法
- 全域 U 就是 key 所有的可能出存在的值的集合
- 根据 U 建了一个列表 T
- 然后在2的这个位置上存值是几,3的位置上存值是几...
- 当我要查5对应的值是几时,拿5过来之后,到 T 下面的5看它的值
- 要是要把5删了就是把它设成空
-
直接寻址技术缺点:
- 当域 U 很大时,需要消耗大量内存,很不实际
- 如果域 U 很大而实际出现的 key 很少,则大量空间被浪费
- 无法处理关键字不是数字的情况

哈希
- 直接寻址表:key 为 k 的元素放到 k 位置上
- 改进直接寻址表:哈希(Hashing)
- 构建大小为 m 的寻址表 T
- key 为 k 的元素放到 h(k) 位置上【h 是个函数(哈希函数),这个函数参数能传的就是域 U 里面所有的值】
- h(k) 是一个函数,其将域 U 映射到表 T[0, 1, ..., m -1]
哈希表
-
哈希表(Hash Table,又称为散列表),是一种线性表的存储结构。哈希表由一个直接寻址表 和一个哈希函数组成。哈希函数 h(k) 将元素关键字 k 作为自变量,返回元素的存储下标
-
假设有一个长度为7的哈希表,哈希函数 h(k)=k%7。元素集合{14, 22, 3, 5}的存储方式如下图

哈希冲突
-
由于哈希表的大小是有限的,而要存储的·1值的总数量是无限的,因此对于任何哈希函数,都会出现两个不同元素映射到同一个位置上的情况
-
比如 h(k)=k%7, h(0)=h(7)=h(14)=...
解决哈希冲突 - 开放寻址法
- 开放寻址法:如果哈希函数返回的位置已经有值,则可以向后探查新的位置来存储这个值
- 线性探查:如果位置 i 被占用,则探查 i + 1, i + 2, ...
- 二次探查:如果位置 i 被占用,则探查 i + 12, i - 12, i + 22, i - 22, ...
- 二度哈希:有 n 个哈希函数,当使用第1个哈希函数 h1 发生冲突时,则尝试使用 h2, h3, ...
解决哈希冲突 - 拉链法
- 拉链法:哈希表每个位置都连接一个链表,当冲突发生时,冲突的元素将被加到该位置链表的最后

哈希表 - 常见哈希函数
-
除法哈希表:
- h(k) = k % m
-
乘法哈希表:
- h(k) = floor(m * (A * key % 1))
- m:大小
- A:一个值(小数)
- A * key % 1:A 乘以 key 对1取模,就是取得小数部分
- floor():向下取整
- h(k) = floor(m * (A * key % 1))
-
全域哈希表:
- ha,b(k) = ((a * key + b) % p) % m a, b = 1, 2, ..., p - 1
哈希表实现
python
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
def __init__(self, iterable=None):
"""
构造函数
:param iterable: 一个列表
"""
self.head = None # 开始时头节点为空
self.tail = None # 开始时尾节点为空
if iterable: # 如果列表中有值
self.extend(iterable)
def append(self, obj):
"""
插入(尾插)
:param obj: 要插入的对象
"""
s = LinkList.Node(obj) # 创建一个节点
if not self.head:
self.head = s # 因为刚开始head是空,所以s是头节点
self.tail = s # 因为刚开始head是空,所以s也是尾节点
else:
self.tail.next = s # 如果不是空就将s插到尾巴上
self.tail = s # 更新尾巴
def extend(self, iterable):
"""
循环插入
:param iterable: 要插入的列表
:return:
"""
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)) + ">>"
# 类似于集合的结构(不允许重复)
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("重复插入")
else:
self.T[i].append(k)
def find(self, k):
i = self.h(k)
return self.T[i].find(k)
if __name__ == '__main__':
ht = HashTable()
ht.insert(0)
ht.insert(1)
ht.insert(3)
ht.insert(102)
ht.insert(508)
print(",".join(map(str, ht.T)))
print(ht.find(102)) # True
print(ht.find(203)) # False
哈希表的应用 - 集合与字典
-
字典与集合都是通过哈希表来实现的
- a = {'name': 'Alex', 'age': '18', 'gender': 'Man'}
-
使用哈希表存储字典,通过哈希函数将字典的键映射为下标。假设 h('name') = 3, h('age') = 1, h('gender') = 4,则哈希表存储为[None, 18, None, 'Alex', 'Man']
-
如果发生哈希冲突,则通过拉链法或开发寻址法解决
哈希表的应用 - md5算法
- MD5(Message-Digest Algorithm 5)曾经 是密码学中常用的哈希函数,可以把任意长度的数据映射为128位的哈希值,其曾经包含如下特征:
*- 同样的消息,其 MD5 值必定相同
-
- 可以快速计算出任意给定的消息的 MD5 值
-
- 除非暴力的枚举所有可能的消息,否则不可能从哈希值反推出消息本身
- 两条消息之间即使只有微小的差别,其对应的 MD5 值也应该是完全不同、完全不相关的
-
- 不能在有意义的时间内人工的构造两个不同的消息使其具有相同的 MD5 值
哈希表的应用 - md5
- 应用举例:文件的哈希值
- 算出文件的哈希值,若两个文件的哈希值相同,则可认为这两个文件是相同的。因此:
*- 用户可以利用它验证下载的文件是否完整
-
- 云存储服务商可以利用它来判断用户要上传的文件是否已经存在于服务器上,从而实现秒传的功能,同时避免存储过多相同的文件副本
- 算出文件的哈希值,若两个文件的哈希值相同,则可认为这两个文件是相同的。因此:
哈希表的应用 - SHA2算法
- 历史上 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 or 512 位。
- SHA-2 具有和 MD5 类似的性质(参见MD5算法的特征)
哈希表的应用 - SHA2算法
- 应用举例:
- 例如,在比特币系统中,所有参与者需要共同解决如下问题:对于一个给定的字符串 U,给定的目标哈希值H,需要计算出一个字符串 V,使得 U + V 的哈希值与H的差小于一个给定值 D。此时,只能通过暴力枚举 V 来进行猜测。首先计算出结果的人可获得一定奖金。而某人首先计算成功的概率与其拥有的计算量成正比,所以其获得的奖金的 期望值与其拥有的计算量成正比。
二、树状数据结构
1. 树
1.1 树的概念
-
树是一种数据结构 比如:目录结构
-
树是一种可以递归定义的数据结构
-
树是由 n 个节点组成的集合:
- 如果 n = 0,那这是一棵空树
- 如果 n > 0,那存在1个节点作为树的根节点,其他节点可以分为 m 个集合,每个集合本身又是一棵树

- 一些概念
-
根节点、叶子节点
- 根节点:一棵树汇总起来最头上那个节点
- 叶子节点:没有孩子的
-
树的深度(高度):往下走了几层深度就是几
-
树的度:
- 节点的度就是这个节点分几个叉
- 树的度是这棵树里面所有节点的最大的那个度(哪个节点分的叉最多就拿它作为树的度)
-
孩子节点 / 父节点
- 孩子节点:eg:D 是 A 的孩子节点
- 父节点:eg:A 是 D 的父节点
-
子树:eg:EIJPQ 是整个一棵树的一棵子树
-
1.2 树的实例:模拟文件系统
代码实现
python
class Node:
def __init__(self, name, type="dir"):
self.name = name
self.type = type # "dir" or "dile"
self.children = [] # 好多next会存在这里(孩子)
self.parent = None # 指向父母
def __repr__(self): # 当调用repr()函数或直接打印对象时,返回对象的name属性值
return self.name
class FileSystemTree:
def __init__(self):
self.root = Node("/") # (根目录)根节点
self.now = self.root # 当前目录
def mkdir(self, name): # 创建文件夹
# name以 "/" 结尾
if name[-1] != "/":
name += "/"
node = Node(name)
self.now.children.append(node)
node.parent = self.now
def ls(self): # 展示当前目录下的所有目录
return self.now.children
def cd(self, name):
if name[-1] != "/":
name += "/"
if name == "../":
self.now = self.now.parent
return
for child in self.now.children:
if child.name == name:
self.now = child
return
raise ValueError("invalid dir") # 报个错
if __name__ == '__main__':
tree = FileSystemTree()
tree.mkdir("var/")
tree.mkdir("bin/")
tree.mkdir("user/")
tree.cd("bin/")
tree.mkdir("python/")
tree.cd("../")
print(tree.ls())
2. 二叉树
2.1 二叉树的概念
-
二叉树的链式存储:将二叉树的节点定义成为一个对象,节点之间通过类似链表的连接方式来连接
-
节点的定义
pythonclass BiTreeNode: def __init__(self, data): self.data = data # 存节点的数据 self.lchild = None # 左孩子 self.rchild = None # 右孩子

代码实现
python
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.rchild = f
root = e
print(root.lchild.rchild.data) # c
2.2 二叉树的遍历
二叉树的遍历方式:

- 前序遍历:EACBDGF
- 左一下右一下

- 中序遍历:ABCDEGF
- 左没了再右

- 后序遍历:BDCAFGE
- 先左后右,先到的排最后

- 层次遍历:EAGCFBD
- 每层从左到右
代码实现
python
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: # 如果不是空
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.rchild = f
root = e
pre_order(root) # E,A,C,B,D,G,F,
print()
in_order(root) # A,B,C,D,E,G,F,
print()
post_order(root) # B,D,C,A,F,G,E,
print()
level_order(root) # E,A,G,C,F,B,D,
3. 二叉搜索树
3.1 二叉搜索树的概念
- 二叉搜索树是一棵二叉树且满足性质:设 x 是二叉树的一个节点。如果 y 是 x 的左子树的一个节点,那么 y.key <= x.key;如果 y 是 x 右子树的一个节点,那么 y.key >= x.key
- 左子树上的所有节点的数值都比某个节点小,右子树上的所有节点的数值都比某个节点大
- 二叉搜索树的操作:查询、插入、删除

3.2 二叉搜索树:插入
对于二叉搜索树来说,中序遍历一定是升序的
python
import random
class BiTreeNode:
def __init__(self, data):
self.data = data # 存的数据
self.lchild = None # 左孩子
self.rchild = None # 右孩子
self.parent = None
class BST:
def __init__(self, li=None):
self.root = None
if li:
for val in li:
self.insert_no_rec(val)
def insert(self, node, val):
"""
递归的
:param node: 插到哪个节点
:param val: 值
:return: 插入到的那个节点
"""
if not node:
node = BiTreeNode(val)
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
return node
def insert_no_rec(self, val):
"""
非递归的
:param val: 值
:return:
"""
p = self.root
if not 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=',')
if __name__ == '__main__':
li = list(range(500))
random.shuffle(li)
tree = BST([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.in_order(tree.root) # 1,2,3,4,5,6,7,8,9,
print()
tree2 = BST(li)
tree2.in_order(tree2.root)
3.3 二叉搜索树:查询
python
import random
class BiTreeNode:
def __init__(self, data):
self.data = data # 存的数据
self.lchild = None # 左孩子
self.rchild = None # 右孩子
self.parent = None
class BST:
def __init__(self, li=None):
self.root = None
if li:
for val in li:
self.insert_no_rec(val)
def query(self, node, val):
"""
递归的查询
:param node: 当下的节点
:param val: 要查询的值
:return: 查询到的节点
"""
if not node: # 递归的终止条件
return None
if node.data < val:
return self.query(node.rchild, val)
elif node.data > val:
return self.query(node.lchild, val)
else:
return node
def query_no_rec(self, val):
"""
非递归的查询
:param val: 要查询的值
:return: 查询到的节点
"""
p = self.root
while p:
if p.data < val:
p = p.rchild
elif p.data > val:
p = p.lchild
else:
return p
return None # p是空的找不到
if __name__ == '__main__':
li = list(range(0, 500, 2))
random.shuffle(li)
tree = BST(li)
print(tree.query_no_rec(4).data)
print(tree.query_no_rec(3))
3.4 二叉搜索树 - 删除操作
-
- 如果要删除的节点是叶子节点:直接删除

-
- 如果要删除的节点只有一个孩子:将此节点的父亲与孩子连接,然后删除节点
- 不管当前节点是有一个左孩子还是有一个右孩子,只要他只有一个孩子,那这个孩子一定替代他的位置

-
- 如果要删除的节点有两个孩子:将其右子树的最小节点(该节点最多有一个右孩子)删除,并替换当前节点

代码实现
python
import random
class BiTreeNode:
def __init__(self, data):
self.data = data # 存的数据
self.lchild = None # 左孩子
self.rchild = None # 右孩子
self.parent = None
class BST:
def __init__(self, li=None):
self.root = None
if li:
for val in li:
self.insert_no_rec(val)
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 __remove_node_1(self, node):
"""
情况1:node是叶子系欸点
:param node: 要删除的节点
:return:
"""
if not node.parent: # 如果node的父亲是空(根节点)
self.root = None
if node == node.parent.lchild: # node是它父亲的左孩子
node.parent.lchild = None
else: # node是它父亲的右孩子
node.parent.rchild = None
def __remove_node_21(self, node):
"""
情况2.1:node只有一个孩子
:param node: 要删除的节点
:return:
"""
if not node.parent: # 根节点
self.root = node.lchild
node.lchild.parent = None
elif node == node.parent.lchild: # node是它父亲的左孩子
node.parent.lchild = node.lchild
node.lchild.parent = node.parent
else: # node是它父亲的右孩子
node.parent.rchild = node.lchild
node.lchild.parent = node.parent
def __remove_node_22(self, node):
"""
情况2.2:node只有一个右孩子
:param node: 要删除的节点
:return:
"""
if not node.parent: # 根节点
self.root = node.rchild
elif node == node.parent.lchild: # node是它父亲的左孩子
node.parent.lchild = node.rchild
node.rchild.parent = node.parent
else: # node是它父亲的右孩子
node.parent.rchild = node.rchild
node.rchild.parent = node.parent
def delete(self, val):
"""
:param val: 要删除的值
:return:
"""
if self.root: # 不是空树
node = self.query_no_rec(val)
if not node: # 不存在
return False
if not node.lchild and not node.rchild: # 1. 左孩子是空,右孩子也是空(叶子节点)
self.__remove_node_1(node)
elif not node.rchild: # 2.1 只有一个左孩子
self.__remove_node_21(node)
elif not node.lchild: # 2.2 只有一个右孩子
self.__remove_node_22(node)
else: # 3. 两个孩子都有
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_22(min_node)
else:
self.__remove_node_1(min_node)
if __name__ == '__main__':
tree = BST([1, 4, 2, 5, 3, 8, 6, 9, 7])
tree.in_order(tree.root)
print()
tree.delete(4)
tree.in_order(tree.root)
3.5 二叉搜索树的效率
- 平均情况下,二叉搜索树进行搜索的时间复杂度为 O(lgn)
- 最坏情况下,二叉搜索树可能非常偏斜
- 解决方案:
- 随机化插入
- AVL 树

4. AVL 树
4.1 AVL 树的概念
- AVL 树:AVL 树是一棵自平衡(任何一个节点两个子树的高度差不能超过1)的二叉搜索树
- AVL 树具有以下性质:
- 根的左右子树的高度之差的绝对值不能超过1
- 根的左右子树都是平衡二叉树

4.2 AVL 树 - 插入
- 插入一个节点可能会破坏 AVL 树的平衡,可以通过旋转操作来进行修正
- 插入一个节点后,只有从插入节点到根节点的路径上的节点的平衡可能被改变。我们需要找出第一个破坏平衡条件的节点,称之为 K。K 的两棵子树的高度差为2
- 不平衡的出现可能有4种情况
4.2.1 AVL 插入 - 右旋
-
- 不平衡是由于对 K 的左孩子的左子树 插入导致的:右旋

4.2.2 AVL 插入 - 左旋
-
- 不平衡是由于对 K 的右孩子的右子树 插入导致的:左旋

4.2.3 AVL 插入 - 右旋-左旋
-
- 不平衡是由于对 K 的右孩子的左子树 插入导致的:右旋 - 左旋

4.2.4 AVL 插入 - 左旋-右旋
-
- 不平衡是由于对 K 的左孩子的右子树 插入导致的:左旋-右旋

代码实现
python
from bst import BiTreeNode, BST
class AVLNode(BiTreeNode):
# 节点类
def __init__(self, data):
BiTreeNode.__init__(self, data)
self.bf = 0
class AVLTree(BST):
def __init__(self, li=None):
BST.__init__(self, li)
def rotate_left(self, p, c):
"""
左旋
:param p: 节点(上面的)
:param c: 节点(下面的)
:return:
"""
s2 = c.lchild
p.rchild = s2
if s2:
s2.parent = p
c.lchild = p
p.parent = c
p.bf = 0 # p的重量(默认左边重就是负的,右边重就是正的)
c.bf = 0 # c的重量
return c
def rotate_right(self, p, c):
"""
右旋
:param p: 节点(上面的)
:param c: 节点(下面的)
:return:
"""
s2 = c.rchild
p.lchild = s2
if s2:
s2.parent = p
c.rchild = p
p.parent = c
p.bf = 0
c.bf = 0
return c
def rotate_right_left(self, p, c):
"""
右旋 - 左旋
:param p: 节点(上面的)
:param c: 节点(下面的)
:return:
"""
g = c.lchild
s3 = g.rchild
c.lchild = s3
if s3:
s3.parent = c
g.rchild = c
c.parent = g
s2 = g.lchild
p.rchild = s2
if s2:
s2.parent = p
g.lchild = p
p.parent = g
# 更新bf
if g.bf > 0:
# 1. 插入到s3上
p.bf = -1
c.bf = 0
elif g.bf < 0:
# 2. 插入到s2上
p.bf = 0
c.bf = 1
else: # 插入的是g
p.bf = 0
c.bf = 0
g.bf = 0
return g
def rotate_left_right(self, p, c):
"""
左旋 - 右旋
:param p: 节点(上面的)
:param c: 节点(下面的)
:return:
"""
g = c.rchild
s2 = g.lchild
c.rchild = s2
if s2:
s2.parent = c
g.lchild = c
c.parent = g
s3 = g.rchild
p.lchild = s3
if s3:
s3.parent = p
g.rchild = p
p.parent = g
# 更新bf
if g.bf < 0:
# 插入到s2上
p.bf = 1
c.bf = 0
elif g.bf > 0:
# 插入到s3上
p.bf = 0
c.bf = -1
else:
p.bf = 0
c.bf = 0
g.bf = 0
return g
def insert_no_rec(self, val):
# 1. 和BST(二叉搜索树)一样,插入
"""
非递归的插入
:param val: 值
:return:
"""
p = self.root
if not p: # 空树
self.root = AVLNode(val)
return
while True:
if val < p.data:
if p.lchild:
p = p.lchild
else: # 左孩子不存在
p.lchild = AVLNode(val)
p.lchild.parent = p
node = p.lchild # node存储的就是插入的节点
break
elif val > p.data:
if p.rchild:
p = p.rchild
else:
p.rchild = AVLNode(val)
p.rchild.parent = p
node = p.rchild
break
else: # val == p.data
return
# 2. 更新balance factor
while node.parent: # 就是node不是根节点
if node.parent.lchild == node: # 传递是从左子树来的,左子树更重了
# 更新node.parent的bf -= 1
if node.parent.bf < 0: # 原来node.parent.bf == -1,更新后变成-2
# 做旋转
# 看node哪边重
g = node.parent.parent # 旋转前旋转的这棵树的根节点的父亲【为了连接旋转之后的子树】
x = node.parent # 旋转前的子树的根
if node.bf > 0: # 右边重
n = self.rotate_left_right(node.parent, node) # 旋转的这棵树的新的根节点
else: # 左边重
n = self.rotate_right(node.parent, node)
# 记得把n和g连起来
elif node.parent.bf > 0: # 原来node.parent.bf == 1,更新之后变成0
node.parent.bf = 0
break
else: # 原来node.parent.bf == 0,更新之后变成-1
node.parent.bf = -1
node = node.parent
continue
else: # 传递是从右子树来的,右子树更重了
# 更新node.parent.bf += 1
if node.parent.bf > 0: # 原来node.parent.bf == 1,更新后变成2
# 做旋转
# 看node哪边重
g = node.parent.parent # 旋转前旋转的这棵树的根节点的父亲【为了连接旋转之后的子树】
x = node.parent # 旋转前的子树的根
if node.bf < 0: # 左边重(node.bf = 1)
n = self.rotate_right_left(node.parent, node)
else: # node.bf = -1
n = self.rotate_left(node.parent, node)
# 记得连起来
elif node.parent.bf < 0: # 原来node.parent.bf == -1,更新后变成0
node.parent.bf = 0
break
else: # 原来node.parent.bf == 0,更新后变成1
node.parent.bf = 1
node = node.parent
continue
# 连接旋转后的子树
n.parent = g
if g: # 涉及根节点都要判断它是否存在
if x == g.lchild:
g.lchild = n
else:
g.rchild = n
break
else: # 说明调整的是根节点
self.root = n
break
if __name__ == '__main__':
# tree = AVLTree([7, 3, 5, 4, 2, 8, 6, 9, 1])
tree = AVLTree([9, 8, 7, 6, 5, 4, 3, 2, 1])
tree.pre_order(tree.root)
print()
tree.in_order(tree.root)
二叉搜索树的扩展应用 - B 树
- B 树(B-Tree):B 树是一棵自平衡的多路搜索树。常用于数据库的索引

第五章、算法进阶
1. 贪心算法
1.1 贪心算法 - 概念
- 贪心算法(又称贪婪算法)是指,在对问题求解时,总是做出当前看来是最好的选择。也就是说,不从整体最优上加以考虑,他所做出的是某种意义上的局部最优解
- 贪心算法并不保证会得到最优解,但是在某些问题上贪心算法的解就是最优解。要会判断一个问题是否用贪心算法来计算
1.2 找零问题
- 假设商店老板需要找零 n 元钱,钱币的面额有:100元、50元、20元、5元、1元,如何找零使得所需钱币的数量最少?
代码实现
python
t = [100, 50, 20, 5]
def change(t, n):
"""
:param t: 面额
:param n: 金额
:return: 每个面额的张数
"""
m = [0 for _ in range(len(t))]
for i, money in enumerate(t):
m[i] = n // money
n = n % money
return m, n # 最后n就是找不开的
if __name__ == '__main__':
print(change(t, 376))
1.3 背包问题
一个小偷在某个商店发现有 n 个商品,第 i 个商品价值 vi 元,重 wi 千克。他希望拿走的价值尽量高,但他的背包最多只能容纳 W 千克的东西。他应该拿走哪些商品?
- 0 - 1背包:对于一个商品,小偷要么把它完整拿走,要么留下。不能只拿走一部分,或把一个商品拿走多次。(商品为金条)
- 分数背包:对于一个商品,小偷可以拿走其中任意一部分。(商品为金砂)
举例:
- 商品1:v1 = 60 w1 = 10
- 商品2:v2 = 100 w2 = 20
- 商品3:v3 = 120 w3 = 30
- 背包容量:W = 50
对于0 - 1背包 和分数背包,贪心算法是否都能得到最优解?为什么?
- 分数背包 可以;0 - 1背包不行,要用动态规划
1.3.1 分数背包
python
goods = [(60, 10), (120, 30), (100, 20)] # 每个商品元组表示(价格, 重量)
goods.sort(key=lambda x: x[0] / x[1], reverse=True) # key=lambda x 这个是固定函数 用来分析元组第一个内容, reversed=True 降序排序
def fractional_backpack(goods, w):
"""
:param goods: 每个商品元组表示(价格, 重量)
:param w: 背包能装多种
:return: 总价值, 每个商品拿多少走
"""
m = [0 for _ in range(len(goods))] # m表示每个商品拿多少走
total_v = 0 # 总价值
for i, (price, weight) in enumerate(goods):
if w >= weight:
m[i] = 1
total_v += price
w -= weight
else:
m[i] = w / weight
total_v += m[i] * price
w = 0
break
return total_v, m
if __name__ == '__main__':
print(fractional_backpack(goods, 50)) # (240.0, [1, 1, 0.6666666666666666])
1.4 拼接数字问题
- 有 n 个非负整数, 将其按照字符串拼接的方式拼接为一个整数。如何拼接可以使得得到的整数最大?
- 例:32, 94, 128, 1286, 6, 71 可以拼接成的最大整数为 94716321286128
python
from functools import cmp_to_key
li = [32, 94, 128, 1286, 6, 71]
def xy_cmp(x, y):
if x + y < y + x: # 这时他们俩要交换
return 1
elif x + y > y + x:
return -1
else:
return 0
def number_join(li):
li = list(map(str, li)) # 将每个元素转成字符串变成一个新列表
li.sort(key=cmp_to_key(xy_cmp))
return "".join(li)
if __name__ == '__main__':
print(number_join(li)) # 94716321286128
1.5 活动选择问题
- 假设有 n 个活动,这些活动要占用同一片场地,而场地在某时间只能供一个活动使用
每个活动都有一个开始时间 si 和结束时间 fi(题目中时间以整数表示),表示活动在 [si, fi) 区间占用场地
问:安排哪些活动能够使该场地举办的活动的个数最多?
- 贪心结论:最先结束的活动一定是最优解的一部分
- 证明:假设 a 是所有活动中最先结束的活动,b 是最优解中最先结束的活动
- 如果 a = b,结论成立
- 如果 a != b,则 b 的结束时间一定晚于 a 的结束时间,则此时用 a 替换掉最优解中的 b,啊 一定不与最优解中的其他活动时间重叠,因此替换后的解也是最优解
python
activities = [(1, 4), (3, 5), (0, 6), (5, 7), (3, 9), (5, 9), (6, 10), (8, 11), (8, 12), (2, 14), (12, 16)]
# 保证活动是按照结束时间排好序的
activities.sort(key=lambda x: x[1])
def activity_selection(a):
res = [a[0]]
for i in range(1, len(a)):
if a[i][0] >= res[-1][1]: # 当前活动的开始时间小于等于最后一个入选活动的结束时间
# 不冲突
res.append(a[i])
return res
if __name__ == '__main__':
print(activity_selection(activities)) # [(1, 4), (5, 7), (8, 11), (12, 16)]
2. 动态规划
2.1 从斐波那契数列看动态规划
- 斐波那契数列:Fn = Fn-1 + Fn-2
- 练习:使用递归 和非递归的方法来求解斐波那契数列的第 n 项
python
def fibnacci(n):
if n == 1 or n == 2:
return 1
else:
return fibnacci(n - 1) + fibnacci(n - 2)
def fibnacci_no_recurision(n):
f = [0, 1, 1]
if n > 2:
for i in range(n - 2):
num = f[-1] + f[-2]
f.append(num)
return f[n]
if __name__ == '__main__':
print(fibnacci(10)) # 55
print(fibnacci_no_recurision(100)) # 354224848179261915075
-
会发现递归的执行效率慢,为什么执行效率慢呢?
- 因为子问题的重复计算
- 非递归的做法可以理解为一种动态规划(DP)的思想
-
动态规划思想
- 最优子结构(递推式)
- 重复子问题
2.2 钢条切割问题
- 某公司出售钢条,出售价格与钢条长度之间的关系如下表:
- 问题:现有一段长度为 n 的钢条和上面的价格表,求切割钢条方案,使得总收益最大化
举例:
- 长度为4的钢条的所有切割方案如下:(c 方案最优)

- 思考:长度为 n 的钢条的不同切割方案有几种?
- 2的 n - 1 次方种

r[i] 是最优解的总收益
2.2.1 钢条切割问题 - 递推式
-
设长度为 n 的钢条切割后最优收益值为 rn,可以得出递推式:
- rn = max(pn, r1 + rn-1, r2 + rn-2, ..., rn-1 + r1)
-
第一个参数 pn 表示不切割
-
其他 n-1 个参数分别表示另外 n-1 种不同切割方案,对方案 i = 1, 2, ..., n-1
- 将钢条切割为长度为 i 和 n - i 两端
- 方案 i 的收益为切割两段的最优收益之和
-
考察所有的 i,选择其中收益最大的方案
2.2.2 钢条切割问题 - 最优子结构
-
可以将求解规模为 n 的原问题,划分为规模更小的子问题:完成一次切割后,可以将产生的两段钢条看成两个独立的钢条切割问题
-
组合两个子问题的最优解,并在所有可能的两段切割方案中选取组合收益最大的,构成原问题的最优解
-
钢条满足最优子结构:问题的最优解由相关子问题的最优解组合而成,这些子问题可以独立求解
-
钢条切割问题还存在更简单的递归求解方法
-
从钢条的左边切割下长度为 i 的一段,只对右边剩下的一段继续进行切割,左边的不再切割
-
递推式简化为:
-
不做切割的方案就可以描述为:左边一段长度为 n,收益为 pn,剩余一段长度为0,收益为 r0 = 0
-
2.2.3 钢条切割问题 - 自顶向下递归实现
python
import time
def cal_time(func):
# 装饰器(算时间)
def wrapper(*args, **kwargs):
t1 = time.time()
result = func(*args, **kwargs)
t2 = time.time()
print("%s running time: %s secs." % (func.__name__, t2 - t1))
return result
return wrapper
# p = [0, 1, 5, 8, 9, 10, 17, 17, 20, 21, 23, 24, 26, 27, 27, 28, 30, 33, 36, 39, 40] # 价格表
p = [0, 1, 5, 8, 9, 10, 17, 17, 20, 24, 30]
def cut_rod_recurision_1(p, n):
if n == 0:
return 0
else:
res = p[n]
for i in range(1, n):
res = max(res, cut_rod_recurision_1(p, i) + cut_rod_recurision_1(p, n - i))
return res
@cal_time
def c1(p, n):
return cut_rod_recurision_1(p, n)
def cut_rod_recurision_2(p, n):
if n == 0:
return 0
else:
res = 0
for i in range(1, n + 1):
res = max(res, p[i] + cut_rod_recurision_2(p, n - i))
return res
@cal_time
def c2(p, n):
return cut_rod_recurision_2(p, n)
if __name__ == '__main__':
print(c1(p, 9))
print(c2(p, 9))
- 为何自顶向下递归实现的效率会这么差?
- 时间复杂度 O(2n)

2.2.4 钢条切割问题 - 动态规划解法(自底向上实现)
- 递归算法由于重复求解相同的子问题,效率极低
- 动态规划的思想:
- 每个子问题只求解一次,保存求解结果
- 之后需要此问题是,只需查找保存的结果
python
import time
def cal_time(func):
# 装饰器(算时间)
def wrapper(*args, **kwargs):
t1 = time.time()
result = func(*args, **kwargs)
t2 = time.time()
print("%s running time: %s secs." % (func.__name__, t2 - t1))
return result
return wrapper
p = [0, 1, 5, 8, 9, 10, 17, 17, 20, 21, 23, 24, 26, 27, 27, 28, 30, 33, 36, 39, 40] # 价格表
# p = [0, 1, 5, 8, 9, 10, 17, 17, 20, 24, 30]
@cal_time
def cut_rod_dp(p, n):
r = [0] # 每种长度钢条最优总收益
for i in range(1, n + 1):
res = 0
for j in range(1, i + 1):
res = max(res, p[j] + r[i - j])
r.append(res)
return r[n]
if __name__ == '__main__':
print(cut_rod_dp(p, 15))
- 时间复杂度:O(n2)

2.2.5 钢条切割问题 - 重构解
- 如何修改动态规划算法,使其不仅输出最优解,还输出最优切割方案?
- 对于每个子问题,保存切割一次时左边切下的长度

python
import time
def cal_time(func):
# 装饰器(算时间)
def wrapper(*args, **kwargs):
t1 = time.time()
result = func(*args, **kwargs)
t2 = time.time()
print("%s running time: %s secs." % (func.__name__, t2 - t1))
return result
return wrapper
p = [0, 1, 5, 8, 9, 10, 17, 17, 20, 21, 23, 24, 26, 27, 27, 28, 30, 33, 36, 39, 40] # 价格表
# p = [0, 1, 5, 8, 9, 10, 17, 17, 20, 24, 30]
def cut_rod_extend(p, n):
r = [0] # 每种长度钢条最优总收益
s = [0] # 每种长度钢条最优方案中切的最小的长度
for i in range(1, n + 1):
res_r = 0 # 价格的最大值
res_s = 0 # 价格最大值对应方案的左边不切割部分的长度
for j in range(1, i + 1):
if p[j] + r[i - j] > res_r:
res_r = p[j] + r[i - j]
res_s = j
r.append(res_r)
s.append(res_s)
return r[n], s
def cut_rod_solution(p, n):
r, s = cut_rod_extend(p, n)
ans = []
while n > 0:
ans.append(s[n])
n -= s[n]
return ans
if __name__ == '__main__':
print(cut_rod_solution(p, 20)) # [2, 6, 6, 6]
2.3 动态规划问题关键特征
-
什么问题可以使用动态规划方法?
- 最优子结构
- 原问题的最优解中涉及多少个子问题
- 在确定最优解使用哪些子问题时,需要考虑多少种选择
- 最优子结构
-
重叠子问题
2.4 最长公共子序列
一个序列的子序列是在该序列中删去若干元素后得到的序列
- 例:"ABCD" 和 "BDF" 都是 "ABCDEFG" 的子序列
最长公共子序列(LGS)问题:给定两个序列 X 和 Y,求 X 和 Y 长度最大的公共子序列
- 例:X = "ABBCBDE" Y = "DBBCDB" LCS(X, Y) = "BBCD"
应用场景:字符串相似度对比

-
最优解的递推式:
- c[i, j] 表示 Xi 和 Yj 的 LCS 长度
-
例如:要求 a = "ABCBDAB" 与 b = "BDCABA" 的 LCS
- 由于最后一位 "B" != "A"
- 因此 LCS(a, b) 应该来源于 LCS(a[:-1], b) 与 LCS(a, b[:-1]) 中更大的那一个
- 由于最后一位 "B" != "A"

代码实现
python
def lcs_length(x, y):
m = len(x)
n = len(y)
c = [[0 for _ in range(n + 1)] for _ in range(m + 1)] # 构建一个m+1行n+1列的二维列表
for i in range(1, m + 1):
for j in range(1, n + 1):
if x[i - 1] == y[j - 1]: # i和j位置上字符匹配的时候,来自于左上方+1
c[i][j] = c[i - 1][j - 1] + 1
else:
c[i][j] = max(c[i - 1][j], c[i][j - 1])
for _ in c:
print(_)
return c[m][n]
def lcs(x, y):
m = len(x)
n = len(y)
c = [[0 for _ in range(n + 1)] for _ in range(m + 1)]
b = [[0 for _ in range(n + 1)] for _ in range(m + 1)] # 1:左上方 2:上方 3:左方
for i in range(1, m + 1):
for j in range(1, n + 1):
if x[i - 1] == y[j - 1]: # i和j位置上字符匹配的时候,来自于左上方+1
c[i][j] = c[i - 1][j - 1] + 1
b[i][j] = 1 # "⬅"
elif c[i - 1][j] > c[i][j - 1]: # 来自于上方
c[i][j] = c[i - 1][j]
b[i][j] = 2 # "⬆"
else:
c[i][j] = c[i][j - 1]
b[i][j] = 3 # "↖"
return c[m][n], b
def lcs_trackback(x, y):
c, b = lcs(x, y)
i = len(x)
j = len(y)
res = []
while i > 0 and j > 0:
if b[i][j] == 1: # 来自左上方 => 匹配
res.append(x[i - 1])
i -= 1
j -= 1
elif b[i][j] == 2: # 来自于上方 => 不匹配
i -= 1 # 向上走
else: # 来自于左方 => 不匹配
j -= 1 # 向上走
return "".join(reversed(res))
if __name__ == '__main__':
print(lcs_length("ABCBDAB", "BDCABA"))
c, b = lcs("ABCDAB", "BDCABA")
for _ in b:
print(_)
print(lcs_trackback("ABCBDAB", "BDCABA")) # BDAB
3. 欧几里得算法
最大公约数
-
约数:如果整数 a 能被整数 b 整除,那么 a 叫做 b 的倍数,b 叫做 a 的约数
-
给定两个整数 a、b,两个数的所有公共约数中的最大值即为最大公约数(Greatest Common Divisor, GCD)
-
例:12与16的最大公约数是4
-
如何计算两个数的最大公约数
- 欧几里得:辗转相除法(欧几里得算法)
- 《九章算术》:更相减损术
最大公约数 - 欧几里得算法
- 欧几里得算法:gcd(a, b) = gcd(b, a mod b)
- 例:gcd(60, 21) = gcd(21, 18) = gcd(18, 3) = gcd(3, 0) = 3
python
def gcd(a, b):
if b == 0:
return a
else:
return gcd(b, a % b)
def gcd2(a, b):
while b > 0:
r = a % b
a = b
b = r
return a
if __name__ == '__main__':
print(gcd(12, 16))
print(gcd2(12, 16))
应用:实现分数计算
- 利用欧几里得算法实现一个分数类,支持分数的四则运算
python
class Fraction:
def __init__(self, a, b):
self.a = a
self.b = b
x = self.gcd(a, b)
self.a /= x
self.b /= x
def gcd(self, a, b):
while b > 0:
r = a % b
a = b
b = r
return a
def zgs(self, a, b):
x = self.gcd(a, b)
return a * b / x
def __add__(self, other):
a = self.a
b = self.b
c = other.a
d = other.b
fenmu = self.zgs(b, d)
fenzi = a * fenmu / b + c * fenmu / d
return Fraction(fenzi, fenmu)
def __str__(self):
return "%d/%d" % (self.a, self.b)
if __name__ == '__main__':
f = Fraction(30, 16)
print(f) # 15/8
a = Fraction(1, 3)
b = Fraction(1, 2)
print(a + b) # 5/6
4. RSA 加密算法简介
4.1 密码与加密
- 传统密码:加密算法是秘密的
- 现代密码系统:加密算法是公开的,密钥是秘密的
- 对称加密
- 非对称密码
4.2 RSA 加密算法
- RSA 非对称加密系统
- 公钥:用来加密的,是公开的
- 私钥:用来解密,是私有的

4.3 RSA 加密算法过程
-
- 随机选取两个质数 p 和 q
-
- 计算 n = pq
-
- 选取一个与 φ(n) 互质的小奇数 e,φ(n) = (p - 1)(q - 1)
-
- 对模 φ(n),计算 e 的乘法逆元 d,即满足 (e * d) mod φ(n) = 1
-
- 公钥 (e, n) 私钥 (d, n)
-
加密过程:c = (m ^ e) mod n
-
解密过程:m = (c ^ d) mod n