02-链表

nobility 发布于 2020-04-04 1561 次阅读


链表

  • 动态数据结构,不需要处理固定容量问题,但是丧失了随机访问的能力
  • 将数据存储在一个单独的节点中(即一个对象中),该节点包括至少俩部分内容(即对象的属性)
    • 存储的数据
    • 指向下一个节点的指针(最后一个节点指向null)
  • 链表没有索引的概念,因为这些数据节点在内存中是分散的,由于链表没有索引的概念,为了找到指定位置的元素,只能设置了一个头指针(永远指向链表头部的指针),也就是说现在只能获得第一个节点,这就够了,因为第一个节点的next就是第二个节点,第二个节点的next就是第三个节点,依次类推,所以每次查找到对应的节点时都需要从头节点开始遍历每个节点的next

普通的单向链表

/**
 * @ClassName: LinkedList
 * @Description: 不带有虚拟头节点的自定义单向链表
 */
public class LinkedList<E> {
  private class Node { //私有的内部类,不希望暴漏给外部使用,使用该类创建节点对象并进行链表的组装
    private E data; //存储节点数据
    private Node next;  //存储下一个节点

    /**
     * @MethodName: Node
     * @Description: 创建节点对象,指定数据和下一个节点
     * @Param data: 数据
     * @Param next: 下一个节点
     */
    public Node(E data, Node next) {
      this.data = data;
      this.next = next;
    }

    /**
     * @MethodName: Node
     * @Description: 创建节点对象,只指定数据,下一个节点为null,即最后一个节点
     * @Param data:
     */
    public Node(E data) {
      this(data, null);
    }

    /**
     * @MethodName: Node
     * @Description: 创建节点对象,此时数据和下一个节点都是null,即最后一个节点并且数据也是null
     */
    public Node() {
      this(null, null);
    }

    @Override
    public String toString() {
      return this.data.toString(); //只打印数据,屏蔽掉节点指针
    }
  }

  private Node head;  //头节点指针,永远指着头
  private int size; //链表长度

  /**
   * @MethodName: LinkedList
   * @Description: 创建一个空链表
   */
  public LinkedList() {
    this.head = null;
    this.size = 0;
  }

  /**
   * @MethodName: getSize
   * @Description: 获取链表中元素的个数
   * @Return int
   */
  public int getSize() {
    return size;
  }

  /**
   * @MethodName: isEmpty
   * @Description: 判断链表是否为空
   * @Return boolean
   */
  public boolean isEmpty() {
    return size == 0;
  }

  /**
   * @MethodName: add
   * @Description: 向指定的位置插入指定的元素
   * 是另一种实现方式,统一了向每个位置添加时的操作,减少向链表头部添加的判断
   * @Param index:
   * @Param element:
   * @Return void
   */
  public void add(int index, E element) {
    if (this.size < 0 || index > this.size) { //若传入位置不合法则抛出异常
      throw new IllegalArgumentException("Add failed, Index is illegal");
    }
    /**
     * 分析:
     *     为指定位置添加指定元素需要先找到指定位置的前一个节点,并改变前一个节点的next
     *     指向要添加的节点,添加节点的next指向前一个节点原来的next
     *注意事项:
     *     改变next指针时需要注意修改的顺序,应该先将前一个节点原来节点的next赋给要添加
     *     节点的next后,再将前一个节点的next指向要添加的元素,否则会导致添加节点自己指
     *     向自己的情况
     */
    if (index == 0) {
      //对于向链表头部添加元素时,由于没有前一个节点,所以需要单独处理
      this.head = new Node(element, this.head); //创建新节点,为头节点
    } else {
      Node prev = this.head; //用来存储上一个节点的指针
      for (int i = 0; i < index - 1; i++) {
        //找前一个节点,需要遍历到指定位置的前一个,所以需要减1
        prev = prev.next; //指针后移
      }
      prev.next = new Node(element, prev.next); //添加节点
    }
    this.size++;  //添加后链表大小加1
  }

  /**
   * @MethodName: get
   * @Description: 用户根据索引获取元素内容
   * @Param index: 用户指定的索引
   * @Return E
   */
  public E get(int index) {
    if (this.size < 0 || index > this.size) { //若传入位置不合法则抛出异常
      throw new IllegalArgumentException("Get failed, Index is illegal");
    }
    Node cur = head; //存储当前节点的指针,从头节点开始遍历
    for (int i = 0; i < index; i++) {
      cur = cur.next; //当前节点指针后移
    }
    return cur.data;  //返回指定节点的内容
  }

  /**
   * @MethodName: set
   * @Description: 用户根据索引修改元素内容
   * @Param index: 用户指定的索引
   * @Param element: 用户指定要修改成的元素
   * @Return void
   */
  public void set(int index, E element) {
    if (this.size < 0 || index > this.size) { //若传入位置不合法则抛出异常
      throw new IllegalArgumentException("Set failed, Index is illegal");
    }
    Node cur = head; //存储当前节点的指针,从第头节点开始遍历
    for (int i = 0; i < index; i++) {
      cur = cur.next; //当前节点指针后移
    }
    cur.data = element; //修改指定节点内容
  }

  /**
   * @MethodName: contains
   * @Description: 根据用户指定的元素判断链表中是否包含该元素
   * @Param element: 用户指定的元素
   * @Return boolean
   */
  public boolean contains(E element) {
    Node cur = head; //存储当前节点的指针,从头节点开始遍历
    while (cur != null) { //若当前节点已经为null,说明当前节点指针已经指到了末尾
      if (element.equals(cur.data)) { //若与用户指定元素相同
        return true;  //返回true
      }
      cur = cur.next; //当前节点指针后移
    }
    return false; //若遍历完整个链表都没有相同的,就返回false
  }

  /**
   * @MethodName: remove
   * @Description: 根据用户指定索引删除节点
   * @Param index: 用户指定索引
   * @Return E
   */
  public E remove(int index) {
    if (this.size < 0 || index > this.size) { //若传入位置不合法则抛出异常
      throw new IllegalArgumentException("Remove failed, Index is illegal");
    }
    /**
     * 分析:
     *     为指定位置删除指定元素需要先找到指定位置的前一个节点,并改变前一个节点的next
     *     指向要被删除节点所指向的节点(即跳过要被删除节点)
     */
    Node delNode = null;  //要删除的节点
    if (index == 0) {
      //对于向链表头部删除元素时,由于没有前一个节点,所以需要单独处理
      delNode = this.head; //要删除的节点是头节点
      this.head = this.head.next; //头节点后移
    } else {
      Node prev = this.head; //用来存储上一个节点的指针
      for (int i = 0; i < index - 1; i++) {
        //找前一个节点,需要遍历到指定位置的前一个,所以需要减1
        prev = prev.next; //指针后移
      }
      delNode = prev.next; //要删除的节点是前一个节点的后面的一个节点
      prev.next = delNode.next; //将前一个节点的next指向要删除节点next所指向的节点,即跳过
    }
    delNode.next = null;  //将要删除节点的指向置为空,会与链表完全脱离,垃圾回收机制会快速回收
    this.size--;  //删除后需要将链表元素个数减1
    return delNode.data;  //返回删除的节点
  }

  /**
   * @MethodName: removeElement
   * @Description: 删除链表中指定元素,仅限第一个
   * @Param element: 要删除的元素
   * @Return void
   */
  public void removeElement(E element) {
    if (element.equals(this.head.data)) { //若头节点就是要删除的节点
      this.remove(0); //删除头节点
      return; //结束执行
    }
    Node perv = this.head; //用来存储上一个节点的指针,从第二个节点开始遍历
    while (perv.next != null) { //若上一个节点的下一个节点,也就是当前节点,不为空说明没有遍历到底
      if (element.equals(perv.next.data)) { //若当前节点是用户要删除的节点,就不再让指针后移
        break;
      }
      perv = perv.next; //指针后移
    }
    if (perv.next != null) {  //若当前指针指向有元素,则是要删除元素
      // 否则就是遍历到末尾也没有找到要删除元素,即删除元素不存在,不用删除
      Node delNode = perv.next; //要删除的节点
      perv.next = delNode.next;  //将前一个节点的next指向要删除节点next所指向的节点,即跳过
      delNode.next = null;   //将要删除节点的指向置为空,会与链表完全脱离,垃圾回收机制会快速回收
      this.size--;  //删除后需要将链表元素个数减1
    }
  }
  
  @Override
  public String toString() {
    StringBuilder sb = new StringBuilder();
    Node cur = this.head; //存储当前节点的指针,从第头节点开始遍历
    while (cur != null) { //若当前节点已经为null,说明当前节点指针已经指到了末尾
      sb.append(cur.data + "-> ");
      cur = cur.next; //当前节点指针后移
    }
    return "LinkedList{" +
        "linked=[" + sb +
        "]}";
  }
}

带有虚拟头的单向链表

对于向链表头部添加元素时,由于没有前一个节点,所以为了节点操作的逻辑统一,就在链表创建时,就创建一个虚拟节点(空的头节点),该节点对用户屏蔽,这时用户添加的节点就至少是第二个节点了,就算用户要向链表头插入元素,头节点就有前一个节点了,因为用户看到的头节点其实是第二个节点

/**
 * @ClassName: LinkedList
 * @Description: 带有虚拟头节点的自定义单向链表,仅仅是将链表头的操作和其他操作统一
 */
public class LinkedList<E> {
  private class Node { //私有的内部类,不希望暴漏给外部使用,使用该类创建节点对象并进行链表的组装
    private E data; //存储节点数据
    private Node next;  //存储下一个节点

    /**
     * @MethodName: Node
     * @Description: 创建节点对象,指定数据和下一个节点
     * @Param data: 数据
     * @Param next: 下一个节点
     */
    public Node(E data, Node next) {
      this.data = data;
      this.next = next;
    }

    /**
     * @MethodName: Node
     * @Description: 创建节点对象,只指定数据,下一个节点为null,即最后一个节点
     * @Param data:
     */
    public Node(E data) {
      this(data, null);
    }

    /**
     * @MethodName: Node
     * @Description: 创建节点对象,此时数据和下一个节点都是null,即最后一个节点并且数据也是null
     */
    public Node() {
      this(null, null);
    }

    @Override
    public String toString() {
      return this.data.toString(); //只打印数据,屏蔽掉节点指针
    }
  }

  private Node dummyHead;  //头节点指针,永远指着头(虚拟头)
  private int size; //链表长度

  /**
   * @MethodName: LinkedList
   * @Description: 创建一个空链表
   */
  public LinkedList() {
    this.dummyHead = new Node(); //创建链表时创建一个虚拟头节点
    this.size = 0;
  }

  /**
   * @MethodName: getSize
   * @Description: 获取链表中元素的个数
   * @Return int
   */
  public int getSize() {
    return this.size;
  }

  /**
   * @MethodName: isEmpty
   * @Description: 判断链表是否为空
   * @Return boolean
   */
  public boolean isEmpty() {
    return this.size == 0;
  }

  /**
   * @MethodName: add
   * @Description: 向指定的位置插入指定的元素
   * 是另一种实现方式,统一了向每个位置添加时的操作,减少向链表头部添加的判断
   * @Param index:
   * @Param element:
   * @Return void
   */
  public void add(int index, E element) {
    if (this.size < 0 || index > this.size) { //若传入位置不合法则抛出异常
      throw new IllegalArgumentException("Add failed, Index is illegal");
    }
    Node prev = this.dummyHead; //用来存储上一个节点的指针
    for (int i = 0; i < index; i++) {
      //找前一个节点,需要遍历到指定位置的前一个,但是现在多了一个虚拟节点,所以无需减1
      prev = prev.next; //指针后移
    }
    prev.next = new Node(element, prev.next); //添加节点
    this.size++;  //添加后链表大小加1
  }

  /**
   * @MethodName: get
   * @Description: 用户根据索引获取元素内容
   * @Param index: 用户指定的索引
   * @Return E
   */
  public E get(int index) {
    if (this.size < 0 || index > this.size) { //若传入位置不合法则抛出异常
      throw new IllegalArgumentException("Get failed, Index is illegal");
    }
    Node cur = this.dummyHead.next; //存储当前节点的指针,从第二个节点开始遍历,跳过虚拟头节点
    for (int i = 0; i < index; i++) {
      cur = cur.next; //当前节点指针后移
    }
    return cur.data;  //返回指定节点的内容
  }

  /**
   * @MethodName: set
   * @Description: 用户根据索引修改元素内容
   * @Param index: 用户指定的索引
   * @Param element: 用户指定要修改成的元素
   * @Return void
   */
  public void set(int index, E element) {
    if (this.size < 0 || index > this.size) { //若传入位置不合法则抛出异常
      throw new IllegalArgumentException("Set failed, Index is illegal");
    }
    Node cur = this.dummyHead.next; //存储当前节点的指针,从第二个节点开始遍历,跳过虚拟头节点
    for (int i = 0; i < index; i++) {
      cur = cur.next; //当前节点指针后移
    }
    cur.data = element; //修改指定节点内容
  }

  /**
   * @MethodName: contains
   * @Description: 根据用户指定的元素判断链表中是否包含该元素
   * @Param element: 用户指定的元素
   * @Return boolean
   */
  public boolean contains(E element) {
    Node cur = this.dummyHead.next; //存储当前节点的指针,从第二个节点开始遍历,跳过虚拟头节点
    while (cur != null) { //若当前节点已经为null,说明当前节点指针已经指到了末尾
      if (element.equals(cur.data)) { //若与用户指定元素相同
        return true;  //返回true
      }
      cur = cur.next; //当前节点指针后移
    }
    return false; //若遍历完整个链表都没有相同的,就返回false
  }

  /**
   * @MethodName: remove
   * @Description: 根据用户指定索引删除节点
   * @Param index: 用户指定索引
   * @Return E
   */
  public E remove(int index) {
    if (this.size < 0 || index > this.size) { //若传入位置不合法则抛出异常
      throw new IllegalArgumentException("Remove failed, Index is illegal");
    }
    Node prev = this.dummyHead; //用来存储上一个节点的指针
    for (int i = 0; i < index; i++) {
      //找前一个节点,需要遍历到指定位置的前一个,但是现在多了一个虚拟节点,所以无需减1
      prev = prev.next; //指针后移
    }
    Node delNode = prev.next; //要删除的节点
    prev.next = delNode.next; //将前一个节点的next指向要删除节点next所指向的节点,即跳过
    delNode.next = null;  //将要删除节点的指向置为空,会与链表完全脱离,垃圾回收机制会快速回收
    this.size--;  //删除后需要将链表元素个数减1
    return delNode.data;  //返回删除的节点
  }
  
  /**
   * @MethodName: removeElement
   * @Description: 删除链表中指定元素,仅限第一个
   * @Param element: 要删除的元素
   * @Return void
   */
  public void removeElement(E element) {
    Node perv = this.dummyHead; //用来存储上一个节点的指针
    while (perv.next != null) { //若上一个节点的下一个节点,也就是当前节点,不为空说明没有遍历到底
      if (element.equals(perv.next.data)) { //若当前节点是用户要删除的节点,就不再让指针后移
        break;
      }
      perv = perv.next; //指针后移
    }
    if (perv.next != null) {  //若当前指针指向有元素,则是要删除元素
      // 否则就是遍历到末尾也没有找到要删除元素,即删除元素不存在,不用删除
      Node delNode = perv.next; //要删除的节点
      perv.next = delNode.next;  //将前一个节点的next指向要删除节点next所指向的节点,即跳过
      delNode.next = null;   //将要删除节点的指向置为空,会与链表完全脱离,垃圾回收机制会快速回收
      this.size--;  //删除后需要将链表元素个数减1
    }
  }
  
  @Override
  public String toString() {
    StringBuilder sb = new StringBuilder();
    Node cur = this.dummyHead.next; //存储当前节点的指针,从第二个节点开始遍历,跳过虚拟头节点
    while (cur != null) { //若当前节点已经为null,说明当前节点指针已经指到了末尾
      sb.append(cur.data + "-> ");
      cur = cur.next; //当前节点指针后移
    }
    return "LinkedList{" +
        "linked=[" + sb +
        "]}";
  }
}

其他实现类型的链表

  • 双向链表:除啦存储的数据外,还有两个指针,一个指向前一个节点,一个指向后一个节点
  • 循环链表:尾节点指向虚拟头节点的双向链表,Java中LinkedList类就是基于循环链表实现的
  • 数组链表:将节点存入数组中,每个节点的next存储下一个节点在数组中的索引,若next为负则未链表结尾,仅适用于已知链表存储的元素个数时
此作者没有提供个人介绍
最后更新于 2020-04-04