整本书的知识点,点击右方链接:整本书笔记知识点
二、线性表
伪码书上讲的也很详细,笔记中有些就不再打了。可以看后面的案例分析中的完整代码。
2.1、线性表的定义和特点
由n (n≥0)个数据特性相同的元素构成的有限序列称为线性表,(n=0)的时候被称为空表。
2.2、案例引入
略
2.3、线性表的类型定义
2.4、线性表的顺序表示和实现
2.4.1、线性表的顺序表示
线性表的顺序存储又被称为顺序表
顺序存储表示:用一组地址连续的存储单元依次存储线性表的数据元素的方式,具有顺序存储结构的特点(数据间的逻辑关系和物理关系是一致的)
假设线性表 L 存储的起始位置为 LOC(A) ,sizeof(ElemType) 是每个数据元素所占用存储空间的大小,则表 L 所对应的顺序存储如下图:
注意:线性表中的位序是从 1 开始的,而数组中元素的下标是从 0 开始的
线性表的顺序存储结构是一种随机存取的存储结构,即通过首地址和元素序号可以在O(1) 时间内找到指定的元素
由于线性表的长度可变,且所需最大存储空间随问题的不同而不同,在C语言中通常使用动态分配的一维数组表示线性表
//----- 顺序表的存储结构-----
#define MAXSIZE 100 //顺序表可能达到的最大长度
typedef struct {
ElemType *elem; //存储空间的基地址
int length; //当前长度
} SqList; //顺序表的结构类型为SqList
2.4.2、顺序表中基本操作的实现
一、顺序表初始化
①、为顺序表L动态分配一个预定义大小的数组空间,使elem指向这段空间的基地址。
②、将表的当前长度设为0。
Status InitList(SqList &L) {
//构造一个空的顺序表 L
L.elem = new ElemType[MAXSIZE]; //为顺序表分配一个大小为MAXSIZE的数组空间,这是C++语句
if (!L.elem) exit(OVERFLOW); //存储分配失败退出
L.length = O; //空表长度为0
return OK;
}
二、顺序表取值
①、判断指定的位置序号 i 值是否合理 (1≤i≤L.length), 若不合理,则返回ERROR。
②、若 i 值合理,则将第 i 个数据元素 L.elem[i-1] 赋给参数 e, 通过 e返回第 i 个数据元素的传值。
Status GetElem(SqList L, int i, ElemType &e) {
if {
(i < 1 || i > L.length) return ERROR; //判断l. 值是否合理,若不合理, 返回 ERROR
e = L.elem[i - 1]; //elem[i-1] 单元存储第 i 个数据元素
return OK;
}
}
顺序表取值算法的时间复杂度为 :O(1)
三、顺序表的按值查找
①、从第一个元素起,依次和 e相比较,若找到与 e相等的元素 L.elem[i], 则查找成功,返回该元素的序号 i+l (数组下标从 0 开始)
②、若查遍整个顺序表都没有找到,则查找失败, 返回0
int LocateELem(SqList L, ElemType e) {
//在顺序表1中查找值为e的数据元素, 返回其序号
for (i = O; i < L.length; i++) {
if (L.elem[i)==e){
return i + l; //查找成功, 返回序号 i+l
}
}
return O; //查找失败, 返回 0
}
顺序表按值查找算法的平均时间复杂度为 O(n)
四、顺序表插入元素
在第 i 个位置插入一个元素时,需从最后一个元素即第 n 个元素开始,依次向后移动一个位置,直至第 i 个元素。
①、判断插入位置 i 是否合法(i 值的合法范围是 1≤i≤n+ I), 若不合法 则返回 ERROR。
②、判断顺序表的存储空间是否已满,若满则返回 ERROR。
③、将第n个至第 i 个位置的元素依次向后移动一个位置,空出第 i 个位置 ( i =n+l 时无需移动)。
④、将要插入的新元素e放入第i个位置。
⑤、表长加1
Status Listinsert(SqList &L, int i, ElemType e) {
//在顺序表 L 中第 i 个位置之前插入新的元素 e, i值的合法范围是 1...i...L.length+l
if ((i < l) || (i > L.length + l)) {
return ERROR; //i值不合法
}
if (L.length == MAXSIZE) {
return ERROR; //当前存储空间已满
}
for (j = L.length - 1; j >= i - 1; j--) {
L.elem[j + l] = L.elem[j]; //插入位置及之后的元素后移
}
L.elem[i - l] = e; //将新元素e放入第l个位置
++L.length; //表长加1
return OK;
}
顺序表插入算法的平均时间复杂度为O(n)
五、顺序表删除元素
删除第 i 个元素时需将第 i+ 1 个至第 n 个元素依次向前移动一个位置 (i = n 时无需移动)
①、判断删除位置 i 是否合法(合法值为 1 ≤ i ≤n), 若不合法则返回 ERROR。
②、将第 i个至第n个的元素依次向前移动一个位置 (i = n时无需移动)
③、表长减 1
Status ListDelete(SqList &L, int i) {
//在顺序表L中删除第J.个元素,J.值的合法范围是 1.;;i.;;L. length
if ((i < l) || (i > L.length)) {
return ERROR; //i值不合法
}
for (j = i; j <= L.length - 1; j++) {
L.elem[j - 1]=1.elem[j]; //被删除元素之后的元素前移
}
--L.length; //表长减 1
return OK;
}
顺序表删除算法的平均时间复杂度为 O(n)
2.5、线性表的链式表示和实现
链式存储结构的特点:用一组任意的存储单元存储线性表的数据元素(这组存储单元可以是连续的,也可以是不连续的),存放数据元素的结点至少包括两个域(指针域、数据域),也可以包含若干个指针域和数据域。
2.5.1、单链表的定义和表示
关于带头结点的单链表及不带头节点的单链表
- 首元结点:是指链表中存储第一个数据元素的结点。
- 头结点:是在首元素结点之前附设的一个结点,其指针指向首元结点。
- 头指针:是指向链表中第一个结点的指针。若链表设有头结点,则头指针所指结点为线性表的头结点;若链表不设头结点,则头指针所指结点为该线性表的首元结点。
带头结点:
不带头结点:
- 带头结点的单链表为空时:L->next==NULL;
- 不带头结点的单链表为空时:L=NULL;
//----- 单链表的存储结构-----
typedef struct LNode {
ElemType data; //结点的数据域
struct LNode *next; //结点的指针域
} LNode, *LinkList; //LinkList 为指向结构体 LNode 的指针类型
2.5.2、单链表基本操作的实现
一、单链表的初始化
①、生成新结点作为头结点,用头指针L 指向头结点。
②、头结点的指针域置空。
Status InitList(LinkList &L) { //构造一个空的单链表L
L = new LNode; //生成新结点作为头结点,用头指针L指向头结点
L->next = NULL; //头结点的指针域置空
return OK;
}
二、单链表的取值
①、用指针p指向首元结点,用 j 做计数器初值赋为1
②、从首元结点开始依次顺着链域 next 向下访问,只要指向当前结点的指针 p 不为空(NULL), 并且没有到达序号为 i 的结点,则循环执行以下操作:
- p指向下一个结点;
- 计数器 j 相应加 1
③、退出循环时, 如果指针p为空, 或者计数器 j 大于 i, 说明指定的序号 i 值不合法(i 大于表长n或 i 小于等于0), 取值失败返回ERROR; 否则取值成功, 此时 j=i 时,p所指的结点就是要找的第 i 个结点,用参数 e 保存当前结点的数据域, 返回OK。
Status GetElem(LinkList L, int i, ElemType &e) {
//在带头结点的单链表L中根据序号l.获取元素的值,用e返回L中第l.个数据元素的值
p = L->next;
j = 1; //初始化,p指向首元结点,计数器]初值赋为1
while (p && j < i) { //顺链域向后扫描,直到p为空或p指向第l.个元素
p = p->next; //p指向下一个结点
++j; //计数器j相应加1
}
if (!p || j > i) return ERROR; // i值不合法 i>n或i≤0
e = p->data; //取第i个结点的数据域
return OK;
}
单链表取值算法的平均时间复杂度为 O(n)
三、单链表的按值查找
①、用指针p指向首元结点
②、从首元结点开始依次顺着链域next向下查找, 只要指向当前结点的指针p不为空, 并且p所指结点的数据域不等于给定值e, 则循环执行以下操作: p指向下一个结点 。
③、返回p。若查找成功,p此时即为结点的地址值,若查找失败,p的值即为NULL
LNode *LocateELem(LinkList L, Elemtype e) {
//在带头结点的单链表L中查找值为e的元素
p = L->next; //初始化,p指向首元结点
while (p && p->data != e) { //顺链域向后扫描,直到p为空或p所指结点的数据域等于e
p = p->next; //p指向下一个结点
}
return p; //查找成功返回值为e的结点地址p, 查找失败p为NULL
}
单链表的按值查找算法的平均时间复杂度为 O(n)
四、单链表的插入
将值为e的新结点插入到表的第 i 个结点的位置上,即插入到结点 ai-1与ai之间。
①、查找结点 ai-1 并由指针p指向该结点
②、生成一个新结点*s
③、将新结点*s 的数据域置为 e
④、将新结点*s 的指针域指向结点ai
⑤、将结点 * p 的指针域指向新结点*s
Status Listinsert(LinkList &L, int i, ElemType e) {
//在带头结点的单链表L中第i个位置插入值为e的新结点
p = L;
j = O;
while (p && (j < i - 1)) {
p = p->next;
++j; //查找到第i-1个结点,p指向该结点
}
if (!p || j > i - 1) return ERROR; //i>n+1 或者 i≤1
s = new LNode; //生成新结点 *s
s->data = e; //将结点*s的数据域置为e
s->next = p->next; //将结点 *s的指针域指向结点 ai
p->next = s; //将结点*p的指针域指向结点*s
return OK;
}
单链表插入算法的时间复杂度为:O(n)
五、单链表的删除
删除单链表的第 i 个结点ai
①、查找结点ai-1 并由指针p指向该结点
②、临时保存待删除结点 ai 的地址在q中 ,以备释放。
③、将结点*p的指针域指向 ai 的直接后继结点
④、释放结点ai的空间
Status ListDelete(LinkList &L, int i) {
//在带头结点的单链表L中,删除第l个元素
p = L;
j = O;
while ((p->next) && (j < i - 1)) { //查找第i-1个结点,p指向该结点
p = p->next;
++j;
}
if (!(p->next) || (j > i - 1)) return ERROR; //当i>n或i<1时,删除位置不合理
q = p->next; //临时保存被删结点的地址以备释放
p->next = q->next; //改变删除结点前驱结点的指针域
delete q; //释放删除结点的空间
return OK;
}
单链表删除算法的时间复杂度为: O(n)
六、创建单链表
前插法:始终让新结点在第一的位置,不常用,因为输入顺序和输出顺序是相反的。
后插法:尾指针r始终指向单链表的表尾,常用(输入顺序和输出顺序相同)
①、创建一个只有头结点的空链表。
②、尾指针r初始化, 指向头结点。
③、根据创建链表包括的元素个数n, 循环n次执行以下操作
- 生成一个新结点*p;
- 输入元素值赋给新结点*p 的数据域;
- 将新结点p 插入到尾结点r之后;
- 尾指针r指向新的尾结点*p
2.5.3、循环链表
2.5.4、双向链表
双向链表的插入:
双向链表的删除:
2.6、顺序表和链表的比较
2.7、线性表的应用
略
2.8、案例分析与实现
线性表的顺序实现案例:顺序表案例
线性表的链式实现案例:单链表案例
第二章小结
第二章习题
(1)顺序表中第一个元素的存储地址是100,每个元素的长度为2,则第5个元素的地址是( )。
A.110 B.108 C.100 D.120
答案:B
解释:顺序表中的数据连续存储,所以第5个元素的地址为:100+2*4=108
(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个结点从小到大排序
答案:A
解释:在顺序表中插入一个结点的时间复杂度都是O(n2),排序的时间复杂度为O(n2)或O(nlog2n)。
顺序表是一种随机存取结构,访问第 i 个结点和求第 i 个结点的直接前驱都可以直接通过数组的下标直接定位,时间复杂度是O(1)
(3) 向一个有127个元素的顺序表中插入一个新元素并保持原来顺序不变,平均要移动 的元素个数为( )。
A.8 B.63.5 C.63 D.7
答案:B
解释:顺序表中插入元素,平均要移动的元素个数为:n/2
(4)链接存储的存储结构所占存储空间( )。
A.分两部分,一部分存放结点值,另一部分存放表示结点间关系的指针
B.只有一部分,存放结点值
C.只有一部分,存储表示结点间关系的指针
D.分两部分,一部分存放结点值,另一部分存放结点所占单元数
答案:A
(5)线性表若采用链式存储结构时,要求内存中可用存储单元的地址( )。
A.必须是连续的 B.部分地址必须是连续的
C.一定是不连续的 D.连续或不连续都可以
答案:D
(6)线性表L在( )情况下适用于使用链式结构实现。
A.需经常修改L中的结点值 B.需不断对L进行删除插入
C.L中含有大量的结点 D.L中结点结构复杂
答案:B
解释:链表最大的优点在于插入和删除时不需要移动数据,直接修改指针即可
(7)单链表的存储密度( )。
A.大于1 B.等于1 C.小于1 D.不能确定
答案:C
解释:存储密度是指一个结点数据本身所占的存储空间和整个结点所占的存储空间之比,假设单链表一个结点本身所占的空间为D,指针域所占的空间为N,则存储密度为:D/(D+N),一定小于1。
(8)将两个各有n个元素的有序表归并成一个有序表,其最少的比较次数是( )。
A.n B.2n-1 C.2n D.n-1
答案:A
解释:当第一个有序表中所有的元素都小于(或大于)第二个表中的元素,只需要用第二个表中的第一个元素依次与第一个表的元素比较,总计比较n次。
(9)在一个长度为n的顺序表中,在第i个元素(1≤i≤n+1)之前插入一个新元素时须向后移动( )个元素。
A.n-i B.n-i+1 C.n-i-1 D.I
答案:B
(10) 线性表L=(a1,a2,……an),下列说法正确的是( )。
A.每个元素都有一个直接前驱和一个直接后继
B.线性表中至少有一个元素
C.表中诸元素的排列必须是由小到大或由大到小
D.除第一个和最后一个元素外,其余每个元素都有一个且仅有一个直接前驱和直接后继。
答案:D
第一个数据元素无直接前驱,最后一个数据元素无直接后继
线性表定义中定义n = 0 时称为线性表中的空表
元素可以随机排列
(11) 创建一个包括n个结点的有序单链表的时间复杂度是( )。
A.O(1) B.O(n) C.O(n2) D.O(nlog2n)
答案:C
解释:单链表创建的时间复杂度是O(n),而要建立一个有序的单链表,则每生成一个新结点时需要和已有的结点进行比较,确定合适的插入位置,所以时间复杂度是O(n2)。
(12) 以下说法错误的是( )。
A.求表长、定位这两种运算在采用顺序存储结构时实现的效率不比采用链式存储结构时实现的效率低
B.顺序存储的线性表可以随机存取
C.由于顺序存储要求连续的存储区域,所以在存储管理上不够灵活
D.线性表的链式存储结构优于顺序存储结构
答案: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;
答案:D
(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;
答案:A
(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;
答案:C