点击上方“蓝字”,发现更多精彩。

前面我们介绍了树的基本概念,并引出了二叉树。值得注意的是,无特征的二叉树在工程上几乎没啥用处,一般都是使用bst、avl,trie,rbtree等具有特殊特征的二叉树。

下面,我们先来看一看二叉搜索树。

BST(Binary Search Tree,二叉搜索树)可以提高查找的性能,二叉搜索树相比于其他数据结构的优势在于查找、插入的时间复杂度较低,平均为O(log n),最差为O(n),此时相当于二叉搜索树变成了链表。从这一个特点我们应该不难发现,树,是链表和数组之间的一种平衡,为了获得更好性能的查找、插入和删除方法。

BST的特点是:

1.若任意节点的左子树不空,则左子树上所有节点的值均小于它的根节点的值;

2.若任意节点的右子树不空,则右子树上所有节点的值均大于或等于它的根节点的值;

3.任意节点的左、右子树也分别为二叉查找树;

中序遍历二叉查找树可得到一个关键字的有序序列,一个无序序列可以通过建构一棵二叉查找树变成一个有序序列,建构树的过程即为对无序序列进行查找的过程。每次插入的新的结点都是二叉查找树上新的叶子结点(作为叶子节点插入),在进行插入操作时,不必移动其它结点,只需改动某个结点的指针,由空变为非空即可。

如图1-1所示,即为一颗二叉搜索树:

写在前面:注意,此时我们实现的二叉搜索树,不考虑有关键字相同的情况,即不考虑不同节点的数据元素相同时的场景,这就好比加减乘除没有学会之前,我们不考虑微积分怎么求解。在前面的文章中,我们手动使用指针搭建了一个树形结构,目的是为了演示二叉树的先序,中序和后序遍历。在今天这篇文章中,我们将实现BST的插入,删除和查找工作。为了搭建出一颗二叉搜索树,那我们先实现插入函数。

基本数据定义:

有一个BST根节点中节点初始化的宏,用于初始化根节点。还是使用Linux内核设计者们的设计思路,如同内核链表的基本套路一样,但是,插入操作的函数接口如何定义是一个值得思考的问题。插入,可以按照值插入,也可以按照节点插入。下面我们演示节点插入的方式,值插入读者可以当做练习自行实现。API:

struct crystal_bst_tree {    struct bst_node my_node;    int num;};int bst_insert(struct bst_root *root, struct crystal_bst_tree *tree)int bst_insert(struct bst_root *root, struct bst_node *tree)

bst_insert函数有两个参数,第一个是根节点,第二个是需要插入的节点,插入节点的定义,可以使用struct crystal_bst_tree *也可以使用struct bst_node *。这两种类型的差别无非是是否需要使用container_of宏处理而已。按照Linux内核设计者们的惯例,我们使用后者,来实现插入操作:

static inline void bst_link_node(struct bst_node * node, struct bst_node * parent,        struct bst_node ** bst_link){  node->bst_left = node->bst_right = NULL;  *bst_link = node;}int bst_insert(struct bst_root *root, struct bst_node *tree){    struct bst_node **new = &(root->bst_node), *parent = NULL;    /* Figure out where to put new node */    while (*new) {      struct crystal_bst_tree *this = bst_entry(*new, struct crystal_bst_tree, my_node);        struct crystal_bst_tree *pri_tree = bst_entry(tree, struct crystal_bst_tree, my_node);    parent = *new;          if (pri_tree->num < this->num)          new= &((*new)->bst_left);      else if (pri_tree->num > this->num)          new = &((*new)->bst_right);      else           return -1;    }    /* Add new node */    bst_link_node(tree, parent, new);  return 1;}

其中parent变量,在这里是没有意义的,但是,我们后面的扩展例子rbtree中会使用到。所以这里暂且保留。插入使用循环版本,想使用递归方式的读者可以自己实践。

有了插入之后,我们实现一下遍历,因为前面的文章已经实现过了,这里直接贴出代码:

void PreOrderTraverse(struct bst_node *T)//先序遍历{    if(T == NULL) {        return ;    }    struct crystal_bst_tree *obj;    obj = bst_entry(T,struct crystal_bst_tree,my_node);    printf("%d ",obj->num);    PreOrderTraverse(T->bst_left);    PreOrderTraverse(T->bst_right);}void InOrderTraverse(struct bst_node *T)//中序遍历{   if(T == NULL) {       return ;    }   InOrderTraverse(T->bst_left);   struct crystal_bst_tree *obj;   obj = bst_entry(T,struct crystal_bst_tree,my_node);   printf("%d ",obj->num);   InOrderTraverse(T->bst_right);}void PostOrderTraverse(struct bst_node * T)//后序遍历{    if(T == NULL) {        return;    }    PostOrderTraverse(T->bst_left);    PostOrderTraverse(T->bst_right);    struct crystal_bst_tree *obj;    obj = bst_entry(T,struct crystal_bst_tree,my_node);    printf("%d ",obj->num);}

有了插入,遍历之后,我们再实现一个查找函数,为什么Linux内核的红黑树把查找函数交给用户自己实现呢?因为此时必须要要根据具体的数据类型来进行操作了,查找可以是根据节点查找,也可以是根据值查找,这里我们选取值查找的方式:

struct crystal_bst_tree *my_search(struct bst_root *root, int num){    struct bst_node *node = root->bst_node;     while (node) {      struct crystal_bst_tree *data = bst_entry(node, struct crystal_bst_tree, my_node);           if (num < data->num)          node = node->bst_left;      else if (num > data->num)          node = node->bst_right;      else          return data;    }        return NULL;}

然后,我们再实现两个函数,查找最小和最大值即查找first和last:

struct bst_node *bst_first(struct bst_root *root){  struct bst_node  *n;  n = root->bst_node;  if (!n) {    return NULL;    }  while (n->bst_left) {    n = n->bst_left;    }  return n;}struct bst_node *bst_last(struct bst_root *root){  struct bst_node  *n;  n = root->bst_node;  if (!n) {    return NULL;    }  while (n->bst_right) {    n = n->bst_right;    }  return n;}

最后,我们实现删除操作。删除操作相比较于其他操作较为复杂一点,其代码逻辑是:

1.若删除结点为叶子结点,即PL(左子树)和PR(右子树)均为空树。由于删去叶子结点不破坏整棵树的结构,则只需修改其双亲结点的指针即可。

2.若删除结点只有左子树PL或右子树PR,此时只要令PL或PR直接成为其双亲结点的左子树(当删除节点是左子树)或右子树(当删除节点是右子树)即可,作此修改也不破坏二叉查找树的特性。

3.若删除结点的左子树和右子树均不空。在删去节点之后,为保持其它元素之间的相对位置不变,可按中序遍历保持有序进行调整,可以有两种做法:其一是令删除节点的左子树为*f的左/右(依删除节点是*f的左子树还是右子树而定)子树,*s为删除节点左子树的最右下的结点,而删除节点的右子树为*s的右子树;其二是令删除节点的直接前驱(in-order predecessor)或直接后继(in-order successor)替代删除节点,然后再从二叉查找树中删去它的直接前驱(或直接后继)。我们按照方法二,使用直接前驱或直接后继来删除节点。

直接前驱:寻找左子树里面的最大值;直接后继:寻找右子树里面的最小值。这两个最大最小值无论是使用递归还是循环,都是非常容易获得的。

以直接前驱法为例子,找到直接前驱之后,将直接前驱替换成需要删除的节点,并删除直接前驱节点即可。如果是直接后继,找到直接后继之后,将直接后继替换成需要删除的节点,并删除直接后继节点即可。

本例使用直接前驱替换的方法:

bool Delete(struct bst_node **node){    struct bst_node *q, *s;    if (!(*node)->bst_right && !(*node)->bst_left) {        free(*node);        *node = NULL;  //该节点为叶子节点,直接删除     } else if (!(*node)->bst_right) { // 右子树空则只需重接它的左子树        q = (*node)->bst_left;        bst_entry(*node, struct crystal_bst_tree, my_node)->num = bst_entry(q, struct crystal_bst_tree, my_node)->num;        (*node)->bst_left = q->bst_left;        (*node)->bst_right = q->bst_right;        free (q);    } else if (!(*node)->bst_left) { // 左子树空只需重接它的右子树        q = (*node)->bst_right;;        bst_entry(*node, struct crystal_bst_tree, my_node)->num = bst_entry(q, struct crystal_bst_tree, my_node)->num;        (*node)->bst_left = q->bst_left;        (*node)->bst_right = q->bst_right;        free (q);    } else { // 左右子树均不空        q = *node;        s = (*node)->bst_left;        while (s->bst_right) {//(寻找直接前驱)            q = s;            s = s->bst_right;        }         bst_entry(*node, struct crystal_bst_tree, my_node)->num = bst_entry(s, struct crystal_bst_tree, my_node)->num; // s指向被删结点的“前驱”        if (q != *node) {            q->bst_right = s->bst_left;  // 重接q的右子树        } else {            q->bst_left = s->bst_left;  // 重接q的左子树        }        free(s);    }    return true;}bool DeleteBST(struct bst_node **node, int key) {    // 若二叉查找树中存在关键字等于key的数据元素时,则删除该数据元素,并返回TRUE;否则返回FALSE    if (!*node) {        return false; //不存在关键字等于key的数据元素    } else {        if (key == bst_entry(*node, struct crystal_bst_tree, my_node)->num) {//找到关键字等于key的数据元素            return Delete(node);        }        else if (key < bst_entry(*node, struct crystal_bst_tree, my_node)->num) {            return DeleteBST(&(*node)->bst_left, key);        } else {            return DeleteBST(&(*node)->bst_right, key);        }    }}

实现上面的代码可以很好的训练自己的代码能力,有递归,有二级指针,有树的知识,一定要自己动手写一写,否则你觉得都是if else,会给你一种很简单的感觉。

最后,再来看看main函数测试用例:

int main(void){    struct bst_root root = BST_ROOT;    #define SZIE  (10)    struct crystal_bst_tree *tmp[SZIE];    int i;    for (i = 0;i < SZIE;i++) {        tmp[i] = malloc(sizeof(struct crystal_bst_tree));    }    tmp[0]->num = 8;    bst_insert(&root,&tmp[0]->my_node);    tmp[1]->num = 3;    bst_insert(&root,&tmp[1]->my_node);    tmp[2]->num = 10;    bst_insert(&root,&tmp[2]->my_node);    tmp[3]->num = 1;    bst_insert(&root,&tmp[3]->my_node);    tmp[4]->num = 6;    bst_insert(&root,&tmp[4]->my_node);    tmp[5]->num = 14;    bst_insert(&root,&tmp[5]->my_node);    tmp[6]->num = 4;    bst_insert(&root,&tmp[6]->my_node);    tmp[7]->num = 7;    bst_insert(&root,&tmp[7]->my_node);    tmp[8]->num = 13;    bst_insert(&root,&tmp[8]->my_node);    //tmp[9]->num = 5;    //bst_insert(&root,&tmp[9]->my_node);    //printf("%p %p\n",tmp[6]->my_node.bst_right,&tmp[9]->my_node);    //DeleteBST(&root.bst_node,7);    printf("\r\nInOrderTraverse:\r\n");    InOrderTraverse(root.bst_node);        printf("\r\n");        struct crystal_bst_tree *first,*last;    first = bst_entry(bst_first(&root),struct crystal_bst_tree,my_node);    printf("first->num = %d\r\n",first->num);    last = bst_entry(bst_last(&root),struct crystal_bst_tree,my_node);    printf("last->num = %d\r\n",last->num);    int num = 7;    struct crystal_bst_tree * t = my_search(&root,num);    if (t) {        printf("finded : %d\n",t->num);    } else {        printf("%d was not find\n",num);    }        return 0;}

运行输出:

使用中序遍历,二叉搜索树输出是一个有序序列,这也是二叉搜索树的应用场景之一。图1-1所示的二叉搜索树,如果我要再插入一个元素为5,你知道应该插入在哪个地址吗?取消main函数中29-31行注释的代码,你可以发现:

tmp[6]的右节点,即元素5,插入在4的右节点处。再打开32行屏蔽的代码,测试删除节点的操作:

至此,我们就完成了BST的操作和练习。前面说到,BST最坏的情况下,算法时间复杂度为O(n),这是因为,当二叉树刚好是一个链表的时候,此时退化成了线性结构,所以,人们又提出了AVL(平衡二叉树),这也是我们下期文章讲解的内容,让我们下期再见,谢谢大家的观看。不要以为工作了学习就与你无关,我们一直都是学生。B站(C语言教程,强烈推荐的视频教程):https://space.bilibili.com/5782182

中根遍历二叉查找树所得序列一定是有序序列_二叉搜索树(BST)相关推荐

  1. 中根遍历二叉查找树所得序列一定是有序序列_数据结构考研学习笔记(九)树、森林...

    点击上面蓝字关注我们 树.森林 1. 树的存储结构 1.1 双亲表示法 1.2 孩子表示法 1.3 孩子兄弟表示法 2. 树. 森林与二叉树的转换 3. 树和森 林的遍历 4. *书的应用-并查集 1 ...

  2. 中根遍历二叉查找树所得序列一定是有序序列_学习数据结构--第六章:查找(查找)

    第六章:查找 1.查找的基本概念 查找:在数据集合中寻找满足某种条件的数据元素的过程. 查找的结果 查找成功和查找失败 查找表:用于查找的数据集合,由同一种数据类型(或记录)的组成,可以是一个数组或链 ...

  3. 【算法】有序链表转换二叉搜索树和从中序与后序遍历序列构造二叉树Java解答参考

    三道算法题 1.有序链表转换二叉搜索树 Java代码参考 2.从中序与后序遍历序列构造二叉树 Java代码参考 3.移除元素 Java代码参考 1.有序链表转换二叉搜索树 给定一个单链表,其中的元素按 ...

  4. [Leetcode][第109题][JAVA][有序链表转换二叉搜索树][分治][快慢指针][中序遍历]

    [问题描述][中等] [解答思路] 1. 分治 快慢指针 复杂度 class Solution {public TreeNode sortedListToBST(ListNode head) {ret ...

  5. PAT甲级1043 Is It a Binary Search Tree :[C++题解]判断二叉搜索树BST、给定前序序列和中序序列

    文章目录 题目分析 题目链接 题目分析 二叉搜索树(BST):左子树小于根结点,右子树大于等于根结点. 二叉搜索树的中序遍历一定是有序序列.所谓中序遍历:先访问左子树,再访问根结点,最后访问右子树. ...

  6. 【数据结构】二叉查找树/二叉搜索树BST(附相关C++代码)

    文章目录 BST相关概念 BST如何添加节点 BST如何遍历 BST如何求最值 BST如何删除节点 BST如何查找节点 如何验证一棵树是BST 本文内容将主要介绍二叉查找树的相关概念,与关于二叉查找树 ...

  7. 在二叉搜索树(BST)中查找第K个大的结点之非递归实现

    一个被广泛使用的面试题: 给定一个二叉搜索树,请找出其中的第K个大的结点. PS:我第一次在面试的时候被问到这个问题而且让我直接在白纸上写的时候,直接蒙圈了,因为没有刷题准备,所以就会有伤害.知耻而后 ...

  8. 1043 Is It a Binary Search Tree (25 分)【难度: 中 / 知识点: 构造二叉搜索树(BST) 】

    https://pintia.cn/problem-sets/994805342720868352/problems/994805440976633856 首先二叉搜索树是左子树的元素都小于根,左子树 ...

  9. 49 - 算法 - 二叉树 - leetcode108.-将有序数组转换为二叉搜索树-中序遍历 - vector

    //vec 构造函数 vec.begin() end() + num ={134} =other vector //一直到null 就行 new(int) root->left ->rig ...

最新文章

  1. Android开发--Http操作介绍(二)
  2. fastjson的使用问题
  3. 垃圾“程序是怎样练成的”——关于《C程序设计伴侣》第A章(四)
  4. 学习笔记Hive(六) —— Hive开发应用
  5. php获取form传递的变量,PHP-将变量传递给Ninja Form字段
  6. 自定义维护视图变量(Maintenance view variant)
  7. mysql错误:……is marked as crashed and should be repai
  8. 2000坐标系xy坐标几位_2000国家大地坐标系转换演示及实践应用专题培训研讨班...
  9. docker选择安装位置_监控摄像机的安装位置选择和焦距选择
  10. python字符串解释_python基础之字符串详解
  11. c#重写了窗体的OnKeyDown事件,但是不执行
  12. python支持向量机_支持向量机(SVM)Python实现
  13. 郭凯天:中国公益慈善行业数字化观察与思考
  14. 【leetcode】416. Partition Equal Subset Sum
  15. ssh - 安全外壳协议的详解,为什么使用它
  16. 计算机设备分配资源的发展,分配系统资源
  17. STM32HAL库使用RX8025
  18. matlab找最大值_matlab求函数最大值
  19. WIN10系统右下角网络连接图标消失解决方案
  20. devicemapper介绍

热门文章

  1. 使用 Rxjs 解决 Angular Component 之间的通信问题
  2. SAP 电商云 Spartacus UI 回归测试 wish-list.core-e2e-spec.ts
  3. SAP 电商云 Spartacus UI set delivery mode HTTP put 请求的触发时机
  4. Jest 测试框架 beforeEach 的设计原理解析
  5. SAP Spartacus 4.0 deprecation 之一 - i18next-xhr-backend
  6. SAP Commerce Cloud 2011装好之后,访问Accelerator时遇到错误
  7. Angular单元测试框架karma-jasmine里类似ABAP unit框架的setup和class_setup方法
  8. SAP Fiori Elements - how to set breakpoint to get converted xml view parsed by f
  9. What does SAP UI5 bindItem occurs
  10. when and where is createContent called