循环链表
对于单链表,由于每个结点只存储了向后的指针,到了尾指针就停止了向后链的操作,将单链表中终端结点的指针由空指针改为指向头结点,就使整个单链表形成一个环,这种头尾相接的链表就称为单向循环链表,简称循环链表
空链表
非空链表
这里的头结点都是虚拟头结点,其实循环链表和单链表的主要差异就在于循环的判断条件上,原来是判断p->next是否为空,现在则是p->next不等于头结点,则循环未结束
用Java语言实现单向循环链表,声明初始化头指针和尾指针,还有一个记录链表有效元素个数变量size。和单链表一样,循环链表也要用到结点,我们创建内部类Node,声明和初始化结点的两个属性数据域data和指针域next
package DS02.动态链表;
import DS01.动态数组.List;
import java.util.Iterator;
//单向循环链表
public class LinkedSingleLoop<E> implements List<E> {
private Node head;
private Node rear;
private int size;
public LinkedSingleLoop(){
head=null;
rear=null;
size=0;
}
//内部类
class Node{ //链表内部的东西,内部类私有,外部不需要知道结点的存在
E data; //数据域 类型由外界决定
LinkedSingleLoop.Node next; //指针域
//构造函数
Node(){
this(null,null);
}
Node(E data, LinkedSingleLoop.Node next){
this.data=data;
this.next=next;
}
@Override
public String toString() {
return data.toString(); //由调用者决定,结点的toString
}
}
@Override
public int getSize() {
return size;
}
@Override
public boolean isEmpty() {
return size==0;
}
对于插入元素方法,元素是存放在结点的数据域中的,先要创建新的结点,这里换一种思路,对于循环链表我们用真实头结点来做,插入元素还有一种特殊情况,链表为空时,也就是我们的第一个图,对其判空,单独讨论即可。
插入元素依旧是三种插入方法,头插、尾插和中间插入,在我前面说单链表时已经讨论过了,不做赘述,相关的操作在代码中也有详细的注释。
@Override
public void add(int index, E e) {
if(index<0||index>size){
throw new IllegalArgumentException("角标越界");
}
Node n=new Node(); //创新结点
if(isEmpty()){ //初始为空时,头尾指针都指向新结点
n.data=e; //同时新结点数据域存放元素e
head=n;
rear=n;
rear.next=head;
}else if(index==size){ //尾插
n.next=rear.next; //先把尾结点的指针域中的地址(也就是头结点地址)给新结点的指针域 (联系头结点和新结点)
rear.next=n; //再把新结点的地址给尾结点的指针域 (联系尾结点和新结点)
rear=n; //移动尾指针 (新结点变成新的尾结点)
}else if(index==0){ //头插
rear.next=n; //先把新结点的地址给尾结点的指针域(联系尾结点和新结点)
n.next=head; //把头结点的地址给新结点n的指针(联系头结点和新结点)
head=n; //移动头指针(新结点变成新的头结点)
}else{ //中间插入
Node p=head; //借助指针p找到指定角标index的前一个结点
for(int i=0;i<index-1;i++){
p=p.next; //移动指针p
} //循环结束时,p在index-1
n.next=p.next; //让新结点指针域指向后一个元素地址(建立后一个元素与新结点的联系)
p.next=n; //让p处结点指针域指向新结点的地址(建立前一个结点与新结点的联系)
}
size++; //有效元素+1
}
@Override
public void addFirst(E e) {
//头插
add(0,e);
}
@Override
public void addLast(E e) {
//尾插
add(size,e);
}
获取角标对应元素,获取元素对应角标,修改元素值,都是借助指针p来找,时间复杂度o(n),把一些特殊情况单独讨论,表头、表尾情况,可以有效降低时间复杂度
//获取角标对应的元素
@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; //借助p指针遍历链表
for(int i=0;i<index;i++){
p=p.next; //移动指针p
}
return p.data; //返回p指针处结点的数据域
}
}
@Override
public E getFirst() {
//获取表头
return get(0);
}
@Override
public E getLast() {
//获取表尾
return get(size-1);
}
//修改指定角标处元素的值,与get()方法类似
@Override
public void set(int index, E e) {
if(index<0||index>size){
throw new IllegalArgumentException("角标越界");
}
Node p=head;
for(int i=0;i<index;i++){
p=p.next;
}
p.data=e;
}
//判断是否包含元素e
@Override
public boolean contains(E e) {
//调用find()方法,看是否能找到
return find(e)!=-1;
}
//获取元素e处的角标
@Override
public int find(E e) {
Node p=head; //借助指针p遍历
for(int i=0;i<size;i++){
if(p.data.equals(e)){ //对比指针p处数据域的元素是否参数e内容一样(这里不能用==比,==比的是地址,equals比的是内容)
return i; //返回角标
}
p=p.next; //循环一次,指针p移动一次
}
return -1;
}
删除元素也是三种方法,头删、尾删和中间删除,本质上都是断开要删除结点与前后结点的联系,使其被回收,不做赘述,相关的操作在代码中也有详细的注释。
@Override
public E remove(int index) {
if(isEmpty()){
throw new IllegalArgumentException("空表");
}
if(index<0||index>size){
throw new IllegalArgumentException("角标越界");
}
E ret=null; //变量ret用于接收返回删除的元素
if(size==1){ //特殊情况:只有一个元素
ret=head.data;
head=null;
rear=null;
}else if(index==size-1){ //尾删
Node p=head;
while(p.next!=rear){ //p指针从头开始,p的下一个为尾结点时跳出
p=p.next; //p往后走
}
ret=rear.data; //ret接收一下
p.next=rear.next; //让倒数第二个结点的指针域指向头结点地址(跳过尾结点)
rear=p; //更新尾指针
}else if(index==1){ //头删
ret=head.data; //把元素值给ret用于返回
rear.next=head.next; //尾结点的指针域指向第二个元素
head=head.next; //头指针往后走,更新
}else{
Node p=head; //借助指针p遍历
for(int i=0;i<index-1;i++){
p=p.next;
}
Node del=p.next; //del存放要删除的结点
ret=del.data; //把结点的元素内容给ret返回
p.next=del.next; //p的指针域指向del的后一个元素,没有人知道del,被回收器回收
}
size--; //有效元素个数递减
return ret; //返回删除的元素
}
@Override
public E removeFirst() {
//头删
return remove(0);
}
@Override
public E removeLast() {
//头删
return remove(size-1);
}
//删除指定元素
@Override
public void removeElement(E e) {
int index=find(e); //先找再删
if(index!=-1){
remove(index);
}else{ //找不到对应角标时,抛异常
throw new IllegalArgumentException("元素不存在");
}
}
清空链表,把构造函数代码拿过来
@Override
public void clear() {
head=null;
rear=null;
size=0;
}
迭代器是遍历输出链表元素,遍历是顺序一遍遍历,所以结束条件不能为p->next不等于头结点,只能是p->next不等于尾结点,但这样就遍历不到尾元素,这里创建一个虚拟头结点,那么next里面就可p指针多移动一次,就可以遍历到尾结点。
//迭代器
@Override
public Iterator<E> iterator() {
return new LinkedSingleLoopIterator(); //创建内部类对象
}
//内部类
public class LinkedSingleLoopIterator implements Iterator<E>{
Node p;
//构造函数
public LinkedSingleLoopIterator(){
p=new Node(); //创建虚拟结点
p.next=head; //从头结点开始遍历
}
@Override
public boolean hasNext() {
return p.next!=rear; //继续条件:p的下一节点不为rear,即尾结点
}
@Override
public E next() {
p=p.next; //p指针移动
return p.data;
}
}
循环链表代码就这样,当然的写完每部分功能一定要测试类中测试,保证代码的正确性,避免全部写完出了问题,不好找。