图解:搜索排序旋转数组

Python实战社群

Java实战社群

长按识别下方二维码,按需求添加

扫码关注添加客服

进Python社群▲

扫码关注添加客服

进Java社群

作者丨景禹

来源丨景禹

题目来源于力扣 33. 搜索旋转排序数组。以此题谈一谈二分思想。

问题描述

升序排列的整数数组 arr 在预先未知的某个点上进行了旋转(例如, [1,2,3,4,5,6,7] 经旋转后可能变为 [3,4,5,6,7,1,2] )。

请你在数组中搜索 key,如果数组中存在这个目标值,则返回它的索引,否则返回 -1 。

输入输出

输入:arr[ ] = [3,4,5,6,7,1,2]  key = 5

输出:2

输入:arr[ ] = [3,4,5,6,7,1,2]  key = 8

输出:-1

提示信息(一定要注意)

1 <= n <= 5000 // 数组长度的要求
-10^4 <= arr[i] <= 10^4  // 数组中元素的取值范围
arr[ ] 中的每个值都 独一无二 // 解题范围
arr[ ] 肯定会在某个点上旋转 // 必定是旋转数组
-10^4 <= key <= 10^4 // 查找关键字 key 的取值范围

题目解析

这道题目最直观的解法是从头到尾遍历数组一次,我们就能找出要查找的元素 key。这种思路的时间复杂度显然是 。第一反应是这种思路的一定是 "语文阅读理解" 能力欠佳,没有抓住问题描述中的 搜索(查找)升序排序旋转数组 三个关键字,一个抓不住面试官提问信息的面试者,必然是得不到面试官认可的。

首先自问何为升序排序的旋转数组?不懂就主动向面试官询问

套路:查找有序数组(包括旋转数组)中的元素优先考虑二分查找算法

二分查找法查找有序数组中元素的时间复杂度为 。但是题目中的数组为升序排序的旋转数组,并不严格满足二分查找的有序性条件。比如原有序数组为 arr[] = {1,2,3,4,5,6,7} ,升序排序的旋转数组为 rotateArray[] = {4,5,6,7,1,2,3}rotateArray 整体不再满足有序性。

但是,我们注意到 rotateArray 实际上可以划分为两个排序的子数组,{4,5,6,7}{1,2,3} 。我们还注意到这两个子数组的分界线两侧的元素一定满足 rotateArray[pivot] > rotateArray[pivot+1] ,其中 pivot 表示旋转数组中最大元素的下标, pivot+1 表示旋转数组中最小元素的下标。

对旋转数组 rotateArray[] = {4,5,6,7,1,2,3}   ,pivot = 3rotateArray[3] = 7 即旋转数组中的最大元素,rotateArray[4] = 1 为最小元素,如图 1-1 所示。

图 1-1

我们的思路也就有了,利用二分查找找到旋转数组 rotateArray 中的 pivot ,然后将旋转数组分成两个有序的子数组  [0, pivot] 和  [pivot+1, n-1] ,  最后在两个有序的子数组中分别使用二分查找法查找输入的关键字 key 即可。

以有序旋转数组  rotateArray[] = {3,4,5,6,7,1,2} 为例,说明二分查找 pivot 的过程。

设置一个 low 指针指向旋转数组的第 0 个元素 3high 指针指向旋转数组的第 6 个元素 2 ,计算 low  和 high 的中点 mid = (low + high) / 2 = 3 如图 1-2 所示:

图 1-2

其中 pivotmid 之间会存在两种情况:

  1. 判断条件 mid < high && arr[mid] > arr[mid+1] 是否满足,该条件的意义是找到的 pivot 即为 mid 。此时 mid = 3high = 6arr[mid] = 6arr[mid+1]=7 ,显然并不满足。

  1. 判断条件 mid > low && arr[mid-1] > arr[mid] 是否满足,该条件的意义时找到的 pivot 为 mid - 1

二分法的核心在于,根据 mid 将搜索空间减半:

判断条件 arr[low] >= arr[mid] ,如果满足,则表明旋转数组在区间 [low, mid] 内非有序数组,要查找的 pivot 位于 [low,mid-1] 之间,递归进行处理即可。反之 arr[low] < arr[mid] ,则表示旋转数组的子区间 [low, mid] 为有序数组,要查找的元素 pivot 位于 [mid+1, high] 非升序排列的区间,递归的进行处理。

如图 1-2 所示,low = 0mid=3arr[low]=arr[0] < arr[mid]=arr[3] ,则递归处理 [mid+1, high] ,即 [4, 6]

此时 low = 4high = 6 ,计算 mid = (4 + 6) / 2 = 5 ,满足条件mid > low && arr[mid-1] > arr[mid],即 5 > 4 && arr[4] > arr[5] ,则 pivot 的下标为 mid-1 ,如图 1-3 所示。

图 1-3

理解上面思路,也不难写出查找 pivot 的代码:

// 查找旋转数组arr中的pivot
int findPivot(int arr[], int low, int high){
    if (high < low)
        return -1;
    if (low == high)
        return low;
    
    int mid = (low + high) / 2;
    if(mid < high && arr[mid] > arr[mid+1]) // pivot == mid
        return mid;
    if(mid > low && arr[mid-1] > arr[mid]) // pivot == mid-1
        return mid-1;
    if(arr[low] >= arr[mid]) // pivot in [low, mid-1]
        return findPivot(arr, low, mid-1);
    return findPivot(arr, mid+1, high); // pivot in [mid+1, high]
}

然后根据 pivot 将数组分割成两个子数组 [0,pivot-1][pivot+1,n-1] 。首先判断 arr[pivot] == key 是否满足,满足也就没有必要在两个子数组中使用二分查找 key 。然后比较 arr[0] <= key ,若满足,则表示要查找的关键字 key 位于子数组 [0,pivot-1] 中,否则位于 [pivot+1,n-1] 中,使用二分查找即可。

例如旋转数组 rotateArray[] = {3,4,5,6,7,1,2} 的 pivot 为 4,要查找的元素为 key = 57 = arr[4] != key ;比较 arr[0] = 3 < key ,则使用二分查找在区间 [0,3] 内查找关键字 key。

在上面完整思路的基础上,也不难写出如下的代码:

#include <stdio.h> 
  
int findPivot(int[], int, int); 
int binarySearch(int[], int, int, int); 
  
// 查找旋转数组 arr 中的关键字 key
int pivotedBinarySearch(int arr[], int n, int key) 
{ 
    int pivot = findPivot(arr, 0, n - 1); 
  
    // 如果pivot不存在,则说明数组不是旋转数组,使用二分查找即可
    if (pivot == -1) 
        return binarySearch(arr, 0, n - 1, key); 
  
    // pivot 存在,先比较arr[pivot]
    if (arr[pivot] == key) 
        return pivot; 
    if (arr[0] <= key) // 确定 key 位于那个区间内
        return binarySearch(arr, 0, pivot - 1, key); 
    return binarySearch(arr, pivot + 1, n - 1, key); 
} 
  
// 查找旋转数组中的pivot,如[3,4,5,6,7,1,2]中的pivot为4,即7的下标
int findPivot(int arr[], int low, int high) 
{ 
    if (high < low) 
        return -1; 
    if (high == low) 
        return low; 
  
    int mid = (low + high) / 2; /*low + (high - low)/2;*/
    if (mid < high && arr[mid] > arr[mid + 1]) 
        return mid; 
    if (mid > low && arr[mid] < arr[mid - 1]) 
        return (mid - 1); 
    if (arr[low] >= arr[mid]) 
        return findPivot(arr, low, mid - 1); 
    return findPivot(arr, mid + 1, high); 
} 
  
// 标准的二分查找
int binarySearch(int arr[], int low, int high, int key) 
{ 
    if (high < low) 
        return -1; 
    int mid = (low + high) / 2; 
    if (key == arr[mid]) 
        return mid; 
    if (key > arr[mid]) 
        return binarySearch(arr, mid + 1, high, key); 
    return binarySearch(arr, low, mid - 1, key); 
} 

int main() 
{ 
    int rotateArr[] = { 3, 4, 5, 6, 7, 1, 2 }; 
    int n = sizeof(rotateArr) / sizeof(rotateArr[0]); 
    int key = 3; 
    printf("key 的下标为 : %d", pivotedBinarySearch(rotateArr, n, key)); 
    return 0; 
} 

复杂度分析

  • 时间复杂度:

  • 空间复杂度:

但上面的代码是否就完美了呢?面试官会告诉我们其实不然。他会提示我们再仔细考虑一下,是否可以将多次调用二分查找的方式优化一下,仅需要一次就能在旋转数组中查找到关键字 key。

以旋转数组 arr[] = {3,4,5,6,7,1,2}、key = 5 为例。

设置一个指向旋转数组第 0 个元素的指针 low ,指向旋转数组第 6 个元素的指针 high ,计算两者的中点 mid = (low + high) / 2 = 3

比较 arr[low]arr[mid] ,从而判断当前数组以 mid 为分割点得到的子数组 [low, mid] 是否有序,当 **arr[low] <= arr[mid] ** 时,表明子数组 arr[low,...,mid] 为有序数组;否则,子数组 arr[mid+1,...,high] 一定为有序子数组。

arr[low] = 3 < arr[mid] = 6 ,则表明子数组 arr[0,...,3] 为有序数组;

子数组 arr[low,...,mid] 为有序数组,判断关键字 key 是否在子数组的范围内,即判断 arr[low] <= key <= arr[mid] 是否满足。若满足则递归地处理子数组 [low,mid-1] ,否则,递归处理子数组 [mid+1, high]

子数组 arr[0,...,3] 为有序数组,且 3 < 5 < 6 ,也就是 key 值可能位于子数组 [0,3] 之内,递归处理子数组 arr[0,...,2] :

新的 low = 0、high = 2 ,则 mid = (low+high)/2 = 1arr[0] <= arr[1] ,子数组 arr[0,1] 为有序子数组,但是 arr[0]arr[1] 均小于要查找的 key = 5 ,所以递归处理子数组 [2,2] :

此时的 low = 2、high = 2mid = 2arr[mid] = 5 ,直接返回 key 值在数组中的下标 2 即可。

查找旋转数组 arr[] = {3,4,5,6,7,1,2}  中的关键字 key = 1 的过程如图 1-5 所示。

图 1-5 查找旋转数组中的关键字 1

实现代码
#include <stdio.h>

int keyBinarySearch(int arr[], int low, int high, int key) {
    if (low > high)
        return -1;
    
    int mid = (low + high) / 2;
    if (arr[mid] == key){
        return mid;
    }
    
    if (arr[low] <= arr[mid]) {
        if (arr[low] <= key && key <= arr[mid]) {
            return keyBinarySearch(arr, low, mid-1, key);
        }
        return keyBinarySearch(arr, mid+1, high, key);
    }
    
    if (arr[mid] <= key && key <= arr[high]) {
        return keyBinarySearch(arr, mid+1, high, key);
    }
    return keyBinarySearch(arr, low, mid - 1, key);
}

int main() 
{
    int arr[] = { 3,4,5,6,7,1,2}; 
    int n = sizeof(arr) / sizeof(arr[0]); 
    int key = 1; 
    int i = keyBinarySearch(arr, 0, n - 1, key); 
    if (i != -1) {
        printf("%d\n", i);
    }
    else{
        printf("key 不存在");
    }
    return 0;     
}

复杂度分析

  • 时间复杂度:

  • 空间复杂度:

那么上面这段代码的是否就完美了呢?如果数组中有重复元素的话又该如何处理,该算法能否处理呢?

比如,对于数组 arr[ ] = [1,1,1,1,1,0,1,1]  or [1,1,0,1,1,1,1,1,1],就无法用上面的两种思路在 的时间查找到元素 0 。如图 3-1 所示,arr[low] = arr[mid] = arr[high] = 1 ,我们就无法确定要查找的关键字是在 [low, mid-1] 左半部分还是在 [mid+1, high] 右半部分。解决办法就是退而求其次,使用 的时间进行查找。

图 3-1 有序旋转数组存在重复元素

所以,果断回答,无法用 时间查找有重复元素的有序旋转数组中的元素。正如题目要求的提示信息一样,旋转数组arr[] 中的每个值都是独一无二的 ,并且 arr[] 肯定会在某个点上旋转

总结

二分查找是一个超越了 查找二分 思想。2 这个数字在计算中意义远远超过其本身的意义。我们甚至可以用下面一种抽象艺术来形象的表示二分思想:

程序员专栏 扫码关注填加客服 长按识别下方二维码进群

近期精彩内容推荐:  

 几句话,离职了

 中国男性的私密数据大赏,女生勿入!

 为什么很多人用“ji32k7au4a83”作密码?

 一个月薪 12000 的北京程序员的真实生活 !


在看点这里好文分享给更多人↓↓

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值