线性表的顺序表示
(今天粉丝一千啦~~~好开心!自己要加油!)
2.2.1 顺序表的定义
线性表的顺序存储又称顺序表。它是用一组地址连续的存储单元依次存储线性表中的数据元素,从而使得逻辑上相邻的两个元素在物理上也相邻。第1个元素存储在线性表的起始位置,第i个元素的存储位置后面紧接着存储的是第i+1
个元素,因此顺序表的特点是表中元素逻辑顺序与其物理顺序相同。
假设线性表L存储的其实位置为LOC(A),sizeof(ElemType)是每个数据元素所占用存储空间的大小,则表L所对应的顺序存储如下表所示。
数组下标 | 顺序表 | 内存地址 |
0 | $a_1$ | LOC(A) |
1 | $a_2$ | LOC(A) + sizeof(ElemType) |
... | ||
i-1 | $a_i$ | LOC(A) + (i-1) $\times$ sizeof(ElemType) |
... | ||
n-1 | $a_n$ | LOC(A) + (n-1) $\times$ sizeof(ElemType) |
... | ||
MaxSize-1 | ... | LOC(A) + (MaxSize-1) $\times$ sizeof(ElemType) |
假定线性表的元素类型为ElemType,则线性表的顺序存储类型描述为:
#define MaxSize 50 //定义线性表的最大长度
typedef struct{
ElemType data[MaxSize]; //顺序表的元素
int length; //顺序表的当前长度
}SqList; //顺序表的类型定义
一维数组可以是静态分配的,也可以是动态分配的。在静态分配时,由于数组的大小和空间事先已经固定,一旦空间占满再加入新的数据将会产生溢出,进而导致程序崩溃。
而在动态分配时,存储数组的空间是在程序执行过程中通过动态存储分配语句分配的。一旦数据空间占满,就另外开辟一块更大的存储空间,用以替换原来的存储空间,从而达到扩充存储数组空间的目的,而不需要为线性表一次性地划分所有空间。
#define InitSize 100 //表长度的初始定义
typedef struct{
ElemType *data; //指示动态分配数组的指针
int MaxSize, length; //数组的最大容量和当前个数
}SeqList; //动态分配数组顺序表的类型定义
C的初始动态分配语句为:
L.data = (ElemType*)malloc(sizeof(ElemType)*InitSize);
C++的初始动态分配语句为:
L.data = new ElemType[InitSize]
注意:动态分配并不是链式存储,它同样属于顺序存储结构,物理结构没有变化,依然是随机存储方式,只是分配的空间大小可以在运行时决定。
- 顺序表的最主要特点是随机访问,即通过首地址和元素符号可以在时间O(1)内找到指定元素。
- 顺序表的存储密度高,每个节点只存储数据元素。
- 顺序表逻辑上相邻的元素物理上也相邻,所以插入和删除操作需要移动大量元素。
2.2.2 顺序表上的基本操作的实现
这里仅给出顺序表的插入操作、删除操作和按值查找的算法,其他操作的算法相对比较简单。
- 插入操作
在顺序表L的第i个位置插入新元素e,若i的输入不合法,则返回false,表示插入失败,否则将顺序表的第i个元素及其后的所有元素右移1个位置,腾出一个空位置插入新元素e,顺序表长度增加1,插入成功,返回true。
bool ListInsert(SqList &L, int i, ElemType e){
// 本算法将元素e插入到顺序表L中的第i个位置。
if(i<1 || i>L.length +1 ){ //判断i的范围是否有效
return false;
}
if(L.length >= MaxSize){ //当前存储空间已满,不能插入
return false;
}
for (int j=L.length; j>=i; j--){ //将第i个元素及之后的元素后移
L.data[j] = L.data[j-1];
}
L.data[i-1] = e; //在位置i处放入e
L.length++; //线性表长度加1
return true;
}
最好情况:在表尾插入,元素后移语句将不执行,时间复杂度为O(1)。
最坏情况:在表头插入,元素后移语句将执行n次,时间复杂度为O(n)。
平均情况:假设
p
i
(
p
i
=
1
n
+
1
)
p_i(p_i =\frac{1}{n+1})
pi(pi=n+11)是在第i个位置上插入一个节点的概率,则在长度为n的线性表中插入一个结点时,所需移动节点的平均次数为:
∑ i = 1 n + 1 p i ( n − i + 1 ) = ∑ i = 1 n + 1 1 n + 1 ( n − i + 1 ) = 1 n + 1 ∑ i = 1 n + 1 ( n − i + 1 ) = 1 n + 1 ⋅ n ( n + 1 ) 2 = n 2 \sum_{i=1}^{n+1}p_i(n-i+1)=\sum_{i=1}^{n+1}\frac{1}{n+1}(n-i+1)=\frac{1}{n+1}\sum_{i=1}^{n+1}(n-i+1)=\frac{1}{n+1}\cdot\frac{n(n+1)}{2}=\frac{n}{2} i=1∑n+1pi(n−i+1)=i=1∑n+1n+11(n−i+1)=n+11i=1∑n+1(n−i+1)=n+11⋅2n(n+1)=2n
因此,线性表插入算法的平均时间复杂度为O(n)。
- 删除操作
删除顺序表L中第i个位置的元素,若成功则返回true,并将被删除的元素用引用变量e返回,否则返回false。
bool ListDelect(SqList &L, int i, ElemType &e){
//本算法实现删除顺序表L中的第i个位置的元素。
if(i<1 || i>L.length +1 ){ //判断i的范围是否有效
return false;
}
e = L.data[i-1]; //将被删除的元素赋值给
for (int j=i; j<L.length; j++){ //当地i个位置后的元素前移
L.data[j-1] = L.data[j]; //线性表长度减1
}
L.length++;
return true;
}
最好情况:在表尾删除,元素前移语句将不执行,时间复杂度为O(1)。
最坏情况:在表头插入,需要移动除第1个元素外的所有元素,时间复杂度为O(n)。
平均情况:假设
p
i
(
p
i
=
1
n
+
1
)
p_i(p_i = \frac{1}{n+1})
pi(pi=n+11)是在第i个位置上删除一个节点的概率,则在长度为n的线性表中删除一个结点时,所需移动节点的平均次数为:
∑ i = 1 n p i ( n − i ) = ∑ i = 1 n 1 n ( n − i ) = 1 n ∑ i = 1 n ( n − i ) = 1 n ⋅ n ( n + 1 ) 2 = n − 1 2 \sum_{i=1}^{n}p_i(n-i)=\sum_{i=1}^{n}\frac{1}{n}(n-i)=\frac{1}{n}\sum_{i=1}^{n}(n-i)=\frac{1}{n}\cdot\frac{n(n+1)}{2}=\frac{n-1}{2} i=1∑npi(n−i)=i=1∑nn1(n−i)=n1i=1∑n(n−i)=n1⋅2n(n+1)=2n−1
因此,线性表删除算法的平均时间复杂度为O(n)。
- 按值查找(顺序查找)
在顺序表L中查找第一个元素的值等于e的元素,并返回其位序。
int LocateElement(SqList L, ElemType e){
//本算法实现查找顺序表中值为e的元素,如果查找成功,返回元素位序,否则返回0。
int i;
for (i=0; i<L.length; i++){
if(L.data[i] == e){
return i+1;
}
}
return 0;
}
最好情况:查找的元素就在表头,仅需比较一次,时间复杂度为O(1)。
最坏情况:在查找的元素在表尾或不存在时,需要比较n次,时间复杂度为O(n)。
平均情况:假设
p
i
(
p
i
=
1
n
)
p_i(p_i = \frac{1}{n})
pi(pi=n1)是查找的元素在第i个位置上的概率,则在长度为n的线性表中查找值为e的元素所需比较的平均次数为:
∑ i = 1 n p i ⋅ i = ∑ i = 1 n 1 n ⋅ i = 1 n n ( n + 1 ) 2 = n + 1 2 \sum_{i=1}^{n}p_i\cdot i=\sum_{i=1}^{n}\frac{1}{n}\cdot i=\frac{1}{n}\frac{n(n+1)}{2}=\frac{n+1}{2} i=1∑npi⋅i=i=1∑nn1⋅i=n12n(n+1)=2n+1
因此,线性表按值查找算法的平均时间复杂度为O(n)。
2.2.3 单项选择题
-
下述()是顺序存储结构的优点
A. 存储密度大
B. 插入运算方便
C. 删除运算方便
D. 方便地运用于各种逻辑结构的存储表示 -
线性表的顺序存储结构是一种()。
A. 随机存取的存储结构
B. 顺序存取的存储结构
C. 索引存取的存储结构
D. 散列存取的存储结构 -
一个顺序表所占用的存储空间大小与()无关。
A. 表的长度
B. 元素的存放顺序
C. 元素的类型
D. 元素中各字段的类型 -
若线性表最常用的操作是存取第i个元素及其前驱和后继元素的值,为了提高效率,应采用()的存储方式。
A. 单链表
B. 双向链表
C. 单循环裢表
D. 顺序表 -
一个线性表最常用的操作是存取任一指定序号的元素和在最后进行插入删除操作,则利用()存储方式可以节省时间。
A. 顺序表
B. 双链表
C. 带头结点的双循环链表
D. 单环链表 -
在n个元素的线性表的数组表示中,以下时间复杂度为O(1)的操作是()。
I.访问第i( 1 ≤ i ≤ n 1\leq i \leq n 1≤i≤n)个结点和求第i( 2 ≤ i ≤ n 2\leq i \leq n 2≤i≤n)个结点的直接前驱
II.在最后一个结点后插入一个新的结点
III.删除第1个结点
IV.在第i( 1 ≤ i ≤ n 1\leq i \leq n 1≤i≤n)个结点后插入一个结点A. I
B. II、III
C. I、II
D. I、II、III -
设线性表有n个元素,严格说来,以下操作中,()在线性表上实现要比链表上实现的的效率高。
I.输出第i( 1 ≤ i ≤ n 1\leq i \leq n 1≤i≤n)个元素值
II.交换第3个元素与第4个元素的值
III.顺序输出这n个元素的值A. I
B. II、III
C. I、II
D. I、II、III -
在一个长度为n的顺序表中删除第i( 1 ≤ i ≤ n 1 \leq i \leq n 1≤i≤n )个元素时,需向前移动()个元素。
A. n
B. i-1
C. n-i
D. n-i+1 -
对于顺序表,访问第i个位置的元素和在第i个位置插入一个元素的时间复杂度为()。
A. O(n),O(n)
B. O(n),O(1)
C. O(1),O(n)
D. O(1),O(1) -
若长度为n的非空线性表采用顺序存储结构,在表的第i个位置插入一个数据元素,i的合法值应该是()。
A. 1 ≤ i ≤ n 1\leq i \leq n 1≤i≤n
B. 1 ≤ i ≤ n + 1 1\leq i \leq n+1 1≤i≤n+1
C. 0 ≤ i ≤ n − 1 0\leq i \leq n-1 0≤i≤n−1
D. 0 ≤ i ≤ n 0\leq i \leq n 0≤i≤n
2.2.4单项选择题答案
-
A
顺序表不像链表要在结点中存放指针域,因此存储密度较大,A正确。B和C是链表的优点。D是错误的,比如对于树形结构,顺序表显然不如链表表示起来方便。 -
A
本题容易误选B,顺序表是一种支持随机存取的顺序存储结构,根据起始地址加上元素的序号,可以很方便地访问到任一元素,即随机存取的概念。注意,顺序存取是一种读写方式,不是存储方式,有别于顺序存储。 -
B
顺序表所占存储空间=表长 × \times ×sizeof(元素的类型),元素的类型显然会影响到存储空间的大小。对于同一类型的顺序表,表越长,则所占存储空间就越大。 -
D
题干实际要求能够最快存取第 i − 1 i-1 i−1、 i − 1 i-1 i−1和 i + 1 i+1 i+1个元素值。A、B、C都只能从头结点依次顺序查找,时间复杂度为O(n),只有顺序表可以随机存取,时间复杂度为O(1)。 -
A
只有顺序表具有随机存取的优点,且在最后进行插入和删除操作不需要移动任何元素。 -
C
I中,时间复杂度显然为O(1);II中,在最后一个结点插入一个新结点不需要移动元素,故也为O(1);III中,第一个结点后的结点需要依次前移,时间复杂度为O(n);IV中,需要移动 n − i n-i n−i个结点,时间复杂度为0(n)。 -
C
顺序输出这n个元素的值,都要依次访问每个元素,故时间复杂度相同。 -
C
需要将 a i + 1 a_{i+1} ai+1~ a n a_n an元素前移一位,共移动 n − ( i + 1 ) + 1 = n − i n-(i+1)+1=n-i n−(i+1)+1=n−i个元素。 -
C
顺序表中,第i个元素的物理地址可以通过起始地址和序号直接计算出,即可在O(1)时间内访问。在第i个位置插入一个元素,需要移动 n − i + 1 n-i+1 n−i+1个元素,时间复杂度为O(n)。 -
B
表元素序号从1开始,而在第n+1个位置插入相当于在表尾追加。
2.2.5 编程题
- 从顺序表中删除具有最小值的元素(假设唯一)并由函数返回被删元素的值。空出的位置由最后一个元素填补,若顺序表为空则显示出错信息并退出运行。
算法思想:搜索整个顺序表,查找最小值元素,并记住其位置,搜索结束后用最后一个元素填补空出的原最小值元素的位置。
bool Del_Min(SqList &L, ElemType &value){
//删除顺序表L中最小值元素节点,并通过引用型参数value返回其值
//我删除成功则返回true,否则返回false
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记忆当前具有最小值的元素。
value = L.data[i];
pos = i;
}
}
L.data[pos]=L.data[L.length-1]; //空出的位置由最后一个元素填补。
L.length--;
return true; //此时value即为最小值。
}
注意:本题也可用函数返回值返回,两者的区别是:函数返回值只能返回一个值,而参数返回(引用传参)可以返回多个值。
- 设计一个高效的算法,将顺序表L的所有元素逆置,要求算法的空间复杂度为O(1)。
算法思想。扫描顺序表L的前半部分元素,对于元素
L.data[i]
( 0 ≤ i < L . l e n g t h 0 \leq i < L.length 0≤i<L.length ),将其与后半部分的对应元素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;
}
}
- 对长度为n的顺序表L,编写一个时间复杂度为O(n)、空间复杂度为O(1)的算法,该算法删除线性表中所有值为x的数据元素。
解法一:用k记录顺序表L中不等于x的元素个数及(即要保存的元素个数),边扫描边统计k,并将不等于x的元素向前移动k个位置,最后修改L的长度。
void del_x_1(SqList &L, ElemType x){
//本算法实现删除顺序表L中所有值为x的数据元素。
int k = 0; //记录值不等于x的元素个数。
for(int i=0;i<L.length;i++){
if(L.data[i]!=x){
L.data[k]=L.data[i];
k++; //不等于x的元素增1。
}
}
L.length=k; //顺序表L的长度等于k
}
解法二:用k记录顺序表L中等于x的元素个数,边扫描L边统计k,并将不等于x的元素前移k个位置,最后修改L的长度。
void del_x_2(SqList &L, ElemType x){
int k=0, i=0; //用k记录值等于x的元素个数。
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; //顺序表L的长度递减。
}
此外,本题还可以考虑设计头尾两个指针从两端向中间移动,凡遇到最左端值为x的元素时,直接将最右端的值非x的元素左移至值为x的数据元素位置,直到两指针相遇。但是这种方法会改变原表中元素的相对位置。
- 从有序顺序表中删除其值在给定值s与t之间(要求s<t)的所有元素,如果s或t不合理或者顺序表为空,则显示出错信息并退出运行。
注意本题与上一题的区别,因为是有序表,所以删除的元素必然是相连的整体。
算法思想:先寻找值大于等于s的第一个元素(第一个删除的元素),然后寻找值大于t的第一个元素(最后一个删除的元素的下一个元素),要将这段元素删除,只需将后面的元素前移。
bool Del_s_t(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; //寻找元素值均小于s,返回。
}
for(j=i; j < L.length && L.data[j] <= t; j++){} //寻找值大于t的第一个元素。
for (;j < L.length;j++, i++){
L.data[i]=L.data[j]; //前移,填补被删元素位置。
}
L.length=i;
return true;
}
- 从顺序表中删除其值在给定值s与t之间(包含s和t,要求S<t)的所有元素,如果s或t不合理或者顺序表为空则显示出错信息并退出运行。
算法思想:从前向后扫描顺序表L,用k记录下元素值在s到t之间的元素个数(初始时k等于0),对于当前扫描的元素,若其值不在s到t之间,则前移k个位置;否则执行k++。由于这样每个不在s到t之间的元素仅移动一次,所以算法效率高。
bool Del_s_t_2(SqList &L, ElemType s, ElemType t){
//删除顺序表L中在给定值s与t(s<t)之间的所有元素。
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 ; //长度减少。
return true;
}
- 从有序顺序表中删除所有其值重复的元素,使表中所有元素的值均不同。
算法思想:注意是有序顺序表,值相同的元素一定在连续的位置上,用类似于直接插入排序的思想,初始时将第一个元素视为非重复的有序表,之后依次判断后面的元素是否与前面的非重复有序表的最后一个元素相同,若相同的继续向后判断,若不同则插入到前面的非重复有序表的最后,直至判断到表尾为止。
bool DelSame(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;
}
对于本题的算法,将有序数列 [ 1 , 2 , 2 , 2 , 2 , 3 , 3 , 3 , 4 , 4 , 5 ] [1,2,2,2,2,3,3,3,4,4,5] [1,2,2,2,2,3,3,3,4,4,5]手动模拟算法的执行过程,在模拟过程中要标注i与j所指示的元素。
思考:如果将本题的有序表改为无序表,你能想到时间复杂度为O(n)的方法吗?提示:使用 散列表。 (请选中空白部分>_+)
- 【重要】将两个有序顺序表合并成一个新的有序顺序表,并由函数返回结果顺序表。
算法思想:首先,按顺序不断取下两个顺序表表头较小的节点存入新的顺序表中,然后看哪个表还有剩余,将剩下的部分加到新的顺序表后面。
bool Merge(SeqList A, SeqList &B, SeqList &C){
//将有序顺序表A与B合并为一个新的有序顺序表C
if(A.length+B.length >C.Max_Size){ //大于顺序表的最大长度。
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;
}
- 已知在一维数组 A [ m + n ] A[m+n] A[m+n]中依次存放着两个线性表( a 1 , a 2 , . . . , a m a_1,a_2,...,a_m a1,a2,...,am)和( b 1 , b 2 , . . . , b n b_1,b_2,...,b_n b1,b2,...,bn)。试编写一个函数,将数组中两个顺序表的位置互换,即将( b 1 , b 2 , . . . , b n b_1,b_2,...,b_n b1,b2,...,bn)放在( a 1 , a 2 , . . . , a m a_1,a_2,...,a_m a1,a2,...,am)的前面。
算法思想:首先将数组 A [ m + n ] A[m+n] A[m+n]的全部元素( a 1 , a 2 , . . . , a m , b 1 , b 2 , . . . , b n a_1,a_2,...,a_m,b_1,b_2,...,b_n a1,a2,...,am,b1,b2,...,bn)原地逆置为( b n , b n − 1 , b n − 2 , . . . , b 1 , a m , a m − 1 , a m − 2 , . . . , a 1 b_n,b_{n-1},b_{n-2},...,b_1,a_m,a_{m-1},a_{m-2},...,a_1 bn,bn−1,bn−2,...,b1,am,am−1,am−2,...,a1),再对前n个元素和后m个元素分别使用逆置算法即可得到( b 1 , b 2 , . . . , b n , a 1 , a 2 , . . . , a m b_1,b_2,...,b_n,a_1,a_2,...,a_m b1,b2,...,bn,a1,a2,...,am),从而实现顺序表的位置互换。
typedef int DataType;
void Reverse(DataType A[], int left, int right, int arraySize){
//逆转数组A
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){
/*
数组A[m+n]中,
从0到m-1存放顺序表(a1,a2,...,am)
从m到m+n-1存放顺序表(b1,b2,...,bn)
算法将这两个表的位置互换
*/
Reverse(A, 0, m+n-1, arraySize); //原地逆置整个表(an~bn --> bn,...,b1,am,...,a1 )
Reverse(A, 0, n-1, arraySize); //对前n个元素逆置(bn,...,b1-->b1,...,bn)
Reverse(A, n, m+n-1, arraySize); //对n~m+n-1个元素逆置(am,...,a1-->a1,...,am)
}
- 线性表( a 1 , a 2 , . . . , a m a_1,a_2,...,a_m a1,a2,...,am)中元素递增有序且按顺序存储于计算机内,要求设计一算法完成用最少时间在表中查找数值为x的元素,若找到将其与后继元素位置相交換,若找不到则将其插入表中并使表中元素仍递增有序。
算法思想:顺序存储的线性表是递增有序,可以按顺序查找,也可以折半查找。题目要求“用最少的时间在表中查找数据为x的元素”,这里应使用折半查找法。
void SearchExchangeInsert(ElemType A[], ElemType x, int arraySize){
int low=0, high=arraySize-1,mid,i; //low与high指向顺序表下界和上界的下标。
while(low<=high){
mid = (low+high)/2; //找中间位置。
if(A[mid]==x){break;} //找到x,退出while循环。
else if(A[mid]<x){ //找到中点的右半部去查。
low=mid+1;
}
else{
high = mid-1; //找到中点的左半部去查。
}
}
//下面的两个if语句只会执行一个。
if(A[mid]==x && mid!=arraySize-1){ //若最后一个元素与x相等,则不存在与其后继交换的操作。
ElemType t = A[mid];
A[mid]=A[mid+1];
A[mid+1]=t;
}
if(low>high){ //查找失败,插入数据元素x
for(i=arraySize-1;i>high;i--){ //后移元素。
A[i+1]=A[i];
}
A[i+1]=x; //插入x
}
}
本题的算法也可写成三个函数:查找函数、交换后继函数与插入函数。写成三个函数的优点是逻辑清晰、易读。
- 设将n(n>1)个整数存放到一维数组R中。试设计一个在时间和空间两方面都尽可能高效的算法。将R中保存的序列环左移p(0<p<n)个位置,即将R中的数据由(
X
0
,
X
1
,
.
.
.
,
X
n
−
1
X_0,X_1,...,X_{n-1}
X0,X1,...,Xn−1)变换为(
X
p
,
X
p
+
1
,
.
.
.
,
X
n
−
1
,
X
0
,
X
1
,
.
.
.
,
X
p
−
1
X_p,X_{p+1},...,X_{n-1},X_0,X_1,...,X_{p-1}
Xp,Xp+1,...,Xn−1,X0,X1,...,Xp−1)。要求:
(1)给出算法的基本设计思想。
(2)根据设计思想,采用C或C++或Java语言描述算法,关键之处给出注释。
(3)说明你所设计算法的时间复杂度和空间复杂度。
答:
(1)算法的基本设计思想:可将这个问题视为把数组 a b ab ab转化成数组 b a ba ba( a a a代表数组的前p个元素。 b b b代表数组中余下的 n − p n-p n−p个元素。先将 a a a逆置得到 a − 1 b a^{-1}b a−1b,再将 b b b逆置得到 a − 1 b − 1 a^{-1}b^{-1} a−1b−1,最后。将整个 a − 1 b − 1 a^{-1}b^{-1} a−1b−1逆置得到 ( a − 1 b − 1 ) − 1 = b a (a^{-1}b^{-1})^{-1}=ba (a−1b−1)−1=ba。设 R e v e r s e Reverse Reverse函数执行将数组元素逆置的操作,对 a b c d e f g h abcdefgh abcdefgh左循环移动3( p = 3 p=3 p=3)个位置的过程如下:
操作 | 得到 |
--- | a b c d e f g h |
Reverse(0, p-1) | c b a d e f g h |
Reverse(0, n-1) | c b a h g f e d |
Reverse(0, n-1) | d e f g h a b c |
(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)上述三个函数的时间复杂度分别为O( p 2 \frac{p}{2} 2p)、O( n − p 2 \frac{n-p}{2} 2n−p)、O( n 2 \frac{n}{2} 2n),故所设计的算法的时间复杂度为O(n),空间复杂度O(1)。
除此之外,可以借助辅助数组来实现。算法思想:创建大小为p的辅助数组S,将R中前p个整数依次暂存在S中,同时将R中后n-p个整数左移,然后将S中暂存的p个数依次放回到R中的后续单元。时间复杂度为O(n),空间复杂度为O§。
- 一个长度为L(L
≥
\geq
≥ 1)的升序序列S,处在第[
L
2
\frac{L}{2}
2L]个位置的数称为S的中位数。例如,若序列
S
1
=
(
11
,
13
,
15
,
17
,
19
)
S_1=(11,13,15,17,19)
S1=(11,13,15,17,19),则
S
1
S_1
S1的中位数是15,两个序列的中位数是含它们所有元素的升序序列的中位数。例如,若
S
2
(
2
,
4
,
6
,
8
,
20
)
S_2(2,4,6,8,20)
S2(2,4,6,8,20),则
S
1
S_1
S1和
S
2
S_2
S2的中位数是11.现在有两个等长升序序列A和B,试设计一个在时间和空间两方面都尽可能高效的算法,找出两个序列A和B的中位数。要求:
(1)给出算法的基本设计思想。
(2)根据设计思想,采用C或C++或Java语言描述算法,关键之处给出注释。
(3)说明你所设计算法的时间复杂度和空间复杂度。
答:
(1)算法的基本设计思想如下:
分别求两个升序序列A、B中的中位数,设为a和b,求序列A、B的中位数过程如下:
① 若a=b,则a和b即为所求中位数,算法结束。
② 若a<b,则舍弃序列A中较小的一半,同时舍弃序列B中较大的一半,要求两次舍弃的长度相等。
③ 若a>b,则舍弃序列A中较大的一半,同时舍弃序列B中较小的一半,要求两次舍弃的长度相等。
在保留的两个升序序列中,重复过程①、②、③,直到两个序列中均只含一个元素为止,较小者即为所求的中位数。
(2)使用C语言描述算法如下:
int M_Search(int A[], int B[], int n){
int s1=0, d1=n-1, m1, s2=0, d2=n-1, m2;
//分别表示序列A和B的首位数末位数和中位数。
while(s1!=d1 ||s2!=d2){
m1 = (s1+d1)/2;
m2 = (s2+d2)/2;
if(A[m1]==B[m2]){
return A[m1]; //满足条件1。
}
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((s2+d2)%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];
}
(3)所设计的算法的时间复杂度为O(log 2 _2 2n),空间复杂度O(1)。
- 已知一个整数序列
A
=
(
a
0
,
a
1
,
.
.
.
,
a
n
−
1
)
A=(a_0,a_1,...,a_{n-1})
A=(a0,a1,...,an−1),其中
0
≤
p
k
<
n
(
0
≤
i
<
n
)
0 \leq p_k < n (0 \leq i < n)
0≤pk<n(0≤i<n).若存在
a
p
1
=
a
p
2
=
.
.
.
=
a
p
m
=
x
a_{p1}=a_{p2}=...=a_{pm}=x
ap1=ap2=...=apm=x且m>
n
2
(
0
≤
p
k
<
n
,
1
≤
k
≤
m
)
\frac{n}{2}(0 \leq p_k<n,1 \leq k \leq m)
2n(0≤pk<n,1≤k≤m),则称x为A的主元素。例如
A
=
(
0
,
5
,
5
,
3
,
5
,
7
,
5
,
5
)
A=(0,5,5,3,5,7,5,5)
A=(0,5,5,3,5,7,5,5),则5为主元素;又如
A
=
(
0
,
5
,
5
,
3
,
5
,
1
,
5
,
7
)
A=(0,5,5,3,5,1,5,7)
A=(0,5,5,3,5,1,5,7),则A中没有主元素。假设A中的n个元素保存在一个一维数组中,请设计一个尽可能高效的算法,找出A的主元素。若存在主元素,则输出该元素;否则输出一1。要求:
(1)给出算法的基本设计思想。
(2)根据设计思想,采用C或C++或Java语言描述算法,关键之处给出注释。
(3)说明你所设计算法的时间复杂度和空间复杂度。
答:
(1)算法的基本设计思想如下:
算法的策略是从前向后扫描数组元素,标记出一个可能成为主元素的的元素Num,然后重新计数,确认Num是否是主元素。算法可分为以下两步:
① 选取候选的主元素。依次扫描所给的数组中的每个整数,将第一个遇到的整数Num保存到c.中,记录Num出现的次数为1;若遇到的下一个整数仍等于Num,则计数加1,否则技计数减1;。当计数减到0时,将遇到的下一个整数保存到c中。计数重新记为1,开始新一轮计数,即从当前位置开始重复上述过程,直到扫描完全部数组元素。
②判断c中元素是否是真正的主元素。再次扫描该数组。统计c中元素出现的次数。若大于
n
2
\frac{n}{2}
2n,则为主元素,否则序列中不存在主元素。
(2)使用C语言描述算法如下:
int Majority(int A[], int n){
int i, c, count=1; //用c来保存候选主元素。count用来计数。
c = A[0]; //设置A[0]为候选主元素
for(i=1;i<n;i++){ //查找候选主元素
if(A[i]==c){
count++; //对A中的候选主元素计数
}
else{
if(count>0){ //处理不是候选主元素的情况
count--;
}
else{ //更换候选主元素,重新计数
c = A[i];
count = 1;
}
}
}
if(count>0){
for(i=count=0;i<n;i++){ //统计候选主元素实际出现的次数
if(A[i]==c){
count++;
}
}
}
if(count > n/2){
return c; //确认候选主元素
}
else{
return -1; //不存在候选主元素
}
}
(3)所设计的算法的时间复杂度为O(n),空间复杂度O(1)。
- 给定一个含n(
n
≥
1
n \geq 1
n≥1)个整数的数组,请设计一个在时间上尽可能高效的算法,找出数组中未出现的最小正整数。例如,数组中{-5,3,2,3}未出现的正整数是1;数组中{1,2,3,4}未出现的正整数是4。
要求:
(1)给出算法的基本设计思想。
(2)根据设计思想,采用C或C++或Java语言描述算法,关键之处给出注释。
(3)说明你所设计算法的时间复杂度和空间复杂度。
答:
(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中出现了小于等于0或者大于n的值时可以不采取任何操作。
经过以上分析可以得出算法流程从A[0]开始遍历A,若0<A[i]<=n,则令B[A[i]-1]=1;否则不做操作。对A遍历结束后,开始遍历数组B,若能查找到第一个满足B[i]==0的下标i,返回i+1即为结果。此时说明A中未出现正整数在1 ~ n之间。若B全不为0,返回i+1(跳出循环时i=n,i+1等于n+1),此时说明A中未出现的最小正整数是n+1。
(2)使用C语言描述算法如下:
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){ //若A[i]的值介于1~n,则标记数组B
B[A[i]-1]=1;
}
}
for(i=0; i<n; i++){ //扫描数组B,找到目标值
if(B[i]==0){break;}
}
return i+1; //返回结果
}
(3)时间复杂度:遍历A一次,遍历B一次,两次循环内操作步骤为O(1)量级,因此时间复杂度为O(n)。空间复杂度:额外分配了B[n],空间复杂度为O(n)。