ListNode 链表算法面试题,源于大厂真题和《剑指offer》,欢迎大家留言补充~~

 链表是一种物理存储单元上非连续、非顺序的存储结构数据元素的逻辑顺序是通过链表中的指针链接次序实现的。

链表由一系列结点(链表中每一个元素称为结点)组成,结点可以在运行时动态生成。每个结点包括两个部分:一个是存储数据元素的数据域,另一个是存储下一个结点地址的指针域。由于不必须按顺序存储,链表在插入/删除数据的时候可以达到O(1)的复杂度,但是查询相对线性表复杂。

本文整理了大厂面试中比较多见的链表面试题,如果遇到更新颖的题目,欢迎大家留言或者私信我,发出来大家一起学习下。

链表数据结构,如下:

// 单向简单链表
public class ListNode {
    int val;
    ListNode next;

    public ListNode() {}
    public ListNode(int val){ this.val = val; }
    public ListNode(int val, ListNode next) {
        this.val = val;
        this.next = next;
    }
}
// 复杂链表
public class RandomListNode {
    int val;
    RandomListNode next;
    RandomListNode random;

    public RandomListNode(){ }
    public RandomListNode(int val){ this.val=val; }
}

1. 从尾到头【打印链表】

题目描述 :输入一个链表,从尾到头打印链表每个节点的值。
思路 :借助栈实现,或使用递归的方法。
    // 方法一:利用【栈】的先入后出特性
    public static List<Integer> printListFromTailToHead(ListNode listNode) {
        List<Integer> list = new ArrayList<>();
        if (listNode != null) {
            ListNode node = listNode;
            Stack<Integer> stack = new Stack<>();
            while (node != null) {
                stack.add(node.val);
                node = node.next;
            }
            while (!stack.isEmpty()) {
                list.add(stack.pop());
            }
        }
        return list;
    }
    // 方法二:递归
    public static List<Integer> printListReverse2(ListNode headNode) {
        List<Integer> list = new ArrayList<Integer>()
        ListNode node = headNode;
        if (node != null) {
            if (node.next != null) {
                list = printListReverse2(node.next);
            }
            list.add(node.val);
        }
        return list;
    }
    // 方法三:Collections.reverse 反转 List
    public static List<Integer> printListReverse2(ListNode headNode) {
        List<Integer> list = new ArrayList<Integer>()
        if (listNode != null) {
            ListNode node = listNode;
            while (node != null) {
                list.add(node.val);
                node = node.next;
            }
            Collections.reverse(list)
        }
        return list;
    }

2.输出【反转】后的链表

题目描述 :输入一个链表,反转链表后,输出新链表的表头。
思路 :定义两个指针,反向输出,或者使用递归操作
    // 解法一:迭代,两个指针,反向输出,时间复杂度:O(n),空间复杂度:O(1)
    public static ListNode reverseList(ListNode head) {
        ListNode pre = null;
        ListNode curr = head;
        while (curr != null) {
            ListNode tmp = curr.next;
            curr.next = pre;
            pre = curr;
            curr = tmp;
        }
        return pre;
    }
    // 解法二:递归,时间复杂度:O(n),空间复杂度:O(n)
    public static ListNode reverseList2(ListNode head) {
        if (head == null || head.next == null) {
            return head;
        }
        ListNode p = reverseList2(head.next);
        head.next.next = head.next;
        head.next = null;
        return p;
    }

3.【合并】2个有序链表

题目描述 :输入两个单调递增的链表,输出两个链表合成后的链表,当然我们需要合成后的链表满足单调不减规则。
思路 :递归与非递归求解,小数放在前面。
    // 解法一:递归,时间复杂度:O(m+n),空间复杂度:O(m+n)
    public static ListNode mergeTwoLists(ListNode list1, ListNode list2) {
        if (list1 == null) {
            return list2;
        }
        if (list2 == null) {
            return list1;
        }
        if (list1.val < list2.val) {
            list1.next = mergeTwoLists(list1.next, list2);
            return list1;
        } else {
            list2.next = mergeTwoLists(list1, list2.next);
            return list2;
        }
    }
    // 解法二:迭代,时间复杂度:O(m+n),空间复杂度:O(1)
    public static ListNode mergeTwoLists2(ListNode list1, ListNode list2) {
        ListNode preHead = new ListNode(-1);
        ListNode pre = preHead;
        while (list1 != null && list2 != null) {
            if (list1.val < list2.val) {
                pre.next = list1;
                list1 = list1.next;
            } else {
                pre.next = list2;
                list2 = list2.next;
            }
            pre = pre.next;
        }
        // 最后一次循环后,肯定剩下一个节点,判断该节点然后追加到pre末尾
        pre.next = (list1 != null ? list1 : list2);
        // pre 与 preHead 实际是相通的,但是指针位置不同
        return preHead.next;
    }

补充:【合并】多个有序链表

利用有序队列 PriorityQueue<> 存储每个链表的头节点,循环取出元素知道队列元素为空。

   public ListNode mergeKListsByQueue(ListNode[] lists) {
        // 放入PriorityQueue 的元素,必须重写 compare 方法
        PriorityQueue<ListNode> queue = new PriorityQueue<>(new Comparator<ListNode>() {
            @Override
            public int compare(ListNode o1, ListNode o2) {
                return Integer.compare(o1.val,o2.val);
            }
        });

        for (ListNode tempNode : lists) {
            queue.offer(tempNode);
        }

        // retList == tempList
        // 但是循环结束以后,tempList的指针在最后,但是retList的指针没有动过,还在最开头
        // 此时,才能使用retList.next获取最终结果(因为首位的0是没用的)
        ListNode<Integer> retList = new ListNode<>(0);
        ListNode<Integer> tempList = retList;
        while (!queue.isEmpty()) {
            tempList.next =  queue.poll();
            tempList = tempList.next;
            if (tempList.next != null) {
                // 把剩下的部分再放回队列,放进去以后,优先级队列自动排序
                queue.offer(tempList.next);
            }
        }
        return retList.next;
    }

4.求链表中倒数【第 K 个节点】

题目描述 :输入一个链表,输出该链表中倒数第 k 个结点。
思路 :定义一快一慢两个指针,快指针走 K 步,然后慢指针开始走,快指针到尾时,慢指针就找到了倒数第 K 个节点。
    public ListNode findKthToTail(ListNode listNode, int k) {
        if (listNode == null || k < 1) {
            return null;
        }
        ListNode fast = listNode;
        ListNode slow = listNode;
        while (k-- > 1) {
            if (fast.next == null) {
                return null;
            }
            fast = fast.next;
        }
        while (fast.next != null) {
            fast = fast.next;
            slow = slow.next;
        }
        return slow;
    }

5.【删除】链表节点

题目描述 :给定单向链表的头指针和一个节点指针,在 O(1) 时间复杂度内删除该节点。
思路将待删除节点的值赋值为下一个节点的值,然后将待删除节点的引用指向待删除节点的下下个节点,如下图所示:

如果题目的限制条件给的很足,完全可以写的很精简:

  • 链表至少包含两个节点。
  • 链表中所有节点的值都是唯一的。
  • 给定的节点为非末尾节点并且一定是链表中的一个有效节点。
  • 不要从你的函数中返回任何结果。

    public void deleteNode(ListNode node)  {
        node.val = node.next.val;
        node.next = node.next.next;
    }

否则,就需要我们先去验证上述的 3 种情况之后,再使用上面的代码:

    public static void deleteNode(ListNode head, ListNode deListNode) {
        if (deListNode == null || head == null)
            return;
        if (head == deListNode) {
            head = null;
        } else {
            // 若删除节点是末尾节点,往后移一个
            if (deListNode.next == null) {
                ListNode pointListNode = head;
                while (pointListNode.next.next != null) {
                    pointListNode = pointListNode.next;
                }
                pointListNode.next = null;
            } 
            // 关键代码
            else {
                deListNode.val = deListNode.next.val;
                deListNode.next = deListNode.next.next;
            }
        }
    }

补充:【删除】链表倒数第K个节点

思路:利用快慢指针法,定位并删除倒数第K个节点

    public ListNode removeNthFromEnd(ListNode head, int n) {
        // 这一部很重要,可以防止删除首节点时,next为空的情况
        ListNode dummy = new ListNode(-1);
        dummy.next = head;
        // 快慢指针,fast 先走n步,然后一起走
        ListNode slow = dummy;
        ListNode fast = dummy;
        while(n-- > 0){
            fast = fast.next;
        }
        // 当fast.next为null时,slow 位于目标的前一个节点
        while(fast.next != null){
            slow = slow.next;
            fast = fast.next;
        }
        // 跳过目标节点
        slow.next = slow.next.next;
        return dummy.next;
    }

6.两个链表的第一个【公共节点】

题目描述:输入两个链表,找出它们的第一个公共结点。

思路一使用两个指针 nodeA,nodeB 分别指向两个链表的头节点 headA,headB,然后同时遍历:

  • 当nodeA节点到达链表A的结尾时,重新指向headB链表的头结点;
  • 当nodeB结点到达链表B的结尾时,重新指向headA链表的额头结点。
    public static ListNode FindFirstCommonNode1(ListNode pHead1, ListNode pHead2) {
        ListNode p1 = pHead1;
        ListNode p2 = pHead2;

        // 注意这个位置,不是 nodeA.val != nodeB.val,
        // 他们两个都是链表的结点,所以直接比较就行;
        // 如果遇到 nodeA 这个结点和一个值的比较时,可以这样写:int a=1; nodeA.val == a; 
        while (p1 != p2){
            p1 = (p1 != null ? p1.next : pHead2);
            // nodaA 到达链表A的结尾时,重新指向 headB 的头结点哦,不是nodeB!!
            p2 = (p2 != null ? p2.next : pHead1);
        }
        return p1;
    }

思路二:先计算链表长度,链表长的先走,移动到和另一个链表的节点数相等的位置,然后再一起移动,判断是否相等。

    public static ListNode FindFirstCommonNode2(ListNode pHead1, ListNode pHead2) {
        if(pHead1 == null || pHead2 == null)
            return null;
        ListNode a = pHead1, b = pHead2;
        int lengthA = length(pHead1), lengthB = length(pHead2);
        if(lengthA > lengthB){
            for(int i=0; i<lengthA-lengthB; i++)
                a = a.next;
        }else{
            for(int i=0; i<lengthB-lengthA; i++)
                b = b.next;
        }
        while(a != b){
            a = a.next;
            b = b.next;
        }
        return a;
    }
    public static int length(ListNode node){
        ListNode tmp = node;
        int count = 0;
        while(tmp != null){
            tmp = tmp.next;
            count++;
        }
        return count;
    }

7.链表中环的【入口节点】

题目描述 :一个链表中包含环,请找出该链表的环的入口结点。

思路一:快慢指针法,用两个指针,一个fast指针,每次走两步,一个slow指针,每次走一步。当fast指针与slow指针相遇时,再让fast指向链表头部,slow位置不变。同时走,同时每次一步,相遇点即为环起点。

    public static ListNode EntryNodeOfLoop(ListNode pHead) {
        ListNode fast = pHead;
        ListNode slow = pHead;
        while (fast != null &&  fast.next != null) {
            // 条件里 fast.next != null,不然这里可能会出问题
            fast = fast.next.next; 
            slow = slow.next;
            if (fast == slow) {
                fast = pHead;
                while (fast != slow) {
                    fast = fast.next;
                    slow = slow.next;
                }
                return slow;
            }
        }
        return null;
    }

思路二:用HashSet来解决,重复的节点就是起点

    public static ListNode EntryNodeOfLoop2(ListNode pHead){
        Set<ListNode> set = new HashSet<>();
        ListNode head = pHead;
        while (head != null) {
            if (!set.add(head)) {
                return head;
            }
            head = head.next;
        }
        return null;
    }

8.【删除】排序链表中重复的节点

题目描述:在一个排序的链表中,存在重复的结点,请删除该链表中重复的结点,所有重复的结点不保留,返回链表头指针。
例子:输入:head = [1,1,2,3],输出:[2,3]
思路 :先新建一个头节点,然后向后查找值相同的节点,重复查找后删除。
    public ListNode deleteDuplicates(ListNode head) {
        if(head == null) {
            return head;
        }
        ListNode dummy = new ListNode(0, head);
        ListNode cur = dummy;
        while(cur.next != null && cur.next.next != null) {
            if(cur.next.val == cur.next.next.val) { 
                int x = cur.next.val;
                // 循环向下,一直删除重复元素
                while(cur.next != null && cur.next.val == x) {
                    cur.next = cur.next.next;
                }
            } else {
                cur = cur.next;
            }
        }
        return dummy.next;
    }

9.【删除】排序链表中重复的元素

题目描述存在一个按升序排列的链表,给你这个链表的头节点  head ,请你删除所有重复的元素,使每个元素 【 只出现一次】 。
例子:输入:head = [1,1,2,3,3],输出:[1,2,3]
思路 :同上,不过要注意是去重。
    public ListNode deleteDuplicates(ListNode head) {
        if (head == null) {
            return  head;
        }
        ListNode cur = head;
        while (cur.next != null){
            if(cur.val == cur.next.val){
                cur.next = cur.next.next;
            } else {
                cur = cur.next;
            }
        }
        return  head;
    }

10.【复杂链表】的复制

题目描述 :输入一个复杂链表(每个节点中有节点值,以及两个指针,一个指向下一个节点,另一个特殊指针指向任意一个节点),返回结果为复制后复杂链表的head。(注意,输出结果中请不要返回参数中的节点引用,否则判题程序会直接返回空)
思路 :先复制链表的 next 节点,将复制后的节点接在原节点后,然后复制其它的节点,最后取偶数位置的节点(复制后的节点)。
    public static RandomListNode RandomClone (RandomListNode pHead) {
        if(pHead == null)
            return null;
        RandomListNode head = new RandomListNode(pHead.val);
        RandomListNode temp = head ;
        while(pHead.next != null) {
            temp.next = new RandomListNode(pHead.next.val);
            if(pHead.random != null) {
                temp.random = new RandomListNode(pHead.random.val);
            }
            pHead = pHead.next ;
            temp = temp.next ;
        }
        return head ;
    }

总结

  • 欢迎大家留言补充~~
  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

Java Punk

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

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

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

打赏作者

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

抵扣说明:

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

余额充值