数据结构与算法学习笔记(2)-数组

前记

  前篇总结复杂度分析,本篇学习数组。

数组

  数组(Array)是一种线性表数据结构。它用一组连续的内存空间来存储一组具有相同类型的数据。
  数组和链表的区别,很多人都说,“链表适合插入、删除,时间复杂度O(1);数组适合查找,查找时间复杂度为O(1)”。实际上,这种表述是不准确的。数组是适合查找操作,但是查找的时间复杂度并不为O(1)。即便是排好序的数组,你用二分查找,时间复杂度也是O(logn)。所以,正确的表述应该是,数组支持随机访问,根据下标随机访问的时间复杂度为 O(1)。
  数组为了保持内存数据的连续性,会导致插入、删除这两个操作比 较低效。

插入操作

  如果在数组的末尾插入元素,那就不需要移动数据了,这时的时间复杂度为O(1)。但如果在数组的开头插入元素,那所有的数据都需要依次往后移动一位,所以最坏时间复杂度是O(n)。因为我们在每个位置插入元素的概率是一样的,所以平均情况时间复杂度为(1+2+…n)/n=O(n)。
  如果数组中的数据是有序的,我们在某个位置插入一个新的元素时,就必须依次搬移k之后的数据。但是,如果数组中存储的数据并没有任何规律,数组只是被当作一个存储数据的集合。在这种情况下,如果要将某个数组插入到第k个位置,为了避免大规模的数据搬移,我们还有一个简单的办法就是,直接将第k位的数据搬移到数组元素的最后,把新的元素直接放入第k个位置。

删除操作

  跟插入数据类似,如果我们要删除第k个位置的数据,为了内存的连续性,也需要搬移数据,不然中间就会出现空洞,内存就不连续了。和插入类似,如果删除数组末尾的数据,则最好情况时间复杂度为O(1);如果删除开头的数据,则最坏情况时间复杂度为O(n);平均情况时间复杂度也为O(n)。

警惕数组的访问越界问题

  数组越界在C语言中是一种未决行为,并没有规定数组访问越界时编译器应该如何处理。因为,访问数组的本质就是访问一段连续内存,只要数组通过偏移计算得到的内存地址是可用的,那么程序就可能不会报任何错误。

容器能否完全替代数组?

  针对数组类型,很多语言都提供了容器类,比如Java中的ArrayList、C++STL中的vector。在项目开发中,什么时候适合用数组,什么时候适合用容器呢?
  数组本身在定义的时候需要预先指定大小,因为需要分配连续的内存空间。如果我们申请了大小为10的数组,当第11个数据需要存储到数组中时,我们就需要重新分配一块更大的空间,将原来的数据复制过去,然后再将新的数据插入。如果使用vector等容器,我们就完全不需要关心底层的扩容逻辑,vector已经帮我们实现好了。每次存储空间不够的时候,它都会将空间自动扩容为2倍大小。
  不过,这里需要注意一点,因为扩容操作涉及内存申请和数据搬移,是比较耗时的。所以,如果事先能确定需要存储的数据大小,最好在创建vector的时候事先指定数据大小。
  作为高级语言编程者,是不是数组就无用武之地了呢?当然不是,有些时候,用数组会更合适些,我总结了几点自己的经验:
  1.如果数据大小事先已知,并且对数据的操作非常简单,用不到vector提供的大部分方法,也可以直接使用数组
  2.还有一个是我个人的喜好,当要表示多维数组时,用数组往往会更加直观。比如int Object[][]array;而用容器的话则需要这样定义:vector<vector> v;
  总结一下,对于业务开发,直接使用容器就足够了,省时省力。毕竟损耗一丢丢性能,完全不会影响到系统整体的性能。但如果你是做一些非常底层的开发,比如开发网络框架,性能的优化需要做到极致,这个时候数组就会优于容器,成为首选。