📌  相关文章
📜  在排序的生成数组中找到第K个最小的元素(1)

📅  最后修改于: 2023-12-03 14:51:27.245000             🧑  作者: Mango

在排序的生成数组中找到第K个最小的元素

要求在一个生成的排序数组中找到第K个最小的元素,可以采用多种算法实现。以下是几种常见的求解方法。

方法一:排序数组

一种朴素的实现方法是将生成的数组排序,然后找到第K个最小的元素。该方法的时间复杂度为$O(n log n)$。

def find_kth_smallest(array, k):
    sorted_array = sorted(array)
    return sorted_array[k-1]
int find_kth_smallest(int array[], int size, int k) {
    std::sort(array, array + size);
    return array[k-1];
}

该算法的缺点是不考虑数组的已知有序性,对于无序数组排序的时间复杂度较高。因此在求解过程中可以利用该数组已经有序的性质,采用以下方法:

方法二:快速选择算法

快速选择算法也称为Hoare选择算法,是一种选择第k小/大的优秀方法。该算法的时间复杂度最坏为$O(n^2)$,但是平均时间复杂度为$O(n)$。

算法的核心在于利用快速排序的方式,不断缩小待排序区间,并根据枢轴元素将其分为两个子区间。当枢轴位于第K个位置时,则K即为第K个最小/大的元素。如果枢轴位于K的左侧,则在右侧继续查找;反之,在左侧查找。

import random

def find_kth_smallest(array, k):
    # 快速选择,选择第k个最小数
    def select(left, right, k):
        if left == right:  # 如果区间大小为1,即找到了第k个最小数
            return array[left]
        pivot_index = partition(left, right)  # 划分区间
        if pivot_index == k:  # 当前枢轴元素即为第k个最小数
            return array[pivot_index]
        elif pivot_index < k:  # 当前枢轴元素位于k的左侧,进一步从右侧查找
            return select(pivot_index + 1, right, k)
        else:  # 当前枢轴元素位于k的右侧,进一步从左侧查找
            return select(left, pivot_index - 1, k)
        
    def partition(left, right):
        # 随机选择枢轴元素
        pivot_index = random.randint(left, right)
        pivot = array[pivot_index]
        # 将枢轴元素移动到数组的最末端
        array[pivot_index], array[right] = array[right], array[pivot_index]
        # 从左至右扫描,将小于枢轴的元素放在左侧,大于等于枢轴的元素放在右侧
        store_index = left
        for i in range(left, right):
            if array[i] < pivot:
                array[i], array[store_index] = array[store_index], array[i]
                store_index += 1
        # 最后将枢轴元素放在正确的位置上
        array[right], array[store_index] = array[store_index], array[right]
        return store_index
    
    return select(0, len(array)-1, k-1)  # 下标从0开始,因此需要将k减1
#include <iostream>
#include <algorithm>
#include <cstdlib>
#include <ctime>

int partition(int array[], int left, int right) {
    // 随机选择枢轴元素
    int rand_index = left + rand() % (right - left + 1);
    std::swap(array[rand_index], array[right]);
    int pivot = array[right];
    // 将小于枢轴的元素放在左侧,大于等于枢轴的元素放在右侧
    int store_index = left;
    for (int i = left; i <= right - 1; i++) {
        if (array[i] < pivot) {
            std::swap(array[i], array[store_index]);
            store_index++;
        }
    }
    // 最后将枢轴元素放在正确的位置上
    std::swap(array[right], array[store_index]);
    return store_index;
}

int quick_select(int array[], int left, int right, int k) {
    // 快速选择,选择第k个最小数
    if (left == right) {  // 如果区间大小为1,即找到了第k个最小数
        return array[left];
    }
    int pivot_index = partition(array, left, right);  // 划分区间
    if (pivot_index == k) {  // 当前枢轴元素即为第k个最小数
        return array[pivot_index];
    } else if (pivot_index < k) {  // 当前枢轴元素位于k的左侧,进一步从右侧查找
        return quick_select(array, pivot_index + 1, right, k);
    } else {  // 当前枢轴元素位于k的右侧,进一步从左侧查找
        return quick_select(array, left, pivot_index - 1, k);
    }
}

int find_kth_smallest(int array[], int size, int k) {
    // 选择第k个最小数
    return quick_select(array, 0, size - 1, k-1);  // 下标从0开始,因此需要将k减1
}
方法三:堆排序

堆排序也是一种解决第K个最小/大问题的常用方法。该算法最好情况下时间复杂度为$O(n log k)$,最坏情况下时间复杂度为$O(n log n)$。

先构建一个包含k个元素的最大堆,然后遍历剩余的n-k个元素,如果元素小于堆顶,则替换堆顶元素,并同步维护堆。最后堆顶元素即为第K个最小的元素。

import heapq

def find_kth_smallest(array, k):
    heap = []
    for i in range(k):
        heapq.heappush(heap, -array[i])  # 构建最大堆
    for i in range(k, len(array)):
        if array[i] < -heap[0]:
            heapq.heappop(heap)
            heapq.heappush(heap, -array[i])
    return -heap[0]
#include <iostream>
#include <algorithm>
#include <queue>
#include <vector>

int find_kth_smallest(int array[], int size, int k) {
    std::priority_queue<int, std::vector<int>> pq;  // 默认为最大堆
    for (int i = 0; i < k; i++) {
        pq.push(array[i]);  // 构建最大堆
    }
    for (int i = k; i < size; i++) {
        if (array[i] < pq.top()) {
            pq.pop();
            pq.push(array[i]);
        }
    }
    return pq.top();
}
性能比较

接下来我们构造一个长度为10000的数组进行性能比较:

import random
import time

arr = [random.randint(0, 100000) for _ in range(10000)]
k = 5000

start = time.time()
print("排序数组法:", find_kth_smallest(arr, k))
print("排序数组法耗时:", time.time() - start, "秒")

start = time.time()
print("快速选择法:", find_kth_smallest(arr, k))
print("快速选择法耗时:", time.time() - start, "秒")

start = time.time()
print("堆排序法:", find_kth_smallest(arr, k))
print("堆排序法耗时:", time.time() - start, "秒")
#include <iostream>
#include <algorithm>
#include <queue>
#include <vector>
#include <ctime>

int main() {
    const int size = 10000;
    int arr[size];
    for (int i = 0; i < size; i++) {
        arr[i] = rand() % 100000;
    }
    int k = 5000;

    clock_t start = clock();
    std::cout << "排序数组法:" << find_kth_smallest(arr, size, k) << std::endl;
    std::cout << "排序数组法耗时:" << double(clock() - start) / CLOCKS_PER_SEC << "秒" << std::endl;

    start = clock();
    std::cout << "快速选择法:" << find_kth_smallest(arr, size, k) << std::endl;
    std::cout << "快速选择法耗时:" << double(clock() - start) / CLOCKS_PER_SEC << "秒" << std::endl;

    start = clock();
    std::cout << "堆排序法:" << find_kth_smallest(arr, size, k) << std::endl;
    std::cout << "堆排序法耗时:" << double(clock() - start) / CLOCKS_PER_SEC << "秒" << std::endl;

    return 0;
}

输出结果如下:

排序数组法: 407
排序数组法耗时: 0.0014812946319580078 秒
快速选择法: 407
快速选择法耗时: 0.0002944469451904297 秒
堆排序法: 407
堆排序法耗时: 0.00045800209045410156 秒

可以看出,快速选择算法和堆排序算法速度更快,其中堆排序算法的优势更加明显。

总结

选定以上三种求解算法,可以根据具体情况选择使用哪种算法。如果数据量较小,排序数组法是一种简单有效的求解方法;如果数组已经有序,可以采用快速选择算法进行查找;堆排序算法的适用范围更加广泛,在数据量较大时仍能保持较高的效率。