人最大的对手,往往不是别人,而是自己的懒惰。
昨天,生拉硬拽的更新一期「学习笔记」;
让我们重新回到课堂学习“树和二叉树”的基础知识;
今天,从零开始写一个 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. 对递归理解有困难的同学,可以看历史文章《递归算法》
这个周末,又一次成功“强迫”自己学习。
感谢各位小伙伴的阅读,这里是一个技术人的学习与分享。