一、反转一个单链表
思路:定义三个结点
- 1、cur 作为遍历整个链表的引用
- 2、prev 代表当前需要翻转的
- 3、curNext 代表需要翻转的下一个节点
public Node reverseList(){
Node cur = this.head;
Node prev = null;
Node newHead = null;
while(cur != null){
Node curNext = cur.next;
if(curNext == null){ //遍历到最后一个节点了
newHead = cur; //将最后一个节点作为链表的第一个节点
}
cur.next = prev;
prev = cur;
cur = curNext;
}
return newHead
}
解析:整个翻转链表的运行逻辑为:
- 1、我们首先定义三个结点的引用cur作为遍历整个链表节点的引用,prev是当前要翻转的节点 ,curNext是需要翻转的下一个节点,newHead用来作为反转后新链表的引用。
- 2、进入链表,起初当cur指向第一个结点时,我们进行判断,判断当前节点是否为空,若不为空,进入whle循环
- 3、进入循环后,我们创建curNext引用,使该引用指向cur.next即此时的第二个节点(所以此时cur是指向第一个节点的,而curNext是指向第二个节点的)
- 4、此时我们需要判断curNext是否为空,因为在第三步中curNext是cur的后一个节点,如果此时curNext为空,那么就说明cur节点是原链表的最后一个个节点。
- 5、显然此时cur是指向第一个节点的,而curNext是指向第二个节点,所以if语句不执行,我们将cur.next指向prev,因为这是第一个while循环,即在链表的开头,所以原本prev为null,所以
cur.next = prev
就等于是让原链表的第一个节点指向了null。注意,这里原链表的第一个节点的指向已经发生改变了,原本第一个结点是应该向右(假设链表自左向右)指向第二个节点,但通过这段后,原链表的第一个节点指向了null,可以理解为它成为了新链表的末尾(原链表自左向右的话,反转后就应该是自右向左地指向了) - 6、接着
prev = cur
就表示让prev指向原链表的第一个节点;cur = curNext
表示让cur指向自己的下一个位置;对于这两行代码可以理解为让prev和cur在链表中所引用的位置都向后移动了一位。 - 7、紧接着进入下一次while循环,此时prev在原链表的第一个节点,cur和curNext都在原链的第二个节点,进入循环后,curNext移向下一个节点(右移一位),之后再进行if语句判断,和后面的引用指向变化,循环即完成了链表的翻转。
- 8、新链表用一个新的引用newHead接受。
二、给定一个带头结点head的非空单链表,返回链表的中间节点,如果有两个中间节点,则返回第二个中间节点。
解法一:求得链表的长度后再通过引用cur去遍历链表,找到找到链表中间的值。但这种方法相当于进行了两次循环,我们与要更优解。
public int sizeNode(){
int count = 0;
Node cur = this.head;
while(cur != null){
count++;
cur = cur.next;
}
return count;
}
public middlenNode1(){
int len = sizeNode() / 2;
Node cur = this.head;
int count = 0;
while(count != len){
cur = cur.next;
count++;
}
return cur;
}
解法二:快慢指针
public Node middleNode2(){
Node fast = this.head;
Node slow = this.head;
while(fast != null && fast.next != null){
fast = fast.next.next;
slow = slow.next;
}
return slow;
}
解析:快慢指针的运行逻辑为:
- 1、我们定义了两个引用在链表的第一个节点处
- 2、当
fast == null
表示快指针已经走过了链表的最后一位(链表长度为偶数),fast.next == null
表示当前处于链表的最后一位链表长度为寄数),此处使用&&逻辑关系符是因为,如果当链表长度为偶数时,快指针将走出链表,此时fast为空,如果使用 || 逻辑关系,while循环将会判断fast.next,则此时就会发生空指针异常。 - 3、
fast = fast.next.next;
快指针一次走两步;slow = slow.next;
慢指针一次走一步
三、删除链表重复节点
在一个排序的链表中,存在重复的节点,请删除该链表中重复的结点,重复的结点不保留,返回链表头指针
public Node delectRepeatNode(){
Node newHead = new Node(-1);//定义一个傀儡结点为我们的新链表
Node tmp = newHead; //tmp引用用来遍历新链表
Node cur = this.head; //cur引用用来遍历原本的链表
while(cur != null){ // 我们需要判断原链表里的所有结点,所以需要循环条件使cur引用走完原链表
if(cur.next != null && cur.val == cur.next.val){
// 如果cur引用没有走到最后一个结点且原链表中遇到连续两个结点的值相同时:
while(cur.next != null && cur.val == cur.next.val){
// 循环cur引用走过所有val值是一样的结点
cur = cur.next;
}
cur = cur.next;
}else{ // 这种情况就说明cur引用的结点并没有重复
tmp.next = cur;
tmp = tmp.next;
cur = cur.next;
}
}
tmp.next = null; // 此时引用tmp指向的是新链表的最后一个结点,需要将它置空
return newHead.next;
}
解析:我们假设一个链表的值为 1 2 3 3 3 3 4
,则代码的运行逻辑为:
- 1、首先新建一个傀儡结点newHead,顾名思义作为头节点,同时使用tmp引用来遍历这个新链表,定义cur来遍历旧链表。
- 2、因为原始链表不为空,所以会进入第一个if判断,此时cur指向1,在if语句处进行判断,发现不满足所述的
cur.val == cur.next.val
,则进入else语句,tmp.next = cur
是让tmp.next
指向cur现在所指向的位置,即将1这个结点放到了新的链表中,然后tmp此时原本是在新链表的傀儡结点处,向后移动一位则到了刚才放进来的这个1的位置。使旧链表中引用cur向后移动一位。 - 3、第二次循环判断是一样的情况,将2放进新链表。
- 4、当进入第三次循环时,
cur.val == cur.next.val
满足进入while循环条件同样满足,此时在while循环中引用cur会一直走到最后一个3的位置,之后引用cur在进行一次向后移动便来到了4位置。 - 5、同样将4位置放进新链表就结束了
- 6、在if语句和whlie语句中增加了&&的判断条件是因为:
当引用cur走到4是此时if的判断条件是cur.val == cur.next.val
,但此时对于4这个位置cur.next
是空,会报空指针异常错误。而while处同样增加了这个&&条件,是因为,如果初始链表是1 2 3 3 3 3
这种情况在最后一次while循环判断的时候cur.next.val
也会出现空指针异常。 - 7、在最后加上
tmp.next = null
也是为了解决1 2 3 3 3
这种情况,使得2结点原本的指向置为空
四、链表分割
以给定值x为基准将链表分割成两部分,所有小于x的结点排在大于或等于x的结点之前,。
public Node cutLinkList(int x){
if(this.head == null){
return null;
}
Node aStar = null;
Node aEnd = null;
Node bStar = null;
Node bEnd = null;
Node cur = this.head; //遍历原链表
while(cur != null){
if(cur.val < x){
// 比x小,存放在前面的链表
if(aStar == null){
// 第一次存放
aStar = cur;
aEnd = cur;
}else{
// 已有数据后的存放
aStar.next = cur;
aEnd = aEnd.next;
}
}else{
// 等于或大于x,存放在后面的链表
if(bStar == null){
//第一次放进去
bStar = cur;
bEnd = cur;
}else{
bStar.next = cur;
bEnd = bEnd.next;
}
}
cur = cur.next;
}
if(aStar == null){
return bStar;
}
aEnd.next = bStar;
if(bStar != null){
bEnd.next = null;
}
return aStar;
}
解析:我们假设一个链表的值为 18,31,15,27,19,23,16
,假设我们选取的x值为19,则代码的运行逻辑为:
- 1、在做链表类的题时,判空总是第一步
- 2、我们需要将它分成两部分,一部分存放比19小的数组成一个链表,定义a链表链表头为
aStat
,链表尾为aEnd
;一部分存放比19大的数组成一个链表,定义b链表头为bStar
,链表尾为bEnd
。 - 3、我们需要遍历原始链表并判断
cur.val
的值与给定值x的大小,当小于时,我们将此时的引用cur所指向的位置给aStar
,但才此次循环中这还是第一次给a链表值,所以需要有个判断条件,aStar == null
,即这时a链表是第一次赋值,此时将a链表的aStar
与aEnd
都指向新进来的这个节点,这次循环走完,将原链表上的引用cur走向下一个结点;当下一次循环时,如果cur.val
还是小于x值,那么就在a链表后面加入引用cur此时所指向的结点即aStar.next = cur;
,并且aEnd
向后走一步。 - 4、另一种
cur.val
大于x的情况与上述过程类似。 - 5、cur在原链表上走完后,我们得到了两条链表,其中a链表的表头为
aStar
,b链表的表头为bStar
,我们需要判断两个链表是否有一个为空。 - 6、a链表在前,b链表在后,当a链表为空时,我们直接返回b链表就行;但当b链表为空或者a和b链表都不为空时,我们首先要将a链表的最后一个结点与b链表的第一个结点连起来
aEnd.next = bStar.next;
,即为链表的结尾。接下来判断当b链表不为空时,将b链表的最后一个结点的next指向为空。最后不论b为不为空都返回a链表的表头aStar
。
五、合并两个有序链表
将两个有序链表合并为一个新的有序链表并返回。新链表是通过拼接给定的两个链表的所有结点组成的
public Node spliceLinkList(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;
headA = headA.next;
}else{
tmp.next = headB;
headB = headB.next;
}
tmp = tmp.next;
}
if(headA != null){
tmp.next = headA;
}
if(headB != null){
tmp.next = headB;
}
return newHead.next;
}
解析:有两个有序链表a,第一个节点为headA;链表b,第一个结点为headB,代码的运行逻辑为:
- 1、为了将两个链表合并成一个新的链表,则我们需要创建一个新的链表,即创建一个傀儡头节点,并有一个引用tmp来遍历这个新链表。
- 2、为了将a链表和b链表放入新链表,则我们的循环条件应该是
headA != null && headB != null
,在进入循环后,判断headA和headB引用所指向的结点的val哪个小,将小的放入新的链表,假设headA.val < headB.val
即让新链表中引用tmp所指向的结点的next指向此时的headA,并且将原链表中引用headA向后走一位。并且,新链表中引用tmp也要向后走一步,走到新添加进来的这个结点。 - 3、当while循环走完有两种情况,
headA==null
即,a链表走完了,另一种情况也相似,此时判断如果a链表先走完了,那直接让tmp此时所指向节点的next直接指向剩余的b链表所剩结点的位置(即指向此时的head),反之同理。 - 4、最后返回傀儡结点的next,即新链表的第一个结点位置。
六、输出链表中的倒数第k个结点
public Node findLastKNode(int k){
Node fast = this.head;
Node slow = this.head;
while(k-1 != 0){
// 每次走之前都判断一下,此时fast是不是已经走到尾结点了
if(fast.next != null){
fast = fast.next;
k--;
}else{
System.out.println("k值不合法");
return null;
}
while(fast.next != null){
slow = slow.next;
fast = fast.next;
}
}
解析:先让快指针走k-1步,之后同时走,当快指针走到最后一个结点时,慢指针此时的位置就是倒数第k个结点。
七、删除链表中等于给定值val的所有节点
public Node delectAllVallNode(int val){
if(this.head == null) return null;
Node prev = this.head;
Node cur = prev.next;
while(cur != null){
if(cur.val != val){
prev = prev.next;
}else{
prev.next = cur.next;
}
cur = cur.next;
}
if(this.head.val == val){
this.head == this.head.next;
}
}
解析:此题主要要注意的是最后当链表走完后要对第一个结点在进行判断一次,因为我们使用引用cur进行判断是从结点的第二个位置开始的。
八、判断链表是否为回文结构
public boolean linkIsPalindrome(){
// 先找到链表的中间节点
Node fast = this.head;
Node slow = this.head;
while(fast.next != null){
fast = fast.next.next;
slow = slow.next;
}
// 此时找到中间节点,即slow后,翻转后半链表
Node cur = slow.next;
while(cur != null){
Node curNext = cur.next;
cur.next = slow;
slow = cur;
cur = curNext;
}
// 接下来就让两边往中间走
Node head = this.head;
while(head != slow){
if(head.val != slow.val){
return false;
}
if(head.next = slow){
return true;
}
head = head.next;
slow = slow.next;
}
return true;
}
解析:主要分为以下三个部分:
- 1、找到链表的中间节点
- 2、翻转中间节点之后的链表
- 3、从两边往中间走,判断每次循环的值是否都相等,注意当链表长度为偶数时,需要额外使用
if(head.next == slow)
来判断。
九、输入两个链表,找出他们的第一个公共(相交)节点
public Node findTwoLinkCrossNode(Node headA, Node headB){
if(headA == null || headB == null){return null;}
// 先求两个链表的长度,假设A长,B短
Node pLong = headA;
Node pShort = headB;
int lenA = 0;
int lenB = 0;
while(pLong != null){
pLong = pLong.next;
lenA++;
}
while(pShort != null){
pShort = pShort.next;
lenB++;
}
// 求完长度要指回开头
pLong = headA;
pShort = headB;
int len = lenA - lenB;
if(len < 0){ // 说明A链比B链短,我们交换一下
pLong = headB;
pShort = headA;
len = lenB - lenA
}
while(len != 0){
pLong = pLong.next;
len--;
}
while(pLong != pShort){
pLong = pLong.next;
pShort = pShort.next;
}
return pLong;
}
解析:此题的主要逻辑为:为了求两条链表相交的第一个结点,根据链表的结构可以知道,在相交点后面的部分两条链表必然是一样(地址长度相同),所以差异必然是在相交之前,所以遍历两条链表求出长度,长出的地方一定是在相交点之前,所以,我们让长的链表先走两链表长度只差的步数,之后一起走,两引用必然在相交点相遇。
十、给定一个链表,判断链表中是否有环
public boolean linkIsCycle(){
if(this.head == null){return false;}
Node fast = this.head;
Node slow = this.head;
while(fast != null && slow.next != null){
if(fast == slow){
break;
}
fast = fast.next.next;
slow = slow.next;
}
if(fast == null || fast.next == null){
return false;
}
return true;
}
解析:运行逻辑为:快慢指针,在环里快指针一定会追上慢指针。
十一、给定一个链表,返回链表开始入环的第一个结点,如果链表无环,则返回null
public Node findLinkInCycleFirstNode(){
if(this.head == null){return null;}
Node fast = this.head;
Node slow = this.head;
while(fast != null && slow.next != null){
if(fast == slow){
break;
}
fast = fast.next.next;
slow = slow.next;
}
if(fast == null || fast.next == null){
return null;
}
slow = this.head;
while(slow != fast){
fast = fast.next;
slow = slow.next;
}
return slow;
}
解析:这题主要就是理解公式X+NC+L = 2(X+L)
。slow和fast相遇的点到入环第一个结点的距离 和 链表第一个节点到入环第一个结点的距离是相等的。
十二、删除链表中的某个节点(除了头节点和尾结点),只能访问该结点
public Node delectSomeNode(Node node){
if(node == null){return null;}
node.val = node.next.val;
node.next = node.next.next;
}
解析:直接将该结点替换为该节点的下一个结点