掘金 后端 ( ) • 2024-04-02 09:43

二叉树是计算机科学中一种重要的数据结构,用于模拟具有树状结构的数据集合。每个节点最多有两个子节点,通常称之为“左子节点”和“右子节点”。二叉树在算法设计、数据库、搜索引擎等领域有着广泛的应用。

二叉树的基本概念

  • 节点(node):树的基本单元,包含数据项和指向其子节点的指针。
  • 根节点(root node):树顶部的节点。
  • 父节点和子节点:在树中,如果节点A的直接下级是节点B,则节点A是节点B的父节点,节点B是节点A的子节点。
  • 叶节点(leaf):没有子节点的节点。
  • 子树(subtree):每个节点和它的后代构成的树称为原树的子树。
  • 边(edge):连接两个节点的线段,即节点引用(指针)。
  • 节点的度(degree):节点的子节点的数量。在二叉树中,度的取值范围是 0、1、2 。
  • 节点的深度(depth):从根节点到该节点所经过的边的数量。
  • 节点的高度(height):从节点到叶子节点的最长路径的边数。
  • 树的高度:根节点的高度。

截屏2024-03-27 16.58.28.png

二叉树的类型

  • 完全二叉树:除了最后一层外,每层都是满的,并且最后一层的节点都尽可能地集中在左侧。

  • 满二叉树:除了叶子节点外,每个节点都有两个子节点。

  • 平衡二叉树(AVL树):任何节点的两个子树的高度差不超过1。

插入与删除节点

与链表类似,在二叉树中插入与删除节点可以通过修改指针来实现

image.png

二叉树的遍历

遍历是指按照某种路径规则,访问树中每个节点一次且仅一次的过程。二叉树的遍历分为四种:前序、中序、后序和层序遍历。

  • 前序遍历:先访问根节点,然后递归地前序遍历左子树,最后递归地前序遍历右子树。
  • 中序遍历:先递归地中序遍历左子树,然后访问根节点,最后递归地中序遍历右子树。
  • 后序遍历:先递归地后序遍历左子树,然后递归地后序遍历右子树,最后访问根节点。
  • 层序遍历:按照树的层次从上到下访问树中的节点。

示例:二叉树的实现与遍历

以下是一个简单的Java实现,展示了如何定义一个二叉树节点以及如何进行前序遍历、中序遍历和后序遍历。

public class BinaryTree<E> {

    // 前序遍历
    public void preOrderTraversal(TreeNode<E> node) {
        if (node == null) return;

        System.out.print(node.data + " ");
        preOrderTraversal(node.left);
        preOrderTraversal(node.right);
    }

    // 中序遍历
    public void inOrderTraversal(TreeNode<E> node) {
        if (node == null) return;

        inOrderTraversal(node.left);
        System.out.print(node.data + " ");
        inOrderTraversal(node.right);
    }

    // 后序遍历
    public void postOrderTraversal(TreeNode<E> node) {
        if (node == null) return;

        postOrderTraversal(node.left);
        postOrderTraversal(node.right);
        System.out.print(node.data + " ");
    }

    private static class TreeNode<E> {
        E data;
        TreeNode<E> left, right;

        public TreeNode(E data) {
            this.data = data;
        }
    }

    public static void main(String[] args) {
        // 构建二叉树
        //      1
        //    /   \
        //   2     3
        //  / \   / \
        // 4   5 6   7
        TreeNode<Integer> root = new TreeNode<>(1);
        root.left = new TreeNode<>(2);
        root.right = new TreeNode<>(3);
        root.left.left = new TreeNode<Integer>(4);
        root.left.right = new TreeNode<Integer>(5);
        root.right.left = new TreeNode<>(6);
        root.right.right = new TreeNode<>(7);

        BinaryTree binaryTree = new BinaryTree();

        // 前序遍历输出
        System.out.println("Preorder traversal: ");
        binaryTree.preOrderTraversal(root);
        System.out.println();

        // 中序遍历输出
        System.out.println("Inorder traversal: ");
        binaryTree.inOrderTraversal(root);
        System.out.println();

        // 后续遍历输出
        System.out.println("Postorder traversal: ");
        binaryTree.postOrderTraversal(root);
        System.out.println();
    }
}

在上面的代码中,我们首先定义了一个TreeNode类,代表二叉树的节点。每个节点包含一个整数值data和两个指向其左右子节点的引用leftright

然后,我们创建了一个BinaryTree类,它包含三种遍历方法:前序遍历(preOrderTraversal)、中序遍历(inOrderTraversal)和后序遍历(postOrderTraversal)。每种遍历方法都接受一个TreeNode类型的参数,这个参数是当前正在访问的节点。

最后,在main方法中,我们创建了一个简单的二叉树实例,并分别对其执行前序、中序和后序遍历,并打印出每个节点的值。

输出结果将会是:

Preorder traversal:
1 2 4 5 3 6 7

Inorder traversal:
4 2 5 1 6 3 7

Postorder traversal:
4 5 2 6 7 3 1

层序遍历

层序遍历通常使用队列来实现。在Java中,我们可以使用LinkedList作为队列。以下是层序遍历的示例代码:

import java.util.LinkedList;
import java.util.Queue;

public class BinaryTree {
    // ... 其他遍历方法 ...

    // 层序遍历
    public void levelOrderTraversal(TreeNode<E> root) {
        if (root == null) {
            return;
        }

        Queue<TreeNode<E>> queue = new LinkedList<>();
        // LinkedList.offer() 方法用于在队列末尾添加元素,它总是返回 true,表示添加操作总是成功的。
        queue.offer(root);

        System.out.println();

        while (!queue.isEmpty()) {
            // LinkedList.poll() 方法用于移除并返回队列的头部元素。如果队列为空,它返回 null 而不是抛出异常。
            TreeNode<E> node = queue.poll();
            System.out.print(node.data + " ");

            if (node.left != null) {
                queue.offer(node.left);
            }
            if (node.right != null) {
                queue.offer(node.right);
            }
        }
    }
    
    public static void main(String[] args) {
        // ... 构建二叉树和其他遍历 ...

        // 层序遍历输出
        System.out.println("Level order traversal: ");
        binaryTree.levelOrderTraversal(root);
        System.out.println();
    }
}

层序遍历的输出结果将会是:

Level order traversal:
1 2 3 4 5 6 7

在二叉树的层序遍历中,我们首先将根节点放入队列。然后,只要队列不为空,我们就从队列中取出一个节点,打印其值,接着如果存在左子节点或右子节点,将它们加入队列。如此循环,直至队列为空,层序遍历就完成了。

我们将依次展示三种特殊二叉树:完全二叉树、满二叉树和平衡二叉树(AVL树)的相关代码。

完全二叉树

完全二叉树是指除了最后一层之外,每一层都被完全填满,且最后一层的所有节点都尽可能地集中在左侧。这里,我们使用一个数组来表示完全二叉树,数组中的每个索引对应二叉树中的一个节点。

完全二叉树的表示和检查

public class CompleteBinaryTree {

    // 使用数组来表示完全二叉树,arr[i]表示节点i的值
    private int[] arr;

    public CompleteBinaryTree(int[] arr) {
        this.arr = arr;
    }

    // 检查给定数组是否表示一个完全二叉树
    public boolean isComplete() {
        int i = 0;
        while (i < arr.length) {
            // 如果左子节点的索引超出数组范围,则不是完全二叉树
            if (2 * i + 1 < arr.length) {
                // 如果右子节点的索引超出数组范围,则当前节点必须是最后一个节点
                if (2 * i + 2 < arr.length) {
                    i++;
                } else {
                    return i == arr.length - 1;
                }
            } else {
                break;
            }
        }
        return true;
    }
    
    // 省略其他方法...
}

满二叉树

满二叉树是指除了叶子节点之外,每个节点都有两个子节点。在满二叉树中,节点总数与树的高度有确定的数学关系:如果树的高度是h,那么节点总数是2^h - 1

满二叉树的表示和检查

public class FullBinaryTree {
    
    // 使用TreeNode类表示二叉树的节点
    public static class TreeNode {
        int val;
        TreeNode left;
        TreeNode right;
    
        TreeNode(int x) {
            val = x;
        }
    }

    // 检查一个二叉树是否是满二叉树
    public boolean isFull(TreeNode root) {
        if (root == null) {
            return true;
        }
        
        // 如果一个节点有一个孩子而没有另一个孩子,则它不可能是满的
        if ((root.left == null && root.right != null) || (root.left != null && root.right == null)) {
            return false;
        }
        
        // 递归检查左右子树
        return isFull(root.left) && isFull(root.right);
    }
    
    // 省略其他方法...
}

平衡二叉树(AVL树)

AVL树是一种自平衡的二叉查找树,它的每个节点的左右子树的高度最多相差1。为了维持这种平衡,插入和删除操作可能会涉及到树的旋转。

AVL树的节点定义与基本旋转操作

public class AVLTree {
    
    // 定义AVL树的节点
    public static class AVLTreeNode {
        int val;
        int height; // 节点的高度
        AVLTreeNode left;
        AVLTreeNode right;
    
        AVLTreeNode(int x) {
            val = x;
            height = 1; // 新节点的高度默认为1
        }
    }

    // 获取节点的高度
    private int height(AVLTreeNode node) {
        return node == null ? 0 : node.height;
    }

    // 计算节点的平衡因子
    private int getBalanceFactor(AVLTreeNode node) {
        if (node == null) {
            return 0;
        }
        return height(node.left) - height(node.right);
    }

    // 右旋转
    private AVLTreeNode rightRotate(AVLTreeNode y) {
        AVLTreeNode x = y.left;
        AVLTreeNode T2```java
        // T2 = x的右子树
        y.left = x.right;
        x.right = y;

        // 更新y和x的高度
        y.height = Math.max(height(y.left), height(y.right)) + 1;
        x.height = Math.max(height(x.left), height(x.right)) + 1;

        // 返回新的根节点
        return x;
    }

    // 左旋转
    private AVLTreeNode leftRotate(AVLTreeNode x) {
        AVLTreeNode y = x.right;
        AVLTreeNode T2 = y.left;

        // 旋转操作
        y.left = x;
        x.right = T2;

        // 更新高度
        x.height = Math.max(height(x.left), height(x.right)) + 1;
        y.height = Math.max(height(y.left), height(y.right)) + 1;

        // 返回新的根节点
        return y;
    }

    // 重新平衡树
    private AVLTreeNode rebalance(AVLTreeNode node) {
        // 更新节点高度
        node.height = Math.max(height(node.left), height(node.right)) + 1;

        int balanceFactor = getBalanceFactor(node);

        // 如果这个节点变得不平衡,那么有四种情况

        // 左左 - 右旋转
        if (balanceFactor > 1 && getBalanceFactor(node.left) >= 0) {
            return rightRotate(node);
        }

        // 左右 - 左右旋转
        if (balanceFactor > 1 && getBalanceFactor(node.left) < 0) {
            node.left = leftRotate(node.left);
            return rightRotate(node);
        }

        // 右右 - 左旋转
        if (balanceFactor < -1 && getBalanceFactor(node.right) <= 0) {
            return leftRotate(node);
        }

        // 右左 - 右左旋转
        if (balanceFactor < -1 && getBalanceFactor(node.right) > 0) {
            node.right = rightRotate(node.right);
            return leftRotate(node);
        }

        // 没有失衡,不需要旋转
        return node;
    }

    // 插入节点到AVL树中,并返回插入后的新树的根
    public AVLTreeNode insert(AVLTreeNode node, int val) {
        if (node == null) {
            return new AVLTreeNode(val);
        }

        if (val < node.val) {
            node.left = insert(node.left, val);
        } else if (val > node.val) {
            node.right = insert(node.right, val);
        } else {
            // 不允许有相同的值(已经存在的值)
            return node;
        }

        // 重新平衡AVL树并返回结果
        return rebalance(node);
    }

    // 省略删除操作和其他辅助函数...
}

在上面的代码中,我们定义了一个AVL树的节点类AVLTreeNode,并且实现了四个主要操作:获取节点的高度height,计算平衡因子getBalanceFactor,右旋转rightRotate和左旋转leftRotate。这些旋转操作是为了重新平衡树,特别是在插入或删除节点后。

我们还实现了一个rebalance方法,它会根据平衡因子来判断一个节点是否平衡,并执行相应的旋转操作以保持AVL树的平衡性。

最后,我们提供了insert方法,它允许在AVL树中插入一个新值。当插入一个值时,我们可能需要进行一系列的旋转操作来保证树的平衡。删除操作也是类似的,但由于其复杂性,在这里我们没有展示。

请注意,这里的代码只展示了AVL树的基本框架和核心功能。在实际应用中,我们需要更加完善的实现,包括错误处理、删除功能以及更多的辅助方法。