1.单链表的概念
1.1什么是链表
- 数据元素随机存储在内存中,通过指针维系数据之间“一对一”的逻辑关系,这样的存储结构就是链表。
思考:
第一个图满足单链表要求,第二个图不满足。
- 链表要环环相扣,核心是一个结点只能有一个后继,但一个结点可以有多个被指向。
(可以理解为,法律倡导一夫一妻,你只能爱一个人,但可以有多个人爱你。)
- 注意可能两个结点的值相同,但并不是同一个结点,例如下图:
1.2链表的相关概念
- 结点:数据元素的存储映像,由数据域(值)和指针域(指向下一个结点的地址)两部分组成。
- 头指针:链表中第一个结点的存储位置,是指向链表第一个结点的指针。
- 头结点:是在链表的首元结点之前附设的一个结点。
- 首元结点:链表中存储第一个数据元素a1的结点。
- *虚拟结点dummyNode:是指一个结点dummyNode,其next指针指向head,即:dummyNode.next=head.
注意:
1、如果我们在算法里使用了虚拟结点,则要注意如果要获得head结点,或者从方法(函数)里返回的时候,则应使用dummyNode.next。
2、dummyNode的val不会被使用,初始化为0或者-1等都是可以的。
虚拟结点用处:1.便于处理首元结点 2.扩充链表的长度
2 .如何构造链表
在LeetCode里一般采用如下的方式:
struct ListNode {
int val; //代表数据
struct ListNode* next; //代表指针
};
例如构造创建一个值为0 1 2 3 4 的链表,代码如下:
#include <stdlib.h>
#include <stdio.h>
struct ListNode {
int val; //代表数据
struct ListNode* next; //代表指针
};
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;
//头指针指向头结点
p = temp;
//3、每创建一个结点,都令其直接前驱结点的指针指向它
for (i = 1; i < 5; i++) {
//创建一个结点
struct ListNode* a = (struct ListNode*)malloc(sizeof(struct ListNode));
a->val = i;
a->next = NULL;
//每次 temp 指向的结点就是 a 的直接前驱结点
temp->next = a;
//temp指向下一个结点(也就是a),为下次添加结点做准备
temp = temp->next;
}
return p;
}
int main() {
struct ListNode* p = NULL;
printf("初始化链表为:\n");
//创建链表{1,2,3,4}
p = initLink();
return 0;
}
3.链表的增删查改
3.1遍历链表
代码如下:
//打印链表
void printList(struct ListNode* p) {
struct ListNode* temp = p;//temp指针用来遍历链表
//只要temp指向结点的next值不是NULL,就执行输出语句。
while (temp) {
// struct ListNode* f = temp;//准备释放链表中的结点
printf("%d ", temp->val);
temp = temp->next;
// free(f);
}
printf("\n");
}
//获取链表的长度
int getLength(struct ListNode* p) {
struct ListNode* temp = p;//temp指针用来遍历链表
int length = 0;
//只要temp指向结点的next值不是NULL,就执行输出语句。
while (temp) {
// struct ListNode* f = temp;//准备释放链表中的结点
length++;
temp = temp->next;
// free(f);
}
return length;
}
int main() {
struct ListNode* p = NULL;
printf("creat list:\t\n");
//创建链表{1,2,3,4}
p = initLink();
printList(p);
int length = getLength(p);
printf("list length:%d\n",length);
return 0;
}
3.2 链表插入
单链表的插入操作要考虑三种情况:首部、中部和尾部。
(1)在链表的表头插入(头插法)
代码段如下:
newnode->next=head;
head=newNode;
(2)在链表中间插入
例如在15和7之间插入一个新节点,代码段如下:
newNode = (struct ListNode *) malloc(sizeof(struct ListNode));
p = firstNode;//用指针p遍历链表
while (p->val!= 15){
p = p->next;
}
newNode = (struct ListNode *) malloc(sizeof(struct ListNode));
newNode->val = 3;
newNode->next = p->next;
p->next = newNode;
其中newNode->next = p->next; p->next = newNode;两行代码顺序不可以调换。
(3)在链表的结尾插入(尾插法)
代码段如下:
newNode = (struct ListNode *) malloc(sizeof(struct ListNode));
p = firstNode;//用p指针操作链表
while (p->next){
p = p->next;
}
newNode->val = 2;
newNode->next = NULL;
p->next = newNode;
3.3链表删除
删除同样分为删除头部元素,删除中间元素和删除尾部元素。
(1)删除表头结点
一般只需执行head=head->next.
p = firstNode;
r = p;
p = p->next;
firstNode = p;
free(r);
(2)删除最后一个结点
例如删除40,其前驱节点为7。遍历时要判断cur->next是否为40,如果是,则只要执行cur->next=null,结点40可以放心删掉。
p = firstNode;
while (p->val!=7){
p = p->next;
}
r = p->next;
// r为尾结点
p->next = NULL;
free(r);
(3)删除中间结点
p = firstNode;
while (p->val!= 15){
//遍历到尾结点的前驱结点
p = p->next;
}
r = p->next;
p->next = p->next->next;
free(r);