[2021校招必看之Java版《剑指offer》-37] 数字在排序数组中出现的次数

1、题目描述

  【JZ37】统计一个数字在排序数组中出现的次数。
  知识点:数组,查找算法
  难度:☆

2、解题思路

  本题其实就是考察查找算法,常用的查找算法有:暴力遍历、二分查找、插值查找和斐波那契查找。
  设数组为array,目标数字为k

2.1 暴力遍历

  因为数组是有序的,所以我只需要一个个去遍历即可,比较简单。需要注意的是,遍历找到相同的数字时,因为是有序数组,后面可能跟着若干个一样的数字,因此,统计好相同数字的个数后要结束算法,后面再遍历下去没有意义。

2.2 二分查找

  二分查找的思路就是不断缩减待查询的数组长度。假设数组长度为10,那么,一开始的左边界就是0,右边界是9。中间索引为(0+9)/2,为4,若array[4]<k,说明k在数组的右半截,我们就把右半部分当成新数组,就不去考虑左边的部分,节省了一半的计算量。
  以此类推,不断缩减数组范围,直到array[4]==k,就找到了。如果缩减到数组长度为0都找不到,则不存在目标数字。

2.3 插值查找

  插值查找是在二分查找的基础上优化的,与二分查找的不同之处在于中间值的计算方式:
在这里插入图片描述
  相比较于二分查找,插值查找能够更快速接近目标值。

2.4 斐波那契查找

  斐波那契查找和上面两个查找的不同之处也在于mid值的计算,斐波那契数列的特点是,第一个元素和第二个元素都为1,以后每个元素都等于前两个元素之和,比如:{1,1,2,3,5,8,13,21,...}
  那如何根据斐波那契数列来计算mid呢?
  假设array的长度为10,那么,我们要把中间某个位置设为mid,然后分为mid前部分和mid后部分。
  首先把array长度扩充为最接近的斐波那契数列元素,现在长度为10,斐波那契数列中,比它大且最接近的,只能是13。array的索引是0到9,那么,拓展的10-12的值从索引9的值复制。
  根据斐波那契数列的特点,13=8+5,所以把array分成前8个和后5个两部分,而这个mid就是前部分的最后一个元素(或者后部分的第一个元素),即所谓的黄金分割点。
  找到mid后,其他计算和前面的二分和插值是一样的。
在这里插入图片描述
  因为索引是从0开始的,所以长度为F[k]的数组,起索引范围为0到F[k]-1。
  由图可知,mid = low + F[k-1]-1。
  也就是说,mid的取值,和两个值有关,low和F[k-1],low也就是左边界,F[k-1]为F[k]的前一个值。
  在计算mid之前,我们需要把要用到的斐波那契数列计算出来。
  为了方便起见,我们生成的斐波那契数列元素个数和array元素个数一致即可。

3、解题代码

3.1 暴力遍历

package pers.klb.jzoffer.medium;

/**
 * @program: JzOffer2021
 * @description: 数字在排序树组中出现的次数
 * @author: Meumax
 * @create: 2020-07-04 20:06
 **/
public class NumberOfK {

    public int GetNumberOfK(int[] array, int k) {
        int count = 0;
        for (int i = 0; i < array.length; i++) {
            if (array[i] == k) {
                int j = i;
                while (j < array.length && array[j] == k) {
                    count++;
                    j++;
                }
                break;
            }
        }
        return count;
    }
}

  时间复杂度:O(N)
  空间复杂度:O(1)

3.2 二分查找

package pers.klb.jzoffer.medium;

/**
 * @program: JzOffer2021
 * @description: 数字在排序树组中出现的次数
 * @author: Meumax
 * @create: 2020-07-04 20:06
 **/
public class NumberOfK {

    // 二分查找
    public int GetNumberOfK(int[] array, int k) {
        int count = 0;  // 重复次数
        int leftIndex = 0;  // 左索引
        int rightIndex = array.length - 1;  // 右索引
        int mid;
        int index = -1;  // 目标数字在array中的其中一个位置

        // 找到目标数字所在的其中一个位置
        while (leftIndex <= rightIndex) {
            mid = midOf(leftIndex, rightIndex);
            if (array[mid] < k) {
                leftIndex = mid + 1;
            } else if (k < array[mid]) {
                rightIndex = mid - 1;
            } else {    // k == array[mid]
                index = mid;
                break;
            }
        }

        // 找到其中一个数字后,往前和往后统计出现了多少次
        if (index != -1) {

            count = 1;

            // 往前统计
            for (int i = index - 1; i >= 0; i--) {
                if (array[i] == k) {
                    count++;
                }
            }

            // 往后统计
            for (int j = index + 1; j < array.length; j++) {
                if (array[j] == k) {
                    count++;
                }
            }
        }

        return count;
    }

    private int midOf(int left, int right) {
        return (left + right) / 2;
    }
}

  时间复杂度:O(logN)
  空间复杂度:O(1)

3.3 插值查找

package pers.klb.jzoffer.medium;

/**
 * @program: JzOffer2021
 * @description: 数字在排序树组中出现的次数
 * @author: Meumax
 * @create: 2020-07-04 20:06
 **/
public class NumberOfK {

    // 插值查找
    public int GetNumberOfK(int[] array, int k) {
        int count = 0;  // 重复次数
        int leftIndex = 0;  // 左索引
        int rightIndex = array.length - 1;  // 右索引
        int mid;
        int index = -1;  // 目标数字在array中的其中一个位置

        // 找到目标数字所在的其中一个位置
        while (leftIndex <= rightIndex) {
            mid = midOf(array, leftIndex, rightIndex, k);
            if (array[mid] < k) {
                leftIndex = mid + 1;
            } else if (k < array[mid]) {
                rightIndex = mid - 1;
            } else {    // k == array[mid]
                index = mid;
                break;
            }
        }

        // 找到其中一个数字后,往前和往后统计出现了多少次
        if (index != -1) {

            count = 1;

            // 往前统计
            for (int i = index - 1; i >= 0; i--) {
                if (array[i] == k) {
                    count++;
                }
            }

            // 往后统计
            for (int j = index + 1; j < array.length; j++) {
                if (array[j] == k) {
                    count++;
                }
            }
        }

        return count;
    }

    // 插值查找的中间值计算
    private int midOf(int[] arr, int left, int right, int key) {
        if (arr[left] == arr[right]) {
            return left;
        } else {
            return left + ((key - arr[left]) / (arr[right] - arr[left])) * (right - left);
        }
    }
}

  时间复杂度:O(loglogN)
  空间复杂度:O(1)

3.4 斐波那契查找

package pers.klb.jzoffer.medium;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

/**
 * @program: JzOffer2021
 * @description: 数字在排序树组中出现的次数
 * @author: Meumax
 * @create: 2020-07-04 20:06
 **/
public class NumberOfK {

    // 插值查找
    public int GetNumberOfK(int[] array, int k) {

        int count = 0;  // 重复次数

        if (array.length == 1) {
            if (array[0] == k) count = 1;

        } else {

            int low = 0;  // 左索引
            int high = array.length - 1;  // 右索引
            int mid;
            int index = -1;  // 目标数字在array中的其中一个位置
            List<Integer> f = fib(array.length);    // 斐波那契数组
            int s = f.size() - 1;    // s为待搜索数组长度所对应的斐波那契数组的数的下标
            int[] arr1 = Arrays.copyOf(array, f.get(s));  // 新建一个数组,长度为斐波那契数组第k个索引对应的数

            // 如果初始待搜索数组长度不是对应斐波那契数组某个数,那就填充到对应上
            for (int i = array.length; i < s; i++) {
                arr1[i] = array[array.length - 1];
            }

            // 找到目标数字所在的其中一个位置
            while (low <= high) {
                mid = midOf(low, f.get(s - 1));    // 斐波那契查找法计算出的中间值
                if (arr1[mid] < k) {
                    s = s - 2;
                    low = mid + 1;
                } else if (k < arr1[mid]) {
                    s = s - 1;
                    high = mid - 1;
                } else {    // k == arr1[mid]
                    // arr1是从arr拷贝过来的,后面是重复值,所以要确定返回哪一个下标
                    if (mid <= high) {
                        index = mid;
                    } else {
                        index = high;
                    }
                    break;
                }
            }

            // 找到其中一个数字后,往前和往后统计出现了多少次
            if (index != -1) {

                count = 1;

                // 往前统计
                for (int i = index - 1; i >= 0; i--) {
                    if (array[i] == k) {
                        count++;
                    }
                }

                // 往后统计
                for (int j = index + 1; j < array.length; j++) {
                    if (array[j] == k) {
                        count++;
                    }
                }
            }
        }
        return count;
    }

    // 斐波那契查找的中间值计算
    private int midOf(int low, int f) {
        return low + f - 1;
    }

    // 生成斐波那契数组
    private List<Integer> fib(int arrLength) {
        List<Integer> f = new ArrayList<Integer>();
        f.add(0, 1);

        for (int i = 1; f.get(f.size() - 1) < arrLength; i++) {
            if (i == 1) {
                f.add(i, 1);
            } else {
                f.add(i, f.get(i - 1) + f.get(i - 2));
            }
        }

        return f;
    }
}

4、解题心得

  暴力遍历是每个人都能想得到的,如果在笔试的时候,使用二分查找既可以快速求解,难度也不会太高。如果是面试,能实现插值查找就最好了,斐波那契查找能说出原理也是OK的。但是不建议在笔试的时候用斐波那契查找,第一是比较复杂,第二点性能相比插值查找相差不远。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值