上一期整体探讨了一下单向链表。在这基础上补充两个点,分别是单向循环链表和双向循环链表。从字面中可以看出是将链表形成个环结构,区别在于这个环是只能一个方向还是两个方向循环。
单向循环链表
单向循环链表可以理解为将单向链表的最后一个节点指向第一个节点,将链表形成一个环。那么由单向循环链表处理的数组数据接口比着单向链表来说,在添加元素和删除元素这两个情况下做不同的处理,保证还是一个环的状态。
添加元素(add(int index, E element)
)
添加元素时需要特别处理 index 为 0 的情况,当在 index 为 0 的位置插入元素时,逻辑上的处理是将新元素的 next 指向原来的 first 位置元素,最后一个元素的 next 指向这个新的 first 位置元素。
在代码处理上要特别注意,若是直接处理 first 节点指向,链表整体数量会增加,那么在拿最后一个元素的位置的时候就会容易出错。这里的处理是先创建一个 newFirst 节点,保持原来完整的链表。
@Override
public void add(int index, E element) {
// 需要先做判断
rangeCheckOfAdd(index);
// 当插入到 index == 0 的位置,需要把最后一个 node 的 next 指向 first(index == 0)
if (index == 0) {
// 插入第一个,并不是说 first 就是 null。也是指向有值的
// 创建 newFirst 的原因是:直接用 first 指向,会导致获取最后一个 node 不是 size -1 的位置
Node<E> newFirst = new Node<>(element, first);
// 拿到最后一个节点
Node<E> last = size == 0 ? newFirst: node(size -1);
last.next = newFirst;
first = newFirst;
}
else {
Node<E> prev = node(index-1);
Node<E> node = new Node<>(element, prev.next);
prev.next = node;
}
size ++;
}
删除元素(E remove(int index)
)
删除元素时,也是要特别留意删除 index == 0 情况。在 index == 0 情况下,如果 size == 1,则是删除最后一个元素,那么需要 first 直接等于 null,否则这个环不会被销毁;如果 size != 1 的情况下,需要在处理之后也要更改一下最后一个节点的指向,将其指向新的 first 指针。
@Override
public E remove(int index) {
// 需要先判断
rangeCheck(index);
Node<E> old = first;
if (index == 0) {
// 只是删除对应的坐标,不是删除所有
// size==1,进行指向的时候,无法被删除完成
if (size == 1) {
first = null;
}
else {
Node<E> last = node(size -1);
first = first.next;
last.next = first;
}
}
else {
Node<E> prev = node(index -1);
old = prev.next;
prev.next = prev.next.next;
}
size --;
return old.element;
}
双向循环链表
双向循环链表也是将链表形成一个环,环中的每个节点都有指向前一个元素的指针和指向后一个元素的指针。链表的数据结构中也会多一个last节点来指向最后一个元素。
区别于其他类型链表的地方依然是添加元素和删除元素这两个。在逻辑处理上会有不同,因为多了指向,在代码处理上会多了一些便捷,但是逻辑上多了一些需要考虑的点。除此之外,双向循环链表在遍历元素的处理中可以用二分法来处理。
@SuppressWarnings("unused")
public class CircleLinkList<E> extends AbstractList<E> {
Node<E> first;
// 链表结构增加 last 指针
Node<E> last;
private static class Node<E> {
// 节点结构增加指向前一个节点指针
Node<E> prev;
Node<E> next;
E element;
public Node(Node<E> prev, E element, Node<E> next) {
this.prev = prev;
this.element = element;
this.next = next;
}
}
添加元素(add(int index, E element)
)
双向链表中添加元素,主要考虑 index == size情况,这种情况下,可以通过 last 是否为 null 判断链表中是否有元素。添加后的节点要更改它的前一个节点指向和后一个节点指向。当只有一个元素是,它的 first 和 last 是同一个。
这里有个新奇的点,判断 index == 0 的情况,是通过 prev == last 来判断。因为 first 节点的前一个节点就是 last 节点。
@Override
public void add(int index, E element) {
rangeCheckOfAdd(index);
if (index == size) { // 在最后面添加
Node<E> oldLast = last;
last = new Node<>(oldLast, element, first);
if (oldLast == null) { // 数组中还没有元素
first = last;
}
else {
oldLast.next = last;
}
last.next = first;
first.prev = last;
}
else {
Node<E> next = node(index);
Node<E> prev = next.prev;
Node<E> node = new Node<>(prev, element, next);
next.prev = node;
prev.next = node;
if (prev == last) { // index == 0
first = node;
}
}
size++;
}
删除元素(E remove(int index)
)
删除元素的时候,如果 size == 1 是,那么就需要将 first 节点和 last 节点都指向 null。
其他情况中,如果 index == 0 则需要更改 first 节点的指向,如果 index == size-1 则需要更改 last 节点的指向。
@Override
public E remove(int index) {
Node<E> node = node(index);
if (size == 1) {
first = null;
last = null;
}
else {
Node<E> prev = node.prev;
Node<E> next = node.next;
prev.next = next;
next.prev = prev;
if (node == first ) { // index == 0
first = next;
}
if (node == last) { // index == size -1
last = prev;
}
}
size--;
return node.element;
}
遍历元素(Node<E> node(int index)
)
正因为可以在这个环中的两个不同方向遍历,那么就可以先判断 index 与中间位置,来确定从头还是从尾遍历。进而减少遍历的次数。这是一个很好的处理。
private Node<E> node(int index) {
rangeCheck(index);
if (index < size/2) {
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;
}
}