上一节讲了链表的基本用法,当我再深入刷题的时候,有被惊到,真的是细节满满,所以这一节来扯一下链表的一些进阶技巧
1.反转一个链表
这个问题就比较抽象了,仔细想很容易绕进去,但是琢磨一会思路还是会越来越清晰,反转链表就是把尾当头,头当尾,所以在实现的时候思想就是,定义cur指向头结点,用一个前驱prev装当前结点的头,用一个curNext装当前结点的尾,头和尾保存以后把cur的尾换成prev,再把prev换成当前cur作为下一个结点的尾,cur的头换成curNext,这样就完成了一个结点的反转,在这样遍历下去直到,直到出循环时cur为空说明链表反转完成了,如下:
public Node reverse(){
Node cur=this.head;
Node prev=null;
Node newHead=null;
while (cur!=null){
Node curNext=cur.next;
if(curNext==null){
newHead=cur;//反链表;;cur.next中存的是cur的引用
} //可以通过.next.next找到.next,而不能用.next去找.next.next;;
cur.next=prev;
prev=cur;
cur=curNext;
}
return newHead;
}
上一节讲过打印链表的方法是从头遍历,但是对于反转以后的链表是不能用disPlay()方法的,因为disPlay()总是从未反转以前链表的头开始打印,反转以后链表的头的下一个结点是null,如果还用该方法打印则只能得到反转后链表的最后一个值,打印反转后链表只能从返回的新的头开始打印,所以需要自己重写一个打印函数,即让disPlay中cur指向newHead即可;
2.返回链表中间结点,若中间结点有两个则返回第二个,这个题不难,但是加上只准遍历链表一遍就有意思了,也就是当链表从头走到尾只有一次,那么数链表有几个元素再找中间元素就不可行了,那解决方法就是快慢指针法,定义一个fast和一个slow指向头结点,fast一次走两步,slow一次走一步,偶数个元素情况下,fast为空,slow就刚好走到了中间结点,奇数个元素fast.next为空slow走到了中间结点,代码如下:
public Node midNum(){
Node fast=this.head;
Node low=this.head;
while (fast!=null&&fast.next!=null){ //主要针对偶数情况,例如1234,fast到3,low在2,fast到4的下一个,while要跳出,
fast=fast.next.next; //但要防止fast。next为空,所以将fast!=null放前面
low=low.next;
}
return low;
}
循环判定条件一定要将fast!=null放在fast.next!=null之前,因为当元素个数为偶数个会出现空指针异常
3.给定一个链表,输出倒数第K个结点,这个题和快慢指针很相似,先将fast和slow指向头结点,然后fast向后走K步,当fast为空,slow就是我们要找的结点。如下:
public class Solution {
public ListNode FindKthToTail(ListNode head,int k) {
ListNode cur=head;
ListNode curNext=head;
if(head==null)return null;
while((k--)>0){
curNext=curNext.next;
if(curNext==null&&k==0)return cur;
else if(curNext==null&&k>0) return null;
}
while(curNext!=null){
cur=cur.next;
curNext=curNext.next;
}
return cur;
}
}
需要注意的是slow也就是代码中的curNext已经空了但是K仍然大于0,说明K值都比链表长了,要返回null,还有当头是空说明没元素,直接返回null就可以了
- 将两个有序链表合并为一个新的有序链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。实现方法有两种,可以递归也可以迭代
class Solution {
public ListNode mergeTwoLists(ListNode l1, ListNode l2) {
if(l1==null)return l2;
if(l2==null)return l1;
if(l1.val<l2.val){
l1.next=mergeTwoLists(l1.next,l2);
return l1;
}
l2.next=mergeTwoLists(l1,l2.next);
return l2;
}
}
图中使用的是递归,逻辑难理解但是代码少,迭代法代码多但是好理解,在此我只提供一些思路,
首先new一个新结点,定义一个cur结点指向新结点,判断l1和l2的值大小,谁小就让cur的下一个拼接谁,然后这cur往后走一步,同时被拼接的链表结点也得往后走一步,否则就死循环了;最后返回新结点的next;因为在这个过程中cur已经到拼接后链表的最后一个元素,而新结点的下一个存储的是拼接后链表的头;
5.以给定值x为基准将链表分割成两部分,所有小于x的结点排在大于或等于x的结点之前 。解决方法与上一题差不多,new 两个新结点,然后遍历链表,值小于X就拼接到第一个新结点后,这两结点都往后走一步,值大于X就往第二个新结点后拼接,然后这两个新结点都往后走一步,当链表为空就将第一个新结点链表的尾拼接到第二个新结点的头的下一个,因为new的新结点是不需要的;代码如下:
public class Partition {
public ListNode partition(ListNode pHead, int x) {
// write code here
if(pHead==null||pHead.next==null)return pHead;
ListNode res=new ListNode(0);
ListNode ans=new ListNode(0);
ListNode rs=res;
ListNode as=ans;
while(pHead!=null){
if(pHead.val<x){
rs.next=new ListNode(pHead.val);
rs=rs.next;
}else{
as.next=new ListNode(pHead.val);
as=as.next;
}
pHead=pHead.next;
}
if(res.next==null)return ans.next;
rs.next=ans.next;
return res.next;
}
}
6.在一个排序的链表中,存在重复的结点,请删除该链表中重复的结点,重复的结点不保留,返回链表头指针。 例如,链表1->2->3->3->4->4->5 处理后为 1->2->5。
解决方法是定义一个prev和Next,prev先指向头,Next指向头的下一个,再定义一个新结点,让cur指向新结点,只要prev和Next不一样,就让cur.next指向prev,如果一样让Next往后走直到不一样的地方,prev指向Next,Next指向Next.next,此时得注意若链表最后几个都一样和只剩最后一个元素不一样,那么当链表最后几个都一样,Next走到空了就让cur.next指向null,当只剩最后一个不一样,当Next.next==null了,得让cur.next指向Next;
public class Solution {
public ListNode deleteDuplication(ListNode pHead)
{
ListNode res=new ListNode(0);
ListNode cur=res;
ListNode prev=pHead;
if(pHead==null||pHead.next==null)return pHead;
ListNode Next=pHead.next;
while(Next!=null){
if(prev.val!=Next.val){
cur.next=prev;
cur=cur.next;
prev=prev.next;
Next=Next.next;
}else{
while(prev.val==Next.val){
Next=Next.next;
if(Next==null){
cur.next=null;
return res.next;
}
}
if(Next.next==null){
cur.next=Next;
return res.next;
}
prev=Next;
Next=Next.next;
}
}
return res.next;
}
}
7.链表的回问结构,即满足1 2 2 1对称,就是回文结构,而1 2 1不是回文结构
这个题很简单就不过多讲解了,只需要将链表元素放入数组进行判断即可
public class PalindromeList {
public boolean chkPalindrome(ListNode A) {
// write code here
int[] arr=new int[900];
int i=0;
int j=0;
while(A!=null){
arr[i++]=A.val;
A=A.next;
}
i--;
while(i>j){
if(arr[i]==arr[j]){
i--;
j++;
}else return false;
}
return true;
}
}
8.输入两个链表,找公共结点,对于 3 1 2 4和5 7 2 4,在2 处两链表引用同一结点
找公共结点就是直接判断结点引用是否相等而不是值是否相等。
这个题就很巧妙,解决方法是定义两个结点a和b分别指向长链表头和短链表头,当b走到null,a走了b的长度个,同时b指向a开始指向的链表头,当a到null时,指向b开始指向的链表头,此时就相当于a走了a+b步,b走了a+b步,若存在相同的引用必然会在某一结点相遇;返回该结点即可
public class Solution {
public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
if(headA==null||headB==null)return null;
ListNode pa=headA;
ListNode pb=headB;
while(pa!=pb){
pa=pa==null?headB:pa.next;
pb=pb==null?headA:pb.next;
}
if(pa!=null)return pa;
else return null;
}
}
9.给定一个链表,判断链表中是否有环。
我看到这个题首先就想到我遍历它1000次,结点还没到空就肯定有环,但是我万万没想到后台测试数据还真就有1000个,那我就遍历10000次,这次总可以了吧,然后用时23ms才通过,超越5%的人,估计那些比我还慢的人用的方法可能更神奇,当然这肯定不是面试时候的方法,只能开开玩笑。
解决方法还是快慢指针法,设头到入环走a步(未知),环到fast和slow相遇结点走b步(已知),当fast走X步,slow总是走X/2步,这个在下一题中将会非常重要,一定要理解,理解,理解!!!
public class Solution {
public boolean hasCycle(ListNode head) {
ListNode fast=head;
ListNode slow=head;
if(head==null)return false;
do{
if(fast.next==null)return false;
fast=fast.next.next;
slow=slow.next;
if(fast==null)return false;
}while(fast!=slow);
return true;
}
}
最终slow出来的时候,slow为环中的一个结点,这个结果也将用于下一题
10.给定一个链表,返回链表开始入环的第一个节点。 如果链表无环,则返回 null。
要找入环第一个结点,怎么找,衔接上一题的结论继续走可知,slow是环内结点,而slow已经走了a+b步,当slow再走a步就到了头结点,也就是slow走a步相当于后退b步,因为slow在环里面,这个循环关系要搞清楚,所以定义一个结点指向头结点,让该结点和slow同时往后走a步,就到了第一次入环的地方
public class Solution {
public ListNode detectCycle(ListNode head) {
if(head==null)return null;
ListNode newHead=head;
ListNode fast=head;
ListNode slow=head;
do{
if(fast.next==null)return null;
fast=fast.next.next;
slow=slow.next;
if(fast==null)return null;
}while(fast!=slow);
if(slow==null)return null;
while(newHead!=slow){
slow=slow.next;
newHead=newHead.next;
}
return newHead;
}//头到入环长为a,slow到相遇地方走a+b,fast比slow多走a+b
//那么slow再走a,即slow后退b,就到了入环的地方,所以让newHead从头出发,slow再走a,
//相遇就是入环地方
}
还有一种麻烦的方法,但容易想的来,就是先判断有没有环,有的话,判断slow走几步可以回去就确定了环元素个数count,然后定义新结点指向头,让环循环,与新结点对比,如果环循环一次没有和新结点相同的,说明新结点不是入环结点,让新结点往后走一步,直到找到相同结点,就找到了入环节点,但是计算量将会相当的大。
以上就是链表的进阶操作,如果还想了解高阶链表操作,我还不会!!!