在上一篇文章中我们简单说了数据结构的概念和数据结构与算法的一些关系,这一篇文章的内容是关于线性表的东西,主要有线性表的定义、存储方式、以及一些常见的链表:单链表、静态链表、循环链表、双向链表等的读取和增删操作流程,结构方面会用图例进行展示,表的一些操作会用C语言代码来展示,代码部分最后会上传到我的GitHub上,地址在文章的最底部。
一、线性表的定义
线性表:由零个或多个数据元素的有限序列,在较复杂的线性表中一个数据元素可以由多个数据项组成。
线性表有两个关键点需要注意:
1.线性表是由多个元素组成,每个元素之间是有顺序的,并且每个元素都有且仅有一个前驱和候机元素
2.线性表是有限的
二、线性表的存储结构
1.顺序存储
线性表的顺序存储是用一段地址连续的存储单元依次存储线性表中的数据元素,它以“物理位置相邻”来表示线性表中数据元素间的逻辑关系,可随机存取表中任一元素
用代码表示顺序存储的结构如下:
#define MAXSIZE 1000 // 存储空间分配量
typedef int ElemType; // 数据存储单元
// 线性表顺序存储结构
typedef struct {
ElemType data[MAXSIZE]; // 数组存储数据元素
int length; // 线性表当前长度
}Sqlist;
注意点:
1.数组的长度是存放线性表的存储空间的长度
2.线性表的长度应该小于等于数组的长度
- 顺序存储结构的删除
在进行线性表数据的插入和删除工作之前我们需要获取线性表中的数据元素,线性表数据元素的获取如下:
#define OK 1 // 函数结果的状态值
#define ERROR 0
#define TRUE 1
#define FALSE 0
typedef int Status;
// 获取元素
Status GetElem(Sqlist L, int i, ElemType * e){
// 如果长度等于0或者要获取的元素的下表不在范围内,返回错误状态
if (L.length == 0 || i < 1 || i > L.length) {
return ERROR;
}
// 返回L中第i个位置的元素值
*e = L.data[i - 1];
return OK;
};
在获取到了线性表中的元素之后,我们就可以对其进行操作,比如删除操作,把一个元素从线性表删除之后我们需要将删除元素位置之后的其他元素往前移动一个位置。如果要删除的这个元素刚好在线性表的最后一个位置,则不用移动其他元素,删除线性表元素的思路如下:
1.先判断需要删除的元素位置是否正确,如果不正确抛出异常。
2.取出需要删除的元素。
3.从删除元素的位置开始遍历到最后一个元素的位置,将这些元素向前移动一个位置。
有了上面的思路,我们删除线性表中的元素代码实现如下:
// 删除操作
Status ListDelete(Sqlist *L, int i, ElemType * e)
{
int k ;
if (L->length == 0) { // 线性表为空
return ERROR;
}
if (i < 1 || i > L->length) { // 位置错误
return ERROR;
}
*e = L->data[i - 1];
// 从删除位置遍历到最后一个元素位置,将它们向前移动一个位置
if (i < L->length) {
for (k = i; k < L->length; k ++) {
L->data[k - 1] = L->data[k];
}
}
// 整表的长度减一
L->length -- ;
return OK;
}
- 顺序存储结构的删除
我们已经实现了删除元素的思路,插入元素其实和删除元素很相近,找到需要将元素插入的位置,放入新的元素,将之后的元素位置向后移动,这里需要注意的是一种好的数据组织形式最终会带来优越的性能提升,所以我们需要研究线性表顺序存储结构在读数据和增删数据的时间复杂度:
1.线性表顺序存储结构中的每一个数据都有自己的位置,我们可以根据这个位置直接找到这个元素,所以线性表顺序存储结构读取数据的时间复杂度是O(1)
2.线性表顺序存储结构在进行插入和删除的时候都会要求插入位置之后n个元素的位置发生变化,所以线性表顺序存储结构增删数据的时间复杂度为O(n)
2.链式存储
线性表的链式存储是指用一组任意的存储单元存储线性表中的数据元素,存储单元可以是连续的也可以是不连续的。但是相比较顺序存储失去了可随机存储的特性,并且这些元素除了存储信息外还需要存储它之后元素的地址。存储数据的域被称为数据域,存储下一个元素的地址的域被称为指针域,这两个部分共同组成了一个元素的存储映像,被称为结点。n个结点组成的链表我们叫它单链表
我们会把链表中的第一个结点前存放一个结点叫做头结点,单链表最后一个结点指针为NULL。为了更好的理解单链表,可以看下一我用keynote画的图:
代码表示如下:
// 线性表单链表存储结构
typedef struct Node{
ElemType data; // 数据域
struct Node * next; // 指针域
} Node;
typedef struct Node * SingleLinkList; // 定义一个单链表
单链表的数据读取
为了更好表达单链表中数据元素信息和该元素指针域锁存储的下一个元素的位置,我们用p->next表示p结点指针域中所存储的地址。用p->data表示p元素所存储的信息。p->next->data指的就是p的下一个结点所存储的信息。有了这些我们接下来看一看单链表的具体读取思路:
1.声明一个指针p指向链表的第一个结点,初始化j从1开始。
2.当j<1时,开始遍历链表,让p的指针向后移动不断的指向下一个结点,j同时累加1。
3.如果遍历到链表末尾p为空,增证明i这个结点不存在。
4.如果查找成功,返回结点p所存储的数据
如果有n个结点,我们需要遍历n次,所以单链表查找数据的时间复杂度是O(n)单链表的数据插入与删除
我们生成一个新的结点e,需要将这个结点e插入到p和p->next之间。我们需要将e的指针域指向额e->next = p->next,再将p的指针域的地址变为e,即p->next = e:
1.声明一个指针p指向链表头结点,初始化j从1开始。
2.当j<1时候,开始遍历链表,让p的指针向后移动,不断指向下一个结点,j+1
3.如果到表尾p为空,第i个结点不存在
4.如果i结点存在,生成新的结点s
5.插入表中s->next = p ->next; p->next = s;
代码实现如下:
// 单链表的插入
Status SingleListInsert (SingleLinkList *L, int i, ElemType e)
{
int j;
SingleLinkList p, s;
p = *L;
j = 1;
while (p && j < 1) {
p = p->next;
++j;
}
if (!p || j > 1) {
return ERROR;
}
// 这里采用malloc函数生成一个全新的结点
s = (SingleLinkList)malloc(sizeof(Node));
s -> data = e;
s -> next = p -> next;
p -> next = s;
return OK;
}
- 单链表的删除
单链表的删除与插入类似,注意将删除结点的空间进行释放,思路:
1.声明指针p指向链表头指针,j从1开始
2.j<1时,遍历链表,让p的指针向后移动,指向下一个结点,j累加1
3.如果到表尾p为空,第i个结点不存在
4.查找成功,将跑p->next赋值给p
5.链表的删除语句是p->next = q->next
6.将q结点,数据复制给e,返回
7.释放q结点
代码实现:
// 单链表的删除
Status SingleListDelete (SingleLinkList * L,int i,ElemType * e)
{
int j;
SingleLinkList p, q;
p = *L;
j = 1;
while (p -> next && j < i) {
p = p -> next;
++j;
}
if (!(p -> next) || j > i) {
return ERROR;
}
q = p ->next;
p ->next = q ->next;
*e = q ->data;
free(q); // 回收此结点,释放内存
return OK;
}
- 单链表的整表创建
单链表的整表创建所占用的空间的大小和位置不是需要预先分配划定的,可以根据系统的情况和实际需求即时生成。
整表创建的思路:
1.声明一个指针p和计数变量i
2.初始化一个空链表L
3.让L的头结点指针指向NULL,创建出一个带头结点指针的单链表
4.开始循环:
①生成一个新结点赋值给p。
②随机生成一数字赋值给p的数据域p->data。
③将p插入到头结点与前一个新结点之间
上面这种方法始终会把新创建的结点放在第一个位置上,这种方法叫做头插法,对应于头插法,还有一种方式是将新的结点放在终端结点的位置上,这种方式叫做尾插法,下面是这两种整表创建方式的代码实现:
// 单链表的整表创建(头插法)
void CreateSingleListHead(SingleLinkList *L, int n)
{
SingleLinkList p; // 声明指针P
int i; // 计数器
// 初始化空链表
*L = (SingleLinkList)malloc(sizeof(Node));
// 让空链表的头结点的指针先只想NULL
(*L)->next = NULL;
for (i = 0; i < n; i ++) {
p = (SingleLinkList)malloc(sizeof(Node)); // 生成新的结点
p ->data = rand() % 100 + 1;
// 让新结点p的指针域指向表头之前指针域锁指向的空间
p ->next = (*L) ->next;
(*L) -> next = p; // 让表头的指针域指向新的结点(插入到表头)
}
}
// 单链表的整表创建(尾插法)
void CreateSingleListTail(SingleLinkList *L, int n)
{
SingleLinkList p, r;
int i;
*L = (SingleLinkList)malloc(sizeof(Node));
r = *L;
for (i = 0; i < n; i ++) {
p = (Node *)malloc(sizeof(Node));
p ->data = rand() % 100 + 1;
r ->next = p;
r = p;
}
r ->next = NULL;
}
- 单链表的整表删除
当我们不再使用这个单链表的时候,可以把它销毁,将释放的内存给其他程序使用
整表删除的思路:
1.声明一个结点p和q
2.将一个结点赋值给p
3.开始循环:
①将下一个结点赋值给q
②将p结点释放
③将q赋值给p
代码实现:
// 单链表的整表删除
Status ClearSingleList (SingleLinkList * L)
{
SingleLinkList p, q;
p = (*L) -> next;
while (p) {
q = p ->next;
free(p);
p = q;
}
(*L) ->next = NULL;
return OK;
}
好了,现在我们开始对比一下线性表顺序存储结构和链式存储结构(单链表)的区别
存储结构 | 分配方式 | 时间性能 | 空间性能 |
---|---|---|---|
顺序存储 | 连续的存储单元 | 查找:O(1) 插入:O(n) | 需预分配空间 |
单链表 | 任意的存储单元 | 查找:O(n) 插入:O(1) | 动态分配,元素个数不受限 |
3.静态链表
上面提到的单链表都是基于指针才能实现,但是有一些语言没有指针的概念,如:Basic,上面用存储元素地址的方式显然是不可行的。所以我们引出了我们要讲的静态链表
静态链表:用数组描述的链表就叫做静态链表,这种实现方法还可以叫做游标实现法
根据下面的图我们来理解一下静态链表
静态链表和单链表的相似之处是都有用于存放信息的数据域和存放下一个元素的地址域,同时也需要对第一个元素和最后一个元素做特殊处理,在静态链表中我们把未使用的数组区域叫做备用链表,数组的第一个元素的地址域cur存放备用链表第一个结点的下标,数组的最后一个元素cur用来存放第一个有数值的元素的下标。代码实现:
// 静态链表
typedef struct {
ElemType data; // 数据域
int cur; // 游标
} Componet, StaticLinkList[MAXSIZE];
- 静态链表的插入
对于静态链表的操作相当于操作数组,不存在空间的申请和释放问题。所以为了更好的完成增加和删除数据的操作,我们把没有被使用的游标链做成一个备用链表,每次插入的时候都从备用链取得第一个结点作为待插入的新结点。这里需要注意的是我们把一个新的元素插入的时候不需要将该元素位置之后的其它元素都向后移动一个位置。这是和顺序结构插入的方式是不同的。具体实现思路简化:
将新插入的元素b先放到最后一排,然后将a元素的cur改为b位置,再将b元素的cur改为c的位置
优点 | 缺点 |
---|---|
插入和删除的时候修改游标,不需要移动元素 | 仍然存在表长难以确定的问题 |
4.循环链表
现在思考一个问题,单链表每个元素的指针域都指向后继元素,我们如果找当前元素的前驱元素需要从链表的起始位置从新开始。所以为了更好的解决这个问题我们引入了循环链表。
循环链表:将单链表中终端节点的指针从NULL指向头结点,整个链表就形成一个环,这样从链表当中任意一个结点出发就可以访问到链表的全部结点。
示例图如下:
循环链表和单链表的最大的差别就是判断终端结点的指针是否为NULL,如果不为NULL并且p->next是头结点,那这个链表就是循环链表
5.循环链表
循环链表虽然能够更加方便的从其中任意一个结点访问到全部元素,但如果我们需要从A结点出发找到A结点的前驱结点如果用循环链表来做还是需要循环链表一次时间复杂度O(n),双向链表的出现解决了这个问题;
循环链表:在单链表中我们再增加一个指针域指向前驱结点,一般我们都构造双向循环链表。
代码:
// 双向链表存储结构
typedef struct DulNode{
ElemType *elem; // 数据域
struct DulNode * prior; // 直接前驱指针
struct DulNode * next; // 直接后继指针
} DullNode, *DuLinkList;
示例图:
双向链表的在进行增删的时候需要将前驱指针和后继指针都发生变化
这篇文章主要介绍了顺序表的定义和一些常见表的存储结构以及特点。下一篇文章会介绍数据结构中的栈和队列的一些相关知识。最后本文示例代码地址在这里;