数据结构入门:线性表(Day 1)——从原理到代码实战

📚 数据结构入门:线性表(Day 1)------从原理到代码实战

day 1的内容只是一个大概,初学者看不太懂的话也没关系,具体详细的每一部分精讲都会在后续的内容当中体现

自由的前提是自律,加油冲起来


🌟 一、线性表:数据世界的"排队规则"

线性表 是数据的"一维队列",核心特性是元素首尾相接、类型统一,就像地铁🚇的乘客队列:

  • 前驱与后继:除首元素无前驱、尾元素无后继,其他元素均有唯一"邻居"
  • 逻辑与物理结构的对比
    • 逻辑结构:线性关系(1对1)
    • 物理结构:顺序存储(数组) vs 链式存储(指针)
🔍 两大物理实现对比
特性 顺序表(数组) 链表
内存分配 连续内存块 动态分散存储
查询速度 O(1)(直接下标访问✨) O(n)(需遍历🚶♂️)
插入/删除效率 O(n)(需移动元素📦) O(1)(修改指针即可🔗)
空间灵活性 固定容量,扩容成本高💸 按需分配,无内存浪费🌱

🏗️ 二、顺序表:内存中的"连续公寓"

顺序表动态扩容流程
flowchart TB subgraph 扩容流程 Start[检查容量] --> Condition{容量不足?} Condition -->|Yes| Step1[申请2倍新空间] Step1 --> Step2[数据迁移] Step2 --> Step3[释放旧内存] Step3 --> Step4[更新容量] Condition -->|No| End[直接插入] end
🔧 C语言代码实现(动态扩容版)
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;              // 更新容量
}

💡 高频面试题:为什么顺序表扩容常采用翻倍策略? (答案:均摊时间复杂度优化,避免频繁扩容)


⛓️ 三、链表:指针串起的"数据珍珠"

双向链表插入操作可视化
sequenceDiagram participant p as 当前节点p participant s as 新节点s participant p_next as p的后继 Note over p,p_next: 初始状态:p → p_next s->>p_next: ① s.next = p.next p_next-->>s: ② p_next.prior = s p->>s: ③ p.next = s s->>p: ④ s.prior = p Note over p,s: 最终状态:p → s → p_next
🔧 双向链表实现(支持反向遍历)
c 复制代码
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原后继的前驱指向s
    p->next = s;          // ③ p的后继改为s
    s->prior = p;         // ④ s的前驱指向p
}

🔍 链表变种大全

  • 单链表(➡️单向指针)
  • 双向链表(↔️双向指针)
  • 循环链表(🔁首尾相连)
  • 静态链表(用数组模拟📇)

🚀 四、线性表实战:从理论到工程

LRU缓存算法流程图解
graph TD A[访问数据] --> B{存在哈希表?} B -->|Yes| C[移动节点到链表头] B -->|No| D[新建节点插入头部] C --> E[返回数据] D --> F{缓存满?} F -->|Yes| G[删除链表尾节点] G --> D F -->|No| D
案例:用链表实现LRU缓存淘汰算法
c 复制代码
// 链表节点结构
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]

答案

c 复制代码
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年真题)

题目 : 将两个非递减有序单链表合并为一个非递减有序单链表,要求用原链表节点,不得开辟新内存。

答案

c 复制代码
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)。

答案

c 复制代码
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)。

答案

c 复制代码
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

答案

c 复制代码
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前保存原后继节点
  • 边界处理:链表长度为奇数/偶数的不同情况测试❗

🧩 课后
c 复制代码
// 已知动态顺序表结构体定义
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 性能王者の终极对决》

🔥 你将解锁这些硬核内容

💥 动态扩容の黑暗兵法

🔧 从「固定数组」到「翻倍策略」,揭秘顺序表如何用「空间换时间」统治数据结构江湖!

graph LR A[初始容量10] -->|插入第11个元素| B[申请20空间] B --> C[复制旧数据] C --> D[旧内存销毁] D --> E[新王者诞生!]

💡 灵魂拷问:为什么Java的ArrayList默认扩容1.5倍?答案下期揭晓!(也可以去我的专栏Java 集合框架大师课:集合框架源码解剖室(五))

效率暴击全对比

🚀 手撕四大操作复杂度,用真实代码告诉你:

操作 时间复杂度 实战场景 翻车案例
随机访问 O(1)✨ 高频查询类API 越界访问导致Segmentation Fault😱
尾部插入 O(1)🎯 实时日志采集系统 未预分配空间引发频繁扩容💸
中间插入 O(n)💣 游戏技能队列插队 百万级数据插入卡死界面🖥️
范围删除 O(n)🌀 数据库批量删除操作 未重置length引发内存泄漏💦
🚀 三大工业级实战
  1. Redis字符串:SDS如何用预分配+惰性删除吊打C字符串?
  2. Tensor底层:NDArray如何用stride魔法实现超高速切片?
  3. MMORPG地图:游戏场景区块为何必须用顺序表存储?

🌌 下期更将放出

  • 顺序表在Linux内核中的魔鬼优化(slab分配器)
  • 用SIMD指令集暴力提升顺序表性能300%的骚操作
  • 10年408真题顺序表题型解题模板(附记忆口诀)

👉 点击关注不迷路,源码级解析即将空降!

相关推荐
橘猫云计算机设计26 分钟前
基于django云平台的求职智能分析系统(源码+lw+部署文档+讲解),源码可白嫖!
数据库·spring boot·后端·python·django·毕业设计
码农小站29 分钟前
MyBatis-Plus 表字段策略详解:@TableField(updateStrategy) 的配置与使用指南
java·后端
技术小丁32 分钟前
使用PHP将PDF转换为图片(windows + PHP + ImageMagick)
后端
李憨憨35 分钟前
深入探究MapStruct:高效Java Bean映射工具的全方位解析
java·后端
仰望星空的打工人35 分钟前
若依改用EasyCaptcha验证码
后端
雷渊36 分钟前
通俗易懂的来解释倒排索引
java·后端·面试
知其然亦知其所以然36 分钟前
面试官狂喜!我用这 5 分钟讲清了 ThreadPoolExecutor 饱和策略,逆袭上岸
java·后端·面试
独立开阀者_FwtCoder39 分钟前
分享 8 个 丰富的 Nextjs 模板网站
前端·javascript·后端
梦兮林夕39 分钟前
06 文件上传从入门到实战:基于Gin的服务端实现(一)
后端·go·gin
fliter41 分钟前
性能比拼: Node.js vs Go
javascript·后端