面试题24.反转链表
双指针迭代
- 定义两个指针,pre 和 cur,pre 在前 cur 在后
- 定义一个暂存节点 temp,存放 pre 的下一个节点
- 每次让 pre 的 next 指向 cur,实现一次局部反转
- 局部反转完成后,pre 和 cur 同时向前移动一个位置
- 循环上述过程,直至 pre 到达链表尾部
class Solution {
public ListNode reverseList(ListNode head) {
if(head == null || head.next == null) return head;
ListNode pre = head, cur = null, temp = null;
while(pre != null) {
temp = pre.next; //暂存下一节点
pre.next = cur;
cur = pre;
pre = temp;
}
return cur;
}
}
- 时间复杂度 O(N) : 遍历链表使用线性大小时间。
- 空间复杂度 O(1) : 变量 pre 和 cur 使用常数大小额外空间。
递归
- 使用递归函数,一直递归到链表的最后一个节点,该节点就是反转后的头节点,记作 res
- 此后,每次函数在返回的过程中,让当前节点的下一个节点的 next 指针指向当前节点
- 同时让当前节点的 next 指针指向 null,从而实现从链表尾部开始的局部反转
class Solution {
public ListNode reverseList(ListNode head) {
if(head == null || head.next == null) {
return head;
}
//这里的cur就是最后一个节点,也就是反转后的头节点
ListNode cur = reverseList(head.next);
/如果链表是 1->2->3->4->5,那么此时的cur就是5
//而head是4,head的下一个是5,下下一个是空
//所以head.next.next 就是5->4
head.next.next = head;
//防止链表循环,需要将head.next设置为空
head.next = null;
//每层递归函数都返回cur,也就是最后一个节点
return cur;
}
}
- 时间复杂度 O(N) : 遍历链表使用线性大小时间。
- 空间复杂度 O(N): 遍历链表的递归深度达到 N ,系统使用 O(N) 大小额外空间。
————————————————————————————————————————
面试题25.合并两个排序的链表
伪头节点+双指针
解题思路:
- 使用双指针 l1 和 l2遍历两链表,根据 l1.val 和 l2.val 的大小关系确定节点添加顺序
- 引入伪头节点:由于初始状态合并链表中无节点,因此循环第一轮时无法将节点添加到合并链表中。解决方案:初始化一个辅助节点 dum 作为合并链表的伪头节点,将各节点添加至 dum 之后。
算法流程:
-
初始化:伪头节点 dum,节点 cur 指向 dum
-
循环合并:当 l1 或 l2 为空时跳出;
-
合并剩余尾部:跳出时有两种情况,即 l1 为空或 l2 为空
- 1.若 l1 = null:将 l2 添加至节点 cur 之后
- 2.否则:将 l2 添加至节点 cur 之后
class Solution {
public ListNode mergeTwoLists(ListNode l1, ListNode l2) {
ListNode dum = new ListNode(-1);
ListNode cur = dum;
while(l1 != null && l2 != null) {
if(l1.val < l2.val) {
cur.next = l1;
l1 = l1.next;
} else {
cur.next = l2;
l2 = l2.next;
}
cur = cur.next;
}
cur.next = (l1 == null ? l2 : l1);
return dum.next;
}
}
- 时间复杂度 O(M+N) : M, N 分别为链表 l1,l2 的长度,合并操作需遍历两链表。
- 空间复杂度 O(1) : 节点引用 dum , cur 使用常数大小的额外空间。
递归
class Solution {
public ListNode mergeTwoLists(ListNode l1, ListNode l2) {
//递归实现
return recur(l1, l2);
}
//实现 l1 和 l2 的合并,返回合并之后的链表
public ListNode recur(ListNode l1, ListNode l2) {
if(l1 == null && l2 == null) return null;
if(l1 == null) return l2;
if(l2 == null) return l1;
//新建头节点
ListNode head = null;
//如果l1.val <= l2.val,那么头节点的值为l1的值,然后开始递归
if(l1.val <= l2.val) {
head = new ListNode(l1.val);
head.next = recur(l1.next, l2);
}
//否则,头节点的值为l2的值,然后开始递归
else {
head = new ListNode(l2.val);
head.next = recur(l1, l2.next);
}
//返回链表
return head;
}
}
————————————————————————————————————————
面试题26.树的子结构
要判断 B 是否是 A 的子结构,像下面这样,我们只需要从根节点开始判断,通过递归的方式比较他的每一个子节点即可
此逻辑代码为:
public boolean isSubStructure(TreeNode A, TreeNode B) {
//边界条件判断,如果 A 和 B 有一个为空,返回false
if(A == null || B == null) return false;
return isSub(A, B);
}
public boolean isSub(TreeNode A, TreeNode B) {
//这里如果B为空,说明B已经访问完了,确定是A的子结构
if(B == null) return true;
//如果B不为空A为空,或者这两个节点值不同,说明B树不是
//A的子结构,直接返回false
if(A == null || A.val != B.val) return false;
//当前节点比较完之后还要继续判断左右子节点
return isSub(A.left, B.left) && isSub(A.right, B.right);
}
但是不一定从根节点开始,B不光有可能是A的子结构,也有可能是A左子树的子结构或者右子树的子结构,所以如果从根节点判断B不是A的子结构,还要继续判断B是不是A左子树的子结构和右子树的子结构
public boolean isSubStructure(TreeNode A, TreeNode B) {
if (A == null || B == null)
return false;
//先从根节点判断B是不是A的子结构,如果不是在分别从左右两个子树判断,
//只要有一个为true,就说明B是A的子结构,满足以下三种情况之一即可
return isSub(A, B) || isSubStructure(A.left, B) || isSubStructure(A.right, B);
}
boolean isSub(TreeNode A, TreeNode B) {
//这里如果B为空,说明B已经访问完了,确定是A的子结构
if (B == null)
return true;
//如果B不为空A为空,或者这两个节点值不同,说明B树不是
//A的子结构,直接返回false
if (A == null || A.val != B.val)
return false;
//当前节点比较完之后还要继续判断左右子节点
return isSub(A.left, B.left) && isSub(A.right, B.right);
}
- 时间复杂度 O(MN) : 其中 M,N 分别为树 A 和 树 B 的节点数量;先序遍历树 A 占用 O(M) ,每次调用 isSub(A, B) 判断占用 O(N) 。
- 空间复杂度 O(M): 当树 A 和树 B 都退化为链表时,递归调用深度最大。当 M≤N 时,遍历树 A 与递归判断的总递归深度为 M ;当 M>N 时,最差情况为遍历至树 A 叶子节点,此时总递归深度为 M。
————————————————————————————————————————
面试题27.二叉树的镜像
递归
根据二叉树镜像的定义,递归遍历(dfs)二叉树,交换每个节点的左/右子节点
1、终止条件:当节点 root 为空时(即越过叶节点),返回 null;
2、递推工作:
- 1.初始化节点 temp,用于暂存 root 的左子节点;
- 2.开启递归右子节点 mirrorTree(root.right),并将返回值作为 root 的左子节点。
- 3.开启递归左子节点 mirrorTree(temp),并将返回值作为 root 的右子节点。
3、返回值:返回当前节点 root
class Solution {
//该方法的作用:返回 root 的镜像二叉树
public TreeNode mirrorTree(TreeNode root) {
if(root == null) return null;
TreeNode tmp = root.left;
//新的左子树是原先右子树的镜像
root.left = mirrorTree(root.right);
root.right = mirrorTree(tmp);
return root;
}
}
- 时间复杂度 O(N) : 其中 N 为二叉树的节点数量,建立二叉树镜像需要遍历树的所有节点,占用 O(N) 时间。
- 空间复杂度 O(N) : 最差情况下(当二叉树退化为链表),递归时系统需使用 O(N) 大小的栈空间。
辅助栈(队列)
利用栈(或队列)遍历树的所有节点 node,并交换每个 node 的左 / 右子节点
算法流程:
- 1.特例处理:当 root 为空时,直接返回 null
- 2.初始化:栈(或队列),加入根节点 root
- 3.循环交换:当栈 stack 为空时跳出;
- 出栈:记为 node
- 添加子节点:将 node 左和右子节点入栈
- 交换:交换 node 的左 / 右子节点
- 4.返回值:返回根节点 root
class Solution {
public TreeNode mirrorTree(TreeNode root) {
if(root == null) return null;
Stack<TreeNode> stack = new Stack<>();
stack.push(root);
while(!stack.isEmpty()) {
TreeNode node = stack.pop();
if(node.left != null) stack.push(node.left);
if(node.right != null) stack.push(node.right);
TreeNode temp = node.left;
node.left = node.right;
node.right = temp;
}
return root;
}
}
- 时间复杂度 O(N) : 其中 N 为二叉树的节点数量,建立二叉树镜像需要遍历树的所有节点,占用 O(N) 时间。
- 空间复杂度 O(N) : 如下图所示,最差情况下,栈 stack 最多同时存储 (N+1)/2 个节点,占用 O(N) 额外空间。