第三章 线性表

引言

  1. 思维导图中,标红的是重点内容,标黄的是次重点。
  2. 本章实验点此查看
  3. 码字不易,如果这篇文章对您有帮助的话,希望您能点赞、收藏、加关注!您的鼓励就是我前进的动力!

知识点思维导图

在这里插入图片描述
(点此查看原图)

补充:

  1. 给函数传参时,若要改动原参数值,则传递指针;若不改动,则传递原参数值。
  2. 顺序存储结构是一种随机存取结构,即能通过首地址和元素序号在时间 O(1) 内找到指定的元素。

注意事项与易错点

  1. 静态链表的关键点:1)下标为0的元素的 cur 值存放备用链表的第一个结点的下标。2)数组最后一个元素的 cur 值存放第一个有数值元素的下标(相当于头结点)。
  2. 循环链表:不用头指针,而是用指向终端结点的尾指针来表示循环链表。
  3. 删除某个结点时,代码的结尾一定要记得释放存储空间。
  4. 双向链表插入、删除的关键就是处理前驱与后继的关系。

题型与算法

一、计算顺序存储结构中数据元素的地址

  1. 公式:
    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)+(i1)c
    其中 L O C ( ) LOC() LOC() 表示获得存储位置的函数。 a i a_i ai表示第 i i i 个数据元素。每个数据元素占 c c c 个存储单元。
  2. 例题:(题目出处
    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)+(51)c=100+(51)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]=539+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])+(2011)1=100+(2011)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. 写代码时,可以先画出数据结构的示意图,来辅助理解。
  2. 要牢记各种类型的数据存储结构,因为插入、删除、修改之类的操作,它们修改的对象就是这些结构体中的成员。

参考资料:
[1] 程杰. 大话数据结构. 北京:清华大学出版社, 2020.
[2]严蔚敏,吴伟民. 数据结构 (C语言版). 北京:清华大学出版社, 1997.

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

沉远

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值