《数据结构与算法》课程笔记 第二章 2.3 排序

1. 排序的基本概念

1 排序概念

排序:将一组数据元素序列重新排序,使得数据元素序列按某个数据项(关键字)有序。

排序依据:是依据数据元素的关键字。

若关键字是主关键字(关键字值不重复),则无论采用何种排序方法,排出的结果都是唯一的。

若关键字是次关键字(关键字值可以重复),则排出的结果可能不唯一。

2 稳定排序和不稳定排序方法

稳定的排序方法:对于任意的数据元素序列,排序前后所有相同关键字的相对位置都不变

不稳定的排序方法:存在一组数据序列,在排序前后,相同关键字的相对位置发生了变化

  • 排序的过程是一个逐步扩大记录的有序序列长度的过程。 

2. 插入排序

基本思想:将无序子序列中的一个或几个记录“插入”到有序子序列中,从而增加有序子序列的长度。

插入排序三部曲:

1 直接插入排序

排序过程:整个排序过程为 n-1 趟插入,即先将序列中第1个记录看成是一个有序子序列,然后从第2个记录开始,逐个进行插入,直至整个序列有序。

实例:

算法实现:

typedef struct
{
	int key;
	float info;
}JD;

void straisort(JD r[], int n) //对长度为 n 的序列排序
{
	int i, j;
	for(i=2;i<=n;i++) //数组从第1位开始存数据
	{
		r[0] = r[i];
		j = i-1;
		while(r[0].key < r[j].key)
		{
			r[j+1] = r[j];
			j--;
		}
		r[j+1] = r[0];
	}
}

性能分析:(比较次数和移动次数)

简单插入排序的本质:比较和交换

  • 序列中逆序的个数,决定交换次数
  • 平均逆序数量为 C(n,2)/2 ,所以 T(n) = O(n2)
  • 简单插入排序复杂度由逆序个数决定 

如何改进简单插入排序复杂度:(希尔排序)

2 希尔排序(缩小增量法) 

1) 基本思想 

基本思想:将一组待排序的数据分割成若干个较小的子文件,对各个子文件分别进行直接插入排序,当文件达到基本有序时,再对整个文件进行一次直接插入排序。 

对待排记录序列先作“宏观”调整,再作“微观”调整。

“宏观”调整指的是:“跳跃式”的插入排序。

2) 排序过程 

  • 首先将记录序列分成若干个子序列
  • 然后分别对每个子序列进行直接插入排序
  • 最后待基本有序时,再进行一次直接插入排序 

  • 其中 d 称为增量,它的值在排序过程中从大到小逐渐缩小,直至最后一趟排序减为1。 

3)算法的实现

void ShellSort(JD r[], int n, int d[], int T)
{
	//r[]为待排序数据,一共有n个;d[]为增量数组,有T个
	int i,j,k;
	JD x;
	k = 0;
	while(k < T)
	{
		for(i=d[k]+1;i<=n;i++) //i+1则进行下一组的排序
		{
			//i为未排序记录的位置
			x = r[i]; //将未排序的数据拷贝给x
			j = i-d[k]; //j为本组位置i前面的一个记录的位置
			while((j > 0)&&(x.key < r[j].key))
			{
				//组内简单插入排序
				r[j+d[k]] = r[j];
				j = j-d[k];
			}
			r[j+d[k]] = x;
		}
		k++;
	}
}

4) 希尔排序的特点

  • 最坏时间复杂度:O(N^{2})

3. 选择排序 

1 简单选择排序

1) 基本思想 

基本思想:从无序子序列中“选择”关键字最小或最大的记录,并将它加入到有序子序列中,以此方法增加记录的有序子序列的长度。 

2) 排序过程

  • 首先通过 n-1 次关键字比较,从 n 个记录中找出关键字最小的记录,将它与第一个记录交换。
  • 再通过 n-2 次比较,从剩余的 n-1 个记录中找出关键字次小的记录,将它与第二个记录交换。
  • 重复上述操作,共进行 n-1 趟排序后,排序结束。 

3) 算法的实现

void smp_selesort(JD r[], int n)
{
	int i, j, k;
	JD x;
	for(i=1;i<n;i++)
	{
		// i 表示第 i 趟,同时也表示最小元素应该放的位置
		k = i;
		for(j=i+1;j<=n;j++)
		{
			if (r[j].key < r[k].key)
			{
				k = j;  //k 用来标记未排序记录中的最小数据位置
			}
		}
		if (i != k)
		{ //交换r[i]和r[k]
			x = r[i];
			r[i] = r[k];
			r[k] = x;
		}
	}
}

4) 性能分析

稳定性分析:

  • 不稳定的排序 

2 归并排序

  • 将两个或两个以上的有序表组合成一个新的有序表,叫归并排序。

2-路归并排序的排序过程:

  • 设初始序列含有 n 个记录,则可看成 n 个有序的子序列,每个子序列长度为1。两两合并,得到 \left \lfloor n/2 \right \rfloor 个长度为2或1的有序子序列。再两两合并,......如此重复,直至得到一个长度为 n 的有序序列为止。

实例:

迭代算法:

  • 将序列的每一个数据看成一个长度为1的有序表,
  • 然后,将相邻两组进行归并得到长度为2的有序表(一趟归并) 
  • 再对相邻两组长度为2的有序表进行下一趟归并得到长度为4的有序表
  • 这样一直进行下去,直到整个表归并成有序表。
  • 如果某一趟归并过程中,单出一个表,该表轮空,等待下一趟归并。

递归思想:

  • 将无序序列划分成大概均等的2个子序列,然后用同样的方法对2个子序列进行归并排序得到2个有序的子序列,再用合并2个有序表的方法合并这2个子序列,得到 n 个元素的有序序列。
void Merge(JD A[], JD TmpArray[], int Lpos, int Rpos, int RightEnd )
{
	//Lpos为左半部分的开始位置,Rpos为右半部分的开始位置
	int i,LeftEnd,NumElements,TmpPos;
	LeftEnd = Rpos-1; //左半部分的结束位置
	TmpPos = Lpos;   //新数组的位置
	NumElements = RightEnd - Lpos + 1; //元素个数
	while(Lpos <= LeftEnd && Rpos <= RightEnd) 
	{
		if (A[Lpos] <= A[Rpos])  //谁小谁拷贝
		{
			TmpArray[TmpPos++] = A[Lpos++];
		}
		else
			TmpArray[TmpPos++] = A[Rpos++];
	}
	while(Lpos <= LeftEnd) //左半部分还有元素
		TmpArray[TmpPos++] = A[Lpos++];
	while(Rpos <= RightEnd) //右半部分还有元素
		TmpArray[TmpPos++] = A[Rpos++];
	for(i=0;i<NumElements;i++,RightEnd--)
	{ //从额外的空间拷贝回数组A
		A[RightEnd] = TmpArray[RightEnd];
		printf("%d",A[RightEnd]);
	}
}

void MSort(JD A[], JD TmpArray[], int Left, int Right)
{
	//A[]为待排序的数据,TmpArray[]是长度与A[]相同的额外的数组空间,起始地址:Left;结束地址:Right
	int Center;
	if (Left < Right) //待排序的数据在数组的下标位置
	{
		Center = (Left + Right)/2;
		MSort(A,TmpArray,Left,Center); //T(N/2)  递归
		MSort(A,TmpArray,Center+1,Right); //T(N/2) 递归
		Merge(A,TmpArray,Left,Center+1,Right); //O(N)
	}
}

void Mergesort(JD A[], int N)
{
	JD *TmpArray; //额外的O(N)空间
	TmpArray = malloc(N*sizeof(JD)); //单独写一个函数,实现只需分配一次额外的空间
	if (TmpArray!=NULL)
	{
		MSort(A,TmpArray,0,N-1);
		free(TmpArray);
	}
	else
		FatalError("No space for tmparray");
}

算法评价:

  • 时间复杂度:每一趟归并的时间复杂度为O(N),总共需要归并 logN 趟,因而,总的时间复杂度为 O(NlogN)。
  • 空间复杂度:2-路归并排序过程中,需要一个与表等长的存储单元数组空间,因此,空间复杂度为 O(N)。

4. 交换排序

通过“交换”无序序列中的记录从而得到其中关键字最小或最大的记录,并将它加入到有序子序列中,以此方法增加记录的有序子序列的长度。交换排序包括:

  • 冒泡排序
  • 快速排序

1 冒泡排序

1) 排序过程

2) 算法实现

void bubble_sort(JD r[], int n)
{
	int m, i, j, flag = 1; //flag为每趟是否有交换操作的标识,flag = 1表示有交换操作
	JD x;
	m = n; //m指向每趟未排序序列的最后位置
	while((m>1) && (flag == 1)) 
	{
		flag = 0; //将标识设为没有交换操作
		for (j=1;j<m;j++)//本趟将最大元素放到未排序序列的最后位置
		{
			if (r[j].key > r[j+1].key) //前面比后面数据大则交换
			{
				flag = 1; //标识置为有交换操作
				x = r[j];
				r[j] = r[j+1];
				r[j+1] = x;
			}
		}
		m--;
	}
}

3) 性能分析

4) 冒泡排序的改进

  • 冒泡排序的结束条件为:最后一趟没有进行“交换记录”。
  • 一般情况下,每经过一趟“冒泡”,“m 减1”,但并不是每趟都如此。增加一个变量记录最后进行交换的位置。 
void bubble_sort(JD r[], int n)
{
	int m, i, j; 
	int lastExchangeIndex; //记录最后一次进行交换的位置
	JD x;
	m = n; //m指向每趟未排序序列的最后位置
	while(m > 1) 
	{
		lastExchangeIndex = 1; //每趟都需赋值为1
		for (j=1;j<m;j++)//本趟将最大元素放到未排序序列的最后位置
		{
			if (r[j].key > r[j+1].key) //前面比后面数据大则交换
			{
				x = r[j];
				r[j] = r[j+1];
				r[j+1] = x;
				lastExchangeIndex = j; //记录下进行交换的记录位置
			}
		}
		m = lastExchangeIndex; //本趟最后一次交换的位置,后面没有进行交换的说明已经排好序了
	}
}

2 快速排序

1) 算法思想

2) 枢纽的选择

3) 排序过程

void qksort(JD r[], int t, int w)
{
	//t=low,w=high
	int i, j, k;
	JD x;
	if(t >= w)
		return;
	i = t;
	j = w;
	x = r[i]; //枢纽元素
	while(i < j)
	{
		while((i < j) && (r[j].key >= x.key)) //枢纽后面的值大于枢纽
			j--;
		if (i < j) 
		{ //当发现枢纽后面比枢纽小的数后,与枢纽进行交换
			r[i] = r[j];
			i++;
		}
		while((i < j) && (r[i].key <= x.key)) //枢纽前面的值小于枢纽
			i++;
		if (i < j)
		{ //当发现枢纽前面比枢纽大的数后,与枢纽进行交换
			r[j] = r[i];
			j--;
		}
		//缺点是需要将枢纽元素进行多次交换
	}
	r[i] = x;
	qksort(r,t,j-1);
	qksort(r,j+1,w);
}

4) 快速排序的改进

  • 三者取中法 和 不交换枢纽元素 

//交换函数
void swap(JD &a, JD &b)
{
	JD x = a;
	a = b;
	b = x;
}
//三数中值函数
const JD & median3(JD A[], int left, int right)
{
	int center = (left + right)/2;
	if(A[center].key < A[left].key)
		swap(A[left],A[center]);
	if(A[right].key < A[left].key)
		swap(A[left],A[right]);
	if(A[right].key < A[center].key)
		swap(A[center],A[right]);
	//将中值放在最后
	swap(A[center],A[right]);
	return A[right];
}

void quicksort(JD A[], int left, int right)
{
	if (left+10 <= right) //元素超过10个才用快排,否则用直接插入排序
	{
		JD pivot = median3(A,left,right);
		int i = left, j = right-1;
		for( ; ; )
		{
			while(A[i].key < pivot.key)
				i++;
			while(A[j].key > pivot.key)
				j--;
			if(i < j)
				swap(A[i],A[j]);
			else 
				break;
		}
		swap(A[i],A[right]); //将枢纽元素放回中间
		quicksort(A,left,i-1);
		quicksort(A,i+1,right);
	}
	else
		straisort(A,right-left+1); //元素少时用直接插入排序
}

5) 快速排序算法的特点

5. 基数排序(多关键字排序)

  • 高位优先多关键字排序
  • 低位优先多关键字排序

1 高位优先多关键字排序

  • 先对 K^{0} 进行排序,并按 K^{0} 的不同值将记录序列分成若干子序列之后
  • 在子序列中 K^{0} 相同的情况下,分别对 K^{1} 进行排序,......
  • 依次类推,直至对最次位关键字 K^{d-1} 排序完成为止。
  • K^{0},K^{1},...,K^{d-1} 关键字的优先级递减。

2 低位优先多关键字排序

  • 先对关键字 K^{d-1} 进行排序,并按 K^{d-1} 的不同值将记录序列分成若干子序列之后
  • 在子序列中 K^{d-1} 相同的情况下,分别对关键字 K^{d-2} 进行排序,......
  • 依次类推,直至对最高位关键字 K^{0} 排序完成为止。
  • K^{0},K^{1},...,K^{d-1} 关键字的优先级递减。

3 链式基数排序操作步骤

const int M=10;

struct radix_sort
{
	int number;
	radix_sort* next;
};

//桶排序
template<int t>
void bucket_sort(int (&a)[t])  //数组的引用做形参时必须指定大小,但如果大小需要改变时,可以使用模板
{
	int sort[M]={0};
	int count;
	int n=sizeof(a)/sizeof(int);
	for (int i=0;i<n;i++)
	{
		sort[a[i]]+=1;
	}

	for(int j=0;j<M;j++)
	{
		if(sort[j]!=0)
		{
			count=sort[j];
			for (int k=0;k<count;k++)
			{
				cout<<j<<"  ";
			}
		}	
	}
	cout<<endl;
}


//基数排序
template<int t>
void Radix_Sort(int (&a)[t],int b,int p)//b为1,表示从个位开始,b表示目前的桶排序位置,p表示一共的位数
{
	radix_sort* Sort[M]={NULL};
	int j,r;
	int n=sizeof(a)/sizeof(int);
	for (int i=0;i<n;i++)
	{
		r=a[i];
		int d=b;
		while(d--)//如64,4=64%10,6=(64/10)%10;用于提取该位(循环直到取到该位结束)
		{ 
			j=r%10;
			r=r/10;
		}
		if (Sort[j]==NULL) 
		{
			radix_sort* p3=new radix_sort;
			p3->number=a[i];
			p3->next=NULL;
			Sort[j]=p3;
		}
		else
		{
			radix_sort* p=new radix_sort;
			p->number=a[i];
			p->next=NULL;
			radix_sort* p1=Sort[j];
			while(p1->next!=NULL)
			{
				p1=p1->next;
			}
			p1->next=p;
		}
	} //用链表存储目前位相同的数据
	int c=0;
	for(int k=0;k<M;k++)
	{
		if (Sort[k]!=NULL)
		{
			radix_sort* p2=Sort[k];
			while(p2->next!=NULL)
			{
				a[c++]=p2->number;
				p2=p2->next;
			}
			a[c++]=p2->number;
		}
	} //把该位的排序结果赋值给数组a
	if (b!=p)
	{
		Radix_Sort(a,++b,p);//从个位开始依次递归
	}
	else
	{
		for(int q=0;q<n;q++)
		{
			cout<<a[q]<<"  ";
		}
		cout<<endl;
	}
}
  • 数组的引用做形参时必须指定大小,但如果大小需要改变时,可以使用模板

猜你喜欢

转载自blog.csdn.net/sinat_35483329/article/details/86598112
今日推荐