数据结构】树、二叉树以及二叉搜索树

复习一下

树的定义:
树(tree)是包含n(n>=0)个结点的有穷集,其中:
(1)每个元素称为结点(node);
(2)有一个特定的结点被称为根结点或树根(root)。
(3)除根结点之外的其余数据元素被分为m(m≥0)个互不相交的集合T1,T2,……Tm-1,其中每一个集合Ti(1<=i<=m)本身也是一棵树,被称作原树的子树(subtree)。
树也可以这样定义:树是由根结点和若干颗子树构成的。树是由一个集合以及在该集合上定义的一种关系构成的。集合中的元素称为树的结点,所定义的关系称为父子关系。父子关系在树的结点之间建立了一个层次结构。在这种层次结构中有一个结点具有特殊的地位,这个结点称为该树的根结点,或称为树根。
我们可以形式地给出树的递归定义如下: 单个结点是一棵树,树根就是该结点本身。
设T1,T2,…,Tk是树,它们的根结点分别为n1,n2,…,nk。用一个新结点n作为n1,n2,…,nk的父亲,则得到一棵新树,结点 n 就是新树的根。我们称n1,n2,…,nk为一组兄弟结点,它们都是结点n的子结点。我们还称T1,T2,…,Tk为结点n的子树。
空集合也是树,称为空树。空树中没有结点。

相关术语

节点的度:一个节点含有的子树的个数称为该节点的度;
叶节点或终端节点:度为0的节点称为叶节点;
非终端节点或分支节点:度不为0的节点;
双亲节点或父节点:若一个节点含有子节点,则这个节点称为其子节点的父节点;
孩子节点或子节点:一个节点含有的子树的根节点称为该节点的子节点;
兄弟节点:具有相同父节点的节点互称为兄弟节点;
树的度:一棵树中,最大的节点的度称为树的度;
节点的层次:从根开始定义起,根为第1层,根的子节点为第2层,以此类推;
树的高度或深度:树中节点的最大层次;
堂兄弟节点:双亲在同一层的节点互为堂兄弟;
节点的祖先:从根到该节点所经分支上的所有节点;
子孙:以某节点为根的子树中任一节点都称为该节点的子孙。
森林:由m(m>=0)棵互不相交的树的集合称为森林;
1
2
3
4
5
6
7
8
9
10
11
12
13
2.二叉树的定义
二叉树是一个连通的无环图,并且每一个顶点的度不大于2。有根二叉树还要满足根结点的度不大于2。有了根结点之后,每个顶点定义了唯一的父结点,和最多2个子结点。

3.二叉树的类型
严格二叉树:二叉树中每个结点要么有两个孩子结点,要么没有孩子结点

满二叉树:二叉树中的每个结点恰好有两个孩子结点且所有叶子结点都在同一层

完全二叉树:在定义完全二叉树之前,假定二叉树的高度为h,对于完全二叉树,如果将所有结点从根结点开始从左至右,从上往下,依次编号(假定根结点的编号为1),那么将得到从1~n(n为结点总数)的完整序列。在遍历过程中对于空指针也应赋予编号。如果所有结点的深度为h或h-1,且在结点编号序列中没有漏掉任何数字,那么这样的二叉树叫做完全二叉树。


4.二叉树的性质
为了讨论二叉树的下述性质,假定树的高度为h,根结点的深度为0。

满二叉树的结点个数n为2 k + 1 − 1 2^{k+1}-12
k+1
−1.因为该树共有h层,所以每一层的满,即有[ 2 0 + 2 1 + 2 2 + . . . + 2 h = 2 h + 1 − 1 ] [2^0+2^1+2^2+...+2^h=2^{h+1}-1][2
0
+2
1
+2
2
+...+2
h
=2
h+1
−1]。
完全二叉树的结点个数为2 h 2^h2
h
~2 h + 1 2^{h+1}2
h+1
-1。
满二叉树的叶子结点个数是2 h 2^h2
h

对于n个结点的完全二叉树,空指针的个数为n+1。
对于任意一棵二叉树,如果其叶子节点n0,度为2的节点为 n2,那么则有 n0 = n2 + 1
可由 n = n0 + n1 + n2 和 n - 1 = n1 + n2(度之和等于 总节点数减 1) 推导而来
5.二叉树搜索树(查找树,排序树)的实现
定义: 二叉树搜索树是一种特殊的、使用很广泛的二叉树,二叉搜索树的特点是,一个节点的左子节点的关键字值小于这个节点,右子节点的关键字值大于或等于这个父节点。

a)主要操作

查找元素
我们已经知道,二叉搜索树的特点是左子节点小于父节点,右子节点大于或等于父节点。查找某个节点时,先从根节点入手,如果该元素值小于根节点,则转向左子节点,否则转向右子节点,以此类推,直到找到该节点,或者到最后一个叶子节点依然没有找到,则证明树中没有该节点

//查找操作
BinaryTreeNode<T> find(T data){
BinaryTreeNode<T> cur = root;
if(root == null) {
return null;
}else {
while(data != cur.getData()) {
if((Integer)data < (Integer)cur.getData()) {
cur = cur.getLeft();
}else {
cur = cur.getRight();
}
if(cur == null)
return null;
}
}
return cur;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
插入元素
插入一个新节点首先要确定插入的位置,小于根结点的放在左子树,大于等于根结点的放在右子树,这个过程类似于查找一个不存在的节点。找到要插入的位置之后,将父节点的左子节点或者右子节点指向新节点即可

//插入操作
void add(T data) {
BinaryTreeNode<T> node = new BinaryTreeNode<>(data);
if(root == null) {
root = node;
}else {
BinaryTreeNode<T> cur = root;
while(true) {
if((Integer)node.getData() < (Integer)cur.getData()) {
if(cur.getLeft() == null){
cur.setLeft(node);
return;
}
cur = cur.getLeft();
}else {
if(cur.getRight() == null) {
cur.setRight(node);
return;
}
cur = cur.getRight();
}
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
遍历树
遍历的意思是根据一种特定顺序访问树的每一个节点, 有三种简单的方法遍历树:前序遍历、中序遍历、后序遍历。二叉搜索树最常用的方法是中序遍历,中序遍历二叉搜索树会使所有的节点按关键字升序被访问到。

遍历树最简单的方法是递归。用该方法时,只需要做三件事(初始化时这个节点是根):

1、调用自身来遍历节点的左子树

2、访问这个节点

3、调用自身来遍历节点的右子树

遍历可以应用于任何二叉树,而不只是二叉搜索树。遍历的节点并不关心节点的关键字值,它只看这个节点是否有子节点
除此之外还有层次遍历和和逆向层次遍历。

//递归先序遍历
void preOrder(BinaryTreeNode<T> root) {
if(root == null)
return;
System.out.print(root.getData() + "->");
preOrder(root.getLeft());
preOrder(root.getRight());
}

//非递归先序遍历
void preNonRecursive(BinaryTreeNode<T> root) {
if(root == null)
return;
MyStack<T> stack = new MyStack<>();
while(true) {
while(root != null) {
System.out.print(root.getData() + "->");
stack.push(root.getData());
root = root.getLeft();
}
if(stack.isEmpty()) {
break;
}
root = (BinaryTreeNode<T>) stack.pop();
root = root.getRight();
}
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
删除元素
树的删除节点操作是最复杂的一项操作。该操作需要考虑三种情况考虑:

1、该节点没有子节点

2、该节点有一个子节点

3、该节点有两个子节点

第一种没有子节点的情况很简单,只需将父节点指向它的引用设置为null即可:
第二种情况也不是很难,这个节点有两个连接需要处理:父节点指向它的引用和它指向子节点的引用。无论要删除的节点下面有多复杂的子树,只需要将它的子树上移,还有一种特殊情况需要考虑,就是要删除的是根节点,这时就需要把它唯一的子节点设置成根节点
最复杂的第三种情况:要删除的节点有两个子节点。显然,这时候不能简单地将子节点上移,因为该节点有两个节点,右子节点上移之后,该右子节点的左子节点和右子节点又怎么安排呢?这时应该想起,二叉搜索树是按照关键升序排列,对每一个关键字来说,比它关键字值高的节点是它的中序后继,简称后继。删除有两个子节点的节点,应该用它的中序后继来替代该节点,后继结点有两种情况:一种是欲删除节点的右子节点没有左子节点,那么它本身就是后继节点,此时,只需要将以此后继节点为根的子树移到欲删除节点的位置,另一种情况是欲删除节点的右子节点有左子节点,这种情况就比较复杂,下面来逐步分析。首先应该意识到,后继节点是肯定没有左子节点的,但是可能会有右子节点。

因为删除是比较复杂的操作,所以在删除操作不是很多的情况下,可以在节点类中增加一个布尔字段,来作为该节点是否已删除的标志。在进行其他操作,比如查找时,之前对该节点是否已删除进行判断。这种思路有点逃避责任,但是在很多时候还是很管用的。

//删除树的结点
boolean delete(T value) {
/*
* 删除节点在二叉树中是最复杂的,主要有三种情况:
* 1. 该节点没有子节点(简单)
* 2. 该节点有一个子节点(还行)
* 3. 该节点有两个子节点(复杂)
* 删除节点的时间复杂度为O(logN)
*/
if(root == null)
return false;
BinaryTreeNode<T> cur = root;//欲删除的结点
BinaryTreeNode<T> parent = null;//欲删除结点的父节点
boolean isLeftChild = true;//标记删除结点是否为其父节点的左孩子
//找到要删除的结点和其父结点
while(cur.getData() != value) {
parent = cur;
if((Integer)cur.getData() > (Integer)value) {
isLeftChild = true;
cur = cur.getLeft();
}else {
//标记删除结点为其父节点的右孩子
isLeftChild = false;
cur = cur.getRight();
}
if(cur == null)
return false;
}

if(cur.getLeft() == null && cur.getRight() == null) {//欲删除结点为叶子结点
if(cur == root) {
root = null;
}else if(isLeftChild){
parent.setLeft(null);
}else {
parent.setRight(null);
}
}else if(cur.getLeft() == null){//欲删除结点只有右子结点
if(cur == root) {
root = cur.getRight();
}else if(isLeftChild){
parent.setLeft(cur.getRight());
}else {
parent.setRight(cur.getRight());
}
}else if(cur.getRight() == null) {//欲删除结点只有左子结点
if(cur == root) {
root = cur.getLeft();
}else if(isLeftChild) {
parent.setLeft(cur.getLeft());
}else {
parent.setRight(cur.getLeft());
}
}else {//欲删除结点有两个子结点
BinaryTreeNode<T> successor = cur.getRight();//欲删除结点的后继结点
BinaryTreeNode<T> successorParent = cur;//欲删除结点的后继结点的父结点
//找到欲删除结点的后继结点
while(successor.getLeft() != null) {
successorParent = successor;
successor = successor.getLeft();
}
if(successorParent == cur) {//后继结点为欲删除结点的右子结点
if(cur == root) {
root = successor;
root.setLeft(cur.getLeft());
}else if(isLeftChild) {
parent.setLeft(successor);
successor.setLeft(cur.getLeft());
}else {
parent.setRight(successor);
successor.setLeft(cur.getLeft());
}
}else {//后继结点不是欲删除结点的右子结点
successorParent.setLeft(successor.getRight());
successor.setRight(successorParent);
if(cur == root) {
root = successor;
root.setLeft(cur.getLeft());
}else if(isLeftChild){
parent.setLeft(successor);
successor.setLeft(cur.getLeft());
}else {
parent.setRight(successor);
successor.setLeft(cur.getLeft());
}
}

}
return true;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
b)辅助操作

获取树的大小
递归地计算左子树和右子树的大小,再+1(当前结点),然后返回给其双亲结点,或者利用层次遍历的非递归算法,每遍历一个结点就自增

//获取树的大小
int size(BinaryTreeNode<T> root) {//递归计算左右子树的大小再+1
// if(root == null)
// return 0;
// return (size(root.getLeft()) + 1 + size(root.getRight()));

//非递归,利用层次遍历
BinaryTreeNode<T> temp;
int count = 0;
LLQueue<BinaryTreeNode<T>> queue = new LLQueue<>();
if(root == null)
return 0;
queue.addQueue(root);
while(!queue.isEmpty()) {
temp = queue.removeQueue();
count++;
if(temp.getLeft() != null)
queue.addQueue(temp.getLeft());
if(temp.getRight() != null)
queue.addQueue(temp.getRight());
}
queue.deleteQueue();
return count;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
获取树的高度
用迭代方法来获取左子树和右子树的最大深度,返回两者最大值

//获取结点的深度
int getDepth(BinaryTreeNode<T> currentNode,int initDeep) {
int leftDeep = initDeep;
int rightDeep = initDeep;
if(currentNode.getLeft() != null) {
leftDeep = getDepth(currentNode.getLeft(),leftDeep + 1);
}
if(currentNode.getRight() != null) {
rightDeep = getDepth(currentNode.getRight(),rightDeep + 1);
}
return leftDeep >= rightDeep ? leftDeep :rightDeep;
}

//获取树的高度,树的高度等于树的深度
int getTreeDepth() {
if(root == null)
return 0;
return getDepth(root,0);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
查找最值
在二叉搜索树中,查找最大值、最小是是很容易实现的,从根循环访问左子节点,直到该节点没有左子节点为止,该节点就是最小值;从根循环访问右子节点,直到该节点没有右子节点为止,该节点就是最大值

原文链接:https://blog.csdn.net/qq_41900081/article/details/86601783

posted @ 2022-07-05 16:08  stdxxd  阅读(164)  评论(0)    收藏  举报