2.0 线性表

线性表的顺序和链式结构
线性链表、循环链表、双向链表

主要内容

线性结构特点
在非空有限集内,存在唯一始末元素、除头全有唯一前驱,除尾全有唯一后继。

2.1 线性表的类型定义
2.2 线性表的顺序表示与实现
2.3 线性表的链式表示与实现
2.3.1 线性链表
2.3.2 循环链表
2.3.3 双向链表

线性表的逻辑结构

定义

一个线性表是n个数据元素的有限序列

特点

1.线性表中所有元素的性质相同。

2.除第一个和最后一个数据元素之外,其它数据元素有且仅有一个前驱和一个后继。第一个数据元素无前驱,最后一个数据元素无后继。

3.数据元素在表中的位置只取决于它自身的序号

ADT

ADT List{
	数据对象:D={ai|ai∈ElemSet,i=1,2,…,n,n≥0}
	数据关系:R={<ai-1,ai>|ai,ai-1∈D,1=2,…,n}
	基本操作:
		InitList( &L);
		DestroyList(&L);
		ClearList(&L);
		ListEmpty(L);
		ListLength(L);
		GetElement(L,i,&e);
		LocateElement(L,e,compare( ))
		PriorElement(L,cur_e,&pre_e)
		NextElement(L,cur_e,&next_e)
		ListInsert(&L,i,e);
		ListDelete(&L,i,&e);
		ListTraverse(L,visit( ))
}ADT List

例题

线性表合并

假设有两个集合A和B分别用两个线性表LA和LB表示,现要求一个新的集合A=A∪B。

void union( List &La, List Lb)
{
    // 将所有在线性表Lb中但不在La中的数据元素插入到La中
	La_Len = ListLength( La ); // 求线性表的长度
	Lb_Len = ListLength( Lb );
	for( i = 1; i <= Lb_Len; i++)
	{
	     GetElem( Lb, i, e); // 取Lb中第i个数据元素赋给e
	     if( !LocateElem( La, e, equal))
	         ListInsert( La, ++La_Len, e); 
             // La中不存在和 e 相同的数据元素,则插入之
       }
} // union

时间复杂度:
O(ListLength( La ) * ListLength( Lb ))

非递减线性表La,Lb的合并

void MergeList( List La, List Lb, List &Lc )
{
	InitList( Lc );
	i = j = 1; // i和j分别是La和Lb的序号
	k = 0;     //k是Lc的序号
	La_Len = ListLength( La );
	Lb_Len = ListLength( Lb );
	while((i <= La_Len) && (j <= Lb_Len))
    {
        GetElem( La, i, ai ); 
        GetElem( Lb, j, bj );
    if( ai < = bj ) {
        ListInsert( Lc, ++k, ai); ++i; 
    	}
	else {
        ListInsert( Lc, ++k, bj ); ++j; 
        }
     }
	while( i <= La_Len ){//若La非空,把La剩余的数据元素插入到Lc中
		GetElem( La, i++, ai );    
		ListInsert( Lc, ++k, ai );
	}
	while( j <= Lb_Len ){//若Lb非空,把La剩余的数据元素插入到Lc中
		GetElem( Lb, j++, bj );      
		ListInsert( Lc, ++k, bj );
	}
}//MergeList

时间复杂度:
O(ListLength(La ) + ListLength( Lb))

线性表的顺序存储结构

顺序表:用一组地址连续的存储单元存放一个线性表

  • 元素地址计算方法
    LOC(ai)=LOC(a1)+(i-1)*L
    L—一个元素占用的存储单元个数
    LOC(ai)—线性表第i个元素的地址
  • 特点
    实现逻辑上相邻—物理地址相邻
    实现随机存取

顺序表的类型定义

#define LIST_INIT_SIZE 100  // 线性表存储空间的初始分配量
#define LISTINCREMENT 10   // 线性表存储空间的分配增量
typedef  struct{
	ElemType *elem;//存储空间基址
	int length;   //当前长度
	int listsize //当前分配的存储容量(以sizeof(ElemType)为单位)
}Sqlist;

顺序表的重要操作

初始化顺序表

Status InitList_Sq(SqList &L)
{ //构造一个空的顺序表L 
	L.elem=(ElemType*)malloc(LIST_INIT_SIZE*sizeof(ElemType));
	if (! L.elem) 
		exit(OVERFLOW);  //存储分配失败
	L.length=0;  //空表长度为0
	L.listsize=LIST_INIT_SIZE; //初始存储容量
	Return OK;
}//InitList_Sq                  

顺序表的插入操作

思路

1.输入是否有效?
2.当前表是否已经满?
3.移动 i 后的元素
4.插入元素
5.表长增1

代码

Status ListInsert_Sq(Sqlist &L, int i, ElemType e)      
{  // 在顺序线性表L的第i个位置之前插入新的元素e
   // i的合法值为1<=i<=ListLength_Sq(L) + 1
   if(i<1 || i>L.length+1) 
   	return ERROR;   //i值不合法
   if(L.length>=L.listsize){ // 当前存储空间已满,增加分配 
   		newbase=(ElemType*)realloc(L.elem,(L.listsize+LISTINCREMENT)*sizeof(ElemType));
         if(!newbase)
         	exit(OVERFLOW); // 存储分配失败
         L.elem=newbase;
         L.listsize+=LISTINCREMENT; // 增加存储容量
   }
   q=&(L.elem[i-1]);    // q为插入位置
   for(p=&(L.elem[L.length-1]); p>=q; --p)
        *(p+1)=*p;   // 插入位置及之后的元素后移
   *q=e;           // 插入e
   ++L.length;    // 表长增1
   return OK;
}                                                                          

顺序表的删除操作

思路

1.输入是否有效?
2.删除(前移元素)
3.表长减1

代码

Status ListDelete_Sq(SqList &L,int i,ElemType &e){
//在顺序线性表L中删除第.i个元素,并用e返回其值       
//i的合法值为  1≤i≤L.length
	if((i<1)||(i>L.Length))
		return ERROR;	// i值不合法或表空       
	p=&(L.elem[i-1]);   //p为被删除元素的位置      
	e=*p;             // 被删除元素的值赋给e       
	q=L.elem+L.length-1; // 表尾元素的位置       
	for (++p; p<=q;++p)  
		*(p-1)=*p;  //被删除元素之后的元素前移       
	--L.length; //表长减1      
	return OK;
}//ListDelete_Sq                                       

顺序表的查找操作

int LocateElem(SqList L,ElemType e, Status(*compare)(ElemType,ElemType)){
    ElemType *p;
    int i=1; // i的初值为第1个元素的位序
    p=L.elem; // p的初值为第1个元素的存储位置
    while(i<=L.length&&!compare(*p++,e))
        ++i;
    if(i<=L.length)
       return i;
    else
       return 0;
 }

顺序表的遍历(函数指针使用说明书)

  • 初始条件:
    顺序线性表L已存在
  • 操作结果:
    依次对L的每个数据元素调用函数vi()。一旦vi()失败,则操作失败。
    若在vi()的形参加'&',表明可通过调用vi()改变元素的值。
Status ListTraverse(SqList ,void(*vi)(ElemType&)){
	ElemType *p;
	int i;
	p=L.elem;
	for(i=1;i<=L.length;i++)
		vi(*p++);
	return OK;
}

顺序表的一般操作

销毁顺序表

Status DestroyList_Sq ( SqList &L) {      
	if (!L.elem) 
		return ERROR;  // 若表L不存在      
	free (L.elem);	   // 若表L已存在,回收动态分配的存储空间
	L.elem = null;      
	L.length = 0;      
	L.Listsize = 0;      
	return OK;
}// DetroyList_Sq

置空线性表

 Status ClearList_Sq ( SqList &L) {      
	if (!L.elem) 
		return ERROR; // 若表L不存在      
	L.length = 0;	//若表L已存在,将L置空      
	return OK;
}// ClearList_Sq

判断空表

Status ListEmpty(SqList L)
 { // 初始条件:顺序线性表L已存在。
   //操作结果:若L为空表,则返回TRUE,否则返回FALSE
	if(L.length==0)
		return TRUE;
	else
		return FALSE;
 }

求表长

int ListLength(SqList L){ 
	//初始条件:顺序线性表L已存在。
	//操作结果:返回L中数据元素个数
	return L.length;
}

取元素操作

Status GetElem_Sq ( SqList L, int i, ElemType &e ) {      
	if((i< 1)||(i>L.length)) 
		return ERROR;  // i 非法      
	e=L.elem[i-1]; //将顺序表中第i 个元素赋值给 e      
	return OK;
}// GetElem_Sq

顺序存储结构的优缺点

优点

  • 逻辑相邻,物理相邻
  • 可随机存取任一元素
  • 存储空间使用紧凑

缺点

  • 插入、删除操作需要移动大量的元素
  • 预先分配空间需按最大空间分配,利用不充分
  • 表容量难以扩充

线性表的链式存储结构

特点

  • 用一组任意的存储单元存储线性表的数据元素
  • 利用指针实现了用不相邻的存储单元存放逻辑上相邻的元素
  • 每个数据元素ai,除存储本身信息外,还需存储其直接后继的信息
  • 结点{
    数据域:元素本身信息
    指针域:指示直接后继的存储位置
    }

线性链表的定义

结点中只含一个指针域的链表叫线性链表,也叫单链表。

typedef struct LNode {
     ElemType data;
     struct LNode  *next;
}Lnode,*LinkList;

LNode* p 和 LinkList p
意思一样,都是建立一个Lnode型的单链表
定义出来的都是1个Lnode型的指针变量,通常用他指向头结点
特别地,注意
LinkList P,Q; /P,Q都是指针 /
LNode P,Q;/只有P是指针* /

  • 头结点
    在单链表第一个结点前附设一个结点叫头结点
    头结点指针域为空表示线性表为空

链表的重要操作

初始化链表

Status InitList_L (LinkList &L) {
	L = (LinkList)malloc(sizeof(LNode));     
	if (!L)
		exit(OVERFLOW);    
	L->next = null;      
	Return OK;
}// InitList_L 

链表的按值查找

Status LocateNode_L(LinkList L,Elemtype key,LNode &e){
	p=L–>next;
	while( p && p–>data!=key)
		p=p–>next;
	if(!p) 
		return ERROR;
	e=p;
	return OK;
}   

链表的插入操作

思路

1.寻找第i-1个结点:
顺指针向后查找,直到p指向第i-1个元素或p->next为空
2.分配新空间
3.插入节点

代码

Status ListInsert_L(LinkList &L, int i, ElemType e){
 //在带头结点的线性链表L中第i元素结点之前插入元素e
	p=L; j=0
	while (p&&j<i-1){
		p=p->next; 
		++j;
	}//☆寻找第i-1个元素结点
	if(!p||j>i-1)
		return ERROR; // i小于1  则 j>i-1
	                  // i大于表长+1 则p为空         
	s=(LinkList)malloc(sizeof(LNode)); //分配新结点
	s->data=e;
	s->next=p->next; p->next=s;   //插入新结点
	return OK;
}//LinstInsert_L

链表的删除操作

思路

1.寻找第i-1个结点:
顺指针向后查找,直到p指向第i-1个元素或p->next为空
2.删除结点(修改其后继指针)
3.回收(释放)结点空间

代码

Status ListDelete_L(LinkList &L, int i, ElemType &e){
	//在带头结点的单链线性表L中,删除第i个元素,并由e返回其值
	p=L; j=0;
	while (p->next&&j<i-1){  //寻找第i-1个结点
		p=p->next; ++j;
	}
	if(!(p->next)||j>i-1)
		return ERROR; //  表中无第i个结点(i不合法)
	                  //  i<1 则 j>i-1
	                  //  i>表长 则 p->next为空
	q=p->next;p->next=q->next;   //删除结点
	e =q->data; free(q);   //  释放结点空间
	return OK;
}//LinstDelete_L                    

链表的一般操作

取元素操作

Status GetElem_L(LinkList L, int i, ElemType &e){
  //L为带头结点的单链表的头指针。
  //当第i个元素存在时,其值赋给e并返回OK,否则返回ERROR
	p=L->next; j=1;  //初始化,p指向第一个结点,j为计数器
	while(p&& j<i){  //顺指针向后查找,直到p指向第i个元素或p为空
		p=p->next;++j;
	}
	if (!p||j>i)
		return ERROR; //第i个元素不存在      
	e=p->data;  //取第i个元素
	return OK;
}//GetElem_L                       

头插法建立单链表

思路

① 建立新节点
② 向新节点中添入内容
③ 使新节点指向链头(第一个元素)
④ 改变头指针,指向新节点

代码

void CreateList_L(LinkList &L, int n) {
	//逆序输入n个元素的值,建立带表头结点的线性链表
	L=(LinkList)malloc(sizeof (LNode));
	L->next=NULL; //先建立一个带头结点的单链表
	for (i=n; i>0;--i){
		p=(LinkList)malloc(sizeof(LNode));//生成新结点
		scanf(&p->data);//输入元素值
		p->next=L->next;L->next=p; //插入到表头/
	}
}//CreateList_L

头插法的幻想图插了一个想不明白就想两个节点

尾插法建立单链表

思路

① 建立新节点
② 向新节点中添入内容
③ 将新节点链入链尾
④ 改变尾指针

代码

void CreateList_L(LinkList &L, int n) {
	//输入n个元素的值,建立带表头结点的线性链表
	L=(LinkList)malloc(sizeof (LNode));
	L->next=NULL; //先建立一个带头结点的单链表
	r=L;
	for (i=1; i<=n;i++){
		p=(LinkList)malloc(sizeof(LNode));//生成新结点
		scanf(&p->data);//输入元素值
		p->next=NULL;
		r->next=p;
		r=p; 
	}
}//CreateList_L

归并2个有序链表

void MergeList_L(LinkList &La, LinkList &Lb, LinkList &Lc){
	pa=La->next; 
	pb=Lb->next;   
	Lc=pc=La;  //用La的头结点作为Lc的头结点     
	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);//释放Lb的头结点
}//MergeList_L                

单链表结构的优缺点

优点

  • 动态结构,整个存储空间为多个链表共用
  • 不需预先分配空间

缺点

  • 指针占用额外存储空间
  • 不能随机存取,查找速度慢

循环链表

  • 在单链表中,将终端结点的指针域NULL改为指向表头结点的或开始结点,就得到了单链形式的循环链表,并简单称为单循环链表。
  • 为了使空表和非空表的处理一致,循环链表中也可设置一个头结点。
  • 由于循环链表中没有NULL指针,故涉及遍历操作时,其终止条件就不再像非循环链表那样判断p或p—>next是否为空,而是判断它们是否等于头指针

双向链表

结构定义

typedef struct DulNode {
     ElemType   data;
     struct DuLNode *prior;
     struct DuLNode *next;
}DuLNode,  *DuLinkList;

插入结点程序

Status ListInsert_DuL(DuLinklist L, int i, ElemType e)
{ 
   DuLinklist s,p;
   if (!(p=GetElemP_DuL(L,i))) 
      return ERROR;      // 在L中确定第i个元素的位置指针p
   if(!(s = (DuLinklist)malloc(sizeof(DuLNode)))) return ERROR;
   s->data = e;                // 构造数据为e的结点s
   s->prior = p->prior;  p-> prior ->next = s;
   s->next = p;        p->prior = s;
   return OK;
}  // ListInsert_DuL

删除结点程序

Status ListDelete_DuL(DuLinklist L, int i, ElemType &e)
{
   DuLinklist p;
   if (!(p=GetElemP_DuL(L,i))) 
        return ERROR;    // 在L中确定第i个元素的位置指针p
   e = p->data;                // 删除的结点的值存入e
   p-> prior ->next = p->next;
   p->next->prior = p->prior;
   free(p);
   return OK;
}  // ListDelete_DuL

带头结点的线性链表类型

具有实用意义的线性链表

 typedef struct LNode // 结点类型
 {
      ElemType data;
      LNode *next;
 }*Link,*Position;

 struct LinkList // 链表类型
 {
      Link head,tail; // 分别指向线性链表中的头结点和最后一个结点
      int len;      // 指示线性链表中数据元素的个数
 };                                                

posted @ 2020-08-14 20:43  夜明_Yoake  阅读(204)  评论(0编辑  收藏  举报