一、复杂度分析(1)

  1. 复杂度分析相比于事后统计法,能够不依赖于运行环境和数据规模的大小,而计算程序的运行效率。
  2. 时间复杂度大O表示法 T(n) = O(f(n)) n 表示数据规模的大小;f(n) 表示每行代码执行的次数总和。O表示代码的执行时间 T(n) 与 f(n) 表达式成正比,由于f(n)可以忽略低阶和常数项,所以计算的技巧:只计算循环最多、量级最大的那段代码;嵌套代码的复杂度等于内外复杂度的乘积
  3. 常用时间复杂度 O(1) 、 O(logn)、 O(nlogn)、O(m+n)、O(m*n)
  4. 空间复杂度分析: 常见的空间复杂度就是 O(1)、O(n)、O(n2)。我们说空间复杂度的时候,是指除了原本的数据存储空间外,算法运行还需要额外的存储空间。

二、复杂度分析(2)

几种时间复杂度分析

  1. 最好和最坏情况时间复杂度,看两种极端情况的复杂度
  2. 平均时间复杂度, 用概率论,求平均情况的复杂度。即各种情况下出现的概率乘以各种情况下的复杂度,再加起来。得到 加权平均值。
  3. 使用场景: 多时候,我们使用一个复杂度就可以满足需求了。只有同一块代码在不同的情况下,时间复杂度有量级的差距,我们才会使用这三种复杂度表示法来区分。
  4. 均摊时间复杂度:不同时间复杂度情况的出现有一定的规律,看是否能将较高时间复杂度那次操作的耗时,平摊到其他那些时间复杂度比较低的操作上。而且,在能够应用均摊时间复杂度分析的场合,一般均摊时间复杂度就等于最好情况时间复杂度。

三、数组

  1. 定义: 线性表结构(逻辑上) + 连续的存储空间(存储方式上) + 存储相同的数据类型。
    数组支持随机访问,根据下标随机访问的时间复杂度为 O(1)。由查找第i个数据的公式为:
    a[i]_address = base_address + i * data_type_size
    二位数组的寻址公式为,对于数组 m*n
    a[i]_address = base_address + (i n + j) data_type_size
  2. 缺点: 插入、删除需要移动数据,复杂度O(n)。
  3. 高级语言中的‘数组’往往是对数组进行封装,或者使用非数组的方式去实现(JavaScript的数组其实是对象),例如封装了删除、插入等操作的细节、支持动态扩容。

四、链表(上)

  1. 定义: 线性表结构(逻辑上) + 非连续的存储空间(存储方式上)
  2. 分类: 单链表、双向链表、循环链表
  3. 循环链表是一种特殊的单向或双向链表。
  4. 链表相比数组,随机访问更麻烦。数组的劣势在于需要连续的存储空间,扩容需要做数据迁移。链表更耗空间。
  5. 双向链表对于单向链表的优势:在删除特定指向的节点或者在特定指向的节点钱插入时,不用遍历查找其前驱节点。

五、链表(下)

  1. 理解指针或引用的含义: 指针是变量(链表上的结点)的地址,它可以访问结点。而结点上又存储着一个指针地址。所以指针地址既是结点上的一个数据,又可以通过它访问另一个结点。
  2. 警惕指针丢失和内存泄漏:这一点要注意的是指针指向语句的顺序
  3. 利用哨兵简化实现难度: 增加无用的哨兵结点,使得操作更统一,不需要对链表为空(对于插入)或只有一个 元素(对于删除)的情况作额外判断。
  4. 重点留意边界条件处理,4个边界条件:
    如果链表为空时,代码是否能正常工作?
    如果链表只包含一个节点时,代码是否能正常工作?
    如果链表只包含两个节点时,代码是否能正常工作?
    代码逻辑在处理头尾节点时是否能正常工作?
  5. 举例画图, 释放一些脑容量,留更多的给逻辑思考

六、栈

一、栈是一种“操作受限”的线性表,只允许在一端插入和删除数据。可以使用数组或链表来实现。理论上数组或链表可以替代栈的功能,而栈是对特定场景的抽象,暴露的可操作接口少,比较可控。
二、复杂度分析

  1. 普通栈: 时间空间复杂度O(1)
  2. 可动态扩容的数组实现的栈: 入栈均摊时间复杂度就为 O(1)

三、应用:函数调用栈、编译器求和、检查括号匹配

七、队列

一、队列和栈类似,是一种操作受限的线性表数据结构
二、队列使用数组实现,随着入队和出队操作整个队列会后移,这是需要在入队发现队列满了时做数据迁移。假设head 指针和 tail 指针指向第一个结点和最后一个结点,n表示队列大小。 tail==n 时,会有数据搬移操作。
三、循环队列: 队空的判断条件是 head == tail,当队满时,(tail+1)%n=head
四、应用:使用阻塞队列,就是在队列为空的时候,从队头取数据会被阻塞。因为此时还没有数据可取,直到队列中有了数据才能返回;如果队列已经满了,那么插入数据的操作就会被阻塞,直到队列中有空闲位置后再插入数据,然后再返回。可以实现一个“生产者 - 消费者模型”。

八、递归

一、用递归来解决的问题要满足三个条件:

  1. 一个问题的解可以分解为几个子问题的解
  2. 这个问题与分解之后的子问题,除了数据规模不同,求解思路完全一样
  3. 存在递归终止条件

二、编写递归函数的思路:分析问题与子问题的关系,求出递归公式,找到递归的终止条件。编写递归代码的关键是,只要遇到递归,我们就把它抽象成一个递推公式,不用想一层层的调用关系,不要试图用人脑去分解递归的每个步骤。
三、递归的例子:斐波那契数列、 阶乘
四、递归的优化

  1. 预防堆栈溢出,可以计算递归的深度,限制递归层数。或者使用尾递归。
  2. 减少重复计算:由于递归过程中存在函数值重复计算的问题,我们可以将计算过的值存起来,当调用函数的参数相同时,就不必重复计算了
  3. 使用循环代替递归,不过复杂度高,不够简洁直观

toln
49 声望1 粉丝

前端开发@深圳


引用和评论

0 条评论