链表高频面试题(增删改查)

前言

链表的算法题较少,不如数组。在回溯贪心动规等高级算法中也很少见链表,本文仅研究高频链表题。

链表定义:

static class ListNode {
        public int val;
        public ListNode next;

        ListNode(int x) {
            val = x;
            next = null;
        }
    }

一、剑指offer52,两个链表的第一个公共子节点

                7cb0d811ecad4a4ab1a255703dcdcb51.png


没思路时:将常用数据结构和常用的算法思想都想一遍。

常用数据结构:数组,链表,队列,栈,hash,集合,树,堆。

常用算法思想查找,排序,双指针,递归,迭代,分治,贪心,回溯,动态规划等(还有暴力(狗头))。

1.暴力解法

        将链表A和链表B中的每一个节点依次进行比较,当出现相同节点时,返回。(时间复杂度和空间复杂度太高,实现也很简单,这里不做代码展示)

2.利用数据结构

        -Hash和集合:

        将一个链表元素全部存入Map中,然后遍历第二个链表检测链表B中元素是否存在在Map<链表节点,NULL>中,存在则返回该节点。

        存入集合中,和hash相同,不赘述,看代码。

                                                                        Hash

 /**
     * 方法1:通过Hash辅助查找
     *
     * @param headA
     * @param headB
     * @return
     */
    public static ListNode findFirstCommonNodeByMap(ListNode headA, ListNode headB) {
        if (headA==null||headB==null){
            return null;
        }
        HashMap<ListNode,Integer> map=new HashMap<>();
        while (headA!=null){
            map.put(headA,null);
            headA=headA.next;
        }
        while (headB!=null){
            if (map.containsKey(headB)){
                return headB;
            }
            headB=headB.next;
        }
        return null;
    }

                                                                        集合

/**
     * 方法2:通过集合来辅助查找
     *
     * @param headA
     * @param headB
     * @return
     */
    public static ListNode findFirstCommonNodeBySet(ListNode headA, ListNode headB) {
        Set<ListNode> set=new HashSet<>();
        while (headA!=null){
            set.add(headA);
            headA=headA.next;
        }
        while (headB!=null){
            if (set.contains(headB)){
                return headB;
            }
            headB=headB.next;
        }
        return null;
    }

        -栈

        在这道题目中队列和链表本身差不多,所以没啥用。我们来看一下栈,栈的关键性质——先进后出,这道题的两条链表有特别的形状:尾巴是一样的,就像两条分支汇入同一条河流,所以我们可以用栈的性质操作尾巴,同时出栈然后找到最后出栈的相同节点,并返回。见代码:

/**
     * 方法3:通过栈
     */
    public static ListNode findFirstCommonNodeByStack(ListNode headA, ListNode headB) {
        Stack<ListNode> stackA=new Stack<>();
        Stack<ListNode> stackB=new Stack<>();
        while (headA!=null){
            stackA.push(headA);
            headA=headA.next;
        }
        while (headB!=null){
            stackB.push(headB);
            headB=headB.next;
        }
        ListNode res=null;
        while (stackA.size()>0&&stackB.size()>0){
            if (stackA.peek()==stackB.peek()){
                res=stackA.pop();
                stackB.pop();
            }else {
                break;
            }
        }
        return res;
    }

-双指针

后文会详细讲用双指针完成这道题,这里就先不实现了。

小结:

在面试时可以直接和面试官说:“可以用HashMap,集合,栈做”,然后证明给面试官看。如果你说用队列解决,然后发现解决不了,就换一种方法,一般不会细究。

二、LeetCode234判断链表是否为回文序列

500cdaa78fa54684bb7e408a25dfe692.png

 我们仍然将数据结构和算法思路想一遍

1.数据结构

        -数组:

        将链表中元素赋值到数组中,然后从两边向中间对比。——该方法逃避了这一题目的考察目标,面试时不建议这么做。

        -栈:

        方法1.将元素全部压入栈中,然后一边出栈,一边重新遍历链表,一边比较两者元素值,只要有一个不相等,就返回false。

        优化方法1,遍历一遍得到总长度,之后第二次遍历链表压栈时只压一半,后一半遍历时边出栈边比较后一半的数据,有不同的就返回false,否则返回true。

        进一步优化:既然要得到长度,那就要遍历一次链表,那我们可以一边遍历一边全部压栈,然后第二遍遍历一半同时出栈进行比较,这样就只需要遍历1.5遍,优化了时间。

        下面展示方法1和优化到最后的两段代码:

                                                                        方法1

/**
     * 方法1:全部压栈
     *
     * @param head
     * @return
     */
    public static boolean isPalindromeByAllStack(ListNode head) {
        ListNode temp = head;
        Stack<Integer> stack = new Stack();
        //把链表节点的值存放到栈中
        while (temp != null) {
            stack.push(temp.val);
            temp = temp.next;
        }
        //然后再出栈
        while (head != null) {
            if (head.val != stack.pop()) {
                return false;
            }
            head = head.next;
        }
        return true;
    }

                                                                        优化版

/**
     * 方法3:只将一半的数据压栈
     *
     * @param head
     * @return
     */
    public static boolean isPalindromeByHalfStack(ListNode head) {
        if (head==null){
            return true;
        }
        Stack<Integer> stack=new Stack<>();
        ListNode temp=head;
        int len=0;
        while (temp!=null){
            stack.push(temp.val);
            len++;
            temp=temp.next;
        }
        len/=2;
        while ((len--)>=0){
            if (head.val!=stack.pop()){
                return false;
            }
            head=head.next;
        }
        return true;
    }

         -链表

        创建一个新的链表newList,将原始链表的oldList的元素逆序放入新链表,然后遍历比较两个链表的元素。

        优化一下:我们通过遍历之后得到总长度,然后重新遍历一半反转,然后后续的一半与新链表比较。

        这两个方法比较简单,这里不做展示。

2.算法思路

        -双指针

        双指针其实是优化了上面的链表的方式,fast一次走两步,slow一次走一步,当fast走到最后时,slow刚好达到链表中间,这样就简化了遍历次数和时间(注意链表长度的奇偶哦)。那么接下来比较反转的一半链表和slow后的一半链表,我们便可以得到结果。

        看代码:

/**
     * 通过双指针的方式来判断
     *
     * @param head
     * @return
     * @pre 反转后链表的头节点
     * @prepre 每个循环用来存放该循环的慢指针的节点,在下个循环中接到反转指针前
     */
    public static boolean isPalindromeByTwoPoints(ListNode head) {
        if (head==null||head.next==null){
            return true;
        }
        ListNode slow=head,fast=head;
        ListNode pre=null,prepre=null;
        while (fast!=null&&fast.next!=null){
            pre=slow;
            slow=slow.next;
            fast=fast.next.next;
            pre.next=prepre;
            prepre=pre;
        }
        if (fast!=null){
            slow=slow.next;
        }
        while (slow!=null){
            if (slow.val!=pre.val){
                return false;
            }
            slow=slow.next;
            pre=pre.next;
        }
        return true;
    }

         -递归

        详细的递归思路在后续的文章当中我们会介绍,这里先看代码:

static ListNode temp;
    public static boolean isPalindromeByRe(ListNode head) {
        temp = head;
        return check(head);
    }
    private static boolean check(ListNode head) {
        if (head == null)
            return true;
        boolean res = check(head.next) && (temp.val == head.val);
        temp = temp.next;
        return res;
    }

 这里体现不出来递归的优势。

三、合并有序链表

        1.leetcode21合并两个有序链表

        fea8a32e48224420bfb2df5f0742490c.png

本题难度并不复杂,在解法上我们不过多赘述。我们选择的是创造一个新的链表,然后依次对比原链表的值的大小,将小的存入新链表中。注意:我们这里创建的新的头节点是真正头节点的前一个节点,所以返回新链表头时注意返回newlist.next。

public static ListNode mergeTwoLists(ListNode list1, ListNode list2) {
        // write code here
        ListNode newHead = new ListNode(-1);
        ListNode res = newHead;
        while (list1 != null || list2 != null) {

            if (list1 != null && list2 != null) {//都不为空的情况
                if (list1.val < list2.val) {
                    newHead.next = list1;
                    list1 = list1.next;
                } else if (list1.val > list2.val) {
                    newHead.next = list2;
                    list2 = list2.next;
                } else { //相等的情况,分别接两个链
                    newHead.next = list2;
                    list2 = list2.next;
                    newHead = newHead.next;
                    newHead.next = list1;
                    list1 = list1.next;
                }
                newHead = newHead.next;
            } else if (list1 != null && list2 == null) {
                newHead.next = list1;
                list1 = list1.next;
                newHead = newHead.next;
            } else if (list1 == null && list2 != null) {
                newHead.next = list2;
                list2 = list2.next;
                newHead = newHead.next;
            }
        }
        return res.next;
    }

 这里用代码非常的臃肿,所以这道题我们主要花费精力在如何优化代码,使他更简洁。

        我们可以看到,这里的while循环内的代码无比臃肿,可以将一部分条件提取出来,我们先只考虑list1和list2都不为空的情况,这时候while内依然还有3种情况:l1>l2,l1<l2,l1==l2,我们可以把相等的情况与小于情况合并,从而进一步简化代码。

        while内部的代码简化了,我们来看看外面的代码,出while循环时至少有一个链表已经遍历完毕了,那么另一个链表的尾巴我们直接接上去就行了!

看代码:

public static ListNode mergeTwoListsMoreSimple(ListNode list1, ListNode list2) {
        ListNode newhead=new ListNode(-1);
        ListNode temp=newhead;
        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 newhead.next;
    }

 2.合并多个有序链表

        我们完成了两个之后多个也非常简单,只要将他们两个两个一步一步合并就行。

public static ListNode mergeKLists(ListNode[] lists) {
        ListNode res = null;
        for (ListNode list : lists) {
            res = mergeTwoListsMoreSimple(res, list);
        }
        return res;
    }

3.leetcode1669合并两个链表

        当我们熟练掌握了上面的两道链表操作题时,很多题都差不多,我们来看一下这道:

c5e2ca3a13744563aa63240365792ba0.png

我们只要找到删除部分前一个节点和后一个节点,然后把L2接上去就OK

看代码:

/**
     * 将l2接入l1指定片段
     * */
    public ListNode mergeInBetween(ListNode list1, int a, int b, ListNode list2) {
        ListNode pre1=list1,post1=list1,post2=list2;
        int i=1,j=1;
        while (i<a){
            pre1=pre1.next;
            i++;
        }
        while (j<=b+1){
            post1=post1.next;
            j++;
        }
        while (post2.next!=null){
            post2=post2.next;
        }
        pre1.next=list2;
        post2.next=post1;
        return list1;
    }

 这里遍历时需要特别注意一下左右边界的情况,遍历时多测测。

四、双指针思想

        所谓的双指针只是两个变量而已,在链表中我们也可以用这个思想来解决许多问题,我们来集中看一下

        1.Leetcode876链表的中间节点

        这道题我们用快慢指针就非常简单,快指针一步2个节点,慢指针一步一个节点,当快指针到最后被时慢指针就在中间。(当偶数时在中间后一个节点上,注意题意)

public ListNode middleNode(ListNode head) {
        ListNode slow=head,fast=head;
        while (fast!=null&&fast.next!=null){
            fast=fast.next.next;
            slow=slow.next;
        }
        return slow;
    }

注意:这里的 fast!=null和fast.next!=null不能颠倒,当颠倒时先判断fast.next是否为空,但是fast可能是空,那么就他可能报空指针异常!!!

 

        2. 寻找倒数第K个元素

        题干:输出链表中倒数第K个元素,从1开始计数。

代码:

public static ListNode getKthFromEnd(ListNode head, int k) {
        ListNode fast = head;
        ListNode slow = head;

        while (fast != null && k > 0) {
            fast = fast.next;
            k--;
        }
        while (fast != null) {
            fast = fast.next;
            slow = slow.next;
        }
        return slow;
    }

 

        3.Leetcode61旋转链表

        ece9f545de244825adf78073590896f6.png 

方法一 

这道题,不管是在链表中还是在数组中都非常常见(以至于我在学校的算法题上都能经常看见),这里我们就可以用双指针的思想来搞定。

我们可以用双指针来找到倒数第k个节点然后就能得到两段{1,2,3}{4,5}然后再将他们换顺序接起来。

代码:

public static ListNode rotateRight(ListNode head, int k) {
        if (head==null||k==0){
            return head;
        }
        ListNode fast=head,slow=head,temp=head;
        int len=0;
        while (temp!=null){
            len++;
            temp=temp.next;
        }
        while (k%len==0){
            return head;
        }
        while (k%len>0){
            k--;
            fast=fast.next;
        }
        while (fast.next!=null){
            slow=slow.next;
            fast=fast.next;
        }
        ListNode res=slow.next;
        slow.next=null;
        fast.next=head;
        return res;
    }

注:这里慢指针的next做空,其实就是原链表做空。

 方法二

        可以利用链表反转,先将整体反转{5,4,3,2,1},然后将前前K个节点{5,4}和后续节点{3,2,1}分别反转便可得到结果。具体内容我们在下一节中会详细描述。

五、链表删除节点

我们先来整理几道leetcode上面相关专题的题目:

1.leetcode203

2.leetcode1474

3.leetcode19

4.leetcode237

5.leetcode83

6.leetcode82

在上篇文章中,我们了解了链表的删除方法,而这些题都是删除方法的拓展。我们来看一下具体解决方法。

        1.leetcode203删除特定节点

        先看题干7e3c83e547724a1bb4fc2ce2b6f6a4c7.png

我们要删除一个节点的时候,我们需要得到这个节点的前一个节点,对头节点也一样。但是头节点是第一个节点怎么办?那我们就创建一个头前的节点(dummyHead)指向它 ,同时创建一个虚拟节点(temp)来操作这个链表。那么我们判断时要判断的就是temp.next.val==val。同样的删除的也是temp.next。删除的操作见我上一篇文章。

我们直接看代码:

public static ListNode removeElements(ListNode head, int val) {
        ListNode dummyHead=new ListNode(-1);
        dummyHead.next=head;
        ListNode temp=dummyHead;
        while (temp.next!=null){
            if (temp.next.val==val){
                temp.next=temp.next.next;
            }else {
                temp=temp.next;
            }
        }
        return dummyHead.next;
    }

 解决了这道题之后,2.leetcode1474 3.leetcode19也可以轻松解决。

这两道题我们就不做解释了,直接看代码吧。

        -leetcode19

2a1815881ad8467e95a31bd06bb28111.png

 

题目的描述和栈的性质非常相符,那我们先用栈来试一试:

public static ListNode removeNthFromEndByStack(ListNode head, int n) {
        ListNode dummy = new ListNode(0);
        dummy.next = head;
        Deque<ListNode> stack = new LinkedList<ListNode>();
        ListNode cur = dummy;
        while (cur != null) {
            stack.push(cur);
            cur = cur.next;
        }
        for (int i = 0; i < n; ++i) {
            stack.pop();
        }
        ListNode prev = stack.peek();
        prev.next = prev.next.next;
        ListNode ans = dummy.next;
        return ans;
    }

然后我们用双指针来试一试:

public static ListNode removeNthFromEndByLength(ListNode head, int n) {
        ListNode dummyHead=new ListNode(-1);
        dummyHead.next=head;
        int i=1;
        ListNode fast=head,slow=dummyHead;
        if(head==null){
            return null;
        }
        while (i<n){
            fast=fast.next;
            i++;
        }
        while (fast.next!=null){
            fast=fast.next;
            slow=slow.next;
        }
        slow.next=slow.next.next;
        return dummyHead.next;
    }

如果说双指针掌握的不是那么好的话,来看看常规做法(在空间复杂度上并没有那么优越)

public static ListNode removeNthFromEndByLength(ListNode head, int n) {
        ListNode dummy = new ListNode(0);
        dummy.next = head;
        int length = getLength(head);
        ListNode cur = dummy;
        for (int i = 1; i < length - n + 1; ++i) {
            cur = cur.next;
        }
        cur.next = cur.next.next;
        ListNode ans = dummy.next;
        return ans;
    }

    public static int getLength(ListNode head) {
        int length = 0;
        while (head != null) {
            ++length;
            head = head.next;
        }
        return length;
    }

         -leetcode1474

     8b94ee543b20473d8f9562dab32cdbff.png

这道题出现了两个变量,那么我们操作的时候便需要2个节点,这让我想到了双指针。

看代码 

public ListNode deleteNodes(ListNode head, int m, int n) {
        ListNode pre =new ListNode(-1);
        ListNode post =new ListNode(-1);
        pre.next=head;
        post=head;
        int x=m,y=n;
        while (post!=null&&post.next!=null){
            while (x>0){
                if(post==null){
                    pre.next=null;
                    return head;
                }//注意空指针异常哦
                pre=pre.next;
                post=post.next;
                x--;
            }
            x=m;
            while (y>0){
                if(post==null){
                    pre.next=null;
                    return head;
                }//注意空指针异常哦
                post=post.next;
                y--;
            }
            y=n;
            pre.next=post;
        }
        return head;
    }

 2.删除重复元素

这类题目在leetcode上我们找了3道例题:leetcode82 leetcode83 leetcode1836(82和83几乎是一样的,1836将链表变成无序的,难度也增加了不少)

我们就以83为例

-leetcode83删除排序链表中的重复元素

0daa732240554acfb3222ce119901358.png

审题时我们可以发现,当出现相同的两个val时因为有序所以他们一定是挨着的,所以我们只要将第一个的next指向下一个不同的节点就可以了

代码:

/**
     * 重复元素保留一个
     *
     * @param head
     * @return
     */
    public static ListNode deleteDuplicate(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;
    }

82我在这里点一下,因为要将所有的重复元素删除,而我们操作一个节点的时候 往往需要利用指向它的前一个节点来操作删除它,所以我们这里的cur的虚拟节点应该取前一个节点,这里就要利用到上面说的dummyHead了。

接下来我们研究一下这道比较复杂的1836

-leetcode1836从未排序的列表中移除重复元素

        这道题目难在因为它是无序的,所以他的重复元素并不是挨着出现的,但是重新排序的代价过大,所以你无法直接操作使重复元素消除,那我们就可以用hash或者集合来存储和查找重复元素(我用的集合set)。

        我们先遍历一边链表,并将元素存入set中,当发现set中已经存在该元素时,我们就将他存入reset中,当遍历完成时,我们将所有的重复元素都存入了reset中(为了防止内存过度占用,我将重复元素仅存入reset中一次),然后我们将链表中存在在reset中的元素都删除便完成了这道题。

代码:

/**
     *从未排序的列表中移除重复元素
     * */
    public ListNode deleteDuplicatesUnsorted(ListNode head) {
        Set<Integer> set=new HashSet<Integer>();
        Set<Integer> reset=new HashSet<Integer>();
        ListNode temp=head;
        while (temp!=null){
            if (reset.contains(temp.val)){
                temp=temp.next;
                continue;
            }
            if (set.contains(temp.val)){
                reset.add(temp.val);
                temp=temp.next;
                continue;
            }
            set.add(temp.val);
            temp=temp.next;
        }
        ListNode dummyHead=new ListNode(-1);
        dummyHead.next=head;
        temp=dummyHead;
        while (temp!=null&&temp.next!=null){
            if (reset.contains(temp.next.val)){
                temp.next=temp.next.next;
                continue;
            }
            temp=temp.next;
        }
        return dummyHead.next;
        
    }

六、重谈第一个公共节点

        还记得前面我们留的一个悬念吗,用双指针解决第一公共节点问题。在前面的问题当中我们用了几个数据结构完成这道题,在空间复杂度上开辟了O(n)的空间,那么有没有办法降低呢。我们来看看双指针的方法和其他方法。

        -拼接法

        如果两个链表等长,那非常简单,我们只要用while(l1==l2)比较两个链表的同一个位置上的节点是否相同,如果相同就返回。但是不是所有题目都是这么巧的,不等长怎么办呢?我们把他们拼起来,将A链表和B链表拼成AB和BA一样了对不对。但是这样就能找到第一个公共节点吗?没错!我们来看下面这张图:

0e2cf1e9bb6645a7a0944f288d22bdb1.png

我们将AB链表分成两部分lefta b和righta b(相同的片段部分),因为right片段相同所以他一定等长, 我们发现这样两个链表就变成了等长且相同部分和原链表相同的两条链表。这样我们就可以用上面的方法将题目解决。如果你觉得新建链表太浪费空间,那么我们就遍历完原链表后开始遍历另一条链表,一样可以解决问题。

看代码:

/**
     * 方法4:通过序列拼接
     */
    public static ListNode findFirstCommonNodeByCombine(ListNode pHead1, ListNode pHead2) {
        if (pHead1 == null || pHead2 == null) {
            return null;
        }
        ListNode p1 = pHead1;
        ListNode p2 = pHead2;
        while (p1 != p2) {
            p1 = p1.next;
            p2 = p2.next;
            if (p1 != p2) {
                if (p1 == null) {
                    p1 = pHead2;
                }
                if (p2 == null) {
                    p2 = pHead1;
                }
            }
        }
        return p1;
    }

有的人可能会疑惑,这个if(p1!=p2)有什么用?当两条链表完全不同时如{1,2,3}和{4,5}两个链表,因为我们每次遍历完链表就会去接一段,所以while(p1==p2==null)不可能出现,那么这两个节点永远不可能相同,就死循环了。用if(p1!=p2)就可以避免这种情况。

        -双指针

        我们来解决这个悬念,双指针怎么做?从上面的双指针的应用我们可以总结出,双指针应用场景是什么?是利用差和倍数,来控制指针的快慢,从而解决问题。这道题的差是什么?当链表长度不一致的时候,差是链表的长度,也是不同部分链表的长度差

53661554c4a8416c8eaf7b12d354429d.png

那我们只要知道大括号的长度,然后让长链表先走到箭头位置,接着长短链表同时遍历,我们不就能转化成等长链表问题了?

看代码:

public static ListNode findFirstCommonNodeBySub(ListNode pHead1, ListNode pHead2) {
        if (pHead1 == null || pHead2 == null) {
            return null;
        }
        ListNode current1 = pHead1;
        ListNode current2 = pHead2;
        int l1 = 0, l2 = 0;
        while (current1 != null) {
            current1 = current1.next;
            l1++;
        }

        while (current2 != null) {
            current2 = current2.next;
            l2++;
        }
        current1 = pHead1;
        current2 = pHead2;

        int sub = l1 > l2 ? l1 - l2 : l2 - l1;

        if (l1 > l2) {
            int a = 0;
            while (a < sub) {
                current1 = current1.next;
                a++;
            }
        }

        if (l1 < l2) {
            int a = 0;
            while (a < sub) {
                current2 = current2.next;
                a++;
            }
        }

        while (current2 != current1) {
            current2 = current2.next;
            current1 = current1.next;
        }

        return current1;
    }

总结

        链表的题目其实不算很难,熟练掌握增删改查、基础数据结构和双指针的基本算法思想,我们就可以解决90%的问题。本文是我的笔记和大家分享,希望和大家共同进步!加油!!!

        

 

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值