关于链表相关的OJ题

✨✨✨专栏:数据结构     

          🧑‍🎓个人主页SWsunlight

一、 OJ题 返回倒数第K个节点

 1、遍历链表一遍:用2个指针,pheadptail先让ptail先走k步,然后让2个指针一起走,快的走到NULL即可

就是数学问题:第n-k个就是倒数第k个

2、也可以遍历一遍算出节点个数n,然后倒数第k个就是节点n-k,在遍历一次即可

代码:

int kthToLast(struct ListNode* head, int k){
    struct ListNode*phead=head;
    struct ListNode*ptail=head;
    //先让快的走k步
    while(k--)
    {
        ptail=ptail->next;
    }
    while(ptail)
    {
         ptail=ptail->next;
         phead=phead->next;
    }
    return phead->val;

}

falst 和 slow

二、OJ题 合并2个有序链表

 注意:题目说的链表是有序的,那么我们可以建立一个哨兵位 然后将各个节点放到哨兵位的后面 进行有序排序即可:

//改名,便于使用
typedef struct ListNode ListNode;
struct ListNode* mergeTwoLists(struct ListNode* list1, struct ListNode* list2) {
    //先判空:
    if(list1==NULL)
        return list2;
    if(list2==NULL)
        return list1;
    ListNode*head;
    ListNode*tail;
    ListNode*l1 = list1;
    ListNode*l2 = list2;
    head = tail = (ListNode*)malloc(sizeof(ListNode));
    //只要有一个为NULL就出来;
    while(l1&&l2)
    {
        //小的先放入后面
        if(l1->val<l2->val)
        {
            tail->next = l1;
            tail=tail->next;
            l1 = l1->next;
        }
        else
        {
            tail->next = l2;
            tail = tail->next;
            l2 = l2->next;
        }
    }
    //出循环2种情况:l1和l2都为NULL
    //              l1或者l2 一个为NULL]
    //不要用循环了,因为其中一个为NULL,tail后面直接就是加上了另一个链表的剩下的节点
    if(l1)
    {
        tail->next = l1;
        tail = tail->next;
    }
    if(l2)
    {
        tail->next = l2;
        tail = tail->next;
    }
    ListNode*ret = head->next;
    //申请的空间还给操作系统
    free(head);
    head = NULL;
    return ret;
}

 三、相交链表

关于这个题,可以先考虑是否相交遍历(同时记录2个指针的长度)对最后一个节点地址进行判断是否相等(相等就是说明这个环相交),若是不相交直接结束程序了;

若是相交,我们就得继续考虑:发现了么,若是相交,看头节点的位置,2个链表会有一个距离差,这个时候我们可以长的链表先走完距离差,然后2个链表一起遍历,同速度一定相遇

注意:我们用地址那判断,不用节点里的数据是因为无法保证后前面数据有相同的,地址更保险

typedef struct ListNode ListNode;
struct ListNode *getIntersectionNode(struct ListNode *headA, struct ListNode *headB) {
    ListNode*l1=headA;
    ListNode*l2=headB;
    //记录节点长度
    int k = 0;
    int n = 0;
    //l1、l2的位置不能到NULL,不然这不就一定相等了(链表的最后一个节点指向的下一个地址为NULL)
    while(l1&&l1->next)
    {
        l1 = l1->next;
        k++;
    }
    while(l2&&l2->next)
    {
        l2 = l2->next;
        n++;
    }
    //若是最后一个节点不相等 说明不为环
    if(l1!=l2)
    {
        return NULL;
    }
    //为环继续进行;
    ListNode* phead1 = headA;
    ListNode* phead2 = headB;
    //假设法:1放长的,2放小的链表
    if(k<n)
    {
        phead1 = headB;
        phead2 = headA;
    }
    //求2个的链表的长度差
    //abc为绝对值函数,返回绝对值
    int key = abs(n-k);
    while(key--)
    {
        phead1=phead1->next;
    }
    //同时遍历,相同时结束
    while(phead1!=phead2)
    {
        phead1 = phead1->next;
        phead2 = phead2->next;
    }
    return phead1;

}

用到了假设法,还有 绝对值函数:abs()函数

四、链表的中间节点

快慢指针即可,快的指针到到尾节点时,慢指针刚好走了一半;不要考虑节点个数为奇数还是偶数,因为题目说了若是2个中间节点返回第2个,如下:当快指针结束会有2种情况,刚好跟节点个数有关 记住节点个数:n      中间节点的公式: n /  2   表示从第一个节点的下一个节点开始数,第几个节点就是中间节点

typedef struct ListNode ListNode;
struct ListNode* middleNode(struct ListNode* head) {
    ListNode* fast = head;
    ListNode* slow = head;
    while(fast&&fast->next)
    {
        fast = fast->next->next;
        slow = slow->next;
    }
    return slow;
}

五、反转链表

看到题目可能会想到创建一个新链表,将原链表的节点以头插的形式插入到新链表中,但是我今天想用的是不创建新的链表,使用迭代的方式进行实现反转

将l1当做尾节点依次类推  

用到了3个指针  l3指向NULL,l2用来保存下个节点的地址,l1用来修改所指向节点的next的地址   

移动步骤如下:1、修改l1的next指向l3

                         2、将l3移动到l1的位置

                         3、将l1移动到l2的位置

                         4、将l2后移一位

结束条件 我们将l2作为条件,当l2指向空时结束,如下:但是l1的next是还没有变的,使用if语句判断 进行修改 即可,最后返回 l1 刚好是头节点的地址

typedef struct ListNode ListNode;
struct ListNode* reverseList(struct ListNode* head) {
    //判NULL,若是为空则直接返回
    if(head==NULL)
    {
        return head;
    } 
    //l2保存下一个节点地址,l1修改其所在节点的next 
    ListNode*l1 = head;
    ListNode*l2 = head->next;
    //存储l1->next的节点,开始时是NULL,因为头节点变成尾节点的next指向的内容为NULL
    ListNode*l3 = NULL;
    while(l2)
    {
        l1->next = l3;
        l3 = l1;
        l1 = l2;
        l2 = l2->next;
        //当l2NULL;l1在尾节点,直接修改尾节点的next内容,
        if(l2==NULL)
        {
            l1->next = l3;
        }
    }
    return l1;
}

注意 :要判空,不然l2赋值时就非法操作了(对空指针进行解引用)

方法二、递归法:

思路:将最后一个节点当成头节点,将前一个节点插到后一个节点的next处,将前一个节点的next指向NULL;

每个函数的头节点head恰好都对应的节点,然后将5的尾插入前一个节点,还有NULL也不要忘记插;

 //递归
struct ListNode* _reverseList(struct ListNode* head)
{
    //2种情况结束,传的头本身就是空
    if(head==NULL||head->next==NULL)
    {
        return head;
    }
    struct ListNode*nwhead=_reverseList(head->next);
    head->next->next=head;
    head->next=NULL;
    return nwhead;

}


struct ListNode* reverseList(struct ListNode* head){
    return _reverseList(head);
}

 六、链表的回文结构:

快慢指针法:

看到回文结构,我们可以用到中间节点和反转链表2个方法结合,CV一下;我们将链表的中间节点后面的节点进行反转,然后进行判断

这里取的是第3个节点进行反转,但是因为我么的第2个节点next指针指向的是第三个,我们没有对它修改,所以拆开以后就是如下的样子:

无论奇数还是偶数都没有影响

typedef struct ListNode ListNode;

class PalindromeList {
  public:
   
    struct ListNode* middleNode(struct ListNode* head) {
        ListNode* fast = head;
        ListNode* slow = head;
        while (fast && fast->next) {
            fast = fast->next->next;
            slow = slow->next;
        }
        return slow;
    }
    struct ListNode* reverseList(struct ListNode* head) {
        //判NULL,若是为空则直接返回
        if (head == NULL) {
            return head;
        }
        //l2保存下一个节点地址,l1修改其所在节点的next
        ListNode* l1 = head;
        ListNode* l2 = head->next;
        //存储l1->next的节点,开始时是NULL,因为头节点变成尾节点的next指向的内容为NULL
        ListNode* l3 = NULL;
        while (l2) {
            l1->next = l3;
            l3 = l1;
            l1 = l2;
            l2 = l2->next;
            //当l2NULL;l1在尾节点,直接修改尾节点的next内容,
            if (l2 == NULL) {
                l1->next = l3;
            }
        }
        return l1;
    }

    bool chkPalindrome(ListNode* A) {
        //pet接受后半部分的反转链表
        ListNode* pet = middleNode(A);
        ListNode*A1 =A;
        ListNode*A2 =reverseList(pet);
        //进行比较
        while(A2)
        {
            if(A2->val!=A1->val)
            {
                return false;
            }
            A2=A2->next;
            A1=A1->next;
        }
        return true;


    }
};

我在牛客网写的这个题目,因为c++是兼容c的牛客题没有c语言这个环境的选项,就直接在c++里面写了

七、移除链表元素: 

3个指针 :

将节点取出来创建成一个“新”的链表(一个记录头一个记录尾)再来一个遍历链表,遍历到NULL时结束循环;以上面题目提供的例子为例的话,当我将所有节点取出重新组合后,为下图第一个图形,发现会有一个6没去掉,因为节点指向下一个节点的next没有修改,所以最后循环结束应该要加一个判断条件 尾指针是否为NULL,不是NULL,就给它next赋值NULL;

typedef struct ListNode ListNode;
struct ListNode* removeElements(struct ListNode* head, int val) {
    //先判空
    if(head==NULL)
    {
        return head;
    }
    //记录头结点
    ListNode* phead= NULL;
    //记录尾节点
    ListNode*ptail = NULL;
    //用来遍历:
    ListNode*pcur=head;
    while(pcur)
    {
       if(pcur->val!=val)
       {
        //是否为空链表
        if(phead==NULL)
        {
            phead= ptail=pcur;
        }
        else
        {
            ptail->next=pcur;
            ptail = ptail->next;
        }
       }
       pcur = pcur->next;
    }
    //出循环以后,若是ptail不是空,将下一个地址给空
    if(ptail)
    {
        ptail->next=NULL;
    }
 
    return phead;
}

 八、随机链表的复制

错位插入,复制每个节点,然后和原链表以如下方式连接起来,形成一个新的链表

如下:要向堆区申请空间,要创建一个节点

给random分配

typedef struct Node Node;
struct Node* copyRandomList(struct Node* head) {
    //创立一个新的节点
    Node*phead = head;
    //phead为NULL时结束
    while(phead)
    {
        //申请节点,创建新的节点,需要申请空间
        Node*node = (Node*)malloc(sizeof(Node));
        node->val = phead->val;
        //我的节点next为head的下一个节点地址
        node->next=phead->next;
        //初始情况:
        node->random = NULL;
        //phead的next指向node
        phead->next=node;
        phead = node->next;
    }
    //对上面的部分random进行分配
    Node* cal = head;
    while(cal)
    { 
        //cal的下一个节点就是要分配的节点
        Node* pov = cal->next;
        if(cal->random==NULL)
        {
            pov->random=NULL;
        }
        else
        {
            pov->random = cal->random->next;
        }
        cal = pov->next;
    }
    //拆开  //裁掉
    cal = head;
    //尾
    Node* povhead=NULL;
    //头不动
    Node* nude = NULL;
    while(cal)
    {
    //判空
     if(povhead==NULL)
     {
        povhead = nude = cal->next;
     }
     else{
        povhead->next = cal->next;
        povhead = povhead->next;

     }

     //恢复原链表
     cal->next = povhead->next;
     cal = cal->next;
        
    }

    return nude;

}

 

<think>嗯,用户的问是关于如何在编写链表算法时避免空指针解引用错误。首先,我需要回忆一下链表操作中常见的空指针情况。比如访问头节点或尾节点的时候,如果链表为空,直接访问next或者prev就会出错。还有在遍历链表时,循环条件设置不当也可能导致访问空指针。 记得引用中提到过带尾指针的单循环链表实现,这可能涉及到如何处理尾节点的next指针,避免指向空的情况。另外,判断链表是否有环的快慢指针算法也需要处理指针的移动,确保在移动前指针不为空。比如引用[3]提到快慢指针每次移动前都要检查是否存在空指针,这样可以避免解引用空指针。 然后,用户提到的合并两个有序链表的例子中,代码里用了条件判断来处理cur1和cur2是否为空,这可能是一个关键点。比如在合并过程中,如果其中一个链表已经遍历完,就直接将尾指针指向另一个链表的剩余部分。这时候需要确保剩下的链表节点不为空,或者正确处理空的情况。 我还需要思考有哪些通用的最佳实践。比如,在访问节点属性之前先检查指针是否为空,使用卫语句提前处理边界条件,初始化指针时赋予安全值,比如头节点的next初始化为null。此外,防御性编程,比如在函数入口检查输入链表是否为null,可以避免很多问。 还有,循环链表的处理可能需要注意终止条件,避免无限循环。例如,在判断双链表是否对称的算法中,引用[4]中的代码用p != q和q != p->next作为循环条件,这可能在处理奇偶节点时防止越界。 另外,测试用例也很重要。比如空链表、单节点链表、头尾节点处理等情况都需要覆盖,确保代码在各种情况下都不会解引用空指针。 可能用户在实际编码中遇到了一些空指针崩溃的问,所以需要具体的解决方案。我应该把这些点整理成结构化的建议,包括输入验证、指针检查、边界处理、防御性编程和测试方法,并结合引用中的例子来说明。</think>### 链表操作中避免空指针解引用错误的最佳实践 #### 一、输入验证阶段 1. **函数入口检查** - 对所有传入的链表参数进行非空判断:`if (head == NULL) return NULL` - 示例:在合并有序链表算法中,需先判断输入链表是否为NULL[^5] 2. **特殊节点检查** - 处理头节点/尾节点时,验证`L->next`或`L->prev`是否存在 - 如判断循环双链表对称性时,需先确保链表不为空[^4] #### 二、指针移动与访问规范 1. **访问前必验证** - 在访问`p->next`或`p->data`前,必须保证`p != NULL` - 示例:快慢指针算法中每次移动前需判断指针有效性[^3] ```c while (fast && fast->next) { // 确保fast和fast->next非空 slow = slow->next; fast = fast->next->next; } ``` 2. **尾指针安全处理** - 带尾指针的循环链表更新时,需维护`tail->next`指向头节点 - 如带尾指针的单循环链表实现中需保证循环闭合性[^1] #### 三、边界条件处理 | 场景 | 解决方案 | 示例代码片段 | |-----------------|----------------------------|--------------------------------| | 空链表 | 提前返回或初始化保护 | `if (L->next == L) return true` | | 单节点链表 | 单独处理头尾指针关系 | `tail->next = head` | | 删除头/尾节点 | 更新相邻节点指针后再释放内存 | `prev->next = curr->next` | #### 四、防御性编程技巧 1. **卫语句(Guard Clauses)** ```c void insertNode(Node* prev, Node* newNode) { if (prev == NULL || newNode == NULL) return; newNode->next = prev->next; prev->next = newNode; } ``` 2. **初始化安全值** - 创建新节点时立即初始化指针:`newNode->next = NULL` 3. **循环终止条件增强** - 遍历时使用`while (curr && curr->next)`替代简单条件 #### 五、测试验证方法 1. **必测场景清单** - ✅ 空链表操作 - ✅ 单节点链表 - ✅ 头/尾节点边界操作 - ✅ 循环链表闭环/开环测试 2. **自动化检测工具** - 使用Valgrind检测内存错误 - 开启编译器警告选项:`-Wall -Wextra` ### 典型案例分析 **合并有序链表时的安全操作[^5]** ```c struct ListNode* mergeTwoLists(struct ListNode* list1, struct ListNode* list2){ Node head; // 虚拟头节点 Node* tail = &head; // 始终指向有效节点 while (list1 && list2) { // 循环条件确保两个指针有效 if (list1->val <= list2->val) { tail->next = list1; list1 = list1->next; // 移动前指针已非空 } else { tail->next = list2; list2 = list2->next; } tail = tail->next; // 移动尾指针 } tail->next = list1 ? list1 : list2; // 三元运算符避免空指针 return head.next; } ```
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值