两个链表第一个公共子节点
力扣(LeetCode)官网 - 全球极客挚爱的技术成长平台
单链表中每个节点只能指向唯一的下一个next,但是可以有多个指针指向一个节点。例如上面c1就可以被a2,b3同时指向。该怎么入手呢?如果一时想不到该怎么办呢?
告诉你一个屡试不爽的方法:将常用数据结构和常用算法思想都想一遍,看看哪些能解决问题。
常用的数据结构有数组、链表、队、栈、Hash、集合、树、堆。常用的算法思想有查找、排序、双指针、递归、迭代、分治、贪心、回溯和动态规划等等。
首先想到的是蛮力法,类似于冒泡排序的方式,将第一个链表中的每一个结点依次与第二个链表的进行比较,当出现相等的结点指针时,即为相交结点。虽然简单,但是时间复杂度高,排除!
再看Hash,先将第一个链表元素全部存到Map里,然后一边遍历第二个链表,一边检测当前元素是否在Hash中,如果两个链表有交点,那就找到了。OK,第二种方法出来了。既然Hash可以,那集合呢?和Hash一样用,也能解决,OK,第三种方法出来了。
队列和栈呢?这里用队列没啥用,但用栈呢?现将两个链表分别压到两个栈里,之后一边同时出栈,一边比较出栈元素是否一致,如果一致则说明存在相交,然后继续找,最晚出栈的那组一致的节点就是要找的位置,于是就有了第四种方法。
哈希
public 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;
}
栈
这里需要使用两个栈,分别将两个链表的结点入两个栈,然后分别出栈,如果相等就继续出栈,一直找到最晚出栈的那一组。这种方式需要两个O(n)的空间
public 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;
}
判断链表是否为回文序列
力扣(LeetCode)官网 - 全球极客挚爱的技术成长平台
方法1:将链表元素都赋值到数组中,然后可以从数组两端向中间对比。这种方法会被视为逃避链表,面试不能这么干。
方法2:将链表元素全部压栈,然后一边出栈,一边重新遍历链表,一边比较两者元素值,只要有一个不相等,那就不是。
方法3:优化方法2,先遍历第一遍,得到总长度。之后一边遍历链表,一边压栈。到达链表长度一半后就不再压栈,而是一边出栈,一边遍历,一边比较,只要有一个不相等,就不是回文链表。这样可以节省一半的空间。
方法4:优化方法3:既然要得到长度,那还是要遍历一次链表才可以,那是不是可以一边遍历一边全部压栈,然后第二遍比较的时候,只比较一半的元素呢?也就是只有一半的元素出栈, 链表也只遍历一半,当然可以。
方法5:反转链表法, 先创建一个链表newList,将原始链表oldList的元素值逆序保存到newList中,然后重新一边遍历两个链表,一遍比较元素的值,只要有一个位置的元素值不一样,就不是回文链表。
方法6:优化方法5,我们只反转一半的元素就行了。先遍历一遍,得到总长度。然后重新遍历,到达一半的位置后不再反转,就开始比较两个链表。
方法7:优化方法6,我们使用双指针思想里的快慢指针 ,fast一次走两步,slow一次走一步。当fast到达表尾的时候,slow正好到达一半的位置,那么接下来可以从头开始逆序一半的元素,或者从slow开始逆序一半的元素,都可以。
方法8:在遍历的时候使用递归来反转一半链表可以吗?当然可以,再组合一下我们还能想出更多的方法,解决问题的思路不止这些了,此时单纯增加解法数量没啥意义了。
栈
public boolean isPalindrome(ListNode head) {
ListNode temp = head;
Stack<Integer> stack = new Stack();
// 把链表节点的值放入栈中
while(temp != null) {
stack.push(temp.val);
temp = temp.next;
}
// 然后一边出栈一边比较
while(head != null) {
if(head.val != stack.pop()) {
return false;
}
head = head.next;
}
return true;
}
合并有序链表
合并两个有序链表
力扣(LeetCode)官网 - 全球极客挚爱的技术成长平台
逐一比较两个链表中每个元素的大小,并按照顺序插入新的链表中,最后将其返回
public ListNode mergeTwoLists(ListNode list1, ListNode list2) {
ListNode preHead = new ListNode(-1);
ListNode pre = preHead;
while (list1 != null && list2 != null) {
if (list1.val <= list2.val) {
pre.next = list1;
list1 = list1.next;
}else {
pre.next = list2;
list2 = list2.next;
}
pre = pre.next;
}
// 最多只有一个没有被合并,这时候判断并合并
pre.next = list1 == null ? list2 : list1;
return preHead.next;
}
合并 K 个链表
先将前两个合并,然后将后面的逐步合并进来(这里只展示比较简单的方法)
public ListNode mergeKLists(ListNode[] lists) {
ListNode res = null;
for (ListNode list: lists) {
res = mergeTwoList(res, list);
}
return res;
}
public ListNode mergeTwoList(ListNode list1, ListNode list2) {
ListNode preHead = new ListNode(-1);
ListNode pre = preHead;
while (list1 != null && list2 != null) {
if (list1.val <= list2.val) {
pre.next = list1;
list1 = list1.next;
}else {
pre.next = list2;
list2 = list2.next;
}
pre = pre.next;
}
pre.next = list1 == null ? list2 : list1;
return preHead.next;
}
一道很无聊的题
力扣(LeetCode)官网 - 全球极客挚爱的技术成长平台
根据遍历下标找到目标链表节点,然后让其指向第二个链表的头节点和尾节点
class Solution {
public ListNode mergeInBetween(ListNode list1, int a, int b, ListNode list2) {
ListNode pre = list1, post1 = list1, post2 = list2;
int i = 0, j =0;
while(j < b && pre != null && post1 != null) {
if(i != a-1) {
pre = pre.next;
i++;
}
if(j != b) {
post1 = post1.next;
j++;
}
}
while(post2.next != null) {
post2 = post2.next;
}
pre.next = list2;
post2.next = post1.next;
return list1;
}
}
双指针
寻找中间节点
力扣(LeetCode)官网 - 全球极客挚爱的技术成长平台
创建快慢指针,快指针一次走两步,慢指针一次走一步。这样,当快指针到达尾部时,慢指针一定是在链表中间,这时候将链表返回就行了
class Solution {
public ListNode middleNode(ListNode head) {
ListNode fast = head;
ListNode slow = head;
while(fast != null && fast.next != null) {
fast = fast.next.next;
slow = slow.next;
}
return slow;
}
}
寻找倒数第k个元素
输入一个链表,输出该链表中倒数第k个节点。本题从1开始计数,即链表的尾节点是倒数第1个节点。
示例
给定一个链表: 1->2->3->4->5, 和 k = 2.
返回链表 4->5.
跟前面的题差不多,使用快慢指针,先将fast指针向后遍历 k+1 个节点,slow 仍指向链表的第一个节点,这时候 fast 和 slow 两个指针之间整好间隔 k 个节点,然后两个指针同时向后走,直到 fast 指针走到空节点时,slow 指针整好指向链表的倒数第 k 个节点
public ListNode getKthFromEnd(ListNode head, int k) {
ListNode fast = head;
ListNode slow = head;
while (fast != null && k > 0) {
fast = fast.next;
k--;
}
while (fast != null) {
fast = fast.next;
slow = slow.next;
}
return slow;
}
旋转链表
力扣(LeetCode)官网 - 全球极客挚爱的技术成长平台
我们可以将链表分为两个部分,后面 k 个元素为一部分,然后将这部分拼接到头部,就实现整体移动
k 的大小可能会超过链表,所以这里需要使用取余的思想:k = k % count
class Solution {
public ListNode rotateRight(ListNode head, int k) {
if(head == null || k == 0) {
return head;
}
ListNode temp = head;
ListNode fast = head;
ListNode slow = head;
int len = 0;
// 计算链表长度
while(head != null) {
head = head.next;
len++;
}
// 移动长度等于链表长度
if(k % len == 0) {
return temp;
}
// 使用取模,是为了防止k大于len
while((k % len) > 0) {
k--;
fast = fast.next;
}
// 快慢指针一起移动
while(fast.next != null) {
fast = fast.next;
slow = slow.next;
}
ListNode res = slow.next;
slow.next = null;
fast.next = temp;
return res;
}
}
删除链表元素
如果按照LeetCode顺序一道道刷题,会感觉毫无章法而且很慢,但是将相似类型放在一起,瞬间就发现不过就是在改改条件不断造题。我们前面已经多次见证这个情况,现在集中看一下与链表删除相关的问题。如果在链表中删除元素搞清楚了,一下子就搞定8道题,是不是很爽?
- LeetCode 237:删除某个链表中给定的(非末尾)节点。传入函数的唯一参数为要被删除的节点。
- LeetCode 203:给你一个链表的头节点 head 和一个整数 val ,请你删除链表中所有满足 Node.val == val 的节点,并返回新的头节点 。
- LeetCode 19. 删除链表的倒数第 N 个节点。
- LeetCode 1474. 删除链表 M 个节点之后的 N 个节点。
- LeetCode 83 存在一个按升序排列的链表,请你删除所有重复的元素,使每个元素只出现一次。
- LeetCode 82 存在一个按升序排列的链表,请你删除链表中所有存在数字重复情况的节点,只保留原始链表中没有重复出现的数字。
我们在链表基本操作部分介绍了删除的方法,至少需要考虑删除头部,删除尾部和中间位置三种情况的处理。而上面这些题目就是这个删除操作的进一步拓展。
删除特定节点
力扣(LeetCode)官网 - 全球极客挚爱的技术成长平台 完整的步骤是:
- 1.我们创建一个虚拟链表头dummyHead,使其next指向head。
- 2.开始循环链表寻找目标元素,注意这里是通过cur.next.val来判断的。
- 3.如果找到目标元素,就使用cur.next = cur.next.next;来删除。
- 4.注意最后返回的时候要用dummyHead.next,而不是dummyHead。
代码:
class Solution {
public ListNode removeElements(ListNode head, int val) {
ListNode dummyNode = new ListNode(-1);
dummyNode.next = head;
ListNode cur = dummyNode;
while(cur.next != null) {
if(cur.next.val == val) {
cur.next = cur.next.next;
}else{
cur = cur.next;
}
}
return dummyNode.next;
}
}
删除倒数第n个节点
力扣(LeetCode)官网 - 全球极客挚爱的技术成长平台
关键字倒数,一般碰到这种题型就用快慢指针来找到那个元素,fast 需要先走 k+1 布,再同步 slow,两个指针一起移动,直到 fast == null
什么时候需要用到虚拟头节点呢?当头节点可能会被改变或者涉及的时候,就需要头节点,比如这一题,头节点也可能是需要删除的节点,因此使用虚拟头节点
class Solution {
public ListNode removeNthFromEnd(ListNode head, int n) {
ListNode demo = new ListNode(0);
demo.next = head;
ListNode fast = demo;
ListNode slow = demo;
for(int i=0; i<n;i++){
fast = fast.next;
}
while(fast.next != null) {
fast = fast.next;
slow = slow.next;
}
slow.next = slow.next.next;
return demo.next;
}
}
删除重复元素
重复元素保留一个
力扣(LeetCode)官网 - 全球极客挚爱的技术成长平台
我们从指针 cur 指向链表的头节点,随后开始对链表进行遍历。如果当前 cur 与cur.next 对应的元素相同,那么我们就将cur.next 从链表中移除;否则说明链表中已经不存在其它与cur 对应的元素相同的节点,因此可以将 cur 指向 cur.next。当遍历完整个链表之后,我们返回链表的头节点即可
class Solution {
public ListNode deleteDuplicates(ListNode head) {
if(head == null) {
return head;
}
ListNode cur = head;
while(cur.next != null) {
if(cur.val == cur.next.val) {
cur.next = cur.next.next;
}else {
cur = cur.next;
}
}
return head;
}
}
重复元素都不要
力扣(LeetCode)官网 - 全球极客挚爱的技术成长平台
当一个都不要时,链表只要直接对cur.next 以及 cur.next.next 两个node进行比较就行了,这里要注意两个node可能为空,稍加判断就行了
class Solution {
public ListNode deleteDuplicates(ListNode head) {
if(head == null) {
return head;
}
ListNode dummy = new ListNode(0,head);
ListNode cur = dummy;
while(cur.next != null && cur.next.next != null) {
if(cur.next.val == cur.next.next.val) {
int x = cur.next.val;
while(cur.next != null && cur.next.val == x) {
cur.next = cur.next.next;
}
}else {
cur = cur.next;
}
}
return dummy.next;
}
}
再论第一个公共子节点问题
拼接两个字符串
先看下面的链表A和B:
A: 0-1-2-3-4-5
B: a-b-4-5
如果分别拼接成AB和BA会怎么样呢?
AB:0-1-2-3-4-5-a-b-4-5
BA:a-b-4-5-0-1-2-3-4-5
我们发现拼接后从最后的4开始,两个链表是一样的了,自然4就是要找的节点,所以可以通过拼接的方式来寻找交点。这么做的道理是什么呢?
我们可以从几何的角度来分析。我们假定A和B有相交的位置,以交点为中心,可以将两个链表分别分为left_a和right_a,left_b和right_b这样四个部分,并且right_a和right_b是一样的,这时候我们拼接AB和BA就是这样的结构:
我们说right_a和right_b是一样的,那这时候分别遍历AB和BA是不是从某个位置开始恰好就找到了相交的点了?
这里还可以进一步优化,如果建立新的链表太浪费空间了,我们只要在每个链表访问完了之后,调整到一下链表的表头继续遍历就行了,于是代码就出来了:
public class Solution {
public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
if(headA == null || headB == null) {
return null;
}
ListNode p1 = headA;
ListNode p2 = headB;
while(p1 != p2) {
p1 = p1.next;
p2 = p2.next;
if(p1 != p2) {
if(p1 == null) {
p1 = headB;
}
if(p2 == null) {
p2 = headA;
}
}
}
return p1;
}
}
循环体里为什么需要加一个判断if (p1 != p2) 。简单来说,如果序列不存在交集的时候陷入死循环,例如 list1是1 2 3,list2是4 5 ,很明显,如果不加判断,list1和list2会不断循环,出不来。
差和使用双指针
假如公共子节点一定存在第一轮遍历,假设La长度为L1,Lb长度为L2.则|L2-L1|就是两个的差值。第二轮遍历,长的先走 L2-L1 ,然后两个链表同时向前走,结点一样的时候就是公共结点了。
public class Solution {
public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
if(headA == null || headB == null) {
return null;
}
ListNode current1=headA;
ListNode current2=headB;
int l1=0,l2=0;
//分别统计两个链表的长度
while(current1!=null){
current1=current1.next;
l1++;
}
while(current2!=null){
current2=current2.next;
l2++;
}
current1=headA;
current2=headB;
int sub=l1>l2?l1-l2:l2-l1;
//长的先走sub步
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;
}
}