排序篇(贰)直、希尔、选、冒泡

但问耕耘,莫问前程


上一篇呢,讲了排序中较为复杂的排序——堆排序。这一篇主要围绕,直接插入、希尔、选择、冒泡排序、计数排序总结。

(1)直接插入排序:

插入排序的实现:

直接插入排序的思想很简单,就是把前K个数看成有序,再让后K个数有序插入到K个有序序列中。

首先封装个打印排序后数组的函数和原始数组

void PrintSort(int* a, int n)
{
	for (int i = 0;i < n;i++)
	{
		printf("%d ", a[i]);
	}
	printf("\n");
}

void TestInsertSort()
{
	int arr[] = { 4,6,7,3,2,8,9,1,5,0 };
	InsertSort(arr,sizeof(arr)/sizeof(int));
	PrintSort(arr, sizeof(arr) / sizeof(int));
}

在写排序代码的时候,更偏向于先写一趟排序的思想再来写整个排序的过程。 

所以在设置end位置的时候是很重要的。所以我们可以在外层套一个循环,每次移动完数据后,及时更新end。

	for (int i = 0;i < n;i++)
	{
		int end = i;  //有序的区间
		int tmp = a[end + 1];
      while(end>=0)
      {
		if (a[end] > tmp)
		{
            a[end+1]=a[end];  //end+1 < end end的数往后挪
			end--;
		}
		else
		{
			a[end + 1] = tmp;
		}
      } 
	}

 代码优化如下:

for (int i = 0;i < n-1;i++)
	{
			int end = i;  //有序的区间
			int tmp = a[end + 1];
			while (end >= 0)
			{
				if (a[end] > tmp)
				{
					a[end+1] = a[end ];  //如果小 则让end往后挪
					end--;
				}
				else
				{
					break;
				}
			}
			a[end + 1] = tmp; //再把保存的原end+1的数给到对的位置
	}

还有一点值得注意:

 在设计外层循环的时候,一定是n-1 ,而不是n。因为当n=10,那么i=9的时候,end+1一定会越界访问元素。

一个极小的数(随机数)被end+1访问,并插在了最前面。

 

 插入排序的性能:

可以看出,插入排序 会把数组的每个元素走一遍。所以它最坏的时间复杂度O(N^2)。例如降序。

没有多余的空间开辟,空间复杂度为O(1)。

且它是一种稳定的排序


(2)希尔排序:

希尔排序本质上,是对直接插入排序的优化,其思想也就比直接插入排序复杂,实现起来也较为复杂。比起直接插入排序,希尔排序多个预排序的概念。

预排序:让一个数组的序列接近有序。

让间隔为gap的数 先进行预排。

 

为什么要设置这个gap 的概念了?

我们可以试想一下,如果没有gap,也就是每次移动一次。9在0~9里面的首位,需要挪动9次才能到末尾。但如果有gap的存在,当gap=3时,只需要三次就可以了。极大地提高了效率。

 假设gap=3;

int end = 0;
	int gap = 3;
	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 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<n-gap个?

根据上面的规律,当gap越小,也就越接近于有序,那么当gap=1的时候,也就是直接插入排序了。

这里对gap 的变量理解有两种:

        

第一种:

 

第二种: 

最后:

	int gap = n;
    //gap情况下的排序
	while (gap >1)
	{
		//gap = gap / 3 + 1;  
		gap = 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;
		}
	}

 希尔排序的性能:

官方给的希尔排序的时间复杂度为O(N^1.3~N^2);

但就我而言,我认为希尔排序的时间复杂度为O(N*㏒₂N~N*log₃N)。

 但希尔排序的不稳定,因为你不知道预排序的时候,相同数的相对位置有什么变化。


(3)选择排序:

选择排顾名思义:
每一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,直到全部待排序的数据元素排完 。
这里我使用双指针的方式来排序:

 

int left = 0;
	int right = n - 1;
	while (left < right)
	{
		int minIndex = left;
		int maxIndex = left;
		//每次left都有变动 用i的区间 来控制left 和 right!!
		for (int i = left;i <=right;i++)
		{
			if (a[i] < a[minIndex])
			{
				minIndex = i;
			}
			if (a[i] > a[maxIndex])
			{
				maxIndex = i;
			}
		}

		Swap(&a[minIndex], &a[left]);
		//这里需要注意
		Swap(&a[maxIndex], &a[right]);
		++left;
		--right;
	}

 因为比较简单也就不过多分析,

不过在找数组最大、最小值到时候,外层循环的i一定要跟着left走!

但是此时的代码仍然存在问题!

//加上个修正就行了
Swap(&a[minIndex], &a[left]);
		//这里需要注意
		if (maxIndex == left)
		{
			maxIndex=minIndex;
		}
		Swap(&a[maxIndex], &a[right]);

 

选择排序的性能:

选择排序犹豫它 排序要遍历整个数组,效率极低,所以在实际中很少用。

时间复杂度达到O(N^2);   且不稳定。


 (4)冒泡(交换)排序:

冒泡排序是初学者 首先接触到的排序方式。

基本思想:

就是序列前后两个 两两两比较、交换。
序的特点是:将值较大的数向序列的后移动,值小的数向序列的前移动。
//总共执行多少趟?
	for (int i = 0;i < n;i++)
	{
		//每趟冒泡交换的次数  i为控制每趟排序的个数
		for (int j = 0;j < n - 1-i;j++)
		{
			if (a[j] > a[j + 1])
			{
				Swap(&a[j], &a[j + 1]);
			}
		}
	}

 当然,可以对冒泡排序做一个优化。也就是当已经是有序(升序)的情况下,也就不用再交换。

 

 冒泡排序的性能:

冒泡排序比较的对数为n-1,n-2,n-3......2,1,成等差数列

因此冒泡排序的时间复杂度也较高O(N^2):

但具有稳定性!!


(5)计数排序:

基本思想:

计数排序的思想很简单,开辟一个和原数组同样大的数组,并以此作为原数组对应位置数的映射。

最后依次写回数组。

 

int max = a[0];
	int min = a[0];
	//确定最大值和最小值 找到范围range
	for (int i = 0;i < n;++i)
	{
		if (a[i] > a[max])
		{
			max = a[i];
		}
		
		if (a[i] < min)
		{
			min = a[i];
		}
	}

	int  range = max - min + 1;
	//+1? 0~9个数  9-0=9;  但事实上有10个数
	//开辟计数数组
	int* Count = (int*)malloc(sizeof(int) * range);
	memset(Count, 0, sizeof(int) * range);

 完成找到range和开辟数组后,就开始计数。

	//计数
	for (int i = 0;i <n;i++)
	{
		//在下标a[i]-min 的处 ++;
		Count[a[i] - min]++;
	}

	//控制原数组的起始位置
	int i = 0;
	//拷回
	for (int j = 0;j <range ;j++)
	{
		while (Count[j]--)  //记录有多少个同样的数
		{
			a[i++] = j + min;
		}
	}

 

 计数排序的性能:

计数排序的时间复杂度可以达到O(N+range),是一种很厉害的排序。

空间复杂度也在O(range);

当然,计数排序的局限性在于很适用于,大范围 数据密集的情况下;


第二篇的排序内容也就讲完了~

感谢你阅读,祝你好运~

 

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值