首先看题
输入整数数组 arr ,找出其中最小的 k 个数。例如,输入4、5、1、6、2、7、3、8这8个数字,则最小的4个数字是1、2、3、4。
示例 1:
输入:arr = [3,2,1], k = 2 输出:[1,2] 或者 [2,1]
示例 2:
输入:arr = [0,1,2,1], k = 1 输出:[0]
看到排序,我啪的一下就站起来了,很快啊,上来就是sort
class Solution {
public:
vector<int> getLeastNumbers(vector<int>& arr, int k) {
sort(arr.begin(), arr.end());
return vector<int>(arr.begin(), arr.begin() + k);
}
};
效率很快,不愧是sort函数,然而面试官可能觉得你是来砸场子的,那么这题应该用什么排序算法呢?
一. 平均时间复杂度O(n2)的排序算法
1.冒泡排序
void BubbleSort(vector<int>& arr, int k) {
int temp;
bool flag;
for (int i = 0; i < k; i ++) {
flag = true;
for (int j = arr.size() - 1; j > i; j --) {
if (arr[j] < arr[j - 1]) {
temp = arr[j];
arr[j] = arr[j - 1];
arr[j - 1] = temp;
flag = false;
}
}
if (flag) return;
}
}
冒泡排序,此题只需交换k趟,所以时间复杂度为O(kn),空间复杂度O(1),然而...
2.选择排序
void SelectSort(vector<int>& arr, int k) {
int temp, minIndex;
for (int i = 0; i < k; i++) {
minIndex = i;
for (int j = arr.size() - 1; j > i; j--) {
if (arr[j] < arr[minIndex]) {
minIndex = j;
}
}
temp = arr[i];
arr[i] = arr[minIndex];
arr[minIndex] = temp;
}
}
选择排序,也只需交换k趟,所以时间复杂度为O(kn),空间复杂度O(1),结果还是一样
值得注意的是,此处
对比冒泡排序的
要多三个通过,所以这题选择排序比冒泡排序要略快一点,大概赋值操作较少,因为冒泡每次循环都会两两交换,进行三次赋值,而选择排序只需将下标赋值一次,然而这还是不足以通过此题。
3.插入排序
void InsertSort(vector<int>& arr, int k) {
int insertNum, i, j;
for (i = 1; i < arr.size(); i++) {
insertNum = arr[i];
for (j = i - 1; j >= 0; j--) {
if (arr[j] > insertNum) {
arr[j + 1] = arr[j];
}
else {
break;
}
}
arr[j + 1] = insertNum;
}
}
插入排序需要全部排序,时间复杂度为O(n^2),空间复杂度O(1),交换次数为在已有序序列中比自身大的值向后移位次数,在特定条件下,比如全列有序,此时不需移位,时间复杂度为O(n),那么来看一下这题的提交结果
插入排序居然AC了,不过时间炒鸡久,在超时的边缘徘徊。。
4.希尔排序
void ShellSort(vector<int>& arr) {
int n = arr.size(), insertNum, d, i, j;
for (d = n/2; d >= 1; d /= 2) {
for (i = d; i < n; i++) {
insertNum = arr[i];
for (j = i - d; j >= 0; j-=d) {
if (j >= 0 && arr[j] > insertNum) {
arr[j + d] = arr[j];
}
else {
break;
}
}
arr[j + d] = insertNum;
}
}
}
希尔排序时间复杂度,取决于每次的步长,一般来说为每次为长度/2,在每个子序列中进行插入排序,因为越有序插入排序所需比较和移位的次数越少,在每个子序列中都是相对有序的,所以希尔排序最好情况下时间复杂度为O(n^1.3),最坏情况下为O(n^2),而此题需要全排列后才能得到前k个,所以此题的时间复杂度不变,空间复杂度为O(1)
希尔排序AC~,希尔牛逼!(破音)
那么那些时间复杂度为O(nlogn)的算法要怎么实现又能怎么优化呢?
二. 平均时间复杂度O(nlogn)的排序算法
1.堆排序
其实堆排序有两种实现算法,我们都来实现一下:
①.对整个数组建立小根堆,每次取最小数,总共取k次,所以时间复杂度为O(klogn)
vector<int> HeapSort1(vector<int>& arr, int k) {
int length = arr.size(), temp;
BuildMinHeap(arr, length);
temp = arr[0];
arr[0] = arr[length - 1];
arr[length - 1] = temp;
for (int i = 1; i < k; i++) {
AdjustMinHeap(arr, 0, length - i);
temp = arr[0];
arr[0] = arr[length - i - 1];
arr[length - i - 1] = temp;
}
return vector<int>(arr.rbegin(), arr.rbegin() + k);
}
void BuildMinHeap(vector<int>& arr, int length) {
for (int i = (length - 1) / 2; i >= 0; i--) {
AdjustMinHeap(arr, i, length);
}
}
void AdjustMinHeap(vector<int>& arr, int i, int length) {
int index, nextIndex, temp = arr[i];
for (index = i; index * 2 + 1 < length;) {
if (index * 2 + 2 < length && arr[index * 2 + 1] > arr[index * 2 + 2]) {
nextIndex = index * 2 + 2;
}
else {
nextIndex = index * 2 + 1;
}
if (temp < arr[nextIndex]) {
break;
}
else {
arr[index] = arr[nextIndex];
index = nextIndex;
}
}
arr[index] = temp;
}
②.对前k个数建立大根堆,k后面的数每次与最大值比较,如果比最大值小则交换,重新构造大顶堆,到最后前k个数就是最小的k个数,所以时间复杂度为O(nlogk)
vector<int> HeapSort2(vector<int>& arr, int k) {
int length = arr.size(), temp;
BuildMaxHeap(arr, k);
for (int i = k; i < length; i++) {
if (arr[0] > arr[i]) {
temp = arr[0];
arr[0] = arr[i];
arr[i] = temp;
}
AdjustMaxHeap(arr, 0, k);
}
return vector<int>(arr.begin(), arr.begin() + k);
}
void BuildMaxHeap(vector<int>& arr, int length) {
for (int i = (length - 1) / 2; i >= 0; i--) {
AdjustMaxHeap(arr, i, length);
}
}
void AdjustMaxHeap(vector<int>& arr, int i, int length) {
int index, nextIndex, temp = arr[i];
for (index = i; index * 2 + 1 < length;) {
if (index * 2 + 2 < length && arr[index * 2 + 1] < arr[index * 2 + 2]) {
nextIndex = index * 2 + 2;
}
else {
nextIndex = index * 2 + 1;
}
if (temp > arr[nextIndex]) {
break;
}
else {
arr[index] = arr[nextIndex];
index = nextIndex;
}
}
arr[index] = temp;
}
可见两种实现方法的耗时差不多,所以两种方法皆可,由于①为klogn、②为nlogk,通过数学推导我们可以得到,当k<n^(k/n)时,使用第一种方法,当k>n^(k/n)时,使用第二种方法。
2.快速排序
void quickSort(vector<int>& arr, int left, int right, int k) {
if (left < right) {
int leftNum = left;
int rightNum = right;
int midNum = arr[left];
while (left < right) {
while (left < right && arr[right] >= midNum) right--;
arr[left] = arr[right];
while (left < right && arr[left] <= midNum) left++;
arr[right] = arr[left];
}
arr[left] = midNum;
if (left == k) {
return;
}
else if (left < k) {
quickSort(arr, left + 1, rightNum, k);
}
else {
quickSort(arr, leftNum, left - 1, k);
}
}
return;
}
快排不需要全排列,只需判断中值是否为k,如果小于k需要再判断右边,如果大于k需要判断左侧,直到取到k为止,这样左边就是最小的k个数,时间复杂度为O(n),空间复杂度由于没有额外申请了常数级参数,所以为O(1), 但如果算递归调用栈的大小,空间复杂度为递归的深度,一般来说为O(logn)。
快排果然快,根据内存消耗看,判题机制应该没有考虑递归栈的大小。
3.归并排序
归并排序是外排序,唯一使用了额外空间的排序,归并排序时间复杂度为O(nlogn),是一种稳定排序方法,空间复杂度为O(n)
vector<int> arr2; // 额外缓存数组
void MergeSort(vector<int>& arr, int left, int right) {
if (left < right) {
int mid = left + (right - left) / 2, i, j, k;
MergeSort(arr, left, mid);
MergeSort(arr, mid + 1, right);
for (i = left; i <= right; i++) {
arr2[i] = arr[i];
}
i = left; j = mid + 1; k = i;
while (i <= mid && j <= right) {
if (arr2[i] <= arr2[j]) {
arr[k++] = arr2[i++];
}
else {
arr[k++] = arr2[j++];
}
}
while(i <= mid) arr[k++] = arr2[i++];
while(j <= right) arr[k++] = arr2[j++];
}
return;
}
看来本题归并排序相对前面几个算法,O(nlogn)的时间还是偏慢的,且需要额外空间,此题不推荐,不过归并排序可以用作数据量较大的排序,因为当数据量巨大无法一次读取到内存中,需要分段排序,最后再汇总在一起。
三. 本题算法选择
那么本题推荐的算法为堆排序,快排,这两种排序在本题目时间复杂度都小于O(nlogn)。