美文网首页安卓面试数据结构与算法
【数据结构与算法】 01 - 动态数组

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

作者: itlu | 来源:发表于2020-12-06 18:11 被阅读0次

    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;
        }
    

    相关文章

      网友评论

        本文标题:【数据结构与算法】 01 - 动态数组

        本文链接:https://www.haomeiwen.com/subject/jvvmwktx.html