文章目录
二叉树与BST树
引言:本问大概说明了二叉树的性质,着重介绍了二叉搜索树的性质,用法和代码实现。
1. 二叉树
1.1 二叉树的定义
每个结点最多有两个子树的树,称为二叉树。一棵深度为k,且有2^k-1个结点的二叉树,称为满二叉树。这种树的特点是每一层上的结点数都是最大结点数。而在一棵二叉树中,除最后一层外,若其余层都是满的,并且或者最后一层是满的,或者是在右边缺少连续若干结点,则此二叉树为完全二叉树。
1.2 二叉树的相关术语
- 树的结点(node):包含一个数据元素及若干指向子树的分支;
- 孩子结点(child node):结点的子树的根称为该结点的孩子;
- 双亲结点:B 结点是A 结点的孩子,则A结点是B 结点的双亲;
- 兄弟结点:同一双亲的孩子结点; 堂兄结点:同一层上结点;
- 祖先结点: 从根到该结点的所经分支上的所有结点
- 子孙结点:以某结点为根的子树中任一结点都称为该结点的子孙
- 结点层:根结点的层定义为1;根的孩子为第二层结点,依此类推;
- 树的深度:树中最大的结点层
- 结点的度:结点子树的个数
- 树的度: 树中最大的结点度。
- 叶子结点:也叫终端结点,是度为 0 的结点;
- 分枝结点:度不为0的结点;
- 有序树:子树有序的树,如:家族树;
- 无序树:不考虑子树的顺序;
1.3 二叉树的性质
(1) 在非空二叉树中,第i层的结点总数不超过 2 i − 1 , i > = 1 2^{i-1} , i>=1 2i−1,i>=1;
(2) 深度为h的二叉树最多有 2 h − 1 2^h-1 2h−1个结点 ( h > = 1 ) (h>=1) (h>=1),最少有h个结点;
(3) 对于任意一棵二叉树,如果其叶结点数为 N 0 N_0 N0,而度数为2的结点总数为 N 2 N_2 N2,则 N 0 = N 2 + 1 N_0=N_2+1 N0=N2+1;
(4) 具有 n n n个结点的完全二叉树的深度为 [ log 2 n ] + 1 [\log_2 n]+1 [log2n]+1(注:[ ]表示向下取整)
(5) 有 N N N个结点的完全二叉树各结点如果用顺序方式(如数组)存储,若 i i i为结点编号,则结点之间有如下关系:
- 如果 i > 1 i>1 i>1,则其父结点的编号为 i / 2 i/2 i/2;
- 如果 2 i < = N 2i<=N 2i<=N,则其左孩子(即左子树的根结点)的编号为 2 i 2i 2i;若 2 ∗ i > N 2*i>N 2∗i>N,则无左孩子;
- 如果 2 i + 1 < = N 2i+1<=N 2i+1<=N,则其右孩子的结点编号为 2 i + 1 2i+1 2i+1;若 2 ∗ i + 1 > N 2*i+1>N 2∗i+1>N,则无右孩子。
1.4 二叉树的遍历
二叉树的遍历分为前序遍历、中序遍历、后序遍历、层序遍历。这里的前中后是访问根的相对位置。
-
前序遍历
首先访问根,再先序遍历左(右)子树,最后先序遍历右(左)子树。即 V L R的顺序
-
中序遍历
首先中序遍历左(右)子树,再访问根,最后中序遍历右(左)子树。即L V R
-
后序遍历
首先后序遍历左(右)子树,再后序遍历右(左)子树,最后访问根。即 L R V
-
层序遍历
即按照层次访问,从上到下,从左到右依序访问。先访问根,访问子女,再访问子女的子女(越往后的层次越低)
2. BST树
2.1. BST树定义
BST树(Binary Sort Tree),二叉排序树,在二叉树的前提下,每个节点和他的左右不为空的孩子满足以下条件:左孩子的值<双亲结点的值<右孩子的值
2.2 BST树的增删查功能
介绍之前,首先给出BST树使用的节点类
/**
* 树的结点类
*
* @param <T>
*/
class Entry<T extends Comparable<T>> {
T data; //数据
Entry<T> left; //左孩子
Entry<T> right; //右孩子
/**
* 构造方法
*
* @param data
*/
public Entry(T data) {
this(data, null, null,1);
}
/**
* 构造方法
*
* @param data
* @param left
* @param right
*/
public Entry(T data, Entry<T> left, Entry<T> right) {
this.data = data;
this.left = left;
this.right = right;
}
}
2.2.1 BST树的查询
BST树的查询操作,比较节点与目标值的大小。节点值大于目标值,向左深入查找;节点值小于目标值,向右深入查找;节点值等于目标值,说明找到该节点。若找到叶子节点,还是没找到,则说明该值不存在。
查询操作的代码实现如下:
/**
* 非递归
* 查询操作
*
* @param value
* @return
*/
public boolean n_query(T value) {
if (root == null) {
return false;
}
Entry<T> cur = root;
while (cur != null) {
if (cur.data.compareTo(value) < 0) {
cur = cur.right;
} else if (cur.data.compareTo(value) > 0) {
cur = cur.left;
} else {
return true;
}
}
return false;
}
/**
* 递归
* 查询操作
*
* @param value
* @return
*/
public boolean query(T value) {
if (root == null)
return false;
return query(this.root, value);
}
/**
* 以node节点开始 寻找value值的节点
*
* @param node
* @param value
* @return
*/
private boolean query(Entry<T> node, T value) {
if (node == null)
return false;
if (node.data.compareTo(value) > 0)
return query(node.left, value);
else if (node.data.compareTo(value) < 0)
return query(node.right, value);
else
return true;
}
2.2.2 BST树的增加
BST树的增加,也要使节点满足有序。插入新节点时,首先找到其该放的位置,过程为首先和根节点比较,比根节点小往左节点走,比根节点大往右走,之后如果为空,则放入节点,若不为空,一样的比较流程,直到找到一个该放的位置。
代码实现如下:
/**
* 非递归
* 插入操作
*
* @param value
*/
public void n_insert(T value) {
//空树 直接返回
if (root == null) {
root = new Entry<>(value);
return;
}
//寻找该插入的位置
Entry<T> cur = root;
Entry<T> parent = cur;
boolean ifLeft = false;//该插入的节点位于父节点的左 还是 右
while (cur != null) {
parent = cur;
if (cur.data.compareTo(value) < 0) {
cur = cur.right;
ifLeft = false;
} else if (cur.data.compareTo(value) > 0) {
cur = cur.left;
ifLeft = true;
} else {
return;//不插入重复值
}
}
//cur == null 插入新节点
if (ifLeft) {
parent.left = new Entry<>(value);
} else {
parent.right = new Entry<>(value);
}
}
/**
* 递归
* 插入操作
*
* @param value
*/
public void insert(T value) {
if (root == null) {
root = new Entry<>(value);
return;
}
insert(this.root, value);
}
/**
* 以node节点开始,寻找合适位置插入
*
* @param node
* @param value
* @return
*/
private Entry<T> insert(Entry<T> node, T value) {
if (node == null) {
return new Entry<>(value);
}
if (node.data.compareTo(value) > 0) {
node.left = insert(node.left, value);
} else if (node.data.compareTo(value) < 0) {
node.right = insert(node.right, value);
} else {
///相等什么也不做
}
return node;
}
2.2.3 BST树的删除
BST树的删除,首先需要找到待删除节点,然后判断该节点是哪种类型:
-
叶子节点,即无孩子节点,直接删除该节点
-
有一个孩子节点,用孩子节点代替该节点
-
有两个孩子节点,这时需要注意,需要找到以该节点为根的树中,最接近该节点值的节点,用之替代该节点。这里有两种合适节点,称为前驱节点和后继节点
-
前驱节点:最大的小于待删除节点的数据的节点。寻找方式为:待删除结点的左孩子开始,若有右孩子,则一直往右寻找,直到找到叶节点,即为前驱节点;
-
后继节点:最小的大于该待删除节点的数据的节点。寻找方式为:待删除结点的右孩子开始,若有左孩子,则一直往左寻找,直到找到叶节点,即为后继节点;
前驱节点和后继节点即为中序遍历时,待删除节点的前一个节点和后一个节点。找到前驱节点或后继节点后,用前驱或后继的值覆盖待删除节点,再删除前驱或后继节点即可。
-
代码实现如下:
/**
* 非递归
* 删除操作
*
* @param value
*/
public void n_remove(T value) {
//空树
if (root == null) {
return;
}
//先找到目标节点
Entry<T> cur = root;//记录该删除的节点
Entry<T> parent = null;//记录该删除的节点的父节点
while (cur != null) {
if (cur.data.compareTo(value) > 0) {
parent = cur;
cur = cur.left;
} else if (cur.data.compareTo(value) < 0) {
parent = cur;
cur = cur.right;
} else break;//找到目标节点
}
//没找到目标节点
if (cur == null) return;
//进行删除
//#3 有左右两个孩子
if (cur.left != null && cur.right != null) {
//找到目标节点的前驱节点
Entry<T> entry = cur;//记录目标节点位置
parent = cur;
cur = cur.left;
while (cur.right != null) {
parent = cur;
cur = cur.right;//找到前驱节点
}
entry.data = cur.data;//用前驱节点的值覆盖目标节点
}
//删除cur #1无左右孩子 #2有一个孩子
Entry<T> child = cur.left;//记录cur的孩子
if (child == null) {
child = cur.right;
}
if (parent == null) {//删除的是根节点
root = child;
} else if (parent.left == cur) {//链接cur的父节点与cur的孩子节点
parent.left = child;
} else {
parent.right = child;
}
//切断cur与child的关联 防止内存泄漏
cur.right = null;
cur.left = null;
}
/**
* 递归
* 删除操作
*
* @param value
* @return
*/
public void remove(T value) {
root = remove(this.root, value);
}
/**
* 以node节点开始 删除value值节点
* 删除完成后,把新的子树的根节点进行返回
*
* @param node
* @param value
* @return 返回每个节点 写到父节点的相应左右子节点域中
*/
private Entry<T> remove(Entry<T> node, T value) {
if (node == null)
return null;
if (node.data.compareTo(value) > 0)
node.left = remove(node.left, value);
else if (node.data.compareTo(value) < 0)
node.right = remove(node.right, value);
else {
//找到该结点
if (node.left != null && node.right != null) {
//该节点有两个子节点 找到其前驱节点 #3
Entry<T> pre = node.left;//前驱节点
while (pre.right != null)
pre = pre.right;
node.data = pre.data;//用前驱节点的值代替待删除结点的值
//删除前驱节点
node.left = remove(node.left, pre.data);
} else {
// 至少有一个孩子结点
if (node.left != null) {//节点的左孩子不为空
return node.left;
} else if (node.right != null) {//节点的右孩子不为空
return node.right;
} else
return null;//删除是叶子节点
}
}
return node;
}
2.3 BST树的遍历
2.3.1 前序遍历
BST树的前序遍历,如同上述增删查操作一样,有递归和非递归两种实现方式。
-
递归实现:按照V L R的顺序,首先打印节点的值,然后递归访问左节点,再递归访问右节点,直到访问到根节点,返回。
/** * 递归 前序遍历 */ public void proOrder() { System.out.print("前序遍历:"); proOrder(root); System.out.println(); } /** * 以node开始前序遍历 * * @param node */ private void proOrder(Entry<T> node) { if (node == null) return; System.out.print(node.data + " "); proOrder(node.left); proOrder(node.right); } -
非递归实现:非递归实现需要借助栈。首先将根节点入栈,然后借助一个循环,做如下操作:
- 栈顶元素出栈
- V L R 因此首先打印其值,若有右孩子,则将右孩子入栈;若其有左孩子,则将左孩子入栈。这里是因为栈先进后出的性质,首先需要L 所以让R先入栈
/** * 非递归 前序遍历 VLR */ public void n_proOrder() { if (root == null) return;//空树 返回 LinkedList<Entry<T>> stack = new LinkedList<>();//栈 stack.push(root); Entry<T> top;//栈顶元素 while (!stack.isEmpty()) { top = stack.pop(); //元素出栈 System.out.print(top.data + " ");//打印V if (top.right != null) stack.push(top.right); //入栈R 先入后出 if (top.left != null) stack.push(top.left); //入栈L } System.out.println(); }
2.3.2 中序遍历
这里需要强调,BST树中序遍历得到的结果就是各节点的值从小到大的有序序列。
-
递归实现:类似于前序遍历的递归实现,不同的是输入是按L V R的顺序进行
/** * 递归 中序遍历 */ public void inOrder() { System.out.print("中序遍历:"); inOrder(root); System.out.println(); } /** * 以node开始中序遍历 * * @param node */ private void inOrder(Entry<T> node) { if (node == null) return; inOrder(node.left); System.out.print(node.data + " "); inOrder(node.right); } -
非递归实现:同样的,需要借助一个栈。首先将节点入栈,因为是L V R的顺序,需要一直向左深度遍历,直到最左最深的子节点,之后将其弹出,输出该节点值,判断其右孩子,存在,继续从该节点向左深度遍历,还是上述步骤;右孩子不存在则再弹出一个子节点,这个子节点肯定是上一个弹出的父节点;如此一直下去,就做到了L V R的顺序
/** * 非递归 中序遍历 LVR */ public void n_inOrder() { if (root == null) return;//空树 返回 LinkedList<Entry<T>> stack = new LinkedList<>(); Entry<T> cur = root;//记录栈顶元素 while (!stack.isEmpty() || cur != null) { if (cur != null) { //当前节点未到最左节点 stack.push(cur); cur = cur.left; //深度遍历 入栈 } else { //此时栈顶元素的左孩子为空,出栈更新栈顶元素 打印 V 若存在则将其右孩子入栈 cur = stack.pop(); System.out.print(cur.data + " "); //R cur = cur.right; } } System.out.println(); }
2.3.3 后序遍历
-
递归实现:类似于上面的两种,只不过是L R V的顺序
/** * 递归后序遍历 */ public void postOrder() { System.out.print("后序遍历:"); postOrder(root); System.out.println(); } /** * 以node开始后序遍历 * * @param node */ private void postOrder(Entry<T> node) { if (node == null) return; postOrder(node.left); postOrder(node.right); System.out.print(node.data + " "); } -
非递归实现:后序遍历的非递归实现更复杂,一个栈无法满足,我们需要借助两个栈。我们要实现L R V的顺序,可以先实现V R L的顺序,然后让其入栈,先进后出,出来就变成了L R V,即后序遍历。而实现V R L的顺序遍历,则可以参照前序遍历的非递归遍历方法,只不过放入顺序变成了先放入L再放入R,以达到R比L先出的效果。
/** * 非递归 后序遍历 LRV */ public void n_postOrder() { if (root == null) return;//空树 返回 //实现类前序遍历 将结果放入另一个栈中 LinkedList<Entry<T>> stack_1 = new LinkedList<>(); LinkedList<Entry<T>> stack_2 = new LinkedList<>(); stack_1.push(root); Entry<T> top;//栈顶元素 while (!stack_1.isEmpty()) { top = stack_1.pop();//V stack_2.push(top);//放入另一个栈 //R L if (top.left != null) stack_1.push(top.left);//先入后出 if (top.right != null) stack_1.push(top.right); } //栈元素全部按照VRL的顺序出栈 放入栈2中 栈2再出栈顺序则为 LRV while (!stack_2.isEmpty()) { top = stack_2.pop(); System.out.print(top.data + " "); } System.out.println(); }
2.3.4 层序遍历
-
递归实现:层序遍历首先有一个工具方法:递归计算树的层数。代码如下:
/** * 计算树的层数 * * @return */ public int level() { return level(root); } /** * 递归 * 以node开始计算树的层数 * * @param node * @return */ private int level(Entry<T> node) { if (node == null) { return 0; } else { int left = level(node.left); int right = level(node.right); return left > right ? left + 1 : right + 1; } }递归层序遍历代码如下:深度遍历树,给定一个层数的传参,每次向下递归都减一,当参数为0,代表到达目标层数,打印该节点的数据并返回。
/** * 递归 * 层序遍历 */ public void levelOrder() { System.out.print("层序遍历:"); int l = level(); for (int i = 0; i < l; i++) { levelOrder(root, i);//打印第i层 } System.out.println(); } /** * 从node开始遍历 打印i层节点 * * @param node * @param i */ private void levelOrder(Entry<T> node, int i) { if (node == null) { return; } if (i == 0) {//找到该层 System.out.print(node.data + " "); return; } levelOrder(node.left, i - 1); levelOrder(node.right, i - 1); } -
非递归实现:层序遍历的非递归实现需要借助一个队列。首先将根节点入队,进行如下操作:
- 对头元素出队 打印该值
- 判断对头元素是否有左右孩子,有的话将其入队
由于队列先进先出的性质,先出的节点打印时入队的其孩子节点也会先出队。
/** * 非递归 层序遍历 */ public void n_levelOrder() { if (root == null) return; LinkedList<Entry<T>> queue = new LinkedList<>();//队列 queue.offer(root); Entry<T> pro; while (!queue.isEmpty()) { pro = queue.poll(); //出队 System.out.print(pro.data + " "); if (pro.left != null) queue.offer(pro.left); //入队 if (pro.right != null) queue.offer(pro.right); } System.out.println(); }
2.4 BST树的性能分析
每个结点的 C ( i ) C(i) C(i)为该结点的层次数。最坏情况下,当先后插入的关键字有序时,构成的二叉排序树蜕变为单支树,树的深度为其平均查找长度 ( n + 1 ) / 2 (n+1)/2 (n+1)/2 (和顺序查找相同),最好的情况是二叉排序树的形态和折半查找的判定树相同,其平均查找长度和 log 2 n \log_2 n log2n成正比。
本文详细介绍了二叉树的基础概念,包括定义、术语、性质和遍历方法。重点阐述了二叉搜索树(BST树)的特性,包括查询、插入、删除操作及其代码实现。同时,对BST树的遍历进行了深入探讨,并分析了其性能。
1148

被折叠的 条评论
为什么被折叠?



