数据结构(Java版) - 二叉树

260 阅读4分钟

Github仓库: JavaDataStructure: Java 版数据结构 (github.com)

3.二叉树

3.1 二叉树的性质

性质1: 在二叉树的第 ii 层上至多有 2i1(i1)2^{i-1}(i \ge 1)个结点

性质2: 深度为 kk 的二叉树至多有 2k1(k1)2^k-1(k \ge 1)个结点

证明: 根据性质1,可知:1、2、4 ... 2k12^{k-1}

根据等比求和公式可知:

Sn=a11qn1q=1×12k12=2k1S_n=a_1 \frac{1-q^n}{1-q}=1 \times \frac{1-2^k}{1-2}=2^k - 1

性质3: 对任何一棵二叉树 TT,如果其终端结点数位 n0n_0 ,度为2的结点数为 n2n_2 ,则 n0=n2+1n_0=n_2+1

证明:

n=n0+n1+n2(公式1n=n_0+n_1+n_2 (公式1)

令分支总数为 BB,可知 B=n1B=n-1(除根节点外,所有结点都有一个分支进入)

B=n1+2n2=n1n=n1+2n2+1(公式2B = n_1 + 2n_2 = n-1 \rightarrow n = n_1 + 2n_2 + 1(公式2)

由公式1和公式2,可得

n0=n2+1n_0=n_2+1

满二叉树: 深度为 kk 且含有 2k12^k-1 个结点的二叉树

满二叉树的特点:每一层上的结点数都是最大结点数,即每一层 ii 的结点数都具有最大值 2i12^{i-1}

完全二叉树: 深度为 kk 的,有 nn 个结点的二叉树,当且仅当其每一个结点都与深度为 kk 的满二叉树中编号为 11nn 的结点一一对应时,称之为完全二叉树。

完全二叉树的特点:叶子结点只可能在层次最大的两层上出现;对任一结点,若其右分支下的子孙的最大层次为 ll,则其左分支下的子孙的最大层次必为 lll+1l+1

性质4: 具有 nn 个结点的完全二叉树的深度为 log2n+1\lfloor \log_2n \rfloor+1

性质5: 如果对一棵有 nn 个结点的完全二叉树(其深度为 log2n+1\lfloor \log_2n \rfloor+1 )的结点按层序编号(从第 11 层到第 log2n+1\lfloor \log_2n \rfloor+1 层,每层从左到右),则对任一结点 i1ini(1 \le i \le n),有

(1)如果 i=1i=1,则结点 ii 是二叉树的根,无双亲;如果 i>1i \gt 1,则 ii 的双亲结点是结点 i2\lfloor \frac{i}{2} \rfloor

(2)如果 2i>n2i \gt n ,则结点 ii 无左孩子(结点 ii 为叶子结点);否则其左孩子是结点 2i2i

(3)如果 2i+1>n2i+1 \gt n,则结点 ii 无右孩子;否则其右孩子是结点 2i+12i+1

3.2 二叉树的存储结构

1.顺序存储结构

数据结构-二叉树.png

顺序存储结构适用于完全二叉树。对于一般二叉树,在最坏的情况下,一个深度为 hh 且只有 hh 个结点的单支树却需要长度为 2k12^k-1的一维数组。这造成空间的极大浪费,所以对于一般二叉树,更适合链式存储结构。

2.链式存储结构

数据结构-二叉树2.png

数据结构-二叉树3.png

3.3 遍历二叉树

3.3.1 先序遍历

// 中左右
public void preOrder(TreeNode root){
    if(root == null){
        return;
    }
    System.out.print(root.value + " ");
    preOrder(root.left);
    preOrder(root.right);
}

public void preOrderUnRecur(TreeNode root) {
    if(root == null){
        return;
    }
    Stack<TreeNode> stack = new Stack<>();
    TreeNode cur = root;
    while(!stack.isEmpty() || cur!=null){
        if(cur != null){
            System.out.print(cur.value + " ");
            stack.push(cur);
            cur = cur.left;
        }else{
            cur = stack.pop();
            cur = cur.right;
        }
    }
}

3.3.2 中序遍历

// 左中右
public void inOrder(TreeNode root){
    if(root == null){
        return;
    }
    inOrder(root.left);
    System.out.print(root.value + " ");
    inOrder(root.right);
}

public void inOrderUnRecur(TreeNode root){
    if(root == null){
        return;
    }
    Stack<TreeNode> stack = new Stack<>();
    TreeNode cur = root;
    while(!stack.isEmpty() || cur!=null){
        if(cur != null){
            stack.push(cur);
            cur = cur.left;
        }else{
            cur = stack.pop();
            System.out.print(cur.value + " ");
            cur = cur.right;
        }
    }
}

3.3.3 后序遍历

// 左右中
public void postOrder(TreeNode root){
    if(root == null){
        return;
    }
    postOrder(root.left);
    postOrder(root.right);
    System.out.print(root.value + " ");
}

public List<Integer> postorderTraversal(TreeNode root) {
    List<Integer> ans = new ArrayList<>();
    if(root==null) return ans;
    Stack<TreeNode> stack = new Stack<>();
    TreeNode cur = root;
    //中右左
    while(!stack.isEmpty() || cur!=null){
        if(cur!=null){
            ans.add(cur.val);
            stack.push(cur);
            cur = cur.right;
        }else{
            cur = stack.pop();
            cur = cur.left;
        }
    }
    //左右中
    Collections.reverse(ans);
    return ans;
}

3.3.4 Morris遍历

一般的遍历方法通过递归或栈实现,平均空间复杂度为 O(log2n)O(log_2n);最差情况达到 O(n)O(n)。Morris(莫里斯)遍历方法把空间复杂度优化到 O(1)O(1) 的二叉树遍历算法。

/**
 * 假设当前节点为cur,并且开始时赋值为根节点root(其实也可以直接对root操作)。
 * 循环判断cur不为空
 * if左子树为空,则cur向右孩子移动
 * if左子树不为空,找到左子树的最右节点(也就是左子树一直往右走的,走到到尽头就是它的最右节点)
 * 如果最右节点的右指针为空,则把右指针指向cur,cur向左孩子移动
 * 如果最右节点的右指针为cur本身,则说明已经访问过一遍了,我们将其置为null,cur向右孩子移动
 * cur为空时,退出循环
 *
 * @param root
 */
public static void morris(TreeNode root) {
    if (root == null) {
        return;
    }
    TreeNode cur = root;
    TreeNode mostRight = null;
    while (cur != null) {
        mostRight = cur.left;
        if (mostRight != null) {
            // 找到cur左子树上最右节点
            while (mostRight.right != null && mostRight.right != cur) {
                mostRight = mostRight.right;
            }
            if (mostRight.right == null) {
                // 此时说明节点还未遍历完,所以指向cur
                mostRight.right = cur;
                cur = cur.left;
                // 回到最外层的 while,继续判断 cur 的情况
                continue;
            } else {
                // mostRight.right 指向 cur
                mostRight.right = null;
            }
        }
        // cur 如果没有左子树,cur 向右移动
        // 或者 cur 左子树上最右节点的右指针是指向 cur 的,cur 向右移动
        cur = cur.right;
    }
}

3.3.4.1 Morris 先序

例子:

数据结构-二叉树12.png

数据结构-二叉树13.png

数据结构-二叉树14.png

数据结构-二叉树15.png

/**
 * 根据 Morris 遍历,加工出先序遍历
 * 1.对于 cur 只能到达一次的节点(无左子树的节点),cur 到达时直接打印
 * 2.对于 cur 可以到达两次的节点(有左子树的节点),cur 第一次到达时打印,第二次到达时不打印
 *
 * @param root
 */
public void morrisPre(TreeNode root) {
    if (root == null) {
        return;
    }
    TreeNode cur = root;
    TreeNode mostRight = null;
    while (cur != null) {
        mostRight = cur.left;
        if (mostRight != null) {
            while (mostRight.right != null && mostRight.right != cur) {
                mostRight = mostRight.right;
            }
            if (mostRight.right == null) {
                mostRight.right = cur;
                System.out.print(cur.val + " ");
                cur = cur.left;
                continue;
            } else {
                mostRight.right = null;
            }
        } else {
            System.out.print(cur.val + " ");
        }
        cur = cur.right;
    }
    System.out.println();
}

3.3.4.2 Morris 中序

/**
 * 根据 Morris 遍历,加工出中序遍历
 * 1.对于 cur 只能到达一次的节点(无左子树的节点),cur 到达时直接打印
 * 2.对于 cur 可以到达两次的节点(有左子树的节点),cur 第一次到达时不打印,第二次到达时打印
 *
 * @param root
 */
public void morrisIn(TreeNode root) {
    if (root == null) {
        return;
    }
    TreeNode cur = root;
    TreeNode mostRight = null;
    while (cur != null) {
        mostRight = cur.left;
        if (mostRight != null) {
            while (mostRight.right != null && mostRight.right != cur) {
                mostRight = mostRight.right;
            }
            if (mostRight.right == null) {
                mostRight.right = cur;
                cur = cur.left;
                continue;
            } else {
                mostRight.right = null;
            }
        }
        System.out.print(cur.val + " ");
        cur = cur.right;
    }
    System.out.println();
}

3.3.4.3 Morris 后序

/**
 * 根据 Morris 遍历,加工出后序遍历
 * 1.对于 cur 只能到达一次的节点(无左子树的节点),直接跳过,没有打印行为
 * 2.对于 cur 可以到达两次的任何一个节点(有左子树的节点)X,cur 第一次到达 X 时没有打印行为;当第二次到达 X 时,逆序打印 X 左子树的右边界
 * 3.cur 遍历完成后,逆序打印整棵树的右边界
 *
 * @param root
 */
public void morrisPos(TreeNode root) {
    if (root == null) {
        return;
    }
    TreeNode cur = root;
    TreeNode mostRight = null;
    while (cur != null) {
        mostRight = cur.left;
        if (mostRight != null) {
            while (mostRight.right != null && mostRight.right != cur) {
                mostRight = mostRight.right;
            }
            if (mostRight.right == null) {
                mostRight.right = cur;
                cur = cur.left;
                continue;
            } else {
                mostRight.right = null;
                printEdge(cur.left);
            }
        }
        cur = cur.right;
    }
    printEdge(root);
    System.out.println();
}

// 逆序打印子树的右边界
public static void printEdge(TreeNode from) {
    TreeNode tail = reverseEdge(from);
    TreeNode cur = tail;
    while (cur != null) {
        System.out.print(cur.val + " ");
        cur = cur.right;
    }
    reverseEdge(tail);
}

// 子树的右边界就相当于一个单链表,通过节点的 right 指针相连
public static TreeNode reverseEdge(TreeNode from) {
    TreeNode pre = null;
    TreeNode next = null;
    while (from != null) {
        next = from.right;
        from.right = pre;
        pre = from;
        from = next;
    }
    return pre;
}

3.4 线索二叉树

由于有 nn 个结点的二叉链表中必定存在 n+1n+1 个空链域,因此可以充分利用这些空链域来存放结点的前驱和后继信息。

有如下规定:

  • 若结点有左子树,则其 lchildlchild 域指示其左孩子,否则令 lchildlchild 域指示其前驱
  • 若结点有右子树,则其 rchildrchild 域指示其右孩子,否则令 rchildrchild 域指示其后继

数据结构-二叉树4.png

其中:

LTag={0lchild域指示结点的左孩子1lchild域指示结点的前驱  RTag={0rchild域指示结点的右孩子1rchild域指示结点的后继LTag= \begin{cases} 0& lchild域指示结点的左孩子\\ 1& lchild域指示结点的前驱 \end{cases} \\\ \\\ RTag= \begin{cases} 0& rchild域指示结点的右孩子\\ 1& rchild域指示结点的后继 \end{cases}

数据结构-二叉树5.png

public class ThrTree {

    // 全局变量,指示遍历过程中的线索二叉树的前驱结点信息
    private BiThrNode preBN = null;
    // 全局变量,指示遍历过程中的二叉树的前驱结点信息
    private TreeNode preTN = null;

    // 头结点
    private BiThrNode thr;

    public ThrTree() {
        // 建立头结点
        thr = new BiThrNode();
    }

    /**
     * 中序线索化
     */
    BiThrNode InThreading(TreeNode root){
        if(root == null) return null;
        BiThrNode node = new BiThrNode(root.val);

        // 左子树递归线索化
        node.left = InThreading(root.left);

        // 左孩子为空时,加上左线索
        if(root.left == null){
            node.LTag = 1;
            node.left = preBN;
        }else{
            node.LTag = 0;
        }
        if(preTN!=null){
            // pre 右孩子为空时,加上右线索
            if(preTN.right == null){
                preBN.RTag = 1;
                preBN.right = node;
            }else{
                preBN.RTag = 0;
            }
        }
        preBN = node;
        preTN = root;

        // 右子树递归线索化
        node.right = InThreading(root.right);

        return node;
    }

    /**
     * 带头结点的二叉树中序线索化
     */
    void InOrderThreading(TreeNode root){
        // 头结点有左孩子,若树非空,则其左孩子为树根
        thr.LTag = 0;
        // 头结点的右孩子指针为右线索
        thr.RTag = 1;
        // 初始化时,右指针指向自己
        thr.right = thr;
        if(root == null){
            // 若树为空,则左指针也指向自己
            thr.left = thr;
        }else{
            // pre 初值指向头结点
            preBN = thr;
            // 头结点的左孩子指向
            thr.left = InThreading(root);

            // pre 为最右结点,pre 的右线索指向头结点
            preBN.right = thr;
            preBN.RTag = 1;
            // 头结点的右线索指向 pre
            thr.right = preBN;
        }
    }

    /**
     * 遍历中序线索二叉树
     */
     void InOrderTraverse_Thr(){
        BiThrNode p = thr.left;
        while (p != thr){
            // 沿左孩子向下
            while (p.LTag == 0){
                p = p.left;
            }
            // 访问其左孩子为空的结点
            System.out.println(p.val);
            // 沿右线索访问后继结点
            while (p.RTag==1 && p.right!=thr){
                p = p.right;
                System.out.println(p.val);
            }
            // 转向右子树
            p = p.right;
        }
    }
}

/**
 * 线索二叉树节点
 */
class BiThrNode{
    int val;
    BiThrNode left;
    BiThrNode right;
    int LTag,RTag;

    public BiThrNode() {
    }

    public BiThrNode(int val) {
        this.val = val;
    }
}

3.5 树和森林

3.5.1 树的存储结构

1.双亲表示法

数据结构-二叉树6.png

2.孩子表示法

数据结构-二叉树7.png

数据结构-二叉树8.png

3.孩子兄弟法

数据结构-二叉树9.png

3.5.2 森林与二叉树的转换

转换规则如下:

  • 兄弟结点相互连接,结点右边的兄弟结点作为该结点的右孩子;父结点的第一个孩子作为其左孩子
  • 树转换为二叉树后;将二叉树B的根节点作为二叉树A的右孩子,两两相连

数据结构-二叉树10.png

3.6 哈夫曼树

3.6.1 哈夫曼树基本概念

哈夫曼树又称最优树,是一类带权路径长度最短的树,在实际中有广泛的用途。

  1. 路径::从树中一个结点到另一个结点所经过的分支构成这两个结点之间的路径
  2. 路径长度:路径上的分支数目
  3. 树的路径长度:从树根到每一结点的路径长度之和
  4. :对结点某一属性的数值化表示
  5. 结点的带权路径长度:从该结点到树根之间的路径长度与结点上权的乘积
  6. 树的带权路径长度:树中所有叶子结点的带权路径长度之和

3.6.2 哈夫曼树性质

  1. 哈夫曼编码是前缀编码

前缀编码 是指对字符集进行编码时,要求字符集中任一字符的编码都不是其它字符的编码的前缀。
例如:设有abcd需要编码表示(其中,a=0、b=10、c=110、d=11,则110的前缀表示的可以是c或者是d跟a,出现这种情况是因为d的前缀11与c的前缀110有重合部分,这个是关键。)
来源---------------百度词条

  1. 哈夫曼编码是最优前缀编码

对于包括n个字符的数据文件,分别以它们的出现次数为权值构造哈夫曼树,则利用该树对应的哈夫曼编码对文件进行编码,能使该文件压缩后对应的二进制文件的长度最短。

3.6.3 哈夫曼树构造

  1. 根据给定的 nn 个权值 w1,w2,,wn{w_1,w_2,\dots,w_n},构造 nn 棵只有根节点的二叉树,这 nn 棵二叉树构成一个森林 FF
  2. 在森林 FF 中选取两棵权值最小的树作为左右子树构造一棵新的二叉树,新二叉树的根节点权值为左右子树的根节点权值之和
  3. 在森林 FF 中删除这两棵树,同时将新二叉树加入 FF
  4. 重复 (2) 和 (3),直到 FF 只含一棵树为止

数据结构-二叉树11.png

3.6.4 哈夫曼树实现

public class HuffmanTree {

    private HuffmanNode root;
    private Map<String,String> nodeCode;

    public HuffmanTree() {
        this.root = null;
        this.nodeCode = new HashMap<>();
    }

    /**
     * 构造哈夫曼树
     * @param nodes name -> weight
     */
    public void create(Map<String,Integer> nodes){
        PriorityQueue<HuffmanNode> pq = new PriorityQueue<>((a,b) -> a.weight-b.weight);
        for(Map.Entry<String,Integer> entry: nodes.entrySet()){
            pq.offer(new HuffmanNode(entry.getKey(),entry.getValue()));
        }

        // 构造哈夫曼树
        while(!pq.isEmpty() && pq.size()>1){
            // 出队权值最小的两个结点
            HuffmanNode child = pq.poll();
            HuffmanNode child2 = pq.poll();
            HuffmanNode parent = new HuffmanNode(child.weight+child2.weight);
            parent.left = child;
            parent.right = child2;
            pq.offer(parent);
        }

        root = pq.poll();
    }

    /**
     * 获取哈夫曼编码
     * @return name -> 哈夫曼编码
     */
    public Map<String,String> getHuffmanCode(){
        if(nodeCode.isEmpty()){
            preorder(root,null,new StringBuilder());
        }
        return nodeCode;
    }

    /**
     * 先序遍历哈夫曼树,并构造哈夫曼编码
     */
    private void preorder(HuffmanNode node,HuffmanNode pre,StringBuilder builder){
        if(node == null) return;
        if(pre != null){
            if(pre.left == node) builder.append("0");
            if(pre.right == node) builder.append("1");
        }
        if(node.left==null && node.right==null){
            nodeCode.put(node.name,builder.toString());
            return;
        }
        preorder(node.left,node,builder);
        builder.deleteCharAt(builder.length()-1);
        preorder(node.right,node,builder);
        builder.deleteCharAt(builder.length()-1);
    }

}

class HuffmanNode{
    String name;
    int weight;
    HuffmanNode left;
    HuffmanNode right;

    public HuffmanNode() {
    }

    public HuffmanNode(int weight) {
        this.weight = weight;
        this.name = null;
        this.left = null;
        this.right = null;
    }

    public HuffmanNode(String name, int weight) {
        this.name = name;
        this.weight = weight;
        this.left = null;
        this.right = null;
    }
}