链表题最能检验出一个人代码内功的深浅,刚开始学习过一遍链表之后,刷了好多题,但是过了一段时间之后,再把链表题打开,感觉好陌生。就想起之前上课的时候,老师说过的一句话:“链表题不刷5遍,你就不要说自己学过链表。”所以,我又重新开始看之前链表的题,都是牛客上和lettcode上面的经典的面试题,下面这些代码都是窝默写出来的。
总结了一下,链表的经典面试题主要有以下几种:
2.
反转一个单链表。 OJ链接
5.
将两个有序链表合并为一个新的有序链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。OJ链接
7.
在一个排序的链表中,存在重复的结点,请删除该链表中重复的结点,重复的结点不保留,返回链表头指 针。OJ链接
8.
链表的回文结构。OJ链接
9.
输入两个链表,找出它们的第一个公共结点。OJ链接
10.
给定一个链表,判断链表中是否有环。OJ链接
12.
其他 。肯定还有其他更难的链表题,可以在牛客和Leetcode上自己找题来刷刷。
0:自定义实现链表
自定义实现链表的一些方法,主要有头插、尾插、任意位置进行插入、删除第一次出现的key、查找是否包含关键字key。比较容易出错的地方在于,任意插入的时候,一定要记得先绑后边,再绑前边,如图所示:
删除第一次出现key的节点:如下图所示,要先遍历找到要删除key的前一个结点cur,然后执行cur.next=node这个语句就完成删除操作了。
public class Node {
public int data;
public Node next;
public Node(int data){
this.data=data;
}
}
public class MyLinkedList {
private Node head;
//头插法
public void addFirst(int data){
//空表的时候
Node node=new Node(data);
if (this.head != null) {
node.next = head;
}
head=node;
}
public void display(){
Node cur=this.head;
while(cur!=null){
System.out.println(cur.data+" ");
cur=cur.next;
}
}
//尾插法
public void addLast(int data){
Node node=new Node(data);
if(head==null){
this.head=node;
}else{
Node cur=this.head;
while(cur.next!=null){//cur
cur=cur.next;
}
cur.next=node;
}
}
//任意插
public void addIndex(int index,int data){
//先要判断下标是否合法
if(index<0||index>getLength()){
System.out.println("下标不合法");
return;
}
if(index==0){
addFirst(data);
return;
}
if(index==getLength()){
addLast(data);
return;
}
//现在在中间进行插入
Node node=new Node(data);
//找到要插入下标的前一个结点
Node cur=searchPrev(index-1);
node.next=cur.next;
cur.next=node;
}
//找到要插入位置index的前一个位置index-1的位置节点
public Node searchPrev(int index){
Node cur=this.head;
int count=0;
while(count<index){
cur=cur.next;
}
return cur;
}
public int getLength(){
int count=0;
Node cur=this.head;
while(cur!=null){
count++;
cur=cur.next;
}
return count;
}
//删除第一次出现的key
public void remove(int key){
if(this.head==null){
return;
}
Node cur=searchPrevNode(key);
//要注意cur是否没有找到。进行判断一下
if(cur==null){
System.out.println("没有找到你要删除的下标");
return;
}
cur.next=cur.next.next;
}
//查找要删除元素的前驱结点
public Node searchPrevNode(int key){
Node cur=this.head;
while(cur!=null){
if(cur.next.data==key){
return cur;
}
cur=cur.next;
}
//没有找到要删的结点
return null;
}
//查找关键字key是否包含在单链表中
public boolean contains(int key){
Node cur=this.head;
while(cur!=null){
if(cur.data==key){
return true;
}
cur=cur.next;
}
return false;
}
1.
删除链表中等于给定值 key
的所有节点。比如要删除key=33的节点,如图所示:
思路:1.定义prev指向其头节点,定义cur指向待删除的节点 ;
2.判断cur当前所指的值和key是否相等,相等的话:执行prev.next=cur.next,意思就是删除 了cur当前指向的这个节点,然后cur继续向下遍历;
3.如果cur当前所指的值和key的值不相等的时候,则让prev=cur,cur继续向下遍历,直到cur=null的为止;
4.当cur=null时,如果还存在值等于key的节点,那么一定为头节点,因为开始判断的时候,没有对头节点进行判断,所以接下来要对头节点进行判断,值相等,则删除。
2.判断cur当前所指的值和key是否相等,相等的话:执行prev.next=cur.next,意思就是删除 了cur当前指向的这个节点,然后cur继续向下遍历;
3.如果cur当前所指的值和key的值不相等的时候,则让prev=cur,cur继续向下遍历,直到cur=null的为止;
4.当cur=null时,如果还存在值等于key的节点,那么一定为头节点,因为开始判断的时候,没有对头节点进行判断,所以接下来要对头节点进行判断,值相等,则删除。
代码如下:
public void removeAllKey(int key){
//定义一个prev指向要删除的节点的前一个
//定义一个cur指向要删除的结点
Node prev=this.head;
Node cur=this.head.next;
while(cur!=null){
if(cur.data==key){
prev.next=cur.next;
cur=cur.next;
}else{
prev=cur;
cur=cur.next;
}
}
//现在如果有还有和key相等的值,那么一定在头节点
if(this.head.data==key){
this.head=this.head.next;
}
}
2.
反转一个单链表。要求:遍历一次,把链表逆置。
思路:1.定义一个前驱节点prev,定义一个cur,如图所示;
2.先改变prev和cur的指向关系,让cur.next=prev,prev再指向cur所指位置,cur继续向下遍历下一个节点;
3.若开始定义的prev=null,当把链表反转过来之后,最后一个节点的next域所指向的为空(图中的123这个节点的next域为null,不用再想方设法地把最后一个节点的 next域置为null了)。
做链表题之前,把思路想清楚,一定要自己画图写代码,遇到不会的自己多画画图,加油,相信自己肯定可以的!
public Node reverseList(){
Node newHead=null;
Node prev=null;
Node cur=this.head;
while(cur!=null){
Node curNext=cur.next;
if(curNext==null){
newHead=cur;
}
cur.next=prev;
prev=cur;
cur=curNext;
}
return newHead;
}
3. 给定一个带有头结点
head
的非空单链表,返回链表的中间结点。如果有两个中间结点,则返回第二个中间结结点。要求:遍历一次链表。
思路:定义两个引用,一个fast,一个slow,让快的一次走两步,慢的一次走一步,循环条件就是,fast!=null&&fast.next!=null,因为快的引用走的比较快,并且这个顺序 不能互换,因为当fast!=null但fast.next=null时,会发生空指针异常。
public Node middleNode(){
Node fast=this.head;
Node slow=this.head;
while(fast!=null&&fast.next!=null){
fast=fast.next.next;
slow=slow.next;
}
return slow;
}
4.
输入一个链表,输出该链表中倒数第
k
个结点。
思路:定义两个引用,一个fast,一个slow,让fast先走k-1步,然后让两个同步走。在对求倒数第k个结点中,让fast先走k-1步做合法性判断的时候,可以进行优化,就是让他走k-1步时,并且再判断一下fast.next是否为空,如果为空了,则没有让fast继续向下走的必要了,直接跳出循环。(比如说:要找倒数第8个结点,但是链表的总长度只有5,那么就不用让fast走k-1=8-1=7步,一旦fast.next==null,直接返回null)。
public Node findKthToTail(int k){
if(k<0||head==null){
return null;
}
//定义两个引用,fast先走k-1步,让偶让fast和slow同时开始走
Node fast=this.head;
Node slow=this.head;
while(k-1>0){
if(fast.next!=null) {
fast = fast.next;
k--;
}else{
System.out.println("k过大");
return null;
}
}
while(fast.next!=null){
fast=fast.next;
slow=slow.next;
}
return slow;
}
5.
将两个有序链表合并为一个新的有序链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。
思路:对两个链表的遍历,定义一个假的头节点,把两个两个链表中的结点串联起来。
public Node mergeTwoLists(Node headA,Node headB){
Node newHead=new Node(-1);
Node tmp=newHead;
//当前两个链表都有数据
while(headA!=null&&headB!=null){
if(headA.val<headB.val){
tmp.next=headA;
tmp=tmp.next;
headA=headA.next;
}else{
tmp.next=headB;
tmp=tmp.next;
headB=headB.next;
}
}
if(headA!=null){
tmp.next=headA;
}
if(headB!=null){
tmp.next=headB;
}
return newHead.next;
}
6.
编写代码,以给定值
x
为基准将链表分割成两部分,所有小于
x
的结点排在大于或等于
x
的结点之前 。
思路:如图所示,进行分段,在分段进行插入的同时还要对区间是否为空进行判断,最后将两个区间进行合并的时候还要考虑两个区间段是否存在没有元素的情况;
public Node partition(int x){
Node bs=null;
Node be=null;
Node as=null;
Node ae=null;
Node cur=this.head;
while(cur!=null){
if(cur.data<x){
//第一次插入
if(bs==null){
bs=cur;
be=bs;
}else{
be.next=cur;
be=cur;
}
}else{
if(as==null){
as=cur;
ae=as;
}else{
ae.next=cur;
ae=cur;
}
}
cur=cur.next;
}
if(bs==null){
return as;
}
be.next=as;
if(as!=null){
ae.next=null;
}
return as;
}
7.
在一个排序的链表中,存在重复的结点,请删除该链表中重复的结点,重复的结点不保留,返回链表头指 针。
思路:对链表进行遍历,关键点是在一个有序的链表中,所以:从头开始遍历,找到和它相同的下一个则跳过,遇到多个相同的值连在一块的时候,采用while()循环;
public Node deleteDuplication(){
Node newHead=new Node(-1);
Node tmp=newHead;
Node cur=this.head;
while(cur!=null){
if(cur.next!=null&&cur.data==cur.next.data){
//相等的时候
while(cur.next!=null&&cur.data==cur.next.data){
cur=cur.next;
}
cur=cur.next;
}else{
tmp.next=cur;
tmp=cur;
cur=cur.next;
}
}
tmp.next=null;//进行这一步的作用就是要保证不出现死循环
return newHead.next;
}
8.
链表的回文结构。
思路:1.先找到中间节点;
2.找到中间结点后,从中间开始进行翻转;
3.两个引用head,slow,一个从前开始往后走,一个从后开始往前走,判断data是否相同,直到相遇的时候;
public boolean chkPalindrome(){
//第一步:先找到中间节点
Node fast=this.head;
Node slow=this.head;
while (fast!=null&&fast.next!=null){
fast=fast.next.next;
slow=slow.next;
}
//第二步:对链表进行翻转
Node cur=slow.next;
while(cur!=null){
Node curNext=cur.next;
cur.next=slow;
slow=cur;
cur=curNext;
}
//第三步:从两边开始向中间遍历,判断值是否相等
while(this.head!=slow){//还没有相遇的时候
if(this.head.data!=slow.data){
return false;
}
//专门为偶数设计的
if(this.head.next==slow){
return true;
}
this.head=this.head.next;
slow=slow.next;
}
return true;
}
9.
输入两个链表,找出它们的第一个公共结点。
思路:1.分别求出两个链表的长度;
2.让最长的链表走两个链表的差值步;
3.让两个引用(pl,指向长链表的头和ps,指向短链表的头)同时走,如果他俩相遇了,那么说明两个链表是相交的。
public Node getIntersectionNode(Node headA,Node headB){
if(headA==null||headB==null){
return null;
}
int lenA=0;
int lenB=0;
Node pl=headA;
Node ps=headB;
while(ps!=null){
lenB++;
ps=ps.next;
}
while(pl!=null){
lenA++;
pl=pl.next;
}
pl=headA;
ps=headB;
int len=lenA-lenB;
if(len<0){
ps=headA;
pl=headB;
len=lenB-lenA;
}
while(len>0){
len--;
pl=pl.next;
}
while(pl!=null&&ps!=null&&pl!=ps){
pl=pl.next;
ps=ps.next;
}
if(pl==ps&&pl!=null){
return pl;
}
return null;
}
10.
给定一个链表,判断链表中是否有环。
思路:1.定义两个引用,一个走的快的fast,和一个走的慢的slow,让fast一次走两步,slow一次走一步;
2.fast每走两步都要判断一下,是否和slow相遇了。
public boolean hasCycle(){
//首先定义两个引用,让一个走一步,一个走两步
Node fast=this.head;
Node slow=this.head;
while(fast!=null&&fast.next!=null){
fast=fast.next.next;
slow=slow.next;
if(fast==slow){
return true;
}
}
return false;
}
11.
给定一个链表,返回链表开始入环的第一个节点。 如果链表无环,则返回
null 。
思路:1.和求解是否有环的思路相似,都是定义两个引用,一个fast和一个slow,fast一次走两步,slow一次走一步,直到fast=null或者fast.next=null跳出循环;
2.跳出循环后判断是因为什么原因退出的,如果fast==null或者fast.next==null则表示链表中没有环;
3.有环退出的,现在就要求解入环的第一个节点。如图所示:
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(fast==slow){
break;
}
}
if(fast==null||fast.next==null){
return null;
}
slow=this.head;
while(slow!=fast){
fast=fast.next;
slow=slow.next;
}
return slow;
}