基于链表排序的通讯录整理

一、题目描述

【问题描述】

读取一组电话号码簿(由姓名和手机号码组成),将重复出现的项删除(姓名和电话号码都相同的项为重复项,只保留第一次出现的项),并对姓名相同手机号码不同的项进行如下整理:首次出现的项不作处理,第一次重复的姓名后面加英文下划线字符_和数字1,第二次重复的姓名后面加英文下划线字符_和数字2,依次类推。号码簿中姓名相同的项数最多不超过10个。最后对整理后的电话号码簿按照姓名进行从小到大排序,并输出排序后的电话号码簿。

【输入形式】

先从标准输入读取电话号码个数,然后分行输入姓名和电话号码,姓名由不超过20个英文小写字母组成,电话号码由11位数字字符组成,姓名和电话号码之间以一个空格分隔,输入的姓名和电话号码项不超过100个。

【输出形式】

按照姓名从小到大的顺序分行输出最终的排序结果,先输出姓名再输出电话号码,以一个空格分隔。

【样例输入】

15

liping 13512345678

zhaohong 13838929457

qiansan 13900223399

zhouhao 18578294857

anhai 13573948758

liping 13512345678

zhaohong 13588339922

liping 13833220099

boliang 15033778877

zhaohong 13838922222

tianyang 18987283746

sunnan 13599882764

zhaohong 13099228475

liushifeng 13874763899

caibiao 13923567890

【样例输出】

anhai 13573948758

boliang 15033778877

caibiao 13923567890

liping 13512345678

liping_1 13833220099

liushifeng 13874763899

qiansan 13900223399

sunnan 13599882764

tianyang 18987283746

zhaohong 13838929457

zhaohong_1 13588339922

zhaohong_2 13838922222

zhaohong_3 13099228475

zhouhao 18578294857

【样例说明】

输入了15个人名和电话号码。其中第一项和第六项完全相同,都是“liping 13512345678”,将第六项删除,第一项保留;

第八项和第一项人名相同,电话不同,则将第八项的人名整理为liping_1;同样,第二项、第七项、第十项、第十三项的人名都相同,将后面三项的人名分别整理为:zhaohong_1、zhaohong_2和zhaohong_3。

最后将整理后的电话簿按照姓名进行从小到大排序,分行输出排序结果。

二、算法选择

之前还想着用冒泡排序,然后交换两个节点的值就可以了,但是发现如果用冒泡的话就必须设计各个节点在链表中的顺序,这个就没法体现链表的优越性了,结合其它博文,发现归并排序是比较适合用来进行链表排序的,在这篇文章[LeetCode] 148. Sort List 链表排序的代码基础上进行了一点改动,改动后的sort和merge函数如下:

infoptr merge(infoptr l1,infoptr l2){
    if(!l1)
        return l2;
    if(!l2)
        return l1;
    if(strcmp(l1->name,l2->name)<=0){
        l1->next=merge(l1->next,l2);
        total_first=l1;
        return l1;
    }
    else{
        l2->next=merge(l1,l2->next);
        total_first=l2;
        return l2;
    }
}

infoptr sortlist(infoptr head){
    if(!head||!head->next)
        return head;
    infoptr slow=head,fast=head,pre=head;
    while (fast&&fast->next){
        pre=slow;
        slow=slow->next;
        fast=fast->next->next;
    }
    pre->next=NULL;
    if(slow!=head)
        return merge(sortlist(head),sortlist(slow));
    return head;//如果只有一个节点,那就不需要排序了
}

三、思路分析和问题记录

3.1 思路分析

应该比较清晰,就是先删除重复项,然后是链表排序,接着是对相同人名不同号码的项进行编号,或者先链表排序,然后再删除重复项,这样删除重复项时就可以直接比较相邻的两个节点了,不需要复杂的算法,也能达到O(n)的时间复杂度

3.2 问题记录

这道题遇到的问题有:
1)重复项的删除有问题,这个就是如果找到了重复并把它删掉后,作为游标的节点指针不需要往后移,因为在删掉重复项后,相当于后面的节点往前移了;如果没找到重复项,才需要往后移
2)排序后的头指针丢失,即不再指向链表第一个节点了,这个好办,就是用一个全局指针,在每次头节点更换后,让这个全局指针指向新的头节点就可以了
3)对于同名不同电话号码的节点编号出错。和重复项的删除那里类似,作为被比较的节点指针,同样不是每一次循环都需要更改的,而是在相邻节点的人名不同时才需要跟个这个指针的指向。
4)相同人名不同电话号码最终的输出顺序出错。这个出错的原因是归并算法的问题,解决方法是只需要保证在归并的时候左边的序列一直在左边就可以了,像下面绿框框出来的那两行:
在这里插入图片描述

四、完整代码

#include <stdio.h>
#include <malloc.h>
#include <string.h>
# define maxname 25
# define maxnumber 15
int num;
typedef struct info{
    char name[maxname];
    char number[maxnumber];
    struct info *next;
}INFO;

typedef INFO* infoptr;
infoptr total_first;
INFO *first=NULL,*p,*q,*iter1,*iter2;
char info_name[maxname],info_number[maxnumber];

infoptr merge(infoptr l1,infoptr l2){
    if(!l1)
        return l2;
    if(!l2)
        return l1;
    if(strcmp(l1->name,l2->name)<=0){
        l1->next=merge(l1->next,l2);
        total_first=l1;
        return l1;
    }
    else{
        l2->next=merge(l1,l2->next);
        total_first=l2;
        return l2;
    }
}

infoptr sortlist(infoptr head){
    if(!head||!head->next)
        return head;
    infoptr slow=head,fast=head,pre=head;
    while (fast&&fast->next){
        pre=slow;
        slow=slow->next;
        fast=fast->next->next;
    }
    pre->next=NULL;
    if(slow!=head)
        return merge(sortlist(head),sortlist(slow));
    return head;//如果只有一个节点,那就不需要排序了
}

int main(){
    scanf("%d",&num);
    int i;
    // 1.构造链表
    for(i=0;i<num;i++){
        scanf("%s %s",info_name,info_number);
        q=(INFO*)malloc(sizeof(INFO));
        strcpy(q->name,info_name);
        strcpy(q->number,info_number);
        q->next=NULL;
        if(first==NULL){
            first=p=q;
        }
        else{
            p->next=q;
            p=p->next;
        }
    }
    total_first=first;

    // 2.判断有没有重复
    for(iter1=first;iter1!=NULL;iter1=iter1->next){
        for(iter2=iter1;iter2->next!=NULL;){
            if(!strcmp(iter1->name,iter2->next->name)&&!strcmp(iter1->number,iter2->next->number)){
                p=iter2->next;
                iter2->next=iter2->next->next;
                free(p);
            }
            else
                iter2=iter2->next;
        }
    }


    // 3.排序
    sortlist(first);

    // 4.编号
    int dupli=0;
    char temp_name[maxname];
    temp_name[0]='_';
    temp_name[2]='\0';
    for(iter1=total_first,p=iter1;iter1!=NULL&&iter1->next!=NULL;iter1=iter1->next){
        if(!strcmp(p->name,iter1->next->name)){
            dupli++;
            char char_dupli=dupli-0+'0';
            temp_name[1]=char_dupli;
            strcat(iter1->next->name,temp_name);
        }
        else{
            dupli=0;
            p=iter1->next;
        }
    }

    for(p=total_first;p!=NULL;p=p->next){
        printf("%s %s\n",p->name,p->number);
    }
}

五、排序算法几个易错点总结

1.看下面绿色框框,我的想法是把l1的头节点拿出来,然后用l1的剩余部分继续和l2排序,但是我平时容易写成l1=merge(l1->next,l2),就漏了绿框框里的->next,为什么会出现这种错误呢?很简单,因为l1可以分成两部分:l1+l1->next,这和错误写法是很接近的想法,但是错误写法实际上把l1的头节点给弄丢了,我们需要保留l1的头节点,然后把用l1->next和l2去进行归并,把这个归并后的链表接到l1后面,也就是l1->next=merge(…)。这个等式的左边起到的说连接的作用,而不是表面上的赋值。
在这里插入图片描述
2.绿框里的代码的先后顺序是不能变的:
在这里插入图片描述
如果颠倒了前后顺序,由于结束循环后pre->next=NULL,所以会导致slow->next=NULL

3.绿框中如果不加if判断,而是直接返回merge(sortlist(l),sortlist(slow)),会导致死循环
在这里插入图片描述
死循环是怎么形成的呢?首先我们要意识到一点,就是对函数fun(arg1(),arg2()),我们会首先访问函数arg2();对sortlist(infoptr head),如果head只有一个结点,那么head和slow是一直一样的,那么会进行到return merge(sortlist(l),sortlist(slow))时,会导致一直执行sortlist(slow),根本没有办法结束它的执行。
其实我现在还不清楚函数作为参数时,是怎么进行执行的切换,目前我就粗暴地理解为它返回NULL时就结束这个递归函数的执行,开始执行其它函数

4.同样,如果不判断head是不是NULL,由于后面会为head->next赋值,所以如果head==NULL,那必会导致SIGSEGV
在这里插入图片描述
5.sortlist函数易错点:
在这里插入图片描述

  • 0
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值