【面试分享】嵌入式面试题常考难点之关于单链表的增删改查

文章目录

  • 【面试分享】嵌入式面试题常考难点之关于单链表的增删改查
  • 一、单链表结点定义
  • 二、增(Create)——插入结点
    • 1. 于链表头部插入结点(头插法)
    • 2. 于链表尾部插入结点(尾插法)
    • 3. 于链表中间插入结点
      • 3-1. 在指定结点前插入结点(前插法)
      • 3-2.在指定结点后插入结点(后插法)
  • 三、删(Delete)——删除结点
    • 1. 根据结点内容删除结点
    • 2. 根据位置删除结点
    • 3. 删除指针指向的结点(经典面试题)
  • 四、改(Update)和查(Read)————修改结点与查找结点

在这里插入图片描述

【面试分享】嵌入式面试题常考难点之关于单链表的增删改查

在众多经典数据结构中,单链表以其简单灵活的特性,成为嵌入式面试题库中的常客,尤其是在考察增删改查(CRUD)操作时。本文旨在深入剖析单链表在面试场景中常考的难点,通过解析这些基本操作的实现细节与优化策略,帮助读者掌握应对相关面试题的技巧,同时提升对链表这一基础数据结构的深刻理解。

单链表,作为一种线性数据结构,其特点在于每个结点包含两部分:存储数据的元素和指向下一个结点的指针。这一结构特性使得单链表在插入、删除等操作上相比数组展现出更高的效率,但也给查找等操作带来了一定挑战。正因如此,面试官倾向于通过单链表的增删改查来评估候选人对指针操作的熟练度、逻辑思维能力以及对时间与空间复杂度的敏感度。

在这里插入图片描述

接下来,我们将依次探讨单链表增(Create)删(Delete)改(Update)查(Read) 操作的核心逻辑、常见陷阱及优化思路,力求为即将步入面试场的开发者们提供一份详实的备考指南。无论是追求极致性能的算法爱好者,还是希望在面试中脱颖而出的求职者,都能从本文中获得宝贵的知识与启发。


一、单链表结点定义

为了方便介绍,本文将使用以下结构体创建链表的结点

typedef struct node {int nodeId;char nodeData[20];struct node *next;
} NODE;extern NODE *head;

并用如下链表初始化函数创建一条初始链表:

NODE *initList(NODE *pHead)
{NODE *temp = NULL;for (int i = MAX_NODE_NUM; i > 0; i--) {temp = (NODE *)malloc(sizeof(NODE));if (temp == NULL) {printf("Memory allocation failed!\n");exit(0);} else {temp->nodeId = i;temp->next = pHead;pHead = temp;sprintf(temp->nodeData, "<Node_%d>", temp->nodeId);}}return pHead;
}

[!NOTE]

上述代码中的 MAX_NODE_NUM 宏定义为 4,也就是初始链表的长度为 4 个结点。

打印链表 ID 的函数:

void printList(NODE *pHead)
{while (pHead != NULL) {printf("%d -> ", pHead->nodeId);pHead = pHead->next;}printf("NULL\n");
}

打印链表 ID 及信息的函数:

void printListData(NODE *pHead)
{while (pHead != NULL) {printf(" %d: %s\n |\n", pHead->nodeId, pHead->nodeData);pHead = pHead->next;}   printf(" NULL\n");
}

main 函数简单测试一下:

int main()
{head = initList(head);printList(head);putchar('\n');printListData(head);return 0;
}

执行结果如下:

1 -> 2 -> 3 -> 4 -> NULL1: <Node_1>|2: <Node_2>|3: <Node_3>|4: <Node_4>|NULL

后续创建链表的新结点,使用以下函数:

NODE *createNewNode()
{NODE *temp = (NODE *)malloc(sizeof(NODE));if (temp == NULL) {printf("Memory allocation failed!\n");exit(0);} else {printf("Enter the Node Id: ");scanf("%d", &temp->nodeId);temp->next = NULL;sprintf(temp->nodeData, "<New_Node_%d>", temp->nodeId);}return temp;
}

[!NOTE]

结点 ID 需要用户手动输入。

二、增(Create)——插入结点

1. 于链表头部插入结点(头插法)

链表的头插法写法也是多种多样,以下是两种常见写法:

  1. 在执行头插法时,创建新结点后插入新结点,并返回新的链表头指针:

    NODE *insertAtHead(NODE *head)
    {NODE *newNode = createNewNode();newNode->next = head;head = newNode;return head;
    }
    
  2. 已经创建了新结点,执行头插法时添加进链表,并返回新的链表头指针:

    NODE *insertAtHead(NODE *pHead, NODE *newNode)
    {newNode->next = head;pHead = newNode;return pHead;
    }
    

其实不管怎么变化,核心只有最后几句代码:

  1. 首先是 newNode->next = head,让新结点的 next 指向链表的头部,接入链表;

    在这里插入图片描述

  2. 然后是 pHead = newNode,让链表头指针指向新结点;
    在这里插入图片描述

  3. 最后返回头指针。

2. 于链表尾部插入结点(尾插法)

链表的为插法跟头插法一样,也是有两种常见写法:

  1. 在执行尾插法时,创建新结点后,先判断链表是否存在,如果存在就添加进链表,否则以该结点为链表头部创建链表,并返回链表头指针:

    NODE *insertAtTail(NODE *pHead)
    {NODE *newNode = createNewNode();NODE *temp = pHead;if (temp == NULL) {pHead = newNode;newNode->next = NULL;} else {while (temp->next != NULL)temp = temp->next;temp->next = newNode;newNode->next = NULL;}return pHead;
    }
    
  2. 已经创建了新结点,执行尾插法时,先判断链表是否存在,如果存在就添加进链表,否则以该结点为链表头部创建链表,并返回链表头指针:

    NODE *insertAtTail(NODE *pHead, NODE *newNode)
    {NODE *temp = pHead;if (temp == NULL) {pHead = newNode;newNode->next = NULL;} else {while (temp->next != NULL)temp = temp->next;temp->next = newNode;newNode->next = NULL;}return pHead;
    }
    

两中尾插法的方式是一样的,当链表存在时,尾插法的插入过程就是通过 while (temp->next != NULL) temp = temp->next; 遍历链表,判断当前结点是否为链表最后一个结点。

在这里插入图片描述

一旦找到链表的最后一个结点,就让该结点的 next 指针指向插入链表的新结点。

在这里插入图片描述

[!NOTE]

为什么头插法不需要判断链表是否存在,而尾插法需要?

头插法和尾插法在插入结点时的处理方式不同,在头插法中,始终是在链表的头部插入一个新结点。这种方法不需要考虑链表是否为空,因为新的头结点将始终指向当前的头结点,即使链表为空(headNULL),这也是有效的。

在尾插法中,需要遍历链表找到最后一个结点,然后在其后插入一个新结点。如果链表为空,就需要特别处理,因为此时链表没有结点(或者说链表不存在),不存在所谓的尾结点。遍历一个不存在的链表,是一种指针的非法访问,会导致段错误

3. 于链表中间插入结点

3-1. 在指定结点前插入结点(前插法)

所谓前插法,就是在链表中找到指定结点,并在该结点前插入新结点。例如,当前链表为 0 -> 1 -> 2 -> 3 -> 4 -> NULL,现在有个新结点 100 要求插入,并指定在结点 3 前插入,插入后链表为 0 -> 1 -> 2 -> 100 -> 3 -> 4 -> NULL

前插法在编码时需要考虑到一些特殊情况:

  1. 链表没有结点(链表不存在)

    这时有两种处理方式,由开发者决定使用哪一种。一是直接返回错误代码,告知功能使用者,链表为空,无法插入新结点。二是以当前新结点为链表头,创建链表,写法参考头插法。

  2. 指定结点为链表头结点

    跟第一种情况第二点一样的处理处理方式差不多,也是头插法的处理方式。

  3. 链表存在,但结点不存在

    如果遍历完这个链表都没找到指定的结点,就可能是参数传递错误,也可能是其他原因,总之这种情况无法插入结点。可以通过输出 Log 的方式提示功能使用者,并作出相应的处理动作。

  4. 单链表不可反向回退

    单链表结点的特性就决定了链表只能单个方向遍历,所以在遍历结点的时候,应该通过临时指针 temp 指向的结点的 next 去找目标结点。如若不然,只是用临时指针 temp 搜索目标结点,就会出现找到目标结点也无法插入的情况,如下图所示:

    在这里插入图片描述

结合以上四种情况,前插法的代码如下所示:

  1. 参数列表中不含新结点,由前插法函数申请新结点:

    NODE *insertBefore(NODE *pHead, int nodeId)
    {NODE *newNode = createNewNode();NODE *temp = pHead;if (temp == NULL) {pHead = newNode;newNode->next = NULL;} else if (temp->nodeId == nodeId) {newNode->next = pHead;pHead = newNode;} else {while (temp->next != NULL && temp->next->nodeId != nodeId)temp = temp->next;if (temp->next == NULL) {printf("Node not found!\n");free(newNode);} else {newNode->next = temp->next;temp->next = newNode;}}return pHead;
    }
    
  2. 参数列表中含新结点指针(常用):

    NODE *insertBefore(NODE *pHead, NODE *newNode, int nodeId)
    {NODE *temp = pHead;if (temp == NULL) {pHead = newNode;newNode->next = NULL;} else if (temp->nodeId == nodeId) {newNode->next = pHead;pHead = newNode;} else {while (temp->next != NULL && temp->next->nodeId != nodeId)temp = temp->next;if (temp->next == NULL) {printf("Node not found!\n");} else {newNode->next = temp->next;temp->next = newNode;}}return pHead;
    }
    

两个代码核心部分是一样的,代码解析如下:

  1. 通过 if (temp == NULL) 判断链表是否存在,如果不存在,新结点 newNode 则作为链表头;
  2. 如果链表已经存在,则通过 else if (temp->nodeId == nodeId) ,判断第一个节点是不是目标节点,如果是,则以头插法的方式,把新结点插在目标结点前,新结点成为新的链表头;
  3. 如果以上两个判断都不是,则通过 while (temp->next != NULL && temp->next->nodeId != nodeId) 遍历链表,直到找到目标结点或者链表遍历结束;
  4. 如果目标结点未找到,则输出 Log。在此处两个代码有一点区别,如果新结点是由前插法内部生成的,要注意把无法插入的新结点释放掉(执行 free(newNode);),如果是传参传进来的新结点,则不需要释放;
  5. 如果找到目标结点,则通过 newNode->next = temp->next;,把新结点挂在链表上。再通过 temp->next = newNode;,把当前结点的 next 指针指向新结点,完成新结点的插入。

以下是前插法执行的动画过程:

在这里插入图片描述

3-2.在指定结点后插入结点(后插法)

后插法相对于前插法要简单一些,只需要找到目标结点并在其后面插入新结点即可,其处理过程有点类似于尾插法。例如,当前链表为 0 -> 1 -> 2 -> 3 -> 4 -> NULL,现在有个新结点 100 要求插入,并指定在结点 2 前插入,插入后链表为 0 -> 1 -> 2 -> 100 -> 3 -> 4 -> NULL

后插法在编码时也由一些需要注意的情况,不过与前面提到的前插法差不多,这里就不赘述了。

后插法的代码如下所示:

  1. 参数列表中不含新结点,由后插法函数申请新结点:

    NODE *insertAfter(NODE *pHead, int nodeId)
    {NODE *newNode = createNewNode();NODE *temp = pHead;if (temp == NULL) {pHead = newNode;newNode->next = NULL;} else {while (temp->next != NULL && temp->nodeId != nodeId)temp = temp->next;if (temp->nodeId != nodeId) {printf("Node not found!\n");free(newNode);} else {newNode->next = temp->next;temp->next = newNode;}}return pHead;
    }
    
  2. 参数列表中含新结点指针(常用):

    NODE *insertAfter(NODE *pHead, NODE *newNode, int nodeId)
    {NODE *temp = pHead;if (temp == NULL) {pHead = newNode;newNode->next = NULL;} else {while (temp->next != NULL && temp->nodeId != nodeId)temp = temp->next;if (temp->nodeId != nodeId) {printf("Node not found!\n");} else {newNode->next = temp->next;temp->next = newNode;}}return pHead;
    }
    

两个代码核心部分是一样的,代码解析如下:

  1. 与前插法相同,通过 if (temp == NULL) 判断链表是否存在,如果不存在,新结点 newNode 则作为链表头;
  2. 如果链表存在,则通过 while (temp->next != NULL && temp->nodeId != nodeId) 遍历链表,直到找到目标结点或者链表遍历结束;
  3. 通过 if (temp->nodeId != nodeId) 判断 while 循环退出的具体原因,如果遍历完链表,目标结点未找到,则输出 Log。在此处两个代码也是有区别,原理跟前插法一样,不赘述。
  4. 如果是找到目标结点,提前结束了 while 循环,则通过 newNode->next = temp->next;,把新结点挂在链表上。再通过 temp->next = newNode;,把当前结点的 next 指针指向新结点,完成新结点的插入。

以下是后插法执行的动画过程:

在这里插入图片描述

三、删(Delete)——删除结点

1. 根据结点内容删除结点

一般链表的结点都有一个所谓的唯一标识符,例如本文使用的结点中的 nodeId,这样可以通过这个唯一标识符找到对应的结点(类似 Python 中的键值对,nodeId 的效果相当于键值对中的 key)。前面使用前插法和后插法都是使用了这个 nodeId 索引到对应的目标结点的。

那么根据结点内容来删除结点,也成了最常见的删除结点的办法,通常这里的结点内容指的就是唯一标识符。在完成这个编码的时候也是需要注意以下两点:

  1. 判断链表是否存在:如果不存在,有可能是链表已经被删完了,或者传参时没传入正确的参数,此时应该立即返回,并提示用户;
  2. 临时指针指向被删结点的上一个结点:即将被删除的结点在被剔除链表之前,它的上一个结点的 next 要先指向被删除的结点的下一个结点,因为单链表不可逆,因此在临时指针应当指在被删结点的上一个结点上,这样才有利于删除的操作。

结合以上两点,根据 nodeId 删除结点的方法如下:

NODE *deleteNodeByNodeId(NODE *pHead, int nodeId)
{NODE *temp = pHead;if (temp == NULL) {printf("List is empty!\n");} else if (temp->nodeId == nodeId) {pHead = temp->next;free(temp);} else {while (temp->next != NULL && temp->next->nodeId != nodeId)temp = temp->next;if (temp->next == NULL) {printf("Node not found!\n");} else {NODE *delNode = temp->next;temp->next = delNode->next;free(delNode);}}return pHead;
}

代码解析如下:

  1. 首先用临时指针 temp 代替链表头指针,通过 if (temp == NULL) 判断链表是否存在;
  2. 如果链表存在,结合结点 ID,通过 else if (temp->nodeId == nodeId) 判断需要删除的结点是否是链表的头结点。如果是,则将头指针后移到下个结点,再将头节点释放;
  3. 如果以上两个情况都不符合,则开始遍历链表,直到找到目标结点或者链表遍历结束;
  4. 退出遍历之后,如果没有找到目标结点则返回;
  5. 如果找到了目标结点,则新建一个指针指向被删除结点,先改变临时结点的 next 指向,再释放目标结点,完成删除。

以下是该函数执行的动画过程:

在这里插入图片描述

2. 根据位置删除结点

这种删除结点的方式不常见,假设链表有 m 个结点,要求删除第 n 个结点(m ≥ n),则在链表中找到第 n 个结点并删除。具体代码如下:

NODE *deleteNodeByPosition(NODE *pHead, unsigned int position)
{NODE *temp = pHead;if (temp == NULL) {printf("List is empty!\n");} else if (position == 0) {pHead = temp->next;free(temp);} else {for (unsigned int i = 0; i < position - 1; i++) {if (temp->next == NULL) {printf("Invalid position!\n");return pHead;}temp = temp->next;}NODE *delNode = temp->next;temp->next = delNode->next;free(delNode);}return pHead;
}

代码前半部分与前面大部分代码相似,就不过多解释,只从 for 循环开始解析,如下:

  1. 因为 0 号结点算链表的第一个结点,所以在 for 循环中的第二个表达式,位置数要减一;
  2. 如果位置大于链表长度,也就是已经遍历完链表了,但还到达指定位置,直接返回;
  3. 如果找到了目标结点,删除结点的方法与上一个代码的方式一样,此处省略。

3. 删除指针指向的结点(经典面试题)

这是一道 C 语言的经典面试题,原题目不太记得,大概就是在单链表中,未给出头指针,只有一个指针指向链表的某个结点,现在要求删除这个结点。

从前面提到两种删除结点的方式来看,我们要删除单链表上的某一个结点 N 的话,都是在 N 结点的上一个结点进行操作的,我们用 M 结点来代替 N 结点的上一个结点。删除的过程,就是让 M 结点的 next 指向 N 结点的下一个结点,然后再释放 N 结点。

但现在指针指在要求被删的结点上,倒退回上一个结点是不可能的事,所以这里就需要换一种思路来完成,那就是“移花接木”。我们都知道,链表主要的作用就是方便管理数据,而数据是可以被复制、转移和修改的,所谓删除结点,可以理解为把这个结点的数据从链表上去除,那么只要这个链表上没有这个结点的数据,不就等同于把这个结点在链表上删除了吗?因此,本题的解法就是:既然我们无法直接删除这个结点,那就把下一个结点的数据复制到当下指针所指的结点上,此时链表上就会有两个数据一样的结点(包括 next 也复制),然后再把当下指针所指的结点的下一个结点释放掉,完成结点的删除。

以下是删除给定指针指向的节点的函数实现:

int deleteNode(NODE *node) {if (node == NULL || node->next == NULL) {printf("Cannot delete the given node.\n");return -1;}NODE *temp = node->next;memcpy(node, temp, sizeof(NODE));free(temp);return 0;
}

代码解析如下:

  1. 先判断 node 指针是否为空,和 node 下一个结点是否存在,满足任意条件直接返回 -1
  2. 用临时指针指向下一个结点,把下一个结点的数据全部复制到本结点上,然后是否下一个结点。

[!NOTE]

为什么 node->next 也不能为 NULL

如果 node->nextNULL,就说明 node 指针指向的是链表的最后一个结点,没办法改变上一个结点的 next 指针指向 NULL。如果把 node 指针所指的结点直接释放掉,并不能使上一个结点的 next 指针指向 NULL,它依然是指向原本 node 所指的地址,而此时该地址已经被释放,后续所有的访问操作都是非法访问。

四、改(Update)和查(Read)————修改结点与查找结点

改和查,我们放在一起来讲解,因为修改结点数据其中就包含了查找结点。其实不单单是修改节点数据时有查找结点的操作,前面提到的前插法和后插法,还有删除结点的两个方法,都包含了查找结点的操作。

在单链表中查找某个结点,通常有两个目的,一个是读取里面的数据,二是修改。

我们先从比较简单的查找结点说起,在前面删除结点的章节就提过,链表的结点都有一个所谓的唯一标识符,一般查找结点也是通过这个唯一标识符查找的,所以查找结点的方法能很快的就写出来,如下:

NODE *searchNode(NODE *pHead, int nodeId)
{NODE *temp = pHead;while (temp != NULL) {if (temp->nodeId == nodeId)return temp;temp = temp->next;}return NULL;
}

其实就是通过遍历的方式,找到对应的结点。

接下来是改数据,一般来说改数据都是根据具体需求来决定,例如我要别某个结点的 nodeData 内容改成其它内容,我的代码可以这样写:

int updateNode(NODE *pHead, int nodeId, char *newData)
{NODE *temp = searchNode(pHead, nodeId);if (temp != NULL) {strcpy(temp->nodeData, newData);return 0;} else {return -1;}
}
下:```c
NODE *searchNode(NODE *pHead, int nodeId)
{NODE *temp = pHead;while (temp != NULL) {if (temp->nodeId == nodeId)return temp;temp = temp->next;}return NULL;
}

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

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

相关文章

Listary(Windows 文件搜索工具)专业版值得购买吗?

说到经典的国货软件&#xff0c;有一款 Win 软件是一定绕不过去的。它就是知名的本地文件搜索工具 Listary&#xff01; 便捷的文件搜索窗口&#xff1b;快捷操作的体验&#xff1b;与系统更匹配的外观设计&#xff1b;更智能的排序和更可靠的索引。 便捷的文件搜索窗口 紧凑…

Java基础(三)——类和对象、构造方法

个人简介 &#x1f440;个人主页&#xff1a; 前端杂货铺 ⚡开源项目&#xff1a; rich-vue3 &#xff08;基于 Vue3 TS Pinia Element Plus Spring全家桶 MySQL&#xff09; &#x1f64b;‍♂️学习方向&#xff1a; 主攻前端方向&#xff0c;正逐渐往全干发展 &#x1…

HarmonyOS Next开发学习手册——弹性布局 (Flex)

概述 弹性布局&#xff08; Flex &#xff09;提供更加有效的方式对容器中的子元素进行排列、对齐和分配剩余空间。常用于页面头部导航栏的均匀分布、页面框架的搭建、多行数据的排列等。 容器默认存在主轴与交叉轴&#xff0c;子元素默认沿主轴排列&#xff0c;子元素在主轴…

多见线程方法

多见线程方法 本节的类代码可以查看上一节的类代码 线程暂停 Thread.sleep(1000);//暂停1000毫秒这就有点像在时间里面学习的*sleep()*函数了 package multiThread2;public class main {public static void main(String[] args) {Animal a1 new Animal("张三",1…

PHP电商系统开发指南数据库管理

回答&#xff1a;数据库管理是电商系统开发的关键&#xff0c;涉及数据的存储、管理和检索。选择合适的数据库引擎&#xff0c;如mysql或 postgresql。创建数据库架构&#xff0c;定义数据的组织方式&#xff08;如产品表、订单表&#xff09;。进行数据建模&#xff0c;考虑实…

java笔记(30)——反射的 API 及其 使用

文章目录 反射1. 什么是反射2. 获取class字段&#xff08;字节码文件对象&#xff09;方式1方式2方式3应用 3. 获取构造方法和权限修饰符前期准备获取所有的公共构造方法获取所有的构造方法获取无参构造方法获取一个参数的构造方法获取一个参数的构造方法获取两个参数的构造方法…

详细介绍MySQL的索引(上)

索引 索引概述 索引(index)是帮助MySQL高效获取数据的数据结构(有序)。在数据之外&#xff0c;数据库系统还维护着满足特定查找算法的数据结构&#xff0c;这些数据结构以某种方式引用(指向数据&#xff0c;这样就可以在这些数据结构上实现高级查找算法&#xff0c;这种数据结…

Ubuntu更新源

一、sudo apt-get update命令 在Debian系中&#xff0c;Ubuntu是很火的一款开源系统产品。使用sudo apt-get update从我们的更新源中获取并更新系统中软件包的列表信息&#xff0c;sudo apt-get update作用如下&#xff1a; 更新软件包列表: 将本地软件包列表与远程仓库中的最…

二叉树第二期:堆的实现与应用

若对树与二叉树的相关概念&#xff0c;不太熟悉的同学&#xff0c;可移置上一期博客 链接&#xff1a;二叉树第一期&#xff1a;树与二叉树的概念-CSDN博客 本博客目标&#xff1a;对二叉树的顺序结构&#xff0c;进行深入且具体的讲解&#xff0c;同时学习二叉树顺序结构的应用…

[数据集][目标检测]桥梁检测数据集VOC+YOLO格式1116张1类别

数据集格式&#xff1a;Pascal VOC格式YOLO格式(不包含分割路径的txt文件&#xff0c;仅仅包含jpg图片以及对应的VOC格式xml文件和yolo格式txt文件) 图片数量(jpg文件个数)&#xff1a;1116 标注数量(xml文件个数)&#xff1a;1116 标注数量(txt文件个数)&#xff1a;1116 标注…

《强化学习的数学原理》(2024春)_西湖大学赵世钰 Ch8 值函数拟合 【基于近似函数的 TD 算法:Sarsa、Q-leaning、DQN】

PPT 截取有用信息。 课程网站做习题。总体 MOOC 过一遍 1、学堂在线 视频 习题 2、相应章节 过电子书 复习 【下载&#xff1a; 本章 PDF GitHub 页面链接】 3、 MOOC 习题 跳过的 PDF 内容 学堂在线 课程页面链接 中国大学MOOC 课程页面链接 B 站 视频链接 PPT和书籍下载网址…

【原创实现 设计模式】Spring+策略+模版+工厂模式去掉if-else,实现开闭原则,优雅扩展

1 定义与优点 1.1 定义 策略模式&#xff08;Strategy Pattern&#xff09;属于对象的⾏为模式。他主要是用于针对同一个抽象行为&#xff0c;在程序运行时根据客户端不同的参数或者上下文&#xff0c;动态的选择不同的具体实现方式&#xff0c;即类的行为可以在运行时更改。…

MySQL1(初始数据库 概念 DDL建库建表 数据库的三大范式 表约束)

目录 一、初始数据库 二、概念 三、DDL建库建表 1. 数据库结构 2. SQL语句分类 3. DDL语句操作数据库 注释&#xff1a; 查看数据库&#xff1a; ​编辑创建数据库&#xff1a; 删除数据库&#xff1a; 选择数据库&#xff1a; 4. 数据库表的字段类型 4.1 字符串…

java将html转成图片

java 将html转成图片 1.导入jar2.代码3.展示结果4.注意事项 最近有一个需求需要根据指定的样式生成图片&#xff0c;使用java原生技术有些麻烦&#xff0c;所以上网搜了下案例&#xff0c;最后发现最好用的还是html2image&#xff0c;这里进行简单总结下。 1.导入jar <!-- 用…

腾讯视频VIP会员账号怎么扫码登录一个帐号登录几个人的设备?

腾讯视频VIP会员账号怎么扫码登录&#xff1f; 腾讯视频VIP会员账号要想实现扫码登录&#xff0c;仅支持在电脑Web网页版和WindowsPC软件上登录腾讯视频时&#xff0c;才可以实现手机QQ扫码登录腾讯视频。 腾讯视频VIP与SVIP会员有什么区别&#xff1f; 腾讯视频VIP会员&…

前端小白必学:对Cookie、localStorage 和 sessionStorage 的简单理解

前言 Cookie、localStorage 和 sessionStorage 作为Web开发领域中广泛采用的三种客户端数据存储技术&#xff0c;它们各自拥有独特的优势、应用场景及限制条件&#xff0c;共同支撑起前端数据管理的多样化需求。也是面试常考题之一&#xff0c;今天就和大家简单谈一下我对它们…

揭开大语言模型(LLM)内部运作的算法逻辑

本文探讨了 Anthropic 的突破性技术&#xff0c;以揭示大型语言模型 (LLM) 的内部工作原理&#xff0c;揭示其不透明的本质。通过深入研究LLM Claude Sonnet 的“大脑”&#xff0c;Anthropic 增强了人工智能的安全性和可解释性&#xff0c;为人工智能的决策过程提供了更深入的…

应用部署方式演变

应用部署方式演变 1.传统部署2.虚拟化部署3.容器化部署 1.传统部署 传统的应用程序部署是将多个应用程序直接部署在操作系统上&#xff0c;一旦其中的某个应用程序出现内存泄漏&#xff0c;那么该程序就会大量吞噬系统内容空间&#xff0c;导致其他应用程序无法正常运行。 2.虚…

如何让两个不同网段的直连地址通信(有点意思)

群里一个朋友出了个题&#xff1a;两个路由器接口直连&#xff0c;一个接口IP是1.1.1.1/30&#xff0c;一个接口IP是2.2.2.2/30&#xff0c;如何让它们通信&#xff1f; 群里的朋友们纷纷献策&#xff1a; 1、用PPP方式连接&#xff0c;直接通 2、配对端IP地址同网段的s…

鱼叉式钓鱼

鱼叉式网络钓鱼&#xff1a; 鱼叉式网络钓鱼是一种网络钓鱼形式&#xff0c;它针对特定个人或组织发送定制消息&#xff0c;旨在引发特定反应&#xff0c;例如泄露敏感信息或安装恶意软件。这些攻击高度个性化&#xff0c;使用从各种来源收集的信息&#xff0c;例如社交媒体资…