算法设计与分析实验:堆排序与分治

目录

一、合并K个升序链表

1.1 采用堆排序的思路

1.2 采用优先队列的思路

1.3 采用分治的思路及具体测试

二、数据流中的中位数

​编辑2.1 具体思路

2.2 代码实现

2.3 测试结果

三、数组中的第k个最大元素

3.1 采用分治思路

3.2 采用最小堆

四、 最小K个数

4.1 采用快速排序思路

4.2 采用堆的思想


一、合并K个升序链表

力扣第23题

1.1 采用堆排序的思路

  1. 具体思路

首先,我们遍历链表数组,将每个链表的头节点添加到一个列表中。

创建一个哑节点dummy和一个指针cur,哑节点用于标记合并后的链表的头节点,cur指针用于遍历合并后的链表。

对列表中的节点进行堆排序,使得列表中最小的节点位于堆顶。

当堆不为空时,取出堆顶节点(即最小节点),将其连接到cur指针的后面,并更新cur指针为新加入的节点。

如果该节点还有下一个节点,则将下一个节点加入到堆中。

重复步骤4和步骤5,直到堆为空。

返回哑节点的下一个节点作为合并后的链表的头节点。

(2)流程展示:以[1,4,5],[1,3,4],[2,6]为例

链表数组:[1 -> 4 -> 5, 1 -> 3 -> 4, 2 -> 6]

步骤1 - 遍历链表数组,将每个链表的头节点添加到列表中:

节点列表:[1, 1, 2]

步骤2 - 创建哑节点和指针cur:

dummy -> None

cur -> None

步骤3 - 对节点列表进行堆排序,使得最小节点位于堆顶:

堆:[1, 1, 2]

步骤4 - 取出堆顶节点,连接到cur指针的后面,并更新cur指针:

dummy -> 1 -> None

cur -> 1

步骤5 - 将下一个节点加入到堆中:

堆:[1, 2]

步骤4 - 取出堆顶节点,连接到cur指针的后面,并更新cur指针:

dummy -> 1 -> 1 -> None

cur -> 1

步骤5 - 将下一个节点加入到堆中:

堆:[1]

步骤4 - 取出堆顶节点,连接到cur指针的后面,并更新cur指针:

dummy -> 1 -> 1 -> 2 -> None

cur -> 2

步骤5 - 堆为空,结束。

合并后的链表:dummy -> 1 -> 1 -> 2 -> None

(3)代码实现

class ListNode:
    def __init__(self, val=0, next=None):
        self.val = val
        self.next = next


def mergeKLists(lists):
    # 创建一个列表存储链表头节点
    nodes = []
    for lst in lists:
        if lst:
            nodes.append(lst)

    # 定义堆排序函数
    def heapify(arr, i, n):
        smallest = i
        left = 2 * i + 1
        right = 2 * i + 2

        if left < n and arr[left].val < arr[smallest].val:
            smallest = left

        if right < n and arr[right].val < arr[smallest].val:
            smallest = right

        if smallest != i:
            arr[i], arr[smallest] = arr[smallest], arr[i]
            heapify(arr, smallest, n)

    # 构建堆
    n = len(nodes)
    for i in range(n // 2 - 1, -1, -1):
        heapify(nodes, i, n)

    # 创建哑节点和指针
    dummy = ListNode(0)
    cur = dummy

    # 合并链表
    while nodes:
        node = nodes[0]
        cur.next = node
        cur = cur.next

        if node.next:
            nodes[0] = node.next
        else:
            nodes[0] = nodes[-1]
            nodes.pop()

        heapify(nodes, 0, len(nodes))

    return dummy.next

# 创建链表
def createLinkedList(nums):
    dummy = ListNode(0)
    cur = dummy
    for num in nums:
        cur.next = ListNode(num)
        cur = cur.next
    return dummy.next

# 打印链表的值
def printLinkedList(head):
    res = []
    cur = head
    while cur:
        res.append(cur.val)
        cur = cur.next
    print(res)

# 测试
lists = [[1,4,5],[1,3,4],[2,6]]
linkedLists = [createLinkedList(lst) for lst in lists]
mergedList = mergeKLists(linkedLists)
printLinkedList(mergedList)

(4)时间/空间复杂度分析

对于给定的链表数组,假设其中一共有k个链表,每个链表的平均长度为n,则该算法的时间复杂度可以分为两部分:

构建堆的时间复杂度:O(k)。

取出最小节点、加入下一个节点并进行堆排序的时间复杂度:假设在列表中一共有m个节点,则每个节点会进出堆一次,所以总共需要进行2m次操作。根据大根堆的性质,堆排序的时间复杂度为O(mlogk)。

所以该算法的总时间复杂度为O(k + mlogk)。

空间复杂度方面,除了存储答案链表的空间外,我们只需要维护一个长度为k的数组来存储每个链表的头节点,所以空间复杂度为O(k)。

需要注意的是,在构建堆的过程中,我们只是将链表的头节点添加到了一个列表中,并没有将链表整个拷贝一遍,所以我们并没有使用额外的O(nk)的空间,而只是使用了O(k)的空间。

1.2 采用优先队列的思路

(1)具体思路

首先,我们需要定义一个优先队列(或最小堆)来存储链表节点。优先队列是一种能够按照节点值大小进行自动排序的数据结构。

遍历链表数组,将每个链表的头节点添加到优先队列中。

创建一个哑节点dummy和一个指针cur,哑节点用于标记合并后的链表的头节点,cur指针用于遍历合并后的链表。

从优先队列中取出节点,将其连接到cur指针的后面,并更新cur指针为新加入的节点。然后,将该节点所在链表的下一个节点(如果有)加入到优先队列中。

重复步骤4,直到优先队列为空。

返回哑节点的下一个节点作为合并后的链表的头节点。

(2)思路简图

在这个简图中,最主要的就是优先队列了。我们可以看到,首先将每个链表的头节点加入优先队列中,并且优先队列会自动进行排序(从小到大)。在遍历优先队列的过程中,每次取出堆顶的最小节点,将其连接到cur指针的后面,并更新cur指针为新加入的节点。然后,将该节点所在链表的下一个节点(如果有)加入到优先队列中。不断地重复这个过程直到优先队列为空,最终返回哑节点的下一个节点作为合并后的链表的头节点。

(3)代码实现

import heapq

# 定义链表节点类
class ListNode:
    def __init__(self, val=0, next=None):
        self.val = val
        self.next = next

def mergeKLists(lists):
    # 创建一个哑节点,方便操作
    dummy = ListNode(0)
    cur = dummy

    # 创建一个优先队列,每个元素都是包含当前节点值、链表索引、节点本身的三元组
    pq = []
    for i in range(len(lists)):
        if lists[i]:
            heapq.heappush(pq, (lists[i].val, i, lists[i]))

    # 对优先队列进行循环,每次取出最小值
    while pq:
        val, i, node = heapq.heappop(pq)
        cur.next = node
        cur = cur.next

        if node.next:
            heapq.heappush(pq, (node.next.val, i, node.next))

    # 将合并后的链表值转换为列表形式,并按照题目要求进行排序
    result = []
    while dummy.next:
        result.append(dummy.next.val)
        dummy = dummy.next
    result.sort()

    # 输出合并后的链表值
    print(result)

# 测试代码
lists = [ListNode(1, ListNode(4, ListNode(5))), ListNode(1, ListNode(3, ListNode(4))), ListNode(2, ListNode(6))]
mergeKLists(lists)

(4)复杂度分析

创建优先队列:遍历lists列表,时间复杂度为O(m),其中m是所有链表中节点的总数。

循环取出最小值:最坏情况下每个节点都会被取出一次,时间复杂度为O(mlogk),其中k是链表的数量。

将合并后的链表值转换为列表并排序:遍历合并后的链表,时间复杂度为O(m);排序操作时间复杂度为O(mlogm),其中m为结果列表的长度。

输出合并后的链表值:遍历结果列表,时间复杂度为O(m)。

综上所述,整个代码的时间复杂度为O(mlogk + mlogm)。其中,m是所有链表中节点的总数,k是链表的数量。

空间复杂度方面,除了存储输入链表外,额外使用了一个优先队列和结果列表。所以空间复杂度为O(m + k)。

1.3 采用分治的思路及具体测试

(1)具体思路

将k个有序链表合并为一个有序链表。如果我们直接将它们合并,时间复杂度最坏为O(kN),其中N为所有链表节点数的总和。分治法是一种非常适合解决此类问题的算法。具体来说,对于一个k个有序链表的数组,我们可以将其分成两部分,分别递归地进行排序,然后将两个排好序的部分合并。这样,每次排序后链表数量都会减半,因此,递归的次数为logk级别。

在每次递归中,我们可以通过合并两个有序链表的方式来合并两个子链表。这个过程可以参考归并排序中两个有序列表的合并过程。具体来说,我们可以用两个指针p和q分别指向两个有序链表头部,然后比较这两个链表当前元素的大小,将较小的元素加入到合并后的链表中,并将指针后移。最后,如果其中一个链表为空,就将另一个链表直接加入到合并后的链表中。

(2)思路简图

链表数组:[1 -> 4 -> 5, 1 -> 3 -> 4, 2 -> 6]

在这个简图中,我们使用了分治思想将链表数组划分成两个子问题,每次递归只处理两个子问题。通过不断地递归合并,最终得到了合并后的有序链表。在合并的过程中,我们可以利用归并排序的思想,比较两个链表头部元素的大小,并将较小的元素加入到合并后的链表中。不断地重复这个过程,直到合并完所有的子链表,最终得到了一个有序的合并链表。由于每次递归都会将链表数量减半,所以递归的次数为logk级别,因此整体的时间复杂度为O(Nlogk),其中N为所有链表节点数的总和。

(3)代码实现

# 定义链表节点类
class ListNode:
    def __init__(self, val=0, next=None):
        self.val = val
        self.next = next

def mergeTwoLists(l1, l2):
    # 如果其中一个链表为空,直接返回另一个链表
    if not l1:
        return l2
    if not l2:
        return l1

    # 定义哑节点和当前节点指针
    dummy = ListNode(0)
    cur = dummy

    # 比较两个链表当前节点值的大小,将较小的那个加入合并后的链表
    while l1 and l2:
        if l1.val < l2.val:
            cur.next = l1
            l1 = l1.next
        else:
            cur.next = l2
            l2 = l2.next
        cur = cur.next

    # 将剩余的链表节点加入合并后的链表中
    if l1:
        cur.next = l1
    if l2:
        cur.next = l2

    return dummy.next


def mergeKLists(lists):
    # 如果链表数组为空,返回空链表
    if not lists:
        return None

    # 如果链表数组中只有一个链表,直接返回该链表
    if len(lists) == 1:
        return lists[0]

    # 将链表数组分为两个子数组,递归地调用mergeKLists函数
    mid = len(lists) // 2
    left = mergeKLists(lists[:mid])
    right = mergeKLists(lists[mid:])

    # 将分别合并后的两个子链表通过合并两个有序链表的方式合并成一个链表
    return mergeTwoLists(left, right)

# 创建链表
def createLinkedList(nums):
    head = ListNode(0)
    cur = head
    for num in nums:
        cur.next = ListNode(num)
        cur = cur.next
    return head.next

# 打印链表
def printLinkedList(head):
    if not head:
        print("链表为空")
    else:
        node = head
        while node:
            print(node.val, end=" ")
            node = node.next
        print()

# 测试示例
lists = [[1,4,5],[1,3,4],[2,6]]
linkedLists = []
for nums in lists:
    linkedLists.append(createLinkedList(nums))

mergedList = mergeKLists(linkedLists)
printLinkedList(mergedList)

(4)复杂度分析

代码中,mergeTwoLists()函数用于合并两个有序链表,mergeKLists()函数则是实现了上述的分治思想。时间复杂度为O(Nlogk),其中N为所有链表节点数的总和,k为链表数量,空间复杂度为O(logk)。

(5)运行结果(三种思路结果一致)

I 如下,测试子列表有[1,4,5],[1,3,4],[2,6]合并结果如下

II 下面是第二种情况,即子链表为空的情况

二、数据流中的中位数

力扣第160题

2.1 具体思路

可以使用两个堆(优先队列)来实现。

一个最大堆(Max Heap),用于存储较小的一半元素

一个最小堆(Min Heap),用于存储较大的一半元素

具体算法如下:

创建一个最大堆(Max Heap)和一个最小堆(Min Heap)。

当需要往数据结构中添加元素时:

先将元素插入到最大堆。

再从最大堆中取出堆顶元素,并将其插入到最小堆。

如果最小堆的元素个数比最大堆多,再从最小堆中取出堆顶元素,并将其插入到最大堆。

当需要计算中位数时:

如果最大堆和最小堆中元素个数相同,则中位数为两个堆顶元素的平均值。

否则,中位数为最大堆的堆顶元素。

2.思路流程展示

这个图示例展示了一个最大堆和一个最小堆的结构。在这个实例中,最大堆存储了较小的一半元素(1, 2, 3, 4, 5),最小堆存储了较大的一半元素(6, 7, 8, 9, 10, 11, 12)。这种方式可以确保最大堆的堆顶元素是整个数据结构的中位数。

当插入新元素时,先将该元素插入最大堆,然后再考虑平衡操作。如果最小堆的元素个数多于最大堆,就将最小堆的堆顶元素移动到最大堆。这样可以保持两个堆的平衡性,同时确保最大堆的堆顶元素是整个数据结构的中位数。

需要注意的是,对于奇数个元素,最大堆和最小堆的大小会相等,此时中位数为两个堆顶元素的平均值。对于偶数个元素,最大堆的大小比最小堆大1,此时中位数为最大堆的堆顶元素。

这种使用两个堆来实现的方法可以在O(logN)的时间复杂度内插入新元素和计算中位数,其中N为数据结构中元素的个数。

2.2 代码实现

import heapq

class MedianFinder:
    def __init__(self):
        self.max_heap = []  # 存储较小的一半元素(最大堆)
        self.min_heap = []  # 存储较大的一半元素(最小堆)

    def addNum(self, num: int) -> None:
        heapq.heappush(self.max_heap, -num)  # 将元素插入最大堆
        heapq.heappush(self.min_heap, -heapq.heappop(self.max_heap))  # 将最大堆的堆顶元素插入最小堆

        if len(self.min_heap) > len(self.max_heap):
            heapq.heappush(self.max_heap, -heapq.heappop(self.min_heap))  # 如果最小堆的元素个数比最大堆多,将最小堆的堆顶元素插入最大堆

    def findMedian(self) -> float:
        if len(self.max_heap) == len(self.min_heap):
            return (-self.max_heap[0] + self.min_heap[0]) / 2.0  # 中位数为堆顶元素的平均值
        else:
            return -self.max_heap[0]  # 中位数为最大堆的堆顶元素

medianFinder = MedianFinder()
medianFinder.addNum(1)
medianFinder.addNum(2)
print(medianFinder.findMedian())  # 输出 1.5
medianFinder.addNum(3)
print(medianFinder.findMedian())  # 输出 2.0

2.3 复杂度分析

这段代码使用了Python的heapq模块来实现堆的操作,并提供了addNum()和findMedian()两个方法。

对于复杂度分析:

addNum()操作:

对于大顶堆self.small,使用heapq.heappushpop()函数将当前元素-num插入到self.small中,返回的是堆顶元素的相反数,即pop出的最大值。

对于小顶堆self.large,使用heapq.heappushpop()函数将当前元素num插入到self.large中,返回的是堆顶元素,即pop出的最小值。

当两个堆的大小不平衡时,根据奇偶情况选择插入到哪个堆中,并将之前pop出的值插入到另一个堆中。

考虑到heappush()和heappop()都是O(logN)的时间复杂度,因此addNum()操作的总时间复杂度为O(logN)。

findMedian()操作:

如果两个堆的大小相等,取出大顶堆的堆顶元素(负数)和小顶堆的堆顶元素,计算平均值,时间复杂度为O(1)。

如果小顶堆self.large的大小大于大顶堆self.small的大小,直接取出小顶堆的堆顶元素,时间复杂度为O(1)。

因此,findMedian()操作的总时间复杂度为O(1)。

总的空间复杂度为O(N),因为堆中最多存储N/2个元素。

2.3 测试结果

输出如下

三、数组中的第k个最大元素

力扣第215题

3.1 采用分治思路

(1)具体思路

可以将该问题转化为寻找排序后第 n - k 小的元素的问题,因为这两个问题是等价的。对于这种问题,我们可以使用快速选择算法,它与快速排序类似。

具体而言,我们可以先选择一个枢轴值 pivot,并将数组中所有小于 pivot 的元素放在其左侧,大于 pivot 的元素放在其右侧。此时,pivot 本身所处的位置就代表了其在整个数组中的排名。如果 pivot 的排名恰好为 n-k,那么它就是第 k 大的数字了;否则,如果 n-k 在 pivot 的左侧,我们只需要在左半部分寻找第 k 大的数字;如果 n-k 在 pivot 的右侧,我们只需要在右半部分寻找第 k 大的数字。

快速选择算法的时间复杂度是 O(n) 的,也就是说平均情况下只需要线性时间复杂度就能找到第 k 大的数字。

(2)代码实现

from typing import List
import random


class Solution:
    def findKthLargest(self, nums: List[int], k: int) -> int:
        n = len(nums)
        pivot_index = self.partition(nums, 0, n - 1)

        while n - k != pivot_index:
            if n - k < pivot_index:
                pivot_index = self.partition(nums, 0, pivot_index - 1)
            else:
                pivot_index = self.partition(nums, pivot_index + 1, n - 1)

        return nums[pivot_index]

    def partition(self, nums: List[int], left: int, right: int) -> int:
        # 随机选择枢轴元素
        pivot_index = random.randint(left, right)
        pivot_value = nums[pivot_index]
        # 将枢轴元素交换到最右侧
        nums[pivot_index], nums[right] = nums[right], nums[pivot_index]

        # 将比枢轴小的元素交换到左边
        store_index = left
        for i in range(left, right):
            if nums[i] < pivot_value:
                nums[i], nums[store_index] = nums[store_index], nums[i]
                store_index += 1

        # 将枢轴元素放到它最终的位置上
        nums[store_index], nums[right] = nums[right], nums[store_index]

        return store_index
def test_findKthLargest():
    s = Solution()

    nums = [3, 2, 1, 5, 6, 4]
    k = 2
    result = s.findKthLargest(nums, k)
    print(result)

test_findKthLargest()

3.2 采用最小堆

(1)具体思路

创建一个最小堆,并初始化为空。

遍历数组中的元素,逐个将元素加入堆中。

如果堆的大小超过了 k,就弹出堆顶元素,保持堆的大小为 k。

最终,堆顶的元素就是数组中第 k 个最大的元素。

在这个算法中,我们通过维护一个最小堆来实现找到第 k 个最大元素。最小堆的特点是堆顶元素始终是堆中的最小值,当堆的大小限制为 k 时,堆顶元素就是第 k 个最大的元素。

(2)算法实现流程如下:

(3)代码实现

import heapq

def findKthLargest(nums, k):
    heap = nums[:k]
    heapq.heapify(heap)  # 将数组转换为最小堆

    for i in range(k, len(nums)):
        if nums[i] > heap[0]:
            heapq.heappop(heap)  # 弹出堆顶元素
            heapq.heappush(heap, nums[i])  # 将当前元素加入堆中

    return heap[0]  # 返回堆顶元素

# 测试样例
nums = [3, 2, 1, 5, 6, 4]
k = 2
print(findKthLargest(nums, k))  # 输出: 5

nums = [3, 2, 3, 1, 2, 4, 5, 5, 6]
k = 4
print(findKthLargest(nums, k))  # 输出: 4

(3)复杂度分析

这段代码使用了最小堆来找到数组中第 k 个最大的元素。下面是其时间和空间复杂度的分析:

时间复杂度:

初始化最小堆 heap 的时间复杂度为 O(k)。

处理剩余 n-k 个元素的时间复杂度为 O((n-k) log k)。

因此,总时间复杂度为 O(k + (n-k) log k) = O(n log k)。

空间复杂度:

需要一个大小为 k 的最小堆来存储前 k 个最大的元素,因此空间复杂度为 O(k)。

综上所述,这段代码的时间复杂度为 O(n log k),空间复杂度为 O(k)。其中,n 表示数组的长度。对于一个输入数组较大,且 k 比较小的情况,这个算法的时间复杂度比直接对整个数组排序的时间复杂度更优秀。

(4)运行结果(两种思路一样)

四、 最小K个数

力扣第1714题

4.1 采用快速排序思路

(1)具体思路

我们可以使用快速排序的思路来解决这个问题。快速排序中每次划分都会把一个数归位,并返回其下标。如果这个下标比 k-1 小,说明前面已经有 k-1 个数比它小了,我们只需要在它后面继续找即可;如果这个下标比 k-1 大,说明前面的 k-1 个数中必有一部分在它的左边,我们只需要在它左边继续找即可。

I 具体步骤:

定义一个函数 partition,用于对数组进行划分,并返回枢轴元素 pivot 的下标。

在 partition 函数中,选择最右边的元素作为枢轴元素,然后使用双指针法对数组进行划分,使得左边的元素都小于等于 pivot,右边的元素都大于 pivot。

如果 pivot 的下标等于 k-1,说明前 k 个最小的元素就是 arr[:k]。如果 pivot 的下标小于 k-1,则在 pivot 右边继续寻找,否则在 pivot 左边继续寻找。

II 流程展示

假设我们要找到数组中第 k 小的元素,初始时整个数组是无序的。我们选择最右边的元素作为枢轴元素(pivot),然后使用双指针法对数组进行划分。

下面是初始状态的数组示例:

[8, 4, 2, 9, 3, 6, 1, 5, 7]

我们选择最右边的元素 7 作为枢轴元素,并使用双指针法进行划分。划分的过程如下:

指针 i 和指针 j 分别从数组的左右两端开始向中间遍历。当指针 i 找到一个比枢轴元素小或者相等的元素时,就将其与指针 j 所指向的元素交换。这样,所有小于或等于枢轴元素的元素都会被放到左边,大于枢轴元素的元素都会被放到右边。

然后,继续移动指针 i 和指针 j 直到相遇。最后,将枢轴元素放到合适的位置,这里是将枢轴元素与指针 i 所指向的元素交换。交换后的结果如下:

此时,枢轴元素 7 已经归位,并且在它的左边都是比它小的元素,在它的右边都是比它大的元素。

接下来,我们根据枢轴元素的位置与 k 的大小进行判断:

如果枢轴元素的下标正好等于 k-1,那么前 k 个最小的元素就是 arr[:k]。

如果枢轴元素的下标小于 k-1,那么前 k 个最小的元素一定在枢轴元素的右边,我们只需要在右边的子数组上继续进行划分查找即可。

如果枢轴元素的下标大于 k-1,那么前 k 个最小的元素一定在枢轴元素的左边,我们只需要在左边的子数组上继续进行划分查找即可。

这样,通过不断地划分和查找,最终我们就能找到第 k 小的元素。

(2)代码实现

def findKSmallest(arr, k):
    def partition(arr, left, right):
        pivot = arr[right]
        i = left
        for j in range(left, right):
            if arr[j] <= pivot:
                arr[i], arr[j] = arr[j], arr[i]
                i += 1
        arr[i], arr[right] = arr[right], arr[i]
        return i

    left, right = 0, len(arr) - 1
    while True:
        pivot_idx = partition(arr, left, right)
        if pivot_idx == k - 1:
            return sorted(arr[:k])
        elif pivot_idx < k - 1:
            left = pivot_idx + 1
        else:
            right = pivot_idx - 1

# 测试样例
arr = [1, 3, 5, 7, 2, 4, 6, 8]
k = 4
print(findKSmallest(arr, k))  # 输出:[1, 2, 3, 4]

(3)复杂度分析

该算法的时间复杂度为 O(n)O(n),其中 nn 是数组的长度。

在主函数中,分区函数 partition 的时间复杂度为 O(n)O(n)。每次分区后需要通过比较中位数所在位置与 k 的大小关系判断下一步操作,而每次分区在期望情况下可以将数组大小减半,所以最好情况下该算法的时间复杂度为 O(nlog⁡n)O(nlogn),最坏情况下退化为 O(n2)O(n2)。

另外,我们还需要考虑递归调用栈的空间复杂度。在最坏情况下,递归深度可能达到 O(n)O(n),因此空间复杂度也为 O(n)O(n)。

综上所述,该算法的时间复杂度为 O(n)O(n),最坏情况下的空间复杂度为 O(n)O(n)。

4.2 采用堆的思想

(1)具体思路

要找出数组中最小的k个数,可以使用堆排序的思想来解决。具体的步骤如下:

构建一个大小为k的最大堆(Max Heap),堆中的元素为arr数组中的前k个数。

从第k+1个元素开始遍历数组arr,对于每个元素num:

若num小于堆顶元素,则将堆顶元素替换为num,并进行堆调整,保持最大堆的性质。

若num大于或等于堆顶元素,则忽略该元素。

遍历完数组之后,最大堆中的元素就是arr中最小的k个数。

思路简图:

初始数组:[1, 3, 5, 7, 2, 4, 6, 8](k=4)

以上简图展示了整个过程,包括初始数组、构建最大堆和遍历数组替换堆顶元素的过程。最后的最大堆中的元素就是数组中最小的k个数。

(2)代码实现

import heapq


def findKSmallest(arr, k):
    max_heap = []
    # 构建大小为k的最大堆
    for i in range(k):
        heapq.heappush(max_heap, -arr[i])

    # 遍历数组
    for i in range(k, len(arr)):
        num = arr[i]
        if num < -max_heap[0]:
            heapq.heappop(max_heap)
            heapq.heappush(max_heap, -num)

    # 最大堆中的元素即为最小的k个数,取其相反数并逆序返回
    return [-x for x in reversed(max_heap)]


# 测试样例
arr = [1, 3, 5, 7, 2, 4, 6, 8]
k = 4
print(findKSmallest(arr, k))  # 输出:[1, 2, 3, 4]

(3)复杂度分析

这段代码的时间复杂度是O(nlogk),其中n是数组的长度。下面是对代码复杂度的分析:

构建大小为k的最大堆的时间复杂度为O(klogk)。

遍历数组的过程中,对于每个元素,最多需要进行一次堆操作(比较和插入),堆操作的时间复杂度是O(logk)。所以遍历数组的时间复杂度是O(nlogk)。

最后从最大堆中取出k个元素并逆序返回,时间复杂度是O(klogk)。

综上,整个算法的时间复杂度是O(klogk + nlogk + klogk) = O(nlogk)。

空间复杂度方面,算法使用了一个大小为k的最大堆作为辅助空间,因此空间复杂度是O(k)。

总结起来,这段代码在时间复杂度和空间复杂度上都是相对较优的,适用于处理较大规模的数据。

(4)运行结果(两种思路一致)

# 测试样例

arr = [1, 3, 5, 7, 2, 4, 6, 8]    k = 4

2024.1.29  天气:小雨

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/355459.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

MySQL知识点总结(二)——explain执行计划、SQL优化

MySQL知识点总结&#xff08;二&#xff09;——explain执行计划、SQL优化 explain执行计划typepossible_keyskeysextra SQL优化SQL优化的流程SQL优化技巧范围查询优化排序优化分组查询优化distinct优化分页查询优化join关联查询优化排序分页 关联查询分组 关联查询 排序in与…

基于FFT + CNN - BiGRU-Attention 时域、频域特征注意力融合的电能质量扰动识别模型

目录 往期精彩内容&#xff1a; 引言 1 快速傅里叶变换FFT原理介绍 第一步&#xff0c;导入部分数据&#xff0c;扰动信号可视化 第二步&#xff0c;扰动信号经过FFT可视化 2 电能质量扰动数据的预处理 2.1 导入数据 第一步&#xff0c;按照公式模型生成单一信号 2.2 …

【Android Gradle 插件】Gradle 基础配置 ② ( Gradle 空白项目构建示例演示 )

一、Gradle 空白项目构建示例演示 在任意一个空白目录 , 创建 build.gradle 构建脚本 , 该脚本是 Gradle 构建的入口 ; 在顶级目录和每个子工程 , 都要有单独的 build.gradle 构建脚本 ; 在 上述 build.gradle 构建脚本中添加如下代码 : println "Hello Gradle !"…

【IM】如何保证消息可用性(一)

目录 1. 基本概念1.1 长连接 和 短连接1.2 PUSH模式和PULL模式 2. 背景介绍2.1 理解端到端的思想 3. 方案选型3.1 技术挑战3.2 技术目标 1. 基本概念 在讲解消息可用性之前&#xff0c;需要理解几个通信领域的基本概念。 1.1 长连接 和 短连接 什么是长连接&#xff0c;短连接…

《幻兽帕鲁》火遍全球,上百个游戏角色竟被曝是AI生成的?

原创 | 文 BFT机器人 最近&#xff0c;一款名为《幻兽帕鲁》&#xff08;Palworld&#xff09;的开放世界生存游戏在社交网络平台上引发了热议&#xff0c;成为了当下最受关注的游戏之一。 这款游戏在1月19日于Steam平台上线抢先体验版本&#xff0c;仅仅24小时之内&#xff0…

2024.1.29每日一题

LeetCode 自由之路 自由之路通向自由&#xff0c;通向睡觉吧&#x1f604; 514. 自由之路 - 力扣&#xff08;LeetCode&#xff09; 题目描述 电子游戏“辐射4”中&#xff0c;任务 “通向自由” 要求玩家到达名为 “Freedom Trail Ring” 的金属表盘&#xff0c;并使用表盘…

K8s 安装部署-Master和Minion(Node)

K8s 安装部署-Master和Minion(Node) 操作系统版本&#xff1a;CentOS 7.4 Master &#xff1a;172.20.26.167 Minion-1&#xff1a;172.20.26.198 Minion-2&#xff1a;172.20.26.210&#xff08;后增加节点&#xff09; ETCD&#xff1a;172.20.27.218 先安装部署ETCD y…

[论文阅读] |RAG评估_Retrieval-Augmented Generation Benchmark

写在前面 检索增强能够有效缓解大模型存在幻觉和知识时效性不足的问题&#xff0c;RAG通常包括文本切分、向量化入库、检索召回和答案生成等基本步骤。近期组里正在探索如何对RAG完整链路进行评估&#xff0c;辅助阶段性优化工作。上周先对评估综述进行了初步的扫描&#xff0…

Python 使用重构重命名一键更改变量名的方法

一个变量有多处引用的情况下&#xff0c;需要重命名&#xff0c;可以使用重构重命名进行一键更改。 方法是:选择变量名–>右键–>Refactor–>Rename&#xff08;也可以使用快捷&#xff1a;选择变量后按下ShiftF6&#xff09;&#xff0c;然后直接输入新的变量名即可…

基于springboot游戏分享网站源码和论文

网络的广泛应用给生活带来了十分的便利。所以把游戏分享管理与现在网络相结合&#xff0c;利用java技术建设游戏分享网站&#xff0c;实现游戏分享的信息化。则对于进一步提高游戏分享管理发展&#xff0c;丰富游戏分享管理经验能起到不少的促进作用。 游戏分享网站能够通过互…

邻接矩阵、关联矩阵

邻接矩阵&#xff1a; 邻接矩阵是一种用来表示图中顶点间相互连接关系的矩阵。在邻接矩阵中&#xff0c;矩阵的行和列都代表图中的顶点。 对于无权图&#xff0c;如果顶点 i 和顶点 j 之间有一条边&#xff0c;则矩阵中的元素 Aij​&#xff08;位于第 i 行和第 j 列&#xff…

3338 蓝桥杯 wyz的数组IV 简单

3338 蓝桥杯 wyz的数组IV 简单 //C风格解法1&#xff0c;通过率50% #include<bits/stdc.h>int main(){std::ios::sync_with_stdio(false);std::cin.tie(nullptr);std::cout.tie(nullptr);int n; std::cin >> n;int ans 0;std::vector<int>a(n);for(auto &am…

基于Java SSM框架实现家政服务中介网系统项目【项目源码+论文说明】

基于java的SSM框架实现家政服务中介网系统演示 摘要 21世纪的今天&#xff0c;随着社会的不断发展与进步&#xff0c;人们对于信息科学化的认识&#xff0c;已由低层次向高层次发展&#xff0c;由原来的感性认识向理性认识提高&#xff0c;管理工作的重要性已逐渐被人们所认识…

【Git配置代理】Failed to connect to github.com port 443 问题解决方法

前言&#xff1a; 在学习代码审计时&#xff0c;有时会需要使用git去拉取代码&#xff0c;然后就出现了如下错误 看过网上很多解决方法&#xff0c;觉得问题的关键还是因为命令行在拉取/推送代码时并没有使用VPN进行代理。 解决办法 &#xff1a; 配置http代理&#xff1a;…

DOM 型 XSS 攻击演示(附链接)

一、介绍 DOM&#xff08;Document Object Model&#xff09;型 XSS&#xff08;Cross-Site Scripting&#xff09;攻击是一种 Web 应用程序中的安全漏洞&#xff0c;其特点是攻击者成功地注入了恶意脚本&#xff0c;这些脚本在用户的浏览器中执行&#xff0c;从而导致恶意行为…

第93讲:MySQL主从复制集群延时从库的核心概念以及使用

文章目录 1.延时从库的概念2.配置从库延时3.模拟主库误删除使用延时从库恢复数据3.1.模拟主库误删除操作3.2.利用从库延时恢复主库误删除的数据 1.延时从库的概念 延时从库和主从延时是两个概念&#xff0c;延时从库指的是认为手动配置一个从库延时复制主库的时间&#xff0c;…

蓝桥杯-循环节长度

两个整数做除法&#xff0c;有时会产生循环小数&#xff0c;其循环部分称为: 循环节。比如&#xff0c;11/136>0.8461553846153..... 其循环节为[846153] 共有 6 位。下面的方法&#xff0c;可以求出循环节的长度。请仔细阅读代码&#xff0c;并填写划线部分缺少的代码。 注…

响应式Web开发项目教程(HTML5+CSS3+Bootstrap)第2版 例5-3 getBoundingClientRect()

代码 <!doctype html> <html> <head> <meta charset"utf-8"> <title>getBoundingClientRect()</title> </head> <script>function getRect(){var obj document.getElementById(example); //获取元素对象var objR…

【BUG】联想Y7000电池电量为0且无法充电解决方案汇总

因为最近火灾很多&#xff0c;所以昨天夜晚睡觉的时候把插线板电源关掉了&#xff0c;电脑也关机了。 各位一定要注意用电安全&#xff0c;网上的那些事情看着真的很难受qvq。 第二天早上起床的时候一看发现电脑直接没电了&#xff0c;插上电源后也是显示 你一定要冲进去啊(ू˃…

编译Opencv3.3 版本遇到的Cuda版本变更导致:CUDA_nppicom_LIBRARY (ADVANCED)链接找不到的问题根本解法:

前言&#xff1a; Opencv 开源库的使用是必须的&#xff0c;但是&#xff0c;开源项目的特性&#xff0c;造成&#xff0c;版本的依赖性比较复杂&#xff0c; 尤其是针对某一款老硬件的SDK&#xff0c;往往随着某个开源库的使用&#xff0c;导致&#xff0c;无法编译的问题&am…