今天是一年一度的植树节,说到树,我就想到了《西游记》中的古树精,今年下半年。。。好了好了,不皮了,咱们直接开花。今天就趁着植树节来种一棵咱们程序员的“树”吧。node
在种树以前,咱们先来了解下什么是树?看个例子:程序员
不对不对,放错了,应该是下面这个:算法
维基百科对于树的定义是:数据库
在计算机科学中,树(英语:tree)是一种抽象数据类型(ADT)或是实现这种抽象数据类型的数据结构,用来模拟具备树状结构性质的数据集合。它是由 n(n>0)个有限结点组成一个具备层次关系的集合。把它叫作“树”是由于它看起来像一棵倒挂的树,也就是说它是根朝上,而叶朝下的。数组
说白了,只要是形如上图的数据结构就叫树。bash
天然界中有“迎客松”、“轩辕柏”这样出名的树,咱们今天要种的树也是咱们IT圈里面的扛把子——“二叉树”。数据结构
二叉树是数据结构中一种重要的数据结构,也是树表家族最为基础的结构之一。post
二叉树的特色是每一个结点最多有两个子树,左边的叫作左子树,右边的叫作右子树,二叉树要么为空,要么由根结点、左子树和右子树组成,而左子树和右子树分别是一棵二叉树。 下面这棵树就是一棵二叉树。学习
接下来的代码均用 Java 展示,完整代码可在公众号「01二进制」后台回复“二叉树”查看。
如今咱们就开始种一棵如上图的树吧。根据定义,咱们了解到,结点是一棵二叉树最重要的元素,而做为一个结点,必须知足如下条件:
所以咱们能够建立一个结点类(TreeNode):
class TreeNode {
String data;
TreeNode left;
TreeNode right;
TreeNode(String data) {
this.data = data;
}
}
复制代码
有了这个结点类以后咱们就能够建立出一个如上图的树了:
// 建立二叉树
private TreeNode createTree() {
TreeNode root = new TreeNode("A");
root.left = new TreeNode("B");
root.right = new TreeNode("C");
root.left.left = new TreeNode("D");
root.left.right = new TreeNode("E");
root.right.left = new TreeNode("F");
root.right.right = new TreeNode("G");
return root;
}
复制代码
你看,一棵树不就种好了吗?
做为一个新时代的好青年,咱们不能把树种好了就无论不顾了,得负起责任啊。最起码你要知道本身的树长啥样吧。因此接下来咱们就来看看如何获取树的特征。
咱们描述一我的的特征每每都是从他的外形、长相、身材入手的,描述一颗树也是如此,咱们接下来将会从下面几个角度去获取树的特征:
// 判断是否为空
public boolean isEmpty(TreeNode root) {
return root == null;
}
复制代码
这里咱们采用递归的方式,由于树的高度是由其子树决定的,因此咱们只须要比较左、右子树的高度而后取最大值便可,代码以下:
// 获取树的高度
private int height(TreeNode root) {
if (root == null)
return 0;//递归结束:空树高度为0
else {
int i = height(root.left);
int j = height(root.right);
return (i < j) ? (j + 1) : (i + 1);
}
}
复制代码
一个树的结点个数一定为其左子树的结点个数 + 右子树的结点个数 +1,所以咱们一样能够用递归很是简单的将其表示出来:
// 获取结点大小
private int size(TreeNode root) {
if (root == null) {
return 0;
} else {
return 1 + size(root.left) + size(root.right);
}
}
复制代码
上一节中咱们获取到了树的特征,但这远远不够,特征只能粗略的描述一个二叉树,想要详细的了解一个二叉树,咱们必须对其进行“遍历”。
遍历二叉树是指以必定的次序访问二叉树中的每一个结点。所谓访问结点是指对结点进行各类操做的简称(最简单的就是访问该结点的值)。
而访问结点无非就 3 个操做:
咱们以根结点为核心,若是先访问根节点在访问左右结点成为前序遍历;若是先访问左结点而后根结点最后右结点则为中序遍历;若最后访问根节点则为后序遍历。
所以上图的遍历结果为:
前序:A B D E C F G
中序:D B E A F C G
后序:D E B F G C A
复制代码
固然这只是咱们本身根据定义手写出来的,该如何用代码表示出来呢?
根据定义咱们知道,前序遍历就是先访问根结点,而后是左结点和右结点,所以用递归能够很简单的展示这一过程:
// 前序遍历二叉树
private void preOrder(TreeNode root) {
if (root != null) {
System.out.print(root.data + " ");
preOrder(root.left);
preOrder(root.right);
}
}
复制代码
同理咱们也能够很快的知道中序和后序遍历了:
// 中序遍历二叉树
private void inOrder(TreeNode root) {
if (root != null) {
inOrder(root.left);
System.out.print(root.data + " ");
inOrder(root.right);
}
}
复制代码
// 后序遍历二叉树
private void postOrder(TreeNode root) {
if (root != null) {
postOrder(root.left);
postOrder(root.right);
System.out.print(root.data + " ");
}
}
复制代码
这里遍历二叉树的代码均是以递归方法完成的,非递归遍历二叉树的过程较为麻烦,因为篇幅限制,这里就不放出来了。若想查看非递归版本的代码可在公众号「01二进制」后台回复“二叉树”查看。
事实上,以人来看一个树的话大多都是一层一层的看,这种遍历方式称为层序遍历。具体思路:用队列实现,先将根节点入队列,只要队列不为空,而后出队列,并访问,接着讲访问节点的左右子树依次入队列。
// 层序遍历
private void levelTravel(TreeNode root) {
if (root == null) return;
Queue<TreeNode> q = new LinkedList<TreeNode>();
q.add(root);
while (!q.isEmpty()) {
TreeNode temp = q.poll();
System.out.print(temp.data + " ");
if (temp.left != null) q.add(temp.left);
if (temp.right != null) q.add(temp.right);
}
}
复制代码
二叉树是种很是强大的数据结构,那她到底强大在哪里呢?咱们来看下面这个简单的例子:
该例来自于Aditya Bhargava 的《算法图解》
假如说,你想从微博中找到一我的,最快的方法通常是二分查找。但当有新用户增长时,都得将新用户插入组别内再排序,由于二分查找法只会有序的组别才有用。
因此就有人想了,若是能够将新增的用户插入到数组的正确位置就行了,这样就不须要在插入后在排序了。
因而就有人设计了一种二叉树:对于每一个结点,左子节点的值都比它小,右子节点值都比它大。以下图所示:
Maggie
排在David
后面,所以向右找Maggie
,排在Manning
前面,所以向左找。
这个运行时间,用大O表示法,平均运行时间是O(log2 N),最差运行时间是O(N)
在有序数组查找时,与二分查找法运行时间相同。
二叉树对比二分查找法优点在于<以下图>:
能够看出插入和删除速度都快。二叉树的缺点也很明显,就是不能随机访问。
上述例子其实就是一个二叉查找树的简易使用,那么除此以外二叉树还有什么常见的应用呢?下面列出四个,有兴趣的小伙伴能够本身搜索相关的文档阅读学习。
种一棵树最好的时间是十年前,然后是如今。咱们经常去后悔过去的事情。遗憾本身犯的错误,遗憾本身错过的机会。虽然现实很让人感到惋惜,但其实不少事情早就该作了,再懊恼又有什么用呢?与其无故抱怨还不如行动起来。当你感到遗憾时,才是行动的最好时机!