• 关于快速查找与匹配


    寒假集训总结(一):关于快速匹配

    寒假进行了数日的集训,感觉收获颇丰,虽然中间由于生病耽误了几天的训练,但后期又跟进进行了补充,仍然获得了许多宝贵的经验以及认识到了自身的不足,这一次先进行一种常见题型的总结,并列出两道典型例题,给出一些个人见解,不保证为最优解法,但至少为AC代码,如有不足,还望指出。

    例题一:7-14 QQ帐户的申请与登陆(25 分)

    7-15 QQ帐户的申请与登陆(25 分)
    实现QQ新帐户申请和老帐户登陆的简化版功能。最大挑战是:据说现在的QQ号码已经有10位数了。

    输入格式:

    输入首先给出一个正整数N(≤10
    ​5
    ​​ ),随后给出N行指令。每行指令的格式为:“命令符(空格)QQ号码(空格)密码”。其中命令符为“N”(代表New)时表示要新申请一个QQ号,后面是新帐户的号码和密码;命令符为“L”(代表Login)时表示是老帐户登陆,后面是登陆信息。QQ号码为一个不超过10位、但大于1000(据说QQ老总的号码是1001)的整数。密码为不小于6位、不超过16位、且不包含空格的字符串。

    输出格式:

    针对每条指令,给出相应的信息:
    
    1)若新申请帐户成功,则输出“New: OK”;
    2)若新申请的号码已经存在,则输出“ERROR: Exist”;
    3)若老帐户登陆成功,则输出“Login: OK”;
    4)若老帐户QQ号码不存在,则输出“ERROR: Not Exist”;
    5)若老帐户密码错误,则输出“ERROR: Wrong PW”。
    

    输入样例:

    5
    L 1234567890 myQQ@qq.com
    N 1234567890 myQQ@qq.com
    N 1234567890 myQQ@qq.com
    L 1234567890 myQQ@qq
    L 1234567890 myQQ@qq.com
    输出样例:
    
    ERROR: Not Exist
    New: OK
    ERROR: Exist
    ERROR: Wrong PW
    Login: OK
    

    原题地址:https://pintia.cn/problem-sets/15/problems/723

    一道典型的数据结构题,主要考察快速查找以及重复的判断,难度一般,此题个人有两种解法,第一种是建立哈希表,可以用C语言实现。笔者通过阅读他人的解法受到了启发,给出原博地址,https://www.cnblogs.com/joeylee97/p/6628506.html;

    第二种是利用C++STL中的map,较为简便,下面先给出C语言哈希表的实现:

    #include<stdio.h>
    #include<stdlib.h>
    #include<string.h>
    typedef long long Datatype_account;         //由于账户可能大于int型的最大值,因此定义为LL型 
    typedef char Datatype_password;             //定义char型变量来标识密码 
    typedef struct List List;                   //声明链表的节点类型 
    typedef struct Hashlist Hashlist;           //声明哈希表的节点类型 
    struct List                                 //定义链表 
    {
        Datatype_account id;
        Datatype_password pa[20];
        List *next;
    };
    struct Hashlist                              //定义哈希表 
    {
        int size;
        List *table;
    };
    Hashlist *creat(int n);                      //定义一个创建哈希表的函数,返回值为哈希表的首地址 
    int nextprime(int n);                        //采用除留余数法,因此需要找到大于总数据规模的最小素数 
    int Hash(Hashlist *H,Datatype_account key);  //定义散列函数 
    List *find(Hashlist *H,Datatype_account key);//定义查找函数 
    void Login(Hashlist *H,Datatype_account key,Datatype_password *p);//定义登录函数 
    void Apply(Hashlist *H,Datatype_account key,Datatype_password *p);//定义申请函数 
    Hashlist *creat(int n)
    {
        Hashlist *H = (Hashlist*)malloc(sizeof(Hashlist));  //首先为散列表首地址分配空间 
        H -> size = nextprime(n);                           //利用除留余数法,得到散列表的大小 
        int i = 0;
        H -> table = (List*)malloc(H -> size * sizeof(List));//为散列表分配空间 
        for(i = 0;i < H -> size;i++)                         //散列表的初始化 
        {
    	    H -> table[i].next = NULL;
    	    H -> table[i].id = 0;
    	    H -> table[i].pa[0] = '';
        }
        return H;
    }
    int nextprime(int n)                                     //寻找素数 
    {
        int i,flag;
        while(1)
        {
    	    flag = 1;
    	    for(i = 2;i < n;i++)
    	    {
    		    if(n % i == 0)
    		    {
    			    flag = 0;
    		    }
    	    }
    	    if(flag)
    	    {
    		    break;
    	    }
    	    else
    	    {
    		    n++;
    	    }
        }
        return n;
    }
    int Hash(Hashlist *H,Datatype_account key) 
    {
        long long index = key % H -> size;            //直接使用除留余数法得到散列函数对应的函数值 
        return index;
    }
    List *find(Hashlist *H,Datatype_account key) 
    {
        long long index = Hash(H,key);                //得到键值对应的函数值 
        List *p = H -> table[index].next;             //将函数值映射到散列表中 
        while(p && key != p -> id)                    //若该节点存在且键值不等于列表中的值,则继续向下查找 
        {
    	    p = p -> next;
        }
        return p;                                     //返回该节点 
    }
    void Login(Hashlist *H,Datatype_account key,Datatype_password *p)
    {
        List *f = find(H,key);                 //定义一个List型节点并按键值查找 
        if(f && !strcmp(f -> pa,p))            //若该节点存在且密码相符则输出登录成功 
        {
    	    printf("Login: OK");
        }
        else if(f && strcmp(f -> pa,p))        //若节点存在但密码不符 
        {
    	    printf("ERROR: Wrong PW");
        }
        else if(!f)                            //若节点不存在 
        {
    	    printf("ERROR: Not Exist");
        }
    }
    void Apply(Hashlist *H,Datatype_account key,Datatype_password *p)
    {
        List *f = find(H,key);                 //仍然是先进行查找 
        if(f)                                  //若节点已存在,则提示错误信息 
        {
    	    printf("ERROR: Exist");
        }
        else                                   //若不存在,则建立节点,加入到散列中 
        {
    	    f = (List*)malloc(sizeof(List));
    	    long long index = Hash(H,key);
    	    f -> next = H -> table[index].next;
    	    H -> table[index].next = f;
    	    f -> id = key;
            strcpy(f -> pa,p);
            printf("New: OK");
        }
    }
    int main(void)
    {
        int n = 0;
        Datatype_account acc;
        Datatype_password pa[20];
        char choose = '';
        scanf("%d",&n);
        Hashlist *H = creat(n);                 //预先分配空间 
        while(n--)
        {
            getchar();
    	    scanf("%c%lld%s",&choose,&acc,&pa);
    	    if(choose == 'L')
    	    {
    		    Login(H,acc,pa);
    	    }
        	else
    	    {
    		    Apply(H,acc,pa);
    	    }
    	    printf("
    ");
        }
        return 0;
    }
    

    下面是C++STl map的实现;

    #include<bits/stdc++.h>
    using namespace std;
    map<string,string>q;                  //构造一个map类,为了简单处理,笔者将容器内的存储对象定义为两个string类 
    void login(string a,string p);        //定义一个登陆函数 
    void apply(string a,string p);        //定义一个申请函数 
    void login(string a,string p)
    {
        map<string,string> :: iterator s;//定义一个对应两个string类对象的map迭代器 
        s = q.find(a);                   //对map进行查找 
        if(s != q.end() && p == s -> second)  //若找到且密码相等,提示登陆成功 
        {
    	    printf("Login: OK");
        }
        else if(s != q.end() && p != s -> second)  //若存在但密码不等,提示失败 
        {
    	    printf("ERROR: Wrong PW");
        }
        else if(s == q.end())                      //若查找失败,则提示不存在 
        {
    	    printf("ERROR: Not Exist");
        }
    }
    void apply(string a,string p)
    {
        map<string,string> :: iterator s;
        s = q.find(a);
        if(s != q.end())                           //若查找成功,则提示账户已存在 
        {
    	    printf("ERROR: Exist");
        }
        else                                       //若查找失败,则将该账户作为新元素插入map中 
        {
    	    q.insert(pair<string,string>(a,p));
    	    printf("New: OK");
        }
    }
    int main(void)
    {
        int i = 0,n = 0;
        string account,password,temp,temp2,choose;
        scanf("%d",&n);
        for(i = 0;i < n;i++)
        {
    	    cin >> choose;
    	    cin >> temp;
    	    cin >> temp2;
    	    if(choose == "L")
    	    {
    		    login(temp,temp2);
    	    }
    	    else
    	    {
    		    apply(temp,temp2);
    	    }
    	    if(i != n - 1)
    	    {
    		    printf("
    ");
    	    }
        }
        return 0;
    }
    

    例题二:7-16 航空公司VIP客户查询(25 分)

    不少航空公司都会提供优惠的会员服务,当某顾客飞行里程累积达到一定数量后,可以使用里程积分直接兑换奖励机票或奖励升舱等服务。现给定某航空公司全体会员的飞行记录,要求实现根据身份证号码快速查询会员里程积分的功能。
    输入格式:

    输入首先给出两个正整数N(≤10
    ​5
    ​​ )和K(≤500)。其中K是最低里程,即为照顾乘坐短程航班的会员,航空公司还会将航程低于K公里的航班也按K公里累积。随后N行,每行给出一条飞行记录。飞行记录的输入格式为:18位身份证号码(空格)飞行里程。其中身份证号码由17位数字加最后一位校验码组成,校验码的取值范围为0~9和x共11个符号;飞行里程单位为公里,是(0, 15 000]区间内的整数。然后给出一个正整数M(≤10
    ​5
    ​​ ),随后给出M行查询人的身份证号码。
    输出格式:

    对每个查询人,给出其当前的里程累积值。如果该人不是会员,则输出No Info。每个查询结果占一行。
    输入样例:

    4 500
    330106199010080419 499
    110108198403100012 15000
    120104195510156021 800
    330106199010080419 1
    4
    120104195510156021
    110108198403100012
    330106199010080419
    33010619901008041x
    输出样例:
    
    800
    15000
    1000
    No Info
    

    原题链接: https://pintia.cn/problem-sets/959995131537092608/problems/959995183282221063

    同样是一道考察快速查找匹配的题目,笔者开始仍然是使用哈希表的方法做,单较为繁琐,后来改为利用Set容器,但有一些小问题,之后再讨论,至于使用哈希表的做法,参照上题,稍作修改以及对散列函数重新分析,可以得到相同的效果,有兴趣可以自行尝试。

    二:对于该类题型的分析:

    我们以7-16为例,可以稍微进行一些散列表的分析,对于查找与匹配等问题,我们需要保证两点,其一是其高效性,其二是其准确性,对于一般的问题,我们往往会采取开数组的方法来处理,而我们又知道,最快捷的访问方法是利用数组的随机访问方式,但可惜的是,我们对于绝大多数数据,无法使其得到单一的与数组下标对应的值,例如7-16,身份证号为18位,若其为纯数字的话,或许有些高级语言原生支持大数存储,那么数组呢,开10^18次方的吗,显然不现实,何况校验码还为字母,这时我们就想在保证其准确性与效率的前提下,使其得到近似于随机访问的O(1)的效率,这时我们就需要使用哈希算法,将其关键字进行离散化,得到固定长度的函数值,并映射到对应的位置上,从而得到较高的效率。举个简单的例子,假设我们要在全校范围内寻找到高为一米八的人,我们可以采取如下方式,带着全校的人名单,一一查找(顺序查找);第二,使全校同学按身高排好队(队头最高),并从队伍中间开始查找,若该同学高于一米八,则向队伍后面查找,若低于一米八,则向 队伍前面查找(二分查找),三,我们预先知道了一米八以上的人占全校人数的三分之一,那么我们直接走向队伍距离队首三分之一处,开始查找(插值搜索),或许这次我找到了一米八的人,但是下次我要找一米六的人呢,再重新排好队(排序),再进行查找吗,显然太费时间了,(何况你的学生们还会有意见),这时我们不妨再开学时来一次统计,例如,一米八到两米的同学的名字放在一张单子上,一米六到一米八的同学的名字放在一个单子上,以此类推,那么下次查找时,我直接拿来对应身高的单子,去查找即可,大大节约了时间。(例子不太恰当,实际上入学体检时已经记录好了,何况学生是会说话的),那么我们将数据进行散列处理的目的就在此,将一系列数据按照预先设定好的函数进行处理,得到固定长度的函数值,(按身高区间放入名单),那么我下次进行查找时,直接将关键字的值放入散列函数处理得到对应的值,并在该区间内查找(在名单内查找),这样可以将搜索范围大大缩小。

    但是随之而来的是第二个问题:冲突

    即关键字的值集中在某一较小区间内,(依据散列函数而定),有可能使不同的键值得到相同的哈希值(例如体校,可能百分之九十的人都身高一米八),这时候会导致查找时可能找到不正确的元素,例如7-16,我们从实际入手,身份证号的意义为1,2位为省,自治区直辖市代码,3,4位为地级市,5,6位为县级代码,714位为出生年月日,1517位为顺序号,最后一位为校验码,那么我们可以得出如下结论(数学证明部分略去不计,数字分析法),可以发现若人员集中在某一地区,或出生年月为某一时间段内,会导致1~14位的数字分布不均匀,较容易产生冲突,这时我们就容易联想到最后几位顺序码,该字段的冲突概率最低,那么我们不妨就取身份证号的最后数位作为键值,并进行散列处理,当然这只是其中一种方式,也是最简单的方式,你仍可以在身份证号中任取数位作为键值(前提是不相邻的数位),依实际情况而定。

    实际上在散列中,冲突是大概率事件,那么较为直接有效的方式就是拉链法,即我们得到相同函数值时,不直接将其存储在对应地址,而是在该地址的基础上,向下延伸(用链式存储),这样我们就解决了不同元素抢占地址的问题,在查找时,我们先得到该地址(缩小范围),再向下查找,直到得到要查找的值(或未找到),当然这种方式也有弊端,即堆积问题,若大多数数据的函数值都集中在该地址上,则其查找效率有可能退化为顺序查找的效率,这时我们可以考虑使用哈希树,在此就不过多展开,拉链法对于这两道例题已经足够。

    对于散列的讨论就先到这里,其他的一些常用的查找方式刚才也提到过,例如二分查找,插值搜索等,这些方法会在有较好的例题时拿来分析,关于方法的选择,并没有最优,只有相对最优(主要还是依据数据规模以及类型而定),这次先到这里,若有问题,还望指正。

  • 相关阅读:
    element-ui 设置input的只读或禁用
    vue 获取后端数据打印结果undefined问题
    用yaml来编写配置文件
    [LeetCode] 28. 实现strStr()
    [LeetCode] 25. k个一组翻转链表
    [LeetCode] 26. 删除排序数组中的重复项
    [LeetCode] 24. 两两交换链表中的节点
    [LeetCode] 23. 合并K个排序链表
    [LeetCode] 21. 合并两个有序链表
    [LeetCode] 22. 括号生成
  • 原文地址:https://www.cnblogs.com/Reloaded/p/8455811.html
Copyright © 2020-2023  润新知