在LeetCode中有一道搜索问题:https://leetcode-cn.com/explore/interview/card/top-interview-questions-medium/50/sorting-and-searching/99/
如果所用数据结构是数组,要求在对数时间复杂度内完成,那么往往会用到分而治之算法,经常将数组一分为二,递归求解。问题的关键是怎么分解数组。
public int findPeakElement(int[] nums) {
if (nums == null || nums.length == 0) {
return -1;
}
if (nums.length == 1) { // 特殊数组
return 0;
}
return findPeakElement(nums, 0, nums.length - 1);
}
private int findPeakElement(int[] nums, int p, int q) {
if (p > q) {
return -1;
}
int m = p + (q - p) / 2;
// 得到的中点m,可能落在数组的两段,考虑到这种情况,需要分类讨论。
if (m == 0 ) {
if (nums[m] > nums[m + 1]) {
return 0;
} else {
return findPeakElement(nums, m + 1, q);
}
}
if (m == nums.length -1) {
if (nums[m] > nums[m - 1]) {
return m;
} else {
return findPeakElement(nums, p, m - 1);
}
}
if (nums[m] > nums[m + 1] && nums[m] > nums[m - 1]) {
return m;
} else if (nums[m] < nums[m + 1]) {
return findPeakElement(nums, m + 1, q);
} else {
return findPeakElement(nums, p, m - 1);
}
}
假设数组非常大,那么会有什么问题呢?递归的深度很大,导致StackOverFlow,那么怎么办呢?改成递推的写法。
public int findPeakElement(int[] nums) {
if (nums == null || nums.length == 0) {
return -1;
}
// 不可或缺,如果删掉下面的if语句,
// 那么 if (nums[m] > pre && nums[m] > next) 必须改成
// 那么 if (nums[m] >= pre && nums[m] >= next)
if (nums.length == 1) {
return 0;
}
int len = nums.length, p = 0, q = nums.length - 1;
while (p <= q) {
int m = p + (q - p) / 2;
int pre = m - 1 >= 0 ? nums[m - 1] : Integer.MIN_VALUE;
int next = m + 1 < len ? nums[m + 1] : Integer.MIN_VALUE;
if (nums[m] > pre && nums[m] > next) {
return m;
}
if (nums[m] < next) {
p = m + 1;
} else {
q = m - 1;
}
}
return -1;
}
这种写法需要注意:
1)while的结束条件是p<q还是p<=q?
2)对于数组越界情况,使用了特殊值来代替,这样比上段代码简洁了不少,但这样会带来问题吗?不会。因为m元素是什么值,不可能比min_value更小,只能是大于等于。
再看一种颇为神奇的写法:)
public int findPeakElement(int[] nums) {
if (nums == null || nums.length == 0) {
return -1;
}
int p = 0, q = nums.length - 1;
while (p <= q) {
if (p == q) {
return p;
}
int m = p + (q - p) / 2;
if (nums[m] < nums[m + 1]) {
p = m + 1;
} else {
q = m;
}
}
return -1;
}
这段代码和之前的二分搜索不太一样,它并没有判断m节点是不是峰值,而是通过不断缩小范围来逼近峰值,这提供了一种分而治之的新思路。但这乍一看并不好理解,其中原理是什么呢?让我们换个角度来思考:该算法会排除掉一半的元素,保证解一定在另一边的元素里,反复循环,那么每次保留的元素均为之前的一半,肯定可以缩小成1个元素,那么该点必是峰值。
这种算法的运行时间要慢些,因为总是要把范围缩小成1才得出结果。
2019-02-25:
重做这道题目,发现以前写得代码不够优雅。不如临时所写:
public int findPeakElement(int[] arr) {
if (arr == null || arr.length == 0) {
return -1;
}
int p = 0;
int q = arr.length - 1;
while (p <= q) {
int x = p + (q - p) / 2;
boolean isGreaterThanRight = x + 1 >= arr.length || arr[x] > arr[x + 1];
boolean isGreaterThanLeft = x - 1 < 0 || arr[x] > arr[x - 1];
if (isGreaterThanRight && isGreaterThanLeft) {
return x;
}
if (!isGreaterThanRight) {
p = x + 1;
} else {
q = x - 1;
}
}
return -1;
}
再来一个递归版本
public int findPeakElement(int[] arr, int p, int q) {
if (arr == null || p > q) {
return -1;
}
int x = p + (q - p) / 2;
System.out.println(p + "\t" + q + "\t" + x);
boolean isGreaterThanRight = x + 1 >= arr.length || arr[x] > arr[x + 1];
boolean isGreaterThanLeft = x - 1 < 0 || arr[x] > arr[x - 1];
if (isGreaterThanRight && isGreaterThanLeft) {
return x;
}
if (!isGreaterThanRight) {
return findPeakElement(arr, x + 1, q);
} else {
return findPeakElement(arr, p, x - 1);
}
}
这两段代码清晰的关键是运用了处理边界的技巧,以及布尔值技巧。