本系列文章
(1) 数据结构浅析(1)-什么是数据结构?
(2) 数据结构浅析(2)-集合
(3) 数据结构浅析(3)-线性结构:数组
(4) 数据结构浅析(4)-线性结构:线性表
(5) 数据结构浅析(5)-线性结构:串
(6) 数据结构浅析(6)-线性结构:栈
(7) 数据结构浅析(7)-线性结构:队列和双端队列
(8) 数据结构浅析(8)-树性结构:树和无序树
(9) 数据结构浅析(9)-树性结构:有序树和二叉树
(10) 数据结构浅析(10)-树性结构:堆
本文目标
试着理解什么是二叉查找树 (Binary Search Tree) 和笛卡尔树 (Cartesian tree),同属于二叉树一类。
什么是二叉查找树
以下定义来自维基百科:
二叉查找树(英語:Binary Search Tree),也称为二叉搜索树、有序二叉树(ordered binary tree)或排序二元樹(sorted binary tree),是指一棵空树或者具有下列性质的二叉树:
- 若任意节点的左子树不空,则左子树上所有节点的值均小于它的根节点的值;
- 若任意节点的右子树不空,则右子树上所有节点的值均大于或等于它的根节点的值;
- 任意节点的左、右子树也分别为二叉查找树。
二叉查找树相比于其他数据结构的优势在于查找、插入的时间复杂度较低。为O(log n)。二叉查找树是基础性数据结构,用于构建更为抽象的数据结构,如集合、多重集、关联数组等。
二叉查找树的性质
对二叉查找树进行中序遍历(什么是中序遍历,请看我这篇文章中关于树的遍历分类)的话,我们可以得到有序的数列。
以下面图片中的二叉查找树为例。
我们可以得到一个从小到大排列的有序数列:1, 3, 4, 6, 7, 8, 10, 13, 14。
二叉查找树的时间复杂度
搜索/查找 过程细节
过程如下:
1) 先从根节点开始找,如果查找的节点的值和根节点的值相同,成功;否则执行下一步
2) 判断查找的节点的值 与 根节点的值得大小。若小于根节点的值,在左子树中递归查找
3) 若大于根节点的值,在右子树中递归查找
4) 若找到最后还是没有匹配的,则不存在;查找不成功
插入过程细节
过程如下:
1) 若当前的二叉查找树位空,直接将目标节点加入作为根节点即可;若不是空的,执行下一步
2) 若目标节点的值比根节点的值小,将目标节点插入到其左子树中
3) 若目标节点的值比根节点的值大,将目标节点插入到其右子树中
删除过程细节
二叉查找树的删除要分3种情况,具体过程如下:
1)若目标节点P 为叶子节点,即PL(左子树)和PR(右子树)均为空树。由于删去叶子结点不破坏整棵树的结构,则只需修改其双亲结点的指针即可。
如下图,图片来源GeeksforGeeks,直接删除20 即可。
2)若目标结点P 有一个子树,即只有左子树PL或右子树PR,此时只要令PL或PR直接成为P 节点的parent 结点f的左子树(当p是左子树)或右子树(当p是右子树)即可,作此修改也不破坏二叉查找树的特性。
如下图,图片来源GeeksforGeeks,删除30,将40移到原先30的位置。
3)若目标结点P 的左子树和右子树均不空。在删去p之后,为保持其它元素之间的相对位置不变,可按中序遍历(inorder)保持有序进行调整。可以有两种做法:其一是令p 的左子树为P 的parent 节点f 的左/右(根据p 是f 的左子树还是右子树而定)子树,s为p 左子树的最右下的结点,而p 的右子树为s 的右子树;其二是令p 的直接前继(in-order predecessor)或直接后继(in-order successor)来替代p,然后再从二叉查找树中删去它的直接前继(或直接后继)。
如下图,图片来源GeeksforGeeks,删除50。我们先找到50 的直接后继(inorder successor)即60,然后复制60 这个节点的Value 到原先50 这个节点上并删除60这个节点。当然直接前继(in-order predecessor)在另外一些情况下会被使用。
以上文字内容选自并修改于维基百科
什么是笛卡尔树
以下定义内容来自于维基百科。
笛卡尔树是一种特定的二叉树数据结构,可由数列构造,在范围最值查询、范围top k查询(range top k queries)等问题上有广泛应用。它具有堆的有序性,中序遍历可以输出原数列。笛卡尔树结构由Vuillmin 在解决范围搜索的几何数据结构问题时提出。
笛卡尔树的每个节点有两个值,key 为分配的优先级,是一个堆 和 val 为节点的值,中序遍历可以输出原数列。笛卡尔树只有建树操作,没有后续的插入,删除等操作,因此建树后的操作通常需要用Treap实现。
笛卡尔树的性质
无相同元素的数列构造出的笛卡尔树具有下列性质:
- 结点一一对应于数列元素。即数列中的每个元素都对应于树中某个唯一结点,树结点也对应于数列中的某个唯一元素
- 中序遍历(in-order traverse)笛卡尔树即可得到原数列。即任意树结点的左子树结点所对应的数列元素下标比该结点所对应元素的下标小,右子树结点所对应数列元素下标比该结点所对应元素下标大。
- 树结构存在堆序性质,即任意树结点所对应数值大/小于其左、右子树内任意结点对应数值
笛卡尔树的建树操作
以下内容来自于GeeksforGeeks:
假设我们有一个array: [5, 10 ,40, 30, 28],那么一个有最大堆的笛卡尔树会如下图显示(最大堆:每个节点都大于其子节点):
假设我们有一个array: [5, 10 ,40, 30, 28],那么一个有最小堆的笛卡尔树会如下图显示(最小堆:每个节点都小于其子节点):
与Treap 的区别
笛卡尔树是二叉树,对于数列而言将其作为二叉搜索树是自然的。
若将二叉搜索树结点关联上一个权值,并且保证此权值在树结构中遵循堆中的序关系,即父结点权值比子结点权值大,则此二叉搜索树又被称为Treap。其名称来源于树与堆两英文词的组合(tree + heap -> treap)。
Treap与笛卡尔树在结构上是相同的,只是两者的应用不同。
两种树的实现
二叉查找树的实现
用preorder 遍历来构造一棵二叉查找树,代码来源:Construct BST from given preorder traversal
// 先构造一个 binary tree 的节点
class Node {
int data;
Node left, right;
Node(int d) {
data = d;
left = right = null;
}
}
class Index {
int index = 0;
}
class BinaryTree {
Index index = new Index();
// 用一个递归程序从pre[] 中来构造BST
// preIndex 用来追踪pre[] 中的指数
Node constructTreeUtil(int pre[], Index preIndex, int key,
int min, int max, int size) {
// Base case
if (preIndex.index >= size) {
return null;
}
Node root = null;
// 如果pre[] 中现有的元素在范围之内,那么它仅仅是现有子树的部分
if (key > min && key < max) {
// 为此子树的根节点配置内存并且增加preIndex
root = new Node(key);
preIndex.index = preIndex.index + 1;
if (preIndex.index < size) {
// 建造一个root 节点的子树;
// 其所有在{min...key} 范围之内的节点会到左子树
// 其中第一个节点会成为左子树的root
// will go in left subtree, and first such
// node will be root of left subtree.
root.left = constructTreeUtil(pre, preIndex,
pre[preIndex.index], min, key, size);
// 其所有在{key...max} 范围之内的节点会到右子树
// 其中第一个节点会成为右子树的root
root.right = constructTreeUtil(pre, preIndex,
pre[preIndex.index], key, max, size);
}
}
return root;
}
// 这个方程主要用preorder 遍历构建了BST
// 这个方程最主要用了 constructTreeUtil()
Node constructTree(int pre[], int size) {
int preIndex = 0;
return constructTreeUtil(pre, index, pre[0], Integer.MIN_VALUE,
Integer.MAX_VALUE, size);
}
// 方程用来打印出BT 的Inorder 遍历节点
void printInorder(Node node) {
if (node == null) {
return;
}
printInorder(node.left);
System.out.print(node.data + " ");
printInorder(node.right);
}
// 测试方程
public static void main(String[] args) {
BinaryTree tree = new BinaryTree();
int pre[] = new int[]{10, 5, 1, 7, 40, 50};
int size = pre.length;
Node root = tree.constructTree(pre, size);
System.out.println("Inorder traversal of the constructed tree is ");
tree.printInorder(root);
}
}
// 以上代码贡献者: Mayank Jaiswal
Output:
Inorder traversal of Binary Tree:
1 5 7 10 40 50
Time Complexity: O(n)
在下一篇文章中,将着重讲的是树形结构(5) - 自平衡二叉查找树和 AVL树。
如果你觉得我的文章有用,顺手点个赞,关注下我的专栏或则留下你的评论吧!