3. 单链表

3. 单链表

文章目录

3.1 概念与结构

概念:链表是⼀种物理存储结构上非连续、非顺序 的存储结构,数据元素的逻辑顺序通过 链表中的 指针链接次序实现的

淡季时⻋次的⻋厢会相应减少,旺季时⻋次的⻋厢会额外增加⼏节。只需要将⽕⻋⾥的某节⻋厢去掉/ 加上,不会影响其他⻋厢,每节⻋厢都是独⽴存在的。

在链表⾥,每节"⻋厢"是什么样的呢?

3.1.1 结点

与顺序表不同的是,链表⾥的每节"⻋厢"都是独⽴申请下来的空间,我们称之为"结点/结点"

结 点的组成主要有两个部分:当前结点要保存的数据和保存下⼀个结点的地址(指针变量)。

图中指针变量plist保存的是第⼀个结点的地址,我们称plist此时"指向"第⼀个结点,如果我们希望 plist"指向"第⼆个结点时,只需要修改plist保存的内容为0x0012FFA0。

链表中每个结点都是独⽴申请的(即需要插⼊数据时才去申请⼀块结点的空间),我们需要通过指针 变量来保存下⼀个结点位置才能从当前结点找到下⼀个结点。

3.1.2 链表的性质

1、链式机构在逻辑上是连续的,在物理结构上不⼀定连续 2、结点⼀般是从堆上申请的

3、从堆上申请来的空间,是按照⼀定策略分配出来的,每次申请的空间可能连续,可能不连续

结合前⾯学到的结构体知识,我们可以给出每个结点对应的结构体代码: 假设当前保存的结点为整型:

复制代码
struct SListNode
{
int data;       //结点数据
struct SListNode* next; //指针变量⽤保存下⼀个结点的地址

};

当我们想要保存⼀个整型数据时,实际是向操作系统申请了⼀块内存,这个内存不仅要保存整型数 据,也需要保存下⼀个结点的地址(当下⼀个结点为空时保存的地址为空)。

当我们想要从第⼀个结点⾛到最后⼀个结点时,只需要在当前结点拿上下⼀个结点的地址就可以了。

3.1.3 链表的打印

给定的链表结构中,如何实现结点从头到尾的打印?

思考:当我们想保存的数据类型为字符型、浮点型或者其他⾃定义的类型时,该如何修改?

3.2 实现单链表

LinkList.h

复制代码
#include <stdio.h>
#include <stdlib.h>

// 定义链表节点结构体
typedef struct MyListNode {
    int val;
    struct MyListNode* next;
} MyListNode;

// 定义链表结构体,包含链表长度和虚拟头节点
typedef struct MyLinkedList {
    int size;
    MyListNode* dummyHead;
} MyLinkedList;

// 初始化链表
MyLinkedList* myLinkedListCreate();

// 获取指定下标的值
int myLinkedListGet(MyLinkedList* obj, int index);

// 头插法
void myLinkedListAddAtHead(MyLinkedList* obj, int val);

// 尾插法
void myLinkedListAddAtTail(MyLinkedList* obj, int val);

// 在指定下标插入节点
void myLinkedListAddAtIndex(MyLinkedList* obj, int index, int val);

// 删除指定下标的节点
void myLinkedListDeleteAtIndex(MyLinkedList* obj, int index);

// 打印链表
void myLinkedListPrint(MyLinkedList* obj);

// 释放链表
void myLinkedListFree(MyLinkedList* obj);

LinkList.c

复制代码
#include "LinkList.h"

// 初始化链表
MyLinkedList* myLinkedListCreate() {
    MyLinkedList* obj = (MyLinkedList*)malloc(sizeof(MyLinkedList));
    obj->size = 0;
    obj->dummyHead = (MyListNode*)malloc(sizeof(MyListNode));
    obj->dummyHead->next = NULL;
    return obj;
}

// 获取指定下标的值
int myLinkedListGet(MyLinkedList* obj, int index) {
    if (index < 0 || index >= obj->size) return -1;
    MyListNode* cur = obj->dummyHead->next;
    for (int i = 0; i < index; i++) {
        cur = cur->next;
    }
    return cur->val;
}

// 头插法
void myLinkedListAddAtHead(MyLinkedList* obj, int val) {
    MyListNode* newNode = (MyListNode*)malloc(sizeof(MyListNode));
    newNode->val = val;
    newNode->next = obj->dummyHead->next;
    obj->dummyHead->next = newNode;
    obj->size++;
}

// 尾插法
void myLinkedListAddAtTail(MyLinkedList* obj, int val) {
    MyListNode* newNode = (MyListNode*)malloc(sizeof(MyListNode));
    newNode->val = val;
    newNode->next = NULL;

    MyListNode* cur = obj->dummyHead;
    while (cur->next != NULL) {
        cur = cur->next;
    }
    cur->next = newNode;
    obj->size++;
}

// 在指定下标插入节点
void myLinkedListAddAtIndex(MyLinkedList* obj, int index, int val) {
    if (index > obj->size) return;
    if (index < 0) index = 0;

    MyListNode* newNode = (MyListNode*)malloc(sizeof(MyListNode));
    newNode->val = val;

    MyListNode* pre = obj->dummyHead;
    for (int i = 0; i < index; i++) {
        pre = pre->next;
    }
    newNode->next = pre->next;
    pre->next = newNode;
    obj->size++;
}

// 删除指定下标的节点
void myLinkedListDeleteAtIndex(MyLinkedList* obj, int index) {
    if (index < 0 || index >= obj->size) return;

    MyListNode* pre = obj->dummyHead;
    for (int i = 0; i < index; i++) {
        pre = pre->next;
    }
    MyListNode* temp = pre->next;
    pre->next = temp->next;
    free(temp);
    obj->size--;
}

// 打印链表
void myLinkedListPrint(MyLinkedList* obj) {
    MyListNode* cur = obj->dummyHead->next;
    printf("链表内容:");
    while (cur != NULL) {
        printf("%d -> ", cur->val);
        cur = cur->next;
    }
    printf("NULL\n");
    printf("链表长度:%d\n\n", obj->size);
}

// 释放链表
void myLinkedListFree(MyLinkedList* obj) {
    MyListNode* cur = obj->dummyHead;
    while (cur != NULL) {
        MyListNode* temp = cur;
        cur = cur->next;
        free(temp);
    }
    free(obj);
}

main.c

复制代码
#include "LinkList.h"

int main() {
    // 创建链表
    MyLinkedList* list = myLinkedListCreate();
    printf("=== 初始化链表 ===\n");
    myLinkedListPrint(list);

    // 头插法测试
    printf("=== 头插法:插入 3, 2, 1 ===\n");
    myLinkedListAddAtHead(list, 3);
    myLinkedListAddAtHead(list, 2);
    myLinkedListAddAtHead(list, 1);
    myLinkedListPrint(list);

    // 尾插法测试
    printf("=== 尾插法:插入 4, 5 ===\n");
    myLinkedListAddAtTail(list, 4);
    myLinkedListAddAtTail(list, 5);
    myLinkedListPrint(list);

    // 指定位置插入
    printf("=== 下标2处插入 99 ===\n");
    myLinkedListAddAtIndex(list, 2, 99);
    myLinkedListPrint(list);

    // 获取元素
    printf("=== 获取下标2的值:%d ===\n\n", myLinkedListGet(list, 2));

    // 删除元素
    printf("=== 删除下标2的节点 ===\n");
    myLinkedListDeleteAtIndex(list, 2);
    myLinkedListPrint(list);

    // 释放内存
    myLinkedListFree(list);

    return 0;
}

运行结果:

3.3链表的分类

链表的结构⾮常多样,以下情况组合起来就有8种(2x2x2)链表结构:

链表说明:

虽然有这么多的链表的结构,但是我们实际中最常⽤还是两种结构:单链表双向带头循环链表

  1. ⽆头单向⾮循环链表:结构简单 ,⼀般不会单独⽤来存数据。实际中更多是作为其他数据结构的⼦结构 ,如哈希桶、图的邻接表等等。另外这种结构在笔试⾯试中出现很多。
  2. 带头双向循环链表:结构最复杂,⼀般⽤在单独存储数据。实际中使⽤的链表数据结构,都是带头 双向循环链表。另外这个结构虽然结构复杂,但是使⽤代码实现以后会发现结构会带来很多优势,实 现反⽽简单了,后⾯我们代码实现了就知道了。

3.4单链表算法题

3.4.2

设计链表

这道题纯基础题来的,主要考察基本功。咱们上面的实现单链表的那些接口函数,其实就是这道题的答案。

因为小编是二刷数据结构了,之前没有养成定时总结博客的好习惯。所以现在在恶补博客,大家一定要定时总结博客呀。

其实我一刷数据结构的时候,就卡了好一段时间在链表,哈哈哈哈,经过几个月的学习,现在已经轻车熟路了

复制代码
// 1. 定义链表节点结构体
typedef struct MyListNode {
    int val;
    struct MyListNode* next;
} MyListNode;

// 2. 定义链表本身结构体(包含大小和虚拟头节点)
typedef struct MyLinkedList{
    int size;
    MyListNode* dummyHead; 
} MyLinkedList;

// 初始化链表
MyLinkedList* myLinkedListCreate() {
    MyLinkedList* obj = (MyLinkedList*)malloc(sizeof(MyLinkedList));
    obj->size = 0;
    // 创建虚拟头节点
    obj->dummyHead = (MyListNode*)malloc(sizeof(MyListNode));
    obj->dummyHead->val = -1;
    obj->dummyHead->next = NULL;
    return obj; // 你原本的代码漏了 return
}

// 获取节点
int myLinkedListGet(MyLinkedList* obj, int index) {
    // 检查下标是否越界
    if (index < 0 || index >= obj->size) {
        return -1;
    }
    
    // cur 从真正的第一个节点开始
    MyListNode* cur = obj->dummyHead->next;
    for (int i = 0; i < index; i++) {
        cur = cur->next;
    }
    return cur->val;
}

// 头插法
void myLinkedListAddAtHead(MyLinkedList* obj, int val) {
    MyListNode* newNode = (MyListNode*)malloc(sizeof(MyListNode));
    newNode->val = val;
    newNode->next = obj->dummyHead->next;
    obj->dummyHead->next = newNode;
    obj->size++;
}

// 尾插法
void myLinkedListAddAtTail(MyLinkedList* obj, int val) {
    MyListNode* cur = obj->dummyHead;
    // 一直往后走,直到找到最后一个节点
    while (cur->next != NULL) {
        cur = cur->next;
    }
    
    MyListNode* newNode = (MyListNode*)malloc(sizeof(MyListNode));
    newNode->val = val;
    newNode->next = NULL;
    cur->next = newNode;
    obj->size++;
}

// 在指定下标插入
void myLinkedListAddAtIndex(MyLinkedList* obj, int index, int val) {
    if (index > obj->size) {
        return;
    }
    if (index < 0) {
        index = 0; // 题目说明如果 index < 0,则在头部插入
    }
    
    // cur 从虚拟头节点开始,走到要插入位置的 前一个节点
    MyListNode* cur = obj->dummyHead;
    for (int i = 0; i < index; i++) {
        cur = cur->next;
    }
    
    MyListNode* newNode = (MyListNode*)malloc(sizeof(MyListNode));
    newNode->val = val;
    newNode->next = cur->next;
    cur->next = newNode;
    obj->size++;
}

// 删除指定下标节点
void myLinkedListDeleteAtIndex(MyLinkedList* obj, int index) {
    if (index < 0 || index >= obj->size) {
        return;
    }
    
    // 走到要删除节点的 前一个节点
    MyListNode* cur = obj->dummyHead;
    for (int i = 0; i < index; i++) {
        cur = cur->next;
    }
    
    MyListNode* temp = cur->next; // temp 就是要删除的节点
    cur->next = temp->next;
    free(temp); // 释放内存
    obj->size--;
}

// 释放整个链表
void myLinkedListFree(MyLinkedList* obj) {
    MyListNode* cur = obj->dummyHead;
    while (cur != NULL) {
        MyListNode* temp = cur;
        cur = cur->next;
        free(temp); // 逐个释放节点
    }
    free(obj); // 最后释放链表结构体
}

3.4.2移除链表元素

移除链表元素

复制代码
/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     struct ListNode *next;
 * };
 */
struct ListNode* removeElements(struct ListNode* head, int val) {
    struct ListNode dummy = {0,head};   //哨兵卫
    struct ListNode* cur = &dummy;
    while(cur->next){
       struct ListNode* nxt = cur ->next;
       if(nxt->val==val) {
        cur->next = nxt->next;
        free(nxt);
       }else{
        cur = cur->next;
       }
    }
    
    
    
    return dummy.next; 
}

3.4.3反转链表

反转链表

其实在王道考研数据结构以及课本教材里,有讲到关于链表逆序(或者称为反转),采用头插法,可以很容易实现。

那么,我们采用头插法实现即可

代码如下:

复制代码
struct ListNode* reverseList(struct ListNode* head) {
    struct ListNode dummy;
    dummy.next = NULL;
    struct ListNode* curr = head;
    while(curr) {
        // 先保存下一个节点
        struct ListNode* next = curr->next;
        // 头插:curr 插到 dummy 后面
        curr->next = dummy.next;
        dummy.next = curr;
        curr = next;
    }
    return dummy.next;
}

如果你要问我,主播主播,你的头插法还是太吃操作了,有没有什么更好的方法秒杀这个题。作为一名极客,我必须回答:有的,有的,兄弟。

对于反转逆置链表,我们有三种方法,除了上面的头插,我们还有迭代,递归。

其实简单讲就是利用几个指针,断开旧链,生成新链。最终返回目标链表

迭代法:

复制代码
struct ListNode* reverseList(struct ListNode* head) {
    struct ListNode* prev = NULL;
    struct ListNode* curr = head;
    while (curr) {
        struct ListNode* next = curr->next;
        curr->next = prev;
        prev = curr;
        curr = next;
    }
    return prev;
}

递归法:

递归走到链表最后一个节点(新头),回溯时让后一个节点指向自己,自身指向 NULL。

复制代码
struct ListNode* reverseList(struct ListNode* head) {
    // 递归终止:空节点 / 只剩一个节点直接返回
    if(head == NULL || head->next == NULL) {
        return head;
    }
    // 递归拿到反转后的新头(原最后一个节点)
    struct ListNode* newHead = reverseList(head->next);
    // 反转指向:后一个节点指向当前节点
    head->next->next = head;
    // 当前节点断开原指向,防止环
    head->next = NULL;
    // 始终返回新头
    return newHead;
}

走一遍 1,2,3 逻辑

  1. reverse (1) → 调用 reverse (2) → 调用 reverse (3)
  2. reverse (3):3->next=NULL,返回 newHead=3
  3. 回到 reverse (2):2->next->next = 2 → 3→2;2->next=NULL;返回 newHead=3
  4. 回到 reverse (1):1->next->next =1 →2→1;1->next=NULL;返回 newHead=3

你要问我这道题还有没有别的方法,我其实还有一些暴力方法,比如利用栈的后进先出特性,可以很好的暴力解决这个问题,但考虑到,目前我还没更新到栈,可能有某些小伙伴看不懂,等我后面更新到栈的时候,我再讲一下用栈的思想解决这个题。

3.4.4 链表的中间结点

876. 链表的中间结点 - 力扣(LeetCode)

这是一道经典的链表问题,为什么经典呢?

可能有些小伙伴会疑惑,我该怎么利用链表的特性寻找中间节点呢?或许我们可以尝试暴力遍历,统计总共有多少个节点,记作sum,然后根据sum进行遍历求解中间节点。非常好,你已经具有暴力思维,再也不是那个一看见算法题就放弃的娃娃了。(其实这段话是对过去的我说的,哈哈哈哈哈)

复制代码
/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     struct ListNode *next;
 * };
 */
struct ListNode* middleNode(struct ListNode* head) {
    
    int n=0;
    struct ListNode* cur = head;
    while(cur!=NULL){

        n++;
        cur=cur->next;
    }
    int k=0;
    cur=head;
    while(k<n/2){

        k++;
        cur=cur->next;
    }
    return cur;
    
}

但是,有一句很有意思的话:五十步笑一百步。我们可以利用快慢双指针,一个快(fast)指针,一个慢(slow)指针。让fast每一次走2步,slow每一次走1步,当fast走到链表末尾时,或者快到末尾时(这个时候因为fast->next->next == null,我们不准fast继续往下走了),slow所在位置就是中间节点。

代码如下:

复制代码
class Solution {
public:
    ListNode* middleNode(ListNode* head) {
        ListNode* slow = head;
        ListNode* fast = head;
        while (fast != NULL && fast->next != NULL) {
            slow = slow->next;
            fast = fast->next->next;
        }
        return slow;
    }
};

后续有关链表的做题思路,解题技巧,以及项目里的具体应用,我会逐一更新在这个账号,敬请期待,欢迎一同学习与交流!!!

相关推荐
劈星斩月1 小时前
监督学习算法 之 决策树
学习·算法·决策树
学计算机的计算基1 小时前
操作系统内存管理全解:虚拟内存、页表、COW、malloc、OOM一篇搞定
java·笔记·算法
tachibana21 小时前
hot100 前 K 个高频元素(347)
java·数据结构·算法·leetcode
小保CPP1 小时前
OpenCV C++提取webp动图里的图片
c++·人工智能·opencv·计算机视觉
To_OC9 小时前
LC 131 分割回文串:刚学回溯时,我连怎么切字符串都想不明白
javascript·算法·leetcode
炸膛坦客10 小时前
单片机/C/C++八股:(二十六)IIC 专题(I²C)---- 上集
c语言·c++·单片机
旖-旎10 小时前
LeetCode 518:零钱兑换||(完全背包)—— 题解
c++·算法·leetcode·动态规划·背包问题
To_OC11 小时前
LC 42 接雨水:暴力超时卡半天?前后缀数组一用就通了
javascript·算法·leetcode
Henry Zhu12311 小时前
C++中的特殊成员函数与智能指针
c++