问题:数组下标为什么从0开始?
数组array,是一种线性的数据结构,在内存中,具有一段连续的内存地址,每个内存单元,存储的是相同的数据类型(意味着内存单元大小是一样的、可确定的),下标是从0开始 int[] a = new int[10] 其中a获得的是这10个元素大小的练习内存空间的首地址,也就是第一个元素的地址
线性数据结构还有:链表、栈、队列;非线性的结构有:堆、树、图
数组特性
内存布局
- 在内存中连续
- 每个内存单元是固定大小,等同于一个元素类型的大小,比如int
- 每个内存单元,存储相同类型的数据
优点:
- 由于内存地址连续,且每个元素大小相同,所以:有了数组首地址,给定下标,就可以通过一次计算,获得任意访问数组的能力,公式如下:
其中base_address 就是用来接收数组定义的标识符,比如 int[] a = new int[10] 里面的a
随机访问复杂度为O(1)通过数组下标
查询可不是O(1),是给定下标,访问其对应元素为O(1)
缺点:
- 由于要保证数组的特性:内存连续,所以在数组中插入、删除时,就会比较不便——因为插入在中间位置k,就需要先向后挪动k+1 到n的所有元素;而删除在中间位置k,也需要向前挪动k+1 到n的所有元素
插入时间复杂度
- 插入在末尾,为O(1),为最好时间复杂度
- 插入在第一个位置,为O(n),为最坏时间复杂度,需要挪动所有元素向后一位
- 平均时间复杂度:能n+1种位置可能(从0到n,到n后追加),每种可能的概率都是1/(n+1) ,加权到其各自的大O函数上就是
删除时间复杂度
- 删除在末尾,为O(1)
- 删除在首部,为O(n)
- 平均时间复杂度为,O(n)
如何改进数组的缺点?(针对删除情况)
为了保证数组内存连续性,每次删除,第一直觉就是要移动后面的元素了,那么如果每次删除不立即移动元素,而是累积多次删除操作,每次删除只是标记我将来要删除那些位置的元素,等到一个时机,比如数组空间不够用了,再一起移动要移动的元素,不就减少了移动操作吗!——jvm的标记清除算法的核心思想
思考数据结构和算法,背后的思想,处理技巧,灵活应用
假设删除a , b, c,之后再统一移动后面的元素,比每次删除都立刻移动,少了2次移动,提高效率!
数组的访问越界
下面这段c代码,会无限打印hello world,为什么?
因为:本来数组的下标只到2,这里循环条件,给到了3,那么此时就访问到了数组外面,即数组访问越界!c语言中,只要不是访问受限的代码,那么所有的内存空间都是可自由访问的
引申:
在函数体内部的变量,是在栈分配,且是连续压栈,在linux进程的内存布局中,栈区空间,从内存高地址,到低地址增长,所以i先分配,地址为高位,arr这个3元素的数组后分配,地址为低位,于栈来说,arr是在i的“上方的”,那么访问arr遍历时,0,1,2这3个下标,又是从低到高的内存地址去访问,i=3的时候,就一下子到了i的地址空间了,(且还有个前提,i的类型和arr的元素类型一致才行)那么i=3的时候,那次循环,的 arr[i] = 0 ,其实是把,i重新赋值给了0,于是,就进入了无限循环!!
int main(int argc, char* argv[]){
int i = 0;
int arr[3] = {0};
for(; i<=3; i++){
arr[i] = 0;
printf("hello world\n");
}
return 0;
}
对数组的封装(容器)
数组越界,的bug,很隐蔽,难以发现,且需要对底层的知识有足够的、系统的理解才可,所以想c++的STL中的vector和java的arrayList,都是针对数组提供的容器类型,它屏蔽了数组的细节,且包装了很多方法,用容器类型,语言会自动判断,减少bug,比如数组越界就会帮你检查!
QA:类似于golang的slice是对array的引用吗?(值类型和引用类型?)
解答:数组下标为什么从0开始
- 历史原因,java和c++等都是c系语言,c就是从0开始下标的,那么为了保持程序员的顺利迁移到新语言,可能作为一种习惯留存下来
- 性能要求:
如果以0开始,那么数组随机访问,元素时的,内存地址计算公式如下:
其中base_address 就是用来接收数组定义的标识符,比如 int[] a = new int[10] 里面的a
如果以1开始,内存地址计算公式如下:
其中base_address 就是用来接收数组定义的标识符,比如 int[] a = new int[10] 里面的a,相对前者,cpu就需要多一次减法运算!