文章目录
面试Tips
业务为主 | 技巧为主 | ||
---|---|---|---|
笔试 | 55% | 45% | 笔试需要淘汰率,更考察经验,笔试卷面分过 35/100 |
面试 | 30% | 70% | 技巧为主反而更考察基础(比如窗口最大值最小值、单调栈、动态规划斜率优化) |
算法题模板
注意点:
- 子串、子数组必须连续,子序列必须连续
Master公式
简介
在编程中,递归是非常常见的一种算法,由于代码简洁而应用广泛,但递归相比顺序执行或循环程序,时间复杂度难以计算,而master公式就是用于计算递归程序的时间复杂度。
公式
T(N) = a * T(N / b) + O(N ^ d)
b:子过程的样本量
a:子过程的计算次数
O(N ^ d):子结果合并的时间复杂度
满足如上公式的程序都可以根据master公式计算时间复杂度:
log(b,a) > d :时间复杂度为O(N ^ log(b,a))
log(b,a) = d :时间复杂度为O(N ^ d * log N)
log(b,a) < d :时间复杂度为O(N ^ d)
对数器 & 比较器
- 对数器
public class Comparator {
//生成随机数组
public static int[] generateRandomArray(int maxSize, int maxValue) {
int[] arr = new int[(int) ((maxSize + 1) * Math.random())];
for (int i = 0; i < arr.length; i++) {
arr[i] = (int) ((maxValue + 1) * Math.random() - (int) ((maxValue + 1) * Math.random()));
}
return arr;
}
public static int[] copyArray(int[] arr) {
if (arr == null) return null;
return Arrays.copyOf(arr, arr.length);
}
//判断两个数组是否完全相同
public static boolean isEquals(int[] arr1, int[] arr2) {
if (arr1 == null && arr2 == null) {
return true;
}
if (arr1 == null || arr2 == null) {
return false;
}
if (arr1.length != arr2.length) {
return false;
}
for (int i = 0; i < arr1.length; i++) {
if (arr1[i] != arr2[i]) {
return false;
}
}
return true;
}
public static void comparator(){
int maxSize = 50;
int maxValue = 100;
int testTime = 1000;
boolean success = true;
for (int i = 0; i < testTime; i++) {
int[] arr = generateRandomArray(maxSize, maxValue);
int[] copy = copyArray(arr);
Arrays.sort(copy);
//TODO 待定排序策略
if (! isEquals(arr, copy)) {
success = false;
break;
}
}
System.out.println(success == true ? "perfect" : "totally wrong!!!");
}
public static void main(String[] args) {
comparator();
}
}
- 比较器 : 重载运算符
一些知识
Map.entrySet
-
java中Map存放的是许多个键值对的数据,而Map.Entry数据类型可以想象成Map中的一个键值对的数据,可以存放一个键与一个值,显然一个Map对象可以看成存储了多个Map.Entry的数据组
-
从Map中获取Map.Entry:
通过Map.entrySet()方法可以从指定的map对象中获取一个Set集合,集合中的元素是Map.Entry类型//寻找数组的多数元素,出现次数>n/2 public int majorityElement(int[] nums) { Map<Integer, Integer> counts = countNums(nums); //相当于是声明了一个键值对的对象,用于存放map中众数的key-value Map.Entry<Integer, Integer> majorityEntry = null; // map.entrySet 获得的是map中的每一个键值对 (将key-value看成时一个 Map.Entry对象)所构成的set集合 //可以很方便的遍历同时获得key,value for (Map.Entry<Integer, Integer> entry : counts.entrySet()) { if (majorityEntry == null || entry.getValue() > majorityEntry.getValue()) { } } for (Map.Entry<Integer, Integer> entry : counts.entrySet()) { if (majorityEntry == null || entry.getValue() > majorityEntry.getValue()) { majorityEntry = entry; } } return majorityEntry.getKey(); } private Map<Integer, Integer> countNums(int[] nums) { Map<Integer, Integer> counts = new HashMap<Integer, Integer>(); for (int num : nums) { if (! counts.containsKey(num)) { counts.put(num, 1); } else { counts.put(num, counts.get(num) + 1); } } return counts; }
链表、二叉树
- 链表和二叉树定义本身还有递归,因此经常可以使用递归来解答。
Set
- set.add();方法
语法 boolean add(E e)
返回值:如果Set集合中不包含要添加的对象,则添加对象并返回true;否则返回false。
参数:e是要添加到Set集合中的对象。
Map
- map.put();方法
public V put(K key, V value) { 如果key不存在,插入成功,返回null;如果key存在,返回之前对应的value。
return putVal(hash(key), key, value, false, true);
- map的key为基本数据类型时为值传递(拷贝一份),引用数据类型时为内存地址,8字节
常见排序
快排
- 可以将快排改造为小样本下使用插入排序,大样本下使用快排
- Arrays.sort(); 方法 基本数据类型使用快排,引用数据类型使用归并排序,保证稳定性
插入排序
public static void insertionSort(int[] arr) {
if (arr == null || arr.length < 2) return;
for (int i = 1; i < arr.length; i++) {
for (int j = i - 1; j >= 0 && arr[j + 1] < arr[j]; j--) {
swap(arr[j], arr[j + 1], arr);
}
}
}
public static void swap(int i, int j, int[] arr) {
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
归并排序
public static void process(int[] arr, int l, int r) {
if (l == r) return;
int mid = l + ((l - r) >> 1);
process(arr, l, mid);
process(arr, mid + 1, r);
merge(arr, l, mid, r);
}
private static void merge(int[] arr, int l, int mid, int r) {
int[] help = new int[l + r + 1];
int i = 0;
int p1 = l;
int p2 = mid + 1;
while (p1 <= mid && p2 <= r) {
help[i++] = arr[p1] <= arr[p2] ? arr[p1++] : arr[p2++];
}
while (p1 <= mid) {
help[i++] = arr[p1++];
}
while (p2 <= r) {
help[i++] = arr[p2++];
}
for (int j = 0; j < help.length; j++) {
arr[l + j] = help[j];
}
}
快速排序
public static void quickSort(int[] arr) {
if (arr == null || arr.length < 2) return;
quickSort(arr, 0, arr.length - 1);
}
private static void quickSort(int[] arr, int L, int R) {
if (L < R) {
swap(arr, L + (int) ((R - L + 1) * Math.random()), R);
//划分区域
int[] p = partition(arr, L, R);
quickSort(arr, L, p[0] - 1);
quickSort(arr, p[1] + 1, R);
}
}
private static int[] partition(int[] arr, int L, int R) {
int less = L - 1;
int more = R;
while (L < more) {
if (arr[L] < arr[R]) {
swap(arr, ++ less, L++);
} else if (arr[L] > arr[R]) {
swap(arr, -- more, L);
} else {
L++;
}
}
swap(arr, more, R);
return new int[]{less + 1, more};
}
private static void swap(int[] arr, int i, int j) {
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
堆排序
public static void heapSort(int[] arr) {
if (arr == null || arr.length < 2) return;
//相当于一个个给元素,建立大根堆
// for (int i = 0; i < arr.length; i++) {
// heapInsert(arr, i);
// }
//相当于一股脑给出整个堆,然后从最右侧叶节点进行调整
for (int i = arr.length - 1; i >= 0; i--) {
heapify(arr, i, arr.length);
}
int heapSize = arr.length;
//0位置数与最后一位的数(n-1)交换,并让heapSize减一,表示从堆中移除该元素(也意味着该元素已经处于了正确的位置上了)
swap(arr, 0, -- heapSize);
while (heapSize > 0) {
heapify(arr, 0, heapSize); //将新换上来的0位置的数不断地heapify至该去的位置
swap(arr, 0, -- heapSize);
}
}
//相当于建堆,不断的与其父比较向上移动
public static void heapInsert(int[] arr, int index) {
while (arr[index] > arr[(index - 1) / 2]) {
swap(arr, index, (index - 1) / 2);
index = (index - 1) / 2;
}
}
//x在index位置,是否能往下移动
public static void heapify(int[] arr, int index, int heapSize) {
int left = 2 * index + 1;
//意味着有孩子,有向下移动的可能性
while (left < heapSize) {
//选出较大的孩子
int largest = left + 1 < heapSize && arr[left + 1] > arr[left]
? left + 1 : left;
//较大的孩子与父节点比较
largest = arr[index] > arr[largest] ? index : largest;
//当前的数已经最大了,不用向下移动。
if (largest == index) {
break;
}
swap(arr, index, largest);
//来到大孩子原来位置
index = largest;
left = index * 2 + 1;
}
}
private static void swap(int[] arr, int i, int j) {
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
堆的应用
/**
* 中位数是有序列表中间的数。如果列表长度是偶数,中位数则是中间两个数的平均值。
* [2,3,4]的中位数是 3
* [2,3] 的中位数是 (2 + 3) / 2 = 2.5
* 设计一个支持以下两种操作的数据结构:
* void addNum(int num) - 从数据流中添加一个整数到数据结构中。
* double findMedian() - 返回目前所有元素的中位数。
*/
public class MedianFinder {
private PriorityQueue<Integer> minQ;
private PriorityQueue<Integer> maxQ;
public MedianFinder() {
//(默认)小根堆,用于存放大于中位数的数
this.minQ = new PriorityQueue<>();
//大根堆,用于存放小于等于中位数的数
this.maxQ = new PriorityQueue<>((x, y) -> y - x);
}
//总是维持 0 <大根堆size-小根堆size <=1; 总量为奇数时,总是peek()大根堆堆顶元素即可
public void addNum(int num) {
//还没加过数 或者 当前的数小于大根堆堆顶,加入大根堆,此时的中位数将小于等于原来中位数,因此可能需要移动元素到小根堆。
if (maxQ.isEmpty() || num <= maxQ.peek()) {
maxQ.add(num);
if (minQ.size() + 1 < maxQ.size()) {
minQ.add(maxQ.poll());
}
} else {
//当前的数大于小根堆堆顶,加入小根堆,此时的中位数将大于等于原来中位数,因此可能需要移动元素到大根堆。
minQ.add(num);
if (minQ.size() > maxQ.size()) {
maxQ.add(minQ.poll());
}
}
}
public double findMedian() {
if (maxQ.size() > minQ.size()) {
return maxQ.peek();
}
return (maxQ.peek() + minQ.peek()) / 2.0; //求平均值,除以2.0得到double类型结果
}
}
数组习题
- 两数组中包含唯一(二)不同元素,可以考虑用异或来解
/**
* 给定一个大小为 n 的数组nums ,返回其中的多数元素。多数元素是指在数组中出现次数 大于 n/2 的元素。
* 你可以假设数组是非空的,并且给定的数组总是存在多数元素。
*/
public class leetcode169 {
//map法
//Map的value存出现次数
public int majorityElement1(int[] nums) {
Map<Integer, Integer> counts = countNums(nums);
//相当于是声明了一个键值对的对象,用于存放map中众数的key-value
Map.Entry<Integer, Integer> majorityEntry = null;
// map.entrySet 获得的是map中的每一个键值对 (将key-value看成时一个 Map.Entry对象)所构成的set集合
//可以很方便的遍历同时获得key,value
for (Map.Entry<Integer, Integer> entry : counts.entrySet()) {
if (majorityEntry == null || entry.getValue() > majorityEntry.getValue()) {
}
}
for (Map.Entry<Integer, Integer> entry : counts.entrySet()) {
if (majorityEntry == null || entry.getValue() > majorityEntry.getValue()) {
majorityEntry = entry;
}
}
return majorityEntry.getKey();
}
private Map<Integer, Integer> countNums(int[] nums) {
Map<Integer, Integer> counts = new HashMap<Integer, Integer>();
for (int num : nums) {
if (! counts.containsKey(num)) {
counts.put(num, 1);
} else {
counts.put(num, counts.get(num) + 1);
}
}
return counts;
}
//排序,中间位置的数一定时众数
public int majorityElement2(int[] nums) {
Arrays.sort(nums);
return nums[nums.length / 2];
}
//二分法查找众数,因为众数数量多余一半,因此众数必在左右其中一侧也是众数
public int majorityElement4(int[] nums) {
return majorityElementRec(nums, 0, nums.length - 1);
}
private int majorityElementRec(int[] nums, int low, int high) {
if (low == high) return nums[low]; //baseCase
int mid = low + (high - low) / 2;
int left = majorityElementRec(nums, low, mid);
int right = majorityElementRec(nums, mid + 1, high);
if (left == right) return left;
//左右区间众数不相同,统计众数出现次数并比较选出答案
int leftCount = countInRange(nums, low, high, left);
int rightCount = countInRange(nums, low, high, right);
return leftCount > rightCount ? left : right;
}
private int countInRange(int[] nums, int low, int high, int num) {
int count = 0;
for (int i = low; i <= high; i++) {
if (nums[i] == num) {
count++;
}
}
return count;
}
//投票法
/**
* 我们维护一个候选众数 candidate 和它出现的次数 count。初始时 candidate 可以为任意值,count 为 0;
* 我们遍历数组 nums 中的所有元素,对于每个元素 x,在判断 x 之前,如果 count 的值为 0,我们先将 x 的值赋予 candidate,随后我们判断 x:
* 如果 x 与 candidate 相等,那么计数器 count 的值增加 1;
* 如果 x 与 candidate 不等,那么计数器 count 的值减少 1;
*/
public int majorityElement5(int[] nums) {
Integer candidate = null;
int count = 0;
for (int num : nums) {
//重新选出一个候选人来
if (count == 0) {
candidate = num;
}
count += candidate == num ? 1 : - 1;
}
return candidate;
}
}
/**
* 给你一个整数数组nums 和一个整数k ,判断数组中是否存在两个 不同的索引i和j ,满足 nums[i] == nums[j] 且 abs(i - j) <= k 。
* 如果存在,返回 true ;否则,返回 false
*/
public class leetcode219 {
//map
public boolean containsNearbyDuplicate1(int[] nums, int k) {
Map<Integer, Integer> map = new HashMap<>();
int n = nums.length;
for (int i = 0; i < n; i++) {
if (map.containsKey(nums[i]) && i - map.get(nums[i]) <= k) {
return true;
} else {
map.put(nums[i], i);
}
}
return false;
}
//滑动窗口法
public boolean containsNearbyDuplicate(int[] nums, int k) {
HashSet<Integer> set = new HashSet<>();
int n = nums.length;
for (int i = 0; i < n; i++) {
if (i > k) {
//每次移除窗口外的元素 [i-k,i] 为窗口大小 如果窗口内含有相同元素,返回true。
set.remove(nums[i - k - 1]);
}
if (! set.add(nums[i])) { //秀啊 set.add();如果添加成功,返回true 不走if内容。
return true;
}
}
return false;
}
}
/**
* 给定一个数组 nums,编写一个函数将所有 0 移动到数组的末尾,同时保持非零元素的相对顺序。
* 请注意 ,必须在不复制数组的情况下原地对数组进行操作。
*/
public class leetcode283 {
public void moveZeroes1(int[] nums) {
if (nums == null) return;
int j = 0;
//遇到不是0的数就复制到nums[j]中
for (int i = 0; i < nums.length; i++) {
if (nums[i] != 0) {
//j相当与槽位,依次选出非0的填入,最后一个非零元素填入后,j++了,j此时指向了最后一个非零元素的后一位,因此最后要把从j开始的数改为零
nums[j++] = nums[i];
}
}
//一口气把非0后面的数字设为零
for (int i = j; i < nums.length; i++) {
nums[i] = 0;
}
}
/**
* 使用双指针,左指针指向当前已经处理好的序列的尾部,右指针指向待处理序列的头部。
* 右指针不断向右移动,每次右指针指向非零数,则将左右指针对应的数交换,同时左指针右移。
* 类似于快排,把非0的数交换到右边,因为非零数只与0做交换,因此非零数相互间次序不变
*/
public void moveZeroes2(int[] nums) {
int n = nums.length, l = 0, r = 0;
while (r < n) {
if (nums[r] != 0) {
swap(nums, l, r);
l++;
}
r++;
}
}
private void swap(int[] nums, int l, int r) {
int temp = nums[l];
nums[l] = nums[r];
nums[r] = temp;
}
}
二分法查找
/**
* 范围查询规律
* 初始化:
* int left = 0;
* int right = nums.length - 1;
* 循环条件
* left <= right
* 右边取值
* right = mid - 1
* 左边取值
* left = mid + 1
* 查询条件
* >= target值, 则 nums[mid] >= target时, 都减right = mid - 1
* > target值, 则 nums[mid] > target时, 都减right = mid - 1
* <= target值, 则 nums[mid] <= target时, 都加left = mid + 1
* < target值, 则 nums[mid] < target时, 都加left = mid + 1
* 结果
* 求大于(含等于), 返回left
* 求小于(含等于), 返回right
* 核心思想: 要找某个值, 则查找时遇到该值时, 当前指针(例如right指针)要错过它, 让另外一个指针(left指针)跨过他(体现在left <= right中的=号), 则找到了
- 在排序数组中查找元素的第一个和最后一个位置
class Solution {
public int[] searchRange(int[] nums, int target) {
int leftIdx = binarySearch(nums, target, true); //true表示可以等于
int rightIdx = binarySearch(nums, target, false) - 1;
if (leftIdx <= rightIdx && rightIdx < nums.length && nums[leftIdx] == target && nums[rightIdx] == target) {
return new int[]{leftIdx, rightIdx};
}
return new int[]{-1, -1};
}
public int binarySearch(int[] nums, int target, boolean lower) {
int left = 0, right = nums.length - 1, ans = nums.length;
while (left <= right) { //令left=right=mid,左右相遇之后再走一步跳出循环
int mid = (left + right) / 2;
//等于时找最左边等于target元素,大于是找第一个大于target的元素
if (nums[mid] > target || (lower && nums[mid] >= target)) {
right = mid - 1;
ans = mid;
} else {
left = mid + 1;
}
}
return ans;
}
}
二叉树
二叉树遍历(递归方法)
重点:递归序!!! 每个节点都会到达三次,根据打印时机不同分为前中后序。
private void inOrder(TreeNode root) {
//1
if (root == null) return;
inOrder(root.left);
//2
System.out.println(root);
//2
inOrder(root.right);
//3
}
private void preOrder(TreeNode root) {
//1
if (root == null) return;
System.out.println(root);
//1
inOrder(root.left);
//2
inOrder(root.right);
//3
}
private void postOrder(TreeNode root) {
//1
if (root == null) return;
inOrder(root.left);
//2
inOrder(root.right);
//3
System.out.println(root);
//3
}
二叉树遍历(非递归方法)
重点:递归序!!! 每个节点都会到达三次,根据打印时机不同分为前中后序。
先序遍历
- 将头节点压入栈中
- 从栈中弹出一个节点cur
- 打印(处理)cur
- 先压入cur右孩子,再压入cur左孩子
- 周而复始 2~4
后序遍历
相当于pre`(头右左),将打印改成压入到收集栈中,最后统一打印
- 将头节点压入栈中
- 从栈中弹出一个节点cur
- 压入收集栈中
- 先压入cur左孩子,再压入cur右孩子
- 周而复始 2~4
- 最后统一打印
中序遍历
- 将头节点压入栈中
- 先将左边界节点一路压入栈中
- 左边界到头后,弹出左边界最后节点
- 如果弹出节点有右孩子的话,周而复始2~4
宽度优先遍历
- 准备一个双端队列,并将头节点入队列
- 弹出队头节点并打印(处理),并将该节点左右孩子入队
- 周而复始 2~ 直至队列为空
层序遍历(特殊宽度优先遍历)
- 准备一个双端队列,并将头节点入队列
- 在while循环中记录队列元素个数n
- 弹出队头节点并打印(处理),并将该节点左右孩子入队
- 循环n次3~步骤
- 进入下一轮while循环
- 周而复始 2~4 直至队列为空
//先序遍历
private static void preOrderUnRecur(TreeNode head) {
if (head != null) {
Stack<TreeNode> stack = new Stack<>();
stack.push(head);
while (stack != null) {
head = stack.pop();
System.out.print(head.val + " ");
if (head.right != null) {
stack.push(head.right);
}
if (head.left != null) {
stack.push(head.left);
}
}
}
}
//后序遍历 在先序遍历基础上改造 打印改为压入收集栈中 先压左再压右 保证头右左顺序
private static void postOrderUnRecur(TreeNode head) {
if (head != null) {
Stack<TreeNode> stack = new Stack<>();
Stack<TreeNode> coll = new Stack<>();
stack.push(head);
while (! stack.isEmpty()) {
head = stack.pop();
coll.push(head);
if (head.left != null) {
stack.push(head.left);
}
if (head.right != null) {
stack.push(head.right);
}
}
while (! coll.isEmpty()) {
System.out.print(coll.pop().val + " ");
}
}
}
//中序遍历 左头右(左头右(左头右(左头右(...))))
//压入栈顺序为先头后左,弹出顺序为先左再头,而找右节点是在弹出打印操作之后发生,因此右节点最后弹出(相当于右树左边界后搞)。
private static void inOrderUnRecur(TreeNode head) {
if (head != null) {
Stack<TreeNode> stack = new Stack<>();
while (! stack.isEmpty() || head != null) {
if (head != null) {
stack.push(head);
head = head.left;
} else {
head = stack.pop();
System.out.println(head.val + " ");
head = head.right;
}
}
}
}
//宽度优先遍历
private static void widthPrioritySearch(TreeNode head) {
if (head == null) return;
Queue<TreeNode> queue = new LinkedList<>();
queue.add(head);
while (! queue.isEmpty()) {
TreeNode cur = queue.poll();
System.out.println(cur);
if (cur.left != null) {
queue.add(cur.left);
}
if (cur.right != null) {
queue.add(cur.right);
}
}
}
//宽度优先遍历,寻找最大层节点数;
private static int searchMaxLevelNodes(TreeNode head) {
if (head == null) return 0;
Queue<TreeNode> queue = new LinkedList<>();
queue.add(head);
//key为当前节点cur,value为当前节点所在层数curNodeLevel
Map<TreeNode, Integer> levelMap = new HashMap<>();
levelMap.put(head, 1);
int curLevel = 1;
int curLevelNodes = 0;
int max = Integer.MIN_VALUE;
while (! queue.isEmpty()) {
TreeNode cur = queue.poll();
int curNodeLevel = levelMap.get(cur);
if (curNodeLevel == curLevel) {
curLevelNodes++;
} else {
//进入下一层,更新最大层节点数,层数++,当前层节点数重置为1
max = Math.max(max, curLevelNodes);
curLevel++;
curLevelNodes = 1;
}
if (cur.left != null) {
levelMap.put(cur.left, curNodeLevel + 1);
queue.add(cur.left);
}
if (cur.right != null) {
levelMap.put(cur.right, curNodeLevel + 1);
queue.add(cur.right);
}
}
return Math.max(max, curLevelNodes);
}
//层序遍历、并分层打印节点值
public List<List<Integer>> levelOrder(TreeNode root) {
List<List<Integer>> res = new ArrayList<>();
Queue<TreeNode> queue = new ArrayDeque<>();
if (root != null) {
queue.add(root);
}
while (!queue.isEmpty()) {
//记录当前层节点数
int n = queue.size();
List<Integer> level = new ArrayList<>();
//循环n次,一口气弹出当前层所有节点,并将下一层所有节点全部入队列
for (int i = 0; i < n; i++) {
TreeNode node = queue.poll();
level.add(node.val);
if (node.left != null) {
queue.add(node.left);
}
if (node.right != null) {
queue.add(node.right);
}
}
res.add(level);
}
return res;
}
二叉树常见题型
判断搜索二叉树
- 中序遍历并判断节点值是否满足升序
public class TreeCode {
public static int preValue = Integer.MIN_VALUE;
//递归 (推荐)
public static boolean isBST(TreeNode head) {
if (head == null) return true;
boolean isLeftBst = isBST(head.left);
if (! isLeftBst) {
return false;
}
if (preValue >= head.val) {
return false;
} else {
preValue = head.val;
}
return isBST(head.right);
}
//递归傻白甜
public static boolean isBST2(TreeNode head) {
List<Integer> inOrderList = new ArrayList<>();
process(head, inOrderList);
for (Integer nodeVal : inOrderList) {
if (nodeVal <= preValue) {
return false;
} else {
nodeVal = preValue;
}
}
return true;
}
private static void process(TreeNode head, List<Integer> inOrderList) {
if (head == null) return;
process(head.left, inOrderList);
inOrderList.add(head.val);
process(head.right, inOrderList);
}
//非递归
public static boolean isBST3(TreeNode head) {
if (head != null) {
Stack<TreeNode> stack = new Stack<>();
int preValue = Integer.MIN_VALUE;
while (stack != null || head != null) {
if (head != null) {
stack.push(head);
head = head.left;
} else {
head = stack.pop();
if (head.val <= preValue) {
return false;
} else {
preValue = head.val;
}
head = head.right;
}
}
}
return true;
}
}
判断完全二叉树
- 当前节点有右无左直接返回 false
- 在遇到非全节点(有一个孩子)后又遇到了非叶子节点 返回false
//判断是否为完全二叉树
public static boolean isCBT(TreeNode head) {
if (head == null) {
return true;
}
Queue<TreeNode> queue = new ArrayDeque<>();
queue.add(head);
boolean leaf = false;
TreeNode l = null;
TreeNode r = null;
while (! queue.isEmpty()) {
head = queue.poll();
l = head.left;
r = head.right;
if ( //有右无左直接返回 或者 在遇到非全节点后又遇到了非叶子节点
l == null && r != null || leaf && (l != null || r != null)
) {
return false;
}
if (l != null) {
queue.add(l);
}
if (r != null) {
queue.add(r);
}
//遇到非全节点标记为true
if (l == null || r == null) {
leaf = true;
}
}
return true;
}
判断平衡二叉树
- 任一节点的两子树高度差不超过一·
- 递归套路: 假设可以从其左树和右树上要信息 定结构体(即需要子树返回的信息)
//判断是否为平衡二叉树
public static boolean isBalanced(TreeNode head) { //主函数
return process2(head).isBalanced;
}
public static ReturnType process2(TreeNode x) {
if (x == null) return new ReturnType(true, 0); //base
//递归地向左右树要信息
ReturnType leftData = process2(x.left);
ReturnType rightData = process2(x.right);
//根据左右树信息加工出自己的信息,作为返回值。
boolean isBalanced = leftData.isBalanced && rightData.isBalanced
&& (Math.abs(leftData.height - rightData.height) < 2);
int height = Math.max(leftData.height, rightData.height) + 1;
return new ReturnType(isBalanced, height);
}
//返回值结构体,分析需要向左右树要哪些信息
public static class ReturnType {
public boolean isBalanced;
public int height;
public ReturnType(boolean isBalanced, int height) {
this.isBalanced = isBalanced;
this.height = height;
}
}
//判断搜索二叉树 递归方法
public static SearchReturnType process3(TreeNode x) {
if (x == null) return null;
SearchReturnType leftData = process3(x.left);
SearchReturnType rightData = process3(x.right);
int min = x.val;
int max = x.val;
if (leftData != null) {
min = Math.min(min, leftData.min);
max = Math.max(max, leftData.max);
}
if (rightData != null) {
min = Math.min(min, rightData.min);
max = Math.max(max, rightData.max);
}
boolean isBST = true;
if (leftData != null && (! leftData.isBST || leftData.max >= x.val)) {
isBST = false;
}
if (rightData != null && (! rightData.isBST || rightData.min <= x.val)) {
isBST = false;
}
if (leftData != null && rightData != null) {
}
return new SearchReturnType(isBST, max, min);
}
public static class SearchReturnType {
public boolean isBST;
public int max;
public int min;
public SearchReturnType(boolean isBST, int max, int min) {
this.isBST = isBST;
this.max = max;
this.min = min;
}
}
//判断满二叉树 递归方法 简单
public static boolean isFBT(TreeNode head) {
if (head == null) return true;
ReturnInfo returnInfo = process4(head);
return ((1 << returnInfo.height) - 1) == returnInfo.nodes;
}
private static ReturnInfo process4(TreeNode x) {
if (x == null) return new ReturnInfo(0, 0);
ReturnInfo leftData = process4(x.left);
ReturnInfo rightData = process4(x.right);
int nodes = leftData.nodes + rightData.nodes + 1;
int height = Math.max(leftData.height, rightData.height) + 1;
return new ReturnInfo(nodes, height);
}
public static class ReturnInfo {
public int nodes;
public int height;
public ReturnInfo(int nodes, int height) {
this.nodes = nodes;
this.height = height;
}
}
二叉树习题
- 找最近公共祖先
//递归方法找公共祖先
public static TreeNode lowestCommonAncestor(TreeNode head, TreeNode o1, TreeNode o2) {
if (head == null || head == o1 || head == o2) {
return head;
}
TreeNode left = lowestCommonAncestor(head.left, o1, o2);
TreeNode right = lowestCommonAncestor(head.right, o1, o2);
if (left != null && right != null) {
return head;
}
return left != null ? left : right;
}
//非递归方法,寻找两节点最低公共祖先
public static TreeNode lca(TreeNode head, TreeNode o1, TreeNode o2) {
//用来存放当前节点的父节点
Map<TreeNode, TreeNode> fatherMap = new HashMap<>();
fatherMap.put(head, head);
process(head, fatherMap);
TreeNode cur1 = o1;
HashSet<TreeNode> set = new HashSet<>();
set.add(head);
//当前节点不是头节点
while (cur1 != fatherMap.get(cur1)) {
//set中保存o1父路径
set.add(cur1);
cur1 = fatherMap.get(cur1);
}
TreeNode cur2 = o2;
while (! set.contains(cur2)) {
cur2 = fatherMap.get(cur2);
}
return cur2;
}
private static void process(TreeNode head, Map<TreeNode, TreeNode> fatherMap) {
if (head == null) return;
fatherMap.put(head.left, head);
fatherMap.put(head.right, head);
process(head.left, fatherMap);
process(head.right, fatherMap);
}
- 微软面试题,打印折纸折痕
//从上至下打印折纸折痕凹凸情况
public static void printAllFolders(int N){
printProcess(1,N,true);
}
//满二叉树
//i为当前节点层数,N为折纸次数即树的深度,down为true 凹
private static void printProcess(int i, int N, boolean down) {
//层数大于树深,直接返回
if (i>N) return;
//中序遍历
printProcess(i+1, N, true);
System.out.print(down?"凹":"凸");
printProcess(i+1, N, false);
}
- 给你一个整数数组 nums ,其中元素已经按 升序 排列,请你将其转换为一棵 高度平衡 二叉搜索树。
public TreeNode sortedArrayToBST(int[] nums) {
return dfs(nums, 0, nums.length - 1);
}
//因为要满足平衡树,因此要选中位值为头节点
//为满足搜索树,左子树的节点从mid左边找,右子树的节点从mid右边找
private TreeNode dfs(int[] nums, int l, int r) {
if (l > r) return null; //base 左边界大于右边界返回空
int mid = l + ((r - l) >> 1);
TreeNode head = new TreeNode(nums[mid]);
//递归构建左右子树
head.left = dfs(nums, l, mid - 1);
head.right = dfs(nums, mid + 1, r);
return head;
}
- 给定一个二叉树,找出其最小深度。(递归)
- 最小深度是从根节点到最近叶子节点的最短路径上的节点数量。
public int minDepth(TreeNode root) {
if (root == null) return 0;
//当前节点为叶子节点
if (root.left == null && root.right == null) return 1;
int l = minDepth(root.left);
int r = minDepth(root.right);
//当前节点只有一个孩子 l、r中必然有一个为0
if (root.left == null || root.right == null) {
return l + r + 1;
}
//两个孩子都在
return Math.min(l, r) + 1;
}
- 给你一个二叉树的根节点 root ,按任意顺序 ,返回所有从根节点到叶子节点的路径。
- 递归方法!!!
public class leetcode257 {
public List<String> binaryTreePaths(TreeNode root) {
List<String> paths = new ArrayList<>();
constructPaths(root, "", paths);
return paths;
}
private void constructPaths(TreeNode root, String path, List<String> paths) {
if (root != null) {
StringBuilder pathSB = new StringBuilder(path);
pathSB.append(Integer.toString(root.val));
//当前节点为叶子节点
if (root.left == null && root.right == null) {
paths.add(pathSB.toString());
} else {
pathSB.append("->");
constructPaths(root.left, pathSB.toString(), paths);
constructPaths(root.right, pathSB.toString(), paths);
}
}
}
}
- 给你一个二叉树的根节点
root
,按 任意顺序 ,返回所有从根节点到叶子节点的路径。
public List<String> binaryTreePaths(TreeNode root) {
List<String> paths = new ArrayList<>();
constructPath(root, "", paths);
return paths;
}
private void constructPath(TreeNode root, String path, List<String> paths) {
if (root != null) {
StringBuilder pathSB = new StringBuilder(path);
pathSB.append(Integer.toString(root.val));
//叶子节点
if (root.left == null && root.right == null) {
paths.add(pathSB.toString());
} else {
pathSB.append("->");
constructPath(root.left, pathSB.toString(), paths);
constructPath(root.right, pathSB.toString(), paths);
}
}
}
- 给你一个二叉搜索树的根节点 root ,返回 树中任意两不同节点值之间的最小差值 。
public class leetcode530 {
int ans, pre;
public int getMinimumDifference(TreeNode root) {
ans = Integer.MAX_VALUE;
pre = - 1;
dfs(root);
return ans;
}
//中序遍历
private void dfs(TreeNode x) {
if (x == null) return;
dfs(x.left);
update(x.val); //处理逻辑
dfs(x.right);
}
private void update(int x) {
//第一个节点
if (pre == - 1) {
pre = x;
} else {
ans = Math.min(x - pre, ans);
pre = x;
}
}
}
- 给你一个二叉树的根节点 root ,计算并返回 整个树 的坡度 。
/**
* 给你一个二叉树的根节点 root ,计算并返回 整个树 的坡度 。
* 一个树的 节点的坡度 定义即为,该节点左子树的节点之和和右子树节点之和的 差的绝对值 。如果没有左子树的话,左子树的节点之和为 0 ;没有右子树的话也是一样。空结点的坡度是 0 。
* 整个树 的坡度就是其所有节点的坡度之和。
*/
public class leetcode563 {
public int findTilt(TreeNode root) {
if (root == null) return 0;
//返回 当前坡度+左子树坡度和+右子树坡度和
return Math.abs(sum(root.left) - sum(root.right)) + findTilt(root.left) + findTilt(root.right);
}
public int sum(TreeNode x) {
if (x == null) return 0;
return sum(x.left) + sum(x.right) + x.val;
}
//坡度差之和
int res;
public int findTilt2(TreeNode root) {
dfs(root);
return res;
}
//dfs就是上面的sum方法,多了一步把每个节点的坡度差累加到res中。
private int dfs(TreeNode x) {
if (x == null) return 0;
int sumLeft = dfs(x.left);
int sumRight = dfs(x.right);
//计算每个节点坡度,并累加
res += Math.abs(sumLeft - sumRight);
return sumLeft + sumRight + x.val;
}
}
- 给定一棵二叉树,你需要计算它的直径长度。
/**
* 给定一棵二叉树,你需要计算它的直径长度。 求长度,因此求出的节点个数最后要减 1
* 一棵二叉树的直径长度是任意两个结点路径长度中的最大值。这条路径可能穿过也可能不穿过根结点。
*/
public class leetcode543 {
//两次递归
public int diameterOfBinaryTree(TreeNode root) {
return f(root) - 1;
}
public int f(TreeNode root) {
if (root == null) return 0;
//过头节点
int max1 = dfs(root.left) + dfs(root.right) + 1;
//不过头节点
int max2 = Math.max(f(root.left), f(root.right));
return Math.max(max1, max2);
}
//查看以x为头的树的高度
private int dfs(TreeNode x) {
if (x == null) return 0;
return Math.max(dfs(x.left), dfs(x.right)) + 1;
}
//单次递归方法 (声明一个全局变量,用于记录最长路径节点数)
int res;
public int diameterOfBinaryTree2(TreeNode root) {
res = 1;
depth(root);
return res - 1;
}
private int depth(TreeNode x) {
if (x == null) return 0;
int L = depth(x.left);
int R = depth(x.right);
res = Math.max(res, L + R + 1); //过当前节点时该子树最大深度 遍历每个节点
return Math.max(res, Math.max(L, R) + 1);
}
}
- 给你两棵二叉树 root 和 subRoot 。
- 没有优化
给你两棵二叉树 root 和 subRoot 。
检验 root 中是否包含和 subRoot 具有相同结构和节点值的子树。如果存在,返回 true ;否则,返回 false 。
public boolean isSubtree(TreeNode r, TreeNode s) {
return dfs(r, s);
}
private boolean dfs(TreeNode r, TreeNode s) {
if (r == null) return false;
return dfs(r.left, s) || dfs(r.right, s) || check(r, s);
}
//判断两课树是否相同
private boolean check(TreeNode r, TreeNode s) {
if (r == null && s == null) return true;
//只有一个为空,或者 两者值不相等
if (r == null || s == null || r.val != s.val) return false;
return check(r.left, s.left) && check(r.right, s.right);
}
树形DP套路
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-XCNL1uJr-1665234185479)(C:\Users\DFL\AppData\Roaming\Typora\typora-user-images\image-20221005201817946.png)]
位运算
/**
* 输入是一个无符号整数(以二进制串的形式),返回其二进制表达式中数字位数为 '1' 的个数
* 算术右移 >> :舍弃最低位,高位用符号位填补;
* 逻辑右移 >>> :舍弃最低位,高位用 0 填补。 符号左边表示要移动的二进制数,符号右边表示要移动多少位。
*/
public int hammingWeight1(int n) {
int res = 0;
while (n != 0) {
res += n & 1;
//逻辑右移
n >>>= 1;
}
return res;
}
public int hammingWeight2(int n) {
int res = 0;
while (n != 0) {
++ res;
//消除最右侧的1
n &= (n - 1);
}
return res;
}
/**
* 给定两个有符号32位整数a和b,不能使用算术运算符,分别实现a和b的加、减、乘、除运算
*/
public static int add(int a, int b) {
int sum = 0;
while (b != 0) { //当进位信息为0时,表示不用加了,无进位相加结果即为最终求和结果
sum = a ^ b; //sum记录无进位相加结果
b = (a & b) << 1; //b记录进位信息
a = sum; //得到进位信息和无进位相加结果后,需要相加得到最终结果,因此继续上面操作
}
return sum;
}
public static int minus(int a, int b) {
return add(a, negNum(b));
}
private static int negNum(int n) {
return add(~ n, 1); //一个数的相反数为,它自己取反后加1
}
public static int multi(int a, int b) {
int res = 0;
while (b != 0) {
if ((b & 1) != 0) { //b末尾为1就累加a
res = add(res, a);
}
a <<= 1;
b >>>= 1; //左移右移保证了结果不变,每次把b中的1移到末尾,然后乘上a右移变大后的结果
}
return res;
}
private static boolean isNeg(int n) {
return n < 0;
}
// 代码一直跑,有问题啊
public static int div(int a, int b) {
int x = isNeg(a) ? negNum(a) : a;
int y = isNeg(b) ? negNum(b) : b;
int res = 0;
for (int i = 31; i > - 1; minus(i, 1)) {
if ((x >> i) >= y) { //y右移不安全,可能出现负数
res |= (1 << i); //x右移前从左往右第一个1,肯定是原来另一个因子那个位置(1<<i)上有1,才让它能移动这么多位
x = minus(x, y << i); //让x等于减后的值继续循环
}
}
//符号不同
return isNeg(a) ^ isNeg(b) ? negNum(res) : res;
}
public static int divide(int a, int b) {
if (b == 0) {
throw new RuntimeException("divisor is 0");
}
if (a == Integer.MIN_VALUE && b == Integer.MIN_VALUE) {
return 1;
} else if (b == Integer.MAX_VALUE) {
return 0;
} else if (a == Integer.MIN_VALUE) {
int res = div(add(a, 1), b);
return add(res, div(minus(res, b), b));
}
return div(a, b);
}
//交换
public static void sway(int[] arr,int i,int j){
if(i!=j){
//不能两个值指向同一地址
arr[i]=arr[i]^arr[j];
arr[j]=arr[i]^arr[j];//就是arr[i]^arr[j]^arr[j]就表示a
arr[i]=arr[i]^arr[j];//表示arr[i]^arr[j]^arr[i]^arr[j]^arr[j]就是b
}
}
// mostRightOne值在二进制位上的位次就是pos得最右第一个1的位置
int mostRightOne = pos & (~pos + 1);
//消除二进制末尾的 1
pos & (pos-1)
//整数二进制奇数位偶数位交换
private static int process(int pos) {
int pre = 0xAAAAAAAA; // 1010 1010 1010 1010 1010 1010 1010 1010
int post = 0x55555555; // 0101 0101 0101 0101 0101 0101 0101 0101
pre &= pos;
post &= pos;
pre >>= 1;
post <<= 1;
return pos + post;
}
//给定一个整数数组,只有一个数出现了一次,其他数字均出现了三次,输出这一个只出现一次的数。
public static int twoSingleNum(int[] arr) {
int[] bit = new int[32];// 每一位求和
for (int a : arr) {
int b = 1;
for (int i = 31; i >= 0; --i) {
if ((a & b) != 0) {// 为1就累加
++bit[i];
}
b <<= 1;// 换位
}
}
int res = 0;
for (int i = 0; i < 32; ++i) {
res = res << 1;
res += (bit[i] % 3);// 取余数
}
return res;
}
**
* 给你两个二进制字符串 a 和 b ,以二进制字符串的形式返回它们的和。
* 输入:a = "11", b = "1"
* 输出:"100"
*/
public class leetcode67 {
public String addBinary(String a, String b) {
//求出累加次数
int maxLength = Math.max(a.length(), b.length());
StringBuilder sb = new StringBuilder();
int carry = 0; //carry % 2 为本位数字,carry / 2 为下一位的进位
for (int i = 0; i < maxLength; i++) {
// i小于各自长度 从最后一位开始计算,
carry += i < a.length() ? (a.charAt(a.length() - 1 - i) - '0') : 0;
carry += i < b.length() ? (b.charAt(b.length() - 1 - i) - '0') : 0;
//这里注意转化为字符,carry%2加上个'0';
sb.append((char) (carry % 2 + '0'));
//下一位的进位信息
carry /= 2;
}
if (carry > 0) {
sb.append('1');
}
sb.reverse();
return sb.toString();
}
// public String toString(int radix) radix默认十进制 //在线OJ(Online Judge)貌似不支持
public static String addBinary2(String a, String b) {
//将a、b由二进制转为十进制,然后调用toString(int radix),将十进制数转为指定进制字符串
return new BigInteger(a, 2).add(new BigInteger(b, 2)).toString(2);
}
}
/**
* 给你一个整数 n ,对于 0 <= i <= n 中的每个 i ,计算其二进制表示中 1 的个数 ,返回一个长度为 n + 1 的数组 ans 作为答案。
* 输入:n = 2
* 输出:[0,1,1]
* 解释:
* 0 --> 0
* 1 --> 1
* 2 --> 10
*/
public class leetcode338 {
//1、朴素
public int[] countBits(int n) {
int[] bits = new int[n + 1];
for (int i = 0; i <= n; i++) {
bits[i] = countOnes(i);
}
return bits;
}
private int countOnes(int x) {
int ones = 0;
while (x > 0) {
ones++;
x &= (x - 1); //消除最右侧的1
}
return ones;
}
//2、DP 右移后补上右移操作丢掉的最末尾的数
public int[] countBits2(int n) {
int[] bits = new int[n + 1];
// bits[0] = 0; 也可以不设置,因为int数组每个元素默认初始化都是0
for (int i = 1; i <= n; i++) { //对于正整数 x,将其二进制表示右移一位,等价于将其二进制表示的最低位去掉
bits[i] = bits[i >> 1] + (i & 1); //先取出最后一位数,奇数为1,偶数为0,当前数1的个数等于该数右移后1的个数+移动前最末尾的数
}
return bits;
}
//3、DP 消除最右侧1后加 1
public int[] countBits3(int n) {
int[] bits = new int[n + 1];
// bits[0] = 0;
for (int i = 1; i <= n; i++) {
bits[i] = bits[i & i - 1] + 1; //i & i - 1 消除最末尾的1,然后在末尾处在加上1,保持与原来的数1的个数相同
}
return bits;
}
public static void main(String[] args) {
int[] bits = new int[4];
for (int i = 0; i < bits.length; i++) {
System.out.println(bits[i]);
}
}
}
/**
* 给定一个整数,写一个函数来判断它是否是 4 的幂次方。如果是,返回 true ;否则,返回 false 。
*/
public static boolean isPowerOfFour1(int n) {
//n小于等于0 false ,n不是2的整数幂 false 二进制表示中 1在偶数位 false
return n > 0 && (n & (n - 1)) == 0 && (n & (0xAAAAAAAA)) == 0;
}
public static boolean isPowerOfFour2(int n) {
//n小于等于0 false ,n不是2的整数幂 false n^x % (n-1) 恒等于 1,相当与每个都n都%(n-1)然后相乘;
return n > 0 && (n & (n - 1)) == 0 && n % 3 == 1;
}
/**
* 不使用比较符号,选出两数较大值,运用了两互斥条件相加来完成if else操作
*/
public static int getMax(int a, int b) {
int c = a - b;
int sa = sign(a); //正数为1,负数为0 0、1起到了相加互斥的效果
int sb = sign(b);
int sc = sign(c);
int difSab = sa ^ sb; //相同为0,不同为1
int sameSab = flip(difSab); //相同为1,不同为0
//返回a的条件 ab符号不同 a>0返回a,ab符号相同 a-b>=0 返回a
int returnA = difSab * sa + sameSab * sc; //互斥,用加号 a>b返回 a a大于0,sa才会为1,总的结果为1 c大于0,sc才会为1,总的结果为1
int returnB = flip(returnA);
return a * returnA + b * returnB;
}
private static int flip(int x) {
return x ^ 1; //符号取反
}
private static int sign(int x) {
return flip((x >> 31) & 1); //正数为1,负数为0
}
奇怪题
/**
* 对于一个正整数,每一次将该数替换为它每个位置上的数字的平方和。
* 然后重复这个过程直到这个数变为 1,也可能是 无限循环 但始终变不到 1。
* 如果这个过程 结果为 1,那么这个数就是快乐数。
*/
//本题关键点为 totalSum 不会越来越大 要么变为1 要么死循环
public class leetcode202 {
public boolean isHappy(int n) {
HashSet<Integer> seen = new HashSet<>();
//没有变成 1 或者 没有进入死循环
while (n != 1 && ! seen.contains(n)) {
seen.add(n);
n = getNext(n);
}
return n == 1;
}
private int getNext(int n) {
int totalSum = 0;
while (n != 0) {
int d = n % 10;
n /= 10;
totalSum += d * d;
}
return totalSum;
}
//隐式链表 判断有环无环
public boolean isHappy2(int n) {
int slow = n;
int fast = getNext(n);
while (fast != 1 && fast != slow) {
slow = getNext(n);
fast = getNext(getNext(fast));
}
return fast == 1;
}
}
/**
* 丑数 就是只包含质因数 2、3 和 5 的正整数。
* 给你一个整数 n ,请你判断 n 是否为 丑数 。如果是,返回 true ;否则,返回 false 。
* 丑数可以表示为2^a * 3^b * 5^c
*/
public boolean isUgly(int n) {
if (n <= 0) return false;
int[] factors = {2, 3, 5};
for (int factor : factors) {
//先一直除一个因数,如果一直能除尽,则n = 1,除不尽换另一个因数尝试,
while (n % factor == 0) {
n /= factor;
}
}
return n == 1;
}
链表
dummyNode
- 在头节点钱可以设置一个空节点dummyNode,这样就可以避免一些问题中的头节点特殊处理,比如在K个一组反转链表问题中:
但是对于第一个子链表,它的头节点 head 前面是没有节点 pre 的。太麻烦了!难道只能特判了吗?答案是否定的。没有条件,我们就创造条件;没有节点,我们就创建一个节点。我们新建一个节点,把它接到链表的头部,让它作为 pre 的初始值,这样 head 前面就有了一个节点,我们就可以避开链表头部的边界条件。这么做还有一个好处,下面我们会看到。
/**
* 给你一个链表的头节点 head 和一个整数 val ,请你删除链表中所有满足 Node.val == val 的节点,并返回 新的头节点 。
*/
public class leetcode203 {
//递归,先对头节点以外的元素进行删除操作
public ListNode removeElements1(ListNode head, int val) {
if (head == null) return null;
//head 抓住其他节点删除操作后返回的新头
head.next = removeElements1(head.next, val);
//如果head值等于val,则移除head
return head.val == val ? head.next : head;
}
public ListNode removeElements2(ListNode head, int val) {
ListNode dummyNode = new ListNode(0);
dummyNode.next = head;
ListNode cur = dummyNode;
while (cur.next != null) {
if (cur.next.val == val) {
//移除链表当前节点下一个元素
cur.next = cur.next.next;
} else {
cur =cur.next;
}
}
return dummyNode.next;
}
}
/**
* 给你单链表的头节点 head ,请你反转链表,并返回反转后的链表。
*/
public class leetcode206 {
public ListNode reverseList1(ListNode head) {
//记录前驱节点
ListNode pre = null;
ListNode cur = head;
while (cur != null) {
//记录后继节点
ListNode next = cur.next;
//此时可以放心修改cur.next指向
cur.next = pre;
//pre、cur 都向后移动
pre = cur;
cur = next;
}
return pre;
}
/**
* A->B->C->D->E
* after reversing
* A->B->NULL NULL<-B<-C<-D<-E A、B之间构成双向链表
*/
public ListNode reverseList2(ListNode head) {
if (head == null || head.next == null) return head; //baseCase
//以B为头节点的链表反转后,A仍然持有B的引用 A.next = B
ListNode newHead = reverseList2(head.next);
// 令 A<-B
head.next.next = head;
//令 NULL<-A
head.next = null;
return newHead;
}
}
/**
* 给你链表的头节点 head ,每k个节点一组进行翻转,请你返回修改后的链表。
* k 是一个正整数,它的值小于或等于链表的长度。如果节点总数不是k的整数倍,那么请将最后剩余的节点保持原有顺序。
*/
public class leetcode25 {
public ListNode reverseKGroup(ListNode head, int k) {
ListNode hair = new ListNode(0);
hair.next = head;
ListNode pre = hair; //pre指向子链表反转后的末尾位置,用于拼接后面的串
//不足k个直接返回原先的头节点
while (head != null) {
ListNode tail = pre; //每次从pre位置跑k步指向第k个节点位置
for (int i = 0; i < k; i++) {
tail = tail.next;
if (tail == null) {
return hair.next;
}
}
ListNode next = tail.next; //pre tail两个变量记录反转前子串头节点前一个位置、结尾位置,用于将反转后的子串拼接回原链表
ListNode[] reverse = myReverse(head, tail);
//反转后新头尾
head = reverse[0];
tail = reverse[1];
//重新将子串拼接回链表
pre.next = head;
tail.next = next;
pre = tail; //下一组
head = tail.next;
}
return hair.next;
}
private ListNode[] myReverse(ListNode head, ListNode tail) {
ListNode pre = tail.next; //反转后新的尾节点指向原来尾节点下一个
ListNode cur = head;
while (pre != tail) { //当前节点cur来到了尾部
ListNode next = cur.next;
cur.next = pre;
pre = cur;
cur = next;
}// 新头 新尾
return new ListNode[]{tail, head};
}
}
字符串
- 经常地,可以创建一个长度26的字母频次统计数组,index为字母 -‘ a’,值为字母出现频次,也可以创建128的char[ ]数组。
/**
* 给定两个字符串 s 和 t ,判断它们是否是同构的。 潜台词:字符是否一一映射。
* example: egg add
* 一个map不能保证一一映射
*/
// 两个map
public boolean isIsomorphic(String s, String t) {
Map<Character, Character> s2t = new HashMap<Character, Character>();
Map<Character, Character> t2s = new HashMap<Character, Character>();
int len = s.length();
for (int i = 0; i < len; ++i) {
char x = s.charAt(i), y = t.charAt(i);
if ((s2t.containsKey(x) && s2t.get(x) != y) || (t2s.containsKey(y) && t2s.get(y) != x)) {
return false;
}
s2t.put(x, y);
t2s.put(y, x);
}
return true;
}
//通过indexOf(); 判断元素第一次出现位置是否相同
public boolean isIsomorphic2(String s, String t) {
int n = s.length();
for (int i = 0; i < n; i++) {
//index Of();没有找到返回=1
if (s.indexOf(s.charAt(i)) != t.indexOf(t.charAt(i))) { //关键
return false;
}
}
return true;
}
/** 与上题解法2异曲同工 都是记录元素第一次出现的位置,在这个位置第一次确定了两个元素的映射关系
*
* 给定一种规律 pattern和一个字符串s,判断 s是否遵循相同的规律。 abba "cat dog dog cat"
* 这里的遵循指完全匹配,例如,pattern里的每个字母和字符串s中的每个非空单词之间存在着双向连接的对应规律。
* public V put(K key, V value) { 如果key不存在,插入成功,返回null;如果key存在,返回之前对应的value。
* return putVal(hash(key), key, value, false, true);
* }
*/
public static boolean wordPattern(String pattern, String s) {
String[] words = s.split(" ");
if (words.length != pattern.length()) return false;
//key 为元素,value为第一次出现是的数组下标
HashMap<Object, Integer> map = new HashMap<>();
for (Integer i = 0; i < pattern.length(); i++) {
//如果key不存在,插入成功,返回null;如果key存在,返回之前对应的value。
//value 为元素第一次出现时的数组下标
if (map.put(pattern.charAt(i), i) != map.put(words[i], i)) {
return false;
}
}
return true;
}
/**
* 给你两个字符串:ransomNote 和 magazine ,判断 ransomNote 能不能由 magazine 里面的字符构成。
* 如果可以,返回 true ;否则返回 false 。 其实就是判断 ransomNote是不是magazine的顺序打乱了的子串
* magazine 中的每个字符只能在 ransomNote 中使用一次。
*/
public boolean canConstruct(String ransomNote, String magazine) {
if (ransomNote.length() > magazine.length()) return false;
//创建长度26的数组,用来统计magazine中每个字母出现的次数,下标为 字母-'a',值为出现次数
int[] cnt = new int[26];
//统计magizine中字母出现次数
for (char c : magazine.toCharArray()) {
cnt[c - 'a']++;
}
//如果ransomNote 中某一字母出现次数比magazine 直接false
for (char c : ransomNote.toCharArray()) {
cnt[c - 'a']--;
if (cnt[c - 'a'] < 0) {
return false;
}
}
return true;
}
/**
* 给定一个字符串 s ,找到 它的第一个不重复的字符,并返回它的索引 。如果不存在,则返回 -1 。
*/
public int firstUniqChar(String s) {
char[] dict = new char[26];
//第一遍统计字母出现次数 loveleetcode
221424411214
for (int i = 0; i < s.length(); i++) {
int index = s.charAt(i) - 'a';
dict[index]++;
}
//第二遍 查字典 找出只出现一次的字母 还是按原来字符串顺序一个个查找,因此能保证找到第一个不重复的字符
for (int i = 0; i < s.length(); i++) {
int index = s.charAt(i) - 'a';
if (dict[index] == 1) {
return i;
}
}
return - 1;
}
贪心策略
- 最常用的就是堆、排序
/*
切割金条花费最少问题
*/
public static int lessMoney(int[] arr) {
//小根堆
PriorityQueue<Integer> pQ = new PriorityQueue<>();
for (int i : arr) {
pQ.add(i);
}
int cur = 0;
int sum = 0;
while (pQ.size() > 1) {
//每次弹出两个做结合,将值放入小根堆中直至小根堆数量为1
cur = pQ.poll()+ pQ.poll();
sum +=cur;
pQ.add(sum);
}
return sum;
}
/**
* 从当前给定项目中挑选出一些使手中资金最大化。
*/
public class IPO {
public static class Node {
public int profit;
public int cost;
public Node(int profit, int cost) {
this.profit = profit;
this.cost = cost;
}
}
public static class MinCostComparator implements Comparator<Node> {
@Override
public int compare(Node o1, Node o2) {
return o1.cost - o2.cost;
}
}
public static class MaxProfitComparator implements Comparator<Node> {
@Override
public int compare(Node o1, Node o2) {
return o2.profit - o1.profit;
}
}
//k 进行k轮 w手中的资金 profit、capital每个项目的利润以及收益
public static int findMaximizedCapital(int k, int w, int[] profit, int[] capital) {
PriorityQueue<Node> minCostQ = new PriorityQueue<>(new MinCostComparator());
PriorityQueue<Node> maxProfitQ = new PriorityQueue<>(new MaxProfitComparator());
//将所有项目扔进由花费组织的小根堆
for (int i = 0; i < profit.length; i++) {
minCostQ.add(new Node(profit[i], capital[i]));
for (int i = 0; i < k; i++) {
//还有项目 并且 手中资金能够解锁项目
while (! minCostQ.isEmpty() && minCostQ.peek().cost <= w) {
maxProfitQ.add(minCostQ.poll());
}
//没项目能选了,提前返回
if (maxProfitQ.isEmpty()) {
return w;
}
w += maxProfitQ.poll().profit;
}
}
return w;
}
}
//也行,不过跑起来没上面性能好 使用了lambda表达式 可以简化代码
public static int findMaximizedCapital2(int k, int w, int[] profit, int[] capital) {
int n = profit.length;
int cur = 0;
int[][] arr = new int[n][2];
for (int i = 0; i < n; i++) {
//第一个表示所需资金,第二个表示可获得收益
arr[i][0] = capital[i];
arr[i][1] = profit[i];
}
//lambda表达式传入比较器 根据所需资金排个序
Arrays.sort(arr, (a, b) -> a[0] - b[0]);
//由净利润组成的大根堆
PriorityQueue<Integer> pq = new PriorityQueue<>((x, y) -> y - x);
for (int i = 0; i < k; i++) {
while (cur < n && arr[cur][0] <= w) {
pq.add(arr[i][1]);
cur++;
}
if (! pq.isEmpty()) {
w += pq.poll();
} else {
break;
}
}
return w;
}
暴力递归
- 暴力递归就是尝试
- 把问题转化为规模缩小了的同类问题的子问题
- 有明确的不需要继续进行递归的条件(base case)
- 有当得到了子问题的结果之后的决策过程
- 不记录每一个子问题的解
- 返回值往往是0 ~ i - 1位置选择确定的情况下,i 位置如何选择返回其结果
- 在尝试的过程中找到可变参数形式最简单,可变参数个数最少的试法
/**
* N皇后问题,没法做总体时间复杂度优化,可以做常数优化。
* num1() res = 365596
* cost time4244ms
* num2() res = 365596
* cost time180ms
*/
public class NQueens {
public static int num1(int n) {
int[] record = new int[n]; //每一个值表示,在第i行皇后放在那一列
return process1(0, record, n);
}
//record[0,1,2...,i-1]表示0-i-1行,皇后放在了那一列
//i表示当前在第i行
//n表示总共多少行多少列
//返回值表示 摆完所有的皇后,合理摆法有多少种
private static int process1(int i, int[] record, int n) {
if (i == n) return 1; //来到了终止行 ([0,n-1])
int res = 0;
for (int j = 0; j < n; j++) { //当前在第i行,尝试所有的列
//如果当前列可以摆,进行深度优先遍历,寻找下一行可选的摆法
if (isValid(record, i, j)) {
record[i] = j;
res += process1(i + 1, record, n);
}
}
return res;
}
private static boolean isValid(int[] record, int i, int j) {
//遍历之前的行 看之前所摆的皇后与第i行皇后是否同列或共斜线
for (int k = 0; k < i; k++) {
if (record[k] == j || Math.abs(record[k] - j) == Math.abs(i - k)) {
return false;
}
}
return true;
}
//方法二:使用位运算
public static int num2(int n) {
if (n < 1 || n > 32) {
return 0;
}
//限制可选范围 后n位是1,前面都是0 该步骤旨在将可供选择的列限制在指定范围内,后面与其他限制取 & 时 前面的0不造成影响
int limit = n == 32 ? - 1 : (1 << n) - 1; -1的二进制表示为32个1 (1 << n)注意时1左移,1放前边
return process2(limit, 0, 0, 0);
}
//1地位置可以放皇后,0的位置不行。
private static int process2(
int limit, //总的可选范围
int colLim, //列限制
int leftDiaLim, //左对角线限制
int rightDialim) { //右对角线限制
//base case
if (colLim == limit) return 1; //列限制数量与可供选择列数量相同,该结果为一个合法的结果。
int mostRightOne = 0;
//当前可供选择的列 在总的可选范围上 排除其他限制
int pos = limit & (~ (colLim | leftDiaLim | rightDialim));
int res = 0;
//还有位置可选时
while (pos != 0) {
mostRightOne = pos & (~ pos + 1); //从最右侧1开始遍历
pos -= mostRightOne; //消除最右侧1,pos移到下一个1继续尝试
res += process2(limit,
colLim | mostRightOne,
(leftDiaLim | mostRightOne) << 1,
(rightDialim | mostRightOne) >>> 1); //无符号右移 32皇后最高位为1 右移后最高位拿0来补
}
return res;
}
public static void main(String[] args) {
int n = 14;
long start = System.currentTimeMillis();
System.out.println(num1(n));
long end = System.currentTimeMillis();
System.out.println("cost time" + (end - start)+"ms");
start = System.currentTimeMillis();
System.out.println(num2(n));
end = System.currentTimeMillis();
System.out.println("cost time" + (end - start)+"ms");
}
}
/**
* Tower of Hanoi
Move 1 from 左 to 中
Move 2 from 左 to 右
Move 1 from 中 to 右
Move 3 from 左 to 中
Move 1 from 右 to 左
Move 2 from 右 to 中
Move 1 from 左 to 中
*/
public static void hanoi(int n) {
if (n > 0) {
func(n, "左", "中", "右"); //从左移动到中
}
}
//i为盘子编号,从上至下1~n 每次移动保证自己的底不违规
private static void func(int i, String from, String to, String other) {
if (i == 1) {
System.out.println("Move 1 from " + from + " to " + to);
} else {
//先把第i-1个圆盘从from移动到other上去 //至于0~i-2个圆盘怎么移动由子过程决定
func(i - 1, from, other, to);
//再将最下面第i个圆盘移动到to位置
System.out.println("Move " + i + " from " + from + " to " + to);
//最后将第i-1个圆盘从other移动到to位置
func(i - 1, other, to, from);
}
}
/**
* 单词所有子序列
*/
public static void printAllSubsequence1(String s) {
char[] chars = s.toCharArray();
List<String> list = new ArrayList<>();
process1(chars, 0, list);
System.out.println(list);
}
private static void process1(char[] str, int i, List<String> res) {
if (i == str.length) {
res.add(String.valueOf(str));
}
boolean[] isVisit = new boolean[26];
for (int j = i; j < str.length; j++) { //在i及后面位置选一个放到i位置,后面自由排列
// 分支限界
if (! isVisit[str[j] - 'a']) {
isVisit[str[j] - 'a'] = true; //去重
swap(i, j, str);
process1(str,i+1,res);
swap(i, j, str);
}
}
}
private static void swap(int i, int j, char[] str) {
char temp = str[i];
str[i] = str[j];
str[j] = temp;
}
public static void printAllSubsequence2(String s) {
char[] chars = s.toCharArray();
process2(chars, 0);
}
private static void process2(char[] str, int i) {
if (i == str.length) {
System.out.println(String.valueOf(str)); //String.valueOf();将其他类型变量转为字符串
return; //base case 别因为打印 忘了要return!!!
}
process2(str, i + 1); //保留当前字母
char temp = str[i];
str[i] = 0;
process2(str, i + 1); //不保留当前字母
str[i] = temp;
}
/**
* 两个绝顶聪明的人,从左右两边拿牌,求获胜者最大值
*/
public class CardsInLIne {
public static int win1(int[] arr) {
if (arr == null || arr.length == 0) return 0;
// 玩家A先手 玩家B先手
return Math.max(f(arr, 0, arr.length - 1), s(arr, 0, arr.length - 1));
}
//先手函数 玩家A
private static int f(int[] arr, int l, int r) {
if (l == r) return arr[l];
// 选择后手函数中最大的 拿走左边的 拿走右边的
return Math.max(arr[l] + s(arr, l + 1, r), arr[r] + s(arr, l, r - 1));
}
//后手函数 玩家B
private static int s(int[] arr, int l, int r) {
if (l == r) return 0; //只剩一张牌,但因为自己是后手,因此没牌可拿了,返回0
//轮到自己先手了,因为绝顶聪明,因此之前先手的人会让后手的人拿到最少
return Math.min(f(arr, l + 1, r), f(arr, l, r - 1));
}
/**
* 1和A对应,2和B对应,3和C对应
* "111" 可以转换为AAA、AK、KA
*/
public class NumToStr {
public static int numToStr(String num) {
char[] chs = num.toCharArray();
return process(0, chs);
}
//返回值:此时来到i位置,i-1上做的决定所构成的结果
private static int process(int i, char[] str) {
if (i == str.length) return 1; //之前做的决定 整体构成一个有效结果
//之前做的决定导致在i位置上无法做决定,整体无效
if (str[i] == '0') {
return 0;
}
if (str[i] == '1') {
//i位置单独转换
int res = process(i + 1, str); //i+1可能等于数组长度,i+1=len 符合第一个if
if (i + 1 < str.length) { //如果i+1小于len,那么i+2就有结果
//i与i+1位置合并一起转换
res += process(i + 2, str);
}
return res;
}
if (str[i] == '2') {
//i位置单独转换
int res = process(i + 1, str);
//i与i+1位置合并一起转换,且0<str[i+1]<6时才合法
if (i + 1 < str.length && str[i + 1] <= '6') {
res += process(i + 2, str);
}
}
//i位置数>=3,只能单独转化
return process(i + 1, str);
}
//dp版本
public static int dpWays(int num) {
if (num < 1) return 0;
char[] str = String.valueOf(num).toCharArray();
int N = str.length;
int[] dp = new int[N + 1];
dp[N] = 1; //从N位置出发有多少种可能性
dp[N - 1] = str[N - 1] == '0' ? 0 : 1;
for (int i = N - 2; i >= 0; i--) { 从后往前计算
if (str[i] == '0') {
dp[i] = 0;
} else {
dp[i] = dp[i + 1] +
(((str[i] - '0') * 10 + str[i + 1] - '0') < 27 ? dp[i + 2] : 0);
}
}
return dp[0];
}
//0~i-1位置确定的情况下,i位置如何选择
/**
* 给定两个长度都为N的数组weights和values,weights[i]和values[i]分别代表i号物品的重量和价值。
* 给定一个正数bag,表示一个载重bag的袋子,你装的物品不能超过这个重量。返回你能装下最多的价值是多少?
*/
public class MaxValue {
public static int maxValue(int[] weights, int[] values, int bag) {
return process1(0, 0, weights, values, bag);
}
//返回值:i...的货物自由选择,返回在该区间可以获得的最大价值
private static int process1(int i, int alreadyWeights, int[] weights, int[] values, int bag) {
if (alreadyWeights >= bag) return 0;
if (i == values.length) return 0;
return Math.max(
//要该商品
values[i] + process1(i + 1, alreadyWeights + weights[i], weights, values, bag),
//不要该商品
process1(i + 1, alreadyWeights, weights, values, bag)
);
}
public class leetcode39 {
public static List<List<Integer>> combinationSum(int[] candidates, int target) {
List<List<Integer>> res = new ArrayList<>();
List<Integer> already = new ArrayList<>();
dfs(0, already, res, candidates, target);
return res;
}
/**
* 给你一个 无重复元素 的整数数组candidates 和一个目标整数target,找出candidates中可以使数字和为目标数target的所有不同组合 ,并以列表形式返回。
* 你可以按 任意顺序返回这些组合。
* candidates 中的同一数字可以 无限制重复被选取 。如果至少一个数字的被选数量不同,则两种组合是不同的。
*/
public static List<List<Integer>> combinationSum(int[] candidates, int target) {
List<List<Integer>> res = new ArrayList<>();
List<Integer> already = new ArrayList<>();
dfs(0, already, res, candidates, target);
return res;
}
//already:已经组合的列表
private static void dfs(int i, List<Integer> already, List<List<Integer>> res, int[] candidates, int target) {
//超过了直接返回
if (i == candidates.length) {
return;
}
if (target == 0) {
// 将already加入到新数组中 不能直接add(already),非基本类型传入的是引用,会在原列表上修改,因此要复制一份
res.add(new ArrayList<Integer>(already));
return;
}
//直接跳过
dfs(i + 1, already, res, candidates, target);
//选择当前值
if (target >= candidates[i]) {
already.add(candidates[i]);
//重复利用,还停留在i位置
dfs(i, already, res, candidates, target - candidates[i]);
//加入后重新移除该元素,使该元素可重复使用
already.remove(already.size() - 1);
}
}
动态规划 P17
-
暴力递归try -> 记忆化搜索(DP)-> 严格表结构(DP) -> 严格表精版(DP)
-
严格表结构 一定要画图!!!!!!!
- 描述可变范围,暴力递归中有几个可变参数就是几维表
- 标出需要计算的终止位置
- 标出不用计算直接出答案的位置 base case,先把已知位置填好
- 分析普通位置的依赖关系,并根据依赖关系、已知数据以及终止位置,得出填表策略,确定填表顺序
- 顺序确定后,将递归调用返回结果改为填表
- 注意填表过程中可能出现的越界问题
-
范围尝试的表,左下半区一律无效(即左边界不能大于右边界)、已知值base case一般为对角线
-
在进行斜率优化时,只需从表中数据找规律即可,没必要回溯原题意
-
可变参数越少越好(表的维度),单可变参数的维度(一个点(即一个整数是最好的)或一个数组等)
-
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ySl2urta-1665234185480)(C:\Users\DFL\AppData\Roaming\Typora\typora-user-images\image-20221006183417888.png)]
-
/**
* 机器人走路问题,暴力递归 vs 记忆化搜索
*/
public class RobotWalk {
// N个位置 结束 开始 走k步
public static int robotWalk1(int N, int E, int S, int K) {
return f1(N, E, S, K);
}
private static int f1(int N, int E, int cur, int rest) {
if (rest == 0) {
return cur == E ? 1 : 0;
}
if (cur == 1) {
return f1(N, E, 2, rest - 1);
}
if (cur == N) {
return f1(N, E, N - 1, rest - 1);
}
return f1(N, E, cur - 1, rest - 1) + f1(N, E, cur + 1, rest - 1);
}
//记忆化搜索
public static int robotWalk2(int N, int E, int S, int K) {
int[][] dp = new int[N + 1][K + 1];
for (int i = 0; i <= N; i++) { //都要带等号,初始全部变成 -1
for (int j = 0; j <= K; j++) {
dp[i][j] = - 1;
}
}
return f2(N, E, S, K, dp);
}
private static int f2(int N, int E, int cur, int rest, int[][] dp) {
//判断是否有缓存,如果有 直接走缓存return
if (dp[cur][rest] != - 1) { //-1意味着没计算过
return dp[cur][rest];
}
//建缓存过程
if (rest == 0) { //没剩余步数了,到终点为1,否则为0
dp[cur][rest] = cur == E ? 1 : 0;
return dp[cur][rest];
}
//还有剩余步数rest>0
if (cur == 1) {
dp[cur][rest] = f2(N, E, 2, rest - 1, dp);
} else if (cur == N) {
dp[cur][rest] = f2(N, E, N - 1, rest - 1, dp);
} else {
dp[cur][rest] = f2(N, E, cur - 1, rest - 1, dp) + f2(N, E, cur + 1, rest - 1, dp);
}
return dp[cur][rest];
}
/**
* 组成aim金额所需要的最少货币数量
*/
public class MinCoins {
public static int minCoins1(int[] arr, int aim) { //暴力递归
return f1(arr, 0, aim);
}
//index。。。后面硬币自由选择,组成rest金额所需最少硬币数
private static int f1(int[] arr, int index, int rest) {
if (rest < 0) return - 1; //没希望,之前决策就错了,无效解
if (rest == 0) return 0; //够了,不用选了 return 0为有效解
//rest>0
// 没硬币,但还不够aim
if (index == arr.length) return - 1;
//还有硬币,且还不够aim金额
int p1 = f1(arr, index + 1, rest); //不选当前硬币
int p2Next = f1(arr, index + 1, rest - arr[index]); //选当前硬币
//其中一个有效
if (p1 == - 1 && p2Next == - 1) return - 1; //下一步选哪个都没戏,这一步就无效
if (p1 == - 1) return p2Next + 1;
if (p2Next == - 1) return p1;
//都有效
return Math.min(p1, p2Next + 1);
}
public static int minCoins2(int[] arr, int aim) { //记忆化搜索
int[][] dp = new int[arr.length + 1][aim + 1];
for (int i = 0; i <= arr.length; i++) {
for (int j = 0; j <= aim; j++) {
dp[i][j] = - 2; //-2表示没算过
}
}
return f2(arr, 0, aim, dp);
}
//index。。。后面硬币自由选择,组成rest金额所需最少硬币数
private static int f2(int[] arr, int index, int rest, int[][] dp) {
//dp二维表中没有rest小于0的区域,这里用硬逻辑表示,二维表左侧的区域都是-1,中了无效区的缓存
if (rest < 0) return - 1; //rest<0
if (dp[index][rest] != - 2) { //有缓存直接走缓存
return dp[index][rest];
}
//构建缓存过程
if (rest == 0) { //rest=0
dp[index][rest] = 0;
} else if (index == arr.length) { //rest > 0 index=N
dp[index][rest] = - 1;
} else { //rest>0 index<N
//还有硬币,且 还不够aim金额
int p1 = f2(arr, index + 1, rest, dp); //不选当前硬币
int p2Next = f2(arr, index + 1, rest - arr[index], dp); //选当前硬币
//其中一个有效
if (p1 == - 1 && p2Next == - 1) {
dp[index][rest] = - 1;
} //下一步选哪个都没戏,这一步就无效
else if (p1 == - 1) {
dp[index][rest] = p2Next + 1;
} else if (p2Next == - 1) {
dp[index][rest] = p1;
} else {
//都有效
dp[index][rest] = Math.min(p1, p2Next + 1);
}
}
return dp[index][rest];
}
//动态规划 严格表依赖 初始化表、填表
//row 为index col为rest
public static int minCoins3(int[] arr, int aim) { //严格表结构
int N = arr.length;
int[][] dp = new int[N + 1][aim + 1];
//初始化边界过程
for (int row = 0; row <= N; row++) {
dp[row][0] = 0; //每一行的0列为0
}
for (int col = 1; col <= aim; col++) { //从第一列开始最后一行的每一列为-1
dp[N][col] = - 1;
}
//一般位置计算过程
for (int index = N - 1; index >= 0; index--) {
for (int rest = 1; rest <= aim; rest++) {
int p1 = dp[index + 1][rest];
int p2Next = - 1; //越界则为-1
if (rest - arr[index] >= 0) {
p2Next = dp[index + 1][rest - arr[index]];
}
if (p1 == - 1 && p2Next == - 1) {
dp[index][rest] = - 1;
} else if (p1 == - 1) {
dp[index][rest] = p2Next + 1;
} else if (p2Next == - 1) {
dp[index][rest] = p1;
} else {
dp[index][rest] = Math.min(p1, 1 + p2Next);
}
}
}
//0行
return dp[0][aim];
}
}
/**
* 规定步数马到达终点的方法数
* 2549ms 暴力递归
* ----------------------------------- 三维表
* 0ms 严格表结构
*/
public class ChineseChess {
public static int horseWalk1(int K, int x, int y) {
return process(K, x, y);
}
//从(0,0)位置要去往(x,y)位置,还需跳step步 要去往的地方
//也可以这样想,从(x,y)位置跳往(0,0)位置
public static int process(int step, int x, int y) {
if (x < 0 || x > 8 || y < 0 || y > 9) {
return 0;
}
if (step == 0) { //剩余0步,如果要跳往的位置为(0,0)就是一种合理的方法
return (x == 0 & y == 0) ? 1 : 0;
}
return + horseWalk1(step - 1, x - 1, y - 2)
+ horseWalk1(step - 1, x - 1, y + 2)
+ horseWalk1(step - 1, x - 2, y - 1)
+ horseWalk1(step - 1, x - 2, y + 1)
+ horseWalk1(step - 1, x + 1, y - 2)
+ horseWalk1(step - 1, x + 1, y + 2)
+ horseWalk1(step - 1, x + 2, y - 1)
+ horseWalk1(step - 1, x + 2, y + 1);
}
public static int horseWalk2(int step, int x, int y) {
if (x < 0 || x > 8 || y < 0 || y > 9 || step < 0) {
return 0;
}
int[][][] dp = new int[step + 1][9][10];
dp[0][0][0] = 1;
//0层不管。只有原点是1,其余都是0(初始默认)
for (int h = 1; h <= step; h++) { //层
for (int r = 0; r < 9; r++) { //枚举所有可能性 同一层不存在依赖关系
for (int c = 0; c < 10; c++) {
dp[h][r][c] += getValue(dp, h - 1, r - 1, c - 2); //getValue();防止越界
dp[h][r][c] += getValue(dp, h - 1, r - 1, c + 2);
dp[h][r][c] += getValue(dp, h - 1, r - 2, c - 1);
dp[h][r][c] += getValue(dp, h - 1, r - 2, c + 1);
dp[h][r][c] += getValue(dp, h - 1, r + 1, c - 2);
dp[h][r][c] += getValue(dp, h - 1, r + 1, c + 2);
dp[h][r][c] += getValue(dp, h - 1, r + 2, c - 1);
dp[h][r][c] += getValue(dp, h - 1, r + 2, c + 1);
}
}
}
return dp[step][x][y];
}
private static int getValue(int[][][] dp, int h, int r, int c) {
if (r < 0 || r > 8 || c < 0 || c > 9 ) {
return 0;
}
return dp[h][r][c];
}
/**
* 每个纸币自由选择,可重复选,aim金额找零的方法数
* 3937256 暴力
* 892ms
* -----------------------------------
* 3937256 严格表结构,枚举版
* 3ms
* -----------------------------------
* 3937256 严格表结构,去枚举版、斜率优化,只和观察有关,不需要在意原题意,咱就做实在人
* 0ms
*/
public class CoinsWays {
public static int way1(int[] arr, int aim) {
return process1(arr, 0, aim);
}
private static int process1(int[] arr, int index, int rest) {
if (index == arr.length) {
return rest == 0 ? 1 : 0;
}
int ways = 0;
//枚举每种面值纸币使用张数,但总金额不要超过rest 2元使用0、1、2、3、4。。。 5元使用0、1、2、3、4、5
for (int zhang = 0; arr[index] * zhang <= rest; zhang++) {
ways += process1(arr, index + 1, rest - arr[index] * zhang); //前面面值搞定了后还剩rest - arr[index] * zhang钱要搞,继续枚举
}
return ways;
}
public static int way2(int[] arr, int aim) {
if (arr == null || arr.length == 0) return 0;
int N = arr.length;
int[][] dp = new int[N + 1][aim + 1];
dp[N][0] = 1;
for (int index = N - 1; index >= 0; index--) { //从下之上填表
for (int rest = 0; rest <= aim; rest++) {
int ways = 0;
for (int zhang = 0; arr[index] * zhang <= rest; zhang++) { //根据依赖关系填表
ways += dp[index + 1][rest - arr[index] * zhang]; //前面面值搞定了后还剩rest - arr[index] * zhang钱要搞,继续枚举
}
dp[index][rest] = ways; //当前层依赖它下一层的某些值
}
}
return dp[0][aim];
}
//优化枚举行为 枚举行为中,当前位置依赖其下一行 rest - arr[index] * zhang(zhang++) 列的值 a+b+c+d+...
// 但观察发现当前行rest-1*arr[index]位置的值是b+c+d得来的,因此当前格子的值就是其下一行同一列的值+同一行、当前列值减一个当前面值的列的值
public static int way3(int[] arr, int aim) {
if (arr == null || arr.length == 0) return 0;
int N = arr.length;
int[][] dp = new int[N + 1][aim + 1];
dp[N][0] = 1;
for (int index = N - 1; index >= 0; index--) {
for (int rest = 0; rest <= aim; rest++) {
dp[index][rest] = dp[index + 1][rest];//依赖下方的格子
if (rest - arr[index] >= 0) {
dp[index][rest] += dp[index][rest - arr[index]]; //以及当前行中,当前列值减一个当前面值的列
}
}
}
return dp[0][aim];
}
基础提升
Manacher算法
- 回文半径数组可以帮助解决很多回文问题
/** i’回文串小于R-i i位置回文半径与i‘相等
* 求最大回文子串长度 i在R内 i在R外 i’回文串大于R-i i位置回文半径等于R-i
* i’回文串等于R-i i位置回文半径起码时R-i,从i位置可能需要扩
*/
public static int maxLcpsLength(String s) {
if (s == null || s.length() == 0) return 0;
char[] str = manacherString(s);
int[] pArr = new int[str.length]; //和处理串长度相同
int C = - 1; //中心
int R = - 1; //R为回文右边界再往右的位置,最右的有效区域为R-1
int max = Integer.MIN_VALUE; //阔出来的最大值
for (int i = 0; i < str.length; i++) {
//i至少的回文区域,先给pArr[i]
//分i在R内,与i在R外 选回文半径较小的 pArr[i'],i到R的距离R-i
pArr[i] = R > i ? Math.min(pArr[2 * C - i], R - i) : 1; //i在R外,回文区域为自己
//从需要验证的位置开始
while (i + pArr[i] < str.length && i - pArr[i] > - 1) {
//从至少的回文区域都尝试一步,看回文区域能不能变大
if (str[i + pArr[i]] == str[i - pArr[i]]) {
pArr[i]++;
} else {
break;
}
}
//更新 R、C
if (i + pArr[i] > R) {
R = i + pArr[i];
C = i;
}
max = Math.max(max, pArr[i]);
}
return max - 1; //处理后的字符串最大回文半径减1,就是原字符串的最大回文长度
}
// accb #a#c#c#b# 因为在回文串长度为偶数时、回文中心线在两个数字中间,若从每个位置i出发往两边扩,会错过一些内容 accb 找不到cc
private static char[] manacherString(String str) {
char[] charArr = str.toCharArray();
char[] res = new char[2 * str.length() + 1];
int index = 0;
for (int i = 0; i < res.length; i++) {
res[i] = (1 & i) == 0 ? '#' : charArr[index++];
}
return res;
}
滑动窗口
- O(N)的平均时间复杂度下,动态生成窗口中最大值
- 使用双向队列
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7G3sNWSN-1665234185481)(C:\Users\DFL\AppData\Roaming\Typora\typora-user-images\image-20221005191724095.png)]
单调栈
- 解决当前数左右两边离当前数最近的比当前数大的值
- 使用一个栈(存放的是数组下标),大数在下小数在上,从左往右遍历数组,遇到比栈顶元素大的数时,就依次弹出并生成信息,直至当前数可以放进去为止,每个弹出的数,左边离当前数最大的是它下面压着的数,右边离它最大的是使它们弹出的数即当前数。
- 若包含相同元素,栈中相同元素放入一个链表中(可以是LInkedList、ArrayList)
Morris遍历
- 时间复杂度O(N),额外空间复杂度O(1)
- Morris遍历无法到达同意节点
- 线索二叉树,使用空闲指针完成遍历,当前节点左树最右节点右指针指向当前节点cur。
- 先序遍历:如果一个节点只到达一次直接打印,到达两次第一次才打印
- 中序遍历:如果一个节点只到达一次直接打印,到达两次第二次才打印
- 后序遍历:如果一个节点能到达两次,第二次打印,然后逆序打印左树的右边界 好复杂 略。。。。。。
public static void morris(TreeNode head) {
if (head == null) return;
TreeNode cur = head;
TreeNode mostRight = null;
while (cur != null) {
mostRight = cur.left;
if (mostRight != null) { //有左子树
while (mostRight.right != null && mostRight.right != cur) { //能够往右走且不回到当前节点
mostRight = mostRight.right; //循环结束到达cur左树最右节点
}
if (mostRight.right == null) { //第一次来到cur
mostRight.right = cur; //最右节点指针指向当前节点cur
cur = cur.left; //cur向左孩子移动
continue; //继续进行循环
} else { //不是第一次来到当前节点,而是顺着左树最右指针来的 mostRight.right ==cur;
mostRight.right = null; //将右指针还原回去
}
}
cur = cur.right; //当前树没有左子树,直接向右移动
}
}
//先序
public static void morrisPre(TreeNode head) {
if (head == null) return;
TreeNode cur = head;
TreeNode mostRight = null;
while (cur != null) {
mostRight = cur.left;
if (mostRight != null) { //有左子树
while (mostRight.right != null && mostRight.right != cur) { //能够往右走且不回到当前节点
mostRight = mostRight.right; //循环结束到达cur左树最右节点
}
if (mostRight.right == null) { //第一次来到cur
System.out.println(cur.val); //第一次来打印
mostRight.right = cur; //最右节点指针指向当前节点cur
cur = cur.left; //cur向左孩子移动
continue; //继续进行循环
} else { //不是第一次来到当前节点,而是顺着左树最右指针来的 mostRight.right ==cur;
mostRight.right = null; //将右指针还原回去
}
} else {
System.out.println(cur.val); //没有左子树直接打印
}
cur = cur.right; //当前树没有左子树,直接向右移动
}
}
//中序
public static void morrisIn(TreeNode head) {
if (head == null) return;
TreeNode cur = head;
TreeNode mostRight = null;
while (cur != null) {
mostRight = cur.left;
if (mostRight != null) { //有左子树
while (mostRight.right != null && mostRight.right != cur) { //能够往右走且不回到当前节点
mostRight = mostRight.right; //循环结束到达cur左树最右节点
}
if (mostRight.right == null) { //第一次来到cur
mostRight.right = cur; //最右节点指针指向当前节点cur
cur = cur.left; //cur向左孩子移动
continue; //继续进行循环
} else { //不是第一次来到当前节点,而是顺着左树最右指针来的 mostRight.right ==cur;
mostRight.right = null; //将右指针还原回去
}
}
System.out.println(cur.val); //没有左树,或者第二次来到该节点打印
cur = cur.right; //当前树没有左子树,直接向右移动
}
}
//后序
public static void morrisPost(TreeNode head) {
if (head == null) return;
TreeNode cur = head;
TreeNode mostRight = null;
while (cur != null) {
mostRight = cur.left;
if (mostRight != null) { //有左子树
while (mostRight.right != null && mostRight.right != cur) { //能够往右走且不回到当前节点
mostRight = mostRight.right; //循环结束到达cur左树最右节点
}
if (mostRight.right == null) { //第一次来到cur
mostRight.right = cur; //最右节点指针指向当前节点cur
cur = cur.left; //cur向左孩子移动
continue; //继续进行循环
} else { //不是第一次来到当前节点,而是顺着左树最右指针来的 mostRight.right ==cur;
mostRight.right = null; //将右指针还原回去
printEdge(cur.left); //第二次来到这个节点,逆序打印左树右边界(相当与链表逆序过程)
}
}
printEdge(head); //跑完后单独打印整棵树的右边界
cur = cur.right; //当前树没有左子树,直接向右移动
}
}
private static void printEdge(TreeNode left) {
}
//Morris中序遍历改造
public static boolean isValidBST(TreeNode head) {
if (head == null) return true;
TreeNode cur = head;
TreeNode mostRight = null;
int preValue = Integer.MIN_VALUE;
while (cur != null) {
mostRight = cur.left;
if (mostRight != null) { //有左子树
while (mostRight.right != null && mostRight.right != cur) { //能够往右走且不回到当前节点
mostRight = mostRight.right; //循环结束到达cur左树最右节点
}
if (mostRight.right == null) { //第一次来到cur
mostRight.right = cur; //最右节点指针指向当前节点cur
cur = cur.left; //cur向左孩子移动
continue; //继续进行循环
} else { //不是第一次来到当前节点,而是顺着左树最右指针来的 mostRight.right ==cur;
mostRight.right = null; //将右指针还原回去
}
}
// System.out.println(cur.val); //没有左树,或者第二次来到该节点打印
if (cur.val <= preValue) {
return false;
}
preValue = cur.val;
cur = cur.right; //当前树没有左子树,直接向右移动
}
return true;
}
有序表
log(N)时机复杂度
- 红黑树
- AVL树
- SB树
- 跳表
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kgvGIeY0-1665234185481)(C:\Users\DFL\AppData\Roaming\Typora\typora-user-images\image-20221007004239373.png)]
不用大于24是因为,24是6、8最小公倍数,能用三个8号袋子装何必要用4个6号袋子装,比如剩27个,不管用哪个袋子装,最后都会剩3个,何必要用6号袋子,并且这个剩余3个苹果的情况在多使用3个8号袋子时也计算过了。
打表法YYDS
网上那些写数学题解的都是垃圾货色,都是打表看出规律后再想的数学解释,就是为了装逼,让你佩服他
/**
* 牛羊每次吃4的整数幂的草,谁最后吃谁赢
*/
public class Winner {
public static String winner1(int n) {
if (n < 5) {
return (n == 0 || n == 2) ? "后手" : "先手";
}
int base = 1; //先手决定吃的草
while (base >= 0) {
if (winner1(n - base).equals("后手")) {
return "先手";
}
if (base > n / 4) {
break;
}
base *= 4;
}
return "后手";
}
public static String winner2(int n) {
if (n % 5 == 0 || (n - 2) % 5 == 0) {
return "后手";
}
return "先手";
}
public static void main(String[] args) {
for (int i = 0; i <= 50; i++) {
// System.out.println(i + " : " + winner1(i));
System.out.println(i + " : " + winner2(i));
}
}
}
提升班习题课
-
表格预处理
-
根据一个随机函数,返回另一个随机函数
/**
* 给定一个函数f,可以1~5的数字等概率返回一个。请加工出1~7的数字等概率返回一个的函数g。
* 给定一个函数f,可以a~b的数字等概率返回一个。请加工出c~d的数字等概率返回一个的函数g。
* 给定一个函数f,以p概率返回0,以1-p概率返回1。请加工出等概率返回0和1的函数g
*/
public class Random {
//以1~5的数字等概率返回一个
public static int f() {
return 0; //空实现
}
//0、1随机发生器
public static int g01() {
int res = 0;
do {
res = f();
} while (res == 3);
return res < 3 ? 0 : 1;
}
//准备3个二进制位
public static int g17() {
int res = 0;
do {
res = g01() << 2 + g01() << 1 + g01(); //随机返回0~6
} while (res == 7);
return res + 1;
}
//以13~21数字等概率返回一个
public static int r() {
return 0; //空实现
}
//0、1随机发生器
public static int r01() {
int res = 0;
do {
res = r();
} while (res == 21);
return res < 17 ? 0 : 1; //二分
}
//准备3个二进制位
public static int g3059() {
int res = 0;
do {
for (int i = 0; i < 5; i++) {
res += (r() << i); //随机返回30~59 0~29
}
} while (res > 29);
return res + 30;
}
//以p概率返回0,以1-p概率返回1。
public static int fp() { //调两次共四个结果 00、01、10、11 其中01、10为等概率事件
return 0; //空实现
}
public static int gp() {
int res1 = - 1;
int res2 = - 1;
do {
res1 = fp();
res2 = fp();
} while ((res1 ^ res2) == 0);
return res1 == 0 ? 0 : 1;
}
}
/**
* 判断括号是否合法字符串 //变种题:求括号最大深度depth = 1()() depth = 2(()) 记录过程中count出现的最大值即可
*/
public class LegalBracket {
public static boolean checkBracket(String s) {
if (s == null || s.length() == 0) return true;
int count = 0;
for (int i = 0; i < s.length(); i++) { //左括号数量>=右括号数量 每个右括号都有左括号可配
if (count < 0) return false;
count = s.charAt(i) == '(' ? count + 1 : count - 1;
}
return count == 0;
}
/**
* 添加多少括号可以让字符串括号合法
*/
public static int addBracket(String s) {
int count = 0;
int ans = 0;
for (int i = 0; i < s.length(); i++) { //左括号数量>=右括号数量 每个右括号都有左括号可配
if (s.charAt(i) == '(') {
count++;
} else { //遇到了右括号,如果此时count==0,说明原来刚好够,所以加上现在这个后需要补一个括号,即ans++;
if (count == 0) { //count不加了
ans++; //补个左括号
} else {
count--;
}
}
}
//count 含义 左括号有多少没对上,ans含义 左括号缺了几个
return ans+count; // count>=0 ((((((())) count = 4 ,ans = 0; ((())))))) count = 0,ans = 4;
}
}
/**
* n个节点的二叉树数量所包含的结构种类个数
*/
public static int numsTres1(int n) {
return process1(n);
}
private static int process1(int n) {
if (n == 0) return 1;
if (n == 1) return 1;
if (n == 2) return 2;
int res = 0;
for (int leftNum = 0; leftNum <= n - 1; leftNum++) {
int leftWays = process1(leftNum);
int rightWays = process1(n - 1 - leftNum);
res += leftWays * rightWays;
}
return res;
}
//dp
public static int numsTres2(int n) {
if (n < 2) return 1;
int[] dp = new int[n + 1]; //0~n;
dp[0] = 1; //0个节点,空树
for (int i = 1; i <= n; i++) { //枚举总的节点个数,0、1、2、3.。。。。n个数为多少
for (int j = 0; j <= i-1; j++) { //枚举左孩子数量
dp[i] += dp[j] * dp[i - j - 1];
}
}
return dp[n];
}
/**
* 集合a、b 可以从任一集合中拿出一元素x放入另一个集合中,两个集合平均值都变大,进行此magic的操作次数最多
*/
public class MagicOps {
// a、b平均值a'、b' 且 a'>b' 从a中拿 x满足 b'<x<a'且x在b中不存在 拿走满足条件的最小元素(吊车尾)
public static int maxOps(int[] arr1, int[] arr2) {
double sum1 = 0;
for (int i = 0; i < arr1.length; i++) {
sum1 += (double) arr1[i];
}
double sum2 = 0;
for (int i = 0; i < arr2.length; i++) {
sum2 += (double) arr2[i];
}
if (avg(sum1, arr1.length) == avg(sum2, arr2.length)) {
return 0;
}
//平均值不相等
int[] arrMore = null;
int[] arrLess = null;
double sumMore = 0;
double sumLess = 0;
if (avg(sum1, arr1.length) > avg(sum2, arr2.length)) { //重定位,可以不用写一堆if-else
arrMore = arr1;
sumMore = sum1;
arrLess = arr2;
sumLess = sum2;
} else {
arrMore = arr2;
sumMore = sum2;
arrLess = arr1;
sumLess = sum1;
}
Arrays.sort(arrMore); //依次选出大于平均值的最小值
HashSet<Integer> setLess = new HashSet<>(); //登记一下,保证所加元素在小集合中没出现过,出现过将导致加入后小集合平均值不变
for (int i : arrLess) {
setLess.add(i);
}
int moreSize = arrMore.length; //平均值大的集合还剩几个元素
int leseSize = arrLess.length; //平均值小的集合还剩几个元素
int ops = 0; //操作了几次
for (int i = 0; i < arrMore.length; i++) {
double cur = (double) arrMore[i];
if (cur < avg(sumMore, moreSize) && cur > avg(sumLess, leseSize)
&& ! setLess.contains(cur)) {
sumMore -= cur;
moreSize--;
sumLess += cur;
leseSize++;
setLess.add(arrMore[i]); //set泛型为整型
ops++;
}
}
return ops;
}
private static double avg(double sum, int size) {
return (double) (sum / size);
}
}
/**
* 最长合法括号子串 leetcode32题,有难度,多画图。
*/
public class LongestValidParentheses {
//子串必须由当前字符结尾,最长有效长度是多少? dp[i]含义
public static int longestValidParentheses(String s) {
if (s == null || s == "") return 0;
char[] str = s.toCharArray();
int[] dp = new int[s.length() + 1];
int res = 0;
int pre = 0;
for (int i = 1; i < s.length(); i++) { //i=0位置必是0,不用算
if (str[i] == ')') { //必须由右括号结尾才进行
// i前一个位置,根据该位置的值往前数dp[i-1]个长度(该位置算出的有效长度)
pre = i - dp[i - 1] - 1; //这个位置不是左括号就肯定是0(( {)} { 这一坨为i-1结尾有效长度 } ) 如果第一个大括号位置不是'(' 肯定会把第二个大括号有效长度变大
if (pre >= 0 && str[pre] == '(') {
// 起码是2这个长度,再加上dp[i-1]前面i-1位置算过的长度,
// 还有可能更多 {(())}(()),后面大括号位置就是pre-1
dp[i] = dp[i - 1] + 2 + (pre > 0 ? dp[pre - 1] : 0);
}
}
res = Math.max(res, dp[i]);
}
return res;
}
}
/**
*使用一个辅助栈完成原始栈的排序:栈顶到栈底从大到小。
*允许使用一个辅助栈,可以申请变量,但不能使用其他数据结构。
*解题思路:辅助栈一直维持由上至下从小到大 ,最终结果由大到小
*/
public class StackSort {
public static void sort(Stack<Integer> stack) {
Stack<Integer> help = new Stack<>();
help.push(stack.pop());
int cur = 0;
while (! stack.isEmpty()) {
cur = stack.pop(); //记住弹出这个值
while (! help.isEmpty() && cur > help.peek()) { //辅助栈一直维持由上至下从小到大
stack.push(help.pop());
}
help.push(cur); //弹够了,可以压入了
}
while (! help.isEmpty()) {
stack.push(help.pop());
}
}
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QHc6jmV9-1665234185482)(C:\Users\DFL\AppData\Roaming\Typora\typora-user-images\image-20221008153839135.png)]
数组矩阵调整问题
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-B9a0kGBC-1665234185482)(C:\Users\DFL\AppData\Roaming\Typora\typora-user-images\image-20221008154247851.png)]
左上角下标、右上角下标, 沿对角线向内移动,错过去时停止
螺旋打印矩阵
分组
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jUlxZ19N-1665234185483)(C:\Users\DFL\AppData\Roaming\Typora\typora-user-images\image-20221008161002688.png)]
arrLess = arr2;
sumLess = sum2;
} else {
arrMore = arr2;
sumMore = sum2;
arrLess = arr1;
sumLess = sum1;
}
Arrays.sort(arrMore); //依次选出大于平均值的最小值
HashSet setLess = new HashSet<>(); //登记一下,保证所加元素在小集合中没出现过,出现过将导致加入后小集合平均值不变
for (int i : arrLess) {
setLess.add(i);
}
int moreSize = arrMore.length; //平均值大的集合还剩几个元素
int leseSize = arrLess.length; //平均值小的集合还剩几个元素
int ops = 0; //操作了几次
for (int i = 0; i < arrMore.length; i++) {
double cur = (double) arrMore[i];
if (cur < avg(sumMore, moreSize) && cur > avg(sumLess, leseSize)
&& ! setLess.contains(cur)) {
sumMore -= cur;
moreSize–;
sumLess += cur;
leseSize++;
setLess.add(arrMore[i]); //set泛型为整型
ops++;
}
}
return ops;
}
private static double avg(double sum, int size) {
return (double) (sum / size);
}
}
/**
- 最长合法括号子串 leetcode32题,有难度,多画图。
*/
public class LongestValidParentheses {
//子串必须由当前字符结尾,最长有效长度是多少? dp[i]含义
public static int longestValidParentheses(String s) {
if (s == null || s == “”) return 0;
char[] str = s.toCharArray();
int[] dp = new int[s.length() + 1];
int res = 0;
int pre = 0;
for (int i = 1; i < s.length(); i++) { //i=0位置必是0,不用算
if (str[i] == ‘)’) { //必须由右括号结尾才进行
// i前一个位置,根据该位置的值往前数dp[i-1]个长度(该位置算出的有效长度)
pre = i - dp[i - 1] - 1; //这个位置不是左括号就肯定是0(( {)} { 这一坨为i-1结尾有效长度 } ) 如果第一个大括号位置不是’(’ 肯定会把第二个大括号有效长度变大
if (pre >= 0 && str[pre] == ‘(’) {
// 起码是2这个长度,再加上dp[i-1]前面i-1位置算过的长度,
// 还有可能更多 {(())}(()),后面大括号位置就是pre-1
dp[i] = dp[i - 1] + 2 + (pre > 0 ? dp[pre - 1] : 0);
}
}
res = Math.max(res, dp[i]);
}
return res;
}
}
/**
*使用一个辅助栈完成原始栈的排序:栈顶到栈底从大到小。
*允许使用一个辅助栈,可以申请变量,但不能使用其他数据结构。
*解题思路:辅助栈一直维持由上至下从小到大 ,最终结果由大到小
*/
public class StackSort {
public static void sort(Stack stack) {
Stack help = new Stack<>();
help.push(stack.pop());
int cur = 0;
while (! stack.isEmpty()) {
cur = stack.pop(); //记住弹出这个值
while (! help.isEmpty() && cur > help.peek()) { //辅助栈一直维持由上至下从小到大
stack.push(help.pop());
}
help.push(cur); //弹够了,可以压入了
}
while (! help.isEmpty()) {
stack.push(help.pop());
}
}
[外链图片转存中...(img-QHc6jmV9-1665234185482)]
### 数组矩阵调整问题
[外链图片转存中...(img-B9a0kGBC-1665234185482)]
左上角下标、右上角下标, 沿对角线向内移动,错过去时停止
螺旋打印矩阵
<img src="C:\Users\DFL\AppData\Roaming\Typora\typora-user-images\image-20221008155202857.png" alt="image-20221008155202857" style="zoom:50%;" />
分组
[外链图片转存中...(img-jUlxZ19N-1665234185483)]