如果有上面这样一棵二叉树,它满足左儿子的值 ≤ 父亲的值 < 右儿子的值这样一个规则,我们来尝试从这棵二叉树中查找是否存在一个元素,该元素的值是7。从这棵二叉树的根节点 (5) 开始查找:
第一步、7 > 5,所以继续查找5的右儿子;
第二步、7 < 8,所以继续查找4的左儿子;
第三步、7 > 6,所以继续查找6的右儿子;
第四步、7 == 7,找到元素7。
在10个元素中查找元素,一共查找了4次,而且最多只需要查找4次。也就是说,即使在最差的情况下,也只需要查找logN次即可找到目标元素,即查找的时间复杂度为O(logN)。这个查找效率与二分查找是一样的。
我们已经知道二分查找了,为什么还需要学习这样的树状结构呢?原因是二分查找一般用于有序数组中的元素的查找,而数组的空间大小是固定的,不能任意插入数据,如果有一个足够大空间的数组,往一个有序数组中插入 (删除) 数据,而且在插入 (删除) 数据后该数组仍然保持有序,那么插入 (删除) 数据的时间复杂度为O(N)。显然,这样的效率是不能让人满意的。而我们这里的树状结构------二叉查找树,则可以动态插入 (删除) 数据,插入 (删除) 数据的时间复杂度为O(logN),而且我们可以用动态添加结点,而不需要事先申请很大的内存空间。
3.1 二叉查找树的查
因为二叉查找树满足左儿子的值 ≤ 父亲的值 < 右儿子的值这个规则,所以在查找二叉树中查找一个元素就变得很简单:
如果要查找的元素与根节点相等,就返回;
如果要查找的元素比根节点大,那就继续在根节点的右儿子结点上递归查找;
如果要查找的元素比根节点小,那就继续在根节点的左儿子结点上递归查找;
递归的出口一共有两个:一个是找到后返回,另一个是确定找不到要找的元素后返回-1
int find(struct Node *root, int n) //从根节点开始查找一个结点。
{
if (root == NULL) //递归的出口1
{
return -1; //找不到就返回-1
}
//递归的出口2
if(root->data == n) //找到就返回
{
return n;
}
else if(n > root->data) //如果n > root->data,就从这个结点的右儿子继续查找
{
return find(root->rightSon, n); //递归查找
}
else //如果n <= root->data,就从这个结点的左儿子继续查找
{
return find(root->leftSon, n); //递归查找
}
}
3.2 二叉查找树的增
因为二叉查找树满足左儿子的值 ≤ 父亲的值 < 右儿子的值这个规则,所以在查找二叉树中增加一个元素就变成为要增加的元素找到一个合适的位置,然后在这个位置上添加一个结点。而找到一个合适的位置的代码与查找一个元素的代码很相似:
void addNode(struct Node **root, int data) //增加一个结点
{
struct Node * temp = createNode(data); //先新建一个结点
struct Node * rr = NULL; //根节点
if (*root == NULL) //不需要判断root == NULL
{
//如果一棵树还没有root,则首先要生成root
*root = temp;
}
//如果有root了,则在root下添加结点。用指针方式
else // root != NULL
{
rr = *root;
//如果data比root的值小,且没有左孩子,则让temp成为root的左孩子
if (data < rr->data && rr->leftSon == NULL)
{
rr->leftSon = temp;
}
//如果data比root的值大,且没有右孩子,则让temp成为root的右孩子
else if (data > rr->data && rr->rightSon == NULL)
{
rr->rightSon = temp;
}
//剩余的情况是data比root的值小,且root有左孩子
//data比root的值大,且root有右孩子
//这两种情况下,都要递归,但是仍然要分两种情况进行递归
else if (data < rr->data)
{
addNode(&rr->leftSon, data); // 递归
}
else
{
addNode(&rr->rightSon, data); // 递归
}
*root = rr; //修改root
}
}
3.3 二叉查找树的改
二叉查找树主要是用来增加、删除和查找元素的。如果对一个结点进行简单的修改,则很容易导致不符合左儿子的值 ≤ 父亲的值 < 右儿子的值这个规则。所以二叉查找树中不设置修改功能。
3.4 二叉查找树的删
因为二叉查找树满足左儿子的值 ≤ 父亲的值 < 右儿子的值这个规则,所以二叉查找树的增加和查找都非常简单。但是同样因为这个规则,二叉查找树的删除功能却比较复杂。下面分情况进行讨论:
① 被删除的结点没有儿子:从父节点直接删除这个结点即可。
② 被删除的结点只有一个左儿子。把父节点的儿子结点指向被删除的结点的儿子接口。
③ 被删除的结点只有一个右儿子。与②的操作相似。
④ 被删除的结点有2个儿子。把左儿子递归的放到右儿子下面。然后把父节点指向右儿子。这四种情况下都要区分被删除的结点是父节点的左儿子还是右儿子。所以一共是8种情况。
我们先写出找父节点的函数。注意,返回值是指针:
struct Node * getFatherNode(struct Node *root, int n)
{
//根节点为NULL或根节点的值==n
if(root == NULL || root->data == n)
{
return NULL; //找不到父节点,返回NULL
}
//已经到了叶子节点了,仍然没有找到
if(root->leftSon == NULL && root->rightSon == NULL)
{
return NULL;//找不到了,返回NULL
}
//如果根节点的值比n大,同时左子树不是NULL
if(root->data > n && root->leftSon != NULL)
{
//如果左儿子的值 == n,说明根节点就是要找的结点
if(root->leftSon->data == n)
{
return root;//返回根节点
}
return getFatherNode(root->leftSon, n); //在左子树中递归查找
}
//如果根节点的值比n小,同时右子树不是NULL
if(root->data < n &am