剑指offer数组类题目汇总(共12道题目)

1.二维数组中的查找

题目描述:

在一个二维数组中(每个一维数组的长度相同),每一行都按照从左到右递增的顺序排序,每一列都按照从上到下递增的顺序排序。请完成一个函数,输入这样的一个二维数组和一个整数,判断数组中是否含有该整数。

解题思路:

很明显,由于该二维数组上到下递增,左到右递增的特殊性,遍历整个矩阵进行查找不是该题目的意图所在。总结规律我们可以发现:应该从矩阵的右上角或者左下角开始查找。

以右上角为例,首先选取右上角的数字,如果该数字等于要查找的数字,则查找过程结束;如果该数字大于要查找的数字,则说明该列其他元素都大于要查找的数字,便可以删掉该列;如果该数字小于要查找的数字,则说明该行其他元素也都小于要查找的数字,便可以删掉该行。

这样,每一次比较都可以剔除一行或者一列,进而缩小查找范围,时间复杂度为O(n)。

举例:

比如在下面的二维数组中查找数字7,查找过程如下:

图列详解
代码如下:

class Find{
    public boolean find(int target,int [][] arr){
        if(arr == null|| arr.length == 0)
            return false;  //数组为空或不是二维数组直接返回false
        int row = arr.length; //定义行
        int col = arr[0].length;//定义列
        int i = 0;
        int j = col - 1;   //从右上角开始
        while(i<row && j>0)
        {
            if (arr[i][j] == target)
                return true;  //找到目标值
            else if (arr[i][j] > target)
                j--;   //这一列不符合
            else
                i++;  //这一行不符合
        }
        return false;
    }
}

**

2.旋转数组的最小数字

**
题目描述:

把一个数组最开始的若干个元素搬到数组的末尾,我们称之为数组的旋转。 输入一个非减排序的数组的一个旋转,输出旋转数组的最小元素。 例如数组{3,4,5,1,2}为{1,2,3,4,5}的一个旋转,该数组的最小值为1。 NOTE:给出的所有元素都大于0,若数组大小为0,请返回0。

解题思路:

本题的直观解法很简单,直接对数组进行一次遍历就可以找到最小值,复杂度为O(n),但是显然这不是本题的意图所在,因为没有利用到任何旋转数组的特性。

进一步分析,如果整个数组是有序的,那我们一定会想到用折半查找来实现。对于旋转数组,我们发现,它实际上可以划分为两个排序的子数组,而且前面数组的元素都不小于后面数组的元素,并且最小值正好就是这两个数组的分界线,由此,我们可以得出以下解决方法。

首先用两个指针low和high分别指向数组的第一个元素和最后一个元素,然后可以找到中间元素mid。对于这个中间元素,有以下两种情况:(1)该元素大于等于low指向的元素,此时最小的元素说明在mid的后面,可以把low=mid;(2)中间元素小于等于high指向的元素,那么最小元素在mid之前,可以high=mid。特别注意:这里不要+1或者-1,因为只有这样才能保证low始终在第一个数组,high始终在第二个数组。依次循环,当最后low和high相差1时,low指向第一个数组的最后一个,high指向第二个数组的第一个(即为我们要找的最小值)。
  除此之外,本题还有两个特殊情况:

将数组前0个元素移动到后面(相当于没有旋转,数组整体有序)。明显我们上面的分析没有包含这种情况,需要特殊处理,方法也很简单,将第一个元素和最后一个元素相比,若第一个元素小于最后一个元素,则说明最小值就是的第一个元素,可以直接返回。

首尾指针指向的数字和中间元素三者都相等时,无法判断中间元素位于哪个子数组,无法缩小问题规模。此时,只能退而求其次,进行顺序查找。
图片详解
  很明显,以上查找的时间复杂度为O(logN)。

public int find(int[] arr) {
        int len = arr.length;
        if (len == 0)
            return 0; //数组长度为零则返回0
        int low = 0;
        int high = len-1;
        if (arr[low] < arr[high])
            return arr[low]; //此情况为按升序排列的数组
        while (low < high){
            int mid = low + (high - low)/2; //寻找中间数
            if (arr[low] == arr[mid] && arr[high] == arr[mid])
                return minInOrder(arr); // 不是一个规则的旋转数组就直接查找最小数
            if (arr[mid] < arr[high])
                high = mid; // 中间数小于最右边的数则说明最小数在次数左边
            else if (arr[mid] > arr[low])
                low = mid;
            if (high - low == 1)
            return arr[high]; //两个指针只相差一位时 说明最大数就在右边的指针上

        }
        return -1;
    }

    private int minInOrder(int[] arr) {
        int num = arr[0];
        for (int a: arr) {
            if (a < num)
                num  = a;
        }
        return num;
    }

3.调整数组顺序使奇数位于偶数前面

题目描述:

输入一个整数数组,实现一个函数来调整该数组中数字的顺序,使得所有的奇数位于数组的前半部分,所有的偶数位于数组的后半部分,并保证奇数和奇数,偶数和偶数之间的相对位置不变。

解题思路:

首先,如果不考虑奇数和奇数,偶数和偶数的相对位置,那么我们有一种双指针解法来求解,类似于快排,维护两个指针,第一个指针指向数组的第一个数字,第二个指针指向数组的最后一个数字。第一个指针向后移,第二个指针向前移,如果第一个指针指向偶数,第二个指针指向的是奇数,则交换着两个数字,接着继续移动直到两指针相遇。

上面的方法看似不错,但是对本题不适用,因为本题有相对位置不变的要求,直接交换会导致相对位置改变。因此,我们采用下面的思路来解决本题。

本题解法:对数组进行遍历,设置两个指针even和odd,even指向当前第一个偶数,odd从这个偶数之后开始查找,找到第一个奇数,此时为了相对位置不变,不能直接交换even和odd,而是将从even到odd-1的元素都依次向后移一个位置,将odd指向的那个奇数放到even的位置。然后再找下一个偶数,重复这一过程,最终就可以将奇数都放到偶数的前面,并且保证了相对位置的不变。
  代码如下:

public void sort(int[] arr){
        int len = arr.length;
        int odd = 0;
        int even = 0;
        while(odd < len && even < len){
            while (even < len && arr[even]%2 != 0)
                even++;
            odd = even +1;
            while (odd < len && arr[odd]%2 ==0)
                odd++;
            if (odd >= len)
                break;
            int tmp = arr[odd];

            for (int i = odd;i>even;i--)
                arr[i] = arr[i-1];
            arr[even] = tmp;
            even++;

        }
    }

4.顺时针打印矩阵

题目描述:

输入一个矩阵,按照从外向里以顺时针的顺序依次打印出每一个数字,例如,如果输入如下4 X 4矩阵: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 则依次打印出数字1,2,3,4,8,12,16,15,14,13,9,5,6,7,11,10.

解题思路:

由于是按照从外到内的顺序依次打印,所以可以把矩阵想象成若干个圈,用一个循环来打印矩阵,每次打印矩阵中的一圈。假设矩阵的行数是row,列数是col,则每次都是从左上角开始遍历,而我们注意到左上角行标和列标总是相同的,假设是start,那么循环继续的条件就是row>start * 2 && col > start * 2。

而对于每一圈的打印,很自然便可以想到遵循从左到右,从上到下,从右到左,从下到上的顺序。但是这里需要注意的是最后一圈的打印,由于矩阵并不一定是方阵,最后一圈有可能退化为只有一行,只有一列,甚至只有一个数,因此要注意进行判断,避免重复打印。
  图片详解

 public void printMatrixClockwisely(int[][] numbers, int rows, int columns) {
        if (numbers == null || rows <= 0 || columns <= 0) {
            return;
        }
        int start = 0;
        while (start * 2 < columns && start * 2 < rows) {
            printMatrixInCircle(numbers, rows, columns, start);
            start++;
        }
    }

    public void printMatrixInCircle(int[][] numbers, int rows, int columns,int start) {
        //静止列号
        int endX = columns - 1 - start;
        //静止行号
        int endY = rows - 1 - start;
        //从左至右打印一行
        for(int i = start; i <= endX; i++){
            System.out.print(numbers[start][i]+"  ");
        }
        //从上到下打印一列
        if(endY > start){
            for(int i = start+1; i<= endY; i++){
                System.out.print(numbers[i][endX] + "  ");
            }
        }
        //从右至左打印一行
        if(endY > start && endX > start){
            for(int i  = endX-1 ; i>= start; i--){
                System.out.print(numbers[endY][i] + "  ");
            }
        }
        //从下到上打印一列
        if(endY - 1 > start && endX > start){
            for(int i = endY-1; i >= start+1 ; i--){
                System.out.print(numbers[i][start] + "  ");
            }
        }
    }

5.数组中出现次数超过一半的数字

题目描述:

数组中有一个数字出现的次数超过数组长度的一半,请找出这个数字。
  例如:输入如下所示的一个长度为9的数组{1,2,3,2,2,2,5,4,2}。由于数字2在数组中出现了5次,超过数组长度的一半,因此输出2。如果不存在则输出0。

解题思路:

本题有以下三种方法可解:

方法一:首先对数组进行排序,在一个有序数组中,次数超过一半的必定是中位数,那么可以直接取出中位数,然后遍历数组,看中位数是否出现次数超过一半,这取决于排序的时间复杂度,最快为O(nlogn)。

方法二:遍历数组,用 HashMap 保存每个数出现的次数,这样可以从map中直接判断是否有超过一半的数字,这种算法的时间复杂度为O(n),但是这个性能提升是用O(n)的空间复杂度换来的。

方法三(最优解法):根据数组特点得到时间复杂度为O(n)的算法。根据数组特点,数组中有一个数字出现的次数超过数组长度的一半,也就是说它出现的次数比其他所有数字出现的次数之和还要多。因此,我们设两个栈,当空栈或者遇到相同数时我们往栈里压入数字,反之弹栈,这样最后就剩一个数字就是我们要找的那个。
  代码如下:

class HalfElements{
    //方式一
    public int find_1( int[] arr){
        Arrays.sort(arr);
        int low = 0;
        int hight = arr.length-1;
        return arr[low + (hight - low)/2];  //中位数就是我们要找的那个
    }
    //方式二
    public  int find_2(int[] arr) {
        Map<Integer, Integer> map = new HashMap();
        for (int i = 0;i<arr.length;i++)
        {
            if (!map.containsKey(arr[i]))
                map.put(arr[i],1); //哈希表里没有的话就往里面存
            else
                map.put(arr[i],map.get(arr[i])+1);//发现相同键值就value值加1
        }
       /* for (Map.Entry<Integer, Integer> entry : map.entrySet()) {
            if (entry.getValue() * 2 > arr.length)
                return entry.getKey();
        }*/
       //两种不同的遍历方式喜欢哪种都行
       for (int Key:map.keySet())
       {
           if (map.get(Key)>arr.length/2)
               return Key;
       }
            return 0; 
    }
//方式三
    public int find_3(int[] arr){
        Stack<Integer> s = new Stack<>();
        for (int i = 0;i<arr.length;i++)
        {
            int a = arr[i];
            
            if (s.isEmpty()||a == s.peek())//当前值与栈里的值相同则入栈
                s.push(a);
            else
                s.pop();
        }
        return s.peek(); //剩下这个就是我们需要的
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值