Java开发 - 树(二叉树,二叉排序树,红黑树)

news2024/9/28 11:21:51

目录

前言

你好,认识一下,我是树

二叉树与二叉排序树

二叉排序树特点

为什么说二叉排序树查询效率要高于链表呢?

元素的类型

比较器

手写二叉排序树

定义一棵二叉树

增加元素

查询元素 

修改元素

删除元素

遍历二叉树

重写toString

二叉排序树的极端情况

平衡二叉树

红黑树

保证平衡

红黑树的规则

红黑树的应用

散列表

向散列表中存数据

hashCode()方法特点

链表的产生原因

链表存在的问题

数组的扩容

结语


前言

学习是一个渐进的过程,如果还没看过我前面写的有关数据结构的文章可以先去看完再回来看这篇,在写这篇之前,我已经预见到了这篇博客会比较长,而且概念性的东西会比较多,如果你对树真的有兴趣,想要一看究竟,那么欢迎你继续读下去,如果你只是临时起意,劝你三思。树在各个语言领域都可谓谈之色变,九成九的程序员在开发中根本就没用过树,但实际上又在不知不觉间用了。那么树是什么?树在Java开发中又有哪些代言人,让我们通过这篇博客来认识下吧。

你好,认识一下,我是树

树,我们在数据库索引的数据结构中已经给大家介绍过,为了方便大家理解,这里给没看过的小伙伴重新写一下:

Tree就是树,顾名思义,像树一样的结构,只要是树,就一定拥有四个属性:

  • 根结点:位于顶端且仅有一个
  • 高度:整个树的层次
  • 度:树中最大子节点数
  • 叶子结点:位于最底层且没有子节点,或者说度为0的节点

看到这里,想必你已经知道了什么是树,下面,我给出一张图让大家看看树的基本数据结构:

看上去很赏心悦目,这是一棵平衡二叉树,也是二叉树的一种,仅做展示,没其他意义,请不要额外联想。在写之前,我们要先明白一件事,集合中是不能添加重复元素的,先不要问,后面你就知道为什么了。

二叉树与二叉排序树

顾名思义,度为2的树就是二叉树,上面的图就是一个二叉树,那什么是二叉排序树呢?二叉排序树(Binary Sort Tree),又称二叉查找树(Binary Search Tree),亦称二叉搜索树。是数据结构中的一类。在一般情况下,查询效率比链表结构要高。如我先前所言,这里我们又提到了链表,如果看过前面文章的小伙伴可以加深对链表的理解。

二叉排序树特点

  • 元素不能重复
  • 左子树中节点均小于根结点
  • 右子树中节点均大于根节点

这一点从此图也能看的出来:

为什么说二叉排序树查询效率要高于链表呢?

如果是一个长度为n的链表,最差的结果就是查n次(单向链表,双向链表查询方式可查看前面关于链表的博客),在二叉排序树中,看图,每次比较后都会排除总量一半的数据,所以查询的效率非常高。这个二叉排序树中有7个元素,假设n为7,链表最多要查7次,这里最多也就是3次,如果数据量翻倍,差别会更大。 

元素的类型

二叉排序树中的元素的类型可以是其他的引用类型,但是他们之间要能比较大小,这就需要他们都实现comparable接口,在类中定义比较的规则,这样,对象也是可以比较大小的。

比较器

比较器分为两种:

  • comparable:内比较器,实现该接口后,需要重写内部抽象方法,在类内部定义比较规则,Collections.sort(list)就是如此
  • comparator:外比较器,比较规则定义在类的外部,通常用于在不改变类原有比较规则的情况下,使用新的/临时的比较规则,此时可以在类的外部实现该接口,定义新的比较规则。Collections.sort(list,comparator)

对集合排序有一个机制,这是前提:  sort方法内部会自动调用集合中元素的compareTo方法

没有这个前提,我们接下来讲的就没有意义!

说起来很笼统,所以我们通过代码来说明:

我们先定义一个学生类,并重写抽象方法:

public class Student implements Comparable<Student>{
    private Integer id;
    private String name;
    private Integer age;

    @Override
    public int compareTo(Student o) {
        return this.id-o.getId();
    }
}

接着我们来把学生按照id升序排列:

List<Student> list = new ArrayList<>();
for (int i=2;i<=10;i++){
    Student student = new Student(i,"student"+i,20+i);
    list.add(student);
}
Student student1 = new Student(1,"Ammy",20);
list.add(student1);
list.forEach(student -> System.out.println(student));
//对list按id升序排列
Collections.sort(list);
list.forEach(student -> System.out.println(student));

可以看到,在学生中我们已经重写了compareTo方法,这里我们就可以直接通过sort方法来处理。

上面我们采用的是内比较器,接下来我们改变规则,采用外比较器来处理:

//按age升序排列
Collections.sort(list, new Comparator<Student>() {
    @Override
    public int compare(Student o1, Student o2) {
        return o1.getAge() - o2.getAge();
    }
});
list.forEach(student -> System.out.println(student));

如果要比较name,因为name肯定是String类型,使用String默认的比较器就可以,和age是不一样的,还是写一下吧:

//按照name升序排列
Collections.sort(list, new Comparator<Student>() {
    @Override
    public int compare(Student o1, Student o2) {
        return o2.getName().compareTo(o1.getName());
    }
});
list.forEach(student -> System.out.println(student));

手写二叉排序树

接下来是我们的重点,手写二叉排序树,带大家理解排序的逻辑和原理。

定义一棵二叉树

其本质上是数据结构,是用来存储数据的,所以我们一般用泛型,让外部告知具体类型,而不能把类型写死。

public class BinarySearchTree<E extends Comparable<E>> {

}

而为了让传入的类型支持比较,必须要对泛型继承Comparable,这也是一个难点,很多人在这里容易写错。

定义完了类,那接着就需要定义根节点:

public class BinarySearchTree<E extends Comparable<E>> {
    //根节点
    private Node root;

    //定义内部类表示节点
    private class Node{
        private E ele;  //节点中保存的元素对象
        private Node left;  //左子树指向的节点
        private Node right;  //右子树指向的节点

        //定义有参构造方法,用于创建节点
        Node(E ele){
            this.ele = ele;
        }
    }
}

这时,一棵树的基本元素就定义好了,接下来就是我们所熟悉的增删改查方法,但写起来,可能并没那么简单。

增加元素

1. 判断root是否为null
	-为null,则将元素封装成节点,称为root,返回true
	-不为null,和root进行比较,比较的目的是将元素尝试添加为root的left/right子树
		- 和root相等,添加失败,返回false,结束
		- 大于>root,尝试添加为root的right;判断right是否为null,
			- 为null,则让新元素封装成节点,称为right,添加成功,返回true
			- 不为null,继续和right的节点进行比较,尝试将元素添加为right的left/right子树
		- 小于,同理

增加元素需要先判断root是否为null

  • 为null,将元素封装成根结点,返回true;
  • 非null,和root进行比较,这是为了判断新元素是添加左子树还是右子树,但不一定添加成功
    • 和root相等,那么添加失败,返回false,结束 
    • 大于root,尝试添加为root的右子树,判断right是否为null;
      • 为null,将新元素封装成节点,成为右子树right,添加完成,返回true;
      • 不为null,和right节点进行比较,尝试将该元素添加为right的left/right子树(这里递归就出现了)
    • 小于root,尝试添加为root的左子树,判断left是否为null;
      • 为null,将新元素封装成节点,成为左子树left,添加完成,返回true;
      • 不为null,和left节点进行比较,尝试将该元素添加为left的left/right子树(这里递归就出现了)

思路理清了,那我们就开始写代码:

    //添加元素
    public boolean add(E e){
        //判断root是否为null,为null,成为根节点
        if (root==null) {
            root = new Node(e);
            return true;
        }
        //root不为null,添加
        return root.append(e);
    }

Node内的添加方法:

        //向某个节点上添加给定的元素,尝试让新元素称为其左/右子树
        public boolean append(E e) {
            if (e.compareTo(ele) == 0) {
                return false;
            } else if (e.compareTo(ele) < 0) {
                if (left == null) {
                    left = new Node(e);
                    return true;
                }
                return left.append(e);
            } else {
                if (right == null) {
                    right = new Node(e);
                    return true;
                }
                return right.append(e);
            }
        }

查询元素 

查询元素也需要先判断root是否为null

  • 为null,树中没有节点,返回null;
  • 非null,判断root节点是否为需要查找的元素
    • 相等,root节点就是需要的元素,返回root节点,结束;
    • 大于ele,就到节点的right去查找相等节点,判断right节点是否存在
      • 为null,说明查询的元素不在此树中,返回null
      • 非null,判断right节点是不是要找的节点,往后一直重复上述操作,直到找到或找不到为止
    • 小于ele,就到节点的left去查找相等节点,判断left节点是否存在
      • 为null,说明查询的元素不在此树中,返回null
      • 非null,判断left节点是不是要找的节点,往后一直重复上述操作,直到找到或找不到为止

用代码来写下这个过程:

    //根据元素查询对应的节点对象
    public Node get(E e){
        //判断root是否为null
        if (root==null)
            return null;
        //root不为空,则判断root是否为目标节点
        return root.isDestNode(e);
    }

Node内的查询:

        //用于判断调用方法的节点是否为目标节点
        public Node isDestNode(E e) {
            //判断e是否和当前节点的元素相等,若相等,说明为目标节点,返回当前节点即可
            if (e.compareTo(ele)==0)
                return this;
            else if (e.compareTo(ele)>0){  //e大于当前节点,到right上继续查找
                //若right为null,说明目标元素不存在树中,返回null
                if (right==null)
                    return null;
                return right.isDestNode(e);
            }else {  //e小于当前节点,到left上继续查找
                if (left==null)
                    return null;
                return left.isDestNode(e);
            }
        }

修改元素

修改其实很简单,首先是查找,查找到之后,直接将新的元素指向那个节点。

我们可以定义这个方法如下:

set(E ele, E newEle)

修改理论上是可行的,但是在修改的时候,会不会引起重新排序呢?这对整个树结构是有影响的,代码似乎也没有刚开始说的那么简单了,重新排序对性能也有影响,所以二叉树可不提供修改方法。

删除元素

这要看删的是谁,如果删除的是叶子结点,那直接删了就好,不需要做什么判断,也不存在排序问题。一旦删除的是中间的节点,看下图:

假如删的是2或者6,这时候就要考虑谁上去的问题了。

所以删除分为三种情况:

  • 删除叶子结点:将其父节点指向它的引用置为null;
  • 删除有一棵子树的节点:将其父节点的指向变更为被删除的节点的子节点;
  • 删除有两颗子树的节点:让被删除节点的前驱节点或者后继节点上来替换此节点;
    • 前驱节点和后继节点是指升序排列后,删除节点的前一个元素叫做前驱节点,删除节点的后一个元素叫做后继节点

代码写起来会非常复杂,体验很差,所以就不写了,理解了就行,有兴趣的同学可自行尝试。

遍历二叉树

遍历的方式分为三种:

  • 先序遍历:根  左  右
  • 中序遍历:左  根  右
  • 后序遍历:左  右  根

什么意思呢?我们根据一棵树来说:

先序遍历:4,2,1,3,6,5,7(即先根,后左,再右)

中序遍历:1,2,3,4,5,6,7(即先左,后根,再右,也叫升序排列)

后序遍历:1,3,2,5,7,6 ,4(即先左,后右,再根)

重写toString

目的是为了实现输出引用,将二叉树中的元素遍历,并以此格式输出:[1,2,3,4,5,6,7]输出,没有节点,返回[]。

    //重写toString
    @Override
    public String toString() {
        //判断root是否为null,为null,返回"[]"
        if (root==null)
            return "[]";
        //进行中序遍历
        StringBuilder builder = new StringBuilder("[");
        return root.middleOrder(builder).replace(builder.length()-1,builder.length(),"]").toString();

Node内的方法:

        public StringBuilder middleOrder(StringBuilder builder) {
            //左
            if (left!=null){
                left.middleOrder(builder);
            }
            //取根的值
            builder.append(ele).append(",");
            //取右
            if (right!=null){
                right.middleOrder(builder);
            }
            //以上三步操作结束,得到遍历树后的元素拼接结果,将最后的逗号替换为]
            return builder;
        }

中序遍历这里有个递归,我们要明白的是,最终所有的代码都要取到根的值,这个节点是相对的,每个节点都会是这个根,这样才能拼接出字符串。大家可以动手尝试下其他遍历方式的写法,这里不再赘述。

二叉排序树的极端情况

这就是失衡二叉树,失衡二叉树是不希望存在的,因为它失去了二叉树本身的优势特点,这种状态和单向链表一样,单向链表的效率如何,想必大家都是知道的。

所以在设计数据结构的时候,已经考虑了这种情况,出现了两种新的数据结构,他们都基于二叉排序树,保留了其优势,又避免了这个缺点。

他们是平衡二叉树红黑树。

平衡二叉树

平衡二叉树又称为AVL树,AVL树通过旋转(左旋/右旋)来保证二叉排序树的平衡状态,AVL树会保证树的左右子树高度差始终是<=1,AVL树达到的平衡状态是绝对平衡(左右子树的高度差<=1),看下图查看旋转变化:

红黑树

红黑树是实现了自平衡的二叉排序树,但并不是平衡二叉树那样的绝对平衡,它的左右子树相差可以大于1,正如其名,所有的节点要么红色,要么黑色。

保证平衡

  • 旋转
  • 调整节点颜色
    • 根结点必须为黑色
    • 新添加的节点必然是红色,但是添加成功后由于旋转,可能会变为黑色

红黑树的规则

  • 节点为红色或者黑色
  • 根结点为黑色
  • 所有叶子结点是黑色
  • 每个红色节点的左右子树为黑色,也就是说,每个叶子结点到根结点的路径上不能出现两个连续的红色节点
  • 任意叶子结点到根节点的黑色节点数量相同,称作黑高相同

这里说明下叶子结点都为黑色的原因,图中看到的有红色,有黑色,大家应该看到我标出来的红色箭头,每个箭头默认还指着一个隐藏的黑色的为null节点 。

红黑树的应用

  • TreeMap
  • TreeSet

由于TreeSet底层调用了TreeMap,所以,我们猜测出,所有的set和map都有这样的关系:

  • HashSet/HashMap  数据结构为散列表,输出引用,得到的结果为无序结果
  • TreeSet/TreeMap  数据结构为红黑树  输出引用,得到的结果为中序遍历的结果 -- 升序排列
  • LinkedHashSet/LinkedHashMap  输出引用,得到的结果为有序的结果

所以,set集合是不可重复的集合,并不是无序的集合,实现类不同,可能就会出现有序的时候。

散列表

散列表其实算是红黑树的范畴,但是其涉及内容太多,所以就单独拉一个大标题来写一些,东西比较多,也是最后一个知识点,看到这里了,不妨继续看下去吧。

应用类为HashMap,散列表是一种数据结构,它的底层实现如下:

  • JDK1.8之前,散列表的数据结构为数组+链表
  • JDK1.8开始,散列表的数据结构为数组+链表+红黑树

向散列表中存数据

  • 根据key调用hashCode()方法,得到哈希码值;
  • 哈希码值再调用散列算法,得到一个整数,这个整数就是保存在数组中的下标;
  • 如果散列桶为空,将键值对直接存入,否则用这个准备存入的值和已经存在散列桶中的值进行比较
    • 相等,则新的value覆盖原来的value
    • 否则,在该散列桶中形成链表

hashCode()方法特点

该方法的目的是尽可能的为不同的对象分配不同的整数,其特点如下:

  • 一次程序运行期间,同一个对象多次调用次方法,哈希码值一定相等;
  • 两个对象调用equals方法结果为true,这两个方法调用hashCode()方法生成的整数一定相等;
  • 两个对象调用equals方法结果为true,这两个方法调用hashCode()方法生成的整数可能相等,也可能不同;

链表的产生原因

  • 不同的对象调用hashCode(),可能会形成相同的整数,最终导致下标一致;
  • 不同的对象调用hashCode(),可能会形成不同的整数,不同的整数再调用散列算法后得到相同的下标;

以上这种现象叫做哈希冲突或者哈希碰撞,要降低这种概率的发生,散列表可通过扩容的方式降低发生概率。

链表存在的问题

我们都知道,链表的查询效率是比较低的,若散列桶中只有一个键值对,那直接就能通过下标找到,但是若有多个的时候,通过下标找到后还需要对链表进行查询,这就降低了查询的效率。

所以我们做的就是尽可能减少链表的产生,怎么做呢?

没错,扩容!当数组中元素达到一定的数量后就对数组进行扩容,一般为原长度的两倍。所以要找一个扩容依据。

扩容因数=可保存最大数/总容量

初始状态总容量16,可保存最大数12,当第13个元素出现时,数组就扩容为原来的2倍。

数组的扩容

当数组中元素达到一定的数量后就对数组进行扩容,一般为原长度的两倍,特别注意,此时数组中的元素会重新进行散列。

散列表的初始容量为2的n次方,如果初始值给的不是2的n次方,则会自动给出相近的且大于设置值的2的n次方的值。比如给9,则容量为16,给17,容量为32。

前面说过,JDK1.8之后,散列表的数据结构变为数组+链表+红黑树,所以自JDK1.8开始,当散列表中元素数量超过64,或者散列桶中链表长度等于8时,会将链表转化为红黑树,红黑树的查询效率更高,可提高整体的查询效率。

结语

到这里,这篇博客就要和大家说再见了,基本知识点都有涵盖,如有遗漏或者讲的不对的地方,欢迎指正,也欢迎留言一起探讨数据结构,写作不易,觉得不错就点赞收藏来鼓励下博主吧。

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

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

相关文章

JAVA注解处理API实战

简介 ​ 插件化注解处理(Pluggable Annotation Processing)API JSR 269提供一套标准API来处理Annotations( JSR 175),实际上JSR 269不仅仅用来处理Annotation&#xff0c;它建立了Java 语言本身的一个模型,它把method、package、constructor、type、variable、enum、annotatio…

工业远程I/O模块 CANopen 通讯

1.对象字典OD 对象字典是每个CANopen设备必须具有的功能&#xff0c;它包含了设备所有可以被访问的参数&#xff0c;客户端使用索引(Index)和子索引(Sub-index)来读写对象字典里的参数。 对象字典主要用来设定设备组态及进行非即时的通讯。每个对象采用16位索引的方式来寻址&…

01Editor最新破解

文章目录01Editor最新版注册算法逆向1.定位注册算法代码2.整体注册算法3.Check1算法分析4.Check2算法分析5.获得正确的任意用户名对应的序列号01Editor最新版注册算法逆向 1.定位注册算法代码 【版本】13.0.164bit\textcolor{green}{【版本】13.0.1\ 64bit}【版本】13.0.1 64b…

Redis集群系列七 —— 散列插槽分析

集群状态日志分析 Redis 分片集群引入了一个逻辑上的插槽或哈希槽概念&#xff0c;将集群划分为16384&#xff08;0~16383&#xff09;个槽位&#xff0c;集群中的每个节点占据一部分槽位数&#xff0c;在逻辑上将集群中的所有节点构成了一块完整的内存空间。 这个日志中可以通…

对象定义-解构-枚举属性遍历以及对象内函数

属性名表达式 定义对象的属性有两种方式 1、直接使用标识符作为属性名 obj.name 2、以表达式作为属性名 obj[ab] 10 let obj {} obj.name 孙悟空 // 孙悟空 obj[a b] 10 // 10 console.log(obj); // {name: 孙悟空, ab: 10}es5中字面量定义对象只能使用一种方法 var …

4.3.3、划分子网的 IPv4 地址

若有一个大型的局域网需要连接到因特网 若需要申请一个 C 类网络地址&#xff0c;其可分配的 IP 地址数量只有 254254254 个&#xff0c;不够使用 因此申请了一个 B 类网络地址&#xff0c;其可分配的 IP 地址数量达到了 655346553465534 个 给每台计算机和路由器的接口分配一…

实验九、消除互补输出级交越失真方法的研究

一、题目 互补输出级交越失真消除方法的研究。 二、仿真电路 基本互补电路和消除交越失真互补输出级如图1所示。晶体管采用 NPN 型晶体管 2N3904 和 PNP 型晶体管 2N3906。二极管采用 1N4009。 在实际的实验中&#xff0c;几乎不可能得到具有理想对称性的 NPN 型和 PNP 型管…

网络编程套接字----UDP协议

文章目录前言一、理解源IP地址和目的IP地址二、认识端口号理解"端口号"和"进程ID"理解源端口号和目的端口号三、认识TCP协议四、认识UDP协议五、网络字节序六、socket编程接口socket常见APIsockaddr结构sockaddr结构sockaddr_in 结构in_addr结构七、地址转…

第三方软件测试▏有效保障软件产品质量的关键性步骤

软件测试作为软件产品生命周期中不可或缺的重要步骤&#xff0c;被许多软件企业所重视。主要是通过对软件产品进行全面的测试&#xff0c;确保软件质量以及满足用户需求。但软件测试不仅仅是个简单的检测工作&#xff0c;而是一个系统性的、有组织性的测试过程&#xff0c;包含…

Linux:安装 telnet 命令

我是 ABin-阿斌&#xff1a;写一生代码&#xff0c;创一世佳话&#xff0c;筑一览芳华。如果小伙伴们觉得不错就一键三连吧~ 声明&#xff1a;原文地址&#xff1a;https://www.pudn.com/news/6332b44a272bb74d44053074.html 其他参考文章&#xff1a;https://www.cnblogs.com…

尚医通-上传医院接口-需求准备(十七)

目录&#xff1a; &#xff08;1&#xff09;数据接口-上传医院接口-需求准备 &#xff08;1&#xff09;数据接口-上传医院接口-需求准备 在医院接口设置的时候说过&#xff0c;我们做的是预约挂号平台&#xff0c;里面有数据的显示&#xff0c;挂号等等相关业务&#xff0c;…

计算机组成原理【by王道考研计算机】

文章目录第一章1. 什么是计算机系统2. 硬件的发展3. 计算机硬件的基本组成冯诺依曼结构现代计算机结构主存储器运算器控制器工作过程实例4. 计算机系统的层次结构五层结构三种级别的语言5. 计算机的性能指标存储器的容量CPU其他常用时间单位第二章1. 进制转换2. 字符与字符串3.…

下一代,容器工具Podman

一、简介 Kubernetes 团队发布了最新的 1.20 版本时&#xff0c;有一枚重磅炸弹&#xff1a;正式宣布弃用 Docker 支持的功能。弃用 Docker 之后&#xff0c;开发者们对其替代品的讨论逐渐热烈&#xff0c;其中 Containerd 和 Podman 倍受期待。 Podman 是一个开源的容器运行…

云原生、20.3k Star......时序数据库 TDengine 的 2022 年精彩纷呈

日月其迈&#xff0c;时盛岁新 2022 的进度条已经加载至“100%” 疫情肆虐下&#xff0c;毫无疑问 这仍然是头顶风雪攀登山峰的一年 好在如今曙光已现 回望这一年&#xff0c;TDengine 也硕果满满 2022 年是 TDengine 创立的第六个年头 我们付出着&#xff0c;也在收获着…

英伟达Orin芯片平台使用TensorRT加速部署YOLO

前言 自动驾驶行业想要在开发板上部署一套算法还是需要花费很大功夫的&#xff0c;因为计算资源的限制以及实时性要求高。并不像在服务器端部署算法那样粗犷。接下来就记录一下YOLO系列部署的过程&#xff0c;后期会把开发板上的问题都记录在此。 一、部署环境 计算平台&…

11.简单的CSS按钮悬停特效

效果 源码 <!DOCTYPE html> <html> <head><title>Button Hover Effects</title><link rel="stylesheet" type="text/css" href="style.css"> </head> <body><a href="#"><…

信贷产品年终总结之风控评分模型

叮咚&#xff0c;信贷年终总结的又一个专题来了&#xff0c;作为报告总结类的系列型文章&#xff0c;近期我们番茄知识星球平台陆续发布了相关年终总结专题&#xff0c;依次为客群特征画像、贷中行为分析、贷后逾期表现等&#xff0c;以上文章可详见之前陆续发布的内容。该业务…

nacos配置部署与管理

nacos配置部署与管理配置文件配置获取顺序&#xff1a;nacos配置热部署方式一&#xff1a;RefreshScope方式二&#xff1a;ConfigurationProperties配置文件 首先新建配置文件 Data ID&#xff1a;配置文件的名称&#xff08;唯一&#xff09;命名规范&#xff1a;服务名称-运…

广州车展|继原力技术之后,长安深蓝半固态电池呼之欲出

新年将至&#xff0c;万象更新。12月30日&#xff0c;2022第二十届广州国际汽车展览会在广州中国进出口商品交易会展馆隆重举办。全新数字纯电品牌长安深蓝携旗下首款战略车型深蓝SL03重磅参展&#xff0c;并邀请深蓝SL03车主亲临车展现场&#xff0c;或进行产品讲解&#xff0…

面向对象分析与设计的底层逻辑

作者&#xff1a;高福来 阿里全球化业务平台团队 在面向对象出现之前&#xff0c;已有面向过程的分析方法&#xff0c;那为什么面向对象被提出了呢&#xff1f;究其本质&#xff0c;人们发现面向过程并非按照人正常认识事物的方式去分析软件。面向过程是一种归纳的分析方法&a…