2. 基础篇
2.1. 数组:从 0 开始编号
- 数组寻址用到偏移量,a[0] 为偏移为 0 的首地址,a[k] 为偏移 k 个 type_size 的位置,若从 1 开始编号,a[k]的位置变为偏移 k-1,多了一次减法运算
a[k]_address = base_address + k * type_size
数组作为非常基础的数据结构,通过下标随机访问数组元素又是其非常基础的编程操作,效率的优化就要尽可能做到极致。所以为了减少一次减法操作,数组选择了从 0 开始编号,而不是从 1 开始
- 历史原因,Java、JavaScript 沿用 C 语言从 0 开始计数的习惯,降低程序员学习的成本
- Matlab 数组下标从 1 开始
- Python 数组下标从 0 开始,但 -1 表示最后一行元素
数据结构类型:
线性表数据结构.jpg
非线性表数据结构.jpg
数组(Array):
- 线性表数据结构
- 连续的内存空间
- 相同类型的数据
2 + 3 → 数组支持随机访问,根据下标随机访问的时间复杂度为O(1)
低效的插入与删除:平均时间复杂度为 O(n)
插入操作:无序情况下,插入到第 k 个位置,可把原来第 k 个位置的元素移动到数组末尾,如此时间复杂度为O(1)
删除操作:记录元素已删除,当数组满后再真正执行搬移数据的操作(批量删除)
例:JVM 标记清除垃圾回收算法,缺点:产生内存碎片
很多时候我们并不是要去死记硬背某个数据结构或者算法,而是要学习它背后的思想和处理技巧,这些东西才是最有价值的。
数组越界问题:
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;
}
若编译器按照内存地址递减的方式分配内存,局部变量连续压栈,arr[3] 越界访问到变量 i,将其重置为 0
数组越界在 C 语言 中是一种未决行为,但 Java 会做越界检查
容器:封装操作细节
- C++ STL 中的vector
- Java 中的 ArrayList
容器的优势:
- ArrayList封装数组操作细节
- ArrayList支持动态扩容,缺点:耗时,建议指定数据大小
数组的不可替代之处:
- ArrayList无法存储基本类型,需封装为类,造成性能消耗
- 数据大小已知且操作简单的情况下,直接使用数组较为方便
- 多维数组的定义更直观,容器套容器过于复杂,Object[][] array VS. ArrayList< ArrayList < object> > array
总结:
- 业务开发用容器,损耗部分不影响整体
- 底层开发用数组,优化网络框架的性能
引申:
for (int i = 0; i < n; i++)
的写法,比起for (int i = 0; i <= n - 1; i++)
,能够直接看出有 n - 0 = n 个数据,更为直观二维数组 a[m][n] 的内存寻址公式
a[i][j]_address = base_address + (i * n + j) * type_size
- JVM 标记清除算法:运行在 JVM 上的 Java 程序,若堆内存被耗尽,则 GC 线程被触发并将程序暂停,执行标记/清除,再让程序恢复运行
- 标记:遍历所有的 GC Roots,将所有 GC Roots 可达的对象标记为存活
- 清除:遍历堆中所有对象,将没有标记的对象清除
缺点:1.效率问题,标记和清除过程效率不高;2.空间问题,产生不连续的内存空间碎片