2、单链表介绍

单链表

一、单链表和顺序表之间的差异

1.1 缓存命中率

在介绍缓存命中率之前首先要先明白CPU的预加载功能

什么是 CPU 预加载(Cache Preloading)

CPU 为了加快访问速度,会使用多级缓存(L1/L2/L3)来提前加载内存中的数据。它不是一个字节一个字节地加载,而是以 Cache Line 为单位(通常是 64 字节)一次性加载一块连续内存。

这就引出了一个关键点:

如果你的数据在内存中是连续的,CPU 一次加载就能命中多个数据项,性能就会更高。

1.2 顺序表 vs 链表:缓存命中率对比

特性顺序表(数组)链表(节点+指针)
内存布局物理地址连续物理地址不连续
CPU 缓存命中率高(预取命中多个元素)低(每次访问可能都要跳地址)
遍历性能快(线性访问)慢(每次都要跟随指针)
插入/删除效率慢(需要搬移元素)快(修改指针即可)
随机访问效率O(1)O(n)
举个例子:遍历加一操作

假设你要对顺序表或链表中的每个元素执行 +1 操作:

  • 顺序表:由于内存连续,CPU 一次预取就能加载多个元素,命中率高,遍历快。
  • 链表:每个节点可能在不同内存位置,CPU 每次都要从主存重新加载,命中率低,遍历慢。

这就是为什么在需要频繁遍历或批量处理数据时,顺序表更适合。

1.3 对于工程上的一些建议

  • 如果你要做大量遍历、排序、批处理 —— 用顺序表(数组)

  • 如果你要频繁插入、删除 —— 用链表

  • 如果你想两者兼顾,可以考虑封装结构体,或使用块状链表、跳表等混合结构

二、单向链表介绍

2.1 什么是链表?

链表是一种线性数据结构,但它不像数组那样在内存中连续存储,而是通过指针将一个个节点连接起来。每个节点通常包含两部分:

  • 数据域:存储实际数据

  • 指针域:指向下一个节点的地址(单向链表)或前后节点地址(双向链表)

在这里插入图片描述在这里插入图片描述

2.2 链表的类型

类型特点说明
单向链表每个节点只指向下一个节点,适合顺序遍历(今天只介绍这一种链表)
双向链表每个节点有两个指针,分别指向前后节点,支持双向遍历
循环链表最后一个节点指向第一个节点,形成闭环,常用于任务调度或缓存轮询等场景
块状链表每个节点存储一个数组块,适合大数据量场景,节省空间,提高访问效率

2.3 c语言定义的单项链表节点

typedef struct SListNode
{
    // 要存储的数据
    SListDataType data;
    // 下一个节点的地址
    struct SListNode* next;
}SListNode;

2.4 接口实现

我在这里写了两个接口

void SListPushBack(SListNode** pphead, SListDataType x);
void SListPopBack(SListNode** pphead);
Slist.h
#pragma once
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
//首先实现的是一个链表
typedef int SListDataType;
#define N 10

typedef struct SListNode
{
    SListDataType data;
    struct SListNode* next;
}SListNode;

//尾插尾删
void SListPushBack(SListNode** pphead, SListDataType x);
void SListPopBack(SListNode** pphead);
//头插头删
void SListPushFront(SListNode** pphead, SListDataType x);
void SListPopFront(SListNode** pphead);

//指定位置插入和删除
void SListInsert(SListNode** pphead, int pos, SListDataType x);
void SListErase(SListNode** pphead, int pos);

//打印链表
void SListPrint(SListNode* phead);

//创建节点
SListNode* CreateSListNode(SListDataType x);
Slist.c
#include"SList.h"
#include <assert.h>

// 打印链表
void SListPrint(SListNode* phead)
{
    SListNode* cur = phead;
    while(cur)
    {
        printf("%d->", cur->data);
        cur = cur->next;
    }
    printf("\n");
}
// 创建节点
SListNode* CreateSListNode(SListDataType x)
{
    SListNode* newnode = (SListNode*)malloc(sizeof(SListNode));
    assert(newnode != NULL); // 确保内存分配成功
    newnode->data = x;
    newnode->next = NULL;
    return newnode;
}

对于尾插法这里传入的是二级指针,插入图片进行解释

在这里插入图片描述

// 尾插法
void SListPushBack(SListNode** pphead, SListDataType x)
{
    SListNode* newnode = CreateSListNode(x);

    if(*pphead == NULL){
      *pphead = newnode;
    }
    else{
        //这个时候我要找到尾部的结点指针
        SListNode* cur = *pphead;
        SListNode* tail = NULL;
        //如果这里设置的是一个cur->next
        //那么就不需要tail来保存地址了
        while(cur)
        {
            tail = cur;
            cur = cur->next;
        }
        tail->next = newnode;
    }
}
// 实现尾删
void SListPopBack(SListNode** pphead)
{
    // 如果链表为空
    if(*pphead == NULL){
        return;
    }
    // 链表中只存储了一个数据
    else if((*pphead)->next == NULL){
        free(*pphead);
        *pphead = NULL;
        return;
    }
    //链表中的数据是大于一,这个时候需要找到尾部节点和前一个节点
    //完成两件事情
    // 1. 释放最后一个节点
    // 2. 前一个节点的指向置为空
    else{
        SListNode* tail = *pphead;
        SListNode* prev = NULL;
        // 这个循环结束时tail就是最后一个值,prev记录了前一个数据
        while(tail->next != NULL)
        {
            // 指向现在的尾
            prev = tail;
            // 指向下一个
            tail = tail->next;
        }
        free(tail);
        // 如果不加入这一不的话,会出现野指针的问题
        prev->next = NULL;
    }
    
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值