剑指Offer编程题笔记之数组相关

前言

这一篇是数组相关的题,这里记录了9题,可能记录的不完整。
数组相关的题,解法多样,效率也各不相同。第二次实现,做得也不流畅。另外,还有两题没做出来,分别是“数组中的逆序对”和“连续子数组的最大和”。

题目

二维数组中的查找

第1题

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

思路:
由题目给出的信息可以得到这样的规律,二维数组中右边的数比左边的数大,下面的数比上面的数大,因此,如果以左下角为起点,往上走数字会变小,往右走则会变大。因此可以利用这个规律从左下角的数字开始与目标数字进行判断,直到走到左上角还没能找到。

实现如下:

public class Solution {
    public boolean Find(int target, int [][] array) {
        int x = array.length-1;
        int y = 0;
        while(x>=0&&y<=array[0].length-1){
            if(array[x][y]==target)
            return true;
        else if(array[x][y]<target)
            y++;
        else
            x--;
        }
        return false;
    }
}

旋转数组的最小数字

第2题

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

思路:
{3,4,5,1,2},观察得到的信息是相邻的两个数字若左边的数字比右边的大,说明右边的数字是最小的数字。因此,可以利用两个紧挨着的前后指针AB,若指针A指向的数字比指针B指向的数字大,说明指针B指向的数是最小的。

public class Solution {
    public int minNumberInRotateArray(int [] array) {
        int length=array.length;
        if(length==0)
            return 0;
        for(int i=0;i<length-2;i++){
            if(array[i]>array[i+1])
                return array[i+1];
        }
        return array[0];
    }
}

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

第3题

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

思路:
如果可以使用额外空间的话,创建两个list,一个用来存奇数,一个用来存偶数,遍历数组,根据奇偶放入list,最后用两个list里的元素重新填充数组。

实现如下:

public class Solution {
    public void reOrderArray(int [] array) {
        ArrayList<Integer> list1 = new ArrayList<>();
        ArrayList<Integer> list2 = new ArrayList<>();
        for(int i=0;i<array.length;i++){
            if(array[i]%2!=0)
                list1.add(array[i]);
            else
                list2.add(array[i]);
        }
        int size1 = list1.size();
        for(int i=0;i<size1;i++)
            array[i] = list1.get(i);
        int size2 = list2.size();
        for(int i=0;i<size2;i++)
            array[i+size1] = list2.get(i);
    }
}

思路2:
和思路1差不多。但使用的容器是数组。
遍历整数数组获取奇数个数n1,偶数个数也可得出。创建一个等长的数组,新数组[0,n1)用于存奇数,[n1,array.length)用于存偶数。遍历整数数组,根据奇偶放入不同位置。遍历完,新数组就是按照前奇后偶的顺序排了。最后遍历新数组,重新填充整形数组。这种解法时间复杂度和空间复杂度都比较良好。

实现如下:

public class Solution {
    public void reOrderArray(int [] array) {
        int length = array.length;
        int[] newArray = new int[length];
        int n1 = 0;
        for(int i=0;i<length;i++)
            if(array[i]%2!=0)
                n1++;
        int n = 0;
        for(int i=0;i<length;i++)
            if(array[i]%2!=0)
                newArray[n++] = array[i];
            else
                newArray[n1++] = array[i];
        for(int i=0;i<length;i++)
            array[i] = newArray[i];
    }
}

另一种思路:
如果空间复杂度为O(1)的话,该怎么办呢。使用两层for循环遍历,前偶后奇则交换。类似冒泡。但时间复杂度为O(N^2)。

实现如下:

public class Solution {
    public void reOrderArray(int [] array) {
        for(int i=0;i<array.length-1;i++)       //表示需要遍历多少轮
            for(int j=array.length-1;j>i;j--)   //从后往前比较
                if(array[j]%2==1 && array[j-1]%2==0){   //前偶后奇则交换
                    int temp = array[j];
                    array[j] = array[j-1];
                    array[j-1] = temp;
                }
    }
}

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

第4题

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

思路:
要找出出现次数超过数组长度一半的数字,一般来说就得依次计算每个数字出现的次数,直到找到符合要求的数字,或遍历完都没找到。如使用HashMap来存储出现的次数。但是呢,这样空间复杂度就不为1。
而这道题,是有捷径的。如果数组中有一个数字出现的次数超过数组长度的一半,那么数组中最中间的元素一定会是那个出现次数超过数组长度一半的数字。
因此,我们可以先找出该数字,再通过一次遍历,计算出该数字出现的次数。再判断是否大于数组长度的一半

实现如下:

public class Solution {
    public int MoreThanHalfNum_Solution(int [] array) {
        int target = array[array.length/2];
        int n = 0;
        for(int i=0;i<array.length;i++){
            if(array[i]==target)
                n++;
        }
        if(n>array.length/2)
            return target;
        return 0;
    }
}

把数组排成最小的数

第5题

题目描述
输入一个正整数数组,把数组里所有数字拼接起来排成一个数,打印能拼接出的所有数字中最小的一个。例如输入数组{3,32,321},则打印出这三个数字能排成的最小数字为321323。

思路:
定义比较规则,然后将数组中的元素按照指定的规则排序。
这个比较规则不是直接比较两个数字的大小,而是比较它们组合出来的数字:
假设要进行的数字是 a=123和b=12,要比较的是12312和12123,即ab和ba。
ab大,则a前b后。ba大则b前a后。

实现如下:

public class Solution {
    public String PrintMinNumber(int [] numbers) {
        ArrayList<Integer> list = new ArrayList();
        for(int i:numbers)
            list.add(i);
        Collections.sort(list,new MyComparator());
        StringBuffer sb = new StringBuffer();
        for(int i:list)
            sb.append(i);
        return sb.toString();
    }
}
class MyComparator implements Comparator<Integer>{
    public int compare(Integer i1,Integer i2){
        String s1 = ""+i1+i2;
        String s2 = ""+i2+i1;
        return s1.compareTo(s2);
    }
}

构建乘积数组

第6题

题目描述
给定一个数组A[0,1,...,n-1],请构建一个数组B[0,1,...,n-1],其中B中的元素B[i]=A[0]*A[1]*...*A[i-1]*A[i+1]*...*A[n-1]。不能使用除法。

思路:
注意,B[i]中没有乘上A[i],然后使用两层for循环,第一层for循环中,设置A[i]=1,就相当于没有乘上A[i]。然后在内循环中通过累成得到数组B的各个元素。这种时间复杂度为O(n^2),有点高。

实现如下:

public class Solution {
    public int[] multiply(int[] A) {
        int[] B = new int[A.length];
        for(int i=0;i<B.length;i++){
            int temp = A[i];
            A[i] = 1;   //设为1,如此使内循环中A[j]会失效
            B[i] = 1;   //初始值为0,因此要设为1,否则,数组B的每个院都会为0,因为0乘以任何数都为0
            for(int j=0;j<A.length;j++){
                B[i] *= A[j];
            }
            A[i] = temp;//还原A[i]的值
        }
        return B;
    }
}

另一种思路:
数组B由数组A累乘得到:

如此,在计算左下的三角形时,B[i]就能利用B[i-1]的值。即B[i]=B[i-1]*A[i-1]。右上的三角形也如此。这样,就省去了很多重复的计算。如此只需遍历这两个三角形就可以求得数组B了。时间复杂度为O(2n)。

实现如下:

public class Solution {
    public int[] multiply(int[] A) {
        int[] B = new int[A.length];
        B[0] = 1;
        for(int i=1;i<B.length;i++){
            B[i] = A[i-1]*B[i-1];
        }
        int temp = 1;
        for(int i=B.length-2;i>=0;i--){
            temp *= A[i+1]; //注意是A[i+1]而不是A[i]
            B[i] *= temp;
        }
        return B;
    }
}

数组中只出现一次的数字

第7题

题目描述
一个整型数组里除了两个数字之外,其他的数字都出现了两次。请写程序找出这两个只出现一次的数字。

思路:
可以使用HashMap,以元素值为key,以次数为value。最后找到两个value为1的键值对。但这需要额外维护一个HashMap。实现略。

另一种思路:
我们可以巧妙的利用位运算。相同的两个数字的异或为0。即1^1=0,1^2!=0,而1^1^2等于2。利用这个特性,遍历数组,求得最后的异或值(设为n)。而n必然就是那两个出现过一次的数字的异或结果。既然n是两个不同数组的异或结果,那么n必然不为0。进而n的二进制中必然存在1,我们找到n中最低位的1,设其值为low。(如n的二进制表达为00011100,那么low为00000100)。通过判断元素与low做与运算的结果是否为1来将数组分成两部分,从而将那两个不同的数字分开。

实现如下:

//num1,num2分别为长度为1的数组。传出参数
//将num1[0],num2[0]设置为返回结果
public class Solution {
    public void FindNumsAppearOnce(int [] array,int num1[] , int num2[]) {
        int n = 0;
        for(int i=0;i<array.length;i++){
            n ^= array[i]; 
        }
        int x = 1;
        while((n&x)==0){    //位运算的级别很低要加上括号,这里是(n&x)==0而不是(n&x)!=1
            x = x<<1;
        }
        num1[0] = 0;
        num2[0] = 0;
        for(int i=0;i<array.length;i++){
            if((array[i]&x)==0) //这里是且不是异或
                num1[0] ^= array[i];
            else
                num2[0] ^= array[i];
        }
    }
}

数组中的重复数字

第8题

题目描述
在一个长度为n的数组里的所有数字都在0到n-1的范围内。 数组中某些数字是重复的,但不知道有几个数字是重复的。也不知道每个数字重复几次。请找出数组中任意一个重复的数字。 例如,如果输入长度为7的数组{2,3,1,0,2,5,3},那么对应的输出是第一个重复的数字2。

思路:
通过一个容器来存储遍历过的值,若容器中已存在该值,说明该值出现了不止一次。这里使用HashSet来实现。

实现如下:

public class Solution {
    /*题目给出了length这个参数,且numbers可能为null,且要求把找到的值赋给duplication[0]*/
    public boolean duplicate(int numbers[],int length,int [] duplication) {
        HashSet set = new HashSet();
        for(int i=0;i<length;i++){//numbers可能为null,因此不用直接写numbers.length
            int key = numbers[i];
            if(!set.contains(key))
                set.add(key);
            else{
                duplication[0] = key;
                return true;
            }
        }
        duplication[0] = -1;
        return false;
    }
}

另一种实现:
使用boolean数组来作为容器:

public class Solution {
    public boolean duplicate(int numbers[],int length,int [] duplication) {
        boolean[] b = new boolean[length];
        for(int i=0;i<length;i++){
            if(b[numbers[i]]==false){
                b[numbers[i]] = true;
            }
            else{
                duplication[0] = numbers[i];
                return true;
            }
        }
        return false;
    }
}

数字在排序数组中出现的次数

第9题
题目描述
统计一个数字在排序数组中出现的次数。

思路:
最简单、暴力的思路,对整个数组进行遍历。

实现如下:

public class Solution {
    public int GetNumberOfK(int [] array , int k) {
        int n = 0;
        for(int i=0;i<array.length;i++){
            if(k==array[i])
                n++;
        }
        return n;
    }
}

但是这样的话并没有利用到题目给出的条件,数组是排序数组。假如这个数组非常的大,假如为{1,2,3,4,…,10000000},然后要找的数字是1,那么上面的解法得循环10000000次,而其实,只需循环两次就可以了,因为数组是排序的。
因此,考虑这种问题时,要想象数据量是非常非常的大。也就是说我们得考虑时间复杂度。

另一种思路:
由于数组是有序的,假设要找的数字是k,那么我们可以找出数组中处于最左位置的k和处于最右位置的k。这样,问题就转变为找这两个k了。而这个查找,利用数组有序的特性,可以使用二分查找。

实现如下:

public class Solution {
    public int GetNumberOfK(int [] array , int k) {
        int firstK = findFirstK(array,k);
        int lastK = findLastK(array,k);
        if(firstK!=-1 && lastK!=-1)
            return lastK-firstK+1;
        return 0;
    }
    private int findFirstK(int[] a, int k){
        int low = 0;
        int high = a.length-1;
        int middle = 0;
        while(low<=high){
            middle = low + (high-low)/2;
            if(a[middle]<k)
                low = middle+1;
            else if(a[middle]>k)
                high = middle-1;
            else if(middle-1>=0&&a[middle-1]==k)    //如果前一个也等于k(middle-1>=0用来确保前一个是存在的)
                high = middle-1;
            else
                return middle;
        }
        return -1;
    }
    private int findLastK(int[] a, int k){
        int low = 0;
        int high = a.length-1;
        int middle = 0;
        while(low<=high){
            middle = low + (high-low)/2;
            if(a[middle]<k)
                low = middle+1;
            else if(a[middle]>k)
                high = middle-1;
            else if(middle+1<a.length &&a [middle+1]==k)//如果后一个也等于k
                low = middle+1;
            else
                return middle;
        }
        return -1;
    }
}

总结

数组相关的题目,可以考虑使用容器来辅助,但这样一来,就需要额外维护一个容器。通常会有更加高效的方法。像题1、题4、题6,找到规律,遍历次数就少了很多了。像题7利用位运算来实现。像题9使用暴力解法代码很简洁,但是效率不高,使用二分查找来解,代码量虽然大,但是效率却高很多。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值