刷题日记 - 11
最近更新:2025-10-20   |   字数总计:2.9k   |   阅读估时:13分钟   |   阅读量:
  1. 刷题日记 - 11
    1. 173. 二叉搜索树迭代器 - 力扣(LeetCode)
    2. 222. 完全二叉树的节点个数 - 力扣(LeetCode)
    3. 236. 二叉树的最近公共祖先 - 力扣(LeetCode)
    4. 637. 二叉树的层平均值 - 力扣(LeetCode)
    5. 103. 二叉树的锯齿形层序遍历 - 力扣(LeetCode)
    6. 3144. 分割字符频率相等的最少子字符串 - 力扣(LeetCode)
    7. 饿了么 8-23 笔试第二题
    8. 93. 复原 IP 地址 - 力扣(LeetCode)
    9. 将IP地址转换为int,再将int转换为IP地址

刷题日记 - 11

173. 二叉搜索树迭代器 - 力扣(LeetCode)

难度:中等

二叉树 中序遍历

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
class BSTIterator {

// 存储二叉搜索树的中序遍历结果
List<Integer> array;

// 指向当前遍历的元素索引
int p;

// 存储中序遍历结果的总大小
int size;

// 构造函数,初始化迭代器并进行中序遍历
public BSTIterator(TreeNode root) {
p = 0;
array = new ArrayList<>();
// 对树进行中序遍历并存储结果
dfs(root);
size = array.size();
}

// 返回中序遍历的下一个元素
public int next() {
return array.get(p++);
}

// 检查是否还有下一个元素
public boolean hasNext() {
if(p < size) return true;
return false;
}

// 对树进行中序遍历的递归函数
private void dfs(TreeNode node){
if(node == null) return;
dfs(node.left); // 递归遍历左子树
array.add(node.val); // 将当前节点值添加到列表
dfs(node.right); // 递归遍历右子树
}
}

实现一个二叉树中序迭代器,我的思路是在初始化的时候就将先中序遍历的结果存入数组,然后根据指针来判断。

但是这个方法无论如何都会先中序遍历一边二叉树,这其实是没必要的,因为这个迭代器又不能回到上一个,把中序遍历的结果存入数组其实很没必要。

本题应该希望我们用栈来模拟中序遍历。


用栈模拟

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
class BSTIterator {

// 栈用于保存遍历过程中节点
Stack<TreeNode> stack;

// 构造函数,初始化栈,并将左子树节点按顺序压入栈中
public BSTIterator(TreeNode root) {
stack = new Stack<>();
if (root != null) stack.push(root);
// 将所有左子节点压入栈中,直到最左边的节点
while (!stack.isEmpty() && stack.peek().left != null) {
stack.push(stack.peek().left);
}
}

// 返回中序遍历的下一个节点值
public int next() {
// 弹出栈顶节点,即当前最小节点
TreeNode node = stack.pop();
// 如果弹出的节点有右子树,处理右子树节点
if (node.right != null) {
stack.push(node.right);
// 将右子树的所有左子节点压入栈中
while (stack.peek().left != null) {
stack.push(stack.peek().left);
}
}
// 返回当前节点的值
return node.val;
}

// 判断是否还有未遍历的节点
public boolean hasNext() {
return !stack.isEmpty();
}
}

可以回顾一下用栈完成二叉树的中序遍历。

222. 完全二叉树的节点个数 - 力扣(LeetCode)

难度:简单

二叉树

1
2
3
4
5
6
class Solution {
public int countNodes(TreeNode root) {
if(root == null) return 0;
return countNodes(root.left) + countNodes(root.right) + 1;
}
}

可以直接遍历得到,时间复杂度O(n)。

但是没有利用到完全二叉树的特性。


如果要利用到完全二叉树的性质。

可以判断根节点的左子树和右子树哪一个是满二叉树,对于是满二叉树的子树,这一部分的节点数计算可以不用通过遍历计数。

如何判断呢?

  • 左子树和右子树都是完全二叉树
  • 完全二叉树的深度可以直接通过一直访问左节点得到
  • 完全二叉树最后一层的节点是从左到右排列的
  • 如果左子树和右子树深度相同,证明左子树是满二叉树 (有可能右子树也是满二叉树,这里也可以通过一直访问右节点来判断右子树是不是满二叉树)
  • 如果深度不同,则右子树是满二叉树
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
class Solution {
public int countNodes(TreeNode root) {
if(root == null) return 0;
int res = 1; // 包含根节点
int leftDepth = countLevel(root.left); // 左子树的深度
int rightDepth = countLevel(root.right); // 右子树的深度
if(leftDepth==rightDepth){
// 证明左子树是完全二叉树
int leftCount = (int)Math.pow(2,leftDepth) - 1;
int rightCount = dfsCount(root.right);
res = res + leftCount + rightCount;
}else{
// 证明右子树是完全二叉树
int leftCount = dfsCount(root.left);
int rightCount = (int)Math.pow(2,rightDepth) - 1;
res = res + leftCount + rightCount;
}
return res;
}

public int dfsCount(TreeNode node){
if(node==null)return 0;
return dfsCount(node.left)+dfsCount(node.right)+1;
}

public int countLevel(TreeNode node){
int count = 0;
while(node!=null){
count++;
node = node.left;
}
return count;
}
}

236. 二叉树的最近公共祖先 - 力扣(LeetCode)

难度:中等

二叉树

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class Solution {
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
return dfs(root, p, q);
}

private TreeNode dfs(TreeNode node, TreeNode p, TreeNode q) {
// 递归终止条件:当前节点是null,当前节点等于p或者q
if (node == null || node == q || node == p) return node;

// 在左子树中查找 p 和 q ,如果返回null则表示左子树中没有p q节点
TreeNode l = dfs(node.left, p, q);

// 在右子树中查找 p 和 q
TreeNode r = dfs(node.right, p, q);

// 左子树中没找到,说明 p 和 q 都在右子树中,返回右子树的结果
if (l == null) return r;

// 右子树中没找到,说明 p 和 q 都在左子树中,返回左子树的结果
if (r == null) return l;

// 如果左右子树中都找到了,则当前节点 node 是最近公共祖先
return node;
}
}

dfs函数只会返回3个值:

  • p、q的共同父节点
  • p节点
  • q节点

dfs先序遍历遇到p或者q节点之后就会往回回溯。

如果对于某一个节点lr都不为null表明它就是最近共公父节点。

如果对于某一个节点l的值为nullr不为null,那么证明两个节点一定在该节点的右节点。

l!=null, r==null同理。

l,r都为null,则表示目标节点都不在这颗节点之下。

637. 二叉树的层平均值 - 力扣(LeetCode)

难度:简单

二叉树

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class Solution {
public List<Double> averageOfLevels(TreeNode root) {
List<Double> res = new ArrayList<>();
if(root==null)return res;
LinkedList<TreeNode> queue = new LinkedList<>();
queue.offerLast(root);
queue.offerLast(null);
double sum = 0;
int count = 0;
while(true){
TreeNode cur = queue.pollFirst();
if(cur!=null){
count++;
sum += cur.val*1.0;
if(cur.left!=null)queue.offerLast(cur.left);
if(cur.right!=null)queue.offerLast(cur.right);
}else{
res.add(sum/count);
sum = 0;
count = 0;
if(queue.isEmpty())break;
queue.offerLast(null);
}
}
return res;
}
}

就是二叉树层序遍历

103. 二叉树的锯齿形层序遍历 - 力扣(LeetCode)

难度:中等

二叉树 链表头插

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
class Solution {
public List<List<Integer>> zigzagLevelOrder(TreeNode root) {
List<List<Integer>> res = new ArrayList<>();
if(root==null)return res;
boolean LEFT = true;
boolean RIGHT = false;
LinkedList<TreeNode> queue = new LinkedList<>();
queue.offerLast(root);
queue.offerLast(null);
boolean STATE = RIGHT;
List<Integer> tmp = new LinkedList<>();
while(true){
TreeNode cur = queue.pollFirst();
if(cur!=null){
// 其余部分都是层序遍历
// 锯齿形层序遍历的特点,有一些层的数据是原始层序遍历Reverse的
// 链表尾插法得到的是原始的,头插法就是Reverse的
if(STATE==LEFT){
tmp.add(0,cur.val);
}else{
tmp.add(cur.val);
}
if(cur.left!=null)queue.offerLast(cur.left);
if(cur.right!=null)queue.offerLast(cur.right);
}else{
res.add(tmp);
tmp = new ArrayList<>();
STATE = STATE==LEFT?RIGHT:LEFT;
if(queue.isEmpty())break;
queue.offerLast(null);
}
}
return res;
}
}

3144. 分割字符频率相等的最少子字符串 - 力扣(LeetCode)

难度:中等

分区动态规划 字符串

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
class Solution {
public int minimumSubstringsInPartition(String s) {
char[] chars = s.toCharArray();
int len = chars.length;
boolean[][] map = new boolean[len][len]; //map[i][j]表示字符串s中下标i->j的子字符串是不是平衡的
int[] counter = new int[26];

// 先处理字符串s,给map赋值
for(int i=0;i<len;i++){
Arrays.fill(map[i],true);
Arrays.fill(counter,0);
counter[chars[i]-'a']++;
for(int j=i+1;j<len;j++){
counter[chars[j]-'a']++;
int tmp = -1;
for(int count:counter){
if(count>0){
if(tmp==-1)tmp = count;
if(count!=tmp){
map[i][j] = false;
break;
}
}
}
}
}

// 动态规划部分
// dp[i]表示,从0->i,最少可以分为成多少个平衡字符串
int[] dp = new int[len];
for(int i=0;i<len;i++){
if(map[0][i])dp[i]=1;
else{
// 状态转移方程dp[i] = min_dp[j:0...i-1] + 1,map[j,i]==true
int min = Integer.MAX_VALUE;
for(int j=1;j<=i;j++){
if(map[j][i]){
min = Math.min(min,dp[j-1]);
}
}
dp[i] = min+1;
}
}
return dp[len-1];
}
}

时间复杂度O(n^2^)


给map赋值那一段可以进行优化,不必每次都遍历一次counter数组

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 给map赋值
for(int i=0;i<len;i++){
Arrays.fill(map[i],true);
Arrays.fill(counter,0);
counter[chars[i]-'a']++;
int count = 1;
int max = 1;
for(int j=i+1;j<len;j++){
if(counter[chars[j]-'a']==0)count++;
counter[chars[j]-'a']++;
max = Math.max(counter[chars[j]-'a'],max);
if(j-i+1!=count*max)map[i][j] = false;
}
}

记录i->j中有多少种字符,以及字符出现的最大次数,如果总字符串不等于字符种类数*字符出现最大次数,那么直接判定该子串不是平衡子串。

饿了么 8-23 笔试第二题

动态规划

abd58ca6eb8097e0596f90cb3d24de3.jpg

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

public class Helloworld {
public static void main(String[] args) {
int[] arr = {1,1,4,5,1,4,1,2,3,4,1,3,5,6,7,1,2,3,6};
int n = arr.length;
long[] dp = new long[n + 1];
int MAX = 1_000_000_001;
for (int i = 0; i < n; i++) {
int max = 0, min = MAX;
for (int j = i; j < n; j++) {
max = Math.max(arr[j], max);
min = Math.min(arr[j], min);
dp[j + 1] = Math.max(dp[j + 1], max - min + dp[i]);
}
}
System.out.println(dp[n]);
}
}

93. 复原 IP 地址 - 力扣(LeetCode)

难度:中等

字符串 dfs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
class Solution {
List<String> res = new ArrayList<>();
public List<String> restoreIpAddresses(String s) {
char[] chars = s.toCharArray();
int len = chars.length;
LinkedList<Integer> list = new LinkedList<>();
dfs(chars,0,len,list);
return res;
}

private void dfs(char[] chars,int cur,int len,LinkedList<Integer> list){
// 结束dfs的条件
if(list.size()==4&&cur==len){
// 符合的情况
StringBuilder sb = new StringBuilder();
sb.append(list.get(0));
sb.append('.');
sb.append(list.get(1));
sb.append('.');
sb.append(list.get(2));
sb.append('.');
sb.append(list.get(3));
res.add(sb.toString());
}else if(list.size()==4&&cur<len)return; // 不符合的情况直接返回

int val = 0; // 计算当前槽位的值(一共四个槽位),初始值为0
for(int i=cur;i<len;i++){
int curElem = chars[i] - '0'; // cur位置的值
val = val*10 + curElem; // 加上cur位置的数字,等于目前槽位的值
if(val<=255){ // 槽位上数字满足0<=val<=255才进行下一轮
list.offerLast(val);
dfs(chars,i+1,len,list);
list.pollLast();
}
if(val==0||val>255)break; // 当val==0,表明cur位置为0,由于不能出现前置0,所以要在这里break
} // 当val>255,不满足,当前槽位所有可能的值都遍历完了

}
}

将IP地址转换为int,再将int转换为IP地址

int占4个字节,IP地址是String,占用的空间7-15个字节。

这样做可以节省空间,IP地址正好是四个八位二进制数,所以正好可以用int表示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public class Test {
public static void main(String[] args) {
String ip = "192.168.0.1";
int res = IP2Int(ip);
System.out.println(res);

String a = Int2IP(res);
System.out.println(a);
}

private static int IP2Int(String ip){
String[] strs = ip.split("\\.");
int res = 0;
for(int i=0;i< strs.length;i++){
int pos = 8*(3-i);
int val = Integer.parseInt(strs[i]) << pos;
res = res|val;
}
return res;
}

private static String Int2IP(int IPint){
String[] strs = new String[4];
for(int i=0;i<4;i++){
int pos = (3-i)*8;
int tmp = IPint & (255 << pos);
int val = tmp >>> pos;
strs[i] = String.valueOf(val);
}
return String.join(".",strs);
}
}

利用位运算,将IP的四个部分分布在32位中的四个8位,然后组合成一个整数。

注意,最后往右移的时候,是无符号右移。

>> 算术右移,如果是正数左边用0填充,如果左边是负数用1填充

>>>逻辑右移,全用0填充