二分搜索树的原理和实现
- 1、下载文档前请自行甄别文档内容的完整性,平台不提供额外的编辑、内容补充、找答案等附加服务。
- 2、"仅部分预览"的文档,不可在线预览部分如存在完整性等问题,可反馈申请退款(可完整预览的文档不适用该条件!)。
- 3、如文档侵犯您的权益,请联系客服反馈,我们会尽快为您处理(人工客服工作时间:9:00-18:30)。
⼆分搜索树的原理和实现
⼀、⽂章简介
本⽂将从⼆叉搜索树的定义和性质⼊⼿,带领⼤家实现⼀个⼆分搜索树,通过代码实现让⼤家深度认识⼆分搜索树。
后⾯会持续更新数据结构相关的博⽂。
⼆、⼆叉树
说树这种结构之前,我们要先说⼀下树这种结构存在的意义。
在我们的现实场景中,⽐如图书馆,我们可以根据分类快速找到我们想要找到的书籍。
⽐如我们要找⼀本叫做《Java编程思想》这本书,我们只需要根据,理⼯科 ==> 计算机 ==>Java语⾔分区就可以快速找到我们想要的这本书。
这样我们就不需要像数组或者链表这种结构,我们需要遍历⼀遍才能找到我们想要的东西。
再⽐如,我们所使⽤的电脑的⽂件夹⽬录本⾝也是⼀种树的结构。
从上⾯的描述我们可知,树这种结构具备天然的⾼效性可以巧妙的避开我们不关⼼的东西,只需要根据我们的线索快速去定位我们的⽬标。
所以说树代表着⼀种⾼效。
在了解⼆分搜索树之前,我们不得不了解⼀下⼆叉树,因为⼆叉树是实现⼆分搜索树的基础。
就像我们后⾯会详细讲解和实现AVL(平衡⼆叉树),红⿊树等树结构,你不得不在此之前学习⼆分搜索树⼀样,他们都是互为基础的。
2.1、⼆叉树的定义:
⼆叉树也是⼀种动态的数据结构。
每个节点只有两个叉,也就是两个孩⼦节点,分别叫做左孩⼦,右孩⼦,⽽没有⼀个孩⼦的节点叫做叶⼦节点。
每个节点最多有⼀个⽗亲节点,最多有两个孩⼦节点(也可以没有孩⼦节点或者只有⼀个孩⼦节点)。
对于⼆叉树的定义我们不通过复杂的数学表达式来叙述,⽽是通过简单的描述,让⼤家了解⼀个⼆叉树长什么样⼦。
1只有⼀个根节点。
2每个节点⾄多有两个孩⼦节点,分别叫左孩⼦或者右孩⼦。
(左右孩⼦节点没有⼤⼩之分哦)
3每个⼦树也都是⼀个⼆叉树
满⾜以上三条定义的就是⼀个⼆叉树。
如下图所⽰,就是⼀颗⼆叉树
2.2、⼆叉树的类型
根据⼆叉树的节点分布⼤概可以分为以下三种⼆叉树:完全⼆叉树,满⼆叉树,平衡⼆叉树。
对于以下树的描述不使⽤数学表达式或者专业术语,因为那样很难让⼈想象到⼀棵树到底长什么样⼦。
满⼆叉树:从根节点到每⼀个叶⼦节点所经过的节点数都是相同的。
如下图所⽰就是⼀颗满⼆叉树。
完全⼆叉树:除去最后⼀层叶⼦节点,就是⼀颗完全⼆叉树,并且最后⼀层的节点只能集中在左侧。
对于上⾯的性质,我们从另⼀个⾓度来说就是将满⼆叉树的叶⼦节点从右往左删除若⼲个后就变成了⼀棵完全⼆叉树,也就是说,满⼆叉树⼀定是⼀棵完全⼆叉树,反之不成⽴。
如下图所⽰:除了图3都是⼀棵完全⼆叉树。
平衡⼆叉树:平衡⼆叉树⼜被称为AVL树(区别于AVL算法),它是⼀棵⼆叉树,⼜是⼀棵⼆分搜索树,平衡⼆叉树的任意⼀个节点的左右两个⼦树的⾼度差的绝对值不超过1,即左右两个⼦树都是⼀棵平衡⼆叉树。
三、⼆分搜索树
3.1、⼆分搜索树的定义
1⼆分搜索树是⼀颗⼆叉树
2⼆分搜索树每个节点的左⼦树的值都⼩于该节点的值,每个节点右⼦树的值都⼤于该节点的值
3任意⼀个节点的每棵⼦树都满⾜⼆分搜索树的定义
上⾯我们给出了⼆分搜索树的定义,根据定义我们可知,⼆分搜索树是⼀种具备可⽐较性的树,左孩⼦ < 当前节点 < 右孩⼦。
这种可⽐较性为我们提供了⼀种⾼效的查找数据的能⼒。
⽐如,对于下图所⽰的⼆分搜索树,如果我们想要查询数据14,通过⽐较,14 < 20 找到 10,14 > 10。
只经过上⾯的两步,我们就找到了14这个元素,如下⾯gif所⽰。
可见⼆分搜索树的查询是多么的⾼效。
3.2、⼆分搜索树的实现
本章我们的重点是实现⼀个⼆分搜索树,那我们规定该⼆分搜索树应该具备以下功能:
1以Node作为链表的基础存储结构
2使⽤泛型,并要求该泛型必须实现Comparable接⼝
3基本操作:增删改查
3.2.1、基础结构实现
通过上⾯的分析,我们可知,如果我们要实现⼀个⼆分搜索树,我们需要我们的节点有左右两个孩⼦节点。
根据要求和定义,构建我们的基础代码如下:
/**
* 描述:⼆叉树的实现
* 需要泛型是可⽐较的,也就是泛型必须实现Comparable接⼝
*
* @Author shf
* @Date 2019/7/22 9:53
* @Version V1.0
**/
public class BST<E extends Comparable> {
/**
* 节点内部类
*/
private class Node{
private E e;
private Node left, right;//左右孩⼦节点
public Node(E e){
this.e = e;
this.left = right;
}
}
/**
* BST的根节点
*/
private Node root;
/**
* 记录BST的 size
*/
private int size;
public BST(){
root = null;
size = 0;
}
/**
* 对外提供的获取 size 的⽅法
* @return
*/
public int size(){
return size;
}
/**
* ⼆分搜索树是否为空
* @return
*/
public boolean isEmpty(){
return size == 0;
}
}
对于⼆分搜索树这种结构我们要明确的是,树是⼀种天然的可递归的结构,为什么这么说呢,⼤家想想⼆分搜索树的每⼀棵⼦树也是⼀棵⼆分搜索树,刚好迎合了递归的思想就是将⼤任务⽆限拆分为⼀个个⼩任务,直到求出问题的解,然后再向上叠加。
所以在后⾯的操作中,我们都通过递归实现。
相信⼤家看了以下实现后会对递归有⼀个深层次的理解。
3.2.2、增
为了让⼤家对⼆分搜索树有⼀个直观的认识,我们向⼆分搜索树依次添加[20,10,6,14,29,25,33]7个元素。
我们来看⼀下这个添加的过程。
增加操作和上⾯的搜索操作基本是⼀样的,⾸先我们要先找到我们要添加的元素需要放到什么位置,这个过程其实就是搜索的过程,⽐如我们要在上图中的基础上继续添加⼀个元素15。
如下图所⽰,我们经过⼀路寻找,最终找到节点14,我们15>14所以需要将15节点放到14节点的右孩⼦处。
有了以上的基本认识,我们通过代码实现⼀下这个过程。
1/**
2 * 添加元素
3 * @param e
4*/
5public void add(E e){
6 root = add(root, e);
7 }
8
9/**
10 * 添加元素 - 递归实现
11 * 时间复杂度 O(log n)
12 * @param node
13 * @param e
14 * @return返回根节点
15*/
16public Node add(Node node, E e){
17if(node == null){// 如果当前节点为空,则将要添加的节点放到当前节点处
18 size ++;
19return new Node(e);
20 }
21if(pareTo(node.e) < 0){// 如果⼩于当前节点,递归左孩⼦
22 node.left = add(node.left, e);
23 } else if(pareTo(node.e) > 0){// 如果⼤于当前节点,递归右孩⼦
24 node.right = add(node.right, e);
25 }
26return node;
27 }
如果你还不是很理解上⾯的递归过程,我们从宏观⾓度分析⼀下,⾸先明确 add(Node node, E e) 这个⽅法是⼲什么的,这个⽅法接收两个参数 node和e,如果node为null,则我们将实例化node。
我们的递归过程正是这样,如果node不为空并按照⼤⼩关系去找到左孩⼦节点还是右孩⼦,然后对该孩⼦节点继续执⾏ add(Node node, E e) 操作,通过按照⼤⼩规则⼀路查找直到找到⼀个符合条件的节点并且该节点为null,执⾏node的实例化即可。
如果看了上⾯的解释你还是有点懵,没问题,继续往下看。
刘慈欣的《三体》不仅让中国的硬科幻登上了世界的舞台,更是给⼴⼤读者普及了诸如“降维打击”之类的热门概念。
“降维打击”之所以给⼈如此之震撼,在于它以极简的⽅式,从更⾼的、全新的技术视⾓有效解决了当前困局。
那么在算法的世界中,“递归”就是这种⽜叉哄哄的“降维打击”技术。
递归思想及:当前问题的求解是否可以由规模⼩⼀点的问题求解叠加⽽来,后者是否可以再由更⼩⼀点的问题求解叠加⽽来……依此类推,直到收敛为⼀个极简的出⼝问题的求解。
如果你能从这段话归
纳出递归就是⼀种将⼤的问题不断的进⾏拆分为更⼩的问题,直到拆分到找到问题的解,然后再向⼤的问题逐层叠加⽽最终求得递归的解。
看了以上解释相信⼤家应该对以上递归过程有了⼀个深层次的理解。
如果⼤家还有疑问建议画⼀画递归树,通过压栈和出栈以及堆内存变化的⽅式详细分析每⼀个步骤即可。
在我之前写的,在分析链表反转的时候对递归的微观过程进⾏了详细的分析,希望对⼤家有所帮助。
3.2.3、查
有了上⾯的基础我们实现⼀个查询的⽅式,应该也不存在很⼤的难度了。
我们设计⼀个⽅法叫 contains 即判断是否存在某个元素。
1/**
2 * 搜索⼆分搜索树中是否包含元素 e
3 * @param e
4 * @return
5*/
6public boolean contains(E e){
7return contains(root, e);
8 }
9
10/**
11 * 搜索⼆分搜索树中是否包含元素 e
12 * 时间复杂度 O(log n)
13 * @param node
14 * @param e
15 * @return
16*/
17public boolean contains(Node node, E e){
18if(node == null){
19return false;
20 } else if(pareTo(node.e) == 0){
21return true;
22 } else if(pareTo(node.e) < 0){
23return contains(node.left, e);
24 } else {
25return contains(node.right, e);
26 }
27 }
从上⾯代码我们不难发现其实和add⽅法的递归思想是⼀样的。
那在此我们就不做详细解释了。
为了后⾯代码的实现,我们再设计两个⽅法,即查找树中的最⼤和最⼩元素。
通过⼆分搜索树的定义我们不难发现,左孩⼦ < 当前节点 < 右孩⼦。
按照这个顺序,对于⼀棵⼆分搜索树中最⼩的那个元素就是左边的那个元素,最⼤的元素就是最右边的那个元素。
通过下图我们不难发现,最⼤的和最⼩的节点都符合我们上⾯的分析,最⼩的在最左边,最⼤的在最右边,但不⼀定都是叶⼦节点。
⽐如图1中的6和33元素都不是叶⼦节点。
通过上⾯的分析,我们应该能很容易的想到,查询最⼩元素,就是使⽤递归从根节点开始,⼀直递归左孩⼦,直到⼀个节点的左孩⼦为null。
我们就找到了该最⼩节点。
查询最⼤值同理。
1/**
2 * 搜索⼆分搜索树中以 node 为根节点的最⼩值所在的节点
3 * @param node
4 * @return
5*/
6private Node minimum(Node node){
7if(node.left == null){
8return node;
9 }
10return minimum(node.left);
11 }
12
13/**
14 * 搜索⼆分搜索树中的最⼤值
15 * @return
16*/
17public E maximum(){
18if (size == 0){
19throw new IllegalArgumentException("BST is empty");
20 }
21return maximum(root).e;
22 }
23
24/**
25 * 搜索⼆分搜索树中以 node 为根节点的最⼤值所在的节点
26 * @param node
27 * @return
28*/
29private Node maximum(Node node){
30if(node.right == null){
31return node;
32 }
33return maximum(node.right);
34 }
3.2.4、删
删除操作我们设计三个⽅法,即:删除最⼩,删除最⼤,删除任意⼀个元素。
3.2.
4.1、删除最⼤最⼩元素
通过对上⾯3.2.3中的查最⼤和最⼩元素我们不难想到⾸先我们要找到最⼤或者最⼩元素。
如3.2.3中的图2所⽰,如果待删除的最⼤最⼩节点如果没有叶⼦节点直接删除。
但是如图1所⽰,如果待删除的最⼤最⼩元素还有孩⼦节点,我们该如何处理呢?对于删除最⼩元素,我们需要将该节点的右孩⼦节点提到被删除元素的呃位置,删除最⼤元素同理。
然后我们再看看图2所⽰的情况,使⽤图1的删除⽅式,也就是对于删除最⼩元素,将该节点的右孩⼦节点提到该元素位置即可,只不过对于图2的情况,右孩⼦节点为null⽽已。
1/**
2 * 删除⼆分搜索树中的最⼩值
3 * @return
4*/
5public E removeMin(){
6if (size == 0){
7throw new IllegalArgumentException("BST is empty");
8 }
9 E e = minimum();
10 root = removeMin(root);
11return e;
12 }
13
14/**
15 * 删除⼆分搜索树中以 node 为根节点的最⼩节点
16 * @param node
17 * @return删除后新的⼆分搜索树的跟
18*/
19//////////////////////////////////////////////////
20// 12 12 //
21// / \ / \ //
22// 8 18 -----> 10 18 //
23// \ / / //
24// 10 15 15 //
25//////////////////////////////////////////////////
26private Node removeMin(Node node){
27if(node.left == null){
28 Node rightNode = node.right;// 将node.right(10)赋值给 rightNode 保存
29 node.right = null;// 将node的right与树断开连接
30 size --;
31return rightNode; // rightNode(10)返回给递归的上⼀层,赋值给 12 元素的左节点。
32 }
33 node.left = removeMin(node.left);
34return node;
35 }
36
37public E removeMax(){
38 E e = maximum();
39 root = removeMax(root);
40return e;
41 }
42
43/**
44 * 删除⼆分搜索树中以 node 为根节点的最⼩节点
45 * @param node
46 * @return
47*/
48//////////////////////////////////////////////////
49// 12 12 //
50// / \ / \ //
51// 8 18 -----> 8 15 //
52// \ / \ //
53// 10 15 10 //
54//////////////////////////////////////////////////
55private Node removeMax(Node node){
56if(node.right == null){
57 Node leftNode = node.left; // 将node.right(15)赋值给 leftNode 保存
58 node.left = null;// 将 node 的 left 与树断开连接
59 size --;
60return leftNode; // leftNode (10)返回给递归的上⼀层,赋值给 12 元素的右节点。
61 }
62 node.right = removeMax(node.right);
63return node;
64 }
3.2.
4.2、删除指定元素
待删除元素可能存在的情况如下:
1第⼀种,只有左孩⼦;
2第⼆种,只有右孩⼦;
3第三种,左右孩⼦都有;
4第四种,待删除元素为叶⼦节点;
第⼀种情况和第⼆种情况的树形状类似3.2.3中的图1,其实他们的处理⽅式和删除最⼤最⼩元素的处理⽅式是⼀样的。
这个就不过多解释了,⼤家可以⾃⼰⼿动画出来⼀棵树试试。
那对于第四种情况就是第⼀种或者第⼆种的特殊情况了,也不需要特殊处理。
和3.2.3中的图1和图2的处理⽅式都是⼀样的。
那我们重点说⼀下第三种情况,这个情况有点复杂。
如上图所⽰,如果我们想删除元素10,我们该怎么做呢?我们通过⼆分搜索树的定义分析⼀下,其实很简单。
⾸先10这个元素⼀定是⼤于他的左⼦树的任意⼀个节点,并⼩于右⼦树的任意⼀个节点。
那我们删除了10这个元素,仍然不能打破平衡⼆叉树的性质。
⼀般思路,我们得想办法找个元素顶替下10这个元素。
找谁呢?这个元素放到10元素的位置以后,仍然还能保证⼤于左⼦树的任意元素,⼩于右⼦树的任意元素。
所以我们很容易想到找左⼦树中的最⼤元素,或者找右⼦树中的最⼩元素来顶替10的位置,如下图1所⽰。
如下图所⽰,⾸先我们⽤7顶替10的位置,如下图2所⽰。
我们删除了10这个元素后,⽤左⼦树的最⼤元素替代10,依然能满⾜⼆分搜索树的定义。
同理我们⽤右孩⼦最⼩的节点替换被删除的元素也是完全可以的。
在我们后⾯的代码实现中,我们使⽤右孩⼦最⼩的节点替换被删除的元素。
1/**
2 * 从⼆分搜索树中删除元素为e的节点
3 * @param e
4*/
5public void remove(E e){
6 root = remove(root, e);
7 }
8
9/**
10 * 删除掉以node为根的⼆分搜索树中值为e的节点, 递归算法
11 * @param node
12 * @param e
13 * @return返回删除节点后新的⼆分搜索树的根
14*/
15private Node remove(Node node, E e){
17if( node == null )
18return null;
19
20if( pareTo(node.e) < 0 ){
21 node.left = remove(node.left , e);
22return node;
23 } else if(pareTo(node.e) > 0 ){
24 node.right = remove(node.right, e);
25return node;
26 } else{ // pareTo(node.e) == 0 找到待删除的节点 node
27
28// 待删除节点左⼦树为空,直接将右孩⼦替代当前节点
29if(node.left == null){
30 Node rightNode = node.right;
31 node.right = null;
32 size --;
33return rightNode;
34 }
35
36// 待删除节点右⼦树为空,直接将左孩⼦替代当前节点
37if(node.right == null){
38 Node leftNode = node.left;
39 node.left = null;
40 size --;
41return leftNode;
42 }
43
44// 待删除节点左右⼦树均不为空
45// 找到右⼦树最⼩的元素,替代待删除节点
46 Node successor = minimum(node.right);
47 successor.right = removeMin(node.right);
48 successor.left = node.left;
49
50 node.left = node.right = null;
51
52return successor;
53 }
54 }
四、⼆分搜索树的遍历
⼆分搜索树的遍历⼤概可以分为⼀下⼏种:
1,深度优先遍历:
(1)前序遍历:⽗节点,左孩⼦,右孩⼦
(2)中序遍历:左孩⼦,⽗节点,右孩⼦
(3)后序遍历:左孩⼦,右孩⼦,⽗节点
2,⼴度优先遍历:按树的⾼度从左⾄右进⾏遍历
如上所⽰,⼤类分为深度优先和⼴度优先,深度有点的三种⽅式,⼤家不难发现,其实就是遍历⽗节点的时机。
⼴度优先呢就是按照树的层级,⼀层⼀层的进⾏遍历。
4.1、深度优先遍历
4.1.1、前序遍历
前序遍历是按照:⽗节点,左孩⼦,右孩⼦的顺序对节点进⾏遍历,所以按照这个顺序对于如下图所⽰的⼀棵树,前序遍历,应该是按照编号所⽰的顺序进⾏遍历的。
递归实现:虽然看着很复杂,其实递归代码实现是⼗分简单的。
看代码吧,请别惊掉下巴。
/**
* 前序遍历
*/
public void preOrder(){
preOrder(root);
/**
* 前序遍历 - 递归算法
* @param node 开始遍历的根节点
*/
private void preOrder(Node node){
if(node == null){
return;
}
// 不做复杂的操作,仅仅将遍历到的元素进⾏打印
System.out.println(node.e);
preOrder(node.left);
preOrder(node.right);
}
-------------前序遍历------------
20
10
6
14
29
25
33
⾮递归实现:如果我们不使⽤递归如何实现呢?可是使⽤栈来实现,这是⼀个技巧,当我们需要按照代码执⾏的顺序记录(缓存)变量的时候,栈是⼀种再好不过的数据结构了。
这也是栈的天然优势,因为JVM的栈内存正是栈这种数据结构。
从根节点开始,每次迭代弹出当前栈顶元素,并将其孩⼦节点压⼊栈中,先压右孩⼦再压左孩⼦。
为什么是先右孩⼦再左孩⼦?因为栈是后进先出的数据结构
1/**
2 * 前序遍历 - ⾮递归
3*/
4public void preOrderNR(){
5 preOrderNR(root);
6 }
7
8/**
9 * 前序遍历 - ⾮递归实现
10*/
11private void preOrderNR(Node node){
12 Stack<Node> stack = new Stack<>();
13 stack.push(node);
14while (!stack.isEmpty()){
15 Node cur = stack.pop();
16 System.out.println(cur.e);
17if(cur.right != null){
18 stack.push(cur.right);
19 }
20if(cur.left != null){
21 stack.push(cur.left);
22 }
23 }
24 }
4.1.2、中序遍历
中序遍历:左孩⼦,⽗节点,右孩⼦。
按照这个顺序,我们不难画出下图。
红⾊数字表⽰遍历的顺序。
递归实现:
1/**
2 * ⼆分搜索树的中序遍历
3*/
4public void inOrder(){
5 inOrder(root);
6 }
7
8/**
9 * 中序遍历 - 递归
10 * @param node
11*/
12private void inOrder(Node node){
13if(node == null){
14return;
15 }
16 inOrder(node.left);
17 System.out.println(node.e);
18 inOrder(node.right);
19 }
-------------中序遍历------------
6
10
14
20
25
29
33
我们观察上⾯的遍历结果,不难发现⼀个现象,打印结果正是按照从⼩到⼤的顺序。
其实这也是⼆分搜索树的⼀个性质,因为我们是按照:左孩⼦,⽗节点,右孩⼦。
我们⼆分搜索树的其中⼀个定义:⼆分搜索树每个节点的左⼦树的值都⼩于该节点的值,每个节点右⼦树的值都⼤于该节点的值。
⾮递归实现:依然是⽤栈保存。
1/**
2 * 中序遍历 - ⾮递归
3*/
4public void inOrderNR(){
5 inOrderNR(root);
6 }
7
8/**
9 * 中序遍历 - ⾮递归实现
10 * 时间复杂度 O(n)
11 * @param node
12*/
13private void inOrderNR(Node node){
14 Stack<Node> stack = new Stack<>();
15while(node != null || !stack.isEmpty()){
16while(node != null){
17 stack.push(node);
18 node = node.left;
19 }
20 node = stack.pop();
21 System.out.println(node.e);
22 node = node.right;
23 }
24 }
4.1.3、后序遍历
后序遍历:左孩⼦,右孩⼦,⽗节点。
遍历顺序如下图所⽰。
1/**
2 * 后序遍历
3*/
4public void postOrder(){
5 postOrder(root);
6 }
7
8/**
9 * 后续遍历 - 递归
10 * 时间复杂度 O(n)
11 * @param node
12*/
13public void postOrder(Node node){
14if(node == null){
15return;
16 }
17 postOrder(node.left);
18 postOrder(node.right);
19 System.out.println(node.e);
20 }
21 -------------后序遍历------------
22 6
23 14
24 10
25 25
26 33
27 29
28 20
⾮递归实现:
1/**
2 * 后序遍历 - ⾮递归
3*/
4public void postOrderNR(){
5 postOrderNR(root);
6 }
7
8/**
9 * 后序遍历 - ⾮递归实现
10 * 时间复杂度 O(n)
11 * @param node
12*/
13private void postOrderNR(Node node){
14 Stack<Node> stack = new Stack<>();
15 Stack<Node> out = new Stack<>();
16 stack.push(node);
17while(!stack.isEmpty()){
18 Node cur = stack.pop();
19 out.push(cur);
20
21if(cur.left != null){
22 stack.push(cur.left);
23 }
24if(cur.right != null){
25 stack.push(cur.right);
26 }
27 }
28while(!out.isEmpty()){
29 System.out.println(out.pop().e);
30 }
31 }
4.2、⼴度优先遍历
⼴度优先遍历:⼜称为,层序遍历,按照⾼度顺序⼀层⼀层的访问整棵树,⾼层次的节点将会⽐低层次的节点先被访问到。
这种遍历⽅式显然是不适合递归求解的。
⾄于为什么,相信经过我们前⾯对递归的分析,⼤家已经很清楚了。
对于层序优先遍历,我们使⽤队列来实现,利⽤队列的先进先出(FIFO)的的特性。
1/**
2 * 层序优先遍历
3 * 时间复杂度 O(n)
4*/
5public void levelOrder(){
6 Queue<Node> queue = new LinkedList<>();
7 queue.add(root);
8while(!queue.isEmpty()){
9 Node node = queue.remove();
10 System.out.println(node.e);
11if(node.left != null){
12 queue.add(node.left);
13 }
14if(node.right != null){
15 queue.add(node.right);
16 }
17 }
18 }
五、⼆分搜索树存在的问题
前⾯我们讲,⼆分搜索树是⼀种⾼效的数据结构,其实这也不是绝对的,在极端情况下,⼆分搜索树会退化成链表,各种操作的时间复杂度⼤打折扣。
⽐如我们向我们上⾯实现的⼆分搜索树中按顺序添加如下元素[1,2,3,4,5],如下图所⽰,我们发现我们的⼆分搜索树其实已经退化成了⼀个链表。
关于这个问题,我们在后⾯介绍平衡⼆叉树(AVL)的时候会讨论如何能让⼆分搜索树保持平衡,并避免这种极端情况的发⽣。
《祖国》
⼩时候
以为你就是远在北京的天安门
长⼤了
才发现原来你就在我的⼼⾥
参考⽂献:
《玩转数据结构-从⼊门到进阶-刘宇波》
《数据结构与算法分析-Java语⾔描述》
如有错误的地⽅还请留⾔指正。