本篇为链表部分的白银挑战难度,会学习到链表相关的一些经典问题的解决办法。
在开始正式内容之前,补充上篇没有发现的一个细节。今天翻群聊天记录的时候,发现一个网名为“真椛难寻”同学发布的公众号,发现他写的博客带有很多自己的理解,比我只是根据学习内容重新转述一遍要强不少。其中我发现了一个关于链表的细节,就是每次涉及到遍历链表的时候,都需要用一个 ListNode 类的对象 current 引用来获取头节点,切记不能直接操作头节点 head ,否则一旦找不到头节点,我们的链表就被破坏了。
一、寻找两个链表的第一个公共子节点
思路:将常用的数据结构和算法思想都想一遍,最终得出一个或者若干个解题思路,并得出最优解。
1.1 哈希
将其中一个链表存入HashMap,ListNode的节点对象作为HashMap的key值存储,再遍历另一个链表,使用 hashMap.containsKey()方法检查HashMap中是否存在指定的键,遍历到第一个存在相同的键值,即跳出遍历,return该节点。
public static ListNode findFirstCommonNodeByMap(ListNode pHead1, ListNode pHead2) {
if (pHead1 == null || pHead2 == null) {
return null;
}
ListNode current1 = pHead1;
ListNode current2 = pHead2;
HashMap<ListNode, Integer> hashMap = new HashMap<ListNode, Integer>();
while (current1 != null) {
hashMap.put(current1, null);
current1 = current1.next;
}
while (current2 != null) {
if (hashMap.containsKey(current2)) {
return current2;
}
current2 = current2.next;
}
return null;
}
1.2 Set集合
思路与哈希差不多,使用set.add()方法将第一个链表的每一个节点放入,再遍历第二个链表,使用set.contains()方法,检查set集合中是否包含第二个链表的节点,最后return这个节点。
public static ListNode findFirstCommonNodeBySet(ListNode headA, ListNode headB) {
Set<ListNode> set = new HashSet<>();
while (headA != null) {
set.add(headA);
headA = headA.next;
}
while (headB != null) {
if (set.contains(headB)) {
return headB;
}
headB = headB.next;
}
return null;
}
1.3 栈
栈的特点是先进后出,可以把他想象成放羽毛球的圆筒,最先放进去的只能最后拿出来。因此,我们将两个链表的每一个节点都分别放入两个栈对象stackA和stackB中,使用stack.peek()方法比较两个栈对象最后放入的节点是否相同,如果相同,则使用stack.pop()方法将最后放入的节点删除,并返回栈顶节点(用一个对象存储下来),当两个栈的顶层节点不相等,结束循环,并返回之前最后一次获得的栈顶元素。
public static ListNode findFirstCommonNodeByStack(ListNode headA, ListNode headB) {
Stack<ListNode> stackA = new Stack();
Stack<ListNode> stackB = new Stack();
while (headA != null) {
stackA.push(headA);
headA = headA.next;
}
while (headB != null) {
stackB.push(headB);
headB = headB.next;
}
ListNode preNode = null;
while (stackB.size() > 0 && stackA.size() > 0) {
if (stackA.peek() == stackB.peek()) {
preNode = stackA.pop();
stackB.pop();
} else {
break;
}
}
return preNode;
}
1.4 拼接字符串
假设有两个链表,链表A的节点为:1 - 2 - 3 - 4 - 5 - 6,链表B的节点为a - b - 4 - 5 - 6,用链表A拼链表B,得到的结果是1 - 2 - 3 - 4 - 5 - 6 - a - b - 4 - 5 - 6,用链表B后面拼上链表A,得到的结果是a - b - 4 - 5 - 6 - 1 - 2 - 3 - 4 - 5 - 6,分别遍历两个拼接以后的链表,就会发现最后几个节点都是一样的,即可得出相同的节点。
接下来就是对使用内存空间优化一下,不要创建两个新的链表,而是在一个链表遍历完以后,直接赋值为另外一个链表,即一个链表遍历完以后,接入另一个链表进行遍历。
public static ListNode findFirstCommonNodeByCombine(ListNode pHead1, ListNode pHead2) {
if (pHead1 == null || pHead2 == null) {
return null;
}
ListNode p1 = pHead1;
ListNode p2 = pHead2;
while (p1 != p2) {
p1 = p1.next;
p2 = p2.next;
if (p1 != p2) {
if (p1 == null) {
p1 = pHead2;
}
if (p2 == null) {
p2 = pHead1;
}
}
}
return p1;
}
接下来我们解释一下,循环体里为什么需要加一个判断if (p1 != p2) 。简单来说,如果序列不存在交集的时候陷入死循环,例如 list1是1 2 3,list2是4 5 ,很明显,如果不加判断,list1和list2会不断循环,出不来。
1.5 差和双指针
通过观察,可以发现两个链表从功公共节点开始后面的节点都是一样的,也就是只要将前面的节点统一遍历后,就可以得到第一个公共节点。但是链表长度并不相同,所以我们第一步就是要获得两个链表的长度,这边称两个长度为l1和l2,然后计算两个链表长度的差值|L2-L1|,让长度更长的链表先遍历|L2-L1|位的长度,此时剩下的长度与短的链表长度一样了,就开始同时遍历两个链表,直到某个点,得到的节点一样,就可以得到结果了。
public static ListNode findFirstCommonNodeBySub(ListNode pHead1, ListNode pHead2) {
if (pHead1 == null || pHead2 == null) {
return null;
}
ListNode current1 = pHead1;
ListNode current2 = pHead2;
int l1 = 0, l2 = 0;
while (current1 != null) {
current1 = current1.next;
l1++;
}
while (current2 != null) {
current2 = current2.next;
l2++;
}
current1 = pHead1;
current2 = pHead2;
int sub = l1 > l2 ? l1 - l2 : l2 - l1;
if (l1 > l2) {
int a = 0;
while (a < sub) {
current1 = current1.next;
a++;
}
}
if (l1 < l2) {
int a = 0;
while (a < sub) {
current2 = current2.next;
a++;
}
}
while (current2 != current1) {
current2 = current2.next;
current1 = current1.next;
}
return current1;
}