21考研复习之《数据结构》——《线性表》王道课本19页代码题目

所有题目会显示题目信息,并一一作答,然后列出课本上所给出的标准答案。
自己写的代码和课本比还有很大差距,我觉得要放弃自己的一套方法,着重记忆一下王道课本的代码。
每道题都尝试找到在leetcode的原题,可以练习提交一下。

1、从顺序表中删除具有最小值的元素(假设唯一)并由函数返回被删除的元素的值。空出的位置由最后一个元素填补,若顺序表为空则显示出错信息并退出运行。

需要考虑到的特殊情况除了题目提示的顺序表为空,还需要注意到最小值元素为顺序表最后一个元素的情况,自己替代自己。分析发现,通过L.length–,这种特殊情况不受影响。

ElemType displace_min_by_last(seq_list &L){
	int len = L.length; //得到顺序表长度
	if(len == 0){ // 顺序表为空
		return -1; //就是一个标志,表示出错
	}
	ElemType min_number=L.data[0]; //最小的数
	int min_index=0; //最小的数的索引
	for(int i=1;i<len;i++){
		if(L.data[i]<min_number){
			min_number = L.data[i];
			min_index = i;
		}
	}
	ElemType e = L.data[min_index];
	L.data[min_index] = L.data[len-1];
	L.length--; // 顺序表长度减1
	return e;
} 

/*王道课本*/
bool Del_Min(sqList &L, ElemType &value){
// 删除顺序表L中最小值元素节点,并通过引用型参数value返回其值
	if(L.length==0)     //表空
		return false;
	value = L.data[0];
	int pos=0;
	for(int i=1;i<L.length;i++)
		if(L.data[i]<value){
			value = L.data[i];
			pos = i;
		}	
	L.data[pos] = L.data[L.length-1];
	L.length--;
	return true;	
}

2、设计一个高效算法,将顺序表L的所有元素逆置,要求算法的空间复杂度为O(1)

空间复杂度要求为O(1),常数个内存空间。采用首位对调,时间复杂度O(n),实际执行T=O(n/2),只需要一个交换单元即可。

void seq_list_reverse(seq_list &L){
	int len = L.length;
	int i=0, j=len-1;
	ElemType t;
	while(i<j){
		// 每次实现一次交换
		t = L.data[j];
		L.data[j] = L.data[i];
		L.data[i] = t;
		i++;
		j--;
	}
}

考虑对算法的测试:
找出所有可能出现的不同情况
1、len=0,i=0, j=-1,while循环没有执行,结束
2、len=1,i=0, j=0,不需要交换,while不执行,结束
3、len为大于1的奇数,假如为5,那么执行过程为 1 5交换,2 4交换,3 3结束,结果正确。
4、len为大于1的偶数,假如为6,那么执行过程为 1 6交换,2 5交换,3 4交换,4 3结束,结果正确。指代的是位序


/*王道课本*/
void Reverse(Sqlist &L){
	ElemType temp;
	for(int i=0;i<L.length/2;i++){
		temp = L.data[i];
		L.data[i] = L.data[L.length-i-1];
		L.data[L.length-i-1] = temp;
	}
}

3、对长度为n的顺序表L,编写一个时间复杂度为O(n)、空间复杂度为O(1)的算法,该算法删除线性表中所有值为x的数据元素。

一遍扫描完成删除和紧凑
测试用例:
1 3 2 3 5 3 2

void delete_all_x(seq_list &L, ElemType x){
	int len = L.length;
	int i=0, empty=-1; // empty始终表示删除x后的最前方的位置
	int num=0; //统计删除的元素的数量 
	while(i<len){
		if(L.data[i]==x){
			if(empty==-1){ //记录第一个x的位置索引
				empty=x;
			}
			num++;
		}
		else if(empty!=-1){ //当前元素不为x,并且前面删除了某个x,当前非x元素前移
			L.data[empty] = L.data[i]
			empty++;
		}
		i++;
	}
	L.length -= num;
}

解法一:
用k记录顺序表L中不等于x的元素个数(即需要保存的元素个数),边扫描L边统计k,并将不等于x的元素向前移动k个位置,最后修改L的长度

/*王道课本*/
void del_x_1(Sqlist &L, ElemType x){
// 本算法实现删除顺序表L中所有值为x的数据元素
// 时间复杂度O(n),一遍扫描
	int k=0;
	for(int i=0;i<L.length;i++)
		if(L.data[i]!=x){
			L.data[k] = L.data[i];
			k++;
		}
	L.length = k;	
}

解法二:
用k记录顺序表L中等于x的元素个数,边扫描L边统计k,并将不等于x的元素前移k个位置,最后修改L的长度。
(我自己写的算法和这个思想一致,但是没有这个版本简洁,学习了。)

/*王道课本*/
void del_x_2(Sqlist &L, ElemType x){
	int k=0,i=0;
	while(i<L.length){
		if(L.data[i]==x)
			k++;
		else
			L.data[i-k] = L.data[i]; // 当前元素前移K个位置
		i++;	
	}
	L.length = L.length - k;
}

4、从有序顺序表中删除其值在给定值s与t之间(要求s

这里提示了顺序表是有序的,想要利用有序的特性的话,分别从两边遍历,left表示第一个大于s的索引,right表示小于s的第一个索引,然后直接进行移位操作,平均时间复杂度和最坏复杂度依旧是O(n),即使使用二分查找logN复杂度确定left和right,但是元素的移动操作依旧确定了时间复杂度为O(n)。
通过以上分析,可以用朴素的算法,一遍遍历,这种算法也使用与顺序表不是有序的。

bool delete_range_s_t(seq_list &L, ElemType s, ElemType t){
	if(s>=t) // 给定的区间不合理
		return false;
	int len = L.length;
	if(len==0) //顺序表为空
		return false;
	int left=-1; //标记左边的将要被填充的位置
	int num=0;
	for(int i=0;i<len;i++){
		if(left==-1 && L.data[i]>s){
			left = i; //最左边的元素
		}
		if(L.data[i]>s && L.data[i]<t)
			num++;
		else if(L.data[i]>=t){ //等于t不删除
			L.data[left] = L.data[i];
			left++; //填充后向后移动
		} 
	}
	L.length -= num; //更新顺序表的长度
	return true;		
}

课本答案:
先寻找大于等于s的第一个元素(第一个删除的元素),然后寻找大于t的第一个元素(最后一个删除的元素的下一个元素),要将这段元素删除,只需将后面的元素前移。

/*王道课本*/
bool Del_s_t2(Sqlist &L, ElemType s, ElemType t){
// 删除有序顺序表L中值在给定s与t之间的所有元素
	int i,j;
	if(s>=t||L.length==0)
		return false;
	for(i=0;i<L.length&&L.data[i]<s;i++); // 寻找大于等于s的第一个元素
	if(i>=L.length)
		return false;
	for(j=i;j<L.length&&L.data[j]<=t;j++); //寻找大于t的第一个元素
	for(;j<L.length;i++,j++)
		L.data[i] = L.data[j];
	L.length = i;
	return true;			
}

有序和无序感觉算法的效率上好像没有差别。

5、从顺序表中删除其值在给定s与t之间(包含s和t,要求s

题目5包含s和t,不是有序的,第4题的算法仍然适用。

bool delete_range_s_t(seq_list &L, ElemType s, ElemType t){
	if(s>=t) // 给定的区间不合理
		return false;
	int len = L.length;
	if(len==0) //顺序表为空
		return false;
	int left=-1; //标记左边的将要被填充的位置
	int num=0; //统计被删除的元素的数量
	for(int i=0;i<len;i++){
		if(left==-1 && L.data[i]>=s){
			left = i; //最左边的元素
		}
		if(L.data[i]>=s && L.data[i]<=t)
			num++;
		else if(L.data[i]>t){ // 等于t也要被删除
			L.data[left] = L.data[i];
			left++; //填充后向后移动
		}
	}
	L.length -= num; //更新顺序表的长度
	return true;		
}

从前向后扫描顺序表L,用k记录下元素值在s到t之间元素的个数(初始时k=0)。对于当前扫描的元素,若其值不在s和t之间,则前移k个位置;否则执行k++。由于这样每个不在s和t之间的元素仅移动一次,所有算法效率高。

/*王道课本*/
bool Del_s_t(Sqlist &L, ElemType s, ElemType t){
//删除顺序表L中值在给定值s与t之间(要求s
	int i,k=0;
	if(L.length==0||s>=t)
		return false; //线性表为空或s、t不合法,返回
	for(i=0;i<L.length;i++){
		if(L.data[i]>=s && L.data[i]<=t)
			k++;
		else
			L.data[i-k]=L.data[i]; //当前元素前移k个位置	
	}	
 	L.length -= k; // 长度减小k
 	return true;
}

课本上写的代码真的好,思想简单,代买清晰。

6、从有序顺序表中删除所有其值重复的元素,使表中所有元素的值均不同。

最直白朴素的算法是使用一个哈希表map,遍历一遍数组,标记每个元素是否在顺序表中已经出现,时间复杂度和空间复杂度都为O(n)。
原地算法的话,每次遍历到某个元素,和前面的元素逐个比较,确定是否重复,时间复杂度O(n2)。
尝试寻找一种算法,时间复杂度为O(n)的原地排序为效率最高的算法。

ps:审题失败,题目给的有序顺序表我给漏条件了

void unique(seq_list &L){
	int len = L.length;
	
}

课本答案:
注意是有序顺序表,值相同的元素一定在连续的位置上,用类似于直接插入排序的思想,初始时将第一个元素视为非重复的有序表。之后依次判断后面的元素是否与前面非重复有序表的最后一个元素相同,若相同则继续向后判断,若不相同则插入到前面的非重复有序表的最后,直到判断到表尾为止。

/*王道课本*/
bool Delete_Same(SeqList &L){
	if(L.length==0)
		return false;
	int i,j;
	for(i=0,j=1;j<L.length;j++)
		if(L.data[i]!=L.data[j]) //查找下一个与上个元素值不同的元素
			L.data[++i] = L.data[j]; //找到后将元素前移
		L.length = i+1;
		return true;		
}

7、将两个有序顺序表合并为一个新的有序顺序表,并由函数返回结果顺序表。

之后将要学习的归并排序的归并的方法
时间复杂度O(m+n)

bool merge_orded_seq_list(seq_list &A, seq_list &B, seq_list &L){
	// 首先判断A和B的长度是否大于L的容量
	if(A.length+B.length>L.max_size)
		return false;
	// 函数中不作对L的清空操作	
	int i=0,j=0,k=0;
	int len_A = A.length, len_B=B.length;
	while(i<len_A && j<len_B){
		if(A.data[i]<B.data[j]){
			L.data[k++] = A.data[i++];
		}
		else{
			L.data[k++] = B.data[j++];
		}
	}
	if(i==len_A){ // A已经合并完毕
		while(j<len_B){
			L.data[k++] = B.data[j++];
		}
	}
	else{ // B已经合并完毕
		while(i<len_A){
			L.data[k++] = A.data[i++];
		}
	}
	L.length = k; //最后不要忘了更新L的length值
}


首先,按顺序不断取下两个顺序表表头较小的结点存入新的顺序表中。然后,看哪个表还有剩余,将剩下的部分加到新的顺序表后面。

/*王道课本*/
bool Merge(SeqList A, SeqList B, SeqList &C){
	//将有序顺序表A与B合并为一个新的有序顺序表C
	if(A.length+B.length>C.maxSize) //大于顺序表的最大长度
		return false;
	int i=0,j=0,k=0;
	while(i<A.length&&j<B.length){ //循环,两两比较,小者存入结果表
		if(A.data[i]<=B.data[j])
			C.data[k++] = A.data[i++];
		else
			C.data[k++] = B.data[j++];	
	}
	//还剩一个没有比较完整的顺序表
	while(i<A.length)
		C.data[k++] = A.data[i++];
	while(j<B.length)
		C.data[k++] = B.data[j++];
	C.length = k;
	return true;			
}

其实这个算法就是后面学的二分的归并排序的归并的过程。
看了课本的标准答案,最后的if判断好像确实可以省略,学到了。

8、已知在一维数组A[m+n]中依次存放两个线性表(a1,a2,a3,…,am)和(b1,b2,b3,…,bn)。试编写一个函数,将数组中两个顺序表的位置互换,即将(b1,b2,b3,…,bn)放在(a1,a2,a3,…,am)的前面。

(我记得数据结构课堂上老师让做过这道题,时间复杂度O(n),递归调用的方式)

朴素思想,开辟一个max(m, n),复制一下a或者b,然后一顿操作,…
如果m=n的话那就好办了,相应位置直接交换即可,仔细想想,这个方法成了递归调用,在这里我还是用上面的朴素实现一下吧。

void displace_m_n(ElemType A[], int m, int n){
	int i=0,j=m,k=0;
	int size = m>n?m:n;
	ElemType t[size]; //开辟内存空间,用来暂存数据
	if(size==m){
		for(i=0;i<m;i++) //复制数据到t
			t[k++] = A[i];
			
		i=0,j=m;
		while(i<n)    // b移动到元素前面
			A[i++]=A[j++];
			
		i=n,j=0;	
		while(j<m)  // 从t中取数据移动到后面m个空间
			A[i++]=t[j++];	
	}
	else{ // size == n
		for(j=m;j<m+n;j++)
			t[k++] = A[j];
		
		i=n,j=0;	
		while(j<m)
			A[i++]=A[j++];
		i=m,j=0;
		while(j<n)
			A[i++]=t[j++];		
	}				
}

这道题很有技术含量,其实我碰到了不止两次了,但是没记住,基础不牢,地动山摇。

/*王道课本*/
typedef int DataType;
void Reverse(DataType A[], int left, int right, int arraySize){
	if(left>=right || right>=arraySize) //参数不合法
		return;
	int mid=(left+right)/2;
	for(int i=0;i<=mid-left;i++){
		DataType temp = A[left+i];
		A[left+i] = A[right-i];
		A[right-i] = temp;
	}	
}

void Exchange(DataType A[], int m, int n, int arraySize){
	Reverse(A, 0, m+n-1, arraySize);
	Reverse(A, 0, n-1, arraySize);
	Reverse(A, n, m+n-1, arraySize);
}

9、线性表(a1,a2,a3,…,am)中的元素递增有序且按顺序存储于计算机内。要求设计一算法,完成用最少时间在表中查找数值为x的元素,若找到则将其与后继元素位置相交换,若找不到则将其插入表中并使表中元素仍递增有序。

最少的时间查找x,使用二分查找,时间复杂度O(log n)

bool find_elem(seq_list &L, ElemType x){
	int l=0,r=L.length-1,k;
	int mid;
	while(l<=r){
		mid = (l+r)/2;
		if(x==L.data[mid]){
			if(mid+1<L.length){ //操作之前需要确保后继元素存在
				ElemType t=L.data[mid+1];
				L.data[mid+1]=L.data[mid];
				L.data[mid]=t;
				return true; //程序退出
			}
		}
		if(x>L.data[mid])
			l=mid+1;
		else
			r=mid-1;	
	}
	if(L.length==L.max_size)
		return false;
	// 元素后移要确定x插入的位置
	
	r=L.length-1; 
	while(r>l){ //元素后移
		L.data[r+1] = L.data[r];
		r--;
	}
	L.data[l] = x;
	L.length++;
	return true;
}

折半查找

/*王道课本*/
void SearchExchangeInsert(ElemType A[], ElemType x, int n){
	int low=0,high=n-1,mid; //low和high指向顺序表下界和上届的下标
	while(low<=high){
		mid = (low+high)/2;
		if(A[mid]==x)
			break;
		else if(A[mid]<x)
			low = mid+1;
		else
			high = mid-1;		
	}
	if(A[mid]==x&&mid!=n-1){
		//若最后一个元素与x相等,则不存在与其后继交换的操作
		t = A[mid];
		A[mid] = A[mid+1];
		A[mid+1] = t;
	}
	if(low>high){ //查找失败,插入数据元素x
		for(i=n-1;i>high;i--)
			A[i+1] = A[i];
		A[i+1] = x;
	}
}

10、设将n(n>1)个整数存放到一维数组R中。设计一个在时间和空间两方面都尽可能高效的算法。将R中保存的序列循环左移p(00,X1,…,XN-1)变换为(Xp,XP+1,…,XN-1,X0,X1,…Xp-1)。要求:

1)给出算法基本设计思想。
2)根据设计思想,采用C或C++或Java语言描述算法,关键之处给出注释。
3)说明你所设计算法的时间复杂度和空间复杂度。


解:
这一道题和第8题是一样的吧
1)算法的基本设计思想:可将这个问题视为把数组ab转换成数组ba(a代表数组的前p个元素,b代表数组中余下的n-p个元素),先将a逆置得到a-1b,再将b逆置得到a-1b-1,最后将整个a-1b-1逆置得到ba。设Reverse函数执行将数组元素逆置的操作,对abcdefgh向左循环移动3个位置的过程如下:

Reverse(0, p-1)  得到cbadefgh
Reverse(p,n-1)   得到cbahgfed
Reverse(0,n-1)   得到defghabc

2)使用C语言描述算法如下:

/*王道课本*/
void Reverse(int R[], int from, int to){
	int i,temp;
	for(i=0;i<(to-from+1)/2;i++){
		temp = R[from+i];
		R[from+i] = R[to-i];
		R[to-i] = temp;
	}
}
void Converse(int R[], int n, int p){
	Reverse(R, 0, p-1);
	Reverse(R, p, n-1);
	Reverse(R, 0, n-1);
}

3)上诉算法中三个Reverse函数的时间复杂度分别为O(p/2)、O((n-p)/2)和O(n/2),故所设计的算法的时间复杂度为O(n),空间复杂度为O(1)。

11、一个长度为L(L>=1)的升序序列S,处在第(L+1)/2个位置的数称为S的中位数。例如,若序列S1=(11, 13, 15, 17, 19),则S1的中位数是15,两个序列的中位数是含它们所有元素的升序序列的中位数。例如S2=(2, 4, 6, 8, 20),则S1和S2的中位数是11。现在又两个等长升序序列A和B,试设计一个在时间和空间两方面都尽可能高效的算法,找出两个序列A和B的中位数,要求:

1)给出算法的基本设计思想。
对于给定的两个长度均为L的升序序列A和B,分别直接找出A和B的中位数为a和b,比较a和b的大小:
不妨设a>b,那么a在整个序列A和B中,至少大于A的一半数(序列A中在A左边的数),大于B的一半数(大于b,并且大于b左边的数),可以计算得数量为N=(L-1)/2+(L-1)/2+1:
L为奇数,N=L
L为偶数,N=L-1
根据这个特性可以确定在中位数不可能在:
1、序列A中a右边的数,
2、序列B中b左边的数。
每次计算缩小范围一半,迭代直到计算结果。
2)根据设计思想,采用C或C++或Java语言描述算法,给出关键之处的注释。

3)说明你所设计的算法的时间复杂度和空间复杂度。


1)分别求两个升序序列A、B的中位数,设为a和b,求序列A和B的中位数过程如下:

  • 若a=b,则a或b即为所求中位数,算法结束
  • 若a
  • 若a>b,则舍弃序列A中较大的一半,同时舍弃序列B中较小的一半,要求两次舍弃的长度相等。
    在保留的两个升序序列中,重复上述过程,直到两个序列中均只含有一个元素为止,较小者即为所求的中位数。
    2)本题代码如下:
int M_Search(int A[], int B[], int n){
	int s1=0,d1=n-1,m1,s2=0,d2=n-1,m2;
	while(s1!=d1||s2!=d2){
		m1 = (s1+d1)/2;
		m2 = (s2+d2)/2;
		if(A[m1]==B[m2])
			return A[m1];
		if(A[m1]<B[m2]){
			if((s1+d1)%2==0){ //元素个数为奇数
				s1 = m1;
				d2 = m2;
			}
			else{
				s1 = m1+1;
				d2 = m2;
			}
		}
		else{
			if((s2+d2)%2==0){
				d1 = m1;
				s2 = m2;
			}
			else{
				d1 = m1;
				s2 = m2+1;
			}
		}			
	}
	return A[s1]<B[s2]?A[s1]:B[s2];
}

3)算法时间复杂度为O(log2n),空间复杂度为O(1)

12、已知一个整数序列A=(a0,a1,…,an-1),其中0<=aip1=ap2=…=apm=x且m>n/2(0<=pk

1)给出算法的基本设计思想。
算法的策略试从前向后扫描数组元素,标记出一个可能成为主元素的元素Num。然后重新计数,确认Num是否是主元素。
算法可以分为以下两步:
(1)选取候选主元素。依次扫描所给数组中的每个整数,将第一个遇到的整数Num保存到c中,记录Num出现的次数为1;若遇到下一个整数仍等于Num,则计数加1,否则计数减1;当计数为0时,将遇到的下一个整数保存到c中,计数重新记为1,开始新一轮计数,即从当前位置开始重复上述过程,直到扫描完全部数组元素。
(2)判断c中元素是否是真正的主元素。再次扫描该数组,统计c中元素出现的次数,若大于n/2,则为主元素;否则序列中不存在主元素。
2)根据设计思想,采用C或者C++或Java语言描述算法,关键之处给出注释。

/*王道课本(稍有改动)*/
int Majority(int A[], int n){
	int i,c,count=0;      // c用来保存候选主元素,count用来计数
	for(i=0;i<n;i++){
		if(count==0){ 
			c = A[i];   // 设置候选主元素
			count = 1;
		}
		else if(c==A[i])
			count++;
		else
			count--;
	}
	if(count==0)
		return -1;
	for(i=count=0;i<n;i++)
		if(c==A[i])
			count++;
	return count>n/2?c:-1;			
}

3)说明你所设计算法的时间复杂度和空间复杂度。
两遍扫描时间复杂度O(n),空间复杂度O(1)。

13、给定一个含n(n>=1)个整数的数组,请设计一个在时间上尽可能高效的算法,找出数组中未出现的最小正整数。例如,数组{-5,3,2,3}中未出现的最小正整数是1;数组{1,2,3}中未出现的最小正整数是4。要求:

1)给出算法的基本设计思想。
使用一个哈希表map,key和value分别为正整数和是否出现过。
遍历一遍数组,使用一个序号c初始化为1,给遍历到的正整数添加到map中,判断序号c是否出现在map中,如果true,c++,否则,继续遍历数组。
最后返回c。
2)根据设计思想,采用C或C++语言描述算法,关键之处给出注释。

int search_min_Z(int A[], int n){
	map<int, bool> m;
	int c=1;
	for(int i=0;i<n;i++){
		if(A[i]==c||m[A[i]]) // c存在
			c++;
		else
			m[A[i]] = true;	// 将A[i]标记为已经存在
	}
	return c;
}

3)说明你所设计的算法的时间复杂度和空间复杂度。
时间复杂度和空间复杂度都是O(n)


1)要求在时间上尽可能高效,因此采用空间换时间的方法。分配一个用与标记的数组B[n],用来标记A中是否出现了1~n中的正整数,B[0]对应正整数1,B[n-1]对应于正整数n,初始化B中全部为0。由于A中含有n个整数,因此可能的返回值是1~n+1,当A中n个数恰好为1~n时返回n+1。当数组A中出现了小于等于0或者大于n的值时,会导致1~n中出现空余位置,返回结果必然在1~n中,因此对于A中出现了小于n或者大于n的值可以不采取任何操作。
经过以上分析,可以得出算法的流程:从A[0]开始遍历A,若0 2)算法实现:

/*王道课本*/
int findMissMin(int A[], int n){
	int i, *B;
	B = (int*)malloc(sizeof(int)*n); // 分配空间
	memset(B,0,sizeof(int)*n);     //赋初值为0
	for(i=0;i<n;i++)
		if(A[i]>0&&A[i]<=n)
			B[A[i]-1] = 1;
	for(i=0;i<n;i++){
		if(B[i]==0)
			break;	
	}		
	return i+1; //返回结果
}

我觉得两个for循环可以合并为一个,并且B不必动态分配,没有写free。静态分配,因为是函数在栈区,函数释放内存释放。
3)时间复杂度:
时间复杂度和空间复杂度均为O(n)

你可能感兴趣的:(考研复习)