旋转数组(二分查找)

1.旋转数组(189 - 中)

题目描述:给定一个数组,将数组中的元素向右移动 k 个位置,其中 k 是非负数。进阶:

  • 尽可能想出更多的解决方案,至少有三种不同的方法可以解决这个问题。
  • 你可以使用空间复杂度为 O(1) 的 原地 算法解决这个问题吗?

示例 :

输入: nums = [1,2,3,4,5,6,7], k = 3
输出: [5,6,7,1,2,3,4]
解释:
向右旋转 1 步: [7,1,2,3,4,5,6]
向右旋转 2 步: [6,7,1,2,3,4,5]
向右旋转 3 步: [5,6,7,1,2,3,4]

思路:多解法,其中使用额外数组,空间复杂度O(N),其余O(1)。

  • 使用额外数组:遍历原数组,将原数组下标为 ii 的元素放至新数组下标为 (i+k)%n 的位置,最后将新数组拷贝至原数组即可。
  • 模拟循环:用一个变量tmp维护移动k次,注意移动数组时倒序进行填充,防止覆盖,时间复杂度O(kn),但是超时。
  • 数组翻转:比较巧妙,先整体翻转,在将0 ~ k - 1和k - 1 ~ n - 1进行翻转。

代码实现:

class Solution {
    // 使用辅助数组
    public void rotate(int[] nums, int k) {
        int n = nums.length;
        int[] tmp = new int[n];
        
        for (int i = 0; i < n; i++) {
            tmp[(i + k) % n] = nums[i];
        }
        System.arraycopy(tmp, 0, nums, 0, n);
    }

    // 模拟移动(超时!)
    public void rotate(int[] nums, int k) {
        int n = nums.length;
        k %= n;

        for (int i = 0; i < k; i++) {
            int temp = nums[n - 1];
            for (int j = n - 1; j > 0; j--) {
                nums[j] = nums[j - 1];
            }
            nums[0] = temp;
        }
    }

    // 数组翻转(先整体翻转,再翻转部分)
    public void rotate(int[] nums, int k) {
        int n = nums.length;
        k %= n;
        reverse(nums, 0, n - 1);
        reverse(nums, 0, k - 1);
        reverse(nums, k, n - 1);
    }

    private void reverse(int[] nums, int i, int j) {
        while (i < j) {
            int tmp = nums[i];
            nums[i] = nums[j];
            nums[j] = tmp;
            i++;
            j--;
        }
    }
}

2.寻找旋转排序数组中的最小值(153 - 中)

题目描述:整数数组 nums 按升序排列,数组中的值 互不相同 。请你找出并返回数组中的 最小元素

示例 :

输入:nums = [3,4,5,1,2]
输出:1
解释:原数组为 [1,2,3,4,5] ,旋转 3 次得到输入数组。

思路:本题可以直接遍历获取最小值,但是考察点二分查找。注意体会二分查找的思想,不要背模板,这里可以画个折线看看。

注:代码中比较元素取数组最后一个元素,也可以取任意一个元素。

代码实现:

public int findMin(int[] nums) {
    int l = 0, r = nums.length - 1;
    while (l < r) {
        int mid = l + ((r - l) >> 1);
        if (nums[mid] > nums[r]) {
            l = mid + 1;
        } else {
            r = mid;
        }
    }
    return nums[l];
}

3.寻找旋转排序数组中的最小值II(154 - 难)

题目描述:整数数组 nums 按升序排列,数组中的值 可能存在重复值 。返回数组中的 最小元素 。同 剑指 Offer 11. 旋转数组的最小数字

示例 :

输入:nums = [2,2,2,0,1]
输出:0

思路:本题可以直接遍历获取最小值,但是考察点二分查找,与上题不同的是数组中可能含有重复值,但是二分的本质是二段性,并非单调性,所以我们还以使用二分。但时间复杂度可能退化O(n),即全部是一种元素。

注意:由于重复元素的存在,我们并不能确定 nums[mid]究竟在最小值的左侧还是右侧,因此我们不能莽撞地忽略某一部分的元素。我们唯一可以知道的是,由于它们的值相同,所以无论 nums[r] 是不是最小值,都有一个它的「替代品」nums[mid],因此我们可以忽略二分查找区间的右端点。

代码实现:

public int findMin(int[] nums) {
    int l = 0, r = nums.length - 1;
    while (l < r) {
        int mid = l + ((r - l) >> 1);
        if (nums[mid] > nums[r]) {
            l = mid + 1;
        } else if (nums[mid] < nums[r]){
            r = mid;
        } else {
            r--;
        }
    }
    return nums[l];
}

4.搜索旋转排序数组(33 - 中)

题目描述:整数数组 nums 按升序排列,数组中的值 互不相同

给你 旋转后 的数组 nums 和一个整数 target ,如果 nums 中存在这个目标值 target ,则返回它的下标,否则返回 -1 。

示例 :

输入:nums = [4,5,6,7,0,1,2], target = 0
输出:4

思路:本题考查二分查找,但是二分查找要求数组有序,关键:

  • 先通过mid和nums[r](任意元素)比较确定有序区间在mid左边还是右边, 如T153
  • 再确定目标元素target在哪边!,这时我们就可以直接根据target位置进行二分查找了。

代码实现:

class Solution {
    // 直接搜索
    public int search(int[] nums, int target) {
        for (int i = 0; i < nums.length; i++) {
            if (nums[i] == target) {
                return i;
            }
        }
        return -1;
    }

    // 二分查找
    public int search(int[] nums, int target) {
        int n = nums.length;
        int l = 0, r = n - 1;
        while (l <= r) {
            int mid = l + ((r - l) >> 1);
            if (nums[mid] == target) return mid;
            if (nums[mid] > nums[r]) {
                if (target >= nums[l] && target < nums[mid]) {
                    r = mid - 1;
                } else {
                    l = mid + 1;
                }
            } else {
                if (target > nums[mid] && target <= nums[r]) {
                    l = mid + 1;
                } else {
                    r = mid - 1;
                }
            }
        }
        return -1;
    }
}

5.搜索旋转排序数组II(81 - 中)

题目描述:整数数组 nums 按升序排列(单调不减),数组中的值 可能存在重复值

给你 旋转后 的数组 nums 和一个整数 target ,请你编写一个函数来判断给定的目标值是否存在于数组中。如果 nums 中存在这个目标值 target ,则返回 true ,否则返回 false 。

示例 :

输入:nums = [2,5,6,0,0,1,2], target = 0
输出:true

思路:经过上述各题的过度,直接使用二分解法。与33题基本相同。不同的是含有重复元素。

时间复杂度:对于这种含重复元素的二分查找问题,最坏时间复杂度O(n),即整个数组都是同一个数,最好时间复杂度O(nlogn)。

代码实现:

public boolean search(int[] nums, int target) {
    int n = nums.length;
    int l = 0, r = n - 1;
    while (l <= r) {
        int mid = l + ((r - l) >> 1);
        if (nums[mid] == target) return true;
        if (nums[mid] > nums[r]) {
            if (target >= nums[l] && target < nums[mid]) {
                r = mid - 1;
            } else {
                l = mid + 1;
            }
        } else if (nums[mid] < nums[r]) {
            if (target > nums[mid] && target <= nums[r]) {
                l = mid + 1;
            } else {
                r = mid - 1;
            }
        } else {
            r--;
        }
    }
    return false;
}

6.在排序数组中查找目标值的首尾元素的索引(34 - 中)

题目描述:给定一个按照升序排列的整数数组 nums,可能含有重复值,和一个目标值 target。找出给定目标值在数组中的开始位置和结束位置。如果数组中不存在目标值 target,返回 [-1, -1]。

要求:你可以设计并实现时间复杂度为 O(log n) 的算法解决此问题吗?

示例 :

输入:nums = [5,7,7,8,8,10], target = 8
输出:[3,4]

思路:代码1在极端情况下,时间复杂度退化为O(n),不符合要求,但容易理解,实现简单。

其实,我们只需要找两个索引即可,大于target - 1的索引(即目标值的起始索引)和第一个大于target的数的索引 - 1,时间复杂度O(nlogn),只调用了两次二分查找。

代码实现:

// 代码1
public int[] searchRange(int[] nums, int target) {
    int n = nums.length;
    int l = 0, r = n - 1;
    while (l <= r) {
        int mid = l + ((r - l) >> 1);
        if (nums[mid] == target) {
            int start = mid, end = mid;
            while (start >= 0 && nums[start] == target) start--;
            while (end < n && nums[end] == target) end++;
            return new int[] {start + 1, end - 1}; 
        } else if (nums[mid] > target) {
            r = mid - 1;
        } else {
            l = mid + 1;
        }
    }
    return new int[] {-1, -1};
}

// 代码2
public int[] searchRange(int[] nums, int target) {
    int n = nums.length;
    int l = 0, r = n - 1;
    int leftIdx = binarySearch(nums, target - 1);
    int rightIdex = binarySearch(nums, target) - 1;
    if (leftIdx <= rightIdex && nums[leftIdx] == target && nums[rightIdex] == target) {
        return new int[] {leftIdx, rightIdex};
    }
    return new int[] {-1, -1};
}
// 返回大于target的第一个索引值
public int binarySearch(int[] nums, int target) {
    int n = nums.length;
    int l = 0, r = n - 1, ans = nums.length;
    while (l <= r) {
        int mid = l + ((r - l) >> 1);
        if (nums[mid] > target) {
            r = mid - 1;
            ans = mid;
        } else {
            l = mid + 1;
        }
    }
    return ans;
}

7.面试题:10.03(补充)

题目描述:返回目标元素的第一个索引,即索引值最小的那个(如果存在),不存在返回-1。

示例 :

 输入: arr = [15, 16, 19, 20, 25, 1, 3, 4, 5, 7, 10, 14], target = 5
 输出: 8(元素5在该数组中的索引)

思路:本题是T82和T34的思路的融合,但注意由于数组是经过旋转,所以不能像T34查找边界。类似这种情况旋转数组:[5,5,5,1,2,3,4,5] 目标值:5,返回的是7,却不是0。

  • 如果左边索引满足条件,直接返回
  • 二分,mid符合,但是我们找最小的,所以要更新右边界。
  • mid不符合,找升序区间继续二分

代码实现:

public int search(int[] nums, int target) {
    int n = nums.length;
    int l = 0, r = n - 1;
    while (l <= r) {
        if (nums[l] == target) return l;
        int mid = l + ((r - l) >> 1);
        // 索引mid的值是目标值(左边可能还有更小的索引值),更新右边界
        if (nums[mid] == target) r = mid;
        if (nums[mid] > nums[l]) {
            if (target >= nums[l] && target <= nums[mid]) {
                r = mid;
            } else {
                l = mid + 1;
            }
        } else if (nums[mid] < nums[l]) {
            if (target >= nums[mid] && target <= nums[r]) {
                l = mid;
            } else {
                r = mid - 1;
            }
        } else {
            l++;
        }
    }
    return -1;
}

8.山脉数组的峰顶索引(852 - 易)

题目描述:符合下列属性的数组 arr 称为 山脉数组 :

  • arr.length >= 3
  • 存在 i(0 < i < arr.length - 1)使得:
    arr[0] < arr[1] < ... arr[i-1] < arr[i]
    arr[i] > arr[i+1] > ... > arr[arr.length - 1]

给你由整数组成的山脉数组 arr ,返回任何满足 arr[0] < arr[1] < ... arr[i - 1] < arr[i] > arr[i + 1] > ... > arr[arr.length - 1] 的下标 i 。

示例 :

输入:arr = [0,2,1,0]
输出:1

思路:本题考察点二分查找,只不过二分的判断条件有点不同。即通过mid相邻点判断最大值所在区间。

代码实现:

public int peakIndexInMountainArray(int[] arr) {
    int n = arr.length;
    int l = 0, r = n - 1;
    while (l < r) {
        int mid = l + ((r - l) >> 1);
        if (arr[mid] < arr[mid + 1]) {
            l = mid + 1;
        } else {
            r = mid;
        }
    }
    return l;
}

9.山脉数组中查找目标值(1095 - 难)

题目描述:给你一个 山脉数组 mountainArr,请你返回能够使得 mountainArr.get(index) 等于 target 最小 的下标 index 值。

如果不存在这样的下标 index,就请返回 -1。

示例 :

输入:array = [1,2,3,4,5,3,1], target = 3
输出:2
解释:3 在数组中出现了两次,下标分别为 2 和 5,我们返回最小的下标 2。

思路:上一题的升级,我们寻找包含重复元素的最小索引值。类比面试题10.03。两阶段:

  • 找到最大值索引,T852
  • 分别在升序和降序区间找目标值。

时间复杂度O(nlogn),进行三次二分查找(也可能两次)。

代码实现:

public int findInMountainArray(int target, MountainArray mountainArr) {
    // 1.查找峰顶索引
    int n = mountainArr.length();
    int l = 0, r = n - 1;
    while (l < r) {
        int mid = l + ((r - l) >> 1);
        if (mountainArr.get(mid) < mountainArr.get(mid + 1)) {
            l = mid + 1;
        } else {
            r = mid;
        }
    }

    // 2.对升序和降序数组分别进行二分查找(用flag标识升序和降序区间)
    int idx = binarySearch(target, mountainArr, 0, l, true);
    return idx != -1 ? idx : binarySearch(target, mountainArr, l + 1, n - 1, false);
}

public int binarySearch(int target, MountainArray mountainArr, int l, int r, boolean flag) {
    while (l <= r) {
        int mid = l + ((r - l) >> 1);
        if (mountainArr.get(mid) == target) return mid;
        if (mountainArr.get(mid) < target) {
            l = flag ? mid + 1 : l;  
            r = flag ? r : mid - 1;
        } else {
            r = flag ? mid - 1 : r;
            l = flag ? l : mid + 1;
        }
    }
    return -1;
}

10.两数相除(29 - 中)

思路:由于题目限定了我们不能使用乘法、除法和 mod 运算符。

核心:我们可以先实现一个「倍增乘法」,然后利用对于 x 除以 y,结果 x / y 必然落在范围 [0, x] 的规律进行二分。

注意:long mid = l + r + 1 >> 1,之前的用法会超时?

public int divide(int a, int b) {
    long x = a, y = b;
    boolean isNeg = (x < 0 && y > 0) || (x > 0 && y < 0);
    if (x < 0) x = -x;
    if (y < 0) y = -y;
    long l = 0, r = x;
    while (l < r) {
        long mid = l + r + 1 >> 1;
        if (mul(mid, y) <= x) {
            l = mid;
        } else {
            r = mid - 1;
        }
    }
    long ans = isNeg ? -l : l;
    if (ans > Integer.MAX_VALUE || ans < Integer.MIN_VALUE) return Integer.MAX_VALUE;
    return (int)ans;
}
// 倍乘函数
public long mul(long a, long k) {
    long ans = 0;
    while (k > 0) {
        if ((k & 1) == 1) ans += a;
        k >>= 1;
        a += a;
    }
    return ans;
}

巨人的肩膀: @甜姨 @宫水三叶

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,098评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,213评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,960评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,519评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,512评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,533评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,914评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,574评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,804评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,563评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,644评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,350评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,933评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,908评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,146评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,847评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,361评论 2 342

推荐阅读更多精彩内容