【Leetcode_Hot100】链表

链表

160. 相交链表

206. 反转链表

234. 回文链表

141. 环形链表

142. 环形链表 II

21. 合并两个有序链表

2. 两数相加

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

25. K 个一组翻转链表

138. 随机链表的复制

148. 排序链表

23. 合并 K 个升序链表

146. LRU 缓存

160. 相交链表

方法一:模拟

依次判断两节点是否相同即可

public class Solution {
    public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
        ListNode nodeA = headA;
        ListNode nodeB = headB;
        int lenA = 0, lenB = 0;

        while(nodeA.next != null) {
            lenA++;
            nodeA = nodeA.next;
        }
        while(nodeB.next != null) {
            lenB++;
            nodeB = nodeB.next;
        }

        int diffLen = lenA>=lenB ? lenA-lenB : lenB-lenA;
        // 保证 A链表 比 B链表 长
        if(lenA < lenB) {
            ListNode temp = headA;
            headA = headB;
            headB = temp;
        }

        while(diffLen-- > 0) {
            headA = headA.next;
        }

        while(headA != null) {
            if(headA == headB) {
                return headA;
            }
            headA = headA.next;
            headB = headB.next;
        }

        return null;
        
    }
}

方法二:快慢指针,消除步长差

图解相交链表

  1. 假设链表A为 ***** x #####,链表B为**** x #####,相交点为x,相交部分为x #####;则链表A中各部分的长度为a(*)+a(1)+a(#),链表B中各部分的长度为b(*)+b(1)+b(#),由上可知a(*)!=b(*),但a(#)=b(#)
  2. 使用pA指针先遍历A再遍历B,到相交点的长度为:a(*)+a(1)+a(#) + b(*)+b(1)
  3. 使用pB指针先遍历B再遍历A,到相交点的长度为:b(*)+b(1)+b(#) + a(*)+a(1)
  4. 那么a(*)+a(1)+a(#) + b(*)+b(1)b(*)+b(1)+b(#) + a(*)+a(1)的关系是怎样的?
    1. 已知a(*)!=b(*),但a(#)=b(#)
    2. 则,a(*)+a(1)+a(#) + b(*)+b(1) = a(*)+a(1)+b(#) + b(*)+b(1) = a(*)+a(1)+ b(#)+ b(*)+b(1) = b(*)+b(1)+b(#) + a(*)+a(1)
public class Solution {
    public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
        if(headA == null || headB == null) {
            return null;
        }

        ListNode pA = headA, pB = headB;

        while(pA != pB) {
            // 当pA==null时,说明遍历到了A的末尾,从B开始遍历
            pA = pA==null ? headB : pA.next;
            // 当pB==null时,说明遍历到了B的末尾,从A开始遍历
            pB = pB==null ? headA : pB.next;
        }
        return pA;
    }
}

206. 反转链表

方法一:迭代

class Solution {
    public ListNode reverseList(ListNode head) {
        ListNode pre=null, cur=head, temp=null;
        while(cur != null){
            temp = cur.next;
            cur.next = pre;
            pre = cur;
            cur = temp;
        }

        return pre;
    }
}

方法二:递归

class Solution {
    public ListNode reverseList(ListNode head) {
        return reverse(null, head);
    }

    private ListNode reverse(ListNode pre, ListNode cur) {
        if(cur == null) {
            return pre;
        }
        ListNode temp = null;

        temp = cur.next;
        cur.next = pre;
        return reverse(cur, temp);
    }
}

234. 回文链表

方法一:快慢指针

/**
 * 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 boolean isPalindrome(ListNode head) {
        if(head == null) {
            return true;
        }

        // 快慢指针,快指针移动到末尾时,慢指针所指位置是中间节点
        ListNode fast = head, slow = head;
        while(fast != null) {
            fast = fast.next;
            if(fast != null) {
                fast = fast.next;
            }
            slow = slow.next;
        }

        // 翻转后半段的链表
        ListNode node = reverse(slow);

        // 比较 经过反转后的后半段链表 和 前半段的链表
        while(node != null) {
            if(head.val != node.val) {
                return false;
            }
            head = head.next;
            node = node.next;
        }

        return true;
    }

    // 反转链表
    private ListNode reverse(ListNode cur) {
        ListNode pre = null, post = null;

        while(cur != null) {
            post = cur.next;
            cur.next = pre;
            pre = cur;
            cur = post;
        }

        return pre;
    }
}

141. 环形链表

方法一:快慢指针

利用快慢指针判断当前链表中是否含有换,如果两个指针相遇,则说明链表中有环

public class Solution {
    public boolean hasCycle(ListNode head) {
        ListNode fast = head, slow = head;
        // 排除只含有一个元素,且无环的情况,例如例三head=[1]
        if(head == null || head.next == null){
            return false;
        }

        // 快慢指针,快指针移两格,慢指针移一格,两指针相遇则说明存在环
        while(fast != null) {
            fast = fast.next;
            if(fast != null) {
                fast = fast.next;
            }
            slow = slow.next;

            // 放在移动后判断,因为一开始指针指向同一位置
            if(fast == slow) {
                return true;
            }
        }

        return false;
        
    }
}

142. 环形链表 II

方法一:快慢指针

fig1

  1. 判断是否存在环(快慢指针在环中相遇)
  2. 判断入环的位置,由上图,
    1. 快指针走过的位置为a+n*(b+c) + b,n为走过的环的圈数;慢指针走过的距离为a+b
    2. 快指针每次移动两步,慢指针每次移动一步
    3. 则有:a+n*(b+c) + b = 2*(a+b),化简:a = (n-1)*(b+c) + c
    4. 因此,一个指针从head开始走a步到入环点,另一个指针在环中走n-1圈+相遇点到入环点的距离;两者相等
public class Solution {
    public ListNode detectCycle(ListNode head) {
        ListNode fast = head, slow = head;

        while(fast!=null && fast.next!=null) {
            fast = fast.next.next;
            slow = slow.next;

            // 快慢指针相遇,存在环
            if(fast == slow) {
                // 寻找环中的第一个节点
                ListNode index1 = head;
                ListNode index2 = fast;
                while(index1 != index2) {
                    index1 = index1.next;
                    index2 = index2.next;
                }
                return index1;

            }
        }

        return null;
    }
}

21. 合并两个有序链表

方法一:迭代

  1. 定义变量:temp用于记录已合并链表的最大元素,list1list2指向两个未合并链表的头元素,head记录已合并的连链表头元素
  2. 依次比较list1list2所指向元素的值,将temp指向两者中的较小一个,并更新指针temp以及list1 / list2
  3. list1 / list2遍历到结尾时,将另一链表的剩余元素插入即可
class Solution {
    public ListNode mergeTwoLists(ListNode list1, ListNode list2) {
        if(list1==null) return list2;
        if(list2==null) return list1;
        
        ListNode temp = null;
        if(list1.val < list2.val) {
            temp = list1;
            list1 = list1.next;
        } else {
            temp = list2;
            list2 = list2.next;
        }
        ListNode head = temp;

        // System.out.println(list1.val + "," + list2.val);

        // 指针变化条件:两指针所指位置均不为空
        while(!(list1==null || list2==null)) {
            if(list1.val < list2.val) {
                temp.next = list1;
                list1 = list1.next;
            } else {
                temp.next = list2;
                list2 = list2.next;
            }
            temp = temp.next;
        }

        // 其中一个指针所指位置为空,则将另一链表的剩余分布添加到当前链表中即可
        if(list1==null) {
            temp.next = list2;
        } else {
            temp.next = list1;
        }

        return head;
    }

}

方法二:迭代(化简)

class Solution {
    public ListNode mergeTwoLists(ListNode list1, ListNode list2) {
        // 设置fakehead,返回值为fakehead.next,不用关心temp究竟指向list1还是list2
        ListNode fakehead = new ListNode(-1);
        ListNode temp = fakehead;

        // 指针变化条件:两指针所指位置均不为空
        while(!(list1==null || list2==null)) {
            if(list1.val < list2.val) {
                temp.next = list1;
                list1 = list1.next;
            } else {
                temp.next = list2;
                list2 = list2.next;
            }
            temp = temp.next;
        }

        // 其中一个指针所指位置为空,则将另一链表的剩余分布添加到当前链表中即可
        temp.next = list1==null ? list2 : list1;

        return fakehead.next;
    }

}

2. 两数相加

方法一:模拟

  1. 题目要求,将两个链表对应的元素相加sum,并将其个位sum%10存到一个新的ListNode中,同时应该注意进位carry=sum/10
  2. 初始变量,使用headtail记录新链表的首尾元素,插入第一个元素时更新首尾指针;之后,一直更新尾指针。
  3. 最后,计算完成后,记得判断最后一次计算是否有进位,如有则将该数据存入链表
class Solution {
    public ListNode addTwoNumbers(ListNode l1, ListNode l2) {
        ListNode head = null, tail = null;

        // 进位
        int carry = 0;

        while(l1 != null || l2 != null) {
            int n1 = l1!=null ? l1.val : 0;
            int n2 = l2!=null ? l2.val : 0;
            int sum = n1 + n2 + carry;

            // 处理首尾节点
            if(head == null) {
                // 头节点,记录首元素
                head = tail = new ListNode(sum % 10);
            } else {
                // 剩余节点使用tail标记,并更新tail指针
                tail.next = new ListNode(sum % 10);
                tail = tail.next;
            }

            carry = sum / 10;
            if(l1 != null) l1 = l1.next;
            if(l2 != null) l2 = l2.next;
        }

        // 更新最后一个进位元素,如果最高位存在进位,则需要单另创建一个节点更新该值,如示例三
        if(carry > 0) {
            tail.next = new ListNode(carry);
        }

        return head;
    }
}

方法二:模拟

  1. 与前述方法类似,只是用fakehead标记头节点,采用temp指针更新元素
  2. 注意:while(l1!=null || l2!=null)while(!(l1==null || l1==null))的区别
class Solution {
    public ListNode addTwoNumbers(ListNode l1, ListNode l2) {
        ListNode fakehead = new ListNode(-1);
        ListNode temp = fakehead;
        int add = 0;

        while(l1!=null || l2!=null) {
            int n1 = l1!=null ? l1.val : 0;
            int n2 = l2!=null ? l2.val : 0;
            int sum = n1 + n2 + add;

            temp.next = new ListNode(sum % 10);
            temp = temp.next;
            add = sum / 10;

            if(l1 != null) l1 = l1.next;
            if(l2 != null) l2 = l2.next;

        }

        if(add > 0) {
            temp.next = new ListNode(add);
        }

        return fakehead.next;

    }
}

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

方法一:双指针

采用快慢指针的方法,快指针给指向null时,慢指针所指元素即为删除元素

  1. 创建fakehead,对于删除头结点的情况不用分类讨论
  2. 注意rightleft的遍历位置,left需要停到待删除位置的前一个位置,不然无法通过left.next = left.next.next修改指针指向
class Solution {
    public ListNode removeNthFromEnd(ListNode head, int n) {
        // 创建fakehead,对于删除头结点的情况不用分类讨论
        ListNode fakehead = new ListNode(-1);
        fakehead.next = head;
        
        ListNode left = fakehead, right = fakehead;

        for(int i=0; i<n; i++) {
            right = right.next;
        }

        while(right.next != null) {
            right = right.next;
            left = left.next;
        }

        left.next = left.next.next;

        return fakehead.next;
    }
}

24. 两两交换链表中的节点

方法一:模拟,双指针

使用firstNodesecondNode记录待交换的两个元素,cur为待交换元素的前一个元素,依次模拟指针的变化即可

class Solution {
    public ListNode swapPairs(ListNode head) {
        ListNode fakehead = new ListNode(-1);
        fakehead.next = head;
        
        ListNode cur = fakehead;
        ListNode firstNode = null, secondNode = null;

        while(cur.next!=null && cur.next.next!=null) {
            firstNode = cur.next;
            secondNode = cur.next.next;

            cur.next = secondNode;
            firstNode.next = secondNode.next;
            secondNode.next = firstNode;

            cur = firstNode;
        }


        return fakehead.next;
    }
}

25. K 个一组翻转链表

方法一:模拟

class Solution {
    public ListNode reverseKGroup(ListNode head, int k) {
        ListNode fakehead = new ListNode(-1);
        fakehead.next = head;
        ListNode pre = fakehead;

        while(head != null) {
            ListNode tail = pre;

            // 剩余元素不足k个,不进行翻转,将结果直接返回即可
            for(int i=0; i<k; i++) {
                tail = tail.next;
                if(tail == null) {
                    return fakehead.next;
                }
            }

            // newhead 为新一组元素首节点
            ListNode newhead = tail.next;
            ListNode[] reverse = myReverse(head, tail);

            head = reverse[0];
            tail = reverse[1];

            pre.next = head;
            tail.next = newhead;
            pre = tail;
            head = pre.next;
        }
        
        return fakehead.next;
    }


    private ListNode[] myReverse(ListNode head, ListNode tail) {
        ListNode prev = tail.next;
        ListNode p = head;
        while(prev != tail) {
            ListNode newhead = p.next;
            p.next = prev;
            prev = p;
            p = newhead;
        }
        return new ListNode[]{tail, head};
    }
}

138. 随机链表的复制

方法一:迭代,哈希表

两次遍历,第一次遍历构建节点并复制val;第二次遍历构建random指针

https://leetcode.cn/problems/copy-list-with-random-pointer/description/comments/2247988

class Solution {
    public Node copyRandomList(Node head) {
        Node fakehead = new Node(-1);
        Node p = head, q = fakehead;
        HashMap<Node, Node> map = new HashMap<>();
  
        // 第一次遍历,新建链表节点,并将p中的val值复制到新结点中
        while(p != null) {
            // q为新创建的节点,将p所指节点放入q中
            q.next = new Node(p.val);
            q = q.next;
            map.put(p, q);
            p = p.next;
        }
        
        // 第二次遍历,建立random的映射:p,q指向逻辑上的同一节点,复制random指针(从map中根据p.random取出节点,并作为q.random)
        p = head;
        // 注意此处:q=fakehead.next,不应为q=head;因为此处的q是应该指向新的链表fakehead.next;而head为旧的链表
        q = fakehead.next;
        while(p != null) {
            if(p.random != null) {
                q.random = map.get(p.random);
            }
            p = p.next;
            q = q.next;
        }

        return fakehead.next;       
    }
}

148. 排序链表

方法一:遍历+替换

遍历一遍链表中的元素值,并记录在ArrayList中;之后利用Collections.sort(list)对当前ArrayList中的数字排序;最后,遍历数组元素,将元素值进行替换

注意,使用该思路实现并不符合题意!!!

class Solution {
    public ListNode sortList(ListNode head) {
        ListNode fakehead = new ListNode(-1);
        fakehead.next = head;
        List<Integer> list = new ArrayList<>();
        
        while(head != null) {
            list.add(head.val);
            head = head.next;
        }

        Collections.sort(list);

        head = fakehead.next;
        for(int temp : list) {
            head.val = temp;
            head = head.next;
        }

        return fakehead.next;
    }
}

方法二:冒泡排序

暴力解【超时】

冒泡排序在链表上的时间复杂度为$O(n^2)$,因为需要多次遍历链表,每次遍历需要比较链表中的所有相邻节点。

class Solution {
    public ListNode sortList(ListNode head) {
        if (head == null || head.next == null) {
            return head;
        }

        boolean isSorted = false;

        // 外层循环:重复冒泡过程直到链表排序完成
        while (!isSorted) {
            isSorted = true; // 假设本次遍历是有序的
            ListNode current = head;

            // 内层循环:冒泡
            while (current.next != null) {
                if (current.val > current.next.val) {
                    // 交换相邻节点的值
                    int temp = current.val;
                    current.val = current.next.val;
                    current.next.val = temp;

                    isSorted = false; // 有交换发生,说明仍然无序
                }
                current = current.next;
            }
        }

        return head;
    }
}

23. 合并 K 个升序链表

方法一:两两合并

依次将前一链表合并到后后一链表中,并将后一链表的头节点记录下来;重复上述过程,即可完成链表的合并;最后,返回值因为ListNode[]中的最后一个元素,即为最后一个链表的头节点。

链表两两合并,可参考上述21. 合并两个有序链表

class Solution {
    // 合并K个链表
    public ListNode mergeKLists(ListNode[] lists) {
        int len = lists.length;
        if(len == 0) return null;
        if(len == 1) return lists[0];

        for(int i=0; i<len-1; i++) {
            // 合并两链表,将前一链表的结果合并到后一链表中
            lists[i+1] = mergeList(lists[i], lists[i+1]);
        }

        // 最后合并后的结果为最后一个链表的头节点
        return lists[len-1];
    }

    // 合并两个链表
    private ListNode mergeList(ListNode list1, ListNode list2) {
        ListNode fakehead = new ListNode(-1);
        ListNode temp = fakehead;

        while(list1!=null && list2!=null) {
            if(list1.val < list2.val) {
                temp.next = list1;
                list1 = list1.next;
            } else {
                temp.next = list2;
                list2 = list2.next;
            }
            temp = temp.next;
        }

        temp.next = list1==null ? list2 : list1;

        return fakehead.next;

    }
}

方法二:分治法

将一个ListNode[]数组一分为二,合并前半部分和右半部分;依次递归执行

mid = (left + right) >> 1(left+right)/2,对于leftright较大的情况,可防止数据溢出。也可写为left+(right-left)/2

class Solution {
    // 合并K个链表
    public ListNode mergeKLists(ListNode[] lists) {
        int len = lists.length;
        if(len == 0) return null;
        if(len == 1) return lists[0];

        return merge(lists, 0, len-1);
    }

    private ListNode merge(ListNode[] lists, int left, int right) {
        if(left == right) return lists[left];
        if(left > right) return null;

        int mid = (left + right) >> 1;
        // 分治法 合并
        return mergeList(merge(lists, left, mid), merge(lists, mid+1, right));

    }

    // 合并两个链表【和前述方法中的两两合并相同】
    private ListNode mergeList(ListNode list1, ListNode list2) {
        ListNode fakehead = new ListNode(-1);
        ListNode temp = fakehead;

        while(list1!=null && list2!=null) {
            if(list1.val < list2.val) {
                temp.next = list1;
                list1 = list1.next;
            } else {
                temp.next = list2;
                list2 = list2.next;
            }
            temp = temp.next;
        }

        temp.next = list1==null ? list2 : list1;

        return fakehead.next;

    }
}

方法三:优先队列

【待补充】

146. LRU 缓存

方法一:双向链表 + 哈希表

  1. 定义四个函数addToHaed, removeNode, moveToHead, removeTail用于处理双端队列中节点移动情况(指针变化情况)
  2. 设置fakehead以及faketail来初始化双向链表
  3. 使用HashMap(cache)记录当前链表中存在的元素
class LRUCache {

    class DLinkedNode {
        int key;
        int value;
        DLinkedNode prev;
        DLinkedNode next;
        public DLinkedNode() {};
        public DLinkedNode(int _key, int _value) {
            key = _key;
            value = _value;
        }
    }

    private Map<Integer, DLinkedNode> cache = new HashMap<>();
    private int size;
    private int capacity;
    private DLinkedNode fakehead, faketail;

    public LRUCache(int capacity) {
        this.size = 0;
        this.capacity = capacity;

        fakehead = new DLinkedNode();
        faketail = new DLinkedNode();
        fakehead.next = faketail;
        faketail.next = fakehead;
    }
    
    public int get(int key) {
        DLinkedNode node = cache.get(key);
        if(node == null) {
            return -1;
        }
        // 访问该元素,将该元素移动到双端列表头节点,表示最近访问
        moveToHead(node);
        return node.value;

    }

    public void put(int key, int value) {
        DLinkedNode node = cache.get(key);
        if(node == null) {
            DLinkedNode newNode = new DLinkedNode(key, value);
            cache.put(key, newNode);
            addToHaed(newNode);
            size++;
            
            // 当前元素值超过了设置容量,将双端列表的最后一个元素(最久为访问)删除
            if(size > capacity) {
                DLinkedNode tail = removeTail();
                cache.remove(tail.key);
                size--;
            }
        } else {
            // 访问了当前元素,将该元素移动到双端列表首位,表示最近访问
            node.value = value;
            moveToHead(node);
        }

    }
 
    
    // 头插法
    private void addToHaed(DLinkedNode node) {
        node.prev = fakehead;
        node.next = fakehead.next;
        fakehead.next.prev = node;
        fakehead.next = node;
    }
 
    // 移除节点,只需要将节点的前后两个指针分别指向前后元素即可
    private void removeNode(DLinkedNode node) {
        node.prev.next = node.next;
        node.next.prev = node.prev;
    }
 
    // 将节点移动到头元素,删除-插入
    private void moveToHead(DLinkedNode node) {
        removeNode(node);
        addToHaed(node);
    }
 
    // 移除节点
    private DLinkedNode removeTail() {
        DLinkedNode res = faketail.prev;
        removeNode(res);
        return res;
    }
    
}

/**
 * Your LRUCache object will be instantiated and called as such:
 * LRUCache obj = new LRUCache(capacity);
 * int param_1 = obj.get(key);
 * obj.put(key,value);
 */
posted @   是你亦然  阅读(9)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· DeepSeek 开源周回顾「GitHub 热点速览」
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· 单线程的Redis速度为什么快?
点击右上角即可分享
微信分享提示