本笔记为本人基于Mooc课程–数据结构整理总结,可用于算法基础学习。
声明:如要转载,请与博主联系,转载请注明出处。
数据对象在计算机中的组织方式
■逻辑结构
■物理存储结构
数据对象必定与一系列加在其上的操作相关联,完成这些操作的方法就是算法
解决问题的方法效率,与数据组织的结构息息相关,也与空间的利用效率有关,还与算法的巧妙程度有关。(time.process_time()
,用来计算程序执行时间)
只描述数据对象集和相关操作集“是什么”,并不涉及“如何做到”的问题。
一个有限指令集,接受一些输入(有时没有),产生输出,一定在有限步骤之后终止
空间复杂度S(n)–占用存储单元的长度
时间复杂度T(n)–耗费时间的长度
衡量一般算法效率用到两种复杂度:
■最坏情况复杂度(主要看这个): T w o r s t ( n ) T_{worst}(n) Tworst(n)
■平均复杂度: T a v g ( n ) T_{avg}(n) Tavg(n)
采用复杂度的渐进表示法: 上界 : O ( f ( n ) ) ∣ 下界 : Ω = Θ ( h ( n ) ) {上界:O(f(n)) | 下界:Ω=Θ(h(n))} 上界:O(f(n))∣下界:Ω=Θ(h(n))
算法复杂度从小到大: l o g n log n logn、 n n n、 n l o g n n log n nlogn、 n 2 n^2 n2、 2 n 2^n 2n
把复杂问题切分成小块,分头解决,最后把结果合并。
线性表(List):由同类型数据元素构成有序线性序列,表中元素个数称为线性表的长度。
线性表顺序存储实现:利用数组的连续存储空间顺序存放线性表的各元素
线性表链式存储实现:不要求逻辑上相邻的两个元素物理上也相邻,通过’‘链’'建立起数据元素之间的逻辑关系
增删改查
多重链表、十字链表
堆栈(Stack):具有一定操作约束的线性表,只在一端(栈顶,Top)做插入(Push)、删除(Pop)。后进先出(Last In First Out, LIFO)
栈的顺序存储实现:栈的顺序存储结构通常由一个 一维数组 和一个记录 栈顶 元素位置的变量组成。
栈的链式存储实现:栈的链式存储结构实际就是一个 单链表 ,叫做 链栈 。插入和删除操作只能在链栈的栈顶进行。Top只能在链表首端。
后缀表达式求值策略:从左向右“扫描”,逐个处理运算数和运算符号
中缀表达式求值策略:将中缀表达式转换为后缀表达式,然后求值。
堆栈其他应用:函数调用及递归实现、深度优先搜索、回溯算法等
队列(Queue):具有一定操作约束的线性表。其只能在 一端插入(入队列,AddQ) ,而在 另一端删除(出队列,DeleteQ) 。 先进先出(FIFO) 。
队列的顺序存储实现:队列的顺序存储结构通常由一个一维数组和一个记录队列头元素位置的变量 front 以及一个记录队列尾元素位置的变量 rear 组成
顺环队列使用额外标记Size或tag来判断是否已满,求余函数
队列的链式存储实现:队列的链式存储结构也可以用一个 单链表 实现。插入和删除操作分别在链表的两头进行。rear指向队尾节点,front指向队头结点
采用不带头结点的单向链表,按照指数递减的顺序排列各项。
树(Tree):表现得是一种层次关系,为 n ( n ≥ 0 ) n(n≥0) n(n≥0)个节点构成的有限集合,当n=0时,称为空树,对于任一颗非空树(n>0),它具备以下性质:
■树中有一个根(root)节点,用r表示
■其余节点可分为m(m>0)个互不相交的有限集 T 1 , T 2 , . . . , T m \bold{T_1,T_2, ...,T_m} T1,T2,...,Tm,其中每个集合本身又是一棵树,称为原来树的”子树(Subtree)“。子树不相交;除了根结点外,每个结点有且仅有一个父结点;一颗N个结点的树有N-1条边。
查找(Searching):根据某个给定关键字K,从集合R中找出关键字与K相同的记录 。
静态查找和动态查找,静态查找不涉及删除和插入。
静态查找( O ( n ) O(n) O(n)):
顺序查找(Sequential Search)可以设置哨兵(查找条件限制)
二分查找(Binary Search, O ( l o g N ) O(logN) O(logN))需是有序数组,判定树上每个节点需要的查找次数刚好为该节点所在层数。
# 二分查找算法
def BinarySearch(list, k)
"""在列表中查询关键字为k的数据元素"""
left = 1 # 初始左边界
right = len(list) # 初始右边界
while left == right:
mid = (left + right) / 2 # 计算中间元素坐标
if k < list[mid]:
right = mid - 1 # 调整右边界
elif k > list(mid):
left = mid + 1 # 调整左边界
else:
return mid # 查找成功,返回数据元素下标
return NotFound # 查找不成功,返回-1
树的一些基本术语:
1.结点的度(Degree):结点的子树个数
2.树的度:树的所有结点中最大的度数
3.叶结点(Leaf):度为0的结点
4.父结点(Parent):有子树的结点是其子树的根节点的父结点
5.子结点(Child):若A结点是B结点的父结点,则称B结点是A结点的子结点;子结点也称孩子结点。
6.兄弟结点(Sibling):具有同一父结点的各结点是彼此的兄弟结点。
7.路径和路径长度:从结点 n 1 n_1 n1到 n x n_x nx的路径为一个结点序列 n , n 2 , . . . , n x n,n_2 ,... , n_x n,n2,...,nx, n i n_i ni是 n i + 1 n_{i+1} ni+1的父结点。路径所包含边的个数为路径的长度。
9.祖先结点(Ancestor):沿树根到某一结点路径上的所有结点都是这个结点的祖先结点。
10.子孙结点(Descendant):某一结点的子树中的所有结点是这个结点的子孙。
11.结点的层次(Level):规定根结点在1层,其它任一结点的层数是其父结点的层数加1。
12.树的深度( Depth):树中所有结点中的最大层次是这棵树的深度。
儿子–兄弟表示法
二叉树T:一个有穷的节点集合。度为2的一种树
二叉树有五种基本形式:
特殊二叉树:斜二叉树、完美二叉树、完全二叉树
性质:
1.一个二叉树第i层的最大结点数为: 2 i − 1 , i ≥ 1 2^{i-1},i≥1 2i−1,i≥1
2.深度为k的二叉树有最大结点总数为: 2 k − 1 , k ≥ 1 2^k-1,k≥1 2k−1,k≥1
3.对任何非空二叉树 T T T,若 n 0 n_0 n0表示叶结点的个数、 n 2 n_2 n2是度为2的非叶结点个数,那么两者满足关系 n 0 = n 2 + 1 n_0=n_2+1 n0=n2+1
对二叉树的重要操作:主要是遍历。
1.顺序存储结构
完全二叉树:按从上至下、从左到右顺序结构
2.链表存储
常用遍历方法有先序(根、左、右)、中序(左、根、右)、后序(左、右、根)、层次遍历(从上到下,从左到右)
先序、中序、和后序遍历过程(递归):遍历过程中经过结点的路线一样,只是访问各结点的时机不同。
基本思路:使用堆栈
二叉树遍历的核心问题:二维结构的线性化
需要一个存储结构保存暂时不访问的结点,存储结构可以为堆栈、队列
队列实现:遍历从根结点开始,首先将根结点入队,然后开始执行循环:结点出队、访问该节点、其左右儿子入队
结构数组表示二叉树:静态链表
二叉搜索树(BST, Binary Search Tree)满足以下性质:
1.非空左子树的所有键值小于其根结点的键值。
2.非空右子树的所有键值大于其根结点的键值。
3.左右子树都是二叉搜索树。
查找(Find):最大元素(最右边结点),最小元素(最左边节点)
插入(Insert):先查找后删除
删除(Delete):考虑三种情况,只删除叶结点、删除只有一个孩子的结点、要删除的结点有左右两棵子树
平衡二叉树(Balanced Binary Tree)AVL树,空树或者任意结点左、右子树高度差的绝对值不超过1,即 ∣ B F ( T ) ∣ ≤ 1 |BF(T)|≤1 ∣BF(T)∣≤1
平衡因子(Balance Factor, 简称BF): B F ( T ) = h L − h R BF(T)=h_L-h_R BF(T)=hL−hR,
其中 h L h_L hL和 h R h_R hR分别为T的左、右子树的高度
给定结点数为n的AVL树的最高高度为 O ( l o g 2 n ) O(log_2n) O(log2n) .
RR旋转(右单旋) 、LL旋转(左单旋)、LR旋转、RL旋转
优先队列(Priority Queue):取出元素的顺序按照元素的 优先权(关键字) 大小,而不是元素进入队列的先后顺序。
堆的两个特性:
结构性:用数组表示的完全二叉树;
有序性:任意结点的关键字是其子树所有结点的最大值(或最小值)
⬛️“最大堆(MaxHeap)”,也称”大顶堆“:最大值
⬛️“最小堆(MinHeap)”, 也称"小顶堆":最小值
插入:可设置哨兵作为插入的限制条件, T ( N ) = O ( l o g N ) T(N)=O(logN) T(N)=O(logN)
最大堆的插入
删除:
最大堆的删除
建立:
建立最大堆:将已经存在的N个元素按最大堆的要求存放在一个一维数组中
哈夫曼树(Huffman Tree)或最优二叉树:WPL最小的二叉树
带权路径长度(WPL):设二叉树有n个叶子结点,每个叶子结点带有权值 W k W_k Wk,从根结点到每个叶子结点的长度为 I k I_k Ik,则每个叶子结点的带权路径长度之和就是: W P L = ∑ k = 1 n w k l k WPL=\sum_{k=1}^n w_kl_k WPL=∑k=1nwklk.
哈夫曼树的构造:每次把权值最小的两棵二叉树合并
哈夫曼树的特点:
◼️ 没有度为1的结点
◼️n个叶子结点的哈夫曼树共有2n-1个结点
◼️哈夫曼树的任意非叶结点的左右子树交换后仍是哈夫曼树;
◼️对同一组权值 w 1 , w 2 , . . . . . . , w n {w_1,w_2,......,w_n} w1,w2,......,wn,可能存在不同构的哈夫曼树
不等长编码
前缀码(prefix code):任何字符的编码都不是另一字符编码的前缀
集合运算:交、并、补、差,判定一个元素是否属于某一集合
并查集:集合并、查某元素属于什么集合
(1)查找某个元素所在的集合(用根结点表示)
(2)集合的并运算
分别找到x1和x2两个元素所在集合树的根结点
如果它们不同根,则将其中一个根结点的父结点指针设置 成另一个根结点的数组下标
TSSN:按秩归并、路径压缩
图(Graph):表示“多对多”的关系
包含一组顶点V(Vertex),一组边E(Edge)
常见术语:无向图、有向图、权重、网络…
在程序中表示:
邻接矩阵G[N][N]–N个顶点从0到N-1编号,浪费时间、空间
对于无向图的存储,用一个长度为N(N+1)/2的一维数组A存储可以省一半空间
邻接表:G[N]为指针数组,对应矩阵每行一个链表,只存非0元素,够稀疏才合算
深度优先搜索(Depth First Search, DFS) 类似于树的先序遍历
广度优先搜索(Breadth First Search, BFS) 类似于树的层次遍历
有N个顶点、E条边,时间复杂度是:
用邻接表存储图,有O(N+E)
用邻接矩阵存储图,有O( N 2 N^2 N2)
在网络中,求两个不同顶点之间的所有路径中,边的权值之和最小的那一条路径
最短路径(shortist path)、源点(source)、终点(destination)
单源\多源最短路径问题
无权图的单元最短路算法:按照 递增(非递减) 的顺序找到各个顶点的最短路(BFS)
有权图的单源最短路算法:按照递增的顺序找出到各个顶点的最短路(Dijkstra算法)
多源最短路算法:
方法一:直接将单元最短路算法调用|V|遍, T = O ( ∣ V ∣ 3 + ∣ E ∣ × ∣ V ∣ ) T =O(|V|^3 + |E| × |V|) T=O(∣V∣3+∣E∣×∣V∣)
对于稀疏图效果好
方法二:Floyd算法, T = O ( ∣ V ∣ 3 ) T=O(|V|^3) T=O(∣V∣3) ,对于稠密图效果好
贪心算法
基本思想:让一棵小树长大(稠密图合算)
基本思想:将森林合并成树(稀疏图合算)
AOV网络(Activity on Vertex)
拓扑序:如果图中从V到w有一条有向路径,则v一定排在w之前。满足此条件的顶点序列称为一个拓扑序。获得一个拓扑序的过程就是拓扑排序。AOV如果有合理的拓扑序,则必定是有向无环图(Directed Acyclic Graph, DAG)
关键路径问题:AOE网络(Activity on Edge),由 绝对不允许延误 的活动组成的路径
一般用于安排项目工序
冒泡排序(最好情况: T = O ( N ) T=O(N) T=O(N),最坏情况: T = O ( N 2 ) T=O(N^2) T=O(N2))
插入排序(最好情况: T = O ( N ) T=O(N) T=O(N),最坏情况: T = O ( N 2 ) T=O(N^2) T=O(N2))
对于下标 i < j , i
每次交换消掉一个逆序对
⬛️ 定理:任意N个不同元素组成的序列平均具有 N ( N − 1 ) / 4 N(N-1)/4 N(N−1)/4个逆序对。
⬛️定理:任何仅以交换相邻元素来排序的算法,其平均时间复杂度为 Ω ( N 2 ) Ω(N^2) Ω(N2)
定义增量序列 D M > D M − 1 > . . . > D 1 = 1 D_M>D_{M-1}>...>D_1=1 DM>DM−1>...>D1=1
对每个 D K D_K DK进行“ D k D_k Dk-间隔”排序( k = M , M − 1 , . . . 1 k=M,M-1,...1 k=M,M−1,...1)
最坏情况: T = Θ ( N 2 ) T=Θ(N^2) T=Θ(N2)
最大堆—>最小堆
定理:堆排序处理N个不同元素的随机排列的平均比较次数是 2 N l o g N − O ( N l o g l o g N ) 2NlogN-O(NloglogN) 2NlogN−O(NloglogN)
虽然堆排序给出最佳平均时间复杂度,但实际效果不如用Sedgewick增量序列的希尔排序。
核心:有序子列的归并
核心:分而治之
统一函数接口
核心:一趟归并
分而治之
取头、中、尾的中位数
伪代码:
小规模数据的处理:
■快速排序的问题
用递归…
对小规模的数据可能还不如插入排序快
■解决方案:
当递归的数据规模充分小,则停止递归,直接调用简单排序(例如插入排序)
在程序中定义一个Cutoff的阈值
间接排序:定义一个字典来用键值对作为“表”(table)
N个数字的排列由若干个独立的环组成
桶排序:
基数排序:
次位排序(L east S ignificant D igit)
主位优先(M ost S ignificant D igit)
查找的本质:已知对象找位置
——>有序安排对象:全序、半序
——>直接”算出“对象位置:散列
散列查找法的两项基本工作:
♦计算位置:构造散列函数确定关键词存储位置;
♦解决冲突:应用某种策略解决多个关键词位置相同的问题
散列表(哈希表)
类型名称:符号表(SymbolTable)
数据对象集:符号表是”名字(Name)-属性(Attribute)“对的集合。
操作集:增删改查
装填因子(Loading Factor):设散列表空间大小为m,t填入表中元素个数是n,则称 α = n / m α=n/m α=n/m为散列表的装填因子
散列(Hashing的基本思想是:
(1)以关键字 key为自变量,通过一个确定的函数h(散列函数),计算出对应的函数值h(key),作为数据对象的存储地址。
(2)可能不同的关键字会映射到同一个散列地址上,即 h ( k e y i ) = h ( k e y j ) \bold {h(key_i)}=\bold {h(key_j)} h(keyi)=h(keyj)
(当 k e y i ≠ k e y j \bold {key_i} ≠ \bold {key_j} keyi=keyj),称为”冲突(Collosion)“,需要某种冲突解决策略。
一个好的散列函数需考虑以下两个因素:
1.计算简单,以便提高转换速度;
2.关键词对应的地址空间分布均匀,以尽量减少冲突。
1.直接定址法:取关键词的某个线性函数值为散列地址,即 h ( k e y ) = a × k e y + b h(key)=a × key + b h(key)=a×key+b(a、b为常数)
2.除留余数法
散列函数为: h ( k e y ) = k e y m o d p h(key) = key \ mod \ p h(key)=key mod p,一般p取 素数
3.数字分析法
分析数字关键字在各位上的变化情况,取比较随机的位作为散列地址
4.折叠法
把关键词分割成位数相同的几个部分,然后叠加
5.平方取中法
把一个数平方后取中间几位数
1.一个简单的散列函数——ASCLL码加和法
对字符型关键词key定义散列函数如下: h ( k e y ) = ( ∑ k e y [ i ] ) m o d T a b l e S i z e h(key) = (\sum key[i]) \ mod \ TableSize h(key)=(∑key[i]) mod TableSize(冲突严重)
2.简单的改进——前3个字符移位法(仍然冲突,空间浪费)
h ( k e y ) = ( k e y [ 0 ] × 2 7 2 + k e y [ 1 ] × 27 + k e y [ 2 ] ) m o d T a b l e S i z e h(key) = (key[0]×27^2 + key[1]×27 + key[2])mod \ TableSize h(key)=(key[0]×272+key[1]×27+key[2])mod TableSize
3.好的散列函数——移位法
涉及关键词所有个字符,并且分布得很好:
h ( k e y ) = ( ∑ i = 0 n − 1 k e y [ n − i − 1 ] × 3 2 i ) m o d T a b l e S i z e h(key) = (\sum^{n-1}_{i=0}key[n-i-1]×32^i) \ mod \ TableSize h(key)=(∑i=0n−1key[n−i−1]×32i) mod TableSize
常用处理思路:
换个位置:开放地址法
同一位置的冲突对象组织在一起:链地址法
若发生了第i次冲突,试探的下一个地址将增加 d i d_i di,基本公式是:
h i ( k e y ) = ( h ( k e y ) + d i ) m o d T a b l e S i z e ( 1 ≤ i < T a b l e S i z e ) h_i(key) = (h(key)+d_i) \ mod \ TableSize \ (1≤i
d i d_i di决定了不同的解决冲突方案:线性探测、平方探测、双散列,其 d i d_i di分别为 i 、 ± i 2 、 i ∗ h 2 ( k e y ) i、±i^2、i*h_2(key) i、±i2、i∗h2(key)
以**增量序列1,2,…,(TableSize-1)**循环试探下一个存储地址。
散列表查找性能分析:
成功平均查找长度(ASLs)
不成功平均查找长度(ASLu)
以增量序列 1 2 , − 1 2 , 2 2 , − 2 2 , … … , q 2 , − q 2 1^2, {-1}^2, 2^2, -2^2, ……, q^2, -q^2 12,−12,22,−22,……,q2,−q2且 q ≤ [ T a b l e S i z e / 2 ] q≤[TableSize/2] q≤[TableSize/2]循环试探下一个存储地址。
有定理显示:如果散列表长度TableSize是某个4k+3(k是正整数)形式的素数时,平方探测法就可以探查到整个散列表空间
双散列探测法: d i d_i di为 i ∗ h 2 ( k e y ) , 2 h 2 ( k e y ) , 3 h 2 ( k e y ) , . . . . . . i*h_2(key), 2h_2(key), 3h_2(key), ...... i∗h2(key),2h2(key),3h2(key),......
对任意的key, h 2 ( k e y ) ≠ 0 h_2(key)≠0 h2(key)=0!
探测序列还应该保证所有的散列存储单元都应该能够被探测到。
选择以下形式有良好效果: h 2 ( k e y ) = p − ( k e y m o d p ) h_2(key) = p - (key \ mod \ p) h2(key)=p−(key mod p)
其中:p 再散列(Rehashing) 当散列表元素太多(即装填因子α太大时),查找效率会下降。实用最大装填因子一般取 0.5 ≤ α ≤ 0.85 \bold {0.5≤α≤0.85} 0.5≤α≤0.85 散列表扩大时,原有元素需要重新计算放置到新表中 将相应位置上冲突的所有关键词存储在同一个单链表中 1.线性探测法的查找性能 可以证明,线性探测法的期望探测次数满足下列公式: p = { 1 2 [ 1 + 1 ( 1 − α ) 2 ] ( 对插入和不成功查找而言 ) 1 2 ( 1 + 1 1 − α ) ( 对成功查找而言 ) p=\begin{cases} \frac{1}{2}[1+\frac{1}{(1-α)^2}] \ (对插入和不成功查找而言) \\ \frac{1}{2}(1+\frac{1}{1-α}) \ (对成功查找而言)\end{cases} p={21[1+(1−α)21] (对插入和不成功查找而言)21(1+1−α1) (对成功查找而言) 2.平方探测法和双散列探测法的查找性能 可以证明,平方探测法和双散列探测法探测次数满足下列公式: p = { 1 1 − α ( 对插入和不成功查找而言 ) − 1 α l n ( 1 − α ) ( 对成功查找而言 ) p=\begin{cases} \frac{1}{1-α} \ (对插入和不成功查找而言) \\ -\frac{1}{α}ln(1-α) \ (对成功查找而言)\end{cases} p={1−α1 (对插入和不成功查找而言)−α1ln(1−α) (对成功查找而言) 3.分离链接法的查找性能 所有地址链表的平均长度定义成装填因子α,α有可能超过1. 不难证明:其期望探测次数p为: p = { α + e − α ( 对插入和不成功查找而言 ) 1 + α 2 ( 对成功查找而言 ) p=\begin{cases} α+e^{-α} \ (对插入和不成功查找而言) \\ 1+\frac{α}{2} \ (对成功查找而言)\end{cases} p={α+e−α (对插入和不成功查找而言)1+2α (对成功查找而言) 串是线性存储的一组数据(默认是字符) T = O(n+m) m a t c h ( j ) = { 满足 p 0 . . . p i = p i − j . . . p j 的最大 i ( < j ) − 1 ( 如果这样的 i 不存在 ) match(j)= \begin{cases} 满足p_0...p_i=p_{i-j}...p_j的最大i(
11.3.4 分离链接法(Separate Chaining)
11.4 散列表的性能分析
第十二讲 串的模式匹配(KMP算法)
12.1 什么是串
12.2 KMP(Knuth、Morris、Pratt)算法
12.3 KMP的算法实现
12.3.1 BuildMatch的实现