【查找】算法笔记 & 代码

相关概念

  • 查找表
    • 静态查找表:只有查找操作
    • 动态查找表:查找、插入 / 删除
  • 关键字 / 关键码 → 主关键字、次关键字

顺序查找

  • 在查找方向的尽头放置“哨兵”:避免了在查找过程中每一次比较后都要判断查找位置是否越界
/* 《大话数据结构》 有哨兵顺序查找 */
int Sequential_Search2(int *a, int n, int key) {
    int i;
    a[0] = key;           /* 设置a[0]为关键字值,我们称之为“哨兵” */
    i = n;                /* 循环从数组尾部开始 */
    while (a[i] != key){
        i--;
    }
    return i;             /* 返回0则说明查找失败 */
}

性能

  • 平均查找次数:\frac{n+1}{2}
  • 时间复杂度:O(n)

n很大时,查找效率极为低下;

个算法非常简单,对静态查找表的记录没有任何要求,适合小型数据

有序表查找

mid 选择不同

折半查找 / 二分查找

前提:线性表必须采用顺序存储,关键码有序

在有序表中,取中间记录作为比较对象,若给定值与中间记录的关键字相等,则查找成功;若给定值小于中间记录的关键字,则在中间记录的左半区继续查找;若给定值大于中间记录的关键字,则在中间记录的右半区继续查找。

  • mid = \frac{low + high}{2} = low+\frac{high - low}{2}

性能

  • 时间复杂度:O(logn)

不适合频繁插入删除

插值查找

待查找关键字 key 与最大、最小关键字比较

  • mid= low + \frac{key - a[low]}{a[high]-a[low]} (high-low)

性能

  • 时间复杂度:O(logn)

 适合表比较长,关键字分布均匀的查找表

斐波那契查找

  • 斐波那契数列:F={0,1,1,2,3,5,8,13,21,......}

在斐波那契数列找一个等于略大于查找表中元素个数的数 F[n],将原查找表扩展为长度为F[n]-1(不足则补a[n])。然后通过斐波那契分割,将 F[n]-1 个元素分割为前半部分 F[n-1]-1 个元素,后半部分 F[n-2]-1 个元素,和中间 F[n-1] 位置的元素

  • mid=low+F[k-1] -1
    key < a[mid],k - 1;k > a[mid],k - 2

性能

  • 时间复杂度:O(logn)

 只涉及加减法运算,不涉及乘除运算

代码

// 《大话数据结构》
int Fibonacci_Search(int *a, int n, int key)
{
    int low, high, mid, i, k;
    low = 1;              /*定义最低下标为记录首位 */
    high = n;             /*定义最高下标为记录末位 */
    k = 0;
    while (n > F[k] - 1)  /* 计算n位于斐波那契数列的位置 */
        k++;
    for (i = n; i < F[k] - 1; i++)   /* 将不满的数值补全 */
        a[i] = a[n];
    while (low <= high)
    {
        mid = low + F[k - 1] - 1;
        if (key < a[mid])
        {
            high = mid - 1;         /* 最高下标调整到分隔下标mid-1处 */
            k = k - 1;              /* 斐波那契数列下标减一位 */
        }
        else if (key > a[mid])
        {
            low = mid + 1;
            k = k - 2;
        }
        else
        {
            if (mid <= n)
                return mid;
            else
                return n;           /* 若mid>n说明是补全数值,返回n */
        }
    }
    return 0;
}

// 生成斐波那契数列
int[] F = new int[n];
F[0] = 0;
F[1] = 1;
for (int i = 2; i < n; i++) 
    F[i] = F[i - 1] + F[i - 2];

线性索引查找

按先后顺序存储的数据(回帖数据、服务器日志数据……)

  • 索引:把一个关键字与它对应的记录相关联。

        一个索引由若干个索引项构成,每个索引项至少应包含关键字和其对应的记录在存储器中的位置等信息

  • 按结构:线性索引、树形索引和多级索引

稠密索引

一个记录对应一个索引项;索引项按照关键码有序排列

可以使用折半、插值、斐波那契等有序查找算法

分块索引

分块,每块对应一个索引项;块内无序,块间有序

分块索引表

  • 最大关键码:每一块最大关键字
  • 记录个数
  • 首元素指针

性能

  • 平均查找次数:\frac{m+1}{2}+\frac{t+1}{2}=\frac{1}{2}(\frac{n}{t}+t)+1,n 个记录分成 m 块,每块有 t 条
  • 时间复杂度:大于O(logn),小于O(n)

倒排索引

索引表中的每一项都包括一个属性值和具有该属性值的各记录的地址。由于不是由记录来确定属性值,而是由属性值来确定记录的位置,因而称为倒排索引。

索引表:

  • 次关键码
  • 记录号表:存储具有相同次关键字的所有记录的记录号

二叉排序树 / 二叉查找树

或者是一棵空树,或者具有以下性质:

  • 若它的左子树不空,则左子树上所有结点的值均小于它的根结构的值;
  • 若它的右子树不空,则右子树上所有结点的值均大于它的根结点的值;
  • 它的左、右子树也分别为二叉排序树。

构造一棵二叉排序树的目的,其实并不是为了排序,而是为了提高查找和插入删除关键字的速度

  • 查找、插入
  • 删除
    • 叶子结点
    • 仅有左或右子树的结点
    • 左、右子树都有的结点:找到需要删除的结点p的直接前驱(或直接后继)s,用s来替换结点p,然后再删除此结点s
typedef struct BiTNode{
    int data;       // 数据
    struct BiTNode *lchild, *rchild;
} BiTNode, *BiTree;

// 删除
Status DeleteBST(BiTree *T, int key)
{
    if (!*T)
        return FALSE;
    else
    {
        if (key == (*T)->data)
            return Delete(T);
        else if (key < (*T)->data)
            return DeleteBST(&(*T)->lchild, key);
        else
            return DeleteBST(&(*T)->rchild, key);
    }
}

// 从二叉排序树中删除结点p,并重接它的左或右子树
Status Delete(BiTree *p)
{
    BiTree q, s;
    if ((*p)->rchild == NULL)
    {
        q = *p;
        *p = (*p)->lchild;
        free(q);
    }
    else if ((*p)->lchild == NULL)
    {
        q = *p;
        *p = (*p)->rchild;
        free(q);
    }
    else
    {
        q = *p; s = (*p)->lchild;
        while (s->rchild)
        {
            q = s; s = s->rchild;
        }
        (*p)->data = s->data;    // s指向被删结点的直接前驱 
        if (q != *p)
            q->rchild = s->lchild;
        else
            q->lchild = s->lchild;
        free(s);
    }
    return TRUE;
}

性能

查找性能取决于二叉排序树的形状

平衡二叉树(AVL树)

  • 高度平衡的 二叉排序树

或者为空树,或者满足:

  • 平衡因子BF:二叉树上结点左子树深度减去右子树深度的值为-1、0、1

构建

在构建二叉排序树的过程中,每当插入一个结点时,先检查是否因插入而破坏了树的平衡性,若是,则找出最小不平衡子树。在保持二叉排序树特性的前提下,调整最小不平衡子树中各结点之间的链接关系,进行相应的旋转,使之成为新的平衡子树

  • 最小不平衡子树:以距离插入结点最近的,且平衡因子的绝对值大于1的结点为根的子树

旋转

  • 最小不平衡子树的根结点与它的子结点符号相同时,计算根结点BF:

        BF > 1,最小不平衡子树右旋(顺时针旋转);

        BF < -1,最小不平衡子树左旋(逆时针旋转);

  • 不同时:先将最小不平衡子树符号统一

性能

  • 时间复杂度:
    • 查找、插入、删除:O(logn)

代码

// 《大话数据结构》
typedef struct BiTNode
{
    int data;
    int bf;
    struct BiTNode *lchild, *rchild;
} BiTNode, *BiTree;

右旋

当传入一个二叉排序树P,将它的左孩子结点定义为L,将L的右子树变成P的左子树,再将P改成L的右子树,最后将L替换P成为根结点

void R_Rotate(BiTree *P)
{
    BiTree L;
    L = (*P)->lchild;
    (*P)->lchild = L->rchild;    
    L->rchild = (*P);
    *P = L;
}

左旋

void L_Rotate(BiTree *P)
{
    BiTree R;
    R = (*P)->rchild;
    (*P)->rchild = R->lchild;
    R->lchild = (*P);
    *P = R;
}

左平衡旋转

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

void LeftBalance(BiTree *T)
{
    BiTree L,Lr;
    L = (*T)->lchild;
    switch (L->bf)     /* 检查T的左子树的平衡度,并作相应平衡处理 */
    {
    /* 新结点插入在T的左孩子的左子树上,要作单右旋处理 */
    case LH:
        (*T)->bf = L->bf = EH;
        R_Rotate(T);
        break;

    /* 新结点插入在T的左孩子的右子树上,要作双旋处理 */
    case RH:
        Lr = L->rchild;
        switch (Lr->bf)            /* 修改T及其左孩子的平衡因子 */
        {
            case LH: 
                (*T)->bf = RH;
                L->bf = EH;
                break;
            case EH: 
                (*T)->bf = L->bf = EH;
                break;
            case RH: 
                (*T)->bf = EH;
                L->bf = LH;
                break;
        }
        Lr->bf = EH;
        L_Rotate(&(*T)->lchild);       /* 对T的左子树作左旋平衡处理 */
        R_Rotate(T);                   /* 对T作右旋平衡处理 */
    }
}

右平衡旋转

多路查找树

B树

  • 阶:结点最大的孩子数目

m阶B树:

  • 如果根结点不是叶结点,则其至少有两棵子树

B+树

散列表查找(哈希表)

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值