[数据结构]第八章-字典

本文介绍了字典这一抽象数据类型,详细阐述了用数组、二叉搜索树(包括插入和删除操作)及AVL树实现字典的方法。特别是AVL树,作为平衡二叉搜索树,确保了查找、插入和删除操作的时间复杂度维持在O(logn)。文中还讨论了如何在AVL树中进行旋转操作以保持平衡。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

当集合中的元素有一个线性序,即全集合是一个有序集时,涉及到与这个线性序有关的集合运算的时候,用符号表表示时会很难实现或效率不高,因此引入字典这一抽象数据类型,字典中元素有一个线性序,且支持涉及线性序的一些集合运算。

字典:以有序集为基础的抽象数据类型。

·支持运算:

1.Member(x,S):成员运算
2.Insert(x,S):插入运算
3.Delete(x,S):删除运算
4.Predecessor(x,S):前驱运算(返回集合S中小于x的最大元素)
5.Successor(x,S):后继运算(返回集合S中大于x的最小元素)
6.Range(x,y,S):区间查找运算(返回集合S中介于x和y之间的所有元素组成的集合)
7.Min(S):最小元运算(返回当前集合S中依线性序最小的元素)

用数组实现字典

用数组实现字典与用数组实现符号表的不同之处在于,可以利用线性序将字典中的元素从小到大依序存储在数组中,用数组下标的序关系来反映字典元素之间的序关系。如在这种表述法下,可以用二分查找来实现Menber运算,时间复杂度O(nlogn)。但缺陷在于插入和删除运算效率较低,由于需要移动部分元素,复杂度O(n)。

用二叉搜索树实现字典

· 二叉搜索树是满足以下条件的二叉树:1.左子树上的所有节点值均小于根节点值,2右子树上的所有节点值均不小于根节点值,3,左右子树也满足上述两个条件。

· 二叉搜索树的中序是所有结点的递增序列。由此性质只需知道了前序或后序则可确定一棵二叉搜索树。

· 二叉搜索树的实现)<- 链接中有用链表实现的代码

插入:
  1.若当前的二叉查找树为空,则插入的元素为根节点
  2.若插入的元素值小于根节点值,则将元素插入到左子树中
  3.若插入的元素值不小于根节点值,则将元素插入到右子树中。

删除:
  1.p为叶子节点,直接删除该节点,再修改其父节点的指针(注意分是根节点和不是根节点),如图a。
这里写图片描述

  2.p为单支节点(即只有左子树或右子树)。让p的子树与p的父亲节点相连,删除p即可;(注意分是根节点和不是根节点);如图b。
这里写图片描述

  3.p的左子树和右子树均不空。找到p的后继y,因为y一定没有左子树,所以可以删除y,并让y的父亲节点成为y的右子树的父亲节点,并用y的值代替p的值;或者方法二是找到p的前驱x,x一定没有右子树,所以可以删除x,并让x的父亲节点成为y的左子树的父亲节点。如图c。
  (找前驱时只要在其左子树中不断找右儿子,而后继则是在右子树中不断的往下找左儿子)
这里写图片描述

· 给定序列x1,x2,…,xn 建立一棵二叉搜索树?
一个方法是把序列元素一个个插入,另一个办法则是先将元素排序然后找出中间的数值放在树根,然后这个序列变被分为两段,递归地分别在左右段中找中间的数值放在左右子树。两种实现都是O(nlogn)

用AVL树实现字典

由于二叉树搜索树在一定条件下可能退化为一条链(如序列1,2,3,4以1为根节点的时候)。这样它相比链表实现的优势已经消失,查找的效率为O(n)。想要使得它的效率维持在O(nlogn)那么就需要用到AVL树。

·AVL树(平衡二叉搜索树):
1. 本身首先是一棵二叉搜索树。
2. 带有平衡条件:每个结点的左右子树的高度之差的绝对值最多为1。

为方便描述左右子树的高度差,这里引入平衡因子的定义:该结点的左子树高度减右子树高度。

如何在插入、删除过程中维持AVL树?
在依照二叉搜索树的插入删除操作不变的基础上,从这个结点开始不断向根回溯直到找到不平衡的位置然后做旋转操作。不平衡的类型分为LL,RR,LR,RL。LL型时单向右旋,RR时单向左旋。LR,RL时双向旋转(先左后右、先右后左),下面进行详细分析。

·AVL树的旋转操作转载+修改(原博好像写错了一部分地方)

情况1(LL):对该结点的左儿子的左子树进行了一次插入。(单向右旋得到右图)
解决办法是将x上移一层,并将z下移一层,由于在原树中k2 > k1,所以k2成为k1的右子树,而y是小于k2的,所以成为k2的左子树。

这里写图片描述

举例:在下图的情况中,插入节点“6”。插入后沿根回找发现“8”子树不平衡,并且是LL的情况。首先我们不考虑其父节点的情况(因为我们创建节点是递归创建的,可以不用考虑其父节点与其的连接)这里“8”的右孩子不发生变化,将其左孩子设为“7”的右孩子,将7的右孩子设为“8”及其子树,然后更新两结点的子树高度值,最后返回“7”节点的指针。

这里写图片描述

AVLTree SingleRotateWithLeft(PAVLNode k2)

{

     PAVLNode k1;

     k1 = k2->l;

     k2->l = k1->r;

     k1->r = k2;

     k2->h = MAX( Height( k2->l ), Height( k2->r ) ) + 1;

     k1->h = MAX( Height( k1->l ), k2->h ) + 1;

     return k1;  /* New root */

}

情况4(RR):对该结点的右儿子的右子树进行了一次插入。(单向左旋得到右图)
解决办法是将z上移一层,并将x下移一层,由于在原树中k2 > k1,所以k1成为k2的左子树,而y是大于k1的,所以成为k1的右子树。

这里写图片描述

举例:在下图的情况下插入节点“6”。插入后沿根回找发现“2”子树不平衡,并且是RR的情况。操作同情况1类似。

这里写图片描述

AVLTree SingleRotateWithRight(PAVLNode k1)

{

    PAVLNode k2;

    k2 = k1->r;

    k1->r = k2->l;

    k2->l = k1;

    k1->h = MAX( Height( k1->l ), Height( k1->r ) ) + 1;

    k2->h = MAX( Height( k2->r ), k1->h ) + 1;

    return k2;  /* New root */

}

情况3(LR):对该结点的左儿子的右子树进行了一次插入。(先左旋再右旋)

这种情况是单旋调整不了的,如下图

这里写图片描述

只能通过左-右双旋调整

这里写图片描述

所以这里正确做法是,先对k3的左子树k1做一次左旋,

这里写图片描述

然后对k3做右旋:

这里写图片描述

AVLTree DoubleRotateWithLeft( PAVLNode k3 )

{

            /* Rotate between K1 and K2 */

            k3->l = SingleRotateWithLeft( k3->l );

            /* Rotate between K3 and K2 */

            return SingleRotateWithRight(k3);

}

情况4(RL):对该结点的右儿子的左子树进行了一次插入。(先右旋再左旋)

这里写图片描述

做法类似,先对k1的右子树进行一次右旋,然后再对k1进行一次左旋。

AVLTree DoubleRotateWithRight( PAVLNode k1 )

{

            /* Rotate between K3 and K2 */

            k1->r = SingleRotateWithRight( k1->r );

            /* Rotate between K1 and K2 */

            return SingleRotateWithLeft( k1 );

}

AVL树插入操作完整代码:

 AVLTree Insert(Item X, AVLTree T )

 {

            if( T == NULL )

            {

                /* Create and return a one-node tree */

                T = (PAVLNode)malloc( sizeof(AVLNode ) );

                if( T == NULL )

                    perror("malloc failed");

                else

                {

                    T->item = X; 

                    T->h = 0;

                    T->l = T->r = NULL;

                    T->count = 1;

                }

            }

            else if(compare(&X,&T->item) == -1)//插入情况1

            {

                T->l = Insert( X, T->l );

                if( Height( T->l ) - Height( T->r ) == 2 )

                    if(compare(&X, &T->l->item ) == -1)//左边左子树 单旋转 

                        T = SingleRotateWithLeft( T );

                    else

                        T = DoubleRotateWithLeft( T );//左边右子树 

            }

            else if( compare(&X,&T->item) == 1 ) //插入情况2

            {

                T->r = Insert( X, T->r );

                if( Height( T->r ) - Height( T->l ) == 2 )

                    if(compare(&X , &T->r->item) == 1)//右边右子树 单旋转

                        T = SingleRotateWithRight( T );

                    else

                        T = DoubleRotateWithRight( T );//右边左子树 

            }

            else//插入情况3

             T->count++;

            /* Else X is in the tree already; we'll do nothing */

            T->h = MAX( Height( T->l ), Height( T->r ) ) + 1;

            return T;

}


作业题

10.1 地鼠安家1

这里写图片描述
这里写图片描述

(这道题判二叉搜索树,本来的做法是在读入的时候就判一下a < b < c然后就没有了,但其实会出现比如图中的3和4结点交换一下的情况下,这种判法就是错误的。一种正确做法是,开两个数组min[i],max[i]来存以i为根节点的子树中的最小和最大结点(递归得到),下面采用另一种比较好的做法是,满足二叉搜索树则必有中序遍历是递增序列的条件。然后广搜得到各结点层数。)

#include<cstdio>
#include<iostream>
#include<queue>
using namespace std;
#define MAX (10000+7)
int n,i,j;

struct Node
{
    int left,right,fa;
}node[MAX];

int d[MAX];
int inorder[MAX];
int root = 0;
int tot = 0;
void check(int r)
{
    if(node[r].left != -1) check(node[r].left);
    inorder[++tot] = r;
    if(node[r].right != -1) check(node[r].right);
}

int deep()
{
    int Max = 1;
    queue<int> que;
    que.push(root);
    d[root] = 1;
    while(!que.empty())
    {
        int f = que.front();
        que.pop();
        if(node[f].left != -1)
        {
            d[node[f].left] = d[f]+1;
            que.push(node[f].left);
            if(Max < d[node[f].left]) Max = d[node[f].left];
        }
        if(node[f].right != -1)
        {
            d[node[f].right] = d[f]+1;
            que.push(node[f].right);
            if(Max < d[node[f].right]) Max = d[node[f].right];
        }
    }
    return Max;
}

int main()
{
    scanf("%d",&n);
    int a,b,c;
    for(i = 0; i < n; i++)
    {
        scanf("%d%d%d",&a,&b,&c);
        node[b].left = a;
        node[b].right = c;
        node[a].fa = b;
        node[c].fa = b;
    }

    for(i = 1; i <= n; i++)
        if(node[i].fa == 0) root = i;

    check(root);

    for(i = 1; i < n; i++)
    {
        if(inorder[i] >= inorder[i+1])//不是二叉搜索树
        {
            printf("-1\n");
            return 0;
        }
    }

    printf("%d\n",deep());

    return 0;
}

10.4 魔法少女小风之相亲大会

这里写图片描述
这里写图片描述

(n的范围10w,二叉搜索树如果退化成链的情况下O(n^2)会T一半。若是离线的做法可以先把所有结点拿出来建一棵二叉搜索树,然后再从头做这些操作,开个标志数组,遇到插入则树中的这个结点标志为1,删除则标志为0,这样复杂度O(nlogn)。如果是在线做法得用AVL树..下面是一个扔了AVL树模板AC的代码..)

#include<iostream>  
#include<cstdio>  
#include<cstdlib>  
using namespace std;  
struct data  
{  
    int l,r,v,size,rnd,w;  
} tr[111113];  
int n,size,root,ans;  
void update(int k)//更新结点信息  
{  
    tr[k].size=tr[tr[k].l].size+tr[tr[k].r].size+tr[k].w;  
}  
void rturn(int &k)  
{  
    int t=tr[k].l;  
    tr[k].l=tr[t].r;  
    tr[t].r=k;  
    tr[t].size=tr[k].size;  
    update(k);  
    k=t;  
}  
void lturn(int &k)  
{  
    int t=tr[k].r;  
    tr[k].r=tr[t].l;  
    tr[t].l=k;  
    tr[t].size=tr[k].size;  
    update(k);  
    k=t;  
}  
void insert(int &k,int x)  
{  
    if(k==0)  
    {  
        size++;  
        k=size;  
        tr[k].size=tr[k].w=1;  
        tr[k].v=x;  
        tr[k].rnd=rand();  
        return;  
    }  
    tr[k].size++;  
    if(tr[k].v==x)tr[k].w++;//每个结点顺便记录下与该节点相同值的数的个数  
    else if(x>tr[k].v)  
    {  
        insert(tr[k].r,x);  
        if(tr[tr[k].r].rnd<tr[k].rnd)lturn(k);//维护堆性质  
    }  
    else  
    {  
        insert(tr[k].l,x);  
        if(tr[tr[k].l].rnd<tr[k].rnd)rturn(k);  
    }  
}  
void del(int &k,int x)  
{  
    if(k==0)return;  
    if(tr[k].v==x)  
    {  
        if(tr[k].w>1)  
        {  
            tr[k].w--;  
            tr[k].size--;  
            return;//若不止相同值的个数有多个,删去一个  
        }  
        if(tr[k].l*tr[k].r==0)k=tr[k].l+tr[k].r;//有一个儿子为空  
        else if(tr[tr[k].l].rnd<tr[tr[k].r].rnd)  
            rturn(k),del(k,x);  
        else lturn(k),del(k,x);  
    }  
    else if(x>tr[k].v)  
        tr[k].size--,del(tr[k].r,x);  
    else tr[k].size--,del(tr[k].l,x);  
}  
int query_rank(int k,int x)  
{  
    if(k==0)return 0;  
    if(tr[k].v==x)return tr[tr[k].l].size+1;  
    else if(x>tr[k].v)  
        return tr[tr[k].l].size+tr[k].w+query_rank(tr[k].r,x);  
    else return query_rank(tr[k].l,x);  
}  
int query_num(int k,int x)  
{  
    if(k==0)return 0;  
    if(x<=tr[tr[k].l].size)  
        return query_num(tr[k].l,x);  
    else if(x>tr[tr[k].l].size+tr[k].w)  
        return query_num(tr[k].r,x-tr[tr[k].l].size-tr[k].w);  
    else return tr[k].v;  
}  
void query_pro(int k,int x)  
{  
    if(k==0)return ;  
    if(tr[k].v<x)  
    {  
        ans=k;  
        query_pro(tr[k].r,x);  
    }  
    else query_pro(tr[k].l,x);  
}  
void query_sub(int k,int x)  
{  
    if(k==0)return;  
    if(tr[k].v>x)  
    {  
        ans=k;  
        query_sub(tr[k].l,x);  
    }  
    else query_sub(tr[k].r,x);  
}  


int main()  
{  

    char str[4];  
    int N,i,x;  
    scanf("%d",&N);  
    for (i = 0; i < N; ++i)  
    {  
        scanf("%s",str);  
        if(str[0] == 'J')//in  
        {  
            scanf("%d",&x);  
            insert(root,x);  
        }  
        else if(str[0] == 'X')//out  
        {  
            scanf("%d",&x);  
            del(root,x);  
        }  
        else if(str[0] == 'W')//output position  
        {             
            scanf("%d",&x);  
            printf("%d\n",query_num(root,x));  
        }  
        else printf("Single dog!\n");  
    }  
    return 0;  
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值