预推免面试-算法-链表

目录

逆序打印链表

反转链表

算法细节

反转链表前N个节点

反转链表的一部分

合并两个有序链表

单链表的分解

合并K个有序链表

复杂度分析

单链表的倒数第k个节点

删除节点

单链表的中点

判断链表是否包含环

如何计算这个环的起点?

链表相交


逆序打印链表

只需要打印的话,先递归地遍历到最后,再打印

反转链表

需要递归实现

// 定义:输入一个单链表头结点,将该链表反转,返回新的头结点
ListNode* reverse(ListNode* head)
{
    if (head == nullptr || head->next == nullptr) {
        return head;
    }
    ListNode* last = reverse(head->next);
    head->next->next = head;
    head->next = nullptr;
    return last;
}

算法细节

reverse函数返回的是反转后的链表的最后一个结点

head是原来的链表中这个节点的前驱结点

head.next.next就是把这个节点的next设置为其前驱结点

再将其前驱结点的后继设置为null

反转链表前N个节点

需要记录后继节点successor

类似于反转链表,只需要稍作修改

// 定义:输入一个单链表头结点,将该链表反转,返回新的头结点
ListNode* reverseN(ListNode* head, int n)
{
    if (n == 1) {
        successor = head->next;
        return head;
    }
    ListNode* last = reverse(head->next, n-1);
    head->next->next = head;
    head->next = successor;
    return last;
}

反转链表的一部分

给一个索引区间 [m, n](索引从 1 开始),仅仅反转区间中的链表元素

实际上是在求head的next节点为头结点的链表的[m-1,n-1]区间反转后的结果,一直递归下去直到m=1

// 注意:cpp 代码由 chatGPT🤖 根据我的 java 代码翻译,旨在帮助不同背景的读者理解算法逻辑。
// 本代码还未经过力扣测试,仅供参考,如有疑惑,可以参照我写的 java 代码对比查看。

ListNode* reverseBetween(ListNode* head, int m, int n) {
    // base case
    if (m == 1) {
        return reverseN(head, n);
    }
    // 前进到反转的起点触发 base case
    head->next = reverseBetween(head->next, m - 1, n - 1);
    return head;
}

合并两个有序链表

类似于归并排序的合并操作

需要创造一条新链表的时候,可以使用虚拟头结点dummy简化边界情况的处理

有了dummy节点这个占位符,可以避免处理空指针的情况,降低代码的复杂性

dummy作为结果的头节点的虚拟的头节点

-1对一个节点来说是一个不太可能的值,标记着这个节点是一个dummy

指针p负责生成结果链表

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode() : val(0), next(nullptr) {}
 *     ListNode(int x) : val(x), next(nullptr) {}
 *     ListNode(int x, ListNode *next) : val(x), next(next) {}
 * };
 */
class Solution {
public:
    ListNode* mergeTwoLists(ListNode* list1, ListNode* list2) 
    {
        ListNode* dummy = new ListNode(-1);
        ListNode* p = dummy;
        ListNode* p1 = list1;
        ListNode* p2 = list2;

        while (p1 != NULL && p2 != NULL)
        {
            if (p1->val > p2->val)
            {
                p->next = p2;
                p2 = p2->next;
            }
            else
            {
                p->next = p1;
                p1 = p1->next;
            }

            p = p->next;
        }

        if (p2 != NULL)
        {
            p->next = p2;
        }

        if (p1 != NULL)
        {
            p->next = p1;
        }

        return dummy->next;
    }
};

单链表的分解

按照给定值x将链表分为小于x和大于等于x的前后两个部分

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode() : val(0), next(nullptr) {}
 *     ListNode(int x) : val(x), next(nullptr) {}
 *     ListNode(int x, ListNode *next) : val(x), next(next) {}
 * };
 */
class Solution {
public:
    ListNode* partition(ListNode* head, int x)
    {
        ListNode* dummy1 = new ListNode(-1);
        ListNode* dummy2 = new ListNode(-1);

        ListNode* p1 = dummy1;
        ListNode* p2 = dummy2;

        ListNode* p = head;

        while (p != NULL)
        {
            if (p->val < x)
            {
                p1->next = p;
                p1 = p1->next;
            }
            else
            {
                p2->next = p;
                p2 = p2->next;
            }

            // p = p->next;
            // 断开原链表中每个结点的next指针
            ListNode* tmp = p->next;
            p->next = nullptr;
            p = tmp; 
        }

        p1->next = dummy2->next;
        
        return dummy1->next;
    }
};

注意其中的插入到新的链表后需要断开原链表中的节点的next指针

但是需要将p->next指针保存下来,用于下一步的遍历

合并K个有序链表

23. 合并 K 个升序链表 - 力扣(LeetCode)

难点在于如何快速得到 k 个节点中的最小节点,接到结果链表上

将链表节点放入一个最小堆,每次从堆顶pop出一个节点后,再push一个当前节点的next进来

复杂度分析

维护k个元素的最小堆的pop和push操作,本质上是每次将插入的元素下降到应该在的位置。最大下降的高度就是logk

所以维护的时间复杂度为O(logk),k为链表的条数

一共有n个节点,所以时间复杂度为O(nlogk)

单链表的倒数第k个节点

难点在于只知道头节点,并且要求只遍历一次

类似于移动窗口

两个指针一个遍历,一个指向另一个指针的倒数第k个前驱结点

删除节点

将x节点的前驱结点和x的后继节点相接

引入dummy防止删除第一个头节点的时候,没有前驱结点的错误

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode() : val(0), next(nullptr) {}
 *     ListNode(int x) : val(x), next(nullptr) {}
 *     ListNode(int x, ListNode *next) : val(x), next(next) {}
 * };
 */
class Solution {
public:
    ListNode* removeNthFromEnd(ListNode* head, int n) 
    {
        ListNode* dummy = new ListNode(-1);
        dummy->next = head;

        ListNode* p1 = dummy;

        for (int i = 0; i < (n+1); i++)
        {
            p1 = p1->next;
        }

        ListNode* p2 = dummy;

        while (p1 != NULL)
        {
            p1 = p1->next;
            p2 = p2->next;
        }

        p2->next = p2->next->next;

        return dummy->next;
    }
};

单链表的中点

使用「快慢指针」的技巧:

我们让两个指针 slow 和 fast 分别指向链表头结点 head

每当慢指针 slow 前进一步,快指针 fast 就前进两步,这样,当 fast 走到链表末尾时,slow 就指向了链表中点

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode() : val(0), next(nullptr) {}
 *     ListNode(int x) : val(x), next(nullptr) {}
 *     ListNode(int x, ListNode *next) : val(x), next(next) {}
 * };
 */
class Solution {
public:
    ListNode* middleNode(ListNode* head)
    {
        ListNode* slow = head;
        ListNode* fast = head;

        while (fast != NULL && fast->next != NULL)
        {
            slow = slow->next;
            fast = fast->next->next;
        }

        return slow;
    }
};

判断链表是否包含环

快慢指针:

fast和slow相遇说明两个指针进入了环,并且fast超过了slow一圈

如何计算这个环的起点?

当快慢指针相遇时,让其中任一个指针指向头节点,然后让它俩以相同速度前进,再次相遇时所在的节点位置就是环开始的位置

因为假设相遇点离环的起点为m,走的距离为k和2k

k是环的长度 的倍数

k-m是起点和头结点的距离,同时也从相遇点走到环的起点

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode(int x) : val(x), next(NULL) {}
 * };
 */
class Solution {
public:
    ListNode *detectCycle(ListNode *head)
    {
        ListNode* slow = head;
        ListNode* fast = head;

        while (fast != NULL && fast->next != NULL)
        {
            slow = slow->next;
            fast = fast->next->next;

            if (fast == slow)
            {
                break;
            }
        }

        if (fast == NULL || fast->next == NULL)
        {
            return NULL;
        }
        
        slow = head;
        while (fast != slow)
        {
            slow = slow->next;
            fast = fast->next;
        }

        return slow;
    }
};

链表相交

链表A由n1加上m的公共节点组成

链表B由n2加上m的公共部分组成

所以有n1+m+n2=n2+m+n1

我们可以让 p1 遍历完链表 A 之后开始遍历链表 B,让 p2 遍历完链表 B 之后开始遍历链表 A,这样相当于「逻辑上」两条链表接在了一起

如果这样进行拼接,就可以让 p1 和 p2 同时进入公共部分

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode(int x) : val(x), next(NULL) {}
 * };
 */
class Solution {
public:
    ListNode *getIntersectionNode(ListNode *headA, ListNode *headB)
    {
        ListNode* p1 = headA;
        ListNode* p2 = headB;
        while (p1 != p2)
        {
            if (p1 == NULL)
            {
                p1 = headB;
            }
            else
            {
                p1 = p1->next;
            }

            if (p2 == NULL)
            {
                p2 = headA;
            }
            else
            {
                p2 = p2->next;
            }
        }

        return p1;
    }
};

如果不存在交点,则最后都同时到达null

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值