Java实现动态数组

在学习Java时,学到的第一个数据结构就是数组。不过,JDK提供的数组是一个静态的,并不能很方便地进行增删改查等操作。今天我们就通过封装静态数组来实现我们自己的动态数组。

创建一个类,因为不知道调用者会传入什么类型的数据到数组中,所以我们这里使用泛型来表示数组类型,从而能保证复用性。

public class DynamicArray<T> {

    private T[] mArray;
    /**
     * 数组中元素个数
     */
    private int mSize;

    public DynamicArray(int length) {

        //noinspection unchecked
        mArray = (T[]) new Object[length];
    }

    public DynamicArray() {

        // 默认数组的长度为10
        this(10);
    }
}

类中提供了两个构造方法,使用者可以自定义数组的长度,若没有指定的话,数组默认的长度为10。当然,既然我们实现的是动态数组,那么后面我们会对这个长度进行扩容。

接着对外提供一些数组常用的方法,如数组长度、元素个数以及是否为空。

/**
 * 获取数组长度
 *
 * @return
 */
public int length() {
    return mArray.length;
}

/**
 * 获取数组中元素个数
 *
 * @return
 */
public int getSize() {
    return mSize;
}

/**
 * 判断数组中元素是否为空
 *
 * @return true 是,false 否
 */
public boolean isEmpty() {
    return mSize == 0;
}

上面几个方法比较简单,就不再详述了。接下来就是我们的增删改查等操作了。

添加元素。其实就是往数组中插入元素,需要提供插入的位置以及插入的元素。在插入之前需要将插入位置及之后的元素向后移动,如下图:

insert.png
/**
 * 在指定位置上添加元素
 *
 * @param index   添加到数组中的位置
 * @param element 需要添加的元素
 */
public void add(int index, T element) {

    // step 1 判断添加位置的合法性
    // 因为数组的元素添加是连贯性的,所以不可能大于当前数组中元素的个数
    if (index < 0 || index > mSize) {

        throw new IllegalArgumentException("添加失败,添加位置不能负数以及不能大于当前数组元数个数");
    }

    // step 2 当数组无空间后,再添加元素时需要进行扩容
    if (mSize == mArray.length) {

        resize(2 * mArray.length);
    }

    // step 3 添加元素时,先把数组中从最后一个到index位置的元素向后移动
    for (int i = mSize - 1; i >= index; i--) {

        mArray[i + 1] = mArray[i];
    }

    // step 4 最后将index的位置赋值新插入的值
    mArray[index] = element;

    // step 5 数组元素个数加1
    mSize++;
}

注释已经写得非常清楚了。在step 2中,当数组满了之后,需要进行扩容,而扩容的大小一般为初始长度的一倍。下面是扩容的实现:

/**
 * 对原数组进行扩容
 *
 * @param newLength 扩容后的长度
 */
private void resize(int newLength) {

    //noinspection unchecked
    T[] newArray = (T[]) new Object[newLength];
    //将旧数组元素拷贝到扩容后的数组中
    for (int i = 0; i < mSize; i++) {

        newArray[i] = mArray[i];
    }
    mArray = newArray;
}

我们还可以对外提供头插入和尾插入。

public void addFirst(T element) {
    add(0, element);
}

public void addLast(T element) {
    add(mSize, element);
}

删除元素。把元素从数组删除时,要记得把被删除元素之后的元素向前移动。如下图:

delete.png
/**
 * 删除指定位置的元素
 *
 * @param index 被删除元素的索引
 * @return 被删除的元素
 */
public T delete(int index) {

    // step 1 参数合法性判断
    if (index < 0 || index > mSize) {

        throw new IllegalArgumentException("删除失败,删除的元素位置不能负数以及不能大于当前数组元数个数");
    }

    // step 2 根据索引记录被删除的元素并返回
    T ret = mArray[index];

    // step 3 将index之后的元素到最后一个元素向前移动
    for (int i = index + 1; i < mSize; i++) {

        mArray[i - 1] = mArray[i];
    }

    // step 4 数组元素减1
    mSize--;

    // step 5 将未删除元素的原数组的最后一个置为null
    mArray[mSize] = null;

    // step 6 添加元素时,若是对数组进行了扩容,为了不浪费内存空间,
    // 当删除了一定量的元素后,需要对数组进行缩容
   // 缩容缩到什么时候为止了?当mArray.length为1时就没有必要继续缩容了
    if (mSize == mArray.length / 4 && mArray.length / 2 != 0) {

        resize(mArray.length / 2);
    }

    return ret;
}

step 5并不是非必要的。因为Java本身的垃圾回收机制,会把没用到的对象进行垃圾回收。至于step 6,为什么当元素个数为数组长度的1/4时才缩容,后面在分析到时间复杂度时候会给出原因。跟添加元素一样,我样也可以对外提供删除头和尾的元素。

public T deleteFirst() {
    return delete(0);
}

public T deleteLast() {
    return delete(mSize - 1);
}

修改元素。因为数组支持随机访问,所以修改数组元素非常快。

/**
 * 修改指定位置的元素
 *
 * @param index
 * @param element
 */
public void update(int index, T element) {

    // 参数合法性判断
    if (index < 0 || index > mSize) {

        throw new IllegalArgumentException("修改失败,修改的元素位置不能负数以及不能大于当前数组元数个数");
    }
    mArray[index] = element;
}

查找元素

/**
 * 获取指定位置的元素
 *
 * @param index
 * @return
 */
public T findElement(int index) {

    // 参数合法性判断
    if (index < 0 || index > mSize) {

        throw new IllegalArgumentException("查找失败,查找的元素位置不能为负数以及不能大于当前数组元数个数");
    }
    return mArray[index];

}

/**
 * 获取指定元素的位置
 *
 * @param element
 * @return -1 表示不存在
 */
public int findIndex(T element) {

    for (int i = 0; i < mSize; i++) {

        if (mArray[i].equals(element)) {

            return i;
        }
    }
    return -1;
}

/**
 * 查看数组中是否包含指定元素
 *
 * @param element
 * @return
 */
public boolean contains(T element) {

    for (int i = 0; i < mSize; i++) {

        if (mArray[i].equals(element)) {

            return true;
        }
    }

    return false;
}

根据查找的API,我们还可以对外提供一个删除方法

/**
 * 删除指定元素
 *
 * @param element
 */
public void deleteElement(T element) {

    int index = findIndex(element);
    if (index != -1) {

        delete(index);
    }
}   

时间复杂度分析

  • 添加操作

      addLast(e)          不需要移动任何元素,时间复杂度为O(1);
      
      addFirst(e)         需向后移动n个元素,时间复杂度为O(n);
      
      add(index,e)        当index越小时,向后移动的元素越多,反之,则越少。所以平均来说是n/2,去掉常数,也就是时间复杂度为O(n);
    
  • 删除操作

      与添加操作一样
      
      deleteLast(e)               O(1)
      
      deleteFirst(e)              O(n)
      
      deleteElement(e)            O(n)
      
      delete(index,e)             O(n/2) = O(n)
      
      最坏情况下的时间复杂度为:      O(n)       
    
  • 修改操作

      update(index,e)             O(1)
    
  • 查询操作

      findElement(index)          O(1)
      
      findIndex(e)                O(n)
      
      contains(e)                 0(n)
      
      最坏情况下的时间复杂度为:      O(n)
    
  • 扩容操作

在添加和删除元素时,会出现一种情况:扩容。根据实现的代码可以看出resize方法的时间复杂度在最坏的情况下为O(n)。但是它并不是每次添加或删除元素就会进行扩容,那么它出现的概率是多少呢?

在代码中,我们数组默认的长度为10,也就是在经过11次addLast操作后会触发resize,总共了进行了21次基本操作。也就是说,每一次addLast操作,约等于2次基本操作(21 / 11 ≈ 2)。以此可以推出:假设长度为n,n+1次addLast会触发resize,进行基本操作的次数为2,这样均摊计算的话,resize的时间复杂度为O(1)。很显然,均摊计算(amortized time complexity)比计算最坏情况有意义。

  • 复杂度震荡

当我们调用addLast后,需要resize,此时,addLast的时间复杂度为O(n),紧接着,我们又调用removeLast,又会触发resize,removeLast的时间复杂度为O(n);不停地这样操作就造成了复杂度的震荡。也就是removeLast时,resize被触发的过于着急。解决这个问题的关键点就在于什么时候缩容。我们可以在当前元素个数为数组长度的1/4时才去缩容,这样能保证缩完容后再马上去调用addLast也不会马上resize。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,657评论 6 505
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,889评论 3 394
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,057评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,509评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,562评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,443评论 1 302
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,251评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,129评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,561评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,779评论 3 335
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,902评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,621评论 5 345
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,220评论 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,838评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,971评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,025评论 2 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,843评论 2 354

推荐阅读更多精彩内容

  • 什么是数组? 数组简单来说就是将所有的数据排成一排存放在系统分配的一个内存块上,通过使用特定元素的索引作为数组的下...
    启明_b56f阅读 912评论 0 0
  • 数组什么是数组?数组简单来说就是将所有的数据排成一排存放在系统分配的一个内存块上,通过使用特定元素的索引作为数组的...
    神豪VS勇士赢阅读 262评论 0 0
  • 前言:终于到了疯狂学习数据结构的时候,换个好看的题图,开始吧.. 数组 什么是数组? 数组简单来说就是将所有的数据...
    我没有三颗心脏阅读 3,316评论 10 24
  • 一 深夜里但你能睡去,不,应该别等到深夜就睡着,可你若是睡不去,但愿也别因是孤独。 孤独是否能毁掉一个人,大概是能...
    是毛线阅读 191评论 0 0
  • 这两节是作文课,教语文的林老师给出的题目是这样的:请同学们课余时间,搞一个小型调查,问一问父母、长辈或其他人,10...
    璃夜丶阅读 160评论 0 0