单向循环链表
什么是循环链表?
我们之前定义了链表,所谓的循环链表就是在原先链表的基础上加以改动,让他进行循环执行,这样就成了循环链表,之前链表的尾指针(rear)的下一个指向的是null,而循环链表最后的尾指针的下一个指向头。
- 注意
我们之前定义的单链表的头节点,是不存储任何元素的,现在我们定义的新的循环链表的头节点,是存储元素的。
循环链表的结构
循环链表的定义实现List 接口的方法
/**
* 获取线性表中元素的个数(线性表的长度)
* @return 线性表中有效元素的个数
* */
public int getSize();
/**
* 判断线性表是否为空
* @return 是否为空的布尔类型值
* */
public boolean isEmpty();
/**
* 在线性表中指定的index角标处添加元素e
* @param index 指定的角标 0<=index<=size
* @param e 要插入的元素
* */
public void add(int index,E e);
/**
* 在线性表的表头位置插入一个元素
* @param e 要插入的元素 指定在角标0处
* */
public void addFirst(E e);
/**
* 在线性表的表尾位置插入一个元素
* @param e 要插入的元素 指定在角标size处
* */
public void addLast(E e);
/**
* 在线性表中获取指定index角标处的元素
* @param index 指定的角标 0<=index<size
* @return 该角标所对应的元素
* */
public E get(int index);
/**
* 获取线性表中表头的元素
* @return 表头元素 index=0
* */
public E getFirst();
/**
* 获取线性表中表尾的元素
* @return 表尾的元素 index=size-1
* */
public E getLast();
/**
* 修改线性表中指定index处的元素为新元素e
* @param index 指定的角标
* @param e 新元素
* */
public void set(int index,E e);
/**
* 判断线性表中是否包含指定元素e 默认从前往后找
* @param e 要判断是否存在的元素
* @return 元素的存在性布尔类型值
* */
public boolean contains(E e);
/**
* 在线性表中获取指定元素e的角标 默认从前往后找
* @param e 要查询的数据
* @return 数据在线性表中的角标
* */
public int find(E e);
/**
* 在线性表中删除指定角标处的元素 并返回
* @param index 指定的角标 0<=index<size
* @return 删除掉的老元素
* */
public E remove(int index);
/**
* 删除线性表中的表头元素
* @return 表头元素
* */
public E removeFirst();
/**
* 删除线性表中的表尾元素
* @return 表尾元素
* */
public E removeLast();
/**
* 在线性表中删除指定元素e
* */
public void removeElement(E e);
/**
* 清空线性表
* */
public void clear();
** 定义的方法**
public class LoopSingle<E> implements List<E>
实现的思想:
我们在实现这个循环链表时,我们已经知道了,链表是由一个个的结点来构成的,而结点里面又存了,元素和指向下一个结点的地址。所以我们需要定义一个结点的内部类,用来实现这个循环链表。
private class Node{
E data; //数据域
Node next; //指针域
public Node(){
this(null,null);
}
public Node(E data,Node next){
this.data=data;
this.next=next;
}
@Override
public String toString() {
return data.toString();
}
}
** 实现类的思想**
我们定义了一个内部类之后,之前的链表的实现,我们是定义了一个虚拟的头节点,所以当链表为空时,头指针和尾指针都是指向头节点的,现在我们定一个了一个真实的头节点,用来存储数据;所以此时当链表为空时,头指针和尾指针都指向的应该是空;所以我们的定义是:
private Node head; //定义头指针
private Node rear; //定义尾指针
private int size; //结点的个数
public LoopSingle() {
head=null;
rear=null;
size=0;
}
** 实现方法**
public int getSize() {
return size;
}
思想: 返回当前结点的个数,就直接返回当前size 的数。
//判断当前链表是否为空
public boolean isEmpty() {
return size==0&&head==null&&rear==null;
}
思想:当我们的链表为空的时候 size 是为零的,头指针和尾指针都是指向空的。
//往指定角标处添加元素e
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++;
}
思想: 添加元素时,我们有几种的特殊的情况,当我们的当前的链表为空的时候,我们插入第一个结点时,此时我们的头节点和尾结点都指向我们插入的第一个结点。此时,head 指向头节点,rear 指向头节点,而rear 的下一个指向的还是当前的结点。还有我们的链表不是为空的时候,我们就要考虑是头插,还是尾插,还是任意插,当我们头插的时候,直接把要插入结点的一个指向当前的头节点,然后把head (头指针)更新到要插入的结点上,然后将尾指针(rear)的下一个指向更新后的头。插尾的时候,先把尾的下一个给插入的结点,把rear (尾指针)更新到当前的结点,把新的尾指针的下一个指向head (头指针),而任意插 的时候,我们需要找到要插入角标结点的前一个结点,我们可以根据告知我们的角标,找到它的前驱,然后进行插入。
插入第一个
头插
尾插
//获取指定角标处结点的元素
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;
}
}
思想:这个也是一样的,获取指定处的角标的时候,判读要获取的元素的位置,头 ,尾,或者是任意的位置时,找到当前的结点,然后返回当前的结点的元素。
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;
}
}
思想:修改指定角标处结点所存的元素为e;
public int find(E e) {
if(isEmpty()){
return -1;
}
Node p=head;
int index=0;
while(p.data!=e){
p=p.next;
index++;
if(p==head){
return -1;
}
}
return index;
}
思想:查找当前的链表中是否存在元素e;这和我们之前找结点的方法不同,我们之前是直接利用角标来进行判断的,现在我们没有了角标,只能利用while循环来进行查找。
public E remove(int index) {
if(index<0||index>=size){
throw new IllegalArgumentException("删除角标非法!");
}
E res=null;
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));
}
@Override
public void clear() {
head=null;
rear=null;
size=0;
}
思想:删除指定角标的结点,我们在这也要判断许多的特殊情况,当我们要删除当前的结点的时候,一般我们要找到当前结点的前驱,可是当我们删到空的时候,那么什么时候为空呢,当当前的链表里只有一个结点的时候,我们再删,此时链表就为空了,当为空时,它的头指针,和尾指针,此时都指向的时空,
删头的时候,我们直接把头更新到下一个,然后把rear的下一个指向新的头,删尾的时候,我们需要找到它的前驱,然后把要删的结点的下一个给前驱的下一个,把尾指针更新到当前尾的前驱;我们要删任意的时候,找到它的前驱,直接把要删的结点的下一个给前驱的下一个,最后让它的size–.
删除到最后一个
删头
删尾
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();
}
思想:打印toString 方法,使用拼接的方法去进行拼接。