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

二叉搜索树

  搜索树数据结构支持许多动态集合操作,包括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. luogu P2812 校园网络【[USACO]Network of Schools加强版】|Tarjan

    题目背景 浙江省的几所OI强校的神犇发明了一种人工智能,可以AC任何题目,所以他们决定建立一个网络来共享这个软件.但是由于他们脑力劳动过多导致全身无力身体被♂掏♂空,他们来找你帮助他们. 题目描述 共 ...

  2. iOS App Extension入门

    转自简书:http://www.jianshu.com/p/8cf08db29356   iOS 10推出了很多新功能,其中有几个高调的变化:通知栏更加实用,电话可以防骚扰,iMessage变得更加有 ...

  3. SpringBoot系列之集成Druid配置数据源监控

    SpringBoot系列之集成Druid配置数据源监控 继上一篇博客SpringBoot系列之JDBC数据访问之后,本博客再介绍数据库连接池框架Druid的使用 实验环境准备: Maven Intel ...

  4. 最全的防火墙(firewalld)

    第1章  防火墙的介绍 1.1  防火墙的介绍 1.1.1 概念 动态管理防火墙服务(图形界面和linux界面都可以实现) 支持不同防火墙的区域信息 属于传输层次的防火墙 1.1.2 防火墙的默认规则 ...

  5. 使用ExcelPackage进行Excel报表

    Nuget包名为 epplus.core 命名空间OfficeOpenXml string localFileName = path + Path.DirectorySeparatorChar + f ...

  6. 【React】393 深入了解React 渲染原理及性能优化

    如今的前端,框架横行,出去面试问到框架是常有的事. 我比较常用React, 这里就写了一篇 React 基础原理的内容, 面试基本上也就问这些, 分享给大家. React 是什么 React是一个专注 ...

  7. 常见排序汇总C&C++

    常见排序主要有以下四种: 1.交换排序 2.选择排序 3.插入排序 4.归并排序 (以下代码基本都有输出每步排序结果) 一.交换排序 交换排序主要是冒泡排序和快排 1.冒泡排序 流程: (1)对数组中 ...

  8. Kubernetes服务发现入门:如何高效管理服务?

    愈发复杂的应用程序正在依靠微服务来保持可扩展性和提升效率.Kubernetes为微服务提供了完美的环境,并能够让其与Kubernetes的工具组件和功能兼容.当应用程序的每个部分放置在一个容器中,整个 ...

  9. CCF-CSP题解 201809-3 元素选择器

    题目要求写一个简易的CSS Selector. 首先用结构体\(<lev,label[],hasId,id[]>\)存储元素.其中\(lev\)表示元素在html树中的深度(这个是因为逻辑 ...

  10. java设计模式(一)动态代理模式,JDK与CGLIB分析

    -本想着这个知识点放到Spring Aop说说可能更合适一点,但因为上一篇有所提到就简单分析下,不足之处请多多评论留言,相互学习,有所提高才是关键! 什么是代理模式: 记得有本24种设计模式的书讲到代 ...