pytorch学习------实现手写数字识别

news2024/12/26 23:46:23

目标

  1. 知道如何使用Pytorch完成神经网络的构建
  2. 知道Pytorch中激活函数的使用方法
  3. 知道Pytorch中torchvision.transforms中常见图形处理函数的使用
  4. 知道如何训练模型和如何评估模型

一、思路和流程分析

流程:

  1. 准备数据,这些需要准备DataLoader
  2. 构建模型,这里可以使用torch构造一个深层的神经网络
  3. 模型的训练
  4. 模型的保存,保存模型,后续持续使用
  5. 模型的评估,使用测试集,观察模型的好坏

二、准备训练集和测试集

本次使用的数据集是pytorch自带的图像数据集MNIST

from torchvision.datasets import MNIST #自带数据集

2.1、图形数据处理方法

2.1.1、torchvision.transforms.ToTensor

把一个取值范围是[0,255]PIL.Image或者shape(H,W,C)numpy.ndarray,转换成形状为[C,H,W]

其中(H,W,C)意思为(高,宽,通道数),黑白图片的通道数只有1,其中每个像素点的取值为[0,255],彩色图片的通道数为(R,G,B),每个通道的每个像素点的取值为[0,255],三个通道的颜色相互叠加,形成了各种颜色

示例如下:

from torchvision import transforms
import numpy as np

data = np.random.randint(0, 255, size=12)
img = data.reshape(2,2,3)
print(img.shape)
img_tensor = transforms.ToTensor()(img) # 转换成tensor
print(img_tensor)
print(img_tensor.shape)

输出如下:

shape:(2, 2, 3)
img_tensor:tensor([[[215, 171],
                 [ 34,  12]],

                [[229,  87],
                 [ 15, 237]],

                [[ 10,  55],
                 [ 72, 204]]], dtype=torch.int32)
new shape:torch.Size([3, 2, 2])

注意:

transforms.ToTensor对象中有__call__方法,所以可以对其示例能够传入数据获取结果

2.1.2、torchvision.transforms.Normalize(mean, std)

给定均值mean,shape和图片的通道数相同(指的是每个通道的均值),方差:std,和图片的通道数相同(指的是每个通道的方差),将会把Tensor规范化处理。

即:Normalized_image=(image-mean)/std
示例如下:

from torchvision import transforms
import numpy as np
import torchvision

data = np.random.randint(0, 255, size=12)
img = data.reshape(2,2,3)
img = transforms.ToTensor()(img) # 转换成tensor
print(img)
print("*"*100)

norm_img = transforms.Normalize((10,10,10), (1,1,1))(img) #进行规范化处理

print(norm_img)

输出如下:

tensor([[[177, 223],
         [ 71, 182]],

        [[153, 120],
         [173,  33]],

        [[162, 233],
         [194,  73]]], dtype=torch.int32)
***************************************************************************************
tensor([[[167, 213],
         [ 61, 172]],

        [[143, 110],
         [163,  23]],

        [[152, 223],
         [184,  63]]], dtype=torch.int32)

注意:在sklearn中,默认上式中的std和mean为数据每列的std和mean,sklearn会在标准化之前算出每一列的std和mean。

但是在api:Normalize中并没有帮我们计算,所以我们需要手动计算

  1. 当mean为全部数据的均值,std为全部数据的std的时候,才是进行了标准化。

  2. 如果mean(x)不是全部数据的mean的时候,std(y)也不是的时候,Normalize后的数据分布满足下面的关系
    n e w _ m e a n = m e a n − x y , m e a n 为原数据的均值, x 为传入的均值 x n e w _ s t d = s t d y , y 为传入的标准差 y \begin{align*} &new\_mean = \frac{mean-x}{y}&, mean为原数据的均值,x为传入的均值x \\ &new\_std = \frac{std}{y} &,y为传入的标准差y\\ \end{align*} new_mean=ymeanxnew_std=ystdmean为原数据的均值,x为传入的均值xy为传入的标准差y

2.1.3、torchvision.transforms.Compose(transforms)

将多个transform组合起来使用。

例如

transforms.Compose([
     torchvision.transforms.ToTensor(), #先转化为Tensor
     torchvision.transforms.Normalize(mean,std) #在进行正则化
 ])

2.2、准备MNIST数据集的Dataset和DataLoader

准备训练集和测试集

from torchvision.datasets import MNIST #自带数据集

def get_dataloader(train=True,batch_size=BATCH_SIZE):   #train为True加载训练数据,False加载测试数据
    #数据集标准化
    transform_fn = Compose([
        ToTensor(),
        Normalize(mean=(0.1307,),std=(0.3081,))  #mean和std的形状和通道数相同
    ])
    #加载数据集
    dataset = MNIST(root="./data",train=train,download=True,transform=transform_fn)
    #使用数据加载器
    data_loader = DataLoader(dataset=dataset,batch_size=batch_size,shuffle=True)

    return data_loader

train==True为训练数据集,train==False为测试数据集

三、构建模型

全连接层:当前一层的神经元和前一层的神经元相互链接,其核心操作就是 y = w x ​ y = wx​ y=wx,即矩阵的乘法,实现对前一层的数据的变换。

模型的构建使用了一个三层的神经网络,其中包括两个全连接层和一个输出层,第一个全连接层会经过激活函数的处理,将处理后的结果交给下一个全连接层,进行变换后输出结果

那么在这个模型中有两个地方需要注意:

  1. 激活函数如何使用
  2. 每一层数据的形状
  3. 模型的损失函数

3.1、激活函数的使用

常用的激活函数为Relu激活函数,他的使用非常简单

Relu激活函数由import torch.nn.functional as F提供,F.relu(x)即可对x进行处理

例如:

In [30]: b
Out[30]: tensor([-2, -1,  0,  1,  2])

In [31]: import torch.nn.functional as F

In [32]: F.relu(b)
Out[32]: tensor([0, 0, 0, 1, 2])

在这里插入图片描述

3.2、模型中数据的形状

  1. 原始输入数据为的形状:[batch_size,1,28,28]
  2. 进行形状的修改:[batch_size,28*28] ,(全连接层是在进行矩阵的乘法操作)
  3. 第一个全连接层的输出形状:[batch_size,28],这里的28是个人设定的,你也可以设置为别的
  4. 激活函数不会修改数据的形状
  5. 第二个全连接层的输出形状:[batch_size,10],因为手写数字有10个类别

构建模型的代码如下:

import torch
from torch import nn
import torch.nn.functional as F

class MnistNet(nn.Module):
    def __init__(self):
        super(MnistNet,self).__init__()
        self.fc1 = nn.Linear(28*28*1,28)  #定义Linear的输入和输出的形状
        self.fc2 = nn.Linear(28,10)  #定义Linear的输入和输出的形状

    def forward(self,x):
        x = x.view(-1,28*28*1)  #对数据形状变形,-1表示该位置根据后面的形状自动调整
        x = self.fc1(x) #[batch_size,28]
        x = F.relu(x)  #[batch_size,28]
        x = self.fc2(x) #[batch_size,10]
  

可以发现:pytorch在构建模型的时候形状上并不会考虑batch_size

3.3、模型的损失函数

首先,我们需要明确,当前我们手写字体识别的问题是一个多分类的问题,所谓多分类对比的是之前学习的2分类

我们在逻辑回归中,我们使用sigmoid进行计算对数似然损失,来定义我们的2分类的损失。

  • 在2分类中我们有正类和负类,正类的概率为 P ( x ) = 1 1 + e − x = e x 1 + e x P(x) = \frac{1}{1+e^{-x}} = \frac{e^x}{1+e^x} P(x)=1+ex1=1+exex,那么负类的概率为 1 − P ( x ) 1-P(x) 1P(x)

  • 将这个结果进行计算对数似然损失 − ∑ y l o g ( P ( x ) ) -\sum y log(P(x)) ylog(P(x))就可以得到最终的损失

那么在多分类的过程中我们应该怎么做呢?

  • 多分类和2分类中唯一的区别是我们不能够再使用sigmoid函数来计算当前样本属于某个类别的概率,而应该使用softmax函数。

  • softmax和sigmoid的区别在于我们需要去计算样本属于每个类别的概率,需要计算多次,而sigmoid只需要计算一次

softmax的公式如下:
σ ( z ) j = e z j ∑ k = 1 K e z K , j = 1 ⋯ k \sigma(z)_j = \frac{e^{z_j}}{\sum^K_{k=1}e^{z_K}} ,j=1 \cdots k σ(z)j=k=1KezKezj,j=1k

例如下图:
在这里插入图片描述

在这里插入图片描述
假如softmax之前的输出结果是2.3, 4.1, 5.6,那么经过softmax之后的结果是多少呢?

Y 1 = e 2.3 e 2.3 + e 4.1 + e 5.6 Y 2 = e 4.1 e 2.3 + e 4.1 + e 5.6 Y 3 = e 5.6 e 2.3 + e 4.1 + e 5.6 Y1 = \frac{e^{2.3}}{e^{2.3}+e^{4.1}+e^{5.6}} \\Y2 = \frac{e^{4.1}}{e^{2.3}+e^{4.1}+e^{5.6}} \\Y3 = \frac{e^{5.6}}{e^{2.3}+e^{4.1}+e^{5.6}} \\ Y1=e2.3+e4.1+e5.6e2.3Y2=e2.3+e4.1+e5.6e4.1Y3=e2.3+e4.1+e5.6e5.6

对于这个softmax输出的结果,是在[0,1]区间,我们可以把它当做概率

和前面2分类的损失一样,多分类的损失只需要再把这个结果进行对数似然损失的计算即可

即:
J = − ∑ Y l o g ( P ) , 其中 P = e z j ∑ k = 1 K e z K , Y 表示真实值 \begin{align*} & J = -\sum Y log(P) &, 其中 P = \frac{e^{z_j}}{\sum^K_{k=1}e^{z_K}} ,Y表示真实值 \end{align*} J=Ylog(P),其中P=k=1KezKezj,Y表示真实值
最后,会计算每个样本的损失,即上式的平均值

我们把softmax概率传入对数似然损失得到的损失函数称为交叉熵损失

在pytorch中有两种方法实现交叉熵损失

  1. criterion = nn.CrossEntropyLoss()
    loss = criterion(input,target)
    
  2. #1. 对输出值计算softmax和取对数
    output = F.log_softmax(x,dim=-1)
    #2. 使用torch中带权损失
    loss = F.nll_loss(output,target)
    

带权损失定义为: l n = − ∑ w i x i l_n = -\sum w_{i} x_{i} ln=wixi,其实就是把 l o g ( P ) log(P) log(P)作为 x i x_i xi,把真实值Y作为权重

四、模型的训练

训练的流程:

  1. 实例化模型,设置模型为训练模式
  2. 实例化优化器类,实例化损失函数
  3. 获取,遍历dataloader
  4. 梯度置为0
  5. 进行向前计算
  6. 计算损失
  7. 反向传播
  8. 更新参数

代码如下:

#实现训练过程
#实例化模型
model = MnistModel()
# 实例化优化器
optimizer = Adam(model.parameters(),lr=0.001)
#加载以前保存的模型(如果有的话)
if os.path.exists("./model/model.pkl"):
    model.load_state_dict(torch.load("./model/model.pkl"))
    #加载以前保存的优化器
    optimizer.load_state_dict(torch.load("./model/optimizer.pkl"))

def train(epoch):
    # 获取训练数据加载器
    data_loader = get_dataloader()
    # 训练
    for idx,(input,target) in enumerate(data_loader):
        # 梯度置零,防止累加
        optimizer.zero_grad()
        # 计算损失
        output = model(input)
        loss = F.nll_loss(output,target)
        # 反向传播
        loss.backward()
        # 梯度更新
        optimizer.step()

        #每训练100次就保存模型,并输出结果
        if idx%100 == 0:
            print('Train Epoch: {} [{}/{} ({:.0f}%)]\tLoss: {:.6f}'.format(
                epoch, idx * len(input), len(data_loader.dataset),
                       100. * idx / len(data_loader), loss.item()))
            #保存模型
            torch.save(model.state_dict(), "./model/model.pkl")
            torch.save(optimizer.state_dict(), "./model/optimizer.pkl")

五、模型的保存和加载

5.1 模型的保存

 torch.save(model.state_dict(), "./model/model.pkl") #保存模型参数
 torch.save(optimizer.state_dict(), "./model/optimizer.pkl") #保存优化器参数

5.2 模型的加载

 model.load_state_dict(torch.load("./model/model.pkl"))
 optimizer.load_state_dict(torch.load("./model/optimizer.pkl"))

六、模型的评估

评估的过程和训练的过程相似,但是:

  1. 不需要计算梯度
  2. 需要收集损失和准确率,用来计算平均损失和平均准确率
  3. 损失的计算和训练时候损失的计算方法相同
  4. 准确率的计算:
    • 模型的输出为[batch_size,10]的形状
    • 其中最大值的位置就是其预测的目标值(预测值进行过sotfmax后为概率,sotfmax中分母都是相同的,分子越大,概率越大)
    • 最大值的位置获取的方法可以使用torch.max,返回最大值和最大值的位置
    • 返回最大值的位置后,和真实值([batch_size])进行对比,相同表示预测成功

代码如下:

def test():
    loss_list = []
    acc_list = []
    test_dataloader = get_dataloader(False,TEST_BATCH_SIZE)
    for idx,(input,target) in enumerate(test_dataloader):
         with torch.no_grad(): #不需要计算梯度
            output = model(input)
            cur_loss = F.nll_loss(output,target)
            loss_list.append(cur_loss)
            #计算准确率
            pred = output.max(dim=-1)[-1]#获取最大值的位置,[batch_size,1]
            cur_acc = pred.eq(target).float().mean()
            acc_list.append(cur_acc)
    print("平均准确率,平均损失",np.mean(acc_list),np.mean(loss_list))

七、完整代码

#!/usr/bin/env python 
# -*- coding:utf-8 -*-
import torch
from torchvision.datasets import MNIST #自带数据集
from torchvision.transforms import Compose,ToTensor,Normalize
from torch.utils.data import DataLoader
import torch.nn as nn
import torch.nn.functional as F  #激活函数
from torch.optim import Adam
import  os
import numpy as np

BATCH_SIZE = 128
TEST_BATCH_SIZE = 1000

#1、使用数据集
#我们使用pytorch中自带的数据集
def get_dataloader(train=True,batch_size=BATCH_SIZE):   #train为True加载训练数据,False加载测试数据
    #数据集标准化
    transform_fn = Compose([
        ToTensor(),
        Normalize(mean=(0.1307,),std=(0.3081,))  #mean和std的形状和通道数相同
    ])
    #加载数据集
    dataset = MNIST(root="./data",train=train,download=True,transform=transform_fn)
    #使用数据加载器
    data_loader = DataLoader(dataset=dataset,batch_size=batch_size,shuffle=True)

    return data_loader

#2、构建模型
class MnistModel(nn.Module):
    def __init__(self):
        super(MnistModel, self).__init__()
        self.fc1 = nn.Linear(1*28*28,28)
        self.fc2 = nn.Linear(28,10)   #手写数字为09一共10个数字,所有输出大小为10

    def forward(self,input):
        """
        :param input:[batch_size,1,28,28]
        :return:
        """
        #1、修改形状
        x = input.view([input.size(0),1*28*28])
        #input = view([-1,1*28*28])
        #2、进行全连接操作
        x = self.fc1(x)
        #3、进行激活函数的处理,形状没有变化
        x = F.relu(x)
        #4、输出层
        out = self.fc2(x)

        return F.log_softmax(out,dim=-1)

#实现训练过程
model = MnistModel()
# 实例化优化器
optimizer = Adam(model.parameters(),lr=0.001)
#加载以前保存的模型(如果有的话)
if os.path.exists("./model/model.pkl"):
    model.load_state_dict(torch.load("./model/model.pkl"))
    #加载以前保存的优化器
    optimizer.load_state_dict(torch.load("./model/optimizer.pkl"))

def train(epoch):
    # 获取训练数据加载器
    data_loader = get_dataloader()
    # 训练
    for idx,(input,target) in enumerate(data_loader):
        # 梯度置零,防止累加
        optimizer.zero_grad()
        # 计算损失
        output = model(input)
        loss = F.nll_loss(output,target)
        # 反向传播
        loss.backward()
        # 梯度更新
        optimizer.step()

        #每训练100次就保存模型,并输出结果
        if idx%100 == 0:
            print('Train Epoch: {} [{}/{} ({:.0f}%)]\tLoss: {:.6f}'.format(
                epoch, idx * len(input), len(data_loader.dataset),
                       100. * idx / len(data_loader), loss.item()))
            #保存模型
            torch.save(model.state_dict(), "./model/model.pkl")
            torch.save(optimizer.state_dict(), './model/optimizer.pkl')

def test():
    loss_list = []
    acc_list = []
    test_dataloader = get_dataloader(False,TEST_BATCH_SIZE)
    for idx,(input,target) in enumerate(test_dataloader):
         with torch.no_grad(): #不需要计算梯度
            output = model(input)
            cur_loss = F.nll_loss(output,target)
            loss_list.append(cur_loss)
            #计算准确率
            pred = output.max(dim=-1)[-1]#获取最大值的位置,[batch_size,1]
            cur_acc = pred.eq(target).float().mean()
            acc_list.append(cur_acc)
    print("平均准确率,平均损失",np.mean(acc_list),np.mean(loss_list))


if __name__ == '__main__':
    #训练
    for i in range(3):
        train(i)
    #评估
    # test()

八、运行效果

8.1、训练效果

在这里插入图片描述

8.2、评估效果

在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1036192.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

NodeRed拖拉拽实现OPCUA数据订阅,发布至MQTT并落库MySQL

背景 几年前曾根据 Node-Red 官网示例进行了简单的体验,当时觉得这东西就是个玩具,拿过来玩一玩可以,不实用;但是如今发现有不少产品对其进行了集成,并做出了复杂的商业应用,这确实是极大的震撼。 使用看似…

Vulnhub系列靶机---JANGOW 1.0.1

文章目录 网卡配置信息收集主机发现端口扫描 漏洞利用反弹Shell提权 靶机文档:JANGOW 1.0.1 下载地址:Download (Mirror) 难易程度:. 网卡配置 水果味儿 信息收集 主机发现 端口扫描 访问80端口 点击site目录 点击页面上方的一个选项&…

【跟小嘉学习区块链】一、区块链基础知识与关键技术解析

系列文章目录 【跟小嘉学习区块链】一、区块链基础知识与关键技术解析 文章目录 系列文章目录[TOC](文章目录) 前言一、区块链基础1.1、区块链的来龙去脉1.1.1、区块链技术起源1.1.2、核心技术发展脉络 1.2、比特币产生的背景与现状1.2.1、现行货币体系存在的问题1.2.2、数字货…

HTTP代理与VPN:网络代理技术的比较

HTTP代理和VPN是两种常见的网络代理技术,它们可以帮助用户隐藏自己的IP地址、保护网络隐私、绕过网络限制等。本文将介绍HTTP代理和VPN的定义、工作原理、优缺点以及使用场景。 一、HTTP代理 HTTP代理是一种通过代理服务器转发网络请求的技术。当用户发起网络请求时…

c语言进阶部分详解(经典回调函数qsort()详解及模拟实现)

大家好!上篇文章(c语言进阶部分详解(指针进阶2)_总之就是非常唔姆的博客-CSDN博客)我已经对回调函数进行了初步的讲解和一个简单的使用事例,鉴于篇幅有限没有进行更加详细的解释,今天便来补上。…

echarts的Y轴设置为整数

场景:使用echarts,设置Y轴为整数。通过判断Y轴的数值为整数才显示即可 yAxis: [{name: ,type: value,min: 0, // 最小值// max: 200, // 最大值// splitNumber: 5, // 坐标轴的分割段数// interval: 100 / 5, // 强制设置坐标轴分割间隔度(取本Y轴的最大…

【算法与数据结构】JavaScript实现十大排序算法(二)

文章目录 关于排序算法快速排序堆排序计数排序桶排序基数排序 关于排序算法 稳定排序: 在排序过程中具有相同键值的元素,在排序之后仍然保持相对的原始顺序。意思就是说,现在有两个元素a和b,a排在b的前面,且ab&#xf…

Windows使用JEnv实现JDK多版本管理

Windows使用JEnv实现JDK多版本管理 JEnv安装Jenv命令 JEnv安装 JEnv是一个帮助我们管理多个JDK安装的工具,并将每个代码库配置为使用特定的JDK版本,而不必改变JAVA_HOME环境变量. 下载链接 windows版 windows版地址JEnvGithub地址: JEnv Jenv命令 添…

如何查看电脑详细配置、型号?这4个技巧 yyds!

知道自己电脑的配置和型号,可以更合理合适的去安装软件,避免出现电脑系统和软件不兼容问题。 了解详细配置信息可以检测一下电脑组件是否是二手的。 从解决实际问题的角度,推荐这4个技巧: 1、右键“此电脑” 2、设备管理器查看…

AJAX的奇妙之旅(1)基础知识

一、简介 AJAX(Asynchronous JavaScript and XML)是一种使用现有标准的新方法。它是一种用于创建快速动态网页的技术。AJAX 最大的优点是在不重新加载整个页面的情况下,可以与服务器交换数据并更新部分网页内容。AJAX 不需要任何浏览器插件&a…

【操作系统笔记一】程序运行机制CPU指令集

内存地址 指针 / 引用 指针、引用本质上就是内存地址,有了内存地址就可以操作对应的内存数据了。 不同的数据类型 字节序 大端序(Big Endian):字节顺序从低地址到高地址顺序存储的字节序小端序(Little Endian&#…

stm32之智能垃圾桶实战

之前用过51做过一个垃圾桶的小项目,这里用32重新搞了一下。视频的效果和之前一样,可参考这个垃圾桶效果 。 一、项目描述(同51) 项目主要是模拟不用手动打开垃圾桶盖,而进行自动操作。自动打开的条件如下&#xff1a…

基于微信小程序的学生选课系统设计与实现(源码+lw+部署文档+讲解等)

文章目录 前言学生小程序端的主要功能有:教师的主要功能有:管理员的主要功能有:具体实现截图论文参考详细视频演示为什么选择我自己的网站自己的小程序(小蔡coding)有保障的售后福利 代码参考源码获取 前言 &#x1f4…

PHP自动识别采集何意网址文章正文内容

在做PHP采集内容时,用过querylist采集组件,但是这个插件采集页面内容时,都必须要写个采集选择器。这样比较麻烦,每个文章页面都必须指定一条采集规则 。就开始着手找一个插件可以能自动识别任意文章url正文内容并采集的&#xff0…

完整答题小程序源码/支持流量主/激励广告强点(答题小程序模板+题库)

源码简介: 完整答题小程序源码/支持流量主/激励广告强点(答题小程序模板题库),完整答题小程序源码有题库,无加密,带激励广告强制点击可提升广告收益。是积分激励的小程序,作为答题小程序开发,是很实用的操…

分库分表MySQL

目录 Mycat入门 分片配置 分片配置(配置Mycat的用户以及用户的权限) 启动服务 登录Mycat Mycat配置 schema.xml 1.schema标签:配置逻辑库,逻辑表的相关信息 1-1.核心属性 1-2.table标签 2.datanode标签:配置数据节点的相关信息 核心属性 3.datahost标签:配置的是节…

机器学习笔记 - 维度诅咒的数学表达

1、点之间的距离 kNN分类器假设相似的点也可能有相同的标签。但是,在高维空间中,从概率分布中得出的点往往不会始终靠近在一起。 我们可以用一个简单的例子来说明这一点。 我们将在单位立方体内均匀地随机绘制点(如图所示),并研究该立方体内测试点的 k 个最近邻将占用多少…

全套配置细节:缺省路由实验配置

1、实验目的 掌握默认路由的适用场合和配置方法 2、实验拓扑 默认路由的配置 3、实验步骤 &#xff08;1&#xff09;配置网络连通性如下。 1&#xff09;R1 的配置如下 &#xff1a; <Huawei>system-view Enter system view, return user view with CtrlZ. [Huaw…

MySQL学习笔记6

MySQL数据库如何存放数据&#xff1f; 注明&#xff1a;我们平常说的MySQL&#xff0c;其实主要指的是MySQL数据库管理软件。 一个MySQL DBMS可以 同时存放多个数据库&#xff0c;理论上一个项目就对应一个数据库。 如博客项目blog数据库&#xff0c;商城项目shop数据库&#…

【剑指Offer】23.链表中环的入口结点

题目 给一个长度为n链表&#xff0c;若其中包含环&#xff0c;请找出该链表的环的入口结点&#xff0c;否则&#xff0c;返回null。 数据范围&#xff1a; n ≤ 10000&#xff0c;1 < 结点值 < 10000 要求&#xff1a;空间复杂度 O(1)&#xff0c;时间复杂度O(n) 例如…