直接选择排序和堆排序以及TopK问题

文章详细介绍了直接选择排序的原理和优化,指出在特定情况下可能出现的问题并提供解决方案。接着,文章转向堆排序,解释了堆的逻辑结构,如何构建小堆和大堆,以及如何通过调整和交换实现升序排序。最后,讨论了使用堆排序解决TopK问题的高效算法,其时间复杂度近似于O(N)。
摘要由CSDN通过智能技术生成

直接选择排序是一个时间复杂度为O(N^2)的排序,而且任何情况下都是为O(N^2),实现它是很简单的,我在这里实现的是一个较为优化的版本,但是治标不治本,时间复杂度仍未O(N^2),实现思路为,每一趟排序找一个最大的,找一个最小的,OK,让我们先看看第一趟排序

代码如下:

void SelectSort(int *a, int n)
{    
    int begin = 0;
    int end = n-1;

    while(begin < end)
    {
        int mini = begin;
        int maxi = end;
        for(int i = begin; i <= end; i++)
        {
            if(a[i] < a[mini])
            {
                mini = i;
            }
            if(a[i] > a[maxi])
            {
                maxi = i;
            }
        }
        
        Swap(&a[mini],&a[begin]);
        Swap(&a[maxi],&a[end]);
    }
}

 这样就将直接选择排序完成了,但真的对了吗?,请大家看一个问题,上面的初始数组为a[ ] = {4,1,8,2,6,3,7,9,5,0}, 经过上面的排序的确能排序成功,但是我只做一个小小的变动,我将最后一个数字 0 改成 10,如果你用上面的代码调试你会发现 结果会成为  1 2 5 4 6 7 8 3 9 10  ,这是因为什么?,让我们画一下图便知道了

 OK,当begin与maxi相等的时候,只需在在上面的基础上 修正一下maxi即可:

代码如下

void SelectSort(int *a, int n)
{    
    int begin = 0;
    int end = n-1;

    while(begin < end)
    {
        int mini = begin;
        int maxi = end;
        for(int i = begin; i <= end; i++)
        {
            if(a[i] < a[mini])
            {
                mini = i;
            }
            if(a[i] > a[maxi])
            {
                maxi = i;
            }
        }
        
        Swap(&a[mini],&a[begin]);
        if(begin == maxi)
        {    
            maxi = mini;
        }    
        Swap(&a[maxi],&a[end]);
    }
}

这样就OK了,但是这个排序的时间复杂度依旧是O(N^2);

堆排序:

因为之前已经介绍过堆排序;这里简单说一下;

堆排序物理结构是一个数组,但逻辑结构是一个完全二叉树,其父亲节点与孩子节点的位置关系为

parents = (child -1) /2;

leftchild = parents * 2+1;

rightchild = parents * 2 +2;

堆排序的前提是: 对于小顶堆来说,每一个父亲都小于他的孩子;

对于一个复杂问题我们首先要分成若干个子问题;

Ok,有这样一个数组, a[ ] = {9,1,2,3,4,5,6,7,8,0}; 对于这样一个数组,它的左右子树都为小堆,但是第一个数据和最后一个数据不符合要求,我们如将他们调整为小堆呢?

有人提出了一个这样的算法,向下调整算法,即从一个树的根节点开始向下调整,如果建小堆,那么 就让这个根节点与它的左右孩子中较小的一个进行比较,如果孩子小,则交换,然后位置更替;否则不交换,停止调整。OK,说了这么多,看看代码吧

void AdjustDown(int* a, int n,int root)
{
    assert(a);
    int parents = root;   
    int child = parents * 2 + 1;
    
    while(child < n)
    {
        if(child + 1 < n && a[child] > a[child + 1)   //child+1<n 防止越界
        {
            child++;
        }
        if(a[child] < a[parents])
        {
            Swap(&a[child],&a[parents]);
            parents = child;
            child = parents * 2 + 1;
        }
        else
        {
            break;
        }
    }
}

这样只调整了一个根节点,那么如何将一个随机数组调整为小堆呢?我们分析一下,叶子节点对于它自身来说,它既可以是小堆也可以是大堆,所以它不用做处理,那么从什么节点处理呢?有人已经总结出来了:从最后一个非叶子节点开始调整,而这个节点也很有特点,这个节点的位置正好是最后一个叶子节点的父亲. OK 代码如下

void HeapSort(int* a, int n)
{
    int i = (n-1-1)/2;  //最后一个非叶子节点
    for(i = (n-1-1)/2;i>=0;i--)
    {
        AdjustDown(a,n,i);   这样就可以建成一个小堆了
    }
}

既然小堆建立完成,那么如何排序(升序)呢?这首先要明确一个问题即,如果是排升序,我们应该建什么堆?是大顶堆呢?还是小顶堆?相信初学者可能很会容易想到要建小堆(当初我也是这么认为的),但是!如果建小堆就问题大了,因为你建小堆的话,把第一个小的数据拿了,那么剩余的数据的结构就全部乱套了,这样你就需要重新建堆,建堆的时间复杂度为O(N),那么这样下来整个对排序的时间复杂度就变成O(N^2),显然这是错误的。OK,聪明如你,你当然会想到要建大堆了,事实上,对于升序来说,堆排序需要建大堆,OK,大堆小堆的问题清楚了,那么接下来如何排序呢?有人提出了一个很精巧的方法,我将第一个数据和最后一个数据进行交换然后数据个数减减,对剩下的数进行向下调整,为什么这样做呢?那是因为这样不会导致其余数据的结构发生变化,即左子树依旧是大堆,右子树也是大堆,我们只需要对堆顶的数据排,那么对这个数据排的时间复杂度是多少呢,高度次即logN次,OK代码如下

void HeapSort(int* a, int n)
{
    int i = (n-1-1)/2;
    for(i = (n-1-1)/2;i >= 0;i--)
    {
        AdjustDown(a,n,i);
    } 

    int tmp = n-1;
    for(tmp = n-1;tmp > 0;tmp--)
    {
        Swap(&a[0],&a[tmp]);
        AdjustDown(a,tmp,0);
    }
}

那么这个对排序的时间复杂度是多少呢?O(NlogN)

这样,堆排序就完成了,是不是挺有意思的?在这里,如果有兴趣的朋友可以去测试一下这两个排序的性能,我在这里给读者们演示一下

在这里创建了100000个随机数,分别用直接插入和堆排序进行排序,时间单位为ms,读者们请看,这二者的差距是很大的。

关于堆排序,还有一个比较有趣的问题,即TopK问题,说啊,假设有N个数据,其内存存不下,数据存在磁盘里面的即文件里面,让我们找出这些数据里面前K(K远小于N)个大的数据。

那么这个问题怎么解决呢?这时候有人提出了一个非常精妙的方法!他怎么做的呢?他这样做的,他说我们先建一个K个数据构成的小堆(时间复杂度为 K),(因为K非常小,所以可以这样操作),在这里,我提一个问题,为什么要建小堆呢?他是这样想的,我们不是要找 前K个大的数据吗,那我就排除小的数据,

怎么排除呢,他说他建小堆,用小堆中堆顶的数据(最小的)去和剩下的(N - K)的数据比较,

遇到比堆顶数据大的,就交换,交换完,重新向下调整(时间复杂度logK);遇到比堆顶数据还小的,继续遍历下一个,直到遍历完。所以总的时间复杂度是O(K + logK * (N - K)),但又因为K是远小于N的,所以这个算法可以近似为O(N),可谓相当之厉害;OK,让我们以代码来试试吧

void HeapTopK(int* a, int n, int k)
{
	//第一步 建 数目为 k 的小堆
	int i = (k - 1 - 1) / 2;
	for (i = (k - 1 - 1) / 2; i >= 0; i--)   // 时间复杂度为 O(k)
	{
		//建小堆
		AdjustDown(a, k, i);
	}
	
	//第二步  与剩余的 n-k个数比较    找到了就交换,然后重新向下排序

	int j = k;
	for (int j = k; j < n; j++)
	{ 
		if (a[j] > a[0])
		{
			Swap(&a[j], &a[0]);
			AdjustDown(a, k, 0);
		}
	}

}

void Test(void)
{
	srand((unsigned int)time(0));

	int N = 100000;
	int* a = (int*)malloc(sizeof(int) * N);
	int i = 0;
	if (a != NULL)
	{
		for (i = 0; i < N; i++)
		{
			a[i] = rand();
			a[i] %= 1000000;
		}
		a[154]  = 1000000 + 1;  //定义了10个最大的数,便于验证
		a[2789] = 1000000 + 2;
		a[14]  =  1000000 + 3;
		a[512]  = 1000000 + 4;
		a[721]  = 1000000 + 5;
		a[162]  = 1000000 + 6;
		a[243]  = 1000000 + 7;
		a[500]  = 1000000 + 8;
		a[444]  = 1000000 + 9;
		a[1412] = 1000000 + 10;
	}

	
	int k = 10;
	HeapTopK(a, N, k);
	
}

 OK,这些就是所有内容了,如果你看到这里,感谢您的阅读,如有问题,请私信我,互相学习,更进一步。

评论 11
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值