数据结构(一)数组

2023-10-30

概述

说起数组我们都不陌生,几乎在每一种编程语言中,基本上都会有数组这种数据类型。不仅如此它还是是最基础最简单的数据结构。尽管如此,可能还是有一些人并没有真正的理解这个基础数据结构的精髓所在。

首先,我们都知道,在java中数组是从 0 开始编号的,但是为什么数组要从 0 开始编号,而不是从 1 开始呢?从 1 开始不是更加的符合人类的思维方式吗?

定义

数组(Array)是一种线性表数据结构。它用一组连续的内存空间,来存储一组具有相同类型的数据。

首先,定义中有几个关键词,需要我们关注一下。理解了这几个关键词,有助于我们彻底理解数组这个数据结构。

第一就是线性表。线性表就是数据排成像一条线一样的结构。每个线性表上的数据最多只有前和后两个方向。其实除了数组,链表、队列、栈等也是线性表结构。

而与它相对应的概念就是非线性表,比如二叉树、堆、图等。之所以称为非线性,是因为,在非线性表中,数据之间并不是简单的前后关系。

第二个关键词是连续的内存空间和相同类型的数据。正是因为这两个限制,它才有了一个堪称 “杀手锏” 的特性:“随机访问”。但有利就有弊,这两个限制也让数组的很多操作变得效率很低,比如想要在数组中删除、插入一个数据,为了保证物理空间的连续性,就需要做大量的数据搬移的工作。

说到数据的访问,我们需要思考一下数组是如何实现根据下标随机访问数组元素的?

我们拿一个长度为 10 的 int 型的数组 int[] a = new int[10] 来举例。在下图中,计算机系统给数组 a[10],分配了一块连续内存空间 1000 ~ 1039,其中,内存块的首地址为 base_address = 1000。

 我们知道,计算机会给每个内存单元分配一个地址,计算机通过地址来访问内存中的数据。当计算机需要随机访问数组中的某个元素是,它会首先通过下面的寻址公式,计算出该元素存储的内存地址:

a[i]_address = base_address + i * data_type_size

其中,data_type_size 表示数组中每个元素的大小。因为数组 a 中存储的是 int 类型的数据,所以 data_type_size就为 4 个字节。

提到数组和链表的区别,很多人都会说,“链表适合插入、删除,时间复杂度为 O(1);数组适合查找,查找时间复杂度为 O(1)”。

实际上,这种表述是不准确的。数组是适合查找操作,但是查找的时间复杂度并不是 O(1)。即便是排好序的数组,用二分查找,时间复杂度也是 O(logn)。所以,正确的表述应该是,数组支持随机访问,根据下标随机访问的时间复杂度为 O(1)。

数组中低效的 “插入” 和 “删除”

数组为了保持内存数据的连续性,会导致插入、删除这两个操作比较低效。原因如下:

插入操作

假设数组的长度为 n ,现在,如果我们需要将一个数据插入到数组中的第 k 个位置。为了把第 k 个位置腾出来,给新来的数据,我们需要将第 k ~ n 这部分的元素都顺序地往后挪一位。那插入操作的时间复杂度是多少呢?我们可以分析一下。

如果在数组的末尾插入元素,那就不需要移动数据了,这时的时间复杂度为 O(1)。但如果在数组的开头插入元素,那所有的数据都需要依次往后移动一位,所以最坏的时间复杂度是 O(n)。因为我们每个位置插入元素的概率是一样的,所以平均情况时间复杂度为 (1+2+3+4+....+n) / n = O(n)。

如果数组中的数据是有序的,我们在某个位置插入一个新的元素时,就必要按照刚才的方法搬移 k 之后的数据。但是,如果数组中存储的数据并没有任何规律,数组只是被当作一个存储数据的集合。在这种情况下,如果要将某个数据插入到第 k 个位置,为了避免大规模的数据搬移,我们还有一个简单的办法就是,直接将第 k 位的数据搬移到数组元素的最后,把新的元素直接放入到第 k 个位置。

我们举例如下:假设数组 a[10] 中存储了如下 5 个元素:a,  b,  c,  d,  e。

现在需要将元素 x 插入到第三个位置。我们只需要将 c 放入到 a[5],将 a[2] 赋值为 x 即可。最后,数组中的元素如下:a,  b,  x,  d,  e,  c。

利用这种技巧,在特定场景下,在第 k 个位置插入一个元素的时间复杂度就会降为 O(1)。

删除操作

跟插入数据类似,如果我们要删除第 k 个位置的数据,为了内存的连续性,也需要搬移数据,不然中间就会出现空隔,内存就不连续了。

和插入类似,如果删除数组末尾的数据,则最好情况时间复杂度为 O(1);如果删除开头的数据,则最坏时间复杂度为 O(n);平均情况时间复杂度也为 O(n)。

实际上,在某些特定场景下,我们并不一定非得追求数组中数据的连续性。如果们将多次删除操作集中在一起执行,删除的效率是不是会提高很多呢?

我们继续来看例子。数组 a[10] 中存储了 8 个元素:a,b,c,d,e,f,g,h。现在,我们要依次删除 a,b,c 这三个元素。

为了避免 d,e,f,g,h 这几个数据会被搬移三次,我们可以先记录下已经删除的数据。每次的删除操作并不是真正的搬移数据,只是记录数据已经被删除。当数组没有更多空间存储数据时,我们再触发一次真正的删除操作,这样就大大减少了删除操作导致的数据搬移。

其实,这种处理删除操作的逻辑,就是 JVM 标记清除垃圾回收算法的核心思想。

数组为什么从 0 开始编号?

从数组存储的内存模型上来看,“下标” 最确切的定义应该是 “偏移(offset)”。如果用 a 来表示数组的首地址,a[0] 就是偏移为 0 的位置,也就是首地址,a[k] 就表示偏移 k 个 type_size 的位置,所以计算 a[k] 的内存地址只需要用这个公式:

a[k]_address = base_address + k * data_type_size

但是,如果数组从 1 开始计数,那我们计算数组元素 a[k] 的内存地址就会变为:

a[k]_address = base_address + (k - 1) * data_type_size

上述两个公式,从 1 开始编号,每次随机访问数组元素都多了一次减法运算,对于 CPU 来说,就是多了一次减法指令。

数组作为非常基础的数据解耦股,通过下标随机访问数组元素又是其非常基础的编程操作,效率的优化肯定就要尽可能的做到极致。素以为了减少一次减法操作,数组选择了从 0 开始编号,而不是从 1 开始。

本文来自极客时间 - 数据结构与算法之美专栏。特此感谢。

版权所有:https://time.geekbang.org/column/article/40961

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

数据结构(一)数组 的相关文章

随机推荐

  • Jmeter性能测试面试基础问答

    性能测试基础 简述实施软件性能测试的流程 a 性能需求分析 挑选用户使用最频繁的功能来做测试 比如 登陆 搜索 提交订单 确定性能指标 比如 事务通过率为100 90 的事务响应时间不超过5秒 并发用户为1000人时CPU和内存的使用率在7
  • 自动化测试很难吗?是的,难。?

    其实现在软件测试有一个特别奇怪的现象 那就是每个人在一起进入这个圈子的时候 他想的就是我要自动化测试 我要学会自动化 自动化会替换手工测试 很多这种心声 我在这给大家来分析一下 首先为什么很多人在入这个圈的时候 会听到 这种声音 比如 学好
  • C语言例题讲解(if语句,循环语句,函数)

    目录 if语句例题 题目分析 代码 题目总结 循环语句例题 题目分析 代码 题目总结 函数例题 题目分析 代码 题目总结 if语句例题 计算1 1 1 2 1 3 1 4 1 5 1 99 1 100 的值 打印出结果 题目分析 1 首先我
  • LinkList集合详解

    LinkList集合详解 1 LinkedList简介 LinkedList类是一个继承于AbstractSequentialList的双向循环链表 它是非同步的 也是非线程安全的 LinkedList实现了List接口 能对它进行队列操作
  • 在elementUI中sort-orders排序,默认为三种,怎么改成两种

    在 table表单中添加sort change事件 sort orders ascending descending
  • 解决:同样的Python程序,在cmd和pycharm都能正常运行,但是在Visual Studio Code却报错,且`conda activate`命令无法激活或切换虚拟环境

    解决 同样的Python程序 在cmd和pycharm都能正常运行 但是在Visual Studio Code却报错 且 conda activate 命令无法激活或切换虚拟环境 1 软件环境 2 问题描述 3 解决方法 4 结果预览 1
  • Linux 文本处理工具 - sed(用于过滤和转换文本)

    Linux 文本处理工具 sed 用于过滤和转换文本 文章目录 Linux 文本处理工具 sed 用于过滤和转换文本 一 简介 二 常用参数 三 动作说明 四 实例 p 显示 d 删除 a 添加 c 替换 w 把符合的行写到指定文件中 i
  • Code-server 云服务器配置docker 运行

    Code server 云服务器配置docker 运行 1 docker安装 安装需要的软件包 yum utils device mapper persistent data lvm2 yum install y yum utils dev
  • android httpClient 支持HTTPS的2种处理方式

    问题 项目中Android https或http请求地址重定向为HTTPS的地址 相信很多人都遇到了这个异常 无终端认证 javax net ssl SSLPeerUnverifiedException No peer certificat
  • Redis学习笔记

    目录 一 redis前言 1 1 Redis简介 1 2 主要特点 1 3redis 的windows安装后 1 4 修改Redis配置文件 二 redis常用数据类型 三 redis常用命令 1 字符串操作命令 2 哈希操作命令 3 列表
  • python爬虫,wallhaven热门壁纸多线程采集下载源码

    新年新气象 祝大家牛转乾坤 牛气冲天 过年期间收到了很多朋友的新年祝福 没有一一回应 见谅 很久没写爬虫了 手生了 在吾爱找了一个练手网站 国外的壁纸网站 wallhaven 这里采集下载热门图片为例 重温一下python图片爬虫 感兴趣的
  • Veeam 备份还原操作手册

    目录 一 安装Bakup Replication 1 1 选择 Backup Replication 1 2 选择Install安装 二 添加VC主机 2 1 VMWARE VSPHERE 添加 2 2 VC主机名 2 3 用户认证 三 配
  • Linux I/O多路复用——epoll模型实现服务端Socket通信

    目录 epoll模型 epoll函数 epoll create epoll ctl epoll wait 程序流程 水平触发 LT 边沿触发 ET select poll epoll对比 为什么ET模式下 需要将套接字设置为非阻塞式 epo
  • C语言_指针

    C语言指针 指针 这个要从直接访问与间接访问说起 在程序中一般通过变量名来引用变量的值 程序通过编译后就会把变量名转化为变量的地址 通过地址对数据进行存取操作 这种方式称为直接访问 而间接访问是将变量i的地址存放在另一变量中 然后通过该变量
  • 手写Spring框架(四)

    逻辑梳理 这部分完成AOP部分 先梳理AOP的步骤 getBean 方法作为入口 而后是几个关键的类 Context在前文都有提到 现在解释一下其他的类 AdviseSupport 通知的工具类 完成配置文件的解析 将Advise和目标类的
  • Spring bean的生命周期

    学习spring源码主框架 从源码角度开发学习Spring bean的生命周期 spring创建bean方法org springframework beans factory support AbstractBeanFactory getB
  • 程序员成长为架构师必备的十项技能

    一 卓越的程序员 1 每个好架构师都是一位出色的程序员 架构师 听起来是如此神秘的一个称号 尤其是在开发领域刚入门不久的菜鸟级程序员眼中 架构师都是高手 都是牛人 都是如此高高在上的存在 不过 在搞了四 五年编程之后 程序员们往往早已失去了
  • 【IT之路】LoadRunner系列-Win7 64bit下搭建Loadrunner11破解版

    一直想提升下性能测试知识 但是都因为这样那样的原因 没有实际上系统梳理下 在此 刚好空出时间来了 一步步把性能测试知识重新拾一下 本文介绍的是在vmware的环境下进行的Loadrunner环境搭建 一 环境准备 Win7 64bit Lo
  • 云计算基础知识:

    云计算 cloud computing 是分布式计算的一种 指的是通过网络 云 将巨大的数据计算处理程序分解成无数个小程序 然后 通过多部服务器组成的系统进行处理和分析这些小程序得到结果并返回给用户 云计算早期 简单地说 就是简单的分布式计
  • 数据结构(一)数组

    概述 说起数组我们都不陌生 几乎在每一种编程语言中 基本上都会有数组这种数据类型 不仅如此它还是是最基础最简单的数据结构 尽管如此 可能还是有一些人并没有真正的理解这个基础数据结构的精髓所在 首先 我们都知道 在java中数组是从 0 开始