HNU-人工智能-实验1-A*算法

人工智能-实验1

计科210x 甘晴void
在这里插入图片描述

一、实验目的

  • 掌握有信息搜索策略的算法思想;

  • 能够编程实现搜索算法;

  • 应用A*搜索算法求解罗马尼亚问题。

二、实验平台

  • 课程实训平台https://www.educoder.net/shixuns/vgmzcukh/challenges

三、实验内容

3.0 题目要求

罗马尼亚问题:agent在罗马尼亚度假,目前位于 Arad 城市。agent明天有航班从Bucharest 起飞,不能改签退票。

现在你需要寻找到 Bucharest 的最短路径,在右侧编辑器补充void A_star(int goal,node &src,Graph &graph)函数,使用编写的搜索算法代码求解罗马尼亚问题:

在这里插入图片描述

3.1 A*算法原理

A*算法的原理是设计一个代价估计函数:其中 **评估函数F(n)**是从起始节点通过节点n的到达目标节点的最小代价路径的估计值,函数G(n)是从起始节点到n节点的已走过路径的实际代价,函数H(n)是从n节点到目标节点可能的最优路径的估计代价 。

函数 H(n)表明了算法使用的启发信息,它来源于人们对路径规划问题的认识,依赖某种经验估计。根据 F(n)可以计算出当前节点的代价,并可以对下一次能够到达的节点进行评估。

采用每次搜索都找到代价值最小的点再继续往外搜索的过程,一步一步找到最优路径。

3.2 算法实现

根据题目要求,实现A*算法,如下图。
在这里插入图片描述

初始给定出发节点,放入openList队列,然后进行扩展操作

  • 扩展操作:对于所有与该节点相连的节点,计算它们的g()值,然后加上h()值,得到f()值,并加入openList队列。
  • openList队列:该队列本质是一个优先队列,以队列中各元素节点的f()值为键进行排序。给定的程序中使用sort+优先队列来实现,实际上这里直接使用优先队列来进行维护会效率更高,这是可以优化的一个点。
  • 选定操作:在扩展,优先化(也就是题中的sort)之后,选定一个f()值最大的作为下一个访问的节点。
  • 访问节点:任何节点的访问操作只能进行一次,但是扩展操作可以进行多次(这其实也可以理解,再次访问该节点显然比原来访问该节点的代价高)
  • 终止条件:按照上述循环重复执行,直至访问到(不是扩展到)目标终点为止。

基本思路可以用下述伪代码展示

void A_star(int goal, node *src, Graph &graph)
{openList.push_back(src);sort(openList.begin(), openList.end(), cmp);while (!openList.empty()){node *now = new node;now = openList.front();openList.erase(openList.begin());// 选定操作(从优先的openList中获取第一个元素)if (now->name == goal){// 终止条件:到达终点,保存退出return;}for (int i = 0; i < 20; i++){if (graph.getEdge(now->name, i) != -1 && !visited[i])// 有边且未被访问过{node *expand = new node(i, now->g + graph.getEdge(now->name, i), h[i], now);openList.push_back(expand);// 执行扩展操作}}sort(openList.begin(), openList.end(), cmp);// 保证按照键优先化}
}

3.3 源码&分析

#include <algorithm>
#include <iostream>
#include <memory.h>
#include <stack>
#include <vector>
#define A 0
#define B 1
#define C 2
#define D 3
#define E 4
#define F 5
#define G 6
#define H 7
#define I 8
#define L 9
#define M 10
#define N 11
#define O 12
#define P 13
#define R 14
#define S 15
#define T 16
#define U 17
#define V 18
#define Z 19using namespace std;int h[20] ={366, 0, 160, 242, 161,178, 77, 151, 226, 244,241, 234, 380, 98, 193,253, 329, 80, 199, 374};struct node
{int g;int h;int f;int name;node(int name, int g, int h){this->name = name;this->g = g;this->h = h;this->f = g + h;};bool operator<(const node &a) const{return f < a.f;}
};class Graph
{
public:Graph(){memset(graph, -1, sizeof(graph));}int getEdge(int from, int to){return graph[from][to];}void addEdge(int from, int to, int cost){if (from >= 20 || from < 0 || to >= 20 || to < 0)return;graph[from][to] = cost;}void init(){addEdge(O, Z, 71);addEdge(Z, O, 71);addEdge(O, S, 151);addEdge(S, O, 151);addEdge(Z, A, 75);addEdge(A, Z, 75);addEdge(A, S, 140);addEdge(S, A, 140);addEdge(A, T, 118);addEdge(T, A, 118);addEdge(T, L, 111);addEdge(L, T, 111);addEdge(L, M, 70);addEdge(M, L, 70);addEdge(M, D, 75);addEdge(D, M, 75);addEdge(D, C, 120);addEdge(C, D, 120);addEdge(C, R, 146);addEdge(R, C, 146);addEdge(S, R, 80);addEdge(R, S, 80);addEdge(S, F, 99);addEdge(F, S, 99);addEdge(F, B, 211);addEdge(B, F, 211);addEdge(P, C, 138);addEdge(C, P, 138);addEdge(R, P, 97);addEdge(P, R, 97);addEdge(P, B, 101);addEdge(B, P, 101);addEdge(B, G, 90);addEdge(G, B, 90);addEdge(B, U, 85);addEdge(U, B, 85);addEdge(U, H, 98);addEdge(H, U, 98);addEdge(H, E, 86);addEdge(E, H, 86);addEdge(U, V, 142);addEdge(V, U, 142);addEdge(I, V, 92);addEdge(V, I, 92);addEdge(I, N, 87);addEdge(N, I, 87);}private:int graph[20][20];
};bool list[20];
vector<node> openList;
bool closeList[20];
stack<int> road;
int parent[20];void A_star(int goal, node &src, Graph &graph)
{openList.push_back(src);sort(openList.begin(), openList.end());while (!openList.empty()){/********** Begin **********/node now = openList.front();if (now.name == goal)return;openList.erase(openList.begin());closeList[now.name] = 1;for (int i = 0; i < 20; i++){if (graph.getEdge(now.name, i) != -1 && !closeList[i]){node expand(i, now.g + graph.getEdge(now.name, i), h[i]);openList.push_back(expand);int flag = true;for (unsigned int j = 0; j < openList.size(); j++){if (openList[j].name == expand.name && openList[j].g < expand.g){flag = false;}}if (flag == true)parent[i] = now.name;}}sort(openList.begin(), openList.end());/********** End **********/}
}void print_result(Graph &graph)
{int p = openList[0].name;int lastNodeNum;road.push(p);while (parent[p] != -1){road.push(parent[p]);p = parent[p];}lastNodeNum = road.top();int cost = 0;cout << "solution: ";while (!road.empty()){cout << road.top() << "-> ";if (road.top() != lastNodeNum){cost += graph.getEdge(lastNodeNum, road.top());lastNodeNum = road.top();}road.pop();}cout << "end" << endl;cout << "cost:" << cost;
}int main()
{Graph graph;graph.init();for (int i = 0; i < 20; i++)parent[i] = -1;node src(0, 0, h[0]);A_star(1, src, graph);print_result(graph);
}

具体分析如下:

  • 结构体定义:结构体 node 定义了节点的属性,包括节点名称 name,从起点到该节点的路径长度 g,该节点到目标节点的估计距离 h,以及综合路径长度和估计距离的总代价 f。重载了小于操作符,以便在优先队列中进行排序。
  • 图的表示:使用二维数组 graph[20][20] 表示图,数组大小为 20x20,即有 20 个节点。数组中存储了节点之间的边的权值。
  • 初始化图:在 Graph 类的 init() 方法中,添加了节点之间的边及对应的权值。
  • A*搜索算法A_star 函数实现了A*搜索算法。它通过优先队列 openList 来管理待扩展的节点,并且利用数组 closeList 来记录已经访问过的节点。parent 数组用于记录每个节点的父节点,方便后续回溯路径。算法首先将起始节点加入到 openList 中,并进行排序。然后,循环进行以下步骤:
    • 取出 openList 中的首节点 now,如果该节点是目标节点,则搜索结束。
    • now 加入到 closeList 中,表示已经访问过。
    • 遍历与当前节点相邻的节点,如果相邻节点未被访问过,则将其加入到 openList 中,并更新其父节点为当前节点,并根据当前节点到该相邻节点的路径长度以及该节点到目标节点的估计距离计算总代价 f
    • 最后对 openList 进行排序,以保证优先扩展代价较小的节点。
  • 打印结果print_result 函数用于打印搜索结果,即输出找到的最短路径以及路径的总代价。
    • 将起始节点压入栈中。
    • 从目标节点开始,通过 parent 数组逐步向上回溯,将经过的节点依次压入栈中,直到回溯到起始节点。
    • 在压入栈的过程中,同时计算路径的总代价。因为 A* 算法是一种启发式搜索算法,搜索到的路径并不一定是最优的,但它会在每一步中选择一个启发性最好的节点进行扩展,因此得到的路径一般是较优的。
    • 最后,从栈中依次弹出节点,打印出完整的路径,并输出路径的总代价。
  • 主函数:在主函数中,首先初始化图,然后调用 A_star 函数进行搜索,并最终打印搜索结果。【注意】主函数是在线平台中没有的,在线平台应该指定了程序入口并做了变量初始化的工作。因此我们用主函数来实现这个工作。

综上所述,这段代码实现了使用A*算法在给定图中寻找从起点到目标点的最短路径,并输出了路径以及路径的总代价。

3.4 基于题目的代码:算法分析

时间复杂度可能趋近于O(n^3),主要原因与维护parent数组的最新性有关,这个后面在讲想法的时候会具体说明。

空间复杂度应该有O(n^2),因为使用邻接矩阵来存储图。

3.5 基于题目的代码:困惑思考

基于题目做这道题的时候,我感觉很困惑。

A*算法不是一个非常复杂的算法,但题目中的一些操作让我感觉有点迷。

①parent数组问题

题目使用openList中保存被扩展的节点,然后用closeList来标记被访问的节点,用parent来存储每个节点的父亲这种方式。

这会带来一个挑战:在一个节点被扩展之后,它不一定被立即访问。

如下面这张图,Sibiu节点的四个子节点中,最右子节点Rimnicu Vilcca先被访问,但后来Sibiu的左起第二个子节点Fagaras又被访问到了。此时若该两个子节点都有相同的另一个子节点M,则M可能同时具有Rimnicu Vilcca和Fagaras两个父节点。这样用一个parent数组显然是没办法表示的(注意这里不能是覆盖关系,因为这两个子节点都是“被扩展”的状态而不是“已被访问”的状态,真正被访问的节点有可能从它们之一产生)

在这里插入图片描述

比如,出现下图所示情况。若Bucharest不是最终节点,则它同时又两个parent,这显然无法用一个parent数组存下。

在这里插入图片描述

因此理想的方法是将parent作为一个属性写入该节点的node结构体中去。但这样还没解决一个问题,输出结果会有点烦。

或者使用后面改进的方法,直接用指针来作为属性。这样只需要指针走一遍,就可以把顺序给呈现出来了。

但是,原题中我也想办法解决了,其实观察或者是从题目中可以发现,一个节点如果发现有比它g()值更小的节点,实际上g()值较大的那个显然就没有用了,即使予以保留,最终也会在较低优先级而不会被调用(这个实际上看的是f()值,事实上是一样的,因为f()=g()+h(),h()显然只与节点有关系)。故我直接略去g()值较大的节点,默认它们直接被淘汰掉了,parent数组只保存g()值最小的那个所对应的。

②vector+sort替代priority_queue

题目中使用了vector来保存访问节点的结构体,再加上sort来保证优先级,其实可以直接用priority_queue来实现,效率更高。

3.6 改进代码-结构体

只展示核心代码,略去重复的宏定义部分和graph类。

使用结构体和指针绕开了parent数组的问题

#include <algorithm>
#include <iostream>
#include <memory.h>
#include <stack>
#include <vector>
#define A-Z (略)using namespace std;int h[20] ={366, 0, 160, 242, 161,178, 77, 151, 226, 244,241, 234, 380, 98, 193,253, 329, 80, 199, 374};struct node
{int g;int h;int f;int name;node *parent;node() {}node(int name, int g, int h, node *parent){this->name = name;this->g = g;this->h = h;this->f = g + h;this->parent = parent;};bool operator<(const node a) const{return f < a.f;}
};class Graph //(略)vector<node *> openList;
node *des;
bool visited[20];bool cmp(node *a, node *b) { return a->f < b->f; }
void A_star(int goal, node *src, Graph &graph)
{openList.push_back(src);sort(openList.begin(), openList.end(), cmp);while (!openList.empty()){node *now = new node;now = openList.front();openList.erase(openList.begin());visited[now->name] = 1;// cout << now->name << endl;// system("pause");if (now->name == goal){des = now;return;}for (int i = 0; i < 20; i++){if (graph.getEdge(now->name, i) != -1 && !visited[i]){node *expand = new node(i, now->g + graph.getEdge(now->name, i), h[i], now);openList.push_back(expand);// cout << "expand: " << expand->name << endl;}}sort(openList.begin(), openList.end(), cmp);}
}void print_result(Graph &graph)
{cout << "solution: ";stack<int> ans;node *now = des;while (now != NULL){ans.push(now->name);// cout << now->name << endl;now = now->parent;}while (!ans.empty()){cout << ans.top() << "-> ";ans.pop();}cout << "end" << endl;cout << "cost:" << des->g << endl;
}int main()
{Graph graph;graph.init();memset(visited, 0, sizeof(visited));node *src = new node(0, 0, h[0], NULL);A_star(1, src, graph);print_result(graph);
}

3.7 改进代码2-<priority_queue>

只需要在上面代码的基础上更改部分即可,但考虑到这里使用的其实是结构体的指针,故重载运算符其实不太好操作。我采取使用比较函数的方法来完成大小的判断。

bool cmp(node *a, node *b) { return a->f > b->f; }
priority_queue<node *, vector<node *>, decltype(&cmp)> openList(&cmp);void A_star(int goal, node *src, Graph &graph)
{openList.push(src);while (!openList.empty()){node *now = new node;now = openList.top();openList.pop();visited[now->name] = 1;// cout << now->name << endl;// system("pause");if (now->name == goal){des = now;return;}for (int i = 0; i < 20; i++){if (graph.getEdge(now->name, i) != -1 && !visited[i]){node *expand = new node(i, now->g + graph.getEdge(now->name, i), h[i], now);openList.push(expand);// cout << "expand: " << expand->name << endl;}}}
}

3.8 运行截图

在这里插入图片描述

四、思考题

1:宽度优先搜索,深度优先搜索,一致代价搜索,迭代加深的深度优先搜索算法哪种方法最优?

首先分析这四种算法,

  • 宽度优先搜索(BFS):通常在最短路径问题上表现优异,但是空间复杂度很高,因为需要保存所有已经访问的节点。
  • 深度优先搜索(DFS):解空间较大,在解相对较浅的问题上可能更有效率,但是可能会陷入无限深度的分支。
  • 迭代加深深度优先搜索(IDDFS):结合了DFS和BFS的优点,在不断增加的深度限制上调用深度受限搜索。对于深度搜索问题而言,是一种比较有效的方法。
  • 一致代价搜索(UCS):保证在图中搜索的每一步都是最小代价的算法,通常在无启发式的情况下用于解决最短路径问题。

对于一般的问题而言,一致代价搜索是更优的。但对于不同问题要具体问题具体分析,如问题的时间或空间限制等。

2:贪婪最佳优先搜索和A*搜索哪种方法最优?

首先分析这两种算法,

  • 贪婪最佳优先搜索:根据启发式函数h()所提供的信息,每次选择看起来最有希望的节点进行扩展,但是它不能保证找到最优解,因为它没有考虑到节点到目标的真实代价。
  • A*搜索算法:通过综合考虑节点的实际代价g()和启发式函数h()的估计值,保证了在每一步都能选择到最优的节点进行扩展,从而保证找到最优解。

A*搜索算法通常在需要找到最优解的问题上更为优秀,因为它考虑了实际代价。

3:分析比较无信息搜索策略和有信息搜索策略。

无信息搜索策略和有信息搜索策略是指搜索算法是否利用额外的信息来指导搜索方向:

  • 无信息搜索策略,如深度优先搜索(DFS)、宽度优先搜索(BFS)和一致代价搜索(UCS),只利用当前节点的信息进行搜索,不考虑节点到目标的距离或代价,因此可能需要更多的搜索步骤来找到解。
  • 有信息搜索策略,如A*搜索算法和贪婪最佳优先搜索,利用启发式函数提供的额外信息(如节点到目标的估计距离)来指导搜索方向,从而更快地找到解。有信息搜索策略通常能更快地找到最优解,但是需要在空间和时间上付出更多的代价来计算和存储启发式函数的值。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/pingmian/6722.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

如何将数据导入python

Python导入数据的三种方式&#xff1a; 1、通过标准的Python库导入CSV文件 Python提供了一个标准的类库CSV文件。这个类库中的reader()函数用来导入CSV文件。当CSV文件被读入后&#xff0c;可以利用这些数据生成一个NumPy数组&#xff0c;用来训练算法模型。 from csv import…

详细介绍如何使用YOLOv9 在医疗数据集上进行实例分割-含源码+数据集下载

深度学习彻底改变了医学图像分析。通过识别医学图像中的复杂模式,它可以帮助我们解释有关生物系统的重要见解。因此,如果您希望利用深度学习进行医疗诊断,本文可以成为在医疗数据集上微调YOLOv9 实例分割的良好起点。 实例分割模型不是简单地将区域分类为属于特定细胞类型,…

基于Springboot的校园竞赛管理系统(有报告)。Javaee项目,springboot项目。

演示视频&#xff1a; 基于Springboot的校园竞赛管理系统&#xff08;有报告&#xff09;。Javaee项目&#xff0c;springboot项目。 项目介绍&#xff1a; 采用M&#xff08;model&#xff09;V&#xff08;view&#xff09;C&#xff08;controller&#xff09;三层体系结构…

stl容器 string类的基本操作

目录 一.string类的构造 二.string类的输出 1.传统字符串输出 2.通过迭代器进行输出 ​编辑 3.C11标准的范围for输出加auto推导类型 三.string类的各种迭代器 begin(&#xff09;和end() 利用迭代器遍历输出 利用迭代器修改字符串的字符 rbgin()和rend() 利用迭代器遍…

创造价值与回报:创业者的思维格局与商业智慧

在纷繁复杂的商业世界中&#xff0c;有一种信念始终贯穿于无数创业者的心中——那就是创造价值。张磊的这句“只要不断地创造价值&#xff0c;迟早会有回报”道出了创业者的核心思维格局和商业智慧。本文将从创业者的角度&#xff0c;探讨创造价值的重要性&#xff0c;以及如何…

5.Spring Security-web权限方案

设置登录的用户名和密码 1.通过配置文件设置用户名密码 spring:security:user:name: xiankejinpassword: 123456 如果没有以上配置&#xff0c;那么就会在后台生成一个随机密码&#xff0c;用户名固定位user。 2.通过配置类设置用户名密码 Configuration public class Sec…

为什么说虚拟化技术是现代网络安全的重要组成部分?

虚拟化技术是一种对计算机资源的抽象和资源管理技术&#xff0c;将电脑的各种实体资源&#xff08;CPU、内存、磁盘空间、网络适配器等&#xff09;予以抽象、转换后呈现出来&#xff0c;并可供分割、组合为一个或多个电脑配置环境。今天德迅云安全带您了解为什么虚拟化技术能成…

翻译: 什么是ChatGPT 通过图形化的方式来理解 Transformer 架构 深度学习一

合集 ChatGPT 通过图形化的方式来理解 Transformer 架构 翻译: 什么是ChatGPT 通过图形化的方式来理解 Transformer 架构 深度学习一翻译: 什么是ChatGPT 通过图形化的方式来理解 Transformer 架构 深度学习二翻译: 什么是ChatGPT 通过图形化的方式来理解 Transformer 架构 深…

基于AT89C51单片机的温度上下限自动控制检报警设计

点击链接获取Keil源码与Project Backups仿真图: https://download.csdn.net/download/qq_64505944/89247694?spm=1001.2014.3001.5501 C 源码+仿真图+毕业设计+实物制作步骤+06 题 目 基于单片机的温度检测调节系统设计 姓 名 学 号 专业班级 指导教师 年 月 日 任务书 …

在Mac OS系统下查看CPU型号以及核心数量

1. 基础信息 一般点开mac的关于本机&#xff0c;显示的是下面的信息&#xff1a; 2. 当前电脑的处理器型号 找到并打开终端输入下面命令&#xff1a; sysctl machdep.cpu.brand_string结果如下图&#xff1a; 3. 当前处理器物理核心数量 找到并打开终端输入下面命令&am…

有没有适合制造企业用的研发项目管理软件?制造业选型案例必看!“追觅”上线奥博思项目管理软件,加速项目交付

智能清洁家电赛道的领军者&#xff1a;追觅科技&#xff08;苏州&#xff09;有限公司&#xff08;以下简称“追觅”&#xff09;成功上线奥博思 PowerProject 数字化项目管理系统。通过 PowerProject 系统&#xff0c;追觅公司能够实现项目全流程的覆盖&#xff0c;从预研阶段…

【电源专题】拿人体的循环系统与板级电源做个比较

一般人可能会觉得电源大概是电子设备里面比较容易搞定的门类。因为,只要线路没有接错,指示灯(如果有)能亮,电源都能工作。从这个方面说,好像是很容易。但是通过多年的经验和经历的坑,发现电源其实是一个很麻烦的东西,稍微有一点不完美就会有大问题出现。 如果将人体也当…

ue引擎游戏开发笔记(30)——对角色移动进行优化:实现人物转向

1.需求分析&#xff1a; 当前我们只实现了通过控制器可使角色进行前后左右的移动&#xff0c;但角色移动时与动画不匹配&#xff0c;并不会进行转向&#xff0c;实现角色随移动转向。 2.操作实现&#xff1a; 1思路&#xff1a;利用反转换函数inverse transform direction获取…

【busybox记录】【shell指令】md5sum

目录 内容来源&#xff1a; 【GUN】【md5sum】指令介绍 【busybox】【md5sum】指令介绍 【linux】【md5sum】指令介绍 使用示例&#xff1a; 128位MD5 - 默认输出 128位MD5 - 将每个文件当做二进制处理 128位MD5 - 从文件中读取MD5值并做检查 128位MD5 - 创建一个BSD风…

李廉洋:5.5-5.6现货黄金,WTI原油必看分析及策略。

美联储2024年5月议息会议将联邦基金利率的目标区间维持在5.25%-5.5%。本次会议声明发生较大变化&#xff0c;宣布6月开始放缓缩表。鲍威尔讲话总体中性偏鸽&#xff0c;指出美联储的下一步行动不太可能是加息。中信证券认为在美国失业率升至4%以上之前&#xff0c;美联储政策重…

【iOS】KVC

文章目录 前言一、KVC常用方法二、key与keypath区别key用法keypath用法 三、批量存值操作四、字典与模型相互转化五、KVC底层原理KVC设值底层原理KVC取值底层原理 前言 KVC的全称是Key-Value Coding&#xff0c;翻译成中文叫做键值编码 KVC提供了一种间接访问属性方法或成员变…

JavaScript中的DOM和BOM

个人主页&#xff1a;学习前端的小z 个人专栏&#xff1a;JavaScript 精粹 本专栏旨在分享记录每日学习的前端知识和学习笔记的归纳总结&#xff0c;欢迎大家在评论区交流讨论&#xff01; 文章目录 &#x1f4af;Web API&#x1f340;1 API的概念&#x1f340;2 Web API的概念…

【C++ | 关键字】C++ 关键字介绍

&#x1f601;博客主页&#x1f601;&#xff1a;&#x1f680;https://blog.csdn.net/wkd_007&#x1f680; &#x1f911;博客内容&#x1f911;&#xff1a;&#x1f36d;嵌入式开发、Linux、C语言、C、数据结构、音视频&#x1f36d; ⏰发布时间⏰&#xff1a;2024-05-04 0…

手摸手,带你用vue撸后台

前言 说好的教程终于来了&#xff0c;第一篇文章主要来说一说在开始写实际业务代码之前的一些准备工作吧&#xff0c;但这里不会教你 webpack 的基础配置&#xff0c;热更新原理是什么&#xff0c;webpack速度优化等等&#xff0c;有需求的请自行 google&#xff0c;相关文章已…

【C++】详解STL的容器之一:list

目录 简介 初识list 模型 list容器的优缺点 list的迭代器 常用接口介绍 获取迭代器 begin end empty size front back insert push_front pop_front push_back pop_back clear 源代码思路 节点设计 迭代器的设计 list的设计 begin() end() 空构造 ins…