单向循环链表

本文详细介绍了单向循环链表的概念,包括其存储结构和如何通过头插法、尾插法进行插入操作,以及如何进行删除操作。讨论了链表为空、只有一个节点时的特殊操作,并提供了get、set、find、contains、clear等函数的实现思路。最后,提到了toString和equals方法。

既然有线性表的链式存储结构,那么我们可以想到一定也有单向循环链表。
下面让我们来看看单向循环链表。
在这里插入图片描述
单向循环链表的本质就是让单链表的尾节点指针指向头结点,这样就可以让整个单链表形成一个环。
这种头尾相连的单链表就叫做单向循环链表。

为了方便操作,我们让头指针不再指向虚拟头结点,而是直接指向真实头结点,也就是链表中的第一个节点。

此时单向循环链表因为存储结构的改变所以不能再用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函数进行测试即可。
单向循环链表的知识就到这里,通过不断的学习,我会进行补充。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值