数据结构与算法(四)链表(上)

206 阅读9分钟

链表(Linked List)

单链表

上篇文章我们设计了一个动态数组,但发现动态数组有个明显的缺点:如果存储不满的话,可能会造成内存空间的大量浪费

怎样才能做到用多少就申请多少呢?

链表就可以做到这一点

链表是一种链式存储的线性表,所有元素的内存地址不一定是连续的

-w1225

单链表的设计

下面我们就来设计一个链表

由于链表的大部分接口和动态数组是一样的,所以我们要先将结构做好划分

-w1287

完整的设计代码如下

声明一个接口文件

// 定义接口,只声明
// 接口里的声明默认都是公共的,不需要再加上public

public interface List<E> {
  // 暴露出去,可以给外界判断使用
  static final int ELEMENT_NOT_FOUND = -1;
  
  /**
   * 清除所有元素
   */
  void clear();

  /**
   * 元素的数量
   * @return
   */
  int size();

  /**
   * 是否为空
   * @return
   */
  boolean isEmpty();

  /**
   * 是否包含某个元素
   * @param element
   * @return
   */
  boolean contains(E element);

  /**
   * 添加元素到尾部
   * @param element
   */
  void add(E element);

  /**
   * 获取index位置的元素
   * @param index
   * @return
   */
  E get(int index);

  /**
   * 设置index位置的元素
   * @param index
   * @param element
   * @return 原来的元素ֵ
   */
  E set(int index, E element);

  /**
   * 在index位置插入一个元素
   * @param index
   * @param element
   */
  void add(int index, E element);

  /**
   * 删除index位置的元素
   * @param index
   * @return
   */
  E remove(int index);

  /**
   * 查看元素的索引
   * @param element
   * @return
   */
  int indexOf(E element);
}

将链表和动态数组的共同实现都抽取到一个父类中

// implements List:要实现该接口
// 在该类中实现公共的逻辑
// abstract:意味抽象类,可以不用完全实现接口中的声明,剩余不是公共的实现交给子类去实现
// 抽象类也是不可以创建实例的
// 该类不对外公开,目的只是为了抽取一些公共逻辑,不需要暴露让别人知道

public abstract class AbstractList<E> implements List<E>  {
  /**
   * 元素的数量
   */
  protected int size;
  
  /**
   * 元素的数量
   * @return
   */
  public int size() {
    return size;
  }

  /**
   * 是否为空
   * @return
   */
  public boolean isEmpty() {
    return size == 0;
  }

  /**
   * 是否包含某个元素
   * @param element
   * @return
   */
  public boolean contains(E element) {
    return indexOf(element) != ELEMENT_NOT_FOUND;
  }

  /**
   * 添加元素到尾部
   * @param element
   */
  public void add(E element) {
    add(size, element);
  }
	
  protected void outOfBounds(int index) {
    throw new IndexOutOfBoundsException("Index:" + index + ", Size:" + size);
  }
	
  protected void rangeCheck(int index) {
    if (index < 0 || index >= size) {
      outOfBounds(index);
    }
}
	
  protected void rangeCheckForAdd(int index) {
    if (index < 0 || index > size) {
      outOfBounds(index);
    }
  }
}

单链表的实现

import com.company.AbstractList;

// extends AbstractList:继承父类AbstractList

public class SingleLinkedList<E> extends AbstractList<E> {
  private Node<E> first; // 首节点
	
  // 内部类
  private static class Node<E> {
    E element; // 当前节点的元素
    Node<E> next; // 下一个节点
	
    public Node(E element, Node<E> next) {
      this.element = element;
      this.next = next;
    }
  }

  @Override
  public void clear() {
    size = 0;
    first = null; // 首节点为null,后面的所有节点也就都会被释放了
  }

  @Override
  public E get(int index) {
    return node(index).element;
  }

  @Override
  public E set(int index, E element) {
    Node<E> node = node(index);
    E old = node.element;
    node.element = element; // 拿到当前节点元素并覆盖
    return old;
  }

  @Override
  public void add(int index, E element) {
    rangeCheckForAdd(index); // 先判断边界元素是否合格
	
    if (index == 0) { // 如果是首元素,直接创建新的节点
      first = new Node<>(element, first);
    } else { // 如果是其他元素,找到其上一个节点来创建
      Node<E> prev = node(index - 1);
      prev.next = new Node<>(element, prev.next);
    }
    size++; // 增加容量
  }

  @Override
  public E remove(int index) {
    rangeCheck(index);
	
    Node<E> node = first;
    if (index == 0) { // 如果是首元素,指向首元素的下一个节点
      first = first.next;
    } else { // 如果是其他元素,找到其上一个节点,将其next指向要删除的节点的下一个节点
      Node<E> prev = node(index - 1);
      node = prev.next;
      prev.next = node.next;
    }
    size--; // 减少容量
    return node.element;
  }

  @Override
  public int indexOf(E element) {
    Node<E> node = first;
    if (element == null) {
      for (int i = 0; i < size; i++) {
        if (node.element == null) return i;
	
        node = node.next;
      }
    } else {
      for (int i = 0; i < size; i++) {
        if (element.equals(node.element)) return i;
	
        node = node.next;
      }
    }
    return ELEMENT_NOT_FOUND;
  }
	
  /**
   * 获取index位置对应的节点对象
   * @param index
   * @return
   */
  private Node<E> node(int index) {
    rangeCheck(index); // 首先判断边界元素是否合格
	
	// 从首节点一直循环找到index的位置 
    Node<E> node = first;
    for (int i = 0; i < index; i++) {
      node = node.next;
    }
    return node;
  }
	
  @Override
  public String toString() {
    StringBuilder string = new StringBuilder();
    string.append("size=").append(size).append(", [");
    Node<E> node = first;
    
    for (int i = 0; i < size; i++) {
      if (i != 0) {
        string.append(", ");
      }
	
      string.append(node.element);
      node = node.next;
    }
    
    string.append("]");
    
    return string.toString();
  }
}

设计点的详细讲解:

1.因为链表和动态数组的公共接口都是一样的,所以声明一个接口文件共同使用

再将链表和动态数组的共同实现抽取到一个公共父类里,两者都继承该父类的实现

不同的接口实现再单独在各自子类里取实现

// 接口声明
public interface List<E> { 
  ....
}

// 公共父类
public abstract class AbstractList<E> implements List<E>  { 
  ....
}

// 单链表
public class SingleLinkedList<E> extends AbstractList<E> {
  ....
}

// 动态数组
public class ArrayList<E> extends AbstractList<E> {
  ....
}

2.清除所有元素时,由于每一个节点都是被上一个节点引用着的,所以只要断掉首节点的引用,后面的节点就都会被释放掉了

@Override
public void clear() {
  size = 0;
  first = null; 
}

3.链表的查找元素,都是要从首节点一直向后遍历查找的

private Node<E> node(int index) {
  rangeCheck(index); 
    
  Node<E> node = first;
  for (int i = 0; i < index; i++) {
    node = node.next;
  }
  return node;
}

4.获取和更改元素时,都是要通过首节点,遍历找到对应位置的节点元素

@Override
public E get(int index) {
  return node(index).element;
}

@Override
public E set(int index, E element) {
  Node<E> node = node(index);
  E old = node.element;
  node.element = element; 
  return old;
}

5.增加和删除元素时,都是要区分首节点和其他节点的查找区别来对应处理的

@Override
public void add(int index, E element) {
  rangeCheckForAdd(index); 
    
  if (index == 0) { // 如果是首元素,直接创建新的节点
    first = new Node<>(element, first);
  } else { // 如果是其他元素,找到其上一个节点来创建
    Node<E> prev = node(index - 1);
    prev.next = new Node<>(element, prev.next);
  }
  size++; // 增加容量
}

@Override
public E remove(int index) {
  rangeCheck(index);
    
  Node<E> node = first;
  if (index == 0) { // 如果是首元素,指向首元素的下一个节点
    first = first.next;
  } else { // 如果是其他元素,找到其上一个节点,将其next指向要删除的节点的下一个节点
    Node<E> prev = node(index - 1);
    node = prev.next;
    prev.next = node.next;
  }
  size--; // 减少容量
  return node.element;
}

其他实现方案

有时候为了让代码更加精简,统一所有节点的处理逻辑,可以在最前面增加一个虚拟的头结点

虚拟的头结点不存储数据

-w1067

实现代码如下

public class SingleLinkedList<E> extends AbstractList<E> {
  private Node<E> first;
	
  public SingleLinkedList2() {
    first = new Node<>(null, null);
  }
	
  private static class Node<E> {
    E element;
    Node<E> next;
    public Node(E element, Node<E> next) {
      this.element = element;
      this.next = next;
    }
  }

  @Override
  public void clear() {
    size = 0;
    first.next = null;
  }

  @Override
  public E get(int index) {
    return node(index).element;
  }

  @Override
  public E set(int index, E element) {
    Node<E> node = node(index);
    E old = node.element;
    node.element = element;
    return old;
  }

  @Override
  public void add(int index, E element) {
    rangeCheckForAdd(index);
	
    Node<E> prev = index == 0 ? first : node(index - 1);
    prev.next = new Node<>(element, prev.next);

    size++;
  }

  @Override
  public E remove(int index) {
    rangeCheck(index);
	
    Node<E> prev = index == 0 ? first : node(index - 1);
    Node<E> node = prev.next;
    prev.next = node.next;
	
    size--;
    return node.element;
  }

  @Override
  public int indexOf(E element) {
    Node<E> node = first.next; // 从虚拟头结点的下一个获取
    if (element == null) {
      for (int i = 0; i < size; i++) {
        if (node.element == null) return i;
	
        node = node.next;
      }
    } else {
      for (int i = 0; i < size; i++) {
        if (element.equals(node.element)) return i;
	
        node = node.next;
      }
    }
    return ELEMENT_NOT_FOUND;
  }
	
  private Node<E> node(int index) {
    rangeCheck(index);
	
    Node<E> node = first.next;
    for (int i = 0; i < index; i++) {
      node = node.next;
    }
    return node;
  }
	
  @Override
  public String toString() {
    StringBuilder string = new StringBuilder();
    string.append("size=").append(size).append(", [");
    Node<E> node = first.next;
    for (int i = 0; i < size; i++) {
      if (i != 0) {
        string.append(", ");
      }
	
      string.append(node.element);
	
      node = node.next;
    }
    string.append("]");
    return string.toString();
  }
}

以上两种方案都可以,可以自行选择哪种更适用

复杂度

get函数set函数的三种复杂度是一致的:

  • 最好复杂度为O(1)
  • 最坏复杂度为O(n)
  • 平均复杂度为O(n)

最好复杂度和最坏复杂度都是需要调用node(int index)进行遍历的

最好复杂度也就是需要查找的元素刚好是首元素

最坏复杂度就是需要查找的元素是最后一个,那么就需要遍历整个节点来找

@Override
public E get(int index) {
  return node(index).element;
}

@Override
public E set(int index, E element) {
  Node<E> node = node(index);
  E old = node.element;
  node.element = element; 
  return old;
}

add函数remove函数的三种复杂度也是一致的:

  • 最好复杂度为O(1)
  • 最坏复杂度为O(n)
  • 平均复杂度为O(n)

最好复杂度也就是需要插入和移除的元素刚好是首元素,那么就不需要遍历

最坏复杂度就是需要插入和移除的元素是最后一个,那么就需要遍历整个节点来找

@Override
public void add(int index, E element) {
  rangeCheckForAdd(index); 
    
  if (index == 0) { // 如果是首元素,直接创建新的节点
    first = new Node<>(element, first);
  } else { // 如果是其他元素,找到其上一个节点来创建
    Node<E> prev = node(index - 1);
    prev.next = new Node<>(element, prev.next);
  }
  size++; // 增加容量
}

@Override
public E remove(int index) {
  rangeCheck(index);
    
  Node<E> node = first;
  if (index == 0) { // 如果是首元素,指向首元素的下一个节点
    first = first.next;
  } else { // 如果是其他元素,找到其上一个节点,将其next指向要删除的节点的下一个节点
    Node<E> prev = node(index - 1);
    node = prev.next;
    prev.next = node.next;
  }
  size--; // 减少容量
  return node.element;
}

动态数组和链表的复杂度对比

-w677

双向链表

使用双向链表可以提升链表的综合性能

-w1329

双向链表的设计

public class LinkedList<E> extends AbstractList<E> {
  private Node<E> first; // 首节点
  private Node<E> last; // 尾节点

  // 内部类
  private static class Node<E> {
    E element; // 当前节点的元素
    Node<E> prev; // 上一个节点
    Node<E> next; // 下一个节点
    
    public Node(Node<E> prev, E element, Node<E> next) {
      this.prev = prev;
      this.element = element;
      this.next = next;
    }
	
    @Override
    public String toString() {
      StringBuilder sb = new StringBuilder();
	
      if (prev != null) {
        sb.append(prev.element);
      } else {
        sb.append("null");
      }
	
      sb.append("_").append(element).append("_");

      if (next != null) {
        sb.append(next.element);
      } else {
        sb.append("null");
      }
	
      return sb.toString();
    }
  }

  @Override
  public void clear() {
    size = 0;
    first = null;
    last = null;
  }

  @Override
  public E get(int index) {
    return node(index).element;
  }

  @Override
  public E set(int index, E element) {
    Node<E> node = node(index);
    E old = node.element;
    node.element = element; // 拿到当前节点元素并覆盖
    return old;
  }

  @Override
  public void add(int index, E element) {
    rangeCheckForAdd(index);

    if (index == size) { // 往最后面添加元素
      Node<E> oldLast = last;
      last = new Node<>(oldLast, element, null);
      
      if (oldLast == null) { // 如果链表没有元素的时候,也会进来这里
        first = last;
      } else {
        oldLast.next = last;
      }
    } else { // 从前面添加元素
      Node<E> next = node(index); 
      Node<E> prev = next.prev; 
      Node<E> node = new Node<>(prev, element, next);
      next.prev = node;
	
      if (prev == null) { // index == 0,也就是插入到第一个节点的位置
        first = node;
      } else {
        prev.next = node;
      }
    }  
	
    size++;
  }

  @Override
  public E remove(int index) {
    rangeCheck(index);

    Node<E> node = node(index);
    Node<E> prev = node.prev;
    Node<E> next = node.next;
	
    if (prev == null) { // index == 0,也就是删除的首节点
      first = next;
    } else {
      prev.next = next;
    }
	
    if (next == null) { // index == size - 1,也就是删除的尾节点
      last = prev;
    } else {
      next.prev = prev;
    }
	
    size--;
    return node.element;
  }

  @Override
  public int indexOf(E element) {
    if (element == null) {
      Node<E> node = first;
      for (int i = 0; i < size; i++) {
        if (node.element == null) return i;
	
        node = node.next;
      }
    } else {
      Node<E> node = first;
      for (int i = 0; i < size; i++) {
        if (element.equals(node.element)) return i;
	
        node = node.next;
      }
    }
    return ELEMENT_NOT_FOUND;
  }
	
  /**
   * 获取index位置对应的节点对象
   * @param index
   * @return
   */
  private Node<E> node(int index) {
    rangeCheck(index);

    if (index < (size >> 1)) { // 分出两部分来查找,从前往后找
      Node<E> node = first;
      for (int i = 0; i < index; i++) {
        node = node.next;
      }
      return node;
    } else { // 从后往前找
      Node<E> node = last;
      for (int i = size - 1; i > index; i--) {
        node = node.prev;
      }
      return node;
    }
  }
	
  @Override
  public String toString() {
    StringBuilder string = new StringBuilder();
    string.append("size=").append(size).append(", [");
    Node<E> node = first;
    for (int i = 0; i < size; i++) {
      if (i != 0) {
        string.append(", ");
      }
	
      string.append(node);
	
      node = node.next;
    }
    string.append("]");
    return string.toString();
  }
}

设计点的详细讲解:

1.链表类增加尾节点的成员变量,便于从后往前查找

内部节点增加成员变量保存上一个节点,并调整构造方法

public class LinkedList<E> extends AbstractList<E> {
  private Node<E> first; // 首节点
  private Node<E> last; // 尾节点

  // 内部类
  private static class Node<E> {
    E element; // 当前节点的元素
    Node<E> prev; // 上一个节点
    Node<E> next; // 下一个节点
    
    public Node(Node<E> prev, E element, Node<E> next) {
      this.prev = prev;
      this.element = element;
      this.next = next;
    }
  }
}

2.清空所有元素时,首节点和尾节点都置为null,整个链表就会被释放了

双向链表看似循环引用,但Java中只要不是被gc root对象引用着的就会被释放

被栈指针指向的对象即为gc root对象,也就是局部变量引用的对象

例如LinkedList<Integer> list = new LinkedList<>()中创建的LinkedList对象即为gc root对象,所以函数作用域一旦结束,局部变量list所指向的LinkedList对象就会被销毁,链表也就会被释放

@Override
public void clear() {
  size = 0;
  first = null;
  last = null;
}

3.通过索引获取节点时,如果查找的是整个链表长度的前一半,顺序就是从前向后查找;如果查找的索引是在后一半,顺序就是从后向前查找

这样做的目的优化了单链表只能从前向后查找的效率

private Node<E> node(int index) {
  rangeCheck(index);

  if (index < (size >> 1)) { 
    Node<E> node = first;
    for (int i = 0; i < index; i++) {
      node = node.next;
    }
    return node;
  } else { // 从后往前找
    Node<E> node = last;
    for (int i = size - 1; i > index; i--) {
      node = node.prev;
    }
    return node;
  }
}

4.添加元素时,主要就是找到索引处的节点,然后将其上一个节点的next和下一个节点的prev指向需要添加的节点

添加元素时要区分临界元素的情况,如果是首尾节点的位置,要对应处理好指向null以及成员变量first、last的指向

如果链表还没有节点的时候,那么first、last指向的都是需要添加的节点,并且它的上一个节点和下一个节点都是null

@Override
public void add(int index, E element) {
  rangeCheckForAdd(index);

  if (index == size) { // 往最后面添加元素
    Node<E> oldLast = last;
    last = new Node<>(oldLast, element, null);
  
    if (oldLast == null) { // 如果链表没有元素的时候,也会进来这里
      first = last;
    } else {
      oldLast.next = last;
    }
  } else { // 从前面添加元素
    Node<E> next = node(index); 
    Node<E> prev = next.prev; 
    Node<E> node = new Node<>(prev, element, next);
    next.prev = node;
    
    if (prev == null) { // index == 0,也就是插入到第一个节点的位置
      first = node;
    } else {
      prev.next = node;
    }
  }  
    
  size++;
}

5.删除元素时,主要就是找到索引处的节点,然后将其上一个节点的next指向其下一个节点;其下一个节点的prev指向其上一个节点,这样该节点没有被任何指引了就会释放掉了

也是要注意临界元素的情况,首元素和尾元素的删除对应改变first、last的指向

@Override
public E remove(int index) {
  rangeCheck(index);

  Node<E> node = node(index);
  Node<E> prev = node.prev;
  Node<E> next = node.next;
    
  if (prev == null) { // index == 0,也就是删除的首节点
    first = next;
  } else {
    prev.next = next;
  }
    
  if (next == null) { // index == size - 1,也就是删除的尾节点
    last = prev;
  } else {
    next.prev = prev;
  }
    
  size--;
  return node.element;
}

双向链表的对比

双向链表对比单向链表

我们来对比下双向链表和单向链表的删除函数,发现操作数据会缩减一半

-w1093

双向链表对比动态数组

动态数组:开辟、销毁内存空间的次数相对较少,但可能造成内存空间浪费(可以通过缩容解决)

双向链表:开辟、销毁内存空间的次数相对较多,但不会造成内存空间的浪费

双向链表和动态数组的选择

如果频繁在尾部进行添加、删除的操作,动态数组、双向链表均可选择

如果频繁在头部进行添加、删除的操作,建议选择使用双向链表

如果有频繁的(在任意位置)添加、删除的操作,建议选择使用双向链表

如果有频繁的查询操作,建议选择使用动态数组

有了双向链表,那单链表是否没有任何用处了?

不是的,在哈希表的设计中就用到了单链表,具体详情请参照哈希表的设计章节