被二分查找树灌了迷魂汤?醒醒吧,看看这篇文章

前言:

可能你会有几年的项目经验,可能在你公司里面也是技术领域的佼佼者,但是这篇二分查找树,你可能不一定都会。

特别注意:文中图片都是动态图,如果看的时候没有看出效果,请刷新一下试试

咱们废话不多说,开整!

二分查找树是个啥?

二分查找树(binary search tree),也叫二分搜索树。可以说是二叉树的一个应用,也是二叉树的一种数据结构,如图:

特点:

对于每一个结点,左孩子小于该节点,有孩子大于该结点。

既然是二叉树,同样是一种动态数据结构,可以使用结点类来存放每个结点,如下:

classNode{

Ee;

Node left;//左孩子

Node right;//右孩子

}

树的这种结构,非常适合递归来实现各种操作,往往也是令人迷惑的场面。如果没有养成递归思想,对树的这种递归操作往往会感到迷惑,下面将剖析各种递归操作。

二分查找树的 add 操作

递归开始于 根节点,通过 return 将 新结点 链入到二叉树中。其中的情形 ① 和 ② 是递归的基准情形,递归调用在情形 ③ 和 ④ 中进行。

privateNodeadd(Node node, E e){

if(node ==null) {

size++;

returnnewNode(e);

}

if(e.compareTo(node.e) <0) {

node.left =add(node.left, e);

}

if(e.compareTo(node.e) >0){

node.right =add(node.right, e);

}

returnnode;

}

Tips: 比较结点值得大小时,我们不能使用基本的操作符(我们类型选用泛型,只能是包装类和引用类,对象大小比较不能用基本操作符);需要使用 comparable 类中得 comparaTo()

二分查找树的 contains 操作

二分查找树给二叉树了一个存储的顺序,使得我们对二叉树的操作变得更简单。

查询操作未对二叉树进行更改,所以不需要 return 结点,只需要通过返回的 布尔值 来判度查找结果。

privateboolean contains(Node node, E e) {

if(node ==null) {

returnfalse;

}elseif(e.compareTo(node.e) ==0) {

returntrue;

}elseif(e.compareTo(node.e) <0) {

returncontains(node.left, e);

}else{

returncontains(node.right, e);

}

}

二分查找树的深度遍历

先根遍历(DLR),又名先序遍历:先访问根结点,再遍历左子树,遍历右子树。

图中出现的二叉树,使用先根遍历结果为:53 12 9 14 64 78

private void perOrder(Node node) {

//DLR

if(node ==null)

return;

else{

System.out.print(node.e+" ");

perOrder(node.left);

perOrder(node.right);

}

}

中序遍历(LDR),又名对称遍历:先遍历左子树,再访问根结点,遍历右子树。

图中出现的二叉树,使用中遍历结果为:9 12 14 53 64 78

private void inOrder(Node node) {

//LDR

if(node ==null)

return;

else{

inOrder(node.left);

System.out.print(node.e+" ");

inOrder(node.right);

}

}

后序遍历(LRD):先遍历左子树,再遍历右子树,再访问根结点。

图中出现的二叉树,使用后序遍历结果为:9 14 12 78 64 53

private void postOrder(Node node) {

//LRD

if(node ==null)

return;

else{

postOrder(node.left);

postOrder(node.right);

System.out.print(node.e+" ");

}

}

看了三种遍历方式,发现虽然顺序有差异,但在编程时,集中体现为打印语句的顺序不同;正是由于递归调用前后语句的执行深度和顺序不同,支持了树的深度遍历。

Tips: 深度遍历是根据递归来定义的,默认的方向是从左到右遍历,也可以从右向左遍历,称这种遍历顺序为逆序遍历。

深度优先遍历非递归实现

先序遍历(DLR):利用队列来辅助实现遍历

算法思路

将根结点入队,当队列不为空时,重复下面步骤:

① 出队队头结点

② 打印队头结点

③ 判断队头结点左子树是否为空。如果左子树为空,入队队头结点的右子树(若右子树不为空);否则,入队队头结点的左子树和右子树(若右子树不为空),同时,保证左子树永远在队头

public void preOrder() {

//先序遍历(前序遍历)

if(root ==null)

return;

Queue que =newLinkedList<>();

que.add(root);

while(!(que.isEmpty())) {

Node node = que.remove();

if(node.left ==null) {

System.out.print(node.e+" ");

if(node.right !=null)

que.add(node.right);

}else{

System.out.print(node.e+" ");

int n = que.size();

que.add(node.left);

if(node.right !=null)

que.add(node.right);

for(int i =0; i < n; i++) {

que.add(que.remove());

}

}

}

}

中序遍历(LDR):利用栈来实现遍历

算法思路

从根节点开始,当结点不为空或者栈不为空时,重复下面步骤:

① 当前结点不为空,入队当前结点,遍历左子树至空树

② 当前结点为空,出队栈顶结点并打印,遍历右子树

publicvoidinOrder(){

Stackstack=newStack<>();

Node node = root;

while(node != null || !(stack.isEmpty())) {

if(node != null) {

stack.push(node);

node = node.left;

}else{

node =stack.pop();

System.out.print(node.e+" ");

node = node.right;

}

}

}

后序遍历(LRD):利用栈来实现遍历

算法思路

从根节点开始,当结点不为空或者栈不为空时,重复下面步骤:

① 当前结点不为空,入栈当前结点和有孩子(右孩子不为空),遍历右子树至空树

② 当前结点为空,栈顶元素右子树为空或者右孩子刚访问过了,出栈并打印栈顶结点,将当前结点设置为刚被访问;否则,遍历右子树

public void postOrder() {

//后序遍历(后根遍历)

Stack stack =newStack<>();

Node node = root;

Node visited =null;

while(node!=null|| !stack.isEmpty()) {

if(node !=null) {

stack.push(node);

if(node.right !=null)

stack.push(node.right);

node = node.left;

}else{

node = stack.pop();

if(node.right ==null|| node.right == visited) {

System.out.print(node.e+" ");

visited = node;

node =null;

}else{

stack.push(node);

node = node.right;

}

}

}

}

二分查找树广度优先遍历

广度优先遍历,又名层次遍历,按照每一层一次遍历所有结点,这里我们借助队列来实现。

从根结点开始,首先入队根结点,重复下面步骤:如果队头结点左右孩子不为空,出队头结点并打印,入队左右孩子结点;否则出队头结点并打印。

public void levelOrder() {

//层序遍历

Queue temporary =newLinkedList<>();

temporary.add(root);

while(!temporary.isEmpty()) {

Node node = temporary.remove();

if(node.left !=null)

temporary.add(node.left);

if(node.right !=null) {

temporary.add(node.right);

}

System.out.print(node.e+" ");

}

}

minimum 和 maximum

minimum 和 maximum 表示二叉查找树中的最小值和最大值,关于最大值和最小值有两种操作,查找二叉查找树中的最大值和最小值,并可以删除最大值,最小值。

查询 minimum 和 maximum

二叉查找树中的最大值和最小值,必定为深度最大的左子树和深度最大的右子树,这样直接使用递归就可以进行操作,查询并没有改变二叉树,所以无需改变根结点。

privateNode minimum(Node node) {

if(node.left !=null) {

returnminimum(node.left);

}else{

returnnode;

}

}

privateNode maximum(Node node) {

if(node.right !=null) {

returnmaximum(node.right);

}else{

returnnode;

}

}

删除 minimum 和 maximum

删除操作会改变二叉查找树的结点,首先遍历左(右)子树,至空树(即到达二分查找树的最大和最小值结点),将最小值(最大值)接点的右子树链入到前驱结点。

privateNoderemoveMinimum(Nodenode) {

if(node.left== null) {

NoderightNode = node.right;

node.right= null;

size--;

returnrightNode;

}else{

node.left= removeMinimum(node.left);

returnnode;

}

}

privateNoderemoveMaximum(Nodenode) {

if(node.right== null) {

NodeleftNode = node.right;

node.left= null;

size--;

returnleftNode;

}else{

node.right= removeMaximum(node.right);

returnnode;

}

}

Tips: 删除结点时,并不能采用循环迭代方法查找并将结点值置为 null 来完成删除;我们在所有的二叉树代码中看到的都是结点的引用,并不能删除结点,我们只能改变结点的引用,将其指向 null,并将删除结果链入二叉树中,没有引用指向的结点 ,会被垃圾回收机制回收。

删除二叉树的任意结点

首先,我们了解一下删除的规则:

① 要删除的结点没有右子树,直接将要删除结点的左子树链入到前驱结点的左子树

② 要删除的结点没有左子树,直接将要删除结点的右子树链入到前驱结点的右子树

③ 要删除的结点有右子树,找到其右子树中的最小值结点,用最小值结点替换要删除的结点,同时,将右子树中的最小值结点删除掉

在删除结点时,我们首先要定位删除结点,根据结点值来遍历左子树和右子树,来找到并删除该节点。

privateNoderemoveNode(Nodenode,Ee) {

if(node == null)

returnnull;

if(e.compareTo(node.e) >0) {

node.right= removeNode(node.right,e);

returnnode;

}elseif(e.compareTo(node.e) <0) {

node.left= removeNode(node.left,e);

returnnode;

}else{

if(node.left== null) {

NoderightNode = node.right;

node.right= null;

size--;

returnrightNode;

}

if(node.right== null) {

NodeleftNode = node.left;

node.left= null;

size--;

returnleftNode;

}

NodeminNode = minimum(node.right);

minNode.right= removeMinimum(node.right);

minNode.left= node.left;

returnminNode;

}

}

打印树形二分查找树

通过二叉树中结点的深度不同,利用逆中序遍历,可以打印逆置 90° 的二叉树:

打印顺序:78 64 53 14 12 9(逆中序遍历)。每次只打印一个结点。

publicvoidprintTree(){

printTree(root,0);

}

privatevoidprintTree(Node node,intn){

if(node ==null) {

return;

}

printTree(node.right, n+1);

for(inti =0; i < n; i++) {

System.out.print("\t");

}

System.out.println(node.e);

printTree(node.left, n+1);

}

至此,关于二分查找树的实现思路和源码都讲完了。还是那句话,如果觉得本文对你有利,请帮忙转发让更多人看到,喜欢石头的也可以关注一下,持续输出干货!

原文链接:

https://blog.csdn.net/weixin_42089228/article/details/106290387

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,377评论 6 496
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,390评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,967评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,344评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,441评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,492评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,497评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,274评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,732评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,008评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,184评论 1 342
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,837评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,520评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,156评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,407评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,056评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,074评论 2 352