当前位置:首页 » 《随便一记》 » 正文

二叉树-我的基础算法刷题之路(七)

11 人参与  2023年04月10日 11:46  分类 : 《随便一记》  评论

点击全文阅读


在这里插入图片描述

本篇博客旨在整理记录自已对二叉树的一些总结,以及刷题的解题思路,同时希望可给小伙伴一些帮助。本人也是算法小白,水平有限,如果文章中有什么错误之处,希望小伙伴们可以在评论区指出来,共勉 ?。

文章目录

一、理论基础:1、常见术语2、基本操作3、种类:4、存储方式:5、遍历方式:深度优先搜索(DFS):广度优先搜索(BFS): 二、二叉查找树的创建1、二叉树的结点类2、二叉树查找树3、二叉树查找树其他便捷方法3.1、查找二叉树中最小的键3.2、查找二叉树中最大的键 三、二叉树的基础遍历1.1、前序、中序、后序遍历1.1.1、前序遍历1.1.2、中序遍历1.1.3、后序遍历 1.2、二叉树的层序遍历1.3、二叉树的最大深度问题 小结二叉树二叉树遍历 最后

一、理论基础:

【二叉树 Binary Tree】是一种非线性数据结构,代表着祖先与后代之间的派生关系,体现着“一分为二”的分治逻辑。类似于链表,二叉树也是以结点为单位存储的,结点包含【值】和两个【指针】。

/* 二叉树结点类 */class TreeNode {    int val;         // 结点值    TreeNode left;   // 左子结点指针    TreeNode right;  // 右子结点指针    TreeNode(int x) { val = x; }}

结点的两个指针分别指向【左子节点 Left Child Node】 和 【右子结点 Right Child Node】,并且称该结果为两个子结点的【父结点 Parent Node】。给定二叉树某结点,将左子结点以下的树称为该结点的【左子树 Left Subtree】,右子树同理。
除了叶节点外,每个结点都有子结点和子树。例如,若将下图的【结点2】看作父结点,那么其左子结点和右子结点分别为【结点4】和【结点5】,左子树和右子树分别为【结点 4 及其以下结点形成的树】和【结点 5 及其以下结点形成的树】。
image.png

1、常见术语

二叉树的常见术语:

【根结点 Root Node】:二叉树最顶层的结点,其没有父结点;【叶节点 Leaf Node】:没有子结点的结点,其两个指针都指向(\text{null});结点所处【层 Level】:从顶至底依次增加,根结点所处层为 1;结点【度 Degree】:结点的子节结点数量。二叉树中,度的范围是0,1,2;【边 Edge】:连接两个结点的边,即结点指针;二叉树【高度】:二叉树中根结点到最远叶结点走过边的数量;结点【深度 Depth】:根结点到该结点走过边的数量;结点【高度 Height】:最远叶结点到该结点走过边的数量;

image.png

高度与深度的定义
值得注意,我们通常将「高度」和「深度」定义为“走过边的数量”,而有些题目或教材会将其定义为“走过结点的数量”,此时高度或深度都需要 + 1 。

2、基本操作

初始化二叉树。 与链表类似,先初始化结点,再构建引用指向(即指针)。

// 初始化结点TreeNode n1 = new TreeNode(1);TreeNode n2 = new TreeNode(2);TreeNode n3 = new TreeNode(3);TreeNode n4 = new TreeNode(4);TreeNode n5 = new TreeNode(5);// 构建引用指向(即指针)n1.left = n2;n1.right = n3;n2.left = n4;n2.right = n5;

插入与删除结点。 与链表类似,插入与删除结点都可以通过修改指针实现。
image.png
在二叉树中插入与删除结点

TreeNode P = new TreeNode(0);// 在 n1 -> n2 中间插入结点 Pn1.left = P;P.left = n2;// 删除结点 Pn1.left = n2;

Note
插入结点会改变二叉树的原有逻辑结构,删除结点往往意味着删除了该结点的所有子树。因此,二叉树中的插入与删除一般都是由一套操作配合完成的,这样才能实现有意义的操作。

3、种类:

满二叉树: 如果一棵二叉树只有度为0的结点和度为2的结点,并且度为0的结点在同一层上,则这棵二叉树为满二叉树。
image.png
完全二叉树: 在完全二叉树中,除了最底层节点可能没填满外,其余每层节点数都达到最大值,并且最下面一层的节点都集中在该层最左边的若干位置。若最底层为第 h 层,则该层包含 1~ 2^(h-1) 个节点。
image.png
二叉搜索树: 二叉搜索树是一个有序树。

若它的左子树不空,则左子树上所有结点的值均小于它的根结点的值;若它的右子树不空,则右子树上所有结点的值均大于它的根结点的值;它的左、右子树也分别为二叉排序树

平衡二叉搜索树: 平衡二叉搜索树:又被称为AVL(Adelson-Velsky and Landis)树,且具有以下性质:它是一棵空树或它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树。
image.png

4、存储方式:

线性存储【数组】、链式存储【指针】

5、遍历方式:

二叉树的遍历分为深度优先搜索和广度优先搜索

深度优先搜索(DFS):

1、前序遍历 中 左 右
2、中序遍历 左 中 右
3、后序遍历 左 右 中

广度优先搜索(BFS):

1、层序遍历
二叉树递归解题方法:

确定递归函数的参数和返回值确定终止条件确定单层递归的逻辑

二、二叉查找树的创建

1、二叉树的结点类

根据对图的观察,我们发现二叉树其实就是由一个一个的结点及其之间的关系组成的,按照面向对象的思想,我们设计一个结点类来描述结点这个事物。
节点类API设计:

类名Node<Key, Value>
构造方法Node(Key key, Value value, Node left, Node right)
成员变量1. public Node left:记录左子结点 2.public Node right:记录右子结点 3.public Key key:存储键 4.public Value value:存储值
代码实现:
private class Node<Key, Value> {        // 存储键        public Key key;        // 存储值        private Value value;        // 记录左子结点        public Node left;        // 记录右子结点        public Node right;        public Node(Key key, Value value, Node left, Node right) {            this.key = key;            this.value = value;            this.left = left;            this.right = right;        }    }

2、二叉树查找树

二叉树查找树API设计

类名<BinaryTree,Value value>
构造方法BinaryTree():创建BinaryTree对象
成员变量1. private Node root:记录根结点 ;2. private int N:记录数中元素的个数
成员方法1. public void put(Key key,Value value):向树中插入一个键值对; 2. private Node put(Node x, Key key, Value val):给指定树x上,添加键一个键值对,并返回添加后的新树;3. public Value get(Key key):根据key,从树中找出对应的值; 4. private Value get(Node x, Key key):从指定的树x中,找出key对应的值; 5. public void delete(Key key):根据key,删除树中对应的键值对; 6. private Node delete(Node x, Key key):删除指定树x上的键为key的键值对,并返回删除后的新树; 7. public int size():获取树中元素的个数;

二叉查找树实现

插入方法put实现思想 :

如果当前树中没有任何一个结点,则直接把新结点当做根结点使用如果当前树不为空,则从根结点开始: 如果新结点的key小于当前结点的key,则继续找当前结点的左子结点 ;如果新结点的key大于当前结点的key,则继续找当前结点的右子结点如果新结点的key等于当前结点的key,则树中已经存在这样的结点,替换该结点的value值即可.

查询方法get实现思想 :
从根节点开始:

如果要查询的key小于当前结点的key,则继续找当前结点的左子结点;如果要查询的key大于当前结点的key,则继续找当前结点的右子结点:如果要查询的key等于当前结点的key,则树中返回当前结点的value。

删除方法delete实现思想 :

找到被删除结点;找到被删除结点右子树中的最小结点minNode删除右子树中的最小结点让被删除结点的左子树称为最小结点minNode的左子树,让被删除结点的右子树称为最小结点minNode的右子树让被删除结点的父节点指向最小结点minNode

代码实现:

/** * @author QIA * @create 2023-04-04-1:43 */public class BinaryTree<Key extends Comparable<Key>, Value> {    // 记录根结点    private Node root;    // 记录树中元素的个数    private int N;    private class Node {        // 存储键        public Key key;        // 存储值        private Value value;        // 记录左子结点        public Node left;        // 记录右子结点        public Node right;        public Node(Key key, Value value, Node left, Node right) {            this.key = key;            this.value = value;            this.left = left;            this.right = right;        }    }    // 获取树中元素的个数    public int size() {        return N;    }    // 向树中添加元素key-value    public void put(Key key, Value value) {        root = put(root, key, value);    }    // 向指定的树x中添加key-value,并返回添加元素后新的树    private Node put(Node x, Key key, Value value) {        // 如果x子树为空        if (x == null) {            // 个数+1            N++;            return new Node(key, value, null, null);        }        // 如果 x 子树不为空        // 比较 x 结点的键和 key 的大小:        int cmp = key.compareTo(x.key);        if (cmp > 0) {            // 新结点的 key 大于当前结点的 key ,继续找当前结点的右子结点            x.right = put(x.right, key, value);        }else if(cmp < 0) {            // 新结点的 key 小于当前结点的 key ,继续找当前结点的左子结点            x.left = put(x.left, key, value);        }else {            // 新结点的 key 等于当前结点的 key ,把当前结点的 value 进行替换            x.value = value;        }        return x;    }    // 查询树中指定 key 对应的value    public Value get(Key key) {        return get(root,key);    }    // 从指定的树x中,查找key对应的值    public Value get(Node x, Key key) {        // x树为null        if (x == null) {            return null;        }        // x树不为null        // 比较key和x结点的键的大小        int cmp = key.compareTo(x.key);        if (cmp > 0) {            // 如果 key大于 x 结点的键,则继续找 x 结点的右子树            return get(x.right, key);        }else if(cmp < 0) {            // 如果 key 小于 x 结点的键,则继续找 x 结点的左子树            return get(x.left, key);        }else {            // 如果 key 等于 x 结点的键,就找到了键为key的结点,只需要返回x结点的值即可            return x.value;        }    }    // 删除树中key对应的value    public void delete(Key key) {        root = delete(root, key);    }    // 删除指定树 x 中的 key 对应的 value,并返回删除后的新树    public Node delete(Node x, Key key) {        if (x == null) {            return null;        }        int cmp = key.compareTo(x.key);        if (cmp > 0) {            /// 新结点的 key 大于当前结点的 key ,继续找当前结点的右子结点            x.right = delete(x.right, key);        }else if(cmp < 0) {            // 新结点的 key 小于当前结点的 key ,继续找当前结点的左子结点            x.left = delete(x.left, key);        }else {            // 新结点的 key 等于当前结点的 key ,当前 x 就是要删除的结点            // 1.如果当前结点的右子树不存在,则直接返回当前节点的左子节点            if (x.right == null) {                return x.left;            }            // 2.如果当前结点的左子树不存在,则直接返回当前结点的右子结点            if (x.left == null) {                return x.right;            }            // 3.当前结点的左右子树都存在            // 3.1 找到右子树中最小的结点            Node minNode = x.left;            while (minNode.left != null) {                minNode = minNode.left;            }            // 3.2 删除右子树中最小的结点            Node n = x,right;            while (n.left != null) {                if (n.left.left == null) {                    n.left = null;                } else {                    n = n.left;                }            }            // 3.3 让被删除结点的左子树被称为最小结点minNode的左子树,让被删除结点的右子树称为最小结点minNode的右子树            minNode.left = x.left;            minNode.right = x.right;            // 3.4 让删除结点的父节点指向最小结点minNode            x = minNode;            // 个数-1            N--;        }        return x;    }    public static void main(String[] args) {        BinaryTree<Integer, String> bt = new BinaryTree<>();        bt.put(4, "二哈");        bt.put(1, "张三");        bt.put(3, "李四");        bt.put(5, "王五");        // 长度        System.out.println(bt.size());        bt.put(1, "老三");        System.out.println(bt.get(1));        System.out.println(bt.size());        bt.delete(1);        System.out.println(bt.size());    }}

3、二叉树查找树其他便捷方法

3.1、查找二叉树中最小的键

public Key min()找出树中最小的键
private Node min(Node x)找出指定树x中,最小键所在的结点
//找出整个树中最小的键public Key min(){return min(root).key;}//找出指定树x中最小的键所在的结点private Node min(Node x){    if (x.left!=null){    return min(x.left);    }else{    return x;    }}

3.2、查找二叉树中最大的键

public Kye max()找出树中最大的键
public Node max(Node x)找出指定树x中,最大键所在的结点
//找出整个树中最大的键public Key max(){return max(root).key;}//找出指定树x中最大键所在的结点public Node max(Node x){    if (x.right!=null){    return max(x.right);    }else{    return x;    }}

三、二叉树的基础遍历

从物理结构角度看,树是一种基于链表的数据结构,因此遍历方式也是通过指针(即引用)逐个遍历结点。同时,树还是一种非线性数据结构,这导致遍历树比遍历链表更加复杂,需要使用搜索算法来实现。
常见的二叉树遍历方式有前序遍历、中序遍历、后序遍历、层序遍历。

1.1、前序、中序、后序遍历

前、中、后序遍历皆属于[深度优先遍历Depth-First Traversal],其体现着一种”先走到尽头,再回头继续“的回溯遍历方式。
如下图所示,左侧是深度优先遍历的的示意图,右上方是对应的递归实现代码。深度优先遍历就像是绕着整个二叉树的外围“走”一圈,走的过程中,在每个结点都会遇到三个位置,分别对应前序遍历、中序遍历、后序遍历。
image.png
复杂度分析
时间复杂度: 所有结点被访问一次,使用O(n)时间,其中n为结点数量。
空间复杂度: 当树退化为链表时达到最差情况,递归深度达到n,系统使用O(nb)栈帧空间。

1.1.1、前序遍历

添加前序遍历的API:
public Queue<Key> preErgodic():使用前序遍历,获取整个树中的所有键
private void preErgodic(Node x,Queue<Key> keys):使用前序遍历,把指定树x中的所有键放入到keys队列中
实现过程中,我们通过前序遍历,把每个结点的键取出,放入到队伍中返回即可。
实现步骤:

把当前结点的key放入队列中;找到当前结点的左子树,如果不为空,递归遍历左子树找到当前结点的右子树,如果不为空,递归遍历右子树

代码:

//使用前序遍历,获取整个树中的所有键public Queue<Key> preErgodic(){    Queue<Key> keys = new Queue<>();    preErgodic(root,keys);    return keys;}//使用前序遍历,把指定树x中的所有键放入到keys队列中private void preErgodic(Node x,Queue<Key> keys){    if (x==null){    return;    }    // 1.把当前结点的key放入到队列中;    keys.enqueue(x.key);    // 2.找到当前结点的左子树,如果不为空,递归遍历左子树    if (x.left!=null){    preErgodic(x.left,keys);    }    // 3.找到当前结点的右子树,如果不为空,递归遍历右子树    if (x.right!=null){    preErgodic(x.right,keys);    }}//测试代码public class Test {    public static void main(String[] args) throws Exception {        BinaryTree<String, String> bt = new BinaryTree<>();        bt.put("E", "5");        bt.put("B", "2");        bt.put("G", "7");        bt.put("A", "1");        bt.put("D", "4");        bt.put("F", "6");        bt.put("H", "8");        bt.put("C", "3");                Queue<String> queue = bt.preErgodic();        for (String key : queue) {        System.out.println(key+"="+bt.get(key));        }    }}

1.1.2、中序遍历

添加中序遍历的API:
public Queue<Key> midErgodic():使用中序遍历,获取震哥哥树中的所有键
private void midErgodic(Node x, Queue<Key> keys):使用中序遍历,把指定树x中的所有键放入到keys队列中
实现步骤:

找到当前结点的左子树,如果不为空,递归遍历左子树把当前结点的key放入到队列中找到当前结点的右子树,如果不为空,递归遍历右子树

代码:

//使用中序遍历,获取整个树中的所有键public Queue<Key> midErgodic(){Queue<Key> keys = new Queue<>();midErgodic(root,keys);    return keys;}//使用中序遍历,把指定树x中的所有键放入到keys队列中private void midErgodic(Node x,Queue<Key> keys){    if (x==null){    return;    }    // 1.找到当前结点的左子树,如果不为空,递归遍历左子树    if (x.left!=null){    midErgodic(x.left,keys);    }    // 2.把当前结点的key放入到队列中;    keys.enqueue(x.key);    // 3.找到当前结点的右子树,如果不为空,递归遍历右子树    if (x.right!=null){    midErgodic(x.right,keys);    }}//测试代码public class Test {    public static void main(String[] args) throws Exception {        BinaryTree<String, String> bt = new BinaryTree<>();        bt.put("E", "5");        bt.put("B", "2");        bt.put("G", "7");        bt.put("A", "1");        bt.put("D", "4");        bt.put("F", "6");        bt.put("H", "8");        bt.put("C", "3");                Queue<String> queue = bt.midErgodic();        for (String key : queue) {        System.out.println(key+"="+bt.get(key));        }    }}

1.1.3、后序遍历

添加后序遍历的API:
public Queue<Key> afterErgodic():使用后序遍历,获取整个树中的所有键
private void afterErgodic(Node x,Queue<Key> keys):使用后序遍历,把指定树x中的所有键放入到keys队列中
实现步骤:

找到当前结点的左子树,如果不为空,递归遍历左子树找到当前结点的右子树,如果不为空,递归遍历右子树把当前结点的key放入到队列中

代码:

//使用后序遍历,获取整个树中的所有键public Queue<Key> afterErgodic(){    Queue<Key> keys = new Queue<>();    afterErgodic(root,keys);    return keys;}//使用后序遍历,把指定树x中的所有键放入到keys队列中private void afterErgodic(Node x,Queue<Key> keys){    if (x==null){    return;    }    //1.找到当前结点的左子树,如果不为空,递归遍历左子树    if (x.left!=null){    afterErgodic(x.left,keys);    }    //2.找到当前结点的右子树,如果不为空,递归遍历右子树    if (x.right!=null){    afterErgodic(x.right,keys);    }    //3.把当前结点的key放入到队列中;    keys.enqueue(x.key);}//测试代码public class Test {    public static void main(String[] args) throws Exception {        BinaryTree<String, String> bt = new BinaryTree<>();        bt.put("E", "5");        bt.put("B", "2");        bt.put("G", "7");        bt.put("A", "1");        bt.put("D", "4");        bt.put("F", "6");        bt.put("H", "8");        bt.put("C", "3");                Queue<String> queue = bt.afterErgodic();        for (String key : queue) {        System.out.println(key+"="+bt.get(key));        }    }}

1.2、二叉树的层序遍历

「层序遍历 Level-Order Traversal」从顶至底、一层一层地遍历二叉树,并在每层中按照从左到右的顺序访问结点。
层序遍历本质上是「广度优先搜索 Breadth-First Traversal」,其体现着一种“一圈一圈向外”的层进遍历方式。
image.png
算法实现
广度优先遍历一般借助「队列」来实现。队列的规则是“先进先出”,广度优先遍历的规则是 ”一层层平推“ ,两者背后的思想是一致的。

复杂度分析
时间复杂度: 所有结点被访问一次,使用O(n)时间,其中n为结点数量。
空间复杂度: 当为满二叉树时达到最差情况,遍历到最底层前,队列中最多同时存在n+1/2 个结点,使用O(n)空间。

添加层序遍历的API:
public Queue<Key> layerErgodic():使用层序遍历,获取整个树中的所有键
实现步骤:

创建队列,存储每一层的结点;使用循环从队列中弹出一个结点: 获取当前结点的key;如果当前结点的左子结点不为空,则把左子结点放入到队列中如果当前结点的右子结点不为空,则把右子结点放入到队列中

image.png
代码:

//使用层序遍历得到树中所有的键public Queue<Key> layerErgodic(){    Queue<Key> keys = new Queue<>();    Queue<Node> nodes = new Queue<>();    nodes.enqueue(root);    while(!nodes.isEmpty()){        Node x = nodes.dequeue();        keys.enqueue(x.key);        if (x.left!=null){        nodes.enqueue(x.left);        }        if (x.right!=null){        nodes.enqueue(x.right);        }    }    return keys;}//测试代码public class Test {    public static void main(String[] args) throws Exception {        BinaryTree<String, String> bt = new BinaryTree<>();        bt.put("E", "5");        bt.put("B", "2");        bt.put("G", "7");        bt.put("A", "1");        bt.put("D", "4");        bt.put("F", "6");        bt.put("H", "8");        bt.put("C", "3");                Queue<String> queue = bt.layerErgodic();        for (String key : queue) {        System.out.println(key+"="+bt.get(key));        }    }}

1.3、二叉树的最大深度问题

需求:
给定一棵树,请计算树的最大深度(树的根节点到最远叶子结点的最长路径上的结点数);
image.png
上面这棵树的最大深度为4
实现:
添加API求最大深度:
public int maxDepth():计算整个数的最大深度
private int maxDepth(Node x):计算指定树x的最大深度
实现步骤:

如果根结点为空,则最大深度为0;计算左子树的最大深度;计算右子树的最大深度;当前树的最大深度 = 左子树的最大深度喝右子树的最大深度中的较大者+1

代码:

//计算整个树的最大深度public int maxDepth() {return maxDepth(root);}//计算指定树x的最大深度private int maxDepth(Node x) {    //1.如果根结点为空,则最大深度为0;    if (x == null) {    return 0;    }    int max = 0;    int maxL = 0;    int maxR = 0;    //2.计算左子树的最大深度;    if (x.left != null) {    maxL = maxDepth(x.left);    }    //3.计算右子树的最大深度;    if (x.right != null) {    maxR = maxDepth(x.right);    }    //4.当前树的最大深度=左子树的最大深度和右子树的最大深度中的较大者+1    max = maxL > maxR ? maxL + 1 : maxR + 1;    return max;}//测试代码public class Test {    public static void main(String[] args) throws Exception {        BinaryTree<String, String> bt = new BinaryTree<>();        bt.put("E", "5");        bt.put("B", "2");        bt.put("G", "7");        bt.put("A", "1");        bt.put("D", "4");        bt.put("F", "6");        bt.put("H", "8");        bt.put("C", "3");                int i = bt.maxDepth();    System.out.println(i);    }}
文章部分图片来自:开源算法书籍《Hello 算法》

小结

二叉树

二叉树是一种非线性数据结构,代表着“一分为二”的分治逻辑。二叉树的结点包含「值」和两个「指针」,分别指向左子结点和右子结点。选定二叉树中某结点,将其左(右)子结点以下形成的树称为左(右)子树。二叉树的术语较多,包括根结点、叶结点、层、度、边、高度、深度等。二叉树的初始化、结点插入、结点删除操作与链表的操作方法类似。常见的二叉树类型包括完美二叉树、完全二叉树、完满二叉树、平衡二叉树。完美二叉树是理想状态,链表则是退化后的最差状态。二叉树可以使用数组表示,具体做法是将结点值和空位按照层序遍历的顺序排列,并基于父结点和子结点之间的索引映射公式实现指针。

二叉树遍历

二叉树层序遍历是一种广度优先搜索,体现着“一圈一圈向外”的层进式遍历方式,通常借助队列来实现。前序、中序、后序遍历是深度优先搜索,体现着“走到头、再回头继续”的回溯遍历方式,通常使用递归实现。

最后

参考资料:《Hello 算法 (hello-algo.com)》

对各位小伙伴有帮助的话,希望可以点赞❤️+收藏⭐,谢谢各位大佬~~???


点击全文阅读


本文链接:http://zhangshiyu.com/post/59127.html

<< 上一篇 下一篇 >>

  • 评论(0)
  • 赞助本站

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。

关于我们 | 我要投稿 | 免责申明

Copyright © 2020-2022 ZhangShiYu.com Rights Reserved.豫ICP备2022013469号-1