「周末福报」如何实现一棵二叉树?

时间:2022-07-24
本文章向大家介绍「周末福报」如何实现一棵二叉树?,主要内容包括其使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。

人最大的对手,往往不是别人,而是自己的懒惰。

昨天,生拉硬拽的更新一期「学习笔记」;

让我们重新回到课堂学习“树和二叉树”的基础知识;

今天,从零开始写一个 Java 版本的二叉树。


正片开始

徒手开撕二叉树之前,了解玩基础知识之后,还需要了解下它的存储结构、抽象数据类型。

存储结构

二叉树通常采用链式存储结构,每个结点至少要有两条链分别链接左、右孩子结点,才能表达二叉树的层次关系。

图 A 二叉树

每个结点元素都包含三个元素:数据 data、左孩子 left,右孩子 right。

抽象数据类型 Abstract Data Type

需要实现的二叉树功能。

MyBinaryTree {
    boolean isEmpty(); // 是否为空二叉树
    int size(); // 二叉树的结点个数
    int height(); // 二叉树的高度
    void preorder(); // 输出先根次序遍历序列
    void inorder(); // 输出中根次序遍历序列
    void postorder(); // 输出后根次序遍历序列
    void levelorder(); // 输出层次遍历序列
    MyBinaryNode insert(T x); // 插入元素x作为根结点并返回
    MyBinaryNode insert(MyBinaryNode p, T x, boolean leftChild); // 插入x作为p的左/右孩子
    void remove(MyBinaryNode parent, boolean leftChild); // 删除 parent 结点的左/右孩子
    void clear(); // 删除二叉树的所有结点
    MyBinaryNode search(T key); // 查找并返回关键字为 Key 的结点
    boolean contains(T key); // 判断是否包含关键字为 key 的元素
    int level(T key); // 返回关键字为 key 结点所在的层次
}

以上的功能基本可以涵盖二叉树的完整使用。

为了跟可能存在的自带类做区分,结点类命名为 MyBinaryNode,二叉树类命名为 MyBinaryTree。


二叉树实现

MyBinaryNode 结点类

为了支持各种数据都能使用,采用泛型 T 定义类。

public class MyBinaryNode<T> {
  public T data; // 数据
  public MyBinaryNode<T> left, right; // 左孩子、右孩子结点

  // 构造结点
  public MyBinaryNode(T data, MyBinaryNode<T> left, MyBinaryNode<T> right) {
    this.data = data;
    this.left = left;
    this.right = right;
  }

  public MyBinaryNode(T data) {
    this.data = data;
  }

  public String toString() {
    return data.toString();
  }
}

MyBinaryTree 二叉树类

为了支持各种数据都能使用,采用泛型 T 定义类。

public MyBinaryNode<T> root;

// 默认构造器
public MyBinaryTree() {
  root = null;
}

/**
 * 是否为空二叉树
 * 
 * @return
 */
public boolean isEmpty() {
  return root == null;
}

插入结点

在二叉树中插入结点会存在两种插入情况。

当创建X结点作为 parent 的左孩子,parent 的原左孩子作为X结点的左孩子。

当创建Y结点作为 parent 的右孩子,parent 的原右孩子作为Y结点的右孩子。

/**
 * 插入元素x作为根结点并返回
 * 
 * @param x
 * @return
 */
public MyBinaryNode<T> insert(T x) {
  return this.root = new MyBinaryNode<T>(x, this.root, null);
}

/**
 * 插入x作为p的左/右孩子
 * 
 * @param p
 * @param x
 * @param leftChild
 * @return
 */
public MyBinaryNode<T> insert(MyBinaryNode<T> parent, T x, boolean leftChild) {
  if (x == null)
    return null;
  if (leftChild)
    return parent.left = new MyBinaryNode<T>(x, parent.left, null);
  else
    return parent.right = new MyBinaryNode<T>(x, null, parent.right);
}

删除结点

在二叉树中删除一个结点,不仅要修改其父母结点的 left 或 right,还要约定如何调整整个子树结构的规则。(涉及后续左、右子树旋转的情况,这里不做展开)

这里直接删除结点,以及它的左、右子树。

/**
 * 删除 parent 结点的左/右孩子
 * 
 * @param parent
 * @param leftChild
 */
public void remove(MyBinaryNode<T> parent, boolean leftChild) {
  if (parent == null)
    throw new NullPointerException();
  if (leftChild)
    parent.left = null;
  else
    parent.right = null;
}

/**
 * 删除二叉树的所有结点
 */
public void clear() {
  this.root = null;
}

先根次序遍历(递归版)

/**
 * 输出先根次序遍历序列
 */
public void preorder() {
  preorder(this.root);
  System.out.println();
}

private void preorder(MyBinaryNode<T> p) {
  if (p != null) {
    System.out.print(p.data.toString() + " ");
    // 按先根次序遍历 p 的左子树
    preorder(p.left);
    // 按先根次序遍历 p 的右子树
    preorder(p.right);
  }
}

中根次序遍历(递归版)

/**
 * 输出中根次序遍历序列
 */
public void inorder() {
  inorder(this.root);
  System.out.println();
}

private void inorder(MyBinaryNode<T> p) {
  if (p != null) {
    inorder(p.left);
    System.out.print(p.data.toString() + " ");
    inorder(p.right);
  }
}

后根次序遍历(递归版)

/**
 * 输出后根次序遍历序列
 */
public void postorder() {
  postorder(this.root);
  System.out.println();
}

private void postorder(MyBinaryNode<T> p) {
  if (p != null) {
    postorder(p.left);
    postorder(p.right);
    System.out.print(p.data.toString() + " ");
  }
}

层次遍历

在二叉树中的层次遍历,指的是兄弟优先。

也就是两个兄弟结点的访问次序是先左后右,连续访问。左兄弟的孩子一定在右兄弟的所有孩子之前访问,左兄弟的后代结点一定在右兄弟的同层后代结点之前访问。

(图 A 中)从 B 到 C,从 C 到 D,从 E 到 F 都没有链结构,也就是说二叉树本身的链结构是无法满足需求的,必须设立辅助的数据结构,指出下一个访问结点是谁。即 “先进先出” 特点的队列。

/**
 * 输出层次遍历序列
 */
public void levelorder() {
  System.out.println("层次遍历:");
  ConcurrentLinkedQueue<MyBinaryNode<T>> que = new ConcurrentLinkedQueue<MyBinaryNode<T>>();
  MyBinaryNode<T> p = this.root;
  while(p != null) {
    System.out.print(p.data + " ");
    if (p.left != null)
        que.add(p.left);
    if(p.right != null)
        que.add(p.right);
    p = que.poll();
  }
  System.out.println();
}

结点个数

/**
 * 二叉树的结点个数
 * 
 * @return
 */
public int size() {
  return size(this.root);
}

private int size(MyBinaryNode<T> p) {
  if (p == null)
    return 0;
  // 后根遍历
  int l = size(p.left);
  int r = size(p.right);
  return l + r + 1;
}

二叉树的高度

/**
 * 二叉树的高度
 * 
 * @return
 */
public int height() {
  return height(this.root);
}

private int height(MyBinaryNode<T> p) {
  if (p == null)
    return 0;
  int l = height(p.left);
  int r = height(p.right);
  if (l == 0 && r == 0)
    return 1;
  if (l > r)
    return ++l;
  else
    return ++r;
}

查询关键字 Key

/**
 * 查找并返回关键字为 Key 的结点
 * @param key
 * @return
 */
public MyBinaryNode<T> search(T key) {
  return search(this.root, key);
}

private MyBinaryNode<T> search(MyBinaryNode<T> p, T key) {
  if (p == null)
    return null;
  if (p.data.equals(key))
    return p;
  MyBinaryNode<T> l = search(p.left, key);
  if (l != null)
    return l;
  MyBinaryNode<T> r = search(p.right, key);
  if (r != null)
    return r;
  return null;
}

/**
 * 判断是否包含关键字为 key 的元素
 * 
 * @param key
 * @return
 */
public boolean contains(T key) {
  return search(this.root, key) != null;
}

/**
 * 返回关键字为 key 结点所在的层次
 * 
 * @param key
 * @return
 */
public int level(T key) {
  return level(this.root, key);
}

private int level(MyBinaryNode<T> p, T key) {
  if (p == null)
    return 0;
  if (p.data.equals(key)) return 1;
  int l = level(p.left, key);
  int r = level(p.right, key);
  if (l == 0 && r == 0)
    return 0;
  if (l > r)
    return ++l;
  else
    return ++r;
}

测试类

public class MyBinaryTreeTest {

  @SuppressWarnings("unused")
  public static void initTree(MyBinaryTree<String> tree) {
    tree.clear();
    MyBinaryNode<String> root = tree.insert("A");

    MyBinaryNode<String> treeB = tree.insert(root, "B", true);
    MyBinaryNode<String> treeC = tree.insert(root, "C", false);
    MyBinaryNode<String> treeD = tree.insert(treeB, "D", true);
    MyBinaryNode<String> treeE = tree.insert(treeC, "E", true);
    MyBinaryNode<String> treeF = tree.insert(treeC, "F", false);
    MyBinaryNode<String> treeG = tree.insert(treeD, "G", false);
    MyBinaryNode<String> treeH = tree.insert(treeF, "H", true);
  }

  public static void main(String[] args) {

    MyBinaryTree<String> tree = new MyBinaryTree<>();
    initTree(tree);

    System.out.print("先根次序遍历:");
    tree.preorder();

    System.out.print("中根次序遍历:");
    tree.inorder();

    System.out.print("后根次序遍历:");
    tree.postorder();

    System.out.println("结点个数:" + tree.size());
    System.out.println("树的高度:" + tree.height());
    tree.levelorder();
    System.out.println(tree.search("G"));
    System.out.println(tree.contains("G"));
    System.out.println(tree.contains("X"));
  }

}

测试结果

先根次序遍历:A B D G C E F H 
中根次序遍历:D G B A E C H F 
后根次序遍历:G D B E H F C A 
结点个数:8
树的高度:4
层次遍历:
A B C D E F G H 
G
true
false

小结

早上起来,满脑子都是树、二叉、左子树、右子树。。。

今天,咬咬牙从零开始完整的捋了捋二叉树的实现。

其实,二叉树还有很多高深的用法。常考题目 - 红黑树、无损压缩 - Huffman 树等等。

当然这些也在我的知识盲区列表之中,后续会好好学习、努力整理出来的。

PS. 对递归理解有困难的同学,可以看历史文章《递归算法》

这个周末,又一次成功“强迫”自己学习。

感谢各位小伙伴的阅读,这里是一个技术人的学习与分享。