4 剑指Offer53-在排序数组中查找数字
统计一个数字在排序数组中出现的次数。
示例 1:
输入: nums = [5,7,7,8,8,10], target = 8 输出: 2
示例 2:
输入: nums = [5,7,7,8,8,10], target = 6 输出: 0
限制:
0 <= 数组长度 <= 50000
排序数组中的搜索问题,首先想到 二分法 解决。
使用遍历数组再 count++ 的话,时间复杂度是 O(n)
但是使用二分法,使时间复杂度降低到 O(log n)
自己第一次提交时的代码:
class Solution { public int search(int[] nums, int target) { int count = 0; for (int i = 0; i < nums.length; i++) { if (nums[i] == target) { count++; } } return count; } }
使用非常粗暴的循环求个数,时间非常慢。看题解知道:
排序数组中的搜索问题,首先想到 二分法 解决。
二分查找法(时间复杂度:O(log n)):
二分查找要求线性表必须顺序存储,并且元素是有序排列的
二分查找(折半查找)的基本思想:减少查找序列的长度,分而治之地进行关键字的查找。他的查找过程是:先确定待查找记录的所在的范围,然后逐渐缩小查找的范围,直至找到该记录为止(也可能查找失败)。
int binarySerch(vector<int> & nums,int low,int hi,int target) { int len = nums.size(); if(low<0 || low > len-1 || hi < 0 || hi > len-1 || 0 == len) { return -1; } while(low <= hi) //注意这里一定要加上= { int mid = low + (hi -low)/2; if(target == nums[mid]) return mid; else if(target > nums[mid]) low = mid+1; else hi = mid-1; } return -1; }
Notes:
线性表与非线性表的区别:
(1) 非线性表:二维数组、多维数组、广义表、树、图
(2) 线性表:
① 顺序存储结构(顺序表):数组、队列、栈
② 链式存储结构(链表):链表
顺序存储结构和链式存储结构的区别:
(1) 链式存储结构的内存地址不一定连续,但顺序存储结构的内存地址一定连续
(2)链式存储适用于在较频繁的插入、删除、更新元素时,而顺序存储结构适用于频繁查询时使用
顺序存储结构和链式存储结构的优缺点:
(1)空间上:顺序比链式节约空间。因为链式结构每一个节点都有一个指针存储域。
(2)存储操作上:顺序支持随机存取,方便操作
(3)插入和删除上:链式比顺序方便。因为顺序表的插入要执行更大的空间复杂度,包括一个从表头索引以及索引后的元素后移。
总结:
查询频繁 + 存储量固定 = 顺序结构
插入删除频繁 + 存储量不固定 = 链式结构
二分法解法1:
将已排序的数组记左右边界的索引为 left / right。本题要求统计数字 target 的出现次数,可以转化为:使用二分法找出有序数组中出现该 target 的左边界和右边界,可以得出 target 的数量为 right - left - 1
算法解析:
初始化:左边界 i = 0,右边界 j = nums.length - 1。
循环二分:
- 计算中点: mid = i + (j - i) / 2; ----> 防止数值溢出
- 若 nums[mid] < target,则 target 在闭区间 [mid + 1, right]中,执行 i = m + 1;
- 若 nums[mid] > target,则 target 在闭区间 [left, mid - 1]中,执行 j = m - 1;
- 若 nums[mid] = target,则 left(左边界) 在闭区间 [i, mid - 1] 中,right(右边界) 在闭区间 [mid + 1, j] 中。需要分一下两种情况:
- 若查找 left,执行 i = m - 1,当 nums[mid] != target 时跳出,此时 j 指向 left;
- 若查找 right,执行 j = m + 1,当 nums[mid] != target 时跳出,此时 i 指向 right;
返回值:找出 left 和 right 之后,最终返回 right - left - 1
代码
class Solution { public int search(int[] nums, int target) { int i = 0; int j = nums.length - 1; while (i <= j) { int mid = i + (j - i) / 2; if (nums[mid] <= target) { i = mid + 1; } else if (nums[mid] > target) { j = mid - 1; } } int right = i; if (j >= 0 && nums[j] != target) { return 0; } i = 0; j = nums.length - 1; while(i <= j) { int m = (i + j) / 2; if(nums[m] < target) { i = m + 1; } else { j = m - 1; } } int left = j; return right - left - 1; } }
提交结果
效率优化:
看上面代码可以看出运用了两次二分法,故可以将二分法封装成一个方法进行调用,此时应该考虑的就是左边界和右边界的查找怎么通过一个循环来找出。
此时有两种方法:
① 将二分查找右边界 right 的代码封装起来,找左边界的时候只需找数组中比 target 小且最邻近的数字的右边界,最后将两结果相减并返回即可
② 将二分查找左边界 left 的代码封装起来,找右边界的时候只需找数组中比 target 大且最邻近的数字的左边界,最后将两结果相减并返回即可
代码:
class Solution { private int rightLoc(int[] nums, int tar) { int i = 0; int j = nums.length - 1; while(i <= j) { int m = i + (j - i) / 2; if(nums[m] <= tar) { i = m + 1; } else { j = m - 1; } } return i; } public int search(int[] nums, int target) { return rightLoc(nums, target) - rightLoc(nums, target - 1); } }
本质上看, helper() 函数旨在查找数字 tar 在数组 nums 中的 插入点 ,且若数组中存在值相同的元素,则插入到这些元素的右边。
提交结果:
二分法解法2:
因为数组是已经排序好的数组,故如果直接找出来 nums[mid] = target,则通过左右边界点的值和 target 做判断,当:
左边界点 < target 时,左边界点右移
右边界点 > target 时,右边界点左移
移动左右边界点,直到左右边界点对应的值相等,最后返回 right - left + 1 即可。
代码:
class Solution { public int search(int[] nums, int target) { int low = 0; int high = nums.length - 1; while (low <= high) { int mid = low + (high - low) / 2; if(nums[mid] < target) { low = mid + 1; } else if (nums[mid] > target) { high = mid - 1; } else { if (nums[low] == nums[high]) { return high - low + 1; } if (nums[low] < target) { low++; } if (nums[high] > target) { high--; } } } return 0; } }
提交结果: