天勤——排序课后题

一,真题试练

一个序列有正有负,编写算法使负数排在非负数前面

思路1,直接插入

我的思路是按直接插入排序来执行,有一个小问题是如果在循环中定义了j,那么循环结束后就无法使用j了。因此要在循环开始前定义j。
同时如果在循环开始前定义j,那么最好就用while循环,因为for循环第一个条件空着比较丑。

void sort(int A[], int n) {
	for (int i = 1; i < n; ++i) {
		if (A[i] < 0) {
			int temp = A[i];
			int j = i - 1;
			while(j>=0){
				if (A[j] >= 0)
					A[j + 1] = A[j];
				else
					break;
				--j;
			}
			A[j + 1] = temp;//在循环外使用j的话,之前就不能在循环中定义j,否则就是局部变量。
		}
	}
}

时间复杂度为:O( n 2 n^{2} n2)

思路2,类似快排

low和high分别从左右向中间遍历,若low遇到正数停下来,high遇到负数停下来。然后交换low和high的值。这样只需遍历n次即可,时间复杂度为O(n)

void sort(int A[], int n) {
	int low = 0;
	int high = n - 1;
	int flag, temp;
	while (low < high) {
		while (low < high && A[high] >= 0)
			--high;
		while (low < high && A[low] < 0)
			++low;
		if (A[high] < 0 && A[low] >= 0) {//当序列有序时,high指向了前面的负数,而low遍历到high前面结束指向的也是负数,无需再交换。
			temp = A[low];
			A[low] = A[high];
			A[high] = temp;
		}
	}
}

长度为0-n-1的数组A中所有数的值不同,且取值范围也是0-n-1。编写算法将A的元素排序到B中

值和序号都是唯一的,那么每个元素的值就可以当做序号。直接遍历一次将B[A[i]]=A[i];注意此题空间复杂度为O(n)。

二,选择题

1,TQ5,对有序序列进行排序,各个算法的时间复杂度。

最快:

1,直接插入排序,for循环遍历每个元素,且每个当前遍历的元素>=前一元素,已经有序故不操作。实际上只进行了n-1次的循环遍历,为O(n)。

2,冒泡排序,若一趟排序中,没有进行任何交换则直接中止遍历。对于有序序列,则是在遍历完一次序列后退出,时间复杂度为O(n)。

最慢:

快速排序,序列有序或逆序时,每层递归都会被分为n-1长度的子表和0长度子表,相当于每次low指向的元素都比后面的元素小,就要从最右边遍历到low。对应的时间复杂度为 O ( n 2 ) O(n^{2}) O(n2)

与初始序列无关:

1,堆排序,要先构建大根堆,使得堆顶元素最大,然后将堆顶元素与堆底元素互换,再对除堆底元素的序列重新调整堆,循环排序。序列有序也要先构建大根堆然后依次循环放置堆顶元素到已排序列,因此在最坏、最好和平均状况下都为 O ( n l o g n ) O(nlog^{n}) O(nlogn)

2,归并排序,实际是借助一个辅助数组,将两个有序序列对比然后将元素从小到大排入原始序列。其对序列的分割排序与原始序列无关,不管序列是否有序都要一一归并,时间复杂度始终为 O ( n l o g n ) O(nlog^{n}) O(nlogn)

3,简单选择排序,有序时,无需移动元素,但是元素之间比较的次数与初始序列状态无关,始终要从头到尾比较,因此时间复杂度始终为 O ( n 2 ) O(n^{2}) O(n2)

4,基数排序,d为位数,r为基数,n为序列长度;需要d趟分配和收集,每趟分配对序列遍历O(n),收集元素时对队列遍历O®,则时间复杂度为O(d(n+r)),与初始序列无关。就算是有序序列,也要按位分别进行元素的分配与收集。

与初始序列有关

1,折半插入排序,与直接插入排序不同,需要对已排序列进行折半查找,找到当前遍历元素的插入位置,而查找时间与原始序列初始状态无关(因为始终是在已排序列中查找),为 O ( n l o g n ) O(nlog^{n}) O(nlogn)。然后找到位置后因为原本就是有序序列,只需插入无需移动元素。最终时间复杂度为 O ( n l o g n ) O(nlog^{n}) O(nlogn)

2,希尔排序,只需要调整增量,然后遍历子表,但是因为已经有序了,因此无需再对子表调整。调整增量时间复杂度为 O ( l o g n ) O(log^{n}) O(logn),子表每次遍历不超过n次,则时间复杂度为 O ( n l o g n ) O(nlog^{n}) O(nlogn)

2,TQ6,n个关键字序列,只要前k个最小关键字。使用哪种算法?

每一趟都可以确定一个元素最终位置的算法有:冒泡排序、简单选择排序、堆排序和快速排序。
①对于快速排序,每次只是将枢轴放入最终位置,不能快速找出最小的。
②对于冒泡排序和简单选择排序,每趟找出一个最小的,只需k趟即可。每一趟都需要对未排序列进行遍历,则需要kn次。
③对于堆排序,也是需要k趟, k l o g n klog^{n} klogn次,但还多了个建立初始堆的时间<=4n,则时间复杂度为 4 n + k l o g n 4n+klog^{n} 4n+klogn
则易知,k>=5时堆排序更好,k<5时,可以选择冒泡和简单选择排序。

3,TQ9 10,原始序列与算法趟数、比较次数。

趟数与原始序列

1,有关:
交换类排序,快速排序,冒泡排序,当序列有序时,快速排序趟数大大提升要从第一个一直分划到最后一个。而冒泡排序只需一趟即可结束。

2,无关:
插入类,无论原始序列如何,都要遍历固定的趟数,每趟插入一个元素,或者一组子表调整。

简单选择排序,每趟都要选择一个元素,因此也是固定趟数。

堆排序,固定要从堆底向堆顶遍历处理,堆顶与堆底元素交换,存入有序序列最终位置。

k路归并排序,将其想象为完全k叉树,序列个数N=最后一层元素数;趟数m=层数h-1;则 k m = k h − 1 = N k^{m}=k^{h-1}=N km=kh1=N m = l o g k N m=log_{k}^{N} m=logkN

基数排序,趟数固定为d次,每次处理一个位的数据。

比较数与原始序列

1,有关
直接插入排序,希尔排序,在执行元素移动时,还需向前遍历有序序列,与当前元素比较。如果序列有序,则无需进行元素移动的比较。

快速排序,若序列有序,那么每次都要总high到low执行比较。

冒泡排序,若序列有序,只比较一趟就中止了。

2,无关
简单选择排序,无论序列是否有序,每趟都要遍历序列依次与当前最小值对比。

折半插入排序,在关键字对比方面,由于采取了折半查找法,固定找出待排位置,然后直接移动无需比较元素大小。每次比较的次数都固定,和初始序列无关,都是在low>high时结束。

4,TQ22,简单选择排序比较、交换次数。

比较,对n个元素代表n个待排位置,每个位置都要遍历待排序列,因此为 O ( n 2 ) O(n^{2}) O(n2)

交换,对于n个元素,如果都不在待排序列第一个的位置上,则都需要依次交换,因此为 O ( n ) O(n) O(n)

5,TQ33,归并排序的两个基本阶段?

1,生成初始归并段
2,对初始归并的进行多次归并。

三,综合题

1,TQ2,快速排序算法改错

题目给的代码,要求找出错误

int partition(int A[], int low, int high)
{
	int pivot = low;
	while (low < high) {
		while (low < high && A[high] > pivot)
			--high;
		A[low] = A[high];
		while (low < high && A[low] < pivot)
			++low;
		A[high] = A[low];
	}
	A[low]= A[pivot];
	return low;
}

void QuickSort(int A[], int low, int high) {
	if (low < high) {
		int pivotpos = partition(A, low, high);
		QuickSort(A, low, pivotpos - 1);
		QuickSort(A, pivotpos + 1, high);
	}
}

易错点

1,int pivot = low;
快速排序中枢轴接收的应该是元素值,而不是下标。如果是下标,那么在快速排序时元素会不断移动,那么原本是枢轴的位置,排序后可能被其他元素替代了,导致错误。
因此接收的是元素值,这样排序后直接将值放入最终的位置即可。

2,左右遍历时要不要移动和枢轴相等的元素。
首先移动相等元素的目的是保证稳定性。
while (low < high && A[high] > pivot)
while (low < high && A[low] < pivot)
但是快速排序是一个不稳定的算法。算法中有两个值相同的元素,原本在后面的元素,可能会和前面大于该元素的枢轴遍历时被交换的前面,导致不稳定性。因此这里加不加等于号都一样。(上机测试过)

2,TQ3,双向冒泡排序

即左边将max冒到右边,–high;然后右边将min冒到左边,++low。反复交替。直到没有冒泡,序列有序便停止。

//冒泡排序
void DoubleBubbleSort(int A[], int n) {
	int low = 0;
	int high = n - 1;
	int flag = 1;
	while (flag == 1) {
		flag = 0;
		for (int i = low; i < high; ++i) {
			if (A[i + 1] < A[i]) {
				swap(A[i], A[i + 1]);
				flag = 1;
			}
		}
		--high;
		for (int j = high; j > low; --j) {
			if (A[j - 1] > A[j])
			{
				swap(A[j - 1], A[j]);
				flag = 1;
			}
		}
		++low;
	}
}

改进点

算法以while (flag == 0)为条件循环;一开始我想着是,如果从左到右的循环没有调整元素,那么第二次从右到左是不是就多对比了一次。 也就是最多多对比了n次。(但其实也没有太大的改进,因为总体还是O( n 2 n^{2} n2))
可以添加一个条件,第一次从左到右遍历后,如果没有调整元素,说明已经有序,后面一次就没必要继续遍历。

void DoubleBubbleSort2(int A[], int n) {
	int low = 0;
	int high = n - 1;
	int flag = 1;
	while (flag == 1) {
		flag = 0;
		for (int i = low; i < high; ++i) {
			if (A[i + 1] < A[i]) {
				swap(A[i], A[i + 1]);
				flag = 1;
				
			}
		}
		--high;
		for (int j = high; j > low && flag == 0; --j) {
			if (A[j - 1] > A[j])
			{
				swap(A[j - 1], A[j]);
				flag = 1;
			}
		}
		++low;
		}
	printf("num:%d\n", num);
}

3,TQ4,复杂性分析

快排空间复杂性

快排的空间复杂度为O( l o g n log^{n} logn),每次递归只存储一些常量(常数级),则实际上的空间复杂性取决于递归层数。
快排的递归过程可以看做一个节点数为n的二叉树,每次选择处一个枢轴,然后分为左右子表,直到只剩下一个元素。则递归数就是层数-1.
最坏的情况,每层只有一个节点,则递归n层。
平均情况,每次都分为左右两个,为完全二叉树,h=⌊ l o g n log^{n} logn⌋+1;则递归层数为⌊ l o g n log^{n} logn⌋。
则平均情况下,为O( l o g n log^{n} logn)。

平均排序最快的算法

快排的时间复杂度为O(递归层数*每层遍历数)=O( n l o g n nlog^{n} nlogn)
尽管有多个算法的时间复杂度都在O( n l o g n nlog^{n} nlogn)这个数量级,但所有算法中的最高次数项X n l o g n nlog^{n} nlogn中,快速排序的常数项最小,因此在同类中表现最优。

归并排序时间复杂度O( n l o g n nlog^{n} nlogn)

1,归并趟数, n l o g n nlog^{n} nlogn
一开始以单一元素为有序序列,进行合并。合并后的再继续两两合并,直到只剩下一个有序序列。
可以看做一个二叉树,与快排不同的是,这个二叉树固定为完全二叉树(也就是不管初始序列如何,归并趟数固定),最下面为n个叶子节点,然后不断两两合并。然后树高-1就是归并趟数 n l o g n nlog^{n} nlogn

2,每趟对比元素次数
在两个有序序列确定每个元素的最终位置,因为序列已经有序了,只需要自左向右对比第一个元素的大小即可,最多也不会超过n次。

则平均,最好,最坏的时间复杂度都为O(趟数*每趟对比数)=O( n l o g n nlog^{n} nlogn)

4,TQ6,基数排序第一步

第一步要补0,如题目给了序列{1,22,100,5};第一步要将序列改为{001,022,100,055}这样。方便后续对各个为进行排序。

5,TQ8,链表实现排序

本题,直接插入排序

首先将L断开,L只保留一个节点。其余节点由p指向为待排序列。

遍历L找到第一个大于等于待排元素的节点,将待排元素插在前面,或者遍历完已排序列,则插在已排序列末尾。

注意:是对已排序列遍历。
依次处理未排序列的第一个元素,对已排序列遍历找到合适的位置将未排序列插入已排序列

其他链表排序思路总结

其他可以用链表实现的有:冒泡排序和简单选择排序
冒泡:数组是从后往前冒泡,而链表特别是单链表不方便从后向前查找,因此可以从前向后把较大的值冒到后面去。
//不管p交换与否,pre位置不变,因此通过pre指针指后移p

简单选择:和直接插入类似,也是先将L置空,p指向剩余元素,然后遍历p每次都找max头插法插入L。

直接插入每一趟是对已排序列遍历找到未排序列位置。简单选择每一趟是对未排序列处理找到MAX,直接插入已排序列后面。

题目提到创建带头结点的链表

那么答题时就应该写一个建立头结点的函数。

void CreateLink(LNode *&L,int A[],int n){
	int i;
	LNode *s,*t;
	L=(LNode*)malloc(sizeof(LNode));
	L->next=NULL;
	t=L;//头结点不能直接参与遍历
	for(i=0;i<n;++i){
		s=(LNode*)malloc(sizeof(LNode));
		s->data=A[i];
		t->next=s;
		t=s;//t每次后移,实现尾插法。
	}
	t->next=NULL;
}

四,真题

1,TQ3,快排处理左右子表顺序

快排不管是先处理左子表还是右子表,都不影响结果。

2,TQ6,大根堆插入新元素

首先将新元素插入堆底。
然后新元素只需要和父节点进行比较,若大于父节点就上浮。
循环直到比父节点小,或者到堆顶。
注意,不需要和兄弟节点对比,因为其兄弟节点原本就是大根堆内的已经小于父节点了。

3,TQ11,如何判定一个序列是不是快速排序产生的

快速排序每排一趟就有一个元素放在最终位置,使得左边所有元素小于该元素,右边所有元素大于该元素。
如果进行了n趟快速排序,则在该序列中至少能找到n个这样放在最终位置的元素。

4,TQ1,哈夫曼树与外部排序

原理

对5个初始归并段进行归并排序,要求在最坏的情况下比较的总次数最少。
可以使用哈夫曼树,以表的长度为节点的值,构建哈夫曼树。也就是依次选择最短的两个表进行归并,哈夫曼树有最小带权路径长度,这样长度短的表参与合并次数多,长度长的表合并次数少,这样总的I/O次数最小。

计算最多的比较次数

如果两个表合起来有n个元素,则最多比较n-1次,因为最后一个元素不需要进行对比,直接输出。
在这里插入图片描述
这样计算最多的对比次数,每次计算归并次数时要-1。
第一次归并 10+35-1=44
第二次归并 45+40-1=84

合计:825次

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

燕南路GISer

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值