数据结构和算法

查找

Posted by JT on October 18, 2018

概要

查找分为静态查找和动态查找。

  • 静态查找:数据集合稳定,不需要添加删除元素
  • 动态查找:数据集合在查找过程中需要同时添加或删除元素

静态查找

顺序查找

按照顺序进行比对,相等就是查找结果,不相等就没有结果。

时间复杂度:O(n)。

插值查找(按比例查找)

在折半查找的基础上,按比率进行查找,适用于数据量大且分布均匀的数据,否则效率低于折半查找。

mid = low + ((key - a[low]) / (a[high] - a[low])) * (high - low);

时间复杂的:O(logn)。

斐波那契查找(黄金比例)

斐波那契数列(a(n) = a(n-1) + a(n-2)): 1,1,2,3,5,8,13,21,…

后二数之比2/3,3/5,5/8,8/13,13/21,… 无限接近黄金比例(0.618)。

将源数据的个数映射到斐波那契数列中,根据斐波那契数列的特性对源数据进行分割查找比较,即比例为0.618的插值查找。

时间复杂的:O(logn)。

线性索引查找

为数据建立已排序索引表,根据索引表查找数据。

稠密索引

建立和数据相同当量的索引表,适用于数据量不是很大的场景。

分块索引

倒排索引

二叉排序树(二叉查找树)

二叉排序树性质:

  • 左子树上所有结点的值均小于他的根结构的值
  • 右子树上所有结点的值均大于他的根结构的值
  • 左、右子树也分别为二叉排序树(递归)

二叉排序树查找

二叉排序树插入

二叉排序树删除

  • 如果该结点是叶子结点,直接删除
  • 如果该结点只有左子树或右子树,直接删除,然后接上
  • 如果既有左子树又有右子树,采用中序排序找到其前驱或后继替换,然后接上

平衡二叉排序树

平衡二叉排序树的特点:

  • 左子树和右子树都是平衡二叉树
  • 左子树和右子树的深度只差(平衡因子BF)的绝对值不超过1

插入,查找,删除的时间复杂度最好情况和最坏情况都维持在O(logN)。

演示一组数据怎么组成一棵AVL树。

int a[] = {4,3,2,7,9,11,10};

  • 插入4,如图:,平衡因子为0.
  • 插入3,如图:,4的平衡因子因为4的左子树增长了,1-0=1
  • 插入2,如图:,显然4的平衡因子大于1了,为了保持平衡那我们就这样做:让4节点的左孩子指向3的右子树(此时为NULL),让3的右孩子指向4,让树根指向3,如图,这种操作我们规定为右旋操作。
  • 插入7,如图:
  • 插入9,如图:,显然节点4不平衡了。那我们就把4的右孩子7的左子树(此时为NULL),让7的左孩子指向4,让3的右孩子指向7,如图:,我们规定此操作为左旋操作,此图是以4为根进行旋转。
  • 插入11,如图:,显然3节点,不平衡了,大家都应该知道以3为根进行左旋。让3的右孩子指向7的左子树(此时为4)。7的左孩子指向3,根指向7,如下图所示:
  • 插入10,如图:,显然节点9不平衡,且是右边高,那我们左旋吧,左旋后的效果是上图右图所示。显然这是不对的,10比11小,但在11的右孩子上。(根本原因是9和11的平衡因子符号不同)那我们在怎么办呢,看下图吧:,以11为根先右旋,,再以9为根左旋。

#define EH 0     // 等高
#define LH 1     // 左高
#define RH -1    // 右高

typedef struct _BitNode
{
    int data;
    int bf;   //平衡因子
    struct _BitNode *lchild, *rchild;
} BitNode, *BiTree;

void R_Rotate(BiTree *T)
{
    BiTree p;
    p=T->lchild;          // 假如此时T指向4,则p指向3;
    T->lchild=p->rchild;  // 把3的右子树挂接到4的左子树上(此例子3右子树为空)
    p->rchild=T;          // 让3的右孩子指向4.
    T=p;                  // 根指向节点3
}

void L_Rotate(BiTree *T)
{
    BiTree p;
    p=T->rchild;         //假如此时T指向4,则p指向7.
    T->rchild=p->lchild; //让7的左子树挂接到4的右子树上
    p->lchild=T;         //让7的左孩子指向4
    T=p;                 //树根指向7
}

void RightBalance(BiTree *T)
{
    BiTree R,rl;             //调用此函数时,以T为根的树,右边高于左边,则T->bf=RH。
    R=T->rchild;             //R是T的右孩子
    switch (R->bf)
    {
        case RH:             //如果T的右孩子和T他们的平衡因子符号相同时,则直接左旋,这是总结中的第2项
            T->bf=R->bf=EH;
            L_Rotate(T);
            break;
        case EH:
            T->bf=RH;
            R->bf=LH;
            L_Rotate(T);
            break;
        case LH:             //如果T的右孩子和T他们的平衡因子符合不同时,需要先以T的右孩子为根进行右旋,再以T为根左旋。
            //rl为T的右孩子的左孩子
            rl=R->lchild;    //2次旋转后,T的右孩子的左孩子为新的根 。注意:rl的右子树挂接到R的左子树上,rl的左子树挂接到T的右子树上
            switch (rl->bf)  //这个switch 是操作T和T的右孩子进行旋转后的平衡因子。
            {
                case EH:
                    T->bf=R->bf=EH;    //这些平衡因子操作,大家可以自己画图操作理解 下面的注解
                    break;
                    //2次旋转后,T的右孩子的左孩子为新的根 。
                    //并且rl的右子树挂接到R的左子树上,rl的左子树挂接到T的右子树上,rl为新根
                case RH:
                    R->bf=EH;
                    T->bf=LH;
                    break;
                    
                case LH:
                    R->bf=RH;
                    T->bf=EH;
                    break;
                default:
                    break;
            }
            rl->bf=EH;
            R_Rotate(T->rchild);   //先左旋,以T->rchild为根左旋。
            L_Rotate(T);           //右旋,以T为根, 左旋后 T是和rl想等,rl是新根
            break;
    }
}

void LeftBalance(BiTree *T)
{
    BiTree L,lr;
    L=T->lchild;
    switch (L->bf)
    {
        case EH:
            L->bf=RH;
            T->bf=LH;
            R_Rotate(T);
            break;
        case LH:
            L->bf=T->bf=EH;
            R_Rotate(T);
            break;
        case RH:
            lr=L->rchild;
            switch (lr->bf)
            {
                case EH:
                    L->bf=L->bf=EH;
                case RH:
                    T->bf=EH;
                    L->bf=LH;
                    break;
                case LH:
                    L->bf=EH;
                    T->bf=RH;
                    break;
                default:
                    break;
            }
            lr->bf=EH;
            L_Rotate(T->lchild);
            R_Rotate(T);
            break;
        default:
            break;
    }
}

bool InsertAVLtree(BiTree *T,int key,bool *taller)
{
    if(!T)               //此树为空
    {
        T=new BitNode;   //直接作为整棵树的根。
        T->bf=EH;
        T->lchild=T->rchild=NULL;
        T->data=key;
        taller=true;
        return true;
    }
    else
    {
        if(key==T->data)      //已有元素,不用插入了,返回false;
        {
            taller=false;
            return false;
        }
        if(key<T->data)      //所插元素小于此根的值,就找他的左孩子去比
        {
            if(!InsertAVLtree(T->lchild,key,taller))   //所插元素小于此根的值,就找他的左孩子去比
            {
                return false;
            }
            if(taller)    //taller为根,则树长高了,并且插入到了此根的左子树上。
            {
                switch (T->bf)            //此根的平衡因子
                {
                    case EH:              //原先是左右平衡,等高
                        T->bf=LH;         //由于插入到左子树上,导致左高=》》LH
                        taller=true;      //继续往上递归
                        break;
                    case LH:
                        LeftBalance(T);   //原先LH,由于插入到了左边,这T这个树,不平衡需要左平衡
                        taller=false;     //以平衡,设taller为false,往上递归就不用进入此语句了,
                        break;
                    case RH:
                        T->bf=EH;         //原先RH,由于插入到左边,导致此T平衡
                        taller=false;
                        break;
                    default:
                        break;
                }
            }
        }
        else
        {
            if(!InsertAVLtree(T->rchild,key,taller))
            {
                return false;
            }
            
            if(taller)
            {
                switch (T->bf)
                {
                    case EH:
                        T->bf=RH;
                        taller=true;
                        break;
                    case LH:
                        T->bf=EH;
                        taller=false;
                        break;
                    case RH:
                        RightBalance(T);
                        taller=false;
                        break;
                    default:
                        break;
                }
            }
        }
    }
}

//中序遍历输出
void InOrderReverse(BiTree *T)
{
    if(T)
    {
        
        InOrderReverse(T->lchild);
        cout<<T->data<<endl;
        InOrderReverse(T->rchild);
    }
}

多路查找树

特点:每个节点可以有多个孩子,且可以存储多个元素,所有元素存在特定关系

2-3树

每个节点有2个或3个孩子。

2-3-4树

同2-3树

B树

一种平衡的多路查找树,2-3树和2-3-4树都是B树的特例。

我们把结点最大的孩子书的数目称为B树的阶,2-3树是3阶的B树,2-3-4树是4阶的B树。

散列表(哈希表)查找

在存储和查找的时候,通关关键字和散列函数计算出查找数据的位置,适用于一对一的映射关系数据。

构造散列函数

  • 计算简单
  • 分布均匀

直接定值法

采用某个线性函数值作为散列地址:f(key) = a * key + b

数字分析法

数字分析法通常适合处理关键字位数比较大的情况,比如手机号,我们可以抽取后四位作为散列地址。

平方取中法

将关键字平方之后去中间若干位数字作为三列地址,适用于不清楚关键字的分布,且关键字不大的情况。

折叠法

将关键字从左到右分割成位数相等的几部分,然后将这几部分叠加求和,并按散列表表长取后几位作为散列地址,适用于不清楚关键字分布,且关键字比较大的情况。

除留余数法

此方法为最常用的构造散列函数的方法,对于散列表长为m的散列函数计算公式为:f(key) = key mod p(p <= m)

随机数法

选择一个随机数,取关键字的随机函数值为他的散列地址,f(key) = random(key),适用于关键字不等的情况。

处理散列冲突

当key1 != key2,f(key1) = f(key2),则为冲突。

开放定址法

一旦发生冲突,就去寻找下一个空的散列地址,只要散列表足够大,空的散列地址总能找到,并将记录存入其中。

公式1:fi(key) = (f(key) + di) MOD m (di=1,2,…m-1)

例如:采用除留取余法过程中产生了冲突,我们就采用上边的方法,算出当前key值得散列地址。

可以修改di的值,例如使用平方运算来尽量解决堆积的问题。

公式2:fi(key) = (f(key) + di) MOD m (di=\(1^2\),-\(1^2\),\(2^2\),-\(2^2\),…\(q^2\),-\(q^2\),q<=m/2)

再散列函数法

采用多个散列函数

链地址法

将产生冲突的数据用链表的形式存储。

公共溢出区法

现在基本表进行查找,再到溢出表查找,凡是冲突的数据都会放到溢出表中。