1、计算机内存管理
我们每天都在面对着如电脑、手机等电子产品,每一台电子设备都有自己的内存大小,如:64G、256G、512G甚至更多
内存到底有什么作用呢?
简单来说,计算机的程序都是运行在内存中的
那么内存是如何存储的呢?
我们可以简单的将内存当作寄存柜
假设一家人要去逛超市,身上共带有四个小包,需要将身上的东西放在四个储物柜中。每个储物柜都有自己对应的编号,方便我们去查找和记录。逛完超市后,再从这四个储物柜中将东西取走
这就是计算机内存的工作原理
每一个储物柜都可以当作一个存储单元,而储物柜编号可以当作每个存储单元的内存地址
扩展知识:为什么连续两个方格内存地址差 8?
因为现在电脑基本是 64 位,64 位表示内存存储最小使用单位是 64 个 bit,也就是 8 个 byte
2、数组的存储和读取
提到数组,大家一定不陌生,也肯定会有很多人会说数组很简单。确实,数组是计算机中最基础的数据结构,每个编程语言中基本都有数组。尽管数组看起来很简单易懂,但是大家有没有想过一个问题:为什么数组的索引会从 0 开始呢?
数组存储
要回答上面这个问题,首先要搞清楚数组到底是怎么在内存中存储的,我们从数组的定义开始
数组(Array)是一种线性表数据结构。它用一组连续的内存空间,来存储一组具有相同数据类型的数据
线性数据结构:表示数组中的数据都是按照前后顺序这种线性顺序排列的
相同数据类型:数组中的每个值的数据类型都相同
数组读取
每个数组都有对应的内存地址,我们称其为数组的开始地址:start_address
数组在定义的时候就已经确定好该数组的数据类型了,我们也就清楚了每个元素需要的内存空间大小,称其为:item_size
因此数组中每个元素的内存地址都可以被计算出来:
// 第一个元素地址
start_address
// 第二个元素地址
start_address + item_size * 1
// 第三个元素地址
start_address + item_size * 2
// 第N个元素地址
start_address + item_size * (N - 1)
综上可知:
-
时间复杂度
数组的索引访问的时间复杂度是O(1)
-
为什么数组的索引是从0开始
内存地址的计算规则设置开始地址为
start_address + item_size * 0
,在计算机中为了方便位置的计算,所以数组索引从 0 开始
3、数组的插入和删除
数组插入
往数组里面插入一个元素的速度,取决于你需要把它插入到哪个位置上
假设我们在为周末的事情做计划,现在暂定计划如下:
eat breakfast // 吃早饭
reading // 看书
have lunch // 吃午饭
have dinner // 吃晚饭
把周末安排设置为一个类,我们预留20个计划,那么代码如下:
public class Plan {
String[] array = new String[20];
private int size = 0;
// 内置4个购物计划
public Plan() {
array[0] = "eat breakfast";
array[1] = "reading";
array[2] = "have lunch";
array[3] = "have dinner";
this.size = 4;
}
public void print() {
for (int i = 0; i < this.size; i++){
System.out.print(this.array[i] + " ");
}
}
public static void main(String[] args) {
Plan p = new Plan();
p.print();
}
}
代码内置了4个周末的计划,并且设置计划数量 size
为 4
代码中提供了
尾部插入
现在我们突然想在吃完饭之后去购物(shopping),该怎么处理呢
每个数组我们都知道开头的内存地址,也知道数组包含多少个元素以及每个元素占用的内存大小是多少。所以在尾部插入元素很容易,尾部的内存地址为:
// n 为当前数组中元素的个数
start_address + item_size * n
因此只需一步就可以插入成功,只需要在原来的代码中加入 add 方法:
public void add(String target) {
this.array[this.size] = target;
this.size++;
}
接下来就可以根据当前计划的数量
size
,在后面继续添加新的计划
中间插入
如果希望在下午来一顿下午茶(afternoon tea),应该怎么处理呢?
我们知道,需要在数组中间部分插入数据,但是为了保证数组的顺序和连续的内存空间,插入地方后面部分数据,需要往后依次移动,步骤如下:
- 移动
shopping
- 移动
have dinner
- 插入
afternoon tea
同理,如果需要插入在开始的地方,那么就需要 N 步
因此平均步数为 (1 + 2+ 3 + ... + N)/N
最终时间复杂度为 O(N)
,也就是数组的插入时间复杂度为线性时间复杂度
复杂度相关的内容大家可以参考 数据结构与算法之复杂度三步走 一文
我们在代码块中新增一个 insert
函数,在索引位置为 3 的地方插入一项任务
public void insert(String target) {
// 索引值为3的地方
int index = 3;
// 第一步:从右侧开始依次右移
for (int i = this.size - 1; i >= index; i--) {
this.array[i+1] = this.array[i];
}
// 第二步:插入元素
this.array[index] = target;
// 调整size
this.size++;
}
空间不够
如果空间不够了的话,系统会自动抛出异常 — Out Of Memory 内存不够啦
因此我们可以:
- 开辟新的内存空间
- 复制原来的数组到新的内存空间
- 再进行插入操作
数组删除
数组的删除操作和插入操作刚好相反
-
删除尾部元素:
直接删除
-
删除中间元素:
- 删除中间元素
- 该元素的右侧元素依次左移
因此我们可知:数组删除的时间复杂度也为 O(N)
4、总结
通过上面的分析,我们可以得出数组的优势与劣势:
-
优势
数组的查询数据特别快,时间复杂度是 O(1)
-
劣势
数组的插入和删除比较慢,时间复杂度是 O(N)
需要连续的内存空间,并且会申请额外的内存空间便于其扩展,这种数据结构对内存要求比较高,利用率低
由于这些优势与劣势,我们在高频查询,低频插入和删除的情况下,可以选择数组作为底层数据结构
Java中的 ArrayList,实际上在底层就是利用数组实现的
最后,小橘子希望大家如果觉得本文有用的话来个 点赞 + 关注,或者分享给身边的朋友们,也希望各位大家能够给小橘子一些建议,加油!