算法导论实验三 树
二叉查找树、红黑树的基本操作实现
文章目录
一、实现原理
1.1 二叉查找树(Binary Search Tree)
二叉查找树又称二叉搜索树,二叉排序树,特点如下:
- 左子树上所有结点值均小于根结点
- 右子树上所有结点值均大于根结点
- 结点的左右子树本身又是一颗二叉查找树
- 二叉查找树中序遍历得到结果是递增排序的结点序列
二叉排序树如图所示:
1.2 红黑树
红黑树中每个结点包含五个域:color,key,left,right和p。如果某结点没有一个子结点或父节点,则该域指向NIL。
一颗二叉树如果满足下面的红黑性质,则为一颗红黑树:
- 每个结点或是红的,或是黑的
- 根结点是黑的
- 每个叶结点(NIL)是黑的
- 如果一个结点是红的,则它的两个儿子都是黑的
- 对每个结点,从该结点到其子孙结点的所有路径上都包含相同数目的黑结点。
红黑树的图如下图所示:
从某个结点 x 出发 (不包括该结点) 到达一个叶结点的任意一条路径上,黑 色结点的个数称为该结点 x 的黑高度,用 bh(x) 表示。
引理:一颗有 n 个内结点的红黑树的高度至多为 2lg(n+1)。
典型操作:其旋转、插入、删除、扩张操作请见教材 P165-P186
1.3 二者性能比较
二、实验要求
2.1 实现下列关于二叉查找树、红黑树的判断、构建、删除等操作。并写出这些操作的流程图或伪代码
2.1.1 二叉查找树的相关操作
Pro1:Given a binary tree, determine if it is a valid binary search tree (BST).
Assume a BST is defined as follows:
1 The left subtree of a node contains only nodes with keys less than the node’s key.
2 The right subtree of a node contains only nodes with keys greater than the node’s key.
3 Both the left and right subtrees must also be binary search trees
e.g:
即验证二叉搜索树,判断每个结点的值是否满足二叉搜索树所规定的范围,因此使用递归的方式来判断,定义函数bool digui(TreeNode* node,int lower,int upper),如果出现结点的值不在(lower,upper)中,则返回不符合二叉搜索树。代码如下所示:
class Solution {
public:
bool helper(TreeNode* node,long lower,long upper){
if(node == nullptr){
return true;
}
if(node->val <= lower || node->val >= upper){
return false;
}
return helper(node->left,lower,node->val) && helper(node->right,node->val,upper);
}
bool isValidBST(TreeNode* root) {
return helper(root,LONG_MIN,LONG_MAX);
}
};
测试用例运行截图如下所示:
时间复杂度为O(n),n为结点个数,在递归调用时每个二叉树的结点最多被访问一次。
空间复杂度为O(n)。递归函数在递归过程中需要为每一层的递归函数分配栈空间,最坏情况下二叉树为一条链,因此递归树高度为n层,最坏情况下空间复杂度为O(n)
二叉树的插入:
分析:二叉树的插入比较简单,从根结点遍历树,找到结点插入的位置即可,插入代码如下所示:
#include <iostream>
using namespace std;
struct Node {
int data;
Node* left;
Node* right;
Node(int data) {
this->data = data;
this->left = nullptr;
this->right = nullptr;
}
};
class BinaryTree {
public:
Node* root;
BinaryTree() {
root = nullptr;
}
void addNode(int data) {
Node* newNode = new Node(data);
if (root == nullptr) {
root = newNode;
} else {
Node* focusNode = root;
Node* parent;
while (true) {
parent = focusNode;
if (data < focusNode->data) {
focusNode = focusNode->left;
if (focusNode == nullptr) {
parent->left = newNode;
return;
}
} else {
focusNode = focusNode->right;
if (focusNode == nullptr) {
parent->right = newNode;
return;
}
}
}
}
}
void preOrderTraversal(Node* focusNode) {
if (focusNode != nullptr) {
std::cout << focusNode->data << " ";
preOrderTraversal(focusNode->left);
preOrderTraversal(focusNode->right);
}
}
};
int main() {
BinaryTree tree;
tree.addNode(50);
tree.addNode(25);
tree.addNode(75);
tree.addNode(12);
tree.addNode(37);
tree.addNode(43);
tree.addNode(30);
tree.preOrderTraversal(tree.root);
return 0;
}
程序对树的前序遍历输出结果为:
与分析的结果一致,因此,代码正确。
二叉树的删除伪代码如下所示:
//该段伪代码TRANSPLANT为辅助过程,即在二叉树T中,将以v为根的子树替换以u为根的子树
TRANSPLANT(T,u,v){
if u .p == NIL{
T.root = v;
}
else if u == u.p.left{
u.p.left = v;
}
else u.p.right = v;
if v != nil{
v.p = u.p;
}
}
//该段代码即在树T中删除结点z
TREE_DELETE(T,z){
//case1:删除结点没有左子结点,直接右子树替换删除结点即可
if z.left == nil{
TRANSPLANT(T,z,z.right);
}
//case2:删除结点没有右子结点,直接左子树替换删除结点即可
if z.right == nil{
TRANSPLANT(T,z,z.left);
}
//case 3,有左右孩子
else{
y = TREE_MINIMUM(z.right);//找到删除结点的直接后继结点
if y.p != z{
//即直接后继结点不是y的右子结点
TRANSPLANT(T,y,y.right);//先用y的右子节点来替换y
y.right = z.right;
y.right.p = y;
}
//用y结点代替z结点
TRANSPLANT(T,z,y);
y.left = z.left;
y.left.p = y;
}
}
二叉树的插入和删除时间复杂度均为O(h)h为二叉树的高度
2.1.2 红黑树
Pro2:Construct a red black tree and delete
Construct a red black tree by {1,5,6,7,8,9,10,11,12,13,14,15}, show the result. Then delete the points in order {14,9,5}, show the result too.
e.g:
红黑树插入,首先声明插入的结点必为红色结点(因为只有插入红色结点,才有可能不破坏红黑树的性质,插入黑色结点必会破坏当前红黑树的性质)
伪代码为:
//该函数是将z结点插入红黑树T中
RB-INSERT(T,z){
y = T.nil;
x = T.root;
//遍历寻找z结点要插入的位置
while(x != T.nil){
y = x;
if z.key < x.key{
x = x.left;
}
else{
x = x.right;
}
}
z.p = y;
if y == T.nil{
//即插入的红黑树为空的
T.root = z;
}
else if z.key < y.key{
//插入红黑树的结点比父节点小
y.left = z;
}
else{
y.right = z;
}
z.left = T.nil;
z.right = T.nil;
z.color = RED;
//先将结点当作红结点插入树中,再对树调用RB-INSERT-FIXUP来维持红黑树性质
RB-INSERT-FIXUP(T,z)
}
RB-INSERT-FIXUP(T,z){
//只有插入后连续两个红色结点才需要调整
while(z.p.color == RED){
if(z.p = z.p.p.left){
//插入结点的父节点是爷结点的左孩子
y = z.p.p.right;//找到插入结点的叔结点
//case1:叔结点为红色结点
if y.color == RED{
z.p.color = BLACK;
y.color = BLACK;
z.p,p.color = BLACK;
z = z.p.p;
}
//case2:叔结点为黑色
else{
if z == z.p.right{
z = z.p;
LEFT-ROTATE(T,z);//左旋
}
z.p.color = BLACK;
z.p.p.color = RED;
RIGHT-ROTATE(T,z.p.p)//右旋
}
}
else (同理,即z.p == z.p.p.right){
//将上述情况代码中的left换为right,right换为left,其它不变
}
}
T.root.color = BLACK;//将根结点颜色变为黑色
}
插入示例红黑树的流程图(省略了叶结点的T.nil)如下所示:
因此最后中序遍历输出为
1 black
5 black
6 black
7 black
8 black
9 red
10 black
11 black
12 black
13 red
14 black
15 red
红黑树的删除伪代码如下所示:
//首先辅助过程RB-TRANSPLANT
RB-TRANSPLANT(T,u,v){
if u.p == T.nil{
T.root = v;
}
else if u == u.p.left{
u.p.left = v;
}
else u.p.right = v;
v.p = u.p;
}
//在红黑树中删除结点的函数
RB-DELETE(T,z){
y = z;
y-original-color = y.color;
//删除结点无左结点
if z.left == T.nil{
x = z.right;
RB-TRANSPLANT(T,z,z.right);
}
//删除结点无右结点
else if z.right == T.nil{
z = z.left;
RB-TRANSPLANT(T,z,z.left);
}
//删除结点有左右结点
else {
y = TREE-MINIMUM(z.right);
y-original-color = y.color;
x = y.right;
if y.p == z{
//直接后继节点是删除结点的右子节点
x.p = y;
}
else{
//直接后继结点不是删除结点的右孩子
RB-TRANSPLANT(T,y,y.right);
y.right = z.right;
y.right.p = y;
}
RB-TRANSPLANT(T,z,y);
y.left = z.left;
y.left.p = y;
y.color = z.color;
}
if y-original-color == BLACK{
RB-DELETE-FIXUP(T,x);
}
}
RB-DELETE-FIXUP(T,x){
while( x != T.root and x.color == BLACK){
if x == x.p.left{
w = x.p.right;
if w.color == RED{
//case1:兄弟结点为红色结点
w.color = BLACK;
x.p.color = RED;
LEFT-ROTATE(T,x.p);
w = x.p.right;
}
if w.left.color == BLACK and w.right.color == BLACK{
//case2:兄弟结点是黑色的,且兄弟结点的两个子结点均是黑色的
w.color = RED;
x = x.p;
}
else{
if w.right.color == BLACK{
//case3:兄弟结点是黑色的,且兄弟结点的左子结点是红色的,右子结点是黑色的
w.left.color = BLACK;
w.color = RED;
RIGHT-ROTATE(T,w);
w = x.p.right;
}
//case4:兄弟结点是黑色的,且兄弟结点的右孩子是红色的
w.color = x.p.color;
x.p.color = BLACK;
w.right.color = BLACK;
LEFT-ROTATE(T,z.p);
x = T.root;
}
}
else{同理,left。right替换即可}
}
x.color = BLACK;
}
红黑树的删除流程如下图所示:
因此最后中序遍历输出为
1 red
6 black
7 blackj
8 red
10 black
11 black
12 black
13 black
15 black
2.2 请说明二叉查找树和红黑树的区别以及时间、空间性能
- 二叉查找树是红黑树的基础,即红黑树一定满足二叉查找树的特性。二叉查找树在极端情况下会出现一条链表的情况,因此出现了红黑树来解决极端情况下性能差的问题。红黑树可以保证二叉树最长的一条路径不会大于最短路径的两倍。
- 二叉查找树的插入、删除操作都需要O(h)的时间复杂度,(h为树的高度,最小logn,最大n)。红黑树的插入、删除操作需要O(logn)的时间复杂度。空间复杂度二者均为O(n),即用n个结点存储数据,无需赘述。
总结
本次实验主要熟悉并实践了二叉查找树和红黑树的插入、删除、判断操作以及流程,红黑的相关操作要比二叉查找树复杂不少。通过该实验,让我对于这两种树有了更深的理解,知识得到了很大的提升。