三十功名尘与土,八千里路云和月


链表 (Linked list)是一种常见的基础数据结构,是一种线性表,但是并不会按线性的顺序存储数据,而是在每一个节点里存到下一个节点的指针(Pointer)。

链表中的基本要素:

  • 结点(也叫节点或元素),每一个结点有两个域,左边部分叫 数据域,用于存放用户数据;右边叫 指针域,一般是存储着到下一个元素的指针
  • head结点,一个特殊的结点,永远指向第一个结点
  • tail结点,一个特殊的结点,永远指向最后一个结点
  • None,链表中最后一个结点指针域的指针指向 None 值,因此也叫 接地点
  1. 必须明确指定链表第一项的位置,一旦我们知道第一项在哪里,第一项可以告诉我们第二项。依此类推,按照一个方向遍历,直到最后一项
  2. 这些节点在逻辑上相连的,但它们在物理内存上并不相连

Python 手动实现链表

接下来我们就是用 Python 来实现一个基础的 单向链表,在 C / C++ 中,通常采用 “指针+结构体” 来实现链表,而在 Python 中,我们可以采用 “引用+类” 来实现链表。

除了简单的链表结构,我们还需要实现如下功能:

  • is_empty() 测试链表是否为空
  • iter() 遍历链表
  • size() 返回链表的节点数
  • search() 查找链表中某节点
  • append() 在尾部增加一个节点
  • add() 在头部增加一个节点
  • remove() 将第一个具有传入值的节点从链表中删除

定义 Node节点类

节点 (Node)是实现链表的基本模块,每个节点至少包含两个重要部分。首先,包括节点自身的数据,称为 “数据域”,其次,每个节点包括下一个节点的 “引用”

class Node:
    def __init__(self, data):
        self.data = data  # 节点值
        self.next = None  # 指向下一个节点,默认None

    def __repr__(self):
        return f'<node data="{self.data}">'

创建一个 Node 对象试试:

n = Node('100')
print(n)       # <node data="100">
print(n.data)  # 100
print(n.next)  # None

此时节点对象数据为 '100',指针指向为 None

链表类

class Linkedlist:
    def __init__(self, node=None):
        self.head = node

在不传入 node 参数的情况下,此类实例后会生成一个空链表对象,初始化了 headtail 节点,且两节点都指向 None,实例化代码如下:

linked = Linkedlist()

is_empty 方法

is_empty 方法检查链表是否是一个空链表,这个方法只需要检查 head 节点是否指向 None 即可,代码如下:

def is_empty(self):
    """检测链表是否为空"""
    return self.head is None

add 方法

add 方法在链表前端添加节点

  1. 检测链表是否为空
  2. 若为空,则直接设置 head 引用新节点
  3. 若非空,首先设置 head 引用新节点,随后将新节点的下一个引用指向旧链表的第一个节点
def add(self, item):
    """添加节点到链表前端"""
    node = Node(item)  # 创建节点
    if self.is_empty():
        self.head = node  # 链表为空, 直接修改
    else:
        # 链表非空,修改 head指向 及 新节点的指向
        self.head, node.next = node, self.head  

size方法

size 方法用于获取链表中节点数量

def size(self):
   """获取链表长度"""
   count = 0  # 计数器,统计链表长度
   cursor = self.head  # 游标, 用于循环遍历链表
   while cursor is not None:
       count += 1
       cursor = cursor.next  # 更新游标位置
   return count

iter 方法

iter 方法用于遍历链表,思路同上

def iter(self):
    """遍历链表内容"""
    cursor = self.head  # 游标
    while cursor is not None:
        yield cursor
        cursor = cursor.next  # 更新游标位置

append 方法

append 方法用于在链表尾部添加节点

  1. 检测链表是否为空
  2. 若为空,则直接设置 head 引用新节点
  3. 若非空,遍历链表至最后一个节点,将该节点指向新节点
def append(self, item):
    """添加节点到链表末尾"""
    node = Node(item)  # 创建节点
    if self.is_empty():
        self.head = node  # 为空,则直接修改
    else:
        cursor = self.head  # 游标
        while cursor.next is not None:
            cursor = cursor.next  # 更新游标位置
        cursor.next = node

search 方法

search 方法用于检索链表中指定节点并返回

def search(self, item):
    """检索指定节点并返回"""
    if self.is_empty():
        return False
    else:
        cursor = self.head
        while cursor is not None:
            if cursor.data == item:
                return cursor  # 存在则返回
            cursor = cursor.next
        else:
            return False  # 不存在返回 False

remove 方法

remove 将第一个具有传入值的节点从链表中删除

实现思路:

  1. 检测链表是否为空
  2. 若为空,直接返回 False
  3. 若非空,遍历链表
    1. 该节点为 头部节点 …
    2. 该节点为 尾部节点 …
    3. 中间节点
def remove(self, item):
    """将第一个具有传入值的节点从链表中删除"""
    if self.is_empty():
        return False  # 链表为空, 直接返回 False
    else:
        cursor = self.head
        prev = None  # 记录上一个节点
        while cursor is not None:
            if cursor.data == item:
                if cursor == self.head:  # 若为 头部节点
                    self.head = cursor.next
                elif cursor.next == None:  # 若为 尾部节点
                    prev.next = None
                else:                      # 中间节点
                    prev.next = cursor.next
                return True
            # 更新游标
            prev = cursor
            cursor = cursor.next
        else:
            return False

链表 & 列表区别

  • 内存分布

    列表是一块连续的内存,而链表可以不是连续内存,链表的节点与节点之间通过指针来联系

  • 插入删除性能

    链表与列表最大的不同在于删除,插入的性能优势。由于链表是非连续的内存,所以不用像列表一样在插入,删除操作时需要进行大面积的成员位移

    例如在 a,b 节点之间插入一个新节点 c,链表只需要:

    1. a 断开指向 b 的执行,将指针指向 c
    2. c 节点将指针指向 b

    这个插入操作仅仅需要移动一下指针即可,插入,删除的时间复杂度只有 O(1)

  • 读取性能

    链表相比之下也有劣势,那就是读取操作远不如列表,列表的读取操作之所以高效,因为它是一块连续的内存,读取的时候可以通过寻址公式快速定位,而链表由于非连续内存,所以必须通过指针一个一个节点遍历

应用场景

  • 撤销功能,这种操作常见于各种文本,图形编辑器中,撤销重做在编辑器场景下属于家常便饭,单向链表由于良好的删除特性在这个场景很适用
  • 实现 图,hashMap 等一些高级数据结构

参考