深入浅出LinkedList与ArrayList(2)

引言

上一篇博文,我们了解了LinkedList与ArrayList的底层构造和效率问题。在这篇博文中,我自己写了两个自己的数据结构来感受效率问题,这些代码的由来源于我在某易的师兄的提问。所以我做了以下整理,希望对大家有所启发,其实我们自己也能写底层的源码。笔者目前整理的一些blog针对面试都是超高频出现的。大家可以点击链接:http://blog.csdn.net/u012403290

需求

需要有一个固定长度的数据结构用于存储数据,在数据插入到阀值的时候,移除最老的数据。使用数组与双向链表实现。

基于数组实现

package com.brickworkers;
/**
 * 
 * @author Brickworker
 * Date:2017年4月18日上午11:35:01 
 * 关于类MyArrayList.java的描述:数组结构
 * Copyright (c) 2017, brcikworker All Rights Reserved.
 */
public class MyArrayList<T> {

    private int size;//定义MyArrayList存储数据的数量

    private Object[] table;//底层用数组存储

    //构造函数,指定数组容量
    public MyArrayList(int capacity) {//构建一个固定容量的MyArrayList
        if(capacity >= 0)
            table = new Object[capacity];
        else
            throw new IllegalArgumentException("capacity is not illegal"+ capacity);
    }

    //新增,只插入到数组尾部
    public void add(T t){
        //如果数据量到阀值,那么触发移除
        if(size == table.length)
            removeOldest();
        table[size] = t;//数据放到数组最后面
        size ++;
    }


    //移除最老的
    public void removeOldest(){
        //数组整体前移,覆盖最老的数据
        for (int i = 0; i < size - 1; i++) {
            table[i] = table[i+1];//把整个数组往前移动一位
        }
        size --;
    }
}

这代码中我没有继承和实现任何接口,其实大家如果尝试写的话,可以继承Iterable和实现AbstractList来实现,这样的话,你就可以重写集合方法,同时还可以用增强for循环来遍历。不过如果要精简还是向上面这段代码一样。

基于双向链表实现

package com.brickworkers;
/**
 * 
 * @author Brickworker
 * Date:2017年4月18日上午11:35:19 
 * 关于类MyLinkedList.java的描述:双向链表结构
 * Copyright (c) 2017, brcikworker All Rights Reserved.
 */
public class MyLinkedList<T> {

    private int size;//MyLinkedList中真实存在的数据

    //避免麻烦,定义首节点和尾节点
    private Node<T> startNode; 

    private Node<T> endNode;

    private int MAX_SIZE;//最大容量

    //定义双向节点
    private static class Node<T>{//静态内部类
        public T date;

        public Node<T> prev;

        public Node<T> next;

        public Node(T t, Node<T> p, Node<T> n) {//节点构造函数
            this.date = t;
            this.prev = p;
            this.next = n;
        }
    }


    //构造函数
    public MyLinkedList(int capacity) {//指定容量,自定义两个节点不计算容量
        if(capacity >=0 ){
            MAX_SIZE = capacity;
            startNode = new Node<T>(null, null, null);//起始节点
            endNode = new Node<T>(null, startNode, null);//尾节点
            startNode.next = endNode;//链接两个节点
            size = 0;
        }else
            throw new IllegalArgumentException("capacity is not illegal"+ capacity);
    }


    //添加到链表结尾
    public void add(T t){
    //如果数据存储达到阀值,那么触发移除操作
        if(size == MAX_SIZE)
            removeOldest();
        //新建包含t数据的节点,并把它插入到最后(注意我说的最后不包括自定义两个节点)
        endNode.prev = endNode.prev.next = new Node<T>(t, endNode.prev, endNode);
        size ++;

    }


    //移除最老的节点
    private void removeOldest(){
    //避免恶意数据
        if(startNode.next == endNode){
            throw new IllegalArgumentException("can not remove Node, the size is 0");
        }
        //移除头结点
        Node<T> p = startNode.next; //p就是头结点(注意我说的头结点不包括自定义的两个节点)
        startNode.next = p.next;
        p.next.prev = startNode;
        size --;
    }

}

上面这段就是用双向链表来实现,其中核心的就是一个Node的静态内部类,配合一个插入和移除的方法。进行测试:

package com.brickworkers;

public class MyListTest {

    static void testList(int size, int forsize){
        long arrStartTime = System.currentTimeMillis();
        MyArrayList<Integer> myArrayList = new MyArrayList<Integer>(size);
        for (int i = 0; i < forsize; i++) {
            myArrayList.add(i);
        }
        System.out.println("数组结构耗时:"+(System.currentTimeMillis() - arrStartTime));

        long linkStartTime = System.currentTimeMillis();
        MyLinkedList<Integer> myLinkedList =new MyLinkedList<Integer>(size);
        for (int i = 0; i < forsize; i++) {
            myLinkedList.add(i);
        }
        System.out.println("双向链表结构耗时:"+(System.currentTimeMillis() - linkStartTime));
    }

    public static void main(String[] args) {
        testList(10000, 1000000);//容量设置为10000, 循环插入1000000次
    }

}

//输出结果:
//数组结构耗时:6154
//双向链表结构耗时:22

从上面的结果可以看出,数据的开销非常巨大。我们考虑为什么MyArrayList开销如此之大呢?核心问题其实是出在移除一个最老的数据后数组整体移动的原因,整个数组的移动开销是非常大的。所以数组实现虽然可行,但是不合理。

我们考虑一下需求,容量一定的时候循环插入,当容量饱和的时候就需要开始移除数据。那么我们可以考虑在数组饱和之后,把新增的数据覆盖即将要移除的数据中。那么其实就是不移动数组,而是移动了数据的下标,我修改了MyArrayList如下:

修改之后的MyArrayList

package com.brickworkers;
/**
 * 
 * @author Brickworker
 * Date:2017年4月18日上午11:35:01 
 * 关于类MyArrayList.java的描述:数组结构
 * Copyright (c) 2017, brcikworker All Rights Reserved.
 */
public class MyArrayList<T> {

    private int size;

    private Object[] table;

    private int pointer; //数组引用

    //构造函数,指定数组容量
    public MyArrayList(int capacity) {
        if(capacity >= 0)
            table = new Object[capacity];
        else
            throw new IllegalArgumentException("capacity is not illegal"+ capacity);
    }

    //新增插入到引用位置
    public void add(T t){
        table[pointer] = t;//按指针指向的地方进行插入
        trimPointer();//指针使用之后需要进行指针调整
        if(size != table.length)//如果数据饱和,size不再增加
            size++;
    }


    //调整指针位置
    public void trimPointer(){
        //如果指针指向最后就回拨到最前
        if(pointer == table.length - 1){
            pointer = 0;//指针归0
        }else{
            pointer++;//指针往前移动一位
        }

        }
}

修改之后的MyArrayList修改的核心是修改了remove的实现,用最新的数据去覆盖最老的数据。测试数据量与上面相同的情况下,测试结果如下:

//数组结构耗时:12
//双向链表结构耗时:22

发现这个时候数组的效率比双向链表还要高,那么我们双向链表如果也和数组一样实现会怎么样呢?以下是我修改之后的双向链表实现:

修改之后的MyLinkedList

package com.brickworkers;

import javax.swing.tree.DefaultTreeCellEditor.EditorContainer;

/**
 * 
 * @author Brickworker
 * Date:2017年4月18日上午11:35:19 
 * 关于类MyLinkedList.java的描述:双向链表结构
 * Copyright (c) 2017, brcikworker All Rights Reserved.
 */
public class MyLinkedList<T> {

    private int size;

    //避免麻烦,定义首节点和尾节点
    private Node<T> startNode;

    private Node<T> endNode;

    private int MAX_SIZE;

    private Node<T> pointerNode;//目标指针

    //定义双向节点
    private static class Node<T>{
        public T date;

        public Node<T> prev;

        public Node<T> next;

        public Node(T t, Node<T> p, Node<T> n) {
            this.date = t;
            this.prev = p;
            this.next = n;
        }
    }


    //构造函数
    public MyLinkedList(int capacity) {
        if(capacity >=0 ){
            MAX_SIZE = capacity;
            startNode = new Node<T>(null, null, null);//起始节点
            endNode = new Node<T>(null, startNode, null);//尾节点
            startNode.next = endNode;//链接两个节点
            size = 0;
        }
        else
            throw new IllegalArgumentException("capacity is not illegal"+ capacity);
    }


    //添加到链表结尾
    public void add(T t){
    //如果双向链表中存储的数据达到阀值之后,就直接把头节点移动到尾部进行值覆盖
        if(size == MAX_SIZE){
            removeFirst2Last();
            pointerNode.date = t;
        }else{//如果没有达到阀值的话,那么就新增一个节点放置双向链表最后
            endNode.prev = endNode.prev.next = new Node<T>(t, endNode.prev, endNode);
            size ++;
        }

    }


    //把头结点移动到尾部
    private void removeFirst2Last(){
        if(startNode.next == endNode){
            throw new IllegalArgumentException("can not remove Node, the size is 0");
        }
        pointerNode = startNode.next;
        startNode.next = pointerNode.next;//解决最头上节点
        pointerNode.next.prev = startNode;//解决指针节点的原本后节点
        pointerNode.next = endNode;//
        pointerNode.prev = endNode.prev;//解决指针节点
        endNode.prev.next = pointerNode;//解决尾节点之前的节点
        endNode.prev = pointerNode;//最后解决尾节点



    }

}

和数组的实现方式一样,在双向链表中当数据饱和之后就需要把最老的节点移动到最前面来,并进行值覆盖。测试数据和原先还是一样,以下是测试结果:

//数组结构耗时:12
//双向链表结构耗时:20

效果不大,但是的确有一点点的优化。希望对大家有所帮助。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值