算法训练第十四天| 第六章 二叉树

今日内容:

● 层序遍历 102
● 226.翻转二叉树
● 101.对称二叉树 2

种类

在我们解题过程中二叉树有两种主要的形式:满二叉树和完全二叉树。

满二叉树:如果一棵二叉树只有度为0的结点和度为2的结点,并且度为0的结点在同一层上,则这棵二叉树为满二叉树。
满二叉树节点个数 2^k - 1, k 为二叉树高度

完全二叉树 的定义如下:在完全二叉树中,除了最底层节点可能没填满外,其余每层节点数都达到最大值,并且最下面一层的节点都集中在该层最左边的若干位置。若最底层为第 h 层,则该层包含 1~ 2^(h-1) 个节点。

满二叉树一定是完全二叉树
堆就是一个完全二叉树, 同时保证子节点与父节点的顺序关系

二叉搜索树 binary search tree
二叉搜索树是一个有序树。 对于每个节点,左子树所有节点都小于中间节点,同时右子树所有节点都大于中间节点
搜索的时间复杂度 O(logn)

平衡二叉搜索树
又被称为AVL(Adelson-Velsky and Landis)树
|左子树高度- 右子树高度| <= 1
左右两个子树的高度差的绝对值不超过1
应用: C++ 中, Set, Map, MultiSet, MultiMap底层实现都是使用平衡二叉搜索树;
unordered_map、unordered_set,unordered_map、unordered_map底层实现是哈希表

自己熟悉的编程语言写算法,一定要知道常用的容器底层都是如何实现的

二叉树的存储方式

链式存储, 用指针链接节点
线性存储 用数组存储(顺序存储)
数组下标:
0
1 2
3 4 5 6
节点 i 的左孩子: 2*i + 1; 右孩子 2 * i + 2;

二叉树的遍历

  1. 深度优先搜索 DFS
    前序遍历,中序遍历,后序遍历,都是DFS。 经常使用递归的方式来实现。也可用迭代法实现

前序 中-左-右
中序 左-中-右
后序 左-右-中
前中后序可以理解为中间节点的位置

  1. 广度优先搜索 BFS
    层序遍历 迭代法 常用队列实现
    BFS应用:
    • 层序遍历 LC 102
    • 最短路径 LC 1162 As far from land as possible

DFS 与 BFS代码实现:

让我们先看看在二叉树上进行 DFS 遍历和 BFS 遍历的代码比较。

DFS 遍历使用递归:

Java

void dfs(TreeNode root) {
    if (root == null) {
        return;
    }
    dfs(root.left);
    dfs(root.right);
}

BFS 遍历使用队列数据结构:

Java

void bfs(TreeNode root) {
    Queue queue = new ArrayDeque<>();
    queue.add(root);
    while (!queue.isEmpty()) {
        TreeNode node = queue.poll(); // Java 的 pop 写作 poll()
        if (node.left != null) {
            queue.add(node.left);
        }
        if (node.right != null) {
            queue.add(node.right);
        }
    }
}

二叉树的定义

常用数据结构要能熟练写出来

class TreeNode{
  int value;
  TreeNode left;
  TreeNode right;

  TreeNode(int val){
      value = val;
      left = null;
      right = null;
  }
}


递归算法的三个要素

  1. 确定递归函数的参数和返回值: 确定哪些参数是递归的过程中需要处理的,那么就在递归函数里加上这个参数, 并且还要明确每次递归的返回值是什么进而确定递归函数的返回类型。

2.确定终止条件: 写完了递归算法, 运行的时候,经常会遇到栈溢出的错误,就是没写终止条件或者终止条件写的不对,操作系统也是用一个栈的结构来保存每一层递归的信息,如果递归没有终止,操作系统的内存栈必然就会溢出。

3.确定单层递归的逻辑: 确定每一层递归需要处理的信息。在这里也就会重复调用自己来实现递归的过程。

迭代

前序
使用 栈结构,弹出中间节点;先放入右子树再放入左子树,这样才能保证左子树先被弹出,从而达到 中-> 左 ->右 顺序
后序
只需在前序遍历基础上修改 : 弹出中间节点后,先放入左子树入栈,再放入右子树,这样出栈的顺序为 中-> 右-> 左, 再对数组进行翻转,得到后序: 左->右-> 中
中序
中序遍历,左中右,先访问顶部的节点,然后一层一层向下访问,直到到达树左面的最底部,再开始处理节点(也就是在把节点的数值放进result数组中),这就造成了处理顺序和访问顺序是不一致的。
迭代法写中序遍历,就需要借用指针的遍历来帮助访问节点,栈则用来处理节点上的元素
对任意节点,先放入顶部节点,再一路向左将左孩子入栈,直到没有左节点。左孩子为空时(此时到叶子节点),从栈中取节点并处理,如此节点有右孩子,入栈右孩子

中序遍历的迭代处理

二叉树的统一迭代法

其实针对三种遍历方式,使用迭代法是可以写出统一风格的代码
就是将访问的节点放入栈中,把要处理的节点也放入栈中但是要做标记。
如何标记呢,就是要处理的节点放入栈之后,紧接着放入一个空指针作为标记。 这种方法也可以叫做标记法。

Java: 迭代法前序遍历代码如下:

class Solution {
    public List preorderTraversal(TreeNode root) {
        List result = new LinkedList<>();
        Stack st = new Stack<>();
        if (root != null) st.push(root);
        while (!st.empty()) {
            TreeNode node = st.peek();
            if (node != null) {
                st.pop(); // 将该节点弹出,避免重复操作,下面再将右中左节点添加到栈中
                if (node.right!=null) st.push(node.right);  // 添加右节点(空节点不入栈)
                if (node.left!=null) st.push(node.left);    // 添加左节点(空节点不入栈)
                st.push(node);                          // 添加中节点
                st.push(null); // 中节点访问过,但是还没有处理,加入空节点做为标记。
                
            } else { // 只有遇到空节点的时候,才将下一个节点放进结果集
                st.pop();           // 将空节点弹出
                node = st.peek();    // 重新取出栈中元素
                st.pop();
                result.add(node.val); // 加入到结果集
            }
        }
        return result;
    }
}

中序:

class Solution {
public List inorderTraversal(TreeNode root) {
        List result = new LinkedList<>();
    Stack st = new Stack<>();
    if (root != null) st.push(root);
    while (!st.empty()) {
        TreeNode node = st.peek();
        if (node != null) {
            st.pop(); // 将该节点弹出,避免重复操作,下面再将右中左节点添加到栈中
            if (node.right!=null) st.push(node.right);  // 添加右节点(空节点不入栈)
            st.push(node);                          // 添加中节点
            st.push(null); // 中节点访问过,但是还没有处理,加入空节点做为标记。

            if (node.left!=null) st.push(node.left);    // 添加左节点(空节点不入栈)
        } else { // 只有遇到空节点的时候,才将下一个节点放进结果集
            st.pop();           // 将空节点弹出
            node = st.peek();    // 重新取出栈中元素
            st.pop();
            result.add(node.val); // 加入到结果集
        }
    }
    return result;
}
}

后序:

class Solution {
   public List postorderTraversal(TreeNode root) {
        List result = new LinkedList<>();
        Stack st = new Stack<>();
        if (root != null) st.push(root);
        while (!st.empty()) {
            TreeNode node = st.peek();
            if (node != null) {
                st.pop(); // 将该节点弹出,避免重复操作,下面再将右中左节点添加到栈中
                st.push(node);                          // 添加中节点
                st.push(null); // 中节点访问过,但是还没有处理,加入空节点做为标记。
                if (node.right!=null) st.push(node.right);  // 添加右节点(空节点不入栈)
                if (node.left!=null) st.push(node.left);    // 添加左节点(空节点不入栈)         
                               
            } else { // 只有遇到空节点的时候,才将下一个节点放进结果集
                st.pop();           // 将空节点弹出
                node = st.peek();    // 重新取出栈中元素
                st.pop();
                result.add(node.val); // 加入到结果集
            }
        }
        return result;
   }
}

102 层序遍历

层序遍历是广度优先搜索(BFS)的应用
利用队列实现 , 记录当前层数节点个数,弹出本层节点同时将他们的左右孩子(即下一层节点)加入数列

!!注意层序遍历处理每层节点时,要用一个变量记录queue size,
不能在for 循环直接用queue.size(),因为循环内部queue.size()在改变


class Solution {
    public List> levelOrder(TreeNode root) {
        List> resList = new ArrayList>();
        
        Queue queue = new ArrayDeque();
        if(root != null){
            queue.add(root);
        }
        while(!queue.isEmpty()){
            int n = queue.size();
            List level = new ArrayList();
            //处理当前层的n个节点
            //***注意层序遍历处理每层节点时,要用一个变量记录queue size,
          // 不能在for 循环直接用queue.size(),因为循环内部queue.size()在改变
            for(int i = 0; i < n; i++){
                //弹出节点,记录节点值并把它的左右孩子加入队列,成为下一层的处理对象
                TreeNode curNode = queue.poll();
                level.add(curNode.val);
                if(curNode.left != null) {
                    queue.add(curNode.left);
                }
                if(curNode.right != null){
                    queue.add(curNode.right);
                }
            }
            resList.add(level);
        }
        return resList;
    }
}

时间复杂度 O(n),每个节点只入队出队一次
空间复杂度 O(n), 队列空间最大为n

226. 翻转二叉树

一: 递归

/**
 * 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 TreeNode invertTree(TreeNode root) {
/**
     * 前后序遍历都可以
     * 中序不行,因为先左孩子交换孩子,再根交换孩子(做完后,右孩子已经变成了原来的左孩子),
        再右孩子交换孩子(此时其实是对原来的左孩子做交换)
     */
        if(root == null) return root;
        invertNode(root);
        invertTree(root.left);
        invertTree(root.right);
        return root;
    }
    
    public TreeNode invertNode(TreeNode node){
        TreeNode temp = node.left;
        node.left = node.right;
        node.right = temp;
        return node;
    }
}

时间复杂度:每个元素都必须访问一次,所以是 O(n)
空间复杂度:最坏的情况下,需要存放 O(h) 个函数调用(h是树的高度),所以是 O(h)

方法二 迭代: BFS 层序遍历

迭代.gif
/**
 * 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 TreeNode invertTree(TreeNode root) {
        if(root == null) return root;
        //BFS. using queue
        Queue queue = new ArrayDeque();
        queue.add(root);
        while(!queue.isEmpty()){
            TreeNode node = queue.poll();
            swapNode(node);
            if(node.left != null) queue.add(node.left);
            if(node.right != null) queue.add(node.right);
        }
        return root;
    }
    
    public void swapNode(TreeNode node){
        TreeNode temp = node.left;
        node.left = node.right;
        node.right = temp;

    }
}

时间复杂度:同样每个节点都需要入队列/出队列一次,所以是 O(n)
空间复杂度:最坏的情况下会包含所有的叶子节点,完全二叉树叶子节点是 n/2个,所以时间复杂度是 0(n)

101. 对称二叉树 (优先掌握递归)

给定一个二叉树,检查它是否是镜像对称的。

首先想清楚,判断对称二叉树,要比较的可不是左右节点!

对于二叉树是否对称,要比较的是根节点的左子树与右子树是不是相互翻转的,理解这一点就知道了其实我们要比较的是两个树(这两个树是根节点的左右子树),所以在递归遍历的过程中,也是要同时遍历两棵树。

那么如果比较呢?

比较的是两个子树的里侧和外侧的元素是否相等。

即,递归的比较

  • 外侧: left.left 和 right.right,
  • 内侧: left.right 和 right.left
/**
 * 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 boolean isSymmetric(TreeNode root) {
        if(root == null) return false;
        //递归三部曲
        //1. 确定递归函数的参数和返回值
        return isSymmetricTree(root.left, root.right);
    }

    boolean isSymmetricTree(TreeNode a, TreeNode b){
        //2. 确定终止条件
        if(a == null && b == null) return true;
        if(a == null || b == null || a.val != b.val) return false;
        //3. 确定单层递归的逻辑
        //比较二叉树外侧是否对称:传入的是左节点的左孩子,右节点的右孩子。
        //比较内测是否对称,传入左节点的右孩子,右节点的左孩子。
        return isSymmetricTree(a.left, b.right) && isSymmetricTree(a.right, b.left);
    }
}

算法的时间复杂度是 O(n),因为要遍历 n 个节点
空间复杂度是 O(n),空间复杂度是递归的深度,也就是跟树高度有关,最坏情况下树变成一个链表结构,高度是n。

你可能感兴趣的:(算法训练第十四天| 第六章 二叉树)