数据结构与算法基础
算是先复习一遍之前学过的数据结构与算法课吧,采用的是看课加书结合的方法。
课程选择的是青岛大学-王卓老师的数据结构课,王老师讲解的非常清晰,而且课程规划和节奏也非常好,观看体验极佳。
附课程链接:数据结构与算法基础(青岛大学-王卓)
下面是课程笔记,记得比较随性,等秋招全部结束我再回来慢慢梳理,该打补丁打补丁。
1. 算法和算法分析
- 事前估计和事后估计
- 假设每条语句所需的时间均为单位时间
- 算法的渐进时间复杂度
- 顺序查找 判断最好情况与最坏情况,分析平均的时间复杂度
- 1< logn < n < nlogn < n^2< n^3 < 2^n < n!
- 渐进的空间复杂度
- 算法本身占据的空间
- 实现的时候需要的辅助空间
2. 线性表的定义
- 同一个线性表中的元素具有相同特性,元素间为线性关系
- 线性表P = ((p1,e1),(p2,e2))
- 顺序存储结构:存储空间的分配不灵活,而且空间复杂度比较高
- 链式结构相对灵活
- 线性表中的数据元素的类型可以为简单类型也可以为复杂类型
- 顺序存储定义:把逻辑上相邻的数据元素存储在物理上相邻的存储单元中的存储结构
- 顺序表与实际的一维数组的序号之间差1,第i个表值实际上是数组的第i-1个数
- 顺序表的插入和删除算法相对来说会比较难:需要判断插入位置是否合法,需要判断当前的表是否已满
- 顺序表属于静态的存储形式,数据元素的个数是不能自由扩充的
- 链表,用物理位置任意的存储单元来存储元素,逻辑顺序与物理顺序不一定相同了
- 链表单结点包含数据域与指针域,头指针指向第一个元素的地址;单链表,双链表和循环链表
- 头结点,额外附加的节点,在首元节点之前加入的,这时,头指针指向头节点
- 头节点不计入链表的长度
- 顺序表是随机存取,而链表是顺序存取
3. 栈和队列
- 栈和队列也都是线性表
- 栈先进后出 递归
- 队列先进先出 排队问题
- 栈
- 只能在表的一端,通常是表尾,进行插入和删除操作的线性表
- LIFO 后进先出 stack Top Base
- 入栈 PUSH(x) 出栈 POP(y)
- 队列
- FIFO 只能在表尾进行插入,在表头进行删除 先进先出
- queue Q(a1, a2, … an)
- 案例
- 进制转换 十进制159转换为八进制数
- 使用栈,先进后出
- 括号匹配的检验
- 圆括号和方括号自己要匹配,不能交叉只能嵌套
- 左括号就进栈,右括号就进行匹配,匹配则pop
- 匹配不了就不匹配
- 表达式求值
- 算符优先算法
- 操作数 运算符 界限符
- 舞伴问题
- 依次从男队和女队队头各出一人配成舞伴,先进先出
- 进制转换 十进制159转换为八进制数
- 栈的表示和操作的实现
- 为了方便操作,top通常指向真正栈顶元素之上的下标地址
- 顺序栈需要有stacksize来表示栈可以使用的最大容量
- 空栈 base==top
- top - base = stacksize 则栈满
- 栈满时的处理方法:报错或分配更大的空间
- 上溢和下溢
- 顺序栈的操作,实际上就是对数组的初始化和指针的移动
- 链栈是运算受限的单链表,只能在链表头部进行操作 LinkStack
- 链栈中的指针方向,an为栈顶元素,a1为栈底元素
- 存储结构实际上是相当灵活的,如何操作都是根据实际问题进行调整的
- 链栈不存在满栈的情况,空栈相当于头指针指向空
- 插入和删除操作都在栈顶进行
- 栈与递归
- 包含自己 或者反复地调用自己
- 必备的三个条件:
- 能够将原来的问题转换为一个新的问题,新问题是与原问题解法相同或者类似
- 可以通过上述转换使问题简化
- 必须有一个明确的递归出口或者递归的边界
- 递归工作栈 包含实参 形参 和返回地址
- 递归->非递归 降低代码的时间开销
- 将尾递归和单向递归转换为循环结构
- 自用栈来模拟系统的运行时栈
- 队列的表示和操作的实现
- 顺序队列-真溢出和假溢出 每移动一次,队中元素都要移动
- 为了解决这种问题,可以将队的空间设想为一个循环的表,将空间反复地利用 Q.rear = (Q.rear + 1) % MAXQSIZE
- 队空队满都是 front == rear
- 另外设置一个标志,记录队空队满
- 另设一个变量,记录元素个数
- 少用一个元素空间*** (Q.rear + 1) % MAXQSIZE == Q.front
- 初始化分配空间的时候需要判断空间是否分配成功
- 队列的链式表示和实现
- 链队是包含头结点的 Q.front指向头结点 Q.rear指向尾结点
4. 串、数组和广义表
- 串的定义 String
- 子串,任意个连续字符组成的子序列
- 子串位置,子串中第一个字符在主串中的位置 从1开始
- 串相等: 长度相等且个各对应位置上的字符都相同
- 串的逻辑结构还是线性结构,存储结构还是顺序存储结构和链式存储结构
- 串的链式存储结构的存储密度较低,可以通过将多个字符存放在一个结点中,提高存储密度
- 串的模式匹配算法
- BF算法(暴力破解) KMP算法(速度快)
- BF简单匹配法,穷举法
- KMP算法无需回溯,可以利用已匹配的串进行加速
- next[j],前缀和后缀模式串子串进行比较
- nextval【j】,相同则继续回溯,不会相同则为next[j]
- 数组
- 结构是固定的,定义以后维度不再改变
- 一般不做插入和删除,只做取元素和修改元素的值
- 一般用顺序存储结构来存储
- 内存空间的地址是一维的
- 二维数组有两种存储方式,以行序为主序或者以列序为主序
- 特殊矩阵的压缩存储
- 广义表
- 又称列表Lists,表中的每一个ai或者是原子,或者是一个广义表,递归定义
- 表头 广义表中的第一个元素
- 表尾 除了表头以外其他元素组成的表
- 长度 广义表最外层包含的元素个数
- 深度 广义表展开后所含括号的重数
5. 树和二叉树
- 树的定义
- 递归定义,由根和子树组成
- 嵌套集合/凹入表示/广义表
- 树的基本术语
- 根结点,非空树中无前驱结点的结点
- 结点的度,结点拥有的子树的个数
- 树的度,树内各结点度的最大值
- 叶子/终端结点 度= 0
- 双亲和孩子
- 兄弟结点 与 堂兄弟(位于同一层的结点)
- 结点的祖先和子孙
- 树的深度,树中结点的最大层次
- 有序树和无序树
- 森林,m棵互不相交的树构成的集合
- 二叉树的定义
- 每个结点最多只有两个分支
- 根节点和两棵互不相交的左子树和右子树组成
- 二叉树的结点的子树哪怕只有一个孩子,也需要区分左子树还是右子树
- 二叉树的性质
- 在二叉树的第i层上至多有2^(i-1)个结点 i>=1
- 深度为k的二叉树至多有2^k-1个结点 k>=1,最少有k个结点
- 对于任何一棵二叉树T,其叶子数为n0,度为2的结点数为n2,n0 = n2 + 1
- B = n-1 = n2 * 2 + n1 * 1 (B为总边数)
- n = 2 * n2 + n1 + 1 = n0 + n1 + n2
- n0 = n2 + 1
- 所有结点都在,达到最大的二叉树——满二叉树
- 深度为k,具有n个结点的二叉树,每个结点都与深度为k的满二叉树1~n的结点一一对应时,称为完全二叉树
- 在满二叉树中,从最后一个结点开始,连续去掉任意个结点,即是一棵完全二叉树
- 具有n个结点的完全二叉树的深度为[log2n] + 1
- 具有n个结点的完全二叉树,第i个结点的双亲结点是[i/2],左孩子是结点2i,右孩子是结点2i+1
- 二叉树的存储结构
- 顺序存储结构,按照满二叉树的结点层次编号,依次存放二叉树中的数据元素
- 顺序存储缺点:数据变化较大时,修改代价较大;对于右单支树,浪费的存储空间较大
- 顺序存储仅适合存储满二叉树或完全二叉树
- 链式存储结构,两个指针域,分别指向左右孩子
- 在n个结点的二叉链表中,有n+1个空指针域
- 遍历二叉树和线索二叉树
- 遍历,需要每个结点均访问一次,但不破坏原来的数据结构
- 得到所有结点的一个线性序列
- 规定先左后右的话,分为先序遍历,中序遍历和后序遍历
- 知道先序和中序,或者由中序和后续,是可以还原出原来的二叉树的
- 根据中序是可以判断根和左子树,右子树的 根据先序判断谁是根
- 后序是左右根,这样最后一个结点一定是根
- 通过递归的方式,传递二叉链表
- 每个结点都会经过3次,根据第几次经过的时候访问,分为先中后序,但是经过的路径都是完全相同的
- 用栈的方法,非递归地进行中序遍历
- 二叉树的层次遍历
- 用队列来进行,当前结点出队时分别入队左右孩子
- 二叉树遍历算法的应用
- 根据输入作为先序序列构建二叉树
- 复制二叉树
- 计算二叉树的深度 return m>n ?(m+1):(n+1)
- 计算二叉树结点总数
- 计算叶子结点的总数
- 线索二叉树 寻找特定序列中某个结点的前驱结点和后续结点
- 如果没有左孩子,就指向前驱结点
- 如果没有右孩子,就指向后继结点
- 对二叉树的每个结点再增设两个标志域Itag rtag
- 树和森林
- 树的存储结构
- 双亲表示法:数据域和双亲域,指示本结点的双亲结点在数组中的位置 找双亲容易,找孩子难
- 孩子链表: 找孩子容易 找双亲比较难
- 孩子兄弟表示法(二叉树表示法): 一个数据域和两个指针域,分别指向第一个孩子结点和下一个兄弟结点
- 树和二叉树的转换,这样就可以通过二叉树的运算来实现对数的操作,通过二叉链表作媒介可以建立一个对应关系
- 通过存储结构来唯一对应
- 树变二叉树,兄弟相连留长子
- 二叉树变回树:左孩右右连双亲 去掉原来右孩线
- 森林转换为二叉树
- 各棵树分别转换为二叉树
- 将每棵树的根结点用线相连
- 以第一棵树的根结点为二叉树的根,再以根结点为轴心,顺时针旋转构成二叉树型结构
- 二叉树转换为森林
- 去掉根结点沿右分支搜索到的所有右孩子间的连线,变成孤立的二叉树
- 将孤立二叉树还原为树
- 树和森林的遍历
- 树的遍历
- 先根遍历
- 后根遍历
- 层次遍历
- 森林的遍历
- 将森林看为三部分:第一棵树的根结点,第一棵树的子树森林,剩余树构成的森林
- 先序遍历、中序遍历
- 树的遍历
- 树的存储结构
- 哈夫曼树及其应用——最优二叉树,判别效率最高
- 基本概念
- 路径,路径长度,两结点间路径上的分支数
- 树的路径长度,从根到每一个结点的路径长度之和
- 完全二叉树是路径长度最短的二叉树
- 权,将树中结点赋给一个有着某种含义的数值
- 结点的带权路径长度:从根结点到该结点的路径长度乘该结点的权
- 树的带权路径长度,树中所有叶子结点的带权路径长度之和
- 哈夫曼树,带权路径长度WPL最短的树
- 度相同的树比较才有意义
- 权值越大的叶子离根越近
- 哈夫曼树的构造算法
- 给定n个权值,构造n棵二叉树
- 以权值最小的树作为左右子树 新的二叉树根结点的权值为原根结点权值之和
- 哈夫曼树一定有2n-1个结点
- 哈夫曼树的结点度数为0或者2,没有度为1的结点
- 通过顺序存储结构来构造哈夫曼树,weight parent lch rch
- 生成的哈夫曼树可以不唯一
- 哈夫曼编码
- 尽可能用不等长的编码,出现次数较多的字符采用尽可能短的编码,出现次数少的采用长的编码
- 长度不等的编码,任意一个字符的编码都不是另外一个字符编码的前缀,不然就会有重码
- 前缀码使得电文总长最短
- 以每个字符在电文中出现的平均概率作为权值
- 从根到每个叶子路径上的标号连接起来,作为该叶子代表的字符的编码,因为都是叶子结点,所以不会有重码
- 左0右1
- 哈夫曼编码是最优前缀码
- 文件的编码和解码
- 编码——先统计字母的频次
- 构造哈夫曼树
- 进行哈夫曼编码
- 查HC[i],得到各字符的哈夫曼编码
- 解码——构造哈夫曼树(接收字符频度表W)
- 依次读入二进制码
- 到达叶子结点时,即可翻译出字符
- 基本概念
6. 图
-
图的定义和术语
- G = (V, E)
- V为顶点,有穷非空
- E为边,有穷集合
- 无向图和有向图
- 完全图:任意两个点都有边相连
- 稀疏图、稠密图、网(边带权重)、邻接、关联、顶点的度(TD(v)),入度和出度
- 路径和路径长度、回路、简单路径(除起点和终点可以相同,其他顶点均不相同)
- 连通图:任意两个顶点之间都存在路径;强连通图和非强连通图
- 权和网
- 子图
- 连通分量,无向图G的极大连通子图
- 极小连通子图,为G的连通子图,再删除一条边就不再是连通的了
- 生成树:包含所有顶点的极小连通子图
- 生成森林:对非连通图,由各个连通分量的生成树的集合
-
案例分析
- 六度空间理论/小世界理论
-
图的类型定义
- 深度优先遍历和广度优先遍历
-
图的存储结构
- 数组表示法(邻接矩阵)
- 链式存储结构 邻接表、邻接多重表、十字链表
- 重点邻接矩阵和邻接表
- 邻接矩阵
- n*n的方阵
- 对角线上的元素都为0
- 无向图的邻接矩阵是对称矩阵
- 顶点i的度,第i行中1的个数
- 有向图,可能是非对称
- 有向图邻接矩阵,第i行出度边;第i列,入度边
- 网的邻接矩阵,对应的元素值为权值,无边为∞
- 邻接矩阵的建立
- 两个数组分别存储顶点表和邻接矩阵
-
常见的应用:
- 最小生成树:Prim(依次添加结点)K(依次添加边)
- 最短路径:Dijistra(依次添加) Floyd(n阶方阵,计算每添加一个结点对其他路径的影响)
- 拓扑排序(深度/广度优先)对有向无环图
- 先找入度为0的点
- 找到以后断开连接,重复1、2直至所有点visited
- 关键路径
- AOE网
- 活动最早开始的时间,和最迟开始的时间
7. 查找
- 查找的基本概念,同一类型数据元素构成的集合
- 根据给定的值,在查找表中找到关键字等于给定值的数据元素
- 主关键字、次关键字
- 目的:查询、检索各种属性、插入新的数据元素、删除
- 评价查找算法:ASL(平均查找长度)
- 提高查找的效率:人为加上某些确定的约束关系
- 线性表的查找:
- 顺序查找(线性查找)
- 每执行一次循环都要进行两次比较
- 改进,将待查关键字key存入表头作为哨兵
- time:ASL = (n+1)/2, mem: O(1)
- 提高效率,将查找概率比较高的排在前面
- 按照查找概率动态地调整记录顺序
- 二分查找
- mid = (low+high)/2
- low = mid - 1
- high = mid + 1
- while(low<=high)
- ASL log2(n+1) - 1
- 仅适用于有序表
- 分块查找
- 需要先建立索引顺序表
- 其实相当于是二分+顺序,首先在索引顺序表上进行二分找到块,然后在用顺序查找在块内查找
- ASL = Lb + Lw ~ log2(n/s + 1) +s/2
- 顺序查找(线性查找)
- 树表的查找:
- 动态查找表,在查找过程中动态生成
- 二叉排序树和平衡二叉树
- 二叉排序树/搜索树/查找树
- 左子树所有结点的值小于根结点的值
- 右子树所有结点的值大于等于根结点的值
- 左子树和右子树又分别为二叉排序树
- 中序遍历序列一定是递增有序的
- 由于其特殊性质,非常适合用递归算法来进行查找
- 最好情况是O(log2n),最差是O(n)
- 如何提高不平衡的二叉排序树的查找效率——>平衡二叉树
- 二叉排序树的插入和删除
- 如果删除的是叶子结点,就直接删除;如果只有左子树或右子树,直接替换;如果既有左子树又有右子树,那么直接用他的前驱结点来代替他,就是左子树的最大结点来代替他
- 平衡二叉树
- AVL树,首先必须是二叉排序树
- 左子树和右子树的高度之差的绝对值小于等于1
- 左子树和右子树也是平衡二叉树
- BF平衡因子,左子树的高度-右子树的高度
- 如果插入新的结点以后造成失衡,就需要调整二叉树
- 以最小的失衡子树根结点作为失衡结点 LL LR RL RR
- 调整原则: 降低高度 保持二叉排序树的性质
- 散列表的查找
- hash函数,Loc(i) = H(key i)
- 冲突:key1 ≠ key2 但是H(key1) = H(key2)
- 同义词:具有相同函数值的多个关键字
- 构造好的散列函数:尽可能简单+分布均匀分布,避免空间浪费
- 制定一个好的解决冲突的方案
- 多种方法,直接定址法、除留余数法
- 直接定址法
- Hash(key) = a*key +b
- 空间效率较低,但是不会产生冲突
- 除留余数法
- Hash(key) = key mod p
- 选取合适的p ,p<=m且为质数
- di增量序列,有冲突就寻找下一个空的散列地址,只要散列表足够大,空的散列地址总能找到
- 线性探测/二次探测(p为4k+3的质数)/伪随机探测法
- 开放地址法
- 链地址法(拉链法),用一个数组将m个单链表的表头指针存储起来,形成一个动态的结构
- 优点,非同义词不会冲突,无聚集现象
- 适用于表长不定的情况
- 直接定址法
- 散列表的查找
- ASL取决于三个值:散列函数、处理冲突的方法、散列表的装填因子α(表中填入的记录数/哈希表的长度)
8. 第八章排序
- 排序的概念
- 将一组杂乱无章的数据按一定的顺序顺次排列起来
- 排序的应用非常广泛
- 插入排序
- 类似抽扑克牌,将待排序的对象按照关键码的大小,插入到已经排好序的序列中
- 顺序法定位插入位置——直接插入排序;二分法定位插入位置——二分插入排序; 缩小增量多遍插入排序——希尔排序
- 直接插入排序,从后往前进行顺序查找,直到找到合适的位置进行插入 time:O(n)~O(n^2) mem: O(1) 稳定
- 折半插入排序,采用折半查找法 time:O(n^2) mem: O(1) 稳定
- 希尔排序
- 定义增量序列
- 递减
- 互质
- 最后一个为1
- 间隔插入排序
- time: O(n1.25)~O(1.6*n1.25)
- mem: O(1)
- 不稳定
- 不适合在链式存储结构上来实现
- 交换排序
- 冒泡排序
- 两两比较,前小后大规则交换
- 一轮结束比后,序列中最大的数移动到了序列最后
- n个记录 n-1轮比较, 第m轮,比较n-m次;
- 如果某一趟没有进行交换,就不需要后续了
- time: O(n^2) mem O(1) 稳定
- 快速排序
- 递归思想
- 任取一个元素为中心
- 比中心小的在左,比中心大的在右,形成左右子表
- 分别对左右子表重复2和3
- 直到每个子表只有1个元素
- time: O(nlogn) mem: O(logn)
- 因为用了递归的思想,所以需要栈来存储函数调用的参数和返回值等,最坏情况是O(n),平均是O(logn)
- 快速排序是一种不稳定的排序方法
- 快速排序不适用于对原本有序或者基本有序的记录序列进行排序
- 冒泡排序
- 选择排序
- 简单选择排序
- 依次找最小的树和前面进行交换
- time:O(n^2),mem:O(1),不稳定的排序
- 堆排序
- ai<= a2i; ai <= a2i+1为小根堆,反之为大根堆;堆为完全二叉树,且任一非叶子结点值均小于(大于)它的孩子结点
- 依次输出堆顶值,再将剩余元素调整成新的堆
- 以小根堆为例:
- 输出堆顶,以最后一个元素代替之
- 根与左右比较,下调
- 直至叶子结点,得到新的堆——“筛选”
- 无序序列如何变成堆?
- 建立初始完全二叉树
- 从最后一个非叶子结点来调整,将以钙元素为根的二叉树调整为堆
- time: O(nlogn) mem: O(1) 不稳定
- 简单选择排序
- 归并排序
- 2-路归并排序,重复将2个有序序列归并为一个
- 使用双指针,合并相邻序列的时候需要新开空间
- time: O(nlog2n) mem: O(n) 稳定
- 基数排序
- 不需要比较,又称桶排序或箱排序
- 采用先分配再收集的方式,如数字序列,按个十百…依次往e[0]~e[9]分配,再收集回来,结束以后就是有序的了
- time: O(k*(n+m)),k:关键字,m:桶的个数
- mem: O(m+n)
- 稳定
完结撒花~✿✿✿