2.1 线性表的概念和性质
2.1.1 线性表是 E E E 中有穷个元素的组成的序列 L = ( e 0 , e 1 , . . . , e n − 1 ) L =(e_{0},e_{1},...,e_{n-1}) L=(e0,e1,...,en−1) ,元素之间的关系 为一个二元组的集合 R = { < e 0 , e 1 > , < e 1 , e 2 > , . . . , < e n − 2 , e n − 1 > } R=\{ <e_{0},e_{1}>,<e_{1},e_{2}>,...,<e_{n-2},e_{n-1}> \} R={<e0,e1>,<e1,e2>,...,<en−2,en−1>}
2.1.2 对于非空的线性表或者线性结构的特点:
- 存在唯一一个首元素
- 存在唯一一个尾元素
- 除首元素外,表中的每一个元素都有且仅有一个前驱元素
- 除尾元素外,表中的每一个元素都有且仅有一个后继元素
2.1.3 线性表的抽象数据类型ADT
2.1.4 线性表的两种存储结构(实现方式):
- 将表中元素顺序的存储地存放在一块连续的存储区中,即顺序表
- 将表中元素存放在通过链接构造起来的一系列存储区中,即链接表
2.2 顺序表
2.2.1 顺序表中元素之间的逻辑关系通过元素在存储区中的物理位置表示(隐式关系),元素的物理位置(内存地址)表示为 : L o c ( e i ) = L o c ( e 0 ) + s i z e ( e ) × i Loc(e_{i})=Loc(e_{0})+size(e)\times i Loc(ei)=Loc(e0)+size(e)×i
2.2.2 顺序表中元素存储结构:
- 顺序结构(用于元素大小相同时)
- 索引结构(用于元素大小不同时)
2.2.3 顺序表全局信息的存储方式:
-
一体式结构(顺序结构)(静态顺序表)
-
分离式结构(链接结构)(动态顺序表)
2.2.4 顺序表基本操作的实现:
-
创建空表
const int max = 100; //表容量,为定值 int num = 0; //当前表中元素个数 int list[max]; //实际上表中元素大小可能不同
-
判断表状态 O ( 1 ) O(1) O(1)
if (num==0) empty(); //表空 else if (num==max) empty(); //表满
-
访问下标 i i i 元素 O ( 1 ) O(1) O(1)
if (0<=i && i<=num=-1) empty(); //非法访问 else return list[i];
-
遍历 O ( n ) O(n) O(n)
int i=0; for (i;i<num;i++) { empty(); }
-
尾端插入元素 O ( 1 ) O(1) O(1)
if (num==max) empty(); //表满无法插入 else { list[num]=n; num++; }
-
插入元素到第 i i i个位置
if (num==max) empty(); //表满无法插入 else if (0<=i && i<num)
-
不保序 O ( 1 ) O(1) O(1)
{ list[num]=list[i]; list[i]=n; num++; } //将原有元素移到表尾
-
保序 O ( n ) O(n) O(n)
{ int j=num; do { list[j]=list[j-1]; j--; } while(j>i); list[i]=n; num++; } //将i后元素都顺移一位
-
-
删除尾端元素 O ( 1 ) O(1) O(1)
num--;
-
删除位置 i i i 的元素
if (0<=i && i<num)
-
不保序 O ( 1 ) O(1) O(1)
{ list[i]=list[num-1]; num--; } //将表尾元素移到空缺位置
-
保序 O ( n ) O(n) O(n)
{ int j=i; do { list[j]=list[j+1]; j++; } while(j<num); num--; } //将i后元素都前移一位
-
2.2.5 元素存储区扩充,扩充的两种策略:
-
每次扩充增加固定数目的存储位置(线性增长)
特点:节省空间,但是扩充操作频繁,操作次数多。
-
每次扩充容量加倍(指数增长)
特点:减少了扩充操作的执行次数,但可能会浪费空间资源。以空间换时间,推荐的方式。
2.3 链接表
2.3.1 链接表的实现方式:
- 将表中的元素分别存储在一批独立的存储块里
- 表结构中的结点都能找到与之相关的下一个结点
- 在前一结点里用链接的方式显式的记录与下一节点之间的关联
2.3.2 单向链表
2.3.2.1 单链表的结点可用一个二元组来表示 ( e l e m , n e x t ) (elem,next) (elem,next), e l e m elem elem保存着表的数据项, n e x t next next存储着下一个结点的标识,则单链表由表节点和指向表头的表头变量(表头指针) h e a d head head组成。
2.3.2.2 为了表示链表的结束,则需要给最后节点的链接域设置一个空连接。
2.3.2.3 在Python中定义一个表节点类:
class LNode:
def __init__(self,elem,next_=None):
self.elem = elem
self.next = next_
2.3.2.4 单链表基本操作的实现:
-
创建空链表
在Python中定义一个单链表类:
class LList: def __init__(self): self._head = None
-
判断表状态 O ( 1 ) O(1) O(1)
if self.head == None: pass #表表空 #链表一般不会满,除非存储空间不足
-
删除链表 O ( 1 ) O(1) O(1)
self.head = None
-
表首插入元素 O ( 1 ) O(1) O(1)
self.head = LNode(elem,self.head) #将表头指针指向新元素,新元素链接原表头元素
-
尾端插入 O ( n ) O(n) O(n)
if self._head = None: #表空直接插入 self._head = LNode(elem) return p = self._head while p.next == None: #将指针不断后移直至最后一个元素 p = p.next p.next = LNode(elem)
-
在第 i i i个元素后定位插入元素 O ( n ) O(n) O(n)
if i == 0: #表首插入 self.head = LNode(elem,self.head) return q = LNode(elem) p = self._head cnt = 1 while p is not None and cnt<i: #指针不断后移直到第i个元素 p = p.next cnt += 1 if p is None: #i不符合范围 raise Error else: q.next = p.next p .next = q
-
删除表首元素 O ( 1 ) O(1) O(1)
if self._head = None: raise Error self._head = self._head.next
-
删除表尾元素 O ( n ) O(n) O(n)
if self._head = None: raise Error p = self._head if p.next is None: #表中只有一个元素 self._head None return while p.ext.next is not None: p = p.next p.next = None
-
删除第 i i i个元素 O ( n ) O(n) O(n)
if i<0: raise Error q = LNode(elem) p = self._head cnt = 1 while p is not None and cnt<i: #指针不断后移直到第i个元素 p = p.next cnt += 1 if p is None: #i不符合范围 raise Error else: p.next = p.next.next
-
遍历 O ( n ) O(n) O(n)
p = self._head while p.next is not None: pass p = p.next
-
定位
-
按下标定位第 i i i个元素 O ( n ) O(n) O(n)
p = self._head cunt = 1 while p is not None and cnt<i: p = p.next cnt += 1 if p is None: raise else: return p.elem
-
按谓词定位 O ( n ) × T O(n)\times T O(n)×T
p = self._head while p is not None: if pred(p.elem): return p = p.next
-
2.3.2.4 单链表的简单变形
- 增加表尾指针 r e a r rear rear
- 循环单链表(即其中最后结点的 next 指向首结点)
2.3.3 双向链表
2.3.3.1 在单链表节点的基础上,扩充一个指向前一节点的链接域 p r e v prev prev
2.3.3.2 继承单链表节点类创建一个双链表节点,双链表类仍然只需要一个头部指针 h e a d head head则可,可基于单链表类派生。
class DLnode(LNode):
def __init__(self,elem,prev=None,next_=None)
LNode.__init__(self,elem,next_)
self.prev = prev
2.3.3.2 也可以根据需要,对双链表做一些变形,例如循环双链表
2.4 总结
2.4.1 不同链接表的简单总结
-
单链表:只有一个方向的链接,支持 O ( 1 ) O(1) O(1) 的前端插入和删除,定位操作或尾端操作都需要$O(n) $时间
-
带尾结点引用的单链表:可很好支持前端/尾端插入和前端弹出,都是 O ( 1 ) O(1) O(1)时间复杂度的操作,但不能支持高效的尾端删除
-
循环单链表:支持高效前端/尾端插入和前端弹出,扫描需注意结束判断
-
双链表:中结点有两个方向的链接,能高效找到前后结点。有尾结点引用时两端插入和删除都是 O ( 1 ) O(1) O(1) 时间操作。循环双链表的性质类似
2.4.2
- 优点:
- 表结构是通过一些链接起来的结点形成的,结构很容易调整修改
- 只通过修改链接,就能灵活地修改表的结构和内容。如加入/删除一个或多个元素,翻转表,重排元素顺序,将表分解为两个或多个等
- 缺点
- 一些操作的开销大:
- 基于位置找到表中元素需要线性时间
- 尾端操作需要线性时间(增加尾指针可以将尾端加入元素变为常量操作,但仍不能有效实现尾端删除)
- 找当前元素的前一元素需要从头扫描表元素(双链表可以解决这个问题,但每个结点要付出更多存储代价)
- 每个元素增加了一个链接域(存储代价),双链表增加两个链接域
- 一些操作的开销大: