数组问题之《下一个排列》《旋转图像》以及二分查找之《搜索二维矩阵》

Posted 爱上口袋的天空

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了数组问题之《下一个排列》《旋转图像》以及二分查找之《搜索二维矩阵》相关的知识,希望对你有一定的参考价值。

一、下一个排列

1、题目说明

        实现获取下一个排列的函数,算法需要将给定数字序列重新排列成字典序中下一个更大的排列。

        如果不存在下一个更大的排列,则将数字重新排列成最小的排列(即升序排列)。

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

以下是一些例子,输入位于左侧列,其相应输出位于右侧列。

1,2,3 → 1,3,2
3,2,1 → 1,2,3
1,1,5 → 1,5,1

2、方法一:暴力法

        最简单的想法就是暴力枚举,我们找出由给定数组的元素形成的列表的每个可能的排列,并找出比给定的排列更大的排列。

        但是这个方法要求我们找出所有可能的排列,这需要很长时间,实施起来也很复杂。因此,这种算法不能满足要求。 我们跳过它的实现,直接采用正确的方法

        复杂度分析:

                时间复杂度:O(n!),可能的排列总计有 n! 个。

                空间复杂度:O(n),因为数组将用于存储排列。


3、方法二:一遍扫描

首先,我们观察到对于任何给定序列的降序排列,就不会有下一个更大的排列。

例如,以下数组不可能有下一个排列:

[9, 5, 4, 3, 1]

这时应该直接返回升序排列。

        所以对于一般的情况,如果有一个“升序子序列”,那么就一定可以找到它的下一个排列。具体来说,需要从右边找到第一对两个连续的数字 a[i] 和 a[i-1],它们满足 a[i]>a[i-1]。

        所以一个思路是,找到最后一个的“正序”排列的子序列,把它改成下一个排列就行了。

 不过具体操作会发现,如果正序子序列后没数了,那么子序列的“下一个”一定就是整个序列的“下一个”,这样做没问题;但如果后面还有逆序排列的数,这样就不对了。比如:

[1,3,8,7,6,2]

        最后的正序子序列是[1,3,8],但显然不能直接换成[1,8,3]就完事了;而是应该考虑把3换成后面比3大、但比8小的数,而且要选最小的那个(6)。接下来,还要让6之后的所有数,做一个升序排列,得到结果:

[1,6,2,3,7,8]

代码实现:

public void nextPermutation(int[] nums) 
        int k = nums.length - 2;
        while( k >= 0 && nums[k] >= nums[k+1] )
            k--;
        // 如果全部降序,以最小序列输出
        if( k < 0 )
            Arrays.sort(nums);
            return;
        

        int i = k + 2;
        while( i < nums.length && nums[i] > nums[k] )
            i++;

        // 交换nums[k]和找到的nums[i-1]
        int temp = nums[k];
        nums[k] = nums[i-1];
        nums[i-1] = temp;

        // k以后剩余的部分反转成升序
        int start = k + 1, end = nums.length - 1;
        while( start < end )
            int reTemp = nums[start];
            nums[start] = nums[end];
            nums[end] = reTemp;
            start++;
            end--;
        
    

复杂度分析:

        时间复杂度:

                O(N),其中 NN 为给定序列的长度。我们至多只需要扫描两次序列,以及进行一次反转操作。

        空间复杂度:O(1),只需要常数的空间存放若干变量。


二、旋转图像

1、题目说明

        给定一个 n × n 的二维矩阵表示一个图像。

        将图像顺时针旋转 90 度。

 说明

        你必须在原地旋转图像,这意味着你需要直接修改输入的二维矩阵。请不要使用另一个矩阵来旋转图像。

示例:

        

给定 matrix = 
[
  [1,2,3],
  [4,5,6],
  [7,8,9]
],
原地旋转输入矩阵,使其变为:
[
  [7,4,1],
  [8,5,2],
  [9,6,3]
]
给定 matrix =
[
  [ 5, 1, 9,11],
  [ 2, 4, 8,10],
  [13, 3, 6, 7],
  [15,14,12,16]
], 
原地旋转输入矩阵,使其变为:
[
  [15,13, 2, 5],
  [14, 3, 4, 1],
  [12, 6, 8, 9],
  [16, 7,10,11]
]

2、分析

        旋转图像,这个应用在图片处理的过程中,非常常见。我们知道对于计算机而言,图像,其实就是一组像素点的集合(所谓点阵),所以图像旋转的问题,本质上就是一个二维数组的旋转问题。


3、方法一:数学方法(转置再翻转)

我们可以利用矩阵的特性。所谓顺时针旋转,其实就是先转置矩阵,然后翻转每一行。

代码如下:

public void rotate(int[][] matrix) 
        int n = matrix.length;//获取二维数组的长度

        // 转置矩阵
        /***
         * 例如:二维数组是
         * [
         *   [1,2,3],
         *   [4,5,6],
         *   [7,8,9]
         * ],
         * 实现如下效果:
         *[
         *   [1,4,7],
         *   [2,5,8],
         *   [3,6,9]
         * ],
         */
        for (int i = 0; i < n; i++)
            for (int j = i; j < n; j++) 
                int tmp = matrix[i][j];
                matrix[i][j] = matrix[j][i];
                matrix[j][i] = tmp;
            
        // 翻转行
        /***
         * 将上面得到的
         * [
         *   [1,4,7],
         *   [2,5,8],
         *   [3,6,9]
         * ],
         * 转换为:
         * [
         *   [7,4,1],
         *   [8,5,2],
         *   [9,6,3]
         * ]
         */
        for( int i = 0; i < n; i++ )
            for( int j = 0; j < n/2; j++ )
                int tmp = matrix[i][j];
                matrix[i][j] = matrix[i][n-j-1];
                matrix[i][n-j-1] = tmp;
            
        
    

复杂度分析:

  • 时间复杂度:O(N^2)

这个简单的方法已经能达到最优的时间复杂度O(N^2) ,因为既然是旋转,那么每个点都应该遍历到,N^2的复杂度不可避免。

  • 空间复杂度:O(1)。旋转操作是原地完成的,只耗费常数空间。

4、方法二:分治(分为四部分旋转)

        方法 1 使用了两次矩阵操作,能不能只使用一次操作的方法完成旋转呢?

        为了实现这一点,我们来研究每个元素在旋转的过程中如何移动。

        第一次旋转:1,3,9,7

        第二次旋转:2,6,8,4

这提供给我们了一个思路,可以将给定的矩阵分成四个矩形并且将原问题划归为旋转这些矩形的问题。这其实就是分治的思想。

具体解法也很直接,可以在每一个矩形中遍历元素,并且在长度为 4 的临时列表中移动它们。

 代码如下:

 public void rotate(int[][] matrix) 
        int n = matrix.length;//获取二维数组长度

        for (int i = 0; i < n / 2 + n % 2; i++) 
            for (int j = 0; j < n / 2; j++) 
                int[] tmp = new int[4];
                int row = i;
                int col = j;
                for (int k = 0; k < 4; k++) 
                    tmp[k] = matrix[row][col];
                    // 定位下一个数
                    int x = row;
                    row = col;
                    col = n - 1 - x;
                

                for (int k = 0; k < 4; k++) 
                    matrix[row][col] = tmp[(k + 3) % 4];
                    int x = row;
                    row = col;
                    col = n - 1 - x;
                
            
        
    

复杂度分析:

  • 时间复杂度:O(N^2) 是两重循环的复杂度。
  • 空间复杂度:O(1) 由于我们在一次循环中的操作是“就地”完成的,并且我们只用了长度为 4 的临时列表做辅助。

5、方法三:分治法改进(单次循环内完成旋转)

        大家可能也发现了,我们其实没有必要分成4个矩阵来旋转。这四个矩阵的对应关系,其实是一目了然的,我们完全可以在一次循环内,把所有元素都旋转到位。

        因为旋转的时候,是上下、左右分别对称的,所以我们遍历元素的时候,只要遍历一半行、一半列就可以了(1/4元素)。

代码如下:

public void rotate(int[][] matrix) 
        int n = matrix.length;

        // 不区分子矩阵,直接遍历每一个元素
        for( int i = 0; i < (n + 1)/2; i++ )
            for( int j = 0; j < n/2; j++ )
                int temp = matrix[i][j];
                matrix[i][j] = matrix[n-j-1][i];
                matrix[n-j-1][i] = matrix[n-i-1][n-j-1]
                matrix[n-i-1][n-j-1] = matrix[j][n-i-1];;
                matrix[j][n-i-1] = temp;
            
        
    

复杂度分析:

  • 时间复杂度:O(N^2),是两重循环的复杂度。
  • 空间复杂度:O(1)。我们在一次循环中的操作是“就地”完成的。

三、二分查找

 1、简介

        二分查找也称折半查找(Binary Search),它是一种效率较高的查找方法,前提是数据结构必须先排好序,可以在对数时间复杂度内完成查找。

        二分查找事实上采用的就是一种分治策略,它充分利用了元素间的次序关系,可在最坏的情况下用O(log n)完成搜索任务。

        它的基本思想是:假设数组元素呈升序排列,将n个元素分成个数大致相同的两半,取a[n/2]与欲查找的x作比较,如果x=a[n/2]则找到x,算法终止;如 果x<a[n/2],则我们只要在数组a的左半部继续搜索x;如果x>a[n/2],则我们只要在数组a的右 半部继续搜索x。

二分查找问题也是面试中经常考到的问题,虽然它的思想很简单,但写好二分查找算法并不是一件容易的事情。


2、实现一个对int数组的二分查找(注意:传递进来的数组必须有序)。

 public static int binarySearch(int[] a, int key)
        int low = 0;
        int high = a.length - 1;
        if ( key < a[low] || key > a[high] )
            return -1;

        while ( low <= high)
            int mid = ( low + high ) / 2;
            if( a[mid] < key)
                low = mid + 1;
            else if( a[mid] > key )
                high = mid - 1;
            else
                return mid;    // 查找成功
        
        // 未能找到
        return -1;
    

3、用递归的方式实现

public static int binarySearch(int[] a, int key, int fromIndex, int toIndex)
        if ( key < a[fromIndex] || key > a[toIndex] || fromIndex > toIndex)
            return -1;

        int mid = ( fromIndex + toIndex ) / 2;

        if ( a[mid] < key )
            return binarySearch(a, key, mid + 1, toIndex);
        else if ( a[mid] > key )
            return binarySearch(a, key, fromIndex, mid - 1);
        else
            return mid;
    

我们总结一下二分查找:

  • 优点是比较次数少,查找速度快,平均性能好;
  • 缺点是要求待查表为有序表,且插入删除困难。

因此,二分查找方法适用于不经常变动而查找频繁的有序列表。使用条件:查找序列是顺序结构,有序。


四、二分查找案例之搜索二维矩阵

1、编写一个高效的算法来判断 m x n 矩阵中,是否存在一个目标值。该矩阵具有如下特性:

  • 每行中的整数从左到右按升序排列
  • 每行的第一个整数大于前一行的最后一个整数

示例 1

 

输入:matrix = [[1,3,5,7],[10,11,16,20],[23,30,34,50]], target = 3
输出:true
示例 2:
输入:matrix = [[1,3,5,7],[10,11,16,20],[23,30,34,50]], target = 13
输出:false
示例 3:
输入:matrix = [], target = 0
输出:false

提示:

  • m == matrix.length
  • n == matrix[i].length
  • 0 <= m, n <= 100
  • -104 <= matrix[i][j], target <= 104

2、需求分析

        既然这是一个查找元素的问题,并且数组已经排好序,我们自然可以想到用二分查找是一个高效的查找方式。

输入的 m x n 矩阵可以视为长度为 m x n的有序数组:

行列坐标为(row, col)的元素,展开之后索引下标为idx = row * n + col反过来,对于一维下标为idx的元素,对应二维数组中的坐标就应该是:

row = idx / n;  col = idx % n;


3、代码实现

 public boolean searchMatrix(int[][] matrix, int target)
        int m = matrix.length;
        if (m == 0) return false;
        int n = matrix[0].length;
        int left = 0;
        int right = m * n - 1;

        // 二分查找,定义左右指针
        while ( left <= right )
            int midIdx = (left + right) / 2;
            int midElement = matrix[midIdx/n][midIdx%n];
            if ( midElement < target )
                left = midIdx + 1;
            else if ( midElement > target )
                right = midIdx - 1;
            else
                return true;    // 找到target
        

        return false;
    

复杂度分析:​​​​​​​

  • 时间复杂度 : 由于是标准的二分查找,时间复杂度为O(log(m n))
  • 空间复杂度没有用到额外的空间,复杂度为O(1)

以上是关于数组问题之《下一个排列》《旋转图像》以及二分查找之《搜索二维矩阵》的主要内容,如果未能解决你的问题,请参考以下文章

二分查找之算法

漫画:“旋转数组”中的二分查找

漫画算法:“旋转数组”中的二分查找

php查找之二分查找

Leetcode题解——算法思想之二分查找

剑指 Offer 11. 旋转数组的最小数字-二分查找