当前位置: 首页 > news >正文

【数据结构】线性表之“双链表(带头循环双向链表)”

- 第 99 篇 -
Date: 2025 - 05 - 25
Author: 郑龙浩/仟墨
【数据结构】
续上一篇: 线性表之“单链表”

文章目录

  • “双链表(带头双向循环链表)” 的实现:
  • 分步解释所有函数:
  • test.c
  • DListNode.h
  • DListNode.c

“双链表(带头双向循环链表)” 的实现:

  • 带头:带头结点
  • 循环:链表中的结点是循环的,头结点前驱是最后一个结点,最后一个结点后继是头结点
  • 双向:每一个结点都有存储前驱结点和后继结点的指针域

分步解释所有函数:

  1. 申请新的结点

    封装这个函数是因为在后边插入的时候多次使用了下面的代码,造成了代码冗余,所以将申请新结点的代码封装起来,用的时候直接调用即可

    1. malloc申请新的空间,宽度为 sizef(DListNode)
    2. 判断malloc内存分配是否成功,失败则退出(概论很小)
    3. 设置结点数据域,将元素 x 存入该结点的 data
    4. 初始化指针域,前驱后继全部都指向自己(完成闭环)
    5. 返回申请的结点的地址
    // 1 申请新的结点
    DListNode* BuyDListNode(ElemType x) {DListNode* NewNode = (DListNode*)malloc(sizeof(DListNode)); // 申请新的结点if (NewNode == NULL) {printf("内存申请失败!\n");exit(EXIT_FAILURE);}NewNode->data = x; // 存储数据NewNode->prev = NewNode;  // 初始时指向自己NewNode->next = NewNode;  // 初始时指向自己return NewNode;
    }
    
  2. 带头结点的双向循环链表初始化 – 创建并返回头结点,即返回哨兵结点

    这个函数的作用和 BuyDListNode 很像,但是意思上不一样!

    • DListInit() 有很强的语义约束 –> 专门用于初始化,而非开辟新的结点的内存空间

    • 返回的头结点指针具有特殊作用(哨兵结点),不是普通节点(普通结点的data存储数据,而哨兵结点的data不存储数据,默认为0

      返回创建的头结点指针,该头结点代表一个空链表

    • 写函数的时候,可以调用申请结点的函数来实现头节点的创建

    步骤:

    1. 调用 BuyDListNode(0) 创建头结点(data 无实际意义)
    2. 由于 BuyDListNode 已让 prevnext 指向自己,无需额外操作
    3. 返回头结点指针
    // 2 带头结点的双向循环链表初始化 -- 创建并返回头结点,即返回哨兵结点
    DListNode* DListInit() {DListNode* NewNode = BuyDListNode(0); // 申请新的结点 // 头结点的 data 无意义,仅作哨兵// 因为BuyDListNode(0_中已经指向了自己所以下面代码可写可不写//NewNode->prev = NewNode; // 新结点前驱指向自己//NewNode->next = NewNode; // 新结点后继指向自己return NewNode; // 返回头结点(哨兵结点)
    }
    
  3. 双向链表清除 – 只释放头节点以外的结点

    清除的意思是保留双链表本身,但是双链表中保存数据的结点要清除(不清除头结点)

    步骤:

    1. 检查头结点有效性(assert),若phead中存储的地址为NULL,则试图访问phead->next会导致程序崩溃,所以需要拦截非法操作
    2. 释放哨兵结点以外的结点(存储数据的结点)
    3. 初始化指针域,将哨兵结点的前驱和后继全部指向自己,完成闭环
    // 3 双向链表清除 -- 只释放头节点以外的结点
    void DListClear(DListNode* phead) {assert(phead); // 防止空指针DListNode* cur = phead->next; // 存放当前的结点的地址while (cur != phead) { // 释放处头结点(哨兵结点)以外的结点DListNode* next = cur->next; // 保存下一个结点地址(防止释放后地址丢失)free(cur); // 释放当前结点内存空间cur = next; // 指向下一个结点}cur->next = cur->prev = cur;
    }
    
  4. 双向链表销毁

    DListClear基础上增加头结点内存释放

    但是销毁的时候要用到二级指针,因为二级指针才能将存放头结点地址的二级指针变量置为NULL

    步骤:

    1. 检查头结点有效性(assert),phead中存储的地址为NULL,则试图访问phead->next会导致程序崩溃,所以需要拦截非法操作
    2. 释放哨兵结点以外的结点(存储数据的结点),使用DListClear
    3. 释放头结点内存空间
    4. phead初始化,即phead中存储的地址为NULL
    // 4 双向链表销毁 -- 销毁要用双指针
    void DListDestroy(DListNode** phead) {assert(*phead); // 防止空指针DListClear(*phead); // 只释放头节点以外的结点free(*phead); // 释放头结点*phead = NULL; // 重新置为NULL
    }
    
  5. 头插

    头插操作是在 哨兵结点(头结点)与 第一个结点之间插入一个新的结点,并且将新的结点与头结点和原第一个结点连接起来

    步骤:

    1. 检查头结点有效性(assert),若phead中存储的地址为NULL,则试图访问phead->next会导致程序崩溃,所以需要拦截非法操作
    2. 申请新的结点
    3. 新结点前驱指向头结点
    4. 新节点后继指向原第一个结点
    5. 原第一结点前驱指向新的结点
    6. 头结点后继指向新的结点
    // 5 头插
    void DListPushFront(DListNode* phead, ElemType x) {assert(phead != NULL); // 避免空指针,确保phead不是NULL// 因为带头双向循环链表是有头结点的,而头结点不存储数据始终为NULL,即不存在头结点给头结点添加数据的情况DListNode* NewNode = BuyDListNode(x); // 申请新的结点// 头插就是让新的结点插到 “哨兵结点(头结点)”与“第一个结点”之间DListNode* first = phead->next; // 表示第一个结点NewNode->prev = phead;	// 1 新结点前驱指向头结点NewNode->next = first;	// 2 新节点后继指向原第一个结点first->prev = NewNode;	// 3 原第一结点前驱指向新的结点phead->next = NewNode;	// 4 头结点后继指向新的结点
    }
    
  6. 尾插

    尾插操作就是在最后一个结点后边插入一个新的结点,并且将新节点与原最后一个结点和头结点连接起来

    步骤:

    1. 检查头结点有效性(assert),若phead中存储的地址为NULL,则试图访问phead->next会导致程序崩溃,所以需要拦截非法操作
    2. 申请新的结点
    3. 新节点前驱指向尾节点
    4. 新节点后继指向头节点
    5. 尾节点后继指向新节点
    6. 头节点前驱指向新节点
    // 6 尾插
    void DListPushBack(DListNode* phead, ElemType x) {assert(phead != NULL); // 避免空指针,确保phead不是NULLDListNode* NewNode = BuyDListNode(x); // 申请新的结点// 尾插就是将新的结点放到 tail 的后边DListNode* tail = phead->prev; // 尾结点NewNode->prev = tail;	// 1 新节点前驱指向尾节点NewNode->next = phead;	// 2 新节点后继指向头节点tail->next = NewNode;	// 3 尾节点后继指向新节点phead->prev = NewNode;	// 4 头节点前驱指向新节点
    }
    
  7. 头删

    头删就是将第一个结点(不是删除哨兵结点)的内存空间释放,并且将头结点与原第二个结点连接起来

    步骤:

    1. 检查头结点有效性(assert),若phead中存储的地址为NULL,则试图访问phead->next会导致程序崩溃,所以需要拦截非法操作
    2. 检查链表是否只有一个头结点(哨兵结点),如果是的话,则不删除,直接return
    3. 头结点后继指向第二个结点
    4. 第二个结点前驱指向头结点
    5. 释放第一个结点
    // 7 头删
    void DListPopFront(DListNode* phead) {assert(phead != NULL); // 避免空指针,确保phead不是NULLif (phead->next == phead) return; // 如果只有头结点(哨兵结点),头结点中不存储data,不删除,直接返回// 切记:头删不是删除“头结点(哨兵结点)”,而是删除第一个结点DListNode* first = phead->next; // 第一个结点(保护要释放的结点的地址,不然重新指向以后,地址丢失)DListNode* second = first->next; // 第二个结点phead->next = second;	// 1 头结点后继指向第二个结点second->prev = phead;	// 2 第二个结点前驱指向头结点free(first);			// 3 释放第一个结点
    }
    
  8. 尾删

    尾删就是将最后一个结点内存空间释放, 并且将原倒数第二个结点与头结点连接起来

    步骤:

    1. 检查头结点有效性(assert),若phead中存储的地址为NULL,则试图访问phead->next会导致程序崩溃,所以需要拦截非法操作
    2. 检查链表是否只有一个头结点(哨兵结点),如果是的话,则不删除,直接return
    3. 头结点前驱指向倒数第二个结点
    4. 倒数第二个结点后继指向头结点
    5. 释放尾结点内存空间
    // 8 尾删
    void DListPopBack(DListNode* phead) {assert(phead != NULL); // 避免空指针,确保phead不是NULLif (phead->next == phead) return; // 如果只有头结点(哨兵结点),头结点中不存储data,不删除,直接返回DListNode* tail = phead->prev; // 尾结点(保护要释放的结点的地址,不然重新指向以后,地址丢失)DListNode* TailPrev = tail->prev; // 倒数第二个结点phead->prev = TailPrev;	// 1 头结点前驱指向倒数第二个结点TailPrev->next = phead;	// 2 倒数第二个结点后继指向头结点free(tail);				// 3 释放尾结点
    }
    
  9. 查找 找到返回结点地址,找不到返回NULL

    从第一个结点(不是哨兵结点)到 最后一个结点找结点

    步骤:

    1. 检查头结点有效性(assert),若phead中存储的地址为NULL,则试图访问phead->next会导致程序崩溃,所以需要拦截非法操作
    2. 从第一个结点找到最后一个结点,如果找到了datax的结点,就返回该结点的地址
    3. 没找到则返回NULL
    // 9 查找 找到返回结点地址,找不到返回NULL
    DListNode* DListFind(DListNode* phead, ElemType x) {assert(phead); // 避免空指针,确保phead不是NULL// 切记:头结点不存储数据,查找无需找头结点DListNode* cur = phead->next;// 从第一个结点到最后一个结点 (结束条件为:cur 走到了头结点)while (cur != phead) {if (cur->data == x) {return cur; // 找到了,返回cur地址}cur = cur->next; // 指向下一个结点}return NULL; // 若没找到,则返回NULL
    }
    
  10. 在 pos 的前面进行插入

    pos 前面插入 数据为 x 的结点,就是在 pos 前驱与 pos 之间插入数据,并将新节点 与 pos 前驱后继连接起来

    步骤:

    1. 检查头结点有效性(assert),若phead中存储的地址为NULL,则试图访问phead->next会导致程序崩溃,所以需要拦截非法操作
    2. 验证pos是否属于某个链表(通过检查闭环)
    3. 申请新的结点
    4. 新结点的前驱指向 原pos前驱结点
    5. 新节点的后继指向 pos结点
    6. 原pos的前驱结点后继指向 新的结点
    7. 原pos的前驱指向 新的结点
    // 10 在pos的前面进行插入(不包括头结点,因为头结点不存储数据)
    void DListInsert(DListNode* pos, ElemType x) {assert(pos != NULL); // 避免空指针,检查pos指针是否为NULL// 验证pos是否属于某个链表(通过检查闭环)assert(pos->next != pos && pos->prev != pos);  // 确保不是孤立节点DListNode* NewNode = BuyDListNode(x); // 申请新的结点// pos 前面插入 数据为 x 的结点,就是在 pos 前驱与 pos 之间插入数据DListNode* PosPrev = pos->prev; // pos 的前驱结点NewNode->prev = PosPrev;	// 1 新结点的前驱指向 原pos前驱结点NewNode->next = pos;	// 2 新节点的后继指向 pos结点PosPrev->next = NewNode;	// 3 原pos的前驱结点后继指向 新的结点pos->prev = NewNode;		// 4 原pos的前驱指向 新的结点
    }
    
  11. 删除 pos 位置的节点

    删除pos结点就是将pos的前驱结点和后继结点连接起来,并且释放pos结点

    步骤:

    1. 检查头结点有效性(assert),若phead中存储的地址为NULL,则试图访问phead->next会导致程序崩溃,所以需要拦截非法操作
    2. 验证pos是否属于某个链表(通过检查闭环)
    3. 新结点的前驱指向 原pos前驱结点
    4. 新节点的后继指向 pos结点
    5. 原pos的前驱结点后继指向 新的结点
    6. 原pos的前驱指向 新的结点
    // 11 删除pos位置的结点(不包括头结点,因为头结点不存储数据)
    void DListErase(DListNode* pos) {assert(pos != NULL); // 避免空指针,检查pos指针是否为NULL && 断言指向头结点(哨兵结点)// 验证pos是否属于某个链表(通过检查闭环)assert(pos->next != pos && pos->prev != pos);  // 确保不是孤立节点// 删除pos结点就是将pos的前驱结点和后继结点连接起来,并且释放pos结点pos->prev->next = pos->next;	// 1 pos的前驱结点后继指向pos的后继结点pos->next->prev = pos->prev;	// 2 pos的后继结点前驱指向pos的前驱结点free(pos);						// 3 释放 pos 结点的内存空间
    }
    
  12. 打印

    从第一个结点打印到最后一个结点

    步骤:

    1. 检查头结点有效性(assert),若phead中存储的地址为NULL,则试图访问phead->next会导致程序崩溃,所以需要拦截非法操作
    2. 验证pos是否属于某个链表(通过检查闭环)
      1. 检查是否只有一个结点(哨兵结点),如果是的话则打印Empty List,并且跳过后边代码,直接return
    3. 从第一个结点打印到最后一个结点(结束条件为cur为哨兵结点「头结点」)
    4. 打印结尾HEAD
    // 12 打印
    void DListPrint(DListNode* phead) {assert(phead); // 避免空指针,检查头指针是否为NULLDListNode* cur = phead->next; // 存储当前结点地址// 若是头结点if (phead->next == phead) {printf("Empty List\n");return;}// 从第一个结点打印到最后一个结点(结束条件为cur为哨兵结点「头结点」)while (cur != phead) {printf("%d -> ", cur->data); // 打印当前结点的数据cur = cur->next; // 指向下一个结点}printf("HEAD\n"); // 最后打印结束
    }
    

test.c

#define _CRT_SECURE_NO_WARNINGS
#include "DListNode.h"
void Check1();
int main(void) {Check1();return 0;
}// 测试1 
void Check1() {// 创建链表DListNode* L = DListInit(); // 新建链表 -- 必须要让L头结点为DListInit(),如果指向NULL会越界访问// 尾插 1 ~ 5printf("尾插1 2 3 4 5\n");for (int i = 1; i <= 5; i++) {DListPushBack(L, i); // 每次尾插}DListPrint(L); // 打印链表// 头插 1 ~ 5printf("头插11 22 33 44 55\n");for (int i = 1; i <= 5; i++) {DListPushFront(L, i*10 + i); // 每次头插}DListPrint(L); // 打印链表// 头删 5 次printf("头删 5 次\n");for (int i = 1; i <= 5; i++) {DListPopFront(L); // 每次头删}DListPrint(L); // 打印链表// 尾删 2 次printf("尾删 2 次\n");for (int i = 1; i <= 2; i++) {DListPopBack(L); // 每次尾插}DListPrint(L); // 打印链表// 在 3 之前插入 33printf("在 3 之前插入 33\n");DListInsert(DListFind(L, 3), 23);DListPrint(L); // 打印链表// 删除数据为 23 的结点printf("删除数据为 23 的结点\n");DListErase(DListFind(L, 23));DListPrint(L); // 打印链表// 清除双链表(保留哨兵)printf("清除双链表(保留哨兵)\n");DListClear(L);if (L->next == L && L->prev == L) {printf("链表已经清除\n");}// 销毁双链表printf("销毁双链表(不保留哨兵)\n");DListDestroy(&L);if (L == NULL) {printf("链表已经销毁\n");}
}

DListNode.h

#define _CRT_SECURE_NO_WARNINGS
#include "stdio.h"
#include "stdlib.h"
#include "assert.h"
typedef int ElemType;
typedef struct DListNode {ElemType data; // 数据struct DListNode* prev; // 指向前驱节点(前驱结点的地址)struct DListNode* next; // 指向后驱结点(后驱结点的地址)
}DListNode;// 申请新的结点
DListNode* BuyDListNode(ElemType x);
// 带头结点的双向循环链表初始化 -- 创建并返回头结点,即返回哨兵结点
DListNode* DListInit();
// 双向链表清除 -- 只释放头节点以外的结点
void DListClear(DListNode* phead);
// 双向链表销毁
void DListDestroy(DListNode** phead);
// 头删头插要重新更新头指针
// 头插
void DListPushFront(DListNode* phead, ElemType x);
// 尾插
void DListPushBack(DListNode* phead, ElemType x);
// 头删
void DListPopFront(DListNode* phead);
// 尾删
void DListPopBack(DListNode* phead);
// 查找 找到返回结点地址,找不到返回NULL
DListNode* DListFind(DListNode* phead, ElemType x);
// 在phead的前面进行插入
void DListInsert(DListNode* pos, ElemType x);
// 删除phead位置的节点
void DListErase(DListNode* pos);
// 打印
void DListPrint(DListNode* phead);

DListNode.c

#define _CRT_SECURE_NO_WARNINGS
#include "DListNode.h"// 1 申请新的结点
DListNode* BuyDListNode(ElemType x) {DListNode* NewNode = (DListNode*)malloc(sizeof(DListNode)); // 申请新的结点if (NewNode == NULL) {printf("内存申请失败!\n");exit(EXIT_FAILURE);}NewNode->data = x; // 存储数据NewNode->prev = NewNode;  // 初始时指向自己NewNode->next = NewNode;  // 初始时指向自己return NewNode;
}
// 2 带头结点的双向循环链表初始化 -- 创建并返回头结点,即返回哨兵结点
DListNode* DListInit() {DListNode* NewNode = BuyDListNode(0); // 申请新的结点 // 头结点的 data 无意义,仅作哨兵// 因为BuyDListNode(0_中已经指向了自己所以下面代码可写可不写//NewNode->prev = NewNode; // 新结点前驱指向自己//NewNode->next = NewNode; // 新结点后继指向自己return NewNode; // 返回头结点(哨兵结点)
}
// 3 双向链表清除 -- 只释放头节点以外的结点
void DListClear(DListNode* phead) {assert(phead); // 防止空指针DListNode* cur = phead->next; // 存放当前的结点的地址while (cur != phead) { // 释放处头结点(哨兵结点)以外的结点DListNode* next = cur->next; // 保存下一个结点地址(防止释放后地址丢失)free(cur); // 释放当前结点内存空间cur = next; // 指向下一个结点}cur->next = cur->prev = cur;
}
// 4 双向链表销毁 -- 销毁要用双指针
void DListDestroy(DListNode** phead) {assert(*phead); // 防止空指针DListClear(*phead); // 只释放头节点以外的结点free(*phead); // 释放头结点*phead = NULL; // 重新置为NULL
}
// 5 头插
void DListPushFront(DListNode* phead, ElemType x) {assert(phead != NULL); // 避免空指针,确保phead不是NULL// 因为带头双向循环链表是有头结点的,而头结点不存储数据始终为NULL,即不存在头结点给头结点添加数据的情况DListNode* NewNode = BuyDListNode(x); // 申请新的结点// 头插就是让新的结点插到 “哨兵结点(头结点)”与“第一个结点”之间DListNode* first = phead->next; // 表示第一个结点NewNode->prev = phead;	// 1 新结点前驱指向头结点NewNode->next = first;	// 2 新节点后继指向原第一个结点first->prev = NewNode;	// 3 原第一结点前驱指向新的结点phead->next = NewNode;	// 4 头结点后继指向新的结点
}
// 6 尾插
void DListPushBack(DListNode* phead, ElemType x) {assert(phead != NULL); // 避免空指针,确保phead不是NULLDListNode* NewNode = BuyDListNode(x); // 申请新的结点// 尾插就是将新的结点放到 tail 的后边DListNode* tail = phead->prev; // 尾结点NewNode->prev = tail;	// 1 新节点前驱指向尾节点NewNode->next = phead;	// 2 新节点后继指向头节点tail->next = NewNode;	// 3 尾节点后继指向新节点phead->prev = NewNode;	// 4 头节点前驱指向新节点
}
// 7 头删
void DListPopFront(DListNode* phead) {assert(phead != NULL); // 避免空指针,确保phead不是NULLif (phead->next == phead) return; // 如果只有头结点(哨兵结点),头结点中不存储data,不删除,直接返回// 切记:头删不是删除“头结点(哨兵结点)”,而是删除第一个结点DListNode* first = phead->next; // 第一个结点(保护要释放的结点的地址,不然重新指向以后,地址丢失)DListNode* second = first->next; // 第二个结点phead->next = second;	// 1 头结点后继指向第二个结点second->prev = phead;	// 2 第二个结点前驱指向头结点free(first);			// 3 释放第一个结点
}
// 8 尾删
void DListPopBack(DListNode* phead) {assert(phead != NULL); // 避免空指针,确保phead不是NULLif (phead->next == phead) return; // 如果只有头结点(哨兵结点),头结点中不存储data,不删除,直接返回DListNode* tail = phead->prev; // 尾结点(保护要释放的结点的地址,不然重新指向以后,地址丢失)DListNode* TailPrev = tail->prev; // 倒数第二个结点phead->prev = TailPrev;	// 1 头结点前驱指向倒数第二个结点TailPrev->next = phead;	// 2 倒数第二个结点后继指向头结点free(tail);				// 3 释放尾结点
}
// 9 查找 找到返回结点地址,找不到返回NULL
DListNode* DListFind(DListNode* phead, ElemType x) {assert(phead); // 避免空指针,确保phead不是NULL// 切记:头结点不存储数据,查找无需找头结点DListNode* cur = phead->next;// 从第一个结点到最后一个结点 (结束条件为:cur 走到了头结点)while (cur != phead) {if (cur->data == x) {return cur; // 找到了,返回cur地址}cur = cur->next; // 指向下一个结点}return NULL; // 若没找到,则返回NULL
}
// 10 在pos的前面进行插入(不包括头结点,因为头结点不存储数据)
void DListInsert(DListNode* pos, ElemType x) {assert(pos != NULL); // 避免空指针,检查pos指针是否为NULL// 验证pos是否属于某个链表(通过检查闭环)assert(pos->next != pos && pos->prev != pos);  // 确保不是孤立节点DListNode* NewNode = BuyDListNode(x); // 申请新的结点// pos 前面插入 数据为 x 的结点,就是在 pos 前驱与 pos 之间插入数据DListNode* PosPrev = pos->prev; // pos 的前驱结点NewNode->prev = PosPrev;	// 1 新结点的前驱指向 原pos前驱结点NewNode->next = pos;	// 2 新节点的后继指向 pos结点PosPrev->next = NewNode;	// 3 原pos的前驱结点后继指向 新的结点pos->prev = NewNode;		// 4 原pos的前驱指向 新的结点
}
// 11 删除pos位置的结点(不包括头结点,因为头结点不存储数据)
void DListErase(DListNode* pos) {assert(pos != NULL); // 避免空指针,检查pos指针是否为NULL && 断言指向头结点(哨兵结点)// 验证pos是否属于某个链表(通过检查闭环)assert(pos->next != pos && pos->prev != pos);  // 确保不是孤立节点// 删除pos结点就是将pos的前驱结点和后继结点连接起来,并且释放pos结点pos->prev->next = pos->next;	// 1 pos的前驱结点后继指向pos的后继结点pos->next->prev = pos->prev;	// 2 pos的后继结点前驱指向pos的前驱结点free(pos);						// 3 释放 pos 结点的内存空间
}
// 12 打印
void DListPrint(DListNode* phead) {assert(phead); // 避免空指针,检查头指针是否为NULLDListNode* cur = phead->next; // 存储当前结点地址// 若是头结点if (phead->next == phead) {printf("Empty List\n");return;}// 从第一个结点打印到最后一个结点(结束条件为cur为哨兵结点「头结点」)while (cur != phead) {printf("%d -> ", cur->data); // 打印当前结点的数据cur = cur->next; // 指向下一个结点}printf("HEAD\n"); // 最后打印结束
}

相关文章:

  • 这个方法关闭PowerBI账户的安全默认值
  • 【Android】基于SurfaceControlViewHost实现跨进程渲染
  • IEEE链接Overleaf
  • 代码随想录第43天:图论4(最小生成树、拓扑排序)
  • 代码随想录算法训练营 Day53 图论Ⅳ 字符串接龙 有向图 岛屿周长
  • 第5章:任务间通信机制(IPC)全解析
  • 云手机安卓12哪个好?掌派云手机安卓12系统上线,开启流畅体验新纪元
  • 小程序使用web-view 修改顶部标题 安全认证文件部署在nginx
  • 从零开始:Python语言进阶之迭代器
  • NotePad++编辑Linux服务器文档
  • uni-app学习笔记十--vu3 computed的运用(二)
  • 图论核心:深度搜索DFS 与广度搜索BFS
  • <uniapp><threejs>在uniapp中,怎么使用threejs来显示3D图形?
  • 无心剑中译戴维斯《悠闲》
  • uni-app学习笔记十四-vue3中emit的使用
  • rabbitmq的使用介绍
  • 【TDengine源码阅读】举例说明pthread_once_t和PTHREAD_ONCE_INIT
  • PPT连同备注页(演讲者模式)一块转为PDF
  • 深入浅出IIC协议 - 从总线原理到FPGA实战开发 -- 第六篇:AXI4-Lite桥接设计
  • 鸿蒙仓颉开发语言实战教程:页面跳转和传参
  • 网站建设 python/seo优化的技巧
  • 在线购物商城平台/我是seo关键词
  • 简单的网页设计作品欣赏/seo模拟点击工具
  • 网站建设后台管理/百度关键词搜索趋势
  • 网站的主要功能/企业网络推广方式
  • 网站怎么做百度商桥/江苏疫情最新消息