zoukankan      html  css  js  c++  java
  • [Data Structure]线性表Linear List2

    线性表

    链接表(链表)

    线性表实现的基本需要:

    • 能够找到表中的首元素(无论直接或间接,通常很容易做到)
    • 从表里的任一个元素出发,可以找到它之后的下一个元素

    显然,把表元素保存在连续的存储区里,自然满足这两个需求,顺序关联是隐含的。但满足这两种需求,并不一定要连续存储元素

    实现线性表的另一方式是基于链接结构,用链接显式地表示元素之间的顺序关联。基于链接技术实现的线性表称为链接表或链表

    实现链接表的基本想法:

    • 把表元素分别存储在一批独立的存储块(称为结点)里
      • 保证从一个元素的结点可找到与其相关的下一个元素的结点
      • 在结点里用链接的方式显式记录元素(结点)之间的关联
    • 这样,只要知道表中第一个结点,就能顺序找到表里其他元素

    表的实现——链接表

    链接表有多种不同的组织方式。下面先讨论最简单的单链表,其中每个结点里记录着下一元素的结点的标识。

    单链表

    单链表结点的形式(链接域保存下一结点的标识)
    0f711736e57676680640be234efc5d5a.png

    在单链表里,与表里的 n 个元素对应的 n 个结点通过链接形成一条结点链。从表里的任一个结点都可以找到保存下一个元素的结点

    要掌握一个单链表,就需要(也仅需要)掌握表的首结点,从它

    • 可以找到表的首元素(表里保存的数据)
    • 还可以找到表中下一结点的位置
      按同样方式继续下去,就可以找到表里的所有数据元素

    表头变量:保存着链表第一个结点的标识(链接)的变量
    8cb892a7ff66d09b02df9bceddb328c9.png

    一个具体的表由一些具体结点构成

    • 每个结点(对象)有自己的标识(下面也常直接称其为链接)
    • 结点之间通过结点链接建立起顺序联系
    • 给表的最后一个结点(表尾结点)的链接域设置一个不会作为结点对象标识的值(Python 里自然应该用 None),称为空链接

    通过判断是否空链接,可以知道是否已经到了表的结束

    • 在做检索表中元素的工作时,据此判断检索工作是否完成
    • 如果表头指针的值是空链接,说明“它所引用的表已经结束”。没有元素就已经结束,说明这个表是空表

    在实现算法时,我们并不需要关心具体的表里各结点的具体链接的值是什么(它们总保存在表结构里),只需要关心链表的逻辑结构

    • 链表的操作也只需根据链表的逻辑结构考虑和实现

    单链表操作:基本操作

    考虑链接表的几个基本操作:

    • 创建空链表:只需将表头变量设置为空链接
      在 Python 里将其设置为 None
    • 删除链表:丢弃表的所有结点,与具体环境有关
      • 在一些语言(如 C 语言)里需要做许多事情,释放所用存储
      • 在 Python 里,只需简单将表指针设 None,就丢掉了整个链表的所有结点。 Python 程序的存储管理系统会自动回收不用的存储
    • 判断表是否为空:将表头变量的值与空链接比较
      • 在 Python 里检查其值是否为 None
    • 判断表是否满:链接表不会满,除非存储空间用完

    单链表操作:加入元素

    给单链表加入元素的一些基本情况

    • 位置可以为首端,尾端,定位。不同位置的操作复杂性可能不同
    • 加入元素不需要移动已有数据,只需为新元素安排一个新结点,然后把新结点连接在表里所需的位置

    通过修改链接,改变表的结构

    首端加入: 1) 创建一个新结点存入数据; 2) 把原链表首结点的链接存入新结点的链接域; 3) 修改表头变量使之引用新结点
    8e688d9396eba90a17b6b2f0cf1f5ce0.png

    尾端加入: 1) 创建一个新结点存入数据; 2) 表空时直接让表头变量引用这个新结点并结束,否则找到表尾结点; 3) 令表尾结点的链接域引用这一新结点,并将新结点的链接域设置为空链接
    a88944c4e5b2df5dd113a63cbb9c49b2.png

    定位加入: 1) 找到新结点加入位置的前一结点,不存在时结束; 2) 创建新结点存入数据; 3) 修改前一结点和新结点的链接域将结点连入
    78a119d04e7e71306355b9950ab8237e.png

    单链表操作:删除元素

    删除元素,所用技术与加入元素类似

    • 首端删除:直接修改表头指针,使之引用当时表头结点的下一个结点。Python 系统里会自动回收无用对象的存储块,下同
    • 尾端删除:找到倒数第二个结点,将其链接域设置为空链接
    • 定位删除:找到要删除元素所在结点的前一结点,修改它的链接域将要求删除的结点从表中去掉
      0aeb9ca386c2684a3d6294b54f87ab0a.png

    单链表操作:扫描和遍历

    许多操作中需要扫描表里一个个结点,可能检查其中的元素,如

    • 这种操作的过程称为遍历,顺序检查一个数据结构的所有元素
    • 求表元素的个数
    • 在表中查找特定位置的元素,或查找满足某些条件的元素
      进行这类操作,都需要用一个(或几个)扫描变量
      有些表操作比较复杂,例如表元素排序
      (排序 待续)

    单链表操作:复杂性

    基本操作:

    • 创建空表: O(1)
    • 删除表:在 Python 里是 O(1)。当然存储管理也需要时间
    • 判断空表: O(1)

    加入元素(都需要加一个 T(分配) 的时间):

    • 首端加入元素: O(1)
    • 尾端加入元素: O(n),因为需要找到表的最后结点
    • 定位加入元素: O(n),平均情况和最坏情况

    删除元素:

    • 首端删除元素: O(1);尾端删除: O(n)
    • 定位删除元素: O(n),平均情况和最坏情况
    • 其他删除通常需要扫描整个表或其一部分, O(n) 操作

    其他操作,如果需要扫描整个表或其一部分,都是 O(n) 操作。如

    • 求表的长度(表中元素个数)
    • 定位表中的元素;等等

    一类典型的表操作是扫描整个表,对表中每个元素做同样的工作(即遍历操作)。例如,输出所有的元素值,将它们累积到一个变量里等。
    这种工作可以通过一个循环完成
    遍历操作的复杂性应该是 O(n) * T(元素操作)

    有可能改造表的表示方式,提高一些操作的效率。例如,如果工作中经常需要求表长度,可以考虑采用下面结构(加一个表头对象):
    a741dbf021efd1dca81e45c3b7df39a2.png
     这样,在加入/删除元素时需要维护个数记录,有得有失

    单链表的Python实现

    实现链接结构,需要定义相应的类,首先是表示结点的类
     下面是一个简单的结点类:

     class LNode : # 只定义初始化操作
         def __init__(self, elm, nxt):
           self.elem = elm
           self.next = nxt
    

    简单的使用代码(Python 允许直接访问对象的普通数据域):

    llist1 = LNode(1, None); pnode = llist1
    for i in range(2, 11):
       pnode.next = LNode(i, None)
       pnode = pnode.next
    pnode = llist1
    while pnode is not None:
       print(pnode.elem)
      pnode = pnode.next
    

    单链表的实现:几个基本操作

    基于结点 LNode 定义一种链接表类型,为此定义一个表类

    class LList:
       def __init__(self):
          self.head = None
    
       def isEmpty(self):
          return self.head == None
        
       def prepend(self, elem):
           self.head = LNode(elem, self.head
    

    LList 对象只有一个 head 域,指向表中的首结点。几个操作(方法):

    • 初始建立的表里没有结点(空表)
    • 根据 head 的值判断是否空表
    • prepend 在表首端加入一个包含新元素的(新)结点

    单链表的实现:尾端加入

    append 在表最后加入一个包含新元素的结点

    def append(self, elem):
       if self.head == None:
       self.head = LNode(elem, None)
       return
    p = self.head
    while p.next != None:
        p = p.next
    p.next = LNode(elem, None)
    

    注意,这里需要区分两种情况:

    • 如果加入新元素时原表为空,就用 head 记录新加的结点
    • 如果表不空,需要先通过循环找到当时表里的最后一个结点,然后用这个结点的 next 域记录新结点的链接
    • 复杂性(最坏情况)显然为 O(n)

    单链表实现:首/尾端弹出

    首/尾端弹出元素的方法(删除操作与此类似)

    def pop(self): # 首端弹出
       if self.head == None:
           raise ValueError
       e = self.head.elem
       self.head = self.head.next
       return e
    
    def poplast(self): # 尾端弹出,显然复杂性为 O(n)
       if self.head == None: # empty list
          raise ValueError
       p = self.head
      if p.next == None: # list with only one element
         e = p.elem; self.head = None
         return e
       while p.next.next != None: # till p.next be the last node
       p = p.next
      e = p.next.elem; p.next = None
      return e
    

    单链表实现:其他操作

    def find(self, pred): # 在表里找到第一个满足 pred 的元素返回
        p = self.head
        while p != None:
           if pred(p.elem):
              return p.elem
             p = p.next
           return None
    
     def printall(self): # 输出表中所有元素
        p = self.head
       while p != None:
           print(p.elem)
           p = p.next
    

    单链表的变形

    单链表并非只有一种设计,可以根据需要和认识修改设计,例如

    • 前面实现的一个缺点是尾端加入操作的效率低
    • 实际中可能经常需要频繁地在表的两端加入元素

    一种可能是采用下面的结构,给表对象增加一个对表尾结点的引用:
    f603ef45971a03212e93a6d4dfc83025.png
     这样,在尾端加入元素,也能做到 O(1)

    • 注意:新设计的链表与前面单链表结构近似,结构变化应该只影响到表的变动操作,非变动操作不需要修改。有可能重用前面定义吗?

    单链表的变形:带尾节点引用

    面向对象技术支持基于已有的类(基类)定义新类(派生类)

    • 派生类继承其基类的所有功能(数据域和方法)
    • 派生类可以定义新的数据域,定义新的方法
    • 派生类可以重新定义基类里已定义的方法(覆盖已有方法)

    回到链表,我们可以基于 LList 定义(具有前述结构的)新链表类

    • 让它继承 LList 的所有非变动操作
    • 增加一个尾结点引用域,重新定义表的变动操作

    通过继承方式定义新类
    class LList1(LList) :

    Python 规定,定义时不注明基类,自动以公共类 object 作为基类
    前面的 LNode 和 LList 都以 object 作为基类

    LList1 定义为 LList 的派生类,覆盖 LList 的一些方法

    class LList1(LList):
       def __init__(self):
          LList.__init__(self) # 调用 LList 的初始化方法
          self.rear = None
    
    def prepend(self, elem):
         self.head = LNode(elem, self.head)
         if self.rear == None: # the empty list
         self.rear = self.head # rear points also to the new node
    
    def append(self, elem):
         if self.head == None:
            self.prepend(elem) # call prepend, do the same
        else:
            self.rear.next = LNode(elem, None)
            self.rear = self.rear.next
    

    首端和尾端删除方法也需要覆盖

    def pop(self):
         if self.head is None:
            raise ValueError
         e = self.head.elem
         if self.rear is self.head: # list with one node
            self.rear = None
         self.head = self.head.next
         return e
    
    def poplast(self):
         return None # to be implemented or simply use this
    

    带尾结点记录的单链表可以很好支持前/尾端加入和首端弹出元素

  • 相关阅读:
    最新超详细VMware虚拟机安装完整教程
    Java网络编程 -- AIO异步网络编程
    Java网络编程 -- NIO非阻塞网络编程
    Java网络编程 -- BIO 阻塞式网络编程
    Java网络编程 -- 网络协议
    自定义FutureTask实现
    JDK容器类List,Set,Queue源码解读
    JDK容器类Map源码解读
    深入理解Java中的锁(三)
    深入理解Java中的锁(二)
  • 原文地址:https://www.cnblogs.com/HuisClos/p/10367625.html
Copyright © 2011-2022 走看看