在开始学习算法至今接近5个月的时间里进步极慢,经过思索还是因为刷题未能集中,无法成体系的进行归纳,今后对算法的学习还是要按照模块来进行,以周为时间单位,争取每周拿下一块知识点。
二叉树的遍历
1. 层序遍历(bfs)
顾名思义:以层为单位进行遍历,一般使用队列(先进先出)作为遍历的方式
第一层的节点加入队列后,如何遍历第二层呢?
将根节点从队列中poll出,将poll出的根节点的左右节点加入队列。
如何遍历第三层以至于第n层?
同样,将第n-1层poll出,对poll出的节点的左右节点add入队列。
1 | public void bfs(node root){ |
2. 前中后序遍历(dfs递归)
二叉树dfs的两个要素:访问相邻节点(体现深度优先)和判断base case(递归截止条件)
2.1 前序遍历
前序遍历的思想类似dfs或者说回溯,递的过程一直往当前节点的左子节点进行遍历,没有就遍历右子节点。归的过程即回溯遍历右子节点。
1 | public void qianXu(node root){ |
2.2 中序遍历
与前序类似,遍历规则变为“左子树——根节点——右子树”。
1 | public void zhongxu(node root){ |
2.3 后序遍历
1 | public void houxu(node root){ |
3. 前中后序遍历(迭代)
借用栈空间进行遍历
3.1 非递归前序遍历
法一:栈
区别于递归遍历的顺序,要利用递归的思路,需要先放右节点进栈,再放左节点进栈,然后循环一直到最后会一直优先取到左节点。达到和递归顺序相仿效果。
1 | public void qianxu2(node root){ |
法二:栈,传统方法,先左节点全部入栈,然后遍历其右节点,再向左遍历将左节点进栈。思路完全就是递归的思路。类似于递归实现的栈空间。
1 | public void qianxu3(node root){ |
3.2 非递归中序遍历
和前序类似,就是节点的打印时机应该选在出栈之后,而不是入栈之前。
1 | public void zhongxu2(node root){ |
3.3 非递归后序遍历
1 | public void houxu2(node root){ |
4. morris遍历方法
Morris
遍历使用二叉树节点中大量指向null
的指针,时间复杂度:O(n), 额外空间复杂度:O(1)
Morris的整体思路就是以某个根结点开始,找到它左子树的最右侧节点之后与这个根结点进行连接
我们可以从 图2 看到,如果这么连接之后,cur 这个指针是可以完整的从一个节点顺着下一个节点遍历,将整棵树遍历完毕,直到 7 这个节点右侧没有指向。
如何构造这个网络?
1 | public void mirros(node root){ |
4.1 morris前序遍历
在某个根节点创建连线的时候打印。因为我们是顺着左边的根节点来创建连线,且创建的过程只有一次。
1 | public void mirros(node root){ |
4.2 mirros中序遍历
与前序遍历的区别在于,从左侧开始顺着右节点打印,也就是将cur1切换到上层节点的时候
1 | public static void inOrderMorris(TreeNode head) { |
树的概念分类
- 树的路径数==叶子节点个数
1. 二叉查找树(BST)
特点:左子树的节点值比父亲节点小,而右子树的节点值比父亲节点大
bst的遍历
一般使用中序遍历,可以直接获取元素值排序后的结果
bst的查找(O(logn))
类似于二分查找,每次和根节点比较后,忽略另一半无关节点
二分查找时间复杂度的计算:
假设用k次查找到,
N/2^k
就表示未查找的元素数,对于最坏情况,该值等于1,此时的k = $log_2n$bst的删除
删除有多个子节点的节点
删除节点在中序遍历后面的节点中,选择大于删除节点的最小值作为后继节点
基于二叉查找树的这种特点,在查找某个节点的时候,可以采取类似于二分查找的思想,快速找到某个节点。n 个节点的二叉查找树,正常的情况下,查找的时间复杂度为 O(logN)。之所以说是正常情况下,是因为二叉查找树有可能出现一种极端的情况(退化为链表),例如:
这种情况也是满足二叉查找树的条件,然而,此时的二叉查找树已经近似退化为一条链表,这样的二叉查找树的查找时间复杂度顿时变成了 O(n)。由此必须防止这种情况发生,为了解决这个问题,于是引申出了平衡二叉树。
2. 平衡二叉树(AVL)
特点:
- 非叶子节点最多拥有两个子节点。
- 非叶子节点值大于左边子节点、小于右边子节点。
- 树的左右两边的层级数相差不会大于1。
- 没有值相等重复的节点。
平衡树的层级结构:平衡二叉树的查询性能和树的层级(高度h)成反比,h值越小查询越快。为了保证树的结构左右两端数据大致平衡。降低二叉树的查询难度一般会采用一种算法机制实现节点数据结构的平衡,实现了这种算法的有比如Treap、红黑树。使用平衡二叉树能保证数据的左右两边的节点层级相差不会大于1,通过这样避免树形结构由于删除增加变成线性链表影响查询效率,保证数据平衡的情况下查找数据的速度近于二分法查找
3. 红黑树
为什么有了平衡树还要有红黑树呢?
平衡树的左右层级数不能大于1过于严格,每次插入操作都会破坏原有结构,这时候就需要红黑树进行一下折中
特点:
- 节点是红色或黑色
- 根节点是黑色
- 每个叶子节点都是黑色的空节点
- 每个红色节点的两个子节点都是黑色(从每个叶子到根的所有路径上不能有两个连续的红色节点)
- 从任一节点到其每个叶子的所有路径都包含相同数目的黑色节点
满足以上的性质,这个二叉树就趋于平衡(注意图中有两个空的叶子节点未画出)
红黑树并不是一个完美平衡二叉查找树,从图1可以看到,根结点P的左子树显然比右子树高,但左子树和右子树的黑结点的层数是相等的,也即任意一个结点到到每个叶子结点的路径都包含数量相同的黑结点(性质5)。所以我们叫红黑树这种平衡为黑色完美平衡。
3.1 自平衡的调整方法(变色和旋转)
变色
为了重新符合红黑树的规则,尝试把红色节点变为黑色,或者把黑色节点变为红色。
下图所表示的是红黑树的一部分,需要注意节点25并非根节点。因为节点21和节点22连续出现了红色,不符合规则4,所以把节点22从红色变成黑色:
但这样并不算完,因为凭空多出的黑色节点打破了规则5,所以发生连锁反应,需要继续把节点25从黑色变成红色:
此时仍然没有结束,因为节点25和节点27又形成了两个连续的红色节点,需要继续把节点27从红色变成黑色:
旋转
左旋
以某个结点作为支点(旋转结点),其右子结点变为旋转结点的父结点,右子结点的左子结点变为旋转结点的右子结点,左子结点保持不变
右旋
以某个结点作为支点(旋转结点),其左子结点变为旋转结点的父结点,左子结点的右子结点变为旋转结点的左子结点,右子结点保持不变
总而言之,你当我儿子,我儿子给你当儿子
3.2 红黑树查找
和二叉搜索树类似,时间复杂度是O(logn)
3.3 红黑树插入
插入操作包括两部分:
- 查找插入的位置
- 插入后自平衡
但插入结点是应该是什么颜色呢?
答案是红色。理由很简单,红色在父结点(如果存在)为黑色结点时,红黑树的黑色平衡没被破坏,不需要做自平衡操作。但如果插入结点是黑色,那么插入位置所在的子树黑色结点总是多1,必须做自平衡。
插入节点情景分析
红黑树是空树:直接插入作为根节点并设置为黑色
插入节点key已存在:
将插入节点颜色设置为当前节点颜色;
replace,更新当前节点的value为插入节点的value
插入节点的父节点为黑色:
直接插入,不会影响黑色完美平衡
插入节点的父节点为红节点:(祖父节点存在,且为黑色)
此时红红相连,红黑树性质被破坏,此时又要细分几种情况
叔叔节点存在且为红色
最简单的处理方法是将“黑红红”改为“红黑红”
如果pp是跟节点则直接黑化
pp的父节点是黑色则不影响
如果此时pp的父节点为红色则还不满足,需要继续进行自平衡处理(右旋?)
1. 无向图
如果一个图结构中,所有的边都没有方向性,那么这种图便称为无向图。典型的无向图,如图所示。由于无向图中的边没有方向性,这样我们在表示边的时候对两个顶点的顺序没有要求
2.有向图
一个图结构中,边是有方向性的,那么这种图就称为有向图,如图所示。由于图的边有方向性,我们在表示边的时候对两个顶点的顺序就有要求。我们采用尖括号表示有向边,例如
2.1 有向有环图和有向无环图的区别
这两张图对应Leecode上的207课程表问题(下面的专栏第3题),第一张对应返回值为true,第二张对应返回值为false;
对于有向(无环)图来说,可以类似看做是一个树(根节点不一定为1,子节点之间有指向关系),可以通过入度和出度来对顶点进行划分。
3. 顶点的度
连接顶点的边的数量称为该顶点的度。顶点的度在有向图和无向图中具有不同的表示。对于无向图,一个顶点V的度比较简单,其是连接该顶点的边的数量,记为D(V)。 例如,上面所示的无向图中,顶点V5的度为3。而V6的度为2。
对于有向图要稍复杂些,根据连接顶点V的边的方向性,一个顶点的度有入度和出度之分。
- 入度是以该顶点为端点的入边数量, 记为ID(V)。
出度是以该顶点为端点的出边数量, 记为OD(V)。
这样,有向图中,一个顶点V的总度便是入度和出度之和,即D(V) = ID(V) + OD(V)。例如,上面所示的有向图中,顶点V5的入度为3,出度为1,因此,顶点V5的总度为4。
4. 邻接顶点
邻接顶点是指图结构中一条边的两个顶点。 邻接顶点在有向图和无向图中具有不同的表示。对于无向图,邻接顶点比较简单。例如,在图二所示的无向图中,顶点V2和顶点V6互为邻接顶点,顶点V2和顶点V5互为邻接顶点等。
对于有向图要稍复杂些,根据连接顶点V的边的方向性,两个顶点分别称为起始顶点(起点或始点)和结束顶点(终点)。有向图的邻接顶点分为两类:
- 入边邻接顶点:连接该顶点的边中的起始顶点。例如,对于组成
这条边的两个顶点,V2是V6的入边邻接顶点。 - 出边邻接顶点:连接该顶点的边中的结束顶点。例如,对于组成
这条边的两个顶点,V6是V2的出边邻接顶点。
bfs专栏
- bfs通常使用队列或栈等数据结构,通过加以约束的offer和poll实现类似于递归实现的效果,基本可以说是套用模板。。。
- bfs题目大致分为几类:树形结构(1,2,4),图形结构(3),网格(5)
1. 二叉树的最大深度
递归算法(max+1)较为简单暂且不论,且看用bfs迭代进行遍历的一个变形,就是每次不从队列取出一个,而出取出一层。
1 | public int maxDepth(Node root){ |
此方法可进行的引申:二叉树的层次遍历,从上到下打印二叉树,
103. 二叉树的锯齿形层次遍历,无非就是加个flag,每层输出的时候正反调换下位置(List接口中的add方法,可以加入索引参数0,是的列表每次从最前面添加,实现倒序输出)
1.1. 二叉树的最小深度
趁热打铁:
1 | class Solution { |
2. 对称二叉树
1 | class Solution { |
bfs的模板基本确定,无非就是入队,出队的方式发生些许变化,这题是非常重要的灵活运用bfs方法的体现。
3. 课程表*
可以说传入的第一个整型参数对应的是顶点数!第二个传入的二维数组就是带指向性的边!(
1 | class solution{ |
4.员工的重要性
和上面的课程表问题有些类似,高等级员工指向低等级员工,,然后一级一级的向下求和,每一级所对应的节点相互间不存在关系,对应的是树结构
而上面的课程表问题,每节课指向的子节点之间可能也有指向关系,所以对应的是图结构(无向环)
5.岛屿数量
方法;线性扫描整个二维网络,如果一个节点包含1,则以其为根节点启动广度优先搜索。将其放入队列中,并将值设为0以标记访问过该节点。迭代的搜索队列中的每一个节点。直到队列为空。
与树状和图结构的区别:对于队列的while循环需嵌套在对于数组的循环内部
1 | class Solution{ |
dfs专栏
1. 树、图、网格
hot100题中的bfs题刷完,转到dfs这,好嘛,都有重复的,正好再用dfs的方法重温一下,看能否bfs一样归纳出一个递归回溯的模板出来。
无论树,图,网格,都是先遍历至边界处,然后开始回溯
重点在于边界的判断方法!!!:
- 遇null返回(1,1.1,2),一般对应二叉树结构,即遍历到叶子节点执行回溯
- for循环无法进行自行返回(3,4,此时的for循环往往为for-each结构),一般对应由List表示的多叉树,图结构,本质上也是遍历到叶子节点执行回溯。以for(String v, u)为例,u即本层遍历节点,v即u指向的下层节点!!!!!
- 超出网格范围和碰到已遍历过的节点,就执行,本质上也就是对应叶子节点
1. 二叉树的最大深度
回忆一下bfs的解法:通过二叉树的层序遍历方法对二叉树进行遍历,常见depth整型变量,每遍历一层depth++
递归解决和bfs的比较:时间复杂度和空间复杂度相同,bfs是通过从上到下进行depth++,dfs是从下到上depth++
1 | public int maxDepth(TreeNode root){ |
1.1. 二叉树的最小深度
1 | class Solution { |
2. 对称二叉树
回忆一下dfs的解法:不同于遍历,此时需成对入栈,关键点在于入栈前的判断
bfs和dfs 的对比:判断方法完全相同
1 | class Solution { |
3. 课程表*
从上面的树结构可以看出:
bfs都是从上往下逐层进行遍历,dfs则是通过递归,直接搜索到最末断点的叶子节点,然后从下往上进行遍历!!!
映射到图结构上也是一般无二:
bfs从入度为0的顶点开始展开(类似于树的根节点),然后是入度为1,2,3…(类似于树的层);
dfs从出度为0的顶点开始展开(类似于树的叶子节点),然后开始回溯。。。
拓扑排序:对于有向图G
图 G中的任意一条有向边 (u,v),u 在排列中都出现在 v 的前面,称该排列为图G的拓扑排序
对于图中的任意一个节点,它在搜索的过程中有三种状态,即:
「未搜索」:我们还没有搜索到这个节点; 「搜索中」:我们搜索过这个节点,但还没有回溯到该节点,即该节点还没有入栈,还有相邻的节点没有搜索完成); 「已完成」:我们搜索过并且回溯过这个节点,即该节点已经入栈,并且所有该节点的相邻节点都出现在栈的更底部的位置,满足拓扑排序的要求。
1 | class Solution{ |
4.员工的重要性
n叉树的dfs模板,无参考自行设计:
1 | class Solution { |
类似于二叉树的前序遍历,此时的回溯过程都是直接return,当所有叶子节点完成遍历,ans的值就已经确定
稍作修改即可实现类似于上面课程表的后序遍历:
1 | class Solution { |
优化方式也是显而易见的,对于二叉树而言,根本不用考虑遍历过节点的重复搜索问题
5.岛屿数量*
这道题就厉害了,网格结构使用dfs的代表之作!!在leetcode上看到了一篇岛屿类问题的通用解法、DFS 遍历框架的文章,且看且分析
- 网格结构要比二叉树结构稍微复杂一些,其实是一种简化的图结构。
- 在结合着上面的bfs解法和树结构相联系,bfs就是以一个节点为起始点启动bfs,然后遍历周围4个节点入队列,依次类推,遍历过的打上标记
- dfs就是选个方向一条道走到头,然后再回头另一个方向走到头。。。
dfs遍历的框架代码:
1 | void dfs(int[][] grid, int r, int c) { |
避免重复遍历:
之所以说网格结构区别于树,类似于图的原因,就在于其相邻的四个节点都可以构成边,因此就需要将遍历过的节点进行标记,将不需要的边断掉!
1 | void dfs(int[][] grid, int r, int c) { |
本题代码:
1 | class Solution{ |
与bfs相比,dfs的速度明显是要快一些!!!
6. 岛屿的周长
此题稍加灵活,算是比较合适的模板练手题目
1 | public int islandPerimeter(int[][] grid) { |
2. 排列构成的树形结构
此类问题的常见题型:
对给定范围的数据进行重排,重排过程往往需要实现数据的重复利用,需要由dfs在每一层都进行回溯,从而达到任意排列组合的效果
同样是变形为图结构的题目,但是有其独特的规律可循
解题步骤:
将排列组合看做树结构
dfs的截止条件:一般为层数(如1.1字符串全排列),也可能为字符串或列表长度,和层数无关(如2.2数字翻译字符串),也可能与字符串长度和层数同时相关(如2 复原IP)
**这里需要非常注意的一点是:截止条件就可以作为递归的参数!!!非常方便**且在满足截止条件时,一般是一条链遍历结束,需先输出后返回;
候选节点:即该层对应的节点,区别于但又类似于二叉树向下遍历时的root.left,root.right,可用for循环遍历本层的所有节点
- 候选节点的选择:题目往往会对候选节点加以限制,不然全排过于好做
恢复状态:候选节点的限制往往通过改变其状态实现,在递归函数的代码行后,还需恢复其状态,保证每一次调用时候选列表不会改变
注意点:
回溯过程中的列表是动态变化的,在add到总列表中的时候需要ans.add(new ArrayList(result))!!!!!!!!!
1. 全排列
1 | class Solution { |
1.1 全排列优化- 字符串的排列
1 | class Solution{ |
2. 复原IP地址
1 | class Solution { |
2.2 把数字翻译成字符串
1 | class Solution { |
3. 电话号码的字母组合
1 | class Solution { |
4. 组合总和
1 | class Solution { |
5. 括号生成
1 | class Solution { |
6. 所有可能的路径
1 | class Solution { |
. 二叉树展开为链表
1 | //方法1:递归的栈结构 |