1. 两数相加问题解析与实现
两数相加是LeetCode上经典的链表操作题目,题目要求我们模拟两个数字相加的过程,这两个数字以链表的形式逆序存储。这种数据结构设计非常巧妙,它使得我们从最低位开始相加变得异常简单。
1.1 问题理解与边界条件
题目给定两个非空链表,表示两个非负整数。每个节点存储一位数字,且数字以逆序方式存储。我们需要返回一个新的链表,表示这两个数的和。关键点在于:
- 数字是逆序存储的,所以链表头就是个位数
- 需要考虑进位的情况
- 两个链表长度可能不一致
- 最后可能还有进位需要处理
常见的边界条件包括:
- 一个链表比另一个长很多
- 最后一位相加产生进位
- 其中一个链表为空的情况
1.2 算法设计与实现
最直接的解法是模拟手工加法过程:
- 初始化一个哑节点(dummy node)作为结果链表的头部
- 初始化进位carry为0
- 同时遍历两个链表,将对应节点的值相加,加上carry
- 计算当前位的值和新的进位
- 创建新节点连接到结果链表
- 如果链表遍历完后仍有进位,需要额外创建一个节点
java复制public ListNode addTwoNumbers(ListNode l1, ListNode l2) {
ListNode dummy = new ListNode(0);
ListNode current = dummy;
int carry = 0;
while (l1 != null || l2 != null) {
int x = (l1 != null) ? l1.val : 0;
int y = (l2 != null) ? l2.val : 0;
int sum = x + y + carry;
carry = sum / 10;
current.next = new ListNode(sum % 10);
current = current.next;
if (l1 != null) l1 = l1.next;
if (l2 != null) l2 = l2.next;
}
if (carry > 0) {
current.next = new ListNode(carry);
}
return dummy.next;
}
1.3 复杂度分析与优化
时间复杂度:O(max(m,n)),其中m和n分别是两个链表的长度。我们需要遍历较长的那个链表。
空间复杂度:O(max(m,n)),结果链表的长度最多为max(m,n)+1。
优化空间有限,因为我们必须处理每个节点的值。不过可以尝试以下优化:
- 原地修改较长的链表来节省空间
- 使用递归实现,虽然空间复杂度相同但代码更简洁
提示:在实际面试中,建议先写出基础解法,确保正确性后再讨论优化可能。
2. 排序链表的归并排序实现
排序链表是LeetCode中中等难度的题目,要求我们在O(nlogn)时间复杂度和常数级空间复杂度下对链表进行排序。归并排序是解决这个问题的理想选择。
2.1 归并排序的基本原理
归并排序采用分治策略:
- 分割:找到链表中点,将链表分成两部分
- 递归:递归地对两部分进行排序
- 合并:将两个已排序的链表合并成一个
对于链表来说,归并排序特别适合,因为:
- 链表不需要像数组那样额外的空间来进行分割
- 链表的合并操作可以在O(1)的空间复杂度下完成
2.2 关键实现步骤
2.2.1 使用快慢指针找中点
快慢指针技巧是链表问题中的常用方法:
- 慢指针每次移动一步
- 快指针每次移动两步
- 当快指针到达末尾时,慢指针正好在中点
java复制private ListNode findMiddle(ListNode head) {
ListNode slow = head;
ListNode fast = head.next;
while (fast != null && fast.next != null) {
slow = slow.next;
fast = fast.next.next;
}
return slow;
}
2.2.2 合并两个有序链表
合并操作是归并排序的核心:
- 创建一个哑节点作为结果链表的头部
- 比较两个链表当前节点的值,将较小的连接到结果链表
- 当一个链表遍历完后,将另一个链表的剩余部分直接连接
java复制private ListNode merge(ListNode l1, ListNode l2) {
ListNode dummy = new ListNode(0);
ListNode current = dummy;
while (l1 != null && l2 != null) {
if (l1.val < l2.val) {
current.next = l1;
l1 = l1.next;
} else {
current.next = l2;
l2 = l2.next;
}
current = current.next;
}
current.next = (l1 != null) ? l1 : l2;
return dummy.next;
}
2.3 完整实现与复杂度分析
将上述两部分组合起来:
java复制public ListNode sortList(ListNode head) {
if (head == null || head.next == null) {
return head;
}
// 找到中点并分割链表
ListNode mid = findMiddle(head);
ListNode rightHead = mid.next;
mid.next = null;
// 递归排序
ListNode left = sortList(head);
ListNode right = sortList(rightHead);
// 合并
return merge(left, right);
}
时间复杂度:O(nlogn),标准的归并排序时间复杂度。
空间复杂度:O(logn),递归调用栈的深度。
注意:虽然题目要求常数空间,但递归实现实际上使用了O(logn)的栈空间。要实现真正的常数空间,需要使用自底向上的归并排序,但实现起来更复杂。
3. 二叉树的中序遍历
中序遍历是二叉树最基本的遍历方式之一,按照"左-根-右"的顺序访问节点。对于二叉搜索树,中序遍历可以得到有序的节点序列。
3.1 递归实现
递归实现是最直观的方法:
java复制public List<Integer> inorderTraversal(TreeNode root) {
List<Integer> res = new ArrayList<>();
inorder(root, res);
return res;
}
private void inorder(TreeNode node, List<Integer> res) {
if (node == null) return;
inorder(node.left, res); // 左
res.add(node.val); // 中
inorder(node.right, res); // 右
}
递归实现的优点是代码简洁,易于理解。缺点是当树很深时可能导致栈溢出。
3.2 迭代实现
使用栈来模拟递归过程:
java复制public List<Integer> inorderTraversal(TreeNode root) {
List<Integer> res = new ArrayList<>();
Stack<TreeNode> stack = new Stack<>();
TreeNode curr = root;
while (curr != null || !stack.isEmpty()) {
// 将左子树全部入栈
while (curr != null) {
stack.push(curr);
curr = curr.left;
}
// 访问节点
curr = stack.pop();
res.add(curr.val);
// 转向右子树
curr = curr.right;
}
return res;
}
迭代实现的空间复杂度是O(h),h是树的高度,通常比递归更节省空间。
3.3 Morris遍历
Morris遍历是一种空间复杂度为O(1)的算法,它通过修改树的结构(遍历完成后会恢复)来实现:
java复制public List<Integer> inorderTraversal(TreeNode root) {
List<Integer> res = new ArrayList<>();
TreeNode curr = root;
while (curr != null) {
if (curr.left == null) {
res.add(curr.val);
curr = curr.right;
} else {
// 找到前驱节点
TreeNode prev = curr.left;
while (prev.right != null && prev.right != curr) {
prev = prev.right;
}
if (prev.right == null) {
prev.right = curr; // 建立线索
curr = curr.left;
} else {
prev.right = null; // 删除线索
res.add(curr.val);
curr = curr.right;
}
}
}
return res;
}
Morris遍历虽然节省空间,但理解和实现起来比较复杂,适合对空间要求严格的场景。
4. 二叉树的最大深度
二叉树的最大深度是指从根节点到最远叶子节点的最长路径上的节点数。这是一个基础但重要的二叉树问题。
4.1 递归解法
递归是最直观的解法:
java复制public int maxDepth(TreeNode root) {
if (root == null) {
return 0;
}
int leftDepth = maxDepth(root.left);
int rightDepth = maxDepth(root.right);
return Math.max(leftDepth, rightDepth) + 1;
}
这个解法体现了分治思想:
- 空树的深度为0
- 非空树的深度等于左右子树深度的较大值加1
时间复杂度:O(n),每个节点访问一次
空间复杂度:最坏情况下O(n)(树退化为链表),平均情况下O(logn)
4.2 迭代解法(BFS)
使用广度优先搜索(BFS)也可以计算最大深度:
java复制public int maxDepth(TreeNode root) {
if (root == null) return 0;
Queue<TreeNode> queue = new LinkedList<>();
queue.offer(root);
int depth = 0;
while (!queue.isEmpty()) {
int size = queue.size();
for (int i = 0; i < size; i++) {
TreeNode node = queue.poll();
if (node.left != null) queue.offer(node.left);
if (node.right != null) queue.offer(node.right);
}
depth++;
}
return depth;
}
BFS解法按层遍历树,每处理完一层深度加1。这种解法的空间复杂度取决于树的宽度。
4.3 迭代解法(DFS)
也可以使用深度优先搜索的迭代实现:
java复制public int maxDepth(TreeNode root) {
if (root == null) return 0;
Stack<TreeNode> stack = new Stack<>();
Stack<Integer> depths = new Stack<>();
stack.push(root);
depths.push(1);
int maxDepth = 0;
while (!stack.isEmpty()) {
TreeNode node = stack.pop();
int currentDepth = depths.pop();
maxDepth = Math.max(maxDepth, currentDepth);
if (node.left != null) {
stack.push(node.left);
depths.push(currentDepth + 1);
}
if (node.right != null) {
stack.push(node.right);
depths.push(currentDepth + 1);
}
}
return maxDepth;
}
这种实现使用两个栈,一个存储节点,一个存储对应的当前深度。空间复杂度是O(n)。
实际应用中,递归解法通常是最简洁和高效的,除非树的深度非常大可能导致栈溢出。面试时可以先给出递归解法,然后讨论其他实现方式。