数据结构入门:线性表(Day 1)——从原理到代码实战
📚 数据结构入门:线性表(Day 1)——从原理到代码实战
day 1的内容只是一个大概,初学者看不太懂的话也没关系,具体详细的每一部分精讲都会在后续的内容当中体现
自由的前提是自律,加油冲起来
🌟 一、线性表:数据世界的“排队规则”
线性表是数据的“一维队列”,核心特性是元素首尾相接、类型统一,就像地铁🚇的乘客队列:
- 前驱与后继:除首元素无前驱、尾元素无后继,其他元素均有唯一“邻居”
- 逻辑与物理结构的对比 :
- 逻辑结构:线性关系(1对1)
- 物理结构:顺序存储(数组) vs 链式存储(指针)
🔍 两大物理实现对比
特性 | 顺序表(数组) | 链表 |
---|---|---|
内存分配 | 连续内存块 | 动态分散存储 |
查询速度 | O(1)(直接下标访问✨) | O(n)(需遍历🚶♂️) |
插入/删除效率 | O(n)(需移动元素📦) | O(1)(修改指针即可🔗) |
空间灵活性 | 固定容量,扩容成本高💸 | 按需分配,无内存浪费🌱 |
🏗️ 二、顺序表:内存中的“连续公寓”
顺序表动态扩容流程
🔧 C语言代码实现(动态扩容版)
typedef struct {int *data; // 动态数组指针int length; // 当前长度int capacity; // 总容量
} SeqList;// 初始化(默认容量10)
void InitList(SeqList *L) {L->data = (int*)malloc(10 * sizeof(int));L->length = 0;L->capacity = 10;
}// 扩容操作(翻倍策略)
void Expand(SeqList *L) {int *new_data = (int*)malloc(2 * L->capacity * sizeof(int)); // 容量翻倍for(int i=0; i<L->length; i++) new_data[i] = L->data[i]; // 数据迁移free(L->data); // 释放旧内存L->data = new_data; // 指向新数组L->capacity *= 2; // 更新容量
}
💡 高频面试题:为什么顺序表扩容常采用翻倍策略?
(答案:均摊时间复杂度优化,避免频繁扩容)
⛓️ 三、链表:指针串起的“数据珍珠”
双向链表插入操作可视化
🔧 双向链表实现(支持反向遍历)
typedef struct DNode {int data;struct DNode *prior; // 指向前驱的指针struct DNode *next; // 指向后继的指针
} DNode;void InsertAfter(DNode *p, DNode *s) {s->next = p->next; // ① 新节点后继指向p的后继if(p->next) p->next->prior = s; // ② p原后继的前驱指向sp->next = s; // ③ p的后继改为ss->prior = p; // ④ s的前驱指向p
}
🔍 链表变种大全:
- 单链表(➡️单向指针)
- 双向链表(↔️双向指针)
- 循环链表(🔁首尾相连)
- 静态链表(用数组模拟📇)
🚀 四、线性表实战:从理论到工程
LRU缓存算法流程图解
案例:用链表实现LRU缓存淘汰算法
// 链表节点结构
typedef struct CacheNode {int key;int value;struct CacheNode *prev;struct CacheNode *next;
} CacheNode;// 访问数据时的链表调整
void UpdateLRU(CacheNode *node, CacheNode **head) {// 1. 断开当前节点node->prev->next = node->next;node->next->prev = node->prev;// 2. 插入到链表头部node->next = (*head)->next;node->prev = *head;(*head)->next->prev = node;(*head)->next = node;
}
📈 应用场景:Redis内存管理、浏览器缓存、CPU缓存体系
📝 五、考研真题精选
📌 考研真题大全解(线性表篇)
精选15年高频考题+深度解析,建议收藏反复练习!
🔥 真题1:顺序表删除重复元素(2023年408真题)
题目:
设计算法删除递增顺序表中所有重复元素,使每个元素只出现一次。要求时间复杂度O(n),空间复杂度O(1)。
示例:
原表:[1,2,2,3,3,3,4]
→ 新表:[1,2,3,4]
答案:
void DeleteDuplicates(SeqList *L) {if (L->length == 0) return;int k = 0; // 新表指针for (int i=1; i<L->length; i++) {if (L->data[i] != L->data[k]) {L->data[++k] = L->data[i];}}L->length = k + 1;
}
解析:
- 双指针法:
k
指向已处理部分的末尾,i
扫描未处理部分 - 核心逻辑:当发现新元素时,
k
先自增再赋值(类似“快慢指针”) - 易错点:忘记处理空表或长度为1的特殊情况❗
🔥 真题2:链表合并(2020年真题)
题目:
将两个非递减有序单链表合并为一个非递减有序单链表,要求用原链表节点,不得开辟新内存。
答案:
Node* MergeList(Node *La, Node *Lb) {Node *dummy = (Node*)malloc(sizeof(Node)); // 虚拟头节点Node *tail = dummy;while (La && Lb) {if (La->data <= Lb->data) {tail->next = La;La = La->next;} else {tail->next = Lb;Lb = Lb->next;}tail = tail->next;}tail->next = La ? La : Lb; // 拼接剩余部分return dummy->next;
}
解析:
- 虚拟头节点技巧:避免处理空链表的边界条件
- 尾插法:始终维护
tail
指针指向合并后的链表末尾 - 断链风险:修改指针前必须保存后继节点(如
La = La->next
)⚠️
🔥 真题3:循环链表判环(2016年真题)
题目:
设计算法判断单链表是否有环,若有环返回环的入口节点。要求空间复杂度O(1)。
答案:
Node* DetectCycle(Node *head) {Node *slow = head, *fast = head;while (fast && fast->next) {slow = slow->next;fast = fast->next->next;if (slow == fast) { // 相遇点Node *p1 = head, *p2 = slow;while (p1 != p2) { p1 = p1->next;p2 = p2->next;}return p1; // 入口点}}return NULL; // 无环
}
解析:
- 快慢指针法:快指针每次走2步,慢指针走1步
- 数学原理:相遇时,从
头节点
和相遇点
同速出发必在入口点相遇 - 复杂度:时间复杂度O(n),空间O(1)(优于哈希表法🚀)
🔥 真题4:顺序表真题(2017年真题)
题目:
已知顺序表L中元素按值递增排列,设计算法删除值在 [x,y]
之间的所有元素,要求时间O(n),空间O(1)。
答案:
void DeleteRange(SeqList *L, int x, int y) {int k = 0; // 新表指针for (int i=0; i<L->length; i++) {if (L->data[i] < x || L->data[i] > y) {L->data[k++] = L->data[i];}}L->length = k;
}
解析:
- 筛选保留法:只保留不在区间内的元素
- 优化点:利用有序特性可二分查找边界,但遍历法更简单直接
- 易错点:未处理区间边界相等的情况(如x=y)🔍
🧩 真题5:链表综合题(2024年新大纲样题)
题目:
设计算法将单链表 L
中所有奇数位置的节点移到偶数位置节点之后。
示例:
输入:1→2→3→4→5→NULL
输出:2→4→1→3→5→NULL
答案:
void Rearrange(Node *head) {if (!head || !head->next) return;Node *odd = head->next; // 偶数链表头Node *even = head; // 奇数链表头Node *p = odd;while (p && p->next) {even->next = p->next;even = even->next;p->next = even->next;p = p->next;}even->next = odd; // 拼接奇偶链表
}
解析:
- 双链表分离法:将奇数节点和偶数节点拆分为两个链表再合并
- 指针操作:注意在修改
next
前保存原后继节点 - 边界处理:链表长度为奇数/偶数的不同情况测试❗
🧩 课后
// 已知动态顺序表结构体定义
typedef struct {int *array;int size;int capacity;
} DynamicArray;// 请编写删除所有偶数的算法(要求时间复杂度O(n))
void RemoveEvenNumbers(DynamicArray *da) {// 你的代码写在这里...
}
💡 解题锦囊:双指针法+扩容逆用,评论区留下你的答案雏形!
📊 历年考点统计表
考点 | 出现年份 | 出现频次 |
---|---|---|
顺序表删除操作 | 2017,2023 | ★★★★☆ |
链表合并/拆分 | 2020,2024 | ★★★★☆ |
链表环检测 | 2016,2019 | ★★★☆☆ |
链表逆置/重组 | 2022,2021 | ★★★★★ |
顺序表查找 | 2018,2015 | ★★☆☆☆ |
🎯 下期高能剧透!《顺序表:内存刺客 VS 性能王者の终极对决》
🔥 你将解锁这些硬核内容 →
💥 动态扩容の黑暗兵法
🔧 从「固定数组」到「翻倍策略」,揭秘顺序表如何用「空间换时间」统治数据结构江湖!
💡 灵魂拷问:为什么Java的ArrayList默认扩容1.5倍?答案下期揭晓!(也可以去我的专栏Java 集合框架大师课:集合框架源码解剖室(五))
⚡ 效率暴击全对比
🚀 手撕四大操作复杂度,用真实代码告诉你:
操作 | 时间复杂度 | 实战场景 | 翻车案例 |
---|---|---|---|
随机访问 | O(1)✨ | 高频查询类API | 越界访问导致Segmentation Fault😱 |
尾部插入 | O(1)🎯 | 实时日志采集系统 | 未预分配空间引发频繁扩容💸 |
中间插入 | O(n)💣 | 游戏技能队列插队 | 百万级数据插入卡死界面🖥️ |
范围删除 | O(n)🌀 | 数据库批量删除操作 | 未重置length引发内存泄漏💦 |
🚀 三大工业级实战
- Redis字符串:SDS如何用预分配+惰性删除吊打C字符串?
- Tensor底层:NDArray如何用stride魔法实现超高速切片?
- MMORPG地图:游戏场景区块为何必须用顺序表存储?
🌌 下期更将放出:
- 顺序表在Linux内核中的魔鬼优化(slab分配器)
- 用SIMD指令集暴力提升顺序表性能300%的骚操作
- 10年408真题顺序表题型解题模板(附记忆口诀)
👉 点击关注不迷路,源码级解析即将空降!