本文参考网课为 数据结构与算法
1 第二章线性表,主讲人 张铭 、王腾蛟 、赵海燕 、宋国杰 、邹磊 、黄群。
本文使用IDE为 Clion
,开发环境 C++14
。
更新:2023 / 10 / 22
线性表
可以用二元组 B = (K,R),K = {a0,a1,…,an-1},R = {r} 来表示:
前驱、后继关系是具有反对称性和传递性的。
线性结构
的特点:
均匀性
线性表
的数据元素可以是各种各样的,但对于同一线性表的各数据元素必定具有相同的数据类型和长度有序性
线性表
中都有自己的位置,且数据元素之间的相对位置是线性的。按复杂程度划分:
线性表
、栈
、队列
、散列表
广义表
、多维数组
、文件
按访问方式划分:
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)。
表目
线性表
的元素(可包含多个数据项)索引
表目
ki 在 线性表
的位置 索引
或 下标
表的长度
线性表
中所含元素的个数 n
空表
线性表
( 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
个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
curr
一个带头节点的 单链表
单链表
:head
head
-> next
,head
!= NULL
head
-> next
== NULL
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;
}
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
使用 线性表
表示,即
适合数据稀疏的情况。
能够较快根据指针域和结点的值恢复该多项式。
数据结构与算法 ↩︎