文章目录
1、树
树(tree)是一种抽象数据类型(ADT),用来模拟具有树状结构性质的数据集合。它是由n(n>0)个有限节点通过连接它们的边组成一个具有层次关系的集合。把它叫做“树”是因为它看起来像一棵倒挂的树,也就是说它是根朝上,而叶朝下的。
①、节点:上图的圆圈,比如A,B,C等都是表示节点。节点一般代表一些实体,在java面向对象编程中,节点一般代表对象。
②、边:连接节点的线称为边,边表示节点的关联关系。一般从一个节点到另一个节点的唯一方法就是沿着一条顺着有边的道路前进。在Java当中通常表示引用。
树有很多种,向上面的一个节点有多余两个的子节点的树,称为多路树,后面会讲解2-3-4树和外部存储都是多路树的例子。而每个节点最多只能有两个子节点的一种形式称为二叉树,这也是本篇博客讲解的重点。
树的常用术语
①、路径:顺着节点的边从一个节点走到另一个节点,所经过的节点的顺序排列就称为“路径”。
②、根:树顶端的节点称为根。一棵树只有一个根,如果要把一个节点和边的集合称为树,那么从根到其他任何一个节点都必须有且只有一条路径。A是根节点。
③、父节点:若一个节点含有子节点,则这个节点称为其子节点的父节点;B是D的父节点。
④、子节点:一个节点含有的子树的根节点称为该节点的子节点;D是B的子节点。
⑤、兄弟节点:具有相同父节点的节点互称为兄弟节点;比如上图的D和E就互称为兄弟节点。
⑥、叶节点:没有子节点的节点称为叶节点,也叫叶子节点,比如上图的H、E、F、G都是叶子节点。
⑦、子树:每个节点都可以作为子树的根,它和它所有的子节点、子节点的子节点等都包含在子树中。
⑧、节点的层次:从根开始定义,根为第一层,根的子节点为第二层,以此类推。
⑨、深度:对于任意节点n,n的深度为从根到n的唯一路径长,根的深度为0;
⑩、高度:对于任意节点n,n的高度为从n到一片树叶的最长路径长,所有树叶的高度为0;
2、二叉搜索树 BST
二叉树:树的每个节点最多只能有两个子节点
上图的第一幅图B节点有DEF三个子节点,就不是二叉树,称为多路树;而第二幅图每个节点最多只有两个节点,是二叉树,并且二叉树的子节点称为“左子节点”和“右子节点”。上图的D,E分别是B的左子节点和右子节点。
如果我们给二叉树加一个额外的条件,就可以得到一种被称作二叉搜索树(binary search tree)的特殊二叉树。
二叉搜索树要求:
若它的左子树不空,则左子树上所有结点的值均小于它的根结点的值; 若它的右子树不空,则右子树上所有结点的值均大于它的根结点的值; 它的左、右子树也分别为二叉排序树。
二叉搜索树作为一种数据结构,那么它是如何工作的呢?它查找一个节点,插入一个新节点,以及删除一个节点,遍历树等工作效率如何,下面我们来一一介绍。
2.1 二叉树的节点类 Node<AnyType>
private static class Node<AnyType> {
AnyType element;
Node<AnyType> leftChild;
Node<AnyType> rightChild;
boolean isDelete;
Node(AnyType theElement) {
this(theElement, null, null);
}
Node(AnyType theElement, Node<AnyType> lt, Node<AnyType> rt) {
element = theElement;
leftChild = lt;
rightChild = rt;
isDelete = false;
}
}
private Node<AnyType> root;
2.2 查找节点 find(x)
查找某个节点,我们必须从根节点开始遍历。
①、查找值比当前节点值大,则搜索右子树;
②、查找值等于当前节点值,停止搜索(终止条件);
③、查找值小于当前节点值,则搜索左子树;
//查找结点
public Node<AnyType> find(AnyType x) {
Node<AnyType> current = root;
int compareResult = x.compareTo(current.element);
while (current != null) {
if (compareResult < 0)
current = current.leftChild;
else if (compareResult > 0)
current = current.rightChild;
else return current;
}
return null;
}
用变量current来保存当前查找的节点,参数key是要查找的值,刚开始查找将根节点赋值到current。接在在while循环中,将要查找的值和current保存的节点进行对比。如果key小于当前节点,则搜索当前节点的左子节点,如果大于,则搜索右子节点,如果等于,则直接返回节点信息。当整个树遍历完全,即 current==null
,那么说明没找到查找值,返回null。
树的效率:查找节点的时间取决于这个节点所在的层数,每一层最多有2n-1个节点,总共N层共有2n-1个节点,那么时间复杂度为 O(logN)
,底数为2。最坏的时间复杂度 O(n)
2.3 查找节点 contains(x)
如果在树T中存在含有项X的节点,那么contains方法返回true,否则返回false.如果树T是空集,则返回false.
/**
* contain
* 如果在树T中存在含有项X的节点,那么contains方法返回true
* 否则返回false.如果树T是空集,则返回false.
* @param x 待检测的元素
* @param node 树,一般为根节点
* @return 存在含有项X的节点 返回true
*/
public boolean contains(AnyType x,BinaryNode<AnyType> node){
if(x == null)
return false;
int compareResult = x.compareTo(node.element);
if(compareResult < 0)
return contains(x, node.leftChild);
else if(compareResult > 0)
return contains(x, node.rightChild);
else return true;
}
2.4 插入节点 insert(x)
二叉搜索树的插入总是在最后一层插入!下图所示,
首先currentNode = 41 ,当要插入的节点是42时,
比较 41 < 42 ----> 右子树 —> currentNode =65;
比较 65 > 42 —> 左子树 —> currentNode=50
比较 50 > 42 ---->左子树 ----> currentNode = Null,所以找到了插入的位置!在此处插入42
//插入新节点
public boolean insert(AnyType x){
return insert(x,root);
}
private boolean insert(AnyType x,BinaryNode<AnyType> node)
{
BinaryNode newNode = new BinaryNode<>(x);
if (root == null) { //如果树为空,则把新插入的节点设为根节点
root = newNode;
return true;
}
else {
BinaryNode<AnyType> current = node;
BinaryNode<AnyType> parentNode = null;
while(current != null) {
parentNode = current;
int compareResult = x.compareTo(current.element);
if (compareResult < 0) {
current = current.leftChild;
if(current == null) {
parentNode.leftChild = newNode;
return true;
}
}
else if (compareResult > 0) {
current = current.rightChild;
if(current == null) {
parentNode.rightChild = newNode;
return true;
}
} else return false;
}
}
}
2.5 找最大/最小值
对于findMin方法,思路是一直找当前节点的左子树,直到currentNode == null!最后寻到的非空节点就是最小值。
对于findMin方法,思路是一直找当前节点的右子树,直到currentNode == null!最后寻到的非空节点就是最大值。
/**
* findMin() findMax()
* 对于findMin方法,思路是一直找当前节点的左子树,直到currentNode == null!最后寻到的非空节点就是最小值。
* 对于findMin方法,思路是一直找当前节点的右子树,直到currentNode == null!最后寻到的非空节点就是最大值。
* @return 最大值/最小值
*/
public AnyType findMin(){
if(isEmpty())
System.out.println("The tree is empty,can't find the minimum node!");
return findMin(root);
}
private AnyType findMin(BinaryNode<AnyType> node){
if(node != null)
while(node.leftChild != null)
node = node.leftChild;
return node.element;
}
public AnyType findMax(){
if(isEmpty())
System.out.println("The tree is empty,can't find the minimum node!");
return findMax(root);
}
private AnyType findMax(BinaryNode<AnyType> node){
if(node != null)
while(node.rightChild != null)
node = node.rightChild;
return node.element;
}
2.6 删除节点 remove() ——方法1
删除节点是二叉搜索树中最复杂的操作,删除的节点有三种情况,前两种比较简单,但是第三种却很复杂。
- 该节点是叶节点(没有子节点)
- 该节点有一个子节点
- 该节点有两个子节点
对于删除,最复杂的是该将被删除的节点A具有两个儿子的情况,一般是该节点的右子树最小的节点B代替节点A,然后在B原来的位置删除掉节点B。
如果删除的次数不多,通常使用懒惰删除:当一个元素要被删除时,它仍在树中,而只是标记为删除。 特别是删除算法会使得左子树比右子树深,因为我们总是用右子树的一个来代替删除的节点。会造成二叉查找树,严重的不平衡。
①、删除没有子节点的节点
要删除叶节点,只需要改变该节点的父节点引用该节点的值,即将其引用改为 null 即可。要删除的节点依然存在,但是它已经不是树的一部分了,由于Java语言的垃圾回收机制,我们不需要非得把节点本身删掉,一旦Java意识到程序不在与该节点有关联,就会自动把它清理出存储器。
@Override
public boolean delete(int key) {
Node current = root;
Node parent = root;
boolean isLeftChild = false;
//查找删除值,找不到直接返回false
while(current.data != key){
parent = current;
if(current.data > key){
isLeftChild = true;
current = current.leftChild;
}else{
isLeftChild = false;
current = current.rightChild;
}
if(current == null){
return false;
}
}
//如果当前节点没有子节点
if(current.leftChild == null && current.rightChild == null){
if(current == root){
root = null;
}else if(isLeftChild){
parent.leftChild = null;
}else{
parent.rightChild = null;
}
return true;
}
return false;
}
删除节点,我们要先找到该节点,并记录该节点的父节点。在检查该节点是否有子节点。如果没有子节点,接着检查其是否是根节点,如果是根节点,只需要将其设置为null即可。如果不是根节点,是叶节点,那么断开父节点和其的关系即可。
②、删除有一个子节点的节点
删除有一个子节点的节点,我们只需要将其父节点原本指向该节点的引用,改为指向该节点的子节点即可。
//当前节点有一个子节点
if(current.leftChild == null && current.rightChild != null){
if(current == root){
root = current.rightChild;
}else if(isLeftChild){
parent.leftChild = current.rightChild;
}else{
parent.rightChild = current.rightChild;
}
return true;
}else{
//current.leftChild != null && current.rightChild == null
if(current == root){
root = current.leftChild;
}else if(isLeftChild){
parent.leftChild = current.leftChild;
}else{
parent.rightChild = current.leftChild;
}
return true;
}
③、删除有两个子节点的节点
当删除的节点存在两个子节点,那么删除之后,两个子节点的位置我们就没办法处理了。既然处理不了,我们就想到一种办法,用另一个节点来代替被删除的节点,那么用哪一个节点来代替呢?
我们知道二叉搜索树中的节点是按照关键字来进行排列的,某个节点的关键字次高节点是它的中序遍历后继节点。用后继节点来代替删除的节点,显然该二叉搜索树还是有序的。(这里用后继节点代替,如果该后继节点自己也有子节点,我们后面讨论。)
那么如何找到删除节点的中序后继节点呢?其实我们稍微分析,这实际上就是要找比删除节点关键值大的节点集合中最小的一个节点,只有这样代替删除节点后才能满足二叉搜索树的特性。
后继节点也就是:比删除节点大的最小节点。
算法:程序找到删除节点的右节点,(注意这里前提是删除节点存在左右两个子节点,如果不存在则是删除情况的前面两种),然后转到该右节点的左子节点,依次顺着左子节点找下去,最后一个左子节点即是后继节点;如果该右节点没有左子节点,那么该右节点便是后继节点。
public Node getSuccessor(Node delNode){
Node successorParent = delNode;
Node successor = delNode;
Node current = delNode.rightChild;
while(current != null){
successorParent = successor;
successor = current;
current = current.leftChild;
}
//将后继节点替换删除节点
if(successor != delNode.rightChild){
successorParent.leftChild = successor.rightChild;
successor.rightChild = delNode.rightChild;
}
return successor;
}
2.7 删除节点 remove() ——方法2
上面讨论过三种不同的情况,最难的删除节点有两个子树。例如图中的50,思路是:找右子树的最小值代替删除节点!
public BinaryNode<AnyType> remove(AnyType t, BinaryNode<AnyType> node) {
if (node == null) {
return node;
}
int compareResult = t.compareTo(node.element);
if (compareResult > 0) {
node.rightChild = remove(t, node.rightChild);
} else if (compareResult < 0) {
node.leftChild = remove(t, node.leftChild);
} else if (node.leftChild != null && node.rightChild != null) {
node.element = findMin(node.rightChild).element;
node.rightChild = remove(node.element, node.rightChild);
} else {
node = (node.leftChild != null) ? node.leftChild : node.rightChild;
}
return node;
}
3、遍历
//中序遍历
private void infixOrder(BinaryNode<AnyType> current){
if(current != null){
infixOrder(current.leftChild);
System.out.print(current.element+" ");
infixOrder(current.rightChild);
}
}
//前序遍历
private void preOrder(BinaryNode<AnyType> current){
if(current != null){
System.out.print(current.element+" ");
infixOrder(current.leftChild);
infixOrder(current.rightChild);
}
}
//后序遍历
private void postOrder(BinaryNode<AnyType> current){
if(current != null){
infixOrder(current.leftChild);
infixOrder(current.rightChild);
System.out.print(current.element+" ");
}
}