剑指offer之数组

目录

面试题3I:数组中重复的数字

面试题3II:不修改数组找出重复数字

面试题4:二维数组中的查找

面试题11:旋转数组的最小数字

面试题21:调整数组顺序使奇数位于偶数前面

面试题29:顺时针打印矩阵

面试题39:数组中出现次数超过一半的数字

面试题40:最小的k个数

面试题42:连续子数组的最大和

面试题45:把数组排成最小的数

面试题51:数组中的逆序对

面试题53:数字在排序数组中出现的次数

面试题56I:数组中只出现一次的两个数字

面试题56II:数组中唯一只出现一次的数字

面试题63:股票的最大利润

面试题66:构建乘积数组


 

面试题3I:数组中重复的数字

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

分析:从头到尾依次扫描该数组,当扫描到下标为 i 的数字时,首先比较这个数字是否等于 i 。若等于,直接扫描下一个;不等于则将其与第a[i]个数字比较,如果相等则找出了第一个重复数字,不相等则交换两者的位置。时间复杂度:O(n)

    public boolean duplicate(int[] numbers,int length,int [] duplication) {
        if(numbers == null || numbers.length == 0)
            return false;
        for(int i = 0;i < numbers.length;i++){
            if(numbers[i] != i){
                if(numbers[numbers[i]] == numbers[i]){
                    duplication[0] = numbers[i];
                    return true;
                }
                int temp = numbers[i];
                numbers[i] = numbers[temp];
                numbers[temp] = temp;
            }
        }
        return false;
    }

面试题3II:不修改数组找出重复数字

题目:在一个长度为n+1的数组里的所有数字都在1到n的范围内,所以数组中至少有一个数字是重复的。请找出数组中任意一个重复的数字,但不能修改输入的数组。例如,如果输入长度为8的数组{2, 3, 5, 4, 3, 2, 6, 7},那么对应的输出是重复的数字2或3.

分析:数组长度为n+1,而数字只从1到n,说明必定有重复数字。可以由二分查找法拓展:把1~n的数字从中间数字m分成两部分,若前一半1~m的数字数目超过m个,说明重复数字在前一半区间,否则,在后半区间m+1~n。每次在区间中都一分为二,直到找到重复数字。时间复杂度:O(nlogn)

    public int getDuplication(int[] numbers) {
        if(numbers == null || numbers.length == 0)
            return -1;
        int low = 1,high = numbers.length - 1;
        while(low <= high){
            int mid = (low + high) / 2;
            int count = countRange(numbers,low,mid);
            if(low == high){
                if(count > 1)
                    return low;
                else
                    return -1;
            }
            if(count > mid - low + 1)
                high = mid;
            else
                low = mid + 1;
        }
        return -1;
    }

    private int countRange(int[] numbers, int low, int high) {
        int count = 0;
        for(int i = 0;i < numbers.length;i++){
            if(numbers[i] >= low && numbers[i] <= high)
                count++;
        }
        return count;
    }

面试题4:二维数组中的查找

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

分析:首先选取数组中右上角的数字。如果该数字等于要查找的数字,则结束;如果该数字大于要查找的数字,则剔除这个数字所在的列;否则剔除这个数字所在的行。选取数组左下角的数字也可。

   public boolean Find(int target, int [][] array) {
        if(array == null || array.length == 0)
            return false;
        int col = array[0].length - 1;
        int row = 0;
        while(col >= 0 && row < array.length){
            if(array[row][col] == target)
                return true;
            if(array[row][col] > target)
                col--;
            else
                row++;
        }
        return false;
    }

面试题11:旋转数组的最小数字

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

分析:二分查找。旋转数组实际上可以划分为两个排序的子数组,而且前面子数组的元素都大于或等于后面子数组的元素,而最小元素刚好是这个子数组的分界线。找到数组的中间元素,判断它位于哪个子数组,从而缩小查找范围。按照上述思路,两个指针始终分别指向两个子数组,它们始终会指向两个相邻的元素,而第二个指针指向的刚好是最小的元素。注意两个特例:旋转0个元素;三个指针指向相同的数无法判断中间数字位于哪个数组,只能采用顺序查找的方式。

    public int minNumberInRotateArray(int [] array) {
        if(array == null || array.length == 0)
            return 0;
        int low = 0,high = array.length - 1;
        int mid = low;//旋转0个数字
        while(array[low] >= array[high]){
            if(high - low == 1)
                return array[high];
            mid = (low + high) / 2;
            //三个数字相同的情况下无法判断中间数字是位于前面的子数组还是后面的子数组
            if(array[mid] == array[low] && array[low] == array[high])
                return sequenceSearch(array,low,high);
            //中间数字位于前面子数组
            if(array[low] <= array[mid])
                low = mid;
                //中间数字位于后面子数组
            else if(array[high] >= array[mid])
                high = mid;
        }
        return array[mid];
    }

    private int sequenceSearch(int[] array, int low, int high) {
        int result = array[low];
        for(int i = low + 1;i <= high;i++){
            if(array[i] < result)
                result = array[i];
        }
        return result;
    }

面试题21:调整数组顺序使奇数位于偶数前面

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

分析:方法一、维护两个指针,利用快排思想,但会改变数与数之间的相互顺序。

注:可将下列函数解耦成两个部分:一是判断数字放在前面还是后面的标准,二是拆分数组的操作,提高代码的可扩展性。

    public void reOrderArray(int [] array) {
        if(array == null || array.length == 0)
            return;
        int low = 0,high = array.length - 1;
        while(low < high){
            while(low < high && array[low] % 2 == 1)
                low++;
            while(low < high && array[high] % 2 == 0)
                high--;
            if(low < high){
                int a = array[low];
                array[low] = array[high];
                array[high] = a;
            }
        }
    }

面试题29:顺时针打印矩阵

题目:输入一个矩阵,按照从外向里以顺时针的顺序依次打印出每一个数字,例如,如果输入如下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.

分析:将打印一圈分为从左到右打印行、从上到下打印列、从右到左打印行、从下到上打印列。每打印一次数字,记一次数,直到打印出矩阵中的左右数字为止。

    public ArrayList<Integer> printMatrix(int [][] matrix) {
        ArrayList<Integer> result = new ArrayList<>();
        if(matrix == null || matrix.length == 0)
            return result;
        int row = 0,row_end = matrix.length-1;
        int col = 0,col_end = matrix[0].length-1;
        int count = (row_end + 1) * (col_end + 1);
        while(count > 0){
            for(int i = col;i <= col_end && count > 0;i++,count--)//从左到右打印行
                result.add(matrix[row][i]);
            row++;
            for(int i = row;i <= row_end && count > 0;i++,count-- )//从上到下打印列
                result.add(matrix[i][col_end]);
            col_end--;
            for(int i = col_end;i >= col && count > 0;i--,count--)//从右到左打印行
                result.add(matrix[row_end][i]);
            row_end--;
            for(int i = row_end;i >= row && count > 0;i--,count--)//从下到上打印列
                result.add(matrix[i][col]);
            col++;
        }
        return result;
    }

面试题39:数组中出现次数超过一半的数字

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

分析:方法一,如果把这个数组排序,这个数就是中位数,即长度为n的数组中第n/2大的数字,可利用快排中patition算法。

     public int MoreThanHalfNum_Solution(int [] array) {
        if(array == null || array.length == 0)
            return 0;
        int mid = array.length >> 1;
        int low = 0,high = array.length - 1;
        int index = partition(array,low,high);
        while(index != mid){
            if(index < mid)
                index = partition(array,index + 1,high);
            else
                index = partition(array,low,index - 1);
        }
        if(isMoreHalf(array,array[mid]))
            return array[mid];
        else
            return 0;

    }
    private int partition(int[] array, int low, int high) {
        int target = array[low];
        while(low < high){
            while(low < high && array[high] >= target)
                high--;
            array[low] = array[high];
            while(low < high && array[low] <= target)
                low++;
            array[high] = array[low];
        }
        array[low] = target;
        return low;
    }

方法二,数组中有一个数字出现的次数超过数组长度的一半,也就是说它出现的次数比其它所有数字加起来的和还要多。因此在遍历数组的时候保存两个值,一个是数组中的一个数字,一个是次数。注意要检验输入的数组是否存在这样一个数。

   public int MoreThanHalfNum_Solution(int [] array) {
        if(array == null || array.length == 0)
            return 0;
        int result = array[0];
        int count = 1;
        for(int i = 1;i < array.length;i++){
            if(count == 0){
                result = array[i];
                count = 1;
            }
            else if(result == array[i])
                count++;
            else
                count--;
        }
        if(isMoreHalf(array,result))
            return result;
        return 0;
    }

    private boolean isMoreHalf(int[] array, int result) {
        int count = 0;
        for(int i = 0;i < array.length;i++){
            if(array[i] == result)
                count++;
        }
        if(count * 2 > array.length)
            return true;
        else
            return false;
    }

面试题40:最小的k个数

题目:输入n个整数,找出其中最小的K个数。例如输入4,5,1,6,2,7,3,8这8个数字,则最小的4个数字是1,2,3,4。

分析:方法一,基于partition函数的时间复杂度为O(n)的算法,但是会修改输入的数组。

   public ArrayList<Integer> GetLeastNumbers_Solution(int [] input, int k) {
        ArrayList<Integer> result = new ArrayList<>();
        if(input == null || input.length == 0 || k <= 0 || k > input.length)
            return result;
        int low = 0,high = input.length - 1;
        int index = partition(input,low,high);
        while(index != k-1){
            if(index < k-1)
                index = partition(input,index + 1,high);
            else
                index = partition(input,low,index - 1);
        }
        for(int i = 0;i <= index;i++)
            result.add(input[i]);
        return result;
    }

方法二,时间复杂度为O(nlogk)的算法,特别适合处理海量数据。可以创建一个大小为k的容器来存储最小的k个数,如果插入时容器已满,可以替换已有的数字。红黑树中的查找、删除和插入都只需要O(logk)时间,因此可以TreeSet来实现容器。

   public ArrayList<Integer> GetLeastNumbers_Solution(int [] input, int k) {
        ArrayList<Integer> result = new ArrayList<>();
        if(input == null || input.length == 0 || k <= 0 || k > input.length)
            return result;
        TreeSet<Integer> set = new TreeSet();
        int i = 0;
        for(;i < k;i++)
            set.add(input[i]);
        for(;i < input.length;i++){
           if(input[i] < set.last()){
               set.remove(set.last());
               set.add(input[i]);
           }
        }
        result.addAll(set);
        return result;
    }

面试题42:连续子数组的最大和

题目:在古老的一维模式识别中,常常需要计算连续子向量的最大和,当向量全为正数的时候,问题很好解决。但是,如果向量中包含负数,是否应该包含某个负数,并期望旁边的正数会弥补它呢?例如:{6,-3,-2,7,-15,1,2,2},连续子向量的最大和为8(从第0个开始,到第3个为止)。给一个数组,返回它的最大连续子序列的和,你会不会被他忽悠住?(子向量的长度至少是1)

分析:举例分析数组的规律。可试着从头到尾累加示例数组中的每个数字,若前面累加的数字为负数,就可不用考虑前面累加的数字,从下一个数字重新开始累加即可。

   public int FindGreatestSumOfSubArray(int[] array) {
        if(array.length == 1)
            return array[0];
        int max = array[0],count = 0;
        for(int i = 0;i < array.length;i++){
            if(count <= 0)
                count = array[i];
            else
                count += array[i];
            if(count > max)
                max = count;
        }
        return max;
    }

面试题45:把数组排成最小的数

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

分析:首先确立o一个规则判断m和n哪个应该排在前面,两个数字m和n能拼成数字mn和nm。如果mn<nm,应该打印出mn,即m应该排在n的前面,此时定义m小于n;反之,如果nm<mn,则定义n小于m。如果mn=nm,则定义m等于n。因存在大数问题,故我们把数字转化为字符串,另外把数字m和数字n拼接起来得到mn和nm,它们的位数肯定是相同的,因此比较它们的大小只需要按照字符串大小的比较规则就可以了。

    public String PrintMinNumber(int [] numbers) {
        if(numbers == null || numbers.length == 0)
            return "";
        String[]str = new String[numbers.length];
        for(int i = 0;i < numbers.length;i++)
            str[i] = String.valueOf(numbers[i]);
        Arrays.sort(str, new Comparator<String>() {
            @Override
            public int compare(String s1, String s2) {
               String add1 = s1 + s2;
               String add2 = s2 + s1;
               return add1.compareTo(add2);
            }
        });
        StringBuilder result = new StringBuilder();
        for(int i = 0;i < str.length;i++)
            result.append(str[i]);
        return result.toString();
    }

面试题51:数组中的逆序对

题目:在数组中的两个数字,如果前面一个数字大于后面的数字,则这两个数字组成一个逆序对。输入一个数组,求出这个数组中的逆序对的总数P。并将P对1000000007取模的结果输出。 即输出P%1000000007。

分析:先把数组分隔成子数组,先统计出子数组内部的逆序对的数目,然后再统计出两个相邻子数组之间的逆序对的数目。在统计逆序对的过程中,先用两个指针分别指向两个子数组的末尾,并每次比较两个指针指向的数字。每一次比较的时候,我们都把较大的数字从后往前复制到一个辅助数组中去,确保辅助数组中的数字是递增排序的,这个排序的过程就是归并排序。

    public int InversePairs(int [] array) {
        if(array == null || array.length == 0 || array.length == 1)
            return 0;
        int[]copy = new int[array.length];
        for(int i = 0;i < array.length;i++)
            copy[i] = array[i];
        return inversePairsCore(array,copy,0,array.length - 1);
    }

    private int inversePairsCore(int[] array, int[] copy, int start, int end) {
        if(start == end){
            copy[start] = array[start];
            return 0;
        }
        int mid = (start + end) / 2;
        int left = inversePairsCore(copy,array,start,mid);
        int right = inversePairsCore(copy,array,mid + 1,end);
        int count = 0;
        int i = mid,j = end,index = end;
        while(i >= start && j >= mid + 1){
            if(array[i] > array[j]){
                copy[index--] = array[i--];
                count += j - mid;
                if(count >= 1000000007)
                    count %= 1000000007;
            }
            else
                copy[index--] = array[j--];
        }
        while(i >= start)
            copy[index--] = array[i--];
        while(j >= mid + 1)
            copy[index--] = array[j--];
        return (count + left + right) % 1000000007;
    }

面试题53:数字在排序数组中出现的次数

题目:统计一个数字在排序数组中出现的次数。

分析:分别用二分查找法找出第一个k和找出第二个k,时间复杂度为O(logn)。注意为了区分下标0和不存在该数的0,在找k时返回的index是从1开始,而不是从0开始。

   public int GetNumberOfK(int [] array , int k) {
        if(array == null || array.length == 0)
            return 0;
        int start = findFirstK(array,k,0,array.length - 1);
        if(start == 0)
            return 0;
        int end = findEndK(array,k,start - 1,array.length - 1);
        return end - start + 1;
    }

    private int findFirstK(int[] array, int k, int low, int high) {
        while(low <= high){
            int mid = (low + high) >> 1;
            if(array[mid] == k){
                if(mid > 0 && array[mid-1] != k || mid == 0)
                    return mid + 1;
                else
                    high = mid-1;
            }
            else if(array[mid] > k)
                high = mid-1;
            else
                low = mid + 1;
        }
        return 0;
    }

    private int findEndK(int[] array, int k, int low, int high) {
        while(low <= high){
            int mid = (low + high) >> 1;
            if(array[mid] == k){
                if(mid < array.length - 1 && array[mid+1] != k || mid == array.length - 1)
                    return mid + 1;
                else
                    low = mid + 1;
            }
            else if(array[mid] > k)
                high = mid - 1;
            else
                low = mid + 1;
        }
        return 0;
    }

面试题56I:数组中只出现一次的两个数字

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

分析:利用异或的性质,任何一个数字异或它自己都等于0.如果从头到尾依次异或数组中的每个数字,最终得到的结果就是两个只出现一次的数字的异或结果,这个结果一定不为0,也就是说这个结果的二进制中一定存在某一位为1.数组可以根据这一位是否为1划分成两个子数组,这样只出现一次的两个数字就分别位于两个子数组中了。

     /*num1,num2分别为长度为1的数组。传出参数
    将num1[0],num2[0]设置为返回结果*/
    public void FindNumsAppearOnce(int [] array,int num1[] , int num2[]) {
        if(array == null || array.length == 0)
            return;
        int result = 0;
        for(int i = 0;i < array.length;i++)
            result ^= array[i];
        int index = 0;
        while((result & 1) == 0){
            index++;
            result >>= index;
        }
        for(int num : array){
            if((num>>index & 1) == 1)
                num1[0] ^= num;
            else
                num2[0] ^= num;
        }
    }

面试题56II:数组中唯一只出现一次的数字

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

分析:用位运算的思路,如果一个数字出现三次,那么它的二进制表示的每一位(0或1)也出现三次。如果把所有出现三次的数字的二进制表示的每一位都分别加起来,那么每一位的和都能被3整除。所以我们可以将所有数字的二进制表示的对应位都加起来,如果某一位能被三整除,那么只出现一次的数字在该位为0;反之,为1。

     public int singleNumber(int[] nums) {
        int[] bitSum = new int[32];
        for(int i = 0;i < nums.length;i++){//将二进制的每一位都相加
            int bitMask = 1;
            for(int j = 31;j >= 0;j--){//依次与1,10,100,...与运算进行位分离
                int bit = nums[i] & bitMask;
                if(bit != 0)
                    bitSum[j] += 1;
                bitMask = bitMask << 1;
            }
        }
        int result = 0;
        for(int i = 0;i < 32;i++){
            result = result << 1;
            result += bitSum[i] % 3;
        }
        return result;
    }

面试题63:股票的最大利润

题目:假设把股票的价格按照时间顺序先后存储在数组中,请问买卖该股票一次可能获得的最大利润是多少?假如,一只股票在某些时间节点的价格为{9,11,8,5,7,12,16,14}。如果我们能在价格为5的时候买入并在价格为16的时候卖出,则能获得最大的利润。

分析:先定义一个函数diff[i]为当卖出价为数组中第i个数字时可能的最大利润。显然,在卖出价固定时,买入价越低获得的利润越大。所以只要能记住之前的i-1个数字中的最小值,就能算出在当前价位卖出时可能得到的最大利润。

   public int maxProfit(int[] prices) {
        if(prices == null || prices.length == 0)
            return 0;
        int min = prices[0],max_profit = 0;
        for(int i = 1; i < prices.length ;i++){
            if(min > prices[i])
                min = prices[i];
            else
                max_profit = Math.max(max_profit,prices[i] - min);
        }
        return max_profit;
    }

面试题66:构建乘积数组

题目:给定一个数组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]。不能使用除法。

分析:可以定义C[i] = A[0] * A[1] * .... *  A[i - 1],D[i] = A[i+1] * A[i+2] * ... * A[n-1];有C[i+1] = C[i] * A[i],D[i] = A[i+1] * D[i+1].

    public int[] multiply(int[] A) {
        int[] B = new int[A.length];
        //计算A[i]*...*A[i-1]
        int temp = 1;
        for(int i = 0;i < A.length;i++){
            B[i] = temp;
            temp *= A[i];
        }
        //计算A[i+1]*...*A[A.length-1]
        temp = 1;
        for(int i = A.length - 1;i >= 0;i--){
            B[i] *= temp;
            temp *= A[i];
        }
        return B;
    }

猜你喜欢

转载自blog.csdn.net/Nibaby9/article/details/104126765
今日推荐