Java当中的数据结构表(ADT)解析(下)

来源:互联网 发布:centos 挂载硬盘到www 编辑:程序博客网 时间:2024/05/17 00:54

1、前言

之前对表(ADT)进行过一些介绍于说明,并且对Java当中ArrayList进行了简单的实现, Java当中的数据结构表(ADT)解析(上)这篇博客主要进行LinkedList的简单实现。以及两种数据结构在效率之上的差别

2、LinkedList的简单实现。

ArrayList底层是由数组实现的,而LinkedList底层是通过链表实现的,但是在Java当中是没有链表这种数据结构的。所以我们要通过自定义来进行。

2.1、分析

我们上述已经知道我们需要通过自定义底层节点,来实现双向链表,所以定义一个Node< T >是必须的,关于链我们直接通过Java当中的索引来做,我们还需要对当前双向链表进行简单的改造,通过标记节点来完成。还需要对该集合的操作进行定义,在这里提供出三个核心方法,其与的方法都是建立在这三个核心方法之上的。最后为了可以通过Iterable进行遍历,提供一个LinkedListIterator。我们可以把我们要完成的在下方总结一下:

  • 自定义Node< T >。
  • 添加头节点和尾节点。
  • 对外提供操作。
  • 对外提供LinkedListIterator 好进行遍历。

2.2、节点和链

我们知道一个节点,应该包含有三个部分,第一个部分应该是指上一个节点的索引,第二部分应该是该节点所含有的数据,第三部分应该是指向下一个节点的索引。

这里写图片描述

具体实现如下所示:

    /**     * 节点类,用来表示链表当中的一个节点。     *     * @author pengchengliu     * @param <AnyType>     */    //TODO : 问题一:为什么选择是private。    //TODO : 问题二:为什么选择是static。    private static class Node<AnyType>{        //TODO :为什么内部属性选用public 。        public AnyType data ;        public Node<AnyType> prev ;        public Node<AnyType> next ;        public Node(AnyType data, Node<AnyType> prev, Node<AnyType> next){            this.data = data ;            this.prev = prev ;            this.next = next ;        }    }

在上述代码当中,笔者提到了三个问题。这这里简单的说一下。

第一个问题:为什么选择是private。

答案 : 应该这个内部类没有必要暴露给外部,外部只要默默的使用该类就Ok,不需要知道内部实现细节,注意这里的外部是指使用LinkedList的外部的类,而不是LinkedList。

第二个问题:为什么选择是static。

答案:这里应该是静态内部类和内部类的区别。想深入理解一下的看客可以去输入了解一下。这里笔者简单的说一下,Node这个类是独立于LinkedList的,也就是Node这个类没有使用到LinkedList这个类的实例,并且Node不依赖于LinkedList实例被实例化。所以选择static了。

第三个问题:为什么内部属性选用public

答案:这个问题好回答,既然整个类都是private,内部是用public也就无所谓了。

2.3、标记节点

了解了上述Node类以后,这里特别说明一下有两个比较特殊的位置,就是第一个元素和最后一个元素,我们第一个元素的第一部分应该是null,而最后一个节点的第三部分应该是null,当我们检测到了最后一个节点的第三部分是null,则代表遍历结束,这是基本的链表结构,但是为了省去不必要的麻烦,我们使用标记节点。代替第一个元素和最后一个元素。为什么选择标记节点。

这里写图片描述

需要注意的是由于第一个元素和最后一个元素都比较特殊,所以通过标记节点头节点和尾节点。

为什么使用标记节点呢?

如果我们不使用标记节点,我们会存在特殊情况,比如删除或者添加第一个元素,这样就会存在一种可能,那就是我们需要改变第一个元素的位置,而如果有标记节点的话,第一个元素的位置和最后一个元素的位置就是一定的,我们规避了这一种特殊情况。

2.4、主要操作

我们知道数据结构重要的地方有两点,第一个比较重要的地方就是集合内的元素,而第二个重要的地方就是操作。

2.4.1、清空集合

因为我们有标记节点,所以我们清空集合只需要,让头节点指向尾节点,尾节点指向头节点,然后让size为0即可。效果图如下所示

这里写图片描述

代码如下所示:

    /**     * 清空当前的容器     */    public void clear() {        beginMarker = new Node<AnyType>(null, null, null);        endMarker = new Node<AnyType>(null, beginMarker, null);        beginMarker.next = endMarker ;        //  重置计数器。        theSize = 0 ;        modCount++ ;    }

2.4.2、插入元素

在固定的位置插入一个元素,也就是说,改变该位置上面前后节点的链即可,具体的步骤如下图所示:

这里写图片描述

代码如下所示:

    /**     * 添加一个元素,将元素item添加到node之前。此方法为其他方法的服务方法,所以不需要暴露给外部类调用     */    private void addBefore(Node<AnyType> node, AnyType item){        Node<AnyType> itemNode = new Node<AnyType>(item, node.prev, node);        itemNode.prev.next = itemNode ;        node.prev = itemNode ;        //更新计数器。        theSize ++ ;        modCount ++ ;    }

2.4.3、移除元素

这个操作方法和上述插入元素,没有什么区别,也是改变前后节点的链的问题。

这里写图片描述

代码实现如下所示:

    /**     * 移除一个元素,将oldNode在原有的集合当前删除。     * @param oldNode     * @return     */    private AnyType remove(Node<AnyType> oldNode){        oldNode.next.prev = oldNode.prev ;        oldNode.prev.next = oldNode.next ;        //更新计数器        theSize-- ;        modCount++ ;        return oldNode.data ;    }

2.4.4、得到节点

通过下标得到相应的Node,具体的代码实现如下所示:

    /**     * 得到一个元素,根据index下表值,得到一个元素     * @param index 下表值     * @return 得到的元素。     */    private Node<AnyType> getNode(int index){        Node<AnyType> tempNode ;        //下标合法性验证        if(index < 0 || index > this.size()){            throw new IndexOutOfBoundsException();        }        //由于是双向列表,所以根据index的值不同在两端获取。使得效率更加的高效,O(N/2)的时间复杂度。        if(index < this.size() / 2){            //从开头开始找,获取第一个元素            tempNode = beginMarker.next ;            for(int i = 0; i < index; i ++){                tempNode = tempNode.next ;            }        } else {            //从结尾开始找,获取最后一个元素。            tempNode = endMarker.prev ;            for(int i = this.size(); i > index; i--){                tempNode = tempNode.prev ;            }        }        return tempNode ;    }

2.5、LinkedListIterator的实现。

代码实现如下所示:

/***     * LinkedList的迭代器。     * @author pengchengliu     *     */    private class LinkedListIterator implements Iterator<AnyType>{        //当前的位置,默认指向第一个元素。        private Node<AnyType> current = beginMarker.next ;        private int expectedModCount = modCount ;        private boolean okToRemove = false ;        @Override        public boolean hasNext() {            return current != endMarker;        }        @Override        public AnyType next() {            //TODO : 为什么需要做这么一步判断呢?            if(modCount == expectedModCount){                throw new ConcurrentModificationException();            }            //TODO :为什么需要这个判断            if(!hasNext()){                throw new NoSuchElementException() ;            }            AnyType data = current.data;            current = current.next ;            okToRemove = true ;            return data;        }        @Override        public void remove() {            if(modCount != expectedModCount){                throw new ConcurrentModificationException();            }            //TODO :为什么需要这个判断            if(!okToRemove){                throw new IllegalStateException();            }            MyLinkedList.this.remove(current.prev);            okToRemove = false ;            expectedModCount++ ;        }    }

在上述代码当中笔者提出了三个问题,在这里笔者简单的说一下。

第一个问题:

   if(modCount == expectedModCount){                throw new ConcurrentModificationException();            }

这个判断存在的必要。在遍历阶段,防止对表结构进行改变,

第二个问题:

    if(!hasNext()){                throw new NoSuchElementException() ;            }

也就是我们常说的越界吧。

第三个问题

    if(!okToRemove){                throw new IllegalStateException();            }

就是没有使用遍历就是调用remove方法。

2.6总体实现

package list;import java.util.ConcurrentModificationException;import java.util.Iterator;import java.util.NoSuchElementException;/** * 自定义LinkedList . * 实现选择是选用头节点和尾节点。可以极大简便很多的特殊情况,例如:删除头节点。额外的调整整个链表的头节点。 * @author pengchengliu * */public class MyLinkedList<AnyType> implements Iterable<AnyType>{    private Node<AnyType> beginMarker ;    private Node<AnyType> endMarker ;    //TODO :为什么会存在modCount计数器呢?    private int theSize ;    private int modCount ;    public MyLinkedList(){        this.clear();    }    /**     * 清空当前的容器     */    public void clear() {        beginMarker = new Node<AnyType>(null, null, null);        endMarker = new Node<AnyType>(null, beginMarker, null);        beginMarker.next = endMarker ;        //  重置计数器。        theSize = 0 ;        modCount++ ;    }    /**     * 获取当前容器的长度。     * @return 当前容器的长度 。     */    public int size(){        return theSize ;    }    /**     * 判断当前集合是否为空     * @return 当前容器是否为空     */    public boolean isEmpty(){        return this.size() == 0 ;    }    /**     * 添加一个节点,到整个集合的最后面。     * @param node     * @return     */    public boolean add(AnyType node){        add(size(), node);        return true ;    }    /**     * 添加一个一个节点,在index的位置上面添加一个元素。     * @param index :     * @param newVaule :     */    public void add(int index, AnyType newVaule){        addBefore(getNode(index), newVaule);    }    /**     * 得到集合当中的一个元素,通过索引得到集合当中的一个元素。     * @param index      * @return     */    public AnyType get(int index){        return getNode(index).data;    }    /**     * 替换一个元素,替换位置在index上面的元素为newValue     * @param index 原有的位置     * @param newValue 新元素的值     * @return 以前index位置的元素值。     */    public AnyType set(int index, AnyType newValue){        Node<AnyType> node = getNode(index);        AnyType oldValue = node.data;        node.data = newValue ;        return oldValue ;    }    /**     * 移除一个元素,移除index位置的元素并且把给元素返回     * @param index : 指定的位置。     * @return : 要移除的元素。     */    public AnyType remove(int index){        return remove(getNode(index));    }    /**     * 添加一个元素,将元素item添加到node之前。此方法为其他方法的服务方法,所以不需要暴露给外部类调用     */    private void addBefore(Node<AnyType> node, AnyType item){        Node<AnyType> itemNode = new Node<AnyType>(item, node.prev, node);        itemNode.prev.next = itemNode ;        node.prev = itemNode ;        //更新计数器。        theSize ++ ;        modCount ++ ;    }    /**     * 移除一个元素,将oldNode在原有的集合当前删除。     * @param oldNode     * @return     */    private AnyType remove(Node<AnyType> oldNode){        oldNode.next.prev = oldNode.prev ;        oldNode.prev.next = oldNode.next ;        //更新计数器        theSize-- ;        modCount++ ;        return oldNode.data ;    }    /**     * 得到一个元素,根据index下表值,得到一个元素     * @param index 下表值     * @return 得到的元素。     */    private Node<AnyType> getNode(int index){        Node<AnyType> tempNode ;        //下标合法性验证        if(index < 0 || index > this.size()){            throw new IndexOutOfBoundsException();        }        //由于是双向列表,所以根据index的值不同在两端获取。使得效率更加的高效,O(N/2)的时间复杂度。        if(index < this.size() / 2){            //从开头开始找,获取第一个元素            tempNode = beginMarker.next ;            for(int i = 0; i < index; i ++){                tempNode = tempNode.next ;            }        } else {            //从结尾开始找,获取最后一个元素。            tempNode = endMarker.prev ;            for(int i = this.size(); i > index; i--){                tempNode = tempNode.prev ;            }        }        return tempNode ;    }    @Override    public Iterator<AnyType> iterator() {        return new LinkedListIterator();    }    /***     * LinkedList的迭代器。     * @author pengchengliu     *     */    private class LinkedListIterator implements Iterator<AnyType>{        //当前的位置,默认指向第一个元素。        //TODO : 下面两个变量存在的意义。        private Node<AnyType> current = beginMarker.next ;        private int expectedModCount = modCount ;        private boolean okToRemove = false ;        @Override        public boolean hasNext() {            return current != endMarker;        }        @Override        public AnyType next() {            //TODO : 为什么需要做这么一步判断呢?            if(modCount == expectedModCount){                throw new ConcurrentModificationException();            }            //TODO :为什么需要这个判断            if(!hasNext()){                throw new NoSuchElementException() ;            }            AnyType data = current.data;            current = current.next ;            okToRemove = true ;            return data;        }        @Override        public void remove() {            if(modCount != expectedModCount){                throw new ConcurrentModificationException();            }            //TODO :为什么需要这个判断            if(!okToRemove){                throw new IllegalStateException();            }            MyLinkedList.this.remove(current.prev);            okToRemove = false ;            expectedModCount++ ;        }    }    /**     * 节点类,用来表示链表当中的一个节点。     *     * @author pengchengliu     * @param <AnyType>     */    //TODO : 问题一:为什么选择是private。    //TODO : 问题二:为什么选择是static。    private static class Node<AnyType>{        //TODO :为什么内部属性选用public 。        private AnyType data ;        public Node<AnyType> prev ;        public Node<AnyType> next ;        public Node(AnyType data, Node<AnyType> prev, Node<AnyType> next){            this.data = data ;            this.prev = prev ;            this.next = next ;        }    }}

3、两者实现方式在效率上面的差别

我们知道了ArrayList适合读,而插入和删除还是选择LinkedList,具体的要看两者之间的差别,还是需要通过一个例子来看。

例子:删除一个List当中为偶数的元素。

public static void testDome(List<Integer> list){    Iterator<Integer> its = list.iterator();    while(its.hasNext()){        if(itr.next() % 2 == 0){            itr.remove();        }    }}

如果是LinkedList,40万条数据,大概时间是0.031秒,而如果是ArrayList大概是2.5分钟,而如果是80万数据的话,LinkedList大概是0.062秒,ArrayList则是10分钟。