当前位置: 首页 > news >正文

考研复习 Day13| 数据结构与算法--线性表

一、线性表的定义和基本操作

1.1 线性表的定义

线性表:由n(n≥0)相同数据类型的元素组成的有限序列

表示形式

L = (a₁, a₂, ···, aᵢ, aᵢ₊₁, ···, aₙ)

术语说明
n表长;n=0 时为空表
a₁表头元素(唯一的“第一个”元素)
aₙ表尾元素(唯一的“最后一个”元素)
直接前驱除第一个元素外,每个元素有且仅有一个
直接后继除最后一个元素外,每个元素有且仅有一个

线性表的特点

特点说明
元素个数有限
逻辑上的顺序性元素之间存在明确的先后次序
元素不可分割每个元素是独立单位
数据类型相同每个元素占有相同大小的存储空间
抽象性只讨论逻辑关系,不考虑具体内容

:线性表是逻辑结构,顺序表和链表是存储结构,二者属于不同层面的概念,不要混淆。


1.2 线性表的基本操作

操作描述

InitList(&L)

初始化表,构造空线性表
Length(L)求表长,返回元素个数
LocateElem(L, e)按值查找,返回元素位置
GetElem(L, i)按位查找,获取第i个元素值
ListInsert(&L, i, e)插入操作,在第i个位置插入e
ListDelete(&L, i, &e)删除操作,删除第i个元素并用e返回
PrintList(L)输出操作,按顺序输出所有元素
Empty(L)判空操作
DestroyList(&L)销毁操作,释放内存空间


① 基本操作的具体实现取决于存储结构
② 符号&表示C++引用调用;C语言中可通过指针实现


第二部分:顺序表(线性表的顺序存储)

2.1 顺序表的定义

顺序表:用一组地址连续的存储单元依次存储线性表中的数据元素,逻辑上相邻的元素在物理位置上也相邻。

存储地址计算

设顺序表L的起始地址为LOC(A),每个元素占sizeof(ElemType)字节,则第i个元素的存储地址为:

LOC(aᵢ) = LOC(A) + (i-1) × sizeof(ElemType)

随机存取:可在 O(1) 时间内直接访问任意位置的元素。

存储结构描述

(1)静态分配

#define MaxSize 50 typedef struct { ElemType data[MaxSize]; int length; } SqList;

静态分配时,数组大小在编译时固定,空间占满后无法插入新元素。

(2)动态分配

#define InitSize 100 typedef struct { ElemType *data; int MaxSize, length; } SeqList;

C语言动态分配:

L.data = (ElemType*)malloc(sizeof(ElemType) * InitSize);

C++动态分配:

L.data = new ElemType[InitSize];

注意:动态分配仍然是顺序存储结构,支持随机存取,只是存储空间可动态调整。

顺序表的优缺点

优点缺点
① 随机访问(O(1)时间)① 插入删除需移动大量元素(O(n))
② 存储密度高,无额外指针开销② 需要连续存储空间,不够灵活

2.2 顺序表上基本操作的实现

考点追踪:顺序表上操作的时间复杂度分析(2023)

1. 顺序表的初始化

静态分配:

void InitList(SqList &L) { L.length = 0; }

动态分配:

void InitList(SeqList &L) { L.data = (ElemType*)malloc(InitSize * sizeof(ElemType)); L.length = 0; L.MaxSize = InitSize; }

2. 插入操作

可以就像一个排好队的一排人,要往其中某个位置插入一人(新元素),因此在插入位置后面的人要从尾部开始依次退后,不然就会踩脚(导致插入数据出现错误)

在第i个位置(1≤i≤L.length+1)插入新元素e。

bool ListInsert(SqList &L, int i, ElemType e) { if (i < 1 || i > L.length + 1) return false; if (L.length >= MaxSize) return false; for (int j = L.length; j >= i; j--) L.data[j] = L.data[j - 1]; L.data[i - 1] = e; L.length++; return true; }

时间复杂度分析

情况移动次数时间复杂度
最好(表尾插入,i=n+1)0O(1)
最坏(表头插入,i=1)nO(n)
平均n/2O(n)

注意:位序从1开始,数组下标从0开始,注意转换。

3. 删除操作

删除第i个位置(1≤i≤L.length)的元素,用e返回其值。

bool ListDelete(SqList &L, int i, ElemType &e) { if (i < 1 || i > L.length) return false; e = L.data[i - 1]; for (int j = i; j < L.length; j++) L.data[j - 1] = L.data[j]; L.length--; return true; }

时间复杂度分析

情况移动次数时间复杂度
最好(删除表尾,i=n)0O(1)
最坏(删除表头,i=1)n-1O(n)
平均(n-1)/2O(n)

4. 按值查找(顺序查找)

int LocateElem(SqList L, ElemType e) { for (int i = 0; i < L.length; i++) if (L.data[i] == e) return i + 1; return 0; }

时间复杂度:平均比较次数 (n+1)/2 →O(n)


第三部分:线性表的链式表示

3.1 单链表的定义

考点追踪:单链表的应用(2009、2012、2013、2015、2016、2019)

结点结构

结点类型定义

typedef struct LNode { ElemType data; struct LNode *next; } LNode, *LinkList;

头指针与头结点

类型空表判断
带头结点L->next == NULL
不带头结点L == NULL

引入头结点的优点(方便操作)

  • 操作统一性:首部插入/删除与其他位置逻辑一致

  • 空表与非空表处理统一:头指针始终非空


3.2 单链表上基本操作的实现

注:默认链表带头结点

1. 单链表的初始化

bool InitList(LinkList &L) { L = (LNode*)malloc(sizeof(LNode)); L->next = NULL; return true; }

2. 求表长

int Length(LinkList L) { int len = 0; LNode *p = L->next; while (p != NULL) { len++; p = p->next; } return len; }

时间复杂度:O(n)

3. 按序号查找

LNode *GetElem(LinkList L, int i) { LNode *p = L; int j = 0; while (p != NULL && j < i) { p = p->next; j++; } return p; }

4. 按值查找

LNode *LocateElem(LinkList L, ElemType e) { LNode *p = L->next; while (p != NULL && p->data != e) p = p->next; return p; }

时间复杂度:O(n)

5. 插入结点操作

考点追踪:单链表插入操作的过程(2016、2024)

bool ListInsert(LinkList &L, int i, ElemType e) { LNode *p = L; int j = 0; while (p != NULL && j < i - 1) { p = p->next; j++; } if (p == NULL) return false; LNode *s = (LNode*)malloc(sizeof(LNode)); s->data = e; s->next = p->next; // 1 新结点指向原后继 p->next = s; // 2 前驱指向新结点 return true; }

重要:步骤①和②的顺序不可颠倒,否则会丢失后继地址。

时间复杂度:O(n),主要开销在查找前驱。若已知结点指针,后插操作O(1)。

前插操作的技巧(O(1)时间):将新结点*s插入目标结点*p之后,再交换*p与*s的数据域,逻辑上等效于前插

s->next = p->next; p->next = s; // 交换数据域 ElemType temp = p->data; p->data = s->data; s->data = temp;

6. 删除结点操作

bool ListDelete(LinkList &L, int i, ElemType &e) { LNode *p = L; int j = 0; while (p->next != NULL && j < i - 1) { p = p->next; j++; } if (p->next == NULL) return false; LNode *q = p->next; e = q->data; p->next = q->next; free(q); return true; }

时间复杂度:O(n)

删除给定结点*p的技巧(O(1)时间,不能用于尾结点):将*p后继结点的(数据)值复制到*p中,然后删除其后继结点。

LNode *q = p->next; p->data = p->next->data; p->next = q->next; free(q);

7. 头插法建立单链表

LinkList List_HeadInsert(LinkList &L) { LNode *s; int x; L = (LNode*)malloc(sizeof(LNode)); L->next = NULL; scanf("%d", &x); while (x != 9999) { s = (LNode*)malloc(sizeof(LNode)); s->data = x; s->next = L->next; L->next = s; scanf("%d", &x); } return L; }

每插入一个结点O(1),总时间复杂度O(n)。元素顺序与输入顺序相反

8. 尾插法建立单链表

LinkList List_TailInsert(LinkList &L) { int x; L = (LNode*)malloc(sizeof(LNode)); LNode *s, *r = L; scanf("%d", &x); while (x != 9999) { s = (LNode*)malloc(sizeof(LNode)); s->data = x; r->next = s; r = s; scanf("%d", &x); } r->next = NULL; return L; }

附设尾指针,时间复杂度O(n)。元素顺序与输入顺序一致

类比C++STL库中的<vector>的push_back尾插操作。


3.3 双链表

结点类型定义

typedef struct DNode { ElemType data; struct DNode *prior, *next; } DNode, *DLinkList;

插入操作(在结点p之后插入s):

考点追踪:双链表中插入操作的实现(2023)

s->next = p->next; // 1 p->next->prior = s; // 2 s->prior = p; // 3 p->next = s; // 4

步骤①必须在步骤④之前,否则会丢失后继地址。(这让我想到了一句话,干事之前要先安排好后路,即先要指向后继结点,防止丢失后继地址,也就是步骤1)

删除操作(删除结点p的后继q):

考点追踪:双链表中删除操作的实现(2016)

p->next = q->next; q->next->prior = p; free(q);

3.4 循环链表

循环单链表

  • 尾结点的next指向头结点

  • 判空条件:L->next == L

  • 从任意结点出发可遍历整个链表

考点追踪:循环单链表中删除首元素的操作(2021)

循环双链表

  • 头结点的prior指向尾结点

  • 尾结点的next指向头结点


3.5 静态链表

用数组模拟链式存储,指针用数组下标(游标)表示。

#define MaxSize 50 typedef struct { ElemType data; int next; // 下一个元素的数组下标 } SLinkList[MaxSize];

结束标志:next == -1


3.6 顺序表和链表的比较

比较维度顺序表链表
存取方式随机存取 O(1)顺序存取 O(n)
逻辑与物理结构逻辑相邻⇔物理相邻逻辑相邻≠物理相邻
按值查找无序O(n);有序O(log₂n)O(n)
按序号查找O(1)O(n)
插入/删除需移动约一半元素 O(n)修改指针 O(1)(已知位置)
空间分配需预分配容量按需扩展,但每结点有指针开销
存储密度高(=1)低(<1)

选择建议

场景推荐结构
规模稳定、以随机访问为主顺序表
动态性强、频繁插入删除链表

思考

1. 顺序表 ≈ 数组

连续内存、随机访问、插入删除慢——就是它的全部特点。

2. 链表

每个结点“拿着”下一个结点的地址。就像寻宝游戏,每张纸条写着下一个线索的位置。

(尾插法莫名得让我想到了“人体蜈蚣”,《十宗罪》看多了?)

3. 头结点的意义 ≈ 哨兵

在链表头部加一个不存数据的“哨兵”,让空链表和非空链表的操作统一,减少边界判断。这和编程中在数组前后加“哨兵”简化循环的逻辑是一样的。

4. 类比学习

学习这些数据结构时让我很容易地就想到了C++STL库中已封装好的<list><vector>等其他容器,其实本质是同一套,只不过C++中的更方便使用。

注:以上内容参考 2027年数据结构考研复习指导 王道论坛 组编,其中有一些个人想法,如有任何错误或不妥,欢迎各位大佬指出,如果各位有一些有意思的想法,也可以和我交流一下~感谢!

明日计划

栈和队列与数组

http://www.jsqmd.com/news/652977/

相关文章:

  • Android BLE 稳定连接的关键,不是扫描,而是 GATT 操作队列
  • 从SRAM到RLDRAM:一文读懂主流存储器的技术演进与选型指南
  • 深色模式(Dark Mode)适配指南
  • 终极免费工具:3秒搞定百度网盘提取码,告别繁琐搜索的完整指南
  • LaTeX子图排版终极指南:用subcaption包实现完美图文混排(附常见报错解决)
  • Rust的#[cfg(debug_assertions)]:调试与发布版本的差异编译
  • 自动化测试工程师缺口扩大3倍:入局黄金期只剩18个月
  • 零基础搞定!全平台 Python + VS Code 开发环境配置保姆级教程
  • springboot私家车位共享系统小程序(文档+源码)_kaic
  • 避开这些坑!R语言做SEM时lavaan/blavaan/brms包的选择与高阶应用指南
  • Qwen3.5-4B-Claude-Opus部署教程:HTTPS反向代理与Nginx安全加固
  • 算法训练营第四天 59. 螺旋矩阵 II
  • 告别每次输密码!手把手教你用Git Bash生成SSH密钥并绑定到GitHub和Sourcetree
  • DataX 实战:从零构建跨库数据同步解决方案
  • SQL如何统计分组内满足条件的唯一项_COUNT与DISTINCT
  • 如何用MATLAB仿真OFDM频谱:从时域补零到相位影响的实践解析
  • 算法训练营第四天|59. 螺旋矩阵 II
  • 实战指南:从零搭建TPshop商城Linux环境与云服务器部署
  • 想学Excel函数,学数据分析的价值分析
  • Java8 Stream sorted排序实战:从Comparator基础到多级排序进阶
  • 预训练模型加载实战:transformers常见报错与版本适配指南
  • FreeRTOS实战:用互斥量和信号量搞定临界区,别再只会关中断了
  • OmenSuperHub:解锁惠普OMEN游戏本性能的终极开源解决方案
  • VScode+MinGW+EGE:一站式图形编程环境搭建与避坑指南
  • 【AI Agent 从入门到精通】第六章:多智能体(Multi-Agent)系统架构详解:从双 Agent 协作到大型多 Agent 系统
  • CSS如何引入媒体查询专用样式_利用media属性实现响应式加载
  • 从零到一:在IDEA中玩转Docker Desktop容器化开发
  • 基于Halcon视觉技术的PCB元件缺失检测实战指南
  • 揭秘Figma-MCP与ClaudeCode:构建像素级UI还原的自动化工作流
  • 大语言模型架构演进:从BERT到GPT再到Mamba的正确打开方式