https://oj.leetcode.com/problems/find-peak-element/
A peak element is an element that is greater than its neighbors.
Given an input array where num[i] ≠ num[i+1]
, find a peak element and return its index.
The array may contain multiple peaks, in that case return the index to any one of the peaks is fine.
You may imagine that num[-1] = num[n] = -∞
.
For example, in array [1, 2, 3, 1]
, 3 is a peak element and your function should return the index number 2.
Your solution should be in logarithmic complexity.
解题思路:
这题O(n)的解法很简单,几乎是straightforward。需要注意的就是题目为什么给nuo[-1] = num[n] = -∞这个条件,就是说边界的元素只要比相邻的一个大就可以了。
public class Solution { public int findPeakElement(int[] num) { if(num.length == 0){ return -1; } if(num.length == 1){ return 0; } // if(num.length == 2){ // return num[0] > num[1] ? 0 : 1; // } for(int i = 0; i < num.length; i++){ if(i - 1 < 0){ if(num[0] > num[1]){ return 0; } continue; } if(i + 1 > num.length - 1){ if(num[i] > num[i - 1]){ return i; } continue; } if(num[i - 1] < num[i] && num[i] > num[i + 1]){ return i; } } return 0; }}
而题目要求用O(logn)的时间求解,对数时间一般都是用折半查找的方法,也就是用start和end两个指针指向开头和结尾,然后根据一定的条件去用mid更新start或者end。这里困难的地方在于,如果mid不是peak element,如何判断peak element究竟是在mid往左还是往右?
解题的关键一定在这里,让我们再来思考一下。
一段数字没有peak,那么这段数字一定是单调递增或者递减的(这么说并不准确,因为递增的最后一个数字是peak)。这是一个非常重要的性质,想一下,如果mid比左大比右小,也就是说以mid为中点的三个元素是递增的,那么mid的右侧一定有peak element!注意是右侧一定有,左侧可能有可能没有。而题目的要求是,如果有多个peak element,返回任意一个就可以了。
如何理解这个结论?以mid为中点的三个元素是递增的,如果mid的右侧仍然一直递增,那么最后一个元素一定是peak element,因为它大于前一个元素,而num[n] = -∞。如果mid的右侧不是一直递增,那么某一点往后肯定突然变小一次,那么这一点就是我们要找的peak element。所以寻找范围只要在mid往右就可以了!我们并不知道mid的左侧是否有peak element,但这无所谓,已经足够了。
反之,如果以mid为中点的三个元素是递减的,搜索范围就是mid往左。
这样看来,O(logn)的解法已经有了。但是还有几个细节需要考虑。
首先,如果mid比两边都小怎么办?结论是peak element一定在mid往左和mid往右都存在。为什么?假设mid往左是从左至右递增的,那么mid -1就是peak element;如果mid往左是从左至右递减的,num[0]就是peak element。mid往右也同样可以证明。所以这时搜索mid两侧的任意一侧都可以。
第二,和二分查找一样,用mid去更新start和end的值的时候,要注意边界值。在这个问题里写过一个比较容易理解的方法,就是用[start,end)这个区间的开闭性来判断。
public class Solution { public int findPeakElement(int[] num) { if(num.length == 0){ return -1; } if(num.length == 1){ return 0; } if(num.length == 2){ return num[0] > num[1] ? 0 : 1; } int start = 0; int end = num.length; while(start < end){ int mid = (start + end) / 2; if(mid - 1 < 0){ if(num[mid] > num[mid + 1]){ return mid; }else{ start = mid + 1; } continue; } if(mid + 1 > num.length - 1){ if(num[mid] > num[mid - 1]){ return mid; }else{ end = mid; } continue; } if(num[mid] > num[mid - 1] && num[mid] > num[mid + 1]){ return mid; } if(num[mid] > num[mid - 1] && num[mid] < num[mid + 1]){ start = mid + 1; } if(num[mid] < num[mid - 1] && num[mid] > num[mid + 1]){ end = mid; } if(num[mid] < num[mid - 1] && num[mid] < num[mid + 1]){ start = mid + 1; // end = mid; //也可以 } } return 0; }}
上面是一个左闭右开的区间,所以初始化时start=0,end=length,循环的条件为start<end,更新值时start=mid + 1,end=mid。
下面是一个左闭右闭的解法。
public class Solution { public int findPeakElement(int[] num) { if(num.length == 0){ return -1; } if(num.length == 1){ return 0; } if(num.length == 2){ return num[0] > num[1] ? 0 : 1; } int start = 0; int end = num.length - 1; while(start <= end){ int mid = (start + end) / 2; if(mid - 1 < 0){ if(num[mid] > num[mid + 1]){ return mid; }else{ start = mid + 1; } continue; } if(mid + 1 > num.length - 1){ if(num[mid] > num[mid - 1]){ return mid; }else{ end = mid - 1; } continue; } if(num[mid] > num[mid - 1] && num[mid] > num[mid + 1]){ return mid; } if(num[mid] > num[mid - 1] && num[mid] < num[mid + 1]){ start = mid + 1; } if(num[mid] < num[mid - 1] && num[mid] > num[mid + 1]){ end = mid - 1; } if(num[mid] < num[mid - 1] && num[mid] < num[mid + 1]){ start = mid + 1; // end = mid; //也可以 } } return 0; }}
update 2015/07/12:
一个简单的解法,只需判断mid和后一个元素的大小就可以了。
public class Solution { public int findPeakElement(int[] nums) { if(nums.length == 0) { return -1; } int left = 0, right = nums.length - 1; while(left <= right) { int mid = (left + right) / 2; if(left == right) { return left; } else if(nums[mid] < nums[mid + 1]) { // mid一定不是,因为它已经小于右侧邻居 left = mid + 1; } else { // mid有可能是,因为他仍谈大于右侧邻居 right = mid; } } return left; }}