排序法系列之二----插入排序(直接插入,希尔排序,二分插入,链表插入排序)的C++代码实现

  

插入排序法包括:直接插入法(稳定),希尔排序法(不稳定),二分插入排序(稳定),链表插入排序

有一个已经有序的数据序列,要求在这个已经排好的数据序列中插入一个数,但要求插入后此数据序列仍然有序,这个时候就要用到一种新的排序方法——插入排序法,插入排序的基本操作就是将一个数据插入到已经排好序的有序数据中,从而得到一个新的、个数加一的有序数据,算法适用于少量数据的排序,时间复杂度为O(n^2)。是稳定的排序方法。插入算法把要排序的数组分成两部分:第一部分包含了这个数组的所有元素,但将最后一个元素除外,而第二部分就只包含这一个元素。在第一部分排序后,再把这个最后元素插入到此刻已是有序的第一部分里的位置。

        1. 从第一个元素开始,该元素可以认为已经被排序

  2. 取出下一个元素,在已经排序的元素序列中从后向前扫描

  3. 如果该元素(已排序)大于新元素,将该元素移到下一位置

  4. 重复步骤3,直到找到已排序的元素小于或者等于新元素的位置

  5. 将新元素插入到下一位置中

  6. 重复步骤2

直接插入排序法的C++代码如下:元素由低到高排序

#include<iostream>
using namespace std;
void InsertSort(int *a,int length)
{
	int i,j;
	for(i=1;i<length;i++)
	{
		int key=a[i];
		for(j=i-1;j>=0;j--)
		{
			if(key>a[j])
				break;
			else
			{
				a[j+1]=a[j];
				a[j]=key;
			}
		}
	}
}
int main()
{
	int b[10]={4,6,2,3,5,1,7,9,8,10};
	InsertSort(b,10);
	for(int i=0;i<10;i++)
	{
		cout<<b[i]<<" ";
	}
	cout<<endl;
	return 0;
}


 以下代码为直接插入排序法利用STL容器和模板实现的:

#include<iostream>
#include<vector>
using namespace std;
template<typename Type>
void InsertionSort(vector<Type>&a)
{
	int n=a.size();
	vector<Type>::iterator it,is;
	for(it=a.begin()+1;it!=a.end();it++)
	{
		is=it;
		Type num=*is;
		while(is!=a.begin()&&num<*(is-1))
		{
			*is=*(is-1);
			is--;
		}
		*is=num;
	}
}
void InsertSort(int *a,int length)
{
	int i,j;
	for(i=1;i<length;i++)
	{
		int key=a[i];
		for(j=i-1;j>=0;j--)
		{
			if(key>a[j])
				break;
			else
			{
				a[j+1]=a[j];
				a[j]=key;
			}
		}
	}
}
int main()
{
	int b[10]={4,6,2,3,5,1,7,9,8,10};
	vector<int>a(b,b+10);
	InsertionSort(a);
	vector<int>::iterator it;
	for(it=a.begin();it!=a.end();it++)
	{
		cout<<*it<<" ";
	}
	cout<<endl;
	InsertSort(b,10);
	for(int i=0;i<10;i++)
	{
		cout<<b[i]<<" ";
	}
	cout<<endl;
	return 0;
}


 

希尔排序(Shell Sort)

插入排序的一种。是针对直接插入排序算法的改进。该方法又称缩小增量排序,因DL.Shell于1959年提出而得名。

 

基本思想

  希尔排序基本思想:
  先取一个小于n的整数d1作为第一个增量,把文件的全部记录分成d1个组。所有距离为d1的倍数的记录放在同一个组中。先在各组内进行 直接插入排序 ;然后,取第二个增量d2<d1重复上述的分组和排序,直至所取的增量dt=1(dt<dt-l<…<d2<d1),即所有记录放在同一组中进行直接插入排序为止。
  该方法实质上是一种分组插入方法。
  给定实例的shell排序的排序过程
  假设待排序文件有10个记录,其关键字分别是:
  49,38,65,97,76,13,27,49,55,04。
  增量序列的取值依次为:
  5,3,1

缩小增量法

  属于插入类排序,是将整个无序列分割成若干小的子序列分别进行插入排序
  排序过程:先取一个正整数d1<n,把所有序号相隔d1的数组元素放一组,组内进行直接插入排序;然后取d2<d1,重复上述分组和排序操作;直至di=1,即所有记录放进一个组中排序为止
  初始:d=5
  49 38 65 97 76 13 27 49* 55 04
  49 13
  |-------------------|
  38 27
  |-------------------|
  65 49*
  |-------------------|
  97 55
  |-------------------|
  76 04
  |-------------------|
  一趟结果
  13 27 49* 55 04 49 38 65 97 76
  d=3
  13 27 49* 55 04 49 38 65 97 76
  13 55 38 76
  |------------|------------|------------|
  27 04 65
  |------------|------------|
  49* 49 97
  |------------|------------|
  二趟结果
  13 04 49* 38 27 49 55 65 97 76
  d=1
  13 04 49* 38 27 49 55 65 97 76
  |----|----|----|----|----|----|----|----|----|
  三趟结果
  04 13 27 38 49* 49 55 65 76 97
  --------------------------------------------------------------------------------------------
  例如对503,17,512,908,170,897,275,653,462,154,509,612,677,765,703,94排序的C++语言算法
  算法思想简单描述:
  在直接插入排序算法中,每次插入一个数,使有序序列只增加1个节点,
  并且对插入下一个数没有提供任何帮助。如果比较相隔较远距离(称为
  增量)的数,使得数移动时能跨过多个元素,则进行一次比较就可能消除
  多个元素交换。D.L.shell于1959年在以他名字命名的排序算法中实现
  了这一思想。算法先将要排序的一组数按某个增量d分成若干组,每组中
  记录的下标相差d.对每组中全部元素进行排序,然后再用一个较小的增量
  对它进行,在每组中再进行排序。当增量减到1时,整个要排序的数被分成
  一组,排序完成。
  下面的函数是一个希尔排序算法的一个实现,初次取序列的一半为增量,
  以后每次减半,直到增量为1。
  希尔排序是不稳定的。
 Shell排序的算法实现: C++代码
 
#include<iostream>
using namespace std;
void HealSort(int *a,int length)
{
	int b[1000]={0};
	int k=length/2;
	int i,j,temp;
	while(k>0)
	{
		for(i=0;i<length-k;i++)
		{
			j=k+i;
			while(j>=k)
			{
				if(a[j]<a[j-k])
				{
					temp=a[j];
					a[j]=a[j-k];
					a[j-k]=temp;
					j=j-k;
				}
				else 
					break;
			}
		}
		k=k/2;
	}
}
int main()
{
	int a[10]={15,6,28,89,61,92,43,24,47,510};
	HealSort(a,10);
	for(int i=0;i<10;i++)
	{
		cout<<a[i]<<" ";
	}
	cout<<endl;
	return 0;
}

算法分析

优劣

  不需要大量的辅助空间,和归并排序一样容易实现。希尔排序是基于插入排序的一种算法, 在此算法基础之上增加了一个新的特性,提高了效率。希尔排序的时间复杂度为 O(N*(logN)2), 没有快速排序算法快 O(N*(logN)),因此中等大小规模表现良好,对规模非常大的数据排序不是 最优选择。但是比O(N2)复杂度的算法快得多。并且希尔排序非常容易实现,算法代码短而简单。 此外,希尔算法在最坏的情况下和平均情况下执行效率相差不是很多,与此同时快速排序在最坏 的情况下执行的效率会非常差。 专家们提倡,几乎任何排序工作在开始时都可以用希尔排序,若在实际使用中证明它不够快, 再改成快速排序这样更高级的排序算法. 本质上讲,希尔排序算法的一种改进,减少了其复制的次数,速度要快很多。 原因是,当N值很大时数据项每一趟排序需要的个数很少,但数据项的距离很长。 当N值减小时每一趟需要和动的数据增多,此时已经接近于它们排序后的最终位置。 正是这两种情况的结合才使希尔排序效率比插入排序高很多。

时间性能

  1.增量序列的选择
  Shell排序的执行时间依赖于增量序列。
  好的增量序列的共同特征:
  ① 最后一个增量必须为1;
  ② 应该尽量避免序列中的值(尤其是相邻的值)互为倍数的情况。
  有人通过大量的实验,给出了目前较好的结果:当n较大时,比较和移动的次数约在nl.25到1.6n1.25之间。
  2.Shell排序的时间性能优于直接插入排序
  希尔排序的时间性能优于直接插入排序的原因:
  ①当文件初态基本有序时直接插入排序所需的比较和移动次数均较少。
  ②当n值较小时,n和n2的差别也较小,即直接插入排序的最好时间复杂度O(n)和最坏时间复杂度0(n2)差别不大。
  ③在希尔排序开始时增量较大,分组较多,每组的记录数目少,故各组内直接插入较快,后来增量di逐渐缩小,分组数逐渐减少,而各组的记录数目逐渐增多,但由于已经按di-1作为距离排过序,使文件较接近于有序状态,所以新的一趟排序过程也较快。
  因此,希尔排序在效率上较直接插人排序有较大的改进。

稳定性

  排序前一个序列中,如果出现N个与关键字相同的数据,那么排序后仍然按照原先序列的排列顺序排列,就说这个算法是稳定的,反之就是不稳定的。通俗地讲就是能保证排序前2个相等的数其在序列的前后位置顺序和排序后它们两个的前后位置顺序相同。在简单形式化一下,如果Ai = Aj, Ai原来在位置前,排序后Ai还是要在Aj位置前。

希尔分析

  希尔排序是按照不同步长对元素进行插入排序,当刚开始元素很无序的时候,步长最大,所以插入排序的元素个数很少,速度很快;当元素基本有序了,步长很小,插入排序对于有序的序列效率很高。所以,希尔排序的时间复杂度会比o(n^2)好一些。由于多次插入排序,我们知道一次插入排序是稳定的,不会改变相同元素的相对顺序,但在不同的插入排序过程中,相同的元素可能在各自的插入排序中移动,最后其稳定性就会被打乱,所以shell排序是不稳定的。

二分插入排序

直接插入排序时后面的元素从后向前逐个比较寻找插入位置,有时候没有必要这样做,因为此时需要寻找合适插入位置的当前这个元素前面的子序列已经有序,如果使用二分查找插入位置往往可以减少平均搜索长度。对于一个待排序的随机序列而言,用二分插入排序取代直接插入排序,尽管总的移动元素次数不可能减少,但是可能减少总的平均比较次数。二分插入排序的平均时间复杂度为O(n2),最坏情况下的时间复杂度为(n2),当待排序序列已经有序时,排序时间复杂度为O(nlogn)。空间复杂度为O(1)。二分插入排序是否稳定依赖于具体实现。

假设一个序列已经有序,此时我们需要向这个序列里插入一个新的值,那么我们如何找到合适的位置呢?

首先跟序列最中间的那个元素比较,如果比最中间的这个元素小,则插入位置在它的左边,否则在它的右边。以当前最中间位置为分割点,如果在左边,则当前最中间位置是待搜索子序列的终点,如果在右边,右边邻接的元素将是待搜索子序列的起点。按照这种原则,继续寻找下一个中间位置,并继续这种过程,直到找到合适的插入位置为止。

问题是何谓合适的插入位置?如果序列中有一个元素与当前待插入的元素值相等,那么插入位置应该选在该元素的前面还是后面呢?选在后面则二分插入排序稳定,选在前面则二分插入排序不稳定。如果序列中有多个元素与当前待插入的元素值相等,插入位置选在哪里呢?选在最后一个的后面则二分插入排序稳定,其它情况均不稳定。这里的“前面”位置和“后面”位置通常被称为上界和下界。

还有,对数组二分插入排序比较简单,那么能对链表进行二分插入排序吗?理论上没有什么问题,如果希望代码复用程度高的话,链表需要提供迭代器。问题关键不在于代码复用性怎么样,而是插入排序的速度太慢,一般不采纳。

二分插入排序的C++实现代码如下:

#include<iostream>
using namespace std;
void BinaryInsertSort(int *a,int length)
{
	int BinaryFind(int *a,int k,int key);
	int i,j,key,num;
	for(i=1;i<length;i++)
	{
		key=a[i];
		num=BinaryFind(a,i-1,a[i]);
		if(num==-2)
			return;
		for(j=i;j>num+1;j--)
		{
			a[j]=a[j-1];
		}
		a[num+1]=key;
	}
}
int BinaryFind(int *a,int k,int key)
{

	if(key>=a[k])
	{
		return k;
	}
	if(key<a[0])
		return -1;
	int j=k/2;
	while(j>0)
	{
		if(key>a[j])
		{
			j=(k+j)/2;
		}
		else if(key<a[j])
		{
			j=j/2;
		}
		if(key>=a[j]&&key<=a[j+1])
		{
			return j;
		}
		else if(key>=a[j-1]&&key<=a[j])
		{
			return j-1;
		}
		
	}
	return -2;
}
int main()
{
	int b[20]={4,6,2,3,5,1,7,9,8,10,14,16,12,13,15,17,19,18,110};
	BinaryInsertSort(b,20);
	for(int i=0;i<20;i++)
	{
		cout<<b[i]<<" ";
	}
	cout<<endl;
	return 0;
}


其中BinaryFind(int*a,int k,int key)是传入数组a,其中数组中前k+1个数已经排好序,要插入的第k+2个数的值为key,返回值是key要插入的地方的前一个位置的下标,根据返回的下标值我们就可以在BinaryInsertSort中将当前的值直接插入到得到的下标的下一个位置。

链表插入排序此处不再详细解释。

 

你可能感兴趣的:(C++,算法,shell,iterator,语言,n2)