基础和技术就像无敌的深渊,小伙子,你要不断的学哟~~…
PART1:先来看看树相关的操作的时间复杂度:
特此鸣谢在leetcode上分享答案的各位大神,让我能够对自己的笔记有如下补充:
先来上概念和技巧,以备不时之查:
-
用
数组
实现的堆就是树(堆就是一个完全二叉树,用数组来存储则不需要节点指针,一般也就是用数组来存储完全二叉树,不是完全二叉树的树不适合用数组存储)- 满二叉树:一个二叉树,如果每一个层的结点数都达到最大值,则这个二叉树就是 满二叉树。也就是说,如果一个二叉树的层数为 K,且结点总数是(2^k) -1 ,则它就是 满二叉树
- 完全二叉树:
除最后一层外,若其余层都是满的,并且最后一层或者是满的,或者是在右边缺少连续若干节点
,则这个二叉树就是 完全二叉树 。- 完全二叉树有一个很好的性质:父结点和子节点的序号有着对应关系。
当根节点的值为 1 的情况下,若父结点的序号是 i,那么左子节点的序号就是 2i,右子节点的序号是 2i+1
。这个性质使得完全二叉树利用数组存储时可以极大地节省空间,以及利用序号找到某个节点的父结点和子节点
- 完全二叉树有一个很好的性质:父结点和子节点的序号有着对应关系。
- 平衡二叉树
- 平衡二叉树 是一棵二叉排序树,可以是一棵空树;如果不是空树,
平衡二叉树的左右两个子树的高度差的绝对值不超过 1
,并且左右两个子树都是一棵平衡二叉树 - 平衡二叉树的常用实现方法有 红黑树、AVL 树、替罪羊树、加权平衡树、伸展树 等。
- 红黑树
- 红黑树特点 :
- 每个节点非红即黑;
- 根节点总是黑色的;
- 每个叶子节点都是黑色的空节点(NIL节点)
- 如果节点是红色的,则它的子节点必须是黑色的(反之不一定)
- 从根节点到叶节点或空子节点的每条路径,必须包含相同数目的黑色节点 (即相同的黑色高度)
- 红黑树的应用 :TreeMap、TreeSet以及JDK1.8的HashMap底层都用到了红黑树。
红黑树就是为了解决二叉查找树的缺陷,因为二叉查找树在某些情况下会退化成一个线性结构
。详细了解可以查看 漫画:什么是红黑树? - 红黑树深入剖析及Java实现~来自美团技术团队
- 红黑树特点 :
- 红黑树
- 平衡二叉树 是一棵二叉排序树,可以是一棵空树;如果不是空树,
- B tree 与B+Tree的区别以及原理和应用场景:
- B-tree的由来?
- 因为在申请内存的时候,不知道要申请多大的内存,所以没办法申请很大的一块内存,所以就变成了一个数组被打断为好几段,然后每段用链表连接起来,这其实就是树的基本模型。
- B-tree和B+tree的区别是什么?
B-tree中非叶子节点可以存值
;但是B+tree非叶子节点不可以存值,只能存key,B+tree值只存在叶子节点中
。- B-tree中叶子节点
没有用指针连接起来
;而B+tree中的叶子节点用指针连接起来,所以B+tree的查询很快,当定位到叶子节点后,只需要遍历叶子节点即可
。B+树相比于B树能够更加方便的遍历。 - B+树简单的说就是变成了一个索引一样的东西。 B+的搜索与B-树也基本相同,区别是
B+树只有达到叶子结点才命中(B-树可以在非叶子结点命中),B+树的性能相当于是给叶子节点做一次二分查找
。 - B+树的查找算法:当B+树进行查找的时候,你首先一定需要记住,就是
B+树的非叶子节点中并不储存节点,只存一个键值方便后续的操作,所以非叶子节点就是索引部分,所有的叶子节点是在同一层上,包含了全部的关键值和对应数据所在的地址指针
。这样其实,进行 B+树的查找的时候,只需要在叶子节点中进行查找就可以了
- 比如数据库那里的非主键索引或者叫二级索引,B+树的叶子节点存的就是二级索引的主键,然后二级索引跳到主键索引那里继续查找
- B-tree和B+tree各自的应用场景有什么不同?
- mysql的MyISAM和InnoDB两个存储引擎的索引实现方式:
- InnoDB中表数据本身就是按B+ Tree组织的一个索引结构,
InnoDB中表里面的叶节点存放的就不是数据记录的地址,而是完整的数据记录
。所以InnoDB这种存储方式,又称为聚集索引
,使得按主键的搜索十分高效,但与聚集索引相反的二级索引搜索需要检索两遍索引:首先二级索引获得主键,然后用主键到主索引中检索到数据记录
。 - 因为主键是InnoDB表记录的”逻辑地址“,
所以InnoDB要求表必须有主键
,MyISAM可以没有。 - MyISAM引擎使用B+ Tree作为索引结构,叶节点存放的是数据记录的地址。因为
MyISAM索引文件和数据文件是分离的,索引文件仅保存数据记录的地址
。 MyISAM引擎的辅助索引(二级索引)和主索引在结构上没有区别,只是辅助索引的key可以重复,叶节点上存放的也是数据记录的地址
。- B树适合那种访问之后能够把相应的数据一起返回的数,B树存在的意义是当都在内存中的时候,若是能够根据key直接把value得到然后返回,不用去磁盘查找, 明显要快很多,因为B+树的数据(value)是存在叶子节点中的,而叶子节点是存在于磁盘中的。也就是说
当数据很小的时候,使用B数把数据跟key一起存放在非叶子节点中,可以更快的加速查找
。
- InnoDB中表数据本身就是按B+ Tree组织的一个索引结构,
- mysql的MyISAM和InnoDB两个存储引擎的索引实现方式:
- B-tree的由来?
- 二叉查找树(Binary Search Tree,简称BST):
二叉查找树或者BST的左子节点的值比父节点的值要小,右节点的值要比父节点的值大
。它的高度决定了它的查找效率【运用了二分查找的思想,可以使得查找所需的最大次数等同于二叉查找树的高度
】。在理想的情况下,二叉查找树增删查改的时间复杂度为O(logN)(其中N为节点数),最坏的情况下为O(N)
。当它的高度为logN+1时,我们就说二叉查找树是平衡的。
- BST的常见操作:
- 查找操作
- BST的插入操作:
- BST的删除操作:
- 查找操作
- BST存在的主要问题是,
BST二叉查找树在插入的时候会导致树倾斜
,不同的插入顺序会导致树的高度不一样,而树的高度直接的影响了树的查找效率。理想的高度是logN,最坏的情况是所有的节点都在一条斜线上,这样的树的高度为N,就变成了一个斜树,也就是二叉查找树此时已经退化成为一个链表了
。也就是变瘸了。
- BST的常见操作:
- 斜树:这棵树已经退化为一个链表了,我们管它叫 斜树
- 二叉树相比于链表,由于父子节点以及兄弟节点之间往往具有某种特殊的关系,这种关系使得我们在树中对数据进行搜索和修改时,相对于链表更加快捷便利。但是,如果二叉树退化为一个链表了,那么那么树所具有的优秀性质就难以表现出来,效率也会大打折,为了避免这样的情况,我们希望每个做 “家长”(父结点) 的,都 一碗水端平,分给左儿子和分给右儿子的尽可能一样多,相差最多不超过一层
- 平衡二叉查找树(Balanced BST):
- 基于BST存在的退化成为链表的问题,一种新的树——平衡二叉查找树(Balanced BST)产生了。
平衡二叉查找树(Balanced BST)在插入和删除的时候,会通过旋转操作将高度保持在logN。其中两款具有代表性的平衡树分别为AVL树和红黑树
。AVL树由于实现比较复杂,而且插入和删除性能差,在实际环境下的应用不如红黑树
。 - 红黑树(Red-Black Tree,以下简称RBTree)的实际应用非常广泛,比如Linux内核中的完全公平调度器、高精度计时器、ext3文件系统等等,
各种语言的函数库如Java的TreeMap和TreeSet
,C++ STL的map、multimap、multiset等。- RBTree也是函数式语言中最常用的持久数据结构之一,在计算几何中也有重要作用。值得一提的是,
Java 8中HashMap的实现也因为用RBTree取代链表,性能有所提升
。
- RBTree也是函数式语言中最常用的持久数据结构之一,在计算几何中也有重要作用。值得一提的是,
- 基于BST存在的退化成为链表的问题,一种新的树——平衡二叉查找树(Balanced BST)产生了。
- 红黑树(Red-Black Tree,以下简称RBTree):
- RBTree在理论上还是一棵BST树,但是它在对BST的插入和删除操作时会维持树的平衡,即保证树的高度在[logN,logN+1](理论上,极端的情况下可以出现RBTree的高度达到2*logN,但实际上很难遇到)。这样RBTree的查找时间复杂度始终保持在O(logN)从而接近于理想的BST。RBTree的删除和插入操作的时间复杂度也是O(logN)。RBTree的查找操作就是BST的查找操作。
- 虽然红黑树的屁事规矩特征很多,但是也正是这些特性使得红黑树从根节点到叶子节点的最长路径不会超过最短路径的2倍
- 什么时候需要变色,什么时候需要旋转?
- 当破坏了红黑树的规则或者说上面的特性时,需要通过两种方法来调整保持这棵树始终是红黑树,这两种方法就是变色和旋转,旋转又分为左旋转【逆时针旋转红黑树的两个节点,使得父节点被自己的右孩子取代,而自己成为自己的左孩子。】和右旋转【顺时针旋转红黑树的两个节点,使得父节点被自己的左孩子取代,而自己成为自己的右孩子。】。
- 红黑树的常见操作:
- RBTree的旋转操作
- 左旋转【逆时针旋转红黑树的两个节点,使得父节点被自己的右孩子取代,而自己成为自己的左孩子。】
- 右旋转【顺时针旋转红黑树的两个节点,使得父节点被自己的左孩子取代,而自己成为自己的右孩子。】
- 左旋转【逆时针旋转红黑树的两个节点,使得父节点被自己的右孩子取代,而自己成为自己的左孩子。】
- RBTree的查找操作:RBTree的查找操作和BST的查找操作是一样的
- RBTree的插入操作:
- RBTree的插入与BST的插入方式是一致的,只不过是在插入过后,可能会导致树的不平衡,
这时就需要对RBTree树进行旋转操作和颜色修复(在这里简称插入修复)
,使得它符合RBTree的定义。新插入的节点是红色的
,插入修复操作如果遇到父节点的颜色为黑则修复操作结束。也就是说,只有在父节点为红色节点的时候是需要插入修复操作的
。
- 插入修复操作分为以下的三种情况,而且新插入的节点的父节点都是红色的:
- 叔叔节点也为红色:
- 叔叔节点为空,且祖父节点、父节点和新节点处于一条斜线上。
- 叔叔节点为空,且祖父节点、父节点和新节点不处于一条斜线上:
- 叔叔节点也为红色:
- RBTree的插入与BST的插入方式是一致的,只不过是在插入过后,可能会导致树的不平衡,
- RBTree的删除操作:
- RBTree的旋转操作
- 红黑树的应用场景:
- 红黑树的应用有很多,其中JDK的集合类TreeMap和TreeSet底层就是红黑树实现的。在Java8中,连HashMap也用到了红黑树。
- RBTree在理论上还是一棵BST树,但是它在对BST的插入和删除操作时会维持树的平衡,即保证树的高度在[logN,logN+1](理论上,极端的情况下可以出现RBTree的高度达到2*logN,但实际上很难遇到)。这样RBTree的查找时间复杂度始终保持在O(logN)从而接近于理想的BST。RBTree的删除和插入操作的时间复杂度也是O(logN)。RBTree的查找操作就是BST的查找操作。
-
一些技巧:
- 用链表实现就是常见的树
- 有时候题目会要求咱们写出来的
算法的时间复杂度为O(nlogN)
,此时咱们就要能反应出来能搞出对数级别的时间复杂度的数据结构就那几个呀:- 二分搜索
- 二叉树相关的数据结构,TreeMap,PriorityQueue…
- 遇到求最值相关的题目咱们的解法大方向就是动态规划之类的去迭代求解,但是算法或者说函数中具体用的数据结构就是:
- 二叉堆。二叉堆实现的优先级队列取最值的时间复杂度为O(logN),但是二叉堆实现的优先级队列默认的顶上元素是最大值,所以一般咱们用来处理或者说删除最大值等
- 平衡搜索二叉树。平衡二叉搜索树也可以求最值,也可以实现修改或者删除一个值,时间复杂度都是O(logN)
- 二叉查找树或者叫二叉搜索树:
- B+Tree是由平衡二叉树演变而来,而平衡二叉树是由二叉查找树演化而来,所以说B树肯定是一颗平衡二叉树和二叉搜索树,这里有包含关系呢;而平衡二叉树也是一颗二叉查找树
-
二叉树的存储:
- 链式存储:和链表类似,二叉树的链式存储依靠指针将各个节点串联起来,不需要连续的存储空间。
- 顺序存储:
- 顺序存储就是利用数组进行存储,数组中的每一个位置仅存储节点的 data,不存储左右子节点的指针,子节点的索引通过数组下标完成。
根结点的序号为 1,对于每个节点 Node,假设它存储在数组中下标为 i 的位置,那么它的左子节点就存储在 2i 的位置,它的右子节点存储在下标为 2i+1 的位置。【如果我们要存储的二叉树不是完全二叉树,在数组中就会出现空隙,导致内存利用率降低】
下来就是咱们的常见树题型喽:
- 顺序存储就是利用数组进行存储,数组中的每一个位置仅存储节点的 data,不存储左右子节点的指针,子节点的索引通过数组下标完成。
- 链式存储:和链表类似,二叉树的链式存储依靠指针将各个节点串联起来,不需要连续的存储空间。
-
二叉树(多叉树以及N叉树等)的题目常见的思路有:
- 递归(
我先搞清楚当前根节点应该做什么,然后让子节点去模仿根节点进行前中后序的遍历就行,不断迭代
)。而递归解法大概可以分为两类思路- 遍历一遍二叉树就能得出答案(这也是回溯算法核心,回溯算法感觉本质上就是暴力遍历或者叫暴力穷举嘛)。如果题目中要求了前中后序中某一个,那这个图片三句口诀正好,如果题目中没要求,那就满地找xxx(root.left, …)…xxx(root.right, …);
- 通过分解问题计算答案,感觉是不是有点像分治算法,或者归并排序(要对一个大东西排序,我先把这个大东西分为两半,然后先对左一半排好序,然后再对又一半排好序,最后把排好序的两半合并起来)。(所以说,有位大佬也说过,所有的回溯、动归、分治都是树的问题)
- 遍历一遍二叉树就能得出答案(这也是回溯算法核心,回溯算法感觉本质上就是暴力遍历或者叫暴力穷举嘛)。如果题目中要求了前中后序中某一个,那这个图片三句口诀正好,如果题目中没要求,那就满地找xxx(root.left, …)…xxx(root.right, …);
- BFS层级遍历
- 有时候也叫迭代方式,其实和咱们下面前中后序遍历一样,核心都是封装好一个函数呗,以备不时之调。
- 递归(
public void traverse(TreeNode root){
......
//先将root加入队列中
Queue<TreeNode> queue = new LinkedList<>();
queue.offer(root);//如果在不违反容量限制的情况下立即执行,则将指定的元素插入到此队列中。
//咱们一般碰到二叉树的最大深度、最小深度、层序遍历结果等问题,会想着构造一个depth或者size或者level等,来记录了当前遍历到的层数
int depth = 1;//或者叫int level = 1,都行,root这一层肯定就算是第一层了呗,所以这个代表层数的变量初始化为1
//while循环嵌套一个for循环,意思就是,while循环从root向最后一层走,指明了前进的方向,for循环利用queue.size()为限制下的i变量从左向右移动移动,指明了横溢的方向
while(!queue.isRmpty()){
......
for(int i = 0; i < queue.size(); i++){
//对左右节点逐个击破
if(queue.poll().left != null){//queue.poll()意思是检索并删除此队列的头,如果此队列为空,则返回 null 。
queue.offer(queue.poll().left);
}
if(queue.poll().right != null){
queue.offer(queue.poll().right);
}
}
depth++;
}
}
上点干活:
二叉树的两个重型武器:
- 二叉树的前序遍历、中序遍历、后序遍历(封装一个咱们自己的工具类,以备不时之调,这句话是不是有点熟悉呀,咱们在学中间件或者访问数据库等等,常会来一个XxxUtils,这里也是一个思想,谁知道咱们这个函数实现的功能在其他哪个地方就用上了呢)
咱们这个函数的功能就是,把以(第一个参数)root为根节点的树按照前序或者中序或者后序的遍历顺序给塞进(第二个参数)传进来的单列集合中
- 那到底,在程序中前序、中序、后序该怎样体现呢?以下面这个题为例,把里面三句话记住就行了
开一题:
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode() {}
* TreeNode(int val) { this.val = val; }
* TreeNode(int val, TreeNode left, TreeNode right) {
* this.val = val;
* this.left = left;
* this.right = right;
* }
* }
*/
class Solution {
//时间复杂度为O(n),n是二叉树节点的个数,每个节点会被访问一次且只会被访问一次;空间复杂度w取决于递归的深度,最坏情况下一条叉可以到O(n)
public List<Integer> inorderTraversal(TreeNode root) {
List<Integer> resultList = new ArrayList<>();
inorder(root, resultList);
return resultList;
}
//一般涉及到树的遍历,咱们就封装一个inorder呀、preorder呀、aferorder呀等三个前中后序方法,以后不同的题目能复用呀,多好。
//咱们这个函数的功能就是,把以(第一个参数)root为根节点的树按照遍历顺序给塞进(第二个参数)传进来的单列集合中
public void inorder(TreeNode root, List<Integer> list){
//官话判空
if(root == null){
return;
}
//inorder(root, ...)表示当前遍历到root节点,那么题目要求中序遍历的话就是左->根->右,所以就是先递归调用inorder(root.left)来遍历root节点的zuo'zi'shu左子树,然后将root的节点的值加入答案,最后再递归调用inorder(root.right, ...)来遍历root节点的右子树。最终写上递归终止条件w也就是碰到空节点停止递归就行
inorder(root.left, list);
list.add(root.val);
inorder(root.right, list);
}
}
那这两道不就把咱们封装的函数改一下,再改一下调用那块就行了。这难道就是传说中的举一反三,哦,天呐
- 二叉树的层级遍历、最大深度、最小深度、节点个数、最短路径等(放到一块是因为如果我能够从上到下,从左到右把树遍历一边,那么我搞一个变量或者单列集合或者双列集合可以记录一下节点或者路径的相关信息,就有解题思路了)
开二题:二叉树的层序遍历
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode() {}
* TreeNode(int val) { this.val = val; }
* TreeNode(int val, TreeNode left, TreeNode right) {
* this.val = val;
* this.left = left;
* this.right = right;
* }
* }
*/
class Solution {
public List<List<Integer>> levelOrder(TreeNode root) {
List<List<Integer>> resultList = new ArrayList<>();
Queue<TreeNode> queue = new LinkedList<>();
if(root != null){
queue.add(root);
}
int depth = 1;
while(!queue.isEmpty()){
List<Integer> level = new ArrayList<>();
int n = queue.size();
for(int i = 0; i < n; i++){
TreeNode tempNode = queue.poll();
level.add(tempNode.val);
if(tempNode.left != null){
queue.add(tempNode.left);
}
if(tempNode.right != null){
queue.add(tempNode.right);
}
}
resultList.add(level);
depth++;
}
return resultList;
}
}
已经用上咱们的重型武器喽
开三题:找二叉树的最大深度(最大深度,也就是在求二叉树的深度,因为二叉树的深度就是从根节点到叶节点依次经过的节点(含根、叶节点)形成树的一条路径,最长路径的长度为树的深度)
//比如计算二叉树最大深度这个问题让你实现maxDepth这个函数,
//方法一:用BFS层级遍历的方式解决
//记录最大深度
int res = 0;
int depth = 0;
//主函数
int maxDepth(TreeNode root){
traverse(root);
return res;
}
//二叉树遍历框架
void traverse(TreeNode root){
if(root == null){
//到达叶子节点
res = Math.max(res, depth);
return;
}
//三角大回环,咱们细看一下这个遍历过程。刚开始depth == 1对吧,相当于是从root节点开始的,
//然后先走到左节点,那深度明显加一了所以depth相应++,然后该从左节点走到右节点了,此时层数是没变的,然后
//然后会再上去继续从根节点开始遍历,那不就相当于又来了个后序遍历,深度自然又减一了嘛,那就得再来一个depth--
//前序遍历位置
depth++;
traverse(root.left);
traverse(root.right);
//后序遍历位置
depth--;
}
//方法二:用递归解决
//时间复杂度:O(n),其中n为二叉树的节点数,遍历整棵二叉树
//空间复杂度:O(n),最坏情况下,二叉树化为链表,递归栈深度最大为n
public int maxDepth(TreeNode root){
if(root == null){
return 0;
}
//递归计算左右子树的最大深度(让左右子节点模仿根节点root,做相同的事),当前深度为两个子树深度较大值再加1。
int leftMax = maxDepth(root.left);
int right = maxDepth(root.right);
//整棵树的最大深度
int result = Math.max(leftMax, rightMax) + 1;
/**
* 或者像下面这样写递归也行.当前深度为两个子树深度较大值再加1。
*/
return Math.max(maxDepth(root.left), maxDepth(root,right)) + 1;
return result;
}
//方法三:这种方法可以和最小深度那个解法统一起来,很好记忆,最小深度就是在for循环中比这个多了一个判断,其他的差不多
//时间复杂度:O(n),其中n为二叉树的节点数,遍历整棵二叉树
//空间复杂度:O(n),辅助队列的空间最坏为n
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode(int x) { val = x; }
* }
*/
class Solution {
public int maxDepth(TreeNode root) {
if(root == null){
return 0;
}
int depth = 0;
//既然是层次遍历,我们遍历完一层要怎么进入下一层,可以用队列记录这一层中节点的子节点。队列类似栈,只不过是一个先进先出的数据结构,可以理解为我们平时的食堂打饭的排队。因为每层都是按照从左到右开始访问的,那自然记录的子节点也是从左到右,那我们从队列出来的时候也是从左到右,完美契合。
Queue<TreeNode> queue = new LinkedList<>();
if(root != null){
queue.offer(root);
}
while(!queue.isEmpty()){
int size = queue.size();
for(int i = 0; i < size; i++){
TreeNode tempNode = queue.poll();
//在刚刚进入某一层的时候,队列中的元素个数就是当前层的节点数。比如第一层,根节点先入队,队列中只有一个节点,对应第一层只有一个节点,第一层访问结束后,它的子节点刚好都加入了队列,此时队列中的元素个数就是下一层的节点数。
if(tempNode.left != null){
queue.offer(tempNode.left);
}
if(tempNode.right != null){
queue.offer(tempNode.right);
}
}
//因此遍历的时候,每层开始统计该层个数,然后遍历相应节点数,精准进入下一层。遍历完一层就可以节点深度就可以加1,直到遍历结束,即可得到最大深度
depth++;
}
return depth;
}
}
已经用上咱们的重型武器喽
那最小深度不就和这个差不多嘛
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode() {}
* TreeNode(int val) { this.val = val; }
* TreeNode(int val, TreeNode left, TreeNode right) {
* this.val = val;
* this.left = left;
* this.right = right;
* }
* }
*/
class Solution {
//计算从起点start到终点target的最近距离,(输入一棵二叉树的根节点,层序遍历这棵树)
public int minDepth(TreeNode root) {
int depth = 0;
if(root == null){//官话判空
return 0;
}
Queue<TreeNode> queue = new LinkedList<>();//老规矩,利用队列实现一个BFS层序遍历
queue.offer(root);//将起点root加入队列
从上到下遍历二叉树的每一层
while(!queue.isEmpty()){
int size = queue.size();
depth++;
/**
*将当前队列中的所有节点向四周扩散,从左到右遍历每一层的每个节点
*/
for(int i = 0; i < size; i++){
TreeNode tempNode = queue.poll();//每次queue都会存这二叉树一整层的节点
if(tempNode.left == null && tempNode.right == null){
return depth;///咱们要找的终点target就是最靠近根节点的那个叶子结点,说明已经到达叶子结点(那起点肯定就是咱们的根节点root喽)
}
//将queue.poll()的相邻节点加入到queue中
if(tempNode.left != null){
queue.offer(tempNode.left);
}
if(tempNode.right != null){
queue.offer(tempNode.right);
}
}
}
return 0;
}
}
//下面这个depth = 1也是亲测有效的哦:
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode() {}
* TreeNode(int val) { this.val = val; }
* TreeNode(int val, TreeNode left, TreeNode right) {
* this.val = val;
* this.left = left;
* this.right = right;
* }
* }
*/
class Solution {
public int minDepth(TreeNode root) {
int depth = 1;
if(root == null){
return 0;
}
Queue<TreeNode> queue = new LinkedList<>();
queue.offer(root);
while(!queue.isEmpty()){
int size = queue.size();
for(int i = 0; i < size; i++){
TreeNode tempNode = queue.poll();
if(tempNode.left == null && tempNode.right == null){
return depth;
}
if(tempNode.left != null){
queue.offer(tempNode.left);
}
if(tempNode.right != null){
queue.offer(tempNode.right);
}
}
depth++;
}
return depth;
}
}
开四题:求以root为根节点的二叉树的最大值
//求以root为根节点的二叉树的最大值
int maxVal(TreeNode root){
if(root == null){
return -1;
}
int left = maxVal(root.left);
int right = maxVal(root.right);
return max(root.val, left, right);
}
开五题:求树的每一层的最大值将最大值们放在一个列表中返回回来
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode() {}
* TreeNode(int val) { this.val = val; }
* TreeNode(int val, TreeNode left, TreeNode right) {
* this.val = val;
* this.left = left;
* this.right = right;
* }
* }
*/
class Solution {
//要求我们获取每一层的最大值,不正好满足队列的出队入队操作嘛
public List<Integer> largestValues(TreeNode root) {
Queue<TreeNode> queue = new LinkedList<>();
if(root != null){
queue.offer(root);
}
List<Integer> result = new LinkedList<>();
while(!queue.isEmpty()){//root到最底层,一层一层遍历
int num = Integer.MIN_VALUE;//初始化num变量为最小值
int queueSize = queue.size();
//while你从上到下,那我for循环就从每一层的左边到右边
for(int i = 0; i < queueSize; i++){
TreeNode tempNode = queue.poll();
num = Math.max(num, tempNode.val);
//根据每个出队的节点,判断此节点的左右子树是否存在,若存在则执行入队操作
if(tempNode.left != null){
queue.offer(tempNode.left);
}
if(tempNode.right != null){
queue.offer(tempNode.right);
}
}
result.add(num);
}
return result;
}
}
开六题:求一颗二叉树上有多少个节点呀、求一颗完全二叉树有多少节点呀、求一颗满二叉树的节点个数呀
//求一颗二叉树上的节点个数
//定义:count(root)返回以root为根节点的树有多少节点
//时间复杂度为O(N)
int count(TreeNode root){
//base case,官话判空
if(root == null){
return 0;
}
//给自己加上子树(左子树和右子树)的节点数就是整棵树的节点数
return 1 + count(root.left) + count(root.right);
}
//求完全二叉树的节点个数:
public int countNodes(TreeNode root){
TreeNode l = root;
TreeNode r = root;
//用两个变量记录左、右子树的高度
int hl = 0;
int hr = 0;
while(l != null){
l = l.left;
hl++;
}
while(r != null){
r = r.right;
hl++;
}
//如果左右子树的高度相同,则是一颗满二叉树
if(hl == hr){
return (int)Math.pow(2, hl) - 1;
}
//如果左右高度不一样,则按照普通二叉树的逻辑计算,向左右子树递归
return 1 + countNodes(root.left) + countNodes(root.right);
}
//求满二叉树的节点个数
//节点总数和树的高度呈现指数关系,时间复杂度为O(logN)
public int countNodes(TreeNode root){
int h = 0;
//计算树的高度
if(root != null){
root = root.left;
h++;
}
//节点总数就是2^h-1
return (int)Math.pow(2, h) - 1;
}
PART2:图
- 图
- 基本概念:比如顶点、边、度、无向图&有向图、无权图&有权图、点这里也能看
- 图的存储:
- 邻接矩阵存储:邻接矩阵将图用二维矩阵存储,如果第i个顶点和第j个顶点之间有关系,且关系权值为n,则 A[i][j]=n
- 无向图的邻接矩阵存储:
在无向图中,我们只关心关系的有无,所以当顶点i和顶点j有关系时,A[i][j]=1,当顶点i和顶点j没有关系时,A[i][j]=0
。【无向图的邻接矩阵是一个对称矩阵,因为在无向图中,顶点i和顶点j有关系,则顶点j和顶点i必有关系。】
- 有向图的邻接矩阵存储:
- 邻接矩阵存储的方式优点是简单直接(直接使用一个二维数组即可),并且,在获取两个定点之间的关系的时候也非常高效(直接获取指定位置的数组元素的值即可)。但是,这种存储方式的缺点也比较明显,那就是比较浪费空间
- 无向图的邻接矩阵存储:
- 邻接表存储:针对邻接矩阵比较浪费内存空间的问题,诞生了图的另外一种存储方法—邻接表
。邻接链表使用一个链表来存储某个顶点的所有后继相邻顶点。对于图中每个顶点Vi,把所有邻接于Vi的顶点Vj链成一个单链表,这个单链表称为顶点Vi的 邻接表
。- 无向图的邻接表存储:
- 有向图的邻接表存储:
- 在无向图中,邻接表元素个数等于边的条数的两倍,如上面所示的无向图中,边的条数为7,邻接表存储的元素个数为14。
在有向图中,邻接表元素个数等于边的条数
,如上面所示的有向图中,边的条数为8,邻接表存储的元素个数为8
- 无向图的邻接表存储:
- 邻接矩阵存储:邻接矩阵将图用二维矩阵存储,如果第i个顶点和第j个顶点之间有关系,且关系权值为n,则 A[i][j]=n
- 图的搜素:
- 回忆一下,二叉树的BFS和DFS的过程如下:
- BFS
- DFS
- BFS
- 广度优先搜索(BFS)
- 广度优先搜索的具体实现方式用到了线性数据结构——队列
- 深度优先搜索(DFS)
深度优先搜索就是“一条路走到黑”,从源顶点开始,一直走到没有后继节点,才回溯到上一顶点,然后继续“一条路走到黑”
- 回忆一下,二叉树的BFS和DFS的过程如下:
PART3:Heap
- 堆:
当我们只关心所有数据中的最大值或者最小值,存在多次获取最大值或者最小值,多次插入或删除数据时,就可以使用堆
。相对于有序数组而言,堆的主要优势在于更新数据效率较高
。 堆的初始化时间复杂度为 O(nlog(n)),堆可以做到O(1)时间复杂度取出最大值或者最小值,O(log(n))时间复杂度插入或者删除数据
- 初始化一个有序数组时间复杂度是 O(nlog(n)),查找最大值或者最小值时间复杂度都是 O(1),但是,
涉及到更新(插入或删除)数据时,时间复杂度为 O(n),即使是使用复杂度为 O(log(n)) 的二分法找到要插入或者删除的数据,在移动数据时也需要 O(n) 的时间复杂度
- 初始化一个有序数组时间复杂度是 O(nlog(n)),查找最大值或者最小值时间复杂度都是 O(1),但是,
堆中的每一个节点值都大于等于(或小于等于)子树中所有节点的值
。或者说,任意一个节点的值都大于等于(或小于等于)所有子节点的值
- 为了方便存储和索引,我们通常用完全二叉树的形式来表示堆,事实上,广为人知的斐波那契堆和二项堆就不是完全二叉树,它们甚至都不是二叉树。
- (二叉)堆是一个数组,它可以被看成是一个 近似的完全二叉树
- 最大堆与最小堆:
- 最大堆:堆中的每一个节点的值都大于等于子树中所有节点的值
- 最小堆:堆中的每一个节点的值都小于等于子树中所有节点的值
- 堆的存储:
- 为了方便存储和索引,(二叉)堆可以用完全二叉树的形式进行存储。
- 【由于完全二叉树的优秀性质,
利用数组存储二叉树即节省空间,又方便索引(若根结点的序号为1,那么对于树中任意节点i,其左子节点序号为 2*i,右子节点序号为 2*i+1)
】
- 【由于完全二叉树的优秀性质,
- 为了方便存储和索引,(二叉)堆可以用完全二叉树的形式进行存储。
- 堆的常见操作:
- 插入删除元素:以最大堆为例
- 插入元素时,
先将要插入的元素放到最后
【作为一个新入职的员工,初来乍到,这个员工需要从基层做起】。然后从底向上,如果父结点比该元素大,则该节点和父结点交换,直到无法交换
- 删除堆顶元素:根据堆的性质可知,最大堆的堆顶元素为所有元素中最大的,最小堆的堆顶元素是所有元素中最小的。
当我们需要多次查找最大元素或者最小元素的时候,可以利用堆来实现
- 删除堆顶元素后,为了保持堆的性质,需要对堆的结构进行调整,我们将这个过程称之为
"堆化"
,堆化的方法分为两种:- 一种是自底向上的堆化,上面的插入元素所使用的就是自底向上的堆化,元素从最底部向上移动。【比如在堆这个公司中,会出现老大离职的现象,老大离职之后,他的位置就空出来了】【但是自底向上的堆化中,我们可以看到数组中出现了“气泡”或者说空缺,这会导致存储空间的浪费。】
- 首先删除堆顶元素,使得数组中下标为1的位置空出。
- 比较根结点的左子节点和右子节点,也就是下标为2,3的数组元素,将较大的元素填充到根结点(下标为1)的位置。【他的位置当然是他的直接下属来替代了,谁能力强就让谁上呗】
- 一直循环比较空出位置的左右子节点,并将较大者移至空位,直到堆的最底部【这个时候又空出一个位置了,老规矩,谁有能力谁上】
- 首先删除堆顶元素,使得数组中下标为1的位置空出。
- 另一种是自顶向下堆化,元素由最顶部向下移动。
- 自顶向下的堆化第一件事情,就是把石头抬起来,从海面扔下去。这个石头就是堆的最后一个元素,
我们将最后一个元素移动到堆顶
- 然后开始将这个石头沉入海底,不停与左右子节点的值进行比较,和较大的子节点交换位置,直到无法交换位置。
- 自顶向下的堆化第一件事情,就是把石头抬起来,从海面扔下去。这个石头就是堆的最后一个元素,
- 一种是自底向上的堆化,上面的插入元素所使用的就是自底向上的堆化,元素从最底部向上移动。【比如在堆这个公司中,会出现老大离职的现象,老大离职之后,他的位置就空出来了】【但是自底向上的堆化中,我们可以看到数组中出现了“气泡”或者说空缺,这会导致存储空间的浪费。】
- 删除堆顶元素后,为了保持堆的性质,需要对堆的结构进行调整,我们将这个过程称之为
- 插入元素时,
- 堆排序:
- 堆排序的过程分为两步:
- 第一步是
建堆,将一个无序的数组建立为一个堆
,以最大堆为例- 建堆的过程就是一个对所有非叶节点的自顶向下堆化过程。也就是说,如果节点个数为n,那么我们需要对n/2到1的节点进行自顶向下(沉底)堆化。
- 将初始的无序数组抽象为一棵树,图中的节点个数为6,所以4,5,6节点为叶节点,1,2,3节点为非叶节点,所以要对1-3号非叶子节点进行自顶向下(沉底)堆化,注意,顺序是从后往前堆化,从3号节点开始,一直到1号节点。
- 3号节点,节点“3”堆化结果:
- 2号节点,节点“7”堆化结果:
- 1号节点,节点“19”堆化结果:
- 3号节点,节点“3”堆化结果:
- 建堆的过程就是一个对所有非叶节点的自顶向下堆化过程。也就是说,如果节点个数为n,那么我们需要对n/2到1的节点进行自顶向下(沉底)堆化。
- 第二步是排序,
将堆顶元素取出,然后对剩下的元素进行堆化,反复迭代,直到所有元素被取出为止
- 由于堆顶元素是所有元素中最大的,所以我们重复取出堆顶元素,将这个最大的堆顶元素放至数组末尾,并对剩下的元素进行堆化即可。
- 这其实是做了一次交换操作,将堆顶和末尾元素调换位置,从而将取出堆顶元素和堆化的第一步(将末尾元素放至根结点位置)进行合并。
- 取出第一个元素并堆化:
- 取出第二个元素并堆化:
- 取出第三个元素并堆化:
- 取出第四个元素并堆化:
- 取出第五个元素并堆化:
- 取出第六个元素并堆化:
- 取出第一个元素并堆化:
- 第一步是
- 堆排序的过程分为两步:
- 插入删除元素:以最大堆为例
巨人的肩膀:
B站各位数据结构与算法老师的课
算法导论
javaGuide
CS-Notes
维基百科
美团技术博客