数据结构&算法-----(7)二分搜索与贪心算法

数据结构&算法-----(7)二分搜索与贪婪

  • 二分搜索(Binary Search)
    • 递归解法
    • 时间复杂度
    • 非递归解法
    • 例题分析一:找确定的边界
    • 例题分析二:找模糊的边界
    • 例题分析三:旋转过的排序数组
    • 例题分析四:不定长的边界
  • 贪心算法(Greedy)
    • 无法全局最优举例:01背包问题
    • 例题一:LeetCode 第 253 题,最少需要会议室数量
          • 思路 1:暴力法
          • 思路 2:贪婪算法
          • 思路 3:开始结束时间分别排序法
    • 例题二:最少需要客服数量(携程笔试题)
    • 例题三:木棍问题(Tencent笔试题)
    • 例题四:一直忘不了的一个贪心题:LeetCode605种花问题(vivo2021届提前批笔试第一题)

二分搜索(Binary Search)

二分搜索(折半搜索)的Wikipedia定义:是一种在有序数组中查找某一特定元素的搜索算法。从定义可知,运用二分搜索的前提是数组必须是排好序的。另外,输入并不一定是数组,也有可能是给定一个区间的起始和终止的位置。

二分查找又称折半查找,优点是比较次数少,查找速度快,平均性能好;其缺点是要求待查表为有序表,且插入删除困难。因此,折半查找方法适用于不经常变动而查找频繁的有序列表

二分搜索算法看似简单,实则很难,而且变形很多。

优点:
时间复杂度是 O(lgn),非常高效。
因此也称为对数搜索

缺点:
要求待查找的数组或者区间是排好序的
对数组进行动态的删除和插入操作并完成查找,平均复杂度会变为 O(n)。此时应当考虑采取自平衡的二叉查找树:

  • O(nlogn) 的时间内用给定的数据构建出一棵二叉查找树;
  • O(logn) 的时间里对目标数据进行搜索;
  • O(logn) 的时间里完成删除和插入的操作。

因此,当输入的数组或者区间是排好序的,同时又不会经常变动,而要求从里面找出一个满足条件的元素的时候,二分搜索就是最好的选择。

二分搜索一般化的解题思路如下:

  • 从已经排好序的数组或区间中取出中间位置的元素,判断该元素是否满足要搜索的条件,如果满足,停止搜索,程序结束。
  • 如果正中间的元素不满足条件,则从它两边的区域进行搜索。由于数组是排好序的,可以利用排除法,确定接下来应该从这两个区间中的哪一个去搜索。
  • 通过判断,如果发现真正要找的元素在左半区间的话,就继续在左半区间里进行二分搜索。反之,就在右半区间里进行二分搜索。

递归解法

优点:简洁;
缺点:执行消耗大

例题:假设我们要从一个排好序的数组里 {1, 3, 4, 6, 7, 8, 10, 13, 14} 查看一下数字 7 是否在里面,如果在,返回它的下标,否则返回 -1。

递归写法的代码模板如下:

// 二分搜索函数的定义里,除了要指定数组 nums 和目标查找数 target 之外
// 还要指定查找区间的起点和终点位置,分别用 low 和 high 去表示。
int binarySearch(int[] nums, int target, int low, int high) {
	// 为了避免无限循环,先判断,如果起点位置大于终点位置,表明这是一个非法的区间
	// 已经尝试了所有的搜索区间还是没能找到结果,返回 -1。
	if (low > high) {
        return -1;
    }

	// 取正中间那个数的下标 middle。
    int middle = low + (high - low) / 2;

    // 判断一下正中间的那个数是不是要找的目标数 target,是,就返回下标 middle。 
    if (nums[middle] == target) {
        return middle;
	}
	
    // 如果发现目标数在左边,就递归地从左半边进行二分搜索。
	if (target < nums[middle]) {
		return binarySearch(nums, target, low, middle - 1);
	} else {
		return binarySearch(nums, target, middle + 1, high);
	}//否则从右半边递归地进行二分搜索。
}

注意:

  • 在计算 middle 下标的时候,不能简单地用 (low + hight) / 2,可能会导致溢出。
  • 在取左半边以及右半边的区间时,左半边是 [low, middle - 1],右半边是 [middle + 1, high],这是两个闭区间。因为已经确定了 middle 那个点不是我们要找的,就没有必要再把它加入到左、右半边了。
  • 对于一个长度为奇数的数组,例如:{1, 2, 3, 4, 5},按照 low + (high - low) / 2 来计算,middle 就是正中间的那个位置,对于一个长度为偶数的数组,例如 {1, 2, 3, 4},middle 就是正中间靠左边的一个位置。

时间复杂度

假设我们要对长度为 n 的数组进行二分搜索,T(n) 是执行时间函数,我们可以得到
T(n) = T(n/2) + 1
代入公式法得:a = 1,b = 2,f(n) = 1
因此:O(nlog(b)a) = O(n0) = 1 等于 O(f(n)),时间复杂度就是 O(nlog(b)alogn) = O(logn)

“对数搜索”

非递归解法

非递归写法的代码模板如下:

int binarySearch(int[] nums, int target, int low, int high) {

    // 在 while 循环里,判断搜索的区间范围是否有效
    while (low <= high) {
    
        // 计算正中间的数的下标
        int middle = low + (high - low) / 2;

	    // 判断正中间的那个数是不是要找的目标数 target。如果是,就返回下标 middle
	    if (nums[middle] == target) {
	        return middle;
	    }
	
		// 如果发现目标数在左边,调整搜索区间的终点为 middle - 1;否则,调整搜索区间的起点为 middle + 1
		if (target < nums[middle]) {
			high = middle - 1;
		} else {
			low = middle + 1;
		}
    }
    
    // 如果超出了搜索区间,表明无法找到目标数,返回 -1  
    return -1;
}

核心步骤

  • 确定搜索的范围和区间
  • 取中间的数判断是否满足条件
  • 如果不满足条件,判定应该往哪个半边继续进行搜索

二分搜索看起来简单,但是ProgrammingPearls这本书的作者JonBentley提到,只有10%的程序员能正确地写出二分搜索的代码。面试题经常是经典二分搜索的变形,但万变不离其中,需要把握好二分搜索的核心。

例题分析一:找确定的边界

边界分上边界和下边界,有时候也被成为右边界和左边界。确定的边界指边界的数值等于要找的目标数。

例题:LeetCode 第 34 题,在一个排好序的数组中找出某个数第一次出现和最后一次出现的下标位置。

示例:输入的数组是:{5, 7, 7, 8, 8, 10},目标数是 8,那么返回 {3, 4},其中 3 是 8 第一次出现的下标位置,4 是 8 最后一次出现的下标位置。

解题思路:
在二分搜索里,比较难的是判断逻辑,对这道题来说,什么时候知道这个位置是不是 8 第一次以及最后出现的地方呢?

把第一次出现的地方叫下边界(lower bound),把最后一次出现的地方叫上边界(upper bound)。

那么成为 8 的下边界的条件应该有两个。
(1)该数必须是 8;
(2)该数的左边一个数必须不是 8:

  • 8 的左边有数,那么该数必须小于 8;
  • 8 的左边没有数,即 8 是数组的第一个数。

而成为 8 的上边界的条件也应该有两个。
(1)该数必须是 8;
(2)该数的右边一个数必须不是 8:

  • 8 的右边有数,那么该数必须大于8;
  • 8 的右边没有数,即 8 是数组的最后一个数。

用递归的方法来寻找下边界,代码如下:

int searchLowerBound(int[] nums, int target, int low, int high) {
    if (low > high) {
        return -1;
    }

    int middle = low + (high - low) / 2;
    
	// 判断是否是下边界时,先看看 middle 的数是否为 target,并判断该数是否已为数组的第一个数,
	// 或者,它左边的一个数是不是已经比它小,如果都满足,即为下边界。
    if (nums[middle] == target && (middle == 0 || nums[middle - 1] < target)) {
        return middle;
    }

	if (target <= nums[middle]) {
		return searchLowerBound(nums, target, low, middle - 1);
	} else {
		return searchLowerBound(nums, target, middle + 1, high);
	} //不满足,如果这个数等于 target,那么就得往左边继续查找。
}

其他的写法都和经典的二分搜索的写法相同,不再赘述。

查找上边界的代码如下:

int searchUpperBound(int[] nums, int target, int low, int high) {
    if (low > high) {
        return -1;
    }

    int middle = low + (high - low) / 2;

    // 判断是否是上边界时,先看看 middle 的数是否为 target,并判断该数是否已为数组的最后一个数,
    // 或者,它右边的数是不是比它大,如果都满足,即为上边界。
    if (nums[middle] == target && (middle == nums.length - 1 || nums[middle + 1] > target)) {
        return middle;
    }
    
	if (target < nums[middle]) {
		return searchUpperBound(nums, target, low, middle - 1);
	} else {
		return searchUpperBound(nums, target, middle + 1, high);
	} //不满足时,需判断搜索方向。
}

LeetCode第34题完整代码:

    public int [] searchRange(int [] nums, int target) {

        if (nums.length == 0)
            return new int [] {-1, -1};

        int [] ans = new int [] {-1,-1};

        ans[0]=findLeft(nums, 0, nums.length-1, target);

        //如果没有找到左边界, 不用再找右边界了
        if (ans[0] != -1) {
            //查找右边界从 左边界+1位置 开始
            ans[1]=findRight(nums, ans[0]+1, nums.length-1, target);
        }

        return ans;
    }

    public int findLeft(int [] nums, int left, int right, int target) {
        if (left > right)
            return -1;

        int middle = left+(right-left)/2;

        //左边界必须大于之前的数, 1.没有前一个数 2.左边界>前一个数
        if (nums[middle] == target && (middle==0 || nums[middle-1] < target)) {
            return middle;
        }

        if (nums[middle] >= target) {
            return findLeft(nums, left, middle-1, target);
        } else {
            return findLeft(nums, middle+1, right, target);
        }
    }

    public int findRight(int [] nums, int left, int right, int target) {
        if (left > right)
            return -1;

        int middle = left+(right-left)/2;

        //右边界必须小于之后的数 1.没有后一个数 2.右边界<后一个数
        if (nums[middle] == target && (middle==nums.length-1 || nums[middle+1] > target)) {
            return middle;
        }

        if (nums[middle] <= target) {
            return findRight(nums, middle+1, right, target);
        } else {
            return findRight(nums, left, middle-1, target);
        }
    }

例题分析二:找模糊的边界

二分搜索可以用来查找一些模糊的边界。模糊的边界指,边界的值并不等于目标的值,而是大于或者小于目标的值。

例题:从数组 {-2, 0, 1, 4, 7, 9, 10} 中找到第一个大于 6 的数。

解题思路
在一个排好序的数组里,判断一个数是不是第一个大于 6 的数,只要它满足如下的条件:

  • 该数要大于 6;
  • 该数有可能是数组里的第一个数,或者它之前的一个数比 6 小。

代码实现:

Integer firstGreaterThan(int[] nums, int target, int low, int high) {
    if (low > high) {
        return null;
    }

    int middle = low + (high - low) / 2;

	//判断middle指向的数是否为第一个比target大的数时,须同时满足两个条件:
	//1.middle这个数必须大于target;
	//2.middle 要么是第一个数,要么它之前的数小于或者等于 target。
    if (nums[middle] > target && (middle == 0 || nums[middle - 1] <= target)) {
        return middle;
    }

	if (target < nums[middle]) {
		return firstGreaterThan(nums, target, low, middle - 1);
	} else {
		return firstGreaterThan(nums, target, middle + 1, high);
	}
}

对于这道题,当不满足条件,而middle的数等于target的时候怎么办?
举例说明,如果要求的是第一个大于6的数,而数组中有多个 6 挨在一起,而此时的 middle 指向其中的一个 6,程序必须得在右半边搜索。

找模糊边界的题,还有在给定数组里,找最后一个比目标数小的数。
举例:在 {-2, 0, 1, 4, 7, 9, 10} 中,求最后一个比 6 小的数。
答案是 4,方法是类似的。

例题分析三:旋转过的排序数组

二分搜索也能在经过旋转了的排序数组中进行。

例题:LeetCode 第 33 题,给定一个经过旋转了的排序数组,判断一下某个数是否在里面。

示例:给定数组为 {4, 5, 6, 7, 0, 1, 2},target 等于 0,答案是 4,即 0 所在的位置下标是 4。

数据结构&算法-----(7)二分搜索与贪心算法_第1张图片

解题思路
对于这道题,输入数组不是完整排好序,还能运用二分搜索吗?思路如下。

一开始,中位数是 7,并不是我们要找的 0,如何判断往左边还是右边搜索呢?这个数组是经过旋转的,即,从数组中的某个位置开始划分,左边和右边都是排好序的。

如何判断左边是不是排好序的那个部分呢?只要比较nums[low]nums[middle]

  • nums[low]<=nums[middle]时,能判定左边这部分一定是排好序的,
  • 否则,右边部分一定是排好序的。

为什么要判断 nums[low] = nums[middle] 的情况呢?
因为计算 middle 的公式是 int middle = low + (high - low) / 2
当只有一个数的时候,low=high,middle=low,同样认为这一边是排好序的。

判定某一边是排好序的,有什么用处呢?能准确地判断目标值是否在这个区间里。

  • 如果 nums[low] <= target && target < nums[middle],则应该在这个区间里搜索目标值。
  • 反之,目标值肯定在另外一边。

LeetCode代码:

    public int search(int[] nums, int target) {

        if (nums.length == 0)
            return -1;

        return binarySearch(nums, target, 0, nums.length-1);
    }

    public int binarySearch(int[] nums, int target, int low, int high) {

        //判断是否已超出了搜索范围,是则返回-1。
        if (low > high) {
            return -1;
        }

        int middle = low + (high - low) / 2;

        if (nums[middle] == target) {
            return middle;
        }

        if (nums[low] <= nums[middle]) { //左半边是排好序的
            if (nums[low] <= target && target < nums[middle]) { //目标值在左半边
                return binarySearch(nums, target, low, middle - 1); //在左半边继续进行二分搜索
            }
            return binarySearch(nums, target, middle + 1, high); //目标值在右半边, 在右半边进行二分搜索
        } else { //右半边是排好序的
            if (nums[middle] < target && target <= nums[high]) { //目标值在右半边
                return binarySearch(nums, target, middle + 1, high); //在右半边继续进行二分搜索
            }
            return binarySearch(nums, target, low, middle - 1); //目标值在左半边, 在左半边进行二分搜索。
        }
    }

在决定在哪一边进行二分搜索的时候,利用了旋转数组的性质,这就是这道题的巧妙之处。

例题分析四:不定长的边界

前面介绍的二分搜索的例题都给定了一个具体范围或者区间,那么对于没有给定明确区间的问题能不能运用二分搜索呢?

例题:有一段不知道具体长度的日志文件,里面记录了每次登录的时间戳,已知日志是按顺序从头到尾记录的,没有记录日志的地方为空,要求当前日志的长度。

解题思路
可以把这个问题看成是不知道长度的数组,数组从头开始记录都是时间戳,到了某个位置就成为了空:
{2019-01-14, 2019-01-17, … , 2019-08-04, …. , null, null, null ...}

思路 1:
顺序遍历该数组,一直遍历下去,当发现第一个 null 的时候,就知道了日志的总数量。很显然,这是很低效的办法。

思路 2:
借用二分搜索的思想,反着进行搜索。

  • 一开始设置 low = 0,high = 1
  • 只要 logs[high] 不为 null,high *= 2
  • logs[high]null 的时候,可以在区间 [0, high] 进行普通的二分搜索

代码实现

// 先通过getUpperBound函数不断地去试探在什么位置会出现空的日志。
int getUpperBound(String[] logs, int high) {
    if (logs[high] == null) {
        return high;
    }
    return getUpperBound(logs, high * 2);
}

// 再运用二分搜索的方法去寻找日志的长度。
int binarySearch(String[] logs, int low, int high) {
    if (low > high) {
        return -1;
    }

    int middle = low + (high - low) / 2;

    if (logs[middle] == null && logs[middle - 1] != null) {
        return middle;
    }

	if (logs[middle] == null) {
		return binarySearch(logs, low, middle - 1);
	} else {
		return binarySearch(logs, middle + 1, high);
	}
}

判断是否是日志的结尾很简单,只要当前的日志为空,而前一个日志不为空即可。

贪心算法(Greedy)

贪婪算法的 Wikipedia 定义:是一种在每一步选中都采取在当前状态下最好或最优的选择,从而希望导致结果是最好或最优的算法。

优点:对于一些问题,非常直观有效。
缺点:

  • 并不是所有问题都能用它去解决;
  • 得到的结果并一定不是正确的,因为这种算法容易过早地做出决定,从而没有办法达到最优解。

无法全局最优举例:01背包问题

下面通过例题来加深对贪婪算法的认识。例题:0-1 背包问题,能不能运用贪婪算法去解决。

有三种策略:

  • 选取价值最大的物品
  • 选择重量最轻的物品
  • 选取价值/重量比最大的物品

策略1:每次尽可能选择价值最大的,行不通。举例说明如下。
物品有:ABC
重量分别是:25,10,10
价值分别是:100,80,80
根据策略,首先选取物品A,接下来就不能再去选其他物品,但是,如果选取 B 和 C,结果会更好。

策略 2:每次尽可能选择轻的物品,行不通。举例说明如下。
物品有:A B C
重量分别为:25, 10, 10
价值分别为:100, 5, 5
根据策略,首先选取物品 B 和 C,接下来就不能选 A,但是,如果选 A,价值更大。

策略 3:每次尽可能选价值/重量比最大的,行不通。举例说明如下。
物品有:A B C
重量是:25, 10, 10
价值是:25, 10, 10
根据策略,三种物品的价值/重量比都是一样,如果选 A,答案不对,应该选 B 和 C。

由上,贪婪算法总是做出在当前看来是最好的选择。即,它不从整体的角度去考虑,仅仅对局部的最优解感兴趣。因此,只有当那些局部最优策略能产生全局最优策略的时候,才能用贪婪算法。

例题一:LeetCode 第 253 题,最少需要会议室数量

LeetCode 第 253 题,会议室II,给定一系列会议的起始时间和结束时间,求最少需要多少个会议室就可以让这些会议顺利召开。

数据结构&算法-----(7)二分搜索与贪心算法_第2张图片

输入输出:
Input: [[0, 30],[5, 10],[15, 20]]
Output: 2

Input: [[7,10],[2,4]]
Output: 1

参考博文:
[LeetCode] 253. Meeting Rooms II

解题思路:

思路 1:暴力法
  • 把所有的会议组合找出来;
  • 从最长的组合开始检查,看看各个会议之间有没有冲突;
  • 直到发现一组会议没有冲突,那么它就是答案。
    很明显,这样的解法是非常没有效率的。
思路 2:贪婪算法
  • 会议按照起始时间顺序进行;
  • 要给新的即将开始的会议找会议室时,先看当前有无空会议室;
  • 有则在空会议室开会,无则开设一间新会议室。

代码实现:

int minMeetingRooms(Interval[] intervals) {
    if (intervals == null || intervals.length == 0)
        return 0;

    // 将输入的一系列会议按照会议的起始时间排序。(数字升序)
    Arrays.sort(intervals, new Comparator<Interval>() {
        public int compare(Interval a, Interval b) { 
        	return a.start - b.start; 
       	}
    });

    // 用一个最小堆来维护目前开辟的所有会议室,最小堆里的会议室按照会议的结束时间排序
    // 最先结束的会议, 优先级最高(数字升序)
	PriorityQueue<Interval> heap = 
		new PriorityQueue<Interval>(intervals.length, new Comparator<Interval>() {
		public int compare(Interval a, Interval b) { 
			return a.end - b.end; 
		}
	});

    // 让第一个会议在第一个会议室里举行。
    heap.offer(intervals[0]);

	for (int i = 1; i < intervals.length; i++) {
		// 从第二个会议开始,对于每个会议,我们都从最小堆里取出一个会议室,
		// 那么这个会议室里的会议一定是最早结束的。
		Interval interval = heap.poll();
		
		if (intervals[i].start >= interval.end) {
			// 若当前要开的会议可以等会议室被腾出才开始,那么就可以重复利用这个会议室。
			interval.end = intervals[i].end;
		} else {
			// 否则,开一个新的会议室。
			heap.offer(intervals[i]);
		}

	    // 把旧的会议室也放入到最小堆里。
	    heap.offer(interval);
    }

    // 最小堆里的会议室个数就是要求的答案,即最少的会议个数。
    return heap.size();
}

为什么贪婪算法能在这里成立?
每当遇到一个新的会议时,总是贪婪地从所有会议室里找出最先结束会议的那个。

为什么这样可以产生最优的结果?
若选择的会议室中会议未结束,则意味着需要开辟一个新会议室,这已经不是当前的最优解了

思路 3:开始结束时间分别排序法

把每个会议的开始时间和结束时间拎出来分别排序,再遍历intervals。遍历的时候,设置一个pointer = 0,
判断当前interval[i]的开始时间是否大于前一个会议的结束时间interval[pointer][end]

  • 如果大于,就说明不需要新开一个会议室;
  • 如果小于,就需要新开一个会议室。

因为题目只在意需要同时开几个会议室,所以start和end可以分别排序。
end只要结束一个,就意味着同时需要的会议室就少一个

  • 时间O(nlogn) - 因为有对input排序
  • 空间O(n) - 有用额外数组对start和end排序
class Solution {
    public int minMeetingRooms(int[][] intervals) {
        if (intervals == null || intervals.length == 0) {
            return 0;
        }
        int len = intervals.length;
        int[] start = new int[len];
        int[] end = new int[len];
        for (int i = 0; i < len; i++) {
            start[i] = intervals[i][0];
            end[i] = intervals[i][1];
        }
        Arrays.sort(start);
        Arrays.sort(end);
        
        int res = 0;
        int pointer = 0;
        for (int i = 0; i < len; i++) {
            if (start[i] < end[pointer]) {
                res++;
            } else {
                pointer++;
            }
        }
        return res;
    }
}

例题二:最少需要客服数量(携程笔试题)

输入一个n表示要输入的通话记录个数,接下来输入n行,每行为逗号相隔的两个整数,
两个数字分别代表呼入时间和挂断时间的时间戳。
举例:10,30,表示[10,30),代表第10秒呼入,第30秒已经挂断,
即第30秒可以接入新的来电; 每一行都是一条通话记录,通话记录已经按呼入时间由小到大排序
输出一个整数;代表最少需要多少客服,可以满足所有旅客来电不用等待。

测试样例:
6
030
050
1020
1530
2050
2065

样例输出:
5

实现代码:

  public static void main(String[] args) {
        Scanner sc =new Scanner(System.in);
        int count =Integer.parseInt(sc.nextLine().trim());
 
        int[][] rows =new int[count][2];
        for(int i=0;i<count;i++){
            String row =sc.nextLine();
            String[] split = row.split(",");
            rows[i][0]=Integer.parseInt(split[0]);
            rows[i][1]=Integer.parseInt(split[1]);
        }
 
        int res=0;
        res = calcMinStaff(rows);
        System.out.println(String.valueOf(res));
    }
    
    static int calcMinStaff(int[][] rows) {
        //每一行保存一个时间段,现需要对所有的时间端做一个分割
        //所有的开始时间放一起,所有的结束时间放一起
        //然后sort
        int[] startArray = new int[rows.length];
        int[] endArray =new int[rows.length];
        for(int i=0;i<rows.length;i++){
            startArray[i]=rows[i][0];
            endArray[i]=rows[i][1];
        }
        Arrays.sort(startArray);
        Arrays.sort(endArray);
        
        int cnt =0; //客服数量
        int eIndex=0;
        //算法关键
        for(int sIndex=0; sIndex<startArray.length; sIndex++){
            if(startArray[sIndex]<endArray[eIndex]){
                cnt++;//需要增加客服
            }
            else {
                eIndex++;//切换到下一个结束时间
            }
        }
        return cnt;
    }

比如输入[1,5],[4,7],[9,10],[2,3],单独对开始时间和结束时间排序,得到如下
startArray=[1,2,4,9] endArray=[3,5,7,10],定义客服数量cnt=0.

然后从最小的开始时间找,如果startArray[startIndex]大于当前的结束时间endArray[endIndex],说明当前不存在重合时间段,那么客服数量不需要增加;如果小于endArray[endIndex],那么说明存在重合时间段,需要增加一个客服。

(最开始startIndex=0,stopIndex=0)当然对于合法输入,排序后的第一个开始时间肯定小于第一个结束时间

根据上面的输入序列推一下:

startArray=[1,2,4,9] endArray=[3,5,7,10], cnt=0

1.startIndex=0,endIndex=0,cnt=0,此时1<3,–>cnt++
2.startIndex=1,endIndex=0,cnt=1,此时2<3,–>cnt++
3.startIndex=2,endIndex=0,cnt=2,此时4>3,–>endIndex++;
4.startIndex=3,endIndex=1,cnt=2,此时9>5,–>endIndex++;

退出循环 最后cnt=2。

例题三:木棍问题(Tencent笔试题)

题目描述:

现有n根木棒,已知它们的长度和重量。要用一部木工机一根一根地加工这些木棒。该机器在加工过程中需要一定的准备时间,是用于清洗机器,调整工具和模板的。木工机需要的准备时间如下:
(1)第一根木棒需要1min的准备时间;
(2)在加工了一根长为l,重为w的木棒之后,接着加工一根长为ll(l<=ll),重为ww(w<=ww)的木棒是不需要任何准备时间的。否则需要一分钟的准备时间。

给定n根木棒,你要找到最少的准备时间。
例如现在有长和重分别为(4,9),(5,2),(2,1),(3,5)和(1,4)的五根木棒,
那么所需准备时间最少为2min,
顺序为(1,4),(3,5),(4,9),(2,1),(5,2)。

输入
输入包含多组测试数据。输入的第一行是一个整数T,表示测试数据的个数。
每个测试例两行:
第一行是一个整数n(1<=n<=5000),表示有多少根木棒;
第二行包括n*2个整数,表示了l1,w1,l2,w2,l3,w3,…,ln,wn,这些数均不大于10000,其中liwi表示第i根木棒的长度和重量。

输出
输出以分钟为单位的最少准备时间。

样例输入

3 
5 
4 9 5 2 2 1 3 5 1 4 
3 
2 2 1 1 2 2 
3 
1 3 2 2 3 1

样例输出

2
1
3

贪心算法Java代码:

import java.util.Arrays;
import java.util.Comparator;
import java.util.Scanner;

public class GreedyStickProblem {

    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        int t = Integer.parseInt(sc.nextLine());
        while(t-->0) {
            int n = Integer.parseInt(sc.nextLine());
            Stick [] sticks = new Stick[n];
            String [] split = sc.nextLine().split(" ");
            for (int i=0; i<n; i++) {
                Stick tmpStick = new Stick(Integer.parseInt(split[2*i]), Integer.parseInt(split[2*i+1]));
                sticks[i] = tmpStick;
            }
            int ans = minTime(n, sticks);
            System.out.println(ans);
        }
        sc.close();
    }

    private static int minTime(int n, Stick [] sticks) {

        Arrays.sort(sticks, new Comparator<Stick>() {
            @Override
            public int compare(Stick o1, Stick o2) {
                if (o1.l != o2.l) {
                    return o1.l-o2.l;
                }
                return o1.w-o2.w;
            }
        });

        for (Stick tmpStick : sticks) {
            System.out.println(tmpStick.l+", "+tmpStick.w);
        }

        int minTime = 0;
        for (int i=0; i<n; i++) {
            //避免重复访问
            if (sticks[i].w>0) {
                minTime++;
                int curW = sticks[i].w;
                for (int j=i+1; j<n; j++) {
                    if (sticks[j].w>=curW) {
                        curW = sticks[j].w;
                        sticks[j].w = 0; //代表这根棍子已经访问过了
                    }
                }
            } //if
        }

        return minTime;
    }
}

class Stick {
    int l;
    int w;
    Stick(int l, int w) {
        this.l = l;
        this.w = w;
    }
}

例题四:一直忘不了的一个贪心题:LeetCode605种花问题(vivo2021届提前批笔试第一题)

假设你有一个很长的花坛,一部分地块种植了花,另一部分却没有。可是,花卉不能种植在相邻的地块上,它们会争夺水源,两者都会死去。

给定一个花坛(表示为一个数组包含0和1,其中0表示没种植花,1表示种植了花),和一个数 n 。能否在不打破种植规则的情况下种入 n 朵花?能则返回True,不能则返回False。

示例 1:
输入: flowerbed = [1,0,0,0,1], n = 1
输出: True

示例 2:
输入: flowerbed = [1,0,0,0,1], n = 2
输出: False

方法一:贪心

我们从左到右扫描数组 flowerbed,如果数组中有一个 0,并且这个 0 的左右两侧都是 0,那么我们就可以在这个位置种花,即将这个位置的 0 修改成 1,并将计数器 count 增加 1。对于数组的第一个和最后一个位置,我们只需要考虑一侧是否为 0。

在扫描结束之后,我们将 count 与 n 进行比较。如果 count >= n,那么返回 True,否则返回 False。

    private static boolean canPlaceFlowers(int[] flowerbed, int n) {
        int count = 0;
        for (int i=0; i<flowerbed.length; i++) {
            if (flowerbed[i] == 0 &&
                    (i == 0 || flowerbed[i - 1] == 0) &&
                    (i == flowerbed.length - 1 || flowerbed[i + 1] == 0)) {
                flowerbed[i] = 1;
                count++;
            }
        }
        return count >= n;
    }
  • 时间复杂度:O(N),其中 N 是数组 flowerbed 的长度。
  • 空间复杂度:O(1)。

方法二:贪心 + 常数优化

我们可以优化方法一中的常数。在扫描数组 flowerbed 时,如果 count 的值已经达到 n,那么我们可以直接跳出循环并返回 True。

    private static boolean canPlaceFlowers(int[] flowerbed, int n) {
        int count = 0;
        for (int i=0; i<flowerbed.length; i++) {
            if (flowerbed[i] == 0 &&
                    (i == 0 || flowerbed[i - 1] == 0) &&
                    (i == flowerbed.length - 1 || flowerbed[i + 1] == 0)) {
                flowerbed[i] = 1;
                count++;
            }
            if (count>=n) {
                return true;
            }
        }
        return false;
    }
  • 时间复杂度:O(N),其中 N 是数组 flowerbed 的长度。
  • 空间复杂度:O(1)。

一点反思:
遇到这个题想的太复杂了,想到【边界补0】和【找三个以上连续的0】
这个解法用py解比较快 ,晒一下解法

class Solution(object):
    def canPlaceFlowers(self, flowerbed, n):
        """
        :type flowerbed: List[int]
        :type n: int
        :rtype: bool
        """
        # 前后补零解决边界问题
        nums=[0]+flowerbed+[0]
        cnt=0
        i=1
        while i<len(flowerbed)+1:
            if nums[i-1]==0 and nums[i]==0 and nums[i+1]==0:
                cnt += 1
                # 可以种花,则需要间隔一个位置,所以+2
                i += 2
            else:
                i += 1
                
        return cnt>=n

你可能感兴趣的:(数据结构&算法)