引言
- 思维导图中,标红的是重点内容,标黄的是次重点。
- 本章实验点此查看。
- 码字不易,如果这篇文章对您有帮助的话,希望您能点赞、收藏、加关注!您的鼓励就是我前进的动力!
知识点思维导图
补充:
- 给函数传参时,若要改动原参数值,则传递指针;若不改动,则传递原参数值。
- 顺序存储结构是一种随机存取结构,即能通过首地址和元素序号在时间 O(1) 内找到指定的元素。
注意事项与易错点
- 静态链表的关键点:1)下标为0的元素的 cur 值存放备用链表的第一个结点的下标。2)数组最后一个元素的 cur 值存放第一个有数值元素的下标(相当于头结点)。
- 循环链表:不用头指针,而是用指向终端结点的尾指针来表示循环链表。
- 删除某个结点时,代码的结尾一定要记得释放存储空间。
- 双向链表插入、删除的关键就是处理前驱与后继的关系。
题型与算法
一、计算顺序存储结构中数据元素的地址
- 公式:
L O C ( a i ) = L O C ( a 1 ) + ( i − 1 ) ∗ c \boxed{LOC(a_i)=LOC(a_1)+(i-1)*c} LOC(ai)=LOC(a1)+(i−1)∗c
其中 L O C ( ) LOC() LOC() 表示获得存储位置的函数。 a i a_i ai表示第 i i i 个数据元素。每个数据元素占 c c c 个存储单元。 - 例题:(题目出处)
1) 假设顺序表第 1 个元素的内存地址是 100,每个元素占用 2 字节内存空间,则第 5 个元素的内存地址是?
已知: L O C ( a 1 ) = 100 , c = 2 , i = 5 LOC(a_1)=100, c=2, i=5 LOC(a1)=100,c=2,i=5。
求: L O C ( a 5 ) LOC(a_5) LOC(a5)。
解:
代入公式得: L O C ( a 5 ) = L O C ( a 1 ) + ( 5 − 1 ) ∗ c = 100 + ( 5 − 1 ) ∗ 2 = 108 LOC(a_5)=LOC(a_1)+(5-1)*c=100+(5-1)*2=108 LOC(a5)=LOC(a1)+(5−1)∗c=100+(5−1)∗2=108
2) 已知二维数组 A 按行优先方式存储,每个元素占用 1 个存储单元。若元素 A[0][0] 的存储地址是 100,A[3][3] 的存储地址是 220,则元素 A[5][5] 的存储地址是?
已知:
L
O
C
(
A
[
0
]
[
0
]
)
=
100
,
L
O
C
(
A
[
3
]
[
3
]
)
=
220
,
c
=
1
LOC(A[0][0] )=100,LOC(A[3][3] )=220, c=1
LOC(A[0][0])=100,LOC(A[3][3])=220,c=1。
求:
L
O
C
(
A
[
5
]
[
5
]
)
LOC(A[5][5])
LOC(A[5][5])。
解:
由
L
O
C
(
A
[
3
]
[
3
]
)
=
L
O
C
(
A
[
0
]
[
0
]
)
+
(
i
A
[
3
]
[
3
]
−
1
)
∗
1
LOC(A[3][3])=LOC(A[0][0])+(i_{A[3][3]}-1)*1
LOC(A[3][3])=LOC(A[0][0])+(iA[3][3]−1)∗1 得:
i
A
[
3
]
[
3
]
=
121
i_{A[3][3]}=121
iA[3][3]=121
分析知:该二维数组每行有39个元素。所以
i
A
[
5
]
[
5
]
=
5
∗
39
+
6
=
201
i_{A[5][5]}=5*39+6=201
iA[5][5]=5∗39+6=201
代入公式得:
L
O
C
(
A
[
5
]
[
5
]
)
=
L
O
C
(
A
[
0
]
[
0
]
)
+
(
201
−
1
)
∗
1
=
100
+
(
201
−
1
)
∗
1
=
300
LOC(A[5][5]) = LOC(A[0][0]) + (201-1)*1 = 100 + (201 - 1) * 1 = 300
LOC(A[5][5])=LOC(A[0][0])+(201−1)∗1=100+(201−1)∗1=300
注意:
①这是二维数组,有第 0 行和第 0 列,算每行元素个数和元素序数时要注意;
②注意是按行优先方式存储。
二、重要算法
(一)顺序表
顺序存储数据结构与初始化
#define MAXSIZE 20//存储空间初始分配量
typedef int ElemType;//ElemType(元素类型)根据实际情况定,此处为int
typedef struct
{
ElemType data[MAXSIZE];//数组,存储数据元素
int length;//线性表当前长度(线性表中元素的个数)
}SqList;
Status InitList_Sq( SqList& L ) {
L.elem = (ElemType*) malloc (n * sizeof (ElemType));
if (L.elem==0) exit(OVERFLOW);
L.length = 0;
L.listsize = LIST_INIT_SIZE;
return OK;
}
1)算法思路:包含顺序存储结构的三个属性。
2)可以通过将「typedef int ElemType;」中 int 改为其他数据类型,来改变 ElemType 的数据类型。
获得元素操作(了解)
#define OK 1
#define ERROR 0
//Status 是函数的返回值类型(即 OK 或 ERROR )
typedef int Status;
//初始条件:L已存在,i 在下标范围内
//操作结果:用 e 返回第 i 个数据元素的值
Status GetElem(SqList L,int i, ElemType *e)
{
if ( L.length == 0 || i < 0 || i > L.length )
return ERROR;
*e=L.data[i-1];
return OK;
}
1)算法思路:当 i (i 为元素序数,即下标加 1) 的范围在数组下标范围内,返回下标为 i-1 的值即可。
插入操作
//初始条件:L已存在,i 在下标范围内。
//操作结果:在 L 中第 i 个位置之前插入 e,表长加一。
Status ListInsert(SqList *L, int i, ElemType e)
{
int k;
//当插入位置不合理或线性表已满,抛出异常。
if ( L-> length == MAXSIZE || i < 0 || i > L->length+1 )
return ERROR;
//若要插入的数据不在表尾,从后往前遍历到第 i 个位置,并将其后移一位。
if ( i <= L -> length + 1 )
{
for ( k = L -> length - 1; k>=i-1; k-- )
L -> data[k + 1] = L -> data[k];
}
//插入数据,表长加一。
L-> data[i-1] = e;
L -> length++;
return OK;
}
1)算法思路:①当插入位置不合理或线性表已满,抛出异常。 ②从后往前遍历到第 i 个位置,并将其后移一位。 ③插入元素到位置 i。 ④表长加一。
2)注意元素下标等于元素序数 -1。
3)此函数要改变顺序线性表的值,故向函数传递的是 L 的地址。
删除操作
//初始条件:L已存在,i 在下标范围内。
//操作结果:删除第 i 个数据元素,并用 e 返回其值,表长减一。
Status ListDelete(SqList *L,int i,ElemType *e)
{
int k;
//若删除位置不合理或表为空,抛出异常。
if ( L-> length == 0 || i < 0 || i > L->length )
return ERROR;
//取出删除元素。
*e = L->data[i-1];
//从前往后遍历,前移删除元素后的元素。
if(i < L->length )
{
for( k = i-1;k < L->length; k++ )
L->data[k] = L->data[k+1];
}
//表长减一。
L->length--;
return OK;
}
1)算法思路:①若删除位置不合理或表为空,抛出异常。②取出删除元素。③从前往后遍历,前移删除元素后的元素。④表长减一。
检索操作
//查找第1个与e满足compare()的元素的位序
int LocateElem_Sq(SqList L,ElemType e,Status(*compare (ElemType, ElemType)){
int i=1; //第1个元素的位序
ElemType p=L.elem; //第1个元素的存储位置
while(i<=L.length && !(*compare)(*p++,e))
++i;
if(i<=L.length)
return i;
else
return 0;
}
1)「 !(* compare)(* p++,e) 」表示 p 和 e 不相等。
合并操作
//La,Lb非递减有序,合并到Lc
void MergeList(SqList La, SqList Lb, SqList &Lc){
int i=0,j=0,k=0;
//如果Lc大小不够,就在分配空间
if(La.length+Lb.length > Lc.listsize) {
Lc.elem = (ElemType*)realloc(Lc.elem,
(La.length+Lb.length)*sizeof(ElemType));
}
//逐元素插入Lc
while( i<La.length && j<Lb.length ){
if(La.elem[i]<=Lb.elem[j])
Lc.elem[k++]=La.elem[i++];
else
Lc.elem[k++]=Lb.elem[j++];
}
//插入La中剩余元素
while(i<La.length)
Lc.elem[k++]=La.elem[i++];
//插入Lb中剩余元素
while(j<Lb.length)
Lc.elem[k++]=Lb.elem[j++];
Lc.length=k;
}
1)注意顺序表Lc要用引用。
2)结尾不要忘了修改Lc的长度。
(二)单链表
链式存储数据结构
typedef struct Node
{
ElemType data;
struct Node *next;
}Node;
typedef struct Node *LinkList;//定义linkList。
1)算法思路:要包括数据域和指针域。
读取操作
//初始条件:L已存在,i 在下标范围内。
//操作结果:用 e 返回第 i 个数据元素的值
Status GetElem(LinkList L, int i, ElemType *e)
{
int j=1;//初始化计数变量 j 。
LinkList p;//声明指针 p 指向链表第一个结点。
p=L->next;
while(p&&j<i)//当p不为空且j<i时,遍历链表,p 指针后移。
{
p=p->next;
j++;
}
if(!p||j>i)//若p 为空或j>i,说明没有找到,,返回异常;
return ERROR;
*e=p->data;//否则找到了,返回点p的数据。
return OK;
}
1)算法思路:①声明指针 p 指向链表第一个结点,初始化计数变量 j 。②当p不为空且j<i时,遍历链表,p 指针后移。③若p 为空或j>i,说明没有找到,返回异常;否则找到了,返回点p的数据。
2)LinkList等于Node* , 要用->不能用圆点运算符。
3)「 if ( !p || j>i ) 」中要用 !p 而不是 p ,因为如果p为空,说明条件为假,则不执行 if 中的语句。所以要用 !p ,这样p为假时, if 中的语句才会执行。
插入操作
//初始条件:L已存在,i 在下标范围内。
//操作结果:在 L 中第 i 个位置之前插入 e,表长加一。
Status ListInsert(LinkList *L, int i, ElemType e)
{
int j=1;//初始化计数变量 j 。
LinkList p,s;//声明指针 p 指向链表第一个结点。
p=*L;
//查找第 i 个元素
while(p&&j<i)
{
p=p->next;
j++;
}
if(!p||j>i)
return ERROR;
//找到后,分配存储空间,进行插入操作,完成。
s=(LinkList)malloc(sizeof(Node));
s->data=e;
s->next=p->next;
p->next=s;
return OK;
}
1)不要将「s->next=p->next;」和「p->next=s;」两句话的顺序弄反了。
2)算法思路:①声明指针 p 指向链表第一个结点,初始化计数变量 j 。②查找第 i 个元素,找不到返回异常。(这一步同查找)③找到后,分配存储空间,进行插入操作,完成。
删除操作
//初始条件:L已存在,i 在下标范围内。
//操作结果:删除第 i 个数据元素,并用 e 返回其值,表长减一。
States ListDelete(LinkList *L, int i, ElemType *e)
{
int j=1;//初始化计数变量 j 。
LinkList p,q;//声明指针 p 指向链表第一个结点。
p=*L;
//查找第 i 个元素
while(p->next && j<i)
{
p=p->next;
j++;
}
if( !(p -> next) || j>i )
return ERROR;
//找到后,调整结点后继关系,收回存储空间,完成。
q=p->next;
p->next=q->next;
*e=q->data;
free(q);
return OK;
}
1)算法思路:①声明指针 p 指向链表第一个结点,初始化计数变量 j 。②查找第 i 个元素,找不到返回异常。(这一步同查找)③找到后,调整结点后继关系,收回存储空间,完成。
清空单链表
//将单链表重新置为一个空表
void ClearList(LinkList &L) {
//每次删除第一个结点,直到删完
LinkList p;
while (L->next){
p=L->next;
L->next=p->next;
free(p);
}
}
单链表的整表创建
//头插法
//随机产生n个元素的值,建立带表头的单链表(头插法)
void CreatListHead(LinkList *L, int n)
{
//声明指针p和计数器变量 i ,初始化一空链表,头结点指向NULL。
LinkList p;
int i;
srand(time(0));
*L=(LinkList)malloc(sizeof(Node));
(*L)->next=NULL;
//循环:分配空间,数据域赋值,插入新节点。
for(i=0;i<n;i++)
{
p=(LinkList)malloc(sizeof(Node));
p->data=rand()%100+1;
p->next=(*L)->next;
(*L)->next=p;
}
}
1)算法思路:①声明指针p和计数器变量 i ,初始化一空链表,头结点指向NULL。②循环:分配空间,数据域赋值,插入新节点。
//尾插法
//随机产生n个元素的值,建立带表头的单链表(尾插法)
void CreatListTail(LinkList *L, int n)
{
//声明指针p和计数器变量 i ,初始化一空链表,头结点指向NULL。
LinkList p,r;
int i;
srand(time(0));
*L=(LinkList)malloc(sizeof(Node));
r=*L;
//循环:分配空间,数据域赋值,插入新节点。
for(i=0;i<n;i++)
{
p=(Node *)malloc(sizeof(Node));
p->data=rand()%100+1;
r->next=p;//表尾结点指向(下一个循环的)新节点
r=p;//将当前的新节点定义为表尾终端节点
}
r->next=NULL;//表示当前链表结束
}
1)「 (*L)->next=p; 」中 *L 要带括号。
2)为什么「 r->next=p;」指向的是下一个循环的新节点: p 在 「 r=p;」时,将此次循环产生的结点 p 赋值给了 r ,存入链表中。此时「 r->next」指向的是 r 自己。但开始下一次循环时,新的结点被赋值给了 p ,此时「 r->next」指向的就是 p 所代表的新结点了。
合并两个单链表
//La和Lb按非递减排序,合并到Lc非递减
void MergeList_L(LinkList &La, LinkList &Lb, LinkList &Lc){
LinkList pa,pb,pc;
//初始化
pa=La->next;
pb=Lb->next;
Lc=pc=La;
//逐元素插入(谁小先插入谁)
while(pa&&pb){
if(pa->data<=pb->data){
pc->next=pa;
pc=pa;
pa=pa->next;
}
else{
pc->next=pb;
pc=pb;
pb=pb->next;
}
}
pc->next=pa?pa:pb;
free(Lb);
}
1)无论是合并顺序表还是合并单链表,其主要思想都是逐个比较元素,谁小先插入谁(或谁大先插入谁)。
2)单链表逐个比较元素插入时,注意赋值语句的次序。
(三)静态链表
静态链表的数据结构
#define MAXSIZE 1000
typedef struct{
ElemType data; //存储数据元素
int cur; //游标
}component,SLinkList[MAXSIZE];
1)针对不设“指针” 的编程语言,用一维数组来表示这种情况下的链表。
2)用数组的一个分量表示一个结点,同时用游标cur代替指针指示结点在数组的相对位置。
3)关键点:①下标为0的元素的cur存放备用链表第一个结点的下标; ②数组最后一个元素的cur存放第一个有数值元素的下标(相当于单链表的头结点); ③cur存储数组下标的值,作用相当于next指针域。
静态链表的初始化操作(了解)
/* 将一维数组space中各分量链成一个备用链表,space[0].cur为头指针,"0"表示空指针 */
Status InitList(StaticLinkList space)
{
int i;
for (i=0; i<MAXSIZE-1; i++)
space[i].cur = i+1;
space[MAXSIZE-1].cur = 0; /* 目前静态链表为空,最后一个元素的cur为0 */
return OK;
}
静态链表的插入操作(了解)
/* 在L中第i个元素之前插入新的数据元素e */
Status ListInsert(StaticLinkList L, int i, ElemType e)
{
int j, k, l;
k = MAXSIZE - 1; /* 注意k首先是最后一个元素的下标 */
if (i < 1 || i > ListLength(L) + 1)
return ERROR;
j = Malloc_SSL(L); /* 获得空闲分量的下标 */
if (j)
{
L[j].data = e; /* 将数据赋值给此分量的data */
for(l = 1; l <= i - 1; l++) /* 找到第i个元素之前的位置 */
k = L[k].cur;
L[j].cur = L[k].cur; /* 把第i个元素之前的cur赋值给新元素的cur */
L[k].cur = j; /* 把新元素的下标赋值给第i个元素之前元素的ur */
return OK;
}
return ERROR;
}
/* 若备用空间链表非空,则返回分配的结点下标,否则返回0 */
int Malloc_SSL(StaticLinkList space)
{
int i = space[0].cur; /* 当前数组第一个元素的cur存的值 */
/* 就是要返回的第一个备用空闲的下标 */
if (space[0]. cur)
space[0]. cur = space[i].cur; /* 由于要拿出一个分量来使用了, */
/* 所以我们就得把它的下一个 */
/* 分量用来做备用 */
return i;
}
/* 初始条件:静态链表L已存在。操作结果:返回L中数据元素个数 */
int ListLength(StaticLinkList L)
{
int j=0;
int i=L[MAXSIZE-1].cur;
while(i)
{
i=L[i].cur;
j++;
}
return j;
}
静态链表的删除操作(了解)
/* 删除在L中第i个数据元素 */
Status ListDelete(StaticLinkList L, int i)
{
int j, k;
if (i < 1 || i > ListLength(L))
return ERROR;
k = MAXSIZE - 1;
for (j = 1; j <= i - 1; j++)
k = L[k].cur;
j = L[k].cur;
L[k].cur = L[j].cur;
Free_SSL(L, j);
return OK;
}
/* 将下标为k的空闲结点回收到备用链表 */
void Free_SSL(StaticLinkList space, int k)
{
space[k].cur = space[0].cur; /* 把第一个元素的cur值赋给要删除的分量cur */
space[0].cur = k; /* 把要删除的分量下标赋值给第一个元素的cur */
}
(四)循环链表
循环链表的合并操作(了解)
p=rearA->next; /* 保存A表的头结点,即① */
rearA->next=rearB->next->next; /* 将本是指向B表的第一个结点(不是头结点)赋值给reaA->next,即② */
q=rearB->next;
rearB->next=p; /* 将原A表的头结点赋值给rearB->next,即③ */
free(q); /* 释放q */
1)关键点:不用头指针,而是将单链表中终端节点的指针端由空指针改为指向头结点。
(五)双向链表
静态链表的相关操作(了解)
//线性表的双向链表存储结构
typedef struct DulNode
{
ElemType data;
struct DuLNode *prior; /*直接前驱指针*/
struct DuLNode *next; /*直接后继指针*/
} DulNode, *DuLinkList;
//插入操作(在结点p后插入s)
s - >prior = p; /*把p赋值给s的前驱*/
s -> next = p -> next; /*把p->next赋值给s的后继*/
p -> next -> prior = s; /*把s赋值给p->next的前驱*/
p -> next = s; /*把s赋值给p的后继*/
//删除操作(删除结点p)
p->prior->next=p->next; /*把p->next赋值给p->prior的后继*/
p->next->prior=p->prior; /*把p->prior赋值给p->next的前驱*/
free(p); /*释放结点*/
1)特点:双向链表是在单链表的每个结点中,再设置一个指向其前驱结点的指针域。
2)注意插入操作语句的次序。
(六)一元多项式的表示及相加
一元多项式的数据结构
//结点的数据元素类型定义为
typedef struct { //项的表示
float coef; //系数
int expn; //指数
} term, ElemType;
//用带表头结点的有序链表表示多项式
typedef LinkList polynomial;
Typedef struct LNode {
ElemType data; //数据域
struct LNode *next; //指针域
}LNode, *LinkList;
一元多项式的相加(了解)
//多项式相加,将和保存到pa中
void AddPolyn( polynomial &pa, polynomial &pb ){
//ha和hb分别指向Pa和Pb的头结点
ha=GetHead(pa);
hb=GetHead(pb) ;
//qa和qb分别指向ha和hb之后的结点
qa=NextPos(pa, ha);
qb=NextPos(pb, hb);
while(qa && qb ) { //qa和qb均非空
//a和b为两表中当前比较元素
a=GetCurElem(qa);
b=GetCurElem(qb);
if(a.expn<b.expn){ //qa当前指数小于qb当前指数
ha=qa;
qa=NextPos(pa, qa);
}
else if(a.expn>b.expn){ //qa当前指数大于qb当前指数
DelFirst(hb,qb);
InsFirst(ha,qb);
qb=NextPos(pb, hb);
ha=NextPos(pa, ha);
}
else{ //qa当前指数等于qb当前指数
sum=a.coef+b.coef;
if (sum!=0){ //修改qa当前结点系数
SetCurElem(qa,sum);
ha=qa;
}
else { //删除qa当前结点
DelFirst(ha,qa);
FreeNode(qa);
}
DelFirst(hb,qb);
FreeNode(qb);
qb=NextPos(pb,hb);
qa=NextPos(pa,ha);
}
}
if (!ListEmpty(pb))
Append(pa, qb); //链接pb剩余结点
FreeNode(hb); //释放pb头结点
}
方法心得
- 写代码时,可以先画出数据结构的示意图,来辅助理解。
- 要牢记各种类型的数据存储结构,因为插入、删除、修改之类的操作,它们修改的对象就是这些结构体中的成员。
参考资料:
[1] 程杰. 大话数据结构. 北京:清华大学出版社, 2020.
[2]严蔚敏,吴伟民. 数据结构 (C语言版). 北京:清华大学出版社, 1997.