一、链表概述
1.1 什么是链表
链表在数据结构中的位置大概如下图所示:
由上图可知以及顾名思义,链表即为链式结构+线性表
- 链式结构:数据无需占用整块内存空间,每个结点后附加一个字段,用来存放后继元素的存储地址,常借助程序设计语言的指针类型来描述
- 线性表:一个线性表是n个具有相同特性的数据元素的有限序列,元素之间为一对一的关系,可以理解为一个结点只能有一个后继(类似自变量与因变量的关系),但可以有多个前驱(多个线性表叠在一起)
注意:值相同不一定是一个结点。在本文中结点与节点均表达同一个意思
1.2 链表的相关概念
教科书中对头结点的定义与此处不同,暂以此文中为准。
结点、虚拟结点、头指针、头结点(首元结点)
每一个结点均由数据域和指针域构成,虚拟结点是首元结点(头结点)之前的附设结点,其指针域指向首元结点。头结点是存储第一个数据的结点。头指针指向第一个结点,有虚拟结点则指向虚拟节点,无虚拟结点则指向头结点。
代码中一般表示为:
dummyNode.next = head
//head为首元结点
//dummyNode为头结点(虚拟结点)
单链表
n个结点按照线性表的结构链接在一起即为线性链表或单链表
其他链表
根据结点所含指针个数、指向、连接方式,链表还有循环链表、双向链表、二叉链表、十字链表、邻接表、邻接多重表等分类。单链表、循环链表和双向链表用于实现线性表的链式存储结构,其他多用于实现非线性结构。
二、 如何构造链表
参考LeetCode中一般采用的方式,给出如下定义:
typedef struct ListNode{
int val; //数据
struct ListNode *next; //指针
};
创建头结点示例:
创建一个值为0 1 2 3 4 的链表(无虚拟结点)
struct ListNode* initLink(){
int i;
//1. 创建头指针
struct ListNode* p = NULL;
//2. 创建头结点
struct ListNode* temp = (struct ListNode*)malloc(sizeof(struct ListNode));
temp->val = 0;
temp->next = NULL;
//头指针指向头结点,此时temp可以用来指向别的结点了
p = temp;
//3. 每创建一个结点,令其直接前驱的指针指向它
for(i = 1; i < 5;i++){
struct ListNode* a = (struct ListNode*)malloc(sizeof(struct ListNode));
a->val = i;
a->val = NULL;
//temp指向新创建好的上一个结点
temp->next = a;
//为下一次添加结点做准备
temp = temp->next;
}
return p;
}
三、 链表的增删改查
3.1 遍历链表
遍历是单链表增删改查的基础,且只能通过从头逐个访问来实现,因此表头非常重要。
代码如下:
//打印链表
void printList(struct* ListNode* p){
//temp指针来遍历
struct ListNode* temp = p;
while(temp != NULL){
//struct ListNode* f = temp; 释放操作,逐个操作,需要再用
printf("%d ",tmep->val);
temp = temp->next;
//free(f);
}
print("\n");
}
//获取链表长度
int32_t getLength(struct ListNode* p ){
struct ListNode* temp = p;
int length = 0;
while(temp != NULL){
//struct ListNode* f = temp; 释放操作,逐个操作,需要再用
length++;
temp = temp->next;
//free(f);
}
return length;
}
在遍历的基础上可完成查找、取值等一系列操作。
3.2插入链表
根据插入位置的不同我们可以将链表的插入操作分为三种情况,分别为:在表头插入、在中间插入、在尾部插入
3.2.1 在表头插入
由前文可知头指针十分重要,因此如何处理头指针十分关键。
创建新结点——新结点指向旧头结点——头指针指向新头结点——完成
3.2.2 在中间插入
创建新结点——新结点指向其后继节点——新节点的前驱指向新节点——完成
3.2.3 在尾部插入
创建新节点——旧尾结点指向新节点——新结点指向NULL——完成
将以上三种情况综合考虑,写出对应的插入函数:
代码如下:
struct ListNode* InsertNode(struct ListNode* head, struct ListNode* nodeInsert,int position){
//首先判断链表是否非空,若空则返回nodeInsert或异常
if(head == NULL){
return nodeInsert;
}
int size = getLength(head);
if(position > size+1 || position < 1){
print("参数越界");
return head;
}
//头部插入
if(position == 1){
nodeInsert->next = head;
head = nodeInsert;
return head;
}
//非头部插入
struct ListNode* pNode = head;
int count = 1;
while(count < position - 1 ) {//落到position位置的前一个结点
pNode = pNode->next;
count++;
}
nodeInsert->next = pNode->next;
pNode->next = nodeInsert;
return head;
}
3.3 链表的删除
同样可以分为头部删除、中间删除和尾部删除
3.3.1 删除头部
保存旧头指针——头指针指向其后继——释放旧头指针
3.3.2 删除中间
保存被删除结点——被删除结点其前驱指向其后继的后继——释放被删除结点
3.3.3 删除尾部
保存被删除结点——被删除结点的前驱指向NULL——释放被删除结点
综合后代码如下:
struct ListNode* deleteNode(struct ListNode* head, int position){
//首先判断链表是否非空,若空则返回NULL
if(head == NULL){
return NULL;
}
int size = getLength(head);
if(position > size+1 || position < 1){
print("参数越界");
return head;
}
if(position == 1){
struct ListNode* f = head;
head = head->next;
free(f);
return head;
}
else{
struct ListNode* cur = head;
int count = 1;
while(count < postion - 1){
cur = cur->next;
count++;
}
struct ListNode* tmp = cur->next;
cur->next = tmp->next;
free(tmp);
return head;
}
}
这就是全部内容啦!