面试题23:链表中环的入口节点
① 使用哈希表(用时6ms)
- 将不重复的非空节点存入哈希表,第一个重复的节点就是环的入口节点。
- 代码如下:
public ListNode detectCycle(ListNode head) {
HashMap<ListNode, Integer> map = new HashMap<>();
while (head != null) {
if (map.containsKey(head)) {
// 第一个重复的节点,环的入口
return head;
}
map.put(head, 1);
head = head.next;
}
return null;//没有环,返回null
}
② 快慢指针(竟然用时0ms)
- 思路分析:
- 判断链表中是否有环: 通过快慢指针,如果链表中有环,则快慢指针会在非null节点相遇;否则,快指针到达null节点,慢指针没有与快指针相遇。
- 获取环的长度: 固定快指针不动,慢指针移动
n
步再次与快指针相遇,则环的长度为n
。 - 快慢指针都回到链表头节点,快指针先走n步。然后快慢指针一起走,每次只走一步,最后相遇的节点就是环的入口节点。
- 代码如下:
public ListNode detectCycle(ListNode head) {
if (head == null) {
return null;
}
// 利用快慢指针,先判断链表是否有环
ListNode fast = head.next, slow = head;
boolean cycled = true;
while (slow != fast) {
// 如果slow等于fast说明有环
if (fast == null || fast.next == null) {
cycled = false;
break;
}
fast = fast.next.next;
slow = slow.next;
}
if (!cycled) {
return null;
}
// 有环,求环的长度
int len = 1;
slow = slow.next;
while (slow != fast) {
len++;
slow = slow.next;
}
// 都回到头节点,查找入口节点
fast = head;
slow = head;
// 快指针先后len步
while (len > 0) {
fast = fast.next;
len--;
}
// 快慢指针一起走
while (slow != fast) {
slow = slow.next;
fast = fast.next;
}
return slow;
}
变形 —— leetcode:环形链表I
- 求链表的入口节点中,通过快慢指针判断链表是否有环。
- 代码如下:
public boolean hasCycle(ListNode head) {
if (head == null) {
return false;
}
ListNode fast = head.next, slow = head;
while (slow != fast) {
// slow和fast指向一样,说明存在环
if (fast == null || fast.next == null) {
// 遇到null节点,说明不是环形链表
return false;
}
fast = fast.next.next;
slow = slow.next;
}
return true;
}
变形 —— leetcode160:相交链表
- 两次遍历:
- 第一次遍历,分别获取每个链表的长度。
- 让较长链表的指针先走
m - n
步,这时两个链表的指针同时移动。如果存在交叉,则会在交叉点相遇;否则,都会到达各自的null节点。
- 两次遍历的代码:
public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
int len1 = 0, len2 = 0;
// 获取链表A和链表B的长度
ListNode p = headA, q = headB;
while (p != null) {
len1++;
p = p.next;
}
while (q != null) {
len2++;
q = q.next;
}
// 长的链表先走
p = headA;
q = headB;
if (len1 > len2) {
int diff = len1 - len2;
while (diff > 0) {
p = p.next;
diff--;
}
} else {
int diff = len2 - len1;
while (diff > 0) {
q = q.next;
diff--;
}
}
// 两个链表同时走
while (p != q) {
p = p.next;
q = q.next;
}
return p;
}
- 走两条链表: 每条链表的指针从自身头节点开始走到null节点,然后接着走另一条链表。如果存在交叉,则在交叉节点相遇;否则,各自走到null节点。
- 特殊情况: 在没有切换到另外一条链表前,就出现了指向相同节点的情况。要么头节点到交叉节点的长度一致,要么没有交叉且头节点到尾结点的长度一致。
- 代码如下:
public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
ListNode p = headA, q = headB;
while (p != q) {
p = (p != null) ? p.next : headB;
q = (q != null) ? q.next : headA;
}
return p;
}
面试题24:反转链表
- 思路:
- 创建为
null
的prev节点,指想新链表的头节点。 - 然后用cur指针指向原链表的头节点,依次遍历链表中的节点;将cur指向的节点的
next
指向prev,并更新prev。 - 为了避免cur节点的
next
节点断开,需要创建临时指针指向cur的next
节点,完成当前节点的转置后,更新cur指针。
- 注意: 由于
while
循环的条件是cur != null
,并且prev初始化为null
,因此不需要检测head是否为null,可以直接将head作为cur指针。 - 代码如下:
public ListNode reverseList(ListNode head) {
ListNode prev = null;
while (head != null) {
ListNode temp = head.next;
head.next = prev;
prev = head;
head = temp;
}
return prev;
}
面试题25:合并两个有序链表
① 迭代
- 头部对齐,谁的节点值更小,谁先加入合并后的链表中。
- 特殊情况:
- 如果某一个为
null
,合并后的链表就是另外一个链表,这时可以直接返回。 - 就算两个链表一样长,也会出现有个链表有剩余的情况,不需要单独添加
null
节点,直接将剩余的链表添加到合并聊表的尾部即可。
- 代码如下,由于代码的设计,我们不需要处理
null
的特殊情况,因为head.next
默认是null。
public ListNode mergeTwoLists(ListNode l1, ListNode l2) {
ListNode head = new ListNode(0);
ListNode p = head;// 指向最新的节点,防止head更改指向
while (l1 != null && l2 != null) {
// 头部对齐,合并链表
if (l1.val < l2.val) {
p.next = l1;
l1 = l1.next;
} else {
p.next = l2;
l2 = l2.next;
}
p = p.next;
}
// 链表有剩余,就算是两个链表一样长,也会有个链表有剩余
if (l1 != null) {
p.next = l1;
}
if (l2 != null) {
p.next = l2;
}
return head.next;
}
② 递归
- 每次传入两个链表,如果一个null,直接返回另一个;否则根据值的大小,插入新的节点,递归进行下一次合并。
- 直接将当前较小值的指针作为新的头节点,头节点的next是递归合并的结果,返回该头节点。
- 代码如下:
public ListNode mergeTwoLists(ListNode l1, ListNode l2) {
if (l1 == null) {
return l2;
}
if (l2 == null) {
return l1;
}
if (l1.val < l2.val) {
l1.next = mergeTwoLists(l1.next, l2);
return l1;
} else {
l2.next = mergeTwoLists(l1, l2.next);
return l2;
}
}
③ 变形 —— leetcode23:k个有序链表的合并
- 按列比较,不借助优先级队列:
- 每次都遍历所有链表,按列比较找到具有最小值的链表下标。
- 如果所有的链表都为null,则停止按列比较。
- 代码如下,花费
352ms
。时间复杂度 O ( m n ) O(mn) O(mn),n
表示链表个数,m
表示最长的链表长度。
public ListNode mergeKLists(ListNode[] lists) {
ListNode head = new ListNode(0);
ListNode p = head;
/*if(lists.length == 1){
return lists[0];
}*/
while (true) {
boolean flag = true; // 是否所有链表为null
int min = Integer.MAX_VALUE;// 记录按列比较中的最小值节点
int min_index = 0;
for (int i = 0; i < lists.length; i++) {
if (lists[i] != null) {
if (lists[i].val < min) {
min = lists[i].val;
min_index = i;
}
flag = false;// 存在非null链表
}
}
if (flag) {
// 所有链表为null,停止按列比较
break;
}
// 存在最小值节点,更新相关指针
p.next = lists[min_index];
p = p.next;
lists[min_index] = lists[min_index].next;
}
// 其实可以不用指定,因为最后加入的节点的next指针一定是null
// 就算没有任何节点加入,head的next默认为null
p.next = null;
return head.next;
}
- 效率优化: 如果只有一个链表不用通过按列比较合并,直接返回结果即可,只减少了
12ms
。 - 借助优先级队列,实现自动比较节点大小,每次获取队列中的头节点即可。
- 代码如下,运行时间6ms。
public ListNode mergeKLists(ListNode[] lists) {
ListNode head = new ListNode(0);
ListNode p = head;
PriorityQueue<ListNode> queue = new PriorityQueue<>(new Comparator<ListNode>() {
@Override
public int compare(ListNode o1, ListNode o2) {
return o1.val - o2.val;
}
});
for (int i = 0; i < lists.length; i++) {
// 预先加入第一列的节点
if (lists[i] != null) {
queue.add(lists[i]);
}
}
while (!queue.isEmpty()) {
ListNode temp = queue.poll();
p.next = temp;
p = p.next;
// 只要被加入节点的next不为null,更新链表的指向
if (temp.next != null) {
queue.add(temp.next);
}
}
// 其实可以不用指定,因为最后加入的节点的next指针一定是null
// 就算没有任何节点加入,head的next默认为null
p.next = null;
return head.next;
}
面试题26:树的子结构
- 根据题意,子结构并不是子树,子结构的要求更低,只要是原树的某部分即可。
- 上图中,右边的树是左边树的子结构,但不是子树!
- 思路:
- 递归判断是够存在子结构,如果其中一棵树的root为null,说明不存在子结构;否则,递归检查当前root的子结构是否一致;
- 如果当前root的子结构不一致,则检查左子树的子结构;还不一致则检查右子树的子结构。
- 判断是否为子结构:判断两个节点的值是否一致且左子树也一致且右子树也一致;如果树B的子节点为null,说明已经检查完毕,直接返回true;如果树A的节点为null,说明树B还有子节点,不是树A的子结构。
- 注意:
- 以root1为基准进行子树判断,可能需要访问root1的左右孩子节点。 如果root1位null,就应该停止比较了
- 题目中规定,如果root2位null,不是任何树的子结构,也可以停止比较。
- 代码:以root1为基准进行子树判断,可能需要访问root1的左右孩子节点。
public boolean HasSubtree(TreeNode root1, TreeNode root2) {
if (root1 == null || root2 == null) {
return false;
}
// 先判断从root开始是否为子结构,如果不是再判断左子树;
// 还不是,则继续判断右子树;巧用||符号的短路原则解决if ... else条件判断
return isSubTree(root1, root2) || HasSubtree(root1.left, root2) ||
HasSubtree(root1.right, root2);
}
public boolean isSubTree(TreeNode root1, TreeNode root2) {
if (root2 == null) {
// 树B遍历完成
return true;
}
if (root1 == null) {
// 树A没有子节点了,树B还有
return false;
}
return (root1.val == root2.val) && isSubTree(root1.left, root2.left) &&
isSubTree(root1.right, root2.right);
}
变形 —— leetcode572:另一个树的子树
- 该题中
s
和t
都不为null
,根据一个树的子结构:
- 先设计一个辅助方法
isSame()
用于判断当前s的子树
和t
是否相同。 - 在原有函数中先调用
isSame()
判断根节点开始的子树和t
是否相同,如果不相同,才递归调用原函数判断s的左子树
和t
是否相同;最后,在判断s的右子树
和t
是否相同。 - 因为
s
和t
都不为null
原本以为无需判断s
和t
是否为null的情况,但是以s为基准进行递归遍历,必须判断s
为null的情况。因为可能调用其左子树或右子树。
- 代码如下:
public boolean isSubtree(TreeNode s, TreeNode t) {
// 虽然s和t都非空,以s为基准递归遍历,可能出现s为null 的情况
if (s==null){
return false;
}
// 先判断s和t,不是子树接着判断s.left和t;否则,接着判断s.right和t
return isSame(s, t) || isSubtree(s.left, t) || isSubtree(s.right, t);
}
public boolean isSame(TreeNode s, TreeNode t) {
// 从当前节点起,递归判断是否为子树
if (s == null && t == null) {
return true;
}
if (s == null || t == null) {
return false;
}
return (s.val == t.val) && isSame(s.left, t.left) && isSame(s.right, t.right);
}