【数据结构】【考研笔记】【C】线性表

本文来源于《数据结构考研复习指导》,仅做笔记记录用。

定义和基本操作

线性表的定义

线性表是具有相同数据类型的n(n>0)个数据元素的有限序列,其中n为表长。当n=0时线性表就是一个空表。若用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)

线性表逻辑特性

  1. a1又称“表头元素”
  2. an又称“表尾元素”
  3. 除第一个元素外,每个元素有且仅有一个直接前驱;除最后一个元素外,每个元素有且仅有一个直接后继

线性表的特点

  1. 表中元素的个数有限
  2. 表中元素具有逻辑上的顺序性,表中元素有先后次序
  3. 表中元素都是数据元素,每个元素都是单个元素
  4. 表中元素的数据类型都相同,这意味着每个元素占有相同大小的存储空间

线性表的基本操作

  • InitList(&L)
    初始化表,构造一个空的线性表。
  • Length(L)
    求表长,返回线性表L的长度
  • LocateElem(L, e)
    按值查找
  • GetElem(L, i)
    按位查找,获取表L中的第i个位置上的元素的值
  • ListInsert(&L, i, e)
    插入操作,在表L中的第i个位置上插入指定元素e
  • ListDelete(&L, i, &e)
    删除操作,删除表L中第i个位置的元素,并用e返回删除元素的值
  • PrintList(L)
    输出操作
  • Empty(L)
    判空操作,若L为空表,则返回true;否则返回false
  • DestroyList(&L)
    销毁操作,销毁线性表,并释放线性表L所占用的内存空间

线性表的顺序表示

顺序表的定义

线性表的顺序存储又称顺序表。它是用一组地址连续的存储单元依次存储线性表中的数据元素,从而使得逻辑上相邻的两个元素在物理位置上也相邻
特点:逻辑顺序与物理顺序相同

存储类型描述

静态分配

在静态分配时,由于数组的大小和空间事先已经固定,一旦空间占满,再加入新的数据将会产生溢出,进而导致程序崩溃。

#define MaxSize 50				// 定义线性表的最大长度
typedef struct {
	ElemType data[MaxSize];		// 顺序表的元素
	int length;					// 顺序表的当前长度
} SqList;						// 顺序表的类型定义
动态分配

动态分配时,存储数组的空间是在程序执行过程中通过动态分配语句分配的,一旦数据空间占满,就另外开辟一块更大的存储空间替换,从而达到扩充存储数组空间的目的。

#define InitSize 100			// 表长度的初始定义
typedef struct {
	ElemType *data;				// 指示动态分配数组的指针
	int MaxSize, length;		// 数组的最大容量和当前个数
} SeqList;						// 动态分配数组顺序表的类型定义

C的初始动态分配语句:L.data=(ElemType *) malloc (sizeof(ElemType) *InitSize);
C++的初始动态分配语句:L.data=new ElemType[InitSize];

顺序表特点

  • 随机访问,通过首地址和元素序号可以在时间O(1)内找到指定元素
  • 存储密度高,每个结点只存储数据元素
  • 逻辑上相邻的元素物理上也相邻
  • 插入和删除操作需要移动大量元素

顺序表上基本操作的实现

插入操作

bool ListInsert(SqList &L, int i, ElemType e) {
	if (i < 1 || i > L.length + 1)			// 判断i的范围是否有效
		return false;
	if (L.length >= MaxSize)				// 当前存储空间已满,不能插入
		return false;
	for (int i = L.length; j >= i; j--)		// 在位置i处放入e
		L.data[j] = L.data[j - 1];			// 线性表长度+1
	L.data[i - 1] = e;
	L.length++;
	return true;
}

时间复杂度:

  • 最好情况:O(1)
    在表尾插入,元素无需移动
  • 最坏情况:O(n)
    在表头插入,元素移动语句执行n次
  • 平均情况:O(n)
    假设pi是在第i个位置上插入一个结点的概率,则在长度为n的线性表中插入一个结点时,所需移动元素平均次数为
    ∑ i = 1 n + 1 p i ( n − i + 1 ) = ∑ i = 1 n + 1 1 n + 1 ( n − i + 1 ) = 1 n + 1 ∑ i = 1 n + 1 ( n − i + 1 ) = 1 n + 1 n ( n + 1 ) 2 = n 2 \sum_{i=1}^{n+1}p_i(n-i+1)=\sum_{i=1}^{n+1}\frac{1}{n+1}(n-i+1)=\frac{1}{n+1}\sum_{i=1}^{n+1}(n-i+1)=\frac{1}{n+1}\frac{n(n+1)}{2}=\frac{n}{2} i=1n+1pi(ni+1)=i=1n+1n+11(ni+1)=n+11i=1n+1(ni+1)=n+112n(n+1)=2n

删除操作

 bool ListDelete(SqList &L, int i, ElemType &e) {
 	if (i < 1 || i > L.length)									// 判断i的范围是否有效
 		return false;
 	e = L.data[i - 1];						// 将被删除的元素赋值给e
 	for (int j = i; j < L.length; j++)		// 将第i个位置后的元素前移
 		L.data[j - 1] = L.data[j];
 	L.length--;								// 线性表长度减1
 	return true;
 }

时间复杂度:

  • 最好情况:O(1)
    删除表尾元素,无需移动元素
  • 最坏情况:O(n)
    删除表头元素,移动除第一个以外的所有元素
  • 平均情况:O(n)
    假设pi是删除第i个位置上结点的概率,则在长度为n的线性表中删除一个节点时,所需移动结点的平均次数为
    ∑ i = 1 n p i ( n − i ) = ∑ i = 1 n 1 n ( n − i ) = 1 n ∑ i = 1 n ( n − i ) = 1 n n ( n − 1 ) 2 = n − 1 2 \sum_{i=1}^np_i(n-i)=\sum_{i=1}^n\frac{1}{n}(n-i)=\frac{1}{n}\sum_{i=1}^n(n-i)=\frac{1}{n}\frac{n(n-1)}{2}=\frac{n-1}{2} i=1npi(ni)=i=1nn1(ni)=n1i=1n(ni)=n12n(n1)=2n1

按值查找(顺序查找)

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

时间复杂度:

  • 最好情况:O(1)
    查找的元素就在表头,仅比较1次
  • 最坏情况:O(n)
    查找的元素在表尾(或不存在),需比较n次
  • 平均情况:O(n)
    假设pi是查找的元素在第i个位置上的概率,则长度为n的线性表中查找值为e的元素所需比较的平均次数是
    ∑ i = 1 n p i × i = ∑ i = 1 n 1 n × i = 1 n n ( n + 1 ) 2 = n + 1 2 \sum_{i=1}^np_i\times i=\sum_{i=1}^n\frac{1}{n}\times i=\frac{1}{n}\frac{n(n+1)}{2}=\frac{n+1}{2} i=1npi×i=i=1nn1×i=n12n(n+1)=2n+1

线性表的链式表示

单链表

单链表的定义

线性表的链式存储又称单链表,是指通过任意的存储单元来存储线性表中的数据元素。为了建立数据元素之间的线性关系,对每个链表结点,除了存放元素自身的信息以外,还需要存放一个指向其后继的指针。单链表结点结构如下:

datanext

单链表是非随机存取的存储结构

存储类型描述

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

头结点和头指针

通常用头指针来标识一个单链表,如单链表L,头指针为NULL时表示一个空表。此外,为了方便,在单链表第一个结点之前附加一个结点,称为头结点

头结点和头指针的区分

不管带不带头结点,头指针始终指向链表的第一个结点;头结点是带头结点的链表中的第一个结点,结点内通常不存储信息。

引入头结点的优点
  1. 使链表的第一个位置上的操作和在表的其他位置的操作一致,无需进行特殊处理
  2. 无论链表是否为空, 头指针都结点可指。因此空表和非空表的处理也得到了统一

链表上基本操作的实现

建立单链表
头插法建立单链表

头插法指从一个空表开始,形成新结点。并将读取的数据存放到新结点的数据域中,然后将新结点插入到当前链表的表头,即头结点之后。

LinkList List_HeadInsert(LinkList &L) {		// 逆向建立单链表
	LNode *s; int x;
	L = (LinkList) malloc (sizeof(LNode));	// 创建头结点
	L->next = NULL;							// 初始为空链表
	scanf("%d", &x);						// 输入结点的值
	while(x != 9999) {						// 输入9999结束
		s = (LNode*) malloc (sizeof(LNode));	// 创建新结点
		s->data = x;
		s->next = L->next;
		L->next = s;						// 将新结点插入表中,L为头指针
		scanf("%d", &x);
	}
	return L;
}

时间复杂度:
每个结点插入时间为O(1),设单链表长为n,则总时间复杂度为O(n)
采用头插法建立单链表时,读入数据的顺序和生成的链表的元素顺序相反

尾插法建立单链表

尾插法需要增加一个尾指针r,使其始终指向当前链表的尾结点。

LinkList List_TailInsert(LinkList &L) {			// 正向建立单链表
	int x;										// 设元素类型为整型
	L = (LinkList) malloc (sizeof(LNode));
	LNode *s, *r = L;							// r为表尾指针
	scanf("%d", &x);							// 输入结点的值
	while (x != 9999) {							// 输入9999表示结束
		s = (LNode *) malloc (sizeof(LNode));
		s->data = x;
		r->next = s;
		r = s;									// r指向新的表尾结点
		scanf("%d", &x);
	}
	r->next = NULL;								// 尾结点指针置空
	return L;
}

时间复杂度:
每个结点插入时间为O(1),设单链表长为n,则总时间复杂度为O(n)
(因为附设了一个尾结点指针,故时间复杂度和头插法相同)

按序号查找结点值

在单链表中从第一个结点出发,顺指针next域逐个往下搜索,直到找到第i个结点为止,否则返回最后一个指针域NULL。

LNode *GetElem(LinkList L, int i) {
	int j = 1;					// 计数,初始为1
	LNode *p = L->next;			// 头结点指针赋给p
	if (i == 0)
		return L;				// 若i等于0,则返回头结点
	if (i < 1)
		return NULL;			// 若i无效,则返回NULL
	while (p && j < i) {		// 从第1个结点开始找,查找第i个结点
		p = p->next;
		j++;
	}
	return p;					// 返回第i个结点的指针,若i大于表长则返回NULL
}

时间复杂度:O(n)

按值查找表结点

从单链表第一个结点开始,由前往后依次比较表中各结点数据域的值,若某结点数据域的值等于给定值e,则返回该结点的指针;若整个单链表没有这样的的结点,则返回NULL。

LNode *LocateElem(LinkList L, ElemType e) {
	LNode *p = L->next;
	while (p != NULL && p->data != e)		// 从第1个结点开始查找data域为e的结点
		p = p->next;
	return p;								// 找到后返回该结点指针,否则返回NULL
}

时间复杂度:O(n)

插入结点操作
后插操作
p = GetElem(L, i - 1);				//查找插入位置的前驱结点
s->next = p->next;
p->next = s;

时间复杂度:O(1)

前插操作

设待插入结点为*s,将*s插入到*p的前面。首先将*s插入到*p的后面,再将p->data与s->data交换

p = GetElem(L, i - 1);
s->next = p->next;					// 修改指针域
p->next = s;
temp = p->data;						// 交换数据域data部分
p->data = s->data;
s->data = temp;

时间复杂度:O(1)

删除结点操作

删除结点操作是将单链表第i个结点删除。先检查删除位置的合法性,后查找表中第i-1个结点,即被删结点的前驱结点,再将其删除。

p = GetElem(L, i - 1);				// 查找删除位置的前驱结点	
q = p->next;						// 令q指向被删除结点
p->next = q->next;					// 将*q结点从链中“断开”
free(q);							// 释放结点存储空间

时间复杂度:O(n)

求表长操作

求表长操作就是计算单链表中数据结点(不含头结点)的个数,需要遍历整个表,每访问一个节点,计数器+1,直到访问到空结点为止。
时间复杂度:O(n)

另:因为单链表长度不包括头结点,因此头结点和不带头结点的单链表在求表长的操作上略有不同。对不带头节点的单链表,当表为空,需要单独处理。

双链表

存储类型描述

typedef struct DNode {				// 定义双链表节点类型
	ElemType data;					// 数据域
	struct DNode *prior, *next;		//前驱和后继指针
} DNode, *DLinklist;

由于双链表可以很方便地找到其前驱结点,因此,插入、删除操作的时间复杂度仅为O(1)。

链表上基本操作的实现

双链表的操作需要注意修改next指针的同时还要修改prior指针。

插入结点操作
p = GetElem(L, i - 1);				// 将结点*s插入到结点*p之后
s->next = p->next;					// 未插入前的p结点下一结点的prior指针指向结点s
p->next->prior = s;
s->prior = p;
p->next = s;
删除结点操作
p = GetElem(L, i - 1);
p->next = q->next;
q->next->prior = p;					// 未删除前结点q的下一结点的prior指针指向结点p
free(q);

循环链表

循环单链表

循环单链表和单链表的区别在于,表中最后一个结点的指针不是NULL,而是改为指向头结点,从而整个链表形成一个环。

循环单链表的判空

循环单链表的判空条件不是头结点的指针是否为空,而是它是否等于头指针。

另:有时对单链表常做的操作是在表头和表尾进行的,此时对循环单链表不设头指针而设置尾指针,从而使得操作效率更高。其原因是,若设置的是头指针,对表尾进行操作需要O(n)的时间复杂度。若设置的是尾指针r,r->next即为头指针,对于表头和表尾的操作都只要O(1)时间复杂度。

循环双链表

由循环单链表的定义推出循环双链表。在循环双链表中,头结点的prior指针要指向表尾结点。在循环双链表L中,某结点*p为尾结点时,p->next == L;当循环双链表为空时,其头结点的prior域和next域都等于L。

静态链表

静态链表借助数组来描述线性表的链式存储结构,结点有数据域data和指针域next。这里的指针是结点的相对地址(数组下标),又称游标。和顺序表一样,静态链表也要预先分配一块连续的内存空间

存储类型描述

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

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

顺序表和链表的比较

比较项顺序表链表
存取(读写)方式可以顺序存取,也可以随机存取只能从表头顺序存取
逻辑结构与物理结构逻辑上相邻的元素,物理位置也相邻逻辑上相邻的元素,物理位置不一定相邻,对应的逻辑关系通过指针链接表示
查找、插入、删除按值查找+顺序表无序:O(n);按值查找+顺序表有序:(折半查找)O(log2n);序号查找:O(1)按值查找:O(n);序号查找:O(n)
空间分配静态存储分配下,一旦存储空间装满则不能扩充。若预先分配过大,可能导致表后端大量闲置;若预先分配过小,容易溢出;动态分配虽然可以扩充,但需要移动大量元素,导致操作效率低,且若没有符合要求的新存储空间会导致分配失败只在需要时申请分配,只要内存有空间就能分配,操作灵活、高效

错题整理

  1. 若长度为n的非空线性表采用顺序存储结构,在表的第i个位置插入一个数据元素,i的合法值应该是1≤i≤n+1。(线性表元素的序号从1开始,而在第n+1个位置插入相当于在表尾追加)
  2. 给定由n个元素的一维数组,建立一个有序单链表的最低时间复杂度为O(nlog2n)。

若先将数组排好序,然后建立链表,建立链表的时间复杂度为O(n),数组排序的最好时间复杂度为O(nlog2n),最终时间复杂度为O(nlog2n)。

  1. 将长度为n的单链表链接在长度为m的单链表后面,其算法的时间复杂度为O(m)。

先遍历长度为m的单链表,找到该单链表的尾结点,然后将其next域指向另一个单链表的首结点,时间复杂度为O(m)。

  1. 单链表中增加一个头结点的目的是方便运算的实现。

单链表设置头结点的好处:

  • 有头结点后,插入和删除元素算法统一,无需判断是否是第一个元素前插入或删除第一个元素
  • 无论链表是否为空,其头指针是指向头结点的非空指针,链表头指针不变,因此空表和非空表的处理统一
  1. 在一个长度为n的带头结点的单链表h上,设有尾指针r,则执行删除单链表最后一个元素操作与链表的表长有关。

删除单链表最后一个结点需要设置其前驱结点指针域为NULL,需要从头开始依次遍历找到该前驱结点,需要O(n)时间,与表长有关。

  1. 在长度为n的有序单链表中插入一个新结点,仍保持有序的时间复杂度为O(n)。

首先要找到单链表中第一个大于x的结点的直接前驱p,在p之后插入结点。查找的时间复杂度为O(n),插入的时间复杂度为O(1)。总时间复杂度为O(n)。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值