树与二叉树
树是最常用的数据结构之一,它是一种非线性表结构,较之前的线性表结构要复杂一些。
树的基本概念
1、树的定义
树是由n个元素组成的有限集合,有三大特点:
1、每个元素称为结点(node);
2、有一个特定的结点,称为根结点或根(root);
3、除根结点外,其余结点被分成m(m>=0)个互不相交的有限集合,而每个子集又都是一棵树(称为原树的子树)
(借用王争老师的图)
2、节点、高度、深度、层
1、节点:树中的元素即被称为节点,连线则代表父子关系如图:
其中A为B\C\D的父节点,相应的B\C\D就称为A的子节点,而没有父节点的E就被称为根节点,没有子节点的G\H\I\J\K\L等则被称为叶子节点。
2、高度:节点的高度为节点到叶子节点的最长路径(即边数)
3、深度:根节点到此节点的路程(遍历的边数)
4、层:节点的深度+1
5、树的高度:根节点的高度
二叉树
二叉树是树中最常用的,顾名思义二叉树就是每个节点最多两个子节点的树,两个子节点也分别称为左子节点和右子节点。
如图,编号为2的特殊二叉树被称为满二叉树,叶子节点全部在最底层,并且除了叶子节点,其他节点都有左子树和右子树。
编号为3的二叉树被称为完全二叉树,完全二叉树叶子节点全部在最底层,并且靠左排列,除了底层外其他节点都有左右子树。满二叉树是特殊的完全二叉树。
最常用的树为二叉查找树,其特点为任一节点,其左子节点小于此节点,右子节点大于此节点
树的遍历
遍历树有三种经典的遍历方法:前序遍历,中序遍历,后序遍历。
前序遍历的顺序为:先遍历根节点,然后左子树,最后右子树。
中序遍历顺序为:先左子树,然后根节点,最后右子树。
后序遍历顺序为:先左子树,然后右子树,最后根节点。
其遍历的代码如下:
void preOrder(Node* root) {
if (root == null) return;
print root // 此处为伪代码,表示打印root节点
preOrder(root->left);
preOrder(root->right);
}
void inOrder(Node* root) {
if (root == null) return;
inOrder(root->left);
print root // 此处为伪代码,表示打印root节点
inOrder(root->right);
}
void postOrder(Node* root) {
if (root == null) return;
postOrder(root->left);
postOrder(root->right);
print root // 此处为伪代码,表示打印root节点
}
树的查找、插入、删除
二叉查找树的查找
一般树会使用二叉查找树,其查找简单快捷。
二叉查找树的查找方法比较容易理解:
在查找所找的值时,从根节点开始,
(1)如果所查值等于根节点则返回根节点
(2)如果所查值小于根节点,则遍历其左子树
(3)如果所查值大于根节点,则遍历其右子树
其代码如下:
public class BinarySearchTree {
private Node tree;
public Node find(int data) {
Node p = tree;
while (p != null) {
if (data < p.data) p = p.left;
else if (data > p.data) p = p.right;
else return p;
}
return null;
}
public static class Node {
private int data;
private Node left;
private Node right;
public Node(int data) {
this.data = data;
}
}
}
二叉查找树的插入
二叉查找树的插入过程有点类似查找操作。新插入的数据一般都是在叶子节点上,所以我们只需要从根节点开始,依次比较要插入的数据和节点的大小关系。
(1)如果要插入的数据比节点的数据大,并且节点的右子树为空,就将新数据直接插到右子节点的位置;如果不为空,就再递归遍历右子树,查找插入位置。
(2)如果要插入的数据比节点数值小,并且节点的左子树为空,就将新数据插入到左子节点的位置;如果不为空,就再递归遍历左子树,查找插入位置
其插入代码如下:
public void insert(int data) {
if (tree == null) {
tree = new Node(data);
return;
}
Node p = tree;
while (p != null) {
if (data > p.data) {
if (p.right == null) {
p.right = new Node(data);
return;
}
p = p.right;
} else { // data < p.data
if (p.left == null) {
p.left = new Node(data);
return;
}
p = p.left;
}
}
}
二叉查找树的删除
二叉查找树的查找、插入操作都比较简单易懂,但是它的删除操作就比较复杂了 。针对要删除节点的子节点个数的不同,需要分三种情况来处理。
第一种情况是,如果要删除的节点没有子节点,我们只需要直接将父节点中,指向要删除节点的指针置为 null。
第二种情况是,如果要删除的节点只有一个子节点(只有左子节点或者右子节点),我们只需要更新父节点中,指向要删除节点的指针,让它指向要删除节点的子节点就可以了。
第三种情况是,如果要删除的节点有两个子节点,这就比较复杂了。我们需要找到这个节点的右子树中的最小节点,把它替换到要删除的节点上。然后再删除掉这个最小节点,因为最小节点肯定没有左子节点(如果有左子结点,那就不是最小节点了),所以,我们可以应用上面两条规则来删除这个最小节点。
删除的代码如下:
public void delete(int data) {
Node p = tree; // p指向要删除的节点,初始化指向根节点
Node pp = null; // pp记录的是p的父节点
while (p != null && p.data != data) {
pp = p;
if (data > p.data) p = p.right;
else p = p.left;
}
if (p == null) return; // 没有找到
// 要删除的节点有两个子节点
if (p.left != null && p.right != null) { // 查找右子树中最小节点
Node minP = p.right;
Node minPP = p; // minPP表示minP的父节点
while (minP.left != null) {
minPP = minP;
minP = minP.left;
}
p.data = minP.data; // 将minP的数据替换到p中
p = minP; // 下面就变成了删除minP了
pp = minPP;
}
// 删除节点是叶子节点或者仅有一个子节点
Node child; // p的子节点
if (p.left != null) child = p.left;
else if (p.right != null) child = p.right;
else child = null;
if (pp == null) tree = child; // 删除的是根节点
else if (pp.left == p) pp.left = child;
else pp.right = child;
}