双指针、快慢指针
翻转链表(递归):head.next.next = head; 对于不同问题迭代和递归相结合
寻找链表中点:快慢指针
1.快慢指针:
2.左右指针:
3.前缀和数组:适用于快速、频繁地计算一个索引区间内的元素之和
class PrefixSum {
// 前缀和数组
private int[] prefix;
/* 输入一个数组,构造前缀和 */
public PrefixSum(int[] nums) {
prefix = new int[nums.length + 1];
// 计算 nums 的累加和
for (int i = 1; i < prefix.length; i++) {
prefix[i] = prefix[i - 1] + nums[i - 1];
}
}
/* 查询闭区间 [i, j] 的累加和 */
public int query(int i, int j) {
return prefix[j + 1] - prefix[i];
}
}
4.差分数组:主要适用场景是频繁对原始数组的某个区间的元素进行增减。
如果想对区间 nums[i…j] 的元素全部加 3,那么只需要让 diff[i] += 3,然后再让 diff[j+1] -= 3 即可:
// 差分数组工具类
class Difference {
// 差分数组
private int[] diff;
/* 输入一个初始数组,区间操作将在这个数组上进行 */
public Difference(int[] nums) {
assert nums.length > 0;
diff = new int[nums.length];
// 根据初始数组构造差分数组
diff[0] = nums[0];
for (int i = 1; i < nums.length; i++) {
diff[i] = nums[i] - nums[i - 1];
}
}
/* 给闭区间 [i, j] 增加 val(可以是负数)*/
public void increment(int i, int j, int val) {
diff[i] += val;
if (j + 1 < diff.length) {
diff[j + 1] -= val;
}
}
/* 返回结果数组 */
public int[] result() {
int[] res = new int[diff.length];
// 根据差分数组构造结果数组
res[0] = diff[0];
for (int i = 1; i < diff.length; i++) {
res[i] = res[i - 1] + diff[i];
}
return res;
}
}
5.螺旋遍历矩阵(leetcode54、59):
6.O(1)时间复杂度实现数组的删除操作
把要删除的元素和数组末尾的元素交换,再删除数组末尾的元素
// 左闭右开,起始都为0
int left = 0, right = 0;
while (right < s.size()) {
// 增大窗口
window.add(s[right]);
right++;
// 进行窗口内数据的一系列更新
...
while (window needs shrink) {
// 缩小窗口
window.remove(s[left]);
left++;
// 进行窗口内数据的一系列更新
...
}
}
1.字符串转换为数字(O(1)复杂度字符串匹配)
/* 在最低位添加一个数字 */
int number = 8264;
// number 的进制
int R = 10;
// 想在 number 的最低位添加的数字
int appendVal = 3;
// 运算,在最低位添加一位
number = R * number + appendVal;
// 此时 number = 82643
/* 在最高位删除一个数字 */
int number = 8264;
// number 的进制
int R = 10;
// number 最高位的数字
int removeVal = 8;
// 此时 number 的位数
int L = 4;
// 运算,删除最高位数字
number = number - removeVal * R^(L-1);
// 此时 number = 264
位数通过字符编码确定。以 ASCII 码为例,ASCII 码其实就是 0~255 这 256 个数字,分别对应所有英文字符和英文符号。那么一个长度为 L 的 ASCII 字符串,我们就可以等价理解成一个 L 位的 256 进制的数字,这个数字就可以唯一标识这个字符串,也就可以作为哈希值。
「使……最大值尽可能小」是二分搜索题目常见的问法。通常是找到某个值使得恰好满足题设条件,然后不断收缩边界,找到满足题设条件的最小值。
1.基本二分搜索
int binarySearch(int[] nums, int target) {
int left = 0;
int right = nums.length - 1; // 注意
while(left <= right) {
int mid = left + (right - left) / 2;
if(nums[mid] == target)
return mid;
else if (nums[mid] < target)
left = mid + 1; // 注意
else if (nums[mid] > target)
right = mid - 1; // 注意
}
return -1;
}
注意点:
2.寻找左侧边界的二分搜索
int left_bound(int[] nums, int target) {
int left = 0, right = nums.length - 1;
// 搜索区间为 [left, right]
while (left <= right) {
int mid = left + (right - left) / 2;
if (nums[mid] < target) {
// 搜索区间变为 [mid+1, right]
left = mid + 1;
} else if (nums[mid] > target) {
// 搜索区间变为 [left, mid-1]
right = mid - 1;
} else if (nums[mid] == target) {
// 收缩右侧边界
right = mid - 1;
}
}
// 判断 target 是否存在于 nums 中
// 此时 target 比所有数都大,返回 -1
if (left == nums.length) return -1;
// 判断一下 nums[left] 是不是 target
return nums[left] == target ? left : -1;
}
注意点:
3.寻找右侧边界的二分搜索
int right_bound(int[] nums, int target) {
int left = 0, right = nums.length - 1;
while (left <= right) {
int mid = left + (right - left) / 2;
if (nums[mid] < target) {
left = mid + 1;
} else if (nums[mid] > target) {
right = mid - 1;
} else if (nums[mid] == target) {
// 这里改成收缩左侧边界即可
left = mid + 1;
}
}
// 最后改成返回 left - 1
if (right < 0) return -1;
return nums[right] == target ? right : -1;
}
注意点同左侧边界搜索
1.带权重的随机算法(leetcode528)
前缀和+二分查找:生成一个线段,权重代表这个数在线段中的长短;在线段中随机选择一个点,这个点落在哪段上就选择哪个点
1、根据权重数组 w 生成前缀和数组 preSum。
2、生成一个取值在 [1, preSum[n - 1]] 之内的随机数(注意不包含0,0是preSum中的占位符),用二分搜索算法寻找大于等于这个随机数的最小元素索引(寻找左边界)。
3、最后对这个索引减一(因为前缀和数组有一位索引偏移),就可以作为权重数组的索引,即最终答案:
2.带黑名单的随机算法(leetcode710)
1、总共N个数,黑名单里m个数,在 [0,N-m) 中取随机数
2、把在 [0,N-m) 范围内的黑名单数映射到数组末尾的非黑名单数
3、取到的随机数如果不是黑名单数,返回该数;如果是黑名单数,返回映射值
两个注意点:
二叉树问题的两种思维模式:
1.后序遍历
前序位置的代码只能从函数参数中获取父节点传递来的数据,而后序位置的代码不仅可以获取参数数据,还可以获取到子树通过函数返回值传递回来的数据。一旦发现题目和子树有关,那大概率要给函数设置合理的定义和返回值,在后序位置进行处理。
2.层序遍历
// 输入一棵二叉树的根节点,层序遍历这棵二叉树
void levelTraverse(TreeNode root) {
if (root == null) return;
Queue<TreeNode> q = new LinkedList<>();
q.offer(root);
// 从上到下遍历二叉树的每一层
while (!q.isEmpty()) {
int sz = q.size();
// 从左到右遍历每一层的每个节点
for (int i = 0; i < sz; i++) {
TreeNode cur = q.poll();
// 将下一层节点放入队列
if (cur.left != null) {
q.offer(cur.left);
}
if (cur.right != null) {
q.offer(cur.right);
}
}
}
}
3.二叉树的构造
先构造出根节点,再递归的构造左子树和右子树
根据前序遍历和后序遍历构造的树不唯一,且前序和后序的边界值都需要确认
4.序列化二叉树
使用哪种遍历方式进行序列化,再使用哪种方式进行反序列化即可
注意:一定要使用分隔符!分隔符最好用 “,”;
5.BST二叉搜索树
二叉搜索树中第K小的元素:
二叉搜索树的搜索框架
void BST(TreeNode root, int target) {
if (root.val == target)
// 找到目标,做点什么
if (root.val < target)
BST(root.right, target);
if (root.val > target)
BST(root.left, target);
}
验证二叉搜索树(使用辅助函数,增加函数参数列表,在参数中携带额外信息,将这种约束传递给子树的所有节点)
boolean isValidBST(TreeNode root) {
return isValidBST(root, null, null);
}
/* 限定以 root 为根的子树节点必须满足 max.val > root.val > min.val */
boolean isValidBST(TreeNode root, TreeNode min, TreeNode max) {
// base case
if (root == null) return true;
// 若 root.val 不符合 max 和 min 的限制,说明不是合法 BST
if (min != null && root.val <= min.val) return false;
if (max != null && root.val >= max.val) return false;
// 限定左子树的最大值是 root.val,右子树的最小值是 root.val
return isValidBST(root.left, min, root)
&& isValidBST(root.right, root, max);
}
二叉搜索树删除节点
TreeNode deleteNode(TreeNode root, int key) {
if (root == null) return null;
if (root.val == key) {
// 这两个 if 把情况 1 和 2 都正确处理了
if (root.left == null) return root.right;
if (root.right == null) return root.left;
// 处理情况 3
// 获得右子树最小的节点
TreeNode minNode = getMin(root.right);
// 删除右子树最小的节点
root.right = deleteNode(root.right, minNode.val);
// 用右子树最小的节点替换 root 节点
minNode.left = root.left;
minNode.right = root.right;
root = minNode;
} else if (root.val > key) {
root.left = deleteNode(root.left, key);
} else if (root.val < key) {
root.right = deleteNode(root.right, key);
}
return root;
}
TreeNode getMin(TreeNode node) {
// BST 最左边的就是最小的
while (node.left != null) node = node.left;
return node;
}
1.归并排序
class Merge {
// 用于辅助合并有序数组
private static int[] temp;
public static void sort(int[] nums) {
// 先给辅助数组开辟内存空间
temp = new int[nums.length];
// 排序整个数组(原地修改)
sort(nums, 0, nums.length - 1);
}
// 定义:将子数组 nums[lo..hi] 进行排序
private static void sort(int[] nums, int lo, int hi) {
if (lo == hi) {
// 单个元素不用排序
return;
}
// 这样写是为了防止溢出,效果等同于 (hi + lo) / 2
int mid = lo + (hi - lo) / 2;
// 先对左半部分数组 nums[lo..mid] 排序
sort(nums, lo, mid);
// 再对右半部分数组 nums[mid+1..hi] 排序
sort(nums, mid + 1, hi);
// 将两部分有序数组合并成一个有序数组
merge(nums, lo, mid, hi);
}
// 将 nums[lo..mid] 和 nums[mid+1..hi] 这两个有序数组合并成一个有序数组
private static void merge(int[] nums, int lo, int mid, int hi) {
// 先把 nums[lo..hi] 复制到辅助数组中
// 以便合并后的结果能够直接存入 nums
for (int i = lo; i <= hi; i++) {
temp[i] = nums[i];
}
// 数组双指针技巧,合并两个有序数组
int i = lo, j = mid + 1;
for (int p = lo; p <= hi; p++) {
if (i == mid + 1) {
// 左半边数组已全部被合并
nums[p] = temp[j++];
} else if (j == hi + 1) {
// 右半边数组已全部被合并
nums[p] = temp[i++];
} else if (temp[i] > temp[j]) {
nums[p] = temp[j++];
} else {
nums[p] = temp[i++];
}
}
}
}
leetcode315 计算右侧小于当前元素的个数:在对 nums[lo…hi] 合并的过程中,每当执行 nums[p] = temp[i] 时,就可以确定 temp[i] 这个元素后面比它小的元素个数为 j - mid - 1。递归的计算,每次merge都能计算当前递归中比temp[i] 小的元素个数,最终通过多次递归判定原数组中比temp[i] 小的元素个数。
for (int p = lo; p <= hi; p++) {
if (i == mid + 1) {
arr[p] = temp[j++];
} else if (j == hi + 1) {
arr[p] = temp[i++];
// 更新 count 数组
count[arr[p].id] += j - mid - 1;
} else if (temp[i].val > temp[j].val) {
arr[p] = temp[j++];
} else {
arr[p] = temp[i++];
// 更新 count 数组
count[arr[p].id] += j - mid - 1;
}
}
2.快速排序
public static void sort(int[] nums) {
shuffle(nums);
sort(nums, 0, nums.length-1);
}
private static void sort(int[] nums, int lo, int hi){
if(lo >= hi){ return; }
int p = partition(nums, lo, hi);
sort(nums, lo, p-1);
sort(nums, p+1, hi);
}
private static int partition(int[] nums, int lo, int hi){
int pivot = nums[lo];
int i = lo + 1, j = hi;
while(i <= j){
while(i < hi && nums[i] <= pivot){ i++; }
while(j > lo && nums[j] > pivot){ j--; }
if(i >= j){ break; }
swap(nums, i, j);
}
swap(nums, lo, j);
return j;
}
private static void shuffle(int[] nums){
Random ran = new Random();
int n = nums.length;
for(int i=0; i<n; i++){
int r = i + ran.nextInt(n - i);
swap(nums, i ,r);
}
}
private static void swap(int[] nums, int i, int j){
int temp = nums[i];
nums[i] = nums[j];
nums[j] = temp;
}
TopK问题:
1)堆
2)二分查找 + partition
public int findKthLargest(int[] nums, int k) {
int l = 0;
int r = nums.length-1;
while(l <= r){
int p = partition(nums, l, r);
if(p == k - 1){ return nums[p]; }
else if(p > k - 1){
r = p - 1;
}else if(p < k - 1){
l = p + 1;
}
}
return 0;
}