找回密码
 立即注册
首页 业界区 安全 代码随想录第三天 | 链表part01

代码随想录第三天 | 链表part01

滤冽 6 天前
链表理论基础
建议:了解一下链表基础,以及链表和数组的区别
文章链接:https://programmercarl.com/链表理论基础.html
不是很了解链表和数组区别的可以先看看以上的文章。
203.移除链表元素
建议: 本题最关键是要理解 虚拟头结点的使用技巧,这个对链表题目很重要。
题目链接/文章讲解/视频讲解::https://programmercarl.com/0203.移除链表元素.html
题目感想:
1.第一种方法通过添加虚拟头节点来实现,方法很简单,新建一个虚拟头节点指向链表的原始头节点,然后开始递推即可,知道当前节点的下一个节点为NULL时退出递推,返回虚拟头节点.NEXT的节点即可,
为什么不返回原始头节点?因为原始头节点可能在处理的过程中被删了。
下面说一下递推逻辑,如果当前节点.NEXT的值为目标删除值,那我们则将当前节点的NEXT赋值为当前节点的NEXT的NEXT,相当于跳过这个节点了嘛,
如果当前节点.NEXT的值不是目标删除值,那么就移动索引到下一个节点,继续递推。
由于作者使用的编程语言是Java不用手动去管理内存,所以就能这么写,其他语言的同学就要注意一下是否需要管理内存了。
  1.     dummy.next = head;
  2.     ListNode cur = dummy;
  3.     while (cur.next != null) {
  4.         if (cur.next.val == val) {
  5.             cur.next = cur.next.next;
  6.         } else {
  7.             cur = cur.next;
  8.         }
  9.     }
  10.     return dummy.next;
复制代码
2.第二种方法是通过递归来实现,这里的递归可能不是很好理解,建议看看b站图码up的关于递归的视频:https://www.bilibili.com/video/BV1ks421w7cA/?spm_id_from=333.337.search-card.all.click&vd_source=25c680837f40da985c0f44b5389b5735
一句话来说就是,把一个大的问题依次判断直到得到一个我们能解决的小问题,然后往回递归就能把大问题给解决了,这里可能有的同学觉得会和分治很像,其实不是的,分治是平铺的,递归是有层次关系的,不过有时候分治的思想和递归的方法会同时应用在解决一个问题上,感兴趣的朋友可以在评论区交流一下。
回到这个题目,这个题目的递归出口就是当前节点的下个节点为NULL,然后往回递归的逻辑是这样的:判断当前节点的值是否是目标,是则返回这个节点的.NEXT节点,不是则返回这个节点,相当于从尾部开始删除嘛,代码如下:
  1. public ListNode removeElements(ListNode head, int val) {
  2.         if (head == null) {
  3.             return head;
  4.         }
  5.         head.next = removeElements(head.next, val);
  6.         if (head.val == val) {
  7.             return head.next;
  8.         }
  9.         return head;
  10.     }
复制代码
707.设计链表
建议: 这是一道考察 链表综合操作的题目,不算容易,可以练一练 使用虚拟头结点
题目链接/文章讲解/视频讲解:https://programmercarl.com/0707.设计链表.html
题目感想:
1.还是有两种常用的方法,第一种是单链表,这里主要说一下核心的思路:我们需要定义好链表节点,然后就是采用虚拟头节点的方式,如果是添加操作我们需要看看是否索引有效,之后找到要添加位置的前置节点,接下来就是让新节点的NEXT指向前置节点的NEXT,前置节点的NEXT指向新节点,需要注意的是我们添加了虚拟头节点,所以遍历时要注意遍历到的节点是否是我们需要的前置节点,删除操作参考上一题,查找也是遍历,在首尾添加其实也是可以复用新增节点的那个方法,首添加注意虚拟头节点,尾添加直接遍历至NEXT为NULL即可找到;
2.第二种方法是双链表法,就是每个节点既有指向后一节点的指针也有指向前一节点的指针,这就使得我们可以从前后两个位置进行遍历,需要注意的是,初始化双链表的时候我们需要将首尾节点互相指向,然后就是为了充分利用双链表,我们在进行一些有关索引的操作时,可以先进行判断该索引是离首节点近还是尾节点近,然后就和之前单链表的遍历逻辑一致了,同样的,由于有两头,我们在进行遍历或者在首尾添加新节点时需要考虑虚拟头尾节点的影响;
  1. //单链表
  2. class MyLinkedList {
  3.     class ListNode {
  4.         int val;
  5.         ListNode next;
  6.         ListNode(int val) {
  7.             this.val=val;
  8.         }
  9.     }
  10.     //size存储链表元素的个数
  11.     private int size;
  12.     //注意这里记录的是虚拟头结点
  13.     private ListNode head;
  14.     //初始化链表
  15.     public MyLinkedList() {
  16.         this.size = 0;
  17.         this.head = new ListNode(0);
  18.     }
  19.     //获取第index个节点的数值,注意index是从0开始的,第0个节点就是虚拟头结点
  20.     public int get(int index) {
  21.         //如果index非法,返回-1
  22.         if (index < 0 || index >= size) {
  23.             return -1;
  24.         }
  25.         ListNode cur = head;
  26.         //第0个节点是虚拟头节点,所以查找第 index+1 个节点
  27.         for (int i = 0; i <= index; i++) {
  28.             cur = cur.next;
  29.         }
  30.         return cur.val;
  31.     }
  32.     public void addAtHead(int val) {
  33.         ListNode newNode = new ListNode(val);
  34.         newNode.next = head.next;
  35.         head.next = newNode;
  36.         size++;
  37.         // 在链表最前面插入一个节点,等价于在第0个元素前添加
  38.         // addAtIndex(0, val);
  39.     }
  40.     public void addAtTail(int val) {
  41.         ListNode newNode = new ListNode(val);
  42.         ListNode cur = head;
  43.         while (cur.next != null) {
  44.             cur = cur.next;
  45.         }
  46.         cur.next = newNode;
  47.         size++;
  48.         // 在链表的最后插入一个节点,等价于在(末尾+1)个元素前添加
  49.         // addAtIndex(size, val);
  50.     }
  51.     // 在第 index 个节点之前插入一个新节点,例如index为0,那么新插入的节点为链表的新头节点。
  52.     // 如果 index 等于链表的长度,则说明是新插入的节点为链表的尾结点
  53.     // 如果 index 大于链表的长度,则返回空
  54.     public void addAtIndex(int index, int val) {
  55.         if (index < 0 || index > size) {
  56.             return;
  57.         }
  58.         //找到要插入节点的前驱
  59.         ListNode pre = head;
  60.         for (int i = 0; i < index; i++) {
  61.             pre = pre.next;
  62.         }
  63.         ListNode newNode = new ListNode(val);
  64.         newNode.next = pre.next;
  65.         pre.next = newNode;
  66.         size++;
  67.     }
  68.     public void deleteAtIndex(int index) {
  69.         if (index < 0 || index >= size) {
  70.             return;
  71.         }
  72.         //因为有虚拟头节点,所以不用对index=0的情况进行特殊处理
  73.         ListNode pre = head;
  74.         for (int i = 0; i < index ; i++) {
  75.             pre = pre.next;
  76.         }
  77.         pre.next = pre.next.next;
  78.         size--;
  79.     }
  80. }
复制代码
  1. //双链表
  2. class MyLinkedList {
  3.     class ListNode{
  4.         int val;
  5.         ListNode next, prev;
  6.         ListNode(int val){
  7.             this.val = val;
  8.         }
  9.     }
  10.     //记录链表中元素的数量
  11.     private int size;
  12.     //记录链表的虚拟头结点和尾结点
  13.     private ListNode head, tail;
  14.     public MyLinkedList() {
  15.         //初始化操作
  16.         this.size = 0;
  17.         this.head = new ListNode(0);
  18.         this.tail = new ListNode(0);
  19.         //这一步非常关键,否则在加入头结点的操作中会出现null.next的错误!!!
  20.         this.head.next = tail;
  21.         this.tail.prev = head;
  22.     }
  23.     public int get(int index) {
  24.         //判断index是否有效
  25.         if(index < 0 || index >= size){
  26.             return -1;
  27.         }
  28.         ListNode cur = head;
  29.         //判断是哪一边遍历时间更短
  30.         if(index >= size / 2){
  31.             //tail开始
  32.             cur = tail;
  33.             for(int i = 0; i < size - index; i++){
  34.                 cur = cur.prev;
  35.             }
  36.         }else{
  37.             for(int i = 0; i <= index; i++){
  38.                 cur = cur.next;
  39.             }
  40.         }
  41.         return cur.val;
  42.     }
  43.     public void addAtHead(int val) {
  44.         //等价于在第0个元素前添加
  45.         addAtIndex(0, val);
  46.     }
  47.     public void addAtTail(int val) {
  48.         //等价于在最后一个元素(null)前添加
  49.         addAtIndex(size, val);
  50.     }
  51.     public void addAtIndex(int index, int val) {
  52.         //判断index是否有效
  53.         if(index < 0 || index > size){
  54.             return;
  55.         }
  56.         //找到前驱
  57.         ListNode pre = head;
  58.         for(int i = 0; i < index; i++){
  59.             pre = pre.next;
  60.         }
  61.         //新建结点
  62.         ListNode newNode = new ListNode(val);
  63.         newNode.next = pre.next;
  64.         pre.next.prev = newNode;
  65.         newNode.prev = pre;
  66.         pre.next = newNode;
  67.         size++;
  68.     }
  69.     public void deleteAtIndex(int index) {
  70.         //判断index是否有效
  71.         if(index < 0 || index >= size){
  72.             return;
  73.         }
  74.         //删除操作
  75.         ListNode pre = head;
  76.         for(int i = 0; i < index; i++){
  77.             pre = pre.next;
  78.         }
  79.         pre.next.next.prev = pre;
  80.         pre.next = pre.next.next;
  81.         size--;
  82.     }
  83. }
复制代码
206.反转链表
建议先看我的视频讲解,视频讲解中对 反转链表需要注意的点讲的很清晰了,看完之后大家的疑惑基本都解决了。
题目链接/文章讲解/视频讲解:https://programmercarl.com/0206.翻转链表.html
题目感想:
1.这个题目有三种方法,先说最简单的,只用三个指针就能迭代完成,主要的思路是,初始化一个节点cur赋值为头节点,初始化一个NULL节点,这个节点主要是用来存cur反转后指向的节点,再初始化一个临时节点用来存NEXT节点是哪个,便于继续遍历,下面进行举例,首先临时节点先保存cur的NEXT节点,然后cur节点的NEXT指向NULL节点,NULL节点再保存cur节点,cur节点转换为临时节点保存的那个节点,开始下一个循环;
  1. public ListNode reverseList(ListNode head) {
  2.         ListNode prev = null;
  3.         ListNode cur = head;
  4.         ListNode temp = null;
  5.         while (cur != null) {
  6.             temp = cur.next;// 保存下一个节点
  7.             cur.next = prev;
  8.             prev = cur;
  9.             cur = temp;
  10.         }
  11.         return prev;
  12.     }
复制代码
2.第二种方法就是递归,这里的递归有两种方式,第一种方式就是从前往后递归,还有一种是从后往前递归,先说从前递归,和上面的方法一一样,也需要三个节点进行工作,然后将反转之后的大块的继续进行递归;
  1. public ListNode reverseList(ListNode head) {
  2.         return reverse(null, head);
  3.     }
  4.     private ListNode reverse(ListNode prev, ListNode cur) {
  5.         if (cur == null) {
  6.             return prev;
  7.         }
  8.         ListNode temp = null;
  9.         temp = cur.next;// 先保存下一个节点
  10.         cur.next = prev;// 反转
  11.         // 更新prev、cur位置
  12.         // prev = cur;
  13.         // cur = temp;
  14.         return reverse(cur, temp);
  15.     }
复制代码
从后往前就是先一路递归到末尾,然后递归回来的时候再处理数据
  1.     ListNode reverseList(ListNode head) {
  2.         // 边缘条件判断
  3.         if(head == null) return null;
  4.         if (head.next == null) return head;
  5.         // 递归调用,翻转第二个节点开始往后的链表
  6.         ListNode last = reverseList(head.next);
  7.         // 翻转头节点与第二个节点的指向
  8.         head.next.next = head;
  9.         // 此时的 head 节点为尾节点,next 需要指向 NULL
  10.         head.next = null;
  11.         return last;
  12.     }
复制代码
需要注意的点:
1.递归的使用还是不是很熟练,明天整理一下递归的相关笔记;

来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!
您需要登录后才可以回帖 登录 | 立即注册