算法5分钟|浅入浅出【数组】

本文深入探讨了数组这一基础数据结构,从线性表的概念出发,解析了数组的定义、下标从0开始的原因、常见操作如随机访问、插入和删除的复杂度分析。此外,还讨论了数组与链表的区别,并揭示了数组下标从0开始能减少CPU指令运算的历史和逻辑原因。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

导读概述

     提到数组,我相信你肯定不陌生,甚至还会自信地说,它很简单啊。是的,在每一种编程语言中,基本都会有数组这种数据类型。不过,它不仅仅是一种编程语言中的数据类型,还是一种最基础的数据结构。尽管数组看起来非常基础、简单,但是我估计很多人都并没有理解这个基础数据结构的精髓。比如有没有想过数据为什么从零开始查找,从1查找不更符合人类思维吗?

带着疑问,本文将从以下几个方面分享一下数组原理及应用

1.什么是线性表

2.什么是数组

3.数组下标为什么从0开始

4.数组的常规操作

5.小结,从本文中学到了什么

线性表

  首先在讲解数组前先理解线性表非线性,因为数组属于线性结构的一种,这样从全局俯视一下数组对你认识数组更有帮助。

什么是线性表

线性表:(linear list)线性表就是数据排成像一条线一样的结构。每个线性表上的数据最多只有前和后两个方向,是一个有序数据元素的集合

图片

线性表包含:数组、链表、队列、栈等都是线性表结构。

图例如下:

图片

什么是非线性表

非线性表:与线性表相对立的概念是非线性表。之所以叫非线性,是因为,在非线性表中,数据之间并不是简单的前后关系

非线性表:二叉树、堆、图等,图例如下:

数组

什么是数组

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

示意图:

图片

数组原理

先来说一下数组的存储原理:

数组用一组连续的内存空间来存储一组具有相同类型的数据。

图片

数组可以根据下标随机访问数据

比如一个整型数据 int[] 长度为5

图片

  • 假设首地址是:1000

  • int是4字节(32位),实际内存存储是位

  • 随机元素寻址

//上图详细公试:
a[i]_address=a[0]_address+i*4

//通用公式:
//base_address首地址相当于a[0],data_type_size 表示数组中每个元素的大小 案例4个字节
a[i]_address = base_address + i * data_type_size

base_address首地址相当于a[0]。

data_type_size 表示数组中每个元素的大小,假设4个字节。

该公式解释了三个方面

  • 连续性分配

  • 相同的类型

  • 下标从0开始

数组操作

读取元素

根据下标读取元素的方式叫作随机读取

案例:int n=nums[3]
/**
 * 获取元素
 * @param index
 * @return
 */
public int get(int index){
    return nums[index];
}

更新元素

案例:nums[3]= 8;
/**
 * 通过数组下标更新数组元素
 * @param index
 * @param n
 */
public void update(int index,int n){
    nums[index] = n;
}

注意不要数组越界

读取和更新都可以随机访问,时间复杂度为O(1)

插入元素 有三种情况:

尾部插入

在数据的实际元素数量小于数组长度的情况下:

直接把插入的元素放在数组尾部的空闲位置即可,等同于更新元素的操作

尾部插入的时间复杂度为O(1)

案例:nums[6]= 12;
/**
 * 尾部插入
 * @param n
 */
public void insertTail(int n){
    nums[nums.length]=n;
}

中间插入

在数据的实际元素数量小于数组长度的情况下:

由于数组的每一个元素都有其固定下标,所以首先把插入位置及后面的元素向后移动,腾出地方,再把要插入的元素放到对应的数组位置上。

中间插入或头部插入的时间复杂度为O(n)

图片

/**
* 数组中间插入
* @param point
* @param n
*/
public void insertMid(int point,int n){
for (int i=0;i<nums.length;i++){
  if(nums[i]!=0){
      nums[i+1]=nums[i];
  }
}
nums[point-1]= n;
}

范围插入

假如现在有一个数组,已经装满了元素,这时还想插入一个新元素,或者插入位置是越界的,这时就要对原数组进行扩容:可以创建一个新数组,长度是旧数组的2倍,再把旧数组中的元素统统复制过去,这样就实现了数组的扩容。

/**
* 数组copy
*/
public void resize(){
  int[] newNums = new int[nums.length*2];
  System.arraycopy(nums,0,newNums,0,nums.length);
  nums = newNums;
}
/**
* 数组扩容
* @param point
* @param n
*/
public  void insertOutOfBounds(int point,int n){
  //数组扩容
  resize();
  nums[point-1]=n;
}

删除元素

数组的删除操作和插入操作的过程相反,如果删除的元素位于数组中间,其后的元素都需要向前挪

动1位。

尾部删除最好的时候复杂度O(1)

头部删除中间删除都需要移动元素时间复杂度O(n)

for(int i=0;i<nums.length;i++)
{ 
    nums[i-1]=nums[i];
}

完整代码:

/** 数组实现 查询 中间插入,尾部插入,数组扩容
* @author lihaoran
*/
public class ArrayDemo {
int[] nums = new int[8];
/**
* 获取元素
* @param index
* @return
*/
public int get(int index){
  return nums[index];
}
/**
* 通过数组下标更新数组元素
* @param index
* @param n
*/
public void update(int index,int n){
  nums[index] = n;
}
/**
* 尾部插入
* @param n
*/
public void insertTail(int n){
  nums[nums.length]=n;
}
/**
* 数组中间插入
* @param point
* @param n
*/
public void insertMid(int point,int n){
  for (int i=0;i<nums.length;i++){
      if(nums[i]!=0){
          nums[i+1]=nums[i];
      }
  }
  nums[point-1]= n;
}
/**
* 数组copy
*/
public void resize(){
  int[] newNums = new int[nums.length*2];
  System.arraycopy(nums,0,newNums,0,nums.length);
  nums = newNums;
}
/**
* 数组扩容
* @param point
* @param n
*/
public  void insertOutOfBounds(int point,int n){
  //数组扩容
  resize();
  nums[point-1]=n;
}

/**
* 删除指定下标值
* @param point
*/
public void deleteMid(int point){
  for (int i=point;i<nums.length;i++){
      nums[i-1]=nums[i];
  }
}
public void display(){
  for(int n:nums){
      System.out.println(n);
  }
 }
}

学到了什么

数组如何实现的随机访问?

数组连续的内存空间相同类型的数据。正是因为这两个特性,从而才使得数组有随机访问的能力;

缺点:这两个特性也让数组的很多操作变得非常低效,比如要想在数组中删除、插入一个数据,为了保证连续性,就需要做大量的数据搬移工作。

补充:随机访问的意思,就是可以随机选择下标,进行数据访问。

为什么很多编程语言的数组都是从0开始编号的Why?

原因一:减少CPU指令运算

  • 下标从0开始:

      数组寻址

a[i]_address = base_address + i * type_size

    其中base_address为数组arr首地址,arr0就是偏移量为0的数组,即数组arr首地址;i为偏移量,type_size为数组类型字节数,比如int为32位,即4个字节。

  •  下标从1开始:

     数组寻址

a[i]_address = base_address + (i-1)*type_size

    比较两个计算公式可以发现公式(2)每次CPU寻址需要多一次 i-1的操作,对于CPU即多了一次减法的指令运算

对于数组这种基础数据结构,无论在哪种高级程序语言中,都是频繁间接(作为容器的基础数据结构,比如Java的ArrayList)或者直接被使用的,因此要尽量减少其消耗CPU资源。

原因二:历史原因

  • 语言出现顺序从早到晚C、Java、JavaScript。

  • C语言数组下标是从0开始->Java也是->JavaScript也是。

  • 低额外的学习和理解成本。

原因三:物理内存的地址是从0开始的

计算机主存是多个连续字节大小的单元组成的数组,每个字节都对应唯一的物理地址,第一个字节的地址为0。

数组的操作复杂度分析

随机访问时间复杂度O(1)

插入

若有一元素想往int[n]的第i个位置插入数据,需要在i-n的位置往后移。

  • 最好情况时间复杂度 O(1)

  • 最坏情况复杂度为O(n)

  • 平均负责度为O(n)

补充:如果数组中的数据不是有序的,也就是无规律的情况下,可以直接把第i个位置上的数据移到最后,然后将插入的数据直接放在第i个位置上。这样时间复杂度就降为 O(1)了。

删除:

与插入类似,为了保持内存的连续性。

  • 最好情况时间复杂度 O(1)

  • 最坏情况复杂度为O(n)

  • 平均负责度为O(n)

数组和链表有什么区别?

  • 数组中的元素存在一个连续的内存空间中,而链表中的元素可以不存在于连续的内存空间。

  • 数组支持随机访问,根据下标随机访问的时间复杂度是O(1);链表适合插入、删除操作,时间复杂度为O(1)。

 • 后记 • 

      通过本章数组分析,希望你对数组有了重新的认识了。本套系列持续更新中,已收录到【算法】和【数据结构】专栏里(干货满满),如果感觉有所收获,可以动动小手指给点个赞,感谢阅读!

扫码关注微信公共账号!!

作者微信公共账号:算法5分钟|浅入浅出【数组】

图片

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值