算法入门第一篇:算法核心:复杂度分析与数组基础

引言:为什么需要学习算法?

你可能也发现,即使是社招,面试官也时不时会抛出几道算法题,从简单的反转链表到复杂的动态规划。这常常让人感到困惑:我一个做游戏开发的,写好 Unity 的 C# 代码,实现好游戏逻辑就行了,为什么还要去死磕这些"看似"与游戏开发不沾边的算法题呢?

答案是:面试官考察的不是你背下了多少道题,而是你解决问题的思维方式和代码质量。

  1. 逻辑思维能力: 算法题往往需要你分解问题、抽象模型、设计解决方案,这和你在 Unity 中设计游戏系统、优化渲染流程的思维是相通的。

  2. 代码实现能力: 算法题要求你把想法清晰、高效地转化为代码,考察你对数据结构和基本语法的掌握程度。

  3. 分析和优化能力: 算法的精髓在于找到最优解。面试官希望看到你不仅能写出能跑的代码,还能分析它的效率瓶颈,并提出优化方案。这直接关系到游戏运行时的性能,比如一个寻路算法的效率就可能决定你的 AI 是流畅行动还是卡顿不堪。

  4. 学习和适应能力: 算法和数据结构是计算机科学的基石。掌握了这些基础,你就能更快地理解新的技术、新的框架,更好地适应不断变化的开发需求。

本系列教程的目的,就是帮助你从零开始,系统地理解经典算法题的解题思路,掌握那些能揭示一种通用模式而非仅仅是死记硬背的题目。我们将从最基础的复杂度分析和数组开始,一步步揭开算法的神秘面纱。

时间复杂度:衡量算法效率的标尺

当我们在编写代码时,我们不仅仅要考虑它能否解决问题,还要关注它解决问题的效率 。效率,通常通过时间复杂度空间复杂度来衡量。

什么是时间复杂度?

时间复杂度(Time Complexity)是衡量一个算法执行时间随输入规模增长而增长的趋势。它不是指算法执行的具体时间(因为这受到计算机硬件、编程语言、编译器等多种因素影响),而是指随着输入数据量 N 的增大,算法执行语句的总次数是如何变化的

我们通常使用大 O 标记法(Big O Notation)来表示时间复杂度。它表示的是算法执行时间的上界,即最坏情况下的时间增长趋势,它会忽略常数项和低阶项,只保留最高阶项。

为什么忽略常数项和低阶项?

因为当 N 足够大时,最高阶项对算法运行时间的影响是决定性的。例如,2N2+100N+500 和 N2,在 N=10000 时,N2 已经远大于 100N+500 了。所以我们只关心最高次幂的项。

常见时间复杂度分析

以下是几种常见的时间复杂度,从高效到低效排列:

  1. O(1):常数时间复杂度

    • 无论输入规模 N 多大,算法执行的步数总是固定的。

    • 示例: 访问数组中的任意元素、哈希表中插入/查找(平均情况)。

      C#

      复制代码
      int[] arr = {1, 2, 3, 4, 5};
      int value = arr[2]; // 直接通过索引访问,一步到位
  2. O(logn):对数时间复杂度

    • 算法的执行时间与输入规模的对数成正比。通常通过"折半"的方式来减少问题规模。

    • 示例: 二分查找(Binary Search)。每次搜索都将问题规模减半。

      C#

      复制代码
      // 假设在一个有序数组中查找某个值
      // 每次查找范围缩小一半,直到找到或范围为空
      // log2(N) 次操作
  3. O(n):线性时间复杂度

    • 算法的执行时间与输入规模 N 成正比。通常涉及对输入数据进行一次遍历。

    • 示例: 遍历数组、查找无序数组中的最大值。

      C#

      复制代码
      // 遍历数组中的所有元素
      for (int i = 0; i < n; i++) {
          // ...
      }
  4. O(nlogn):线性对数时间复杂度

    • 常见的"高效"排序算法的时间复杂度,如归并排序(Merge Sort)和快速排序(Quick Sort)。通常是 N 次对数操作的组合。

    • 示例: 归并排序,每次将数组对半分(logn 层),每层合并操作需要 O(n) 时间。

  5. O(n2):平方时间复杂度

    • 算法的执行时间与输入规模 N 的平方成正比。通常涉及嵌套循环,外层循环 N 次,内层循环也 N 次。

    • 示例: 冒泡排序(Bubble Sort)、选择排序(Selection Sort)、两层嵌套循环遍历二维数组。

      C#

      复制代码
      // 两层嵌套循环
      for (int i = 0; i < n; i++) {
          for (int j = 0; j < n; j++) {
              // ...
          }
      }
  6. O(2n):指数时间复杂度

    • 算法的执行时间随输入规模 N 的增长呈指数级增长。通常出现在递归问题中,且存在大量重复计算(未优化)。

    • 示例: 未优化的斐波那契数列递归计算、某些穷举搜索问题。这种复杂度通常无法接受。

  7. O(n):阶乘时间复杂度

    • 算法的执行时间随输入规模 N 的增长呈阶乘级增长。最差的复杂度,几乎只出现在穷举所有排列组合的问题中。

    • 示例: 旅行商问题(Traveling Salesman Problem)的暴力解法。

如何分析一段代码的时间复杂度?
  1. 关注循环: 循环的次数是影响复杂度的主要因素。

    • 如果循环次数是常数,则 O(1)。

    • 如果循环次数与 N 成正比,则 O(N)。

    • 如果有多层嵌套循环,复杂度是各层循环次数的乘积。

  2. 关注递归: 递归的深度和每次递归调用的次数决定了复杂度。

  3. 关注分支: 条件语句(if/else)不增加复杂度(除非分支内部包含循环或递归)。

  4. 忽略常数和低阶项: 例如 O(2N) 简化为 O(N),O(N2+N) 简化为 O(N2)。

举例分析:

C#

复制代码
// 例子1: O(1)
int Sum(int a, int b) {
    return a + b; // 只有一步操作
}

// 例子2: O(n)
void PrintArray(int[] arr) {
    for (int i = 0; i < arr.Length; i++) { // 循环 N 次
        Console.WriteLine(arr[i]);
    }
}

// 例子3: O(n^2)
void MatrixMultiply(int[,] matrixA, int[,] matrixB) {
    int n = matrixA.GetLength(0);
    for (int i = 0; i < n; i++) {       // 外层循环 N 次
        for (int j = 0; j < n; j++) {   // 内层循环 N 次
            // ... 常数操作
        }
    }
}

// 例子4: O(log n)
int BinarySearch(int[] arr, int target) {
    int left = 0;
    int right = arr.Length - 1;
    while (left <= right) { // 循环次数取决于每次对半分的次数
        int mid = left + (right - left) / 2;
        if (arr[mid] == target) {
            return mid;
        } else if (arr[mid] < target) {
            left = mid + 1;
        } else {
            right = mid - 1;
        }
    }
    return -1;
}

空间复杂度:衡量算法内存消耗的标尺

**空间复杂度(Space Complexity)**是衡量一个算法在执行过程中临时占用存储空间大小的趋势。它同样用大 O 标记法表示。

  • O(1):常数空间复杂度

    • 算法执行所需的额外空间不随输入规模 N 变化。

    • 示例: 只使用少量变量的计算。

  • O(n):线性空间复杂度

    • 算法执行所需的额外空间与输入规模 N 成正比。

    • 示例: 创建一个与输入数组大小相同的辅助数组。

  • O(n2):平方空间复杂度

    • 算法执行所需的额外空间与输入规模 N 的平方成正比。

    • 示例: 创建一个 NtimesN 的二维数组。

如何分析空间复杂度?

  1. 变量: 声明的变量通常占用常数空间。

  2. 数据结构: 算法中使用的额外数组、链表、哈希表等数据结构所占空间。

  3. 递归调用栈: 递归深度决定了调用栈占用的空间。

举例分析:

C#

复制代码
// 例子1: O(1) 空间复杂度
int Sum(int a, int b) {
    int result = a + b; // 只使用一个额外变量 result
    return result;
}

// 例子2: O(n) 空间复杂度
int[] CopyArray(int[] arr) {
    int[] newArr = new int[arr.Length]; // 创建了一个大小为 N 的新数组
    for (int i = 0; i < arr.Length; i++) {
        newArr[i] = arr[i];
    }
    return newArr;
}

// 例子3: 递归的 O(n) 空间复杂度 (递归栈)
int Fibonacci(int n) {
    if (n <= 1) return n;
    return Fibonacci(n - 1) + Fibonacci(n - 2); // 递归深度为 N
}

理解时间复杂度和空间复杂度,是你在算法面试中能够与面试官有效交流、并提出优化方案的基础。很多时候,空间换时间是一种常见的优化策略,比如利用哈希表来将 O(n2) 的查找优化到 O(n) 甚至 O(1)。

数组:最基础也是最重要的基石

数组(Array)是所有编程语言中最基本、最常用的数据结构之一。在 C# 中,你可以声明 int[]string[] 甚至是 GameObject[]。它是一系列相同类型元素的有序集合,并且通常是在内存中连续存储的

数组的特性
  1. 连续存储: 数组的元素在内存中是紧密排列的。

  2. 随机访问: 由于连续存储,可以通过索引 O(1) 时间直接访问任意元素。这是数组最大的优势。

  3. 定长(通常): 在很多语言中(包括 C# 的原生数组),数组一旦创建,其大小就固定了。如果需要改变大小,通常需要创建一个新的数组并复制旧数组的元素。

  4. 插入和删除效率低: 由于连续存储的特性,在数组中间插入或删除元素,需要移动后续所有元素,其时间复杂度为 O(N)。

数组的基本操作
  • 创建与初始化:

    C#

    复制代码
    int[] numbers = new int[5]; // 创建一个包含5个整数的数组,默认值为0
    int[] scores = { 90, 85, 92, 78, 95 }; // 创建并初始化
  • 访问元素: 通过索引访问,索引从 0 开始。

    C#

    复制代码
    int firstScore = scores[0]; // 90
    int thirdScore = scores[2]; // 92
  • 遍历数组:

    C#

    复制代码
    // for 循环
    for (int i = 0; i < scores.Length; i++) {
        Console.WriteLine(scores[i]);
    }
    // foreach 循环 (适用于只读遍历)
    foreach (int score in scores) {
        Console.WriteLine(score);
    }
  • 修改元素:

    C#

    复制代码
    scores[1] = 88; // 将第二个元素从 85 修改为 88
  • 插入/删除元素(中低效率操作):

    这些操作需要手动实现元素的移动。例如,在索引 i 处插入元素,需要将 i 及之后的所有元素向后移动一位;删除同理。

    C#

    复制代码
    // 假设在索引为1的位置插入99
    // 实际操作会创建一个更大的新数组,然后复制,或者手动移动
    // 这里只示意逻辑,实际C#中可使用List<T>
    // 对于原生数组,插入删除会比较麻烦,所以面试题通常是要求原地修改或者移除。
经典面试题与解法:数组篇

理解数组的基本特性后,我们来看几个经典的面试题,它们不仅考察你对数组的掌握,更引入了重要的解题技巧。


1. 两数之和 (Two Sum)
  • 题目描述:

    给定一个整数数组 nums 和一个整数目标值 target,请你在该数组中找出和为目标值 target 的那两个整数,并返回它们的数组下标。

    你可以假设每种输入只会对应一个答案。但是,数组中同一个元素不能使用两遍。

    示例:

    输入: nums = [2, 7, 11, 15], target = 9

    输出: [0, 1]

    解释: 因为 nums[0] + nums[1] == 9 ,所以返回 [0, 1]。

  • 解题思路 1:暴力枚举 (Brute Force)

    最直观的方法是遍历数组中每一个元素 x,然后再次遍历后续元素 y,检查它们是否满足 x + y == target。

    • 代码实现:

      C#

      复制代码
      public int[] TwoSumBruteForce(int[] nums, int target) {
          for (int i = 0; i < nums.Length; i++) {
              for (int j = i + 1; j < nums.Length; j++) { // j 从 i+1 开始,避免重复使用同一元素
                  if (nums[i] + nums[j] == target) {
                      return new int[] { i, j };
                  }
              }
          }
          // 题目保证有解,所以理论上不会执行到这里
          throw new ArgumentException("No two sum solution");
      }
    • 复杂度分析:

      • 时间复杂度:O(N2)。外层循环执行 N 次,内层循环执行 N−1,N−2,...,1 次,大约是 NtimesN/2 次操作,所以是 O(N2)。

      • 空间复杂度:O(1)。只使用了常数个额外变量。

  • 解题思路 2:哈希表优化 (Hash Table Optimization) - 空间换时间

    暴力解法之所以慢,是因为我们每次都要遍历内部循环来寻找 target - x。如果能更快地找到这个"差值"呢?

    我们可以利用哈希表(Dictionary/HashMap)的平均 O(1) 查找效率。

    遍历数组,对于每个元素 num:

    1. 计算**complement = target - num**。

    2. 检查哈希表中是否已经存在 complement

      • 如果存在,说明我们找到了两个数,直接返回 complement 的索引和当前 num 的索引。

      • 如果不存在,将当前 num 和它的索引存入哈希表,以便后续元素查找。

    • 代码实现:

      C#

      复制代码
      using System.Collections.Generic; // 引入 Dictionary
      
      public int[] TwoSumHashTable(int[] nums, int target) {
          // key: 数组元素的值, value: 数组元素的索引
          Dictionary<int, int> map = new Dictionary<int, int>();
      
          for (int i = 0; i < nums.Length; i++) {
              int complement = target - nums[i];
              // 检查哈希表中是否包含 complement
              if (map.ContainsKey(complement)) {
                  // 如果包含,说明找到了,返回 complement 的索引和当前元素的索引
                  return new int[] { map[complement], i };
              }
              // 如果不包含,将当前元素及其索引加入哈希表
              map[nums[i]] = i;
          }
          throw new ArgumentException("No two sum solution");
      }
    • 复杂度分析:

      • 时间复杂度:O(N)。我们只遍历了一次数组,哈希表的查找和插入操作平均都是 O(1)。

      • 空间复杂度:O(N)。在最坏情况下,哈希表会存储数组中所有的元素。

  • 思考: 这个题目完美地体现了**"空间换时间"**的策略。通过牺牲额外的内存空间(哈希表),我们极大地提升了算法的运行速度。在面试中,当你给出暴力解后,面试官通常会期待你提出这种优化方案。


2. 移除元素 (Remove Element)
  • 题目描述:

    给你一个数组 nums 和一个值 val,你需要原地移除所有数值等于 val 的元素,并返回移除后数组的新长度。

    不要使用额外的数组空间,你必须仅使用 O(1) 额外空间并原地修改输入数组。元素的顺序可以改变。你不需要考虑数组中超出新长度后面的元素。

    示例:

    输入: nums = [3, 2, 2, 3], val = 3

    输出: 2, nums = [2, 2, _, _] (新长度为 2,原数组的前两个元素是 2)

  • 解题思路:快慢指针 (Two Pointers - Fast and Slow)

    "原地修改"且"O(1) 空间"是这道题目的关键提示。快慢指针是解决这类数组原地修改问题的利器。

    我们维护两个指针:

    • 快指针 (Fast Pointer) :遍历整个数组,寻找不等于 val 的元素。

    • 慢指针 (Slow Pointer) :指向当前应该放置不等于 val 的元素的位置。

    遍历过程中:

    1. 如果快指针指向的元素不等于 val,说明它是一个需要保留的元素。我们将其复制到慢指针指向的位置,然后同时将快指针和慢指针都向前移动一步。

    2. 如果快指针指向的元素等于 val,说明它是一个需要移除的元素。我们跳过它,只将快指针向前移动一步。慢指针保持不动,因为它指向的位置还没有被填充。

    最终,慢指针所指向的位置就是新数组的长度。

    • 代码实现:

      C#

      复制代码
      public int RemoveElement(int[] nums, int val) {
          int slow = 0; // 慢指针,指向下一个要放置非val元素的位置
          // fast 从 0 开始遍历整个数组
          for (int fast = 0; fast < nums.Length; fast++) {
              if (nums[fast] != val) { // 如果快指针指向的元素不是 val
                  nums[slow] = nums[fast]; // 将其复制到慢指针位置
                  slow++; // 慢指针向前移动
              }
              // 如果 nums[fast] == val,则 fast 指针继续前进,slow 指针不动,相当于跳过了 val
          }
          return slow; // slow 指针最终的位置就是新数组的长度
      }
    • 复杂度分析:

      • 时间复杂度:O(N)。快指针遍历了整个数组一次。

      • 空间复杂度:O(1) 。只使用了两个额外变量(fastslow)。

  • 变种:移除有序数组中的重复项 (Remove Duplicates from Sorted Array)

    这道题和 Remove Element 思路几乎完全一样,只是判断条件从 nums[fast] != val 变成了 nums[fast] != nums[slow](确保只有不重复的元素才被保留)。这进一步强化了快慢指针在"原地"操作中的通用性。


3. 旋转数组 (Rotate Array)
  • 题目描述:

    给定一个整数数组 nums,将数组中的元素向右轮转 k 个位置,其中 k 是非负数。

    示例:

    输入: nums = [1, 2, 3, 4, 5, 6, 7], k = 3

    输出: [5, 6, 7, 1, 2, 3, 4]

    解释:

    向右旋转 1 步: [7, 1, 2, 3, 4, 5, 6]

    向右旋转 2 步: [6, 7, 1, 2, 3, 4, 5]

    向右旋转 3 步: [5, 6, 7, 1, 2, 3, 4]

  • 解题思路 1:使用额外数组 (Using Extra Array)

    最直接的方式是创建一个新的数组,然后将原数组的元素按照旋转后的顺序填充到新数组中。

    • 思路: 元素 nums[i] 旋转 k 次后,它的新位置是 (i + k) % N(其中 N 是数组长度)。

    • 代码实现:

      C#

      复制代码
      public void RotateExtraArray(int[] nums, int k) {
          int n = nums.Length;
          k %= n; // 确保 k 在 [0, n-1] 范围内
          int[] newArr = new int[n];
          for (int i = 0; i < n; i++) {
              newArr[(i + k) % n] = nums[i]; // 将原位置 i 的元素放到新位置 (i+k)%n
          }
          // 将新数组的内容复制回原数组
          for (int i = 0; i < n; i++) {
              nums[i] = newArr[i];
          }
      }
    • 复杂度分析:

      • 时间复杂度:O(N)。两次遍历数组。

      • 空间复杂度:O(N)。创建了一个新的辅助数组。

  • 解题思路 2:三次反转 (Three Reversals) - 原地操作的优雅解法

    这是一种非常巧妙且高效的原地旋转方法,其核心思想是:

    1. 反转整个数组。

    2. 反转前 k 个元素。

    3. 反转剩下 N-k 个元素。

    我们通过一个例子来理解:nums = [1, 2, 3, 4, 5, 6, 7], k = 3

    1. 反转整个数组: [7, 6, 5, 4, 3, 2, 1]

    2. 反转前 k 个元素 (即前 3 个): [5, 6, 7, 4, 3, 2, 1]

    3. 反转剩下 N-k 个元素 (即后 4 个): [5, 6, 7, 1, 2, 3, 4]

    得到了正确结果!这种方法的优点是原地操作且高效

    • 辅助函数:反转数组的一部分

      C#

      复制代码
      // 辅助函数:反转数组中从 start 到 end 范围的元素
      private void Reverse(int[] nums, int start, int end) {
          while (start < end) {
              int temp = nums[start];
              nums[start] = nums[end];
              nums[end] = temp;
              start++;
              end--;
          }
      }
    • 代码实现:

      C#

      复制代码
      public void RotateThreeReversals(int[] nums, int k) {
          int n = nums.Length;
          k %= n; // 处理 k > n 的情况
      
          // 1. 反转整个数组: [1,2,3,4,5,6,7] -> [7,6,5,4,3,2,1]
          Reverse(nums, 0, n - 1);
          // 2. 反转前 k 个元素: [7,6,5,4,3,2,1] -> [5,6,7,4,3,2,1]
          Reverse(nums, 0, k - 1);
          // 3. 反转剩下的 n-k 个元素: [5,6,7,4,3,2,1] -> [5,6,7,1,2,3,4]
          Reverse(nums, k, n - 1);
      }
    • 复杂度分析:

      • 时间复杂度:O(N) 。每次 Reverse 操作都是线性时间,总共执行三次。

      • 空间复杂度:O(1)。完全是原地操作,没有使用额外空间。

  • 思考: 三次反转法是一个非常经典的技巧,它展示了通过巧妙的步骤组合,可以在不使用额外空间的情况下解决复杂问题。在面试中,如果你能给出这种解法,会给面试官留下深刻印象。


4. 合并两个有序数组 (Merge Sorted Array)
  • 题目描述:

    给你两个按 非递减顺序 排列的整数数组 nums1 和 nums2,另有两个整数 m 和 n ,分别表示 nums1 和 nums2 中的元素数目。

    请你将 nums2 合并到 nums1 中,使合并后的数组同样按 非递减顺序 排列。

    注意:最终,合并后数组不应由函数返回,而是存储在数组 nums1 中。为了应对这种情况,nums1 的初始长度为 m + n,其中前 m 个元素表示应合并的元素,后 n 个元素为 0 ,应忽略。nums2 的长度为 n。

    示例:

    输入:nums1 = [1, 2, 3, 0, 0, 0], m = 3, nums2 = [2, 5, 6], n = 3

    输出:[1, 2, 2, 3, 5, 6]

  • 解题思路:双指针 (从后往前合并)

    这道题的难点在于原地合并到 nums1 中,并且 nums1 的后 n 位是空的。如果我们从前往后合并,那么 nums1 中已有的元素可能会被 nums2 中的元素覆盖掉,导致数据丢失。

    关键在于从后往前合并 。由于 nums1 后半部分有足够的空间,我们可以将 nums1nums2 的最大元素(因为都是有序的)从末尾开始逐一比较,然后将较大的那个放到 nums1 的末尾(也就是合并后数组的末尾)。

    我们维护三个指针:

    • p1:指向 nums1 中有效部分的末尾(即 m-1)。

    • p2:指向 nums2 的末尾(即 n-1)。

    • p:指向合并后数组的末尾(即 m+n-1)。

    从后往前遍历:

    1. p1p2 都还在有效范围内时:

      • 比较 nums1[p1]nums2[p2]

      • 将较大的元素放到 nums1[p] 的位置。

      • 相应的指针 (p1p2) 和 p 都向前移动一步。

    2. p1 已经越界,但 p2 还在有效范围内时:

      • 说明 nums1 的有效元素已经全部处理完毕,但 nums2 中还有剩余元素。

      • nums2 中剩余的元素全部复制到 nums1 的剩余空白位置。

      • p2p 都向前移动一步。

    • 代码实现:

      C#

      复制代码
      public void Merge(int[] nums1, int m, int[] nums2, int n) {
          int p1 = m - 1; // nums1 的有效末尾指针
          int p2 = n - 1; // nums2 的末尾指针
          int p = m + n - 1; // 合并后数组的末尾指针
      
          // 当 nums2 还有元素未处理时,继续循环
          while (p2 >= 0) {
              // 如果 nums1 还有有效元素,并且 nums1[p1] >= nums2[p2]
              // 注意 p1 >= 0 的条件,避免 nums1 已经处理完而访问越界
              if (p1 >= 0 && nums1[p1] >= nums2[p2]) {
                  nums1[p] = nums1[p1]; // 将 nums1 中的较大元素放到合并位置
                  p1--; // nums1 指针前移
              } else {
                  // 否则,nums2[p2] 更大,或者 nums1 已经没有有效元素了
                  nums1[p] = nums2[p2]; // 将 nums2 中的元素放到合并位置
                  p2--; // nums2 指针前移
              }
              p--; // 合并位置指针前移
          }
      }
    • 复杂度分析:

      • 时间复杂度:O(m+n) 。指针 pm+n-1 遍历到 0,每个元素至多被检查和移动一次。

      • 空间复杂度:O(1)。完全是原地操作,没有使用额外空间。

  • 思考: 这个题目是双指针技巧的又一个经典应用,尤其强调了从后往前操作的思维,这在涉及到数组原地修改且尾部有足够空间时非常有用。

总结与练习

本篇我们深入探讨了算法的基础------时间复杂度空间复杂度 ,理解了如何去分析代码的效率。然后,我们学习了最基础的数据结构------数组 ,掌握了它的基本特性,并通过两数之和 (引入哈希表和空间换时间)、移除元素 (引入快慢指针)、旋转数组 (引入三次反转法)和合并两个有序数组(引入从后往前双指针)这四个经典面试题,初步接触了面试中常见的解题思路和技巧。

这些技巧都是后续学习的基础,它们的核心思想贯穿在更复杂的数据结构和算法中。请务必理解每种解法的思路,而不仅仅是记住代码。

本篇核心知识点回顾:

  • 时间复杂度与空间复杂度:大 O 标记法,以及各种常见复杂度的含义与分析方法。

  • 数组特性:随机访问 O(1),插入删除 O(N)。

  • 快慢指针:用于数组原地修改、链表问题。

  • 哈希表应用:用于快速查找、空间换时间优化。

  • 三次反转法:用于数组原地旋转。

  • 从后往前合并:处理数组原地合并问题,避免数据覆盖。

课后练习(推荐力扣 LeetCode 题目):

  1. 两数之和 (Two Sum)LeetCode 1

  2. 移除元素 (Remove Element)LeetCode 27

  3. 移除重复项 (Remove Duplicates from Sorted Array)LeetCode 26

  4. 旋转数组 (Rotate Array)LeetCode 189

  5. 合并两个有序数组 (Merge Sorted Array)LeetCode 88

  6. 加一 (Plus One)LeetCode 66 (简单题,考察数组边界处理)

请你花时间理解这些题目,尝试用不同的方法解决它们,并分析它们的复杂度。如果你能手写出这些题目的最优解,那么你已经迈出了算法学习的坚实一步!


接下来,我们将在第二篇《链表的艺术:结构、操作与指针魔法》中,深入探讨链表这一非连续存储的数据结构,以及如何用指针的魔法解决链表相关的难题。敬请期待!

相关推荐
快去睡觉~5 分钟前
力扣137:只出现一次的数字Ⅱ
数据结构·算法·leetcode
阑梦清川8 分钟前
folo介绍和fluent reader阅读器的使用(RSS订阅技术)
算法
隐-梵30 分钟前
2025年测绘程序设计模拟赛一--地形图图幅编号及图廓点经纬度计算
windows·经验分享·visualstudio·c#
2501_9248793637 分钟前
密集表盘漏检率↓79%!陌讯多模态融合算法在电表箱状态识别的边缘优化
人工智能·算法·计算机视觉·目标跟踪·智慧城市
云云3211 小时前
亚矩阵云手机:解锁 Shopee/Lazada 东南亚电商运营“通关密码
大数据·人工智能·物联网·线性代数·智能手机·矩阵
lixy5792 小时前
wpf 动态转圈等待提示框
c#·wpf
Sunlightʊə2 小时前
05.LinkedList与链表
java·数据结构·算法·链表
qq_513970442 小时前
力扣 hot100 Day67
算法·leetcode·职场和发展
南無忘码至尊2 小时前
Unity编辑器工具:一键为场景中所有MeshRenderer对象添加指定脚本
unity·c#·游戏引擎·游戏开发
Cx330❀2 小时前
【数据结构初阶】--单链表(二)
数据结构·经验分享·算法·leetcode