刷题笔记之链表

这篇博客探讨了链表数据结构,包括单链表和双链表的操作,如添加、删除结点,以及如何设计链表。文章重点讲述了双指针技巧在链表中的应用,例如在环形链表、寻找链表中点问题上的策略。还提供了多个相关编程题目的解题思路,如141.环形链表、142.环形链表2、160.相交链表等。
摘要由CSDN通过智能技术生成

前言

本博客是对链表做相关练习时所做笔记,主要内容来源链表

  • 与数组相似,链表也是一种线性数据结构。
  • 链表有两种类型:单链表和双链表。
  • 与数组相似,双指针技巧在链表中也常被使用。

文中主要介绍了单链表与双链表的基本操作,以及双指针在链表中的应用。后续会不断增加一些题目的练习。

单链表

  • 单链表中的每个结点不仅包含,还包含链接到下一个结点的引用字段。通过这种方式,单链表将所有结点按顺序组织起来。

  • 结点结构

    // 单链表定义
    public class SinglyListNode {
        int val; //值
        SinglyListNode next; //下一个结点的引用字段
        SinglyListNode(int x) { 
            val = x; 
        }
    }
    

    在大多数情况下,我们将使用头结点(第一个结点)来表示整个列表。

  • 操作

    添加操作:让新来的结点有所指向

    1. 在给定的结点 prev 之后添加新结点cur

      链表1

      cur.next = prev.next;
      prev.next = cur;
      
    2. 添加头结点

      cur.next = head;
      head = cur;
      

    删除操作

    1. 从单链表中删除现有结点 cur

      找到 cur 的上一个结点 prev 及其下一个结点 next ;链接 prevcur 的下一个节点 next

      prev.next = prev.next.next;
      

      因此,关键在于找到 cur 的上一个结点 prev,这个过程的时间复杂度为O(n)

    2. 删除第一个结点

      想要删除第一个结点,可以简单地将下一个结点分配给 head

  • 为了让第一个结点不再特殊,我们常常可以在链表最前面额外地添加一个哑结点哨兵结点来作为头结点,这样结构永远不为空。其在树和链表中被广泛用作伪头、伪尾等,通常不保存任何数据,用来简化插入和删除操作。

707.设计链表

  • 题目描述:设计链表的实现。您可以选择使用单链表或双链表。假设链表中的所有节点都是 0-index 的。

  • 示例

    MyLinkedList linkedList = new MyLinkedList();
    linkedList.addAtHead(1);
    linkedList.addAtTail(3);
    linkedList.addAtIndex(1,2);   //链表变为1-> 2-> 3
    linkedList.get(1);            //返回2
    linkedList.deleteAtIndex(1);  //现在链表是1-> 3
    linkedList.get(1);            //返回3
    
  • 分析

    添加一个哨兵结点可大大简化操作。

  • 代码

    class Node {
        int val;
        Node next;
        public Node(int val) {
            this.val = val;
        }
    }
    
    class MyLinkedList {
        Node head; //头结点
        int size; //记录链表的大小
    
        /**
         * Initialize your data structure here.
         */
        public MyLinkedList() {
            head = new Node(0); //添加一个哨兵结点作为头结点,可简化操作
            size = 0;
        }
    
        /**
         * Get the value of the index-th node in the linked list. If the index is invalid, return -1.
         */
        public int get(int index) {
            if (index < 0 || index >= size) return -1;
            Node cur = head;
            for (int i = 0; i <= index; i++) {
                cur = cur.next;
            }
            return cur.val;
        }
    
        /**
         * Add a node of value val before the first element of the linked list. After the insertion, the new node will be the first node of the linked list.
         */
        public void addAtHead(int val) {
            addAtIndex(0, val);
        }
    
        /**
         * Append a node of value val to the last element of the linked list.
         */
        public void addAtTail(int val) {
            addAtIndex(size, val);
        }
    
        /**
         * Add a node of value val before the index-th node in the linked list. If index equals to the length of linked list, the node will be appended to the end of linked list. If index is greater than the length, the node will not be inserted.
         */
        public void addAtIndex(int index, int val) {
            if (index > size) return;
            if (index < 0) index = 0;
            //找到第index结点前面的那个结点
            Node prev = head;
            for (int i = 0; i < index; i++) {
                prev = prev.next;
            }
            Node cur = new Node(val);
            cur.next = prev.next;
            prev.next = cur;
            size++;
        }
    
        /**
         * Delete the index-th node in the linked list, if the index is valid.
         */
        public void deleteAtIndex(int index) {
            if (index < 0 || index >= size) return;
            //找到第index结点前面的那个结点
            Node prev = head;
            for (int i = 0; i < index; i++) {
                prev = prev.next;
            }
            prev.next = prev.next.next;
            size--;
        }
    }
    

双指针技巧

  • 我们在数组中已经使用过双指针技巧,有两种情况:
    1. 两个指针从不同位置出发:一个从始端开始,另一个从末端开始;
    2. 两个指针以不同速度移动:一个指针快一些,另一个指针慢一些。
  • 对于单链表,由于只能在一个方向上遍历链表,所以一般使用第二种即快慢指针。其在判断链表是否有环、寻找链表中点等问题上非常有用。

141.环形链表

  • 题目描述:给定一个链表,判断链表中是否有环。

    如果链表中有某个节点,可以通过连续跟踪 next 指针再次到达,则链表中存在环。 为了表示给定链表中的环,我们使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。 如果 pos 是 -1,则在该链表中没有环。注意:pos 不作为参数进行传递,仅仅是为了标识链表的实际情况。

    如果链表中存在环,则返回 true 。 否则,返回 false 。

  • 示例

    输入:head = [3,2,0,-4], pos = 1
    输出:true
    解释:链表中有一个环,其尾部连接到第二个节点。
    
  • 分析

    本题可直接用哈希表来判断结点是否再次出现即可,时间复杂度为O(n),空间复杂度为O(n)

    更优的解法就是使用快慢指针,初始时快慢指针都指向头结点。快指针每次走两步,慢指针每次走一步,如果能再次相遇,说明有环,否则,一直到快指针走到链表尾,证明无环。

    关键在于快慢指针的步长,不能过大,也不能过小。

  • 代码

    /**
     * Definition for singly-linked list.
     * class ListNode {
     *     int val;
     *     ListNode next;
     *     ListNode(int x) {
     *         val = x;
     *         next = null;
     *     }
     * }
     */
    public class Solution {
        public boolean hasCycle(ListNode head) {
            ListNode fast = head;
            ListNode slow = head;
    
            while(fast != null && fast.next != null) {
                //快指针每次走两步
                fast = fast.next.next;
                //慢指针每次走一步
                slow = slow.next;
                //两个指针相遇时证明有环
                if(fast == slow) {
                    return true;
                }
            }
    
            return false;
        }
    }
    

142.环形链表2

  • 题目描述:在环形链表的基础上,返回链表开始入环的第一个节点。 如果链表无环,则返回 null

  • 示例

    输入:head = [3,2,0,-4], pos = 1
    输出:返回索引为 1 的链表节点
    解释:链表中有一个环,其尾部连接到第二个节点
    
  • 分析

    依旧使用快慢指针,快指针每次走两步,慢指针每次走一步。

    假设从头结点到入环点的距离为a,从入环点到相遇点(Q)的距离为b,从相遇点(Q)到入环点的距离为c,如下图:

    142题

    最后得到的等式为a=(n-1)(b+c)+c,其中b+c为环的长度,也就是说在相遇点时,如果我们将一个指针first指向头结点,与慢指针同时移动,最终会在入环点相遇,此时first走了a的距离,慢指针走了n-1圈加上c

  • 代码

    /**
     * Definition for singly-linked list.
     * class ListNode {
     *     int val;
     *     ListNode next;
     *     ListNode(int x) {
     *         val = x;
     *         next = null;
     *     }
     * }
     */
    public class Solution {
        public ListNode detectCycle(ListNode head) {
            ListNode fast = head;
            ListNode slow = head;
            ListNode first = head;
            while(fast!=null && fast.next!=null) {
                fast = fast.next.next;
                slow = slow.next;
                //到达相遇点
                if(fast == slow) {
                    //first必与slow在入环的第一个结点相遇
                    while(first != slow) {
                        first = first.next;
                        slow = slow.next;
                    }
                    return first;
                }
            }
            return null;
        }
    }
    

160.相交链表

  • 题目描述:编写一个程序,找到两个单链表相交的起始节点。如果两个链表没有交点,返回null,假定链表没有循环。

  • 分析

    比较直观的思路就是首先比较两个链表的长度,然后让较长链表的头结点指针往后移,直至剩余的结点长度与较短的链表长度一致,之后两个链表的头结点同时移动,直至指向相同的结点,即找到了交点。这种方法代码写起来比较繁琐,因为需要求两个链表的长度,并判断它们的长短。

    一种较为简单的方法就是首先创建两个指针p1p2,分别指向链表A和B的头结点,同时向后遍历;若p1到达尾部,则让它重新指向链表B的头结点,若p2到达尾部,则让它重新指向链表A的头结点,最终它们会相遇在交点。原理如下图:

    在这里插入图片描述

    p1走过的距离为a+c+bp2走过的距离为b+c+a,最终相遇在交点。

  • 代码

    /**
     * Definition for singly-linked list.
     * public class ListNode {
     *     int val;
     *     ListNode next;
     *     ListNode(int x) {
     *         val = x;
     *         next = null;
     *     }
     * }
     */
    public class Solution {
        public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
            ListNode p1 = headA;
            ListNode p2 = headB;
            while(p1 != p2) {
                //p1到达尾部,则让它重新指向链表B的头结点
                if(p1 == null) p1 = headB;
                else p1 = p1.next;
                //p2到达尾部,则让它重新指向链表A的头结点
                if(p2 == null) p2 = headA;
                else p2 = p2.next;
                
            }
            return p1;
        }
    }
    

19.删除链表的倒数第 N 个结点

  • 题目描述:给你一个链表,删除链表的倒数第 n 个结点,并且返回链表的头结点。

  • 示例

    输入:head = [1,2,3,4,5], n = 2
    输出:[1,2,3,5]
    
  • 分析

    本题依旧是双指针技巧在链表中的典型应用。

    要找到倒数第n个结点,我们可以考虑先使一个指针指向正数第n个结点,然后再令另一个指针指向头结点,使两个指针同时移动,当第一个指针到达链表最后一个结点时,后一个指针恰好指向倒数第n个结点。

    而我们要删除此结点,就必须找到其前一个结点,因为涉及到删除操作,为了使第一个结点不再特殊,我们首先可以考虑增加一个哑结点。只要我们将第二个指针初始时指向哑结点,就能找到要删除结点的前一个结点。

  • 代码

    /**
     * Definition for singly-linked list.
     * public class ListNode {
     *     int val;
     *     ListNode next;
     *     ListNode() {}
     *     ListNode(int val) { this.val = val; }
     *     ListNode(int val, ListNode next) { this.val = val; this.next = next; }
     * }
     */
    class Solution {
        public ListNode removeNthFromEnd(ListNode head, int n) {
            //增加一个哨兵结点作为头结点
            ListNode dummy = new ListNode(0,head);
            ListNode p1 = dummy, p2 = dummy;
            //使p2指向正数第n个结点
            for(int i=0; i<n; i++) {
                p2 = p2.next;
            }
            //p1,p2同时移动,p2到达尾结点时,p1到达倒数第n个结点前一个结点
            while(p2.next != null) {
                p1 = p1.next;
                p2 = p2.next;
            }
            //删除
            p1.next = p1.next.next;
            return dummy.next;
        }
    }
    

双链表

  • 与单链表不同的是,双链表的每个结点中都含有两个引用字段

  • 结点结构

    class DoublyListNode {
        int val;
        DoublyListNode next, prev;
        DoublyListNode(int x) {val = x;}
    }
    

    与单链表一样,也是使用头结点来表示整个列表。

  • 操作

    • 添加操作:在现有的结点 prev 之后插入一个新的结点 cur

      链表2

      //首先将 cur 与 prev 和 next(prev的下一个结点)连接
      cur.next = prev.next;
      cur.prev = prev;
      //再处理剩余指向
      prev.next = cur;
      cur.next.prev = cur;
      

      对于开头和结尾,除了可以分情况处理外,也可以使用虚拟头结点和尾结点,添加操作就统一了。

    • 删除操作:删除一个现有的结点 cur

      与单链表不同,使用prev字段可以很容易地在常量时间内获得前一个结点。

      cur.prev.next = cur.next;
      

      对于开头和结尾,除了可以分情况处理外,也可以使用虚拟头结点和尾结点,删除操作就统一了。

题目练习

206.反转链表

  • 题目描述:给你单链表的头结点 head ,请你反转链表,并返回反转后的链表。链表可以选用迭代或递归方式完成反转。你能否用两种方法解决这道题?

  • 分析

    对于迭代法,可以从前往后遍历链表,把当前结点移出链表并插到链表的头部(头插法),这样一次遍历就完成了链表的反转。需要注意的细节就是为了能找到下一个要遍历的结点,我们可以增加一个指针prev,指向要遍历的结点的前一个结点。

    对于递归,相对来说复杂一点,假设链表的其余部分已经被反转,那么关键就在于如果反转前面的部分。对于当前结点cur,如果它后面的部分已经反转,要反转它,就要使cur.next.next = cur; cur.next = null;

  • 代码

    迭代方式

    class Solution {
        public ListNode reverseList(ListNode head) {
            if(head==null || head.next==null) return head;
    
            ListNode prev = head, cur = head.next;
            while(cur != null) {
                //将当前结点移出链表
                prev.next = cur.next;
                //将当前结点插到头部
                cur.next = head;
                head = cur;
                cur = prev.next;
            }
    
            return head;
        }
    }
    

    递归方式

    class Solution {
        public ListNode reverseList(ListNode head) {
            if(head==null || head.next==null) return head;
            //反转链表的其余部分
            ListNode newHead = reverseList(head.next);
            //对当前结点进行反转
            head.next.next = head;
            head.next = null;
            return newHead;
        }
    }
    

203.移除链表元素

  • 题目描述:给你一个链表的头节点 head 和一个整数 val ,请你删除链表中所有满足 Node.val == val 的节点,并返回 新的头节点

  • 分析

    较为简单,需要注意的是涉及的增删操作加一个虚拟头结点往往更加简单。

  • 代码

    class Solution {
        public ListNode removeElements(ListNode head, int val) {
            ListNode dummy = new ListNode(0,head);
            ListNode prev = dummy;
            while(prev.next != null) {
                //等于val时删除结点
                if(prev.next.val == val) {
                    prev.next = prev.next.next;
                }else{
                    //不等于val时就往后移动
                    prev = prev.next;
                }
            }
            return dummy.next;
        }
    }
    

328.奇偶链表

  • 题目描述:给定一个单链表,把所有的奇数节点和偶数节点分别排在一起。请注意,这里的奇数节点和偶数节点指的是节点编号的奇偶性,而不是节点的值的奇偶性。请尝试使用原地算法完成。

  • 示例

    输入: 1->2->3->4->5->NULL
    输出: 1->3->5->2->4->NULL
    
  • 分析

    我们可以通过迭代的方式将奇数结点和偶数结点分离成奇数链表和偶数链表,然后将偶数链表连接在奇数链表之后,合并后的链表即为结果链表。

    在迭代的过程中,对于每一对奇偶结点,由于奇数结点在偶数结点前面,因此每一步首先更新奇数结点,然后更新偶数结点。

  • 代码

    class Solution {
        public ListNode oddEvenList(ListNode head) {
            if(head==null || head.next==null) return head;
            //奇数结点的指针
            ListNode odd = head;
            //偶数结点的指针
            ListNode evenHead = head.next, even = evenHead;
    
            //通过迭代的方式将奇数结点和偶数结点分离成两个链表
            while(even!=null && even.next!=null) {
                //更新奇数结点
                odd.next = even.next;
                odd = odd.next;
                //更新偶数结点
                even.next = odd.next;
                even = even.next;
            }
            //合并奇偶链表
            odd.next = evenHead;
            
            return head;
            
        }
    }
    

234.回文链表

  • 题目描述:请判断一个链表是否为回文链表。

  • 示例

    输入: 1->2
    输出: false
    输入: 1->2->2->1
    输出: true
    
  • 分析

    如果不考虑空间复杂度,我们可以直接将链表转化为数组,然后再判断就非常简单了。

    而如果要就地判断,可以参考206题反转链表,我们可以先找到链表中点,然后将链表中点后的结点反转,再通过双指针比较判断就非常简单了。当然,最后可以再将链表结构恢复,不过本题未做要求。

  • 代码

    class Solution {
        public boolean isPalindrome(ListNode head) {
            if(head==null || head.next==null) return true;
            //使用快慢指针找到前半部分的尾结点
            ListNode slow=head, fast=head.next;
            while(fast!=null && fast.next != null) {
                slow = slow.next;
                fast = fast.next.next;
            }
            //反转后半部分链表
            slow.next = reverseList(slow.next);
            ListNode cur = slow.next;
            //判断是否是回文
            while(cur != null) {
                if(head.val != cur.val) return false;
                head = head.next;
                cur = cur.next;
            }
            //恢复链表
            slow.next = reverseList(slow.next);
            return true;
        }
        //反转链表
        public ListNode reverseList(ListNode head) {
            if(head==null || head.next==null) return head;
            ListNode prev = head, cur = head.next;
            while(cur != null) {
                //将当前结点移出链表
                prev.next = cur.next;
                //将当前结点插到头部
                cur.next = head;
                head = cur;
                cur = prev.next;
            }
            return head;
        }
    }
    
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值