二叉树和堆(优先队列)

前言:

        本章会讲解二叉树及其一些相关练习题,和堆是什么。

二叉树:

二叉树的一些概念:

        一棵二叉树是有限节点的集合,该集合可能为空。二叉树的特点是每一个节点最多有两个子树,即二叉树不存在度大于2的节点。且二叉树子树有左右之分,子树顺序不能颠倒。

        还有两种特殊的二叉树,完全二叉树和满二叉树。

        满二叉树是就是没有度为1的节点。所以当有k层时,它有2^k -1个节点。

        完全二叉树有度为1的节点且是连续的。

        所以我们可以根据节点的个数计算树的高度。

二叉树的性质: 

        若规定根节点层数是1,则一颗非空二叉树第i层上最多有2^(i-1)个节点。

        若规定根节点层数是1,则深度为h的二叉树的最大节点数为2^h-1个节点。

        对任何一颗二叉树如果度为0的节点数是n0,度为2的节点数是n2,则有n0=n2+1。

        若规定根节点层数为1,则有n个节点的满二叉树深度为h=LogN。

         在具有2n个节点的完全二叉树中,叶子结点个数为n。

练习题:

二叉树的最大深度:

        OJ链接:力扣(LeetCode)官网 - 全球极客挚爱的技术成长平台

        思路:整棵树的高度 = (左子树的高度 + 右子树的高度)的最大值 + 1。

        其实也就是求树的高度,这里我们利用递归来实现:

class Solution {public int maxDepth(TreeNode root) {if (root == null) {return 0;}return Math.max(maxDepth(root.left), maxDepth(root.right)) + 1;}
}

判断是否为平衡二叉树: 

        OJ链接:力扣(LeetCode)官网 - 全球极客挚爱的技术成长平台

        这里需要用到二叉树的最大深度来完成:

class Solution {public boolean isBalanced(TreeNode root) {if (root == null) {return true;}return treeHeigth(root) >= 0;//时间复杂度:O(n)}public int treeHeigth(TreeNode root) {if (root == null) {return 0;}int leftHeigth = treeHeigth(root.left);if (leftHeigth < 0) {return -1;}int rightHeigth = treeHeigth(root.right);if (leftHeigth >= 0 && rightHeigth >= 0&& Math.abs(leftHeigth - rightHeigth) <= 1) {return Math.max(leftHeigth, rightHeigth) + 1;} else {return -1;}}
}

相同的树: 

        OJ链接:力扣(LeetCode)官网 - 全球极客挚爱的技术成长平台

class Solution {public boolean isSameTree(TreeNode p, TreeNode q) {if (p == null && q != null || p != null && q == null) {return false;}//此时,要么两个都为空 要么两个都不为空if (p == null && q == null) {return true;}//此时两个都不为空if (p.val != q.val) {return false;}//p != null && q != null && p.val == q.valreturn isSameTree(p.left, q.left) && isSameTree(p.right, q.right);//时间复杂度为min(n,m)}
}

另一棵树的子树: 

        OJ链接:力扣(LeetCode)官网 - 全球极客挚爱的技术成长平台

        这里我们需要用到判断两树是否相同的代码:

class Solution {public boolean isSubtree(TreeNode root, TreeNode subRoot) {//可能会有空的情况if (root == null || subRoot == null) {return false;}//类似于BF算法//1.是不是和根节点相同if (isSameTree(root, subRoot)) {return true;}//2.判断是不是root的左子树if (isSubtree(root.left, subRoot)){return true;}//3.右子树if (isSubtree(root.right, subRoot)) {return true;}//4.返回return false;//时间复杂度 O(M * N)}public boolean isSameTree(TreeNode p, TreeNode q) {if (p == null && q != null || p != null && q == null) {return false;}//此时,要么两个都为空 要么两个都不为空if (p == null && q == null) {return true;}//此时两个都不为空if (p.val != q.val) {return false;}//p != null && q != null && p.val == q.valreturn isSameTree(p.left, q.left) && isSameTree(p.right, q.right);//时间复杂度为min(n,m)}
}

翻转二叉树: 

        OJ链接:力扣(LeetCode)官网 - 全球极客挚爱的技术成长平台

class Solution {public TreeNode invertTree(TreeNode root) {if (root == null) {return null;}TreeNode tmp = root.left;root.left = root.right;root.right = tmp;invertTree(root.left);invertTree(root.right);return root;}
}

对称二叉树: 

        OJ链接:力扣(LeetCode)官网 - 全球极客挚爱的技术成长平台

class Solution {public boolean isSymmetric(TreeNode root) {//根的值一样//1.左树的左 和 右树的右//2.左树的右 和 右树的左if (root == null) {return true;}return isSymmetricChild(root.left, root.right);}private boolean isSymmetricChild(TreeNode leftTree, TreeNode rightTree) {if (leftTree == null && rightTree != null || leftTree != null && rightTree == null) {return false;}if (leftTree == null && rightTree == null) {return true;}if (leftTree.val != rightTree.val) {return false;}return isSymmetricChild(leftTree.left, rightTree.right) && isSymmetricChild(leftTree.right, rightTree.left);}
}

最近公共祖先: 

        OJ链接:力扣(LeetCode)官网 - 全球极客挚爱的技术成长平台

class Solution {public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {if (root == null) {return null;}//利用链表相交的道理//之后利用栈来完成Stack<TreeNode> stack1 = new Stack<>();Stack<TreeNode> stack2 = new Stack<>();getPath(root, p, stack1);getPath(root, q, stack2);while (stack1.size() != stack2.size()) {if (stack1.size() > stack2.size()) {stack1.pop();} else {stack2.pop();}}while (stack1.peek() != stack2.peek()) {stack1.pop();stack2.pop();}return stack1.peek();}private boolean getPath(TreeNode root, TreeNode node, Stack<TreeNode> stack) {if (root == null || node == null) {return false;}stack.push(root);if (root == node) {return true;}boolean flag1 = getPath(root.left, node, stack);if (flag1 == true) {return true;}boolean flag2 = getPath(root.right, node, stack);if (flag2 == true) {return true;}stack.pop();return false;}
}

求树的第K层节点个数:

        求树的第k层节点个数,如果不用层序遍历,我们可以使用递归。

        思路:整棵树第k层多少个节点 = 左子树的第k-1层节点 + 右子树的第k-1层节点。

        A的第3层 = A左树的第2层 + A右树的第2层

int CountLevel(TreeNode root, int k) {if (root == null) {return 0;}if (k == 1) {return 1;}return CountLevel(root.left, k - 1) + CountLevel(root.right, k - 1);
} 

找节点:

        我们要找一个节点的位置,找到返回它的地址,否则返回null。

TreeNode find(TreeNode root, char val) {if (root == null) {return null;}if (root.val = val) {return root;}TreeNode ret1 = find(root.left, val);if (ret1 != null) {return ret1;//不去右边了,因为找到了}TreeNode ret2 = find(root.right, val);if (ret2 != null) {return ret2;}return null;
}

根据树的前序遍历构建一棵树:

        oj链接:二叉树遍历__牛客网

class TreeNode {public char val;public TreeNode left;public TreeNode right;public TreeNode(char val) {this.val = val;}
}// 注意类名必须为 Main, 不要有任何 package xxx 信息
public class Main {public static int i = 0;public static void main(String[] args) {Scanner in = new Scanner(System.in);// 注意 hasNext 和 hasNextLine 的区别while (in.hasNextLine()) { // 注意 while 处理多个 caseString str = in.nextLine();TreeNode root = creatNode(str);inorder(root);}}public static TreeNode creatNode(String str) {//1.遍历str// for (int i = 0; i < str.length(); i++) {//     char ch = str.charAt(i);// }TreeNode root = null;if (str.charAt(i) != '#') {root = new TreeNode(str.charAt(i));i++;root.left = creatNode(str);root.right = creatNode(str);} else {i++;}//2.根据字符串创建二叉树//3.返回根节点return root;}public static void inorder(TreeNode root) {if (root == null) {return ;}inorder(root.left);System.out.print(root.val + " ");inorder(root.right);}}

判断是否为完全二叉树: 

        12节(2:44)。

        我们利用层序遍历,每次都把所有节点加入队列,包括null。之后遇到null就跳出,之后再判断(此时如果是完全二叉树,则队列中所有元素为null;否则则不是完全二叉树)。

boolean isCompleteTree(TreeNode root) {if (root == null) {return true;}Queue<TreeNode> queue = new LinkedList<>();queue.offer(root);while (!queue.isEmpty()) {TreeNode cur = queue.poll();if (cur != null) {queue.offer(cur.left);queue.offer(cur.right);} else {break;}}//判断队列中是否有非空元素while (!queue.isEmpty()) {TreeNode cur = queue.peek();if (cur == null) {queue.poll();} else {return false;}}return true;
}

        这里我们使用队列的性质来完成。 

层序遍历:

        OJ链接:力扣(LeetCode)官网 - 全球极客挚爱的技术成长平台

class Solution {public List<List<Integer>> levelOrder(TreeNode root) {//利用队列List<List<Integer>> link = new ArrayList<>();Queue<TreeNode> queue = new ArrayDeque<>();if (root != null) {queue.offer(root);}while(!queue.isEmpty()) {int n = queue.size();List<Integer> level = new ArrayList<>();for (int i = 0; i < n; i++) {TreeNode node = queue.poll();level.add(node.val);if (node.left != null) {queue.add(node.left);}if (node.right != null) {queue.add(node.right);}}link.add(level);}return link;}
}

根据前序遍历和中序遍历构建二叉树:

        OJ链接:力扣(LeetCode)官网 - 全球极客挚爱的技术成长平台

class Solution {public int preIndex;public TreeNode buildTree(int[] preorder, int[] inorder) {return buildTreeChild(preorder, inorder, 0, inorder.length - 1);}private TreeNode buildTreeChild(int[] preorder, int [] inorder, int inbegin, int inend) {if (inbegin > inend) {return null;}TreeNode root = new TreeNode(preorder[preIndex]);int rootIndex = findIndexRoot(inorder, inbegin, inend, preorder[preIndex]);if (rootIndex == -1) {return null;}preIndex++;root.left = buildTreeChild(preorder, inorder, inbegin, rootIndex - 1);root.right = buildTreeChild(preorder, inorder, rootIndex + 1, inend);return root;}private int findIndexRoot(int[] inorder, int inbegin, int inend, int target) {while (inbegin <= inend) {if (inorder[inbegin] == target) {return inbegin;}inbegin++;}return -1;}
}

根据中序遍历和后序遍历构建二叉树: 

        OJ链接:力扣(LeetCode)官网 - 全球极客挚爱的技术成长平台

class Solution {public int endIndex;public TreeNode buildTree(int[] inorder, int[] postorder) {endIndex = postorder.length - 1;return buildTreeChild(inorder, postorder, 0, postorder.length - 1);}private TreeNode buildTreeChild(int[] inorder, int[] postorder, int begin, int end) {if (begin > end) {return null;}TreeNode root = new TreeNode(postorder[endIndex]);int rootIndex = findTreeNode(inorder, begin, end, postorder[endIndex]);if (rootIndex < 0) {return null;}endIndex--;//这里要先创建右树root.right = buildTreeChild(inorder, postorder, rootIndex + 1, end);root.left = buildTreeChild(inorder, postorder, begin, rootIndex - 1);return root;}private int findTreeNode(int[] inorder, int begin, int end, int key) {while (begin <= end) {if (inorder[begin] == key) {return begin;}begin++;}return -1;}
}

前序遍历非递归:

        此时我们借助栈来完成。

void preOrderNor(TreeNode root) {if (root == null) {return;}Stack<TreeNode> stack = new Stack<>();TreeNode cur = root;while (cur != null || !stack.isEmpty()) {while (cur != null) {stack.push(cur);System.out.print(cur.val + " ");cur = cur.left;}TreeNode top = stack.pop();cur = top.right;} 
}

后序遍历非递归:

void postOrderNor(TreeNode root) {if (root == null) {return;}Stack<TreeNode> stack = new Stack<>();TreeNode cur = root;while (cur != null || !stack.isEmpty()) {while (cur != null) {stack.push(cur);cur = cur.left;}TreeNode top = stack.peek();TreeNode prev = null;//方便记录if (top.right == null || top.right == prev) {System.out.print(top.val + " ");stack.pop();prev = top;} else {cur = top.right;}}
}

堆(优先级队列):

堆的概念:

        我们可以将数组想成一个二叉树,堆的逻辑结构是一颗完全二叉树,物理结构是一个数组。我们可以得出左右孩子和父节点的数学关系。

        建立堆,可以分为两种,一种建立小堆,一种建立大堆。我们利用向下调整算法来建立堆。

向下调整算法: 

        我们可以将数组想象成二叉树,但是向下调整算法必须保证左右树必须已经建好堆,所以我们从数组的最后开始建堆,也就是从最后一颗子树开始,根据公式,最后一棵树的位置(下标)就是(n - 1 - 1) / 2,之后逐个向下调整并建好堆。接下来给出该算法:

public class TestHeap {public  int[] elem;public  int usedSize;public TestHeap() {this.elem = new int[10];}public void initElem(int[] array) {for (int i = 0; i < array.length; i++) {elem[i] = array[i];usedSize++;}}public void createHeap() {for (int parent = (usedSize - 1 - 1) / 2; parent >= 0; parent--) {AdjustDown(parent, usedSize);}}private void AdjustDown(int parent, int len) {int child = parent * 2 + 1;//建大堆while (child < len) {if (elem[child] < elem[child + 1] && child + 1 < len) {child++;}if (elem[parent] < elem[child]) {//交换swap(parent, child);parent = child;child = parent * 2 + 1;} else {break;}}}private void swap(int a, int b) {int tmp = elem[a];elem[a] = elem[b];elem[b] = tmp;}}

优先级队列(PriorityQueue):

        其实就是堆,但是我们还是要先了解一下什么是优先级队列。

        优先级队列,有些情况下,操作的数据可能带有优先级,一般出队列时,可能需要优先级高的元素先出队列。此时一般的队列显然不合适。比如玩手机时,有人打来电话,系统就应该优先处理打来的电话。

        这种数据结构应该提供两个最基本的操作,一个是返回最高优先级对象,一个是添加新的对象。这就被称为优先级队列,优先级队列底层是一个完全二叉树。

        这里优先级队列底层是使用数组实现的,操作规则是使用队列来完成的。 

        不能插入null对象,可以插入任意多个元素,内部可以实现自动扩容。

        当我们进行删除优先级队列元素时,需要从队列头部开始删除,如果从尾部开始删除,则相当于向上建堆,向上调整建堆时间复杂度会很大,所以我们进行头删。

public static void main(String[] args) {PriorityQueue<Integer> priorityQueue = new PriorityQueue<>();//堆priorityQueue.offer(10);priorityQueue.offer(5);priorityQueue.offer(6);System.out.println(priorityQueue.peek());//当我们实例化一个 priorityQueue 之后,默认是一个小根堆
}

        此时队头元素为5,可以发现默认是小堆。 所以我们如何将其改为大堆呢?

构建大堆(Comparable接口): 

        我们不能随意向其中插入数据,因为我们其实会进行比较。举个例子:

class Student {public int age;public String name;public Student(int age, String name) {this.age = age;this.name = name;}@Overridepublic String toString() {return "Student{" +"age=" + age +", name='" + name + '\'' +'}';}
}public class Test2 {public static void main(String[] args) {PriorityQueue<Student> priorityQueue = new PriorityQueue<>();priorityQueue.offer(new Student(22, "wowo"));priorityQueue.offer(new Student(21, "wda"));}
}

        此时报错,因为没有指定类型去建堆。 所以我们其实可以想到可能其中使用了Comparable接口。

         所以可以发现当我们使用无参构造器时,默认优先队列的容量是11。而且可以发现其使用了比较器。

        看一看出,里面重载了构造方法,所以我们可以传入比较器来完成效果。比如此时我们是一个小堆,第一个元素是10,之后插入5:

        我们再观察Integer中的Comparable接口中的compareTo方法。 

        也就是说,此时我们将返回值改变即可将小根堆改为大根堆。 

public static void main(String[] args) {Imp imp = new Imp();PriorityQueue<Integer> priorityQueue = new PriorityQueue<>(imp);//使用自己的比较器//堆priorityQueue.offer(10);priorityQueue.offer(5);
}
class  Imp implements Comparator<Integer> {@Overridepublic int compare(Integer o1, Integer o2) {return o1.compareTo(o2);}
}

        所以我们可以通过自己实现的比较器来构建大根堆。

观察源码:

         可以看到,当数组容量小于64时,每次增加2;当容量大于64时,每次扩容1.5倍。

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

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

相关文章

进程间通信-消息队列

消息队列的公共资源是链表结构。 通信双方不会和消息队列进行挂接&#xff0c;而是像管道一样&#xff0c;访问内存中的消息队列。 消息队列由操作系统维护&#xff0c;但是由通信的某一方创建和删除 通信双方都需要获取到消息队列&#xff0c;和共享内存一样。 当发送方有数据…

阿里云游戏服务器租用费用价格组成,费用详单

阿里云游戏服务器租用价格表&#xff1a;4核16G服务器26元1个月、146元半年&#xff0c;游戏专业服务器8核32G配置90元一个月、271元3个月&#xff0c;阿里云服务器网aliyunfuwuqi.com分享阿里云游戏专用服务器详细配置和精准报价&#xff1a; 阿里云游戏服务器租用价格表 阿…

图(高阶数据结构)

目录 一、图的基本概念 二、图的存储结构 2.1 邻接矩阵 2.2 邻接表 三、图的遍历 3.1 广度优先遍历 3.2 深度优先遍历 四、最小生成树 4.1 Kruskal算法 4.2 Prim算法 五、最短路径 5.1 单源最短路径-Dijkstra算法 5.2 单源最短路径-Bellman-Ford算法 5.3 多源最…

Vue - 快速入门(一)

阅读文章可以收获&#xff1a; 1. 明白什么是vue 2. 如何创建一个vue实例 3. vue中的插值表达式如何使用 4. 如何安装vue的开发者工具 Vue 概念 什么是vue&#xff1f; Vue 是一个用于 构建用户界面 的 渐进式 框架 框架优点&#xff1a;大大提升开发效率 (70%↑) 缺点…

蓝桥杯官网练习题(翻转)

问题描述 小蓝用黑白棋的 n 个棋子排成了一行&#xff0c;他在脑海里想象出了一个长度为 n 的 01 串 T&#xff0c;他发现如果把黑棋当做 1&#xff0c;白棋当做 0&#xff0c;这一行棋子也是一个长度为 n 的 01 串 S。 小蓝决定&#xff0c;如果在 S 中发现一个棋子…

Depth Anything放入MVS中?

这是Depth Anything的深度值depth&#xff0c;这个depth通过depth depth_anything(image)求得。 但想要把这个深度值depth嵌入到三维重建算法框架中&#xff0c;并不是一件容易得事情&#xff0c;拿OpenMVS举例&#xff0c;下图是OpenMVS输出深度图的函数。 OpenMVS的深度值保…

Vue中使用 Element-ui form和 el-dialog 进行自定义表单校验清除表单状态

文章目录 问题分析 问题 在使用 Element-ui el-form 和 el-dialog 进行自定义表单校验时&#xff0c;出现点击编辑按钮之后再带年纪新增按钮&#xff0c;出现如下情况&#xff0c;新增弹出表单进行了一次表单验证&#xff0c;而这时不应该要表单验证的 分析 在寻找多种解决…

「深度学习」dropout 技术

一、工作原理 1. 正则化网络 dropout 将遍历网络的每一层&#xff0c;并设置消除神经网络中节点的概率。 1. 每个节点保留/消除的概率为0.5: 2. 消除节点&#xff1a; 3. 得到一个规模更小的神经网络&#xff1a; 2. dropout 技术 最常用&#xff1a;反向随机失活 "…

开局一个破碗的故事例子

在一个寒冷的冬日&#xff0c;一个瘦弱的小姑娘拿着一个破碗&#xff0c;孤独地走在被白雪覆盖的街道上。她的名字叫小梅&#xff0c;她的父母早逝&#xff0c;留下她一个人在这个世界上艰难地生活。 小梅的破碗里只有几个铜板&#xff0c;那是她前一天沿街乞讨所得&#xff0c…

创新S3存储桶检索:Langchain社区S3加载器搭载OpenAI API

在瞬息万变的数据存储和处理领域&#xff0c;将高效的云存储解决方案与先进的 AI 功能相结合&#xff0c;为处理大量数据提供了一种变革性的方法。本文演示了使用 MinIO、Langchain 和 OpenAI 的 GPT-3.5 模型的实际实现&#xff0c;重点总结了存储在 MinIO 存储桶中的文档。 …

C语言之随心所欲打印三角形,金字塔,菱形(倒金字塔)

个人主页&#xff08;找往期文章包括但不限于本期文章中不懂的知识点&#xff09;&#xff1a; 我要学编程(ಥ_ಥ)-CSDN博客 目录 三角形 金字塔 倒金字塔 菱形 三角形 题目&#xff1a;根据输入的行数打印对应的三角形。&#xff08;用 * 号打印&#xff09; #includ…

k8s报错记录(持续更新中....)

k8s报错记录(持续更新中…) 1. 部署k8s遇到kube-flannel已经构建&#xff0c;但是coredns一直处于ContainerCreating和pending状态 解决问题&#xff1a; 通过 kubectl describe pod -n kube-system coredns-7ff77c879f-9ls2b 查看pod的详细信息&#xff0c;报错说是cni 配置没…

spring 入门 一

文章目录 Spring简介Spring的优势Spring的体系结构 Spring快速入门Spring程序开发步骤导入Spring开发的基本包坐标编写Dao接口和实现创建Spring核心配置文件在Spring配置文件中配置UserDaoImpl使用Spring的API获得Bean实例 Spring配置文件Bean标签基本配置Bean标签范围配置Bean…

Windows10安装PCL1.14.0及点云配准

一、下载visual studio2022 下载网址&#xff1a;Visual Studio: 面向软件开发人员和 Teams 的 IDE 和代码编辑器 (microsoft.com) 安装的时候选择"使用C的桌面开发“&#xff0c;同时可以修改文件路径&#xff0c;可以放在D盘。修改文件路径的时候&#xff0c;共享组件、…

Stable Diffusion 模型下载:DreamShaper(梦想塑造者)

文章目录 模型介绍生成案例案例一案例二案例三案例四案例五案例六案例七案例八案例九案例十 下载地址 模型介绍 DreamShaper 是一个分格多样的大模型&#xff0c;可以生成写实、原画、2.5D 等多种图片&#xff0c;能生成很棒的人像和风景图。 条目内容类型大模型基础模型SD 1…

《统计学简易速速上手小册》第7章:时间序列分析(2024 最新版)

文章目录 7.1 时间序列数据的特点7.1.1 基础知识7.1.2 主要案例&#xff1a;股票市场分析7.1.3 拓展案例 1&#xff1a;电商销售预测7.1.4 拓展案例 2&#xff1a;能源消耗趋势分析 7.2 时间序列模型7.2.1 基础知识7.2.2 主要案例&#xff1a;股价预测7.2.3 拓展案例 1&#xf…

Day39- 动态规划part07

一、爬楼梯 题目一&#xff1a;57. 爬楼梯 57. 爬楼梯&#xff08;第八期模拟笔试&#xff09; 题目描述 假设你正在爬楼梯。需要 n 阶你才能到达楼顶。 每次你可以爬至多m (1 < m < n)个台阶。你有多少种不同的方法可以爬到楼顶呢&#xff1f; 注意&#xff1a;…

SpringCloud-Nacos服务分级存储模型

Nacos 服务分级存储模型是 Nacos 存储服务注册信息和配置信息的核心模型之一。它通过将服务和配置信息按照不同级别进行存储&#xff0c;实现了信息的灵活管理和快速检索&#xff0c;为微服务架构下的服务发现和配置管理提供了高效、可靠的支持。本文将对 Nacos 服务分级存储模…

C++重新入门-C++运算符

目录 1. 算术运算符 2. 关系运算符 3.逻辑运算符 4.位运算符 5.赋值运算符 6.杂项运算符 7.C 中的运算符优先级 运算符是一种告诉编译器执行特定的数学或逻辑操作的符号。C 内置了丰富的运算符&#xff0c;并提供了以下类型的运算符&#xff1a; 算术运算符关系运算符逻…

高仿原神官网UI 纯html源码

高仿原神官网UI源码介绍 如果您希望打造一个与原神官方网站相似的外观和用户体验&#xff0c;但又不想使用复杂的框架或模板&#xff0c;那么我们的高仿原神官网UI源码将是一个完美的选择。它采用纯HTML5构建&#xff0c;无需任何额外的CSS或JavaScript库支持&#xff0c;即可…