查找本质就是排除的过程,不外乎顺序查找、二分查找、哈希查找、二叉排序树查找、DFS/BFS查找
一、p39-JZ3 找出数组中重复的数字(利用特性)
方法1:全部排序再进行逐个扫描找重复。 时间复杂度n*logn 空间复杂度1
cpp
class Solution {
public:
int duplicate(vector<int>& nums) {
int n = nums.size();
if (n == 0) return -1;
sort(nums.begin(), nums.end());
for (int i = 1; i < n; ++i)
if (nums[i] == nums[i - 1]) return nums[i];
return -1;
}
};
方法2:构造哈希------找到第一个重复的数就返回,否则正常插入 时间复杂度n 空间复杂度n
cpp
class Solution {
public:
int duplicate(vector<int>& nums) {
int n = nums.size();
if (n == 0) return -1;
unordered_set<int> s;
for (int i = 0; i < n; ++i)
if (s.count(nums[i])) return nums[i];
else s.insert(nums[i]);
return -1;
}
};
那么我们能否避免n的空间复杂度呢??
方法3:边排序边比较------我们会发现数组中的数字都在0-n-1的范围内,如果这个数组中没有重复的数字,那么当这个数组排序之后数字i将出现在下标为i的位置,同时数组中有些位置可能没有数据。 因此我们可以重拍这个数组,当扫描到下标为i的数字m的时候,首先看看m和i是否相同,如果不相同则拿他和第m个数字进行比较,如果相等说明找到了重复数字,如果不相等则把第i个数和第m个数进行交换,把m放在属于他的位置,接下来再重复这个比较、交换的过程,直到我们发现了一个重复的数字。 此时我们会发现每个数字至多比较2次就可以找到自己的位置,所以时间复杂度是n 空间复杂度1
cpp
class Solution {
public:
int duplicate(vector<int>& nums) {
//重排 边排序边比较
int n = nums.size();
if (n == 0)return -1;
for (int i = 0; i < n; ++i) {
while (nums[i] != i)
if (nums[i] == nums[nums[i]]) return nums[i];
else swap(nums[i], nums[nums[i]]);
}
return -1;
}
};
二、扩展p41-JZ3 不修改原数组基础上找出重复数(二分)
这题和上一题相似,但是区别1:不可修改原数组 。区别2:该题中1-n只有n个数,但是数组中包含超过n个数,所以根据鸽巢原理,一定至少有一个数字是重复的!!
我们把1-n的数字从中间的数字m分成两部分(概数一定出现在左半部分或者右半部分),前半部分为1->m 后面一半为m->n-1,如果1-m的数字超过了m,那么这一半钟一定包含了重复的数字,否则另一半m+1->n-1的区间里一定包含重复的数字,这样我们可以继续把包含重复数字的区间一分为2,直到找到一个重复的数字。
按照上面的二分查找的思路,那么countrange会给调用logn次,而每次需要n的时间,所以时间复杂度为n*logn,空间复杂度1 相比于直接用哈希的做法而言,等同于用时间换空间!!但是要注意的是这个算法并不能保证找出所有重复的数字!!也不能确定该数究竟出现了几次 (所以一定要问清面试官的要求,看是要找出任意一个 还是找出所有,或者是对时间性能有什么要求,比如是空间还是时间优先)
这里使用左端点区间二分法
cpp
class Solution {
public:
int countrange(vector<int>& nums, int begin, int end) {
int count = 0;
int n = nums.size();
for (int i = 0; i < n; ++i)
if (nums[i] >= begin && nums[i] <= end)
++count;
return count;
}
int findDuplicate(vector<int>& nums) {
// 不修改原数组的基础上找到重复的数字
int n = nums.size();
if (n == 0)
return -1;
int left = 1, right = n - 1;
while (left < right) {
int mid = left + (right - left) / 2;
int count = countrange(nums, left, mid);
if (count > (mid - left + 1))
right = mid; // 说明区间一定在左边
else
left = mid + 1;
}
// 此时left==right了 我们去看看该数的数目
if (countrange(nums, left, right) > 1)
return left;
return -1;
}
};
三、p44-JZ4 二维数组中的查找某数(杨氏矩阵)(利用特性)
要利用他每一行和每一列都递增的特性!!先从右上角或者左下角进行比较(比如右上角,如果比右上角小,此时就可以排除列,比右上角大就可以排除行,所以无论如何都可以排除一行或者一列!!)。这样的时间复杂度就是O(m+n)
cpp
class Solution {
public:
bool Find(int target, vector<vector<int> >& array) {
int i = 0, j = array[0].size() - 1;
while (i < array.size() && j >= 0)
if (target < array[i][j]) --j;
else if (target > array[i][j]) ++i;
else return true;
return false;
}
};
四、p82-JZ11 旋转数组的最小数字(二分)
我们要注意旋转之后的数组实际上可以划分为两个排序的子数组,而前面的元素普遍大于等于后面子数组的元素!最小的元素恰好是这两个子数组的分界线!
假如该题是严格升序的,那么图应该如下所示,此时我们会发现他具有二段性(根据某个规律可以将数组一分为二,然后再舍去其中一部分!!)
此时我们要找的是右边区间的左端点,所以要用左端点区间二分法,此时当中间的数比左边大的时候,说明该数字在左区间,左区间要跳过去,当中间的数比右边小的时候,说明该数字在右区间,此时要右区间要跟过来。
但是有两种特殊情况:1、如果恰好旋转了n次,此时相当于原数组没有变化,那么此时直接返回第一个就可以了!!2、因为该题不是严格增,而是非递减,所以存在相等的情况,比如{1,0,1,1,1} 很有可能出现nums[mid]==left==right 此时我们就无法进行二分了!!这个时候我们只能按照笨方法进行一个个遍历了!!
cpp
class Solution {
public:
int minNumberInRotateArray(vector<int>& nums) {
int left = 0, right = nums.size() - 1;
while (left < right) {
if (nums[left] < nums[right]) return nums[left];//情况1
int mid = left + (right - left) / 2;
if (nums[mid] > nums[left]) left = mid + 1;
else if (nums[mid] < nums[right]) right = mid;
else ++left;//此时说明nums[left]==nums[right]==nums[mid] 情况2
}
return nums[left];
}
};
五、p263-JZ53 排序数组中查找数字(二分)
如果我们直接用朴素二分找到一个3,那么我们并不确定左边有多少右边有多少,所以我们需要尝试用左端点区间和右端点区间法找到这个数字的范围。 即找到第1个k和最后1个k。
cpp
class Solution {
public:
int GetNumberOfK(vector<int>& nums, int k) {
//只要找到第一个k和最后一个k 就可以统计出k在数组中出现的次数
int n=nums.size();
if(n==0) return 0;
//第一个k要用左端点区间法
int left=0,right=n-1;
while(left<right){
int mid=left+(right-left)/2;
if(nums[mid]<k) left=mid+1;
else right=mid; //最终会落在区间的左端点
}
if(nums[left]!=k) return 0;//找不到就返回
//第二个k要用右端点区间法
int begin=0,end=n-1;
while(begin<end){
int mid=begin+(end-begin+1)/2;
if(nums[mid]<=k) begin=mid;//最终会落在区间的右端点
else end=mid-1;
}
return end-right+1;
}
};
六、扩展p266-JZ53 0~n-1中缺失的数字(二分)
这题有一个只管的做法就是直接用等差求和公式n(n-1)/2求出数字0~n-1所有数字之和,然后减掉整个数组的数字,就可以得到缺失的数字了,但是这种做法需要n的时间复杂度
我们会发现该题具有二段性,即1、缺席位置之前的数和下标是一样的 2、缺席位置之后的数和下标是不一致的。所以此时我们就可以通过下标和数是否一致来进行二分查找!!
因为我们要找的是不符合要求的第一个,所以要用左端点区间法
cpp
class Solution {
public:
int takeAttendance(vector<int>& nums) {
int n=nums.size();
int left=0,right=n-1;
while(left<right){
int mid=left+(right-left)/2;
if(nums[mid]==mid) left=mid+1;//说明mid在左区间 要跳跃
else right=mid;
}
//此时right指向的就是第一个缺席的同学的位置
//但是极端情况如果缺席的正好是最后一个同学
if(right==nums[right]) return right+1;
return right;
}
};
七、p197-JZ38 字符串的排列(DFS)
我们可以把他拆分成小问题,根据实例2我们知道有重复的数字,所以我们可以先排序一下让相同的字母挨着一块,当前面的数跟自己相等且没有选的时候,那么自己也不能选。
cpp
class Solution {
public:
vector<string> ret;
string path;
bool check[10] = {0};
void dfs(string s) {
if (path.size() == s.size()) {
ret.emplace_back(path);
return;
}
for (int i = 0; i < s.size(); ++i) {
if(check[i]||i>0&&s[i-1]==s[i]&&!check[i-1]) continue;//但是前面的数没选
path.push_back(s[i]);
check[i] = true;
dfs(s);
path.pop_back();
check[i] = false;
}
}
vector<string> Permutation(string str) {
if (str.empty()) return {};
//要去重 所以排序一下
sort(str.begin(), str.end());//保证相同的放在一起
dfs(str);
return ret;
}
};
八、P89-JZ12 矩阵中的路径(DFS)
用标记数组和向量数组
cpp
class Solution {
public:
bool check[21][21] = {0}; //标记数组
int dx[4] = {0, 0, -1, 1};
int dy[4] = {1, -1, 0, 0}; //向量数组
int m,n; //长度和宽度
bool hasPath(vector<vector<char>>& nums, string word) {
m = nums.size();
if (m == 0) return false;
n = nums[0].size();
for (int i = 0; i < m; ++i)
for (int j = 0; j < n; ++j)
if (nums[i][j] == word[0])
if (dfs(nums, word, i, j, 1)) return true;
return false;
}
bool dfs(vector<vector<char>>& nums, string& word, int i, int j, int pos) {
if (pos == word.size()) return true;
check[i][j] = true;
for (int k = 0; k < 4; ++k) {
int x = dx[k] + i, y = dy[k] + j;
if (x >= 0 && x < m && y >= 0 && y < n && !check[x][y] &&
word[pos] == nums[x][y])
if (dfs(nums, word, x, y, pos + 1)) return true;
}
check[i][j] = false; //找错了就回溯
return false;
}
};
九、p92-JZ13 机器人的运动范围(DFS/BFS)
思路1:DFS
cpp
class Solution {
public:
int dx[4]={-1,1,0,0};
int dy[4]={0,0,1,-1};
int vis[101][101]={0};//标记数组
int ret=0;//统计格子数
int movingCount(int threshold, int rows, int cols) {
if(threshold==0) return 1;
dfs(threshold,rows,cols,0,0);
return ret;
}
void dfs(int threshold, int m, int n,int i,int j){
if(i==m||j==n) return;
vis[i][j]=true;
++ret;
for(int k=0;k<4;++k){
int x=dx[k]+i,y=dy[k]+j;
if(x>=0&&x<m&&y>=0&&y<n&&!vis[x][y]&&check(threshold,x,y))
dfs(threshold,m,n,x,y);
}
}
bool check(int threshold,int x,int y){
int sum=0;
while(x){
sum+=x%10;
x/=10;
}
if(sum>threshold) return false;
while(y){
sum+=y%10;
y/=10;
}
return sum<=threshold;
}
};
思路2:BFS
cpp
class Solution {
public:
int dx[4] = {-1, 1, 0, 0};
int dy[4] = {0, 0, 1, -1};
int vis[101][101] = {0}; //标记数组
int movingCount(int threshold, int rows, int cols) {
if (threshold == 0) return 1;
int ret = 1; //统计格子数
queue<pair<int, int>> q;
q.push({0, 0});
vis[0][0] = true;
while (!q.empty()) {
auto&[i, j] = q.front();
q.pop();
for (int k = 0; k < 4; ++k) {
int x = dx[k] + i, y = dy[k] + j;
if (x >= 0 && x < rows && y >= 0 && y < cols && !vis[x][y] &&
check(threshold, x, y)) {
q.push({x, y});
++ret;
vis[x][y] = true;
}
}
}
return ret;
}
bool check(int threshold, int x, int y) {
int sum = 0;
while (x) {
sum += x % 10;
x /= 10;
}
if (sum > threshold) return false;
while (y) {
sum += y % 10;
y /= 10;
}
return sum <= threshold;
}
};