本片文章将会带你从指针开始,从易到难逐步过渡到链表
从指针到链表:深入理解 C 语言中的链表结构
链表是408的热门考点之一,这里用c语言的方式带大家从指针开始逐步理解链表
一、啥是指针
在 C 语言中,指针是一种特殊的数据类型,它存储另一个变量的内存地址,下面是指针的基本操作:
1.1 指针的定义和初始化
int a = 10; // 定义一个整数变量
int *p = &a; // 定义一个指针变量,并初始化为变量 a 的地址
/*
int *p = &a;
上面这一步还可以写为
int *p;
p = &a
*/
这里,p
是一个指向 int
类型的指针,它存储了变量 a
的地址。我们可以通过 *p
来访问 p
指向的值(即 a
的值)。
1.2 指针的使用
用于访问变量的值
int b = 20;
p = &b; // 修改指针 p 指向变量 b 的地址
*p = 30; // 通过指针修改 b 的值为 30
1.3 动态内存分配
动态内存分配允许在程序运行时请求内存:
malloc:分配一块指定大小的内存,并返回指向该内存的指针。
int *p = (int *)malloc(sizeof(int)); // 分配一个整数大小的内存
if (p != NULL) {
*p = 30; // 使用分配的内存
}
free:释放之前分配的内存,防止内存泄漏。
free(p); // 释放内存
p = NULL; // 将指针置为 NULL
二、链表概述
链表是一种链式数据结构,由一系列节点(Node)组成,每个节点包含数据和指向下一个节点的指针。与数组相比,链表在插入和删除操作上更具灵活性,因为它不需要移动大量元素。
2.1 节点结构定义
在 C 语言中,我们可以使用结构体(struct)来定义链表节点。每个节点包含数据部分和一个指向下一个节点的指针。
typedef struct Node {
int data; // 节点存储的数据
struct Node *next; // 指向下一个节点的指针
} Node;
2.2 链表的基本操作
链表的基本操作包括创建链表、插入节点、删除节点和遍历链表。下面是一些常见操作:
2.2.1 创建链表
Node* createNode(int data) {
Node* newNode = (Node*)malloc(sizeof(Node)); // 分配内存
newNode->data = data; // 设置数据
newNode->next = NULL; // 初始化下一个指针为 NULL
return newNode;
}
2.2.2 插入节点
插入节点有多种方式,比如在链表的头部、尾部或中间。
将节点插入到链表头部的代码:
void insertAtHead(Node** head, int data) {
Node* newNode = createNode(data); // 创建新节点
newNode->next = *head; // 将新节点的下一个指针指向当前头节点
*head = newNode; // 更新头节点为新节点
}
插入至尾部
void insertAtTail(Node** head, int data) {
Node* newNode = createNode(data); // 创建新节点
if (*head == NULL) { // 如果链表为空,则直接设置头节点为新节点
*head = newNode;
return;
}
Node* last = *head; // 初始化 last 指向头节点
while (last->next != NULL) { // 遍历到链表的最后一个节点
last = last->next;
}
last->next = newNode; // 将最后一个节点的 next 指向新节点
}
中间插入,这里展示的是传入上一个位置进行插入的方法,还有一种方法是传入位置,然后for遍历到该位置
void insertAfter(Node* prevNode, int data) {
if (prevNode == NULL) {
printf("不能传入空指针!\n");
return;
}
Node* newNode = createNode(data); // 创建新节点
newNode->next = prevNode->next; // 新节点的 next 指向前一个节点的 next
prevNode->next = newNode; // 前一个节点的 next 指向新节点
}
2.2.3 删除节点
删除含有目标值的节点:
void deleteNode(Node** head, int data) {
Node *temp = *head, *prev = NULL;
if (temp != NULL && temp->data == data) {
*head = temp->next; // 如果要删除的节点是头节点
free(temp); // 释放内存
return;
}
while (temp != NULL && temp->data != data) {
prev = temp;
temp = temp->next;
}
if (temp == NULL) return; // 节点不在链表中
prev->next = temp->next; // 将前一个节点的 next 指针指向被删除节点的下一个节点
free(temp); // 释放内存
}
2.2.4 遍历链表
遍历链表可以用来访问链表中的每一个节点:
void printList(Node* head) {
Node* temp = head;
while (temp != NULL) {
printf("%d -> ", temp->data);
temp = temp->next;
}
printf("NULL\n");
}
三、热门链表题
在480考试中,链表相关的题目是常见的考点。我挑选了几道热门链表题:
3.1 题目1:反转链表
题目描述:
给定一个链表,反转链表并返回其头节点。
解题思路:
反转链表可以通过三个指针来实现:当前节点指针(current),前一个节点指针(prev),和下一个节点指针(next)。
题目链接
代码实现:
Node* reverseList(Node* head) {
Node* prev = NULL;
Node* current = head;
Node* next = NULL;
while (current != NULL) {
next = current->next; // 保存下一个节点
current->next = prev; // 反转当前节点的指针
prev = current; // 移动前一个节点指针
current = next; // 移动当前节点指针
}
head = prev; // 更新头节点为最后一个节点
return head;
}
3.2 题目2:合并两个有序链表
题目描述:
给定两个有序链表,将它们合并为一个有序链表。
解题思路:
使用两个指针分别遍历两个链表,并根据节点值的大小将节点添加到新链表中。
题目链接
代码实现:
// 合并两个已排序的链表
Node* mergeTwoLists(Node* l1, Node* l2) {
Node dummy; // 创建虚拟头节点
Node* tail = &dummy; // 指向虚拟头节点
dummy.next = NULL; // 初始化虚拟头节点的 next 为 NULL
// 当两个链表都不为空时
while (l1 != NULL && l2 != NULL) {
if (l1->data < l2->data) {
tail->next = l1; // 将 l1 的节点添加到结果链表中
l1 = l1->next; // 移动 l1 指针到下一个节点
} else {
tail->next = l2; // 将 l2 的节点添加到结果链表中
l2 = l2->next; // 移动 l2 指针到下一个节点
}
tail = tail->next; // 移动 tail 指针到下一个节点
}
// 如果 l1 还有剩余节点
if (l1 != NULL) {
tail->next = l1; // 将 l1 的剩余部分添加到结果链表中
} else {
tail->next = l2; // 如果 l1 已经遍历完,则将 l2 的剩余部分添加到结果链表中
}
return dummy.next; // 返回合并后的链表头节点
}
3.3 题目3:寻找链表的中间节点
题目描述:
给定一个链表,找出链表的中间节点。如果链表的长度是偶数,则返回第二个中间节点。
原题链接
解题思路:
使用快慢指针。快指针每次移动两步,慢指针每次移动一步。快指针到达链表末尾时,慢指针刚好在中间。
代码实现:
Node* findMiddle(Node* head) {
if (head == NULL) return NULL;
Node* slow = head;
Node* fast = head;
while (fast != NULL && fast->next != NULL) {
slow = slow->next;
fast = fast->next->next;
}
return slow;
}
3.4 题目4: 反转链表
题目描述:
给定一个单链表,反转该链表。
解题思路:
迭代法:使用三个指针 prev, current, 和 next 来逐步反转链表。
初始化:
prev:指向反转后的链表的当前头节点。
current:指向当前正在处理的节点。
next:暂时保存 current 的下一个节点。
循环:
每次循环中,next 保存 current 的下一个节点,防止丢失。
current->next 指向 prev,完成反转操作。
prev 和 current 向前移动一位。
重复此过程直到 current 为 NULL。
代码实现:
Node* reverseList(Node* head) {
Node* prev = NULL;
Node* current = head;
Node* next = NULL;
while (current != NULL) {
next = current->next; // 保存下一个节点
current->next = prev; // 反转当前节点的 next
prev = current; // 移动 prev 和 current
current = next;
}
return prev; // 最后 prev 就是新的头节点
}
四、408链表原题解析
4.1 真题1: 重排链表(19年)
将链表的前半部分和后半部分交替插入到新的链表中
题目链接
思路:
1、找到链表的中点,并将链表拆分成两部分。
2、逆置后半部分链表。
3、合并前半部分和逆置后的后半部分链表。
代码
// 寻找链表的中间节点
struct ListNode* middleNode(struct ListNode* head) {
struct ListNode* slow = head; // 慢指针,每次移动一步
struct ListNode* fast = head; // 快指针,每次移动两步
// 当快指针及其下一个节点都不为空时继续移动
while (fast != NULL && fast->next != NULL) {
slow = slow->next; // 慢指针移动一步
fast = fast->next->next; // 快指针移动两步
}
return slow; // 返回中间节点
}
// 反转链表
struct ListNode* reverseList(struct ListNode* head) {
struct ListNode* prev = NULL; // 用于记录反转后的链表的当前头节点
struct ListNode* curr = head; // 当前正在处理的节点
// 遍历链表
while (curr != NULL) {
struct ListNode* nextTemp = curr->next; // 保存当前节点的下一个节点
curr->next = prev; // 反转当前节点的 next 指针
prev = curr; // 移动 prev 和 curr
curr = nextTemp;
}
return prev; // 返回反转后的链表的头节点
}
// 合并两个链表
void mergeList(struct ListNode* l1, struct ListNode* l2) {
struct ListNode* l1_tmp;
struct ListNode* l2_tmp;
// 当两个链表都不为空时
while (l1 != NULL && l2 != NULL) {
l1_tmp = l1->next; // 保存 l1 的下一个节点
l2_tmp = l2->next; // 保存 l2 的下一个节点
// 交错合并链表
l1->next = l2; // l1 的下一个节点变为 l2
l1 = l1_tmp; // 移动 l1
l2->next = l1; // l2 的下一个节点变为 l1
l2 = l2_tmp; // 移动 l2
}
}
// 重排链表
void reorderList(struct ListNode* head) {
if (head == NULL) {
return; // 如果链表为空,直接返回
}
struct ListNode* mid = middleNode(head); // 找到中间节点
struct ListNode* l1 = head; // 第一部分链表
struct ListNode* l2 = mid->next; // 第二部分链表
mid->next = NULL; // 断开两部分链表
// 反转第二部分链表
l2 = reverseList(l2);
// 合并两部分链表
mergeList(l1, l2);
}
4.2 真题2: 移除重复节点(带有表头结点的单链表)(15年)
题目:移除未排序链表中的重复节点。保留最开始出现的节点。
源题链接(真题根据此题出的,只在源题上增加了头节点)
思路:双重循环遍历
代码:
// 删除链表中的重复节点
struct ListNode* removeDuplicateNodes(struct ListNode* head) {
struct ListNode* ob = head; // 外层循环的指针,用于遍历链表
// 遍历链表
while (ob != NULL && ob->next != NULL) {
struct ListNode* oc = ob; // 内层循环的指针,用于查找重复元素
// 遍历剩余的链表,查找与 ob->next 相同的值
while (oc->next != NULL) {
// 如果找到了重复的值
if (oc->next->val == ob->next->val) {
oc->next = oc->next->next; // 跳过重复节点
} else {
oc = oc->next; // 移动 oc 到下一个节点
}
}
ob = ob->next; // 移动 ob 到下一个节点
}
return head; // 返回处理后的链表头节点
}
五、结语
408数据结构题大部分在leetcode中都是有原型的,可以尝试先将热门100题中的简单题和大部分中等难度题给解决,相信你一定会有所收获
希望这篇文章能够帮助到你,有任何疑问都可以在评论区提出,我会尽我所能解答