你天天都那么努力,忍受了那么多的寂寞和痛苦。可我也没见你有多优秀。java
当我仍是一个年轻男孩的时候画的一张关于树的画。node
当你第一次学习编码时,大部分人都是将数组做为主要数据结构来学习。git
以后,你将会学习到哈希表。若是你是计算机专业的,你确定须要选修一门数据结构的课程。上课时,你又会学习到链表,队列和栈等数据结构。这些都被统称为线性的数据结构,由于它们在逻辑上都有起点和终点。github
当你开始学习树和图的数据结构时,你会以为它是如此的混乱。由于它的存储方式不是线性的,它们都有本身特定的方式存储数据。算法
这篇文章帮助你更好的理解树形数据结构并尽量的帮你解决你的疑问。本章咱们将学到编程
当学习编程时,咱们更容易理解线性的数据结构而不是树和图的数据结构。小程序
树是众所周知的非线性数据结构。它们不以线性方式存储数据。他们按层次组织数据。微信小程序
咱们所说的层次组织究竟是是什么呢?数组
想象一下咱们的家谱:祖父母,父母,子女,兄弟姐妹等等,咱们一般按层次结构组织家谱。微信
个人家庭族谱
上图是个人家谱。tossico,akikazu,hitomi和takemi是个人祖父母。
Toshiaki 和 Juliana 是个人父母。
TK 、Yuji 、Bruno 和 Kaio 是我父母的孩子(我和个人兄弟们)。
另外一个层次结构的例子是企业的组织结构。
公司的结构也是是一个层次结构的例子
在 HTML 中,文档对象模型(DOM)是树形结构的。
文档对象模型(dom)
HTML 标签包含其余的标签。咱们有一个 head 标签和 body 标签。这些标签包含特色的元素。head 标签中有 meta 和 title 标签。body 标签中有在用户界面展现的标签,如 h1 、a 、li 等等。
树(tree
)是被称为结点(node
)的实体的集合。结点经过边(edge
)链接。每一个结点都包含值或数据(value/date
),而且每结节点可能有也可能没有子结点。
树的首结点叫根结点(即root
结点)。若是这个根结点和其余结点所链接,那么根结点是父结点(parent node
,与根结点链接的是子结点(child node
)。
全部的结点都经过边(edge
)链接。它是树中很重要得一个概念,由于它负责管理节点之间的关系。
叶子结点(leaves
)是树末端,它们没有子结点。像真正的大树同样,咱们能够看到树上有根、枝干和树叶。
树的高度(height
)和深度(depth
)
如今咱们来讨论一个特殊的树类型。咱们把它叫做二叉树。
“在计算机科学领域,二叉树是一种树形数据结构,它的每一个节点最多有两个孩子,被叫做左孩子和右孩” — Wikipedia
当咱们要实现二叉树时,咱们须要牢记的第一件事是它是一个结点集合。每一个结点都有三个属性:value
,left_child``和right_child
。
那么咱们怎么才能实现一个有这三个属性的简单二叉树呢?
咱们来实现一个二叉树的例子
/** * Created on 2018/4/16. * * @author zlf * @since 1.0 */
public class BinaryTree {
public BinaryTree left; //左节点
public BinaryTree right; //右节点
public String data; //树的内容
public BinaryTree() {
}
/** * 构造方法 * * @param data * @param left * @param right */
public BinaryTree(String data, BinaryTree left, BinaryTree right) {
this.left = left;
this.right = right;
this.data = data;
}
/** * 构造方法 * * @param data */
public BinaryTree(String data) {
this(data, null, null);
}
复制代码
好,这就是咱们的二叉树类
当咱们实例化一个对象时,咱们把值(点的相关数据)做为参数传递给类。看上面类的左孩子结点和右孩子结点。两个都被赋值为null。
为何?
由于当咱们建立节点时,它尚未孩子,只有结点数据。
代码测试
/** * 构建树 */
public static void testCreate() {
BinaryTree node = new BinaryTree("a");
System.out.println("【node data】:" + node.getData());
System.out.println("【node left data】:" + (node.left==null?"null":node.left.getData()));
System.out.println("【node right data】:" + (node.right==null?"null":node.right.getData()));
}
复制代码
输出:
【node data】:a
【node left data】:null
【node right data】:null
复制代码
咱们能够将字符串'a'做为参数传给二叉树结点。若是将值、左孩子结点、右孩子结节点输出的话,咱们就能够看到这个值了。
下面开始插入部分的操做。那么咱们须要作些什么工做呢?
有两个要求:
若是当前的结点没有左孩子结点,咱们就建立一个新结点,而后将其设置为当前结点的左结点。
若是已经有了左结点,咱们就建立一个新结点,并将其放在当前左结点的位置。而后再将原左结点值为新左结点的左结点。
图形以下:
下面是插入的代码:
/** * 插入节点 ,若是当前的节点没有左节点,咱们就建立一个新节点,而后将其设置为当前节点的左节点。 * * @param node * @param value */
public static void insertLeft(BinaryTree node, String value) {
if (node != null) {
if (node.left == null) {
node.setLeft(new BinaryTree(value));
} else {
BinaryTree newNode = new BinaryTree(value);
newNode.left = node.left;
node.left = newNode;
}
}
}
复制代码
再次强调,若是当前结点没有左结点,咱们就建立一个新结点,并将其置为当前结点的左结点。不然,就将新结点放在左结点的位置,再将原左结点置为新左结点的左结点。
一样,咱们编写插入右结点的代码
/** * 同插入左结点 * @param node * @param value */
public static void insertRight(BinaryTree node, String value) {
if (node != null) {
if (node.right == null) {
node.setRight(new BinaryTree(value));
} else {
BinaryTree newNode = new BinaryTree(value);
newNode.right = node.right;
node.right = newNode;
}
}
}
复制代码
可是这还不算完成。咱们得测试一下。
咱们来构造一个像下面这样的树:
下面是这棵树的实现代码:
/** * 测试插入结点 */
public static void testInsert() {
BinaryTree node_a = new BinaryTree("a");
node_a.insertLeft(node_a, "b");
node_a.insertRight(node_a, "c");
BinaryTree node_b = node_a.left;
node_b.insertRight(node_b, "d");
BinaryTree node_c = node_a.right;
node_c.insertLeft(node_c, "e");
node_c.insertRight(node_c, "f");
BinaryTree node_d = node_b.right;
BinaryTree node_e = node_c.left;
BinaryTree node_f = node_c.right;
System.out.println("【node_a data】:" + node_a.getData());
System.out.println("【node_b data】:" + node_b.getData());
System.out.println("【node_c data】:" + node_c.getData());
System.out.println("【node_d data】:" + node_d.getData());
System.out.println("【node_e data】:" + node_e.getData());
System.out.println("【node_f data】:" + node_f.getData());
}
复制代码
输出:
【node_a data】:a
【node_b data】:b
【node_c data】:c
【node_d data】:d
【node_e data】:e
【node_f data】:f
复制代码
插入已经结束
如今,咱们来考虑一下树的遍历。
树的遍历有两种选择,深度优先搜索(DFS)和广度优先搜索(BFS)。
DFS是用来遍历或搜索树数据结构的算法。从根节点开始,在回溯以前沿着每个分支尽量远的探索。 — Wikipedia
BFS是用来遍历或搜索树数据结构的算法。从根节点开始,在探索下一层邻居节点前,首先探索同一层的邻居节点。 — Wikipedia
下面,咱们来深刻了解每一种遍历算法。
DFS 在回溯和搜索其余路径以前找到一条到叶节点的路径。让咱们看看这种类型的遍历的示例。
输出结果为: 1–2–3–4–5–6–7
为何?
让咱们分解一下:
当咱们深刻到叶结点时回溯,这就被称为 DFS 算法。
既然咱们对这种遍历算法已经熟悉了,咱们将讨论下 DFS 的类型:前序、中序和后序。
这和咱们在上述示例中的做法基本相似。
/** * 前序遍历 * * @param node */
public static void preOrder(BinaryTree node) {
if (node != null) {
System.out.println(node.data);
if (node.left != null) {
node.left.preOrder(node.left);
}
if (node.right != null) {
node.right.preOrder(node.right);
}
}
}
复制代码
示例中此树的中序算法的结果是3–2–4–1–6–5–7。
左结点优先,以后是中间,最后是右结点。
代码实现:
/** * 中序遍历 * * @param node */
public static void inOrder(BinaryTree node) {
if (node != null) {
if (node.left != null) {
node.left.inOrder(node.left);
}
System.out.println(node.data);
if (node.right != null) {
node.right.inOrder(node.right);
}
}
}
复制代码
以此树为例的后序算法的结果为 3–4–2–6–7–5–1 。
左结点优先,以后是右结点,根结点的最后。
代码实现:
/** * 后序遍历 * * @param node */
public static void postOrder(BinaryTree node) {
if (node != null) {
if (node.left != null) {
node.left.postOrder(node.left);
}
if (node.right != null) {
node.right.postOrder(node.right);
}
System.out.println(node.data);
}
}
复制代码
BFS是一层层逐渐深刻的遍历算法
下面这个例子是用来帮咱们更好的解释该算法。
咱们来一层一层的遍历这棵树。本例中,就是1-2-5-3-4-6-7.
代码实现:
/** * 广度排序 * * @param node */
public static void bfsOrder(BinaryTree node) {
if (node != null) {
Queue<BinaryTree> queue = new ArrayDeque<BinaryTree>();
queue.add(node);
while (!queue.isEmpty()) {
BinaryTree current_node = queue.poll();
System.out.println(current_node.data);
if (current_node.left != null) {
queue.add(current_node.left);
}
if (current_node.right != null) {
queue.add(current_node.right);
}
}
}
}
复制代码
为了实现BFS算法,咱们须要用到一个数据结构,那就是队列。
队列具体是用来干什么的呢?
请看下面解释。
二叉搜索树有时候被称为二叉有序树或二叉排序树,二叉搜索树的值存储在有序的顺序中,所以,查找表和其余的操做可使用折半查找原理。——Wikipedia
二叉搜索树中的一个重要性质是,二叉搜索树中一个节点的值大于其左结点,可是小于其右结点
代码实现二叉树搜索
如今想像一下咱们有一棵空树,咱们想将几个节点添加到这棵空树中,这几个结点的值为:50、7六、2一、四、3二、100、6四、52。
首先咱们须要知道的是,50是否是这棵树的根结点。
如今咱们开始一个一个的插入结点
你注意到这里的模式了吗?
让咱们把它分解。
代码实现:
/** * 插入树 * * @param node * @param value */
public void insertNode(BinaryTree node, Integer value) {
if (node != null) {
if (value <= Integer.valueOf(node.data) && node.left != null) {
node.left.insertNode(node.left, value);
} else if (value <= Integer.valueOf(node.data)) {
node.left = new BinaryTree(String.valueOf(value));
} else if (value > Integer.valueOf(node.data) && node.right != null) {
node.right.insertNode(node.right, value);
} else {
node.right = new BinaryTree(String.valueOf(value));
}
}
}
复制代码
看起来很简单。
该算法的强大之处是其递归部分,即第9行和第13行。这两行代码均调用 insertNode 方法,并分别为其左结点和右结点使用它。第11行和第15行则在子结点处插入新结点。
咱们如今要构建的算法是关于搜索的。对于给定的值(整数),咱们会搜索出咱们的二叉查找树有或者没有这个值。
须要注意的一个重要事项是咱们如何定义树的插入算法。 首先咱们有根结点。全部左子的节点值都比根结点小。全部右子树的节点值都比根结点大。
让咱们看一个例子。
假设咱们有这棵树。
如今咱们想知道是否有一个结点的值为52。
让咱们把它分解。
代码实现:
/** * 查找节点是否存在 * * @param node * @param value * @return */
public boolean findNode(BinaryTree node, Integer value) {
if (node != null) {
if (value < Integer.valueOf(node.data) && node.left != null) {
return node.left.findNode(node.left, value);
}
if (value > Integer.valueOf(node.data) && node.right != null) {
return node.right.findNode(node.right, value);
}
return value == Integer.valueOf(node.data);
}
return false;
}
复制代码
代码分析:
删除是一个更复杂的算法,由于咱们须要处理不一样的状况。对于给定值,咱们须要删除具备此值的结点。想象一下这个节点的如下场景:它没有孩子,有一个孩子,或者有两个孩子。
# |50| |50|
# / \ / \
# |30| |70| (DELETE 20) ---> |30| |70|
# / \ \
# |20| |40| |40|
复制代码
若是要删除的结点没有子结点,咱们简单地删除它。该算法不须要重组树。
# |50| |50|
# / \ / \
# |30| |70| (DELETE 30) ---> |20| |70|
# /
# |20|
复制代码
在这种状况下,咱们的算法须要使节点的父节点指向子结点。若是节点是左孩子,则使其父结点指向其子结点。若是结点是右孩子,则使其父结点指向其子结点。
# |50| |50|
# / \ / \
# |30| |70| (DELETE 30) ---> |40| |70|
# / \ /
# |20| |40|
复制代码
当节点有两个孩子,则须要从该节点的右孩子开始,找到具备最小值的结点。咱们将把具备最小值的这个节点置于被删除的节点的位置。
代码实现:
/** * 删除节点 * @param node * @param value * @param parent * @return */
public boolean removeNode(BinaryTree node, Integer value, BinaryTree parent) {
if (node != null) {
if (value < Integer.valueOf(node.data) && node.left != null) {
return node.left.removeNode(node.left, value, node);
} else if (value < Integer.valueOf(node.data)) {
return false;
} else if (value > Integer.valueOf(node.data) && node.right != null) {
return node.right.removeNode(node.right, value, node);
} else if (value > Integer.valueOf(node.data)) {
return false;
} else {
if (node.left == null && node.right == null && node == parent.left) {
parent.left = null;
node.clearNode(node);
} else if (node.left == null && node.right == null && node == parent.right) {
parent.right = null;
node.clearNode(node);
} else if (node.left != null && node.right == null && node == parent.left) {
parent.left = node.left;
node.clearNode(node);
} else if (node.left != null && node.right == null && node == parent.right) {
parent.right = node.left;
node.clearNode(node);
} else if (node.right != null && node.left == null && node == parent.left) {
parent.left = node.right;
node.clearNode(node);
} else if (node.right != null && node.left == null && node == parent.right) {
parent.right = node.right;
node.clearNode(node);
} else {
node.data=String.valueOf(node.right.findMinValue(node.right));
node.right.removeNode(node.right,Integer.valueOf(node.right.data),node);
}
return true;
}
}
return false;
}
复制代码
value
和 parent
。咱们想找到值等于该 value
的 node
,而且该 node
的父节点对于删除该 node
是相当重要的。true
。不然返回 false
。value
的 node
。若是该 value
小于 current node
值,咱们进入左子树,递归处理(当且仅当,current node
有左孩子)。若是该值大于,则进入右子树。递归处理。true
。从第11行到第31行,咱们处理了这些状况。因此直接返回 true
,这就够了。/** * 清空n节点 * * @param node */
public void clearNode(BinaryTree node) {
node.data = null;
node.left = null;
node.right = null;
}
复制代码
/** * 查找树中最小值 */
public Integer findMinValue(BinaryTree node) {
if (node != null) {
if (node.left != null) {
return node.left.findMinValue(node.left);
} else {
return Integer.valueOf(node.data);
}
}
return null;
}
复制代码
原文连接:Everything you need to know about tree data structures
从个人 github 中下载,【译】数据结构中关于树的一切(java版)
🙂🙂🙂关注微信小程序java架构师历程 上下班的路上无聊吗?还在看小说、新闻吗?不知道怎样提升本身的技术吗?来吧这里有你须要的java架构文章,1.5w+的java工程师都在看,你还在等什么?