数据结构复习笔记
目录
Chapter 1_introduction
1.1-ADT和OO
- ADT:Abstract data type
- OO:object-oriented
1.2-递归算法
- 计算斐波那契数列
- 全排列
- n个元素的全排列 =(n-1个元素的全排列)+(另一个元素作为前缀)
- 汉诺塔问题
Chapter 2_Algorithm Analysis
2.1-算法复杂度
2.1.1-空间复杂度
- 一个算法的空间复杂度只考虑在运行过程中为局部变量分配的存储空间的大小,它包括为参数表中形参变量分配的存储空间和为在函数体中定义的局部变量分配的存储空间两个部分。
- 若一个算法为递归算法,其空间复杂度为递归所使用的堆栈空间的大小
- 形参
- 若形参为数组,则只需要为它分配一个存储由实参传送来的一个地址指针的空间,即一个机器字长空间;
- 若形参为引用方式,则也只需要为其分配存储一个地址的空间,用它来存储对应实参变量的地址,以便由系统自动引用实参变量。
2.1.2-时间复杂度
-
计算时间复杂度
- 先找出算法的基本操作,然后根据相应的各语句确定它的执行次数
- 最坏情况、最好情况、平均情况
- 先找出算法的基本操作,然后根据相应的各语句确定它的执行次数
-
各种时间复杂度表达方式
-
O O O:渐进上界(最坏情况),小于等于的意思。
-
Ω \Omega Ω:渐进下界(最好情况),大于等于的意思。
-
Θ \Theta Θ:用于当函数f可以由同一函数g限定上下限时,等于的意思。
- 如果 O O O和 Ω \Omega Ω的表达式一样,就是 Θ \Theta Θ
-
o o o:为函数f提供一个下界,小于的意思。
-
2.2-算法分析实例
- 选择排序
- 冒泡排序
- 顺序搜索
- 查找最大值
- 插入排序算法一:用尽一切可能
- 二分查找
- 秩排序(Rank Sort)
- 也称计数排序
- 步骤:
- 1)找到最大值m和最小值n,申请m-n+1额外空间
- 2)遍历待排序集合,将每一个元素出现的次数记录到元素值对应的额外空间内
- 3)按正确顺序输出
- 时间复杂度:O(n+k)
- 空间复杂度:O(k)
- 稳定性:稳定
- 特别说明:
- 虽然计数排序看上去很强大,但是它存在两大局限性:
- 1.当数列最大最小值差距过大时,并不适用于计数排序
- 2.当数列元素不是整数时,并不适用于计数排序
- 最大子序列和(重点)
- 算法一:遍历一切可能
- O(N3)
- 算法二:双重循环法
- O(N2)
- 算法三:分治法
- 递归实现
- O(Nlog N)
- 最佳算法:只遍历一遍
- 原理:
- 1)任何负的 子序列都不可能是最大子序列和 的前缀
- 2)当加上下标 j 所在的元素后当前序列的和变成负数时,根据1)可以从 j+1 处重新开始计算下一段子序列的和。
- O(N)
- 原理:
- 算法一:遍历一切可能
Chapter 3_List
3.1-数组
3.2-单链表(重点)
-
单链表是一个线性数据结构,数据是以结点来表示
-
单链表的结点:
-
data:当前结点的数据
-
next:指针,指向后继结点
-
head:头指针,初始结点无前驱,故应设头指针head指向开始结点
-
终端结点:最后一个结点,无后继,故终端结点的指针域为空,即null
注意:
- 单链表的头指针是不是第一个结点
- 链表由头指针唯一确定,单链表可以用头指针的名字来命名
-
-
单链表类定义
-
ListNode:代表结点的类
-
LinkedList:代表链表本身的类
-
LinkedListItr:结点迭代器,代表当前位置的类
-
单链表操作
-
find:查找,O(N)
-
remove:删除,O(1)
p.current.next = p.current.next.next;
-
findPrevious:找到结点的前一个结点,O(N)
-
insert:插入,O(N)
p.current.next = new ListNode(x, p.current.next);
-
zeroth:返回头指针的LinkedListItr
-
first:返回第一个结点,即header.next
-
3.3-双链表(大概率不考)
- 类似单链表,但每个结点有两个指针,一个指向后续结点(right),一个指向前驱结点(left)
- 双链表的操作
- 插入
- 删除
- 双链表易实现双链表循环链表
- 无headerNode
- 将最后一个结点的right指针指向第一个结点
- 有headerNode
- 将最后一个结点的right指针指向headerNode
- 无headerNode
3.4-循环链表
- 循环链表的实现
- 无头指针
- 将最后一个结点的next指针指向第一个结点
- 有头指针
- 将最后一个结点的next指针指向header
- 无头指针
- 循环链表的应用
- 解决约瑟夫(Josephus)问题
Chapter 3.1_Stack and Queue
3.1.1-栈(重点)
- 堆栈是一个线性数据结构,其插入和删除发生在同一端,这一端叫做栈顶,另一端称为栈底。
- 栈的特性:先进后出,后进先出
- 栈的实现:
- 链表实现
- Array实现
- 栈操作
- pop:出栈(退栈)
- push:进栈(压栈)
- top:返回栈顶元素
- 当多个堆栈共存时会浪费空间
- 当只有两个堆栈时,我们可以通过将一个堆栈的底部固定在位置0,将另一个堆栈的底部固定在位置MaxSize-1来节省空间和时间效率。
- 两个堆栈向阵列的中间增长。
3.1.2-队列(重点)
- 队列
- 队列是一个线性数据结构,只允许在队头(front)进行删除操作,而在队尾(rear)进行插入操作
- 队列的特性:先进先出,后进后出
- 队列的实现:
- 链表实现
- Array实现
- 队列操作(重点)
- add:入队,在队尾添加元素
- delete:出队,删除队首元素
-
- front=front+1; O(1)
-
- 队列左移一位 O(n)
-
- 循环队列
- 需要记录 front 和 back
- 用数组实现循环队列:
- 方案一:当 front 或 back 到达Array.length-1时,将其设置为 0
- 方案二:
back = (back+1) % theArray.length
front = (front + 1) % theArray.length
- 用链表实现循环队列:
- 将队尾元素的后继指针设为队首元素
Chapter 4_Tree
4.1-树
- 非线性的数据结构
- 度:
- 结点的度:子结点的数量
- 树的度数:所有的最大度数
- 结点:
- 叶结点:度数是0的元素
- 根结点:是树上最顶部的结点
- 树高:是指到到达叶结点最长路径的长度(树的层数减一)
- 深度:是指一个结点到达根结点路径的长度
4.2-二叉树(重点)
4.2.1-二叉树的性质
- 包含n个元素的二叉树有n-1条边
- 第i层至多2i个节点(根为第0层)
- 高度为h的二叉树,至少h+1个元素,至多2h+1-1个元素
- 如果叶的个数为n0,度数为2的节点个数为n2,则 n0=n2+1
- 包含n个元素的二叉树,最小高度为log2(n+1)向上取整再减1,最大高度为n-1
4.2.2-特殊的二叉树(重点)
完全二叉树
- 堆就是完全二叉树
满二叉树
- 特殊的完全二叉树(每一层都达到最大结点数)
- 结点数为 2h+1-1
4.2.3-二叉树的表示
-
二叉树的数组表示
- 最适合完全二叉树
- 如果无该结点,则设为null,而非0
-
二叉树的链表表示
-
二叉链表
leftChild | element | rightChild
-
三叉链表
leftChild | element | parent | rightChild
-
4.2.4-二叉树的遍历
(三种+按层次)|(非递归算法要求看懂)
深度优先搜索
-
先序遍历
-
根-左-右
-
递归实现
-
非递归实现
-
利用栈实现
-
遍历思路:
1)访问根节点,根节点入栈并进入其左子树,进而访问左子树的根节点并入栈,再进入下一层左子树,……,如此重复,直至当前节点为空。
2)如栈非空,则从栈顶退出上一层的节点,并进入该节点的右子树。while(p != null || !stack.isEmpty()){ //遍历这个节点的子节点,直到到达这个节点的叶子节点 while(p != null){ visitDate(p);//输出当前节点 stack.push(p);//把当前节点保存到堆栈中,以便遍历完左子树后遍历右子树 p = p.left; } //最后一个叶子节点输出后,在栈中取该节点的根节点,遍历这个根节点的右孩子。 if (!stack.isEmpty()) { Btree<T> pop = stack.pop(); p = pop.right; } }
-
-
-
中序遍历
-
左-根-右
-
递归实现
-
非递归实现
-
利用栈实现
-
遍历思路:
1)根节点入栈并进入其左子树,进而左子树的根节点入栈,再进入下一层左子树,……,如此重复,直至当前节点为空。
2)如栈非空,则从栈顶退出上一层的节点,访问出栈节点,并进入该节点的右子树。while(p != null || !stack.isEmpty()){ if (p != null) { stack.push(p);//当前节点入栈 p = p.left;//遍历他的左节点 }else{ p = stack.pop();//当前节点为空,则取出栈顶元素, visitDate(p);//打印栈顶元素 p = p.right;//遍历该节点的右节点 } }
-
-
-
后序遍历
-
左-右-根
-
递归实现
-
非递归实现
-
利用栈实现
-
遍历思路:
1)根节点入栈并进入其左子树,进而左子树的根节点入栈,再进入下一层左子树,……,如此重复,直至当前节点为空。
2)若栈非空,如果栈顶节点p的右子树为空,或者p的右孩子是刚访问的节点q,则退栈、访问p节点,并将p置为空,如果栈顶节点p有右子树且右子树未访问,则进入p的右子树。while(p != null || !stack.isEmpty()){ //由根节点向下遍历,知道找到该根节点下的最后一个叶子节点 while (p != null) { stack.push(p);//非叶子节点入栈 p = p.left;//指向该节点的左孩子 } //p为空,栈非空,说明遍历完了左孩子,处于叶子节点状态 if (!stack.isEmpty()) { p = stack.pop();//栈顶出栈 //因为如果该节点有右节点,肯定是先访问完右节点才开始访问跟节点的 //p.right == null:表示没有右节点,可以直接访问根节点 //p.right == q:刚访问完该节点右节点,则可以访问我该节点 if (p.right == null || p.right == q) { visitDate(p);//访问当前节点 q = p;//记录这个节点 p = null; }else{//开始遍历右孩子 p = p.right; } } }
-
-
广度优先搜索
-
按层次遍历
-
用队列实现:
-
将根放入队列
-
while(队列非空){
从队列取出一个元素并访问;
如果该元素有右子树就将它放入队列;
如果该元素有左子树就将它放入队列;
}
-
4.2.5-二叉树的构造
- 能唯一确定的:
- 先序遍历和中序遍历
- 中序遍历和后序遍历
- 不能唯一确定的:
- 先序遍历和后序遍历
4.2.6-字符串
- Java与C/C++的不同处:
- Java语言的字符串不是字符数组,所以不能以字符数组方式进行 一些操作。 如, str[1] = ‘a’ 是错误的,而只能通过方法(函数) 来进行操作。
4.2.7-树和森林的表示
- 双亲表示法
- 左子女右兄弟表示法
- 将普通的树转化为二叉树
- (重点)(联系并查集)
- 森林转化为二叉树
- 每棵树转为二叉树
- 把每棵二叉树根用右链相连
4.2.8-树和森林的遍历
- 普通树的深度优先遍历:
- a) 先序次序遍历(先根)——与转换成的二叉树的先序遍历相同
- b) 后序次序遍历(后根)——与转换成的二叉树的中序遍历相同
- 普通树的广度优先遍历:
- 按层次遍历
- 森林的遍历:
- 转化为二叉树后:
- 先根次序遍历——二叉树的先序遍历
- 中根次序遍历——二叉树的中序遍历
- 后根次序遍历——二叉树的后序遍历
- 转化为二叉树后:
4.2.8-线索化树(Thread Tree)
机器存储
- 一个结点增加两个标记域:
- leftchild | leftthread | data | rightthread | rightchild
- leftThread:
- 0:leftchild 指向左子女
- 1:leftchild 指向前驱
- rightThread:
- 0:rightThread 指向右子女
- 1:rightThread 指向后继
线索化的方法
- 对二叉树进行线索化的算法比较简单,只要设置一个pre指针。若要进行前序线索化,就对此二叉树进行前序遍历,pre指向当前访问结点的直接前驱,然后将结点的空指针域按照线索树的定义相连,遍历结束时,这棵二叉树也就前序线索化成功了。
- 同理,可以对二叉树进行中序和后序线索化。
- 利用线索二叉树进行中序遍历时,不必采用栈,速度较一般二叉树的遍历速度快,且节约存储空间。
在没有栈的支持下的对线索树的遍历的算法是经常出现的考题,考虑到中序线索树的特点,对中序线索树不仅可以进行中序的遍历,还可以进行前序和后序的遍历。值得提出的是,后序的线索树若没有指向双亲的指针或者不用栈,则无法对其进行遍历
4.2.9-霍夫曼树
增长树
- 对原二叉树中度为1的结点, 增加一个空树叶
- 对原二叉树中的树叶, 增加两个空树叶
- 相关概念
- 外通路长度(外路径)E: 根到每个外结点(增长树的叶子) 的路径长度的总和(边数)
- 内通路长度(内路径)I: 根到每个内结点(非叶子)的路径长度的总和(边数)
- 结点的带权路径长度: 一个结点的权值与结点的路径长度的乘积
- 带权的外路径长度: 各叶结点的带权路径长度之和。
- 带权的内路径长度:各非叶结点的带权路径长度之和。
霍夫曼树
- 带权外路径值最小的增长树
- Huffman 算法
- 思想: 权大的外结点靠近根, 权小的远离根。
- 算法:
- 第一步:从m个权值中找出两个最小值W1,W2作为叶结点构成一个二叉树,根W=W1+W2
- 第二步:然后对m-1个权值W, W3,W4,…,Wm由小到大排序,重复第一步
霍夫曼编码
- 是霍夫曼树在数据编码中一种应用。 具体用于通信的二进制编码中。
- 先构造霍夫曼树,然后将每个节点左子女的边标上0,右子女标上1。 这样从根到每个叶子的路径上的号码连接起来, 就是外结点对应的字符的二进制编码
- 例子:
- 设一电文出现的字符为D={M,S,T,A,Q, K} , 每个字符出现的频率为W={10,29,4,8,15,7}, 如 何对上面的诸字符进行二进制编码, 使得
1)该电文的总长度最短。
2)为了译码,任一字符的编码不应是另一字符的编码的前缀
- 设一电文出现的字符为D={M,S,T,A,Q, K} , 每个字符出现的频率为W={10,29,4,8,15,7}, 如 何对上面的诸字符进行二进制编码, 使得
- 算法: 利用Huffman算法, 把{10,29,4,8,15,7}作为外部结点的权, 构造具有最小带权外路径长度的扩充二叉树,把每个结点的左子女的边标上0, 右子女标上1。 这样从根到每个叶子的路径上的号码连接起来, 就是外结点的字符编码。
Chapter 4.1_SpecificTrees
4.1.1-Binary Search Trees
- 二叉搜索树
- 它或者是一棵空树
- 或者是具有下列性质的二叉树:
- 每个元素都有一个key值,并且没有两个元素具有相同的key值;因此,所有key值都是不同的
- 若它的左子树不空,则左子树上所有结点的值均小于它的根结点的值;
- 若它的右子树不空,则右子树上所有结点的值均大于它的根结点的值;
- 它的左、右子树也分别为二叉搜索树
- 索引二叉搜索树
- 通过在每个树节点上添加leftSize字段从普通二叉搜索树派生出来
- leftSize字段中的值 = 节点左子树中的元素数 + 1
- leftSize | left | element | right
- 从二叉搜索树中删除一个节点
- 删除叶节点:
- 直接删除
- 节点只有一个儿子:
- 将该节点的的父节点中原本指向该节点的链指向该节点的儿子
- 节点有两个儿子:
- 一般的删除策略是,用其右子树的最小节点代替该节点的数据并递归地删除那个节点
- 删除叶节点:
4.1.2-AVL Tree(重点)
- 自平衡的二叉搜索树
- 为了提高二叉搜索树的搜索效率,减少平均搜索长度,引入了AVL树
- AVL树的定义:
- 是二叉搜索树
- 任何节点的两个子树的高度差最大为1
- 树高:从根到每个叶节点的最长路径
- 每个节点:leftSize | left | element | right | balance(height)
- 具有n个元素的AVL树的高度为log2n,因此n个元素的AVL搜索树可以在O(log2n)时间内被搜索
- 插入节点
- 首先要正确地插入
- 找到有可能发生的最小不平衡子树
- 判别插入在不平衡子树的外侧还是内侧
- 根据3的判别结果,再进行单旋还是双旋
- 删除节点
- 与二叉搜索树的删除方法基本一样,用其右子树的最小节点代替该节点的数据,但删除X后,以X为根的子树高度减1,可能影响到从X到根结点上每个结点的平衡,因此要进行一系列调整
- 注意:
- AVL树的最后一层以上并不一定是完全二叉树
- 检测一个二叉树是不是一个二叉搜索树,可以看其中序遍历的结果是不是递增
4.1.3-B-Tree(重点)
-
m路搜索树(m-way Search Trees)
- 它或者是一棵空树,或者是具有下列性质的树:
- 在相应的扩展搜索树(通过用外部节点替换零指针获得)中,每个内部节点最多有m个子节点,并且在1到m-1元素之间
- 每个含有p个元素的节点都有p+1个子节点。
- 考虑具有p个元素的任何节点:
- C0 k1 C1k2 … kp Cp
- ki为节点内元素,Ci为子节点
- C0:根为C0的子树中的元素的key值小于k1
Cp:具有根Cp的子树中的元素的key值大于kp
Ci:具有根Ci的子树中的元素的key值大于ki但小于ki+1,1<=i<=p
- 它或者是一棵空树,或者是具有下列性质的树:
-
m阶B树(平衡的m路搜索树)
-
它或者是一棵空树,或者是具有下列性质的多叉搜索树:
- 根结点至少有两个子女
- 非根非叶结点至少有ceil(m/2)个子女,这里ceil代表向上取整。
- 所有的外部结点都位于同一层,可以用空指针表示,是查找失败到达的位置。
-
B树要求每个内部节点至少两个子节点,故二阶B树为完全二叉树
-
在2阶B-树中,每个内部节点至少有2个子节点,并且所有外部节点都必须在同一级别上,因此2阶B-树是完全二叉树
-
在3阶B树(有时也称为2-3树)中,每个内部节点有2或3个子节点
-
B树中插入节点:
-
总是插入在外部节点之的上一层
-
Case1:子节点数<m,按顺序插入节点
-
Case2:当前节点是满节点,插入后引起分裂,把中间大小的关键码上提到父节点,可能再次引起分裂——这种情况下可能引起树增高一层
-
-
B树中删除节点:
-
Case1:删除叶元素节点中的key值
-
如果节点有超过ceil(m/2)个孩子,可以直接删除这个节点
-
如果节点有ceil(m/2)个孩子,删除后,子节点数不满足B树
-
向最近的兄弟节点借一个节点(如果兄弟节点允许的话)
-
如果临近的左右兄弟节点都是有ceil(m/2)个孩子
- 删除后,将节点及其同级节点与父节点中它们之间的元素合并为单个节点
- 可能会导致父节点出现新的合并
- 如果树根合并,树的高度将降低1
-
-
-
Case2:删除上层节点中的key值
- 删除它
- 将其替换为右子树中的最小键或左子树中的最大键
- 因为删除了叶节点中的一个键,所以执行Case1中提到的调整
-
-
Chapter 5_Hashing
5.1-散列函数
- H(x) = x mod i, i 通常是一质数
- 哈希表大小 = i
- 碰撞频率
- 碰撞频率 = 碰撞的元素数量 / 总元素数量
- 散列函数优势
- 顺序搜索:O(n)
- 二叉搜索:O( log 2 n \log_2n log2n )
- 散列表法:O©
5.2-散列函数的碰撞问题
- 线性探测 linear Probing
- d+1, d+2, d+3…
- 二次探测 Quadratic probing
- d+1, d+22, d+32…
- 双重散列 Double Hashing
- 添加一个H2(x)得计算值为c
- d, d+c, d+2c, d+3c…
- 再散列 rehashing(重点)
- 一般当表项数 > 表的70% 时,可再散列
- 一般取比 2*i (原表长)大的最小质数再散列
- 分离链接法 Separate Chaining
- 将散列到同一个值的所有元素保留到一个链表中
Chapter 6_Priority Queue
6.1-优先级队列
- 普通的队列是一种先进先出的数据结构,元素在队列尾追加,而从队列头删除。
- 在优先队列中,元素被赋予优先级。当访问元素时,具有最高优先级的元素最先删除。优先队列具有最高级先出 (first in, largest out)的行为特征。
- 优先级队列的表示
- 采用堆数据结构来实现。
- 使用无序线性列表实现:
- 在列表的右端执行插入,θ(1)
- 删除需要搜索具有最大优先级的元素θ(n)
- 优先级队列的操作:
- 在最小优先级队列中,find操作查找具有最小优先级的元素,而delete操作删除此元素。
- 在最大优先级队列中,find操作查找具有最大优先级的元素,而delete操作删除此元素。
6.2-堆
- 堆通常是一个可以被看做一棵完全二叉树的数组对象。
- 堆的性质:
- 堆中某个节点的值总是不大于或不小于其父节点的值;
- 堆总是一棵完全二叉树。
- 堆属性非常有用,因为堆常常被当做优先队列使用,因为可以快速地访问到“最重要”的元素。
- **内存占用:**普通树占用的内存空间比它们存储的数据要多。你必须为节点对象以及左/右子节点指针分配内存。堆仅仅使用一个数据来存储数组,且不使用指针。
**注意:**堆的根节点中存放的是最大或者最小元素,但是其他节点的排序顺序是未知的。例如,在一个最大堆中,最大的那一个元素总是位于 index 0 的位置,但是最小的元素则未必是最后一个元素。唯一能够保证的是最小的元素是一个叶节点,但是不确定是哪一个。
parent(i) = floor((i - 1)/2) // floor函数,其功能是“向下取整”
left(i) = 2i + 1
right(i) = 2i + 2 = left(i) + 1
- 更多性质
- 一个高度为 h 的堆有 h+1 层
- 如果一个堆有 n 个节点,那么它的高度是 h = f l o o r ( log 2 ( n ) ) h = floor(\log_2(n)) h=floor(log2(n))
- 如果最下面的一层已经填满,那么那一层包含 2^h 个节点。树中这一层以上所有的节点数目为 2^h - 1
6.2.1-堆操作
- 有两个原始操作用于保证插入或删除节点以后堆是一个有效的最大堆或者最小堆:
- 上滤:
shiftUp()
,如果一个节点比它的父节点大(最大堆)或者小(最小堆),那么需要将它同父节点交换位置。这样是这个节点在数组的位置上升。 - 下滤:
shiftDown()
,如果一个节点比它的子节点小(最大堆)或者大(最小堆),那么需要将它向下移动。这个操作也称作“堆化(heapify)”。 - shiftUp 或者 shiftDown 是一个递归的过程,所以它的时间复杂度是 O(log n)。
- 上滤:
- 基于这两个原始操作还有一些其他的操作:
- 插入:
insert(value)
,在堆的尾部添加一个新的元素,然后使用shiftUp
来修复对。 - 删除:
remove()
,移除并返回最大值(最大堆)或者最小值(最小堆)。为了将这个节点删除后的空位填补上,需要将最后一个元素移到根节点的位置,然后使用shiftDown
方法来修复堆。removeAtIndex(index)
: 和remove()
一样,差别在于可以移除堆中任意节点,而不仅仅是根节点。当它与子节点比较位置不时无序时使用shiftDown()
,如果与父节点比较发现无序则使用shiftUp()
。 - 替换:
replace(index, value)
:将一个更小的值(最小堆)或者更大的值(最大堆)赋值给一个节点。由于这个操作破坏了堆属性,所以需要使用shiftUp()
来修复堆属性。
- 插入:
- 上面所有的操作的时间复杂度都是 O(log n),因为 shiftUp 和 shiftDown 都很费时。还有少数一些操作需要更多的时间:
search(value)
:堆不是为快速搜索而建立的,但是replace()
和removeAtIndex()
操作需要找到节点在数组中的index,所以你需要先找到这个index。时间复杂度:O(n)。buildHeap(array)
:通过反复调用insert()
方法将一个(无序)数组转换成一个堆。如果你足够聪明,你可以在 O(n) 时间内完成。
- 堆还有一个
peek()
方法,不用删除节点就返回最大值或者最小值。时间复杂度 O(1) 。
6.2.2-堆的插入
- 第一步是将新的元素插入到数组的尾部。
- 然后递归上滤来调整堆
插入例子:最大堆的数组是: [ 10, 7, 2, 5, 1 ],将数字 16
插入到这个堆中:
第一步是将新的元素插入到数组的尾部。数组变成:[ 10, 7, 2, 5, 1, 16 ],相应的树变成了:
16
被添加最后一行的第一个空位。
但是,现在的树不满足最大堆的定义,因为 2
在 16
的上面,所以需要交换 16
和 2
来调整堆。
现在还没有完成,因为 10
也比 16
小。我们继续递归调整,直到它的父节点比它大或者到达树的顶部。这就是所谓的上滤shift-up,每一次插入操作后都需要进行。它将一个太大或者太小的数字“上滤”到树的顶部。最后我们完成堆的插入:
6.2.3-堆的删除
- 第一步是将元素删除。
- 将最后一个原色放在空位
- 然后递归下滤来调整堆
同上例:将这个树中的 (10)
删除:
现在顶部有一个空的节点,怎么处理?
当插入节点的时候,将新的值返给数组的尾部。现在我们来做相反的事情:我们取出数组中的最后一个元素,将它放到树的顶部,然后再修复堆属性。
现在来看怎么 shift-down 1
。为了保持最大堆的堆属性,我们需要树的顶部是最大的数据。现在有两个数字可用于交换 7
和 2
。我们选择这两者中的较大者称为最大值放在树的顶部,所以交换 7
和 1
,现在树变成了:
继续堆化直到该节点没有任何子节点或者它比两个子节点都要大为止:
6.2.4-堆排序
-
方法
-
- 建堆:初始化一个n个元素的最大堆O(n)
- 方法:从index = A.length / 2 - 1 一直到根结点(index = 0)进行maxHeapify调整
-
- 输出并调整:每次删除最大元素并调整堆O(log2n)
-
方法:将根和最后一个结点交换,再做一系列调整(递归的)
(先把根移到最后,用虚线连在堆上,排序结束后所有都是虚线连接的)
-
-
时间复杂度是O(n)+O(nlog2n)=O(nlog2n)
-
堆排序是不稳定的
-
(在堆排序中,当先后出现两个同样的元素时,后出现的元素相对较小)
注意:
初始化为最大堆,堆排序后为从小到大排序的序列;
初始化为最小堆,堆排序后为从大到小排序的序列。
Chapter 7_Disjoint Set
7.1-并查集
并查集主要用于解决一些元素分组的问题。它管理一系列不相交的集合。
7.1.1-初始化
一开始,我们先将它们的父节点设为自己,即让每个元素成为一个集合
int fa[MAXN]; // fa[i] = j 代表i的父节点是j
inline void init(int n) {
for (int i = 1; i <= n; ++i)
fa[i] = i;
}
7.1.2-合并(Union)
把两个不相交的集合合并为一个集合:将一个的父节点设为另一个即可(用秩(树高)选择)
inline void merge(int i, int j) {
fa[find(i)] = find(j);
}
7.1.3-查询(Find)(重点)
- 查询两个元素是否在同一个集合中。
- 我们用递归的写法实现对代表元素的查询:一层一层访问父节点,直至根节点(根节点的标志就是父节点是本身)。要判断两个元素是否属于同一个集合,只需要看它们的根节点是否相同即可。
int find(int x) {
if(x == fa[x])
return x;
else
return find(fa[x]);
}
7.1.4-合并(路径压缩)
int find(int x) {
if(x == fa[x])
return x;
else{
fa[x] = find(fa[x]); //父节点设为根节点
return fa[x]; //返回父节点
}
}
Chapter 8_Graph//TODO
8.1-图的概念
- Graph=(V, E)
- V:顶点集
- E:边集
- 相关概念
- 无向图:不区分(a, b)和(b, a)边
- 有向图:区分(a, b)和(b, a)边
- 有向图的边有起点和终点
- 完全图:任意两个顶点之间都有边
- 分完全有向图和完全无向图
- 顶点的度数:顶点上的边数
- 在有向图中,有入度和出度
- 所有点的度数之和为边数的两倍
- 子图:同离散数学
- 路径和环路(简单路径和简单环路)
- 连通图与连通分量
- 在无向图中,如果存在从顶点v1到v2的路径,则v1和v2是连通的
- 在无向图中,如果两个任意顶点是连通的,则该图是连通图
- 网络
- 将权重分配给边时,生成的数据对象称为加权图和加权有向图。
- 网络指加权连通图和加权连通有向图。
- 生成树
- 最小连通子图n顶点生成树有n-1条边时称为连通图的生成树
- 注意:本课程不讨论有自环和多重边(即两个顶点之间有不止一条边)的图
8.2-邻接矩阵
- 是一个二维数组
- 如果u和v之间有边(u, v),置A[u][v]为1,否则置为0
- 如果是带权的边,则将数组值置为权值,并使用一个标记(通常是无穷)表示不存在的边
- 无向图的邻接矩阵是对称矩阵
- 除了邻接矩阵,还可以用邻接表存储
- 即对每个点用链表记录与其相连的顶点
8.3-图的遍历
8.3.1-深度优先遍历(DFS)
- depth first search
- 算法思想:
- 从图中某个顶点V0出发,访问它,然后选择一个V0邻接到的未被访问的一个邻接点V1出发深度优先遍历图,当遇到一个所有邻接于它的结点都被访问过了的结点U时,回退到前一次刚被访问过的拥有未被访问的邻接点W,再从W出发深度遍历……直到连通图中的所有顶点都被访问过为止
- 算法分析
- 递归方法实现
- 算法中用一个辅助数组visited[]:
- 0:未访问
- 1:访问过了
- 算法复杂度:
- 用邻接表表示 O(n+e)
- 用邻接矩阵表示 O(n2)
- 深度优先生成树
8.3.2广度优先遍历(BFS)
- breadth first search
- 算法思想:
- 从图中某顶点V0出发,在访问了V0之后依次访问V0的各个未曾访问过的邻接点,然后分别从这些邻接点出发广度优先遍历图,直至图中所有顶点都被访问到为止.
- 算法分析
- 算法同样需要一个辅助数组visited[] 表示顶点是否被访问过
- 还需要一个队列,记正在访问的这一层和上一层的顶点
- 算法显然是非递归的.
- 每个顶点进队列一次且只进一次
- 算法中循环语句至多执行n次。
- 算法复杂度:
- 用邻接表表示 O(n+e)
- 用邻接矩阵表示 O(n2)
- 对每个被访问过的顶点,循环检测矩阵中n个元素
8.4-最小生成树
8.4.1-生成树
- 生成树的概念
- 定义:
- 设G=(V, E)是一个连通的无向图(或是强连通有向图)从图G中的任一顶点出发作遍历图的操作,把遍历走过的边的集合记为TE(G),显然 G’=(V, TE)是G之子图,G’被称为G的生成树(spanning tree)
- n个节点的生成树有n-1条边
- 生成树的代价:TE(G)上诸边代价(权)之和
- 生成树不唯一
- 定义:
- 找到一个网络的最小生成树,即各边权的总和为最小的生成树
- 两个算法:Prim和Kruskal,都采用了逐步求解(Grandy)的策略
8.4.2-最小生成树
Grandy策略
- 贪心思想
- 设:连通网络N={V,E}, V中有n个顶点。
- 1)先构造n个顶点、 0条边的森林F={T0,T1,……,Tn-1}
- 2)每次向F中加入一条边。该边是一端在F的某棵树Ti上而另一端不在Ti上的所有边中具有最小权值的边。这样使F中两棵树合并为一棵,树的棵数-1
- 3)重复n-1次
Kruskal算法
- 算法步骤:
- 1)把无向图中的所有边排序
- 2)一开始的最小生成树为 T (V, TE)
- 3)在E中选一条权最小的边(u,v)加入T,一定要满足(u,v) 不和已有的边构成回路
- 4)一直到TE中加满n-1条边为止
- 数据准备
- 图用邻接矩阵表示,edge(边的信息)
- 图的顶点信息在顶点表 Verticelist中
- 边的条数为CurrentEdges
- 取最小的边以及判别是否构成回路
- 取最小的边利用: 最小堆(MinHeap)
- 算法分析
- i. 建立e条边的最小堆
- 检测邻接矩阵——O(n2)
- 插入e条边,每次执行一次上滤(建堆需要),总时间需要O(elog2e)
- ii.构造最小生成树
- e次出堆操作,每次出堆进行一次下滤——O(elog2e)
- 2e次查找操作——O(elog2n)
- n-1次union操作——O(n)
- 所以,总的计算时间为:O(elog2e+elog2n+n2+n)
- i. 建立e条边的最小堆
- 算法时间复杂度:O(n2)
Prim算法
- 算法步骤:
- 生成树的顶点集合 U(最后也有n个),
- 一开始为空的集合TE
- 1)U加入一个任何起始顶点
- 2)每次选择一条边。这条边是所有边(u,v)中代价(权)最小的边且要满足(u,v) 不和已有的边构成回路
- 3)一直到TE中加满n-1条边为止
- 数据准备
- Lowcost[]:存放生成树顶点集合内顶点到生成树外各顶点的边上的当前最小权值;
- nearvex[]:记录生成树顶点集合外各顶点,距离集合内那个顶点最近。
-
算法分析
- 算法时间复杂度:
- 采用邻接矩阵,改进了实现效率 O(n2)
8.4.3-最短路径
- 从顶点v到顶点w的权重最小的路径
- 三种算法:
- 1)边上权值为非负情况的从一个结点到其它各结点的最短路径 (单源最短路径)(Dijkstra算法)
- 2)边上权值为任意值的单源最短路径
- 3)边上权值为非负情况的所有顶点之间的最短路径
Dijkstra算法
-
求一个点到其他各个顶点的最短路径
-
算法思想
-
按最短路径长度递增的次序产生最短路径。
-
不可能从一条长的路径出发绕一圈而绕出比短路径更短的路线。
-
-
数据准备
- 距离值数组dist:
- 路径数组path:每次放由v0到达该顶点的前一顶点(-1代表暂时无法到达该点)
- 注意:这两个数组都会在算法执行过程中不断被更新
-
算法步骤:
- 1)在起点v0到直接有连线的各顶点的path中找最小的v’,即为v0到v’的最短路径
- 2)从起点v0到余下点中最短的path
- 这里的path可以不是直接连线,而是可以是经过前面已找到的最短path的顶点
- 3)直至找到所有最短路径
-
算法时间复杂度:
- 采用邻接矩阵,改进了实现效率 O(n2)
8.5-活动网络
8.5.1-AOV网络
- 用顶点表示活动,用弧表示活动间的优先关系的有向图称为AOV网。
- AOV网中,不应该出现有向环
- 拓扑排序
- 拓扑序列不是唯一的
- 算法思想:
- 1)从图中选择一个入度为0的结点输出之。(如果一个图中,同时存在多个入度为0的结点,则随便输出一个结点)
- 2)从图中删掉此结点及其所有的出边。
- 3)反复执行以上步骤:
- a)直到所有结点都输出了,则算法结束
- b)如果图中还有结点,但入度不为0,则说明有环路
- 为了避免每次从头到尾查找入度为0的顶点,建立入 度为0的顶点栈,栈顶指针为top,初始化时为-1
- 算法复杂度:n个顶点,e条边
- 建栈O(n)
- 每个结点输出一次,每条边被检查一次O(n+e)
- 所以为:O(n+n+e)
8.5.2-AOE网络
- 用边表示的活动网络(AOE网络),又称为事件顶点网络
- 顶点:表示事件(event)
- 事件,或者称为状态
- 表示它的入边代表的活动已完成,它的出边代表的活动可以开始
- 有向边:表示活动
- 边上的权——表示完成一项活动需要的时间
- 顶点:表示事件(event)
- 与AOV网不同
- 有唯一的入度为0的开始结点
- 唯一的出度为0的完成结点
- 关键路径
- 目的:利用AOE,研究完成整个工程需要多少时间,加快那些活动的速度后,可使整个工程提前完成
- 关键路径:具有从开始顶点(源点)到完成顶点(汇点)的最长(加权)的路径
- 找关键活动的算法:
- 定义:
- 对事件而言,即顶点
- Ve[i]-表示事件Vi的可能最早发生时间定义为从源点V0到Vi的最长路径长度
- Vl[i]-表示事件Vi的允许的最晚发生时间。是在保证整个AOE网络完成的前提下, 事件Vi允许发生的最晚时间,Vl[i] = Ve[n-1] - (Vi 到 Vn-1的最长路径长度)
- 对活动而言,即边
- e[k]:表示活动k=<Vi,Vj>的最早开始时间,即等于事件Vi的可能最早发生时间,e[k]=Ve[i]
- l[k]:表示活动k=<Vi,Vj>的允许的最迟开始时间,l[k]= Vl[j]-完成该活动所需要的时间
- 松弛时间
- l[k]-e[k],表示活动ak的最早可能开始时间和最迟允许开始时间的时间余量。
- l[k]=e[k]表示没有松弛时间的关键活动
- 对事件而言,即顶点
- 算法分析:
- 拓扑排序Ve[i]
- 逆拓扑排序求Vl[i]
- 求各活动e[k]和l[k]
- 复杂度为O(n+e)
- 定义:
Chapter 9_Sorting
9.1-排序概述
- 排序:n个对象的序列R[0],R[1],R[2],…R[n-1]按其关键码的大小,进行由小到大(非递减)或由大到小(非递增)的次序重新排序的。
- 关键码(key)
- 两大类:
- 内排序:对内存中的n个对象进行排序
- 外排序:内存放不下,还要使用外存的排序。
9.2-插入排序
排序算法 | 最小比较 | 最大比较 | 时间复杂度 | 最小移动 | 最大移动 | 空间复杂度 | 稳定性 |
---|---|---|---|---|---|---|---|
直接插入排序 | n | n2/2 | O(n2) | 0 | (n2+3n-4)/2 | O(1) | 稳定 |
二分法插入排序 | nlog2n | nlog2n | O(nlog2n) | 0 | O(1) | 稳定 | |
表插入排序 | n | n2/2 | O(n2) | 0 | 0 | O(n) | 稳定 |
shell排序 | O(1) | 不稳定 |
9.2.1-直接插入排序
- 前i-1个元素已经排好序,对第i个元素插入,将i与i-1,i-2,i-3……比较,直至找到合适的位置
9.2.2-二分法插入排序
- 原理同二分查找,即查找插入的位置
- 折半查找所需比较次数与初始排序无关,仅依赖于对象个数
9.2.3-表插入排序
- 链表插入排序
9.2.4-shell排序(缩小增量排序)
-
希尔排序
-
方法
-
1)取一增量(间隔gap<n),按增量分组,对每组使用直接插入排序或其他方法进行排序。
(即每隔gap个的元素为一组)
-
2)减少增量(分的组减少,但每组记录增多)。直至增量为1,即为一个组时。
-
-
算法性能与选择的缩小增量有关,但到目前还不知如何选择最好结果的缩小增量序列。
-
平均比较次数与移动次数大约n1.3左右。
9.3-交换排序
排序算法 | 最小比较 | 最大比较 | 时间复杂度 | 最小移动 | 最大移动 | 空间复杂度 | 稳定性 |
---|---|---|---|---|---|---|---|
冒泡排序 | n-1 | n(n-1)/2 | O(n2) | 0 | 3n(n-1)/2 | O(1) | 稳定 |
快速排序 | nlog2n | n2/2 | O(nlog2n) | <=nlog2n | <=nlog2n | O(log2n) | 不稳定 |
- 不断的交换反序的对偶,直到不再有反序的对偶为止。
9.3.1-Bubble Sort
- 1)从头到尾做一遍相邻两元素的比较,有颠倒则交换,记下交换的位置。一趟结束,一个或多个最大(最小)元素定位。
- 2)去掉已定位的的元素,重复1,直至一趟无交换。
9.3.2-Quick Sort(重点)
-
分划交换排序
-
1)在n个对象中,取一个对象(如第一个对象——基准pivot),按该对象的关键码把所有<=该关键码的对象分划在它的左边。>该关键码的对象分划在它的右边。
- 对左边和右边(子序列)分别再用快排序。
-
算法:用递归方法实现
-
选取pivot
- 方法1:随机选取pivot, 但随机数的生成一般是昂贵的。
- 方法2:三数中值分割法(Median-of-Three partitioning)
- 一般选左端、右端和中心位置上的三个元素的中值作为pivot
9.4-选择排序
排序算法 | 比较次数 | 时间复杂度 | 最小移动 | 最大移动 | 空间复杂度 | 稳定性 |
---|---|---|---|---|---|---|
直接选择排序 | n(n-1)/2 | O(n2) | 3(n-1) | 3(n-1) | O(1) | 不稳定 |
堆排序 | O(nlog2n) | O(nlog2n) | O(1) | 不稳定 |
9.4.1-直接选择排序
- 首先在n个记录中选出关键码最小(最大)的记录,然后与第一个记录(最后第n个记录) 交换位置,再在其余的n-1个记录中选关键码 最小(最大)的记录,然后与第二 个记录( 第n-1个记录)交换位置,直至选择了n-1个记录。
- 比较次数:n-1+n-2+…+1=n(n-1)/2=O(n2) 与原始记录次序无关。
9.4.2-锦标赛排序(树形选择排序)
-
1)n个对象的关键码两两比较得到 n/2 个 比较的优胜 者(关键码小者)保留下来, 再对这 n/2 个对象再进行关键 码的两两比较, …直至选出一个最小的关键码为止。如果n不是2的K次幂,则让叶结点数补足
-
2)输出最小关键码。 再进行调整: 即把叶子结点上,该最小关键码改为最大值后,再进行
由底向上的比较,直至找到一个最小的关键码(即次小关 键码)为止。重复2,直至把关键码排好序。
9.4.3-堆排序
见Chapter 6
9.5-分配排序
9.5.1-基数排序(桶排序)
-
步骤:
-
以LSD为例,假设原来有一串数值:73, 22, 93, 43, 55, 14, 28, 65, 39, 81
-
首先根据个位数的数值,在走访数值时将它们分配至编号0到9的桶子中并输出新序列:
81, 22, 73, 93, 43, 14, 55, 65, 28, 39
-
接着再进行一次分配,这次是根据十位数来分配并输出新序列:
14, 22, 28, 39, 43, 55, 65, 73, 81, 93
-
9.6-归并排序(Merge Sort)
排序算法 | 比较次数 | 时间复杂度 | 移动次数 | 空间复杂度 | 稳定性 |
---|---|---|---|---|---|
归并排序 | O(nlog2n) | O(nlog2n) | O(n) | 不稳定 |
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0mRwXsFF-1610277830759)(/Users/zhaoyuzhou/Library/Application Support/typora-user-images/截屏2021-01-06 23.32.00.png)]