IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 数据结构与算法 -> 2021面试题 -> 正文阅读

[数据结构与算法]2021面试题

一、Java 基础

二、集合框架

1、hashMap的解读

? ?hashMap 是一个以键值对形式存储的一个集合类。他在jdk1.7 和 jdk1.8 之间,他的实现策略有所不同,其中比较重要的两个区别就是数据结构 头插尾插

? ?在JDK1.7的时候,hashMap 采用的数据结构是数组加链表。但是到了JDK1.8 之后就是数组加链表加红黑树了。加入红黑树是为了提高他的查询效率。

? ?还有一点就是在JDK1.7之前,当我们遇到哈希碰撞,需要在链表上添加数据的时候,采用的是头插法;但是到了JDK1.8的时候,改用了尾插法。因为头插法在多线程的情况下,会导致一些问题。比如说,他会形成循环链表,因为他本身就是线程不安全的,当有多个线程线程同时走到扩容的方法时候,多个线程同时扩容,会产生一个死的 循环的 链表,循环链表的坏处就是,当我新插入一个新的节点的时候,他就会永远找不到尾结点。永远找不到尾结点就跟死循环一样,把CPU卡死,耗尽CPU性能,所以为了解决这个问题,在JDK1.8之后,改为了尾插法。

? ?接下来就以JDK1.8,聊聊 hashMap的原理。

? ?首先我们在创建hashMap的时候,根据阿里开发手册要求,让我们传入一个它的初始化容量。就是在我们预知的前提下,我们预知将来可能要插入多少条数据的情况下,我们最好能传入一个初始化容量,而且这个容量最好是一个2的次幂。

? ?当然,如果不传,这个初始化容量就是16,或者你传入了15,最后通过它底层的 tableSizeFor 方法,通过一系列的 与运算,也会得到一个距离15最近的一个2的次幂,也就是16。

? ?然后,我们在往hashMap里面添加数据的时候,就会产生两个问题。一个就是扩容的问题,还一个是树化的问题。

? ?关于扩容,在 hashMap 里面有一个成员变量,叫做加载因子,默认是 0.75。当我们插入的节点数量 >= 容量 * 加载因子。也就是默认的 >= 16 * 0.75 = 12。也就是当我们插入的数据大于等于12的时候,他就会进行一个扩容。

? ?关于树化,在源码里面也有一个成员变量,叫树化的最小容量,默认是64,也就是当这个数组容量不足64的时候,会优先选择扩容而不是树化。只有数组容量大于了 64,并且它的链表长度 >= 8,这时候才会进行树化。还有一个成员变量是 树的阈值,就是当这个树化结构 小于 6 的时候,他又会回到链表结构了。

? ? ?源码分析

/**
 * Constructs an empty <tt>HashMap</tt> with the specified initial
 * capacity and the default load factor (0.75).
 *
 * @param  initialCapacity the initial capacity.
 * @throws IllegalArgumentException if the initial capacity is negative.
 */
public HashMap(int initialCapacity) {
    this(initialCapacity, DEFAULT_LOAD_FACTOR);
}

/**
 * 再 new 一个 hashMap的时候可以传入一个初始化容量,
 * 这个再阿里巴巴的开发手册里也是要求我们去传入这个容量的。
 * 这个初始容量就是我们数组的初始默认大小(他一定是2的幂次方)
 * 因为他源码里面的一个tableSizeFor这个方法,他就会通过一系列的
 * 位移运算,返回一个最接近2的次幂的一个数,用这个作为数组容量
 * 
 * 
 * 
 * 
 */
 
// 默认的初始化容量, 1 左移 4 位  也就是 1 * 2^4 = 16 
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16 

// 最大的容量
static final int MAXIMUM_CAPACITY = 1 << 30;

// 默认的加载因子,在数组扩容的时候,当数组达到某个阈值的时候,才会扩容
// 为什么是0.75?这是他们经过大量的计算得出的最佳结果,他在当初始容量
// 乘以加载因子,当大于这个数的时候,才回去扩容
static final float DEFAULT_LOAD_FACTOR = 0.75f;

// 树化的一个阈值,就是在链表达到这个阈值的时候,才会树化,生成红黑树
// 为什么是8,因为根据泊松分布原则,到了第八个节点的时候,他的树化概率就
// 会非常的低了,因为树化节点大小是普通节点的两倍,所以我们要避免树化、
static final int TREEIFY_THRESHOLD = 8;

// 树化的一个阈值,当树形结构低于6的时候,转为链表结构
static final int UNTREEIFY_THRESHOLD = 6;

// 最小的树化容量默认是64
static final int MIN_TREEIFY_CAPACITY = 64;

? ?

三、扩容的时机

1、数组的size大于等于 初始容量*加载因子 16*0.75=12;

2、扩容的大小 <<1 左移了一位 就是乘以2倍

四、树化的时机

1、数组容量大于等于 64

2、链表的长度大于等于 8

五、树化的过程

1、把普通节点Node转换为树形节点treeNode

2、调用treeify进行树化

六、头插与尾插

1、JDK1.7采用的头插法

????????JDK1.8采用的尾插法 数据结构:加入红黑树

2、头插法的优点:

????????他的查找效率会更好,它只要进行一次hash,找到它的头结点,然后让新节点next指向头结点,把这个新节点直接赋值给table就可以的。

头插法的缺点:

????????它在多线程的情况下,有可能会产生一个循环链表,因为他本身就不是线程安全的,当有多个线程同时走到扩容的方法的时候,多个线程同时扩容,会产生一个死的循环的链表,循环链表的坏处就是,当我新插入一个新的节点的时候,他就会永远找不到尾结点,永远找不到尾结点就跟死循环一样,把我们CPU直接卡死。

尾插法需要遍历,直到找到最后一个尾结点,才能插入。

??

三、多线程、高并发

1、实现多线程的几种方法

? ? ? ? 1、继承 Thread 类 ;

? ? ? ? 2、实现 Runnable 接口;??

? ? ? ? 3、实现 Callable 接口,它是有返回值的,可以抛异常,实现是 call() 方法

? ? ? ? 4、基于线程池 创建

2、继承 Thread 类 代码

// Lock三部曲
// 1、 new ReentrantLock(); 英 /ri??entr?nt/   软安串得
// 2、 lock.lock(); // 加锁
// 3、 finally =>  lock.unlock(); // 解锁  英 /?fa?n?li/  

new Thread(()->{
    // 线程调用资源类,这里写你的业务代码
    },"A").start();
    
    
public class MyThread extends Thread {
    public void run() {
        System.out.println("MyThread.run()");
    }
}

MyThread myThread1 = new MyThread();
myThread1.start();    

3、实现 Runnable 接口 代码

// 如果自己的类已经 extends 另一个类,就无法直接 extends Thread,
// 此时,可以实现一个 Runnable 接口。
Runnable 接口。
public class MyThread extends OtherClass implements Runnable {
    public void run() {
        System.out.println("MyThread.run()");
    }
}

//启动 MyThread,需要首先实例化一个 Thread,并传入自己的 MyThread 实例:
MyThread myThread = new MyThread();
Thread thread = new Thread(myThread);
thread.start();
//事实上,当传入一个 Runnable target 参数给 Thread 后, Thread 的 run()方法就会调用
target.run()
public void run() {
    if (target != null) {
        target.run();
    }
}

4、ExecutorService、Callable、Future 有返回值的线程 代码

// 有返回值的任务必须实现 Callable 接口,类似的,无返回值的任务必须 Runnable 接口。执行
// Callable 任务后,可以获取一个 Future 的对象,在该对象上调用 get 就可以获取到 Callable 任务
// 返回的 Object 了,再结合线程池接口 ExecutorService 就可以实现传说中有返回结果的多线程了。

public class Demo01 {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        int taskSize = 10;
        //创建一个线程池
        ExecutorService pool =  Executors.newFixedThreadPool(taskSize);
        // 创建多个有返回值的任务
        List<Future> list = new ArrayList<>();
        for (int i = 0; i < taskSize; i++) {
            Callable callable= new MyCallable();
            // 执行任务并获取 Future 对象
            Future future = pool.submit(callable);
            list.add(future);
        }
        // 关闭线程池
        pool.shutdown();
        // 获取所有并发任务的运行结果
        for (Future future : list) {
            // 从 Future 对象上获取任务的返回值,并输出到控制台
            System.out.println("res = " + future.get().toString());
        }
    }
}

class MyCallable implements Callable<Integer> {
    @Override
    public Integer call() throws Exception {
        System.out.println("call()");
        return 1024;
    }
}

5、基于线程池的方式 代码

? ? ? ? 线程池:三大方法、7大参数、4种拒绝策略

线程池的好处:

????????1、降低资源的消耗

????????2、提高响应的速度

????????3、方便管理。

线程复用、可以控制最大并发数、管理线程

?5.1、线程池:三大方法

// 单个线程
ExecutorService threadPool = Executors.newSingleThreadExecutor();
// 创建一个固定的线程池的大小
ExecutorService threadPool = Executors.newFixedThreadPool(5);
// 可伸缩的,遇强则强,遇弱则弱
ExecutorService threadPool = Executors.newCachedThreadPool(); 

// Executors 工具类、3大方法
public class Demo01 {
    public static void main(String[] args) {
         // 单个线程
         ExecutorService threadPool = Executors.newSingleThreadExecutor();
         // 创建一个固定的线程池的大小
         ExecutorService threadPool = Executors.newFixedThreadPool(5);
         // 可伸缩的,遇强则强,遇弱则弱
         ExecutorService threadPool = Executors.newCachedThreadPool(); 
        
        try {
            for (int i = 0; i < 100; i++) {
                // 使用了线程池之后,使用线程池来创建线程
                threadPool.execute(()->{
                    System.out.println(Thread.currentThread().getName()+" ok");
                });
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            // 线程池用完,程序结束,关闭线程池
            threadPool.shutdown();
        }
    }
}

5.2、线程池:七大参数

  1. int corePoolSize, // 1、核心线程池大小
  2. int maximumPoolSize, // 2、最大核心线程池大小
  3. long keepAliveTime, // 3、超时了没有人调用就会释放
  4. TimeUnit unit, // 4、超时单位
  5. BlockingQueue<Runnable> workQueue, // 5、阻塞队列
  6. ThreadFactory threadFactory, // 6、线程工厂:创建线程的,一般不用动
  7. RejectedExecutionHandler handle // 7、拒绝策略

源码分析:

// 创建单一线程的线程池
public static ExecutorService newSingleThreadExecutor() {
        return new FinalizableDelegatedExecutorService
                (new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS,
                        new LinkedBlockingQueue<Runnable>()));
}
// 创建 固定线程数的线程池
public static ExecutorService newFixedThreadPool(int nThreads){
        return new ThreadPoolExecutor(5,5,
        0L,TimeUnit.MILLISECONDS,
        new LinkedBlockingQueue<Runnable>());
}
// 创建 缓存的线程池
public static ExecutorService newCachedThreadPool(){
        return new ThreadPoolExecutor(0,Integer.MAX_VALUE,
        60L,TimeUnit.SECONDS,
        new SynchronousQueue<Runnable>());
}

//本质ThreadPoolExecutor()
public ThreadPoolExecutor(
        int corePoolSize,    // 1、核心线程池大小
        int maximumPoolSize, // 2、最大核心线程池大小
        long keepAliveTime,  // 3、超时了没有人调用就会释放
        TimeUnit unit,       // 4、超时单位
        BlockingQueue<Runnable> workQueue, // 5、阻塞队列
        ThreadFactory threadFactory, // 6、线程工厂:创建线程的,一般不用动
        RejectedExecutionHandler handle // 7、拒绝策略) {
        if(corePoolSize< 0||
        maximumPoolSize<=0||
        maximumPoolSize<corePoolSize ||
        keepAliveTime< 0)
        throw new IllegalArgumentException();
        if(workQueue==null||threadFactory==null||handler==null)
        throw new NullPointerException();
        this.acc=System.getSecurityManager()==null?
        null:
        AccessController.getContext();
        this.corePoolSize=corePoolSize;
        this.maximumPoolSize=maximumPoolSize;
        this.workQueue=workQueue;
        this.keepAliveTime=unit.toNanos(keepAliveTime);
        this.threadFactory=threadFactory;
        this.handler=handler;
}

5.3、线程池:四种拒绝策略

// 队列满了,还有进来的,不处理这个,直接抛出异常 
new ThreadPoolExecutor.AbortPolicy() 
// 谁调用的再回到那个线程,由调用线程处理该任务
new ThreadPoolExecutor.CallerRunsPolicy() 
//队列满了,丢掉任务,不会抛出异常!
new ThreadPoolExecutor.DiscardPolicy() 
//队列满了,尝试去和最早的竞争,也不会抛出异常!
new ThreadPoolExecutor.DiscardOldestPolicy() 

5.4、线程池:小结与扩展

?

????????根据阿里开发手册的强制要求:线程池不予许使用 Executors 创建,而是通过 ThreadPoolExecutor 的方式,来规避资源耗尽的风险。

????????因为使用 FixedThreadPool 和 SingleThreadPool,他允许的请求队列长度为 Integer.MAX_VALUE,他的值约为 21亿。这可能会堆积大量的请求,从而导致 OOM,内存就爆了。

????????因为使用 CachedThreadPool,他允许创建的线程数量为 Integer.MAX_VALUE,值也约为 21 亿,这可能会创建大量的线程,从而导致 OOM,内存也爆掉了。

????????所以我们正常工作中用的都是 ThreadPoolExecutor,这种手动创建线程池的方式,就是写一个线程池的配置类,然后加上 @Configuration 这个注解,手写一个 ThreadPoolTaskExecutor 类。后面要用 这个类就 @Autowired一下就行。

5.5、工作中的 线程池配置类

@Configuration
public class ThreadPoolConfig
{
    // 核心线程池大小
    private int corePoolSize = 50;

    // 最大可创建的线程数
    private int maxPoolSize = 200;

    // 最大线程到底该如何定义
    // 1、CPU 密集型,几核,就是几,可以保持CPu的效率最高!
    // 2、IO 密集型 > 判断你程序中十分耗IO的线程,
    // 程序 15个大型任务 io十分占用资源!
    // 获取CPU的核数
    int cpu_number = Runtime.getRuntime().availableProcessors()
    System.out.println(cpu_number);

    // 队列最大长度
    private int queueCapacity = 1000;

    // 线程池维护线程所允许的空闲时间
    private int keepAliveSeconds = 300;

    @Bean(name = "threadPoolTaskExecutor")
    public ThreadPoolTaskExecutor threadPoolTaskExecutor()
    {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setMaxPoolSize(maxPoolSize);
        executor.setCorePoolSize(corePoolSize);
        executor.setQueueCapacity(queueCapacity);
        executor.setKeepAliveSeconds(keepAliveSeconds);
        // 线程池对拒绝任务(无线程可用)的处理策略
        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
        //队列满了,尝试去和最早的竞争,也不会抛出异常!
        //executor.setRejectedExecutionHandler(new ThreadPoolExecutor.DiscardOldestPolicy());
        return executor;
    }
}

// 调用
@Autowired
ThreadPoolConfig threadPoolConfig;

@Test
void te() {
    ThreadPoolTaskExecutor threadPoolTaskExecutor = threadPoolConfig.threadPoolTaskExecutor();
    for (int i = 0; i < 50; i++) {
        final int j = i;
        threadPoolTaskExecutor.execute(()->{
            System.out.println(Thread.currentThread().getName()+" --j = "+j+" OK");
        });
    }
}

6、如何停止一个正在运行的线程

????????1、使用 interrupt (英?/??nt??r?pt/? 英特 rua 普特)方法来中断线程

????????2、使用 抛异常 停止线程

7、notify() 和 notifyAll() 有什么区别?

????????1、notify 随机唤醒某一个wait线程,notifyAll会唤醒全部wait的线程

????????2、所以 notify 可能会造成死锁,而notifyAll 不会

8、sleep()和 wait() 有什么区别?

????????1、sleep() 属于 Thread类中的静态方法 ;而 wait() 属于object类中的成员方法。

????????2、sleep() 是线程类 Thread 的方法,他不涉及线程通信,调用的时候会暂停这个线程的指定时间,但是监控依然保持着。所以不会释放锁,只要到时间自动恢复;而 wait()是 Object类的方法,他是用于线程间的通信,调用这个wait的方法,会放弃这个对象锁,进入等待队列。等待调用notify或者notifyAll唤醒线程,他才会进入到对象锁定池,准备获得对象锁进入运行状态。

????????3、wait、notify、notifyAll,他们只能在同步方法或者同步代码块中使用;而sleep() 可以再任何地方使用。

????????4、sleep()方法必须要捕获一个中断异常(Interrupted Exception 英?/?k?sep?n/? 一可 塞普省);而 wait()、notify、notifyAll不需要捕获异常。

? ? ? ? 5、wait() 方法必须要配合 while循环使用,不能使用 if 判断,这会导致虚假唤醒。

while (number!=0){ //0
    // 等待
    this.wait();
}

? ? ? ? 5.1、如果需要精准唤醒,就需要 JUC包下的Condition(译:条件 英?/k?n?d??n/ 肯第省),

他可以精准的通知和唤醒线程

// Reentrant 译:可重入 英 /ri??entr?nt/ 瑞 安穿特
private Lock lock = new ReentrantLock();
// 创建多个 condition 做唤醒、通知
private Condition condition1 = lock.newCondition();
private Condition condition2 = lock.newCondition();
private Condition condition3 = lock.newCondition();
    // 在 while 循环里 condition1 等待
    while (number!=1){
        // 等待
        condition1.await();
    }
    // 业务代码 
    // signal (译:信号通知   英 /?s?ɡn?l/  C哥老)
    // 唤醒 2号 condition,做到精准通知
    condition2.signal();
    

9、volatile 是什么?

volatile 是 java虚拟机提供的一种轻量级的同步框架:

作用:

????????1、保证可见性

????????2、不保证原子性

????????3、禁止指令重排序????????

9.1、保证可见性

????????谈到可见性就要谈到 JMM。JMM 就是Java内存模型,他本身就是一种抽象的概,实际上并不存在。他描述的就是一种规范或规则。

在 JMM 关于同步的规定:

????????1、加锁 和 解锁 都必须是同一把锁

????????2、线程加锁前,必须读取主内存的最新值,到自己的工作内存中

????????3、线程解锁前,必须把共享变量的值刷新会主内存中

四、Spring 框架

1、Spring MVC 的执行流程

?

1、 用户发送请求至前端控制器DispatcherServlet。?

2、 DispatcherServlet收到请求调用HandlerMapping处理器映射器。

3、 处理器映射器找到具体的处理器(可以根据xml配置、注解进行查找),生成处理器对象及处理器拦截器(如果有则生成)一并返回给DispatcherServlet。

4、 DispatcherServlet调用HandlerAdapter处理器适配器。

5、 HandlerAdapter经过适配调用具体的处理器(Controller,也叫后端控制器)。

6、 Controller执行完成返回ModelAndView。

7、 HandlerAdapter将controller执行结果ModelAndView返回给DispatcherServlet。

8、 DispatcherServlet将ModelAndView传给ViewReslover视图解析器。

9、 ViewReslover解析后返回具体View。

10、DispatcherServlet根据View进行渲染视图(即将模型数据填充至视图中)。

11、 DispatcherServlet响应用户

自己的话描述:

?? ? ? ? 1、首先用户发起请求,到前段控制器,也就是 DispatcherServlet,它相当于一个转发器,负责接收请求,响应结果。也就是相当于 mvc 模式中的 c。

? ? ? ? 2、然后,DispatcherServlet 接收到请求,再去调用 HandlerMapping 也就是 处理器映射器。他就是根据 用户请求的 rul 找到对应的 Handler,就是处理器。并且在 SpringMVC 中,提供了 不同的映射器实现不同的映射方式。例如:通过配置文件,也就是 xml 文件 ;还有 实现接口方式 和 注解方式。

? ? ? ? 3、然后,这个 处理器映射器 根据相应的映射方式(xml 配置、或者注解等),找到这个具体的 Handler 处理器。生成 处理器对象,及处理器拦截器(如果有则生成)一并返回给?DispatcherServlet 。?

? ? ? ? 4、DispatcherServlet? 再去调用 HandlerAdapter 处理器适配器。(Adapter 英?/??d?pt?(r) :额大普特/ ),它的作用就是按照特定规则,也就是?HandlerAdapter 的要求执行 Handler。并且这个?HandlerAdapter 也用到了一个 适配器模式,他可以通过扩展适配器 对 更多类型的 处理器 进行执行。

? ? ? ? 5、然后,这个?HandlerAdapter 经过适配后,再去调用具体的 Handler 处理器,这就是我们所写的 Controller 层的代码了,前四部都是 SpringMVC帮我做好的。而我们这层 Controller 就叫 后端控制器,DispatcherServlet 叫做 前段控制器

? ? ? ? 6、然后,这个 Controller 执行完后,就开始放回一个 ModelAndView。

? ? ? ? 7、HandlerAdapter 再将 Controller 的执行结果,就是这个 ModelAndView 返回给?DispatcherServlet

? ? ? ? 8、DispatcherServlet 再将这个 ModelAndView 传给 ViewReslover 视图解析器。(Reslover 英[r??z?lv?] :瑞子over)。他的作用就是根据 逻辑视图名 解析成 真正的视图 View。

? ? ? ? 9、ViewReslover 解析后,返回具体的 View。

? ? ? ? 10、?DispatcherServlet 根据 View 进行 渲染视图(就是将 Model 里面的数据 填充到 View里面)

? ? ? ? 11、最后就是?DispatcherServlet ?将整个封装好的页面 响应给用户了,这就是一个完整的 SpringMVC 工作流程了。

?

?

?

?

五、MySQL 面试题

六、Redis 缓存

七、RabbitMQ 消息中间件

八、ElasticSearch 搜索引擎

九、Zookeeper + Dubbo 微服务

十、Linux 面试题

  数据结构与算法 最新文章
【力扣106】 从中序与后续遍历序列构造二叉
leetcode 322 零钱兑换
哈希的应用:海量数据处理
动态规划|最短Hamilton路径
华为机试_HJ41 称砝码【中等】【menset】【
【C与数据结构】——寒假提高每日练习Day1
基础算法——堆排序
2023王道数据结构线性表--单链表课后习题部
LeetCode 之 反转链表的一部分
【题解】lintcode必刷50题<有效的括号序列
上一篇文章      下一篇文章      查看所有文章
加:2021-08-07 12:20:33  更:2021-08-07 12:21:02 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/25 18:33:05-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码