1. 两个链表的第一个公共子节点问题
输入两个链表,找出它们的第一个公共节点。例如下面的两个链表:
两个链表的头结点都是已知的,相交之后成为一个单链表,但是相交的位置未知,并且相交之前的结点数也是未知的,请设计算法找到两个链表的合并点。
1.1 哈希和集合
先将一个链表元素全部存到Map里, 然后一边遍历第二个第二个链表, 一边检测Hash中是否存在当前节点, 如果有交点, 那么一定能够检测出来。对于本题, 使用集合更加适合, 代码也更加整洁。
/**
* 使用集合找出两个链表的第一个公共子节点
* @param headA
* @param headB
* @return
*/
public ListNode findFirstCommonNodeBySet(ListNode headA, ListNode headB) {
Set<ListNode> set = new HashSet<>();
// 先遍历链表A, 将所有节点保存到set集合中
while (headA != null) {
set.add(headA);
headA = headA.next;
}
// 再遍历链表B, 判断set集合中是否包含链表B的节点
while (headB != null) {
if (set.contains(headB)) {
return headB;
}
headB = headB.next;
}
return null;
}
1.2 使用栈
分别将两个链表压入两个栈中, 然后同时出栈, 如果相同就继续出栈, 一直找到最晚出栈的那一组。
图示:
代码实现:
/**
* 使用栈找出两个链表的第一个公共子节点
* @param headA
* @param headB
* @return
*/
public 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 (stackA.size() > 0 && stackB.size() > 0) {
if (stackA.peek() == stackB.peek()) {
preNode = stackA.pop();
stackB.pop();
}else {
break;
}
}
return preNode;
}
1.3 拼接两个字符串
将链表A和B拼接形成两个新链表, 新链表形式为AB和BA, 通过这种方式消除了两个链表非公共部分长度差, 分别遍历两个链表, 出现的第一个相同节点即为第一个公共子节点。
图示:
代码实现:
/**
* 思路: 将两个链表拼接成AB和BA形式
* 通过拼接消除非公共部分长度差 (因为两个链表有公共子节点, 拼接后, 新形成的两个链表长度一致, 所以链表的公共部分就都到了后面)
* @param headA
* @param headB
* @return
*/
public ListNode findFirstCommonNode(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;
}
1.4 差和双指针
思路和拼接字符串方法类似, 只不过该方法不是消除非公共部分长度差, 而是先让长的那个链表先遍历两个链表的长度差, 再同时遍历, 第一个相同的节点即为第一个公共子节点。
/**
* 方法5:通过差值来实现
*
* @param pHead1
* @param pHead2
* @return
*/
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;
}
2. 判断两个链表是否为回文链表
示例:
输入: 1->2->2->1
输出: true
2.1 使用栈实现
将链表元素全部压栈, 然后一边出栈, 一边重新遍历链表, 比较两者元素值, 只要有一个不相等, 就不是回文链表。
图示:
代码实现:
/**
* 全部压栈
*
* @param head
* @return
*/
public static boolean isPalindromeByAllStack(ListNode head) {
ListNode temp= head;
Stack<Integer> stack = new Stack<>();
while (temp != null) {
stack.push(temp.val);
temp = temp.next;
}
while (stack.size() > 0) {
if (head.val != stack.pop()) {
return false;
}
temp = head.next;
}
return true;
}
优化:
先得到链表的总长度, 之后一边遍历链表, 一边压栈。到达链表长度一半后就不再压栈, 而是一边出栈, 一边继续遍历链表, 同时比较两者元素值, 只要有一个不相等, 就不是回文链表, 这样可以节省一半的空间。
/**
* 只将一半的数据压栈
*
* @param head
* @return
*/
public static boolean isPalindromeByHalfStack(ListNode head) {
if (head == null) {
return true;
}
ListNode temp = head;
Stack<Integer> stack = new Stack<>();
// 链表长度
int length = 0;
while (temp != null) {
// 将链表节点的值压入栈中
stack.push(temp.val);
temp = temp.next;
length++;
}
// 链表长度除以2
length >>= 1;
while (length-- >= 0) {
if (head.val != stack.pop()) {
return false;
}
head = head.next;
}
return true;
}
2.2 反转链表法
将原链表逆序保存到一个新链表中, 然后重新一边遍历两个链表,一遍比较元素的值,只要有一个位置的元素值不一样,就不是回文链表。
/**
* 反转链表法
*
* @param head
* @return
*/
public static boolean isPalindromeByReverse(ListNode head) {
// 将链表反转
ListNode prev = null;
ListNode curr = head;
while (curr != null) {
ListNode temp = curr.next;
curr.next = prev;
prev = curr;
curr = temp;
}
// 遍历两个链表并比较元素值
while (prev != null && head != null) {
if (prev.val != head.val) {
return false;
}
prev = prev.next;
head = head.next;
}
return true;
}
优化:
使用快慢指针, 快指针一次走两步, 慢指针一次走一步。当快指针到达表尾的时候, 慢指针正好到达一半的地方, 那么接下来可以从头开始逆序一半的元素,或者从slow开始逆序一半的元素,都可以。
/**
* 通过双指针的方式来判断
*
* @param head
* @return
*/
public static boolean isPalindromeByTwoPoints(ListNode head) {
if (head == null || head.next == null) {
return true;
}
// 定义快慢指针
ListNode fast = head, slow = head;
ListNode curr = head, prev = null;
while (fast != null && fast.next != null) {
curr = slow;
slow = slow.next;
fast = fast.next.next;
curr.next = prev;
prev = curr;
}
// fast指针走到倒数第二个节点的情况
if (fast != null) {
slow = slow.next;
}
while (curr != null && slow != null) {
if (curr.val != slow.val) {
return false;
}
curr = curr.next;
slow = slow.next;
}
return true;
}
3. 合并有序链表
3.1 合并两个有序链表
将两个升序链表合并为一个新的升序链表并返回,新链表是通过拼接给定的两个链表的所有节点组成的。
解决思路:
新建一个链表, 分别遍历两个链表, 每次将最小的那个节点接到新链表上。
/**
*
* @param list1
* @param list2
* @return
*/
public static ListNode mergeTwoLists(ListNode list1, ListNode list2) {
ListNode current = new ListNode(-1);
// 虚拟头节点
ListNode newHead = current;
while (list1 != null && list2 != null) {
if (list1.val < list2.val) {
current.next = list1;
// 链表1接着遍历, 链表2暂不遍历, 等待链表1下一个节点继续比较元素值
list1 = list1.next;
} else {
current.next = list2;
list2 = list2.next;
}
current = current.next;
}
//下面的两个while最多只有一个会执行
while (list2 != null) {
current.next = list2;
list2 = list2.next;
current = current.next;
}
while (list1 != null) {
current.next = list1;
list1 = list1.next;
current = current.next;
}
return newHead.next;
}
优化代码:
最后的两个while循环最多只会执行一个, 所以只需要判断哪一个链表不为空, 并将那一个链表后面的部分全部接入新链表表尾即可。
/**
* 优化后的实现方法
*
* @param list1
* @param list2
* @return
*/
public static ListNode mergeTwoListsMoreSimple(ListNode list1, ListNode list2) {
ListNode current = new ListNode(-1);
// 虚拟头节点
ListNode newHead = current;
while (list1 != null && list2 != null) {
if (list1.val < list2.val) {
current.next = list1;
// 链表1接着遍历, 链表2暂不遍历, 等待链表1下一个节点继续比较元素值
list1 = list1.next;
} else {
current.next = list2;
list2 = list2.next;
}
current = current.next;
}
// 最多只有一个还未被合并完,直接接上去就行了,这是链表合并比数组合并方便的地方
current.next = list1 == null ? list2 : list1;
return newHead.next;
}
3.2 合并K个链表
在合并两个链表的基础上, 先将前两个链表合并, 之后再将后面的链表逐步合并起来即可。
/**
* 合并K个链表
*
* @param lists
* @return
*/
public static ListNode mergeKLists(ListNode[] lists) {
ListNode res = null;
for (ListNode list : lists) {
res = mergeTwoListsMoreSimple(res, list);
}
return res;
}
4 双指针
4.1 寻找中间节点
给定一个头结点为 head 的非空单链表,返回链表的中间结点。如果有两个中间结点,则返回第二个中间结点。
示例1
输入:[1,2,3,4,5]
输出:此列表中的结点 3
示例2:
输入:[1,2,3,4,5,6]
输出:此列表中的结点 4
使用快慢指针, 慢指针一次走一步, 快指针一次走两步。当快指针到达表尾时, 慢指针必然位于中间位置。
/**
* 寻找中间节点
* @param head
* @return
*/
public static 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;
}
4.2 寻找倒数第K个元素
输入一个链表,输出该链表中倒数第k个节点。本题从1开始计数,即链表的尾节点是倒数第1个节点。
示例
给定一个链表: 1->2->3->4->5, 和 k = 2.
返回链表 4->5.
使用快慢指针, 先将快指针向后遍历到 k + 1个, 慢指针仍然指向链表的第一个节点, 此时两个指针之间刚好间隔 k 个节点。之后两个指针同时向后走, 当快指针到达链表的尾部空节点时, 慢指针刚好指向链表的倒数第 k 个节点。
代码实现:
/**
* 找链表倒数第K个结点
* @param head
* @param k
* @return
*/
public static ListNode getKthFromEnd(ListNode head, int k) {
ListNode fast = head, slow = head;
while (fast != null && k-- > 0 ) {
fast = fast.next;
}
while (fast != null) {
fast = fast.next;
slow = slow.next;
}
return slow;
}
4.3 旋转链表
给你一个链表的头节点 head ,旋转链表,将链表每个节点向右移动 k 个位置。
示例1:
输入:head = [1,2,3, 4,5], k = 2
输出:[4,5,1,2,3]
思路:
先使用双指针策略找到倒数 k 的位置, 将 倒数 k 位置前的部分断开并接入到表尾。
需要注意的是:
k可能大于链表长度, 所以首先应获取到链表长度len, 如果 k % len == 0, 则不用旋转, 直接返回头节点。
实现步骤:
- 快指针先走 k 步
- 慢指针和快指针一起走
- 快指针到达链表尾部时, 慢指针所在位置刚好是要断开的位置。将快指针指向的节点连到原链表头部, 慢指针指向的节点断开和下一个节点的联系 (指向 null 即可)
- 返回结束时慢指针指向节点的下一个节点
代码实现:
/**
* 旋转链表
* @param head
* @param k
* @return
*/
public static ListNode rotateRight(ListNode head, int k) {
if (head == null || k == 0) {
return head;
}
// 获取链表长度
int len = 0;
ListNode temp = head;
while (head != null) {
len++;
head = head.next;
}
head = temp;
if (k % len == 0) {
return head;
}
ListNode fast = head, slow = head;
// 1. 快指针先走 k 步
while ( (k % len) > 0) {
k--;
fast = fast.next;
}
// 2. 慢指针和快指针一起走
while (fast.next != null) {
fast = fast.next;
slow = slow.next;
}
// 3. 快指针到达链表尾部时, 慢指针所在位置刚好是要断开的位置。将快指针指向的节点连到原链表头部, 慢指针指向的节点断开和下一个节点的联系 (指向 null 即可)
fast.next = head;
temp = slow.next;
slow.next = null;
// 4. 返回结束时慢指针指向节点的下一个节点
return temp;
}
5. 删除链表元素
5. 1 删除特定节点
给你一个链表的头节点 head 和一个整数 val ,请你删除链表中所有满足 Node.val == val 的节点,并返回新的头节点 。
示例1:
输入:head = [1,2,6,3,4,5,6], val = 6
输出:[1,2,3,4,5]
遍历链表, 找出满足要求的节点, 将该节点的前驱节点指向该节点的下一个节点, 该节点会在某个时刻被gc回收掉。
实现步骤:
- 创建一个虚拟链表头节点dummyHead, 使其next指向head
- 开始循环链表寻找目标元素
- 找到目标元素, 就使用cur.next = cur.next.next来删除
- 最后返回dummyHead.next
代码实现:
/**
* 删除特定值的结点
*
* @param head
* @param val
* @return
*/
public static ListNode removeElements(ListNode head, int val) {
ListNode dummyHead = new ListNode(0);
dummyHead.next = head;
ListNode temp = dummyHead;
while (temp.next != null) {
if (temp.next.val == val) {
temp.next = temp.next.next;
} else {
temp = temp.next;
}
}
return dummyHead.next;
}
5.2 删除倒数第 n 个节点
给你一个链表,删除链表的倒数第n个结点,并且返回链表的头结点。
示例1:
输入:head = [1,2,3,4,5], n = 2
输出:[1,2,3,5]
方法1: 计算链表长度
首先遍历一次链表, 得到链表的长度 L, 然后重新遍历链表, 当遍历到 L - n + 1 个节点时, 就是我们要删除的节点。
代码实现:
/**
* 方法1:利用链表长度
*
* @param head
* @param n
* @return
*/
public static ListNode removeNthFromEndByLength(ListNode head, int n) {
ListNode dummyHead = new ListNode(0);
dummyHead.next = head;
int length = getLength(head);
ListNode current = head;
int index = 1;
while (index++ != length - n) {
current = current.next;
}
current.next = current.next.next;
return dummyHead.next;
}
方法2: 双指针
快指针先走 n 步, 然后两个指针同时向前走, 当快指针走到队尾的时候, 慢指针所指向的就是我们要找的节点。
代码实现:
/**
* 方法2:通过双指针
*
* @param head
* @param n
* @return
*/
public static ListNode removeNthFromEndByTwoPoints(ListNode head, int n) {
ListNode dummy = new ListNode(0);
dummy.next = head;
ListNode fast = head, slow = dummy;
while (fast != null && n-- > 0) {
fast = fast.next;
}
while (fast != null) {
fast = fast.next;
slow = slow.next;
}
slow.next = slow.next.next;
return dummy.next;
}
5.3 删除重复节点
5.3.1 重复元素保留一个
存在一个按升序排列的链表,请你删除所有重复的元素,使每个元素只出现一次。
示例1:
输入:head = [1,1,2,3,3]
输出:[1,2,3]
思路:
由于给定的链表是排好序的, 因此重复的元素在链表中的位置是连续的, 因此我们只需要对链表进行一次遍历就可以删除重复的元素。具体
地, 从头节点开始遍历, 如果当前节点 cur 与 cur.next 的元素相同, 我们就将cur.next从链表中删除; 否则说明链表中已经不存在其他与cur对应元素相同的节点, 因此可以将cur指向cur.next。
代码实现:
/**
* 重复元素保留一个
*
* @param head
* @return
*/
public static ListNode deleteDuplicate(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;
}
5.3.3 重复元素都不要
存在一个按升序排列的链表,请你删除链表中所有存在数字重复情况的节点,只保留原始链表中没有重复出现的数字。
示例1:
输入:head = [1,2,3,3,4,4,5]
输出:[1,2,5]
思路:
当一个都不要时,链表只要直接对cur.next 以及 cur.next.next 两个node进行比较就行了,这里要注意两个node可能为空,稍加判断就行了。
代码实现:
/**
* 重复元素都不要
*
* @param head
* @return
*/
public static ListNode deleteDuplicates(ListNode head) {
if (head == null) {
return head;
}
ListNode dummyHead = new ListNode(0);
dummyHead.next = head;
ListNode current = dummyHead;
// 从头节点开始比较
while (current.next != null & current.next.next !=null) {
if (current.next.val == current.next.next.val) {
int x = current.next.val;
while (current.next != null && current.next.val == x) {
current.next = current.next.next;
}
}else {
current = current.next;
}
}
return dummyHead.next;
}