线性表:
线性表是最基本的数据结构之一.线性表是一组元素的抽象.
3.1线性表的概念和表抽象数据类型
3.11表的概念和性质
线性表是一种线性关系
在一个费控的线性表里,存在着唯一一个首元素和唯一的一个尾元素(末元素),除了首元素之外,表中的每个元素e都有且仅有一个前驱元素;除了尾元素之外的每个元素都有且仅有一个后继元素.
3.12表抽象数据类型
研究数据机构的实现问题,主要考虑两个方面:
1.计算机内存的特点,以保存元素和元素的顺序信息的需要.
2.各种重要操作的效率
基于各方面的考虑,人们提出了两种基本的实现模型:
1.将表中的元素顺序地存放在一大块连续的存储区里,这样实现的表是顺序表(连续表)这种实现中,元素间的顺序关系由他们的存储顺序自然表示.
2.将表元素存放在yu通过链接构造起来的一系列存储块里,这样实现的表成为链接表.简称链表.
3.13顺序表的实现:
顺序表的基本实现方式很简单:表中元素顺序存放在一片足够大的连续存储区里,首元素存入存储区的开始位置,其余元素依次顺序存放.元素之间的逻辑关系通过元素在存储空间里的物理位置表示.
基本实现方式:
最常见情况是一个表里保存的元素类型相同,因此存储每个表元素所需的存储量相同,可以在表里等距安排同样大小的存储位置.这种安排可以直接映射到计算机内存和单元,表中任何元素位置的计算非常简单,存取操作可以在I(O)时间内完成.
设有一个顺序表对象,其元素存储在一片元素存储区,该存储区的起始位置(内存地址),已知为lo,假定编号从0开始,元素e0自然应存储在内存地址是Loc(e0)=lo,在假定表中的一个元素所需的存储单元数为c = size(元素),这种情况下,简单的元素ei的地址计算公式:
$$
Loc(e_i)=Loc(e_0)+c*i
$$
-
表元素的大小通常是静态确定的,此时计算机硬件可以支持高效的表元素的访问.
如图:
-
如果表元素的大小不同,只要略微改变顺序表的存储结构,仍然可以保证O(1)时间的元素访问操作.
如图:
此时表元素大小不统一,按照上面的计算公式无法计算出位置,我们将实际元素另行存储,在顺序表中各单元保存相对应的元素的引用信息(链接),由于每个链接存储量相同,通过统一公式计算出元素链接的存储位置,而后顺链接做一次间接访问,就得到了实际元素的数据了.此时,c是存储一个连接所需的存储量.
在一个表中存续期间,其长度可能会发生变化:
- 如果一开始确定元素个数分配存储.例如python的tuple,适合创建不变的顺序表.
- 如果是变动的,在建立这种表时,应该保留一个空位,以满足增加元素的需要.
顺序表基本操作的实现:
创建和访问:
- 创建空表
- 简单的判断操作
- 访问给定下标i的元素
- 遍历操作
- 查找给定元素d的位置
- 查找给定元素d在位置k之后的第一次出现位置
变动操作:
加入元素
- 在尾端加入新数据
- 新数据存入元素存储区的第i个单元
删除元素
- 尾端删除元素
- 删除位置i的数据
- 基于条件的删除
表的顺序实现的总结:
- 优点:O(1)时间的按位置访问元素;元素在表里存储紧凑,除表元素存储区之外只需要O(1)空间存放少量辅助信息.
- 缺点:需要连续的存储区存放表中元素,如果表很大,则需要大片的连续存储空间,一旦确定了存储区的大小,可容纳的单元个数并不随着插入删除元素操作的进行而改变,如果大片存储区只存储了少量数据,造成浪费.另外,在执行加入和删除操作,需要移动很多数据,效率低.很难事先估计出元素存储区的大小.
顺序表的结构:
两种基本实现方式:
一体式结构和分离式结构:
-
一体式结构:
存储信息单元与元素存储区一连续的方式安排在一块存储区里,整体性强,易于管理.计算公式:
$$
Loc(e_i)=Loc(L)+C+i*size(e)
$$
其中C是数据成分max,num的存储量. -
分离式结构:
表对象里只保存与整个表有关的信息.实际元素存放在另一个独立空间,通过链接与基本表对象关联,这样表对象大小统一,不同表对象可以关联不同大小的元素存储区,访问仍然是常量时间完成.
优点:
分离式存储最大的优点:可以在标识不变的情况下,为表对象换一块更大的存储区
如果是一体式结构,存储区满了,加入新元素就会失败,一般不可能直接扩大存储,只能另建容量更大的表,把之前的元素搬过去.
如果采用分离式结构,另外申请一块更大的元素新存储区,把表中已有的元素复制到新存储区.用新元素存储区替换原来的元素存储区,实际加入新元素.这样做出一个可扩容的表,只要程序的运行环境有空闲存储,不会因为满了而导致操作无法进行,这种技术实现的顺序表成为动态顺序表.
后端插入和存储区扩充:
动态顺序表的大小从0逐渐扩充到n,如果采取前端插入或者一般定位插入方式加入数据项,每次操作的时间开销与表长度有关,整个增长过程的时间复杂度是O(n^2)
那么后端插入呢?
由于不需要移动元素,一次操作的复杂度就是O(1),但是连续加入一些数据后,当前元素存储区满了,需要更换存储区,需要复制表中的元素,复制时间是O(m)(m是元素个数),那么怎么选择新存储区的大小?
存储区扩充?
线性增长:每次替换存储增加10个元素存储位置,复杂度是O(n).或者是假定表元素个数从0增加到1024,复杂度也还是O(n).不同的策略带来不同的操作复杂度.
Python的list:
python中的list与tuple就是采取了顺序表的实现技术.
tuple是不变的表,因此不支持改变内部状态的任何操作,在其他方面,他与list性质类似.
list的基本实现技术:
python标准类型list就是一种元素个数可变的线性表,可以加入和删除元素,在各种操作中维持已有的元素顺序.其重要实现约束有:
- 基于下边的高效元素访问和更新,时间复杂度是O(1)
- 允许任意加入元素,而且不断加入元素的过程中,表对象的标识(函数id等到的值)不变
- 由于要求O(1)时间的元素访问,并能维持元素的顺序,这种表只能采用连续表技术,表中的元素保存在一块连续存储区间.
- 要求能容纳任意多的元素,就必须能更换元素存储区.要想更换存储区是list对象的标识不变,只能采取分离式实现技术.
list就是一种采用分离式技术实现的动态顺序表.
python官方系统中,list实现才用了实际策略是建立空表时,系统分配可以容纳8个元素的存储区,如果元素存储区满了就换一个4倍大的存储区;如果表很大就改变策略,这里的很大是一个确定的参数50000,更换存储区时容量加倍.如上所述,这套技术实现的list,尾端加入元素的平均时间复杂度是O(1).
一些重要的操作:
- len()是O(1)操作
- 元素的访问,赋值,尾端加入,尾端删除都是O(1)
- 一般位置的元素加入,切片替换,切片删除,表拼接等都是O(n)操作,pop操作默认是删除尾端元素并返回,时间复杂度是O(1),指定非尾端的pop操作为O(n)时间复杂度.
- lst.clear()是O(1)
- lst.reverse()是O(n)
- 标准类型list仅有的特殊操作是sort,sort()函数的排序时间复杂度是O(nlogn)
python的一个问题:
没有提供检查一个list对象的当前存储容量操作,也没有设置容量的操作,一切与容量有关的处理都是python解释器自动完成的.
- 优点:降低变成负担,避免人为操作可能引起的错误.
- 缺点:限制了表使用方式.
顺序表的简单总结:
- 最重要的特点:是O(1)时间的定位元素访问
- 由于元素在顺序表的存储区里连续排列,加入和删除操作有可能移动很多元素,操作代价高.
- 只有特殊的尾端插入,删除操作时O(1)时间复杂度.
- 顺序表的优缺点都是在于其存储的集中方式和连续性,从缺点看,表结构不够灵活,不宜调整,变化,如果一个表的使用中需要经常修改结构,用顺序表实现,反复操作代价会很高.
4.链接表
线性表的另一种实现方式
线性表的基本需求:
-
能够找到表中的首元素
-
从表里的任意元素出发,可以找到它之后的下一个元素.
实现线性表的另一种的常用方式是基于链接结构,用链接关系显式表示元素之间的顺序关系,基于这种链接方式实现线性表成为链接表或链表.
采用链接方式实现线性表的基本思想:
- 把表中的元素分别存储在一批独立的存储块中.
- 保证从组成表结构中任一个结点可找到与其相关的下一个结点.
- 在前一个结点用链接方式显式地记录与下一结点之间的关联.
单链表:
单向链表的结点是一个二元组,a)图其表元素域elem保存着作为表元素的数据项,链接域里保存着同一个表里的下一个结点的标识.b)图从首结点p出发可以找到这个表的任意结点.
也就是说,为了掌握一个表,只需要用一个变量保存着这个表的首结点的引用,这样的变量是表头变量或表头指针.
总结:
- 一个单链表有一些具体的表结点组成
- 每个结点是一个对象,有自己的标识.
- 结点之间通过结点链接建立起单向的顺序联系.
基本链表操作:
-
创建空链表:
表头设置空链接
-
删除链表:
只需将表指针赋值为None,Python解释器自动收回不用的存储.
-
判断表是否为空:
将表头变量的值与空链表比较,在python'就是检查相应的变量的值是否是None
-
判断表是否满:
一般而言表不会满,除非程序用完所有的可用存储空间
加入元素:
表首端插入:
1.创建新结点并存入数据.
2.把原链表首结点的链接存入新结点的链接域next,这一操作将原表的一串结点链接在刚刚建立的新结点之后
3.修改表头变量,使之指向新结点.
class LNode:
def __init__(self,elem,next_=None):
self.elem = elem
self.next_ = next_
q = LNode(13)
q.next = head.next
head = q
一般情况的元素插入:
1.创建一个新结点并存入数据
2.把pre(变量pre指向要插入元素位置的前一节点)所指结点next域的值存入新结点的链接域next,这个操作将原表的在pre所指结点之后的一段链接到新结点之后
3.修改pre的next域,使之指向新结点
q = LNode(13)
q.next = pre.next
pre.next = q
删除元素
删除表首元素:
删除表的第一个结点,表头指针指向表中第二个结点
head = head.next
一般情况的元素删除:
修改前一个变量pre指向,另其指向后一个结点,丢弃的结点自动回收.
pre.next = pre.next.next
扫描,定位,遍历:
扫描:
由于单链表只有一个方向的链接,开始情况只有表头变量在掌握中,对表的检查只能是从表头变量开始,沿着表中的链接逐步进行,这种操作就是扫描
p = head
while p is not None and 条件:
对p的数据所需操作
p = p.next
这里的循环条件,循环中的操作有具体问题决定,循环中使用的辅助变量p是扫描指针.
按下标定位:
首结点的元素下标看作是0,其余依次排列,第i个元素所在的结点操作叫按下标定位.
p = head
while p is not None and i>0:
i -= 1
p = p.next
按元素定位:
p = head
while p is not None and not pred(p.elem):
p = p.next
pred谓词?
链表操作的复杂度:
-
创建空表:O(1)
-
删除表:O(1)
-
判断空表:O(1)
-
加入元素:
首端加入:O(1)
尾端加入:O(n)
定位加入:O(n)
-
删除元素:
首端删除:O(1)
尾端删除:O(n)
定位删除:O(n)
其他删除:通常也是扫描整个或部分表,O(n)
自定义异常:
首先为链表类定义一个新的异常类
class LinkedListUnderflow(ValueError):
pass
循环单链表
最后一个结点的next域不用None,而是指向表的第一个结点
同时支持O(1)时间的表头/表尾插入和O(1)时间的表头删除.
双链表:
结点之间的双向链接,不仅支持两端的高效性操作,一般结点的操作也会更加方便.这样也会付出代价,每个结点都需要增加一个链接域,增加空间开销与结点数成正比,复杂度O(n)
p.prev.next = p.next
p.next.prev = p.prev
使p所指的结点从表中退出,其余结点保持顺序和链接.
加入一个结点,则需要四次赋值.
双链表类:
class DLNode(LNode):
def __init__(self,elem,prev = None,next_ = None):
LNode.__init__(self,elem,next_)
self.prev = prev
循环双链表:
让表尾结点的next域指向表的首结点,而让表首结点的prev域指向尾结点.
在这种存在双向链接,不论是掌握着表的首结点还是尾结点,都能高效实现首尾两端的元素加入/删除操作O(1)复杂度.
两个链表的操作:
链表反转:
单链表支持元素反转,但是只支持从前向后,不支持从后向前.
对于链表有两种方法实现元素反转:
1.可以在结点之间搬动元素.
2.修改结点的链接关系,通过改变连接顺序改 变元素的顺序
不断的在表的首端取下结点,最后取下的就是尾结点.
def rev(self):
p = None
while self._head is not None:
q = self._head
self._head = q.next#摘下原来的首结点
q._next = p #p=None
p = q#将刚摘下来的结点加入p引用的节点序列
self._head = p
链表排序:
python中有sort()函数,将列表中的元素从小到大的进行排序,标准函数sorted,对各种序列进行排序,sorted(lst)生成一个新的表(lst类型的对象),其中的元素是lst的元素排序结果.
def list_sort(lst):
for i in range(1,len(lst)):
#开始时已将[0:1]片段排序好了
x = lst[i]
j = i
while j>0 and lst[j-1]>x:
lst[j]=lst[j-1]
j-=1
lst[j]=x
单链表的排序算法:
这里只有next链接,扫描只能向下一个方向移动.
1.移动表中的元素
基于移动元素的单链表的排序算法:
过程:扫描指针crt指向当前考虑节点(假定表元素是x),在一个大循环中每次处理一个表元素并前进一步,对一个元素的处理分两步:第一步:从头开始扫描小于或者等于x的元素,直至找到了第一个大于x的表元素,第二步:将x放在正确位置,将其他的表元素后移.
def sort1(self):
if self._head is None:
return
crt = self._head.next
while crt is not None:
x = crt.elem
p = self._head
while p is not crt and p.elem <= x:
p = p.next
while p is not crt:
y = p.elem
p.elem = x
x = y
p = p.next
crt.elem = x
crt = crt.next
2.调整结点之间的链接关系:
就是取下链表结点,将其插入一段元素递增的结点链中的正确位置.
函数里用rem记录除了第一个元素之外的结点段,然后通过循环把这些结点逐一插入_head关联的排序段.
def sort(self):
p = self._head
if p is None or p.next is None:
return
rem = p.next
p.next = None
while rem is not None:
p = self._head
q = None
while p is not None and p.elem<=rem.elem:
q =p
p = p.next
if q is None:
self._head =rem
else:
q.next = rem
q = rem
rem = rem.next
q.next = p
Josephus问题:
假设有n个人围坐在一起,要求从第k个人开始报数,报到m个数的人退出,然后从下一个人开始继续报数,并按照同样规则退出,直至所有退出,要求按顺序输出各出列人的编号.