查找--二叉查找树

我们很多时候都在搜索,搜索相关电影的演员表,搜索相关英语单词的解释、含义,搜索特定人物的联系方式等等,我们为了解决这种问题,会使用到符号表这种工具,而符号表里面会有很多种不同的实现:比如说基于数组的符号表,基于树的符号表,这篇博客就是记录使用二叉树这种数据结构来实现一个符号表。


1.概念:树这种数据结构,应该有很多人用过了,主要是由节点和指向节点的指针构成。我们可以回想一下在一个int数组里面查找一个特定的数字,我们会使用二分法查找,但是二分法的首要条件就是此数组已经是排好序了的。

那么如何在树这种数据结构里面表现出“已排序”这种状态呢?我们就可以引申出二叉查找树的概念:一棵二叉查找树是一棵二叉树,其中每个节点都含有一个可以比较的键,以及相关联的值,其中任意一个节点的键都大于此节点左子树任意一个节点的键,都小于此节点右子树任意一个节点的键。

我们使用左小右大这种规则来定义树的节点顺序,变相的实现了树的节点的“有序”,好了,现在我们就可以把二分法施加在树这种数据结构上面了。


2.构造:我们既然选择了二叉树这种数据结构,那么我们首先就要定义一个节点类,里面包含了Key , Value ,和指向左子树的指针和指向右子树的指针。

template<typename Key, typename Value>
class Node {
public:
        Node(Key k, Value val) : key(k) , value(val) {}
        Key key;
        Value value;
        Node* leftChild;
        Node* rightChild;
};

接下来我们就要想我们的树会有着什么样的操作:

i.插入操作:我们要向树中插入一个键值对,如果这个键存在于树中,那么我们就用新的值替换旧值,如果这个键不存在于树中,那么我们就在恰当的位置新建一个节点.

ii.查找操作:我们要查找一个键所对应的值。

iii.删除操作:我们要在树里面删除一个指定的键值对。

这三个操作分别对应了三个函数:put(Key k, Value v) get(Key k) del(Key k);


其中put函数接受三个参数,一个是当前的节点指针,一个是键,一个是值:因为按照前文所说,我们要插入一个键值对,首先要看是否存在相应的键,那么肯定是一个查找操作,为什么不用get函数呢?因为我们put这个函数是需要递归操作的,在插入的时候是需要重置一些指针的值的,所以我们不能使用get这个函数。

template<typename Key, Value Value>
Node* put(Node* root, Key k, Value v)
{
        if (root == nullptr)           //如果没有找到已存在的键的话,那么就直接新建一个节点
        {
                root = new Node(k, v);
        }

        if (root->key == k)    root->val = v; //如果等于键,那么更新旧值
        else if (root->key < k)  root->rightChild = put(root->rightChild, k, v); //如果大于键,那么在右子树里面插入
        else if (root->key > k)  root->leftChild = put(root->leftChild, k, v);    //如果小于键,那么在左子树里面插入
        
        return root;    //返回当前节点
}


get函数接两个参数:键和当前处理的节点指针,然后我们就会在树里面寻找相应的键,然后返回指向目标节点的指针:

template<typename Key, Value Value>
Node* get(Node* root, Key k)
{
        if (root == nullptr)      //如果没有找到相应的
        {
              return nullptr;
        }

        if (root->key == k)   return root;      //如果当前节点键如果等于目标键,直接返回值
        else if (root->key < k)   return get(root->rightChild, k);     // 如果当前节点键如果小于目标键,在右子树里面查找

        else if (root->key > k)   return get(root->leftChild, k);      // 如果当前节点键如果等于目标键,在左子树里面查找

        return root;
}

del函数接受两个参数:键和当前处理的节点指针,我们随后会查找相应的键,并且执行删除操作,在所有的操作里面,删除操作是最复杂的,在删除操作之前,我们需要了解二叉树的中序遍历:中序遍历是一个递归的过程:先访问当前节点的左子树,然后再访问当前节点,最后再访问当前节点的右子树,因为查找二叉树的性质,我们如果对一棵查找二叉树执行中序遍历的话,那么我们就会得到一个按照键的大小排好序的序列。

接下来我们考虑所有的删除情况:

i.待删除的节点有左子树,没有右子树 : 我们直接把左子节点顶到删除节点位置。

ii.待删除的节点没有左子树,有右子树 : 我们直接把右子节点订到删除节点位置。

iii.待删除的节点既有左子树,又有右子树 : 我们可以想象一个已经排好序的数组:如果我们删除一个数组中任意一个值,那么后面的第一个数字就会把被删除的数字的位置占掉,所以我们也遵循这个思路:我们把要删除的节点中序遍历之后的那个节点(相当于数组中的后面一个数)顶到删除节点的位置,并且重置相关的指针,为此,我们需要两个辅助函数来帮助我们:寻找被删除节点的下一个节点 min,删除最小的节点 deleteMin.


Node* min(Node* root)
{
        if (root != nullptr && root->leftChild == nullptr)    return root;
        return min(root->leftChild);
}

Node* deleteMin(Node* target)
{
        if (target == nullptr)    return nullptr;
        
        if (target->leftChild == nullptr)
        {
                Node* currentRight = target->rightChild;
                delete target;
                return currentRight;
        }
        else
        {
                target->leftChild = deleteMin(target->leftChild);
        }
        return target;
}

我们借助这两个函数,我们就可以写出删除一个节点的代码:

Node* deleteKey(Node* current, Key k)
{
	if (current->key < k)
	{
		current->right = deleteKey(current->right, k);
	}
	else if (current->key > k)
	{
		current->left = deleteKey(current->left, k);
	}
	else        //找到待删除的节点之后,我们要对三种情况进行分析
	{
		if (current->right == nullptr)        //当删除的节点的右子树为空时
		{
			Node* currentLeft = current->left;
			delete current;
			return currentLeft;
		}
		if (current->left == nullptr)        //当删除的节点的左子树为空时
		{
			Node* currentRight = current->right;
			delete current;
			return currentRight;
		}
		Node* currentMin = min(current->right);       //当删除的节点的左右子树都不为空时,找到后续节点,并重置相关指针
		Node* returnNode = new Node(currentMin->key, currentMin->value, currentMin->numberOfChildren);
		deleteMin(current->right);
		returnNode->left = current->left;
		returnNode->right = current->right;
		delete current;
		return returnNode;
	}
	return current;
}


这样,我们就可以得到一棵查找二叉树的三个基本操作了:插入,查找,删除。


3.效率:

i.空间:N * sizeof(Node);

ii.时间:因为树可能不平衡,所以我们会得到最差时间复杂度:插入:O(N),查找O(N) [在这种情况下,树完全左斜,或者完全右斜]

      平均时间复杂度为: 插入O(1.39 lgN), 查找O(1.39 lgN);


4.进阶:

二叉查找树没有考虑到极端情况:完全左斜,完全右斜,会导致不平衡发生,这种情况发生时,跟顺序遍历链表查找一样,很慢,没有达到二分的目的,为了解决这种情况,我们会用到另外一种数据结构--平衡二叉树[红黑树],这种高度平衡的二叉树会把这种极端情况消除,并且能得到更好的查找效率。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值