【数据结构与算法】 01 - 动态数组

1. 什么是数据结构 ?

  1. 数据结构是计算机存储组织数据的方式。

1.1 线性结构

  1. 数组、链表、栈、队列、哈希表。

1.2 树形结构

  1. 二叉树、AVL树、红黑树、B树、堆、Trie、哈夫曼树、并查集。

1.3 图形结构

  1. 邻接矩阵、邻接表。

2. 线性表

  1. 线性表是有n个相同类型元素的有限序列。(n >= 0)。
线性表

a1是首节点(首元素),an是尾节点(尾元素)。
a1 是 a2 的前驱,a2是a1的后继。

2.1 常见的线性表

  1. 数组。

  2. 链表。

  3. 栈。

  4. 队列。

  5. 哈希表(散列表)。

2.2 数组

  1. 数组是一种顺序存储的线性表,所有元素的 内存地址都是连续的
数组
  1. 在很多编程语言中,数组都有一个致命的缺点:无法动态的修改容量。

  2. 在实际开发中,我们希望数组容量是可以动态改变的。

2.3 动态数组

动态数组的简单实现 (int类型)
  1. 定义一个ArrayList
  1. 定义成员和静态变量 :
    /**
     * 数组的尺寸
     */
    private int size = 0;

    /**
     * 数组实例
     */
    private int[] elements;

    /**
     * 定义一个默认的数组容量 为 10
     */
    private static final int DEFAULT_CAPACITY = 10;
    /**
     * 元素未找到 返回 -1
     */
    private static final int ELEMENT_NOT_FOUNT = -1;
  1. 定义有参构造和无参构造用于初始化数组:
    /**
     * 无参构造,通过this(param) 调用有参构造
     */
    public ArrayList() {
        // this() 调用自身有参构造函数
        this(DEFAULT_CAPACITY);
    }

    /**
     * 定义有参构造方便进行初始化
     *
     * @param capacity
     */
    public ArrayList(int capacity) {
         // 如果传递的容量 小于默认的容量将使用默认容量 否则使用传递的数组容量
        capacity = capacity < DEFAULT_CAPACITY ? DEFAULT_CAPACITY : capacity;
        elements = new int[capacity];
    }
  1. size() 方法 返回数组的长度:
   /**
     * 返回数组中元素的个数
     *
     * @return
     */
    public int size() {
        return size;
    }
  1. isEmpty() 判断当前数组是否为空:
   /**
     * 数组是否为空
     *
     * @return
     */
    public boolean isEmpty() {
        // 判断size 是不是等于 0 即可 如果size == 0 表示 数组是空的
        return size == 0;
    }
  1. contains(int element) 判断数组中是否包含某个元素:
  public boolean contains(T element) {
        return indexOf(element) != ELEMENT_NOT_FOUND;
    }
  1. void add(int element) 向数组中添加指定的元素:
 public void add(int element) {
        // size是动态变化的 初始值是 0 添加第一个元素 在添加完成一个元素最后将 size + 1 操作
        // elements[size++] = element;
        // 直接调用在指定位置添加元素的方法
        add(size, element);
    }
  1. int get(int index) 返回指定index处的元素:
 public int get(int index) {
        // 首先判断一下传递进来的索引值是否合法
        rangeCheck(index);
        return elements[index];
    }
  1. int set(int index, int element) 设置index位置的元素为某值,并返回之前的值:
public int set(int index, int element) {
        // 首先判断一下传递进来的索引值是否合法
        rangeCheck(index);
        int old = elements[index];
        elements[index] = element;
        return old;
    }
  1. void add(int index, int element) 向指定位置添加元素 :
简单图解
public void add(int index, int element) {
        // 这里index的范围是允许等于size的因为插入的时候可能是在最后一个位置插入一个元素
        rangeCheckForAd(index);
        for (int i = size - 1; i >= index; i--) {
            elements[i + 1] = elements[i];
        }
        // 将空出的位置填入值
        elements[index] = element;
        // size 加一
        size++;
    }
  1. int remove(int index) 删除index位置的元素 并返回删除的值: 由于数组的内存地址是连续的,无法直接挖掉某个元素,所以必须将数组进行移动,删除元素。
简单图解

图中需要将 index = 3 位置的元素 55 删除 ,需要移动的范围是 [4,6],未移动之前的数组 size = 7 , index = 3. 得出需要移动的通项 [index + 1 , size - 1]

public int remove(int index) {
        rangeCheck(index);
        // 记录一下即将需要删除的值
        int old = elements[index];

        // 循环的范围就是需要移动的元素的范围 , 因为需要移动这么多个元素 从 [index + 1,size -1]
        for (int i = index + 1; i <= size - 1; i++) {
            elements[i - 1] = elements[i];
        }
        // 长度减一
        size--;
        return old;
    }
  1. int indexOf(int element) 查找元素在数组中的位置:
public int indexOf(int element) {
        for (int i = 0; i < size; i++) {
            if (elements[i] == element) {
                return i;
            }
        }
        return ELEMENT_NOT_FOUNT;
    }
  1. void clear() 清除数组中所有元素:
public void clear() {
        size = 0;
    }
  1. 重写默认的 toString() 方法 :
 @Override
    public String toString() {
        // 字符串拼接使用StringBuilder 提高效率
        StringBuilder str = new StringBuilder();

        // 拼接格式 size = 3 [88,33,99]
        str.append("size = ").append(size).append(" [ ");

        for (int i = 0; i < size; i++) {
            // 和 i != size -1 对比优先选择 i != 0, 因为 size - 1 需要进行一次运算
            if (i != 0) {
                str.append(", ");
            }
            str.append(elements[i]);
        }

        str.append("]");
        return str.toString();
    }

15 . 检查 index 的范围:

  /**
     * 检查index的范围
     *
     * @param index
     */
    private void rangeCheck(int index) {
        if (index < 0 || index >= size) {
            // index 是传递的索引 >= size 表示该索引已经越界
            ouOfBound(index);
        }
    }

    /**
     * 统一抛出异常
     *
     * @param index
     */
    private void ouOfBound(int index) {
        throw new IndexOutOfBoundsException("index:" + index + "size:" + size);
    }
  1. 检查 index的范围-添加元素时是允许 size == index :
  /**
     * 判断添加元素时的index 的范围
     *
     * @param index
     */
    private void rangeCheckForAdd(int index) {
        // 添加的时候可能是添加到了最后一个元素 所以是允许 index 等于 size的
        if (index < 0 || index > size) {
            // index 是传递的索引 >= size 表示该索引已经越界
            ouOfBound(index);
        }
    }

2.4 细节优化

使用泛型改写动态数组
@SuppressWarnings("all")
public class ArrayList<T> {

    /**
     * 记录的是数组的尺寸
     */
    private int size = 0;

    /**
     * 定义一个数组
     */
    private T[] elements;

    /**
     * 数组默认容量
     */
    private final static int DEFAULT_CAPACITY = 10;

    /**
     * 元素未找到的时候返回 -1
     */
    private final static int ELEMENT_NOT_FOUND = -1;


    /**
     * 无参构造调用有参构造
     */
    public ArrayList() {
        this(DEFAULT_CAPACITY);
    }

    /**
     * @param capacicy
     */
    public ArrayList(int capacicy) {
        // 判断当前传递的数组容量和默认的数组容量 如果传递容量小于默认的容量就使用默认的数组容量
        capacicy = capacicy < DEFAULT_CAPACITY ? DEFAULT_CAPACITY : capacicy;
        // 如果是 int 分配的是连续的四十个字节
        // 对象数组中存储的是对象的地址值 因为不同对象大占用的内存空间可能是不一样的
        // 对象不再不引用的时候就会被释放
        elements = (T[]) new Object[capacicy];
    }

    /**
     * 返回数组的尺寸
     *
     * @return
     */
    public int size() {
        return size;
    }

    /**
     * 判断数组是否为空
     *
     * @return
     */
    public boolean isEmpty() {
        // 如果size 等于 0 表示数组为空
        return size == 0;
    }

    /**
     * 判断数组中是否存在某个元素
     *
     * @param element
     * @return
     */
     public boolean contains(T element) {
        return indexOf(element) != ELEMENT_NOT_FOUND;
     }

    /**
     * 向数组末尾添加一个元素
     *
     * @param element
     */
    public void add(T element) {
        add(size, element);
    }

    /**
     * 向数组中指定位置添加某个元素
     *
     * @param index
     * @param element
     */
    public void add(int index, T element) {
        // 检查index的范围
        rangeCheckForAdd(index);

        // 判断当前的容量进行扩容操作看看后面一个位置
        ensureCapacity(size + 1);

        // 这里的移动必须是从后往前移
        for (int i = size - 1; i >= index; i--) {
            elements[i + 1] = elements[i];
        }
        elements[index] = element;
        size++;
    }

    /**
     * 根据 index 获取元素
     *
     * @param index
     * @return
     */
    public T get(int index) {
        rangeCheck(index);
        return elements[index];
    }

    /**
     * 将 index 位置的元素设置为  element 并将原来的元素返回
     *
     * @param index
     * @param element
     * @return
     */
    public T set(int index, T element) {
        rangeCheckForAdd(index);
        // 将旧元素事先保存起来
        T oldElement = elements[index];
        elements[index] = element;
        return oldElement;
    }

    /**
     * 移除 index 处的元素 并返回被移除的元素
     *
     * @param index
     * @return
     */
    public T remove(int index) {
        rangeCheck(index);
        // 记录需要删除的元素
        T oldElement = elements[index];
        for (int i = index + 1; i <= size - 1; i++) {
            elements[i - 1] = elements[i];
        }
        size--;
        // 最后一个位置的元素 清空
        elements[size] = null;
        return oldElement;
    }

    /**
     * 返回某个元素在数组中的位置
     *
     * @param element
     * @return
     */
    public int indexOf(T element) {
        for (int i = 0; i < size; i++) {
            if (elements.equals(elements)) {
                return i;
            }
        }
        return ELEMENT_NOT_FOUND;
    }


    /**
     * 清空数组
     * <p>
     * 现在泛型的形式 size = 0 已经不能这样写了
     * <p>
     * 不引用的对象就会被销毁
     */
    public void clear() {
        for (int i = 0; i < size; i++) {
            elements[i] = null; // 将 数组中 实例 与 每个对象断开
        }
        // elements = null; // 直接释放数组不可行 ,下次需要再 new 一个数组
        size = 0;
    }

    /**
     * 判断index的范围
     *
     * @param index
     */
    private void rangeCheck(int index) {
        if (index < 0 || index >= size) {
            outOfBound(index);
        }
    }

    private void outOfBound(int index) {
        throw new IndexOutOfBoundsException(" index : " + index + " size : " + size);
    }


    /**
     * 适用于添加方法
     *
     * @param index
     */
    private void rangeCheckForAdd(int index) {
        if (index < 0 || index > size) {
            outOfBound(index);
        }
    }

    /**
     * 保证数组的容量
     *
     * @param capacity
     */
    private void ensureCapacity(int capacity) {
        // 获取当前数组元素最大的容量
        int oldCapacity = elements.length;
        if (oldCapacity > capacity) {
            return;
        }
        // 否则将进行扩容 扩容为原来的 1.5倍 (oldCapacity >> 1) => (oldCapacity  / 2)
        int newCapacity = oldCapacity + (oldCapacity >> 1);
        // 使用新的容量创建一个新的数组
        T[] newElements = (T[]) new Object[newCapacity];
        // 将原数组中的元素移动到新的数组中
        for (int i = 0; i < size; i++) {
            newElements[i] = elements[i];
        }
        // 将原来的数组实例指向新的数组实例
        elements = newElements;
        System.out.println(oldCapacity + "扩容为:" + newCapacity);
    }

    @Override
    public String toString() {
        StringBuilder str = new StringBuilder();
        str.append("size = ").append(size).append(" [");
        for (int i = 0; i < size; i++) {
            if (i != 0) {
                str.append(", ");
            }
            str.append(elements[i]);
        }
        str.append("] ");
        return str.toString();
    }
}

创建数组时使用所有类的父类 Object 创建对象数组
 elements = (T[]) new Object[capacicy];
clear() 方法细节
对象数组
  1. clear() 方法进行优化 。因为当前数组中存储的是对象,在释放数组的资源的时候需要进行如下的处理,防止资源的浪费:
public void clear() {
        for (int i = 0; i < size; i++) {
            elements[i] = null; // 将 数组中 实例 与 每个对象断开
        }
        // elements = null; // 直接释放数组不可行 ,下次需要再 new 一个数组
        size = 0;
    }
remove()方法细节
  1. 在移除某个元素时,需要将元素从后往前移动,最后会保留最后一个 size - 1的位置存着一个之前的对象,此时需要将其进行释放:
public T remove(int index) {
        rangeCheck(index);
        // 记录需要删除的元素
        T oldElement = elements[index];
        for (int i = index + 1; i <= size - 1; i++) {
            elements[i - 1] = elements[i];
        }
        size--;
        // 最后一个位置的元素 清空
        elements[size] = null;
        return oldElement;
    }
数组动态扩容细节
  1. 当在添加元素的时候需要对数组进行动态的扩容操作:

首先传递的参数为,我们即将添加元素的位置。将此位置作为标准 和 数组当前的容量进行比较。
如果当前的数组容量 数组.length 是大于该位置所需容量的,将直接返回,否则将进行扩容操作。
扩容是将数组扩容为原来的 1.5 倍。之后创建一个新的数组对象,将原数组中的元素复制到新数组中。
将原数组的引用指向新的数组对象。

   /**
     * 保证数组的容量
     *
     * @param capacity
     */
    private void ensureCapacity(int capacity) {
        // 获取当前数组元素最大的容量
        int oldCapacity = elements.length;
        if (oldCapacity > capacity) {
            return;
        }
        // 否则将进行扩容 扩容为原来的 1.5倍 (oldCapacity >> 1) => (oldCapacity  / 2)
        int newCapacity = oldCapacity + (oldCapacity >> 1);
        // 使用新的容量创建一个新的数组
        T[] newElements = (T[]) new Object[newCapacity];
        // 将原数组中的元素移动到新的数组中
        for (int i = 0; i < size; i++) {
            newElements[i] = elements[i];
        }
        // 将原来的数组实例指向新的数组实例
        elements = newElements;
        System.out.println(oldCapacity + "扩容为:" + newCapacity);
    }
如何判断某个对象是否被回收
  1. 重写实体类中的 finalize方法,该方法在对象被回收之前会执行。
@Override
    protected void finalize() throws Throwable {
        super.finalize();
        System.out.println("对象被回收");
    }
  1. 但是对象不再被引用是JVM不会马上进行回收,如果需要进行立即回收可以使用如下代码 :
  // 提醒 JVM的垃圾回收机制 回收垃圾
  System.gc();
重写equals() 方法指定比较规则
  1. equals() 方法 默认比较的是对象的地址值。
/**
     * 重写 equals方法 指定比较规则
     * @param obj
     * @return
     */
    @Override
    public boolean equals(Object obj) {
        Person person = (Person) obj;
        return this.name == person.name && this.age == person.age;
    }
空值处理
  1. 在可以使用泛型的动态数组中可能会出现包含空值的情况,其实其中的 indexOf() 方法就需要进行处理。
   /**
     * 返回某个元素在数组中的位置,如果在数组中是允许存储空值的在判断的时候就需要注意对空值的处理
     *
     * @param element
     * @return
     */
    public int indexOf(T element) {
        if (element == null) {
            for (int i = 0; i < size; i++) {
                if (elements[i] == null) {
                    return i;
                }
            }
        } else {
            // 需要将 element 写在前面 elements[i] 可能会出现空值 null 如果不进行处理的话可能就会出现空指针异常
            for (int i = 0; i < size; i++) {
                if (element.equals(elements[i])) {
                    return i;
                }
            }
        }
        return ELEMENT_NOT_FOUND;
    }
优化 remove()方法中的循环条件
  1. 优化前 :
public T remove(int index) {
        rangeCheck(index);
        // 记录需要删除的元素
        T oldElement = elements[index];
        for (int i = index + 1; i <= size - 1; i++) {
            elements[i] = elements[i - 1];
        }
        size--;
        // 最后一个位置的元素 清空
        elements[size] = null;
        return oldElement;
    }
  1. 优化后:
   public T remove(int index) {
        rangeCheck(index);
        // 记录需要删除的元素
        T oldElement = elements[index];
        // 原来的循环条件 : int i = index + 1; i <= size - 1; i++
        for (int i = index; i < size; i++) {
            elements[i] = elements[i - 1];
        }
        size--;
        // 最后一个位置的元素 清空
        elements[size] = null;
        return oldElement;
    }
优化 add() 方法中的循环条件
  1. 优化前:
 public void add(int index, T element) {
        // 检查index的范围
        rangeCheckForAdd(index);

        // 判断当前的容量进行扩容操作看看后面一个位置
        ensureCapacity(size + 1);

        // 这里的移动必须是从后往前移  
        for (int i = size - 1; i >= index; i--) {
            elements[i] = elements[i - 1];
        }
        elements[index] = element;
        size++;
    }
  1. 优化后:
 public void add(int index, T element) {
        // 检查index的范围
        rangeCheckForAdd(index);

        // 判断当前的容量进行扩容操作看看后面一个位置
        ensureCapacity(size + 1);

        // 这里的移动必须是从后往前移  原来的循环条件 int i = size - 1; i >= index; i--
        // 原来的循环条件 int i = size - 1; i >= index; i--
        for (int i = size; i > index; i--) {
            elements[i] = elements[i - 1];
        }
        elements[index] = element;
        size++;
    }
优化Person对象中的equals()方法
  1. equals() 方法中添加对类型的检查,判断是否属于当前类型或其子类等。
@Override
    public boolean equals(Object obj) {
        // 为了严谨起见 需要进行以下修改
        if (obj == null) {
            return false;
        }
        // 判断其是否属于Person类型 ,如果是则将其进行强制类型转换这样才不会报错
        if (obj instanceof Person) {
            Person person = (Person) obj;
            return this.name == person.name && this.age == person.age;
        }
        return false;
    }
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,445评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,889评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,047评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,760评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,745评论 5 367
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,638评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,011评论 3 398
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,669评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,923评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,655评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,740评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,406评论 4 320
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,995评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,961评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,197评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,023评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,483评论 2 342

推荐阅读更多精彩内容