链表
前面学习的顺序存储线性表,优缺点都十分明显,易于展示数据和查找操作,不易于增删改查。
而另一种线性表,正好相反,增删改查比顺序存储方便灵活,这就是链表,我所接触到的第一个在灵活应用指针的数据结构。
单链表结构
链表由两个组件组成:数据域、指针域。
链表是由多个节点组成的,每个节点都有数据域和指针域。
数据域是存放数据的成员,指针域是建立链表数据元素之间逻辑关系的重要概念。对于单链表,数据域存储数据本身,指针域则是指向下一个节点。
节点的结构组成:
typedef struct Node{
int data; //数据域,存放数据
struct Node * next; //指针域,指向下一个节点
}Node;
typedef struct Node * LinkList; //链表与数组名类似,存储指向首节点的地址
一个完整的链表,至少由头指针、节点组成,头指针就是上面的LinkList,它本身是指向节点的指针,这很类似于数组名本质是首元素地址一样。
链表的最后一个节点的指针域是NULL,意味着链表的结束。
单链表的读取、插入、删除
对于前面学习的顺序存储,我们要查找某个确定位置的元素是极其容易的,直接用数组表示法list[i]就可以,那么单链表呢?很遗憾,单链表无法这样做,我们必须靠遍历的方式一个一个节点去读取,直到读取到我们所要的那个位置的数据。
读取第i个元素的算法:(暂时没有加入查找失败的代码)
- 声明一个指针p指向链表的第一个节点,初始化位置表达变量j为1(因为是从第一个节点开始)
- j<i时,对链表进行遍历,让p不断指向下一个节点,j同时也递增。
- j<i为False时,说明p已经指向了第i个节点了,此时把这个节点的数据返回出来即可。
void GetElem(LinkList L, int i, int *e){
LinkList p;//声明一个指向节点的指针p
p=L->next;//初始化p为头结点的地址
int j = 1;
while(p && j<i){
p=p->next;//p不断更新为当前指向的节点的下一个节点,直到指向第i个节点
++j;
}
*e = p->data; //将查找好的这个节点的数据赋给变量e
}
单链表的读取操作,时间复杂度为O(n),不如顺序存储的O(1)。
既然读取这么麻烦,那么单链表的意义是啥呢?那就看插入和删除操作了,这是链表的最大的优点,顺序存储任何一个插入删除操作时间复杂度都是O(n),一旦要同时处理多个插入和删除操作,那工作量简直了,不过单链表就要简单很多。
对单链表第i的位置进行插入的算法:
- 声明一个指针p指向头结点,初始化位置表达器j为1;
- j<i时,对链表进行遍历,让p不断指向下一个节点,j同时也递增。
- j<i为False时,说明p已经指向了第i-1个节点了,此时,向系统申请一个空节点s的内存空间。
- 将要插入的数据e的值赋给s的数据域;
- 初始化新节点s的指针域指向第i+1个位置的节点,让第i位置的节点next指针指向这个新节点s.
void LinkListInsert(LinkList *L,int i,int e){//参数是指向链表的指针,提示我们这个函数要修改这个链表的结构
int j = 1;
LinkList p, s;
p = *L; //让p指向头节点
while(p && j<i){
p=p->next;
++j;
}
s=(LinkList)malloc(sizeof(Node));//申请一个新节点s的内存空间;
s->data = e;
s->next = p->next;//初始化新节点s的指针域指向第i+1个位置的节点
p->next = s;//让第i位置的节点next指针指向这个新节点s.
}
显然,单链表插入操作的时间复杂度虽然为O(n),但其最大值只会比顺序存储的O(n)更小,而且对内存空间的操作更加灵活简单。
同样的,删除操作也很简单,算法为:
- 声明一个指针p指向头结点,初始化位置表达器j为1;
- j<i时,遍历链表,让p不断向后移动,直到指向第i-1个节点
- 把p的后继的节点的地址(也就是p->next)赋给指针q,并用变量e保存q的数据
- 把p(第i-1个节点)的后继改为q的后继(第i+1个节点)
- 用free()释放掉q(第i个节点)的内存空间,删除完成
void LinkListDelete(LinkList *L,int i,int *e){//修改链表操作,传递链表的指针为参数
int j = 1;
LinkList q, p = *L;
while(p && j<i){
p = p->next;
++j;
}//循环结束,p此时指向了我们要删除的那个节点前面那个
q = p->next; //将第i个节点赋给q
p->next = q->next;//第i个节点前面那个节点的后继改为第i个节点的后面那个节点
*e = q->data;//保存即将被删除的那个节点的数据
free(q);//free掉节点q,也就是删除掉q节点
}
对比顺序存储,链表在处理多个数据的插入和删除就非常方便了。
单链表的创建
链表的操作经常伴随着遍历,虽然看上去可能比较麻烦,但链表本身就是为了方便增删改查而存在的数据结构。
单链表的创建主要就是遍历的算法过程,首先介绍头插法创建的算法
- 声明一个指向链表的指针p和计数器变量j;
- 初始化一个空链表L;
- 让L的头结点指向NULL。也就是建立一个带头结点的单链表。
- 遍历循环:生成一个新节点p,给p随便赋予一个数据,然后将p插入到头结点和上一个创建的节点之间
void CreatListHead(LinkList *L,int n){
*L = (LinkList)malloc(sizeof(Node));
*L->next = NULL;
LinkList p;
for(int i=0;i < n;++i){
p = (LinkList)malloc(sizeof(Node));
p->data = i;
p->next = (*L)->next; //第一次循环时,是在*L和NULL之间添加p,第二次以后的循环是在*L和上一次循环的p之间插入一个新节点
(*L)->next = p;
}
}
另一种是尾插法:
- 声明两个指向链表的指针p、r和计数器变量j;
- 初始化一个空链表L作为头结点;
- 把L赋给r,r一直作为尾结点。
- 遍历循环:生成一个新节点p,给p随便赋予一个数据,把p作为当前尾结点r的后继,然后再把p赋给r。
- 常见了n个新节点后,把尾结点后继赋予NULL,表示链表结束。
void CreatListHead(LinkList *L,int n){
LinkList p, r;
*L = (LinkList)malloc(sizeof(Node));
r = *L; //r一直作为尾部的节点
for(int i=0;i < n;++i){
p = (LinkList)malloc(sizeof(Node)); //生成新节点
p = r->next;//新节点作为当前尾结点的后继
p->data = i;
r = p;//更新尾结点
}
r->next = NULL;//尾结点最后指向NULL,表示链表到此结束。
}
两种创建方法可以按使用情况来选择。
单链表的整表删除
前面我们学到,单链表的创建是用遍历循环,依次malloc()出新的节点,那么要删除之前创建的链表呢?是利用while循环依次free()来释放内存删除。
整表删除算法:
- 声明指针p和q;
- 把链表第一个节点(头指针的后继)的值赋给p;
- while循环:把p的后继赋给q,然后释放p的内存,再将q当前的值(删除前p的后继)赋给p;
- 最后让头结点指向NULL。
void ClearList(LinkList *L){
LinkList p,q;
p = (*L)->next;
while(p){ //只要p还能指向实际的内存,循环就继续
q = p->next;
free(p);
p = q;
}
(*L)->next;
}
不得不说,C语言的内存管理真的要好好学了。
单链表和顺序存储的选择
- 若线性表需要频繁查找,很少进行插入和删除等操作时,优先使用顺序存储结构。
- 当线性表元素个数变化较大或者根本不知道最终会有多大的线性表时,应该采用单链表