【Go 数据结构】树与哈希表的具体实现

树是一种由 n 个有限节点组成的具有层次关系的集合。

树的定义:

  1. 节点之间有层次关系,分为父节点和子节点
  2. 有唯一一个的根节点,该节点没有父节点
  3. 除了根节点,每个节点有且只有一个父节点
  4. 每一个节点本身以及它的后代也是一棵树,是一个递归结构
  5. 没有后代的节点成为叶子节点,没有节点的树称为空树

二叉树:每个节点最多只有两个儿子节点的树

满二叉树:叶子节点与叶子节点之间的高度差为 0 的二叉树。即整棵树是满的。树形呈现出满三角形结构。

完全二叉树:完全二叉树是由满二叉树而引出来的。这里我们设二叉树的深度为 k,除了第 k 层以外,其他各层节点树都达到了最大值,且第 k 层所有的节点都连续集中在最左侧。

树常见的数学特征:

  1. 高度为 h 的二叉树至少 h + 1 个节点
  2. 高度为 h 的二叉树至少 2 ^ h + 1 个节点
  3. 含有 n 个节点的二叉树的高度至多为 n - 1
  4. 含有 n 个节点的二叉树的高度至少为 log n
  5. 在二叉树的第 i 层,至多有 2 ^ (i - 1) 个节点

链表实现二叉树

// TreeNode is a tree node
type TreeNode struct {
	Data  string    // data
	Left  *TreeNode // left child
	Right *TreeNode // right child
}

当然了,我们也可以使用 数组 来表示二叉树,但是一般用来表示完全二叉树

对于一棵有 n 个节点的完全二叉树,从上到下,从左到右进行序号编号,对于一个任意节点,编号 i = 0表示树根节点,编号 i 的节点的左右儿子节点编号分别是:2 * i2 * i + 1, 父节点的编号为 i / 2

树的遍历

对于一棵树的遍历,我们有如下四种遍历方法:

  • 先序遍历:先访问根节点,再访问左子树,最后访问右子树
  • 后序遍历:先访问左子树,再访问右子树,最后访问根节点
  • 中序遍历:先访问左子树,再访问根节点,最后访问右子树
  • 层序遍历:每一层从左到右地访问每一个节点

实现树的前三种遍历打印结果:

// PreOrder 先序遍历 根左右
func PreOrder(tree *Node) {
	if tree == nil {
		return
	}
	fmt.Println(tree.Data, " ")
	PreOrder(tree.Left)
	PreOrder(tree.Right)
}

// MidOrder 中序遍历 左根右
func MidOrder(tree *Node) {
	if tree == nil {
		return
	}

	MidOrder(tree.Left)
	fmt.Println(tree.Data, " ")
	MidOrder(tree.Right)
}

// PostOrder 后续遍历 左右根
func PostOrder(tree *Node) {
	if tree == nil {
		return
	}
	PostOrder(tree.Left)
	PostOrder(tree.Right)
	fmt.Println(tree.Data, " ")
}

在实现树的层序遍历的时候,我们一般会使用队列作为辅助数据结构来实现。

  1. 首先将树的树根节点放入到队列中。
  2. 从队列中 Remove 出节点,先打印节点值,如果该节点有左子树,左子树入队,如果该节点有右子树,右子树入队。
  3. 重复2,直到队列中再无其他元素。

在实现之前我们先实现一些辅助函数,此处的函数是基于我们上一次的链式队列的修改。

// LinkNode 定义链表节点
type LinkNode struct {
	Next  *LinkNode
	Value *Node
}

// LinkQueue 定义链表队列
type LinkQueue struct {
	root *LinkNode
	size int
	lock sync.Mutex
}

// Add 入队
func (q *LinkQueue) Add(v *Node) {
	q.lock.Lock()
	defer q.lock.Unlock()

	// 如果队列为空,我们将新节点作为队列的根节点
	if q.root == nil {
		q.root = new(LinkNode)
		q.root.Value = v
	} else {
		// 队列不为空,新建一个节点,采用尾插法实现
		newNode := new(LinkNode)
		newNode.Value = v

		// 找到尾节点
		nowNode := q.root
		if nowNode.Next != nil {
			nowNode = nowNode.Next
		}

		nowNode.Next = newNode
	}
	q.size++
}

// Remove 出队
func (q *LinkQueue) Remove() *Node {
	q.lock.Lock()
	defer q.lock.Unlock()

	if q.size == 0 {
		return nil
	}

	// 找到队头节点
	top := q.root
	v := top.Value

	// 将对头元素出队
	q.root = top.Next

	q.size--
	return v
}

// Size 队列大小
func (q *LinkQueue) Size() int {
	return q.size
}

接下来,实现我们的层序遍历:

// LayerOrder 层序遍历
func LayerOrder(tree *Node) {
	if tree == nil {
		return
	}

	// 借助队列实现层序遍历
	queue := new(LinkQueue)

	// 将根节点入队
	queue.Add(tree)

	// 层序遍历
	for queue.Size() > 0 {
		// 获取队列头元素
		element := queue.Remove()
		// 输出
		fmt.Println(element.Data, " ")

		// 将左右子树入队
		if element.Left != nil {
			queue.Add(element.Left)
		}

		if element.Right != nil {
			queue.Add(element.Right)
		}
	}
}

哈希表

首先,我们来理清楚些概念:

线性查找

线性查找,也被称作顺序查找,是一种非常基础且直观的查找算法。顾名思义,线性查找会按照顺序查找数据,直到找到所需要的数据为止。

线性查找的步骤如下:

  1. 从数据集合的第一个元素开始。
  2. 将当前元素与所查找的目标元素进行比较。
  3. 如果当前元素和目标元素相等,那么返回当前元素的位置,查找结束。
  4. 如果当前元素和目标元素不相等,则继续检查下一个元素。
  5. 如果已经检查完所有元素但还没有找到目标元素,那么返回一个表示“未找到”的结果。

线性查找的优势在于它不需要预先对数据进行排序,这在一些需要频繁插入和删除的场景中会非常有用。此外,对于较小的数据集,线性查找是足够有效的。

但是,对于大规模数据集,线性查找的效率并不高,因为在最坏的情况下,线性查找可能需要检查集合中的每一个元素。

散列查找

哈希查找(也称为散列查找)是一种使用哈希哈希表存储数据,通过哈希函数快速查找数据的方法。

散列查找的步骤如下:

  1. 选择一个哈希函数:哈希函数会接受一个输入(或者叫键),并返回一个整数,这个整数就是在哈希表中存放数据的位置。
  2. 创建哈希表:创建一个可以存放数据的哈希表,通常是一个数组。大小可以视实际情况而定。
  3. 插入数据:当你有一份数据需要插入哈希表时,会把这份数据的键放入哈希函数,得到一个哈希值,然后把数据存放到这个哈希值对应的位置。
  4. 查找数据:当你需要查找一份数据时,也是把这份数据的键放入哈希函数,得到一个哈希值,然后去这个哈希值对应的位置取出数据。由于哈希值的计算速度非常快,所以查找的速度也非常快。

虽然散列查找的速度很快。但是在实际应用中,还需要处理一些复杂的问题,如碰撞问题。当两个键的哈希值相同(这称为哈希碰撞),就需要有一种方法来处理,最常见的处理方法包括开放寻址法链地址法

接下来我们将已经引入开放寻址法和链地址法。

开放寻址法

开放寻址法是解决哈希冲突的一种方法。它的基本思想是如果哈希函数返回的位置已经有数据了,即发生了冲突,那么就从当前位置起,依据某种探查规则,在哈希表中找到另一个位置,直到找到一个空的位置或者达到查找上限。

常见的探查规则有以下三种:

  • 线性探查:线性探查的步骤是,如果哈希函数返回的位置已经有数据了,就顺序往下查找,直到找到一个空的位置。比如初始位置是 i ,那么就依次查找 i+1 , i+2 , i+3 …,直到找到空的位置。这种方法简单,但可能导致数据在哈希表中的分布不均匀,产生一种叫做“聚集”的现象。
  • 二次探查:二次探查的步骤是,如果哈希函数返回的位置已经有数据了,那么就按照平方的规则往下查找,直到找到一个空的位置。比如初始位置是i,那么就依次查找i+1², i+2², i+3²…,直到找到空的位置。这种方法相对于线性探查能更好地防止聚集问题。
  • 双重哈希:双重哈希的步骤是,使用一个额外的哈希函数来解决哈希冲突。比如初始哈希函数返回位置 i ,如果 i 位置已经有数据了,那么就按照另一个哈希函数的规则进行探查,直到找到一个空的位置。这种方法可以避免聚集,但需要计算额外的哈希函数,增加了一些计算复杂性。

开放寻址法的主要优点是实现简单,结构紧凑,不需要额外的链表或数组结构。缺点是可能会有较差的缓存性能,并且需要处理较复杂的删除操作。

链地址法

链地址法也叫做链式哈希,是一种用来解决哈希碰撞问题的方法。当哈希函数返回的位置已经有数据了,即发生哈希碰撞时,链地址法是将这些哈希值相同的元素,放到同一个链表中。

链地址法的步骤如下:

  • 首先,初始化哈希表,每个位置都链接到一个链表,一开始这些链表都是空的。
  • 当我们要插入一个元素时,首先计算这个元素的哈希值,然后找到对应的链表,我们把这个元素插入到链表尾部。
  • 当我们要查找一个元素时,也是首先计算这个元素的哈希值,找到对应的链表,然后在链表中进行顺序查找。

链地址法的优点是处理哈希碰撞简单,不会出现表满的情况;并且在哈希表的大小固定,且哈希值分布均匀时,查找效果较好。它的缺点是需要额外的存储空间来存放指向链表的指针,并且可能存在比较长的链表,会降低查找的效率。

哈希函数

哈希函数(Hash function)是任意长度的输入(也叫做预映射,pre-image),通过散列算法,变换成固定长度的输出,该输出就是哈希值。

哈希函数的构造规则主要基于以下几个目标:

  • 确定性:对于同一个输入,无论执行多少次哈希函数,输出的哈希值始终不变。也就是说,如果 a=b,那么 hash(a)=hash(b)
  • 快速计算:哈希函数需要能快速地计算出哈希值。给定一个输入,进行哈希运算的效率应该很高。
  • 雪崩效应:即使只是微小的输入变化,也会产生巨大的输出变化。换句话说,如果 a≠b,那么 hash(a)hash(b) 的值应该差别很大。
  • 散列均匀:哈希函数应该能保证散列值在哈希表中均匀分布,避免哈希冲突。

哈希函数在很多不同的场合都有应用,例如在数据结构中的哈希表,而在密码学中哈希函数通常用来验证数据的完整性,比如MD5,SHA1,SHA2等。

在目前计算哈希速度最快的哈希算法是 xxhash

说完了一些基础的概念,接下来我们来实现一下简单的链式哈希表。

实现链式哈希表

在介绍先介绍一个小知识点,防止大家疑惑。

我们在实现时,使用到了一个加载因子 factor 这个变量主要用来控制哈希表的扩容与缩容。

我们设定当加载因子 factor <= 0.125 时进行数组缩容,每次将容量对半砍。当加载因子 factor >= 0.75 进行数组扩容,每次将容量翻倍。

定义数据

const (
	// 扩容因子
	expandFactor = 0.75
)

// 键值对
type keyPairs struct {
	key   string
	value interface{}
	next  *keyPairs
}

// HashMap 哈希表
type HashMap struct {
	array        []*keyPairs
	len          int
	capacity     int
	capacityMask int
	lock         sync.Mutex
}

初始化

// NewHashMap 初始化哈希表
func NewHashMap(capacity int) *HashMap {
	// 默认容积为2的幂
	defaultCapacity := 1 << 4
	if capacity <= defaultCapacity {
		capacity = defaultCapacity
	} else {
		capacity = 1 << int(math.Ceil(math.Log2(float64(capacity))))
	}

	// 新建一个哈希表
	hashtable := new(HashMap)
	hashtable.capacity = capacity
	hashtable.capacityMask = capacity - 1
	return hashtable
}

获取长度

// Len 返回哈希表中键值对的个数
func (hashtable *HashMap) Len() int {
	return hashtable.len
}

计算哈希值

// value 计算哈希值
var value = func(key []byte) uint64 {
	h := xxhash.New()
	h.Write(key)
	return h.Sum64()
}

获取下标

// hashIndex 计算哈希值并获取下标
func (hashtable *HashMap) hashIndex(key string, mask int) int {
	// 计算哈希值
	hash := value([]byte(key))
	index := hash & uint64(mask)
	return int(index)
}

插入元素

// Put 插入键值对
func (hashtable *HashMap) Put(key string, value interface{}) {
	hashtable.lock.Lock()
	defer hashtable.lock.Unlock()

	// 获取下标
	index := hashtable.hashIndex(key, hashtable.capacityMask)
	// 此下标在哈希表中的值
	element := hashtable.array[index]
	if element == nil {
		// 此下标没有元素,则插入
		hashtable.array[index] = &keyPairs{
			key:   key,
			value: value,
		}
	} else {
		// 此下标已经有元素,则插入到上一个元素的后面
		var lastPairs *keyPairs

		for element != nil {
			if element.key == key {
				element.value = value
				return
			}
			lastPairs = element
			element = element.next
		}

		// 找不到元素,则插入到最后
		lastPairs.next = &keyPairs{
			key:   key,
			value: value,
		}
	}
	// 长度加一
	newLen := hashtable.len + 1

	// 计算扩容因子,如果长度大于容积的75%,则扩容
	if float64(newLen)/float64(hashtable.capacity) >= expandFactor {
		// 新建一个原来两倍大小的哈希表
		newhashtable := new(HashMap)
		newhashtable.array = make([]*keyPairs, hashtable.capacity*2)
		newhashtable.capacity = hashtable.capacity * 2
		newhashtable.capacityMask = newhashtable.capacity*2 - 1

		// 遍历原哈希表,将元素插入到新哈希表
		for _, pairs := range hashtable.array {
			for pairs != nil {
				newhashtable.Put(pairs.key, pairs.value)
				pairs = pairs.next
			}
		}

		hashtable.array = newhashtable.array
		hashtable.capacity = newhashtable.capacity
		hashtable.capacityMask = newhashtable.capacityMask
	}
	hashtable.len = newLen
}

获取元素

// Get 获取键值对
func (hashtable *HashMap) Get(key string) (value interface{}, ok bool) {
	hashtable.lock.Lock()
	defer hashtable.lock.Unlock()

	// 获取下标
	index := hashtable.hashIndex(key, hashtable.capacityMask)

	// 此下标在哈希表中的值
	element := hashtable.array[index]

	// 遍历元素,如果元素的key等于key,则返回
	for element != nil {
		if element.key == key {
			return element.value, true
		}
		element = element.next
	}
	return nil, false
}

删除元素

// Delete 删除键值对
func (hashtable *HashMap) Delete(key string) {
	hashtable.lock.Lock()
	defer hashtable.lock.Unlock()

	// 获取下标
	index := hashtable.hashIndex(key, hashtable.capacityMask)

	// 此下标在哈希表中的值
	element := hashtable.array[index]

	// 如果为空链表,则直接返回
	if element == nil {
		return
	}

	// 如果第一个元素的key等于key,则删除
	if element.key == key {
		hashtable.array[index] = element.next
		hashtable.len--
		return
	}

	// 下一个键值对
	nextElement := element.next
	for nextElement != nil {
		if nextElement.key == key {
			element.next = nextElement.next
			hashtable.len--
			return
		}
		element = nextElement
		nextElement = nextElement.next
	}
}

遍历哈希表

// Range 遍历哈希表
func (hashtable *HashMap) Range() {
	hashtable.lock.Lock()
	defer hashtable.lock.Unlock()

	for _, pairs := range hashtable.array {
		for pairs != nil {
			fmt.Println(pairs.key, pairs.value)
			pairs = pairs.next
		}
	}
	fmt.Println("len:", hashtable.len)
}

哈希表总结

哈希查找总的来说是一种用空间去换时间的查找算法,时间复杂度达到 O ( 1 ) {O(1)} O(1)级别。

总结

本次我们介绍使用Go语言实现数据结构中的树和哈希表,并且详细介绍了哈希表的具体实现。数据结构这一系列我们没有涉及到具体的细节的讲解,适合有一定数据结构基础的童鞋,本系列代码已经上传至Github,欢迎大家 Star。

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Marin说PCB之国产电源芯片方案 ---STC2620Q

随着小米加入的造车大家庭&#xff0c;让这个本来就卷的要死的造车大家庭更加卷了。随之带来的蝴蝶效应就是江湖上各个造成门派都开始了降本方案的浪潮啊&#xff0c;开始打响价格战了。各家的新能源车企也是不得不开始启动了降本方案的计划了&#xff0c;为了应对降价的浪潮。…

手游广告归因新选择:Xinstall助力精准衡量投放效果

在手游市场竞争日益激烈的今天&#xff0c;广告主们面临着如何精准衡量广告投放效果的难题。手游广告归因平台的出现&#xff0c;为广告主们提供了一种全新的解决方案。而Xinstall&#xff0c;作为其中的佼佼者&#xff0c;正以其独特的优势&#xff0c;助力广告主们破解这一难…

【AI大模型】AI大模型热门关键词解析与核心概念入门

&#x1f680; 作者 &#xff1a;“大数据小禅” &#x1f680; 文章简介 &#xff1a;本专栏后续将持续更新大模型相关文章&#xff0c;从开发到微调到应用&#xff0c;需要下载好的模型包可私。 &#x1f680; 欢迎小伙伴们 点赞&#x1f44d;、收藏⭐、留言&#x1f4ac; 目…

SPSS多元线性回归

&#xff08;要满足&#xff09;模型的假设条件需要对数据进行怎样处理&#xff1f;&#xff1f; 为了使数据满足多元线性回归的条件&#xff0c;通常需要进行以下预处理步骤&#xff1a; 1. 数据清洗&#xff1a;处理缺失值、异常值和重复值&#xff0c;确保数据质量。 2. 特…

python-oracledb 已率先支持 Oracle 23ai

python-oracledb 介绍 python-oracledb (以下简称 oracledb) 是 Python cx_Oracle 驱动程序的新名称&#xff0c;如果你仍在使用 cx_Oracle&#xff0c;建议升级到最新版本的 oracledb。 oracledb 驱动程序是一个开源模块&#xff0c;使 Python 程序能够访问 Oracle 数据库。默…

美业SaaS系统多门店收银系统源码-【卡升组合促销规则】讲解分享

美业管理系统源码 博弈美业SaaS系统 连锁多门店美业收银系统源码 多门店管理 / 会员管理 / 预约管理 / 排班管理 / 商品管理 / 促销活动 PC管理后台、手机APP、iPad APP、微信小程序 1、什么是卡升组合促销&#xff1f; 原价购买的卡项&#xff0c;卡状态正常的情况下&…

分红76.39亿,分红率再创新高,成长活力无限的伊利带来丰厚回报

伊利47万股东&#xff0c;又等来了一个好消息。 4月29日&#xff0c;伊利股份发布2023年报&#xff0c;实现营业总收入1261.79亿元&#xff0c;归母净利润104.29亿元&#xff0c;双创历史新高&#xff0c;实现连续31年稳健增长。 在递交亮眼成绩单的同时&#xff0c;乳业巨头伊…

MyBatis的其他查询操作

前言&#xff1a;在上篇博客介绍了MyBatis的一些增删改查操作&#xff0c;接下来介绍其他查询操作 目录 1 其他查询操作 1.1 多表查询 1.1.1 准备工作 1.1.2 数据查询 1.2 #{}和${} 1.2.1 #{}和${}使用 1.2.2 #{}和${}的区别 1.3 排序功能 1.4 like查询 2 数据库连接池 2.1 …

C++反射之检测struct或class是否实现指定函数

目录 1.引言 2.检测结构体或类的静态函数 3.检测结构体或类的成员函数 3.1.方法1 3.2.方法2 1.引言 诸如Java, C#这些语言是设计的时候就有反射支持的。c没有原生的反射支持。并且&#xff0c;c提供给我们的运行时类型信息非常少&#xff0c;只是通过typeinfo提供了有限的…

【吃透Java手写】1- Spring(上)-启动-扫描-依赖注入-初始化-后置处理器

【吃透Java手写】Spring&#xff08;上&#xff09;启动-扫描-依赖注入-初始化-后置处理器 1 准备工作1.1 创建自己的Spring容器类1.2 创建自己的配置类 ComponentScan1.3 ComponentScan1.3.1 Retention1.3.2 Target 1.4 用户类UserService Component1.5 Component1.6 测试类 2…

AI实景自动无人直播软件:引领直播行业智能化革命;提升直播效果,无人直播软件助力智能讲解

随着科技的快速发展&#xff0c;AI实景自动无人直播软件正在引领直播行业迈向智能化革命。它通过智能讲解、一键开播和智能回复等功能&#xff0c;为商家提供了更高效、便捷的直播体验。此外&#xff0c;软件还支持手机拍摄真实场景或搭建虚拟场景&#xff0c;使直播画面更好看…

Unity 性能优化之动态批处理(四)

提示&#xff1a;仅供参考&#xff0c;有误之处&#xff0c;麻烦大佬指出&#xff0c;不胜感激&#xff01; 文章目录 前言一、动态合批是什么&#xff1f;二、使用动态批处理1.打开动态合批2.满足条件 三、检查动态合批是否成功五、动态合批弊端总结 前言 动态批处理是常用优…

Flutter笔记:手动配置VSCode中Dart代码自动格式化

Flutter笔记 手动配置VSCode中Dart代码自动格式化 - 文章信息 - Author: 李俊才 (jcLee95) Visit me at CSDN: https://jclee95.blog.csdn.netMy WebSite&#xff1a;http://thispage.tech/Email: 291148484163.com. Shenzhen ChinaAddress of this article:https://blog.csd…

pcm转MP3怎么转?只需3个步骤~

PCM&#xff08;Pulse Code Modulation&#xff09;是一种用于数字音频编码的基础技术&#xff0c;最早起源于模拟音频信号数字化的需求。通过PCM&#xff0c;模拟音频信号可以被精确地转换为数字形式&#xff0c;为数字音频的发展奠定了基础。 MP3文件格式的多个优点 MP3的优…

【深度学习】网络安全,SQL注入识别,SQL注入检测,基于深度学习的sql注入语句识别,数据集,代码

文章目录 一、 什么是sql注入二、 sql注入的例子三、 深度学习模型3.1. SQL注入识别任务3.2. 使用全连接神经网络来做分类3.3. 使用bert来做sql语句分类 四、 深度学习模型的算法推理和部署五、代码获取 一、 什么是sql注入 SQL注入是一种常见的网络安全漏洞&#xff0c;它允许…

模糊的图片文字,OCR能否正确识别?

拍照手抖、光线不足等复杂的环境下形成的图片都有可能会造成文字模糊&#xff0c;那这些图片文字对于OCR软件来说&#xff0c;是否能否准确识别呢&#xff1f; 这其中的奥秘&#xff0c;与文字的模糊程度紧密相连。想象一下&#xff0c;如果那些文字对于我们的双眼来说&#x…

sed小实践2(随手记)

删除/etc/passwd的第一个字符 #本质是利用sg替换&#xff0c;将第一个字符替换成空 sed s|^.||g /etc/passwd删除/etc/passwd的第二个字符 sed -r s|^(.).(.*$)|\1\2|g /etc/passwd sed -r s|^(.).|\1|g /etc/passwd删除/etc/passwd的最后一个字符 sed s|.$||g /etc/passwd删…

Java快速入门系列-11(项目实战与最佳实践)

第十一章&#xff1a;项目实战与最佳实践 11.1 项目规划与需求分析项目规划需求分析实例代码 11.2 系统设计考虑实例代码 11.3 代码实现与重构实例代码 11.4 性能优化与监控实例代码 11.5 部署与持续集成/持续部署(CI/CD)实例代码 11.1 项目规划与需求分析 在进行任何软件开发…

基于Vumat的修正JC本构模型的切削研究

JC渐进损伤本构是研究切削中的重要本构模型&#xff0c;主要包括材料硬化和损伤两部分&#xff1a;其中&#xff0c;原始JC的硬化部分本构为&#xff1b; 添加图片注释&#xff0c;不超过 140 字&#xff08;可选&#xff09; 材料屈服应力的硬化解耦为三部分独立的效应&#x…

blender导出gltf模型混乱

最近用户给了几个blender文件&#xff0c;在blender打开是这样的&#xff1a; 我导出成gltf候&#xff0c;在本地打开时&#xff0c;底部发生了改变&#xff1a; 可以看出来&#xff0c;底部由原来的类型box变为了两个平面&#xff0c;后来我查了下blender里的属性设置&#xf…
最新文章