Bootstrap

数据结构——线性表(C语言)

线性表的定义和特点

1.定义:由n(n≥0)个数据特性相同的元素构成的有限序列称为线性表
说明:线性结构的基本特点是除第一个元素无直接前驱,最后一个元素无直接后继之外,其他每个元素都有一个前驱和后继。
线性表中元素的个数n(n≥0)定义为线性表的长度,n=0时称为空表

2.对于非空的线性表或线性结构,其特点是:

  • 存在唯一的一个被称作“第一个”的数据元素;
  • 存在唯一的一个被称作“最后一个”的数据元素;
  • 除第一个之外,结构中的每个数据元素均只有一个前驱;
  • 除最后一个之外,结构中的每个数据元素均只有一个后继。

线性表的顺序表示和实现

线性表的顺序存储表示

线性表的顺序表示指的是用一组地址连续的存储单元依次存储线性表的数据元素,这种表示也称作线性表的顺序存储结构或顺序映像。通常,称这种存储结构的线性表为顺序表。其特点是,逻辑上相邻的数据元素,其物理次序也是相邻的。
假设线性表的每个元素需占用 l 个存储单元,并以所占的第一个单元的存储地址作为数据元素的存储起始位置。则线性表中第 i+1 个数据元素的存储位置LOC( a i + 1 a_{i+1} ai+1)和第 i 个数据元素的存储位置LOC( a i a_i ai)之间满足下列关系:
L O C ( a i + 1 ) = L O C ( a i ) + 1 LOC(a_{i+1})=LOC(a_i)+1 LOC(ai+1)=LOC(ai)+1
一般来说,线性表的第 i 个数据元素 a i a_i ai的存储位置为:
L O C ( a i ) = L O C ( a 1 ) + ( i − 1 ) × l LOC(a_i)=LOC(a_1)+(i-1)×l LOC(ai)=LOC(a1)+(i1)×l
式中,LOC( a 1 a_1 a1)是线性表的第一个数据元素 a 1 a_1 a1的存储位置,通常称作线性表的起始位置或基地址。

顺序表中基本操作的实现

1.初始化
为顺序表分配一预定义大小的数组空间,并将线性表的当前长度设为 0。

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

2.取值
判断指定的位置序号 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;  //判断i值是否合理,若不合理,返回ERROR
	e=L.elem[i-1];   //elem[i-1]单元存储第i个数据元素
	return OK;
}

算法的时间复杂度为O(1)。
3.查找
从第一个元素起,依次和 e 相比较,若找到与 e 相等的元素L.elem[i],则查找成功,返回该元素的序号 i+1;若查遍整个顺序表都没有找到,则查找失败,返回0。

int LocateElem(Sqlist l, ElemType e)
{//在顺序表L中查找值为e的数据元素,返回其序号
	for(i = 0 ; i < L.length ; i++)
		if(L.elem[i] == e) return i+1;   //查找成功,返回序号i+1
	return 0;    //查找失败,返回0
}

平均查找长度:
A S L = 1 n ∑ i = 1 n i = n + 1 2 ASL=\frac{1}{n}\displaystyle\sum_{i=1}^{n} i=\frac{n+1}{2} ASL=n1i=1ni=2n+1
算法的平均时间复杂度为O(n)。
4.插入

  • 检查 i 值是否超出所允许的范围 (1≤i≤n+1) ,若超出, 则进行“超出范围”错误处理;
  • 将线性表的第 i 个元素和它后面所有元素均后移一个位置;
  • 将新元素写入到空出的第 i 个位置上;
  • 使线性表的长度增 1。
Status ListInsert_Sq(SqList &L, int i, ElemType e) 
{// 在顺序表L的第 i 个元素之前插入新的元素e,i的合法范围为1≤i≤L.length+1
	if (i < 1 || i > L.length+1) return ERROR; // 插入位置不合法
	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; // 增加存储容量
    for(j=L.length-1;j>=i-1;j--)
    	L.elem[j+1]=L.elem[j];  //插入位置及之后的元素后移
    L.elem[i-1]=e;
    ++L.length;   //将新元素e放入第i个位置
    return OK;    //表长加1

所需移动元素的期望值(平均次数)为:
E i n s = 1 n + 1 ∑ i = 1 n + 1 ( n − i + 1 ) = n 2 E_{ins}=\frac{1}{n+1}\sum_{i=1}^{n+1}(n-i+1)=\frac{n}{2} Eins=n+11i=1n+1(ni+1)=2n
算法的平均时间复杂度为O(n)。
5.删除

  • 检查 i 值是否超出所允许的范围 (1 ≤ i ≤ n),若超出,则进 行“超出范围”错误处理;
  • 将线性表的第 i 个元素后面的所有元素均前移一个位置;
  • 线性表的长度减 1。
Status ListDelete(SqList &L, int i) 
{//在顺序表L中删除第i个元素,i值的合法范围是1≤i≤L.length
	if((i<1)||(i>L.length))  return ERROR;//i值不合法
	for(j=i;j<=L.length-1;j++)
		L.elem[j-1]=L.elem[j];   //被删除元素之后的元素前移
	--L.length;    //表长减1
	return OK;
} 

所需移动元素次数的期望值(平均次数)为:
E d e l = 1 n ∑ i = 1 n ( n − i ) = n − 1 2 E_{del}=\frac{1}{n}\sum_{i=1}^{n}(n-i)=\frac{n-1}{2} Edel=n1i=1n(ni)=2n1
算法的平均时间复杂度为O(n)。
顺序表的缺点:在做插入或删除操作时,需移动大量元素,另外由于数组有长度相对固定的静态浪费,当表中数据元素个数较多且变化较大时,操作过程相对复杂,必然导致存储空间的浪费。

线性表的链式表示和实现

单链表的定义和表示

线性表链式存储结构的特点是:用一组任意的存储单元存储线性表 的数据元素(这组存储单元可以是连续的,也可以是不连续的)。因此,为了表示每个数据元素 a i a_i ai与其直接后继数据元素 a i + 1 a_{i+1} ai+1之间的逻辑关系,对数据元素 a i a_i ai来说,除了存储其本身的信息之外,还需存储一个指示其直接后继的信息。这两部分信息组成数据元素 a i a_i ai的存储映像,称为结点。它包括两个域:其中存储数据元素信息的域称为数据域;存储直接后继存储位置的域称为指针域。指针域中存储的信息称作指针。n个结点( a i a_i ai(1≤i≤n)的存储映像)链结成一个链表,即为线性表( a 1 a_1 a1 a 2 a_2 a2,···, a n a_n an)的;链式存储结构。又由于此链表的每个结点中只包含一个指针域,故又称线性链表单链表

单链表基本操作的实现

1.初始化

  • 生成新结点作为头结点,用头指针L指向头结点;
  • 头结点的指针域置空。
Status InitList(LinkList &L)
{//构成一个空的单链表L
	L=new LNode;  //生成新结点作为头结点,用头指针L指向头结点
	L->next=null;  //头结点的指针域置空
	return OK;
}

2.取值

  • 用指针p指向首元结点,用 j 做计数器初值赋为1。
  • 从首元结点开始依次顺着链域 next 向下访问,只要指向当前结点的指针 p 不为空(NULL),并且没有到达序号为 i 的结点,则循环执行以下操作:
    • p指向下一个结点;
    • 计数器 j 相应加1。
  • 退出循环时,如果指针p为空,或者计数器 j 大于 i ,说明指定的序号 i 值不合法,取值失败返回ERROR;否则取值成功,此时 j=1 时,p 所指的结点就是要找的第 i 个结点,用参数 e 保存当前结点的数据域,返回 OK。
 Status GetElem(LinkList L,int i,ElemType &e)
 {//在带头结点的单链表L中根据序号i获取元素的值,用e返回L中第i个数据元素的值
 	p=L->next;j=i;  //初始化,p指向首元结点,计数器j初值赋为1
 	while(p&&j<i)  //顺链域向后扫描,直到p为空域p指向第i个元素
 	{
 		p=p->next;   //p指向下一个结点
 		++j;   //计数器j相应加1
 	}
 	if(!p||j>i) return ERROR;   //i值不合法 i>n或i<=0
 	e=p->data;   //取第i个结点的数据域
 	return OK;
 }

平均查找长度:
A S L = 1 n ∑ i = 1 n ( i − 1 ) = n − 1 2 ASL=\frac{1}{n}\displaystyle\sum_{i=1}^{n} (i-1)=\frac{n-1}{2} ASL=n1i=1n(i1)=2n1
算法的平均时间复杂度为O(n)。
3.查找

  • 用指针p指向首元结点。
  • 从首元结点开始依次顺着链域next向下查找,只要指向当前结点的指针 p 不为空,并且 p 所指向的数据域不等于给定值,则循环指向以下操作: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)。
4.插入
将值为e的新结点插入到第i个结点的位置上,即插入到结点 a i − 1 a_{i-1} ai1 a i a_{i} ai之间,步骤如下:

  • 查找结点 a i − 1 a_{i-1} ai1并由指针p指向该结点;
  • 生成一个新结点 ∗ s *s s
  • 将新结点 ∗ s *s s的数据域置为e;
  • 将新结点 ∗ s *s s的指针域指向结点 a i a_{i} ai
  • 将结点 ∗ p *p p的指针域指向新结点 ∗ s *s s
Status ListInsert(LinkList &L,int i,ElemType e)
{//在带头结点的单链表L中第i个位置插入值为e的新结点
	p=L;j=0;
	while(p&&(j<i-1))
		{p=p->next;++j;}  //查找第i-1个结点,p指向该结点
	if(!p||j>i-1)  return ERROR;  //j>n+1或者i<1
	s=new LNode;  //生成新结点*s
	s->data=e;  //将新结点*s的数据域置为e
	s->next=p->next; //将新结点*s的指针域指向结点$a_{i}$
	p->next=s;  //将结点*p的指针域指向新结点*s
	return OK;
}

算法的平均时间复杂度为O(n)。
5.删除

  • 查找结点 a i − 1 a_{i-1} ai1并由指针p指向该结点;
  • 临时保存待删除结点 a i a_{i} ai的地址q中,以备释放;
  • 将结点 ∗ p *p p的指针域指向 a i a_{i} ai的直接后继结点;
  • 释放结点 a i a_{i} ai的空间。
Status ListDelete(LinkList &L, int i) 
{//在带头结点的单链表L中,删除第i个元素
	p = L;    j = 0;
	while ((p->next) && (j < i-1)) //查找第i-1个结点,p指向该结点
		{  p = p->next;   ++j; } 
	if  (!(p->next) || j > i-1) 
    return ERROR;  // 删除位置不合理
    q = p->next;   
    p->next = q->next;  // 删除并释放结点
	e = q->data;   
	free(q);
	return OK;
 }

算法的平均时间复杂度为O(n)。
6.创建单链表
(1)前插法
通过将新结点逐个插入链表的头部(头结点之后)来创建链表,每次申请一个新结点,读入相应的数据元素值,然后将新结点插入到头结点之后。

void CreateList_H(LinkList &L, int n) 
{ // 逆位序输入 n 个元素的值,建立带表头结点的单链表 L。
   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;   // 插入到表头
   	}
 } 

算法的平均时间复杂度为O(n)。
(2)后插法
通过将新结点逐个插入到链表的尾部来创建链表。同前插法一样,每次申请一个新结点,读入相应的数据元素值。不同的是,为了使新结点能够插入到表尾,需要增加一个尾指针r指向链表的尾结点。

void CreateList_R(LinkList &L,int n)
{
	L=new LNode;
	L->next=null;  //先建立一个带头结点的空链表
	r=L;  //尾指针r指向头结点
	for(i=0;i<n;++i)
	{
		p=new LNode;  //生成新结点
		scanf(&p->data);  //输入元素值赋给新结点*p的数据域
		p->next=null;r->next=p; //将新结点*p插入尾结点*r之后
		r=p;  //r指向新的尾结点*p
	}
}

算法的平均时间复杂度为O(n)。

其他链表

循环链表

详见link

双向链表

详见link

小结

(1)线性表的逻辑结构特性是指数据元素之间存在着线性关系,在计算机中表示这种关系的两类不同的存储结构是顺序存储结构(顺序表)和链式存储结构(链表)。
(2)对于顺序表,元素存储的相邻位置反映出其逻辑上的线性关系,可借助数组来表示。给定数组的下表,便可以存取相应的元素,可称为随机存取结构。而对于链表,是依靠指针来反映其线性逻辑关系的,链表结点的存取都要从头指针开始,顺链而行, 所以不属于随机存取结构,可称之为顺序存取结构。
下表为顺序表和链表的比较
在这里插入图片描述
(3)单链表、循环链表和双向链表的比较
在这里插入图片描述

;