一、二叉树基本概念
二叉树的其中一个重要应用,是提供一种快速查找数据的方法,即:将数据节点按照某种规律形成一棵二叉树,然后利用二叉树特殊的逻辑结构减少搜索数据的次数,提高查找的效率。
这种按照某种规律构建,用来提高搜索性能的二叉树,被称为搜索二叉树(Binary Search Tree),即BST。
具体而言,二叉树提高搜索效率的秘诀在于:按照“小-中-大”(当然“大-中-小”也是一样的)的规律来存储数据,即对于任意一个节点,都可以明确找到其值大于或等于其左孩子节点,且小于或等于其右孩子节点。如下图所示:
比如需要用二叉树储存整个年级的数学成绩
由于树中所有的节点均满足“小-中-大”的规律,因此当从根开始查找某个节点时速度比顺序查找要快得多,比如要找节点38,当发现38大于根节点13后,就可以确定13的左子树一定没有38,这就去掉了半边子树的节点。
因此,二叉搜索树又被称为二叉排序树、二叉查找树。
实际上,对于一棵二叉树而言,其搜索节点的时间复杂度,最糟糕的情形是其退化为链表,最乐观的情形是完美或完全二叉树,那么其搜索时间复杂度就是介于:
最差:T(n)=O(n)
最优:T(n)=O(log2n)
二、二叉树的算法设计
1、构建二叉树节点
struct node { //数据域 datatype data; //指针域 struct node * Lchild;//指向左孩子指针 struct node * Rchild;//指向右孩子指针 };
2、插入节点
对于BST而言,插入一个节点主要是要保持其“小-中-大”的逻辑不变,因此插入的节点的逻辑可以从根节点开始,一步步寻找新节点的“最终归宿”,比如在如下BST中,要插入新节点16,最终应该插入到节点17的左孩子处。
在实现插入算法的时候,由于树状结构本身是递归的,因此可以使用递归函数更优雅地实现插入算法。如下:
情况①:
第一次插入节点给这个二叉树,二叉树是空的,则直接把Root根指针指向新节点
struct node *Root=NULL; Root = bstInsert(Root,25);
对应插入代码为:
if(root == NULL) return new;
情况②:
非第一次插入节点
递进深入二叉树
递进的条件:
只要节点的Lchild或Rchild不为NULL 则以下一个节点作为根 继续深入
回归的条件:
到了 尾巴为NULL 同时满足大小关系条件 则返回当前节点地址
// 将新数据data(以整型为例),插入到二叉搜索树root中 // 插入节点后,返回新的BST的根 node *bstInsert(node *root, int data) { // 准备好新节点,并将数据填入其中 node *new = (node *)malloc(sizeof(node)); new->data = data; new->lchild = NULL; new->rchild = NULL; // 若此时BST为空,则new称为二叉树的根节点 if(root == NULL) return new;//只要满足这个条件就开始回归 // 若新节点比根节点小,那么新节点应该插入左子树中 // 至于插入到左子树的具体什么位置就不用管了,直接递归即可 if(data < root->data) root->lchild = bstInsert(root->lchild, data);//左递进 // 若新节点比根节点大,那么新节点应该插入右子树中 // 至于插入到右子树的具体什么位置就不用管了,直接递归即可 else if(data > root->data) root->rchild = bstInsert(root->rchild, data);//右 // 若已存在,则不处理 else { printf("%d已存在\n", data); } free(new); return root; }
3、删除节点
(1)删除一个BST的节点要比插入困难一点,但同样是要遵循一个原则,即:删除节点后仍然要保持“小-中-大”的逻辑关系。
(2)假设要删除的节点是x,大体思路如下:
- 若要删除的节点小于根节点,则递归地在左子树中删除x
- 若要删除的节点大于根节点,则递归地在右子树中删除x
若要删除的节点恰好就是根节点,则分如下几种情况:
- 根节点若有左子树,则用左子树中最大的节点max替换根节点,并在左子树中递归删除max
- 否则,若有右子树,则用右子树中最小的节点min替换根节点,并在右子树中递归删除min
- 否则,直接删除根节点
(3)举个例子
以下图为例,假设在一棵二叉树中要删除节点15,在找到节点之后,判断其有左子树,那么就沿着其左子树找到最右下角(最大)的节点19,替换要删除的节点15,然后再将多余的节点19删掉:
(4)示例代码
// 将数据(以整型为例)data从二叉树中删除 // 并返回删除之后的二叉树的根 node *bstRemove(node *root, int data) { if(root == NULL) return NULL; // 若data小于根节点,则递归地在左子树中删除它 if(data < root->data) root->lchild = bstRemove(root->lchild, data); // 若data大于根节点,则递归地在右子树中删除它 else if(data > root->data) root->rchild = bstRemove(root->rchild, data); // 若data恰好就是根节点,则分如下几种情况: else { // a. 根节点若有左子树,则用左子树中最大的节点max替换根节点 // 并在左子树中递归删除max if(root->lchild != NULL) { node *max; for(max=root->lchild; max->rchild!=NULL; max=max->rchild); root->data = max->data; root->lchild = bstRemove(root->lchild, max->data); } // b. 否则,若有右子树,则用右子树中最小的节点min替换根节点 // 并在右子树中递归删除min else if(root->rchild != NULL) { node *tmp; for(tmp=root->rchild; tmp->lchild!=NULL; tmp=tmp->lchild); root->data = tmp->data; root->rchild = bstRemove(root->rchild, tmp->data); } // c. 否则,直接删除根节点 else { free(root); return NULL; } } return root; }
(5)总结
- 先递进的找到待删除节点
- 根据找到待删除节点分析其三种情况 有左子树—优先找到左子树最大数的节点 只有右子树,从右子树中找到最小的那个数的节点
待删除节点时叶子–直接删除—开始回归 - 如果上面的红色部分情况 找到了左子树中最大 或找了右子树最小的 拿这个数替换掉待删除节点的树
- 把找到的这个替换的节点作为新的待删除节点,重复上面步骤直到满足回归条件
4、遍历二叉树
// 前序遍历 void preOrder(node *root) { //1 空树 2 遇到一个度为0/1的节点 ---- 回归条件 if(root == NULL) return; // 先访问根节点 printf("%d", root->data); // 再依次使用前序算法,遍历其左、右子树 --- 递进 preOrder(root->lchild); preOrder(root->rchild); } // 中序遍历 void inOrder(node *root) { if(root == NULL) return; // 先访问左子树 inOrder(root->lchild); // 再访问根节点 printf("%d", root->data); // 再访问右子树 inOrder(root->rchild); } // 后序遍历 void postOrder(node *root) { if(root == NULL) return; // 先依次使用后序算法,遍历其左、右子树 postOrder(root->lchild); postOrder(root->rchild); // 再访问根节点 printf("%d", root->data); }
5、层次遍历
对于按层遍历,则需要借助队列来达到这一目的。具体做法是:
- 创建一个队列,并将根节点指针入队
- 判断队列是否为空:
a. 是则退出程序 - b. 否则让队头元素出队,并将队头的左右孩子依次入队
c. 循环此步骤
示例代码:
void levelOrder(node *root) { if(root == NULL) return; // 将根节点入队 linkQueue *q = initQueue(); enQueue(q, root); node *tmp; while(!isEmpty(q)) { // 出队并访问队头 outQueue(q, &tmp); printf("%d\t", tmp->data); // 依次将其左右孩子(若有)入队 if(tmp->lchild != NULL) enQueue(q, tmp->lchild); if(tmp->rchild != NULL) enQueue(q, tmp->rchild); } printf("\n"); }