leetcode 162 Find Peak Element
这个题目是MIT 6.006算法课中提到的第一个问题,也就是一维数组的Peak Finder问题。如果感兴趣看一看一下我的MIT 6.006 Lecture 1-b 笔记。
在对比不同解题思路的同时,我还对比了不同语言(c、C++、Python)。能够非常明显的看出在效率方面:c > C++ > Python;时间复杂度最高的简单算法用c语言写的效率也要大于C++写的低时间复杂度的算法,更不用说Python。当然如果看简洁程度,Python还是更优。详细情况请看具体实现。
问题描述
A peak element is an element that is greater than its neighbors.
峰值元素是指比相邻元素大的元素
Given an input array
nums
, where nums[i] $\neq$ nums[i+1], find a peak element and return its index.给定输入数组
nums
,规定nums[i]$\neq$nums[i+1]。从该数组中找到一个峰值元素并返回它的索引值。The array may contain multiple peaks, in that case return the index to any one of the peaks is fine.
给定的数组中可能含有多个峰值,只需找到其中任意一个即可。
You may imagine that nums[-1] = nums[n] = $-\infty$.
你可以假设nums[-1] = nums[n] = $-\infty$。
解体思路
题目中很多的假设可以用来提升我们算法的效率,比如nums[i]$\neq$nums[i+1]、nums[-1] = nums[n] = $-\infty$。
线性扫描
从左往右扫描整个数组,找出第一个出现的峰值元素。第一反应是看每个元素的左右两边来判断是否为峰值,其实只需要可能右边邻居,因为左边已经比较过了,一定是小于该元素的。
时间复杂度
最坏情况下(元素自左向右递增)我们要扫描整个数组。所以时间复杂度为$O(n)$。空间复杂度
只用到了常数额外空间,所以空间复杂度是$O(1)$。
递归二分搜索
递归二分搜索算法和下面的迭代二分搜索都属于分治策略_devide&conquer_的一种。根据题目我们可以看出,考虑中间元素mid
,如果nums[mid] < nums[mid+1]
,那么mid
元素的右边一定存在峰值元素。因为我们只需找出峰值元素中的任意一个,我们就不再需要考虑mid
元素的左边一半了,这样问题的规模也就缩小了一半。
- 时间复杂度
$T(n) = T(\frac{n}{2}) + \Theta(1) = T(\frac{n}{4}) + 2\Theta(1) = … = log_2(n) \times \Theta(1)=O(log_2(n))$ - 空间复杂度
递归二分搜索中,每次递归表用都会占用上次一半的额外空间,所以总的额外空间是$log_2(n)$。也就是说空间复杂度也是$O(log_2(n))$。
迭代二分搜索
迭代二分搜索和递归二分搜索整体思路一样,时间复杂度也是相同的(迭代在常数项上一般优于递归)。差别在于迭代二分搜索不会占用太多的额外空间,所以空间复杂度是$O(1)$。
所以能用迭代的地方尽量不用递归。
具体实现
线性扫描算法——c语言实现
1 | int findPeakElement(int* nums, int numsSize) { |
虽然是$\Theta(n)$的时间复杂度,但是用c语言编写的话用时为0ms。
递归二分搜索——C++实现
1 | int search(vector<int>& nums, int s, int e){ |
用时4ms
迭代二分搜索——Python3实现
1 | def findPeakElement(self, nums): |
单纯按照时间复杂度来比较,迭代二分搜索应该是最快的,但是用Python实现用时却是同样时间复杂度C++实现的8倍,用时32ms。
超慢实现
通过分析超慢实现,我们可以避免影响算法效率的低级错误。
c语言超慢实现
1 | int findPeakElement(int* nums, int numsSize) { |
仔细看这段代码,其实并没有什么低价错误,只不过是在计算中间值m的时候算式稍微复杂些。和最优实现0ms差别也不大,用时4ms。
C++超慢实现
1 | int findPeakElement(vector<int>& nums) { |
和上段代码不同,这段代码就明显犯了低级错误:在循环代码中加入了低效率因素。在for
循环语句中使用nums.size()
,使得每次循环都会调用size()
函数,严重影响算法性能,用时8ms。
至于如何优化算法性能,请见如何优化程序性能
Python3超慢实现
1 | def findPeakElement(self, nums): |
循环中加入了太多的分支,一定程度上拖慢了程序的性能,用时76ms。
参考文档
- Leetcode国际版:https://leetcode.com/
- 可能漏掉某些参考文档,请作者联系添加引用或删除相关内容。