背景

去年下半年,我在微信书架里加入了许多技术书籍,各种类别的都有,断断续续的读了一部分。

没有计划的阅读,收效甚微。

新年伊始,我准备尝试一下其他方式,比如阅读周。每月抽出1~2个非连续周,完整阅读一本书籍。

这个“玩法”虽然常见且板正,但是有效,已经坚持阅读七个月。

已读完书籍《架构简洁之道》、《深入浅出的Node.js》、《你不知道的JavaScript(上卷)》、《你不知道的JavaScript(中卷)》、《你不知道的JavaScript(下卷)》、《数据结构与算法JavaScript描述》、《WebKit技术内幕》、《前端架构:从入门到微前端》

当前阅读周书籍《秒懂算法:用常识解读数据结构与算法》

阅读周·秒懂算法:用常识解读数据结构与算法:算法为何重要_有序数组

算法为何重要

算法就是完成特定任务所需的一组操作。

有序数组

有序数组和第1章中的“传统”数组几乎完全一致。它们唯一的区别在于有序数组中的值是按顺序排列的。也就是说,插入新值时,这个值必须被放到一个合适的格子中,以免打乱数组的顺序。

以数组[3, 17, 80, 202]为例,假设要插入值75。如果这是一个传统数组,那么可以像下图这样在末尾插入75。但如果这是有序数组,那么别无选择,只能把75插入合适的位置,以保证数组的值是递增的,下面来一步步分析这个过程。

第1步:检查索引0处的值来确定要在它前面还是后面插入新值75。因为75比3大,所以必须插到它右边。不过,因为依然不知道具体位置,所以必须检查下一个格子。这样的步骤叫作比较。我们会比较要插入的值和有序数组中现有的值。

第2步:检查下一个格子的值。75比17大,所以还得继续右移。

第3步:检查下一个格子的值。这次的值是80,比要插入的75大。因为我们碰到了第一个比75大的值,所以得出结论:要保证有序数组的有序性,75必须紧挨着放在80的左边。为此,需要右移数据为75腾出空间。

第4步:把最后一个值右移。

第5步:把倒数第二个值右移。

第6步:把75插到正确的位置。

在这个例子中,数组有4个元素,插入用了6步。而对于包含N个元素的有序数组,插入则需要花N+2步。

有序数组的查找

假设我们有一个常规数组[17, 3, 75, 202, 80]。如果要查找一个数组中不存在的值22,则需要检查每个元素,因为22有可能在数组中的任何位置。除非在检查到数组末尾之前就找到这个值,否则只能全部检查一遍。

而对有序数组来说,即便值不在数组中,也能提前结束查找。假设要在有序数组[3, 17,75, 80, 202]中查找值22,因为22不可能在75右边,所以只需检查到75即可。

有序数组线性查找的Ruby实现如下:

deflinear_search(array, search_value)
  # 遍历数组中的每个元素:
  array.each_with_index do |element, index|
    # 如果找到了值,就返回其索引:
    if element == search_value
      return index
    # 如果找到了一个比所查找值大的元素,那么可以提前退出循环:
    elsif element > search_value
      break
    end
  end
  # 如果没有找到所查找的值,则返回nil:
  return nil
end
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.

这种方法有两个参数:array是查找的有序数组,search_value是要查找的值。

要在上面的范例数组中查找22,可以像下面这样调用上面的函数:

plinear_search([3, 17, 75, 80, 202], 22)
  • 1.

如你所见,linear_search方法在寻找search_value时需要遍历数组的每一个元素。当遍历到的element大于search_value时,查找立刻结束。这是因为剩下的数组中不可能含有search_value。

二分查找

下面来看看如何将二分查找应用于有序数组。假如有序数组有9个元素。假设我们想在这个有序数组中查找7。二分查找的过程如下:

第1步:从中间的格子开始查找。因为可以用数组长度除以2来计算其索引,所以可以立刻跳转到这个格子,然后检查其中的值。因为值是9,所以我们知道7在它的左边。这样就排除了9右边的一半的格子(包括它自己)。

第2步:在9左边的格子中,检查最中间的值。因为中间有两个值,所以可以随机选择其中一个,这里以左边的值为例,这个值是4,7一定在它的右边。我们可以排除4及其左边的格子。

第3步:现在还有两个可能是7的格子。随机选择两个格子中左边的那个。

第4步:检查最后一个格子。找到7用了4步。

代码实现:二分查找

以下是二分查找的Ruby实现:

defbinary_search(array, search_value)
  # 首先,设定要查找的值所在位置的上下限。下限就是数组的第一个值,而上限就是最后一个值:
  lower_bound = 0
  upper_bound = array.length - 1
  # 在循环中不停检查上下限之间最中间的值:
  while lower_bound <= upper_bound do
    # 我们找到了上下限之间的中点:(因为在Ruby中整数除法的结果会向下取整,所以无须担心这个值不是整数。)
    midpoint = (upper_bound + lower_bound) / 2
    # 检查中点的值:
    value_at_midpoint = array[midpoint]
    # 如果中点的值就是要查找的值,那么查找结束。如果不是,那么就根据这个值与要查找的值的大小关系调整上下限:
    if search_value == value_at_midpoint
      return midpoint
    elsif search_value < value_at_midpoint
      upper_bound = midpoint - 1
    elsif search_value > value_at_midpoint
      lower_bound = midpoint + 1
    end
  end
  # 如果下限已经超过上限,那么就意味着要查找的值不在这个数组中:
  return nil
end
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.

binary_search方法同样以array和search_value为参数。可以像下面这样调用这个方法:

pbinary_search([3, 17, 75, 80, 202], 22)
  • 1.

二分查找与线性查找

对于比较小的有序数组,二分查找相对于线性查找的优势并不大。

对线性查找来说,有多少个元素就有多少步。每次加倍数据量,查找的步骤数也要加倍。而二分查找在加倍数据量时,只需要多1步。

有序数组的插入更慢,而查找更快。

总结

要达成一个计算目标通常有多种方法,而你选择的算法可能会严重影响代码的速度。

还有一点很重要:通常没有完美适用于任何情况的数据结构或者算法。例如,有序数组可以用二分查找,但这并不意味着就应该一直使用它。在某些不太需要查找数据而只需插入数据的场合,因为插入操作更迅速,所以传统数组可能更合适。

分析算法的方法就是计算其需要的步骤数。


作者介绍非职业「传道授业解惑」的开发者叶一一。《趣学前端》、《CSS畅想》等系列作者。华夏美食、国漫、古风重度爱好者,刑侦、无限流小说初级玩家。如果看完文章有所收获,欢迎点赞👍 | 收藏⭐️ | 留言📝。