探索数据结构之精髓:单链表解密

news2024/10/6 1:47:14

文章目录

  • 1. 前言
  • 2. 单链表的特点
  • 3. 单链表的基础操作
    • 🍑 接口总览
    • 🍑 初始化操作
    • 🍑 插入操作
      • 🍅 优化操作
    • 🍑 删除操作
      • 🍅 优化操作
    • 🍑 获取元素
      • 🍅 按置查找
      • 🍅 按值查找
    • 🍑 打印操作
    • 🍑 获取长度
    • 🍑 判空操作
    • 🍑 翻转链表
    • 🍑 释放操作
  • 4. 总结


1. 前言

上一篇文章说过顺序表(线性表的顺序存储)的最大缺点是,在插入和删除操作可能会移动大量元素,去保证元素之间的内存不能有空隙,而这,会导致程序的执行效率变低。

那么如何弥补这个缺点呢?这就涉及到了今天的内容:采用线性表的链式存储来保存数据元素。

线性表的链式存储也非常基础和常用,它不需要使用连续的内存空间。从名字可以得知,所谓链式存储,是通过“链(指针)”建立元素之间的关系,保证元素之间像一条线一样按顺序排列。这样,在插入和删除元素的时候,就不需要为了保证内存空间的连续性,去进行数据元素的大量迁移,只需要修改指向元素的指针即可。

用链式存储实现的线性表叫做链表,链表比顺序表稍复杂一些。它可以具体分为单链表、双链表、循环链表、静态链表这四种。今天先讲解单链表。

2. 单链表的特点

顺序表与单链表保存数据元素的区别如下:

在这里插入图片描述

可以看到,左侧顺序表中存储的元素在内存中紧密相连。其中,每个存储数据元素的内存空间被称为一个节点。

而右侧单链表中存储的元素在内存中并不需要紧密相连。在单链表中,每个节点不但用于存放一个数据元素(数据域),还要额外存放一个用于指向后继节点的指针也称后继指针(指针域),最后一个节点的指针域指向 nullptr。

单链表数据存储描述图如下:

在这里插入图片描述

在书写单链表相关代码时,有时为了更方便更统一的对链表进行操作,会在单链表的第一个节点之前再增设一个和其他节点类型相同的节点,称为头结点(也称哨兵节点)。

头结点的数据域可以不存储任何信息,也可以存储比如单链表长度等额外信息。头结点的指针域指向第一个节点。注意,头结点始终位于任何其他节点之前,就算我们需要向链表的首部插入节点,那插入的节点也要位于头结点之后。

带头结点的单链表数据存储描述图如下:

在这里插入图片描述

那不带头结点的单链表,和带头结点的单链表有什么不同呢?我们来对比一下。

初始化时:不带头结点的单链表(有时也称不带头链表)在初始化时不创建任何节点,而带头结点的单链表(有时也称带头链表)在初始化时要把头结点创建出来(可以把该头结点看成是第 0 个节点)。

实际元素数据的位置:带头结点的单链表中的头节点不存放实际元素数据。头节点之后的下一个节点才开始存放数据。

代码操作:不带头结点的单链表在编写基本操作代码(比如插入、删除等)时更繁琐,往往需要对第一个或者最后一个数据节点进行单独的处理。

在书写单链表的基本操作代码时,多数情况下,我们都会使用带头结点的代码实现方式。

3. 单链表的基础操作

了解整体框架之后,下面我们就来看一看单链表的具体实现代码,包括基本框架、插入、删除、获取以及其它的一些常用操作。

🍑 接口总览

单链表是由一个个的节点组成,所以,我们首先要定义出单个节点。

//单链表中每个节点的定义
template<class T> //T代表数据元素的类型
struct Node
{
	T _data;    //数据域,存放数据元素
	Node<T>* _next;  //指针域,指向下一个同类型(和本节点类型相同)节点
};

然后把单链表相关的类的基本框架实现出来。

//单链表的定义
template<class T>
class LinkList
{
public:
    //构造函数
    LinkList();      

    //析构函数
    ~LinkList();     
public:
    //在第i个位置插入指定元素e
    bool ListInsert(int i, const T& e);  

    //优化插入操作
    bool ListInsert2(int i, const T& e);
    void InsertNode(Node<T>* cur, const T& e);

    //删除第i个位置的元素
    bool ListDelete(int i);  

    //优化删除操作
    bool ListDelete2(int i);
    void DeleteNode(Node<T>* pdel);

    //获得第i个位置的元素值
    bool GetElem(int i, T& e);      

    //按元素值查找其在单链表中第一次出现的位置
    int  LocateElem(const T& e);         

    //输出单链表中的所有元素
    void DispList();    

    //获取单链表的长度
    int  ListLength();    

    //判断单链表是否为空
    bool Empty();    

    //翻转单链表
    void ReverseList();                  

private:
    Node<T>* _head; //头指针(指向链表第一个节点的指针, 如果链表有头结点则指向头结点)
    int _length; //单链表当前长度(当前有几个元素)
};

🍑 初始化操作

通过构造函数对单链表进行初始化

代码如下:

//通过构造函数对单链表进行初始化
template <class T>
LinkList<T>::LinkList()
{
    _head = new Node<T>; //先创建一个头结点
    _head->_next = nullptr;
    _length = 0;  //头结点不计入单链表的长度
}

在上面的 LinkList 类模板的构造函数中,通过 new 创建了一个头结点。

在 main 主函数中,加入如下代码创建一个单链表对象。

LinkList<int> sl;

🍑 插入操作

如果我们想在单链表的第 i 个位置插入指定的元素(也可以称为插入指定的节点),那么只需要找到单链表中的第 i-1 个节点并将新节点插入该节点之后即可。

这里要注意,单链表中的位置编号从 1 开始,对于带头节点的单链表,我们不计算这个头节点的。

单看上面这段话有些绕,我们看一下把元素 a5 插入到单链表第 2 个位置前后对比图:

在这里插入图片描述

代码如下:

template<class T>
bool LinkList<T>::ListInsert(int i, const T& e)
{
    //判断插入位置i是否合法,i的合法值应该是1到length+1之间
    if (i < 1 || i >(_length + 1)) {
        cout << "元素" << e << "插入的位置" << i << "不合法,合法的位置是1到" << _length + 1 << "之间" << endl;
        return false; //插入失败
    }

    Node<T>* cur = _head;

    //整个for循环用于找到第i-1个节点
    for (int j = 0; j < (i - 1); ++j) //j从0开始,表示cur刚开始指向的是第0个节点(也就是头结点)
    { 
        cur = cur->_next;
    }

    //当循环结束以后, cur指向插入位置的前一个位置
    Node<T>* node = new Node<T>; //new一个新节点
    node->_data = e;
    node->_next = cur->_next; //让新节点链上后续链表,因为cur->_next指向后续的链表节点 
    cur->_next = node; //让当前位置链上新节点,因为node指向新节点

    cout << "成功在位置为" << i << "处插入元素" << e << endl;  
    _length++; //实际表长+1  

    return true; //插入成功
}

上面的代码中,新节点的创建以及修改新老节点的指向,那么这些行对应的节点指向示意图要怎么画呢?

其实很简单,如下图所示:

在这里插入图片描述

我们可以给出一组数据测试一下:

在这里插入图片描述

🍅 优化操作

我们分析一下插入操作的时间复杂度。这里只需要关注 for 循环的执行次数与问题规模 n 的关系,问题规模 n 在这里指的是单链表当前长度 _length。

  • 如果将元素插入到单链表的开头(位置 1),则 for 循环一次都不会执行,这是最好情况时间复杂度 O(1)。
  • 如果将元素插入到单链表的末尾,并且假设单链表中已经有其他元素(非空),则 for 循环会循环 n-1 次,这是最坏情况时间复杂度 O(n)。
  • 平均情况时间复杂度其实在顺序表中已经做过很详细的分析,这里很类似,平均情况时间复杂度为 O(n),时间开销主要源于插入位置的寻找。

另外,在实际的应用中,往往也会涉及到向某个已知节点之前插入一个新节点的情况。传统的做法是必须要利用头指针 _head 从前向后找到该已知节点的前趋节点。

在上面的图中,要将 a5 插入到 a2 之前必须要先从前向后找到 a1 节点,算法的平均情况时间复杂度为 O(n)。

那有没有什么更好的方法呢?

  • 将新节点 a5 插入到 a2 节点之后(a2 节点是已知的无需查找)。
  • 将 a2 和 a5 两个节点的数据域中的元素值互换。

如下图所示:

在这里插入图片描述

最终也可以达到将 a5 插入到 a2 之前的效果,而且这样修改后的算法时间复杂度为 O(1)。

代码如下:

//在节点cur之前插入新节点,新节点数据域元素值为e
template<typename T>
void LinkList<T>::InsertNode(Node<T>* cur, const T& e)
{
    Node<T>* newNode = new Node<T>; //new一个新节点
    newNode->_next = cur->_next; //让新节点链上cur后面的链表
    cur->_next = newNode; //让cur链上新节点

    T tmp = cur->_data; //先保存cur节点中的数据
    cur->_data = e; //再把cur节点中的数据修改为e
    newNode->_data = tmp; //最后再把新节点中的数据修改为cur事先保存好的数据
    
    _length++; //实际表长+1  
}

//查找插入位置的节点
template<class T>
bool LinkList<T>::ListInsert2(int i, const T& e)
{
    //判断插入位置i是否合法,i的合法值应该是1到length+1之间
    if (i < 1 || i >(_length + 1)) {
        cout << "元素" << e << "插入的位置" << i << "不合法,合法的位置是1到" << _length + 1 << "之间" << endl;
        return false; //插入失败
    }

    Node<T>* cur = _head;

    //整个for循环用于找到第i个节点
    for (int j = 0; j < i; ++j) //j从0开始,表示cur刚开始指向的是第0个节点(也就是头结点)
    {
        cur = cur->_next;
    }

    //当循环结束以后, cur指向插入位置
    InsertNode(cur, e);
    return true; //插入成功
}

我们可以给出一组数据测试一下:

在这里插入图片描述

注意,上面这种方法必须提前知道插入位置节点的地址,所以重新写了一个 Insert 函数用来查找 i 位置的节点。

最后,如果需要频繁的向单链表的末尾插入新节点,从算法执行所耗费时间的角度去看,每次用 for 循环从前向后寻找插入位置的做法并不好。

我们可以考虑引入一个表尾指针,这个指针在单链表为空时,会指向头结点,在单链表非空时,要注意始终保持其指向最后一个节点。

这样,通过表尾指针在单链表的末尾插入新节点就会变得非常容易了。

🍑 删除操作

关于删除操作,如果想删除单链表的第 i 个位置的元素,那只需要找到单链表中的第 i-1 个节点,并将其指针域指向第 i+1 个节点,同时释放第 i 个节点所占的内存,就可以了。

代码如下:

//删除第i个位置的元素
template<class T>
bool LinkList<T>::ListDelete(int i)
{
    if (_length < 1)
    {
        cout << "当前单链表为空,不能删除任何数据!" << endl;
        return false;
    }
    if (i < 1 || i > _length)
    {
        cout << "删除的位置" << i << "不合法,合法的位置是1到" << _length << "之间" << endl;
        return false;
    }

    Node<T>* cur = _head; //cur指向头节点

    //整个for循环用于找到第i-1个节点
    for (int j = 0; j < (i - 1); ++j) //j从0开始,表示cur刚开始指向的是第0个节点(头结点)
    {
        cur = cur->_next; //cur会找到当前要删除的位置所代表的节点的前一个节点的位置,比如要删除第2个位置的节点,cur会指向第1个位置(节点)
    }

    Node<T>* delNode = cur->_next; //delNode指向待删除的节点
    cur->_next = delNode->_next; //先让第i-1个节点的next指针指向第i+1个节点
    cout << "成功删除位置为" << i << "的元素,该元素的值为" << delNode->_data << endl;
    _length--;  //删除节点以后,让实际表长-1
    delete delNode; //释放节点
    return true; //删除成功
}

我们可以给出一组数据测试一下:

在这里插入图片描述

🍅 优化操作

我们分析一下删除操作的时间复杂度。

  • 如果删除单链表开头位置的节点,那么 for 循环一次都不会执行,这是最好情况时间复杂度 O(1)。
  • 如果删除单链表末尾位置的节点,并且假设单链表中已经有其他元素(非空),则 for 循环会循环 n-1 次,这是最坏情况时间复杂度 O(n)。
  • 平均情况时间复杂度在顺序表中也做过很详细的分析,这里很类似,平均情况时间复杂度为 O(n),时间开销主要源于删除位置的寻找。

在实际的应用中,往往我们也会涉及到删除某个指定节点的情况。传统的做法是必须要利用头指针 _head 从前向后找到这个被删除节点的前趋节点。比如要将 a2 删除,就要先从前向后找到 a1 节点,算法的平均情况时间复杂度为 O(n)。

想一想,怎么才能优化这个操作呢?

  • 将 a2 后继节点 a3 中数据拷贝到 a2 节点的数据域中。
  • 将 a2 节点的指针域指向 a3 的后继节点 a4。
  • 释放 a3 节点所占用的内存。

我们看一下这个流程的思路:

在这里插入图片描述

这样修改后的算法时间复杂度就是 O(1) 了。

代码如下:

//删除cur所指向的节点
template<class T>
void LinkList<T>::DeleteNode(Node<T>* cur)
{
    //比如:a1 -> a2 -> a3 -> a4, 现在要删除a2节点
    Node<T>* pNode = cur->_next;
    cur->_data = cur->_next->_data; //将a2后继节点a3中数据拷贝到a2节点的数据域中
    cur->_next = cur->_next->_next; //让a2节点的指针域指向a3的后继节点a4
    delete pNode; //释放a3节点所占用的内存。
    _length--; //长度-1
}

//删除第i个位置的元素
template<class T>
bool LinkList<T>::ListDelete2(int i)
{
    if (_length < 1)
    {
        cout << "当前单链表为空,不能删除任何数据" << endl;
        return false;
    }
    if (i < 1 || i > _length)
    {
        cout << "删除的位置" << i << "不合法,合法的位置是1到" << _length << "之间" << endl;
        return false;
    }

    Node<T>* cur = _head; //cur指向头节点

    //整个for循环用于找到第i个节点
    for (int j = 0; j < i; ++j) //j从0开始,表示cur刚开始指向的是第0个节点(头结点)
    {
        cur = cur->_next; 
    }

    //循环结束, cur找到当前要删除的位置所代表的节点
    DeleteNode(cur);
    
    return true; //删除成功
}

我们可以给出一组数据测试一下:

在这里插入图片描述

注意,上面这种方法必须提前知道删除位置节点的地址,所以重新写了一个 Delete 函数用来查找删除 i 位置的节点地址。

但要注意,如果要删除的节点正好是单链表的最后一个节点,那就没法用上述快捷高效的方式来编写代码了(代码会报错)。我们还是必须用传统做法,利用头指针找到该将被删除节点的前趋节点,来删除某个指定节点。

🍑 获取元素

在元素获取操作这里,通常分为两种情况:按位置获取和按元素值获取。

🍅 按置查找

首先是按位置获取单链表中的元素值。

代码如下:

//获得第i个位置的元素值
template<class T>
bool LinkList<T>::GetElem(int i, T& e)
{
    if (_length < 1)
    {
        cout << "当前单链表为空,不能获取任何数据" << endl;
        return false;
    }

    if (i < 1 || i > _length)
    {
        cout << "获取元素的位置" << i << "不合法,合法的位置是1到" << _length << "之间" << endl;
        return false;
    }

    Node<T>* cur = _head;
    for (int j = 0; j < i; ++j)
    {
        cur = cur->_next;
    }
    e = cur->_data;
    cout << "成功获取位置为" << i << "的元素,该元素的值为" << e << endl;
    return true;
}

我们可以给出一组数据测试一下:

在这里插入图片描述

显然,按位置获取单链表元素操作的平均情况时间复杂度为 O(n)。

🍅 按值查找

按元素值查找其在单链表中第一次出现的位置

代码如下:

//按元素值查找其在单链表中第一次出现的位置
template<class T>
int LinkList<T>::LocateElem(const T& e)
{
    Node<T>* cur = _head; //从头节点的下一个开始依次查找
    for (int i = 1; i <= _length; ++i)
    {
        if (cur->_next->_data == e)
        {
            cout << "值为" << e << "的元素在单链表中第一次出现的位置为" << i << endl;
            return i;
        }
        cur = cur->_next;
    }
    cout << "值为" << e << "的元素在单链表中没有找到" << endl;
    return -1;  //返回-1表示查找失败
}

我们可以给出一组数据测试一下:

在这里插入图片描述

按元素值查找其在单链表中第一次出现位置操作的平均情况时间复杂度,依旧为 O(n)。

🍑 打印操作

输出单链表中的所有元素

代码如下:

//输出单链表中的所有元素,时间复杂度为O(n)
template<class T>
void LinkList<T>::DispList()
{
    Node<T>* cur = _head->_next;
    while (cur != nullptr) //循环遍历整个链表, 直到为空
    {
        cout << cur->_data << " ";
        cur = cur->_next;
    }
    cout << endl;
}

我们可以给出一组数据测试一下:

在这里插入图片描述

🍑 获取长度

获取单链表的长度

代码如下:

//获取单链表的长度,时间复杂度为O(1)
template<class T>
int LinkList<T>::ListLength()
{
    return _length; //直接返回长度即可
}

我们可以给出一组数据测试一下:

在这里插入图片描述

🍑 判空操作

判断单链表是否为空

代码如下:

//判断单链表是否为空,时间复杂度为O(1)
template<class T>
bool LinkList<T>::Empty()
{
    if (_head->_next == nullptr) //单链表为空(如果是不带头结点的单链表则用if(_head == nullptr)来判断是否为空)
    {
        return true;
    }
    return false;
}

我们可以给出一组数据测试一下:

在这里插入图片描述

🍑 翻转链表

所谓翻转单链表,就是把单链表中节点的排列顺序反过来。

比如原来节点的排列顺序为 a1、a2、a3、a4,那么翻转后节点的排列顺序就是 a4、a3、a2、a1。

这里要注意的是,并不是针对节点数据域中的数据进行翻转,而是针对整个节点进行翻转(比如原来位于单链表尾部的节点经过翻转后排到了单链表的头部)。

先来思考一下解决思路。

  • 把头节点和第一个节点分到一起作为第一部分。
  • 把剩余的节点分成一部分。
  • 每次从剩余的节点中的最前面拿出一个节点插入到第一部分单链表的首部。

下图展示了翻转单链表的步骤,先将头结点和 a1 分到一起作为第一部分,将 a2、a3、a4 分到一起作为第二部分,然后摘取第二部分的首部节点 a2 插到第一部分的 a1 之前,再摘取第二部分的首部节点 a3 插入到第一部分的 a2 之前……最终就可以实现整个单链表的翻转。

在这里插入图片描述

代码如下:

//翻转单链表,时间复杂度为O(n)
template<class T>
void LinkList<T>::ReverseList()
{
    if (_length <= 1)
    {
        //如果顺序表中没有元素或者只有一个元素,那么就不用做任何操作
        return;
    }

    //至少有两个节点才会走到这里
    Node<T>* latterCur = _head->_next->_next;  //指向从第二个节点开始的后续节点
    _head->_next->_next = nullptr;  //第一个节点的指针域先置空

    Node<T>* ptmp;
    while (latterCur != nullptr)
    {
        //比如a1、a2、a3、a4共4个节点,第一次执行该循环时的指向看下面代码中的注释
        ptmp = latterCur;  //ptmp代表a2
        latterCur = latterCur->_next; //现在latterCur指向a3

        ptmp->_next = _head->_next;  //a2指向a1
        _head->_next = ptmp; //头结点指向a2 
    }
}

我们可以给出一组数据测试一下:

在这里插入图片描述

🍑 释放操作

最后,说一下对单链表的释放操作,我们不但要释放单链表中带有数据的节点,也要释放头结点。

析构函数代码如下:

//通过析构函数对单链表进行资源释放,时间复杂度为O(n)
template<class T>
LinkList<T>::~LinkList()
{
    Node<T>* curNode = _head->_next;
    Node<T>* ptmp;
    while (curNode != nullptr) //该循环负责释放数据节点
    {
        ptmp = curNode;
        curNode = curNode->_next;

        delete ptmp;
    }
    delete _head; //释放头结点
    _head = nullptr; //把头节点置为空
    _length = 0; //链表长度置为0
}

4. 总结

单链表特点如下:

  • 并不需要大片的连续存储空间来存放数据元素,扩容很方便。
  • 插入和删除节点非常方便,平均情况时间复杂度为 O(n)。当然,如果不考虑需要预先查找到插入和删除位置只单纯考虑插入和删除动作本身,那么时间复杂度仅为 O(1)。不管怎么说,与数组相比,链表更适合插入、删除操作频繁的场景。
  • 存放后继指针要额外消耗存储空间,体现了利用空间换时间来提高算法效率的编程思想。但对于内存紧张的硬件设备,就要考虑单链表是否适合使用了。
  • 因为内存空间不连续,无法实现随机访问链表中的元素。要查找某个位置节点中的元素只能从链表的第一个节点开始沿着指针链逐个元素找下去,平均情况时间复杂度为 O(n)。

单链表的操作代码相比于数组更加复杂,实现也更加容易出错,因此写代码时除了要有清晰的逻辑思维之外,写完以后对代码进行测试也是非常重要和必要的——尤其是对边界情况的测试。在这里给出一些代码书写和测试的建议。

  • 单链表是后面学习的其他链表的基础,因此应该通过多画图的方式理清代码逻辑,边看图边写自己认为正确的逻辑代码。
  • 当链表为空的时候,测试代码能否正常工作。
  • 当链表只有一个数据节点时,测试代码能否正常工作。
  • 分别测试在处理链表中第一个和最后一个节点时代码能否正常工作。
  • 发现程序执行异常并百思不得其解时,通过设置断点对代码进行调试,逐行跟踪并观察代码的执行情况就是必须的解决问题的手段。

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

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

相关文章

从C出发 20 --- 函数参数深度剖析

我们在编写这个函数的时候参数 n 的值具体是多少&#xff1f; 在编写一个函数的时候参数值是没法确定的&#xff0c;所以将 (int n) 这个参数命名为形参 那么这个参数的值什么时候指定&#xff0c;具体函数调用的时候指定 比如 在main 里面调用 实参用来初始化形参 初始化之…

安装多个版本的Node.js的方法

要在同一台计算机上安装多个版本的Node.js&#xff0c;可以使用以下几种方法&#xff1a; 使用nvm&#xff08;Node Version Manager&#xff09;&#xff1a;nvm是一个用于管理多个Active Node.js版本的工具。您可以使用nvm轻松地在系统中安装、卸载和切换不同版本的Node.js。…

Leetcode每日一题——“消失的数字”

各位CSDN的uu们你们好呀&#xff0c;今天&#xff0c;小雅兰又开新专栏了&#xff0c;以后会在Leetcode上面进行刷题&#xff0c;尽量每天写一道&#xff0c;请大家监督我&#xff01;&#xff01;&#xff01;好啦&#xff0c;让我们进入Leetcode的世界吧 力扣 对于这道题目&a…

南卡OE Pro上线!开放式耳机新里程碑!前所未有的音质舒适双冠

当前耳机市场放眼望去几乎都是TWS的天下&#xff0c;但是大多数的蓝牙耳机都存在着以下痛点&#xff1a;长时间佩戴涨耳、出现听诊器效应、耳朵内部发痒以及与外界声音隔开缺乏安全性等等问题。 为此&#xff0c;国内智能声学品牌厂商NANK南卡针对用户的特点&#xff0c;于近日…

中小企业如何实施知识管理策略?

随着信息化建设的深入&#xff0c;IT不仅成为企业运营的基础平台&#xff0c;而且在ERP、CRM、OA等信息系统内沉淀了大量的知识&#xff0c;成为企业创新的源泉&#xff0c;大中企业信息化建设中知识管理越来越重要。 应该如何实现知识管理呢&#xff1f; 知识管理本身就属于企…

Flutter系列(七)ListView 图文列表详解

完整工程&#xff1a; Flutter系列&#xff08;四&#xff09;底部导航顶部导航图文列表完整代码_摸金青年v的博客-CSDN博客 详细解读: Flutter系列&#xff08;五&#xff09;底部导航详解_摸金青年v的博客-CSDN博客 Flutter系列&#xff08;六&#xff09;顶部导航详解_摸金青…

初学SSM时做的-IKUN图书管理系统

项目介绍 项目工具:IntelliJ IDEA 2021.2.2 图书后台管理系统&#xff0c;采用SpringBootMybatiusThymeleaf&#xff0c;页面使用Element框架&#xff0c;使用RESTful API风格编写接口。 数据库使用mysql 已实现功能 基本增删改查,联表查询 拦截器登录验证 项目技术栈 Spr…

4.15--设计模式之创建型之责任链模式(总复习版本)---脚踏实地,一步一个脚印

一、什么是责任链模式&#xff1a; 责任链模式属于行为型模式&#xff0c;是为请求创建了一个接收者对象的链&#xff0c;将链中每一个节点看作是一个对象&#xff0c;每个节点处理的请求均不同&#xff0c;且内部自动维护一个下一节点对象。 当一个请求从链式的首端发出时&a…

C++(Qt)软件调试---qmake编译优化和生成调试信息(9)

C(Qt)软件调试—qmake编译优化和生成调试信息&#xff08;9&#xff09; 文章目录C(Qt)软件调试---qmake编译优化和生成调试信息&#xff08;9&#xff09;1、前言1.1 编译器优化是什么1.2 调试信息是什么1.3 测试环境2、Qt编译模式说明3、比较Linux下Qt三种编译模式1.1 编译生…

DevOps系列文章 - K8S知识体系

环境搭建部分&#xff1a; 1、安装前的准备工作 # 关闭防火墙 systemctl stop firewalld systemctl disable firewalld# 查看hostname并修改hostname # 查看本机hostname hostnamectl set-hostname k8s-master # 把本机名设置成k8s-master hostnamectl status # 查看修改结…

Linux复习 / 文件系统QA梳理

文章目录前言Q&A文件的基本理解Q&#xff1a;谈谈你是怎么理解文件的&#xff1f;Q&#xff1a;什么是当前工作路径&#xff1f;Q&#xff1a;文件与操作系统有着怎样的关系&#xff1f;Q&#xff1a;为什么语言要封装系统接口&#xff1f;文件描述符Q&#xff1a;系统和进…

基于JSP的网上购物系统的设计与实现(论文+源码)_kaic

摘 要 近些年来&#xff0c;社会的生产力和科技水平在不断提高&#xff0c;互联网技术也在不断更新升级&#xff0c;网络在人们的日常生活中扮演着一个重要角色&#xff0c;它极大地方便了人们的生活。为了让人们实现不用出门就能逛街购物&#xff0c;网络购物逐渐兴起慢慢变得…

ES6(声名、解构)

参考文档&#xff1a; ES6 入门教程https://es6.ruanyifeng.com/ 注意&#xff1a;内容较多&#xff1a;只看引用部分的内容即可&#xff08;代码一眼过搭配理解&#xff09; 一、变量声明 1. let let声名的变量只在所处于的块级有效 let a 10; if (true) {let b 20;cons…

人工智能 AI 绘画发展史

到了去年4月, 著名人工智能团队OpenAI 也发布了新模型 DALLE 2代&#xff0c;该名称来源于著名画家达利&#xff08;Dal&#xff09;和机器人总动员&#xff08;Wall-E&#xff09;, 同样支持从文本描述生成效果良好的图像. 而很多读者对AI绘画开始产生特别的关注, 或许是从以下…

有趣的Hack-A-Sat黑掉卫星挑战赛——AES加密通信链路侧信道攻击leaky

国家太空安全是国家安全在空间领域的表现。随着太空技术在政治、经济、军事、文化等各个领域的应用不断增加&#xff0c;太空已经成为国家赖以生存与发展的命脉之一&#xff0c;凝聚着巨大的国家利益&#xff0c;太空安全的重要性日益凸显[1]。而在信息化时代&#xff0c;太空安…

如何将Springboot项目通过IDEA打包成jar包,并且转换成可执行文件

首先在IDEA打开你的项目&#xff0c;需要确认项目可以正常运行&#xff0c;然后点击页面右侧的Maven,运行Lifecycle下的package, 此时在项目的target目录下就可以看到一个jar包 这个时候你可以在jar包所在目录下执行cmd窗口&#xff0c;运行 java -jar campus-market-0.0.1-S…

BUUCTF-SimpleRev

下载文件 查壳 没有加壳 并且是64 放入ida64 SHIFTF12 访问字符串 得到关键字符串 双击 然后 CRTLX 查找交互 F5 反编译 得到了代码 开始代码审计 我们可以发现有两个十六进制的东西 r 对其转换为字符串 srcSLCDN v9wodah 然后继续往下看 发现textjoin函数 我们进入看…

STM32+AT24C02实现易变参数存储

AT24C02是一个2K位串行CMOS E2PROM&#xff0c; 内部含有256个8位字节存储单元&#xff0c;该器件通过IIC总线接口进行操作&#xff0c; AT24C02把存储空间分为 32 页&#xff0c;每页可存储8个字节的数据&#xff0c;具有硬件数据写保护功能&#xff0c;100万次擦写&#xff0…

Docker理论总结

目录 容器技术的演变 物理机时代 虚拟化时代 容器化时代 容器化技术的应用场景 Docker介绍与基本概念 标准化的应用打包 Docker的发展 Docker是容器化平台 Docker体系架构 容器与镜像 Docker执行流程 容器内部 Tomcat容器内部结构 在容器中执行命令 容器生命周…

( “树” 之 DFS) 104. 二叉树的最大深度 ——【Leetcode每日一题】

104. 二叉树的最大深度 给定一个二叉树&#xff0c;找出其最大深度。 二叉树的深度为根节点到最远叶子节点的最长路径上的节点数。 说明: 叶子节点是指没有子节点的节点。 示例&#xff1a; 给定二叉树 [3,9,20,null,null,15,7]&#xff0c; 返回它的最大深度 3 。 思路&am…