本文参考网课为 数据结构与算法
1 第二章线性表,主讲人 张铭 、王腾蛟 、赵海燕 、宋国杰 、邹磊 、黄群。
本文使用IDE为 Clion
,开发环境 C++14
。
更新:2023 / 10 / 22
数据结构与算法 | 第二章:线性表
- 线性表总览
- 线性结构
- 概念
- 特点
- 分类
- 复杂程度
- 访问方式
- 操作方式
- 栈
- 队列
- 概念
- 逻辑结构
- 存储结构
- 顺序表
- 链表
- 运算
- 线性表
- 顺序表
- 概念
- 运算
- 插入元素
- 删除元素
- 链表
- 概念
- 分类
- 单链表
- 结点
- 查找
- 插入
- 删除
- 运算分析
- 双链表
- 结点
- 插入
- 删除
- 循环链表
- 链表的边界条件
- 顺序表与链表的比较
- 示例
- 参考链接
线性表总览
线性结构
概念
线性表
可以用二元组 B = (K,R),K = {a0,a1,…,an-1},R = {r} 来表示:
- 有一个唯一的开始节点。开始节点是没有前驱的,有一个唯一的直接后继;
- 有一个唯一的终止节点。终止节点是没有后继的,有一个唯一的直接前驱;
- 除了开始节点、终止节点以外的节点皆称为内部节点。每一个内部节点都有且仅有一个唯一的直接前驱和直接后继。
<ai,ai+1>,ai 是 ai+1 的前驱,ai+1 是 ai 的后继。
前驱、后继关系是具有反对称性和传递性的。
反对称性是指<ai,ai+1> 成立,但是,<ai+1,ai> 不成立
传递性是指<ai,aj>,<aj,ak> 则 <ai,ak>
特点
线性结构
的特点:
均匀性
虽然不同线性表
的数据元素可以是各种各样的,但对于同一线性表的各数据元素必定具有相同的数据类型和长度有序性
各数据元素在线性表
中都有自己的位置,且数据元素之间的相对位置是线性的。
分类
复杂程度
按复杂程度划分:
- 简单
线性表
、栈
、队列
、散列表
- 高级
广义表
、多维数组
、文件
访问方式
按访问方式划分:
- 直接访问型(
direct access
)
根据元素的下标即可直接访问到元素 - 顺序访问型(
sequential access
)
必须在表内挨个查询 - 目录索引型(
directory access
)
通过目录索引查找目标元素
操作方式
按操作划分:
线性表
所有表目
都是同一类型结点的线性表
不限制操作形式
根据存储的不同分为顺序表
、链表
栈
(LIFO
,Last in First Out
)
插入
和删除
操作都限制在表的同一端进行队列
(FIFO
,First In First Out
)
插入
在表的一端,删除
在另一端
栈
k0 最先进入栈,最后出栈;
…
ki+1 最后进入栈,最先出栈。
先进后出,后进先出。
队列
先进先出,后进后出。
概念
线性表
,简称 表
,是零个或多个元素的有穷序列,通常可以表示成 k0,…,k1,…,kn-1(n>=1)。
表目
线性表
的元素(可包含多个数据项)索引
i称为表目
ki 在线性表
的位置索引
或下标
表的长度
线性表
中所含元素的个数n
空表
长度为0的线性表
(n=0
)
线性表
的特点有操作灵活(长度可增长可缩短)、数据规模小(易存储和运算)。
定义1个 线性表
可以从3个方面着手:逻辑结构
、存储结构
、运算
。如果2个数据结构从这3方面中有任何1个方面存在不同,即不同的数据结构。
逻辑结构
逻辑结构
的主要属性包括:
线性表
的长度
与存储结构
和数据规模相关- 表头(
head
)、表尾(tail
) - 当前位置(
current position
)
存储结构
根据 存储结构
的不同,线性表
可分为 顺序表
和 链表
:
顺序表
顺序表
将元素按索引值从小到大存放在一片相邻的连续区域。结构紧凑,存储密度为1。其在物理结构上的关系也表达了相应的逻辑关系。
链表
链表
通过指针链接的关系来表达各个元素在逻辑上的关系。因为指针的存在,链表
需要额外的存储空间,即指针开销,因此,链表
的存储效率不如 顺序表
。
链表
分为 单链表
、双链表
和 循环链表
。
运算
线性表
的运算包括以下几个方面:
- 创建和清除
- 建立
线性表
- 清除
线性表
- 增删改
- 插入新元素
- 删除某元素
- 修改某元素
- 查
- 排序
- 检索
线性表
根据 存储结构
的不同,线性表
可分为 顺序表
和 链表
。
顺序表
概念
顺序表
,也称 向量
,采用定长的一维数组存储结构实现的。
顺序表
的主要特点有:
- 元素的类型相同;
- 元素顺序地存储在连续存储空间中,每一个元素有唯一的索引值;
- 使用常数作为向量长度
- 数组存储
- 通过元素下标可快速访问目标元素,
线性表
中的任意元素都可以随机存取
元素地址计算如下所示:
Loc(ki) = Lock(k0) + c * i, c = sizeof(ELEM)
第i个元素地址 = 起始地址 + 元素存储长度 * 第i个元素
顺序表
的类定义:
class arrList: public List<T>{ // 顺序表,向量
private: // 线性表的取值类型和取值空间
T * aList; // 私有变量,存储顺序表的实例
int maxSize; // 私有变量,顺序表实例的当前长度
int curLen; // 私有变量,顺序表实例的当前长度
int position; // 私有变量,当前处理位置
public:
arrList(const int size){ // 创建新表,设置表实例的最大长度
maxSize = size; aList = new T[maxSize];
curLen = position = 0;
}
~arrList(){ // 析构函数,用于消除该表实例
delete [] aList;
}
};
void clear(){ // 返回当前实际长度
delete [] aList; curLen = position = 0;
aList = new T[maxSize];
}
int length(); // 返回当前实际长度
bool append(cost T value); // 在表尾添加元素V
bool insert(const int p, const T valule); // 插入元素
bool delete(const int p); // 删除位置p上元素
bool setValue(const int p, const T value);// 设元素值
bool getValue(const int p, T&value); // 返回元素
bool getPos(int &p, const T value); // 查找元素
运算
顺序表
进行插入、删除运算的算法分析:
- 表中元素的移动
- 插入:移动
n-i
个 - 删除:移动
n-i-1
个
- 插入:移动
- 表中每个位置被插入和删除的概率不同或相同
i
的位置插入和删除的概率分别是 pi 和 pi’- 插入的平均移动次数
- 删除的平均移动次数
- 插入的平均移动次数
i
的位置插入和删除的概率相同,即 pi = 1/(n+1),pi’ = 1/n
时间代价为O(n)
- 插入的平均移动次数
- 删除的平均移动次数
- 插入的平均移动次数
插入元素
template <class T> bool arrList<T> :: insert(const int p, const T value){ // 设元素的类型为T,aList是存储顺序表的数组
// p是新元素value的插入位置,如果插入成功则返回true,否则则返回false;
int i;
if (curLen >= maxSize){ // 检查顺序表是否溢出
cout << "The list is overflow" << endl; return false;
}
if (p<0 || p>curLen){ // 检查插入位置是否合法
cout << "Insertion point is illegal" << endl; return false;
}
for (i = curLen; i>p; i--)
aList[i] = aList[i-1]; // 从表尾curLen-1起往右移动直到p
aList[p] = value; // 位置p处插入新元素
curLen++; // 表的实际长度增1
return true;
}
删除元素
template <class T> // 设元素的类型为T;
bool arrList<T> :: delete(const int p){ // aList是存储顺序表的数组;
// p为即将删除元素的位置。删除成功则返回true,否则则返回false;
int i;
if (curLen <= 0){ // 检查顺序表是否为空
count << "No element to delete \n" << endl;
return false;
}
if (p<0 || p>curLen-1){ // 检查删除位置是否合法
count << "deletion is illegal\n" << endl;
return false;
}
for (i=p; i<curLen-1;i++)
aList[i] = aList[i+1]; // 从位置p开始每个元素左移直到curLen
curLen--; // 表的实际长度减1
return true;
}
链表
概念
链表
是通过指针把一串存储结点链接成一个链。存储结点由两部分组成,数据域
和 指针域
(后继地址)。
链表
根据链接方式和指针多少可以分为 单链
、双链
和 循环链
:
单链
双链
循环链
分类
单链表
一个简单的 单链表
- 整个
单链表
:head
- 第一个结点:
head
- 空表判断:
head
==NULL
- 当前结点 a1:
curr
一个带头节点的 单链表
- 整个
单链表
:head
- 第一个结点:
head
->next
,head
!=NULL
- 空表判断:
head
->next
==NULL
- 当前结点 a1:
fence
->next
(curr
隐含 )
结点
单链表
的结点类型:
template <class T> class Link{
public:
T data; // 用于保存结点元素的内容
Link<T> * next; // *next, 指向后继结点的指针
Link(const T info, const Link<T>* nextValue = NULL){
data = info;
next = nextValue;
}
Link(const Link<T>* nextValue) {
next = nextValue;
}
};
查找
template <class T> // 线性表的元素类型为T
Link<T> *linkList <T>::setPos(int i){
int count = 0;
if (i == -1) // i为-1则定位到头结点
return head;
Link<T> *p = head -> next; // 循环定位。若i为0则定位到第1个结点
while (p != NULL && count < i){
p = p -> next; // 指向第i结点,i=0,1,..., 当链表中结点数小于i时返回NULL
count ++;
};
return p;
}
插入
分为以下几个步骤:
- 创建新结点
- 新结点指向右边的结点
- 左边结点指向新结点
template <class T> // 线性表的元素类型为T
bool linkList<T>::insert(const int i, const T value){ // 将value插入第i个结点
Link<T> *p, *q; // 假设p和q 2个结点
if ((p = setPos(i-1)) == NULL){ // 设p是第i个结点的前驱结点;如果p是空的,则为非法插入点,返回false
cout << "非法插入点" << endl;
return false;
}
q = new link<T>(value, p -> next); // 设新结点q,q的值为value,q是p的后继结点
p -> next = q;
if (p == tail) // 如果p是尾结点
tail = q; // 设尾结点是q
return true;
}
删除
分为以下几个步骤:
- 用 p 指向元素 x 的结点的前驱结点
- 删除元素为 x 的结点
- 释放 x 占据的空间
template <class T> // 线性表的元素类型为T
bool linkList<T>::delete((const int i)){ // 删除第i个结点
Link<T> *p, *q; // 假设p和q 2个结点
if ((p = setPos(i-1))==NULL || p==tail){ // 假设p是第i个结点的前驱结点;如果p是NULL或者尾结点,返回false
count << "非法结点" << endl;
return false;
}
q = p -> next // q是p的后继结点
if (q == tail){ // 如果q是尾结点,则p的next指向为NULL,因为p的next是第i个结点而第i个结点会被删除
tail = p;
p -> next = NULL;
}
else // 如果q不是尾结点,则p的next指向为q的next,因为p的next是第i个结点 即q 而第i个结点会被删除
p -> next = q -> next
delete q;
return true;
}
运算分析
在 单链表
中,对一个结点操作,往往必须先从第一个点开始找到目标结点,即用一个指针指向它:
p = head;
while (没有到达) p = p -> next;
单链表
的时间复杂度 O(n)
:
- 定位
O(n)
- 插入
O(n) + O(1)
- 删除
O(n) + O(1)
双链表
为弥补 单链表
的不足而产生 双链表
。因为 单链表
的 next
字段仅仅指向后继结点,而不能有效地找到前驱结点。反之亦然。因此,双链表
相比于 单链表
,增加一个指向前驱的指针。
结点
双链表
的结点类型:
template <class T> class Link{
public:
T data; // 设T结点,用于保存结点元素的内容
Link<T> * next; // 用于指向后继结点的指针
Link<T> * prev; // 指向前驱结点的指针
Link(const T info, Link<T>* preValue=NULL, Link<T>* nextValue=NULL){ // 给定值和前后指针的构造函数
data = info;
next = nextValue;
prev = preValue;
}
Link(Link<T>* preValue=NULL, Link<T>* nextValue=NULL){ // 给定前后指针的构造函数
next = nextValue;
prev = preValue;
}
};
插入
删除
循环链表
将 单链表
或者 双链表
的头尾结点链接起来,就是一个 循环链表
。
相比于单纯的 单链表
或 双链表
,从 循环链表
的任一结点出发,都能访问到表中其它结点。不增加额外存储花销,却给不少操作带来了方便。
链表的边界条件
- 针对特殊结点的处理
- 针对头指针
- 针对尾指针
- 非循环链表尾结点
tail
的指针域保持为NULL
- 循环链表尾结点
tail
的指针回指头结点head
- 非循环链表尾结点
- 针对链表的处理
- 空链表的特殊处理
- 插入或删除结点时指针勾链的顺序
- 指针移动的正确性
- 插入
- 查找或遍历
顺序表与链表的比较
比较项 | 顺序表 | 链表 |
---|---|---|
存储开销 | 1. 不需要使用指针,即不需要额外的存储空间开销来存放指针域。 2. 如果整个数组元素很满,则没有结构性存储开销。 | 1. 每个元素都存在指针,即需要额外的存储空间开销来存放指针域。 2. 存储利用指针,动态地按照需要为表中新的元素分配存储空间。 |
时间代价 | 1. 插入、删除元素时间代价为 O(n) 2. 查找元素时间代价为常数时间。 | 1. 插入、删除元素时间代价为 O(1) 。2. 查找元素时间代价为 O(n) 。 |
访问 | 1. 对表内元素的读访问十分简洁便利 | |
灵活性 | 1. 需要预先申请固定长度的连续空间 | 1. 不需要预先申请内存空间,表的长度可以动态变化。可以较为方便地插入、删除内部元素。 |
存储密度 | n 表示 线性表 中当前元素的数目。P 表示指针的存储单元大小(通常为 4 bytes) 。E 表示数据元素的存储单元大小。D 表示在数据中存储的 线性表 元素的最大数目。顺序表 的空间需求为 DE n 越大,顺序表 的空间效率就更高。 | n 表示 线性表 中当前元素的数目。P 表示指针的存储单元大小(通常为 4 bytes) 。E 表示数据元素的存储单元大小。D 表示在数据中存储的 线性表 元素的最大数目。链表 的空间需求为 n(P+E) 。 |
应用 | 适合存储静态数据。 适合总结点数目大概可以估计,而不是无法预估需要预先申请多大内存的场景。 适合结点比较稳定(插入、删除少)的场景。 | 适合存储动态数据。 适合结点数目无法预知。 适合结点动态变化(插入、删除多)的场景。 |
示例
以 顺序表
和 链表
来表达一元多项式:
-
假设一元多项式为 Pn(x) = p0 + p1x + p2x2 + … + pnxn
使用线性表
表示,即只存系数(第i
个元素存 xi 的系数)
适合数据密集的情况 -
假设一元多项式为 p(x) = 1 + 2x10000 + 4x40000
使用线性表
表示,即
适合数据稀疏的情况。
能够较快根据指针域和结点的值恢复该多项式。
参考链接
数据结构与算法 ↩︎