数据结构第十一周笔记(5)—— 散列查找 (慕课浙大版本--XiaoYu)

129 阅读6分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第27天,点击查看活动详情

小白专场[陈越]:电话聊天狂人-C语言实现

小白-PM.1 题意理解与解法分析

所有电话号码统计一下,打电话或者接电话的总次数是最多的,那这个人就叫做电话聊天狂人

image-20220831124929950

解法1:-排序

第1步:读入最多2×10五次方个电话号码,每个号码存为长度为11的字符串 第2步:按字符串非递减顺序排序 第3步:扫描有序数组,累计同号码出现的次数,并且更新最大次数

优势:编程简单快捷

缺点:如果这是在现实应用场合,这些号码不是一次性出现给你的,而是每天没分每秒的不断的在进来,每时每刻我们都可能被停下来问这个聊天狂人是谁。那每次都需要做一个NlogN的排序,要做N次这种事情,所以整个算法的复杂度就变成了N2logN

所以这个算法不好拓展解决动态插入的问题

方法2:-直接映射

为什么是2x10的10次方个单位,因为每个电话有11位,而第一位都是1,把1排除掉

image-20220831130043024

优势:编程简单快捷,动态插入快

缺点:下标超过了unsigned long,因为我们平常用到的最长长度的是10位的

直接映射需要存储img个短整型数字,大约是多少字节空间?差不多40GB,image-20220831134221137

我们需要扫描多少个单位才能找到我们需要的那个电话狂人?

=>为了要找2x10的五次方个号码,必须要扫描2x10的10次方个单元,太浪费啦

解法3-带智商的散列

注意:散列表的头节点的个数一共比2N要略大一点

image-20220831134934368

小白-PM.2 程序框架搭建

int main()
{
    创建散列表;
    读入号码插入表中;
    扫描表输出狂人;
    return 0;   
}
int main()
{
    int N,i;
    ElementType Key;//声明这个为了配合我们HashTable里面声明的类型
    HashTable H;
    
    scanf("%d",&N);
    H = CreateTable(N*2);//创建一个散列表
    for(i=0;i<N;i++){
        scanf("%s",Key);Insert(H,Key);//每次读进来一个号码我把它存在key这个字符串里面,然后把它这个字符串插到表里面
        scanf("%s",Key);Insert(H,Key);//每次读入进来的是一对
    }
    ScanAndOutput(H);//ScanAndOutput是自己取的名字
    DestoryTable(H);//HashTable的空间要记得释放掉,写在DestoryTable函数里
    return 0;
}

创建表需要的流程

image-20220831144530212要调用插入函数的话肯定会用到其他两个函数,一个是散列函数Hash,另一个是要找到合适的位置把它插入,Find

ScanAndOutput要做的事情:image-20220831144828537

小白-PM.3 输出狂人

void ScanAndOutput( HashTable H)
{
    int i,MaxCnt = PCnt = 0;
    ElementType MinPhone;
    List Ptr;
    MinPhone[0] = '\0';//最小电话号码初始化为一个空的字符串
    for(i=0;i<H->TableSize;i++){//扫描链表
        Ptr = H->Heads[i].Next;
        while(Ptr){
            if(Ptr->Count >MaxCnt){//更新最大通话次数
                MaxCnt = Ptr->Count;
                strcpy(Minphone,Ptr->Data);//狂人的电话号码copy到我们将要输出的最小的电话号码里
                PCnt = 1;//最狂的只能有一个,初始化次数
            }
            else if(Ptr->Count == MaxCnt ){//狂人不止一个的情况,并列的狂人,最大通话次数一样
                PCnt ++;//狂人计数
                if(strcmp(Minphone,Ptr->Data)>0)//Ptr->Data>0意味着前面号码大后面号码小,那我们就要把最小号码复制到下方这个更新字符串里面去
                    strcpy(Minphone,Ptr->Data);//更新狂人的最小手机号码
            }
            Ptr = Ptr->Next;
        }
    }
    printf("%s %d",MinPhone,MaxCnt);//输出一个是最小的电话号码,另一个是记录这个最大的通话次数
    id(PCnt > 1) printf("%d",PCnt);//狂人如果大于1的话我还得把这个统计数据输出出来
    printf("\n");
}

小白-PM.4 模块的引用与裁剪

image-20220831151057159

#define KEYLENGTH 11//关键词字符串的最大长度
//关键词类型用字符
typedef char ElementType[KEYLENGTH+1];//长度要加1是因为在C语言中,里面字符串的结尾还占了一个位置
typedef int Index;//散列地址类型//分离链接法的部分
typedef struct LNode *PtrToLNode;
struct LNode{
    ElementType Data;//电话号码
    PtrToLNode Next;//指向下一个节点的指针
    int Count;//计数器,我们定义为一个整型的Count
};
typedef PtrToLNode Position;
typedef PtrToLNode List;
​
typedef struct TblNode *HashTable;
struct TblNode{//散列表结点定义
    int TableSize;//表的最大长度
    List Heads;//指向链表头结点的数组
}

NextPrime模块

#define MAXTABLESIZE 1000000
int NextPrime(int N)
{/*返回大于N且不超过AXTABLESIZE的最小素数*/
    int i,p = ( N % 2 ) ? N+2 : N+1;/*从大于N的下一个奇数开始*/
    while( p <= MAXTABLESIZE ){
        for( i=(int)sqrt(p);i>2;i--)
            if(!(p%i))break;//p不是素数
        if(i == 2) break;//for正常结束,说明p是素数
        else p += 2;//否则试探下一个奇数
    }
    return p;
}
​
HashTable CreateTable( int TableSize )
{
    HashTable H;
    int i;
    H = (HashTable)malloc(sizeof(struct TblNode));
    H->TableSize = NextPrime(TableSize);
    H->Heads = (List)malloc(H->TableSize*sizeof(struct LNode));
    for( i = 0;i < H->TableSize; i++){
        H->Heads[i].Data[0] = '\0';H->Heads[i].Next = NULL;
        H->Heads[i].Count = 0;//头节点定义为0,虽然不写也没关系,它本身是一个空节点。但写上是个好习惯,表示这个节点里所有的变量都有一个初始化的值,不要有个节点空着的不知道这是干嘛的
    }
    return H;
}
int Hash(int Key,int P)//Key是整数,当我们把它传给这个Hash函数去处理的时候我们应该已经把那电话号码后五位截取出来了并且把它变成一个整数了,而不是直接把11位电话号码传进来
{
    //除留余数法,散列函数
    return Key%P; 
}

原始的Find函数

#deine MAXD 5//参与散列映射计算的字符个数,从后往前数5位
Position Find( HashTable H,ElementType Key )
{
    Position P;
    Index Pos;
    //初始散列位置
    Pos = Hash(Key,H->TableSize );//在原始的这个是直接把Key传到Hash函数里面去算的
    //但在我们这个问题里就不能这么做,在Key传进去之前得先把它后五位截出来变成一个整数,变成
    Pos = Hash(atoi(Key+KEYLENGTH-MAXD),H->TableSize)//把数传给atoi转化为整数再把整数传给散列函数去计算
    
    P = H->Heads[Pos].Next;//从该链表的第一个结点开始
    //当未到表尾,并且Key未找到时
    while(P && strcmp(P->Data,Key))
        P = P->Next;
    
    return P;//此时P或者指向找到的结点,或者为NULL
}

插入函数

bool Insert(HashTable H,ElementType Key)
{
    Position P,NewCell;
    Index Pos;
    
    P = Find(H,Key);
    if(!P){//关键词未找到,可以插入
        NewCell = (Position)malloc(sizeof(struct LNode));
        strcpy(NewCell->Data,Key);
        //初始散列位置
        //Pos = Hash(Key,H->TableSize);这里进行改动
        NewCell->Count = 1;//当我们要插入新的电话号码的时候,这个新的电话号码节点的计数器要初始化为1
        Pos = Hash(atoi(Key+KEYLENGTH-MAXD),H->TableSize);
        //将NewCell插入为H->Heads[Pos]链表的第一个结点
        NewCell->Next = H->Heads[Pos].Next;
        H->Heads[Pos].Next = NewCell;
        return true;
    }
    else{//关键词已存在
        //printf("键值已存在");//表示我找到了这个电话号码,但在实际显示中我们不能显示这一行,需要修改一下
        P->Count++;//找到这个电话号码计数器要加1,表示我们多打了一次电话或多接了一次电话
        return false;
    }
}