Bootstrap

LeetCode 热题 100 之链表2

1.合并两个有序链表

在这里插入图片描述
思路分析(双指针法):可以使用双指针的方式,逐步将两个链表的节点合并到一个新的链表中。

  • 初始化一个新的链表头节点和一个指向新链表末尾的指针current;
  • 使用两个指针分别指向两个链表的头节点;
  • 比较l1和l2的节点值,将较小值的节点连接到current的后面,然后移动current和选中的节点指针;
  • 当其中一个链表遍历完后,将另一个链表的剩余部分直接连接到新链表后面;
  • 返回dummy->next作为合并后的新链表的头节点。

具体实现代码(详解版):

/**
 * 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(0);//创建一个哨兵节点,简化边界情况
       ListNode* current = &dummy;//指针

       while(list1 && list2){
          if(list1->val < list2->val){
             current->next = list1;
             list1 = list1->next;
          }else{
             current->next = list2;
             list2 = list2->next;
          }
          current = current->next;
       }
       //将剩余的链表加到新链表末尾
       if(list1) current->next = list1;
       else current->next = list2;

       return dummy.next;//实际头节点
    }
};

此算法的时间复杂度为 O ( m + n ) O(m+n) O(m+n),其中m和n分别是l1和l2的长度,空间复杂度 O ( 1 ) . O(1). O(1).,因为外面只使用了常量级别的额外空间。

2.两数相加

在这里插入图片描述
思路分析:要将两个链表表示的数字相加,可以采用逐位相加的方式来实现。链表中的数字是逆序存储的,所以我们从头节点开始,一位一位地相加,并处理进位。

  • 逐位相加
    • 从两个链表的头节点开始,分别取出每个链表对应的节点值相加;
    • 如果链表较短,则该链表的当前节点视为0(表示没有这一位)
  • 处理进位
    • 如果两个节点的值相加(加上前一位的进位)大于等于10,则取个位数作为该位的值,进位保留给下一位;
  • 生成新链表
    • 将每位计算出的值生成新链表的节点并连接到结果链表上
  • 处理剩余进位
    • 最后如果还有进位,则将进位作为新节点追加到结果链表末尾。

具体实现代码(详解版):

class Solution {
public:
    ListNode* addTwoNumbers(ListNode* l1, ListNode* l2) {
        ListNode dummy(0);//哨兵节点
        ListNode* current = &dummy;//指向当前处理节点的指针,从哨兵节点开始
        int carry = 0;//进位初始为0
        
        //遍历两个链表,直到两个链表都遍历完
        while(l1 != nullptr || l2 != nullptr){
            //取出当前节点的值,如果链表已遍历完则视为0
            int x = (l1 != nullptr) ? l1->val : 0;
            int y = (l2 != nullptr) ? l2->val : 0;

            int sum = x + y + carry;//求和,要加上进位哦
            carry = sum / 10;//更新进位 若当前位的和大于等于10则进位1,否则为0
            
            // 创建一个新节点存储当前位的结果,取sum的个位数
            current->next = new ListNode(sum % 10);
            current = current->next;// 将current指针移动到新创建的节点

            // 将l1和l2分别指向下一个节点(若有下一个节点)
            if(l1 != nullptr) l1 = l1->next;
            if(l2 != nullptr) l2 = l2->next;
        }
        //如果还有进位,则添加一个新节点
        if(carry > 0) current->next = new ListNode(carry);

        return dummy.next;//返回结果链表的头节点
    }
};
  • 时间复杂度: O ( m a x ( m , n ) ) O(max(m,n)) O(max(m,n)),其中m和n分别是l1和l2的长度,因为需要遍历两个链表的每个节点
  • 空间复杂度: O ( m a x ( m , n ) ) O(max(m,n)) O(max(m,n)),用于存储结果链表的节点数。

3.删除链表的第n个节点

在这里插入图片描述
在对链表进行操作时,一种常用的技巧是添加一个哑节点(dummy node),它的 next 指针指向链表的头节点。这样一来,我们就不需要对头节点进行特殊的判断了。

例如,在本题中,如果我们要删除节点 y,我们需要知道节点 y 的前驱节点 x,并将 x 的指针指向 y 的后继节点。但由于头节点不存在前驱节点,因此我们需要在删除头节点时进行特殊判断。但如果我们添加了哑节点,那么头节点的前驱节点就是哑节点本身,此时我们就只需要考虑通用的情况即可。

特别地,在某些语言中,由于需要自行对内存进行管理。因此在实际的面试中,对于「是否需要释放被删除节点对应的空间」这一问题,我们需要和面试官进行积极的沟通以达成一致。下面的代码中默认不释放空间。

思路分析1(计算链表长度):

  • 先从头节点开始对链表进行一次遍历,得到链表的长度 L
  • 随后我们再从头节点开始对链表进行一次遍历,当遍历到第 L−n+1 个节点时,它就是我们需要删除的节点。
  • 为了方便删除操作,我们可以从哑节点开始遍历 L−n+1 个节点。当遍历到第 L−n+1 个节点时,它的下一个节点就是我们需要删除的节点,这样我们只需要修改一次指针,就能完成删除操作。
  • 在这里插入图片描述
    具体实现代码(详解版):
class Solution {
public:
    int getLength(ListNode* head){
        int len = 0;
        while(head != nullptr){
            len ++;
            head = head->next;
        } 
        return len;
    }
    ListNode* removeNthFromEnd(ListNode* head, int n) {
       ListNode* dummy = new ListNode(0,head);//创建哨兵节点
       int len = getLength(head);//获取链表的长度
       ListNode* cur = dummy;//当前指针从哨兵节点开始
       //移动指针到倒数第n+1个节点
       for(int i = 1 ; i < len - n + 1 ; i ++){
           cur = cur->next;
       }
       //删除倒数第n个节点
       cur->next = cur->next->next;
       //返回删除节点后的链表头
       ListNode* res = dummy->next;
       delete dummy;//释放哨兵节点
       return res;//返回链表的新头节点
    }
};
  • 时间复杂度:O(L),其中L是链表的长度
  • 空间复杂度:O(1).

思路分析2(栈):我们也可以在遍历链表的同时将所有节点依次入栈。根据栈「先进后出」的原则,我们弹出栈的第 n 个节点就是需要删除的节点,并且目前栈顶的节点就是待删除节点的前驱节点。这样一来,删除操作就变得十分方便了。

  • 创建哨兵节点dummy,值为0,其next指针指向链表的头节点head;
  • 创建一个栈stk来存储链表节点的指针,当前指针指向哨兵节点;
  • 遍历链表,将每个节点的指针压入栈中;
  • 找出倒数第n个节点:弹出栈中的n个节点,这样栈顶指针将指向倒数第n+1个节点;
  • 删除目标节点:只需要让prev指向要删除节点的下一个节点即可。prev->next = prev->next->next;
  • 然后返回链表头,且释放dummy节点的内存
    在这里插入图片描述

具体实现代码(详解版):

ListNode* removeNthFromEnd(ListNode* head, int n) {
    ListNode* dummy = new ListNode(0, head); // 创建哨兵节点
    stack<ListNode*> stk; // 创建一个栈来存储链表节点
    ListNode* cur = dummy; // 当前指针指向哨兵节点

    // 遍历链表,将每个节点压入栈中
    while (cur) {
        stk.push(cur);
        cur = cur->next;
    }

    // 弹出栈中 n 个节点,找到倒数第 n + 1 个节点
    for (int i = 0; i < n; i++) stk.pop();

    // 找到倒数第 n + 1 个节点
    ListNode* prev = stk.top(); // prev 指向要删除节点的前一个节点
    prev->next = prev->next->next; // 删除倒数第 n 个节点

    // 返回删除节点后的链表头
    ListNode* res = dummy->next;
    delete dummy; // 释放哨兵节点
    return res; // 返回链表的新头节点
}

  • 时间复杂度:O(L),其中L是链表的长度
  • 空间复杂度:O(L),其中L是链表的长度,主要为栈的开销。

思路分析3(双指针法)

  • 哨兵节点:dummy初始值为0,指向head
  • 快慢指针
    • first指针先走n+1步,使得first和second指针相距n步
    • 然后同时移动first和second指针,当first到达链表尾部时,second位于待删除节点的前一个节点
  • 删除节点:second->next = second->next->next; 删除倒数第 n 个节点。
  • 返回结果:返回dummy->next,即新的链表头.

具体实现代码(详解版):

/**
 * 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(0,head);

       //定义两个指针,初始都指向哨兵节点
       ListNode* first = dummy;
       ListNode* second = dummy;

       //让first指针先移动n+1步,保证两个指针相距n步
       for(int i = 0 ; i <= n ; i ++) first = first->next;

       //同时移动first和second指针,直到first到达链表末尾
       while(first != nullptr){
           first = first->next;
           second = second->next;
       }

       //此时second指向倒数第n+1个节点,删除其下一个节点
       second->next = second->next->next;

       //返回删除节点后的链表头
       ListNode* res = dummy->next;
       delete dummy;
       return res;
    }
};
  • 时间复杂度:O(L),其中L是链表的长度
  • 空间复杂度:O(1).

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

在这里插入图片描述
思路分析1(递归)

  • 基本情况
  • 确定新头节点
    • 创建一个指针 newHead,指向当前头节点的下一个节点(即 head->next)。这个节点将成为新的头节点。
  • 递归调用
    • 递归调用 swapPairs 函数来处理剩下的节点(即从 newHead->next 开始的子链表)。这个调用将返回处理后的链表
  • 交换节点
    • 更新 head->next 指向递归返回的结果(即 newHead 之后的节点),实现对节点的连接
    • 然后将 newHead->next 指向 head,完成两个节点的交换
  • 返回新头节点
    • 最后返回newhead,作为当前链表的新的头节点。

具体实现代码(详解版):

class Solution {
public:
    ListNode* swapPairs(ListNode* head) {
        // 基础情况:如果链表为空或只有一个节点,直接返回头节点
        if (head == nullptr || head->next == nullptr) {
            return head;
        }
        
        // newHead 是将要交换的第二个节点
        ListNode* newHead = head->next; 
        
        // 递归交换 newHead 之后的节点对
        head->next = swapPairs(newHead->next); 
        
        // 交换头节点和 newHead
        newHead->next = head; 
        
        // 返回新的头节点
        return newHead; 
    }
};

  • 时间复杂度:O(n),其中 n 是链表的节点数量。需要对每个节点进行更新指针的操作。

  • 空间复杂度:O(n),其中 n 是链表的节点数量。空间复杂度主要取决于递归调用的栈空间。

思路分析2(迭代)

  • 虚拟头节点:创建一个虚拟头节点 dummy,使其 next 指向原链表的头节点 head。这有助于处理链表的头节点,避免特殊情况的判断。
  • 指针初始化
    • prev 初始化为虚拟头节点,用于跟踪已经处理的节点。
    • first 和 second 用于指向当前需要交换的两个节点。
  • 循环迭代
    • 使用 while 循环,检查 prev->next 和 prev->next->next 是否为 nullptr,以确保存在两个可交换的节点
    • 在循环中获取当前的 first 和 second 节点。
  • 交换节点
    • 将first的next指向second的下一个节点,这样first就不再指向second
    • second的next指向first,完成了first和second的交换
    • 更新prev->next = seond,指向新的头节点
  • 更新指针:将prev更新为first,以便处理下一个节点对的交换
  • 返回结果
    • 返回虚拟头节点的 next,即新的链表头。
    • 最后释放虚拟头节点的内存。

具体实现代码(详解版):

class Solution {
public:
    ListNode* swapPairs(ListNode* head) {
        // 创建一个虚拟头节点,简化边界条件的处理
        ListNode* dummy = new ListNode(0);
        dummy->next = head;
        
        ListNode* prev = dummy; // 前一个节点
        ListNode* first; // 第一个节点
        ListNode* second; // 第二个节点
        
        // 迭代链表,直到没有更多的节点可以交换
        while (prev->next != nullptr && prev->next->next != nullptr) {
            first = prev->next; // 第一个节点
            second = first->next; // 第二个节点
            
            // 交换节点
            first->next = second->next; // 将第一个节点指向第三个节点
            second->next = first; // 将第二个节点指向第一个节点
            prev->next = second; // 将前一个节点指向新的头节点(第二个节点)
            
            // 更新指针
            prev = first; // 将前一个节点移动到交换后的第一个节点
        }
        
        ListNode* result = dummy->next; // 获取新链表的头节点
        delete dummy; // 释放虚拟节点的内存
        return result; // 返回新链表的头节点
    }
};

  • 时间复杂度:O(n),其中n是链表中节点的数量,每个节点被访问了一次;
  • 空间复杂度:O(1),没有使用额外的空间来存储节点,只使用了常数级别的额外变量
;