《C和指针》读书笔记(第十二章 使用结构和指针)

news2024/11/22 11:20:10

目录

  • 0 简介
  • 1 链表
  • 2 单链表
    • 2.1 在单链表中插入
      • 2.1.1 初次尝试
      • 2.1.2 优化插入函数
      • 2.1.3 在指定位置插入节点(补充)
    • 2.2 其他链表操作
      • 2.2.1 单链表的创建
      • 2.2.2 单链表的删除
        • 2.2.2.1 删除指定位置的节点
        • 2.2.2.2 删除整个链表
  • 3 双链表
    • 3.1 在双链表中插入
      • 3.1.1 按顺序插入
      • 3.1.2 在指定位置插入节点(补充)
    • 3.2 其他链表操作
      • 3.2.1 双链表的创建
      • 3.2.2 双链表的删除
        • 3.2.2.1 删除指定位置的节点
        • 3.2.2.2 删除整个链表
  • 4 总结

0 简介

纸上得来终觉浅,绝知此事要躬行。前几章学习了结构体、联合体和指针的相关知识。本章就是对这些知识的综合应用。

书中是以链表作为实例的,严格意义上来说,链表属于数据结构与算法的相关内容,关于这方面的知识,想学习的同学推荐《大话数据结构》这本书。书中的内容在通俗易懂的同时,又不乏严谨性。

链表,顾名思义,就是像铁链一样,环环相扣的表。链表是由节点 一个个连起来的,每个节点由两部分组成,分别是数据指向下一个节点的指针(数据可能不止一个,但是一般的教材和资料的举例中都是一个)。

比较遗憾的是,在本书中,仅仅给了链表的插入操作相关程序,但如果没有了链表的创建删除(非必需,但最好有)程序,书中的实例是无法运行的。因此,在我们本篇文章中,特意添加了其他所需的相关程序。

书中部分插图有误,请注意甄别。

本章内容提要:
第十二章 使用结构和指针

1 链表

如上所述,链表是一种数据结构,由一个个节点构成,通常节点是动态分配的,根据结构的不同,可分为单链表、双链表和环形链表等。本章重点介绍单链表和双链表。

2 单链表

在单链表中,每个节点包含一个指向链表下一个节点的指针。链表最后一个节点的指针字段为NULL,提示链表后面不再有其他节点。我们来看个单链表的示意图:
在这里插入图片描述
root节点又叫根节点,链表就是基于此进行创建的。链表的创建分成两种,一种是头插法,一种是尾插法

2.1 在单链表中插入

链式的结构,使得链表在插入和删除新数据的时候比数组效率高很多(平均速度),所以链表的插入操作显得尤为重要。本章重点讲了单链表的插入操作。

2.1.1 初次尝试

标题取的不是很准确,更加准确地说,应该是在单链表中插入节点。我们将书中第一个版本的程序完善后,得到了如下的内容:

先定义节点,声明相关函数,所以定义.h文件如下:

#pragma once
typedef struct NODE {
	struct NODE *link;
	int value;
}Node;

int sll_insert(Node *current, int new_value);

然后定义插入函数,添加如下的.c文件

#include <stdlib.h>
#include <stdio.h>
#include "sll_node.h"
#define FALSE 0
#define TRUE 1
int sll_insert(Node *current, int new_value)
{
	Node *previous = (Node *)malloc(sizeof(Node));
	Node *new;
	while (current->value < new_value) 
	{
		previous = current;
		current = current->link;
	}
	new = (Node *)malloc(sizeof(Node));
	if (new == NULL)
		return FALSE;
	new->value = new_value;

	new->link = current;
	previous->link = new;

	return TRUE;
}

最后写主函数,添加如下的.c文件:

#include <stdlib.h>
#include <stdio.h>
#include "sll_node.h"
//单链表的整表创建
void CreateList(Node *L, int n, int *point)
{
	Node *p1, *r;
	int i;
	r = L;
	for (i = 0; i < n; i++)
	{
		p1 = (Node *)malloc(sizeof(Node));
		p1->value = point[i];
		r->link = p1;
		r = p1;
	}
	r->link = NULL;
}
//单链表的整表删除
void  ClearList(Node *L)
{
	Node *p, *q;
	p = L->link;
	while (p != NULL)
	{
		q = p->link;
		free(p);
		p = q;
	}
	L->link = NULL;
	printf("clear all\n");
}
int main()
{
	int ele[10];
	//链表数值设置
	for (int i = 0; i < 10; i++)
	{
		ele[i] = i * 5;
	}
	//定义根指针
	Node *p = (Node *)malloc(sizeof(Node));
	//定义第一个节点的指针
	Node *new_head = NULL;
	//创建链表
	printf("原始链表:\n");
	CreateList(p, 10, ele);
	//读取并打印链表
	new_head = p->link;

	while (new_head->link != NULL)
	{
		printf("%d->\t", new_head->value);
		new_head = new_head->link;
	}
	printf("%d", new_head->value);
	printf("\n");
	//插入值为8的节点
	sll_insert(p, 8);
	//再次读取并打印链表
	printf("插入节点后的链表:\n");
	new_head = p->link;
	while (new_head->link != NULL)
	{
		printf("%d->\t", new_head->value);
		new_head = new_head->link;
	}
	printf("%d", new_head->value);
	printf("\n");
	//删除整个链表
	ClearList(p);
	system("pause");
	return 0;
}

打印输出如下:
在这里插入图片描述
可以看到,已经将节点插入到了所需的位置(位置由节点的值确定),看看节点的具体插入过程(为简便起见,只画出前三个节点)。

我们需要先找到正确的插入位置:

	while (current->value < new_value) 
	{
		previous = current;
		current = current->link;
	}

这部分代码很好理解,如果当前值比新值小,则继续前往下一个节点。如图所示:
在这里插入图片描述
直到当前值大于或等于新值:
在这里插入图片描述

然后我们再想办法插入新值:

	new->value = new_value;

	new->link = current;
	previous->link = new;

共分成3步:

  1. 将要插入的值赋给新创建的节点。
  2. 将新节点的指针指向当前节点。
  3. 前一个节点的指针指向新节点。

具体过程如下图所示:
在这里插入图片描述
至此,链表的新节点插入完成。

2.1.2 优化插入函数

书中这里的优化指的是,若是将一个比第一个节点更小的值传入链表,则会导致程序无法运行,因为传入的是第一个节点。但我们并不存在这个问题,因为我们传入的是根节点。

仅仅有一个小小的地方需要优化,就是在寻找插入位置之前,更改保证当前指针不是空指针,否则不会存在value。 修改后的程序如下所示(仅展示修改部分):

	while (current != NULL && current->value < new_value)
	{
		previous = current;
		current = current->link;
	}

2.1.3 在指定位置插入节点(补充)

此外,还有一种插入方法,就是在指定的位置插入节点。实现起来也并不困难。

// 在指定位置插入节点
void sll_insertNode(Node** head, int data, int position) 
{
	if (*head == NULL || position <= 1) {
		// 如果链表为空或插入位置为头部,则直接在头部插入节点
		Node* newNode =  sll_createLinkedList(data);
		newNode->link = *head;
		*head = newNode;
	}
	else {
		// 否则在指定位置插入节点
		Node* temp = *head;
		int currentPosition = 1;

		while (currentPosition < position - 1 && temp->link != NULL) {
			temp = temp->link;
			currentPosition++;
		}

		if (currentPosition < position - 1) {
			printf("插入位置超出链表长度\n");
			return;
		}

		Node* newNode = sll_createLinkedList(data);
		newNode->link = temp->link;

		temp->link = newNode;
	}
}

2.2 其他链表操作

当然,对于链表本身而言,不仅仅只有插入操作,还包含了整个链表的创建删除。已经在上述代码中有所体现。

2.2.1 单链表的创建

单链表的创建有两种方法,分别是头插法尾插法。上述的例子中采用了尾插法,因为这样更符合我们的常规思维。

//单链表的整表创建
void CreateList(Node *L, int n, int *point)
{
	Node *p1, *r;
	int i;
	r = L;
	for (i = 0; i < n; i++)
	{
		p1 = (Node *)malloc(sizeof(Node));
		p1->value = point[i];
		r->link = p1;
		r = p1;
	}
	r->link = NULL;
}

链表的创建过程如下图所示(仅仅给出了前两个节点的创建过程,后面操作类似):
在这里插入图片描述

2.2.2 单链表的删除

删除分成两部分,一部分是删除指定位置的节点,另一部分是删除整个链表,前者程序稍微难一些。

2.2.2.1 删除指定位置的节点

同在指定位置插入一样。删除前要先找到位置(当然是前一个节点的位置)。然后再将前一个节点的link指针绕过要删除的节点,直接跳到下一个节点,再删除当前节点,操作完成。

// 删除指定位置的节点
void sll_deleteNode(Node** head, int position)
{
	if (*head == NULL) {
		return;
	}

	Node* temp = *head;
	int currentPosition = 1;
	//找到该节点的前一个节点
	while (currentPosition < position - 1 && temp->link != NULL) {
		temp = temp->link;
		currentPosition++;
	}

	if (currentPosition < position - 1 || temp->link == NULL) {
		printf("删除位置超出链表长度\n");
		return;
	}
	//如果删除头节点,则该节点后移,否则无法访问该链表
	if (temp == *head) {
		*head = temp->link;
	}
	//获取当前节点指针,否则到时候无法获取
	Node* next_free = temp->link;
	//调整指针位置
	temp->link = temp->link->link;

	free(next_free);
}

2.2.2.2 删除整个链表

删除整个链表就很简单了,逐个访问下一个节点,并删除当前节点即可。

//单链表的整表删除
void sll_deleteLinkedList(Node** head)
{
	Node* current = *head;
	Node* next;

	while (current != NULL) {
		next = current->link;
		free(current);
		current = next;
	}

	*head = NULL;
}

3 双链表

单链表的替代方案就是双链表。在一个双链表中,每个节点都包含两个指针——指向前一个节点的指针指向后一个节点的指针。这可以使我们以任何方向遍历双链表,甚至可以忽前忽后地在双链表中访问。下图展示了一个双链表。
在这里插入图片描述

3.1 在双链表中插入

3.1.1 按顺序插入

与单链表相比,双链表肯定要难一些,因为需要修改更多的指针。但其基本思想还是不变的,那就是先找需要插入的位置,然后将新节点插入。

先定义节点:

typedef struct NODE {
	struct NODE *fwd;
	struct NODE *bwd;
	int value;
}Node;

再定义插入函数,较之于单链表,双链表要复杂一些:

int dll_insert(Node *rootp, int value)
{
	Node *this;
	Node *next;
	Node *newnode;
	//查看value是否已经存在于链表中,如果是就返回
	//否则,为新值创建一个节点
	for (this = rootp; (next = this->fwd) != NULL; this = next)
	{
		if (next->value == value)
			return 0;
		if (next->value > value)
			break;
	}
	newnode = (Node *)malloc(sizeof(Node));
	if (newnode == NULL)
		return FALSE;
	newnode->value = value;
	//把新值添加到链表中
	if (next != NULL)
	{
		//情况1或2:并非位于链表尾部
		if (this != rootp)         /*情况1:并非位于链表的起始位置*/
		{
			newnode->fwd = next;
			this->fwd = newnode;
			newnode->bwd = this;
			next->bwd = newnode;
		}
		else                       /*情况2:位于链表的起始位置*/
		{
			newnode->fwd = next;
			rootp->fwd = newnode;
			newnode->bwd = NULL;
			next->bwd = newnode;
		}
	}
	else
	{
		//情况3或4:位于链表的尾部
		if (this != rootp)
		{
			newnode->fwd = NULL;
			this->fwd = newnode;
			newnode->bwd = this;
			rootp->bwd = newnode;
		}
		else
		{
			newnode->fwd = NULL;
			rootp->fwd = newnode;
			newnode->bwd = NULL;
			rootp->bwd = newnode;
		}
	}
	return TRUE;
}

最后是主函数的相关程序:

#include <stdlib.h>
#include <stdio.h>
#include "dll_node.h"
int main()
{
	Node* head = NULL;
	// 创建链表(只有头节点,值为0)
	head = dll_createLinkedList(0);

	//插入节点
	dll_insert(head, 5);
	dll_insert(head, 10);
	dll_insert(head, 15);
	dll_insert(head, 20);

	// 删除指定位置的节点
	dll_deleteNode(&head, 3);

	// 打印链表
	dll_printLinkedList(head);

	// 删除整个链表
	dll_deleteLinkedList(&head);
	system("pause");
	return 0;
}

参考单链表,多链表的程序便不难看懂。

另外还有链表的打印函数:

void dll_printLinkedList(Node* head) {
	Node* temp = head;
	while (temp != NULL) {
		printf("%d ", temp->value);
		if (temp->fwd != NULL)
			printf("->");
		temp = temp->fwd;
	}
}

打印输出:
在这里插入图片描述
可以看到,在创建完链表之后,再插入需要的值,最后再将第三个值为10的节删除,就变成了上面的样子。

3.1.2 在指定位置插入节点(补充)

当然,如果想在指定位置插入节点也是可以的:

void dll_insertNode(Node** head, int data, int position) {
	if (*head == NULL || position <= 1) {
		// 如果链表为空或插入位置为头部,则直接在头部插入节点
		Node* newNode = createLinkedList(data);
		newNode->fwd = *head;
		if (*head != NULL) {
			(*head)->bwd = newNode;
		}
		*head = newNode;
	}
	else {
		// 否则在指定位置插入节点
		Node* temp = *head;
		int currentPosition = 1;

		while (currentPosition < position - 1 && temp->fwd != NULL) {
			temp = temp->fwd;
			currentPosition++;
		}

		if (currentPosition < position - 1) {
			printf("插入位置超出链表长度\n");
			return;
		}

		Node* newNode = createLinkedList(data);
		newNode->bwd = temp;
		newNode->fwd = temp->fwd;

		if (temp->fwd != NULL) {
			temp->fwd->bwd = newNode;
		}

		temp->fwd = newNode;
	}
}

3.2 其他链表操作

3.2.1 双链表的创建

与单链表类似,双链表也有相同的操作,比如整个链表的创建删除

链表的创建程序:

Node* dll_createLinkedList(int data) 
{
	Node* newNode = (Node*)malloc(sizeof(Node));
	if (newNode == NULL) {
		printf("内存分配失败\n");
		exit(1);
	}
	newNode->value = data;
	newNode->bwd = NULL;
	newNode->fwd = NULL;
	return newNode;
}

程序比较好理解,分配一个节点的内存,并得到指向该节点的指针,赋初值,前后均无节点连接。

3.2.2 双链表的删除

3.2.2.1 删除指定位置的节点

对于双链表来说,由于一个节点同时涉及到两个指针,因此删除指定位置的节点相对来说比较难,具体如下。

// 删除指定位置的节点
void dll_deleteNode(Node** head, int position)
{
	if (*head == NULL) {
		return;
	}

	Node* temp = *head;
	int currentPosition = 1;

	while (currentPosition < position && temp != NULL) {
		temp = temp->fwd;
		currentPosition++;
	}

	if (currentPosition < position || temp == NULL) {
		printf("删除位置超出链表长度\n");
		return;
	}

	if (temp == *head) {
		*head = temp->fwd;
	}

	if (temp->bwd != NULL) {
		temp->bwd->fwd = temp->fwd;
	}

	if (temp->fwd != NULL) {
		temp->fwd->bwd = temp->bwd;
	}

	free(temp);
}

其中最重要的只有4行:

	if (temp->bwd != NULL) {
		temp->bwd->fwd = temp->fwd;
	}

	if (temp->fwd != NULL) {
		temp->fwd->bwd = temp->bwd;
	}

直接让需要删除的节点的前后节点可相互访问,然后再删除当前节点,删除结束。

3.2.2.2 删除整个链表

双链表的删除程序:

void deleteLinkedList(Node** head) 
{
	Node* current = *head;
	Node* next;

	while (current != NULL) {
		next = current->fwd;
		free(current);
		current = next;
	}

	*head = NULL;
}

删除函数也比较好理解,先找到下一个节点,再将当前节点内存释放。这个顺序是不能反的,否则释放完之后,无法获取下一个节点的指针,将导致删除失败。

在此程序中并不需要关注bwd指针,当我们释放当前节点内存后,其bwd指针自然也就没有了意义,存储该指针的内存被释放,可以存储其他信息。

4 总结

单链表是一种使用指针来存储值的数据结构。链表中的每个节点包含一个字段,用于指向链表的下一个节点。

双链表中的每个节点包含两个link字段:其中一个指向链表的下一个节点,另一个指向链表的前一个节点。

除此之外,常见的还有循环链表等数据结构,不过这些都不是很常见。掌握了链表的创建,删除,插入,查找方法(删除不难),就基本上掌握了本章的全部内容。

书中给的链表插入操作是按照顺序插入,而在很多实际案例中,都是按照预先规定的位置插入节点的。这里会有一些差别。而在本文中,两种方法都分别进行了阐述。

---------------------------------------end---------------------------------------

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

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

相关文章

需求吞吐效能提升 65%!这家世界500强如何加速业务转型?

昨日&#xff0c;ONES 受邀参加由中关村智联软件服务业质量创新联盟主办的 TiD 2023 质量竞争力大会。会上&#xff0c;ONES 研发效能改进资深咨询顾问董晓红&#xff0c;发表了主题为《解码大型集团企业研发效能提升关键策略》的演讲。 董晓红在研发管理、研发工具链集成、敏捷…

销售管理七要,阿里生存下来的秘诀

企业销售管理“七要”&#xff1a;阿里巴巴生存下来的秘诀 纯纯的干货&#xff0c;有方法有实践 阿里巴巴靠B2B诚信通生存下来的 叫“中供铁军”&#xff0c;很多身影在O2O大战中出现 趣讲大白话&#xff1a;功夫深&#xff0c;铁棒磨成针 【趣讲信息科技260期】 *************…

LVS负载均衡DR(直接路由)模式

在LVS&#xff08;Linux Virtual Server&#xff09;负载均衡中的DR&#xff08;Direct Routing&#xff09;模式下&#xff0c;数据包的流向如下&#xff1a; 客户端发送请求到负载均衡器&#xff08;LVS&#xff09;的虚拟IP&#xff08;VIP&#xff09;。负载均衡器&#x…

SpringBoot | RestTemplate异常处理器ErrorHandler使用详解

关注wx&#xff1a;CodingTechWork 引言 在代码开发过程中&#xff0c;发现很多地方通过RestTemplate调用了第三方接口&#xff0c;而第三方接口需要根据某些状态码或者异常进行重试调用&#xff0c;此时&#xff0c;要么在每个调用的地方进行异常捕获&#xff0c;然后重试&am…

企业文件数据防泄密软件——「天锐绿盾」透明加密保护防泄密管理软件系统

摘要&#xff1a;本文介绍了一款名为「天锐绿盾」的公司文件加密软件&#xff0c;该软件旨在保护公司重要文件的安全性。通过对软件进行详细分析和测试&#xff0c;我们发现「天锐绿盾」具有强大的加密功能、简便的操作界面和高度的兼容性。本文将详细介绍软件的特点、优势及其…

Mysql存储引擎中InnoDB与Myisam的主要区别

在mysql命令窗口中,输入show engins,可以看到mysql的所有引擎,那么这么多的引擎,我们经常使用到的也就两种,MyISAM和InnoDB,这两种引擎究竟有什么区别呢? 1, 事务处理 innodb 支持事务功能,myisam 不支持。 Myisam 的执行速度更快,性能更好。 2,select ,update ,inse…

Shell 编程基础01

0:目录 1.创建新的虚拟机项目 2.linux常见命令和配置时间同步器 3.文件属性 4.if for while和方法 1.创建新的虚拟机项目 默认下一步到虚拟机命名 默认下一步设置磁盘大小 自定义硬件 删除打印机设置映像地址 启动虚拟机 选择 install centOS 7 选择英文 设置时…

[Flash CS6]使用AIR拓展屏幕

虽然目前没多少人使用Flash了&#xff0c;但还是记录一下 一、工具 Adobe Flash CS6 AIR3.2 For Desktop&#xff08;Flash CS6 自带的&#xff09; 二、设置步骤 1.将舞台改为想要拓展的屏幕尺寸大小&#xff08;以下以3840x1080位例子&#xff09; 2.打开AIR 3.2 for Desk…

鸽王-稚晖君,“远征”A1启程

看到这篇文章的人&#xff0c;想必对野生钢铁侠-稚晖君&#xff0c;都有所了解。作为华为的天才少年&#xff0c;获得了很多的荣誉&#xff0c;作为B站有名的鸽王&#xff0c;在沉浮一段时间后终于要带着新的东西和大家见面了。动态-哔哩哔哩https://b23.tv/Jv7tIjg 众所周知&a…

TopSolid安装步骤

安装TopSolid&#xff0c;选择要安装的功能&#xff0c;一般只安装Design即可&#xff0c;然后在“工具”选项卡一般只选择图示的两个就可以了 使用管理员权限运行文件 “TopSolid2021\Setup\Redist\Sentinel RMS License Manager\Tools\WlmAdmin.exe” 按图示操作&#xff1…

720全景虚拟三维数字展馆丰富了营销体验

传统的展览形式往往受到场地和空间的限制&#xff0c;展品数量和种类有限。而3D数字展厅突破了时空的束缚&#xff0c;企业可以将更多的产品、服务和文化元素以数字化形式展示&#xff0c;无需考虑展览面积和运输成本&#xff0c;大大提升了展览的灵活性和内容丰富度。数字化虚…

软件架构生态化-多角色交付的探索实践

作为一个技术架构师&#xff0c;不仅仅要紧跟行业技术趋势&#xff0c;还要结合研发团队现状及痛点&#xff0c;探索新的交付方案。在日常中&#xff0c;你是否遇到如下问题 “ 业务需求排期长研发是瓶颈&#xff1b;非研发角色感受不到研发技改提效的变化&#xff1b;引入ISV …

# 深入理解高并发编程(一)

深入理解高并发编程&#xff08;一&#xff09; 文章目录 深入理解高并发编程&#xff08;一&#xff09;SimpleDateFormat线程安全问题重现问题线程不安全的原因解决办法局部变量synchronized锁Lock锁ThreadLocalDateTimeFormatter Thread源码解读Thread类定义线程的状态定义r…

基于随机数据重置系统时钟的Windows功能正在造成严重破坏

Windows安全时间播种会将时钟重置为偏离正确时间几个月或几年 几个月前&#xff0c;挪威数据中心的一名工程师遇到了一些令人困惑的错误&#xff0c;导致Windows服务器突然将其系统时钟重置为未来55天。该工程师依靠服务器来维护一个路由表&#xff0c;当手机号码从一个运营商…

数据传承之道:MySQL主从复制实践指南

&#x1f60a; 作者&#xff1a; 一恍过去 &#x1f496; 主页&#xff1a; https://blog.csdn.net/zhuocailing3390 &#x1f38a; 社区&#xff1a; Java技术栈交流 &#x1f389; 主题&#xff1a; 数据传承之道&#xff1a;MySQL主从复制实践指南 ⏱️ 创作时间&#…

多维时序 | MATLAB实现WOA-CNN-BiGRU-Attention多变量时间序列预测

多维时序 | MATLAB实现WOA-CNN-BiGRU-Attention多变量时间序列预测 目录 多维时序 | MATLAB实现WOA-CNN-BiGRU-Attention多变量时间序列预测预测效果基本介绍模型描述程序设计参考资料 预测效果 基本介绍 多维时序 | MATLAB实现WOA-CNN-BiGRU-Attention多变量时间序列预测 1.程…

能耗监测管理系统在产业园区中的应用分析

摘要&#xff1a;随着电信公司企业级智能化办公系统的不断迭代优化及财务辅助系统与各个业务系统之间的壁垒不断打破、融合&#xff0c;能耗监测管理系统在企业生产运行管理中&#xff0c;为实现企业能耗数据归集&#xff0c;“节能减排、降本增效”提供了系统支撑及可行性保障…

tkinter高级布局:PanedWindow和notebook

文章目录 PanedWindownotebook tkinter系列&#xff1a; GUI初步&#x1f48e;布局&#x1f48e;绑定变量&#x1f48e;绑定事件&#x1f48e;消息框&#x1f48e;文件对话框Frame控件&#x1f48e;PanedWindow和notebook控件扫雷小游戏&#x1f48e;强行表白神器 tkinter传统…

小航助学GESP_C++一级模拟测试卷第2套(含题库答题软件账号)

需要在线模拟训练的题库账号请点击 小航助学编程在线模拟试卷系统&#xff08;含题库答题软件账号&#xff09;_程序猿下山的博客-CSDN博客 需要在线模拟训练的题库账号请点击 小航助学编程在线模拟试卷系统&#xff08;含题库答题软件账号&#xff09;_程序猿下山的博客-CSD…