链表

线性表:

​ 线性表是最基本的数据结构之一.线性表是一组元素的抽象.

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个数的人退出,然后从下一个人开始继续报数,并按照同样规则退出,直至所有退出,要求按顺序输出各出列人的编号.

posted @ 2019-08-14 21:41  醉醺醺的  阅读(194)  评论(0编辑  收藏  举报