二叉排序树

二叉排序树是信息学竞赛中一个相当简单又相当复杂的东西,其实考到的次数不多,但是还是挺常用的。
【定义】
二叉排序树的定义是这样的:设当前节点为o,且节点的数据结构是这样的:

struct Node{

Node* left;

Node* right;

int value;

int cmp(int x){

if (value == x) return -1;

return value < x;

}

};

则:o.left->value < o.value,o.right->value > o.value。在这里不考虑重复的元素,则对于每一个节点而言,这个节点的左孩子结点小于这个节点,右孩子节点大于这个节点。
因此我们可以得出推论:每一棵二叉排序树的中序遍历的序列是有序的!中序遍历先访问左孩子结点,再访问当前节点,最后访问右孩子节点。而”最左“的节点是最小的,”最左“的节点的父节点第二小……一直到”最右“的节点最大,这就是”二叉排序树“的来源,其中序遍历的序列是有序的。
既然是一种这样的数据结构,那自然就是可以维护一个有序集合的。有序集合,就必须有查询、插入、删除之类的基本操作,下面来逐一介绍。
【查询】
因为二叉排序树的所有节点都满足左子结点小于当前节点,右子结点大于当前子节点,所以我们可以用一种类似二分查找的思路。比如说先比较当前的节点与待查找值,如果相等就找到了,小于则像左边递归搜索,大于则向右边递归搜索。

const int NOT_FOUND = -1; Node* find(Node* o,int x){ if (o == NULL) return NOT_FOUND; int d = o -> cmp(x); if (d == -1) return o; if (d == 1) return find(o -> left,x); //左子树 if (d == 0) return find(o -> right,x); //右子树 }

不过,这个过程也可以直接用循环迭代实现。循环的迭代更为高效,而且不会爆栈,所以可以处理更大规模的数据了,代码如下:

const int NOW_FOUND = -1; Node* find(Node* o,int x) { while (o != NULL){ int d = o -> cmp(x); if (d == -1) break; if (d == 1) o = o -> left; if (d == 0) o = o -> right; } return (o == NULL ? NOT_FOUND : o); }



【插入】
插入同时也是很简单的。插入是这样的:先找到一个 符合要求的空位,接着直接插入,一般来说也是用递归的写法。和查询差不多。不过插入也可以认为是先找到位置再赋值。

void insert(Node* &root,int x){ if (root == NULL){ root = new Node(); root -> value = x; return; } int d = root -> cmp(x); if (d == 0) insert(root -> right,x); else if (d == 1) insert(root -> left,x); else if (d == -1) insert(root -> left,x); }

因为是普通的二叉排序树,所以说没有那么复杂。
【删除】
删除的操作倒比较麻烦了。首先,我们要分情况讨论。如果当前要删的节点是叶节点,那就直接删,而其他的节点就不行了,要进行一些特殊的处理,使得这个节点变成一个叶节点。一般来说,有两种方法,分别是利用【旋转】操作,还有一种的思路比较直观,实现起来比较简单的方法,只需要一次交换即可。但是最具有通用性的还是用【旋转】操作。在这里,我们就顺便定义一下【旋转】操作。

【旋转】
考虑下面的两颗树,可以发现,它们的中序遍历序列是一样的,但是节点的位置却发生了改变。
二叉排序树 - wenjianwei1 - 算法的设计
 
二叉排序树 - wenjianwei1 - 算法的设计
 
它们的中序遍历都是aobkc,但是点o不再是根了。这说明,我们可以在不改变二叉排序树性质的情况下,改变二叉排序树的形态!实际上,在旋转中,从第一张图的形态变为第二张图的形态,叫做左旋,其逆操作叫做右旋。这是很形象的名字。观察一下,我们只需要改变o的右孩子,k的左孩子,也就是说,代码很简短。下面是左旋和右旋的代码:
左旋:

void left_rotate(Node* o){ if (o == NULL) return; Node* k = o -> right; o -> right = k -> left; k -> left = o; }


右旋:

void right_rotate(Node* k){ if (k == NULL) return; Node* o = k -> left; k -> left = o -> right; o -> right = k; }


【删除(续)】
讲完了旋转,继续回到删除。
既然要把某个点从其本来的位置一直移到叶节点上,那么我们完全可以一直旋转。在Node类型中多记一个size,表示以当前节点为根的子树大小,而每一次旋转的时候都要维护一下,不难写出相应的maintain()函数,思路很直观。
然后,就一直将节点向子树大小较小的那一边旋转即可,很显然必定会旋转到叶节点。不过要加一个特判,不然的话就访问了指向NULL的节点的大小了。指向NULL的节点的size应该是0。用一个迭代的写法就很容易写出来了,非常类似于查找和插入。

void _delete(Node* o){ if (o == NULL) return; int rightsize = 0,leftsize = 0; while (o -> size > 1){ if (o -> right != NULL) rightsize = o -> right -> size; if (o -> left != NULL) rightsize = o -> left -> size; if (rightsize > leftsize) right_rotate(o); else left_rotate(o); } o = NULL; }


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值