本篇博客旨在整理记录自已对二叉树的一些总结,以及刷题的解题思路,同时希望可给小伙伴一些帮助。本人也是算法小白,水平有限,如果文章中有什么错误之处,希望小伙伴们可以在评论区指出来,共勉 。
【二叉树 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 及其以下结点形成的树】。
二叉树的常见术语:
高度与深度的定义
值得注意,我们通常将「高度」和「深度」定义为“走过边的数量”,而有些题目或教材会将其定义为“走过结点的数量”,此时高度或深度都需要 + 1 。
初始化二叉树。 与链表类似,先初始化结点,再构建引用指向(即指针)。
// 初始化结点
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;
插入与删除结点。 与链表类似,插入与删除结点都可以通过修改指针实现。
在二叉树中插入与删除结点
TreeNode P = new TreeNode(0);
// 在 n1 -> n2 中间插入结点 P
n1.left = P;
P.left = n2;
// 删除结点 P
n1.left = n2;
Note
插入结点会改变二叉树的原有逻辑结构,删除结点往往意味着删除了该结点的所有子树。因此,二叉树中的插入与删除一般都是由一套操作配合完成的,这样才能实现有意义的操作。
满二叉树: 如果一棵二叉树只有度为0的结点和度为2的结点,并且度为0的结点在同一层上,则这棵二叉树为满二叉树。
完全二叉树: 在完全二叉树中,除了最底层节点可能没填满外,其余每层节点数都达到最大值,并且最下面一层的节点都集中在该层最左边的若干位置。若最底层为第 h 层,则该层包含 1~ 2^(h-1) 个节点。
二叉搜索树: 二叉搜索树是一个有序树。
平衡二叉搜索树: 平衡二叉搜索树:又被称为AVL(Adelson-Velsky and Landis)树,且具有以下性质:它是一棵空树或它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树。
线性存储【数组】、链式存储【指针】
二叉树的遍历分为深度优先搜索和广度优先搜索
1、前序遍历 中 左 右
2、中序遍历 左 中 右
3、后序遍历 左 右 中
1、层序遍历
二叉树递归解题方法:
根据对图的观察,我们发现二叉树其实就是由一个一个的结点及其之间的关系组成的,按照面向对象的思想,我们设计一个结点类来描述结点这个事物。
节点类API设计:
类名 | Node |
---|---|
构造方法 | 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;
}
}
二叉树查找树API设计
类名 | |
---|---|
构造方法 | 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实现思想 :
查询方法get实现思想 :
从根节点开始:
删除方法delete实现思想 :
代码实现:
/**
* @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());
}
}
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;
}
}
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;
}
}
从物理结构角度看,树是一种基于链表的数据结构,因此遍历方式也是通过指针(即引用)逐个遍历结点。同时,树还是一种非线性数据结构,这导致遍历树比遍历链表更加复杂,需要使用搜索算法来实现。
常见的二叉树遍历方式有前序遍历、中序遍历、后序遍历、层序遍历。
前、中、后序遍历皆属于[深度优先遍历Depth-First Traversal],其体现着一种”先走到尽头,再回头继续“的回溯遍历方式。
如下图所示,左侧是深度优先遍历的的示意图,右上方是对应的递归实现代码。深度优先遍历就像是绕着整个二叉树的外围“走”一圈,走的过程中,在每个结点都会遇到三个位置,分别对应前序遍历、中序遍历、后序遍历。
复杂度分析
时间复杂度: 所有结点被访问一次,使用O(n)时间,其中n为结点数量。
空间复杂度: 当树退化为链表时达到最差情况,递归深度达到n,系统使用O(nb)栈帧空间。
添加前序遍历的API:
public Queue
private void preErgodic(Node x,Queue
实现过程中,我们通过前序遍历,把每个结点的键取出,放入到队伍中返回即可。
实现步骤:
代码:
//使用前序遍历,获取整个树中的所有键
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));
}
}
}
添加中序遍历的API:
public Queue
private void midErgodic(Node x, Queue
实现步骤:
代码:
//使用中序遍历,获取整个树中的所有键
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));
}
}
}
添加后序遍历的API:
public Queue
private void afterErgodic(Node x,Queue
实现步骤:
代码:
//使用后序遍历,获取整个树中的所有键
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));
}
}
}
「层序遍历 Level-Order Traversal」从顶至底、一层一层地遍历二叉树,并在每层中按照从左到右的顺序访问结点。
层序遍历本质上是「广度优先搜索 Breadth-First Traversal」,其体现着一种“一圈一圈向外”的层进遍历方式。
算法实现
广度优先遍历一般借助「队列」来实现。队列的规则是“先进先出”,广度优先遍历的规则是 ”一层层平推“ ,两者背后的思想是一致的。
复杂度分析
时间复杂度: 所有结点被访问一次,使用O(n)时间,其中n为结点数量。
空间复杂度: 当为满二叉树时达到最差情况,遍历到最底层前,队列中最多同时存在n+1/2 个结点,使用O(n)空间。
添加层序遍历的API:
public Queue
实现步骤:
//使用层序遍历得到树中所有的键
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));
}
}
}
需求:
给定一棵树,请计算树的最大深度(树的根节点到最远叶子结点的最长路径上的结点数);
上面这棵树的最大深度为4
实现:
添加API求最大深度:
public int maxDepth():计算整个数的最大深度
private int maxDepth(Node x):计算指定树x的最大深度
实现步骤:
代码:
//计算整个树的最大深度
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);
}
}
对各位小伙伴有帮助的话,希望可以点赞❤️+收藏⭐,谢谢各位大佬~~