1、双向循环链表(DoubleCircleLinkedList)
双向循环链表与双向链表的唯一区别是:双向循环链表尾节点的next
指向头节点,构成了一个循环;双向循环链表头节点的prev
指向尾节点,构成了另一个循环。
注:
- 循环链表虽然首尾相连,但并不表示该链表没有头节点和尾结点。
- 循环链表的头节点是第一个添加的节点,并不是环中的任意节点都可能是头节点。
2、双向循环链表的设计
(1)类的结构设计
在编程之前,我们需要先对双向循环链表类(DoubleCircleLinkedList
)的结构进行设计。
双向循环链表类所需要的成员与双向链表类完全一样,主要包括:
(2)接口设计
双向循环链表的接口也与双向链表完全一样,主要包括:
- int size(); //元素的数量
- boolean isEmpty(); //是否为空
- boolean contains(E element); //是否包含某元素
- E get(int index); //获取index位置对应的元素
- E set(int index, E element); //设置index位置的元素(覆盖)
- void add(E element); //添加元素到尾部
- void add(int index, E element); //在index位置添加元素
- E remove(int index); //删除index位置对应的元素
- int indexOf(E element); //查看元素的位置
- void clear(); //清除所有元素
注:接口是供别人在外部通过对象调用的,所以要设计成公有(public
)成员。
3、双向循环链表的实现
3.1、编程实现
在双向链表项目的基础上:
①再新建一个名称为DoubleCircleLinkedList
(包名为:com.atangbiji
)的类,用来自己实现双向循环链表;
②仍然使用名称为Main
(包名为:com.atangbiji
)的类,来调用(测试)自己封装的双向循环链表。
③接口实现,我们这里直接考虑链表元素均为泛型的情况。
(1)定义私有成员变量和内部静态节点(Node)类
DoubleCircleLinkedList.java
文件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| package com.atangbiji;
public class DoubleCircleLinkedList<E> { private int size; private Node<E> first; private Node<E> last; private static class Node<E> { Node<E> prev; E element; Node<E> next; public Node(Node<E> prev, E element, Node<E> next) { this.prev = prev; this.element = element; this.next = next; } } }
|
(2)实现size()接口
DoubleCircleLinkedList.java
文件:
1 2 3 4 5 6 7 8
|
public int size() { return size; }
|
(3)实现isEmpty()接口
DoubleCircleLinkedList.java
文件:
1 2 3 4 5 6 7 8
|
public boolean isEmpty() { return size == 0; }
|
(4)实现get(int index)接口
为了方便获取任意节点,我们定义一个私有方法Node
用来获取索引为index
的节点。当index<size/2
时,从前向后找;当index>=size/2
时,从后向前找。
DoubleCircleLinkedList.java
文件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
|
private Node<E> node(int index) { if (index < 0 || index >= size) { throw new IndexOutOfBoundsException("索引出错,Index = " + index +"; Size =" + size); } 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; } }
|
获取链表索引为index
的元素,只需:
1 2 3
| ①首先从链表中获取索引为index的节点对象;
②再从该节点对象中获取其中的元素即可。
|
DoubleCircleLinkedList.java
文件:
1 2 3 4 5 6 7 8
|
public E get(int index) { return node(index).element; }
|
(5)实现set(int index, E element)接口
类似地,设置链表索引为index
的元素,只需:
1 2 3
| ①首先从链表中获取索引为index的节点对象;
②再设置该节点对象中的元素即可。
|
DoubleCircleLinkedList.java
文件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
|
public E set(int index, E element) { Node<E> node = node(index); E old = node.element; node.element = element; return old; }
|
(6)实现indexOf(E element)接口
只需遍历链表中的元素,判断链表中是否存在该元素即可。若存在,返回索引。若不存在,返回-1
。
双向循环链表的indexOf(E element)
接口与动态数组的indexOf(E element)
接口类似,这里不再赘述。
DoubleCircleLinkedList.java
文件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| private static final int ELEMENT_NOT_FOUND = -1;
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; }
|
(7)实现contains(E element)接口
在链表中查找该元素,若返回不为-1
,则说明该元素存在。
DoubleCircleLinkedList.java
文件:
1 2 3 4 5 6 7 8
|
public boolean contains(E element) { return indexOf(element) != ELEMENT_NOT_FOUND; }
|
(8)实现clear()接口
清空链表只需将所有节点的内存释放,同时将size
置为0
即可。
DoubleCircleLinkedList.java
文件:
1 2 3 4 5 6 7 8 9 10 11
|
public void clear() { first = null; last = null; size = 0; }
|
注:将第一个节点和最后一个结点的引用设为空(null
)后,虽然各节点还在被循环引用。但JVM
的内存处理机制是:没有被“GC Root
对象”引用的对象就会被当做垃圾清除。
GC Root
对象主要包括:被栈指针(局部变量)指向的对象等多种对象。当我们在调用接口时,我们所使用的DoubleCircleLinkedList
对象就是一种GC Root
对象。所以,Java
中,将第一个节点和最后一个结点的引用设为空(null
),即可实现所有节点的内存释放。
附:为了证明该方法的确释放了所有节点的内存,我们同样可以在内部静态节点(Node
)类中重写Java
中自带的finalize()
方法进行测试。测试过程与动态数组泛化后的clear()
接口测试方法类似,这里不再赘述。
(9)实现remove(int index)接口
与双向链表一样,删除双向循环链表中某一节点的元素只需:
1 2 3 4 5 6 7
| ①找到被删除节点;
②获取被删除节点的前一节点和后一节点;
③让前一节点的next直接指向后一节点,让后一节点的prev直接指向前一节点;
④链表存放元素的数量-1。
|
此时,被删除节点便没有被任何“GC Root
对象”引用,进而被当做垃圾清除。
注意分析头节点(index=0
)、尾节点(index=size-1
)和size=1
的情况。
DoubleCircleLinkedList.java
文件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47
|
public E remove(int index) { Node<E> removeNode = node(index); Node<E> prevNode = removeNode.prev; Node<E> nextNode = removeNode.next; if (1 == size) { first = null; last = null; } else if (0 == index) { nextNode.prev = last; last.next = nextNode; first = nextNode; }else if ((size - 1) == index) { prevNode.next = first; first.prev = prevNode; last = prevNode; }else { prevNode.next = nextNode; nextNode.prev = prevNode; } size--; return removeNode.element; }
|
(10)实现add(int index,E element)接口
链表添加新元素不用考虑是否需要扩容的问题。与双向链表一样,双向循环链表在index
处添加新元素只需:
1 2 3 4 5 6 7
| ①创建一个新节点;
②分别获取新节点的前一节点和后一节点;
③断开index结点与前一结点的连接,并将新节点分别与前一节点和后一节点相连;
④链表存放元素的数量+1。
|
由于双向循环链表只是尾节点的next
要指向首节点,头节点的prev
要指向尾节点。所以,一般情况下,单向循环链表的插入元素操作和单向链表的插入元素操作没有任何变化。只当在头部、尾部和size=0
时插入元素两者才有区别。
注意分析头节点(index=0
)、尾节点(index=size
)和size=0
的情况。
添加一般节点:
添加头节点(index=0):
添加尾节点(index=size):
当size=0时:
![添加Size=0 添加Size=0]()
DoubleCircleLinkedList.java
文件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74
|
public void add(int index,E element) { if (index < 0 || index > size) { throw new IndexOutOfBoundsException("索引出错,Index = " + index +"; Size =" + size); } if (size == 0) { Node<E> newNode = new Node<E>(null,element,null); first = newNode; last = newNode; newNode.next = newNode; newNode.prev = newNode; } else if (size == index) { Node<E> newNode = new Node<E>(null,element,null); Node<E> nextNode = first; Node<E> prevNode = last; newNode.next = nextNode; prevNode.next = newNode; newNode.prev = prevNode; nextNode.prev = newNode; last = newNode; } else if (0 == index) { Node<E> newNode = new Node<E>(null,element,null); Node<E> nextNode = first; Node<E> prevNode = last; newNode.next = nextNode; prevNode.next = newNode; first = newNode; newNode.prev = prevNode; nextNode.prev = newNode; } else { Node<E> newNode = new Node<E>(null,element,null); Node<E> nextNode = node(index); Node<E> prevNode = nextNode.prev; newNode.next = nextNode; prevNode.next = newNode; newNode.prev = prevNode; nextNode.prev = newNode; } size++; }
|
(11)实现add(E element)接口
add(E element)
接口是add(int index,E element)
接口的特例。我们只需在size
处添加新元素即可。
DoubleCircleLinkedList.java
文件:
1 2 3 4 5 6 7 8
|
public void add(E element) { add(size,element); }
|
(12)打印链表中的所有元素
同动态数组一样,为了打印对象链表中的所有元素,我们可以通过重写(也称覆盖)toString()
方法,并在其中完成字符串的拼接的方式即可实现。
为了更加直观地展现双向循环链表,我们在输出各节点元素时打印“上一节点元素←该节点元素→下一节点元素”。
注:Java
中字符串拼接建议使用StringBuilder
类。
DoubleCircleLinkedList.java
文件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
|
@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 (node.prev != null) { string.append(node.prev.element).append("<-"); } string.append(node.element); if (node.next != null) { string.append("->").append(node.next.element); } if (i != size - 1) { string.append(", "); } node = node.next; } string.append("]"); return string.toString(); }
|
3.2、接口测试
双向循环链表同样要注意边界测试,如:当index
为0
、size - 1
、size
时的情况。
在Main
类(Main.java
文件)中新建一个DoubleCircleLinkedList
对象,用来测试自己封装的单向循环链表。这里仅以添加和删除元素接口为例进行测试,其它接口的测试方法与此类似,这里不再赘述。
Main.java
文件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| package com.atangbiji;
public class Main { public static void main(String[] args) { DoubleCircleLinkedList<Integer> linkedList = new DoubleCircleLinkedList <Integer>(); linkedList.add(11); linkedList.add(22); linkedList.add(33); linkedList.add(44); linkedList.add(linkedList.size() - 1,null); linkedList.add(55); linkedList.add(linkedList.size(),66); linkedList.add(0,0); linkedList.add(0,null); System.out.println(linkedList); linkedList.remove(linkedList.size() - 1); System.out.println(linkedList); linkedList.remove(5); System.out.println(linkedList); int linkedListSize = linkedList.size(); for (int i = 0; i < linkedListSize; i++) { linkedList.remove(0); System.out.println(linkedList); }
} }
|
运行该程序,输出结果为:
![测试结果 测试结果]()
(本讲完,系列博文持续更新中…… )
参考文献:
[1] 《恋上数据结构与算法》,小码哥MJ
[2] 《数据结构与算法》,邓俊辉
如何获取源代码?
关注 “阿汤笔迹” 微信公众号,在后台回复关键词 “数据结构与算法设计与实现” ,即可获取项目源代码。
![阿汤笔迹微信公众平台 阿汤笔迹微信公众平台]()
原文地址:http://www.atangbiji.com/2023/07/23/DoubleCircleLinkedList/
博主最新文章在个人博客 http://www.atangbiji.com/ 发布。