链表的常用操作

在链表的学习过程中,为了加深对链表相关知识的巩固与理解,因此做了一些LeetCode上的题。一下都是一些比较典型的链表相关的编程题,同时附上解题思想,一方面作为自己的学习总结,另一方面也帮助大家更好的理解一些编程思想。
1、删除链表中等于给定值 val 的所有节点
思想:遍历链表,先处理头节点head为val的所有情况,然后再处理中间节点为val的所有情况。
若head.val=val,则让head=head.next, 此处用循环判断的目的是防止链表前不止一个节点的值为val,
在处理中间节点为val的情况,cur为我们设置的当前节点的引用,cur从头开始遍历链表,若cur.next=val,则让cur.next=cur.next.next;此处注意两点,因为我们之前已经处理过头节点为val的情况,因此此时再遍历链表的时候,头节点的值已经不可能为val,因此cur从头开始遍历的时候,我们只需关注cur.next是否为val了,第二点要注意的是,当cur.next=val时,我们要删除cur.next节点的时候需要做操作cur.next=cur.next.next,因此就需要对cur.next进行判空,否则就会抛出空指针异常。

/**
 * Definition for singly-linked list.
 * public class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode(int x) { val = x; }
 * }
 */
class Solution {
     public ListNode removeElements(ListNode head, int val) {
         if(head==null){
             return null;
         }        
        while(head.val==val){
            if(head.next!=null)
               head=head.next;
            else
                return null;
        }
        //二次判空是为了防止整个链表的所有结点值都等于val的情况
         if(head==null){
             return null;
         }
         //此时已处理了头节点为val的所有情况
         ListNode cur=head;
        while(cur.next!=null){
            if(cur.next.val==val){
                cur.next=cur.next.next;
            }else{
                cur=cur.next;
            }  
        }
        return head;
    }
}

2、反转一个单链表
反转一个单链表可以有两种思想:
(1)先定义一个头结点的引用,引用对象为新链表的头,之后再对原链表进行头删,头删的元素再头插到新链表中,由此便可对原链表进行反转。
(2)定义3个引用分别为p1,p2,p3,刚开始让p1指向null,p2指向原链表的头,p3=p2.next;为了方便理解,下面画图来解释,
每次让p3=p2.next,再让p2.next=p1,最后再让p1,p2,p3向后移
在这里插入图片描述
最后我们只需再判断p2==null,然后返回p1即可。
在这里插入图片描述

考虑到效率问题,我们直接用第二种思路实现单链表的反转(我们刚开始让p1=head,其实并不影响,只需最后让尾节点的next为null即可)

/**
 * Definition for singly-linked list.
 * public class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode(int x) { val = x; }
 * }
 */
class Solution {
    public ListNode reverseList(ListNode head) {
        if(head==null||head.next==null){
            return head;
        }
        ListNode p1=head;
        ListNode p2=head.next;
        ListNode p3=null;
        while(p2!=null){
            p3=p2.next;
            p2.next=p1;
            p1=p2;
            p2=p3;         
        }
        head.next=null; //尾节点置空
        head=p1;
        return head;
    }
}

3、给定一个带有头结点 head 的非空单链表,返回链表的中间结点。如果有两个中间结点,则返回第二个 中间结点。
思想:采用快慢指针的方式,定义两个引用(这里我们用指针来描述)fast和slow,刚开始fast和slow都指向链表的头节点,之后让快指针一次走两步,慢指针一次走一步,当快指针走到链表尾的时候,慢指针正好走到链表中间的位置,这个时候慢指针所指向的节点就是我们要求的中间节点了。

/**
 * Definition for singly-linked list.
 * public class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode(int x) { val = x; }
 * }
 */
class Solution {
    public ListNode middleNode(ListNode head) {
        if(head==null){
            return null;
        }
        ListNode fast=head;
        ListNode slow=head;
        //为避免出现对空指针解引用,因此我们需要对fast和fast.next都进行判空操作
        while(fast!=null&&fast.next!=null){
            fast=fast.next.next;
            slow=slow.next;
        }
        return slow;
    }
}

4、 输入一个链表,输出该链表中倒数第k个结点
思想:思想与上一题相同,既然要找链表的倒数第k个节点,我们只需让快指针先走k步,再让快指针和慢指针每次各走一步,那么当快指针走到链表尾的时候,慢指针所指向的位置就是链表的倒数第k个节点。

public class Solution {
    public ListNode FindKthToTail(ListNode head,int k) {
        
        ListNode fast=head;
        ListNode slow=head;
        while((k--)!=0){
            if(fast==null){
                return null;
            }else
            fast=fast.next;
        }
        while(fast!=null){
            fast=fast.next;
            slow=slow.next;
        }
        return slow;
    }
}

5、将两个有序链表合并为一个新的有序链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的
思想:同时遍历两个链表,将其中较小元素的节点插入到新链表的尾部,该链表的指针向后走,新链表的指针向后走,当其中任一链表遍历完后,再将剩下的未遍历完的链表插入到新链表的尾部即可。

class Solution {
    public ListNode mergeTwoLists(ListNode l1, ListNode l2) {
        if(l1==null&&l2==null){
            return null;
        }    
        ListNode newNode=null; //指向新链表的头节点
         ListNode cur=newNode; //指向新链表的当前节点
         ListNode node1=l1; //指向链表1的当前节点
         ListNode node2=l2; //指向链表2的当前节点
         while((node1!=null)&&(node2!=null)){
             if(node1.val<=node2.val){
                 //判断新链表的头节点是否为空
                 if(newNode==null){
                     newNode=node1;
                 }else{ 
                     cur.next=node1;
                 }
                 cur=node1;
                 node1=node1.next;
             }else{
                 if(newNode==null){
                     newNode=node2;  
                 }else{
                     cur.next=node2;
                 }
                 cur=node2;
                 node2=node2.next;
             }
         }
         //将未遍历完的链表插入到新链表的尾部
         if(node1!=null){
           //防止一开始链表2就为null的情况
             if(newNode==null){
                 return node1;
             }
             cur.next=node1;
         }
         if(node2!=null){
          //防止一开始链表1就为null的情况
             if(newNode==null){
                 return node2;
             }
             cur.next=node2;
         }
         return newNode;
    }
}

6、编写代码,以给定值x为基准将链表分割成两部分,所有小于x的结点排在大于或等于x的结点之前
思想:创建两个新链表,其头结点分别为small,big;两个新链表对应的尾节点分别为slast和blast,遍历原链表,当原链表当前节点的值<=x时,将该节点尾插到头节点为small的链表中,当原结点的值>x时,将该节点尾插到头节点为big的链表中,遍历完之后若small链表不为null,再将big链到small链表尾部,返回small即可,若small为空,直接返回big即可;需要注意的是,一定要确保新链表尾节点的next值一定为null,否则会报错(之前在这块出了问题)。

public class Partition {
    public ListNode partition(ListNode pHead, int x) {
        if(pHead==null||pHead.next==null){
            return null;
        }
        ListNode small=null;   //小于等于x的节点组成的新链表的头节点
        ListNode slast=small;  //小于等于x的节点组成的新链表的尾节点
        ListNode big=null;    //大于x的节点组成的新链表的头节点
        ListNode blast=big;  //大于x的节点组成的新链表的尾节点
        ListNode cur=pHead; //原链表的当前节点
        while(cur!=null){
            if(cur.val<x){
                if(small==null){
                    small=cur;
                }else{
                    slast.next=cur;
                }
                slast=cur;
                cur=cur.next;
            }else{
                if(big==null){
                    big=cur;
                }else{
                    blast.next=cur;
                }
                blast=cur;
                cur=cur.next;
            }
        }
        //确保尾节点为null
        if(blast!=null){
            blast.next=null;
        }
        if(small==null){
            return big;
        }else{
            slast.next=big;
            return small;
        }
    }
}

7、 在一个排序的链表中,存在重复的结点,请删除该链表中重复的结点,重复的结点不保留,返回链表头 指针
思想:创建一个新链表的头节点,遍历原链表,当原链表的当前值和下一个节点的值相等时当前节点往后走,不相等时尾插到新链表的结尾(这里注意,若刚开始没有遍历到重复节点,则直接将cur的值尾插到新链表中,若遍历到了重复节点,循环cur向后走,当cur.val!=cur.next.val出循环,此时需让cur再向后走一次才能尾插到新链表中,避免最后一个重复元素不能删除的情况)
注意点:如果链表中只有一个节点,则不存在重复节点,直接返回该链表头即可

public class Solution {
    public ListNode deleteDuplication(ListNode pHead)
    { 
       //先对链表判空
        if(pHead==null){
            return null;
        }
        //链表中只有一个节点
        if(pHead.next==null){
            return pHead;
        }
        ListNode head=new ListNode(0); //先定义一个假的头节点作为新链表的头
        ListNode last=head; //新链表的尾
        ListNode cur=pHead;//原链表的当前节点
        while(cur!=null){
          //如果当前节点和当前节点的下一个节点值重复,指针一直向后走
            if(cur.next!=null&&cur.val==cur.next.val){
                while(cur.next!=null&&cur.val==cur.next.val){
                    cur=cur.next; 
                }
                cur=cur.next;//避免最后一个重复节点不能删除
            }else{ //否则将该节点尾插到新链表的尾
                last.next=cur;
                last=last.next;
                cur=cur.next;
            }
        }
        last.next=null;  //链表的最后要置空
        return head.next;
    }
}

8、链表的回文结构
对于一个链表,请设计一个时间复杂度为O(n),额外空间复杂度为O(1)的算法,判断其是否为回文结构。
思想:这里要利用栈的后进先出的特征。定义一个存储节点类型的栈,从头到尾遍历原链表,将原链表的每一个节点入栈,再次遍历链表,看原链表的当前元素值是否与栈顶元素值相同,若不同直接返回false,若相同,栈中元素出栈,原链表中指向当前元素的指针向后走,直到栈为空,此时链表也遍历到了最后一个元素,则证明该链表为回文结构。
改进:因为回文结构是两边对称相等的,因此我们可以考虑让链表的一半入栈即可,利用前面快慢指针的思想,找到链表的中间节点,然后从该节点开始让其之后的元素依次入栈,那么出栈的时候只要栈中元素出完了,便认为该链表为回文结构,这种方法降低了算法的时间复杂度,提升了效率。

public class PalindromeList {
    public boolean chkPalindrome(ListNode A) {
        Stack<ListNode> stack=new Stack<ListNode>();
        if(A==null&&A.next==null){
            return true;
        }
        ListNode fast=A;
        ListNode slow=A;
        while(fast.next!=null&&fast.next.next!=null){
            fast=fast.next.next;
            slow=slow.next;
        }
        while(slow!=null){
            stack.push(slow);
            slow=slow.next;
        }
        while(!stack.isEmpty()){
            if(A.val!=stack.pop().val){
                return false;
            }else{
                 A=A.next;
            }
        }
        return true;
    }
}

9、 输入两个链表,找出它们的第一个公共结点。
思想:分别计算出两个链表的长度,得到他们的长度差L,然后让较长链表的指针先走L不,再让两个指针一同出发,相遇时的节点便为他们的第一个公共节点。
画图看更明显
在这里插入图片描述
链表1的长度为6,链表2的长度为5,链表1的当前节点p1先走一步到蓝色节点位置,然后两个链表的指针p1,p2同时走,相遇时即为第一个公共节点。

public class Solution {
    public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
        if(headA==null||headB==null){
            return null;
        }
        ListNode p1=headA;
        ListNode p2=headB;
        int a=0;
        int b=0;
        while(p1!=null){
            a++;
            p1=p1.next;
        }
        while(p2!=null){
            b++;
            p2=p2.next;
        }
        int tmp=(b-a)>0?(b-a):(a-b);
        while(tmp!=0){
            if(a>b){
                headA=headA.next;
            }else{
                headB=headB.next;
            }
            tmp--;
        }
        while(headA!=null&&headB!=null){
            if(headA==headB){
                return headA;
            }
            headA=headA.next;
            headB=headB.next;
        }
        return null;
    }
}

10、给定一个链表,判断链表中是否有环
思想:定义两个指针快指针和慢指针,两个指针都从头开始,快指针一次走两步,慢指针一次走一步,若链表带环,两个指针一定能在环内相遇,若没带环,则一定有个出口,以此判断链表是否带环。

public class Solution {
    public boolean hasCycle(ListNode head) {
        if(head==null||head.next==null){
            return false;
        }
        ListNode fast=head;
        ListNode slow=head;
        while(fast.next!=null&&fast.next.next!=null){
            fast=fast.next.next;
            slow=slow.next;
            if(fast==slow){
                return true;
            }
        }
        return false;
    }
}

11、 给定一个链表,返回链表开始入环的第一个节点。 如果链表无环,则返回 NULL
思想:在判断链表有环的基础上,找到环的入口节点,同样利用快慢指针的思想找到两个指针的相遇点cur,找到之后再让一个指针从头开始走,我们假设是fast,fast从头开始走,一次走一步,fast继续从相遇点开始走,一次走一步,当两个指针再次相遇时相遇点便为环的入口点。
证明如下:
用S表示任一时刻慢指针所走的距离,F表示任一时刻快指针所走的距离,环的长度H,两个指针相遇时满足:S2=F;并且F-S=nH (即相遇时快指针比慢指针多走了n个环的长度),为方便计算我们取n=1,也就是说相遇时慢指针走的步数正好是环的长度,如图(图比较丑,大概也能看),假设在m点相遇,由上式推理我们可知a点到b点的距离就等于m点到b点的距离(这里指优弧的长度),当在m点相遇后,让fast从a点出发,slow从m点出发,当下次相遇时一定在b点,即为环的入口点。
在这里插入图片描述

public class Solution {
    public ListNode detectCycle(ListNode head) {
        boolean flag=ishuan(head);
        if(flag==true){
            ListNode fast=head;
            ListNode slow=head;
            int n=0;
            while(true){
                fast=fast.next.next;
                slow=slow.next;
                n++;
                if(fast==slow){
                    break;
                }
            }
            //fast从头出发
            fast=head;
            while(n!=0){
                fast=fast.next;
                n--;
            }
            slow=head;
            while(fast!=slow){
                fast=fast.next;
                slow=slow.next;
            }
            return fast;
        }
        return null;
    }
    //判断是否为环
    public boolean ishuan(ListNode head){
        if(head==null||head.next==null){
            return false;
        }
        ListNode fast=head;
        ListNode slow=head;
        while(fast.next!=null&&fast.next.next!=null){
            fast=fast.next.next;
            slow=slow.next;
            if(fast==slow){
                return true;
            }
        }
        return false;
    }
}

12、给定一个链表,每个节点包含一个额外增加的随机指针,该指针可以指向链表中的任何节点或空节点。 要求返回这个链表的深度拷贝。
思想:我们可以利用映射的关系,将原链表节点与新链表节点一一映射,这样就可通过原链表节点找到新链表节点,从而通过映射建立起指针的相互关系。映射我们可以通过map来实现。 map的key为原链表节点,value为新链表节点。通过原链表key获得新链表value将其返回即可。

class Solution {
    public Node copyRandomList(Node head) {
        if(head==null){
            return null;
        }
        //key为原链表节点,value为新链表节点
        Map<Node,Node> map=new HashMap<>();
        Node cur=head;
        while(cur!=null){
          //复制原链表的每个节点给新链表的当前节点
            map.put(cur,new Node(cur.val));
            cur=cur.next;
        }
        //从原链表的头开始,建立指针的映射
        cur=head;
        while(cur!=null){
            //将原链表的next映射到新链表的next,建立起next指针的相互关系
            map.get(cur).next=map.get(cur.next);
            //将原链表的random映射到新链表的random,建立起random指针的相互关系
            map.get(cur).random=map.get(cur.random);
            cur=cur.next;
        }
        return map.get(head);
    }
}
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值