【数据结构】Map和Set

Map和Set

1. 搜索树

1.1 概念

二叉搜索树是左子树比根节点小,右子树比根节点大的二叉树。(如果左右子树不为空的话是这样,但是左右子树也可以为空)

1.2 操作——查找

查找的思想与二分查找类似。

如果根节点的值和所要查找的值相同,那么就返回。

如果根节点的值比查找的值小,那么就往这个结点的右子树走。

如果根节点的值比查找的值大,那么就往这个结点的左子树走。

(每次都可以筛选掉一半的数据)

1.2.1 代码
/*** 操作——搜索* @param val 搜索的值* @return boolean*/
public boolean search(int val) {
// 1. 定义一个cur进行遍历
TreeNode cur = root;
// 2. 比较
while (cur != null) {if (cur.val < val) {cur = cur.right;} else if (cur.val > val) {cur = cur.left;} else  {return true;}
}
return false;
}

1.3 操作——插入

插入和查找差不多。

先找到要插入数据应该存放的位置(一定会是叶子结点)

然后看是放在这个结点的左边还是右边。

/*** 操作——插入* @param val*/
public boolean insert(int val) {// 1. 如果是空树if (root == null) {root = new TreeNode(val);return true;}// 2. 定义一个cur进行遍历,一个parent进行保存上一步cur的位置TreeNode cur = root;TreeNode parent = cur;while (cur != null) {// 保存这次的cur结点,方便后续的插入结点parent = cur;if (cur.val < val) {cur = cur.right;} else if (cur.val > val) {cur = cur.left;} else  {return false;}}if (parent.val < val) {parent.right = new TreeNode(val);} else {parent.left = new TreeNode(val);}return true;
}

1.4 操作——删除(难点)

删除的情况分为三种:

规定 node为将要删除的结点,parent 为要删除结点的父节点。

  1. node.left = null

    1.1 如果 node是根节点,root.right = node.right;

    1.2 如果node不是根节点,是parent 的右子树, parent.right = node.right

    1.3 如果node不是根节点,是parent 的左子树, parent.left= node.left

  2. node.right = null

    1.1 如果 node是根节点,root.left= node.left;

    1.2 如果node不是根节点,是parent 的右子树, parent.right = node.right

    1.3 如果node不是根节点,是parent 的左子树, parent.left= node.left

  3. node.right != null && node.left != null

    使用 “替罪羊” 方式进行删除:并灭有真正删除node结点,而是将node左子树的最大值进行替换上去,然后

/*** 删除的子函数* @param node* @param parent*/
public void removeNode(TreeNode node, TreeNode parent) {// 叶子节点——直接删除if (node.left == null && node.right == null) {node = null;}// 1. node没有左子树if (node.left == null) {if (node == parent.left) {parent.left = node.right;} else {parent.right = node.left;}// 2. node 没有右子树} else if (node.right == null) {if (node == parent.left) {parent.left = node.right;} else {parent.right = node.left;}// 3. node 左右子树都有// “替罪羊” 删除方式} else {// 找到node左子树的最右边,即为左子树中最大的值,进行填补;也可以选择右子树的最大值进行填补(最左边// target即为替罪羊TreeNode target = node.left;TreeNode targetParent = target;while (target.right != null) {targetParent = target;target = target.right;}// 到达最右边node.val = target.val;// “虚假”的删除targetParent.right = target.left;// target没有右子树,直接接管左子树即可}
}

1.5 性能分析

最好的情况:为平衡二叉树的时候,即为log2N。

最坏的情况:为单分支树的时候,即为N(产生了AVL树来防止这种情况的发生,其可以左旋,右旋,左右双旋,右左双旋)

2. 搜索

2.1 场景

在以往学过的搜索思想中,我们只学过二分查找(log2N,必须是有序的数组)和直接遍历(n,对数据无要求,效率低下),这种思想对于静态的数据能够有效地检索,但是无法进行删除、添加操作,但是,Map和Set可以进行动态的操作。

2.2 模型

Map:存储的是key-value键值对,key就是所要存储的数据据,value就是这个数据所附带的值(可以是这个key出现的次数,也可以是这个key的关键字等信息)。

Set:存储的是key的集合,不能够有重复,底层仍然使用Map进行初始化。

3. Map的使用

3.1 关于Map的说明

Map 是单独的一个接口,没有继承自Collection接口,并且存储的类型是<key, value>,key不可以重复(搜索的时候就是按照key的值进行搜索,重复便不能够进行搜索)

3.2 关于Map.Entry<K, V>的说明

Entry<K, V> 是Map中的一个内部接口,其能够返回Map的key以及value,并且能够设置value(但是没有提供设置key的方法),Entry<K, V>返回的是一个包含<K, V>键值对的对象,能够用其初始化Iterator,可直接定义变量。

Map.Entry<K, V>能够表示Map的映射项,而Map又没有实现iterator,所以通常用来遍历Map。

Map中有**values()方法来获得value,有keySet()**方法来获得key的一个Set,但是都一次只能访问一个内容,Entry<K, V>很好地实现了这个一次访问两个的效果。

方法解释
K getKey()获得Map中的key
V getValue()获得Map中的value
V setValue(V value);设置Map中的value

使用方法:

Map<String, Integer> map = new TreeMap<>();
map.put("初中读了几年",3);
map.put("高中读了几年",3);
map.put("大学读了几年",2);for (Map.Entry<String,Integer> entry1 : map.entrySet()) {System.out.println("String:" + entry1.getKey() + " Value:" + entry1.getValue());
}// Map.Entry<String,Integer>得到的是一个<String,Integer>的变量,可直接看作<String,Integer>
Map.Entry<String,Integer> entry;
Iterator<Map.Entry<String,Integer>> it = map.entrySet().iterator();
it.next().setValue(99);for (Map.Entry<String,Integer> entry2 : map.entrySet()) {System.out.println("String:" + entry2.getKey() + " Value:" + entry2.getValue());
}
3.3 Map的常用方法说明
方法解释
V put(K key, V value);将K,V的元素放进Map
V get(Object key);得到Map中某个key对应的value
Set keySet();返回一个全是key的Set
V remove(Object key);删除key结点
Set<Map.Entry<K, V>> entrySet();返回<K, V>键值对的Set集合(可用<K, V>键值对遍历)
boolean containsKey(Object key);返回Map中是否存在这个key
boolean containsValue(Object value);返回Map中是否存在这个value
default V getOrDefault(Object key, V defaultValue);返回Map中是否存在这个key,如果不存在,则返回defaultValue
  • 注意:
  1. 关于Set<Map.Entry<K, V>> entrySet()产生的代码如下,
Set<Map.Entry<String, Integer>> set = map.entrySet();
for(Map.Entry<String, Integer> e : set) {System.out.println("String:" + e.getKey() + " Value:" + e.getValue());
}
  1. Map有两种实现类,分别是HashMap和TreeMap。

    Map底层结构TreeMapHashMap
    底层结构红黑树哈希桶
    插入/删除/查找时间复杂度O(log2N)O(1)
    是否有序有序(插入的时候对于key进行了排序)无序(存储是按照HashCode进行存储)
    线程安全不安全不安全
    插入/查找/删除区别按照key值插入,进行了元素之间的比较通过哈希函数,按照哈希地址插入
    比较与覆写key值必须能够比较,否则抛出ClassCastException异常(需要实现Comparable/Comparator接口)自定义类型需要覆写equals和 hashCode方法(需要利用哈希函数进行插入)
    应用场景需要key有序的时候更需要效率的时候
  2. Map中不存在两个相同的key值,但是value可以重复,当多次put进同一个key时,会更新这个key的值。

  3. 在TreeMap中key值不能为空,但是HashMap的key可以为空

    主要是因为TreeMap中,put方法会调用比较器函数,如果传过来的是一个null,那么也就无法比较。

    但是HashMap中,虽然用的是哈希函数,但是在哈希函数的内部使用了hash()方法,里面对于key为null的情况作了处理:如果key为null,那么就将其置为0。(但是只能有一个key为0的结点,因为同样,一个0只能计算出一个hash地址,重复插入key为null的结点,只会更新其value值)

  4. Map中的key不能修改,要修改只能删除后再添加进行修改

4. Set的使用

Set与Map的不同点在于Set只存储key,并且Set继承自Collection接口。

4.1 常见方法说明

方法解释
boolean add(E e);向集合中添加元素e
Object[] toArray();将集合变为数组
boolean contains(Object o);查看集合中是否存在o元素
Iterator iterator();返回迭代器
boolean containsAll(Collection<?> c);如果调用方包含c中的所有元素则返回true
boolean addAll(Collection<? extends E> c);将集合c中的所有元素加到调用方中,可以达到去重的效果
boolean retainAll(Collection<?> c);使调用方成为与c的子集

注意:

  1. Set中的key不能修改,要修改只能删除后再添加进行修改

  2. Set的key是唯一值

  3. TreeSet的底层是使用TreeMap进行初始化的,仍然使用键值对进行初始化,但是只传入了Set中的K,另外一个使用了Object对象。

    public TreeSet() {this(new TreeMap<E,Object>());
    }
    
  4. 同Map,TreeSet不能插入null的key,但是HashSet可以。

  5. Set的实现类有HashSet、TreeSet、LinkedHashSet(能够记录插入次序、继承了HashMSet、HashSet中有HashMap、HashMap中有这个结点,所以能够记录次序)

    Node(int hash, K key, V value, Node<K,V> next) {this.hash = hash;this.key = key;this.value = value;this.next = next;
    }
    
  6. Set最大的功能就是对元素进行去重

    Set底层结构TreeSetHashSet
    底层结构红黑树哈希桶
    插入/删除/查找时间复杂度O(log2N)O(1)
    是否有序关于key有序b不一定有序
    插入/删除/查找区别利用红黑树进行操作计算出哈希地址后才会进行对应的操作
    比较与覆写key必须能够比较,否则抛异常:ClassCastException自定义类型需要覆写equals和HashCode方法
    应用场景需要key有序查找效率为先

5. 哈希表

在以往的搜索方法中,都是需要遍历(直接搜索、二分查找)进行,但是理想的搜索方式是不需要遍历,直接能够找到,就像抓中药的时候,药师会直接抽出一个箱子,取出对应的中药,而不是一个一个箱子拉开看是不是符合自己需要的中药。

所以,哈希表也应该像中药一样,能够建立起中药和中药箱上的关系,使得药师能够根据药箱的外表就能够找到中药。所以哈希表也需要有一个映射关系,使得数据的存储位置和它的关键码能够产生联系。

实现思路:

插入元素:

根据待插入元素的关键码,根据某个计算方法计算出一个地址,按此地址进行存放。

搜索元素:

对于待搜索元素的关键码进行同样的计算,根据得到的地址进行查找。

上述的计算方法即为哈希方法,地址即为哈希地址,数据按此方式进行存放组成的结构叫做散列表(HashTable)。

5.2 冲突——概念

当进行插入的时候,势必会对多个插入元素计算出相同的哈希地址,比如我们要存储int,采用的哈希函数是:hash(key) = key % array,length;那么当我们数组长度为10,插入一个4,再插入一个14的时候,他俩都需要往下标为4的地址上存放,这样的情况就叫做哈希冲突。

5.3 冲突——避免

我们可以采用更改哈希函数、增大容器容量等方法来进行避免冲突,但是这只是一时的避免,在日后的数据越来越多的情况下,势必会再次出现冲突,那时候就又需要进行处理新的冲突。

5.3.4 冲突——避免——哈希函数设计

引起冲突的一个原因可能就是哈希函数设置的不够合理,就比如上述:hash(key) = key % array,length这个函数简单易懂,但是去容易产生冲突,如果我们更改为:hash(key) = (key + i^2) % array,length(其中 i 为插入数据的个数)那就降低了冲突的可能性。

哈希函数的设计原则:

  1. 应该尽可能的简单

  2. 函数计算出来的地址应该能够使得数据能够均匀的分布在散列表当中

  3. 哈希函数的定义域必须包括预备存储的所有数据的关键码(这样才能对于所有的数据进行计算),

    而且值域(算出来的哈希地址)必须在散列表的容量之中。

常见的哈希函数:

  1. 直接定制法:(常用)

    使用线性函数:hash(key) = a * key +b

    优点:简单均匀

    缺点:需要事先知道数据的分布才能实现函数

  2. 除留余数法:(常用)

    hash(key) = key % array.length

    优点:简单

    缺点:如果数据分散,则空间利用率低,适合处理数据集中的情况

  3. 平方取中法(了解)

    对数据进行平方后,取中间的x位作为哈希地址。

    适合数据不是很大,又不知道其分布的情况

  4. 折叠法(了解)

    将数据折叠成几段长度相等的部分,然后叠加求和,对于后几位按照散列表的长度进行取余。

  5. 随机数法(了解)

    hash(key) = random(key)

  6. 数学分析法(了解)

    通过对于数据的分析,选择出不宜重复的几项,然后将其选做哈希地址。

    比如:如果要存储公司里员工及其手机号,那么就可以选择使用后四位作为散列表地址。

5.5 冲突——避免——负载因子调节(重点掌握)

负载因子:α = 已存储的数据个数 / 散列表的长度

一般负载因子不会超过0.8,超过0.8就会频繁出现冲突问题。

所以当负载因子超过0.8的时候,我们就需要对于散列表进行调整。

5.6 冲突——解决

常见的两种方法:开散列和闭散列。

5.7 闭散列

闭散列:也叫开放地址法,当发生冲突的时候,如果哈希表未被装满,那就可以把发生冲突的数据存放到下一个空位置

  1. 线性探测
    在这里插入图片描述

这种处理方式的坏处是不能够随便删除元素,比如下表存储了12在2的后面,如果把2删除,那么32就会找不到,因为这种存储方式是通过依次遍历空位置来查找元素的,当22删除后,那个位置就为空,32就被误认为是没有尾数为2的元素的。
在这里插入图片描述


  1. 二次探测(二次方)

    线性探测对于在哈希地址同一个位置的数据进行了简单的处理,但是这种方式过于粗暴(直接挨着往后找),所以二次探测找下一个空位置的方式:Hi = (H0 ± i2) % array.length,其中 i = 1,2,3…,H0 为发生冲突的为位置。

    当发生冲突时,先取 i=1 试试看,如果这个位置仍然有元素,那就取 i = 2 看。

    在这里插入图片描述

研究表明:当表的长度为质数且表装载因子a不超过0.5时,新的表项一定能够插入,而且任何一个位置都不 会被探查两次。因此只要表中有一半的空位置,就不会存在表满的问题。在搜索时可以不考虑表装满的情况,但在插入时必须确保表的装载因子a不超过0.5,如果超出必须考虑增容(增容后,全部元素需要重新哈希)

综上,闭散列的最大缺陷就是空间利用率低,这也是哈希的缺陷。

5.8 开散列/哈希桶

开散列法又叫链地址法,因为开散列是通过一个每个元素是一个链表结点的数组(哈希表)来存储数据的。

首先对关键码利用哈希函数进行计算散列地址,如果有相同的地址,那就归于一个下标,在这个下表下有一个单链表,多出来的元素往上串即可,各链表的头结点都在哈希表中。

在开散列中进行搜索是先找到下标,然后在这个下标存储的链表中进行遍历查找所需元素。

5.9 冲突严重时的解决办法

  1. 每个桶的背后是另一张哈希表
  2. 每个桶的背后是一棵搜索树

5.10 实现

public class HashBucket {// 一个结点就是一个桶,每个桶里装单链表的头结点static class Node {private int key;private int value;Node next;public Node() {}public Node(int key, int value, Node next) {this.key = key;this.value = value;this.next = next;}}public Node[] array;public int size;public static final double LOAD_FACTOR = 0.75;/*** 构造函数*/public HashBucket() {array = new Node[8];size = 0;}/*** 插入函数* @param key* @param value*/public int put(int key, int value) {// 这里使用最简单粗暴的哈希函数int index = key % array.length;//先找是否已经存在keyfor (Node node = array[index]; node != null; node = node.next) {if (node.key == key) {int oldValue = node.value;node.value = value;// 返回更新前的值return oldValue;}}Node newNode = new Node(key, value, null);if (array[index] == null) {array[index] = newNode;} else {// 尾插Node tail = array[index];while (tail.next != null) {tail = tail.next;}tail.next = newNode;}size++;// 判断是否需要扩容if (loadFactor() > LOAD_FACTOR) resize();// 使用函数的目的是为了每次进行判断都能重新计算return value;}double loadFactor() {return size * 1.0 / array.length;}/*** 扩容函数*/private void resize() {// 扩容需要将全部元素进行重新哈希Node[] newArray = new Node[array.length * 2];/*这种方式连着空位置一起进行了迁移for (int i = 0; i < array.length; i++) {int newIndex = array[i].key % newArray.length;newArray[newIndex] = array[i];}*/for (int i = 0; i < array.length; i++) {Node next = null;// 把array的每个结点都遍历一遍,如果为空就直接跳过,不为空则一直遍历进行重新哈希for (Node cur = array[i]; cur != null; cur = next){next = cur.next;int newIndex = cur.key % newArray.length;newArray[newIndex] = array[i];}}array = newArray;}/*** 得到key对应的value值* @param key* @return*/public int get (int key) {int index = key % array.length;Node cur = array[index];while (cur.key != key) {cur = cur.next;}if (cur != null) return cur.value;else return -1;}
}

5.11 性能分析

因为哈希表构建了数据关键码和散列表地址的映射,能够根据数据直接取出对应的值,虽然总是存在冲突,但是这并不是不可解决的问题,所以时间复杂度一般认为是O(1)。

5.12 和Java类集的关系

  1. HashMap和HashSet就是利用了哈希表实现的Map和Set
  2. Java中使用哈希桶解决冲突
  3. Java中负载因子超过0.75后会转变为红黑树提高效率
  4. Java中计算哈希地址就是使用 hashcode()方法进行计算,如果要使用自定义的类进行插入,那么必须覆写 equals()hashcode()

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

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

相关文章

wangEditor富文本编辑器的使用

文章目录 &#x1f7e2; wangeditor 富文本⭐️安装 wangeditor⭐️demo 模板⭐️效果图 ✒️总结 &#x1f7e2; wangeditor 富文本 一款开源 Web 富文本编辑器&#xff0c;开箱即用&#xff0c;配置简单 wangedito 官网 简洁易用、功能强大、文档教程丰富支持 JS、Vue、Rea…

【Note详细图解】中缀表达式如何转为后缀表达式?数据结构

中缀表达式 中缀表达式&#xff08;中缀记法&#xff09;是一个通用的算术或逻辑公式表示方法&#xff0c;操作符是以中缀形式处于操作数的中间&#xff08;例&#xff1a;3 4&#xff09;&#xff0c;中缀表达式是人们常用的算术表示方法。 前缀或后缀记法不同的是&#xf…

【JVM】类加载器

【JVM】类加载器 文章目录 【JVM】类加载器0. 类加载器概述1. 类加载器的分类1.1 启动类加载器1.2 Java中的默认类加载器1.2.1 扩展类加载器1.2.2 应用程序类加载器 2. 双亲委派机制2.1 类的双亲委派机制是什么&#xff1f;2.2 打破双亲委派机制2.2.1 自定义类加载器2.2.2 线程…

并行和并发有什么区别?

并行和并发 并行和并发最早其实描述的是 Java 并发编程里面的概念。他们强调的是 CPU 处理任务的能力。简单来说&#xff1a; 并发&#xff0c;就是同一个时刻&#xff0c;CPU 能够处理的任务数量&#xff0c;并且对于应用程序来说&#xff0c;不会出现卡顿现象。并行&#x…

【Linux】冯诺依曼体系结构以及初始操作系统

文章目录 冯诺依曼体系结构操作系统概念设计OS的目的定位如何理解管理 总结系统调用和库函数概念 冯诺依曼体系结构 我们常见的计算机&#xff0c;如笔记本。我们不常见的计算机&#xff0c;如服务器&#xff0c;大部分都遵守冯诺依曼体系。 截至目前&#xff0c;我们所认识…

HDFS 基本 shell 操作

HDFS 基本 shell 操作 1.1 创建目录1.2 上传指令1.3 创建空文件1.4 向分布式文件系统中的文件里追加内容1.5 查看指令1.6 下载指令1.7 合并下载1.8 移动hdfs中的文件1.9 复制hdfs中的文件到hdfs的另一个目录1.10 删除命令1.11 查看磁盘利用率和文件大小1.12 修改权限1.13 修改文…

专门解决数学问题的大模型

01 项目介绍 LLEMMA&#xff1a;一个专门解决数学问题的开源大语言模型&#xff0c;能力超过所有已知的开源模型 LLEMMA由多个大学和Eleuther AI公司共同研发&#xff0c;模型能够理解和生成数学表达式、解决数学问题&#xff0c;并与其他计算工具&#xff08;如Python解释器…

修改el-date-picker宽度

<div style"width: 100%"><el-date-pickerstyle"width:100%"v-model"value"type"datetimerange"start-placeholder"开始日期"end-placeholder"结束日期":default-time"[12:00:00]"value-forma…

pytorch 入门 (五)案例三:乳腺癌识别-VGG16实现

本文为&#x1f517;小白入门Pytorch内部限免文章 &#x1f368; 本文为&#x1f517;小白入门Pytorch中的学习记录博客&#x1f366; 参考文章&#xff1a;【小白入门Pytorch】乳腺癌识别&#x1f356; 原作者&#xff1a;K同学啊 在本案例中&#xff0c;我将带大家探索一下深…

Response Header中不暴露Server(IIS)版本、ASP.NET及相关版本等信息

ASP MVC开发的Web默认情况下会在请求的回应中暴露Server、X-AspNet-Version、X-AspNetMvc-Version、X-Powered-By等相关服务端信息&#xff0c;公开这些敏感信息会存在一定的安全风险。 X-SourceFiles标头用于被IIS / IIS Express中某些调试模块理解&#xff0c;它包含到磁盘上…

【Vue】初步认识<script setup>语法糖和组合式 API

▒ 目录 ▒ &#x1f6eb; 导读需求开发环境 1️⃣ &#x1f6eb; 导读 需求 最近写代码的时候&#xff0c;发现<script setup>这样的代码&#xff0c;没见过&#xff0c;好奇&#xff0c;想知道。 所以就有了这篇文章。 很多文章都说setup是vue3的特权。但是&#xff…

Vue图片路径问题(动态引入)

vue项目中我们经常会遇到动态路径的图片无法显示的问题&#xff0c;以下是静态路径和动态路径的常见使用方法。 1.静态路径 在日常的开发中&#xff0c;图片的静态路径通过相对路径和绝对路径的方式引入。 相对路径&#xff1a;以.开头的&#xff0c;例如./、../之类的。就是…

MySQL主从架构

1 主从架构解决了什么问题 随着业务的持续增长&#xff0c;单体数据库满足不了业务的需求&#xff0c;可能会出现负载过重&#xff0c;操作数据库速度变慢的情况。为了解决这个问题&#xff0c;数据库一般采用一主一从、一主多从的架构。 为了操作提高效率&#xff0c;减轻压…

sql在线练习

SQLBolt - 学习 SQL - SQL 简介https://sqlbolt.com/拿走不谢&#xff01;&#xff01;&#xff01; UIUC什么乱七八糟的啊

探讨下前端测试的常见场景

前端测试 场景 这边指的测试是指白盒测试&#xff0c;用代码来测试代码。 测试有利于提升代码质量。 代码功能和需求一致。根据需求&#xff0c;写测试。测试通过了&#xff0c;则表明需求实现了。保证代码重构后&#xff0c;未改坏以前的功能。代码重构后&#xff0c;能通过…

一文告诉你样机是什么,分享几个常用的样机模板

一个项目的诞生通常需要经历头脑构思、绘制设计和最终着陆。在这个过程中&#xff0c;样机制作往往是在着陆实践之前进行的。俗话说&#xff1a;“样机使用得好&#xff0c;草稿过早”。样机设计是产品或网站最终设计的生动、静态和视觉表现。它为用户提供了一种模拟现实的方式…

信息系统项目管理师教程 第四版【第7章-项目立项管理-思维导图】

信息系统项目管理师教程 第四版【第7章-项目立项管理-思维导图】 课本里章节里所有蓝色字体的思维导图

【uniapp】短信验证码输入框

需求是短信验证码需要格子输入框 如图 网上找了一个案例改吧改吧 直接上代码 结构 <template><view class"verify-code"><!-- 输入框 --><input id"input" :value"code" class"input" :focus"isFocus"…

数据结构之树(图解)

文章目录 前言一、树是什么&#xff1f;二、树的特点三、树的相关概念四、树的表示方法&#xff08;孩子兄弟表示法&#xff09;总结 前言 在学习完线性结构&#xff0c;例如顺序表、链表、栈、队列后&#xff0c;我们要开始学习一个新的数据结构----树 一、树是什么&#xf…

小白如何在一个月写一篇论文(中文核心,SCI)

小白如何半年发3篇sci的我教你如何快速“水”一篇sci论文_哔哩哔哩_bilibili 计算机视觉&#xff0c;cv领域 半年发3篇sci的我教你如何快速“水”一篇sci论文 计算机视觉(辅导 SCI EI 核心) 微信&#xff1a;whbwqq123或主页加up 小白如何快速写出一篇论文并成功发表&…