前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >「周末福报」如何实现一棵二叉树?

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

作者头像
FoamValue
发布2020-09-01 15:59:24
4120
发布2020-09-01 15:59:24
举报
文章被收录于专栏:FoamValue

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

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

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

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


正片开始

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

存储结构

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

图 A 二叉树

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

抽象数据类型 Abstract Data Type

需要实现的二叉树功能。

代码语言:javascript
复制
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 定义类。

代码语言:javascript
复制
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 定义类。

代码语言:javascript
复制
public MyBinaryNode<T> root;

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

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

插入结点

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

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

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

代码语言:javascript
复制
/**
 * 插入元素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,还要约定如何调整整个子树结构的规则。(涉及后续左、右子树旋转的情况,这里不做展开)

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

代码语言:javascript
复制
/**
 * 删除 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;
}

先根次序遍历(递归版)

代码语言:javascript
复制
/**
 * 输出先根次序遍历序列
 */
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);
  }
}

中根次序遍历(递归版)

代码语言:javascript
复制
/**
 * 输出中根次序遍历序列
 */
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);
  }
}

后根次序遍历(递归版)

代码语言:javascript
复制
/**
 * 输出后根次序遍历序列
 */
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 都没有链结构,也就是说二叉树本身的链结构是无法满足需求的,必须设立辅助的数据结构,指出下一个访问结点是谁。即 “先进先出” 特点的队列。

代码语言:javascript
复制
/**
 * 输出层次遍历序列
 */
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();
}

结点个数

代码语言:javascript
复制
/**
 * 二叉树的结点个数
 * 
 * @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;
}

二叉树的高度

代码语言:javascript
复制
/**
 * 二叉树的高度
 * 
 * @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

代码语言:javascript
复制
/**
 * 查找并返回关键字为 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;
}

测试类

代码语言:javascript
复制
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"));
  }

}

测试结果

代码语言:javascript
复制
先根次序遍历: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. 对递归理解有困难的同学,可以看历史文章《递归算法》

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

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

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2020-07-18,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 FoamValue 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 正片开始
  • 徒手开撕二叉树之前,了解玩基础知识之后,还需要了解下它的存储结构、抽象数据类型。
    • 存储结构
      • 插入结点
        • 删除结点
          • 层次遍历
          领券
          问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档