统一建模语言(Unified Modeling Language,缩写UML)是非专利的第三代建模和规约语言。 UML是一种开放的方法,用于说明、可视化、构建和编写一个正在开发的、面向对象的、软件密集系统的制品的开放方法。 分类: ● UML模型 ○ 功能模型:从用户的角度展示系统的功能,包括用例图。 ○ 对象模型:采用对象,属性,操作,关联等概念展示系统的结构和基础,包括类别图、对象图。 ○ 动态模型:展现系统的内部行为。包括序列图,活动图,状态图。 ● UML图 ○ 结构性图形(Structure diagrams)强调的是系统式的建模: ■ 静态图(static diagram):包括类图、对象图、包图 ■ 实现图(implementation diagram):包括组件图、部署图 ■ 剖面图 ■ 复合结构图 ○ 行为式图形(Behavior diagrams)强调系统模型中触发的事件 ■ 活动图 ■ 状态图 ■ 用例图 ○ 交互性图形(Interaction diagrams),属于行为图形的子集合,强调系统模型中的资料流程 ■ 通信图 ■ 交互概述图 ■ 时序图 ■ 时间图 下面详细讲解类图
作用:解析项目的系统结构和架构层次,可以简洁明了的帮助我们理解项目中类之间的关系。 类图的格式: ● 类名:粗体(类是抽象类则类名显示为斜体) ● 属性: ○ 格式:可见性 名称:类型[=默认值] ○ 可见性一般为public、private和protected,在类图分别用+、-和#表示 ● 方法: ○ 格式:可见性 名称(参数列表 参数1,参数2) :返回类型 ■ 可见性如上名称表达式的介绍 ■ 名称就是方法名 ■ 参数列表是可选的项,多参数的话参数直接用英文逗号隔开 ■ 返回值也是个可选项,返回值类型可以说基本的数据类型、用户自定义类型和void。(如果是构造方法,则无返回类型)
类与类之间的关系: 泛化(继承)、实现、依赖、关联、聚合、组合
聚合:部分可以脱离整理而存在 组合:部分若脱离了整体,则不复存在 关联:长期性的 依赖:临时性的
]]>思路:使用KMP算法,一定要构造next数组(即前缀表)
KMP算法 1.构造next数组(和模式串大小一致) 2.模式串用next数组匹配文本串
那怎么构造next数组呢,思路:
1.初始化 2.处理前后缀不相同的情况 3.处理前后缀相同的情况
注:以下统称haystack为文本串, needle为模式串
时间复杂度:其中n为文本串长度,m为模式串长度,因为在匹配的过程中,根据前缀表不断调整匹配的位置,可以看出匹配的过程是O(n),之前还要单独生成next数组,时间复杂度是O(m)。时间复杂度是O(n+m)。
法一:前缀表不减一
class Solution {
public int strStr(String haystack, String needle) {
//前后缀不减一的实现方式
//边界判断
if (needle.length() == 0) return 0;
int[] next = new int[needle.length()];//创建一个和模式串一样大的next数组
getNext(next, needle);//得到填充好的next数组
//使用next数组来做匹配 在文本串里找是否出现过模式串
//定义两个下标,j指向模式串起始位置,i指向文本串起始位置
int j = 0;//和next数组中j的起始位置对应
for (int i = 0; i < haystack.length(); i++) {
//不匹配的情况
while (j > 0 && needle.charAt(j) != haystack.charAt(i)) {//模式串的j>0,=0的话,下面的next就是next[-1]就出界了
//找到回退的位置,即改变前缀末尾的位置j
j = next[j - 1];
}
if (needle.charAt(j) == haystack.charAt(i)) {
j++;//前缀末尾后移
}
//判断在文本串s里出现了模式串t呢,如果j指向了模式串t的末尾,那么就说明模式串t完全匹配文本串s里的某个子串了
if (j == needle.length()) {
return i - needle.length() + 1;//因为返回的是下标,所以要+1;比如:长度是3,其最大下标就是2,数组下标从0开始的
}
}
//跳出循环了,说明找不到匹配的模式串的了
return -1;
}
//构造next数组
/*
思路:1.初始化
2.处理前后缀不相同的情况
3.处理前后缀相同的情况
*/
private void getNext(int[] next, String s) {
//定义两个指针i和j,j指向前缀末尾位置,i指向后缀末尾位置
//next[i] 表示 i(包括i)之前最长相等的前后缀长度(其实就是j)
//next[j]就是记录着j(包括j)之前的子串的相同前后缀的长度
int j = 0;
next[0] = j;
for (int i = 1; i < s.length(); i++) {//从1开始,因为0的位置肯定是0的,只有一个字符的时候,不存在相等的前后缀
//不相等前后缀的情况,回退 这里用while 是因为只要不相等就一致回退到相等或者到索引为1的位置
while (j > 0 && s.charAt(j) != s.charAt(i)) {
//改变前缀的末尾
j = next[j - 1];//不匹配字符的上一个位置
}
// 找到相同的前后缀
if (s.charAt(j) == s.charAt(i)) {
j++;//索引为i的位置的值加1
next[i] = j;//更新当前下标为i的next数组
}
}
}
}
]]>单例模式就是一个类只能有一个对象 使用场景:
- 你希望这个类只有一个且只能有一个实例;
- 项目中的一些全局管理类(Manager)可以用单例来实现。
下面要介绍的几种单例模式的实现方式:
饿汉式: 步骤一:
public class SingleObject {
private static SingleObject instance = new SingleObject();
// 构造函数为private,避免被实例化
private SingleObject(){};
// 获取唯一可用的对象
public static SingleObject getInstance() {
return instance;
}
public showMessage(){
System.out.println("hello guy!");
}
}
步骤二:
public class SingleObjectDemo {
public static void main(String[] args) {
// 获取唯一可用的对象
SingleObject object = SingleObject.getInstance();
// 调用方法
object.showMessage();
}
}
步骤三:
hello guy!
以上就是饿汉式的实现方式,优点是没有加锁,执行效率高;支持多线程(利用的是JVM的类加载机制,在类初始化时就已经被加载,保证同一时刻只有一个线程获取到实例) ps:这种方式是最常用的,但是容易产生垃圾对象
懒汉式-线程安全
public class SingleObject {
private static SingleObject instance;
private SingleObject(){};
public static synchronized SingleObject getInstance() {
if (instance = null) {
instance = new SingleObject();
}
return instance;
}
}
ps:优点是支持多线程,lazy加载(一开始先不实例化可以避免内存的浪费);必须使用但是使用synchronized才能保证单例,但是加了加锁操作,效率比较低,99%情况下是不需要同步的,不推荐使用。
懒汉式—线程不安全
public class SingleObject {
private static SingleObject instance;
private SingleObject(){};
public static SingleObject getInstance() {
if (instance = null) {
instance = new SingleObject();
}
return instance;
}
}
ps:这种方式是最基本的实现方式,这种实现最大的问题就是不支持多线程。因为没有加锁 synchronized,所以严格意义上它并不算单例模式。
双检锁(DCL,即 double-checked locking)
public class Singleton {
private volatile static Singleton singleton;
private Singleton (){}
public static Singleton getSingleton() {
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
}
ps:多了volatile,同时,又用了synchronized,这种方式采用双锁机制,安全且在多线程情况下能保持高性能。 使用场景:getInstance() 的性能对应用程序很关键。
登记式
public class Singleton {
private static class SingletonHolder{
private static final Singleton INSTANCE = new Singleton();
}
private Singleton(){};
public static final Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
}
PS:使用了静态内部类,这种方式能达到双检锁方式一样的功效,但实现更简单。对静态域使用延迟初始化,应使用这种方式而不是双检锁方式。这种方式只适用于静态域的情况,双检锁方式可在实例域需要延迟初始化时使用。
解释:这种方式同样利用了 classloader 机制来保证初始化 instance 时只有一个线程,它跟饿汉式方式不同的是:饿汉式只要 Singleton 类被装载了,那么 instance 就会被实例化(没有达到 lazy loading 效果);而这种方式是 Singleton 类被装载了,instance 不一定被初始化。因为 SingletonHolder 类没有被主动使用,只有通过显式调用 getInstance 方法时,才会显式装载 SingletonHolder 类,从而实例化 instance。
使用场景:如果实例化 instance 很消耗资源,所以想让它延迟加载,另外一方面,又不希望在 Singleton 类加载时就实例化,因为不能确保 Singleton 类还可能在其他的地方被主动使用从而被加载,那么这个时候实例化 instance 显然是不合适的。这个时候,这种方式相比饿汉式就显得比较合理
枚举
public enum Singleton {
INSTANCE;
public void whateverMethod() {
}
}
总结::一般情况下,不建议使用懒汉方式,建议使用饿汉方式(线程安全,但没有懒加载)。只有在要明确实现 lazy loading 效果时,才会使用登记方式。如果涉及到反序列化创建对象时,可以尝试使用枚举方式。如果有其他特殊的需求,可以考虑使用双检锁方式。
]]>public interface Strategy {
public int doOperation(int num1, int num2);
}
● 创建实现接口的实体类:分别是加、减、乘的类,里面封装了这三种算法
public class OperationAdd implements Strategy{
@Override
public int doOperation(int num1, int num2) {
return num1 + num2;
}
}
public class OperationSubtract implements Strategy{
@Override
public int doOperation(int num1, int num2) {
return num1 - num2;
}
}
public class OperationMultiply implements Strategy{
@Override
public int doOperation(int num1, int num2) {
return num1 * num2;
}
}
● 创建 Context 类 (Strategy类是Context的一部分——聚合关系)
public class Context {
private Strategy strategy;// Strategy类是Context的一部分
// 有参构造
public Context(Strategy strategy){
this.strategy = strategy;
}
public int executeStrategy(int num1, int num2){
return strategy.doOperation(num1, num2);
}
}
● 使用Context来改变策略,达到使用不同代码逻辑的作用
public class StrategyPatternDemo {
public static void main(String[] args) {
Context context = new Context(new OperationAdd());
System.out.println("10 + 5 = " + context.executeStrategy(10, 5));
context = new Context(new OperationSubtract());
System.out.println("10 - 5 = " + context.executeStrategy(10, 5));
context = new Context(new OperationMultiply());
System.out.println("10 * 5 = " + context.executeStrategy(10, 5));
}
}
结果:
10 + 5 = 15
10 - 5 = 5
10 * 5 = 50
]]>索引就是用来帮助表快速检索目标数据的 那么,索引有哪几种创建方式: ● CREATE
CREATE INDEX indexName ON tableName (columnName(length) [ASC|DESC]); indexName:当前创建的索引,创建成功后索引的名字; tableName:要在哪张表上创建一个索引,这里指定表名; columnName:要为表中的哪个字段创建索引,这里指定字段名; length:如果字段存储的值过长,选用值的前多少个字符创建索引; ASC|DESC:指定索引的排序方式,ASC是升序,DESC是降序,默认ASC
● ALTER
ALTER TABLE tableName ADD INDEX indexName(columnName(length) [ASC|DESC]);
● DDL (建表时创建索引)适合已经确定了索引项的情况下建立
CREATE TABLE tableName(
columnName1 INT(8) NOT NULL,
columnName2 ....,
.....,
INDEX [indexName] (columnName(length))
);
不同的索引有不同的创建方式:(后面会具体介绍各类索引)
● 唯一索引
-- 方式①
CREATE UNIQUE INDEX indexName ON tableName (columnName(length));
-- 方式②
ALTER TABLE tableName ADD UNIQUE INDEX indexName(columnName);
-- 方式③
CREATE TABLE tableName(
columnName1 INT(8) NOT NULL,
columnName2 ....,
.....,
UNIQUE INDEX [indexName] (columnName(length))
);
● 主键索引
-- 方式①
ALTER TABLE tableName ADD PRIMARY KEY indexName(columnName);
-- 方式②
CREATE TABLE tableName(
columnName1 INT(8) NOT NULL,
columnName2 ....,
.....,
PRIMARY KEY [indexName] (columnName(length))
);
● 全文索引——不常用
-- 方式①
ALTER TABLE tableName ADD FULLTEXT INDEX indexName(columnName);
-- 方式②
CREATE FULLTEXT INDEX indexName ON tableName(columnName);
注意点: ○ 5.6版本的MySQL中,存储引擎必须为MyISAM才能创建。 ○ 创建全文索引的字段,其类型必须要为CHAR、VARCHAR、TEXT等文本类型。 ○ 如果想要创建出的全文索引支持中文,需要在最后指定解析器:with parser ngram。 ○ 优化器无法自动选择全文索引,他有自己的语法
● 空间索引(仅有MyISAM支持空间索引)—— 不常用
ALTER TABLE tableName ADD SPATIAL KEY indexName(columnName);
注意:空间索引必须要建立在类型为GEOMETRY、POINT、LINESTRING、POLYGON的字段上 ● 联合索引 ○ 联合索引的意思是可以使用多个字段建立索引。那该如何创建联合索引呢,不需要特殊的关键字,方法如下:
CREATE INDEX indexName ON tableName (column1(length),column2...);
ALTER TABLE tableName ADD INDEX indexName(column1(length),column2...);
注意: ● 使用联合索引时,SELECT语句的查询条件中,必须包含组成联合索引的第一个字段,此时才会触发联合索引,否则是无法使用联合索引的。 ● 创建主键索引时,必须要将索引字段先设为主键,否则会抛1068错误码。 ● 这里也不能使用CREATE(指方式①)语句创建索引,否则会提示1064语法错误。不过:一般主键索引都会在建表的DDL语句中创建,不会在表已经建立后再创建 ● 同时创建索引时,关键字要换成KEY,并非INDEX,否则也会提示语法错误。
● 查看索引
SHOW INDEX FROM tableName
每个字段的含义:
● ①Table:当前索引属于那张表。 ● ②Non_unique:目前索引是否属于唯一索引,0代表是的,1代表不是。 ● ③Key_name:当前索引的名字。 ● ④Seq_in_index:如果当前是联合索引,目前字段在联合索引中排第几个。 ● ⑤Column_name:当前索引是位于哪个字段上建立的。 ● ⑥Collation:字段值以什么方式存储在索引中,A表示有序存储,NULL表无序。 ● ⑦Cardinality:当前索引的散列程度,也就是索引中存储了多少个不同的值。 ● ⑧Sub_part:当前索引使用了字段值的多少个字符建立,NULL表示全部。 ● ⑨Packed:表示索引在存储字段值时,以什么方式压缩,NULL表示未压缩, ● ⑩Null:当前作为索引字段的值中,是否存在NULL值,YES表示存在。 ● ⑪Index_type:当前索引的结构(BTREE, FULLTEXT, HASH, RTREE)。 ● ⑫Comment:创建索引时,是否对索引有备注信息。 后续排除问题、性能调优时,可以通过分析其中的Cardinality字段值,如果该值少于数据的实际行数,那目前索引有可能失效 ● 如果新建错了索引,只能删除再重新创建 DROP INDEX indexName ON tableName; ● 指定索引
SELECT * FROM table_name FORCE INDEX(index_name) WHERE .....;
这个关键字的用法是:当一条查询语句在有多个索引可以检索数据时,显式指定一个索引,减少优化器选择索引的耗时。但是如果对业务系统不熟悉,还是得让优化器自己来选择。
数据库是基于磁盘工作的,所有数据都再磁盘上面存储,索引也是数据的一种,同样存储在磁盘上,但是索引最终会以哪种方式进行存储,这是由索引的数据结构来决定的,同时索引机制又是由存储引擎实现的,不同的存储引擎下的索引文件,保存在本地的格式是不同的。 当数据量越少时,创建索引越好 ,因为创建索引时,会基于原有的数据重新在本地创建索引文件,并同时做好排序并与表数据产生映射的关系。
● 数据结构层次划分 ○ B+Tree类型:MySQL中最常用的索引结构,大部分引擎支持,有序 ○ Hash类型:大部分存储引擎都支持,字段值不重复的情况下查询最快,无序 ○ R-Tree类型:MyISAM引擎支持,也就是空间索引的默认结构类型 ○ T-Tree类型:NDB-Cluster引擎支持,主要用于MySQL-Cluster服务中
B+树和哈希索引是最常见的索引结构,B+Tree是有序的,哈希是无序的。在MySQL中创建索引时,其默认的数据结构就为B+Tree,可以在建表时使用using字段改变索引的数据结构。
CREATE INDEX indexName ON tableName (columnName(length) [ASC|DESC]) USING HASH;
● 字段数量的层次划分 ○ 单列索引 ■ 主键索引 ■ 唯一索引 ■ 普通索引 ○ 多列索引 ■ 联合索引、组合索引、复合索引 、多值索引...很多种叫法,本质即由多个字段组成的索引
使用多列索引的注意事项:当建立多列索引后,一条SELECT语句,只有当查询条件中了包含了多列索引的第一个字段时,才能使用多列索引 ● 使用一个字段值中的前N个字符创建出的索引,就可以被称为前缀索引 (length指定长度) 前缀索引能够在很大程度上,节省索引文件的存储空间,也能很大程度上提升索引的性能 ● 功能逻辑层次划分 ○ 普通索引、唯一索引、主键索引、全文索引、空间索引 ● 存储方式划分 ○ 聚簇索引:也被称为聚集索引、簇类索引 ■ 逻辑上连续且物理空间上的连续 ○ 非聚簇索引:也叫非聚集索引、非簇类索引、二级索引、辅助索引、次级索引 ■ 逻辑上的连续,物理空间上不连续
注意:
]]>1.一张表中只能存在一个聚簇索引,一般都会选用主键作为聚簇索引,其他字段上建立的索引都属于非聚簇索引,或者称之为辅助索引、次级索引。 2.误区:虽然MySQL默认会使用主键上建立的索引作为聚簇索引,但也可以指定其他字段上的索引为聚簇索引,一般聚簇索引要求索引必须是非空唯一索引才行。 3.如果表中就算没有定义主键,InnoDB中会选择一个唯一的非空索引作为聚簇索引,但如果非空唯一索引也不存在,InnoDB隐式定义一个主键来作为聚簇索引(一般适合采用带有自增性的顺序值)。
时间复杂度:nlogn
具体步骤如下:
class Solution {
public int[] sortArray(int[] nums) {
quickSort(nums, 0, nums.length - 1);
return nums;
}
// 快速排序函数
public static int[] quickSort(int[] nums, int left, int right) {
if (left >= right) {
return null;
}
int partition = partition(nums, left, right);
quickSort(nums, left, partition - 1);//在划分位置的左边子区域再划分,直到不可划分left >= right不能进循环条件
quickSort(nums, partition + 1, right); //在划分位置的右边子区域再划分,直到不可划分left >= right不能进循环条件
return nums;
}
public static int partition(int[] nums, int left, int right) {
int pivot = nums[left];
while (left < right) {
while (left < right && nums[right] >= pivot) {
right--;
}
nums[left] = nums[right];
while (left < right && nums[left] <= pivot) {
left++;
}
nums[right] = nums[left];
}
nums[left] = pivot; //当前left,right 指向同一个位置了 nums[right] = pivot也行
return left;// 返回划分的位置
}
}
]]>思路:遍历每一层的时候,记录次数;当该节点的左右节点都为空的时候,直接返回当前的深度即可,如果左右节点不为空就加入队列
class Solution {
public int minDepth(TreeNode root) {
if (root == null) {
return 0;
}
Queue<TreeNode> que = new LinkedList<>();
que.offer(root);
int min = 0;
while (!que.isEmpty()) {
int len = que.size();
min++;
for (int i = 0; i < len; i++) {
TreeNode node = que.poll();
if (node.left == null && node.right == null) {
return min;//该节点的左右节点大都为空直接返回当前的深度
}
//加入该节点的左右节点
if (node.left != null) que.offer(node.left);
if (node.right != null) que.offer(node.right);
}
}
return min;
}
}
]]>该题目和116的区别:就是116题是完全二叉树,这道题不是完全二叉树,题解完全一样的。
略
思路:依旧是层序遍历,计算共有多少层即可
class Solution {
public int maxDepth(TreeNode root) {
if (root == null) {
return 0;
}
Queue<TreeNode> que = new LinkedList<>();
que.offer(root);
int count = 0;//记录深度
while (!que.isEmpty()) {
int len = que.size();
count++;
for (int i = 0; i < len; i++) {
TreeNode node = que.poll();//弹出并记录
//放入节点的左右节点
if (node.left != null) que.offer(node.left);
if (node.right != null) que.offer(node.right);
}
}
return count;
}
}
]]>思路:
下标i小于数组最大下标
的每一层的每一个节点指向下一个节点的值(只是查看值,而不弹出,用peek())/*
// Definition for a Node.
class Node {
public int val;
public Node left;
public Node right;
public Node next;
public Node() {}
public Node(int _val) {
val = _val;
}
public Node(int _val, Node _left, Node _right, Node _next) {
val = _val;
left = _left;
right = _right;
next = _next;
}
};
*/
class Solution {
public Node connect(Node root) {
if (root == null) {
return root;
}
Queue<Node> que = new LinkedList<>();
que.offer(root);
while (!que.isEmpty()) {
int len = que.size();//每一层的数量是会改变的,记录下来
for (int i = 0; i < len; i++) {
Node node = que.poll();
//连接每一层的节点,当该层的节点的数量大于1就可连接
if (i < len - 1) {
node.next = que.peek();//不弹出该节点,只是查看
}
//扩展下一层的节点放入队列中
if (node.left != null) que.offer(node.left);
if (node.right != null) que.offer(node.right);
}
}
return root;
}
}
]]>思路:层序遍历,取每一层的最大值,放到一维数组,最后返回结果集
Collections.emptyList()//这个方法返回的List是Collections类的一个静态内部类,它继承AbstractList后并没有实现add()、remove()等方法,因此这个返回值List并不能增加删除元素
用到:
class Solution {
public List<Integer> largestValues(TreeNode root) {
if (root == null) {
return Collections.emptyList();//直接返回空列表,减少开销
}
Queue<TreeNode> que = new LinkedList<>();//创建队列
List<Integer> res = new ArrayList<>();//结果集
que.offer(root);//根节点入队
while (!que.isEmpty()) {
int len = que.size();//记录当前节点的数量
int max = Integer.MIN_VALUE;//先赋予int类型的最小值
for (int i = 0; i < len; i++) {//找每一层的最大值
TreeNode node = que.poll();//从队列中取出元素
max = Math.max(max, node.val);//找出该层的最大值
//放入该节点的左右节点到队列
if (node.left != null) que.offer(node.left);
if (node.right != null) que.offer(node.right);
}
//将每一层的最大值放到结果集
res.add(max);
}
return res;
}
}
]]>思路:依然是层序遍历,只是节点的孩子有多个了,children也是List
类型的,将节点的所有孩子遍历后全部放入队列即可:
遍历方式
class Solution {
public List<List<Integer>> levelOrder(Node root) {
List<List<Integer>> result = new ArrayList<>();
Queue<Node> que = new LinkedList<>();
if (root != null) {
que.offer(root);
}
while (!que.isEmpty()) {
List<Integer> list = new ArrayList<>();
int len = que.size();
for (int i = 0; i < len; i++) {
Node node = que.poll();
list.add(node.val);//取出节点的值加入到结果集
//添加节点的孩子到队列,是Node类型的List
List<Node> children = node.children;
//遍历孩子,放到队列中
// for (int j = 0; j < children.size(); j++) {
// if (children.get(j) != null ) {
// que.offer(children.get(j));//将节点加入到队列
// }
// }
for (Node child : children) {
if (child != null) {
que.offer(child);
}
}
}
result.add(list);
}
return result;
}
}
N叉树(节点)的定义:
// Definition for a Node.
class Node {
public int val;
public List<Node> children;//孩子也是节点类型的
public Node() {}//构造器
public Node(int _val) {//有参构造器
val = _val;
}
public Node(int _val, List<Node> _children) {//有参构造器
val = _val;
children = _children;
}
};
]]>思路:
class Solution {
public List<Double> averageOfLevels(TreeNode root) {
Queue<TreeNode> que = new LinkedList<>();
//判断空
if (root != null) {
que.offer(root);
}
List<Double> list = new ArrayList<>();
while (!que.isEmpty()) {
int len = que.size();
double sum = 0.0;
for (int i = 0; i < len; i++) {//这里不能用while判断 会改变len的值
TreeNode node = que.poll();//弹出并临时记录
sum += node.val;//求和
//将该层的平均值存放到一维数组中
if (node.left != null) {
que.add(node.left);
}
if (node.right != null) {
que.add(node.right);
}
}
list.add(sum / len);
}
return list;
}
}
注意:这里不能用while
不能用 while(len > 0) {
...
len--;
}
因为len改变了,就无法计算sum/len了,这点要注意
]]>class Solution {
public List<List<Integer>> levelOrderBottom(TreeNode root) {
/*思路:
1.广搜得到二维数组的结果
2.将二维数组反转
*/
//创建一个结果二维数组
List<List<Integer>> res = new ArrayList<>();
Queue<TreeNode> que = new LinkedList<>();
if (root == null) {
return res;
}
que.offer(root);//根节点放入队列
while (!que.isEmpty()) {
//创建一维数组保存当前层的节点
List<Integer> list = new ArrayList<>();
//保存当前层的节点的数量
int len = que.size();
for (int i = 0; i < len; i++) {
//保存当前出队的节点
TreeNode node = que.poll();
//将其值存入到一维数组
list.add(node.val);
//将该节点的左右节点入队
if (node.left != null) que.offer(node.left);//空节点不入队
if (node.right != null) que.offer(node.right);
}
//将该层的节点的值存放到一维数组中
res.add(list);
}
//开辟一个二维数组的空间,存放反转后的二维数组的结果
List<List<Integer>> result = new ArrayList<>();
for (int i = res.size() - 1; i >= 0; i--) {//数组下标从0开始,最大长度是数组大小减1
result.add(res.get(i));//get获取对应位置的元素
}
//返回结果
return result;
}
}
]]>思路:
定义数组和队列:将每一层的结果保存在一个一维数组中,将每一层的一维数组保存在二维数组中 判断队列是否为空
遍历当前层,用size记录当前层的大小,控制当前遍历的次数
加入当前节点的下一层的左右孩子
此时又重新获取了下一层的节点的数量
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode() {}
* TreeNode(int val) { this.val = val; }
* TreeNode(int val, TreeNode left, TreeNode right) {
* this.val = val;
* this.left = left;
* this.right = right;
* }
* }
*/
class Solution {
public List<List<Integer>> levelOrder(TreeNode root) {
//创建一个二维数组 存放结果
List<List<Integer>> res = new ArrayList<>();
//创建一个队列
Queue<TreeNode> que = new LinkedList<TreeNode>();
//BFS广度优先搜索——使用队列实现
if (root != null) {
que.offer(root);
};
//广度优先搜索模板
/*如果不为空队列不为空,就创建一个数组,记录下队列的大小,将队列中的节点的值存入数组*/
while (!que.isEmpty()) {
//创建数组
List<Integer> list = new ArrayList<>();
//记录当前层数的节点个数 即当前层的队列的大小
int len = que.size();//队列的元素是会改变的
while (len > 0) {//只处理len个节点
//存放取出的节点
TreeNode node = que.poll();
list.add(node.val);//将节点的值放到数组中(一维数组)
//将该节点的左右节点都放到队列中 这样就改变了队列的节点的个数了
if (node.left != null) que.offer(node.left);
if (node.right != null) que.offer(node.right);
len--;
}
//将该层的节点(一维数组)放到二维数组里面
res.add(list);
}
return res;
}
}
]]>思路分析:根据题目示例 matrix = [[1,2,3],[4,5,6],[7,8,9]] 的对应输出 [1,2,3,6,9,8,7,4,5] 可以发现,顺时针打印矩阵的顺序是 “从左向右、从上向下、从右向左、从下向上” 循环。
class Solution {
public int[] spiralOrder(int[][] matrix) {
if (matrix.length == 0) return new int [0];//矩阵为空,返回空数组即可
int l = 0, r = matrix[0].length-1, t = 0, b = matrix.length - 1, x = 0;//r 是列长度,即宽度b是行长度,即高度
int[] res = new int[(r + 1) * (b + 1)];//数组的大小就是二维数组相乘的大小
while (true) {
//left to right
for (int i = l; i <= r; i++) {
res[x++] = matrix[t][i];
}
//top大于bottom出边界了
if(++t > b) {
break;
}
//top to bottom
for (int i = t; i <= b; i++) {
res[x++] = matrix[i][r];
}
if (--r < l) {
break;
}
//right to left
for (int i = r; i >= l; i--) {
res[x++] = matrix[b][i];
}
if (--b <t) {
break;
}
//bottom to top
for (int i = b; i >= t; i--) {
res[x++] = matrix[i][l];
}
if (++l > r) {
break;
}
}
return res;
}
}
]]>小厂笔试第一题
解决方案包括递归法和非递归法:
- 递归法 实现前中后序遍历
- 非递归法即迭代法,包括:
- 深度优先搜索 DFS 使用栈模拟 实现前中后序遍历 => 基于栈的深搜其实还不好写统一的前中后序遍历,但是有统一的写法,一刷的时候,由于比较赶,先不写。先掌握递归的写法🐛
- 广度优先搜索 BFS 使用队列模拟 实现层序遍历
法一:递归法(这道题目使用前序遍历和后序遍历都可以,唯独中序遍历不方便,因为中序遍历会把某些节点的左右孩子翻转了两次)
首先确定三要素:
class Solution {
//递归函数
public TreeNode invertTree(TreeNode root) {
//递归法
if (root == null) {
return root;
}
//直接交换,使用前序遍历
swapChildren(root);//中
invertTree(root.left);//左节点放入 反转的时候将左节点的左右子节点翻转
invertTree(root.right);//右节点放入
return root;
}
//交换左右节点
public void swapChildren (TreeNode root) {
TreeNode temp = root.left;
root.left = root.right;
root.right = temp;
}
}
法二:迭代法—层序遍历
思路:层序遍历,每一层分别反转
class Solution {
public TreeNode invertTree(TreeNode root) {
//层序遍历 每一层的节点分别翻转
if (root == null) {return null;}
Queue<TreeNode> que = new LinkedList<>();
que.offer(root);
while (!que.isEmpty()) {
int len = que.size();
for (int i = 0; i < len; i++) {
TreeNode node = que.poll();
swapChildren(node);
if (node.left != null) que.offer(node.left);
if (node.right != null) que.offer(node.right);
}
}
return root;
}
//交换左右节点
public void swapChildren (TreeNode root) {
TreeNode temp = root.left;
root.left = root.right;
root.right = temp;
}
}
]]>双周赛 —6300. 最小公共值
题目链接:https://leetcode.cn/contest/biweekly-contest-96/problems/minimum-common-value/
自己写的:
class Solution {
public int getCommon(int[] nums1, int[] nums2) {
//哈希法 set集合实现,因为无法判断输入数组的大小 浪费空间会比较大
//判断临界条件
// if ( nums1 == null || nums1.length == 0 || nums2 == null || nums2.length == 0) {
// return -1;//返回-1
// }
//存入nums1数组到set集合
Set<Integer> set = new HashSet<>();
for (int i : nums1) {
set.add(i);
}
//存放结果集
Set<Integer> resSet = new HashSet<>();
//遍历nums2同时判断nums1的集合中否存在这个元素,存在的放入resSet中
for (int i : nums2) {
if (set.contains(i)) {
resSet.add(i);
}
}
//返回数组的第一个元素
int[] res = resSet.stream().mapToInt(x -> x).toArray();
if (res != null && res.length != 0) {
Arrays.sort(res);//升序
return res[0];
} else {
return -1;
}
}
}
优化:因为题目告诉我们,它们已经按非降序排序(即已按升序排序)不用将交集放到另外一个集合了,直接第一个找到了就返回该数就好。
class Solution {
public int getCommon(int[] nums1, int[] nums2) {
Set<Integer> set = new HashSet<Integer>();
for (int num : nums1) {
set.add(num);
}
for (int num2 : nums2) {
if (set.contains(num2)) {
return num;
}
}
return -1;
}
}
]]>一些提示:
如果哈希值比较少、特别分散、跨度非常大,使用数组就造成空间的极大浪费!
这道题目没有限制数值的大小,就无法使用数组来做哈希表了
直接使用set 不仅占用空间比数组大,而且速度要比数组慢,set把数值映射到key上都要做hash计算的
思路分析:简单的说就是,将一个数组转化成set集合,再将另外一个数组转化成set集合前判断是否在第一个集合出现过,出现过的再放到另外一个set结果集中。
class Solution {
public int[] intersection(int[] nums1, int[] nums2) {
//判断临界条件
if (nums1 == null || nums1.length == 0 || nums2 == null || nums2.length == 0) {
return new int[0];
}
//先将数组nums1存入set集合中,它当然也会去重,set集合中不会出现重复的元素
Set<Integer> set1 = new HashSet<>();
//存放结果
Set<Integer> resSet = new HashSet<>();
//遍历数组nums1
for (int i : nums1) {
set1.add(i);
}
//遍历数组nums2
for (int i : nums2) {
//判断nums1里面有没有
if (set1.contains(i)) {
resSet.add(i);
}
}
//将集合转化为数组
return resSet.stream().mapToInt(x -> x).toArray();
/*这个代码使用了Java 8中的流(Stream) API,它提供了一种高效的方式来处理数据。
具体来说,这句话的各个部分的作用如下:
resSet.stream():对 resSet 集合进行流操作,返回一个 Stream<Integer> 类型的流。
.mapToInt(x -> x):对流中的每个元素执行 map 操作,将元素映射成一个 int 类型的值。x -> x 表示的是一个Lambda表达式,它的作用是将元素原封不动地映射成 int 类型。
.toArray():将流中的所有 int 值存储在一个 int 数组中,然后将该数组作为结果返回。*/
常数阶:常数阶的操作数量与输入数据大小 n无关,即不随着 n的变化而变化
对数阶:与指数阶正好相反,后者反映“每轮增加到两倍的情况”,而前者反映“每轮缩减到一半的情况”。对数阶仅次于常数阶,时间增长得很慢,是理想的时间复杂度。
线性阶:常出现于单层循环
线性对数阶:常出现于嵌套循环中,两层循环的时间复杂度分别为 O(logn) 和 O(n) 。
主流排序算法的时间复杂度都是 O(nlogN) ,例如快速排序、归并排序、堆排序等。
平方阶:常出现于嵌套循环,外层循环和内层循环都为 O(n)
指数阶:增长得非常快,在实际应用中一般是不能被接受的。若一个问题使用「暴力枚举」求解的时间复杂度是 O(2^n) ,那么一般都需要使用**「动态规划」或「贪心算法」**等算法来求解
思路分析:
使用一个数组记录字符串S出现的次数,若该字母出现了一次,该位置的元素值加一;再遍历另外一个字符串,若出现一次就在数组对应位置的值减一。
因为字母是按顺序的,ASCII码上,每个字母也是相差1,随便取一个字母减去‘a’,就会得到该字母的下标了,然后在对应的record数组上,让该索引下的值+1
时间复杂度:O(n)
空间复杂度:O(1)
class Solution {
public boolean isAnagram(String s, String t) {
//哈希法 用数组来实现
//定义一个统计字母出现次数的字符串
int[] record = new int[26];//开辟26个空间即可
for (int i = 0; i < s.length(); i++) {
record[s.charAt(i) - 'a']++;//因为字母是按顺序的,ASCII码上,每个字母也是相差1,随便取一个字母减去‘a’,就会得到该字母的下标了,然后在对应的record数组上,让该索引下的值+1
}
for (int i = 0; i < t.length(); i++) {
record[t.charAt(i) - 'a']--;
}
//只要record数组里面,不是0的,说明该位置的元素在两个字符串中不相等
for (int count: record) {
if (count != 0) {
return false;
}
}
return true;
}
}
]]>class Solution {
public int findKthLargest(int[] nums, int k) {
// 快速排序 小的放到右边,大的放到左边即可
quickSort(nums, 0, nums.length - 1);
return nums[k - 1];
}
public int[] quickSort(int[] nums, int left, int right) {
if (left >= right) {
return null;
}
int pivot = partion(nums, left, right);
quickSort(nums, left, pivot - 1);
quickSort(nums, pivot + 1,right);
return nums;
}
public int partion(int[] nums, int left, int right) {
int pivot = nums[left];
while (left < right) {
while (left < right && nums[right] <= pivot) {
right--;
}
nums[left] = nums[right];
while (left < right && nums[left] >= pivot) {
left++;
}
nums[right] = nums[left];
}
nums[left] = pivot;
return left;
}
}
]]>理论基础:快速排序是一种常见的排序算法,使用分治的思想来排序一个数组或列表。它的基本思想是选择一个基准数,将数组分成两个部分,一部分是小于基准数的,另一部分是大于等于基准数的,然后再对这两部分递归地进行排序。
好的情况:时间复杂度:nlogn
具体步骤如下:
class Solution {
public int[] sortArray(int[] nums) {
quickSort(nums, 0, nums.length - 1);
return nums;
}
// 快速排序函数
public static int[] quickSort(int[] nums, int left, int right) {
if (left >= right) {
return null;
}
int partition = partition(nums, left, right);
quickSort(nums, left, partition - 1);//在划分位置的左边子区域再划分,直到不可划分left >= right不能进循环条件
quickSort(nums, partition + 1, right); //在划分位置的右边子区域再划分,直到不可划分left >= right不能进循环条件
return nums;
}
public static int partition(int[] nums, int left, int right) {
int pivot = nums[left];
while (left < right) {
while (left < right && nums[right] >= pivot) {
right--;
}
nums[left] = nums[right];
while (left < right && nums[left] <= pivot) {
left++;
}
nums[right] = nums[left];
}
nums[left] = pivot; //当前left,right 指向同一个位置了 nums[right] = pivot也行
return left;// 返回划分的位置
}
}
缺点:如果输入的数组是基本有序的,快速排序的效率会受到影响,因为快速排序的时间复杂度在最坏情况下是 O(n^2),即每次划分都只划分出一个子区间。如果输入的数组基本有序,每次划分可能都会划分出极度不平衡的子区间,使得快排退化成冒泡排序。
双轴快排
分析:双轴快排是一种改进的快速排序算法,其基本思想是将待排序数组划分成三个区域:小于基准元素的区域、等于基准元素的区域和大于基准元素的区域。与传统快排相比,双轴快排使用两个轴值,分别从两端扫描数组,将数组划分成三个区域。具体流程如下:
选取两个轴值p和q,p<q,将数组分成左、中、右三个部分,左部分中所有元素均小于p,右部分中所有元素均大于q,中部分中所有元素均介于p和q之间。
对中部分进行递归排序。
对左、右部分进行递归排序。
由于双轴快排采用两个轴值进行划分,因此每次划分可以减少更多的无用比较,从而提高了排序效率。同时,由于其采用了三路划分的思想,可以处理包含大量重复元素的数组。
双轴快排的时间复杂度为O(nlogn),与传统快排相同,但是实际运行效率更高,尤其是在处理包含大量重复元素的数组时。
class Solution {
public int[] sortArray(int[] nums) {
dualPivotQuickSort(nums, 0, nums.length - 1);
return nums;
}
private void dualPivotQuickSort(int[] nums, int left, int right) {
if (left >= right) {
return;
}
if (nums[left] > nums[right]) {
swap(nums, left, right);
}
int pivot1 = nums[left], pivot2 = nums[right];
int i = left + 1, lt = left + 1, gt = right - 1;
while (i <= gt) {
if (nums[i] < pivot1) {
swap(nums, i++, lt++);
} else if (nums[i] > pivot2) {
swap(nums, i, gt--);
} else {
i++;
}
}
swap(nums, left, --lt);
swap(nums, right, ++gt);
dualPivotQuickSort(nums, left, lt - 1);
dualPivotQuickSort(nums, lt + 1, gt - 1);
dualPivotQuickSort(nums, gt + 1, right);
}
private void swap(int[] nums, int i, int j) {
int temp = nums[i];
nums[i] = nums[j];
nums[j] = temp;
}
}
]]>思路分析:
分析三种情况:
代码步骤:
剪枝的条件:
字符串—长度是奇数的话是一定不匹配的
题解:
class Solution {
public boolean isValid(String s) {
//创建一个栈,存放字符
Deque<Character> deque = new LinkedList<>();
char ch;
for (int i = 0; i < s.length(); i++) {
ch = s.charAt(i);
//碰到左括号,就把相应的右括号放入栈中
if (ch == '(') {
deque.push(')');//压入
} else if (ch == '[') {
deque.push(']');
} else if (ch == '{') {
deque.push('}');
}//处理第三种情况和第二种情况,即第三种:栈为空了,栈没有要去匹配的右括号了
//第二种:栈里面的右括号和字符串遍历的左括号不匹配
else if (deque.isEmpty() || deque.peek() != ch) {
return false;
} else {
deque.pop();//弹出
}
}
//遍历完字符串了,最后解决第一种情况:就是栈中多出来了右括号,但是字符串已经遍历完了
//判断最后的栈内为不为空 不为空放的肯定是右括号
return deque.isEmpty();//不为空返回false
}
}
]]>二分查找法是一种在有序数组中查找某一特定元素的搜索算法。搜索过程从数组的中间元素开始,如果中间元素正好是要查找的元素,则搜索过程结束;如果某一特定元素大于或者小于中间元素,则在数组大于或小于中间元素的那一半中查找,而且跟开始一样从中间元素开始比较。如果在某一步骤数组为空,则代表找不到。这种搜索算法每一次比较都使搜索范围缩小一半
public int binarySearch(int[] nums, int target) {
// 在区间[left,right]中查找元素,左闭右闭
int left = 0;
int right = nums.length - 1;
// 由于是在区间[left,right]中查找
// 因此当left=right时,区间内还有一个元素需要查找
while (left <= right) {
// 计算中间点
int mid = left + (right-left)/2;
// 如果target == nums[mid]则表示已经找到,返回mid
if (target == nums[mid]) {
return mid;
// 如果target < nums[mid],表示目标值可能在左半边
} else if (target < nums[mid]){
// 由于是在左闭右闭的区间[left,right]中查找
// 而target < nums[mid],因此mid不再需要考虑
// 所以right = mid - 1,即在[left,mid-1]中继续查找
right = mid - 1;
// 如果target > nums[mid],表示目标值可能在右半边
} else if (target > nums[mid]){
// 由于是在左闭右闭的区间[left,right]中查找
// 而target > nums[mid],因此mid不再需要考虑
// 所以left = mid + 1,即在[mid+1,right]中继续查找
left = mid + 1;
}
}
// 未找到返回-1
return -1;
}
]]>通过一个快指针和慢指针在一个for循环下完成两个for循环的工作。
暴力解法时间复杂度:O(n^2)
双指针时间复杂度:O(n)
27.移除元素🤓🤓 https://leetcode.cn/problems/remove-element/
快慢指针:通过一个快指针和慢指针在一个for循环下完成两个for循环的工作
class Solution {
public int removeElement(int[] nums, int val) {
int slowIndex = 0;//定义慢指针
for (int fastIndex = 0; fastIndex < nums.length; fastIndex++) {
//快指针去查找数组的值是否等于val
if (val != nums[fastIndex]) {
//nums[slowIndex++] = nums[fastIndex];
nums[slowIndex] = nums[fastIndex];
slowIndex++;
}
}
return slowIndex;
}
}
26.删除排序数组中的重复项🤓🤓
思路分析:
利用数组有序的特点,可以通过双指针的方法删除重复元素。定义两个指针 fast 和 slow 分别为快指针和慢指针,快指针表示遍历数组到达的下标位置,慢指针表示下一个不同元素要填入的下标位置,初始时两个指针都指向下标 1。当数组的长度大于0的时候,至少都包含了一个不重复的元素,因此,nums[0]保持原状即可,从下标1开始。
class Solution {
public int removeDuplicates(int[] nums) {
//判断数组是否为0
int n = nums.length;
if (n == 0) {
return 0;
}
int fast = 1, slow = 1;//从1开始 因为数组至少有一个元素是不重复的
while (fast < n) {//如果整个数组包括0的话,要<=n
if (nums[fast] != nums[fast - 1]) {
nums[slow] = nums[fast];//不相等则记录在慢指针中
// slow = slow + 1 ;//先加了再赋值
++slow;
}
++fast;
}
return slow;
}
}
]]>初刷(一题控制在半小时)
5分钟内没有头绪,直接看题解,尽力去理解,用规范的代码风格,"抄写",不懂也没关系,以后再看。
精刷(一题控制在1小时)
针对初刷未理解的题,尽量全面理解答案的要义,至少能够"默写"出来,调试通过。将解题的思路,以及标准的答案记录到笔记中,笔记做好分类。如果还是未能理解,做好标记。
过题(一题控制在5——10分钟)
按照分类的笔记,大量复习题目。大脑过一遍解题思路,跟正确思路对比是否正确。不需要写题
复刷(一题控制在20分钟)
自言自语分析理解题意,讲清楚自己的思路,写题,检查,一遍跑通!
模拟(一题控制在30分钟)
做一道新题,看看是否能达到复刷的效果,计入笔记。
由于自己练手项目的数据库用的mongodb,想着练手,不重要,当时没做安全考虑,将配置文件上传到了仓库。后来想着还是添加到忽略的文件中,但是不起作用,改得了一次提交,改不了所有的提交记录,由于之前的提交记录仍然存在账号密码,想着应该没人会搞吧,即使搞了也不重要,就没去折腾了。
好家伙,话音刚落,第二天,拿不到后台数据,发现被黑了,这是第一次
要我比特币,后来直接回封邮件给他 fk y ** m 当时的处置方法是,重新删掉原来的数据库,再创建mogodb容器,设置账号密码(设的比较简单)
3.29,前端又拿不到数据了,发现数据库又被黑了 6 ,这一次是另外的人
还是同样的操作,重建了数据库,这次快多了。设置了比较强的密码!!
期待下一次!🥲 💤
]]>接下来的日子里,忙项目、背面经、找实习、备考等等,似乎忙不过来了 计划一下,好好渡过,忙有所得,忙有所思 加油吧!
]]>电脑买的是拯救者的y7000,20款的,当时花了6100左右,想着以后可以自己扩展内存条,并且对硬盘空间要求不是很大,就买了8g的内存,512的硬盘款的,便宜点。具体配置:
测试actions
问题
问题解决
over! 把自己Typora 的博客都迁移到这!