前端刷题笔记(二)贪心+二分算法

185 阅读4分钟

参考:

贪心算法

贪心算法本质:通过问题中⼀些隐藏较深的规律,来减少冗余计算。

134. 加油站

/**
 * @param {number[]} gas
 * @param {number[]} cost
 * @return {number}
 */
var canCompleteCircuit = function(gas, cost) {
    // 油量变化值为gas[i]-cost[i];
    // 希望从起点出发,油量变化值的和始终大于0,那么就可以环绕一圈
    // 因此,找到那个使得油量变化值之和最小的点,作为起点
    let start = 0;
    let sum = 0;
    let minSum = Infinity;
    for(let i=0; i< gas.length;i++) {
        sum+= gas[i]-cost[i];
        if(sum<minSum) {
            start = i+1;
            minSum = sum;
        }
        // console.log("sum="+sum);
    }
    if(sum<0) return -1;
    return start===gas.length? 0 : start;
};

剑指 Offer 14- I. 剪绳子

/**
 * @param {number} n
 * @return {number}
 */
var cuttingRope = function(n) {
    // 通过数学公式推导可得:最好分隔为长度为3的多份;其次是最后一份长度为2;如果最后一份长度为1,则将前一份拆为2+2;
    let a = Math.floor(n/3);
    let b = n%3;
    console.log("a=",a,"b=",b);
    if(n<=3) return n-1;
    if(b===0) return Math.pow(3,a);
    if(b===2) return Math.pow(3,a)*2;
    if(b===1) return Math.pow(3,a-1)*4;
};

贪心算法玩跳跃游戏

55. 跳跃游戏

/**
 * @param {number[]} nums
 * @return {boolean}
 */
var canJump = function(nums) {
    // 计算从当前位置出发,能否到达最后的坐标
    // [2,4,3,4,8]
    let n = nums.length;
    // let jump = new Array(n).fill(0);
    let maxDis = 0; //记录当前能走到的最大坐标
    for(let i=0;i<n-1;i++) {
        // jump[i] = i + nums[i];
        if(i>maxDis) return false;  //走不到i的位置的话,返回false
        maxDis = Math.max(maxDis,i+nums[i]);
    }
    return maxDis >= n-1 ? true :false;

};

45. 跳跃游戏 II

/**
 * @param {number[]} nums
 * @return {number}
 */
var jump = function(nums) {
    let n = nums.length;
    let farthest = 0;
    let end = 0;
    let jumps = 0;
    for(let i = 0;i<n-1;i++){
        // 假设你总是可以到达数组的最后一个位置
        farthest = Math.max(farthest,nums[i]+i);
        if(end == i) {
            // 每次都选择能够跳到最远的位置
            end = farthest;
            jumps++;
        }
    }
    return jumps;
};

贪心算法做区间调度

给你很多形如[start,end]的闭区间,请你设计一个算法,算出这些区间中最多有几个互不相交的区间

例如:intvs=[[1,3],[2,4],[3,6]],这些区间最多有两个区间互不相交,即[[1,3],[3,6]],你的算法应该返回 2。注意边界相同并不算相交。

思路:

  1. 首先将各个闭区间,按照end数值升序排序;
  2. 每次选择最早结束的区间,并将与这个区间相交的区间去除;
  3. 重复步骤 2,直到 intvs 为空为止。之前选出的那些 x 就是最大不相交子集。

452. 用最少数量的箭引爆气球

/**
 * @param {number[][]} points
 * @return {number}
 */
function sortInterval(a,b) {
    return a[1] - b[1];
}

var findMinArrowShots = function(points) {
    // 问题本质:求互不相交的区间的数量
    points.sort(sortInterval);
    // console.log(points);
    let x_end = points[0][1];
    let count = 1;
    for(point of points) {
        if(point[0]>x_end){
            x_end = point[1];
            count++;
        }
    }
    return count;
};

435. 无重叠区间

/**
 * @param {number[][]} intervals
 * @return {number}
 */
 // 去除区间的最小数量,使剩余区间互不重叠,因此只需要返回(n-最大不相交子集数)
 // 二维数组的排序
var sortInterval = function(a,b) {
    return a[1] - b[1];
}

var eraseOverlapIntervals = function(intervals) {
    intervals.sort(sortInterval);
    // console.log(intervals);
    let count = 1; //用于记录不相交的区间
    let x_end = intervals[0][1];//最早结束的区间
    let n = intervals.length;
    for(interval of intervals) {
        if(interval[0]>=x_end) {
            count++;
            x_end = interval[1];
        }
    }
    return n-count;
};

1024. 视频拼接

/**
 * @param {number[][]} clips
 * @param {number} time
 * @return {number}
 */
var sortClips = function(a,b) {
    if(a[0]===b[0]){
        return b[1]-a[1];
    }
    return a[0]-b[0];
}
var videoStitching = function(clips, time) {
// 1.按照起点对clips中的视频升序排序,如果多个视频起点相同,那么按照终点降序排序
// 2.因此clip[0]必定会被选择
// 3.下一个选择start比上一个视频end小的(必定重合),且长度最大的
    clips.sort(sortClips);
    // console.log(clips);
    let count = 0;
    let n = clips.length;
    let cur_end = 0;
    let next_end = 0;
    let i = 0;
    while(i<n && clips[i][0]<=cur_end) {
        while( i<n && clips[i][0]<=cur_end){
            next_end = Math.max(clips[i][1],next_end);
            i++;
        }
        cur_end = next_end;
        count++;
        if(cur_end>=time) return count;
    }
    return -1; 
};

二分

针对有序数列进行查找时,优先考虑二分,最经典的二分搜索的算法复杂度为O(logN);

几个需要注意的地方:

  • mid =left+Math.floor((right-left)/2)mid = Math.floor((left+right)/2) 结果是一样的,但是可以有效防止由于left+right过大而导致溢出的情况;
  • right = nums.length-1时,说明搜索的区间为[left,right],是一个闭区间;因此停止搜索的条件应该是该区间为空,即left>right的情况,故while循环中的条件应该为left<=right;
  • 因此 left = mid+1,right = mid -1
//二分搜索的基本思路
 var binarySearch = function(nums,target){
     let left = 0, right = ...;
     while(...){
         mid = left + Math.floor((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 ...
 }
 
 //寻找左侧边界的二分搜索,如nums = [1,2,2,2,3],target 为 2,此算法返回的索引是找到的第一个target值
 //寻找右边界:返回的索引是找到的最后一个target值
     if(nums[mid]===target){
             //找左边界
             right = mid -1;
             //找右边界
             left = mid + 1;
         } else if(nums[mid]<target) {
             left = mid + 1;
         } else if(nums[mid]>target) {
             right = mid -1;
         }
     //边界情况(找左边界)
     if(left>=nums,length || nums[left] !== target) return -1;
     //边界情况(找右边界)
     if(right<0 ||  nums[right] !== target) return -1;

704. 二分查找

/**
 * @param {number[]} nums
 * @param {number} target
 * @return {number}
 */
var search = function(nums, target) {
    let left = 0,right = nums.length -1; //闭区间[left,right];
    // 当区间为空的时候,停止搜索,即为left>right
    let mid;
    while(left<=right) {
        mid = left+Math.floor((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;
};

34. 在排序数组中查找元素的第一个和最后一个位置

/**
 * @param {number[]} nums
 * @param {number} target
 * @return {number[]}
 */
var searchRange = function(nums, target) {
    let range = new Array(2).fill(0);
    let left = 0;
    let right = nums.length - 1;
    let mid;
    //找左边界
    while(left <= right) {
        mid = left + Math.floor((right-left)/2);
        if(nums[mid]===target) {
            right = mid -1;
        } else if(nums[mid] > target) {
            right = mid -1;
        } else if(nums[mid] < target) {
            left = mid + 1;
        }
    }
    if(nums[left]!=target || left>=nums.length) return [-1,-1];
    range[0] = left;
    //找右边界
    left = 0; right=nums.length-1;
    while(left <= right) {
        mid = left + Math.floor((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;
        }
    }
    range[1] = right;
    return range;
};

剑指 Offer 11. 旋转数组的最小数字

/**
 * @param {number[]} numbers
 * @return {number}
 */
var minArray = function(numbers) {
    let left = 0;
    let right = numbers.length-1;
    let mid;
    while(left<right){
         mid = left+Math.floor((right-left)/2);
        if(numbers[right]<numbers[mid]) {
            left = mid+1;
        } else if(numbers[right] > numbers[mid]) {
            right = mid;
        } else {
            right--;
        }
    }
    return numbers[left];
};

剑指 Offer 53 - II. 0~n-1中缺失的数字 - 力扣(LeetCode)

/**
 * @param {number[]} nums
 * @return {number}
 */
//特征:找到使得 数组的值nums[mid]>数组下标mid 的最开始的那个值
var missingNumber = function(nums) {
    let left = 0;
    let right = nums.length-1;
    let mid;
    while(left<=right) {
        mid = left+Math.floor((right-left)/2);
        if(nums[mid]===mid) {
            left = mid+1;
        } else if(nums[mid]>mid) {
            right = mid-1;
        }
    }
    return left;
};

240. 搜索二维矩阵 II

/**
 * @param {number[][]} matrix
 * @param {number} target
 * @return {boolean}
 */
 // 1. 选取左下角的值作为初始值key 
 // 2. 如果目标值大于key,因为是最左边的值(最小),所以col++
 // 3. 如果目标值小于,那么更小的值只可能是上一行,所以row--
var searchMatrix = function(matrix, target) {
    let row = matrix.length-1;
    let col = 0;
    while(row>=0 && col<=matrix[0].length-1){
        if(matrix[row][col]===target) {
            return true;
        } else if(matrix[row][col]>target) {
            row--;
        }  else if(matrix[row][col]<target) {
            col++;
        }
    }
    return false;
};

二分搜索的泛化

  • 首先,你要从题目中抽象出一个自变量 x,一个关于 x 的函数 f(x),以及一个目标值 target
    • f(x) 必须是在 x 上的单调函数(单调增单调减都可以)
    • 题目是让你计算满足约束条件 f(x) == target 时的 x 的值
  • 找到 x 的取值范围作为二分搜索的搜索区间,初始化 left 和 right 变量。
  • 根据题目的要求,确定应该使用搜索左侧还是搜索右侧的二分搜索算法,写出解法代码。 875. 爱吃香蕉的珂珂
/**
 * @param {number[]} piles
 * @param {number} h
 * @return {number}
 */
// 定义:速度为 x 时,需要 f(x) 小时吃完所有香蕉
// f(x) 随着 x 的增加单调递减
var f = function(piles,x) {
    let hours = 0;
    for(let i=0;i<piles.length;i++) {
        hours += Math.floor(piles[i]/x);
        if(piles[i]%x != 0) hours++;
    }
    return hours;
}
// 找最小的能吃完的速度,左边界
var minEatingSpeed = function(piles, h) {
    let left = 1, right=0 , mid;
    for(let i=0;i<piles.length;i++) right+=piles[i];
    // console.log(left,right);
    while(left<=right) {
        mid = left + Math.floor((right-left)/2);
        // console.log(left,right,mid,f(piles,mid));
        if(f(piles,mid) === h) {
           right = mid-1;
        } else if(f(piles,mid)<h) {
            right = mid-1;
        } else if(f(piles,mid)>h) {
            // 在这个速度下,所需的时间大于警卫回来的时间
            left = mid + 1;
        }
    }
    return left;
};

1011. 在 D 天内送达包裹的能力

/**
 * @param {number[]} weights
 * @param {number} days
 * @return {number}
 */

// f(x,weights) 为每天送x的量,返回的天数 
// f(x)是一个单调递减函数
var f = function(x,weights) {
    let day=0;
    for(let i=0; i<weights.length;) {
         let cap = x;
         while (i < weights.length) {
                if (cap < weights[i]) break;
                else cap -= weights[i];
                i++;
        }
        day++;
    }
    return day;
}
var shipWithinDays = function(weights, days) {
    let left = 1,right=0, mid;
    for(let i=0; i<weights.length;i++) {
        left = Math.max(weights[i],left);
        right += weights[i];
    }
    while(left<=right) {
        mid = left + Math.floor((right-left)/2);
        if(f(mid,weights)=== days){
            right = mid -1;
        } else if(f(mid,weights)>days){
            left = mid + 1;
        } else if(f(mid,weights)<days){
            right = mid -1;
        }
    }
    return left;
};