数据结构专题——线性表之单链表及其Java实现

上一篇博客中,博主和同学们一起认识了本系列中的第一个数据结构——线性表。在提供了抽象数据类型(ADT List)和Java版的MyList之后,我们又基于Java语言实现了在内存中顺序存储的线性表(也称为顺序表)。

尽管我们在遇到频繁访问列表中元素时,ArrayList是一个很好的选择;但当我们需要频繁添加、删除列表中元素,尤其是在表的前端进行增删操作时,ArrayList就不是一个特别好的选择了。

为了解决添加、删除元素时需要大量其它元素移动的问题,我们今天一起来接触在内存中链式存储的线性表(也称为链表)。

链式存储的线性表同样需要一组存储单元去存储数据元素,但是这组存储单元可以是内存上连续的,也可以是内存上不连续的

对于顺序存储的线性表来说,每个存储单元只需要存储数据元素即可,因为它们在内存上是连续的,第一个数据元素所占的内存空间之后,一定是下一个数据元素。

但是对于链式存储的线性表来说,除了要存数据元素之外,还需要存它的后继元素的存储地址,因为下一个数据元素很可能在内存中任意位置。

因此,对于每一个数据元素ai来说,除了存储其本身的数据信息之外,还需要存储一个指示其直接后继元素的内存位置。我们把存储数据元素信息的域称为数据域,把存储直接后继元素位置的域称为指针域。这两部分信息组成数据元素ai的存储映像,称为节点(Node)。n个节点连接成一个链表(a1,a2, … ,an),因为这个链表的每个节点中只包含一个指针域(指向它的直接后继元素),所以这种链表也成为单链表。

  • 下面我们就来一起用Java实现一个单链表MySinglyLinkedList
    public class MySinglyLinkedList<E> implements MyList<E> {

    //单链表中的首节点,此节点不存放元素,只用来指向单链表中的第一个节点
    private Node firstNode;
    //单链表的元素数量
    private int size;

    public MySinglyLinkedList() {
        firstNode = new Node(null, null);
        this.size = 0;
    }
    //内部类Node,用来表示单链表中每一个数据元素
    class Node{
        //数据域
        E data;
        //后继指针域,指向下一个数据元素
        Node nextNode;
        //提供一个构造函数
        Node(E data, Node nextNode){
            this.data = data;
            this.nextNode = nextNode;
        }
    }
  • 接下来我们实现获取元素个数和查看是否为空的方法,很简单单行代码即可实现
    @Override
    public int size() {
        // TODO Auto-generated method stub
        return this.size;
    }

    @Override
    public boolean isEmpty() {
        // TODO Auto-generated method stub
        return size()==0;
    }
  • 然后是两种不同的插入方法
    @Override
    public boolean add(E e) {
        // TODO Auto-generated method stub
        //在单链表的结尾插入一个元素,就相当于在index为size的位置处插入元素
        add(size(),e);
        return true;
    }

    @Override
    public void add(int index, E element) {
        // TODO Auto-generated method stub
        //一旦要插入元素的位置为负或大于目前的元素数量就抛出异常
                //此处允许index等于size,相当于在单链表末尾插入元素
        if(index<0 || index>size())
            throw new IndexOutOfBoundsException();
        //需要插入的元素
        Node newNode = new Node(element, null);
        //由于单链表只能从前往后遍历,因此要想在某个位置插入元素,必须先获得它前一个位置上的元素节点
        //私有方法getPrevNodeByIndex可以用来获取该元素节点
        Node posNode = getPrevNodeByIndex(index);
        //需要被插入元素的后继元素指向索引位置的原节点
        newNode.nextNode = posNode.nextNode;
        //前一个位置的节点的后继元素指向被插入的节点
        posNode.nextNode = newNode;
        this.size++;
    }
    //私有方法getPrevNodeByIndex获取index前一个位置的节点
    private Node getPrevNodeByIndex(int index) {
        Node posNode = firstNode;
        for(int i=0; i<index;i++) {
            posNode = posNode.nextNode;
        }
        return posNode;
    }
  • 紧接着是两个不同的删除方法
    @Override
    public boolean remove(Object o) {
        // TODO Auto-generated method stub
        if(isEmpty())
            return false;
        //如果需要被删除的元素为null
        if(o == null) {
            for(Node n = firstNode; n.nextNode!=null; n = n.nextNode) {
                if(n.nextNode.data == null) {
                    Node targetNode = n.nextNode;
                    n.nextNode = targetNode.nextNode;
                    //让被删除节点的数据域,指针域和它的引用变量都指向null,方便GC工作
                    targetNode.data = null;
                    targetNode.nextNode = null;
                    targetNode = null;
                    this.size--;
                    return true;
                }
            }
            return false;
        }else {
            for(Node n = firstNode; n.nextNode!=null; n = n.nextNode) {
                if(o.equals(n.nextNode.data)) {
                    Node targetNode = n.nextNode;
                    n.nextNode = targetNode.nextNode;
                    //让被删除节点的数据域,指针域和它的引用变量都指向null,方便GC工作
                    targetNode.data = null;
                    targetNode.nextNode = null;
                    targetNode = null;
                    this.size--;
                    return true;
                }
            }
            return false;
        }
    }

    @Override
    public E remove(int index) {
        // TODO Auto-generated method stub
        //一旦要插入元素的位置为负或大于目前的元素数量就抛出异常
                //此处不允许index等于size
        checkIndexValidation(index);
        //获取要被删除节点的前驱节点
        Node prevNode = getPrevNodeByIndex(index);
        //获取要被删除的节点
        Node targetNode = prevNode.nextNode;
        //取出要被删除节点的数据域
        E data = targetNode.data;
        //要被删除节点的前驱节点的后继节点指向要被删除节点的后继节点
        prevNode.nextNode = targetNode.nextNode;
        //让被删除节点的数据域,指针域和它的引用变量都指向null,方便GC工作
        targetNode.data = null;
        targetNode.nextNode = null;
        targetNode = null;
        this.size--;
        return data;
    }
  • 之后是获取,修改,查看是否包含,以及清空整个列表这几个简单的方法了:
    @Override
    public E get(int index) {
        // TODO Auto-generated method stub
        checkIndexValidation(index);
        return getPrevNodeByIndex(index).nextNode.data;
    }

    @Override
    public E set(int index, E element) {
        // TODO Auto-generated method stub
        checkIndexValidation(index);
        Node targetNode = getPrevNodeByIndex(index).nextNode;
        E oldData = targetNode.data;
        targetNode.data = element;
        return oldData;
    }
    @Override
    public boolean contains(Object o) {
        // TODO Auto-generated method stub
        if(isEmpty())
            return false;
        if(o == null) {
            for(Node n = firstNode; n.nextNode!=null; n = n.nextNode) {
                if(n.nextNode.data == null) {
                    return true;
                }
            }
            return false;
        }else {
            for(Node n = firstNode; n.nextNode!=null; n = n.nextNode) {
                if(o.equals(n.nextNode.data)) {
                    return true;
                }
            }
            return false;
        }
    }
    @Override
    public void clear() {
        // TODO Auto-generated method stub
        for(Node n = firstNode.nextNode; n !=null;) {
            Node next = n.nextNode;
            n.data = null;
            n.nextNode = null;
            n = next;
        }
        firstNode.nextNode = null;
        this.size = 0;
    }

我们在这里也分析一下MyLinkedList的特点:

我们要读取或修改某一个位置的节点需要花费线性时间 O(n)去遍历列表;
我们要增加或删除某一个(已知位置的)节点最坏情况下要花费常数时间 O(1)

因此,当我们在遇到频繁访问列表中元素时,ArrayList是一个很好的选择;但当我们需要频繁在已知位置添加、删除列表中元素时,LinkedList有更好的性能。

本节博客的源码全部放在这里,欢迎大家下载,下载后记得修改源码中的package名字哦。

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值