9.1 KiB
title | localeTitle |
---|---|
Binary Search Trees | 二叉搜索树 |
二叉搜索树
树是由具有以下特征的节点组成的数据结构:
- 每棵树都有一个根节点(在顶部)有一些值。
- 根节点具有零个或多个子节点。
- 每个子节点都有零个或多个子节点,依此类推。这会在树中创建一个子树。每个节点都有自己的子树,由他的孩子和他们的孩子等组成。这意味着每个节点本身都可以是一棵树。
二叉搜索树(BST)添加了以下两个特征:
- 每个节点最多包含两个子节点。
- 对于每个节点,其左后代节点的值小于当前节点的值,而当前节点的值小于右后代节点(如果有的话)。
BST建立在二进制搜索算法的基础上,允许快速查找,插入和删除节点。它们的设置方式意味着,平均而言,每次比较都允许操作跳过大约一半的树,因此每次查找,插入或删除都需要与树中存储的项目数的对数成比例的时间, O(log n)
。然而,有时候最糟糕的情况可能发生,当树不平衡时,所有这三个函数的时间复杂度都是O(n)
。这就是为什么自平衡树(AVL,红黑等)比基本BST更有效的原因。
**最糟糕的情况示例:**当您继续添加_始终_大于节点之前的节点(它的父节点)时会发生这种情况,当您始终添加值低于其父节点的节点时,也会发生同样的情况。
BST的基本操作
- 创建:创建一个空树。
- 插入:在树中插入一个节点。
- 搜索:在树中搜索节点。
- 删除:从树中删除节点。
创建
最初创建没有任何节点的空树。必须指向根节点的变量/标识符用NULL
值初始化。
搜索
您总是开始在根节点搜索树并从那里向下移动。您将每个节点中的数据与您要查找的数据进行比较。如果比较的节点不匹配,那么您可以继续使用右子项或左子项,这取决于以下比较的结果:如果您要搜索的节点低于您要比较的节点,你继续前往左边的孩子,否则(如果它更大)你会去找右边的孩子。为什么?因为BST是结构化的(根据其定义),正确的孩子总是比父母大,而左孩子总是较小。
插入
它与搜索功能非常相似。您再次从树的根开始并递归下去,搜索插入新节点的正确位置,方法与搜索功能中说明的相同。如果树中已存在具有相同值的节点,则可以选择是否插入副本。有些树允许重复,有些则不允许。这取决于具体的实施。
删除
当您尝试删除节点时,可能会发生3种情况。如果有,
- 没有子树(没有孩子):这个是最简单的子树。您只需删除节点,无需任何其他操作。
- 一个子树(一个子树):您必须确保在删除节点后,其子节点将连接到已删除节点的父节点。
- 两个子树(两个子节点):您必须找到并替换要删除的节点及其后续节点(右侧子树中最常用的节点)。
创建树的时间复杂度为O(1)
。搜索,插入或删除节点的时间复杂度取决于树h
的高度,因此最坏的情况是O(h)
。
节点的前身
前置任务可以被描述为在您当前所在节点之前的节点。要查找当前节点的前一个节点,请查看左子树中最右侧/最大的叶节点。
节点的后继者
后继者可以被描述为在您当前所在节点之后的节点。要查找当前节点的后继节点,请查看右侧子树中最左侧/最小的叶节点。
特殊类型的BT
- 堆
- 红黑树
- B树
- Splay树
- N-ary树
- Trie(基数树)
运行
数据结构:数组
- 最坏情况表现:
O(log n)
- 最佳表现:
O(1)
- 平均表现:
O(log n)
- 最坏情况的空间复杂度:
O(1)
其中n
是BST中的节点数。
BST的实施
这是一个BST节点的定义,它有一些数据,引用它的左右子节点。
struct node {
int data;
struct node *leftChild;
struct node *rightChild;
};
搜索操作
每当要搜索元素时,从根节点开始搜索。然后,如果数据小于键值,则在左子树中搜索元素。否则,搜索右子树中的元素。对每个节点遵循相同的算法。
struct node* search(int data){
struct node *current = root;
printf("Visiting elements: ");
while(current->data != data){
if(current != NULL) {
printf("%d ",current->data);
//go to left tree
if(current->data > data){
current = current->leftChild;
}//else go to right tree
else {
current = current->rightChild;
}
//not found
if(current == NULL){
return NULL;
}
}
}
return current;
}
插入操作
无论何时插入元素,首先要找到其正确的位置。从根节点开始搜索,然后如果数据小于键值,则在左子树中搜索空位置并插入数据。否则,在右子树中搜索空位置并插入数据。
void insert(int data) {
struct node *tempNode = (struct node*) malloc(sizeof(struct node));
struct node *current;
struct node *parent;
tempNode->data = data;
tempNode->leftChild = NULL;
tempNode->rightChild = NULL;
//if tree is empty
if(root == NULL) {
root = tempNode;
} else {
current = root;
parent = NULL;
while(1) {
parent = current;
//go to left of the tree
if(data < parent->data) {
current = current->leftChild;
//insert to the left
if(current == NULL) {
parent->leftChild = tempNode;
return;
}
}//go to right of the tree
else {
current = current->rightChild;
//insert to the right
if(current == NULL) {
parent->rightChild = tempNode;
return;
}
}
}
}
}
二进制搜索树(BST)还使我们能够快速访问前辈和后继者。 前置任务可以被描述为在您当前所在节点之前的节点。
- 要查找当前节点的前任,请查看左子树中最右边/最大的叶节点。 后继者可以被描述为在您当前所在节点之后的节点。
- 要查找当前节点的后继节点,请查看右子树中最左侧/最小的叶节点。
让我们看一下在树上运行的几个过程。
由于树是递归定义的,因此编写在树本身上递归的例程是很常见的。
因此,例如,如果我们想要计算树的高度,即根节点的高度,我们可以继续并递归地执行,通过树。所以我们可以说:
- 例如,如果我们有一个nil树,那么它的高度为0。
- 否则,我们是1加上左子树和右子树的最大值。
- 因此,如果我们查看一个叶子,例如,那个高度将是1,因为左子的高度是nil,是0,nil右子的高度也是0.所以最大值是0,然后是1加0。
高度(树)算法
if tree = nil:
return 0
return 1 + Max(Height(tree.left),Height(tree.right))
这是C ++中的代码
int maxDepth(struct node* node)
{
if (node==NULL)
return 0;
else
{
int rDepth = maxDepth(node->right);
int lDepth = maxDepth(node->left);
if (lDepth > rDepth)
{
return(lDepth+1);
}
else
{
return(rDepth+1);
}
}
}
我们还可以考虑计算树的大小,即树的节点数。
- 同样,如果我们有一个零树,我们有零节点。
- 否则,我们有左子节点中的节点数加上我们自己的1节点加上右子节点中的节点数。所以1加上左树的大小加上右树的大小。
大小(树)算法
if tree = nil
return 0
return 1 + Size(tree.left) + Size(tree.right)
这是C ++中的代码
int treeSize(struct node* node)
{
if (node==NULL)
return 0;
else
return 1+(treeSize(node->left) + treeSize(node->right));
}
关于freeCodeCamp YouTube频道的相关视频
以下是常见的二叉树类型:
完整二叉树/严格二叉树:如果每个节点只有0或2个子节点,则二叉树已满或严格。
18
/ \
15 30
/ \ / \
40 50 100 40
在完全二进制树中,叶节点的数量等于内部节点的数量加1。
完整的二进制树:二进制树是完整的二进制树,如果所有级别都被完全填充,除了可能是最后一级,最后一级是尽可能保留所有键
18
/ \
15 30
/ \ / \
40 50 100 40
/ \ /
8 7 9