zoukankan      html  css  js  c++  java
  • [数据结构

    一、什么是二叉排序树?

    对于普通的顺序存储来说,插入、删除操作很简便,效率高;而这样的表由于无序造成查找的效率很低

    对于有序线性表来说(顺序存储的),查找可用折半、插值、斐波那契等查找算法实现,效率高;而因为要保持有序,在插入和删除时不得不耗费大量的时间

    那么,如何既使得插入和删除效率不错,又可以比较高效率地实现查找的算法呢?


    先看一个例子:

    现在我们的目标是插入和查找同样高效。假设我们的数据集开始只有一个数 {62},然后现在需要将 88 插入数据集,于是数据集成了 {62,88},还保持着从小到大有序。再查找有没有 58,没有则插入,可此时要想在线性表的顺序存储中有序,就得移动 62 和 88 的位置,如下左图,可不可以不移动呢?嗯,当然是可以,那就是二叉树结构。当我们用二叉树的方式时,首先我们将第一个数 62 定为根结点,88 因为比 62 大,因此让它做 62 的右子树,58 因比 62 小,所以成为它的左子树。此时 58 的插入并没有影响到 62 与 88 的关系,如下右图所示。


    也就是说,若我们现在需要对集合 {62,88,58,47,35,73,51,99,37,93} 做查找,在我们打算创建此集合时就考虑用二叉树结构,而且是排好序的二叉树来创建。如下图所示,62、88、58 创建好后,下一个数 47 因比 58 小,是它的左子树(见③),35 是 47 的左子树(见④),73 比 62 大,但却比 88 小,是 88 的左子树(见⑤),51 比 62 小、比 58 小、比 47 大,是 47 的右子树(见⑥),99 比 62、88 都大,是 88 的右子树(见⑦),37 比 62、58、47 都小,但却比 35 大,是 35 的右子树(见⑧),93 则因比 62、88 大是 99 的左子树(见⑨)。


    这样我们就得到了一棵二叉树,并且当我们对它进行中序遍历时,就可以得到一个有序的序列 {35,37,47,51,58,62,73,88,93,99},所以我们通常称它为二叉排序树。

    二叉排序树(Binary Sort Tree),又称为二叉查找树。它或者是一棵空树,或者是具有下列性质的二叉树:

    • 若它的左子树不空,则左子树上所有结点的值均小于它的根结点的值
    • 若它的右子树不空,则右子树上所有结点的值均大于它的根结点的值
    • 它的左、右子树也分别为二叉排序树

    构造一棵二叉排序树的目的,其实并不是为了排序,而是为了提高查找和插入删除关键字的速度。不管怎么说,在一个有序数据集上的查找,速度总是要快于无序的数据集的,而二叉排序树这种非线性的结构,也有利于插入和删除的实现。


    二、基本操作

    首先提供一个二叉树的结构:

    /* 二叉树的链表结点结构定义 */
    typedef  struct BiTNode	/* 结点结构 */
    {
    	int data;	// 结点数据
    	struct BiTNode *lchild, *rchild;	// 左右孩子指针
    } BiTNode, BiTree;
    

    2.1 二叉排序树查找操作

    思路:当二叉树非空时,首先将待查找的键值与根节点的键值比较,若小于根节点的键值,则继续查找左子树,否则查找右子树。重复上述过程,直至查找成功返回 TRUE,否则返回 FALSE。

    // 递归查找二叉排序树T中是否存在key,指针f指向T的双亲,其初始调用值为NULL
    Status searchBST(BiTree *T, int key, BiTree *f, BiTree **p)
    {
    	if (T == NULL) // 查找不成功,指针p指向查找路径上访问的最后一个结点并返回FALSE
    	{
    		*p = f;
    		return FALSE;
    	}
    	else if (T->data == key) // 查找成功,则指针p指向该数据元素结点,并返回TRUE 
    	{
    		*p = T;
    		return TRUE;
    	}
    	
    	if (key < T->data)
    		return searchBST(T->lchild, key, T, p);  // 在左子树中继续查找(注意此时f的赋值)
    	else
    		return searchBST(T->rchild, key, T, p);  // 在右子树中继续查找
    }
    

    2.2 二叉排序树插入操作

    思路:与查找类似,但需要一个父节点来进行赋值。代码如下:

    //  当二叉排序树T中不存在关键字等于key的数据元素时,插入key并返回TRUE,否则返回FALSE
    Status insertBST(BiTree **T, int key)
    {
    	BiTree *p; // 指针p指向查找路径上访问的最后一个结点
    
    	// 不存在关键字等于key的数据元素时,才插入
    	if (!searchBST(*T, key, NULL, &p)) // 通过指针p获得查找路径上访问的最后一个结点的地址
    	{
    		BiTNode *temp = (BiTNode *)malloc(sizeof(BiTNode));
    		temp->data = key;
    		temp->lchild = temp->rchild = NULL;
    
    		// 根据key与最后一个节点key值比较大小,决定是在左子树还是右子树插入
    		if (p == NULL)
    			*T = temp;	// T为空树,则插入temp为新的根结点
    		else if (key < p->data)
    			p->lchild = temp;
    		else
    			p->rchild = temp;
    
    		return TRUE;
    	}
    	else
    		return FALSE;  // 树中已有关键字相同的结点,不再插入
    }
    

    2.3 二叉排序树删除元素操作

    对于二叉排序树的删除要注意,我们不能因为删除了结点,而让这棵树变得不满足二叉排序树的特性,所以删除需要考虑多种情况。

    (1)删除叶子结点

      直接删除,不影响原树,如下图所示:


    (2)删除仅有左或右子树的结点

      节点删除后,将它的左子树或右子树整个移动到删除节点的位置就可以,子承父业,如下图所示:


    (3)删除左右子树都有的结点

    我们仔细观察一下,47 的两个子树中能否找出一个结点可以代替 47 呢?果然有,37 或者 48 都可以代替 47,此时在删除 47后,整个二叉排序树并没有发生什么本质的改变。

    为什么是 37 和 48?对的,它们正好是二叉排序树中比它小或比它大的最接近 47 的两个数。也就是说,如果我们对这棵二叉排序树进行中序遍历,得到的序列 {29, 35, 36, 37, 47, 48, 49, 50, 51, 56, 58, 62, 73, 88, 93, 99}。

    因此,比较好的办法就是,找到删除结点 p 的中序序列的直接前驱(或直接后驱)s,用 s 来替换结点 p,然后删除结点 s,如下图所示:


    根据我们对删除结点三种情况的分析:

    1. 叶子结点;
    2. 仅有左或右子树的结点;
    3. 左右子树都有的结点。

    下面是 deleteNode 的代码:

    // 从二叉排序树中删除结点p,并重接它的左或右子树。
    Status deleteNode(BiTree **p)
    {
    	BiTree *q, *temp;
    	if ((*p)->rchild == NULL) // 右子树空则只需重接它的左子树(待删结点是叶子也走此分支)
    	{
    		q = *p; *p = (*p)->lchild; free(q);
    	}
    	else if ((*p)->lchild == NULL) // 只需重接它的右子树
    	{
    		q = *p; *p = (*p)->rchild; free(q);
    	}
    	else /* 左右子树均不空 */
    	{
    		q = *p; 
    		temp = (*p)->lchild;
    
    		while (temp->rchild) // 转左,然后向右到尽头(找待删结点的前驱)
    		{
    			q = temp;
    			temp = temp->rchild;
    		}
    
    		(*p)->data = temp->data; // temp指向被删结点的直接前驱(将被删结点前驱的值取代被删结点的值)
    		if (q != *p)
    			q->rchild = temp->lchild; // 重接q的右子树
    		else
    			q->lchild = temp->lchild; // 重接q的左子树
    
    		free(temp);
    	}
    
    	return TRUE;
    }
    

    2.4 二叉排序树删除结点操作

    下面这个算法是递归方式对二叉排序树 T 查找 key,查找到时删除。

    // 若二叉排序树T中存在关键字等于key的数据元素时,则删除该数据元素结点
    Status deleteBST(BiTree **T, int key)
    {
    	if (!*T) // 不存在关键字等于key的数据元素
    		return FALSE;
    	else
    	{
    		if (key == (*T)->data) // 找到关键字等于key的数据元素
    			return deleteNode(T);
    		else if (key < (*T)->data)
    			return deleteBST(&(*T)->lchild, key);
    		else
    			return deleteBST(&(*T)->rchild, key);
    
    	}
    }
    

    可以看出,这段代码和前面的二叉排序树查找几乎完全相同,唯一区别在于当找到对应 key 值的结点时,执行的是删除操作。


    三、主函数执行

    int main(void)
    {
    	int a[10] = { 62, 88, 58, 47, 35, 73, 51, 99, 37, 93 };
    	BiTree *T = NULL;
    
    	// 插入元素
    	for (int i = 0; i<10; i++)
    	{
    		insertBST(&T, a[i]);
    	}
    
    	// 删除元素
    	deleteBST(&T, 93);
    	deleteBST(&T, 47);
    
    	// 中序递归遍历
    	printf("中序递归遍历: ");
    	inOrderTraverse(T); // 中序递归遍历,会得到一个有序的序列
    
    	printf("
    
    另外,本样例建议断点跟踪查看二叉排序树结构
    
    ");
    
    	return 0;
    }
    

    输出结果如下图所示:


    四、二叉排序树总结

    总之,二叉排序树是以链接的方式存储,保持了链接存储结构在执行插入或删除操作时不用移动元素的优点,只要找到合适的插入和删除位置后,仅需修改链接指针即可。

    而对于二叉排序树的查找,走的就是从根结点到要查找的结点的路径,其比较次数等于给定值的结点在二叉排序树的层数。技术情况,最少为1次,即根结点就是要找的结点;最多也不会超过树的深度。也就是说,二叉排序树的查找性能取决于二叉排序树的形状。可问题是,二叉排序树的形状是不确定的

    例如 {62, 88, 58, 47, 35, 73, 51, 99, 37, 93} 这样的数组,我们可以构建如下左图的二叉排序树。但如果数组元素的次序是从小到大有序,如 {35, 37, 47, 51, 58, 62, 73, 88, 93, 99},则二叉排序树就成了极端的右斜树,注意它依然是一棵二叉排序树,如下右图。此时,同样是查找结点 99,左图只需要两次比较,而右图就需要 10 次比较才可以得到结果,二者差异很大。


    也就是说,我们希望二叉排序树是比较平衡的,即其深度与完全二叉树相同,那么查找的时间复杂也就为 O(logn),近似于折半查找。而像上图右边这种情况,查找时间复杂度为 O(n),这等同于顺序查找。因此,如果我们希望对一个集合按二叉排序树查找,最好是把它构建成一棵平衡的二叉排序树


    参考:

    《大话数据结构 - 第8章》 查找

    二叉排序树


  • 相关阅读:
    AI 数值计算
    AI 主成分分析(PCA)
    AI 线性代数
    AI 奇异值分解(SVD)
    AI 协同过滤
    AI 卷积神经网络
    AI 随机梯度下降(SGD)
    Ecshop里添加多个h1标题
    Ecshop之ajax修改表里的状态(函数化处理)
    url地址形式的传参格式拼接
  • 原文地址:https://www.cnblogs.com/linuxAndMcu/p/10339436.html
Copyright © 2011-2022 走看看