通关算法题之 ⌈链表⌋

news2024/11/22 15:39:51

链表

删除元素

203. 移除链表元素

给你一个链表的头节点 head 和一个整数 val ,请你删除链表中所有满足 Node.val == val 的节点,并返回 新的头节点 。

示例

img
输入:head = [1,2,6,3,4,5,6], val = 6
输出:[1,2,3,4,5]
class Solution {
public:
    ListNode* removeElements(ListNode* head, int val) {
        if(!head) return head;
        ListNode* dummy = new ListNode(0);
        dummy->next = head;
        ListNode* p = dummy;
        while(p && p->next){
            if(p->next->val == val){
                p->next = p->next->next;
            }else{
                p = p->next;
            }
        }
        return dummy->next;
    }
};

83. 删除排序链表中的重复元素

给定一个已排序的链表的头 head , 删除所有重复的元素,使每个元素只出现一次 ,返回 已排序的链表 。

示例:

img
输入:head = [1,1,2,3,3]
输出:[1,2,3]
class Solution {
public:
    ListNode* deleteDuplicates(ListNode* head) {
        if(!head) return head;
        ListNode* p = head;
        while(p->next){
            if(p->next->val == p->val){
                p->next = p->next->next;
            }else{
                p = p->next;
            }
        }
        return head;
    }
};

相交链表

160. 相交链表

给你两个单链表的头节点 headAheadB ,请你找出并返回两个单链表相交的起始节点。如果两个链表不存在相交节点,返回 null。图示两个链表在节点 c1 开始相交。题目数据 保证 整个链式结构中不存在环。注意,函数返回结果后,链表必须 保持其原始结构

img

这题难点在于,由于两条链表的长度可能不同,两条链表之间的节点无法对应:

image-20220505184443179

如果用两个指针 p1p2 分别在两条链表上前进,并不能同时走到公共节点,也就无法得到相交节点 c1解决这个问题的关键是,通过某些方式,让 p1p2 能够同时到达相交节点 c1 —— 使用双指针。

如果用两个指针 p1p2 分别在两条链表上前进,我们可以让 p1 遍历完链表 A 之后开始遍历链表 B,让 p2 遍历完链表 B 之后开始遍历链表 A,这样相当于「逻辑上」两条链表接在了一起。

如果这样进行拼接,就可以让 p1p2 同时进入公共部分,也就是同时到达相交节点 c1

image-20220505184819556

那如果两个链表没有相交点,是否能够正确的返回 null 呢?这个逻辑可以覆盖这种情况的,相当于 c1 节点是 null 空指针,可以正确返回 null。

class Solution {
public:
    ListNode *getIntersectionNode(ListNode *headA, ListNode *headB) {
        ListNode* p1 = headA, * p2 = headB;
        while(p1 != p2){
            if(p1) p1 = p1->next;
            else p1 = headB;
            if(p2) p2 = p2->next;
            else p2 = headA;
        }
        return p1;
    }
};

合并链表

21、合并两个有序链表

将两个升序链表合并为一个新的 升序 链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。
示例:

img
输入:l1 = [1,2,4], l2 = [1,3,4]
输出:[1,1,2,3,4,4]

用指针分别访问两个链表,根据题意比较节点的大小,赋值到新的链表。

1

这个算法的逻辑类似于「拉拉链」,l1, l2 类似于拉链两侧的锯齿,指针 p 就好像拉链的拉索,将两个有序链表合并。合并后 list1 和 list2 最多只有一个还未被合并完,我们直接将链表末尾指向未合并完的链表即可。

代码中还用到一个链表的算法题中是很常见的「虚拟头结点」技巧,也就是 dummy 节点,它相当于是个占位符,可以避免处理空指针的情况,降低代码的复杂性。

struct ListNode{
    int val;
    ListNode* next;
    ListNode(int x): val(x), next(nullptr){}
}

class Solution {
public:
    ListNode* mergeTwoLists(ListNode* list1, ListNode* list2) {
        //虚拟头节点
        ListNode* dummy = new ListNode(-1);
        ListNode* p = dummy, *p1 = list1, *p2 = list2;
        while(p1 && p2){
            // 比较 p1 和 p2 两个指针,将值较小的的节点接到 p 指针
            if(p1->val > p2->val){
                p->next = p2;
                p2 = p2->next;
            }else{
                p->next = p1;
                p1 = p1->next;
            }
            p = p->next;
        }
        p->next = !p1 ? p2 : p1;
        return dummy->next;
    }
};

23、合并 k 个有序链表

给你一个链表数组,每个链表都已经按升序排列。请你将所有链表合并到一个升序链表中,返回合并后的链表。

示例:

输入:lists = [[1,4,5],[1,3,4],[2,6]]
输出:[1,1,2,3,4,4,5,6]
解释:链表数组如下:
[
  1->4->5,
  1->3->4,
  2->6
]
将它们合并到一个有序链表中得到。
1->1->2->3->4->4->5->6

本题是上一道题目21. 合并两个有序链表 的延伸,利用 优先级队列(二叉堆) 进行节点排序即可。

具体步骤

  1. 新建虚拟节点dummy,指向合并后的链表, 新建一个优先级队列priority_queue
  2. 将所有的链表的第一个元素加入队列;
  3. 将队列的第一元素出队列,插入到新链表的尾部;
  4. 将第一元素的下一个元素入队,比较剩下的链表的第1个元素和第i个链表的第2个元素;
  5. 重复3-4步, 得到新的队列。
class Solution {
public:
    struct Comp{
        //小根堆,从小到大排序
        bool operator()(ListNode* l1, ListNode* l2){
            return l1->val > l2->val;
        }
    }; 
    
    ListNode* mergeKLists(vector<ListNode*>& lists) {
        //优先级队列,最小堆
        priority_queue<ListNode*, vector<ListNode*>, Comp> pq;
        //虚拟节点
        ListNode* dummy = new ListNode(-1);
        ListNode* p = dummy;
        // 将所有的链表的第一个元素加入队列
        for(ListNode* head : lists){
            if(head) pq.push(head);
        }
        while(!pq.empty()){
            // 获取最小节点,接到结果链表中
            ListNode *node = pq.top(); pq.pop();
            p->next = node;
            p = p->next;
            if(node->next) pq.push(node->next);
        }
        return dummy->next;
    }
};

这个算法是面试常考题,它的时间复杂度是多少呢?

优先级队列 pq 中的元素个数最多是 k,所以一次 poll 或者 add 方法的时间复杂度是 O(logk);所有的链表节点都会被加入和弹出 pq所以算法整体的时间复杂度是 O(Nlogk),其中 k 是链表的条数,N 是这些链表的节点总数

86. 分隔链表

给你一个链表的头节点 head 和一个特定值 x ,请你对链表进行分隔,使得所有小于 x 的节点都出现在 大于或等于 x 的节点之前。

你应当 保留 两个分区中每个节点的初始相对位置。

示例

img
输入:head = [1,4,3,2,5,2], x = 3
输出:[1,2,2,4,3,5]

只需要遍历链表的所有节点,小于x的放到一个小的链表中,大于等于x的放到一个大的链表中,最后再把这两个链表串起来即可。

image.png
class Solution {
public:
    ListNode* partition(ListNode* head, int x) {
        ListNode* smaller = new ListNode(0);
        ListNode* larger = new ListNode(0);
        ListNode* p1 = smaller, * p2 = larger, * p = head;
        while(p){
            if(p->val < x){
                p1->next = p;
                p1 = p1->next;
            }else{
                p2->next = p;
                p2 = p2->next;
            }
            p = p->next;
        }
        p1->next = larger->next;
        p2->next = nullptr; // 注意这一步
        return smaller->next;
    }
};

328. 奇偶链表

给定单链表的头节点 head ,将所有索引为奇数的节点和索引为偶数的节点分别组合在一起,然后返回重新排序的列表。第一个节点的索引被认为是奇数 , 第二个节点的索引为偶数 ,以此类推。

请注意,偶数组和奇数组内部的相对顺序应该与输入时保持一致。你必须在 O(1) 的额外空间复杂度和 O(n) 的时间复杂度下解决这个问题。

示例

img
输入: head = [1,2,3,4,5]
输出: [1,3,5,2,4]

分别维护两个链表,一个链表接入索引为奇数的节点,另一个链表接入索引为偶数的节点,然后组合两个链表。

class Solution {
public:
    ListNode* oddEvenList(ListNode* head) {
        ListNode* odd = new ListNode(0);
        ListNode* even = new ListNode(0);
        ListNode* p1 = odd, * p2 = even, * p = head;
        int index = 1;
        while(p){
            if(index % 2 == 1){
                p1->next = p;
                p1 = p1->next;
            }else{
                p2->next = p;
                p2 = p2->next;
            }
            p = p->next;
            index++;
        }
        p1->next = even->next;
        p2->next = nullptr;
        return odd->next;
    }
};

倒数节点

剑指 Offer 22. 链表中倒数第k个节点

输入一个链表,输出该链表中倒数第k个节点。为了符合大多数人的习惯,本题从1开始计数,即链表的尾节点是倒数第1个节点。

例如,一个链表有 6 个节点,从头节点开始,它们的值依次是 1、2、3、4、5、6。这个链表的倒数第 3 个节点是值为 4 的节点。

示例:

给定一个链表: 1->2->3->4->5, 和 k = 2.
返回链表 4->5.

如何只遍历一次链表,就算出倒数第 k 个节点?可以做到的,如果是面试问到这道题,面试官肯定也是希望你给出只需遍历一次链表的解法。

首先,我们先让一个指针 p1 指向链表的头节点 head,然后走 k 步:

image-20220505145454803

趁这个时候,再用一个指针 p2 指向链表头节点 head,此时若把p1当作链表尾部的NULL,则p2 就是倒数第k个节点。:

image-20220505145900194

那如何让p1成为真正的尾节点而且p1p2之间的位置关系保持不变呢?俩指针一同向后移动呗,直到p1 == NULL为止,此时p1就是真正的倒数第k个节点。

image-20220505150130107

这道题目重点在于理解双指针移动的原理与方法,方法理解了,代码实现就非常简单了。

class Solution {
public:
    ListNode* getKthFromEnd(ListNode* head, int k) {
        ListNode* p1 = head;
        //p1 移动 k 步
        for(int i = 0; i < k; i++){
            p1 = p1->next;
        }
        ListNode* p2 = head;
        //p1 和 p2 一同向后移动
        while(p1){
            p1 = p1->next;
            p2 = p2->next;
        }
        return p2;
    }
};

19. 删除链表的倒数第 N 个结点

给你一个链表,删除链表的倒数第 n 个结点,并且返回链表的头结点。

示例:

img
输入:head = [1,2,3,4,5], n = 2
输出:[1,2,3,5]

要删除倒数第 n 个节点,就得获得倒数第 n + 1 个节点的指针。获取单链表的倒数第 k 个节点,就是想考察 双指针技巧 中快慢指针的运用,一般都会要求只遍历一次链表,就算出倒数第 k 个节点。

注意:使用了虚拟头结点的技巧,也是为了防止出现空指针的情况。比如说链表总共有 5 个节点,题目就让你删除倒数第 5 个节点,也就是第一个节点,那按照算法逻辑,应该首先找到倒数第 6 个节点,但第一个节点前面已经没有节点了,这就会出错。但有了虚拟头节点 dummy 的存在,就避免了这个问题,能够对这种情况进行正确的删除。

class Solution {
public:
    ListNode* removeNthFromEnd(ListNode* head, int n) {
        ListNode* dummy = new ListNode(-1);
        dummy->next = head;
        // 删除倒数第 n 个,要先找倒数第 n + 1 个节点
        ListNode* p = find(dummy, n + 1);
        // 删掉倒数第 n 个节点
        p->next = p->next->next;
        return dummy->next;
    }

    ListNode* find(ListNode* head, int n){
        ListNode* p1 = head;
        for(int i = 0; i < n; i++){
            p1 = p1->next;
        }
        ListNode* p2 = head;
        while(p1){
            p1 = p1->next;
            p2 = p2->next;
        }
        return p2;
    }
};

环形链表

876. 链表的中间结点

题目:给定一个头结点为 head 的非空单链表,返回链表的中间结点。如果有两个中间结点,则返回第二个中间结点。

如果想一次遍历就得到中间节点,就要使用「快慢指针」的技巧:

我们让两个指针 slowfast 分别指向链表头结点 head每当慢指针 slow 前进一步,快指针 fast 就前进两步,这样当 fast 走到链表末尾时,slow 就指向了链表中点

上述思路的代码实现如下:

class Solution {
public:
    ListNode* middleNode(ListNode* head) {
        ListNode* fast = head, * slow = head;
        // 快指针走到末尾时停止
        while(fast && fast->next){
            slow = slow->next;
            fast = fast->next->next;
        }
        return slow;
    }
};

需要注意的是,如果链表长度为偶数,也就是说中点有两个的时候,我们这个解法返回的节点是靠后的那个节点。另外,这段代码稍加修改就可以直接用到判断链表成环的算法题上。

141. 环形链表

给你一个链表的头节点 head ,判断链表中是否有环。如果链表中存在环,则返回 true ; 否则,返回 false

示例:

img
输入:head = [3,2,0,-4], pos = 1
输出:true
解释:链表中有一个环,其尾部连接到第二个节点。

经典题目了,要使用双指针技巧中的快慢指针,每当慢指针 slow 前进一步,快指针 fast 就前进两步。

如果 fast 最终遇到空指针,说明链表中没有环;如果 fast 最终和 slow 相遇,那肯定是 fast 超过了 slow 一圈,说明链表中含有环。

class Solution {
public:
    bool hasCycle(ListNode *head) {
        ListNode* fast = head, * slow = head;
        while(fast && fast->next){
            fast = fast->next->next;
            slow = slow->next;
            // 快慢指针相遇,说明含有环
            if(fast == slow) return true;
        }
        // 不含有环
        return false;
    }
};

142. 环形链表 II

给定一个链表的头节点 head ,返回链表开始入环的第一个节点。 如果链表无环,则返回 null不允许修改 链表。

示例:

img
输入:head = [3,2,0,-4], pos = 1
输出:返回索引为 1 的链表节点
解释:链表中有一个环,其尾部连接到第二个节点。

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

假设快慢指针相遇时,慢指针 slow 走了 k 步,那么快指针 fast 一定走了 2k 步:

image-20220505155433363

fast 一定比 slow 多走了 k 步,这多走的 k 步其实就是 fast 指针在环里转圈圈,所以 k 的值就是环长度的「整数倍」。

假设相遇点距环的起点的距离为 m,那么结合上图的 slow 指针,环的起点距头结点 head 的距离为 k - m,也就是说如果从 head 前进 k - m 步就能到达环起点。

巧的是,如果从相遇点继续前进 k - m 步,也恰好到达环起点。因为结合上图的 fast 指针,从相遇点开始走k步可以转回到相遇点,那走 k - m 步肯定就走到环起点了:

image-20220505155537778

所以,只要我们把快慢指针中的任一个重新指向 head,然后两个指针同速前进,k - m 步后一定会相遇,相遇之处就是环的起点了。

class Solution {
public:
    ListNode *detectCycle(ListNode *head) {
        ListNode* fast = head, * slow = head;
        while(fast && fast->next){
            fast = fast->next->next;
            slow = slow->next;
            if(fast == slow) break;
        }
        // fast 遇到空指针说明没有环
        if(!fast || !fast->next) return NULL;
        // 重新指向头结点
        slow = head;
        // 快慢指针同步前进,相交点就是环起点
        while(slow != fast){
            fast = fast->next;
            slow = slow->next;
        }
        return slow;
    }
};

翻转链表

206. 反转链表

给你单链表的头节点 head ,请你反转链表,并返回反转后的链表。

示例:

img
输入:head = [1,2,3,4,5]
输出:[5,4,3,2,1]

解法一:递归法

class Solution {
public:
    //定义:将以head为起点的链表反转,并返回反转之后的头结点
    ListNode* reverseList(ListNode* head) {
        if(!head || !head->next) return head;
        ListNode* last = reverseList(head->next);
        head->next->next = head;
        head->next = nullptr;
        return last;
    }
};

对于递归算法,最重要的就是明确递归函数的定义。具体来说,我们的 reverse 函数定义是这样的:

输入一个节点 head,将「以 head 为起点」的链表反转,并返回反转之后的头结点

image-20220508092702168

那么输入 reverse(head) 后,会在这里进行递归:

ListNode* last = reverseList(head->next);
image-20220508092851331

reverseList(head->next) 执行完成后,整个链表就成了这样:

image-20220508093035486

并且根据函数定义,reverse 函数会返回反转之后的头结点,我们用变量 last 接收了。

现在再来看下面的代码:

head->next->next = head;
image-20220508093159593

接下来:

head->next = NULL;
return last;
image-20220508093304789

这样整个链表就反转过来了!递归代码就是这么简洁优雅,不过其中有两个地方需要注意:

1、递归函数要有 base case,也就是这句:

if(!head || !head->next) return head;

意思是如果链表为空或者只有一个节点的时候,反转结果就是它自己,直接返回即可。

2、当链表递归反转之后,新的头结点是 last,而之前的 head 变成了最后一个节点,别忘了链表的末尾要指向 null:

head->next = NULL;

理解了这两点后,我们就可以进一步深入了,接下来的问题其实都是在这个算法上的扩展。

解法二:迭代法,双指针法

class Solution {
public:
    ListNode* reverseList(ListNode* head) {
        ListNode* pre = nullptr, * cur = head, * tmp = head;
        while(cur){
            tmp = cur->next;
            cur->next = pre;
            pre = cur;
            cur = tmp;
        }
        return pre;
    }
};

注意:

  • precur指针都要向后移动,所以还要记录cur后一个节点的指针tmp,因为cur->next = pre改变了cur的指向,cur下一个节点无法获得,所以要在该语句之前用tmp提前记录;
  • while循环结束后,cur指向NULLpre指向翻转后链表的头节点,所以要返回pre
  • 还可理解为在[head, NULL)区间(左闭右开)内翻转链表。

92. 反转链表 II

给你单链表的头指针 head 和两个整数 leftright ,其中 left <= right 。请你反转从位置 left 到位置 right 的链表节点,返回 反转后的链表

示例:

img
输入:head = [1,2,3,4,5], left = 2, right = 4
输出:[1,4,3,2,5]

首先思考一下:如何反转链表前 N 个节点

比如说对于下图链表,执行 reverseN(head, 3)

image-20220508094103751

解决思路和反转整个链表差不多,只要稍加修改即可:

ListNode* successor = NULL; // 后驱节点

// 定义:反转以 head 为起点的 n 个节点,返回新的头结点
ListNode* reverseN(ListNode* head, int n) {
    if (n == 1) {
        // 记录第 n + 1 个节点
        successor = head->next;
        return head;
    }
    // 以 head->next 为起点,需要反转前 n - 1 个节点
    ListNode* newHead = reverseN(head->next, n - 1);
    head->next->next = head;
    // 让反转之后的 head 节点和后面的节点连起来
    head->next = successor;
    return newHead;
}

具体的区别:

1、base case 变为 n == 1,反转一个元素,就是它本身,同时要记录后驱节点

2、刚才我们直接把 head->next 设置为 NULL,因为整个链表反转后原来的 head 变成了整个链表的最后一个节点。但现在 head 节点在递归反转之后不一定是最后一个节点了,所以要记录后驱 successor(第 n + 1 个节点),反转之后将 head 连接上。

image-20220508094450123

这个函数能看懂,就离实现「反转一部分链表」不远了。

首先,如果 left == 1,就相当于反转链表开头的 right 个元素,也就是我们刚才实现的功能;

如果 right != 1 怎么办?如果我们把 head 的索引视为 1,那么我们是想从第 left 个元素开始反转;如果把 head->next 的索引视为 1 呢?那么相对于 head->next,反转的区间应该是从第 left - 1 个元素开始的;那么对于 head->next->next 呢……

class Solution {
public:
    ListNode* reverseBetween(ListNode* head, int left, int right) {
        if(left == 1) return reverseN(head, right);
        // 前进到反转的起点触发 base case
        head->next = reverseBetween(head->next, left - 1, right - 1);
        return head;
    }
    
    ListNode* successor = nullptr;
    ListNode* reverseN(ListNode* head, int n){
        if(n == 1){
            successor = head->next;
            return head;
        }
        ListNode* last = reverseN(head->next, n - 1);
        head->next->next = head;
        head->next = successor;
        return last;
    }
};

24. 两两交换链表中的节点

给你一个链表,两两交换其中相邻的节点,并返回交换后链表的头节点。你必须在不修改节点内部的值的情况下完成本题(即,只能进行节点交换)。

示例

img
输入:head = [1,2,3,4]
输出:[2,1,4,3]

解法一:递归法

class Solution {
public:
    // 定义:返回链表节点两两交换后的头节点
    ListNode* swapPairs(ListNode* head) {
        if(!head || !head->next) return head;
        ListNode* newHead = head->next;
        head->next = swapPairs(newHead->next);
        newHead->next = head;
        return newHead;
    }
};

解法二:迭代法

初始时,cur指向虚拟头结点,然后进行如下三步:

image-20220510111404759

打羊胎素展开链表:

image-20220510111531466
class Solution {
public:
    ListNode* swapPairs(ListNode* head) {
        ListNode* dummy = new ListNode(0);
        dummy->next = head;
        ListNode* p = dummy;
        while(p->next && p->next->next){
            ListNode* tmp1 = p->next;
            ListNode* tmp2 = tmp1->next;
            ListNode* tmp3 = tmp2->next;
            p->next = tmp2;
            tmp2->next = tmp1;
            tmp1->next = tmp3;
            p = tmp1;
        }
        return dummy->next;
    }
};

25. K 个一组翻转链表

给你链表的头节点 head ,每 k 个节点一组进行翻转,请你返回修改后的链表。k 是一个正整数,它的值小于或等于链表的长度。如果节点总数不是 k 的整数倍,那么请将最后剩余的节点保持原有顺序。

你不能只是单纯的改变节点内部的值,而是需要实际进行节点交换。

示例:

img
输入:head = [1,2,3,4,5], k = 2
输出:[2,1,4,3,5]

输入 headreverseKGroup 函数能够把以 head 为头的这条链表进行翻转。我们要充分利用这个递归函数的定义,把原问题分解成规模更小的子问题进行求解。

1、先反转以 head 开头的 k 个元素

image-20220508113128936

2、将第 k + 1 个元素作为 head 递归调用 reverseKGroup 函数

image-20220508113301946

3、将上述两个过程的结果连接起来

image-20220508113408376

4、最后函数递归完成之后就是这个结果,完全符合题意:

image-20220508113522192
class Solution {
public:
    ListNode* reverseKGroup(ListNode* head, int k) {
        if(!head) return nullptr;
        ListNode* a = head, * b = head;
        // 区间 [a, b) 包含 k 个待反转元素
        for(int i = 0; i < k; i++){
            // 不足 k 个,不需要反转,base case
            if(!b) return head;
            b = b->next;
        }
        // 反转前 k 个元素
        ListNode* newHead = reverse(a, b);
        // 递归反转后续链表并连接起来
        a->next = reverseKGroup(b, k);
        return newHead;
    }
	// 定义:反转区间 [a, b) 的元素,注意是左闭右开
    ListNode* reverse(ListNode* a, ListNode* b){
        ListNode* pre =nullptr, * cur = a, * nxt = a;
        while(cur != b){
            nxt = cur->next;
            cur->next = pre;
            pre = cur;
            cur = nxt;
        }
        return pre;
    }
};

这里使用迭代法来写翻转链表reverse函数,其中三个指针的移动过程如图所示:

8

61. 旋转链表

给你一个链表的头节点 head ,旋转链表,将链表每个节点向右移动 k 个位置。

img

示例

输入:head = [1,2,3,4,5], k = 2
输出:[4,5,1,2,3]

假设链表的长度为len,为了将链表每个节点向右移动 k 个位置,只需要将链表的后 k % len个节点移动到链表的最前面,然后将链表的后k % len个节点和前 len - k个节点连接到一块即可。

1、首先遍历整个链表,求出链表的长度n,并找出链表的尾节点tail

image-20220510114658985

2、由于k可能很大,所以我们令 k = k % n,然后再次从头节点head开始遍历,找到第n - k个节点p,那么1 ~ p是链表的前 n - k个节点,p+1 ~ n是链表的后k个节点。

image-20220510114840024

3、接下来就是依次执行 tail->next = headhead = p->nextp->next = nullptr,将链表的后k个节点和前 n - k个节点拼接到一块,并让head指向新的头节点p->next,新的尾节点即p节点的next指针指向null

image-20220510115248903

4、最后返回链表的新的头节点head

class Solution {
public:
    ListNode* rotateRight(ListNode* head, int k) {
        if(!head || !k) return head;
        ListNode* p = head;
        int n = 1;
        while(p->next){
            n++;
            p = p->next;
        }
        k = k % n;
        ListNode* tail = head;
        for(int i = 1; i < n - k; i++){
            tail = tail->next;
        }
        p->next = head;
        ListNode* newHead = tail->next;
        tail->next = nullptr;
        return newHead;
    }
};

链表重排

143. 重排链表

给定一个单链表 L 的头节点 head ,单链表 L 表示为:L0 → L1 → … → Ln - 1 → Ln
请将其重新排列后变为:L0 → Ln → L1 → Ln - 1 → L2 → Ln - 2 → …
不能只是单纯的改变节点内部的值,而是需要实际的进行节点交换。

示例

输入:head = [1,2,3,4]
输出:[1,4,2,3]

注意观察链表是怎么变化的,方法:寻找链表中点 + 链表逆序 + 合并链表

目标链表即为将原链表的左半端和反转后的右半端合并后的结果,这样我们的任务即可划分为三步:

  • 找到原链表的中点(参考「876. 链表的中间结点」),使用快慢指针来 O(N) 地找到链表的中间节点。
  • 将原链表的右半端反转(参考「206. 反转链表」),使用迭代法实现链表的反转。
  • 将原链表的两端合并,因为两链表长度相差不超过 1,因此直接合并即可。
image-20220510210809294 image-20220510211017122
class Solution {
public:
    void reorderList(ListNode* head) {
        if(!head) return;
        ListNode* mid = middleNode(head);
        ListNode* l1 = head;
        ListNode* l2 = mid->next;
        mid->next = nullptr;
        l2 = reverse(l2);
        merge(l1, l2);
    }

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

    ListNode* reverse(ListNode* head){
        ListNode* pre = nullptr, * cur = head;
        while(cur){
            ListNode* tmp = cur->next;
            cur->next = pre;
            pre = cur;
            cur = tmp;
        }
        return pre;
    }

    void merge(ListNode* l1, ListNode* l2){
        ListNode* p1, * p2;
        while(l1 && l2){
            p1 = l1->next;
            p2 = l2->next;
            l1->next = l2;
            l1 = p1;
            l2->next = l1;
            l2 = p2;
        }
    }
};

148. 排序链表

给你链表的头结点 head ,请将其按 升序 排列并返回排序后的链表 。

示例

img
输入:head = [4,2,1,3]
输出:[1,2,3,4]

通过递归实现链表归并排序,有以下两个环节:

Picture2.png
class Solution {
public:
    // 定义:排序链表
    ListNode* sortList(ListNode* head) {
        if(!head || !head->next) return head;
        ListNode* mid = middleNode(head);
        ListNode* l1 = head, * l2 = mid->next;
        mid->next = nullptr;
        // 注意:两链表排序之后再合并
        return merge(sortList(l1), sortList(l2));
    }
	// 定义:找链表的中心节点
    ListNode* middleNode(ListNode* head){
        // 注意 fast = head->next,若有两个中心节点则找到靠左的那个
        ListNode* fast = head->next, * slow = head;
        while(fast && fast->next){
            slow = slow->next;
            fast = fast->next->next;
        }
        return slow;
    }
	// 定义:合并两个链表
    ListNode* merge(ListNode* list1, ListNode* list2) {
        if(!list1 && !list2) return list1;
        ListNode* dummy = new ListNode(-1);
        ListNode* p = dummy, *p1 = list1, *p2 = list2;
        while(p1 && p2){
            if(p1->val > p2->val){
                p->next = p2;
                p2 = p2->next;
            }else{
                p->next = p1;
                p1 = p1->next;
            }
            p = p->next;
        }
        p->next = !p1 ? p2 : p1;
        return dummy->next;
    }
};

234. 回文链表

给你一个单链表的头节点 head ,请你判断该链表是否为回文链表。如果是,返回 true ;否则,返回 false

示例:

img
输入:head = [1,2,2,1]
输出:true

双指针技巧,从两端向中间逼近即可:

bool isPalindrome(string s) {
    int left = 0, right = s.length - 1;
    while (left < right) {
        if (s[left] != s[right]) return false;
        left++; right--;
    }
    return true;
}

以上代码很好理解,因为回文串是对称的,所以正着读和倒着读应该是一样的,这一特点是解决回文串问题的关键

解法一:利用后序遍历

如果想正序打印链表中的 val 值,可以在前序遍历位置写代码;反之,如果想倒序遍历链表,就可以在后序遍历位置操作。

class Solution {
public:
    ListNode* left;
    bool res = true;
    bool isPalindrome(ListNode* head) {
        left = head;
        traverse(head);
        return res;
    }

    void traverse(ListNode* right){
        if(!right) return;
        traverse(right->next);
        // 后序遍历位置
        if(left->val != right->val) res = false;
        left = left->next;
    }
};

利用后序遍历,算法的时间和空间复杂度都是 O(N)。能不能不用额外的空间,解决这个问题呢?

解法二:优化空间复杂度

1、先通过 ⌈双指针技巧⌋ 中的快慢指针来找到链表的中点:

image-20220509115340350

2、如果fast指针没有指向null,说明链表长度为奇数,slow还要再前进一步:

image-20220509115524911

3、从slow开始反转后面的链表,现在就可以开始比较回文串了:

image-20220509115621642
class Solution {
public:
    bool isPalindrome(ListNode* head) {
        ListNode* left = head;
        ListNode* right = middleNode(head);
        right = reverseList(right);
        while(right){
            if(left->val != right->val) return false;
            left = left->next;
            right = right->next;
        }
        return true;
    }

    ListNode* middleNode(ListNode* head){
        ListNode* slow = head, * fast = head;
        while(fast && fast->next){
            fast = fast->next->next;
            slow = slow->next;
        }
        // 链表长度为奇数,`slow还要再前进一步
        if(fast) slow = slow->next;
        return slow;
    }

    ListNode* reverseList(ListNode* head) {
        ListNode* pre = nullptr, * cur = head, * tmp = head;
        while(cur){
            tmp = cur->next;
            cur->next = pre;
            pre = cur;
            cur = tmp;
        }
        return pre;
    }
};

算法总体的时间复杂度 O(N),空间复杂度 O(1),已经是最优的了。

另一个版本,找中间节点函数middleNode另一种写法:

class Solution {
public:
    bool isPalindrome(ListNode* head) {
        ListNode* mid = middleNode(head);
        ListNode* left = head, * right = mid->next;
        mid->next = nullptr;
        right = reverse(right);
        while(right){
            if(left->val != right->val) return false;
            left = left->next;
            right = right->next;
        }
        return true;
    }
    
    ListNode* middleNode(ListNode* head){
        ListNode* fast = head->next, * slow = head;
        while(fast && fast->next){
            fast = fast->next->next;
            slow = slow->next;
        }
        return slow;
    }

    ListNode* reverse(ListNode* head){
        ListNode* pre = nullptr, * cur = head, * tmp;
        while(cur){
            tmp = cur->next;
            cur->next = pre;
            pre = cur;
            cur = tmp;
        }
        return pre;
    }
};

两数之和

445. 两数相加 II

给定两个 非空链表 l1l2 来代表两个非负整数。数字最高位位于链表开始位置。它们的每个节点只存储一位数字。将这两数相加会返回一个新的链表。可以假设除了数字 0 之外,这两个数字都不会以零开头。

示例

img
输入:l1 = [7,2,4,3], l2 = [5,6,4]
输出:[7,8,0,7]

本题的主要难点在于链表中数位的顺序与我们做加法的顺序是相反的,为了逆序处理所有数位,可以使用:把所有数字压入栈中,再依次取出相加,计算过程中需要注意进位的情况

class Solution {
public:
    ListNode* addTwoNumbers(ListNode* l1, ListNode* l2) {
        stack<int> s1, s2;
        while(l1){
            s1.push(l1->val);
            l1 = l1->next;
        }
        while(l2){
            s2.push(l2->val);
            l2 = l2->next;
        }
        int carry = 0, cur = 0; //进位,本位
        ListNode* res = nullptr;
        while(!s1.empty() || !s2.empty() || carry){
            int a = s1.empty() ? 0 : s1.top();
            int b = s2.empty() ? 0 : s2.top();
            if(!s1.empty()) s1.pop();
            if(!s2.empty()) s2.pop();
            int sum = a + b + carry;
            carry = sum / 10;
            cur = sum % 10;
            ListNode* node = new ListNode(cur);
            node->next = res;
            res = node;
        }
        return res;
    }
};

2. 两数之和

给你两个 非空 的链表,表示两个非负的整数。它们每位数字都是按照 逆序 的方式存储的,并且每个节点只能存储 一位 数字。请你将两个数相加,并以相同形式返回一个表示和的链表。你可以假设除了数字 0 之外,这两个数都不会以 0 开头。

输入:l1 = [2,4,3], l2 = [5,6,4]
输出:[7,0,8]
解释:342 + 465 = 807.
输入:l1 = [9,9,9,9,9,9,9], l2 = [9,9,9,9]
输出:[8,9,9,9,0,0,0,1]

注意要根据进位的结果,判断是否需要在链表的结尾添加结点。

class Solution {
public:
    ListNode* addTwoNumbers(ListNode* l1, ListNode* l2) {
        ListNode* p1 = l1, * p2 = l2;
        ListNode* dummy = new ListNode(-1);
        ListNode* p = dummy;
        int cur = 0, add = 0;
        while(p1 || p2 || add){
            int val1 = p1 ? p1->val : 0;
            int val2 = p2 ? p2->val : 0;
            if(p1) p1 = p1->next;
            if(p2) p2 = p2->next;
            int sum = val1 + val2 + add;
            cur = sum % 10;
            add = sum / 10;
            p->next = new ListNode(cur);
            p = p->next;
            // 若最后的进位是1,则还要加上去
            if(add == 1){
                p->next = new ListNode(1);
            }
        }
        return dummy->next;
    }
};

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/170083.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

2022 IoTDB Summit:宝武智维徐少锋《Apache IoTDB 在宝武装备远程智能运维平台中的使用案例》...

12 月 3 日、4日&#xff0c;2022 Apache IoTDB 物联网生态大会在线上圆满落幕。大会上发布 Apache IoTDB 的分布式 1.0 版本&#xff0c;并分享 Apache IoTDB 实现的数据管理技术与物联网场景实践案例&#xff0c;深入探讨了 Apache IoTDB 与物联网企业如何共建活跃生态&#…

01【Vector CP】- 详解PN局部网络管理

Partial Networking概述 Partial Networking适用的ECU类型: 总线上依然有通信ECU关闭通信堆栈ECU要求降低功耗 Note:Partial Networking功能及其所有子功能是可选的,必须进行配置。为了控制此类ECU的关断和唤醒,CAN NM提供了一种额外的算法: NM PDU 消息中的用户数据部分…

java命名规范

Java总体命名规范1、项目名全部小写2、包名全部小写3、类名首字母大写&#xff0c;其余组成词首字母依次大写&#xff0c;驼峰命名4、变量名&#xff0c;方法名首字母小写&#xff0c;如果名称由多个单词组成&#xff0c;除首字母外的每个单词的首字母都要大写&#xff0c;保证…

企业数字化转型的关键一步,建立数据意识

数字化给社会带来的巨大的科技革命和产业革命&#xff0c;让数字经济成为新的高速增长的国民经济支柱&#xff0c;让数据变为第五大生产要素&#xff0c;发挥巨大价值&#xff0c;成为企业重要资产。不仅如此&#xff0c;在数字化的影响下&#xff0c;数字化转型成为了个人、机…

ue4.27空项目打包不卡ue5.1空项目打包运行卡的要命研究测试

知乎上有人提问 lumen是如何做到不卡顿的,我却不以为然,我的显卡虽然算不上好显卡,但是也不至于独立显卡一个空项目都玩不起吧,打个cf,玩腾讯模拟器和平精英吃鸡肯定还是会很流畅的. 一个空项目都卡,怎么能说是我电脑配置问题呢??? 再来看提示 image.pngimage.png反射设置的…

2023 过春年,烟花依然了无缘;这能难倒程序猿?一键三连过大年!

效果图镇楼 序 不知道是在什么时候&#xff0c;济南就开始都在传&#xff1a;“今年不再限制放烟花啦&#xff01;”。一些集市上也开始有了售卖烟花的摊子 大家都很兴奋&#xff0c;很多小伙伴开始购买烟花。特别是今年特别火的 “加特林 &#x1f631;” 但是大家兴奋劲还没…

<C++>红黑树

文章目录1. 红黑树的概念2. 红黑树的性质3. 红黑树节点定义4. 红黑树的插入操作5. 红黑树的验证6. 红黑树与AVL树的比较7. 红黑树模拟实现STL中的map与set1. 红黑树的概念 红黑树&#xff0c;是一种二叉搜索树&#xff0c;但在每个结点上增加一个存储位表示结点的颜色&#xf…

小程序管理还能这样做,让小程序管理更高效

说起小程序&#xff0c;作为开发者或者企业用户不得不面临一个问题就是&#xff0c;需要小程序承载的业务越来越多的时候&#xff0c;小程序的数量也呈现增长&#xff0c;随之而来的就是小程序开发、维护等一系列管理中会出现的问题。 包括到小程序的代码包管理、小程序上下架…

Toolwtech Graphics3D.NET 2.0.x专业版Crack

您是否正在寻找一种易于使用、功能强大且 100% 托管的 .NET 组件来快速开发 3D 图形或 3D 数据可视化应用程序&#xff1f;请尝试 Graphics3D.NET。使用 Graphics3D.NET&#xff0c;您可以在几分钟内开发出专业的 3D 可视化应用程序。 Graphics3D.NET 特点&#xff1a; * 100% …

点亮 LED

1.在 Linux 系统下&#xff0c;一切皆文件&#xff01;应用层如何操控底层硬件&#xff0c;同样也是通过文件 I/O 的方式来实现。设备文件通常在/dev/目录下&#xff0c;我们也把/dev 目录下的文件称为设备节点。设备节点并不是操控硬件设备的唯一途径&#xff0c;除此之外&…

Vue模板语法(二)

Vue条件语句 1、v-if、v-else、v-else-if v-if、v-else、v-else-if用于根据条件来渲染某一块的内容&#xff1a; 这些内容只有在条件为true时&#xff0c;才会被渲染出来&#xff1b;这三个指令与JavaScript的条件语句if、else、else if类似&#xff1b; v-if的渲染原理&a…

单片机开发---ESP32-S3模块上手

背景介绍 想起来之前做的半成品单片机游戏机&#xff0c;又想继续做一个&#xff0c;不过之前那个单片机驱动屏幕速率太低&#xff0c;已经无法改进了。所以这次斥巨资购买了一款顶配的ESP32S开发板&#xff0c;做个简单的游戏机&#xff0c;没问题。 完整介绍链接 这花花绿绿…

通过logstash将Redis数据写入ElasticSearch

点击上方蓝字关注我使用logstash将Redis中数据自动同步至ES中1. 部署Redis上传编译好的Redis二级制安装包&#xff0c;使用redis用户启动redis即可具体编译安装过程可参考https://mp.weixin.qq.com/s/RaWy0sqRxcAti1qbv-GbZQ2. 部署logstash下载二进制安装包wget https://ar…

HTTP FLV交互流程及实例解析

HTTP FLV交互流程及实例解析 文章目录HTTP FLV交互流程及实例解析HTTP FLV传输方式HTTP FLV 抓包分析结束语HTTP FLV传输方式 前文已经介绍过&#xff0c;HTTP FLV利用了一个HTTP的协议约定&#xff0c;http 的content-length头字段如果不存在&#xff0c;则客户端就会一直接收…

ESP32-Camera性能(简单)测试评估

TOC 1. ESP32-Camera简介 最近因为接触了ESP32摄像头相关的资料和信息&#xff0c;稍微简单整理下&#xff0c;希望对该方案有兴趣的朋友可以有所帮助。 1.1 资料&信息 The Internet of Things wit ESP32ESP32-S Series开发模组Github: ESP32-Camera 1.2 ESP-EYE摄像头…

别忘记我:通过局部-全局内容建模进行文本擦除方法

本文简要介绍了发表于ECCV 2022的论文“Don’t Forget Me: Accurate Background Recovery for Text Removal via Modeling Local-Global Context”的相关工作。该论文针对文本擦除中存在的复杂背景修复的问题&#xff0c;提出了CTRNet&#xff0c;它利用局部和全局的语义建模提…

一个 go-sql-driver 的离奇 bug

文&#xff5c;郝洪范京东技术专家Seata-go 项目共同发起人微服务底层技术的探索与研究。本文 3482 字 阅读 7 分钟对于 Go CURD Boy 来说&#xff0c;相信 github.com/go-sql-driver/mysql 这个库都不会陌生。基本上 Go 的 CURD 都离不开这个特别重要的库。我们在开发 Seata-g…

LabVIEW将现有数据文件映射至TDMS数据文件格式

LabVIEW将现有数据文件映射至TDMS数据文件格式在某些情况下&#xff0c;可能无法使用TDMS文件格式&#xff0c;例如客户或供应商指定必须使用某种格式存储数据。有些传统仪器可能会自动使用某种自定义格式提供数据输出文件。此外&#xff0c;已经用某种方式收集的传统测量数据无…

PyQt6快速入门-自定义Widget

自定义Widget 文章目录 自定义Widget1、准备工作2、重写paintEvent事件3、Position策略4、更新显示5、绘制条形框5.1 绘制计算5.2 绘制条形框6、自定义样式7、添加鼠标交互能力8、完整代码QPainter是Qt中所有小部件绘制的基础。在本文中,详细介绍如何构建一个全新的自定义 GUI…

vue文本点击样式设置

vue文本点击样式设置嘚吧嘚干就完了光标边小手文本域样式修改hover语法语法一语法二语法三语法四学以致用&#xff0c;效果实现嘚吧嘚 相信当家在写代码的过程中&#xff0c;文本的点击事件是常有的吧&#xff0c;如历史搜索记录、页面跳转等。本次就就分享一下文本点击样式设…