【数据结构】大话单链表
一、链表是啥?先从"串珠子"说起
你小时候肯定玩过串珠子吧?一颗珠子接着一颗,用线串起来,能弯能折,想加颗珠子就加,想拆颗珠子就拆。单链表就跟这串珠子一个道理:
- 每颗珠子 = 链表的"节点"(Node)
- 珠子里的东西 = 节点的"数据域"(存储数据)
- 串珠子的线 = 节点的"指针域"(指向下一个节点)
咱们平时用的数组就像固定长度的铁盒子,大小一开始就定死了,想多放个东西都不行;而链表就像橡皮筋串珠子,长短能随便变,灵活得很!
单链表的结构大概是这样:
头节点(存长度) → 第1个节点 → 第2个节点 → ... → 最后一个节点 → NULL
(注意:头节点不算实际数据节点,就像串珠子的线绳头,方便咱们操作整个链表)
二、手把手教你定义链表
要实现链表,首先得告诉计算机"节点"长啥样。用C++来说,就是定义一个结构体:
typedef int ElemType; // 数据类型,这里先拿int举例
struct Node; // 提前声明
typedef struct Node* LinkList; // 链表指针,方便操作
typedef struct Node
{ElemType data; // 数据域:存具体数值LinkList next; // 指针域:存下一个节点的地址
}Node;
你看,每个节点就像个小盒子,左边装数据,右边装个"箭头"指向隔壁盒子。
三、创建链表:从0到1串起珠子
创建链表有两种方法:头插法(往最前面加珠子)和尾插法(往最后面加珠子)。咱们先学尾插法,更符合咱们平时"按顺序串珠子"的习惯。
/*** 创建单链表(尾插法)* @param len 要创建的链表长度* @return 头节点指针(相当于拿到了整串珠子)*/
LinkList CreateListHead(int len)
{// 先做个"绳头"(头节点)LinkList pHead = new Node();pHead->data = 0; // 头节点数据域存链表长度,初始0pHead->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颗就行。
/*** 查找第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颗的箭头指向新珠子。
/*** 在第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颗的箭头跳过要删的珠子,直接指向下一颗,最后把摘掉的珠子扔掉(释放内存)。
/*** 删除第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--; // 长度减1return true;
}
删除一定要记得释放内存,不然就像摘了珠子却堆在桌子上,占地方还乱(这叫"内存泄漏")。
七、改:把第i颗珠子里的东西换了
想把第2颗珠子里的红珠子换成蓝珠子?很简单:先找到第i颗珠子,直接把里面的数据换掉就行。
/*** 修改第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;
}
修改是最简单的操作,找到位置直接换内容就行,不用动珠子的顺序。
八、遍历:把所有珠子过一遍
想看看整串珠子都有啥?从头开始,一个个看过去,直到最后一颗。
/*** 遍历输出所有元素* @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函数测试一下:
#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个位置插入元素66if (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个元素为99if (ListUpdate(L, 1, 99)) {std::cout << "修改后:" << std::endl;ListTraverse(L);} else {std::cout << "修改失败" << std::endl;}// 6. 销毁链表DestroyList(L);delete L; // 释放头节点return 0;
}
十、总结:链表这东西,就这么回事儿
单链表其实就是用指针把节点串起来,核心操作就四个字:增、删、改、查。跟数组比,它的优势是插入删除方便(不用挪动一堆元素),缺点是不能直接"跳"到某个位置(必须从头遍历)。
记住几个关键点:
- 头节点不算实际数据,主要是方便操作
- 操作时要注意指针的指向,别把链表"弄断"了
- 删除节点后一定要释放内存,不然会内存泄漏
