2.单链表练习
1. 链表的基本概念
链表(Linked List)是一种常见的数据结构,用于存储一系列元素,这些元素可以是任意类型的数据。链表中的每个元素被称为节点(Node),每个节点包含两部分:一个存储数据的值(或称为数据域),以及一个指向下一个节点的引用(或称为指针或链接)。
链表与数组相比,具有一些优势和不同之处:
-
动态性: 链表的大小可以在运行时动态地改变,而数组的大小通常是静态的。
-
插入和删除: 在链表中插入或删除元素相对容易,只需修改节点的引用,不需要像数组一样移动大量元素。
-
空间利用: 链表可以有效地利用内存,因为每个节点只需存储自身的值和下一个节点的引用,而数组可能需要一块连续的内存空间。
链表有几种常见的类型,其中最常见的是单向链表和双向链表:
-
单向链表(Singly Linked List): 每个节点包含一个数据域和一个指向下一个节点的引用。链表的首节点称为头节点,链表的尾节点的下一个节点引用为空。
-
双向链表(Doubly Linked List): 每个节点包含一个数据域和两个引用,分别指向前一个节点和后一个节点。这使得在链表中可以双向遍历。
基本操作:
-
插入操作: 插入节点涉及创建一个新的节点,并将其插入到合适的位置。对于单向链表,需要修改前一个节点的引用,对于双向链表,还需要修改后一个节点的前向引用。
-
删除操作: 删除节点涉及将待删除节点的前一个节点的引用指向待删除节点的下一个节点。同样,对于双向链表,还需要修改后一个节点的前向引用。
-
搜索操作: 从头节点开始,按顺序遍历链表,查找特定值的节点。
-
遍历操作: 从头节点开始,按顺序访问链表的每个节点,执行所需的操作。
链表在许多编程场景中都有用途,例如实现栈、队列、缓存等数据结构,也常用于解决某些特定的问题,如链表反转、寻找中间节点等。然而,需要注意链表的操作可能比数组稍微复杂,因为需要更多的指针操作。
2. 题目中的结构体
struct ListNode {int val;ListNode* next;ListNode() : val(0), next(nullptr) {}ListNode(int x) : val(x), next(nullptr) {}ListNode(int x, ListNode* next) : val(x), next(next) {}
};
ListNode* next;是单链表特性,除了储存value以外还都储存了next,指向下一个节点的指针。
这段代码定义了一个名为 ListNode 的结构体,用于表示链表中的节点。这个结构体有三个不同的构造函数,以便于创建节点对象:
- 默认构造函数:
ListNode() : val(0), next(nullptr) {}
这个构造函数会创建一个值为0、下一个节点为空的链表节点。
- 带有一个整数参数的构造函数:
ListNode(int x) : val(x), next(nullptr) {}
这个构造函数会创建一个具有给定整数值 x、下一个节点为空的链表节点。
- 带有两个参数的构造函数:
ListNode(int x, ListNode* next) : val(x), next(next) {}
这个构造函数会创建一个具有给定整数值 x 和指向下一个节点的指针 next 的链表节点。
在这个结构体中,val 成员变量用于存储节点的值,而 next 成员变量是一个指向下一个节点的指针。这样,我们就可以通过创建多个 ListNode 对象,并使用 next 指针将它们链接在一起,从而形成一个链表。
这个结构体的定义允许我们在链表操作中方便地创建和操作节点,以实现链表的常见操作,如插入、删除和遍历。
2. 移除链表元素
2.1 以案例来学习链表,普通的删除方法
复习指针
1. delete ptr不是删除指针, 而是释放指针指向的内存
这里因为tmp和head都指向了同一内存空间,所以delete tmp就是释放之前的head, 因为我们要对head进行一个变动
while (head != NULL && head->val == val) { // 注意这里不是ifListNode* tmp = head;head = head->next;delete tmp;}
2. 比较容易的错误写法
在删除非头结点的时候容易犯这个错误, 因为是链表,写的时候就很容易参照链表的delete函数的写法,就是直接给一个tmp, 然后cur->next, 然后就直接删除,其实这样是不对的,因为链表删掉了中间节点还要考虑前后的加在一起
// 删除非头结点的节点ListNode *cur = head; // 当前指针是headwhile (cur != NULL){if (cur->val == val){ListNode* tmp = cur; // tmp, head指向同一内存空间cur = cur->next; // 头结点指针指向原来链表的第二个节点delete tmp;}
#include <iostream>struct ListNode {int val;ListNode* next;ListNode() : val(0), next(nullptr) {}ListNode(int x) : val(x), next(nullptr) {}ListNode(int x, ListNode* next) : val(x), next(next) {}
};class Solution {
public:ListNode* removeElements(ListNode* head, int val) {// 删除头节点,同时也判断新的头结点是否需要删除while (head != NULL && head->val == val){ListNode *tmp = head; // 这里让tmp指向需要被移除头结点同一地址head = head->next; // 这里头结点的指针指向下一个节点delete tmp; // 移除tmp, 同样也释放掉需要操作头结点的指针}// 删除非头结点的节点,这里不只是简单的删除,还要考虑把前一个后一个链接起来/*ex: 1->2->3->2->5, 处理2, 因为这里处理的是非头节点,在1位置链接3处理2,在3链接5处理2如果遍历的时候在2处理2,还要写一个prev指针如果5要处理,就在前一个地方(2)处理,5的next是nullptr, (2)链接5就行了*/ListNode *cur = head; // 这里已经考虑完了头结点, 所以这个头结点不用操作while (cur != NULL && cur->next != NULL){ // 遍历链表的基本手法if (cur->next->val == val){ ListNode *tmp = cur->next;cur->next = cur->next->next;delete tmp;}else {cur = cur->next;}}return head;}
};// 辅助函数:打印链表
void printList(ListNode* head) {ListNode* current = head;while (current != nullptr) {std::cout << current->val << " ";current = current->next;}std::cout << std::endl;
}// 这个地方跟上面删除非头结点最大的区别就是不用维护这个代码
void deleteList(ListNode* head) {ListNode* current = head;while (current != nullptr) {ListNode* tmp = current;current = current->next;delete tmp;}
}int main() {Solution solution;// 示例 1ListNode* head1 = new ListNode(1);head1->next = new ListNode(2);head1->next->next = new ListNode(6);head1->next->next->next = new ListNode(3);head1->next->next->next->next = new ListNode(4);head1->next->next->next->next->next = new ListNode(5);head1->next->next->next->next->next->next = new ListNode(6);int val1 = 6;ListNode* newHead1 = solution.removeElements(head1, val1);std::cout << "Case 1: ";printList(newHead1);// 示例 2ListNode* head2 = nullptr;int val2 = 1;ListNode* newHead2 = solution.removeElements(head2, val2);std::cout << "Case 2: ";printList(newHead2);// 示例 3ListNode* head3 = new ListNode(7);ListNode* current3 = head3;for (int val : {7, 7, 7}){current3->next = new ListNode(val);current3 = current3->next;}std::cout << "Oringin case 3: ";printList(head3);int val3 = 7;ListNode* newHead3 = solution.removeElements(head3, val3);std::cout << "Case 3: ";printList(newHead3);// 释放节点内存deleteList(newHead1);deleteList(newHead2);deleteList(newHead3);return 0;
}
2.2 虚拟头结点写法
这样就只用考虑处理非头结点的情况就好了, 注意这里实例化了之后要用引用,这里也是复习一下实例化和在堆上生成内存返回指针,下面有两种写法
栈上的内存(Stack):当你创建一个局部变量或对象(例如ListNode dummy(0);)时,这个对象是在栈上分配内存的。这些对象的生命周期是确定的,当它们所在的作用域结束时,它们会自动被销毁,内存会被释放。
堆上的内存(Heap):当你使用new关键字(例如ListNode* dummy = new ListNode(0);)时,对象是在堆上分配内存的。这些对象的生命周期是不确定的,你需要显式地使用delete来释放内存。
使用指针的一个主要优点是,它允许你在运行时动态地创建和销毁对象。这给了你更大的灵活性,但代价是你需要更仔细地管理内存。
实例化虚拟头节点
class Solution {
public:ListNode* removeElements(ListNode* head, int val) {ListNode dummy(0);dummy.next = head;// 处理非头节点ListNode *cur = &dummy; // 这里已经考虑完了头结点, 所以这个头结点不用操作while (cur != NULL && cur->next != NULL){ // 遍历链表的基本手法if (cur->next->val == val){ ListNode *tmp = cur->next;cur->next = cur->next->next;delete tmp;}else {cur = cur->next;}}return dummy.next;}
};
指针虚拟头结点
class Solution {
public:ListNode* removeElements(ListNode* head, int val) {ListNode *dummy = new ListNode(999);// 处理非头节点ListNode *cur = dummy; // 这里已经考虑完了头结点, 所以这个头结点不用操作while (cur != NULL && cur->next != NULL){ // 遍历链表的基本手法if (cur->next->val == val){ ListNode *tmp = cur->next;cur->next = cur->next->next;delete tmp;}else {cur = cur->next;}}return dummy->next;}
};
3. 设计一个链表
通过完整的设计一个链表理解链表是怎么遍历的,新的节点怎么在各个地方链接起来
#include <iostream>class MyLinkedList {
public:struct ListNode // 先创建节点的结构体, val, next, 以及ListNode的构造函数{int val;ListNode *next;ListNode(int x) : val(x), next(nullptr) {}};private:ListNode *head; // 指向头结点的指针int size; // 链表的尺寸public:MyLinkedList() {head = NULL;size = 0;}int get(int index) {// 这个就是取出索引的值if (index < 0 || index >= size)return -1;ListNode *cur = head;for (int i = 0; i < index; i++){cur = cur->next;}return cur->val;}void addAtHead(int val) {/*1 -> 2 -> 3 -> 4 添加valval -> 1 -> 2 -> 3 -> 4 */ListNode *new_node = new ListNode(val); // 定义一个新的节点new_node->next = head; // val链接头结点head = new_node; // 更新头结点size++; }void addAtTail(int val) {/*1 -> 2 -> 3 -> 4 添加val1 -> 2 -> 3 -> 4 -> val*/// 如果刚初始化也可以使用这个接口if (size == 0){addAtHead(val);return;}ListNode *cur = head; // 当前指针位置,从head开始while (cur->next != NULL){ // 这里遍历到最后一个节点cur = cur->next;}ListNode *new_node = new ListNode(val); // 定义指向新的节点的指针cur->next = new_node; // 链接起来size++; }void addAtIndex(int index, int val) {if (index < 0 || index > size)return;if (index == 0) {addAtHead(val);return;}ListNode *cur = head;for (int i = 0; i < index - 1; i++){cur = cur->next;}// 现在来到要加的前一个ListNode *new_node = new ListNode(val);new_node->next = cur->next;cur->next = new_node;size++;}void deleteAtIndex(int index) {if (index < 0 || index >= size) return;ListNode *cur = head;if (index == 0) {head = head->next;delete cur;--size;return;}for (int i = 0; i < index - 1; i++){cur = cur->next;}// 现在来到要删除的前一个ListNode *tmp = cur->next;cur->next = cur->next->next;delete tmp;size--;}
};int main() {MyLinkedList *myLinkedList = new MyLinkedList();myLinkedList->addAtHead(1);myLinkedList->addAtTail(3);myLinkedList->addAtIndex(1, 2);std::cout << myLinkedList->get(1) << std::endl;myLinkedList->deleteAtIndex(1);std::cout << myLinkedList->get(1) << std::endl;delete myLinkedList;return 0;
}
4. 反转链表
4.1 双指针法
class Solution {
public:ListNode* reverseList(ListNode* head) {ListNode *cur = head; // 定义当前位置指针ListNode *prev = NULL; // 定义当前位置指针while (cur != NULL){ListNode *next_node = cur->next;cur->next = prev; // 这里链接起来 prev = cur; // prev上一位cur = next_node; // cur上一位}return prev;}
};
递归的写法,复习一下递归
class Solution {
public:ListNode* reverse(ListNode* prev, ListNode* cur){if (cur == NULL)return prev;ListNode *next_node = cur->next;cur->next = prev;return reverse(cur, next_node);}public:ListNode* reverseList(ListNode* head) {return reverse(NULL, head);}
};
完整的代码
#include <iostream>struct ListNode {int val;ListNode *next;ListNode() : val(0), next(nullptr) {}ListNode(int x) : val(x), next(nullptr) {}ListNode(int x, ListNode *next) : val(x), next(next) {}
};class Solution {
public:ListNode* reverse(ListNode* prev, ListNode* cur){if (cur == NULL)return prev;ListNode *next_node = cur->next;cur->next = prev;return reverse(cur, next_node);}public:ListNode* reverseList(ListNode* head) {return reverse(NULL, head);}
};// 辅助函数:打印链表
void printList(ListNode* head) {ListNode* current = head;while (current != nullptr) {std::cout << current->val << " ";current = current->next;}std::cout << std::endl;
}int main() {Solution solution;// 示例ListNode* head = new ListNode(1);head->next = new ListNode(2);head->next->next = new ListNode(3);head->next->next->next = new ListNode(4);std::cout << "Original List: ";printList(head);ListNode* newHead = solution.reverseList(head);std::cout << "Reversed List: ";printList(newHead);return 0;
}相关文章:
2.单链表练习
1. 链表的基本概念 链表(Linked List)是一种常见的数据结构,用于存储一系列元素,这些元素可以是任意类型的数据。链表中的每个元素被称为节点(Node),每个节点包含两部分:一个存储数…...
Wordpress 安装插件和主题报错
安装主题和插件的时候,就是这个恶心的报错, Wordpress plugin install: Could not create directory 这是权限惹的祸,如下一顿操作猛如虎,就解决了。 sudo chown -R www:www wp-content/themes sudo chown -R www:www wp-conte…...
Spring Cloud 2022.x版本使用gateway和nacos实现动态路由和负载均衡
文章目录 1、nacos下载安装1.1、启动服务器1.2、关闭服务器1.3、服务注册&发现和配置管理接口 2、代码示例2.1、app1工程代码2.2、app2工程代码2.3、gateway网关工程代码 3、动态配置网关路由3.1、配置动态路由3.2、配置为负载模式 4、gateway配置规则4.1、请求转发&#x…...
CSS中如何隐藏元素但保留其占位空间(display:none vs visibility:hidden)?
聚沙成塔每天进步一点点 ⭐ 专栏简介⭐ display: none;⭐ visibility: hidden;⭐ 如何选择⭐ 写在最后 ⭐ 专栏简介 前端入门之旅:探索Web开发的奇妙世界 记得点击上方或者右侧链接订阅本专栏哦 几何带你启航前端之旅 欢迎来到前端入门之旅!这个专栏是为…...
无涯教程-机器学习 - 数据可视化
在上一章中,无涯教程讨论了数据对于机器学习算法的重要性,以了解具有统计信息的数据,还有另一种称为可视化的方式来理解数据。 借助数据可视化,可以看到数据的属性保持什么样的关联,这是查看要素是否与输出相对应的最…...
springboot设置日志输出级别
一、日志等级 trace:最低等级 debug:调试用,通常用于跟踪程序进展 info: 记录用,通常用于记录程序行为 warn:警告 error:错误 fatal:灾难性错误,最高等级 配置application.yml 实现…...
buildAdmin的使用笔记
安装buildAdmin 下载完整包,解压进入 buildadmin 的文件夹, 输入命令 composer install 启动的时候使用, php think run 就可以了 为什么启动只需要, php think run 这种启动方式, 我是头一回看见 ,后来才…...
RealVNC配置自定义分辨率(AlmaLinux 8)
RealVNC 配置自定义分辨率(AlmaLinux8) 参考RealVNC官网 how to set up resolution https://help.realvnc.com/hc/en-us/articles/360016058212-How-do-I-adjust-the-screen-resolution-of-a-virtual-desktop-under-Linux-#standard-dummy-driver-0-2 …...
LA@特征值和特征向量的性质
文章目录 方阵特征值和特征向量的性质👺特征值之和特征值之积推论:特征值判定方阵的可逆性 证明小结 导出性质可逆矩阵的特征值性质转置矩阵和特征值矩阵多项式的特征值不同特征值的特征向量线性无关定理推论推广 特征向量线性组合特征值的重数性质 方阵特征值和特征…...
Springboot使用kafka事务-生产者方
前言 在上一篇文章中,我们使用了springboot的AOP功能实现了kafka的分布式事务,但是那样实现的kafka事务是不完美的,因为请求进来之后分配的是不同线程,但不同线程使用的kafka事务却是同一个,这样会造成多请求情况下的…...
您的计算机已被.halo勒索病毒感染?恢复您的数据的方法在这里!
导言: 在当今数字时代,网络安全已经成为了我们生活和工作中不可或缺的一部分。然而, .Halo 勒索病毒的出现,使网络威胁变得更加真切和具体。本文91数据恢复将深入介绍 .Halo 勒索病毒的危害,详细探讨如何高效地恢复被其…...
生成式AI颠覆传统数据库的十种方式
对于生成式AI的所有闪光点,这个新时代最大的转变可能深埋在软件堆栈中。AI算法正在不易觉察地改变一个又一个数据库。他们正在用复杂、自适应且看似更直观的AI新功能颠覆传统数据库。 与此同时,数据库制造商正在改变我们存储信息的方式,以便…...
el-date-picker自定义只能选中当前月份和半年内月份等
需求:el-date-picker只能选中当前月期和当前月期往前半年,其他时间就禁用了不让选择了,因为没数据哈哈。当然也可以选择往前一年等。 一、效果 二、写个日期选择器 :picker-options:日期选项 value-format:选择后的格…...
Pyecharts教程(十一):使用Pyecharts绘制带有滑动数据缩放功能的K线图
Pyecharts教程(十一):使用Pyecharts绘制带有滑动数据缩放功能的K线图 作者:安静到无声 个人主页 目录 Pyecharts教程(十一):使用Pyecharts绘制带有滑动数据缩放功能的K线图前言步骤总结推荐专栏前言 K线图是金融市场分析中常见的图表类型之一,它能够直观地展示价格的变化…...
2023年高教社杯数学建模思路 - 案例:ID3-决策树分类算法
文章目录 0 赛题思路1 算法介绍2 FP树表示法3 构建FP树4 实现代码 建模资料 0 赛题思路 (赛题出来以后第一时间在CSDN分享) https://blog.csdn.net/dc_sinor?typeblog 1 算法介绍 FP-Tree算法全称是FrequentPattern Tree算法,就是频繁模…...
POJ 3273 Monthly Expense 二分
我们对每个月花费的最小花费进行二分,对于每一次二分的值mid,计算能花的月份数量,如果月份数量小于等于m,我们就不断的缩小mid,直到找到月份数量小于等于m 与 月份数量大于m的临界值,取最后一次满足条件的m…...
图论(基础)
知识: 顶点,边 | 权,度数 1.图的种类: 有向图 | 无向图 有环 | 无环 联通性 基础1:图的存储(主要是邻接矩阵和邻接表) 例一:B3643 图的存储 - 洛谷 | 计算机科学教育新生态 (…...
docker的运行原理
Docker 是一个开源的容器化技术,它能够让开发者将应用及其依赖打包到一个轻量级的、可移植的容器中,这个容器可以在几乎任何机器上一致地运行。要了解 Docker 的运行原理,我们首先要理解以下几个核心概念: 容器 (Container): 容器是一个轻量级的、独立的、可执行的软件包,…...
vue自定义键盘
<template><div class"mark" click"isOver"></div><div class"mycar"><div class"mycar_list"><div class"mycar_list_con"><p class"mycar_list_p">车牌号</p>…...
k8s 安装 kubernetes安装教程 虚拟机安装k8s centos7安装k8s kuberadmin安装k8s k8s工具安装 k8s安装前配置参数
k8s采用master, node1, node2 。三台虚拟机安装的一主两从,机器已提前安装好docker。下面是机器配置,k8s安装过程,以及出现的问题与解决方法 虚拟机全部采用静态ip, master 30机器, node1 31机器, node2 32机器 机器ip 192.168.164.30 # ma…...
学校招生小程序源码介绍
基于ThinkPHPFastAdminUniApp开发的学校招生小程序源码,专为学校招生场景量身打造,功能实用且操作便捷。 从技术架构来看,ThinkPHP提供稳定可靠的后台服务,FastAdmin加速开发流程,UniApp则保障小程序在多端有良好的兼…...
华为OD机试-食堂供餐-二分法
import java.util.Arrays; import java.util.Scanner;public class DemoTest3 {public static void main(String[] args) {Scanner in new Scanner(System.in);// 注意 hasNext 和 hasNextLine 的区别while (in.hasNextLine()) { // 注意 while 处理多个 caseint a in.nextIn…...
Spring Boot面试题精选汇总
🤟致敬读者 🟩感谢阅读🟦笑口常开🟪生日快乐⬛早点睡觉 📘博主相关 🟧博主信息🟨博客首页🟫专栏推荐🟥活动信息 文章目录 Spring Boot面试题精选汇总⚙️ **一、核心概…...
uniapp微信小程序视频实时流+pc端预览方案
方案类型技术实现是否免费优点缺点适用场景延迟范围开发复杂度WebSocket图片帧定时拍照Base64传输✅ 完全免费无需服务器 纯前端实现高延迟高流量 帧率极低个人demo测试 超低频监控500ms-2s⭐⭐RTMP推流TRTC/即构SDK推流❌ 付费方案 (部分有免费额度&#x…...
OpenPrompt 和直接对提示词的嵌入向量进行训练有什么区别
OpenPrompt 和直接对提示词的嵌入向量进行训练有什么区别 直接训练提示词嵌入向量的核心区别 您提到的代码: prompt_embedding = initial_embedding.clone().requires_grad_(True) optimizer = torch.optim.Adam([prompt_embedding...
智能AI电话机器人系统的识别能力现状与发展水平
一、引言 随着人工智能技术的飞速发展,AI电话机器人系统已经从简单的自动应答工具演变为具备复杂交互能力的智能助手。这类系统结合了语音识别、自然语言处理、情感计算和机器学习等多项前沿技术,在客户服务、营销推广、信息查询等领域发挥着越来越重要…...
JavaScript基础-API 和 Web API
在学习JavaScript的过程中,理解API(应用程序接口)和Web API的概念及其应用是非常重要的。这些工具极大地扩展了JavaScript的功能,使得开发者能够创建出功能丰富、交互性强的Web应用程序。本文将深入探讨JavaScript中的API与Web AP…...
苹果AI眼镜:从“工具”到“社交姿态”的范式革命——重新定义AI交互入口的未来机会
在2025年的AI硬件浪潮中,苹果AI眼镜(Apple Glasses)正在引发一场关于“人机交互形态”的深度思考。它并非简单地替代AirPods或Apple Watch,而是开辟了一个全新的、日常可接受的AI入口。其核心价值不在于功能的堆叠,而在于如何通过形态设计打破社交壁垒,成为用户“全天佩戴…...
《Docker》架构
文章目录 架构模式单机架构应用数据分离架构应用服务器集群架构读写分离/主从分离架构冷热分离架构垂直分库架构微服务架构容器编排架构什么是容器,docker,镜像,k8s 架构模式 单机架构 单机架构其实就是应用服务器和单机服务器都部署在同一…...
LangChain 中的文档加载器(Loader)与文本切分器(Splitter)详解《二》
🧠 LangChain 中 TextSplitter 的使用详解:从基础到进阶(附代码) 一、前言 在处理大规模文本数据时,特别是在构建知识库或进行大模型训练与推理时,文本切分(Text Splitting) 是一个…...
