leetcode 162 Find Peak Element

女生的网名这么多〃 提交于 2019-12-02 11:03:36

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]\neqnums[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]\neqnums[i+1]、nums[-1] = nums[n] = -\infty

线性扫描

从左往右扫描整个数组,找出第一个出现的峰值元素。第一反应是看每个元素的左右两边来判断是否为峰值,其实只需要可能右边邻居,因为左边已经比较过了,一定是小于该元素的。

  • 时间复杂度
    最坏情况下(元素自左向右递增)我们要扫描整个数组。所以时间复杂度为O(n)O(n)

  • 空间复杂度

    只用到了常数额外空间,所以空间复杂度是O(1)O(1)

递归二分搜索

递归二分搜索算法和下面的迭代二分搜索都属于分治策略_devide&conquer_的一种。根据题目我们可以看出,考虑中间元素mid,如果nums[mid] < nums[mid+1],那么mid元素的右边一定存在峰值元素。因为我们只需找出峰值元素中的任意一个,我们就不再需要考虑mid元素的左边一半了,这样问题的规模也就缩小了一半。

  • 时间复杂度
    T(n)=T(n2)+Θ(1)=T(n4)+2Θ(1)=...=log2(n)×Θ(1)=O(log2(n))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))
  • 空间复杂度
    递归二分搜索中,每次递归表用都会占用上次一半的额外空间,所以总的额外空间是log2(n)log_2(n)。也就是说空间复杂度也是O(log2(n))O(log_2(n))

迭代二分搜索

迭代二分搜索和递归二分搜索整体思路一样,时间复杂度也是相同的(迭代在常数项上一般优于递归)。差别在于迭代二分搜索不会占用太多的额外空间,所以空间复杂度是O(1)O(1)

所以能用迭代的地方尽量不用递归。

具体实现

线性扫描算法——c语言实现

int findPeakElement(int* nums, int numsSize) {
    int i = 0;
    for(;i < numsSize; i++)
    {
        if(nums[i] > nums[i + 1])
        {
            return i;
        }
    }
    return i-1;
}

虽然是Θ(n)\Theta(n)的时间复杂度,但是用c语言编写的话用时为0ms

递归二分搜索——C++实现

int search(vector<int>& nums, int s, int e){
        if(s == e)
            return s;
        int mid = (s + e) / 2;
        if(nums[mid] < nums[mid + 1])
            return search(nums,mid+1,e);
        else
            return search(nums,s,mid);
    }
    int findPeakElement(vector<int>& nums) {
        int len = nums.size();
        return search(nums,0,len-1);
    }

用时4ms

迭代二分搜索——Python3实现

def findPeakElement(self, nums):
        """
        :type nums: List[int]
        :rtype: int
        """
        start = 0 
        end = len(nums) - 1
        while start <= end:
            if start == end:
                return start
            mid = (start + end)//2
            if(nums[mid] < nums[mid+ 1]):
                start = mid + 1
            else:
                end = mid

单纯按照时间复杂度来比较,迭代二分搜索应该是最快的,但是用Python实现用时却是同样时间复杂度C++实现的8倍,用时32ms

超慢实现

通过分析超慢实现,我们可以避免影响算法效率的低级错误。

c语言超慢实现

int findPeakElement(int* nums, int numsSize) {
    int l = 0;
    int r = numsSize  -1 ;
    
    while (l < r) {
        int m = l + (r - l)/2;
        if (nums[m] <  nums[m+1]) {
            l = m + 1;
        } else {
            r = m;
        }
    }
    return r;
}

仔细看这段代码,其实并没有什么低价错误,只不过是在计算中间值m的时候算式稍微复杂些。和最优实现0ms差别也不大,用时4ms。

C++超慢实现

int findPeakElement(vector<int>& nums) {
        if (nums.size() == 1)
            return 0;
        if (nums.size() == 2)            
            return nums[0] > nums[1] ? 0 : 1;
    //----------------------------------------------
        int peak = 0;
        for (int i = 0 ; i < nums.size(); i++)
        {            
            if (i-1 >= 0 && i+1 < nums.size() )
            {
                if(nums[i] > nums[i-1] && nums[i] > nums[i+1])
                    return i;
            }
            else if (i+1 == nums.size() && nums[i] > nums[i-1])                
            {
                return i;
            }
                
        }
        return peak;
    }

和上段代码不同,这段代码就明显犯了**低级错误:在循环代码中加入了低效率因素。**在for循环语句中使用nums.size(),使得每次循环都会调用size()函数,严重影响算法性能,用时8ms。

至于如何优化算法性能,请见如何优化程序性能

Python3超慢实现

def findPeakElement(self, nums):
        """
        :type nums: List[int]
        :rtype: int
        """
        
        if len(nums) == 1:
            return 0
        
        if len(nums) == 2:
            return 0 if nums[0] >= nums[1] else 1
        
        low = 0
        high = len(nums) - 1
        
        while low < high:
            
            mid = low + (high - low) // 2
            
            if mid == 0 and nums[0] >= nums[1]:
                return 0
            if mid == len(nums)-1 and nums[-1] >= nums[-2]:
                return len(nums)-1
            if nums[mid-1] <= nums[mid] and nums[mid] >= nums[mid+1]:
                return mid
            if nums[mid+1] > nums[mid]:
                low = mid + 1
            else:
                high = mid
        
        return low

循环中加入了太多的分支,一定程度上拖慢了程序的性能,用时76ms。

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!