数组中的第K个最大元素 - 力扣(LeetCode)

在未排序的数组中找到第 k 个最大的元素。请注意,你需要找的是数组排序后的第 k 个最大的元素,而不是第 k 个不同的元素。

示例 1:
输入: [3,2,1,5,6,4] 和 k = 2
输出: 5

示例 2:
输入: [3,2,3,1,2,4,5,5,6] 和 k = 4
输出: 4

说明:
你可以假设 k 总是有效的,且 1 ≤ k ≤ 数组的长度。

分析:

本质上是个排序问题,但是需要注意的是重复的元素,然后不需要排完,事实上,排到k就可以。示例2还是蛮奇怪的,首先,他这个数据是带重的; 其次,他这个同样的数,是加k的

快排法

快排核心思想就是分治和分区,partition,每次用piviot把数据分成两(三)部分。

我们选择数组区间 A[0…n-1] 的最后一个元素 A[n-1] 作为 pivot,对数组 A[0…n-1] 原地分区,这样数组就分成了三部分,A[0…p-1]、A[p]、A[p+1…n-1]。

如果 p+1=K,那 A[p] 就是要求解的元素;如果 K>p+1, 说明第 K 大元素出现在 A[p+1…n-1] 区间,我们再按照上面的思路递归地在 A[p+1…n-1] 这个区间内查找。同理,如果 K<p+1,那我们就在 A[0…p-1] 区间查找。

“如果 K>p+1, 说明第 K 大元素出现在 A[p+1…n-1] 区间” 这里其实不太对,因为有重复数据的出现,还要考虑等于的情况。
话说为什么每次Pivot都选最后一个数?是不是对于随机的数据,选每个数都一样的?
下图是从小往大排,作为示例理解一下可以,不过k大也差不多,就是倒过来,从大往小排。

clipboard.png


clipboard.png

clipboard.png

算法复杂度O(n)
每一次都是找到一个标定点,然后将这个标定点挪到数组中合适的位置,注意这个所谓的合适的位置,恰恰是这个数组在排好序后,最终所处的位置。比如说在这个例子中,如果我们把4挪到的这个位置,这个位置,相应的也是数组中的第4个位置,那么整个数组的第4名就是4。第4名以前的元素都在4的前面,第4名以后的元素都在4的后面。

如果问,这个数组第6名是什么呢,我们在做完这一次partition之后就完全不用管,前面的部分了,因为我们知道我们的标定点在第4位,那么第6名的位置一定在这个第4位的后面的部分。我们只需要继续的递归的去求解,后面的这部分的第2名是谁就行了,如果我问你的事儿,这个数组的第2位是谁呢,上面,我们把4方到这个位置之后,我们只需要继续求解,前面这一部分的第2名是谁就好了,那么整体的思路就是这个样子。

class Solution:
    def findKthLargest(self, nums, k):
        """
        :type nums: List[int]
        :type k: int
        :rtype: int
        """
        def partition(left, right, pivot_index):
            pivot = nums[pivot_index]
            # 1. move pivot to end
            nums[pivot_index], nums[right] = nums[right], nums[pivot_index]  
            
            # 2. move all smaller elements to the left
            store_index = left
            for i in range(left, right):
                if nums[i] < pivot:
                    nums[store_index], nums[i] = nums[i], nums[store_index]
                    store_index += 1

            # 3. move pivot to its final place
            nums[right], nums[store_index] = nums[store_index], nums[right]  
            
            return store_index
        
        def select(left, right, k_smallest):
            """
            Returns the k-th smallest element of list within left..right
            """
            if left == right:       # If the list contains only one element,
                return nums[left]   # return that element
            
            # select a random pivot_index between 
            pivot_index = random.randint(left, right)     
                            
            # find the pivot position in a sorted list   
            pivot_index = partition(left, right, pivot_index)
            
            # the pivot is in its final sorted position
            if k_smallest == pivot_index:
                 return nums[k_smallest]
            # go left
            elif k_smallest < pivot_index:
                return select(left, pivot_index - 1, k_smallest)
            # go right
            else:
                return select(pivot_index + 1, right, k_smallest)

        # kth largest is (n - k)th smallest 
        return select(0, len(nums) - 1, len(nums) - k)

为什么上述解决思路的时间复杂度是 O(n)?

第一次分区查找,我们需要对大小为 n 的数组执行分区操作,需要遍历 n 个元素。第二次分区查找,我们只需要对大小为 n/2 的数组执行分区操作,需要遍历 n/2 个元素。依次类推,分区遍历元素的个数分别为、n/2、n/4、n/8、n/16.……直到区间缩小为 1。

如果我们把每次分区遍历的元素个数加起来,就是:n+n/2+n/4+n/8+…+1。这是一个等比数列求和,最后的和等于 2n-1。所以,上述解决思路的时间复杂度就为 O(n)。

如果,每次取数组中的最小值,将其移动到数组的最前面,然后在剩下的数组中继续找最小值,以此类推,执行 K 次,找到的数据不就是第 K 大元素了吗?

不过,时间复杂度就并不是 O(n) 了,而是 O(K n)。时间复杂度前面的系数不是可以忽略吗?O(K n) 不就等于 O(n) 吗?

这个可不能这么简单地划等号。当 K 是比较小的常量时,比如 1、2,那最好时间复杂度确实是 O(n);但当 K 等于 n/2 或者 n 时,这种最坏情况下的时间复杂度就是 O(n2) 了。

还有优先队列的写法,学不明白,以后再更
clipboard.png

参考文章:极客时间 王争 算法与数据结构之美


yint
388 声望21 粉丝

转AIing


引用和评论

0 条评论