第9章 排序

本章介绍排序相关的知识

 所谓排序,就是要整理数组中的记录,使之按关键字递增(或递减)的次序排列起来。 其确切定义如下。
 输入:n个记录Rj,R₂,...,Ra,其相应的关键字分别为Kj,K₂,.,Kn。
 输出:R₁₁,R₁₂,...,Rin,使得K≤K₂≤...≤Kn(或Ki≥K₂≥...≥Kin)。
 排序可以分为下面5类。
 口插入排序;
 口选择排序;
 口交换排序;
 口归并排序:
 口分配排序。

面试题1  编程实现直接插入排序
 考点:直接插入排序算法的实现
 出现频率:★★★★
 面试题1编程实现直接插入排序315
 【解析】
 1.直接插入排序
 直接插入排序是稳定的排序方法。直接插入排序的基本思想:假设待排序的记录存放 在数组R[1…n]中。初始时,R[1]自成1个有序区,无序区为R[2..n]。从i=2起直至i=n为 止,依次将R[i]插入当前的有序区R[1..-1]中,生成含n个记录的有序区。
 第i-1趟直接插入排序:
 通常将一个记录R[i](i=2,.,n-1)插入到当前的有序区,使得插入后仍保证该区间
 里的记录是按关键字有序的操作,称为第i-1趟直接插入排序。
 排序过程的某一中间时刻,R被划分成两个子区间:R[1...i-1](已排好序的有序区)和
 R[i..n](当前未排序的部分,可称为无序区)。
 直接插入排序的基本操作是将当前无序区的第1个记录R[i]插入到有序区R[1..i-1]中 适当的位置上,使R[1..i]变为新的有序区。因为这种方法每次使有序区增加1个记录,通 常称为增量法。
 插入排序与打扑克时整理手上的牌非常类似。摸来的第1张牌无须整理,此后每次从 桌上的牌(无序区)中摸最上面的1张并插入左手的牌(有序区)中正确的位置上。为了 找到这个正确的位置,须自左向右(或自右向左)将摸来的牌与左手中已有的牌逐一比较。
 由直接插入排序的基本思想很容易得到下面简单的方法。
 (1)在当前有序区R[1...i-1]中查找R[i]的正确插入位置k(1≤k≤i-1)。
 (2)将R[k..i-1]中的记录均后移一个位置,腾出k位置上的空间插入R[i]。
 这里我们使用升序排序,也就是说,如果R[j]的关键字大于等于R[1...i-1]中所有记录 的关键字,则R[i]就是插入的位置。
 还有一种改进的方法,即查找比较操作和记录移动操作交替地进行。其具体做法如下。
 将待插入记录R[i]的关键字从右向左依次与有序区中记录R[j](j=i-1,i-2,....1)的 关键字进行比较:
 (1)如果R[]的关键字大于R[i]的关键字,则将R[j]后移一个位置:
 (2)如果R[j]的关键字小于或等于R[j]的关键字,则查找过程结束,j+1即为R[j]的插 入位置。
 关键字比R[i的关键字大的记录均已后移,所以j+1的位置已经腾空,只要将R[i]直接 插入此位置即可完成一趟直接插入排序。
 2.实现
 我们使用上面介绍的改进的方法,即查找比较操作和记录移动操作交替地进行。代码 如下。

#include <iostream>
using namespace std;

//直接插入排序
void insert_sort(int a[], int n)
{
	int i, j, temp;

	for (i = 1; i < n; i++) //需要选择n-1次
	{
		//暂存下标为1的数。下标从1开始,因为开始时
		//下标为0的数。前面没有任何数,此时认为它是排好顺序的
		temp = a[i];
		for(j = i - 1; j >= 0 && temp < a[j]; j--)
		{
			//如果满足条件就往后挪。最坏的情况就是temp比a[0]小,它要放在最前面
			a[j + 1] = a[j];
		}

		a[j + 1] = temp;  //找到下标为i的数的放置位置
	}
}
	 
static void print_array(int a[], int len)
{
	for (int i = 0; i < len; i++)  //循环打印数组的每个元素
	{
		std::cout << a[i] << " ";
	}
	std::cout << endl;
}
  
int main()
{
	int a[] = { 7, 3, 5, 8, 9, 1, 2, 4, 6 };
	cout << "before insert sort:";
	print_array(a, 9);
	insert_sort(a, 9);		//进行直接插入排序
	cout << "after insert sort:";
	print_array(a, 9);
	return 0;
}

 insert_sort函数的插入次数是len-1,因为当数组只有一个a[0]时,我们认为a[0]就是已 经排好序的了。局部变量i用于表示对哪一个元素进行插入操作,j表示插入到哪个目标元 素的后面,temp保存需要插入的元素。这里最坏的情况就是temp比a[0]都小,此时j为-1, 需要把temp作为新的a[0]。
 测试结果如下。

面试题2  编程实现希尔(Shell)排序
 考点:Shell排序算法的实现
 出现频率:★★★★
 【解析】
 1.希尔(Shell)排序
 希尔(Shell)排序是D.L.shell于1959年提出的,它属于插入排序方法,是不稳定的排 序方法。
 我们知道,在直接插入排序算法中,每次插入一个数,使有序序列只增加1个节点, 并且对插入下一个数没有提供任何帮助。如果比较相隔较远距离(称为增量)的数,使得 数移动时能跨过多个元素,则进行一次比较就可能消除多个元素交换。
 希尔(Shell)排序算法先将要排序的一组数按某个增量d分成若干组,每组中记录的 下标相差d对每组中全部元素进行排序,然后用一个较小的增量对它进行再次分组,并对 每个新组重新进行排序。当增量减到1时,整个要排序的数被分成一组,排序完成。因此 希尔排序实质上是一种分组插入方法。
 希尔排序的时间性能优于直接插入排序,其原因如下。
 口 当数组初始状态基本有序时,直接插入排序所需的比较和移动次数均较少。
 口当n值较小时,n和n2的差别也较小,即直接插入排序的最好时间复杂度O(n)和 最坏时间复杂度0(n2)差别不大。
 口在希尔排序开始时,增量较大,分组较多,每组的记录数目少,故各组内直接插 入较快,后来增量d逐渐缩小,分组数逐渐减少,而各组的记录数目逐渐增多。 但由于已经按d-1作为距离排过序,数组较接近于有序状态,所以新的一趟排序过 程也较快。
 因此,希尔排序在效率上较直接插入排序有较大的改进。
 另外,由于分组的存在,相等的元素可能会分在不同组,导致它们的次序可能发生变 化,因此希尔排序是不稳定的。
 2.实现
 我们可以这样来设置增量:初始时取序列的一半为增量,以后每次减半,直到增量为1。 代码如下。

#include <iostream>
using namespace std;

void shell_sort(int a[], int len)
{
	int h, i, j, temp;
	
	for (h = len / 2; h > 0; h = h / 2)  //控制增量
	{
		for (i = h; i < len; i++)  //这个for循环就是前面的直接插入排序
		{
			temp = a[i];
			for (j = i - h; (j >= 0 && temp < a[j]); j -= h)
			{
				a[j + h] = a[j];
			}
			a[j + h] = temp;
		}
	}
}

static void print_array(int a[], int len)
{
	for (int i = 0; i < len; i++)
	{
		cout << a[i] << " ";
	}
	cout << endl;
}

int main()
{
	int a[] = { 7, 3, 5, 8, 9, 1, 2, 4, 6 };
	cout << "before shell sort:";
	print_array(a, 9);
	shell_sort(a, 9);			//进行shell排序
	cout << "after shell sort:";
	print_array(a, 9);
	return 0;
}


shell_sort函数使用了循环设置增量(代码第8行),里面又嵌套了一个直接插入排序的 算法。注意这个嵌套的算法代码实现,它与上个例题中的代码相比只有一点不同,就是现 在的增量是h,而原来的增量是1。
 测试结果如下。

 面试题3  编程实现冒泡排序
 考点:冒泡排序算法的实现
 出现频率:★★★★
 【解析】
 1.冒泡排序
 冒泡排序的方法为:将被排序的记录数组A[1.…n]垂直排列,每个记录A[i]看作重量为 A[j]气泡。根据轻气泡不能在重气泡之下的原则,从下往上扫描数组A:凡扫描到违反本原 则的轻气泡,就使其向上“飘浮”。如此反复进行,直到最后任何两个气泡都是轻者在上、重 者在下为止。
 冒泡排序是稳定的排序。下面是具体的算法。
 (1)初始状态下,A[1.n]为无序区。
 (2)第一趟扫描:从无序区底部向上依次比较相邻的两个气泡的重量,若发现轻者 在下、重者在上,则交换二者的位置。即依次比较(A[n],A[n-1]),(A[n-1],A[n-2]),…,
 (A[2],A[1]);对于每对气泡(A[j+1],A[Jj]),若A[j+1]<A[j],则交换A[j+1]和A[j]的 内容。
 第一趟扫描完毕时,“最轻”的气泡就飘浮到该区间的顶部,即关键字最小的记录被放 在最高位置A[1]上。
 (3)第二趟扫描:扫描A[2..n]。扫描完毕时,“次轻”的气泡飘浮到A[2]的位置上。
 (4)第i趟扫描:A[1.i-1]和A[i..n]分别为当前的有序区和无序区。扫描仍是从无序 区底部向上,直至该区顶部。扫描完毕时,该区中最轻气泡飘浮到顶部位置A[i]上,结果
 是A[1..i]变为新的有序区。
 最后,经过n-1趟扫描可得到有序区A[1…n]。
 2.实现
 根据前面冒泡扫描的方法,可以写出下面的排序代码。

void bubble_sort_1(int a[], int len)
{
	int i = 0;
	int j = 0;
	int temp = 0;     //用于交换

	for (i = 0; i < len - 1; i++)   //进行n-1趟扫描
	{
		for (j = len - 1; j >= i; j--)  //从后往前交换,这样最小值冒泡到开头部分
		{
			if (a[j] < a[j-1])   //如果a[]]小于a[j-1],则交换两元素的值
			{
				temp = a[j];
				a[j] = a[j - 1];
				a[j - 1] = temp;
			}
		}
	}
}

这个代码有一个小问题,就是假如进行第i次扫描前,数组已经排好序了,但是它还会 进行下一次的扫描,显然以后的扫描都是没有必要的。
我们可以对上面的这个代码进行一点改进,代码如下。

void bubble_sort_2(int a[], int len)
{
	int i = 0;
	int j = 0;
	int temp = 0;							//用于交换
	int exchange = 0;						//用于记录每次扫描时是否发生交换

	for (i = 0; i < len - 1; i++)			//进行n-1趟扫描
	{
		exchange = 0;						//每趟扫描之前对exchange置0
		for (j = len - 1; j >= i; j--)		//从后往前交换,这样最小值冒泡到开头部分
		{
			if (a[j] < a[j - 1])			//如果a[j]小于a[j-1],交换两元素的值
			{
				temp = a[j];
				a[j] = a[j - 1];
				a[j - 1] = temp;
				exchange = 1;				//发生交换,exchange置1
			}
		}

		if (exchange != 1)					//此趟扫描没有发生过交换,说明已经是排序的
			return;							//不需要进行下次扫描
	}
}

这里我们使用一个局部变量exchange来记录在本次扫描时有没有进行过数据交换。每 次扫描之前,把exchange置0(代码第10行)。如果扫描时发生数据交换,则把exchange置1(代码第18行):如果没有,则说明数组已经是排序的了,不需要进行下一趟扫描(代码第22行)。
对两种冒泡排序的测试main函数如下。

int main()
{
	int a[] = { 7, 3, 5, 8, 9, 1, 2, 4, 6 };
	cout << "before bubble sort:";
	print_array(a, 9);
	//bubble_sort_1(a, 9);			 //冒泡排序
	bubble_sort_2(a, 9);		 //改进的冒泡排序
	cout << "after bubble sort:";
	print_array(a, 9);
	return 0;
}

测试结果如下。

面试题4  编程实现快速排序
 考点:快速排序算法的实现
 出现频率:★★★★
 【解析】
 1.快速排序
 快速排序是C.R.A.Hoare于1962年提出的一种划分交换排序。它采用了一种分治的策 略,通常称其为分治法(Divide-and-ConquerMethod)。分治法的基本思想是:将原问题分解 为若干个规模更小但结构与原问题相似的子问题。递归地解这些子问题,然后将这些子问 题的解组合为原问题的解。
 快速排序的基本思想:设当前待排序的无序区为A[low..high],利用分治法可描述为:
 (1)分解:在A[low..high]中任选一个记录作为基准(pivot),以此基准将当前无序区 划分为左、右两个较小的子区间A[low...pivotpos-1]和A[pivotpos+1..high],并使左边子区 间中所有记录的关键字均小于等于基准记录(pivot),右边的子区间中所有记录的关键字均 大于等于pivot,而基准记录pivot则位于正确的位置上,它无须参加后续的排序。
 注意,划分的关键是要求出基准记录所在的位置pivotpos。划分的结果可以简单地表示 为(pivot-A[pivotpos]):A[low...pivotpos-1]≤A[pivotpos]≤A[pivotpos+1..high],其中low ≤pivotpos≤high。
 (2)求解:通过递归调用快速排序对左、右子区间A[low..pivotpos-1]和A[pivotpos+
 1..high]快速排序。
 (3)组合:当“求解”步骤中的两个递归调用结束时,其左、右两个子区间已有序。 对快速排序而言,“组合”步骤无须做什么,可看作空操作。
 2.实现
 源代码如下。

void quick_sort(int a[], int low, int high)
{
	int i, j, pivot;
	if (low < high)
	{
		pivot = a[low];
		i = low;
		j = high;
		while (i < j)
		{
			while (i < j && a[j] >= pivot)
				j--;
			if (i < j)
				a[i++] = a[j];     //将比pivot小的元素移到低端
			while (i < j && a[i] <= pivot)
				i++;
			if (i < j)
				a[j--] = a[i];     //将比pivot大的元素移到高端
		}
		a[i] = pivot;				//pivot移到最终位置
		quick_sort(a, low, i - 1);  //对左区间递归排序
		quick_sort(a, i + 1, high);  //对右区间递归排序
	}
}

这里pivot代表基准值,它的初始值为a[low]。局部变量i和j分别代表low和high的 位置。接着按照下面的步骤进行一趟交换。
 (1)把比pivot小的元素移到低端(low)。
 (2)把比pivot大的元素移到高端(high)。
 (3)pivot移到最终位置,此时这个位置的左边元素的值都比pivot小,而其右边元素的 值都比pivot大。
 (4)对左、右区间分别进行递归排序。从而把前三步的粗排序逐渐地细化,直至最终 low和high交汇。
 测试程序如下。

int main()
{
	int data[9] = { 54,38,96,23,15,72,60,45,83 };
	quick_sort(data, 0, 8);     //快速措序
	for (int i = 0; i < 9; i++)
		cout << data[i] << " ";   //打印排序后的数组

	return 0;
}

 执行结果:

面试题5 编程实现选择排序
 考点:直接选择排序算法的实现
 出现频率:★★★
 【解析】
 1.直接选择排序
 直接选择排序的基本思想:n个记录的直接选择排序可经过n-1趟直接选择排序得到有 序结果。
 (1)初始状态:无序区为A[1n],有序区为空。
 (2)第1趟排序:在无序区A[1.n]中选出最小的记录A[k],将它与无序区的第1 个记录A[1]交换,使A[1..1]和A[2..n]分别变为记录个数增加1的新有序区和记录个 数减少1的新无序区。
 (3)第i趟排序:第i趟排序开始时,当前有序区和无序区分别为A[1..i-1]和A[i.n]
 (I≤i≤n-1)。该趟排序从当前无序区中选出关键字最小的记录A[k],将它与无序区的第1 个记录A[i]交换,使A[1..i]和A[i+1..n]分别变为记录个数增加1的新有序区和记录个数 减少1的新无序区。
 这样,n个记录的文件的直接选择排序可经过n-1趟直接选择排序得到有序结果。 直接选择排序是不稳定的。
 2.实现
 源代码如下。

#include <iostream>
using namespace std;

void select_sort(int a[], int len)
{
	int i, j, x,l;
	
	for (i = 0; i < len; i++)    //进行n-1次遍历
	{
		x = a[i];					//每次遍历前x和1的初值设置
		l = i;
		for (j = i; j < len; j++)	//遍历从1位置向数组尾部进行
		{
			if (a[j] < x)
			{
				x = a[j];			//x保存每次遍历搜索到的最小数
				l = j;				//1记最最小数的位置
			}
		}
		a[l] = a[i];				//把最小元素与a[1]进行交换
		a[i] = x;
	}
}

int main()
{
	int data[9] = { 54,38,96,23,15,72,60,45,83 };
	select_sort(data, 9);     //选择措序
	for (int i = 0; i < 9; i++)
		cout << data[i] << " ";   //打印排序后的数组
	return 0;
}

 select_sort函数进行了n-1趟排序。局部变量x和1分别记录每次遍历时所得的最小元 素值及所在位置,代码第20~21行利用它们进行与a[i]的交换。以main函数中的data数组 为例,说明其具体步骤。
 (1)第1次排序:数组各元素为54,38,96,23,15,72,60,45,83,此时i为0, 遍历整个数组得到最小元素15,然后与a[0]进行交换,结果为15,38,96,23,54,72,
 60,45,83。
 (2)第2次排序:此时i为1,遍历从a[1]开始到数组末尾结束,得到最小元素23,然 后与a[1]进行交换,结果为15,23,96,38,54,72,60,45,83。
 (3)第3次排序:此时i为2,遍历从a[2]开始到数组末尾结束,得到最小元素38,然后 与a[2]进行交换,结果为15,23,38,96,54,72,60,45,83。
 显然,每次排序都选出了一个最小的元素,与遍历起始位置的元素进行交换。通过n-1 次这样的排序,最终把整个数组进行了排序。
 执行结果为:

面试题6编程实现堆排序
 考点:堆排序算法的实现
 出现频率:★★★
 【解析】
 1.堆排序
 堆排序定义:n个序列Al,A2,…,An称为堆,有下面两种不同类型的堆。
 口 小根堆:所有子结点都大于其父节点,即Ai≤A2i且Ai≤A2i+1。
 口 大根堆:所有子结点都小于其父节点,即Ai≥A2i且Ai≥A2i+1。
 若将此序列所存储的向量A[1..n]看为一棵完全二叉树的存储结构,则堆实质上是满足 如下性质的完全二叉树:树中任一非叶结点的关键字均不大于(或不小于)其左、右子节 点(若存在)的关键字。
 因此堆排序(HeapSort)是树形选择排序。在排序过程中,将R[L.n]看成一棵完全二叉 树的顺序存储结构,利用完全二叉树中双亲结点和孩子结点之间的内在关系,在当前无序 区中选择关键字最大(或最小)的记录。
 用大根堆排序的基本思想:
 (1)先将初始A[1.n]建成一个大根堆,此堆为初始的无序区。
 (2)再将关键字最大的记录A[1](堆顶)和无序区的最后一个记录A[n]交换,由此得
 到新的无序区A[1..n-1]和有序区A[n],且满足A[1.n-1]≤A[n]。
 (3)由于交换后新的根A[1]可能违反堆性质,故应将当前无序区A[1.n-1]调整为堆。
 然后再次将A[1.n-1]中关键字最大的记录A[1]和该区间的最后一个记录A[n-1]交换,由此 得到新的无序区A[1…n-2]和有序区A[n-1…n],且仍满足关系A[1n-2]≤A[n-1..n],同样要 将A[1…n-2]调整为堆。
 (4)对调整的堆重复进行上面的交换,直到无序区只有一个元素为止。
 构造初始堆必须用到调整堆的操作,现在说明Heapify函数思想方法。

每趟排序开始前,A[L..i]是以A[1]为根的堆,在A[1]与A[j]交换后,新的无序区A[1.i-1] 中只有A[1]的值发生了变化,故除A[1]可能违反堆性质外,其余任何结点为根的子树均是 堆。因此,当被调整区间是A[low..high]时,只须调整以A[low]为根的树即可。
 可以使用“筛选法”进行堆的调整。A[low]的左、右子树(若存在)均已是堆,这两 棵子树的根A[2low]和A[2low+1]分别是各自子树中关键字最大的节点。若A[low]不小于 这两个孩子节点的关键字,则A[low]未违反堆性质,以A[low]为根的树已是堆,无须调 整;否则必须将A[low]和它的两个孩子节点中关键字较大者进行交换,即A[low]与A[large] (A[large]=max(A[2low],A[2low+1]))交换。交换后又可能使节点A[large]违反堆性质。 同样,由于该节点的两棵子树(若存在)仍然是堆,故可重复上述调整过程,对以A[large] 为根的树进行调整。此过程直至当前被调整的节点已满足堆性质,或者该节点已是叶子 为止。上述过程就像过筛子一样,把较小的关键字逐层筛下去,而将较大的关键字逐层 选上来。
 2.实现
 源代码如下。

#include <iostream>
using namespace std;

int heapSize = 0;
//返回左子节点索引
int Left(int index)
{
	return ((index << 1) + 1);
}

//返回右子节点索引
int Right(int index)
{
	return ((index << 1) + 2);
}

//交换a,b的值
void swap(int* a, int* b)
{
	int temp = *a;
	*a = *b;
	*b = temp;
}

//arraytindex]与其左、右子树进行递归对比
//用最大值替换array[index],index表示堆顶索引
void maxHeapify(int array[], int index)
{
	int largest = 0;			//最大数
	int left = Left(index);		//左子节点索引
	int right = Right(index);	//右子节点索引

	//把largest赋为堆顶与其左子节点的较大者
	if ((left <= heapSize) && (array[left] > array[index]))
		largest = left;
	else
		largest = index;

	//把largest与堆顶的右子节点比较,取较大者
	if ((right <= heapSize) && (array[right] > array[largest]))
		largest = right;

	//此时largest为堆顶、左子节点、右子节点中的最大者
	if (largest != index)
	{
		//如果堆顶不是最大者,则交换,并递归调整堆
		swap(&array[index], &array[largest]);
		maxHeapify(array, largest);
	}
}

//初始化堆,将数组中的每一个元素置放到适当的位置
//完成之后,堆顶的元素为数组的最大值
void buildMaxHeap(int array[], int length)
{
	int i;
	heapSize = length;			//堆大小赋为数组长度
	for (i = (length >> 1); i >= 0; i--)
		maxHeapify(array, i);
}

void heap_sort(int array[], int length)
{
	int i;
	//初始化堆
	buildMaxHeap(array, (length - 1));
	for (i = (length - 1); i >= 1; i--)
	{
		//堆顶元素array[0](数组的最大值)被置换到数组的尾部array[1]
		swap(&array[0], &array[i]);
		heapSize--;				//从堆中移除该元素
		maxHeapify(array, 0);	//重建堆
	}
}

int main()
{
	int a[8] = {45, 68, 20, 39, 88, 97, 46, 59};
	heap_sort(a, 8);     //堆措序
	for (int i = 0; i < 8; i++)
		cout << a[i] << " ";   //打印排序后的数组
	cout << endl;
	return 0;
}



heap_sort函数按下面步骤进行。
 (1)调用buildMaxHeap对数组进行堆的初始化(代码第57行)
 (2)由于堆顶元素(array[0])的值是最大的(大根堆),因此把它与数组尾部进行交换。 并把heapSize递减1,即从堆中移除数组尾部元素。
 (3)由于只有剩下的堆顶元素(array[0])不满足堆,因此调用maxHeapify重建堆。
 (4)对前面两步进行循环调用,直到堆中只含有堆顶,此时heapSize变为1(i为0)。 其中buildMaxHeap函数初始化堆时也调用了maxHeapify函数,而maxHeapify使用递 归的方法把堆调整为大根堆。
 测试结果为:

面试题7实现归并排序的算法(使用自顶向下的方法)
 考点:归并排序算法的实现
 出现频率:★★★
 【解析】
 1.归并排序
 归并排序(Merge Sort)是利用“归并”技术来进行排序。归并是指将若干个已排序的 子文件合并成一个有序的文件。
 两路归并算法的基本思路:设两个有序的子文件(相当于输入堆)放在同一向量中相 邻的位置上:A[low...m],a[m+1...high],先将它们合并到一个局部的暂存向量Temp(相当 于输出堆)中,待合并完成后将Temp复制回A[low..high]中。
 归并排序有两种实现方法:自底向上和自顶向下。
 自底向上方法的基本思想:
 (1)第1趟归并排序时,将待排序的文件A[1.n]看作n个长度为1的有序子文件,将 这些子文件两两归并。若n为偶数,则得到n/2个长度为2的有序子文件;若n为奇数,则最后一个子文件不参与归并。故本趟归并完成后,前n/2个有序子文件长度为2,但最后一
 个子文件长度仍为1。
 (2)第2趟归并则是将第1趟归并所得到的n/2个有序的子文件两两归并,如此反复, 直到最后得到一个长度为n的有序文件为止。
 (3)上述每次归并操作,均是将两个有序的子文件合并成一个有序的子文件,故称其 为“二路归并排序”。类似地,有k(k>2)路归并排序。
 自顶向下算法的设计,形式更为简洁。设归并排序的当前区间是A[low..high],步骤如下。
 (1)分解:将当前区间一分为二,即求分裂点。
 (2)求解:递归地对两个子区间A[low..mid]和A[mid+1..high]进行归并排序。
 (3)组合:将已排序的两个子区间A[low..mid]和A[mid+1.high]归并为一个有序的区 间R[low...high]。
 (4)递归的终结条件:子区间长度为1(一个记录自然有序)。
 2.实现
 归并排序算法可用顺序存储结构,也易于在链表上实现。本题中我们使用数组结构。 根据前面介绍过的自顶向下算法步骤,可以实现如下程序。
#include <iostream>
using namespace std;

//将分治的两端按大小次序填入临时数组,最后把临时数组拷贝到原始数组中
//1Pos到rPos-1为一端,rPos到rEnd为另外一端
void merge(int a[], int tmp[], int lPos, int rPos, int rEnd)
{
	int i, lEnd, numElements, tmpPos;
	lEnd = rPos - 1;
	tmpPos = lPos;						//从左端开始
	numElements = rEnd - lPos + 1;		//数组长度

	while (lPos <= lEnd && rPos <= rEnd)
	{
		if (a[lPos] <= a[rPos])			//比较两端的元素值
			tmp[tmpPos++] = a[lPos++];	//把较小的值先放入tmp临时数组
		else
			tmp[tmpPos++] = a[rPos++];
	}

	//到这里,左端或右端只能有一端还可能含有剩余元素
	while (lPos <= lEnd)				//把左端剩余的元素放入tmp
		tmp[tmpPos++] = a[lPos++];

	while (rPos <= rEnd)				//把右端剩余的元素放入tmp
		tmp[tmpPos++] = a[rPos++];

	for (i = 0; i < numElements; i++, rEnd--)
		a[rEnd] = tmp[rEnd];			//把临时数组拷贝到原始数组
}

void msort(int a[], int tmp[], int low, int high)
{
	if (low >= high)	//结束条件,原子结点return
		return;

	int middle = (low + high) / 2;			//计算分裂点		
	msort(a, tmp, low, middle);				//对子区间[low,middle]递归做归并排序		
	msort(a, tmp, middle + 1, high);		//对子区间[middle+1,high]递归做归并排序	
	merge(a, tmp, low, middle + 1, high);	//组合,把两个有序区合井为一个有序区		
}

void merge_sort(int a[], int len)
{
	int* tmp = nullptr;
	tmp = new int[len];					//分配临时数组空间
	if (tmp != nullptr)
	{
		msort(a, tmp, 0, len - 1);		//调用msort归井排序
		delete []tmp;					//释放临时数组内存
	}
}

int main()
{
	int a[8] = { 8,6,1,3,5,2,7,4 };
	merge_sort(a, 8);
	for (int i = 0; i < 8; i++)
		cout << a[i] << " ";   //打印排序后的数组
	cout << endl;
	return 0;
}

merge_sort函数是归并的最外层调用,它调用了msort函数,msort是归并算法的递归 实现。它的步骤与前面介绍的相同,分为三个步骤:
 (1)代码第37行,计算分裂点,把区间一分为二。
 (2)代码第38~39行,递归地对两个子区间A[low..middle]和A[middle+1..high]进行 归并排序。
 (3)代码第40行,调用Merge函数合并两个排序后的区间。

 Merge函数将分治的两端(这两端是已经排好序的)按大小次序填入临时数组,最后把 临时数组拷贝到原始数组中。

面试题8 使用基数排序对整数进行排序
 考点:基数排序算法的实现
 出现频率:★★
 【解析】
 1.基数排序
 基数排序是箱排序的改进和推广。
 箱排序也称桶排序(Bucket Sort),其基本思想是:设置若干个箱子,依次扫描待排序 的记录R[0],R[1],…,R[n-1],把关键字等于k的记录全都装入到第k个箱子里(分配), 然后按序号依次将各非空的箱子首尾连接起来(收集)。
 例如,要将一副混洗的52张扑克牌按点数A<2<…<J<Q<K排序,需设置13个“箱子”, 排序时依次将每张牌按点数放入相应的箱子里,然后依次将这些箱子首尾相接,就得到了 按点数递增顺序排列的一副牌。
 基数排序是基于多关键字的,什么是多关键字呢?如果文件中任何一个记录R[j]的关键 字都由d个分量构成,而且这d个分量中每个分量都是一个独立的关键字,则文件是多关 键字的(比如扑克牌有两个关键字:点数和花色)。
 通常实现多关键字排序有两种方法:
 口 最高位优先(Most Significant Digit first,MSD); 口最低位优先(LeastSignificantDigit first,LSD)。
 基数排序是典型的LSD排序方法,其基本思想是:从低位到高位依次对数据进行箱排 序。在d趟箱排序中,所需的箱子数就是基数rd(可能的取值个数),这就是“基数排序” 名称的由来。
 比如,对于值范围为10~99的整数序列:45,13,58,64,29,74,39,18,使用基 数排序需要10个箱子(从0~9标号)进行分配和收集。我们如果把每一个数看成由两个关键字构成(个位数和十位数),那么可以对它们进行两次分配和收集(分别对于个位和十 位),具体步骤如下。
 (1)对序列的各个元素按个位进行顺序装箱,即45装入5号箱,13装入3号箱,58 和18装入8号箱,64和74装入4号箱,29和39装入9号箱。
 (2)从0到9号箱顺序依次收集到原序列,即3号箱的13,4号箱的64和74,5号箱
 的45,8号箱的58和18,9号箱的29和39被依次收集。序列变为13,64,74,45,58,
 18,29,39。
 (3)对序列的各个元素按十位进行顺序装箱,即13和18装入1号箱,29装入2号箱, 30装入3号箱,45装入4号箱,58装入5号箱,64装入6号箱,74装入7号箱。
 (4)再次从0到9号箱顺序收集到原序列,序列变为13,18,29,30,45,58,64,74。 此时完成基数排序。
 对于一个两位数来说,其十位数当然比个位数关键。因此使用LSD时,先对个位数开 始分配和收集。
 2.实现
 前面我们已经分析过了使用基数排序对整数序列进行排序的具体步骤。因此,如果整 数的范围没有指明,则我们需要查找数组最大的元素有多少位数,以便确定需要进行几次 分配和收集,还需要知道每一位是什么。比如数据167,我们不仅需要知道167是一个三位 数,而且还需要知道它的个位是7,十位是6,百位是1。
 程序代码如下。

#include <iostream>
#include <math.h>
using namespace std;

int find_max(int a[], int len)				//查找长度为len的数组的最大元素
{
	int max = a[0];							//max从a[0]开始
	for (int i = 1; i < len; i++)
	{
		if (max < a[i])						//如果发现元素比max大
			max = a[i];						//就重新给max赋值
	}
	return max;
}

//计算number有多少位
int digit_number(int number)
{
	int digit = 0;
	do 
	{
		number /= 10;
		digit++;
	} while (number != 0);
	return digit;
}

//返回number上第Kth位的数字
int kth_digit(int number, int kth)
{
	number /= pow(10, kth);
	return number % 10;
}

//对长度为1en的数组进行基数排序
void radix_sort(int a[], int len)
{
	int* temp[10];								//指针数组,每一个指针表示一个箱子
	int count[10] = { 0,0,0,0,0,0,0,0,0,0 };	//用于存储每个箱子装有多少元素
	int max = find_max(a, len );				//取得序列中的最大整数
	int maxDigit = digit_number(max);			//得到最大整数的位数
	int i, j, k;
	for (i = 0; i < 10; i++)
	{
		temp[i] = new int[len];					//使每一个箱子能装下len个int元素	
		memset(temp[i], 0, sizeof(int) * len);	//初始化为0
	}
	for (i = 0; i < maxDigit; i++)
	{
		memset(count, 0, sizeof(int) * 10);		//每次装箱前把count清空
		for (j = 0; j < len; j++)
		{
			int xx = kth_digit(a[j], i);		//将数据安装位数放入到替存数组中
			temp[xx][count[xx]] = a[j];
			count[xx]++;						//此箱子的计数递增
		}
		int index = 0;
		for (j = 0; j < 10; j++)				//将数据从暂存数组中取回,放入原始数组中
		{
			for (k = 0; k < count[j]; k++)		//把箱子里所有的元素都取回到原始数组
			{
				a[index++] = temp[j][k];
			}
		}
	}
}

int main()
{
	int a[] = {22, 32, 19, 53, 47, 29};
	radix_sort(a, 6);
	for (int i = 0; i < 6; i++)
		cout << a[i] << " ";   //打印排序后的数组
	cout << endl;
	return 0;
}

下面简单说明一下radix_sort函数的执行步骤。
 (1)代码第40~41行,调用find_max取得序列中的最大整数,并调用digit_number 得到其最大位数maxDigit。
 (2)代码第43~47行,分配10个足够大的箱子来存放序列中的整数。
 (3)代码第51~56行,针对序列中整数的个位数,进行第一次分配箱子。
 (4)代码第57~64行,依次收集每个箱子的元素,放回到原始数组中。
 步骤(3)和步骤(4)一共需要进行maxDigit次,每一次针对序列中整数的不同位数 进行分配箱子。代码第53行调用了kth_digit计算元素各个位数的数字,以确定放入哪一个 箱子。另外,在radix_sort函数里还有一个局部数组count,它被用来在每次分配箱子后, 保存各箱子里所含整数元素的个数。

运行结果:

面试题9   选择题——各排序算法速度的性能比较
 考点:各排序算法速度的性能比较
出现频率:★★★★
 下面哪种排序法对1,2,3,5,4最快?(  )
 A.quick sort
 B.buble sort
 C.merge sort
 【解析】
 选择排序算法的时候,需要考虑以下几点。
 口 数据的规模;
 口数据的类型;
 口 数据已有的顺序。
 一般来说,当数据规模较小时,应选择直接插入排序或冒泡排序。任何排序算法在数 据量小时基本体现不出差距。
 考虑数据的类型,比如全部是正整数时,应该考虑使用桶排序。
 考虑数据已有顺序,快速排序是一种不稳定的排序(当然可以改进)。对于大部分排好 的数据,快速排序会浪费大量不必要的步骤。我们说快速排序好,是指大量随机数据下, 使用快速排序的效果最理想,而不是指所有情况。
 根据题目来看,1,2,3,5,4数据量极小,已经基本排好序。所以,此时冒泡排序是 最佳选择。
 【答案】
 B

面试题10 各排序算法的时间复杂度的比较
 考点:各排序算法的时间复杂度的比较
 出现频率:★★★
 写出下列算法的时间复杂度。
 (1)冒泡排序;
 (2)选择排序;
 (3)插入排序:
 (4)快速排序;
 (5)堆排序:
 (6)归并排序。
 【答案】
 冒泡排序算法的时间复杂度是O(n^2)。
 选择排序算法的时间复杂度是O(n^2)。
 插入排序算法的时间复杂度是O(n^2)。
 快速排序是不稳定的,最理想情况下的算法时间复杂度是O(nlog2n),最坏是O(n^2)。
 堆排序算法的时间复杂度是O(nlogn)。
 归并排序算法的时间复杂度是O(nlog2n)。

https://blog.csdn.net/chenyijun/category_1624461.html

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
#这只是一个半成品,只是实现了,一个完整的订票过程,对于,如果刷票,自己研究 #简单过程 # 第一、getRandAndCookie() 获得cook 和一个随机数用于登录 # 第二、getEnterRandCode() 得到登录时的识别码 # 第三、setuseandpassword(randcode,use,password) 发送随机数、识别码和用户及密码。由于随机数只在内部使用,所以定义成了全局变量, # 第四、GetTrainList() 得到所有车站列表,'@bjb|北京北|VAP|beijingbei|bjb|0' 其文、拼音、拼音缩写、所一个ID(唯一),其主要是可以,通过上面的列表,找到它的唯一ID,TranCityToId('南昌') # 第五、GetTrainNumList(date,fromstationid,tostationid,starttime) 得到哪到哪的所在车次,消息格式如下,其所以,一下车次的的ID:"id":"650000K1060I" # {"end_station_name":"北京西","end_time":"16:18","id":"650000K1060I","start_station_name":"深圳","start_time":"10:54","value":"K106"} # 通过ChangeToTrainNumId('K106')得到车次ID # 第六、QueryTrain(fromstationid,tostationid,date,stationNum,starttime) 就是点击查询按键,得到是否有能预订,格式如下 #       南昌         20:12,    北京西        07:38,11:26,--,--,--,--,10,有,有,--,有,有,--,<a name='btn130_2' class='btn130_2' # 通过choiceSubmitNum(stationNum,trainsubmitinfo)提取出getSelected()消息 # 第七、submitRequest(choiceSubmitNum(stationNum,trainsubmitinfo),date,starttime) 就是点击预订按钮 # 第八、getrandCheckCode()得到提交订单的识别码 # 第十、CheckInMyTicket(info,randcode,peoples)点击提交,如果成功的话,就会返回{"errMsg":"Y"} # 出于,网络是UTF8格式,所以,必须# -*- coding: utf-8 -*-,(当然,自己转换也是可以的) # 出于这一个控制台信息,所以,识别码的图片在脚本同一目录 #得到头信息
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值