链表理论基础

建议:了解一下链表基础,以及链表和数组的区别

文章链接:https://programmercarl.com/链表理论基础.html

不是很了解链表和数组区别的可以先看看以上的文章。

203.移除链表元素

建议: 本题最关键是要理解 虚拟头结点的使用技巧,这个对链表题目很重要。

题目链接/文章讲解/视频讲解::https://programmercarl.com/0203.移除链表元素.html

题目感想:

1.第一种方法通过添加虚拟头节点来实现,方法很简单,新建一个虚拟头节点指向链表的原始头节点,然后开始递推即可,知道当前节点的下一个节点为NULL时退出递推,返回虚拟头节点.NEXT的节点即可,

为什么不返回原始头节点?因为原始头节点可能在处理的过程中被删了。

下面说一下递推逻辑,如果当前节点.NEXT的值为目标删除值,那我们则将当前节点的NEXT赋值为当前节点的NEXT的NEXT,相当于跳过这个节点了嘛,

如果当前节点.NEXT的值不是目标删除值,那么就移动索引到下一个节点,继续递推。

由于作者使用的编程语言是Java不用手动去管理内存,所以就能这么写,其他语言的同学就要注意一下是否需要管理内存了。

    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;

2.第二种方法是通过递归来实现,这里的递归可能不是很好理解,建议看看b站图码up的关于递归的视频:https://www.bilibili.com/video/BV1ks421w7cA/?spm_id_from=333.337.search-card.all.click&vd_source=25c680837f40da985c0f44b5389b5735

一句话来说就是,把一个大的问题依次判断直到得到一个我们能解决的小问题,然后往回递归就能把大问题给解决了,这里可能有的同学觉得会和分治很像,其实不是的,分治是平铺的,递归是有层次关系的,不过有时候分治的思想和递归的方法会同时应用在解决一个问题上,感兴趣的朋友可以在评论区交流一下。

回到这个题目,这个题目的递归出口就是当前节点的下个节点为NULL,然后往回递归的逻辑是这样的:判断当前节点的值是否是目标,是则返回这个节点的.NEXT节点,不是则返回这个节点,相当于从尾部开始删除嘛,代码如下:

 public ListNode removeElements(ListNode head, int val) {
if (head == null) {
return head;
} head.next = removeElements(head.next, val);
if (head.val == val) {
return head.next;
}
return head;
}

707.设计链表

建议: 这是一道考察 链表综合操作的题目,不算容易,可以练一练 使用虚拟头结点

题目链接/文章讲解/视频讲解:https://programmercarl.com/0707.设计链表.html

题目感想:

1.还是有两种常用的方法,第一种是单链表,这里主要说一下核心的思路:我们需要定义好链表节点,然后就是采用虚拟头节点的方式,如果是添加操作我们需要看看是否索引有效,之后找到要添加位置的前置节点,接下来就是让新节点的NEXT指向前置节点的NEXT,前置节点的NEXT指向新节点,需要注意的是我们添加了虚拟头节点,所以遍历时要注意遍历到的节点是否是我们需要的前置节点,删除操作参考上一题,查找也是遍历,在首尾添加其实也是可以复用新增节点的那个方法,首添加注意虚拟头节点,尾添加直接遍历至NEXT为NULL即可找到;

2.第二种方法是双链表法,就是每个节点既有指向后一节点的指针也有指向前一节点的指针,这就使得我们可以从前后两个位置进行遍历,需要注意的是,初始化双链表的时候我们需要将首尾节点互相指向,然后就是为了充分利用双链表,我们在进行一些有关索引的操作时,可以先进行判断该索引是离首节点近还是尾节点近,然后就和之前单链表的遍历逻辑一致了,同样的,由于有两头,我们在进行遍历或者在首尾添加新节点时需要考虑虚拟头尾节点的影响;

//单链表
class MyLinkedList { class ListNode {
int val;
ListNode next;
ListNode(int val) {
this.val=val;
}
}
//size存储链表元素的个数
private int size;
//注意这里记录的是虚拟头结点
private ListNode head; //初始化链表
public MyLinkedList() {
this.size = 0;
this.head = new ListNode(0);
} //获取第index个节点的数值,注意index是从0开始的,第0个节点就是虚拟头结点
public int get(int index) {
//如果index非法,返回-1
if (index < 0 || index >= size) {
return -1;
}
ListNode cur = head;
//第0个节点是虚拟头节点,所以查找第 index+1 个节点
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 = head.next;
head.next = newNode;
size++; // 在链表最前面插入一个节点,等价于在第0个元素前添加
// addAtIndex(0, val);
} public void addAtTail(int val) {
ListNode newNode = new ListNode(val);
ListNode cur = head;
while (cur.next != null) {
cur = cur.next;
}
cur.next = newNode;
size++; // 在链表的最后插入一个节点,等价于在(末尾+1)个元素前添加
// addAtIndex(size, val);
} // 在第 index 个节点之前插入一个新节点,例如index为0,那么新插入的节点为链表的新头节点。
// 如果 index 等于链表的长度,则说明是新插入的节点为链表的尾结点
// 如果 index 大于链表的长度,则返回空
public void addAtIndex(int index, int val) {
if (index < 0 || index > size) {
return;
} //找到要插入节点的前驱
ListNode pre = head;
for (int i = 0; i < index; i++) {
pre = pre.next;
}
ListNode newNode = new ListNode(val);
newNode.next = pre.next;
pre.next = newNode;
size++;
} public void deleteAtIndex(int index) {
if (index < 0 || index >= size) {
return;
}
//因为有虚拟头节点,所以不用对index=0的情况进行特殊处理
ListNode pre = head;
for (int i = 0; i < index ; i++) {
pre = pre.next;
}
pre.next = pre.next.next;
size--;
}
}
//双链表
class MyLinkedList { class ListNode{
int val;
ListNode next, prev;
ListNode(int val){
this.val = val;
}
} //记录链表中元素的数量
private int size;
//记录链表的虚拟头结点和尾结点
private ListNode head, tail;
public MyLinkedList() {
//初始化操作
this.size = 0;
this.head = new ListNode(0);
this.tail = new ListNode(0);
//这一步非常关键,否则在加入头结点的操作中会出现null.next的错误!!!
this.head.next = tail;
this.tail.prev = head;
}
public int get(int index) {
//判断index是否有效
if(index < 0 || index >= size){
return -1;
}
ListNode cur = head;
//判断是哪一边遍历时间更短
if(index >= size / 2){
//tail开始
cur = tail;
for(int i = 0; i < size - index; i++){
cur = cur.prev;
}
}else{
for(int i = 0; i <= index; i++){
cur = cur.next;
}
}
return cur.val;
}
public void addAtHead(int val) {
//等价于在第0个元素前添加
addAtIndex(0, val);
}
public void addAtTail(int val) {
//等价于在最后一个元素(null)前添加
addAtIndex(size, val);
}
public void addAtIndex(int index, int val) {
//判断index是否有效
if(index < 0 || index > size){
return;
} //找到前驱
ListNode pre = head;
for(int i = 0; i < index; i++){
pre = pre.next;
}
//新建结点
ListNode newNode = new ListNode(val);
newNode.next = pre.next;
pre.next.prev = newNode;
newNode.prev = pre;
pre.next = newNode;
size++;
}
public void deleteAtIndex(int index) {
//判断index是否有效
if(index < 0 || index >= size){
return;
} //删除操作
ListNode pre = head;
for(int i = 0; i < index; i++){
pre = pre.next;
}
pre.next.next.prev = pre;
pre.next = pre.next.next;
size--;
}
}

206.反转链表

建议先看我的视频讲解,视频讲解中对 反转链表需要注意的点讲的很清晰了,看完之后大家的疑惑基本都解决了。

题目链接/文章讲解/视频讲解:https://programmercarl.com/0206.翻转链表.html

题目感想:

1.这个题目有三种方法,先说最简单的,只用三个指针就能迭代完成,主要的思路是,初始化一个节点cur赋值为头节点,初始化一个NULL节点,这个节点主要是用来存cur反转后指向的节点,再初始化一个临时节点用来存NEXT节点是哪个,便于继续遍历,下面进行举例,首先临时节点先保存cur的NEXT节点,然后cur节点的NEXT指向NULL节点,NULL节点再保存cur节点,cur节点转换为临时节点保存的那个节点,开始下一个循环;

 public ListNode reverseList(ListNode head) {
ListNode prev = null;
ListNode cur = head;
ListNode temp = null;
while (cur != null) {
temp = cur.next;// 保存下一个节点
cur.next = prev;
prev = cur;
cur = temp;
}
return prev;
}

2.第二种方法就是递归,这里的递归有两种方式,第一种方式就是从前往后递归,还有一种是从后往前递归,先说从前递归,和上面的方法一一样,也需要三个节点进行工作,然后将反转之后的大块的继续进行递归;

 public ListNode reverseList(ListNode head) {
return reverse(null, head);
} private ListNode reverse(ListNode prev, ListNode cur) {
if (cur == null) {
return prev;
}
ListNode temp = null;
temp = cur.next;// 先保存下一个节点
cur.next = prev;// 反转
// 更新prev、cur位置
// prev = cur;
// cur = temp;
return reverse(cur, temp);
}

从后往前就是先一路递归到末尾,然后递归回来的时候再处理数据

    ListNode reverseList(ListNode head) {
// 边缘条件判断
if(head == null) return null;
if (head.next == null) return head; // 递归调用,翻转第二个节点开始往后的链表
ListNode last = reverseList(head.next);
// 翻转头节点与第二个节点的指向
head.next.next = head;
// 此时的 head 节点为尾节点,next 需要指向 NULL
head.next = null;
return last;
}

需要注意的点:

1.递归的使用还是不是很熟练,明天整理一下递归的相关笔记;

代码随想录第三天 | 链表part01的更多相关文章

  1. 代码随想录训练营day 4|链表基础理论,移除链表元素,设计链表,反转链表

    链表理论基础 链表是一种由指针串联在一起的线性结构,每一个节点都由一个数据域和一个指针域组成. 链表的类型有:单链表.双链表.循环链表. 链表的存储方式:在内存中不连续分布. 链表的定义很多人因为不重 ...

  2. 代码随想录-day1

    链表 今天主要是把链表专题刷完了,链表专题的题目不是很难,基本都是考察对链表的操作的理解. 在处理链表问题的时候,我们通常会引入一个哨兵节点(dummy),dummy节点指向原链表的头结点.这样,当我 ...

  3. 剑指offer题目系列三(链表相关题目)

    本篇延续上一篇剑指offer题目系列二,介绍<剑指offer>第二版中的四个题目:O(1)时间内删除链表结点.链表中倒数第k个结点.反转链表.合并两个排序的链表.同样,这些题目并非严格按照 ...

  4. 代码随想录第十三天 | 150. 逆波兰表达式求值、239. 滑动窗口最大值、347.前 K 个高频元素

    第一题150. 逆波兰表达式求值 根据 逆波兰表示法,求表达式的值. 有效的算符包括 +.-.*./ .每个运算对象可以是整数,也可以是另一个逆波兰表达式. 注意 两个整数之间的除法只保留整数部分. ...

  5. 代码随想录第八天 |344.反转字符串 、541. 反转字符串II、剑指Offer 05.替换空格 、151.翻转字符串里的单词 、剑指Offer58-II.左旋转字符串

    第一题344.反转字符串 编写一个函数,其作用是将输入的字符串反转过来.输入字符串以字符数组 s 的形式给出. 不要给另外的数组分配额外的空间,你必须原地修改输入数组.使用 O(1) 的额外空间解决这 ...

  6. 代码随想录 day0 博客怎么写

    前言 2.25日开始记录自己的博客生涯以及代码随想录训练营的每日内容 一.题目链接怎么找?怎么设置连接? 力扣题目链接1:力扣 二.正文怎么写? 二分查找 算法思路: 二分查找需要保证数组为有序数组同 ...

  7. C代码实现非循环单链表

    C代码实现非循环单链表, 直接上代码. # include <stdio.h> # include <stdlib.h> # include <malloc.h> ...

  8. .net之工作流工程展示及代码分享(三)数据存储引擎

    数据存储引擎是本项目里比较有特色的模块. 特色一,使用接口来对应不同的数据库.数据库可以是Oracle.Sqlserver.MogoDB.甚至是XML文件.采用接口进行对应: public inter ...

  9. Python实现C代码统计工具(三)

    目录 Python实现C代码统计工具(三) 声明 一. 性能分析 1.1 分析单条语句 1.2 分析代码片段 1.3 分析整个模块 二. 制作exe Python实现C代码统计工具(三) 标签: Py ...

  10. 持续集成之代码质量管理-Sonar [三]

    转载:https://www.abcdocker.com/abcdocker/2053 摘要 Sonar 是一个用于代码质量管理的开放平台.通过插件机制,Sonar 可以集成不同的测试工具,代码分析工 ...

随机推荐

  1. 【MathType】word2016数学公式编号

    问题 毕业论文排版中,对数学公式需要类似(3-1)的格式. 解决技巧 在写论文初稿的时候,先不要于公式的编号,先给它编一个号,比如(3) (2) (4)的. 最后写完了以后,再再添加section , ...

  2. mac输入法 cpu占用,解决mac使用输入法出现卡顿延迟

    1.介绍 网上有各种方法,例如有touchbar的macbook关闭输入建议:定时重启"简体中文输入法"进程:关闭"显示器具有单独的空间" 这些方法网上都能看到 ...

  3. MongoDB入门介绍与案例分析

    一.MongoDB 数据库定位 首先我们来看一下 MongoDB 是什么样的数据库.数据库分两大类: OLTP(Online Transaction Processing)联机事务处理. OLAP(O ...

  4. BGP四大属性

    **公认必遵**:Origin.AS_Path.Next_hop(所有BGP路由都必须识别这类属性,且必须在Update报文中传递,如果缺少就报错) Origin:指示路由信息的来源(如IGP.EGP ...

  5. MySQL 的 JSON 查询

    MySQL 的 JSON 路径格式 MySQL 使用特定的 JSON 路径表达式语法来导航和提取 JSON 文档中的数据 基本结构 MySQL 中的 JSON 路径遵循以下通用格式 $[路径组件] 路 ...

  6. 话说Hangfire

    参考文档 www.hangfire.io github.com/HangfireIO/Hangfire .NET Core开源组件:后台任务利器之Hangfire

  7. Jenkins pipeline jenkinsfile的两种写作方式声明式和脚本式

    Jenkins pipeline jenkinsfile的两种写作方式,声明式和脚本式. 为什么需要pipeline? 在多年前Jenkins成为最流行的持续集成服务器的Jenkins 1.x时代,所 ...

  8. Sentinel源码—7.参数限流和注解的实现

    大纲 1.参数限流的原理和源码 2.@SentinelResource注解的使用和实现 1.参数限流的原理和源码 (1)参数限流规则ParamFlowRule的配置Demo (2)ParamFlowS ...

  9. PC端网页/web通过自定义协议唤起启动windows桌面应用

    PC端网页/web通过自定义协议唤起启动windows桌面应用 步骤: 写注册表 调用 Windows Registry Editor Version 5.00 [HKEY_CURRENT_USER\ ...

  10. 关于Cesium渲染PrimitiveCollection和图层的树状管理的问题

    原文:关于Cesium渲染PrimitiveCollection和图层的树状管理的问题 - 搜栈网 (seekstack.cn)