当前位置: 代码网 > it编程>编程语言>C/C++ > 【数据结构】第十六弹---C语言实现希尔排序

【数据结构】第十六弹---C语言实现希尔排序

2024年07月31日 C/C++ 我要评论
希尔排序的预排序实现,整体实现,时空复杂度分析,与直接插入排序的性能比较~~~

个人主页:

💗系列专栏: 【c语言详解】 【c++详解】

目录

1、希尔排序( 缩小增量排序 )

1.1、预排序实现

1.2、希尔排序代码实现

1.3、代码测试

1.4、时空复杂度分析

1.5、性能比较

总结


上一弹我们学习了直接插入排序,通过时空复杂度分析,时间复杂度为o(n^2),一般情况效率较低,有没有对直接插入排序进行优化的排序呢???没错,我们这一弹讲解的排序就是对直接插入排序的优化的排序!!!
 

1、希尔排序( 缩小增量排序 )

希尔排序是一种基于插入排序的算法,通过引入增量的概念来改进插入排序的性能

动图如下: 

实现思路

1.1、预排序实现

预排序:

假设当前增量为5:

首先,增量为5,我们将数组元素分为增量(5)个子序列,每个子序列由原数组中相隔增量位置上的元素组成。所以我们有如下子序列:


然后对每个子序列进行独立的插入排序:

一趟排序之后的数组:

完成了一轮希尔排序,此时整个数组并不完全有序,但是已经比原始的数组更接近有序了。然后减小增量,通常是将原来的增量除以2(或者除以3+1),现在选择下一个增量为 2,按照此排序规则继续预排序即可,直到增量为1时,则为直接插入排序,此时则排序完成。

一个子序列排序实现:

int gap;
int end;
int tmp = a[end + gap];
while (end >= 0)
{
	if (a[end] > tmp)
	{
		a[end + gap] = a[end];
		end-=gap;
	}
	else
    {
		break;
    }
}
a[end + gap] = tmp;

与直接插入代码不同的是,这里对end所加减的均为gap;

单次插入完成后,我们来控制单个子序列的整个过程,每实现一次排序,下一次插入的数据为end+gap。

单趟排序实现:

int gap;

for (int i = 0; i < n-gap; i += gap)
{
	int end = i;
	int tmp = a[end + gap];
	while (end >= 0)
	{
		if (a[end] > tmp)
		{
			a[end + gap] = a[end];
			end -= gap;
		}
		else
        {
			break;
	    }
    }
	a[end + gap] = tmp;
}

这里for循环的条件为 i <n-gap 防止数组越界.

完成单个子序列的排序后,我们再对整个子序列排序:

int gap;
for (int j = 0; j < gap; j++)
{
	for (int i = 0; i < n - gap; i += gap)
	{
		int end = i;
		int tmp = a[end + gap];
		while (end >= 0)
		{
			if (a[end] > tmp)
			{
				a[end + gap] = a[end];
				end -= gap;
			}
			else
            {
				break;
		    }
       }
		a[end + gap] = tmp;
	}
}

外层循环(for (int j = 0; j < gap; j++))意在对每个以gap为间隔的分组进行遍历。

优化:

int gap = 3;
	
for (int i = 0; i < n - gap; i++)
{
	int end = i;
	int tmp = a[end + gap];
	while (end >= 0)
	{
		if (a[end] > tmp)
		{
			a[end + gap] = a[end];
			end -= gap;
		}
		else
        {
			break;
	    }
    }
	a[end + gap] = tmp;
}

这里我们将原先代码中的i += gap修改为i++意味着这次不是按照一组一组进行了,是一次排序完每个组的第二个元素,再进行下一个元素的排序。 

1.2、希尔排序代码实现

我们先对预排序的增量进行分析:

所以在实现希尔排序时,给gap固定值是行不通的。

void shellsort(int* a, int n)
{
	int gap = n;
	while (gap > 1)
	{
		gap = gap / 3 + 1;//博主写的是/3+1也可以是gap/2
		for (int i = 0; i < n - gap; i++)
		{
			int end = i;
			int tmp = a[end + gap];
			while (end >= 0)
			{
				if (a[end] > tmp)
				{
					a[end + gap] = a[end];
					end -= gap;
				}
				else
                {
					break;
                }
			}
			a[end + gap] = tmp;
		}
	}
}

这里无论gap是奇数还是偶数,这里gap最终都会除以到值为1。

在这里:

这里gap预排序次数还是有点多,因此我们可以再次进行修改,让gap每次除以3,为了使gap最后能回到1,我们进行加一处理。

 注意:

1.3、代码测试

测试代码:

//测试希尔排序
int main()
{
	int a[] = { 9,8,7,6,5,4,3,2,1,0 };//给一组数据
	int sz = sizeof(a) / sizeof(a[0]);//计算数组元素个数
	printf("排序前:\n");
	arrayprint(a, sz);
	shellsort(a, sz);
	printf("排序后:\n");
	arrayprint(a, sz);
	return 0;
}

测试结果:

1.4、时空复杂度分析

《数据结构(c语言版)》--- 严蔚敏
 

《数据结构-用面相对象方法与c++描述》--- 殷人昆

时间复杂度:

因为咋们的gap是按照knuth提出的方式取值的,而且knuth进行了大量的试验统计,我们暂时就按照:o(n^1.25) 到  o(1.6* n^1.25) 来算。

空间复杂度:

插入排序的空间复杂度为o(1),因为它是一个原地排序算法,不需要额外的存储空间来排序。

1.5、性能比较

我们在前面一弹提到了clock()函数可以获取程序启动到函数调用时之间的cpu时钟周期数,我们在这里通过具体的排序算法来进行比较性能。

注意:clock()函数的头文件是#include<time.h>,时间的单位为毫秒。

尽量使用release模式进行测试,因为release效率更高。

测试代码:

void testop()
{
	srand(time(0));//随机数种子
	const int n = 100000;
	int* a1 = (int*)malloc(sizeof(int) * n);//动态开辟n个元素
	int* a2 = (int*)malloc(sizeof(int) * n);
	for (int i = 0; i < n; ++i)
	{
		a1[i] = rand() + i;//随机数只有3万,为了更加随机再加上i
		a2[i] = a1[i];
	}
    //clock计算程序运行到此时的时间 毫秒
	int begin1 = clock();//排序前程序运行时间
	insertsort(a1, n);
	int end1 = clock();//排序后程序运行时间
	int begin2 = clock();
	shellsort(a2, n);
	int end2 = clock();
	printf("insertsort:%d\n", end1 - begin1);//程勋运行时间的差值即排序运行的时间
	printf("shellsort:%d\n", end2 - begin2);
	free(a1);//释放空间
	free(a2);
}

当n为10万时,release版本测试出来的结果: 

 

 当n为100万时,release版本测试出来的结果: 

希尔排序的特性总结:

总结


本篇博客就结束啦,谢谢大家的观看,如果公主少年们有好的建议可以留言喔,谢谢大家啦!

(0)

相关文章:

版权声明:本文内容由互联网用户贡献,该文观点仅代表作者本人。本站仅提供信息存储服务,不拥有所有权,不承担相关法律责任。 如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 2386932994@qq.com 举报,一经查实将立刻删除。

发表评论

验证码:
Copyright © 2017-2025  代码网 保留所有权利. 粤ICP备2024248653号
站长QQ:2386932994 | 联系邮箱:2386932994@qq.com