首页 > 其他分享 >【数据结构】之数组详解

【数据结构】之数组详解

时间:2024-10-17 17:47:59浏览次数:3  
标签:遍历 int 元素 System 索引 详解 数组 数据结构

数组是数据结构中最基础的概念之一,它在编程中被广泛应用。理解数组的工作原理、操作方式以及底层实现,对于我们构建更复杂的数据结构和算法至关重要。本文将从多个角度深入剖析数组,并以Java语言示例进行讲解,帮助你建立对数组的深刻理解。

一、什么是数组?

数组是一种线性数据结构,用于存储相同数据类型的元素序列。我们可以把数组想象成一排连续的储物柜,每个柜子都有一个唯一的编号(索引),用于存放物品。

a89cd80846c1462dafd2532c240c5a0e.png

书面解释:数组是由一组元素(值或变量)组成的数据结构,每个元素有至少一个索引或键来标识

二、数组的特点

  1. 元素类型相同: 数组中所有元素必须是相同数据类型,例如int、double、String等。

  2. 内存空间连续: 数组的所有元素在内存中是连续存储的,这使得访问元素非常高效。

  3. 索引访问: 每个元素都有一个唯一的索引,从0开始,可以通过索引快速访问元素。

  4. 固定大小: 数组一旦创建,其大小就固定了,不能动态扩展。

三、数组的底层实现

Java 中数组结构为

  • 8 字节 markword

  • 4 字节 class 指针(压缩 class 指针的情况)

  • 4 字节 数组大小(决定了数组最大容量是 $2^{32}$)

  • 数组元素 + 对齐字节(java 中所有对象大小都是 8 字节的整数倍12,不足的要用对齐字节补足)

例如

int[] array = {1, 2, 3, 4, 5};

的大小为 40 个字节,组成如下

8 + 4 + 4 + 5*4 + 4(alignment)   =  40
                         |
                    表示补全的

四、Java中创建和初始化数组

在Java中,可以使用以下语法创建数组:

// 创建一个长度为5的int数组
int[] numbers = new int[5];

// 创建一个长度为3的String数组并初始化
String[] names = {"Alice", "Bob", "Charlie"};

 

五、数组的基本操作

访问元素: 通过索引访问数组元素。

int firstNumber = numbers[0]; 
System.out.println("第一个元素是:" + firstNumber);

修改元素: 通过索引修改数组元素。

names[1] = "David"; 
System.out.println("第二个名字现在是:" + names[1]);

遍历数组: 使用循环结构遍历数组中的所有元素。

for (int i = 0; i < numbers.length; i++) {
  System.out.println("元素 " + i + ": " + numbers[i]);
}

获取数组长度: 使用length属性获取数组的长度。

int arrayLength = names.length;
System.out.println("数组长度:" + arrayLength);

 

六、数组的常见应用

  1. 存储数据: 存储一组相关的数据,例如学生成绩、商品价格等。

  2. 实现其他数据结构: 作为其他数据结构的底层实现,例如栈、队列等。

  3. 算法: 很多算法都依赖于数组,例如排序算法、查找算法等。

七、数组的局限性

  1. 插入和删除元素效率低: 在数组中间插入或删除元素需要移动后续元素,效率较低。

  2. 大小固定: 数组的大小在创建时就固定了,不能动态调整。

八、动态数组实现

为了克服数组大小固定的限制,我们可以实现动态数组,即在需要时自动扩容的数组。以下是用Java实现的动态数组类:

public class DynamicArray<T> {
    private static final int DEFAULT_CAPACITY = 10; // 默认容量
    private T[] data; // 存储元素的数组
    private int size; // 当前元素数量

    // 无参构造函数,使用默认容量
    public DynamicArray() {
        this(DEFAULT_CAPACITY);
    }

    // 有参构造函数,指定容量
    public DynamicArray(int capacity) {
        if (capacity < 0) {
            throw new IllegalArgumentException("Capacity cannot be negative: " + capacity);
        }
        data = (T[]) new Object[capacity]; // 创建指定容量的数组
        size = 0; // 初始化元素数量为0
    }

    // 获取数组大小
    public int size() {
        return size;
    }

    // 判断数组是否为空
    public boolean isEmpty() {
        return size == 0;
    }

    // 通过索引获取元素
    public T get(int index) {
        checkIndex(index); // 检查索引是否合法
        return data[index]; // 返回指定索引的元素
    }

    // 通过索引设置元素
    public void set(int index, T value) {
        checkIndex(index); // 检查索引是否合法
        data[index] = value; // 将指定索引的元素设置为新的值
    }

    // 在数组末尾添加元素
    public void add(T value) {
        if (size == data.length) { // 如果数组已满
            resize(data.length * 2); // 扩容至两倍
        }
        data[size++] = value; // 在数组末尾添加元素,并更新元素数量
    }

    // 扩容
    private void resize(int newCapacity) {
        T[] newData = (T[]) new Object[newCapacity]; // 创建新的数组
        System.arraycopy(data, 0, newData, 0, size); // 将旧数组元素复制到新数组
        data = newData; // 将新数组赋值给data
    }

    // 检查索引是否合法
    private void checkIndex(int index) {
        if (index < 0 || index >= size) {
            throw new IndexOutOfBoundsException("Index: " + index + ", Size: " + size);
        }
    }

    // 使用for循环遍历数组
    public void printAll() {
        for (int i = 0; i < size; i++) {
            System.out.print(data[i] + " ");
        }
        System.out.println();
    }

    // 使用foreach循环遍历数组
    public void forEachPrint() {
        for (T element : data) {
            if (element != null) { // 避免打印空元素
                System.out.print(element + " ");
            }
        }
        System.out.println();
    }
}

 

九、数组操作的时间复杂度

操作时间复杂度说明
访问元素O(1)通过索引直接访问,时间复杂度为常数级。
修改元素O(1)通过索引直接修改,时间复杂度为常数级。
在末尾添加元素O(1)在数组末尾添加元素,时间复杂度为常数级。
在中间插入元素O(n)需要移动后续元素,时间复杂度为线性级,n为数组长度。
删除元素O(n)需要移动后续元素,时间复杂度为线性级,n为数组长度。
数组扩容O(n)需要复制所有元素到新的数组,时间复杂度为线性级,n为数组长度。

十、二维数组详解

二维数组本质上是一个数组,其中每个元素又是一个数组。可以将二维数组想象成一个表格,每个元素都有对应的行索引和列索引。

// 创建一个3行5列的二维数组
int[][] matrix = {
    {1, 2, 3, 4, 5},
    {3, 2, 1, 2, 1},
    {3, 4, 5, 8, 8},
};

// 遍历二维数组
for (int i = 0; i < matrix.length; i++) {
  for (int j = 0; j < matrix[i].length; j++) {
    System.out.print(matrix[i][j] + " ");
  }
  System.out.println();
}

图解如下 : 

8ebb2c612e444a4486bcaacc5121a545.png

十一、利用局部性原理和代码解释二维数组的ij遍历为什么比ji遍历快

在实际应用中,我们通常需要遍历二维数组的所有元素。遍历方式对性能的影响很大,其中 i 行 j 列遍历(先遍历行,再遍历列)的效率通常高于 j 列 i 行遍历(先遍历列,再遍历行)。

这是因为 CPU 会将频繁访问的数据加载到缓存中,以便更快地访问。ij 遍历方式访问内存地址是连续的,更符合局部性原理,CPU 缓存命中率更高,从而提高了访问效率。(缓存的最小缓存单元是缓存行,从而解释了ij遍历方式更快)

扩展:空间局部性的概念

  • cpu 读取内存(速度慢)数据后,会将其放入高速缓存(速度快)当中,如果后来的计算再用到此数据,在缓存中能读到的话,就不必读内存了

  • 缓存的最小存储单位是缓存行(cache line),一般是 64 bytes,一次读的数据少了不划算啊,因此最少读 64 bytes 填满一个缓存行,因此读入某个数据时也会读取其临近的数据,这就是所谓空间局部性

以下代码演示了两种遍历方式的性能差异:

public class ArrayTraversal {
    public static void main(String[] args) {
        int[][] matrix = new int[10000][10000];
        long startTime, endTime;

        // ij 遍历
        startTime = System.nanoTime();
        for (int i = 0; i < matrix.length; i++) {
            for (int j = 0; j < matrix[i].length; j++) {
                matrix[i][j]++;
            }
        }
        endTime = System.nanoTime();
        System.out.println("ij遍历时间:" + (endTime - startTime) / 1000000 + "ms");

        // ji 遍历
        startTime = System.nanoTime();
        for (int j = 0; j < matrix[0].length; j++) {
            for (int i = 0; i < matrix.length; i++) {
                matrix[i][j]++;
            }
        }
        endTime = System.nanoTime();
        System.out.println("ji遍历时间:" + (endTime - startTime) / 1000000 + "ms");
    }
}

运行结果会显示 ij 遍历的时间明显少于 ji 遍历。


@@利用局部性原理解释如下@@

外i内j:缓存一次读取一行,先命中1,然后2,然后3......这样按行命中,速度极快

ac5907c1f2104649a344ca762b79cda6.png

外j内i:缓存也是每次读取一行,但是只命中一个,又读取新的一行,还是命中一个,按列命中,依次类推,效率极低,如果不能充分利用缓存的数据,就会造成效率低下

81e9239b7cd545efbef8a7755db7d41f.png

十二、什么是数组的越界?

数组越界是指访问数组时,索引超出了数组的有效范围。在Java中,如果访问数组时索引小于0或者大于等于数组长度,就会抛出 IndexOutOfBoundsException 异常。

int[] numbers = {1, 2, 3};

// 索引越界
int error = numbers[3]; // 抛出 ArrayIndexOutOfBoundsException

为了避免数组越界,需要在访问数组时,始终检查索引是否在有效范围内。

总结

数组是数据结构的重要组成部分,理解其原理和操作特性对于学习更复杂的数据结构和算法至关重要。本文从多个维度深入解析了数组,并结合Java代码示例进行讲解,希望能帮助各位看官更好地掌握数组。下期见,谢谢~

 

标签:遍历,int,元素,System,索引,详解,数组,数据结构
From: https://blog.csdn.net/weixin_64178283/article/details/143018086

相关文章

  • Topk问题与堆排序(Java数据结构)
    前言:    接触完堆之后,也逐渐对堆了如指掌,最后再来讨论一下两个问题。    有如下场景:    1、全国有几千所大学,我如何能够快速找出排名前10的大学?    2、我如何对这10所大学排好序?    为了用堆解决问题,接下来我们就来一起学习Top......
  • Java数据结构二叉树面试题精华(画图详解)
    前言:    针对二叉树,因为涉及到递归,需要跟多的练习强化递归的思想,其中也包括需要画图理解一些想不通的问题来提升自己!    一下面这些题为例,一起来提升自己的逻辑思维能力!(可能其中一些题已经写过,但是希望能再写一遍有助于提高代码能力)相同的树:      ......
  • Witness组件详解
    Witness组件详解 ......
  • Nuxt.js 应用中的 app:resolve 事件钩子详解
    title:Nuxt.js应用中的app:resolve事件钩子详解date:2024/10/17updated:2024/10/17author:cmdragonexcerpt:app:resolve是Nuxt.js中的生命周期钩子,在解析app实例后调用。这个钩子允许开发者在应用完全初始化后执行一些自定义操作,比如注册插件、设置中间件或进......
  • 【Docker系列】docker-compose down 命令详解
    ......
  • 实验三: JavaScript数组与函数
    实验目的熟练掌握常用JavsScript的数组、自定义函数、作用域。实验内容数组定义及元素获取;数组的遍历;数组内容的增删改查;数组的排序;数组的反转、截取、合并、元素拼接函数的声明;函数的调用;匿名函数;作用域。实验步骤:数组定义及元素获取;数组的遍历;数组内容的增删改查......
  • C语言[数组作函数参数]
    输入10个整数作为一个数组,要求判断并且输出其中最大的值和它是数组中的第几位数。本次代码调用max函数数组元素为a[1]~a[9]代码如下:#include<stdio.h>intmain(){  intmax(intx,inty);  inti,m,n,a[10];  printf("enter10intergernumber:"); ......
  • HttpUtils 详解
    一、详解1.1介绍现如今的Web项目,由服务端向外发起网络请求的场景,基本上随处可见!传统情况下,在服务端代码里访问http服务时,一般会使用JDK的HttpURLConnection或者Apache的HttpClient,不过这种方法使用起来太过繁琐,而且api使用起来非常的复杂,还得操心资源回收。1.2......
  • 代码随想录算法训练营第二天|209长度最小的子数组、59螺旋矩阵
    1leetcode209长度最小的子数组题目链接:209.长度最小的子数组文章链接:代码随想录(programmercarl.com)视频链接:拿下滑动窗口!|LeetCode209长度最小的子数组思路:没有思路,看到这道题有一种想立马退出的感觉,无从下手1.1暴力搜索1.1.1python版本这个版本的新知识就是定义......
  • 解析华为鸿蒙next:Web组件自适应布局与渲染模式详解
    本文旨在深入探讨华为鸿蒙HarmonyOSNext系统(截止目前API12)的技术细节,基于实际开发实践进行总结。主要作为技术分享与交流载体,难免错漏,欢迎各位同仁提出宝贵意见和问题,以便共同进步。本文为原创内容,任何形式的转载必须注明出处及原作者。Web组件大小自适应页面内容布局使用......