今天开始总结一下线性表这一章的内容。
文章目录
线性表
首先来说一下分类
|- 顺序存储 ———— 顺序表
|
线性表---| |- 单链表
| |- 双链表
|- 链式存储 ---|- 循环链表
|- 静态链表
线性表:有相同数据类型、有限序列。
线性表的主要操作如下:
函数名 | 实现操作 |
---|---|
InitList(&L) | 初始化,构造一个空表 |
Length(L) | 求表长 |
LocateElem(L, e) | 按值查找 |
GetElem(L, i) | 按位置查找 |
ListInsert(&L, i, e) | 插入 |
ListDelete(&L, i, &e) | 删除 |
PrintList(L) | 输出所有元素 |
Empty(L) | 判断L是否为空 |
DestoryList(&L) | 销毁 |
一、顺序表
特点:随机存取,顺序存储
(易错点:顺序存取是一种读写方式,不是存储方式,有别于顺序存储。)
顺序表不用在节点中存放指针域,因此存储密度较大。
1. 顺序表定义
逻辑顺序与物理顺序相同
注意:线性表中元素位序从1开始,数组从0开始
//静态分配存储空间
# define MaxSize 50
typedef struct{
ElemType data[MaxSize];
int length;
}SqList;
//动态分配存储空间
# define InitSize 100
typedef struct{
ElemType *data; //指示动态分配数组的指针
int MaxSize, length; //数组的最大容量,当前个数
}SqList;
//初始动态分配语句
L.data = (ElemType*)malloc(sizeof(ElemType)*InitSize);
2. 插入
时间复杂度:O(n)
在L的第i个位置(1<=i<=L.length+1)插入新元素e
第i个元素开始及其后面的元素向右移动一个位置,腾出的空位放e
在第 i 个位置插入元素,需要移动 n-i+1 个元素。
//判断i位置合法
//判断数组是否满
for(j = L.length; j >= i; j--){ //后移元素
L.data[j] = L.data[j - 1];
}
L.data[i - 1] = e;
L.length++;
3. 删除
时间复杂度:O(n)
删除L的第i个元素
将第i个位置的元素赋值给e,再将第i个位置之后的所有元素前移
删除第 i 个元素,需要移动 n-i 个元素。
//判断i位置合法
//判断数组是否空
e = L.data[j - 1];
for(j = i; j <= L.length+1; j++){
L.data[j-1] = L.data[j];
}
L.length--;
4. 按值查找
时间复杂度:O(n)
找到L中第一个值为e的元素,然后返回其位置
(若小标为i的元素值为e,则返回其位序 i+1)
for(j = 0; j <= L.length+1; j++){
if(L.data[j] == e){
return j+1;
}
else{
return -1;
}
}
二、单链表
1. 定义
不需要使用地址连续的存储单元
//单链表中节点类型定义
typedef struct LNOde{
ElemType data;
struct LNode *next;
}LNode, *LinkList;
- 通常用“头指针”来标识单链表
如单链表L,头指针为NULL时,表示一个空表。
头指针始终指向链表的第一个结点,与是否带头结点无关 - 在单链表之前附加一个结点,称为“头结点”
头结点的指针域指向线性表的第一个结点,数据域可以不设任何信息,也可以记录表长等信息。 - 头结点,优点:
(1)开始结点的位置存在头结点的指针域,所以在链表第一个位置上的操作和其他位置操作一致,无需特殊处理。
(2)无论链表是否空,头指针都是指向头结点的非空指针,所以空表和非空表的处理就一致了。
2. 头插建立单链表
时间复杂度:O(n)
从空表开始,生成一个新的结点,并将读取的数据存放到新的结点,然后将新结点插入到当前链表的表头。
头插法生成的链表,读入数据的顺序与生成链表中元素的顺序是相反的。
while(x != 9999){
s->data = x;
s->next = L->next;
L->next = s;
}
3. 尾插建立单链表
时间复杂度:O(n)
将新结点插入到当前链表的表尾。需要增加一个尾指针r,r始终指向当前链表的尾结点。
while(x != 9999){
s->data = x;
r->next = s;
r = s;
}
r->next = NULL; //尾结点指针置空
4. 按序号查找
时间复杂度:O(n)
LNode *p = L->next; //头结点指针赋给p
if(i == 0){
return L;
}
if(i < 1){
return NULL;
}
while(p && j<i){ //p不为空,且j<i
p = p->next;
j++;
}
5. 按值查找
时间复杂度:O(n)
LNode *p = L->next;
while(p != NULL && p->data != e){
p = p->next;
}
6. 插入结点
算法的时间主要消耗在找 i-1 ,时间复杂度为O(n),如果在给定结点后面插入,则时间复杂度为O(1)
将值为x的元素插入到单链表的 i个位置上。
先找到待插入位置的前驱结点(i-1),在其后面插入新结点。
这样的操作方式又称为:后插操作(常用)
p = GetElem(L, i - 1);
s->next = p->next;
p->next = s;
前插操作:将 *s 插入到 *p 前面(可以将其转化为后插操作)
解决思路:仍然将 *s 插入到 *p 后面,然后将p->data与s->data交换即可。
时间复杂度O(1)
s->next = p->next;
p->next = s;
temp = p->data;
p->data = s->data;
s->data = temp;
7. 删除结点
时间复杂度:O(n),和插入一样,时间消耗在查找。
删除链表的第 i 个结点。首先要找到第 i-1 个结点,即被删结点的前驱,再将其删除。
p = GetElem(L, i-1);
q = p->next;
p->next = q->next;
free(q);
8. 求表长
时间复杂度:O(n)
从第一个结点开始依次访问,每访问一个结点,计数器加1,直到访问到空结点为止。
注意:单链表的长度不包括头结点,因此带头结点和不带头结点求表长操作不同。不带头结点的,当表为空时,要单独处理。
三、双链表
单链表只能从前向后遍历,如果要访问某个结点的前驱,只能从头遍历,时间复杂度O(n),所以引入了双链表。
双链表有两个指针:prior 和 next
typedef struct DNode{
ElemType data;
struct DNode *prior, *next;
}DNode, *DLinkList;
1. 插入
时间复杂度:O(1)
在 p 所指的结点之后插入结点 *s.
s->next = p->next;
p->next->prior = s;
s->prior = p;
p->next = s;
上述代码顺序不唯一,但(1)(2)要在(4)之前。
2. 删除
时间复杂度:O(1)
删除结点 *p 的后继结点 *q.
p-next = q->next;
q->next->prior = p;
free(q);
四、循环链表
1. 循环单链表
表尾结点 *r 的 next 域指向L,故表中没有指针域为NULL的结点。
判空的条件不是头结点指针是否为空,而是它是否等于头指针。
2. 循环双链表
头结点的 prior 指针要指向表尾结点。
某结点 *p 为尾结点时,p->next==L; 当L为空表时,头结点的prior和next都等于L.
五、静态链表
借助数组来描述线性表的链式存储结构,结点也有data域和next域。
与链表指针不同的是:这里的指针是结点的相对地址(数组下标),又称为游标。
可以分配较大的空间。
静态链表以 next==-1 作为结束标志。静态链表的插入、删除与动态链表相同,只需要修改指针,不需要移动元素。
# define MaxSize 50
typedef struct{
ElemType data;
int next;
}SLinkList[MaxSize];
小结
1. 双链表和单链表的区别
双链表:
- 执行按值查找、按位查找的操作和单链表相同。
执行插入、删除操作有较大的不同。 - 由于方便找到前驱,故插入、删除时间复杂度O(1).
2. 循环单链表和单链表的区别
循环单链表:
- 最后一个结点的指针不是NULL,而是指向头结点,形成一个环。
插入、删除操作与单链表几乎一样;(如果操作在表尾进行则有不同)
在任何位置上的插入、删除操作都是一样的,不用判断是不是在表尾。 - 单链表只能从表头结点开始往后顺序遍历链表,循环单链表可以从表中任一节点开始遍历链表。
3. 顺序表和链表的区别
顺序表 | 链表 | |
---|---|---|
存取方式 | 顺序存取或随机存取 | 顺序存取 |
逻辑与物理结构 | 逻辑上相邻的元素,物理上也相邻 | 逻辑相邻,物理不一定相邻 |
按值查找 | 无序:O(n) ;有序:O(log2 n) | O(n) |
按序号查找 | O(1) | O(n) |
插入、删除 | O(n),要移动元素 | O(n)(主要消耗在查找位置,单纯操作为O(1)) |
空间分配 | 预先分配空间,不能扩充 | 需要的时候申请分配,灵活高效 |
4.应用
- 常用操作:在最后一个元素之后插入元素和删除第一个元素。
应该用 仅有尾指针的单循环链表。(可表示队列,因为队列是表头删除、表尾插入) - 常用操作:在表尾插入结点和删除结点。
应该用 带头结点的双循环链表。 - 常用操作:删除第一个元素,删除最后一个元素、第一个元素之前插入新元素、最后一个元素之后插入新元素。
应该用:只有头结点指针 没有尾结点指针的循环双链表。