当前位置: 首页 > news >正文

2.单链表练习

1. 链表的基本概念

链表(Linked List)是一种常见的数据结构,用于存储一系列元素,这些元素可以是任意类型的数据。链表中的每个元素被称为节点(Node),每个节点包含两部分:一个存储数据的值(或称为数据域),以及一个指向下一个节点的引用(或称为指针或链接)。

链表与数组相比,具有一些优势和不同之处:

  1. 动态性: 链表的大小可以在运行时动态地改变,而数组的大小通常是静态的。

  2. 插入和删除: 在链表中插入或删除元素相对容易,只需修改节点的引用,不需要像数组一样移动大量元素。

  3. 空间利用: 链表可以有效地利用内存,因为每个节点只需存储自身的值和下一个节点的引用,而数组可能需要一块连续的内存空间。

链表有几种常见的类型,其中最常见的是单向链表和双向链表:

  1. 单向链表(Singly Linked List): 每个节点包含一个数据域和一个指向下一个节点的引用。链表的首节点称为头节点,链表的尾节点的下一个节点引用为空。

  2. 双向链表(Doubly Linked List): 每个节点包含一个数据域和两个引用,分别指向前一个节点和后一个节点。这使得在链表中可以双向遍历。

基本操作:

  1. 插入操作: 插入节点涉及创建一个新的节点,并将其插入到合适的位置。对于单向链表,需要修改前一个节点的引用,对于双向链表,还需要修改后一个节点的前向引用。

  2. 删除操作: 删除节点涉及将待删除节点的前一个节点的引用指向待删除节点的下一个节点。同样,对于双向链表,还需要修改后一个节点的前向引用。

  3. 搜索操作: 从头节点开始,按顺序遍历链表,查找特定值的节点。

  4. 遍历操作: 从头节点开始,按顺序访问链表的每个节点,执行所需的操作。

链表在许多编程场景中都有用途,例如实现栈、队列、缓存等数据结构,也常用于解决某些特定的问题,如链表反转、寻找中间节点等。然而,需要注意链表的操作可能比数组稍微复杂,因为需要更多的指针操作。

2. 题目中的结构体

struct ListNode {int val;ListNode* next;ListNode() : val(0), next(nullptr) {}ListNode(int x) : val(x), next(nullptr) {}ListNode(int x, ListNode* next) : val(x), next(next) {}
};

ListNode* next;是单链表特性,除了储存value以外还都储存了next,指向下一个节点的指针。

这段代码定义了一个名为 ListNode 的结构体,用于表示链表中的节点。这个结构体有三个不同的构造函数,以便于创建节点对象:

  1. 默认构造函数:
ListNode() : val(0), next(nullptr) {}

这个构造函数会创建一个值为0、下一个节点为空的链表节点。

  1. 带有一个整数参数的构造函数:
ListNode(int x) : val(x), next(nullptr) {}

这个构造函数会创建一个具有给定整数值 x、下一个节点为空的链表节点。

  1. 带有两个参数的构造函数:
ListNode(int x, ListNode* next) : val(x), next(next) {}

这个构造函数会创建一个具有给定整数值 x 和指向下一个节点的指针 next 的链表节点。

在这个结构体中,val 成员变量用于存储节点的值,而 next 成员变量是一个指向下一个节点的指针。这样,我们就可以通过创建多个 ListNode 对象,并使用 next 指针将它们链接在一起,从而形成一个链表。

这个结构体的定义允许我们在链表操作中方便地创建和操作节点,以实现链表的常见操作,如插入、删除和遍历。

2. 移除链表元素

2.1 以案例来学习链表,普通的删除方法

复习指针

1. delete ptr不是删除指针, 而是释放指针指向的内存

这里因为tmp和head都指向了同一内存空间,所以delete tmp就是释放之前的head, 因为我们要对head进行一个变动

        while (head != NULL && head->val == val) { // 注意这里不是ifListNode* tmp = head;head = head->next;delete tmp;}

2. 比较容易的错误写法

在删除非头结点的时候容易犯这个错误, 因为是链表,写的时候就很容易参照链表的delete函数的写法,就是直接给一个tmp, 然后cur->next, 然后就直接删除,其实这样是不对的,因为链表删掉了中间节点还要考虑前后的加在一起

        // 删除非头结点的节点ListNode *cur = head; // 当前指针是headwhile (cur != NULL){if (cur->val == val){ListNode* tmp = cur; // tmp, head指向同一内存空间cur = cur->next; // 头结点指针指向原来链表的第二个节点delete tmp;}
#include <iostream>struct ListNode {int val;ListNode* next;ListNode() : val(0), next(nullptr) {}ListNode(int x) : val(x), next(nullptr) {}ListNode(int x, ListNode* next) : val(x), next(next) {}
};class Solution {
public:ListNode* removeElements(ListNode* head, int val) {// 删除头节点,同时也判断新的头结点是否需要删除while (head != NULL && head->val == val){ListNode *tmp = head;   // 这里让tmp指向需要被移除头结点同一地址head = head->next;      // 这里头结点的指针指向下一个节点delete tmp;             // 移除tmp, 同样也释放掉需要操作头结点的指针}// 删除非头结点的节点,这里不只是简单的删除,还要考虑把前一个后一个链接起来/*ex: 1->2->3->2->5, 处理2, 因为这里处理的是非头节点,在1位置链接3处理2,在3链接5处理2如果遍历的时候在2处理2,还要写一个prev指针如果5要处理,就在前一个地方(2)处理,5的next是nullptr, (2)链接5就行了*/ListNode *cur = head; // 这里已经考虑完了头结点, 所以这个头结点不用操作while (cur != NULL && cur->next != NULL){ // 遍历链表的基本手法if (cur->next->val == val){    ListNode *tmp = cur->next;cur->next = cur->next->next;delete tmp;}else {cur = cur->next;}}return head;}
};// 辅助函数:打印链表
void printList(ListNode* head) {ListNode* current = head;while (current != nullptr) {std::cout << current->val << " ";current = current->next;}std::cout << std::endl;
}// 这个地方跟上面删除非头结点最大的区别就是不用维护这个代码
void deleteList(ListNode* head) {ListNode* current = head;while (current != nullptr) {ListNode* tmp = current;current = current->next;delete tmp;}
}int main() {Solution solution;// 示例 1ListNode* head1 = new ListNode(1);head1->next = new ListNode(2);head1->next->next = new ListNode(6);head1->next->next->next = new ListNode(3);head1->next->next->next->next = new ListNode(4);head1->next->next->next->next->next = new ListNode(5);head1->next->next->next->next->next->next = new ListNode(6);int val1 = 6;ListNode* newHead1 = solution.removeElements(head1, val1);std::cout << "Case 1: ";printList(newHead1);// 示例 2ListNode* head2 = nullptr;int val2 = 1;ListNode* newHead2 = solution.removeElements(head2, val2);std::cout << "Case 2: ";printList(newHead2);// 示例 3ListNode* head3 = new ListNode(7);ListNode* current3 = head3;for (int val : {7, 7, 7}){current3->next = new ListNode(val);current3 = current3->next;}std::cout << "Oringin case 3: ";printList(head3);int val3 = 7;ListNode* newHead3 = solution.removeElements(head3, val3);std::cout << "Case 3: ";printList(newHead3);// 释放节点内存deleteList(newHead1);deleteList(newHead2);deleteList(newHead3);return 0;
}

2.2 虚拟头结点写法

这样就只用考虑处理非头结点的情况就好了, 注意这里实例化了之后要用引用,这里也是复习一下实例化和在堆上生成内存返回指针,下面有两种写法

栈上的内存(Stack):当你创建一个局部变量或对象(例如ListNode dummy(0);)时,这个对象是在栈上分配内存的。这些对象的生命周期是确定的,当它们所在的作用域结束时,它们会自动被销毁,内存会被释放。

堆上的内存(Heap):当你使用new关键字(例如ListNode* dummy = new ListNode(0);)时,对象是在堆上分配内存的。这些对象的生命周期是不确定的,你需要显式地使用delete来释放内存。

使用指针的一个主要优点是,它允许你在运行时动态地创建和销毁对象。这给了你更大的灵活性,但代价是你需要更仔细地管理内存。

实例化虚拟头节点

class Solution {
public:ListNode* removeElements(ListNode* head, int val) {ListNode dummy(0);dummy.next = head;// 处理非头节点ListNode *cur = &dummy; // 这里已经考虑完了头结点, 所以这个头结点不用操作while (cur != NULL && cur->next != NULL){ // 遍历链表的基本手法if (cur->next->val == val){    ListNode *tmp = cur->next;cur->next = cur->next->next;delete tmp;}else {cur = cur->next;}}return dummy.next;}
};

指针虚拟头结点

class Solution {
public:ListNode* removeElements(ListNode* head, int val) {ListNode *dummy = new ListNode(999);// 处理非头节点ListNode *cur = dummy; // 这里已经考虑完了头结点, 所以这个头结点不用操作while (cur != NULL && cur->next != NULL){ // 遍历链表的基本手法if (cur->next->val == val){    ListNode *tmp = cur->next;cur->next = cur->next->next;delete tmp;}else {cur = cur->next;}}return dummy->next;}
};

3. 设计一个链表

通过完整的设计一个链表理解链表是怎么遍历的,新的节点怎么在各个地方链接起来

#include <iostream>class MyLinkedList {
public:struct ListNode   // 先创建节点的结构体, val, next, 以及ListNode的构造函数{int val;ListNode *next;ListNode(int x) : val(x), next(nullptr) {}};private:ListNode *head;    // 指向头结点的指针int size;          // 链表的尺寸public:MyLinkedList() {head = NULL;size = 0;}int get(int index) {// 这个就是取出索引的值if (index < 0 || index >= size)return -1;ListNode *cur = head;for (int i = 0; i < index; i++){cur = cur->next;}return cur->val;}void addAtHead(int val) {/*1 -> 2 -> 3 -> 4   添加valval -> 1 -> 2 -> 3 -> 4  */ListNode *new_node = new ListNode(val); // 定义一个新的节点new_node->next = head;  // val链接头结点head = new_node;        // 更新头结点size++;                 }void addAtTail(int val) {/*1 -> 2 -> 3 -> 4   添加val1 -> 2 -> 3 -> 4 -> val*/// 如果刚初始化也可以使用这个接口if (size == 0){addAtHead(val);return;}ListNode *cur = head; // 当前指针位置,从head开始while (cur->next != NULL){ // 这里遍历到最后一个节点cur = cur->next;}ListNode *new_node = new ListNode(val); // 定义指向新的节点的指针cur->next = new_node;                   // 链接起来size++; }void addAtIndex(int index, int val) {if (index < 0 || index > size)return;if (index == 0) {addAtHead(val);return;}ListNode *cur = head;for (int i = 0; i < index - 1; i++){cur = cur->next;}// 现在来到要加的前一个ListNode *new_node = new ListNode(val);new_node->next = cur->next;cur->next = new_node;size++;}void deleteAtIndex(int index) {if (index < 0 || index >= size) return;ListNode *cur = head;if (index == 0) {head = head->next;delete cur;--size;return;}for (int i = 0; i < index - 1; i++){cur = cur->next;}// 现在来到要删除的前一个ListNode *tmp = cur->next;cur->next = cur->next->next;delete tmp;size--;}
};int main() {MyLinkedList *myLinkedList = new MyLinkedList();myLinkedList->addAtHead(1);myLinkedList->addAtTail(3);myLinkedList->addAtIndex(1, 2);std::cout << myLinkedList->get(1) << std::endl;myLinkedList->deleteAtIndex(1);std::cout << myLinkedList->get(1) << std::endl;delete myLinkedList;return 0;
}

4. 反转链表

4.1 双指针法

class Solution {
public:ListNode* reverseList(ListNode* head) {ListNode *cur = head; // 定义当前位置指针ListNode *prev = NULL; // 定义当前位置指针while (cur != NULL){ListNode *next_node = cur->next;cur->next = prev;     // 这里链接起来           prev = cur;           // prev上一位cur = next_node;      // cur上一位}return prev;}
};

递归的写法,复习一下递归

class Solution {
public:ListNode* reverse(ListNode* prev, ListNode* cur){if (cur == NULL)return prev;ListNode *next_node = cur->next;cur->next = prev;return reverse(cur, next_node);}public:ListNode* reverseList(ListNode* head) {return reverse(NULL, head);}
};

完整的代码

#include <iostream>struct ListNode {int val;ListNode *next;ListNode() : val(0), next(nullptr) {}ListNode(int x) : val(x), next(nullptr) {}ListNode(int x, ListNode *next) : val(x), next(next) {}
};class Solution {
public:ListNode* reverse(ListNode* prev, ListNode* cur){if (cur == NULL)return prev;ListNode *next_node = cur->next;cur->next = prev;return reverse(cur, next_node);}public:ListNode* reverseList(ListNode* head) {return reverse(NULL, head);}
};// 辅助函数:打印链表
void printList(ListNode* head) {ListNode* current = head;while (current != nullptr) {std::cout << current->val << " ";current = current->next;}std::cout << std::endl;
}int main() {Solution solution;// 示例ListNode* head = new ListNode(1);head->next = new ListNode(2);head->next->next = new ListNode(3);head->next->next->next = new ListNode(4);std::cout << "Original List: ";printList(head);ListNode* newHead = solution.reverseList(head);std::cout << "Reversed List: ";printList(newHead);return 0;
}

相关文章:

2.单链表练习

1. 链表的基本概念 链表&#xff08;Linked List&#xff09;是一种常见的数据结构&#xff0c;用于存储一系列元素&#xff0c;这些元素可以是任意类型的数据。链表中的每个元素被称为节点&#xff08;Node&#xff09;&#xff0c;每个节点包含两部分&#xff1a;一个存储数…...

Wordpress 安装插件和主题报错

安装主题和插件的时候&#xff0c;就是这个恶心的报错&#xff0c; Wordpress plugin install: Could not create directory 这是权限惹的祸&#xff0c;如下一顿操作猛如虎&#xff0c;就解决了。 sudo chown -R www:www wp-content/themes sudo chown -R www:www wp-conte…...

Spring Cloud 2022.x版本使用gateway和nacos实现动态路由和负载均衡

文章目录 1、nacos下载安装1.1、启动服务器1.2、关闭服务器1.3、服务注册&发现和配置管理接口 2、代码示例2.1、app1工程代码2.2、app2工程代码2.3、gateway网关工程代码 3、动态配置网关路由3.1、配置动态路由3.2、配置为负载模式 4、gateway配置规则4.1、请求转发&#x…...

CSS中如何隐藏元素但保留其占位空间(display:none vs visibility:hidden)?

聚沙成塔每天进步一点点 ⭐ 专栏简介⭐ display: none;⭐ visibility: hidden;⭐ 如何选择⭐ 写在最后 ⭐ 专栏简介 前端入门之旅&#xff1a;探索Web开发的奇妙世界 记得点击上方或者右侧链接订阅本专栏哦 几何带你启航前端之旅 欢迎来到前端入门之旅&#xff01;这个专栏是为…...

无涯教程-机器学习 - 数据可视化

在上一章中&#xff0c;无涯教程讨论了数据对于机器学习算法的重要性&#xff0c;以了解具有统计信息的数据&#xff0c;还有另一种称为可视化的方式来理解数据。 借助数据可视化&#xff0c;可以看到数据的属性保持什么样的关联&#xff0c;这是查看要素是否与输出相对应的最…...

springboot设置日志输出级别

一、日志等级 trace&#xff1a;最低等级 debug&#xff1a;调试用&#xff0c;通常用于跟踪程序进展 info: 记录用&#xff0c;通常用于记录程序行为 warn&#xff1a;警告 error&#xff1a;错误 fatal&#xff1a;灾难性错误&#xff0c;最高等级 配置application.yml 实现…...

buildAdmin的使用笔记

安装buildAdmin 下载完整包&#xff0c;解压进入 buildadmin 的文件夹&#xff0c; 输入命令 composer install 启动的时候使用&#xff0c; php think run 就可以了 为什么启动只需要&#xff0c; php think run 这种启动方式&#xff0c; 我是头一回看见 &#xff0c;后来才…...

RealVNC配置自定义分辨率(AlmaLinux 8)

RealVNC 配置自定义分辨率&#xff08;AlmaLinux8&#xff09; 参考RealVNC官网 how to set up resolution https://help.realvnc.com/hc/en-us/articles/360016058212-How-do-I-adjust-the-screen-resolution-of-a-virtual-desktop-under-Linux-#standard-dummy-driver-0-2 …...

LA@特征值和特征向量的性质

文章目录 方阵特征值和特征向量的性质&#x1f47a;特征值之和特征值之积推论:特征值判定方阵的可逆性 证明小结 导出性质可逆矩阵的特征值性质转置矩阵和特征值矩阵多项式的特征值不同特征值的特征向量线性无关定理推论推广 特征向量线性组合特征值的重数性质 方阵特征值和特征…...

Springboot使用kafka事务-生产者方

前言 在上一篇文章中&#xff0c;我们使用了springboot的AOP功能实现了kafka的分布式事务&#xff0c;但是那样实现的kafka事务是不完美的&#xff0c;因为请求进来之后分配的是不同线程&#xff0c;但不同线程使用的kafka事务却是同一个&#xff0c;这样会造成多请求情况下的…...

您的计算机已被.halo勒索病毒感染?恢复您的数据的方法在这里!

导言&#xff1a; 在当今数字时代&#xff0c;网络安全已经成为了我们生活和工作中不可或缺的一部分。然而&#xff0c; .Halo 勒索病毒的出现&#xff0c;使网络威胁变得更加真切和具体。本文91数据恢复将深入介绍 .Halo 勒索病毒的危害&#xff0c;详细探讨如何高效地恢复被其…...

生成式AI颠覆传统数据库的十种方式

对于生成式AI的所有闪光点&#xff0c;这个新时代最大的转变可能深埋在软件堆栈中。AI算法正在不易觉察地改变一个又一个数据库。他们正在用复杂、自适应且看似更直观的AI新功能颠覆传统数据库。 与此同时&#xff0c;数据库制造商正在改变我们存储信息的方式&#xff0c;以便…...

el-date-picker自定义只能选中当前月份和半年内月份等

需求&#xff1a;el-date-picker只能选中当前月期和当前月期往前半年&#xff0c;其他时间就禁用了不让选择了&#xff0c;因为没数据哈哈。当然也可以选择往前一年等。 一、效果 二、写个日期选择器 :picker-options&#xff1a;日期选项 value-format&#xff1a;选择后的格…...

Pyecharts教程(十一):使用Pyecharts绘制带有滑动数据缩放功能的K线图

Pyecharts教程(十一):使用Pyecharts绘制带有滑动数据缩放功能的K线图 作者:安静到无声 个人主页 目录 Pyecharts教程(十一):使用Pyecharts绘制带有滑动数据缩放功能的K线图前言步骤总结推荐专栏前言 K线图是金融市场分析中常见的图表类型之一,它能够直观地展示价格的变化…...

2023年高教社杯数学建模思路 - 案例:ID3-决策树分类算法

文章目录 0 赛题思路1 算法介绍2 FP树表示法3 构建FP树4 实现代码 建模资料 0 赛题思路 &#xff08;赛题出来以后第一时间在CSDN分享&#xff09; https://blog.csdn.net/dc_sinor?typeblog 1 算法介绍 FP-Tree算法全称是FrequentPattern Tree算法&#xff0c;就是频繁模…...

POJ 3273 Monthly Expense 二分

我们对每个月花费的最小花费进行二分&#xff0c;对于每一次二分的值mid&#xff0c;计算能花的月份数量&#xff0c;如果月份数量小于等于m&#xff0c;我们就不断的缩小mid&#xff0c;直到找到月份数量小于等于m 与 月份数量大于m的临界值&#xff0c;取最后一次满足条件的m…...

图论(基础)

知识&#xff1a; 顶点&#xff0c;边 | 权&#xff0c;度数 1.图的种类&#xff1a; 有向图 | 无向图 有环 | 无环 联通性 基础1&#xff1a;图的存储&#xff08;主要是邻接矩阵和邻接表&#xff09; 例一&#xff1a;B3643 图的存储 - 洛谷 | 计算机科学教育新生态 (…...

docker的运行原理

Docker 是一个开源的容器化技术,它能够让开发者将应用及其依赖打包到一个轻量级的、可移植的容器中,这个容器可以在几乎任何机器上一致地运行。要了解 Docker 的运行原理,我们首先要理解以下几个核心概念: 容器 (Container): 容器是一个轻量级的、独立的、可执行的软件包,…...

vue自定义键盘

<template><div class"mark" click"isOver"></div><div class"mycar"><div class"mycar_list"><div class"mycar_list_con"><p class"mycar_list_p">车牌号</p>…...

k8s 安装 kubernetes安装教程 虚拟机安装k8s centos7安装k8s kuberadmin安装k8s k8s工具安装 k8s安装前配置参数

k8s采用master, node1, node2 。三台虚拟机安装的一主两从&#xff0c;机器已提前安装好docker。下面是机器配置&#xff0c;k8s安装过程&#xff0c;以及出现的问题与解决方法 虚拟机全部采用静态ip, master 30机器, node1 31机器, node2 32机器 机器ip 192.168.164.30 # ma…...

2023年高教社杯数学建模思路 - 案例:感知机原理剖析及实现

文章目录 1 感知机的直观理解2 感知机的数学角度3 代码实现 4 建模资料 # 0 赛题思路 &#xff08;赛题出来以后第一时间在CSDN分享&#xff09; https://blog.csdn.net/dc_sinor?typeblog 1 感知机的直观理解 感知机应该属于机器学习算法中最简单的一种算法&#xff0c;其…...

OTFS-ISAC雷达部分最新进展(含matlab仿真+USRP验证)

OTFS基带参数设置 我将使用带宽为80MHz的OTFS波形进行设计&#xff0c;对应参数如下&#xff1a; matlab Tx仿真 Tx导频Tx功率密度谱 帧结构我使用的是经典嵌入导频帧结构&#xff0c;Tx信号波形的带宽从右图可以看出约为80Mhz USRP验证 测试环境 无人机位于1m处 Rx导频Rx…...

Cell | 超深度宏基因组!复原消失的肠道微生物

期刊&#xff1a;Cell IF&#xff1a;64.5 (Q1) 发表时间&#xff1a;2023.6 研究背景 不同的生活方式会影响微生物组组成&#xff0c;但目前微生物组的研究严重偏向于西方工业化人群&#xff0c;其中工业化人群的特点是微生物群多样性较低。为了理解工…...

Centos7 设置代理方法

针对上面变量的设置方法&#xff1a; 1、在/etc/profile文件 2、在~/.bashrc 3、在~/.zshrc 4、在/etc/profile.d/文件夹下新建一个文件xxx.sh 写入如下配置&#xff1a; export proxy"http://192.168.5.14:8118" export http_proxy$proxy export https_proxy$pro…...

Android versions (Android 版本)

Android versions (Android 版本) All Android releases https://developer.android.com/about/versions Android 1.0 G1 Android 1.5 Cupcake Android 1.6 Donut Android 2.0 Eclair Android 2.2 Froyo Android 2.3 Gingerbread Android 3.0 Honeycomb Android 4.0 Ic…...

LNMP 平台搭建(四十)

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 目录 前言 搭建LNMP 一、安装Nginx 二、安装Mysql 三、安装PHP 四、部署应用 前言 LNMP平台指的是将Linux、Nginx、MySQL和PHP&#xff08;或者其他的编程语言&#xff0c;如…...

pcie 6.0/7.0相对pcie 5.0的变化有哪些?

引言 话说&#xff0c;小编在CSDN博客跟客服机器人聊天&#xff0c;突然看到有个搜索热搜“pcie最全科普贴”。小编有点似曾相识呀&#xff0c;我就好奇点击了一下&#xff0c;没想到几年前写的帖子在CSDN又火了一把。 说到这里&#xff0c;顺带给自己打个广告哈&#xff5e; …...

百度Apollo:自动驾驶技术的未来应用之路

文章目录 前言一、城市交通二、出行体验三、环境保护四、未来前景总结 前言 随着科技的不断进步&#xff0c;自动驾驶技术正逐渐成为现实&#xff0c;颠覆着我们的出行方式。作为中国领先的自动驾驶平台&#xff0c;百度Apollo以其卓越的技术和开放的合作精神&#xff0c;正在…...

C++之std::distance应用实例(一百八十八)

简介&#xff1a; CSDN博客专家&#xff0c;专注Android/Linux系统&#xff0c;分享多mic语音方案、音视频、编解码等技术&#xff0c;与大家一起成长&#xff01; 优质专栏&#xff1a;Audio工程师进阶系列【原创干货持续更新中……】&#x1f680; 人生格言&#xff1a; 人生…...

中国建筑出版传媒许少辉八一新书乡村振兴战略下传统村落文化旅游设计日

中国建筑出版传媒许少辉八一新书乡村振兴战略下传统村落文化旅游设计日...