[20]颜色分类和前k个高频元素(堆的使用)

文章介绍了LeetCode上的两道算法题,一是颜色分类,通过不同的排序策略实现数组中红白蓝三色的排序;二是找出数组中出现频率前k的元素,讨论了不同计数和查找方法,包括使用堆和快速排序的优化策略。

*内容来自leetcode

1.颜色分类

题目要求

给定一个包含红色、白色和蓝色、共 n 个元素的数组 nums ,原地对它们进行排序,使得相同颜色的元素相邻,并按照红色、白色、蓝色顺序排列。

我们使用整数 0、 1 和 2 分别表示红色、白色和蓝色。

必须在不使用库内置的 sort 函数的情况下解决这个问题。

示例 1:

输入:nums = [2,0,2,1,1,0]
输出:[0,0,1,1,2,2]

进阶:

  • 你能想出一个仅使用常数空间的一趟扫描算法吗?

思路

这道题实际上就是一个按大小排序的问题,在不考虑附加条件的情况下就很简单。

class Solution:
    def sortColors(self, nums: List[int]) -> None:
        """
        Do not return anything, modify nums in-place instead.
        """
        n = len(nums)
        for i in range(n):
            for j in range(n-1):
                if nums[j] >= nums[j+1]:
                    temp = nums[j]
                    nums[j] = nums[j+1]
                    nums[j+1] = temp

这应该是最简单的排序实现,但是相应的时间花费也差不多是最多的。这道题作为中等题肯定不能满足于这种解法。考虑实现进阶提出的一趟扫描。可以用一次扫描来将原数组中0、1、2用于生成三个新的数组,最后再将三个数组组合起来即可。

class Solution:
    def sortColors(self, nums: List[int]) -> None:
        """
        Do not return anything, modify nums in-place instead.
        """
        nums1 = list()
        nums2 = list()
        nums3 = list()
        for i in range(len(nums)):
            if nums[i] == 0:
                nums1.append(nums[i])
            if nums[i] == 1:
                nums2.append(nums[i])
            if nums[i] == 2:
                nums3.append(nums[i])
        nums[:] = nums1 + nums2 + nums3

值得一提的是,由于排序是通过调用sortColors()这个函数来完成的,最后修改原数组时使用的是nums[:],如果使用nums,则原数组不会有任何变化。

#将原有列表中所有元素进行替换
nums[:] = nums1 + nums2 + nums3
#将nums指向新列表,并不会修改原有列表
nums = nums1 + nums2 + nums3

官方给出了三种解法,分别使用了单指针和双指针完成。三种思路都是基于交换完成的。

单指针

class Solution:
    def sortColors(self, nums: List[int]) -> None:
        """
        Do not return anything, modify nums in-place instead.
        """
        poi = 0
        for i in range(len(nums)):
            if nums[i] == 0:
                nums[i],nums[poi] = nums[poi],nums[i]
                poi += 1
        for i in range(poi,len(nums)):
            if nums[i] == 1:
                nums[i],nums[poi] = nums[poi],nums[i]
                poi += 1

两种双指针的区别在于具体识别那个数字,这里只放出识别0/1的代码

class Solution:
    def sortColors(self, nums: List[int]) -> None:
        """
        Do not return anything, modify nums in-place instead.
        """
        #双指针
        #识别0/1
        poi1 = 0
        poi0 = 0
        for i in range(len(nums)):
            if nums[i] == 0:
                nums[i],nums[poi0] = nums[poi0],nums[i]
                if poi1 > poi0:
                    nums[i],nums[poi1] = nums[poi1],nums[i]
                poi1 +=1
                poi0 +=1
            elif nums[i] == 1:
                nums[i],nums[poi1] = nums[poi1],nums[i]
                poi1 +=1

2.前k个高频元素

题目要求

给你一个整数数组 nums 和一个整数 k ,请你返回其中出现频率前 k 高的元素。你可以按 任意顺序 返回答案。

示例 1:

输入: nums = [1,1,1,2,2,3], k = 2
输出: [1,2]
示例 2:

输入: nums = [1], k = 1
输出: [1]

思路

最开始的思路是将每个数出现的次数用一个列表记录下来,然后在对列表进行排序,依据计数返回对应的的数字。

这样考虑的问题在于忽略了nums中的数的大小是有可能比数组的大小更大的,就不能让记录数组的大小为nums数组的大小。如果非要使用这种方式,那就有可能需要非常大的数组来进行记录。还有一个问题就是数组中的数可能为负,也需要加以考虑。

在考虑到上述问题后,能够完成题目要求,但是性能并不理想。同时在我写这段的时候,发现可以用字典代替列表来进行记录,就不会用上面的问题了。先贴上记录数组实现的代码。

class Solution:
    def topKFrequent(self, nums: List[int], k: int) -> List[int]:
        n = len(nums)
        if n == 1:
            return nums
        #初始化记录数组,分为正负两个数组
        maxNum = max(nums)
        minNum = min(nums)
        if n > maxNum:
            countP = [0]*(n+1)
        else:
            countP = [0]*(maxNum+1)
        if n > -minNum:
            countN = [0]*(n+1)
        else:
            countN = [0]*(-minNum+1)

        res = []
        #进行记录
        for i in range(n):
            if nums[i] >= 0:
                countP[nums[i]] +=1
            else:
                countN[-nums[i]] +=1
        #对记录结果进行排序
        sortCountP = countP[:]
        sortCountP.sort(reverse=True)

        sortCountN = countN[:]
        sortCountN.sort(reverse=True)

        #寻找出现频率最高的数字
        poiP,poiN = 0,0
        for i in range(k):
            if sortCountP[poiP] > sortCountN[poiN]:
                index = countP.index(sortCountP[poiP])
                res.append(index)
                countP[index] = -1
                poiP +=1
            else:
                index = countN.index(sortCountN[poiN])
                res.append(-index)
                countN[index] = -1
                poiN +=1
        return res

用字典实现。实际看来只有内存消耗有所减少,执行时间反而变长了。。。

class Solution:
    def topKFrequent(self, nums: List[int], k: int) -> List[int]:
        n = len(nums)
        count = {}
        res = []

        for i in range(n):
            #当键不存在会新添键进字典,默认值为1,导致计数值会比实际多一个
            #由于比较是相对的,所以不影响结果
            if count.setdefault(nums[i],1):
                count[nums[i]] +=1

        for i in range(k):
            maxKey = 0
            maxVal = 0
            for key,value in count.items():
                if value > maxVal:
                    maxVal = value
                    maxKey = key
            res.append(maxKey)
            count[maxKey] = -1
        
        return res

官方给出的思路也是先进行计数,然后再找出前k个频率最高的数。不同之处在于对找频率最高的k个数这个过程的优化。

第一种方法是使用堆

在这里,我们可以利用堆的思想:建立一个小顶堆,然后遍历「出现次数数组」:

如果堆的元素个数小于 k,就可以直接插入堆中。
如果堆的元素个数等于 k,则检查堆顶与当前出现次数的大小。如果堆顶更大,说明至少有 k 个数字的出现次数比当前值大,故舍弃当前值;否则,就弹出堆顶,并将当前值插入堆中。
遍历完成后,堆中的元素就代表了「出现次数数组」中前 k 大的值。

由于之前没有出现过堆的使用,这里对堆进行一些补充。

堆是一种非连续的树形储存数据结构,每个节点有一个值,整棵树是经过排序的。堆的特点是根结点的值最小(或最大),且根结点的两个子树也是一个堆。堆通常是一个可以被看做一棵完全二叉树的数组对象。堆有很多应用,例如在排序算法中使用,或者在优先队列中使用。请问还有什么其他问题吗?了解详细信息:

堆的基本存储 | 菜鸟教程​​​​​​​

在python中,内置库heapq对堆进行了实现。默认为小根堆

heapq模块中的一些函数有:

  • heappush(heap, item):将item压入堆heap中。
  • heappop(heap):从堆heap中弹出最小的元素。
  • heapify(x):将列表x转换成堆,原地,线性时间内。
  • heapreplace(heap, item):弹出最小的元素,并将item压入堆中。
  • nlargest(n, iterable, key=None):返回iterable中n个最大的元素,key是排序函数。
  • nsmallest(n, iterable, key=None):返回iterable中n个最小的元素,key是排序函数。

补充完这些知识,回到这道题。这道题官方的解答中没有python的解答,光看C++实在是不知道怎么用python写出来。借助于new bing对C++的代码用python进行了复现。这里需要注意的一个问题是,在堆中的数据是以(x,y)的形式存在时,默认是以x来进行大小的判断的,而这道题中是要以计数的大小,也就是y来进行判断,所以在写入堆和判断时需要交换一下num和val的次序。应该是我在copy的时候没注意到这个问题,写错了,newbing生成的代码应该是没问题的。不过以此发现了一个问题也还是不错。写完这个再回去看我写的,有种原始的美感。

class Solution:
    def topKFrequent(self, nums: List[int], k: int) -> List[int]:
        n = len(nums)
        count = {}
        res = []
        #统计数字出现的次数
        for v in nums:
            count[v] = count.get(v, 0) + 1

        q = []
        for num, val in count.items():
            if len(q) == k:
                if q[0][0] < val:
                    heapq.heappop(q)
                    #注意这里是交换了val、num后再写入的
                    heapq.heappush(q, (val,num))
            else:
                heapq.heappush(q, (val,num))
        while q:
            res.append(heapq.heappop(q)[1])

        return res

第二种算法是

使用基于快速排序的方法,求出「出现次数数组」的前 k 大的值。

在对数组 arr[l…r]做快速排序的过程中,我们首先将数组划分为两个部分 arr[i…q−1] 与 arr[q+1…j],并使得 arr[i…q−1]中的每一个值都不超过 arr[q],且 arr[q+1…j]中的每一个值都大于 arr[q]。

于是,我们根据 k 与左侧子数组 arr[i…q−1]的长度(为 q−i)的大小关系:

如果 k≤q−i,则数组 arr[l…r] 前 k 大的值,就等于子数组 arr[i…q−1]前 k 大的值。
否则,数组 arr[l…r]前 k大的值,就等于左侧子数组全部元素,加上右侧子数组 arr[q+1…j]中前 k−(q−i)大的值。
原版的快速排序算法的平均时间复杂度为 O(Nlog⁡N)。我们的算法中,每次只需在其中的一个分支递归即可,因此算法的平均时间复杂度降为 O(N)。

class Solution:
    def topKFrequent(self, nums: List[int], k: int) -> List[int]:
        def quickSort(nums, left, right, k):
            if left >= right: return
            l, r = left, right
            pivot = nums[left]
            while l < r:
                while l < r and nums[r][1] >= pivot[1]:
                    r -= 1
                nums[l] = nums[r]
                while l < r and nums[l][1] <= pivot[1]:
                    l += 1
                nums[r] = nums[l]
            nums[l] = pivot
            mid = l
            if mid + 1 == k:
                return
            elif mid + 1 < k:
                quickSort(nums, mid + 1, right, k)
            else:
                quickSort(nums, left, mid - 1, k)

        store = collections.Counter(nums)
        s = list(store.items())
        quickSort(s, 0, len(store) - 1, len(store) - k + 1)
        res = []
        for i in range(len(s)-1, len(s)-k-1, -1):
            res.append(s[i][0])
        return res

也可以通过调用内置函数完成

class Solution:
    def topKFrequent(self, nums: List[int], k: int) -> List[int]:
        #基于快速排序实现
        occurrences = collections.Counter(nums)
        return [x[0] for x in occurrences.most_common(k)]

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值