链表高频面试算法题-leetcode

111 阅读13分钟

通常的解题思路

一个屡试不爽的方法:将常用数据结构和常用算法思想都想一遍,看看哪些能解决问题。 常用的数据结构有数组、链表、队、栈、Hash、集合、树、堆。常用的算法思想有查找、排序、双指针、 递归、迭代、分治、贪心、回溯和动态规等等。

1.两个链表第一个公共子节点

剑指offer52题 输入两个链表,找到它们的第一个公共节点。 两个链表的头结点都是已知的,相交之后成为一个单链表,但是相交的位置未知,并且相交之前的结点数 也是未知的,请设计算法找到两个链表的合并点。 :::info 解题思路:

  1. 哈希和集合:先遍历A链表,将链表中所有的节点存到一个哈希表或集合中,再遍历第二个链表B,找到第一次出现再哈希表或集合中的节点,即第一个公共节点,如果没有,就说明不存在
  2. 栈:将A,B链表中的节点分别压入两个栈中,再同时出栈,如果出栈第一个元素不相同,就说明不存在公共子节点,如果相同,则依次出栈找到不同的节点。 :::

方法一:哈希和集合

public ListNode findFirstCommonNodeBySet(ListNode headA,ListNode headB){
    方法1:哈希和集合
    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;
}

方法二:栈

   public ListNode findFirstCommonNodeBySet(ListNode headA,ListNode headB){
      //方法2:栈
      Stack<ListNode> stackA=new Stack<>();
      Stack<ListNode> stackB=new Stack<>();
      while (headA!=null){
         stackA.add(headA);
         headA=headA.next;
      }
      while (headB!=null){
         stackB.add(headB);
         headB=headB.next;
      }
      ListNode preNode=null;
      while (stackA.size()>0&&stackB.size()>0){
         if(stackA.peek()==stackB.peek()){
            preNode=stackA.pop();
            stackB.pop();
         }else {
            break;
         }
      }
      return preNode;
   }

2.判断链表是否为回文链表

234.回文链表 给你一个单链表的头节点 head ,请你判断该链表是否为回文链表。如果是,返回 true ;否则,返回 false 。

示例 1: 输入:head = [1,2,2,1] 输出:true 示例 2: 输入:head = [1,2] 输出:false :::info 解题思路:

  1. 数组:将链表的元素都存到一个数组中,再从数组两端向中间遍历
  2. 栈:链表节点依次入栈,再出栈的同时,让链表从头开始遍历,比较每一个元素
    1. 优化:将链表的元素压入栈中的同时,计算链表的长度,再比较的时候,只要比较一半的元素
  3. 反转链表:创建一个newList,将会问链表反转存入newList中,再比较前一半的元素
  4. 快慢指针: :::

方法一:栈

class Solution {
    public boolean isPalindrome(ListNode head) {
        //栈
        Stack<ListNode> stack=new Stack<>();
        ListNode cur=head;
        int len=0;
        while (cur!=null){
            stack.push(cur);
            cur=cur.next;
            len++;
        }
        int count=len/2;
        while (count>0){
            if(head.val!=stack.pop().val){
                return false;
            }else {
                count--;
                head=head.next;
            }
        }
        return true;

    }
}

方法二:快慢指针

class Solution {
    public boolean isPalindrome(ListNode head) {
        //快慢指针
        ListNode slow=head;
        ListNode fast=head;
        Stack<ListNode> stack=new Stack<>();
        int count=0;
        while (fast!=null){
            stack.push(slow);
            fast=fast.next;
            slow=slow.next;
            count++;
            if(fast!=null){
                fast=fast.next;
                count++;
            }else {
                break;
            }
        }
        if(count%2==1){
            stack.pop();
        }
        while (slow!=null){
            if(slow.val!=stack.pop().val){
                return false;
            }
            slow=slow.next;
        }
        return true;
}
}

3.合并有序列表

3.1合并两个有序列表

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

示例 1: 输入:l1 = [1,2,4], l2 = [1,3,4] 输出:[1,1,2,3,4,4] 示例 2: 输入:l1 = [], l2 = [] 输出:[] 示例 3: 输入:l1 = [], l2 = [0] 输出:[0] :::info 解题思路: 同时遍历两个链表,定义一个newHead,每次将两个链表中较小的节点存入到newHead下一个节点 :::

class Solution {
    public ListNode mergeTwoLists(ListNode list1, ListNode list2) {
        ListNode newNode=new ListNode(-1);
        ListNode res=newNode;
        ListNode cur1=list1;
        ListNode cur2=list2;
        while (cur1!=null&&cur2!=null){
            if(cur1.val>cur2.val){
                newNode.next=new ListNode(cur2.val);
                cur2=cur2.next;
            }else {
                newNode.next=new ListNode(cur1.val);
                cur1=cur1.next;
            }
            newNode=newNode.next;
        }
        if (cur1!=null){
            newNode.next=cur1;

        }
        if (cur2!=null){
            newNode.next=cur2;
        }
        return res.next;

    }
}

3.2合并K个链表

合并K个链表 给你一个链表数组,每个链表都已经按升序排列。 请你将所有链表合并到一个升序链表中,返回合并后的链表。

示例 1: 输入:lists = [[1,4,5],[1,3,4],[2,6]] 输出:[1,1,2,3,4,4,5,6] 解释:链表数组如下: [ 1->4->5, 1->3->4, 2->6 ] 将它们合并到一个有序链表中得到。 1->1->2->3->4->4->5->6 示例 2: 输入:lists = [] 输出:[] 示例 3: 输入:lists = [[]] 输出:[] :::info 解题思路:

  1. 合并k个链表,有多种方式,例如堆、归并等等。如果面试遇到,我倾向先将前两个合并,之后再将后面的逐步合并进来,这样的的好处是只要将两个合并的写清楚,合并K个就容易很多,现场写最稳妥:
  2. 可以将k个链表中的元素全部存入到集合中,集合排序后,在添加到链表中 :::

方法一

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

3.3一道无聊的好题

合并两个链表 给你两个链表 list1 和 list2 ,它们包含的元素分别为 n 个和 m 个。 请你将 list1 中下标从 a 到 b 的全部节点都删除,并将list2 接在被删除节点的位置。 下图中蓝色边和节点展示了操作后的结果: image.png 请你返回结果链表的头指针。

示例 1: image.png 输入:list1 = [0,1,2,3,4,5], a = 3, b = 4, list2 = [1000000,1000001,1000002] 输出:[0,1,2,1000000,1000001,1000002,5] 解释:我们删除 list1 中下标为 3 和 4 的两个节点,并将 list2 接在该位置。上图中蓝色的边和节点为答案链表。 示例 2: image.png输入:list1 = [0,1,2,3,4,5,6], a = 2, b = 5, list2 = [1000000,1000001,1000002,1000003,1000004] 输出:[0,1,1000000,1000001,1000002,1000003,1000004,6] 解释:上图中蓝色的边和节点为答案链表。 :::info 解题思路: 找到要删除的前一个节点pre,和删除后的下一个节点behind 然后令pre指向list2,list2的最后一个节点指向behind :::

class Solution {
    public ListNode mergeInBetween(ListNode list1, int a, int b, ListNode list2) {
        ListNode cur =list1;
        //移除部分的前一个节点
        ListNode pre=null;
        //移除部分后一个节点
        ListNode behind=null;
        while (b>=0){
            if(a-1==0){
                pre=cur;
            }
            cur=cur.next;
            b--;
            a--;
        }
        behind=cur;
        pre.next=list2;
        ListNode cur2=list2;
        while (cur2.next!=null){
            cur2=cur2.next;
        }
        cur2.next=behind;
        return list1;
        
    }
}

4.双指针专题

4.1寻找中间节点

寻找中间节点 给你单链表的头结点 head ,请你找出并返回链表的中间结点。 如果有两个中间结点,则返回第二个中间结点。

示例 1: 输入:head = [1,2,3,4,5] 输出:[3,4,5] 解释:链表只有一个中间结点,值为 3 。 示例 2: 输入:head = [1,2,3,4,5,6] 输出:[4,5,6] 解释:该链表有两个中间结点,值分别为 3 和 4 ,返回第二个结点。

:::info 解题思路: 双指针:

  1. 定义两个指针 slow和fast,
  2. fast每次走两步,slow每次走一步
  3. 如果节点个数为奇数,fast走到尾节点时,slow正好位于中间节点
  4. 如果节点个数为偶数,fast走到null(尾节点的下一个节点)slow正好为第二个中间节点
  5. 当fast为空或fast下一个节点为空的时候跳出循环

注意:搞清楚终止遍历的条件, 当fast为空或fast下一个节点为空的时候跳出循环 :::

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

4.2寻找倒数第k个元素

寻找倒数第k个元素 实现一种算法,找出单向链表中倒数第 k 个节点。返回该节点的值。 **注意:**本题相对原题稍作改动 示例: 输入: 1->2->3->4->5 和 k = 2 输出: 4 说明: 给定的 k 保证是有效的。

:::info 解题思路: 同样也是双指针问题:因为要找倒数第k个元素 我们定义两个指针fast和slow,让fast先走k步,再同时让两指针往后移动,直到fast节点为空 放回slow指针即可 :::

class Solution {
    public int kthToLast(ListNode head, int k) {
        ListNode fast=head;
        ListNode slow=head;
        while (k>0){
            fast=fast.next;
            k--;
        }
        while (fast!=null){
            fast=fast.next;
            slow=slow.next;
        }
        return slow.val;

    }
}

4.3旋转链表

旋转链表 你一个链表的头节点 head ,旋转链表,将链表每个节点向右移动 k_ _个位置。

示例 1: 输入:head = [1,2,3,4,5], k = 2 输出:[4,5,1,2,3] 示例 2: 输入:head = [0,1,2], k = 4 输出:[2,0,1]

:::info 解题思路:

  1. 统计节点个数count
  2. 同时将每个节点存入一个List集合中
  3. 如果k%count==0,直接返回头结点
  4. 将否则,尾节点指向头结点
  5. 计算index=k%count
  6. 让集合中第count-index-1个节点指向空
  7. 返回第count-index节点

注意:具体让第几个节点指向空,返回第几个节点,最笨的方法就是将具体数值带入求取 :::

class Solution {
    public ListNode rotateRight(ListNode head, int k) {
        ListNode cur=head;
        List<ListNode> list=new ArrayList<>();

        if(cur==null){
            return null;
        }
        if(cur.next==null){
            return cur;
        }
        int count=1;
        while (cur.next!=null){
            list.add(cur);
            count++;
            cur=cur.next;
        }
        list.add(cur);
        int index = k % count;
        if(index==0){
            return head;
        }
        cur.next=head;
        ListNode listNode = list.get(count-index-1);
        listNode.next=null;
        return list.get(count-index);


    }
}

5.删除链表元素专题

5.1删除特定节点

移除链表元素 给你一个链表的头节点 head 和一个整数 val ,请你删除链表中所有满足 Node.val == val 的节点,并返回 新的头节点

示例 1: 输入:head = [1,2,6,3,4,5,6], val = 6 输出:[1,2,3,4,5] 示例 2: 输入:head = [], val = 1 输出:[] 示例 3: 输入:head = [7,7,7,7], val = 7 输出:[]

:::info 解题思路:

  1. 定义一个虚拟节点,让它指向链表头节点
  2. 依次遍历它的下一个节点,如果等于val
  3. 就cur=cur.next; :::
class Solution {
    public ListNode removeElements(ListNode head, int val) {
        ListNode cur=new ListNode(-1);
        ListNode res=cur;
        cur.next=head;
        if(cur==null) return null;
        while (cur.next!=null){
            if(cur.next.val==val){
                cur.next=cur.next.next;
            }else {
                cur=cur.next;
            }
        }
        return res.next;

    }
}

5.2删除倒数第n个节点

删除倒数第n个节点 给你一个链表,删除链表的倒数第 n_ _个结点,并且返回链表的头结点。

示例 1: 输入:head = [1,2,3,4,5], n = 2 输出:[1,2,3,5] 示例 2: 输入:head = [1], n = 1 输出:[] 示例 3: 输入:head = [1,2], n = 1 输出:[1]

:::info 解题思路:

  1. 利用双指针 fast ,slow ,fast比slow先走n步
  2. 当fast指针到末尾时候
  3. 删除slow下一个节点,即slow.next=slow.next.next;
  4. 返回链表头结点

注意:如果要删除第一个节点(特殊情况),即第一步fast==null 时候,我们直接返回head.next :::

class Solution {
    public ListNode removeNthFromEnd(ListNode head, int n) {
        ListNode fast=head;
        ListNode slow=head;
        //利用双指针 fast ,slow ,fast比slow先走n步
        while (n>0){
            if(fast==null){
                return null;
            }
            fast=fast.next;
            n--;
        }
        //如果要删除第一个节点(特殊情况),即第一步fast==null 时候,我们直接返回head.next
        if(fast==null){
            return head.next;
        }
        //当fast指针到末尾时候
        while (fast!=null){
            if(fast.next==null){
                break;
            }
            fast=fast.next;
            slow=slow.next;
        }
        //删除slow下一个节点,即slow.next=slow.next.next;
        slow.next=slow.next.next;
        //返回链表头结点
        return head;

    }
}

5.3删除重复元素

5.3.1重复元素保留一个

重复元素保留一个 给定一个已排序的链表的头 head , 删除所有重复的元素,使每个元素只出现一次 。返回 已排序的链表

示例 1: 输入:head = [1,1,2] 输出:[1,2] 示例 2: 输入:head = [1,1,2,3,3] 输出:[1,2,3]

:::info 解题思路:

  1. 双指针遍历, :::
class Solution {
    public ListNode deleteDuplicates(ListNode head) {
        ListNode res=new ListNode(-101);
        ListNode newHead=res;
        ListNode cur=head;
        while (cur!=null){
            if(newHead.val!=cur.val){
                newHead.next=new ListNode(cur.val);
                newHead=newHead.next;
            }
            cur=cur.next;
        }
        return res.next;

    }
}

5.3.2 重复元素都不要

重复元素都不要 给定一个已排序的链表的头 head , 删除原始链表中所有重复数字的节点,只留下不同的数字 。返回 已排序的链表

示例 1: 输入:head = [1,2,3,3,4,4,5] 输出:[1,2,5] 示例 2: 输入:head = [1,1,1,2,3] 输出:[2,3]

class Solution {
    public ListNode deleteDuplicates(ListNode head) {
        HashMap<Integer,Integer> hm=new HashMap<>();
        ListNode cur=head;
        while (cur!=null){
            hm.put(cur.val,hm.getOrDefault(cur.val,0)+1);
            cur=cur.next;
        }
        ListNode res=new ListNode();
        ListNode myres=res;
        Set<Map.Entry<Integer, Integer>> entries = hm.entrySet();
        List<Integer> list=new ArrayList<>();


        for (Map.Entry<Integer, Integer> entry : entries) {
            if(entry.getValue()==1){
                list.add(entry.getKey());
            }
        }
        list.sort(new Comparator<Integer>() {
            @Override
            public int compare(Integer o1, Integer o2) {
                return o1-o2;
            }
        });
        for (Integer integer : list) {
            res.next=new ListNode(integer);
            res=res.next;
        }
        return myres.next;

    }
}

:::info 参考的解题思路: 直接比较cur.next和cur.next.next, 如果cur.next!.val=cur.next.next.val,让cur=cur.next 如果相同的化,就找到下一个不等于cur.next.val的节点,并且跳过中间所有的节点,重新判断cur.next!.val=cur.next.next.val,让cur=cur.next :::

class Solution {
    public ListNode deleteDuplicates(ListNode head) {
        if (head == null) {
            return head;
        }
        ListNode res = new ListNode(0, head);
        ListNode cur = res;
        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 res.next;
    }
}

6.再论第一个公共子节点

在我们第一题寻找公共子节点部分,我们看到使用Hsh或者栈是可以解决问题的,但是这样可能只能得80 分,因为这不一定是面试官想要的答案,为什么呢?因为不管你使用栈还是集合都需要开辟一个O()的空 间,那如果只使用一两个变量能否解决问题呢? 可以的,这里我们就看一下另外两种解决方式。

6.1拼接两个字符串

:::info 解题思路: 先看下面的链表A和B: A:0-1-2-3-4-5 B:a-b-4-5 如果分别拼接成AB和BA会怎么样呢? AB:0-1-2-3-4-5-a-b-4-5 BA:a-b-4-5-0-1-2-3-4-5 我们发现拼接后从最后的4开始,两个链表是一样的了,自然4就是要找的节点,所以可以通过拼接的方式 来寻找交点。这么做的道理是什么呢?我们可以从几何的角度来分析。我们假定A和B有相交的位置,以交 点为中心,可以将两个链表分别分为left a和right a,left b和right b这样四个部分,并且right a和 right b是一样的,这时候我们拼接AB和BA就是这样的结构: 我们说right a和right b是一样的,那这时候分别遍历AB和BA是不是从某个位置开始恰好就找到了相交的 点了? 这里还可以进一步优化,如果建立新的链表太浪费空间了,我们只要在每个链表访问完了之后,调整到一 下链表的表头继续遍历就行了,于是代码就出来了: 这里很多人会对为什么循环体里f(p1!=p2)这个判断有什么作用,我们在代码后面解释 :::

   public ListNode findFirstCommonNodeBySet(ListNode headA,ListNode headB){
      //方法3:拼接两个字符串
      if(headA==null||headB==null){
         return null;
      }
      ListNode p1=headA;
      ListNode p2=headB;
      while (p1!=p2){
         p1=p1.next;
         p2=p2.next;
         if(p1!=p2){
             //如果一个链表访问完,让他指向另一个链表
            if(p1==null){
               p1=headB;
            }
            if(p2==null){
               p2=headA;
            }
         }
      }
      return p1;
   }

6.2差和双指针

:::info 解题思路:

  1. 分别求出两个链表的长度len1,len2
  2. 求出差值:n
  3. 让长的链表先走n步
  4. 然后再同时遍历两个链表 :::
public class Solution {

   public ListNode findFirstCommonNodeBySet(ListNode headA,ListNode headB){
      //方法4:差和双指针
      ListNode cura=headA;
      int lena=0;
      while (cura!=null){
         lena++;
         cura=cura.next;
      }
      ListNode curb=headB;
      int lenb=0;
      while (curb!=null){
         lenb++;
         curb=curb.next;
      }
      cura=headA;
      curb=headB;
      if(lenb>lena){
         int sub=lenb-lena;
         while (sub>0){
            curb=curb.next;
            sub--;
         }
      }
      if(lena>lenb){
         int sub=lena-lenb;
         while (sub>0){
            cura=cura.next;
            sub--;
         }
      }
      while (cura!=null&&curb!=null){
         if(cura.val==curb.val){
            return cura;
         }
         cura=cura.next;
         curb=curb.next;
      }
      return null;
   }