今天我们要介绍的是一种特殊的二叉树,也就是搜索二叉树。搜索二叉树又叫排序树,其实我认为还是排序树这个名字更适合它。
这就是一棵搜索二叉树,可以看出来,他的孩子的数值是有一定的规律的,根的左孩子一定比根小,根的右孩子一定比根要大。在更多的应用中,他不只是数字的大小还会有更多的应用,今天我们就拿数字的大小来对他进行一定的讲解。
搜索二叉树在一定条件下查找数字是一个相对方便的情况,但是他的时间复杂度是多少呢?其实他并不是我们所想象的lgn的时间复杂度,为什么呢?因为都知道时间复杂度是计算的最坏的结果,结合上边的图,那你说我现在给你一个数组,数组中的数据是8,10,14,13,就像上边的树一样我只要右半部分,并且创建的顺序就是以8为根的树,当然这四个数据,也可以是以13位根,那8、10在13左边,14在根右边同样也是一个相对平衡的树,所以数组的数据出场顺序也是很重要的,但是不能排除最差的情况,所以他的时间复杂度仍然是0(n)。
其实搜索二叉树在创建的时候并没有过多的复杂算法, 只不过就是在我们之前所写的二叉树创建的时候判断一下比当前根节点大还是小,该放在当前结点的左边还是右边。搜索二叉树复杂的是插入和删除函数。
typedef int DataType; typedef struct BSTreeNode { struct BSTreeNode* _left; struct BSTreeNode* _right; DataType _data; }BSTreeNode;
这是我们搜索二叉树的数据结构。
BSTreeNode* BuyBSTreeNode(DataType x) { BSTreeNode *NewNode = (BSTreeNode*)malloc(sizeof(BSTreeNode)); NewNode->_data = x; NewNode->_left = NULL; NewNode->_right = NULL; return NewNode; }
这里是和正常二叉树通用的分配空间函数。
之后是我们的插入函数
int BSTreeInsert(BSTreeNode** tree, DataType x) { BSTreeNode *cur = *tree; BSTreeNode *parent = *tree; BSTreeNode *New = NULL; if (*tree == NULL) { *tree=BuyBSTreeNode(x); return 0; } while (cur) { if (cur->_data > x) { parent = cur; cur = cur->_left; } else { if (cur->_data < x) { parent = cur; cur = cur->_right; } else { return -1; } } } New = BuyBSTreeNode(x); if (parent->_data>x) { parent->_left = New; } else { parent->_right = New; } return 0; }
这里需要做的和以前不同的是再插入的时候需要判断根节点的值,来判断你当前插入的数值应该在根节点的左边还是在右边。这里的函数参数传入了二级指针,二级指针传入代表着可能会修改根节点的数值,也就是在当根节点是空的时候,传入的第一个数据时需要将根节点的数值进行改变所以需要传入二级指针。之后就是通过数值不断的比较来寻找正确的插入位置,这里我们是不能插入相同的数值的,所以当当前指向结点的数值和传入参数的数值相等的时候我们会返回错误值-1;当找到一个空的时候就代表可以在这个位置插入你当前数据,但是二叉树是需要连接起来的,你的父亲结点的指针是需要指向你当前创建的新结点的,所以在函数的开始你就需要创建一个parent结点来记录你cur结点的父亲结点,当给你的cur分配好空间之后,需要将parnet的孩子指针指向新开辟的空间。
const BSTreeNode* BSTreeFind(BSTreeNode* tree, DataType x) { BSTreeNode *cur = tree; while (cur) { if (cur->_data > x) { cur = cur->_left; } else { if (cur->_data < x) { cur = cur->_right; } else { return cur; } } } return NULL; }
查找函数也并不复杂,如果找到了就返回这个结点,没找到就返回空值。
int BSTreeRemove(BSTreeNode** tree, DataType x) { BSTreeNode *cur = *tree; BSTreeNode *parent = *tree; BSTreeNode *next = NULL; assert(*tree); if ((cur->_left == NULL) && (cur->_right = NULL)) { *tree = NULL; return 0; } while (cur) { if (cur->_data > x) { parent = cur; cur = cur->_left; } else { if (cur->_data < x) { parent = cur; cur = cur->_right; } else { break; } } } if (cur == NULL) { printf("没有找到数据\n"); return -1; } //左右都不为空的情况说明需要找数值来替换他 找左孩子的最右孩子或者右孩子的最左孩子 if ((cur->_left != NULL) && (cur->_right != NULL)) { if (cur->_right->_left != NULL) { parent = cur->_right; next = cur->_right->_left; while (next->_left) { parent = next; next = next->_left; } cur->_data = next->_data; cur = next; } else { next = cur->_right; if (parent->_left == cur) { parent->_left = cur->_right; } else { parent->_right = cur->_right; } next->_left = cur->_left; free(cur); cur = NULL; return 0; } } if (cur->_left == NULL){ if (parent->_left == cur) { parent->_left = cur->_right; } else { parent->_right = cur->_right; } free(cur); cur = NULL; return 0; } if (cur->_right == NULL) { if (parent->_left == cur) { parent->_left = cur->_left; } else { parent->_right = cur->_left; } free(cur); cur = NULL; return 0; } }
这里的删除要分几种情况,最简单的情况就是你删除的结点是叶子结点,这时候他的删除不会对树的结构有过大的影响,其次是你删除的结点是左为空或者右为空的情况,有一个为空,这时候需要让你要删除的结点的父亲指向你有孩子的那个方向。最复杂的情况就是你要删除的结点左右孩子都不为空,这时候如果轻易直接删除会对整个树的结构有较大的影响,并且如果是真的删除这个结点的话,是很复杂的情况。其实这里大家可以想一下, 之前我们在完成堆的时候,如果堆的堆顶元素pop之后,我们是怎么处理的,这里其实和那里是一样的道理。我们同样是去找一个数来替换掉这个要删除的数值,然后删除那个替换的数值。
那既然是排序二叉树,就不能随意找一个数值来进行替换。那应该找哪个数值呢?当你替换之后你的二叉树依然是根节点比左边大,比右边小,所以我们就可以找你当前要删除这个节点的左孩子的最右节点,也就是左孩子这棵子树中的最大数,或者是右孩子的最左节点,也就是右孩子这个子树中的最小数值。找到之后将这个数值将你要删除的那个位置的数据进行覆盖。
if ((cur->_left != NULL) && (cur->_right != NULL)) { if (cur->_right->_left != NULL) { parent = cur->_right; next = cur->_right->_left; while (next->_left) { parent = next; next = next->_left; } cur->_data = next->_data; cur = next; } else { next = cur->_right; if (parent->_left == cur) { parent->_left = cur->_right; } else { parent->_right = cur->_right; } next->_left = cur->_left; free(cur); cur = NULL; return 0; } }
但是虽然你左右都为空的时候需要找数值来替换,但是有一种情况是特殊的
凑合看这一棵树。
当你想删除7结点的时候,它的左右孩子都不是空,这时候你需要找7左孩子的右结点,也就是6的右孩子,或者说7的右孩子的左节点,也就是8的左孩子。但是你发现这两个结点都是空的都没有数值,所以这种情况需要特殊处理
next = cur->_right; if (parent->_left == cur) { parent->_left = cur->_right; } else { parent->_right = cur->_right; } next->_left = cur->_left; free(cur); cur = NULL; return 0;
也就是只需要直接将你的要删除的7结点的父亲结点5指向7的右孩子,然后让7的左孩子变成8的左孩子。
到这里你要删除的结点左右都有孩子的情况已经处理完了,接下来要处理的是,要删除的结点不是所有孩子都不为空。也就是左为空或者说右为空。
if (cur->_left == NULL){ if (parent->_left == cur) { parent->_left = cur->_right; } else { parent->_right = cur->_right; } free(cur); cur = NULL; return 0; } if (cur->_right == NULL) { if (parent->_left == cur) { parent->_left = cur->_left; } else { parent->_right = cur->_left; } free(cur); cur = NULL; return 0; }
左为空,那就让你的父亲结点指向你的右孩子,如果右为空那就让你的父亲结点指向你的左孩子还是上边的图假如你要删除8,此时你的左为空,那就直接让你的父亲结点指向你的那个指针直接指向你的孩子。
这里有人会想是不是还有一种情况,那就是左右都为空,这里其实不需要了,因为如果你左右都为空,那他就是进入左为空的判断为真条件,然后让你的父亲结点指向你的右孩子,因为你是叶子那你的右孩子就是空,这里也是直接让你的父亲指向空。
这个程序还有一个巧妙的地方就是在
if ((cur->_left != NULL) && (cur->_right != NULL)) { if (cur->_right->_left != NULL) { parent = cur->_right; next = cur->_right->_left; while (next->_left) { parent = next; next = next->_left; } cur->_data = next->_data; cur = next; }
有人说这种情况你找到一个数替换了你的cur这里叫那个位置叫next,此时你需要将next的结点释放掉,但是你没有释放啊,但是为什么运行出来是对的呢?这里巧妙的语句在cur = next;
我们将cur指向了next,也就是要删除的那个位置,然后这时候程序结束了吗?没有我们没有返回值而是让程序继续运行,这时候就可以看成我们本来就是要删除next位置的数据,next是最左或者最右孩子,那删除他不就是还用到我们下边的代码吗?
本来想让非递归的插入、查找和删除也放在这里,但是发现文章太长了,之后我会给大家单独写一篇文章来介绍递归删除、查找和插入的函数。
因为搜索二叉树他的中序遍历是升序的
void BTreeInOrder(BSTreeNode* root)//中序遍历 { if (root == NULL) return; else { BTreeInOrder(root->_left); printf("%d", root->_data); BTreeInOrder(root->_right); } }
所以这里我用了之前写的中序遍历二叉树来检查程序的正确性。
int main() { int a[] = {5,3,4,1,7,8,2,6,0,9}; BSTreeNode* tree = NULL; for (int i = 0; i < sizeof(a)/sizeof(a[0]); ++i) { BSTreeInsert(&tree, a[i]); } BTreeInOrder(tree); printf("\n"); /*const BSTreeNode* node = BSTreeFind(tree, 2); printf("Find 2? %d\n", node->_data); */ BSTreeRemove(&tree, 5); //BSTreeRemove(&tree, 8); //BSTreeRemove(&tree, 3); //BSTreeRemove(&tree, 7); BTreeInOrder(tree); system("pause"); return 0; }
这是我用的测试程序。
这里我测试了删除5也就是根节点的情况。