栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > C/C++/C#

树 - 03 二叉排序树

C/C++/C# 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

树 - 03 二叉排序树

写在前面:
上一讲我们介绍了二叉树的代码实现以及其遍历方法,这一讲我们来看一看进阶版的二叉树 —— 二叉排序树(二叉搜索树)。第一次接触这一讲知识点的朋友一时反应不过来不用太焦虑,其实是非常正常的,看讲解以及代码的同时动手画一画或者实现以下会让你更容易理解~

性质

二叉排序树它和普通的二叉树不同:
(1)如果根结点的左子树不为空,那么它左子树的所有结点的值都小于根结点。
(2)如果根结点的右子树不为空,那么它右子树的所有结点的值都大于根结点。
(3)根结点的左子树和右子树同样遵循上面两条性质。
根据性质我们可以知道,这颗排序二叉树的实现必然逃不了递归了,接下来我们来看看排序二叉树的代码是如何实现的。

二叉排序树的构建及插入

假设我们要构建起下面这颗二叉排序树,要进行如何操作呢:

(1)构建根结点 8 。

(2)插入结点 2 ,可以发现比根结点小,故插入根结点的左子树。

(3)插入结点 9 ,可以发现比根结点大,故插入根结点的右子树。

(4)插入结点 11 ,可以发现比根结点右孩子的值还要大,所以就插入到根结点右孩子的右子树。

(5)插入结点 23 ,可以发现比之前的值都要大,所以直接插到最右边的子树。

(6)插入结点 1 ,可以发现比之前的值都要小,所以直接插入到最左子树。

(7)最后插入结点 4 ,可以发现比根结点的左孩子要大,并且其左孩子没有右子树,所以直接插入即可。

整个操作的思想就是递归的思想,不断判断要插入结点的值和当前结点的值是大还是小,然后进入对应分支。

struct tree_node {
	int data;
	tree_node *left_child;	//左孩子
	tree_node *right_child;	//右孩子
};

tree_node *root = NULL;		//根结点指针
tree_node *temp = NULL;		//临时指针

//初始化根结点
void init_root(int key) {
	root = new tree_node;
	root->data = key;
	root->left_child = root->right_child = NULL;
}

//插入孩子
void insert_child(int key) {
	if (root == NULL) {
		cout << "请先创建根节点" << endl;
		return;
	}
	temp = root;	//用临时指针从根结点开始寻找合适的插入位置
	while (1) {
		//如果插入的值比当前结点要小,进入判断
		if (key < temp->data) {
			//如果当前结点的左子树为空,则直接构建新结点进行插入
			if (temp->left_child == NULL) {
				tree_node *new_node = new tree_node;
				new_node->data = key;
				new_node->left_child = new_node->right_child = NULL;	//将新结点的左指针和右指针都初始化为空
				temp->left_child = new_node;	//将当前结点的左指针指向新结点
				break;
			} 
			//如果其左子树不为空,则进入其左子树继续进行判断
			else {
				temp = temp->left_child;
			}
		} 
		//如果插入的值比当前节点要大,进入判断
		else {
			//如果当前结点的右子树为空,则直接构建新结点继续插入
			if (temp->right_child == NULL) {
				tree_node *new_node = new tree_node;
				new_node->data = key;
				new_node->left_child = new_node->right_child = NULL;	将新结点的左指针和右指针都初始化为空
				temp->right_child = new_node;	//将当前结点的右指针指向新结点
				break;
			} 
			//如果其右子树不为空,则进入其右子树继续进行判断
			else {
				temp = temp->right_child;
			}
		}
	}
}
二叉排序树的查找操作

我们这里的查找操作就不想数组查找那样那么直接,需要从根结点开始往下遍历进行比较。那么如果我们想要查找上面这颗二叉树的结点 4 ,该如何进行查找呢:
(1)先从根结点开始查找。
(2)可以发现我们要查找的结点 4 要比根结点 8 要小,所以我们进入根结点的左子树进行查找。

(3)结点 4 比结点 2 的值要大,所以我们进入其右子树进行查找。


(4)结果发现,当前遍历到的结点就是我们要查找的值,直接返回即可。

二叉排序树的删除操作

二叉排序树的删除操作就要比插入操作更加繁琐一些,需要分情况进行讨论,第一次学习不要慌张,一时头脑混乱非常正常,静下心来反复思考就可以吸收啦~
我们可以将删除操作分为三种情况:
(1)删除结点是叶子结点
如果删除结点是根结点,那么直接delete根结点指针即可,因为此时树中只有一个结点了。否则,同样delete删除结点即可,因为
假如我们要删除上面那棵树的结点 23 ,我们只用找到其父结点,然后将其父结点指向删除结点的指针指向空即可。


(2)如果删除的结点只有一个孩子
如果删除结点只有左孩子,那么只需将删除结点的左孩子连接到其父结点即可;如果删除结点只有右孩子,那么只需将删除结点的右孩子连接到其父结点即可。
如果删除结点是根结点则需另外操作,将根结点指针指向它的孩子即可。
假如我们要删除结点 11 ,那只用将结点 11 的右孩子直接接到其父结点 9 即可。

(3)如果删除结点既有左孩子也有右孩子。
这里我们有两种方法去实现,根结点和树中间的结点操作都是一样的,第一种方法是找到删除结点左子树中值最大的那个结点,然后与删除结点的值替换,并删除该结点。实际操作时,我们要从删除结点的左孩子开始查找,因为删除结点的左子树中,值最大的无非就是其左孩子或在左孩子的右子树当中,这里我们就拿删除根结点进行举例:


第二种方法是找到删除结点右孩子中最小的那个,交换并删除。同样,实际操作中也是从删除结点的右孩子开始查找,删除结点右子树中最小值一定在其右孩子或右孩子的左子树当中。这两种方法都保证删除后的树中结点仍然满足二叉排序树的性质,即左孩子都比根结点小,右孩子都比根结点大:


接下来我们来看代码的操作,这里我们是以上面第一种方法来实现的。另外,我们在实现代码时,会将一些操作合并在一起,因为有些情况的实现方法是相同的,比如说在当删除结点是叶子结点或删除结点只有一个孩子时,可以合在一起讨论,具体看下面的代码详解(代码略微有点长,需要大家细细品味和思考):

//删除结点
void delete_node(tree_node *node, int key) {
	//判断是否有根结点
	if (root == NULL) {	
		cout << "还没有创建根结点" << endl;
	}
	
	//判断当前结点是否为空
	if (node == NULL) {	
		return;
	} 
	else {
		//如果找到了要删除的结点
		if (node->data == key) {
			tree_node *prev = prev_node(root, node, key);	//找到删除结点的父节点
			temp = NULL;
			//1、如果删除结点的右孩子是否为空(包含左孩子也为空即叶子结点的情况)
			if (node->right_child == NULL) {
				//判断删除结点是否为根结点
				if (node == root) {
					//判断根结点左孩子是否为空,如果为空则直接删除根结点(因为树中只有一个结点,其左右孩子都是空的)
					if (node->left_child == NULL) {
						delete[]root;
						root = NULL;
						return;
					}
					//否则直接将根结点指针指向当前要删除结点的左孩子,因为右孩子为空不用管
					root = prev->left_child;
					delete[]prev;
					prev = NULL;
					return;
				}
				
				//如果删除结点不是根结点,则要判断删除结点是其父结点的左孩子还是右孩子
				//注意:这里的操作我们用到了prev、temp和node三个指针,不要弄混他们的指向
				temp = node;	//将临时指针指向要删除的结点
				node = node->left_child;	//将node指针指向删除结点的左孩子(已知其右孩子为空)
				
				//如果是左孩子(注意这里要先判断父结点左孩子是否为空,如果为空去直接调用会报错)
				if (prev->left_child != NULL && prev->left_child->data == temp->data) {
					prev->left_child = node;	//将父结点的左孩子指向删除结点的左孩子
					delete[]temp;
					temp = NULL;
				}
				//如果是右孩子
				else {
					prev->right_child = node;	//将父结点的右孩子指向删除结点的左孩子
					delete[]temp;
					temp = NULL;
				}
			}
			//2、如果删除结点的右孩子不为空,但是左孩子为空(操作与上面类似)
			else if (node->left_child == NULL) {
				//判断删除结点是否为根结点
				if (node == root) {
					//这里不用再判断其右孩子是否为空,因为只有右孩子为空才能进入到这里
					root = prev->right_child;
					delete[]prev;
					prev = NULL;
					return;
				}
				
				temp = node;
				node = node->right_child;	//将node指针指向删除结点的右孩子
				
				//判断删除结点是父结点的哪个孩子(同样要先判断指针是否为空)
				if (prev->left_child != NULL && prev->left_child->data == key) {
					prev->left_child = node;	//将父结点的左孩子指向删除结点的右孩子
					delete[]temp;
					temp = NULL;
				} else {
					prev->right_child = node;	//将父结点的右孩子指向删除结点的右孩子
					delete[]temp;
					temp = NULL;
				}
			}
			//3、如果删除的结点左右孩子都不为空即在树的中间位置
			else {
				temp = node;	//将temp指向删除结点,作为删除结点左孩子的父结点
				tree_node *left_max = temp->left_child;	//找到删除结点左孩子中的最大值
				
				//寻找删除结点左孩子中是否有右孩子(如果有,则找到最大值)
				while (left_max->right_child != NULL) {
					temp = left_max;	//随时更新temp指针,使他始终指向left_max的父结点(方便后续操作)
					left_max = left_max->right_child;
				}
				
				//将左孩子的最大值赋给删除的结点,这样该结点的左孩子的所有值还是小于该结点
				node->data = left_max->data;	
				
				//判断当初指向删除结点的temp指针的指向是否有改变(注意temp指向的始终是left_max的父结点)
				if (temp != node) {
					//如果改变了,说明删除结点的左孩子中存在右孩子,则需要将temp的右孩子指向left_max的左孩子
					temp->right_child = left_max->left_child;
					delete[]left_max;
					left_max = NULL;
				} else {
					//如果没有改变,说明删除结点的左孩子中没有右孩子,则最大值就是删除结点的左孩子,直接将temp指向最大左孩子的左孩子即可
					temp->left_child = left_max->left_child;
					delete[]left_max;
					left_max = NULL;
				}
			}
		} 
		//如果删除的值比当前结点的值小,则往左孩子递归
		else if (key < node->data) {
			return delete_node(node->left_child, key);
		}
		//如果删除的值比当前结点的值大,则往右孩子递归
		 else if (key > node->data) {
			return delete_node(node->right_child, key);
		}
	}
}
全部代码
#include 
using namespace std;

struct tree_node {
	int data;
	tree_node *left_child;	//左孩子
	tree_node *right_child;	//右孩子
};

tree_node *root = NULL;		//根结点指针
tree_node *temp = NULL;		//临时指针
void init_root(int);
void insert_child(int);
void show_tree(tree_node *);	//遍历二叉树
void delete_node(tree_node *, int);
tree_node *prev_node(tree_node *, tree_node *, int);

int main() {
	init_root(8);
	insert_child(2);
	insert_child(9);
	insert_child(11);
	insert_child(23);
	insert_child(1);
	insert_child(4);
	show_tree(root);
	cout << endl;
	delete_node(root, 8);
	show_tree(root);
	cout << endl;
	delete_node(root, 2);
	delete_node(root, 9);
	delete_node(root, 11);
	delete_node(root, 23);
	delete_node(root, 1);
	show_tree(root);
	cout << endl;
	delete_node(root, 4);
	show_tree(root);
	cout << endl;
	delete_node(root, 4);
}

//初始化根结点
void init_root(int key) {
	root = new tree_node;
	root->data = key;
	root->left_child = root->right_child = NULL;
}

//插入孩子
void insert_child(int key) {
	if (root == NULL) {
		cout << "请先创建根节点" << endl;
		return;
	}
	temp = root;	//用临时指针从根结点开始寻找合适的插入位置
	while (1) {
		//如果插入的值比当前结点要小,进入判断
		if (key < temp->data) {
			//如果当前结点的左子树为空,则直接构建新结点进行插入
			if (temp->left_child == NULL) {
				tree_node *new_node = new tree_node;
				new_node->data = key;
				new_node->left_child = new_node->right_child = NULL;	//将新结点的左指针和右指针都初始化为空
				temp->left_child = new_node;	//将当前结点的左指针指向新结点
				break;
			}
			//如果其左子树不为空,则进入其左子树继续进行判断
			else {
				temp = temp->left_child;
			}
		}
		//如果插入的值比当前节点要大,进入判断
		else {
			//如果当前结点的右子树为空,则直接构建新结点继续插入
			if (temp->right_child == NULL) {
				tree_node *new_node = new tree_node;
				new_node->data = key;
				new_node->left_child = new_node->right_child = NULL;	将新结点的左指针和右指针都初始化为空
				temp->right_child = new_node;	//将当前结点的右指针指向新结点
				break;
			}
			//如果其右子树不为空,则进入其右子树继续进行判断
			else {
				temp = temp->right_child;
			}
		}
	}
}

//遍历二叉树
void show_tree(tree_node *node) {
	if (root == NULL) {
		cout << "还没有创建根结点";
	}
	if (node == NULL) {
		return;
	}
	show_tree(node->left_child);
	cout << node->data << " ";	//中序遍历(用于二叉排序树的输出)
	show_tree(node->right_child);
}

//进行查找操作(找到给定结点的父结点)
tree_node *prev_node(tree_node *root_node, tree_node *node, int key) {
	//如果查找的结点就是根结点,直接返回根结点
	if (node == root_node) {
		return node;
	}
	//否则,进行递归查找
	else {
		//判断当前遍历的结点的左孩子是否为查找结点
		if (root_node->left_child != NULL && root_node->left_child->data == key) {
			return root_node;
		}
		//判断当前遍历的结点的右孩子是否为查找结点
		else if (root_node->right_child != NULL && root_node->right_child->data == key) {
			return root_node;
		}
		//如果左右孩子都不是查找结点,则继续往左右孩子进行递归操作
		else if (key < root->data) {
			return prev_node(root_node->left_child, node, key);
		} else {
			return prev_node(root_node->right_child, node, key);
		}
	}
}

//删除结点
void delete_node(tree_node *node, int key) {
	//判断是否有根结点
	if (root == NULL) {
		cout << "还没有创建根结点" << endl;
	}

	//判断当前结点是否为空
	if (node == NULL) {
		return;
	} else {
		//如果找到了要删除的结点
		if (node->data == key) {
			tree_node *prev = prev_node(root, node, key);	//找到删除结点的父节点
			temp = NULL;
			//1、如果删除结点的右孩子是否为空(包含左孩子也为空即叶子结点的情况)
			if (node->right_child == NULL) {
				//判断删除结点是否为根结点
				if (node == root) {
					//判断根结点左孩子是否为空,如果为空则直接删除根结点(因为树中只有一个结点,其左右孩子都是空的)
					if (node->left_child == NULL) {
						delete[]root;
						root = NULL;
						return;
					}
					//否则直接将根结点指针指向当前要删除结点的左孩子,因为右孩子为空不用管
					root = prev->left_child;
					delete[]prev;
					prev = NULL;
					return;
				}

				//如果删除结点不是根结点,则要判断删除结点是其父结点的左孩子还是右孩子
				//注意:这里的操作我们用到了prev、temp和node三个指针,不要弄混他们的指向
				temp = node;	//将临时指针指向要删除的结点
				node = node->left_child;	//将node指针指向删除结点的左孩子(已知其右孩子为空)

				//如果是左孩子(注意这里要先判断父结点左孩子是否为空,如果为空去直接调用会报错)
				if (prev->left_child != NULL && prev->left_child->data == temp->data) {
					prev->left_child = node;	//将父结点的左孩子指向删除结点的左孩子
					delete[]temp;
					temp = NULL;
				}
				//如果是右孩子
				else {
					prev->right_child = node;	//将父结点的右孩子指向删除结点的左孩子
					delete[]temp;
					temp = NULL;
				}
			}
			//2、如果删除结点的右孩子不为空,但是左孩子为空(操作与上面类似)
			else if (node->left_child == NULL) {
				//判断删除结点是否为根结点
				if (node == root) {
					//这里不用再判断其右孩子是否为空,因为只有右孩子为空才能进入到这里
					root = prev->right_child;
					delete[]prev;
					prev = NULL;
					return;
				}

				temp = node;
				node = node->right_child;	//将node指针指向删除结点的右孩子

				//判断删除结点是父结点的哪个孩子(同样要先判断指针是否为空)
				if (prev->left_child != NULL && prev->left_child->data == key) {
					prev->left_child = node;	//将父结点的左孩子指向删除结点的右孩子
					delete[]temp;
					temp = NULL;
				} else {
					prev->right_child = node;	//将父结点的右孩子指向删除结点的右孩子
					delete[]temp;
					temp = NULL;
				}
			}
			//3、如果删除的结点左右孩子都不为空即在树的中间位置
			else {
				temp = node;	//将temp指向删除结点,作为删除结点左孩子的父结点
				tree_node *left_max = temp->left_child;	//找到删除结点左孩子中的最大值

				//寻找删除结点左孩子中是否有右孩子(如果有,则找到最大值)
				while (left_max->right_child != NULL) {
					temp = left_max;	//随时更新temp指针,使他始终指向left_max的父结点(方便后续操作)
					left_max = left_max->right_child;
				}

				//将左孩子的最大值赋给删除的结点,这样该结点的左孩子的所有值还是小于该结点
				node->data = left_max->data;

				//判断当初指向删除结点的temp指针的指向是否有改变(注意temp指向的始终是left_max的父结点)
				if (temp != node) {
					//如果改变了,说明删除结点的左孩子中存在右孩子,则需要将temp的右孩子指向left_max的左孩子
					temp->right_child = left_max->left_child;
					delete[]left_max;
					left_max = NULL;
				} else {
					//如果没有改变,说明删除结点的左孩子中没有右孩子,则最大值就是删除结点的左孩子,直接将temp指向最大左孩子的左孩子即可
					temp->left_child = left_max->left_child;
					delete[]left_max;
					left_max = NULL;
				}
			}
		}
		//如果删除的值比当前结点的值小,则往左孩子递归
		else if (key < node->data) {
			return delete_node(node->left_child, key);
		}
		//如果删除的值比当前结点的值大,则往右孩子递归
		else if (key > node->data) {
			return delete_node(node->right_child, key);
		}
	}
}

如果大家有什么问题的话,欢迎在下方评论区进行讨论哦~

【上一讲】树 - 02 二叉树
【下一讲】树 - 04 线索二叉树

转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/867383.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号