一、什么是链表
定义:链式存储结构的特点是用一组任意的存储单元存储线性表的 数据元素,这组存储单元可以是连续的,也可以是不连续的。
物理存储结构如下:图中每一个节点均代表存储的数据,并且都包含该节点的下一个节点的位置信息。
二、链表的分类
我们可以把链表分为3类:
1)单链表 : 链表中的元素节点只能指向下一个节点或者空节点,节点直接不能相互指向。
2)双向链表 : 链表中的每个元素节点都有两个指向,一个指向上一个节点,另一个指向下一个节点。
3)循环链表 : 在单链表和双向链表的基础上,将两种链表的最后一个节点指向第一个节点的头部,从而形成循环。
我们本节只用java实现双向链表。
三、双向链表的实现
我们通过双向链表来手动实现LinkedList,具体看代码实现。
1 class LinkedList<E>{ 2 3 private transient int size; //维护大小 transient序列化时该值不被序列化 4 5 private transient Node<E> first; //第一个节点 6 7 private transient Node<E> last; //最后一个节点 8 9 public LinkedList(){ 10 this.size = 0; //初始化节点大小为0 11 } 12 //增 13 public boolean add(E element){ //在末尾添加节点 14 final Node<E> l = last; //最后一个节点 15 final Node<E> newNode = new Node(l, element, null); //构建需要添加的新节点 16 last = newNode; //因为是往末尾添加元素,不管任何情况,该节点必定为最后一个节点,所以last标记为最后一个节点 17 if(l == null) //如果最后一个节点为空,则说明该链表还没有节点,则创建的节点即为头节点也为尾节点 18 first = newNode; 19 else //不为空,则说明尾节点需要将下一个节点的位置指向刚新建的节点 20 l.next = newNode; 21 size ++; 22 return true; 23 } 24 public boolean add(int index, E element){ //在index位置添加element,将原来index位置的节点挂在新添加的节点上 25 checkIndex(index); 26 if(index == size) //如果添加位置和size一样大,说明是在末尾添加 27 add(element); 28 else //非末尾添加 29 linkNode(element, node(index)); 30 return true; 31 } 32 //删 33 public boolean remove(E element){ //删除元素为element的节点 34 for(Node<E> x = first; x != null; x = x.next){ //依次循环,将链表中所有满足的element全部删除,这里有判空操作是因为传进来的elemet值不确定,为了避免发生空指针异常,需要做判空处理 35 if((element != null && element.equals(x.item)) 36 || (element == null && x.item == element)){ 37 unlink(x); 38 } 39 } 40 return true; 41 } 42 public E remove(int index){ //删除下标index位置的元素 43 checkIndex(index); 44 return unlink(node(index)); 45 } 46 //改 47 public E set(int index, E element){ //将下标index位置的节点元素设置为element 48 checkIndex(index); 49 Node<E> node = node(index); 50 E oldValue = node.item; 51 node.item = element; //将原来的元素更改为新的元素,其它连接关系不变 52 return oldValue; 53 } 54 //查 55 public E get(int index){ //根据下标位置去获取节点元素 56 return node(index).item; 57 } 58 public int size(){ //获取链表长度大小 59 return this.size; 60 } 61 62 /** 63 * 断开链接,建立新的链接 64 * @param node 被删除的节点信息 65 * @return 返回被删除的节点元素 66 */ 67 private E unlink(Node<E> node){ 68 final Node<E> prev = node.prev; //保存node节点的上一个节点信息 69 final E item = node.item; //保存node节点的元素信息 70 final Node<E> next = node.next; //保存node节点的下一个节点信息 71 if(prev == null){ //如果node节点的上一个节点信息为空,则说明node节点为头节点,则需要将头节点指针first指向node节点的下一个节点 72 first = next; 73 } else { //node上一个节点存在 74 prev.next = next; //node的上一个节点的下一个节点指针指向node节点的下一个节点 75 node.prev = null; //断开node节点与上一个节点之间的连接关系 76 } 77 if(next == null) { //node的下一个节点为空,则说明node本身为尾节点,则需要更新last指向新的尾节点,即为node节点的上一个节点 78 last = prev; 79 } else { //node的下一个节点存在 80 next.prev = prev; //需要将node节点的下一个节点的上一个节点指向node节点的上一个节点 81 node.next = null; //断开node节点与下一个节点之间的连接关系 82 } 83 node.item = null; //经过上面两步操作之后,node节点的上下节点之间的关系已经断开,此时node变成了孤立的节点,再将该节点的元素置为null,方便gc进行回收 84 size --; 85 return item; 86 } 87 88 /** 89 * 在node节点前面插入新节点 90 * @param e 需要插入的元素 91 * @param node 被插入节点位置 92 */ 93 private void linkNode(E e, Node<E> node){ 94 final Node<E> pre = node.prev; //保存node节点的上一个节点信息 95 final Node<E> newNode = new Node(pre, e, node); //构建新的节点,同时该节点的上一个节点信息及下一个节点信息均已包含 96 node.prev = newNode; //将node节点上一个节点指向新建立的节点 97 if(pre == null) //node节点的上一个节点为空,则说明node原本为头节点,此时在node节点前插入一个新节点,则头指针需要变更 98 first = newNode; 99 else 100 pre.next = newNode; //将node上一个节点的下一个节点指向新建立的节点 101 size ++; 102 } 103 104 /** 105 * 找到index位置的节点,为了提升效率,这里先判断范围 106 * @param index 下标位置 107 * @return 108 */ 109 private Node<E> node(int index){ 110 //在前半段 111 if(index < (size >> 1)){ 112 Node<E> node = first; 113 for(int i = 0; i < index; i++) 114 node = node.next; 115 return node; 116 } 117 //在后半段 118 Node<E> node = last; 119 for(int i = size - 1; i > index; i--) 120 node = node.prev; 121 return node; 122 } 123 124 //检查下标index是否越界 125 private void checkIndex(int index){ 126 if(index < 0 || index > size) 127 throw new IndexOutOfBoundsException("index: " + index + ", size:" + size); 128 } 129 130 //节点定义 131 private static class Node<E>{ 132 E item; //数据元素 133 Node<E> next; //下一个节点 134 Node<E> prev; //上一个节点 135 136 Node(Node<E> prev, E item, Node<E> next){ 137 this.prev = prev; 138 this.item = item; 139 this.next = next; 140 } 141 } 142 }
链表具备增删改查功能,通过上面代码,我们对双向链表也有了一个清楚的了解。具体也不多说,每一行都有注释。