秋招算法备战第13天 | 239. 滑动窗口最大值、347.前 K 个高频元素

239. 滑动窗口最大值 - 力扣(Leetcode)

因为时间关系直接读代码啦,这里是实现了一个单调队列,注意调用deque中的方法有popleft、append以及pop

from collections import deque

class MyQueue: #单调队列(从大到小
    def __init__(self):
        self.queue = deque() #这里需要使用deque实现单调队列,直接使用list会超时
    
    #每次弹出的时候,比较当前要弹出的数值是否等于队列出口元素的数值,如果相等则弹出。
    #同时pop之前判断队列当前是否为空。
    def pop(self, value):
        if self.queue and value == self.queue[0]:
            self.queue.popleft()#list.pop()时间复杂度为O(n),这里需要使用collections.deque()
            
    #如果push的数值大于入口元素的数值,那么就将队列后端的数值弹出,直到push的数值小于等于队列入口元素的数值为止。
    #这样就保持了队列里的数值是单调从大到小的了。
    def push(self, value):
        while self.queue and value > self.queue[-1]:
            self.queue.pop()
        self.queue.append(value)
        
    #查询当前队列里的最大值 直接返回队列前端也就是front就可以了。
    def front(self):
        return self.queue[0]
    
class Solution:
    def maxSlidingWindow(self, nums: List[int], k: int) -> List[int]:
        que = MyQueue()
        result = []
        for i in range(k): #先将前k的元素放进队列
            que.push(nums[i])
        result.append(que.front()) #result 记录前k的元素的最大值
        for i in range(k, len(nums)):
            que.pop(nums[i - k]) #滑动窗口移除最前面元素
            que.push(nums[i]) #滑动窗口前加入最后面的元素
            result.append(que.front()) #记录对应的最大值
        return result

下面是GPT4生成的代码,更加简洁易懂

from collections import deque

def maxSlidingWindow(nums, k):
    if not nums:
        return []
    window = deque()
    result = []
    for i, x in enumerate(nums):
        # 如果滑动窗口左边界存在元素,且左边界元素小于当前元素,我们就将其从窗口中移除
        while window and nums[window[-1]] <= x:
            window.pop()
        # 新元素从右边入窗口
        window.append(i)
        # 当左边界元素的索引小于滑动窗口的左边界,我们就将其从窗口中移除
        if window[0] == i - k:
            window.popleft()
        # 当窗口的大小达到k的时候开始记录答案
        if i >= k - 1:
            result.append(nums[window[0]])
    return result

347. 前 K 个高频元素 - 力扣(Leetcode)

时间原因,直接看代码,用到了heapq的heappush和heappop

#时间复杂度:O(nlogk)
#空间复杂度:O(n)
import heapq
class Solution:
    def topKFrequent(self, nums: List[int], k: int) -> List[int]:
        #要统计元素出现频率
        map_ = {} #nums[i]:对应出现的次数
        for i in range(len(nums)):
            map_[nums[i]] = map_.get(nums[i], 0) + 1
        
        #对频率排序
        #定义一个小顶堆,大小为k
        pri_que = [] #小顶堆
        
        #用固定大小为k的小顶堆,扫描所有频率的数值
        for key, freq in map_.items():
            heapq.heappush(pri_que, (freq, key))
            if len(pri_que) > k: #如果堆的大小大于了K,则队列弹出,保证堆的大小一直为k
                heapq.heappop(pri_que)
        
        #找出前K个高频元素,因为小顶堆先弹出的是最小的,所以倒序来输出到数组
        result = [0] * k
        for i in range(k-1, -1, -1):
            result[i] = heapq.heappop(pri_que)[1]
        return result

GPT4给出的代码更为简单

import collections
import heapq

def topKFrequent(nums, k):
    count = collections.Counter(nums)   
    return heapq.nlargest(k, count.keys(), key=count.get)

这段代码首先用collections.Counter计算每个元素出现的频率,然后用heapq.nlargest来找出频率最高的k个元素。

这个算法的时间复杂度是O(n log k),其中n是数组的长度,因为Counter的计算需要O(n)的时间,而heapq.nlargest需要O(n log k)的时间。

需要注意的是,heapq.nlargest函数返回的结果可能不是按频率降序排序的,如果需要按频率降序排序的结果,可以使用sorted函数再排序一次。

下面是不调包的解法

def topKFrequent(nums, k):
    # Step 1: 构建哈希表,记录每个元素的频率
    freq_map = {}
    for num in nums:
        if num in freq_map:
            freq_map[num] += 1
        else:
            freq_map[num] = 1

    # Step 2: 将哈希表的键值对转化为列表,按照频率进行降序排序
    freq_list = [(key, value) for key, value in freq_map.items()]
    freq_list.sort(key=lambda x: x[1], reverse=True)

    # Step 3: 提取出频率最高的k个元素
    return [freq_list[i][0] for i in range(k)]

关于小顶堆

小顶堆(也称为最小堆)是一种特殊的树形数据结构,它满足以下性质:

  • 它是一颗完全二叉树:除了最底层,其它所有层的节点数都达到最大,最底层的节点都连续集中在左侧。
  • 任意节点的值都不大于其子节点的值。这意味着最小的元素总是位于根节点。

小顶堆常常被用于解决需要快速找到一组数据中最小值的问题。它的主要操作有:

  • 插入:将新的元素插入到堆中,并重新调整堆的结构使其保持小顶堆的性质。时间复杂度是O(log n)。
  • 删除最小值:删除根节点(即当前的最小值),并重新调整堆的结构使其保持小顶堆的性质。时间复杂度是O(log n)。
  • 堆化:将一个无序的数组调整为一个小顶堆。时间复杂度是O(n)。

以下是一个使用Python实现小顶堆的基本代码:

class MinHeap:
    def __init__(self):
        self.heap = []

    def parent(self, i):
        return (i - 1) // 2

    def left_child(self, i):
        return 2 * i + 1

    def right_child(self, i):
        return 2 * i + 2

    def insert(self, key):
        self.heap.append(key)
        self.heapify_up(len(self.heap) - 1)

    def heapify_up(self, i):
        while (i != 0) and (self.heap[self.parent(i)] > self.heap[i]):
            self.heap[i], self.heap[self.parent(i)] = self.heap[self.parent(i)], self.heap[i]
            i = self.parent(i)

    def extract_min(self):
        if self.size() == 1:
            return self.heap.pop()
        root = self.heap[0]
        self.heap[0] = self.heap[-1]
        self.heap.pop()
        self.heapify_down(0)
        return root

    def heapify_down(self, i):
        left = self.left_child(i)
        right = self.right_child(i)
        smallest = i

        if (left < self.size()) and (self.heap[i] > self.heap[left]):
            smallest = left
        if (right < self.size()) and (self.heap[smallest] > self.heap[right]):
            smallest = right
        if smallest != i:
            self.heap[i], self.heap[smallest] = self.heap[smallest], self.heap[i]
            self.heapify_down(smallest)

    def size(self):
        return len(self.heap)

这个类实现了小顶堆的基本操作,包括插入一个元素,删除最小元素,以及从一个元素开始向上或向下调整堆的结构。

关于快排

快速排序是一种使用分治策略(Divide and Conquer)的排序算法。其基本思想是,选择一个基准元素,将小于基准元素的值放在其左边,将大于或等于基准元素的值放在其右边,这样基准元素就位于了整个序列的中间,这个过程被称为分区(partition)。然后,对基准元素左右两边的子序列分别进行快速排序。递归这个过程,直到所有子序列都只包含一个元素,这时候整个序列就已经被排序完成。

以下是一个简洁易懂的快速排序的 Python 实现:

def quicksort(arr):
    if len(arr) <= 1:
        return arr
    pivot = arr[len(arr) // 2]
    left = [x for x in arr if x < pivot]
    middle = [x for x in arr if x == pivot]
    right = [x for x in arr if x > pivot]
    return quicksort(left) + middle + quicksort(right)

这个函数首先检查输入数组的长度,如果长度小于或等于1,就直接返回数组,因为长度为1的数组已经是有序的。

然后函数选择中间的元素作为基准元素,然后通过列表推导式将数组分为三部分:小于基准的元素,等于基准的元素,和大于基准的元素。然后递归地对小于基准和大于基准的部分进行快速排序,最后返回排序后的结果。

注意这个实现虽然简洁,但并不是最优的。因为它需要额外的空间来存储左、中、右三个部分,而且需要遍历数组三次来分割三个部分。更高效的实现通常会在原地进行分区和排序,只需要O(log n)的额外空间(用于函数调用栈)并且只需要遍历一次数组。

以下是一个更优的快速排序实现,该实现在原地(in-place)进行排序,避免了额外的空间消耗,而且只需要一次遍历即可完成分区。

def quicksort(arr, low, high):
    if low < high:
        pi = partition(arr, low, high)
        quicksort(arr, low, pi - 1)
        quicksort(arr, pi + 1, high)

def partition(arr, low, high):
    pivot = arr[high]
    i = low - 1
    for j in range(low, high):
        if arr[j] <= pivot:
            i += 1
            arr[i], arr[j] = arr[j], arr[i]
    arr[i+1], arr[high] = arr[high], arr[i+1]
    return i + 1

# 调用方式:
arr = [10, 7, 8, 9, 1, 5]
n = len(arr)
quicksort(arr, 0, n-1)
print("Sorted array is:", arr)

上述代码中,quicksort函数接受一个待排序的数组和两个索引作为参数,分别代表待排序子数组的首尾。它首先检查待排序子数组的长度,如果长度大于1,就调用partition函数对其进行分区,然后递归地对基准元素左右两边的子数组进行快速排序。

partition函数则负责进行分区。它首先选择子数组的最后一个元素作为基准元素,然后从左到右遍历子数组,将小于或等于基准元素的值移到基准元素的左边。在遍历过程中,变量i始终指向了最后一个小于或等于基准元素的位置。遍历完之后,基准元素被放置在正确的位置(即i+1的位置),然后函数返回基准元素的位置。

注意这个实现只需要O(log n)的额外空间(用于函数调用栈),并且只需要一次遍历就可以完成分区。因此,它比前面提供的简洁版本更高效。

总结

  1. 需要掌握通过deque实现单调队列的方法
  2. heapq实现小顶堆的方法需要了解
  3. 复习了一下快排的内容

附录

代码随想录算法训练营第十三天 | 239. 滑动窗口最大值、347. 前 K 个高频元素_小蛙先森的博客-CSDN博客

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值