开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第2天,点击查看活动详情
链表
链表和数组的作用相同。都是用来存储数据。创建数组时,我们会直接分配出所有我们需要的内存。但是对于链表,我们每次只分配出一个节点(node) 的内存。链表使用指针将各个节点组合到一起,这样就形成了一个连一个的链式的结构,这就是链表(Linked List)这个名称的由来。
链表的结构
由图可知,链表是由一个一个的节点组成的。每一个节点分为两个部分
- 数据域:用于储存节点数据
- 指针域:用于指向下一个节点
所以就需要用到结构体,一个简单的链表结构体定义如下
struct link {
int data;//数据域
struct link *next;//指针域,next意为指向下一个link结构体的指针
};
我们使用 malloc() 函数来为每个节点分配内存。节点的头部只含有指向第一个节点的指针。
链表特殊的储存结构决定了对链表数据特殊的访问方式,即单链表只能顺序访问,不能随机访问。那么想要访问单链表,首先要找到链表的头。
链表的头,我们用头指针来表示它,所以一个完整的链表结构示意图如下
关于头节点:
头指针:
- 头指针是指向第一个结点的指针,若链表有头结点,则是指向头结点的指针
- 头指针具有标识作用,所以常用的头指针冠以链表的名字(指针变量的名字)
- 无论链表是否为空,头指针均不为空
- 头指针是链表的必要元素
头节点:
- 头结点是为了操作的统一和方便而设立的(比如使用头插法的时候),放在第一个元素的结点之前,其数据域一般无意义(但也可以存放链表的长度)
- 可以没有头节点
带头节点的单链表:
不带头节点的单链表:
单链表的头插法建立(不带头节点)
链表结构如下:
struct Node {
int data;//数据域
struct Node *next;//指针域,next意为指向下一个Node结构体的指针
};
typedef struct Node node;
- 定义一个指向Node结构体的指针,这个指针就是头指针。
node *head;
head = NULL;//指向NULL就是指向空的意思
- 定义并初始化一个节点pnew,将head指向节点
node *pnew;//新节点
pnew = (node *)malloc(sizeof(node));//为节点分配内存
pnew->next = NULL;//先将新节点的next置为NULL;
if(pnew == NULL){
printf("内存不足");
exit(0);
}
因为我们建立的单链表不带头节点,所以进行头插的时候需要分两种情况,一种是插入第一个节点时,其他节点的插入为第二种情况。
if(head == NULL){
head = pnew;//将头指针head指向新节点
}
第二个节点插入链表,图解:
大家想一想在第二个节点插入链表的过程中,是a操作在前还是b操作在前?
pnew->next = head;//pnew指向head指向的节点
head = pnew;//head头指针指向pnew
之后节点的插入过程与第二个插入过程一样。
节点的插入与这个过程也是一样的。
总结几点建立单链表需要注意的过程:
- 需要定义头指针,初始化节点时需要为节点分配内存。
- 注意每个节点还有头指针的指向,建议画图理解。
- 将最后一个节点指向NULL。
尾插法
类似于头插法的道理,只不过此次的插入的位置是在链表的尾部,所以首先把文件遍历到尾部,然后进行操作。
#include <stdio.h>
#include <stdlib.h>
typedef struct node{
int data;
struct node *next;
}lnode;
// 头插法创建链表
lnode* createHead(){
lnode *head,*p,*q;
head=p=(lnode *)malloc(sizeof(lnode));
p->next=NULL;
for (int i = 0; i < 10; i++) {
q=(lnode *)malloc(sizeof(lnode));
q->data=i;
q->next=p->next;
p->next=q;
}
return head;
}
// 尾插法创建链表
lnode* createTail(){
lnode *head,*p,*q;
head=p=(lnode *)malloc(sizeof(lnode));
for (int i = 0; i < 10; i++) {
q=(lnode *)malloc(sizeof(lnode));
q->data=i;
q->next=NULL;
p->next=q;
p=q;
}
return head; //head是头
}
//遍历链表
void print(lnode *list){
lnode *p=list->next;
while(p!=NULL){
printf("%d\n",p->data);
p=p->next;
}
}
int main()
{
lnode *list;
list=createTail();//创建一个单链表,尾插法
print(list);
return 0;
}
节点的删除
假设有如图所示的链表
cur节点就是我们要删除的节点,我们怎么删除它呢?
只需这样一行代码就搞定,但是由于建立链表的时候,我们为每一个节点都分配了内存,所以我们在删除节点的时候也应该释放其内存,用free()函数来释放内存。
pre->next = cur->next;//让前一节点的指针域指向待删节点的下一节点
free(cur);//释放删除节点的内存
拓展:循环链表和双向链表
循环链表
将单链表中最后结点的指针端由指向NULL改为指向头结点(或第一个节点),就使整个单链表形成一个环,这种头尾相接的单链表称为循环单链表,简称循环链表(circular linked list)。和单向链表一样,循环链表不一定必须有头结点,但是带有头结点循环链表使得空链表和非空链表的处理一致。
双向链表
双向链表(double linked list)是在单链表的每个结点中,在设置一个指向其前驱结点的指针域。双向链表有两个指针域,一个指向前驱一个指向后继。其结构可以设计成:
typedef struct DulNode
{
int data;
struct DulNode * prev;
struct DulNode * next;
}DulNode,
typedef DulNode * pDuLinkList;
双向链表能够反向遍历。