数据结构入门:线性表(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真题顺序表题型解题模板(附记忆口诀)

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

相关推荐
石榴树下4 分钟前
00. 马里奥的 OAuth 2 和 OIDC 历险记
后端
uhakadotcom5 分钟前
开源:subdomainpy快速高效的 Python 子域名检测工具
前端·后端·面试
似水流年流不尽思念21 分钟前
容器化技术了解吗?主要解决什么问题?原理是什么?
后端
Java水解23 分钟前
Java中的四种引用类型详解:强引用、软引用、弱引用和虚引用
java·后端
i听风逝夜23 分钟前
看好了,第二遍,SpringBoot单体应用真正的零停机无缝更新代码
后端
柏油1 小时前
可视化 MySQL binlog 监听方案
数据库·后端·mysql
舒一笑2 小时前
Started TttttApplication in 0.257 seconds (没有 Web 依赖导致 JVM 正常退出)
jvm·spring boot·后端
M1A12 小时前
Java Enum 类:优雅的常量定义与管理方式(深度解析)
后端
AAA修煤气灶刘哥3 小时前
别再懵了!Spring、Spring Boot、Spring MVC 的区别,一篇讲透
后端·面试
柏油3 小时前
MySQL 字符集 utf8 与 utf8mb4
数据库·后端·mysql