diff --git "a/MySQL\345\246\202\344\275\225\345\256\236\347\216\260RR\351\232\224\347\246\273\347\272\247\345\210\253\344\270\213\347\232\204\351\230\262\345\271\273\350\257\273--\345\205\263\351\224\256\347\202\271\351\227\264\351\232\231\351\224\201\344\270\216MVCC.drawio" "b/MySQL\345\246\202\344\275\225\345\256\236\347\216\260RR\351\232\224\347\246\273\347\272\247\345\210\253\344\270\213\347\232\204\351\230\262\345\271\273\350\257\273--\345\205\263\351\224\256\347\202\271\351\227\264\351\232\231\351\224\201\344\270\216MVCC.drawio" new file mode 100644 index 0000000000..789e5b0b2c --- /dev/null +++ "b/MySQL\345\246\202\344\275\225\345\256\236\347\216\260RR\351\232\224\347\246\273\347\272\247\345\210\253\344\270\213\347\232\204\351\230\262\345\271\273\350\257\273--\345\205\263\351\224\256\347\202\271\351\227\264\351\232\231\351\224\201\344\270\216MVCC.drawio" @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/README.md b/README.md index e7c9e6eecb..4c0f8b42e1 100644 --- a/README.md +++ b/README.md @@ -22,63 +22,64 @@ ## :pencil2: 算法 -- [剑指 Offer 题解](https://github.com/CyC2018/CS-Notes/blob/master/notes/剑指%20Offer%20题解%20-%20目录.md) -- [Leetcode 题解](https://github.com/CyC2018/CS-Notes/blob/master/notes/Leetcode%20题解%20-%20目录.md) -- [算法](https://github.com/CyC2018/CS-Notes/blob/master/notes/算法%20-%20目录.md) +- [剑指 Offer 题解](./notes/剑指%20Offer%20题解%20-%20目录.md) +- [Leetcode 题解](./notes/Leetcode%20题解%20-%20目录.md) +- [算法](./notes/算法%20-%20目录.md) - [字节跳动内推](assets/内推.md) ## :computer: 操作系统 -- [计算机操作系统](https://github.com/CyC2018/CS-Notes/blob/master/notes/计算机操作系统%20-%20目录.md) -- [Linux](https://github.com/CyC2018/CS-Notes/blob/master/notes/Linux.md) +- [计算机操作系统](./notes/计算机操作系统%20-%20目录.md) +- [Linux](./notes/Linux.md) ## :cloud: 网络 -- [计算机网络](https://github.com/CyC2018/CS-Notes/blob/master/notes/计算机网络%20-%20目录.md) -- [HTTP](https://github.com/CyC2018/CS-Notes/blob/master/notes/HTTP.md) -- [Socket](https://github.com/CyC2018/CS-Notes/blob/master/notes/Socket.md) +- [计算机网络](./notes/计算机网络%20-%20目录.md) +- [HTTP](./notes/HTTP.md) +- [Socket](./notes/Socket.md) +- [WebSocket](./notes/WebSocket.md) ## :floppy_disk: 数据库 -- [数据库系统原理](https://github.com/CyC2018/CS-Notes/blob/master/notes/数据库系统原理.md) -- [SQL 语法](https://github.com/CyC2018/CS-Notes/blob/master/notes/SQL%20语法.md) -- [SQL 练习](https://github.com/CyC2018/CS-Notes/blob/master/notes/SQL%20练习.md) -- [MySQL](https://github.com/CyC2018/CS-Notes/blob/master/notes/MySQL.md) -- [Redis](https://github.com/CyC2018/CS-Notes/blob/master/notes/Redis.md) +- [数据库系统原理](./notes/数据库系统原理.md) +- [SQL 语法](./notes/SQL%20语法.md) +- [SQL 练习](./notes/SQL%20练习.md) +- [MySQL](./notes/MySQL.md) +- [Redis](./notes/Redis.md) ## :coffee: Java -- [Java 基础](https://github.com/CyC2018/CS-Notes/blob/master/notes/Java%20基础.md) -- [Java 容器](https://github.com/CyC2018/CS-Notes/blob/master/notes/Java%20容器.md) -- [Java 并发](https://github.com/CyC2018/CS-Notes/blob/master/notes/Java%20并发.md) -- [Java 虚拟机](https://github.com/CyC2018/CS-Notes/blob/master/notes/Java%20虚拟机.md) -- [Java I/O](https://github.com/CyC2018/CS-Notes/blob/master/notes/Java%20IO.md) +- [Java 基础](./notes/Java%20基础.md) +- [Java 容器](./notes/Java%20容器.md) +- [Java 并发](./notes/Java%20并发.md) +- [Java 虚拟机](./notes/Java%20虚拟机.md) +- [Java I/O](./notes/Java%20IO.md) ## :bulb: 系统设计 -- [系统设计基础](https://github.com/CyC2018/CS-Notes/blob/master/notes/系统设计基础.md) -- [分布式](https://github.com/CyC2018/CS-Notes/blob/master/notes/分布式.md) -- [集群](https://github.com/CyC2018/CS-Notes/blob/master/notes/集群.md) -- [攻击技术](https://github.com/CyC2018/CS-Notes/blob/master/notes/攻击技术.md) -- [缓存](https://github.com/CyC2018/CS-Notes/blob/master/notes/缓存.md) -- [消息队列](https://github.com/CyC2018/CS-Notes/blob/master/notes/消息队列.md) +- [系统设计基础](./notes/系统设计基础.md) +- [分布式](./notes/分布式.md) +- [集群](./notes/集群.md) +- [攻击技术](./notes/攻击技术.md) +- [缓存](./notes/缓存.md) +- [消息队列](./notes/消息队列.md) ## :art: 面向对象 -- [面向对象思想](https://github.com/CyC2018/CS-Notes/blob/master/notes/面向对象思想.md) -- [设计模式](https://github.com/CyC2018/CS-Notes/blob/master/notes/设计模式%20-%20目录.md) +- [面向对象思想](./notes/面向对象思想.md) +- [设计模式](./notes/设计模式%20-%20目录.md) ## :wrench: 工具 -- [Git](https://github.com/CyC2018/CS-Notes/blob/master/notes/Git.md) -- [Docker](https://github.com/CyC2018/CS-Notes/blob/master/notes/Docker.md) -- [构建工具](https://github.com/CyC2018/CS-Notes/blob/master/notes/构建工具.md) -- [正则表达式](https://github.com/CyC2018/CS-Notes/blob/master/notes/正则表达式.md) +- [Git](./notes/Git.md) +- [Docker](./notes/Docker.md) +- [构建工具](./notes/构建工具.md) +- [正则表达式](./notes/正则表达式.md) ## :watermelon: 编码实践 -- [代码可读性](https://github.com/CyC2018/CS-Notes/blob/master/notes/代码可读性.md) -- [代码风格规范](https://github.com/CyC2018/CS-Notes/blob/master/notes/代码风格规范.md) +- [代码可读性](./notes/代码可读性.md) +- [代码风格规范](./notes/代码风格规范.md) ## :memo: 后记 diff --git "a/assets/1641890623956-ani--\347\272\277\347\250\213\347\212\266\346\200\201\350\275\254\346\215\242.jpg" "b/assets/1641890623956-ani--\347\272\277\347\250\213\347\212\266\346\200\201\350\275\254\346\215\242.jpg" new file mode 100644 index 0000000000..f2680e072e Binary files /dev/null and "b/assets/1641890623956-ani--\347\272\277\347\250\213\347\212\266\346\200\201\350\275\254\346\215\242.jpg" differ diff --git a/notes/Java IO.md b/notes/Java IO.md index d95eee4f9f..b0484676f0 100644 --- a/notes/Java IO.md +++ b/notes/Java IO.md @@ -28,6 +28,7 @@ * [选择器](#选择器) * [套接字 NIO 实例](#套接字-nio-实例) * [内存映射文件](#内存映射文件) + * [待补充AIO] * [对比](#对比) * [八、参考资料](#八参考资料) diff --git "a/notes/Java \345\271\266\345\217\221.md" "b/notes/Java \345\271\266\345\217\221.md" index cb01195074..30ebfc4c87 100644 --- "a/notes/Java \345\271\266\345\217\221.md" +++ "b/notes/Java \345\271\266\345\217\221.md" @@ -11,6 +11,7 @@ * [Daemon](#daemon) * [sleep()](#sleep) * [yield()](#yield) + * [线程通信方式](#线程通信方式) * [三、中断](#三中断) * [InterruptedException](#interruptedexception) * [interrupted()](#interrupted) @@ -149,12 +150,12 @@ public static void main(String[] args) { ### Executor -Executor 管理多个异步任务的执行,而无需程序员显式地管理线程的生命周期。这里的异步是指多个任务的执行互不干扰,不需要进行同步操作。 +Executors 管理多个异步任务的执行,而无需程序员显式地管理线程的生命周期。这里的异步是指多个任务的执行互不干扰,不需要进行同步操作。 主要有三种 Executor: - CachedThreadPool:一个任务创建一个线程; -- FixedThreadPool:所有任务只能使用固定大小的线程; +- FixedThreadPool:所有任务只能使用固定大小的线程,但是线程池默认使用了无界队列; - SingleThreadExecutor:相当于大小为 1 的 FixedThreadPool。 ```java @@ -210,6 +211,42 @@ public void run() { } ``` +### 线程通信方式 + +(来自https://www.nowcoder.com/exam/interview/75826387/test?paperId=50270024) + +在Java中,常用的线程通信方式有两种,分别是利用**Monitor实现**线程通信、利用**Condition实现**线程通信。 + +线程同步是线程通信的前提,所以究竟采用哪种方式实现通信,取决于线程同步的方式。 如果是采用**synchronized**关键字进行同步,则需要依赖**Monitor**(同步监视器)实现线程通信,Monitor就是锁对象。在synchronized同步模式下,锁对象可以是任意的类型,所以通信方法自然就被定义在Object类中了,这些方法包括:wt()、notify()、notifyAll()。一个线程通过Monitor调用wt()时,它就会释放锁并在此等待。当其他线程通过Monitor调用notify()时,则会唤醒在此等待的一个线程。当其他线程通过Monitor调用notifyAll()时,则会唤醒在此等待的所有线程。 + +JDK1.5新增了Lock接口及其实现类,提供了更为灵活的同步方式。如果是采用**Lock对象**进行同步,则需要依赖**Condition**实现线程通信,Condition对象是由Lock对象创建出来的,它依赖于Lock对象。Condition对象中定义的通信方法,与Object类中的通信方法类似,它包括awt()、signal()、signalAll()。通过名字就能看出它们的含义了,当通过Condition调用awt()时当前线程释放锁并等待,当通过Condition调用signal()时唤醒一个等待的线程,当通过Condition调用signalAll()时则唤醒所有等待的线程。 + +加分回答--线程同步是基于同步队列实现的,而线程通信是基于等待队列实现的。当调用等待方法时,即将当前线程加入等待队列。当调用通知方法时,即将等待队列中的一个或多个线程转移回同步队列。因为synchronized只有一个Monitor,所以它就只有一个等待队列。而Lock对象可以创建出多个Condition,所以它拥有多个等待队列。多个等待队列带来了极大的灵活性,所以基于Condition的通信方式更为推荐。 比如,在实现生产消费模型时,生产者要通知消费者、消费者要通知生产者。相反,不应该出现生产者通知生产者、消费者通知消费者这样的情况。如果使用synchronized实现这个模型,由于它只有一个等待队列,所以只能把生产者和消费者加入同一个队列,这就会导致生产者通知生产者、消费者通知消费者的情况出现。采用Lock实现这个模型时,由于它有多个等待队列,可以有效地将这两个角色区分开,就能避免出现这样的问题。 + + + +### **关于线程池** +【线程池的意义】 +线程池可以有效地管理线程:它可以**管理线程的数量**,可以避免无节制的创建线程,导致超出系统负荷直至崩溃。它还可以**让线程复用**,可以大大地减少创建和销毁线程所带来的开销。 +【**线程池的核心参数**】 +线程池需要依赖一些参数来控制任务的执行流程,其中最重要的参数有:corePoolSize(核心线程数)、workQueue(等待队列)、maximunPoolSize(最大线程数)、handler(拒绝策略)、keepAliveTime(空闲线程存活时间)。 +【线程池的运作步骤】 +当我们向线程池提交一个任务之后,线程池按照如下步骤处理这个任务: + + 1. 判断线程数是否达到corePoolSize,若没有则新建线程执行该任务,否则进入下一步。 + 2. 判断等待队列是否已满,若没有则将任务放入等待队列,否则进入下一步。 + 3. 判断线程数是否达到maximunPoolSize,如果没有则新建线程执行任务,否则进入下一步。 + 4. 采用初始化线程池时指定的拒绝策略,拒绝执行该任务。 + 5. 新建的线程处理完当前任务后,不会立刻关闭,而是继续处理等待队列中的任务。如果线程的空闲时间达到了keepAliveTime,则线程池会销毁一部分线程,将线程数量收缩至corePoolSize。 第2步中的队列可以有界也可以无界。若指定了无界的队列,则线程池永远无法进入第3步,相当于废弃了maximunPoolSize参数。这种用法是十分危险的,如果任务在队列中产生大量的堆积,就很容易造成内存溢出。 + +【线程池池创建工具--建议用**ThreadPoolExecutor**】 + JDK为我们提供了一个名为Executors的线程池的创建工具,该工具创建出来的就是带有无界队列的线程池,所以一般在工作中我们是不建议使用这个类来创建线程池的。 第4步中的拒绝策略主要有4个:让调用者自己执行任务、直接抛出异常、丢弃任务不做任何处理、删除队列中最老的任务并把当前任务加入队列。这4个拒绝策略分别对应着RejectedExecutionHandler接口的4个实现类,我们也可以基于这个接口实现自己的拒绝策略。 在Java中,线程池的实际类型为ThreadPoolExecutor,它提供了线程池的常规用法。该类还有一个子类,名为ScheduledThreadPoolExecutor,它对定时任务提供了支持。在子类中,我们可以周期性地重复执行某个任务,也可以延迟若干时间再执行某个任务。 +在Java中正常创建线程池应该使用ThreadPoolExecutor类。虽然Executors类可以方便地创建线程池,但它使用的默认参数可能不适合所有情况。例如,Executors.newFixedThreadPool方法创建的线程池默认使用无界队列,可能会导致内存溢出。此外,Executors类隐藏了线程池的细节,使得无法对其进行更灵活的配置和优化。因此,建议使用ThreadPoolExecutor类手动创建线程池,并根据具体情况进行参数配置。 + +【线程池的生命周期状态】 线程池的生命周期包含5个状态:RUNNING、SHUTDOWN、STOP、TIDING、TERMINATED。这5种状态的状态值分别是:-1、0、1、2、3。在线程池的生命周期中,它的状态只能由小到大迁移,是不可逆的。 1. RUNNING:表示线程池正在运行。 2. SHUTDOWN:执行shutdown()时进入该状态,此时队列不会清空,线程池会等待任务执行完毕。 3. STOP:执行shutdownNow()时进入该状态,此时现线程池会清空队列,不再等待任务的执行。 4. TIDING:当线程池及队列为空时进入该状态,此时线程池会执行钩子函数,目前该函数是一个空的实现。 5. TERMINATED:钩子函数执行完毕后,线程进入该状态,表示线程池已经死亡。 +(来自https://www.nowcoder.com/exam/interview/75826387/test?paperId=50270024) + + ## 三、中断 一个线程执行完毕之后会自动结束,如果在运行过程中发生异常也会提前结束。 @@ -334,6 +371,7 @@ future.cancel(true); Java 提供了两种锁机制来控制多个线程对共享资源的互斥访问,第一个是 JVM 实现的 synchronized,而另一个是 JDK 实现的 ReentrantLock。 + ### synchronized **1. 同步一个代码块** @@ -489,6 +527,15 @@ public static void main(String[] args) { ### 比较 +| **比较点** | **synchronized** | **ReentrantLock** | +| ------------------------------------------------------------ | ------------------------------------------------------------ | ---------------------------------------------- | +| **锁的实现** | JVM实现 | JDK实现 | +| **性能** | 新版本Java已优化很多,例如自旋锁等,性能与ReentrantLock大致相同 | 与ReentrantLock大致相同 | +| **等待是否可中断**(当持有锁的线程长期不释放锁的时候,正在等待的线程可以选择放弃等待,改为处理其他事情。) | 不可中断 | 可中断 | +| **公平锁**(指多个线程在等待同一个锁时,必须按照申请锁的时间顺序来依次获得锁) | 非公平 | 默认非公平,但也可以是公平的 | +| **锁绑定多个条件** | | 一个ReentrantLock可以同时绑定多个Condition对象 | + + **1. 锁的实现** @@ -726,9 +773,29 @@ after [Java SE 9 Enum Thread.State](https://docs.oracle.com/javase/9/docs/api/java/lang/Thread.State.html) -## 七、J.U.C - AQS +![线程状态转换图](..\assets\1641890623956-ani--线程状态转换.jpg) + +## 七、J.U.C - AQS(AbstractQueuedSynchornizer) +**关于AQS** +AbstractQueuedSynchronizer)是**队列同步器,是用来构建锁的基础框架**,Lock实现类都是基于AQS实现的。AQS是基于**模板方法模式进行设计的,所以锁的实现需要继承AQS并重写它指定的方法**。A**QS内部定义了一个FIFO的队列来实现线程的同步,同时还定义了同步状态来记录锁的信息**。 AQS的模板方法,将管理同步状态的逻辑提炼出来形成标准流程,这些方法主要包括:独占式获取同步状态、独占式释放同步状态、共享式获取同步状态、共享式释放同步状态。以独占式获取同步状态为例,它的大致流程是: + + 1. 尝试以独占方式获取同步状态。 + + 2. 如果状态获取失败,则将当前线程加入同步队列。 + + 3. 自旋处理同步状态,如果当前线程位于队头,则唤醒它并让它出队,否则使其进入阻塞状态。 其中,有些步骤无法在父类确定,则提炼成空方法留待子类实现。例如,第一步的尝试操作,对于公平锁和非公平锁来说就不一样,所以子类在实现时需要按照场景各自实现这个方法。 AQS的同步队列,是一个双向链表,AQS则持有链表的头尾节点。对于尾节点的设置,是存在多线程竞争的,所以采用CAS的方式进行修改。对于头节点设置,则一定是拿到了同步状态的线程才能处理,所以修改头节点不需要采用CAS的方式。 AQS的同步状态,是一个int类型的整数,它在表示状态的同时还能表示数量。通常情况下,状态为0时表示无锁,状态大于0时表示锁的重入次数。另外,在读写锁的场景中,这个状态标志既要记录读锁又要记录写锁。于是,锁的实现者就将状态表示拆成高低两部分,高位存读锁、低位存写锁。 加分回答 同步状态需要在并发环境下修改,所以需要保证其线程安全。由于AQS本身就是锁的实现工具,所以不适合用锁来保证其线程安全,因为如果你用一个锁来定义另一个锁的话,那干脆直接用synchronized算了。实际上,同步状态是被volatile修饰的,该关键字可以保证状态变量的内存可见性,从而解决了线程安全问题。(来自https://www.nowcoder.com/exam/interview/75826387/test?paperId=50270024) java.util.concurrent(J.U.C)大大提高了并发性能,AQS 被认为是 J.U.C 的核心。 + 1. **原子类** 从JDK 1.5开始,并发包下提供了atomic子包,这个包中的原子操作类提供了一种用法简单、性能高效、线程安全地更新一个变量的方式。在atomic包里一共提供了17个类,属于4种类型的原子更新方式,分别是原子更新基本类型、原子更新引用类型、原子更新属性、原子更新数组。 + +2. **锁** 从JDK 1.5开始,并发包中新增了Lock接口以及相关实现类用来实现锁功能,它提供了与synchronized关键字类似的同步功能,只是在使用时需要显式地获取和释放锁。虽然它缺少了隐式获取释放锁的便捷性,但是却拥有了多种synchronized关键字所不具备的同步特性,包括:**可中断地获取锁**、**非阻塞地获取锁**、**可超时地获取锁**。 + +3. **线程池** 从JDK 1.5开始,并发包下新增了内置的线程池。其中,**ThreadPoolExecutor**类代表常规的线程池,而它的子类ScheduledThreadPoolExecutor对定时任务提供了支持,在子类中我们可以周期性地重复执行某个任务,也可以延迟若干时间再执行某个任务。此外,Executors是一个用于创建线程池的工具类,由于该类创建出来的是带有无界队列的线程池,所以在使用时要慎重。 + +4. **并发容器** 从JDK 1.5开始,并发包下新增了大量高效的并发的容器,这些容器按照实现机制可以分为三类。第一类是以**降低锁粒度来**提高并发性能的容器,它们的类名以Concurrent开头,如ConcurrentHashMap。第二类是采用**写时复制技术实**现的并发容器,它们的类名以CopyOnWrite开头,如CopyOnWriteArrayList。第三类是采用**Lock实现的阻塞队列,内部创建两个Condition分别用于生产者和消费者的等待**,这些类都实现了BlockingQueue接口,如ArrayBlockingQueue。 + +5. **同步工具** 从JDK 1.5开始,并发包下新增了几个有用的并发工具类,一样可以保证线程安全。其中,Semaphore类代表信号量,可以控制同时访问特定资源的线程数量;CountDownLatch类则允许一个或多个线程等待其他线程完成操作;CyclicBarrier可以让一组线程到达一个屏障时被阻塞,直到最后一个线程到达屏障时,屏障才会打开,所有被屏障拦截的线程才会继续运行。 +(来自https://www.nowcoder.com/exam/interview/75826387/test?paperId=50270024) ### CountDownLatch @@ -814,6 +881,13 @@ public class CyclicBarrierExample { before..before..before..before..before..before..before..before..before..before..after..after..after..after..after..after..after..after..after..after.. ``` +### Exchanger +Java中的Exchanger类是一个同步工具类,用于在两个线程之间交换数据。它提供了一个exchange()方法,当两个线程都调用该方法时,它们会被阻塞,直到彼此都调用了该方法,然后交换数据并返回。 + +Exchanger类的实现原理是基于AQS(AbstractQueuedSynchronizer)同步器。当一个线程调用exchange()方法时,它会尝试获取同步器的锁,如果同步器的状态为0,则表示没有其他线程在等待交换数据,该线程会将自己加入到同步器的等待队列中,并阻塞自己。当另一个线程也调用了exchange()方法时,它会尝试获取同步器的锁,如果同步器的状态为1,则表示有一个线程在等待交换数据,该线程会将自己从等待队列中取出,并与另一个线程交换数据,然后释放同步器的锁,使得两个线程都可以继续执行。 + +需要注意的是,Exchanger类只能用于两个线程之间交换数据,如果需要多个线程之间交换数据,可以使用CyclicBarrier或CountDownLatch等其他同步工具类。 + ### Semaphore Semaphore 类似于操作系统中的信号量,可以控制对互斥资源的访问线程数。 diff --git "a/notes/Java\344\270\216C++\345\220\216\347\253\257\345\274\200\345\217\221.md" "b/notes/Java\344\270\216C++\345\220\216\347\253\257\345\274\200\345\217\221.md" new file mode 100644 index 0000000000..3f50ca03db --- /dev/null +++ "b/notes/Java\344\270\216C++\345\220\216\347\253\257\345\274\200\345\217\221.md" @@ -0,0 +1,80 @@ +# 关键字 + +| 关键字 | Java | C++ | +| --------------------- | ------------------------------------------------------------ | ------------------------------------------------------------ | +| **volatile** | 它表示**可见性**。即被该关键字修改的变量的读操作一定在该变量的写更新后。 | (1)**易变性**--在汇编层面来看,即读取该变量的值时直接从内存中读取,而不是从寄存器。
(2)"**不可优化**"特性。
(3)"**顺序性**"--保证volatile变量间的顺序性,编译器不会进行乱序优化。 | +| **static** | (1)修饰类中成员变量。
(2)修饰类中方法。表示该方法属于整个类而不属于类的特定实例
(3)修饰代码块。 | 控制变更的**存储方式和可见性**
(1)修饰局部变量——原本局部变量是存放在栈区的,并且局部变量的生命周期在该语句块执行结束时便结束了。但是如果**用static进行修饰的话,该变量便存放在静态数据区,其生命周期一直持续到整个程序执行结束**。但是在这里要注意的是,虽然用static对局部变量进行修饰过后,其生命周期以及存储空间发生了变化,但是其作用域并没有改变,其仍然是一个局部变量,作用域仅限于该语句块。
(2)修饰全局变量——原本对于一个全局变量,它既可以在本源文件中被访问到,也可以在同一个工程的其它源文件中被访问(只需用extern进行声明即可)。用static对全局变量进行修饰**改变了其作用域的范围,由原来的整个工程可见变为本源文件可见**。
(3)修饰函数。——与修饰全局变量大同小异,就是改变了函数的作用域。
(4)C++中的static(中的某个函数用static进行修饰,则表示该函数属于一个类而不是属于此类的任何特定对象;如果对类中的某个变量进行static修饰,表示该变量为类以及其所有的对象所有。它们在存储空间中都只存在一个副本。可以通过类和对象去调用。 ) | +| **const**(常量限定符) | (1)修饰**类**。——表明该类不能被继承
(2)修饰对象。——表明该对象引用不可修改,当然对象的值依然可被修改
(3) | const名叫常量限定符,用来限定特定变量,以通知编译器该变量是不可修改的。习惯性的使用const,可以避免在函数中对某些不应修改的变量造成可能的改动。

(1)const修饰基本数据类型

1.const修饰一般常量及数组

基本数据类型,修饰符const可以用在类型说明符前,也可以用在类型说明符后,其结果是一样的。在使用这些常量的时候,只要不改变这些常量的值便好。

2.const修饰指针变量*及引用变量&

如果const位于星号*的左侧,则const就是用来修饰指针所指向的变量,即指针指向为常量;

如果const位于星号的右侧,const就是修饰指针本身,即指针本身是常量。

(2)const应用到函数中,

1.作为参数的const修饰符

调用函数的时候,用相应的变量初始化const常量,则在函数体中,按照const所修饰的部分进行常量化,保护了原对象的属性。
[注意]:参数const通常用于参数为指针或引用的情况;

2.作为函数返回值的const修饰符

声明了返回值后,const按照"修饰原则"进行修饰,起到相应的保护作用。

(3)const在类中的用法

不能在类声明中初始化const数据成员。正确的使用const实现方法为:const数据成员的初始化只能在类构造函数的初始化表中进行
类中的成员函数:A fun4()const; 其意义上是不能修改所在类的的任何变量。

(4)const修饰类对象,定义常量对象
常量对象只能调用常量函数,别的成员函数都不能调用。

http://www.cnblogs.com/wintergrass/archive/2011/04/15/2015020.html | +| **extern** | —— | 在C语言中,修饰符extern用在变量或者函数的声明前,用来说明“此变量/函数是在别处定义的,要在此处引用”。

注意extern声明的位置对其作用域也有关系,如果是在main函数中进行声明的,则只能在main函数中调用,在其它函数中不能调用。其实要调用其它文件中的函数和变量,只需把该文件用#include包含进来即可,为啥要用extern?因为用extern会加速程序的编译过程,这样能节省时间。

在C++中extern还有另外一种作用,用于指示C或者C++函数的调用规范。比如在C++中调用C库函数,就需要在C++程序中用extern “C”声明要引用的函数。这是给链接器用的,告诉链接器在链接的时候用C函数规范来链接。主要原因是C++和C程序编译完成后在目标代码中命名规则不同,用此来解决名字匹配的问题。 | + +#### C/C++特有---宏定义和展开、内联函数区别 + + **内联函数是代码被插入到调用者代码处的函数**。如同 #define 宏,内联函数通过避免被调用的开销来**提高执行效率**,尤其是它能够通过调用(“过程化集成”)被编译器优化。 **宏定义不检查函数参数,返回值什么的,只是展开,相对来说,内联函数会检查参数类型,所以更安全。** 内联函数和宏很类似,而区别在于,**宏是由预处理器对宏**进行替代,而**内联函数是通过编译器控制**来实现的。而且内联函数是真正的函数,只是在需要用到的时候,内联函数像宏一样的展开,所以取消了函数的参数压栈,减少了调用的开销。 + +宏是预编译器的输入,然后宏展开之后的结果会送去编译器做语法分析。宏与函数等处于不同的级别,操作不同的实体。宏操作的是 token, 可以进行 token的替换和连接等操作,在语法分析之前起作用。而函数是语言中的概念,会在语法树中创建对应的实体,内联只是函数的一个属性。 +对于问题:有了函数要它们何用?答案是:一:函数并不能完全替代宏,有些宏可以在当前作用域生成一些变量,函数做不到。二:内联函数只是函数的一种,内联是给编译器的提示,告诉它最好把这个函数在被调用处展开,省掉一个函数调用的开销(压栈,跳转,返回) + +内联函数也有一定的局限性。就是函数中的执行代码不能太多了,如果,内联函数的函数体过大,一般的编译器会放弃内联方式,而采用普通的方式调用函数。这样,内联函数就和普通函数执行效率一样 + +内联函数必须是和函数体申明在一起,才有效。 + +[宏定义和内联函数区别](http://www.cnblogs.com/chengxuyuancc/archive/2013/04/04/2999844.html) + +# STL原理及实现 + +> Java中集合类(接口collection的子类)、map接口 + +STL提供六大组件,彼此可以组合套用: + +> 1、容器(Containers):各种数据结构,如:序列式容器vector、list、deque、关联式容器set、map、multiset、multimap。用来存放数据。从实现的角度来看,STL容器是一种class template。 + +> 2、算法(algorithms):各种常用算法,如:sort、search、copy、erase。从实现的角度来看,STL算法是一种 function template。注意一个问题**:任何的一个STL算法,都需要获得由一对迭代器所标示的区间,用来表示操作范围**。这一对迭代器所标示的区间都是前闭后开区间,例如[first, last) + +> 3、迭代器(iterators):容器与算法之间的胶合剂,**是所谓的“泛型指针”**。共有五种类型,以及其他衍生变化。从实现的角度来看,迭代器是一种将 operator*、operator->、operator++、operator- - 等指针相关操作进行重载的class template。所有STL容器都有自己专属的迭代器,只有容器本身才知道如何遍历自己的元素。原生指针(native pointer)也是一种迭代器。 + +> 4、仿函数(functors):行为类似函数,可作为算法的某种策略(policy)。从实现的角度来看,仿函数是一种重载了operator()的class或class template。一般的函数指针也可视为狭义的仿函数。 + +> 5、配接器(adapters):一种用来修饰容器、仿函数、迭代器接口的东西。例如:STL提供的queue 和 stack,虽然看似容器,但其实只能算是一种容器配接器,因为它们的底部完全借助deque,所有操作都由底层的deque供应。改变 functors接口者,称为function adapter;改变 container 接口者,称为container adapter;改变iterator接口者,称为iterator adapter。 + +> 6、配置器(allocators):负责空间配置与管理。从实现的角度来看,**配置器是一个实现了动态空间配置、空间管理、空间释放的class template**。 + +这六大组件的交互关系:container(容器) 通过 allocator(配置器) 取得数据储存空间,algorithm(算法)通过 iterator(迭代器)存取 container(容器) 内容,functor(仿函数) 可以协助 algorithm(算法) 完成不同的策略变化,adapter(配接器) 可以修饰或套接 functor(仿函数) + +序列式容器: +vector-数组,元素不够时再重新分配内存,拷贝原来数组的元素到新分配的数组中。 +list-单链表。 +deque-分配中央控制器map(并非map容器),map记录着一系列的固定长度的数组的地址.记住这个map仅仅保存的是数组的地址,真正的数据在数组中存放着.deque先从map中央的位置(因为双向队列,前后都可以插入元素)找到一个数组地址,向该数组中放入数据,数组不够时继续在map中找空闲的数组来存数据。当map也不够时重新分配内存当作新的map,把原来map中的内容copy的新map中。所以使用deque的复杂度要大于vector,尽量使用vector。 + +stack-基于deque。 +queue-基于deque。 +heap-完全二叉树,使用最大堆排序,以数组(vector)的形式存放。 +priority_queue-基于heap。 +slist-双向链表。 + +关联式容器: +set,map,multiset,multimap-基于红黑树(RB-tree),一种加上了额外平衡条件的二叉搜索树。 + +hash table-散列表。将待存数据的key经过映射函数变成一个数组(一般是vector)的索引,例如:数据的key%数组的大小=数组的索引(一般文本通过算法也可以转换为数字),然后将数据当作此索引的数组元素。有些数据的key经过算法的转换可能是同一个数组的索引值**(碰撞问题,可以用线性探测,二次探测来解决**),STL是用开链的方法来解决的,每一个数组的元素维护一个list,他把相同索引值的数据存入一个list,这样当list比较短时执行删除,插入,搜索等算法比较快。 + +hash_map,hash_set,hash_multiset,hash_multimap-基于hashtable。 + +[STL六大组件] (http://blog.csdn.net/chenguolinblog/article/details/30336805) +什么是“标准非STL容器”? + +list和vector有什么区别? + +> vector拥有一段连续的内存空间,因此支持随机存取,如果需要高效的随即存取,而不在乎插入和删除的效率,使用vector。 +> list拥有一段不连续的内存空间,因此不支持随机存取,如果需要大量的插入和删除,而不关心随即存取,则应使用list。 + +# 虚函数 + +C++的多态分为静态多态(编译时多态)和动态多态(运行时多态)两大类。静态多态通过重载、模板来实现;动态多态就是通过本文的主角虚函数来体现的。 + +虚函数实现原理:包括虚函数表、虚函数指针等 + +虚函数的作用说白了就是:当调用一个虚函数时,被执行的代码必须和调用函数的对象的动态类型相一致。编译器需要做的就是如何高效的实现提供这种特性。不同编译器实现细节也不相同。大多数编译器通过vtbl(virtual table)和vptr(virtual table pointer)来实现的。 当一个类声明了虚函数或者继承了虚函数,这个类就会有自己的vtbl。vtbl实际上就是一个函数指针数组,有的编译器用的是链表,不过方法都是差不多。vtbl数组中的每一个元素对应一个函数指针指向该类的一个虚函数,同时该类的每一个对象都会包含一个vptr,vptr指向该vtbl的地址。 + + + +C/C++的具体总结可看[cpp-backend-reference/back-end.md at master · chankeh/cpp-backend-reference (github.com)](https://github.com/chankeh/cpp-backend-reference/blob/master/back-end.md) + diff --git a/notes/Jenkins.md b/notes/Jenkins.md new file mode 100644 index 0000000000..abdbe14afc --- /dev/null +++ b/notes/Jenkins.md @@ -0,0 +1,43 @@ +按Git的学习之旅的安排 + +# Jenkins简介 + +Java开发的开源软件--一定需要**jdk环境** + +持续集成工具,用于监控持续重复的工作--假设没有Jenkins的话会怎样--**手动**build、**手动**部署? + +官网: https://jenkins.io + +# Jenkins的下载与安装 + +必须要java8。 + +jenkins.msi---Windows平台中,直接双击安装 ,比较方便。 + +jenkins.war---web项目,java -jar jenkins.war或直接放到tomcat等web容器的web-app下面,启动tomcat. + +安装配置默认插件、配置等。 + + ![image-20231128105304073](..\assets\image-20231128105304073--Jenkins.png) + +# Jenkins的工作流程 + +1. 如何帮助我们自动完成这些工作的呢---->Jenkins的工作流程 + +![image-20231128110758572](..\assets\image-20231128110758572--Jenkins工作流.png) + +![image-20231128112504570](..\assets\image-20231128112504570--Jenkins-work.png) + +1. + +2. 拉取代码机制 + + > 让Jenkins任务对应一个仓,到该拉取代码。 + +3. 项目构建与发布 + + + +4. 自动集成发布--不需要手动让Jenkins去拉取、构建和部署。 + + diff --git a/notes/Kubernetes.md b/notes/Kubernetes.md new file mode 100644 index 0000000000..507177a088 --- /dev/null +++ b/notes/Kubernetes.md @@ -0,0 +1,78 @@ +[TOC] + +# Kubernetes + +多节点之间的container管理 & 编排。 + +https://docs.docker.com/get-started/overview/ + + + +相同功能的有docker swarm、mesos。 + +## POD + +一个或多个容器的最小操作单元 + +## ReplicaSet + +通过一组字段。维护和管理POD。 + +may never need to manupulate ReplicaSet,use Deployment instead + +![image-20231130152959672](C:\Users\h00284904\Documents\ForWork\学习\kubernetes\image-20231130152959672--ReplicaSet及Deployment.png) + +## Label + +标签和选择算符 + +## Service + +![image-20231130153923991](C:\Users\h00284904\Documents\ForWork\学习\kubernetes\image-20231130153923991--Service.png) + + + +## node + +Kubernetes中的工作节点。 + +![image-20231130154534477](C:\Users\h00284904\Documents\ForWork\学习\kubernetes\image-20231130154534477--node和Kubernetes集群.png) + +![image-20231130154628846](C:\Users\h00284904\Documents\ForWork\学习\kubernetes\image-20231130154628846--Kubernetes集群架构.png) + +kubectl: k8s集群的入口 创建poid 各种资源 kubectl ---> kubectl create pod nginx-pod --image=nginx:latest + +master 认证授权: 对于整个k8s集群是否有操作权限,或者有什么样的权限 + +master节点 api server: 接受kubectl的命令,检查语法是否有错误, + +scheduler: 调度, 判断w1\w2 …节点是否满足pod资源分配要求(带宽、内存、CPU) + +> 说明: 集群中,master节点一般不承担创建业务的pod.系统pod一般都创建在master节点上。 + + + +## 搭建K8s集群 + + + +## K8s存储相关的内容及进阶 + +``` sh +exportfs及exportfs -r + +systemctl restart rpcbind & systemctl enable rpcbind + +showmount +``` + + + + + +https://note.youdao.com/s/CPsWTGBI + +CI/CD + +Docker与Kubernetes + diff --git a/notes/Linux.md b/notes/Linux.md index c11905d24d..72e91df4c2 100644 --- a/notes/Linux.md +++ b/notes/Linux.md @@ -1063,6 +1063,38 @@ $ printf '%10s %5i %5i %5i %8.2f \n' $(cat printf.txt) 是由 Alfred Aho,Peter Weinberger 和 Brian Kernighan 创造,awk 这个名字就是这三个创始人名字的首字母。 awk 每次处理一行,处理的最小单位是字段,每个字段的命名方式为:\$n,n 为字段号,从 1 开始,\$0 表示一整行。 +```sh +Usage: awk [POSIX or GNU style options] [--] 'program' file ... +POSIX options: GNU long options: (standard) + -f progfile --file=progfile + -F fs --field-separator=fs + -v var=val --assign=var=val +Short options: GNU long options: (extensions) + -b --characters-as-bytes + -c --traditional + -C --copyright + -d[file] --dump-variables[=file] + -D[file] --debug[=file] + -e 'program-text' --source='program-text' + -E file --exec=file + -g --gen-pot + -h --help + -i includefile --include=includefile + -l library --load=library + -L[fatal|invalid|no-ext] --lint[=fatal|invalid|no-ext] + -M --bignum + -N --use-lc-numeric + -n --non-decimal-data + -o[file] --pretty-print[=file] + -O --optimize + -p[file] --profile[=file] + -P --posix + -r --re-interval + -s --no-optimize + -S --sandbox + -t --lint-old + -V --version +``` 示例:取出最近五个登录用户的用户名和 IP。首先用 last -n 5 取出用最近五个登录用户的所有信息,可以看到用户名和 IP 分别在第 1 列和第 3 列,我们用 \$1 和 \$3 就能取出这两个字段,然后用 print 进行打印。 @@ -1098,6 +1130,16 @@ root 0 bin 1 daemon 2 ``` +示例: 过滤日志文件中返回码及其个数 +```sh +awk -F"|" '{print $7}' /opt/logs/interface.log | sort | uniq -c | sort -nrk 1 | awk '{printf "返回码==%-10s , 返回码个数==%s\n",$2,$1}' + +示例: 查询某指定文本日志文件中,根据某分隔符的第几个字段的值为指定值的文本在该文本日志件中的打印 +```sh +grep "example" logfile.txt | awk -F',' '$2=="example" {print}' +``` +> `grep "example" logfile.txt`:首先使用grep命令查找包含"example"的行,并将结果输出到标准输出。 +> `awk -F',' '$2=="example" {print}'`:然后使用awk命令对grep的输出进行处理。`-F','`指定分隔符为逗号。`$2=="example"`表示只处理第2个字段为"example"的行。`{print}`表示打印符合条件的行。 awk 变量: @@ -1117,6 +1159,18 @@ dmtsai lines: 3 columns: 10 dmtsai lines: 4 columns: 10 dmtsai lines: 5 columns: 9 ``` +[netstat命令和awk命令查看并发连接数](https://www.cnblogs.com/wayne173/p/5652043.html) +```sh +netstat -n|awk '/^tcp/ {++S[$NF]} END {for(a in S) print a,S[a]}' +``` + +> 这个shell命令的作用是查看TCP连接状态的数量统计。 +> +> 具体解释如下: +> +> 1. `netstat -n`:使用netstat命令查看网络连接状态,其中-n参数表示不将IP地址和端口号转换为对应的域名和服务名称,以便更快地输出结果。 +> 2. `|`:将netstat命令的输出结果通过管道符号传递给awk命令。 +> 3. `awk '/^tcp/ {++S[$NF]} END {for(a in S) print a, S[a]}'`:使用awk命令对netstat命令的输出结果进行处理。 - `/^tcp/`:使用正则表达式匹配以tcp开头的行。 - `{++S[$NF]}`:对每个状态进行计数,使用数组S存储计数结果,其中$NF表示每行的最后一个字段,即TCP连接状态。 - `END {for(a in S) print a, S[a]}`:在处理完所有行后,使用for循环遍历数组S,输出每个TCP连接状态和对应的数量。 综上,该命令的输出结果将显示每个TCP连接状态的数量统计。 ## 十、进程管理 diff --git a/notes/MySQL.md b/notes/MySQL.md index 9b198fd43b..aec7519b41 100644 --- a/notes/MySQL.md +++ b/notes/MySQL.md @@ -28,6 +28,7 @@ * [六、复制](#六复制) * [主从复制](#主从复制) * [读写分离](#读写分离) + * [七、设计案例](#七设计案例) * [参考资料](#参考资料) @@ -412,6 +413,47 @@ MySQL 提供了 FROM_UNIXTIME() 函数把 UNIX 时间戳转换为日期,并提

+ +## 七、设计案例 +**题目描述** +在设计一个大型系统时,你被要求优化一个包含近千万记录的表,当前对于该表的CRUD(创建、读取、更新、删除)操作执行效率低下。请阐述你将采取哪些步骤和策略来提升这个表的性能? + +**题目解析** +面对近千万数据的表,CRUD操作变慢的问题,可以采用以下策略进行优化: + +**分析查询**:首先,需要分析慢查询日志,找出最常见的查询模式和瓶颈所在,确定优化的重点区域。** +**索引优化**: +创建合适的索引,特别是对于经常用于WHERE子句中的列。 +使用复合索引,确保索引的列顺序符合查询中最左侧的条件。 +避免索引过多或冗余,这会导致更新操作变慢。 +**查询优化**: +重构查询,避免使用SELECT *,而是指定需要的列。 +优化JOIN操作,确保所有JOIN的列都有索引。 +使用EXISTS代替IN,提高查询效率。 +**分库分表**: +根据业务需求和数据访问模式,将数据水平切分到多个数据库或表中。 +分表可以减少单表的数据量,降低查询和更新的复杂度。 +**事务优化**: +调整事务隔离级别,减少锁的竞争,如使用READ_COMMITTED而不是REPEATABLE_READ。 +使用悲观锁或乐观锁策略,根据业务场景选择合适的锁机制,减少锁等待时间。 +**缓存策略**: +实施读写分离,使用缓存如Redis存储热点数据,减少对数据库的直接访问。 +对查询结果进行缓存,减少重复查询。 +**硬件与配置**: +升级硬件资源,如增加内存、使用SSD硬盘。 +调整数据库配置,优化缓冲池大小、日志缓冲等参数。 +**数据库中间件**: +引入如Sharding-JDBC或MyCat等中间件,进行数据分片和读写分离。 +**监控与调优**: +设置性能监控,持续观察数据库性能指标。 +定期进行性能调优,根据实际负载调整策略。 +**数据归档**: +对历史数据进行归档处理,将不活跃的数据迁移到单独的表或数据库中,减轻主表的负担。 +通过上述策略的实施,可以显著提高大型数据表的CRUD操作效率,优化整体数据库性能。 + +参考链接 +https://mp.weixin.qq.com/s/hZsoQIIYNxP_uaHc7Tkv5g + ## 参考资料 - BaronScbwartz, PeterZaitsev, VadimTkacbenko, 等. 高性能 MySQL[M]. 电子工业出版社, 2013. diff --git a/notes/Redis.md b/notes/Redis.md index b41f5d8c57..2108fae86c 100644 --- a/notes/Redis.md +++ b/notes/Redis.md @@ -57,13 +57,14 @@ Redis 支持很多特性,例如将内存中的数据持久化到硬盘中, ## 二、数据类型 -| 数据类型 | 可以存储的值 | 操作 | -| :--: | :--: | :--: | -| STRING | 字符串、整数或者浮点数 | 对整个字符串或者字符串的其中一部分执行操作\ 对整数和浮点数执行自增或者自减操作 | -| LIST | 列表 | 从两端压入或者弹出元素 \ 对单个或者多个元素进行修剪,\ 只保留一个范围内的元素 | -| SET | 无序集合 | 添加、获取、移除单个元素\ 检查一个元素是否存在于集合中\ 计算交集、并集、差集\ 从集合里面随机获取元素 | -| HASH | 包含键值对的无序散列表 | 添加、获取、移除单个键值对\ 获取所有键值对\ 检查某个键是否存在| -| ZSET | 有序集合 | 添加、获取、删除元素\ 根据分值范围或者成员来获取元素\ 计算一个键的排名 | + +| Redis值数据类型 | 底层数据结构 | 支持的操作命令 | 使用场景 | +| ------------------------------ | ---------------------------------------------- | ------------------------------------------------------------ | ------------------------------------------------------------ | +| String 字符串 | | set key value/get key/del key | 对整个字符串或者字符串的其中一部分执行操作
对整数和浮点数执行自增或者自减操作 | +| List 列表(可存在重复值) | | rpush list-key item/lpush list-key item/lrange list-key startIdx endIdx/lindex list-key idx/lpop list-key | 从两端压入或者弹出元素
对单个或者多个元素进行修剪
只保留一个范围内的元素 | +| Set 集合(不存在重复,非有序) | | sadd set-key item/smembers set-key/sismember set-key item2 | 添加、获取、移除单个元素
检查一个元素是否存在于集合中
计算交集、并集、差集
从集合里面随机获取元素 | +| Hash 散表列(键值对无序散列表) | 字典(两个dictht,使用拉链法解决冲突),rehash | hset hash-key sub-key1 value1/hgetall hash-key/hdel hash-key sub-key1 | 添加、获取、移除单个键值对
获取所有键值对
检查某个键是否存在 | +| ZSET 有序集合 | 跳跃表 | zadd zset-key 728 member1/zrange zset-key startIdx endIdx withscores/zrangebyscrore zset-eky 0 800 withscores/zrem zset-eky member | 添加、获取、删除元素
根据分值范围或者成员来获取元素
计算一个键的排名 | > [What Redis data structures look like](https://redislabs.com/ebook/part-1-getting-started/chapter-1-getting-to-know-redis/1-2-what-redis-data-structures-look-like/) @@ -552,6 +553,12 @@ def main(): ## 十二、Sentinel Sentinel(哨兵)可以监听集群中的服务器,并在主服务器进入下线状态时,自动从从服务器中选举出新的主服务器。 +**监控(Monitoring)** 不间断检测Redis的运行状态. +**提醒(Notification** 当监控的Redis出现问题时,通过API向管理员或者其他应用程序 发送通知. +**自动故障迁移(Automatic failover** 当一个主服务器不能正常工作时, Sentinel 会开始一次自动故障迁移操作, 它会将失效主服务器的其中一个从服务器升级为新的主服务器, 并让失效主服务器的其他从服务器改为复制新的主服务器; 当客户端试图连接失效的主服务器时, 集群也会向客户端返回新主服务器的地址, 使得集群可以使用新主服务器代替失效服务器。 +**协议** 流言协议(gossip protocols)来接收关于主服务器是否下线的信息, 并使用投票协议(agreement protocols)来决定是否执行自动故障迁移 。 +Redis主从复制 搭建 +https://gper.gupaoedu.com/articles/7e7e7f7ff5g5bgc3 ## 十三、分片 diff --git a/notes/WebSocket.md b/notes/WebSocket.md new file mode 100644 index 0000000000..989996dde4 --- /dev/null +++ b/notes/WebSocket.md @@ -0,0 +1,107 @@ +# 1.WebSocket的基本概念 +WebSocket是一种网络通信协议,很多高级功能都需要用到它。 + +# 2. 为什么需要WebSocket? +初次接触 WebSocket 的人,都会问同样的问题:我们已经有了 HTTP 协议,为什么还需要另一个协议?它能带来什么好处? +答案很简单,因为 HTTP 协议有一个缺陷:通信只能由客户端发起。 +举例来说,我们想了解今天的天气,只能是客户端向服务器发出请求,服务器返回查询结果。HTTP 协议做不到服务器主动向客户端推送信息。 + +这种单向请求的特点,注定了如果服务器有连续的状态变化,客户端要获知就非常麻烦。我们只能使用"轮询":每隔一段时候,就发出一个询问,了解服务器有没有新的信息。最典型的场景就是聊天室。 +轮询的效率低,非常浪费资源(因为必须不停连接,或者 HTTP 连接始终打开(HTTP长连接))。因此,工程师们一直在思考,有没有更好的方法。WebSocket 就是这样发明的。 + +**一句话: WebSocket是一种全双工的通信,相对于HTTP这种单工通信的方式,解决需要全双工通信的场景需求。** + +# 3.WebSocket的基础知识 +WebSocket 协议在2008年诞生,2011年成为国际标准。所有浏览器都已经支持了。 +它的最大特点就是,服务器可以主动向客户端推送信息,客户端也可以主动向服务器发送信息,是真正的双向平等对话,属于服务器推送技术的一种。websocket是一种全新的协议,协议名为"ws"。 +![lQLPJwygMTQM3xnNAezNAl2wD4lIfhmO8PAGXIW2ifwuAA_605_492](https://github.com/HuaHero/CS-Notes/assets/2776844/cfd9b458-3a27-42a7-a964-2b0e40657255) + +其他特点包括: +(1)建立在 TCP 协议之上,服务器端的实现比较容易。 +(2)与 HTTP 协议有着良好的兼容性。**默认端口也是80和443,并且握手阶段采用 HTTP 协议**,因此握手时不容易屏蔽,能通过各种 HTTP 代理服务器。 +(3)数据格式比较轻量,性能开销小,通信高效。 +(4)可以发送文本,也可以发送二进制数据。 +(5)没有同源限制,客户端可以与任意服务器通信。 +(6)协议标识符是ws(如果加密,则为wss),服务器网址就是 URL。 + +# 4.应用举例 +XX司API协议对接(HTTP协议),主要目的是相机和平台建立websocket双向通信,平台侧能够使用LAPI协议主动获取相机侧的数据信息,相机侧也可以以LAPI的格式类型推送主动推送给平台; +两者以websocket创建连接,后续使用LAPI协议进行数据获取和推送,对于内部产品的对接提供了便利;对于外部产品,使用提供的协议文档同样可以对接我司设备,免除了我们对接外部产品的工作量,如下图报文数据即websocket协议的交互。 +![lQLPJw-cKuOJCdnMg80CXbAcjGulSPjiQAZchrKvrJ4A_605_131](https://github.com/HuaHero/CS-Notes/assets/2776844/92277af6-dfdf-4256-9504-3729a4e7510f) + +* WS和HTTP的消息头与交互过程(以注册认证协议举例),设备向服务器发起注册,伪代码如下: + ![lQLPJwncmuOlQPnMp80CXbC8gcupW0Q84wZch5MiJcUA_605_167](https://github.com/HuaHero/CS-Notes/assets/2776844/e36bd3dd-89f0-4fb8-88a5-e739d675fd60) +DeviceType:设备类型, +DeviceCode: 设备序列号 +**Algorithm:签名算法** +**Nonce: 随机字符,每次发起随机不同** +以上这些都是协议字段内容,**根据不同的协议均可更新修改**。 +注册报文示例: +![lQLPKHrNz7CtvHnM0M0CXbBLCKF4E9MKdwZch6C32vMA_605_208](https://github.com/HuaHero/CS-Notes/assets/2776844/631608f6-748c-4f96-9168-9f3dd0b758cc) +响应报文示例: +![lQLPJwhDVSkNrDl8zQJdsDZArAW-wQMOBlyH0CBstAA_605_124](https://github.com/HuaHero/CS-Notes/assets/2776844/20923a05-04b3-4d29-b145-ff89485e1c26) + +onnection:Upgrade必须设置为Upgrade,表示客户端希望连接升级 +Upgrade:websocket必须设置为WebSocket,表示在取得服务器响应之后,使用HTTP升级将HTTP协议转换(升级)为WebSocket协议。 +Sec-WebSocket-key:随机字符串,用于验证协议是否为WebSocket协议而非HTTP协议 +Sec-WebSocket-Version:表示使用WebSocket的哪一个版本。 +Sec-WebSocket-Accept:根据Sec-WebSocket-key和特殊字符串计算。验证协议是否为WebSocket协议(Accept是根据请求中的Key生成的,将其与一个固定字符串进行拼接后,使用SHA1算法得到hash后,再进行base64编码)。 +Sec-WebSocket-Location:与Host字段对应,表示请求WebSocket协议的地址。 +HTTP/1.1 101 Switching Protocols:101状态码表示升级协议,在返回101状态码后,HTTP协议完成工作,转换为WebSocket协议。此时就可以进行全双工双向通信了。 +以上为注册标准格式,后面注册成功后,可以只发具体的Lapi协议。 + +# 5. 个人认为核心知识点&HTTP或TCP等: + 1. 连接建立过程: **通过HTTP 1.1建立连接**后,服务端WebSocketServerProtocolHandler在接收指定url升级连接(Connection: Upgrade,Upgrade: websocket, Sec-Websocket-Key等)请求通过后,响应101状态码(Connection: Upgrade,Upgrade: websocket,Sec-WebSocket-Accept)响应之后,连接就与HTTP无关了,变成了WebSocket连接 + 2. 连接释放过程:若服务端不支持的自定义协议或掩码错误、或操作码中为close + 3. 连接生命周期:连接建立后就是持久性连接。对于需要区分空间连接或忙碌状态,有**操作码**close、ping、pong这几种控制码; + +WebSocket协议的通信报文是**二进制格式**,大致包含以下字段: +![image](https://github.com/HuaHero/CS-Notes/assets/2776844/c7e73f02-d719-4be4-9021-4d85cbbdc1c2) + +(1)FIN: 占用一个bit位。如果其值是1(抓包工具显示为true),表示该帧这是消息的 +最后一个数据帧;如果其值是0(抓包显示为false),表示该帧不是消息的最后一个数据帧。 +(2)opcode:WebSocket帧的操作码,占用4个bit位。操作码opcode的值决定了应该如何解析后续的数据载荷(Data Payload)。如果操作代码是不认识的,那么接收端应该断开链接。 +WebSocket协议的操作码取值说明,具体如下表所示: +WebSocket控制帧有3种:Close、Ping以及Pong。控制帧的opcode操作码定义为0x08(关 +闭帧)、0x09(Ping帧)、0x0A(Pong帧)。Close关闭帧很容易理解,客户端如果接受到关闭帧, +就关闭连接;当然,客户端也可以发送关闭帧给服务端,服务端收到该帧之后也会关闭连接。 + +Ping和Pong是WebSocket的心跳帧,用来保证客户端维持正常在线状态。WebSocket为了 +保持客户端、服务端的实时双向通信,需要确保客户端、服务端之间的TCP通道保持连接没 +有断开。然而,如果长时间没有数据往来的连接,依旧保持着双向连接,可能会浪费服务端 +的连接资源,所以,需要关闭这些长时间空闲的连接。 +但是,有一些场景,还是需要保持那些长时间空闲的连接,如何避免被误关闭呢?这个 +时候,可以采用Ping和Pong两个心跳帧来完成。一般来说只有服务端给客户端发送Ping,然 +后客户端发送Pong来回应,表明自己仍然在线。 +(3)Mask:一个bit位(值为1时抓包会显示为true),表示是否要对**数据载荷进行掩码** +操作。客户端向服务端发送数据时,需要对数据进行掩码操作;从服务端向客户端发送数据 +时,不需要对数据进行掩码操作,如果服务端接收到的数据没有进行掩码操作,服务器需要 +断开连接。所有的客户端发送到服务端的数据帧,Mask值都是1。 + +(4)Masking-Key:掩码键,如果Mask值为1,需要用这个掩码键来对数据进行反掩码, +才能获取到真实的通信数据。为了避免被网络代理服务器误认为是HTTP请求,从而招致代 +理服务器被恶意脚本的攻击,WebSocket客户端必需掩码所有送给服务器的数据帧。 +客户端必须**为发送的每一个数据帧选择新的不同掩码值**,并要求是这个掩码值是无序的、 +无法预测的。**在掩码算法的选择上,为了保证随机性,可以借助密码学中的随机数生成器生 +成每一个新掩码值。** +(5)Payload Length:通信报文中数据载荷的长度。 +(6)Payload:通信报文数据帧的有效数据载荷,也就是真正的通信消息内容。 + +`说 明 +以上 WebSocket 通信报文字段,在 IETF 所发布的 WebSocket 标准规范 RFC6455 中有 +更加详细定义和说明,具体可以参考标准规范。 +` + + + +# 6. 参考链接 +* How to Use WebSockets(http://cjihrig.com/blog/how-to-use-websockets/) +* WebSockets - Send & Receive Messages(https://tutorialspoint.com/sebsockets/websockets_send_eceive_messages.htm) +* Introducing WebSockets: Bringing Sockets to the Web(https://www.html5rocks.com/en/tutorials/websockets/basics/) + + + + + + + diff --git "a/notes/docker\345\222\214kubernetes.md" "b/notes/docker\345\222\214kubernetes.md" new file mode 100644 index 0000000000..fef69fa304 --- /dev/null +++ "b/notes/docker\345\222\214kubernetes.md" @@ -0,0 +1,385 @@ +[TOC] + + + +# 发展历程 + +1. 早期的物理机 + +2. 虚拟化技术--虚拟机 + +3. 容器化docker -- 典型的C/S架构,Go语言开发(适合高并发),[hub.docker.com]() + + ![image-20231129092939522](C:\Users\h00284904\Desktop\image-20231129092939522--dockerArchitecture.png) + + ```sh + docker pull ** + docker push + + docker run -d --name my-tomcat -p 9999:8080 tomcat:8.0 + docker stop ** + + docker version + + docker images/docker ls + # 取别名 + docker tag + + docker ps + docker rmi -f + ``` + + # Image深入讨论 + + ![image-20231129093219217](C:\Users\h00284904\Desktop\image-20231129093219217--Image深入.png) + + 在docker中任何一个image:tomcat\redis\mysql,其最底层就是一个最小内核的linux kernel. + + github.com/docker-library + + dockerfile: + + > FROM debian: buster-slim + > + > RUN ... + > + > CMD [mysqld] + + docker build [dockerFinleName] . + + + +4. 自定义镜像怎么办? + + tomcat、redis、mysql、rocketmq等中间件都已有官方写好了的镜像 + + 根据image就可以创建出container + + 而且image是可以发给不同的人使用的,只要对应机器上安装了DockerEngine即可。 + + ``` dockerfile + FROM openjdk:8 + COPY dockerfile-demo-0.0.1-SNAPSHOT.jar jack-dockerfile-image.jar + CMD ["java","-jar","jack-dockerfile-image.jar"] + ``` + + + + springboot项目-->image-->container + + ``` shell + docker build -t jack-dockerfile-image . + ``` + + ```sh + docker run -d --name sb01 -p 6661:8080 jack-dockerfile-image + ``` + + ```sh + docker logs [容器名称] + ``` + + + +5. image共享给别人使用 + + * 需要创建一个镜像 + + * 上传到公共的仓库 + + * 当前本地需要登录一个公共仓 + + ```shell + sudo docker login -username=hqh registry.cn-hangzhou.aliyuncs.com + ``` + + + + * push到公共仓 + + ``` sh + docker push registry.cn-hangzhou.aliyuncs.com/jack-kubernetes/jack-dockerfile-image + ``` + + + + * 其它人拉取使用 + + ``` sh + docker pull registry.cn-hangzhou.aliyuncs.com/jack-kubernetes/jack-dockerfile-image + ``` + + + + * + + + + # Container深入探讨 + + ![ ](C:\Users\h00284904\Desktop\image-20231129110137511--container.png) + + ```sh + docker exec -it my-tomcat bash + cat /etc/issue + ``` + + 能否将已修改的container打包成新的镜像,打包分享给其他人使用----可以。 + + ```sh + docker commit [tomcat03] [gupao-customized-tomcat-image] + ``` + + * container常见操作 + + ```sh + docker ps -aq + docker rm -f conatiner + docker exec -it 容器名 + docker commit + # 查看docker占用CPU等资源情况 + docker stats + # 可以设置container使用资源的上限 + docker run -d --memory 300M --name jack-tomcat-memory-limit tomcat:8.0 + docker run -d --cup-shareds 10 --name jack-tomcat-cpu-limit tomcat:8.0 + ``` + + + + * + +6. 为了资源隔离 + + * Linux已提供的支持 + + ![image-20231129113551798](C:\Users\h00284904\Desktop\image-20231129113551798--linuxKernel.png) + + kernel.org/doc + + * 怎样降低开发或运维人员使用这些技术的门槛--Linux Container(LXC) + + linuxcontainers.org——依然有门槛,更多的还是针对 Linux运维人员,出发点:基于隔离出物理机的资源,为了快速搭建出一个物理机中的虚拟资源 + + ​ ——未面向app + + docker 在LXC基础上提出了针对具体的app进行隔离解决方案,image,contaner. + + bocker + + * + + # Doker数据持久化和网络 + + ```sh + docker volume ls + # 自定义 + docker volume create 自定义volume名 + docker volume inspect [volume名称] + docker volume rm -f * + ``` + + + + ## volume数据持久化 + + VOLUME /var/lib/mysql,有了VOLUME关键字之后,将容器的/var/lib/mysql实际在对应hosthost上建一个相应目录(在/var/lib/docker/volumes/*下)。 + + ```sh + docker run -d --name hqh-mysql -p 3301:3306 -v hqh-mysql-volume:/var/lib/mysql -e MYSQL_ROOT_PASSWORD=jack666 --privileged mysql:5.7 + ``` + + + +bind的概念 + +- -v ~/shop:/usr/local/tomcat/webapps/shop ,少民volume这个层次面试。bind mounting + +- 把目录和专门的服务器绑定。 + + ## docker中的网络 + + https://landscape.cncf.io/?category=container-runtime&grouping=category + + + +```sh +# 本地网卡信息 +ls /sys/class/net +ip a +ip link + +cd /etc/sysconfig/network-scripts +ip addr add ip dev eth0 +systemctl restart network +ipup eth0 +ipdown eth1 + +docker network ls +``` + +不同namespace下的网络 + +```sh +ip netns add ns1 + +ip netns add ns2 +ip netns add ns3 +ip netns list + +ip nets delete ns3 delete ns3 + +ip netns exec ns1 ip a +ip netns exec ns1 ifup lo + +# 创建了veth-ns1和veth-ns2,并成对 +ip link add veth-ns1 type veth peer name veth-ns2 +# 将各自关联到对应命名空间中 +ip link set veth-ns1 netns1 +ip link set veth-ns2 netns2 + +ip netns exec ns1 ip addr add 192.168.0.11/24 dev veth-ns1 +ip netns exec ns2 ip addr add 192.168.0.12/24 dev veth-ns2 + +ip netns exec ns1 ip link set veth-ns1 up +ip netns exec ns2 ip link set veth-ns2 up + +ip netns exec ns1 ping 192.168.0.12 +``` + +veth pair: virtual ethernet pair,一个虚拟 + +![image-20231129162902479](C:\Users\h00284904\Desktop\image-20231129162902479--veth.png) + +### docker网络之bridge + +![image-20231129164624972](C:\Users\h00284904\Desktop\image-20231129164624972--veth2.png) + +![image-20231129165011607](C:\Users\h00284904\Desktop\image-20231129165011607--veth4.png) + +通过veth-pair彼此之间的通信。 + +```sh +docker network ls +docker network inspect bridge +# 也可以自定义网侧面 +docker network create --subnet=172.18.0.0/24 tomcat-net-name +# 通过docker run命令 +docker run -d --name nginx01 -p 6061:80 --network tomcat-net-name + +# 将网络添加下 +docker network connect tomcat-name-net tomcat2 +``` + +其它模式:none\network ls + +各个namespace这间,是共享宿主机分配的带宽,还是分配固定的带宽?—— + + + +## 基于Docker搭建MySQL高可用集群 + +MySQL单机部署--会增加不可用风险, + +实现MySQL高可用,搭建MySQL集群,-->伴随要解决数据一致性问题。 + +### PXC + +Percona Xtradb Cluster,percona.com + +多主方案+同步复制-->强一制性, + +方法(1) 原生 pxc 依赖 一步步手动搭建 + +(2) docker image方式搭建 + + + +![image-20231129174636101](C:\Users\h00284904\Documents\ForWork\学习\docker\docker-mysql.pdf) + + + +```sh +# 获取pxc镜像,其实就相当于获取mysql镜像 +docker pull percona/percona-xtradb-cluster:5.7.21 + +# 设置网络 +docker network create -subnet=172.19.0.0/24 pxc-cluster-net bridge + +# 数据持久化(后续让容器进行关联) +docker volume create pxc-v1 +docker volume create pxc-v2 +docker volume create pxc-v3 + +# 创建容器 +docker tag percona/percona-xtradb-clustrer:5.7.21 pxc +docker run -d --name=node1 -p 3301:3306 pxc -e MYSQL_ROOT_PASSWORD=hqh666 -v pxc-v1:/var/lib/mysql -e CLUSTER_NAME=PXC --net=pxc-cluster-net -e XTRBACKUP_PASSWORD=hqh666 pxc + +docker run -d --name=node2 -p 3302:3306 pxc -e MYSQL_ROOT_PASSWORD=hqh666 -v pxc-v2:/var/lib/mysql -e CLUSTER_NAME=PXC --net=pxc-cluster-net -e XTRBACKUP_PASSWORD=hqh666 -e CLUSTER_JOIN=node1 pxc + + +docker run -d --name=node3 -p 3303:3306 pxc -e MYSQL_ROOT_PASSWORD=hqh666 -v pxc-v3:/var/lib/mysql -e CLUSTER_NAME=PXC --net=pxc-cluster-net -e XTRBACKUP_PASSWORD=hqh666 -e CLUSTER_JOIN=node1 pxc +``` + +InnoDB cluster: 一主多从 + + + +## docker compose【一步创建多个容器--单台】-->docker swarm/Meos/kubernetes【一步创建多个容器--多台】 + +(1)安装docker-compose + +```shell +sudo curl -L "https://github.com/docker/compose/release/download/1.24.1/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose +``` + + + +(2)定义一个yaml文件 + +```yaml +version: "3.9" +# service <---->container +services: + # 第一个container名字 + web: + # 本地 image Dockerfile + build: . + # -p 5000:5000 + ports: + - "8000:5000" + # 第二个container名字 + redis: + image: "redis:alpine" + +networks: + app-net: + driver: bridge +``` + + + +(3)通过docker compose一键启动 + +```sh +docker compose up +``` + + + +## 容器编排&容器管理 + +如果是docker单机环境,其实docker compose也就够用了 + +但是因为一台宿主机器资源毕竟有限,所以肯定是希望多台docker组成的集群环境,就需要一起管理容器。 + +Docker Swarm、Mesos、Kubernetes + +* Docker Swarm + + scheduler——调度、计算 + + network + + health + + discovery——manager要知道work node在哪,w-->信息注册到manager diff --git "a/notes/\347\274\223\345\255\230.md" "b/notes/\347\274\223\345\255\230.md" index f20d1dd5ab..0ec85f1d90 100644 --- "a/notes/\347\274\223\345\255\230.md" +++ "b/notes/\347\274\223\345\255\230.md" @@ -84,26 +84,35 @@ CDN 主要有以下优点: ## 四、缓存问题 -### 缓存穿透 +### 缓存穿透(Cache Penetration) -指的是对某个一定不存在的数据进行请求,该请求将会穿透缓存到达数据库。 +指的是对某个一定**不存在的**数据进行请求,该请求将会穿透缓存到达数据库(即数据**既不在缓存,也不在数据库**,例如特异构造的攻击数据)。 解决方案: - 对这些不存在的数据缓存一个空数据; - 对这类请求进行过滤。 +- 使用布隆过滤器(布隆过滤器--判断存在的不一定真存在,判断不存在的一定不存在;只支持增加操作,是一个位图) -### 缓存雪崩 +### 缓存雪崩(Cache Avalanche) -指的是由于数据没有被加载到缓存中,或者缓存数据在同一时间大面积失效(过期),又或者缓存服务器宕机,导致大量的请求都到达数据库。 +指的是由于****数据没有被加载到缓存中,或者缓存数据在同一时间大面积失效(过期),又或者缓存服务器宕机**,导致****大量的请求都到达数据库****。 在有缓存的系统中,系统非常依赖于缓存,缓存分担了很大一部分的数据请求。当发生缓存雪崩时,数据库无法处理这么大的请求,导致数据库崩溃。 解决方案: -- 为了防止缓存在同一时间大面积过期导致的缓存雪崩,可以通过观察用户行为,合理设置缓存过期时间来实现; +- 为了防止缓存在同一时间大面积过期导致的缓存雪崩,可以通过观察用户行为,合理**设置缓存过期时间**来实现; - 为了防止缓存服务器宕机出现的缓存雪崩,可以使用分布式缓存,分布式缓存中每一个节点只缓存部分的数据,当某个节点宕机时可以保证其它节点的缓存仍然可用。 -- 也可以进行缓存预热,避免在系统刚启动不久由于还未将大量数据进行缓存而导致缓存雪崩。 +- 也可以进行**缓存预热,避免在系统刚启动不久由于还未将大量数据进行缓存而导致缓存雪崩**。 + +### 缓存击穿(Hotspot Ivalid) +**热点数据缓存过期**。我们的业务通常会有几个数据会被频繁地访问,比如秒杀活动,这类被频地访问的数据被称为热点数据。 + +如果缓存中的某个热点数据过期了,此时大量的请求访问了该热点数据,就无法从缓存中读取,直接访问数据库,数据库很容易就被高并发的请求冲垮,这就是缓存击穿的问题。 +解决方案: +- 互斥锁方案,保证同一时间只有一个业务线程更新缓存,未能获取互斥锁的请求,要么等待锁释放后重新读取缓存,要么就返回空值或者默认值。 +- 不给热点数据设置过期时间,由后台异步更新缓存,或者在热点数据准备要过期前,提前通知后台线程更新缓存以及重新设置过期时间; ### 缓存一致性 @@ -307,4 +316,5 @@ public class LRU implements Iterable { - [缓存那些事](https://tech.meituan.com/cache_about.html) - [一致性哈希算法](https://my.oschina.net/jayhu/blog/732849) - [内容分发网络](https://zh.wikipedia.org/wiki/%E5%85%A7%E5%AE%B9%E5%82%B3%E9%81%9E%E7%B6%B2%E8%B7%AF) +- [缓存一致性问题](https://xiaolincoding.com/redis/architecture/mysql_redis_consistency.html#%E5%A6%82%E4%BD%95%E4%BF%9D%E8%AF%81%E4%B8%A4%E4%B8%AA%E6%93%8D%E4%BD%9C%E9%83%BD%E8%83%BD%E6%89%A7%E8%A1%8C%E6%88%90%E5%8A%9F) - [How Aspiration CDN helps to improve your website loading speed?](https://www.aspirationhosting.com/aspiration-cdn/) diff --git "a/\345\201\232\351\242\230\346\203\205\345\206\265\347\273\237\350\256\241--\350\275\257\344\273\266\347\274\226\347\250\213\347\237\245\350\257\206\344\270\216\345\272\224\347\224\250.md" "b/\345\201\232\351\242\230\346\203\205\345\206\265\347\273\237\350\256\241--\350\275\257\344\273\266\347\274\226\347\250\213\347\237\245\350\257\206\344\270\216\345\272\224\347\224\250.md" new file mode 100644 index 0000000000..a26e92fd8b --- /dev/null +++ "b/\345\201\232\351\242\230\346\203\205\345\206\265\347\273\237\350\256\241--\350\275\257\344\273\266\347\274\226\347\250\213\347\237\245\350\257\206\344\270\216\345\272\224\347\224\250.md" @@ -0,0 +1,388 @@ + + +# 做题情况统计--软件编程知识与应用 + +| 编程规范理解与应用(JAVA) | 合计 | 总题数 | 正确答题数 | 正确率 | +| ------------------------------------------------------------ | ------------------------------------------------------------ | ------ | ---------- | ------ | +| 核心类库:掌握集合与泛型、多线程编程、反射相关规范 | 集合与泛型:规范相关; 并发与多线程:规范相关,ArrayList动态数组、Vector | 4 | 4 | 100.0% | +| 代码风格:掌握命名、注释、代码格式规范 | 命名 | 1 | 1 | 100.0% | +| 扩展类库**:掌握IO编程、日志、正则表达式、数据库编程、网络编程相关规范** | IO编程:规范相关; 数据库编程:规范相关; 序列化:规范相关 | 4 | 3 | 75.0% | +| 语法知识:掌握变量、数据类型、表达式、控制语句、方法、面向对象、异常相关规范 | 面向对象:规范相关; 数据类型:规范相关 | 6 | 3 | 50.0% | + +| 编程语言能力(JAVA) | 合计 | 总题数 | 正确答题数 | 正确率 | +| ------------------------------------------------------------ | ------------------------------------------------------------ | ------ | ---------- | ------ | +| **JVM**:理解Class格式与类加载过程,**掌握运行时数据区结构与定位方法**,**理解GC原理与常用GC回收器,了解JIT机制** | 运行时数据区; Class格式; JIT; GC; 类加载 | 10 | 8 | 80.0% | +| 语法知识:深入理解变量、数据类型、表达式、控制语句、方法、面向对象、异常的语法特性 | 方法; 数据类型; 语法知识:深入理解变量、数据类型、表达式、控制语句、方法、面向对象、异常的语法特性; 异常体系; 控制语句 | 10 | 8 | 80.0% | +| **性能优化**:掌握代码级性能优化方法,**理解JVM的性能调优方法** | 集合:性能相关; GC:性能相关; 性能优化:掌握代码级性能优化方法,理解JVM的性能调优方法; 方法:性能相关; 表达式:性能相关; 运行时数据区:性能相关; 控制语句:性能相关; IO编程:性能相关 | 8 | 4 | 50.0% | +| 核心类库:熟练掌握集合与泛型、多线程编程、反射的实战用法 | 反射; 线程; 集合与泛型 | 6 | 3 | 50.0% | +| 扩展类库:熟练运用IO编程、正则表达式、安全类库等重要类库,掌握数据库编程、网络编程。 | 正则表达式; IO编程; 数据库编程 | 4 | 1 | 25.0% | + + + +# 题目回顾 + +1. 类加载的~~几个~~**7**个阶段: + + > (1)加载(字节码转换为方法区的运行时存储结构) + > + > (2)验证( Class 文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚机自身安全) + > + > (3)准备(为类变量--static修饰--的变量分配内存并设置初始值--方法区) + > + > (4)解析(将常量池的符号引用替换为直接引用) + > + > (5) 初始化(虚拟机执行类构造器的过程;**虚拟机会保证一个类的 () 方法在多线程环境下被正确的加锁和同步**) + > + > (6)使用(类被创建实例并被使用--实例化是分配在堆中) + > + > (7)卸载(当类不再被使用时,JVM对其进行垃圾回收,并释放其占用的内存空间) + > + > 一般(2)到(4)这三个阶段又叫链接阶段。 + +* 类加载——双亲委派机制(如果一个类加载器收到了类加载请求,先看父类加载器能否加载,一直如此到顶端加载器判断能否加载,如果顶端可以加载,则顶端加载,如果不行,则本类加载器加载) + + 1) 类加载时,判断两个类是否是同一个类,除了要求它们的名字相同外,还要求加载它们的类加载器实例是同一个,只有两者同时满足的情况下,JVM才认为这两个class是相同的。 + + 2)ClassLoader的loadClass方法加载不存在的类会抛ClassNotFoundException,并不是说返回NULL。 + + 3)自定义类加载器实现**继承****ClassLoader**后**,重写了**findClass方法加载指定路径上的class;** + + 4)BoostrapClassLoader加载核心库类(%JAVA_HOME%/lib目录下的jar包和类或者被-Xbootclasspath参数指定的路径中的所有类)--它是用**原生代码(使用C++)**来实现的,**并非继承java.lang.ClassLoader** + + 5)Extension加载器加载%JRE_HOME%/lib/ext下的类和jar包,或者java.ext.dirs系统变量所指定的路径下的jar包,System加载器加载CLASSPATH环境指定路径的类, + + 6)AppClassLoadrer负载加载当前应用classpath下的所有jar包和类。 + +1. + +2. jinfo,info 查看运行环境参数、查看JVM系统参数、命令行参数---**不能看内存** + +3. **值不可变类型: Integer、String,在Java 8中,Integer缓存池的大小默认为-128~127。不可变类型有:Boolean, Byte, Character,Short, Double, Float, Integer, Long, String, BigDecimal** + +4. 反射相关: + + > * getType() 方法返回的是对象的运行时类型,即 Class 对象,而 getGenericType() 方法返回的是对象的泛型类型,即 Type 对象。 + > + > * 具体来说,getType() 方法返回的是 Class 对象,它代表了对象的实际类型,例如 String、Integer、List 等。而 getGenericType() 方法返回的是 Type 对象,它代表了对象的泛型类型,例如 List、Map 等。 + > + > 因此,getType() 方法只能获取对象的实际类型,而无法获取对象的泛型类型信息;而 getGenericType() 方法则可以获取对象的泛型类型信息,包括泛型参数的类型、数量等。 + +5. Java集合类 + + * ArrayList——当内部存储空间不够时,扩大50%。默认长度是10。 + + * Vector—— 当内部存储空间不够时,扩大1倍。默认长度是10。 + + * StringBuffer——默认长度是16 + + * StringBuilder——默认长度是16 + + * HashSet——默认长度是16 + + * HashMap——默认长度为16,允许key和value都为null + + * HashTable——默认长度是11 + + * XxxBlockingQueue(array的要手工指定,linked默认是Integer.MAX_VALUE) + + **线程安全的集合:**vector、hashtable、StringBuffer、ConcurrentMap(不可存放key和value都为null的对象) + + 非线程安全的集合: ArrayList、LinkeList(双队列)、HashMap、StringBuilder、LinkedHashSet、LinkedHashMap——保持了插入的顺序-->LRU、PriorityQueue + + + +6. 重写与重载: + + **重写(Override)**: + + * 发生在**父子类**之间,方法不能抛出新的检查异常或者比被重写方法申明更加宽泛的异常 + + * 重写的返回类型与被重写的返回类型可以不相同,但必须是父类返回值的派生类 + + * 声明为**final**或**static**的**方法**不能被重写 + + **重载(Overload)** + + ​ 发生在同一个类中 + + + +7. 在java中关于抽象类的描述正确的是() + + A、抽象类必须包含一个抽象方法 ——这个我很容易认为是对的,抽象类可以包含抽象方法,也可以包含非抽象方法 + + B、抽象类的方法必须是抽象的 ——这个我很容易认为是对的 + + C、声明抽象类必须带有abstract ——这个我很容易认为是错的 + + D、抽象类可以被实例化 —— 这个显然错误。抽象方**法必须在子类**中被实现。**抽象类的声明必须带有abstract关键字**。主要作用是**为了被其他类继承**。 + + interface中的属性的默认修饰权限:public static final,方法属性:public abstract + +8. JVM: + + 以下程序运行结果如何,micro.yam文件不存在。 + + 考察异常类及其继承关系: + + ``` + /** + * 科目二 题目,如代码,程序运行结果如何: + * A、文件路径, finish + * B、ooo,打印异常堆栈,finish + * C、ooo,打印异常堆栈 + * D、直接抛出类加载异常 + * + * @author h00284904 + * @since 2021-09-27 + */ + public class JVM_FileParse_Verify { + public static final String A = JVM_FileParse_Verify.class.getClassLoader().getResource("micro.yam").getPath(); + public static void main(String[] args) { + try { + System.out.println(JVM_FileParse_Verify.A); + } catch (Throwable e) { + System.out.println("ooo"); + e.printStackTrace(); + } + System.out.println("finish"); + // Exception in thread "main" java.lang.ExceptionInInitializerError + // Caused by: java.lang.NullPointerException + // at com.huawei.hqh.JVM_FileParse_Verify.(JVM_FileParse_Verify.java:14) + } + } + ``` + + 因为A就在主类中,且是静态,所在调用main之前就已抛出异常 + + ``` + package com.huawei.hqh; + + class JVM_FileParser { + public static final String A = JVM_FileParser.class.getClassLoader().getResource("micro.yam").getPath(); + } + /** + * 功能描述 + * + * @author h00284904 + * @since 2021-09-28 + */ + public class JVM_FileParser_Verify2 { + public static void main(String[] args) { + try { + System.out.println(JVM_FileParser.A); + } catch (Throwable e) { + System.out.println("ooo"); + e.printStackTrace(); + } + System.out.println("finish"); + } + //ooo + // java.lang.ExceptionInInitializerError + // at com.huawei.hqh.JVM_FileParser_Verify2.main(JVM_FileParser_Verify2.java:15) + // Caused by: java.lang.NullPointerException + // at com.huawei.hqh.JVM_FileParser.(JVM_FileParser_Verify2.java:4) + // ... 1 more + //finish + } + ``` + +9. 线程: + + java 中断线程, 下面哪些情况可能导致中断当前线程的运行? (A、B、D) + + A. 抛出一个异常 + + B. 当前线程调用wait()方法等待竞争资源 + + C. 当前线程创建一个新线程时 + + D. 当前线程调用sleep()方法时 + + https://zhuanlan.zhihu.com/p/149205707 + + https://www.cnblogs.com/paddix/p/5381958.html + +10. **类文件**: + + ClassFile { + + u4 magic; ——固定为“0xCAFEBABE”,作用是确定该文件为一个可被JVM加载的Class文件。 + + u2 minor_version; ——Class文件的副版本 + + u2 major_version; ——Class文件的主版本 + + u2 constant_pool_count; + + cp_info constant_pool[constant_pool_count-1]; + + u2 access_flags; + + u2 this_class; + + u2 super_class; + + u2 interfaces_count; + + u2 interfaces[interfaces_count]; + + u2 fields_count; field_info fields[fields_count]; + + u2 methods_count; method_info methods[methods_count]; + + u2 attributes_count; attribute_info attributes[attributes_count]; + + } + + 其中,u1、u2、u4分别代表1、2、4个字节无符号数。 + +  说明: +   javac -g TestClass.java-g:生成所有的调试信息,包括局部变量名和行号信息。 + javap -c TestClass > TCC.txt, + + 对于javap常用的参数:-c:输出字节码Code-l(小写L):输出Code、LineNumberTable与LocalVariableTable-s:输出方法签名(方法的接收参数列表和返回值)-verbose:包含-c、-l以及输出class文件的编译版本,常量池,Stack, Locals, Args_size对于javap而言,常用的就是-c或-verbose + 作者:徐家三少 + 链接:https://juejin.cn/post/6844903464481325064 + +11. **泛型**使用之PECS(Producer Extends Consumer Super)原则:**当只想从集合中获取元素,请把这个集合看成生产者,请使用,这就是Producer extends原则,PECS原则中的PE部分。** + + **泛型与PECS:https://www.jianshu.com/p/e5b8cd33ec94?u_atoken=b39aa8a6-b196-4d41-9782-7c25662c0cc9&u_asession=01BjCr_1l-lFSEGUHFjg5OsLL3pscZ9J7HCTj7CD9shJSrg_xVl5cUdEmfKxgML9ipX0KNBwm7Lovlpxjd_P_q4JsKWYrT3W_NKPr8w6oU7K-eTQ6hLYwsFz-l_HWOSEkocCO_LNVcRl1Qbia_a5plzmBkFo3NEHBv0PZUm6pbxQU&u_asig=05WsD83ID0Tr7mSzPvQnLSMC7_9WEyYVY8Q0d4iFV0VaqFLtDNGi6xzsFcPbJg6RBo-VQeq2NrTZ_RTkeuVVUtppPLivPJBTV_CPH31hd3Lk9XVuwQ6ImA-4GrwQ-pYJgYvCAChf9--VSC925Oa2ydvUzNZFdNwYN16nMxAeCwSH39JS7q8ZD7Xtz2Ly-b0kmuyAKRFSVJkkdwVUnyHAIJzQhDv5RpUbAmToYFDMC0YyH-SIxHNXAMYgQIJs2NKSYja2GRgmoIOFBw7FYOdgQIyu3h9VXwMyh6PgyDIVSG1W_ladG6YsfXrB7slA80ltmTMcP86Dpk-9o5WW2hT6bxqq2XrK0uO2qG0zzYpsjVKEj0v_O4rXVVATYuGBfXtkIFmWspDxyAEEo4kbsryBKb9Q&u_aref=Q%2BYoK3NQIQx85iMjzFEOmRFHgdc%3D** + + 总结PECS原则如果你只需要从集合中获得类型T , 使用通配符如果你只需要将类型T放到集合中, 使用通配符如果你既要获取又要放置元素,则不使用任何通配符。例如ListPECS即 Producer extends Consumer super, 为了便于记忆。(《effective java》第28条)为何要PECS原则?你还记得前面提到泛型是不可变(**协变、逆变、不变**概念)吗?即List和List之间没有任何继承关系。API的参数想要同时兼容2者,则只能使用PECS原则。这样做提升了API的灵活性。 + 在java集合API中,大量使用了PECS原则,例如java.util.Collections中的集合复制的方法:`public static void copy(List dest, List src) { ... } `集合复制是最典型的用法:复制源集合src,主要获得元素,所以用复制目标集合dest,主要是设置元素,所以用**当然,为了提升了灵活性,自然牺牲了部分功能。鱼和熊掌不能兼得。**补充说明这里的错误全部是编译阶段不是运行阶段,编译阶段程序是没有运行。所以不能用运行程序的思维来思考。使用泛型,就是要在编译阶段,就找出类型的错误来。 + 作者:安静的猫咪先生 + 链接: https://www.jianshu.com/p/e5b8cd33ec94 + +12. 考察**NIO**: + + 要从文件中读出第10个字节—FileInputStream,与FileChannel关系等 + + readAllLines + + 哪些**不属于**NIO的buffer: StringBuffer,CharBuffer,IntBuffer,FloatBuffer + + skip(9), + +13. **Netty**的高性能体现在哪?ABCDE + + A、非阻塞IO + + B、内存零拷贝 + + C、内存池 + + D、串行化处理读写 + + E、高性能序列化协议 + + 解读 Netty的高性能表现: + + (1)IO线程模型:**同步非阻塞**,用最少的资源做更多的事情?何解 + + (2)内存零拷贝:尽量减少不必要的内存拷贝,实现了更高效率的传输 + + (3)内存池设计:申请的内存可以重用,主要指直接内存。内部实现是通过一棵二叉查找树来管理内存分配情况。 + + (4)串行化处理读写:避免使用锁带来的性能开销。即消息的处理尽可能在同一个线程内完成,期间不进行线程切换,这样避免了多线程竞争和同步锁。表面上看,串行化设计似乎CPU利用率不高,并发程度不够。但是,通过调整NIO线程池的线程参数,可以同时启动多个串行化的线程并行运行,这种局部无锁化的串行线程设计相比一队列里多个工作线程模型性能更优。 + + (5)高性能序列化协议:支持ProtoBuf等高性能序列化协议 + + (6)高效并发编程的体现:volatile的大量、正确使用; CAS和原子类的广泛使用;线程安全容器的使用;通过读写锁提升并发性能。 + +14. 关于**Java8 Stream**的描述正确的是: **Stream跟迭代器类似,再次遍历需要重新生成。** + + Stream的特点: + + 1) 无存储,Stream不是一种数据结构,也不保存数据,数据源可以是数组、容器、I/O或Channel等; + 2) 为函数式编程而生,对Stream的任何修改都不会修改数据源; + 3) 惰性执行,Stream上的中间操作并不会立即执行,只有等到用户真正需要结果时才会执行; + 4) 一次消费,Stream只能被消费一次,一旦遍历过就会失效,就像容器的迭代器那样,想要再次遍历必须重新生成 + + + +15. 字符流 和 字节流: + + * **字节流**的基类**InputStream/OutputStream**,**字符流**是**Reader/Writer** + * 字符流最终都会按照字节流处理 + +16. Optional——主要用来处理每个程序员都会碰到的空指针异常问题,Java9还补充了or()、IfPresentOrElse()及stream()。属于可选值的封装类,即可以为空,也可以包含对象。 + + 关于java optional参照 https://www.jianshu.com/p/63830b7cb743 + +17. **Java 本地方法**的正确定义:"它一般在本地声明,异地用C和C++来实现。它的声明有几点要注意:1)native与访问控制符前后的关系不受限制。2)必须在返回类型之前。3)它一般为非抽象类方法。4)native方法在异地实现,象抽象方法一样,所以没有方法体,以分号结束。如下列5种声明: native public void f(); 正确。 + private native void f(); 正确。 + protected int native f(); 错误,返回类型位置不对,返回类型必须在native之后。 + public abstract native void f(); native必然不是abstract的。 + native int f(){} 错误,因为存在方法体{} + public static native f(); 正确。static与native方法位置随意。" + + 可参照 https://blog.csdn.net/x_panda/article/details/17120479 + +18. **正则表达式** + + 正则表达式/a+(bab)?(caac)*/,下列选项中该正则表达式的子集是?A + + A. /a{2} B. /a+(bab){0,1}(ca)+(ca)/ C. /(bab)(caca)/ D. /a(bab){2}(caac)*/ + + FutureTask : https://pdai.tech/md/java/thread/java-thread-x-juc-executor-FutureTask.html + +19. 加解密 + + **推荐使用的对称加密算法有: AES** + + **推荐使用的非对称算法有: RSA 推荐使用的数字签名算法有: DSA ECDSA** + +20. 若程序中要获取当前操作系统登录的用户名,应采用如下哪种方法?() + + A、提示用户输入 + + B、由环境变量获取:System.getEnv('USER') + + C、由JVM获取:System.getProperty('user.name') + + D、启动程序的时候,由程序输入参数输入 + + 从安全考虑和正确性考虑,A和D不行,我错误的选了B,但查规范,没有这样用的,直接获取“USER”,而且B也与操作系统相关。看来答案应该是C,JVM屏蔽了操作系统细节,且规范文档中倒是其它例子有用到。 + + + + http://ilearning.huawei.com/edx/subportal/portal/tseacademy?tab_name=tsea-software-development + +21. 安全编程规范 + + | 序号 | 类型 | 提要 | + | ---- | --------------- | ------------------------------------------------------------ | + | 1 | 安全目标 | 机密、完整、可用 | + | 2 | 不可信数据 | 文件、注册表、网络、环境变量、命令行、用户输入、用户态数据、进程间通信、函数参数、全局变量 | + | 3 | 可信数据校验 | 1、标准(文件路径)、归一(NFD编码 或 字符串java.text.Normalizer.normalize()/Normalizer.Form.NFKC) 2、输入正则清理,不推荐断言 3、黑白校验 4、输出脱敏 | + | 4 | SQL注入 | RDBMS:SQL注入(绕过检查、篡改数据、探测数据库结构、控制系统) 1、注入案例 ●没有引号,直接拼接字段的(拼接字段如果强转int可避免注入,但强转会出异常) ●order by 后拼接sql的 ●仅校验数据长度无效 2、措施 1、参数化预编译,重复调用效率高,推荐 (拼接后再预编译也可导致注入) 1.1、myBatis '$key$'替换**可注入**,#key#不可注入 1.2、存储过程也可注入 2、输入黑白校验 (黑、白并非推荐,无特殊要求时可优先考虑白) 3、转码,仅针对引号限制字段 | + | 5 | OS注入 命令注入 | OS注入(Dos攻击、篡改数据,隐藏恶意活动) 1、使用jdk的API、输入校验、转码(推荐,对参数注入无效) 2、使用&拼接命令注入,Runtime.exec()避免shell方式执行,Runtime.loadLibrary(用户输入)可能导致注入 3、ping.exe后的均为参数 4、目录遍历攻击,getCanonicalPath()替代getAbsolutePath() 5、POSIX下可使用exec,建议使用WIN32 API CreateProcess 6、外部输入禁止特殊字符对命令注入无效 | + | 6 | XML | 白名单、安全xml库(如dom4j)、转码 1、XXE(外部实体注入) 1.1、危害: ●获取服务端数据:]> ●探测内部网络:]> ●引用无限文件DOS ]> 1.2、措施【entities限定 或 重写 的都是XXE】 ●禁止包含实体 .setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);//DOCTYPE ●禁止使用外部实体和参数实体.setFeature("http://xml.org/sax/features/external-general-entities", false); .setFeature("http://xml.org/sax/features/external-parameter-entities", false); ●覆写解析器解析实体的方法 XMLReader resolveEntity(String publicId, String systemId) ●提取实体内容校验 比较基础数据值替代=/!=,防止异常时数值为负 4、内部类要么是static的,要么就不要实现接口 | + | 18 | 加密 | 1、不安全的加密算法: 分组加密DES、 哈希算法MD5 2、推荐的对称密码算法 AES, 非对称加密 RSA | + +22. 开发者测试 + + | 序号 | 类型 | 提要 | + | ---- | ------------ | ------------------------------------------------------------ | + | 1 | DT基础概念 | 1、UT(功能/函数单元测试,注重黑盒测试,关注的是功能、接口,可以DevOps中CI门禁)、IT(集成测试)、ST(系统/子系统测试) | + | 2 | DT测试设计 | 1、根据需求为达到性价比的测试设计,设计测试用例+;
2、测试要识别因子、因子值、方法。因子值要归类——等价类划分、边界;
因子值组合设计测试用例——AC(全排列组合,测试用例个数=各个因子的取值个数相乘)
BC(基础组合,测试用例个数=**各个因子的取值个数相加 - 因子个数 + 1** )
EC(Each Comosite, 测试用例个数=最多因子值的因子值个数) | + | 3 | 测试基础框架 | ( http://ilearning.huawei.com/edx/next/micro/course-v1:HuaweiX+CNE202007271113018-242+microcourse?blockID=8b4313d932774f83b749e38abaca493f )
Java测试基础框架——Junit,是Xunit中的实例:
Maven框架,Junit4.11及以上。一般流程为:(1)数据准备;(2)调用函数(测试);(3)断言
@Test ,函数命名一般为when…then…
高级断言——assertThat,统一了API,包括is\not\allof\anyof\…
Fixture——BeforeClass->RunWith->Before->TestMethod—>After->AfterClass
异常测试——(1)Try/Catch中要有断言;(2)assertThrow (3)@Test ExceptedException (4)@Test Except
超时——由于测试用例要满足F.I.R.S.T,其中Fast要求不能一直超时阻塞,@Test(timeout=X milliseconds)——适用于方法; @ Rule public Timeout globalTimeout = Timeout.seconds(3)
异常忽略说明——@Ignore(String description)
Junit4 参数化查询——{(输入,输出),},@RunWith(Parameterized.class)
Junit 进阶优化——Rules : @Rule, Suite:把目前用例都归档
Java测试框架——Mockito: | + +23. Java程序调试 + + http://ilearning.huawei.com/edx/courses/course-v1:HuaweiX+CNE050000006949+Self-paced/courseware/46151a32eb0e49969a4ab2da6e3f2a56/a181bd2cd9c145509d9434fbc02a1f39/?child=first + +24. + diff --git "a/\347\256\227\346\263\225--\344\271\235\351\230\263\347\245\236\345\212\237.md" "b/\347\256\227\346\263\225--\344\271\235\351\230\263\347\245\236\345\212\237.md" new file mode 100644 index 0000000000..f87b76be35 --- /dev/null +++ "b/\347\256\227\346\263\225--\344\271\235\351\230\263\347\245\236\345\212\237.md" @@ -0,0 +1,2132 @@ +[TOC] + +# 算法总结 + +| 经典算法 | 解决的问题 | 关键步骤 | 时间复杂度 | 空间复杂度 | 使用的数据结构 | 注意事项 | +| ------------------------------------------------------------ | ------------------------------------------------------------ | ------------------------------------------------------------ | ------------------------------------------------------------ | ---------- | ---------------- | ------------------------------------------------------------ | +| 递归思想(**入门**,自己调用自己)
**不要跳进递归,而是利用明确的定义来实现算法逻辑。** | 1、斐波那契数列fab(N)
(if(N<=1){return N;} return fab(N - 1) + fab(N - 2))
2、爬台阶/跳台阶 | 1、**大问题分解为小问题**(问题规模要变小,或难度要变易)——递推公式;明确当前层应返回什么?
2、**递归终止条件要明确**——没有终止条伯会进入死循环 | 与递归深度有关,
如Fib数为O(2^n).
如对于递归解决反转区间链表的算法,为O(n) | O(n) | 函数自己调用自己 | 处理的技巧是:不要跳进递归,而是利用明确的定义来实现算法逻辑。
1、不要思考整体,而是把目光聚焦局部,只看一个运算符。
说白了,解决递归相关的算法问题,就是一个化整为零的过程,你必须瞄准一个小的突破口,然后把问题拆解,大而化小,利用递归函数来解决。
2、明确递归函数的定义是什么,相信并且利用好函数的定义。

这也是前文经常提到的一个点,因为递归函数要自己调用自己,你必须搞清楚函数到底能干嘛,才能正确进行递归调用。 | +| | | | | | | | + +## 递归思想 (九阳神功入门,自己调用自己)) + +### 解决的问题 + +1、斐波那契数列 + +```java +fab(N) { + if(N<=1) + { + return N; + } + return fab(N - 1) + fab(N - 2); +} +``` + + + +2、爬台阶/跳台阶 + +### 关键步骤 + +1、**大问题分解为小问题(**问题规模要变小,或难度要变易)——递推公式;明确当前层应返回什么? +2、**递归终止条件要明确**——没有终止条伯会进入死循环 + +### 时间复杂度 + +与递归深度有关, +如Fib数为O(2^n). +如对于递归解决反转区间链表的算法,为O(n) + +### 空间复杂度 + +与具体逻辑有关 + +### 使用的数据结构 + +函数自己调用自己(只是传入的参数规模变小) + +### 注意事项 + +处理的技巧是:**不要跳进递归,而是利用明确的定义来实现算法逻辑**。
1、不要思考整体,而是把目光聚焦局部,只看一个运算符。
说白了,解决递归相关的算法问题,就是一个化整为零的过程,你必须瞄准一个小的突破口,然后把问题拆解,大而化小,利用递归函数来解决。
2、明确递归函数的定义是什么,相信并且利用好函数的定义。

这也是前文经常提到的一个点,因为递归函数要自己调用自己,你必须搞清楚函数到底能干嘛,才能正确进行递归调用。 + ** 有时会用到缓存来避免重复计算** +## 分治策略(九阳神功**入门**) + +### 解决的问题 + +1、快速排序(基准数+分区)/归并排序(分别有序,两个指针,合并)、傅立叶变换 +2、反转二叉树(根下翻转——DFS)、路径和 + +### 关键步骤 + +1、大问题分解为小问题(问题规模要变小,或难度要变易或解决方案很明确——**具有最优子结构(可以分解为规模较小的相同问题)**); +2、利用子问题的解可以合并为该问题的最终解。 + +```java +// 如下合并排序示例,是递归调用的。将数组不断分成两半,直至每个子数组仅有一个元素。然后再将它们合并成一个有序数组。 +class Solution { + public void sort(int[] nums, int lo, int hi) { + if (lo < hi) { + int mid = lo + (hi - lo) / 2; + + // 分 + // 对数组的两部分分别排序 + sort(nums, lo, mid); + sort(nums, mid + 1, hi); + + // 治 + // 合并两个排好序的子数组 + merge(nums, lo, mid, hi); + } + } + + // 合并升序 + private void merge(int[] nums, int lo, int mid, int hi) { + int[] tmp = new int[hi - lo + 1]; + + int i = lo; + int j = mid + 1; + int k = 0; + while (i <= mid && j <= hi) { + if (nums[i] <= nums[j]) { + tmp[k++] = nums[i++]; + } else { + tmp[k++] = nums[j++]; + } + } + while (i <= mid) { + tmp[k++] = nums[i++]; + } + while (j <= hi) { + tmp[k++] = nums[j++]; + } + + for (int l = 0; l < tmp.length; l++) { + nums[lo + l] = tmp[l]; + } + } +} +``` + +### 时间复杂度 + +O(lgN) + +### 空间复杂度 + +O(n) + +### 使用的数据结构 + +无固定,视问题而定。 +主要是思维 + +### 注意事项 + +1、不要思考整体,而是把目光聚焦局部,只看一个运算符。 +说白了,解决递归相关的算法问题,就是一个化整为零的过程,你必须瞄准一个小的突破口,然后把问题拆解,大而化小,利用递归函数来解决。 +2、明确递归函数的定义是什么,相信并且利用好函数的定义。 + +这也是前文经常提到的一个点,因为递归函数要自己调用自己,你必须搞清楚函数到底能干嘛,才能正确进行递归调用。 + +分治分治,分而治之,这一步就是把原问题进行了「分」,我们现在要开始「治」了。 + +1 + 2 * 3 可以有两种加括号的方式,分别是: + +(1) + (2 * 3) = 7 + +(1 + 2) * (3) = 9 + +或者我们可以写成这种形式: + +1 + 2 * 3 = [9, 7] + +而 4 * 5 当然只有一种加括号方式,就是 4 * 5 = [20]。再回头看下题目给出的函数签名: +这就是我们之前说的第二个关键点,明确函数的定义,相信并且利用这个函数定义。 + +```java +List diffWaysToCompute("(1 + 2 * 3) - (4 * 5)") { + List res = new LinkedList<>(); + /****** 分 ******/ + List left = diffWaysToCompute("1 + 2 * 3"); + List right = diffWaysToCompute("4 * 5"); + /****** 治 ******/ + for (int a : left) + for (int b : right) + res.add(a - b); + + return res; + +} +``` + + + +// 回顾九阳神功第二式——分治思想。 + * 开始拿到题目: + * 给你一个由数字和运算符组成的字符串 expression ,按不同优先级组合数字和运算符,计算并返回所有可能组合的结果。你可以 按任意顺序 返回答案。 + * 符号范围为+、-、*,表达式中数字为【0,99】,表达式长度在20个字符长度类 + * 输入一个表达式字符串,要返回不同可能组合的结果值。怎么看,都应该是穷举遍历类算法--不过,不管怎样,都要递归。 + * --再按我们正常计算表达式的逻辑,其实就是要计算某一个符号的左边值与右边值,按算术计算后的结果值。 + + ```java + class Solution2 { + private HashMap> memo = new HashMap(); + // 分治和递归,考虑加括号的位置-只考虑每个计算,不要考虑整体,瞄准一个小口突破 + List diffWaysToCompute(String input) { + + + List resultList = new ArrayList<>(); + + int strLen = input.length(); + for (int i = 0; i < strLen; i++) { + char curChar = input.charAt(i); + if (isAdd(curChar) || isMinus(curChar) || isMulti(curChar)) { + // 分 + // 以运算符分成运算符的两边,分别计算 + List left = diffWaysToCompute(input.substring(0, i)); + List right = diffWaysToCompute(input.substring(i + 1, strLen)); + + // 治 + // 根据子问题的结果合成原问题的结果 + for (Integer leftEle : left) { + for (Integer rightEle : right) { + if (isMulti(curChar)) { + int result1 = leftEle * rightEle; + resultList.add(result1); + } else if (isAdd(curChar)) { + int result2 = leftEle + rightEle; + resultList.add(result2); + } else if (isMinus(curChar)) { + int result3 = leftEle - rightEle; + resultList.add(result3); + } + } + } + } + } + + // 经过递归之后,如果仍没有结果 + if (resultList.isEmpty()) { + resultList.add(Integer.parseInt(input)); + } + return resultList; + } + + public static boolean isMulti(char c) { + if (c == '*') { + return true; + } + return false; + } + + public static boolean isAdd(char c) { + if (c == '+') { + return true; + } + return false; + } + + public static boolean isMinus(char c) { + if (c == '-') { + return true; + } + return false; + } + } + ``` + + ​ + +## 单调栈&单调队列(九阳神功第1式)********* + +### 解决的问题 + + (1)栈内(队列内)的存在单调顺序 + (2)元素入栈之前会将破坏单调性的元素进行处理(因此,入栈前一般有一个while类循环) + + 单调栈解决:元素左边第一个比当前观察元素小(单调递增栈) + 元素左边第一个比当前观察元素大(单调递减栈),例如接雨水、每日温度、柱状图中最大面积 + 单调队队解决:求区间【0,i】的最值问题,单调队列能做,单调栈不行。 + ——通常应用在一维数组上 + 20230805回顾温习—重新默写单调栈解决行星碰撞问题,为什么用单调栈—严格来说,行星碰撞问题不是单调栈,但如果把碰撞情况作为破坏单调情况,则就可以理解单调栈。为什么用单调栈。其实就是利用栈的后进先出特性同时用栈是作为辅助记忆空间,刚好栈的后进先出特性相当于当前元素去与原来的以遍历的元素中从后往前去一一比较操作 + +### 关键步骤 + + ```java + // 给你一个数组 temperatures,这个数组存放的是近几天的天气气温,你返回一个等长的数组,计算:对于每一天,你还要至少等多少天才能等到一个更暖和的气温;如果等不到那一天,填 0。 +import java.util.Arrays; +import java.util.Stack; + +/** + * 可信认证,九阳神功第一式:单调栈或单调队列 + * + * 单调栈解决:元素左边第一个比当前观察元素小(单调递增栈) 元素左边第一个比当前观察元素大(单调递减栈), + * + * 即解决:--找出不按套路出牌的那个元素, + * + * (1)栈内(队列内)的存在单调顺序--这是正常时候 + * (2)元素入栈之前会将破坏单调性的元素进行处理(因此,入栈前一般有一个while类循环) + * + * 本题: 给你一个数组 temperatures,这个数组存放的是近几天的天气气温,你返回一个等长的数组, + * 计算:对于每一天,你还要至少等多少天才能等到一个更暖和的气温;如果等不到那一天,填 0 + */ + +public class LeftDayForNextHotDay { + public static void main(String[] args) { + int[] temperatures1 = new int[]{73, 74, 75, 71, 69, 72, 76, 73}; + + int[] res1 = dailyTemperatures(temperatures1); + System.out.println(Arrays.toString(res1)); + } + + public static int[] dailyTemperatures(int[] tempratures) { + int[] res = new int[tempratures.length]; + + // 单调递减栈: 本来如果一天比一天温度低或不变,则不需要处理,只有当出现某一天温度上升了,破坏了一直不用处理的一天比一天不 + Stack descendStack = new Stack<>(); + + for (int i = 0; i < tempratures.length; i++) { + // 当前元素 + int curTemp = tempratures[i]; + + // 栈非空时往里面加入比当前栈顶索引指向的元素大的元素--破坏了栈的单调性时 + while (!descendStack.isEmpty() && curTemp >= tempratures[descendStack.peek()]) { + // + // 得到索引间距 + res[descendStack.peek()] = i - descendStack.peek(); + + // 出栈不满足递减的在栈中的元素 + descendStack.pop(); + } + + // 直到当前元素能入栈满足单调递减 + descendStack.push(i); + } + return res; + } +} + ``` + + + + // 单调队列且队首和队尾都可以进行出队操作(因为队尾、队首都要操作——因此需要使用deque数据结构),只有队尾能进入。 单调递增队列:从队尾往前扫描,比当前元素大的出队列,直至碰到比之小的或队列为空,则当前元素入队列(跟单调递减栈的操作一样)。 + +### 时间复杂度 + + O(n) + +### 空间复杂度 + + O(n) + +### 使用的数据结构 + + 单调栈常用: + 直接用栈Stack<> = new Stack(); + queue = new ArrayDequeue()或 + stack = new LinkedList()或dequeue + 单调队列常用:dequeue + +### 注意事项 + + 暂无 + +## 并查集(九阳神功第2式) + +### 解决的问题 + +(1)维护无向图连通性; +(2)判断是否产生环--Kruskal算法(最小生成树加边法) +朋友圈、冗余边、句子相似性、岛屿周长/最大面积/人工岛 + +### 关键的步骤 + +用集合中的元素来代表该集合(**优化**:**按秩合并、路径压缩**) +if (parent[x] == x) //是根时 + return x; +find(parent, parent[x]) + +### 时间复杂度 + +O(lgN) + +### 空间复杂度 + +O(n) + +### 使用的数据结构 + +数组 + +### 注意事项 + +暂无 + +## 滑动窗口(九阳神功第3式)********* + +### 解决的问题 + +找出满足XX条件的最YY的区间(子数组/子串) +滑动窗口算法可以用以解决数组/字符串的子元素问题,它可以将嵌套的循环问题转换为单循环问题,从而降低时间复杂度 + +### 关键步骤 + +解决一些查找满足一定条件的连续区间的性质(长度、最值等) +——由于区间是连续的,当区间变化,通过旧有计算结果、剪枝优化 + +```java +String slideWindow(String Str) { + 异常及边界判断; + 左指针初始为0; + for(int right = 0; right < Str.length; right++){ + 进行窗口内的一系列更新; + /*** debug 输出的位置 ***/ + printf("window: [%d, %d)\n", left, right); + while(判断左窗口是否需要收缩) { + 左指针向前滑动缩小窗口; + 进行窗口内数据的更新 + } + } +} +``` + +### 时间复杂度 + +O(n) + +### 空间复杂度 + +O(1) + +### 使用的数据结构 + +临时变量、两个指针/索引 + +### 注意事项 + +暂无 + +## 前缀和(九阳神功第4式) + +### 解决的问题 + +和为K的子数组(**如果不连续——不能用**,不连续的话,预处理没有用); +——**通过预处理,降低查询时间** +**典型题目:和为K的子数组、和被K整除的子数组、连续的子数组之和** + +### 关键步骤 + +```java +prefixSum[0]=0; +for(int i = 0; i < num.length; i++) { + prefixSum[i + 1] = prefixSum[i] + num[i]; +} +``` + +优化点:map<前缀和, 次数> + +### 时间复杂度 + +O(n) + +### 空间复杂度 + +O(n) + +### 使用的数据结构 + +数组 + +### 注意事项 + +暂无 + +## 差分(九阳神功第5式) + +### 解决的问题 + +Bi = Ai - Ai-1,即相邻两数之差。差分数组中,**每个点上记录变化值**。(因有增有减,通过求和判断 是否有超过指定容量的情况发生,超过则不满足要求) +典型问题:**拼车、航班、买股票最佳时机** + +### 关键步骤 + +```java +Bi = Ai - Ai-1 +``` + +### 时间复杂度 + +O(n) + +### 空间复杂度 + +O(n) + +### 使用的数据结构 + +数组 + +### 注意事项 + +暂无 + +## 拓扑排序(九阳神功第6式)——专业级 + +### 解决的问题 + +算法:(1)从DAG图中找出一个没有前驱(即入度为0)的顶点并输出;(2)从图中删除该顶点和所有以它为起点的边;(3)重复(1)和(2)步骤,直到DAG图为空,或者没有入度为0的顶点为止。 +典型问题:**序列重建、课程表、火星词典** + +### 关键步骤 + +```java + +``` + +### 时间复杂度 + +O(n) + +### 空间复杂度 + +O(n) + +### 使用的数据结构 + +有向无环图(DAG,Directed Acyclic Graph)的所有顶点的线性序列。 +每个顶点在DAG中出现且仅出现一次;若图中有A到B的路径,那么表示对应序列中A在B前。 + +### 注意事项 + +暂无 + +## 字符串(九阳神功第7式) + +### 解决的问题 + +典型问题:最长回文子串、(1)字符串的子串(2)字符串的子序列、复原IP地址、字符串相乘、中缀转后缀、字符串最短路径、计算器、日志敏感过滤 + +### 关键步骤 + +```java +[i, j)=Str.substring(i,j); +右边往中间走(回文串):Str.charAt(i) = Str.charAt(len - i - 1) +``` + +### 时间复杂度 + + + +### 空间复杂度 + + + +### 使用的数据结构 + +字符串。解决字符串问题的有时候也会用**滑动窗口、栈、DFS\BFS、Hash、二分查找** + +### 注意事项 + +暂无 + +## 二分查找(九阳神功第8式)************* + +### 解决的问题 + +前提:数据已排好序(如果未排好序,则给它排好序) +特点:(1)**能随机访问--所以一般是数组**;(2)**中间能推断两边** +典型问题:**搜索二维矩阵、寻找两个有序数组的中位数、搜索旋转排序数组** +**如何判断使用二分:** +(1)序列**是有序的且数据量巨大**,需要对处理时间复杂度优化; +(2)给出**一个要达到的目标值**,求出某个自变量能满足目标的最小/最大值,该**自变量和目标之间存在单调关系(单调增或单调减)**。 +(3)寻找一个数、寻找左侧边界、寻找右侧边界 + +### 关键步骤 + +```java +// low,mid,high;根据目标与mid的比对,重新确定查找区间 +public int searchInsert(int[] arr, int target) { + int left = 0; + int right = arr.length - 1; + while(left <= right) { + int mid = left + ((right - left)>> 1); + if(arr[mid] == target) { + 做相关处理; + }else if(arr[mid] < target){ + left = mid + 1; + }else { + right = mid - 1; + } + } + return 相关值; +} +``` + +### 时间复杂度 + +O(lgN) + +### 空间复杂度 + +O(n) + +### 使用的数据结构 + +数组 + +### 注意事项 + +暂无 + +## BFS广搜(第9式) + +### 解决的问题 + +**层序遍历、最短路径**——优化:多源BFS(只要把多个源点同时放入初始队列——已知目标顶点的情况下,可以分别从起点和目标顶点(终点)执行广度优先遍历,直到遍历的部分有交集,这是双向广度优先遍历的思想) + +**典型问题: 单词拆分、被围区域、迷宫、22年3月4日的地铁最优换乘线路(站点最少、同样少时换乘最少)** + +### 关键步骤 + +```java +void bfs(TreeNode root){ + if(root == null) return null; + Queue(TreeNode) queue = new ArrayDeQueue(); + queue.add(root); + while(!queue.isEmpty()) { + // Java中队列 用 offer/poll进队列/出队列; + // 堆栈用push/pop + TreeNode curNode = queue.poll(); + if (curNode.left != null) { + queue.offer(curNode.left); + } + if (curNode.right != null) { + queue.offer(curNode.right); + } + } +} + +// 实际复杂的树如果真的需要一层遍历完后,则如下: +private void bfs(int coreCount, BinTreeNode root) { + if (root == null) { + return; + } + Queue treeQueue = new ArrayDeque<>(); + treeQueue.offer(root); + while (!treeQueue.isEmpty()) { + List curLayerNodeLst = new ArrayList<>(); + while (!treeQueue.isEmpty()) { + BinTreeNode curNode = treeQueue.poll(); + curLayerNodeLst.add(curNode); + } + if (curLayerNodeLst.isEmpty()) { + break; + } + // 当前层的操作符个数统计 + int tmpOperatorCnt = 0; + for (BinTreeNode node : curLayerNodeLst) { + if (node.nodeType == BinTreeNode.OPERATOR) { + tmpOperatorCnt++; + } + if (node.left != null) { + treeQueue.add(node.left); + } + if (node.right != null) { + treeQueue.add(node.right); + } + } + resultCnt += tmpOperatorCnt / coreCount; + if (tmpOperatorCnt % coreCount != 0) { + resultCnt += 1; + } + } + } +``` + +### 时间复杂度 + +O(lgN) + +### 空间复杂度 + +O(n) + +### 使用的数据结构 + +Queue、 +HashSet + +### 注意事项 + +需要**注意剪枝**,避免形成环(基于图这一数据结构),都有visited记录已访问过的顶点,避免无穷搜索下去! +遍历二维数组——如果你把二维矩阵中的每一个位置看做一个节点,这个节点的上下左右四个位置就是相邻节点,那么整个矩阵就可以抽象成一幅网状的「图」结构。——二维矩阵本质上是一幅「图」,**所以遍历的过程中需要一个visited布尔数组防止走回头路** + +## DFS深搜&回溯(第10式) + +### 解决的问题 + +遍历或搜索树和图。——可以产生目标图的拓扑排序--无权最长路径问题。 +难点在于模型转换——虽然用对应类的逻辑概念来表示也可,实际上用的较多的是(1)邻接矩阵; (2)邻接表 +**遍历二维数组(岛屿数量问题https://leetcode.cn/problems/number-of-islands/):** + +对于**回溯——在于是否选择某一点,选择和不选择结果可能不一样。** +**回溯算法通常用于解决组合问题、排列问题、子集问题、棋盘问题、迷宫问题等。回溯算法的核心思想是穷举所有可能的解,并在搜索过程中剪枝,以避免不必要的搜索。** + +**回溯算法的时间复杂度和空间复杂度都与搜索树的大小相关。**在最坏情况下,搜索树的大小与解的数量成指数关系,因此回溯算法的时间复杂度和空间复杂度都是指数级别的。但是,在实际应用中,通过剪枝等优化措施,可以大大减少搜索树的大小,从而提高算法的效率。O(N!) +**关键点:** + +**(1)都是基于递归——因此都需要注意结束条件,注意每层调用应该返回的值。** + +**(2)回溯步骤基本是看结束条件,做出选择,递归调用,撤销选择;DFS则是判断结束条件,按先序遍历/中序遍历/后序遍历策略递归调用。** + +### 关键步骤 + +```java +void dfs(TreeNode root) { + // 判断边界 + if (root == null) { + return; + } + dfs(root.left); + dfs(root.right); +} + +或 +void dfs(parent, depth, parentChildrenMap, result) { + List<> children = parentChildrenMap.get(parent_depth); + if (children == null) { + result.add(result); + return; + } + for (child: children) { + dfs(child, depth + 1, parentChildrenMap, result); + } + // 如果需要父节点 + result.add(parent); +} + +// 回溯 +/** + * 输入一组字符串,要求输出满足下面条件的字符串拼接结果,求其长度的最大值 + * 条件: 拼接结果中所有字母唯一,不存在重复----去重 + * 例1--“un”、"iq"、"ue",有3!=6 种结果,最长的无重复字付的拼接结果为“ique”或者"ueiq"(不用第一个字符串), 因此输出结果应该为4 + * 例2--“赵克明hfasjefusidfhjd”, 输出应该为0 + * + * @author h00805266 + * @since 2023-05-20 + */ +public class DynamicPlan { + public static void main(String[] args) { + Solution solution = new Solution(); + + String[] test1 = new String[] {"un", "iq", "ue"}; + int result1 = solution.getMaxUniqStringLen(test1); + System.out.println(result1); + + String[] test2 = new String[] {"hfasjefusidfhjd"}; + int result2 = solution.getMaxUniqStringLen(test2); + System.out.println(result2); + } +} + +class Solution { + private int maxLength = 0; + + // 算法复杂度为 O(n * 2^n) + public int getMaxUniqStringLen(String[] strs) { + maxLength = 0; + if (strs == null || strs.length == 0) { + return 0; + } + + // StringBuffer线程安全,但速度慢。 StringBuilder非线程安全,但速度快。记录拼接后的字符串 + StringBuilder sb = new StringBuilder(); + + // 记录每个字符是否出现过 + boolean[] used = new boolean[26]; + + // 采用回溯算法 + backtrack(strs, sb, used); + + return maxLength; + } + + /** + * 回溯法--DFS中退出前,回退之前选择 + * + * @param strs 字符串数组 + * @param sb 拼接结果 + * @param used 字符出现记录 + */ + private void backtrack(String[] strs, StringBuilder sb, boolean[] used) { + if (sb.length() > maxLength) { + System.out.println("temporary appended string: " + sb.toString()); + maxLength = sb.length(); + } + + for (String str : strs) { + boolean canAppend = true; + + // 先看该字符串中是否有出现过的字符 + for (char c : str.toCharArray()) { + if (used[c - 'a']) { + // 已经出现过 + canAppend = false; + // 该字符串不能被拼接,本字符串的处理退出 + break; + } + // 根据例2,单字符串中也不允许出现重复字符 + used[c - 'a'] = true; + } + + if (canAppend) { + // 记录新字符串中字符出现过 + for (char c : str.toCharArray()) { + used[c - 'a'] = true; + } + sb.append(str); + backtrack(strs, sb, used); + + // 为什么回溯--因为有可能前面做的选择影响后面的 + sb.delete(sb.length() - str.length(), sb.length()); + for (char c : str.toCharArray()) { + used[c - 'a'] = false; + } + } // if + } + } +} +``` + +### 时间复杂度 + +O(lgN) + +### 空间复杂度 + +O(n) + +### 使用的数据结构 + +树节点的数据结构可以用语言本身提供的map/dict,也可以自己写一个类/结构体。如果C语言要用树这种数据结构,可以定义类似下面的结构体: + +```c++ +typedef struct tagTreeNode { + + char name[LINE_BUF_LEN]; // 目录名 + + struct tagTreeNode * children[100]; + + int childCount; + +}TreeNode; +``` + +在建立树的过程中,关键是要找到当前层次的父节点,将当前节点添加到父节点下面,从而构造出整棵树。 + +### 注意事项 + +需要**注意剪枝**,避免形成环(基于图这一数据结构),都有visited记录已访问过的顶点,避免无穷搜索下去! +遍历二维数组——如果你把二维矩阵中的每一个位置看做一个节点,这个节点的上下左右四个位置就是相邻节点,那么整个矩阵就可以抽象成一幅网状的「图」结构。——二维矩阵本质上是一幅「图」,**所以遍历的过程中需要一个visited布尔数组防止走回头路** + +## 动态规划(第11式) + +### 解决的问题 + +最优子优解结构——从子问题的最优解推导出原问题的**最优解 (变化的量就是状态)** +01背包思路——对每一个状态i,枚举下一个状态j,如果拼接后的字符串中所有字母唯一,则更新状态dp[next]的值。最终,遍历所有状态,求出最大值即为所求。 +解决一个背包最多能装多少价值的问题,其中每个物品只有一个,且不能分割。这种问题通常被称为0/1背包问题。 + +### 关键步骤 + +```java +public class Knapsack { + /** + * 01背包算法 + * @param W 背包容量 + * @param wt 物品重量数组 + * @param val 物品价值数组 + * @param n 物品数量 + * @return 背包能装下的最大价值 + */ + public static int knapSack(int W, int[] wt, int[] val, int n) { + int[][] dp = new int[n + 1][W + 1]; // 创建二维数组,用于存储子问题的最优解 + + // 初始化第一行和第一列为0,表示背包容量为0或没有物品可选时,最大价值为0 + for (int i = 0; i <= n; i++) { + dp[i][0] = 0; + } + for (int j = 0; j <= W; j++) { + dp[0][j] = 0; + } + + // 填充dp数组,选择每一件物品下每一种容量情况下的 + for (int i = 1; i <= n; i++) { + for (int j = 1; j <= W; j++) { + if (wt[i - 1] <= j) { // 当前物品重量小于等于背包容量 + dp[i][j] = Math.max(val[i - 1] + dp[i - 1][j - wt[i - 1]], dp[i - 1][j]); + // 取当前物品和不取当前物品的最大值 + } else { // 当前物品重量大于背包容量 + dp[i][j] = dp[i - 1][j]; // 不取当前物品 + } + } + } + + return dp[n][W]; // 返回背包能装下的最大价值 + } + + public static void main(String[] args) { + int[] val = new int[]{60, 100, 120}; // 物品价值数组 + int[] wt = new int[]{10, 20, 30}; // 物品重量数组 + int W = 50; // 背包容量 + int n = val.length; // 物品数量 + System.out.println(knapSack(W, wt, val, n)); // 输出背包能装下的最大价值 + } +} +// 上面为01背包的模板,其中,`w`数组表示每个物品的重量,`v`数组表示每个物品的价值,`C`表示背包的容量。`dp[i][j]`表示前`i`个物品放入容量为`j`的背包中所能获得的最大价值。 + +// 虽然这个算法结果错误,但对于演示动态规划步骤是一个参考 +class Solution2 { + // 动态规划----01背包思路,对每一个状态i,枚举下一个状态j,如果拼接后的字符串中所有字母唯一,则更新状态dp[next]的值。最终,遍历所有状态,求出最大值即为所求。 + public int getMaxUniqStringLen(String[] strs) { + if (strs == null || strs.length == 0) { + return 0; + } + boolean[] used = new boolean[26]; + // 2^n, 状态为i时的最大长度 + int[] dp = new int[1 << strs.length]; + + for (int i = 0; i < strs.length; i++) { + if (!checkAndSetUsed(strs[i], used)) { + dp[1 << i] = strs[i].length(); + } + + } + + // 枚举每一种状态 + for (int i = 0; i < (1 << strs.length); i++) { + if (dp[i] == 0) { + continue; + } + for (int j = 0; j < strs.length; j++) { + if ((i & (1 << j)) != 0) { + continue; + } + + // 下一个状态 + int next = i | (1 << j); + boolean flag = true; + for (int k = 0; k < strs[j].length(); k++) { + if ((i & (1 << (strs[j].charAt(k) - 'a'))) != 0) { + flag = false; + break; + } + } + if (flag) { + dp[next] = Math.max(dp[next], dp[i] + strs[j].length()); + } + } + } + int ans = 0; + for (int i = 0; i < (1 << strs.length); i++) { + ans = Math.max(ans, dp[i]); + } + return ans; + } + + /** + * 检查字符串中是否出现过重复字符并且记录字符已出现 + * + * @param str + * @return + */ + private boolean checkAndSetUsed(String str, boolean[] used) { + if (str == null || str.length() == 0) { + return false; + } + + for (char c : str.toCharArray()) { + if (used[c - 'a']) { + return true; + } + used[c - 'a'] = true; + return false; + } + return false; + } +} +``` + +### 时间复杂度 + +时间复杂度为O(N*V),其中N为物品的数量,V为背包的容量 + +### 空间复杂度 + +空间复杂度为O(V),因为只需要一个一维数组来存储状态 + +### 使用的数据结构 + +数组 + +### 注意事项 + +暂无 + +## 贪心算法(第12式)********* + +### 解决的问题 + +主要用来解决需要“**找到要做某事的最小次数**”或“**找到某些情况下适合的最大物品数量**” +典型问题:**最大硬币问题、最多区间、字典最小点覆盖、0/1背包、最少加油次数(https://mp.weixin.qq.com/s/k-z_oewAqMYc3vpmOm4gEQ)** + +### 关键步骤 + +下面我们介绍两种方法巧解这道题,分别是**数学图像解法**和**贪心解法**。 +主要是**每一步都选择最优方案,最终获得全局方案**。 +**贪心思路的本质,如果找不到重复计算,那就通过问题中一些隐藏较深的规律,来减少冗余计算。** + +### 时间复杂度 + + + +### 空间复杂度 + + + +### 使用的数据结构 + +无固定,视问题而定。主要是**思维。** + +### 注意事项 + +暂无 + +## 字典树(Trie)& 哈希树(冲突存在) + +### 解决的问题 + +典型问题:**检索字符串数据集中的键、自动补全、IP路由(最长路径匹配)、前序排序** + +### 关键步骤 + +键的插入、查找 + +### 时间复杂度 + +O(m)或O(mlogN) + +### 空间复杂度 + +O(n) + +### 使用的数据结构 + +科目一认证题纲中考察的基础数据结构中字符串 + +### 注意事项 + +虽然本题也可以用BFS完成,但是更适合用DFS来解;BFS适合查找最短路径等场景。 + +员工1/2/4的解法,最坏情况下,时间复杂度是O(N3);员工3的Java解法时间复杂度是O(N2);员工5的Go解法时间复杂度是O(N)。 + +在日常工作中,如果数据量不大,我们也可以采用一些更容易理解,或者工作量更小、占用内存少的实现方案。 + +寻找最近公共祖先可以参考leetcode 236. 二叉树的最近公共祖先 https://leetcode.cn/problems/lowest-common-ancestor-of-a-binary-tree/ + +2) 树的构建 + +按层序恢复二叉树,参考【案例开放010】1106上机编程“服务器组的均匀拆分”学习交流 http://3ms.huawei.com/km/groups/3803117/blogs/details/9328645?l=zh-cn + +按路径分隔符提供树的层次关系,参考【案例开放047】0813上机编程“文件树”学习交流 http://3ms.huawei.com/km/groups/3803117/blogs/details/10851537?l=zh-cn + +3) 树的深度计算和遍历 + +高效计算节点的最大深度,参考【案例开放053】0924上机编程“二叉树最大深度”学习交流 http://3ms.huawei.com/km/groups/3803117/blogs/details/11060743 + +## 字典序问题 + +### 解决的问题 + +​ 字符串比较:Java String类实现了compareTo接口,可用于两个字符串的比较(基于**字典序**),用法为str1.compareTo(str2)——返回一个整数值,按字典序str1等于str2字符串时返回0,str1大于str2时返回值大于0,小于时返回值小于0。 + +​ 字符串序列排序:sort 函数可用于字符串序列的排序,默认按字典序升序排序。基于sort函数有多种方式可以实现降序排序,同时存储字符串可用List或Array: + + + +### 关键步骤 + +### 时间复杂度 + +### 空间复杂度 + +### 使用的数据结构 + +### 注意事项 + +## 图的遍历 + +### 解决的问题 + +https://labuladong.github.io/algo/di-yi-zhan-da78c/shou-ba-sh-03a72/tu-lun-ji--d55b2/ + +### 关键步骤 + +```java +/ 图和树有稍微的差别——树一定最终到叶子节点,图则可能成环 + +// 图的遍历 +// 记录被访问过的节点 +boolean[] visited; + +// 记录从起点到当前节点的路径 +boolean[] onPath; + +// 图的遍历框架 +void traverse(Graph graph, int s) { + if (visited[s]) return; + // 经过s节点,记录为已遍历 + visited[s] = true; + + // 用了回溯?——区别: 回溯中 做选择 和 撤销选择 在 for循环里面, 而对于onPath的数组的操作在for循环外 + // 做选择: 标记S节点在路径上 + onPath[s] = true; + for (int neighbor: graph.neighbors(s)) { + traverse(graph, int neighbor); + } + // 撤销选择: s节点离开路径 + onPath[s] = false; +} +``` + + + +### 时间复杂度 + + + +### 空间复杂度 + + + +### 使用的数据结构 + +**邻接表: L**ist[] graph; // 这样每个graph[x]就是x的邻接点,这个是没有权重的。 +如果加上权重,List [] graph;// graph[x]即是x的所有邻接点列表,例如,graph[x].get(0)[0]可以是第一个邻接点的值,,graph[x].get(0)[1]可以是第一个邻接点的权重。 +**邻接矩阵:** matrix[][], matrix[x][y]可以为x到y的权重。 + +### 注意事项 + +无 + +# 重要算法模板 + +| 重要算法 | 重要程序 | 算法模板 | 算法示例 | +| -------- | -------- | -------- | -------- | + +## 滑动窗口(https://labuladong.github.io/algo/di-ling-zh-bfe1b/wo-xie-le--f02cd/) + +### 重要程序 + +5星 + +### 算法模板 + +```java +// 注意:java 代码由 chatGPT🤖 根据我的 cpp 代码翻译,旨在帮助不同背景的读者理解算法逻辑。 +// 本代码还未经过力扣测试,仅供参考,如有疑惑,可以参照我写的 cpp 代码对比查看。 + +/* 滑动窗口算法框架 */ +void slidingWindow(String s) { + // 用合适的数据结构记录窗口中的数据 + Map window = new HashMap(); + + int left = 0, right = 0; + while (right < s.length()) { + // c 是将移入窗口的字符 + char c = s.charAt(right); + window.put(c, window.getOrDefault(c, 0) + 1); + // 增大窗口 + right++; + // 进行窗口内数据的一系列更新 + ... + + /*** debug 输出的位置 ***/ + // 注意在最终的解法代码中不要 print + // 因为 IO 操作很耗时,可能导致超时 + System.out.printf("window: [%d, %d)\n", left, right); + /********************/ + + // 判断左侧窗口是否要收缩 + while (left < right && window needs shrink) { + // d 是将移出窗口的字符 + char d = s.charAt(left); + window.put(d, window.get(d) - 1); + // 缩小窗口 + left++; + // 进行窗口内数据的一系列更新 + ... + } + } +} +``` + +### 算法示例代码 + +```java +/** + * https://leetcode.cn/problems/longest-substring-without-repeating-characters/description/ + * + * @author h00805266 + * @since 2023-03-10 + */ +public class LengthOfLongestSubstring { + public static void main(String[] args) { + Solution solution = new Solution(); + + String s1 = "abcabcbb"; + int result1 = solution.lengthOfLongestSubstring(s1); + System.out.println(result1); + + String s2 = "bbbbb"; + int result2 = solution.lengthOfLongestSubstring(s2); + System.out.println(result2); + + String s3 = "pwwkew"; + int result3 = solution.lengthOfLongestSubstring(s3); + System.out.println(result3); + } +} + +class Solution { + public int lengthOfLongestSubstring(String s) { + if (s == null || s.length() == 0) { + return 0; + } + // 初始左指针 + int left = 0; + int right = 0; + + // 临时变量,记录临时最大长度 + int maxSubArrLen = Integer.MIN_VALUE; + // 记录窗口中各个字符的出现次数 + Map charCntMap = new HashMap<>(); + while (right < s.length()) { + charCntMap.put(s.charAt(right), charCntMap.getOrDefault(s.charAt(right), 0) + 1); + + // 开始出现重复,则开始收缩窗口的起点 + while (charCntMap.get(s.charAt(right)) >= 2) { + // 更新窗口各字符的记录 + charCntMap.put(s.charAt(left), charCntMap.get(s.charAt(left)) - 1); + + // 窗口起点往后移 + left++; + } + maxSubArrLen = Math.max(maxSubArrLen, right - left + 1); + right++; + } + + return maxSubArrLen == Integer.MIN_VALUE ? 0 : maxSubArrLen; + } +} +``` + +## 二分查找(https://labuladong.github.io/algo/di-ling-zh-bfe1b/wo-xie-le--3c789/) + +### 重要程度 + +5星 + +### 算法模板 + +```java +int binarySearch(int[] nums, int target) { + int left = 0, right = ...; + + while(...) { + int mid = left + (right - left) / 2; + if (nums[mid] == target) { + ... + } else if (nums[mid] < target) { + left = ... + } else if (nums[mid] > target) { + right = ... + } + } + return ...; +} + // 其中 ... 标记的部分,就是可能出现细节问题的地方; 计算 mid 时需要防止溢出,代码中 left + (right - left) / 2 就和 (left + right) / 2 的结果相同,但是有效防止了 left 和 right 太大,直接相加导致溢出的情况。 +``` + + + +### 算法代码示例 + +```java +// 寻找一个数,基本的查找 +int binarySearch(int[] nums, int target) { + int left = 0; + int right = nums.length - 1; // 注意 + + while(left <= right) { + int mid = left + (right - left) / 2; + if(nums[mid] == target) + return mid; + else if (nums[mid] < target) + left = mid + 1; // 注意 + else if (nums[mid] > target) + right = mid - 1; // 注意 + } + return -1; +} +// 前者 [left, right] 两端都闭的区间。这个区间其实就是每次进行搜索的区间。搜索区间为空的时候应该终止 + +比如说给你有序数组 nums = [1,2,2,2,3],target 为 2,此算法返回的索引是 2,没错。但是如果我想得到 target 的左侧边界,即索引 1,或者我想得到 target 的右侧边界,即索引 3,这样的话此算法是无法处理的。 + +这样的需求很常见,你也许会说,找到一个 target,然后向左或向右线性搜索不行吗?可以,但是不好,因为这样难以保证二分查找对数级的复杂度了 + +// 寻找左侧边界 +int left_bound(int[] nums, int target) { + int left = 0; + int right = nums.length; // 注意 + + while (left < right) { // 注意 + int mid = left + (right - left) / 2; + if (nums[mid] == target) { + right = mid; + } else if (nums[mid] < target) { + left = mid + 1; + } else if (nums[mid] > target) { + right = mid; // 注意 + } + } + return left; +} + +// 寻找右边界 +int right_bound(int[] nums, int target) { + int left = 0, right = nums.length; + + while (left < right) { + int mid = left + (right - left) / 2; + if (nums[mid] == target) { + left = mid + 1; // 注意 + } else if (nums[mid] < target) { + left = mid + 1; + } else if (nums[mid] > target) { + right = mid; + } + } + return left - 1; // 注意 +} +``` + +## 并查集(https://labuladong.github.io/algo/di-yi-zhan-da78c/shou-ba-sh-03a72/bing-cha-j-323f3/) + +### 重要程度 + +4星 + +### 算法模板 + +```java +class UF { + // 连通分量个数 + private int count; + // 存储每个节点的父节点 + private int[] parent; + + // n 为图中节点的个数 + public UF(int n) { + this.count = n; + parent = new int[n]; + for (int i = 0; i < n; i++) { + parent[i] = i; + } + } + + // 将节点 p 和节点 q 连通 + public void union(int p, int q) { + int rootP = find(p); + int rootQ = find(q); + + if (rootP == rootQ) + return; + + parent[rootQ] = rootP; + // 两个连通分量合并成一个连通分量 + count--; + } + + // 判断节点 p 和节点 q 是否连通 + public boolean connected(int p, int q) { + int rootP = find(p); + int rootQ = find(q); + return rootP == rootQ; + } + + public int find(int x) { + if (parent[x] != x) { + parent[x] = find(parent[x]); + } + return parent[x]; + } + + // 返回图中的连通分量个数 + public int count() { + return count; + } +} +/* +1、自反性:节点 p 和 p 是连通的。 + +2、对称性:如果节点 p 和 q 连通,那么 q 和 p 也连通。 + +3、传递性:如果节点 p 和 q 连通,q 和 r 连通,那么 p 和 r 也连通。 +*/ +``` + + + +### 代码示例 + +## 前缀和&Hash + +重要程度 + +4星 + +### 算法模板 + +```java +prefixSum[0] = 0 +for (int i = 0; i < arr.length(); i++) { + prefixSum[i+1] = prefixSum[i] + arr[i]; +} +``` + +### 代码示例 + + + +## 贪心 + +### 重要程度 + +4星 + +### 算法模板 + +### 代码示例 + + + +## 单调栈 + +### 重要程度 + +3星 + +### 算法模板 + +```java +// 抽象思考:把数组的元素想象成并列站立的人,元素大小想象成人的身高。这些人面对你站成一列,如何求元素「2」的下一个更大元素呢?很简单,如果能够看到元素「2」,那么他后面可见的第一个人就是「2」的下一个更大元素,因为比「2」小的元素身高不够,都被「2」挡住了,第一个露出来的就是答案。 +int[] nextGreaterElement(int[] nums) { + int n = nums.length; + // 存放答案的数组 + int[] res = new int[n]; + Stack s = new Stack<>(); + // 倒着往栈里放 + for (int i = n - 1; i >= 0; i--) { + // 判定个子高矮 + while (!s.isEmpty() && s.peek() <= nums[i]) { + // 矮个起开,反正也被挡着了。。。 + s.pop(); + } + // nums[i] 身后的更大元素 + res[i] = s.isEmpty() ? -1 : s.peek(); + s.push(nums[i]); + } + return res; +} +``` + +行星碰撞的问题解决——采用栈的思想倒是比较难想到。 + +### 代码示例 + + + +## 差分及其变体(https://labuladong.github.io/algo/di-er-zhan-a01c6/tan-xin-le-9bedf/sao-miao-x-2e810/) + +### 重要程度 + +### 算法模板 + +```java +// 扫描过程中的计数器 +int count = 0; +// 双指针技巧 +int res = 0, i = 0, j = 0; +while (i < n && j < n) { + if (begin[i] < end[j]) { + // 扫描到一个红点 + count++; + i++; + } else { + // 扫描到一个绿点 + count--; + j++; + } + // 记录扫描过程中的最大值 + res = Math.max(res, count); +} +``` + + + +### 代码示例 + + + +## Leetcode Patterns 做题思想 + +### 算法模板 + +1. If input array is sorted then 遇到有序数组用二分或双指针 + +Binary search +Two pointers + +1. If input array is not sorted then 遇到无序数组用双指针或前缀和 + +Two pointers +Prefix sumConvert to sorted Array + +1. If asked for all permutations/subsets then 求排列或子集用回溯 + +Backtracking + +1. If given a tree/graph then 遇到树就用深度优先搜索或广度优先搜索 + +DFS +BFS + +1. If given a linked list then 遇到链表用双指针 + +Two pointers + +1. If recursion is banned then 无法递归就用栈 + +Stack + +1. If must solve in-place then 要原地返回结果就交换数据或用一个指针存储多个数据 + +Swap corresponding values +Store one or more different values in the same pointer - Bits + +1. If asked for maximum/minimum subarray/subset/options then 求最大或最小子数组、子集就用动态规划 + +Dynamic programming + +1. If asked for top/least K items then 求 top/least K 就用堆 + +Heap + +1. If asked for common strings then + +Map +Trie +Else + +Map/Set for O(1) time & O(n) space +Sort input for O(nlogn) time and O(1) space + +# 常用技巧 + +## 二维数组与一维数组相互交换 + +A【M行,N列】,A\[i][j] + +M*N个数的一维数组,B【i * N + j】 + +## 一维数组的排序和相等 + +```java +Arrays.sort()、Arrays.equal() +``` + +## 字符串 + +统计每个字符出现的次数 + +```java +timesMap.put(str.charAt(i), timesMap.getOrDefault(str.charAt(i), 0) + 1) +``` + +## java中,判断某个字符是字母,是否是大写或小写,是否是数字,是否是十进制、八进制、二进制 + +```java +// 在Java中,可以使用Character类中的一些静态方法来判断一个字符的类型。 + +// 1. 判断是否是字母 + +// 可以使用Character类中的isLetter(char c)方法来判断一个字符是否是字母。例如: + + +char c = 'A'; +if (Character.isLetter(c)) { + System.out.println(c + "是字母"); +} else { + System.out.println(c + "不是字母"); +} + +// 2. 判断是否是大写字母 + +// 可以使用Character类中的isUpperCase(char c)方法来判断一个字符是否是大写字母。例如: + +char c = 'A'; +if (Character.isUpperCase(c)) { + System.out.println(c + "是大写字母"); +} else { + System.out.println(c + "不是大写字母"); +} + +// 3. 判断是否是小写字母 + +// 可以使用Character类中的isLowerCase(char c)方法来判断一个字符是否是小写字母。例如: + +char c = 'a'; +if (Character.isLowerCase(c)) { + System.out.println(c + "是小写字母"); +} else { + System.out.println(c + "不是小写字母"); +} + +// 4. 判断是否是数字 + +// 可以使用Character类中的isDigit(char c)方法来判断一个字符是否是数字。例如: + +char c = '1'; +if (Character.isDigit(c)) { + System.out.println(c + "是数字"); +} else { + System.out.println(c + "不是数字"); +} + +//5. 判断是否是十进制数字 + +// 可以使用Character类中的isDigit(char c)方法来判断一个字符是否是十进制数字。例如: +char c = '1'; +if (Character.isDigit(c)) { + System.out.println(c + "是十进制数字"); +} else { + System.out.println(c + "不是十进制数字"); +} + + +//6. 判断是否是八进制数字 + +//可以使用Character类中的isOctalDigit(char c)方法来判断一个字符是否是八进制数字--至少JDK8中实际没有看到此方法。例如: + +char c = '7'; +if (Character.isOctalDigit(c)) { + System.out.println(c + "是八进制数字"); +} else { + System.out.println(c + "不是八进制数字"); +} + +// 7. 判断是否是二进制数字 + +//可以使用Character类中的isDigit(char c)方法来判断一个字符是否是二进制数字。例如: + +char c = '1'; +if (c == '0' || c == '1') { + System.out.println(c + "是二进制数字"); +} else { + System.out.println(c + "不是二进制数字"); +} + +``` + +## 无重复的最长子串 + +滑动窗口/双指针,[i,j)。如果找到相同的字符,则i右移,否则j右移。当找到相同字符时,取j-i的最大值 + +## 最长回文字串 + +扩展中心法:分别做当前i的奇对称和偶对称的最大值,然后得到起、终位置, +start = i - (len - 1)/2 end = i + len /2 + +## 树和图 + +中序遍历二叉树;**递归思想:只要树不为空,按照左中右的顺序递归遍历**。 +迭代思想:**使用stack模拟递归过程而已。左子树不为空时则入栈,否则出栈,取right为当前栈,直到node和stack都为空结束。** + + + +## 推荐回溯的套路--有时要加一些减枝技巧 + +需要一个结束条件,需要递归查询。为提高性能需要设置“剪枝”条件。找到和删除tmpList中的最后一个元素(保证递归完tmpList被清空) + + + +# 典型题目的多种解法 + +## 二进制位和位移算法(集合中的每个元素都可选或不选) + +```java +class Solution { + public List> subSets(int[] nums) { + List> res = new ArrayList<>(); + + // 循环遍历所有可能的子集,i表示当前子集的二进制表示(0——无元素,1---表示第num.length -1的元素出现,2---表示num.length + // -2的元素出现),从0到2的nums.length次方-1循环——掩码取数 + for (int i = 0; i < (1 << nums.length); i++) { + // 每个子集,创建一个空的整数列表sub,用于存储当前子集中包含的元素 + List sub = new ArrayList<>(); + // 遍历数组中元素,当当前元素应该在子集中时,则将其添加到sub中 + for (int j = 0; j < nums.length; j++) { + // 如果当前i(即子集的二进制表示的值)右移j位表示是第j个位置位于j是1,表示第j个应该出现 + if (((i >> j) & 1) == 1) { + sub.add(nums[j]); + } + } + res.add(sub); + } + return res; + } +} +``` + +## 求输入数组nums[]的各幂子集 + +### 思路 + +从空集开始,每次遍历整数数组中的一个元素,将该元素添加到当前结果集中的每个子集中,形成新的子集,并将新的子集添加到结果集中。最终得到的结果集包含了原数组的所有子集 + +### 代码示例 + +```java +class Solution { + public List> subSets(int[] nums) { + // 先记录结果集 + List> res = new ArrayList<>(); + + // 首先有空集--相当于种子,而集合中所有元素都要往里面加 + res.add(new ArrayList<>()); + + // 集合中所有元素都要往里面加 + for (int num : nums) { + // 当前结果集的大小 + int resultSize = res.size(); + + // 遍历当前结果集的每个子集 + for (int i = 0; i < resultSize; i++) { + // 对于第i个子集 + List newSub = new ArrayList<>(res.get(i)); + // 将新元素num加入到该子集形成新 + newSub.add(num); + // 将新子集放到结果集中 + res.add(newSub); + } + } + return res; + } +} +``` + +## 遍历回溯树 + +遍历回溯树的方法——回溯方法。使用一个链表track记录当前子集,使用一个列表res记录所有子集。在回溯过程中,每次将当前子集加入结果列表,然后递归生成下一个子集,最后回溯,将当前元素从当前子集中移除。 + +### 代码示例 + +```java +class Solution { + // 定义一个公共方法,接收一个整数数组,返回一个包含所有子集的列表 + public List> subSets(int[] nums) { + // 定义一个结果列表 + List> res = new ArrayList<>(); + // 定义一个链表,用于记录当前子集 + LinkedList track = new LinkedList<>(); + // 将空集加入结果列表 + res.add(new LinkedList()); + // 回溯生成所有子集 + backTrack(nums, 0, track, res); + // 返回结果列表 + return res; + } + + // 定义一个回溯方法,用于生成所有子集 + void backTrack(int[] nums, int start, LinkedList track, List> res) { + // 如果当前子集的长度等于原数组的长度,说明已经生成了一个子集,直接返回 + if (track.size() == nums.length) { + return; + } + // 遍历原数组,从start位置开始 + for (int i = start; i < nums.length; i++) { + // 将当前元素加入当前子集 + track.addLast(nums[i]); + // 将当前子集加入结果列表 + res.add(new LinkedList(track)); + // 递归生成下一个子集 + backTrack(nums, i + 1, track, res); + // 回溯,将当前元素从当前子集中移除 + track.removeLast(); + } + } +} +``` + +## 二叉树的最近公共祖先 + +```java +// 百度百科中最近公共祖先的定义为:“对于有根树 T 的两个节点 p、q,最近公共祖先表示为一个节点 x,满足 x 是 p、q 的祖先且 x 的深度尽可能大(一个节点也可以是它自己的祖先)。” +public TreeNode nearestCommonAncestor(TreeNode root, TreeNode p, TreeNode q){ + // 因为是从根出发 + return findCommonAncesstor(root, p, q); +} + + +// DFS方式 +private TreeNode findCommonAncesstor(root, p, q){ + if (root == null) { + return null; + } + if (root == p || root == q) { + // 如果在往下深度走的过程中,刚好是到达某一点,说明这一点就是它们的公共祖先 + return root; + } + + // 找左边--说实话,从这里开始往后,还真是忘了咋写了 + TreeNode left = findCommonAncesstor(root.left, p, q); + // 找右边 + TreeNode right = findCommonAncesstor(root.right, p, q); + + // 这里前提是因为p和q一定在树中,所以两边不为空,则说明此时的root即为它们的共同祖先,又因为这是递归返回,所以返回的是最靠近两个的祖先 + if (left != null && right != null) { + return root; + } + return left != null ? left : right; +} +``` + +## DFS遍历二维数组 + +```java +// 因为dfs函数遍历到值为0的位置会直接返回,所以只要把经过的位置都设置为0,就可以起到不走回头路的作用。--这就是FloodFill算法 +class Solution { + public int numOfIslands(char[][] grid) { + int res = 0; + int m = grid.length; + int n = grid[0].length; + + // 遍历每个节点 + for (int i = 0; i < m; i++) { + for (int j = 0; j < n; j++) { + // 如果是岛屿,则将与该岛屿相连的岛屿都淹没,直至海水,从而标志找到了一块岛屿 + if (grid[i][j] == '1') { + res++; + dfs(grid, i, j); + } + } + } + return res; + } + + private void dfs(char[][] grid, int i, int j) { + int m = grid.length; + int n = grid[0].length; + // 边界条件1-- 到达边界了 + if (i < 0 || j < 0 || i >= m || j >= n) { + return; + } + // 边界条件2--已经是海水了 + if (grid[i][j] == '0') { + return; + } + // 不是海水的,则淹没,将(i,j)变成海水 + grid[i][j] = '0'; + // 继续深度 + // --往矩阵该点的下邻节点 + dfs(grid, i + 1, j); + // --往矩阵该点的上邻节点 + dfs(grid, i - 1, j); + // --往矩阵该点的左邻节点 + dfs(grid, i, j - 1); + // --往矩阵该点的右邻节点 + dfs(grid, i, j + 1); + } +``` + +## 个人不熟悉点 + +### java中最大堆、最小堆(可以通过优先队列(PriorityQueue)来实现) + +````java +以下为AI生成内容,仅供参考: + +最小堆和最大堆是常用的数据结构,Java中可以通过优先队列(PriorityQueue)来实现。 + +最小堆的实现: + +```java +import java.util.PriorityQueue; + +public class MinHeap { + public static void main(String[] args) { + PriorityQueue minHeap = new PriorityQueue<>(); + minHeap.add(3); + minHeap.add(1); + minHeap.add(4); + minHeap.add(2); + minHeap.add(5); + + while (!minHeap.isEmpty()) { + System.out.print(minHeap.poll() + " "); + } + } +} +``` + +// 输出结果为:1 2 3 4 5 + +最大堆的实现: + +```java +import java.util.Collections; +import java.util.PriorityQueue; + +public class MaxHeap { + public static void main(String[] args) { + PriorityQueue maxHeap = new PriorityQueue<>(Collections.reverseOrder()); + maxHeap.add(3); + maxHeap.add(1); + maxHeap.add(4); + maxHeap.add(2); + maxHeap.add(5); + + while (!maxHeap.isEmpty()) { + System.out.print(maxHeap.poll() + " "); + } + } +} +``` + +// 输出结果为:5 4 3 2 1 + +// 需要注意的是,最大堆需要在创建PriorityQueue对象时传入Collections.reverseOrder()参数,表示使用逆序比较器来实现最大堆。 +```` + +### 树类、给定数组-1表示父节点,数组a[i]表示节点i的父节点类题目 + +1、直接方法,根据**路径a[i]不断往上遍历while(a[i] != - 1)** {i = a[i];***,pathList.add(i)可记录到根节点的路径}; +2、构造树——个人这块在构造树时容易出错,**要做是否有父节点、子节点的判断**,**易出错**。 + +### java集合类框架 + +(• 最重要:HashMap和ArrayList) +注:通常总的计算量超过100万就不适合打点标记法,关于数据规模的估算可以参考【案例开放023】上机编程题之性能学习交流http://3ms.huawei.com/km/groups/3803117/blogs/details/9892189?l=zh-cn + +### 采用ForEach的写法 + +前提是需要在forEach写法下拿值时不为空,因为ForEach情况下实际是有个先拿列表的动作,只有列表不为空,才能拿到列表每个项 +```java +// 这个关键判空没有做导致异常退出 + if (nodeParentsMap.get(keyNodeId) != null) { + for (Integer parentNodeId : nodeParentsMap.get(keyNodeId)) { + resultLst.add(parentNodeId); + dfsForParentSelfCheck(parentNodeId, resultLst, keyNodeSet, nodeParentsMap); + } + } + + +``` + +### 区间相关问题(区间覆盖、区间交集、区间调度) + +(1)**排序——意义是啥**?**不排序时直接两层遍历O(n^2),排序之后有覆盖、交集、不相交三种情况**:常见的排序方法就是按照区间起点排序,**或者先按照起点升序排序,若起点相同,则按照终点降序排序。**当然,如果你非要按照终点排序,无非对称操作,本质都是一样的。——排序的意义:后续处理不用再考虑线段谁前谁后;时间复杂度变为排序+一层遍历 +(2)**画图。勤动手,**两个区间的相对位置有几种?不同的相对位置怎么处理? +例如:集装箱调度问题?区间覆盖、区间合并、区间交集、会议室预订 + +```java +class Solution { + // 删除覆盖的区间,输入: 二维数组列表,代表区间的【起点,终点】,输出:剩下的区间数 + public int removeCoveredIntervals(int[][] intvs) { + // 先按照起点升序,再按照终点降序--这样可以拿到最长的合并区间 + Arrays.sort(intvs, (a, b) -> { + if (a[0] == b[0]) { + return b[1] - a[1]; + } + return a[0] - b[0]; + }); + + // 排好序后,记录合并区间的起点与终点,初始时为第1个, + int left = intvs[0][0]; + int right = intvs[0][1]; + + int coverCount = 0; + + // 一次遍历数组(去掉第1个记录) + for (int i = 1; i < intvs.length; i++) { + int[] intv = intvs[i]; + // 找到覆盖区间 + if (left <= intv[0] && right >= intv[1]) { + coverCount++; + } + // 找到交集区间--则合并交集,因为排好序了,这里就不用再操心考虑两个区间谁前谁后了,因为这里是按顺序遍历的 + if (right >= intv[0] && right <= intv[1]) { + right = intv[1]; + } + + // 找到完全不相交的区间——更新更新覆盖区域的起点、终点,因为已排序,所以不用担心对前面的影响 + if (right < intv[0]) { + left = intv[0]; + right = intv[1]; + } + } + return intvs.length - coverCount; + } +} +``` + +### 递归-->DFS/回溯->分治思想 + +1、递归终止条件; +2、当前调度层应该返回什么?做什么操作? +例如解决求表达式的多种计算结果\数组排列\字符串合并求无重复字符的合并字符串最大长度--输入是什么、输出是什么 + +### 贪心思想 + +例如**解决加油站问题**、解决产品质检点灯问题--输入是什么、输出是什么 + +### 数组转Set + +```java +Set keyNodeSet = new HashSet(Arrays.asList(keyNodes)); +// 不要是如下 +Set keyNodeSet = new HashSet<>(Arrays.asList(keyNodes)); +// 上述得到的keyNodeSet的size是1,没法用keyNodeSet的contains()做相应判断 +``` + +### 数组转list + +Arrays.asList( )--**-适配器模式**:模板方法,public static List asList(T ...a);Integer[] arr = {1,2,3};可以List lst = Arrays.asList(arr);也可以List lst = asList(1, 2, 3); +注意该方法的返回值是java.util.Arrays类中一个私有静态内部类java.util.Arrays.ArrayList, +它并非java.util.ArrayList类。 +java.util.Arrays.ArrayList类具有set(),get(),contains()等方法, +但是不支持添加add()或删除remove()方法,调用这些方法会报错。 + +也就是说,此种方法残缺:重新得到的 list 不能 add( ) 或者 remove( ); + +此时选用java.util.ArrayList类构造器构造List,如: + +先List list = Arrays.asList; +再List newList = new ArrayList<>(list); + +### List转数组 + +list.toArray(T[] a); + +输出指定类型的数组,输出的数组类型与括号中参数类型一致; + +必须是包装类(String、Integer、Character等),不能是基本数据类型了(string、int、char); +要转为基本类型,可以使用流来操作,list.stream().mapToInt(Integer::intValue).toArray(); + +### List转Set + +```java +Set set = new HashSet(new ArrayList()); +``` + +### 数组输出问题 + +不需要手写代码输出数组—Arrays.toString(数组),这样便于节约时间,差异看数组输出结果! + +### 数组初始化值填充 + +```java +int[] result = new int[input.length]; +Arrays.fill(result, -1); +``` + +### 集合Collection里List、Set、Map删除元素的方法总结 + +对于Map,可以使用remove(key)方法来删除指定的键值对。如果该键存在,则会将其对应的值返回并从Map中删除该键值对;如果该键不存在,则返回null。需要注意的是,如果在遍历Map时删除元素,可能会导致ConcurrentModificationException异常。 + +对于List,可以使用remove(index)方法来删除指定位置的元素。如果该位置存在元素,则会将其返回并从List中删除;如果该位置不存在元素,则会抛出IndexOutOfBoundsException异常。需要注意的是,如果在遍历List时删除元素,可能会导致ConcurrentModificationException异常。 + +在删除元素时,需要考虑到可能会影响到其他元素的位置和索引,因此需要谨慎操作。同时,如果在多线程环境下操作,需要考虑到线程安全问题。 +(1)List删除时有两个重载方法:E remove(int index); //返回删除的元素 + boolean remove(Object o); //返回bool值 +而重载的优先级顺序是——先匹配参数个数->参数类型的最佳匹配--直接所属类->向上转型直至没有父类(从包装类到父类到接口)->可变参数列表 +在Java中,Map和List都可以单独删除某个元素。 + + 对于list的循环删除,推荐采用迭代器方法,同时注意是使用迭代器的remove方法。 + Iterator it = list.iterator(); +```java +while (it.hasNext()) { + Integer x = it.next(); + if (x >= baseNum) { + it.remove(); + //list.remove(x); //java.util.ConcurrentModificationException + } +} +``` + +另外,LinkedList有removeLast()可以来删除链表中的最后一个元素的。该方法会将链表的最后一个节点从链表中移除,并返回该节点的值。如果链表为空,则该方法会抛出NoSuchElementException异常。 +(2)Map的删除: +map(循环删除)也有类似情况,因此此处举一例,map可以安全删除的例子 + +代码同上,只需要调用iterator.remove();即可,不要用map.remove(key); +https://cloud.tencent.com/developer/article/1497457 + +### java中由一个字符串构造成一个含该字符串的列表有哪些方法,请举例说明 + +````java +小鲁班(p_xiaoluban) 2023-10-09 20:10 +以下为AI生成内容,仅供参考,不允许未经评估直接将生成内容植入工作输出件中。 + +在Java中,可以使用以下方法将一个字符串构造成一个包含该字符串的列表: + +1. 使用Arrays.asList()方法: +```java +String str = "Hello"; +List list = Arrays.asList(str); +``` + +2. 使用Collections.singletonList()方法: +```java +String str = "Hello"; +List list = Collections.singletonList(str); +``` + +3. 使用ArrayList的构造函数: +```java +String str = "Hello"; +List list = new ArrayList<>(Arrays.asList(str)); +``` + +4. 使用Stream的collect()方法: +```java +String str = "Hello"; +List list = Stream.of(str).collect(Collectors.toList()); +``` + +这些方法都可以将一个字符串构造成一个只包含该字符串的列表。 +```` + +### Map的putIfAbsent、computeIfAbsent、computeIfPresent、compute + +put方法——如果是添加成功,返回null, 如果已经存在此key了 会用新值覆盖旧值 然后把oldValue返回出来 +putIfAbsent——如果给定的key不存在(或者key对应的value为null),就把value放进去,并返回null。如果存在,则返回当前值(不会把value放进去); +computeIfAbsent——如果给定的key不存在(或者key对应的value为null),就去计算mappingFunction的值(计算结果决定了返回值有两种情况如下,我们姑且叫计算的结果为newValue):若key已经存在,就不会执行mappingFunction。返回oldValue。 +newValue == null,不会替换旧值。返回null +newValue != null,替换旧值。返回新值 + +### 模拟类的题目 + +1、常见方法--先观察数据,估算复杂度(心法),以示例代入尝试寻找解决方法(相当于由具体来总结抽象出算法思路) + Hash是常用的提升效率的手段