C++课堂作业二之反转链表

1问题链接:

https://www.patest.cn/contests/pat-b-practise/1025

2解题想法:

这题原来用数组打过,现在是想保留暂存数据的数组,然后按顺序提取出来到创建的链表里,再写一个传入反转首尾地址的链表反转函数来进行反转。

3GITHUB链接

具体的解决过程长而繁,这里直接给出代码链接,有稍加注释。然后主要想说一下反转的做法(其实是老师课件里的):举个例子来说,反转整个链表就是对数据首节点进行后删,把删掉的节点后插到head之后,也就是数据首节点之前,重复这个操作直到原来的数据首节点变成尾节点,这时反转也就完成了。

4具体过程:

这题这学期的c上机课上打过,当时自己是没有打出来,看着讲题同学的PPT也是折腾了好久才打出来。当初用的是数组,但是这次的主要目的是练习链表,所以我是打算在原来的代码基础上修改成存到链表里,然后在链表里反转后再把链表输出来。

然后毕竟原来没有打出来,所以还是大概说明一下模仿的ppt的想法:先把输入的数据存到一个结构体数组里,输入的时候用另外一个数组来标记题目里的地址(具体的可以看注释),根据标记数组把数据按照题目里的顺序存到另外一个结构体数组里,然后对这个排好序的数组进行反转的操作,最后再把这个数组输出来。
代码如下:

#include<stdio.h>
//定义了个结构体来存储数据
struct Node
{
    int Address;
    int Date;
    int Next;
};
//记录题目里数据地址的标记数组
int mark[100000]={0};
//暂时的数据存储数组和实际操作输出的数组
struct Node node1[100000],node2[100000];
int main()
{
    //用来进行反转操作的函数
    void reverse(struct Node *p,int k);       
    int head,N,k;
    //输入数据的首地址、数据个数、还有反转时用到的k
    scanf("%d %d %d",&head,&N,&k);  
    int i,start;
    //输入乱序的数据
    for(i=0;i<N;i++)
    {
        scanf("%d %d %d",&node1[i].Address,&node1[i].Date,&node1[i].Next);
        //标记数组的下标对应题目里的地址,元素则对应该地址数据在存储数组node1里的位置
        mark[node1[i].Address]=i;
        //记录第一个数据在存储数组中的位置
        if(node1[i].Address==head) start=i;
    }    
    //把数据按题目中要求的顺序存到node2数组里    
    i=0;
    //先是start记录的第一个数据
    node2[0]=node1[start];
    //从第一个数据里获取第二个地址,也就是node[1].Next
    //利用标记数组找到该地址的数据在node1数组中的位置,也就是mark[node[1].Next]
    //把该数据整体赋值给node2[1],也就是按顺序的第二个数据
    //接下来是第三个、第四个...直到出现题目中的结束地址-1
    while(node2[i].Next!=-1)
    {
        i++;
        node2[i]=node1[mark[node2[i-1].Next]];	
    }       
    int count=i;
    i=0;
    //对按题目要求排好序的node2数组进行反转操作
    while(i+k<=count+1)
    {
        reverse(&node2[i],k);
        i=i+k;
    }   
    //按要求输出操作后的数据
    for(i=0;i<count;i++)
    {
        //不足五位补零
        //直接输出下一个数据的地址,反转的时候没有改变数据里的Next
        printf("%05d %d %05d
",node2[i].Address,node2[i].Date,node2[i+1].Address);
    }       
    //结束的地址-1需要特判,它不能补零
    printf("%05d %d -1",node2[count].Address,node2[count].Date);    
    return 0;
} 
//用来进行反转的函数
//传入反转的起始地址和反转个数
void reverse(struct Node *p,int k)
{	
    int i;
    struct Node t;
    for(i=0;i<k/2;i++)
    {		
        //改变的数组下标指向的数组元素,元素本身在内存里的储存位置没变
        t=*(p+i);
        *(p+i)=*(p+k-i-1);
        *(p+k-i-1)=t;
    }
}

然后这是原来的提交结果:

至少原来的是对的,所以修改的前提条件是满足了的,接下来就是怎么改成链表了。

想法是拆分成链表的创建,链表的输出,以及最主要的链表的反转等部分,其中链表的反转按照课件里的想法还用到了后插和后删操作(比如反转整个链表就是对第一个数据节点进行后删,然后把该节点后插到head后面,再对第一个后删再后插到head后面,直到原来的第一个数据节点变成最后一个,这时反转也就完成了),就是现在需要改成要传入反转的首尾地址。

主要的想法就是:保留原来暂时储存数据的结构体数组,然后用链表的创建函数从数组按顺序提取出数据到新建的节点里,并记录链表的长度,然后是把链表分成每K个一组,将每组的首尾地址传入链表反转的函数里,将其整个反转再接回去,最后输出。下面是一些链表的操作函数:

creat

在main函数里直接把head指向了题目要求的第一个节点,第一个节点不再单独申请空间。

(ps:后来发现没有头节点的后删和后插是会有问题的)

void creat(struct Node *head)
{
    //定义个尾节点,用来连接新节点
    //定义个新节点,用来申请新空间
    struct Node *tail,*newp;
    //大概是初始化
    head->Next=NULL;
    tail=head;
    //创建链表
    while(1)
    {
        //申请新节点
        newp=(struct Node *)malloc(sizeof(struct Node));
        //申请空间失败的报错
        if(newp==NULL)
        {
            printf("overflow
");
            //整个程序暂停
            exit(1);
        }
        //利用原来的方法将储存在数组里的数据存到新节点里
        newp->Address=node[mark[tail->After]].Address;
        newp->Date=node[mark[tail->After]].Date;
        newp->After=node[mark[tail->After]].After;
        newp->Next=NULL;
        //把新节点接到链表里
        tail->Next=newp;
        //尾节点后移一位
        tail=newp;
        //输到尾地址-1结束
        if(tail->After==-1)break;
    }
}

deleteAfter

(ps:后来发现这个函数有错,执行的判断什么的,删除的是尾节点的话会出错)

struct Node *deleteAfter(struct Node *p)
{
    //定义个结构体指针指向要删除的节点
    struct Node *t=p->Next;
    if(t->Next!=NULL)
    {   
        //直接将p与后面的节点的后面的接起来
        //达到后删的目的
        //因为还要用到删除的节点,故并不释放空间
        p->Next=t->Next;
        p->After=t->Next->Address;
    }	
    //返回被删除的节点
    return t;
}

insertAfter

void insertAfter(struct Node *p,struct Node *newp)
{
    //将新节点指向要接入的位置
    newp->Next=p->Next;
    newp->After=p->Next->Address;
    //将接入位置的前一个节点指向新节点
    //完成链表的插入
    p->Next=newp;
    p->After=newp->Address;
}

reverse

(ps:准确的说,首地址是要反转的节点的前一个节点的地址)

void reverse(struct Node *head,struct Node *tail)
{
    //调用后删和后插操作
    struct Node *deleteAfter(struct Node *p);
    void insertAfter(struct Node *p,struct Node *newp);       
    //记录尾节点指向的节点地址
    //用于判断循环的结束条件
    int mark=tail->After;
    //定义rear指向第一个数据节点
    struct Node *rear=head,*del; 
    //大概是k为1的操作,也就是什么都不做      
    if(head==tail)
    {
        return;
    }
    //通过后删和后插来实现反转
    while(rear->After!=mark)
    {
        del=deleteAfter(rear);
        insertAfter(head,del);
    }
}

链表的输出操作没有什么好说的,这里就不贴了。然后在尝试整个链表反转时发现了问题:创建链表的时候,head直接指向了数据首节点,然后反转的里面后插操作就会有问题,插到head之后就是插到数据首节点之后,然后就相当于删了首节点之后的一个节点之后又给接回去了,根本没变。。。所以现在是想创建的时候加个不存数据的头节点(大概头节点的存在意义就是这个吧),这样head的后插就相当于数据首节点的前插了(有想过前插,但好像那样链表要双向的,还要指向前一个,所以还是放弃了)。

(ps:后来发现课件有说头节点就是为了方便后删和后插,毕竟单向的链表(指向下一个元素)也是后插和后删好实现)

然后,还在改。。。2016.05.20

后来是把head指向了一个申请的结构体空间,这个结构体(也就是没有数据的头节点)的Next再指向储存第一个数据的结构体数组里的元素,然后又在creat,display,reverse等函数里修改了一下,下面是输出反转前后的链表的运行结果(先是尝试整个反转):

然后就是解决每k个节点反转的问题,主要想法就是创建链表的时候记录一下长度,看要反转几次,也就是调用几次reserve函数,每次改变一下要反转的首尾地址就好。大概代码如下:

(ps:也是有bug的代码,首尾地址的传递没有处理好)

int len,j;
struct Node *head,*ph,*pt,*p;
head=(struct Node *)malloc(sizeof(struct Node));
//头节点指向数据首节点
head->Next=&node[first];
//记录链表长度
len=creat(head);
//初始反转的首地址
ph=head;
//一共执行len/k次反转
for(i=0;i<len/k;i++)
{
	p=ph;    
	for(j=0;j<k;j++)
	{
		p=p->Next;
	}
    //获取反转的尾地址
	pt=p;
    //调用函数进行反转
	reverse(ph,pt);
    //让下次反转的首地址为前一次的尾地址
	ph=pt;
}

测试样例可以倒是可以过:

然后我就直接交了一发:

还是回去多试试几个样例好了。。。

折腾来折腾去(只会用输出来判断哪出错也是够了。。。),发现的bug是每组反转不能很好的衔接在一起,原来的首尾指向的数据反转后已经换了位置,然后下一轮的首尾地址按原来的赋值就会乱掉,首地址会是链表的第一个数据节点。举个例子来说:把样例中的k改为3,输出的结果会是3、4、1、2、5、6,第一遍的反转确实把链表变成了3、2、1、4、5、6,但是原来的尾节点3已经跑到了第一位,下一次反转的首地址就会变成3的地址,也会变成反转2、1、4,所以输出就会变成是3、4、1、2、5、6。

上面的bug改完之后,结果是停止运行。。。最后发现是后删操作有问题,就里面的执行判断有错,如果删的是尾节点,会崩溃的。

一番修改之后,这是提交结果:

心情真是复杂。。。我到底又漏掉了什么。看到群里说单节点什么的,随意改了一下都没调试就交上去:

额,起码知道哪里错了。然后又改了一下,终于对了(开心):

最终代码已上传GITHUB,这里就不贴了,这里再来一个传送门

2016.05.21

原文地址:https://www.cnblogs.com/mingyueanyao/p/5511741.html