算法通关村第一关——链表白银挑战笔记

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;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值