问题
给定一个升序排列的整型数组A,其元素的值都两两不相等。请设计一高效的算法找出中间所有A[i] = i的下标。 并分析其复杂度。(不分析复杂度不得分)
Java Code
public class FindSubscript {
@Test
public void test() {
int[] nums = new int[]{-2,-1,2,3,4};
ArrayList<Integer> res = findSubscript2(nums);
for(int ele : res) {
System.out.print(ele + " ");
}
}
/***********************************************************************************/
//版本一:对数组元素逐个遍历
public ArrayList<Integer> findSubscript1(int[] nums) {
int n = nums.length;
ArrayList<Integer> allSub = new ArrayList<Integer>();
for(int i = 0; i < n; ++i) {
if(nums[i] == i)
allSub.add(i);
else if(nums[i] > i)
break;
}
return allSub;
}
/***********************************************************************************/
//版本二:先二分查找区域中的某个元素,再分别线性查找区域的左边界和右边界
public ArrayList<Integer> findSubscript2(int[] nums) {
ArrayList<Integer> allSub = new ArrayList<Integer>();
int n = nums.length;
//如果数组为空,或其首元素(尾元素)大于(小于)自身的下标,则不可能会有元素值等于其下标
if(n == 0 || nums[0] > 0 || nums[n-1] < n-1)
return allSub;
//否则至少有一个元素的值等于其下标
int pivot = binaryFindPivot(nums, 0, n-1);
allSub.add(pivot);
//向左查找所有的目标元素
for(int i = pivot-1; i >= 0 && i == nums[i]; --i)
allSub.add(i);
//向右查找所有的目标元素
for(int i = pivot+1; i < n && i == nums[i]; ++i)
allSub.add(i);
return allSub;
}
//二分查找数组中元素值等于其下标的元素(保证至少有一个)
public int binaryFindPivot(int[] nums, int i, int j) {
int mid = (i + j)/2;
if(nums[mid] == mid)//如果如果中间元素的值正好等于其下标
return mid;
else if(nums[mid] > mid)//如果中间元素的值大于其下标,在左半边区域寻找目标元素
return binaryFindPivot(nums, i, mid - 1);
else
return binaryFindPivot(nums, mid + 1, j);
}
/***********************************************************************************/
//版本三:二分查找区域以及区域的左右边界
public ArrayList<Integer> findSubscript3(int[] nums) {
ArrayList<Integer> allSub = new ArrayList<Integer>();
int n = nums.length;
if(n == 0 || nums[0] > 0 || nums[n-1] < n-1) return allSub;
int[] bound = binaryFind(nums, 0, n-1);
for(int i = bound[0]; i <= bound[1]; ++i)
allSub.add(i);
return allSub;
}
//二分寻找数组中元素值等于下标的这个区域(即区域的左边界和右边界)
public int[] binaryFind(int[] nums, int i, int j) {
int mid = (i + j)/2;
if(nums[mid] == mid) {//如果如果中间元素的值正好等于其下标
i = binarySearchLower(nums, i, mid);//在左半边区域搜索左边界
j = binarySearchUpper(nums, mid, j);//在右半边区域搜索右边界
return new int[]{i, j};
}else if(nums[mid] > mid)//如果中间元素的值大于其下标,在左半边区域寻找左边界和右边界
return binaryFind(nums, i, mid - 1);
else
return binaryFind(nums, mid + 1, j);
}
//二分查找区域的左边界(数组在位置j附近有一段区域内元素值等于其下标)
public int binarySearchLower(int[] nums, int i, int j) {
int mid = (i + j)/2;//mid选择正中间或偏左
if(nums[mid] == mid) {//case1:如果中间元素的值正好等于其下标,
if(mid == 0 || nums[mid - 1] < mid - 1)//且其左边的元素小于下标值或当前元素是数组首元素,
return mid;//则已经找到左边界
else
j = mid;//否则继续在左半边区域搜索左边界
}else if(nums[mid] < mid) {//case2:如果中间元素的值小于其下标,
if(nums[mid + 1] == mid + 1) return mid + 1;//且其右边元素正好等于下标,则已经找到左边界
i = mid;//否则继续在右半边区域搜索左边界
}else//case3:如果中间元素的值大于其下标
j = mid;
return binarySearchLower(nums, i, j);
}
//二分查找区域的右边界(数组在位置i附近有一段区域内元素值等于其下标)
public int binarySearchUpper(int[] nums, int i, int j) {
int mid = (i + j + 1)/2;//mid选择正中间或偏右
if(nums[mid] == mid) {
if(mid == j || nums[mid + 1] > mid + 1)
return mid;
else
i = mid;
}else if(nums[mid] > mid) {
if(nums[mid - 1] == mid - 1) return mid - 1;
j = mid;
}else
i = mid;
return binarySearchUpper(nums, i, j);
}
}
分析
版本一解法的复杂度为O(n);版本二解法的复杂度为O(log(n))+O(m),其中m为数组中元素值等于下标的区域的长度;版本三解法的复杂度为O(log(n))+O(m),如果只需要查找区域的左右边界,则复杂度为O(log(n))。
本题的关键在于发现问题的规律,即如果升序数组中出现元素值等于其下标,则这些元素必定连成一个区间段,且不会出现2个以上的这种区间段。
由于数组是有序的,所以很容易想到用二分查找的方法,版本二和三的区别在于,前者只通过二分查找得到这个区间中的某一个元素,然后线性查找这个区间得到所有符合条件的元素,后者则是先用二分查找得到这个区间段在数组中的大致范围,然后继续用二分法查找这个区间的左边界和右边界。
代码中binarySearchLower函数中第9行的判断是不可省略的,因为当i+1=j时,mid本就等于i,此后指针i和j不会再发生变化了,所以如果不判断mid+1的值,则binarySearchLower会一直递归直到堆栈溢出。另外,每次判断mid+1有一个好处,如果mid + 1正好就是左边界,那么我们可以提前找到它而少几次二分查找。对于binarySearchUpper函数也是类似的。