python实现双向链表

写一个双向链表,实现append,pop,insert,remove,iter,__getitem__功能

数组: 数组(Array)是一种线性表数据结构。它用一组连续的内存空间,来存储一组具有相同类型的数据。

  • 数组需要一块连续的内存空间来存储,对内存的要求比较高。如果我们申请一个100MB大小的数组,当内存中没有连续的、足够大的存储空间时,即便内存的剩余总可用空间大于100MB,仍然会申请失败。
  • 链表恰恰相反,它并不需要一块连续的内存空间,它通过“指针”将一组零散的内存块串联起来使用,所以如果我们申请的是100MB大小的链表,根本不会有问题。

python实现双向链表_第1张图片

数组与链表的插入和删除操作:

  • 在进行数组的插入、删除操作时,为了保持内存数据的连续性,需要做大量的数据搬移,所以时间复杂度是O(n)

例如在数组索引1(1, 2, 3, 4)处插入,为保持内存数据的连续,还需把3,4往后搬移一个位置。

  • 在链表中插入或者删除一个数据,并不需要为了保持内存的连续性而搬移结点,因为链表的存储空间本身就不是连续的。针对链表的插入和删除操作,只需要考虑相邻结点的指针改变,所以对应的时间复杂度是O(1)

数组与链表的访问操作

  • 链表要想随机访问第k个元素,就没有数组那么高效了。因为链表中的数据并非连续存储的,需要根据指针一个结点一个结点地依次遍历,直到找到相应的结点。所以,链表随机访问的性能没有数组好,需要O(n)的时间复杂度
  • 数组根据首地址和下标,通过寻址公式就能直接计算出对应的内存地址,时间复杂度是O(1)。

数组的寻址公式:a[i]_address = base_address + i * data_type_size;其中data_type_size表示数组中每个元素的大小。

python实现双向链表_第2张图片

需要注意的是:尽管链表单纯删除操作时间复杂度是O(1),但遍历查找的时间是主要的耗时点,对应的时间复杂度为O(n)。根据时间复杂度分析中的加法法则,删除值等于给定值的结点对应的链表操作的总时间复杂度为O(n)。


1 需求分析

根据链表结构特征进行抽象,需要设计两个类

  • 节点类Item:记录节点元素信息,包括节点value,节点指针seek,seek指向上一个节点和下一个节点位置
  • 容器类LinkedList:保存每一个节点,记录链表头head、尾tail;因为数据在容器,因此还需在容器实现链表的增、删、改、查。

head、tail:记录起始、终止位置

指针概念:
有些语言有“指针”的概念,比如C语言;有些语言没有指针,取而代之的是“引用”,比如Java、Python。不管是“指针”还是“引用”,实际上,它们的意思
都是一样的,都是存储所指对象的内存地址。

将某个变量赋值给指针,实际上就是将这个变量的地址赋值给指针,或者反过来说,指针中存储了这个变量的内存地址,指向了这个变量,通过指针就能找到这
个变量。

单向链表和双向链表。 在实际的软件开发中,从链表中删除一个数据无外乎这两种情况:

  • 删除结点中“值等于某个给定值”的结点;
  • 删除给定指针指向的结点。

对于第一种情况,不管是单链表还是双向链表,为了查找到值等于给定值的结点,都需要从头结点开始一个一个依次遍历对比,直到找到值等于给定值的结点,然后再通过我前面讲的指针操作将其删除。
对于第二种情况,我们已经找到了要删除的结点,但是删除某个结点q需要知道其前驱结点,而单链表并不支持直接获取前驱结点,所以,为了找到前驱结点,我们还是要从头结点开始遍历链表。但是对于双向链表来说,这种情况就比较有优势了。因为双向链表中的结点已经保存了前驱结点的指针,不需要像单链表那样遍历。所以,针对第二种情况,单链表删除操作需要O(n)的时间复杂度,而双向链表只需要在O(1)的时间复杂度内就搞定了。
插入操作同理。

2 利用列表实现带bug的初始功能

class LinkedItem:
    def __init__(self, info, back_seek=None, front_seek=None):
        self.info = info
        self.back_seek = back_seek
        self.front_seek = front_seek


class LinkedList:
    def __init__(self):
        # 所谓的链表无序,是指链表的一个个节点在内存中存在任何地方,不是在堆栈中有序保存的,通过指针,指向下一个节点在内存中的位置。
        # 这里的用列表只是一个容器,节点总要保存吧,与链表无序是两回事,而且列表保存的是链表的引用,只是一个个ID。
        self.__linked_list = []  # 不需要插入的列表来说,检索方便,但是插入、remove不合适

    def append(self, node: LinkedItem):
        if self.__linked_list:
            self.__linked_list[-1].back_seek = id(node)
        self.__linked_list.append(node)

    def iter_nodes(self, reverse=False):
        n = len(self.__linked_list)
        while n > -1:
            yield self.__linked_list[n]
            n -= 1


class DoubleLinkedList(LinkedList):
    """使用列表索引,暴力解法
    优点:检索方便
    缺点:对于频繁插入、remove的运用场景,效率太低,不合适"""
    def __init__(self):
        super(DoubleLinkedList, self).__init__()
        self.__linked_list = []

    def append(self, node: LinkedItem):
        if self.__linked_list:
            self.__linked_list[-1].back_seek = id(node)
            node.front_seek = id(self.__linked_list[-1])
        self.__linked_list.append(node)

    def pop(self, node_seek):
        node_idx = None
        for idx, cur_node in enumerate(self.__linked_list):
            if id(cur_node) == node_seek:
                node_idx = idx
                break
        # todo 考虑起点、终点、以及节点不存在情况
        back_node = self.__linked_list[node_idx-1]
        back = self.__linked_list[node_idx + 1]
        back_node.back_seek = id(back)
        back.front_seek = id(back_node)
        return self.__linked_list.pop(node_idx)

    def remove(self, node: LinkedItem):
        node_id = id(LinkedItem)
        for idx, cur_node in enumerate(self.__linked_list):
            if id(cur_node) == node_id:
                # todo 考虑起点、终点、以及节点不存在情况
                self.__linked_list[idx-1].back_seek = id(self.__linked_list[idx+1])
                self.__linked_list[idx+1].front_seek = id(self.__linked_list[idx - 1])
                break
        return self.__linked_list.remove(node)

    def insert(self, node: LinkedItem, insert_seek):
        node_id = id(LinkedItem)
        insert_idx = None
        for idx, cur_node in enumerate(self.__linked_list):
            if id(cur_node) == insert_seek:
                # todo 考虑起点、终点、以及节点不存在情况
                insert_idx = idx+1
                self.__linked_list[idx].back_seek = node_id
                self.__linked_list[idx+1].front_seek = node_id
                node.front_seek = id(self.__linked_list[idx])
                node.back_seek = id(self.__linked_list[idx+1])
                break
        self.__linked_list.insert(insert_idx, node)

3 利用指针实现完整需求

class Item:
    def __init__(self, val, next_node=None, front_node=None):
        self.val = val
        self.next_node = next_node
        self.front_node = front_node

    def __repr__(self):
        return str(self.val)

    def __str__(self):
        return str(self.val)


class LinkedListImprove:
    """
    容器,记录节点,实现迭代、append、pop、insert、remove、getitem
    append:增加节点
    pop:从尾部弹出节点
    insert:在指定索引处插入节点,超界头部或尾部插入
    remove:移除指定索引处的节点,超界移除尾部
    getitem:获取指定索引节点
    """
    def __init__(self):
        self.head = None
        self.tail = None

    def append(self, val):
        """尾部增加节点"""
        node = Item(val)
        # 更新head、tail
        # 0个节点、1个节点、2个节点情况
        if self.head is None:
            self.head = node
        else:
            self.tail.next_node = node
            node.front_node = self.tail
        self.tail = node

    def iter_items(self, reverse=False):
        # 支持正反迭代
        current = self.tail if reverse else self.head
        while current:
            yield current
            current = current.front_node if reverse else current.next_node

    def getitem(self, idx):
        """获取指定索引处的节点"""
        current = None
        for i, tmp_node in enumerate(self.iter_items()):
            if i == idx:
                current = tmp_node
                break
        if current is None:
            raise IndexError('Index out of range')
        return current

    def pop(self):
        """从尾部弹出节点"""
        if self.head is None:  # 没有节点
            raise Exception('Empty')

        ret_node = self.tail
        prev = ret_node.front_node
        if prev is None:  # 只有1个节点
            self.head = None
        else:  # 两个或以上
            prev.next_node = None
        self.tail = prev
        return ret_node

    def insert(self, idx, val):
        """在指定索引处插入节点,超界尾部追加"""
        node = Item(val)
        current = None
        for i, tmp_node in enumerate(self.iter_items()):
            if i == idx:
                current = tmp_node
                break
        # 超界尾部追加
        if current is None:
            self.append(val)
            return
        # 头部插入,处理head
        prev = current.front_node
        if prev is None:
            node.next_node = current
            current.front_node = node
            self.head = node
        else:
            # 中间插入
            prev.next_node = node
            node.front_node = prev
            node.next_node = current
            current.front_node = node

    def remove(self, idx):
        """移除指定索引处的节点,超界return"""
        current = None
        for i, tmp_node in enumerate(self.iter_items()):
            if i == idx:
                current = tmp_node
                break
        if current is None:
            return
        prev = current.front_node
        next_node = current.next_node
        # 只有一个节点
        if prev is None and next_node is None:
            self.head = None
            self.tail = None
        # 头部移除,两个以上节点
        elif prev is None and next_node:
            next_node.front_node = None
            self.head = next_node
        # 尾部移除,两个以上节点
        elif prev and next_node is None:
            prev.next_node = None
            self.tail = prev
        else:
            prev.next_node = next_node
            next_node.front_node = prev
        del current


if __name__ == '__main__':
    # 测试
    lst = LinkedListImprove()
    item = Item('start')
    lst.append(item)
    item = Item(1)
    lst.append(item)
    item = Item(2)
    lst.append(item)
    item = Item(3)
    lst.append(item)
    item = Item(4)
    lst.append(item)
    item = Item(5)
    lst.append(item)
    item = Item(6)
    lst.append(item)
    item = Item('mid')
    lst.append(item)
    item = Item(7)
    lst.append(item)
    item = Item(8)
    lst.append(item)
    item = Item('end')
    lst.append(item)
    for n in lst.iter_items():
        print(n)
    print('~~~~~~~~~~~~~~~~~~~~~')
    lst.pop()
    lst.pop()
    lst.pop()
    for n in lst.iter_items():
        print(n)
    print('~~~~~~~~~~~~~~~~~~~~~')
    lst.insert(1, 0)
    lst.insert(0, 'enter')
    lst.insert(15, 'exit')
    for n in lst.iter_items():
        print(n)
    print('~~~~~~~~~~~~~~~~~~~~~')
    lst.remove(0)
    lst.remove(8)
    lst.remove(8)
    lst.remove(8)
    for n in lst.iter_items():
        print(n)
    print('~~~~~~~~~~~~~~~~~~~~~')

4 将链表封装成容器

容器功能的魔术方法有__len__、__getitem__、__iter__、__setitem__

功能简介

  • __getitem__(self, item):该方法实现了通过lst[key]方式访问容器元素的功能,对于字典,item就是key,对于序列对象item就是索引index,序列对象还切片功能也是通过该方法实现。
  • __setitem__(self, key, value): 该方法实现了通过lst[key] = value方法修改容器元素的功能,对于字典key、value就是键值对,序列对象就是在指定索引处插入的元素值.
  • __iter__: 必须返回一个可迭代对象。
  • __len__: 容器必须实现该方法。对于一个类的实例,如果位于逻辑表达式位置或bool(instance),如果类中未实现bool,则会调用__len__。这也是为什么容器位于逻辑表达式位置时可以等效布尔。

代码实现

from tool.logger_define import get_log


logger = get_log(__name__)


class Item:
    def __init__(self, val, back=None, prev=None):
        self.val = val
        self.back = back
        self.prev = prev

    def __repr__(self):
        return str(self.val)

    __str__ = __repr__


class LinkedList:
    def __init__(self):
        self.head = None
        self.tail = None
        self.len = 0

    def __len__(self):
        return self.len

    def __iter__(self, reverse=False):
        current = self.tail if reverse else self.head
        while current:
            yield current
            current = current.prev if reverse else current.back

    def __getitem__(self, idx):
        """获取索引为idx的节点;支持负向索引

        通过lst[key]实现访问,字典item就是key,序列对象就是索引index ,序列对象还要能切片
        """
        item = None
        idx = idx if idx >= 0 else (abs(idx) if abs(idx) > self.len else (idx + self.len))  # 正、负索引超界都返回None
        for i, cur_item in enumerate(self):
            if i == idx:
                item = cur_item
                break
        return item

    def __setitem__(self, idx, val):
        """通过lst[key] = value实现修改,字典就是key、value对,序列对象就是在指定索引处插入元素"""
        self.insert(idx, val)

    def insert(self, idx, val):
        """在指定索引插入节点, 超界尾部插入"""
        # 链表为空
        if self.head is None:
            self.append(val)
            return
        current_item = self[idx]
        logger.info("current_item={}".format(current_item))
        # 正向索引超界,尾部插入;负向索引超界,头部插入
        if current_item is None:
            if idx >= 0:
                self.append(val)
                return
            current_item = self.head
        item = Item(val)
        prev = current_item.prev
        current_item.prev = item
        item.back = current_item
        if prev is None:
            # 头部插入,更新head
            self.head = item
        else:
            item.prev = prev
            prev.back = item
        self.len += 1

    def append(self, value):
        """尾部追加节点"""
        item = Item(value)
        # 节点数为0
        if self.tail is None:
            self.head = item
        else:
            self.tail.back = item
            item.prev = self.tail
        self.tail = item
        self.len += 1

    def pop(self):
        """从尾部移除节点"""
        if self.tail is None:
            return
        ret_node = self.tail
        prev = self.tail.prev
        if prev is None:
            # 只有一个节点
            self.head = None
        else:
            prev.back = None
        self.tail = prev
        self.len -= 1
        return ret_node

    def remove(self, idx):
        """移除指定索引位置的节点"""
        current_node = self[idx]
        if current_node is None:
            raise IndexError("Index out of range")
        prev = current_node.prev
        back = current_node.back
        # 尾部移除
        if back is None:
            self.pop()
            return
        # 头部移除,更新头部
        if prev is None:
            self.head = back
        else:
            prev.back = back
        back.prev = prev
        self.len -= 1


def main():
    lst = LinkedList()
    # lst = GuardLinkedList()
    for j in ['start', 1, 2, 3, 4, 'mid', 5, 6, 7, 8, 9, 10, 'end']:
        lst.append(Item(j))
    logger.info('~~~~~~~~~测试迭代~~~~~~~~~~~~~~~')
    logger.info([cur_node for cur_node in lst])
    logger.info('~~~~~~测试索引~~~~~~~')
    logger.info("{}".format([lst[-len(lst)], lst[-1], lst[18], lst[-100]]))
    logger.info('~~~~~~~~测试__setitem__~~~~~~~~~~~')
    lst[len(lst)] = 'insert15'
    lst[0] = 'insert0'
    lst[3] = 'insert3'
    logger.info([cur_node for cur_node in lst])
    logger.info('~~~~测试remove操作~~~~~~~')
    logger.info("before:{}".format([cur_node for cur_node in lst]))
    logger.info(-len(lst))
    lst.remove(-len(lst)), lst.remove(-1), lst.remove(2)
    logger.info("after:{}".format([cur_node for cur_node in lst]))


if __name__ == '__main__':
    main()

5 使用哨兵,将双向链表改造为带头链表

针对链表的插入、删除操作,需要对插入第一个结点和删除最后一个结点的情况进行特殊处理。这样代码实现起来就会很繁琐,不简洁,而且也容易因为考虑不全而出错。如何来解决这个问题呢?

哨兵,解决的是国家之间的边界问题。同理,这里说的哨兵也是解决“边界问题”的,不直接参与业务逻辑。利用哨兵简化实现难度

python实现双向链表_第3张图片

如果我们引入哨兵结点,在任何时候,不管链表是不是空,head指针都会一直指向这个哨兵结点。这种有哨兵结点的链表叫带头链表。相反,没有哨兵结点的链表就叫作不带头链表。

代码实现–继承自LinkedList

from tool.logger_define import get_log


logger = get_log(__name__)

class GuardLinkedList(LinkedList):
    def __init__(self):
        super(GuardLinkedList, self).__init__()
        self.head = Item('Guard')
        self.tail = self.head
        self.len = 1

    def insert(self, idx, val):
        """在指定索引插入节点, 超界尾部插入"""
        current_item = self[idx]
        logger.info("current_item={}".format(current_item))
        # 正向索引超界,尾部插入;负向索引超界,头部插入
        if current_item is None:
            if idx >= 0:
                self.append(val)
                return
            current_item = self.head.back  # 负向索超界,取哨兵节点的下一个节点
        if current_item is self.head:
            # 解决索引为0时取到了哨兵节点
            current_item = self.head.back
        item = Item(val)
        prev = current_item.prev
        current_item.prev = item
        item.back = current_item
        item.prev = prev
        prev.back = item
        self.len += 1

    def append(self, value):
        """尾部追加节点"""
        item = Item(value)
        self.tail.back = item
        item.prev = self.tail
        self.tail = item
        self.len += 1

    def pop(self):
        """从尾部移除节点"""
        ret_node = None
        if self.tail is self.head:
            # 哨兵节点
            return ret_node
        ret_node = self.tail
        prev = ret_node.prev
        prev.back = None
        self.tail = prev
        self.len -= 1
        return ret_node

    def remove(self, idx):
        """移除指定索引位置的节点"""
        current_node = self[idx]
        if current_node is None:
            raise IndexError("Index out of range")
        if current_node is self.head:
            current_node = self.head.back
        prev = current_node.prev
        back = current_node.back
        # 尾部移除
        if back is None:
            self.pop()
            return
        prev.back = back
        back.prev = prev
        self.len -= 1

你可能感兴趣的:(数据结构与算法,python面向对象,python,链表)