前言
本文习题均来自25王道考研的教材课后习题,在给出答案的基础上加上自己的分析,方便大家理解学习,部分题目我列出了我本人的解法和王道书的解法,大家对比着看会更有体会。算法大题不需要找到最优算法,考试中直接使用暴力解法就可以,不要在乎时空复杂度,哪怕用最烂的算法,只要写对,并且对应的时空复杂度写对,最多扣3分,如果是次优算法,只扣1-2分。最优算法是为了学习不是为了答题!
附一些初始化顺序表的基本函数代码,以下代码声明一个顺序表,表中数据是:100到109。
#include<iostream>
using namespace std;
#define InitSize 10
//创建一个顺序表
struct SqList {
int* data;
int length;
int MaxSize = InitSize;
};
typedef SqList SqList;
//初始化表
bool InitSqList(SqList& L) {
L.data = (int*)malloc(sizeof(int) * InitSize); //申请内存空间
if (L.data == NULL) //内存分配失败
return false;
L.length = 0;
}
//向表中插入一些基本元素,用于初始化表
void InsertSqList(SqList& L) {
for (int i = 0; i < L.MaxSize; i++) {
L.data[i] = i + 100;
L.length++;
}
}
//输出表L的值。
void PrintSqList(SqList L) {
for (int i = 0; i < L.length; i++)
cout << L.data[i] << " ";
cout << endl;
}
//初始化表,插入一些基本数据并打印
void test01(SqList& L) {
InitSqList(L);
InsertSqList(L);
//PrintSqList(L);
}
int main() {
SqList L;
cout<<"输出表L内容如下:"
test01(L);
cout << "初始化完成";
return 0;
}
题目
1.从顺序表中删除最小值的元素(假设唯一)并由函数返回被删元素的值,空出的位置由最后一个元素填补,若顺序表为空,则显示出错信息并退出运行。
思路:搜索整个顺序表,查找最小值元素并记住其位置,搜索结束后用最后一个元素填补空出的原来最小值元素的位置,然后让表长 -1 。
bool Delete_Min(SqList& L, int& value) { //对表L删除操作,并用value保存删除的元素并带出函数外
if (L.length == 0) {
cout << "表为空,不合法!" << endl;
return false;
}
value = L.data[0]; //给value一个初始值
int p = 0;
for (int i = 0; i < L.length; i++) { //遍历L的所有元素
if (L.data[i] < value) {
value = L.data[i]; //及时更新value的值为当前扫描到的最小值
p = i; //p是最小元素的下标
}
}
L.data[p] = L.data[L.length - 1]; //用最后一个元素的值覆盖下标为p的元素的值
L.length--; //数组长度减一,表示删除末尾元素
return true;
}
2.设计一个高效算法,将顺序表L的所有元素逆置,要求算法的空间复杂度为O(1)。
思路:扫描顺序表 L 的前半部分元素,对于元素 L.data[i](0<-i<L.length/2),将其与后半部分的对应元素L.data[L.length-i-1] 进行交换。
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[i] = L.data[L.length - i - 1];
L.data[L.length - i - 1] = temp;
}
}
3.对长度为n的顺序表,编写一个时间复杂度为O(n)、空间复杂度为O(1)的算法,该算法删除顺序表中所有值为x的数据元素
思路:用k记录表L中元素等于x的个数,一边扫描L,一边将不等于x的元素往前移动k位,最后将表长 -k。
void Delete_X(SqList& L, int x) {
int k = 0;
for (int i = 0; i < L.length; i++) {
if (L.data[i] == x)
k++;
else if (L.data[i] != x)
L.data[i - k] = L.data[i];
}
L.length -= k;
}
4.从顺序表中删除值在给定值s和t之间(包含s和t,要求s<t)的所有元素,若s或t不合理或者顺序表为空,则显示出错信息并退出运行。
思路:从前向后扫描顺序表L,用k记录值在s和t之间的元素个数,对于扫描的元素,若其不在 [s,t] 区间中,则前移k个位置,否则k++。由于每个不符合条件的元素只移动一次,故算法效率高。
bool Delete_s_t(SqList& L, int s, int t) {
int k = 0;
if (s >= t || L.length == 0) //错误输入!
return false;
for (int i = 0; i < L.length; i++) {
if (L.data[i] >= s && L.data[i] <= t)
k++;
else
L.data[i - k] = L.data[i];
}
L.length -= k;
return true;
}
5.从有序顺序表中删除所有值重复的元素,使得表中所有元素的值均不同。
因为是有序表,所有值相同的元素一定在连续的位置上,用类似于直接插入排序的思想,初始将第一个元素视为非重复的有序表。之后依次判断后面的元素是否与前面非重复的有序表的最后一个元素相同,若相同,则继续向后判断,若不同,则插入前面的非重复有序表的末尾,直至判断到表尾为止。
bool Delete_Same(SqList& L) {
if (L.length == 0)
return false;
int i, j; //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;
}
//测试数据
void InsertSqList(SqList& L) {
//插入其他值 10 11 12 12 12 13 14 40 40 50
L.length = 10;
L.data[0] = 10;
L.data[1] = 11;
L.data[2] = 12;
L.data[3] = 12;
L.data[4] = 12;
L.data[5] = 13;
L.data[6] = 14;
L.data[7] = 40;
L.data[8] = 40;
L.data[9] = 50;
}
6.将两个有序顺序表合并为一个新的有序顺序表,并由函数返回结果顺序表
思路:把AB两个表的元素依次比较,小的存入C表,最后会剩一个表,把剩余部分加到C表后面。
bool Merge(SqList A, SqList B, SqList &C) {
if (A.length + B.length > C.length) {
cout << "AB表长大于C的表长" << endl;
return false;
}
int i = 0, j = 0, k = 0;
while (i < A.length && j < B.length) { //小的存入C表
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;
}
7.在一维数组A[m+n]中依次存放两个线性表A和B,分别有m、n个元素。编写一个函数,将数组中两个顺序表的位置互换,即将B放在A的前面。
思路:先将数组A[m+n]中的全部元素(a1,a2,a3,...,am,b1,b2,b3,...,bn)原地逆置为(bn,...,b2,b1,am,...,a2,a1),然后对前n个元素和后m个元素分别使用逆置算法,即可得到(b1,b2,b3,...,bn,a1,a2,a3,...,am)。这个和第二题的逆置算法稍有不同,不过本质是相同的。
void Reverse(int 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++) { //mid - left是要进行逆置的表长的一半
int temp = A[left + i];
A[left + i] = A[right - i];
A[right - i] = temp;
}
}
void Exchange(int 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);
}
8.线性表L(有n个元素)中的元素递增有序且按顺序存储于计算机内。要求设计一个算法,完成用最少时间在表中查找数值为x的元素,若找到,则将其与后继元素位置相交换,若找不到,则将其插入表中并使表中元素依然递增有序。
思路:顺序存储的线性表递增有序,可以顺序查找,也可以折半查找,根据题目要求需要用折半查找。
void SearchExchangeInsert(SqList& L, int x) {
//折半查找,目的是为了找到x的下标位置,用mid表示
int left = 0, right = L.length - 1;
int mid;
while (left <= right) {
mid = (left + right) / 2;
if (L.data[mid] == x)
break;
else if (L.data[mid] < x)
left = mid + 1;
else
right = mid - 1;
}
if (L.data[mid] == x && mid != L.length - 1) { //如果最后一个元素和x相等,则不存在与其后继的交换操作。
int temp = L.data[mid];
L.data[mid] = L.data[mid + 1];
L.data[mid + 1] = temp;
}
if (left > right) { //如果没找到x
int i;
for (i = L.length - 1; i > right; i--)
L.data[i + 1] = L.data[i];
L.data[i + 1] = x; //插入x
}
}
9.给定三个序列A、B、C,长度均为n,且均为无重复元素的递增序列,请设计一个时间上尽可能高效的算法,朱行输出同时存在于这三个序列中的所有元素。
思路:用三个工作指针从小到大遍历三个数组,当三个下标变量指向的元素相等时,输出并向前推进指针,否则仅移动小于最大元素的下标变量,直到某个下标变量移出数组范围,即可停止。
每个指针移动次数不超过n次,且每次循环至少有一个指针后移,所以时间复杂度为O(n),算法只用到了常数个变量,空间复杂度为O(1)。
//自己写的:
void SameKey(SqList A, SqList B, SqList C) {
int i = 0, j = 0, k = 0; //声明三个指针分别指向三个数组
while (i < A.length && j < B.length && k < C.length) { //当三个数组都没有遍历完的时候
if (A.data[i] == B.data[j] && A.data[i] == C.data[k]) { //如果满足条件,则打印,并且指针向后移一位
cout << A.data[i] << " ";
i++,j++,k++;
}
else {
if (A.data[i] <= B.data[j] && A.data[i] <= C.data[k]) //如果A的元素最小,i++
i++;
else if (B.data[j] <= A.data[i] && B.data[j] <= C.data[k])//如果B的元素最小,j++
j++;
else if (C.data[k] <= A.data[i] && C.data[k] <= B.data[j])//如果C的元素最小,k++
k++;
}
}
}
//王道书给的,是对数组操作的,本质一样,用了max函数,更简洁了
void SameKey(int A[], int B[], int C[], int n) {
int i = 0, j = 0, k = 0;
while (i < n && j < n && k < n) {
if (A[i] = B[j] && B[j] == C[k]) {
printf("%d\n", A[i]);
i++, j++, k++;
}
else {
int maxNum = max(A[i], max(B[j], C[k]));
if (A[i] < maxNum)
i++;
if (B[j] < maxNum)
j++;
if (C[k] < maxNum)
k++;
}
}
}
//一些测试数据,调用的是我自己写的那个函数,是对顺序表操作的
bool InitSqList(SqList& L) {
L.data = (int*)malloc(sizeof(int) * InitSize); //申请内存空间
if (L.data == NULL) //内存分配失败
return false;
L.length = 0;
}
int main() {
SqList A,B,C;
InitSqList(A);
InitSqList(B);
InitSqList(C);
A.length = 3;
B.length = 3;
C.length = 3;
A.data[0] = 1;
A.data[1] = 4;
A.data[2] = 5;
B.data[0] = 2;
B.data[1] = 4;
B.data[2] = 4;
C.data[0] = -1;
C.data[1] = 3;
C.data[2] = 4;
SameKey_me(A, B, C);
return 0;
}
10.设将n(n>1)个整数存放到一维数组R中,设计一个在时间和空间两方面都尽可能高效的算法。将R中保存的序列循环左移p个位置,即将R中的数据由(X0,X1,...,Xn-1)变换为(Xp,Xp+1,...,Xn-1,X0,X1,...,Xp-1)
思路:将问题视为把数组ab转换成数组ba(a代表数组的前p个元素,b代表数组的后n-p个元素),先将a逆置得到“a逆b”,再将b逆置得到“a逆b逆”,最后将整个“a逆b逆”逆置得到ba。用Reverse函数执行逆置操作,可以写出如下代码。
算法中三个Reverse函数的时间复杂度分别为 O(p/2)、O((n-p)/2)和O(n/2),故所设计的算法的时间复杂度为O(n),空间复杂度为O(1)。
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);
}
11.一个长度为L的升序序列S,处在中间的元素是S的中位数,若S为偶数,则处在中间的后一位元素为S的中位数。例如,若S1=(11,13,15,17,19),则S1的中位数是15;两个序列的中位数是他们所有元素的升序序列的中位数,例如,若S2=(2,4,6,8,20),则S1和S2的中位数是11.现在有两个等长升序序列A和B,试设计一个在时间和空间两方面都尽可能高效的算法,找出两个序列A和B的中位数。
思路:分别求两个升序序列A、B的中位数,设为a和b,求序列A、B的中位数过程如下:
-
若a=b,则a或b即为所求的中位数,算法结束;
-
若a<b,则舍弃序列A中较小的一半,同时舍弃序列B中较大的一半,要求两次舍弃的长度相等;
-
若a>b,则舍弃序列A中较大的一半,同时舍弃序列B中较小的一半,要求两次舍弃的长度相等。
在保留的两个升序序列中,重复过程123,直到两个序列中均只含一个元素时为止,较小者即为所求的中位数。
int M_Search(int A[], int B[], int n) {
int s1, d1, m1, s2, d2, m2;
s1 = 0, d1 = n - 1;
s2 = 1, d2 = n - 1;
while (s1 != d1 || s2 != d2) {
m1 = (s1 + d1) / 2;
m2 = (s2 + d2) / 2;
if (A[m1] == B[m2]) //满足条件 1
return A[m1];
if (A[m1] < B[m2]) { //满足条件 2
if ((s1 + d1) % 2 == 0) { //若元素个数为奇数
s1 = m1; //舍弃A中间点以前的部分,且保留中间点
d2 = m2; //舍弃B中间点以后的部分,且保留中间点
}
else { //若元素个数为偶数
s1 = m1 + 1; //舍弃A的前半部分
d2 = m2; //舍弃B的后半部分
}
}
else { //满足条件 3
if ((s1 + d1) % 2 == 0) { //若元素个数为奇数
d1 = m1; //舍弃A中间点以后的部分,且保留中间点
s2 = m2; //舍弃B中间点以前的部分,且保留中间点
}
else { //元素个数为偶数
d1 = m1; //舍弃A的后半部分
s2 = m2 + 1; //舍弃B的前半部分
}
}
}
return A[s1] < B[s2] ? A[s1] : B[s2];
}
以上是答案中的算法,是最优算法,时间复杂度为O(log2n),空间复杂度为O(1)。很棒,但不适用于考试,因为你想不到,也写不全。于是有了次优算法:归并排序的思想。
我们只需要找到两个数组合并起来之后的第“N/2”的下标位置的元素,这就是我们要找的合并数组的中位数。所以用两个指针 i,j,分别指向A,B两个数组,依次比较对应元素大小,把小的那个的指针往后移,当比较了 n 次之后, i 和 j 指向的那个较小的元素就是要找的中位数。
void M_Search(SqList A, SqList B) {
int n = A.length; //n为单个表长
int i = 0, j = 0;
while(i + j != n - 1){ //注意这里,数组下标从0开始,表长应该减一,否则会溢出
if (A.data[i] <= B.data[j])
i++; //小指针往后移
else if (A.data[i] > B.data[j])
j++; //小指针往后移
}
if (A.data[i] <= B.data[j])
cout << "A和B的中位数为:" << A.data[i];
else if (A.data[i] > B.data[j])
cout << "A和B的中位数为:" << B.data[j];
}
代码解析
-
两个表共有 2n 个元素,需要比较 n 个元素,第 n 个元素即为我们要找的中位数;
-
每次比较之后指针指向的较小的那个元素的指针往后移;
-
判定循环结束条件为 i+j!=n-1 ,注意是 n-1 不是 n ,因为表长为 n 的话数组下标最大是 n-1 ,体会一下;
-
最后输出结果是两个指针中指向的较小的那个元素。