一、题目陈述
给定一个长度为 n 的整数数列,以及一个整数 k ,请你求出数列从小到大排序后的第k个数。
题目翻译一下就是要从给定的集合中选定第k小的元素。本题采用分治算法进行处理较为适宜。
二、代码说明
分治算法本质是将一个大问题分解成若干个小问题,随后分别解决这些小问题,最后将解集合起来得到整个问题的解。在这个前提下,代码参考了快速排序的分区思想,但并没有对整个数组进行排序。通过分区操作,问题的规模被逐渐缩小,最终找到数组中第k小的元素。下面对代码进行具体的介绍。
快速排序分区示意图
Swap函数的作用是交换两个整数的值,它接受两个指向整数的指针作为参数,然后交换这两个指针所指向的整数的值。
void swap(int* a, int* b) {
int temp = *a;
*a = *b;
*b = temp;
}
Partition函数是快速排序算法中的关键步骤,其对数组进行分区,将小于基准值的数放在左边,大于基准值的数放在右边。其接受的参数中,’arr[]’是待分区的数组,’low’和’high’是分区范围。这个函数选择数组中最后一个元素作为基准值,通过遍历数组,将≤基准值的元素移到基准值的左边,>基准值的元素移到右边,最后将基准值放到合适的位置,它的返回值是基准值的索引位置。
int partition(int arr[], int low, int high) {
int pivot = arr[high]; // 选择最后一个元素作为基准值
int i = low - 1;
for (int j = low; j < high; j++) {
if (arr[j] <= pivot) {
i++;
swap(&arr[i], &arr[j]);
}
}
swap(&arr[i + 1], &arr[high]);
return i + 1;
}
FindKthSmallest函数用于找到数组中第k小的元素,包含了快速排序算法。接受的参数中,’arr[]’是待查找的数组,’low’和’high’是查找范围,‘k’是要找到的第k小的元素的位置。它通过调用’partition’函数将数组分区,然后根据基准值的索引位置与k的关系,递归地在左边或右边的子数组中继续查找,直到找到第k小的元素为止,最后返回第k小元素的值。
int findKthSmallest(int arr[], int low, int high, int k) {
if (low <= high) {
int pivot = partition(arr, low, high);
if (pivot == k - 1) {
return arr[pivot];
} else if (pivot > k - 1) {
return findKthSmallest(arr, low, pivot - 1, k);
} else {
return findKthSmallest(arr, pivot + 1, high, k);
}
}
return -1; // 数组为空或k超出范围时返回-1
}
在main函数中,首先读取数组大小’n’和要查找的第k小的元素的位置’k’,随后进行数组的输入,最后调用findKthSmallest函数完成计算。
int main() {
int n, k;
scanf("%d %d", &n, &k);
int arr[n];
for (int i = 0; i < n; i++) {
scanf("%d", &arr[i]);
}
int kthSmallest = findKthSmallest(arr, 0, n - 1, k);
printf("%d\n", kthSmallest);
return 0;
}
三、算法分析
通常来说,面对一般性选择问题的算法步骤为先对数组进行排序,随后根据索引取出第k小的数。这样的作法时间复杂度最优时为O(nlogn),通常为O(n^2)。
下面进行一般性选择问题解法示例。
#include <stdio.h>
// 冒泡排序
void bubbleSort(int arr[], int n) {
for (int i = 0; i < n - 1; i++) {
for (int j = 0; j < n - i - 1; j++) {
if (arr[j] > arr[j + 1]) {
int temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
}
}
}
}
int main() {
int n, k;
printf("输入数组S的长度: ");
scanf("%d", &n);
int arr[n];
printf("输入数组S的元素: ");
for (int i = 0; i < n; i++) {
scanf("%d", &arr[i]);
}
printf("输入k的值: ");
scanf("%d", &k);
bubbleSort(arr, n);
printf("第k小的元素为: %d\n", k, arr[k - 1]);
return 0;
}
这段代码使用冒泡排序,并根据输入的k值输出第k小的元素。冒泡排序在最坏情况下需要进行n次遍历,每次遍历要比较n-i-1次相邻元素并进行交换,因此冒泡排序的时间复杂度为O(n^2)。随后的输出第k小元素部分时间复杂度为O(1),因此这段代码的时间复杂度为O(n^2)。
下面将实验别的排序方法对时间复杂度的影响。
先测试堆排序。
#include <stdio.h>
// 堆排序核心函数
void heapify(int arr[], int n, int i) {
int largest = i; // 将当前节点初始化为最大值
int left = 2 * i + 1;
int right = 2 * i + 2;
// 如果左子节点大于根节点
if (left < n && arr[left] > arr[largest]) {
largest = left;
}
// 如果右子节点大于根节点
if (right < n && arr[right] > arr[largest]) {
largest = right;
}
// 如果最大值不是根节点,交换根节点和最大值
if (largest != i) {
int temp = arr[i];
arr[i] = arr[largest];
arr[largest] = temp;
// 递归调用堆化函数处理子树
heapify(arr, n, largest);
}
}
// 堆排序函数
void heapSort(int arr[], int n) {
// 建立最大堆,从最后一个非叶子节点开始
for (int i = n / 2 - 1; i >= 0; i--) {
heapify(arr, n, i);
}
// 逐个取出堆顶元素并调整堆
for (int i = n - 1; i > 0; i--) {
// 将当前堆顶元素(最大值)与最后一个元素交换
int temp = arr[0];
arr[0] = arr[i];
arr[i] = temp;
// 调整堆,将剩余元素重新堆化
heapify(arr, i, 0);
}
}
int main() {
int n, k;
printf("输入数组S的长度: ");
scanf("%d", &n);
int arr[n];
printf("输入数组S的元素: ");
for (int i = 0; i < n; i++) {
scanf("%d", &arr[i]);
}
printf("输入k的值: ");
scanf("%d", &k);
heapSort(arr, n);
printf("第k小的元素为: %d\n", arr[k - 1]);
return 0;
}
堆排序的时间复杂度为O(nlogn),与冒泡排序相比,在处理大型数据集时更占优势,不过在处理较少数据时,性能可能会低于冒泡排序。
下面测试快速排序。
#include <stdio.h>
// 交换两个元素
void swap(int* a, int* b) {
int temp = *a;
*a = *b;
*b = temp;
}
// 分区函数,将小于等于基准值的数放在左边,大于基准值的数放在右边
int partition(int arr[], int low, int high) {
int pivot = arr[high]; // 选择最后一个元素作为基准值
int i = low - 1;
for (int j = low; j < high; j++) {
if (arr[j] <= pivot) {
i++;
swap(&arr[i], &arr[j]);
}
}
swap(&arr[i + 1], &arr[high]);
return i + 1;
}
// 使用快速排序算法对数组进行排序
void quickSort(int arr[], int low, int high) {
if (low < high) {
int pivot = partition(arr, low, high);
quickSort(arr, low, pivot - 1);
quickSort(arr, pivot + 1, high);
}
}
int main() {
int n, k;
printf("输入数组S的长度: ");
scanf("%d", &n);
int arr[n];
printf("输入数组S的元素: ");
for (int i = 0; i < n; i++) {
scanf("%d", &arr[i]);
}
printf("输入k的值: ");
scanf("%d", &k);
quickSort(arr, 0, n - 1);
printf("第k小的元素为: %d\n", arr[k - 1]);
return 0;
}
快速排序算法与实验代码思想较为相似,均有分治的思想,不同之处在于前者先进行排序后直接取索引对应的值,后者则是通过一次次的分区进行不完全排序。
最后是本实验代码的运行时间。
通过几种方法的对比可以看出,冒泡排序在处理大量数据时速度显著减缓,堆排序和快速排序则受影响较小。然而,即便使用了高效率的排序算法,代码的运行速度仍然比不过使用分治算法的实验代码,分治算法节约了7~8倍的运行时间。
四、实验总结
本次实验以分治算法为核心思想,实现了找出第k小元素的代码。随后,将分治算法与一般选择性算法进行对比,并分别比较了冒泡排序、堆排序、快速排序等排序方式,通过程序输出运行时间,具体准确地进行时间复杂度比较。
首先,冒泡排序在数据量增大时算法效率明显下降,而即使是效率较高的快速排序与堆排序,在处理相同较大数据量时效率仍不及分治选择算法,后者平均节约7~8倍运行时间,得出结论:分治算法在面对较大数据量时是具有时间复杂度上的优势的。
最后结论是:
对于处理大规模数据集的排序问题,应优先选择堆排序或快速排序等时间复杂度较低的算法,以减少运行时间;而对于topK问题等需要找到第k小或第k大元素的情况,分治算法是更好的选择。
五、实验代码
#include <stdio.h>
// 交换两个整数的值
void swap(int* a, int* b) {
int temp = *a;
*a = *b;
*b = temp;
}
// 分区函数,将小于等于基准值的数放在左边,大于基准值的数放在右边
int partition(int arr[], int low, int high) {
int pivot = arr[high]; // 选择最后一个元素作为基准值
int i = low - 1;
for (int j = low; j < high; j++) {
if (arr[j] <= pivot) {
i++;
swap(&arr[i], &arr[j]);
}
}
swap(&arr[i + 1], &arr[high]);
return i + 1;
}
// 使用快速排序的思想找到第k小的数
int findKthSmallest(int arr[], int low, int high, int k) {
if (low <= high) {
int pivot = partition(arr, low, high);
if (pivot == k - 1) {
return arr[pivot];
} else if (pivot > k - 1) {
return findKthSmallest(arr, low, pivot - 1, k);
} else {
return findKthSmallest(arr, pivot + 1, high, k);
}
}
return -1; // 数组为空或k超出范围时返回-1
}
int main() {
int n, k;
scanf("%d %d", &n, &k);
int arr[n];
for (int i = 0; i < n; i++) {
scanf("%d", &arr[i]);
}
int kthSmallest = findKthSmallest(arr, 0, n - 1, k);
printf("%d\n", kthSmallest);
return 0;
}