数据结构篇(1) - 5000字细嗦什么是数组!!!

数据结构概述

基本定义

数组是一种基础的线性数据结构,它由相同类型的元素组成,并存储在一段连续的内存空间中。每个元素可以通过其索引(下标)进行访问,索引通常从0开始。

核心特性

  • 连续内存:数组元素在内存中占据连续的空间,这使得数组支持高效的随机访问
  • 固定大小:数组在创建时需要指定大小,一旦创建,大小通常不可改变(静态数组)
  • 类型统一:数组中的所有元素必须具有相同的数据类型
  • 索引访问:通过索引可以在O(1)时间内直接访问任意位置的元素

应用场景

  1. 需要频繁随机访问元素的场景
  2. 数据元素数量固定且已知的场景
  3. 需要利用连续内存特性进行缓存优化的场景
  4. 实现其他复杂数据结构的基础(如实现栈、队列、矩阵等)

数组结构示意图

Array
+int[] data
+int size
+int length
+T get(int index)
+void set(int index, T value)
+int indexOf(T value)

数组内存布局

内存空间
索引0

元素A
索引1

元素B
索引2

元素C
索引3

元素D
索引4

元素E
基地址

0x1000

数组插入操作流程





开始插入操作
检查数组是否已满
抛出溢出异常
插入位置是否在尾部
直接添加到末尾
从后向前移动元素
腾出插入位置
结束

原理与核心概念

底层原理

数组的核心原理建立在连续内存分配的基础上。当创建一个数组时,操作系统会分配一块连续的内存空间,这块空间的大小等于数组容量乘以单个元素的大小。由于元素在内存中连续排列,我们可以通过基地址和元素大小的简单计算来定位任意元素:

地址计算公式元素地址 = 基地址 + 索引 × 元素大小

这种直接寻址方式使得数组具有极快的随机访问能力,无论访问数组中的哪个元素,所需时间都是相同的。

核心操作

操作 时间复杂度 空间复杂度 描述
随机访问 O(1) O(1) 通过索引直接访问元素
查找元素 O(n) O(1) 线性查找或二分查找
插入元素 O(n) O(1) 需要移动后续元素
删除元素 O(n) O(1) 需要移动后续元素
修改元素 O(1) O(1) 直接通过索引修改

关键细节

动态数组的扩容机制

静态数组的大小是固定的,而动态数组(如Python的list、Java的ArrayList)能够在需要时自动扩容。典型的扩容策略是:

  1. 当数组空间不足时,创建一个容量更大的新数组(通常是原容量的1.5倍或2倍)
  2. 将原数组的所有元素复制到新数组中
  3. 释放原数组的内存

虽然扩容操作的时间复杂度是O(n),但由于扩容不是频繁发生(摊还分析后),动态数组的插入操作的均摊时间复杂度仍为O(1)。

数组下标越界问题

访问数组时使用的索引必须在合法范围内(0到size-1)。越界访问可能导致:

  • 程序抛出IndexOutOfBoundsException异常
  • 访问到其他变量的内存空间,造成数据损坏
  • 潜在的安全漏洞(数组越界攻击)

代码实现

Java 实现

java 复制代码
public class Array<E> {
    private E[] data;
    private int size;

    @SuppressWarnings("unchecked")
    public Array(int capacity) {
        this.data = (E[]) new Object[capacity];
        this.size = 0;
    }

    public Array() {
        this(10);
    }

    public int getSize() {
        return size;
    }

    public int getCapacity() {
        return data.length;
    }

    public boolean isEmpty() {
        return size == 0;
    }

    public void addLast(E e) {
        add(size, e);
    }

    public void addFirst(E e) {
        add(0, e);
    }

    public void add(int index, E e) {
        if (index < 0 || index > size) {
            throw new IllegalArgumentException("Add failed. Require index >= 0 and index <= size.");
        }
        if (size == data.length) {
            resize(2 * data.length);
        }
        for (int i = size - 1; i >= index; i--) {
            data[i + 1] = data[i];
        }
        data[index] = e;
        size++;
    }

    public E get(int index) {
        if (index < 0 || index >= size) {
            throw new IllegalArgumentException("Get failed. Index is illegal.");
        }
        return data[index];
    }

    public void set(int index, E e) {
        if (index < 0 || index >= size) {
            throw new IllegalArgumentException("Set failed. Index is illegal.");
        }
        data[index] = e;
    }

    public boolean contains(E e) {
        for (int i = 0; i < size; i++) {
            if (data[i].equals(e)) {
                return true;
            }
        }
        return false;
    }

    public int find(E e) {
        for (int i = 0; i < size; i++) {
            if (data[i].equals(e)) {
                return i;
            }
        }
        return -1;
    }

    public E remove(int index) {
        if (index < 0 || index >= size) {
            throw new IllegalArgumentException("Remove failed. Index is illegal.");
        }
        E ret = data[index];
        for (int i = index + 1; i < size; i++) {
            data[i - 1] = data[i];
        }
        size--;
        data[size] = null;
        if (size == data.length / 4) {
            resize(data.length / 2);
        }
        return ret;
    }

    public E removeFirst() {
        return remove(0);
    }

    public E removeLast() {
        return remove(size - 1);
    }

    public void removeElement(E e) {
        int index = find(e);
        if (index != -1) {
            remove(index);
        }
    }

    @SuppressWarnings("unchecked")
    private void resize(int newCapacity) {
        E[] newData = (E[]) new Object[newCapacity];
        for (int i = 0; i < size; i++) {
            newData[i] = data[i];
        }
        data = newData;
    }

    @Override
    public String toString() {
        StringBuilder res = new StringBuilder();
        res.append(String.format("Array: size = %d, capacity = %d\n", size, data.length));
        res.append("[");
        for (int i = 0; i < size; i++) {
            res.append(data[i]);
            if (i != size - 1) {
                res.append(", ");
            }
        }
        res.append("]");
        return res.toString();
    }
}

复杂度分析

时间复杂度

操作 平均情况 最坏情况 最佳情况
随机访问 O(1) O(1) O(1)
查找(无序) O(n) O(n) O(1)
查找(有序) O(log n) O(log n) O(1)
尾部插入 O(1) O(n) O(1)
中间插入 O(n) O(n) O(n)
尾部删除 O(1) O(1) O(1)
中间删除 O(n) O(n) O(n)

空间复杂度

  • 最坏情况: O(n)
  • 平均情况: O(n)
  • 最好情况: O(1)(固定大小数组)

优化策略

  1. 空间预分配:在已知大致元素数量时,提前分配足够的空间,避免频繁扩容
  2. 缩容策略:当元素数量远小于容量时,适当缩减数组大小以节省内存
  3. 缓存友好:利用数组的连续内存特性,提高CPU缓存命中率
  4. 批量操作:对于需要插入或删除多个元素的操作,尽量合并进行,减少移动次数

应用实例

实例一:移动零 点击查看详情信息

问题描述 :

给定一个数组 nums,将数组中的所有 0 移动到数组的末尾,同时保持非零元素的相对顺序。

解决思路 :

使用双指针技术,一个指针用于遍历数组,另一个指针指向非零元素应该放置的位置。

代码实现:

java 复制代码
class Solution {
    public void moveZeroes(int[] nums) {
        if (nums == null || nums.length == 0) return;
        
        int left = 0;
        for (int right = 0; right < nums.length; right++) {
            if (nums[right] != 0) {
                nums[left] = nums[right];
                left++;
            }
        }
        while (left < nums.length) {
            nums[left++] = 0;
        }
    }
}

总结与思考

核心知识点

  1. 数组的连续内存特性决定了其O(1)的随机访问能力
  2. 数组的插入和删除操作需要移动元素,时间复杂度为O(n)
  3. 动态数组通过扩容机制实现了灵活性,但需要考虑均摊复杂度
  4. 双指针技巧是数组相关问题的重要解题工具

优缺点

优点:

  • 支持O(1)时间的随机访问,查询效率高
  • 内存连续,对CPU缓存友好,访问速度快
  • 实现简单直观,是其他数据结构的基础

缺点:

  • 插入和删除操作需要移动元素,效率较低
  • 静态数组大小固定,无法动态调整
  • 需要连续的内存空间,可能导致内存碎片问题

与其他数据结构的对比

特性 数组 链表 哈希表
随机访问 O(1) O(n) O(1)平均
插入/删除(头部) O(n) O(1) O(1)
插入/删除(尾部) O(1) O(1) O(1)
内存连续性
内存开销

*注:动态数组的尾部插入均摊复杂度为O(1)

学习心得

数组作为最基础的数据结构,是学习其他数据结构的起点。理解数组的原理对于掌握时间复杂度分析、内存管理等内容至关重要。在实际开发中,虽然很少直接使用原生数组,但动态数组(List、ArrayList)的使用非常广泛,它们的底层原理与数组一致。

学习数组时,建议重点关注:

  • 理解连续内存分配带来的优势和问题
  • 掌握双指针技巧在各种问题中的应用
  • 理解动态数组的扩容机制和均摊分析

其他题目

相关推荐
这就是佬们吗9 小时前
Windows 的 CMD 网络环境:解决终端无法联网与更新的终极指南
java·windows·git·python·spring·maven
yuanmenghao9 小时前
自动驾驶中间件iceoryx - 同步与通知机制(一)
开发语言·网络·驱动开发·中间件·自动驾驶
企鹅会滑雪9 小时前
【无标题】
开发语言·python
幻云20109 小时前
Next.js 之道:从全栈思维到架构实战
开发语言·javascript·架构
阿豪学编程9 小时前
【Linux】线程同步和线程互斥
linux·开发语言
寻星探路9 小时前
【深度长文】深入理解网络原理:TCP/IP 协议栈核心实战与性能调优
java·网络·人工智能·python·网络协议·tcp/ip·ai
轻竹办公PPT9 小时前
实测多款 AI:2026 年工作计划 PPT 哪种更好修改
人工智能·python·powerpoint
旦沐已成舟9 小时前
Django的学习之路~
python·django
LightYoungLee9 小时前
算法(五)树 Trees V2
学习·算法·深度优先