二叉树:
每个节点,除了key之外,还有3个属性:父亲、左孩子、右孩子,其中,父亲一定大于左孩子,一定小于(等于)右孩子
搜索二叉树:
利用二叉树的这些性质,我们可以得出,在这样一个结构下,插入、删除、查找的步骤,最多不会超过树的高度。注意哈,这里的二叉树,并不是平衡的,所以,其树的高度并不等于lgN,所以,最坏情况下,这个树的插入、查找、删除的最差性能,等于树的节点树N,跟链表的性能相等
空间上,每个数据,都会多一个父亲、左右孩子这三个数据的指针空间
哨兵节点:
就是定义一个特殊的节点,这个节点用来表示NULL节点,其目的是,能够大大简化我们代码的简洁度(NULL不是一个节点,而哨兵节点是一个节点,其也有父亲、孩子)
插入和查找操作:
每次插入,都将数据插入到叶子节点上,方法就是:从根节点开始,如果比当前值大,就比较其右孩子,否则,比较其左孩子,直到遇到空节点(卫星节点),那么这个卫星节点的位置,就是需要插入的位置。这种情况有一个例外,那就是如果当前节点的值等于待插入的值,这时候就不要再和孩子们比较,而是做统一处理,加入到这个节点的右孩子节点上,查找操作,思路也一样的,唯一需要约定的就是,总是返回第一个被找到的值相等的节点,而不是返回其右孩子节点(如果右孩子节点的值也相等的话)
删除操作:
删除操作,分为3种情况:
1、待删除的节点,没有左孩子
这种情况,简单的将右孩子替换掉当前节点的位置,就可以了(不会破坏“父亲一定大于左孩子,一定小于(等于)右孩子”的性质)
2、待删除的节点,没有右孩子
跟第一种情况一样,简单的将左孩子替换掉当前节点的位置就可以了
3、又有左孩子,又有右孩子
这种情况是最复杂的一种,这时需要我们好好想下,我们的目标是什么:节点删除后,不破坏二叉树的性质,即“父亲一定大于左孩子,一定小于(等于)右孩子”
由于右孩子可能与当前待删除节点的值相等,情况更复杂,所以,我们考虑从左孩子中,找出一个节点,替换掉待删除节点。
那么找哪个节点比较合适呢?前驱!原因很简单,前驱是最接近待删除节点的那个节点,也就是在左树中,这个节点的值是最大的,而且这个值一定没有右孩子(它最大嘛),所以将这个值放到待删除节点的位置上,一来可以保证其左孩子都比它小(不破坏二叉树的性质),二来前驱节点的右孩子一定为空,方便直接将其右孩子节点指向待删除节点的右孩子!
这里需要注意的是,前驱不一定就是待删除节点的左孩子哦,简单的例子:如果左孩子还有一个右孩子,显然其前驱是这个右孩子,待删除节点是前驱的父亲的父亲
中序遍历:
先输出左孩子,在输出节点本身,再输出右孩子(节点本身的输出位置位于左右孩子的中间,这个就叫做中序遍历,很容易就得出什么是前序遍历和后续遍历)。
由二叉树的性质:“父亲一定大于左孩子,一定小于(等于)右孩子”,得出中序遍历出来的结果,就是一个从小到大的已排好序的数组
废话就说到这些,下面上代码(友情提醒下,请好好理解代码中的注释,然后结合上面的文字,我想可以很容易的理解二叉树上的这些操作):
先给出节点类的定义,该类在后续都会用到,以后将不会再重复帖代码:
//
// TreeNode.h
// p1
//
// Created by MinerGuo on 14/11/25.
// Copyright (c) 2014年 MinerGuo. All rights reserved.
//
#ifndef __p1__TreeNode__
#define __p1__TreeNode__
#include <stdio.h>
class TreeNode;
class TreeNode {
public:
TreeNode * _parent;
TreeNode * _leftChild;
TreeNode * _rightChild;
int _key;
TreeNode(int key){
_parent = NULL;
_parent = NULL;
_rightChild = NULL;
_key = key;
}
};
//哨兵,表示空元素
static TreeNode * treeNodeNil = new TreeNode(-1);
#endif /* defined(__p1__TreeNode__) */
//
// SearchTree.h
// p1
//二叉搜索树
//
// Created by MinerGuo on 14/11/24.
// Copyright (c) 2014年 MinerGuo. All rights reserved.
//
#ifndef __p1__SearchTree__
#define __p1__SearchTree__
#include <stdio.h>
#include "TreeNode.h"
#include "ArrayGenerater.h"
#include <iostream>
class SearchTree {
ArrayGenerater _ArrayGenerater;
TreeNode * _root;
int _itemCount;
int _readIndex;
public:
SearchTree(){
_root = treeNodeNil;
_root->_leftChild = treeNodeNil;
_root->_rightChild = treeNodeNil;
_root->_parent = treeNodeNil;
_itemCount = 0;
}
TreeNode * getRoot(){
return _root;
}
//插入一个元素
void insert(int key){
TreeNode * _item = new TreeNode(key);
_item->_leftChild = treeNodeNil;
_item->_rightChild = treeNodeNil;
TreeNode * _tmp = treeNodeNil;
TreeNode * _current_item = _root;
while (_current_item != treeNodeNil) {
_tmp = _current_item;
if(_item->_key > _current_item->_key){
_current_item = _current_item->_rightChild;
}else if(_item->_key < _current_item->_key){
_current_item = _current_item->_leftChild;
}else{
//如果相等,那就不要往下找了,立即在此结束,保证相等的元素,都在一起
break;
}
}
_itemCount++;
_item->_parent = _tmp;
if (_tmp == treeNodeNil) {
//作为根节点插入
_root = _item;
}else if(_tmp ->_key == key){
//相同节点,统一插入右子节点
_item->_rightChild = _tmp->_rightChild;
_item->_rightChild->_parent = _item;
_tmp->_rightChild = _item;
}else if (_item->_key > _tmp->_key){
_tmp->_rightChild = _item;
}else{
_tmp->_leftChild = _item;
}
}
//中序遍历:先遍历左子节点,再遍历当前节点,再遍历右子节点
//这样遍历出来的结果,就是一个有序的数组
int * readByMiddleOrder(int * result,TreeNode * currentItem){
if (result == NULL) {
result = new int [_itemCount];
currentItem = _root;
_readIndex = 0;
}
if(currentItem->_leftChild != treeNodeNil){
readByMiddleOrder(result,currentItem->_leftChild);
}
*(result + _readIndex) = currentItem->_key;
_readIndex++;
if(currentItem->_rightChild != treeNodeNil){
readByMiddleOrder(result,currentItem->_rightChild);
}
//释放资源
delete currentItem;
return result;
}
//搜索
TreeNode * search(TreeNode * node,int key){
if(node == treeNodeNil || key == node->_key){
return node;
}
if(key < node->_key){
return search(node->_leftChild,key);
}
return search(node->_rightChild,key);
}
//获取最小值
TreeNode * getMinimun(TreeNode * node){
//这里可以体现出哨兵节点的优势,可以简化这里while条件的写法
while (node->_leftChild != treeNodeNil) {
node = node->_leftChild;
}
return node;
}
//获取最大值
TreeNode * getMaxmun(TreeNode * node){
while (node->_rightChild != treeNodeNil) {
node = node->_rightChild;
}
return node;
}
//辅助方法:用 X 替换 Y,这里只是将父节点的子节点的指针给改了,并未修改节点x的孩子节点,这个留给调用者处理
void trasplant(TreeNode * root,TreeNode * x,TreeNode * y){
if(y == y->_parent->_leftChild){
y->_parent->_leftChild = x;
}else if(y == y->_parent->_rightChild){
y->_parent->_rightChild = x;
}else{
//y是根节点
_root = x;
}
if(x != treeNodeNil){
x->_parent = y->_parent;
}
}
//删除一个节点
void deleteNode(TreeNode * node){
if(node->_leftChild == treeNodeNil){
//如果要删除的节点,没有左子节点,则直接拿右子节点替换当前节点
trasplant(_root,node->_rightChild,node);
}else if (node->_rightChild == treeNodeNil){
//如果要删除的节点,没有右子节点,则直接拿左子节点替换当前节点
trasplant(_root,node->_leftChild,node);
}else{
//左右孩子都有,则统一选择从左子节点中,求出当前待删节点的前驱(最接近待删节点的数,也就是左边最大的那个数,如果拿这个数据替换当前待删节点,很显然,依然维持了搜索树的属性:左边都比当前节点小,右边都比当前节点大)
TreeNode * firstSmallInChild = getMaxmun(node->_leftChild);
if(firstSmallInChild->_parent != node){
//前驱不是待删节点的左孩子,则需要先将前驱给抠出来:
//第一步:将这个前驱的左孩子,放到前驱的位置上来
trasplant(_root,firstSmallInChild->_leftChild,firstSmallInChild);
//第二步:第一步结束时,前驱的左孩子的位置,空出来了,于是将待删除节点的左孩子,放到前驱的左孩子的位置上(我们的目标就是要将前驱放到待删除节点的位置上嘛)
firstSmallInChild->_leftChild = node->_leftChild;
//第三步:前驱的左孩子的父亲,设置为前驱(第二步的完善)
firstSmallInChild->_leftChild->_parent = firstSmallInChild;
//疑问:为啥这里不对前驱的右孩子进行处理呢?道理很简单,前驱是最大的,它不可能有比它还大的数字,所以右孩子位置肯定是空着的
}
//这时,我们已经将要替换到待删除节点位置的节点,给扣出来了,而且因为是前驱,其右孩子位置一定是空着的
//将前驱放到带删除的位置上来
trasplant(_root,firstSmallInChild,node);
//将待删除节点的右孩子,放到前驱的右孩子位置上,替换完成
firstSmallInChild->_rightChild = node->_rightChild;
firstSmallInChild->_rightChild->_parent = firstSmallInChild;
}
}
//测试删除元素,以下是测试代码,_ArrayGenerater类的实现,这里没给出,读者自己实现吧,其实就是生成随机的数而已
void testDelete(int deleteCount,int maxNumber){
while (deleteCount > 0) {
int randomKey = _ArrayGenerater.generateOneNumber(maxNumber);
TreeNode * _deleteNode = search(_root,randomKey);
if (_deleteNode != treeNodeNil) {
deleteNode(_deleteNode);
deleteCount--;
_itemCount--;
//释放资源
delete _deleteNode;
}
}
}
};
#endif /* defined(__p1__SearchTree__) */