zoukankan      html  css  js  c++  java
  • 数据结构与算法--线性表

    线性表的概念和表抽象数据类型

    表的概念和性质

    集合E上的一个线性表就是E中有一组有穷个元素排成的序列, 在一个表中可以包含0个或多个元素, 序列中每个元素在表里有一个确定的位置。称为该元素的下标。不包含任何元素的表成为空表。

    一个表中包含元素的个数称为这个表的长度。显然, 空表的长度为0

    在一个非空的线性表里, 存在着唯一的一个首元素和唯一的一个尾元素。除首元素之外的每个元素e都有且仅有一个前驱元素; 除尾元素外的每个元素e都有且仅有一个后继元素。

    表抽象数据类型

    ADT  List:                     # 一个表抽象数据类型
            List(self)              # 表构造操作, 创建一个新表
            is_empty(self)         # 判断self是否为一个空表
            len(self)                # 获得self的长度
            prepend(self, elem)      # 将元素elem作为第一个元素加入到表中
            append(self, elem)       # 将元素elem作为最后一个元素加入到表中       
            insert(self, elem, i)    # 将元素elem作为第i个元素加入到表中
            del_first(self)          # 删除表中的首元素
            del_last(self)           # 删除表中的尾元素
            del(self, i)             # 删除表中的第i个元素
            search(self, elem)       # 查找元素elem在表中出现的位置, 不出现时返回-1
            forall(self, op)         # 对表中的每个元素执行op

    其中, 各种操作中的self参数表示被操作的List对象, 其他参数用于为操作提供信息, 其中的elem参数均表示参与操作的表元素。i表示表中元素的位置(下标)。最后一个操作的op表示对表元素的某个具体的操作, 应该在实际调用遍历的时候提供。

    顺序表的实现

    基本实现方式

    最常见的情况是一个表里保存的元素类型相同, 因此存储每个元素所需的存储量相同, 可以在表里等距安排同样大小的存储位置。这种安排可以直接映射到计算机内存和单元, 表中任何元素位置的计算非常简单, 存取操作可以在O(1)时间内完成

    设有一个顺序表对象, 其元素存储在一片元素存储区,该存储区的起始位置(内存地址)已知是l。假定表元素编号从0开始, 元素e0自然应存储在内存位置LOC(e0) = l。再假定表中的一个元素所需的存储单元数为c=size(元素), 在这种情况下, 就有下面简单的元素ei的地址计算公式

    LOC(ei) = LOC(e0) + C * i

    表元素的大小size通常可以静态确定(例如, 元素是整数或实数, 或者包含一组确定元素的复杂结构), 在这种情况下, 计算机硬件将能支持高效的元素访问。另一方面, 如果表中元素的大小有可能不同, 也不难处理, 只需要略微改变顺序表的存储结构, 就仍然可以保持O(1)时间的元素访问操作

    如果表元素大小统一, 顺序表元素存储区的基本表示方式如图a所示, 元素的下标是其逻辑地址, 可以用上面的公式计算出其物理地址(实际地址), 元素访问是具有O(1)复杂度的操作

    如果表元素的大小不统一, 可以采用b布局方案, 将实际元素另行存储, 在顺序表中各单元位置保持其对应元素的引用信息(链接)。由于每个链接所需的存储量相同。通过上述统一公式, 可以计算出元素链接的存储位置,而后顺链接做一次间接访问, 就能得到实际元素的数据了。

    在建立一个顺序表时, 一种可能是按照建立时确定元素个数分配存储, 这种做法适合创建不变的顺序表, 例如Python的tuple对象。如果考虑变动的表, 就必须区分表中的元素个数和元素存储区的容量。在建立这种表时, 一个合理的做法就是分配一块足以容纳当前需要记录的元素的存储块, 还应该保留一些空块, 以满足满足元素的需求。

    元素存储区的大小决定了表的容量, 这是分配存储块时确定的, 对于这个元素存储区, 容量保存不变, 而元素个数的记录要与表中实际元素的个数保存一致, 在表元素变化时(加入或删除元素时), 都需要更新这一记录

    顺序表基本操作的实现

    假设用max表示表的容量, 即可能存储的最大元素的个数, num表示当前元素的个数

    创建和访问操作

      创建空表: 创建空表时, 需要分配一块元素存储, 记录表的容量并将元素计数值设置为0, 注意, 创建新表的存储区后, 应立即将两个表信息记录域(max和num)设置好, 保证这个表处于合法状态

      简单判断操作: 当且仅当num=0时表示表为空, 当且仅当num=max时表示表已满, 这两个简单操作的时间复杂度为O(1)

      访问给定下标i的元素: 访问表中第i个元素时, 需要检查i的值是否在表的合法元素范围内, 即0<=i<=num-1。超出范围的访问是非法访问。位置合法时需要计算出元素的实际位置, 由给定位置得到元素的值, 显然这个操作不依赖与表中元素的个数, 因此也是O(1)时间的操作

      遍历操作: 要顺序访问表元素, 只需在遍历过程中用一个整数变量标记遍历达到的位置。每次访问元素时, 通过存储区开始位置和上述标记变量的值在O(1) 时间内可算出相应元素的位置。完成元素的访问。找下一个元素的操作就是加1, 找前一个元素的操作就是减1。

      查找给定元素d的(第一次出现的)位置: 在没有给其他信息的情况下, 只能通过用d与表中的元素逐个比较的方式实现检索, 称为线性检索。这里只需要用一个基于下标的循环, 每步用d与当前下标的元素进行比较即可。

      查找给定元素d在位置k之后第一次出现的位置: 与上面操作的实现方式类似, 只是需要从k+1位置的元素开始比较。而不是从位置0开始。

    变动操作: 加入元素

      尾端加入新数据项: 这种情况下要求把数据项存入当时表中的第一个空位。即下标num的位置, 如果这时num=max, 即元素个数等于容量, 表满了, 操作就会失败, 如果表不满则直接存入元素, 并更新元素计数值, 显然这是一个O(1)操作。

      新数据项存入元素存储区的第i个单元: 首先需要检查下标i是否为合法位置, 从0到num都是合法位置。确定后就可以考虑插入了, 通常情况下位置i已有数据, 要把新数据存入这里, 就必须把该项数据移走。移动数据的方式需要根据操作的要求确定。如果操作不要求位置原元素的相对位置(不要求保存), 可以采用简单处理方式, 将原来位于I位置的元素移动到位置num, 放到其他已有元素之后, 腾出位置i放入新元素, 最后把num加1。这一操作仍然是O(1)操作。如果要求保持原有元素的顺序(保存), 必须把位置i之后的元素逐个下移, 最后把数据项存入位置i。一般而言受限于表中元素的个数, 最坏和平均情况都是O(n)

    变动操作: 删除元素

      尾端删除数据: 只需将元素计数值减一,这样, 原来的表尾元素不再位于合法的表下标范围, 相当于删除了。

      删除位置i的数据: 如果没有保存要求, 可以直接将当时位于num-1位置的数据项拷贝到位置i, 覆盖原来的元素。如果有保存要求, 就需要将位置i之后元素的数据项逐项上移。删除后修改表的元素计数值。

      基于条件的删除: 这种删除不是给定被删除元素的位置, 而是给出需要删除的数据项d本身, 这种操作也需要通过循环实现, 循环中逐个检查元素, 查到要找到的元素后删除。

      尾端删除元素和非保存定位删除操作的时间复杂度都为O(1)、而保存定位删除的时间复杂度为O(n)、基于条件删除的时间复杂度为O(n)

    顺序表的结构

    两种基本实现方式

    由于表的全局信息只需要常量存储, 对于不同的表, 这部分的信息完全规模完全相同。根据计算机内存的特点, 至少有两种可行的表示方式

    一体式结构: 存储表信息的单元与元素存储区以连续的方式安排在一块存储区里, 几部分数据的整体形成了一个完整的表对象。比较紧凑, 有关信息集中在一起,整体性强, 便于管理。由于连续存放, 从表对象L出发, 根据下标访问元素, 仍然可以用与前面类似的计算公式计算位置, 只需加一个常量:
                                              LOC(ei) = LOC(e0) + C + i * size(e)

    其中C是数据成分max和num的存储量。这种方式的缺点是元素存储区是表对象的一部分, 因此不同的表对象大小不一。另一个问题是创建后元素存储区就固定了。

    分离式结构: 表对象里只保存与整个表有关的信息(容量和元素个数)。实际元素存放在另一个独立的存储区内。通过链接与基本表对象关联。这种表对象的大小统一, 但不同表对象可以关联不同大小的元素存储区。这种实现的缺点是一个表通过多个(两个)独立的对象实现, 创建和管理工作复杂一些。

    替换元素存储区

    分离式实现的最大优点是带来了一种新的可能, 可以在标识不变的情况下, 为表对象换一块存储区。一般而言, 不可能直接扩大表的存储区。要想继续程序的工作, 就只能另外创建一个容量更大的表对象, 把元素搬过去。

    采用分离式技术实现替换元素存储区的操作过程如下:
    1) 另外申请一块更大的元素存储区

    2) 把表中已有的元素复制到新的元素存储区

    3) 用新的元素存储区替换原来的元素存储区(改变表对象的元素区的链接)

    4) 实际加入新元素

    把采用这种技术实现的顺序表也称为动态顺序表, 因为其容量可以在使用中动态变化。

    list的基本实现技术

    Python标准类型list就是一种元素个数可变的线性表, 可以加入和删除元素, 在各种操作中维持已有元素的顺序。在Python官方实现中, list就是采用一种分离式技术实现的动态线性表。

    在Python官方系统中, list采用了如下的实现策略: 在建立空表(或很小的表)时, 系统分配一块能容纳8个元素的存储区; 在执行插入操作(insert或append)时, 如果元素区满就换一块4倍大的存储区。但如果当时的表已经很大了, 系统将改变策略, 换存储区时容量加倍。这里"很大"是一个实现确定的参数, 目前的值为50000。引入后一个策略是为了避免出现多空闲的存储位置。

    一些主要操作的性质

    • 基于下标(位置)访问元素的时间复杂度为O(1)
    • len()是O(1)的操作, 因为表中必须记录元素个数
    • 元素的访问和赋值, 尾端加入和尾端删除(包括尾端切片删除)都是O(1)操作
    • 一般位置的元素加入、切片替换、切片删除、表拼接(extend)等都是O(n)操作。pop操作默认为删除表尾元素并将其返回, 时间复杂度为O(1), 一般情况下的pop操作(指定非尾端位置)为O(n)时间复杂度

    顺序表的常用操作如下:

    class OrderList(object):
    
        def __init__(self):
            self.max = 8
            self.num = 0
            self.list = []  # 表构造操作, 创建一个新表
    
        def is_empty(self):  # 判断self是否为一个空表
            if self.num == 0:
                return True
    
            return False
    
        def len(self):  # 获得self的长度
            return self.num
    
        def prepend(self, elem):  # 将元素elem作为第一个元素加入到表中
            if self.max == self.num:
                return '表已满'
    
            self.list.insert(0, elem)
            self.num += 1
            return self.list
    
        def append(self, elem):  # 将元素elem作为最后一个元素加入到表中
            if self.max == self.num:
                return '表已满'
    
            self.list.append(elem)
            self.num += 1
            return self.list
    
        def insert(self, elem, i):  # 将元素elem作为第i个元素加入到表中
            if self.max == self.num:
                return '表已满'
    
            self.list.insert(i, elem)
            self.num += 1
            return self.list
    
        def del_first(self):  # 删除表中的首元素
            if self.is_empty():
                return '表为空表表, 无法删除'
    
            self.list.pop(0)
            self.num -= 0
    
            return self.list
    
        def del_last(self):  # 删除表中的尾元素
            if self.is_empty():
                return '表为空表表, 无法删除'
    
            self.list.pop()
            self.num -= 0
            return self.list
    
        def del_i(self, i):  # 删除表中的第i个元素
            if 0 <= i <= self.num - 1:
                result = self.list.pop()
                self.num -= 0
                return self.list
            else:
                return '要删除的元素不存在'
    
        def search(self, elem):  # 查找元素elem在表中出现的位置, 不出现时返回-1
            for i in range(self.num):
                if elem == self.list[i]:
                    return i
            return -1
    
    
    if __name__ == '__main__':
        list = OrderList()
        print(list.is_empty())
        print(list.len())
        print(list.prepend(1))
        print(list.append(7))
        print(list.append(8))
        print(list.insert(1, 2))
        print(list.del_first())
        print(list.del_last())
        print(list.del_i(2))
        print(list.search(7))
    View Code

    链表

    链接表

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

    采用链接方式实现线性表的基本想法如下:

    • 把表中的元素分别存储在一批独立的存储块(称为表的结点)里。
    • 保证从组成表结构的任意一个结点可找到与其相关的下一个结点
    • 在前一节点里用链接的方式显式记录与下一个结点之间的关联

    这样只要能找到组成一个表结构的第一个结点, 就能顺序找到属于这个表的其他结点。从这些结点里可以看到表中的所有元素

    单链表

    单向链接表的结点是一个二元组, 其表元素域elem保存着作为表元素的数据项(或者数据项的关联信息), 链接域next里保存着一个表里的下一个结点的标识

    在最常见形式的单链表里, 与表里的n个元素对应的n个结点通过链接形式形成一条结点链。从引用表的首结点变量(变量P)可以找到这个表的首结点, 从表中任意一个结点可以找到保存着该表下一个元素的结点(表中下一结点)。

    如果想要掌握一个单链表, 那么只需要掌握这个表的首结点, 也就是说, 只需要用一个变量保存着这个表的首结点的引用(标识或称为链接)。这个变量就称为表头变量或表头指针

    为了表示一个链表的结束, 只需给表的最后结点(表尾结点)的链接域设置一个不会作为结点对象标识的值(称为空链接), 在Python中, 可以使用None表示这种情况。通过判断一个(域或变量)的值是否为空链接, 可知是否已到链表的结束。如果一个表头指针的值是空链接, 就说明"它所引用的链表已经结束", 这是没有元素的结束, 说明该表为空表。

    class LNode(object):  # 表节点类
        def __init__(self, elem, next_=None):
            self.elem = elem
            self.next = next_

    一个简单的表节点类, 这个类中只有一个初始化方法, 它给对象的两个域赋值, 方法的第二个参数用名字在next_, 是为了避免与标准函数next重名。

    基本链表操作

      创建空链表: 只需要把相应的表头变量设置为空链接, 在Python中设置为None

      删除链表: 应丢弃这个链表里所有的结点。在Python中只需要将表指针设置为None, 就抛弃了所有的结点

      判断表是否为空: 将表头变量的值与空链接比较。在Python中就是检查相应的变量的值是否为None

      判断表是否已满: 一般而言链表不会满, 除非程序用完了所有可用的存储空间

    加入元素

    在链表中加入新元素时,并不需要移动已有的数据, 只需为新元素安排一个新结点, 然后根据操作要求, 把新结点连在表中的正确位置。也就是说, 插入新元素的操作是通过修改链接, 接入新结点, 从而改变表结构的方式实现的

      表首端插入: 这是最简单的情况, 这一操作需要通过三步完成

        1) 创建一个新结点并存入数据

        2) 把原链表首结点的链接存入新结点的链接域next

        3) 修改表头变量, 使之指向新结点。

    注意, 即使插入前head指向是空表, 上面三步也能正常完成工作, 这个插入只是一次安排新存储和几次赋值, 操作具有常量时间复杂度

    示例代码块如下:

    q = LNode(13)
    q.next = head.next
    head = q

      一般情况的元素插入: 设变量pre已指向要插入元素位置的前一结点, 操作也分三步

        1) 创建一个新结点并存入数据

        2) 把pre所指结点next域的值存入新结点的链接域next

        3) 修改pre的next域, 使之指向新结点,

    注意: 即使在插入前pre所指结点是表中的最后一个结点, 上述操作也能将新结点正确接入, 作为新的表尾结点。

    示例代码如下:

    q = LNode(13)
    q.next = pre.next
    pre.next = q

    删除元素

      删除表首元素: 删除表中第一个元素对应于删除表的第一个结点, 为此只需要修改表头指针, 令其指向表中第二个结点。丢弃不用的结点将被Python自动回收

    示例代码如下:

    head = head.next

      一般情况下的元素删除: 一般情况删除需先找到要删除元素所在结点的前一结点, 设用变量pre指向, 然后修改pre的next域, 使之指向被删结点的下一结点。

    示例代码如下:

    pre.next = pre.next.next

    如果在其他编程语言中删除结点, 还可能要自己释放存储, Python的自动存储管理机制能自动处理这方面的问题。

    扫描、定位和遍历

    由于单链表只有一个方向的链接, 开始的情况只有表头变量在掌握中, 所以对表内容的一切检查都只能从表头变量开始, 沿着表中链接逐步进行。这种操作称为链表的扫描, 这种过程的基本操作模式是:

    p = head
    while p is not None and 还需其他的条件:
        对p所指节点里的数据所需的操作
        p = p.next

    循环中所用的辅助变量p称为扫描指针。注意, 每次扫描循环必须用一个扫描指针控制变量, 每步迭代前必须检查其值是否为None, 保证随后的合法性。

      按下标定位: 按Python的惯例, 链表的首结点的下标应看做为0, 其他元素依次排列。确定第i个元素所在结点的操作为按下标定位。

    p = head
    while p is not None and i > 0:
        i -= 1
        p = p.next

      按元素定位: 假设需要在链表里找到满足谓词pred的元素。

    p = head
    while p is not None and not pred(p.elem):
        p = p.next

    循环结束时或者p是None; 或者pred(p.elem)是True, 找到了所需的元素。

    完整的扫描称为遍历

    p = head
    while p is  not None:
        print(p.elem)
        p = p.next

    链表操作的复杂度

    • 创建空表: O(1)
    • 删除表: 在Python里是O(1)
    • 判断空表: O(1)
    • 加入元素(都需要加一个T(分配)时间):
      • 首端加入元素: O(1)
      • 尾端加入元素: O(n) 因为要找到表的最后一个结点
      • 定位加入元素: O(n) 平均情况和最坏情况
    • 删除元素: 
      • 首端删除元素: O(1)
      • 尾端删除元素: O(n)
      • 定位删除元素: O(n) 平均情况和最坏情况

    扫描、定位和遍历操作都需要检查一批结点, 其复杂度受到表结点的约束, 都是O(n)操作

    求表的长度

    在使用链表时, 经常要用到求表的长度, 为此可以定义一个函数:

    def length(head):
        p, n = head, 0
        while p is not None:
            n += 1
            p = p.next
    
        return n

    显然, 这个求表长度的算法所用的时间与表结点个数成正比, 具有O(n)时间复杂度

    List类的定义、初始化函数和简单操作

    现在基于结点类LNode定义一个单链表对象的类, 在这个表对象里只有一个引用链接结点_head域, 初始化为None表示建立的是空表。判断表空的操作是检查_head; 在表头插入数据的操作是prepend, 它把包含新元素的结点链接放在最前面; 操作pop删除表头数据结点并返回这个结点里的数据。

    # 自定义单链表异常类
    class LinkedListUnderflow(ValueError):
        pass
    
    
    # 单链表类
    class LList(object):
        def __init__(self):
            self._head = None
    
        def is_empty(self):
            return self._head is None
    
        def prepend(self, elem):
            self._head = LNode(elem, self._head)
    
        def pop(self):
            if self._head is None:  # 无结点, 引发异常
                raise LinkedListUnderflow('in pop')
            e = self._head.elem
            self._head = self._head.next
            return e

    这里把LList对象的_head域作为对象的内部表示, 不希望外部使用。

    后端操作

    在链表后端插入元素, 首先是一个扫描循环, 找到链表最后一个结点, 把包含新元素的结点插入在其后即可。

    def append(self, elem):
        if self._head is None:
            self._head = LNode(elem)
            return
        p = self._head
        while p.next is not None:
            p = p.next
        p.next = LNode(elem)    

    这里要区分两种情况, 如果原表为空, 引用新结点的就应该是表对象的_head域, 否则就是已有的最后结点的next域。

    删除最后一个结点, 要从链表删除一个结点, 就必须找到这个结点的前一个结点。在尾端删除操作时, 扫描循环应该找到表中倒数第二个结点, 也就是p.next.next为None的结点。需要处理两个特殊情况: 如果表空没有可返回的元素时应该引发异常。表中只有一个元素的情况下, 需要特殊处理, 因为这时候应该修改表头指针。一般情况下是先通过循环找到位置, 取出最后一个结点的数据并删除

    def pop_last(self):
        if self._head is None: # 空表
            raise LinkedListUnderflow('in pop_last')
        p = self._head
        if p.next is None: # 表中只有一个元素
            e = p.elem
            self._head = None
            return e
        while p.next.next is not None: # 直到p.next是最后结点
            p = p.next
        e = p.next.elem
        p.next = None
        return e

    其他操作

    LList类的下一个方法是找到满足条件的元素, 这个方法有一个参数, 调用时通过参数提供一个判断谓词, 该方法返回第一个满足条件谓词的表元素。

    def find(self, pred):
        p = self._head
        while p is not None:
            if pred(p.elem):
                return p.elem
            p = p.next
        

    查看表情况

    def printall(self):
        p = self._head
        while p is not None:
            print(p.elem, end=' ')
            if p.next is not None:
                print(',', end=' ')
            p = p.next
        print(' ')

    表的遍历

    传统的遍历是为了汇集对象类定义一个遍历函数, 它以一个操作为参数, 将其作用到汇集对象的每个元素上。

    def for_each(self, proc):
        p = self._head
        while p is not None:
            proc(p.elem)
            p = p.next

    proc的实参应该是可以作用与表元素的操作函数, 它将被作用域每个元素。

    list1.for_each(print)

    在Python中, 内部汇集类型的遍历机制是迭代器, 标准使用方式是放在for语句头部。要想定义迭代器, 最简单的是定义生成器函数

    def elements(self):
        p = self._head
        while p is not None:
            yield p.elem
            p = p.next

    有了这个方法, 在代码里就可以写

    for x in list1.elements():
        print(x)

    单链表实现的源代码如下:

    class LList(object):
        def __init__(self):
            self._head = None
    
        def is_empty(self):
            return self._head is None
    
        def prepend(self, elem):
            self._head = LNode(elem, self._head)
    
        def pop(self):
            if self._head is None:  # 无结点, 引发异常
                raise LinkedListUnderflow('in pop')
            e = self._head.elem
            self._head = self._head.next
            return e
    
        def append(self, elem):
            if self._head is None:
                self._head = LNode(elem)
                return
            p = self._head
            while p.next is not None:
                p = p.next
            p.next = LNode(elem)
    
        def pop_last(self):
            if self._head is None:  # 空表
                raise LinkedListUnderflow('in pop_last')
            p = self._head
            if p.next is None:  # 表中只有一个元素
                e = p.elem
                self._head = None
                return e
            while p.next.next is not None:  # 直到p.next是最后结点
                p = p.next
            e = p.next.elem
            p.next = None
            return e
    
        def find(self, pred):
            p = self._head
            while p is not None:
                if pred(p.elem):
                    return p.elem
                p = p.next
    
        def printall(self):
            p = self._head
            while p is not None:
                print(p.elem, end=' ')
                if p.next is not None:
                    print(',', end=' ')
                p = p.next
            print(' ')
    
        def for_each(self, pred):
            p = self._head
            while p is not None:
                pred(p.elem)
                p = p.next
    
        def elements(self):
            p = self._head
            while p is not None:
                yield p.elem
                p = p.next
    View Code

    链表的变形和操作

    单链表的简单变形

    单链表的一个缺点是尾端加入元素的效率低, 因为这时只能从表头开始查找, 直到找到表的最后一个元素, 而后才能链接新结点。能不能改进表的设计, 提高后端插入操作的效率?

    表对象增加一个表尾结点引用域。有了这个域, 只需常量时间就能找到尾结点, 在尾结点加入新结点的操作的时间复杂度为O(1)

    通过继承和扩充定义新链表类

    链表类LList提供了新链表类的许多功能, 应该尽可能的想办法利用。那么应该考虑把新链表类作为LList类的派生类

    class LList1(LList):
        pass  # 方法定义和其他

    初始化和变动操作

    在LList1的初始化函数中, 首先应该初始化LList对象的那些数据域, 然后还需要初始化一个尾结点引用域,用_rear作为域名, 将它也初始化为None

    def __init__(self):
        super().__init__()
        self._rear = None

    考虑尾端插入操作, 如果原来链表为空, 新加入的第一个结点也是最后一个结点, 这说明需要重新定义prepend覆盖原来的操作。

    可以使用检查_rear是否为None的方式判断空表, 实际上要求在表空的时候, 不仅_head是None, _rear也必须是None。当然, 只判断_head为空即可

    def prepend(self, elem):
        if self._head is None:
            self._head = LNode(elem)
            self._rear = self._head
        else:
            self._head = LNode(elem, self._head)

    增加了尾结点引用后, 可以直接找到尾结点, 在其后增加结点的操作也能更快完成

    def append(self, elem):
        if self._head is None:
            self._head = LNode(elem)
            self._rear = self._head
        else:
            self._rear.next = LNode(elem)
            self._rear = self._rear.next

    弹出元素的操作pop(), 不需要重新定义。

    最后是弹出末元素的操作, 现在删除了尾结点之后还需要更新_rear。由于确定了统一用_head的值判断表为空, 删除最后一个结点使表变空时, 不需要给_rear赋值为None.

    def pop_last(self):
        if self._head is None:  # 是空表
            raise LinkedListUnderflow('in pop_last')
        p = self._head
        if p.next is None:  # 表中只有一个元素
            e = p.elem
            self._head = None
            return e
        while p.next.next is not None:  # 直到p.next是最后结点
            p = p.next
        e = p.next.elem
        p.next = None
        self._head = p
       return e

    新链表类源码如下:

    class LList1(LList):
        def __init__(self):
            super().__init__()
            self._rear = None
    
        def prepend(self, elem):
            if self._head is None:
                self._head = LNode(elem)
                self._rear = self._head
            else:
                self._head = LNode(elem, self._head)
    
        def append(self, elem):
            if self._head is None:
                self._head = LNode(elem)
                self._rear = self._head
            else:
                self._rear.next = LNode(elem)
                self._rear = self._rear.next
    
        def pop_last(self):
            if self._head is None:  # 是空表
                raise LinkedListUnderflow('in pop_last')
            p = self._head
            if p.next is None:  # 表中只有一个元素
                e = p.elem
                self._head = None
                return e
            while p.next.next is not None:  # 直到p.next是最后结点
                p = p.next
            e = p.next.elem
            p.next = None
            self._head = p
    View Code

    循环单链表

    单链表另一个常见变形是循环单链表(简称循环链表), 其中最后一个结点的next域不用None, 而是指向表的第一个结点。在链表对象里记录表尾结点即可, 这样可以同时支持O(1)时间的表头/表尾插入和O(1)时间的表头删除。 

    class LCList(object):  # 循环单链表类
        def __init__(self):
            self._rear = None
    
        def is_empty(self):
            return self._rear is None
    
        def prepend(self, elem):  # 首端插入
            p = LNode(elem)
            if self._rear is None:
                p.next = p  # 建立一个结点环
                self._rear = p
            else:
                p.next = self._rear.next
                self._rear.next = p
    
        def append(self, elem):  # 表尾插入
            self.prepend(elem)
            self._rear = self._rear.next
    
        def pop(self):  # 前端弹出
            if self._rear is None:
                raise LinkedListUnderflow('in pop of LCList')
            p = self._rear.next
            if self._rear is p:
                self._rear = None
            else:
                self._rear = p.next
            return p.elem
    
        def printall(self):  # 输出表元素
            if self.is_empty():
                return
            p = self._rear.next
            while True:
                print(p.elem)
                if p is self._rear:
                    break
                p = p.next
    View Code

    双链表

    单链表只有一个方向的连接, 只能做一个方向的扫描和逐步操作, 即使增加了尾结点的引用, 也只能支持O(1)时间的首端元素的加入/删除和尾元素的加入操作。如果希望两端删除和插入操作都能高效完成, 需要加入另一方向的链接。这样就得到了双链表。当然这也是需要付出代价: 每个节点都增加了一个链接域, 增加的空间开销与结点数成正比, 是O(n)

    结点操作

    假定结点的下一结点的引用域是next, 前一结点的引用域是prev。

    先考虑结点的删除, 只要掌握着双链表里的一个结点, 就可以把它从表中取下, 并把剩余结点正确链接好。

    p.prev.next = p.next
    p.next.prev = p.prev

    双链表类

    双链表的结点与单链表的不同, 结点里多了一个反向引用域, 

    class DLNode(object):
        def __init__(self, elem, prev=None, next_=None):
            self.elem = elem
            self.prev = prev
            self.next = next_

    双链表源码如下:

    class DLList(object):  # 双链表类
        def __init__(self):
            self._head = None
            self._rear = None
    
        def prepend(self, elem):
            p = DLNode(elem, None, self._head)
            if self._head is None:  # 空表
                self._rear = p
            else:  # 非空表, 设置prev的引用
                p.next.prev = p
            self._head = p
    
        def append(self, elem):
            p = DLNode(elem, self._rear, None)
            if self._head is None:  # 空表插入
                self._head = p
            else:  # 非空表, 设置next的引用
                p.prev.next = p
            self._rear = p
    
        def pop(self):
            if self._head is None:
                raise LinkedListUnderflow('in pop of DLList')
            e = self._head.elem
            self._head = self._head.next
            if self._head is not None:  # _head空时不需要做任何事
                self._head.prev = None
            return e
    
        def pop_list(self):
            if self._head is None:
                raise LinkedListUnderflow('in pop_list of DLList')
            e = self._rear.elem
            self._rear = self._rear.next
            if self._rear is None:
                self._head = None
            else:
                self._rear.next = None
            return e
    View Code
  • 相关阅读:
    C# WPF 窗体传递消息
    WPF ProgressBar 样式
    WPF的TextBox以及PasswordBox显示水印文字
    Win8.1 Hyper-V 共享本机IP上网
    ASP.NET MVC入门-Startup 类:注册服务和使用中间件
    ASP.NET MVC入门-Program类:程序的入口
    Unity3d AssetBundle 资源加载与管理
    C#考核知识点总结
    数据结构与算法之美-字符串匹配(上)
    LeetCode-探索链表-综合问题
  • 原文地址:https://www.cnblogs.com/featherwit/p/13259018.html
Copyright © 2011-2022 走看看