HDU数据结构期末复习-2.线性表

《数据结构》复习笔记

HDU-STEA_banjiu修改于2021年1月9日

参考书目:严蔚敏《数据结构(第二版)》、王导论坛《数据结构考研复习指导》、HDU-STEA_YY《<数据结构>复习笔记》

第二章 线性表

1.线性表的定义

相同数据类型 n ( n ≥ 0 ) n(n\ge0) n(n0)个数据元素的有限序列,其中 n n n为表长,当 n = 0 n=0 n=0时,线性表是一个空表。

若用 L L L命名线性表,则一般表示为:
L = ( a 1 , a 2 , . . . , a i , a i + 1 , . . . , a n ) L=(a_1,a_2,...,a_i,a_{i+1},...,a_n) L=(a1,a2,...,ai,ai+1,...,an)
线性表的逻辑特性如下:

  • a 1 a_1 a1是唯一的”第一个元素“,称为表头元素
  • a n a_n an是唯一的”最后一个元素“,称为表尾元素
  • 除表头元素外,每个元素有且仅有一个直接前驱。
  • 除表尾元素外,每个元素有且仅有一个直接后驱。

线性表的特点如下:

  • 元素的个数有限
  • 元素具有逻辑上的顺序性,表中元素有先后次序
  • 元素都是数据元素,每个元素都是单个元素
  • 元素的数据类型都相同,占有相同大小的存储空间

线性表是一种逻辑结构,顺序表和链表是指存储结构。

2.线性表的基本操作

  1. InitList(&L)
    

    构造空表,即表的初始化;

  2. DestroyList(&L)
    

    销毁线性表;

  3. ListLength(L)
    

    求表的结点个数,即表长;

  4. GetNode(L,i,&e)
    

    按位查找,取表中第i个结点,要求1≤i≤ListLength(L);

  5. LocateNode(L,x)
    

    按值查找,查找L中值为x的结点并返回结点在L中的位置,有多个x则返回首个,没有则返回特殊值表示查找失败。

  6. InsertList(&L,i,x)
    

    插入操作,在表的第i个位置插入值为x的新结点,要求 1 ≤ i ≤ L i s t L e n g t h ( L ) + 1 1≤i≤ListLength(L)+1 1iListLength(L)+1

  7. DeleteList(L,i)
    

    删除操作,删除表的第i个位置的结点,要求 1 ≤ i ≤ L i s t L e n g t h ( L ) 1≤i≤ListLength(L) 1iListLength(L)

3.顺序表的定义

线性表的顺序存储,用一组地址连续的存储单元依次存储线性表中的数据元素,使得逻辑上相邻的两个元素在物理位置上也相邻。

第1个元素存储在线性表的起始位置,第 i i i个元素的存储位置后面紧接着存储的是第 i + 1 i+1 i+1个元素, i i i为元素 a i a_i ai在线性表中的位序

4.顺序表结点的存储地址计算公式

假设线性表 L L L存储的起始位置为 L O C ( A ) LOC(A) LOC(A),每个元素的占用空间为 s i z e o f ( E l e m T y p e ) sizeof(ElemType) sizeof(ElemType)

则线性表中第 i + 1 i+1 i+1个数据元素的存储位置 L O C ( a i + 1 ) LOC(a_{i+1}) LOC(ai+1)和第 i i i个数据元素的存储位置 L O C ( a i ) LOC(a_{i}) LOC(ai)之间满足下列关系:
L O C ( a i + 1 ) = L O C ( a i ) + s i z e o f ( E l e m T y p e ) LOC(a_{i+1})=LOC(a_{i})+sizeof(ElemType) LOC(ai+1)=LOC(ai)+sizeof(ElemType)
一般来说,线性表的第 i i i个数据元素 a i a_{i} ai的存储位置为
L O C ( a i ) = L O C ( A ) + ( i − 1 ) × s i z e o f ( E l e m T y p e ) LOC(a_{i})=LOC(A)+(i-1)\times sizeof(ElemType) LOC(ai)=LOC(A)+(i1)×sizeof(ElemType)

线性表中元素的位序是从1开始的,而数组元素的下标是从0开始的

5.线性表的顺序存储类型

(1)静态分配
#define Maxsize 50 		//定义线性表的最大长度
typedef struct{		
    int data[Maxsize];	//顺序表的元素
    int length;			//顺序表的当前长度
}SqList;				//顺序表的类型定义
(2)动态分配
#define InitSize 100 	//定义线性表的最大长度
typedef struct{		
    int *data;			//指示动态分配数组的指针
    int MaxSize,length;	//数组的最大容量和当前个数
}SeqList;				//动态分配数组顺序表的类型定义

C的初始动态分配语句为

L.data=(ElemType*)malloc(sizeof(ElemType)*InitSize);

动态分配并不是链式存储,同样属于顺序存储结构,只是分配的空间大小可以在运行时决定

6.顺序表上的基本操作

(1)插入
void insertlist(SeqList *L, ElemType x, int i)
{
    if (i < 1 || i > L->length + 1)
        error("position error"); 	//位置i不合法
    if (L->length >= listsize)
        error("overflow"); 			//存储空间已满
    for (int j = L->length - 1; j >= i - 1; j--)
        L->data[j + 1] = L->data[j]; //结点后移
    L->data[i - 1] = x;              //插入节点x
    L->length++;                     //表长度加一
}

在顺序表上插入要移动表的 n / 2 n/2 n/2结点,算法的平均时间复杂度为 O ( n ) O(n) O(n)

(2)删除
void delete (SeqList *L, int i)
{
    int j;
    if (i < 1 || i > L->length)
        error("position error"); //位置i不合法
    for (j = i; j <= L->length - 1; j++)
        L->data[j - 1] = L->data[j]; //结点前移
    L->length--;                     //表长度减一
}

在顺序表上删除要移动表的 ( n + 1 ) / 2 (n+1)/2 (n+1)/2结点,算法的平均时间复杂度为 O ( n ) O(n) O(n)

(3)按值查找(顺序查找)
int LocateNode(SqList L,ElemTye x){
	int i;
	for(i = 0; i < L.length; i++)
        if(L.data[i] == x)
            return i+1;			//下表为i的元素等于x,返回其位序i+1
    return 0;					//退出循环,说明查找失败
}

在顺序表上查找的次数为 ( n + 1 ) / 2 (n+1)/2 (n+1)/2次,算法的平均时间复杂度为 O ( n ) O(n) O(n)

(4) 合并
void merge(SeqList La, SeqList Lb, SeqList &Lc)
{
    //已知顺序线性表La和Lb的元素按值非递减排列
    //归并La和Lb得到新的顺序线性表Lc,Lc的元素也按值非递减排列
    ElemType *pa, *pb, *pc, *pa_last, *pb_last;
    pa = La.elem;
    pb = Lb.elem;                      // 指针pa,pb的初始值分别指向两个表的第一个元素
    Lc.length = La.length + Lb.length; // 新表长度为待合并两表的长度之和
    Lc.elem = new ElemType[Lc.length]; // 为合并后新表分配一个数组空间
    pc = Lc.elem;                      // 指针pc指向新表的第一个元素
    pa_last = La.elem + La.length - 1; // 指针pa_last指向La表的最后一个元素
    pb_last = Lb.elem + Lb.length - 1; // 指针pb_last指向Lb表的最后一个元素

    while (pa <= pa_last && pb <= pb_last)
    { // 两个表都非空
        if (*pa <= *pb)
            *pc++ = *pa++; // 一次摘取两表中值最小的节点
        else
            *pc++ = *pb++;
    }
    while (pa <= pa_last)
        *pc++ = *pa++; // Lb表已经达到表尾,将La中剩余元素加入Lc
    while (pb <= pb_last)
        *pc++ = *pb++; // La表已经达到表尾,将Lb中剩余元素加入Lc
}

算法的时间复杂度为 O ( L a . l e n g t h + L b . l e n g t h ) O(La.length+Lb.length) O(La.length+Lb.length)

7.单链表的定义

线性表的链式存储,通过一组任意的存储单元(可连续,可不连续)来存储线性表中的数据元素。为了建立数据元素之间的线性关系,对每个链表结点,除存放元素自身的信息外,还需要存放一个指向其后继的指针。

(1)单链表的结点结构

单链表的结点结构如下:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-IzSgVOhd-1610297169031)(C:\Users\HP\AppData\Roaming\Typora\typora-user-images\image-20210110163847722.png)]

其中data为数据域,存放数据元素;next为指针域,存放其后继结点的位置。

指针域中存储的信息称作指针。n个节点的链结成一个链表,即为线性表的链式存储结构。

以下为单链表中结点类型的描述:

typedef struct LNode	//定义单链表结点类型
{
    ElemTyoe data;		//数据域
    struct LNode *next;	//指针域
}LNode,*LinkList;

有一个指针域的链表称单链表。
在C语言中,单链表可由头指针唯一确定,可用“结构指针”来描述。此外,为了操作上的方便,在单链表第一个结点前附加一个结点,称为头结点

(2)头结点和头指针的区分:
  • 不管带不带头结点,头指针始终指向链表的第一个结点
  • 头结点是带头结点的链表的第一个结点,结点内通常不存储信息。
(3)引入头结点的优点
  • 链表第一个位置的操作无需特殊处理;
  • 将空表和非空表的处理统一。

8.单链表上的基础操作的实现

(1)建立单链表
头插法

前插法通过将新结点逐个插入链表的头部(头结点之后)来创建链表,每次申请一个新结点,读入相应的数据元素值,然后将新结点插入到头结点之后 。

  1. 创建一个只有头结点的空链表。
  2. 根据创建链表结点元素个数 n ,循环n次执行以下操作:
    1. 生成一个新结点 *p ;
    2. 将输入元素赋新结点 *p 的数据域中;
    3. 将新结点 *p 插入到头结点之后。
//采用头插法建立单链表
/*
	建立链表 - 头插法 - 有头节点

	+------+	     +------+	 +------+	 +------+
	|   L  |   =>    |node_1| -> |node_2| -> |node_3| -> NULL
	+------+	     +------+	 +------+	 +------+
		  \			  /
			 +------+
			 |   s  |
			 +------+

*/
LinkList List_headInsert(LinkList &L){
	LNode* s; 							//临时节点,用于保存声明的节点
	L = (LinkList)malloc(sizeof(LNode));//生成链表头节点
	L->next = NULL;
	L->data = 0;						//头节点的data保存链表长度
	int x;								//暂时存储输入元素,用作赋值
	scanf("%d", &x);
	while (x!= -1) {
		s = (LNode*)malloc(sizeof(LNode));
		s->data = x;
		s->next = L->next;
		L->next = s;					//将新节点插入表中,L为头指针
		L->data++;
        
		scanf("%d", &x);
	}
	return L;
}
尾插法

头插法虽然简单,但生成的链表中结点的次序和输入数据的顺序不一致。尾插法则可保证两者的次序一致。

1.增加一个尾指针r,使其始终指向当前链表的尾结点。

2.将新结点插入当前链表的表尾。

//采用尾插法建立单链表
/*
	创建链表 - 尾插法 - 有头节点

	+------+	+------+	+------+
	|   L  | -> |node_1| -> |node_2|   ____
	+------+	+------+	+------+	  |
										  v
						+------+ 	  +------+
						|  r   |  ->  |   s  |
						+------+	  +------+

*/
LinkList List_TailInsert(LinkList &L) {
	L = (LinkList)malloc(sizeof(LNode));
	LNode* s,*r=L;					//用r来保存尾部位置
	
	L->next = NULL;
	L->data = 0;
	int x;							//暂时存储结点的值
	scanf("%d", &x);
	while (x != -1) {
		s = (LNode*)malloc(sizeof(LNode));
		s->data = x;				
		r->next = s;				//r指向新的表尾结点
		r = s;
		scanf("%d", &x);
	}
	r->next = NULL;					//尾结点指针置空
	return L;
}
(2)查找运算

时间复杂度为O(n)。

1) 按序号查找
LNode* GetELem(LinkList L, int i) {
	int j = 1;				//计数,初始为1
	LNode* p = L->next;
	if (i == 0)
		return 0;			//若i=0,则返回头节点
	if (i < 1)
		return NULL;		//若i无效,则返回NULL
	while (p && j < i) {	//从第一个节点开始找,查找第i个节点
		p=p->next;
		j++;
	}
	return p;				//返回第i个节点的指针,若i大于表长,则返回NULL
}
2) 按值查找
LNode* LocateElem(LinkList L, int e) {
	LNode* p = L->next;
	while (p!=NULL&&p->data!=e)	//从第一个节点开始查找data域为e的节点
	{
		p = p->next;
	}
	return p;					//找到后返回该节点指针,否则返回NULL
}
(3)插入运算

时间复杂度为O(n)。

void Insertlist(linklist L, datatype x, int i)
{
    listnode *p;
    int j;
    p = L;
    j = 0;
    while (p && j < i - 1) //寻找第i-1个节点
    {
        p = p->next;
        ++j;
    }

    if (!p || j > i - 1)
        error("position error");              //i小于1或者大于表长加1
    s = (listnode *)malloc(sizeof(listnode)); //生成新节点
    s->data = x;                              //数据域赋值
    s->next = p->next;                        //新结点*s的指针域指向*p的后继结点
    p->next = s;							  //*p指针域指向新插入结点*s
}
(4) 删除运算

时间复杂度为O(n)。

Void DeleteList(linklist head, int i)
{
    //删除第i个节点
    listnode *p, *r;
    p = GetNode(head, i - 1); //寻找第i-1个节点
    if (p == NULL || p->next == NULL)
        error(“position error ”);
    r = p->next;
    p->next = r->next; //将第i-1个节点的指针域指向第i+1个节点
    free(r); //释放空间
}

9.双链表

在结点中增加一个指针域,形成的链表中有两条不同方向的链称为双链表。

双链表结点中有两个指针prior和next,分别指向其前驱节点和后继结点。

typedef struct DNode			//定义双链表结点类型
{
    ElemType data;				//数据域
    struct DLNode *prior;		//前驱指针
    struct DLNode *next;		//后继指针
} DLNode, *DLinkList;
1) 双链表的前插操作

时间复杂度为 O ( 1 ) O(1) O(1)

Void dinsertbefore(dlistnode *p, datatype x)
{
    dlistnode *s = malloc(sizeof(dlistnode));
    s->data = x;
    s->prior = p->prior;
    s->next = p;
    p->prior->next = s;
    p->prior = s;
}
2) 双链表的删除操作

时间复杂度为O(1)。

Void ddeletenode(dlistnode *p)
{
    p->prior->next = p->next;
    p->next->prior = p->prior;
    free(p);
}

10.循环链表

循环链表是一种首尾相连的链表。它的特点是表中最后一个节点的指针域指向头结点,整个链表形成一个环。

在执行增加节点操作时,无需增加存储量,仅对表的链接方式修改使表的处理灵活方便。

循环链表的操作和线性链表基本一致,差别仅在于算法中的循环条件不是p或p->next是否为空,而是它们是否等于头指针。

(1)空循环链表

仅由一个自成循环的头结点表示。

(2)循环单链表的表示

很多时候表的操作是在表的首尾位置上进行,此时头指针表示的单循环链表就显的不够方便,改用尾指针 r e a r rear rear来表示循环单链表。用头指针表示的循环单链表查找开始结点的时间是 O ( 1 ) O(1) O(1),查找尾结点的时间是 O ( n ) O(n) O(n);用尾指针表示的循环单链表查找开始结点和尾结点的时间都是 O ( 1 ) O(1) O(1)

(3)循环双链表

与循环单链表不同的是,在循环双链表中,头结点的prior指针还要指向表尾结点。

在循环双链表 L L L中,某结点*p为尾结点时,p->next==L;当循环双链表为空表时,其头结点的prior域和next域都等于 L L L

11.静态链表

与前面所讲的指针不同的是,这里的指针是结点的相对地址(数组下标),又称游标。和顺序表一样,静态链表也要预先分配一块连续的内存空间。

静态链表结构类型的描述如下:

#define Maxsize 50			//静态链表的最大长度
typedef struct{				//静态链表的结构定义
    ElemType data;			//存储数据元素
    int next;				//下一个元素的数组下标
}SLinkList[MaxSize];

静态链表以next==-1作为其结束的标志。

12.顺序表和链表的比较

  1. 基于空间的考虑:顺序表的存储空间是静态分配的,链表的存储空间是动态分配的。顺序表的存储密度比链表大。因此,在线性表长度变化不大,易于事先确定时,宜采用顺序表作为存储结构。
  2. 基于时间的考虑:顺序表是随机存取结构,若线性表的操作主要是查找,很少有插入、删除操作时,宜用顺序表结构。对频繁进行插入、删除操作的线性表宜采用链表。若操作主要发生在表的首尾时采用尾指针表示的单循环链表。

13.存储密度公式

( 结 点 数 据 本 身 所 占 的 存 储 量 ) / ( 整 个 结 点 结 构 所 占 的 存 储 总 量 ) (结点数据本身所占的存储量)/(整个结点结构所占的存储总量) ()/()

存储密度:顺序表=1,链表<1。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值