数据结构-03-链表

链表

定义

在计算机科学中,链表是数据元素的线性集合,其每个元素都指向下一个元素,元素存储上并不连续,链表中已知的是链表的头节点;

链表可以分类为:

  • 单向链表,每个元素只知道其下一个元素是谁
    在这里插入图片描述

  • 双向链表,每个元素知道其上一个元素和下一个元素
    在这里插入图片描述

  • 循环链表,通常的链表尾节点 tail 指向的都是 null,而循环链表的 tail 指向的是头节点 head

在这里插入图片描述
链表内还有一种特殊的节点称为哨兵(Sentinel)节点,也叫做哑元( Dummy)节点,它不存储数据,通常用作头尾,用来简化边界判断,如下图所示
在这里插入图片描述

随机访问性能

根据 index 查找,时间复杂度 O ( n ) O(n) O(n)

插入或删除性能

  • 起始位置: O ( 1 ) O(1) O(1)
  • 结束位置:如果已知 tail 尾节点是 O ( 1 ) O(1) O(1),不知道 tail 尾节点是 O ( n ) O(n) O(n)
  • 中间位置:根据 index 查找时间 + O ( 1 ) O(1) O(1)

单向链表

根据单向链表的定义,首先定义一个存储 value 和 next 指针的类 Node,和一个描述头部节点的引用

**
 * @Package com.cxd.consumer.linked
 *
 * @author chenxd8
 * @date 2023/9/28 10:20
 * @version V1.0
 */
public class SinglyLinkedList {
    
    

    // 头部节点
    private Node node;

    /**
     * 节点类
     */
  private static class Node {
    
    
    // 链表中保存的值
    Integer value;
    // 保存下一个节点的地址(指针)
    Node node;

    /**
     * * Node 定义为内部类,是为了对外**隐藏**实现细节,没必要让类的使用者关心 Node 结构
     * * 定义为 static 内部类,是因为 Node **不需要**与 SinglyLinkedList 实例相关,
     * 多个 SinglyLinkedList实例能共用 Node 类定义
     * @param value
     * @param node
     */
    public Node(Integer value, Node node) {
    
    
        this.node = node;
        this.value = value;
    }

    public Node() {
    
    

    }
  }
}

头部添加

  /**
   * 首部添加,每次添加进来的数据都需要将指针指向下一节点,下一节点就是上次的首届点
   * * 如果 this.head == null,新增节点指向 null,并作为新的 this.head
   * * 如果 this.head != null,新增节点指向原来的 this.head,并作为新的 this.head
   *   * 注意赋值操作执行顺序是从右到左
   * @param value
   */
  public void addFirst(Integer value) {
    
    
        this.head = new Node(value, this.head);
    }

遍历链表

  /**
   * while遍历
   * @param consumer
   */
  public void loopWile(Consumer<Node> consumer){
    
    
       Node curr = this.head;
       while (curr != null) {
    
    
         consumer.accept(curr.node);
         curr = curr.node;
       }
    }

  /**
   * 
   * @param consumer
   */
  public void loopFor(Consumer<Node> consumer) {
    
    
      for(Node curr = this.head; curr != null; curr = this.head.node) {
    
    
        consumer.accept(curr);
      }
    }

以上两种遍历都可以把要做的事以 Consumer 函数的方式传递进来

  • Consumer 的规则是一个参数无返回值,因此像 System.out::println 方法等都是 Consumer
  • 调用 Consumer 时,将当前节点 curr.value 作为参数传递给它

迭代器遍历

  • hasNext 用来判断是否还有必要调用 next
  • next 做两件事
    • 返回当前节点的 value
    • 指向下一个节点
  • NodeIterator 要定义为非 static 内部类,是因为它与 SinglyLinkedList 实例相关,是对某个 SinglyLinkedList 实例的迭代

递归遍历

尾部添加

  • 注意,找最后一个节点,终止条件是 curr.next == null
  • 分成两个方法是为了代码清晰,而且 findLast() 之后还能复用

尾部添加多个

  • 先串成一串 sublist
  • 再作为一个整体添加

根据索引获取

同样,分方法可以实现复用

插入

插入包括下面的删除,都必须找到上一个节点

删除

  • 第一个 if 块对应着 removeFirst 情况
  • 最后一个 if 块对应着至少得两个节点的情况
    • 不仅仅判断上一个节点非空,还要保证当前节点非空

单向链表(带哨兵)

观察之前单向链表的实现,发现每个方法内几乎都有判断是不是 head 这样的代码,能不能简化呢?

用一个不参与数据存储的特殊 Node 作为哨兵,它一般被称为哨兵或哑元,拥有哨兵节点的链表称为带头链表

  • 具体存什么值无所谓,因为不会用到它的值

加入哨兵节点后,代码会变得比较简单,先看几个工具方法

  • findNode 与之前类似,只是 i 初始值设置为 -1 对应哨兵,实际传入的 index 也是 [ − 1 , ∞ ) [-1, \infty) [1,)
  • findLast 绝不会返回 null 了,就算没有其它节点,也会返回哨兵作为最后一个节点

这样,代码简化为

对于删除,前面说了【最后一个 if 块对应着至少得两个节点的情况】,现在有了哨兵,就凑足了两个节点

猜你喜欢

转载自blog.csdn.net/weixin_46012189/article/details/133376907