算法与数据结构学习笔记(1)

87 阅读6分钟

算法与数据结构学习笔记(1)

1.1初识算法

1.1.1算法定义

算法(algorithm) 是在有限时间内解决特定问题的一组指令或操作步骤,它具有以下特性。

  • 问题是明确的,包含清晰的输入和输出定义。
  • 具有可行性,能够在有限步骤、时间和内存空间下完成。
  • 各步骤都有确定的含义,在相同的输入和运行条件下,输出始终相同。

1.1.2数据结构定义

数据结构(data structure) 是组织和存储数据的方式,涵盖数据内容、数据之间关系和数据操作方法,它具有以下设计目标。

  • 空间占用尽量少,以节省计算机内存。
  • 数据操作尽可能快速,涵盖数据访问、添加、删除、更新等。
  • 提供简洁的数据表示和逻辑信息,以便算法高效运行。

//数据结构设计是一个充满权衡的过程

2.1复杂度分析

2.1.1复杂度理论估算

时间复杂度和空间复杂度

2.1.2定义

它描述了随着输入数据大小的增加,算法执行所需时间和空间的增长趋势。仅通过一些计算来评估算法的效率。

2.2迭代与递归

2.2.1迭代

迭代(iteration)是一种重复执行某个任务的控制结构。

1.for循环

/* for 循环 */
int forLoop(int n) {
    int res = 0;
    // 循环求和 1, 2, ..., n-1, n
    for (int i = 1; i <= n; i++) {
        res += i;
    }
    return res;
}

2.while循环

/* while 循环 */
int whileLoop(int n) {
    int res = 0;
    int i = 1; // 初始化条件变量
    // 循环求和 1, 2, ..., n-1, n
    while (i <= n) {
        res += i;
        i++; // 更新条件变量
    }
    return res;
}

while 循环比 for 循环的自由度更高。在 while 循环中,我们可以自由地设计条件变量的初始化和更新步骤。

/* while 循环(两次更新) */
int whileLoopII(int n) {
    int res = 0;
    int i = 1; // 初始化条件变量
    // 循环求和 1, 4, 10, ...
    while (i <= n) {
        res += i;
        // 更新条件变量
        i++;
        i *= 2;
    }
    return res;
}

以上两者时间复杂度与n成线性关系

3.嵌套循环

/* 双层 for 循环 */
String nestedForLoop(int n) {
    StringBuilder res = new StringBuilder();
    // 循环 i = 1, 2, ..., n-1, n
    for (int i = 1; i <= n; i++) {
        // 循环 j = 1, 2, ..., n-1, n
        for (int j = 1; j <= n; j++) {
            res.append("(" + i + ", " + j + "), ");
        }
    }
    return res.toString();//n=2,(1,1),(1,2),(2,1),(2,2)
}

此时的时间复杂度与n^2成正比,每一次嵌套都是一次“升维”,将会使时间复杂度提高至“立方关系”“四次方关系”,以此类推。

2.2.2递归

定义

递归(recursion) 是一种算法策略,通过函数调用自身来解决问题。它主要包含两个阶段。

  1. :程序不断深入地调用自身,通常传入更小或更简化的参数,直到达到“终止条件”。
  2. :触发“终止条件”后,程序从最深层的递归函数开始逐层返回,汇聚每一层的结果。

而从实现的角度看,递归代码主要包含三个要素。

  1. 终止条件:用于决定什么时候由“递”转“归”。
  2. 递归调用:对应“递”,函数调用自身,通常输入更小或更简化的参数。
  3. 返回结果:对应“归”,将当前递归层级的结果返回至上一层。

/* 递归 */
int recur(int n) {
    // 终止条件
    if (n == 1)
        return 1;
    // 递:递归调用
    int res = recur(n - 1);
    // 归:返回结果
    return n + res;
}//对1+2+...+n进行求和

递归就是将问题分解为子问题,不断分解下去,直至基本情况时终止

1. 调用栈

  • 函数的上下文数据都存储在称为“栈帧空间”的内存区域中,直至函数返回后才会被释放。因此,递归通常比迭代更加耗费内存空间
  • 递归调用函数会产生额外的开销。因此递归通常比循环的时间效率更低

在实际中,编程语言允许的递归深度通常是有限的(默认Java虚拟机的递归深度在6246左右,超出会报栈溢出错误)

2. 尾递归

如果函数在返回前的最后一步才进行递归调用,则该函数可以被编译器或解释器优化,使其在空间效率上与迭代相当。这种情况被称为尾递归(tail recursion)

  • 普通递归:当函数返回到上一层级的函数后,需要继续执行代码,因此系统需要保存上一层调用的上下文。
  • 尾递归:递归调用是函数返回前的最后一个操作,这意味着函数返回到上一层级后,无须继续执行其他操作,因此系统无须保存上一层函数的上下文。
/* 尾递归 */
int tailRecur(int n, int res) {
    // 终止条件
    if (n == 0)
        return res;
    // 尾递归调用
    return tailRecur(n - 1, res + n);
}

原理:“递”的过程与普通递归一样,但是用一个变量来记住每级调用得到的值(代替“归”的操作),此时返回值的时候就可以不需要“递”时产生的上下文,此时就可以逐级快速返回第一级调用并返回值

注:有些语言如Python默认不支持尾递归优化

3. 递归树

斐波那契数列

/* 斐波那契数列:递归 */
int fib(int n) {
    // 终止条件 f(1) = 0, f(2) = 1
    if (n == 1 || n == 2)
        return n - 1;
    // 递归调用 f(n) = f(n-1) + f(n-2)
    int res = fib(n - 1) + fib(n - 2);
    // 返回结果 f(n)
    return res;
}

把普通递归合在一起形成一棵树即一棵层数为 n 的递归树(recursion tree)。

2.2.3 两者对比

迭代递归
实现方式循环结构函数调用自身
时间效率效率通常较高,无函数调用开销每次函数调用都会产生开销
内存使用通常使用固定大小的内存空间累积函数调用可能使用大量的栈帧空间
适用问题适用于简单循环任务,代码直观、可读性好适用于子问题分解,如树、图、分治、回溯等,代码结构简洁、清晰

内在联系

以上述递归函数为例,求和操作在递归的“归”阶段进行。这意味着最初被调用的函数实际上是最后完成其求和操作的,这种工作机制与栈的“先入后出”原则异曲同工

因此,我们可以使用一个显式的栈来模拟调用栈的行为,从而将递归转化为迭代形式:

/* 使用迭代模拟递归 */
int forLoopRecur(int n) {
    // 使用一个显式的栈来模拟系统调用栈
    Stack<Integer> stack = new Stack<>();
    int res = 0;
    // 递:递归调用
    for (int i = n; i > 0; i--) {
        // 通过“入栈操作”模拟“递”
        stack.push(i);
    }
    // 归:返回结果
    while (!stack.isEmpty()) {
        // 通过“出栈操作”模拟“归”
        res += stack.pop();
    }
    // res = 1+2+3+...+n
    return res;
}

注意:实际生产中不一定需要此类操作,此处仅为演示