算法刷题1【剑指offer系列之数组】

数组(按照牛客题目顺序)

(思路写得可能不是很全,看不懂的话强烈建议看书,我自己都很后悔第一次刷没有看书就做题,思路主要是书上,代码绝大部分自己写的,也有部分是看了牛客网讨论区的大佬写的)
2020.05.19

1、字符串替换

请实现一个函数,将一个字符串中的每个空格替换成“%20”。例如,当字符串为We Are Happy.则经过替换之后的字符串为We%20Are%20Happy。
思路:这题如果使用stringBuilder来做其实难度不大,但是题目的本意不是这样,使用stringBuilder感觉有点增加了空间复杂度,所以我们直接操作字符串(字符数组)
但是如果从前往后替换的话,遍历到第i个字符,需要将i以后的字符都向后移动,这样的时间复杂度是o(n2),显然不好。
从前往后不行的话,我们从后往前遍历,注意防止数组越界,我们需要先对字符数组扩容,扩容就是增加(2count)个空格。
重点:从后向前替换的时候的技巧 例如:“we are lucky”
0 1 2 3 4 5 6 7 8 9 10 11
w e a r e l u c k y
可以得知count=2;(空格的个数)。 所以在替换的时候7-11的字母要向后移动count×2个位置,3~5字母要向后移动(count-1)×2个位置。 所以得到 从后往前遍历(注意起点是为扩容的字符数组的最后一个字符):
1、遇到不是空格,则往后移动(i+2
count个单位);
2、若是空格,则先count–,再填入%20(%就是i+2*count),以为其实位置已经空出来了。
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
w e a r e l u c k y
w e a r e l u c k y

在替换的时候直接在空格处写入%20了
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
w e a r e l u c k y
w e % 2 0 a r e % 2 0 l u c k y
复杂度:时间o(n),空间o(1)

public class ReplaceStr {
     

    public static void main(String[] args) {
     
        StringBuffer str = new StringBuffer("hello world");
        System.out.println(replaceSpace(str));
        //  System.out.println(replaceSpace1(str));
    }

    public static String replaceSpace(StringBuffer str) {
     
        if (str == null || str.length() == 0) {
     
            return "";
        }

        int count = 0, len = str.length();
        //1.计算空格的个数
        for (int i = 0; i < len; i++) {
     
            if (str.charAt(i) == ' ') {
     
                count++;
            }
        }
        //2、扩容字符串,使用count--的话会错,因为count这个变量后面还要使用
        for (int i = 0; i < count; i++) {
     
            str.append(' ');
            str.append(' ');
        }

        //3、开始移位:难点是下面的位置变换
        char[] chars = str.toString().toCharArray();
        for (int i = len - 1; i >= 0; i--) {
     
            if (chars[i] != ' ') {
     
                chars[i + count * 2] = chars[i];
            } else {
     
                count--;
                chars[i + 2 * count] = '%';
                chars[i + 2 * count + 1] = '2';
                chars[i + 2 * count + 2] = '0';
            }
        }
        return String.valueOf(chars);
    }

相关题目:从尾到头遍历的思想的应用

算法刷题1【剑指offer系列之数组】_第1张图片

2、在旋转数组找中最小值

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

思路:可以采用二分法解答这个问题,
mid = low + (high - low)/2
需要考虑三种情况:
(1)array[mid] > array[high]:
出现这种情况说明是将排序数组小的部分旋转到了后面,最小的值肯定再后面部分。比如[3,4,5,6,0,1,2],此时最小数字一定在mid的右边,因此low = mid + 1
(2)array[mid] == array[high]:
出现这种情况是最容易忽略的,不好判断。类似 [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的左边。因为右边必然都是递增的。(相当于将排序数组前面的0个元素都搬到后,注意排序数组本身仍然是旋转数组)因此,high = mid
**注意这里有个坑:**如果待查询的范围最后只剩两个数,那么mid 一定会指向下标靠前的数字
比如 array = [4,6]
array[low] = 4 ;array[mid] = 4 ; array[high] = 6 ;
如果high = mid - 1,就会产生错误, 因此high = mid
但情形(1)中low = mid + 1就不会错误

public int minNumberInRotateArray(int[] array) {
     
        if (array==null||array.length==0){
     
            return 0;
        }
        int len=array.length,low=0,high=len-1,mid=0;
        while (low<=high){
     
            //这里需要注意运算优先级
            mid=low+((high-low)>>1);
            //只和最后一个比,这是和二分查找的区别,相当于target是最后一个数
            if (array[mid]>array[high]){
     
                //case1 [3,4,5,6,0,1,2]
                low=mid+1;
            }else if (array[mid]==array[high]){
     
               //case2  [1,0,1,1,1] 或者[1,1,1,0,1]
                high--;
            }else {
     
                //case3 [2,2,3,4,5,6,6]
                high=mid;//和二分不同
            }
        }
        return array[mid];
    }

排序数组找数字首先想到二分法

算法刷题1【剑指offer系列之数组】_第2张图片

2020.05.20

3、整数数组划分奇偶部分

输入一个整数数组,实现一个函数来调整该数组中数字的顺序,使得所有的奇数位于数组的前半部分,所有的偶数位于数组的后半部分,并保证奇数和奇数,偶数和偶数之间的相对位置不变。(其实书上原题是不要求稳定的,但是牛客网要求稳定性就增加了难度)
思路:这题如果是不要求稳定性的话其实就是荷兰国旗问题,时间是o(n)的,但是现在要求稳定性,所以我们可以使用o(n)的空间来保证稳定性。
(1)先使用荷兰国旗问题来划分,如果是奇数的话不需要处理,因为奇数肯定还是稳定的,但是偶数的话我们就先将这个数放入队列中;
(2)划分完成以后从even的下一位,即even++开始,将队列中的数字覆盖原来的数组,这样就可以保证稳定性了。

public class reOrderArray {
     
    public static void main(String[] args) {
     
        int[]arr={
     1,2,3,4,5,6};
        reOrderArray(arr);
        System.out.println();
    }

    /**
     * 奇数推着偶数走:使用队列保证偶数的稳定性
     * 荷兰国旗问题的思路就是分割数组的模板
     * @param array
     */
    public static void reOrderArray(int [] array) {
     
        ArrayDeque<Integer>queue=new ArrayDeque<>();
        if (array==null||array.length==0){
     
            return;
        }
        int even=-1,cur=0,len=array.length;
        while (cur<len){
     
            if (array[cur]%2!=0){
     
                swap(array,cur++,++even);
            }else {
     
                //装入队列,保证偶数的稳定性,奇数肯定是稳定的
                queue.offer(array[cur]);
                //遇到偶数
                 cur++;
            }
        }
        //将偶数稳定性复原,poll太多次是会空指针异常的
        for (int j=even+1;j<len;j++){
     
            array[j]=queue.poll();
        }
    }

    private static void swap(int[]arr,int i,int j){
     
        int temp=arr[i];
        arr[i]=arr[j];
        arr[j]=temp;
    }
}

4、输出 数组中重复次数大于数组长度一半的数组元素

数组中有一个数字出现的次数超过数组长度的一半,请找出这个数字。例如输入一个长度为9的数组{1,2,3,2,2,2,5,4,2}。由于数字2在数组中出现了5次,超过数组长度的一半,因此输出2。如果不存在则输出0。
这题的话很多种解法,而且都挺多扩展的
(1)使用hashMap实现 时间o(n)空间o(n)
思路很简单,先统计出现的次数,后判断有没有出现的次数大于len/2的,有个技巧可以降低时间,就是将统计和判断放在一个循环里面进行,如果判断出出现的次数超过len/2,那么直接return,就不需要继续了

public class Solution {
     
    public int MoreThanHalfNum_Solution(int [] array) {
     
        if(array==null||array.length==0){
     
            return 0;
        }
        HashMap<Integer,Integer>map=new HashMap<>();
        int len=array.length;
        for(int i=0;i<len;i++){
     
            if(map.containsKey(array[i])){
     
                map.put(array[i],map.get(array[i])+1);
            }else{
     
                map.put(array[i],1);
            }
            //这样就不用再遍历map
            if(map.get(array[i])>len/2){
     //超过,没有=
                return array[i];
            }
        }
        return 0;
    }
}

(2) 使用候选者+生存点数的思路(时间o(n)+空间o(1))
如果有符合条件的数字,则它出现的次数一定比其他所有数字出现的次数和还要多。所以我们遍历数组,遇到和候选者相等则times++,不等times–(相当于是消耗了一次候选者);times=0则需要换候选。
在遍历数组时保存两个值:一是候选者,一个是候选者的生存点数。
遍历下一个数字时,若它与候选者字相同,则生存点数加1,否则减1;若次数为0,则保存下一个数字,并将生存点数置为1。遍历结束后,所保存的数字即为所求
注意还再进行验证,判断它是否符合条件即可。
为什么要验证呢?
首先第一个for循环结束后得到的num是什么?如果这个数组中存在个数大于数组长度一半的数,那么这个num一定是这个数,因为数组中所有不是num的数,一定会被这个数覆盖,所以最后得到的数是num。但是,如果这个数组中根本不存在个数大于数组长度一半的数,那么这个num就是一个不确定的值,这也是为什么找出num之后,还要再做一次循环验证这个数出现的个数是不是大于数组长度一半的原因。

  /**
     * 很好的策略:使用候选+生存点数一次去除两个数
     *
     * @param input
     * @return
     */
    public int MoreThanHalfNum_Solution(int[] input) {
     
        if (input == null || input.length == 0) {
     
            return 0;

        }
        int len = input.length;
        int cur = 0, times = 0;
        for (int i = 0; i < len; i++) {
     
            //1、说明需要重新选候选
            if (times == 0) {
     
                cur = input[i];
                times = 1;
            } else if (cur == input[i]) {
     
                //2、找到相同的,生存+1
                times++;
            } else {
     
                //3、找到不同的,生存-1
                times--;
            }
        }
        //遍历结束:需要判断最后剩下的是不是真的次数超过一半
        times = 0;
        for (int i : input) {
     
            if (i == cur) {
     
                times++;
            }
        }
        return times > len / 2 ? cur : 0;
    }

扩展:找出出现次数大于1/3的数

要求超过1/3肯定最多是两个数,可以使用两个候选;如果发现当前的数不是两个候选,则全部的生存点数-1,相当于同时消除3个不相同的数字;其他情况类似。最后如果存在结果的话,cand1和cand2这两个数就是结果。

public class Find1of3Nums {
     

    public static void main(String[] args) {
     
        int[] arr={
      0 ,1 ,1, 1, 1, 2, 2 ,3 ,3 ,4, 4, 5};
        List<Integer> list = majorityElement(arr);
        PrintUtils.printList(list);
    }
    public  static List<Integer> majorityElement(int[] nums) {
     
        ArrayList<Integer> res = new ArrayList<Integer>();
        if (nums == null || nums.length == 0) {
     
            return res;
        }
        int len=nums.length,cur=0;
        int can1=0,times1=0,can2=0,times2=0;
        while (cur<len){
     
            if (times1==0){
     
                can1=nums[cur];
                times1=1;
            }else if (times2==0){
     
                can2=nums[cur];
                times2=1;
            }else if (nums[cur]==can1){
     
                times1++;
            }else if (nums[cur]==can2){
     
                times2++;
            }else {
     
                //都不是两个候选的话全部-1
                times1--;
                times2--;
            }
            cur++;
        }
        //然后判断是否超多1/3
        times1=0;
        times2=0;
        for (int i=0;i<len;i++){
     
            if (nums[i]==can1){
     
                times1++;
            }else if (nums[i]==can2){
     
                times2++;
            }
        }
        int target=len/3;
        if (times1>target){
     
            res.add(can1);
        }
        if (times2> target){
     
            res.add(can2);
        }
        return res;
    }
}

(3)使用快排的patition函数
其实这题的意思就是找一个数组的中位数,就是找出排序后n/2的位置的数再进行验证。快速排序的 partition() 方法, 会返回一个整数 j 使得 a[lo … j - 1] 小于等于 a[j], 且 a[j + 1 … hi] 大于等于 a[j],此时 a[j] 就是数组的第 j 大元素. 可以利用这个特性找出数组的第 n / 2 个元素.如果patition函数返回的下标>n/2,说明中位数在左边,end=mid-1(和二分查找不同,二分是end=mid)去找,如果下标

 /**
     * 使用快排的patition函数来做:找超过一半的数其实就是找中位数+判断是否是超过一半
     *
     * @param input
     * @return
     */
    public int MoreThanHalfNum_Solution1(int[] input) {
     
        if (input == null || input.length == 0) {
     
            return 0;
        }
        int len = input.length, start = 0, end = len - 1, target = len / 2, index = 0;
        while (start < end) {
     
            index = patition(input, start, end);
            if (index == target) {
     
                break;
            } else if (index < target) {
     
                //说明在右边
                start = index + 1;
            } else {
     
                //说明在左边
                end = index - 1;
            }
        }
        //判断是否是超过一半
        int times = 0;
        for (int i = 0; i < len; i++) {
     
            if (input[i] == input[target]) {
     
                times++;
            }
        }
        return times > target ? input[target] : 0;
    }

    /**
     * patition函数:只返回一个下标
     *
     * @param input
     * @param start
     * @param end
     * @return
     */
    private int patition(int[] input, int start, int end) {
     
        int less = start - 1, more = end, cur = start;
        while (cur < more) {
     
            if (input[cur] > input[end]) {
     
                //大于则more动但cur不动
                swap(input, cur, --more);
            } else if (input[cur] == input[end]) {
     
                //等于则cur动
                cur++;
            } else {
     
                swap(input, cur++, ++less);
            }
        }
        //需要多一个基准归位
        swap(input, more, end);
        return less + 1;
    }

    private void swap(int[] input, int i, int j) {
     
        int temp = input[i];
        input[i] = input[j];
        input[j] = temp;
    }

扩展:找出数组中最小的k个数

输入n个整数,找出其中最小的K个数。例如输入4,5,1,6,2,7,3,8这8个数字,则最小的4个数字是1,2,3,4,。
这题最优解就是使用patition函数,时间是o(n),空间o(1),思路和上一题基本一样,就是使用patition函数按照第k个数(下标是k-1)进行划分。

 /**
     * 最优解  partition的函数很重要
     *
     * @param input
     * @param k
     * @return
     */
    public ArrayList<Integer> GetLeastNumbers_Solution(int[] input, int k) {
     
        ArrayList<Integer> res = new ArrayList<>();
        if (input == null || input.length == 0) {
     
            return res;
        }
        int len = input.length, start = 0, end = len - 1,index;
        //需要进行错误处理
        if (k>len){
     
            return res;
        }
        while (start < end) {
     
            index= patition(input, start, end);
            if (index < k - 1) {
     
                //说明k-1的数在右边,应该按照右边来划分
                start = index + 1;
            } else if (index == k - 1) {
     
                //说明按照k-1的数进行划分了
                break;
            } else {
     
                //说明k-1小的数在左边,应该往左边划分
                end = index - 1;
            }
        }
        for (int i=0;i<k;i++){
     
            res.add(input[i]);
        }
        return res;
    }

    /**
     * patition
     *
     * @param input
     * @param start
     * @param end
     * @return
     */
    private int patition(int[] input, int start, int end) {
     
        int less = start - 1, more = end , cur = start;
        while (cur < more) {
     
            if (input[cur] < input[end]) {
     
                swap(input, cur++, ++less);
            } else if (input[cur] == input[end]) {
     
                cur++;
            } else {
     
                swap(input, cur, --more);
            }
        }
        swap(input,more,end);
        return less + 1;
    }

    private void swap(int[] input, int i, int j) {
     
        int temp = input[i];
        input[i] = input[j];
        input[j] = temp;
    }

本题还有一个不错的解,就是使用大根堆来实现,注意是大根堆,不是小根堆存放小的数,遇到比堆顶小的数则进来,保持堆顶元素是最大的。这样就不需要o(n)的空间,而只是需要o(k)
思路:如果堆没有满,则直接入堆,如果堆满了,就比较当前数和堆顶元素,若当前数小于堆顶(已经是堆的最大了),则弹出堆顶、再入堆,相当于替换了堆顶(但是会调整堆)。遍历完后堆的元素就是前k小的数。

   /**
     * 首先想到的是使用大根堆 o(nlogk)+o(n)
     *
     * @param input
     * @param k
     * @return
     */
    public ArrayList<Integer> GetLeastNumbers_Solution(int[] input, int k) {
     
        ArrayList<Integer> res = new ArrayList<>();
        if (input == null || input.length == 0||k>input.length) {
     
            return res;
        }
        PriorityQueue<Integer> Maxhead = new PriorityQueue<Integer>(new Comparator<Integer>() {
     
            @Override
            public int compare(Integer o1, Integer o2) {
     
                return o2 - o1;
            }
        });
        //入堆
        for (int i=0;i<input.length;i++){
     
            if (Maxhead.size()<k){
     
                Maxhead.offer(input[i]);
            }else {
     
                //如果大于,则需要比较交换
                if (input[i]<Maxhead.peek()){
     
                    Maxhead.poll();
                    Maxhead.offer(input[i]);
                }
            }
        }
      while (!Maxhead.isEmpty()){
     
          res.add(Maxhead.poll());
      }
        return res;

    }

算法刷题1【剑指offer系列之数组】_第3张图片算法刷题1【剑指offer系列之数组】_第4张图片算法刷题1【剑指offer系列之数组】_第5张图片2020.05.21

5、给一个数组,返回它的最大连续子序列的和

HZ偶尔会拿些专业问题来忽悠那些非计算机专业的同学。今天测试组开完会后,他又发话了:在古老的一维模式识别中,常常需要计算连续子向量的最大和,当向量全为正数的时候,问题很好解决。但是,如果向量中包含负数,是否应该包含某个负数,并期望旁边的正数会弥补它呢?例如:{6,-3,-2,7,-15,1,2,2},连续子向量的最大和为8(从第0个开始,到第3个为止)。给一个数组,返回它的最大连续子序列的和,你会不会被他忽悠住?(子向量的长度至少是1)(其实这一题是实用算法ppt上面有的)
思路:这题其实可以用dp来做,但是我dp还不熟,接下来需要整理一个dp专题来。算法刷题1【剑指offer系列之数组】_第6张图片现在的思路:使用记录当前和curSum以及结果res(全局最大),每次遍历判断curSum,如果<=0,说明需要重新开始,因为这是说明i之前的部分是“拖累”的,所以还不如重新开始;否则则加上当前的数,说明i之前的部分还是有用的。同时每次循环需要结算,使用res(初始为Integer.MIN_VALUE)然后记录全局最大。

public class FindGreatestSumOfSubArray {
     
    public static void main(String[] args) {
     
        FindGreatestSumOfSubArray sumOfSubArray=new FindGreatestSumOfSubArray();
       int[] arr={
     1,-2,3,10,-4,7,2,-5};
        int i = sumOfSubArray.FindGreatestSumOfSubArray(arr);
        System.out.println(i);
    }

    public int FindGreatestSumOfSubArray(int[] array) {
     
        if (array==null||array.length==0){
     
            return 0;
        }
        //记录当前和以及结果
        int curNum=0,res=Integer.MIN_VALUE;
        for (int i=0;i<array.length;i++){
     
            if (curNum<=0){
     
                //相当于重开始
                curNum=array[i];
            }else {
     
                curNum+=array[i];
            }
            res=Math.max(res,curNum);
        }
        return res;
    }

}

6、找出数组中数字组成最小的数

输入一个正整数数组,把数组里所有数字拼接起来排成一个数,打印能拼接出的所有数字中最小的一个。例如输入数组{3,32,321},则打印出这三个数字能排成的最小数字为321323。
思路:这题和字符串数组拼接成字符串的字典序最小的题完全一样,核心就是贪心算法(证明很麻烦)。本题思路先将整型数组转换成String数组,然后将String数组排序,最后将排好序的字符串数组拼接出来。关键就是制定排序规则。

  • 排序规则如下:
  • 若ab > ba 则 a > b,
  • 若ab < ba 则 a < b,
  • 若ab = ba 则 a = b;
  • 解释说明:
  • 比如 “3” < "31"但是 “331” > “313”,所以要将二者拼接起来进行比较
  • 为什么不是直接比较两个字符串的大小哪个小就放前面呢?比如”ba“和"b",如果哪个小就哪个放前面的策略的结果是bba,但实际上是bab
    注意:变成字符串再处理也是为了处理大数,防止溢出。
public class PrintMinNumber {
     
    public String PrintMinNumber(int[] numbers) {
     
        if (numbers == null || numbers.length == 0) {
     
            return "";
        }
        //1、先变成字符数组(数组之间的变换只能是循环遍历)
        int len = numbers.length;
        String[] strings = new String[len];
        for (int i = 0; i < len; i++) {
     
            strings[i] = String.valueOf(numbers[i]);
        }
        //2、按照贪心排序
        Arrays.sort(strings, new Comparator<String>() {
     
            @Override
            public int compare(String o1, String o2) {
     
                return (o1 + o2).compareTo(o2 + o1);
            }
        });
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < len; i++) {
     
            sb.append(strings[i]);
        }
        return sb.toString();
    }
}

2020.05.22

6、逆序对问题

在数组中的两个数字,如果前面一个数字大于后面的数字,则这两个数字组成一个逆序对。输入一个数组,求出这个数组中的逆序对的总数P。并将P对1000000007取模的结果输出。 即输出P%1000000007
这题其实就是归并排序的使用,和小和问题基本一样。
思路:逆序对的定义是左边p1比右边p2大的数,在归并过程中,使用下面方法进行“压榨”:右边指针p2的数小于左边p1的数,那么左边从p1~mid分界点的数都是大于右边指针的数(因为进行数组归并的前提就是每个子数组都是有序的,否则就是继续划分),所以这时候逆序对的数量就是(mid-p1+1),循环进行压榨。
最后的结果就是左子数组结果+右子数组结果+归并两个子数组的结果

public class InversePairsCount {
     

    public static void main(String[] args) {
     
        InversePairsCount count=new InversePairsCount();
        int[] arr={
     1,2,3,4,5,6,7,0};
        int i = count.InversePairs(arr);
        System.out.println(i);
    }
    public int InversePairs(int[] array) {
     
        if (array == null || array.length == 0) {
     
            return 0;
        }
        //不能改变原来的数组
        int[] copy = Arrays.copyOf(array, array.length);
        return (int)( megerSort(array, 0, copy.length - 1)%1000000007);
    }

    /**
     * 其实求数组前半部分和后半部分的关系的题都可以往归并排序的上面去想
     *
     * @param array
     * @param left
     * @param right
     * @return
     */
    private long megerSort(int[] array, int left, int right) {
     
        if (left >= right) {
     
            return 0;
        }
        int mid = left + ((right - left) >> 1);
        //左边+右边+两个数组合并
        return megerSort(array, left, mid)+
                megerSort(array, mid + 1, right)+meger(array, left, mid, right);
    }

    /**
     * 关键函数
     *
     * @param array
     * @param left
     * @param mid
     * @param right
     */
    private long meger(int[] array, int left, int mid, int right) {
     
        int[] help = new int[right - left + 1];
        int p1 = left, p2 = mid + 1, index = 0;
        long res=0;
        while (p1 <= mid && p2 <= right) {
     
            //结算需要放在上面,因为经过归并后p1和p2都会变大
            res += array[p1] > array[p2] ? (mid - p1 + 1) : 0;
            help[index++] = array[p1] <= array[p2] ? array[p1++] : array[p2++];
            //结算
        }
        while (p1 <= mid) {
     
            help[index++] = array[p1++];
        }
        while (p2 <= right) {
     
            help[index++] = array[p2++];
        }
        //数组归位
        for (int i = 0; i < help.length; i++) {
     
            array[left + i] = help[i];
        }
        return res;
    }

}

扩展:数组的小和问题

算法刷题1【剑指offer系列之数组】_第7张图片小和的意思是在当前数之前找出比当前数小的数相加的和,不是说找到前面有多少个数比当前数小。其实可以转换为在后面找出比当前数大的个数再乘当前数的值,因为小和的计算其实是重复的。所以小和就是a*n(n是a以后大于a的个数)
算法刷题1【剑指offer系列之数组】_第8张图片算法刷题1【剑指offer系列之数组】_第9张图片和上面的其实就是计算res的这一行代码不同而已

public class SmallSum {
     
    
    public int smallSum(int[] arr){
     
        if (arr==null||arr.length<2){
     
            return 0;
        }
        return  mergerSort(arr,0,arr.length-1);
    }

    private int mergerSort(int[] arr, int l, int r) {
     
        
        if (l==r){
     //只有一个数
            return 0;
        }
        int mid=l+(r-l)>>1;
        //左排好序,右排好序,整体再归并
        //左小和+右小和+整体归并的小和
        return mergerSort(arr,l,mid)+mergerSort(arr,mid+1,r)+
                merge(arr,l,mid,r);
    }


    private int merge(int[] arr, int l, int mid, int r) {
     

        int []help=new int[r-l+1];
        int i=0;
        int p1=l,p2=mid+1;
        //记录小和
        int result=0;
        while (p1<=mid&&p2<=r){
     
            //榨取的关键,右比当前大则加上右边大的个数*当前的数
            result+=arr[p1]<arr[p2]?(r-p2+1)*arr[p1]:0;
            //放入辅助数组
            help[i++]=arr[p1]<=arr[p2]?arr[p1++]:arr[p2++];
        }
        while (p1<=mid){
     
            help[i++]=arr[p1++];
        }
        while (p2<=r){
     
            help[i++]=arr[p2++];
        }
        //复制回原来数组
        for (i=0;i<help.length;i++){
     
            arr[l+i]=help[i];//因为arr下标有可能不是从0开始
        }
        return result;
    }

}

2020.05.23

7、统计一个数字在排序数组中出现的次数。

思路:遍历就可以搞定,但是时间是o(n),显然不是这题考察的地方。正确思路市二分查找的应用,二分找出第一个k的下标(也就是在arr[mid]=k的时候还需要判断左边一个是不是k,如果还是k,说明第一个k还在左边,需要继续high=mid-1,找到最后一个k的下标的思路同理),然后二分出最后一个k的下标,这样时间是o(logn),最优。

public class Test {
     
    public static void main(String[] args) {
     
        Test test = new Test();
        int[] arr = {
     1, 3, 3, 3, 3, 4, 5};
        int i = test.GetNumberOfK(arr, 2);
        System.out.println(i);
    }

    public int GetNumberOfK(int[] array, int k) {
     
        if (array == null || array.length == 0) {
     
            return 0;
        }
        //需要特别处理一下没有存在的元素:否则是0-0+1
        if (getLast(array, k) != -1 && getFirst(array, k) != -1) {
     
            return getLast(array, k) - getFirst(array, k) + 1;
        }else {
     
            return 0;
        }
    }

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

    private int getLast(int[] array, int k) {
     
        int low = 0, high = array.length - 1, mid = 0;
        while (low <= high) {
     
            mid = low + ((high - low) >> 1);
            if (array[mid] < k) {
     
                low = mid + 1;
            } else if (array[mid] == k) {
     
                //需要判断右边
                if (mid + 1 <= high && array[mid + 1] == k) {
     
                    //说明还需去右边找
                    low = mid + 1;
                } else {
     
                    return mid;
                }
            } else {
     
                high = mid - 1;
            }
        }
        return -1;
    }

8、找出数组中出现一次的数字

一个整型数组里除了两个数字之外,其他的数字都出现了两次。请写程序找出这两个只出现一次的数字。
方法1,使用hashmap来记录次数,这是最简单的思路,这样时间o(n)最优,但是空间o(n)
思路:使用位运算中的异或运算
首先:位运算中异或的性质:两个相同数字异或为0,一个数和0异或还是它本身。当一个数只有出现一次时,我们把数组中所有的数,依次异或运算,最后剩下的就是落单的数,因为成对儿出现的都抵消了。这样就是找出数组中缺失的一个数的思路。
依照这个思路,我们来看两个数(我们假设是ab)出现一次的数组。我们首先还是先异或,剩下的数字肯定是ab异或的结果,这个结果的二进制中的1,表现的是A和B的不同的位,假设a的二进制这一位是0,则b的对应位肯定是1,否则按位异或是0。所以我们就取右边第一个1所在的位数进行划分,假设是低位第3位,我们就可以接着把原数组分成两组,分组判断是第3位是否为1。这样,其他的数相同的话肯定分在一个组,因为相同数字所有位都相同。这样,我们就把原来的数组分成两个子数组,且缺失的两个数会分别位于两个子数组,然后把这两个组按照寻找缺失的唯一一个数的思路,依次异或求出结果,就是这两个只出现一次的数字。
所以这题的难点在于
(1)知道如何求出缺失的一个数
(2)找出划分的策略,使得缺失的两个数拆开
时间o(n),空间o(1),就是最优了。

public class FindNumsAppearOnce {
     

    /**
     * num1,num2分别为长度为1的数组。传出参数
     * 将num1[0],num2[0]设置为返回结果
     *
     * @param array
     * @param num1
     * @param num2
     */
    public void FindNumsAppearOnce(int[] array, int num1[], int num2[]) {
     
        if (array == null || array.length == 0) {
     
            return;
        }
        //1、先异或整个数组
        int len = array.length, split = 1, res = 0;
        for (int i = 0; i < len; i++) {
     
            res ^= array[i];
        }
        //2、找出异或结果从右边开始的第一个1
        while ((res & split) == 0) {
     
            split = split << 1;
        }
        //3、分组进行异或
        int res1 = 0, res2 = 0;
        for (int i = 0; i < len; i++) {
     
            if ((array[i] & split) != 0) {
     
                //说明这个数的二进制表示分割位为1
                res1 ^= array[i];
            } else {
     
                res2 ^= array[i];
            }
        }
        num1[0] = res1;
        num2[0] = res2;
    }

扩展1:1~N找出缺少的一个,考虑溢出问题

(1)前提只是缺失一个数,而且没有重复。只需要一直异或就可以,连续的异或也是相互抵消的,不单单是相同成对出现才会抵消,比如123,01–10–11最后结果也是0。在这种情况下,连续异或的结果就是缺失的数字。

 /**
     * 前提是没有重复,且数字是1~n只缺失一个
     *
     * @param arr
     * @return
     */
    public static int process(int[] arr) {
     
        if (arr == null || arr.length == 0) {
     
            return Integer.MIN_VALUE;
        }
        int res = 0;
        for (int n : arr) {
     
            res = res ^ n;
        }
        return res;
    }

但是显然这样的时间是o(n),因为我们没有使用到数组有序这个条件。看到数组有序,我们应该立刻想到二分!!!
分析:找到缺失的数字a,a之前的数字的数值和下标是相等的,a之后是不相等的,也就是a是第一个数值和下标不相等的元素。所以,本题转化为:求有序数组中第一个下标与数值不等的数—显然可以用二分,如果arr[mid]!=mid+1(数组下标从0开始),还需要判断前一个即arr[mid-1]?=mid才能得出mid是不是第一个下标数值不等的元素,是的话返回mid+1(还是需要注意下标)。

  /**
     * 使用二分查找的思想
     *
     * @param arr
     * @return
     */
    public static int process2(int[] arr) {
     
        if (arr == null || arr.length == 0) {
     
            return -1;
        }
        int start = 0, end = arr.length - 1, mid = 0;
        while (start <= end) {
     
            mid = start + ((end - start) >> 1);
            if (arr[mid] == mid + 1) {
     
                //如果中间还是相等,说明不等在右边
                start=mid+1;
            }else {
     
                //还需要判断前一个是否相等
                if (arr[mid-1]==mid){
     
                    return mid+1;
                }else {
     
                    //前一个就不等的话,需要去左边找
                    end=mid-1;
                }
            }
        }
        return -1;
    }

扩展2:无序数组缺失多个数且数组存在的数可能有重复的话,比如{1, 4, 6,5,5},需要找出缺失的数2和3。

其实思路不难,可以先对数组排序再操作,但是对数组排序的话就改变了原来的数组,所以我们不对原来的数组排序,可以使用bitMap,对bitMap置位再取出其实就类似于排序,bitMap还可以用于海量数据排序(别忘了这个用处。)
具体如下:
(1)先遍历一遍数组,找出数组的最大数,这样才能分配bitMap的大小(max/32+1)
(2)再遍历一次数组,进行bitMap的置位操作
(3)从1遍历到max(不是遍历数组),查看bitMap对应的位置是否false,false的话说明这个数没有出现,就找到了结果。
时间0(n),空间o(n/32)

/**
     * 缺失多个且存在的数可能有重复的话
     *
     * @param arr
     * @return
     */
    public static ArrayList<Integer> process1(int[] arr) {
     
        ArrayList<Integer> res = new ArrayList<>();
        if (arr == null || arr.length == 0) {
     
            return res;
        }
        //1、第1次遍历找出数组最大
        int max=Integer.MIN_VALUE;
        for (int i:arr){
     
            max=Math.max(max,i);
        }
        BitSet bitSet = new BitSet((max/ 32) + 1);
        //2、第2次遍历先置位
        for (int i : arr) {
     
            //指定位置set为true
            bitSet.set(i);
        }
        //3、第3次遍历判断是否有值,注意这里的取值不是遍历数组
        for (int i = 1; i <=max; i++) {
     
            //说明存在有数
            if (!bitSet.get(i)) {
     
                res.add(i);
            }
        }
        return res;
    }

扩展3 找出数组中有且只有一个出现一次的数,其余的数都出现3次。算法刷题1【剑指offer系列之数组】_第10张图片

public class FindNumsAppearOnlyOnce {
     
    public static void main(String[] args) {
     
        int[] arr={
     1,1,1,3,3,3,4};
        int i = find(arr);
        System.out.println(i);
    }

    public static int find(int[] arr) {
     
        if (arr == null || arr.length == 0) {
     
            return -1;
        }
        int[] bitSum = new int[32];
        int len = arr.length, bitMask;
        for (int i = 0; i < len; i++) {
     
            bitMask = 1;
            //对数组从右往左进行赋值
            for (int j = 31; j >= 0; j--) {
     
                if ((arr[i] & bitMask) != 0) {
     
                    bitSum[j] += 1;
                }
                bitMask = bitMask << 1;
            }
        }
        //从右往左进行结算(左到右也可以)
        int res=0;
        for (int i=31;i>=0;i--){
     
            //不能被3整除就是因为出现一次的数字的对应位置为1
          if (bitSum[i]%3!=0){
     
              //将二进制的值变成十进制
              res+=Math.pow(2,(31-i));
          }
        }
        return res;
    }
}

算法刷题1【剑指offer系列之数组】_第11张图片2020.05.24

9、求出给定特定数的连续正数序

小明很喜欢数学,有一天他在做数学作业时,要求计算出9~16的和,他马上就写出了正确答案是100。但是他并不满足于此,他在想究竟有多少种连续的正数序列的和为100(至少包括两个数)。没多久,他就得到另一组连续正数和为100的序列:18,19,20,21,22。现在把问题交给你,你能不能也很快的找出所有和为S的连续正数序列? Good Luck!
输出描述:
输出所有和为S的连续正数序列。序列内按照从小至大的顺序,序列间按照开始数字从小到大的顺序
思路:双指针实现窗口,我们根据窗口内值之和来确定窗口的位置和宽度。当low指针追上high指针时,退出循环,因为要求序列至少是2个数字。
(1)若curSum (2)若CurSum=sum,则结算,注意结算以后需要low++
()若curSum>sum,说明太大,需要变小,则low++
因为是连续序列,所以求curSum可以直接使用等差数列的求和公式,移动low和high的时候curSum不需要改变(这里和下面的扩展有不同),而是选择在每次循环的开始重新计算CurSum。

public class FindContinuousSequence {
     
    public ArrayList<ArrayList<Integer>> FindContinuousSequence(int sum) {
     
        ArrayList<ArrayList<Integer>> res = new ArrayList<>();
        if (sum <= 0) {
     
            return res;
        }
        int low=1,high=2,Cursum=0;
        //因为至少是两个数,所以循环条件是low
        while (low<high){
     
            Cursum=((low+high)*(high-low+1))>>1;
            if (Cursum==sum){
     
                ArrayList list=new ArrayList();
                for (int i=low;i<=high;i++){
     
                    list.add(i);
                }
                res.add(list);
                //注意结算完以后慢指针也是需要动的
                low++;
            }else  if (Cursum<sum){
     
                //需要变大
                high++;
            }else {
     
                //需要变小
                low++;
            }
        }
        return res;
    }

}

扩展1:正数数组求累加和等于特定值的最长子数组

给定一个全为正数无序数组和k,找出和为k的最长子数组长度
思路:也是使用窗口,思路和上面基本一样,不同点在于指针变化的时候curSum也需要变化,因为curSum不能计算而得,需要带着前一个的信息。
算法刷题1【剑指offer系列之数组】_第12张图片

public class Test {
     
    public static void main(String[] args) {
     
        int[] arr = {
     3, 1, 1, 1, 1, 6,4,2};
        int maxLength = getMaxLength(arr, 6);
        System.out.println(maxLength);
    }

    public static int getMaxLength(int[] arr, int k) {
     
        if (arr == null || arr.length == 0 || k <= 0) {
     
            return 0;
        }
        int len = arr.length, left = 0, right = 0, curSum = 0, res = 0;
        while (left <= right && right < len) {
     
            if (curSum < k) {
     
                //1、太小则累加:注意数组访问之前都需要进行下标判断
                curSum += arr[right++];
            } else if (curSum == k) {
     
                //2、找到符合条件的,需要结算,因为上面找到curSum以后还进行了right++
                //因此长度直接是right-left
                res = Math.max(res, (right - left));
                //不要忘记也需要改变
                curSum -= arr[left++];
            } else {
     
                //3、太大,需要变小
                curSum -= arr[left++];
            }
        }
        return res;
    }

}

注意:right的结算应该是“先改变再结算”,而不是“先结算再改变”,因为如果结算了发现curSum太大了,应该是只有left改变,right不变;如果在程序中使用了“先结算再改变”的话,其实right也是改变了。因此上面的代码其实是有问题的。
正确代码:

public static int maxSubArrayLen(int[] nums, int s) {
     
        if (nums == null || nums.length == 0 || s <= 0) {
     
            return 0;
        }
        int left = 0, right = 0, len = nums.length, curSum = nums[0], res = 0;
        while (right < len) {
     
            if (curSum < s) {
     
                //小于,则往右走(注意先往右走了再结算下一个)
                right++;
                if (right == len) {
     
                    break;
                }
                curSum += nums[right];
            } else if (curSum == s) {
     
                //等于则结算,同时左指针后移,当前和变小
                res = Math.max(res, (right - left + 1));
                curSum -= nums[left++];
            } else {
     
                //大于的话需要变小,左指针后移
                curSum -= nums[left++];
            }
        }
        return res;
    }

扩展2:数组随意,求累加和等于特定值的最长子数组

思路:因为有正有负有0,所以我们不能通过窗口内的和来进行求解,因为如果curSum太小,需要变大,我们如何移动指针?所以这个思路是行不通的。
求子数组的问题很多都是需要确定以某一位开始或者结束的特征来想,所以我们这题的思路就是找出以某个数结尾的第一次和的下标,有点抽象。举例如下:
算法刷题1【剑指offer系列之数组】_第13张图片算法刷题1【剑指offer系列之数组】_第14张图片注意:这个是使用hashMap处理这类题需要注意的地方,很容易错。为了保证能够获取从0开始的子数组,在开始遍历之前需要先往map中加入(0,-1)表示第一次出现和为0的下标是-1.

//时间空间都是o(N)
public class LongestSumSubArrayLength {
     

    public static int maxLength(int[] arr, int k) {
     
        if (arr == null || arr.length == 0) {
     
            return 0;
        }
        HashMap<Integer, Integer> map = new HashMap<Integer, Integer>();
        //1、注意点:没有遍历之前需要加这个
        map.put(0, -1); // important
        //记录结果
        int res = 0;
        int sum = 0;
        for (int i = 0; i < arr.length; i++) {
     
            //sum没有减少的情况
            sum += arr[i];
            //2、当前是sum,希望找到累加和是sum-k的位置
            if (map.containsKey(sum - k)) {
     
                //记录全局最大
                res = Math.max(i - map.get(sum - k)+1, res);
            }
            //3、如果已经包含了sum的话,map啥都不做,这样map中才是第一次出现的下标
            //没有包含的话map存入当前的sum和下标
            if (!map.containsKey(sum)) {
     
                map.put(sum, i);
            }
        }
        return res;
    }

扩展3、数组随意,求小于等于(巨难)

给定一个数组,值可以为正、负和0,请返回累加和小于等于k的最长子数组长度。–太难了,先不做

10、排序数组中找出和为给定数的两个数

输入一个递增排序的数组和一个数字S,在数组中查找两个数,使得他们的和正好是S,如果有多对数字的和等于S,输出两个数的乘积最小的。
思路:其实这题和上面的扩展2有点类似的思想,我们可以使用HashSet来存数,在找到a的时候判断一下set中有没有(S-a),如果有则直接找到结果,这样时间是o(n),空间o(n)
最优的思路:使用窗口,但是这次的两个指针是从前后进行夹逼,因为需要求的是两个数的和,而不是整个窗口内的值,low++是变大,high–是变小(这实际上是使用了排序数组的特性。如果遇到满足条件的,则可以直接跳出循环,返回结果,因为第一次乘积其实就是最小的(由正方形面积最大可以得出,a+b恒定,则ab的相差越大,乘积越小)。

public ArrayList<Integer> FindNumbersWithSum(int[] array, int sum) {
     
        ArrayList<Integer> res=new ArrayList<>();
        if (array==null||array.length==0){
     
            return res;
        }
        int len=array.length,low=0,high=len-1;
        while (low<high){
     
            if (array[low]+array[high]<sum){
     
                low++;
            }else if (array[low]+array[high]==sum){
     
                res.add(array[low]);
                res.add(array[high]);
                break;
            }else {
     
                high--;
            }
        }
        return res;
    }

2020.05.25

11、扑克牌抽出顺子

LL今天心情特别好,因为他去买了一副扑克牌,发现里面居然有2个大王,2个小王(一副牌原本是54张_)…他随机从中抽出了5张牌,想测测自己的手气,看看能不能抽到顺子,如果抽到的话,他决定去买体育彩票,嘿嘿!!“红心A,黑桃3,小王,大王,方片5”,“Oh My God!”不是顺子…LL不高兴了,他想了想,决定大\小 王可以看成任何数字,并且A看作1,J为11,Q为12,K为13。上面的5张牌就可以变成“1,2,3,4,5”(大小王分别看作2和4),“So Lucky!”。LL决定去买体育彩票啦。 现在,要求你使用这幅牌模拟上面的过程,然后告诉我们LL的运气如何, 如果牌能组成顺子就输出true,否则就输出false。为了方便起见,你可以认为大小王是0。
思路:
(1)先对数组进行排序,因为数字的最大就是13,所以其实我们可以使用类似与hash表的方式来排序,这样的时间就是o(1)
(2)因为0可以是任何数字,所以我们用数字间隔和0的个数比较来判断是否是顺子
需要注意
(1)出现对子肯定不是顺子
(2)统计缺失的时候应该慢指针从zero(0的个数开始,而不是从0开始,因为0肯定是不需要统计的)比如:0 2 3 4 6,其实这是顺子23456,但是如果从0开始计算的话,0-2直接会算到缺失一个数,这样就错了。
算法刷题1【剑指offer系列之数组】_第15张图片在这里插入图片描述算法刷题1【剑指offer系列之数组】_第16张图片

public class IsContinuous {
     

    public static void main(String[] args) {
     
        IsContinuous isContinuous=new IsContinuous();
        int[] arr={
     0,1,3,4,5};
        boolean continuous = isContinuous.isContinuous(arr);
        System.out.println(continuous);
    }

    public boolean isContinuous(int[] numbers) {
     
        if (numbers == null || numbers.length < 5) {
     
            return false;
        }
        //1、对数组排序
        Arrays.sort(numbers);
        //2、找出0的个数
        int zero=0,miss=0,len=numbers.length;
        for (int i=0;i<len;i++){
     
            if (numbers[i]==0){
     
                zero++;
            }
        }
        //3、找出空缺的个数,使用双指针
        //0肯定不需要统计,所以可以从0以后开始
        int  slow=zero,fast=slow+1;
        while (fast<len){
     
            //说明出现对子,不可能是顺子
            if (numbers[slow]==numbers[fast]){
     
                return false;
            }
            //结算空缺:中间有可能确实很多个数,所以不能用相差不等于1就+1来结算
            miss+=(numbers[fast]-(numbers[slow]+1));
            //移动到下一个数
            slow=fast;
            fast++;
        }
        //4、判断空缺的数和0个数
        return zero>=miss;
    }

}

12 找出数组中任意一个重复的数字

在一个长度为n的数组里的所有数字都在0到n-1的范围内。数组中某些数字是重复的,但不知道有几个数字是重复的。也不知道每个数字重复几次。请找出数组中任意一个重复的数字。例如,如果输入长度为7的数组{2,3,1,0,2,5,3},那么对应的输出是第一个重复的数字2。
思路:
(1)排序----这种方法实质上也是找出的最小的重复元素(时间o(nlogn),空间o(1))
(2)Hash表:一个循环,统计的时候判断,但是需要空间o(n)
最优解:数组的长度为 n数字都在 0 到 n-1 的范围内这是特殊性质,我们可以将每次遇到的数进行"归位",具体做法:
(1)如果i!=numbers[i],说明i没有到自己应该到的地方,则和下标为index=numbers[i]的数进行交换,一直循环,直到i=numbers[i],也就是归位了
(2)如果在交换的过程中,当某个数i发现自己的"位置"numbers[i]已经被占用即numbers[i]=number[numbers[i]],则numbers出现重复
算法刷题1【剑指offer系列之数组】_第17张图片

 public static void main(String[] args) {
     
        FindRepeat findRepeat=new FindRepeat();
        int []arr={
     2,3,1,0,2,5,3};
        int[] res=new int[2];
        boolean b = findRepeat.duplicate1(arr, 7, res);
        System.out.println(res[0]);
        System.out.println(b);
    }

    /**
     * 简单方法是使用hashSet
     *
     * @param numbers
     * @param length
     * @param duplication
     * @return
     */
    public boolean duplicate(int numbers[], int length, int[] duplication) {
     
        if (numbers == null || length == 0) {
     
            duplication[0] = -1;
            return false;
        }
        HashSet<Integer> set = new HashSet<>();
        for (int i = 0; i < length; i++) {
     
            if (set.contains(numbers[i])) {
     
                duplication[0] = numbers[i];
                return true;
            } else {
     
                set.add(numbers[i]);
            }
        }
        duplication[0] = -1;
        return false;
    }

    /**
     * 最优解:数组的长度为 n数字都在 0 到 n-1 的范围内,
     * 我们可以将每次遇到的数进行"归位",当某个数发现自己的"位置"被相同的数占了,
     * 则出现重复。
     *
     * @param numbers
     * @param length
     * @param duplication
     * @return
     */
    public boolean duplicate1(int numbers[], int length, int[] duplication) {
     
        if (numbers == null || length == 0) {
     
            duplication[0] = -1;
            return false;
        }
        int index=0;
        for (int i = 0; i < length; i++) {
     
            //如果没有归位,则一直循环,归位了才i++
            while (i != numbers[i]) {
     
                index=numbers[i];
                //交换之前需要判断是否出现重复
                if (numbers[index]==numbers[i]){
     
                    duplication[0]=numbers[i];
                    return true;
                }else {
     
                    swap(numbers,index,i);
                }
            }
        }
        duplication[0] = -1;
        return false;
    }

    private void swap(int[] arr,int i,int j){
     
        int temp=arr[i];
        arr[i]=arr[j];
        arr[j]=temp;
    }

扩展:不修改数组找出重复的数字(二分的应用)

不使用辅助空间,时间是nlogn(但是排序的话就改变了原来的数组,所以也是不能排序),使用辅助空间的话,使用set也可以啊
算法刷题1【剑指offer系列之数组】_第18张图片算法刷题1【剑指offer系列之数组】_第19张图片明天再写

 /**
     * 不改变原来的数组且不能使用辅助空间找出存在重复的数---还没写好
     *
     * @param arr
     * @return
     */
    public int duplicate2(int[] arr) {
     
        if (arr == null || arr.length == 0) {
     
            return -1;
        }
        //长度n+1的数组的数字都是在1~n的范围内,因此初始是1不是0
        int start = 1, end = arr.length - 1, mid = 0, count;
        while (start <= end) {
     
            mid = start + ((end - start) >> 1);
            count = countRange(arr, start, mid);
            //1、说明只有一个数
            if (start == end) {
     
                if (count > 1) {
     
                    //说明是重复的
                    return start;
                } else {
     
                    //只有一个数说明没有找到
                    break;
                }
            }
            if (count > mid - start + 1) {
     
                //重复数在左边
                end = mid;
            } else {
     
                //重复的数在右边
                start = mid+1 ;
            }
        }
        return -1;
    }

    /**
     * 统计大小在start~end范围内的数的个数
     * 注意范围是直接使用下标,而不是数组的值
     *
     * @param arr
     * @param start
     * @return
     */
    private int countRange(int[] arr, int start, int end) {
     
        if (arr == null || arr.length == 0) {
     
            return 0;
        }
        int count = 0;
        for (int i = 0; i <arr.length; i++) {
     
            if (arr[i] >= start && arr[i] <= end) {
     
                count++;
            }
        }
        return count;
    }

2020.05.26

13、给定数组A构建数组B,B[i]=A[0]A[1]…*A[i-1]A[i+1]…*A[n-1]。

给定一个数组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]。不能使用除法。
思路:暴力解的话时间复杂度为O(N2),i前面累乘,i后面累乘,再把两个中间结果累乘
算法刷题1【剑指offer系列之数组】_第20张图片

public class MultiplyArr {
     
    public int[] multiply(int[] A) {
     
        if (A==null|| A.length==0){
     
            return null;
        }
        int len=A.length;
        int[]B=new int[len];
        //1、自上而下计算下三角
        //如果将B的开始元素置为0,则所有结果都是0
        B[0]=1;
        for (int i=1;i<len;i++){
     
            //B[i-1]是复用了上一行的结果
            B[i]=B[i-1]*A[i-1];
        }
        //2、自下而上计算上三角
        int top=1;
        for (int i=len-2;i>=0;i--){
     
           //A[i+1]复用下一行的结果
            top=top*A[i+1];
            //3、从下而上结算结果
            B[i]=B[i]*top;
        }
        return B;
    }
}

2020.05.27

14、找出所有滑动窗口里数值的最大值(数组boss)

给定一个数组和滑动窗口的大小,找出所有滑动窗口里数值的最大值。例如,如果输入数组{2,3,4,2,6,2,5,1}及滑动窗口的大小3,那么一共存在6个滑动窗口,他们的最大值分别为{4,4,6,6,6,5}; 针对数组{2,3,4,2,6,2,5,1}的滑动窗口有以下6个: {[2,3,4],2,6,2,5,1}, {2,[3,4,2],6,2,5,1}, {2,3,[4,2,6],2,5,1}, {2,3,4,[2,6,2],5,1}, {2,3,4,2,[6,2,5],1}, {2,3,4,2,6,[2,5,1]}。
本题是我认为剑指offer中数组最难的一题,实际上我们需要设计一个结构来快速找到窗口中的最大值。
算法刷题1【剑指offer系列之数组】_第21张图片
算法刷题1【剑指offer系列之数组】_第22张图片算法刷题1【剑指offer系列之数组】_第23张图片算法刷题1【剑指offer系列之数组】_第24张图片算法刷题1【剑指offer系列之数组】_第25张图片总结思路:滑动窗口不是队列,实际指的是下标索引的范围,比如,0-2, 1-3 , 2-4,3-5,而双向队列是为了存储最大值的辅助空间。为了得到滑动窗口的最大值,队列可以从两端删除元素,因此使用双端队列。大的原则: 对新来的元素k,将其与双端队列中队尾的元素相比较
(1)窗口的开始下标和队头元素比较,判断队头元素X是否在窗口之内,不在了,直接移出队列
(2)前面比k小的,直接移出队列(因为不再可能成为后面滑动窗口的最大值了!),
(3)判断窗口位置是否合法,合法的话可以进行结算, 队列的第一个元素是滑动窗口中的最大值
具体的思路:(要记住队列放的是下标,不是数)
(1)使用begin来判断窗口的开始位置,begin随遍历i而更新
(2) 新来的元素,需要先判断队列是否为空,若为空,则直接入队
(3)判断窗口begin和队头元素的大小,如果begin大的话说明开始的位置是队头元素的后面,也就是队头元素不在窗口内了
(4)while循环从队尾开始,将队列中比当前元素小的数移除,需要对队列判空,否则会空指针;注意移除完小的元素以后,当前元素需要入队,不要忘了
(5)开始结算队头元素

public ArrayList<Integer> maxInWindows(int[] num, int size) {
     
        ArrayList<Integer> res = new ArrayList<>();
        if (num == null || num.length == 0 || size <= 0) {
     
            return res;
        }
        //队列放的是下标
        ArrayDeque<Integer> queue = new ArrayDeque<>();
        /*
        begin是用于保存当前窗口的第一个值在原始数组中的【下标】,
        这样写的目的是为了少写一些判断边界条件的代码。
         */
        int len = num.length, begin = 0;
        for (int i = 0; i < len; i++) {
     
            begin = i - size + 1;
            //1、队列为空,直接下标入队
            if (queue.isEmpty()) {
     
                queue.offer(i);
            }
            //2、判断开始的下标大于队头,则队头要出队--不能放在前面,会空指针
            if (begin > queue.peekFirst()) {
     
                queue.pollFirst();
            }
            //3、将队列中全部小于当前元素的数都出队,因为小的元素不可能再是max
            while (!queue.isEmpty() && num[queue.peekLast()] <= num[i]) {
     
                queue.pollLast();
            }
            //小的都出队以后再入队
            queue.offer(i);
            //4、开始的位置大于0才能结算--结算肯定是放在最后才结算的
            if (begin >= 0) {
     
                res.add(num[queue.peekFirst()]);
            }
        }
        return res;

    }

扩展:队列的最大值

定义一个队列并且实现add、remove和max函数,时间复杂度都为O(1)
(其实还是挺难的)
add函数:数据队列直接入队,但是max队的话需要按照上一题的逻辑进行判断,为了方便,队列存的结点是数据+下标,新来一个元素count++,对应的结点的index也随之变化
remove函数(我觉得最难),显然数据队也是可以直接删除,难点是max队列是否出队的判断,如果数据队要出队的下标和max队头下标一样,说明max队头存的就是数据队的这个结点, 也就是要删除的这个结点是当前最大的,数据队删除以后,则max队头也要出队, 否则数据队已经没有这个大的元素了,但是max还有,显然错误。

public class MaxInQueue {
     

    /**
     * 队列结点
     */
    private class QueueData {
     
        int index;
        int data;

        QueueData(int index, int data) {
     
            this.index = index;
            this.data = data;
        }
    }

    private Deque<QueueData> max;
    private Queue<QueueData> queue;
    private int count;

    public MaxInQueue() {
     
        max = new ArrayDeque<>();
        queue = new ArrayDeque<>();
        count = 0;
    }

    /**
     * 添加元素:count是用来标记加入的结点的"时间戳",方便删除
     *
     * @param data
     */
    public void add(Integer data) {
     

        while (!max.isEmpty() && max.peekLast().data < data) {
     
            max.pollLast();
        }
        QueueData queueData = new QueueData(count, data);
        count++;
        //加入max
        max.offer(queueData);
        //数据队直接入队
        queue.offer(queueData);
    }

    /**
     * 移除元素
     *
     * @return
     */
    public int remove() {
     
        if (queue.isEmpty()) {
     
            throw new IllegalArgumentException("Empty Queue!");
        }
        /*
        如果数据队要出队的下标和max队头下标一样,说明max队存的就是数据队的这个结点,
        也就是要删除的这个结点是当前最大的,数据队删除以后,则max队也要出队,
        否则数据队已经没有这个大的元素了,但是max还有,显然错误
         */
        if (queue.peek().index == max.peekFirst().index) {
     
            max.pollFirst();
        }
        return queue.poll().data;
    }

    /**
     * 获取max,就是max队的队头
     *
     * @return
     */
    public int max() {
     
        if (queue.isEmpty()) {
     
            throw new IllegalArgumentException("Empty Queue!");
        }
        return max.peekFirst().data;
    }

    /**
     * 测试用例
     */
    public static void main(String[] args) {
     
        int[] queue = {
     2, 3, 4, 2, 6, 2, 5, 1};
        MaxInQueue maxInQueue = new MaxInQueue();
        maxInQueue.add(5);
        maxInQueue.add(2);
        maxInQueue.add(3);
        System.out.println(maxInQueue.max());
        int remove = maxInQueue.remove();
        System.out.println("删除第一个"+remove);
        System.out.println(maxInQueue.max());
        int remove1 = maxInQueue.remove();
        System.out.println("删除第2个"+remove1);
        System.out.println(maxInQueue.max());


    }
}

你可能感兴趣的:(算法)