Python数据结构与算法(2.6)——块状链表

Posted 盼小辉丶

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Python数据结构与算法(2.6)——块状链表相关的知识,希望对你有一定的参考价值。

Python数据结构与算法(2.6)——块状链表

0. 学习目标

块状链表 (Unrolled Linked Lists) 是单链表的变体,其降低了访问单链表中指定位置元素的时间复杂度,块状链表中的每个块结点(简称块)中存储了多个数据元素结点,每个块中的结点使用一个循环链表进行连接。本节讲介绍块状链表的基本概念并实现其基本操作。
通过本节学习,应掌握以下内容:

  • 块状链表的基本概念及实现方法
  • 块状链表基本操作的实现及时间复杂度
  • 利用块状链表的基本操作实现复杂算法

1. 块状链表简介

1.1 块状链表介绍

与顺序表相比,链表的最大优势之一是在插入或删除元素并不需要移动数据元素位置,因此进行插入和删除操作更加高效,但是访问链表元素的时间复杂度为 O ( n ) O(n) O(n),为了提高数据访问操作的效率,单链表有一个简单的变体,称为块状链表。块状链表的每个结点(为了与数据元素结点进行区别,以下我们将其称为块)中都存储多个数据元素结点,在每个块中使用循环链表连接块内的数据元素结点(以下将数据元素结点简称为结点),块状链表示意图如下所示:

假设块状链表中的结点数量不超过 n n n 个,那么除了最后一个块之外,所有每个块中都包含 ⌈ n ⌉ \\lceil \\sqrt n \\rceil n 个元素(其中 ⌈ ⌉ \\lceil \\rceil 表示向上取整)。 因此,链表中块的数量不会超过 ⌊ n ⌋ \\lfloor \\sqrt n \\rfloor n 个(其中 ⌊ ⌋ \\lfloor \\rfloor 表示向下取整)。

1.2 块状链表中结点类

块状链表中的结点与单链表中的结点并无太大差别,我们可以直接使用单链表中的结点类,但为了更快的找到块内循环链表的尾结点(时间复杂度为 O ( 1 ) O(1) O(1),用以快速执行块状链表的一些操作),我们使用前驱指针来代替后继指针:

结点类实现如下:

class Node:
    def __init__(self, data=None):
        self.data = data
        self.previous = None
    
    def __str__(self):
        return str(self.data)

1.3 块状链表中块类

每个块类中都包含一个使用上述结点的单向循环链表,因此在此类中还需要包含一些循环链表的基本操作;同时每个块还包括一个指向下一个块的指针 next

Note:从上图可以看到块中循环链表不带头结点,这样做的原因是令头指针指向链表的第一个结点,获取第一个结点的时间复杂度为 O ( 1 ) O(1) O(1),可以方便块状链表的一些操作,接下来也可以看到不带头结点的链表与带有头结点的链表操作实现的不同之处。

1.3.1 块的初始化

块的创建需要初始化块的头指针 block_head 以及块的后继指针 next

class LinkedBlock:
    def __init__(self, data=None):
        self.block_head = None
        self.next = None
        self.node_count = 0

其中 next 指针指向下一个块,node_count 用于记录块中的节点数。

1.3.2 获取块中链表长度

获取块中链表长度只需要重载 __len__ 返回 node_count 属性即可:

    def __len__(self):
        return self.node_count

1.3.3 获取块中链表指定位置元素

由于使用的是前驱链,遍历块内链表时需要从链尾开始,为了获取元素在链表中的真实序号,我们需要首先获取链表长度,并在每次遍历一个元素后减 1,而位置 0 处的元素我们也可以在遍历开始时获得:

    def __getitem__(self, index):
        count = self.node_count
        current = self.block_head
        if index != 0:
            while count > index:
                current = current.previous
                count -= 1
        return current.data

同样,我们也可以修改块内链表指定位置元素:

    def __setitem__(self, index, value):
        count = self.node_count
        current = self.block_head
        if index != 0:
            while count > index:
                current = current.previous
                count -= 1
        current.data = value

1.3.4 删除块中链表指定位置元素

由于没有头结点,删除块中链表指定位置元素需要分为以下两种情况:

  • 要删除的结点是第一个结点,即头指针指向的结点
    • 遍历得到第一个结点的后继节点 tmp;
    • tmp 的前驱结点指向第一个结点的前驱结点;
    • 令头指针指向 tmp 结点即可
    • 如果链表中只有一个结点,那么只需要删除该结点,并令头结点指向 None

  • 要删除的结点不是第一个结点
    • 遍历到待删除位置结点 current
    • 将待删除结点的后继节点 nextprevious 指针指向待删除结点的前驱结点。

    def __delitem__(self, index):
        count = self.node_count
        current = self.block_head
        if index != 0:
            while count > index:
                next_node = current
                current = current.previous
                count -= 1
            next_node.previous = current.previous
        else:
            if self.node_count == 1:
                # 如果链表中只有一个结点
                self.block_head = None
            else:
                tmp = current
                while tmp.previous != self.block_head:
                    tmp = tmp.previous
                tmp.previous = self.block_head.previous
                self.block_head = tmp
        self.node_count -= 1
        del current

1.3.5 在块中链表指定位置插入结点

与删除操作类似,插入结点时也需要分为两种情况:

  • 插入链表头部
    • 如果链表为空,只需要将链表头指针指向待插结点,然后将其 previous 指针指向自身;
    • 否则,将当前链表的第一个结点的 previous 指针指向待插结点,待插结点的 previous 指针指向链尾结点,并将链表头指针指向待插结点。

  • 插入链表头部以外位置
    • 遍历链表找到插入位置结点 current
    • 将待插结点的 previous 指针指向 current 结点的前驱结点;
    • current 结点的 previous 指针指向待插结点。

    def insert_first(self, node):
        if self.node_count == 0:
            # 链表为空
            node.previous = node
        else:
            node.previous = self.block_head.previous
            self.block_head.previous = node
        self.block_head = node
        self.node_count += 1 
    def insert(self, index, node):
        if index == 0:
            # 在链表头插入结点
            self.insert_first(node)
        else:
            # 在其它位置插入节点
            count = self.node_count
            current = self.block_head
            while count > index:
                # 查找插入位置
                current = current.previous
                count -= 1
            # 插入新结点
            node.previous = current.previous
            current.previous = node
            self.node_count += 1

由于第一个结点的 previous 指针指向链尾结点,因此可以方便的实现 append 函数用于追加元素:

    def append(self, node):
        if self.node_count == 0:
            node.previous = node
            self.block_head = node
        else:
            node.previous = self.block_head.previous
            self.block_head.previous = node        
        self.node_count += 1

1.3.6 打印块中链表

使用 str 函数调用对象上的 __str__ 方法可以创建适合打印的字符串表示:

    def __str__(self):
        """使用|作为块与块之间的分割符"""
        s = "|"
        if self.block_head:
            current = self.block_head.previous
            count = 0
            while current != self.block_head:
                count += 1
                s = str(current) + s
                current = current.previous
                if count < self.node_count:
                    s = '<--' + s 
            s = str(self.block_head)  + s
        s = '|' + s
        return s

2. 块状链表的实现

块状链表中的块以单链表的形式进行连接,除了最后一个块,和第一个块外,都有一个前驱块和一个后继块。

2.1 块状链表的初始化

块状链表的初始化,首先实例化一个块,然后将链表的头指针 list_head 指向它,并初始化链表长度和每个块容纳的最大结点数。

import math

class UnrolledLinkedList:
    def __init__(self):
        block = LinkedBlock()
        self.list_head = block
        self.length = 0
        self.block_size = 4

其中 length 表示链表长度,block_size 表示每个块能容纳的最大结点数了,由于每个块中的节点数为 ⌈ n ⌉ \\lceil \\sqrt n \\rceil n ,因此 block_size 的大小会随着链表长度的变化动态改变。

2.2 获取块状链表的长度

重载 __len__ 从对象返回 length 的值用于求取块状链表长度:

    def __len__(self):
        return self.length

2.3 读取指定位置元素

在块状链表中,读取指定位置元素的时间复杂度为 O ( n ) O(\\sqrt n) O(n )

  1. 首先查找包含第 i i i 个结点的块,即第 ⌈ k ⌈ n ⌉ ⌉ \\lceil \\frac k \\lceil \\sqrt n \\rceil \\rceil n k 个块,其时间复杂度为 O ( n ) O(\\sqrt n) O(n ),因为我们可以通过遍历不超过 ( n ) (\\sqrt n) (n ) 个块找到它;
  2. 在该块中找到第 k % ⌈ n ⌉ k \\% \\lceil \\sqrt n\\rceil k%n 个结点,其中 % \\% % 表示求模运算;此步骤的时间复杂度同样为 ( n ) (\\sqrt n) (n ),因为单个块中的节点数不超过 ⌈ n ⌉ \\lceil \\sqrt n \\rceil n
    def __getitem__(self, index):
        if index > self.length - 1 or index < 0:
            raise IndexError("UnrolledLinkedList assignment index out of range")
        else:
            # 查找块号
            block_size = self.block_size
            i = (index + block_size) // block_size
            current_b = self.list_head
            i -= 1
            while i > 0:
                current_b = current_b.next
                i -= 1
            # 查找结点
            j = index % block_size
            value = current_b[j]
            return value

类似的,我们也可以实现修改指定位置元素的操作,只需要重载 __setitem__ 操作,它的时间复杂度同样为 O ( n ) O(\\sqrt n) O(n )

    def __setitem__(self, index, value):
        if index > self.length - 1 or index < 0:
            raise IndexError("UnrolledLinkedList assignment index out of range")
        else:
            # 查找块号
            block_size = self.block_size
            i = (index + block_size) // block_size
            current_b = self.list_head
            i -= 1
            while i > 0:
                current_b = current_b.next
                i -= 1
            # 查找结点
            j = index % block_size
            current_b[j] = value

2.4 查找指定元素

查找指定元素的操作与其它链表类似,遍历整个链表,如果在链表中发现与指定元素相同的数据节点,返回其在链表中的序号,此操作的时间复杂度为 O ( n ) O(n) O(n)

    def locate(self, value):
        current = self.list_head
        count = 0
        while current:
            index = current.locate(value)
            if index >= 0:
                count += index
                return count
            else:
                count = count + self.block_size + 1
                current = current.next
        raise ValueError(" is not in sequential list".format(value))

2.5 在指定位置插入新结点

假设我们在位置 index 处插入一个结点 node,并且 node 应该放在第 i i i 个块中,那么第 i i i 个块中的结点和第 i i i 个块之后的块中的结点必须进行移位操作——向链表尾部移动,以保证每个块中的结点数不超过 ⌈ n ⌉ \\lceil \\sqrt n \\rceil n ,如果链表的最后一个块空间不足——结点数超过 ⌈ n ⌉ \\lceil \\sqrt n \\rceil n ,则需要在尾部添加一个新块。完成上述操作后,可能还需要对块状链表进行动态调整,以满足块状链表规则。

    def insert(self, index, node):
        if index > self.length or index < 以上是关于Python数据结构与算法(2.6)——块状链表的主要内容,如果未能解决你的问题,请参考以下文章

链表算法:合并两个有序链表将小于x的数据置于链表前部分链表的回文判断

Python数据结构与算法(2.4)——双向链表

zList一个块状链表算法可以申请和释放同种对象指针,对于大数据量比直接new少需要差不多一半内存

Python数据结构与算法(2.7)——跳表

[转]OI省选算法汇总

数据结构与算法学习