单线程 CPU

标签: 数组 排序 堆(优先队列)

难度: Medium

给你一个二维数组 tasks ,用于表示 n​​​​​​ 项从 0n - 1 编号的任务。其中 tasks[i] = [enqueueTimei, processingTimei] 意味着第 i​​​​​​​​​​ 项任务将会于 enqueueTimei 时进入任务队列,需要 processingTimei 的时长完成执行。

现有一个单线程 CPU ,同一时间只能执行 最多一项 任务,该 CPU 将会按照下述方式运行:

  • 如果 CPU 空闲,且任务队列中没有需要执行的任务,则 CPU 保持空闲状态。
  • 如果 CPU 空闲,但任务队列中有需要执行的任务,则 CPU 将会选择 执行时间最短 的任务开始执行。如果多个任务具有同样的最短执行时间,则选择下标最小的任务开始执行。
  • 一旦某项任务开始执行,CPU 在 执行完整个任务 前都不会停止。
  • CPU 可以在完成一项任务后,立即开始执行一项新任务。

返回 CPU 处理任务的顺序。

 

示例 1:

输入:tasks = [[1,2],[2,4],[3,2],[4,1]]
输出:[0,2,3,1]
解释:事件按下述流程运行: 
- time = 1 ,任务 0 进入任务队列,可执行任务项 = {0}
- 同样在 time = 1 ,空闲状态的 CPU 开始执行任务 0 ,可执行任务项 = {}
- time = 2 ,任务 1 进入任务队列,可执行任务项 = {1}
- time = 3 ,任务 2 进入任务队列,可执行任务项 = {1, 2}
- 同样在 time = 3 ,CPU 完成任务 0 并开始执行队列中用时最短的任务 2 ,可执行任务项 = {1}
- time = 4 ,任务 3 进入任务队列,可执行任务项 = {1, 3}
- time = 5 ,CPU 完成任务 2 并开始执行队列中用时最短的任务 3 ,可执行任务项 = {1}
- time = 6 ,CPU 完成任务 3 并开始执行任务 1 ,可执行任务项 = {}
- time = 10 ,CPU 完成任务 1 并进入空闲状态

示例 2:

输入:tasks = [[7,10],[7,12],[7,5],[7,4],[7,2]]
输出:[4,3,2,0,1]
解释:事件按下述流程运行: 
- time = 7 ,所有任务同时进入任务队列,可执行任务项  = {0,1,2,3,4}
- 同样在 time = 7 ,空闲状态的 CPU 开始执行任务 4 ,可执行任务项 = {0,1,2,3}
- time = 9 ,CPU 完成任务 4 并开始执行任务 3 ,可执行任务项 = {0,1,2}
- time = 13 ,CPU 完成任务 3 并开始执行任务 2 ,可执行任务项 = {0,1}
- time = 18 ,CPU 完成任务 2 并开始执行任务 0 ,可执行任务项 = {1}
- time = 28 ,CPU 完成任务 0 并开始执行任务 1 ,可执行任务项 = {}
- time = 40 ,CPU 完成任务 1 并进入空闲状态

 

提示:

  • tasks.length == n
  • 1 <= n <= 105
  • 1 <= enqueueTimei, processingTimei <= 109

Submission

运行时间: 358 ms

内存: 54.1 MB

class Solution:
    def getOrder(self, tasks: List[List[int]]) -> List[int]:
        n = len(tasks)
        indices = list(range(n))
        indices.sort(key=lambda x: tasks[x][0])

        ans = list()
        # 优先队列
        q = list()
        # 时间戳
        timestamp = 0
        # 数组上遍历的指针
        ptr = 0
        
        for i in range(n):
            # 如果没有可以执行的任务,直接快进
            if not q:
                timestamp = max(timestamp, tasks[indices[ptr]][0])
            # 将所有小于等于时间戳的任务放入优先队列
            while ptr < n and tasks[indices[ptr]][0] <= timestamp:
                heapq.heappush(q, (tasks[indices[ptr]][1], indices[ptr]))
                ptr += 1
            # 选择处理时间最小的任务
            process, index = heapq.heappop(q)
            timestamp += process
            ans.append(index)
        
        return ans

Explain

该题解采用的是模拟CPU任务调度的方式。首先,通过索引排序,以确保任务按照入队时间被正确处理。使用优先队列(小顶堆)来管理和选择当前可执行的任务,依据是任务的执行时间和索引顺序。算法从模拟时间为0开始,遍历所有任务。如果当前没有任务可以执行,时间会跳转到下一个任务的入队时间。当有一个或多个任务可以执行时,会将它们按照执行时间推入优先队列中。每次从队列中取出执行时间最短的任务进行执行,并更新时间戳。通过这种方式,我们可以模拟CPU的操作过程,并按顺序记录每个任务的执行。

时间复杂度: O(n log n)

空间复杂度: O(n)

class Solution:
    def getOrder(self, tasks: List[List[int]]) -> List[int]:
        n = len(tasks)
        indices = list(range(n))
        indices.sort(key=lambda x: tasks[x][0])  # 按入队时间排序任务索引

        ans = list()
        q = list()  # 优先队列,用于选择最短执行时间的任务
        timestamp = 0  # 当前模拟的时间戳
        ptr = 0  # 指向当前考虑进队列的任务的指针
        
        for i in range(n):
            # 如果优先队列为空,跳转时间戳到下一个任务的入队时间
            if not q:
                timestamp = max(timestamp, tasks[indices[ptr]][0])
            # 将当前时间戳之前的所有任务加入优先队列
            while ptr < n and tasks[indices[ptr]][0] <= timestamp:
                heapq.heappush(q, (tasks[indices[ptr]][1], indices[ptr]))  # 入队:(执行时间,任务索引)
                ptr += 1
            # 从队列中取出执行时间最短的任务执行
            process, index = heapq.heappop(q)
            timestamp += process  # 更新时间戳为当前任务执行完毕的时间
            ans.append(index)  # 记录任务执行顺序
        
        return ans

Explore

优先队列(小顶堆)在这种情况下被使用,因为它能够高效地支持所需的操作。在CPU任务调度问题中,我们需要能够快速选择并移除具有最短执行时间的任务。使用小顶堆,我们可以在O(log n)的时间复杂度内插入新任务,并在同样的时间复杂度下获取和删除最小元素(即执行时间最短的任务)。相比之下,如果使用列表或链表,虽然插入操作的时间复杂度为O(1),但查找并删除最小元素的时间复杂度将会是O(n),这在任务数量较多时将显著增加算法的总体执行时间。因此,优先队列(小顶堆)是处理此类问题的更有效的数据结构。