复习算法总结

一、二分法 找到数组中第一个数, 和最后一个数。  

关键点: 找第一个数-> 那么小于target的都可以舍弃 ,   找最后一个数, 大于target的都可以舍弃

当left = mid 时,  要加1, int mid = left + (right - left + 1)/2 ;

private int findFirstPosition(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) {
// 下一轮搜索区间是 [mid + 1..right]
left = mid + 1;
} else {
// nums[mid] >= target,下一轮搜索区间是 [left..mid]
right = mid;
}
}

// 退出循环以后不能确定 nums[left] 是否等于 target,因此需要再判断一次
if (nums[left] == target) {
return left;
}
return -1;
}

private int findLastPosition(int[] nums, int target) {
int left = 0;
int right = nums.length - 1;
while (left < right) {
int mid = left + (right - left + 1) / 2;
if (nums[mid] > target) {
// 下一轮搜索区间是 [left..mid - 1]
right = mid - 1;
} else {
// 下一轮搜索区间是 [mid..right]
left = mid;
}
}
// 主程序先执行 findFirstPosition,能执行到 findLastPosition 说明数组中一定存在等于 target 的元素,因此这里不用判断 nums[left] 是否等于 target
return left;
}

 

二、下一个排列

算法: 找到最会一个升序,最小值坐标, 之后的位置找到最后一个大于它的坐标, 交换位置, 最小值之后进行升序排序

核心代码

int j = nums.length - 1;
while (j >= 0 && nums[i] >= nums[j]) {
j--;
}

从后向前找, 找到大于nums[i]的最后一个数。

 

三、 二分法搜索旋转数组

核心:  多了一步判断mid在左还是右,     先取确定的值,不确定的直接舍弃。

int lo = 0, hi = nums.length - 1, mid = 0;
while (lo <= hi) {
mid = lo + (hi - lo) / 2;
if (nums[mid] == target) {
return mid;
}
// 先根据 nums[mid] 与 nums[lo] 的关系判断 mid 是在左段还是右段
if (nums[mid] >= nums[lo]) {
// 再判断 target 是在 mid 的左边还是右边,从而调整左右边界 lo 和 hi
if (target >= nums[lo] && target < nums[mid]) {
hi = mid - 1;
} else {
lo = mid + 1;
}
} else {
if (target > nums[mid] && target <= nums[hi]) {
lo = mid + 1;
} else {
hi = mid - 1;
}
}
}
return -1;

 

四、 二分法找到插入位置

核心: right = length 可能查到最后。 找大于等于, 那么小于直接舍弃区间

public int searchInsert(int[] nums, int target) {
int len = nums.length;
int left = 0;
int right = len;
// 在区间 nums[left..right] 里查找第 1 个大于等于 target 的元素的下标
while (left < right) {
int mid = left + (right - left) / 2;
if (nums[mid] < target){
// 下一轮搜索的区间是 [mid + 1..right]
left = mid + 1;
} else {
// 下一轮搜索的区间是 [left..mid]
right = mid;
}
}
return left;
}

 

五、组合之合2

核心: 如何去重, 下一次坐标从i+1开始。    Deque<Integer> path = new ArrayDeque<>(len);  使用addFirst 和removeLast来进行回溯。

 

public List<List<Integer>> combinationSum2(int[] candidates, int target) {
int len = candidates.length;
List<List<Integer>> res = new ArrayList<>();
if (len == 0) {
return res;
}

// 关键步骤
Arrays.sort(candidates);

Deque<Integer> path = new ArrayDeque<>(len);
dfs(candidates, len, 0, target, path, res);
return res;
}

/**
* @param candidates 候选数组
* @param len 冗余变量
* @param begin 从候选数组的 begin 位置开始搜索
* @param target 表示剩余,这个值一开始等于 target,基于题目中说明的"所有数字(包括目标数)都是正整数"这个条件
* @param path 从根结点到叶子结点的路径
* @param res
*/
private void dfs(int[] candidates, int len, int begin, int target, Deque<Integer> path, List<List<Integer>> res) {
if (target == 0) {
res.add(new ArrayList<>(path));
return;
}
for (int i = begin; i < len; i++) {
// 大剪枝:减去 candidates[i] 小于 0,减去后面的 candidates[i + 1]、candidates[i + 2] 肯定也小于 0,因此用 break
if (target - candidates[i] < 0) {
break;
}

// 小剪枝:同一层相同数值的结点,从第 2 个开始,候选数更少,结果一定发生重复,因此跳过,用 continue
if (i > begin && candidates[i] == candidates[i - 1]) {
continue;
}

path.addLast(candidates[i]);
// 调试语句 ①
// System.out.println("递归之前 => " + path + ",剩余 = " + (target - candidates[i]));

// 因为元素不可以重复使用,这里递归传递下去的是 i + 1 而不是 i
dfs(candidates, len, i + 1, target - candidates[i], path, res);

path.removeLast();
// 调试语句 ②
// System.out.println("递归之后 => " + path + ",剩余 = " + (target - candidates[i]));
}

}

 

六、跳跃游戏

核心: 定义几个关键变量, 从头开始遍历数组,不断更新这几个变量。  每个位置的最大距离不断更新

每次i== end时,   先跳出一步,如果i不能等于end, 也就是end >= length-1, 那么step就是最终跳跃的结果。

public int jump(int[] nums) {
int length = nums.length;
int end = 0;
int maxPosition = 0;
int steps = 0;
for (int i = 0; i < length - 1; i++) {
maxPosition = Math.max(maxPosition, i + nums[i]);
if (i == end) {
end = maxPosition;
steps++;
}
}
return steps;
}

 

七、全排列1,   不包含重复元素

核心:考虑i是从i开始 还是i+1开始, 显然这两种都有问题, 需要从0开始,但是从0开始需要有一个变量记录,现在都有哪些元素使用了,

public List<List<Integer>> permute(int[] nums) {
int len = nums.length;
// 使用一个动态数组保存所有可能的全排列
List<List<Integer>> res = new ArrayList<>();
if (len == 0) {
return res;
}

boolean[] used = new boolean[len];
Deque<Integer> path = new ArrayDeque<>(len);

dfs(nums, len, 0, path, used, res);
return res;
}

private void dfs(int[] nums, int len, int depth,
Deque<Integer> path, boolean[] used,
List<List<Integer>> res) {
if (depth == len) {
res.add(new ArrayList<>(path));
return;
}

for (int i = 0; i < len; i++) {
if (!used[i]) {
path.addLast(nums[i]);
used[i] = true;

System.out.println(" 递归之前 => " + path);
dfs(nums, len, depth + 1, path, used, res);

used[i] = false;
path.removeLast();
System.out.println("递归之后 => " + path);
}
}
}

 

八、全排列2

核心: 可以包含重复元素, 需要减枝:   当前层, 前面的没有使用, 现在的也不允许使用。

if (i > 0 && nums[i] == nums[i - 1] && !used[i - 1]) {
continue;
}

// 回溯使用这个

Deque<Integer> path = new ArrayDeque<>(len);

 

public List<List<Integer>> permuteUnique(int[] nums) {
int len = nums.length;
List<List<Integer>> res = new ArrayList<>();
if (len == 0) {
return res;
}

// 排序(升序或者降序都可以),排序是剪枝的前提
Arrays.sort(nums);

boolean[] used = new boolean[len];
// 使用 Deque 是 Java 官方 Stack 类的建议
Deque<Integer> path = new ArrayDeque<>(len);
dfs(nums, len, 0, used, path, res);
return res;
}

private void dfs(int[] nums, int len, int depth, boolean[] used, Deque<Integer> path, List<List<Integer>> res) {
if (depth == len) {
res.add(new ArrayList<>(path));
return;
}

for (int i = 0; i < len; ++i) {
if (used[i]) {
continue;
}

// 剪枝条件:i > 0 是为了保证 nums[i - 1] 有意义
// 写 !used[i - 1] 是因为 nums[i - 1] 在深度优先遍历的过程中刚刚被撤销选择
if (i > 0 && nums[i] == nums[i - 1] && !used[i - 1]) {
continue;
}

path.addLast(nums[i]);
used[i] = true;

dfs(nums, len, depth + 1, used, path, res);
// 回溯部分的代码,和 dfs 之前的代码是对称的
used[i] = false;
path.removeLast();
}
}

 

九、不同路径

核心: 采用压缩空间法, 如果第一列是1, 那么第一次初始好之后不用管, 如果发现有障碍物, 那么后面所有都为0.

int n = obstacleGrid.length, m = obstacleGrid[0].length;
int[] f = new int[m];

f[0] = obstacleGrid[0][0] == 0 ? 1 : 0;
for (int i = 0; i < n; ++i) {
for (int j = 0; j < m; ++j) {
if (obstacleGrid[i][j] == 1) {
f[j] = 0;
continue;
}
if (j - 1 >= 0 && obstacleGrid[i][j - 1] == 0) {
f[j] += f[j - 1];
}
}
}

return f[m - 1]; 

非压缩空间方法:

// 定义 dp 数组并初始化第 1 行和第 1 列。
int m = obstacleGrid.length, n = obstacleGrid[0].length;
int[][] dp = new int[m][n];
for (int i = 0; i < m && obstacleGrid[i][0] == 0; i++) {
dp[i][0] = 1;
}
for (int j = 0; j < n && obstacleGrid[0][j] == 0; j++) {
dp[0][j] = 1;
}

// 根据状态转移方程 dp[i][j] = dp[i - 1][j] + dp[i][j - 1] 进行递推。
for (int i = 1; i < m; i++) {
for (int j = 1; j < n; j++) {
if (obstacleGrid[i][j] == 0) {
dp[i][j] = dp[i - 1][j] + dp[i][j - 1];
}
}
}
return dp[m - 1][n - 1];

 

十、 加一

核心: 给当前位加1 如果不等于0, 那么直接返回就可以。

public int[] plusOne(int[] digits) {
        int len = digits.length;
        for (int i = len - 1; i >= 0; i--) {
            digits[i] = (digits[i] + 1) % 10;
            if (digits[i] != 0) {
                return digits;
            }
        }
        digits = new int[len + 1];
        digits[0] = 1;
        return digits;
 }

 

posted @ 2022-05-04 13:18  Star-Shining  阅读(33)  评论(0)    收藏  举报