记得以前上C语言课的时候,只要提到链表,脑海里就只有两个词,一是高大上,二是难。今天让我们来看看链表到底是何方神圣。高手还请多多指教!
(注:今主要讨论单链表。链表分为单链表、静态链表、循环链表和双向链表)
一、定义:链表是线性表的链接存储表示
二、特点:每个元素(表项)由结点(Node)构成。
1.
结点可以连续,可以不连续存储
2.
结点的逻辑顺序与物理顺序可以不一致
3.
表可扩充
首先,我们来定义一张链表:
typedef char ListData;
typedef struct node { //链表结点
ListData data;
//结点数据域
struct node * link; //结点链域
} ListNode;
typedef ListNode * LinkList;
有了一张表之后,首先需要给它初始化,不然不可以使用(注意:以下的函数模块都是接口函数,需要在主函数中调用)
/* 初始化链表 */
int ListInit(LinkList *L)
{
(*L) = (LinkList)malloc (sizeof(Node)); //首先需要使用molloc函数在堆上为链表开辟一段内存
if (NULL == (*L))
{
return FAILURE;
}
(*L)->next = NULL;
return SUCCESS;
}
初始化一张表后就需要往表里面插入元素,不然就只是一张空表
/* 插入模块 */
int ListInsert(LinkList *L, int i, ElemType e)
{
LinkList p = *L;
int j = 1;
while(p != NULL && j < i)
{
p = p->next;
j++;
}
if(p == NULL || j > i)
{
return FAILURE;
}
LinkList n = (LinkList)malloc(sizeof(Node));
if(NULL == n)
{
return FAILURE;
}
n->data = e;
n->next = p->next;
p->next = n;
return SUCCESS;
}
光看代码,似乎有些难懂,下面我们就来讲讲是如何插入结点(要插入的元素就保存在结点的数据域,而结点的指针域则用来指向下一个结点,这样一个一个的结点就像被链子连接起来一样,从而形成一张链表)的:
如上图所示,想要 把结点x插入到头结点的后面,这里假设x这个结点的地址是0x100,把x插入到head后面就是让head指向x即可。那么就需要把x的地址(即0x100)给头结点的指针域(即head->next),但是,现在头结点的指针域是NULL,如果直接把0x100给它,那么NULL就被覆盖了,这是不允许的,所以我们的先把这个NULL赋给x的指针域(即x->next)。OK,相信大家现在应该了解了怎么把结点插入到头结点后面了。那么问题来了,怎么把结点插入到两个结点之间呢?
首先,我们还是上图先
本来结点a是接在head后面的,现在它们之间有个第三者x想要插足,那么它需要做那些事呢?首先它需要把指向a的“箭头”断开,使之指向自己,在把自己的“箭头”指向a,就完成了“插足”。这里我们假设a的地址为0x400, x 的地址为0x900,而x->next指向谁不明确,也不需要知道,可以把它理解为一个垃圾地址。插入之前,head->next为0x400,现在要使之指向x,即将0x900赋值给head->next .但是现在有个问题,如果直接这样赋值就会把原本指向a的0x400给覆盖了,那么谁再来指向a呢。 所以在此之前,我们得先把a的地址赋值给x->next ,那么,那个垃圾地址就被a的地址所覆盖了,即x就指向了a,然后我们现在就可以把x的地址放心的交给head->next 了。
x->next = head->next;
head->next = x;
代码就两句,而且是不是看起来跟讲解的有些不一样。每错,代码里面没有出现a ,这是为什么呢?因为在实际中,你并不知道原本接在head后面的元素的名称是什么,但是没关系,我们有head->next ,它就是用来存放接在后面的结点的地址,所以,把a的地址给x->head,就是x->next = head->next;
还有一点,比如把x的地址赋值给head->next,为什么可以直接写为head->next = x;这是因为,这个结点名称可以代表这个结点的地址。
/* 删除结点 */
int ListDelete(LinkList L, int i, ElemType *e)
{
LinkList p = L;
int j = 1;
LinkList tmp;
while(p != NULL && j < i)
{
p = p->next;
j++;
}
if(p == NULL || j > i)
{
return FAILURE;
}
tmp = p->next;
p->next = tmp -> next;
free(tmp);
return SUCCESS;
}
删除就是插入的逆过程,这里就不上图了,想要删除中间的元素,我们只需要将这个元素之后的元素的地址赋给要删除的元素前面一个元素指向的下一个地址就好。(读者可自行脑补一下它的过程,并不复杂)这里要说明的是,删除了结点之后要记得释放这段内存,否则会造成内存泄漏。
三、 顺序表和链表的区别
存储分配的方式:
顺序表的存储空间是静态分配的
链表的存储空间是动态分配的
存储密度 = 结点数据(data)本身所占的存储量/结点结构所占的存储总量
顺序表的存储密度 = 1
链表的存储密度 < 1
插入/删除时移动元素个数:
顺序表平均需要移动近一半元素
链表不需要移动元素,只需要修改指针
若插入/删除仅发生在表的两端,宜采用带尾指针的循环链表