算法通关村第一关——链表白银挑战笔记
1. 五种方法解决两个链表的第一个公共子节点问题
1.1 哈希和集合
先将一个链表元素全部存到Map里,然后一边遍历第二个链表,一边检测Hash中是否存在当前结点,如果有交点,那么一定能检测出来。
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;
}
1.2 使用栈
这里需要使用两个栈,分别将两个链表的结点入两个栈,然后分别出栈,如果相等就继续出栈,一直找到最晚出栈的那一组。这种方式需要两个O(n)的空间,所以在面试时不占优势,但是能够很好锻炼我们的基础能力。
import java.util.Stack;
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 拼接两个字符串
public ListNode findFirstCommonNode(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;
}
1.4 差和双指针
假如公共子节点一定存在第一轮遍历,假设La长度为L1,Lb长度为L2。则 |L2 - L1| 就是两个的差值。第二轮遍历,长的先走 |L2 - L1|,然后两个链表同时向前走,结点一样的时候就是公共结点了。
public ListNode findFirstCommonNode(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;
//长的先走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(current1 != current2){
current1 = current1.next();
current2 = current2.next();
}
return current1;
}
2. 判断链表是否为回文序列
示例1:
输入:1 -> 2 -> 2 -> 1
输出:true
进阶:你能否用O(n)事件复杂度和O(1)空间复杂度解决此题?
将链表元素全部压栈,然后一边出栈,一边重新遍历链表,一边比较,只有有一个不相等的,那就不是回文链表了。
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;
}
3. 合并有序链表
3.1 合并两个有序链表
public ListNode mergeTwoLists(ListNode list1, ListNode list2){
ListNode newHead = new ListNode(-1);
ListNode res = newHead;
while(list1 != null && list2 != null){
if(list1.val < list2.val){
newHead.next = list1;
list1 = list1.next;
}else(list1.val > list2.val){
newHead.next = list2;
list2 = list2.next;
}else{
//相等的情况,分别接两个链
newHead.next = list1;
list1 = list1.next;
newHead = newHead.next;
newHead.next = list2;
list2 = list2.next;
}
nextHead = newHead.next;
}
//下面两个while最多只有一个会执行
while(list1 != null){
newHead.next = list1;
list1 = list1.next;
newHead = newHead.next;
}
while(list2 != null){
newHead.next = list2;
list2 = list2.next;
newHead = newHead.next;
}
return res.next;
}
优化代码:
public ListNode mergeTwoLists(ListNode list1, ListNode list2){
ListNode curNode = new ListNode(-1);
ListNode res = curNode;
while(list1.val != null && list2.val != null){
if(list1.val <= list2.val){
curNode.next = list1;
list1 = list1.next;
}else{
curNode.next = list2;
list2 = list2.next;
}
curNode = curNode.next;
}
curNode.next = list1 == null ? list2 : list1;
return res.next;
}
3.2 合并k个链表
public ListNode mergeKLists(ListNode[] lists){
ListNode res = null;
for(ListNode list: lists){
res = mergeKLists(res, list);
}
return res;
}
3.3 一道无聊的好题
按部就班遍历找到list1保留部分的尾节点和list2的尾节点,将两表连接起来就可以了。
public ListNode mergeInBetween(ListNode list1, int a, int b, ListNode list2){
ListNode pre1 = list1, post1 = list1, post2 = list2;
int i = 0, int j = 0;
while(pre1 != null && post1 != null && j < b){
if(i != a - 1){
pre1 = pre1.next;
}
if(j != b){
post1 = post1.next;
}
}
post1 = post1.next;
while(post2 != null){
post2 = post2.next;
}
pre1.next = list2;
post2.next = post1;
return list1;
}
4. 双指针专题
4.1 寻找中间结点
快慢指针。
public ListNode middleNode(ListNode head){
ListNode slow = head, fast = head;
while(fast != null && fast.next != null){
slow = slow.next;
fast = fast.next.next;
}
return slow;
}
4.2 寻找倒数第k个元素
先将fast向后遍历到第 k + 1 个节点,slow仍然指向链表的第一个节点,此时指针fast与slow二者之间刚好间隔k个节点。之后两个指针同步向后走,当fast走到链表的尾部空节点时,slow指针刚好指向链表的第k个节点。
需要特别注意的是链表的长度可能小于k,寻找k位置的时候必须判断fast是否为null。
public ListNode getKthFromEnd(ListNode head, int k){
ListNode fast = head, slow = head;
while(fast != null && k > 0){
fast = fast.next;
k--;
}
while(fast != null){
slow = slow.next;
fast = fast.next;
}
return slow;
}
4.3 旋转链表
public ListNode rotateRight(ListNode head, int k){
if(head == null || int k = 0){
return head;
}
//这里三个变量都指向链表头结点
ListNode temp = head;
ListNode fast = head;
ListNode slow = head;
int len = 0;
//这里head先走一遍,统计出链表的元素个数,完成之后head就变成null了
while(head != null){
head = head.next;
len++;
}
if( k % len == 0){
return temp;
}
//从这里开始fast从头结点开始向后走
//这里使用取模,是为了防止k大于len的情况
//例如,如果len=5,那么k=2和7,效果是一样的
while((k % len) > 0){
k--;
fast = fast.next;
}
//快指针走了k步了,然后快慢指针一起向后执行
//当fast到尾结点的时候,slow刚好在倒数第k个位置上
while(fast.next != null){
fast = fast.next;
slow = slow.next;
}
ListNode res = slow.next;
slow.next = null;
fast.next = temp;
return res;
}
5. 删除链表元素专题
5.1 删除特定结点
输入:head = [1, 2, 3, 4, 5, 6], val = 6
输出:[1, 2, 3, 4, 5]
public ListNode removeElements(ListNode head, int val){
ListNode dummyHead = new ListNode(0);
dummyHead.next = head;
ListNode cur = dummyHead;
while(cur.next != null){
if(cur.next.val == val){
cur.next = cur.next.next;
}else{
cur = cur.next;
}
}
return dummyHead.next;
}
5.2 删除倒数第n个结点
输入:head = [1, 2, 3, 4, 5], n = 2
输出:[1, 2, 3, 5]
方法1:计算链表长度
首先从头节点开始对链表进行一次遍历,得到链表的长度L。随后我们再从头节点开始对链表进行一次遍历,当遍历到第L-n+1个节点时,它就是我们需要删除的节点。
public ListNode removeNthFromEnd(ListNode head, int n){
ListNode dummy = new ListNode(0);
dummy.next = head;
int len = getLength(head);
ListNode cur = dummy;
for(int i = 1; i < len - n + 1; i++){
cur = cur.next;
}
cur.next = cur.next.next;
ListNode ans = dummy.next;
return ans;
}
public int getLength(ListNode head){
int len = 0;
while(head != null){
len++;
head = head.next;
}
return len;
}
方法2:双指针
我们定义first和second两个指针,first先走N步,然后second再开始走,当first走到队尾的时候,second就是我们要的节点。
public ListNode removeNthFromEnd(ListNode head, int n){
ListNode dummy = new ListNode(0);
dummy.next = head;
ListNode fast = dummy, slow = dummy;
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;
ListNode ans = dummy.next;
return ans;
}
5.3 删除重复元素
5.3.1 重复元素保留一个
输入:head = [1,1,2,3,3]
输出:[1,2,3]
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;
}
5.3.2 重复元素都不要
输入:head = [1,2,3,3,4,4,5]
输出:[1,2,5]
当一个都不要时,链表只要直接对cur.next 以及cur.next.next两个node进行比较就行了,这里要注意两个node可能为空,稍加判断就行了。
public ListNode deleteDuplicates(ListNode head){
if(head == null){
return head;
}
ListNode dummy = new ListNode(0);
dummy.next = 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;
}