文章目录
1. 剑指 Offer 03. 数组中重复的数字
思路1:先排序,再遍历,排序之后如nums[i]==nums[i+1]则找到一个重复数字;
class Solution {
public int findRepeatNumber(int[] nums) {
Arrays.sort(nums);
for(int i=0;i<nums.length-1;i++)
{
if(nums[i]==nums[i+1])
return nums[i];
}
return -1;
}
}
思路2:使用set集合,添加失败时就返回一个重复数字;
class Solution {
public int findRepeatNumber(int[] nums) {
HashSet<Integer> set=new HashSet<>();
for(int num:nums)
{
if(!set.add(num))
return num;
}
return -1;
}
}
//O(n)
//O(n)
思路3:上面两种做法都没有完全利用题目中所给的条件,nums数组中的数字范围是0-n-1,和数组的下标范围一致,因此可以遍历数组,使得数字x放到索引x的位置上,如果存在重复的x,那么在第二次遇见x时就发现相应索引x的位置上面已有数字,直接返回重复数字x
class Solution {
public int findRepeatNumber(int[] nums) {
int i=0;
while(i<nums.length)
{
if(nums[i]==i)//i位置放置数字i 符合条件
{
i++;
continue;
}
// 当前元素x是nums[i] 所以x应该放在位置x上 但是如果x==nums[x](nums[i]==nums[nums[i]]) 说明位置x上面已经放置过x了 也就是说x重复 则返回结果
if(nums[i]==nums[nums[i]])
return nums[i];
int tmp=nums[i];// i位置和nums[i]位置对应数字交换
//交换之后nums[i]==i
nums[i]=nums[tmp];
nums[tmp]=tmp;
}
return -1;
}
}
//O(n)
//O(1)
2. 剑指 Offer 53 - I. 在排序数组中查找数字 I
思路1: 遍历一遍数组,使用一个计数器,时间复杂度是O(n) 这里不再赘述
思路2:由于数组是有序的,因此考虑使用二分查找算法来解决这个问题,统计次数,换个角度,只要知道目标数字的开始出现位置以及最后出现位置就可以知道该数字出现的次数,因此可以使用带左边界的二分算法和带右边界的二分算法来实现
个数=右边界-左边界+1
class Solution {
public int search(int[] nums, int target) {
int start=binary_search_leftBound(nums,target);
int end=binary_search_rightBound(nums,target);
if(start==-1||end==-1)//返回-1说明这个元素不存在
return 0;
return end-start+1;
}
//寻找左边界
public int binary_search_leftBound(int nums[],int target) {
if(nums.length==0)
return -1;
int left=0,right=nums.length;// [left,right) 左闭右开
while(left<right)
{
int mid=left+(right-left)/2;
if(nums[mid]==target)
right=mid;//找到了也不返回 缩小右边界
else if(nums[mid]>target)
right=mid;//[left,mid)<==>[left,mid-1]
else if(nums[mid]<target)
left=mid+1;//[mid+1,right)
}
if(left==nums.length)//结束条件为left==right right可能等于nums.length
return -1;
return nums[left]==target?left:-1;
}
//寻找右边界
public int binary_search_rightBound(int nums[],int target) {
if(nums.length==0)
return -1;
int left=0,right=nums.length;// [left,right) 左闭右开
while(left<right)
{
int mid=left+(right-left)/2;
if(nums[mid]==target)
left=mid+1;
else if(nums[mid]>target)
right=mid;
else if(nums[mid]<target)
left=mid+1;
}
//结束条件left==right right可能等于0 等于0是开区间 取不到0
if(left==0)
return -1;
return nums[left-1]==target?left-1:-1;
}
}
//O(logn)
//O(1)
3. 剑指 Offer 53 - II. 0~n-1中缺失的数字
思路1: 用等差数列公式求出0+1+2+…+n=sum 再遍历数组求出数组元素之和sum 缺失的数字就是sum-sum1 时间复杂度O(n)
class Solution {
public int missingNumber(int[] nums) {
int n=nums.length;
//第1项0 最后一项是n 总项数n+1
int sum1=(0+n)*(n+1)/2;
int sum2=0;
for(int num:nums)
{
sum2+=num;
}
return sum1-sum2;
}
}
//O(n)
//O(1)
思路2:递增排序,有序—>二分, 根据nums[i]==i?这个标准我们将数组分为左子数组(nums[i]==i)和右子数组(nums[i]!=i),因为只要某个位置上nums[i]不等于索引i,i及以后的位置数字与索引都对不上号。
ex: [0,1,2,3,4,5,6,7,9] 缺失的是8 [0:7]区间下标都等于元素 但是下标8的位置元素却是9 因此返回缺失数字(也即对不上的下标)
class Solution {
public int missingNumber(int[] nums) {
int left=0,right=nums.length-1;
while(left<=right)
{
int mid=left+(right-left)/2;
if(nums[mid]==mid)//右子数组的首位元素位于[mid+1,right]中 [left:mid]都是一一对应的
left=mid+1;
else //不对应 说明
right=mid-1; //左子数组的末位元素位于[left,mid-1]中 因为mid这个位置已经不对应了
//所以一一对应的左子数组的末位元素一定在区间[left,mid-1]中
//跳出时,变量 i 和 j分别指向 “右子数组的首位元素” 和 “左子数组的末位元素” 。因此返回 i(i是下标)即可。
}
return left;//left指向右子数组的第一个元素 也就是缺失的数字
}
}
//O(logn)
//O(1)
4. 剑指 Offer 04. 二维数组中的查找
从数组的右上角往左下角看,数组元素的分布就像是一棵二叉搜索树,因此可以将target元素与当前节点比较,相等则返回,大于当前节点就再和当前节点的右子节点比较,小于当前节点就再和当前节点的左子节点比较
class Solution {
public boolean findNumberIn2DArray(int[][] matrix, int target) {
int m=matrix.length;//行数
if(m==0)
return false;
int n=matrix[0].length;//列数
int i=0,j=n-1;//从右上角元素(根节点)开始查找
while(i<m&&j>=0)
{
if(matrix[i][j]==target)
return true;
else if(matrix[i][j]>target)
j--;//查找左子节点
else if(matrix[i][j]<target)
i++;//查找右子节点
}
return false;
}
}
//O(m+n) 最多循环遍历n+m次 也就是找左下角的元素
//O(1)
5. 剑指 Offer 11. 旋转数组的最小数字
思路1:线性查找,由于数组原始是升序的,如果发生了旋转,那么最小值前面的原始比其大,也就是说只要发现nums[i]>nums[i+1],就知道nums[i+1]是最小值;如果未发生旋转,或者数组只有一个元素,或者数组发生了旋转但是元素都是一样的,这些情况只需要返回数组的第一个元素即可
class Solution {
public int minArray(int[] numbers) {
for(int i=0;i<numbers.length-1;i++)
{
//前面元素大于后面,说明后面的元素就是最小值
if(numbers[i]>numbers[i+1])
return numbers[i+1];
}
return numbers[0];//考虑未旋转或数组只有一个元素的情况
}
}
//O(N)
//O(1)
思路2:二分查找,mid=(i+j)/2, 比较nums[mid]和nums[j],如果nums[mid]>nums[j],说明nums[mid]位于左有序数组中(比如[3,4,5,1,2]中的[3,4,5]),如果nums[mid]<nums[j], nums[mid]位于右有序数组中 比如([3,4,5,1,2]中的[1,2]),如果nums[mid]==nums[j],不能判断nums[mid]位于哪个有序数组中
注意:不能根据nums[mid]和nums[i]的大小来比较 本质上是因为j一定在右排序数组中,而i不一定 旋转点之前的元素属于左排序数组 旋转点以及之后的元素属于右排序数组 左排序数组中的元素>右排序数组中的元素 特别的,当没有旋转时,只有右排序数组
反例: i=0 j=4 mid=2 nums[mid]>nums[i]
[1,2,3,4,5] 旋转点x=0(没有旋转) i j 都属于右排序数组 mid在右排序数组
[3,4,5,1,2] 旋转点x=3 i属于左排序数组 j属于右排序数组 mid在左排序数组
class Solution {
public int minArray(int[] numbers) {
int i=0,j=numbers.length-1;
//i<j如果写成i==j也可以,但是没有必要
//当i==j时,mid=(i+j)/2=j nums[mid]=nums[j] j--
//多一步来跳出循环
while(i<j)
{
int mid=i+(j-i)/2;
//nums[mid]位于左子有序数组
//最小值位于区间[mid+1,j]
//nums[mid]本身不可能是最小值
if(numbers[mid]>numbers[j])
i=mid+1;
//nums[mid]位于右子有序数组
//最小值位于区间[i,mid]
//nums[mid]本身可能是最小值
else if(numbers[mid]<numbers[j])
j=mid;
//相等的情况不能直接判断nums[mid]位于哪个区间
//只能减小j来缩小范围,又可以理解为去重
else if(numbers[mid]==numbers[j])
j--;
}
return numbers[i];
}
}
//O(logn)
//O(1)
6. 剑指 Offer 50. 第一个只出现一次的字符
二次遍历,第一次遍历记录各个字符出现的次数,第二次遍历找出第一个只出现一次的字符,记录次数方法可以用HashMap,也可以用自定义的数组
class Solution {
public char firstUniqChar(String s) {
int count[]=new int[26];
for(int i=0;i<s.length();i++)
{
count[s.charAt(i)-'a']++;
}
for(int i=0;i<s.length();i++)
{
if(count[s.charAt(i)-'a']==1)
return s.charAt(i);
}
return ' ';//没有返回单个空格
}
}
//O(n)
//O(1) 数组大小固定为26