《剑指offer》数组专题 (牛客10.22)

考点:遍历、查找、排序、时空效率、思维、排序算法

// Q01 二维部分有序数组查找 【善用性质】

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

由于矩阵部分有序,向上数字递减,向右数字递增:

  1. 目标数字只会出现在行首小于该数的行中
  2. 遍历行i ,若列j 对应的元素大于目标,那么在前 i-1 行,目标只会出现在 j列及以后。

因此,可以从左下角开始查找。目标比左下角数字小时,上移;当要目标比左下角数字大时,右移。
搜索的路线是阶梯状,复杂度为O(行数+列数)。

class Solution {
public:
    bool Find(int target, vector<vector<int> > array) {
        int nrows = array.size();
        int ncols = array[0].size();
         
        int i = nrows - 1, j = 0;
         
        while(i >= 0 && j < ncols){
            if(array[i][j] == target) return true;
            else if(array[i][j]< target ){
                j++;
            }else i--;
        }
         
        return false;
    }
};

WA警告:

之前的错误代码版本如下,由于判断条件中的 && 写成 , 造成段错误(数组越界访问):

class Solution {
public:
    bool Find(int target, vector<vector<int> > array) {
        int nrows = array.size();
        int ncols = array[0].size();
        if( ncols == 0 || nrows == 0) return false;
        for(int i = nrows - 1, j = 0; i >= 0, j < ncols; i--){
            if( array[i][j] == target )
                return true;
            if( array[i][j] < target){
                i++;
                j++;
            }
        }
          
        return false;
    }
};

// Q06 旋转数组中的最小元素 【二分 || 暴力】

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

由于旋转后,数组前半段非减,后半段非减,所以从后往前遍历,转折点一定是最小元素。如果没有转折点,那么所有元素都相等。
​ . .··
​ . ·``

但是这个转折点可以试着二分。由于转折处的右侧是最小元素,所以选择尽量让区间右端点落在最小元素上。那么:

  1. array[mid] > array[high]
    出现这种情况的array类似[3,4,5,6,0,1,2],此时最小数字一定在mid的右边。
    low = mid + 1
  2. array[mid] == array[high]:
    出现这种情况的array类似 [1,0,1,1,1] 或者[1,1,1,0,1],此时最小数字不好判断在mid左边还是右边,这时只好一步步缩小区间。
    high = high - 1
  3. array[mid] < array[high]:
    出现这种情况的array类似[2,2,3,4,5,6,6],此时最小数字一定就是array[mid]或者在mid的左边。因为右边必然都是递增的。
    high = mid

注意,考虑最后区间长为2或为3 的情况,上述的收敛方式都会使得 low 和 high 最终都指向最小元素。

class Solution {
public:
    int minNumberInRotateArray(vector<int> rotateArray) {
        if(rotateArray.size()==0)
            return 0;
        int lo =0, hi = rotateArray.size()-1,mid;
         
        while(lo< hi){
            mid = lo+((hi-lo)>>1);
            if(rotateArray[mid]>rotateArray[hi])
                lo=mid+1;
            else if (rotateArray[mid] == rotateArray[hi])
                hi-=1;
            else hi=mid;
        }
        return rotateArray[hi];
    }
};

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

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

方法1:新开数组,先添加奇数,再添加偶数。代码略。

方法2:不借助额外内存

/**
 * 1.要想保证原有次序,则只能顺次移动或相邻交换。
 * 2.i从左向右遍历,找到第一个偶数。
 * 3.j从i+1开始向后找,直到找到第一个奇数。
 * 4.将[i,...,j-1]的元素整体后移一位,最后将找到的奇数放入i位置,然后i++。
 * 5.終止條件:j向後遍歷查找失敗。
 */
public void reOrderArray2(int [] a) {
    if(a==null||a.length==0)
        return;
    int i = 0,j;
    while(i<a.length){
        while(i<a.length&&!isEven(a[i]))
            i++;
        j = i+1;
        while(j<a.length&&isEven(a[j]))
            j++;
        if(j<a.length){
            int tmp = a[j];
            for (int j2 = j-1; j2 >=i; j2--) {
                a[j2+1] = a[j2];
            }
            a[i++] = tmp;
        }else{// 查找失敗
            break;
        }
    }
}
boolean isEven(int n){
    if(n%2==0)
        return true;
    return false;
}

/ Q19 顺时针打印矩阵 【使用边界变量】

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

/**
 A--------------------------------------------------------
 * 思路1:vis数组记录 - 简单但浪费空间
 * 思路2:用变量记录当前是第几圈 - 边界计算有点麻烦
 * 思路☆:用四个变量分别记录当前的四周边界,检查是否在边界内 && 边界是否满足条件
 T--------------------------------------------------------
 * 注意边界的检查(保证[在边界内 && 边界合法]) 
 */

import java.util.ArrayList;
public class Solution {
    public ArrayList<Integer> printMatrix(int [][] matrix) {
        ArrayList<Integer> ans = new ArrayList<>();

        if(matrix.length == 0 || matrix[0].length == 0)
            return ans;

        int nrow = matrix.length, ncol = matrix[0].length;
        int top=0,left=0,bottom=nrow-1,right=ncol-1;
        while (top<=bottom&&left<=right){
            // top: left -> right
            for(int i=left;i<=right;i++){
                ans.add(matrix[top][i]);
            }top++;
            // right: top -> bottom
            for(int i=top;i<=bottom&&left<=right;i++){//其实这里left<=right的判断多余,因为left和right的值都还没有变化
                ans.add(matrix[i][right]);
            }right--;
            // bottom: right -> left
            for(int i=right;i>=left&&top<=bottom;i--){
                ans.add(matrix[bottom][i]);
            }bottom--;
            // left: bottom -> top
            for(int i=bottom;i>=top&& left<=right;i--){
                ans.add(matrix[i][left]);
            }left++;
        }
        return ans;
    }
}

/ Q28 找出数组中出现超过一半的数字

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

方法 1:HashMap统计各数字出现次数

/**
 * 用 HashMap 统计各个数字出现的次数
 * 注意 HashMap 的使用方法
 */

import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;

public class Solution {
    public int  MoreThanHalfNum_Solution(int [] array) {
        HashMap<Integer,Integer> map = new HashMap<>();

        for(int i=0;i<array.length;i++){
            if(!map.containsKey(array[i])){
                map.put(array[i],1);
            }else{
                int cnt = map.get(array[i]);
                map.put(array[i],++cnt);
            }
        }

        Iterator iter = map.entrySet().iterator();
        while(iter.hasNext()){
            Map.Entry entry = (Map.Entry)iter.next();
            Integer key = (Integer)entry.getKey();
            Integer val = (Integer)entry.getValue();
            if(val>array.length/2){
                return key;
            }
        }
        return 0;
    }
}

方法 2:基于Partition查找第K大 O(n)

​ 对于该数组,若存在出现次数超过一半的数字,那么数组排序后该数字一定位于数组中间(中位数),于是可以利用O(n)的查找第K(=n/2)大的算法。

/**
 * 由于每次 partition 后,pivot 左边的值都较小,右边的值都较大,
 * 所以当 pivot 位置为 K 时,pivot 的值为第 K 大的值。
 * 不断对区间进行划分使得 pivot 位于 K, 然后检验它出现的次数是否大于一半。
 */

public class Solution {
    public int  MoreThanHalfNum_Solution(int [] array) {
        if(array == null||array.length ==0)
            return 0;
        int mid = array.length/2;
        int start = 0, end = array.length-1;
        int index = Partition(array,start,end); // 左闭右闭
        while(index!=mid){
            if(index > mid){
                index = Partition(array,start,index-1);
            }else{
                index = Partition(array,index+1,end);
            }
        }
        int result = array[index];

        if(check(array,result,mid))
            return result;

        return 0;
    }

    private int Partition(int[] arr, int start, int end) {
        int pivot = arr[start];
        int left = start, right = end;
        while (left < right) {
            // 因为pivot取的是最左边,所以要先从右边找
            while (left < right && arr[right] >= pivot) right--;
            arr[left] = arr[right];
            while (left < right && arr[left] <= pivot) left++;
            arr[right] = arr[left];
        }
        arr[left] = pivot;
        return left;// left 为最后枢值所在的位置
    }

    private boolean check(int[] arr, int result, int mid){
        int times=0;

        for (int anArr : arr) {
            if (anArr == result)
                times++;
        }
        if(times*2>arr.length)
            return true;
        return false;
    }
}

方法 3:遍历判断,两两相消

​ 如果有符合条件的数字,则它出现的次数比其他所有数字出现的次数和还要多。
​ 思路的本质是,将一对不同的数相消,那么符合条件的数字一定是最后留下来的数字。
​ 在遍历数组时保存两个值:一是数组中一个数字,一是次数。遍历下一个数字时,若它与之前保存的数字相同,则次数加1,否则次数减1;若次数为0,则保存下一个数字,并将次数置为1。遍历结束后,所保存的数字即为所求。然后再判断它是否符合条件即可。

/ Q30 连续子数组的最大和 【dp ,Math.max()】

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

/**
 * dp[i] 为以 array[i] 为末尾的最大子向量和
 * dp[i] = max(dp[i-1]+a[i], a[i])
 */

public class Solution {
    public int FindGreatestSumOfSubArray(int[] array) {
        if(array==null||array.length==0)
            return 0;

        int rst = array[0],dp = array[0];

        for(int i=1;i<array.length;i++){
            dp = Math.max(dp+array[i],array[i]);
            rst=Math.max(rst,dp);
        }

        return rst;
    }
}

/ Q32 把数组排成最小的数【Collections.sort(),Integer.toString(int n),stringBuilder.append(str)】

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

/**
 * 不是比较字典序!
 *
 * 先将整型数组转换成String数组,然后排序,最后将排好序的字符串数组拼接。
 * 关键就是制定排序规则:
 * 若ab > ba 则 a > b,
 * 若ab < ba 则 a < b,
 * 若ab = ba 则 a = b;
 * 解释说明:
 * 比如 "3" < "31"但是 "331" > "313",所以要将二者拼接起来进行比较
 */

import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;

public class Solution {
    public String PrintMinNumber(int [] numbers) {
        if(numbers==null||numbers.length==0)
            return "";
        ArrayList<String> arrayList = new ArrayList<>();
        for(int n:numbers)
            arrayList.add(Integer.toString(n));

        // 重定义 Collections.sort()的排序规则
        Collections.sort(arrayList, new Comparator<String>() {
            @Override
            public int compare(String o1, String o2) {
                String s1=o1+o2;
                String s2=o2+o1;
                return s1.compareTo(s2); // 返回的值小于0则表示 s1<s2, s1 排在 s2 前
            }
        });


        StringBuilder rst=new StringBuilder();
        for(String s:arrayList){
            rst.append(s);
        }

        return rst.toString();
    }
}

简洁写法如下,暂时还没看懂。

public String PrintMinNumber(int [] numbers) {
        ArrayList<Integer> list = new ArrayList();
        Arrays.stream(numbers).forEach(list::add);
 
        list.sort((str1, str2) -> {
            String s1 = str1 + "" + str2;
            String s2 = str2 + "" + str1;
            return s1.compareTo(s2);
        });
 
        String s = "";
        for (int j : list) {
            s += j;
        }
 
        return s;
    }

Array.sort() to_string(int n)

/// Q35 数组中的逆序对

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

输入:

题目保证输入的数组中没有的相同的数字数据范围: 对于%50的数据,size<=10^4 对于%75的数据,size<=10^5 对于%100的数据,size<=2*10^5

示例:

1,2,3,4,5,6,7,0的逆序对数为7。

方法 1:归并排序统计逆序对

归并排序的递归过程是,先将区间一分为二,得到两个有序的左右区间,然后将左右区间合并。

得到有序左右区间的同时可以计算出区间内的逆序数,合并时可以得到区间间的逆序数。区间的逆序数 = 两个子区间的逆序数之和 + 子区间间逆序数。

归并时需要借助 O(n)的辅助空间。

public class Solution {
    public int InversePairs(int[] array) {
        if (array == null || array.length == 0)
            return 0;
        int[] arr = array.clone();
        int[] copy = arr.clone();
        int MOD = 1000000007;
        long ans = MergeSort(arr, copy, 0, arr.length, MOD);// [ , )
        return (int) (ans % MOD);
    }

    private long MergeSort(int[] arr, int[] copy, int start, int end, int MOD) {// [ , )
        long count = 0;
        if (start + 1 < end) {
            int mid = (start + end) / 2;
            long a = MergeSort(arr, copy, start, mid, MOD);
            long b = MergeSort(arr, copy, mid, end, MOD);
            long c = Merge(arr, copy, start, mid, end, MOD);
            count = a + b + c;
        }
        return count % MOD;
    }

    private long Merge(int[] arr, int[] copy, int start, int mid, int end, int MOD) {
        long count = 0;
        
        System.arraycopy(arr, start, copy, start, end - start);
        int i = start, j = mid, k = start;
        while (i < mid && j < end) {
            if (copy[i] <= copy[j]) {
                arr[k++] = copy[i++];
            } else {
                count += (mid - i);
                arr[k++] = copy[j++];
            }
        }
        while (i < mid) arr[k++] = copy[i++];
        while (j < end) arr[k++] = copy[j++];

        return count % MOD;
    }
}

方法 2:树状数组

也许要加离散化就没有写了。

树状数组求逆序数的原理
首先明确树状数组在此问题中维护信息是某个区间中数字出现的个数,将源数据按其原本顺序插入树状数组,第i个数字插入的方式为将树状数组的第a[i]位设为1,同时更新覆盖到它的父区间,Query(a[i])可求得[1, a[i]]的区间和,这恰好代表第i个数字前小于等于它的个数,等于的只可能是自身,故小于它的有Query(a[i])-1个,那么大于它的显然就有i-1-(Query(a[i])-1) = i-Query(a[i])

for (int i = 1; i <= n; i++) {
    Update(i, 1);
    ans += i - Query(a[i]);
}

int lowbit(int x) {
    return x & (-x);
}
void Update(int pos, int val) {
    for (int i = pos; i <= n; i += lowbit(i)) {
        c[i] += val;
    }
}
int Query(int pos) {
    int ans = 0;
    for (int i = n; i > 0; i -= lowbit(i)) {
        ans += c[i];
    }
    return ans;
}

参考:https://blog.csdn.net/Tc_To_Top/article/details/79562501

/ Q37 数字在排序数组中出现的次数 【二分查找写法分析,equal_range()】

默认是升序。

方法 1:手写二分查找,搜索 k-0.5和 k+0.5

(注意:如果查找的数在数组中存在,这种二分写法是会返回错误的)

/**
 * 由于是有序数组中查找,考虑二分;
 * 由于是整数,所以可以稍微变一下,搜索k-0.5和k+0.5
 * 两个位置相减,即为k出现的次数
 */
public class Solution {
    public int GetNumberOfK(int [] array , int k) {
        if(array==null||array.length==0)
            return 0;
        return BiSearch(array, k+0.5) - BiSearch(array, k-0.5) ;
    }

    private int BiSearch(final int[] array,double x){
        // 返回第一个大于x的数的位置
        int left=0,right = array.length-1,mid;
        while(left<=right){
            mid= (right-left)/2+left;
            if(array[mid]<=x)// 如果需要的是第一个大于等于x的数的位置呢?
                left=mid+1;
            else
                right=mid-1;

        }
        return left;
    }
}

二分写法的思路分析:
由于要返回第一个大于等于x的数的位置,用最后的left指定为目标位置,初始范围为[0, array.length](因为没有查找到时该返回数组end)。查找过程中,当
a[mid] <x 时,a[mid] 在目标左侧,left = mid+1
a[mid] >x 时,a[mid] 在目标右侧,right = mid-1
a[mid]==x 时,a[mid] 可能为目标,也可能在目标右侧,right = mid
最后,当区间内只有两个元素时,若left所指为目标,right左移,否则left右移,两者重合结束查找。

二分总结

二分查找当查找的任务不同时,left = mid 或者 left = mid + 1 或者 right = mid 或者right = mid + 1 的写法是不同的,要分情况讨论,原则是 left 与 right 的赋值要保证目标在 [left, right]内,最后,考虑区间长度为2时的情况,然后给定循环结束条件是 left <= right,还是 left < right。

//搜索k和k+1
public class Solution {
    public int GetNumberOfK(int [] array , int k) {
        if(array==null||array.length==0)
            return 0;
        return BiSearch(array, k+1) - BiSearch(array, k) ;
    }

    private int BiSearch(final int[] array,int x){
        // 返回第一个大于等于x的数的位置
        int left=0,right = array.length,mid;
        //为了使得当元素在数组中不存在时返回正确 right != array.length-1
        while(left<right){
            mid= (right-left)/2+left;
            if(array[mid]<x)
                left=mid+1;
            else
                right=mid;
        }
        return left;
    }
}

方法 2:C++ STL equal_range()

lower_bound(begin, end,val): 返回容器中第一个大于或等于val的元素的 iterator 位置。
upper_bound(begin, end, val): 返回容器中第一个大于val的元素的 iterator 位置。
binary_search(begin, end, val): 返回容器中是否有元素等于val
equal_range(begin, end, val): 返回 lower_boundupper_bound的 pair。

class Solution {
public:
    int GetNumberOfK(vector<int> data ,int k) {
        auto resultPair = equal_range(data.begin(), data.end(),k);
        return resultPair.second - resultPair.first;
    }
};

// Q40 找出数组中两个只出现一次的数字 【位运算】

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

我们知道,如果只有一个数字出现一次的话,所有元素异或结果便是该数。
在本题,所有元素异或结果为两个数的异或结果。显然,我们无法从两个数的异或结果中推导出原来的数。
但是,若异或结果二进制中某bit为1,说明那两个数该bit上一定不同,由此可以把数组中的数分为两类,一类该bit为0,一类该bit为1,要找的两个数一定分别在两类中。把两类数分别异或,就得到了结果。

//num1,num2分别为长度为1的数组。传出参数
//将num1[0],num2[0]设置为返回结果
public class Solution {
    public void FindNumsAppearOnce(int [] array,int num1[] , int num2[]) {
        if(array == null || array.length==1)
            return ;

        int xor = 0;
        for(int x:array){
            xor^=x;
        }

        if (xor == 0)
            return;

        int fisrtBit = (xor&(xor-1))^xor;// 结合上次学到的 n&(n-1) ,找到第一个为1 的bit ^. ^  ,或者 Xor&~(Xor - 1)
        
        num1[0]=num2[0]=0;
        for(int x:array){
            if((x&fisrtBit)!=0) num1[0]^=x;
            else num2[0]^=x;
        }
    }
}

拓展:数组a中只有一个数出现一次,其他数字都出现了3次,找出这个数字

申请了32位数组,然后把原数组中的每一个数字,展开成二进制,哪一位为1,那么bits[]那一位就+1. 最终,判断bit中每一位是否是3的倍数(或者是0),如果是,那么我们要找的数字在这一位肯定为0,反之为1

/**
     * 数组a中只有一个数出现一次,其他数字都出现了3次,找出这个数字
     * @param a
     * @return
     */
public static int find1From3(int[] a){
    int[] bits = new int[32];
    int len = a.length;
    for(int i = 0; i < len; i++){
        for(int j = 0; j < 32; j++){
            bits[j] = bits[j] + ( (a[i]>>>j) & 1);
        }
    }
    int res = 0;
    for(int i = 0; i < 32; i++){
        if(bits[i] % 3 !=0){
            res = res | (1 << i);
        }
    }
    return res;
}

/ Q50 数组中重复的数字 【不借助辅存】

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

当一个数字被访问过后,可以设置对应位上的数值 + n,之后再遇到相同的数时,会发现对应位上的数已经大于等于n了,那么直接返回这个数即可。

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

Q51 构建乘积数组

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

先顺着乘,再倒着乘。

public class Solution {
    public int[] multiply(int[] A) {
        int[] B = new int[A.length];
        if (B.length != 0) B[0] = 1;
        for (int i = 1; i < B.length; i++) {
            B[i] = B[i - 1] * A[i - 1];
        }

        int temp = 1;
        for (int j = B.length - 2; j >= 0; j--) {
            temp *= A[j + 1];
            B[j] *= temp;
        }
        return B;
    }
}
原文地址:https://www.cnblogs.com/weseewe/p/11726881.html