数组与链表的应用—数组内存模型

在计算机里,全部的数据结构本质上能够归为两类:数组和链表java

数组的内存模型面试

1.一维数组数组

  什么是数组?缓存

  在计算机科学中,数组能够被定义为是一组被保存在存储连续空间中,而且具备相同类型的数据元素的集合。而数组中的每个元素均可以经过索引来进行访问。数据结构

  例:以java语言中一个例子说明一下数组的内存模型,当定义了一个拥有5个元素的int数组后,看看内存是长什么样子?dom

  int[] data = new int[5];函数

  经过上面的声音,计算机会在内存中分配一段连续的空间给这个data数组。如今假设在一个32位上的机器上运行这段程序,java的int类型的数据占据了4个字节的空间,同时也假设计算机分配的地址是从0X80000000开始的,整个data数组在计算机内存中分配的模型以下图所示:学习

                                      

   这种分配连续空间的内存模型同时也揭示了数组在数据结构中的另一个特性,即随机访问(Random Access),随机访问这个概念在计算机科学中被定义为:能够用同等的时间访问到一组数据中的任意一个元素。这个特性除了和连续的内存空间模型有关之外,其实也和数组如何经过索引访问到特定的元素有关。this

  在计算机中,为何在访问数组中的第一个元素时,程序通常都是表达成如下这样的:编码

  data[0]

  也就是说,数组的第一个元素是经过索引“0”来进行访问的,第二个元素是经过索引“1”来进行访问的,......,这种从0开始进行索引的编码的方式被称为“Zero-based Indexing”。固然了在计算机世界中,也存在着其余的编码方式,像Visual Basic中的某些函数索引采用1-based Indexing的,也就是说第一个元素是经过索引“1”来获取的,像这种方式就很少说了。等之后有时间慢慢研究。

  为何数组的第一个元素要用过索引“0”来进行访问呢?缘由就在于获取数组元素的方式是按如下的公式来进行获取的:

  base_address + index(索引) * data_size(数据类型大小)

  索引在这里能够看作是一个偏移量(Offset),仍是以上面的例子来进行说明:

                                                                  

  data这个数组被分配到的起始地址是0X80000000,是由于int类型数据占据了4个字节的空间,若是咱们要访问第五个元素data[4]的时候,按照上面的公式,只须要取得0X80000000 + 4 * 4 = 0X80000010这个地址的内容就能够了。随机访问的背后原理其实也就是利用这个公式达到了同等的时间访问到一组数据中的任意元素。

 

2.二维数组

  上面所提到的数组是属于一维数组的范畴,咱们平时可能还会听到一维数组的其余叫法,例如向量(Vector)或者表(Table)。由于在数学上,二维数组能够很好的用来表达矩阵(Matrix)这个概念,因此不少时候咱们又会将矩阵或者二维数组这种称呼交替使用。

  若是咱们按照下面的方式声明一个二维数组:

  int[][] data = new int[2][3];

  在面试中咱们知道了数组的起始地址,在基于上面的二维数组声明的前提下,data[0][1] 这个元素的内存地址是多少呢?标准答案实际上是“没法肯定”,什么?标准答案是没法肯定,别着急,由于这个问题的答案其实和二维数组在内存中的寻址方式有关。而这其实涉及到计算机内存究竟是以行优先(Row-Major Order)仍是以列优先(Column-Major Order)存储的。

  假设如今有一个二维数组,以下图所示:

                   

  下面咱们就这看看行优先或列优先形成的内存模型会形成什么样的区别:   

  (1)行优先

    行优先的内存模型保证的每一行的每一个相邻元素都保存在了相邻的连续空间中,对于上面的例子,这个内存模型以下图所示,假设起止地址是0X80000000:

                                                                                

    能够看到,在二维数组的每一行中,每一个相邻的元素都保存在了相邻的连续内存里。

    在以行优先存储的内存模型中,假设咱们要访问data[i][j]里的元素,获取数组的方式是按照如下公式进行获取的:

    base_address + data_size * (i * number_of_column + j)

    回到一开始的问题里,当咱们访问data[0][1]这个值时,能够套用上面的公式,其获得的值就是咱们要找的0X80000004地址的值,也就是2。

    0x80000000 + 4 x (0 x 3 + 1) =  0x80000004

                              

   (2)列优先

    列优先的内存模型保证了每一列的每一个相邻元素都保存在了相邻的连续内存中,对于上面的例子,这个二维数组的内存模型以下图所示:

                          

    能够看到,在二维数组的每一列中,每一个相邻的元素都保存在了相邻的连续的内存中。

    在以列优先存储的内存模型中,假设咱们要访问data[i][j]里的元素,获取数组元素的方式是按照一下公式获取的:

    base_address + data_size * (i + number_of_row * j)

    当咱们访问data[0][1]这个值时,能够套用上面的公式,其获得的值就是咱们要找的0x80000008地址的值:

    0x80000000 + 4 * (0 + 2 * 1) = 0x80000008

                  

    因此回到一开始那个问题里,行优先仍是列优先存储方式会形成data[0][1]元素的内存地址不同。

3.多维数组

  多维数组其实本质上和前面介绍的一维数组和二维数组是同样的,若是咱们按照下面的方式来声明一个三位数组:

  int[][][] data = new int[2][3][4]; 

  则能够把这个数组想象成两个int[3][4]这样的二维数组,对于多维数组则能够以此类推,下面把行优先和列优先的内存寻址方式列出来:

  假设声明一个data[S1][S2][S3]...[Sn]的多维数组,若是要访问data[D1][D2][D3]...[Dn]的元素,内存寻址计算方式按照以下方式寻址:

  行优先:

  base_address + data_size * (Dn + Sn * (Dn - 1 + Sn - 1 * (Dn - 2 + Sn - 2 * (... + S2 * D1 )... )))

  列优先:

  base_address + data_size * (D1 + (S1 * (D2 + S2 * (D3 + S3 * (... + Sn - 1 * Dn)...))))

  cpu在读取内存数据的时后,一般会有一个cpu缓存策略,也就是说再cpu读取程序指定地址的数值时,cpu会把它地址相邻的一些数据一并读取,并放到更高一级的缓存中,好比L1或者L2缓存。当数据存放到这种缓存上的时候,读取的速度有可能会比直接从内存上读取的速度快10倍以上。

  在高级语言中经常使用的C/C++和Objective-C都是行优先的内存模型,而Fortran或者Matlab是列优先的内存模型。

“高效”的访问与“低效”的插入删除

  从前面的的数组内存模型学习中,咱们知道了访问一个数组的元素是随机访问方式,只须要按照上面讲到的寻址方式来获取相应位置的数值即可,因此访问数组元素的复杂度是O(1)。

  对于保存基本类型(Primitive Type)数组来讲,它们的内存大小在一开始就已经肯定好了,咱们称他为静态数组(Static Array)。静态数组的大小是没法改变的,因此咱们没法对这种数组进行插入和删除操做。可是在使用高级语言的时候,好比java,咱们知道java中的ArrayList这种Collection提供了像add和remove这样的API来进行插入和删除操做,这种数组可称之为动态数组(Dynamic Array)。

  咱们一块儿来看看add和remove函数在java Open-jdk11中的源码,一块儿分析他们的时间复杂度:

  在java Connection中,底层的数据结构其实仍是使用的数组,通常在初始化的时候会分配一个比咱们在初始化时设定好的大小更大的空间,以方便之后进行增长元素的操做。

  假设全部的元素都保存在elementData[]这个数组中,add函数的主要时间复杂度来源于如下源码片断:

1.add(int index,E element)函数源码:

  首先来看看add(int index,E element)这个函数的源码: 

public void add(int index,E element){          
    rangeCheckForAdd(index);
    modCount++;
    final int s;
    Object[] elementData;
    if((s = size) == (elementData = this.element).length){
       elementData = grow();
    }
    System.arraycopy(elementData,index,elementData,index +1,s - index);
    elementData[index] = element;
    size = s + 1;   
}        

  能够看到add函数调用了一个System.arraycopy的函数进行内存操做,s在这里表明了ArrayList的size,当咱们调用add函数的时候,函数在实现的过程当中到底发生了什么?咱们来看一个例子。

  假设elementData里面存放着如下元素:

                             

  当咱们调用的add(1,4)函数,也就是在index为1的地方插入4这个元素,在add函数中则会执行System.arraycopy(elementData,1,elementData,2,6 - 2)语句,它的意思是将重elementData数组index为1的地址开始,复制日后的4个元素到elementData数组为2的地址位置,以下图所示:

                                                               

  红色部分表明执行完System.arraycopy函数的结果,最后执行elementData[1] = 4;这条语句:

                                                              

  由于这里涉及到每一个元素的复制,平均下来的时间复杂度至关于O(n)。

2.remove(int index)函数源码:

  

 1 public E remove(int index){
 2  Objects.checkIndex(index,size); 3 final Object[] es = elementData; 4 5 @SuppressWarnings("unchecked") E oldValue = (E) es[index]; 6  fastRemove(es,index); 7 8 return oldValue; 9 } 10 11 private void fastRemove(Object[] es,int i){ 12 modCount++; 13 final int newSize; 14 if((newSize = size -1) > i){ 15 System.arraycopy(es,i+1,es,i,newSize - i); 16  } 17 es[size = newSize] = null; 18 }

  这里的newSize指原来的elementData的size - 1,当咱们调用remove(1)会发生什么呢?咱们仍是如下面的例子来解释。

                                                              

  若是调用remove(1)函数,也就是删除在index为1这个地方的元素,在remove函数中则会执行System.arraycopy(elementData,2,elementData,1,2)语句,它的意思是将从elementData数组index为2的地址开始,复制后面的两个元素到elementData数组到index为1的地址位置,以下图所示:

                                                             

  由于这里一样涉及到每一个元素的复制,平均下来时间复杂度至关于O(n)。

 

心得:

  这是我学数据结构的第一节课内容,由于基础太薄弱,看完视频后,感受老师在讲的时候什么都明白,而后回来再看老师的笔记仍是一头雾水,因而乎就把老师的笔记一个字一个字的打入了博客当中,这些除了图片以外其余彻底是手打的,只为加强记忆力和理解力,打完了以后对里面的内容掌握率感受仍是不高,我会继续学习,把我所学到的知识所有写入个人博客中,供你们学习和交流。(根据蔡元楠老师讲解的数据结构精讲整理此笔记)

相关文章
相关标签/搜索