力扣数组练习题(下一个排列、搜索旋转排序数组)

83 阅读5分钟

下一个排列

来源:力扣(LeetCode) 链接:leetcode.cn/problems/ne…

整数数组的一个 排列 就是将其所有成员以序列或线性顺序排列。

例如,arr = [1,2,3] ,以下这些都可以视作 arr 的排列:[1,2,3][1,3,2][3,1,2][2,3,1]

整数数组的 下一个排列 是指其整数的下一个字典序更大的排列。更正式地,如果数组的所有排列根据其字典顺序从小到大排列在一个容器中,那么数组的 下一个排列 就是在这个有序容器中排在它后面的那个排列。如果不存在下一个更大的排列,那么这个数组必须重排为字典序最小的排列(即,其元素按升序排列)。

例如,arr = [1,2,3] 的下一个排列是 [1,3,2] 。
类似地,arr = [2,3,1] 的下一个排列是 [3,1,2] 。
而 arr = [3,2,1] 的下一个排列是 [1,2,3] ,因为 [3,2,1] 不存在一个字典序更大的排列。

给你一个整数数组 nums ,找出 nums 的下一个排列。

必须 原地 修改,只允许使用额外常数空间。

示例 1:

输入:nums = [1,2,3]

输出:[1,3,2]

示例 2:

输入:nums = [3,2,1]

输出:[1,2,3]

示例 3:

输入:nums = [1,1,5]

输出:[1,5,1]

提示:

  • 1 <= nums.length <= 100
  • 0 <= nums[i] <= 100

代码

class Solution {
    public void nextPermutation(int[] nums) {
        int i = nums.length - 2;
        while (i >= 0 && nums[i] >= nums[i+1]) {
            i--;
        }
        if (i >= 0) {
            int j = nums.length - 1;
            while (j >= 0 && nums[j] <= nums[i]) {
                j--;
            }
            swap(nums, i, j);
        }
        reverse(nums, i+1);
    }
    
    private void swap(int[] nums, int i, int j) {
        int temp = nums[i];
        nums[i] = nums[j];
        nums[j] = temp;
    }
    
    private void reverse(int[] nums, int start) {
        int i = start, j = nums.length - 1;
        while (i < j) {
            swap(nums, i, j);
            i++;
            j--;
        }
    }
}

思路分析

这个算法的时间复杂度为O(n),其中n是数组的长度。

这个算法的思路比较巧妙。我们需要将给定的排列变为字典序中的下一个排列,如果这是最后一个排列,则将其变为第一个排列。我们可以将这个问题看作是将一个数列中的后缀变为字典序中的最小排列。我们需要找到一个尽可能小的前缀,它不是一个非递减序列。然后,我们可以在这个前缀中找到一个比它最后一个元素要大的元素,并将它与最后一个元素交换。最后,我们将前缀的后缀部分反转,得到字典序中的下一个排列。

例如,给定排列[1, 3, 2],我们需要找到一个前缀,它不是一个非递减序列。在这种情况下,前缀为[1],最后一个元素为3。我们需要在前缀中找到一个比3大的最小元素,这是2。我们交换3和2,得到[2, 3, 1]。最后,我们将前缀的后缀部分反转,得到[2, 1, 3],这是字典序中的下一个排列。

这个算法的优点是它非常巧妙,时间复杂度为O(n),空间复杂度为O(1)。缺点是它不是很直观,需要进行一些推理。

搜索旋转排序数组

来源:力扣(LeetCode) 链接:leetcode.cn/problems/se…

整数数组 nums 按升序排列,数组中的值 互不相同 。

在传递给函数之前,nums 在预先未知的某个下标 k(0 <= k < nums.length)上进行了 旋转,使数组变为 [nums[k], nums[k+1], ..., nums[n-1], nums[0], nums[1], ..., nums[k-1]](下标 从 0 开始 计数)。例如, [0,1,2,4,5,6,7] 在下标 3 处经旋转后可能变为 [4,5,6,7,0,1,2] 。

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

你必须设计一个时间复杂度为 O(log n) 的算法解决此问题。

示例 1:

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

示例 2:

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

示例 3:

输入:nums = [1], target = 0 输出:-1

提示:

  • 1 <= nums.length <= 5000
  • -104 <= nums[i] <= 104
  • nums 中的每个值都 独一无二
  • 题目数据保证 nums 在预先未知的某个下标上进行了旋转
  • -104 <= target <= 104

代码

class Solution {
    public int search(int[] nums, int target) {
        int n = nums.length;
        int left = 0, right = n - 1;
        while (left <= right) {
            int mid = left + (right - left) / 2;
            if (nums[mid] == target) {
                return mid;
            }
            if (nums[left] <= nums[mid]) {
                if (nums[left] <= target && target < nums[mid]) {
                    right = mid - 1;
                } else {
                    left = mid + 1;
                }
            } else {
                if (nums[mid] < target && target <= nums[right]) {
                    left = mid + 1;
                } else {
                    right = mid - 1;
                }
            }
        }
        return -1;
    }
}

思路分析

这个算法的时间复杂度为O(log n),其中n是数组的长度。它使用二分查找算法来解决问题。

这个算法的思路是非常巧妙的。我们可以将问题看作是在一个旋转排序数组中查找目标元素。我们使用二分查找算法来查找目标元素,但是由于数组是旋转排序的,我们不能直接使用二分查找算法。相反,我们需要根据数组中间元素的值以及左右边界元素的值来确定我们应该搜索哪一部分数组。

具体地,我们先找到中间元素。如果中间元素等于目标元素,我们就返回中间元素的下标。否则,我们需要根据数组的特性来决定搜索哪一部分数组。如果左边的元素小于等于中间元素,说明左半部分数组是排好序的,我们可以根据目标元素与左边元素和中间元素的大小关系来判断我们应该搜索哪一部分数组。如果目标元素在左半部分数组中,我们就继续搜索左半部分数组,否则就搜索右半部分数组。如果左边的元素大于中间元素,说明右半部分数组是排好序的,我们同样可以根据目标元素与中间元素和右边元素的大小关系来判断我们应该搜索哪一部分数组。

这个算法的优点是它非常巧妙,时间复杂度为O(log n),空间复杂度为O(1)。缺点是它的实现有一些细节需要处理,需要认真思考。