DS:八大排序之直接插入排序、希尔排序和选择排序

时间:2024-02-20 11:08:30

                                         创作不易,感谢三连支持!! 

一、排序的概念及运用

1.1 排序的概念

排序:所谓排序,就是使一串记录,按照其中的某个或某些关键字的大小,递增或递减的排列起               来的操作。
稳定性:假定在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记                   录的相对次序保持不变,即在原序列中,r[i]=r[j],且r[i]在r[j]之前,而在排序后的序列                   r[i]仍在r[j]之前,则称这种排序算法是稳定的;否则称为不稳定的。
内部排序:数据元素全部放在内存中的排序。
外部排序:数据元素太多不能同时放在内存中,根据排序过程的要求不能在内外存之间移动数据                      的排序。

关于这些基础概念我会在后面慢慢介绍! 

1.2 排序的运用

我们在淘宝购买商品的时候,可以选择让商品根据销量、信用、价格、综合程度进行排序

 还有高校排名,以及考试的排名,都是通过排序来完成的!!

排序存在的意义:帮助我们筛选出最优的选择

1.3 常见的排序算法

二、直接插入排序

2.1 思路

直接插入排序的思想:把待排序的记录按其关键码值的大小逐个插入到一个已经排好序的有序序列中,直到所有的记录插入完为止,得到一个新的有序序列 。

 这就和我们小时候玩扑克牌摸牌整理的一样,一次与前面的排比较找到合适的位置插入!

2.2 直接插入排序的实现

        当插入第i(i>=1)个元素时,前面的array[0],array[1],…,array[i-1]已经排好序,此时用array[i]的排序码与array[i-1],array[i-2],…的排序码顺序进行比较,找到插入位置即将array[i]插入,原来位置上的元素顺序后移

           我们先按照上面的思路,先模拟摸一张牌的过程,假设目前手上的牌是2 4 9 然后摸到了1张3,我们设置最后一张牌9的下标位置为end(2),然后让新摸的牌为temp(a[3]),开始慢慢往前比较,发现较大的就交换位置。

    int end=2;
	int temp=a[3];
	while (end >= 0)
	{
		if (a[end] > temp)//如果前面的数比后面的数大,就前面元素插入到后面的位置
			{        
              a[end + 1] = a[end];
             --end;
              }
		else
			break;
		
	}
	a[end+1] = temp;//不写在循环里面,是避免end减成-1,此时说明新加入的牌是最小的,正好放在一开始的位置

 上述过程可以实现插入一张牌,那么整体的实现就在外面加个for循序即可!!

void InsertSort(int* a, int n)
{
	for (int i = 0; i < n - 1; i++)
	{
		int end = i;
		int temp = a[i+1];
		while (end >= 0)
		{
			if (a[end] > temp)//如果前面的数比后面的数大,就前面元素插入到后面的位置
				a[end + 1] = a[end];
			else
				break;
			--end;
		}
		a[end + 1] = temp;//不写在循环里面,是避免end减成-1,此时说明新加入的牌是最小的,正好放在一开始的位置
	}
}

但要注意的是:外面的for循环的判断条件,i < n - 1, 也就是说i最多走到n - 2的位置即倒数第二个元素,原因是:tmp是每次要插入的元素,而tmp = a[end +1]是end的下一个位置,如果让end到最后一个元素的位置即n-1处,那tmp = a[end+1]就会越界!所以i只能到倒数第二个元素的位置!

2.3 复杂度分析

时间复杂度:O(N^2)  ---> 单趟是O(N),最坏情况N个元素都要走一次单趟(基本上逆序)

空间复杂度:O(1)  ---> 额外使用空间的个数是常数个

当要排序的序列接近有序时性能最好O(N)(接近有序)

三、希尔排序

3.1 思路

         希尔排序其实是直接插入排序的一种变形,我们知道对于直接插入排序来说,最坏的情况就是逆序,此时的时间复杂度就是O(N^2),最好的情况是接近有序,此时时间复杂度为O(N),这个时候希尔有了一个想法:有没有一种方法可以让一组无序的数据经过处理后使他接近有序,然后再最后实现一次直接插入排序呢?

      最后希尔发明出来了希尔排序

3.2 希尔排序的实现

具体思路:

1、对无序的数组进行预排序,使其接近有序。

2、最后再来一次直接插入排序

 这里的预排指的是:间隔gap的元素为一组,总计gap组,我们先假设gap为3,然后我们画个图来理解一下:

 根据我们之前写的直接插入排序算法,我们可以先实现将红色的一组进行排序的算法

int gap = 3;
for (int i = 0; i < n - gap; i+=gap)
{
	int end = i;
	int temp = a[i + gap];
	while (end >= 0)
	{
		if (a[end] > temp)//如果前面的数比后面的数大,就前面元素插入到后面的位置
			a[end + gap] = a[end];
		else
			break;
		end -= gap;
	}
	a[end + gap] = temp;
}

 我们发现,如果我们一开始让i=1,就可以实现蓝色组的排序,让i=2的话,就可以实现绿色组的排序,所以为了让三组都完成排序,我们再外面再嵌套一层循环!

int gap = 3;
for (int j = 0; j < gap; j++)
{
	for (int i = j; i < n - gap; i += gap)
	{
		int end = i;
		int temp = a[i + gap];
		while (end >= 0)
		{
			if (a[end] > temp)//如果前面的数比后面的数大,就前面元素插入到后面的位置
				a[end + gap] = a[end];
			else
				break;
			end -= gap;
		}
		a[end + gap] = temp;
	}
}

这样我们就实现了三组的预排序了!! 

但其实上面的代码还可以优化成两层循环!!

int gap = 3;
	for (int i = 0; i < n - gap; i ++)
	{
		int end = i;
		int temp = a[i + gap];
		while (end >= 0)
		{
			if (a[end] > temp)//如果前面的数比后面的数大,就前面元素插入到后面的位置
				a[end + gap] = a[end];
			else
				break;
			end -= gap;
		}
		a[end + gap] = temp;
	}

刚刚那种写法是一组一组去完成预排,而现在这种写法是实现多组并排,效果是一样的!! 

这样的预排序有什么意义呢?

1、 gap越大,大的数可以更快到后面,小的数可以更快到前面,但是越不接近有序

2、gap越小,大的小的就挪动的越慢,但是也越接近有序

3、gap==1时,就是直接插入排序(我们可以发现当gap等于1时,这个预排序算法与直接插入排序算法的写法是一样的!!)

现在来分析gap该取多少合适?

     首先,gap是不能随便取的,因为比如说有100万个数据,gap取3,显然是不合适的,所以我们的gap一定要跟数据个数n建立联系,gap具体取多少是最合适的没有得到很好的证明,所以我们使用Knuth的思路来将我们的希尔排序完善好!!

void ShellSort(int* a, int n)
{
	//gap>1  预排序
	//gap=1 直接插入排序
	int gap = n;
	while (gap > 1)
	{
		gap = gap / 3 + 1;//这是为了保证gap最后一定为0
		for (int i = 0; i < n - gap; i++)
		{
			int end = i;
			int temp = a[i + gap];
			while (end >= 0)
			{
				if (a[end] > temp)//如果前面的数比后面的数大,就前面元素插入到后面的位置
					a[end + gap] = a[end];
				else
					break;
				end -= gap;
			}
			a[end + gap] = temp;
		}
	}
}

需要注意的是:gap = gap / 3 + 1是为了保证gap最后一定会等于1,也就是一定会在最后进行一次直接插入排序,保证有序,而前面gap>1的过程都是在进行预排序!!

3.3 复杂度分析

因为预排是一个逐渐转好的过程,所以我们还按照最坏情况去考虑是不合理的,因此这边是难以计算的,我们看看书上的讲解

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

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

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

四、选择排序

4.1 思路

选择排序的思想:每一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,直到全部待排序的数据元素排完 。

      这个其实也跟摸扑克牌有关,但是这次跟直接插入排序不一样的是,直接插入排序是一次摸一张牌然后插入调整,而选择排序是一次性拿了所有牌,再逐个把小的数往前放!

4.2 选择排序的实现

1、在元素集合array[i]--array[n-1]中选择关键码最大(小)的数据元素

2、若它不是这组元素中的最后一个(第一个)元素,则将它与这组元素中的最后一个(第一个)元素交换
3、在剩余的array[i]--array[n-2](array[i+1]--array[n-1])集合中,重复上述步骤,直到集合剩余1个元素

我们拿到所有的牌后,每次都把最小的牌往前放

void SelectSort(int* a, int n)
{
	for (int begin = 0; begin < n; begin++)
	{
		int min = begin;//记录最小元素的下标
		for (int i = begin+1; i < n; i++)
		{
			if (a[min] > a[i])
				min = i;//记录最小的牌的下标
		}
		Swap(&a[begin], &a[min]);
	}
}

 但是每次遍历就记一张最小的牌,效率太低下了,所以我们改造一下该算法,使得该算法每遍历一次就记住最小的牌和最大的牌,然后分别放在两边!!

void SelectSort(int* a, int n)
{
	int left = 0; 
	int right = n - 1;
	while (left < right)
	{
		int min = left;
		int max = left;
		for (int i = left+1; i <= right; i++)
		{
		
			if (a[min] > a[i])
			    min = i;
		    if (a[max] < a[i])
				max = i;
		}
		//这里要考虑一种情况,就是如果最大的数恰好就在最左端,那么就会导致第二次swap换到后面的就不是最大的数而是最小的数了
		Swap(&a[min], &a[left]);
		//如果max和begin重叠,修正一下
		if (max == left)
			max = min;
		Swap(&a[max], &a[right]);
		left++;
		right--;
	}
}

易错点1:min和max要从他们后面的第一张牌开始去一张一张比较 

易错点2:交换的时候,如果最大的元素恰好在最左边,那么就有可能被最小的元素给交换过去了,所以这个时候要注意及时地修正!!

4.3 复杂度分析

时间复杂度:O(N^2)

单趟无论选择一个还是选择两个,都得遍历一遍,复杂度为O(N),整体还得遍历一遍O(N)

空间复杂度:O(1)