【前端算法60题】1. 什么时候使用 数组 + 双指针?

254 阅读6分钟

一、做一道题

  1. 首先我们看题 Leetcode 26

给你一个 非严格递增排列 的数组 nums ,请你 原地 删除重复出现的元素,使每个元素 只出现一次 ,返回删除后数组的新长度。元素的 相对顺序 应该保持 一致 。然后返回 nums 中唯一元素的个数。

考虑 nums 的唯一元素的数量为 k ,你需要做以下事情确保你的题解可以被通过:

  • 更改数组 nums ,使 nums 的前 k 个元素包含唯一元素,并按照它们最初在 nums 中出现的顺序排列。nums 的其余元素与 nums 的大小不重要。
  • 返回 k
  1. 以下是题解

/**
 * @param {number[]} nums
 * @return {number}
 */
var removeDuplicates = function(nums) {
    const len = nums.length;
    if(len === 0) return 0;
    let slow = 1;
    let fast = 1;
    while(fast < len) {
        if(nums[fast] !== nums[fast-1]) {
            nums[slow] = nums[fast];
            ++slow;
        }
        ++fast;
    }
    return slow;
};

逻辑:

  • 定义两个指针,快指针和慢指针,开始时都指向数组的第 2 个元素,第一个元素必然不会重复。
  • 快指针负责遍历数组,慢指针负责指向最后一个不重复的元素。
  • 遍历数组时,当快指针遇到和慢指针不同的元素时,就将该元素复制到慢指针的下一个位置,并将慢指针向前移动一位。
  • 当快指针遍历完数组后,慢指针后面的所有位置都是重复元素,可以不考虑。返回慢指针的位置加一,即为非重复元素的数量。

时间复杂度:O(n)因为只需遍历一次数组。

空间复杂度:O(1)因为只是创建了额外的变量。

二、分析数据结构

  1. 介绍数组

数组是一种基本的数据结构,其特点是元素在内存中连续存储,每个元素可以通过索引直接访问。

  1. 适合使用数组的情况:

  • 需要随机访问:当你需要频繁地通过索引访问元素时,数组是非常合适的,因为访问元素的时间复杂度是 O(1)。
  • 元素数量已知:当你提前知道元素的数量,并且这个数量不会发生大的变化时,数组是一个不错的选择。
  • 数据量不是特别大:因为数组是连续的内存块,如果数据量特别大,可能会遇到内存分配问题。
  • 需要高效的缓存利用率:由于数组元素在内存中是连续存放的,它们可以高效地利用 CPU 缓存。
  1. 不太适合使用数组的情况:

  • 元素数量经常变动:如果经常需要添加或删除元素,数组可能不是最佳选择,因为这样的操作会导致数组的重新分配或者移动大量的元素,从而带来高时间复杂度。
  • 需要特殊操作:例如,频繁的插入、删除等操作,链表或其他数据结构可能更有优势。
  • 不确定数据量大小:如果不确定会存储多少数据,动态数组(如 Python 中的 list 或 C++ 中的 vector)或其他数据结构可能更合适。
  • 需要复杂的数据操作:如频繁查找、排序、合并等,特定的数据结构(如树、哈希表、集合等)可能更有优势。

三、分析算法

  1. 介绍快慢指针

快慢指针是一种常用的双指针技巧。它涉及到使用两个指针,一个移动得快(通常每次两步),另一个移动得慢(通常每次一步)。

  1. 适合使用快慢指针的情况:

  • 检测循环:在链表中检测是否存在环。快指针每次移动两步,慢指针每次移动一步。如果存在环,两个指针最后就会相遇。
  • 找中点:在链表中查找中间的节点。当快指针到末尾的时候,慢指针刚好在中点。
  • 数组去重,就如上文的题目一样。
  • 有序数组/链表合并:合并两个有序数组或链表时,可以使用两个指针分别指向两个数组/链表的档期那元素,然后逐个比较和合并。
  • 查找满足条件的子数组或子串:例如在数组中找到满足某个条件的连续子数组。
  1. 通常不适合使用的情况

  • 需要随机访问:例如需要频繁地查找、插入或删除数组/链表中的某个位置的元素。
  • 深度优先搜索或广度优先搜索:这种情况下,通常使用递归或队列来实现。
  • 数据结构为树或图:虽然有些树的问题可以用双指针解决(如判断一个树是不是对称的),但大多数树或图的问题不适合使用快慢指针。
  1. 边界条件

使用快慢指针时,要特别注意边界条件和可能出现的异常情况。例如:

  • 快指针是否会超出数组的边界或链表的末尾。
  • 是否需要先判断数组或链表是否为空。
  • 是否存在其他特殊情况或特殊输入需要处理。

四、深度分析

  1. 数组的特点

  • 技巧:可以直接访问,且空间是连续性的,这使得它能够高效得利用CPU的缓存。
  • 流程:初始化时数组需要分配连续的内存块,使用索引读取或写入元素,要访问的时候可以用循环来遍历。
  • 学科:数组对于内存管理和算法优化有着重要的作用。
  • 本质:数组具有简约性和确定性。
  1. 快慢指针

  • 技巧:利用两个指针速度的差异、相对位置形成一种关系从而解决问题。
  • 流程:初始化时设置同一个或不同的起点,根据问题的需要决定移动策略是一快一慢,或者具有固定距离差异,当满足某些条件后终止。
  • 学科:快慢指针是解决某些特定问题的算法策略,尤其在解决链表和数组问题时非常有用。
  • 本质:通过两个元素的相对动作解决问题,体现了二元的思考模式;通常用于寻找某种平衡或者中点,体现了动态过程中的平衡思想。

五、小结与作业

相信大家对于快慢指针和数组有了一定的理解,有问题可以在评论区探讨。

作业:请解决 leetcode 141 问题