数据结构|链表

完成阅读您将会了解链表的:

  1. 概念
  2. 构建方法
  3. 基本操作
  4. C++实现
  5. Rust实现

1. 概念

链表Linked List)最早在1955年由兰德公司(Rand Corporation)在其编写的信息处理语言中作为原始数据类型而开发。链表数组分别是两种基础数据储存类型——链式储存Linked Memory)与顺序储存Contiguous Memory)中线性数据Linear Data)的代表。

链表具有优秀的增删性能,在已知节点Accessed Node)附近插入或删除已知节点的时间复杂度为\(\Theta(1)\)。链表采用的链式结构提升了内存利用率Memory Utilization)。然而链表节点不只包含数据,需占用更多空间,且因为节点地址不连续,缓存友好不如数组。

链表的常见形式为单链表Singly Linked List)、双链表Doubly Linked List)、循环单链表Circular Singly Linked List)以及循环双链表Circular Doubly Linked List),分别如图1-4[1]。编程以单链表为例。

图1:单链表结构

图1:单链表结构

图2:双链表结构

图2:双链表结构

图3:循环单链表结构

图3:循环单链表结构

图4:循环双链表结构

图4:循环双链表结构

2. 构建方法

链表的构建重点在于节点Node)。节点中至少包含本身携带的数据以及相邻节点的访问入口。不同的链表在节点中的区别主要体现在邻接关系Adjacent Relation)。单链表每个节点拥有后续节点的单向访问权,而双链表节点拥有相邻节点的相互访问权。

构建单链表时,只须创建头结点并依次添加节点后返回头结点作为整个链表的访问入口。构建双链表过程与单链表类似,理论上最后返回表中任意一个节点都可以访问整表,但一般情况下习惯以头结点作为代表。

3. 基本操作

操作 描述 时间复杂度(单/双) 空间复杂度
AT(i) 访问元素 \(\Theta(n)\) \(\Theta(1)\)
INSERT(i,value) 链中i位置插入value \(\Theta(n)\) \(\Theta(1)\)
DELETE(I) 链中删除i位置节点 \(\Theta(n)\) \(\Theta(1)\)
PUSH_BACK(value) 链尾压入 \(\Theta(n)\)/\(\Theta(1)\) \(\Theta(1)\)
POP_BACK() 链尾弹出 \(\Theta(n)\)/\(\Theta(1)\) \(\Theta(1)\)
PUSH(value) 链头压入 \(\Theta(1)\) \(\Theta(1)\)
POP() 链头弹出 \(\Theta(1)\) \(\Theta(1)\)
SEARCH(value) 搜索value值节点 \(\Theta(n)\) \(\Theta(1)\)

如图5-8[1:1]及伪代码1,插入Insert)新节点与删除Delete)指定节点,如图9-12[1:2]和伪代码2-3,是链表的核心基础操作。
图5:准备插入新节点

图5:准备插入新节点

图6:插入步骤1

图6:插入步骤1

图7:插入步骤2

图7:插入步骤2

图8:完成插入

图8:完成插入

伪代码1:单链表插入节点
变量说明:N \(\rightarrow\) 插入位置前驱节点;K \(\rightarrow\)新节点

\[\begin{aligned} &INSERT(N,K)\\ &~~~~~~K.next \leftarrow N.next \\ &~~~~~~N.next \leftarrow K \end{aligned} \]


图9:准备删除目标节点

图9:准备删除目标节点

图10:删除步骤1

图10:删除步骤1

图11:删除步骤2

图11:删除步骤2

图12:完成删除

图12:完成删除

伪代码2:单链表删除节点
变量说明:N \(\rightarrow\) 待删除位置前驱节点;next \(\rightarrow\) 下个节点访问入口;garbage \(\rightarrow\) 内存垃圾袋

\[\begin{aligned} &DELETE(N)\\ &~~~~~~garbage \leftarrow N.next \\ &~~~~~~N.next \leftarrow N.next.next \\ &~~~~~~Recycle(garbage) \end{aligned} \]


对于单链表而言,除尾节点外,可以不借助其前驱节点,伪性地直接删除给定节点。


伪代码3:单链表删除节点(伪),
变量说明:N \(\rightarrow\) 非尾的待删除节点;next \(\rightarrow\) 下个节点访问入口;garbage \(\rightarrow\) 内存垃圾袋;value\(\rightarrow\)节点值/数据

\[\begin{aligned} &DELETE(N) \\ &~~~~~~garbage \leftarrow N.next \\ &~~~~~~N.value \leftarrow N.next.value\\ &~~~~~~N.next \leftarrow N.next.next \\ &~~~~~~Recycle(garbage) \end{aligned} \]


4. C++实现

template <class T> struct Node {
  T val;
  Node *next;
  Node(auto val, auto next) : val(val), next(next) {}
};

template <class T> class Linkedlist {
  Node<T> *head;
  size_t len;

public:
  Linkedlist() : head(nullptr), len(0) {}
  ~Linkedlist() {
    while (head) {
      auto garbage = head;
      head = head->next;
      delete garbage;
    }
  }
  inline void push(auto key) noexcept {
    head = new Node<T>(key, head);
    ++len;
  }
  inline void pop() noexcept {
    if (head) {
      auto garbage = head;
      head = head->next;
      delete garbage;
      --len;
    }
  }
  constexpr auto at(auto i) const noexcept {
    auto iter = head;
    while (i--)
      iter = iter->next;
    return iter;
  }
  constexpr auto size() const noexcept { return len; }
};

此外,C++标准库提供了单链表forward_list与双链表list直接使用。

5. Rust实现

impl<T> Linkedlist<T> {
    #[inline]
    pub fn new() -> Self { Self { head: None, size: 0 } }
    #[inline]
    pub fn push(&mut self, val: T) {
        self.head = Some(Box::new(Node { next: self.head.take(), val }));
        self.size += 1;
    }
    #[inline]
    pub fn pop(&mut self) {
        if let Some(node) = self.head.take() {
            self.head = node.next;
            self.size -= 1;
        }
    }
    pub fn at(&self, mut i: usize) -> Option<&T> {
        if i >= 0 && i < self.size {
            let mut iter = self.head.as_ref();
            while let Some(node) = iter {
                if i == 0 { break; }
                iter = node.next.as_ref();
                i -= 1;
            }
            Some(&iter.unwrap().val)
        } else { None }
    }
    #[inline]
    pub fn len(&self) -> usize { self.size }
}

impl<T> Drop for Linkedlist<T> {
    fn drop(&mut self) {
        let mut iter = self.head.take();
        while let Some(mut node) = iter { iter = node.next; }
    }
}

struct Linkedlist<T> {
    head: Option<Box<Node<T>>>,
    size: usize,
}

struct Node<T> {
    val: T,
    next: Option<Box<Node<T>>>,
}

此外,Rust标准库提供了双链表LinkedList直接使用。

6. 自我测试

伪代码实践
1. 遍历单链表;
2. 仅遍历一次,搜索给定单链表中间节点;
3. 仅遍历一次,搜索给定单链表倒数第n个节点;
4. 如果给定单链表有环,计算环的长度;
5. 判断两个可能有环的单链表是否相交以及寻找交点。

LeetCode选荐

  1. Linked List Cycle
  2. Linked List Cycle II
  3. Reverse Linked List II
  4. Reverse Nodes in k-Group
  5. Intersection of Two Linked Lists

测试参考解答

让每一天足够精致,期待与您的再次相遇! ^_^


  1. 图片引自tutorialspoint,在此鸣谢。 ↩︎ ↩︎ ↩︎

posted @ 2021-07-06 19:28  我的名字被占用  阅读(171)  评论(0)    收藏  举报