Re:从零开始的数据结构 2 单链表

为了表示每个数据元素ai与其直接后继数据元素ai+1之间的逻辑关系,对数据元素ai来说,除了存储其本身的信息之外,还需存储一个指示其直接后继的信息(即直接后继的存储位置)。我们把存储数据元素信息的域称为数据域,把存储直接后继位置的域称为指针域。指针域中存储的信息称做指针或链。这两部分信息组成数据元素ai的存储映像,称为结点(Node)。

n个结点(ai的存储映像)链结成一个链表,即为线性表(a1,a2,...,an)的链式存储结构,因为此链表的每个结点中只包含一个指针域,所以叫做单链表。

头指针与头结点

头指针

链表中第一个结点的存储位置叫做头指针,如果有头结点则指向头结点。最后一个节点的指针指向空。

头结点

在单链表的第一个结点前附设一个结点,称为头结点。头结点的数据域可以不存储任何信息。也可以存储如线性表的长度等附加信息。

异同

头指针:

  1. 头指针具有标识作用,所以常用头指针冠以链表的名字。

头结点:

  1. 头结点不是必须的。
  2. 有了头节点,第一个节点的插入、删除等操作和其他节点一样。

空表

线性表为空,若有头结点,则头结点指向第一个节点的指针为空。

若没有头结点,则头指针为空。

节点

节点里面存储了数据域和指向下一个节点的指针。用一个类来表示。

  /**
   * 节点
   */
  private static class Node<T> {
    private T data; // 节点的数据
    private Node<T> next; // 指向下一个节点的指针
	
    public Node() {} 
     
    public Node(T data, Node<T> next) {
      this.data = data;
      this.next = next;
    }
  }
复制代码

单链表的初始化

单链表需要一个头指针和尾指针,尾指针一定是 null,可以不声明。可以选择有无头结点。还需要一个计数器来计数。

初始化时,有头结点的话需要把头指针指向头结点,头节点的指针指向 null。没有头结点的话头指针指向 null。

  private Node<T> head; // 头指针
  private Node<T> tail; // 尾指针
  private int size; // 节点的个数
  private boolean hasHeadNode = false; // 是否有头节点  
  
   /**
   * 初始化一个空表没有头结点
   */
  public MyLinkedList() {
    this.size = 0;
    head = null; // 头指针指向空
    tail = null; // 尾指针指向空
    hasHeadNode = false;
  }

  /**
   * 初始化一个带头结点的空表
   *
   * @param data 头结点的数据域,可以为 null
   */
  public MyLinkedList(@Nullable T data) {
    Node<T> headNode = new Node<>(); // 头节点
    headNode.data = data;
    headNode.next = null;
    // 头指针指向头结点
    head = headNode;
    tail = null; // 尾指针指向空
    this.size = 0;
    hasHeadNode = true;
  }
复制代码

查找

在顺序表查找是很容易的。单链表中没有计数,只存储了下一个节点的位置,所以我们每次都要从头遍历查找并计数。 因为单链表没有定义表厂,所以不可以用 for 循环来查找,核心思想是 工作指针后移,这也是很多算法的常用技术。 最坏的时间复杂度是O(n)。

查找第 i 个节点的步骤:

  1. 声明一个指针 p 指向链表第一个结点,初始化 j 从 1 开始;
  2. 当 j < i 时,就遍历链表,让 p 的指针向后移动,不断指向下一结点,j 累加 1;
  3. 若到链表末尾 p 为空,则说明第i个结点不存在;
  4. 否则查找成功,返回结点 p 的数据。
  /**
   * 查找某个节点的指针
   *
   * @param index 节点索引
   * @return 节点指针
   */
  private Node<T> getNode(int index) throws Exception {
    if (size == 0) {
      throw new Exception("表为空");
    }

    // 检查 index 是否合法
    checkInvalid(index);

    Node<T> p = head; // 初始化指针 p 指向第一个节点,即将头指针赋值给 p。

    if (hasHeadNode) { // 有头结点
      for (int i = 0; i <= index; i++) {
        p = p.next;
      }
    } else { // 没有头结点, 相比有头结点就要少循环一次
      for (int i = 0; i < index; i++) {
        p = p.next;
      }
    }
    return p;
  }
复制代码

添加

要把 s 插入到 p 的后面,只需要把 p 中的指针指向 s,将 s 的指针指向 p.next。

在表头和表尾插入:

在第 i 个数据后面插入节点的步骤:

  1. 声明一个指针 P 指向链表的头结点,初始化 j = 1。
  2. 当 j < i 时,就遍历链表,让 p 的指针向后移动,不断指向下一个节点,j 累加 1。
  3. 若到链表末尾 p 为空,则说明第 i 个结点不存在;
  4. 否则查找成功,在系统中生成一个空结点 s;
  5. 将数据元素 e 赋值给 s->data;
  6. 单链表的插入标准语句 s->next=p->next; p->next=s;
  7. 返回成功。

头插法和尾插法

不需要再特定位置插入的话,有头插法和尾插法 2 种方式,即在头部插入和在尾部插入。插入之前都需要先查找。

 /**
   * 默认是尾插法
   *
   * @param t 元素
   * @throws Exception
   */
  @SuppressWarnings("unchecked") @Override public void add(T t) throws Exception {
    Node<T> node = new Node();
    node.data = t;
    node.next = tail;

    if (size == 0) { // 空表时从第一个插入
      if (hasHeadNode) {
        head.next = node;
      } else {
        head = node;
      }
    } else {
      // 得到最后一个节点
      Node<T> lastNode = getNode(size - 1);
      // 指向新插入的节点
      lastNode.next = node;
    }
    size++;
  }

  /**
   * 头插法
   *
   * @param t 元素
   */
  @SuppressWarnings("unchecked") public void addAfterHead(T t) throws Exception {
    Node<T> node = new Node();
    node.data = t;
    node.next = tail;

    Node<T> p = head; // 将头指针赋值给临时指针 p
    if (size == 0) { // 空表时从第一个插入
      if (hasHeadNode) {
        head.next = node;
      } else {
        head = node;
      }
    } else {
      // 有头结点时,将头结点指针指向新插入的节点,将新插入节点的指针指向原头节点指针指向的节点
      if (hasHeadNode) {
        head.next = node;
        node.next = p.next;
      } else { // 没有头结点时,头指针指向新插入的节点,新插入的节点指向第一个节点
        head = node;
        node.next = p;
      }
    }
    size++;
  }
  
  
复制代码

在指定位置插入

想要在第 i 个位置插进去,就需要改变第 i-1 个位置节点的指针,新插进的节点的指针指向原来第 i 个位置的节点。

  /**
   * 指定插在某个位置
   *
   * @param index 元素索引
   * @param obj 元素实例
   * @throws Exception
   */
  @Override public void insert(int index, T obj) throws Exception {
    //检查 index 合法
    checkInvalid(index);

    Node<T> newNode = new Node<>();
    newNode.data = obj;

    if (index == 0) { // 如果要插入到第一个位置
      if (hasHeadNode) {
        newNode.next = head.next;
        head.next = newNode;
      } else {
        newNode.next = head;
        head = newNode;
      }
    }else {
      Node<T> node = getNode(index - 1);// 得到前一个节点
      newNode.next = node.next;
      node.next = newNode;
    }
    size++;
  }
复制代码

批量添加

上述每次添加一个都需要重新查找一遍节点,这样时间复杂度就是 O(n的平方),很费时。如果批量插入的话,只需要在插入第一个数据的时候查找一次,后续的节点直接接着插在末尾,时间复杂度只有0(n)。

这里我只实现了尾插法的批量添加:

  /**
   * 批量从尾部加入数据
   */
  @SuppressWarnings("unchecked") public void add(T... data) throws Exception {
    Node<T> p = null; // 临时指针 p 指向最后一个节点
    for (int i = 0; i < data.length; i++) {
      Node<T> node = new Node();
      node.data = data[i];
      node.next = tail;

      if (i == 0) {
        Log.d(TAG, "批量插入- 第一条开始");
        // 第一个先查找再插入,后续直接一个一个插到后面。
        if (size == 0) { // 空表时从第一个插入
          if (hasHeadNode) {
            head.next = node;
          } else {
            head = node;
          }
        } else {
          // 得到最后一个节点
          Node<T> lastNode = getNode(size - 1);
          // 指向新插入的节点
          lastNode.next = node;
        }
        size++;

        p = node;
        Log.d(TAG, "批量插入- 第一条结束");
      } else {
        Log.d(TAG, "批量插入- 第" + (i + 1) + "条开始");
        p.next = node;  // 最后一个节点指针指向新插入的节点
        size++;
        p = node;
        Log.d(TAG, "批量插入- 第" + (i + 1) + "条结束");
      }
    }
  }

复制代码

删除

要把 p 节点后面的节点删除,只需要将 p 节点的指针指向 p->next->next,即 p 的后面第二个节点。再将 p-> next 指向 null,删除 p->next 节点。

删除第 i 个节点的步骤:

  1. 声明一个指针 P 指向链表的头结点,初始化 j = 1。
  2. 当 j < i 时,就遍历链表,让 p 的指针向后移动,不断指向下一个节点,j 累加 1。
  3. 若到链表末尾 p 为空,则说明第 i 个结点不存在;
  4. 否则查找成功,将欲删除的结点 p->next 赋值给 p 的前一个节点 q->next;
  5. 删除第 i 个节点。
 /**
   * 删除某个位置的节点
   *
   * @param index 元素的索引
   * @throws Exception
   */
  @Override public void delete(int index) throws Exception {
    if (size == 0) {
      throw new Exception("表为空");
    }

    checkInvalid(index);

    if (index == 0) { // 如果是删除第一个位置
      if (hasHeadNode) {
        if (head.next.next == null) { // 如果索引为 1 的位置的节点为空,代表只有一个节点
          head.next = null; // 直接将头节点的指针指向空
        } else {
          head.next = head.next.next; // 直接头结点的指针指向索引为 1 的位置的节点
        }
      } else {
        if (head.next == null) { // 如果索引为 1 的位置的节点为空,代表只有一个节点
          head = null; // 直接将头指针指向空
        } else {
          head = head.next; // 直接头指针指向索引为 1 的位置的节点
        }
      }
    } else {
      Node<T> preNode = getNode(index - 1);// 得到前一个节点的指针
      preNode.next = preNode.next.next; // 将前一个节点的指针指向后一个
    }
    size--;
  }
复制代码

清空整张表

清空的只要把头结点指向空就行。

  /**
   * 清空整张表
   */
  public void clear() {
    this.size = 0;
    if (hasHeadNode) {
      head.next = null; // 头结点的指针指向空
    } else {
      head = null; // 头指针指向空
    }
  }

复制代码

和顺序表的比较

总结

从整个算法来说,我们很容易推导出:单链表的插入和删除的时间复杂度都是 O(n)。如果在我们不知道第i个结点的指针位置,单链表数据结构在插入和删除操作上,与线性表的顺序存储结构是没有太大优势的。但如果,我们希望从第i个位置,插入 10 个结点,对于顺序存储结构意味着,每一次插入都需要移动 n-i 个结点,每次都是 O(n)。而单链表,我们只需要在第一次时,找到第i个位置的指针,此时为 O(n),接下来只是简单地通过赋值移动指针而已,时间复杂度都是O(1)。

显然,对于插入或删除数据越频繁的操作,单链表的效率优势就越是明显。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值