0J面试题 链表
文章目录
- 0J面试题 链表
- 一、单链表
- 1.1删除链表中等于给定值val的所有节点
- 1.2反转一个单链表
- 1.3链表的中间结点
- 1.4链表中倒数第k个节点
- 1.5合并两个有序链表
- 1.6链表分割
- 1.7删除链表中重复的节点
- 1.8链表的回文结构
- 1.9环形链表
- 2.0 环形链表 ||
- 2.1相交链表
一、单链表
1.1删除链表中等于给定值val的所有节点
OJ链接
📜描述:
给你一个链表的头节点 head
和一个整数 val
,请你删除链表中所有满足 Node.val == val
的节点,并返回 新的头节点 。
📝大体思路:
定义一个引用变量cur来判断当前节点是否为删除的值的节点,在定义prev,是cur的前驱,用来跳过删除的值的节点
假设跳过的val是8:
❗️核心代码:
if(cur.val == val) {
prev.next = cur.next;
cur = cur.next;
}else {
prev = cur;
cur = cur.next;
}
注意的细节:
1、head不能为空。
2、如果删除的值是第一个,那就放在最后处理
💬完整代码:
public void removeAllKey(int val) {
if(this.head == null) {//head不能为空。
return;
}
Node prev = this.head;
Node cur = this.head.next;
while (cur != null) {
if(cur.val == val) {
prev.next = cur.next;
cur = cur.next;
}else {
prev = cur;
cur = cur.next;
}
}
//最后判断头节点
if(this.head.val == val) {
this.head = this.head.next;
}
}
1.2反转一个单链表
OJ链接
📜描述:
给你单链表的头节点 head
,请你反转链表,并返回反转后的链表。
什么是反转链表?
但是我们可以用另外一种方式看
📝大体思路:
反转链表可以采用头插法的方式,链表的最后next域是一个null,所以我们就把第一个节点next域设成null,curNext的引用变量记录下一个节点,再用cur引用变量指向当前节点,用prev改变next域,使方向反转
❗️核心代码:
while(cur != null) {
Node curNext = cur.next;
cur.next = newHead;
newHead = cur;
cur = curNext;
}
注意细节:
1.因为反转设第一个节点next域设为空,所以newHead先设为null
2.如果只有一个节点head或没有节点直接返回head
完整代码:
public Node reverseList() {
if(head == null||head.next == null)
return head;
Node cur = head;
Node newHead = null;
while(cur != null) {
Node curNext = cur.next;
cur.next = newHead;
newHead = cur;
cur = curNext;
}
return newHead;
}
1.3链表的中间结点
OJ链接
📜描述:
给定一个头结点为 head
的非空单链表,返回链表的中间结点。
如果有两个中间结点,则返回第二个中间结点。
这道题比较简单,
📝大体思路:
举个例子,两个同学比赛400m,一个同学是另一个同学速度的两倍,快的同学跑到终点的时候,慢的同学刚好跑到了200m,正好是整个跑道的中点,所以我们也new一个fast和slow的引用变量,使fast速度是slow的两倍,使他多走一步,fast走到null,返回slow
❗️核心代码:
while(fast!=null && fast.next!=null) {
fast=fast.next.next;
slow=slow.next;
}
注意细节:
如果有两个中间结点,则返回第二个中间结点。
💬完整代码:
public Node middleNode() {
if(head == null) return head;
Node fast = head;
Node slow = head;
while(fast != null && fast.next!=null) {
fast = fast.next.next;
slow = slow.next;
}
return slow;
}
1.4链表中倒数第k个节点
OJ链接
📜描述
输入一个链表,输出该链表中倒数第k个结点。
我们只需要遍历一遍链表
📝大体思路:
我们还是new一个fast和一个slow,fast和slow始终差k-1步,当fast走到了k-1步的时候,fast和slow一起走,直到fast为null,返回slow。
❗️核心代码:
while( k-1 != 0) {
if(fast.next != null) {
fast = fast.next;
k--;
}else{
return null;
}
}
while(fast.next != null) {
fast = fast.next;
slow = slow.next;
}
return slow;
注意细节:
1、head为空直接返回null;
2、判断k合不合法,k超过了总链表长度我们直接在k-1!=0直接限制,只需要判断k<0即可;
完整代码:
public Node findKthToTail(int k) {
if(head == null) return null;
if(k < 0 ) {
return null;
}
Node fast = head;
Node slow = head;
while( k-1 != 0) {
if(fast.next != null) {
fast = fast.next;
k--;
}else{
return null;
}
}
while(fast.next != null) {
fast = fast.next;
slow = slow.next;
}
return slow;
}
1.5合并两个有序链表
OJ链接
📜描述:
将两个升序链表合并为一个新的 升序 链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。
📝大体思路:
定义一个傀儡节点,和tmp引用变量指向这个傀儡对象,使两个链表headA,headB互相比较,谁小就把他放在傀儡节点的后面,tmp指向最新的节点,循环;最后如果tmp在headB链表中next域为null,直接tmp的下一个节点直接指向headA,反之headA,同理。最后返回傀儡节点。
❗️核心代码:
while (headA != null && headB != null) {
if (headA.val < headB.val) {
tmp.next = headA;
headA = headA.next;
} else {
tmp.next = headB;
headB = headB.next;
}
tmp = tmp.next;
}
注意细节:
傀儡节点可以赋值负数,不影响
💬完整代码:
public static Node mergeTwoLists (Node headA, Node headB){
if (headA == null) return headB;
if (headB == null) return headA;
if (headA == null && headB == null) return null;
Node newH = new Node(0);
Node tmp = newH;
while (headA != null && headB != null) {
if (headA.val < headB.val) {
tmp.next = headA;
headA = headA.next;
} else {
tmp.next = headB;
headB = headB.next;
}
tmp = tmp.next;
}
if (headB == null) {
tmp.next = headA;
}
if (headA == null) {
tmp.next = headB;
}
return newH;
}
1.6链表分割
OJ链接
📜描述
现有一链表的头指针 Head,给一定值x,编写一段代码将所有小于x的结点排在其余结点之前,且不能改变原来的数据顺序,返回重新排列后的链表的头指针。
📝大体思路:
最后分割顺序保持不变,所以可以采用尾插法的思想。
竟然是链表分割那我们就可以分为两段一段是<x的,另一段是>=x的,第一段定义起点bs和终点be,另一段as和ae,他们都设置为null,cur这个引用变量每次与x比较,将<x放左边,bs始终保持不变,be随着变化往后移,>x放右边,反之同理,最后串起来就ok。
❗️核心代码:
while(cur != null) {
if(cur.val < x) {
//第一次插入
if(bs == null) {
bs = cur;
be = cur;
}else {
be.next = cur;
be = be.next;
}
}else {
//第二部分第一次插入
if(as == null) {
as = cur;
ae = cur;
}else {
ae.next = cur;
ae = ae.next;
}
}
cur = cur.next;
注意细节:
考虑如果链表全<x的时候,第二段就没有,as就是null直接返回第一段就行了,
如果>x的时候,那么第一段就没有节点,返回第二段as,
💬完整代码:
public Node partition( int x) {
Node cur = this.head;
Node bs = null;
Node be = null;
Node as = null;
Node ae = null;
while(cur != null) {
if(cur.val < x) {
//第一次插入
if(bs == null) {
bs = cur;
be = cur;
}else {
be.next = cur;
be = be.next;
}
}else {
//第二部分第一次插入
if(as == null) {
as = cur;
ae = cur;
}else {
ae.next = cur;
ae = ae.next;
}
}
cur = cur.next;
}
if(bs == null) {
return as;
}
//bs!=null
be.next = as;
if(as != null) {
ae.next = null;
}
return bs;
}
/*if(bs == null) {
return as;
}else {
be.next = as;
ae.next = null;//注意问题,ae可能为空
return bs;
}*/
1.7删除链表中重复的节点
OJ链接
📜描述
在一个排序的链表中,存在重复的结点,请删除该链表中重复的结点,重复的结点不保留,返回链表头指针。 例如,链表1->2->3->3->4->4->5 处理后为 1->2->5
📝大体思路:
定义cur引用变量,如果当前val域与下一个节点val域相同就让他跳过下个节点,如果是多个挨在一起的就使用循环,循环结束cur还在重复的节点上,所以多走一步;如果节点不重复,定义一个傀儡节点和tmp引用变量,cur用来判断是否为重复节点,不是则tmp的next域等于cur,cur直到为null停下来
❗️核心代码:
while (cur != null) {
//cur.next != null : 判断是不是只有一个节点 或者 是不是尾巴节点
if(cur.next != null && cur.val == cur.next.val) {
//cur再走的过程当中,有可能剩下的都是相同的
while ( cur.next != null && cur.val == cur.next.val) {
cur = cur.next;
}
cur = cur.next;//多走一步
注意细节:
1、循环结束,要多走一步;
2、万一只有一个节点cur.next为空,会出现空指针异常,所以要加上cur.next!=null;
3、如果链表都重复也要加上cur.next!=null,避免空指针异常
4、最后tmp需要手动设置null,有可能不是null,避免程序错误
💬完整代码:
public Node deleteDuplication() {
Node newHead = new Node(-1);
Node tmp = newHead;
Node cur = this.head;
while (cur != null) {
//cur.next != null : 判断是不是只有一个节点 或者 是不是尾巴节点
if(cur.next != null && cur.val == cur.next.val) {
//cur再走的过程当中,有可能剩下的都是相同的
while ( cur.next != null && cur.val == cur.next.val) {
cur = cur.next;
}
cur = cur.next;//多走一步
}else {
tmp.next = cur;
tmp = tmp.next;
cur = cur.next;
}
}
tmp.next = null;//手动设置,防止最后一个节点是重复的
return newHead.next;
}
1.8链表的回文结构
OJ链接
📜描述
对于一个链表,请设计一个时间复杂度为O(n),额外空间复杂度为O(1)的算法,判断其是否为回文结构。
给定一个链表的头指针A,请返回一个bool值,代表其是否为回文结构。保证链表长度小于等于900。
测试样例:
1->2->2->1
返回:true
📝大体思路:
快慢指针+反转,1.2和1.3的结合,总体来说掌握中间节点和反转链表,就会很简单,反转完毕后,slow和head一起走直到相遇停止。
❗️核心代码:
//1、找中间节点
Node slow = this.head;
Node fast = this.head;
while (fast != null && fast.next != null) {
fast = fast.next.next;
slow = slow.next;
}
//slow 指向的节点 就是中间节点
//2、进行翻转
Node cur = slow.next;
while (cur != null) {
Node curNext = cur.next;
cur.next = slow;
slow = cur;
cur = curNext;
}
注意细节:
如果回文结构是1->2->2->1,最后一起走的时候head的next域等于slow就好了,不然按照正常执行代码是会出现错误
💬完整代码:
//链表回文
public boolean chkPalindrome() {
if(this.head == null) {
return true;
}
if(this.head.next == null) {
//只有一个节点
return true;
}
//1、找中间节点
Node slow = this.head;
Node fast = this.head;
while (fast != null && fast.next != null) {
fast = fast.next.next;
slow = slow.next;
}
//slow 指向的节点 就是中间节点
//2、进行翻转
Node cur = slow.next;
while (cur != null) {
Node curNext = cur.next;
cur.next = slow;
slow = cur;
cur = curNext;
}
//翻转完毕 slow指的地方就是 最后一个节点
while (slow != head) {
if(slow.val != head.val) {
return false;
}
if(head.next == slow) {
return true;
}
head = head.next;
slow = slow.next;
}
return true;
}
1.9环形链表
OJ链接
📜描述:
给定一个链表,判断链表中是否有环。
📝大体思路:
也是需要用到快慢指针思想,fast比slow的速度快一倍,如果他们相遇就有环,不能相遇无环。
❗️核心代码:
fast = fast.next.next;
slow = slow.next;
注意细节:
为什么fast走两步而不是三步,因为会错过与slow的最佳相遇时机,走两步刚刚好
💬完整代码:
//链表是否有环
public boolean hasCycle1() {
Node fast = this.head;
Node slow = this.head;
while(fast != null && fast.next != null) {
fast = fast.next.next;
slow = slow.next;
if(slow == fast) {
return true;
}
}
return false;
}
2.0 环形链表 ||
OJ链接
📜描述
给定一个链表,返回链表开始入环的第一个节点。 如果链表无环,则返回 null
。
📝大体思路:
需要一个公式证明一个结论,看图说话。
有图可知:
由这个公式可以推出,x的路程==y的路程,就可以写代码了
❗️核心代码:
while(fast != null && fast.next != null) {
fast = fast.next.next;
slow = slow.next;
if(slow == fast) {
break;
}
}
注意细节:
break跳出来的时候,有可能是不满足循环条件跳出来的,也有可能是条件语句跳出来的
💬完整代码:
//给定一个链表,返回链表开始入环的第一个节点。 如果链表无环,则返回 null
public Node detectCycle() {
Node fast = this.head;
Node slow = this.head;
while(fast != null && fast.next != null) {
fast = fast.next.next;
slow = slow.next;
if(slow == fast) {
break;
}
}
if(fast == null || fast.next == null) {
return null;
}
//slow和fast是相遇的
slow = this.head;
while (fast != slow) {
fast = fast.next;
slow = slow.next;
}
return slow;
}
2.1相交链表
OJ链接
📜描述:
给你两个单链表的头节点 headA
和 headB
,请你找出并返回两个单链表相交的起始节点。如果两个链表没有交点,返回 null
。
📝大体思路:
话语不变描述,还是看图说话吧
如果是上面这种形式,相交之前都是相同的节点个数,就比较简单,两个链表直接定义两个引用变量,pl和ps,循环,直到pl==ps就找到公共节点。
❗️核心代码:
while(pl!=ps) {
pl=pl.next;
ps=ps.next;
}
//最终都没有相交都返回null,这段代码写不写都没问题
if(pl==null && ps==null) {
return null;
}
return ps;
如果是上面这种形式,我们就要想办法,让相交之前走的节点数相同,所以,首先计算相交之前两个链表的节点个数,然后像个链表相交之前的节点个数之差,就是长链表应该先走多少步,然后再一起走,直到相交。
💬完整代码:
//两个链表,找出他们的第一个公共节点
public static Node getIntersectionNode(Node headA, Node headB) {
if(headA==null) return null;
if(headB==null) return null;
int lenA=0;//用来计算相交之前的长度
int lenB=0;//用来计算相交之前的长度
Node pl=headA;
Node ps=headB;
while(pl!=null) {
lenA++;
pl=pl.next;
}
while(ps!=null) {
lenB++;
ps=ps.next;
}
//走出来pl和ps都为null
pl=headA;
ps=headB;
//< == >
int len=lenA-lenB;
//:pl永远指向的是长的链表 ps永远指向短的链表 len永远是一个正数
if(len<0) {
pl=headB;
ps=headA;
len=lenB-lenA;
}
//长链表先走的步数
while(len!=0) {
pl=pl.next;
len--;
}
while(pl!=ps) {
pl=pl.next;
ps=ps.next;
}
//最终都没有相交都返回null,这段代码写不写都没问题
if(pl==null && ps==null) {
return null;
}
return ps;
}