C语言:单链表OJ篇

微软技术分享 微软全球最有价值专家
全栈领域优质创作者
博客专家认证
2025-02-04 20:28:42

1.反转链表

(1)题目描述

https://i-blog.csdnimg.cn/direct/6553162f1c2c457d9948368d5f1ee85e.png

(2)解题思路

迭代

  • 假设链表为 1→2→∅,我们想要把它改成 ∅←1←2

具体思路:

  • 在遍历链表时,将当前节点(cur)的 next 指针改为指向前一个节点。由于节点没有引用其前一个节点,因此必须事先存储其前一个节点,这里小编用prev。在更改引用之前,还需要存储后一个节点,,这里小编用next。最后返回新的头引用,不难想到就是迭代到最后的prev指针。

代码实现:

struct ListNode* reverseList(struct ListNode* head) {
    struct ListNode* prev = NULL;
    struct ListNode* curr = head;
    while (curr) {
        struct ListNode* next = curr->next;
        curr->next = prev;
        prev = curr;
        curr = next;
    }
    return prev;
}

(3)复杂度分析

  • 时间复杂度:O(n),其中 n 是链表的长度。需要遍历链表一次。
  • 空间复杂度:O(1)

2.链表的中间节点

(1)题目描述

https://i-blog.csdnimg.cn/direct/8272d932dd9044f683dfd3e07f4c04ee.png

(2)解题思路

快慢指针

  • 用两个指针 slow fast 一起遍历链表。slow 一次走一步,fast 一次走两步。那么当 fast 到达链表的末尾时,slow 必然位于中间。

代码:

struct ListNode* middleNode(struct ListNode* head) {
    struct ListNode* slow = head, * fast = head;
    while(fast){
        if(fast->next == NULL){
            break;
        }
        fast = fast->next->next;
        slow = slow->next;
    }
    return slow;
}

注意:

  • 这里还得考虑链表仅有一个节点的情况,所以做了特殊处理。

(3)复杂度分析

  • 时间复杂度:O(n),其中 n 是链表的长度。需要遍历链表一次(快慢指针遍历次数之和)。
  • 空间复杂度:O(1)

3.返回倒数第K个节点

(1)题目描述

https://i-blog.csdnimg.cn/direct/a01c866bd43c4dfca682f9368a2c3657.png

(2)解题思路

先后指针

双指针,一个指针p2先走k步,然后两个(p1,p2)一起走。又题目说k是有效的,所以就没有判断先走的指针p2是否越界

代码实现:

int kthToLast(struct ListNode* head, int k) {
    struct ListNode* slow = head, * fast = head;
    while(k--){
        fast = fast->next;
    }
    while(fast){
        fast = fast->next;
        slow = slow->next;
    }
    return slow->val;
}

(3)复杂度分析

时间复杂度: O(n)

空间复杂度: O(1)

4.合并两个有序链表成一个新的有序链表(相对)

(1)题目描述

https://i-blog.csdnimg.cn/direct/88ce02e5d40b4f7d87eb30d28bd99b44.png

(2)解题思路

双指针 + 哨兵位

  • 首先,我们设定一个哨兵节点 dummy,这可以在最后让我们比较容易地返回合并后的链表。我们维护一个 cur指针,我们需要做的是调整它的next指针。然后,我们重复以下过程,直到 list1 或者 list2指向了 NULL:如果 list1 当前节点的值小于等于 list2,我们就把 list1 当前的节点接在 cur节点的后面同时将 list1 指针往后移一位。否则,我们对 list2 做同样的操作。不管我们将哪一个元素接在了后面,我们都需要把 cur向后移一位。
  • 在循环终止的时候, l list1list2 至多有一个是非空的。由于输入的两个链表都是有序的,所以不管哪个链表是非空的,它包含的所有元素都比前面已经合并链表中的所有元素都要大。这意味着我们只需要简单地将非空链表接在合并链表的后面,并返回合并链表即可。

代码实现:

struct ListNode* mergeTwoLists(struct ListNode* list1, struct ListNode* list2) {
   struct ListNode dummy = {};
   struct ListNode* cur = &dummy;             //建立哨兵位
   while(list1 && list2){
    if(list1->val > list2->val){
        cur->next = list2;
        list2 = list2->next;
    }
    else{
        cur->next = list1;
        list1 = list1->next;
    }
    cur = cur->next;
   }
   cur->next = list1 ? list1 : list2;        //链接剩余部分
   return dummy.next;
}

(3)复杂度分析

复杂度分析

时间复杂度:O(n+m),其中 n 和 m 分别为两个链表的长度。

空间复杂度:O(1)

5.链表分割

(1)题目描述

https://i-blog.csdnimg.cn/direct/8f9cfc2859c54a3c822ec21c8f9d16b9.png

(2)解题思路

双指针 + 双哨兵位

  • 我们只需维护两个链表 dummy1dummy2 即可,dummy 链表按顺序存储所有小于x的节点,dummy2 链表按顺序存储所有大于等于x的节点。遍历完原链表后,我们只要将 dummy1 链表尾节点指向 dummy2 链表的头节点即能完成对链表的分割。

代码实现:

struct ListNode* partition(struct ListNode* head, int x) {
    struct ListNode dummy1 = {}, dummy2 = {};
    struct ListNode* cur1 = &dummy1, * cur2 = &dummy2, * fail = head;
    while(fail){
        //把大于等于x的节点放在一个链表1中
        if(fail->val >= x){
            cur1 = cur1->next = fail;
        }
        //把小于x的节点放在另一个链表2中
        else{
            cur2 = cur2->next = fail;
        }
        fail = fail->next;
    }
    //合并链表
    cur2->next = dummy1.next;
    cur1->next = NULL;
    return dummy2.next;
}

(3)复杂度分析

时间复杂度: O(n)

空间复杂度: O(1)

6.回文链表

(1)题目描述

https://i-blog.csdnimg.cn/direct/57f2713c3e88462fba14ef5d53777c8e.png

(2)解题思路

我们可以将链表的后半部分反转(修改链表结构),然后将前半部分和后半部分进行比较。比较完成后我们应该将链表恢复原样

整个流程分为4个步骤:

  • 1.找到前半部分链表的尾节点。
  • 2.反转后半部分链表。
  • 3.判断是否回文。
  • 4.恢复链表。

代码实现:

struct ListNode* reverseList(struct ListNode* head) {
    struct ListNode* prev = NULL;
    struct ListNode* curr = head;
    while (curr != NULL) {
        struct ListNode* nextTemp = curr->next;
        curr->next = prev;
        prev = curr;
        curr = nextTemp;
    }
    return prev;
}

struct ListNode* endOfFirstHalf(struct ListNode* head) {
    struct ListNode* fast = head;
    struct ListNode* slow = head;
    while (fast->next != NULL && fast->next->next != NULL) {
        fast = fast->next->next;
        slow = slow->next;
    }
    return slow;
}

bool isPalindrome(struct ListNode* head) {
    if (head == NULL) {
        return true;
    }

    // 找到前半部分链表的尾节点并反转后半部分链表
    struct ListNode* firstHalfEnd = endOfFirstHalf(head);
    struct ListNode* secondHalfStart = reverseList(firstHalfEnd->next);

    // 判断是否回文
    struct ListNode* p1 = head;
    struct ListNode* p2 = secondHalfStart;
    bool result = true;
    while (result && p2 != NULL) {
        if (p1->val != p2->val) {
            result = false;
        }
        p1 = p1->next;
        p2 = p2->next;
    }

    // 还原链表并返回结果
    firstHalfEnd->next = reverseList(secondHalfStart);
    return result;
}

(3)复杂度分析

时间复杂度:O(n)

空间复杂度:O(1)。我们只会修改原本链表中节点的指向,而在堆栈上的堆栈帧不超过 O(1)。

  • 1.相交链表

(1)题目描述

https://i-blog.csdnimg.cn/direct/d9997aec71d24701993f926c166b6d00.png

(2)解题思路

简单思路:如果相交,则必有交点及之后两链表长度一样,各自加上交点之前不同或相同的链表长度,就是各自的链表总长度。所以我们用两个指针分别指向长链表和短链表,先让指向长链表的指针走差距步,再同时走就可以走到相交点(如果存在)。

代码实现:

struct ListNode *getIntersectionNode(struct ListNode *headA, struct ListNode *headB) {
    //先得到两条链表的长度
    int lA = 0, lB = 0;
    struct ListNode* failA = headA, * failB = headB;
    while(failA){
        failA = failA->next;
        ++lA;
    }
    while(failB){
        failB = failB->next;
        ++lB;
    }
    if(failB != failA) return NULL;
    int gap = lA > lB ? lA-lB : lB-lA;
    //一个小小的技巧找到长的链表并走差距步
    struct ListNode* longlist = headA, * shortlist = headB;
    if(lA < lB){
        longlist = headB;
        shortlist = headA;
    }
    while(gap--){
        longlist = longlist->next;
    }
    //依次比较
    while(longlist && shortlist){
        if(longlist == shortlist){          //该处不可用值相等作为相等条件
            return longlist;
        }
         longlist = longlist->next;
         shortlist = shortlist->next;
    }
    return NULL;
}

巧妙思路:只有当链表 headA headB都不为空时,两个链表才可能相交。因此首先判断链表headAheadB 是否为空,如果其中至少有一个链表为空,则两个链表一定不相交,返回 NULL 当链表headA headB 都不为空时,创建两个指针p1 p2,初始时分别指向两个链表的头节点 headA headB,然后将两个指针依次遍历两个链表的每个节点。具体做法如下:

  • 每步操作需要同时更新指针 p1 p2
  • 如果指针 p1 不为空,则将指针 p1 移到下一个节点;如果指针 p2 不为空,则将指针 p2 移到下一个节点。
  • 如果指针 p1 为空,则将指针 p1移到链表 headB 的头节点;如果指针 p2 为空,则将指针 p2 移到链表 headA 的头节点。
  • 当指针 p1p2 指向同一个节点(NULL也包含在内)时,返回它们指向的节点(NULL)。

证明方法:

情况一:两个链表相交

链表headA headB 的长度分别是m n。假设链表 headA 的不相交部分有 a个节点,链表headB的不相交部分有 b 个节点,两个链表相交的部分有c个节点,则有 a+c=mb+c=n。 如果 a=b,则两个指针会同时到达两个链表相交的节点,此时返回相交的节点; 如果 a!=b,则指针p1会遍历完链表 headA,指针 p2 会遍历完链表 headB,两个指针不会同时到达链表的尾节点,然后指针 p1移到链表headB的头节点,指针 p2 移到链表 headA 的头节点,然后两个指针继续移动,在指针 p1移动了 a+c+b 次、指针 p2 移动了 b+c+a 次之后,两个指针会同时到达两个链表相交的节点,该节点也是两个指针第一次同时指向的节点,此时返回相交的节点。

情况二:两个链表不相交

链表 headAheadB 的长度分别是 m n。考虑当 m=nm!=n时,两个指针分别会如何移动: 如果 m=n,则两个指针会同时到达两个链表的尾节点,然后同时变成空值 NULL,此时返回 NULL; 如果 m!=n,则由于两个链表没有公共节点,两个指针也不会同时到达两个链表的尾节点,因此两个指针都会遍历完两个链表,在指针 p1 移动了 m+n 次、指针p2 移动了 n+m 次之后,两个指针会同时变成空值 NULL,此时返回NULL

代码实现:

struct ListNode *getIntersectionNode(struct ListNode *headA, struct ListNode *headB) {
    if (headA == NULL || headB == NULL) {
        return NULL;
    }
    struct ListNode *p1 = headA, *p2 = headB;
    while (p1 != p2) {
        p1 = p1 == NULL ? headB : p1->next;
        p2 = p2 == NULL ? headA : p2->next;
    }
    return p1;
}

(3)复杂度分析

说明:此处小编不完全采用大O的渐进表示法,而用相对精准的表示法,以此看出解法二的优势之处。并且以不相交为例

  • 解法一:时间复杂度:2m + 2n ;空间复杂度:O(1)
  • 解法二:时间复杂度:2m + 2n2m(也是2n) ;空间复杂度:O(1)

实际上,小编认为两种解法都各有优势和劣势:

  • 解法一代码较长但是可读性强,思路也好想;
  • 解法二代码简洁运行速度略胜一筹,但思路不好想,可读性也不强。

2.随机链表的复制

(1)题目描述

https://i-blog.csdnimg.cn/direct/e47aadea1e0d4f258c0ae192b2c45cc9.png

https://i-blog.csdnimg.cn/direct/fc9d1439131a41b883bebed299ca7f41.png

  • 简单来说,就是复制一条带有随机指针的单链表。

(2)解题思路

  • 我们首先将该链表中每一个节点拆分为两个相连的节点,例如对于链表 A→B→C,我们可以将其拆分为 A→A ′ →B→B ′ →C→C ′ 。对于任意一个原节点 S,其拷贝节点 S ′ 即为其后继节点。
  • 这样,我们可以直接找到每一个拷贝节点 S ′的随机指针应当指向的节点,即为其原节点 S 的随机指针指向的节点 T 的后继节点 T ′ 。需要注意原节点的随机指针可能为空,我们需要特别判断这种情况。
  • 当我们完成了拷贝节点的随机指针的赋值,我们只需要将这个链表按照原节点与拷贝节点的种类进行拆分即可,只需要遍历一次。同样需要注意最后一个拷贝节点的后继节点为空,我们需要特别判断这种情况。

代码实现:

struct Node* copyRandomList(struct Node* head) {
    if (head == NULL)
        return NULL;
    //
    struct Node* cur = head;
    while (cur) {
        struct Node* copy = (struct Node*)malloc(sizeof(struct Node));
        copy->val = cur->val;
        struct Node* next = cur->next;
        cur->next = copy;
        cur = copy->next = next;
        copy->random = NULL;
    }
    //yyds
    cur = head;
    while (cur) {
        if (cur->random != NULL) 
            cur->next->random = cur->random->next;
        cur = cur->next->next;
    }
    //
    cur = head;
   struct Node dummy = {};
   struct Node* phead = &dummy;
   while(cur){
    struct Node* copy = cur->next;
    struct Node* next = copy->next;
    phead = phead->next = copy;
    cur = cur->next = next;
   }
   return dummy.next;
}

(3)复杂度分析

时间复杂度:O(n)

空间复杂度:O(1)。注意返回值不计入空间复杂度。

  • 7.环形链表

(1)题目描述

https://i-blog.csdnimg.cn/direct/9358613c0d174597add9b8bc2bb5b856.png

https://i-blog.csdnimg.cn/direct/d5a8089a4cca485894642043505da5d6.png

(2)解题思路

快慢指针

但要注意:最好使得快指针fast 一次走两步,慢指针slow 一次走一步使得两指针的速度之差为1 。这样就不可能会出现有环不相交的问题。

代码实现:

bool hasCycle(struct ListNode *head) {
    //快慢指针看是否会相遇,但最好快指针一次走两步,慢指针一次走一步,就不会出现有环却不相遇的情况
    struct ListNode* fast = head, * slow = head;
    if(head == NULL) return false;
    while(fast && fast->next){
        fast = fast->next->next;
        slow = slow->next;
        if(fast == slow){
            return true;
        }
    }
    return false;
}

(3)复杂度分析

  • 时间复杂度:O(N),其中 N 是链表中的节点数。
  • 空间复杂度:O(1)。我们只使用了两个指针的额外空间。

8.环形链表2

(1)题目描述

https://i-blog.csdnimg.cn/direct/4e1f091dfe1c4f91b861e04c596a818d.png

https://i-blog.csdnimg.cn/direct/09da9246330c434eaad7f28204186bc7.png

(2)解题思路

思路一:先找到相交点再把相交的点next指针指向空,这样就转变成了头指针head 和·相遇点的next 指针找交点问题。

代码实现:

 struct ListNode *getIntersectionNode(struct ListNode *headA, struct ListNode *headB) {
    if (headA == NULL || headB == NULL) {
        return NULL;
    }
    struct ListNode *p1 = headA, *p2 = headB;
    while (p1 != p2) {
        p1 = p1 == NULL ? headB : p1->next;
        p2 = p2 == NULL ? headA : p2->next;
    }
    return p1;
}

struct ListNode *detectCycle(struct ListNode *head) {
    if (head == NULL) return NULL;
    struct ListNode* fast = head, * slow = head;
    while (fast && fast->next) {
        //先走,防止因为头指针相等
        fast = fast->next->next;
        slow = slow->next;
       if (fast == slow) {
            struct ListNode* ptr = fast->next;
            fast->next = NULL;
            return getIntersectionNode(ptr, head);
        }
    }
     return NULL;
}

思路二:借助定理——两个快慢指针他们相交于环内一位置,而一指针从该位置开始走,同时另一从链表的头结点开始走,它们最终会第一次相交于环开始的那个节点。(怎么得到这个定理的一定要掌握,因为HR问到会问这个)

定理推导:

  • 我们使用两个指针,fast slow。它们起始都位于链表的头部。随后,slow 指针每次向后移动一个位置,而 fast 指针向后移动两个位置。如果链表中存在环,则 fast 指针最终将再次与 slow 指针在环中相遇。
  • 设链表中环外部分的长度为 aslow 指针进入环后,又走了 b 的距离与 fast 相遇。此时,fast 指针已经走完了环的 n 圈,因此它走过的总距离为 a + n(b + c) + b = a + (n + 1)b + nc
  • 根据题意,任意时刻,fast 指针走过的距离都为 slow 指针的 2 倍。因此,我们有a + (n + 1)b + nc = 2(a + b)⟹a = c + (n−1)(b + c)
  • 有了a = c + (n−1)(b + c)的等量关系,我们会发现:从相遇点到入环点的距离加上 n−1 圈的环长,恰好等于从链表头部到入环点的距离。
  • 因此,当发现 slow fast相遇时,我们再额外使用一个指针 ptr。起始,它指向链表头部;随后,它和 slow 每次向后移动一个位置。最终,它们会在入环点相遇。

代码实现:

struct ListNode *detectCycle(struct ListNode *head) {
    //这题要做出就一定要推出一个定理:两个快慢指针他们相交于环内一位置,而一指针从该位置开始走,同时另一从链表的头结点开始走,它们最终会第一次相交于环开始的那个节点。(怎么得到这个定理的一定要掌握,因为HR问到会问这个)
    struct ListNode* fast = head, * slow = head;
    if (head == NULL) return NULL;
    while (fast && fast->next) {
        //先走,防止因为头指针相等
        fast = fast->next->next;
        slow = slow->next;
       if (fast == slow) {
        struct ListNode* ptr = head;
            while(ptr != slow){
                ptr = ptr->next;
                slow = slow->next;
            }
            return ptr;
        }
    }
     return NULL;
}

(3)复杂度分析

快乐的时光总是短暂,咱们下篇博文再见啦!!!如果小编的文章会对你有帮助的话不要忘了,记得给小编点点赞和收藏支持一下,在此非常感谢!!!


文章来源: https://blog.csdn.net/z15879084549/article/details/145353685
版权声明: 本文为博主原创文章,遵循CC 4.0 BY-SA 知识共享协议,转载请附上原文出处链接和本声明。


...全文
31 回复 打赏 收藏 转发到动态 举报
写回复
用AI写文章
回复
切换为时间正序
请发表友善的回复…
发表回复

3,165

社区成员

发帖
与我相关
我的任务
社区描述
微软技术社区为中国的开发者们提供一个技术干货传播平台,传递微软全球的技术和产品最新动态,分享各大技术方向的学习资源,同时也涵盖针对不同行业和场景的实践案例,希望可以全方位地帮助你获取更多知识和技能。
windowsmicrosoft 企业社区
社区管理员
  • 微软技术分享
  • 郑子铭
加入社区
  • 近7日
  • 近30日
  • 至今
社区公告

微软技术社区为中国的开发者们提供一个技术干货传播平台,传递微软全球的技术和产品最新动态,分享各大技术方向的学习资源,同时也涵盖针对不同行业和场景的实践案例,希望可以全方位地帮助你获取更多知识和技能。

予力众生,成就不凡!微软致力于用技术改变世界,助力企业实现数字化转型。

试试用AI创作助手写篇文章吧