算法通关村第一关——链表白银挑战
1. 面试题 02.07. 链表相交
1. 思路
没有思路时的思考方式:
其实就是,我们看到题目,要去思考一下常用数据结构和常用的算法思想,这些常用的数据结构和算法思想适用于什么条件的题目,例如这题:
常用的数据结构:数组,链表,队,栈,Hash,集合,树,堆等。
常用的算法思想:查找,排序,双指针,递归,迭代,分治,贪心,回溯和动态规划等。
首先我看题目,可以看出有一段的链表相交,也就是有一段的节点是一样的,那么思路就是,用什么数据结构,将这些结点放进去之后,拿出来可以快速作比较。
-
数组?
使用数组存取和读取都很废空间和时间,而且也很难存,不对
-
Hash和集合?
好像可以喔,Hash存取,集合存取,先把一条链表存到数据结构,再拿出来与另一条链表对比,即可
- 栈和队?
队,好像没有什么用吧。
栈,好像不错栈的话,存进去,先进后出,直接把尾部拿出来对比,完美
2. Hash和集合
HashMap的方式:
public class Solution {
public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
if(headA == null || headB == null){
return null;
}
ListNode curA = headA;
ListNode curB = headB;
HashMap<ListNode, Integer> hashMap = new HashMap<>();
while(curA != null){
hashMap.put(curA, null);
curA = curA.next;
}
while(curB != null){
if(hashMap.containsKey(curB)){
return curB;
}
curB = curB.next;
}
return null;
}
}
运行,没得问题,但是我们会发现,其实只是使用了key,那还不如直接用集合。
那集合有list和set,我们需要判断一个集合有没有出现某个节点,list只能一个个遍历,set可以contains直接查到,所以我们集合就使用set集合。
public class Solution {
public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
if(headA == null || headB == null){
return null;
}
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;
}
}
没有问题啦~但是发现其实时间和Hash差不多,但是数据量大了肯定差别还是有的
3. 栈
使用栈的,时间复杂度为O(m+n),空间复杂度也是,所以简单学一学就行,因为需要使用栈额外的存储链表A和B
public class Solution {
public ListNode getIntersectionNode(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.isEmpty() && !stackB.isEmpty()) {
if (stackA.peek() == stackB.peek()) {
preNode = stackA.pop();
stackB.pop();
} else {
break;
}
}
return preNode;
}
}
4. 拼接字符串
假设,这两个链表有公共交点:
链表A:0-1-2-3-4-5
链表B:a-b-4-5
将两个链表互相拼接:
链表A:0-1-2-3-4-5-a-b-4-5
链表B:a-b-4-5-0-1-2-3-4-5
然后发现,最后两个相等的4-5就是公共交段
所以只需要,访问完本链表,再指针选到另一个链表,然后就能够一起到达公共交点位置
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;
}
}
5. 双指针
其实根据拼接字符串我们就能感觉出来
假设链表A的长度为a,链表B的长度为b
遍历两遍,第一遍遍历得到|a-b|的差值,第二遍,先让长的链表走|a-b|,再一起遍历,就能找到同一个点
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;
// 拿到链表1的长度
while (current1 != null) {
current1 = current1.next;
l1++;
}
// 拿到链表2的长度
while (current2 != null) {
current2 = current2.next;
l2++;
}
current1 = headA;
current2 = headB;
// 计算两个链表的差值
int sub = l1 > l2 ? l1 - l2 : l2 - l1;
// 链表1长的情况,先让1走差值
if (l1 > l2) {
int a = 0;
while (a < sub) {
current1 = current1.next;
a++;
}
}
// 链表2长的情况,先让2走差值
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. 回文链表
力扣234题:回文链表
我这里一开始想到了两种方法:
- 使用栈,一边遍历一边记录,将前一半压入栈,到中间位置的时候,再遍历栈和后面一半
- 使用集合,直接按顺序记录,到时候从后往前遍历,即可
思考了一下,使用集合需要遍历两次,使用栈好一点
后面看了算法村的思路:可以直接使用双指针,边遍历边反转,然后再遍历,完美
1. 双指针+反转一半
public static boolean isPalindromeByTwoPoints(ListNode head) {
if (head == null || head.next == null) {
return true;
}
ListNode slow = head, fast = head;
ListNode pre = head, prepre = null;
while(fast != null && fast.next != null){
// 5. 前指针移动
pre = slow;
// 1. 慢指针移动一个位置
slow = slow.next;
// 2. 快指针移动两个个位置
fast = fast.next.next;
// 3. 前指针,指向它的前一个位置,也就是反转
pre.next = prepre;
// 4. 记录前一个指针位置的指针,向前移动
prepre = pre;
}
// 如果不为空,代表,链表个数是单数
if (fast != null) {
slow = slow.next;
}
// 这时候遍历pre和slow,pre记录了前半部分指针反转后样子和slow后半部分
// 当都为null,说明是回文
while (pre != null && slow != null) {
if (pre.val != slow.val) {
return false;
}
pre = pre.next;
slow = slow.next;
}
return true;
}
那双指针可以找一半,那么压栈也可以找到一半。
时间复杂度为O(n),其中n为链表的长度。空间复杂度为O(1),只使用了常数级别的额外空间。
2. 双指针+压栈一半
可以使用栈结合双指针的方式,不反转链表,改成压栈,效果一样
public boolean isPalindrome(ListNode head) {
if (head == null || head.next == null) {
return true;
}
ListNode slow = head, fast = head;
Stack<ListNode> stack = new Stack<>();
while(fast != null && fast.next != null){
// 3. 将慢指针移动的结点压进去
stack.push(slow);
// 1. 慢指针移动一个位置
slow = slow.next;
// 2. 快指针移动两个个位置
fast = fast.next.next;
}
// 如果不为空,代表,链表个数是单数
if (fast != null) {
slow = slow.next;
}
// 当都为null,说明是回文
while (slow != null && !stack.isEmpty()) {
if (slow.val != stack.pop().val) {
return false;
}
slow = slow.next;
}
return true;
}
时间复杂度为 O(n),其中 n 为链表的长度。空间复杂度为 O(n/2),即栈的大小,其中 n 为链表的长度。
所以:根据时间复杂度和空间复杂度,很明显还是“双指针+反转”靠谱点
3. 合并两个有序链表
3.1 力扣21题:合并两个有序链表
这道题最简单的方式就是新建一个链表,然后一路判断节点val,哪个小就指向谁,不过不够优雅
所以我一开始就直接想的是让两个链表判断互相指向,然后又参考了通关村的讲解进行了一步步修改
1. 直接在原链表进行修改
class Solution {
public ListNode mergeTwoLists(ListNode list1, ListNode list2) {
// 如果其中一个链表为空,直接返回另一个链表
if (list1 == null) {
return list2;
}
if (list2 == null) {
return list1;
}
ListNode result;
// 选择较小的节点作为结果链表的头节点,并将其保存在result中
if (list1.val < list2.val) {
result = list1;
list1 = list1.next; // 移动到下一个节点
} else {
result = list2;
list2 = list2.next; // 移动到下一个节点
}
ListNode current = result; // 当前节点指向结果链表的头节点
// 循环比较两个链表的当前节点值,选择较小的节点连接到结果链表中
while (list1 != null && list2 != null) {
if (list1.val < list2.val) {
current.next = list1; // 将list1的节点连接到结果链表中
list1 = list1.next; // 移动到下一个节点
} else {
current.next = list2; // 将list2的节点连接到结果链表中
list2 = list2.next; // 移动到下一个节点
}
current = current.next; // 将当前节点向后移动一个位置
}
// 将剩余未连接的节点连接到结果链表的末尾
if (list1 != null) {
current.next = list1;
} else {
current.next = list2;
}
return result; // 返回结果链表的头节点
}
}
空间复杂度为O(1),时间复杂度为O(m+n)
2. 使用new的节点来指向
跟我的方式差不多,但是这个方式更好理解
public static ListNode mergeTwoLists2(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 if (list1.val > list2.val) {
newHead.next = list2;
list2 = list2.next;
} else { // 相等的情况,分别接两个链
newHead.next = list2;
list2 = list2.next;
newHead = newHead.next;
newHead.next = list1;
list1 = list1.next;
}
newHead = newHead.next; // 将当前节点指针向后移动
}
// 将剩余未连接的节点连接到结果链表的末尾
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; // 返回结果链表的头节点
}
空间复杂度为O(1),时间复杂度为O(m+n)
3. 进一步优化
在循环中,我们通过比较节点的值来选择较小的节点连接到结果链表中,并将当前节点指针向后移动。
最后,我们只需要判断哪个链表还有剩余节点未连接完毕,然后将其直接接到结果链表的末尾。
public static ListNode mergeTwoListsMoreSimple(ListNode l1, ListNode l2) {
ListNode prehead = new ListNode(-1); // 新建一个虚拟头节点
ListNode prev = prehead; // 保存结果链表的当前节点
// 遍历两个链表,比较节点的值,并将较小的节点连接到结果链表中
while (l1 != null && l2 != null) {
if (l1.val <= l2.val) {
prev.next = l1;
l1 = l1.next;
} else {
prev.next = l2;
l2 = l2.next;
}
prev = prev.next; // 将当前节点指针向后移动
}
// 最多只有一个还未被合并完,直接接上去就行了
prev.next = l1 == null ? l2 : l1;
return prehead.next; // 返回结果链表的头节点
}
4. 使用递归
当使用递归解决问题时,可以遵循三要素:递归的定义、递归的拆解、递归的出口。以下是使用递归实现合并两个有序链表的详细步骤:
- 首先,定义递归的函数
mergeTwoLists
,该函数接收两个链表list1
和list2
作为参数,并返回合并后的链表。 - 接下来,处理递归的出口条件:
- 如果
list1
为空,说明已经遍历完了其中一个链表,直接返回另一个链表list2
。 - 如果
list2
为空,同样说明已经遍历完了其中一个链表,直接返回另一个链表list1
。
- 如果
- 然后,处理递归的拆解部分:
- 比较
list1
和list2
的当前节点值的大小,如果list1.val
小于等于list2.val
,则将list1
的当前节点连接到合并后的链表中,然后对剩余的部分继续递归调用mergeTwoLists
函数,传入list1.next
和list2
作为参数。 - 如果
list1.val
大于list2.val
,则将list2
的当前节点连接到合并后的链表中,然后对剩余的部分继续递归调用mergeTwoLists
函数,传入list1
和list2.next
作为参数。
- 比较
- 最后,返回合并后的链表。
- [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-RxX4LCCa-1690711991692)(C:\Users\Kaizhi\AppData\Roaming\Typora\typora-user-images\image-20230720122025449.png)]
从这个图可以看出,把每一个要指向的位置压进去,最后取出来,就是链表了~
下面是按照这个思路实现的代码:
public ListNode mergeTwoLists(ListNode list1, ListNode list2) {
// 递归的出口条件
if (list1 == null) {
return list2;
}
if (list2 == null) {
return list1;
}
// 比较当前节点值,选择较小的节点连接到合并后的链表中,然后递归调用合并剩余部分
if (list1.val <= list2.val) {
list1.next = mergeTwoLists(list1.next, list2);
return list1;
} else {
list2.next = mergeTwoLists(list1, list2.next);
return list2;
}
}
3.2 合并k个升序链表
最简单的方式就是将链表两个两个合并,一路遍历过去,简单,面试的时候这么写比较不容易出错
class Solution {
public ListNode mergeKLists(ListNode[] lists) {
ListNode res = null;
for(ListNode list : lists){
res = mergeTwoLists(res, list);
}
return res;
}
public ListNode mergeTwoLists(ListNode l1, ListNode l2){
ListNode humyNode = new ListNode(-1);
ListNode prev = humyNode;
while(l1 != null && l2 != null){
if(l1.val >= l2.val){
prev.next = l2;
l2 = l2.next;
}else{
prev.next = l1;
l1 = l1.next;
}
prev = prev.next;
}
prev.next = l1 == null ? l2 : l1;
return humyNode.next;
}
}
这个方法是时间最长的,但是呐,最简单的,还有其他方法,我没看,肝不动了
3.3 合并两个链表
这道题有点点无聊,比简单题还简单,就没什么好说的,但是看到标题1669,那就说明算法题出到没得出了?啊哈
class Solution {
public ListNode mergeInBetween(ListNode list1, int a, int b, ListNode list2) {
ListNode l1 = list1;
ListNode l2 = list2;
for(int i=0; i<a-1; i++){
l1 = l1.next;
}
ListNode la = l1;
for(int i=a-1; i<b+1; i++){
l1 = l1.next;
}
ListNode lb = l1;
la.next = l2;
while(l2.next != null){
l2 = l2.next;
}
l2.next = lb;
return list1;
}
}
4. 双指针专题
4.1 寻找中间节点
快慢指针
这里我直接想到了快慢指针的方法,因为之前做过类似的题目,所以我也不写其他的方法了,这种又快又好
核心的地方就是,快指针比慢指针多走一个点,当快指针到null了,慢指针就是中间点
class Solution {
public ListNode middleNode(ListNode head) {
ListNode l1 = head;
ListNode l2 = head;
while(l2 != null && l2.next != null){
l1 = l1.next;
l2 = l2.next.next;
}
return l1;
}
}
4.2 剑指 Offer 22. 链表中倒数第k个节点
前后指针
我也是做过类似的题,所以这道题也是直接写出来了
核心就是:两个指针,一个先走k个节点,然后再一起走,直到null,后走的节点就是位置了
class Solution {
public ListNode getKthFromEnd(ListNode head, int k) {
ListNode slow = head;
while(k != 0){
head = head.next;
k--;
}
ListNode fast = head;
while(fast != null){
slow = slow.next;
fast = fast.next;
}
return slow;
}
}
4.3 旋转链表
快慢指针
这道题的做法有点像上一题,一个先走,然后再一起走,到达位置,快指针指向链表头,慢指针指向null,over
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;
}
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;
}
}
5. 删除链表元素专题
5.1 删除特定节点
我们前面说过,我们删除节点cur时,必须知道其前驱pre节点和后继next节点,然后让pre.next=next。
对于删除,我们注意到首元素的处理方式与后面的不一样。为此,我们可以先创建一个虚拟节点dummyHead,使其指向head,也就是dummyHead.next=head,这样就不用单独处理首节点了
完整的步骤是:
1.我们创建一个虚拟链表头dummyHead,使其next指向head。
2.开始循环链表寻找目标元素,注意这里是通过cur.next.val来判断的。3.如果找到目标元素,就使用curnext = 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;
}
}
5.2 删除链表的倒数第 N 个结点
方法一:计算链表的长度
顾名思义,先计算链表的长度,然后再遍历一次,找到要删除的节点
public static ListNode removeNthFromEndByLength(ListNode head, int n) {
ListNode dummy = new ListNode(0);
dummy.next = head;
int length = getLength(head);
ListNode cur = dummy;
for (int i = 1; i < length - n + 1; ++i) {
cur = cur.next;
}
cur.next = cur.next.next;
ListNode ans = dummy.next;
return ans;
}
public static int getLength(ListNode head) {
int length = 0;
while (head != null) {
++length;
head = head.next;
}
return length;
}
方法二:双指针
思路跟前面的双指针专题里的解题思路差不多,主要是要找到那个节点的前一个节点
使用双指针的方法要注意,链表的长度小于等于n的情况
class Solution {
public ListNode removeNthFromEnd(ListNode head, int n) {
ListNode dummyNode = new ListNode(0);
dummyNode.next = head;
ListNode fast = head;
ListNode slow = dummyNode;
for (int i = 0; i < n; i++) {
if (fast == null) { // 链表长度小于等于 n
return dummyNode.next;
}
fast = fast.next;
}
while (fast != null) {
fast = fast.next;
slow = slow.next;
}
slow.next = slow.next.next; // 删除指定节点
return dummyNode.next;
}
}
5.3 删除重复元素
5.3.1 重复元素保留一个
重复元素保留一个,那就只需要判断当前元素的值与下一个元素的值是否相等即可
class Solution {
public ListNode deleteDuplicates(ListNode head) {
ListNode cur = head;
while(cur != null && cur.next != null){
if(cur.val == cur.next.val){
cur.next = cur.next.next;
}else{
cur = cur.next;
}
}
return head;
}
}
5.3.2 重复元素都不要
这题我的做法是双指针,然后使用一个变量记录是否为重复元素
class Solution {
public ListNode deleteDuplicates(ListNode head) {
if(head == null || head.next == null) return head;
ListNode dummyNode = new ListNode(0);
dummyNode.next = head;
ListNode slow = dummyNode;
ListNode fast = head;
Boolean isDup = false;
while(fast != null && fast.next != null){
if(fast.val == fast.next.val){
fast = fast.next;
isDup = true;
}else{
if(isDup){
slow.next = fast.next;
fast = slow.next;
}else{
slow = slow.next;
fast = fast.next;
}
isDup = false;
}
}
if(isDup){
slow.next = fast.next;
}
return dummyNode.next;
}
}
结束啦!!!!