一、链表是啥?先从"串珠子"说起
你小时候肯定玩过串珠子吧?一颗珠子接着一颗,用线串起来,能弯能折,想加颗珠子就加,想拆颗珠子就拆。单链表就跟这串珠子一个道理:
- 每颗珠子 = 链表的"节点"(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;
}
十、总结:链表这东西,就这么回事儿
单链表其实就是用指针把节点串起来,核心操作就四个字:增、删、改、查。跟数组比,它的优势是插入删除方便(不用挪动一堆元素),缺点是不能直接"跳"到某个位置(必须从头遍历)。
记住几个关键点:
- 头节点不算实际数据,主要是方便操作
- 操作时要注意指针的指向,别把链表"弄断"了
- 删除节点后一定要释放内存,不然会内存泄漏