本系列全部文章的代码都是用JavaScript实现,之因此用JavaScript实现是由于它能够直接在浏览器宿主中运行代码,即在浏览器中按f12打开控制台,选择console按钮,在下面空白的文本框把本例的代码黏贴上去回车便可运行。方便各位同窗学习和调试。python
数组这个概念相信各位同窗在平常写代码的时候确定会常常用到,咱们一般用数组做为容器来存储数据。基本上每一种编程语言都有这种数据结构,它是一个基础的数据结构,下面将仔细的讲解数组的原理及应用。算法
什么是数组呢?按照专业的名词解释,数组是一种线性表数据结构,它用连续的内存空间来存储一组具备相同类型的数据。从定义里咱们能够看到几个关键词,分别是线性表(Linear List)和连续的内存空间和相同类型的数据。编程
线性表的意思其实就是数据排成像一条线同样的结构。每一个线性表上的数据最多只有前和后两个方向。其实除了数组,链表、队列、栈等都是线性表结构。而与线性表对立的则是非线性表 ,好比二叉树、图、堆等。之因此叫非线性,是由于非线性表中的数据并非简单的先后关系。数组
当咱们声明一个数组的时候,计算机就会为数组分配一个连续的内存空间。假如咱们声明的数组长度是10,在数组中存储的元素都说int类型的数据,若是内存的首地址为1000,则计算机为数组分配了1000~1039的连续内存空间。数组和链表不一样的一点就是数组存储的都是连续的内存空间,而链表存储的都说不连续的内存空间,因此若是一个计算机的内存只有1G的状况下,咱们声明了一个占用1G内存的数组颇有可能会致使内存溢出,由于有可能内存里有不连续的空间,而声明1G内存的链表则不会出现这种状况。浏览器
结合上面所说的两点,数组因为是线性的而且是连续的内存空间,随机访问的时候时间复杂度很是的快,为O(1)。数组的随机访问并不须要遍历自己,只须要知道下标就能够得出值。可是有利也有弊,与快速的查询相反的就是在插入和删除的时候所要耗费更多的复杂度。在这里须要提一点的是,数组是随机查找的时候时间复杂度为O(1),不能笼统的认为数组在执行查找操做的时候时间复杂度为O(1),若是你用二分查找来对数组进行查找操做,耗费的时间复杂度为O(logn)。bash
上面提到数组因为连续的内存空间致使了在执行插入和删除操做的时候占用大量的性能。首先咱们来讲一下插入操做在数组的执行过程。数据结构
假设咱们声明了一个数组长度为n,若是咱们要插入的数组在数组第m个位置的时候,为了可以让数据成功的插入下标m当中,咱们要把m到n这一部分的数据日后移一位,而后把数据放入下标m当中。那若是数据是要插入到数组最后面的话,那时间复杂度也只是O(1),若是是在开头插入的话时间复杂度则为O(n),由于每一个位置的几率都是同样的,因此咱们能够获得平均时间复杂度为:。编程语言
若是一个数组是有序的,咱们为了保持数组的有序性,的确只能用上述的方法来解。可是若是数组是无序的,为了不大规模的数据移动,咱们能够把当前下标m的数据放到最后面,把咱们的值放入到下标m当中。利用这个方法咱们能够将时间复杂度降到O(1),性能将极大的提高。函数
同理在删除中,若是咱们要删除下标为m的元素,为了内存的连续性,也须要把m到n后面的数据往前移,否则就不连续。删除的最好时间复杂度是O(1),即删除的是结尾的数据的时候。最坏时间复杂度则为O(n),即在开头的数据被删除。它的平均时间复杂度的公式也和上面插入的公式同样,结果为O(n)。布局
那么若是咱们对数组进行频繁的删除操做,程序的性能将会极大的下降,有时候办法能够解决呢?这个时候咱们能够借助JVM标记清除垃圾回收算法来实现。当执行删除操做的时候咱们并非真的把数组里的元素给删除掉,而是给该元素标记一个删除状态,等到后面数组没有更多的空间存储数组的时候再一次性的执行删除操做,极大地减小数据的迁移。下面用JavaScript代码来简单的实现一下:
var arr = new Array(10)
var count = 0
function insertArr(obj) {
if (typeof arr[9] === 'object') {
var tempArr = []
for (var a = 0; a < arr.length; a++) {
if (!arr[a].removeSign) {
arr[a].index = tempArr.length
arr[a].removeSign = false
tempArr.push(arr[a])
}
}
arr = tempArr
count = tempArr.length
if (arr.length === 10) {
console.error('数组越界')
return
}
}
arr[count] = {
value: obj.value,
removeSign: false,
index: count
}
count++
}
function removeArr(index){
if (arr.length === 0) {
console.error('数组长度为0,不能删除元素')
return
}
else if (index > arr.length) {
console.error('数组越界')
return
}
// 若是当前的已标记为true则查看下一个元素是否为true,若是不是则标记为true,是的话则继续递归
if (arr[index].removeSign) {
return removeArr(++index)
}
arr[index].removeSign = true
}复制代码
这个代码的含义是声明一个长度为10的数组,存入的都是对象,对象里的value属性表明它的值,removeSign属性表示的是删除标志,为false的时候表示的是未删除,index属性表示的是下标。下面的一个测试用例表示在数组里存入10个数,而后删除其中三个,最后添加一个元素后获得长度为8的数组。整个程序在存入是数据大于数组长度的时候才会发生数组的删除操做。
for (let a = 0; a < 8; a++) {
insertArr({
value: a,
removeSign: false
})
}
removeArr(2)
insertArr({
value: 10,
removeSign: false
})
insertArr({
value: 11,
removeSign: false
})
removeArr(1)
removeArr(2)
removeArr(3)
insertArr({
value: 13,
removeSign: false
})复制代码
数组越界问题在不一样的编程语言中会出现不同的结果。就拿上面的JavaScript代码为例,因为JavaScript的数组是动态的,因此即便你声明一个长度为10的数组,你也能够给数组的第十一位赋值,以后数组的长度就会变成11。而像Java这种静态语言,自己就有对数组长度是否越界进行检查,当你给数组第十一位赋值的时候就会报数组越界的问题,而像C语言,状况则更复杂。下面写个代码来举例:
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;
}
复制代码
上面的这个代码在C语言环境中是无限循环输出hello world,为何会出现这种状况呢?那是由于在 C 语言中,只要不是访问受限的内存,全部的内存空间都是能够自由访问的,函数体内的局部变量存在栈上,且是连续压栈。在Linux进程的内存布局中,栈区在高地址空间,从高向低增加。变量i和arr在相邻地址,且i比arr的地址大,因此arr越界正好访问到i。固然,前提是i和arr元素同类型,不然那段代码还是未决行为。而且不少计算机病毒也正是利用到了代码中的数组越界能够访问非法地址的漏洞,来攻击系统,因此写代码的时候必定要警戒数组越界。
从数组存储的内存模型上来看,“下标”最确切的定义应该是“偏移(offset)”。上面说到,咱们定义一个数组时,计算机会给每一个内存单元分配一个地址,计算机经过地址来访问内存中的数据。当计算机须要随机访问数组中的某个元素时,它会首先经过下面的寻址公式,计算出该元素存储的内存地址公式以下:
a[i]_address = base_address + i * data_type_size
其中 data_type_size 表示数组中每一个元素的大小。若是用 a 来表示数组的首地址,a[0] 就是偏移为 0 的位置,也就是首地址,a[k] 就表示偏移 k 个 type_size 的位置,因此计算 a[k] 的内存地址只须要用这个公式:
a[k]_address = base_address + k * type_size
可是,若是数组从 1 开始计数,那咱们计算数组元素 a[k] 的内存地址就会变为:
a[k]_address = base_address + (k-1)*type_size
对比两个公式,咱们不难发现,从 1 开始编号,每次随机访问数组元素都多了一次减法运算,对于 CPU 来讲,就是多了一次减法指令。数组做为很是基础的数据结构,经过下标随机访问数组元素又是其很是基础的编程操做,效率的优化就要尽量作到极致。因此为了减小一次减法操做,数组选择了从 0 开始编号,而不是从 1 开始。不过其余编程语言不必定数组下标就是从0开始,好比MATLAB,而像python则能够负下标。
上面讲到的是一维数组的内存寻址公式,若是到一个m*n的二维数组,当它的下标i<m,j<n时,它的公式以下:
a[i][j]address = base_address + n * i * type_size + j * type_size = base_address + ( i * n + j) * type_size
同理a*b*c的三维数组,当它的下标i<a,j<b,k<c时,公式以下:
a[i][j][k]address = base_address + bc * i * type_sizze + c * j * type_size + k * type_size = base_address + (bc * i + c * j + k) * type_size
上一篇文章:数据结构与算法的重温之旅(二)——复杂度进阶分析
下一篇文章:数据结构与算法的重温之旅(四)——链表