文章目录
注意点
- 判断数据的大小,然后决定使用正常题解或者其他方式当参数为对象时,需要先判断参数是否为空;
- 对于给定的参数,考虑参数最小时的情况,如长度为1或者0
- 使用到节点属性前,先判断节点是否为空;
- 在循环中使用if分支,要考虑分支体内是否跳出本次循环
- 使用循环时,要考虑循环的终止条件,当利用循环从集合中取出特定元素时,取值操作要在循环体内和循环体外定义两次,保证判断条件的不断更新;
- 使用循环时,要特别注意循环的终止条件
- 当对一个字符串进行倍数复制时,要额外定义一个临时变量存储当前值,然后再对当前对象执行自增操作,因为每次操作,当前变量都在变化,不能
str=+str
; - 倍数复制时,当前已占据一;
- 需要对固定结构的序列做增删操作时,可以定义一额外的动态结构来保存中间结果,不直接在元结构上改变
- 对某一序列执行相同的操作,可以将该序列定义为数组
- 在循环中(i自增判断),结束条件不能使用动态链表或者栈的大小,因为在循环体内更改链表的大小,会造成循环条件的动态改变
- 仅对相邻的两个元素进行操作,可以考虑使用栈结构,取出数据后,要注意数据的再次保存
- 需要使用字典序时,可以将两字符串拼接完成后,用
compareTo
方法进行比较 - 循环条件中只使用范围,不使用具体指(存在击穿)
- 集合中可以存储空值,并且该空值可以被取出
基础
- 二叉树的层次遍历
- 使用队列存储层层次上的节点,然后取出遍历,循环,条件为队列非空,提前将根节点加入队列中
- 使用两个队列,额外队列中仅存储当前层次的节点,当主队列为空时,将层次队列中的值添加到主队列
- 针对每一层,可以在取数前计算其节点数count,然后取出count个数,进行下一层次遍历,可以单独得到每一层次的节点
Queue<TreeNode> q=new ArrayDeque();
q.add(root);
while (!q.isEmpty()){
//count计数,仅记录当前层次
int count=q.size();
List<Integer> item=new LinkedList<>();
while(count>0){
TreeNode treeNode=q.poll();
item.add(treeNode.val);
if(treeNode.left!=null){
q.add(treeNode.left);
}
if(treeNode.right!=null){
q.add(treeNode.right);
}
count--;
}
res.add(item);
}
return res;
- 使用队列模拟栈
1. 使用两个队列,都用于存值,其中必有一个保持为空
2. 将数据插入空的队列中,然后将另一个存有数据的队列值取出,存入该队列,此时,另一队列为空,本队列非空(每插入一个数据,就要执行一次反转操作,如3个数据,要反转好多次)
3. 取值时,从存有值得队列中直接取值即可
4. 插入比较耗时 - 使用栈来模拟队列
1. 使用两个栈来模拟队列,一个栈作为输入,另一个栈作为输出
2. 存数据时存入输入栈,
3. 取数据值从输出栈取数据,当输出栈为空时,将输入栈中所有元素存入输出栈,再进 行取值操作 - 二叉树的遍历,迭代,递归实现
递归
- 以根节点为参数,返回值为空
- 当根非空时,以左右节点为根节点进行分解
- 左右节点递归顺序以及根节点值读取顺序以遍历方式来决定
public void pre(TreeNode root) {
if(root==null){
return;
}
//其位置决定其为前序,中序或后序
System.out.println(root.val);
pre(root.left);
pre(root.right);
}
迭代1(改变原结构)
- (后序)使用栈来存储节点,先存根节点,循环,栈非空
- 存储左右节点时,保存一份左右节点的值,然后再栈中现存右节点,再存左节点,存储后,断开与根节点的联系
- 判断节点左右都为空时,取出存储值,(会按照左,右,根的顺序执行)
public List<Integer> postorderTraversal(TreeNode root) {
List<Integer> list = new ArrayList<>();
Stack<TreeNode> stack = new Stack<>();
if (root == null) {
return list;
}
stack.push(root);
while (!stack.isEmpty()) {
TreeNode r = stack.peek();
TreeNode left=r.left;
TreeNode right= r.right;
if (r.right != null) {
stack.push(r.right);
r.right=null;
}
if (r.left != null) {
stack.push(r.left);
r.left=null;
}
if(left==null&&right==null){
list.add(stack.pop().val);
}
}
return list;
}
迭代2(不改变原结构)
- 以左节点,不断遍历,直至左节点为空,此时,回退到父节点(回退后不能再重新遍历左节点),先左后右
- 左节点为空后,若右节点非空,则存入右节点,重新循环;否则,取出当前节点值,并从栈中弹出,回退到上一节点
- 回退到上一节点后,需要判断其左右子树是否都遍历过了,加上上一判断条件,故记录上一次被弹出节点,作为判断右子树是否被访问依据;对于左子树,在遍历至空后,该变当前值为null,对右子树判断前,再次从栈中取值(左子树循环的条件中,值不能直接从栈中取值,使用其他变量)
public List<Integer> postorderTraversal(TreeNode root) {
List<Integer> list = new ArrayList<>();
Stack<TreeNode> stack = new Stack<>();
if (root == null) {
return list;
}
stack.push(root);
TreeNode l=stack.peek().left;
TreeNode last=null;
TreeNode r=null;
while (!stack.isEmpty()) {
while(l!=null){
stack.push(l);
l=stack.peek().left;
}
r=stack.peek();
if(r.right!=null&&r.right!=last){
stack.push(r.right);
l=stack.peek().left;
}else{
TreeNode c=stack.pop();
list.add(c.val);
l=null;
last=c;
}
}
return list;
}
迭代3(使用前序)
- 前序迭代中,每次存储根节点的值(先存右节点,再存左节点),每次取值时,为从栈弹出的根节点,为根左右,
- 改变前序存储顺序,先存左,再存右,则下次循环取出节点为右节点,序列变为根右左,将结果序列反转,则变为后序
//前序遍历
public List<Integer> preorderTraversal(TreeNode root) {
if (root == null) {
return null;
}
List<Integer> list = new ArrayList<Integer>();
Stack<TreeNode> s = new Stack<TreeNode>();
s.push(root);
while (!s.isEmpty()) {
TreeNode node = s.pop();
list.add(node.val);
if (node.right != null) {
s.push(node.right);
}
if (node.left != null) {
s.push(node.left);
}
}
return list;
}
- 根据前序,中序构造二叉树
- 使用链表重新封装中序数组,或使用开始和结束下标的方式
- 还可以使用map封装中序队列,提高查询速度
- 以前序数组中元素为根节点,分割中序数组为左右部分,然后进行递归操作,递归返回分界点
- 将左右子树看成一个单独的树来处理,递归的终结位置为链表长度为1或者为null,直接构造该节点对象,然后返回
- 根据前序顺序,先处理左树,再处理右树,划分的左右子树中,不要包括根节点,故右子树的划分初始值要index位置加1;
- 参数中传入前序数组以及相应的下标,处理左子树时,下标为参数中的根下标+1,右子树传入根下标为根下标+中序左链表的长度+1;
- 可以根据以根节点分割的中序左右子树的链表得到左右子树的长度
public TreeNode reConstructBinaryTree(int[] pre, int[] in) {
List<Integer> li = new ArrayList<>(in.length);
for (int i : in) {
li.add(i);
}
return help(pre, 0, li);
}
public static TreeNode help(int[] pre, int i, List<Integer> li) {
if (li == null || li.size() == 0) {
return null;
}
if (li.size() == 1) {
return new TreeNode(li.get(0));
}
List<Integer> left = li.subList(0, li.indexOf(pre[i]));
List<Integer> right = li.subList(li.indexOf(pre[i])+1, li.size());
TreeNode root = new TreeNode(pre[i]);
root.left = help(pre, i +1, left);
root.right = help(pre, i + left.size() + 1, right);
return root;
}
- 根据中序和后序构造二叉树
- 处理时,与上相反,先处理右树,再处理左树
- 以后序序列为主,反向进行,,末尾为根元素
- 左树下标为 根-右树长度-1
public TreeNode buildTree( int[] inorder,int[] preorder) {
if (preorder.length == 0)
return null;
List<Integer> li = new ArrayList<>(inorder.length);
for (int i : inorder)
li.add(i);
return buildTree(preorder.length-1, li, preorder);
}
public TreeNode buildTree(int r, List<Integer> order, int[] post ) {
if (r <0)
return null;
int rootIndex = order.indexOf(post[r]);
if (rootIndex == -1) {
return null;
}
TreeNode root = new TreeNode(post[r]);
List<Integer> left = order.subList(0, rootIndex);
List<Integer> right = order.subList(rootIndex + 1, order.size());
root.right = buildTree(r-1, right, post);
root.left= buildTree(r - right.size()-1, left, post);
return root;
}
- hashset,treeset的实现以及原理
- 红黑树在map中的使用
技巧
集合操作
list.remove()
方法直接传入数字,会按照删除下标的方式来删除,传入(integer)2
时,会按照删除对象的方式来删除- int[]类型的数组不能直接转换成list,只有引用类型的数组可以转换为list
list.subList()
并不生成新的链表,引用的仍为原对象,且增改均会反映到源对象上
排序
- 排序中,比较器可以使用方法体中的其他对象,并非一定在列表中的对象
- 键值对的排序,可以在比较器中引用该map,如
PriorityQueue<Integer> heap = new PriorityQueue<Integer>((n1, n2) -> count.get(n1) - count.get(n2));
,count为map,往heap存值时,仅存其key即可
排列组合
- 可以使用01二进制位数来表示序列中数据的排列组合,1左移n位表示n个数的排列,0~ n 中每一个数右移0~ n次,判断对应的每一位数是否应该存在
1<<n(n+1位)
表示n个数据的排列,其中,包含空(0),在0~n的数值中,n值不可达,表示n位数据的排列组合,最大值为n位全1- 判断相应位上是否为1,使用右移位,与1相与
(i>>j)&1
,j为位数,判断第1位(从一开始)时,j应该为0,故j的范围也是0~n,n不可达 - 也可以使用回溯法求解
位运算
^
异或,相同位0,不同为1,一个数,和其本身异或的结果为0,与0异或的结果为其本身- 异或可以用来求解多对重复值中仅有一个不同的情况
|
按位或,只要有一个为1,则为1,可以用来设定某个数的某一位为1;&
按位与,两个都为1,结果才为1,可以用来取某些特定位上的值,比如设定本身某些位为1,然后与其他数按位与,得到另一个数对应二进制位上的值~
取反,a & (-a)
,可以得到a的最低非零位,负数是将证书取反加一,结果中,除a最低非零以外,其余都为0- 左移相当于乘以2,右移相当于除2(奇数为减一除2)
- 判断对应的j位是否为1
int b = 1 << j; int c = i & b;
- 使j位为0
int s2 = ~(1 <<j);s2 = i & s2;
- 判断字符串是否包含另一个字符串时(多次不通知判断),可以将字符串转换为相应的位数,然后,进行按位与操作,若不包含,则值为零(转换时要注意相同字母重复添加)
- 给数据添加不同位时,可使用按位或运算,可以避免相同为上的重复添加
- 可以按位与判断不同位上的值,但相应的判断结果也应该是 2 n 2^n 2n,并非总是为1
- int位运算时,可以转换为long ,防止溢出
特性数
- 众数是出现次数1/2频度以上,那么若现将数组排序,中位数必为众数!
- 一个数+1时,总会有这么一个规律“某一位后的数字,全部被置为相反数”,故某个范围内的每个数按位与时,只需要求出开始值与结束值得相同前缀,若不存在,则为0
- 对于其他数重复,仅有两个不重复的数,先使其异或,得到结果中必定某一位为一,使用
x=a & (-a)
得到异或结果中含一的最低为,根据该位,将原数组分为两部分,则每一部分中为仅含一个不同项,其他都是重复项,(对于重复项,在该位上,要么都为1,要么都为0)通过按位与结果是否为得到的仅含一的值相同(nums[i] & x) == x
来划分数组,
题型
二叉树
- 前序验证二叉树,其中空节点为叶子节点
解: 使用栈保存节点,利用栈的回退特性,遇到要空节点时,判断站定是否为空,若为空,则将栈顶两元素弹出,并存入# 代替,若不为空,则存入(此时不可判断最终结果);进行下一次判断;至所有结果遍历完成,最终结果中,若仅剩余一个#,则正确;依次剪短二叉树的分支,并使用空来代替该分支
前n个某种特性的数据序列
- 如求前n个质数等,将其置于堆中,使其自然排序,而后,取出堆顶最小值,根据某种特性再进行拓展;使用堆得每次插入和弹出都会再次排序的特性,快速构造出某序列,且其经过排序,但是,插入时,要考虑生成值得重复问题,且已插入堆中的值,即使未使用,也不可丢弃,否则,会使数据不完整
- 根据primes列表生成新的数据
- 可以使用多指针,记录结果数据,使primes中的每一项在相乘时对应结果数据中的不同项,如primes为2,3,5;初始为1,则primes中每一项对应结果中的第一项,而后,值2对应的索引加一,另外两个不变
- 在生成数据时,存在重复计算,每次生成固定长度的结果集(primes的长度),从中取最小值,取出最小值后,判断最小值对应primes中的哪一项,对应项在res中的索引加一
- 当生成的数据中存在重复值时,则重复值对应的结果索引都加一
- 使用多指针,而后合并序列,得到最优结果,手动处理排序结果而非利用堆来处理
分割连续值
- (659)判断连续值时,需要考虑存在数组中存在重复值,使用hashmap保存重复值map,而后,根据该map作为判断
- 遍历原数组,因为其中有重复项,遍历过程中即可判断能否可行,根据map中的value判断该值是否已经被完全使用,构造endmap,存放序列的末尾,因为重复项,所以相同的序列可能有多个
- 遍历时。先判断是否已经被使用完毕,通过map判断,而后,判断能否追加到之前的序列中,需要更改endmap,最后,判断能否生成新的序列,查找并更改map即可,在此步骤中,若map值为0,则移除该键;若都不可行,则不能生成连续序列
序列中仅一个不存在
1.若重复项为2,则使用异或,得到唯一不同项
2. (137)若为多个重复项,则定义32位数组,存储每个数每一位相加的结果,然后对每位判断能否被整除
第k大数
- 使用堆维护前k大的数,遍历完成后弹出
n数和问题
- (两数和)多次遍历,取值
- 使用map结构,遍历一次,另一个值从map 中直接寻找
- (三数和)三重嵌套遍历
- 两重遍历,最后一层同过map来查找
- 排序,一层遍历,后两个数从数组左右依次增减判断(
o
(
n
2
)
o(n^2)
o(n2))
- 当需要去除重复时,注意,i位置与i+1相同时,略过i+1位置因为i+1匹配的项必定在Ide结果中存在
- 当j和k有重复时,直接略过某一个相同元素即可;但要注意其范围仅为i–n,并非从零开始,否则,会有重复;同时,需要注意全部都为零的情况,故是j不在i+1位置时,开始与之前比较,k不在末尾位置时,与之后比较
- for循环中,continue操作不需要i++;
- while循环中,使用if分支时要注意结束条件
public List<List<Integer>> threeSum(int[] nums) {
List<List<Integer>> res = new LinkedList<>();
Arrays.sort(nums);
for (int i = 0; i < nums.length - 2; i++) {
//i位置去重
if(i!=0&&nums[i]==nums[i-1]){
continue;
}
int j = i + 1;
int k = nums.length - 1;
while (j < k) {
// //j,k位置去重
if (j!=i+1&&nums[j] == nums[j - 1]) {
j++;
continue;
}
if(k!=nums.length-1&&nums[k]==nums[k+1]){
k--;
continue;
}
if (nums[j] + nums[k] > -nums[i]) {
k--;
} else if (nums[j] + nums[k] < -nums[i]) {
j++;
} else if (nums[j] + nums[k] == -nums[i]) {
List<Integer> li = new ArrayList<>(3);
li.add(nums[i]);
li.add(nums[j]);
li.add(nums[k]);
j++;
res.add(li);
}
}
}
return res;
}
- (四数和) 四重嵌套遍历
- 三重遍历,最后map
- 排序,双重遍历,最后两个数通过数组左右判断查找
滑动窗口
- Java 中优先队列并不可删除指定元素
- 暴力排序破解
- 使用双端队列,队列中存储下标,且队列中值保持降序,当新值插入时,若队尾小于该元素,则队尾弹出
- 使用下标来计算队首下标何时弹出,当下标等于i-k时,队首弹出,弹出后,下个元素仍为最大值
- 共产生n-k+1个元素
特性
- (134) 判断多个加油站,给定加油量以及消耗量,判断能否走完全程,起步量为0
- 当从i走到k时,油量消耗完,则意味着无论从i到k的那一站出发,都不可能走完全程,因为在这段范围内,到达每个站时,其存量是
>=0
的,故下一次应从k+1位置开始 - 仅需要一次遍历,当在k+1位置时,0到k是可以走通,当遍历完成时,说明k+1到n是可以走通的,故k+1到k+1是可以走通(存在唯一解)
- 通过一次遍历,判断从总量上判断能否走通,同时,在遍历过程中,假定起始点,定义余量,当余量为负值时,重置余量且更改起始点,至遍历完成
- 遍历完成后,从总量判断是否存在可行点,若存在,则可使用上述过程推断,得到起始点,若不存在,即使余量为正,也不存在可通过
- 当从i走到k时,油量消耗完,则意味着无论从i到k的那一站出发,都不可能走完全程,因为在这段范围内,到达每个站时,其存量是
- (767)字符串重构,不相邻
- 使用map存储字符串数量,然后每次去数量最多的两个进行排列,而后数量减一,重新排列
- 存入堆时,要先判断数量是否大于字母总数量的一半(考虑奇偶
counts[i] <= (S.length() + 1) / 2
),若大于,则不可行 - 使用最大堆进行排列,每次取出两个值,左右排列,重复取出·
size>1
,循环中取两次值 - 最终若仅剩一个值,则添加到最后,若为0,则直接输出
概念
字符串
- 处理字符串中的每一项时,使用char来进行处理
- 对于字符串中的数值,转换为char后可以
if (c >= '0' && c <= '9') { num = num * 10 + c - '0';}
来进行处理 - 字符串的反转,使用builder的reverse方法
- 对字符串中的字母进行排序,可以将其转换为char数组,对char进行排序,然后,重新生成新的字符串
- 对字符串数组进行排序,可以使用string的
compareto
方法
栈
- 先进后出,仅仅操作栈顶元素
- 栈,回退数据,向某一方向延伸后可回退到初始方向;也可用于内部嵌套问题
- 栈弹出前,要判断大小是否还可以弹出数据
堆|优先队列
PriorityQueue
java优先队列使用堆来实现,其中,可以在构造时指定比较器来确定其升序或者降序new PriorityQueue<Integer>((n1, n2) -> n1 - n2);
使用lamb可以快速定义比较器- 优先队列的时间复杂度于其大小有关,因此,可以使其大小维持在一常数级别,保证一定的速度,如
if (heap.size() > k) heap.poll();
- 存值时,要考虑重复值问题
- 每次插入和弹出都会再次排序
最小值
- 数的最小值时为其序列为增序列
贪心算法
- 进行决策时,要考虑到临界值问题,如两数比较时,不足两数;亦或者左右比较交换后,会影响之前数据的升序或者降序
回溯法
- 字符串 正则匹配
public boolean match(String str, String pattern) {
return helper(str, pattern, "");
}
public boolean helper(String str, String pattern, String pre) {
if (str.equals(pattern)) {
return true;
}
if (pattern.length() == 0) {
return false;
}
if (pattern.charAt(0) == '*') {
return helper(str, pattern.substring(1), "");
}
boolean res = false;
char[] strArrays = str.toCharArray();
char[] patternArrays = pattern.toCharArray();
if (strArrays.length > 0 && (strArrays[0] == patternArrays[0] || patternArrays[0] == '.')) {
if (pattern.length() > 1 && patternArrays[1] == '*') {
// * 代表0个值
res = helper(str, pattern.substring(2), "");
// * 代表1个值
res = res || helper(str.substring(1), pattern.substring(2), "");
// * 代表多个值
res = res || helper(str.substring(1), pattern, "");
} else {
res = helper(str.substring(1), pattern.substring(1), str.substring(0, str.length() - 1));
}
} else {
if (pattern.length() > 1 && patternArrays[1] == '*') {
// * 代表0个值
res = helper(str, pattern.substring(2), "");
}
}
return res;
}
分治算法(递归)
- 当问题规模可以缩小且缩小后的处理过程与原过程相同
- 注意参数的类型
- 如果在合并操作是,不进行结果的修改或删除,可以不使用深拷贝
- 参数在传入下一级之前,如有需要,要进行深拷贝
卡特兰数
- 若 令 h ( 0 ) = 1 , h ( 1 ) = 1 , c a t a l a n 数 满 足 递 推 式 : h ( n ) = h ( 0 ) ∗ h ( n − 1 ) + h ( 1 ) ∗ h ( n − 2 ) + . . . + h ( n − 1 ) h ( 0 ) ( n > = 2 ) 若令h(0)=1,h(1)=1,catalan数满足递推式:h(n)= h(0)*h(n-1)+h(1)*h(n-2) + ... + h(n-1)h(0) (n>=2) 若令h(0)=1,h(1)=1,catalan数满足递推式:h(n)=h(0)∗h(n−1)+h(1)∗h(n−2)+...+h(n−1)h(0)(n>=2),递推公式的通解: h ( n ) = C ( 2 n , n ) / n + 1 ( 或 者 C ( 2 n , n ) − C ( 2 n , n + 1 ) ) ) h(n)=C(2n,n)/n+1(或者C(2n,n)-C(2n,n+1))) h(n)=C(2n,n)/n+1(或者C(2n,n)−C(2n,n+1)))
- 一个序列,有规律的根据其中的每一个点将完整的序列划分为两个序列,每个点得到的结果(左右序列的组合)相加;同时,划分出的序列又进行同样的步骤,进行划分;当序列足够小时,得到结果
- 注意:
- 注意分割点与左右序列的关系
- 序列会被不断缩减,当最小化时,得到结果
- 左右序列中不包含分割点
- 构造二叉搜索树:
- 以初始序列中每一个节点为根
- 每一个根可以将整个序列划分为两部分;求出以左右序列中每个点为根所生成的所有属性结构
- 已经被划分的两部分序列还可以继续划分,直至长度为0或一
- 至最小情况时,构造树型结构返回,因为其为组合,故返回结果为集合
- 上一层获取到返回结果后,要进行左右序列结果的组合,才能得到对应序列以该点为根的所有树形结构,同时,将所在序列中不同点为根所生成的结构存储到集合中返回到上一级;
- 与普通递归的区别在与于,合并时,进行了结果的组合处理,同时,也进行了层次遍历
- 细节:
- 递归方法中,参数表示序列,以开始和结束坐标表示
- 返回结果为集合,要以所使用的的根节点为根节点进行组合
- 拆分序列时,以根节点左右划分序列,终止条件为划分的序列start>end,或者start=end;此时序列长度为0或者1,当序列长度为2时,仍可以继续划分
- 节点中可以存空
- 相同的属性结构会生成多次,但其本应该生成对次
- 要以参数传来的序列中的每一个值为根节点求解
- 括号问题
- 以某个出栈的位置来划分左右序列,该位置与左右序列的关系为该位置的括号包含着已经出栈的序列,即左序列
- n对括号,求解为其中的一对括号,包含着其他括号,从1到n,包含着的为左序列,未包含的为右序列
- 划分序列时,不包含当前括号,当前括号包含左括号
for(String l:left){
for(String r:right){
list.add("("+l+")"+r);
}
}
- 给出一个n,要求一个长度为2n的01序列,使得序列的任意前缀中1的个数不少于0的个数;
- 1表示进栈,0表示出栈,借用栈想法求解