zoukankan      html  css  js  c++  java
  • 关于快速查找与匹配

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

    寒假进行了数日的集训,感觉收获颇丰,虽然中间由于生病耽误了几天的训练,但后期又跟进进行了补充,仍然获得了许多宝贵的经验以及认识到了自身的不足,这一次先进行一种常见题型的总结,并列出两道典型例题,给出一些个人见解,不保证为最优解法,但至少为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位的数字分布不均匀,较容易产生冲突,这时我们就容易联想到最后几位顺序码,该字段的冲突概率最低,那么我们不妨就取身份证号的最后数位作为键值,并进行散列处理,当然这只是其中一种方式,也是最简单的方式,你仍可以在身份证号中任取数位作为键值(前提是不相邻的数位),依实际情况而定。

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

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

  • 相关阅读:
    改造vant日期选择
    css3元素垂直居中
    npm综合
    (转)网页加水印方法
    Mac下IDEA自带MAVEN插件的全局环境配置
    隐藏注册控件窗口
    High performance optimization and acceleration for randomWalk, deepwalk, node2vec (Python)
    How to add conda env into jupyter notebook installed by pip
    The Power of WordNet and How to Use It in Python
    背单词app测评,2018年
  • 原文地址:https://www.cnblogs.com/Reloaded/p/8455811.html
Copyright © 2011-2022 走看看