目录
4.剑指 offer 65 不用加减乘除做加法 --- 位运算
8.剑指 offer 68-2 最小的K个数 --- 排序问题
1.剑指 offer 54 二叉搜索树第K大节点
二叉搜索树的特性是 前序排序是增序的
所以,直接通过前序搜索得到排序列表
返回对应的值即可
2.剑指 offer 55-1 二叉树的深度
这个防止调用过多,导致超时
这个算第至少第三遍了
3.剑指 offer 55-2 平衡二叉树
是对剑指 offer 55-1 二叉树的深度 进阶调用
左右高度差不大于1
记住这个判断的条件要在每个节点进行判断
4.剑指 offer 65 不用加减乘除做加法 --- 位运算
有符号整数通常用补码来表示和存储,补码具有如下特征:
-
正整数的补码与原码相同;负整数的补码为其原码除符号位外的所有位取反后加 1。
-
可以将减法运算转化为补码的加法运算来实现。
-
符号位与数值位可以一起参与运算。
class Solution {
public int add(int a, int b) {
while (b != 0) {
int carry = (a & b) << 1;
a = a ^ b;
b = carry;
}
return a;
}
}
5.剑指 offer 64 求1加到n
方法一:递归调用
没有重复调用
方法二:数学公式
n * (1 + n) / 2
6.剑指 offer 68-1 二叉搜索树的最近公共祖先
首先 , 空判断
第二个判断是 最终判断? 任意一个节点是根节点 则其祖先为根节点
递归调用
二叉搜索树特性 --- 值
p和q在当前根节点的值 都在左侧 ,将其根节点变更为左孩子节点 进行递归调用
同理右侧
如果 分别在两侧 则当前根节点为 该树的最近公共祖先
class Solution {
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
if(root == null) return null;
if(root == p || root == q) return root;
if(p.val < root.val && q.val < root.val){
return lowestCommonAncestor(root.left,p ,q);
} else if(p.val > root.val && q.val > root.val){
return lowestCommonAncestor(root.right , p ,q);
}
return root;
}
}
7.剑指 offer 68-2 二叉树的最近公共祖先
与上一题的区别是 没有二叉搜索树的关于值的特性
递归调用的次数增加,因为数字没有规律
然后 进行判断递归调用的结果
如果为空,则代表以此次调用为根节点的没有最近公共祖先
不为空则表示找到了
然后 进行左右树判断
class Solution {
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
if(root == null) return null;
if(root == p || root == q) return root;
TreeNode left = lowestCommonAncestor(root.left, p, q);
TreeNode right = lowestCommonAncestor(root.right, p, q);
if(left != null && right != null){
return root;
} else if(left != null){
return left;
} else {
return right;
}
}
}
8.剑指 offer 68-2 最小的K个数 --- 排序问题
调api进行排序
使用大根堆对象进行排序
class Solution {
public int[] getLeastNumbers(int[] arr, int k) {
int[] vec = new int[k];
if(k == 0){
return vec;
}
PriorityQueue<Integer> queue = new PriorityQueue<>((n1, n2) -> n2 - n1);
for (int i = 0; i < k; i++) {
queue.offer(arr[i]);
}
for (int i = k; i < arr.length; i++) {
if(queue.peek() > arr[i]){
queue.poll();
queue.offer(arr[i]);
}
}
for (int i = 0; i < k; i++) {
vec[i] = queue.poll();
}
return vec;
}
}
前两个for存在的必要:减小空间复杂度 --- 将小根堆的大小维持在k
这道题目的收获,学会使用小根堆对象的API
9.剑指 offer 41 数据流的中位数
分别使用大根堆 ,和小根堆 进行记录
就是一边从大的记录,一边从小的开始记录,记录到最后不就是中位数了嘛
重点:
这里面要抠得是细节
这个是重新定义中间值 归属大的小的 ---- 防止判断的逻辑问题
if (queMax.size() + 1 < queMin.size()) {
queMax.offer(queMin.poll());
}
比如说 那个加一的细节
class MedianFinder {
PriorityQueue<Integer> queMin;
PriorityQueue<Integer> queMax;
public MedianFinder() {
queMin = new PriorityQueue<Integer>((a, b) -> (b - a));
queMax = new PriorityQueue<Integer>((a, b) -> (a - b));
}
public void addNum(int num) {
if (queMin.isEmpty() || num <= queMin.peek()) {
queMin.offer(num);
if (queMax.size() + 1 < queMin.size()) {
queMax.offer(queMin.poll());
}
} else {
queMax.offer(num);
if (queMax.size() > queMin.size()) {
queMin.offer(queMax.poll());
}
}
}
public double findMedian() {
if (queMin.size() > queMax.size()) {
return queMin.peek();
}
return (queMin.peek() + queMax.peek()) / 2.0;
}
}
10.剑指 offer 44 数字序列中的某一位数字
数学 二分查找
有一个标准的二分查找的标志
数字以0123456789101112131415…的格式序列化到一个字符序列中。在这个序列中,第5位(从下标0开始计数)是5,第13位是1,第19位是4,等等。
请写一个函数,求任意第n位对应的数字。
class Solution {
public int findNthDigit(int n) {
int low = 1 , high = 9 ;
while (low < high){
int mid = (high - low) / 2 + low;
if(totalDigits(mid) < n){
low = mid + 1;
} else {
high = mid;
}
}
int d = low;
int prevDigits = totalDigits(d - 1);
int index = n - prevDigits - 1;
int start = (int) Math.pow(10 , d - 1);
int num = start + index / d;
int digitIndex = index % d;
int digit = (num / (int)(Math.pow(10 , d - digitIndex - 1))) % 10;
return digit;
}
int totalDigits(int length){
int digits = 0;
int curLength = 1 , curCount = 9;
while (curLength <= length){
digits += curLength * curCount;
curLength++;
curCount *= 10;
}
return digits;
}
}
这道题目的进度是题目是读懂了,题解是看懂了,代码从二分查找下面就没有看懂了
11.面试题45 把数组排成最小的数 --- 字符串 排序
应该是第二遍这道题目了
核心思想是:两个数拼接后,谁在前更小
按照这个思想进行排序
内置排序 和 快排
都是自定义排序方法
class Solution {
public String minNumber(int[] nums) {
String[] strs = new String[nums.length];
for (int i = 0; i < nums.length; i++) {
strs[i] = String.valueOf(nums[i]);
}
Arrays.sort(strs , (x , y) -> (x + y).compareTo(y + x));
StringBuilder res = new StringBuilder();
for (String str : strs) {
res.append(str);
}
return res.toString();
}
}
void quickSort(String[] strs, int l, int r) {
if(l >= r) return;
int i = l, j = r;
String tmp = strs[i];
while(i < j) {
while((strs[j] + strs[l]).compareTo(strs[l] + strs[j]) >= 0 && i < j) j--;
while((strs[i] + strs[l]).compareTo(strs[l] + strs[i]) <= 0 && i < j) i++;
tmp = strs[i];
strs[i] = strs[j];
strs[j] = tmp;
}
strs[i] = strs[l];
strs[l] = tmp;
quickSort(strs, l, i - 1);
quickSort(strs, i + 1, r);
}
12.面试题61 扑克牌中的顺子
首先,这道题的难点是 对扑克牌不熟悉
顺子的条件
-
边界限制 0 ~ 14
-
大小王为0 可以允许重复, 其他的不允许重复
-
允许大超过1
-
最大牌 - 最小牌 < 5 则可构成顺子
class Solution {
public boolean isStraight(int[] nums) {
Set<Integer> repeat = new HashSet<>();
int max = 0, min = 14;
for(int num : nums) {
if(num == 0) continue; // 跳过大小王
max = Math.max(max, num); // 最大牌
min = Math.min(min, num); // 最小牌
if(repeat.contains(num)) return false; // 若有重复,提前返回 false
repeat.add(num); // 添加此牌至 Set
}
return max - min < 5; // 最大牌 - 最小牌 < 5 则可构成顺子
}
}
13.剑指 offer 07 重建二叉树
根据 前序 和 中序 重建二叉树
空判断
使用栈进行当前已建好的树节点
栈的顶端是 当前的根节点
class Solution {
public TreeNode buildTree(int[] preorder, int[] inorder) {
if(preorder == null || preorder.length == 0) return null;
TreeNode root = new TreeNode(preorder[0]);
Deque<TreeNode> stack = new LinkedList<>();
stack.push(root);
int inorderIndex = 0;
for (int i = 1; i < preorder.length; i++) {
int preorderVal = preorder[i];
TreeNode node = stack.peek();
if(node.val != inorder[inorderIndex]){
node.left = new TreeNode(preorderVal);
stack.push(node.left);
} else {
while (!stack.isEmpty() && stack.peek().val == inorder[inorderIndex] ){
node = stack.pop();
inorderIndex++;
}
node.right = new TreeNode(preorderVal);
stack.push(node.right);
}
}
return root;
}
}
14 .34在排序数组中查找元素的第一个和最后一个位置
使用两个二分查找,找到左右边界
寻找target在数组里的左右边界,有如下三种情况:
-
情况一:target 在数组范围的右边或者左边,例如数组{3, 4, 5},target为2或者数组{3, 4, 5},target为6,此时应该返回{-1, -1}
-
情况二:target 在数组范围中,且数组中不存在target,例如数组{3,6,7},target为5,此时应该返回{-1, -1}
-
情况三:target 在数组范围中,且数组中存在target,例如数组{3,6,7},target为6,此时应该返回{1, 1}
class Solution {
int[] searchRange(int[] nums, int target) {
int leftBorder = getLeftBorder(nums, target);
int rightBorder = getRightBorder(nums, target);
// 情况一
if (leftBorder == -2 || rightBorder == -2) return new int[]{-1, -1};
// 情况三
if (rightBorder - leftBorder > 1) return new int[]{leftBorder + 1, rightBorder - 1};
// 情况二
return new int[]{-1, -1};
}
int getRightBorder(int[] nums, int target) {
int left = 0;
int right = nums.length - 1;
int rightBorder = -2; // 记录一下rightBorder没有被赋值的情况
while (left <= right) {
int middle = left + ((right - left) / 2);
if (nums[middle] > target) {
right = middle - 1;
} else { // 寻找右边界,nums[middle] == target的时候更新left
left = middle + 1;
rightBorder = left;
}
}
return rightBorder;
}
int getLeftBorder(int[] nums, int target) {
int left = 0;
int right = nums.length - 1;
int leftBorder = -2; // 记录一下leftBorder没有被赋值的情况
while (left <= right) {
int middle = left + ((right - left) / 2);
if (nums[middle] >= target) { // 寻找左边界,nums[middle] == target的时候更新right
right = middle - 1;
leftBorder = right;
} else {
left = middle + 1;
}
}
return leftBorder;
}
}
15 . 1094 拼车
差分方程还是不熟悉 , 这道题目再议
差分方程 使用数组
第一个循环 构造差分方程
差分方程 存储 该段有多少人
res[i] = res[i - 1] + diff[i]; // 车上剩余多少人 + 该段上了多少人
class Solution {
public boolean carPooling(int[][] trips, int capacity) {
// 0 <= fromi < toi <= 1000
int[] diff = new int[1001];
for(int[] trip : trips){
// 站台从0开始,
int i = trip[1];
// 乘客在车上的区间是 [trip[1], trip[2] - 1]
int j = trip[2] - 1;
int val = trip[0];
diff[i] += val;
if(j + 1 < diff.length){
diff[j + 1] -= val;
}
}
int[] res = new int[1001];
res[0] = diff[0];
// 不要忘记判断第一个站台res[0]
if(res[0] > capacity){
return false;
}
for(int i = 1; i < 1001; i++){
res[i] = res[i - 1] + diff[i]; // 车上剩余多少人 + 该段上了多少人
if(res[i] > capacity){
return false;
}
}
return true;
}
}