刷题遇到的小知识点,在这里做个笔记,主要是记录想法、理解、一些牛逼的操作、没见过的算法、数据结构。
看到一篇博客,感触良多,适合正在努力学习算法的你:算法这一站是新的起点
多看,多练
67题,需要根据输入的字符串a、b中长度较短的进行循环,所以我的思路是,首先要判断两个字符串的长度,再进行调换……
public String addBinary(String a, String b) {
int len1 = a.length();
int len2 = b.length();
if(len1 < len2) return addBinary(b, a); //这里是重点,码住!!
/*
后面是其它操作,省略……
*/
}
适用的地方:在方法开始的位置,递归的时候前面没有做太多的操作,不然得不偿失
(题号:LCP2)
a + 1 / b 必定是最简分数,所以不用求GCD(最大公约数,还有一个词叫LCM是最小公倍数)了。 (前提:a是整数,b是一个最简分数) 因为b是最简分数,所以 1 / b肯定也是一个最简分数,加上一个整数仍然是最简分数:(ab + 1)/ b = a …… 1
对于求最大公约数的方法,有辗转相除法和更相减损法
辗转相除法:两个数相除取余,然后用被除数与余数相除求余,直到余数为零,此时的被除数即为最大公约数
(185题)一个不错的思路:比如说取每个部门中工资前三高的员工(limit用不了),自连接,条件是工资比我高的员工,然后判断工资比我高的员工(注意相同工资的去重)的个数是不是小于3,是的话说明我就是工资前三高 的员工
SET 字段=CASE ... END
CASE
WHEN 字段与某个值比较 THEN '解释数据1'
WHEN 字段与某个值比较可以使用聚合函数 THEN '解释数据2'
ELSE '解释数据3'
END
CASE 字段名
WHEN '值1' THEN '解释数据1'
WHEN '值2' THEN '解释数据2'
ELSE '解释数据3'
END
id % 2 = 0
,而在其它地方要用聚合函数,如MOD(id, 2) = 0
IF(判断条件, "true条件为true时", "条件为false时")
哈希表是最典型的时间换空间,对于一个数组,可以使用哈希表,将数组中的内容当做索引(key),数组下标当做值,当要检索数组中是否存在某个值时,速度比数组快,一个简单的应用:在一个元素不重复的数组中找两个数的和是0(可以是任何数,为了方便假定是0),可以先把数组元素存入到散列表中,在遍历数组时,在散列表中查找是否存在(0-arr[i]
)的元素。
(第442题:题目大致的意思,一个数组中有些元素出现两次而其他元素出现一次,找到所有出现两次的元素,限定条件:1 ≤ a[i] ≤ n (n为数组长度))
我没有注意到这个限定条件,直接就是遍历一遍数组,把每个元素存入到散列表中,如果表中已经存在该值了,就把这个值返回,这个思路的时间复杂度O(n),空间复杂度也是O(n)。当打开评论的时候,真的是另一个新天地,大佬们的代码在时间复杂度没变的同时居然没有使用额外的空间!!
大佬们的思路:因为限定条件的存在,所以可以将数组中的元素不重复的散列在输入数组的范围中,然后再通过正负来标记一个数是否出现过。
大写变小写、小写变大写:字符 ^= 32
大写变小写、小写变小写:字符 |= 32
小写变大写、大写变大写:字符 &= -33
a ^ b ^ c <=> a ^ c ^ b
0 ^ n => n
n ^ n => 0
第260题,136题的升级版,一数数组中只出现一次的数变为了两个。这时候的思路:先整体异或一遍,求出两个只出现一次的数 x,y 的异或结果xor,根据 xor 二进制位上为 1 的位(比如说最后一个为 1 的位),将数组分成两部分,x,y 刚好被分别分到了数组的两个部分中,然后对两部分的数组分别进行异或运算,就可以求出两个数
一段字符串,左往中间走加1计数,从中间往右走减1计数,取最左边的数(计数为1)和最右边的数(计数为0),这时候两个数不统一(一个为0,一个为1),这时可以用下面的方法:一个先加1或者减1再判断,另一个先判断再加1或者减1
//这里两次判断均使用的是数字0,
for (int i = 0; i < inputs.length; i++) {
char currentChar = inputs[i];
if (currentChar == '(') {
//注意这里是先判断,再加1
if (count > 0) {
sb.append(currentChar);
}
count++;
} else {
//这里是先减1,再判断
count--;
if (count > 0) {
sb.append(currentChar);
}
}
}
遍历图和二叉数
计算二叉数和图的深度均可,计算二叉数的深度目前为止个人觉得使用dfs又顺手一点
注意:有的和路径相关的,求最佳啥的,是使用 dp 来做的,要具体分析
使用时要考虑原来的值到底变不变,先加1 还是后加1,
i+1不会改变原来的值。
i++和++i都会改变原来的值,单独使用时,都可以。但是和其它函数一起使用时,就要注意了,
搜索值是数组元素,从0
开始计数,得到搜索值的索引值;
搜索值不是数组元素,且在数组范围内,从1
开始计数,得“ - 插入点索引值”;
搜索值不是数组元素,且大于数组内元素,索引值为 – (length + 1);
搜索值不是数组元素,且小于数组内元素,索引值为 – 1。
总之:如果搜不到,则插入点为 - ( 索引值 + 1 )
//LIS代码片段
for (int num : nums) {
int i = Arrays.binarySearch(dp, 0, len, num);
if (i < 0) {
i = -(i + 1);
}
dp[i] = num;
if (i == len) {
len++;
}
}
int arr = {......};
int lo = 0, hi = arr.length;
while(lo < hi) {
//这里也可以写成 /2,因为只要是 2 的方幂,Java 的编译器都会转成位运算去计算
//mid = low + (high - low) >> 1 这样写更稳妥一些,写成下面的形式可能出现int溢出
int mid = (hi + lo) >>> 1;
if(arr[mid] < x)
lo = mid + 1;
else
hi = mid;
}
return arr[lo];
893题,对于一个没有顺序、小写字母、长度较短的字符串,都可以用这种方式处理,来统计每个字母出现次数是否相同
int[] primes = new int[]{2,3,5,7,11,13,17,19,23,29,31,37,41,43,47,53,59,61,67,71,73,79,83,89,97,101};
Set<String> set = new HashSet<>();
for(String a : A){
long odd = 1;
long even = 1;
char[] cs = a.toCharArray();
for(int i=0;i<cs.length;i++){
if(i % 2 == 0){
even *= primes[cs[i]-'a'];
}else{
odd *= primes[cs[i]-'a'];
}
}
set.add(odd+"_"+even);
}
(1)使用Set集合来标记遍历过的节点,遍历过的加到Set中(在入队时加入),使用contains方法确定是否遍历过
(2)带层数的bfs:
//先创建一个队列,并将队列的起始节点加入到队列中
Queue<Integer> queue = new LinkedList<>();
queue.offer(id);
//使用Set集合来去除掉已经遍历过的节点
Set<Integer> visited = new HashSet<>();
visited.add(id);
//len为层数
int len = 0;
//bfs,如果要选取某一层的元素,则在这里加个条件:len < 层数
while (!queue.isEmpty()) {
//这个size为当前层的元素个数
int size = queue.size();
//将当前层的握有元素出队,并将下一层的所有元素入队
for (int i = 0; i < size; i++) {
Integer a = queue.poll();
for (int j = 0; j < friends[idd].length; j++) {
if (!visited.contains(friends[idd][j])) {
//如果该元素之前没有被遍历过的话就加入到队列和Set集合中
queue.add(friends[idd][j]);
visited.add(friends[idd][j]);
}
}
}
//层数加1
len++;
}
Knuth 洗牌算法的伪代码:
for(int i = n - 1; i >= 0 ; i -- )
swap(arr[i], arr[Math.random() * (i + 1)])
Inside-Out Algorithm
int i = 0;
int[] array = {......};
while(i < n){
int tmp = Math.random() * (i + 1);//随机生成一个从0到i(包含i)的数
swap(array[i], array[tmp]);//交换下标为i的数和下标为tmp的数
i++;//后移
}
Java.util.Collections
类下有一个静态的shuffle()
方法,可以对List
集合进行洗牌,如下:
static void shuffle(List list)
:使用默认随机源对列表进行置换,所有置换发生的可能性都是大致相等的。
static void shuffle(List list, Random rand)
:使用指定的随机源对指定列表进行置换,所有置换发生的可能性都是大致相等的,假定随机源是公平的。
注意:如果给定一个整型数组,用
Arrays.asList()
方法将其转化为一个集合类,有两种途径:
用
List
,用list=ArrayList(Arrays.asList(ia)) shuffle()
打乱不会改变底层数组的顺序。用
List
,然后用list=Arrays.aslist(ia) shuffle()
打乱会改变底层数组的顺序。
Knuth 洗牌算法:https://www.jianshu.com/p/4be78c20095e
三种洗牌算法shuffle:https://blog.csdn.net/qq_26399665/article/details/79831490
先列出dp方程,再根据dp方程来写程序,
基本上DP的题,能列出dp方程,程序也就写出来了,还有就是,要能想到这道题是用dp来做
一些经典的动态规化题,必刷:
- 70. 爬楼梯
- 121. 买卖股票的最佳时机
- 198. 打家劫舍
- 300. 最长上升子序列(LIS)
- 1143. 最长公共子序列(LCS)
- 72. 编辑距离
Catalan数的定义:令h(0)=1
,Catalan数满足递归式:h(n)= h(0)*h(n-1) + h(1)*h(n-2) + ... + h(n-1)h(0) (其中n>=0)
。该递推关系的解为:h(n) = C(2n-2,n-1)/n,n=1,2,3,...
(其中C(2n-2,n-1)表示2n-2个中取n-1个的组合数)。
卡特兰数的前几位分别是:规定h(0)=1,而h(1)=1,h(2)=2,h(3)=5,h(4)=14,h(5)=42,h(6)=132,h(7)=429,h(8)=1430,h(9)=4862,h(10)=16796,h(11)=58786,h(12)=208012,h(13)=742900,h(14)=2674440,h(15)=9694845。
常见的题型:
n个节点构成的二叉搜索树,有多少种可能(93题)?简单思路:左子树有0个节点,则右子树有n-1个节点,左子树有1个节点,则右子数有n-2个节点……以此类推,可以dp来做
n对括号有多少种合法匹配方式?考虑n对括号,相当于有2n个符号,n个左括号、n个右括号。动态规化的思想:可以设问题的解为dp(2n)。第0个符号肯定为左括号,与之匹配的右括号必须为第2i+1字符。因为如果是第2i个字符,那么第0个字符与第2i个字符间包含奇数个字符,而奇数个字符是无法构成匹配的。通过简单分析,可以得出如下的递推式 f(2i) = f(0)*f(2i-2) + f(2)*f(2i - 4) + ... + f(2i - 4)*f(2) + f(2i-2)*f(0)
。简单解释一下,f(0) * f(2n-2)表示第0个字符与第1个字符匹配为一个括号,以这个括号为准,剩下的括号被分成了这对括号里的0个字符,和这对括号外的2n-2个字符,然后对这两部分求解。f(2)*f(2n-4)表示第0个字符与第3个字符匹配,同时剩余字符分成两个部分,一部分为2个字符,另一部分为2n-4个字符。依次类推。
进出栈问题:一个栈(无穷大)的进栈序列为1,2,3,…,n,有多少个不同的出栈序列?和括号匹配问题相同,进栈看成左括号,出栈看成是右括号。
参考了这个,写的不错:Catalan数相关的算法问题
上周的周赛(2020.1.12),第三题(1319题)就是用并查集写的,可惜我太菜了,没有写出来,所以特地来学习一下
先看了别人的博客,对并查集有了一定的了解:
虽然是C语言写的,但是写的非常有趣,适合入门:超有爱的并查集~
i == arr[i]
i == arr[i]
,就是有几个树(连通分量)常用方法模版:
public int makeConnected(int n, int[][] connections) {
if (n - 1 > connections.length) {
return -1;
}
int[] arr = new int[n];
//初始化并查集
for (int i = 0; i < arr.length; i++) {
arr[i] = i;
}
//合并
for (int[] connection : connections) {
union(connection[0], connection[1]);
}
//计算连通分量的个数
int count = 0;
for (int i = 0; i < n; i++) {
if (parent[i] == i) {
count++;
}
}
}
//查找树的根,同时进行路径压缩
private int findRoot(int[] arr, int node) {
return arr[node] == node ? node : (arr[node] = findRoot(arr, arr[node]));
}
//合并两个树
private void union(int[] arr, int node1, int node2) {
int root1 = findRoot(node1);
int root2 = findRoot(node2);
if (root1 != root2) {
arr[root1] = root2;
}
}
一道读题就得读半天的题:你需要返回一个字符串,这个字符串就是你提交的代码本身。
关键点:
\n
",所以代码就写一行了char c = 34;
34表示的字符为双引号:""
",涉及字符串的双引号, 记得用ASCII码输出代替,不要用"
,你会陷入无限套娃的烦恼class Solution { public String q() { char c = 34; return s+c+s+c+';'+'}'; } static String s = "class Solution { public String q() { char c = 34; return s+c+s+c+';'+'}'; } static String s = ";}
减治、分治与变治
n&(n-1)
:将n
的二进制表示中的最低位的1
改为0
1e9+7(1000000007)
取模大数阶乘,大数的排列组合等,一般都要求将输出结果对1000000007
取模,主要有以下原因:
1000000007
是一个质数int32
位的最大值为2147483647
,所以对于int32
位来说1000000007
足够大int64
位的最大值为2^63-1
,对于1000000007
来说它的平方不会在int64
中溢出,因为(a∗b)%c=((a%c)∗(b%c))%c
,所以相乘时两边都对1000000007
取模,再保存在int64
里面不会溢出 。约瑟夫问题是个著名的问题:N
个人围成一圈,第一个人从1
开始报数,报M
的将被杀掉,下一个人接着从1
开始报。如此反复,最后剩下一个,求最后的胜利者。
递推公式:f(N, M) = (f(N−1, M) + M) % N
f(N,M)
表示,N个人报数,每报到M时杀掉那个人,最终胜利者的编号f(N−1,M)
表示,N-1个人报数,每报到M时杀掉那个人,最终胜利者的编号原理这篇博客讲的挺不错的:约瑟夫环——公式法(递推公式)
关键点:
- 问题1:假设我们已经知道11个人时,胜利者的下标位置为6。那下一轮10个人时,胜利者的下标位置为多少?
- 答:其实吧,第一轮删掉编号为3的人后,之后的人都往前面移动了3位,胜利这也往前移动了3位,所以他的下标位置由6变成3。
- 问题2:假设我们已经知道10个人时,胜利者的下标位置为3。那下一轮11个人时,胜利者的下标位置为多少?
- 答:这可以看错是上一个问题的逆过程,大家都往后移动3位,所以
f(11, 3) = f(10, 3) + 3
。不过有可能数组会越界,所以最后模上当前人数的个数,f(11, 3) =(f(10, 3) + 3)% 11
求两个数的最大公约数:
// 辗转相除法
private int gcd (int a, int b) {
return b == 0? a: gcd(b, a % b);
}
求多个数的最大公约数:(gcd的结合律)
gcd(a, b, c) = gcd(gcd(a, b), c)
字典树又名前缀树,Trie
树,是一种存储大量字符串的树形数据结构,相比于HashMap存储,在存储单词(和语种无关,任意语言都可以)的场景上,节省了大量的内存空间。
下图演示了一个保存了8个单词的字典树的结构,8个单词分别是:“A”, “to”, “tea”, “ted”, “ten”, “i”, “in”, “inn”。
怎么理解这颗树呢?你从根节点走到叶子节点,尝试走一下所有的路径。你会发现,每条从根节点到叶子节点的路径都构成了单词(有的不需要走到叶子节点也是单词,比如 “i
” 和 “in
”)。trie
树里的每个节点只需要保存当前的字符就可以了(当然你也可以额外记录别的信息,比如记录一下如果以当前节点结束是否构成单词)。
trid树的练习:208. 实现 Trie (前缀树)
trie
树的使用:
trie
树的进阶版,Merkle Patricia Tree
,他能够高效、安全地验证大型数据结构中的数据经常会遇到在二维数组中使用bfs时,要遍历四个方向或者八个方向,这时可以使用下面的方法避免写4个判断或者4个循环
//定义偏移数组
int[] dx = {0, 0, 1, -1}; //八个方向:{0, 0, 1, -1, 1, 1, -1, -1}
int[] dy = {1, -1, 0, 0}; //八个方向:{1, -1, 0, 0, 1, -1, -1, 1}
//在偏移数组中循环4(偏移数组的长度)次
for (int i = 0; i < 4; i++) {
//获取偏移之后的数组坐标
int newX = x + dx[i];
int newY = y + dy[i];
//越界检查与条件判断,m和n分别表示二维数组的大小为m*n
if (newX < 0 || newX >= m || newY < 0 || newY >= n) {
continue;
}
/*
执行其它操作
*/
}
单调栈就是比普通的栈多一个性质,即维护一个栈内元素单调递增或者递减。比如当前某个单调递减的栈的元素从栈底到栈顶分别是:[10, 9, 8, 3, 2]
,如果要入栈元素5
,需要先把 2
和 3
从栈中pop
出去,满足单调递减为止,即变成[10, 9, 8]
,然后再入栈5
,就是[10, 9, 8, 5]
。
相应的题:
- 42. 接雨水
- 84. 柱状图中最大的矩形
先沿对角线翻转,再沿水平线或者垂直线翻转,可以实现方阵顺时针或逆时针旋转90度。
不同的对角线和水平/垂直线搭配,旋转的方向不同:
树状数组(Fenwick Tree)是用数组来模拟树形结构,可以解决大部分基于区间上的更新以及求和问题。树状数组中修改和查询的复杂度都是O(logN)
。
数状数组的功能主要有下面两个:
update(i, v)
: 把序列 i
位置的数加上一个值 v
query(i)
: 查询序列 [1]
到 [i]
区间的和,即 i
位置的前缀和public class FenwickTree {
private int[] tree;
private int len;
public FenwickTree(int n) {
this.len = n;
tree = new int[n + 1];
}
/**
* 单点更新:将 index 这个位置 + delta
*
* @param i
* @param delta
*/
public void update(int i, int delta) {
// 从下到上,最多到 size,可以等于 size
while (i <= this.len) {
tree[i] += delta;
i += lowbit(i);
}
}
// 区间查询:查询小于等于 tree[index] 的元素个数
// 查询的语义是「前缀和」
public int query(int i) {
// 从右到左查询
int sum = 0;
while (i > 0) {
sum += tree[i];
i -= lowbit(i);
}
return sum;
}
//求 x 的二进制中从最低位到高位连续零的长度(称为lowbit)
public int lowbit(int x) {
return x & (-x);
}
}
用到的地方:
参考:
- 树状数组详解
- 力扣题解:树状数组的详细分析(含实例)
map.getOrDefault(key, defaultValue)
:如果没有key,则取出defaultValue,否则取出key对应的value值map.entrySet()
生成Set集合,遍历集合将键值对存放到优先队列PriorityQueue中,并指定Comparatorjavafx.util.Pair对象
:指一对
键值对,只能存放一对,可以用于方法返回两个参数的情况,与map中的Entry类似,方法也相同(getKey(),getValue()),不过比map更轻量。使用时导包:javafx.util.Pair
stringBuilder.setLength(0);
持续更新中……