数据结构与算法(十)循环链表

循环链表


对于单链表,由于每个结点只存储了向后的指针,到了尾指针就停止了向后链的操作,将单链表中终端结点的指针由空指针改为指向头结点,就使整个单链表形成一个环,这种头尾相接的链表就称为单向循环链表,简称循环链表

空链表

非空链表

这里的头结点都是虚拟头结点,其实循环链表和单链表的主要差异就在于循环的判断条件上,原来是判断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;
        }
    }

循环链表代码就这样,当然的写完每部分功能一定要测试类中测试,保证代码的正确性,避免全部写完出了问题,不好找。

发布了70 篇原创文章 · 获赞 56 · 访问量 1980

猜你喜欢

转载自blog.csdn.net/qq_43624033/article/details/103617972