二叉树和二叉搜索树(binary_tree)
JAVA 版本
1 树结构
在本地编辑器写代码需要注意输入输出处理 创建一个二叉树 并输出二叉树
1.1 树结构code
// 树结构
public class TreeNode{
int val;
TreeNode left;
TreeNode right;
TreeNode(int x){
val = x;
}
}
1.2 构建二叉树
构建二叉树有不仅这一种方法
public TreeNode createBinaryTree(LinkedList<Integer> list) {
TreeNode node = null;
if(list==null || list.isEmpty()) {
return null;
}
Integer data = list.removeFirst();
if(data != null) {
node = new TreeNode(data);
System.out.println("创建节点:"+node.val);
node.left = createBinaryTree(list);
node.right = createBinaryTree(list);
}
return node;
}
2 树的遍历(DFS)
前序遍历:根左右
中序遍历:左根右
后序遍历:左右根
2.1 (lee-144) 二叉树的前序遍历
递归
public void preOrder(TreeNode root) {
if(root==null) {
return;
}
System.out.print(root.val);
preOrder(root.left);
preOrder(root.right);
}
迭代
若使用栈对二叉树进行先序遍历,我们已知先序遍历是先访问自身,然后访问左子树,最后访问右子树,因此,当一个结点被访问后,我们可将该结点的右子树的根结点先入栈,后将左子树的根结点入栈,最后在出栈时,会先访问左子树,后访问右子树。
public List<Integer> preOrderTravel(TreeNode root){
if(root==null) {
return new LinkedList<>();
}
List<Integer> res = new LinkedList<>();
Deque<TreeNode> stack = new LinkedList<>(); //使用Deque模拟栈
stack.addFirst(root); //根节点先入栈
while(!stack.isEmpty()) {
TreeNode node = stack.removeFirst();
res.add(node.val);
if(node.right != null) {
stack.addFirst(node.right);
}
if(node.left != null) {
stack.addFirst(node.left);
}
}
System.out.print(res);
return res;
}
2.2 (lee-94) 二叉树的中序遍历
递归
public void midOrder(TreeNode root) {
if(root==null) {
return;
}
midOrder(root.left);
//在递归中间写操作
System.out.print(root.val);
midOrder(root.right);
}
迭代
若使用栈中序遍历二叉树,我们需要将树中最左边的结点入栈,因此需要先通过循环找到树中最左边的结点入栈,出栈时先访问该结点,若该结点有右子树,继续寻找右子树中的最左边的结点,重复上述步骤即可。
public List<Integer> midOrderTravel(TreeNode root){
if(root==null) {
return new LinkedList<Integer>();
}
List<Integer> res = new LinkedList<Integer>();
Deque<TreeNode> stack = new LinkedList<>();
TreeNode node = root;
while(node !=null || !stack.isEmpty()) {
while(node != null) {
stack.addLast(node);
node = node.left;
}
node = stack.removeLast();
res.add(node.val);
node = node.right;
}
System.out.print(res);
return res;
}
2.3 (lee-145) 二叉树的后序遍历
递归
public void beOrder(TreeNode root) {
if(root==null) {
return;
}
beOrder(root.left);
beOrder(root.right);
//在递归后面写操作
System.out.print(root.val);
}
迭代
若使用栈后序遍历二叉树,我们观察后序遍历的结果发现,后序跟前序非递归有些类似,不过后序是先访问跟节点,然后左子节点,再访问右子节点,依次压入栈; 从栈里取出时,是后序遍历的反序,需要翻转。这里利用Deque的性质,可每次插入到链头,相当于翻转。
public List<Integer> beOrderTravel(TreeNode root){
if (root == null) {
return new LinkedList<>();
}
LinkedList<Integer> res = new LinkedList<>();
Deque<TreeNode> stack = new LinkedList<>();
stack.addFirst(root);
while(!stack.isEmpty()) {
TreeNode node = stack.removeFirst();
res.addFirst(node.val);
if (node.left != null) {
stack.addFirst(node.left);
}
if (node.right != null) {
stack.addFirst(node.right);
}
}
System.out.print(res);
return res;
}
2.4 DFS深度搜索
从上到下
public List<Integer> dfsUpToDown(TreeNode root){
List<Integer> res = new LinkedList<Integer>();
dfs(root,res);
System.out.print(res.toString());
return res;
}
private void dfs(TreeNode node, List<Integer> res) {
if(node==null) {
return;
}
res.add(node.val);
dfs(node.left,res);
dfs(node.right,res);
}
从下到上(分治法)
public List<Integer> dfsDownToUp(TreeNode root){
return divideAndConquer(root);
}
private List<Integer> divideAndConquer(TreeNode node) {
List<Integer> res = new LinkedList<Integer>();
if(node==null) {
return null;
}
//分治
List<Integer> left = divideAndConquer(node.left);
List<Integer> right = divideAndConquer(node.right);
//合并结果
res.add(node.val);
System.out.print(node.val);
if(left != null) {
res.addAll(left);
}
if(right != null) {
res.addAll(right);
}
return res;
}
3 树的遍历(BFS)
public List<Integer> levelOrder(TreeNode root) {
if(root==null) {
return null;
}
List<Integer> res = new LinkedList<Integer>();
Queue<TreeNode> queue = new LinkedList<TreeNode>();
queue.offer(root);
while(!queue.isEmpty()) {
TreeNode node = queue.poll();
res.add(node.val);
System.out.print(node.val);
if(node.left != null) {
queue.offer(node.left);
}
if(node.right != null) {
queue.offer(node.right);
}
}
return res;
}
3.1(lee-102) 二叉树的层序遍历
/*
* (lee-102)题目:二叉树的层序遍历
* 给你一个二叉树,请你返回其按 层序遍历 得到的节点值。 (即逐层地,从左到右访问所有节点)。
* 思路:用一个队列记录一层的元素,然后扫描这一层元素添加下一层元素到队列(一个数进去出来一次,所以复杂度 O(logN))
*/
public static class Solution{
public List<List<Integer>> levelOrder(TreeNode root){
List<List<Integer>> res = new ArrayList<List<Integer>>();
if(root==null) {
return res;
}
Queue<TreeNode> queue = new LinkedList<TreeNode>();
queue.offer(root);
while(! queue.isEmpty()) {
int size = queue.size();
List<Integer> list = new ArrayList<Integer>();
for(int i = 0;i < size;i++) {
TreeNode node = queue.poll();
list.add(node.val);
if(node.left != null) {
queue.offer(node.left);
}
if(node.right != null) {
queue.offer(node.right);
}
}
res.add(list);
}
return res;
}
}
3.2(lee-107) 二叉树的层序遍历2
/*
* (lee-107)题目:二叉树的层序遍历2
* 给定一个二叉树,返回其节点值自底向上的层次遍历。 (即按从叶子节点所在层到根节点所在的层,逐层从左向右遍历)
* 思路:在层级遍历的基础上,翻转一下结果即可,这里利用LinkedList的性质,每次插入到首部,就相当于翻转了
*/
public static class Solution{
public List<List<Integer>> levelOrderBottom(TreeNode root){
List<List<Integer>> res = new LinkedList<>();
if(root==null) {
return res;
}
Queue<TreeNode> queue = new LinkedList<TreeNode>();
queue.offer(root);
while(!queue.isEmpty()) {
int size = queue.size();
List<Integer> list = new ArrayList<Integer>();
for(int i = 0;i <size;i++) {
TreeNode node = queue.poll();
list.add(node.val);
if(node.left != null) {
queue.offer(node.left);
}
if(node.right != null) {
queue.offer(node.right);
}
}
res.add(0, list);//每次插入到Index=0的位置,即首部,相当于反转。
}
return res;
}
}
3.3(lee-103) 二叉树的锯齿形层序遍历
/*
* (lee-103)题目:二叉树的锯齿形层序遍历
* 给定一个二叉树,返回其节点值的锯齿形层次遍历。Z字形遍历,即先从左往右,再从右往左进行下一层遍历,以此类推,层与层之间交替进行。
* 思路:在层级遍历的基础上,定义一个flag,当从右向左遍历时,实现翻转
*/
public static class Solution{
public List<List<Integer>> zigzagLevelOrder(TreeNode root){
List<List<Integer>> res = new ArrayList<>();
if(root==null) {
return res;
}
Queue<TreeNode> queue = new LinkedList<>();
queue.offer(root);
boolean flag = true;
while(!queue.isEmpty()) {
int size = queue.size();
List<Integer> list = new LinkedList<Integer>();
for(int i = 0;i < size;i++) {
TreeNode node = queue.poll();
if(!flag) { //如果flag为false,表示需要从右向左遍历,插入首部实现翻转
list.add(0,node.val);
}else {
list.add(node.val);
}
if(node.left!= null) {
queue.offer(node.left);
}
if(node.right!=null) {
queue.offer(node.right);
}
}
if(! flag) {
flag = true;
}else {
flag = false;
}
res.add(list);
}
return res;
}
}
3.4(NC-136) 输出二叉树的右视图
/*
* (NC-136)题目:输出二叉树的右视图
* 请根据二叉树的前序遍历,中序遍历恢复二叉树,并打印出二叉树的右视图
* 输入:[1,2,4,5,3],[4,2,5,1,3]
* 返回值:[1,3,5]
* 思路:先重建二叉树,再使用层序遍历打印二叉树的右视图
*/
public static class Solution{
public int[] solve (int[] xianxu, int[] zhongxu) {
TreeNode node = reConstructBinaryTree(xianxu, zhongxu);
return levelOrder(node);
}
//使用先序遍历和中序遍历重建二叉树
public TreeNode reConstructBinaryTree(int [] pre,int [] in) {
if(pre.length==0 || in.length==0) {
return null;
}
LevelOrderCase loc = new LevelOrderCase();
TreeNode root = loc.new TreeNode(pre[0]);
for(int i =0;i <in.length;i++) {
if(in[i]==pre[0]) {
root.left = reConstructBinaryTree(Arrays.copyOfRange(pre, 1, i+1), Arrays.copyOfRange(in, 0, i));
root.right = reConstructBinaryTree(Arrays.copyOfRange(pre, i+1, pre.length), Arrays.copyOfRange(in, i+1, in.length));
break;
}
}
return root;
}
//使用层序遍历输出二叉树的右视图
public int[] levelOrder(TreeNode root) {
if(root ==null) {
return null;
}
Queue<TreeNode> queue = new LinkedList<>();
queue.offer(root);
List<Integer> list = new ArrayList<>();
while(!queue.isEmpty()) {
int size = queue.size();
for(int i = 0;i < size;i++) {
TreeNode node = queue.poll();
if(i==size-1) { //注意
list.add(node.val);
}
if(node.left != null) {
queue.offer(node.left);
}
if(node.right != null) {
queue.offer(node.right);
}
}
}
int[] res = new int[list.size()];
for(int i = 0;i <res.length;i++) {
res[i] = list.get(i);
}
return res;
}
}
4 完全二叉树(CBT)
二叉树
1.每个结点关联的子结点至多有两个(可为0,1,2),即一个结点至多有两棵子树。
2.二叉树的两棵子树分别称作它的左子树和右子树,即:子树有左右之分(因此二叉树与树有不同结构,不是树的特殊情况)。
满二叉树
树中每个分支结点(非叶结点)都有两棵非空子树。
1
/ \
2 3
/ \
4 5
完全二叉树
对于一个树高为h的二叉树,如果其第0层至第h-1层的节点都满。
如果最下面一层节点不满,则所有的节点在左边的连续排列,空位都在右边。这样的二叉树就是一棵完全二叉树。
1
/ \
2 3
/
4
最优二叉树(哈夫曼树)
树的带权路径长度达到最小,称这样的二叉树为最优二叉树,也称为哈夫曼树(Huffman Tree)。
哈夫曼树是带权路径长度最短的树,权值较大的结点离根较近。
4.1 (NC-84) 完全二叉树节点数
给定一棵完全二叉树的头节点head,返回这棵树的节点个数。
如果完全二叉树的节点数为N,请实现时间复杂度低于O(N)的解法。
DFS + 分治
class TreeNode{
int val;
TreeNode left;
TreeNode right;
TreeNode(int x){
val = x;
}
}
/*
* 1.分治递归
* 思路: 完全二叉树的子树也是完全二叉树,完全二叉树的左右子树中至少有一颗是满二叉树;
* 计算一颗满二叉树节点个数为2^h - 1 , h为该满二叉树的高度。
*/
public int nodeNum(TreeNode head) {
if(head==null) {
return 0;
}
int rightHigh = complateTreeHigh(head.right);
int leftHigh = complateTreeHigh(head.left);
if(rightHigh == leftHigh) {
return (int) (Math.pow(2, leftHigh) + nodeNum(head.right));
}else {
return (int) (Math.pow(2, rightHigh) + nodeNum(head.left));
}
}
//计算完全二叉树的高度
private int complateTreeHigh(TreeNode root) {
int high = 0;
while(root != null) {
high++;
root = root.left;
}
return high;
}
BFS
// BFS
public int nodeNum1(TreeNode head) {
if(head==null) {
return 0;
}
int count = 0;
Queue<TreeNode> queue = new LinkedList<>();
queue.offer(head);
while(!queue.isEmpty()) {
int size = queue.size();
count += size;
for(int i = 0;i < size;i++) {
TreeNode cur = queue.poll();
if(cur.left != null) {
queue.offer(cur.left);
}
if(cur.right != null) {
queue.offer(cur.right);
}
}
}
return count;
}
}
5 二叉搜索树(BST)
二叉查找树,也称二叉搜索树,或二叉排序树。其定义也比较简单,要么是一颗空树,要么就是具有如下性质的二叉树:
(1) 若任意节点的左子树不空,则左子树上所有结点的值均小于它的根结点的值;
(2) 若任意节点的右子树不空,则右子树上所有结点的值均大于它的根结点的值;
(3) 任意节点的左、右子树也分别为二叉查找树;
(4) 没有键值相等的节点。
二叉查找树的操作
(1)查找:从根节点开始,比根节点大进入右支,比根节点小进入左支,依次类推,直到查找到目标值。
(2)插入:如果是空树,就首先生成根节点;不是空树就按照查找的算法,找到父节点,然后作为叶子节点插入,如果值已经存在就插入失败。
(3)删除:如果删除的是叶节点,可以直接删除;
如果被删除的元素有一个子节点,可以将子节点直接移到被删除元素的位置;
如果有两个子节点,这时候就采用中序遍历,找到待删除的节点的后继节点,将其与待删除的节点互换,此时待删除节点的位置已经是叶子节点,可以直接删除。
性质:对二叉查找树进行中序遍历,即可得到有序的数列。
复杂度:二叉查找树的查询复杂度和二分查找一样,插入和查找的时间复杂度均为O(logn);但是在最坏的情况下仍然会有O(n)的时间复杂度。原因在于插入和删除元素的时候,树没有保持平衡。
5.1(lee-98) 验证二叉搜索树
给定一个二叉树,判断其是否是一个有效的二叉搜索树。
- 每个节点中的值必须大于(或等于)存储在其左侧子树中的任何值。
- 每个节点中的值必须小于(或等于)存储在其右子树中的任何值。
思路1:非递归法,如果中序遍历得到的节点的值小于等于前一个 preVal,说明不是二叉搜索树
思路2:递归法,判断左 MAX < 根 < 右 MIN
(1)递归
//思路1:递归法,判断左 MAX < 根 < 右 MIN
public static class Solution{
public boolean isVailsBST(TreeNode root) {
Integer lower = null;
Integer upper = null;
return dfs(root,lower,upper);
}
private boolean dfs(TreeNode node, Integer lower, Integer upper) {
if(node==null) {
return true;
}
if((lower != null && node.val <= lower)||(upper != null && node.val >= upper)) {
return false;
}
return dfs(node.right, node.val, upper) && dfs(node.left, lower, node.val);
}
}
(2)迭代
public static class Solution{
public boolean isVaildBST(TreeNode root) {
Deque<TreeNode> stack = new LinkedList<>();
Double preval = -Double.MAX_VALUE;
TreeNode node = root;
while(node != null || !stack.isEmpty()) {
while( node != null) {
stack.addLast(node);
node = node.left;
}
node = stack.removeLast();
if(node.val <= preval) { // 如果中序遍历得到的节点的值小于等于前一个 preVal,说明不是二叉搜索树
return false;
}
preval = (double) node.val;
node = node.right;
}
return true;
}
}
5.2(lee-701) 二叉搜索树中的插入操作
/*
* (lee-70)题目:二叉搜索树中的插入操作
* 给定二叉搜索树(BST)的根节点和要插入树中的值,将值插入二叉搜索树。 返回插入后二叉搜索树的根节点。
* 思路:找到最后一个叶子节点满足插入条件 递归插入即可
*/
public static class Solution{
public TreeNode insertIntoBST(TreeNode root,int val) {
if(root==null) {
return null;
}
if(root.val < val) {
root.right = insertIntoBST(root.right, val);
}else {
root.left = insertIntoBST(root.left, val);
}
return root;
}
}
5.3(lee-450) 删除二叉搜索树中的节点
/**
* (lee-450) 删除二叉搜索树中的节点
* 给定一个二叉搜索树的根节点 root 和一个值 key,删除二叉搜索树中的 key 对应的节点,并保证二叉搜索树的性质不变。返回二叉搜索树(有可能被更新)的根节点的引用。
* 一般来说,删除节点可分为两个步骤:
* 首先找到需要删除的节点;
* 如果找到了,删除它。
*/
public class DeleteBST {
public class TreeNode {
int val;
TreeNode left;
TreeNode right;
TreeNode(int x) { val = x; }
}
public TreeNode deleteNode(TreeNode root, int key) {
if(root==null) {
return null;
}
if(key > root.val) { //要删除的节点在右子树
root.right = deleteNode(root.right, key);
}else if(key < root.val) { //要删除的节点在左子树
root.left = deleteNode(root.left, key);
}else { //删除当前节点
if(root.left==null && root.right==null) { //当前节点是叶子节点,直接删除
root = null;
}else if(root.right != null) { //右子树存在,删除后继节点,后继节点移动到当前位置
root.val = successor(root);
root.right = deleteNode(root.right, root.val);
}else { //左子树存在,删除前驱节点,前驱节点移动到当前位置
root.val = predecessor(root);
root.left = deleteNode(root.left, root.val);
}
}
return root;
}
//删除前驱节点
private int predecessor(TreeNode node) {
node = node.left;
while(node.right != null) {
node = node.right;
}
return node.val;
}
//删除后继节点
private int successor(TreeNode node) {
node = node.right;
while(node.left != null) {
node = node.left;
}
return node.val;
}
}
5.4 (lee-95) 不同的二叉搜索树2
给定一个整数 n,生成所有由 1 … n 为节点所组成的互不相同的二叉搜索树。
输入:n = 3
输出:[[1,null,2,null,3],[1,null,3,2],[2,1,3],[3,1,null,null,2],[3,2,null,1]]
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;
}
}
/*
* 递归、回溯
* 思路:生成根i,生成左子树[1,i),生成右子树(i,n],拼接
*/
public List<TreeNode> generateTrees(int n) {
if(n==0) {
return new LinkedList<>();
}else {
return allTrees(1,n);
}
}
private List<TreeNode> allTrees(int start, int end) {
List<TreeNode> res = new LinkedList<>();
if(start > end) {
res.add(null);
return res;
}
for(int i = start; i<= end;i++) { // 枚举可行根节点
List<TreeNode> left = allTrees(start, i-1); // 获得所有可行的左子树集合
List<TreeNode> right = allTrees(i+1, end); // 获得所有可行的右子树集合
for(TreeNode leftNode:left) { // 从左子树集合中选出一棵左子树,从右子树集合中选出一棵右子树,拼接到根节点上
for(TreeNode rightNode:right) {
TreeNode root = new TreeNode(i);
root.left = leftNode;
root.right = rightNode;
res.add(root);
}
}
}
return res;
}
5.5 (lee-96) 不同的二叉搜索树
给你一个整数 n ,求恰由 n 个节点组成且节点值从 1 到 n 互不相同的 二叉搜索树 有多少种?返回满足题意的二叉搜索树的种数。
输入: 3
输出: 5
(1) 回溯+递归(参考lee-95recursion)
/**
* 1.回溯+递归(参考lee-95recursion)
* 超出时间限制
* @param n
* @return
*/
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;
}
}
public int numTrees1(int n) {
if(n == 0) {
return 0;
}else {
return allTrees(1,n).size();
}
}
private List<TreeNode> allTrees(int start, int end) {
List<TreeNode> res = new LinkedList<>();
if(start > end) {
res.add(null);
return res;
}
for(int i = start; i<= end;i++) { // 枚举可行根节点
List<TreeNode> left = allTrees(start, i-1); // 获得所有可行的左子树集合
List<TreeNode> right = allTrees(i+1, end); // 获得所有可行的右子树集合
for(TreeNode leftNode:left) { // 从左子树集合中选出一棵左子树,从右子树集合中选出一棵右子树,拼接到根节点上
for(TreeNode rightNode:right) {
TreeNode root = new TreeNode(i);
root.left = leftNode;
root.right = rightNode;
res.add(root);
}
}
}
return res;
}
(2) 公式(卡塔兰数)
/**
* 公式(卡塔兰数)
* 时间复杂度 : O(n)
* 空间复杂度 :O(1)
*/
public int numTrees(int n) {
long c = 1;
for(int i = 0;i <n;i++) {
c = c * 2 * (2 * i + 1) /(i + 2);
}
return (int) c;
}
(3) DP
/**
* DP
* 时间复杂度 : O(n^2)
* 空间复杂度 :O(n)
*/
public int numTrees2(int n) {
int[] dp = new int[n+1];
dp[0] = 1;
dp[1] = 1;
for(int i = 2;i <= n;i++) {
for(int j = 1;j <= i;j++) {
dp[i] += dp[j-1] * dp[i-j];
}
}
return dp[n];
}
6 平衡二叉树(AVL)
平衡二叉搜索树,又被称为AVL树,由于普通的二叉查找树会容易失去”平衡“,极端情况下,二叉查找树会退化成线性的链表,会导致插入和查找的复杂度下降到 O(n)。
- 性质:它是一棵空树或它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树。因此,平衡二叉树是一棵高度平衡的二叉查找树。
要构建跟维系一棵平衡二叉树就比普通的二叉树要复杂的多。
(1) 插入:在构建一棵平衡二叉树的过程中,当有新的节点要插入时,检查是否因插入后而破坏了树的平衡,如果是,则需要做旋转去改变树的结构。当新节点插入后,有可能会有导致树不平衡,这时候就需要进行调整,而可能出现的情况就有4种,分别称作LL,LR,RL,RR。
LL:在原来平衡的二叉树上,在节点的左子树的左子树下,有新节点插入,导致节点的左右子树的高度差为2;
解决:一次旋转----只需要对节点进行右旋即可
LR:在原来平衡的二叉树上,在节点的左子树的右子树下,有新节点插入,导致节点的左右子树的高度差为2
解决:两次旋转----将左右进行第一次旋转,将左右先调整成左左,然后再对左左进行调整
RL:在原来平衡的二叉树上,在节点的右子树的左子树下,有新节点插入,导致节点的左右子树的高度差为2
解决:两次旋转----将右左进行第一次旋转,将右左先调整成右右,然后再对右右进行调整
RR:在原来平衡的二叉树上,在节点的右子树的右子树下,有新节点插入,导致节点的左右子树的高度差为2
解决:一次旋转----只需要对节点进行左旋即可
LR跟RL互为镜像,LL跟RR也互为镜像;
LL跟RR一样,只需要旋转一次就能把树调整平衡,而LR跟RL也一样,都要进行旋转两次才能把树调整平衡。
(2) 删除:删除了节点之后要维系二叉树的平衡,但是删除二叉树节点总结起来就两个判断:删除的是什么类型的节点?删除了节点之后是否导致失衡?
节点的类型有三种:1.叶子节点;2.只有左子树或只有右子树;3.既有左子树又有右子树。
针对这三种节点类型,处理删除节点后导致的失衡问题:
1)当删除的节点是叶子节点
则将节点删除,然后从父节点开始,判断是否失衡;
如果没有失衡,则再判断父节点的父节点是否失衡,直到根节点,此时到根节点还发现没有失衡,则说此时树是平衡的;
如果中间过程发现失衡,则判断属于哪种类型的失衡(LL,LR,RL,RR),然后进行调整。
(2)删除的节点只有左子树或只有右子树
这种情况其实就比删除叶子节点的步骤多一步,就是将节点删除;
然后把仅有一支的左子树或右子树替代原有结点的位置,后面的步骤就一样了;
从父节点开始,判断是否失衡,如果没有失衡,则再判断父节点的父节点是否失衡,直到根节点;
如果中间过程发现失衡,则根据失衡的类型进行调整。
(3)删除的节点既有左子树又有右子树
这种情况又比上面这种多一步,就是中序遍历;
找到待删除节点的前驱或者后驱都行,然后与待删除节点互换位置,然后把待删除的节点删掉;
后面的步骤也是一样,判断是否失衡,然后根据失衡类型进行调整。
时间复杂度
查询复杂度是 O(logN) —平衡二叉树是一棵高度平衡的二叉搜索树
插入复杂度是 O(1) —失衡的情况有4种,左左,左右,右左,右右,即一旦插入新节点导致失衡需要调整,最多也只要旋转2次
删除:删除节点时有可能因为失衡,导致需要从删除节点的父节点开始,不断的回溯到根节点,如果这棵平衡二叉树很高的话,那中间就要判断很多个节点。所以后来也出现了综合性能比其更好的树—红黑树。
6.1(lee-110) 平衡二叉树
/*
* (lee-110)题目:平衡二叉树
* 给定一个二叉树,判断它是否是高度平衡的二叉树
* 思路:分治法
* 左边平衡 && 右边平衡 && 左右两边高度 <= 1,
* 遍历一遍,如果有左右两边高度 > 1,则不是高度平衡的二叉树
*/
public static class Solution{
private boolean res = true;
public boolean isBalanced(TreeNode root) {
maxDepth(root);
return res;
}
private int maxDepth(TreeNode root) {
if(root==null) {
return 0;
}
int leftDepth = maxDepth(root.left);
int rightDepth = maxDepth(root.right);
if(Math.abs(leftDepth-rightDepth)>1) {
res = false;
}
return Math.max(leftDepth, rightDepth)+1;
}
}
5 分治法的应用
先分别处理局部,再合并结果
使用场景
- 快速排序
- 归并排序
- 二叉树相关
分治法的模板
- 递归返回条件
- 分段处理
- 合并结果
// 伪代码
public class Solution {
public ResultType traversal(TreeNode root) {
if (root == null) {
// do something and return
}
// Divide
ResultType left = traversal(root.left);
ResultType right = traversal(root.right);
// Conquer
ResultType result = Merge from left and right
return result;
}
}
5.1 分治法遍历二叉树
5.1.1(lee-104) 二叉树的最大深度
/*
* (lee-104)题目:二叉树的最大深度
* 给定一个二叉树,找出其最大深度
* 思路:分治法
*/
public static class Solution {
// 计算maxDepth(root.left),maxDepth(root.right)就是分治,
// Math.max(maxDepth(root.left), maxDepth(root.right)) + 1就是合并的过程
public int MaxDepth(TreeNode root) {
if(root==null) {
return 0;
}
return Math.max(MaxDepth(root.left), MaxDepth(root.right))+1;
}
}
5.1.2(lee-124) 二叉树的最大路径和
/*
* (lee-124)题目:二叉树的最大路径和
* 给定一个非空二叉树,返回其最大路径和
* 思路:分治法
* 输入:root = [-10,9,20,null,null,15,7] 输出:42
* 解释:最优路径是 15 -> 20 -> 7 ,路径和为 15 + 20 + 7 = 42
*/
public static class Solution{
private int maxSum = Integer.MIN_VALUE;
public int maxPathSum(TreeNode root) {
maxGain(root);
return maxSum;
}
private int maxGain(TreeNode node) {
if(node == null) {
return 0;
}
int leftGain = Math.max(maxGain(node.left), 0); // 递归计算左右子节点的最大贡献值 // 只有在最大贡献值大于 0 时,才会选取对应子节点
int rightGain = Math.max(maxGain(node.right), 0);
int curMaxSum = node.val + leftGain + rightGain;// 节点的最大路径和取决于该节点的值与该节点的左右子节点的最大贡献值
maxSum = Math.max(curMaxSum, maxSum); // 更新最大路径和
return node.val+Math.max(leftGain, rightGain); // 返回节点的最大贡献值
}
}
5.1.3(lee-236) 二叉树的最近公共祖先
/*
* (lee-236)题目:二叉树的最近公共祖先
* 给定一个二叉树, 找到该树中两个指定节点的最近公共祖先
* 思路:分治法
* 有左子树的公共祖先或者有右子树的公共祖先,就返回子树的祖先,否则返回根节点
*/
public static class Solution{
public TreeNode loweatCommonAncestor(TreeNode root,TreeNode p ,TreeNode q) {
if(root==null || root == p || root == q) {
return root;
}
TreeNode left = loweatCommonAncestor(root.left, p, q);
TreeNode right = loweatCommonAncestor(root.right, p, q);
if(left == null) {
return right;
}else if(right ==null){
return left;
}else {
return root;
}
}
}
5.1.4(off-07) 重建二叉树
/*
* (offer-07)题目:重建二叉树
* 输入某二叉树的前序遍历和中序遍历的结果,请重建出该二叉树。
* 假设输入的前序遍历和中序遍历的结果中都不含重复的数字。
* 例如输入前序遍历序列{1,2,4,7,3,5,6,8}和中序遍历序列{4,7,2,1,5,3,8,6},则重建二叉树并返回{1,2,5,3,4,6,7}。
*/
public static class Solution5{
public TreeNode reConstructBinaryTree(int [] pre,int [] in) {
if(pre.length==0 || in.length==0) {
return null;
}
DivideMergeCase dmc = new DivideMergeCase();
TreeNode root = dmc.new TreeNode(pre[0]);
for(int i =0;i <in.length;i++) {
if(in[i]==pre[0]) {
root.left = reConstructBinaryTree(Arrays.copyOfRange(pre, 1, i+1), Arrays.copyOfRange(in, 0, i));
root.right = reConstructBinaryTree(Arrays.copyOfRange(pre, i+1, pre.length), Arrays.copyOfRange(in, i+1, in.length));
break;
}
}
return root;
}
}
5.1.5 (lee-114) 二叉树展开为链表
给定一个二叉树,原地将它展开为一个单链表。
输入: [1,2,5,3,4,null,6]
输出:[1,null,2,null,3,null,4,null,5,null,6]
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;
}
}
(1) 前序遍历–递归
/**
* 1.前序遍历--递归
* 思路:将二叉树展开为单链表之后,单链表中的节点顺序即为二叉树的前序遍历访问各节点的顺序。
* 因此,可以对二叉树进行前序遍历,获得各节点被访问到的顺序。
* 由于将二叉树展开为链表之后会破坏二叉树的结构,因此在前序遍历结束之后更新每个节点的左右子节点的信息,将二叉树展开为单链表。
* 时间复杂度:O(n)
* 空间复杂度:O(n)
* @param root
*/
public List<TreeNode> flatten(TreeNode root) {
List<TreeNode> list = new ArrayList<>();
preOrder(root, list);
int size = list.size();
for(int i = 1;i < size;i++) {
TreeNode pre = list.get(i - 1);
TreeNode cur = list.get(i);
pre.left = null;
pre.right = cur;
}
return list;
}
public void preOrder(TreeNode root, List<TreeNode> list) {
if(root == null) {
return;
}
list.add(root);
preOrder(root.left, list);
preOrder(root.right, list);
}
(2) 前序遍历–迭代
/**
* 2.前序遍历--迭代
* @param root
*/
public void flatten2(TreeNode root) {
if(root == null) {
return;
}
List<TreeNode> list = new ArrayList<>();
Deque<TreeNode> stack = new LinkedList<>();
stack.addFirst(root);
while(!stack.isEmpty()) {
TreeNode node = stack.removeFirst();
list.add(node);
if(node.right != null) {
stack.addFirst(node.right);
}
if(node.left != null) {
stack.addFirst(node.left);
}
}
int size = list.size();
for(int i = 1;i< size;i++) {
TreeNode pre = list.get(i-1);
TreeNode cur = list.get(i);
pre.left = null;
pre.right = cur;
}
}
(3) 前序遍历和展开同步进行
/**
* 3.前序遍历和展开同步进行
* 思路:每次从栈内弹出一个节点作为当前访问的节点,获得该节点的子节点,如果子节点不为空,则依次将右子节点和左子节点压入栈内(注意入栈顺序)。
* 展开为单链表的做法是,维护上一个访问的节点 pre,每次访问一个节点时,令当前访问的节点为 cur,
* 将 pre 的左子节点设为 null 以及将 pre 的右子节点设为 cur,然后将 cur 赋值给 pre,进入下一个节点的访问,直到遍历结束。
* 需要注意的是,初始时 pre 为 null,只有在 pre 不为 null 时才能对 pre 的左右子节点进行更新。
* 时间复杂度:O(n)
* 空间复杂度:O(n)
* @param root
*/
public void flatten3(TreeNode root) {
if(root == null) {
return;
}
Deque<TreeNode> stack = new LinkedList<>();
TreeNode pre = null;
stack.addFirst(root);
while(!stack.isEmpty()) {
TreeNode cur = stack.removeFirst();
if(pre != null) {
pre.left = null;
pre.right = cur;
}
TreeNode left = cur.left;
TreeNode right = cur.right;
if(right != null) {
stack.addFirst(right);
}
if(left != null) {
stack.addFirst(left);
}
pre = cur;
}
}
(4) 寻找前驱节点
/**
* 4.寻找前驱节点
* 前两种方法都借助前序遍历,前序遍历过程中需要使用栈存储节点。
* 时间复杂度:O(n)
* 空间复杂度:O(1)
* @param root
*/
public void flatten4(TreeNode root) {
TreeNode cur = root;
while(cur != null) {
if(cur.left != null) {
TreeNode next = cur.left;
TreeNode preNode = next;
while(preNode.right != null) {
preNode = preNode.right;
}
preNode.right = cur.right;
cur.left = null;
cur.right = next;
}
cur = cur.right;
}
}
5.2 归并排序
//归并排序 先分段再合并
public int[] mergeSortRoot(int[] nums) {
mergeSort(nums,0,nums.length-1);
return nums;
}
private void mergeSort(int[] nums, int left, int right) {
if(left < right) {
int mid = (left + right)/2;
//分治
mergeSort(nums, left, mid);
mergeSort(nums, mid+1, right);
//合并
merge(nums,left,mid,right);
}
}
private void merge(int[] nums, int left, int mid, int right) {
int i = left, j = mid + 1, k = left;
int[] tmp = new int[nums.length];
while (i <= mid && j <= right) {
if (nums[i] > nums[j]) {
tmp[k++] = nums[j++];
} else {
tmp[k++] = nums[i++];
}
}
while (i <= mid) {
tmp[k++] = nums[i++];
}
while (j <= right) {
tmp[k++] = nums[j++];
}
for (i = left; i <= right; i++) {
nums[i] = tmp[i];
}
}
5.3 快速排序
//快速排序 把一个数组分为左右两段,左段小于右段,类似分治法没有合并过程
public int[] quickSort(int[]nums) {
quickSort(nums,0,nums.length-1);
return nums;
}
private void quickSort(int[] nums, int start, int end) {
if(start < end) {
//pivot是分割点,pivot左边的比nums[pivot]小,右边的比nums[pivot]大
int pivot = partition(nums,start,end);
quickSort(nums, 0, pivot-1);
quickSort(nums, pivot+1, end);
}
}
private int partition(int[] nums, int start, int end) {
int target = nums[end];
int i = start;
// 将小于target移到数组前面
for(int j=start;j < end;j++) {
if(nums[j] < target) {
swap(nums,i,j);
i++;
}
}
// 把中间的值换为用于比较的基准值
swap(nums,i,end);
return i;
}
private void swap(int[] nums, int i, int j) {
int temp = nums[j];
nums[j] = nums[i];
nums[i] = temp;
}
7 总结
- 掌握二叉树递归与非递归遍历
- 理解 DFS 前序遍历与分治法
- 理解 BFS 层次遍历
8 练习链接
- [lee-144 二叉树的前序遍历]
- [lee-94 二叉树的中序遍历]
- [lee-145 二叉树的后序遍历]
- lee-102 二叉树的层序遍历
- lee-107 二叉树的层序遍历2
- lee-103 二叉树的锯齿形遍历
- NC-136 输出二叉树的右视图
- [NC-84 完全二叉树的节点数]
- lee-98 验证二叉搜索树
- lee-701 二叉搜索树中的插入操作
- lee-450 删除二叉搜索树中的节点
- lee-95 不同的二叉搜索树2
- lee-96 不同的二叉搜索树
- lee-110 平衡二叉树
- lee-104 二叉树的最大深度
- lee-124 二叉树的最大路径和
- lee-236 二叉树的最近公共祖先
- off-07 重建二叉树
- lee-114 二叉树展开为链表