前言
前文提到了单链表,双链表等,除了这两种链表之外,相信大家一定听过大名鼎鼎的循环链表(以下简称为环链表)。
环链表与单双链表其实是两个分类,我们常常所说的单链表和双链表可以称作单向非环链表和双向非环链表,其他两种显而易见。
事实上,我们常使用的环链表是双向环链表,比如,java中的LinkedList(网上很多博客这么说),正是由于此特性,LinkedList在java中还被封装为栈,队列与双端队列(也名双向队列)!提到栈和队列,大家都不陌生,后进先出(LIFO)和先进先出(FIFO)嘛,那双端队列是个什么鬼呢?它就是可以在两端插入和移除元素的数据结构,既能先进先出也能后进先出!
既然是这样,那我们之前封装的双向链表也能做到,为什么还要双向循环链表呢?
为此,我特地去看了LinkedList的源码,其实,jdk1.8中的LinkedList底层实现并不是双向循环链表,它使用的是双向链表!并且,与我们前文封装的双向链表相比,它大体的思想…差不多!
说了这么多,既然java都用双向链表了,咱们为什么还要学双向环链表呢,岂不是多此一举??
我个人认为,思维才是最重要的,形式有多种,但其本质都是相通的,我们要学的不是某一种数据结构,而是它所蕴含的深层次的思想以及该思想所适用的场景。–哲学家路西菲尔曾经说过:)
双向环链表
这里直接开始介绍双向环链表!其实阅读到了这里的小伙伴,我相信光从名字都能想象得出双向环链表的样子,以及其实现的思路,fighting!
所以,双向环链表应该是这样:
还是,这样?
还真是个环!当然了,两个都是。这是我的想象,你们想象的一定比我这个好看。
对于计算机来说,不存在环的。。。
这个环当然是逻辑上的,所以,我们需要一个头(head),这便是梦想开始的地方,那我们还需要尾(tail)吗?都梦想了还想什么end?
Node
双向环链表的Node与双向链表的Node是一样的,从图上也能看到,代码如下:
private class Node {
T value;
Node precursor;
Node successor;
Node(T value, Node precursor, Node successor) {
this.value = value;
this.precursor = precursor;
this.successor = successor;
}
Node(T value) {
this(value, null, null);
}
Node() {
this(null, null, null);
}
@Override
public String toString() {
return value.toString();
}
}
属性
双向环链表需要一个头结点,这个头结点用以记录链表开始,同样的头结点也是结束,所谓的尾结点,我们可以将头结点的前驱看作尾结点,但没必要列为属性,所以,常见的循环链表的实现,只含有head,其次,最常用的属性size。
同时,我们这里双向环链表不使用虚拟结点实现,从上图这个环可以看到,对于双向环链表来说,不使用虚拟结点同样很简单。你可能会问,什么时候使用虚拟结点呢?我的答案是,你觉得不好理解关于链表的头尾相关结点操作或者关于遍历不好理解,可以借助于虚拟结点!
private Node head;
private int size;
辅助函数
获取元素数量,判空,是否包含元素等是各个数据结构几乎都有的辅助函数:
获取元素数量:
//获取链表元素数量
public int size() {
return size;
}
判断链表是否为空:
//获取链表是否为空链表
public boolean isEmpty() {
return size == 0;
}
构造器
对于链表这样的动态结构来说,构造器较为简单,size赋予初始值0,head置为空,表示链表为空链表,因为不存在任何元素。
public TwoWayCirculateLinkedList() {
head = null;
size=0;
}
增删结点
增加第一个结点:
这里,我们可以看到head是首尾相连自循环的,这也体现了循环链表的特点,链表始终是一个环,空链表可以看作null的自我连接!
第一个元素的添加是特殊的,因为没有采用虚拟结点,故而代码如下:
if (size == 0) {
head = new Node(e);
head.precursor = head;
head.successor = head;
}
增加第二个结点:
第三个及更多:
向尾部添加:
向中间添加:
向头部添加:
我们可以看到,双向循环链表的关键在于循环的处理,正是由于其循环特性,其head所在的地方,同样可以当作其tail所在的地方,即首尾相连,这样的好处便是,我们从head可以顺序遍历,也可以逆序遍历,需要进行这样操作的数据集,我们可以将该数据集看作一个环。
由于我们没有借助虚拟头尾结点,所以对head的操作同样是特殊的,因为这会涉及到head的迁移操作,所以对在头部添加的操作,单独使用代码表示为:
//在头部进行插入时,对head向后迁移
if (index == 0) {
Node prev = head.precursor;
Node cur = new Node(e);
cur.successor = head;
cur.precursor = prev;
prev.successor = cur;
head.precursor = cur;
head = cur;
}
这一步相较复杂,可以这样理解,将此时的head看作普通 结点,在head与其前驱之间添加了一个结点,最后将这个结点设置为head。
**综上,可以得到添加操作为四种情况:
第一种,当链表为空链表时,head指向新添加结点,且自循环;
第二种,当链表在头部添加结点时,head向后迁移;
第三种,当链表在index小于等于size的二分之一时,通过head向后继结点的方向遍历寻址,找到新增结点的前驱,并进行添加结点操作;
第四种,当链表在index大于size的二分之一时,通过head向前驱结点的方向遍历寻址,找到新增结点的前驱,并进行添加结点操作;
**
为什么要这么复杂呢?
其一是因为,当欲添加结点在链表的前半部分,通过起点向后寻址更快,同理,在前半部分时,通过起点向前寻址更快,这充分利用了环链表这个”环“的特性;
其二是因为,我个人觉得写博客,写得更详细些便于理解,没有坏处,其实,我所写的数据结构的很多说明可以略写,很多代码也可以更加简单,但我始终认为,多写一些比少写一些更好,包括代码注释,因为时间久了,你可能都读不懂自己的代码了,更甚至,当时的任何思路都没有了。
添加操作的代码如下:
public void add(int index, T e) {
if (index < 0 || index > size) {
throw new IllegalArgumentException("Add failed.Illegal index.");
}
if (size == 0) {
head = new Node(e);
head.precursor = head;
head.successor = head;
} else {
//在头部进行插入时,对head向后迁移
if (index == 0) {
Node prev = head.precursor;
Node cur = new Node(e);
cur.successor = head;
cur.precursor = prev;
prev.successor = cur;
head.precursor = cur;
head = cur;
} else {
Node prev = head;
if (index <= size / 2) {
//index小于等于size的一半,通过头部向后遍历寻址
//注意边界 此时寻找的是欲添加结点的前驱
for (int i = 0; i < index - 1; i++) {
prev = prev.successor;
}
} else {
//否则,通过头部向前遍历寻址
for (int i = size; i >= index; i--) {
prev = prev.precursor;
}
}
Node newNode = new Node(e);
Node succ = prev.successor;
newNode.successor = succ;
newNode.precursor = prev;
prev.successor = newNode;
succ.precursor = newNode;
}
}
size++;
}}
对于删除结点,其思路是与增加结点一致的,并且它与增加结点的操作应当是完全相反的,同样的,我们将之区分为以下几种情况:
删除最后一个结点:
与增加结点类似,删除最后一个结点(head)是特殊的,其操作为head置空:
if (size == 1) {
head = null;
}
删除倒数第二个结点:
从头部删除:
与增加相同,对head的删除操作同样是特殊的,因为这涉及到head的迁移,即原head的后继变成新的head,其代码如下:
//对头部进行删除时,将head替换为其后继元素
if (index == 0) {
//获取原head前驱和后继
Node prev = head.precursor;
Node succ = head.successor;
prev.successor = succ;
succ.precursor = prev;
head = succ;
//显式地将原head引用置空
cur.precursor = null;
cur.successor = null;
}
此处,也可以将head替换为其前驱元素,利用其”环“的特性。
从中间删除:
从尾部删除:
总上,删除结点代码如下:
public T remove(int index) {
if (size == 0) {
throw new IllegalArgumentException("Remove failed.LinkedList is null.");
}
//删除操作,index最大为size-1,此时index为size的结点尚不存在
if (index < 0 || index >= size) {
throw new IllegalArgumentException("Remove failed.Illegal index.");
}
Node cur = head;
if (size == 1) {
head = null;
} else {
//对头部进行删除时,将head替换为其后继元素
if (index == 0) {
//获取原head前驱和后继
Node prev = head.precursor;
Node succ = head.successor;
prev.successor = succ;
succ.precursor = prev;
head = succ;
//显式地将原head引用置空
cur.precursor = null;
cur.successor = null;
} else {
if (index <= size / 2) {
//index小于等于size的一半,通过头部向后遍历寻址
//注意边界取值,此时寻找的是index对应值,而非前驱,所以与前文添加结点不同
for (int i = 0; i < index ; i++) {
cur = cur.successor;
}
} else {
//否则,通过头部向前遍历寻址
for (int i = size; i > index; i--) {
cur = cur.precursor;
}
}
Node prev = cur.precursor;
Node succ = cur.successor;
prev.successor = succ;
succ.precursor = prev;
cur.precursor = null;
cur.successor = null;
}
}
size--;
return cur.value;
}
改查元素
到了这一步,就相对简单了,其实在增删的操作里是蕴含着改查的:
用一张图表示:
获得index索引的元素代码为:
public T get(int index) {
if (isEmpty()) {
throw new IllegalArgumentException("Get failed.Linklist is empty.");
}
if (index < 0 || index >= size) {
throw new IllegalArgumentException("Get failed.Illegal index.");
}
Node cur = head;
if (index <= size / 2) {
for (int i = 0; i < index; i++) {
cur = cur.successor;
}
} else {
for (int i = size; i > index; i--) {
cur = cur.precursor;
}
}
return cur.value;
}
修改元素,只要在获得元素的基础上,进行改动,这里,需要注意的仍然是遍历边界取值:
public void set(int index, T e) {
if (isEmpty()) {
throw new IllegalArgumentException("Set failed.Linklist is empty.");
}
if (index < 0 || index >= size) {
throw new IllegalArgumentException("Set failed.Illegal index.");
}
Node cur = head;
if (index <= size / 2) {
for (int i = 0; i < index; i++) {
cur = cur.successor;
}
} else {
for (int i = size; i > index; i--) {
cur = cur.precursor;
}
}
cur.value = e;
}
头尾操作
对于双向环链表而言,头尾操作的时间复杂度为O(1),所以我们提供对头尾结点的操作函数:
对头部的操作:
public void addFirst(T e) {
add(0, e);
}
public T removeFirst() {
return remove(0);
}
public void setFirst(T e){
set(0,e);
}
public T getFirst(){
return get(0);
}
对尾部的操作:
public void addLast(T e) {
add(size, e);
}
public T removeLast() {
return remove(size - 1);
}
public void setLast(T e){
set(size-1,e);
}
public T getLast(){
return get(size-1);
}
完整代码
以下代码已经通过我的测试–来自帅气的路西菲尔:)
package com.lucifer.linkedlist;
/**
* @program: thinking-in-all
* @description:
* @author: Lucifini
* @create: 2020-01-05
**/
public class TwoWayCirculateLinkedList<T> {
private class Node {
T value;
Node precursor;
Node successor;
Node(T value, Node precursor, Node successor) {
this.value = value;
this.precursor = precursor;
this.successor = successor;
}
Node(T value) {
this(value, null, null);
}
Node() {
this(null, null, null);
}
@Override
public String toString() {
return value.toString();
}
}
private Node head;
private int size;
//获取链表元素数量
public int size() {
return size;
}
//获取链表是否为空链表
public boolean isEmpty() {
return size == 0;
}
public TwoWayCirculateLinkedList() {
head = null;
size = 0;
}
public void add(int index, T e) {
if (index < 0 || index > size) {
throw new IllegalArgumentException("Add failed.Illegal index.");
}
if (size == 0) {
head = new Node(e);
head.precursor = head;
head.successor = head;
} else {
//在头部进行插入时,对head向后迁移
if (index == 0) {
Node prev = head.precursor;
Node cur = new Node(e);
cur.successor = head;
cur.precursor = prev;
prev.successor = cur;
head.precursor = cur;
head = cur;
} else {
Node prev = head;
if (index <= size / 2) {
//index小于等于size的一半,通过头部向后遍历寻址
//注意边界 此时寻找的是欲添加结点的前驱
for (int i = 0; i < index - 1; i++) {
prev = prev.successor;
}
} else {
//否则,通过头部向前遍历寻址
for (int i = size; i >= index; i--) {
prev = prev.precursor;
}
}
Node newNode = new Node(e);
Node succ = prev.successor;
newNode.successor = succ;
newNode.precursor = prev;
prev.successor = newNode;
succ.precursor = newNode;
}
}
size++;
}
public T remove(int index) {
if (size == 0) {
throw new IllegalArgumentException("Remove failed.LinkedList is null.");
}
//删除操作,index最大为size-1,此时index为size的结点尚不存在
if (index < 0 || index >= size) {
throw new IllegalArgumentException("Remove failed.Illegal index.");
}
Node cur = head;
if (size == 1) {
head = null;
} else {
//对头部进行删除时,将head替换为其后继元素
if (index == 0) {
//获取原head前驱和后继
Node prev = head.precursor;
Node succ = head.successor;
prev.successor = succ;
succ.precursor = prev;
head = succ;
//显式地将原head引用置空
cur.precursor = null;
cur.successor = null;
} else {
if (index <= size / 2) {
//index小于等于size的一半,通过头部向后遍历寻址
//注意边界取值,此时寻找的是index对应值,而非前驱,所以与前文添加结点不同
for (int i = 0; i < index; i++) {
cur = cur.successor;
}
} else {
//否则,通过头部向前遍历寻址
for (int i = size; i > index; i--) {
cur = cur.precursor;
}
}
Node prev = cur.precursor;
Node succ = cur.successor;
prev.successor = succ;
succ.precursor = prev;
cur.precursor = null;
cur.successor = null;
}
}
size--;
return cur.value;
}
public void set(int index, T e) {
if (isEmpty()) {
throw new IllegalArgumentException("Set failed.Linklist is empty.");
}
if (index < 0 || index >= size) {
throw new IllegalArgumentException("Set failed.Illegal index.");
}
Node cur = head;
if (index <= size / 2) {
for (int i = 0; i < index; i++) {
cur = cur.successor;
}
} else {
for (int i = size; i > index; i--) {
cur = cur.precursor;
}
}
cur.value = e;
}
public T get(int index) {
if (isEmpty()) {
throw new IllegalArgumentException("Get failed.Linklist is empty.");
}
if (index < 0 || index >= size) {
throw new IllegalArgumentException("Get failed.Illegal index.");
}
Node cur = head;
if (index <= size / 2) {
for (int i = 0; i < index; i++) {
cur = cur.successor;
}
} else {
for (int i = size; i > index; i--) {
cur = cur.precursor;
}
}
return cur.value;
}
public void addFirst(T e) {
add(0, e);
}
public T removeFirst() {
return remove(0);
}
public void setFirst(T e) {
set(0, e);
}
public T getFirst() {
return get(0);
}
public void addLast(T e) {
add(size, e);
}
public T removeLast() {
return remove(size - 1);
}
public void setLast(T e) {
set(size - 1, e);
}
public T getLast() {
return get(size - 1);
}
@Override
public String toString() {
StringBuilder res = new StringBuilder();
res.append("head [<-> ");
Node cur = head;
for (int i = 0; i < size; i++) {
res.append(cur.toString()).append(" <-> ");
cur = cur.successor;
}
res.append(" ]");
return res.toString();
}
}
总结
以上便是双向环链表的一种实现了,这个环链表还是有很多考虑不周的情况,代码也有些赘余,看到这里的小伙伴可以试着优化一下。
前文我们实现的链表都是通过虚拟结点来辅助实现,当我们这里通过不使用虚拟结点来实现,明显感觉到更加复杂和难以理解,所以说,数据结构与算法中,一些小小的技巧,会牵动很多的东西,小伙伴们可以试着将带有虚拟结点的版本实现出来。
另外,前文实现的双向链表有着头尾两个结点,其实双向环链表也可以使用两个结点来实现,或许,这便是数据结构与算法的魅力吧!不拘泥于形式,一切随心所欲!我们所需要理解的仍然是其本质的东西,关于链表,它所核心的东西便是动态的增删结点,通过前驱或者后继结点来寻找目标结点,对于其头部或者尾部来说,其增删查改的时间复杂度都是O(1),所以,链表也是天然的栈队列等结构,其次,链表有着天然的递归性质,其实像链表,树等动态结构,大多都有着递归性质,使用递归,可以帮助我们理解这些数据结构,简化代码,最好的是,使用各种方法去实现一遍。
最后,未来我会分享更多的知识,如有不正,敬请指出,我是路西菲尔,期待与你一同成长!
转载请说明出处,原文来自路西菲尔的博客https://blog.youkuaiyun.com/csdn_1364491554/article/details/103818397