【数据结构】大话单链表

一、链表是啥?先从"串珠子"说起

你小时候肯定玩过串珠子吧?一颗珠子接着一颗,用线串起来,能弯能折,想加颗珠子就加,想拆颗珠子就拆。单链表就跟这串珠子一个道理:

  • 每颗珠子 = 链表的"节点"(Node)
  • 珠子里的东西 = 节点的"数据域"(存储数据)
  • 串珠子的线 = 节点的"指针域"(指向下一个节点)

咱们平时用的数组就像固定长度的铁盒子,大小一开始就定死了,想多放个东西都不行;而链表就像橡皮筋串珠子,长短能随便变,灵活得很!

单链表的结构大概是这样:

复制代码
头节点(存长度) → 第1个节点 → 第2个节点 → ... → 最后一个节点 → NULL

(注意:头节点不算实际数据节点,就像串珠子的线绳头,方便咱们操作整个链表)

二、手把手教你定义链表

要实现链表,首先得告诉计算机"节点"长啥样。用C++来说,就是定义一个结构体:

cpp 复制代码
typedef int ElemType;  // 数据类型,这里先拿int举例
struct Node;  // 提前声明
typedef struct Node* LinkList;  // 链表指针,方便操作
typedef struct Node
{
    ElemType data;  // 数据域:存具体数值
    LinkList next;  // 指针域:存下一个节点的地址
}Node;

你看,每个节点就像个小盒子,左边装数据,右边装个"箭头"指向隔壁盒子。

三、创建链表:从0到1串起珠子

创建链表有两种方法:头插法(往最前面加珠子)和尾插法(往最后面加珠子)。咱们先学尾插法,更符合咱们平时"按顺序串珠子"的习惯。

cpp 复制代码
/**
 * 创建单链表(尾插法)
 * @param len 要创建的链表长度
 * @return 头节点指针(相当于拿到了整串珠子)
 */
LinkList CreateListHead(int len) 
{
    // 先做个"绳头"(头节点)
    LinkList pHead = new Node();
    pHead->data = 0;  // 头节点数据域存链表长度,初始0
    pHead->next = nullptr;  // 刚开始绳头后面啥也没有

    LinkList tmp = pHead;  // 临时指针,跟着珠子串移动,始终指着最后一颗珠子
    LinkList mynode = nullptr;  // 新珠子

    for (int i = 0; i < len; i++) {
        // 造一颗新珠子
        mynode = new Node();
        mynode->data = rand() % 100 + 1;  // 随便给个1-100的数当数据
        mynode->next = nullptr;  // 新珠子后面暂时没别的珠子
        
        // 把新珠子串到最后
        tmp->next = mynode;  // 让当前最后一颗珠子的箭头指向新珠子
        tmp = mynode;  // 现在新珠子成了最后一颗,tmp移过去

        pHead->data++;  // 链表长度加1,头节点记得账
    }

    return pHead;  // 把串好的珠子(带绳头)交出去
}

这段代码的关键是tmp指针,它就像你的手,始终拿着当前最后一颗珠子,新珠子来了就接在后面,然后手再挪到新珠子上。

四、查:找第i颗珠子里的东西

想知道第3颗珠子里装的啥?这就是"查找"操作。步骤很简单:从第一颗珠子开始,一个一个往后数,数到第i颗就行。

cpp 复制代码
/**
 * 查找第i个元素的值
 * @param L 整个链表(绳头)
 * @param i 要找的位置(从1开始数)
 * @param e 用来存找到的值
 * @return 找到返回true,找不到返回false
 */
bool GetElem(LinkList L, int i, ElemType* e)
{
    if (i <= 0) return false;  // 位置不能是0或负数,哪有第0颗珠子?

    LinkList p = L->next;  // 从第一颗实际珠子开始找
    int j = 1;  // 计数器,当前数到第几颗了

    // 一边往后走,一边数,直到数到第i颗
    while (p && j < i) {
        p = p->next;  // 移到下一颗
        ++j;  // 计数器加1
    }

    if (!p) return false;  // 数过头了(比如总共3颗,找第5颗)

    *e = p->data;  // 把找到的值存起来
    return true;
}

记住:链表不像数组能直接"跳"到第i个位置,只能从头一个个往后挪,这是链表的特点(也是小缺点)。

五、增:在第i个位置加颗新珠子

想在第2颗和第3颗珠子中间加一颗新的?步骤是:先找到第i-1颗珠子,把新珠子的箭头指向第i颗,再让第i-1颗的箭头指向新珠子。

cpp 复制代码
/**
 * 在第i个位置插入新元素
 * @param L 整个链表
 * @param i 要插入的位置
 * @param e 要插入的值
 * @return 成功返回true
 */
bool ListInsert(LinkList L, int i, ElemType e)
{
    if (i <= 0) return false;  // 位置不合法

    LinkList p = L;  // 从绳头开始找
    int j = 0;  // 计数器(绳头算第0个)

    // 找到第i-1个位置(要插在第i个前面,得先找到第i-1个)
    while (p && j < i - 1) {
        p = p->next;
        ++j;
    }

    if (!p) return false;  // 位置太靠后,插不了

    // 造新珠子
    LinkList newNode = new Node();
    newNode->data = e;
    // 关键步骤:先连后断(别把后面的珠子弄丢了)
    newNode->next = p->next;  // 新珠子的箭头指向原来第i颗珠子
    p->next = newNode;  // 第i-1颗珠子的箭头指向新珠子

    L->data++;  // 长度加1,头节点记上
    return true;
}

这里的"先连后断"很重要,就像串珠子时,先把新珠子跟后面的珠子串好,再跟前面的连上,不然容易把后面的珠子弄丢。

六、删:把第i颗珠子摘掉

想把第3颗珠子摘掉?步骤是:找到第i-1颗珠子,记下要删的珠子,让第i-1颗的箭头跳过要删的珠子,直接指向下一颗,最后把摘掉的珠子扔掉(释放内存)。

cpp 复制代码
/**
 * 删除第i个位置的元素
 * @param L 整个链表
 * @param i 要删除的位置
 * @param e 存被删掉的值
 * @return 成功返回true
 */
bool ListDelete(LinkList L, int i, ElemType* e)
{
    if (i <= 0) return false;

    LinkList p = L;  // 从绳头开始找
    int j = 0;

    // 找到第i-1个位置
    while (p && j < i - 1) {
        p = p->next;
        ++j;
    }

    if (!p || !p->next) return false;  // 没找到要删的珠子

    LinkList delNode = p->next;  // 记下要删的珠子
    *e = delNode->data;  // 存下删掉的值
    p->next = delNode->next;  // 跳过要删的珠子,直接连后面的

    delete delNode;  // 把摘掉的珠子扔垃圾桶(释放内存)
    L->data--;  // 长度减1
    return true;
}

删除一定要记得释放内存,不然就像摘了珠子却堆在桌子上,占地方还乱(这叫"内存泄漏")。

七、改:把第i颗珠子里的东西换了

想把第2颗珠子里的红珠子换成蓝珠子?很简单:先找到第i颗珠子,直接把里面的数据换掉就行。

cpp 复制代码
/**
 * 修改第i个位置的元素值
 * @param L 整个链表
 * @param i 要修改的位置
 * @param e 新的值
 * @return 成功返回true
 */
bool ListUpdate(LinkList L, int i, ElemType e)
{
    if (i <= 0) return false;

    LinkList p = L->next;  // 从第一颗珠子开始找
    int j = 1;

    while (p && j < i) {
        p = p->next;
        ++j;
    }

    if (!p) return false;  // 没找到

    p->data = e;  // 直接改数据
    return true;
}

修改是最简单的操作,找到位置直接换内容就行,不用动珠子的顺序。

八、遍历:把所有珠子过一遍

想看看整串珠子都有啥?从头开始,一个个看过去,直到最后一颗。

cpp 复制代码
/**
 * 遍历输出所有元素
 * @param L 整个链表
 */
void ListTraverse(LinkList L)
{
    LinkList p = L->next;  // 从第一颗珠子开始
    std::cout << "链表元素:";
    while (p) {  // 只要还有珠子就继续
        std::cout << p->data << " ";
        p = p->next;  // 移到下一颗
    }
    std::cout << std::endl;
}

九、完整代码+测试

把上面的功能拼起来,再写个main函数测试一下:

cpp 复制代码
#include <iostream>
#include <cstdlib>  // 用于rand()
#include <ctime>    // 用于设置随机数种子

// 节点定义
typedef int ElemType;
struct Node;
typedef struct Node* LinkList;
typedef struct Node
{
    ElemType data;
    LinkList next;
}Node;

// 创建链表(尾插法)
LinkList CreateListHead(int len) 
{
    LinkList pHead = new Node();
    pHead->data = 0;
    pHead->next = nullptr;

    LinkList tmp = pHead, mynode = nullptr;
    for (int i = 0; i < len; i++) {
        mynode = new Node();
        mynode->data = rand() % 100 + 1;  // 1-100随机数
        mynode->next = nullptr;
        
        tmp->next = mynode;
        tmp = mynode;

        pHead->data++;
    }

    return pHead;
}

// 查找元素
bool GetElem(LinkList L, int i, ElemType* e)
{
    if (i <= 0) return false;

    LinkList p = L->next;
    int j = 1;
    while (p && j < i) {
        p = p->next;
        ++j;
    }

    if (!p) return false;

    *e = p->data;
    return true;
}

// 插入元素
bool ListInsert(LinkList L, int i, ElemType e)
{
    if (i <= 0) return false;

    LinkList p = L;
    int j = 0;
    while (p && j < i - 1) {
        p = p->next;
        ++j;
    }

    if (!p) return false;

    LinkList newNode = new Node();
    newNode->data = e;
    newNode->next = p->next;
    p->next = newNode;

    L->data++;
    return true;
}

// 删除元素
bool ListDelete(LinkList L, int i, ElemType* e)
{
    if (i <= 0) return false;

    LinkList p = L;
    int j = 0;
    while (p && j < i - 1) {
        p = p->next;
        ++j;
    }

    if (!p || !p->next) return false;

    LinkList delNode = p->next;
    *e = delNode->data;
    p->next = delNode->next;

    delete delNode;
    L->data--;
    return true;
}

// 修改元素
bool ListUpdate(LinkList L, int i, ElemType e)
{
    if (i <= 0) return false;

    LinkList p = L->next;
    int j = 1;
    while (p && j < i) {
        p = p->next;
        ++j;
    }

    if (!p) return false;

    p->data = e;
    return true;
}

// 遍历元素
void ListTraverse(LinkList L)
{
    LinkList p = L->next;
    std::cout << "链表元素:";
    while (p) {
        std::cout << p->data << " ";
        p = p->next;
    }
    std::cout << std::endl;
}

// 销毁链表(释放所有内存)
void DestroyList(LinkList L)
{
    LinkList p, q;
    p = L->next;
    while (p) {
        q = p->next;
        delete p;
        p = q;
    }
    L->next = nullptr;  // 头节点的next置空
    std::cout << "链表已销毁" << std::endl;
}

int main()
{
    srand((unsigned int)time(NULL));  // 设置随机数种子

    // 1. 创建一个长度为5的链表
    LinkList L = CreateListHead(5);
    std::cout << "初始链表(长度" << L->data << "):" << std::endl;
    ListTraverse(L);

    // 2. 查找第3个元素
    ElemType e;
    if (GetElem(L, 3, &e)) {
        std::cout << "第3个元素是:" << e << std::endl;
    } else {
        std::cout << "查找第3个元素失败" << std::endl;
    }

    // 3. 在第2个位置插入元素66
    if (ListInsert(L, 2, 66)) {
        std::cout << "插入66后(长度" << L->data << "):" << std::endl;
        ListTraverse(L);
    } else {
        std::cout << "插入失败" << std::endl;
    }

    // 4. 删除第4个元素
    if (ListDelete(L, 4, &e)) {
        std::cout << "删除的元素是:" << e << ",删除后(长度" << L->data << "):" << std::endl;
        ListTraverse(L);
    } else {
        std::cout << "删除失败" << std::endl;
    }

    // 5. 修改第1个元素为99
    if (ListUpdate(L, 1, 99)) {
        std::cout << "修改后:" << std::endl;
        ListTraverse(L);
    } else {
        std::cout << "修改失败" << std::endl;
    }

    // 6. 销毁链表
    DestroyList(L);
    delete L;  // 释放头节点

    return 0;
}

十、总结:链表这东西,就这么回事儿

单链表其实就是用指针把节点串起来,核心操作就四个字:增、删、改、查。跟数组比,它的优势是插入删除方便(不用挪动一堆元素),缺点是不能直接"跳"到某个位置(必须从头遍历)。

记住几个关键点:

  • 头节点不算实际数据,主要是方便操作
  • 操作时要注意指针的指向,别把链表"弄断"了
  • 删除节点后一定要释放内存,不然会内存泄漏
相关推荐
澪吟3 小时前
算法性能的核心度量:时间复杂度与空间复杂度全解析
数据结构·算法
苏纪云3 小时前
算法<C++>——双指针操作链表
c++·算法·链表·双指针
louisdlee.4 小时前
扫描线1:朴素扫描线
数据结构·c++·算法·扫描线
仰泳的熊猫4 小时前
LeetCode:1905. 统计子岛屿
数据结构·c++·算法·leetcode
THGML5 小时前
排序算法解析
数据结构·算法·排序算法
OKkankan5 小时前
模板的进阶
开发语言·数据结构·c++·算法
拾光Ծ5 小时前
【高阶数据结构】哈希表
数据结构·c++·哈希算法·散列表
我不会插花弄玉5 小时前
c语言实现栈【由浅入深-数据结构】
c语言·数据结构
熬了夜的程序员6 小时前
【LeetCode】88. 合并两个有序数组
数据结构·算法·leetcode·职场和发展·深度优先