目录
[1. 顺序遍历查找](#1. 顺序遍历查找)
[2. 按索引查找(模仿数组)](#2. 按索引查找(模仿数组))
[3. 查找前一个节点](#3. 查找前一个节点)
4.创建链表的时候,一般是结构体指针,那我如果创建的是结构体的话,有什么区别吗?
1.什么是链表
链表 是一种线性数据结构,由一系列节点组成,每个节点包含:
-
数据域:存储实际数据
-
指针域:存储下一个节点的内存地址
1.1基本结构
在C语言中,链表通常这样定义:
cpp
// 定义链表节点
struct ListNode {
int val; // 数据域(存储整数值)
struct ListNode *next; // 指针域(指向下一个节点)
};
1.2链表的特点
优点:
-
动态大小:无需预先指定大小,可以动态增长和缩小
-
高效插入删除:在已知位置插入删除的时间复杂度为O(1)
-
内存利用率:不需要连续内存空间
缺点:
-
随机访问慢:访问第n个元素需要O(n)时间
-
额外内存开销:每个节点都需要存储指针
-
缓存不友好:节点在内存中不连续
1.3基本操作示例
创建链表
cpp
// 创建新节点
struct ListNode* createNode(int value) {
struct ListNode* newNode = (struct ListNode*)malloc(sizeof(struct ListNode));
newNode->val = value;
newNode->next = NULL;
return newNode;
}
// 构建链表: 1->2->3->4
struct ListNode* head = createNode(1);
head->next = createNode(2);
head->next->next = createNode(3);
head->next->next->next = createNode(4);
遍历链表
cpp
void printList(struct ListNode* head) {
struct ListNode* current = head;
while (current != NULL) {
printf("%d -> ", current->val);
current = current->next;
}
printf("NULL\n");
}
插入节点(头部插入和尾部插入)
cpp
// 在头部插入
void insertAtHead(struct ListNode** head, int value) {
struct ListNode* newNode = createNode(value);
newNode->next = *head;
*head = newNode;
}
// 在尾部插入
void insertAtTail(struct ListNode** head, int value) {
struct ListNode* newNode = createNode(value);
if (*head == NULL) {
*head = newNode;
return;
}
struct ListNode* current = *head;
while (current->next != NULL) {
current = current->next;
}
current->next = newNode;
}
删除节点(按值删除)
cpp
void deleteNode(struct ListNode** head, int value) {
if (*head == NULL) return;
// 如果要删除头节点
if ((*head)->val == value) {
struct ListNode* temp = *head;
*head = (*head)->next;
free(temp);
return;
}
// 查找要删除的节点
struct ListNode* current = *head;
while (current->next != NULL && current->next->val != value) {
current = current->next;
}
if (current->next != NULL) {
struct ListNode* temp = current->next;
current->next = current->next->next;
free(temp);
}
}
2.链表的反转
1.个人在第一次解决链表反转使用的方法
准确来说,他不是反转链表,而是创建了一个新链表,与原链表相反
cpp
struct ListNode* ReverseList(struct ListNode* head) {
if (head == NULL) return NULL;
struct ListNode* node = NULL;
struct ListNode* last = NULL;
struct ListNode* current = head; // 保护原head,使用current遍历
while (current != NULL) {
node = (struct ListNode*)malloc(sizeof(struct ListNode));
node->val = current->val; // 复制值
node->next = last;
last = node;
current = current->next; // 移动保护后的指针
}
return last;
}
该方法的运行逻辑是,在每次循环时,创建一个新的结点,来保存原链表的头节点,每次循环的新节点插入上一次循环的新节点的前面,实现链表反转。
优点是:保存了原链表,代码逻辑简单
缺点是:每个节点都需要重新分配内存(内存开销大),频繁的malloc操作影响效率,空间复杂度高
2.迭代法(最常用)
cpp
// 迭代法反转链表
struct ListNode* reverseList(struct ListNode* head) {
struct ListNode *prev = NULL;
struct ListNode *curr = head;
struct ListNode *next = NULL;
while (curr != NULL) {
next = curr->next; // 保存下一个节点
curr->next = prev; // 反转指针
prev = curr; // 移动prev
curr = next; // 移动curr
}
return prev; // 新的头节点
}
与我自己的方法,有相似点,都是在新链表头部插入。区别在于我个人的方式复制一份头结点,迭代法直接将头结点的指向修改。
3.递归法
cpp
// 递归法反转链表
struct ListNode* reverseListRecursive(struct ListNode* head) {
// 递归终止条件
if (head == NULL || head->next == NULL) {
return head;
}
// 递归反转剩余部分
struct ListNode* newHead = reverseListRecursive(head->next);
// 调整指针
head->next->next = head;
head->next = NULL;
return newHead;
}
4.头插法
cpp
// 头插法反转链表
struct ListNode* reverseListHeadInsert(struct ListNode* head) {
struct ListNode *newHead = NULL;
struct ListNode *curr = head;
struct ListNode *next = NULL;
while (curr != NULL) {
next = curr->next; // 保存下一个节点
curr->next = newHead; // 当前节点插入到新链表头部
newHead = curr; // 更新新链表头
curr = next; // 移动到下一个节点
}
return newHead;
}
好像和迭代法没区别,只是换了变量名,不过一个是凸显每次调转指针指向,一个是为了凸显在新链表头部插入
5.使用栈(辅助空间法)
cpp
#include <stdio.h>
#include <stdlib.h>
#define MAX_SIZE 100
// 使用栈反转链表
struct ListNode* reverseListUsingStack(struct ListNode* head) {
if (head == NULL) return NULL;
// 创建栈
struct ListNode* stack[MAX_SIZE];
int top = -1;
// 将节点压入栈
struct ListNode* curr = head;
while (curr != NULL && top < MAX_SIZE - 1) {
stack[++top] = curr;
curr = curr->next;
}
// 新的头节点是栈顶元素
struct ListNode* newHead = stack[top];
curr = newHead;
// 弹出栈中元素构建反转链表
while (top > 0) {
curr->next = stack[--top];
curr = curr->next;
}
curr->next = NULL; // 最后一个节点的next设为NULL
return newHead;
}
不是很想了解,同为储存结构,暂时不想交合使用
3.链表的一下有趣问题
1.所以链表就是很多个结构体组成的?
链表就是由许多个结构体实例通过指针连接组成的。
2.那有数组的话为什么还要链表呢?
核心区别:内存布局
数组:连续内存
链表:非连续内存
什么时候用数组?什么时候用链表?
使用数组当:
-
✅ 数据量固定或可预测
-
✅ 需要频繁随机访问
-
✅ 内存紧张
-
✅ 需要缓存友好性
使用链表当:
-
✅ 数据量变化频繁
-
✅ 频繁插入删除
-
✅ 不需要随机访问
-
✅ 内存充足
简单说:
-
如果你主要做查询 → 用数组
-
如果你主要做插入删除 → 用链表
这就是为什么两种数据结构都需要存在的原因!它们互补而非替代。
3.我如何查询链表呢?只能通过遍历吗?
链表只能通过遍历来查询,这是链表的一个重要特性。
因为链表的节点在内存中是随机分布的,每个节点只知道自己下一个节点的地址:
1. 顺序遍历查找
cpp
// 查找值为target的节点
struct ListNode* findNode(struct ListNode* head, int target) {
struct ListNode* current = head;
while (current != NULL) {
if (current->val == target) {
return current; // 找到目标节点
}
current = current->next;
}
return NULL; // 没找到
}
2. 按索引查找(模仿数组)
cpp
// 获取第index个节点(索引从0开始)
struct ListNode* getNodeAtIndex(struct ListNode* head, int index) {
struct ListNode* current = head;
int count = 0;
while (current != NULL) {
if (count == index) {
return current;
}
count++;
current = current->next;
}
return NULL; // 索引超出范围
}
3. 查找前一个节点
cpp
// 查找某个节点的前一个节点
struct ListNode* findPrevious(struct ListNode* head, struct ListNode* target) {
if (head == NULL || head == target) {
return NULL; // 头节点没有前驱
}
struct ListNode* current = head;
while (current->next != NULL) {
if (current->next == target) {
return current;
}
current = current->next;
}
return NULL; // 没找到
}
4.创建链表的时候,一般是结构体指针,那我如果创建的是结构体的话,有什么区别吗?
方法一:使用结构体指针(动态分配)
cpp
// 创建新节点(在堆上分配内存)
struct ListNode* createNode(int value) {
struct ListNode* newNode = (struct ListNode*)malloc(sizeof(struct ListNode));
newNode->val = value;
newNode->next = NULL;
return newNode; // 返回指针
}
// 构建链表(所有节点在堆上)
struct ListNode* head = createNode(1); // 堆内存
head->next = createNode(2); // 堆内存
head->next->next = createNode(3); // 堆内存
head->next->next->next = createNode(4); // 堆内存
方法二:使用结构体变量(静态分配)
cpp
// 直接创建结构体变量(在栈上分配内存)
struct ListNode node1 = {1, NULL};
struct ListNode node2 = {2, NULL};
struct ListNode node3 = {3, NULL};
struct ListNode node4 = {4, NULL};
// 连接节点
node1.next = &node2; // 使用取地址符&
node2.next = &node3;
node3.next = &node4;
struct ListNode* head = &node1; // 头指针指向第一个节点