1. 二分查找算法精解
二分查找是算法面试中的常客,也是程序员必须掌握的基础算法之一。它的核心思想是通过不断缩小搜索范围来快速定位目标元素。让我们深入分析几个典型的二分查找变种问题。
1.1 基础二分查找实现
搜索插入位置(LeetCode 35)是二分查找最基础的应用场景。给定一个排序数组和一个目标值,在数组中找到目标值,并返回其索引。如果目标值不存在于数组中,返回它将会被按顺序插入的位置。
java复制class Solution {
public int searchInsert(int[] nums, int target) {
int left = 0, 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 {
right = mid - 1; // 目标在左半部分
}
}
// 循环结束时,left 的位置就是 target 应该插入的位置
return left;
}
}
关键点解析:
- 循环条件是
left <= right,确保所有元素都被检查mid = left + (right - left)/2的写法可以避免整数溢出- 当
nums[mid] < target时,说明目标在右半部分,因此left = mid + 1- 当循环结束时,
left的位置就是目标值应该插入的位置
1.2 二分查找变种:旋转数组搜索
搜索旋转排序数组(LeetCode 33)是二分查找的一个经典变种。虽然数组被旋转过,但我们可以利用二分查找的特性来解决这个问题。
java复制class Solution {
public int search(int[] nums, int target) {
int left=0, right=nums.length-1;
if(nums[left]==target) return left;
if(nums[right]==target) return right;
while(left<right){
if(nums[left]==target) return left;
if(nums[right]==target) return right;
int mid=(left+right)/2;
if(nums[mid]==target) return mid;
//如果左边有序
if(nums[left]<=nums[mid]){
//查看target是否在有序的之中
if(target>=nums[left]&&target<nums[mid]){
//如果在,那么更新right
right=mid-1;
}else{
//target不在有序的之中,那么更新待测数组
left=mid+1;
}
}else{
//右边有序
if(target>nums[mid]&&target<=nums[right]){
left=mid+1;
}else{
right=mid-1;
}
}
}
return -1;
}
}
解题思路:
- 虽然数组被旋转过,但mid左右两侧的其中一侧一定是有序的
- 如果左侧有序:查看target是否在左侧,如果在,更新到左侧,否则,更新到右侧
- 如果右侧有序:查看target是否在右侧,如果在,更新到右侧,否则,更新到左侧
1.3 二分查找变种:寻找旋转点
寻找旋转排序数组中的最小值(LeetCode 153)是另一个常见的二分查找变种问题。
java复制class Solution {
public int findMin(int[] nums) {
int left=0, right=nums.length-1;
while(left<right){
int mid=(left+right)/2;
//mid>right,表明右侧为乱序,最小值在右侧
if(nums[mid]>nums[right]){
//这里可以+1,因为mid已经比right大了,所以直接跳过mid
left=mid+1;
}
//mid<right,表明左侧为乱序,最小值在左侧
if(nums[mid]<nums[right]){
//这里不可+1,因为mid可能为最小值
right=mid;
}
}
//最后left一定会等于right,此时所处的位置就是min的位置
return nums[left];
}
}
关键点:
- 虽然数组旋转过,但是一定有一侧有序,而最小值一定在另一侧无序的当中
- 如果左侧有序:更新到右侧
- 如果右侧有序:更新到左侧
- 最后left一定会等于right,此时所处的位置就是最小值的位置
2. 栈与队列的高级应用
栈和队列是算法中常用的数据结构,掌握它们的应用场景和变种用法对解决实际问题至关重要。
2.1 栈的基本应用:括号匹配
有效的括号(LeetCode 20)是栈的经典应用场景。我们需要检查字符串中的括号是否有效配对。
java复制class Solution {
public boolean isValid(String s) {
Deque<Character> stack=new ArrayDeque<>();;
for(int i=0;i<s.length();i++){
//如果栈为空,那么压入一个元素
if(stack.isEmpty()){
//优化条件,不加也行
if(s.charAt(i)==')'||s.charAt(i)==']'||s.charAt(i)=='}') return false;
stack.push(s.charAt(i));
continue;
}
//取出栈顶元素,与下一个元素匹配
char top=stack.pop();
//如果匹配为一对,则出栈
if((top=='('&&s.charAt(i)==')')||(top=='['&&s.charAt(i)==']')||(top=='{'&&s.charAt(i)=='}')){
continue;
}else{
//如果不为一对,则新元素入栈
stack.push(top);
stack.push(s.charAt(i));
}
}
//如果最终栈中有残留元素,表明无法完美匹配,则不有效括号
return stack.isEmpty();
}
}
实现要点:
- 使用栈来存储遇到的左括号
- 遇到右括号时,检查是否与栈顶的左括号匹配
- 如果匹配则弹出栈顶元素,否则返回false
- 最后检查栈是否为空,空则表示所有括号都匹配成功
2.2 栈的高级应用:字符串解码
字符串解码(LeetCode 394)是一个更复杂的栈应用问题,需要处理嵌套的编码字符串。
java复制class Solution {
public String decodeString(String s) {
Deque<Character> stack=new ArrayDeque<>();
for(int i=0;i<s.length();i++){
int sum=0;
StringBuilder res=new StringBuilder();
//一直压栈,压到]出现
if(s.charAt(i)!=']'){
stack.push(s.charAt(i));
continue;
}
//base:作为后面sum的进位辅助
int base=1;
//如果为字母,那么构造字符串res
while(!stack.isEmpty()&&stack.peek()>='a'&&stack.peek()<='z'){
res.insert(0, stack.pop());
}
//如果为[,那么直接弹出
if(!stack.isEmpty()&&stack.peek()=='['){
stack.pop();
}
//如果为数字,那么构造复制次数sum
while(!stack.isEmpty()&&stack.peek()>='0'&&stack.peek()<='9'){
int num=stack.pop()-'0';
sum=sum+num*base;
base*=10;
}
//拼接字符串,并重新压入栈,比如此时拼接好abcabc,还需要继续压入栈
for(int cnt=0;cnt<sum;cnt++){
for(int j=0;j<res.length();j++){
stack.push(res.charAt(j));
}
}
}
//拼接栈中的最终结果,返回
StringBuilder result = new StringBuilder();
while (!stack.isEmpty()) {
result.insert(0, stack.pop());
}
return result.toString();
}
}
解题思路:
- 使用栈作为主要数据结构
- 遇到
]时触发解码操作- 将栈顶的字符组合成字符串,并根据前面的数字进行重复
- 然后再压回栈中
- 最后将栈中所有字符拼接成最终结果
2.3 单调栈应用:每日温度
每日温度(LeetCode 739)是单调栈的典型应用,需要找到每一天之后更高温度出现的天数。
java复制class Solution {
public int[] dailyTemperatures(int[] temperatures) {
//构建等长的结果数组
int[] res=new int[temperatures.length];
//栈中存放一个用数组实现的键值对,键是气温的索引,值是具体气温
Deque<int[]> stack=new ArrayDeque<>();
//遍历气温数组
for(int i=0;i<temperatures.length;i++){
//第一次开头,栈为空,压入一个元素
if(stack.isEmpty()){
stack.push(new int[]{i, temperatures[i]});
continue;
}
//不断弹出栈顶的元素,与待压入的元素比较,只要栈顶元素气温更低,就表明找到了未来某天的高气温
//注意while循环中必须只能由一个pop,多了就会导致多弹出。其余的用peek
while(!stack.isEmpty()&&stack.peek()[1]<temperatures[i]){
//在结果数组的相同位置写入两个气温相隔的天数
res[stack.peek()[0]]=i-stack.pop()[0];
}
//压入新的气温
stack.push(new int[]{i, temperatures[i]});
}
return res;
}
}
实现要点:
- 维护一个单调递减的栈
- 当遇到更高的温度时,就弹出并"结算"栈中所有比它低的温度
- 计算当前温度与栈中温度的天数差,存入结果数组
- 将当前温度压入栈中
3. 堆(优先队列)的应用
堆(优先队列)是一种特殊的树形数据结构,常用于解决Top K问题等场景。
3.1 堆的基本应用:数组中的第K个最大元素
数组中的第K个最大元素(LeetCode 215)是堆的经典应用场景。
java复制class Solution {
public int findKthLargest(int[] nums, int k) {
//初始化最小堆,长度为k
PriorityQueue<Integer> heap=new PriorityQueue<>(k);
for(int i=0;i<nums.length;i++){
//压入前k个元素
if(i<k) {
heap.offer(nums[i]);
continue;
}
//如果当前元素大于堆顶元素,换入
if(nums[i]>heap.peek()){
heap.poll();
heap.offer(nums[i]);
}
}
//返回堆顶元素即为第k大元素
return heap.peek();
}
}
实现思路:
- 维护一个容量为k的最小堆
- 堆顶元素是堆中最小的元素,也是当前的第k大元素
- 遍历数组,如果元素大于堆顶元素,则替换堆顶元素
- 最后堆顶元素就是第k大元素
3.2 堆的高级应用:前K个高频元素
前K个高频元素(LeetCode 347)需要结合哈希表和堆来解决。
java复制class Solution {
public int[] topKFrequent(int[] nums, int k) {
//建立哈希表
Map<Integer, Integer> map=new HashMap<>();
//小堆,排序方式为比较数组的第二个元素,即频率
PriorityQueue<int[]> heap=new PriorityQueue<>(k, (a, b)->a[1]-b[1]);
//把频率放入哈希表,如果数字存在,则频率加1,如果不存在则放入新数字,频率设为1
for (int num : nums) {
map.put(num, map.getOrDefault(num, 0) + 1);
}
//把哈希表中的前k个元素放入堆,然后遍历剩下的元素,如果比堆顶(最小的元素)大,则替换进入,如果小,则跳过
int cnt=0;
for(Map.Entry<Integer, Integer> entry : map.entrySet()){
if(cnt<k){
heap.offer(new int[]{entry.getKey(), entry.getValue()});
cnt++;
continue;
}
int num=heap.peek()[1];
if(num<entry.getValue()){
heap.poll();
heap.offer(new int[]{entry.getKey(), entry.getValue()});
}
}
//最终留存在堆中的元素就是频率最高的前k个元素
int[] res=new int[k];
int i=0;
while(!heap.isEmpty()){
res[i]=heap.poll()[0];
i++;
}
return res;
}
}
解题步骤:
- 使用哈希表统计每个数字的出现频率
- 创建容量为k的最小堆,按频率排序
- 遍历哈希表,维护堆的大小为k,保留频率最高的k个元素
- 最后将堆中的元素输出为结果数组
4. 动态规划专题
动态规划是算法设计中的重要方法,适用于有重叠子问题和最优子结构性质的问题。
4.1 基础DP问题:爬楼梯
爬楼梯(LeetCode 70)是最经典的动态规划入门问题。
java复制class Solution {
public int climbStairs(int n) {
//构建状态表
int[] dp=new int[n+1];
//特殊情况
if(n<=2){
return n;
}
//初始化状态表
dp[0]=0;
dp[1]=1;
dp[2]=2;
//填表
for(int i=3;i<=n;i++){
//状态转移方程
dp[i]=dp[i-2]+dp[i-1];
}
return dp[n];
}
}
动态规划思路:
- 定义dp[i]为爬到第i阶楼梯的方法数
- 初始化:dp[1]=1, dp[2]=2
- 状态转移方程:dp[i] = dp[i-1] + dp[i-2]
- 因为每次可以爬1或2阶,所以当前阶的方法数等于前一阶和前两阶方法数之和
4.2 二维DP问题:最小路径和
最小路径和(LeetCode 64)是二维动态规划的典型问题。
java复制class Solution {
public int minPathSum(int[][] grid) {
//构建dp数组
int[][] dp=new int[grid.length][grid[0].length];
//初始化,0,0位置只能是grid本身
dp[0][0]=grid[0][0];
//填表,状态转换方程为 dp[i][j]=Math.min(dp[i-1][j], dp[i][j-1])+grid[i][j];
for(int i=0;i<grid.length;i++){
for(int j=0;j<grid[0].length;j++){
if(i==0&&j==0) continue;
if(i==0){
dp[i][j]= dp[i][j-1]+grid[i][j];
continue;
}
if(j==0){
dp[i][j]=dp[i-1][j]+grid[i][j];
continue;
}
dp[i][j]=Math.min(dp[i-1][j], dp[i][j-1])+grid[i][j];
}
}
//返回
return dp[dp.length-1][dp[0].length-1];
}
}
解题要点:
- dp(i,j)的值为grid(i,j) + min(上面的,左面的)
- 状态转移方程为 dp[i][j] = Math.min(dp[i-1][j], dp[i][j-1]) + grid[i][j]
- 需要特殊处理第一行和第一列的边界情况
4.3 字符串DP问题:编辑距离
编辑距离(LeetCode 72)是字符串处理中经典的动态规划问题。
java复制class Solution {
public int minDistance(String word1, String word2) {
//构建dp数组
int [][] dp=new int[word1.length()+1][word2.length()+1];
//初始化第一行和第一列
for(int i=0;i<dp.length;i++){
dp[i][0]=i;
}
for(int i=0;i<dp[0].length;i++){
dp[0][i]=i;
}
//填表
for(int i=1;i<dp.length;i++){
for(int j=1;j<dp[0].length;j++){
//两字符相等
if(word1.charAt(i-1)==word2.charAt(j-1)){
//状态转移方程1
dp[i][j]=dp[i-1][j-1];
//不相等
}else{
//状态转移方程2
dp[i][j]=Math.min(dp[i-1][j-1]+1, Math.min(dp[i-1][j]+1, 1+dp[i][j-1]));
}
}
}
//返回
return dp[dp.length-1][dp[0].length-1];
}
}
算法解析:
- dp[i][j]表示word1的前i个字符转换成word2的前j个字符所需的最小操作数
- 初始化:空字符串转换为任意长度字符串需要相应次数的插入操作
- 状态转移:
- 如果字符相等,则dp[i][j] = dp[i-1][j-1]
- 如果字符不等,则取替换、删除、插入三种操作的最小值加1
5. 数组与字符串处理技巧
数组和字符串是算法题中最常见的数据结构,掌握它们的处理技巧至关重要。
5.1 双指针技巧:颜色分类
颜色分类(LeetCode 75)是经典的荷兰国旗问题,可以使用三指针法高效解决。
java复制class Solution {
public void sortColors(int[] nums) {
//初始化三指针
int left=0, right=nums.length-1, ptr=0;
//循环条件ptr<=right
while(ptr<=right){
//ptr=1:ptr++,continue
if(nums[ptr]==1){
ptr++;
continue;
}
//ptr=2:交换,right--,不continue继续检查ptr当前值
if(nums[ptr]==2){
swap(ptr, right, nums);
right--;
}
//ptr=0:交换,ptr++,left++,continue
if(nums[ptr]==0){
swap(ptr, left, nums);
ptr++;
left++;
continue;
}
}
}
//交换方法
public void swap(int a, int b, int[] nums){
int x=nums[a];
nums[a]=nums[b];
nums[b]=x;
}
}
三指针法解析:
- left指针:指向最后一个0的下一个位置
- right指针:指向第一个2的前一个位置
- ptr指针:当前遍历的指针
- 当nums[ptr]==0时,与left交换,left和ptr都右移
- 当nums[ptr]==2时,与right交换,right左移
- 当nums[ptr]==1时,ptr右移
5.2 数组技巧:下一个排列
下一个排列(LeetCode 31)需要理解排列的生成规律。
java复制class Solution {
public void nextPermutation(int[] nums) {
//遍历指针
int ptr=nums.length-2;
//找拐点
while(ptr>=0){
if(nums[ptr+1]<=nums[ptr]){
ptr--;
continue;
}else{
break;
}
}
//如果没找到拐点,表明这个数组是一个类似"4,3,2,1"这个样的单调形式,那么直接sort
if(ptr==-1){
Arrays.sort(nums);
return;
}
//找到右侧比这个数字大的最小数字,交换
for(int i=nums.length-1;i>=0;i--){
if(nums[i]>nums[ptr]){
swap(ptr, i, nums);
break;
}
}
//反转右侧,因为右侧一定是一个单调递减,现在将其变成单调递增
int left=ptr+1;
int right=nums.length-1;
while(left<right){
swap(left, right, nums);
left++;
right--;
}
}
//抽取的交换方法
public void swap(int a, int b, int[] nums){
int temp=nums[a];
nums[a]=nums[b];
nums[b]=temp;
}
}
算法步骤:
- 从后向前查找第一个相邻升序的元素对 (i,j)
- 如果找不到这样的元素对,说明整个数组是降序排列的,直接反转整个数组
- 在j及之后的元素中,从后向前查找第一个大于nums[i]的元素nums[k]
- 交换nums[i]和nums[k]
- 反转j及之后的元素
5.3 快慢指针技巧:寻找重复数
寻找重复数(LeetCode 287)可以转化为链表环检测问题。
java复制class Solution {
public int findDuplicate(int[] nums) {
int slow=0, fast=0;
//数组版"判断有无环"
while(true){
slow=nums[slow];
fast=nums[nums[fast]];
if(slow==fast) break;
}
//找到环的入口点
slow = 0;
while(slow != fast){
slow = nums[slow];
fast = nums[fast];
}
return slow;
}
}
解题思路:
- 将数组视为链表,数组值表示下一个节点的索引
- 使用快慢指针法检测环
- 找到环的入口点,即为重复数字
- 这种方法时间复杂度O(n),空间复杂度O(1)
6. 贪心算法应用
贪心算法在解决某些最优化问题时非常有效,它通过局部最优选择来达到全局最优。
6.1 跳跃游戏系列
跳跃游戏(LeetCode 55)和跳跃游戏II(LeetCode 45)是贪心算法的典型应用。
跳跃游戏实现:
java复制class Solution {
public boolean canJump(int[] nums) {
//最远可达位置
int maxReach=nums[0];
for(int i=0;i<nums.length;i++){
//如果该位置无法到达,表明剩下所有位置也都无法到达,直接返回false
if(i>maxReach) return false;
//不断更新最大位置
maxReach=Math.max(maxReach, nums[i]+i);
}
return true;
}
}
跳跃游戏II实现:
java复制class Solution {
public int jump(int[] nums) {
int jump=0;
//当前跳的最远距离
int currentJump=0;
//下一跳的最远距离(最有潜力的距离)
int maxCover=nums[0];
//遍历所有元素
for(int i=0;i<nums.length-1;i++){
//更新下一跳最远距离
maxCover=Math.max(maxCover, i+nums[i]);
//到达当前跳的最远距离,必须进行下一跳了
if(i==currentJump){
//进行下一跳
jump++;
//更新当前最远距离
currentJump=maxCover;
//如果当前最远距离已经包含了终点,意味着这一跳已经能跳到终点,直接结束
if(currentJump>=nums.length-1){
break;
}
}
}
return jump;
}
}
贪心策略分析:
- 跳跃游戏:维护一个最远可达位置,遍历数组时更新这个值
- 如果当前位置超过了最远可达位置,说明无法到达终点
- 跳跃游戏II:维护当前跳跃范围和下一跳的最远距离
- 当到达当前跳跃边界时,增加跳跃次数并更新跳跃范围
6.2 划分字母区间
划分字母区间(LeetCode 763)需要贪心地尽可能划分更多的片段。
java复制class Solution {
public List<Integer> partitionLabels(String s) {
//哈希表存入所有元素的最后一个出现的位置
Map<Character, Integer> map=new HashMap<>();
for(int i=0;i<s.length();i++){
map.put(s.charAt(i), i);
}
//设置right为当前片段的尾,left为当前元素的头
List<Integer> res=new ArrayList<>();
int right=0;
int left=0;
//遍历数组
for(int i=0;i<s.length();i++){
//不断更新尾
right=Math.max(right, map.get(s.charAt(i)));
//如果走到尾,表明后续片段中不再存在当前片段的任何一个元素
if(i==right){
//计算长度,加入结果
res.add(right+1-left);
//设置left为新片段头
left=i+1;
}
}
return res;
}
}
算法步骤:
- 统计每个字符最后出现的位置
- 遍历字符串,维护当前片段的结束位置
- 当当前位置等于当前片段的结束位置时,表示可以划分一个片段
- 记录片段长度,并开始新的片段
7. 位运算技巧
位运算在某些问题中可以提供非常高效的解决方案,掌握常见的位运算技巧很有必要。
7.1 只出现一次的数字
只出现一次的数字(LeetCode 136)可以利用异或运算的特性高效解决。
java复制class Solution {
public int singleNumber(int[] nums) {
//创建哈希表
Map<Integer, Integer> map=new HashMap<>();
//遍历,出现一次放入哈希,出现两次移除出哈希
for(int i=0;i<nums.length;i++){
if(!map.containsKey(nums[i])){
map.put(nums[i], 1);
}else{
map.remove(nums[i]);
}
}
//拿出唯一的哈希键
Map.Entry<Integer, Integer> entry = map.entrySet().iterator().next();
Integer key = entry.getKey();
return key;
}
}
位运算优化版:
java复制public int singleNumber(int[] nums) { int result = 0; for (int num : nums) { result ^= num; } return result; }异或运算特性:
- 任何数和0异或都是它本身
- 任何数和自身异或都是0
- 异或运算满足交换律和结合律
7.2 多数元素
多数元素(LeetCode 169)可以使用摩尔投票法高效解决。
java复制class Solution {
public int majorityElement(int[] nums) {
Arrays.sort(nums);
return nums[(nums.length)/2];
}
}
摩尔投票法实现:
java复制public int majorityElement(int[] nums) { int count = 0; Integer candidate = null; for (int num : nums) { if (count == 0) { candidate = num; } count += (num == candidate) ? 1 : -1; } return candidate; }摩尔投票法要点:
- 维护一个候选人和计数器
- 遇到相同元素计数器加1,不同则减1
- 计数器为0时更换候选人
- 最后剩下的候选人就是多数元素
8. 动态规划进阶问题
8.1 完全平方数
完全平方数(LeetCode 279)是典型的动态规划问题。
java复制class Solution {
public int numSquares(int n) {
//初始化dp数组,每个元素存储的是构成当前索引值的最小平方数个数
int dp[]=new int[n+1];
//填表
for(int i=1;i<n+1;i++){
//先按照最坏情况,即构成当前索引的平方数全为1,i/1=i
dp[i]=i;
//尝试减去比i小的平方数j*j,然后跳转到i-j*j的索引位置,查看平方数最小个数+1,与当前的个数相比,取其小,更新
for(int j=0;j*j<=i;j++){
dp[i]=Math.min(dp[i], dp[i-j*j]+1);
}
}
//最后返回构成n的最少平方数个数
return dp[n];
}
}
动态规划思路:
- dp[i]表示组成数字i所需的最少完全平方数个数
- 初始化:最坏情况是全部由1组成,即dp[i]=i
- 状态转移:对于每个i,尝试所有可能的j*j,取最小值
- dp[i] = min(dp[i], dp[i-jj]+1) 对所有jj <= i
8.2 零钱兑换
零钱兑换(LeetCode 322)是另一个经典的动态规划问题。
java复制class Solution {
public int coinChange(int[] coins, int amount) {
//构建dp数组
int[] dp=new int[amount+1];
//初始化,dp0=0,其他都设为amount+1,即不可达
dp[0]=0;
for(int i=1;i<amount+1;i++){
dp[i]=amount+1;
}
//循环填表
for(int i=0;i<=amount;i++){
//逐个尝试硬币金额
for(int j=0;j<coins.length;j++){
//判断哪种更小,状态转换方程:dp[i]=Math.min(dp[i], dp[i-coins[j]]+1)
if(coins[j]<=i) dp[i]=Math.min(dp[i], dp[i-coins[j]]+1);
}
}
//返回最终结果,如果最终仍是不可达,返回-1
if(dp[amount]==amount+1) return -1;
return dp[amount];
}
}
算法解析:
- dp[i]表示组成金额i所需的最少硬币数
- 初始化:dp[0]=0,其他为amount+1(表示不可达)
- 状态转移:对于每个金额i,尝试所有可能的硬币面额
- dp[i] = min(dp[i], dp[i-coin]+1) 对所有coin <= i
- 最后检查dp[amount]是否被更新,否则返回-1
8.3 单词拆分
单词拆分(LeetCode 139)是字符串处理与动态规划的结合。
java复制class Solution {
public boolean wordBreak(String s, List<String> wordDict) {
//将列表转换为哈希set,所有的collection类型都能转换,转换方法就是Set<type> set=new HashSet<>(target object)
Set<String> wordSet = new HashSet<>(wordDict);
//构建dp数组,元素代表0到当前索引的字符串能否被拆分
boolean[] dp=new boolean[s.length()+1];
//初始化,dp[0]为空,能被拆分,直接为true
dp[0]=true;
for(int i=1;i<s.length()+1;i++){
dp[i]=false;
}
//填表
for(int i=0;i<s.length()+1;i++){
for(int j=0;j<i;j++){
//将单词分为三段区间,0-j;j-i,i-结尾。如果0-j可拆,并且j-i也可拆,那么表明0-i可拆,即dpi=true
if (dp[j] && wordSet.contains(s.substring(j, i))) {
dp[i] = true;
break; // 找到一种拆分即可
}
}
}
return dp[s.length()];
}
}
动态规划思路:
- dp[i]表示字符串前i个字符能否被拆分成字典中的单词
- 初始化:dp[0]=true(空字符串可以被拆分)
- 状态转移:对于每个位置i,检查所有j < i,如果dp[j]为true且s.substring(j,i)在字典中,则dp[i]=true
- 最终返回dp[s.length()]的值
9. 数组与矩阵问题
9.1 杨辉三角
杨辉三角(LeetCode 118)是经典的二维数组问题。
java复制class Solution {
public List<List<Integer>> generate(int numRows) {
//创建返回列表
List<List<Integer>> res=new ArrayList<>();
//构建第一行并加入
List<Integer> first=new ArrayList<>();
first.add(1);
res.add(first);
//构建其他行
for(int i=1;i<numRows;i++){
//取出前一行
List<Integer> lastList=res.get(res.size()-1);
List<Integer> row=new ArrayList<>();
//挨个加入元素
for(int j=0;j<=lastList.size();j++){
//加入前后元素
if(j==0||j==lastList.size()){
row.add(1);
continue;
}
//状态转换方程
row.add(lastList.get(j-1)+lastList.get(j));
}
//加入返回列表
res.add(row);
}
return res;
}
}
算法要点:
- 每一行的第一个和最后一个元素都是1
- 其他元素等于上一行相邻两个元素之和
- 使用动态规划的思想逐行构建
- 时间复杂度O(n^2),空间复杂度O(n^2)
9.2 乘积最大子数组
乘积最大子数组(LeetCode 152)需要考虑负负得正的情况。
java复制class Solution {
public int maxProduct(int[] nums) {
//构建两条数组min和max,构建min是因为需要考虑
int[] max=new int[nums.length];
int[] min=new int[nums.length];
//初始化头元素
max[0]=nums[0];
min[0]=nums[0];
//填表
for(int i=1;i<nums.length;i++){
int num=nums[i];
//状态转换方程:cur=max(当前位置,当前位置*max[上一个],当前位置*min[上一个]
max[i]=Math.max(num, Math.max(max[i-1]*num, num*min[i-1]));
min[i]=Math.min(num, Math.min(min[i-1]*num, num*max[i-1]));
}
//查询结果并返回
int res=max[0];
for(int i=0;i<max.length;i++){
res=Math.max(max[i], res);
}
return res;
}
}
解题思路:
- 因为需要考虑负负得正的情况,所以需要同时维护当前最大值和最小值
- 对于每个元素,计算它与前一个最大值和最小值的乘积
- 新的最大值是当前元素、当前元素乘以前一个最大值、当前元素乘以前一个最小值三者中的最大者
- 新的最小值同理取三者中的最小者
- 最后遍历整个数组找出最大的乘积值
9.3 分割等和子集
分割等和子集(LeetCode 416)可以转化为背包问题。
java复制class Solution {
public boolean canPartition(int[] nums) {
//判断和是否为奇
int sum=0;
for(int