一文通数据结构与算法之——链表+常见题型与解题策略+Leetcode经典题

Posted 尚墨1111

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了一文通数据结构与算法之——链表+常见题型与解题策略+Leetcode经典题相关的知识,希望对你有一定的参考价值。

1 链表

链表是最基本的数据结构,面试官常常用链表来考察面试者的基本能力,而且链表相关的操作相对而言比较简单,也适合考察写代码的能力。链表的操作也离不开指针,指针又很容易导致出错。综合多方面的原因,链表题目在面试中占据着很重要的地位。

以下内容思路主要参考:算法面试题 | 链表问题总结

1.1 常见题型及解题策略

1.1.1 LeetCode中关于链表的题目有以下五种类型题:

  • 删除链表节点
  • 反转链表
  • 合并链表
  • 排序链表
  • 环形链表


1.1.2 解题策略

  • dummy虚拟头节点,专门处理头结点可能会被改动的情况
  • 快慢双指针

1.2 链表的基本内容

推荐一篇文章:基础知识讲的很清楚,Java数据结构与算法之链表

链表的分类:单链表(分带头结点和不带头结点的单链表,就是head里面有没有data的区别)、双向链表、循环链表

重点理解指针的概念

如下代码 ans.next 指向什么?
  
ans = ListNode(1)
ans.next = head   //ans.next 指向取决于最后切断 ans.next 指向的地方在哪,所以ans.next指向head
head = head.next  //ans 和 head 被切断联系了
head = head.next

ans = ListNode(1)
head = ans// ans和head共进退
head.next = ListNode(3)
head.next = ListNode(4)
// ans.next 指向什么?ListNode(3)
ans = ListNode(1)
head = ans           //head 和 ans共进退
head.next = ListNode(3)
head = ListNode(2)   //head 和 ans 的关系就被切断了
head.next = ListNode(4)

居然找到人跟我一样卡在这里了,笑死

1.2.1 链表的基本结构:

* public class ListNode {
*     int val;
*     ListNode next;
*     ListNode() {}
*     ListNode(int val) { this.val = val; }
*     ListNode(int val, ListNode next) { this.val = val; this.next = next; }
* }

1.2.2 插入新元素

//0.找到要插入的位置
temp = 待插入位置的前驱节点.next	      //1.先用一个临时节点把 待插位置后面的内容先存起来
待插入位置的前驱节点.next = 待插入指针  //2.将新元素插入
待插入指针.next = temp		     //3.再把后面的元素接到新元素的next

1.2.3 删除某个元素

待删除位置的前驱节点.next = 待删除位置的前驱节点.next.next

1.2.4 遍历单链表

当前指针 =  头指针
while 当前节点不为空 {
   print(当前节点)
   当前指针 = 当前指针.next
}

for (ListNode cur = head; cur != null; cur = cur.next) {
    print(cur.val)
}

1.3 删除链表结点类题目

1.3.1 题解方法

  1. 画草图:理解指针的变动与思考逻辑!!(重要!实用!)
  2. 边界条件:怎么处理不会有空指针异常?在循环里放什么停止条件
  • 如果是遍历链表元素,while(node!=null)
  • 如果是删除某个元素,需要,while(node.next!=null)
  • 需要考虑的仅仅是被改变 next 指针的部分,并且循环之后哪个指针在最后的节点处,就判断谁
//比如快慢指针,输出中间节点,slow和fast的指针都在变,但是fast先指向链表尾巴,所以判断 fast
//同时每个判断next.next的都必须先判断,next,才能保证 奇偶链长 中不会出现空指针异常
while(fast.next!=null && fast.next.next!=null){
            slow = slow.next;
            fast = fast.next.next;
        }
  1. 只要会删除头结点,都要进行dummy虚指针
  2. 特殊的需求可以考虑结合各种工具类,比如删除重复里面,利用HashSet,删除倒数第k个,利用栈LinkedList

1.3.2 可能出现的问题

NullPointerException,就是当前节点为空,我们还去操作它的 next

② 输出不了结果,一定是指针移动出了问题

1.3.3 题库列表:

237. 删除链表中的节点 ====面试题 02.03. 删除中间节点

203. 移除链表元素(虚拟头结点)

19. 删除链表的倒数第 N 个结点(双指针经典类型)

237、删除链表中的节点
//237.传入待删除结点,直接将当前节点的值改为next的值,next指向next.next,实现原地更新。
public void deleteNode(ListNode node) {
    node.val = node.next.val;
    node.next = node.next.next;
}
203、移除链表元素

① 如果删除的节点是中间的节点,则问题似乎非常简单:

  • 选择要删除节点的前一个结点 prev
  • prevnext 设置为要删除结点的 next

② 当要删除的一个或多个节点位于链表的头部时,要另外处理

三种方法:

  1. 删除头结点时另做考虑(由于头结点没有前一个结点)
  2. 添加一个虚拟头结点,删除头结点就不用另做考虑
  3. 递归
  4. 双指针法

即便是参考别人的代码,一看就看的懂,但其实我们有时候不知道内涵,只要自己闭着眼睛敲一遍,发现了问题,才知道是怎么考虑出来的

// 执行耗时:1 ms,击败了99.79% 的Java用户
// 内存消耗:39.4 MB,击败了49.10% 的Java用户
// 时间复杂度是O(n)。空间复杂度O(1)

public ListNode removeElements(ListNode head, int val) {
        //删除值相同的头结点后,可能新的头结点也值相等,用循环解决
        //比如输入 [7 7 7 7] 删除7,我一开始是直接用 if,发现有些案例无法通过才知道用while的原因
        while(head!=null && head.val==val){
            head = head.next;
        }
		//因为前面是对head的操作,所以极可能最后完了,head为空,所以把判断的过程放在后面
        //我本来是吧if放在删除头结点的前面打,结果报错空指针异常,所以才知道为什么判空的要放在后面
        if(head==null){
            return head;
        }

        ListNode temp = head;//临时指针
        while(temp.next!=null){
            if(temp.next.val==val){
                temp.next = temp.next.next;
            }else{
                temp = temp.next;
            }
        }
        return head;
    }

添加一个虚拟头结点

//执行耗时:1 ms,击败了99.79% 的Java用户
//内存消耗:39.2 MB,击败了82.52% 的Java用户
//时间复杂度是O(n)。空间复杂度O(1)
  
public ListNode removeElements(ListNode head, int val){
//        创建虚节点
        ListNode dummyNode = new ListNode(val-1);
        dummyNode.next = head;
  
        ListNode prev = dummyNode;
        while(prev.next!=null){
            if(prev.next.val==val){
                prev.next = prev.next.next;
            }else{
                prev = prev.next;
            }
        }
        return dummyNode.next;
    }

递归

//时间复杂度是O(n)。递归的方法调用栈深度是n,所以空间复杂度O(n),会超时
public ListNode removeElements(ListNode head, int val){
        if(head==null){
            return head;
        }
        // 因为递归函数返回的是已经删除节点之后的头结点
        // 所以直接接上在head.next,最后就只剩下判断头结点是否与需要删除的值一致了
        head.next = removeElements(head.next,val);
        if(head.val==val){
            return head.next;
        }else{
            return head;
        }
    }
剑指 Offer 18. 删除链表的节点

双指针

// 剑指 Offer 18. 删除链表的节点

public ListNode deleteNode(ListNode head, int val){
        if(head.val==val){//因为互不相等,如果头指针相等,直接返回
            return head.next;
        }
        //双指针
        ListNode pre = head;
        ListNode cur = head.next;
        while(cur!=null && cur.val!=val){//找元素
            pre = cur;
            cur = cur.next;
        }
        if(cur!=null){//找到了,进行删除操作
            pre.next = cur.next;
        }
        return head;//删完了,返回
    }
面试题 02.01. 移除重复节点
// 面试题 02.01. 移除重复节点
// 法一:借助HashSet的特征
// 移除未排序链表中的重复节点。保留最开始出现的节点,重复的元素不一定连续
    public ListNode removeDuplicateNodes(ListNode head) {
        if (head == null) {
            return head;
        }
        ListNode temp = head;
        HashSet<Integer> set = new HashSet<>();
        set.add(head.val);
        while(temp.next!=null){
            if(set.add(temp.next.val)){//加进去说明不重复
                temp = temp.next;
            }else{
                temp.next = temp.next.next;//原地删除
            }
        }
        return head;
    }
// 法二:用空间换时间
// 双重循环,一个定位一个遍历后序,用时间换空间
    public ListNode removeDuplicateNodes(ListNode head) {
        if(head==null){
            return head;
        }
        ListNode pre = head;
        while(pre!=null){
            ListNode cur = pre;
            while(cur.next!=null){
                if(cur.next.val==pre.val){
                    cur.next = cur.next.next;
                }else{
                    cur = cur.next;
                }
            }
            pre = pre.next;
        }
        return head;
    }
82. 删除排序链表中的重复元素 II
    //升序链表,删除链表中所有重复的节点【1 1 1 1 2 3】-->【2 3】
    //双指针记录pre 用cur记录相同的数,加虚头节点
    public ListNode deleteDuplicates(ListNode head) {
        if(head==null){
            return head;
        }
        ListNode dummy = new ListNode(0);//可能删除头结点,所以使用虚节点
        dummy.next = head;
        ListNode pre = dummy;
        ListNode cur = dummy.next;

        while(cur!=null && cur.next!=null){//画图最好理解
            if(cur.val==cur.next.val ){
                //如果有奇数个相同的值,就删不完,所以必须用while循环
                while(cur!=null && cur.next!=null && cur.val==cur.next.val ){
                    cur = cur.next;//找到最后一个相等的数
                }
                pre.next = cur.next;
                cur = pre.next;
            }else{
                pre = cur;
                cur = cur.next;
            }
        }
        return dummy.next;
    }
19、删除链表的倒数第 N 个结点
// 删除链表的倒数第 n 个结点,并且返回链表的头结点
// 双指针

public ListNode removeNthFromEnd(ListNode head, int k){
        if(head==null) return head;
//        可能会删除头结点
        ListNode dummy = new ListNode(0,head);
        ListNode pre = dummy.next;
        for (int i = 0; i < k; i++) {
            pre = pre.next;
        }
        ListNode cur = dummy;
        while(pre!=null){
            cur = cur.next;
            pre = pre.next;
        }
        cur.next = cur.next.next;
        return dummy.next;
    }
//    另外一个方法,利用栈的先进后出特点,效率会更低
//    执行耗时:1 ms,击败了19.42% 的Java用户
//    内存消耗:37.7 MB,击败了5.02% 的Java用户
public ListNode removeNthFromEnd(ListNode head, int n) {
    if(head==null){
        return head;
    }
    ListNode dummy = new ListNode(0,head);
    ListNode temp = dummy;
    LinkedList<ListNode> stack = new LinkedList<>();
    while(temp!=null){
        stack.push(temp);
        temp = temp.next;
    }
    for (int i = 0; i < n; i++) {
        stack.pop();
    }
    ListNode pre = stack.peek();
    pre.next = pre.next.next;
    return dummy.next;
}
876、链表的中间结点
//执行耗时:0 ms,击败了100.00% 的Java用户
//内存消耗:35.7 MB,击败了68.38% 的Java用户
public ListNode middleNode(ListNode head) {
        ListNode slow = head;
        ListNode fast = head;
        //如果不加fast != null,链表元素个数为偶数时会报空指针异常
        while(fast!=null && fast.next!=null){
            slow = slow.next;
            fast = fast.next.next;
        }
        return slow;
    }
86、分隔链表

两个临时链表

// 给你一个链表的头节点 head 和一个特定值 x ,请你对链表进行分隔,使得所有 小于 x 的节点都出现在 大于或等于 x 的节点之前。
// 另外创建一个链表,遍历原来的链表,删除小于的接上去。可能删除头结点
    public ListNode partition(ListNode head, int x) {
        ListNode small = new ListNode(0);//可能会动头结点,所以需要虚节点
        ListNode smallHead = small;//要记住头结点,所以需要另外设置Head
        ListNode large = new ListNode(0);
        ListNode largeHead = large;

        while(head!=null){
            if(head.val<x){
                small.next = head;
                small = small.next;
            }else{
                large.next = head;
                large = large.next;
            }
            head = head.next;
        }
        large.next = null;//再拼接两个链表,尾巴指向null
        small.next = largeHead.next;
        return smallHead.next;
    }
  
328、奇偶链表(分割链表的变形)

两个临时链表的变形

    // 给定一个单链表,把所有的奇数节点和偶数节点分别排在一起。
    // 请注意,这里的奇数节点和偶数节点指的是节点编号的奇偶性,而不是节点的值的奇偶性。
    // 法一,利用额外空间

public ListNode oddEvenList(ListNode head) {
        if(head==null){
            return head;
        }

        ListNode odd = new ListNode(0);
        ListNode oddHead = odd;
        ListNode even = new ListNode(0);
        ListNode evenHead = even;

        int count = 1;
        while(head!=null){
            if(count%2==1){//奇数
                odd.next = head;
                odd = odd.next;
            }else{
                even.next = head;
                even = even.next;
            }
            head = head.next;
            count++;
        }
        even.next = null;
        odd.next = evenHead.next;
        return oddHead.next;
    }

直接双指针前后遍历奇数偶数

//    不需要额外空间,双指针操作
    public ListNode oddEvenList(ListNode head) {
        if(head==null){
            return head;
        }

        ListNode odd = head;
        ListNode even = head.next;
        ListNode evenHead = even;

        while(even!=null && even.next!=null){
            odd.next = even.next;//先把奇数连起来
            odd = odd.next;//移动奇数的指针
            even.next = odd.next;//此时加偶数
            even = even.next;//移动偶数的指针
        }
        odd.next = evenHead;
 

以上是关于一文通数据结构与算法之——链表+常见题型与解题策略+Leetcode经典题的主要内容,如果未能解决你的问题,请参考以下文章

一文通数据结构与算法之——贪心算法+常见题型与解题策略+Leetcode经典题

一文通数据结构与算法之——回溯算法+常见题型与解题策略+Leetcode经典题

一文通数据结构与算法之——图+常见题型与解题策略+Leetcode经典题

一文通数据结构与算法之——二叉树+常见题型与解题策略+Leetcode经典题

Python数据结构与算法篇-- 链表的应用与常见题型

超详细一文学会链表解题