【学习笔记】《数据结构与算法之美》基础篇:数组

2. 基础篇

2.1. 数组:从 0 开始编号

  1. 数组寻址用到偏移量,a[0] 为偏移为 0 的首地址,a[k] 为偏移 k 个 type_size 的位置,若从 1 开始编号,a[k]的位置变为偏移 k-1,多了一次减法运算
a[k]_address = base_address + k * type_size

数组作为非常基础的数据结构,通过下标随机访问数组元素又是其非常基础的编程操作,效率的优化就要尽可能做到极致。所以为了减少一次减法操作,数组选择了从 0 开始编号,而不是从 1 开始

  1. 历史原因,Java、JavaScript 沿用 C 语言从 0 开始计数的习惯,降低程序员学习的成本
  • Matlab 数组下标从 1 开始
  • Python 数组下标从 0 开始,但 -1 表示最后一行元素


数据结构类型

线性表数据结构.jpg
非线性表数据结构.jpg


数组(Array)

  1. 线性表数据结构
  2. 连续的内存空间
  3. 相同类型的数据

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

容器的优势

  1. ArrayList封装数组操作细节
  2. ArrayList支持动态扩容,缺点:耗时,建议指定数据大小

数组的不可替代之处

  1. ArrayList无法存储基本类型,需封装为类,造成性能消耗
  2. 数据大小已知且操作简单的情况下,直接使用数组较为方便
  3. 多维数组的定义更直观,容器套容器过于复杂,Object[][] array VS. ArrayList< ArrayList < object> > array


总结

  • 业务开发用容器,损耗部分不影响整体
  • 底层开发用数组,优化网络框架的性能

引申

  1. for (int i = 0; i < n; i++) 的写法,比起 for (int i = 0; i <= n - 1; i++),能够直接看出有 n - 0 = n 个数据,更为直观

  2. 二维数组 a[m][n] 的内存寻址公式

a[i][j]_address = base_address + (i * n + j) * type_size
  1. JVM 标记清除算法:运行在 JVM 上的 Java 程序,若堆内存被耗尽,则 GC 线程被触发并将程序暂停,执行标记/清除,再让程序恢复运行
  • 标记:遍历所有的 GC Roots,将所有 GC Roots 可达的对象标记为存活
  • 清除:遍历堆中所有对象,将没有标记的对象清除

缺点:1.效率问题,标记和清除过程效率不高;2.空间问题,产生不连续的内存空间碎片


你可能感兴趣的:(【学习笔记】《数据结构与算法之美》基础篇:数组)