全面理解链表数据结构:各种节点操作、做题技巧,易错点分析与题目清单(C++代码示例,不断更新)

news2024/11/24 12:22:33

什么是链表

链表是一种线性数据结构,它包含的元素并不是物理上连续的,而是通过指针进行连接。链表中的每个元素通常由一个节点表示,每个节点包含一个数据元素和一个或多个链接(指针)。

在这里插入图片描述

链表的主要类型包括:

  • 单向链表(Singly Linked List):每个节点包含一个指向下一个节点的指针。链表的遍历从头节点开始,沿着每个节点的指针,直到遇到一个指向null的指针(这是链表的尾部)。

在这里插入图片描述

  • 双向链表(Doubly Linked List):每个节点有两个指针,一个指向前一个节点,另一个指向后一个节点。这允许从两个方向遍历链表。

在这里插入图片描述

  • 循环链表(Circular Linked List):它可以是单向的也可以是双向的,但其特点是链表的尾节点指向链表的头节点,形成一个循环。
    在这里插入图片描述

与数组相比,链表有以下特点和区别:

  • 动态大小:链表的大小是动态的,可以根据需要添加或删除节点。相比之下,数组的大小在创建时就已经确定,并且不能改变。

  • 效率的不同:在链表中插入和删除元素通常更有效率,因为这些操作不需要移动其他元素,只需更改相关节点的指针即可。另一方面,数组中的插入和删除操作可能需要移动大量的元素。然而,数组可以通过索引直接访问任何元素,而在链表中访问特定元素需要从头节点开始逐个遍历。

  • 内存使用:在链表中,每个元素都需要额外的存储空间来存储指针,这导致链表的内存使用效率相对较低。另一方面,数组中的元素在内存中是连续存储的,没有额外的存储开销。

  • 内存分配方式:数组需要连续的内存空间,而链表则可以利用内存中的任何空闲区域,只要能够容纳单个节点就可以。

这些特性使得链表和数组在不同的情况下各有优势。例如,如果您需要频繁地在序列中间插入和删除元素,链表可能是一个好的选择。然而,如果您的主要操作是随机访问元素,那么数组可能是更好的选择

接下来,就由我来细细的给大家讲解每种类型的链表,我们如何去自定义他们的数据结构,以及如何进行各种的节点操作。

单链表(单向链表)

  • 这里我们主要去讲解一下单向链表的各种简单的操作,比如添加,删除一个节点,然后就是反转链表。
  • 因为很多算法题的基础,其实就是在这些基础操作上去做变种,掌握了这些基础操作,你的链表也就算是入门级别的了。

定义单链表节点结构体**

我们都知道,单向链表这种数据结构都是由一个个节点组成的,那单向链表节点这种结构体都由什么组成呢?当然是由一个节点的值,与节点的一个next指针组成,以及构建节点的构造函数组成。其中节点的next指针,指向了该链表的下一个节点的元素位置。

所以,当我们创建单向链表节点的这个结构体的时候,需要指定以下元素:

  • 节点的值
  • 节点的next指针
  • 节点的构造函数(用于初始化一个新的节点,不然如何创建一个新的节点(链表)呢?

具体代码如下:

struct ListNode {
    int val; // 节点的值
    ListNode *next; // 节点的next指针
    ListNode(int x) : val(x), next(nullptr) {} // 初始化一个链表的构造函数
}

创建链表

  • 当我们想要构造一个链表节点,或者搭建一个全新的链表,new关键字就会发挥作用。这个操作需要我们主动去分配一块新的内存空间,以存储新的节点信息。然而,这仅仅只是创建了一个节点。如果我们想要创建一个完整的链表,那就需要逐个创建节点,然后利用next指针把这些节点逐一连接起来。这种操作在解决算法题目的过程中是常见的。

  • 让我们以创建链表为例进行说明。首先,我们通过 new关键字创建一个节点,把需要的数据存储在这个新分配的内存空间里。然后,通过重复这个步骤,我们可以创建多个节点。最后,通过使用next指针,将这些独立的节点串联起来,形成一个完整的链表。

按照我们刚刚给出的 ListNode 结构体定义,我们可以这样创建一个简单的链表:

int main() {
    ListNode* node1 = new ListNode(1);
    ListNode* node2 = new ListNode(2);
    ListNode* node3 = new ListNode(3);

    node1->next = node2;
    node2->next = node3;

    // 遍历链表,打印值
    ListNode* current = node1;
    while (current != NULL) {
        std::cout << current->val << " ";
        current = current->next;
    }
    return 0;
}

插入操作

  • 其实插入一个节点也是很有讲究的,要看你插入到什么位置。
  • 比如,在给定节点前插入一个节点。与给定节点后插入一个节点。

所以接下来我们就把这两种种插入操作,来分开讲解一下。

从前插入

  • 注意,我们需要判断这个节点与链表是否存在为空的情况,以及这个节点是否在链表内

具体代码如下:

ListNode * insertBeforeNode(ListNode * head, ListNode * target, int val){
    // 如果链表为空,给定的节点也为空,则直接返回nullptr
    if(head == nullptr && target == nullptr) return nullptr;

    // 如果链表不为空,给定的节点为空,等于在尾后插入一个节点
    if(target == nullptr){
        ListNode * prev = head;
        ListNode * cur = head->next;
        while(cur != nullptr){
            prev = cur;
            cur = cur->next;
        }
        ListNode * node = new ListNode(val);
        prev->next = node;
        return head;
    }

    // 如果链表不为空,给定的节点也不为空
    ListNode * prev = head;
    ListNode * cur = prev->next;
    while(cur != target && cur != nullptr){
        prev = prev->next;
        cur = cur->next;
    }
    ListNode * temp = new ListNode(val);
    prev->next = temp;
    temp->next = cur;
    return head;
}

从后插入

  • 从后插入,比从前插入更简单,只要确定这个节点在链表中,就可以完成插入操作

具体的代码如下:

// 从指定节点后插入一个节点
ListNode * insertAfterNode(ListNode * head, ListNode * target, int val){

    // 如果链表为空,或者指定的节点为空,则无法在指定位置后插入这个节点,直接返回
    if(head == nullptr || target == nullptr) return head;

    // 如果链表不为空,指定的节点不为空,则我们在指定的位置后,插入这个节点
    ListNode * cur = head;
    while(cur != nullptr){
        if(cur == target){
            ListNode * temp = cur->next;
            ListNode * node = new ListNode(val);
            cur->next = node;
            node->next = temp;
            break;
        }
        cur = cur->next;
    }
    return head;
}

删除操作

在链表中如果说要删除一个节点了话,无外乎与这么两种情况:

  • 一个是你题目给了你链表的头结点,让你去删除一个指定的元素,
  • 还有一种是原地删除,就给了你一个需要让你删除的节点,让你原地删除这个节点

有链表头节点的情况

  • 如果我们确定了这个节点在这个链表内,我们需要找到这个节点的前驱节点,即可完成删除操作

具体的代码如下:

ListNode * deleteNode(ListNode * head, ListNode * target){

    // 如果链表为空,或者指定节点为空,则直接返回
    if(head == nullptr || target == nullptr) return head;

    ListNode * prev = head;
    ListNode * cur = prev->next;
    while(cur != nullptr){
        if(cur == target){
            ListNode * temp = cur->next;
            prev->next = temp;
            delete cur;
            break;
        }
        prev = prev->next;
        cur = cur->next;
    }
    return head;
}

只有被删除节点的这种情况

  • 我们原地删除一个节点
    void deleteNode(ListNode* node) {
        node->val = node->next->val;
        ListNode * temp = node->next;
        node->next = node->next->next;
        delete temp;
        temp = nullptr;
    }

查找操作

  • 查找操作这个范围其实挺大的,因为我们可以有无数种查找条件,同样的就得自己去写无数种符合特定查找的逻辑。
  • 而要去实现特定查找的逻辑其实就没有所谓的定式啦,不过万变不离其宗,也都是我下面解释的这几种方式的变种而已。

查找链表的中间节点

  • 在这里我们给出两种实现方式,一种就是中规中矩的实现,先遍历一遍链表,查找链表节点的个数,然后计算出中间或者中间偏厚的那个节点的下标是多少,之后再遍历一遍链表找到这个节点。
  • 第二种方式便是快慢指针法,快指针一次走两步,慢指针一次走一步,这样,当快指针走完的时候,慢指针正好也就走了链表的一般。
  • 虽然两种方式的时间复杂度都是O(n),但是后者的的确确比第一种方式快了将近一倍的速度。

一般的实现方式

ListNode * findMiddleNode(ListNode * head){
    // 如果链表为空,或者链表只有一个节点,则返回head本身
    if(head == nullptr || head->next == nullptr) return head;
    int count = 0;
    ListNode * cur = head;
    while(cur !=nullptr){
        ++count;
        cur = cur->next;
    }
    int mid = count / 2;
    cur = head;
    while(mid > 0){
        --mid;
        cur = cur->next;
    }
    return cur;
}

快慢指针法:

ListNode * findMiddleNode(ListNode * head) {
    if(head == nullptr || head->next == nullptr) return head;
    ListNode * slow = head;
    ListNode * fast = head;
    while(fast->next != nullptr && fast->next->next != nullptr){
        slow = slow->next;
        fast = fast->next->next;
    }
    return slow;
}

查找两条链表是否有交点

  • 实际上就是让你去寻找两条链表中是这样的一个节点,它的节点中的val与next指针指向的对象是否完全相等,如果找到了则返回第一个这样的节点,否则返回nullptr。
  • 这个问题一共有三种方法可以解决,这里继续赘述了,这篇文章的总体长度就会太长。
  • 希望大家有兴趣的话,可以跳转到我的这篇文章去看看具体怎样实现
  • 《程序员面试金典(第6版)》面试题 02.07. 链表相交(查找节点操作,哈希表,双指针(先走n步法,交替遍历法))

查找链表的成环点(检测链表是否成环)

  • 这个问题也十分的简单,具体的方法和上一个问题没有什么太大的区别。我们也是可以去用哈希法,与双指针法去完成的。
  • 具体的实现还是请大家看我的这篇文章:《程序员面试金典(第6版)》面试题 02.08. 环路检测(哈希法,双指针,检测链表是否有环)
  • 结合着题目,我详细大家一定能更容易理解这个问题的奥义。

修改操作

  • 修改操作其实也是一个和查找操作一样,范围很大的问题。由于特定的要求,我们有很多种修改的方式
  • 那么最常见的一种修改,就是反转链表了。这里我们就单单介绍一下这一种方式吧

反转操作(原地反转,在原链表上进行操作)

反转一个链表有很多种方式,这里我们主要介绍两种,分别是迭代法,与递归法

迭代法

  • 这种方法其实就是双指针法,精髓就是找一个前驱指针和一个当前指针,之后从头往后逐个反转节点,最后返回即可

在这里插入图片描述

具体实现请看代码:

ListNode * reverseList1(ListNode * head){
    // 如果链表为空,或者说是只有一个节点,就直接返回
    if(head == nullptr || head->next == nullptr) return head;
    
    ListNode * prev = nullptr;
    ListNode * cur = head;
    while(cur!=nullptr){
        ListNode * temp = cur->next;
        cur->next = prev;
        prev = cur;
        cur = temp;
    }
    return prev;
}

递归法:

  • 这种方法的本质就是递归,我们需要找到最后一个节点,然后直接返回最后一个节点。
  • 不变的单层递归逻辑就是让上一个节点执行前一个节点,然后当前节点指向空

在这里插入图片描述

具体的实现请看代码

ListNode * reverseList2(ListNode * head){
    // 如果链表为空,或者说是只有一个节点,就直接返回
    if(head == nullptr || head->next == nullptr) return head;

    ListNode * p = reverseList2(head->next);
    head->next->next = head;
    head->next = nullptr;
    return p;
}

双链表(双向链表)

  • 双链表与单链表的区别在与,双链表里面多了一个prev指针,指向这个节点的前一节点。其他的与单向链表一致

  • 但由于双向链表其实我们做题的时候,很少用到自己自定义双向链表的情况,所以这里就不放细细拆解自定义双向链表里面的所有操作了。

  • 我写了一个自定义双向链表类,大家感兴趣的话,看看即可,没有什么特别的难处

自定义双向链表类(带哨兵节点的那种)

  • 具体的代码如下:
class DoublyLinkedList {
private:
    struct Node {
        int val;
        Node* prev;
        Node* next;
        Node(int val) : val(val), prev(nullptr), next(nullptr) {}
    };

    Node* head;
    Node* tail;

public:
    DoublyLinkedList() {
        // 创建哨兵节点
        head = new Node(0); // 头部哨兵节点
        tail = new Node(0); // 尾部哨兵节点
        // 将哨兵节点连接起来
        head->next = tail;
        tail->prev = head;
    }

    void addNode(int val) {
        // 在链表尾部添加新的节点
        Node* node = new Node(val);
        node->prev = tail->prev;
        node->next = tail;
        tail->prev->next = node;
        tail->prev = node;
    }

    void removeNode(Node* node) {
        // 从链表中移除节点
        node->prev->next = node->next;
        node->next->prev = node->prev;
        delete node;
    }

    ~DoublyLinkedList() {
        // 从头部开始删除所有节点
        Node* node = head;
        while (node) {
            Node* next_node = node->next;
            delete node;
            node = next_node;
        }
    }
};

链表的易错点分析

使用自定义单链表和双链表时,需要注意一些常见的易错点,这些主要包括:

  • 空指针解引用:链表操作中的空指针解引用是很常见的错误。例如,当试图访问链表的下一个节点或者前一个节点时,一定要确保当前节点不是尾节点或头节点。

  • 内存管理:在创建新的节点或者删除现有的节点时,需要正确处理内存分配和释放,避免内存泄漏。尤其是在删除节点时,一定要记住先更新链表的连接关系,再释放节点内存。

  • 链表循环引用:特别是在双向链表中,可能会出现头尾相连形成循环引用的情况,这会导致访问链表时陷入无限循环。需要小心处理头尾节点的连接关系,避免出现循环引用。

  • 链表断裂:在进行节点插入和删除操作时,如果没有正确更新相邻节点的连接关系,可能会导致链表断裂。例如,在插入新节点时,不仅需要更新新节点和相邻节点的连接关系,还需要更新相邻节点和新节点的连接关系。

  • 边界条件处理:在进行链表操作时,常常需要处理一些边界条件,例如链表为空、只有一个节点、插入或删除的是头节点或尾节点等情况。这些边界条件如果处理不当,可能会导致程序错误。

  • 忽视哨兵节点的影响:如果链表使用了哨兵节点,需要在处理链表时考虑到哨兵节点的存在。特别是在遍历链表、计算链表长度等操作时,要注意不要将哨兵节点计入。

以上就是在使用自定义单链表和双链表时需要注意的一些常见易错点。只要小心处理这些问题,就能避免大部分链表操作中的错误。

我作为一个初学者时,常犯的一个链表错误

错误原因:

  • 我自己做单链表的题目的时候,还会犯这样的一个错误,就是需要我新创建一个链表的操作,我经常搞混,跑去原链表上进行操作了,还不知道我已经修改了原链表

错误解析

  • 这是一个很常见的问题,通常是由于对变量的引用和赋值操作不够清晰所导致的。在 C++ 中,当你写 Node* node = head; 这样的语句时,你创建的 node 是原链表头节点 head 的一个引用,也就是说,他们都指向同一个物理对象。因此,对 node 的任何修改也会影响到 head。

  • 如果你需要创建一个新的链表,而不是在原链表上进行操作,你应该为新链表的每个节点创建新的内存空间,而不是直接使用原链表的节点。这可以通过 new 关键字来实现

  • 比如:Node* node = new Node(head->val);

链表的做题技巧(leetcode)

技巧一:创建链表的时候,使用哨兵节点

在数据结构中,哨兵节点(也称为哑元节点、哨兵元素等)是一种特殊的节点,它主要用于简化链表操作的编程实现。
对于单向链表来说,通常只有一个哨兵节点,位与链表的头部。
对于双向链表来说,通常有两个哨兵节点,一个位于链表的头部,一个位于链表的尾部。

使用哨兵节点的好处主要有以下几点:

  • 简化边界条件的处理:在进行链表操作时,需要考虑很多边界条件,如插入或删除元素时,元素可能在链表的头部或尾部。使用哨兵节点可以将这些特殊情况转化为一般情况,简化代码的复杂度。

  • 提高代码的效率和可读性:由于哨兵节点消除了许多特殊情况的需要,代码通常更短,更易于阅读和理解。同时,由于边界检查的需求减少,代码的效率可能会有所提高。

  • 方便进行循环操作:在具有头尾哨兵节点的双向链表中,可以通过从一个哨兵节点开始,绕链表进行循环操作,这在某些应用场景中可能非常有用。

  • 保护真实数据:哨兵节点可以作为保护屏障,防止对链表进行错误的修改。

但是,也要注意哨兵节点的使用会消耗额外的存储空间,虽然这在大多数情况下并不是问题。总的来说,是否使用哨兵节点,需要根据实际情况以及特定应用的需求来决定。

技巧二,谁说节点只能放一个数据?

  • 根据题目的需要,在我们自定义创建节点的数据域的时候,不只是可以存放一个数据,可以存放多个数据。

技巧三,能创建新链表尝试解决问题的,先创建新链表解决问题。

  • 虽然说创建新的链表会比在原链表上解决问题要多花一定的空间。但是,如果你不是指针大师了话,你很容易操作错你的指针,导致一些未知的bug出现。
  • 所以,你可以先尝试创建新链表解决问题,之后再尝试在原链表上解决问题。

技巧四,前驱指针(删除,反转,插入操作可能都会用到)

  • 当我们要删除某一个节点时,我们之知道链表的头节点。我们的链表是单链表,那这个时候就需要用两个指针去找到那个节点,并删除那个节点了。
  • 前驱指针顾名思义也就是在指向当前节点的前一个节点的指针。
  • 当我们的当前指针找到了要删除的节点时,我们的前驱指针也就正好指向到了被删除节点的前一个节点。
  • 直接删除就好

链表题目清单

  • 《程序员面试金典(第6版)》面试题 02.01. 移除重复节点(哈希映射,多指针暴力破解,节点的删除操作)
  • 《程序员面试金典(第6版)》面试题 02.02. 返回倒数第 k 个节点(双指针法,节点的查找操作)
  • 《程序员面试金典(第6版)》面试题 02.03. 删除中间节点(特殊的删除节点操作)
  • 《程序员面试金典(第6版)》面试题 02.04. 分割链表(创建新链表)
  • 《程序员面试金典(第6版)》面试题 02.05. 链表求和(构建一个新链表)
  • 《程序员面试金典(第6版)》面试题 02.06. 回文链表(双指针(快慢指针),查找链表中间节点,反转链表)
  • 程序员面试金典(第6版)》面试题 02.07. 链表相交(查找节点操作,哈希表,双指针(先走n步法,交替遍历法))
  • 《程序员面试金典(第6版)》面试题 02.08. 环路检测(哈希法,双指针,检测链表是否有环)

总结

最后的最后,如果你觉得我的这篇文章写的不错的话,请给我一个赞与收藏,关注我,我会继续给大家带来更多更优质的干货内容

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

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

相关文章

全志V3S嵌入式驱动开发(系统image创建和烧入)

【 声明&#xff1a;版权所有&#xff0c;欢迎转载&#xff0c;请勿用于商业用途。 联系信箱&#xff1a;feixiaoxing 163.com】 前面几篇文章&#xff0c;我们说到了怎么下载编译器、怎么编译uboot、怎么编译kernel和根文件系统。这样一步一步下来&#xff0c;虽然繁琐&#x…

chatgpt赋能python:Python按列排序详解

Python按列排序详解 在数据处理中&#xff0c;按列排序是一个非常常见的操作。Python作为一门流行的编程语言&#xff0c;针对按列排序操作也提供了丰富的工具和库。本篇文章将介绍Python按列排序的方法和实例&#xff0c;并为读者提供一些有用的技巧。 为什么要按列排序&…

chatgpt赋能python:Python扫描二维码:优化SEO的有效方法

Python扫描二维码&#xff1a;优化SEO的有效方法 在当今数字化时代&#xff0c;二维码是一种无处不在的技术&#xff0c;用于链接到网站&#xff0c;推广产品等等。然而&#xff0c;很少有人意识到&#xff0c;优化二维码可以提高网站的搜索引擎优化&#xff08;SEO&#xff0…

MySQL运维篇(一)

一.日志 1.1 错误日志 错误日志是 MySQL 中最重要的日志之一&#xff0c;它记录了当 mysqld 启动和停止时&#xff0c;以及服务器在运行过程中发生任何严重错误时的相关信息。当数据库出现任何故障导致无法正常使用时&#xff0c;建议首先查看此日志。 错误日志是默认开启的…

GaussDB内存过载分析

问题现象 数据库进程内存占比较高 长时间占比较高 观察监控平台内存占用的变化曲线&#xff0c;无论当前数据库是否有业务在运行&#xff0c;数据库进程内存占总机器内存的比例长时间处于较高状态&#xff0c;且不下降。执行作业期间占比较高 数据库进程在没有业务执行时&…

chatgpt赋能python:Python文件操作-查找指定内容

Python 文件操作 - 查找指定内容 在日常开发和数据处理中&#xff0c;我们经常需要查找文件中指定的内容。Python 提供了简单而强大的文件操作函数和模块&#xff0c;使得文件查找操作变得简单和高效。本文将介绍如何使用 Python 查找指定内容的方法。 搜索整个文件 最基本的…

2023年最好的10+个WordPress表格插件

WordPress表格插件可让您简洁明了地呈现数据。借助交互式表格&#xff0c;访问者可以根据自己的喜好轻松查看、过滤和排序您的数据&#xff0c;从而提升您网站的用户体验。 但是&#xff0c;询问任何尝试从头开始构建表格的站点所有者&#xff0c;他们会报告说体验可能是一个挑…

Rust每日一练(Leetday0012) 首末位置、插入位置、有效数独

目录 34. 查找元素的首末位置 Find-first-and-last-position-of-element-in-sorted-array &#x1f31f;&#x1f31f; 35. 搜索插入位置 Search Insert Position &#x1f31f; 36. 有效的数独 Valid Sudoku &#x1f31f;&#x1f31f; &#x1f31f; 每日一练刷题专栏…

【Python】csv与json,哪个才是你的数据之选?

知识目录 一、写在前面✨二、读写csv文件2.1 什么是CSV文件2.2 csv文件的优点2.3 应用 三、读取json文件3.1 json介绍3.2 例题 四、总结撒花&#x1f60a; 一、写在前面✨ 大家好&#xff01;我是初心&#xff0c;希望我们一路走来能坚守初心&#xff01; 今天跟大家分享的文…

C语言函数大全-- y 开头的函数

C语言函数大全 y 开头的函数1. yperror1.1 函数说明1.2 演示示例 2. yp_match2.1 函数说明2.2 演示示例 3. y0【零阶第二类贝塞尔函数】3.1 函数说明3.2 演示示例3.3 运行结果 4. y1【一阶第二类贝塞尔函数】4.1 函数说明4.2 演示示例4.3 运行结果 5. yn【n 阶第二类贝塞尔函数…

chatgpt赋能python:Python捕获多个异常:提高程序的健壮性

Python 捕获多个异常&#xff1a;提高程序的健壮性 在编写Python程序时&#xff0c;我们经常会遇到各种异常情况。如果没有适当的异常处理机制&#xff0c;程序就会因为异常而崩溃。为了提高程序的健壮性&#xff0c;我们需要对可能出现的异常情况进行处理。在Python中&#x…

Linux及其常用命令

学习 遇见狂神说 为什么Linux这么重要&#xff1f;一些目录的常识Linux常用命令目录管理文件属性文件内容链接Vim编辑器账号管理用户组管理 为什么Linux这么重要&#xff1f; 因为在企业级开发中&#xff0c;我们的操作对象都是服务器&#xff0c;不是自己的Windows主机了。这…

Anaconda使用总结(conda操作,环境操作,包管理)

Anaconda使用总结 配合Pycharm使用conda命令conda操作环境操作包管理Anaconda源下载包&#xff1a;whl文件本地安装Github源安装PIP和Condaconda换源 其他 背景&#xff1a;Anaconda作为深度学习最流行的pipeline之一&#xff0c;可以方便的修改和导出学习环境&#xff0c;每次…

Adobe推出了PS新功能Generative Fill(创成式填充);生成式 AI 将改变电脑架构;

&#x1f680; Adobe推出了PS新功能Generative Fill&#xff08;创成式填充&#xff09;&#xff0c;利用生成式AI来增删图像中的任何一处细节。 Adobe推出了PS新功能Generative Fill&#xff08;创成式填充&#xff09;&#xff0c;利用生成式AI来增删图像中的任何一处细节。…

叮咚音乐门铃芯片方案推荐 WTN6006-8S 低功耗 高性价比

​ 随着物联网技术的不断发展&#xff0c;智能家居已经成为了生活中不可或缺的一部分。作为智能家居中的重要组成部分&#xff0c;门铃同样需要进行智能化升级&#xff0c;在改善用户体验、保障家庭安全方面起到了重要作用。本文将介绍一种基于音乐芯片的叮咚门铃应用方案…

[数据集][目标检测]目标检测数据集大白菜数据集VOC格式1557张

数据集格式&#xff1a;Pascal VOC格式(不包含分割路径的txt文件和yolo格式的txt文件&#xff0c;仅仅包含jpg图片和对应的xml) 图片数量(jpg文件个数)&#xff1a;1557 标注数量(xml文件个数)&#xff1a;1557 标注类别数&#xff1a;1 标注类别名称:["cabbage"] 每…

chatgpt赋能python:Python编程中的按键事件

Python编程中的按键事件 在Python编程中&#xff0c;按键事件是一项非常有用的功能。通过监控按下键盘中的特定键&#xff0c;您可以触发程序的某些特定操作&#xff0c;这些操作可用于增强应用程序的功能和用户体验。 按键事件的基本工作原理 Python中的按键事件基于图形用…

Java——TCP UDP Socket编程

目录 一、网络的相关概念 &#xff08;一&#xff09;网络通信 &#xff08;二&#xff09;网络 &#xff08;三&#xff09;ip地址 &#xff08;四&#xff09;ipv4地址分类 &#xff08;五&#xff09;域名 &#xff08;六&#xff09;网络通信协议 &#xff08;七&a…

数据结构 第四章:串

文章目录 一、串的定义和实现1.1串的定义和基本操作1.1.1串的定义1.1.2串的基本操作1.1.3小结 1.2串的存储结构1.2.1顺序存储1.2.2链式存储1.2.3基于顺序存储实现基本操作1.2.4小结 二、串的模式匹配2.1什么是字符串的模式匹配2.2朴素模式匹配算法2.3KMP算法2.4求next数组2.5KM…

python+vue空巢老人网上药店购药系统9h2k5

本空巢老人购药系统主要包括三大功能模块&#xff0c;即用户功能模块、家属功能模块和管理员功能模块。 &#xff08;1&#xff09;管理员模块&#xff1a;系统中的核心用户是管理员&#xff0c;管理员登录后&#xff0c;通过管理员功能来管理后台系统。主要功能有&#xff1a;…