(一)线性表的链式表示
? ? ? ? 线性表的链式存储结构的特点是用一组任意的存储单元存储线性表的数据元素(这组存储单元可以是连续的,也可以是不连续的)。? ? ?这些数据元素可以存在内存未被占用的任意位置。
? ? 为了表示每个数据元素ai与其直接后继数据元素ai+1之间的逻辑关系,对数据元素ai来说,除了存储其本身的信息之外,还需存储一个指示其直接后继的信息(即直接后继的存储位置)。这两部分信息组成数据元素ai的存储映像,称为结点(Node)。我们把存储数据元素信息的域称为数据域,把存储直接后继位置的域称为指针域。指针域中存储的信息称做指针或链。n个结点链结成一个链表,即为线性表的链式存储结构。?
?Ⅱ?头结点、头指针和首元结点
? ? ?一个完整的链表是由头指针和诸多个结点构成的。每个链表都必须有头指针,但头结点不是必须的。?一个完整的链表应该由以下几部分构成:
头指针:一个和结点类型相同的指针,它的特点是:永远指向链表中的第一个结点。记录链表中第一个元素的存储位置,就是用头指针实现。
?结点:链表中的节点又细分为头结点、首元结点和其它结点:
- ? 头结点:某些场景中,为了方便解决问题,会在链表的开头放置一个空结点,这样的结点就称为头结点。头结点可以是位于链表开头、数据域为空(不利用)的结点。头结点可以不存储任何信息,也可以存储如线性表的长度等附加信息,头结点的指针域存储指向第一个结点的指针。
- 首元结点:指的是链表开头第一个存有数据的结点。
- 其他节点:链表中其他的节点。
头指针域、头结点的区别
?是否为必要元素:头指针是链表的必要元素,无论链表是否为空,头指针均不为空。头结点不一定是链表必需元素。
?作用方面:头指针是指向链表第一个结点的指针,若链表有头结点,则是指向头结点的指针。而且其具有标识作用,常用头指针冠以链表的名字。而头结点主要是为了操作的统一和方便而设立的,比如对第一元素结点前插入结点和删除第一结点,其操作就和其它结点的操作统一了。
? ? 结点由存放数据元素的数据域和存放后继结点地址的指针域组成。?
??
链表的结点类型:
//线性表的链式存储结构
typedef struct LNode
{
Data data;//数据域:存放元素
struct LNode* next;//指针域:指向后继结点
}LinkNode;//声明单链表结点类型
初始化链表:
//初始化链表
LinkNode* InitList()
{
LinkNode* L = (LinkNode*)malloc(sizeof(LinkNode));
assert(L);//断言,如果空,就返回
L->next = NULL;
return L;
}
插入:前插、尾插、指定插
关键:找到待插入指针的前驱指针,令前驱指针后面插入新的指针。
?
//增:头插法 插入 1234 输出4321
void CreateListF(LinkNode* L, Data data)
{
LinkNode* s;
s = (LinkNode*)malloc(sizeof(LinkNode));
assert(s);
s->data = data;
s->next = L->next;
L->next = s;
}
//尾插法
void CreateListW(LinkNode* L, Data data)
{
assert(L);
LinkNode* news, * pre = L;
news = (LinkNode*)malloc(sizeof(LinkNode));
assert(news);
news->data = data; news->next = NULL;
//关键:找到所插结点的前驱结点
while (pre->next != NULL)
{
pre = pre->next;
}
pre->next = news;
}
//指定位置插入,就是通过条件找到其前驱结点,代码于上面类似,待君实现
?插入就是要找到所需的前驱结点,通过前驱结点实现插入。
删除:找到前驱结点,设置替代品所删除的结点
//删:num:删除指定位置,第几个
void DeleLink(LinkNode* L, int num)
{
assert(L);
if (L->next == NULL) return;
int temp = 0;
LinkNode* pre = L;
LinkNode* del = (LinkNode*)malloc(sizeof(LinkNode));
//找到所求目标的前驱结点
while (pre)
{
if (temp == num - 1)
{
break;
}
pre = pre->next;
temp++;
}
if (temp == num - 1)
{
assert(pre);
del = pre->next;
assert(del);
pre->next = del->next;
free(del);
del = NULL;
}
else
{
printf("长度传入失败\n");
}
}
?
完整代码:
#include <stdio.h>
#include <stdlib.h>
#include <malloc.h>
#include <stdbool.h>
#include <assert.h>
typedef int Data;//抽象化数据类型
typedef struct LNode
{
Data data;//数据域存放元素
struct LNode* next;//指针域:指向后继结点
}LinkNode;//声明单链表结点类型
//初始化
LinkNode* InitList()
{
LinkNode* L = (LinkNode*)malloc(sizeof(LinkNode));
assert(L);
L->next = NULL;
return L;
}
//增:头插法 插入 1234 输出4321
void CreateListF(LinkNode* L, Data data)
{
LinkNode* s;
s = (LinkNode*)malloc(sizeof(LinkNode));
assert(s);
s->data = data;
s->next = L->next;
L->next = s;
}
//尾插法
void CreateListW(LinkNode* L, Data data)
{
assert(L);
LinkNode* news, * pre = L;
news = (LinkNode*)malloc(sizeof(LinkNode));
assert(news);
news->data = data; news->next = NULL;
//关键:找到所插结点的前驱结点
while (pre->next != NULL)
{
pre = pre->next;
}
pre->next = news;
}
//遍历:相当于求长度
void Print(LinkNode* L)
{
assert(L);
LinkNode* NurL = L;
while (NurL->next !=NULL)
{
printf("%d ", NurL->next->data);
NurL = NurL->next;
}
printf("\n");
}
//查找:相当于改 查找某个数据在链表的位置
int LocateData(LinkNode* L, Data a)
{
assert(L->next);
int num = 0;//元素位置
while (L != NULL)
{
if (a == L->data)
{
return num;
}
L = L->next;
num++;
}
printf("该链表找不到所求元素\n");
return -1;
}
//万金油函数->判空;
bool ListEmpty(LinkNode* L)
{
return (L->next == NULL);
}
//删:num:删除指定位置,第几个
void DeleLink(LinkNode* L, int num)
{
assert(L);
if (L->next == NULL) return;
int temp = 0;
LinkNode* pre = L;
LinkNode* del = (LinkNode*)malloc(sizeof(LinkNode));
//找到所求目标的前驱结点
while (pre)
{
if (temp == num - 1)
{
break;
}
pre = pre->next;
temp++;
}
if (temp == num - 1)
{
assert(pre);
del = pre->next;
assert(del);
pre->next = del->next;
free(del);
del = NULL;
}
else
{
printf("长度传入失败\n");
}
}
int main()
{
LinkNode* L = InitList();
CreateListW(L, 1);
CreateListW(L, 2);
CreateListW(L, 3);
CreateListW(L, 66);
Print(L);
DeleLink(L, 4);
Print(L);
printf(" %d ", LocateData(L, 66));
return 0;
}
实现效果:
总结:它们的时间复杂度都是O(n)。如果在我们不知道第 i 个结点的指针位置,单链表数据结构在插入和删除操作上,与线性表的顺序存储结构是没有太大优势的。对于某些特定位置的插入,对于顺序存储结构来说,每一次插入都需要移动 n-i 个结点。每次都是 O(n)。而单链表,我们只需要在第一次时,找到第 i 个位置的指针,此时为O(n),接下来就是通过赋值移动指针而已,时间复杂度都是O(1)。对于插入和删除数据越频繁的操作,单链表的效率优势就越是明显。
?
顺序表与链表的区别:
存取(读写)方式: ? ? 顺序表可以顺序存取,也可以随机存取,链表只能从表头顺序存取元素。例如在第i个位置上执行存或取的操作,顺序表仅需一次访问,而链表则需从表头开始依次访问i次。 ?逻辑结构与物理结构: ? ? ? ? 采用顺序存储时,逻辑上相邻的元素,对应的物理存储位置也相邻。而采用链式存储时,逻辑上相邻的元素,物理存储位置不一定相邻,对应的逻辑关系是通过指针链接来表示的。 ?查找、插入和删除操作: ? ? ? ?对于按值查找,顺序表无序时,两者的时间复杂度均为O(n):顺序表有序时,可采用折半查找,此时的时间复杂度为O(log?n)。 ? ? ?对于按序号查找,顺序表支持随机访问,时间复杂度仅为O(l),而链表的的平均时间复 杂度为O(n)。顺序表的插入、删除操作,平均需要移动半个表长的元素。链表的插。修改相关结点的指针域即可。由于链表的每个结点都带有指针域,故而存储密度不够大。
空间分配: ? ? ? 顺序存储在静态存储分配情形下,一旦存储空间装满就不能扩充,若再加入新元素,则会出现内存溢出,因此需要预先分配足够大的存储空间。预先分配过大,可能会导致顺序表后部大量闲置;预先分配过小,又会造成溢出。动态存储分配虽然存储空间可以扩充,但需要移动大量元素,导致操作效率降低,而且若内存中没有更大块的连续存储空间,则会导致分配失败。链式存储的结点空间只在需要时申请分配,只要内存有空间就可以分配,操作灵活、高效。
|