天天看点

数据结构学习笔记 --- 排序(插入排序、希尔排序)

1. 引言 

本文主要讲解一些常见的排序算法。

2. 插入排序

(1 ) 直接插入排序(straight insertion sort)的作法是:每次从无序表中取出第一个元素,把它插入到有序表的合适位置,使有序表仍然有序。

第一趟比较前两个数,然后把第二个数按大小插入到有序表中; 第二趟把第三个数据与前两个数从前向后扫描,把第三个数按大小插入到有序表中;依次进行下去,进行了(n-1)趟扫描以后就完成了整个排序过程。

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

    折半插入排序算法的具体操作为:在将一个新元素插入已排好序的数组的过程中,寻找插入点时,将待插入区域的首元素设置为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]。

    折半插入排序算法是一种稳定的排序算法,比直接插入算法明显减少了关键字之间比较的次数,因此速度比直接插入排序算法快,但记录移动的次数没有变,所以折半插入排序算法的时间复杂度仍然为O(n^2),与直接插入排序算法相同。

(3) 2-路插入排序是在折半插入排序的基础上再改进之,其目的是减少排序过程中移动记录的次数,但为此需要n个记录的辅助空间。时间复杂度为O(n^2)。理解:所谓的2-路,是指优先插入在序列前面或后面,然后再考虑插入到中间。

#include "ds.h"

#define 	MAX_SIZE 	20			// 一个用作示例的小顺序表的最大长度
typedef		int			KeyType;	// 定义关键字类型为整型
typedef 	int 		InfoType; // 定义其它数据项的类型

#define 	EQ(a,b) 	((a)==(b))
#define 	LT(a,b) 	((a)<(b))
#define 	LQ(a,b) 	((a)<=(b))

struct RedType						// 记录类型
{
	KeyType		key;				// 关键字项
	InfoType	otherinfo;			// 其它数据项,具体类型在主程中定义
};

struct SqList						// 顺序表类型
{
	RedType		r[MAX_SIZE+1];		// r[0]闲置或用作哨兵单元
	int 		length;				// 顺序表长度
};

// 对顺序表L作直接插入排序。算法10.1
void InsertSort(SqList &L)
{
	int 	i, j;
	for (i = 2; i <= L.length; ++i)
	{
		if (LT(L.r[i].key, L.r[i-1].key))	// "<",需将L.r[i]插入有序子表
		{
			L.r[0] = L.r[i];				// 复制为哨兵
			for (j = i - 1; LT(L.r[0].key, L.r[j].key); --j)
				L.r[j+1] = L.r[j];			// 记录后移
			L.r[j+1] = L.r[0];				// 插入到正确位置
		}
	}
}

// 对顺序表L作折半插入排序。算法10.2
void BInsertSort(SqList &L)
{
	int i, j, m, low, high;
	for (i = 2; i <= L.length; ++i)
	{
		L.r[0] = L.r[i];			// 将L.r[i]暂存到L.r[0]
		low  = 1;
		high = i - 1;
		while (low <= high)			// 在r[low..high]中折半查找有序插入的位置
		{
			m = (low + high)/2;		// 折半
			if LT(L.r[0].key, L.r[m].key)
				high = m - 1;		// 插入点在低半区
			else
				low = m + 1;		// 插入点在高半区
		}
		for (j = i - 1; j >= high + 1; --j)
			L.r[j+1] = L.r[j];		// 记录后移
		L.r[high+1] = L.r[0];		// 插入
	}
}

// 2_路插入排序
void P2_InsertSort(SqList &L)
{
	int i, j, first, final;
	RedType *d;
	d = (RedType*)malloc(L.length*sizeof(RedType)); // 生成L.length个记录的临时空间
	d[0] = L.r[1]; 		// 设L的第1个记录为d中排好序的记录(在位置[0])
	first = final = 0;	// first、final分别指示d中排好序的记录的第1个和最后1个记录的位置
	for (i = 2; i <= L.length; ++i)	// 依次将L的第2个~最后1个记录插入d中
	{
		// 待插记录小于d中最小值,插到d[first]之前(不需移动d数组的元素)
		if (L.r[i].key < d[first].key)
		{
			first = (first-1+L.length)%L.length;
			d[first] = L.r[i];
		}
		else if (L.r[i].key > d[final].key)
		{
			final = final + 1;
			d[final] = L.r[i];
		}
		else
		{
			// 待插记录大于d中最小值,小于d中最大值,插到d的中间(需要移动d数组的元素)
			j = final++;	// 移动d的尾部元素以便按序插入记录
			while (L.r[i].key < d[j].key)
			{
				d[(j+1)%L.length] = d[j];
				j = (j-1+L.length)%L.length;
			}
			d[j+1] = L.r[i];
		}
	}
	
	for (i = 1; i <= L.length; i++)			// 把d赋给L.r
		L.r[i] = d[(i+first-1)%L.length];	// 线性关系
		
}	

void print(SqList L)
{
   int i;
   for(i=1;i<=L.length;i++)
     printf("(%d,%d)",L.r[i].key,L.r[i].otherinfo);
   printf("\n");
}

#define N 8
int main()
{
   RedType d[N]={{49,1},{38,2},{65,3},{97,4},{76,5},{13,6},{27,7},{49,8}};
   SqList l1,l2,l3;
   int i;
   for(i=0;i<N;i++) // 给l1.r赋值
     l1.r[i+1]=d[i];
   l1.length=N;
   l2=l3=l1; // 复制顺序表l2、l3与l1相同
   printf("排序前:\n");
   print(l1);
   InsertSort(l1);
   printf("直接插入排序后:\n");
   print(l1);
   BInsertSort(l2);
   printf("折半插入排序后:\n");
   print(l2);
   P2_InsertSort(l3);
   printf("2_路插入排序后:\n");
   print(l3);
}
           

3. 希尔排序

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

排序过程:先取一个正整数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

#include "ds.h"
/* 希尔排序的基本思想是:先将整个待排序记录序列分割成为若干子序列分别进行直接插入排序,待整个序列中的记录基本有序时,再对全体记录进行一次直接插入排序 */
#define 	MAX_SIZE 	20			// 一个用作示例的小顺序表的最大长度
typedef		int			KeyType;	// 定义关键字类型为整型
typedef 	int 		InfoType; // 定义其它数据项的类型

#define 	EQ(a,b) 	((a)==(b))
#define 	LT(a,b) 	((a)<(b))
#define 	LQ(a,b) 	((a)<=(b))

struct RedType						// 记录类型
{
	KeyType		key;				// 关键字项
	InfoType	otherinfo;			// 其它数据项,具体类型在主程中定义
};

struct SqList						// 顺序表类型
{
	RedType		r[MAX_SIZE+1];		// r[0]闲置或用作哨兵单元
	int 		length;				// 顺序表长度
};

// 对顺序表L作一趟希尔插入排序。本算法是和一趟直接插入排序相比,
// 作了以下修改:
// 1.前后记录位置的增量是dk,而不是1;
// 2.r[0]只是暂存单元,不是哨兵。当j<=0时,插入位置已找到。算法10.4
void ShellInsert(SqList &L, int dk)
{
	int i, j;
	for (i = dk + 1; i <= L.length; i++)
	{
		// 需将L.r[i]插入有序增量子表
		if LT(L.r[i].key, L.r[i-dk].key)
		{
			L.r[0] = L.r[i];		// 暂存在L.r[0]
			for (j = i-dk; j > 0 && LT(L.r[0].key, L.r[j].key); j -= dk)
				L.r[j+dk] = L.r[j];	// 记录后移,查找插入位置
			L.r[j+dk] = L.r[0];		// 插入
		}
	}
}

void print(SqList L)
{
	int i;
	for (i = 1; i <= L.length; i++)
	{
		printf("%d ", L.r[i].key);
	}
	printf("\n");
}

void print1(SqList L)
{
	int i;
	for (i = 1; i <= L.length; i++)
     	printf("(%d,%d)", L.r[i].key, L.r[i].otherinfo);
   	printf("\n");
}

// 按增量序列dlta[0..t-1]对顺序表L作希尔排序。算法10.5
void ShellSort(SqList &L, int dlta[], int t)
{
	int k;
	for (k = 0; k < t; ++k)
	{
		ShellInsert(L, dlta[k]);
		printf("第%d趟排序结果: ", k+1);
     	print(L);
	}
}

#define N 10
#define T 3
int main()
{
   RedType d[N]={{49,1},{38,2},{65,3},{97,4},{76,5},{13,6},{27,7},{49,8},{55,9},{4,10}};
   SqList l;
   int dt[T]={5,3,1}; // 增量序列数组
   for(int i=0;i<N;i++)
     l.r[i+1]=d[i];
   l.length=N;
   printf("排序前: ");
   print(l);
   ShellSort(l,dt,T);
   printf("排序后: ");
   print1(l);
}
           

继续阅读