LeetCode:经典题之21、24 题解及延伸

系列目录

88.合并两个有序数组
52.螺旋数组
567.字符串的排列
643.子数组最大平均数

150.逆波兰表达式
61.旋转链表
160.相交链表
83.删除排序链表中的重复元素

389.找不同
1491.去掉最低工资和最高工资后的工资平均值
896.单调序列

206.反转链表
92.反转链表II

141.环形链表
142.环型链表

21.合并两个有序列表
24.两辆交换链表中的节点

876.链表的中间节点
143. 重排链表

2.两数相加
445.两数相加II



⚠️小tips

一般处理链表问题

相比迭代,递归算法更容易想到,但空间复杂度更高

而迭代仅需常数级的空间复杂度≈O(1),但有时会有些费脑子

21. 合并两个有序列表

🌟链表+递归+迭代

原题链接


C++
若未特殊标明,以下题解均写用C++

方法一 递归
/**
 * 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) {}
 * };
 */

// 宏定义没有 ;分号
#define list1 l1
#define list2 l2

class Solution {
public:
    ListNode* mergeTwoLists(ListNode* list1, ListNode* list2) {
        if (l1 == nullptr) 
            return l2;
        
        else if (l2 == nullptr)
            return l1;
        
        else if (l1->val <= l2->val) {
            // 比较 l1->next 和 刚刚参与比较的 l2
            l1->next = mergeTwoLists(l1->next, l2);
            // 还要继续利用原 l1找到l1.next,所以后返回 l1
            return l1;
        } else {
            // 同理
            l2->next = mergeTwoLists(l2->next, l1);
            return l2;
        }
    }
};

方法二 迭代
/**
 * 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) {}
 * };
 */

// 注意顺序不要反了,原名在前
 # define list1 l1
 # define list2 l2
class Solution {
public:
    ListNode* mergeTwoLists(ListNode* list1, ListNode* list2) {
        // 定义一个哨兵💂 快速返回合并后的链表
        // 不妨定义值为 -1 
        ListNode* dummyHead = new ListNode(-1);
        // 维护(preserve)一个 prev指针,让其移动
        ListNode* prev = dummyHead;
        while (l1 != nullptr && l2 != nullptr) {
            if (l1->val <= l2->val) {
                prev->next = l1;
                l1 = l1->next;
            } else {
                prev->next = l2;
                l2 = l2->next;
            }

            // 调整 prev的位置
            prev = prev->next;
        }

        // l1 或 l2 定会有一个先指向空
        // 三元条件运算符
        prev->next = l1 == nullptr ? l2 : l1;

        return dummyHead->next;
    }
};

有关 三元条件运算符和 哨兵(哑节点/虚拟头节点)
可参考这篇文章





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

🌟链表+递归+迭代

原题链接


C++
若未特殊标明,以下题解均写用C++

方法一 递归
/**
 * 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* swapPairs(ListNode* head) {
        // 相邻节点两两交换,原链表第2个节点即新链表的第1个节点
        // 而newHead->next 又恰为除去这两个节点的链表的头节点
        // 且swapParis() 的变量是链表的 头节点
        // 故可以递归地完成两两交换

        // 如果链表长度为 0或1,无法交换——不如说无需交换
        if (head == nullptr || head->next == nullptr)
            return head;
        
        ListNode *newHead = head->next;
        head->next = swapPairs(newHead->next);
        newHead->next = head;

        // newHead 为交换后链表的头节点
        return newHead;
    }
};

方法二 迭代
/**
 * 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* swapPairs(ListNode* head) {
        // new一个 虚拟头节点
        ListNode* dummy = new ListNode(0);
        dummy->next = head;
        // 定义临时(temporary)节点 temp,记录尚未处理好的链表的头节点
        ListNode* temp = dummy;
        while (temp->next != nullptr && temp->next->next != nullptr) {
            // 定义 node1(节点1) 和 node2
            ListNode* node1 = temp->next;
            ListNode* node2 = temp->next->next;
            temp->next = node2;
            node1->next = node2->next;
            // 用完再更新 node2
            node2->next = node1;

            // 记得更新 temp
            // 指针跟着节点走的
            temp = node1;
        }
        // 记得删除这个 dummy
        ListNode *res = dummy->next;
        delete dummy;

        return res;
    }
};

内存泄漏 (Memory Leak)

指程序在申请内存后,未能正确地释放,导致系统内存的浪费;还有可能导致程序崩溃乃至系统可用内存耗尽

对于任何动态内存分配的问题,处理好内存泄漏是非常必要的

为了避免内存泄漏,可以采取以下措施:

  1. 确保每个new都有对应的delete:每当使用new来创建一个节点时,都应该确保在适当的时候使用delete来释放它 这通常发生在节点不再需要时,例如在删除节点或链表被销毁时
  2. 使用智能指针:C++11及以后的版本引入了智能指针 (如unique_ptrshared_ptr ),它们可以自动管理内存,并在适当的时候释放它 使用智能指针可以大大简化内存管理,并减少内存泄漏的风险
  3. 避免野指针:野指针是指已经被释放但仍然被引用的指针 如果试图通过野指针访问内存或释放已经释放的内存,都会导致不可预测的行为,包括内存泄漏 因此,应该确保在释放内存后立即将指针设置为nullptr,以避免野指针的问题
  4. 使用RAII (Resource Acquisition Is Initialization )原则:RAII是一种编程技术,它要求将资源的生命周期与对象的生命周期绑定在一起 通过这种方法,可以确保在对象不再需要时自动释放其占用的资源,包括内存
  5. 进行内存泄漏检测:使用工具 (如Valgrind、AddressSanitizer等 )来检测内存泄漏 这些工具可以帮助你发现潜在的内存泄漏问题,并提供有关如何修复它们的建议





递归和迭代

迭代( Iteration)

迭代是通过重复执行代码块来处理数据集合或任务的一种方法

在C++中,我们通常使用循环结构来实现迭代或是处理链表问题等


递归( Recursion)

递归是一种函数通过调用自身来解决问题的编程技术

递归函数通常有两个基本部分:

  1. 基本情况( Base Case):确定何时停止递归
  2. 递归步骤( Recursive Step):将问题分解为更小的部分,并调用自身来处理这些部分

AcWing.741斐波那契数列

输入整数 N,求出斐波那契数列中的第 N 项是多少
斐波那契数列的第 0 项是 0,第 1 项是 1,从第 2 项开始的每一项都等于前两项之和

输入格式

第一行包含整数 T,表示共有 T 个测试数据

接下来 T 行,每行包含一个整数 N


输出格式

每个测试数据输出一个结果,每个结果占一行,

结果格式为 Fib(N) = x,其中 N 为项数,x 为第 N 项的值


数据范围

0≤N≤60


输入样例:

3
0
4
2

输出样例:

Fib(0) = 0
Fib(4) = 3
Fib(2) = 1
#include <iostream>

using namespace std;

int main()
{   
    
    // 数组的前两项值定义为 0 和 1
    long long f[61] = {0, 1};
    // f[0] = 0, f[1] = 1;
    for (int i = 2; i <= 60; i ++) {
        f[i] = f[i - 1] + f[i - 2];
    }
    
    int n;
    cin >> n;
    
    while (n --) {
        int x;
        cin >> x;
        
        // 别忘了这里是 lld
        printf("Fib(%d) = %lld\n", x, f[x]);
    }
    
    return 0;
}

迭代与递归的比较

  • 效率:在某些情况下,迭代可能比递归更高效,因为递归可能会导致大量的函数调用和栈空间使用
    然而,对于某些问题( 如树形结构的遍历),递归可能更直观和易于理解
  • 内存使用:递归可能会使用更多的栈空间,因为每次函数调用都会将局部变量和返回地址推入栈中
    而迭代通常只需要固定数量的变量来存储状态
  • 适用性:不是所有问题都适合使用递归解决 对于可以转换为循环结构的问题,迭代通常是更好的选择
    然而,对于某些问题( 如分治算法),递归可能是更自然和更简洁的解决方案
  • 可读性:递归代码通常更简洁和易于理解( 尤其是对于具有递归性质的问题),但也可能导致更深的调用栈和更复杂的调试过程 迭代代码通常更直观,但可能需要更多的代码行来实现相同的功能
  • 23
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值