数据结构与算法-二叉树

1,007 阅读7分钟

1,树的基本概念

  • 一棵树可以有节点,根节点,父节点,子节点,兄弟节点

  • 空树:就是一棵树没有节点

  • 根节点:一棵树只有一个节点,也就是根节点

  • 树可以有,左子树,右子树

  • 节点的度:就是子树的个数

  • 树的度:所有节点中度的最大值

  • 叶子节点:度为0的节点

  • 非叶子节点:度不为0的节点

  • 层树:根节点在第一层,根节点的子节点在第2层,以此类推
  • 节点的深度:从根节点到当前节点的唯一路径上的节点总数
  • 节点的高度:从当前节点到最远叶子节点的路径上的节点总数
  • 树的深度:所有节点深度中的最大值
  • 树的高度:所有节点中高度的最大值
  • 树的深度等于树的高度
  1. 有序树:树中任意节点的子节点之间有顺序关系
  2. 无序树:树中任意节点的子节点之间没有顺序关系

2,二叉树

2.1,二叉树的特点

  1. 每个节点的度最大为2(最大拥有2棵子树)
  2. 左子树和右子树是有顺序的
  3. 即使某节点只有一棵子树,也要区分左右子树
  4. 二叉树是有序树

2.2,二叉树的性质

  1. 非空二叉树的第i层,最多有2^ (i - 1)个节点(i >= 1)
  2. 在高度为h的二叉树上最多有2^h - 1个节点(h >= 1)
  3. 对于任何一棵非空二叉树,如果叶子节点个数为n0,度为2的节点个数n2,则有:n0 = n2 + 1
  4. 假设度为1的节点个数为n1,那么二叉树的节点总数n = n0 + n1 + n2
  5. 二叉树的边数T = n1 + 2*n2 = n - 1 = n0 + n1 + n2 - 1
  6. 因此n0 = n2 + 1

2.3,真二叉树

  • 所有节点的度都要么为0,要么为2

2.4,满二叉树

  1. 满二叉树:最后一层节点的度都为0,其它节点的度都为2
  2. 在同样高度的二叉树中,满二叉树的叶子节点数量最多,总节点数量最多
  3. 满二叉树一定是真二叉树,真二叉树不一定是满二叉树

假设满二叉树的高度为h( h >= 1)

第i层的节点数量为:2 ^ (i - 1)

叶子节点数量: 2 ^ (h - 1)

总节点数量 n

n = 2^h - 1 = 2^0 + 2^1 + 2^2 + …… + 2^(h - 1)

h = log2(n + 1)

2.5, 完全二叉树

  1. 完全二叉树:对节点从上至下,左至右开始编号,其所有编号都能与相同高度的满二叉树中的编号对应
  2. 叶子节点只会出现在最后2层,最后1层的叶子节点都靠左对齐
  3. 完全二叉树从根节点到倒数第二层的一棵满二叉树
  4. 满二叉树一定是完全二叉树,完全二叉树不一定是满二叉树

2.5.1,完全二叉树的性质

  1. 度为1的节点只有左子树
  2. 度为1的节点要么是1个,要么是0个
  3. 同样节点数量的二叉树,完全二叉树的高度最小 

2.6,二叉树的遍历

2.6.1,前序遍历

力扣地址:leetcode-cn.com/problems/bi…

  • 访问顺序:先访问根节点,然后前序遍历左子树,在前序遍历右子树

    //前序遍历
    public void preorderTraversal() {
    preorderTraversal(root);
    }
    private void preorderTraversal(Node node) {
    if (node == null) return;
    System.out.println(node.element);
    preorderTraversal(node.left);
    preorderTraversal(node.right);
    }

    //非递归,使用栈实现

    class Solution { public List preorderTraversal(TreeNode root) { List res = new ArrayList(); preorder(root, res); return res; }

    public void preorder(TreeNode root, List<Integer> res) {
        if (root == null) {
            return;
        }
        res.add(root.val);
        preorder(root.left, res);
        preorder(root.right, res);
    }
    

    }

2.6.2,中序遍历

力扣地址:leetcode-cn.com/problems/bi…

  • 访问顺序:先遍历左子树,在遍历根节点,最后中序遍历右子树

    //递归 public void inorderTraversal() {
    inorderTraversal(root);
    }
    private void inorderTraversal(Node node) {
    if (node == null) return;
    inorderTraversal(node.left);//
    System.out.println(node.element);
    inorderTraversal(node.right);
    } //栈实现 class Solution { public List inorderTraversal(TreeNode root) { List res = new ArrayList(); Deque stk = new LinkedList(); while (root != null || !stk.isEmpty()) { while (root != null) { stk.push(root); root = root.left; } root = stk.pop(); res.add(root.val); root = root.right; } return res; } }

2.6.3,后序遍历

力扣地址:leetcode-cn.com/problems/bi…

  • 访问顺序:先遍历左子树,在后序遍历右子树,最后访问根节点

    //递归 public void postorderTraversal() {
    postorderTraversal(root);
    }
    private void postorderTraversal(Node node) {
    if (node == null) return; postorderTraversal(node.left);
    postorderTraversal(node.right);
    System.out.println(node.element);
    } //迭代 class Solution { public List postorderTraversal(TreeNode root) { List res = new ArrayList(); if (root == null) { return res; }

        Deque<TreeNode> stack = new LinkedList<TreeNode>();
        TreeNode prev = null;
        while (root != null || !stack.isEmpty()) {
            while (root != null) {
                stack.push(root);
                root = root.left;
            }
            root = stack.pop();
            if (root.right == null || root.right == prev) {
                res.add(root.val);
                prev = root;
                root = null;
            } else {
                stack.push(root);
                root = root.right;
            }
        }
        return res;
    }
    

    }

2.6.4,层序遍历

力扣地址:leetcode-cn.com/problems/bi…

  • 从上到下,从左到右依次访问每一个节点

    public void levelOrderTraversal() {        
        if (root == null) return;        
        //创建队列,先进先出        
        Queue<Node<E>> queue = new LinkedList<>();
        //加入队列
    
        queue.offer(root);        
        //队列不为空,就循环        
        while (!queue.isEmpty()) {
            //弹出对头            
            Node<E> node = queue.poll();            
            System.out.println(node.element);            
            //node左子节点不为null,左子节点入队            
            if (node.left != null) {                
                queue.offer(node.left);            
            }            
            //node右子节点不为null,右子节点入队            
            if (node.right != null) {                
                queue.offer(node.right);            
            }        
        }    
    }
    

3,二叉搜索树

3.1,思考:在n个动态数组的整数中搜索某个整数,查看其是否存在?

  1. 假设使用动态数组存放元素,从第0个位置开始遍历搜索,平均时间复杂度为:O(n)
  2. 如果维护一个有序的动态数组,使用二分搜索,最坏时间复杂度为:O(logn)
  3. 但是添加,删除的平均时间复杂是O(n)
  4. 针对这个需求,我们可以使用二叉搜索树进行优化

3.2,什么是二叉搜索树

  1. 二叉搜索树是二叉树的一种,应用非常广泛的一种二叉树,又称为:二叉查找树,二叉排序树
  2. 任意一个节点的值都大于其左子树所有节点的值
  3. 任意一个节点的值都小于其右子树所有节点的值
  4. 它的左右子树也是一棵二叉树
  5. 二叉搜索树可以大大提高搜索数据的效率

3.3,二叉搜索树的接口设计和实现

3.3.1,添加节点

  1. 找到父节点

  2. 创建新的节点

  3. parent.left = node 或者 parent.right = node

//添加节点
    public void add(E element) {        
        elementNotNullCheck(element);                
        // 添加第一个节点        
        if (root == null) {            
            root = new Node<>(element, null);            
            size++;            
            return;        
         }                
        // 添加的不是第一个节点        
        // 找到父节点        
        Node<E> parent = root;        
        Node<E> node = root;        
        int cmp = 0;        
        do {            
            //添加元素和节点元素比较            
            cmp = compare(element, node.element);            
            parent = node;            
            //大于0,放入右节点            
            if (cmp > 0) {                
                node = node.right;            
            } else if (cmp < 0) {//小于0,放入左节点                
                node = node.left;            
            } else { // 相等                
                node.element = element;                
                return;            
            }        
        } while (node != null);
            // 看看插入到父节点的哪个位置        
            Node<E> newNode = new Node<>(element, parent);        
            if (cmp > 0) {//大于0,放入父节点的右节点            
                parent.right = newNode;        
            } else {//放入父节点的左节点            
                parent.left = newNode;        
            }        
            size++;    
    }

3.3.2,删除节点

  1. 删除叶子节点:

node == node.parent.left , node.parent.left = null

node == node.parent.right , node.parent.right = null

node.parent == null , root = null

  1. 删除度为1的节点

  • 用子节点代替原节点的位置

child是node.left 或者 child 是 node.right

       用child代替node的位置

  • 如果node为左子节点

child.parent = node.parent

node.parent.left = child

  • 如果node为右子节点

child.parent = node.parent

node.parent.right = child

  • 如果是根节点

root = child

child.parent = null

3.  删除度为2的节点

先用前驱或者后继节点的值覆盖原节点的值

然后,删除相应的前驱或者后继节点

    //删除节点    
    private void remove(Node<E> node) {        
        if (node == null) return;                
        size--;                
        if (node.hasTwoChildren()) { // 度为2的节点            
            // 找到后继节点            
            Node<E> s = successor(node);            
            // 用后继节点的值覆盖度为2的节点的值            
            node.element = s.element;            
            // 删除后继节点            
            node = s;        
        }               
         // 删除node节点(node的度必然是1或者0)        
         Node<E> replacement = node.left != null ? node.left : node.right;                
        if (replacement != null) { // node是度为1的节点            
            // 更改parent            
            replacement.parent = node.parent;            
            // 更改parent的left、right的指向            
            if (node.parent == null) { // node是度为1的节点并且是根节点                
                root = replacement;            
            } else if (node == node.parent.left) {                
                node.parent.left = replacement;            
            } else { // node == node.parent.right                
                node.parent.right = replacement;            
            }        
        } else if (node.parent == null) { // node是叶子节点并且是根节点            
            root = null;        
        } else { // node是叶子节点,但不是根节点            
            if (node == node.parent.left) {                
                node.parent.left = null;            
            } else { // node == node.parent.right                
                node.parent.right = null;            
            }        
        }    
}

4,平衡二叉搜索树

  1. 平衡二叉树其实就是对,一棵普通的二叉树的左右子树的高度进行平衡
  2. 当节点的数量固定时,左右子树的高度越接近,这棵二叉树就越平衡
  3. 最理想的平衡:像完全二叉树,满二叉树那样,高度是最小的

4.1,如何改进二叉搜索树?

  1. 首先,节点的添加、删除顺序是无法限制的,可以认为是随机的
  2. 所以在节点的添加,删除操作之后,想办法让二叉搜索树恢复平衡
  3. 如果接着继续调整节点的位置,完全可以达到理想平衡,但是付出的代价可能会比较大
  4. 比较合理的改进方案是:用尽量少的调整次数达到适度平衡即可
  5. 一棵达到适度平衡的二叉树,可以称之为:平衡二叉搜索树
  6. 经典常见的平衡二叉搜索树有:AVL树,红黑树