休息了两天,状态恢复了一下,补充点基础知识。

二叉搜索树

  搜索树数据结构支持许多动态集合操作,包括Search,minimum,maximum,predecessor(前驱),successor(后继),INSERT和DELETE等。因此我们使用一颗搜索树既可以作为一个字典又可以作为一个优先队列。且二叉搜索树上的基本操作所花费的时间与这棵树的高度成正比。二叉搜索树有两个很重要的变体,红黑树与B树,这个我们之后有机会再补一篇文章。

  顾名思义,一棵二叉搜索树是以一棵二叉树来组织的。如图所示,这样的一棵树可以用一个链表的数据结构来表示,其中每个结点就是一个对象。除了结点的key之外,每个结点还包含属性left,right,p。分别用于指向这个结点的左孩子,右孩子与父节点。如果不存在,则对应的属性值为null。根结点是树种唯一父指针为null的结点。

  关于二叉搜索树有这么一些特点。对任何结点x,其左子树中的关键字最大值不超过x.key,其右子树中的关键字的最小值不小于x.key。不同的二叉搜索树可以代表同一组值的集合。如上图所示。

中序遍历

  二叉搜索树的这种性质允许我们通过一个简单的递归算法来按序(x.key的顺序)输出二叉搜索树的所有关键字。这种算法被称为中序遍历(inorder tree walk)。这样命名的原因是输出的子树的根的关键字位于根的左子树与右子树的关键字值之间。类似地,先序遍历,后序遍历输出的根的关键字在其左右子树的关键字值的前/后。一个中序遍历的伪算法如下:

INORDER-TREE-WALK(x)
{
if (x != Null)
{
INORDER-TREE-WALK(x.left);
print x;
INORDER-TREE-WALK(x.right);
}
}

  对于之前图中的两棵二叉搜索树,他们中序遍历得到的结果是一样的,2,5,5,6,7,8.

查询二叉搜索树

  前面有提到,二叉搜索树支持许多集合操作。我们来看看一下利用二叉搜索树完成这些操作的思路。

  查找

    假如我们要寻找关键字值为k的结点,如果这个结点存在就返回,否则就返回null。可以利用下面的方法来进行查找。

Search(x,k) //x为要进行查找的树的根结点,k为要查找的关键字值
{
if(x == Null || x.key ==k)
{
return x;
} if(k < x.key)
{
return Search(x.left, k);
}
else
{
return Search(x.right, k);
}
}

  思路很简单,我们判断当前这个结点是否是我们要寻找的结点,如果不是则决定使用左子树或右子树来继续查找。但上面的方式其实还是一种递归,我们可以用一种while循环的方式来改写它。对于计算机来说,这种方式效率应该会更高一些。

Search(x, k) //同样x为树的根结点,k为要查找的关键字的值
{
while(x != Null && x.key != k)
{
if(k < x.key)
{
x = x.left;
}
else
{
x = x.right;
}
} return x;
}

  

  最大值与最小值

基于二叉搜索树的性质,对任何结点x,其左子树中的关键字最大值不超过x.key,其右子树中的关键字的最小值不小于x.key。这样我们去寻找一棵二叉搜索树种的最大值或最小值可以简化操作为不断的寻找右/左孩子,直到对应的右/左子孩子的值为null。以最大值举例,这样找到的结点值一定不小于它的父节点,而这个父节点也一定不小于它父节点,依次直到根结点。根结点又一定不小于左子树中的任意结点。

  后继与前驱

    我们先来看看后序与前驱的定义。给定一棵二叉搜索树种的一个结点,如果按照中序遍历的次序查找它的后继,如果所有的关键字互不相同,则一个结点x的后继是大于x的.Key的最小关键字的结点。同样,结点x的前驱是小于x.Key的最大关键字的结点。二叉搜索树的结构允许我们通过没有任何关键字的比较来确定一个结点的后继。如果后继存在,这样的过程将返回结点x的后继,否则就返回Null。

SUCCESSOR(x)
{
if(x.right != null)
{
return TREE-MINIMUM(x.right); // 找到右子树中的最小值。
}
var y = x.p; while(y != null && x == y.right)
{
x = y;
y = y.p;
} return y;
}

  我们以上面寻找后继的伟算法为例,看看找后继结点的思路。前驱结点的寻找方式与之大同小异。根据后继的定义,它的值一定不小于x.key。因此,如果结点x的右子树存在,那么x.Right中的最小值就是x的后继。因为根据二叉搜索树的定义,这个最小值一定满足要求。如果x的右子树不存在,我们需要向x的祖先中寻找x的后继了。(x的左子树一定不满足要求,故舍去)。我们先找到x的父节点y,判读一下x是y的左孩子还是右孩子。如果x是左孩子,那x的父结点y就是x的后继。因为y.Key >= x.Key。如果x是y的右孩子,说明y的值仍小于x,此时我们应继续寻找。直到遇到根结点,或当期迭代的x是父节点的左孩子。

  另算法导论上有一个定理,即在一棵高度为h的二叉搜索树上,动态集合上的操作查找,最小值,最大值,寻找后继与前驱可以在O(h)的时间内完成。这里就不贴证明了。

 

  插入和删除

插入和删除一个结点会引起由二叉搜索树表示的动态集合的变化。一定要修改数据结构来反应这个变化,但修改要保持二叉搜索树性质的成立。我们来看看插入一个结点的伪算法

TREE-INSERT(T,z) // T为要插入的树,z为要插入的新结点
{
y = null;
x = T.root;
while(x != null) // 这个while是为了确定要插入的结点z的父结点
{
y = x;
if(z.key < x.key)
{
x = x.left;
}
else
{
x = x.right;
}
} z.p = y; if(y == null) //这里判断是否新插入的z为根结点,或z应该为其父结点的左孩子还是右孩子
{
T.root = z;
}
else if( z.key < y.key)
{
y.left = z;
}
else
{
y.right = z;
}
}

  插入的思路是这样的,首先我们找到树根结点x,然后用新插入的结点的值与x的值进行比较,来确定新插入的结点应该在树根的左子树还是右子树中。然后这样不断迭代下去最终确定新插入的结点z的父结点。然后第二步去修改其父结点的属性,确定该新插入的结点是否为根结点。或是父结点的左孩子还是右孩子。注意,插入与删除操作都会涉及到两部分的变化,一是被操作的结点本身,二是与该被操作的结点所关联的结点,如其父结点,孩子结点。

  删除

  从一棵二叉搜索树种删除结点可以分为下面三种情况:

  1. 如果要删除的结点z没有孩子结点,那么我们只要简单的将其删除,并修改z的父结点,用null来替换z。
  2. 如果要删除的结点z只有一个孩子结点,那么将这个孩子结点x替换到之前z结点的位置,并修改z结点的父节点,将之前指向z的孩子结点替换为x。
  3. 如果要删除的结点z有两个孩子,那么我们要先找到z的后继y(此时它一定在z的右子树中),然后用y替换z的位置。z原来的右子树应成为y的新的右子树,并且z的左子树成为y的新的左子树。这种情况有一些复杂,因为这与y(z的后继)是不是z的右孩子有关。
    • 如果y是z的右孩子,潜在的说明了这样一个条件。y没有左孩子。我们来回顾一下后继的定义就知道,z的后继一定是不小于z的最小值。假设y有左孩子,那么z的后继显然应该是y的左孩子而不是y,这与我们的条件矛盾。所以,这种情况下y只有右子树。因此,我们只需要用y来替换之前z在二叉搜索树种的位置,然后把z原来的左孩子赋给y即可。
    • 如果y不是z的右孩子,此时情况有些复杂。我们需要将z的右子树拿出来做一些调整,调整的结果为y为这棵新树的根结点。如果实现了这种调整,剩下的操作就与之前是一样了----只需要用y来替换之前z在二叉搜索树种的位置,然后把z原来的左孩子赋给y即可。

  为了实现删除操作,我们需要先定义这样一个子过程TRANSPLANT,它是用另一棵子树替换一棵子树并成为其双亲的孩子结点。当TRANSAPLANT用一棵以v为根的子树来替换一棵以u为根的子树时,结点u的双亲就变为结点v的双亲,并且最后v成为u的双亲的对应的孩子。

TRANSPLANT(T, u, v) //树T,原子树u,新子树v
{
if(u.p == null) //v成为u的双亲的对应的孩子。
{
T.root = v;
}
else if(u == p.left)
{
u.p.left = v;
}
else
{
u.p.right = v;
} if(v != null)
{
v.p = u.p; //结点u的双亲就变为结点v的双亲
}
}

  有了这个子过程,我们再来看看在树T中删除结点z的伪算法:

 DELETE(T, z)
{
if(z.left = null)
{
TRANSPLANT(T, z, z.right);
}
else if (z.right == null)
{
TRANSPLANT(T, z, z.left);
}
else
{
y = TREE-MINIMUM(z.right); //这里要注意一下,这里实际上取得的是y的后继!因为前面的讨论,当z有两个孩子时候,z的后继必然在z的右子树中,简化为直接取右子树的最小值作为后继! if(y.p != z) //注意这里的if里的逻辑,代表y不是z的右孩子,此时我们先用y的右孩子来替换y,然后把z的右孩子赋给y。这里其实是新生成了一颗已y为根结点的子树!
{
TRANSPLANT(T, y, y.right);
y.right = z.right;
y.right.p = y;
} TRANSPLANT(T,z,y);
y.left = z.left;
y.left.p = y;
} }

  有定理可以证明一棵高度为h的二叉搜索树,实现动态集合操作INSERT和DELETE的运行时间均为O(h).

  二叉搜索树,完结撒花✿✿ヽ(°▽°)ノ✿。

LeetCode刷题191130 --基础知识篇 二叉搜索树的更多相关文章

  1. LeetCode初级算法--树02:验证二叉搜索树

    LeetCode初级算法--树02:验证二叉搜索树 搜索微信公众号:'AI-ming3526'或者'计算机视觉这件小事' 获取更多算法.机器学习干货 csdn:https://blog.csdn.ne ...

  2. [LeetCode] 96. Unique Binary Search Trees 独一无二的二叉搜索树

    Given n, how many structurally unique BST's (binary search trees) that store values 1 ... n? Example ...

  3. [LeetCode] 99. Recover Binary Search Tree 复原二叉搜索树

    Two elements of a binary search tree (BST) are swapped by mistake. Recover the tree without changing ...

  4. [LeetCode] 98. Validate Binary Search Tree 验证二叉搜索树

    Given a binary tree, determine if it is a valid binary search tree (BST). Assume a BST is defined as ...

  5. [LeetCode] 96. Unique Binary Search Trees 唯一二叉搜索树

    Given n, how many structurally unique BST's (binary search trees) that store values 1...n? For examp ...

  6. [LeetCode] Unique Binary Search Trees II 独一无二的二叉搜索树之二

    Given n, generate all structurally unique BST's (binary search trees) that store values 1...n. For e ...

  7. [LeetCode] Convert BST to Greater Tree 将二叉搜索树BST转为较大树

    Given a Binary Search Tree (BST), convert it to a Greater Tree such that every key of the original B ...

  8. [LC] 108题 将有序数组转换为二叉搜索树 (建树)

    ①题目 将一个按照升序排列的有序数组,转换为一棵高度平衡二叉搜索树. 本题中,一个高度平衡二叉树是指一个二叉树每个节点 的左右两个子树的高度差的绝对值不超过 1. 示例: 给定有序数组: [-10,- ...

  9. 每日一题 - 剑指 Offer 36. 二叉搜索树与双向链表

    题目信息 时间: 2019-06-29 题目链接:Leetcode tag: 二叉搜索树 中序遍历 递归 深度优先搜索 难易程度:中等 题目描述: 输入一棵二叉搜索树,将该二叉搜索树转换成一个排序的循 ...

随机推荐

  1. 用workspace管理工程,并解决多静态库依赖

    from:http://www.cnblogs.com/perryxiong/p/3759818.html   最近我在项目中遇到一些工程之间的管理问题. 模型: 其中 库A 是一个公共的基础静态库, ...

  2. Springboot实现登录功能

    SpringBoot简介 Spring Boot是由Pivotal团队提供的全新框架,其设计目的是用来简化新Spring应用的初始搭建以及开发过程.该框架使用了特定的方式来进行配置,从而使开发人员不再 ...

  3. ARTS-S python抽象方法抽象类

    # coding: utf-8 from abc import ABC, abstractmethod class AbstractClassExample(ABC): def __init__(se ...

  4. 使用Vim编辑器,如何退出

    我们输入“冒号”,即":"(不需双引号),在下方会出现冒号,等待输入命令,我输入的是WQ.功能如下. W:write,写入 Q:quit,退出 再回车,就保存退出了 其实,保存退出 ...

  5. java获取每月的第一天和最后一天

    // 获取当前年份.月份.日期 Calendar cale = null; cale = Calendar.getInstance(); // 获取当月第一天和最后一天 SimpleDateForma ...

  6. android studio 刚安装需要配置的东西

    智能提示 调整log区域的字体 快捷键中文乱码 自动导入包 意思是创建成员变量的时候,以m开头 下载插件 提高编译的速度

  7. ansible 基础命令

    ansible 命令总结 1. Ad-HOC: 适合临时执行任务2. Playbook: 适合一些复杂的部署和配置环境 一 . Ad-HOC: 适合临时执行任务ansible-doc -l 查看ans ...

  8. 在Docker中跑Hadoop与镜像制作

      重复造轮子,这里使用重新打包生成一个基于Docker的Hadoop镜像:   Hadoop集群依赖的软件分别为:jdk.ssh等,所以只要这两项还有Hadoop相关打包进镜像中去即可: 配置文件准 ...

  9. 在 Spring Boot 项目中使用 activiti

    新建springBoot项目时勾选activiti,或者在已建立的springBoot项目添加以下依赖: <dependency> <groupId>org.activiti& ...

  10. Java连载62-使用throws关键字处理异常

    ​一.处理异常的两种方式 1.所有的编译时异常,要求程序员在编写程序阶段,必须对它进行处理,如果不处理的话,编译就会无法通过,处理异常有两种方法:捕捉和声明抛出. 2.捕捉:try.....catch ...