代码随想录刷题day3丨链表理论基础,203.移除链表元素,707.设计链表,206.反转链表

代码随想录刷题day3丨链表理论基础,203.移除链表元素,707.设计链表,206.反转链表

1.链表理论基础

  • 文章链接:https://programmercarl.com/%E9%93%BE%E8%A1%A8%E7%90%86%E8%AE%BA%E5%9F%BA%E7%A1%80.html

1.1链表

  • 概述:链表是一种通过指针串联在一起的线性结构,每一个节点由两部分组成,一个是数据域一个是指针域(存放指向下一个节点的指针),最后一个节点的指针域指向null(空指针的意思)

  • 链表的入口节点称为链表的头结点也就是head

    在这里插入图片描述

  • 链表的类型

    • 单链表:上面说的就是单链表

    • 双链表:每一个节点有两个指针域,一个指向下一个节点,一个指向上一个节点。既可以向前查询也可以向后查询。

      在这里插入图片描述

    • 循环链表:链表首尾相连。可以用来解决约瑟夫环问题。

      在这里插入图片描述

      • 约瑟夫问题:

        • 约瑟夫问题是个著名的问题:N个人围成一圈,第一个人从1开始报数,报M的将被杀掉,下一个人接着从1开始报。如此反复,最后剩下一个,求最后的胜利者。
          例如只有三个人,把他们叫做A、B、C,他们围成一圈,从A开始报数,假设报2的人被杀掉。

          首先A开始报数,他报1。侥幸逃过一劫。
          然后轮到B报数,他报2。非常惨,他被杀了
          C接着从1开始报数
          接着轮到A报数,他报2。也被杀死了。
          最终胜利者是C

        • 解决方案:

          • 暴力解法:N个人看作是N个链表节点,节点1指向节点2,节点2指向节点3,……,节点N-1指向节点N,节点N指向节点1,这样就形成了一个环。然后从节点1开始1、2、3……往下报数,每报到M,就把那个节点从环上删除。下一个节点接着从1开始报数。最终链表仅剩一个节点。它就是最终的胜利者。

          • 公式法:

            • 为了方便导出递推公式,重新定义问题: N个人编号为1,2,……,N,依次报数,每报到M时,杀掉那个人,求最后胜利者的编号?

              • 递推公式:f(N,M)=(f(N−1,M)+M)%N

                • f(N,M)表示,N个人报数,每报到M时杀掉那个人,最终胜利者的编号

                • f(N−1,M)表示,N-1个人报数,每报到M时杀掉那个人,最终胜利者的编号

  • 链表的存储方式

    • 链表是通过指针域的指针链接在内存中各个节点。所以链表中的节点在内存中不是连续分布的 ,而是散乱分布在内存中的某地址上,分配机制取决于操作系统的内存管理。

1.2链表的操作

1.2.1删除节点
  • 删除D节点,如图所示:只要将C节点的next指针 指向E节点就可以了。

    在这里插入图片描述

1.2.2添加节点
  • 添加F节点,如图所示:

    在这里插入图片描述

1.3性能分析

插入/删除(时间复杂度)查询(时间复杂度)适用场景
数组O(n)O(1)数据量固定,频繁查询,较少增删
链表O(1)O(n)数据量不固定,频繁增删,较少查询
  • 数组在定义的时候,长度就是固定的,如果想改动数组的长度,就需要重新定义一个新的数组。
  • 链表的长度可以是不固定的,并且可以动态增删, 适合数据量不固定,频繁增删,较少查询的场景。

1.4定义链表节点

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;
    }
}

2.题目

2.1移除链表元素

  • 题目链接:https://leetcode.cn/problems/remove-linked-list-elements/description/

    在这里插入图片描述

  • 视频讲解:https://www.bilibili.com/video/BV18B4y1s7R9/?vd_source=1651d2836ebdf1a7446e95eebf87f882

  • 文档讲解:https://programmercarl.com/0203.%E7%A7%BB%E9%99%A4%E9%93%BE%E8%A1%A8%E5%85%83%E7%B4%A0.html

  • 解题思路:

    • 法一:直接使用原来的链表来进行删除操作。

      • 移除头结点和移除其他节点的操作是不一样的,因为链表的其他节点都是通过前一个节点来移除当前节点,而头结点没有前一个节点。

      • 所以头结点如何移除呢,其实只要将头结点向后移动一位就可以,这样就从链表中移除了一个头结点。

      • 举例:

        • 移除头结点

        在这里插入图片描述

        • 移除其它节点

          在这里插入图片描述

    • 法二:设置一个虚拟头结点在进行删除操作。这样原链表的所有节点就都可以按照统一的方式进行移除了。

      在这里插入图片描述

      • 最后呢在题目中,return 头结点的时候,别忘了 return dummyNode->next;, 这才是新的头结点
  • 代码:

    • 用原来的链表操作:

      //单链表
      /**
       * Definition for singly-linked list.
       * 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; }
       * }
       */
      class Solution {
          public ListNode removeElements(ListNode head, int val) {
              while (head != null && head.val == val) {
                  head = head.next;
              }
              if (head == null) {
                  return head;
              }
              ListNode cur = head;
              while (cur != null && cur.next != null) {
                  if (cur.next.val == val) {
                      cur.next = cur.next.next;
                  } else {
                      cur = cur.next;
                  }
              }
              return head;
          }
      }
      
    • 设置一个虚拟头节点:

      /**
       * Definition for singly-linked list.
       * 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; }
       * }
       */
      class Solution {
          public ListNode removeElements(ListNode head, int val) {
              // 设置一个虚拟的头结点
              ListNode dummy = new ListNode();
              dummy.next = head;
              ListNode cur = dummy;
              while(cur.next != null){
                  if(cur.next.val == val){
                      cur.next = cur.next.next;
                  }else{
                      cur = cur.next;
                  }
              }
              return dummy.next;
          }
      }
      
  • 总结:做题时最好画下草图,方便理解。关键在于理解指针如何移动,定义动态指针cur实现遍历操作。使用虚拟头结点会更方便。

2.2设计链表

  • 题目链接:https://leetcode.cn/problems/design-linked-list/description/

    在这里插入图片描述

  • 视频讲解:https://www.bilibili.com/video/BV1FU4y1X7WD/?vd_source=1651d2836ebdf1a7446e95eebf87f882

  • 文档讲解:https://programmercarl.com/0707.%E8%AE%BE%E8%AE%A1%E9%93%BE%E8%A1%A8.html

  • 解题思路:

    • 题目大致意思分为以下几个:(n从0开始)
      • 获取第n个节点的值(第0个节点就是head节点)
      • 头部插入节点
      • 尾部插入节点
      • 第n个节点前插入节点
      • 删除第n个节点
    • 该题统一使用虚拟头结点的方式
  • 代码:

    //单链表
    //时间复杂度: 涉及 index 的相关操作为 O(index), 其余为 O(1)
    //空间复杂度: O(n)
    class ListNode{
        int val;
        ListNode next;
        ListNode(){
        }
        ListNode(int val){
            this.val = val;
        }
    }
    
    class MyLinkedList {
        //size存储链表元素的个数
        int size;
        //虚拟头节点
        ListNode dummyhead;
    
        //初始化链表
        public MyLinkedList() {
            size = 0;
            dummyhead = new ListNode();
        }
        
        //获取第index个节点的数值,注意index是从0开始的,第0个节点就是头结点
        public int get(int index) {
            //如果index非法,返回 -1
            if(index < 0 || index >= size){
                return -1;
            }
            ListNode cur = dummyhead;
            for(int i = 0;i <= index;i++){
                cur = cur.next;
            }
            return cur.val;
        }
        
        public void addAtHead(int val) {
            ListNode newNode = new ListNode(val);
            newNode.next = dummyhead.next;
            dummyhead.next = newNode;
            size++;
        }
        
        public void addAtTail(int val) {
            ListNode newNode1 = new ListNode(val);
            ListNode cur = dummyhead;
            while(cur.next != null){
                cur = cur.next;
            }
            cur.next = newNode1;
            size++;
        }
        
        public void addAtIndex(int index, int val) {
            if(index < 0 || index > size){
                return;
            }
            ListNode newNode2 = new ListNode(val);
            ListNode cur = dummyhead;
            for(int i = 0;i < index;i++){
                cur = cur.next;
            }
            newNode2.next = cur.next;
            cur.next = newNode2;
            size++;
        }
        
        public void deleteAtIndex(int index) {
            if(index < 0 || index >= size){
                return;
            }
            ListNode cur = dummyhead;
            for(int i = 0;i < index;i++){
                cur = cur.next;
            }
            cur.next = cur.next.next;
            size--;
        }
    }
    
  • 总结:

    在这里插入图片描述

    • cur 的位置非常重要

2.3反转链表

  • 题目链接:https://leetcode.cn/problems/reverse-linked-list/description/

    在这里插入图片描述

  • 视频讲解:https://www.bilibili.com/video/BV1nB4y1i7eL/?vd_source=1651d2836ebdf1a7446e95eebf87f882

  • 文档讲解:https://programmercarl.com/0206.%E7%BF%BB%E8%BD%AC%E9%93%BE%E8%A1%A8.html

  • 解题思路:双指针解法(建议)或者递归法

    • 如果再定义一个新的链表,实现链表元素的反转,其实这是对内存空间的浪费。

    • 其实只需要改变链表的next指针的指向,直接将链表反转 ,而不用重新定义一个新的链表,如图所示:

      在这里插入图片描述

    • 之前链表的头节点是元素1, 反转之后头结点就是元素5 ,这里并没有添加或者删除节点,仅仅是改变next指针的方向。

    • 步骤:

      • 首先定义一个cur指针,指向头结点,再定义一个pre指针,初始化为null。
      • 然后就要开始反转了,首先要把 cur->next 节点用tmp指针保存一下,也就是保存一下这个节点。
      • 为什么要保存一下这个节点呢,因为接下来要改变 cur->next 的指向了,将cur->next 指向pre ,此时已经反转了第一个节点了。
      • 接下来,就是循环走如下代码逻辑了,继续移动pre和cur指针。
      • 最后,cur 指针已经指向了null,循环结束,链表也反转完毕了。 此时我们return pre指针就可以了,pre指针就指向了新的头结点。
  • 代码:

    • 双指针解法

      /**
       * Definition for singly-linked list.
       * 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; }
       * }
       */
      //时间复杂度: O(n)
      //空间复杂度: O(1)
      class Solution {
          public ListNode reverseList(ListNode head) {
              ListNode cur = head;
              ListNode pre = null;
              ListNode temp = null;
              while(cur != null){
                  temp = cur.next;
                  cur.next = pre;
                  pre = cur;
                  cur = temp;
              }
              return pre;
          }
      }
      
    • 递归法

      //时间复杂度: O(n)
      //空间复杂度: O(n)
      /**
       * Definition for singly-linked list.
       * 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; }
       * }
       */
      class Solution {
          public ListNode reverseList(ListNode head) {
              return reverse(null,head);
          }
      
          private ListNode reverse(ListNode pre,ListNode cur){
              if(cur == null){
                  return pre;
              }
              ListNode temp = null;
              temp = cur.next;// 先保存下一个节点
              cur.next = pre;// 反转
              // 更新prev、cur位置
              // prev = cur;
              // cur = temp;
              return reverse(cur,temp);
          }
      }
      
  • 总结:

    • temp 作为中间量保存 cur

    • 终止条件就是 cur= null时

    • 递归法相对抽象一些,但是其实和双指针法是一样的逻辑,同样是当cur为空的时候循环结束,不断将cur指向pre的过程。

    • 分析总结图

      在这里插入图片描述

  • 8
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值