天天看点

经典排序算法系列之一:插入排序

1.序言

        这是经典排序算法系列的第一篇,我们首先从插入排序算法开始。这是一个对少量元素排序的有效算法,主要包括:直接插入排序,二分插入排序(又称折半插入排序),链表插入排序,希尔排序(又称缩小增量排序)。对于每一种插入排序算法,我都阐述了其基本思想,给出了该算法的代码,并分析了代码的运行效率,最后对各种插入排序进行了比较。

2.直接插入排序

2.1基本思想

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

2.2算法思路

        为了帮助我们理解直接插入排序算法,我们可以从生活中的一个实例来理解直接插入排序算法的基本思想。该算法的机理与我们在生活中打牌时,理牌的做法差不多。在刚开始摸牌的时候,我们的左手是空的,牌面朝下放在桌子上的,接着,一次从桌上摸起一张牌,并将它插入到左手一把牌中正确的位置,为了找到这张牌的正确位置,我们需将它与手中已有的牌从右到左地进行比较,如下图所示。无论在什么时候,手中的牌都是已经排好序的,而这些牌都是原先桌上那副牌里最顶上的一些牌。

经典排序算法系列之一:插入排序
经典排序算法系列之一:插入排序

假如现在我们要对一个数组a={5,2,4,6,1,3}用直接插入排序算法进行排序,其基本思路如上图所示,实现该算法的核心代码如下:

//直接插入排序算法
void straightInsertSort(int *a, int length)
{
	int i, j;
	int temp;
	for(i = 1; i <= length; i++)
	{
		//存储要插入的数值
		temp = a[i];
		//从有序部分右端到左端逐个比较
		for(j = i-1; j >= 0 && a[i] < a[j]; j--)
		{
			//当要插入的数值比数组值a[j]小则将a[j]后移一位
			a[j+1] = a[j];
		}
		a[j+1] = temp;
	}
}
           

2.3代码效率

        在分析插入排序时,我们既考虑了最好情况,也考虑了最坏情况。可以明显看出,最好情况则是输入已经排好序的数组,最坏情况则是输入按照逆序排序的数组。在最好情况下,比较次数时间复杂度为常量O(1),进行了n轮,所以最好情况下的时间复杂度为O(n);在最坏情况下,比较次数和移动次数的时间复杂度为O(n),进行了n轮,所以最坏情况下的时间复杂度为O(n^2)。可以看出直接插入排序算法是一种稳定的排序算法,并且适合大部分有序的输入数组。还有一点要注意的是,直接插入排序算法并不适合大数量级排序。

3.折半插入排序

3.1基本思想

    二分插入排序(binary insertion sort)又称折半插入排序,是对插入排序算法的一种改进,由于排序算法过程中,就是不断的依次将元素插入前面已排好序的序列中。由于前半部分为已排好序的数列,这样我们不用按顺序依次寻找插入点,可以采用折半查找的方法来加快寻找插入点的速度。

   假设待排序的记录存放在数组V[1..n]中。初始时,V[1..i-1]为有序区,V[i..n]为无序区。在插入第i个元素时,对前面的0~i-1元素进行折半,先跟他们中间的那个元素比,如果小,则对前半再进行折半,否则对后半进行折半,直到left>right,然后再把第i个元素前1位与目标位置之间的所有元素后移,再把第i个元素放在目标位置上。

3.2算法思路

    折半插入排序算法的具体操作为:在将一个新元素插入已排好序的数组的过程中,寻找插入点时,将待插入区域的首元素设置为a[low],末元素设置为a[high],则轮比较时将待插入元素与a[m],其中m=(low+high)/2相比较,如果比参考元素小,则选择a[low]到a[m-1]为新的插入区域(即high=m-1),否则选择a[m+1]到a[high]为新的插入区域(即low=m+1),如此直至low<=high不成立,即将此位置之后所有元素后移一位,并将新元素插入a[high+1]。实现二分插入排序的核心代码如下:

//折半插入排序算法
void binaryInsertSort(int *a, int length)
{
	int i, j;
	int temp;
	int low, middle, high;
	//遍历数组查找有序部分和无序部分临界点
	for(i = 1; i < length; i++)
	{
		if(a[i] < a[i-1])
		{
			//存储要插入的数值
			temp = a[i];
			//存储要插入的有序数组的左端下标值
			low = 0;
			//存储要插入的有序数组的右端下标值
			high = i-1;
			//寻找要插入的位置
			while(low <= high)
			{
				middle = (low+high)/2;
				//插入值若比有序数组中间值小则插入位置在左端,否则在右端
				if(a[i] < a[middle])
				{
					high = middle-1;
				}
				else
				{
					low = middle+1;
				}
			}
			//后移比插入值大的有序数组部分
			for(j = i; j > high; j--)
			{
				a[j] = a[j-1];
			}
			//插入数值
			a[high] = temp;
		}
	}
}
           

3.3代码效率

        折半搜索比顺序搜索查找快, 所以折半插入排序就平均性能来说比直接插入排序要快。它所需的排序码比较次数与待排序对象序列的初始排列无关, 仅依赖于对象个数。在插入第i个对象时, 需要经过logi+1 次排序码比较, 才能确定它应插入的位置。因此, 将 n 个对象(为推导方便, 设为 n=2k )用折半插入排序所进行的排序码比较次数为n*logn。与直接插入排序算法相同,折半插入排序算法是一种稳定的算法。

    当 n 较大时, 折半插入排序的总排序码比较次数比直接插入排序的最坏情况要好得多, 但比其最好情况要差。在对象的初始排列已经按排序码排好序或接近有序时, 直接插入排序比折半插入排序执行的排序码比较次数要少。折半插入排序的对象移动次数与直接插入排序相同, 依赖于对象的初始排列。

4.链表插入排序

4.1基本思想

    对于数组中的一组对象V[1], …, V[n], 若V[1], …, V[i-1]已经通过指针link, 按其排序码从小到大链接起来。现要插入V[i], i = 2, 3, …, n, 则必须在前面i-1个链接起来的对象当中, 循链检测比较, 找到 V[i] 应插入的位置,把V[i] 链入, 并修改相应链接指针。从而得到V[1], …, V[i]的一个通过指针排好的链表。

4.2算法思路

        链表插入排序算法的机理和直接插入算法类似,将链表分为有序部分和无序部分,将无序部分的节点与有序部分链表从头节点到尾节点逐个比较,找出最佳位置插入新增节点即可,实现该算法的核心代码如下:

//链表插入排序
//定义链表结点类型
typedef struct Node   
{
 ElemType data;
 struct Node * next;
}Node;

void linkedInsertSort(List L)
{
	Node *head=L;
	//有序区的尾节点
	Node *tail;
	//无序区的首元节点
	Node *unsorted;
	//用来记录插入位置的前驱
	NOde *pre;
	//作为有序区的游标
	Node *p;
	
	if(head->next!=NULL)
	{
		tail=head->next;
		while(tail->next!=NULL)
		{
			unsorted=tail->next;
			pre=head;
			p=pre->next;
			//逐个比较寻找插入位置
			while(unsorted->data<=p->data&&unsorted!=p)
			{
				pre=p;
				p=p->next; 
			}
			if(p==unsorted)
			{
				tail=unsorted;
			}
			else
			{
				tail->next=unsorted->next;
				pre->next=unsorted;
				unsorted->next=p;
			}
		}
	}
	else 
	{
		printf("the list is NULL!\n");
	}
}
           

4.3代码效率

        使用链表插入排序,每插入一个对象,最大排序码比较次数等于链表中已排好序的对象个数,最小排序码比较次数为1。故总的排序码比较次数最小为 n-1,最大为n*(n-1)/2,算法在unsorted->data== p->data时,将unsorted插在p的后面,所以,链表插入排序方法是稳定的。

5.希尔排序

5.1基本思想

    希尔排序方法又称为缩小增量排序。该方法的基本思想是:设待排序对象序列有n个对象,首先取一个整数gap<n作为间隔,将全部对象分为gap个子序列,所有距离为gap的对象放在同一个子序列中,在每一个子序列中分别施行直接插入排序。然后缩小间隔gap,例如取gap=gap/2,重复上述的子序列划分和排序工作。直到最后取gap=1,将所有对象放在同一个序列中排序为止。

5.2算法思路

        为了让我们更好的理解希尔排序,我现在给大家演示一个实例,假如我们对数组a={49,38,65,97,76,13,27,49*,55,04}进行希尔排序,具体步骤如下图所示:

经典排序算法系列之一:插入排序

        由上面的结果我们可以很清楚的了解到希尔排序的机理,并且可以知道希尔排序是一种不稳定的排序。根据上面的步骤我们知道希尔排序就是将数组进行多次分组并使用直接插入算法排序,直接插入排序算法是每比较一次就移动1位,而在希尔排序中我们每比较一次移动d位就刚好,于是我们很容易对直接插入排序算法进行改造就可以实现希尔排序算法,下面是实现希尔排序算法的核心代码:

//希尔排序算法
void ShellSort(int *a, int n, int d)
{
	int i, j;
	int temp;
	while(d > 0)
	{
		for(i = d+1; i < n; i++)
		{
			//存储要插入的值
			temp = a[i];
			//遍历分组寻找要插入的位置
			for(j = i-d; j >=0 && a[i] < a[j]; j = j-d)
			{
				a[j+d] = a[j];
			}
			a[j+d] = temp;
		}
		//减小间隔值
		d = d/2;
	}
}
           

5.3代码效率

        开始时d的值较大,每组中的元素较少,排序速度较快;随着排序进展,d值逐渐变小,每组中元素的个数逐渐变多,由于前面工作的基础,大多数元素已基本有序,所以排序速度仍然很快。时间效率:至今数学上尚未解决的难题。 约为O(n^1.3)。希尔排序是一种不稳定的排序算法。

          希尔排序是基于插入排序的一种算法, 在此算法基础之上增加了一个新的特性,提高了效率。希尔排序的时间复杂度为 O(N*(logN)^2), 没有快速排序算法快 O(N*(logN)),因此中等大小规模表现良好,对规模非常大的数据排序不是 最优选择。但是比O(N2)复杂度的算法快得多。并且希尔排序非常容易实现,算法代码短而简单。 此外,希尔算法在最坏的情况下和平均情况下执行效率相差不是很多,与此同时快速排序在最坏 的情况下执行的效率会非常差。

6.参考资料

《算法导论》机械工业出版社 Thomas H.Cormen   Charles E.Leiserson   Ronlad L.Rivest   Clifford Stein

《数据结构》清华大学出版社 李春葆 尹为民

7.学习心得

        在学习插入排序算法的时候,我加深了对排序算法的一些理解。我觉得在插入排序算法里面应该重点理解直接插入排序算法,后面的几种算法都是由直接插入排序算法变化或改造就可以得到,还有在学习算法的时候应该注意多思考,多动手,读者应该试着在理解其基本思想之后动手实现其算法的核心代码,毕竟实践出真知,只有自己实践并理解了,这个算法才是你的算法。

(注:代码是本人所写,如有错误,请多多指教,重在交流!)