【学习笔记,算法】二叉搜索树(BST)学习笔记

BST调了一天,最后遍历参数错了,没药救了……

本文所有代码均使用数组+结构体,不使用指针!

前言——BST是啥

BST 二叉搜索树是基于二叉树的一种树,一种特殊的二叉树。

二叉搜索树要么是一颗空树,要么满足一下特点(性质)的二叉树:

  1. 它的左子树要么为空,要么它(左子树)的所有节点均小于它的根节点。
  2. 它的右子树要么为空,要么它(右子树)的所有节点均大于它的根节点。
  3. 它的左、右子树也分别是二叉搜索树。

直观的说,如果中序遍历一棵二叉搜索树,则会产生一个有序数列。

如:1.PNG,中序遍历会产生序列:1 2 5 6 8 9

No.1——算法复杂度分析

二叉搜索树是"排过序"的二叉树,并非"用于排序"的二叉树。

它的优势在于"有序性",而且其插入和查找的时间复杂度均为$O(h)$,一般情况下$h=O(\log_{2}n)$,n代表节点数,h代表树的高度。堆的插入算法虽然时间复杂度为$O(\log_{2}n)$,但并不具有有序性。

No.2——使用范围

对比上述的的分析,发现BST的特点是:

  1. 有序
  2. 插入、查找等算法高效

因此,BST使用范围是:要经常对有序数列进行"动态的"插入或查找等工作

No.3——基本操作

BST的基本操作很多,一时半会也讲不过来,就从易到难的讲吧。

No.3-1——三种遍历方式

BST的遍历与二叉树和树一样,有三种:先序遍历、中序遍历、后续遍历。

三种遍历的方式:

  1. 先序遍历:根→左→右 (DLR)
  2. 中序遍历:左→根→右 (LDR) (结合定义,想一想,为什么中序遍历就是有序的??)
  3. 后续遍历:左→右→根 (LRD)

想必大家都知道了吧,上代码。

/*========遍历========*/
void bl(int how,int now)
{
    if(how==1){        //先序遍历
        cout<<a[now].data<<" ";
        if(a[now].l!=-1)
            bl(1,a[now].l);
        if(a[now].r!=-1)
            bl(1,a[now].r);
    }
    if(how==2){        //中序遍历
        if(a[now].l!=-1)
            bl(2,a[now].l);
        cout<<a[now].data<<" ";
        if(a[now].r!=-1)
            bl(2,a[now].r);
    }
    if(how==3){        //后续遍历
        if(a[now].l!=-1)
            bl(3,a[now].l);
        if(a[now].r!=-1)
            bl(3,a[now].r);
        cout<<a[now].data<<" ";
    }
}

No.3-2——建树与插入

强烈建议使用父亲孩子表示法!!!

所谓建树,就是构建一颗树,建树的时候必然涉及到插入。

也没有什么好说的,根据定义走,他怎么说,你怎么做。

  1. 第一个为根节点
  2. 比根大,往右对比
  3. 比根小,往左对比
  4. 如果当前为空,插入成功。

就这四步,代码来了

/*========插入========*/
void into(int sum,int now,int tot)
{
    if(sum<a[now].data)
        if(a[now].l!=-1)
            into(sum,a[now].l,tot);
        else {
            a[now].l=tot;
            a[a[now].l].data=sum;
            a[a[now].l].fa=now;
        }
    else if(sum>a[now].data)
        if(a[now].r!=-1)
            into(sum,a[now].r,tot);
        else {
            a[now].r=tot;
            a[a[now].r].data=sum;
            a[a[now].r].fa=now;
        }
}
/*========构建========*/
void init()
{
    cin>>n;
    int x,tot;
    for(tot=1;tot<=n;tot++){
        scanf("%d",&x);
        if(tot==1)
            a[tot].data=x;
        else
            into(x,1,tot);
    }
}

No.3-3——查找

查找也是二叉搜索树必不可少的一个操作,我这里find()返回了是第几个数,方便删除。

实现很简单,只用熟练掌握二叉搜索树的性质,便可轻易打出以下代码:

/*========查找========*/
int find(int now,int sum)
{
    cout<<now<<endl;
    if(a[now].data==sum) return now;
    if(sum<a[now].data)
        if(a[now].l!=-1) return find(a[now].l,sum);
        else return 0;
    else if(sum>a[now].data)
        if(a[now].r!=-1) return find(a[now].r,sum);
        else return 0;
}
void init2()
{
    cin>>n;
    int i,x;
    for(i=1;i<=n;i++){
        cin>>x;
        if(find(ROOT,x)) printf("Yes\n");
        else printf("No\n");
    }
}

No.3-4——求前驱后继

前驱后继就是在中序遍历时他的前一个与后一个,如图:

如图,八的前驱是六,后继是九。

这太简单了!

放到树上说,前驱就是该节点左儿子的最右节点,后继则是该节点右儿子的最左节点

放一个前驱的代码,后继自己推!

/*========前驱========*/
int pred(int now)
{
    if(a[now].r!=-1) return pred(a[now].r);
    else return now;
}

No.3-5——删除

本操作有一定难度,请务必弄懂

删除要分一些情况讨论,见下:

  1. 没有孩子
  2. 只有一个孩子
  3. 有两个孩子

    3-2. **删除根**
    

第一种情况好办,删掉就行了。

第二种情况也行,删掉后接上左/右孩子。

第三种情况有些复杂,需要用到前驱/后继(用哪个没有一定要求),用它的前驱/后继代替它,同时删除(一个递归过程,又一次调用删除函数)它的前驱/后继。

看一组图吧(有点大):

Delete.png这是一颗二叉搜索树,我们要删掉3。
Delete2.png找到前驱。
Delete3.png替换。
Delete4.png最后变成这个样子。

最后讲一讲如何删根节点,其实与其他删除差不多,只需在开个ROOT变量存储根是谁就行了(根默认为1)

处于一些原因,不放代码。

No.4——写在最后

其实个人认为二叉搜索树的实用价值不大,主要用于练手与预备知识,比方说Treap、平衡树、堆……都需要用到二叉搜索树的性质。

综合代码算了,删除你们自己打吧!

No.4-1——练习题

  1. 模板题
  2. [[HNOI2002]营业额统计](https://www.luogu.org/problemnew/show/P2234)

但,算法之路,才刚刚开始!

Last modification:July 21st, 2019 at 04:30 pm

2 comments

  1. Atlantis

    %%%

    1. Garbage
      @Atlantis

      禁止%人,在%就删了!

Leave a Comment