必须要掌握的单链表操作大全

前言

号外号外,笔者最近在系统整理一些 Java 后台方面的面试题和参考解答,有找工作需求的童鞋,欢迎关注我的 Github 仓库,如果觉得不错可以点个 star 关注 :

  • 1、awesome-java-interview
  • 2、awesome-java-notes

链表相关的各种常用操作实现方法

经常刷 leetcode 的童鞋一定对单链表的各种骚操作不陌生,单链表可以玩出各种各样的骚操作,掌握单链表的各种常规操作一方面对于锻炼我们的编程思维和编程能力很有帮助,另一方面,面试的时候面试官也会经常问一些单链表相关的题目,可以让自己提前预热吧,对此感到不陌生。

总之,帮助很大就是了。废话不多说,以下主要根据自己平时刷题总结了一些高频的、常规的单链表操作题目,后续有时间还会不断补上,目前大体思路主要写在注释中,后续有时间再单独将思路整理出来。

1)单链表反转
2)链表中环的检测
3)返回链表中环的入口节点
4)两个有序链表的合并
5)链表中倒数第 n 个节点
6)求链表的中间节点
7)删除有序链表中的重复节点,只保留其中一个节点
8)删除有序链表中的所有重复节点 9)在 O(1)
时间内删除链表的节点
10)从尾到头打印链表
11)两个链表的第一个公共节点

package com.offers.leetcode.linkedlist;

import java.util.ArrayList;
import java.util.LinkedList;

/**
 * 1)单链表反转
 * 2)链表中环的检测
 * 3)返回链表中环的入口节点
 * 4)两个有序链表的合并
 * 5)链表中倒数第 n 个节点
 * 6)求链表的中间节点
 * 7)删除有序链表中的重复节点,只保留其中一个节点
 * 8)删除有序链表中的所有重复节点
 * 9)在 O(1) 时间内删除链表的节点
 * 10)从尾到头打印链表
 * 11)两个链表的第一个公共节点
 *
 *
 * @author Rotor
 * @since 2019/10/23 22:49
 */
public class LinkedListAlgo {

    // 1)单链表反转
    public static Node reverse(Node list) {
        Node curr = list, pre = null;
        while (curr != null) {
            Node next = curr.next;
            curr.next = pre;
            pre = curr;
            curr = next;
        }
        return pre; // 此时前驱结点pre为反转后的头结点
    }

    /**
     * 2)检测链表是否有环
     *
     * 采用两个指针的方式可以判断链表是否有环。设置快慢两个指针,快指针一次走两步,慢指针一次走
     * 一步,如果链表有环,那么最终快慢指针会在环中的某个节点相遇(类比于在操场中跑步,跑得快的
     * 童鞋最终会追上跑得慢的童鞋,在跑道的某处相遇)。
     *
     * 时间复杂度:O(n)
     * 空间复杂度:O(1) 只需要临时存储快慢指针等几个指针的内存,因此只需常数级别的空间复杂度
     *
     * @param list
     * @return
     */
    public static boolean checkCircle(Node list) {
        if (list == null) return false;

        Node fast = list; // 快指针依次走两步
        Node slow = list; // 慢指针一次走一步
        while (fast.next != null && fast.next.next != null) {
            fast = fast.next.next;
            slow = slow.next;
            if (fast == slow) {
                return true;
            }
        }

        return false;
    }

    /**
     * 3)返回链表中环的入口节点
     *
     * 先检测链表中是否有环,如果有环,则慢指针继续从环中相遇节点往前遍历,并重新定义一个临时的指针指向头结点,与慢指针一样每次走
     * 一步,两者相遇处即为环的入口节点。
     *
     * 时间复杂度:O(n)
     * 空间复杂度:O(1)
     *
     * @param head
     * @return
     */
    public Node entryNodeOfList(Node head) {
        // 链表为空或者只有一个节点时,没有环
        if (head == null || head.next == null) {
            return null;
        }

        Node fast = head;
        Node slow = head;
        while (fast.next != null && fast.next.next != null) {
            slow = slow.next;
            fast = fast.next.next;
            if (slow == fast) {
                Node newSlow = head;
                while (newSlow != slow) {
                    slow = slow.next;
                    newSlow = newSlow.next;
                }
                return slow;
            }
        }

        return null;
    }

    /**
     * 4)有序链表的合并
     *
     * @param l1 链表 l1
     * @param l2 链表 l2
     * @return 合并后的链表
     */
    public Node mergeTwoLists(Node l1, Node l2) {
        Node soldier = new Node(0); // 利用哨兵节点简化实现难度
        Node p = soldier;

        while (l1 != null && l2 != null) {
            if (l1.data < l2.data) {
                p.next = l1;
                l1 = l1.next;
            } else {
                p.next = l2;
                l2 = l2.next;
            }
            p = p.next;
        }

        /**
         * 处理还未处理的节点,哪条链表不为空则将其插入合并的链表中
         */
        if (l1 != null) {
            p.next = l1;
        }
        if (l2 != null) {
            p.next = l2;
        }
        return soldier.next;
    }

    /**
     * 5)链表中倒数第 K 个节点
     *
     * 定义两个指针,如果第一个指针走到链表尾节点时,第二个节点正好走到链表中的倒数第 K 个节点处,那么此时第二个节点即为倒数第 K
     * 个节点。也就是说,此时两个节点之间的距离为 k-1。所以我们现在可以反过来,先让第一个指针走 k-1 步,然后两个指针再同时每次往
     * 前移动一步,当第一个指针走到链表尾结点处时,此时第二个节点正好指向倒数第 K 个节点。
     *
     * 时间复杂度:O(n) 需要遍历一遍链表
     * 空间复杂度:O(1) 不需要额外存储空间,只需要临时存储几个指针
     *
     * @param list 单链表
     * @param k 倒数第 K 个节点
     * @return 返回倒数第 K 个节点
     */
    public static Node deleteLastKth(Node list, int k) {
        if (list == null || k == 0) {
            return null;
        }

        Node aheadNode = list; // 先走 k-1 个步的指针
        Node behhindNode = list; // 后走的指针
        // 第一个指针先移动 k-1 步
        for (int i = 0; i < k - 1; i++) {
            // k 的值比链表的节点数大
            if (aheadNode.next == null) {
                return null;
            }
            aheadNode = aheadNode.next;
        }

        // 将两个指针同时移动,直到第一个指针指向链表尾
        while (aheadNode.next != null) {
            aheadNode = aheadNode.next;
            behhindNode = behhindNode.next;
        }
        return behhindNode;
    }

    /**
     * 6)求链表中间节点
     *
     * 采用快慢指针的方式查找单链表的中间节点,快指针一次走两步,慢指针一次走一步,当快指针走到
     * 链表尾时,慢指针刚好到达中间节点。
     *
     * 时间复杂度:O(n)
     * 空间复杂度:O(1)
     *
     * @param list
     * @return
     */
    public static Node findMiddleNode(Node list) {
        if (list == null) return null;

        Node fast = list;
        Node slow = list;

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

    /**
     * 7)删除有序链表中的重复节点,只保留其中一个节点
     *
     * 这题要求在一个有序的链表里面删除重复的元素,只保留一个,
     * 也是比较简单的一个题目,我们只需要判断当前指针以及下一个指针是否重复,如果是,则删除下一个指针就可以了。
     *
     * @param head 链表
     * @return 删除了重复元素后的链表
     */
    public Node deleteDulplicatesI(Node head) {
        if (head == null || head.next == null) {
            return head;
        }

        int val = head.data;
        Node p = head;
        while (p != null && p.next != null) {
            if (p.next.data != val) {
                val = p.next.data;
                p = p.next;
            } else {
                // 删除 next
                Node n = p.next.next;
                p.next = n;
            }
        }

        return head;
    }

    /**
     * 8)删除有序链表中所有重复节点
     *
     * 不同于上一题中可以保留一个,这次需要全部删除。也就是说,当前节点及其后面紧跟着的所有的重复节点都要删除掉,
     * 为了链表不断开,我们可以在遍历的时候记录一个前驱结点 prev,用来处理删除重复节点之后链表的重新连接问题。
     *
     * 时间复杂度:O(n)
     * 空间复杂度:O(1)
     *
     * @param head 链表
     * @return 删除所有重复节点之后的链表
     */
    public Node deleteDulplicatesII(Node head) {
        if (head == null || head.next == null) {
            return head;
        }

        // 建立一个新的头节点代替原来的头结点 head(相当于新建一个哨兵节点),当作头结点的 prev;
        // 因为链表有序,所以新建节点的值设为头结点的值减去 1,可以保证一定不会与链表中其他节点的值相同。
        Node soldier = new Node(head.data - 1);
        soldier.next = head;
        Node prev = soldier;
        Node p = head;
        while (p != null && p.next != null) {
            // 如果没有重复,则 prev=p, next为p.next
            if (p.data != p.next.data) {
                prev = p;
                p = p.next;
            } else {
                // 如果有重复,继续遍历,直到不重复的节点
                int val = p.data;
                Node n = p.next.next;
                while (n != null) {
                    if (n.data != val) {
                        break;
                    }
                    n = n.next;
                }

                // 删除重复节点
                prev.next = n;
                p = n;
            }
        }
        // 这里不能返回
        return soldier.next;
    }

    /**
     * 10)删除链表节点(假设要删除的节点确实在链表中)——常规法
     *
     * 删除某个链表节点需要遍历找到该链表的前一个节点,由于单链表没有指针指向前一个节点,所以需要从头开始遍历,
     * 时间复杂度为 O(n)
     *
     * @param head
     * @param toBeDeleted
     */
    public void deleteNodeI(Node head, Node toBeDeleted) {
        if (head == null || toBeDeleted == null) {
            return;
        }

        // 链表中只有一个节点或者第一个节点就是要删除的节点
        if (head == toBeDeleted) {
            head = head.next;
        } else {
            Node curr = head;
            while (curr.next != null && curr.next != toBeDeleted) {
                curr = curr.next;
            }
            // 要删除节点不存在的情况
            if (curr.next == null) {
                return;
            }
            // cur为toBeDel的前一个结点
            curr.next = curr.next.next;
        }
    }

    // 删除链表节点——在O(1)时间内删除链表节点
    public void deleteNodeII(Node head, Node toBeDeleted) {
        if (head == null || toBeDeleted == null) {
            return;
        }

        // 要删除的节点不是最后一个节点
        if (toBeDeleted.next != null) {
            Node p = toBeDeleted.next;
            toBeDeleted.data = p.data; // 将要删除节点的后继节点的值覆盖它
            toBeDeleted.next = p.next;
            // 既是头结点也是尾节点
        } else if (head == toBeDeleted) {
            head = head.next;
        // 要删除的节点仅仅是尾节点,即在含有多个节点的链表中删除链表尾节点,此时因为要删除节点不存在后继节点,所以需要从头
        // 开始白能力链表,找到它的前驱节点
        } else {
            Node curr = head;
            while (curr.next != toBeDeleted) {
                curr = curr.next;
            }
            curr.next = null;
        }
    }

    /**
     * 10)从尾到头打印链表——使用栈的方法
     *
     * 从尾到头打印链表,可以顺序遍历一遍链表然后依次将遍历到的每个节点
     * 的值压入栈中,因为栈是“后进先出”的,所以得到的就是尾节点在前,头节点在后的列表。
     */
    public ArrayList<Integer> printListReversingly_Interatively(Node head) {
        LinkedList<Integer> stack = new LinkedList<>();
        Node pHead = head;
        while (pHead != null) {
            stack.push(pHead.data);
            pHead = pHead.next;
        }

        return new ArrayList<>(stack);
    }

    /**
     * 10)从尾到头打印链表——使用递归的方法
     *
     * 递归的本质也是栈(系统默认创建)。我们可以利用递归,先递归到最后一个节点再依次返回。但如果链表很长的话
     * 不适用于递归,因为递归的深度会很大,可能会造成堆栈溢出。
     *
     * @param head
     * @return
     */
    // 这是一个类成员变量
    private ArrayList<Integer> stack = new ArrayList<>();

    public ArrayList<Integer> printListReversingly_Recursively(Node head) {
        if (head != null) {
            printListReversingly_Recursively(head.next);
            stack.add(head.data);
        }
        return stack;
    }

    /**
     * 11)两个链表的第一个公共节点
     *
     * 首先遍历得到两个链表的长度,就可以得知哪个链表的长度更大。同时也就可以得知短的链表长度比长的链表的长度
     * 短多少,此时可以让长的链表先在链表上走若干步(两链表的长度之差),然后此时接同时继续遍历两个链表,找到
     * 第一个相同的节点就是它们的第一个公共节点。
     *
     * 时间复杂度:O(m+n) m和n分别表示两个链表的长度,因为要遍历两个链表得到它们的长度,所以时间复杂度为 O(m+n)
     * 空间复杂度:O(1) 不需要栈等额外的存储空间,所以为 O(1)
     *
     * @param list1
     * @param list2
     * @return
     */
    public Node findFirstCommonNode(Node list1, Node list2) {
        // 得到两个链表的长度
        int lengthOfList1 = getLengthOfList(list1);
        int lengthOfList2 = getLengthOfList(list2);
        int lengthDif = lengthOfList1 - lengthOfList2;

        Node pListHeadLong = list1; // 假设 list1 为更长的链表
        Node pListHeadShort = list2;
        if (lengthOfList1 < lengthOfList2) {
            pListHeadLong = list2;
            pListHeadShort = list1;
            lengthDif = lengthOfList2 - lengthOfList1;
        }

        // 先让长链表先走 lengthDir 步,让后两个链表再同时走,
        for (int i = 0; i < lengthDif; i++) {
            pListHeadLong = pListHeadLong.next;
        }

        while (pListHeadLong != null && pListHeadShort != null
        || (pListHeadLong != pListHeadShort)) {
            pListHeadLong = pListHeadLong.next;
            pListHeadShort = pListHeadShort.next;
        }
        // 得到第一个公共节点
        Node firstCommonNode = pListHeadLong;
        return firstCommonNode;
    }

    // 查找链表的长度
    public int getLengthOfList(Node list) {
        int lengthOfList = 0;
        while (list != null) {
            lengthOfList++;
            list = list.next;
        }
        return lengthOfList;
    }


    public static Node createNode(int value) {
        return new Node(value, null);
    }

    public static class Node {
        private int data;
        private Node next;

        public Node(int data) {
            this.data = data;
        }

        public Node(int data, Node next) {
            this.data = data;
            this.next = next;
        }
        public int getData() {
            return data;
        }
    }
}


后记

如果你同我一样想要努力学好数据结构与算法、想要刷 LeetCode 和剑指 offer,欢迎关注我 GitHub 上的 LeetCode 题解:awesome-java-notes

你可能感兴趣的:(数据结构与算法,leetcode,单链表常见操作方法实现,单链表反转,链表中环的检测,返回链表中环的入口节点,两个有序链表的合并,链表中倒数第K个节点,单链表的删除,单链表重复元素的删除,两个链表的第一个公共节点)