python-数据结构与算法
工具
算法可视化
https://pythontutor.com/
https://visualgo.net/zh
参考
数据结构脑图: https://naotu.baidu.com/file/b832f043e2ead159d584cca4efb19703?token=7a6a56eb2630548c
算法脑图 :https://naotu.baidu.com/file/0a53d3a5343bd86375f348b2831d3610?token=5ab1de1c90d5f3ec
https://shimo.im/docs/9ty8pjk6ckxGrkQt/read
概念
存取/存储 – 还需要再看
- 随机存取
数据读取与写入,需要的时间与该数据在存储器中的物理地址无关
典型数据类型:数组(因为根据线性公式可以轻松的访问任何一个内存地址下的数据)
- 顺序存取
数据的读取与写入,所需的时间与该数据在存储器中的物理地址有关,其是按记录的逻辑顺序进行操作的
典型数据类型:链表 (因为访问第n个数据必须先访问前n-1个数据)
- 顺序存储
把逻辑上相邻的数据元素存储在物理位置上相邻的存储单元中,数据元素之间的逻辑关系由存储单元的邻接关系表达
节省存储空间;可以实现数据元素的随机存取
不便于对数据的修改,如插入、删除等
产生磁盘碎片,因为顺序存储只能使用相邻的一整块存储单元
典型数据类型:数组
- 随机存储
用一组任意的存储单元存储线性表的元素,存储单元可以是连续的或不连续的。不要求逻辑上相邻的元素在物理地址上也相邻,而是借助指示元素存储地址的指针来表示元素的逻辑关系
不产生磁盘碎片,数据修改方便
占用内存大,随机存储的每个节点需要数据域和指针域组成
只能实现顺序存取
典型数据类型:链表
2. 算法分析
目标:程序运行时间如何随问题规模变化
控制语句与计算资源无关
赋值语句包含表达式计算和变量存储两个基本资源
例子:求和运算 T ( n ) = n + 1 T(n)=n+1 T(n)=n+1
n:变量的规模,影响算法执行时间的主要因素
n+1:算法操作步骤数
T(n):执行时间
2.1 复杂度
2.1.1 时间复杂度
输入数据大小为N时,算法运行所需要的时间;衡量计算操作随数据大小N变化时的变化情况
时间:算法的计算操作数
类型
最差:大 O O O表示法,平均,最佳
2.1.2 空间复杂度
输入数据大小为N时,算法运行所使用的的暂存空间+输出空间大大小
输入空间:存储输入数据所需的空间大小
暂存空间:算法运行过程中,存储所有中间变量和对象等数据所需的空间大小
输出空间:算法运行返回时,存储输出数据所需的空间大小
暂存空间 https://leetcode.cn/leetbook/read/illustration-of-algorithm/r8ytog/
指令空间
编译后,程序指令所使用的内存空间
数据空间
变量使用的空间
声明的常量、变量、动态数据、动态对象栈帧空间
程序调用函数是基于栈实现的,函数再调用期间,占用常量大小的栈帧空间,直至返回后释放
栈帧空间的累计常出现于递归调用中
2.2 大 O O O表示法
数量级函数 O ( f ( n ) ) O(f(n)) O(f(n))
描述 T ( n ) T(n) T(n)中,随规模n增加时, T ( n ) T(n) T(n)中增长最快的部分
例子:
二分查找
l
o
g
2
n
log_{2}^{n}
log2n
2.3 数据类型性能
list
索引、赋值、append()
添加都是
O
(
1
)
O(1)
O(1),与执行时间无关
a=list()+[k]
是
O
(
n
+
k
)
O(n+k)
O(n+k)
二分查找
https://blog.csdn.net/qq_45978890/article/details/116094046
数组/字符串
解题方法
- 逆序遍历
- 双指针吧,首选双指针
快慢指针:fast快速遍历,slow标志已完成的列表尾部,或待处理的元素头部
对撞指针:left指针,right指针
- 哈希表(字典),第二考虑字典
- 位(元素位)运算
- 结合排序算法
- 匹配相关KMP-还没看
栈
先进后出
深度优先DFS
队列
先进先出
from collections import deque
queue=deque() # 实例化
quque.append(1) # 入队
queue.popleft() # 出队
广度优先BFS
位运算
双端队列
首尾均能进出
链表
边+节点组成
每个节点(必须)包括
数据本身
指向下一个节点的引用信息
python 链表是基于
list
数据结构实现的
伪头(使得数据结构中永不为空)
双链表不仅包括伪头还包括伪尾
class ListNode:
def __init__(self,x):
self.val=x
self.next=None
解题方法
- 遍历法
- 双指针,优化遍历
- 递归,!!!
递归
实际上使用的是调用栈进行递归
- 必须有一个基本结束条件(最小规模问题的直接解决)
- 改变状态,向基本结束条件演变(减小问题规模)
- 调用自身
import sys
sys.getrecursionlimit() # 系统调用栈最大深度
sys.setrecursionlime(number) # 设置调用栈最大深度
递归与分而治之
分而治之是递归的思想,递归是分而治之的代码实现
递归与递推
递归:自顶向下,直接面向问题,直接解决问题
递推:自底向上,从这个问题最开始的样子出发,一点点的逐步演化成为最终想要解决问题的样子自底向上思考的方向直接从一个问题的源头开始,逐步求解
自顶向下,先拆分,需要借助数据结构(栈)记录拆分过程中的每个子问题
动态规划
问题的最优解包含了更小规模子问题的最优解,该问题可以能够用动态规划求解
在问题可被分解为彼此独立且离散的子问题时,可以使用动态规划,子问题依赖动态规划不行
动态规划的两个思考方向
记忆化递归:对应了上述自顶向下的问题解决方向
递推:对应了自底向上的逐步求解问题的方向
动态规划与递归
递归是减小规模
动态规划是增加规模求解
贪婪算法
每一步选择局部最优解
排序
冒泡排序
每个epoch逐次交换相邻的顺序
for j in range(len(nums)-1,0,-1):
for i in range(j): # j=len(nums)-1
if nums[i]>nums[i+1]:
nums[i],nums[i+1]=nums[i+1],nums[i]
选择排序
每个epoch只交换最大的顺序
for j in range(len(nums)-1,0,-1):
num_max_idx=j
for i in range(j):
if nums[i]>nums[num_max_idx]:
num_max_idx=i
nums[j],nums[num_max_idx]=nums[num_max_idx],nums[j]
归并排序
先分解:二分查找
然后递归 归并(合并排序)
快速排序
分治思想–递归
中位值
左右游标,交换,递归
def quicksort(arr,left=None,right=None):
left=0 if not isinstance(left,(int,float)) else left
right=len(arr)-1 if not isinstance(right,(int,float)) else right
if left<right:
partitionIndex=partition(arr,left,right) # 进行划分
quicksort(arr,left,partitionIndex-1) # 左半部分
quicksort(arr,partitionIndex+1,right) # 右半部分
return arr
def partition(arr,left,right): # 以最左边为参考基准,在传入right的时候,这里并不包括分割点
pivot=left
index=pivot+1
i=index
while i<=right:
if arr[i]<arr[pivot]:
swap(arr,i,index)
index+=1
i+=1
swap(arr,pivot,index-1)
return index-1
def swap(arr,i,j): # 交换数值,inplace
arr[i],arr[j]=arr[j],arr[i]
计数排序
堆排序
希尔排序
基数排序
稳定排序
相同数据下,相对位置不发生改变
稳定:冒泡,插入,归并
不稳定:选择,快速
散列表(字典)
散列表
建立了关键字和存储地址之间的一种直接映射关系
根据给定的关键字来计算出关键字在表中的地址的数据结构
散列函数
将数据项映射到一个散列槽中
就是一个 把查找表中的 关键字 映射成该关键字对应的地址 的函数 address=hash(key)
完美散列函数,能够保证所有数据都能够确定一个唯一的散列槽,没有任何冲突
见哈希算法
冲突解决
开放地址方法,线性探测
会出现数据聚集的现象
跳跃探测
散列表示一个稀疏数组(总是有空白元素的数组称为稀疏数组)(注释:散列表的单元叫做表元,在dict的散列表中,每个键值对都占用一个表元,每个表元有两个部分,一个是对键的引用,一个是对值得引用,因为所有表元的大小一致,所以可以通过偏移量来读取某个表元)
如果要把一个对象放入散列表中,首先要计算这个元素键的散列值,利用hash()
算法
树
- 节点
每个节点具有名称,或键值对,节点还可以保存额外数据项,数据项根据不同的应用而变
兄弟节点:同一父节点的节点
- 边
根节点:只有出边
叶节点:只有入边
- 层级
根节点算层级0
从根节点开始到达一个节点的路径所包含的边的数量
- 高度
树中所有节点的最大层级
(树种所有节点的最大层级数+1)
class TreeNode:
def __init__(self,val=0,left=None,right=None):
self.val=val
self.left=left
self.right=right
遍历方法
- 栈 – 前序遍历
- 递归 树实际上具有递归定义
二叉树
节点从根开始的路径是唯一的
每个节点最多有两个子节点(最多有n个节点叫做n叉树)
遍历
- 先序遍历:根-左-右
- 中序遍历:左-根-右
- 后序遍历:左-右-根
- 层序遍历:广度优先,队列数据结构(先、中、后序遍历利用的都是深度优先)
迭代遍历-栈 – 深度优先
补充,统一迭代写法https://leetcode.cn/problems/binary-tree-postorder-traversal/solution/er-by-forestsking-17uq/
https://leetcode.cn/problems/binary-tree-inorder-traversal/solution/yan-se-biao-ji-fa-yi-chong-tong-yong-qie-jian-ming/
# 前序遍历
# res 遍历的结果
# stack 模拟栈
res,stack=[],[]
stack.append(root)
while stack:
node=stack.pop()
res.append(node.val)
if node.right is not None:
stack.append(node.right)
if node.left is not None:
stack.append(node.left)
# 中序遍历 这个版本不好理解
res,stack=[],[]
while stack or curr:
while curr:
stack.append(curr)
curr=curr.left
node=stack.pop()
res.append(node.val)
curr=node.right
# 后序遍历,逆向思维-巧妙解法(非正规)
res,stack=[],[]
stack.append(root)
while stack:
node=stack.pop()
res.append(node.val)
if node.left is not None:
stack.append(node.left)
if node.right is not None:
stack.append(node.right)
res.reverse()
树与递归
# 前序遍历
node=root
res=[]
def preorder(node,res):
if node is None:
return
res.append(node.val)
self.preorder(node.left,res)
self.preorder(node.right,res)
# 中序遍历
def inorder(node,res):
if node is None:
return
self.inorder(node.left,res)
res.append(node.val)
self.indorder(node.right,res)
# 后序遍历
def inorder(node,res):
if node is None:
return
self.inorder(node.left,res)
self.indorder(node.right,res)
res.append(node.val)
二叉搜索树
对于二叉搜索树,可以通过中序遍历得到一个递增的有序序列
完全二叉树
https://www.bilibili.com/video/BV1W54y1X7A9?p=8
哈希/加密
哈希
哈希函数可以把任意长度的数据(字节串)计算出一个固定长度的结果数据
特点
相同的源数据,采用相同的哈希算法,计算出来的哈希值一定相同
不能的源数据,使用相同的哈希算法,可能会产生相同的值,叫做碰撞率
不管源数据多大,相同的哈希算法,计算出来的哈希值长度一样长
哈希值结果长度越长,碰撞率越低,(计算的市场也越长)
算法不可逆,不能通过哈希值反算出源数据(哈希不同于加密解密)
应用场景
- 校验信息
计算哈希值https://www.mobilefish.com/services/hash_generator/hash_generator.php
import hashlib
加密解密
加解密算法,可逆
哈希算法对很大的数据产生比较小的哈希值,而加密算法,如果源数据很大,加密后的数据也很大
分类
对称加密:加密和解密使用相同的秘钥
不对称加密:加解密使用不同的秘钥,公钥(用于加密),私钥(用于解密)
第三方库
公钥私钥
逻辑关系https://zhuanlan.zhihu.com/p/113522792