数据结构与算法笔记(2)

69 阅读7分钟

空间复杂度

用于衡量算法占用内存空间随着数据量变大时的增长趋势。这个概念与时间复杂度非常类似,只需将“运行时间”替换为“占用内存空间”

相关空间:

输入空间:存储算法的输入数据

暂存空间:存储算法在 运行过程中的变量对象函数以及上下文数据

输出空间:存储输出数据:

一般而言空间复杂度统计的是后面两个

而暂存空间又分为数据、栈帧和指令三个空间。一般统计后面两个

计算方法:

一般而言在推算算法的空间复杂度只看最差因为要保证内存的绝对安全。一般而言最差体现在最差输入数据和算法运行时候的峰值内存

常见类型:

常数阶<对数阶<线性阶<平方阶<指数阶

数据结构

数据结构分类:

一般的分类方法有两个维度,可以从逻辑结构和物理结构来进行分类。

逻辑结构:

线性结构和非线性,指是不是线性排列的,非线性的排列可能有树,图,堆,哈希表等等

物理结构:

一般看在内存空间中的排布是不是连续的,有些是连续的,比如数组,有些是非连续的,比如链表。所以说有些时候需要根据内存判断使用数据结构,如果缺连续的内存 空间的话就要使用一些分散的数据结构,从底层决定了数据在计算机中访问更新增删的方法。所有的数据结构都是基于数组,链表或者二者的组合而实现的

基本数据类型:

基本数据类型是 CPU 可以直接进行运算的类型,在算法中直接被使用,主要包括以下几种。

  • 整数类型 byteshortintlong
  • 浮点数类型 floatdouble ,用于表示小数。
  • 字符类型 char ,用于表示各种语言的字母、标点符号甚至表情符号等。
  • 布尔类型 bool ,用于表示“是”与“否”判断。

数组与链表

数组:

数组是一种线性数据结构,也就是说它存在于一段连续的内存空间中,而元素在数组中的位置则称之为索引。

数组的增删改查:

增:

数组在增加一个元素进去的时候,需要把后面所有的元素都后移一位,又因为数组的长度一般是固定的(传统意义的数组),所以最后一位的元素就会被挤出去变得无意义。其算法的时间复杂度是o(n);

删:

与增同理,在中间删除一个元素的时候,数组后面的所有元素都要往前移一位,所以会造成一段内存的无意义(浪费)时间复杂度同样是o(n)。

查:

通过遍历数组可以实现查询,用一个for循环每轮判断是否匹配即可,时间复杂度为O(n)

改:

数组因为是一段连续的内存空间,所以锁定某个元素的时候非常简单,只需要做一次计算即可,这也是数组的优点之一,比如一个数组的开始位置是000,每个元素长度为2,则索引为3的元素位置为000+2*3=006,时间复杂度为O(1)。

数组的扩容:

数组的扩容需要重新分配一段内存为扩容后的大小以及把之前的数组一个一个复制过去,是一个O(n)操作,在数组很大的时候是很耗时的。

优点和局限性:

数组存储在连续的内存空间内,且元素类型相同。这种做法包含丰富的先验信息,系统可以利用这些信息来优化数据结构的操作效率。

  • 空间效率高:数组为数据分配了连续的内存块,无须额外的结构开销。
  • 支持随机访问:数组允许在 O(1) 时间内访问任何元素。
  • 缓存局部性:当访问数组元素时,计算机不仅会加载它,还会缓存其周围的其他数据,从而借助高速缓存来提升后续操作的执行速度。

连续空间存储是一把双刃剑,其存在以下局限性。

  • 插入与删除效率低:当数组中元素较多时,插入与删除操作需要移动大量的元素。
  • 长度不可变:数组在初始化后长度就固定了,扩容数组需要将所有数据复制到新数组,开销很大。
  • 空间浪费:如果数组分配的大小超过实际所需,那么多余的空间就被浪费了。

数组的应用:

  • 随机访问:如果我们想随机抽取一些样本,那么可以用数组存储,并生成一个随机序列,根据索引实现随机抽样。
  • 排序和搜索:数组是排序和搜索算法最常用的数据结构。快速排序、归并排序、二分查找等都主要在数组上进行。
  • 查找表:当需要快速查找一个元素或其对应关系时,可以使用数组作为查找表。假如我们想实现字符到 ASCII 码的映射,则可以将字符的 ASCII 码值作为索引,对应的元素存放在数组中的对应位置。
  • 机器学习:神经网络中大量使用了向量、矩阵、张量之间的线性代数运算,这些数据都是以数组的形式构建的。数组是神经网络编程中最常使用的数据结构。
  • 数据结构实现:数组可以用于实现栈、队列、哈希表、堆、图等数据结构。例如,图的邻接矩阵表示实际上是一个二维数组。

链表

什么是链表?

链表是一个通过初始化节点和构建节点之间的引用关系而形成的数据结构,通过指向next来依次访问所有的节点(在有指针的语言中是通过指针指向,在不支持指针的语言中是通过引用指向)

节点插入:

链表的节点插入相对简单,只需要改变插入节点的前一个节点的指向为插入节点以及让插入节点的指向是原有的下一个节点即可

/* 在链表的节点 n0 之后插入节点 P */
function insert(n0, P) {
    const n1 = n0.next;
    P.next = n1;
    n0.next = P;
}

节点删除:

和节点的插入大同小异,只需要把删除节点的前一个节点的指向指到删除节点的后一个节点即可

/* 删除链表的节点 n0 之后的首个节点 */
function remove(n0) {
    if (!n0.next) return;
    // n0 -> P -> n1
    const P = n0.next;
    const n1 = P.next;
    n0.next = n1;
}

节点访问:

链表的访问较为麻烦,需要从头节点开始向后遍历访问直到找到

/* 访问链表中索引为 index 的节点 */
function access(head, index) {
    for (let i = 0; i < index; i++) {
        if (!head) {
            return null;
        }
        head = head.next;
    }
    return head;
}

节点的查找:

遍历链表然后找到值为target的索引,输出该节点在链表中的索引,也是属于线性查找

/* 在链表中查找值为 target 的首个节点 */
function find(head, target) {
    let index = 0;
    while (head !== null) {
        if (head.val === target) {
            return index;
        }
        head = head.next;
        index += 1;
    }
    return -1;
}

数组与链表的对比

数组是连续存储,链表是分散存储

数字长度不可变,链表可变

数组元素内存占用少,但是可能浪费,链表占用相对较多(因为链表多一个指向引用的元素)