前面有讲到数组跟链表的数据结构。二者都有优缺点。数组插入太慢,链表查询太慢,就想有没有这样一种数据结构,既能像链表那样快速地插入删除,又能像数组那样快速查找。于是树这种数据结构就诞生了。
树是由边跟节点构成。如下图。
一棵树的每个节点可以有一个或多个子节点,或者没有子节点。每个节点最多有2个子节点的的树叫做二叉树。二叉树的左右2个子节点分别叫做左子节点,右子节点。每个节点的右子节点的关键字都大于或等于父节点,左子节点都小于父节点的二叉树叫做二叉搜索树。如下图。后面主要讲述二叉搜索树。
熟悉一种数据结构都需要了解它的增删改查。树这种数据结构也不例外,这里是以二叉搜索树做为代码例子。
首先的是节点类,这个节点类是一个内部静态类。包含表示数据的属性,左右节点。实际上节点这个类很灵活,可以根据各种需求来设计它的属性。但是都要包含左右节点。(当然不包括你的树没有左右节点QAQ)
public static class Node {
public int data; //关键字数据域,以int为例子。
public Node left; //左节点
public Node right; //右节点
public Node(int data) {
this.data = data;
}
}
表示二叉搜索树的类,包含基本的增删查方法(改也就是在查的基础上添加操作,就不独立出来了)。
public class BinarySearchTree {
public Node rootNode;
/**
* 查询
* @param value
*/
public boolean find(int value) {
//.........
}
/**
* 增加
* @param value
*/
public boolean insert(int value) {
//.....
return false;
}
/**
* 删除
* @param value
*/
public void delete(int value) {
}
二叉树的插入方法,emm这个是二叉搜索树。它的特点是左边的节点要比右边的节点小,右边的节点比左边的节点大。所以在插入一个数据都是从根节点开始,比较当前节点。比根节点小的往左边分支走,比根节点大的往右边分支走,直到找到需要插入的位置(这个节点一定是为null的,因为是待插入的节点)。以下是一种实现方式。
public boolean insert(int value) {
Node newNode = new Node(value);
if (rootNode == null) {
rootNode = newNode;
return true;
} else {
Node current = rootNode;
Node father = null;
while (current != null) {
father = current;
if (value < current.data) {//走左边
current = current.left;
if (current == null) {
father.left = newNode;
return true;
}
} else { //走右边
current = current.right;
if (current == null) {
father.right = newNode;
return true;
}
}
}
}
return false;
}
二叉搜索树的查找方法的原理跟插入方法原理是一样的,先从根节点对比节点数据是不是需要寻找的那个节点;如果不是再判断左分支找还是右分支走,直到找到那个节点或者不存在那样的节点。
public boolean find(int value) {
Node current = rootNode;
while (current.data != value) {
if (value < current.data) {
current = current.left;
} else if (value > current.data) {
current = current.right;
}
if (current == null) {
return false;
}
}
return true;
}
二叉搜索树的删除方法要比查找跟插入方法要复杂一些。被删除的节点就会有一下几种情况:
- 被删除的是叶子节点(没有左右子节点);
- 被删除的节点有一个子节点;
- 被删除的节点有2个子节点;
针对第一种情况是最简单的,被删除的节点没有子节点,直接将原本指向该节点修改成指向null,断开与节点的关系就可以(需要特殊考虑删除节点是否为根节点)。第二种情况下,除了将原来指向被删除节点的连接关系断开外,还需要在该(被删除)节点的所有子节点中找到一个新的节点代替被删除节点。但由于被删除节点只有一个子节点(左或右),所以代替被删除节点的就是被删除节点的左子节点或右子节点(也需要考虑删除节点是否根节点)。最后一种情况,需要从被删除节点的左右子树找到替代节点,就不能随便找一个子节点了。二叉搜索树左右节点的特点,在删除后整个树结构也应该要保持。如下图
从图,删除25这个节点,取代节点为30。在这个树结构中,新节点30的所有左子节点都会比所有右子节点小,这是符合二叉搜索特点的。所以这个新节点(替代被删除节点)必定在被删除节点的右子树部分的最左子节点(最小的那个)。方法就是首先找到右子节点,然后沿着这个右子节点所有左子节点往下查找,最后一个左子节点就是目标节点了。但是还存在这样的情况:被删除节点的右子节点没有左子节点。这种情况就跟第二种情况有些类似。不同的是现在这种情况下被删除节点的右子节点替代后还需要增加指向被删除节点的所有左子节点,而第二种情况下是不需要增加这个步骤的。最后将原本指向被删除节点的节点指向替换节点,替换节点指向被删除节点指向的子树(左子树)。具体代码逻辑:
public boolean delete(int value) {
Node current = rootNode;
Node parentNode = rootNode;
boolean isLeftNode = false;
//找到要被删除的那个节点
while (current != null && current.data != value) {
parentNode = current;
if (value < current.data) {
current = current.left;
isLeftNode = true;
} else if (value > current.data) {
current = current.right;
isLeftNode = false;
}
if (current == null) {
return false;
}
}
//需要判断三种情况:1.没有子节点;2.只有一个子节点;3.有两个子节点;
if (current != null && current.left == null && current.right == null) {
if (current == rootNode) {
rootNode = null;
} else if (isLeftNode) {
parentNode.left = null;
} else {
parentNode.right = null;
}
} else if (current != null && current.left == null) {
if (current == rootNode) {
rootNode = current.right;
} else if (isLeftNode) {
parentNode.left = current.right;
} else {
parentNode.right = current.right;
}
} else if (current != null && current.right == null) {
if (current == rootNode) {
rootNode = current.left;
} else if (isLeftNode) {
parentNode.left = current.left;
} else {
parentNode.right = current.left;
}
} else if (current != null) {//被删除节点有左右节点
Node nextRootNode = getNextRootNode(current);//即将取代被删除节点的节点.
if (current == rootNode) {
rootNode = nextRootNode;
} else if (isLeftNode) {
parentNode.left = nextRootNode;
} else {
parentNode.right = nextRootNode;
}
} else {
return false;
}
return true;
}
/**
* 获取被删除子节点后下一个替代的子节点。
* @param delNode
* @return
*/
private Node getNextRootNode(Node delNode) {
Node targetParent = delNode;
Node targetNode = delNode.right;
Node current = delNode.right;
if (targetNode.left == null) {//没有左子节点,删除目标节点后整个树结构不需要改变
targetNode.left = delNode.left; //目标节点的左节点指向被删除目标节点的左节点(右节点不不需要变)
return targetNode;
}
while (current != null) {
targetParent = targetNode;
targetNode = current;
current = current.left;
}
targetNode.left = delNode.left; //目标节点的左节点指向被删除目标节点的左节点
targetNode.right = delNode.right; //目标节点的右节点指向被删除目标节点的右节点
targetParent.left = targetNode.right; //原本指向目标节点(一定是作为左节点)的指向目标节点的右节点
return targetNode;
}
删除节点是二叉搜索树中基本方法中最复杂的一个。需要区分各种不同情况。是否有子节点?子节点有多少个?是否需要从子树中找到替代节点?替代删除节点的新节点的指向关系?是否是根节点?等等。需要更加严谨复杂的判断处理。
二叉树的遍历
二叉树遍历有三种方式,分别是前序遍历,中序遍历,后序遍历。
- 前序遍历:优先访问(根)父节点,再访问左子节点,右子节点;
- 中序遍历:优先访问左子节点,再访问(根)父节点,右节点;
- 后序遍历:优先访问左子节点,再访问右节点,(根)父节点;
实际上树的遍历就是用递归的思路,理解了树的3种遍历模式思想后,使用递归来实现:
/**
* 中序遍历
* @param node
*/
public void disPlayCenter(Node node) {
if (node != null) {
disPlayCenter(node.left);//访问左节点
Log.d("tree", "disPlayCenter: " + node.data);//使用log表示为访问了这个节点
disPlayCenter(node.right); //访问右节点
}
}
/**
* 前序遍历
* @param node
*/
public void disPlayFront(Node node) {
if (node != null) {
Log.d("tree", "disPlayCenter: " + node.data);
disPlayFront(node.left);
disPlayFront(node.right);
}
}
/**
* 后序遍历
* @param node
*/
public void disPlayBehind(Node node) {
if (node != null) {
disPlayBehind(node.left);
disPlayBehind(node.right);
Log.d("tree", "disPlayCenter: " + node.data);
}
}