想刷算法,但又苦于不知道如何开始的同学该怎么做?

2,458 阅读10分钟

算法是所有程序员们逃不掉的一座大山,如何开始学习算法是许多人面临的一大难题。直接刷力扣等平台的题目可能会让人感到挫败,因为题目难度较大,容易打击自信心。本文将为你提供一个循序渐进的学习指南,帮助你轻松上手算法学习。

首先你要知道算法是什么? 他是一种解决问题的方法或过程

我们要打好算法的基础再去刷题,先打地基,再建房子。这里我给大家介绍两个概念,时间复杂度、空间复杂度

时间复杂度和空间复杂度

学过数据结构的朋友们应该对这两个词不陌生,他两是算法分析中的两个重要概念。

时间复杂度

首先介绍的是时间复杂度,它是用来衡量算法执行时间随输入规模增长的变化趋势。它帮助我们评估算法的效率,特别是在输入规模非常大时的表现。

下面是常见的时间复杂度
  1. O(1) :常数时间复杂度,无论输入规模如何,执行时间不变。
  2. O(n) :线性时间复杂度,执行时间与输入规模成正比。
  3. O(log n) :对数时间复杂度,执行时间随着输入规模的增加而缓慢增长。
  4. O(n log n) :线性对数时间复杂度,介于线性和平方之间。
  5. O(n^2) :平方时间复杂度,执行时间与输入规模的平方成正比。
  6. O(n^3) :立方时间复杂度,执行时间与输入规模的立方成正比。
  7. O(2^n) :指数时间复杂度,执行时间随输入规模呈指数增长。
  8. O(n!) :阶乘时间复杂度,执行时间随输入规模呈阶乘增长。
计算时间复杂度的方法
  1. 确定执行次数 T(n) :计算代码的执行次数。全部加起来的。
  2. 抓主要矛盾:忽略系数和低阶项,只关注最高阶项。
  3. 得出时间复杂度 O(n) :由执行次数 T(n) 得到时间复杂度 O(n),表示执行次数的增长趋势。

示例

function solution(n) {
    var len = arr.length;  // 1

    for (let i = 1; i <= len; i++) {  // 1 + n+1 + n                       
        console.log(arr[i]);      // n
    }
}
// T(n)=1+1+n+1+n+n = 3n+3
// 代码执行次数 T(n)
solution()

在这个例子中,我们逐行计算了每一步的执行次数,有一个细节是 i<=len会执行 n+1 次,而不是n次,虽然第n+1次他不符合条件,但是仍然是指向了判断的。

下面是详细计算过程

  1. 确定执行次数 T(n)

    将上述每一步的执行次数相加:

  • 获取数组长度:1
  • 初始化循环变量:1
  • 条件判断:n + 1
  • 更新循环变量:n
  • 循环体内的操作:n

因此,总的执行次数 T(n) 为:T(n)=1+1+n+1+n+n = 3n+3

  1. 抓主要矛盾

    • 由于只有一次循环,没有嵌套循环或其他复杂操作,所以主要矛盾是 n 次执行。
  2. 得出时间复杂度 O(n)

    • 执行次数是 n,因此时间复杂度是 O(n)
这里是我们的两个小Tips:
边界理论:
  • 当输入规模无限大时,时间复杂度表达的是执行次数的增长趋势。
  • 因此,系数和低阶项可以忽略,只关注最高阶项。
算法的选择:
  • 算法不仅仅是解法,而是选择一个具有更优时间复杂度的解法。
  • 例如,冒泡排序的时间复杂度是 O(n^2) ,而快速排序的时间复杂度是 O(n log n) ,后者通常更高效。

空间复杂度

空间复杂度是用来衡量算法在运行过程中占用的额外空间的大小情况。它帮助我们评估算法对内存的使用情况。

常见的空间复杂度
  1. O(1) :常数空间复杂度,占用的空间不随输入规模变化。
  2. O(n) :线性空间复杂度,占用的空间与输入规模成正比。
  3. O(n^2) :平方空间复杂度,占用的空间与输入规模的平方成正比。
计算空间复杂度的方法
  1. 确定使用的额外空间:计算算法在运行过程中额外使用的空间。
  2. 忽略输入本身的大小:只关注额外使用的空间。
  3. 得出空间复杂度 O(n) :由额外使用的空间大小得到空间复杂度 O(n)。

示例

function traverse(arr) {
    const outlen = arr.length
    for (let i = 0; i < outlen; i++) {
        console.log(arr[i])
    }
}

大家能知道上面的代码的空间复杂度是多少吗,大家思考一下!

想必很多人会认为是 n+2 就是 O(n)

  • 数组 arr 占用的空间:n
  • 变量 outlen 占用的空间:1
  • 循环变量 i 占用的空间:1

但其实是错误的!这里的arr并不能算进去,这是因为我们关注的是算法在运行过程中额外占用的空间,而不包括输入数据本身占用的空间。这是因为输入数据通常是给定的,我们无法控制其大小,而额外空间是我们可以通过优化算法来减少的。

所以它真正的时间复杂度是 O(1)!

数据结构

介绍完了时间复杂度和空间复杂度,我们就可以继续介绍算法需要了解的 数据结构,了解常用的数据结构是提高我们算法能力的关键一步,所以接下来我们来讲一讲数据结构,我们也可从另外一个角度说数据结构是什么?他是ADT抽象数据结构

抽象数据类型(Abstract Data Type, ADT)是从数学角度描述数据类型的一种方式。它定义了一组数据和可以在这些数据上执行的操作,但不关心这些操作的具体实现细节。

ADT 的主要组成部分包括:

  1. 数据的存储结构:数据在内存中的物理表示方式。
  2. 特定的行为:可以在数据上执行的操作集合。
  3. 数据的约束条件:数据结构的规则和限制。

线性数据结构和非线性数据结构

我们讲回数据结构,它可以分为两类,线性非线性

线性数据结构
数组
  • 最简单最好用:数组是最基本和最容易理解的数据结构之一。
  • 内置方法很多,开箱即用:大多数编程语言提供了丰富的数组内置方法。
  • 建议优先考虑数组:在大多数情况下,数组是最优的选择,特别是当数据是连续存储且需要快速访问时。
  • 除非不连续:如果数据不是连续存储的,或者需要频繁插入和删除操作,数组可能不是最佳选择。
  • 有丰富的 API:数组提供了多种内置方法,如 pushpopshiftunshiftslicesplicemapfilterreduce 等。

想必数组大家都非常熟悉,在我们平常写代码时,基本上是天天见面,那你知道如何初始化数组吗?这个看起来很简单,但是也是面试官爱问的细节!

那你知道如何初始化数组吗?我们在JS语言中可以用两种方法

  1. let arr = new Array()
  2. let arr = []

在算法和数据结构的上下文中,const arr = [] 虽然可以创建一个数组但它缺乏一些关键的信息和约束条件,使其不够严谨。特别是当你需要初始化一个特定长度的数组时,使用 const arr = new Array(7) 是一个更合适的选择!!

访问数组

他们都可以创建一个数组。呢么如何访问数组呢,我们一般都是直接用数组下标,即 arr[index]

遍历数组

在 JavaScript 中,有多种方法可以遍历数组。每种方法都有其特定的用途和优势。以下是常见的几种遍历数组的方法:

  • for
  • foreach
  • map
  • filter
  • reduce
  • for of
  • for in

平常最常用的就是for,还有foreach,大家知道他们两兄弟,谁的性能更好吗,

const len = arr.length;
// 此循环   遍历
// for (let i = 0; i < len; i++) {
//     console.log(arr[i], i);
// }
arr.forEach((item, index) => {
    console.log(item, index);
})

其实很简单,因为foreach里面有回调函数

  • 函数调用开销forEach 方法内部会为数组中的每个元素调用一次回调函数。每次调用函数都会有一定的开销,包括函数调用的栈管理、参数传递和上下文切换。
  • 上下文切换:每次调用回调函数时,JavaScript 引擎需要保存当前的执行上下文,然后切换到回调函数的上下文,执行完后再恢复原来的上下文。

并且for他有更好的优化!

  • 更好的优化机会:传统的 for 循环通常更容易被解释器优化。现代 JavaScript 引擎(如 V8)可以更好地识别和优化简单的循环结构,从而提高性能。
链表
  • 节点 + 指针:每个节点包含数据和指向下一个节点的指针。
  • 动态插入和删除:插入和删除操作不需要移动其他元素,时间复杂度为 O(1)。
  • 不连续存储:节点在内存中不一定连续存储。
  • 后进先出(LIFO) :最后插入的元素最先被移除。
  • 栈顶操作:主要操作包括入栈(push)、出栈(pop)和获取栈顶元素(peek)。

非线性数据结构

非线性数据结构中的元素之间没有线性的前后关系,可以有多个前驱和后继。常见的非线性数据结构包括树、图和哈希表。

1. 树

特点

  • 层次结构:节点之间有父子关系。
  • 根节点:树的最顶端节点。
  • 叶子节点:没有子节点的节点。
  • 子树:每个节点都可以看作是一个子树的根。

常见类型

  • 二叉树:每个节点最多有两个子节点。
  • 二叉搜索树:左子树的所有节点值小于根节点值,右子树的所有节点值大于根节点值。
  • 平衡树:如 AVL 树、红黑树,保证树的高度平衡。
  • :完全二叉树,分为最大堆和最小堆。

应用场景

  • 文件系统:表示目录结构。
  • 数据库索引:加速数据查询。
  • 表达式解析:解析和计算表达式。
2. 图

特点

  • 节点和边:由节点(顶点)和边组成。
  • 复杂关系:节点之间可以有多条边,表示复杂的关系。

常见类型

  • 有向图:边有方向。
  • 无向图:边没有方向。
  • 加权图:边有权重。

应用场景

  • 社交网络:表示用户之间的关系。
  • 路径规划:寻找最短路径或最小生成树。
  • 依赖关系:表示任务之间的依赖关系。
3. 哈希表

特点

  • 哈希函数:通过哈希函数将键映射到数组的索引。
  • 快速查找:平均时间复杂度为 O(1)。

应用场景

  • 缓存:存储和快速检索数据。
  • 数据库索引:加速数据查询。
  • 集合操作:集合的交集、并集等操作。

END

算法纵然很难,但是如果你不去尝试你又怎么能知道呢,打好算法基础,再去循序渐进的刷算法,我相信大家都可以做到攀越这座高山!!