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

文章目录

  • 【面试分享】嵌入式面试题常考难点之关于单链表的增删改查
  • 一、单链表结点定义
  • 二、增(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 nodeName[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 -> NULL

 1: <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;
}

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

一步步带你解锁Stable Diffusion:老外都眼馋的 SD 中文提示词插件分享

大家好我是极客菌&#xff01;今天我们继续来分享一个外国人都眼馋的 SD 中文提示词插件。 那我们废话不多说&#xff0c;直接开整。 SD 的插件安装&#xff0c;小伙伴们应该都会了吧&#xff0c;我这里再简单讲下哦&#xff0c;到「扩展」中的「可下载」中点击「加载扩展列表…

图像、色彩波和抗混叠

阮一峰的博文图像与滤波中分析了图像和波的关系&#xff0c;主要结论如下&#xff1a; 图像本质上就是各种色彩波的叠加。图像就是色彩的波动&#xff1a;波动大&#xff0c;就是色彩急剧变化&#xff1b;波动小&#xff0c;就是色彩平滑过渡。色彩剧烈变化的地方&#xff0c;…

车载系统类 UI 风格品质非凡

车载系统类 UI 风格品质非凡

桌面上的记事软件是什么 大家都在用什么记事软件

你是否经常因为琐事繁多而感到焦虑&#xff1f;是否曾在忙碌的工作中遗漏了重要的事项&#xff1f; 作为一名文字工作者&#xff0c;我深知记事的重要性。在繁杂的工作和生活中&#xff0c;我们需要的不仅仅是一个简单的记事本&#xff0c;而是一个能够帮助我们高效管理时间、…

Spring Boot如何集成Spring Data JPA?

&#x1f345; 作者简介&#xff1a;哪吒&#xff0c;CSDN2021博客之星亚军&#x1f3c6;、新星计划导师✌、博客专家&#x1f4aa; &#x1f345; 哪吒多年工作总结&#xff1a;Java学习路线总结&#xff0c;搬砖工逆袭Java架构师 &#x1f345; 技术交流&#xff1a;定期更新…

Http客户端-Feign 学习笔记

作者介绍&#xff1a;计算机专业研究生&#xff0c;现企业打工人&#xff0c;从事Java全栈开发 主要内容&#xff1a;技术学习笔记、Java实战项目、项目问题解决记录、AI、简历模板、简历指导、技术交流、论文交流&#xff08;SCI论文两篇&#xff09; 上点关注下点赞 生活越过…

Embedding的概念和展开

前言 本章&#xff0c;我们介绍一个非常细的细节技术。让我们微调大模型的一些特性和能力。 在大模型的AI套路演化过程中&#xff0c;其实经历了太多的技术革新和方式变化&#xff0c;Embedding其实也可能是其中一个高速湮灭的技术点之一。 对比LoRA现在大红大紫&#xff0c…

深度学习 - Transformer 组成详解

整体结构 1. 嵌入层&#xff08;Embedding Layer&#xff09; 生活中的例子&#xff1a;字典查找 想象你在读一本书&#xff0c;你不认识某个单词&#xff0c;于是你查阅字典。字典为每个单词提供了一个解释&#xff0c;帮助你理解这个单词的意思。嵌入层就像这个字典&#xf…

初识 Embedding,为何大家都基于它搭建私人智能客服?

随着 AI 技术的发展&#xff0c;大家在日常使用过程中经常会碰到一些目前 GPT4 也无法解决的问题&#xff1a; 无法获取个人私有数据信息&#xff0c;进行智能问答无法获取最新信息&#xff0c;LLM 模型训练都是都是有截止日期的无法定制化私有的专属模型&#xff0c;从而在某…

算法常见问题

1.c虚函数 虚函数是用来实现多态(polymorphism) 的一种机制。通过使用虚函数&#xff0c;可以在子类中重写父类中定义的方法&#xff0c;并且在运行时动态地确定要调用哪个方法。 在类定义中将一个成员函数声明为虚函数&#xff0c;需要使用 virtual 关键字进行修饰 。 通过指向…

山海相逢,因你而至!第九届全球边缘计算大会·深圳站圆满召开!

2024年6月22日&#xff0c;第九届全球边缘计算大会在深圳盛大开幕。本次盛会由边缘计算社区主办&#xff0c;并获得了EMQ、研华科技、华为等重量级单位的鼎力支持。大会汇聚了来自全球各地的业界精英&#xff0c;共同探讨边缘计算的前沿技术、应用趋势以及创新实践&#xff0c;…

Isaac Sim 9 物理(1)

使用Python USD API 来实现 Physics 。 以下内容中&#xff0c;大部分 Python 代码可以在 Physics Python 演示脚本文件中找到&#xff0c;本文仅作为个人学习笔记。 一.设置 USD Stage 和物理场景 Setting up a USD Stage and a Physics Scene USD Stage不知道怎么翻译&#…

docker 部署的 wordpress 接入阿里云短信服务 详细实操介绍

一、阿里云短信服务配置&#xff1a; 1、登录 阿里云短信服务 完成指引短信相关配置 2、创建RAM用户 并完成授权 出于安全及规范考虑 需通过RAM 用户来完成OponApl 接口调用&#xff0c;创建成功需完成短信接口&#xff08;AliyunDysmsFullAccess、AliyunDysmsReadOnlyAccess…

【大模型】大模型微调方法总结(二)

1.Adapter Tuning 1.背景 2019年谷歌的研究人员首次在论文《Parameter-Efficient Transfer Learning for NLP》提出针对 BERT 的 PEFT微调方式&#xff0c;拉开了 PEFT 研究的序幕。他们指出&#xff0c;在面对特定的下游任务时&#xff0c;如果进行 Full-Fintuning&#xff0…

执行yum命令报错Could not resolve host: mirrors.cloud.aliyuncs.com; Unknown error

执行yum命令报错 [Errno 14] curl#6 - "Could not resolve host: mirrors.cloud.aliyuncs.com; Unknown error 修改图中所示两个文件&#xff1a; vim epel.repo vim CentOS-Base.repo 将所有的http://mirrors.cloud.aliyuncs.com 修改为http://mirrors.aliyun.com。 修改…

数据库逻辑结构设计-实体和实体间联系的转换、关系模式的优化

一、引言 如何将数据库概念结构设计的结果&#xff0c;即用E-R模型表示的概念模型转化为关系数据库模式。 E-R模型由实体、属性以及实体间的联系三个要素组成 将E-R模型转换为关系数据库模式&#xff0c;实际上就是要将实体及实体联系转换为相应的关系模式&#xff0c;转换…

【深度学习】基于深度离散潜在变量模型的变分推理

1.引言 1.1.讨论的目标 阅读并理解本文后&#xff0c;大家应能够&#xff1a; 掌握如何为具有离散潜在变量的模型设定参数在可行的情况下&#xff0c;使用精确的对数似然函数来估计参数利用神经变分推断方法来估计参数 1.2.导入相关软件包 # 导入PyTorch库&#xff0c;用于…

LLM vs SLM 大模型和小模型的对比

语言模型是能够生成自然人类语言的人工智能计算模型。这绝非易事。 这些模型被训练为概率机器学习模型——预测适合在短语序列中生成的单词的概率分布&#xff0c;试图模仿人类智能。语言模型在科学领域的重点有两个方面&#xff1a; 领悟情报的本质。 并将其本质体现为与真实…

Java学习十一—Java8特性之Stream流

一、Java8新特性简介 2014年3月18日&#xff0c;JDK8发布&#xff0c;提供了Lambda表达式支持、内置Nashorn JavaScript引擎支持、新的时间日期API、彻底移除HotSpot永久代。 ​ Java 8引入了许多令人兴奋的新特性&#xff0c;其中最引人注目的是Lambda表达式和Stream API。以…

十年磨一剑,华火电燃组合灶重磅问世,引领厨房新时代

十年磨一剑&#xff0c;华火研发团队经过不懈努力&#xff0c;成功将等离子电生明火技术与电陶炉红外线光波炉技术精妙融合&#xff0c;打造出的这款具有划时代是意义的电燃组合灶HH-SZQP60&#xff0c;终于在 2024 年6月震撼登场&#xff0c;该灶以其卓越的创新技术和独特的产…