第二章 线性表
一、基本知识点
(1)线性表的顺序存储结构和链式存储结构的优缺点。
(2)顺序表的插人和删除操作过程及其实现。
(3)单链表的查找、插人和删除操作过程及其实现。
(4)双链表的查找、插人和删除操作过程及其实现。
(5)循环链表的查找、插人和删除操作过程及其实现。
(6)有序表的二路归并算法的思路及其实现算法,以及该算法的时间复杂度分析。
(7)利用线性表求解复杂的应用问题。
二、要点归纳、基本实现
1、线性表的特征:
(1)有且仅有一个开始结点(表头结点)a1,它没有直接前驱,只有一个直接后继;
(2)有且仅有一个终端结点(表尾结点)an,它没有直接后继,只有一个直接前驱;
(3)其它结点都有一个直接前驱和直接后继;
(4)元素之间为一对一的线性关系。
2、顺序表的实现:
#include <iostream>
using namespace std;
#define MaxSize 50
typedef int ElemType;
typedef struct
{
ElemType data[MaxSize]; //存放顺序表元素
int length; //存放顺序表的长度
} SqList; //顺序表的类型
void CreateList(SqList*& L, ElemType a[], int n) //建立顺序表
{
L = (SqList*)malloc(sizeof(SqList)); //分配存放线性表的空间
for (int i = 0; i < n; i++)
L->data[i] = a[i]; //将元素a[i]存放到L中
L->length = n; //设置L的长度为n
}
void InitList(SqList*& L) //初始化线性表
{
L = (SqList*)malloc(sizeof(SqList)); //分配存放线性表的空间
L->length = 0;
}
void DestroyList(SqList*& L) //销毁线性表
{
free(L);
}
bool ListEmpty(SqList* L) //判断线性表是否为空表
{
return(L->length == 0);
}
int ListLength(SqList* L) //求线性表的长度
{
return(L->length);
}
void DispList(SqList* L) //输出线性表
{
for (int i = 0; i < L->length; i++)
{
cout<< L->data[i]<< " ";
}
cout<< endl;
}
bool GetElem(SqList* L, int i, ElemType& e) //求线性表中的某个数据元素的值
{
if (i<1 || i>L->length)
{
return false; //参数i错误时返回false
}
e = L->data[i - 1]; //取元素值
return true; //成功找到元素时返回true
}
int LocateElem(SqList* L, ElemType e) //按元素值查找
{
int i = 0;
while (i < L->length&& L->data[i] != e)
{
i++; //查找元素e
}
if (i >= L->length) return 0; //未找到时返回0
else return i + 1; //找到后返回其逻辑序号
}
bool ListInsert(SqList*& L, int i, ElemType e) //插入数据元素
{
int j;
if (i<1 || i>L->length + 1) return false; //参数i错误时返回false
i--; //将顺序表位序转化为elem下标
for (j = L->length; j > i; j--) //将data[i]及后面元素后移一个位置
{
L->data[j] = L->data[j - 1];
}
L->data[i] = e; //插入元素e
L->length++; //顺序表长度增1
return true; //成功插入返回true
}
bool ListDelete(SqList*& L, int i, ElemType& e) //删除数据元素
{
int j;
if (i<1 || i>L->length) return false; //参数i错误时返回false
i--; //将顺序表位序转化为elem下标
e = L->data[i];
for (j = i; j < L->length - 1; j++) //将data[i]之后的元素前移一个位置
{
L->data[j] = L->data[j + 1];
}
L->length--; //顺序表长度减1
return true; //成功删除返回true
}
3、单链表的实现:
#include <iostream>
using namespace std;
typedef int ElemType;
typedef struct LNode
{
ElemType data; //存放元素值
struct LNode* next; //指向后继结点
} LinkNode; //声明单链表结点类型
void CreateListF(LinkNode*& L, ElemType a[], int n) //头插法建立单链表
{
LinkNode* s;
L = (LinkNode*)malloc(sizeof(LinkNode));
L->next = NULL; //创建头结点,其next域置为NULL
for (int i = 0; i < n; i++) //循环建立数据结点s
{
s = (LinkNode*)malloc(sizeof(LinkNode));
s->data = a[i]; //创建新结点s
s->next = L->next; //将结点s插在原开始结点之前,头结点之后
L->next = s;
}
}
void CreateListR(LinkNode*& L, ElemType a[], int n) //尾插法建立单链表
{
LinkNode* s, * r;
L = (LinkNode*)malloc(sizeof(LinkNode)); //创建头结点
L->next = NULL;
r = L; //r始终指向终端结点,开始时指向头结点
for (int i = 0; i < n; i++) //循环建立数据结点s
{
s = (LinkNode*)malloc(sizeof(LinkNode)); //创建新结点s
s->data = a[i]; //创建数据结点s
r->next = s; //将结点s插入结点r之后
r = s;
}
r->next = NULL; //终端结点next域置为NULL
}
void InitList(LinkNode*& L) //初始化线性表
{
L = (LinkNode*)malloc(sizeof(LinkNode));
L->next = NULL; //创建头结点,其next域置为NULL
}
void DestroyList(LinkNode*& L) //销毁线性表
{
LinkNode* pre = L, * p = pre->next; //pre指向结点p的前驱结点
while (p != NULL) //扫描单链表L
{
free(pre); //释放pre结点
pre = p; //pre、p同步后移一个结点
p = pre->next;
}
free(pre); //此时p为NULL,pre指向尾结点,释放它
}
bool ListEmpty(LinkNode* L) //判断线性表是否为空表
{
return(L->next == NULL);
}
int ListLength(LinkNode* L) //求线性表的长度
{
int i = 0;
LinkNode* p = L; //p指向头结点,i置为0(即头结点的序号为0)
while (p->next != NULL)
{
i++;
p = p->next;
}
return(i); //循环结束,p指向尾结点,其序号n为结点个数
}
void DispList(LinkNode* L) //输出线性表
{
LinkNode* p = L->next; //p指向首结点
while (p != NULL) //p不为NULL,输出p结点的data域
{
cout<< p->data<< " ";
p = p->next; //p移向下一个结点
}
cout<< endl;
}
bool GetElem(LinkNode* L, int i, ElemType& e) //求线性表中的某个数据元素值
{
int j = 0;
LinkNode* p = L; //p指向头结点,j置为0(即头结点的序号为0)
if (i <= 0) return false; //i错误返回假
while (j < i && p != NULL) //找第i个结点p
{
j++;
p = p->next;
}
if (p == NULL) return false; //不存在第i个数据结点
else //存在第i个数据结点
{
e = p->data;
return true;
}
}
int LocateElem(LinkNode* L, ElemType e) //按元素值查找
{
LinkNode* p = L->next; //p指向首结点,i置为1(即首结点的序号为1)
int n = 1;
while (p != NULL && p->data != e) //查找data值为e的结点,其序号为n
{
p = p->next;
n++;
}
if (p == NULL) return(0); //不存在值为e的结点,返回0
else return(n); //存在值为e的结点,返回其逻辑序号n
}
bool ListInsert(LinkNode*& L, int i, ElemType e) //插入数据元素
{
int j = 0;
LinkNode* p = L, * s; //p指向头结点,j置为0(即头结点的序号为0)
if (i <= 0) return false; //i错误返回假
while (j < i - 1 && p != NULL) //查找第i-1个结点p
{
j++;
p = p->next;
}
if (p == NULL) return false; //未找到位序为i-1的结点
else //找到位序为i-1的结点*p
{
s = (LinkNode*)malloc(sizeof(LinkNode));
s->data = e; //创建新结点s,其data域置为e
s->next = p->next; //将s结点插入到结点p之后
p->next = s;
return true;
}
}
bool ListDelete(LinkNode*& L, int i, ElemType& e) //删除数据元素
{
int j = 0;
LinkNode* p = L, * q; //p指向头结点,j置为0(即头结点的序号为0)
if (i <= 0) return false; //i错误返回假
while (j < i - 1 && p != NULL) //查找第i-1个结点
{
j++;
p = p->next;
}
if (p == NULL) return false; //未找到位序为i-1的结点
else //找到位序为i-1的结点p
{
q = p->next; //q指向要删除的结点
if (q == NULL) return false; //若不存在第i个结点,返回false
e = q->data;
p->next = q->next; //从单链表中删除q结点
free(q); //释放q结点
return true;
}
}
4、双链表的实现:
#include <iostream>
using namespace std;
typedef int ElemType;
typedef struct DNode //定义双链表结点类型
{
ElemType data;
struct DNode *prior; //指向前驱结点
struct DNode *next; //指向后继结点
} DLinkNode;
void CreateListF(DLinkNode *&L,ElemType a[],int n)
//头插法建双链表
{
DLinkNode *s;
L=(DLinkNode *)malloc(sizeof(DLinkNode)); //创建头结点
L->prior=L->next=NULL;
for (int i=0;i<n;i++)
{
s=(DLinkNode *)malloc(sizeof(DLinkNode));//创建新结点
s->data=a[i];
s->next=L->next; //将结点s插在原开始结点之前,头结点之后
if (L->next!=NULL)
{
L->next->prior=s;
}
L->next=s;
s->prior=L;
}
}
void CreateListR(DLinkNode *&L,ElemType a[],int n)
//尾插法建双链表
{
DLinkNode *s,*r;
L=(DLinkNode *)malloc(sizeof(DLinkNode)); //创建头结点
L->prior=L->next=NULL;
r=L; //r始终指向终端结点,开始时指向头结点
for (int i=0;i<n;i++)
{
s=(DLinkNode *)malloc(sizeof(DLinkNode));//创建新结点
s->data=a[i];
r->next=s;s->prior=r; //将结点s插入结点r之后
r=s;
}
r->next=NULL; //尾结点next域置为NULL
}
void InitList(DLinkNode *&L)
{
L=(DLinkNode *)malloc(sizeof(DLinkNode)); //创建头结点
L->prior=L->next=NULL;
}
void DestroyList(DLinkNode *&L)
{
DLinkNode *pre=L,*p=pre->next;
while (p!=NULL)
{
free(pre);
pre=p;
p=pre->next;
}
free(pre);
}
bool ListEmpty(DLinkNode *L)
{
return(L->next==NULL);
}
int ListLength(DLinkNode *L)
{
DLinkNode *p=L;
int i=0;
while (p->next!=NULL)
{
i++;
p=p->next;
}
return(i);
}
void DispList(DLinkNode *L)
{
DLinkNode *p=L->next;
while (p!=NULL)
{
cout<< p->data<< " ";
p=p->next;
}
cout<< endl;
}
bool GetElem(DLinkNode *L,int i,ElemType &e)
{
int j=0;
DLinkNode *p=L;
if (i<=0) return false; //i错误返回假
while (j<i && p!=NULL)
{
j++;
p=p->next;
}
if (p==NULL)
return false;
else
{
e=p->data;
return true;
}
}
int LocateElem(DLinkNode *L,ElemType e)
{
int n=1;
DLinkNode *p=L->next;
while (p!=NULL && p->data!=e)
{
n++;
p=p->next;
}
if (p==NULL)
return(0);
else
return(n);
}
bool ListInsert(DLinkNode *&L,int i,ElemType e)
{
int j=0;
DLinkNode *p=L,*s;
if (i<=0) return false; //i错误返回假
while (j<i-1 && p!=NULL)
{
j++;
p=p->next;
}
if (p==NULL) //未找到第i-1个结点
return false;
else //找到第i-1个结点p
{
s=(DLinkNode *)malloc(sizeof(DLinkNode)); //创建新结点s
s->data=e;
s->next=p->next; //将结点s插入到结点p之后
if (p->next!=NULL)
p->next->prior=s;
s->prior=p;
p->next=s;
return true;
}
}
bool ListDelete(DLinkNode *&L,int i,ElemType &e)
{
int j=0;
DLinkNode *p=L,*q;
if (i<=0) return false; //i错误返回假
while (j<i-1 && p!=NULL)
{
j++;
p=p->next;
}
if (p==NULL) //未找到第i-1个结点
return false;
else //找到第i-1个结点p
{
q=p->next; //q指向要删除的结点
if (q==NULL)
return false; //不存在第i个结点
e=q->data;
p->next=q->next; //从单链表中删除*q结点
if (p->next!=NULL) p->next->prior=p;
free(q); //释放q结点
return true;
}
}
三、练习题
1.选择题
(1)顺序表中第一个元素的存储地址是100,每个元素的长度为2,则第5个元素的地址是()。
A.110 B.108 C.100 D.120
(2)在n个结点的顺序表中,算法的时间复杂度是O(1)的操作是()。
A.访问第i个结点(1≤i≤n)和求第i个结点的直接前驱(2≤i≤n)
B.在第i个结点后插入一个新结点(1≤i≤n) C.删除第i个结点(1≤i≤n)
D.将n个结点从小到大排序
(3)向一个有127个元素的顺序表中插入一个新元素并保持原来顺序不变,平均要移动的元素个数为()。A.8 B.63.5 C.63 D.7
(4)链接存储的存储结构所占存储空间()。
A.分两部分,一部分存放结点值,另一部分存放表示结点间关系的指针
B.只有一部分,存放结点值
C.只有一部分,存储表示结点间关系的指针
D.分两部分,一部分存放结点值,另一部分存放结点所占单元数
(5)线性表若采用链式存储结构时,要求内存中可用存储单元的地址()。
A.必须是连续的 B.部分地址必须是连续的 C.一定是不连续的 D.连续或不连续都可以
(6)线性表L在()情况下适用于使用链式结构实现。
A.需经常修改L中的结点值
B.需不断对L进行删除插入
C.L中含有大量的结点
D.L中结点结构复杂
解释:链表最大的优点在于插入和删除时不需要移动数据,直接修改指针即可。
(7)单链表的存储密度()。
A.大于1 B.等于1 C.小于1 D.不能确定
(8)将两个各有n个元素的有序表归并成一个有序表,其最少的比较次数是()。
A.n B.2n-1 C.2n D.n-1
(9)在一个长度为n的顺序表中,在第i个元素(1≤i≤n+1)之前插入一个新元素时须向后移动()个元素。
A.n-i B.n-i+1 C.n-i-1 D.i
(10)线性表L=(a1,a2,……an),下列说法正确的是()。
A.每个元素都有一个直接前驱和一个直接后继
B.线性表中至少有一个元素
C.表中诸元素的排列必须是由小到大或由大到小
D.除第一个和最后一个元素外,其余每个元素都有一个且仅有一个直接前驱和直接后继。
(11)创建一个包括n个结点的有序单链表的时间复杂度是()。
A.O(1) B.O(n) C.O(n2) D.O(nlog2n)
(12)以下说法错误的是()。
A.求表长、定位这两种运算在采用顺序存储结构时实现的效率不比采用链式存储结构时实现的效率低
B.顺序存储的线性表可以随机存取
C.由于顺序存储要求连续的存储区域,所以在存储管理上不够灵活
D.线性表的链式存储结构优于顺序存储结构
(13)在单链表中,要将s所指结点插入到p所指结点之后,其语句应为()。
A.s->next=p+1; p->next=s;
B.(*p).next=s; (*s).next=(*p).next;
C.s->next=p->next; p->next=s->next;
D.s->next=p->next; p->next=s;
(14)在双向链表存储结构中,删除p所指的结点时须修改指针()。
A.p->next->prior=p->prior; p->prior->next=p->next;
B.p->next=p->next->next; p->next->prior=p;
C.p->prior->next=p; p->prior=p->prior->prior;
D.p->prior=p->next->next; p->next=p->prior->prior;
(15)在双向循环链表中,在p指针所指的结点后插入q所指向的新结点,其修改指针的操作是()。
A.p->next=q; q->prior=p; p->next->prior=q; q->next=q;
B.p->next=q; p->next->prior=q; q->prior=p; q->next=p->next;
C.q->prior=p; q->next=p->next; p->next->prior=q; p->next=q;
D.q->prior=p; q->next=p->next; p->next=q; p->next->prior=q;
(16)在长度为n(n>=1)的循环双链表中,删除尾结点的时间复杂度为()。
A.O(1) B.O(n) C.O(n2) D.O(nlog2n)
(1)答案:B
解释:顺序表中的数据连续存储,所以第5个元素的地址为108。
(2)答案:A
解释:顺序表是一种随机存取结构,访问第i个结点和求第i个结点的直接前驱都可以直接通过数组的下标直接定位,时间复杂度是O(1)。
(3)答案:B
(4)答案:A
(5)答案:D
(6)答案:B
解释:链表最大的优点在于插入和删除时不需要移动数据,直接修改指针即可。
(7)答案:C
(8)答案:A
(9)答案:B
(10)答案:D
(11)答案:C
(12)答案:D
(13)答案:D
(14)答案:A
(15)答案:C
(16)答案:A
2.算法设计题
1、假设一个顺序表L中的所有元素为整数,设计一个算法调整该顺序表,使其中所有小于零的元素放在所有大于等于零的元素的前面。
解:先让i、j分别指向顺序表L的第一个元素和最后一个元素。当i<j时循环,i从前向后扫描顺序表L,找大于等于0的元素,j从后向前扫描顺序表L,找小于0的元素,当i<j时将两元素交换。对应的算法如下:
void fun(SqList *&L)
{
int i=0;
int j=L->length-1;
while(i<j)
{
while(L->data[i]<0)
{
i++;
}
while(L->data[j]>=0)
{
j--;
}
if(i<j)
{
swap(L->data[i],L->data[j]);
}
}
}
2、设计一个算法,将一个带头结点的数据域依次为a1、a2、…、an(n≥3)的单链表的所有结点逆置,即第1个结点的数据域变为an,第2个结点的数据域变为an-1…尾结点的数据域变为a1。
解:首先让p指针指向首结点,将头结点的next域设置为空,表示新建的单链表为空表。用p扫描单链表的所有数据结点,将结点p采用头插法插人到新建的单链表中。对应的算法如下:
void Reverse(LinkList*& L)
{
LinkList *p = L->next, *q;
L->next = NULL;
while(p!=NULL)
{
q = p->next;
p->next = L->next; //让p作为头结点插入
L->next = p;
p = q; //让p指向下一结点
}
}
3、一个线性表(a2, a2 ,…, an)(n>3)采用带头结点的单链表L存储。设计一个高效算法求中间位置的元素(n为奇数时对应 n 2 \frac {n}{2} 2n的元素,n为偶数时对应 n + 1 2 \frac {n+1}{2} 2n+1的元素)。
解:让p、q首先指向首结点,然后在p结点后面存在两个结点时循环,p后移两个结点,q后移一个结点。当循环结束时,q指向的就是中间位置结点。
ElemType MinNode(LinkList *& L)
{
LinkList *p = L->next,*q=p;
while(p->next!=NULL && p->next->next!=NULL)
{
p = p->next->next;
q = p->next;
}
return q->data;
}
4、设计一个算法在带头结点的非空单链表L中第一个最大值结点(最大值结点可能有多个)之前插人一个值为x的结点。
解:先在单链表L中查找第一个最大值结点的前驱结点maxpre,然后在其后面插入值为x的结点。
void Insert(LinkList *& L, ElemType x)
{
LinkList *p = L->next, *pre = L;
LinkList *maxp = L->next, *maxpre = L;
while(p!=NULL)
{
if(maxp->data<p->data) //找到更大的
{
maxp = p;
maxpre = pre;
}
pre = p;
p = p->next;
}
s = (LinkNode *)malloc(sizeof(LinkList));
s->data = x;
s->next = maxpre->next;
maxpre->next = s;
}
5、设有一个双链表h,每个结点中除了有prior、data和next几个域以外,还有一个访问频度域freq,在链表被启用之前,其值均初始化为零。每当进行LocateNode(h,x)运算时,令元素值为x的结点中freq域的值加1,并调整表中结点的次序,使其按访问频度的递减次序排列,以便使频繁访问的结点总是靠近表头。试写一个符合上述要求的LocateNode运算的算法。
解:在DLinkNode类型的定义中添加整型freq域,将该域初始化为0。在每次查找到一个结点p时将其freq域增1,再与它前面的一个结点pre进行比较,若p结点的freq域值较大,则两者交换,如此找一个合适的位置。
bool LocateNode(DLinkNode *h, ElemType x)
{
DLinkNode *p = h->next, *pre;
while(p!=NULL && p->data!=x)
{
p = p->next;
}
if(p==NULL)
{
return false;
}
else
{
p->freq++;
pre = p->prior;
while(pre!=h && pre->freq < p->freq)
{
p->prior=pre->prior; //交换结点p和结点pre的位置
p->prior->next=p;
pre->next=p->next;
if(pre->next!=NULL) //若p结点不是尾结点
{
pre->next->prior=pre;
}
p->next=pre;
pre->prior=p;
pre=p->prior; //q指向结点P的前驱结点
}
return true;
}
}
6、在结点个数大于1的循环单链表中,指针p指向其中某个结点,当执行以下程序段后让指针s指向结点p的前驱结点,请填空
s = p;
while(_____)
{
s = s->next;
}
答案:s->next!=p
7、[顺序表算法]设计一个高效算法,将顺序表L中的所有元素逆置,要求算法的空间复杂度为O(1)。
解:扫描顺序表L的前半部分元素,对于元素L->data[i](0≤ilength/2),将其与后半部分对应的元素L->data[L->length-i-1]进行交换。
void reverse(SqList*&L)
{
for(int i=0;i<L->length/2;i++)
{
//交换一半的元素
swap(L->data[i],L->data[L->length-i-1]);
}
}
8、[单链表算法]某非空单链表L中的所有元素为整数,设计一个算法将所有小于零的结点移到所有大于等于零的结点的前面。
解:用p指针扫描单链表L,pre指向其前驱结点。当p不空时循环,若p所指结点的data值小于0,通过pre指针将其从链表中移去,然后将p结点插入到表头,并置p=pre->next;否则,pre、p同步后移一个结点。
void move(LinkNode *& L)
{
LinkNode *p = L->next,*pre = p;
while(p!=NULL)
{
if(p->data < 0)
{
pre->next = p->next; //通过pre指针将p从链表中移去
p->next = L->next; //将p结点插入到表头
L->next = p;
p = pre->next;
}
else
{
pre = p;
p = p->next;
}
}
}