zoukankan      html  css  js  c++  java
  • 链表

    线性表:

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

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

  • 相关阅读:
    hihocoder1238 Total Highway Distance(树形dp)
    POJ2104 K-th Number(主席树)
    ansible安装使用入门
    TIDB资料收集
    elasticssearch+kibanna入门(撰写中)
    fabric-sdk-java在IDEA中的使用
    安装hyperledger fabric V1.0.1
    fabric读书笔记
    fabric默认样例的分析
    在eclipse中安装go编辑器阅读fabric代码
  • 原文地址:https://www.cnblogs.com/Zhao159461/p/11354893.html
Copyright © 2011-2022 走看看