数据结构的万能钥匙:掌握数据操作这几招

3,517 阅读5分钟

本文来源于公众号:勾勾的Java宇宙(微信号:Javagogo),干货!

原文链接:mp.weixin.qq.com/s/laFis0z2P…

作者:公瑾

要想灵活使用数据结构,你需要先弄清楚数据在代码中被处理、加工的最小单位动作,也就是数据结构的基本操作,有了这些动作之后,你才可以基于此去选择更合适的数据结构。

从一个例题深度拆解代码的数据操作

例题:在一个数组中找出出现次数最多的那个元素的数值。

例如,输入数组 a=[1,2,3,4,5,5,6] 中,只有 5 出现了两次,其余都是 1 次,显然 5 出现的次数最多,则输出 5

为了降低时间复杂度,我们引入了 k-v 的字典的数据结构。

那么问题来了,究竟是什么原因促使我们使用字典的数据结构呢?如果不使用字典,改为使用数组行不行呢?

为了回答这些问题,我们先看一下究竟此处代码需要对数据进行哪些操作。

这段代码处理数据的核心思路是:

  1. 根据原始数组计算每个元素出现的次数;

  2. 根据第一步的结果,找到出现次数最多的元素。

首先来分析第一步统计出现次数的处理。此时,你还不知道应该采用什么数据结构。

对于每一次的循环,你得到了输入数组中的某个元素 a[i] 。接着,你需要判断这个元素在未知的数据结构中是否出现过:

  • 如果出现了,就需要对出现的次数加 1
  • 如果没有出现过,则把这个元素新增到未知数据结构中,并且把次数赋值为 1

这里的数据操作包括以下 3 个。

  • 查找: 看能否在数据结构中查找到这个元素,也就是判断元素是否出现过

  • 新增: 针对没有出现过的情况,新增这个元素

  • 改动: 针对出现过的情况,需要对这个元素出现的次数加 1

接下来一起分析第二步,访问数据结构中的每个元素,找到次数最多的元素。这里涉及的数据操作很简单,只有查找。

因此,这段代码需要高频使用查找的功能。

此时,第一步的查找动作嵌套在 for 循环中,如果你的代码不能在 O(1) 的时间复杂度内完成,则代码整体的时间复杂度并没有下降。而能在 O(1) 的时间复杂度内完成查找动作的数据结构,只有字典类型。

这样,外层 for 循环是 O(n) 的时间复杂度,内部嵌套的查找操作是 O(1) 的时间复杂度,整体计算下来,就仍然是 O(n) 的时间复杂度。

字典的查找是通过键值对的匹配完成的,它可以在 O(1) 时间复杂度内,实现对数值条件查找。

现在,我们换个解决方案。

假设采用两个数组,分别按照对应顺序记录元素及其对应的出现次数。数组对于元素的查找只能逐一访问,时间复杂度是 O(n),也就是说,在 O(n) 复杂度的 for 循环中,又嵌套了 O(n) 复杂度的查找动作,所以时间复杂度是 O(n²)。

因此,这里的数据结构,只能采用字典类型。

以不变应万变:掌握数据处理的基本操作

不管是数组还是字典,都需要额外开辟空间,对数据进行存储。而且数据存储的数量,与输入的数据量一致。因此,消耗的空间复杂度相同,都是 O(n)。

由前面的分析可见,同样采用复杂的数据结构消耗了 O(n) 的空间复杂度,其对时间复杂度降低的贡献有可能不一样。

因此,我们必须要设计合理的数据结构,以达到降低时间损耗的目的。

  1. 首先我们分析这段代码到底对数据先后进行了哪些操作

  2. 然后再根据分析出来的数据操作,找到合理的数据结构

经过对代码的拆解,你会发现,即便是很复杂的代码对数据的处理也只有这 3 个基本操作:增、删、查。

即使你遇到更复杂的问题,无非就是这些基本操作的叠加和组合。只要围绕这 3 个数据处理的操作进行分析,就能得出解决问题的最优方案。

如何进行分析呢?可以参考下面这些步骤:

  1. 这段代码对数据进行了哪些操作?

  2. 这些操作中,哪个操作最影响效率,对时间复杂度的损耗最大?

  3. 哪种数据结构最能帮助你提高数据操作的使用效率?

这 3 个步骤构成了设计合理数据结构的方法论。


欢迎大佬们关注公众号 勾勾的Java宇宙(微信号:Javagogo),拒绝水文,收获干货!