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

news2024/11/16 13:00:00

文章目录

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

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

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

相关文章

免费下载!Windows10企业版 LTSC 2019最新镜像!

Windows10 企业版 LTSC 2019版本以微软 Windows 10 企业版 LTSC 为基础&#xff0c;进行精心地优化制作&#xff0c;制作全程没有连接网络&#xff0c;确保无病毒残留&#xff0c;还升级了防火墙功能&#xff0c;能有效地保护电脑安全&#xff0c;让大家能舒心操作。该版本包含…

基于Python学生成绩管理系统详细设计和实现(源码+LW+调试文档+讲解等)

&#x1f497;博主介绍&#xff1a;✌全网粉丝10W,CSDN作者、博客专家、全栈领域优质创作者&#xff0c;博客之星、平台优质作者、专注于Java、小程序技术领域和毕业项目实战✌&#x1f497; &#x1f31f;文末获取源码数据库&#x1f31f; 感兴趣的可以先收藏起来&#xff0c;…

开源模型破局OpenAI服务限制,15分钟灵活搭建RAG和Agent应用

简介&#xff1a; 今天&#xff0c;我们做了两个实验&#xff0c;目标在15分钟内&#xff0c;完成下载社区的开源模型&#xff0c;部署成API&#xff0c;替换LlamaIndex中RAG和LangChain中OpenAI接口Agent的最佳实践&#xff0c;并取得符合预期的结果。 实验一 实验目标&…

基于 Paimon 的袋鼠云实时湖仓入湖实战剖析

在当今数据驱动的时代&#xff0c;企业对数据的实施性能力提出了前所未有的高要求。为了应对这一挑战&#xff0c;构建高效、灵活且可扩展的实时湖仓成为数字化转型的关键。本文将深入探讨袋鼠云数栈如何通过三大核心实践——ChunJun 融合 Flink CDC、MySQL 一键入湖至 Paimon …

AI绘画Stable Diffusion:超级质感真人大模型,逼真青纯!

大家好&#xff0c;我是设计师阿威 今天和大家分享一个具有超级质感的基于SD1.5的真人大模型&#xff1a;极致质感-DgirlV5&#xff0c;该模型追求质感的不断优化&#xff0c;细到发丝&#xff0c;当前最新版本是V5.1&#xff0c;修正了V5版本整体色彩发红的问题。 作者对该模…

OFDM 802.11a的FPGA实现:发射部分,bug更正,以及更新说明。

目录 bug1bug2bug3bug4 微信公众号获取更多FPGA相关源码&#xff1a; OFDM 802.11a的FPGA实现:发射部分完整工程 经过各位朋友的提醒&#xff0c;这部分内容有些许bug&#xff0c;目前已经更正&#xff0c;后续还会持续更新。获取更新内容&#xff0c;可以进上述链接&#xf…

BurpSuite2024.5.3专业版,仅支持Java21以上

01更新介绍 此版本引入了对 WebSocket 的 Burp Scanner 支持、对录制的登录编辑器的改进、WebSocket 匹配和替换规则以及许多性能改进。我们还删除了一些冗余的扫描检查。 Burp Scanner 对 WebSockets 的支持我们更新了内部代理的配置&#xff0c;以允许 WebSocket 流量。这使…

CORE Mobility Errorr的调试

在运行CORE tutorial 3中的mobility示例时&#xff0c;出现如下错误&#xff1a; 当看到这个问题的时候&#xff0c;并没有仔细去分析日志和现象&#xff0c;在core-daemon的进程打印界面只看了一下最后的出错堆栈&#xff1a; 2024-06-27 10:43:48,614 - ERROR - _server:_ca…

Arduino - 旋转编码器 - 伺服电机

Arduino - 旋转编码器 - 伺服电机 Arduino - Rotary Encoder In this tutorial, We are going to learn how to program Arduino to rotate a servo motor according to the rotary encoder’s output value. 在本教程中&#xff0c;我们将学习如何对Arduino进行编程&#xff…

浏览器页面打不开(网络连接正常的情况下)

目录 一、代理被打开 二、DNS被篡改 三、网络配置出现问题 四、浏览器配置问题 一、代理被打开 1. 右击Wifi图标 2.点击“打开网络和internet设置” 3. 点击代理&#xff0c;把该窗口所有的东西都关闭&#xff0c;尤其是代理服务器 二、DNS被篡改 如果第一个方法不行&am…

平板WPS转换的PDF文件保存位置解析

在日常工作和生活中&#xff0c;我们经常需要将文档转换成PDF格式进行分享&#xff0c;以确保接收者能够无障碍地查看文件内容&#xff0c;不受软件版本或操作系统的限制。WPS作为一款功能强大的办公软件&#xff0c;也提供了文档转换为PDF的功能。然而&#xff0c;有时在转换并…

一本顶三本?入门LLM大模型必读《大模型应用开发极简入门》附PDF书籍

今天带来的是最近刚出版的新书&#xff1a; 《大模型应用开发极简入门&#xff1a;基于 GPT-4 和ChatGPT》 。 这本书是 O’Reilly 出版的&#xff0c;两位共同作者是来自 Worldline 公司的机器学习研究员 Olivier Caelen 和 数据工程师 Marie-Alice Blete。这两位作者一位侧重…

Kafka入门到精通(二)-安装Zookeeper

前言&#xff1a;因为我们要学习kafka&#xff0c;那么我们必须先安装了解下Zookeeper&#xff1b; Zookeeper简介 Zookeeper是一个开源的分布式协调服务&#xff0c;由Apache维护&#xff0c;旨在为分布式系统提供一致性、可靠性和高效的数据管理。 它通过提供一系列简单…

【Java Web】MVC架构模式

目录 一、MVC是什么? 二、MVC组成结构 三、MVC模式下项目常见包 一、MVC是什么? MVC&#xff08;ModelViewController&#xff09;是软件工程中的一种软件架构模式&#xff0c;它把软件系统分为模型、视图和控制器三个基本部分。用一种业务逻辑、数据、界面显示分离的方法组织…

Django项目部署:uwsgi+daphne+nginx+vue部署

一、项目情况 项目根目录&#xff1a;/mnt/www/alert 虚拟环境目录&#xff1a;/mnt/www/venv/alert 激活虚拟环境&#xff1a;source /mnt/www/venv/alert/bin/activate 二、具体配置 1、uwsgi启动配置 根目录下&#xff1a;新增 uwsgi.ini 注意&#xff1a;使用9801端…

LSTM理解

目录 一、LSTM的本质 二、LSTM的原理 三、LSTM的应用 本文将从LSTM的本质、LSTM的原理、LSTM的应用三个方面&#xff0c;带您一文搞懂长短期记忆网络Long Short Term Memory | LSTM。 一、LSTM的本质 RNN 面临问题&#xff1a;RNN&#xff08;递归神经网络&#xff09;在处理…

劳易测应用案例:橡胶密炼生产线安全改造项目(下)

橡胶密炼是汽车轮胎制造流程中的核心环节&#xff0c;主要负责将橡胶与多种添加剂混合&#xff0c;确保均匀分散&#xff0c;以制备合格的橡胶材料。橡胶密炼生产线由多个关键设备组成&#xff0c;包括切胶机、导切机、称重和输送系统、密炼机、开炼机以及胶片冷却机等&#xf…

《分析模式》漫谈07-怎样把一张图从不严谨改到严谨

DDD领域驱动设计批评文集 做强化自测题获得“软件方法建模师”称号 《软件方法》各章合集 下图是《分析模式》原书第2章的图2.10&#xff0c;里面有一些错误和考虑不周的地方&#xff1a; 2004中译本和2020中译本的翻译如下&#xff1a; 基本上都是照搬&#xff0c;没有改过…

Flutter第十五弹 Flutter插件

目标&#xff1a; 1.Flutter插件是什么&#xff1f;有什么作用&#xff1f; 2.怎么创建Flutter插件&#xff1f; 一、什么是插件 在flutter中&#xff0c;一个插件叫做一个package&#xff0c;使用packages的目的就是为了达到模块化&#xff0c;可以创建出可被复用和共享的代…

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

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