嵌入式Linux学习 | 数据结构 (Day04)链表升级(进阶优化 + 柔性数组原理 + 双向循环链表完整实现 + 高频面试深挖)

链表是 C 语言数据结构核心内容,也是嵌入式、底层开发、后端面试高频考点。传统单向、双向链表存在内存碎片化、内存利用率低、数据扩展性差等核心缺陷。


一、链表核心痛点:结点内存碎片化严重

1.1 传统链表内存结构问题

传统链表结点设计中,单个数据结点会分割为两段独立内存空间,内存不连续,是性能短板。

  • 常规链表结点结构:
cs 复制代码
struct Node{
    int data;         // 数据段:独立内存
    struct Node *next;// 指针段:独立内存
};

1.2 缺陷深度分析

  1. 内存碎片化 :频繁调用mallocfree创建和删除结点,堆内存产生大量零散碎片,长期运行导致程序卡顿;
  2. 缓存效率低:数据域与指针域内存不连续,CPU 缓存无法批量读取,数据访问效率下降;
  3. 扩展性差:固定数据类型结点,仅能存储单一数据,无法通用化设计;
  4. 内存分配繁琐:每个结点需要独立分配内存,增加系统开销。

1.3 优化核心思路

核心问题 :如何让单个链表结点仅占用一段连续内存最优方案C 语言变长结构体(柔性数组),Linux 内核、底层开源框架通用优化手段。


二、变长结构体(柔性数组)核心原理

2.1 定义规范

  1. 结构体最后一个成员 必须为数组,长度为01
  2. 0 长度数组不占用结构体本身内存空间,仅作为内存寻址标记;
  3. 内存分配时,采用结构体大小+自定义数据长度方式,一次性分配连续内存;
  4. 数组起始地址为额外扩展内存的起始位置,实现动态扩容。

2.2 基础示例

cs 复制代码
struct test{
    int a;        // 固定成员
    char c;       // 固定成员
    char arr[0];  // 柔性数组,无内存占用
};
// 一次性分配连续内存:结构体+100字节扩展空间
struct test *p = malloc(sizeof(struct test) + 100);

2.3 核心优势

  • 单次内存分配,杜绝碎片化;
  • 内存整块连续,CPU 读取效率更高;
  • 支持动态数据长度,适配任意数据类型;
  • 结构简洁,底层开发标配。

三、进阶实战:柔性数组版双向循环链表

结合变长结构体 设计通用型双向循环链表,支持任意数据类型、头插、尾插、查找、删除、遍历、销毁全功能,代码模块化、自带封装,工业级标准实现。

3.1 头文件 llist.h (接口声明 + 结构定义)

cs 复制代码
#ifndef __LLIST_H__
#define __LLIST_H__

// 链表基础结点结构
// 末端char data[0] 为柔性数组,实现变长存储
struct node_st {
	struct node_st *prev;  // 前驱结点指针
	struct node_st *next;  // 后继结点指针
	char data[0];          // 变长数据起始地址,不占用结构体空间
};

// 链表管理结构体
typedef struct {
	struct node_st head;   // 循环链表头结点
	int size;              // 单个数据存储大小,支持通用数据
}dlisthead_t;

// 自定义函数指针类型
typedef void (*pri_t)(const void *data);        // 数据打印函数
typedef int (*cmp_t)(const void *data, const void *key);  // 数据比较函数

// 函数声明
// 链表初始化,创建链表头,指定单数据大小
int dlisthead_init(dlisthead_t **dlist, int size);
// 链表头插操作
int dlist_add(dlisthead_t *dlist, const void *data);
// 链表尾插操作
int dlist_add_tail(dlisthead_t *dlist, const void *data);
// 判断链表是否为空
int dlist_empty(const dlisthead_t *dlist);
// 按关键字删除结点
int dlist_delete(dlisthead_t *dlist, const void *key, cmp_t cmp);
// 按关键字查询结点
void *dlist_search(const dlisthead_t *dlist, const void *key, const cmp_t cmp);
// 遍历链表,打印所有数据
void dlist_traval(const dlisthead_t *dlist, pri_t pri);
// 完整销毁链表,释放所有内存
void dlist_destroy(dlisthead_t **dlist);

#endif

3.2 源文件 llist.c (函数完整实现)

cs 复制代码
#include <stdlib.h>
#include <string.h>
#include "llist.h"

/**
 * @brief  初始化双向循环链表
 * @param  dlist: 链表头结构体指针
 * @param  size: 单个数据占用内存大小
 * @return 成功返回0,失败返回-1
 */
int dlisthead_init(dlisthead_t **dlist, int size)
{
	*dlist = malloc(sizeof(dlisthead_t));
	if (NULL == *dlist)
	{
		return -1;
	}
	// 初始化循环链表,头结点自环
	(*dlist)->head.prev = &(*dlist)->head;
	(*dlist)->head.next = &(*dlist)->head;
	// 设定数据存储大小
	(*dlist)->size = size;

	return 0;
}

/**
 * @brief  判断链表是否为空
 * @param  dlist: 链表头
 * @return 为空返回1,不为空返回0
 */
int dlist_empty(const dlisthead_t *dlist)
{
	// 首尾指针均指向头结点,代表链表为空
	return dlist->head.prev == &dlist->head && dlist->head.next == &dlist->head;
}

/**
 * @brief  内部函数:创建数据结点
 * @param  data: 待存储数据
 * @param  size: 数据大小
 * @return 新结点地址
 * @note  核心:一次性分配【结点+数据】整块连续内存
 */
static struct node_st *__create_node(const void *data, int size)
{
	// 柔性数组核心:整块连续内存分配
	struct node_st *node = malloc(sizeof(struct node_st) + size);
	if (NULL == node)
	{
		return NULL;
	}
	// 拷贝数据至柔性数组区域
	memcpy(node->data, data, size);
	node->prev = NULL;
	node->next = NULL;

	return node;
}

/**
 * @brief  内部函数:通用结点插入方法
 * @param  node: 待插入结点
 * @param  front: 前驱结点
 * @param  behind: 后继结点
 */
static void __insert(struct node_st *node,  struct node_st *front, struct node_st *behind)
{
	node->prev = front;
	node->next = behind;
	front->next = node;
	behind->prev = node;
}

/**
 * @brief  链表头插操作
 * @param  dlist: 链表头
 * @param  data: 待插入数据
 * @return 成功返回0,失败返回-1
 */
int dlist_add(dlisthead_t *dlist, const void *data)
{
	struct node_st *new_node = __create_node(data, dlist->size);
	if (NULL == new_node)
	{
		return -1;
	}
	// 插入到头结点与第一个结点之间
	__insert(new_node, &dlist->head, dlist->head.next);
	return 0;
}

/**
 * @brief  链表尾插操作
 * @param  dlist: 链表头
 * @param  data: 待插入数据
 * @return 成功返回0,失败返回-1
 */
int dlist_add_tail(dlisthead_t *dlist, const void *data)
{
	struct node_st *new_node = __create_node(data, dlist->size);
	if (NULL == new_node)
	{
		return -1;
	}
	// 插入到尾结点与头结点之间(循环链表)
	__insert(new_node, dlist->head.prev, &dlist->head);

	return 0;
}

/**
 * @brief  遍历链表
 * @param  dlist: 链表头
 * @param  pri: 自定义打印函数
 */
void dlist_traval(const dlisthead_t *dlist, pri_t pri)
{
	struct node_st *cur;
	// 从头结点下一个开始,遍历至头结点结束
	for (cur = dlist->head.next; cur != &dlist->head; cur = cur->next)
	{
		pri(cur->data);
	}
}

/**
 * @brief  内部函数:按关键字查找结点
 * @param  dlist: 链表头
 * @param  key: 查找关键字
 * @param  cmp: 自定义比较函数
 * @return 找到返回结点地址,否则返回NULL
 */
static struct node_st *__find(const dlisthead_t *dlist, const void *key, cmp_t cmp)
{
	struct node_st *cur;
	for (cur = dlist->head.next; cur != &dlist->head; cur = cur->next)
	{
		if (cmp(cur->data, key) == 0)
		{
			return cur;
		}
	}
	return NULL;
}

/**
 * @brief  内部函数:删除指定结点
 * @param  del: 待删除结点指针
 */
static void __delete(struct node_st **del)
{
	// 解除结点链表关联
	(*del)->prev->next = (*del)->next;
	(*del)->next->prev = (*del)->prev;
	// 释放整块连续内存,无内存碎片
	free(*del);
	*del = NULL;
}

/**
 * @brief  按关键字删除结点
 * @param  dlist: 链表头
 * @param  key: 删除关键字
 * @param  cmp: 比较函数
 * @return 成功返回0,失败返回-1
 */
int dlist_delete(dlisthead_t *dlist, const void *key, cmp_t cmp)
{
	if (dlist_empty(dlist))
	{
		return -1;
	}
	struct node_st *f = __find(dlist, key, cmp);
	if (NULL == f)
	{
		return -1;
	}
	__delete(&f);
	return 0;
}

/**
 * @brief  内部默认比较函数,用于链表销毁
 * @return 固定返回0,匹配所有结点
 */
static int __always_cmp(const void *data, const void *key)
{
	return 0;
}

/**
 * @brief  销毁整个链表,彻底释放内存
 * @param  dlist: 链表头指针
 */
void dlist_destroy(dlisthead_t **dlist)
{
	// 循环删除所有数据结点
	while (1)
	{
		if (-1 == dlist_delete(*dlist, NULL, __always_cmp))
		{
			break;
		}
	}
	// 释放链表头结构体
	free(*dlist);
	*dlist = NULL;
}

/**
 * @brief  按关键字查找数据
 * @param  dlist: 链表头
 * @param  key: 查找关键字
 * @param  cmp: 比较函数
 * @return 找到返回数据地址,否则返回NULL
 */
void *dlist_search(const dlisthead_t *dlist, const void *key, const cmp_t cmp)
{
	if (dlist_empty(dlist))
	{
		return NULL;
	}
	struct node_st *f = __find(dlist, key, cmp);
	if (NULL == f)
	{
		return NULL;
	}
	return f->data;
}

3.3 代码核心亮点解析

  1. 柔性数组核心应用 结点末尾char data[0]作为数据存储区,通过sizeof(struct node_st) + size一次性分配整块内存,彻底解决内存碎片
  2. 通用化设计 结合函数指针pri_tcmp_t,支持整型、结构体、字符串等任意数据类型,复用性极强;
  3. 双向循环结构头尾闭环设计,头插、尾插操作时间复杂度O(1),增删效率远超单向链表;
  4. 模块化封装 内部函数加__标识,隔离底层逻辑,外部仅暴露接口,代码耦合度低;
  5. 完整内存管理销毁函数递归清空所有结点 + 链表头,杜绝内存泄漏。

四、高频面试题拓展(深度解析)

4.1 面试原题

题目 :给定一个链表,仅允许遍历一次 ,如何快速找到链表的中间结点?核心解法快慢指针法(龟兔算法)

4.2 算法原理

  1. 定义两个指针:slow(慢指针)、fast(快指针);
  2. 初始状态:双指针同时指向链表头结点;
  3. 移动规则:
    • 慢指针:每次走 1 步;
    • 快指针:每次走 2 步;
  4. 终止条件:快指针到达链表末尾;
  5. 结果:慢指针停留位置即为链表中间结点。

4.3 分场景分析

① 非循环普通链表
  • 结点数为奇数 :慢指针指向正中间结点;
  • 结点数为偶数 :慢指针指向中间靠左结点;
② 双向循环链表(本节实现结构)
  • 链表为闭环结构,快慢指针持续循环;
  • 双指针最终会在环内相遇,相遇点即为链表中间位置;
  • 拓展:快慢指针法可判断链表是否存在环,是面试经典延伸题。

4.4 算法代码实现

cs 复制代码
// 单向链表结点结构
struct ListNode {
    int val;
    struct ListNode *next;
};

// 单次遍历查找中间结点
struct ListNode* findMiddle(struct ListNode* head){
    struct ListNode *slow = head;
    struct ListNode *fast = head;

    // 快指针抵达末尾,停止遍历
    while(fast != NULL && fast->next != NULL)
    {
        slow = slow->next;      // 慢指针一步
        fast = fast->next->next;// 快指针两步
    }
    return slow;
}

复杂度分析

  • 时间复杂度:O(n),仅单次遍历;
  • 空间复杂度:O(1),无额外内存开销。

五、全篇核心总结

  1. 链表升级本质 传统链表多段内存分割,碎片严重;变长结构体(柔性数组) 实现结点整块连续内存,是底层开发标配优化方案。
  2. 柔性数组核心要点结构体末尾定义、不占内存、动态扩展内存、单次分配,四大核心特性;
  3. 双向循环链表本节完整代码实现,模块化、通用化、内存安全,可直接用于课程设计、项目开发;
  4. 面试核心考点快慢指针法解决单次遍历查找中间结点,兼容普通链表与循环链表,延伸链表判环考点;
  5. 学习延伸柔性数组广泛应用于 Linux 内核、网络协议、嵌入式开发,是底层进阶必备知识点。
相关推荐
有谁看见我的剑了?12 小时前
linux 添加硬盘后系统识别不到硬盘处理
linux·运维·服务器
papership12 小时前
【入门级-数据结构-3、特殊树:完全二叉树的数组表示法】
数据结构·算法·链表
smj2302_7968265212 小时前
解决leetcode第3911题.移除子数组元素后第k小偶数
数据结构·python·算法·leetcode
山甫aa13 小时前
差分数组 ----- 从零开始的数据结构
数据结构
酿情师13 小时前
yihan:一款面向连续网页学习的智能侧边栏插件
学习·学习方法·工具·学习工具
早日退休!!!13 小时前
《数据结构选型指南》笔记
数据结构·数据库·oracle
丑八怪大丑13 小时前
Java数据结构与集合源码
数据结构
yc_122413 小时前
用 Visual Studio 远程调试 Linux:从零到流畅的完整指南
linux·ide·visual studio
瞎某某Blinder14 小时前
DFT学习记录[6]基于 HES06的能带计算+有效质量计算
python·学习·程序人生·数据挖掘·云计算·学习方法