链表

相关算法

 

链表结构

/**
 * 算法链表结构
 */
public class ListNode {
     public int val;
     public ListNode next;
     ListNode() {}
     public ListNode(int val) { this.val = val; }
     ListNode(int val, ListNode next) { this.val = val; this.next = next; }

    public static ListNode build(Integer... obj){
         return build(Arrays.asList(obj));
    }
    public static ListNode build(List<Integer> arr){
         if(arr.isEmpty()){
             return null;
         }

        ListNode listNode = new ListNode(arr.get(0));
         ListNode cur=listNode;
        for (int i = 1; i < arr.size(); i++) {
            cur.next=new ListNode(arr.get(i));
            cur=cur.next;
        }
        return listNode;
    }

    /**
     * 构造成环的链表
     * @param index 成环的位置
     * @param obj 链表节点内容
     * @return
     */
    public static ListNode buildCycle(Integer index,Integer... obj){
        List<Integer> arr = Arrays.asList(obj);
        ListNode listNode = new ListNode(arr.get(0));
        ListNode cur=listNode;
        ListNode pos=null;
        for (int i = 1; i < arr.size(); i++) {
            if(index==i){
                pos=cur;
            }
            cur.next=new ListNode(arr.get(i));
            cur=cur.next;
        }
        cur.next=pos;
        return listNode;
    }

    public void print(){
         ListNode node=this;
         while(node!=null){
             System.out.print(node.val+",");
             node=node.next;
         }
        System.out.println();
    }
 }

反转链表

题目给定链表头结点,要求原地反转该链表,返回链表新的头结点。 例:1->2->3,反转后链表应为3->2->1,返回节点3;
思路

/**
     * 这种思路的核心在于不需要动head,真正原地移动,但需要构建一个假节点方便返回
     * pre-1-2-3
     * 拿掉2,所以要先获取到2;
     * 1和3建立连接;变为pre-1-3   2-3
     * 2指向pre后面的节点 pre-1-3  2-1-3
     * pre指向2 pre-2-1-3 
     * 全程不需要动head,当head或者head.next为空时,表明最后一个节点也移动到了pre后面
     * 因此返回pre.next
     */
    static ListNode reverse(ListNode head) {
        ListNode pre=new ListNode(-1);
        pre.next=head;
        ListNode next;
        //pre-1-2-3
        while(head!=null&&head.next!=null){
            //next=2
            next=head.next;
            //1-3
            head.next=next.next;
            //2-1
            next.next=pre.next;
            //pre-2
            pre.next=next;
        }
        return pre.next;
    }
    /**
     * 这个解法的思路我称之为接头霸王:核心构建一个新链表,节点为null。即同时存在null 1-2-3,然后一个一个让右边这个链表的头摘下来指向左边的头
     * null 1-2-3
     * 1-null 2-3
     * 2-1-null 3
     * 3-2-1-null null
     * 右边链表头没了之后,要让next作为头,所以还要先获取next
     * 左边链表有了新头之后,要作为pre,给下个头做准备,因此pre=head
     */
    static ListNode reverse2(ListNode head) {
        ListNode pre=null,next=null;
        while(head!=null){
            next=head.next;
            //接头
            head.next=pre;
            //左边链表更新
            pre=head;
            //右边链表更新
            head=next;
        }
        return pre;
    }

链表环节点

判断是否成环

题目给你一个链表的头节点 `head` ,判断链表中是否有环。
思路类比于龟兔赛跑,只要存在环,那么两个不一样速度的指针终会相遇,因此,采用快慢指针来做实现

static Boolean hasCycle(ListNode head) {
        ListNode slow = head,fast = head;
        while(slow!=null&&fast!=null){
            slow=slow.next;
            fast=fast.next;
            if(fast!=null){
                fast=fast.next;
                if(slow==fast){
                    return true;
                }
            }
        }
        return false;
}

找到成环的首节点

题目给定一个链表的头节点 head ,返回链表开始入环的第一个节点。 如果链表无环,则返回 null。
思路当快慢指针相遇时,快指针走过的路是慢指针走过的路的2倍; 假设相遇是在b点, fast=a+b+c; slow=a+b; fast=2slow=2(a+b)=a+b+c; c=a+b或者说a=c-b; 既然当前slow处于b,那么slow再走c-b就到了相遇的节点,而c-b正好是a,所以让fast从链表的头节点开始一步一步走,当和slow相遇时,即表明走了a步,那就是环节点。

static ListNode detectCycle(ListNode head) {
        ListNode slow = head,fast = head;
        while(slow!=null&&fast!=null){
            slow=slow.next;
            fast=fast.next;
            if(fast!=null){
                fast=fast.next;
                if(slow==fast){
                    fast=head;
                    while(slow!=fast){
                        fast=fast.next;
                        slow=slow.next;
                    }
                    return fast;
                }
            }
        }
        return null;
}

删除链表的倒数第N个节点

思路:

典型的双指针应用,通过先让fast指针走n步,然后slow和fast一起走,当fast走到头的时候,slow走到的就是倒数第n个节点。由于要断开,因此还要记录old节点。

public ListNode removeNthFromEnd(ListNode head, int n) {
        ListNode old=new ListNode(-1,head),result=old;
        ListNode slow=head,fast=head;
        while(n>0){
            fast=fast.next;
            n--;
        }
        while(fast!=null){
            old=slow;
            slow=slow.next;
            fast=fast.next;
        }
        old.next=slow!=null?slow.next:null;
        return result.next;
    }

旋转链表

给你一个链表的头节点 head ,旋转链表,将链表每个节点向右移动 k 个位置。

思路:

思路1:多次旋转链表

如例图所示,首先将整个链表反转,即变为54321,然后反转前k个链表45321,然后反转后k个链表45123

思路2:先遍历整个链表,获取到链表的长度n,然后计算真正要移动的k=k%n;让快指针走k步,当快指针走到尾结点时,慢指针刚好走到待移动的节点,返回结果是慢指针,然后让快指针指向head。(有些边界细节要优化,但思路是这样的),时间复杂度是O(2n)

思路3:对于思路2的进一步优化,核心有两点,首先还是要让快指针走,当快指针已经走了k步的时候,表明k<=n的,那么慢指针开始走,当快指针走到tail的时候,慢指针就走到了新的head。当k>n的时候,快指针走到tail,slow也还是head,此时已经有了n,因此直接计算k=k%n,让慢指针走n-k步,就可以找到新的head,该思路的时间复杂度是O(n+k)

代码:

这里只展示第三种思路的代码,其他逻辑请看 代码

// @Title: 旋转链表 (Rotate List)
// @Author: qisiii
// @Date: 2024-09-25 15:16:48
// @Runtime: 0 ms
// @Memory: 41.6 MB
// @comment: 一种只需要遍历O(n+k)次的解法
// @flag: GREEN
/**
 * 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; }
 * }
 * 遍历次数最多O(n+k)次
 * 当k<n,通过双指针去找到第k个节点和第n个节点,返回结果是k.next,让n.next=head即可
 * 当k>n,此时双指针中fast走到最后,只需要让slow指针再走n-k步即可,当然n=n%k,这里面n正好是k的倍数的时候,是不用旋转的,因此直接return head;
 */
class Solution {
    public ListNode rotateRight(ListNode head, int k) {
        if(k==0||head==null){
            return head;
        }
        int count = 0;
        ListNode fast = head;
        ListNode slow = head;
        //当k<=length的时候,通过让fast先走k步找到两个链表的尾结点,如例1中的3和5
        while (fast.next != null) {
            fast = fast.next;
            if (count >= k) {
                slow = slow.next;
            }
            count++;
        }
        //当k>length的时候,此时fast已经处于尾部,接下来只需要让slow移动(length-k%length)步就可以了,
        //还是拿例1距离,假如n为7,length是5,此时length-2就是slow要走的步数
        if (slow==head) {
            int length=count+1;
            //如果k刚好是length的倍数,那就不用旋转
            if(k%length==0){
                return slow;
            }
            count=length-k%length-1;
            while(count>0){
                slow=slow.next;
                count--;
            }
        }
        ListNode result = slow.next;
        fast.next = head;
        slow.next = null;
        return result;
    }
}
Licensed under CC BY-NC-SA 4.0
最后更新于 2024-10-18