数据结构2

📅 2026/7/27 9:00:25
数据结构2
一、内核链表详细理论解析1.1 什么是内核链表内核链表是 Linux 内核中最基础、最常用的数据结构之一。它本质上是一个双向循环链表但与大学课程里常见的数据域 指针域式的传统链表完全不同。内核链表的设计哲学是机制与策略分离——链表本身只提供连接机制不关心连接的是什么数据。1.2 传统链表的痛点在普通链表中节点结构体直接把数据和指针耦合在一起struct student { int id; struct student *next; };struct teacher { char name[20]; struct teacher *next; };这意味着对于每一种数据类型都必须重新实现一套链表操作插入、删除、遍历。代码冗余且难以维护。1.3 内核链表的革命性设计内核链表将链接功能抽象为一个极小的结构体struct list_head它只包含两个指针struct list_head *prev;struct list_head *next;任何需要链表功能的数据结构只需将这个list_head作为一个成员嵌入即可struct student { int id; struct list_head list; // 嵌入的链表节点 };这样一来所有数据结构共享同一套链表操作逻辑。插入、删除、遍历等操作只需针对list_head成员进行无需关心外部结构体的具体类型。二、栈详细理论解析2.1 栈的定义与特性栈是一种操作受限的线性表它只允许在一端称为栈顶进行数据的插入压栈 / Push和删除弹栈 / Pop。另一端称为栈底。栈的核心特性是后进先出LIFO, Last In First Out。想象一叠盘子你总是把新洗好的盘子放在最上面压栈取用的时候也从最上面拿弹栈。最底下的盘子是最早放进去的却要等到所有上面的盘子都拿走之后才能被取出。2.2 系统栈与数据结构栈学习栈时必须区分两个容易混淆的概念对比维度系统栈运行时栈数据结构栈维护者操作系统 / 编译器程序员手动实现作用管理函数调用、局部变量、返回地址解决算法问题如括号匹配、表达式求值内存区域进程虚拟地址空间中的栈区通常在堆区或数据区生长方向通常从高地址向低地址增长满减栈取决于实现无硬件限制两者都遵循 LIFO 原则但应用层面完全不同。2.3 顺序栈数组实现的细节底层结构使用一段连续的内存空间数组存储元素并维护一个整型变量top作为栈顶指针。空栈判断通常令top -1表示空栈。入栈先检查栈是否已满然后top再将元素存入data[top]。出栈先检查栈是否为空然后取出data[top]再top--。优点实现简单存取速度快缓存友好。缺点容量固定存在栈满溢出的风险。根据top指针的指向不同又细分为满栈top指向当前栈顶元素本身。空栈top指向栈顶元素的下一个空闲位置。增栈栈向高地址方向增长。减栈栈向低地址方向增长如 x86 架构。2.4 链式栈链表实现链式栈使用单向链表存储元素并将链表的头部作为栈顶。因为链表的头部插入和删除都是 O(1)天然适合栈的操作。入栈相当于链表的头插法——创建新节点让其指向原头节点然后更新头指针。出栈相当于链表的头删法——保存头节点的数据将头指针后移释放原头节点。判空检查头指针是否为NULL。优点无固定容量只要内存足够就能无限增长。缺点每个节点有额外的指针开销内存碎片化。选型建议如果数据规模已知且不大用顺序栈更简单高效如果数据量不可预测或非常大用链式栈更安全。三、队列重点对象完整代码 逐行注释队列是先进先出FIFO的线性结构。队尾插入队头删除。下面给出三种队列的完整实现每行均有解释。3.1 顺序队列数组实现#define MAX 100 typedef struct { int data[MAX]; // 存放队列元素的数组 int front; // 队头下标 int rear; // 队尾下标指向队尾元素的下一个空位 } SeqQueue;void initSeqQueue(SeqQueue *q)void initSeqQueue(SeqQueue *q) { q-front q-rear 0; // 空队列时 front rear }int isEmptySeq(SeqQueue *q)int isEmptySeq(SeqQueue *q) { return q-front q-rear; // 相等表示队列为空 }int isFullSeq(SeqQueue *q)int isFullSeq(SeqQueue *q) { return q-rear MAX; // rear 已到达数组最末端 }int enSeqQueue(SeqQueue *q, int val)int enSeqQueue(SeqQueue *q, int val) { if (isFullSeq(q)) return -1; // 队满则失败 q-data[q-rear] val; // 将新元素放入 rear 指向的位置 q-rear; // rear 指针后移 return 0; }int deSeqQueue(SeqQueue *q, int *val)int deSeqQueue(SeqQueue *q, int *val) { if (isEmptySeq(q)) return -1; // 队空则失败 *val q-data[q-front]; // 取出队头元素 q-front; // front 指针后移逻辑删除 return 0; }int getFrontSeq(SeqQueue *q, int *val)int getFrontSeq(SeqQueue *q, int *val) { if (isEmptySeq(q)) return -1; *val q-data[q-front]; // 仅读取不改变 front return 0; }注意假溢出——当 front 不断后移即使数组前部有空位rear 到达 MAX 后也无法继续插入。这就是顺序队列的最大缺陷。3.2 循环队列数组实现解决假溢出#define MAX 10 // 实际最大存储 MAX-1 个元素 typedef struct { int data[MAX]; int front; int rear; } CirQueue;void initCirQueue(CirQueue *q)void initCirQueue(CirQueue *q) { q-front q-rear 0; // 空队列 }int isEmptyCir(CirQueue *q) / int isFullCir(CirQueue *q)int isEmptyCir(CirQueue *q) { return q-front q-rear; // 首尾相等为空 } int isFullCir(CirQueue *q) { return (q-rear 1) % MAX q-front; // 尾指针下一个是头指针则为满 }int sizeCir(CirQueue *q)int sizeCir(CirQueue *q) { return (q-rear - q-front MAX) % MAX; // 取模计算有效元素个数 }int enCirQueue(CirQueue *q, int val)int enCirQueue(CirQueue *q, int val) { if (isFullCir(q)) return -1; // 满则失败 q-data[q-rear] val; // 放入 rear 位置 q-rear (q-rear 1) % MAX; // rear 循环后移 return 0; }int deCirQueue(CirQueue *q, int *val)int deCirQueue(CirQueue *q, int *val) { if (isEmptyCir(q)) return -1; // 空则失败 *val q-data[q-front]; // 取出队头 q-front (q-front 1) % MAX; // front 循环后移 return 0; }int getFrontCir(CirQueue *q, int *val)int getFrontCir(CirQueue *q, int *val) { if (isEmptyCir(q)) return -1; *val q-data[q-front]; return 0; }void printCirQueue(CirQueue *q)void printCirQueue(CirQueue *q) { if (isEmptyCir(q)) { printf(队列为空\n); return; } int i q-front; while (i ! q-rear) { // 从 front 到 rear 遍历 printf(%d , q-data[i]); i (i 1) % MAX; } printf(\n); }3.3 链式队列链表实现无容量限制typedef struct Node { int data; struct Node *next; } Node; typedef struct { Node *front; Node *rear; int size; } LinkQueue;void initLinkQueue(LinkQueue *q) / int isEmptyLink(LinkQueue *q)void initLinkQueue(LinkQueue *q) { q-front q-rear NULL; // 头尾指针均置空 q-size 0; } int isEmptyLink(LinkQueue *q) { return q-front NULL; }int enLinkQueue(LinkQueue *q, int val) // 入队尾插法int enLinkQueue(LinkQueue *q, int val) { Node *newNode (Node*)malloc(sizeof(Node)); // 1. 创建新节点 if (newNode NULL) return -1; // 内存不足 newNode-data val; newNode-next NULL; if (isEmptyLink(q)) { // 2. 队列为空 q-front q-rear newNode; // 头尾都指向新节点 } else { q-rear-next newNode; // 3. 原尾节点链接新节点 q-rear newNode; // 4. 更新尾指针 } q-size; return 0; }int deLinkQueue(LinkQueue *q, int *val) // 出队头删法int deLinkQueue(LinkQueue *q, int *val) { if (isEmptyLink(q)) return -1; // 队空失败 Node *temp q-front; // 暂存队头节点 *val temp-data; // 传出数据 q-front q-front-next; // 队头后移 if (q-front NULL) // 出队后为空 q-rear NULL; // 尾指针也需置空 free(temp); // 释放原队头 q-size--; return 0; }int getFrontLink(LinkQueue *q, int *val)int getFrontLink(LinkQueue *q, int *val) { if (isEmptyLink(q)) return -1; *val q-front-data; return 0; }void destroyLinkQueue(LinkQueue *q)void destroyLinkQueue(LinkQueue *q) { Node *cur q-front; while (cur) { // 遍历释放所有节点 Node *next cur-next; free(cur); cur next; } q-front q-rear NULL; q-size 0; }注意链式队列只要内存允许就可以无限入队是最灵活的队列实现方式。四、哈希表散列表详细理论说明4.1 为什么需要哈希表在普通数组或链表中查找一个元素通常需要遍历时间复杂度为 O(n)。即使是有序数组可以使用二分查找达到 O(log n)但仍然不够快。哈希表的目标是无论数据量多大理想情况下都能在 O(1) 时间内完成插入、删除和查找操作。这是通过一种空间换时间的策略实现的。4.2 哈希函数从 Key 到数组下标哈希表的核心是一段连续的数组称为桶数组。我们需要一个哈希函数散列函数它接收一个关键码Key计算出一个整数索引使得该 Key 对应的数据就存放在数组的这个位置上。例如对于字符串键常用的 BKDR 哈希算法是hash hash * 131 ch将每个字符的 ASCII 值迭代计算后再对表长取模得到下标。4.3 哈希冲突不可避免的难题如果两个不同的 Key比如张三和李四经过哈希函数计算后得到了相同的下标就发生了哈希冲突。因为数组的一个位置只能存一份数据冲突必须解决。冲突处理是哈希表设计的核心。4.4 链地址法拉链法详解这是应用最广泛的冲突解决方案C STL 的unordered_map和 Linux 内核的哈希表都采用了此方法。其思路如下桶数组不存储数据本身而是存储链表的头指针。当插入一个 Key-Value 对时先计算哈希值找到对应的桶。如果该桶的链表为空直接插入新节点。如果链表非空遍历链表检查 Key 是否已存在。若存在则更新 Value否则将新节点插入链表通常使用头插法因为最近插入的数据可能很快被再次访问。查找时同样计算哈希值定位桶然后遍历该桶的链表逐个比对 Key 直到找到目标。删除时从链表中摘除对应节点并释放内存。在链地址法中即使冲突发生所有冲突元素都和平共处在一个链表里互不影响。4.5 开放定址法简介另一种解决冲突的方法是不使用链表而是当发生冲突时按一定规则寻找下一个空位存放。常见的有线性探测1, 2, 3...、二次探测、双重哈希等。这种方法空间利用率高但删除操作复杂且容易出现聚集现象导致性能下降。工程上使用较少。4.6 负载因子与扩容哈希表的性能与负载因子已存元素数 / 桶数量密切相关。负载因子越高冲突概率越大链表越长操作效率越低。通常当负载因子超过某个阈值如 0.75时哈希表需要进行扩容创建一个更大的桶数组将原有数据重新哈希并迁移到新表中。这个过程代价较高但能保证长期的高效运行。4.7 哈希表操作流程总结创建分配桶数组所有桶置空。插入计算哈希 → 定位桶 → 遍历链表判断重复 → 头插新节点。查找计算哈希 → 定位桶 → 遍历链表比较 Key。删除计算哈希 → 定位桶 → 从链表中移除节点并释放。销毁遍历所有桶逐一释放链表中的所有节点最后释放桶数组。一句话总结哈希表通过哈希函数将 Key 映射为下标用数组实现随机访问用链表解决冲突从而在大多数情况下实现常数级的时间复杂度是现代软件系统中最重要的数据结构之一。