【经典算法】LeetCode 2两数相加(Java/C/Python3/Go实现含注释说明,中等)

简介: 【经典算法】LeetCode 2两数相加(Java/C/Python3/Go实现含注释说明,中等)
  • 作者简介:阿里非典型程序员一枚 ,记录在大厂的打怪升级之路。 一起学习Java、大数据、数据结构算法(公众号同名
  • ❤️觉得文章还不错的话欢迎大家点赞👍➕收藏⭐️➕评论,💬支持博主,记得点个大大的关注,持续更新🤞
    ————————————————-

题目描述

给定两个非空链表来表示两个非负整数,它们各自的位数是按照逆序的方式存储的,并且它们的每个节点只能存储一位数字。

请你将两个数相加,并以相同形式返回一个表示和的链表。

你可以假设除了数字 0 之外,这两个数都不会以 0 开头。

示例:

输入:(2 -> 4 -> 3) + (5 -> 6 -> 4)

输出:7 -> 0 -> 8

解释:342 + 465 = 807

原题:LeetCode 2

思路及实现

方式一:模拟手工加法

思路

我们可以模拟手工加法的过程。从头开始遍历两个链表,将对应位置的数字相加,并处理进位。如果两个链表的长度不同,则较短的链表后面视为0。

代码实现

Java版本
public class ListNode {
    int val;
    ListNode next;
    ListNode(int x) { val = x; }
}
public ListNode addTwoNumbers(ListNode l1, ListNode l2) {
    // 哑节点,方便处理头节点
    ListNode dummy = new ListNode(0);
    ListNode curr = dummy;
    int carry = 0;
    while (l1 != null || l2 != null) {
        int sum = carry;
        if (l1 != null) {
            sum += l1.val;
            l1 = l1.next;
        }
        if (l2 != null) {
            sum += l2.val;
            l2 = l2.next;
        }
        carry = sum / 10;
        curr.next = new ListNode(sum % 10);
        curr = curr.next;
    }
    // 如果最后还有进位,需要额外添加一个节点
    if (carry > 0) {
        curr.next = new ListNode(carry);
    }
    return dummy.next;
}

说明:

此代码使用了一个哑节点dummy,它的next指向真正的头节点,方便我们处理链表的头节点。在遍历链表时,我们计算当前位置的和sum,包括进位carry。然后将和的个位数创建新的节点,curr指向新的节点继续遍历。最后,如果遍历完两个链表后还有进位,需要额外添加一个节点。

C语言版本
typedef struct ListNode {
    int val;
    struct ListNode *next;
} ListNode;
ListNode* createNode(int val) {
    ListNode* node = (ListNode*)malloc(sizeof(ListNode));
    node->val = val;
    node->next = NULL;
    return node;
}
ListNode* addTwoNumbers(ListNode* l1, ListNode* l2) {
    ListNode* dummy = createNode(0);
    ListNode* curr = dummy;
    int carry = 0;
    while (l1 != NULL || l2 != NULL) {
        int sum = carry;
        if (l1 != NULL) {
            sum += l1->val;
            l1 = l1->next;
        }
        if (l2 != NULL) {
            sum += l2->val;
            l2 = l2->next;
        }
        carry = sum / 10;
        curr->next = createNode(sum % 10);
        curr = curr->next;
    }
    if (carry > 0) {
        curr->next = createNode(carry);
    }
    return dummy->next;
}

说明:

C语言版本与Java版本类似,不过需要注意C语言需要手动分配内存,并且使用malloc来创建新的节点。

Python3版本
class ListNode:
    def __init__(self, x):
        self.val = x
        self.next = None
def addTwoNumbers(l1: ListNode, l2: ListNode) -> ListNode:
    dummy = ListNode(0)
    curr = dummy
    carry = 0
    while l1 or l2:
        sum_val = carry
        if l1:
            sum_val += l1.val
            l1 = ll1.next
        if l2:
            sum_val += l2.val
            l2 = l2.next
        carry = sum_val // 10
        curr.next = ListNode(sum_val % 10)
        curr = curr.next
    if carry > 0:
        curr.next = ListNode(carry)
    return dummy.next

说明:

Python版本的代码结构与其他版本相似,但语法更简洁。在Python中,我们不需要显式地分配内存或管理指针,因为Python会自动处理这些。

Golang版本
package main
import "fmt"
type ListNode struct {
    Val  int
    Next *ListNode
}
func addTwoNumbers(l1 *ListNode, l2 *ListNode) *ListNode {
    dummy := &ListNode{}
    curr := dummy
    carry := 0
    for l1 != nil || l2 != nil {
        sum := carry
        if l1 != nil {
            sum += l1.Val
            l1 = l1.Next
        }
        if l2 != nil {
            sum += l2.Val
            l2 = l2.Next
        }
        carry = sum / 10
        curr.Next = &ListNode{Val: sum % 10}
        curr = curr.Next
    }
    if carry > 0 {
        curr.Next = &ListNode{Val: carry}
    }
    return dummy.Next
}
func main() {
    // 测试代码
    // 创建链表 l1: 2 -> 4 -> 3
    l1 := &ListNode{Val: 2}
    l1.Next = &ListNode{Val: 4}
    l1.Next.Next = &ListNode{Val: 3}
    // 创建链表 l2: 5 -> 6 -> 4
    l2 := &ListNode{Val: 5}
    l2.Next = &ListNode{Val: 6}
    l2.Next.Next = &ListNode{Val: 4}
    // 调用函数
    result := addTwoNumbers(l1, l2)
    // 打印结果
    for result != nil {
        fmt.Print(result.Val, " ")
        result = result.Next
    }
}

说明:

Golang版本同样使用了哑节点,代码结构和思路与其他版本一致。在Go中,我们使用指针来操作链表节点。

复杂度分析

  • 时间复杂度:O(max(m, n)),其中m和n分别为两个链表的长度。我们需要遍历两个链表中的所有节点一次。
  • 空间复杂度:O(max(m, n))。在最坏的情况下,当两个链表的长度不同时,结果链表的长度将等于较长链表的长度,因此需要额外分配相应数量的节点。

方式二:递归

思路

递归方式也可以解决这个问题。我们可以递归地调用addTwoNumbers函数来处理链表的剩余部分,并将结果和进位传递回上一层递归。

代码实现

Java版本
public class ListNode {
    int val;
    ListNode next;
    ListNode(int x) { val = x; }
}
public class Solution {
    public ListNode addTwoNumbers(ListNode l1, ListNode l2) {
        return addTwoNumbersRecursive(l1, l2, 0);
    }
    
    private ListNode addTwoNumbersRecursive(ListNode l1, ListNode l2, int carry) {
        if (l1 == null && l2 == null && carry == 0) {
            return null;
        }
        
        int val1 = (l1 != null) ? l1.val : 0;
        int val2 = (l2 != null) ? l2.val : 0;
        int sum = val1 + val2 + carry;
        
        ListNode newNode = new ListNode(sum % 10);
        newNode.next = addTwoNumbersRecursive(l1 != null ? l1.next : null, l2 != null ? l2.next : null, sum / 10);
        
        return newNode;
    }
}

说明:

如果其中一个链表为空,直接返回另一个链表。计算当前节点的和,并创建新的头节点。递归地处理链表剩余部分,并将结果连接到新头节点。如果当前和大于等于10,则处理进位。

C语言版本
#include <stdio.h>
#include <stdlib.h>
struct ListNode {
    int val;
    struct ListNode *next;
};
struct ListNode* addTwoNumbers(struct ListNode* l1, struct ListNode* l2) {
    struct ListNode* head = NULL;
    struct ListNode** node = &head;
    int carry = 0;
    while (l1 || l2 || carry) {
        int sum = carry;
        if (l1) {
            sum += l1->val;
            l1 = l1->next;
        }
        if (l2) {
            sum += l2->val;
            l2 = l2->next;
        }
        carry = sum / 10;
        *node = malloc(sizeof(struct ListNode));
        (*node)->val = sum % 10;
        (*node)->next = NULL;
        node = &((*node)->next);
    }
    return head;
}

说明:

在C语言中,初始化头结点和当前处理结点:使用head变量来记录结果链表的头部,用node指针变量指向当前链表节点的地址,初始指向头结点地址。

遍历两个链表:当l1、l2或carry(进位)非空时,循环继续。每一步计算当前和及进位。

更新节点与进位:分别从l1和l2取值相加,再加上之前的进位值。计算后得到新的进位值和当前节点的值。

构建结果链表:为当前和的余数创建新节点,连接到结果链表末尾。

返回结果:返回头结点head。

C++版本

C++的实现更为简洁,主要是因为C++具备自动内存管理和类的特性,使得代码更易写和理解。

#include <iostream>
class ListNode {
public:
    int val;
    ListNode *next;
    ListNode(int x) : val(x), next(NULL) {}
};
ListNode* addTwoNumbers(ListNode* l1, ListNode* l2) {
    ListNode *head = new ListNode(0), *node = head;
    int carry = 0;
    while (l1 || l2 || carry) {
        int sum = (l1 ? l1->val : 0) + (l2 ? l2->val : 0) + carry;
        carry = sum / 10;
        node->next = new ListNode(sum % 10);
        node = node->next;
        
        if (l1) l1 = l1->next;
        if (l2) l2 = l2->next;
    }
    ListNode *toDelete = head;
    head = head->next;
    delete toDelete; // 删除哑节点
    return head;
}

说明

使用哑节点:这里创建了一个哑节点head来简化边界条件处理,便于在链表前添加节点。实际返回的是head->next。

处理两个链表和进位:循环继绀直到l1、l2和carry都处理完毕。在每一步,计算当前位置的值和新的进位。

添加新节点:根据每一步计算的结果创建新节点并链接到结果链表。

清理哑节点:在返回结果前,删除初始化时创建的哑节点。

Python3

在Python中,可以使用递归实现链表的相加。Python中链表节点通常通过类来实现,下面是一个示例:

class ListNode:
    def __init__(self, val=0, next=None):
        self.val = val
        self.next = next
def addTwoNumbers(l1, l2, carry=0):
    if not l1 and not l2 and not carry:
        return None
    val1 = l1.val if l1 else 0
    val2 = l2.val if l2 else 0
    carry, out = divmod(val1 + val2 + carry, 10)
    currentNode = ListNode(out)
    currentNode.next = addTwoNumbers(l1.next if l1 else None, l2.next if l2 else None, carry)
    return currentNode
Go语言

在Go语言中,链表节点通常通过结构体来实现,下面是一个递归实现链表相加的示例:

type ListNode struct {
    Val int
    Next *ListNode
}
func addTwoNumbers(l1 *ListNode, l2 *ListNode) *ListNode {
    return addTwoNumbersRecursive(l1, l2, 0)
}
func addTwoNumbersRecursive(l1 *ListNode, l2 *ListNode, carry int) *ListNode {
    if l1 == nil && l2 == nil && carry == 0 {
        return nil
    }
    
    val1, val2 := 0, 0
    if l1 != nil {
        val1 = l1.Val
    }
    if l2 != nil {
        val2 = l2.Val
    }
    sum := val1 + val2 + carry
    
    newNode := &ListNode{Val: sum % 10}
    if l1 != nil || l2 != nil || sum >= 10 {
        if l1 != nil {
            l1 = l1.Next
        }
        if l2 != nil {
            l2 = l2.Next
        }
        newNode.Next = addTwoNumbersRecursive(l1, l2, sum/10)
    }
    return newNode
}

说明

请注意,在Go语言中,通常使用指针来操作链表节点,以便能够修改链表的结构。在上面的代码中,addTwoNumbers 函数接收两个 *ListNode 类型的参数,即链表的头节点指针,并返回一个新的链表头节点指针。

递归函数addTwoNumbers计算当前节点的和,并递归地处理剩余节点。在递归调用中,我们传递了下一个节点的指针,并在每个递归步骤中处理当前节点的和。carry函数用于处理链表尾部可能出现的进位。

printList函数用于打印链表,它遍历链表并打印每个节点的值,直到链表末尾。 在main函数中,我们创建了两个示例链表l1l2,然后调用addTwoNumbers函数计算它们的和,并使用printList`函数打印结果链表。

复杂度分析

  • 时间复杂度:O(max(m, n)),其中m和n分别为两个链表的长度。每个节点最多被遍历一次。
  • 空间复杂度:O(max(m, n))。由于使用了递归,函数调用的深度最坏情况下是链表的长度。因此,递归会占用与链表长度相同的栈空间。注意,这并不包括创建结果链表时分配的额外空间。如果只考虑栈空间,复杂度为O(max(m, n));但如果同时考虑结果链表,则空间复杂度仍然是O(max(m, n))。

总结

方式 优点 缺点 时间复杂度 空间复杂度 其他
迭代方式 - 直观易懂,易于实现 - 代码相对较长 O(max(m, n)) O(max(m, n)) m和n分别为两个链表的长度
递归方式 - 代码简洁,易于理解 - 可能存在栈溢出风险(链表过长时) O(max(m, n)) O(max(m, n))(递归栈空间)或 O(1)(不考虑递归栈) 递归深度受链表长度限制

相似题目

相似题目 难度 链接
leetcode 445. 两数相加 II 中等 力扣-445
leetcode 67. 二进制求和 简单 力扣-67
leetcode 369. 给定数字的频率将数字排序 中等 力扣-369
leetcode 349. 两个数组的交集 简单 力扣-349

这些相似题目涉及到加法运算、链表操作、数组操作等,对于理解链表相加问题及其变种有一定的帮助。通过解决这些相似题目,可以加深对链表、数组等数据结构以及加法运算的理解和应用。

欢迎一键三连(关注+点赞+收藏),技术的路上一起加油!!!代码改变世界

  • 关于我:阿里非典型程序员一枚 ,记录在大厂的打怪升级之路。 一起学习Java、大数据、数据结构算法公众号同名),回复暗号,更能获取学习秘籍和书籍等
相关文章
|
4天前
|
Python
Python Google风格注释详解
Google风格注释是Python代码注释的一种标准化格式,它提供了一种规范的注释格式,使得代码更加易读、易于维护。Google风格注释使用三个双引号来包围注释内容,并按照一定规范编写。在注释中使用动词短语来描述函数的行为,并使用被动语态。在注释中使用正确的标点符号和缩进,使得注释易于阅读和理解。通过使用Google风格注释,我们可以为代码提供清晰的文档和说明,使得代码更加易读、易于维护。
10 3
|
4天前
|
Python
Python reStructuredText风格注释详解
reStructuredText风格注释是Python代码注释的一种标准化格式,它提供了一种规范的注释格式,使得代码更加易读、易于维护。reStructuredText风格注释使用两个等号来包围注释标题,并按照一定规范编写。通过使用reStructuredText风格注释,我们可以为代码提供清晰的文档和说明,使得代码更加易读、易于维护。
9 2
|
5天前
|
Java 开发工具 开发者
【编程语言】Python平台化为何比Java差?
【编程语言】Python平台化为何比Java差?
12 1
|
8天前
|
Java Go C#
编程语言C#、C++、Java、Python、go 选择哪个好?
我想说的是,不论选择哪种编程语言,决定选择的都是你最终的目的,做选择之前,先充分调研每一个选择项,再做选择思路就会非常清晰了。
25 3
|
8天前
|
存储 算法 调度
力扣中级算法(Python)
力扣中级算法(Python)
|
8天前
|
算法 Python
力扣初级算法(Python)(二)
力扣初级算法(Python)(二)
|
13天前
|
SQL 算法 数据可视化
LeetCode题目92:反转链表ll 【python 递归与迭代方法全解析】
LeetCode题目92:反转链表ll 【python 递归与迭代方法全解析】
|
13天前
|
SQL 算法 数据可视化
LeetCode题目99:图解中叙遍历、Morris遍历实现恢复二叉树搜索树【python】
LeetCode题目99:图解中叙遍历、Morris遍历实现恢复二叉树搜索树【python】
|
13天前
|
存储 算法 数据可视化
python多种算法对比图解实现 验证二叉树搜索树【力扣98】
python多种算法对比图解实现 验证二叉树搜索树【力扣98】
|
13天前
|
Java 开发者 Python
Java开发者的Python快速进修指南:函数基础
【6月更文挑战第5天】本文探讨了Python函数与Java方法的区别。Python函数使用`def`关键字声明,无需修饰符,参数支持默认值和可变参数,可通过关键字指定顺序。Java则无默认参数,且需按顺序传递。Python函数可返回多个值,而Java需封装为对象。文中还介绍了Python的可变参数(*numbers)、关键字参数(**info)及内置函数如`range`、`zip`、`all`和`any`的用法。