JS 中的二分搜索:试图找到一致的心理模型

Joj*_*oji 3 javascript algorithm binary-search

这几天在刷LeetCode,遇到了挑战162.Find Peak Element

\n
\n

峰值元素是严格大于其邻居的元素。

\n

给定一个整数数组nums,找到一个峰值元素,并返回其索引。如果数组包含多个 Peak,则返回任意 Peak 的索引。

\n

你可能会想象到nums[-1] = nums[n] = -\xe2\x88\x9e

\n

您必须编写一个及时运行的算法O(log n)

\n

限制条件:

\n
    \n
  • 1 <= nums.length <= 1000
  • \n
  • -231 <= nums[i] <= 231 - 1
  • \n
  • nums[i] != nums[i + 1]对于所有有效的i
  • \n
\n
\n

本题是关于使用二分查找来查找数组中的峰值元素。

\n

我知道我们可以将数组视为交替的升序和降序序列。这是我的解决方案

\n
var findPeakElement = function(nums) {\n    if(nums.length <= 1) return 0\n    let left = 0, right = nums.length - 1\n    \n    while(left <= right) {\n        const mid = left + right >>> 1\n        if(nums[mid] > nums[mid + 1]) {\n            right = mid - 1\n        } else {\n            left = mid + 1\n        }\n    }\n    \n    \n    return left === nums.length ? left - 1 : left\n};\n
Run Code Online (Sandbox Code Playgroud)\n

如果nums[mid]大于数组中的下一个元素,我们知道我们处于降序子数组中,并且峰值元素必须位于左侧,反之亦然,如果nums[mid]小于下一个元素。到目前为止,一切都很好。但让我困惑的是我最终应该返回哪个索引 -left或者right?为了弄清楚这一点,我需要经历一系列的试验和错误。

\n

如果我稍微调整问题以找到山谷元素,例如,[1, 3, 20, 4, 1, 0]山谷元素应该是0。虽然我可以推理如何缩小窗口,但我似乎仍然无法弄清楚在二分搜索结束时应该返回哪个索引。

\n

这是我尝试通过镜像我所做的事情来返回数组中的山谷元素findPeakElement

\n
var findValleyElement = function (nums) {\n  if (nums.length <= 1) return 0\n  let left = 0,\n    right = nums.length - 1\n\n  while (left <= right) {\n    const mid = (left + right) >>> 1\n    if (nums[mid] > nums[mid + 1]) {\n      left = mid + 1\n    } else {\n      right = mid - 1\n    }\n  }\n\n  return right\n}\n
Run Code Online (Sandbox Code Playgroud)\n

但这次我不能用作right返回的索引。我需要用left它来代替。如果不通过一堆示例,我似乎无法想到一种一致的思考方式,这确实不理想,因为您仍然可能会错过一些边缘情况。

\n

所以我的问题是,在思考这些二分搜索问题时,我们是否可以采用一些一致的思维模型,特别是我们应该返回哪个索引来满足要求。

\n

tri*_*cot 5

当以下条件成立时:

\n
if(nums[mid] > nums[mid + 1]) {\n
Run Code Online (Sandbox Code Playgroud)\n

...那么这可能mid一种解决方案,甚至可能是唯一的解决方案。因此,这意味着您不应将其从范围中排除,但right = mid - 1您确实将其排除。你应该设置right = mid. 为了避免潜在的无限循环,循环条件应该是left < right。这将确保循环始终结束:保证范围在每次迭代中变得更小*

\n

* 让我们假设left == right + 1在某个时刻。然后mid将变得等于left(因为总和中的奇数位被 删除>>>)。现在要么我们做right = mid,要么我们做left = mid + 1。无论哪种情况我们都会得到left == right。在所有其他情况下left < right,我们得到的 amid严格位于这两个限制之间,然后范围肯定会变得更小。

\n

一旦循环退出,left就等于right。该范围(1)中唯一可能的索引就是索引。

\n

现在不再需要检查是否leftnums.length,因为这不会发生:根据我们选择的while条件,left永远不会大于right,...只能等于它。由于right是一个有效的索引,因此不需要这种超出范围的检查。

\n

另外数组大小为1的情况现在不需要特殊处理。

\n

所以:

\n
var findPeakElement = function(nums) {\n    let left = 0,\n        right = nums.length - 1;\n\n    while (left < right) {\n        const mid = (left + right) >>> 1;\n        if (nums[mid] > nums[mid + 1]) {\n            right = mid;\n        } else {\n            left = mid + 1;\n        }\n    }\n\n    return left;\n};\n
Run Code Online (Sandbox Code Playgroud)\n

山谷而不是高峰

\n
\n

这是我返回山谷元素的尝试

\n
\n

如果你想找到元素,它并不总是有效,除非问题中的以下假设从此改变:

\n
\n

你可能会想象nums[-1] = nums[n] = -\xe2\x88\x9e

\n
\n

...对此:

\n
\n

你可能会想象nums[-1] = nums[n] = \xe2\x88\x9e

\n
\n

一旦达成一致,您只需将上述代码块中的比较从 更改nums[mid] > nums[mid + 1]nums[mid] < nums[mid + 1]

\n