既然有线性表的链式存储结构,那么我们可以想到一定也有单向循环链表。
下面让我们来看看单向循环链表。

单向循环链表的本质就是让单链表的尾节点指针指向头结点,这样就可以让整个单链表形成一个环。
这种头尾相连的单链表就叫做单向循环链表。
为了方便操作,我们让头指针不再指向虚拟头结点,而是直接指向真实头结点,也就是链表中的第一个节点。
此时单向循环链表因为存储结构的改变所以不能再用LinkedList 链表来实现了。但是他们都是单向的所以都要有节点的信息,都要私有化一个节点的内部类。
实际上单向循环链表本质上是特殊的线性表,所以我们要实现之前写的list接口。
1.下面我们来定义LoopSingle类
代码如下:
public class LoopSingle<E> implements List<E> {
private Node head; //定义一个头指针
private Node rear; //定义一个尾指针
private int size; //定义循环链表节点个数
public LoopSingle() { //创建单向循环链表的无参构造函数
head=null; //创建时为空,所以头指针和尾指针都指向空,size=0
rear=null;
size=0;
}
public LoopSingle(E[] arr){ //创建单向循环链表的有参构造函数
//传入一个数组,封装成循环链表
}
private class Node{ //私有化节点这个内部类
E data; //数据域
Node next; //指针域
public Node(){ //创建节点的无参构造函数
this(null,null);
}
public Node(E data,Node next){ //创建节点的有参构造函数
this.data=data;
this.next=next;
}
}
一.插入
1.链表为空时
链表为空时head指针和rear指针都指向空,因为表中无带有元素的节点。

反之我们也可以看出判断单向循环链表为空的条件:head指向空,rear指向空,size=0
2.链表为空时插入一个节点

此时链表为空,head=null;rear=null;size=0。
我们要插入包含“A”元素的新节点
1.让head指向“A”元素所在的节点
2.让“A”元素所在节点的后继指针指向“A”元素所在节点(自身的数据域)
3.让rear指向“A”元素所在节点
这样就完成了链表为空时插入一个数据节点的操作,效果如下图。

此时就完成了size为1的一个单向循环链表
二.链表不为空时尾插

此时我们要把“B”元素所在节点用尾插法插入到链表中
此时的尾节点是“A”元素所在节点
1.把“A”元素所在节点的后继指针赋给“B”元素所在节点
2.让“A”元素所在节点指向“B”元素所在节点
3.rear后移,指向新的尾节点(“B”元素所在节点)
效果如下图

三.链表不为空时头插法

此时链表中有两个节点,我们要用头插法插入“C”元素所在节点。
注意:我们在单向循环链表中采用的是真实头结点。所以此时头插法就是在“A”元素所在节点的前面插入“C”元素所在节点。
1.让“B”元素所在节点指向“C”元素所在节点
2.让“C”元素所在节点指向“A”元素所在节点
3.head指向“C”元素所在节点
效果如下图

插入情况就是上面这几种
2.add()类 插入代码如下:
@Override
public void add(int index, E e) {
if(index<0||index>size){
throw new IllegalArgumentException("插入角标非法!");
}
Node n=new Node(e,null);
if(isEmpty()){ //特殊情况
head=n;
rear=n;
rear.next=head;
}else if(index==0){ //头插
n.next=head;
head=n;
rear.next=head;
}else if(index==size){//尾插
n.next=head;
rear.next=n;
rear=n;
}else{ //一般情况,通过遍历链表找到要插入位置的前一个节点位置
Node p=head;
for(int i=0;i<index-1;i++){
p=p.next;
}
n.next=p.next;
p.next=n;
}
size++;
}
@Override
public void addFirst(E e) { //头插
add(0,e);
}
@Override
public void addLast(E e) { //尾插
add(size,e);
}
四.删除
1.链表不为空时删头
在上图的基础上我们来进行删头。
它的操作顺序刚好和我们刚才的链表不为空时头插法是相反的。
此时链表中有三个节点。
1.让head指向此时头结点的下一个节点(删除节点的后继节点)
2.把“C”元素所在节点的后继指针赋给“B”元素所在节点,相当于“B”元素所在节点指向了“A”元素所在节点。
3.让“C”元素所在节点指向空(null)
这样我们在链表不为空时头删就完成了。
效果如下图:

2.链表不为空时删尾

此时链表中有两个节点,我们要删尾,也就是删除“B”元素所在节点。
因为是单向链表,所以我们删除的时候要从头开始遍历找到删除节点的前一个节点。
1.把“B”元素所在节点的后继指针赋给“A”元素所在的节点
2.rear指针前移指向“A”元素所在节点
3.“B”元素所在节点指向空(要删除的节点指向null)
效果如下图:

3.链表只有一个节点时删除

此时我们可以看到,链表中只有一个节点。
现在我们要删除它。
其实很简单,只需要让hear指向空,rear指向空,size=0就可以了。
再细致一点可以让“A”元素所在节点指向空
效果如下图:

删除和插入的几种情况介绍完了,我们来总结一下。
链表为空时,头插和尾插是一样的操作。尾插相当于特殊的头插。
链表只有一个节点时,删除就直接让head指针和rear指针都指向空、size=0就可以了。
上面两种是特殊情况其他都是正常的插入和删除。
3.remove()删除函数代码如下:
@Override
public E remove(int index) {
if(index<0||index>=size){ //先判断删除下标是否非法
throw new IllegalArgumentException("删除角标非法!");
}
E res=null; //用res来存放删除元素
if(size==1){ //特殊情况 //只有一个节点时删除
res=head.data;
head=null;
rear=null;
}else if(index==0){ //头删
res=head.data;
head=head.next;
rear.next=head;
}else if(index==size-1){ //尾删
res=rear.data;
Node p=head;
while(p.next!=rear){
p=p.next;
}
p.next=rear.next;
rear=p;
}else{ //其他情况删除,需要通过遍历链表找到删除节点的前一个节点
Node p=head;
for(int i=0;i<index-1;i++){
p=p.next;
}
Node del=p.next;
res=del.data;
p.next=del.next;
}
size--;
return res;
}
@Override
public E removeFirst() { //头删
return remove(0);
}
@Override
public E removeLast() { //尾删
return remove(size-1);
}
@Override
public void removeElement(E e) { //指定位置删除
remove(find(e));
}
4.get()函数 获取传入下标的节点的数据
在获取传入下标的节点的数据时,要先判断查找的下标是否非法。
然后分为三种情况,分别是获取头结点的数据,获取尾节点的数据,获取中间节点的数据。
@Override
public E get(int index) {
if(index<0||index>=size){
throw new IllegalArgumentException("查找角标非法!");
}
if(index==0){ //获取头节点的数据
return head.data;
}else if(index==size-1){ //获取尾节点的数据
return rear.data;
}else{ //其他情况,遍历链表找到要获取数据节点的前一个节点
Node p=head;
for(int i=0;i<index;i++){
p=p.next;
}
return p.data;
}
}
@Override
public E getFirst() { //获取头结点的数据
return get(0);
}
@Override
public E getLast() { //获取尾节点的数据
return get(size-1);
}
5.set()函数 指定数据和下标修改链表中的节点
这里还是先判断修改下标是否非法
然后有三种情况,特殊的头节点修改和尾节点修改,还有中间节点的修改。
@Override
public void set(int index, E e) {
if(index<0||index>=size){ //判断修改的下标是否非法
throw new IllegalArgumentException("修改角标非法!");
}
if(index==0){ //修改头结点
head.data=e;
}else if(index==size-1){ //修改尾节点
rear.data=e;
}else{ //修改中间节点,从头节点开始遍历,一直到要修改节点的前一个节点结束
Node p=head;
for(int i=0;i<index;i++){
p=p.next;
}
p.data=e;
}
}
6.find()函数 传入数据查找数据在链表中节点的下标
要查找先要判断链表是否为空,是空返回-1。
不为空继续查找,这里因为不知道循环的次数所以不能用for循环。
但是我们知道循环的终止条件,当我们查找时,从头结点开始遍历,找到了就返回所找的数据所在节点的下标,没找到就继续遍历,一直遍历到尾节点结束,终止循环。
@Override
public int find(E e) {
if(isEmpty()){ //查找先判断链表是否为空,为空返回-1
return -1;
}
Node p=head;
int index=0;
while(p.data!=e){ //循环终止条件为找到数据节点的下标结束
p=p.next;
index++;
if(p==head){ //当遍历完尾节点时没找到,说明链表中不存在要找的数据,返回-1
return -1;
}
}
return index;
}
7.contains() 判断是否包含
因为前面已经写好了find()函数,所以这里直接调用find()函数就好。
找到了返回数据所在节点的下标
没找到则返回-1
@Override
public boolean contains(E e) {
return find(e)!=-1;
}
8.clear() 清空链表
清空链表只需要让链表的头指针和尾指针都指向空
然后size=0即可
@Override
public void clear() {
head=null;
rear=null;
size=0;
}
9.toString() 返回链表中每个数据和有效数据元素的个数
@Override
public String toString() {
StringBuilder sb=new StringBuilder();
sb.append("LoopSingle:size="+getSize()+"\n");
if(isEmpty()){ //先判断链表是否为空
sb.append("[]"); //空链表返回【】
}else{
sb.append('['); //不是空链表先加上【
Node p=head;
while(true){ //循环到最后一个元素,然后末尾加上】
sb.append(p.data); //拼接每个数据
if(p.next==head){
sb.append(']');
break;
}else{
sb.append(',');
}
p=p.next;
}
}
return sb.toString();
}
10.equals() 比较传入链表
@Override
public boolean equals(Object obj) {
if(obj != null) { //先判断传入的数据是否为空,为空直接返回false
return false;
}
if(obj == this) { //再判断传入数据是否等于现有链表中的数据,等于返回true
return true;
}
if(obj instanceof LinkedList) {//判断传入数据类型是否属于链表,属于再进行判断,不属于返回false
LinkedList l = (LinkedList)obj; //给传入的数据类型创建名为“l”对象
if(getSize() == l.getSize()) { //先判断数据个数是否相等
for (int i = 0; i < getSize(); i++) {
if(get(i) != l.get(i)) { //再通过遍历判断每个数据是否相等
return false;
}
}
return true;
}
}
return false;
}
接下里再写一个main函数进行测试即可。
单向循环链表的知识就到这里,通过不断的学习,我会进行补充。
本文详细介绍了单向循环链表的概念,包括其存储结构和如何通过头插法、尾插法进行插入操作,以及如何进行删除操作。讨论了链表为空、只有一个节点时的特殊操作,并提供了get、set、find、contains、clear等函数的实现思路。最后,提到了toString和equals方法。
2660

被折叠的 条评论
为什么被折叠?



