问题描述
有n个整数,找出其中最小的k个数,时间复杂度尽可能低
解决这道问题有很多种方法,最常见的解法笔者认为有两大类,第一类是全排序,第二类是部分排序。
- 在全排序中,复杂度较低的,对于数量比较大的数据集而言,可以考虑采用快速排序和堆排序,复杂度O(nlogn)。对于数量比较小的数据集而言,可以考虑其他排序方式,比如插入排序。
- 对于部分排序,题目没有要求最小的k个数有序,也没有要求最后的(n-k)个数有序,因此也就没有必要对所有的元素进行排序。因此,部分排序的基本思想是:
1)从n个序列中找到前k个数,找到这k个数中的最大值max;
2)将max与剩余的(n-k)个数比较,若剩余的(n-k)个数中,有比它小的,>则将这个数与max换位;
重复执行以上两个步骤,直到剩余(n-k)个数里没有比max值更小的数为止。
因此,部分排序相对于全排序而言,又快捷了一层,但是选择什么样的方式进行部分排序又是一个优化的点。最容易想到的是在数组中完成这个算法功能,那么它的时间复杂度会是 O(klogk+(n-k)logk)=O(nlogk)
当然,部分排序也可以采用其他的排序方案,有时候,选择合适的数据结构也可以显著提升算法效率 :通过改变数据的存储结构,利用堆的特点,用堆代替数组,在相同的部分排序思想下,采用堆排序的时间复杂度优于其他排序,因此,笔者重点介绍这个方法。
方法1–部分排序,构建容量为k的大顶堆
算法描述
1)遍历给定整数的前k个数,建立一个容量为k的最大堆(根结点数值大于子结>点),假设这k个数即为给定n个数里最小的k个数,遍历和建堆耗时O(k),此时k个数里最大的数在根结点。
2)遍历剩余的(n-k)个数,将遍历到的数xi与堆顶元素比较,如果小于堆顶元素,则用xi替换堆顶元素,更新堆,这里的时间复杂度为O(logk);如果大于或等于堆顶元素,则继续往下遍历。
时间复杂度分析
最坏的情况,如果每次遍历剩余的(n-k)个数时,都需要更新堆,则时间复杂度为
建堆费用+更新堆费用=O(klogk+(n-k)logk)=O(nlogk)
算法实现
/***********************
Author:tmw
date:2017-11-16
update:2019-3-22
************************/
#include <stdio.h>
#include <stdlib.h>
#define swap(a,b,t) (t=a,a=b,b=t)
/**
* 寻找前k个数
* 方法2:维护一个大小为k的大顶堆
**/
//将无序序列转变成大顶堆序列
//规定无序元素从数组的1号地址开始填
void heap_sort_one(int* array, int father_index, int array_len)
{
int child_index=0;
int temp=0;
for(child_index=father_index*2; child_index<=array_len; child_index=child_index*2)
{
/**这里的child+1是因为后面有个array[child_index+1] 防止溢出**/
if(child_index+1<=array_len && array[child_index]<array[child_index+1])
child_index++;
if(child_index<=array_len && array[father_index]<array[child_index])
{
swap(array[father_index],array[child_index],temp);
father_index = child_index; //更新后继续更新孩子结点的左右子树
}
}
}
/**寻找最小的K个数 --- 维护一个K个元素的大顶堆
时间复杂度:建堆费用+更新堆费用=O(klogk+(n-k)logk)=O(nlogk)
**/
void heap_sort_topK(int* array, int array_len, int k)
{
int i;
int temp=0;
/**建立一个k的大顶堆**/
for(i=k/2; i>=1; i--) /**建堆:从下往上**/
heap_sort_one(array,i,k);
/** 原堆排序代码
for(i=array_len; i>0; i--)
{
swap(array[1],array[i],temp);
heap_sort_one(array,1,i-1);
}
**/
/**遍历集合setB={n-k,...,n}元素:
1)若存在元素b[i]<堆顶元素heapTOP,则swap(b[i],heapTop),重新维护当前前k个元素的大顶堆
2)若b[i]>堆顶元素heapTOP,则继续遍历setB中的元素
**/
for(i=array_len-k; i<=array_len; i++)
{
if(array[i]<array[1])
{
swap(array[i],array[1],temp);
heap_sort_one(array,1,k); /**更新堆:从上往下**/
}
}
printf("\n");
for(i=1; i<=k; i++)
printf("%d ",array[i]);
}
算法测试及测试结果
int main()
{
int i=0;
int array1[11]={0,0,4,3,9,10,1,-1,2,100,8};
for(i=1; i<11; i++)
printf("%d ",array1[i]);
printf("\n序列中最小的4个数为:");
heap_sort_topK(array1,10,4);
printf("\n\n");
int array2[10] ={0,50,10,90,30,70,40,80,60,20};
for(i=1; i<10; i++)
printf("%d ",array2[i]);
printf("\n序列中最小的4个数为:");
heap_sort_topK(array2,9,4);
return 0;
}
比起方法1,下面这种方法笔者比较喜欢,它能把平均复杂度降到线性
方法2–线性选择算法
算法描述
联想快速排序的思想,可以考虑这样一种快速选择算法fast_select(S,k)
算法开始:选取一个中轴元素pivot(可参考快速排序里的三数取中方法),将集合S分割成左右两边(左:Sl;右Sr),左边元素都比它小(或相等),右边元素都比它大;
1)如果k小于左边元素的个数,那么第k个最小的元素一定在中轴元素左半边,对左半边做递归:fast_select(Sl,k);
2)如果k等于左边元素个数加1,即中轴元素就是第k个最小元素,程序返回这个元素
3)如果k大于左边元素个数,那么第k个最小的元素一定在中轴元素右半边,对右半边做递归:fast_select(Sr,k-|Sl|-1);
关于程序的返回
如果要查找的k的个数小于Sl的个数,则直接返回Sl中较小的k个元素;
如果要查找的k的个数大于Sl的个数,则返回Sl中所有元素和Sr中较小的k-|Sr|个元素。
线性选择算法时间复杂度
线性选择算法在平均情况下能做到 O(n) 的时间复杂度
算法实现
/***********************
Author:tmw
date:2017-11-17
************************/
#include <stdio.h>
#include <stdlib.h>
#define swap(x,y,t) (t=x,x=y,y=t)
//三数取中
int median3(int *array , int low , int high)
{
int mid = (low+high)/2;
int temp;
if(array[high]<array[mid])
swap(array[high],array[mid],temp);
if(array[high]<array[low])
swap(array[high],array[low],temp);
//以上:保证array[high]为三者中最大
if(array[low]<array[mid])
swap(array[low],array[mid],temp);
//以上:保证array[low]为三者中的中间值
return array[low];
}
int fast_select( int* array , int low , int high , int k )
{
//三数取中,中枢元素存在array[low]中
array[low] = median3(array,low,high);
int pivot = array[low];//pivot对中枢元素的值做备份
int i = low;
int j = high;
while( i < j )
{
while( i < j && array[j] >= pivot )
j--;
array[i] = array[j];
while( i < j && array[i] <= pivot)
i++;
array[j] = array[i];
}
array[i]=pivot;//此时,pivot的左边都比它小,右边都比它大
/**线性选择主算法**/
if( k <= i )//如果k小于左边元素的个数,那么第k个最小的元素一定在中轴元素左半边,对左半边做递归
return fast_select(array,low,i-1,k);
if( k > i + 1)//如果k大于左边元素个数,那么第k个最小的元素一定在中轴元素右半边,对右半边做递归
return fast_select(array,i+1,high,k);
else
return array[i];
}
算法测试及测试结果
int main()
{
printf("测试代码\n");
int k = 4;
int i;
int a2[7] = {1,8,23,6,90,5,8};
printf("原串:\t ");
for(i=0;i<7;i++)
printf("%d ",a2[i]);
fast_select(a2,0,6,k);
printf("\n最小的%d个数:",k);
for(i=0;i<k;i++)
printf("%d ",a2[i]);
printf("\n");
int a4[9] = {100,56,23,7,17,0,23,65,2};
printf("\n\n原串:\t ");
for(i=0;i<9;i++)
printf("%d ",a4[i]);
fast_select(a4,0,8,k);
printf("\n最小的%d个数:",k);
for(i=0;i<k;i++)
printf("%d ",a4[i]);
return 0;
}
梦想还是要有的,万一实现了呢~~~ヾ(◍°∇°◍)ノ゙