C语言——链表及链表反转

目录

1.什么是链表

1.1基本结构

1.2链表的特点

优点:

缺点:

1.3基本操作示例

创建链表

遍历链表

插入节点(头部插入和尾部插入)

删除节点(按值删除)

2.链表的反转

1.个人在第一次解决链表反转使用的方法

2.迭代法(最常用)

4.头插法

3.链表的一下有趣问题

1.所以链表就是很多个结构体组成的?

2.那有数组的话为什么还要链表呢?

3.我如何查询链表呢?只能通过遍历吗?

[1. 顺序遍历查找](#1. 顺序遍历查找)

[2. 按索引查找(模仿数组)](#2. 按索引查找(模仿数组))

[3. 查找前一个节点](#3. 查找前一个节点)

4.创建链表的时候,一般是结构体指针,那我如果创建的是结构体的话,有什么区别吗?

方法一:使用结构体指针(动态分配)

方法二:使用结构体变量(静态分配)


1.什么是链表

链表 是一种线性数据结构,由一系列节点组成,每个节点包含:

  • 数据域:存储实际数据

  • 指针域:存储下一个节点的内存地址

1.1基本结构

在C语言中,链表通常这样定义:

cpp 复制代码
// 定义链表节点
struct ListNode {
    int val;               // 数据域(存储整数值)
    struct ListNode *next; // 指针域(指向下一个节点)
};

1.2链表的特点

优点:

  1. 动态大小:无需预先指定大小,可以动态增长和缩小

  2. 高效插入删除:在已知位置插入删除的时间复杂度为O(1)

  3. 内存利用率:不需要连续内存空间

缺点:

  1. 随机访问慢:访问第n个元素需要O(n)时间

  2. 额外内存开销:每个节点都需要存储指针

  3. 缓存不友好:节点在内存中不连续

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;  // 头指针指向第一个节点