嵌入式Linux学习 | 数据结构 (Day03)顺序表与单链表 超详细解析(含 C 语言实现 + 作业 + 避坑指南)

作为数据结构的开篇核心知识点,顺序表和单链表是线性表的两种最基础实现形式,二者相辅相成、优缺点互补,是所有后端开发、算法学习的核心地基,也是校招、初阶面试的高频考点。

一、顺序表(线性结构的顺序存储结构)

1. 核心定义(小白必懂)

顺序表是线性表的顺序存储结构 ,它在计算机内存中开辟一段连续的、固定大小的地址空间 ,按照线性表中元素的逻辑顺序,依次将数据元素存储在这段连续地址中。简单来说,顺序表的本质就是数组,但和普通数组不同的是:顺序表会严格维护元素的 "线性逻辑关系"(即元素之间的前后顺序),且通常会配套实现增删改查等完整操作,是一种 "封装好的数组应用"。

2. 核心优势(结合实际场景)

  1. 有序后查找效率极高,支持随机访问 顺序表的最大优势的是支持 "随机访问"------ 只要知道元素的下标(索引),就能通过公式 地址 = 起始地址 + 下标 × 元素大小 直接定位到元素,时间复杂度为 O(1) 。比如:在一个有序的顺序表中查找某个元素,配合二分查找算法,时间复杂度可降至 O(log₂n),这也是顺序表在 "查找频繁" 场景中(如学生成绩查询、字典检索)的核心价值。
  2. 结构简单,易于实现和调试底层依托数组实现,无需复杂的指针操作,代码逻辑简洁,上手成本极低。无论是 C 语言、Java 还是 Python,都能快速实现顺序表的基础功能,适合新手入门练手。
  3. 内存开销小,访问速度快顺序表的元素存储连续,CPU 缓存命中率更高(缓存会预加载连续内存的数据),相比链表的分散存储,访问速度更有优势。

3. 核心劣势(深度剖析,直击痛点)

  1. 插入、删除元素效率极低,数据量越大越明显 顺序表的核心特性是 "内存地址连续",而这也是它插入、删除低效的根源。为了保证插入 / 删除后,元素依然保持连续的地址和线性逻辑,必须批量移动后续所有元素:
    • 头部插入 / 删除:需要移动全部元素(比如在数组 [1,2,3,4] 头部插入 0,需将 1、2、3、4 依次后移一位);
    • 中间插入 / 删除:需要移动从插入位置到末尾的所有元素(比如在 [1,2,3,4] 中间插入 5 到索引 2,需将 3、4 后移一位);
    • 尾部插入 / 删除:无需移动元素(直接在末尾添加 / 删除),但这种情况仅为特例。总体而言,插入 / 删除的时间复杂度为 O(n),当数据量达到 10 万、100 万级时,效率会急剧下降。
  2. 内存空间固定,利用率低,易造成浪费或溢出 顺序表初始化时,需要提前分配固定大小的内存空间:
    • 若分配的空间过大,会造成内存浪费(比如只存 10 个元素,却分配了 1000 个元素的空间);
    • 若分配的空间过小,当元素数量超过空间大小时,会出现 "内存溢出",无法继续存储数据。虽然可以通过 "扩容"(重新分配更大的连续空间,将原数据拷贝过去)解决溢出问题,但扩容过程会消耗额外的时间和内存(拷贝数据的时间复杂度为 O (n))。
  3. 延伸思考(面试常考) 问:如果地址不连续,是不是就能规避插入删除低效的问题?答:完全可以 !这就是我们接下来要讲的链式存储结构(链表) ------ 链表放弃了 "地址连续" 的要求,用指针串联分散的元素,彻底解决了顺序表插入删除低效的痛点,但同时也牺牲了部分查找效率,这就是 "取舍" 的设计思想。

二、线性结构的链式存储结构 ------ 链表

顺序表的短板,恰好是链表的优势;链表的短板,也恰好是顺序表的优势。二者的对比学习,是理解 "数据结构设计思想" 的关键。

1. 基础概念(拆解通俗,避免晦涩)

  • 链表:线性表的链式存储结构 ,数据元素(结点)在内存中地址不连续,分散存储在内存的各个角落。
  • 单链表:只有一条 "后继指针链" 的链表,是最基础、最常用的链表结构(后续的双链表、循环链表,都是在单链表基础上拓展而来)。
  • 什么是「链」?核心就是指针(或引用) ------ 指针就像 "绳子",将分散在内存中的各个数据结点串联起来,维系元素之间的线性逻辑关系,这也是 "链表" 名字的由来。

2. 单链表核心原理(图文级拆解)

(1)结点(Node)------ 链表的最小单元

单链表的每一个数据成员,称为一个结点,每个结点都包含两部分,缺一不可:

  1. 数据域(data):存储真实的数据(比如 int、char、结构体等任意类型数据);
  2. 指针域(next):存储下一个结点的内存地址,通过这个指针,将当前结点与下一个结点关联起来。
(2)地址不连续,如何维系线性结构?(核心难点)

链表的结点在内存中是分散的,没有连续的地址,它之所以能保持 "线性结构"(元素前后有序),核心靠指针的关联

  1. 每个结点的指针域(next),都指向它的后继结点(下一个结点)的地址;
  2. 相对于当前结点,上一个结点称为前驱结点(单链表中,前驱结点无法直接获取,只能通过遍历查找);
  3. 链表的最后一个结点(尾结点),其指针域(next)指向 NULL(空指针),表示链表的结束;
  4. 为了方便管理整个链表,我们通常会设置一个头结点(不存储真实数据),头结点的指针域指向链表的第一个数据结点(首元结点)。
(3)头结点的作用(避坑重点)

很多新手会忽略头结点,导致链表操作出现 bug,这里明确头结点的 3 个核心作用:

  1. 统一操作:无论链表是否为空,头结点都存在,这样插入、删除的代码逻辑可以统一(无需单独处理 "空链表" 的特殊情况);
  2. 避免混乱:头结点不存储数据,仅用于指向首元结点,避免将首元结点误当作 "头",导致链表操作出错;
  3. 存储链表信息:头结点中可以存储链表的额外信息(如本文后续实现中的 "数据元素大小"),方便管理链表。

三、单链表 C 语言 完整实现(ADT + 接口函数 + 详细注释)

我们采用抽象数据类型 (ADT) 设计,通用性极强,可存储任意类型数据(int、char、结构体等),代码包含详细注释,小白也能看懂,复制就能编译运行。

1. 头文件与数据结构定义

<stdio<stdlib<string.h>

// 定义比较函数指针类型(用户自定义数据比较规则,适配任意数据类型)// 返回值:0 - 相等,非 0 - 不相等typedef int (*cmp_t)(const void *data1, const void *data2);

// 1. 单链表数据结点结构体(存储真实数据)struct node_st {void *data; // 数据域:存储任意类型数据的地址(通用性关键)struct node_st *next; // 指针域:指向后继结点的地址};

// 2. 单链表头结点结构体(管理整个链表,不存储真实数据)typedef struct {struct node_st *head; // 指向链表第一个数据结点(首元结点)int size; // 数据元素的大小(字节数),用于内存分配} listhead_t;

cs 复制代码
### 2. 核心接口函数实现(含异常处理,避免bug)
#### (1)初始化链表头结点
```c
/**
 * @brief  初始化链表头结点
 * @param  list:指向头结点指针的指针(用于修改外部头结点指针)
 * @param  size:数据元素的大小(字节数)
 * @return 0-初始化成功,-1-初始化失败(内存分配失败)
 */
int listhead_init(listhead_t **list, int size) {
    // 1. 校验参数(避免空指针异常)
    if (list ==<= 0) {
        printf("初始化失败:参数错误(list为空或size非法)\n");
        return -1;
    }

    // 2. 为头结点分配内存
    *list = (listhead_t *)malloc(sizeof(listhead_t));
    if (*list == NULL) {
        printf("初始化失败:内存分配失败\n");
        return -1;
    }

    // 3. 初始化头结点成员
    (*list)->head = NULL;  // 初始时链表为空,头结点不指向任何数据结点
    (*list)->size = size;  // 记录数据元素大小

    printf("链表头结点初始化成功\n");
    return 0;
}
(2)增:头插法(在链表头部插入结点)
cs 复制代码
/**
 * @brief  头插法:在链表头部插入数据结点
 * @param  list:已初始化的头结点指针
 * @param  data:要插入的数据(传入数据的地址)
 * @return 0-插入成功,-1-插入失败(参数错误/内存分配失败)
 */
int list_add(listhead_t *list, const void *data) {
    // 1. 校验参数
    if (list == NULL || data == NULL) {
        printf("头插失败:参数错误(list或data为空)\n");
        return -1;
    }

    // 2. 为新数据结点分配内存
    struct node_st *new_node = (struct node_st *)malloc(sizeof(struct node_st));
    if (new_node == NULL) {
        printf("头插失败:内存分配失败\n");
        return -1;
    }

    // 3. 为新结点的数据域分配内存,并拷贝数据
    new_node->data = malloc(list->size);
    if (new_node->data == NULL) {
        printf("头插失败:数据域内存分配失败\n");
        free(new_node);  // 释放已分配的结点内存,避免内存泄漏
        return -1;
    }
    memcpy(new_node->data, data, list->size);  // 拷贝数据到新结点

    // 4. 插入新结点(核心:修改指针指向)
    new_node->next = list->head;  // 新结点的next指向原首元结点
    list->head = new_node;        // 头结点的head指向新结点,新结点成为新的首元结点

    printf("头插数据成功\n");
    return 0;
}
(3)增:尾插法(在链表尾部插入结点)
cs 复制代码
/**
 * @brief  尾插法:在链表尾部插入数据结点
 * @param  list:已初始化的头结点指针
 * @param  data:要插入的数据(传入数据的地址)
 * @return 0-插入成功,-1-插入失败(参数错误/内存分配失败)
 */
int list_add_tail(listhead_t *list, const void *data) {
    // 1. 校验参数
    if (list == NULL || data == NULL) {
        printf("尾插失败:参数错误(list或data为空)\n");
        return -1;
    }

    // 2. 为新数据结点分配内存(和头插一致)
    struct node_st *new_node = (struct node_st *)malloc(sizeof(struct node_st));
    if (new_node == NULL) {
        printf("尾插失败:内存分配失败\n");
        return -1;
    }
    new_node->data = malloc(list->size);
    if (new_node->data == NULL) {
        printf("尾插失败:数据域内存分配失败\n");
        free(new_node);
        return -1;
    }
    memcpy(new_node->data, data, list->size);
    new_node->next = NULL;  // 尾结点的next必须指向NULL

    // 3. 找到链表的尾结点(遍历链表)
    struct node_st *tail = list->head;  // 用于遍历的指针
    if (tail == NULL) {
        // 链表为空(没有数据结点),直接将新结点作为首元结点
        list->head = new_node;
    } else {
        // 链表非空,遍历到最后一个结点(next为NULL的结点)
        while (tail->next != NULL) {
            tail = tail->next;
        }
        tail->next = new_node;  // 尾结点的next指向新结点
    }

    printf("尾插数据成功\n");
    return 0;
}
(4)删:按关键字删除结点
cs 复制代码
/**
 * @brief  按关键字删除结点(需用户提供比较函数)
 * @param  list:已初始化的头结点指针
 * @param  key:要删除的关键字(传入关键字地址)
 * @param  cmp:比较函数(用户自定义,判断数据与关键字是否相等)
 * @return 0-删除成功,-1-删除失败(参数错误/链表为空/未找到关键字)
 */
int list_del(listhead_t *list, const void *key, cmp_t cmp) {
    // 1. 校验参数
    if (list == NULL || key == NULL || cmp == NULL) {
        printf("删除失败:参数错误(list/key/cmp为空)\n");
        return -1;
    }

    // 2. 处理链表为空的情况
    if (list->head == NULL) {
        printf("删除失败:链表为空,无数据可删\n");
        return -1;
    }

    // 3. 遍历链表,查找要删除的结点(需要记录前驱结点)
    struct node_st *curr = list->head;    // 当前结点(要查找的结点)
    struct node_st *prev = NULL;          // 前驱结点(当前结点的上一个结点)

    while (curr != NULL) {
        // 调用比较函数,判断当前结点数据是否与关键字相等
        if (cmp(curr->data, key) == 0) {
            // 找到要删除的结点,修改指针指向,释放内存
            if (prev == NULL) {
                // 要删除的是首元结点(前驱结点为空)
                list->head = curr->next;
            } else {
                // 要删除的是中间/尾结点,前驱结点的next指向当前结点的next
                prev->next = curr->next;
            }
            // 释放当前结点的内存(先释放数据域,再释放结点)
            free(curr->data);
            free(curr);
            printf("删除关键字成功\n");
            return 0;
        }
        // 未找到,继续遍历
        prev = curr;
        curr = curr->next;
    }

    // 4. 遍历结束,未找到关键字
    printf("删除失败:未找到指定关键字\n");
    return -1;
}
(5)改:按关键字修改结点数据
cs 复制代码
/**
 * @brief  按关键字修改结点数据(需用户提供比较函数)
 * @param  list:已初始化的头结点指针
 * @param  key:要修改的关键字(传入关键字地址)
 * @param  cmp:比较函数(用户自定义)
 * @param  new_data:新的数据(传入新数据地址)
 * @return 0-修改成功,-1-修改失败(参数错误/链表为空/未找到关键字)
 */
int list_update(const listhead_t *list, const void *key, cmp_t cmp, const void *new_data) {
    // 1. 校验参数
    if (list == NULL || key == NULL || cmp == NULL || new_data == NULL) {
        printf("修改失败:参数错误(list/key/cmp/new_data为空)\n");
        return -1;
    }

    // 2. 处理链表为空的情况
    if (list->head == NULL) {
        printf("修改失败:链表为空,无数据可改\n");
        return -1;
    }

    // 3. 遍历链表,查找要修改的结点
    struct node_st *curr = list->head;
    while (curr != NULL) {
        if (cmp(curr->data, key) == 0) {
            // 找到结点,替换数据(拷贝新数据到数据域)
            memcpy(curr->data, new_data, list->size);
            printf("修改关键字成功\n");
            return 0;
        }
        curr = curr->next;
    }

    // 4. 未找到关键字
    printf("修改失败:未找到指定关键字\n");
    return -1;
}
(6)查:按关键字查找结点
cs 复制代码
/**
 * @brief  按关键字查找结点(需用户提供比较函数)
 * @param  list:已初始化的头结点指针
 * @param  key:要查找的关键字(传入关键字地址)
 * @param  cmp:比较函数(用户自定义)
 * @return 找到:返回结点数据域地址;未找到:返回NULL
 */
void *list_find(const listhead_t *list, const void *key, cmp_t cmp) {
    // 1. 校验参数
    if (list == NULL || key == NULL || cmp == NULL) {
        printf("查找失败:参数错误(list/key/cmp为空)\n");
        return NULL;
    }

    // 2. 处理链表为空的情况
    if (list->head == NULL) {
        printf("查找失败:链表为空,无数据可查\n");
        return NULL;
    }

    // 3. 遍历链表,查找结点
    struct node_st *curr = list->head;
    while (curr != NULL) {
        if (cmp(curr->data, key) == 0) {
            // 找到结点,返回数据域地址(用户可通过该地址获取数据)
            printf("找到指定关键字\n");
            return curr->data;
        }
        curr = curr->next;
    }

    // 4. 未找到关键字
    printf("查找失败:未找到指定关键字\n");
    return NULL;
}
(7)销毁整个链表(释放所有内存,避免泄漏)
cs 复制代码
/**
 * @brief  销毁整个链表,释放所有内存(头结点+所有数据结点)
 * @param  list:指向头结点指针的指针(用于修改外部头结点指针为NULL)
 */
void list_destroy(listhead_t **list) {
    // 1. 校验参数
    if (list == NULL || *list == NULL) {
        printf("销毁失败:参数错误(list为空或链表已销毁)\n");
        return;
    }

    // 2. 遍历链表,释放所有数据结点
    struct node_st *curr = (*list)->head;
    struct node_st *next = NULL;  // 记录下一个结点,避免释放后无法遍历
    while (curr != NULL) {
        next = curr->next;  // 先记录下一个结点
        free(curr->data);   // 释放数据域内存
        free(curr);         // 释放结点内存
        curr = next;        // 移动到下一个结点
    }

    // 3. 释放头结点内存,并将外部指针置为NULL(避免野指针)
    free(*list);
    *list = NULL;

    printf("链表销毁成功,所有内存已释放\n");
}

3. 测试用例(完整可运行,方便读者验证)

cs 复制代码
// 测试用例:以int类型数据为例,演示链表所有接口的使用
// 1. 自定义比较函数(int类型)
int cmp_int(const void *data1, const void *data2) {
    return *(int *)data1 - *(int *)data2;  // 相等返回0,不相等返回差值
}

// 2. 主函数测试
int main() {
    listhead_t *list = NULL;
    int size = sizeof(int);  // 数据元素为int类型,大小为4字节

    // 1. 初始化链表
    if (listhead_init(&list, size) != 0) {
        return -1;
    }

    // 2. 头插数据(10, 20, 30)
    int a = 10, b = 20, c = 30;
    list_add(list, &a);
    list_add(list, &b);
    list_add(list, &c);  // 此时链表:30→20→10→NULL

    // 3. 尾插数据(40)
    int d = 40;
    list_add_tail(list, &d);  // 此时链表:30→20→10→40→NULL

    // 4. 查找数据(20)
    int key1 = 20;
    int *find_data = (int *)list_find(list, &key1, cmp_int);
    if (find_data != NULL) {
        printf("找到的数据:%d\n", *find_data);
    }

    // 5. 修改数据(将20改为200)
    int new_data = 200;
    list_update(list, &key1, cmp_int, &new_data);  // 此时链表:30→200→10→40→NULL

    // 6. 删除数据(10)
    int key2 = 10;
    list_del(list, &key2, cmp_int);  // 此时链表:30→200→40→NULL

    // 7. 销毁链表
    list_destroy(&list);

    return 0;
}

4. 编译运行说明(小白必看)

  1. 将上述代码复制到 C 语言编译器(如 Dev-C++、VS Code)中,保存为.c文件(如linklist.c);
  2. 编译命令:gcc linklist.c -o linklist(Linux/Mac)或直接点击编译器 "运行" 按钮(Windows);
  3. 运行结果:会依次打印各操作的执行结果,验证所有接口功能正常;
  4. 注意事项:若提示 "内存分配失败",可检查编译器内存设置,或减少数据量。

四、高频编程作业(必练 + 思路详解 + 代码提示)

链表的核心在于 "指针操作",而以下两道作业题,是面试高频考点,也是巩固指针操作的最佳练习,建议大家先自己动手写,再参考思路和提示。

作业 1:单链表逆序(面试必考)

核心要求

将单链表的结点顺序完全反转(如原链表:1→2→3→4→NULL,反转后:4→3→2→1→NULL),要求时间复杂度 O (n),空间复杂度 O (1)(不允许使用额外数组 / 链表存储)。

实现思路(三步法,通俗易懂)
  1. 定义三个指针,分别指向当前结点、前驱结点、后继结点:
    • prev:指向当前结点的前驱结点(初始为 NULL);
    • curr:指向当前正在处理的结点(初始为链表首元结点);
    • next:指向当前结点的后继结点(用于保存下一个结点,避免反转后丢失)。
  2. 遍历链表,逐个反转指针指向:
    • 先保存当前结点的后继结点(next = curr->next);
    • 将当前结点的指针域指向前驱结点(curr->next = prev);
    • 移动前驱和当前指针(prev = curr; curr = next);
  3. 遍历结束后,修改头结点指向:
    • 遍历结束时,curr为 NULL,prev指向原链表的尾结点(反转后的首元结点);
    • 将头结点的head指向prev,完成逆序。
代码提示(关键部分)
cs 复制代码
// 单链表逆序函数
void list_reverse(listhead_t *list) {
    if (list == NULL || list->head == NULL || list->head->next == NULL) {
        // 链表为空或只有一个结点,无需逆序
        return;
    }

    struct node_st *prev = NULL;
    struct node_st *curr = list->head;
    struct node_st *next = NULL;

    while (curr != NULL) {
        next = curr->next;  // 保存后继结点
        curr->next = prev;  // 反转当前结点指针
        prev = curr;        // 移动prev
        curr = next;        // 移动curr
    }

    list->head = prev;  // 头结点指向反转后的首元结点
}

作业 2:实现双链表(拓展进阶)

核心要求

在单链表基础上,实现双链表(双向链表),要求支持双向遍历、双向插入 / 删除,并实现完整的接口函数(初始化、增、删、改、查、销毁)。

核心拓展点
  1. 双链表的结点结构:在单链表结点基础上,增加前驱指针prev,每个结点同时指向前一个和后一个结点;
  2. 核心优势:支持双向遍历(既能从前往后,也能从后往前),删除指定结点时,无需遍历查找前驱结点(直接通过prev获取),效率更高;
  3. 注意事项:插入、删除时,需要同时修改两个指针(prevnext),避免指针混乱。
双链表结构定义与核心接口提示
cs 复制代码
// 双链表结点结构体
struct dnode_st {
    void *data;              // 数据域
    struct dnode_st *prev;   // 前驱指针(指向前一个结点)
    struct dnode_st *next;   // 后继指针(指向后一个结点)
};

// 双链表头结点结构体
typedef struct {
    struct dnode_st *head;   // 指向首元结点
    struct dnode_st *tail;   // 指向尾结点(方便尾插/尾删)
    int size;                // 数据元素大小
} dlisthead_t;

// 核心接口(参考单链表,修改指针操作)
int dlisthead_init(dlisthead_t **list, int size);  // 初始化
int dlist_add(dlisthead_t *list, const void *data);  // 头插
int dlist_add_tail(dlisthead_t *list, const void *data);  // 尾插
int dlist_del(dlisthead_t *list, const void *key, cmp_t cmp);  // 删除
// 其他接口(改、查、销毁)类似,需同时处理prev和next指针

五、顺序表 VS 单链表 总结对比(面试高频考点)

特性 顺序表 单链表
存储地址 连续 不连续
底层结构 数组 结点 + 指针
查找速度 快(随机访问,O (1)) 慢(遍历查找,O (n))
插入 / 删除 慢(需移动元素,O (n)) 快(仅修改指针,O (1))
内存利用率 较低(固定空间,易浪费) 较高(按需分配,无浪费)
内存开销 小(无额外指针开销) 大(每个结点需额外存储指针)
缓存命中率 高(连续存储) 低(分散存储)
适用场景 查找多、增删少(如成绩查询、字典检索) 增删多、查找少(如消息队列、链表编辑器)
实现难度 简单(无需指针操作) 复杂(需处理指针,易出现野指针、内存泄漏)

核心总结(划重点)

  1. 顺序表和单链表的核心区别:是否要求内存地址连续,这一区别决定了二者的优缺点和适用场景;
  2. 设计思想:没有完美的数据结构,只有适合场景的数据结构 ------ 顺序表牺牲增删效率换取查找效率,链表牺牲查找效率换取增删效率;
  3. 学习重点:单链表是链式结构的基础,掌握 "结点、指针、头结点" 的设计,以及插入、删除时的指针操作,就能轻松拓展双链表、循环链表;
  4. 避坑提醒:链表操作中,一定要注意 "内存泄漏"(忘记释放结点内存)和 "野指针"(指针指向已释放的内存),这是新手最容易犯的错误。

六、常见问题与避坑指南(小白必看)

  1. 问题 1:单链表插入结点时,指针顺序搞反,导致链表断裂?解决:先保存后继结点,再修改当前结点指针,最后移动指针(如头插时,先new_node->next = list->head,再list->head = new_node)。
  2. 问题 2:删除结点后,忘记释放内存,导致内存泄漏?解决:删除结点时,先释放数据域内存,再释放结点本身,避免只释放结点、遗漏数据域。
  3. 问题 3:链表销毁后,外部指针未置为 NULL,导致野指针?解决:销毁链表时,释放头结点后,一定要将外部头结点指针置为 NULL(如*list = NULL)。
  4. 问题 4:顺序表扩容时,拷贝数据出错?解决:扩容时,使用memcpy函数拷贝原数据,确保拷贝的字节数等于 "原数据个数 × 元素大小",避免拷贝不完整。
  5. 问题 5:单链表无法直接获取前驱结点,如何优化?解决:使用双链表,通过prev指针直接获取前驱结点,适合需要频繁获取前驱结点的场景。
相关推荐
vortex57 小时前
HackMyVm靶机Artig复盘
linux·渗透测试·靶机·hmv
谷哥的小弟7 小时前
(最新版)腾讯云服务器项目部署教程(4)— 部署项目
linux·运维·服务器·云计算·腾讯云·云服务器·项目部署
知识分享小能手8 小时前
R语言入门学习教程,从入门到精通,R语言层次关系数据可视化(7)
学习·信息可视化·r语言
承渊政道8 小时前
【动态规划算法】(子序列问题解题框架与典型案例)
数据结构·c++·学习·算法·leetcode·macos·动态规划
计算机安禾8 小时前
【Linux从入门到精通】第48篇:Linux集群与负载均衡——LVS与Keepalived高可用
linux·负载均衡·lvs
酸钠鈀8 小时前
AI M61SDK Ubuntu 环境搭建
linux·运维·ubuntu
JiaWen技术圈8 小时前
netfiler 协议栈钩子
linux·运维·服务器·安全
m0_629494738 小时前
LeetCode 热题 100-----15.轮转数组
数据结构·算法·leetcode
wefg18 小时前
【C语言】用 C 语言实现多态
c语言·开发语言