数据结构和算法之【数组】

目录

认识数组

数组的定义

根据索引计算元素的地址

Java中数组的结构

组成部分

示例

动态数组

自实现一个动态数组

编码

测试

Java中的动态数组

底层存储方式

随机添加或删除元素

扩容机制

获取元素

判断是否包含某元素

LeetCode-88题

题解

测试


认识数组

数组的定义

计算机科学中,数组是由一组元素组成的数据结构,每个元素都有索引来标识

根据索引计算元素的地址

数组的存储规则是连续存储元素的,所以数组中元素的地址可以通过其索引计算出来。

如:已知数组的数据起始地址BaseAddress,可以由公式:BaseAddress + (index * size) 来计算索引为index的元素地址

  • index为索引,在Java、C语言中都是从0开始
  • size是每个元素占用的字节,如int占4字节,double占8字节

通过索引查找元素的时间复杂度为O(1),不会受到数据规模的影响

Java中数组的结构

组成部分

  • 8字节的markword
  • 4字节的类型指针(指针压缩的情况下)
  • 4字节的数组长度
  • 数组元素大小
  • 对齐填充(不足8的整数倍时补充至8的整数倍)

示例

一、需求:定义一个数组,计算其所占空间大小

java 复制代码
// 定义一个数组array,计算array所占用的空间大小
int[] array = {1,2,3,4,5};

二、理论:数组array所占用的空间大小为40byte

  1. markword:8byte
  2. 类型指针:4byte
  3. 数组长度:4byte
  4. 数组元素大小:5 * 4 = 20byte (int类型元素占用4byte大小)
  5. 对齐填充:4byte

三、证明:通过编程来求证数组array所占用的空间大小为:40byte

  • 添加依赖
XML 复制代码
<dependency>
    <groupId>org.openjdk.jol</groupId>
    <artifactId>jol-core</artifactId>
    <version>0.9</version>
</dependency>
  • 编码
java 复制代码
package algorithm.array;

import org.openjdk.jol.info.ClassLayout;

public class ArrayStructure {
    public static void main(String[] args) {
        // 定义一个数组array
        int[] array = {1, 2, 3, 4, 5};
        // 查看数组array所占用的空间大小
        System.out.println(ClassLayout.parseInstance(array).toPrintable());
    }
}
  • 查看测试结果

动态数组

自实现一个动态数组

编码

java 复制代码
package algorithm.array;

import java.util.Arrays;
import java.util.Iterator;
import java.util.function.Consumer;

/**
 * 实现一个动态数组
 */
public class DynamicArray implements Iterable<Object> {
    // 存储容器:数组
    private Object[] container;
    // 容器中有效元素个数
    private int size;
    // 默认初始化容量
    private static final int DEFAULT_INIT_CAPACITY = 10;
    // 空数组
    private static final Object[] EMPTY_ARRAY = {};

    /**
     * 无参构造方法:将容器设置为空数组
     */
    public DynamicArray() {
        container = EMPTY_ARRAY;
    }

    /**
     * 可以指定容量的构造方法
     */
    public DynamicArray(int initCapacity) {
        if (initCapacity < 0) {
            throw new IllegalArgumentException("指定容量不合法");
        }
        container = new Object[initCapacity];
    }

    /**
     * 获取元素(通过下标访问元素)
     * 时间复杂度:O(1)
     */
    public Object get(int index) {
        if (index < 0 || index >= size) {
            throw new ArrayIndexOutOfBoundsException("不合法的下标");
        }
        return container[index];
    }

    /**
     * 顺序添加元素
     * 非每次添加元素都会扩容,因此不考虑扩容带来的时间开销,时间复杂度:O(1)
     */
    public boolean add(Object element) {
        return add(size, element);
    }

    /**
     * 随机添加元素
     * 涉及元素的移动,时间复杂度:O(n)
     */
    public boolean add(int index, Object element) {
        if (index < 0 || index > size) {
            throw new ArrayIndexOutOfBoundsException("不合法的下标");
        }
        // 扩容(包含判断是否要进行扩容的逻辑)
        grow(size + 1);
        if (index < size) {
            // 非顺序添加,需要移动元素
            System.arraycopy(container, index, container, (index + 1), (size - index));
        }
        container[index] = element;
        size++;
        return true;
    }

    /**
     * 数组扩容
     */
    private void grow(int minCapacity) {
        // 此时若容器为空数组,则创建默认容量大小的数组
        if (container == EMPTY_ARRAY) {
            container = new Object[DEFAULT_INIT_CAPACITY];
            return;
        }
        // 若此时无需扩容
        if (container.length >= minCapacity) {
            return;
        }
        // 需要扩容
        int oldCapacity = container.length;
        // 扩容为原来的1.5倍大小
        int newCapacity = oldCapacity + (oldCapacity >> 1);
        // 保证扩容达到最小需要的容量
        newCapacity = Math.max(newCapacity, minCapacity);
        // 创建新容器,并赋值
        container = Arrays.copyOf(container, newCapacity);
    }

    /**
     * 随机删除元素
     * 涉及元素的移动,时间复杂度:O(n)
     */
    public boolean remove(int index) {
        if (index < 0 || index >= size) {
            throw new ArrayIndexOutOfBoundsException("不合法的下标");
        }
        if (index == size - 1) {
            // 删除最后一个元素
            container[index] = null;// help gc
        } else {
            // 不是删除最后一个元素(非顺序删除),需要移动元素
            System.arraycopy(container, (index + 1), container, index, (size - index - 1));
            container[size - 1] = null;// help gc
        }
        size--;
        return true;
    }

    /**
     * 是否为空
     */
    public boolean isEmpty() {
        return size == 0;
    }

    /**
     * 元素个数
     */
    public int size() {
        return size;
    }

    /**
     * 使动态数组具有增强for的能力
     */
    @Override
    public Iterator<Object> iterator() {
        return new Iterator<Object>() {
            private int p;

            @Override
            public boolean hasNext() {
                return p < size;
            }

            @Override
            public Object next() {
                return container[p++];
            }
        };
    }

    /**
     * 遍历容器中所有元素
     */
    public void foreach(Consumer<Object> consumer) {
        for (int i = 0; i < size; i++) {
            consumer.accept(container[i]);
        }
    }
}

测试

  • 添加元素、删除元素、遍历
java 复制代码
package algorithm.array;

public class DynamicArrayTest {
    public static void main(String[] args) {
        // 实例化动态数组
        DynamicArray dynamicArray = new DynamicArray();
        // 查看动态数组
        watchDynamicArray(dynamicArray);

        // 添加元素
        dynamicArray.add("111");
        dynamicArray.add("aaa");
        dynamicArray.add("222");
        dynamicArray.add("bbb");
        dynamicArray.add(2, "333");
        dynamicArray.add(0, "000");
        dynamicArray.add(6, "ccc");

        // 查看动态数组
        watchDynamicArray(dynamicArray);

        // 删除元素
        dynamicArray.remove(0);
        dynamicArray.remove(5);
        dynamicArray.remove(3);

        // 查看动态数组
        watchDynamicArray(dynamicArray);
    }

    /**
     * 查看动态数组的情况
     */
    public static void watchDynamicArray(DynamicArray dynamicArray) {
        // 是否为空
        System.out.println("是否为空:" + dynamicArray.isEmpty());
        // 元素个数
        System.out.println("元素个数:" + dynamicArray.size());
        // 遍历
        System.out.println("遍历方式一:");
        for (Object item : dynamicArray) {
            System.out.println(item);
        }
        System.out.println("遍历方式二:");
        dynamicArray.foreach(System.out::println);
        System.out.println("----------------------------------");
    }
}
bash 复制代码
是否为空:true
元素个数:0
遍历方式一:
遍历方式二:
----------------------------------
是否为空:false
元素个数:7
遍历方式一:
000
111
aaa
333
222
bbb
ccc
遍历方式二:
000
111
aaa
333
222
bbb
ccc
----------------------------------
是否为空:false
元素个数:4
遍历方式一:
111
aaa
333
bbb
遍历方式二:
111
aaa
333
bbb
----------------------------------
  • 数组的扩容
java 复制代码
package algorithm.array;

public class DynamicArrayTest {
    public static void main(String[] args) {
        // 实例化动态数组
        DynamicArray dynamicArray = new DynamicArray();
        // 添加10个元素,到达临界值
        for (int i = 0; i < 10; i++) {
            dynamicArray.add(i);
        }
        // 查看动态数组
        watchDynamicArray(dynamicArray);

        // 继续添加元素,数组发生扩容
        dynamicArray.add("b");
        dynamicArray.add(0, "a");
        // 查看动态数组
        watchDynamicArray(dynamicArray);
    }

    /**
     * 查看动态数组的情况
     */
    public static void watchDynamicArray(DynamicArray dynamicArray) {
        // 是否为空
        System.out.println("是否为空:" + dynamicArray.isEmpty());
        // 元素个数
        System.out.println("元素个数:" + dynamicArray.size());
        // 遍历
        System.out.println("遍历数组中的元素:");
        dynamicArray.foreach(item -> System.out.print(item + "\t"));
        System.out.println();
        System.out.println("----------------------------------------------");
    }
}
bash 复制代码
是否为空:false
元素个数:10
遍历数组中的元素:
0	1	2	3	4	5	6	7	8	9	
----------------------------------------------
是否为空:false
元素个数:12
遍历数组中的元素:
a	0	1	2	3	4	5	6	7	8	9	b	
----------------------------------------------

Java中的动态数组

在Java中提供了动态数组:ArrayList

底层存储方式

java 复制代码
package java.util;

// ...

public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
    // ...

    // 存储容器:使用数组存储元素
    transient Object[] elementData;

    // 默认初始化容量
    private static final int DEFAULT_CAPACITY = 10;

    // 定义一个空数组
    private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

    // 记录数组中有效的元素个数
    private int size;
    

    // 使用无参构造方法实例化ArrayList时,存储容器为空数组
    // 在第一次添加元素时,创建长度为10的数组,体现懒加载思想
    public ArrayList() {
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }

    // 顺序添加元素
    // 这里说下,顺序添加元素的时间复杂度是O(1),并非每次添加元素都会扩容
    public boolean add(E e) {
        ensureCapacityInternal(size + 1); 
        // ...
    }

    private void ensureCapacityInternal(int minCapacity) {
        ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
    }

    private static int calculateCapacity(Object[] elementData, int minCapacity) {
        // 判断是否为第一次添加元素(无参构造方法实例化时将存储容器赋值为DEFAULTCAPACITY_EMPTY_ELEMENTDATA)
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
            // DEFAULT_CAPACITY=10,通过无参构造方法实例化且第一次添加元素时,这里返回10
            return Math.max(DEFAULT_CAPACITY, minCapacity);
        }
        return minCapacity;
    }

    private void ensureExplicitCapacity(int minCapacity) {
        // ...
            
            grow(minCapacity);
    }

    private void grow(int minCapacity) {
        // ...
            
            // 通过无参构造方法实例化且第一次添加元素时,minCapacity=10
            newCapacity = minCapacity;

        // ...

        // 通过无参构造方法实例化且第一次添加元素时,创建长度为10的数组容器
        elementData = Arrays.copyOf(elementData, newCapacity);
    }

    // ...
}

随机添加或删除元素

数组存储元素的特点是连续的,对于随机增删元素,需要进行元素的移动,时间复杂度为O(n)

java 复制代码
// 随机添加元素
public void add(int index, E element) {
    // ...
    
    // 移动容器中的元素
    System.arraycopy(elementData, index, elementData, index + 1,
            size - index);
    elementData[index] = element;
    size++;
}

// 随机删除元素
public E remove(int index) {
    // ...
        
        // 移动容器中的元素
        System.arraycopy(elementData, index+1, elementData, index,
                numMoved);
    elementData[--size] = null; // clear to let GC do its work
    return oldValue;
}

扩容机制

ArrayList的扩容机制体现在它的grow方法中

java 复制代码
 private void grow(int minCapacity) {
     // 旧容量
     int oldCapacity = elementData.length;
     // 新容量 = 旧容量的1.5倍大小
     int newCapacity = oldCapacity + (oldCapacity >> 1);
     // ...
     // 扩容为原来的1.5倍
     elementData = Arrays.copyOf(elementData, newCapacity);
 }

获取元素

通过下标获取元素,性能高,时间复杂度为O(1)

java 复制代码
// 通过下标获取元素
public E get(int index) {
    // ...

    return elementData(index);
}

E elementData(int index) {
    return (E) elementData[index];
}

判断是否包含某元素

底层使用的是equals方法

java 复制代码
// 判断是否包含元素o
public boolean contains(Object o) {
    return indexOf(o) >= 0;
}

public int indexOf(Object o) {
    // ...
        for (int i = 0; i < size; i++)
            // 使用equals方法进行判断
            if (o.equals(elementData[i]))
                return i;
    }
    return -1;
}

LeetCode-88题

题解

java 复制代码
class Solution {
    /**
     * 合并两个有序数组
     * 这里使用的是双指针的方法来解题
     */
    public void merge(int[] nums1, int m, int[] nums2, int n) {
        if (m == 0) {
            System.arraycopy(nums2, 0, nums1, 0, n);
            return;
        }
        if (n == 0) {
            return;
        }
        int[] help = new int[m + n];
        // 定义两个指针
        int p1 = 0;
        int p2 = 0;
        int i = 0;

        while (p1 < m && p2 < n) {
            help[i++] = nums1[p1] < nums2[p2] ? nums1[p1++] : nums2[p2++];
        }
        while (p1 < m) {
            help[i++] = nums1[p1++];
        }
        while (p2 < n) {
            help[i++] = nums2[p2++];
        }

        // 放入数组nums1中
        System.arraycopy(help, 0, nums1, 0, (m + n));
    }
}

测试

相关推荐
想不明白的过度思考者1 小时前
【MyBatis 知识点解析】#{} 与 ${} 的区别及 SQL 注入实战演示
java·数据库·spring boot·sql·mybatis
惊讶的猫1 小时前
maven介绍_1
java·maven
小钻风33661 小时前
Java函数式编程-lambda表达式
java·开发语言·python
承渊政道2 小时前
C++学习之旅【⽤哈希表封装myunordered_map和myunordered_set以及位图和布隆过滤器介绍】
数据结构·c++·学习·哈希算法·散列表·hash-index·图搜索算法
0 0 02 小时前
CCF-CSP 37-4集体锻炼【C++】考点:数学(最大公因数gcd特性),常数优化
开发语言·c++·算法
Han.miracle2 小时前
Spring IoC 与 DI 思想及实践详解
java
程序员小明儿2 小时前
量子计算探秘:从零开始的量子编程与算法之旅 · 第三篇
算法·量子计算
开源盛世!!2 小时前
3.9-3.11学习笔记
数据结构·算法
Irissgwe2 小时前
基础I/O
java·linux·前端