考研408之链表精讲

本片文章将会带你从指针开始,从易到难逐步过渡到链表

从指针到链表:深入理解 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题中的简单题和大部分中等难度题给解决,相信你一定会有所收获

希望这篇文章能够帮助到你,有任何疑问都可以在评论区提出,我会尽我所能解答

  • 14
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值