面试题 - 大厂常考的8道链表算法题

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

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

示例:
在这里插入图片描述

输入:head = [1,2,3,4,5], n = 2
输出:[1,2,3,5]
方法思路
1、哑结点(Dummy Node):引入哑结点作为头节点的前驱,统一处理删除头节点的情况。
2、快指针先行:快指针先移动 n 步,确保快慢指针之间的间隔为 n。
3、同步移动:快慢指针同时移动,直到快指针到达链表末尾。此时慢指针指向待删除节点的前驱。
4、删除节点:修改慢指针的 next 指针,跳过待删除节点。

代码实现:

class Solution {
    public ListNode removeNthFromEnd(ListNode head, int n) {
        ListNode dummy = new ListNode(0);
        dummy.next = head;
        ListNode fast = dummy, slow = dummy;
        
        // 快指针先移动n步
        for (int i = 0; i < n; i++) {
            fast = fast.next;
        }
        
        // 同步移动,直到快指针到达末尾
        while (fast.next != null) {
            fast = fast.next;
            slow = slow.next;
        }
        
        // 删除倒数第n个节点
        slow.next = slow.next.next;
        return dummy.next;
    }
}

2. 反转链表

题目:给你单链表的头节点 head ,请你反转链表,并返回反转后的链表。

示例:
在这里插入图片描述

输入:head = [1,2,3,4,5]
输出:[5,4,3,2,1]
方法思路:迭代法

1、初始化指针:定义前驱指针 prev(初始为 null)和当前指针 curr(初始为头节点 head)。
2、遍历链表:每次循环中,保存当前节点的下一个节点,将当前节点的 next 指针指向前驱节点,然后更新前驱和当前指针。
3、终止条件:当当前指针 curr 为 null 时,遍历结束,此时 prev 指向反转后的新头节点。
代码实现:

class Solution {
    public ListNode reverseList(ListNode head) {
       ListNode prev = null;
        ListNode curr = head;
        while (curr != null) {
            ListNode nextTemp = curr.next; // 保存下一个节点
            curr.next = prev;              // 反转指针
            prev = curr;                   // 前驱指针后移
            curr = nextTemp;               // 当前指针后移
        }
        return prev; // 返回新头节点  
    }
}

3.合并两个有序链表

题目:将两个升序链表合并为一个新的升序链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。

示例:
在这里插入图片描述

方法思路:

1、处理特殊情况:若其中一个链表为空,直接返回另一个链表。
2、使用哑结点(Dummy Node):简化头节点的处理,避免复杂的条件判断。
3、迭代比较:同时遍历两个链表,选择较小值的节点连接到新链表,并移动相应指针。
4、连接剩余节点:当其中一个链表遍历完成后,将另一个链表的剩余部分直接接入新链表。
代码实现:

/**
 * Definition for singly-linked list.
 * public class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode(int x) { val = x; }
 * }
 */
class Solution {
    public ListNode mergeTwoLists(ListNode l1, ListNode l2) {
        if(l1==null){
            return l2;
        }
        if(l2==null){
            return l1;
        }
        ListNode result = new ListNode(0);
        
        if(l1.val<l2.val){
            result =l1;
            result.next =mergeTwoLists(l1.next,l2);
            return result;
        }
        result =l2;
        result.next =mergeTwoLists(l1,l2.next);
        return result;
        
        
    }
}

4. 环形链表

题目:给你一个链表的头节点head ,判断链表中是否有环。

示例:
在这里插入图片描述

要判断一个链表是否有环,可以使用快慢指针的方法。快指针每次移动两步,慢指针每次移动一步。如果链表中有环,快指针最终会追上慢指针,两者相遇;如果快指针到达链表末尾(遇到null),则链表无环。

方法思路:

1、初始化指针: 快指针fast和慢指针slow都指向头节点。
2、循环遍历链表: 只要快指针可以继续移动两步(即fast.next和fast.next.next存在),就继续移动指针。
3、移动指针: 每次循环中,慢指针移动一步,快指针移动两步。
4、检查相遇: 如果快慢指针相遇,说明有环,返回true;否则,当快指针无法继续移动时,返回false。
代码实现:

public class Solution {
    public boolean hasCycle(ListNode head) {
        if (head == null) {
            returnfalse;
        }
        ListNode slow = head;
        ListNode fast = head;
        while (fast.next != null && fast.next.next != null) {
            slow = slow.next;
            fast = fast.next.next;
            if (slow == fast) {
                returntrue;
            }
        }
        returnfalse;
    }
}

// 定义链表节点
class ListNode {
    int val;
    ListNode next;
    ListNode(int x) {
        val = x;
        next = null;
    }
}

5. 排序链表

题目:给你链表的头结点 head ,请将其按升序排列并返回 排序后的链表 。

示例:
在这里插入图片描述

输入:head = [4,2,1,3]
输出:[1,2,3,4]
方法思路:

1、快慢指针分割链表:用 fast=slow.next 确保偶数节点时左半部分略短,避免死循环
2、递归排序子链表:left = sortList(head), right = sortList(tmp)
3、合并有序链表:经典的链表归并操作
代码实现:

class Solution {
    public ListNode sortList(ListNode head) {
        // 递归终止条件:空节点或单节点
        if (head == null || head.next == null) 
            return head;
        
        // 快慢指针找中点(关键点:fast从head.next开始)
        ListNode fast = head.next, slow = head;
        while (fast != null && fast.next != null) {
            slow = slow.next;
            fast = fast.next.next;
        }
        
        // 分割链表为左右两部分
        ListNode tmp = slow.next;
        slow.next = null; // 切断链表
        
        // 递归排序子链表
        ListNode left = sortList(head);
        ListNode right = sortList(tmp);
        
        // 合并两个有序链表
        ListNode dummy = new ListNode(0);
        ListNode cur = dummy;
        while (left != null && right != null) {
            if (left.val < right.val) {
                cur.next = left;
                left = left.next;
            } else {
                cur.next = right;
                right = right.next;
            }
            cur = cur.next;
        }
        cur.next = (left != null) ? left : right;
        
        return dummy.next;
    }
}

6. 回文链表

题目: 给你一个单链表的头节点 head ,请你判断该链表是否为回文链表。如果是,返回 true ;否则,返回 false。
在这里插入图片描述

输入:head = [1,2,2,1]
输出:true
要判断一个单链表是否为回文链表,可以采用反转后半部分链表并与前半部分比较的方法,这样可以在O(n)时间复杂度和O(1)空间复杂度内解决问题。

方法思路:

1、找到中间节点:使用快慢指针法找到链表的中间节点。快指针每次移动两步,慢指针每次移动一步,当快指针到达链表末尾时,慢指针正好位于中间位置。
2、反转后半部分链表:从中间节点的下一个节点开始反转后半部分链表。
3、比较前后部分:将前半部分链表和反转后的后半部分链表逐一比较,如果所有节点值相同,则为回文链表。
代码实现:

class Solution {
    public boolean isPalindrome(ListNode head) {
        if (head == null || head.next == null) {
            returntrue;
        }
        
        // 找到前半部分的尾节点
        ListNode firstHalfEnd = endOfFirstHalf(head);
        // 反转后半部分链表
        ListNode secondHalfStart = reverseList(firstHalfEnd.next);
        
        // 判断是否是回文
        ListNode p1 = head;
        ListNode p2 = secondHalfStart;
        boolean result = true;
        while (result && p2 != null) {
            if (p1.val != p2.val) {
                result = false;
            }
            p1 = p1.next;
            p2 = p2.next;
        }
        
        // 恢复链表结构(可选)
        firstHalfEnd.next = reverseList(secondHalfStart);
        
        return result;
    }
    
    // 使用快慢指针找到前半部分的尾节点
    private ListNode endOfFirstHalf(ListNode head) {
        ListNode fast = head;
        ListNode slow = head;
        while (fast.next != null && fast.next.next != null) {
            fast = fast.next.next;
            slow = slow.next;
        }
        return slow;
    }
    
    // 反转链表
    private ListNode reverseList(ListNode head) {
        ListNode prev = null;
        ListNode curr = head;
        while (curr != null) {
            ListNode nextTemp = curr.next;
            curr.next = prev;
            prev = curr;
            curr = nextTemp;
        }
        return prev;
    }
}

7. k个一组反转

题目:给你链表的头节点 head ,每 k 个节点一组进行翻转,请你返回修改后的链表。

示例:
在这里插入图片描述

输入:head = [1,2,3,4,5], k = 2
输出:[2,1,4,3,5]
要解决每k个节点一组翻转链表的问题,我们可以采用迭代的方法,通过分段处理链表,确保每组k个节点被正确翻转,最后将各组连接起来。

方法思路

1、分段检查:使用辅助函数检查当前剩余链表是否有足够的k个节点。
2、翻转每组节点:对于每组k个节点,使用标准的链表翻转方法进行翻转。
3、连接各组:将翻转后的子链表正确连接到已处理的部分,并继续处理后续节点。
代码实现:

class Solution {
    public ListNode reverseKGroup(ListNode head, int k) {
        ListNode dummy = new ListNode(0);
        dummy.next = head;
        ListNode prev = dummy;
        ListNode curr = head;
        
        while (curr != null) {
            ListNode start = curr;
            ListNode end = getKthNode(start, k);
            
            if (end == null) {
                // 剩余节点不足k个,无需翻转
                break;
            }
            
            ListNode nextGroupHead = end.next;
            end.next = null; // 断开当前组与后续链表的连接
            
            // 翻转当前组
            ListNode newHead = reverse(start);
            
            // 将前驱节点连接到新头部
            prev.next = newHead;
            // 当前组的尾部(原start)连接到下一组的头部
            start.next = nextGroupHead;
            
            // 更新前驱节点为当前组的尾部
            prev = start;
            // 当前指针移动到下一组的头部
            curr = nextGroupHead;
        }
        
        return dummy.next;
    }
    
    // 辅助函数:找到从节点node开始的第k个节点
    private ListNode getKthNode(ListNode node, int k) {
        while (node != null && k > 1) {
            node = node.next;
            k--;
        }
        return node;
    }
    
    // 辅助函数:翻转链表
    private ListNode reverse(ListNode head) {
        ListNode prev = null;
        ListNode curr = head;
        while (curr != null) {
            ListNode next = curr.next;
            curr.next = prev;
            prev = curr;
            curr = next;
        }
        return prev;
    }
}

8. LRU缓存

题目:请你设计并实现一个满足 LRU (最近最少使用) 缓存 约束的数据结构。

示例:

输入
[“LRUCache”, “put”, “put”, “get”, “put”, “get”, “put”, “get”, “get”, “get”]
[[2], [1, 1], [2, 2], [1], [3, 3], [2], [4, 4], [1], [3], [4]]
输出
[null, null, null, 1, null, -1, null, -1, 3, 4]

解释

LRUCache lRUCache = new LRUCache(2);
lRUCache.put(1, 1); // 缓存是 {1=1}
lRUCache.put(2, 2); // 缓存是 {1=1, 2=2}
lRUCache.get(1);    // 返回 1
lRUCache.put(3, 3); // 该操作会使得关键字 2 作废,缓存是 {1=1, 3=3}
lRUCache.get(2);    // 返回 -1 (未找到)
lRUCache.put(4, 4); // 该操作会使得关键字 1 作废,缓存是 {4=4, 3=3}
lRUCache.get(1);    // 返回 -1 (未找到)
lRUCache.get(3);    // 返回 3
lRUCache.get(4);    // 返回 4

方法思路:
在这里插入图片描述

代码实现:

import java.util.HashMap;
import java.util.Map;

class LRUCache {

    // 双向链表节点定义
    class DLinkedNode {
        int key;
        int value;
        DLinkedNode prev;
        DLinkedNode next;
        public DLinkedNode() {}
        public DLinkedNode(int key, int value) {
            this.key = key;
            this.value = value;
        }
    }

    private Map<Integer, DLinkedNode> cache = new HashMap<>(); // 哈希表用于快速查找
    private int capacity; // 缓存容量
    private int size;     // 当前缓存大小
    private DLinkedNode head, tail; // 虚拟头尾节点简化边界操作

    public LRUCache(int capacity) {
        this.size = 0;
        this.capacity = capacity;
        // 初始化虚拟头尾节点并相互连接
        head = new DLinkedNode();
        tail = new DLinkedNode();
        head.next = tail;
        tail.prev = head;
    }

    /**
     * 获取键对应的值,若不存在返回-1
     * 并将该节点移至头部表示最近使用
     */
    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) { // 存在则更新并移至头部
            node.value = value;
            moveToHead(node);
        } else { // 不存在则创建新节点
            DLinkedNode newNode = new DLinkedNode(key, value);
            cache.put(key, newNode); // 加入哈希表
            addToHead(newNode);      // 插入链表头部
            size++;
            if (size > capacity) {   // 超过容量则移除尾部节点
                DLinkedNode tailNode = removeTail();
                cache.remove(tailNode.key); // 同步删除哈希表项
                size--;
            }
        }
    }

    // 将节点添加到链表头部
    private void addToHead(DLinkedNode node) {
        node.prev = head;
        node.next = head.next;
        head.next.prev = node;
        head.next = node;
    }

    // 从链表中移除节点
    private void removeNode(DLinkedNode node) {
        node.prev.next = node.next;
        node.next.prev = node.prev;
    }

    // 将节点移至头部:先移除再插入头部
    private void moveToHead(DLinkedNode node) {
        removeNode(node);
        addToHead(node);
    }

    // 移除尾部节点(最久未使用)
    private DLinkedNode removeTail() {
        DLinkedNode tailNode = tail.prev; // 尾部节点是虚拟尾节点的前驱
        removeNode(tailNode);
        return tailNode;
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小马不敲代码

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值