线性表
定义
- 相同数据类型的 n 个数据元素 的有限序列
- 每个元素所占空间一样大
- 有次序
- 数据元素 的 位序 从 1 开始
- n 叫作 表长
- n=0 时,线性表是空表
- 除 表头元素 每个元素都有一个 直接前驱
- 除 表尾元素 每个元素都有一个 直接后继
基本操作
-
初始化线性表
- 分配内存空间
-
销毁线性表
- 释放内存空间
-
插入
-
删除
-
按值查找
-
按位查找
-
什么时候 需要 传入参数 的 引用 “&” 【 c++ 中支持】
- 对参数的修改结果需要 “带回来”
顺序表
用 顺序存储 方式 实现的线性表,逻辑相邻 物理也相邻
定义
- 用 顺序存储 方式 实现的线性表
实现方式
静态分配
|
|
- 定义后 表长无法修改,顺序表的大小、容量 不可变,存储空间是静态的
动态分配
|
|
- c 语言中 使用 malloc 和 free 两个函数来 动态申请和释放内存空间
|
|
顺序表的特点
-
优点
-
随机访问,可在 O(1) 时间内 找到 第 i 个元素
-
存储密度高
-
-
缺点
-
拓展容量不方便
-
插入、删除操作不方便
-
基本操作
基于 静态 分配实现的顺序表
插入操作
- 在表 L 的 第 i 个位置 插入指定元素
|
|
- 插入操作的时间复杂度
- 最坏 O(n)
- 最好 O(1)
- 平均 O(n)
删除操作
- 删除表 L 的 第 i 个位置 的元素,并用e返回删除元素的值
|
|
- 删除操作的时间复杂度
- 最坏 O(n)
- 平均 O(n)
- 最好 O(1)
链表
链表 是用 链式存储 的顺序表
单链表
-
每个结点 除了存放数据元素 还要存储指向下一个元素的指针
-
优点:不要求大片连续空间,改变容量方便
-
缺点:不支持随机存储,要耗费一定空间存放指针
-
头结点 是一个 空节点 ,不存放数据 的 空结点
- 只能在 头结点 之后插入和删除
-
代码定义:
1 2 3 4typedef struct LNode{ // 定义单链表 结点类型 ElemType data; // 数据域:每个结点 存放一个数据元素 struct LNode *next; // 指针域:指针指向 下一个结点 } LNode, *LinkList; // 别名 第一个是 结点LNode, 第二个是 指向结点的指针类型 的别名 -
链表增加新的结点
1 2 3 4 5 6 7 8 9 10// 增加一个新的结点 struct LNode *p = (struct LNode*)malloc(sizeof(struct LNode)); // 申请一片内存空间,并用指针 p 指向这个结点 // 使用了 typedef 后 struct LNode 就用它的别名 LNode 即可 // 等价于 LNode *p = (struct LNode*)malloc(sizeof(LNode)); // typedef 等价于 typedef struct LNode LNode; typedef struct Lnode *LinkList;LNode *强调 这是一个 结点LinkList强调这是一个链表
不带 头结点 的单链表
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19typedef struct LNode{ ElemType data; struct LNode *next; } LNode, *LinkList; bool InitList(LinkList &L){ L = Null; // 空表 暂时没有任何结点 防止脏数据 return true; } bool Empty(LinkList L){ return (L==Null); } void test(){ LinkList L; // 声明一个指向链表的指针; 注意 此处并没有创建一个结点 // 初始化一个空表 InitList(L); }带头结点 的 单链表
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26typedef struct LNode{ ElemType data; // 数据域 struct LNode *next; // 指针域 } LNode, *LinkList; // 初始化一个带头结点的单链表 bool InitList(LinkList &L){ L = (LNode *)malloc(siezeof(LNode)); // 分配一个头结点 if(L==NULL) return false; // 内存不足 分配失败 L->next = NULL; // 头结点 之后 暂无 结点 return true; } void test(){ LinkList L; // 声明一个指向链表的指针 InitList(L); // 初始化 空 链表 } // 判断带头结点 的单链表 是否为空 bool empty(Linklist L){ if(L->next == NULL) return true; else return false; }带 头结点 的链表 写代码更方便 插入、删除等基本操作更好写
插入
按位序插入
-
带头结点
->(箭头运算符)的本质是:通过指向结构体的指针,访问该结构体内部的成员1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32typedef struct LNode{ ElemType data; struct LNode * next; }LNode, *LinkList; /* 在第i个位置插入元素e 本质:先找到第i-1个元素 把新插入的节点的后继 指向第i-1个元素的后继 再将第i-1个元素的 后继 指向插入的节点 */ bool ListInsert(LinkList &L, int i, ElemType e){ if(i<1) return false; LNode *p; // 指针p 当前扫描到的结点 int j=0; // while 循环中 当前的 p 指向哪个结点 p = L; //L指向头结点,头结点是第0个结点(不存放数据) while(p!=NULL && j<i-1){ // 找到第i个结点 p = p->next; j++; } if(p==NULL){ // i值不合法:第 i-1 个结点不存在的情况 return false; } LNode *s = (LNode *)malloc(sizeof(LNode)); s->data = e; s->next = p->next; p->next = s; // 将结点s连到p之后 return true; } -
不带头结点
插入和删除 第一个元素时,需要更改头指针。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30typeded struct LNode{ ElemType data; struct LNode *next; }LNode, *LinkList; bool ListInsert(LinkList &L, int i, Elemtype e){ if(i<1) return false; if(i==1){ LNode *s = (LNode *)malloc(sizeof(LNode)); s->data = e; s->next = L; L = s; // 头指针 指向新结点 return true; } LNode *p; int j=1; p=L; while(p!=NULL && j<i-1){ p=p->next; j++; } if(p==NULL) return fals; LNode *s = (LNode *)malloc(sizeof(LNode)); s->data = e; s->next = p->next; p->next = s; return ture; }
指定节点后插操作
- 带头结点
|
|
指定节点前插操作
不传入头结点 ,虽然 前驱结点未知 但是可以 插入在其之后 交换其 存放的 数据的值
|
|
删除
按位序删除(带头结点)
|
|
删除指定节点
类似于前插操作的 实现
|
|
双链表
在 单链表的基础上增加 一个 指针域,指向前驱结点
|
|
双链表的初始化(带 头结点)
|
|
双链表的后插操作
|
|
双链表的删除
需要注意 边界条件
|
|
双链表的遍历
|
|
双链表 不可随机存取;按位查找、按值查找操作 都只能用遍历 的 方式实现
循环链表
循环单链表
- 表尾结点 的 next 指针 指向 头结点
- 从一个 结点 出发 可以找到其它任何一个结点
|
|
循环双链表
- 表头结点 的 prior 指向表尾结点
- 表尾结点 的 next 指向表头结点
|
|
静态链表
用数组方式 实现的 链表
分配一整片的连续内存空间,各个结点集中安置
- 优点
- 增 删 操作 不需要大量移动元素
- 缺点
- 不能随机存取,只能从 头结点开始 依次往后查找
- 容量固定不变
- 适用场景
- 不支持 指针 的 低级语言
- 数据元素 固定不变的场景
- 如 操作系统 的 文件分配表 FAT
静态链表定义
|
|
插入位序为i的结点
- 找到一个 空 的结点,存入数据元素
- 判断结点 为 空,可以在初始化的时候 把 游标 设置为一个特殊值 比如 都赋值 -2
- 从头结点出发 找到 位序为 i-1的结点
- 修改新结点的 next
- 修改 i-1号 结点的 next
顺序表 和 链表的对比
逻辑结构
- 都是 线性表
物理结构(存储结构)
- 顺序表 随机存取 存储密度高
- 大片连续空间分配不方便
- 链表 离散的小空间分配,改变容量方便
- 不可随机存取 存储密度低
基本操作
-
创建
- 线性表 需 预分配大片连续空间
- 静态分配 内存大小不可变
- 动态分配 如需改变 需要移动大量元素
- 链表
- 只需要分配一个头结点(也可不分配,只声明一个头指针),方便拓展
- 线性表 需 预分配大片连续空间
-
销毁
- 链表 依次删除 free
- 顺序表 先修改表长
- 静态分配 系统自动回收
- 动态数组 手动free
-
增 删
- 时间复杂度 都是 O(n)
- 但链表的 开销更小
- 时间复杂度 都是 O(n)
-
查找
- 按位查找
- 顺序表 O(1)
- 链表 O(n)
- 按值查找
- 顺序表 无序O(n) 有序 O(log_2 n)
- 链表 O(n)
- 按位查找
表长 难以估计、经常需要 增加/删除 用 链表
表长 可预估 查询(搜索) 操作较多 用 顺序表
- 如 顺序表和链表 的 开放式问题 的答题思路
- 从 逻辑结构、存储结构、运算操作 三个方便 阐述各自特点 再给出结论