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

今日内容:

● 层序遍历 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应用:

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<TreeNode> 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<Integer> preorderTraversal(TreeNode root) {
        List<Integer> result = new LinkedList<>();
        Stack<TreeNode> 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<Integer> inorderTraversal(TreeNode root) {
        List<Integer> result = new LinkedList<>();
    Stack<TreeNode> 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<Integer> postorderTraversal(TreeNode root) {
        List<Integer> result = new LinkedList<>();
        Stack<TreeNode> 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<List<Integer>> levelOrder(TreeNode root) {
        List<List<Integer>> resList = new ArrayList<List<Integer>>();
        
        Queue<TreeNode> queue = new ArrayDeque<TreeNode>();
        if(root != null){
            queue.add(root);
        }
        while(!queue.isEmpty()){
            int n = queue.size();
            List<Integer> level = new ArrayList<Integer>();
            //处理当前层的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<TreeNode> queue = new ArrayDeque<TreeNode>();
        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。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,639评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,277评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,221评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,474评论 1 283
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,570评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,816评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,957评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,718评论 0 266
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,176评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,511评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,646评论 1 340
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,322评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,934评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,755评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,987评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,358评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,514评论 2 348

推荐阅读更多精彩内容