【数据结构】PTA 两个有序链表序列的合并 C语言 【详】

本题要求实现一个函数,将两个链表表示的递增整数序列合并为一个非递减的整数序列。

函数接口定义:

List Merge( List L1, List L2 );

其中List结构定义如下:

typedef struct Node *PtrToNode;
struct Node {
    ElementType Data; /* 存储结点数据 */
    PtrToNode   Next; /* 指向下一个结点的指针 */
};
typedef PtrToNode List; /* 定义单链表类型 */

L1L2是给定的带头结点的单链表,其结点存储的数据是递增有序的;函数Merge要将L1L2合并为一个非递减的整数序列。应直接使用原序列中的结点,返回归并后的带头结点的链表头指针。

裁判测试程序样例:

#include <stdio.h>
#include <stdlib.h>

typedef int ElementType;
typedef struct Node *PtrToNode;
struct Node {
    ElementType Data;
    PtrToNode   Next;
};
typedef PtrToNode List;

List Read(); /* 细节在此不表 */
void Print( List L ); /* 细节在此不表;空链表将输出NULL */

List Merge( List L1, List L2 );

int main()
{
    List L1, L2, L;
    L1 = Read();
    L2 = Read();
    L = Merge(L1, L2);
    Print(L);
    Print(L1);
    Print(L2);
    return 0;
}

/* 你的代码将被嵌在这里 */

输入样例:

3
1 3 5
5
2 4 6 8 10

输出样例:

1 2 3 4 5 6 8 10 
NULL
NULL

AC代码:

List Merge( List L1, List L2 ) {
    List L0, temp, temp1, temp2;
    L0 = (List)malloc(sizeof(struct Node));
    L0->Next = NULL;
    List L = L0; // 防止移动头结点

    temp1 = L1->Next; // 跳过头结点
    temp2 = L2->Next; 

    while (temp1 || temp2) {
        if (!temp2 || (temp1 && temp1->Data <= temp2->Data)) {
            temp = temp1;
            temp1 = temp1->Next;
        } else {
            temp = temp2;
            temp2 = temp2->Next;
        }

        temp->Next = NULL; // 避免悬挂指针
        L->Next = temp;
        L = L->Next;
    }

    // 清空 L1 和 L2 的数据部分
    L1->Next = NULL;
    L2->Next = NULL;

    return L0;
}

这里给出一个可供在自己编译器上运行的参考Read和Print函数,只是注意测试数据输入要与下图统一

// 从标准输入读取链表
List Read() {
    List head, p ,p0;
    ElementType value;

    // 创建头结点
    head = (List)malloc(sizeof(struct Node));
    head->Next = NULL;

    // 读取第一个元素
    scanf("%d", &value);

    while (value != -1) { // 假设输入-1作为链表结束的标志
        p = (List)malloc(sizeof(struct Node));
        p->Data = value;
        p->Next = NULL;

        if (head->Next == NULL) {
            head->Next = p; // 连接头结点与数据节点
        } else {
            p0->Next = p;
        }
		p0=p;
        scanf("%d", &value); // 继续读取下一个元素
    }
    return head; // 返回头结点
}

// 打印链表,空链表输出NULL
void Print(List L) {
    List p = L->Next;
    if (p == NULL) {
        printf("NULL\n");
        return;
    }
    while (p != NULL) {
        printf("%d ", p->Data);
        p = p->Next;
    }
    printf("\n");
}

解题过程:

这道题踩了坑,让我更深刻的意识到访问结点时判断结点非空的重要性,因为访问到错误的内存地址会导致程序异常结束,却不报任何错误,在PTA中反映出的就是“段错误”

思路:

将Data值较小的结点或某一链表剩余结点从原链表中抽离出来并添加到新链表中

错误代码及解题过程剖析记录:

List Merge( List L1, List L2 ){
    List L0,temp,temp1,temp2;
    L0=(List)malloc(sizeof(struct Node));
    List L=L0;//防止移动头指针
    temp1=L1->Next,temp2=L2->Next;//跳过头指针
    while(temp1 || temp2){
        while((!temp1 && temp2) || temp1->Data >= temp2->Data){
            temp=temp2->Next;
            temp2->Next=NULL;
            L->Next=temp2;
            temp2=temp;
            L=L->Next;
        }
        while((temp1 && !temp2) || temp1->Data < temp2->Data){
            temp=temp1->Next;
            temp1->Next=NULL;
            L->Next=temp1;
            temp1=temp;
            L=L->Next;
        }
    }
    return L0;
}

关于 SIGSEGV错误:

 SIGSEGV:在POSIX兼容的平台上,SIGSEGV是当一个进程执行了一个无效的内存引用,或发生段错误时发送给它的信号。SIGSEGV的符号常量在头文件signal.h中定义。因为在不同平台上,信号数字可能变化,因此符号信号名被使用。通常,它是信号#11。

Program received signal SIGSEGV, Segmentation fault.-CSDN博客

简单来说就是访问了无效的内存地址,而这种错误直接运行是不会出现的,只有调试时会跳出如图警告

第一个错误点:未判断结点是否非空

while((!temp1 && temp2) || temp1->Data >= temp2->Data)

当temp1,temp2均为空时,第一个判断点不满足,此时第二个判断点直接访问了空结点的Data,造成错误

解决:while((!temp1 && temp2) || (temp2 && temp1->Data >= temp2->Data))

第二个错误点:未将L1和L2置空

这个比较好发现,第一个错误点解决了之后就会有运行结果出来L

根据题目的要求,在合并之后 L1L2 应该成为空链表。这意味着我们需要将 L1L2 中的所有节点移动到新链表中,而不留下任何节点。

解决:L1->Next=NULL; L2->Next=NULL;

修改后正确代码:

(注释部分和原部分效果相同,都可以将Data值较小的结点或某一链表剩余结点从原链表中抽离出来并添加到新链表中,只是过程有所不同)

List Merge( List L1, List L2 ){
    List L0,temp,temp1,temp2;
    L0=(List)malloc(sizeof(struct Node));
    List L=L0;//防止移动头指针
    L->Next=NULL;
    temp1=L1->Next,temp2=L2->Next;
    while(temp1 || temp2){
        while((!temp1 && temp2) || (temp2 && temp1->Data >= temp2->Data)){
            temp=temp2->Next;
            temp2->Next=NULL;
            L->Next=temp2;
            temp2=temp;
            L=L->Next;
            
            /*temp=temp2;
            temp2=temp2->Next;
            temp->Next=NULL;
            L->Next=temp;
            L=L->Next;*/
        }
        while((temp1 && !temp2) || (temp1 && temp1->Data < temp2->Data)){
            temp=temp1->Next;
            temp1->Next=NULL;
            L->Next=temp1;
            temp1=temp;
            L=L->Next;

            /*temp=temp1;
            temp1=temp1->Next;
            temp->Next=NULL;
            L->Next=temp;
            L=L->Next;*/
        }
    }
    L1->Next=NULL;
    L2->Next=NULL;
    return L0;
}

代码的精简过程:

而对于注释掉的第二种方法我们发现后面三步是一模一样的,所以可以将其分出来,而while中的语句也可以简化,于是得到下列代码

这里或许容易有疑问,为什么if里面第一个判断不用判断temp1了呢?因为这里将原本的while改成了if,所以每一次进行if判断都会经过外层的while判断,但是仍然要注意第二个条件中判断temp1

while (temp1 || temp2) {
        if (!temp2 || (temp1 && temp1->Data <= temp2->Data)) {
            temp = temp1;
            temp1 = temp1->Next;
        } else {
            temp = temp2;
            temp2 = temp2->Next;
        }
        temp->Next = NULL; // 避免悬挂指针
        L->Next = temp;
        L = L->Next;
    }

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值