utf8mb4
字符集具有以下特征:BMP
( Basic Multilingual Plane
基本多语种平面 )和 补充字符( Supplementary Multilingual Plane
补充多语种平面)utf8mb4
与utf8mb3
字符集形成对比,后者只支持BMP
字符,每个字符最多使用三个字节BMP
字符,utf8mb4
和utf8mb3
具有相同的存储特征:相同的码值、相同的编码、相同的长度。utf8mb4
需要4个字节来存储它,而utf8mb3
根本不能存储该字符。在将utf8mb3
列转换为utf8mb4
列时,不需要担心转换补充字符,因为没有补充字符。utf8mb4
是utf8mb3
的超集,所以对于下面这样的连接操作,结果是字符集utf8mb4
和排序规则utf8mb4_col
:SELECT CONCAT(utf8mb3_col, utf8mb4_col);
结论:
数据库或者数据表的字段值由utf8mb3
迁移到utf8mb4
,由于utf8mb4
和utf8mb3
具有相同的存储特征:相同的码值、相同的编码、相同的长度所以并不会出现乱码的问题。
处理方案:
ALTER TABLE author_info MODIFY COLUMN address VARCHAR(255) CHARACTER SET utf8mb4;
for i in range(0x20000, 0x20127): # Unicode范围U+20000到U+20126 print(chr(i).encode('utf-8').decode('utf-8'))
ALTER TABLE author_info CONVERT TO CHARACTER SET utf8mb4;
]]>
/** * @author shaoming */public class ByteCodeView { public static void main(String[] args) { System.out.println(factorial(5)); } public static int factorial(int n) { if (n == 0) { return 1; } return n * factorial(n - 1); }}
示例只展示最基础的准备指
javac ByteCodeView.java
javap -verbose ByteCodeView > ByteCode.txt
字节码助记符请参考网络上的助记符表
Classfile /Users/shaoming/workspace/self/extension/src/cn/shellming/t0/ByteCodeView.class Last modified 2021-8-2; size 528 bytes MD5 checksum 825e7254ffc19ed5e1ec428c3604dac7 Compiled from "ByteCodeView.java"public class cn.shellming.t0.ByteCodeView // 类在工程内的类路径 minor version: 0 // JDK 小版本号 major version: 52 // JDK 大版本号 flags: ACC_PUBLIC, ACC_SUPER // public限定类,当调用invokespecial时,需要特殊处理父类的方法Constant pool: // 常量池 #1 = Methodref #6.#18 // java/lang/Object."<init>":()V #2 = Fieldref #19.#20 // java/lang/System.out:Ljava/io/PrintStream; #3 = Methodref #5.#21 // cn/shellming/t0/ByteCodeView.factorial:(I)I #4 = Methodref #22.#23 // java/io/PrintStream.println:(I)V #5 = Class #24 // cn/shellming/t0/ByteCodeView #6 = Class #25 // java/lang/Object #7 = Utf8 <init> #8 = Utf8 ()V #9 = Utf8 Code #10 = Utf8 LineNumberTable #11 = Utf8 main #12 = Utf8 ([Ljava/lang/String;)V #13 = Utf8 factorial #14 = Utf8 (I)I #15 = Utf8 StackMapTable #16 = Utf8 SourceFile #17 = Utf8 ByteCodeView.java #18 = NameAndType #7:#8 // "<init>":()V #19 = Class #26 // java/lang/System #20 = NameAndType #27:#28 // out:Ljava/io/PrintStream; #21 = NameAndType #13:#14 // factorial:(I)I #22 = Class #29 // java/io/PrintStream #23 = NameAndType #30:#31 // println:(I)V #24 = Utf8 cn/shellming/t0/ByteCodeView #25 = Utf8 java/lang/Object #26 = Utf8 java/lang/System #27 = Utf8 out #28 = Utf8 Ljava/io/PrintStream; #29 = Utf8 java/io/PrintStream #30 = Utf8 println #31 = Utf8 (I)V{ public cn.shellming.t0.ByteCodeView(); descriptor: ()V flags: ACC_PUBLIC // public 方法 Code: // 方法栈的大小为1,局部变量表的大小为1,入参数量为1 默认无参的构造方法怎么会有入参呢?其实Java语言有一个潜规则:在任何实例方法里面都可以通过this来访问到此方法所属的对象。Java只是把这种机制后推到编译阶段完成而已。所以,这里的1都是指this这个参数而已。 stack=1, locals=1, args_size=1 0: aload_0 // 从局部变量0中装载引用类型值入栈 1: invokespecial #1 // Method java/lang/Object."<init>":()V , 调用常量池中的 "#1"初始化父类的构造方法 4: return // 返回销毁方法栈 LineNumberTable: line 6: 0 public static void main(java.lang.String[]); descriptor: ([Ljava/lang/String;)V flags: ACC_PUBLIC, ACC_STATIC // 一个public static 的方法 Code: stack=2, locals=1, args_size=1 0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream; 通过getstatic获取系统的输出流静态方法 3: iconst_5 // 把int常量5入栈 4: invokestatic #3 // 调用静态方法 factorial:(I)I 7: invokevirtual #4 // Method java/io/PrintStream.println:(I)V 10: return LineNumberTable: line 9: 0 line 10: 10 public static int factorial(int); descriptor: (I)I flags: ACC_PUBLIC, ACC_STATIC Code: stack=3, locals=1, args_size=1 0: iload_0 // 从局部变量0中装载引用类型值入栈 就是读入参5 [5] 1: ifne 6 // 若栈顶int类型值不为0则跳转到6 [5] 4: iconst_1 // 定义一个常量1并压入栈中 [1] 5: ireturn // 返回栈顶数据 6: iload_0// [5] 7: iload_0// [5, 5] 8: iconst_1 // [1, 5, 5] 9: isub // [4, 5] 10: invokestatic #3 // 用栈顶数据调用 factorial:(I)I 13: imul// 将栈顶两int类型数相乘,结果入栈 14: ireturn// 返回栈顶数据 LineNumberTable: line 13: 0 line 14: 4 line 16: 6 StackMapTable: number_of_entries = 1 frame_type = 6 /* same */}SourceFile: "ByteCodeView.java"
]]>
2020年已经完全过去了,总结本来元旦放假时就想写了,但是由于平时工作过于肝,就利用假期好好休息了。好吧,那么就打开2020年的时间轴吧!
回想那时还是挺快乐的,在大鸟网愉快的搬砖,中午和同事一起去吃饭,下雪了一起去大望京公园看雪,玩雪,滚雪球,好像在2019年的年终总结里有一些那时快乐的图片,可惜中间博客出了一次问题照片全挂了。搬砖一直搬到19年大年二十九,我想我在写19年总结时绝对没想到过20年是这么的魔幻。
因为疫情和一些个人原因我从大鸟网离职了,去谋求新的环境和新的方向,面对这突如其来的疫情,我认为当时我的决定真是有些大胆,之后跟人说起,他们都说:“你真是勇气可嘉”。但从现在来看我很感谢我自己当时果断的决定。
疫情的来袭,我辞职在家后进行更加系统的学习,基本上每周都有一两场的面试或者笔试来检验我的学习成果,我其实非常感谢我的第一个面试官,前前后后面试了两个小时,把我所有的问题全部暴露出来了,接下来我可以按照问题逐一击破。说实话三月实际上是我最痛苦的一个月,因为每天基本都是白天面试被泼冷水,晚上努力给自己打鸡血,那时也动摇过也曾想过,我是不是真的不适合在这个行业生存,后来我又想起了19年我面试时别人告诉我的问题,一个人获取另一个人的肯定那么那个人要足够的自信和好的心态,那时正是我所需要的。但感谢我自己最后还是义无反顾的坚持了下来。
我很幸运,经过不懈的努力在四月份拿到了三家公司的Offer最后三选一去了一家我认为前景还不错的公司,来到公司后感觉同事们都还不错,是我喜欢的类型虽然整体制度和环境远远比不上大鸟网但自己也可以接受。
过了五一假期,我开始在我师傅(虽然我从来没这么叫过他但他对我绝对Nice当然偶尔他也会有点小脾气)指导下独自负责一个项目。在这个过程中指导我使用一些公司内部的基础组件,以及一些流程,当时排期从五月一直排到六月,时间相当充裕。给了我大量的时间去熟悉业务,慢慢的我得到了成长。
业务和基础组件熟悉的差不多了,公司开始了一个有趣的新项目,我负责其中一些功能的开发,那时真的压力挺大因为基本上写的东西是要上线的如果有问题感觉挺丢脸的,我的担心成了真,记得有次上线我写的一个功能在你回归时出了问题,上线当晚如果功能不上上去,其他人负责的功能也要相应下掉回滚,那时我慌了一个人搞不定,结果我师傅一眼就看出问题在哪儿了,平时coding的时候只知道用不知道看底层实现,结果酿成问题。不得不说姜还是老的辣,我师傅说我对代码没有敬畏,从那之后我写代码时就对一些容易异常的地方加以注意了。
再到后来组里的人马壮大了,我公司也逐渐适应了我又随着另外一个师傅一起写另外一个意思的项目,它是一个逻辑错综复杂的中间件,我也是第一次开了眼。一个状态下会有很多个分支,我看了一遍已有的代码,倒吸了一口凉气。忍着我暴躁的心情继续熟悉着代码,后来产品安排了一些需求我发现我明白是怎么回事但是写的时候缺无从下手,我十分苦恼导致几次排期delay,“不能再这样了”,于是乎我在空闲时间多想我这位师傅请教当时设计时的设计思路以及相关功能的核心原理,渐渐的我能“兼容了”哈哈哈。那时真的很开心!后来有一段时间一边熟悉一边写新功能虽然中间出了很多问题,自己写的code没有自信,在一次紧急扩容时他告诉我你一定要对自己写好的代码有自信,你才能更有自信的去coding
后来我负责了中间件中的一个小模块的研发,第一次自己规划所有,自己写的代码被自己一次又一次的重构,直到自己满意。完成后的成就感和满足感真的爆棚,春节前虽然现在这个服务依然存在很多问题,依然没有为公司带来什么价值,但是我相信在以后的迭代中它将变得完美。
在2020年自己真的面对了很大的挑战和压力,年初刚毕业从新找工作时,我的能力也给了我继续向前的动力,你能迈过2020年这么恶劣环境的坎,那么接下来你还会畏惧其他挑战与压力吗?“艰难方显勇毅,磨砺始得玉成” 2021请你加油💪💪💪。
❌ 上半年把六级过了
✅ 读5本以上的技术书籍并加以应用练习真正做到掌握并且能给别人讲明白。
❌ 读5本能引发思考的书把读书总结成Blog
✅ 持续关注学习基础知识掌握总结成Blog
❌ 对一种运动能够持续坚持下去(电竞除外)
✅ 交一些工作中的好朋友
🌟 读5本以上的技术书籍
🌟 运动 运动 运动(重要的事情说三遍)
🌟 学会理财
🌟 过一项职业资格
🌟 开源一些东西
⏰ 2021年02月17日 06:59:41
👨 ShellMing
类继承结构通常表现为,泛化(generalize)与实现(realize)
自行车是车;猫是动物
人可以泛化为男人和女人,方向可以泛化为东方、西方、南方、北方,当然可以更具体。
public class People{....}public class Male extend People{....}public class Female extend People{....}
一个抽象的概念,不能直接定义使用,如“笔”是一个抽象的概念无法直接用来定义对象,只有指明具体的类型才可以用来定义对象,如“钢笔”、“铅笔”、“圆珠笔”等。在实际代码中C++
和Java
都有抽象类,Java
中还有接口这个概念。
public abstract class Write {.....}public class Pen extend Write {.....}public class Pencil extend Write {.....}
聚合关系用于表示实体对象之间的关系,表示整体由部分构成,例如班级由学生构成,整体不与部分强依赖也就是说即使放假了班级解散了,但是学生还在。
public class ClassGrade { private Student student;}public class Student {.....}
与聚合关系不同组合关系是一种强依赖的聚合关系,如果整体不在了,那么部分也就随之消失了。
例如人的手有五根手指组成,如果手没了,剩下你自己想吧........
public class Hand { private One one; private Two two; private Three three; private Four four; private Five five;}
它描述不同类的对象之间的结构关系;它是一种静态关系, 通常与运行状态无关,一般由常识等因素决定的;它一般用来定义对象之间静态的、天然的结构; 所以,关联关系是一种“强关联”的关系;例如学生与学校之间的关系
public class Student { ..... private School school; .....}
与关联关系不同的是,它是一种临时性的关系,通常在运行期间产生,并且随着运行时的变化; 依赖关系也可能发生变化;
依赖是有方向的,双向依赖是一种非常糟糕的结构,我们总是应该保持单向依赖,杜绝双向依赖的产生;在最终代码中,依赖关系体现为类构造方法及类方法的传入参数,箭头的指向为调用关系;依赖关系除了临时知道对方外,还是“使用”对方的方法和属性;其实就是一种类初始化或参数调用的关系。
/* * class init */public class Student { private String name; private String sex; public Student(String name, String sex){ this.name = name; this.sex = sex; }}/* * method invocation */public class Main{ public static int additive (int x, int y){ return x + y; } public static void main (String[] args){ int result = additive(100, 100); System.out.print(result); }}
]]>
int
数据类型无法找到大于或等于13的数字的阶乘太大而int
变量无法容纳,其最大值仅为2147483647(2 ^ 31 -1)
。即使我们使用long
数据类型,大于或等于21
的阶乘也会产生溢出。要计算大于21
的阶乘,需要使用java.math
包中的BigInteger
类。顾名思义,BigInteger
类旨在容纳非常大的整数值,该值甚至大于长整型数的最大值,例如2 ^ 63 -1
或9223372036854775807L
。但不能使用递归来计算较大数量的阶乘,而是需要为此使用for
循环。还值得注意的是,类似于java.lang.String
和其他包装器类, BigInteger
在Java中也是不可变的,这意味着将结果存储回相同的变量很重要,否则,计算结果将会丢失。BigInteger
将数字存储为2的补数。此外,它还支持模块化算术,位操作,素数测试,素数生成,GCD计算和其他其他操作。
public static String bigNumberFactorial(int num){ BigInteger res = BigInteger.ONE; for (int i = num; i > 0; --i){ res = res.multiply(BigInteger.valueOf(i)); } return res.toString();}
]]>
在计算机科学中,二分查找算法(英语:binary search algorithm),也称折半搜索算法(英语:half-interval search algorithm)、对数搜索算法(英语:logarithmic search algorithm),是一种在有序数组中查找某一特定元素的搜索算法。搜索过程从数组的中间元素开始,如果中间元素正好是要查找的元素,则搜索过程结束;如果某一特定元素大于或者小于中间元素,则在数组大于或小于中间元素的那一半中查找,而且跟开始一样从中间元素开始比较。如果在某一步骤数组为空,则代表找不到。这种搜索算法每一次比较都使搜索范围缩小一半。
时间复杂度:O(logN)
空间复杂度:O(1)
public int search(int[] nums, int target){ int lo = 0; int hi = nums.length - 1; while(lo <= hi) { int mid = (lo + hi) >>> 1; if(nums[mid] < target) { lo = mid + 1; } else if(nums[mid] > target) { hi = mid - 1; } else { return mid; } } return -1; }
public int search(int[] nums, int target) { return search(nums, 0, nums.length - 1, target); } public int search(int[] nums, int lo, int hi, int target) { if (lo > hi) { return -1; } int mid = (hi + lo) >>> 1; if (nums[mid] == target) { return mid; } else if (nums[mid] > target) { return search(nums, lo, mid - 1, target); } else { return search(nums, mid + 1, hi, target); } }
]]>
CREATE TABLE consumers( `consumerId` varchar(11) NOT NULL PRIMARY KEY COMMENT '用户Id', `consumerAge` int(10) NOT NULL COMMENT '用户年龄', `consumerName` varchar(120) NOT NULL COMMENT '用户名' KEY `idx_age` (`consumerAge`),)ENGINE = InnoDB default charset = utf8mb4 comment '用户表';
可以看出consumerId
是一个主键索引是一个字符类型的数据如果在查询是给consumerId
字段赋值为一个数值类型,这时MySQL
会将SQL
语句中与原数据类型不匹配的值隐式转换成匹配的数据类型(如果可以的话)。
select * from consumers where consumerId = 18521436711;
这时这条SQL查询语句就不会使用索引,可以用EXPLAIN
来进行分析一下。
EXPLAIN select * from consumers where consumerId = 18521436711\G;
*************************** 1. row *************************** id: 1 select_type: SIMPLE table: consumers partitions: NULL type: ALLpossible_keys: PRIMARY key: NULL key_len: NULL ref: NULL rows: 2 filtered: 50.00 Extra: Using where
EXPLAIN 字段详解
由上面的EXPLAIN
的分析结果可知,在执行SQL
时MySQL
的SQL
分析器分析时预测会使用主键索引,但是实际执行时并没有使用,而使用了FULL SCAN
全表扫描。
select * from consumers where consumerId = '18521436711';
*************************** 1. row *************************** id: 1 select_type: SIMPLE table: consumers partitions: NULL type: constpossible_keys: PRIMARY key: PRIMARY key_len: 46 ref: const rows: 1 filtered: 100.00 Extra: NULL
还是上面那个表结构这时我们要查询年龄大于18
又刚好满2
年的人使用到的SQL
如下
select * from consumers where consumerAge - 2 = 18;
语句是可以正常执行的可以查询出结果但是EXPLAIN
分析后的结果如下
*************************** 1. row *************************** id: 1 select_type: SIMPLE table: consumers partitions: NULL type: ALLpossible_keys: NULL key: NULL key_len: NULL ref: NULL rows: 2 filtered: 100.00 Extra: Using where
由以上分析可以看出这个查询也是没有使用到索引,而是使用了FULL SCAN
全表扫描。这时应该对SQL
语句进行调整如下
select * from consumers where consumerAge = 18 + 2;
select * from consumers where consumerAge = 20;
*************************** 1. row *************************** id: 1 select_type: SIMPLE table: consumers partitions: NULL type: refpossible_keys: idx_age key: idx_age key_len: 4 ref: const rows: 1 filtered: 100.00 Extra: NULL
还是上面那个表结构查询consumerId
为185
开头时
select * from consumers where left(consumerId,3) = '185';
*************************** 1. row *************************** id: 1 select_type: SIMPLE table: consumers partitions: NULL type: ALLpossible_keys: NULL key: NULL key_len: NULL ref: NULL rows: 2 filtered: 100.00 Extra: Using where
由上面分析可以看出key
为NULL
说明在实际查询时并没有使用到主键索引。这时将SQL
语句调整为
select * from consumers where consumerId LIKE '185%';
*************************** 1. row *************************** id: 1 select_type: SIMPLE table: consumers partitions: NULL type: rangepossible_keys: PRIMARY key: PRIMARY key_len: 46 ref: NULL rows: 2 filtered: 100.00 Extra: Using where
这时在观察EXPLAIN
分析Key
值是PRIMARY
说明在查询过程中使用到了主键索引。
以上这三种情况都会导致索引的失效。
索引的使用依赖于BTree索引树的遍历而索引树的遍历依赖于BTree底层叶子结点的有序性当发生以上这三种情况后,有可能这个字段新的排列顺序和原来索引树叶子结点层的排序顺序就不一样了。索引树叶子结点的有序性就会被破坏,当执行SQL时MySQL的执行器无法判断原来的索引树是否还能被检索使用,就会导致执行器不去使用索引而使用全表扫描。
还是上面的那个表结构,现在有一个需求是查询以6711
结尾的consumerId
那么你可能会毫不犹豫的写出下面这条SQL
语句
select * from consumers where consumerId LIKE '%6711'
*************************** 1. row *************************** id: 1 select_type: SIMPLE table: consumers partitions: NULL type: ALLpossible_keys: NULL key: NULL key_len: NULL ref: NULL rows: 2 filtered: 50.00 Extra: Using where
你会发现这条语句没有使用索引然后你又试了试
select * from consumers where consumerId LIKE '%6711%'
*************************** 1. row *************************** id: 1 select_type: SIMPLE table: consumers partitions: NULL type: ALLpossible_keys: NULL key: NULL key_len: NULL ref: NULL rows: 2 filtered: 50.00 Extra: Using where
发现还是没有使用索引然后你又进行了修改
select * from consumers where consumerId LIKE '6711%'
*************************** 1. row *************************** id: 1 select_type: SIMPLE table: consumers partitions: NULL type: rangepossible_keys: PRIMARY key: PRIMARY key_len: 46 ref: NULL rows: 1 filtered: 100.00 Extra: Using where
你会发现这样居然可以使用上索引,这是因为MySQL中的索引树搜索也是遵循最左匹配原则,BTree索引树的叶子结点的有序性也是建立在最左匹配的基础上的,如果使用索引键的中部或者后部进行SQL查询由于违背了最左匹配原则,所以MySQL在执行SQL时无法利用索引树进行搜索所以索引会失效。当然还有以下情况。当一个表中有联合索引**(a,b,c)时使用的索引时应使用(a),(a,b),(a,b,c)如果使用(b),(c),(b,c)时是不会使用到索引这可能是所有网上的说法**但是官方文档 8.3.6 Multiple-Column Indexes中确实也是这么举例的。于是我建了一张测试表
create table test( `id` int(10) NOT NULL PRIMARY KEY , `a` int(10) NOT NULL , `b` int(10) NOT NULL , `c` int(10) not null , key idx_a_b_c(a,b,c))ENGINE = InnoDB
+----+---+---+---+| id | a | b | c |+----+---+---+---+| 1 | 2 | 3 | 4 || 2 | 3 | 4 | 5 || 3 | 4 | 5 | 6 || 4 | 5 | 6 | 7 |+----+---+---+---+
开始分析
explain select * from test where a = 2\G;
*************************** 1. row *************************** id: 1 select_type: SIMPLE table: test partitions: NULL type: refpossible_keys: idx_a_b_c key: idx_a_b_c key_len: 4 ref: const rows: 1 filtered: 100.00 Extra: Using index
explain select * from test where a = 2 and b = 3\G;
*************************** 1. row *************************** id: 1 select_type: SIMPLE table: test partitions: NULL type: refpossible_keys: idx_a_b_c key: idx_a_b_c key_len: 8 ref: const,const rows: 1 filtered: 100.00 Extra: Using index
explain select * from test where a = 2 and b = 3 and c = 4\G;
*************************** 1. row *************************** id: 1 select_type: SIMPLE table: test partitions: NULL type: refpossible_keys: idx_a_b_c key: idx_a_b_c key_len: 12 ref: const,const,const rows: 1 filtered: 100.00 Extra: Using index
以上这三条可以看到key
都是idx_a_b_c
说明执行时使用了索引。接下来看看不会执行索引的。
explain select * from test where c = 6\G;
*************************** 1. row *************************** id: 1 select_type: SIMPLE table: test partitions: NULL type: indexpossible_keys: idx_a_b_c key: idx_a_b_c key_len: 12 ref: NULL rows: 4 filtered: 25.00 Extra: Using where; Using index
😺 这怎么也走索引了
但是查询的explain
结果中显示用到索引的情况类型是不一样的。可观察explain
结果中的type
字段。你的查询中分别是:
index:这种类型表示是MySQL
会对整个该索引进行扫描。要想用到这种类型的索引,对这个索引并无特别要求,只要是索引,或者某个复合索引的一部分,MySQL
都可能会采用index
类型的方式扫描。但是呢,缺点是效率不高,MySQL
会从索引中的第一个数据一个个的查找到最后一个数据,直到找到符合判断条件的某个索引。
ref:这种类型表示mysql会根据特定的算法快速查找到某个符合条件的索引,而不是会对索引中每一个数据都进行一一的扫描判断,也就是所谓你平常理解的使用索引查询会更快的取出数据。而要想实现这种查找,索引却是有要求的,要实现这种能快速查找的算法,索引就要满足特定的数据结构。简单说,也就是索引字段的数据必须是有序的,才能实现这种类型的查找,才能利用到索引。
type :显示的是访问类型,是较为重要的一个指标,结果值从好到坏依次是
system > const > eq_ref > ref > fulltext > ref_or_null > index_merge > unique_subquery > index_subquery > range > index > ALL
一般来说,得保证查询至少达到range级别,最好能达到ref。
关于联合索引最左匹配原则其实网上和官方文档一样,但是当你实验的时候却是另一种结果关于最左匹配原则请参考:
]]>InnoDB存储引擎将数据放在一个逻辑的表空间中,这个表空间就像黑盒一样由InnoDB存储引擎自身进行管理。从MySQL4.1版本开始,它可以将每个InnoDB存储引擎的表单独存放到一个独立的ibd文件中。此外,InnoDB存储引擎支持用裸设备来建立表空间。
InnoDB通过使用多版本并发控制(MVCC)来获得高并发性,并且实现了SQL标准的4种隔离级别,默认为REPEATABLE级别。同时,使用一种被称为next-key locking的策略来避免幻读(phantom)现象的产生。除此之外,InnoDB存储引擎还提供了插入缓存(insert buffer)、二次写(Double Write)、自适应哈希索引(adaptive hash index)、预读(Read ahead)等高性能和高可用的功能。
对于表中数据的存储,InnoDB存储引擎采用了聚集(clustered)的方式,因此每张表的储存都是按主键的顺序进行存放。如果没有显式地在表定义时指定主键,InnoDB存储引擎会为每一行生成一个6字节的ROWID,并以此作为主键。
InnoDB存储引擎有多个内存块,可以认为这些内存块组成了一个大的内存池,
主要作用是负责刷新内存池中的数据,保证缓冲池中的内存缓存的是最近的数据,
将以修改的数据文件刷新到磁盘文件
保证在数据库发生异常的情况下InnoDB能恢复到正常的运行环境
InnoDB储存引擎是对线程模型,因此其后台有多个不同的后台线程,负责处理不同的任务
Master Thread 是一个非常核心的后台线程,主要负责将缓冲池中的数据异步刷新到磁盘,保证数据的一致性,包括脏页的刷新、合并插入缓冲、UNDO页回收等
在InnoDB引擎中大量使用了AIO(Async IO)来处理写请求,这样可以极大提高数据库的性能。而IO Thread的工作主要是负责这些IO请求的回调。共有4种IO线程 write、read、insert buffer和log IO thread从InnoDB1.0之后read thread和write thread 分别增大到4个可以通过innodb_read_io_threads和innodb_write_io_threads参数进行设置
事务被提交后,奇所使用的undolog可能不再需要,因此Purge Thread来回收已经使用并分配的undo页在InnoDB1.1之前Purge Thread的工作由Master Thread中完成,在InnoDB1.1之后purge操作可以独立单独的线程中进行减轻了Master Thread的工作压力,从而提高了了CPU的使用率以及提升储存引擎的性能。Purge的独立线程可以在MySQL的配置文件中手动开启
[mysqld]innodb_purge_threads=1
Purge Cleaner Thread是在InnoDB1.2.x版本时引入。其作用是将之前版本中脏页的刷新操作都放入单独的线程里来完成。目的是减轻Master Thread的工作以及用户查询线程的阻塞。
InnoDB存储引擎是基于磁盘存储的,其中的记录按照页的方式进行管理。因此可以视为磁盘数据库系统。CPU速度和硬盘速度相差很远,所以一般基于磁盘的数据库通常使用缓冲池技术来提高数据库的性能。
缓冲池就是一块内存区域数据库进行读取页的操作时,首先将从资盘读到页存放在缓冲池中,下一次在读相同的页时,首先判断该页是否在缓冲池中,若在缓冲池中则称该页在缓冲池中被命中直接读取该页,否则读取磁盘上的页。
对于数据库中页的修改操作,则首先修改缓冲池中的页,然后再以一定的频率刷新到磁盘上,页从缓冲池刷新回磁盘的操作并不是每次也发生更新时出发而是通过Checkpoint的机制刷新回磁盘。缓冲池的大小也可以设置
innodb_buffer_pool_size
缓冲池中数据页的类型有:索引页、数据页、undo页、插入缓冲页、自适应哈希索引、InnoDB储存的的锁信息、数据字典信息等。缓冲池中索引页和数据页占很大一部分,从InnoDB1.0以后的版本中允许有多个缓冲池的实例每个页根据哈希值平均分配到不同的缓冲池实例中这样可以减少数据库内部的资源竞争,增加数据库的并发性。多实例的配置也可以在配置文件中修改
innodb_buffer_pool_instances
数据库中的缓冲池通过LRU(最近最少使用)算法进行管理。就会死把使用最频繁的放在LRU列表的前端,而最少使用的页放在LRU列表的尾端,当缓冲池满的时候首先释放LRU列表尾部的数据页,InnoDB对传统的LRU算法进行了一些优化,在LRU列表中加入了midpoint位置,新读到的页虽然是最新访问的页,但并不将它放在LRU列表的首都而是放在LRU列表的midpoint位置其中midpoint位置位于LRU列表长度的5/8处midpoint也可以通过配置文件进行修改(以百分之作为单位)
innodb_old_blocks_pct = 50
以上参数代表midpoint位置在LRU列表的50%处,在InnoDB引擎中把midpoint之后的列表称为old list之前的列表称为new list,也可以简单的把new list中的页是活跃的热点数据。为什么使用这种LRU的算法呢?若直接将读取到的页放入LRU的首都那么缓冲区中的页很容易由于索引或数据扫描操作被刷新出去,导致缓冲池效率受影响。为了解决这个问题InnoDB引入了一个配置参数进一步的管理LRU列表
innodb_old_blocks_time
用于表示页读取midpoint位置后等待多久才会被加入LRU列表的热端。
LRU列表用来管理已经读取的页,但当数据库刚启动时,LRU列表是空的,即没有任何的页。这时都存放在Free列表中。当需要从缓冲池中分页时,首先从Free列表中查找是否有可用的空闲页,若有则将该页从Free列表中删除,放入LRU列表中,否则淘汰LRU列表末尾的页,将该内存空间分配给新的页。当页从LRU列表的old部分加入到new部分时称此操作为page made young,而因为innodb_old_blocks_time的设置而导致页没有从old部分移动到new部分的操作称为page not made young。通过查看InnoDB引擎的状态中的Buffer pool hit rate表示缓存中的命中率改值不应该小于95%否则需要观察是否由于全表扫描引起的LRU列表污染。
在LRU列表中的页被修改后,该页称为脏页,就是缓冲池中的页和磁盘上的页的数据产生了不一致。这时数据库会通过CheckPoint机制将脏页刷新回磁盘,而Flush列表中的页即为脏页列表,需要注意的是,脏页既在LRU列表中也存在Flush列表中。LRU列表用于管理缓冲池中页的可用性,Flush列表用于管理将脏页刷新回磁盘,二者互不影响。
InnoDB引擎首先将重做日志先放入这个缓冲区,然后按一定频率将其刷新到重做日志文件。重做日志一般不需要很大,因为一般情况下每一秒会将重做日志缓冲刷新到日志文件,因此用户只需要保证每秒产生的事务量在这个缓冲大小之内即可,该值也可以通过配置文件改变
innodb_log_buffer_size
在通常情况下8MB的重做日志缓冲池足以满足绝大部分的应用,因为重做日志在下列三种情况下会将重做日志缓冲中的内容刷新到外部磁盘的重做日志中
如果一条DML语句如Update、Delete改变了页中的记录,那么此时页是脏的,即缓冲池中的页的版本要比磁盘的新,数据库需要将新版本的页从缓冲池刷新到磁盘。但是每当一个页发生变化就将新的页刷新到磁盘那这个开销就会变的很大,数据库的性能也就变得非常差,同时如果在从缓冲池把脏页刷新到硬盘时发生了宕机那么数据就不能恢复了,为了避免数据丢失,当前事务数据库系统普遍采用了Write Ahead Log策略,即当数据库提交事务时先写重做日志,然后再修改页,这样宕机时的数据就可以通过重做日志来进行恢复。通过重做日志来完成数据恢复,这也是事务ACID中D的要求。
1.缩短数据库的恢复时间
2.缓冲池不够用时,将脏页刷新到磁盘
3.重做日志不可用时,刷新脏页
当数据库发生宕机时,数据库不需要重做所有的日志,因为CheckPoint之前的页已经刷新到磁盘,故只需要对CheckPoint后面的重做日志进行恢复这样就大大减少了恢复的时间。
CheckPoint所做的事情无外乎是将缓存池中的脏页刷回到磁盘,不同之处在于每次刷新多少页到磁盘,每次从哪里取脏页,以及什么时间触发CkeckPoint在InnoDB储存引擎内部,有两种CheckPoint分别为
其中Sharp CheckPoint发生在数据库关闭时将所有的脏页都刷新回磁盘其参数为
innodb_fast_shutdown=1
但是数据库在运行时使用Sharp CheckPoint那么数据库的可用性就会受到很大的影响,所以在InnoDB储存引擎内部使用了FuzzyCheckPoint进行页的刷新,即指刷新一部分脏页而不是刷新所有的脏页回硬盘。几种情况下的Fuzzy CheckPoint
以每秒或每十秒的速度从缓存池的脏页列表中刷新一定比例回硬盘整个过程是异步的不会影响查询线程
InnoDB需要保证LRU列表中需要有差不多100个空闲页可用如果没有或不足那么就会将LRU列表尾部的页移除,如果这些页中有脏页那么就会触发CheckPoint
重做日志在不可用的情况这时需要强制将一些页刷回到硬盘,而此时脏页是从脏页列表中选取的。也是为了保证重做日志的循环使用的可用性。
这个很好理解就是脏页的数量太多了导致引擎强制进行CheckPoint,总之目的就是为了保证缓冲池中有足够多的可用页。强制CheckPoint的阈值也是可以通过参数进行配置
innodb_max_dirty_pages_pct
Master Thread是具有最高优先级别的线程,内部分为多个循环
Master Thread会在这四种循环中进行切换执行不同的任务
分为每秒的操作和每十秒的操作
每秒的操作
操作 | 发生 |
---|---|
日志缓冲刷新到硬盘,即使这个事务还没有提交 | 总是 |
合并插入缓存 | 可能 |
至多刷新100个缓冲池中的脏页到硬盘 | 可能 |
如果用户没有活动切换到background loop | 可能 |
每十秒的操作
操作 | 发生 |
---|---|
刷新100个脏页到磁盘 | 可能 |
合并至多5个插入缓冲 | 总是 |
将日志缓冲刷新到磁盘 | 总是 |
删除无用的Undo页 | 总是 |
刷新100个或者10个脏页到硬盘 | 总是 |
当钱没有用户活动或者数据库关闭就会切换到这个循环background loop的操作
操作 | 发生 |
---|---|
删除无用的Undo页 | 总是 |
合并20个插入缓冲 | 总是 |
跳回到主循环 | 总是 |
不断刷新100个页直到符合条件 | 可能(跳转到flush loop中完成) |
当flush loop中没有事情可做了引擎会切换到Suspend loop,将Master Thread挂起等待事件发生。
InsertBuffer和数据页一样,也是物理页的以个组成部分,对于非聚簇索引的插入或更新操作不是每一次都直接插入索引页中,而是先判断插入的非聚簇索引是否在缓冲池中,若在则直接插入若不在则先放入一个InsertBuffer的对象然后以一定频率和情况进行InsertBuffer和辅助索引页子节点的merge操作这时通常能将多个插入合并到一个操作中就可以提高非聚簇索引的插入性能。InsertBuffer需要满足以下两个条件引擎才会使用
ChangeBuffer是在InnoDB1.0之后的版本引入的可以看作是InsertBuffer的升级,ChangeBuffer的出现使InnoDB可以对DML操作INSERT、DELETE、UPDATE都进行缓冲他们分别是InsertBuffer、DeleteBuffer、PurgeBuffer和之前InsertBuffer一样ChangeBuffer适用对象依然是非唯一的辅助索引对一条记录进行Update操作分为两个过程:
因此DeleteBuffer对应update操作的第一个过程,即将记录标记为删除PurgeBuffer对应update操作个第二个过程即将记录真正的删除,ChangeBuffer也是可以通过配置文件选择性的开启
innodb_change_buffering=[inserts][deletes][purges][changes][all][none]
也可以配置ChangeBuffer占缓冲池的最大百分比,默认值为25也就是1/4缓冲池内存空间,最大值为50。
innodb_change_buffer_max_size
如果说InsertBuffer带给InnoDB存储引擎的是性能上的提升,那么double write带给InnoDB引擎的是数据页的可靠。
如果在数据库发生宕机时,可能InnoDB储存引擎正在写入某个页到表中,而这个页只写了一部分,比如16KB的页,只写了前4KB之后发生了宕机,这种情况被称为部分写失效在引擎未使用doublewrite技术前会出现写失效导致数据丢失的情况。
doublewrite由两部分组成,一部分在内存中的doublewrite buffer大小为2MB另一部分是物理磁盘上共享表空间中连续128个页,即2个区,大小同样为2MB,在对缓冲池的脏页进行刷新时,并不直接写磁盘,而是会通过memcpy函数将脏页先复制到内存中的doublewrite buffer,之后通过doublewrite buffer再分两次,每次1MB顺序地写入共享表空间的物理磁盘上,人后马上调用fsync函数,同步磁盘,避免缓冲写带来的问题,在这个过程中,因为doublewrite页时连续的因此这个过程是顺序写,开销并不大。在完成doublewrite页的写入后,再将doublewrite buffer中的页写入各个表空间的文件中,此时写入是离散的。如下图
哈希是一种很快的查找方法,一般情况下hash算法得当时间复杂度为O(1),而B+树的查找次数,取决于树的高度,一般B+树在生产环境中高度在3~4左右,故需要3~4次查询。
InnoDB引擎会监控表上个索引页的查询。如果观察到建立哈希索引可以带来速度提升,就会建立哈希索引,称之为自适应哈希索引(AHI)AHI通过缓冲池的B+树的页构造而来,因此建立的速度很快,而且不需要对整张表构建哈希索引。InnoDB引擎会自动根据访问的频率和模式自动的为某些热点页建立哈希索引。
AHI有一个要求,即对这个页的连续访问模式必须是一样的,访问模式是指查询的条件一样此外AHI还有如下条件
在InnoDB引擎中read ahead方式的读取都是通过AIO完成,脏页的刷新即磁盘的写入操作则全部由AIO完成。
当刷新一个脏页时InnoDB引擎会检测该页所在区的所有页如果有脏页那么一起进行刷新,这样做的好处显而易见,通过AIO可以将多个IO写入操作合并为一个IO操作,故这种工作机制在传统机械硬盘上有显著的优势但需要考虑:
可以认为MVCC是行级锁的一个变种,但是它在很多情况下避免了加锁操作,因此开销更低。虽然实现机制有所不同,但大都实现了非阻塞读的操作,写操作也只锁定必要的行。
MVCC的实现是通过保存数据在某个时间点的快照实现的。也就是说不管需要执行行多长时间,每个事务看到的数据都是一致的。根据事务开始时间的不同,每个事务对同一张表,同一时刻看到的数据可能是不一样的。对于不同的引擎MVCC实现是不同的典型的有乐观并发控制和悲观并发控制。
InnoDB的MVCC是通过在每行记录后面保存两个隐藏的列实现的,这两个列,一个保存的创建时间,一个保存的过期时间。当然存储的并不是实际的时间值,而是系统版本号。每开始一个新的事务,系统版本号都会自动递增。事务开始时刻的系统版本海会作为事务的版本号,用来和查询到的每行记录的版本号进行比较。
]]>1.选定Pivot中心轴
2.将大于Pivot的数字放在Pivot的右边
3.将小于Pivot的数字放在Pivot的左边
4.分别对左右子序列重复前三步操作
时空复杂度
算法图解
public class QuickSort { public int[] sort(int[] sourceArray) { int[] res = Arrays.copyOf(sourceArray, sourceArray.length); return qc(res, 0, res.length - 1); } public int[] qc(int[] arr, int left, int right) { if (left < right) { int partitionIndex = partition(arr, left, right); qc(arr, left, partitionIndex - 1); qc(arr, partitionIndex + 1, right); } return arr; } public int partition(int[] arr, int left, int right) { int pivot = left; int index = pivot + 1; for (int i = index; i <= right; ++i) { if (arr[i] < arr[pivot]) { swap(arr, i, index); index++; } } swap(arr, pivot, index - 1); return index - 1; } public void swap(int[] arr, int i, int j) { int temp = arr[i]; arr[i] = arr[j]; arr[j] = temp; }}
]]>
给定一个有序整数数组,元素各不相同且按升序排列,编写一个算法,创建一棵高度最小的二叉搜索树。
给定有序数组: [-10,-3,0,5,9]
,
一个可能的答案是:[0,-3,9,-10,null,5]
,它可以表示下面这个高度平衡二叉搜索树:
0 / \ -3 9 / / -10 5
由于是有序数组,就取数组中间点为根节点,左边为左子树,右边为右子树,依次递归。
/** * Definition for a binary tree node. * public class TreeNode { * int val; * TreeNode left; * TreeNode right; * TreeNode(int x) { val = x; } * } */class Solution { public TreeNode sortedArrayToBST(int[] nums) { return helper(nums,0,nums.length - 1); } public TreeNode helper(int[] nums,int left,int right){ if(left > right){ return null; } int mid = (left + right) / 2; TreeNode node = new TreeNode(nums[mid]); node.left = helper(nums,left,mid - 1); node.right = helper(nums,mid + 1,right); return node; }}
]]>
实现一种算法,删除单向链表中间的某个节点(除了第一个和最后一个节点,不一定是中间节点),假定你只能访问该节点。
输入:单向链表a->b->c->d->e->f中的节点c结果:不返回任何数据,但该链表变为a->b->d->e->f
实际上删除的是给定节点next节点给定节点val和next节点val做了值Copy
/** * Definition for singly-linked list. * public class ListNode { * int val; * ListNode next; * ListNode(int x) { val = x; } * } */class Solution { public void deleteNode(ListNode node) { node.val = node.next.val; node.next = node.next.next; }}
]]>
给定一棵二叉搜索树,请找出其中第k大的节点。
输入: root = [3,1,4,null,2], k = 1 3 / \ 1 4 \ 2输出: 4
输入: root = [5,3,6,2,4,null,null,1], k = 3 5 / \ 3 6 / \ 2 4 / 1输出: 4
1 ≤ k ≤ 二叉搜索树元素个数
平衡二叉树中序倒序遍历,提前返回
/** * Definition for a binary tree node. * public class TreeNode { * int val; * TreeNode left; * TreeNode right; * TreeNode(int x) { val = x; } * } */class Solution { int res,k; public int kthLargest(TreeNode root, int k) { this.k = k; loop(root); return res; } public void loop(TreeNode node){ if(node == null) return; loop(node.right); if(k == 0) return; if(--k == 0) res = node.val; loop(node.left); }}
]]>
定义一个函数,输入一个链表的头节点,反转该链表并输出反转后链表的头节点。
输入: 1->2->3->4->5->NULL输出: 5->4->3->2->1->NULL
限制:
0 <= 节点个数 <= 5000
经典递归
/** * Definition for singly-linked list. * public class ListNode { * int val; * ListNode next; * ListNode(int x) { val = x; } * } */class Solution { public ListNode reverseList(ListNode head) { if(head == null || head.next == null){ return head; } ListNode cur = reverseList(head.next); head.next.next = head; head.next = null; return cur; }}
]]>
在一个数组 nums 中除一个数字只出现一次之外,其他数字都出现了三次。请找出那个只出现一次的数字。
输入:nums = [3,4,3,3]输出:4示例 2:输入:nums = [9,1,7,9,7,9,7]输出:1
1 <= nums.length <= 100001 <= nums[i] < 2^31
HashCounter
class Solution { public int singleNumber(int[] nums) { int res = 0; Map<Integer,Integer> map = new HashMap<>(); for(int i : nums){ if(map.containsKey(i)){ map.put(i,map.get(i) + 1); }else{ map.put(i,1); } } for (Map.Entry<Integer,Integer> entry : map.entrySet()){ if(entry.getValue() == 1){ res = entry.getKey(); } } return res; }}
没看懂的写法
class Solution { public int singleNumber(int[] nums) { int a = 0,b =0; for(int i:nums){ b = ~a&(b^i); a = ~b&(a^i); } return b; }}
]]>
输入一个链表的头节点,从尾到头反过来返回每个节点的值(用数组返回)。
输入:head = [1,3,2]输出:[2,3,1]
0 <= 链表长度 <= 10000
Stack FILO
/** * Definition for singly-linked list. * public class ListNode { * int val; * ListNode next; * ListNode(int x) { val = x; } * } */class Solution { public int[] reversePrint(ListNode head) { if (head == null){ return new int[0]; } Stack<Integer> stack = new Stack<>(); while (head != null){ stack.push(head.val); head = head.next; } int[] res = new int[stack.size()]; for (int i = 0; i < res.length; ++i){ res[i] = stack.pop(); } return res; }}
经典回溯
/** * Definition for singly-linked list. * public class ListNode { * int val; * ListNode next; * ListNode(int x) { val = x; } * } */class Solution { int[] res; int i; public int[] reversePrint(ListNode head) { recur(head,0); return res; } public void recur(ListNode node, int count){if(node == null){ res = new int[i]; i--; return;}i++;recur(node.next,i);res[i - count] = node.val; }}
]]>
考虑一个简单的软件应用场景,一个软件系统可以提供多个外观不同的按钮(如圆形按钮、矩形按钮、菱形按钮等), 这些按钮都源自同一个基类,不过在继承基类后不同的子类修改了部分属性从而使得它们可以呈现不同的外观,如果我们希望在使用这些按钮时,不需要知道这些具体按钮类的名字,只需要知道表示该按钮类的一个参数,并提供一个调用方便的方法,把该参数传入方法即可返回一个相应的按钮对象,此时,就可以使用简单工厂模式。
简单工厂模式(Simple Factory Pattern):又称为静态工厂方法(Static Factory Method)模式,它属于类创建型模式。在简单工厂模式中,可以根据参数的不同返回不同类的实例。简单工厂模式专门定义一个类来负责创建其他类的实例,被创建的实例通常都具有共同的父类。
简单工厂模式包含如下角色:
Factory:工厂角色
工厂角色负责实现创建所有实例的内部逻辑
Product:抽象产品角色
抽象产品角色是所创建的所有对象的父类,负责描述所有实例所共有的公共接口
ConcreteProduct:具体产品角色
具体产品角色是创建目标,所有创建的对象都充当这个角色的某个具体类的实例。
public interface User { String listUserInfo();}
public class Admin implements User { @Override public String listUserInfo() { return "ADMIN"; } public void add(){ } public void del(){ } public void edit(){ } public void query(){ }}
public class Guest implements User { @Override public String listUserInfo() { return "GUEST"; } public void edit(){ } public void query(){ }}
public class UserFactory { public static User getUser(String type){ switch (type){ case "admin": return new Admin(); case "guest": return new Guest(); default: throw new RuntimeException("无用户类别"); } }}
public class ToGetUser { public static void main(String[] args) { User admin = UserFactory.getUser("admin"); User guest = UserFactory.getUser("guest"); System.err.println(admin.listUserInfo()); System.err.println(guest.listUserInfo()); }}
工厂类含有必要的判断逻辑,可以决定在什么时候创建哪一个产品类的实例,客户端可以免除直接创建产品对象的责任,而仅仅“消费”产品;简单工厂模式通过这种做法实现了对责任的分割,它提供了专门的工厂类用于创建对象。
客户端无须知道所创建的具体产品类的类名,只需要知道具体产品类所对应的参数即可,对于一些复杂的类名,通过简单工厂模式可以减少使用者的记忆量。
通过引入配置文件,可以在不修改任何客户端代码的情况下更换和增加新的具体产品类,在一定程度上提高了系统的灵活性。
由于工厂类集中了所有产品创建逻辑,一旦不能正常工作,整个系统都要受到影响。
使用简单工厂模式将会增加系统中类的个数,在一定程序上增加了系统的复杂度和理解难度。
系统扩展困难,一旦添加新产品就不得不修改工厂逻辑,在产品类型较多时,有可能造成工厂逻辑过于复杂,不利于系统的扩展和维护。
简单工厂模式由于使用了静态工厂方法,造成工厂角色无法形成基于继承的等级结构。
工厂类负责创建的对象比较少:由于创建的对象较少,不会造成工厂方法中的业务逻辑太过复杂。
客户端只知道传入工厂类的参数,对于如何创建对象不关心:客户端既不需要关心创建细节,甚至连类名都不需要记住,只需要知道类型所对应的参数。
创建型模式对类的实例化过程进行了抽象,能够将对象的创建与对象的使用过程分离。
简单工厂模式又称为静态工厂方法模式,它属于类创建型模式。在简单工厂模式中,可以根据参数的不同返回不同类的实例。简单工厂模式专门定义一个类来负责创建其他类的实例,被创建的实例通常都具有共同的父类。
简单工厂模式包含三个角色:工厂角色负责实现创建所有实例的内部逻辑;抽象产品角色是所创建的所有对象的父类,负责描述所有实例所共有的公共接口;具体产品角色是创建目标,所有创建的对象都充当这个角色的某个具体类的实例。
简单工厂模式的要点在于:当你需要什么,只需要传入一个正确的参数,就可以获取你所需要的对象,而无须知道其创建细节。
简单工厂模式最大的优点在于实现对象的创建和对象的使用分离,将对象的创建交给专门的工厂类负责,但是其最大的缺点在于工厂类不够灵活,增加新的具体产品需要修改工厂类的判断逻辑代码,而且产品较多时,工厂方法代码将会非常复杂。
简单工厂模式适用情况包括:工厂类负责创建的对象比较少;客户端只知道传入工厂类的参数,对于如何创建对象不关心。
插入排序的代码实现虽然没有冒泡排序和选择排序那么简单粗暴,但它的原理应该是最容易理解的了,因为只要打过扑克牌的人都应该能够秒懂。插入排序是一种最简单直观的排序算法,它的工作原理是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。
插入排序和冒泡排序一样,也有一种优化算法,叫做拆半插入。
1.将第一待排序序列第一个元素看做一个有序序列,把第二个元素到最后一个元素当成是未排序序列。
2.从头到尾依次扫描未排序序列,将扫描到的每个元素插入有序序列的适当位置。(如果待插入的元素与有序序列中的某个元素相等,则将待插入元素插入到相等元素的后面。)
public int[] sort(int[] sourceArray) { long t1 = System.currentTimeMillis(); int[] arr = Arrays.copyOf(sourceArray, sourceArray.length); for(int i = 1; i < arr.length; ++i){int temp = arr[i];int j = i;while(j > 0 && temp < arr[j - 1]){arr[j] = arr[j - 1];j--;}if(i != j){ arr[j] = temp;}}return arr; }
public int[] sort(int[] sourceArray) { int[] arr = Arrays.copyOf(sourceArray,sourceArray.length); int low,high,m,temp,i,j; for(i = 1;i<arr.length;i++){ low = 0; high = i-1; while(low <= high){ m = (low+high)/2; if(arr[m] > arr[i]) { high = m - 1; } else { low = m + 1; } } temp = arr[i]; for(j=i;j>high+1;j--){ arr[j] = arr[j-1]; } arr[high+1] = temp; } return arr; }
]]>
单例模式属于创建型模式的一种。在应用这个模式时,单例对象的类必须保证只有一个实例存在。许多时候整个系统只需要拥有一个的全局对象,这样有利于我们协调系统整体的行为。比如在某个服务器程序中,该服务器的配置信息存放在一个文件中,这些配置数据由一个单例对象统一读取,然后服务进程中的其他对象再通过这个单例对象获取这些配置信息。这种方式简化了在复杂环境下的配置管理。
实现单例模式的思路是:一个类能返回对象一个引用(永远是同一个)和一个获得该实例的方法(必须是静态方法,通常使用getInstance这个名称);当我们调用这个方法时,如果类持有的引用不为空就返回这个引用,如果类保持的引用为空就创建该类的实例并将实例的引用赋予该类保持的引用;同时我们还将该类的构造函数定义为私有方法,这样其他处的代码就无法通过调用该类的构造函数来实例化该类的对象,只有通过该类提供的静态方法来得到该类的唯一实例。
单例模式在多线程的应用场合下必须小心使用。如果当唯一实例尚未创建时,有两个线程同时调用创建方法,那么它们同时没有检测到唯一实例的存在,从而同时各自创建了一个实例,这样就有两个实例被构造出来,从而违反了单例模式中实例唯一的原则。 解决这个问题的办法是为指示类是否已经实例化的变量提供一个互斥锁(虽然这样会降低效率)。
public class Singleton{ private static Singleton singleton = null; private Singleton(){} public static Singleton getInstance(){ if(singleton == null){ return new Singleton; } return singleton; }}
私有静态变量 singleton
被延迟实例化,这样做的好处是,如果没有用到该类,那么就不会实例化 singleton
,从而节约资源。
这个实现在多线程环境下是不安全的,如果多个线程能够同时进入 if (singleton == null)
,并且此时 singleton
为 null
,那么会有多个线程执行 singleton = new Singleton();
语句,这将导致实例化多次 singleton
。
public class Singleton{ private static Singleton singleton = null; private Singleton(){} public synchronized static Singleton getInstance(){ if(singleton == null){ return new Singleton; } return singleton; }}
只需要对 getInstance()
方法加锁,那么在一个时间点只能有一个线程能够进入该方法,从而避免了实例化多次singleton
。
但是当一个线程进入该方法之后,其它试图进入该方法的线程都必须等待,即使 singleton
已经被实例化了。这会让线程阻塞时间过长,因此该方法有性能问题,不推荐使用。
public class Singleton{ private static Singleton singleton = new Singleton(); private Singleton(){} public static Singleton getInstance(){ return singleton; }}
线程不安全问题主要是由于 singleton
被实例化多次,采取直接实例化 singleton
的方式就不会产生线程不安全问题。
但是直接实例化的方式也丢失了延迟实例化带来的节约资源的好处。
public class Singleton{ private Singleton(){} private static InnerClass{ private static final Singleton singleton = new Singleton(); } public static Singleton getInstance(){ return InnerClass.singleton; }}
静态内部类的优点是:外部类加载时并不需要立即加载内部类,内部类不被加载则不去初始化singleton
,故而不占内存。即当Singleton
第一次被加载时,并不需要去加载InnerClass
,只有当getInstance()方法第一次被调用时,才会去初始化singleton
,第一次调用getInstance()
方法会导致虚拟机加载InnerClass
类,这种方法不仅能确保线程安全,也能保证单例的唯一性,同时也延迟了单例的实例化。
那么,静态内部类又是如何实现线程安全的呢?首先,我们先了解下类的加载时机。
new
、getstatic
、setstatic
或者invokestatic
这4个字节码指令时,对应的Java
代码场景为:new
一个关键字或者一个实例化对象时、读取或设置一个静态字段时(final
修饰、已在编译期把结果放入常量池的除外)、调用一个类的静态方法时。java.lang.reflect
包的方法对类进行反射调用的时候,如果类没进行初始化,需要先调用其初始化方法进行初始化。main()
方法的类),虚拟机会先初始化这个类。java.lang.invoke.MethodHandle
实例最后的解析结果REF_getStatic、REF_putStatic、REF_invokeStatic
的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化。这5种情况被称为是类的主动引用,注意,这里《虚拟机规范》中使用的限定词是"有且仅有",那么,除此之外的所有引用类都不会对类进行初始化,称为被动引用。静态内部类就属于被动引用的行列。
我们再回头看下getInstance()
方法,调用的是InnerClass.singleton
,取的是InnerClass
里的singleton
对象,跟DCL
方法不同的是,getInstance()
方法并没有多次去new
对象,故不管多少个线程去调用getInstance()
方法,取的都是同一个singleton
对象,而不用去重新创建。当getInstance()
方法被调用时,InnerClass
才在Singleton
的运行时常量池里,把符号引用替换为直接引用,这时静态对象singleton
也真正被创建,然后再被getInstance()
方法返回出去,这点同饿汉模式。那么singleton
在创建过程中又是如何保证线程安全的呢?在《深入理解JAVA虚拟机》中,有这么一句话:
虚拟机会保证一个类的<clinit>()
方法在多线程环境中被正确地加锁、同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的<clinit>()
方法,其他线程都需要阻塞等待,直到活动线程执行<clinit>()
方法完毕。如果在一个类的<clinit>()
方法中有耗时很长的操作,就可能造成多个进程阻塞(需要注意的是,其他线程虽然会被阻塞,但如果执行<clinit>()
方法后,其他线程唤醒之后不会再次进入<clinit>()
方法。同一个加载器下,一个类型只会初始化一次,在实际应用中,这种阻塞往往是很隐蔽的。
故而,可以看出singleton
在创建过程中是线程安全的,所以说静态内部类形式的单例可保证线程安全,也能保证单例的唯一性,同时也延迟了单例的实例化。
那么,是不是可以说静态内部类单例就是最完美的单例模式了呢?其实不然,静态内部类也有着一个致命的缺点,就是传参的问题,由于是静态内部类的形式去创建单例的,故外部无法传递参数进去,例如Context
这种参数,所以,我们创建单例时,可以在静态内部类与DCL
模式里自己斟酌。
public class Singleton{ private volatile static Singleton singleton = null; private Singleton(){} public static Singleton getInstance(){ if(singleton == null){ synchronized(Singleton.class){ if(singleton == null){ return new Singleton; } } } return singleton; }}
singleton
只需要被实例化一次,之后就可以直接使用了。加锁操作只需要对实例化那部分的代码进行,只有当 singleton
没有被实例化时,才需要进行加锁。
双重校验锁先判断 singleton
是否已经被实例化,如果没有被实例化,那么才对实例化语句进行加锁。
考虑下面的实现,也就是只使用了一个 if
语句。在 singleton == null
的情况下,如果两个线程都执行了if
语句,那么两个线程都会进入if
语句块内。虽然在 if
语句块内有加锁操作,但是两个线程都会执行 singleton = new Singleton();
这条语句,只是先后的问题,那么就会进行两次实例化。因此必须使用双重校验锁,也就是需要使用两个 if
语句:第一个 if
语句用来避免 singleton
已经被实例化之后的加锁操作,而第二个 if
语句进行了加锁,所以只能有一个线程进入,就不会出现 singleton == null
时两个线程同时进行实例化操作。
if (singleton == null) { synchronized (Singleton.class) { singleton = new Singleton(); }}
singleton
采用 volatile
关键字修饰也是很有必要的, singleton = new Singleton();
这段代码其实是分为三步执行:
singleton
分配内存空间singleton
singleton
指向分配的内存地址但是由于 JVM
具有指令重排的特性,执行顺序有可能变成 1>3>2
。指令重排在单线程环境下不会出现问题,但是在多线程环境下会导致一个线程获得还没有初始化的实例。例如,线程 T1
还没有执行2就执行了 1
和3
,此时T2
调用 getInstance()
后发现 singleton
不为空,因此返回 singleton
,但此时 singleton
还未被初始化。
使用 volatile
可以禁止 JVM
的指令重排,保证在多线程环境下也能正常运行。
在《Effective Java》最后推荐了这样一个写法,简直有点颠覆,不仅超级简单,而且保证了线程安全。这里引用一下,此方法无偿提供了序列化机制,绝对防止多次实例化,及时面对复杂的序列化或者反射攻击。单元素枚举类型已经成为实现Singleton的最佳方法。
public enum Singleton { INSTANCE;}
很多人会对枚举法实现的单例模式很不理解。这里需要深入理解的是两个点:
private
类型的构造函数对于第一点实际上enum内部是如下代码:
public enum Singleton { INSTANCE; // 这里隐藏了一个空的私有构造方法 private Singleton () {}}
对于一个标准的enum单例模式,最优秀的写法还是实现接口的形式:
interface ISingleton{ void doSomething();}public enum Singleton implements ISingleton{ // 枚举 INSTANCE { @Override public void doSomething() { System.out.println(getName() + " " + getAge()); } }; private String name; private Integer age; public String getName() { return name; } public void setName(String name) { this.name = name; } public Integer getAge() { return age; } public void setAge(Integer age) { this.age = age; } private static Singleton getInstance(){ return Singleton.INSTANCE; }}
]]>
首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置
再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。
重复第二步,直到所有元素均排序完毕。
public class SelectionSort implements ArraySort { @Override public int[] sort(int[] sourceArray) { long t1 = System.currentTimeMillis(); int[] res = Arrays.copyOf(sourceArray,sourceArray.length); // 总共要经过 N-1 轮比较 for (int i = 0; i < res.length - 1; ++i){ int min = i; // 每轮需要比较的次数 N-i for (int j = i + 1; j < res.length; ++j){ if (res[min] > res[j]){ min = j; } } // 将找到的最小值和i位置所在的值进行交换 if (min != i){ int temp = res[min]; res[min] = res[i]; res[i] = temp; } } System.err.println(this.getClass().getName() + " " + (System.currentTimeMillis() - t1)); return res; }}
]]>
比较相邻的元素。如果第一个比第二个大,就交换他们两个。
对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对。这步做完后,最后的元素会是最大的数。
针对所有的元素重复以上的步骤,除了最后一个。
持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。
public class BubbleSort implements ArraySort { @Override public int[] sort(int[] sourceArray) { long t1 = System.currentTimeMillis(); int[] res = Arrays.copyOf(sourceArray,sourceArray.length); for(int i = 1; i < res.length; ++i){ boolean flag = true; for (int j = 0; j < res.length - i; ++j){ if(res[j] > res[j + 1]){ int temp = res[j]; res[j] = res[j + 1]; res[j + 1] = temp; flag = false; } } if (flag){ break; } } System.err.println(this.getClass().getName() + " " + (System.currentTimeMillis() - t1)); return res; }}
]]>
给定一个链表,两两交换其中相邻的节点,并返回交换后的链表。
你不能只是单纯的改变节点内部的值,而是需要实际的进行节点交换。
给定 1->2->3->4, 你应该返回 2->1->4->3.
/** * Definition for singly-linked list. * public class ListNode { * int val; * ListNode next; * ListNode(int x) { val = x; } * } */class Solution { public ListNode swapPairs(ListNode head) { ListNode pre = new ListNode(0);pre.next = head;ListNode temp = pre;while(temp.next != null && temp.next.next != null){ListNode start = temp.next;ListNode end = temp.next.next;temp.next = end;start.next = end.next;end.next = start;temp = start;}return pre.next; }}
start:temp = 0->7->2->4->3// ListNode start = temp.next;start = 7->2->4->3// ListNode end = temp.next.next;end = 2->4->3// temp.next = end;temp = 0->2->4->3// start.next = end.next;start = 7->4->3// end.next = start;end = 2->7->4->3end:
]]>
给定两个非空链表来代表两个非负整数。数字最高位位于链表开始位置。它们的每个节点只存储单个数字。将这两数相加会返回一个新的链表。
你可以假设除了数字 0 之外,这两个数字都不会以零开头。
如果输入链表不能修改该如何处理?换句话说,你不能对列表中的节点进行翻转。
输入: (7 -> 2 -> 4 -> 3) + (5 -> 6 -> 4)
输出: 7 -> 8 -> 0 -> 7
public class Solution { public ListNode addTwoNumbers(ListNode l1, ListNode l2) { ListNode res = null; Stack<Integer> p = new Stack<>(); Stack<Integer> q = new Stack<>(); while (l1 != null){ p.push(l1.val); l1 = l1.next; } while (l2 != null){ q.push(l2.val); l2 = l2.next; } int carry = 0; while (!p.isEmpty() || !q.isEmpty()){ int x = (p.size() > 0) ? p.pop() : 0; int y = (q.size() > 0) ? q.pop() : 0; int sum = x + y + carry; carry = sum / 10; sum %= 10; ListNode t = new ListNode(sum); t.next = res; res = t; } if (carry > 0){ ListNode t = new ListNode(carry); t.next = res; res = t; } return res; }
]]>
给出两个 非空 的链表用来表示两个非负的整数。其中,它们各自的位数是按照 逆序 的方式存储的,并且它们的每个节点只能存储 一位 数字。
如果,我们将这两个数相加起来,则会返回一个新的链表来表示它们的和。
您可以假设除了数字 0
之外,这两个数都不会以 0
开头。
输入:(2 -> 4 -> 3) + (5 -> 6 -> 4)输出:7 -> 0 -> 8原因:342 + 465 = 807
/** * Definition for singly-linked list. * public class ListNode { * int val; * ListNode next; * ListNode(int x) { val = x; } * } */class Solution { public ListNode addTwoNumbers(ListNode l1, ListNode l2) { ListNode res = new ListNode(0);ListNode temp = res,q = l1,p = l2;int carry = 0;while(q != null || p != null){int x = (q != null) ? q.val : 0;int y = (p != null) ? p.val : 0;int sum = x + y + carry;carry = sum / 10;sum = sum % 10;temp.next = new ListNode(sum);temp = temp.next;if(q != null) q = q.next;if(p != null) p = p.next;} if(carry > 0){temp.next = new ListNode(carry);}return res.next; }}
]]>
适用范围:对数据库中的数据进行一些简单操作,如insert,delete,update,select等.
适用范围:对数据库中的某些对象(例如,database,table)进行管理,如CREATE,ALTER和DROP.
区别:
DML
操作是可以手动控制事务的开启、提交和回滚的。
DDL
操作是隐性提交的,不能rollback
!
名词解释(仅限本文)
<databaseName>
数据库名称<tableName>
数据表名称<fieldName>
字段名称<aliasName>
别名<condition>
条件<number>
数值<offset>
偏移量<value>
值<func>
函数<OP>
操作符USE <databaseName>; #使用数据库
show databases; # 返回可用数据库的列表show tables; # 返回使用数据库中可用的表show columns from <tableName>; # 返回表中的字段信息show status; # 用于显示广泛的服务器状态信息show create database <databaseName>; # 显示创建数据库的MySQL语句show create table <tableName>; # 显示创建表的MySQL语句show grants; # 显示授予用户的安全权限show errors; # 显示服务器的错误show warnings; # 显示服务器警告信息
数据类型允许限制可存储在列中的数据。例如,数值数据类型列只能接受数值。
数据类型允许在内部更有效地存储数据。可以用一种比文本串更简洁的格式存储数值和日期时间值。
数据类型允许变换排序顺序。如果所有数据都作为串处理,则1位于10之前,而10又位于2之前(串以字典顺序排序,从左边开始比较,一次一个字符)。作为数值数据类型,数值才能正确排序。
定长串接受长度固定的字符串,其长度是在创建表时指定的。例如,名字列可允许30个字符,而社会安全号列允许11个字符(允许的字符数目中包括两个破折号)。定长列不允许多于指定的字符数目。它们分配的存储空间与指定的一样多。因此,如果串Ben存储到30个字符的名字字段,则存储的是30个字符,CHAR属于定长串类型。变长串存储可变长度的文本。有些变长数据类型具有最大的定长,而有些则是完全变长的。不管是哪种,只有指定的数据得到保存(额外的数据不保存)TEXT属于变长串类型。
既然变长数据类型这样灵活,为什么还要使用定长数据类型?回答是因为性能。MySQL处理定长列远比处理变长列快得多。此外,MySQL不允许对变长列(或一个列的可变部分)进行索引。这也会极大地影响性能。
数据类型 | 说明 |
---|---|
CHAR | 1~255个字符的定长串它的长度必须在创建时指定,否则MySQL假定为CHAR(1) |
ENUM | 接受最多64K个串组成的一个预定义集合的某个串 |
LONGTEXT | 与TEXT相同,但最大长度为4GB |
MEDIUMTEXT | 与TEXT相同,但最大长度为16K |
SET | 接受最多64个串组成的一个预定义集合的零个或多个串 |
TEXT | 最大长度为64K的变长文本 |
TINYTEXT | 与TEXT相同,但最大长度为255字节 |
VARCHAR | 长度可变,最多不超过255字节。如果在创建时指定为VARCHAR(n),则可储存0到n个字符长度n<=255 |
数据结构 | 说明 |
---|---|
DATE | 表示1000-01-01~9999-12-31的日期格式为YYYY-MM-DD |
DATETIME | DATE和TIME的组合 |
TIMESTAMP | 功能和DATETIME相同(但范围较小) |
TIME | 格式为HH:MM:SS |
YEAR | 用2位数字表示范围是70(1970年)~69(2069年),用4位数字表示是1901~2155年 |
数据类型 | 说明 |
---|---|
BIT | 位字段 1~64位 |
BIGINT | 整数值 -9223372036854775808~9223372036854775807,UNSIGNED 0~18446744073709551615 |
BOOLEAN | 布尔标志 0/1 |
DECIMAL | 精度可变的浮点值 |
DOUBLE | 双精度浮点值 |
FLOAT | 单精度浮点值 |
INT | 整数值 -2147483648~2147483647,UNSIGNED 0~4294967295 |
MEDIUMINT | 整数值支持-8388608~8388607,UNSIGNED 0~16777215 |
REAL | 4字节浮点值 |
SMALLINT | 整数值 支持-32768~32767,UNSIGNED 0~65535 |
TINYINT | 整数值 支持-128~127, UNSIGNED 0~255 |
数据类型 | 说明 |
---|---|
BLOB | Blob最大长度为64KB |
MEDIUMBLOB | Blob最大长度为16MB |
LONGBLOB | Blob最大长度为4GB |
TINYBLOB | Blob最大长度为255字节 |
select <fieldName>,<fieldName>,···,···, from <tableName>; # 从指定的表中检索一个或多个名为<fieldName>的列select * from <tableName>; # 从指定的表中检索所有字段列"*"是一个通配符select distinct <fieldName> from <tableName>; # 显示唯一的字段distinct应用于所有列而不仅是前置它的列select ç from <tablelName> limit <number>;# 使用select语句检索单个列或多个列返回不多于指定number行数select <fieldName> from <tableName> limit <offset>,<number>;# 使用select语句检索单个列或多个列返回从offset的开始的不多于指定number的行数offset是从0开始计算的
select <fieldName> from <tableName> order by <fieldName>,<fieldName>,···,···;# 用select语句检索出的数据可使用order by子语句,order by子句取一个或多个列进行排序select <fieldName> from <tableName> order by <fieldName>,<fieldName>,···,···DESC(ASC);# 用select语句检索出的数据可使用order by子语句,order by子句取一个或多个列进行排序默认为升序排列,DESC指定为降序排列
select <fieldName> from <tableName> where <condition>;# 根据where后的条件过滤检索数据 同时使用order by和where子语句时应该让order by位于where之后select <fieldName> from <tableName> where <fieldName>BETWEEN 5 AND 10;# 过滤出5到10之间的数据行包括指定的开始值和结束值也就是说包括5和10的记录select <fieldName> from <tableName> where <fieldName> IS NULL;# 从检索到的数据中过滤空值
NULL与不匹配
在通过过滤选择出不具有特定值的行时,你可能希望返回具有NULL
值的行。但是,不行。因为未知具有特殊的含义,数据库不知道它们是否匹配,所以在匹配过滤或不匹配过滤时不返回它们。因此,在过滤数据时,一定要验证返回数据中确实给出了被过滤列具有NULL
的行。
where子语句操作符
操作符 | 说明 |
---|---|
= | 等于 |
<> | 不等于 |
!= | 不等于 |
< | 小于 |
<= | 小于等于 |
> | 大于 |
>= | 大于等于 |
BETWEEN | 在指定两个值之间 |
select <fieldName>,···,··· from <tableName> where <fieldName> <OP> <Value> and <fieldName> <OP> <Value>;# 通过不止一个列进行过滤多个过滤条件用and连接select <fieldName>,···,··· from <tableName> where <fieldName> <OP> <Value> or <fieldName> <OP> <Value>;# 满足任一条件进行过滤select <fieldName>,···,··· from <tableName> where (<fieldName> <OP> <Value> or <fieldName> <OP> <Value>) and <fieldName> <OP> <Value>;# or和and是有计算次序的SQL中and的计算次序优先级比or高所以要用"()"来提升计算次序select <fieldName>,···,··· from <tableName> where <fieldName> in (<value>,<Value>,···,···);# IN操作符用来指定条件范围,范围中的每个条件都可以进行匹配IN操作符完成与OR相同的功能select <fieldName>,···,··· from <tableName> where <fieldName> not in (<value>,<Value>,···,···);# NOT操作符有且只有一个功能,那就是否定它之后所跟的任何条件。
select <fieldName>,···,···,from <tableName> like '<OP>';
通配符 | 说明 |
---|---|
% | 替代 0 个或多个字符 |
_ | 替代一个字符 |
不要过度使用通配符。如果其他操作符能达到相同的目的,应该使用其他操作符。
在确实需要使用通配符时,除非绝对有必要,否则不要把它们用
在搜索模式的开始处。把通配符置于搜索模式的开始处,搜索起来是最慢的。
仔细注意通配符的位置。如果放错地方,可能不会返回想要的数据
select <fieldName>,···,···, from <tbaleName> where <fieldName> REGEXP <OP>;
符号 | 说明 |
---|---|
^ | 匹配输入字符串的开始位置。如果设置了 RegExp 对象的 Multiline 属性,^ 也匹配 '\n' 或 '\r' 之后的位置。 |
$ | 匹配输入字符串的结束位置。如果设置了RegExp 对象的 Multiline 属性,$ 也匹配 '\n' 或 '\r' 之前的位置。 |
. | 匹配除 "\n" 之外的任何单个字符。要匹配包括 '\n' 在内的任何字符,请使用象 '[.\n]' 的模式。 |
[...] | 字符集合。匹配所包含的任意一个字符。例如, '[abc]' 可以匹配 "plain" 中的 'a'。 |
[^...] | 负值字符集合。匹配未包含的任意字符。例如, '[^abc]' 可以匹配 "plain" 中的'p'。 |
p1|p2|p3 | 匹配 p1 或 p2 或 p3 |
* | 匹配前面的子表达式零次或多次。 |
+ | 匹配前面的子表达式一次或多次。 |
{n} | n 是一个非负整数。匹配确定的 n 次。 |
{n,m} | m 和 n 均为非负整数,其中n <= m。 |
select Concat(<fieldName>,<filedName>) from <tableName>;
Concat()拼接串,即把多个串连接起来形成一个较长的串。
Concat()需要一个或多个指定的串,各个串之间用逗号分隔。
select Concat(<fieldName>,<filedName>) AS <aliasName> from <tableName>;# 别名(alias)是一个字段或值的替换名。别名用AS关键字赋予。
select <fieldName1>,<fieldName2>,<fieldName1> <OP> <fieldName> AS <aliasName> from <tableName>;# 可以对字段进行算术运算结果生成新的字段
操作符 | 说明 |
---|---|
+ | 加 |
- | 减 |
* | 乘 |
/ | 除 |
select <fieldName>,<func>(<fieldName>) as <alias> from <tableName>;
函数 | 说明 |
---|---|
Left() | 返回串左边的字符 |
Length() | 返回字符串的长度 |
Locate() | 找出串的一个子串 |
Lower() | 将串转换为小写 |
LTrim() | 去掉串左边的空格 |
Right() | 返回串右边的字符 |
RTrim() | 去掉串右边的空格 |
Soundex() | 返回SOUNDEX值 |
SubString() | 返回子串的字符 |
Upper() | 将串转换为大写 |
函数 | 说明 |
---|---|
AddDate() | 增加一个日期(天、周等) |
AddTime() | 增加一个时间(时、分等) |
CurDate() | 返回当前日期 |
CurTime() | 返回当前时间 |
Date() | 返回日期时间的日期部分 |
DateDiff() | 计算两个日期之差 |
Date_Add() | 高度灵活的日期运算函数 |
Date_Format() | 返回一个格式化的日期或时间串 |
Day() | 返回一个日期的天数部分 |
DayOfWeek() | 对于一个日期,返回对应的星期几 |
Hour() | 返回一个时间的小时部分 |
Minute() | 返回一个时间的分钟部分 |
Month() | 返回一个日期的月份部分 |
Now() | 返回当前日期和时间 |
Second() | 返回一个时间的秒部分 |
Time() | 返回一日期时间的时间部分 |
Yesr() | 返回一个日期的年份部分 |
yyyy-mm-dd
。因此,2005年9月1日
,给出为2005-09-01
select <fieldName>,···,··· from <tableName> where Date(<filedName>) = 'yyyy-mm-dd';
函数 | 说明 |
---|---|
Abs() | 返回一个树的绝对值 |
Cos() | 返回一个角度的余弦 |
Exp() | 返回一个数的指数值 |
Mod() | 返回除操作的余数 |
Pi() | 返回圆周率 |
Rand() | 返回一个随机数 |
Sin() | 返回一个角度的正弦 |
Sqrt() | 返回一个数的平方根 |
Tan() | 返回一个角度的正切 |
函数 | 说明 |
---|---|
AVG() | 返回某列的平均值 |
COUNT() | 返回某列的行数 |
MAX() | 返回某列的最大值 |
MIN() | 返回某列的最小值 |
SUM() | 返回某列值之和 |
select <func>(<fieldName>) as <aliasName> from <tableName>;
select <func>(distinct <fieldName>) as <aliasName> from <tableName> where <fieldName> <OP> <Value>;
如果指定列名,则
DISTINCT
只能用于COUNT()
。DISTINCT
不能用于COUNT(*)
,因此不允许使用COUNT(DISTINCT
),否则会产生错误。类似地,DISTINCT
必须使用列名,不能用于计算或表达式。
select count(*) as <aliasName>, MIN(<fieldName>) as min, Max(<fieldName>) as max, AVG(<fieldName>) as avgfrom <tableName>;# SELECT语句可根据需要包含多个聚集函数
select <fieldName>,···,··· from <tableName> group by <fieldName>;
group by
子句规则
GROUP BY
子句可以包含任意数目的列。这使得能对分组进行嵌套,为数据分组提供更细致的控制。GROUP BY
子句中嵌套了分组,数据将在最后规定的分组上进行汇总。换句话说,在建立分组时,指定的所有列都一起计算(所以不能从个别的列取回数据)。GROUP BY
子句中列出的每个列都必须是检索列或有效的表达式(但不能是聚集函数)。如果在SELECT
中使用表达式,则必须在GROUP BY
子句中指定相同的表达式。不能使用别名。SELECT
语句中的每个列都必须在GROUP BY
子句中给出。NULL
值,则NULL
将作为一个分组返回。如果列中有多行NULL
值,它们将分为一组。GROUP BY
子句必须出现在WHERE
子句之后,ORDER BY
子句之前。使用WITH ROLLUP关键字,可以得到每个分组以及每个分组汇总级别(针对每个分组)的值。
select <fieldName>,···,··· from <tableName> group by <fieldName> with rollup;
WHERE子句的条件(包括通配符条件和带多个操作符的子句)。所学过的有关WHERE的所有这些技术和选项都适用于HAVING。它们的句法是相同的,唯一的差别是WHERE过滤行,而HAVING过滤分组。
select <fieldName>,···,··· from <tableName> group by <fieldName> having <condition>;
不要忘记ORDER BY 一般在使用GROUP BY子句时,应该也给出ORDER BY子句。这是保证数据正确排序的唯一方法。千万不要仅依赖GROUP BY排序数据。
select <fieldName>,···,··· from <tableName> group by <fieldName> having <condition> order by <condition>;
子句 | 说明 | 是否必须使用 |
---|---|---|
select | 要返回的列或表达式 | 是 |
from | 从中检索数据的表 | 仅在从表选择数据时使用 |
where | 行级过滤 | 否 |
group by | 分组说明 | 仅在按组计算聚集时使用 |
having | 组级过滤 | 否 |
order by | 输出排序顺序 | 否 |
limit | 要检索的行数 | 否 |
SELECT
语句根据需求进行合并select <fieldName> from <tableName> where <fieldName> in (select <fieldName> from <tableName> where <condition>);
select <fieldName>,···,··· from <tableName>where <fieldName> in(select <fieldName> from <tableName> where <fieldName> in(select <fieldName> from <tableName> where <condition>));
select <fieldName>, <fieldName>, (select count(*) from <tableName> where <condition>) as <aliasName>from <tableName>order by <fieldName>;
inner join
关键字select <fieldName>,···from <tableName> inner join <tableName>on <tableName>.<fieldName> = <tableName>.<fieldName>;
where
做等值连接select <fieldName>,···from <tableName>,<tableName>,···where <tableName>.<fieldName> = <tableName>.<fieldName>;
select <aliasName>.<fieldName>,<aliasName>.<fieldName>from <tableName> as p1,<tableName> as p2where p1.<fieldName> = p2.<fieldName>and p2.<fieldName> = <Value>
select p1.prod_id,p1.from products as p1,products as p2where p1.vend_id = p2.vend_idand p2.prod_id = 'DTNTR'
自然连接是把同名列通过等值测试连接起来的,同名列可以有多个。
内连接和自然连接的区别:内连接提供连接的列,而自然连接自动连接所有同名列。
select c.*,o.order_num,o.order_date,ol.prod_id,ol.quantity,ol.item_pricefrom customers as c,orders as o , orderitems as olwhere c.cust_id = o.cust_idand ol.order_num = o.order_numand prod_id = 'FB'
外连接保留了没有关联的那些行。分为左外连接,右外连接以及全外连接,左外连接就是保留左表没有关联的行。
左外连接(left join / left outer join): 满足on
条件表达式,左外连接是以左表为准,返回左表所有的数据,与右表匹配的则有值,没有匹配的则以空(null
)取代。
右外连接(right join / right outer join):满足on
条件表达式,右外连接是以右表为准,返回右表所有的数据,与左表匹配的则有值,没有匹配的则以空(null
)取代。
全外连接(full join / full outer join):满足on
条件表达式,返回两个表符合条件的所有行,a
表没有匹配的则a
表的列返回null
,b
表没有匹配的则b
表的列返回null
,即返回的是左连接和右连接的并集。
交叉连接(cross join):交叉连接将会返回被连接的两个表的笛卡尔积,返回结果的行数等于两个表行数的乘积。
使用 UNION 来组合两个查询,如果第一个查询返回 M 行,第二个查询返回 N 行,那么组合查询的结果一般为 M+N 行。
每个查询必须包含相同的列、表达式和聚集函数。
默认会去除相同行,如果需要保留相同行,使用 UNION ALL。
只能包含一个 ORDER BY 子句,并且必须位于语句的最后。
SELECT colFROM mytableWHERE col = 1UNIONSELECT colFROM mytableWHERE col =2;
视图是虚拟的表,本身不包含数据,也就不能对其进行索引操作。
对视图的操作和对普通表的操作一样。
数据库中只存放了视图的定义,而没有存放视图中的数据,这些数据存放在原来的表中。
使用视图查询数据时,数据库系统会从原来的表中取出对应的数据。
视图中的数据依赖于原来表中的数据,一旦表中数据发生改变,显示在视图中的数据也会发生改变。
视图具有如下好处:
CREATE VIEW myview ASSELECT Concat(col1, col2) AS concat_col, col3*col4 AS compute_colFROM mytableWHERE col5 = val;
存储过程可以看成是对一系列 SQL 操作的批处理。
使用存储过程的好处:
命令行中创建存储过程需要自定义分隔符,因为命令行是以 ; 为结束符,而存储过程中也包含了分号,因此会错误把这部分分号当成是结束符,造成语法错误。
包含 in、out 和 inout 三种参数。
给变量赋值都需要用 select into 语句。
每次只能给一个变量赋值,不支持集合的操作。
delimiter //create procedure myprocedure( out ret int ) begin declare y int; select sum(col1) from mytable into y; select y*y into ret; end //delimiter ;
call myprocedure(@ret);select @ret;
由于不要求反复建立一系列处理步骤,这保证了数据的完整性。如果所有开发人员和应用程序都使用同一(试验和测试)存储过程,则所使用的代码都是相同的。这一点的延伸就是防止错误。需要执行的步骤越多,出错的可能性就越大。防止错误保证了数据的一致性。
简化对变动的管理。如果表名、列名或业务逻辑(或别的内容)有变化,只需要更改存储过程的代码。使用它的人员甚至不需要知道这些变化。这一点的延伸就是安全性。通过存储过程限制对基础数据的访问减少了数据讹误(无意识的或别的原因所导致的数据讹误)的机会。
提高性能。因为使用存储过程比使用单独的SQL语句要快。
存在一些只能用在单个请求中的MySQL元素和特性,存储过程可以使用它们来编写功能更强更灵活的代码
在存储过程中使用游标可以对一个结果集进行移动遍历。游标主要用于交互式应用,其中用户需要对数据集中的任意行进行浏览和修改。
使用游标的四个步骤:
声明游标,这个过程没有实际检索出数据;
打开游标;
取出数据;
关闭游标;
delimiter //create procedure myprocedure(out ret int) begin declare done boolean default 0; declare mycursor cursor for select col1 from mytable; # 定义了一个 continue handler,当 sqlstate '02000' 这个条件出现时,会执行 set done = 1 declare continue handler for sqlstate '02000' set done = 1; open mycursor; repeat fetch mycursor into ret; select ret; until done end repeat; close mycursor; end // delimiter ;
基本术语:
transaction
)指一组 SQL
语句;rollback
)指撤销指定 SQL
语句的过程;commit
)指将未存储的 SQL
语句结果写入数据库表;savepoint
)指事务处理中设置的临时占位符(placeholder
),你可以对它发布回退(与回退整个事务处理不同)。不能回退 SELECT
语句,回退 SELECT
语句也没意义;也不能回退 CREATE
和 DROP
语句。
MySQL
的事务提交默认是隐式提交,每执行一条语句就把这条语句当成一个事务然后进行提交。当出现 START TRANSACTION
语句时,会关闭隐式提交;当 COMMIT
或 ROLLBACK
语句执行后,事务会自动关闭,重新恢复隐式提交。
设置 autocommit
为 0
可以取消自动提交;autocommit
标记是针对每个连接而不是针对服务器的。
如果没有设置保留点,ROLLBACK
会回退到 START TRANSACTION
语句处;如果设置了保留点,并且在 ROLLBACK
中指定该保留点,则会回退到该保留点。
START TRANSACTION// ...SAVEPOINT delete1// ...ROLLBACK TO delete1// ...COMMIT
CREATE TABLE mytable ( # int 类型,不为空,自增 id INT NOT NULL AUTO_INCREMENT, # int 类型,不可为空,默认值为 1,不为空 col1 INT NOT NULL DEFAULT 1, # 变长字符串类型,最长为 45 个字符,可以为空 col2 VARCHAR(45) NULL, # 日期类型,可为空 col3 DATE NULL, # 设置主键为 id PRIMARY KEY (`id`));
ALTER TABLE mytableADD col CHAR(20);
ALTER TABLE mytableDROP COLUMN col;
DROP TABLE mytable;
INSERT INTO mytable(col1, col2)VALUES(val1, val2);
INSERT INTO mytable1(col1, col2)SELECT col1, col2FROM mytable2;
CREATE TABLE newtable ASSELECT * FROM mytable;
UPDATE mytableSET col = valWHERE id = 1;
DELETE FROM mytableWHERE id = 1;
TRUNCATE TABLE mytable;
使用更新和删除操作时一定要用 WHERE 子句,不然会把整张表的数据都破坏。可以先用 SELECT 语句进行测试,防止错误删除。
MySQL 的账户信息保存在 mysql 这个数据库中。
USE mysql;SELECT user FROM user;
创建账户
新创建的账户没有任何权限。
CREATE USER myuser IDENTIFIED BY 'mypassword';
修改账户名
RENAME USER myuser TO newuser;
删除账户
DROP USER myuser;
查看权限
SHOW GRANTS FOR myuser;
授予权限
账户用 username@host 的形式定义,username@% 使用的是默认主机名。
GRANT SELECT, INSERT ON mydatabase.* TO myuser;
删除权限
GRANT 和 REVOKE 可在几个层次上控制访问权限:
REVOKE SELECT, INSERT ON mydatabase.* FROM myuser;Copy to clipboardErrorCopied
更改密码
必须使用 Password() 函数进行加密。
SET PASSWROD FOR myuser = Password('new_password');
]]>
给定一个链表,判断链表中是否有环。
为了表示给定链表中的环,我们使用整数 pos
来表示链表尾连接到链表中的位置(索引从 0
开始)。 如果 pos
是 -1
,则在该链表中没有环。
输入:head = [3,2,0,-4], pos = 1输出:true解释:链表中有一个环,其尾部连接到第二个节点。
输入:head = [1,2], pos = 0输出:true解释:链表中有一个环,其尾部连接到第一个节点。
输入:head = [1], pos = -1输出:false解释:链表中没有环。
进阶:
你能用 O(1)
(即,常量)内存解决此问题吗?
有环链表Set遍历去重检测
public boolean hashSolution(ListNode head) { Set<ListNode> set = new LinkedHashSet<>(); while (head != null) { if (set.contains(head)) { return true; } set.add(head); head = head.next; } return false;}
双指针一次慢指针走一步快指针一次走两步,相遇了就有环
public class Solution { public boolean hasCycle(ListNode head) { if(head == null || head.next == null){ return false; } ListNode fast = head.next.next; ListNode slow = head.next; while(fast != null && fast.next != null && slow != null){ if(fast == slow){ return true; } fast = fast.next.next; slow = slow.next; } return false; }}
]]>
实现一种算法,找出单向链表中倒数第 k 个节点。返回该节点的值。
注意:本题相对原题稍作改动
输入: 1->2->3->4->5 和 k = 2输出: 4说明:
给定的 k 保证是有效的。
双指针一看就明白
/** * Definition for singly-linked list. * public class ListNode { * int val; * ListNode next; * ListNode(int x) { val = x; } * } */class Solution { public int kthToLast(ListNode head, int k) { ListNode fast = head,slow = head; int count = 0; while(fast != null){ fast = fast.next; count++; if(count > k){ slow = slow.next; } } return slow.val; }}
效率低的方法
/** * Definition for singly-linked list. * public class ListNode { * int val; * ListNode next; * ListNode(int x) { val = x; } * } */class Solution { public int kthToLast(ListNode head, int k) { Map<Integer,Integer> map = new HashMap<>(); int index = 0; while(head != null){ map.put(index++,head.val); head = head.next; } return map.get(index - k); }}
]]>
给定一个带有头结点 head
的非空单链表,返回链表的中间结点。
如果有两个中间结点,则返回第二个中间结点。
输入:[1,2,3,4,5]输出:此列表中的结点 3 (序列化形式:[3,4,5])返回的结点值为 3 。 (测评系统对该结点序列化表述是 [3,4,5])。注意,我们返回了一个 ListNode 类型的对象 ans,这样:ans.val = 3, ans.next.val = 4, ans.next.next.val = 5, 以及 ans.next.next.next = NULL.
输入:[1,2,3,4,5,6]输出:此列表中的结点 4 (序列化形式:[4,5,6])由于该列表有两个中间结点,值分别为 3 和 4,我们返回第二个结点。
给定链表的结点数介于 1 和 100 之间。
平凡的暴力解法
/** * Definition for singly-linked list. * public class ListNode { * int val; * ListNode next; * ListNode(int x) { val = x; } * } */class Solution { public ListNode middleNode(ListNode head) { ListNode root = head,index = head; int count = 0; while (root != null){ root = root.next; count++; } count /= 2; int ss = 0; while (index != null){ index = index.next; ss++; if (ss == count){ break; } } return index; }}
快慢指针图一看就懂
/** * Definition for singly-linked list. * public class ListNode { * int val; * ListNode next; * ListNode(int x) { val = x; } * } */class Solution { public ListNode middleNode(ListNode head) { ListNode fast = head; ListNode slow = head; while(fast != null && fast.next != null){ fast = fast.next.next; slow = slow.next; } return slow; }}
]]>
这是一个二叉树
1,3,4,2,5
4,3,2,1,5
4,2,3,5,1
1,3,5,4,2
1,3,4,2,5
class Solution { List<Integer> preOrder = new ArrayList<>(); List<Integer> inOrder = new ArrayList<>(); List<Integer> postOrder = new ArrayList<>(); // 前序遍历 public void preOrder(TreeNode treeNode) { preOrder.add(treeNode.val); if (treeNode.left != null){ preOrder(treeNode.left); } if (treeNode.right != null){ preOrder(treeNode.right); } } // 中序遍历 public void inOrder(TreeNode treeNode){ if (treeNode.left != null){ inOrder(treeNode.left); } inOrder.add(treeNode.val); if (treeNode.right != null){ inOrder(treeNode.right); } } // 后序遍历 public void postOrder(TreeNode treeNode){ if (treeNode.left != null){ postOrder(treeNode.left); } if (treeNode.right != null){ postOrder(treeNode.right); } postOrder.add(treeNode.val); } // 广度优先搜索 public List<Integer> bfs(TreeNode treeNode){ List<Integer> bfs = new ArrayList<>(); if (treeNode == null){ return bfs; } Queue<TreeNode> queue = new LinkedList<>(); queue.offer(treeNode); while (!queue.isEmpty()){ TreeNode t = queue.poll(); bfs.add(t.val); if (t.left != null){ queue.offer(t.left); } if (t.right != null){ queue.offer(t.right); } } return bfs; } // 深度优先搜索 public List<Integer> dfs(TreeNode treeNode){ List<Integer> dfs = new ArrayList<>(); Stack<TreeNode> stack = new Stack<>(); stack.push(treeNode); if (treeNode == null){ return dfs; } while (!stack.isEmpty()){ TreeNode t = stack.pop(); dfs.add(t.val); //先让右结点入栈,在pop时确保左结点先出栈 if (t.right != null){ stack.push(t.right); } if (t.left != null){ stack.push(t.left); } } return dfs; } public static void main(String[] args) { Solution solution = new Solution(); TreeNode t = new TreeNode(1); t.left = new TreeNode(3); t.right = new TreeNode(5); t.left.left = new TreeNode(4); t.left.right = new TreeNode(2); solution.preOrder(t); solution.inOrder(t); solution.postOrder(t); System.err.println(solution.preOrder); System.err.println(solution.inOrder); System.err.println(solution.postOrder); System.err.println(solution.bfs(t)); System.err.println(solution.dfs(t)); }}class TreeNode { int val; TreeNode left; TreeNode right; TreeNode(int x) { val = x; }}
[1, 3, 4, 2, 5][4, 3, 2, 1, 5][4, 2, 3, 5, 1][1, 3, 5, 4, 2][1, 3, 4, 2, 5]
]]>
在二维数组grid
中,grid[i][j]
代表位于某处的建筑物的高度。 我们被允许增加任何数量(不同建筑物的数量可能不同)的建筑物的高度。 高度 0
也被认为是建筑物。
最后,从新数组的所有四个方向(即顶部,底部,左侧和右侧)观看的“天际线”必须与原始数组的天际线相同。 城市的天际线是从远处观看时,由所有建筑物形成的矩形的外部轮廓。 请看下面的例子。
建筑物高度可以增加的最大总和是多少?
输入: grid = [[3,0,8,4],[2,4,5,7],[9,2,6,3],[0,3,1,0]]输出: 35解释: The grid is:[ [3, 0, 8, 4], [2, 4, 5, 7], [9, 2, 6, 3], [0, 3, 1, 0] ]
从数组竖直方向(即顶部,底部)看“天际线”是:[9, 4, 8, 7]
从水平水平方向(即左侧,右侧)看“天际线”是:[8, 7, 9, 3]
在不影响天际线的情况下对建筑物进行增高后,新数组如下:
gridNew = [ [8, 4, 8, 7], [7, 4, 7, 7], [9, 4, 8, 7], [3, 3, 3, 3] ]
1 < grid.length = grid[0].length <= 50
。grid[i][j]
的高度范围是: [0, 100]
。
一座建筑物占据一个grid[i][j]
:换言之,它们是 1 x 1 x grid[i][j]
的长方体。
自顶向下的写法效率不高但容易理解
class Solution { public int maxIncreaseKeepingSkyline(int[][] grid) { int[] top = top(grid); int[] left = left(grid); return increase(top,left,grid); } public int[] top(int[][] grid){ int[] top = new int[grid.length]; int max = 0; for (int j = 0; j < grid.length; ++j) { for (int i = 0; i < grid.length; ++i) { if (max < grid[i][j]) { max = grid[i][j]; } } top[j] = max; max = 0; } return top; } public int[] left(int[][] grid){ int[] left = new int[grid.length]; int max = 0; for (int j = 0; j < grid.length; ++j){ for (int i = 0; i < grid.length; ++i){ if (max < grid[j][i]){ max = grid[j][i]; } } left[j] = max; max = 0; } return left; } public int increase(int[] top, int[] left, int[][]grid){ int res = 0; int max = 0; for (int j = 0; j < grid.length; ++j){ for (int i = 0; i < grid.length; ++i){ int min = Math.min(left[j],top[i]); res += min - grid[j][i]; } left[j] = max; max = 0; } return res; }}
省了一次循环♻️
class Solution { public static int maxIncreaseKeepingSkyline(int[][] grid) { int ans = 0; int len = grid.length; int [] rowMax = new int[len]; int [] columnMax = new int [len]; for (int i = 0; i < len; i++){ for (int j = 0; j < len; j++) { if(grid[i][j] > rowMax[i]) rowMax[i] = grid[i][j]; if(grid[i][j] > columnMax[j]) columnMax[j] = grid[i][j]; } } for (int i = 0; i < len; i++) { for (int j = 0; j < len; j++) { ans += Math.min(rowMax[i],columnMax[j]) - grid[i][j]; } } return ans; }}
]]>
输入一个链表,输出该链表中倒数第k
个节点。为了符合大多数人的习惯,本题从1开始计数,即链表的尾节点是倒数第1
个节点。例如,一个链表有6
个节点,从头节点开始,它们的值依次是1、2、3、4、5、6
。这个链表的倒数第3
个节点是值为4
的节点。
给定一个链表: 1->2->3->4->5
, 和 k = 2
.
返回链表 4->5
.
ArrayList
避免第二次循环(实际上可能比第二次循环还要慢😂)
List<ListNode> listNodes = new ArrayList<>(); while (head != null){ listNodes.add(head); head = head.next; } return listNodes.get(listNodes.size() - k);
快慢指针快指针先走K
步慢指针开始走利用快慢指针的间隔找到倒数的节点
class Solution { public ListNode getKthFromEnd(ListNode head, int k) { ListNode fast = head,slow = head; for (int i = 0; i < k; ++i){ fast = fast.next; } while (fast != null){ fast = fast.next; slow = slow.next; } return slow; }}
]]>
给定一个包含大写字母和小写字母的字符串,找到通过这些字母构造成的最长的回文串。
在构造过程中,请注意区分大小写。比如 "Aa"
不能当做一个回文字符串。
注意:
假设字符串的长度不会超过 1010。
示例 1:
输入:"abccccdd"
输出:7
解释:
我们可以构造的最长的回文串是"dccaccd"
, 它的长度是7
。
如图'A'~'a'
之间有58
个元素 利用数组计数器进行计数。ans += i % 2 == 0 ? i : --i;
如果String
中某个元素出现次数为奇数把它弱化为偶数最后比较ans
与String
的长度如果小于String
的长度就补一个奇数个数。
class Solution { public int longestPalindrome(String s) { int[] cnt = new int[58]; for(char c : s.toCharArray()){ cnt[c - 'A'] += 1; } int ans = 0; for(int i : cnt){ ans += i % 2 == 0 ? i : --i; } return ans < s.length() ? ++ans : ans; }}
]]>
给你一份『词汇表』(字符串数组) words
和一张『字母表』(字符串) chars
。
假如你可以用 chars
中的『字母』(字符)拼写出 words
中的某个『单词』(字符串),那么我们就认为你掌握了这个单词。
注意:每次拼写时,chars
中的每个字母都只能用一次。
返回词汇表 words
中你掌握的所有单词的 长度之和。
输入:words = ["cat","bt","hat","tree"], chars = "atach"
输出:6
解释:
可以形成字符串 "cat"
和 "hat"
,所以答案是 3 + 3 = 6
。
输入:words = ["hello","world","leetcode"], chars = "welldonehoneyr"
输出:10
解释:
可以形成字符串 "hello"
和 "world"
,所以答案是 5 + 5 = 10
。
提示:
1 <= words.length <= 1000
1 <= words[i].length, chars.length <= 100
Solution One
就是个ArrayCounter
public int countCharacters(String[] words, String chars) { int[] chars_count = count(chars); // 统计字母表的字母出现次数 int res = 0; for (String word : words) { int[] word_count = count(word); // 统计单词的字母出现次数 if (contains(chars_count, word_count)) { res += word.length(); } } return res;}// 检查字母表的字母出现次数是否覆盖单词的字母出现次数boolean contains(int[] chars_count, int[] word_count) { for (int i = 0; i < 26; i++) { if (chars_count[i] < word_count[i]) { return false; } } return true;}// 统计 26 个字母出现的次数int[] count(String word) { int[] counter = new int[26]; for (int i = 0; i < word.length(); i++) { char c = word.charAt(i); counter[c-'a']++; } return counter;}
Solution Two
MapCounter效率慢
class Solution { public int countCharacters(String[] words, String chars) { int res = 0; Map<Character,Integer> charsMap = countWords(chars); for (String s : words){ Map<Character,Integer> wordMap = countWords(s); if (isLegal(charsMap,wordMap)){ res += s.length(); } } return res; } private boolean isLegal(Map<Character, Integer> charsMap, Map<Character, Integer> wordMap) { for (Map.Entry<Character,Integer> wordEntry : wordMap.entrySet()){ if (!charsMap.containsKey(wordEntry.getKey())){ return false; }else if(!((wordEntry.getValue()) <=(charsMap.get(wordEntry.getKey())))){ return false; } } return true; } public Map<Character,Integer> countWords(String s){ Map<Character,Integer> charMap = new HashMap<>(); char[] wordChar = s.toCharArray(); for (char c : wordChar) { if (charMap.containsKey(c)) { charMap.put(c,charMap.get(c) + 1); }else { charMap.put(c,1); } } return charMap; }}
]]>
矩形以列表 [x1, y1, x2, y2]
的形式表示,其中 (x1, y1)
为左下角的坐标,(x2, y2)
是右上角的坐标。
如果相交的面积为正,则称两矩形重叠。需要明确的是,只在角或边接触的两个矩形不构成重叠。
给出两个矩形,判断它们是否重叠并返回结果。
输入:rec1 = [0,0,2,2], rec2 = [1,1,3,3]
输出:true
输入:rec1 = [0,0,1,1], rec2 = [1,0,2,1]
输出:false
说明:
两个矩形 rec1
和 rec2
都以含有四个整数的列表的形式给出。
矩形中的所有坐标都处于 -10^9
和 10^9
之间。
方法一:检查位置
思路
我们尝试分析在什么情况下,矩形 rec1
和 rec2
没有重叠。
想象一下,如果我们在平面中放置一个固定的矩形 rec2
,那么矩形 rec1
必须要出现在 rec2
的「四周」,也就是说,矩形 rec1
需要满足以下四种情况中的至少一种:
矩形 rec1
在矩形 rec2
的左侧;
矩形 rec1
在矩形 rec2
的右侧;
矩形 rec1
在矩形 rec2
的上方;
矩形 rec1
在矩形 rec2
的下方。
何为「左侧」?如果矩形 rec1
在矩形 rec2
的左侧,那就表示我们可以找到一条竖直的线(可以与矩形的边重合),使得矩形 rec1
和 rec2
被分在这条竖线的两侧。对于「右侧」、「上方」以及「下方」,它们的定义与「左侧」是类似的。
算法
我们将上述的四种情况翻译成代码。具体地,我们用 (rec[0], rec[1])
表示矩形的左下角,(rec[2], rec[3])
表示矩形的右上角,与题目描述一致。对于「左侧」,即矩形 rec1
在 x
轴上的最大值不能大于矩形 rec2
在 x 轴上最小值。对于「右侧」、「上方」以及「下方」同理。因此我们可以翻译成如下的代码:
左侧:rec1[2] <= rec2[0]
;
右侧:rec1[0] >= rec2[2]
;
上方:rec1[1] >= rec2[3]
;
下方:rec1[3] <= rec2[1]
。
class Solution { public boolean isRectangleOverlap(int[] rec1, int[] rec2) { return!((rec1[2] <= rec2[0]) || (rec1[0] >= rec2[2]) || (rec1[3] <= rec2[1]) || (rec1[1] >= rec2[3])); }}
方法二:检查区域
思路
如果两个矩形重叠,那么它们重叠的区域一定也是一个矩形,那么这代表了两个矩形与 xx
轴平行的边(水平边)投影到 xx
轴上时会有交集,与 yy
轴平行的边(竖直边)投影到 yy
轴上时也会有交集。因此,我们可以将问题看作一维线段是否有交集的问题。
算法
矩形 rec1
和 rec2
的水平边投影到 xx
轴上的线段分别为 (rec1[0], rec1[2]) 和 (rec2[0], rec2[2])
。根据数学知识我们可以知道,当 min(rec1[2], rec2[2]) > max(rec1[0], rec2[0])
时,这两条线段有交集。对于矩形 rec1
和 rec2
的竖直边投影到 yy
轴上的线段,同理可以得到,当 min(rec1[3], rec2[3]) > max(rec1[1], rec2[1])
时,这两条线段有交集。
class Solution { public boolean isRectangleOverlap(int[] rec1, int[] rec2) { return (Math.min(rec1[2], rec2[2]) > Math.max(rec1[0], rec2[0]) && Math.min(rec1[3], rec2[3]) > Math.max(rec1[1], rec2[1])); }}
]]>
给你两棵二叉树,原始树 original
和克隆树 cloned
,以及一个位于原始树 original
中的目标节点 target
。
其中,克隆树 cloned
是原始树 original
的一个 副本 。
请找出在树 cloned
中,与 target
相同 的节点,并返回对该节点的引用(在 C/C++
等有指针的语言中返回 节点指针,其他语言返回节点本身)。
你 不能 对两棵二叉树,以及 target
节点进行更改。
只能 返回对克隆树 cloned
中已有的节点的引用。
进阶:如果树中允许出现值相同的节点,你将如何解答?
输入: tree = [7,4,3,null,null,6,19], target = 3
输出: 3
解释: 上图画出了树 original 和 cloned。target 节点在树 original 中,用绿色标记。答案是树 cloned 中的黄颜色的节点(其他示例类似)。
输入: tree = [7], target = 7
输出: 7
输入: tree = [8,null,6,null,5,null,4,null,3,null,2,null,1], target = 4
输出: 4
输入: tree = [1,2,3,4,5,6,7,8,9,10], target = 5
输出: 5
输入: tree = [1,2,null,3], target = 2
输出: 2
树中节点的数量范围为 [1, 10^4]
。
同一棵树中,没有值相同的节点。target
节点是树 original
中的一个节点,并且不会是 null
。
二叉树前序递归遍历
/** * Definition for a binary tree node. * public class TreeNode { * int val; * TreeNode left; * TreeNode right; * TreeNode(int x) { val = x; } * } */class Solution { public final TreeNode getTargetCopy(final TreeNode original, final TreeNode cloned, final TreeNode target) { if(original == null){ return cloned; } if(original == target){ return cloned; } TreeNode res = getTargetCopy(original.left,cloned.left,target); if(res != null){ return res; } res = getTargetCopy(original.right,cloned.right,target); if(res != null){ return res; } return null; }}
]]>
括号。设计一种算法,打印n对括号的所有合法的(例如,开闭一一对应)组合。
说明:解集不能包含重复的子集。
例如,给出 n = 3,生成结果为:
[ "((()))", "(()())", "(())()", "()(())", "()()()"]
class Solution { public List<String> generateParenthesis(int n) { List<String> list = new ArrayList<>(); if(n == 0){ return list; } compute(list,"",0,0,n); return list; } private void compute(List<String> list,String res,int left,int right,int n){ if(res.length() == n * 2){ list.add(res); return; } if(left < n){ compute(list,res + "(", left + 1,right,n); } if(right < left){ compute(list,res + ")", left,right + 1,n); } }}
public void recur(int level, int param) { // terminator 递归终止条件 if (level > MAX_LEVEL) { // process result return; } // process current logic 递归处理逻辑 process(level, param); // drill down 下探下一层 recur( level: level + 1, newParam); // restore current status 清除递归状态 }
]]>
平面上有 n
个点,点的位置用整数坐标表示 points[i] = [xi, yi]
。请你计算访问所有这些点需要的最小时间(以秒为单位)。
你可以按照下面的规则在平面上移动:
每一秒沿水平或者竖直方向移动一个单位长度,或者跨过对角线(可以看作在一秒内向水平和竖直方向各移动一个单位长度)。
必须按照数组中出现的顺序来访问这些点。
输入:points = [[1,1],[3,4],[-1,0]]
输出:7
解释:一条最佳的访问路径是: [1,1] -> [2,2] -> [3,3] -> [3,4] -> [2,3] -> [1,2] -> [0,1] -> [-1,0]
从 [1,1]
到 [3,4]
需要 3
秒
从 [3,4]
到 [-1,0]
需要 4
秒
一共需要 7
秒
输入:points = [[3,2],[-2,2]]
输出:5
提示:
points.length == n
1 <= n <= 100
points[i].length == 2
-1000 <= points[i][0], points[i][1] <= 1000
class Solution { public int minTimeToVisitAllPoints(int[][] points) { int count = 0; for (int i = 1; i < points.length; ++i){ count += calcdeDis(points[ i - 1][0],points[ i - 1][1],points[i][0],points[i][1]); } return count; } public int calcdeDis(int x1,int y1,int x2,int y2){ return Math.max(Math.abs(x1 - x2),Math.abs(y1 - y2)); }}
对于平面上的两个点 x = (x0, x1)
和 y = (y0, y1)
,设它们横坐标距离之差为 dx = |x0 - y0|
,纵坐标距离之差为 dy = |x1 - y1|
,对于以下三种情况,我们可以分别计算出从 x
移动到 y
的最少次数:
dx < dy
:沿对角线移动 dx
次,再竖直移动 dy - dx
次,总计 dx + (dy - dx) = dy
次;
dx == dy
:沿对角线移动 dx
次;
dx > dy
:沿对角线移动 dy
次,再水平移动 dx - dy
次,总计 dy + (dx - dy) = dx
次。
可以发现,对于任意一种情况,从 x
移动到 y
的最少次数为 dx
和 dy
中的较大值 max(dx, dy)
,这也被称作 x
和 y
之间的 切比雪夫距离。
由于题目要求,需要按照数组中出现的顺序来访问这些点。因此我们遍历整个数组,对于数组中的相邻两个点,计算出它们的切比雪夫距离,所有的距离之和即为答案。
]]>编写一个函数,不用临时变量,直接交换numbers = [a, b]
中a
与b
的值。
输入: numbers = [1,2]
输出: [2,1]
提示:
numbers.length == 2
简单的加法交换
class Solution { public int[] swapNumbers(int[] numbers) { numbers[0] += numbers[1]; numbers[1] = numbers[0] - numbers[1]; numbers[0] = numbers[0] - numbers[1]; return numbers; }}
异或运算
class Solution { public int[] swapNumbers(int[] numbers) { numbers[0] ^= numbers[1]; numbers[1] ^= numbers[0]; numbers[0] ^= numbers[1]; return numbers; }}
]]>
给你一个单链表的引用结点 head
。链表中每个结点的值不是 0
就是 1
。已知此链表是一个整数数字的二进制表示形式。
请你返回该链表所表示数字的 十进制值 。
输入:head = [1,0,1]
输出:5
解释:二进制数 (101)
转化为十进制数 (5)
输入:head = [0]
输出:0
输入:head = [1]
输出:1
输入:head = [1,0,0,1,0,0,1,1,1,0,0,0,0,0,0]
输出:18880
输入:head = [0,0]
输出:0
链表不为空。
链表的结点总数不超过 30
。
每个结点的值不是 0
就是 1
。
/** * Definition for singly-linked list. * public class ListNode { * int val; * ListNode next; * ListNode(int x) { val = x; } * } */class Solution { public int getDecimalValue(ListNode head) { StringBuilder stringBuilder = new StringBuilder(); while (head !=){ stringBuilder.append(head.val); head = head.next; } return Integer.parseInt(stringBuilder.toString(),2); }}
/** * Definition for singly-linked list. * public class ListNode { * int val; * ListNode next; * ListNode(int x) { val = x; } * } */class Solution { public int getDecimalValue(ListNode head) { ListNode cur = head; int ans = 0; while (cur != null) { ans <<= 1; ans += cur.val; cur = cur.next; } return ans; }}
]]>
TinyURL
是一种URL简化服务, 比如:当你输入一个URL https://leetcode.com/problems/design-tinyurl
时,它将返回一个简化的URL
http://tinyurl.com/4e9iAk
.
要求:设计一个 TinyURL
的加密 encode
和解密 decode
的方法。你的加密和解密算法如何设计和运作是没有限制的,你只需要保证一个URL
可以被加密成一个TinyURL
,并且这个TinyURL
可以用解密方法恢复成原本的URL
。
public class Codec { // Encodes a URL to a shortened URL. public String encode(String longUrl) { char[] en = longUrl.toCharArray(); for (int i = 0; i < en.length; ++i){ en[i] ^= 31; } return String.copyValueOf(en); } // Decodes a shortened URL to its original URL. public String decode(String shortUrl) { char[] de = shortUrl.toCharArray(); for (int i = 0; i < de.length; ++i){ de[i] ^= 31; } return String.copyValueOf(de); }}
class Solution { // Encodes a URL to a shortened URL. public String encode(String longUrl) { char[] en = longUrl.toCharArray(); for (int i = 0; i < en.length; ++i){ en[i] = (char) (en[i] + 1); } return String.copyValueOf(en); } // Decodes a shortened URL to its original URL. public String decode(String shortUrl) { char[] de = shortUrl.toCharArray(); for (int i = 0; i < de.length; ++i){ de[i] = (char) (de[i] - 1); } return String.copyValueOf(de); }}
]]>
给定一个大小为 n
的数组,找到其中的多数元素。多数元素是指在数组中出现次数大于 ⌊ n/2 ⌋
的元素。
你可以假设数组是非空的,并且给定的数组总是存在多数元素。
输入: [3,2,3]
输出: 3
输入: [2,2,1,1,1,2,2]
输出: 2
HashMap计数器
class Solution { public int majorityElement(int[] nums) { int len = nums.length / 2; Map<Integer,Integer> map = new HashMap<>(); for (int num : nums) { if (map.containsKey(num)) { map.put(num, map.get(num) + 1); } else { map.put(num, 1); } } for(Map.Entry<Integer,Integer> entry : map.entrySet()){ if (entry.getValue() > len){ len = entry.getKey(); break; } } return len; }}
Java Arrays.sort排序API
class Solution { public int majorityElement(int[] nums) { Arrays.sort(nums); return nums[nums.length >> 1]; }}
候选人(cand_num
)初始化为nums[0]
,票数count
初始化为1
。
当遇到与cand_num
相同的数,则票数count = count + 1
,否则票数count = count - 1
。
当票数count
为0
时,更换候选人,并将票数count
重置为1
。
遍历完数组后,cand_num
即为最终答案。
为何这行得通呢?
投票法是遇到相同的则票数 + 1
,遇到不同的则票数 - 1
。
且“多数元素”的个数> ⌊ n/2 ⌋
,其余元素的个数总和<= ⌊ n/2 ⌋
。
因此“多数元素”的个数 -
其余元素的个数总和 肯定 >= 1
。
无论数组是1 2 1 2 1
,亦或是1 2 2 1 1
,总能得到正确的候选人。
class Solution { public int majorityElement(int[] nums) { int cand_num = nums[0], count = 1; for (int i = 1; i < nums.length; ++i) { if (cand_num == nums[i]) ++count; else { if (--count == 0) { cand_num = nums[i]; count = 1; } } } return cand_num; }}
]]>