二分查找很好的解决了查找问题,将时间复杂度从 O(n)降到了O(logn)。 可是二分查找的前提条件是数据必须是有序的,而且具备线性的下标。 对于线性表,能够很好的应用二分查找,可是在插入和删除操做时则可能会形成整个线性表的动荡,时间复杂度达到了O(n) 链表更是无法应用二分查找。php
因而有了下面将要介绍的算法,其在查找、插入、删除都可以达到O(logn)的时间复杂度 —— 二叉查找树node
见名知意,其数据结构基础为二叉树,初次接触到二叉树时并无感受到其有什么突出之处。但看到经过二叉树构建出的二叉查找树方案时,确被深深的震撼了。git
二叉查找树(英语:Binary Search Tree),也称二叉搜索树、有序二叉树(英语:ordered binary tree),排序二叉树(英语:sorted binary tree),是指一棵空树或者具备下列性质的二叉树:程序员
若任意结点的左子树不空,则左子树上全部结点的值均小于它的根结点的值; 若任意结点的右子树不空,则右子树上全部结点的值均大于它的根结点的值; 任意结点的左、右子树也分别为二叉查找树; 没有键值相等的结点。github
根据上面的规则咱们先来定义一颗二叉树 算法
这里能够很容易看出其规律,不须要过多的解释。bash
如今再插入一个元素13。 13>12因此往右边走来到14,13 < 14则左走,发现14没有左孩子,因此将13插入之,获得下面这张图 数据结构
按照上面插入的思路,能够很容易实现搜索操做。而且发现其查找的时间复杂度就为这颗树的深度。单元测试
根据彻底二叉树的性质,具备n个结点的彻底二叉树的深度为
[logn] + 1
测试
忽略掉+1
获得二叉查找树的查找时间复杂度为 O(logn)
,可是实际上并不是如此,后面咱们分析。
二叉树的遍历有前序、中序、后序遍历三种方式,这里着重介绍后序遍历。 对二差查找树进行中序遍历时,能够获得一个asc
的排序结果。如上面的树中序遍历的结果是 3, 8, 9, 12, 13, 14。 中序遍历从一颗子树最左的节点开始输出,既该树的最小值
。实现中序遍历只须要将数据收集点置于左递归点与右递归点之间,这样说仍是有些含糊了,看代码吧
/**
* 中序遍历
* @param $root
* @return array
*/
public function inorder($root)
{
$data = [];
if ($root->left) {
$data = array_merge($data, $this->inorder($root->left)); //左孩子递归点
}
$data[] = $root->data; // 这里是中序遍历的数据收集点
if ($root->right) {
$data = array_merge($data, $this->inorder($root->right)); // 右孩子递归点
}
return $data;
}
复制代码
前驱与后继, 以9节点为例, 12属于9的后继,8属于9的前驱。
咱们给这颗树多加几个结点
删除树中的结点分为不少种状况,如被删除的结点不存在子结点,只存在左子树/右子树,左右子树都存在,这里已覆盖率最广的左右子树都存在为例。
分析一个需求时要并非需求存在多少中状况咱们就写多少种状况。而应该分析状况之间的关系,是否存在重复,或者属于关系等,程序员应该作的就是提取需求的本质,力求于最简洁的实现
如今咱们打算删除25这个结点,你会怎么作? 若是只是简单把18来顶替原来25的位置,则须要对18这颗子树的孩子们进行从新调整。18只有三个孩子还好,可是当孩子成千上万时,显然会形成大面积的调整。 因此我但愿可以找到一个更好的节点来代替25,按照算法导论中的描述,咱们应该寻找该结点的前驱或者后继来代替,好比图中的24和27分别是25的前驱和后继。
为何要使用前驱或者后缀来代替?这点我十分不肯定,我给本身的理由是
- 该结点是一个特殊值,属于某颗子树的最大值或者最小值,具备肯定性,能够被比较好的定义且查找出来。
- 因为该结点属于被删除节点的前驱或者后继,则删除该结点对数据结构形成的影响最小。我并不肯定是对什么的数据结构形成的影响最小
上面描述的状况的图解以下 ↓
删除还存在一些其余的状况,好比下面这种状况↓
对于这种状况直接将30提高到25便可,接下来看一下看php的代码实现:
public function delete($root, $data)
{
if (!$root) {
return null;
}
if ($root->data === $data) {
if ($root->left) {
// 左转
$node = $root->left;
$parent = $root;
$toward = 'left';
while ($node->right) {
$parent = $node;
$toward = 'right';
$node = $node->right;
}
$root->data = $node->data;
$parent->{$toward} = $this->delete($node, $node->data);
} else {
return $root->right;
}
} elseif ($root->data > $data) {
// 若是root的左孩子没有被删除,那就原样返回回来, 若是被删除了,那就找个孩子代替
$root->left = $this->delete($root->left, $data);
} else {
$root->right = $this->delete($root->right, $data);
}
return $root;
}
复制代码
因为php有内存回收机制,所以咱们没有办法像c同样直接去修改内存,因此这里借助递归的特性来解决这个问题 $root->left = $this->delete($root->left, $data);
作相似这样一个处理,这可能会有些理解上的困难。但总归仍是可以明白的~
除了递归解决外,也能够用下面这种办法。 即定义一个parent和toward来作一个导向,这在上面的代码中也有体现。该方法更加适用于迭代处理
$parent = $root;
$toward = 'left';
while ($node->right) {
$parent = $node;
$toward = 'right';
$node = $node->right;
}
复制代码
更详细的实习细节和调用示例请参考单元测试。
因为php没有像js同样的字面量对象或者c同样的struct。所以直接使用对象来表示树中的结点
class BiTNode
{
public $data;
public $left;
public $right;
public function __construct($data, $left = null, $right = null)
{
$this->data = $data;
$this->left = $left;
$this->right = $right;
}
}
复制代码
在查找的时候指出了,二叉查找树的查询的时间复杂度并非严格意义上的O(logn) 是由于有这样的状况发生, 假设须要插入 12, 10, 9, 5, 4, 1这几个数据,那么咱们会获得这样一颗歪脖子树
固然二叉查找树依旧是各类树的根基,还请认真理解。