- C++
GESP五级|单链表、双向链表、循环链表 零基础竞赛教程
- @ 2026-7-31 20:23:26
GESP五级|单链表、双向链表、循环链表 零基础竞赛教程
前置说明
链表属于 GESP C++五级数据结构考点,信奥入门基础内容。 数组:内存连续,随机访问快;插入删除中间元素需要大量移动数据。 链表:内存分散存放,依靠指针相连;访问元素需要从头遍历;任意位置插入、删除效率更高。
名词通俗解释 结点:存放数据 + 指针的组合单元 指针:记录下一个结点在哪里
一、单链表(基础必考)
通俗原理
每个结点包含两部分:数据 + 后继指针(指向下一个结点)
最后一个结点指针置空 NULL,代表链表结束。
只能从头向后遍历,不能反向走。
结构体定义
#include <iostream>
using namespace std;
// 定义单链表结点
struct Node
{
int data; // 结点存储的数据
Node* next; // 指向下一个结点的指针
// 构造函数,快速创建结点
Node(int val) : data(val), next(NULL) {}
};
int main()
{
// 1. 创建结点
Node* n1 = new Node(10);
Node* n2 = new Node(20);
Node* n3 = new Node(30);
// 2. 连接结点,构建链表 10 -> 20 -> 30
n1->next = n2;
n2->next = n3;
n3->next = NULL; // 尾结点,无后续
// 3. 遍历链表
Node* p = n1;
while(p != NULL)
{
cout << p->data << " ";
p = p->next; // 指针走到下一个结点
}
cout << endl;
// 释放内存(竞赛可省略,工程规范要求)
delete n1; delete n2; delete n3;
return 0;
}
封装常用操作完整版(带注释,考场模板)
#include <iostream>
using namespace std;
struct Node
{
int data;
Node* next;
Node(int val) : data(val), next(NULL) {}
};
// 在链表尾部插入结点
void insertTail(Node* &head, int val)
{
Node* newnode = new Node(val);
if(head == NULL) // 链表为空
{
head = newnode;
return;
}
Node* p = head;
while(p->next != NULL) // 找到最后一个结点
{
p = p->next;
}
p->next = newnode;
}
// 删除指定数值结点
void removeVal(Node* &head, int val)
{
if(head == NULL) return;
// 头结点就是目标
if(head->data == val)
{
Node* temp = head;
head = head->next;
delete temp;
return;
}
Node* p = head;
// 找到待删除结点的前驱
while(p->next != NULL && p->next->data != val)
{
p = p->next;
}
if(p->next == NULL) return; // 没找到
Node* temp = p->next;
p->next = p->next->next;
delete temp;
}
// 遍历输出链表
void printList(Node* head)
{
Node* p = head;
while(p != NULL)
{
cout << p->data << " ";
p = p->next;
}
cout << endl;
}
int main()
{
Node* head = NULL; // 链表头指针,初始空链表
insertTail(head, 1);
insertTail(head, 2);
insertTail(head, 3);
printList(head);
removeVal(head, 2);
printList(head);
return 0;
}
✅优点:结构最简单,代码短 ❌缺点:只能向后遍历;删除结点必须先找到前驱结点
二、双向链表(双链表,五级重点)
通俗原理
每个结点包含三部分:数据 + 前驱指针 + 后继指针 可以向前、向后双向遍历;删除结点不需要单独查找前驱。 头结点前驱 = NULL,尾结点后继 = NULL。
完整带注释模板
#include <iostream>
using namespace std;
// 双向链表结点
struct DNode
{
int data;
DNode* prev; // 指向前一个结点
DNode* next; // 指向下一个结点
DNode(int val) : data(val), prev(NULL), next(NULL) {}
};
// 尾部插入
void insertTail(DNode* &head, int val)
{
DNode* newnode = new DNode(val);
if(head == NULL)
{
head = newnode;
return;
}
DNode* p = head;
while(p->next != NULL)
{
p = p->next;
}
p->next = newnode;
newnode->prev = p; // 新结点向前绑定
}
// 删除指定数值结点
void removeVal(DNode* &head, int val)
{
if(head == NULL) return;
DNode* p = head;
while(p != NULL && p->data != val)
{
p = p->next;
}
if(p == NULL) return; // 不存在
// 情况1:待删结点是头结点
if(p->prev == NULL)
{
head = p->next;
}
else
{
p->prev->next = p->next;
}
// 情况2:待删结点不是尾结点
if(p->next != NULL)
{
p->next->prev = p->prev;
}
delete p;
}
// 正向遍历
void printForward(DNode* head)
{
DNode* p = head;
while(p != NULL)
{
cout << p->data << " ";
p = p->next;
}
cout << endl;
}
int main()
{
DNode* head = NULL;
insertTail(head,10);
insertTail(head,20);
insertTail(head,30);
printForward(head);
removeVal(head,20);
printForward(head);
return 0;
}
✅优点:双向行走;删除操作更方便 ❌缺点:每个结点多存一个指针,占用更多内存
三、循环链表(单循环链表,竞赛常用版本)
通俗原理
基于单链表改造:尾结点不再指向NULL,尾结点next指向头结点 链表首尾相连,形成圆环,可以无限循环遍历。
注意:遍历必须记录起点,否则死循环!
完整带注释模板
#include <iostream>
using namespace std;
struct Node
{
int data;
Node* next;
Node(int val) : data(val), next(NULL) {}
};
// 尾部插入,构建循环链表
void insertTailCircle(Node* &head, int val)
{
Node* newnode = new Node(val);
if(head == NULL)
{
head = newnode;
newnode->next = head; // 唯一结点,自环
return;
}
Node* p = head;
// 找到尾结点(next指向head)
while(p->next != head)
{
p = p->next;
}
p->next = newnode;
newnode->next = head; // 新尾结点指向头
}
// 遍历循环链表,从head开始
void printCircle(Node* head)
{
if(head == NULL) return;
Node* p = head;
do
{
cout << p->data << " ";
p = p->next;
}while(p != head); // 回到起点停止,防止死循环
cout << endl;
}
int main()
{
Node* head = NULL;
insertTailCircle(head, 1);
insertTailCircle(head, 2);
insertTailCircle(head, 3);
printCircle(head);
return 0;
}
✅优点:可以从任意位置遍历整条链表;适合环形问题(约瑟夫环经典题) ❌缺点:一不小心写出无限循环,边界判断要小心
四、三种链表对比(五级选择题考点)
-
单链表 结点:数据 + next指针 遍历方向:只能向后 适用:简单增删、基础链表题目
-
双向链表 结点:data + prev + next 遍历方向:向前、向后双向 适用:需要频繁查找前驱、双向访问场景
-
循环单链表 结点:data + next 尾结点指向头结点,形成环 适用:环形模拟,经典【约瑟夫问题】
五、GESP五级考场0基础避坑指南
- 指针操作最容易出错,修改指针顺序不能颠倒;
- 使用
new创建结点,删除使用delete;竞赛时间紧张内存泄漏一般不扣分; - 循环链表遍历一定要设置终止条件,禁止无限循环;
- 双向链表删除结点时,前后两条指针都要修改,漏一条链表断裂;
- 传递头指针建议使用引用
Node* &head,否则函数内修改无法对外生效; - 访问指针成员务必用
->;普通变量用.,不要混用。
六、竞赛高频考题方向
- 链表遍历、查找指定数值
- 头部/尾部插入、按位置删除结点
- 链表反转(单链表高频大题)
- 循环链表实现约瑟夫环(GESP五级压轴题型)
2 条评论
-
admin SU @ 2026-7-31 20:25:42
C++链表零基础完整教程:单链表|双向链表|循环单链表
学习顺序建议:单链表 → 双向链表 → 循环链表 核心概念:链表由**节点(Node)**串联而成;每个节点存放【数据】+【指针】。 数组:连续内存;链表:分散内存,依靠指针相连;优势:任意位置插入/删除不需要大规模移动元素
通用前置知识
结构体节点、指针基础
// 指针复习:Node *p 代表p存储节点地址,*p代表节点本体 // p->data 等价于 (*p).data // new Node() 新建节点;delete p 释放节点(防止内存泄漏)
一、单链表(最基础)
结构:
[数据|下一个节点地址] → [数据|下一个节点地址] → nullptr尾节点指针 =nullptr,代表链表结束。常用操作清单
- 头插法(头部新增节点)
- 尾插法(尾部新增节点)
- 遍历输出链表
- 根据数值删除节点
- 销毁链表
完整带注释代码
#include <iostream> using namespace std; // 定义单链表节点 struct Node { int data; // 存放数据 Node *next; // 指向下一个节点的指针 // 构造函数,方便快速创建节点 Node(int val) { data = val; next = nullptr; } }; // 头插:在链表最前面插入数值val void headInsert(Node *&head, int val) { Node *new_node = new Node(val); new_node->next = head; // 新节点指向原来第一个节点 head = new_node; // 头指针更新为新节点 } // 尾插:在链表末尾插入数值val void tailInsert(Node *&head, int val) { Node *new_node = new Node(val); // 如果链表为空 if (head == nullptr) { head = new_node; return; } // 找到最后一个节点 Node *p = head; while (p->next != nullptr) { p = p->next; } p->next = new_node; } // 遍历打印链表 void printList(Node *head) { Node *p = head; while (p != nullptr) { cout << p->data << " "; p = p->next; } cout << endl; } // 删除第一个值为val的节点 void delNode(Node *&head, int val) { if (head == nullptr) return; // 情况1:要删除的是头节点 if (head->data == val) { Node *temp = head; head = head->next; delete temp; return; } // 情况2:删除中间/尾部节点 Node *p = head; // 找到待删除节点的前一个节点 while (p->next != nullptr && p->next->data != val) { p = p->next; } if (p->next == nullptr) return; // 没找到 Node *temp = p->next; p->next = p->next->next; delete temp; } // 释放所有节点,防止内存泄漏 void destroyList(Node *&head) { Node *p = head; while (p != nullptr) { Node *temp = p; p = p->next; delete temp; } head = nullptr; } int main() { Node *head = nullptr; // 初始链表为空 tailInsert(head, 10); tailInsert(head, 20); tailInsert(head, 30); cout << "原始链表:"; printList(head); // 10 20 30 headInsert(head, 5); cout << "头插5后:"; printList(head); // 5 10 20 30 delNode(head, 20); cout << "删除20后:"; printList(head); // 5 10 30 destroyList(head); return 0; }单链表缺点
只能向后遍历;删除当前节点必须先找到前驱节点,无法直接找到上一个节点。
二、双向链表(双链表)
结构:
nullptr ← [前驱|数据|后继] ↔ [前驱|数据|后继] ↔ [前驱|数据|后继] → nullptr每个节点两个指针:prev:指向前一个节点next:指向下一个节点
优点:可以向前、向后双向遍历;删除节点不用单独寻找前驱
#include <iostream> using namespace std; // 双向链表节点 struct DNode { int data; DNode *prev; // 前驱指针 DNode *next; // 后继指针 DNode(int val) { data = val; prev = nullptr; next = nullptr; } }; // 尾插 void tailInsert(DNode *&head, int val) { DNode *new_node = new DNode(val); if (head == nullptr) { head = new_node; return; } DNode *p = head; while (p->next != nullptr) p = p->next; p->next = new_node; new_node->prev = p; } // 正向遍历 void printForward(DNode *head) { DNode *p = head; while (p != nullptr) { cout << p->data << " "; p = p->next; } cout << endl; } // 反向遍历(双链表独有!单链表做不到) void printBackward(DNode *head) { if (head == nullptr) return; DNode *p = head; // 走到尾部 while (p->next != nullptr) p = p->next; // 向前走 while (p != nullptr) { cout << p->data << " "; p = p->prev; } cout << endl; } // 删除值为val的节点 void delDNode(DNode *&head, int val) { if (head == nullptr) return; DNode *p = head; while (p != nullptr && p->data != val) p = p->next; if (p == nullptr) return; // 找不到 // 情况1:待删节点是头节点 if (p->prev == nullptr) { head = p->next; if (head != nullptr) head->prev = nullptr; } // 情况2:中间/尾部节点 else { p->prev->next = p->next; if (p->next != nullptr) p->next->prev = p->prev; } delete p; } // 销毁链表 void destroyDList(DNode *&head) { DNode *p = head; while (p != nullptr) { DNode *temp = p; p = p->next; delete temp; } head = nullptr; } int main() { DNode *head = nullptr; tailInsert(head, 1); tailInsert(head, 2); tailInsert(head, 3); cout << "正向:"; printForward(head); // 1 2 3 cout << "反向:"; printBackward(head); // 3 2 1 delDNode(head, 2); cout << "删除2,正向:"; printForward(head); //1 3 destroyDList(head); return 0; }双向链表注意点
插入、删除操作需要同时维护 prev 和 next 两个指针,容易漏写导致断链!
三、循环单链表(常用循环链表;零基础先学这个,循环双链表作为拓展)
普通单链表尾节点
next=nullptr循环单链表:尾节点的next指向头节点,形成环形!示意图:
头→节点2→节点3→头特点:没有
nullptr,遍历终止条件:回到头节点#include <iostream> using namespace std; struct CNode { int data; CNode *next; CNode(int val) { data = val; next = nullptr; } }; // 尾插法(循环单链表) void tailInsertCircle(CNode *&head, int val) { CNode *new_node = new CNode(val); // 空链表 if (head == nullptr) { head = new_node; new_node->next = head; // 自环 return; } // 找到尾部:next != head CNode *p = head; while (p->next != head) { p = p->next; } p->next = new_node; new_node->next = head; } // 遍历循环链表 void printCircle(CNode *head) { if (head == nullptr) return; CNode *p = head; do { cout << p->data << " "; p = p->next; } while (p != head); // 回到头节点停止 cout << endl; } // 删除指定数值节点 void delCircleNode(CNode *&head, int val) { if (head == nullptr) return; CNode *p = head; CNode *pre = nullptr; // 寻找目标节点 do { if (p->data == val) break; pre = p; p = p->next; } while (p != head); if (p->data != val) return; // 未找到 // 情况1:链表只有一个节点 if (p->next == head && pre == nullptr) { delete p; head = nullptr; return; } // 情况2:删除头节点 if (p == head) { // 先找到尾节点 CNode *tail = head; while (tail->next != head) tail = tail->next; head = head->next; tail->next = head; } else { // 删除中间/尾部节点 pre->next = p->next; } delete p; } // 释放循环链表 void destroyCircle(CNode *&head) { if (head == nullptr) return; CNode *p = head; CNode *temp; do { temp = p; p = p->next; delete temp; } while (p != head); head = nullptr; } int main() { CNode *head = nullptr; tailInsertCircle(head,10); tailInsertCircle(head,20); tailInsertCircle(head,30); cout << "循环链表:"; printCircle(head); //10 20 30 delCircleNode(head, 20); cout << "删除20:"; printCircle(head); //10 30 destroyCircle(head); return 0; }循环链表经典场景
约瑟夫环问题(报数出圈),最适合使用循环链表。
三大链表横向对比表
类型 指针数量 遍历方向 尾部标记 难度 特点 单链表 1(next) 只能向后 nullptr ⭐ 代码最简单,删除需要前驱 双向链表 2(prev+next) 向前+向后 ⭐⭐⭐ 操作灵活,维护两个指针 循环单链表 1(next) 向后循环 尾→头 ⭐⭐ 环形结构,约瑟夫环专用 📌 零基础学习重点&踩坑提醒
- 引用
Node *&head函数内部修改头指针必须加引用,否则修改无效! - 链表遍历循环条件不要写死,区分普通链表
p!=nullptr和循环链表p!=head - 删除节点务必使用delete释放内存,竞赛不强制,但正规写法必须写
- 插入节点顺序不能颠倒!指针赋值顺序错直接断链
带头结点链表模板(竞赛首选)
什么是头结点? 在链表最前方额外放一个不存储有效数据的节点,
head永远指向它。 ✅优势:所有插入、删除操作统一逻辑,不需要单独判断是否操作头节点,大幅减少分支代码,考场不容易写错。一、带头结点 · 单链表
#include <iostream> using namespace std; // 节点定义 struct Node { int data; Node *next; Node(int val) { data = val; next = nullptr; } }; // 创建带头结点的空链表 Node* createList() { // head是头结点,不存数据 Node *head = new Node(-1); head->next = nullptr; return head; } // 尾插法 void tailInsert(Node *head, int val) { Node *new_node = new Node(val); Node *p = head; // 找到最后一个有效节点 while(p->next != nullptr) p = p->next; p->next = new_node; } // 在pos位置后插入val(pos为数值) void insertAfter(Node *head, int pos, int val) { Node *p = head->next; while(p != nullptr && p->data != pos) p = p->next; if(p == nullptr) return; Node *new_node = new Node(val); new_node->next = p->next; p->next = new_node; } // 删除第一个值为val的节点 void delNode(Node *head, int val) { Node *p = head; // p停在目标节点的前驱 while(p->next != nullptr && p->next->data != val) p = p->next; if(p->next == nullptr) return; Node *temp = p->next; p->next = temp->next; delete temp; } // 遍历输出(跳过头结点) void printList(Node *head) { Node *p = head->next; while(p != nullptr) { cout << p->data << " "; p = p->next; } cout << endl; } // 销毁链表(包括头结点) void destroyList(Node *head) { Node *p = head; while(p != nullptr) { Node *temp = p; p = p->next; delete temp; } } int main() { Node *head = createList(); tailInsert(head,10); tailInsert(head,20); tailInsert(head,30); cout << "初始链表:"; printList(head); insertAfter(head,20,25); cout << "20后面插入25:"; printList(head); delNode(head,10); cout << "删除10:"; printList(head); destroyList(head); return 0; }二、带头结点 · 双向链表
#include <iostream> using namespace std; struct DNode { int data; DNode *prev; DNode *next; DNode(int val) { data = val; prev = nullptr; next = nullptr; } }; // 创建带头结点空双链表 DNode* createDList() { DNode *head = new DNode(-1); head->prev = nullptr; head->next = nullptr; return head; } // 尾插 void tailInsert(DNode *head, int val) { DNode *new_node = new DNode(val); DNode *p = head; while(p->next != nullptr) p = p->next; p->next = new_node; new_node->prev = p; } // 删除值val节点 void delDNode(DNode *head, int val) { DNode *p = head->next; while(p != nullptr && p->data != val) p = p->next; if(p == nullptr) return; p->prev->next = p->next; if(p->next != nullptr) p->next->prev = p->prev; delete p; } // 正向打印 void printForward(DNode *head) { DNode *p = head->next; while(p != nullptr) { cout << p->data << " "; p = p->next; } cout << endl; } void destroyDList(DNode *head) { DNode *p = head; while(p != nullptr) { DNode *temp = p; p = p->next; delete temp; } } int main() { DNode *head = createDList(); tailInsert(head,1); tailInsert(head,2); tailInsert(head,3); cout << "正向:"; printForward(head); delDNode(head,2); cout << "删除2:"; printForward(head); destroyDList(head); return 0; }三、带头结点 · 循环单链表
#include <iostream> using namespace std; struct CNode { int data; CNode *next; CNode(int val) { data = val; next = nullptr; } }; // 创建带头结点循环链表 CNode* createCircle() { CNode *head = new CNode(-1); head->next = head; // 头结点自环 return head; } // 尾插 void tailInsert(CNode *head, int val) { CNode *new_node = new CNode(val); CNode *p = head; while(p->next != head) p = p->next; p->next = new_node; new_node->next = head; } // 删除节点 void delCircleNode(CNode *head, int val) { CNode *p = head; while(p->next != head && p->next->data != val) p = p->next; if(p->next == head) return; CNode *temp = p->next; p->next = temp->next; delete temp; } void printCircle(CNode *head) { CNode *p = head->next; while(p != head) { cout << p->data << " "; p = p->next; } cout << endl; } void destroyCircle(CNode *head) { CNode *p = head->next; while(p != head) { CNode *temp = p; p = p->next; delete temp; } delete head; } int main() { CNode *head = createCircle(); tailInsert(head,10); tailInsert(head,20); tailInsert(head,30); cout << "循环链表:"; printCircle(head); delCircleNode(head,20); cout << "删除20:"; printCircle(head); destroyCircle(head); return 0; }📌 无头结点 VS 带头结点总结
- 无头结点(之前版本)
空链表:
head=nullptr;插入头部、删除头部需要单独判断,分支多。适合理解原理,不推荐竞赛做题。 - 带头结点(本套代码)
链表永远不会
head=nullptr;所有操作代码统一,不需要特殊处理首元素。信奥/GESP竞赛优先写这种!
学习建议
先跑通【带头结点单链表】,吃透后再学习双向、循环版本。
-
@ 2026-7-31 20:24:12
追加内容:单链表反转 + 约瑟夫环(循环链表实现)
承接上文GESP五级链表教程,代码全部详细注释,0基础可读,竞赛直接默写。
一、单链表反转(GESP五级经典大题)
思路通俗讲解
准备三个指针:前驱pre、当前cur、后继nxt。 依次改变每个结点next指针方向,让链表掉头。
#include <iostream> using namespace std; // 单链表结点 struct Node { int data; Node* next; Node(int val) : data(val), next(NULL) {} }; // 尾部插入结点 void insertTail(Node* &head, int val) { Node* newnode = new Node(val); if (head == NULL) { head = newnode; return; } Node* p = head; while (p->next != NULL) p = p->next; p->next = newnode; } // 链表遍历输出 void printList(Node* head) { Node* p = head; while (p != NULL) { cout << p->data << " "; p = p->next; } cout << endl; } // 反转单链表,返回新链表头结点 Node* reverseList(Node* head) { Node* pre = NULL; // 当前结点的前一个结点 Node* cur = head; // 当前正在操作的结点 Node* nxt = NULL; // 保存下一个结点,防止断链 while (cur != NULL) { nxt = cur->next; // 先记下后面的结点,防止链表断掉 cur->next = pre; // 反转:当前结点指向前面 pre = cur; // pre往前走一步 cur = nxt; // cur往前走一步 } return pre; // 循环结束pre是新头结点 } int main() { Node* head = NULL; insertTail(head, 1); insertTail(head, 2); insertTail(head, 3); insertTail(head, 4); cout << "原链表:"; printList(head); head = reverseList(head); cout << "反转后:"; printList(head); return 0; }考场易错提醒
- 一定要先用
nxt = cur->next保存后继,修改指针后无法找到后续结点; - 最终返回值是pre,不是cur;
- 空链表、只有1个结点的链表代码可以自动兼容,无需额外特判。
二、循环链表实现约瑟夫环(GESP五级压轴题型)
题目通俗描述
n个人围成一圈,从第一个开始报数,报到k的人出局;下一个人重新开始报数,不断循环,求出最后剩下人的编号。
#include <iostream> using namespace std; // 循环链表结点 struct Node { int id; // 人的编号 Node* next; Node(int val) : id(val), next(NULL) {} }; // 创建n个人的循环链表 Node* createCircle(int n) { if (n <= 0) return NULL; Node* head = new Node(1); Node* tail = head; // 依次创建2~n号结点 for (int i = 2; i <= n; i++) { Node* newnode = new Node(i); tail->next = newnode; tail = newnode; } tail->next = head; // 尾结点连回头,形成环 return head; } // 约瑟夫环模拟:n人,数k出局 int Josephus(int n, int k) { Node* head = createCircle(n); Node* p = head; // 找到尾结点(head前一个结点,方便删除) while (p->next != head) p = p->next; // 剩余人数大于1就持续淘汰 while (p->next != p) { // 向后走k-1次,到达要删除结点的前驱 for (int i = 1; i < k; i++) { p = p->next; } Node* del = p->next; // 需要淘汰的结点 cout << "出局:" << del->id << endl; p->next = del->next; // 跳过被删除结点 delete del; // 释放结点 } int ans = p->id; delete p; return ans; } int main() { int n, k; cout << "输入人数n、报数上限k:"; cin >> n >> k; int last = Josephus(n, k); cout << "最后留下编号:" << last << endl; return 0; }运行示例
输入:
5 3输出顺序出局:3,1,5,2;最后剩余:4考场重点提示
- 保存待删除结点的前驱结点,才能修改链表指针;
- 循环终止条件:
p->next == p,只剩最后一个结点; - 不要直接从目标结点开始遍历,无法完成删除操作。
拓展小结(选择题考点)
- 单链表反转:三指针迭代写法是竞赛首选,递归写法理解难度高,考场优先迭代;
- 约瑟夫环两种解法:小规模用循环链表模拟;超大范围用数学递推公式;GESP五级一般考察链表模拟版本。
- 一定要先用
- 1